feat: add frontend manual extraction flow with review screen (refs #136)

- Create useManualExtraction hook: submit PDF to OCR, poll job status, track progress
- Create useCreateSchedulesFromExtraction hook: batch create maintenance schedules from extraction
- Create MaintenanceScheduleReviewScreen: dialog with checkboxes, inline editing, batch create
- Update DocumentForm: remove "(Coming soon)", trigger extraction after upload, show progress
- Add 12 unit tests for review screen (rendering, selection, empty state, errors)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-11 10:48:46 -06:00
parent a281cea9c5
commit 40df5e5b58
5 changed files with 863 additions and 6 deletions

View File

@@ -4,7 +4,7 @@ import { UpgradeRequiredDialog } from '../../../shared-minimal/components/Upgrad
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { Checkbox, FormControlLabel } from '@mui/material';
import { Checkbox, FormControlLabel, LinearProgress } from '@mui/material';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import dayjs from 'dayjs';
import { useCreateDocument, useUpdateDocument, useAddSharedVehicle, useRemoveVehicleFromDocument } from '../hooks/useDocuments';
@@ -13,6 +13,8 @@ import type { DocumentType, DocumentRecord } from '../types/documents.types';
import { useVehicles } from '../../vehicles/hooks/useVehicles';
import type { Vehicle } from '../../vehicles/types/vehicles.types';
import { useTierAccess } from '../../../core/hooks/useTierAccess';
import { useManualExtraction } from '../hooks/useManualExtraction';
import { MaintenanceScheduleReviewScreen } from '../../maintenance/components/MaintenanceScheduleReviewScreen';
interface DocumentFormProps {
mode?: 'create' | 'edit';
@@ -95,6 +97,31 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({
const removeSharedVehicle = useRemoveVehicleFromDocument();
const { hasAccess } = useTierAccess();
const canScanMaintenance = hasAccess('document.scanMaintenanceSchedule');
const extraction = useManualExtraction();
const [reviewDialogOpen, setReviewDialogOpen] = React.useState(false);
// Open review dialog when extraction completes
React.useEffect(() => {
if (extraction.status === 'completed' && extraction.result) {
setReviewDialogOpen(true);
}
}, [extraction.status, extraction.result]);
const isExtracting = extraction.status === 'pending' || extraction.status === 'processing';
const handleReviewClose = () => {
setReviewDialogOpen(false);
extraction.reset();
resetForm();
onSuccess?.();
};
const handleSchedulesCreated = (_count: number) => {
setReviewDialogOpen(false);
extraction.reset();
resetForm();
onSuccess?.();
};
const resetForm = () => {
setTitle('');
@@ -234,6 +261,18 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({
setError(uploadErr?.message || 'Failed to upload file');
return;
}
// Trigger manual extraction if scan checkbox was checked
if (scanForMaintenance && documentType === 'manual' && file.type === 'application/pdf') {
try {
await extraction.submit(file, vehicleID);
// Don't call onSuccess yet - wait for extraction and review
return;
} catch (extractionErr: any) {
setError(extractionErr?.message || 'Failed to start maintenance extraction');
return;
}
}
}
resetForm();
@@ -538,8 +577,8 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({
<LockOutlinedIcon fontSize="small" />
</button>
)}
{canScanMaintenance && (
<span className="ml-1 text-xs text-slate-500 dark:text-titanio">(Coming soon)</span>
{canScanMaintenance && scanForMaintenance && (
<span className="ml-1 text-xs text-slate-500 dark:text-titanio">PDF will be scanned after upload</span>
)}
</div>
)}
@@ -569,6 +608,34 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({
<div className="text-sm text-slate-600 dark:text-titanio mt-1">Uploading... {uploadProgress}%</div>
)}
</div>
{isExtracting && (
<div className="md:col-span-2 mt-2">
<div className="flex items-center gap-3 p-3 rounded-lg border border-primary-200 bg-primary-50 dark:border-abudhabi/30 dark:bg-scuro">
<div className="flex-1">
<div className="text-sm font-medium text-slate-700 dark:text-avus mb-1">
Scanning manual for maintenance schedules...
</div>
<LinearProgress
variant={extraction.progress > 0 ? 'determinate' : 'indeterminate'}
value={extraction.progress}
sx={{ borderRadius: 1 }}
/>
<div className="text-xs text-slate-500 dark:text-titanio mt-1">
{extraction.progress > 0 ? `${extraction.progress}% complete` : 'Starting extraction...'}
</div>
</div>
</div>
</div>
)}
{extraction.status === 'failed' && extraction.error && (
<div className="md:col-span-2 mt-2">
<div className="text-red-600 dark:text-red-400 text-sm p-3 rounded-lg border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20">
Extraction failed: {extraction.error}
</div>
</div>
)}
</div>
{error && (
@@ -576,10 +643,10 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({
)}
<div className="flex flex-col sm:flex-row gap-2 mt-4">
<Button type="submit" className="min-h-[44px]">
{mode === 'edit' ? 'Save Changes' : 'Create Document'}
<Button type="submit" className="min-h-[44px]" disabled={isExtracting}>
{isExtracting ? 'Scanning...' : mode === 'edit' ? 'Save Changes' : 'Create Document'}
</Button>
<Button type="button" variant="secondary" onClick={onCancel} className="min-h-[44px]">Cancel</Button>
<Button type="button" variant="secondary" onClick={onCancel} className="min-h-[44px]" disabled={isExtracting}>Cancel</Button>
</div>
<UpgradeRequiredDialog
@@ -587,6 +654,16 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({
open={upgradeDialogOpen}
onClose={() => setUpgradeDialogOpen(false)}
/>
{extraction.result && (
<MaintenanceScheduleReviewScreen
open={reviewDialogOpen}
items={extraction.result.maintenanceSchedules}
vehicleId={vehicleID}
onClose={handleReviewClose}
onCreated={handleSchedulesCreated}
/>
)}
</form>
</LocalizationProvider>
);