import { useState, useCallback, useRef, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useMutation } from '@tanstack/react-query'; import { Box, Typography, Card, CardContent, TextField, Button, Grid, MenuItem, Autocomplete, CircularProgress, } from '@mui/material'; import SendIcon from '@mui/icons-material/Send'; import { toast } from 'react-toastify'; import { proposalsApi, type CreateProposalRequest, type ServiceCategory, type Priority } from '../../../lib/api/proposals'; import { customersApi, type Customer } from '../../../lib/api/customers'; import { SERVICE_CATEGORIES, PRIORITIES } from '../../../constants'; import { queryClient } from '../../../lib/queryClient'; // Fix: WEB-M5 — form state allows empty serviceCategory during editing; validated before submit interface ProposalFormState { workOrderNumber: string; customerName: string; customerAddress: string; scopeOfWork: string; serviceCategory: ServiceCategory | ''; priority: Priority; notes: string; } // Fix: WEB-M6 — file validation constants const MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024; // 25 MB const ALLOWED_MIME_TYPES = ['application/pdf']; export default function ProposalFormPage() { const navigate = useNavigate(); const [form, setForm] = useState({ workOrderNumber: '', customerName: '', customerAddress: '', scopeOfWork: '', serviceCategory: '', priority: 'Standard', notes: '', }); const [customers, setCustomers] = useState([]); const [customerLoading, setCustomerLoading] = useState(false); const [addresses, setAddresses] = useState([]); const [vendorFile, setVendorFile] = useState(null); const searchDebounceRef = useRef | null>(null); useEffect(() => { return () => { if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); }; }, []); const handleChange = (field: keyof ProposalFormState, value: string) => { setForm((prev) => ({ ...prev, [field]: value })); }; const searchCustomers = useCallback(async (query: string) => { if (query.length < 2) return; setCustomerLoading(true); try { const results = await customersApi.search(query); setCustomers(results); } catch { setCustomers([]); } finally { setCustomerLoading(false); } }, []); const handleCustomerSelect = (_: unknown, value: Customer | string | null) => { if (!value) { setForm((prev) => ({ ...prev, customerName: '', customerAddress: '' })); setAddresses([]); return; } if (typeof value === 'string') { setForm((prev) => ({ ...prev, customerName: value, customerAddress: '' })); setAddresses([]); } else { setForm((prev) => ({ ...prev, customerName: value.name, customerAddress: value.addresses[0] || '' })); setAddresses(value.addresses); } }; const createMutation = useMutation({ mutationFn: async () => { // Fix: WEB-M5 — cast validated form state to the strict request type const request: CreateProposalRequest = { workOrderNumber: form.workOrderNumber, customerName: form.customerName, customerAddress: form.customerAddress, scopeOfWork: form.scopeOfWork, serviceCategory: form.serviceCategory as ServiceCategory, priority: form.priority, notes: form.notes || undefined, }; const proposal = await proposalsApi.create(request); if (vendorFile) { try { const { uploadUrl, vendorProposalId } = await proposalsApi.uploadAttachment(proposal.id, vendorFile.name); await fetch(uploadUrl, { method: 'PUT', body: vendorFile, headers: { 'Content-Type': vendorFile.type || 'application/pdf' }, }); await proposalsApi.confirmUpload(proposal.id, vendorProposalId); } catch { toast.warning(`Proposal ${proposal.proposalNumber} was created, but vendor PDF upload failed. You can re-upload from the proposal detail page.`); } } return proposal; }, onSuccess: (proposal) => { queryClient.invalidateQueries({ queryKey: ['proposals'] }); toast.success(`Proposal ${proposal.proposalNumber} submitted`); navigate(`/proposals/${proposal.id}`); }, onError: (error: Error) => { toast.error(error.message || 'Failed to submit proposal'); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); createMutation.mutate(); }; const isValid = form.workOrderNumber.trim() && form.customerName.trim() && form.customerAddress.trim() && form.scopeOfWork.trim() && form.serviceCategory; return ( New Proposal Request handleChange('workOrderNumber', e.target.value)} /> (typeof opt === 'string' ? opt : opt.name)} loading={customerLoading} onInputChange={(_, value, reason) => { if (reason === 'input') { handleChange('customerName', value); } if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); searchDebounceRef.current = setTimeout(() => searchCustomers(value), 300); }} onChange={handleCustomerSelect} renderInput={(params) => ( {customerLoading && } {params.InputProps.endAdornment} ), }, }} /> )} /> {addresses.length > 1 ? ( handleChange('customerAddress', e.target.value)} > {addresses.map((addr) => ( {addr} ))} ) : ( handleChange('customerAddress', e.target.value)} /> )} handleChange('serviceCategory', e.target.value)} > {SERVICE_CATEGORIES.map((cat) => ( {cat} ))} handleChange('priority', e.target.value)} > {PRIORITIES.map((p) => ( {p} ))} handleChange('scopeOfWork', e.target.value)} placeholder="Describe the work needed..." /> handleChange('notes', e.target.value)} /> ); }