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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user