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, } 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 { 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_COLORS } 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 [dirty, setDirty] = useState(false); 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, }); 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'); }, }); 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'); }, }); const sendMutation = useMutation({ mutationFn: () => adminApi.sendProposal(id!), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.proposals, id] }); toast.success('Proposal marked as sent'); }, }); const reviseMutation = useMutation({ mutationFn: () => adminApi.reviseProposal(id!), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.proposals, id] }); toast.success('Revision created'); }, }); const regenerateMutation = useMutation({ mutationFn: () => adminApi.generateSuggestions(id!), onSuccess: () => { toast.info('AI suggestion generation started'); }, }); 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 ( ); } 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} {/* 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 }} /> {/* 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 && ( )} {/* 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)} )} ); }