mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 18:20:57 +00:00
215 lines
6.6 KiB
TypeScript
215 lines
6.6 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Button,
|
|
Divider,
|
|
FileInput,
|
|
Group,
|
|
Modal,
|
|
NumberInput,
|
|
Select,
|
|
Switch,
|
|
Textarea,
|
|
} from '@mantine/core';
|
|
import { Upload } from 'lucide-react';
|
|
|
|
import { useToast } from '@/hooks/use-toast';
|
|
import { useCreateInspectionReport, useUploadInspectionAttachments } from '@/hooks/useWarehouses';
|
|
import {
|
|
INSPECTION_REPORT_TYPES,
|
|
INSPECTION_STATUSES,
|
|
type InspectionReportType,
|
|
type InspectionResultStatus,
|
|
} from '@/types/warehouse';
|
|
import { extractErrorMessage } from './options';
|
|
|
|
interface InspectionReportModalProps {
|
|
opened: boolean;
|
|
onClose: () => void;
|
|
inventoryId: string | null;
|
|
}
|
|
|
|
const REPORT_TYPE_LABELS: Record<InspectionReportType, string> = {
|
|
INSPECTION: 'Inspection',
|
|
DAMAGE: 'Damage',
|
|
WEIGHT_LOSS: 'Weight loss',
|
|
MISSING_ITEM: 'Missing item',
|
|
GENERAL: 'General',
|
|
};
|
|
|
|
const STATUS_LABELS: Record<InspectionResultStatus, string> = {
|
|
PASSED: 'Passed',
|
|
FAILED: 'Failed',
|
|
NEEDS_REVIEW: 'Needs review',
|
|
};
|
|
|
|
/** Batch 4.5 — record an inspection / damage report with optional image upload. */
|
|
export function InspectionReportModal({ opened, onClose, inventoryId }: InspectionReportModalProps) {
|
|
const { toast } = useToast();
|
|
const createReport = useCreateInspectionReport();
|
|
const uploadAttachments = useUploadInspectionAttachments();
|
|
|
|
const [reportType, setReportType] = useState<InspectionReportType>('INSPECTION');
|
|
const [inspectionStatus, setInspectionStatus] = useState<InspectionResultStatus>('PASSED');
|
|
const [hasDamage, setHasDamage] = useState(false);
|
|
const [damageDescription, setDamageDescription] = useState('');
|
|
const [hasWeightLoss, setHasWeightLoss] = useState(false);
|
|
const [expectedWeight, setExpectedWeight] = useState<number | ''>('');
|
|
const [actualWeight, setActualWeight] = useState<number | ''>('');
|
|
const [hasMissingItems, setHasMissingItems] = useState(false);
|
|
const [missingItemsDescription, setMissingItemsDescription] = useState('');
|
|
const [remarks, setRemarks] = useState('');
|
|
const [files, setFiles] = useState<File[]>([]);
|
|
|
|
const submitting = createReport.isPending || uploadAttachments.isPending;
|
|
|
|
const reset = () => {
|
|
setReportType('INSPECTION');
|
|
setInspectionStatus('PASSED');
|
|
setHasDamage(false);
|
|
setDamageDescription('');
|
|
setHasWeightLoss(false);
|
|
setExpectedWeight('');
|
|
setActualWeight('');
|
|
setHasMissingItems(false);
|
|
setMissingItemsDescription('');
|
|
setRemarks('');
|
|
setFiles([]);
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!inventoryId) return;
|
|
try {
|
|
const report = await createReport.mutateAsync({
|
|
inventoryId,
|
|
payload: {
|
|
reportType,
|
|
inspectionStatus,
|
|
hasDamage,
|
|
damageDescription: damageDescription.trim() || undefined,
|
|
hasWeightLoss,
|
|
expectedWeight: expectedWeight === '' ? undefined : Number(expectedWeight),
|
|
actualWeight: actualWeight === '' ? undefined : Number(actualWeight),
|
|
hasMissingItems,
|
|
missingItemsDescription: missingItemsDescription.trim() || undefined,
|
|
remarks: remarks.trim() || undefined,
|
|
},
|
|
});
|
|
|
|
if (files.length > 0) {
|
|
await uploadAttachments.mutateAsync({ reportId: report.id, files });
|
|
}
|
|
|
|
toast({ title: 'Inspection report saved' });
|
|
reset();
|
|
onClose();
|
|
} catch (error) {
|
|
toast({ variant: 'destructive', title: 'Save failed', description: extractErrorMessage(error) });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal opened={opened} onClose={onClose} title="Inspection / Report" centered size="lg">
|
|
<Group grow>
|
|
<Select
|
|
label="Report type"
|
|
data={INSPECTION_REPORT_TYPES.map((t) => ({ value: t, label: REPORT_TYPE_LABELS[t] }))}
|
|
value={reportType}
|
|
onChange={(v) => setReportType((v as InspectionReportType) ?? 'INSPECTION')}
|
|
allowDeselect={false}
|
|
/>
|
|
<Select
|
|
label="Inspection status"
|
|
data={INSPECTION_STATUSES.map((s) => ({ value: s, label: STATUS_LABELS[s] }))}
|
|
value={inspectionStatus}
|
|
onChange={(v) => setInspectionStatus((v as InspectionResultStatus) ?? 'PASSED')}
|
|
allowDeselect={false}
|
|
/>
|
|
</Group>
|
|
|
|
<Divider my="md" label="Damage" labelPosition="left" />
|
|
<Switch
|
|
label="Has damage"
|
|
checked={hasDamage}
|
|
onChange={(e) => setHasDamage(e.currentTarget.checked)}
|
|
color="orange"
|
|
/>
|
|
{hasDamage && (
|
|
<Textarea
|
|
mt="xs"
|
|
label="Damage description"
|
|
value={damageDescription}
|
|
onChange={(e) => setDamageDescription(e.currentTarget.value)}
|
|
/>
|
|
)}
|
|
|
|
<Divider my="md" label="Weight loss" labelPosition="left" />
|
|
<Switch
|
|
label="Has weight loss"
|
|
checked={hasWeightLoss}
|
|
onChange={(e) => setHasWeightLoss(e.currentTarget.checked)}
|
|
color="orange"
|
|
/>
|
|
{hasWeightLoss && (
|
|
<Group grow mt="xs">
|
|
<NumberInput
|
|
label="Expected weight (kg)"
|
|
min={0}
|
|
value={expectedWeight}
|
|
onChange={(v) => setExpectedWeight(v === '' ? '' : Number(v))}
|
|
/>
|
|
<NumberInput
|
|
label="Actual weight (kg)"
|
|
min={0}
|
|
value={actualWeight}
|
|
onChange={(v) => setActualWeight(v === '' ? '' : Number(v))}
|
|
/>
|
|
</Group>
|
|
)}
|
|
|
|
<Divider my="md" label="Missing items" labelPosition="left" />
|
|
<Switch
|
|
label="Has missing items"
|
|
checked={hasMissingItems}
|
|
onChange={(e) => setHasMissingItems(e.currentTarget.checked)}
|
|
color="orange"
|
|
/>
|
|
{hasMissingItems && (
|
|
<Textarea
|
|
mt="xs"
|
|
label="Missing items description"
|
|
value={missingItemsDescription}
|
|
onChange={(e) => setMissingItemsDescription(e.currentTarget.value)}
|
|
/>
|
|
)}
|
|
|
|
<Divider my="md" />
|
|
<Textarea
|
|
label="Remarks"
|
|
value={remarks}
|
|
onChange={(e) => setRemarks(e.currentTarget.value)}
|
|
/>
|
|
|
|
<FileInput
|
|
mt="md"
|
|
label="Images / documents"
|
|
placeholder="Upload jpg, png or pdf"
|
|
accept="image/jpeg,image/png,application/pdf"
|
|
leftSection={<Upload size={16} />}
|
|
multiple
|
|
value={files}
|
|
onChange={setFiles}
|
|
clearable
|
|
/>
|
|
|
|
<Group justify="flex-end" mt="lg">
|
|
<Button variant="default" onClick={onClose} disabled={submitting}>
|
|
Cancel
|
|
</Button>
|
|
<Button color="green" onClick={handleSubmit} loading={submitting} disabled={!inventoryId}>
|
|
Save report
|
|
</Button>
|
|
</Group>
|
|
</Modal>
|
|
);
|
|
}
|