import { useState, useEffect, useCallback } from 'react'; import { useParams, useNavigate, useBlocker } from 'react-router-dom'; import { useQuery, useMutation } from '@tanstack/react-query'; import { Box, Typography, Card, CardContent, Grid, Button, Chip, Divider, TextField, Dialog, DialogTitle, DialogContent, DialogActions, CircularProgress, Alert, Tooltip, Link, Skeleton, } from '@mui/material'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import SaveIcon from '@mui/icons-material/Save'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import NavigateNextIcon from '@mui/icons-material/NavigateNext'; import SendIcon from '@mui/icons-material/Send'; import RefreshIcon from '@mui/icons-material/Refresh'; import HistoryIcon from '@mui/icons-material/History'; import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; import UndoIcon from '@mui/icons-material/Undo'; import { toast } from 'react-toastify'; import { proposalsApi, type ProposalDetail } from '../../../lib/api/proposals'; import { lineItemsApi, type LineItem } from '../../../lib/api/lineItems'; import { adminApi } from '../../../lib/api/admin'; import { queryClient } from '../../../lib/queryClient'; import { QUERY_KEYS } from '../../../constants/queryKeys'; import LineItemEditor, { type EditableLineItem } from '../../../components/admin/LineItemEditor'; import VendorDataPanel from '../../../components/admin/VendorDataPanel'; import SimilarProposalsPanel from '../../../components/admin/SimilarProposalsPanel'; import { STATUS_CHIP_STYLES } from '../../../constants'; export default function AdminWorkspace() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [lineItems, setLineItems] = useState([]); const [refinedScope, setRefinedScope] = useState(''); const [approveDialogOpen, setApproveDialogOpen] = useState(false); const [sendDialogOpen, setSendDialogOpen] = useState(false); const [reviseDialogOpen, setReviseDialogOpen] = useState(false); // Fix: WEB-M11 — add dialog state for Return to Review action const [returnToReviewDialogOpen, setReturnToReviewDialogOpen] = useState(false); const [dirty, setDirty] = useState(false); // Fix: WEB-M7 — capture error state for failed fetch const { data: proposal, isLoading, isError: isProposalError, error: proposalError, refetch: refetchProposal } = useQuery({ queryKey: [QUERY_KEYS.proposals, id], queryFn: () => proposalsApi.getById(id!), enabled: !!id, }); // Fix: WEB-M9 — capture loading state for line items skeleton const { data: fetchedLineItems, isLoading: lineItemsLoading } = useQuery({ queryKey: [QUERY_KEYS.proposalLineItems, id], queryFn: () => lineItemsApi.getAll(id!), enabled: !!id, }); const { data: similarProposals } = useQuery({ queryKey: ['similarProposals', id], queryFn: () => adminApi.getSimilar(id!), enabled: !!id, }); const hasSimilar = similarProposals && similarProposals.length > 0; useEffect(() => { if (fetchedLineItems) { setLineItems( fetchedLineItems.map((li) => ({ id: li.id, description: li.description, quantity: li.quantity, unit: li.unit, unitPrice: li.unitPrice, totalPrice: li.totalPrice, pricingMode: li.pricingMode, sortOrder: li.sortOrder, source: li.source, })) ); } }, [fetchedLineItems]); useEffect(() => { if (proposal) { setRefinedScope(proposal.refinedScope || proposal.scopeOfWork); } }, [proposal]); useEffect(() => { if (!dirty) return; const handler = (e: BeforeUnloadEvent) => { e.preventDefault(); }; window.addEventListener('beforeunload', handler); return () => window.removeEventListener('beforeunload', handler); }, [dirty]); const blocker = useBlocker( useCallback( ({ currentLocation, nextLocation }: { currentLocation: { pathname: string }; nextLocation: { pathname: string } }) => dirty && currentLocation.pathname !== nextLocation.pathname, [dirty], ), ); const saveMutation = useMutation({ mutationFn: async () => { await adminApi.updateProposal(id!, { refinedScope }); await lineItemsApi.bulkUpdate( id!, lineItems.map((li) => ({ id: li.id, description: li.description, quantity: li.quantity, unit: li.unit, unitPrice: li.unitPrice, totalPrice: li.totalPrice, pricingMode: li.pricingMode, sortOrder: li.sortOrder, source: li.source, })) ); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.proposalLineItems, id] }); queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.proposals, id] }); setDirty(false); toast.success('Changes saved'); }, onError: (error: Error) => { toast.error(`Save failed: ${error.message}`); }, }); const approveMutation = useMutation({ mutationFn: async () => { if (dirty) { await saveMutation.mutateAsync(); } await adminApi.approveProposal(id!); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.proposals, id] }); queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.proposalLineItems, id] }); setApproveDialogOpen(false); setDirty(false); toast.success('Proposal approved'); }, onError: (error: Error) => { toast.error(`Approval failed: ${error.message}`); }, }); const sendMutation = useMutation({ mutationFn: () => adminApi.sendProposal(id!), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.proposals, id] }); setSendDialogOpen(false); toast.success('Proposal marked as sent'); }, // Fix: WEB-H5 — add missing onError handler onError: (error: Error) => { toast.error(`Send failed: ${error.message}`); }, }); const reviseMutation = useMutation({ mutationFn: () => adminApi.reviseProposal(id!), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.proposals, id] }); setReviseDialogOpen(false); toast.success('Revision created'); }, // Fix: WEB-H6 — add missing onError handler onError: (error: Error) => { toast.error(`Revision failed: ${error.message}`); }, }); const regenerateMutation = useMutation({ mutationFn: () => adminApi.generateSuggestions(id!), onSuccess: () => { toast.info('AI suggestion generation started'); }, // Fix: WEB-H5 — add missing onError handler onError: (error: Error) => { toast.error(`Regeneration failed: ${error.message}`); }, }); // Fix: WEB-M11 — wire returnToReview API method to UI const returnToReviewMutation = useMutation({ mutationFn: () => adminApi.returnToReview(id!), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.proposals, id] }); queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.proposalLineItems, id] }); setReturnToReviewDialogOpen(false); toast.success('Proposal returned to review'); }, onError: (error: Error) => { toast.error(`Return to review failed: ${error.message}`); }, }); const pdfMutation = useMutation({ mutationFn: () => adminApi.getPdf(id!), onSuccess: (data) => { if (data?.downloadUrl) { window.open(data.downloadUrl, '_blank'); toast.success('PDF ready — downloading'); } else { toast.info('PDF generation queued — try again in a few seconds'); } }, onError: () => { toast.error('Failed to generate PDF'); }, }); const handleLineItemsChange = (updated: EditableLineItem[]) => { setLineItems(updated); setDirty(true); }; if (isLoading) { return ( ); } // Fix: WEB-M7 — show error state with retry button when fetch fails if (isProposalError) { return ( refetchProposal()}> Retry } > Failed to load proposal{proposalError instanceof Error ? `: ${proposalError.message}` : '.'} ); } if (!proposal) { return ( Proposal not found ); } // Fix: WEB-M10 — explicit state transition guards const isEditable = proposal.status === 'InReview' || proposal.status === 'Revised'; const canApprove = isEditable && lineItems.length > 0; const isApproved = proposal.status === 'Approved'; const canSend = isApproved; const isSent = proposal.status === 'Sent'; const canRevise = isSent; const bidTotal = lineItems.reduce((sum, li) => sum + li.totalPrice, 0); // Tooltip messages for disabled actions const approveDisabledReason = !isEditable ? `Cannot approve: proposal is ${proposal.status}` : lineItems.length === 0 ? 'Add at least one line item before approving' : bidTotal <= 0 ? 'Total bid must be greater than zero' : ''; const sendDisabledReason = !canSend ? `Cannot send: proposal must be Approved (currently ${proposal.status})` : ''; const reviseDisabledReason = !canRevise ? `Cannot revise: proposal must be Sent (currently ${proposal.status})` : ''; return ( {/* Header */} navigate('/admin')} sx={{ fontSize: 14, color: '#0B5A73', cursor: 'pointer' }} > Admin Queue {proposal.proposalNumber} {/* Proposal Info Bar */} Customer {proposal.customerName} WO # {proposal.workOrderNumber} Category {proposal.serviceCategory} Priority Address {proposal.customerAddress} {/* Three-Panel Layout */} {/* Left Panel: Reference */} Submission Details Submitted By {proposal.submittedByName || '-'} Submitted {new Date(proposal.submittedAt).toLocaleString()} Original Scope {proposal.scopeOfWork} {proposal.notes && ( <> Notes {proposal.notes} )} {/* Center Panel: Editor */} Refined Scope { setRefinedScope(e.target.value); setDirty(true); }} disabled={!isEditable} size="small" sx={{ mb: 2 }} /> {/* Fix: WEB-M9 — show loading skeleton while line items are being fetched */} {lineItemsLoading ? ( {Array.from({ length: 4 }).map((_, i) => ( ))} ) : ( )} {/* Right Panel: Similar Proposals (hidden when empty) */} {hasSimilar && ( { const maxSort = lineItems.length > 0 ? Math.max(...lineItems.map((i) => i.sortOrder)) : 0; handleLineItemsChange([ ...lineItems, { ...item, id: null, sortOrder: maxSort + 1, source: 'Historical' }, ]); }} disabled={!isEditable} /> )} {/* Action Bar */} {dirty && ( Unsaved changes )} {!dirty && } {/* Fix: WEB-M10 — state transition guards with tooltip explanations */} {isEditable && ( <> )} {isApproved && ( <> {/* Fix: WEB-M11 — wire Return to Review button for approved proposals */} )} {isSent && ( )} {/* Unsaved Changes Navigation Guard */} blocker.reset?.()}> Unsaved Changes You have unsaved changes. Are you sure you want to leave? {/* Approve Confirmation Dialog */} setApproveDialogOpen(false)}> Approve Proposal Approve {proposal.proposalNumber} for {proposal.customerName}? Line Items: {lineItems.length} Total Bid: {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(bidTotal)} {proposal.vendorTotalCost != null && ( Vendor Cost: {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(proposal.vendorTotalCost)} )} {/* Mark as Sent Confirmation Dialog */} setSendDialogOpen(false)}> Mark as Sent Mark {proposal.proposalNumber} as sent to {proposal.customerName}? This action cannot be undone. {/* Create Revision Confirmation Dialog */} setReviseDialogOpen(false)}> Create Revision Create a new revision of {proposal.proposalNumber}? The current version will be marked as revised and a new editable copy will be created. {/* Fix: WEB-M11 — Return to Review Confirmation Dialog */} setReturnToReviewDialogOpen(false)}> Return to Review Return {proposal.proposalNumber} to In Review status? This will clear the approval and allow further edits to line items and scope. ); }