import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { ThumbsUp, ThumbsDown, X, RotateCcw, ImageOff } from 'lucide-react'; import { api } from '../api/client'; import { FAILURE_REASON_KEYS } from './EditArchiveModal'; import { verdictSourceKey } from '../utils/verdictSource'; import { PrintModal } from './PrintModal'; import { useToast } from '../contexts/ToastContext'; interface ConfirmOutcomeDialogProps { archiveId: number; onClose: () => void; } /** * Post-print outcome confirmation (#1898), Formlabs-style: finish photo plus * Good / Reject. Opens from the print_confirm_request WebSocket event (via * Layout), from the pending badge on an archive card, and from the * ?confirm= deep link a push notification carries. Rejecting offers an * optional reason and a one-click reprint through the regular PrintModal. */ export function ConfirmOutcomeDialog({ archiveId, onClose }: ConfirmOutcomeDialogProps) { const { t } = useTranslation(); const { showToast } = useToast(); const queryClient = useQueryClient(); const [rejecting, setRejecting] = useState(false); const [rejectReason, setRejectReason] = useState(''); const [showReprint, setShowReprint] = useState(false); const { data: archive } = useQuery({ queryKey: ['archive', archiveId], queryFn: () => api.getArchive(archiveId), }); const verdictMutation = useMutation({ mutationFn: (data: { user_verdict: 'good' | 'reject'; failure_reason?: string }) => api.updateArchive(archiveId, { ...data, user_verdict_source: 'dialog' }), onSuccess: (_updated, variables) => { queryClient.invalidateQueries({ queryKey: ['archives'] }); queryClient.invalidateQueries({ queryKey: ['archive', archiveId] }); showToast( variables.user_verdict === 'good' ? t('confirmOutcome.savedGood') : t('confirmOutcome.savedReject'), 'success', ); if (variables.user_verdict === 'good') { onClose(); } }, onError: () => showToast(t('confirmOutcome.saveFailed'), 'error'), }); const name = archive?.print_name || archive?.filename || ''; // The finish photo is prepended to archive.photos at print end (#1397); // fall back to the thumbnail when no camera shot exists. const photoFilename = archive?.photos?.[0] ?? null; const photoUrl = photoFilename ? api.getArchivePhotoUrl(archiveId, photoFilename) : null; const alreadyDecided = archive?.user_verdict != null; // Why this print already has a verdict — the plate-clear default answers // prompts on its own, and without this the answer looks like it appeared // from nowhere (#1898). const sourceKey = verdictSourceKey(archive?.user_verdict_source); const handleGood = () => verdictMutation.mutate({ user_verdict: 'good' }); const handleRejectSave = (reprint: boolean) => { verdictMutation.mutate( { user_verdict: 'reject', ...(rejectReason ? { failure_reason: rejectReason } : {}) }, { onSuccess: () => { if (reprint) { setShowReprint(true); } else { onClose(); } }, }, ); }; if (showReprint && archive) { return ( ); } return (
{/* Header */}

{t('confirmOutcome.title')}

{/* Photo */}
{photoUrl ? ( {name} ) : (
{t('confirmOutcome.noPhoto')}
)}

{name}

{alreadyDecided ? ( <>

{archive?.user_verdict === 'good' ? t('confirmOutcome.alreadyGood') : t('confirmOutcome.alreadyRejected')}

{sourceKey && (

{t(sourceKey)}

)} ) : !rejecting ? (
) : (
)} {!alreadyDecided && !rejecting && ( )}
); }