import { useState, useEffect, useRef } from 'react'; import { useParams, useNavigate } 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, Menu, MenuItem, IconButton, } 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 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 DownloadIcon from '@mui/icons-material/Download'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; 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, type AuditEntry } 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_COLORS, STATUS_LABELS, PRIORITY_LABELS } from '../../../constants'; const ACTION_LABELS: Record = { Submit: 'Submitted', Edit: 'Edited', Approve: 'Approved', ReturnToReview: 'Returned to Review', MarkSent: 'Marked Sent', CreateRevision: 'Revision Created', EditLineItem: 'Line Items Updated', EditScope: 'Scope Edited', Regenerate: 'Regenerated', GeneratePDF: 'PDF Generated', Download: 'Downloaded', AssignAdmin: 'Admin Assigned', UpdateRole: 'Role Updated', Revise: 'Revised', }; function formatAction(action: string): string { return ACTION_LABELS[action] || action.replace(/([A-Z])/g, ' $1').trim(); } function formatDetails(details: string): string { try { const parsed = JSON.parse(details); return parsed.message || details; } catch { return details; } } export default function AdminWorkspace() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [lineItems, setLineItems] = useState([]); const [refinedScope, setRefinedScope] = useState(''); const [workOrderNumber, setWorkOrderNumber] = useState(''); const [poNumber, setPoNumber] = useState(''); const [approveDialogOpen, setApproveDialogOpen] = useState(false); const [sendDialogOpen, setSendDialogOpen] = useState(false); const [reviseDialogOpen, setReviseDialogOpen] = useState(false); const [returnToReviewDialogOpen, setReturnToReviewDialogOpen] = useState(false); const [dirty, setDirty] = useState(false); const [revMenuAnchor, setRevMenuAnchor] = useState(null); const [downloadingRevId, setDownloadingRevId] = useState(null); const { data: proposal, isLoading } = useQuery({ queryKey: [QUERY_KEYS.proposals, id], queryFn: () => proposalsApi.getById(id!), enabled: !!id, }); const { data: fetchedLineItems } = useQuery({ queryKey: [QUERY_KEYS.proposalLineItems, id], queryFn: () => lineItemsApi.getAll(id!), enabled: !!id, }); const { data: auditTrail } = useQuery({ queryKey: ['audit', id], queryFn: () => adminApi.getAudit(id!), enabled: !!id, }); const { data: revisionHistory } = useQuery({ queryKey: [QUERY_KEYS.proposals, id, 'history'], queryFn: () => adminApi.getHistory(id!), enabled: !!id, }); 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); setWorkOrderNumber(proposal.workOrderNumber || ''); setPoNumber(proposal.poNumber || ''); } }, [proposal]); const dirtyRef = useRef(dirty); dirtyRef.current = dirty; useEffect(() => { if (!dirty) return; const handler = (e: BeforeUnloadEvent) => { e.preventDefault(); }; window.addEventListener('beforeunload', handler); return () => window.removeEventListener('beforeunload', handler); }, [dirty]); const saveMutation = useMutation({ mutationFn: async () => { await adminApi.updateProposal(id!, { refinedScope, workOrderNumber, poNumber }); 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] }); queryClient.invalidateQueries({ queryKey: ['audit', id] }); setDirty(false); toast.success('Changes saved'); }, }); 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] }); queryClient.invalidateQueries({ queryKey: ['audit', id] }); setApproveDialogOpen(false); setDirty(false); toast.success('Proposal approved'); }, }); const sendMutation = useMutation({ mutationFn: () => adminApi.sendProposal(id!), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.proposals, id] }); queryClient.invalidateQueries({ queryKey: ['audit', id] }); setSendDialogOpen(false); toast.success('Proposal marked as sent'); }, }); const reviseMutation = useMutation({ mutationFn: () => adminApi.reviseProposal(id!), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.proposals, id] }); queryClient.invalidateQueries({ queryKey: ['audit', id] }); setReviseDialogOpen(false); toast.success('Revision created'); }, }); const returnToReviewMutation = useMutation({ mutationFn: () => adminApi.returnToReview(id!), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.proposals, id] }); queryClient.invalidateQueries({ queryKey: ['audit', id] }); setReturnToReviewDialogOpen(false); toast.success('Proposal returned to review'); }, }); const regenerateMutation = useMutation({ mutationFn: () => adminApi.generateSuggestions(id!), onSuccess: () => { toast.info('AI suggestion generation started'); }, }); const pdfMutation = useMutation({ mutationFn: (regenerate?: boolean) => adminApi.getPdf(id!, regenerate ?? false), onSuccess: (data) => { if (data?.downloadUrl) { window.open(data.downloadUrl, '_blank'); queryClient.invalidateQueries({ queryKey: ['audit', id] }); toast.success('PDF ready'); } 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); }; const DOWNLOADABLE_STATUSES = ['Approved', 'Sent', 'Revised']; const handleDownloadPdf = async (revId: string, proposalNumber: string, revision: number) => { setDownloadingRevId(revId); try { const result = await proposalsApi.getPdf(revId); if (result) { const a = document.createElement('a'); a.href = result.downloadUrl; a.download = `${proposalNumber}-rev${revision}.pdf`; a.click(); URL.revokeObjectURL(result.downloadUrl); toast.success('PDF downloaded'); } else { toast.info('PDF not yet generated for this revision'); } } catch { toast.error('Failed to download PDF'); } finally { setDownloadingRevId(null); } }; if (isLoading) { return ( ); } if (!proposal) { return ( Proposal not found ); } const isEditable = proposal.status === 'InReview' || proposal.status === 'Revised'; const isApproved = proposal.status === 'Approved'; const isSent = proposal.status === 'Sent'; const bidTotal = lineItems.reduce((sum, li) => sum + li.totalPrice, 0); return ( {/* Header */} {proposal.proposalNumber} setRevMenuAnchor(e.currentTarget)} onDelete={(e) => setRevMenuAnchor(e.currentTarget as HTMLElement)} deleteIcon={} sx={{ cursor: 'pointer' }} /> setRevMenuAnchor(null)} > {(revisionHistory ?? []) .sort((a, b) => a.currentRevision - b.currentRevision) .map((rev) => ( { setRevMenuAnchor(null); if (rev.id !== id) { if (dirty && !window.confirm('You have unsaved changes. Are you sure you want to switch revisions?')) return; navigate(`/admin/proposals/${rev.id}`); } }} sx={{ display: 'flex', justifyContent: 'space-between', gap: 2, minWidth: 160 }} > Rev {rev.currentRevision} {STATUS_LABELS[rev.status] || rev.status} {DOWNLOADABLE_STATUSES.includes(rev.status) && ( { e.stopPropagation(); setRevMenuAnchor(null); handleDownloadPdf(rev.id, rev.proposalNumber, rev.currentRevision); }} disabled={downloadingRevId === rev.id} > )} ))} {/* Proposal Info Bar */} Customer {proposal.customerName} Site {proposal.customerAddress} WO # {isEditable ? ( { setWorkOrderNumber(e.target.value); setDirty(true); }} sx={{ width: 140, display: 'block', '& input': { fontSize: '0.875rem', lineHeight: 1.5, fontWeight: 600, p: 0 }, '& .MuiInput-root': { mt: 0 } }} /> ) : ( {workOrderNumber || '—'} )} PO # {isEditable ? ( { setPoNumber(e.target.value); setDirty(true); }} sx={{ width: 120, display: 'block', '& input': { fontSize: '0.875rem', lineHeight: 1.5, fontWeight: 600, p: 0 }, '& .MuiInput-root': { mt: 0 } }} /> ) : ( {poNumber || '—'} )} Category {proposal.serviceCategory} Priority {PRIORITY_LABELS[proposal.priority] || proposal.priority} {/* 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 }} /> {/* Right Panel: Similar Proposals */} { 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 && } {isEditable && ( <> )} {isApproved && ( <> )} {isSent && ( <> )} {proposal.status === 'Revised' && ( )} {/* 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. {/* Return to Review Confirmation Dialog */} setReturnToReviewDialogOpen(false)}> Return to Review Return {proposal.proposalNumber} to review? This will clear the approval and allow further editing. {/* Audit Trail */} {auditTrail && auditTrail.length > 0 && ( Activity Log {auditTrail.map((entry) => ( {new Date(entry.timestamp).toLocaleString()} {entry.userName} {entry.details ? formatDetails(entry.details) : ''} ))} )} ); }