import React, { useState, useMemo, useEffect } from 'react'; import { ProjectState, ProjectDocument, UserRole, BOQItem, Unit, Priority } from '../types'; import DocumentManager from './DocumentManager'; import ChangeOrderManager from './ChangeOrderManager'; import ManualOverrideToggle from './ManualOverrideToggle'; import { PlusCircle, X, Search, Filter, ArrowUpDown, ChevronDown, ArrowUp, ArrowDown, Activity, RotateCcw, Sparkles, Loader2, ChevronUp, Layers, Flag, Save, Info, CheckCircle2, FileText, UploadCloud, Link as LinkIcon, Download, FileUp } from 'lucide-react'; import { suggestPlannedUnitCost, parseBOQDocument } from '../services/localAnalysisService'; interface MasterControlProps { data: ProjectState; onAddDocument: (doc: ProjectDocument) => void; onAddBOQItem: (item: BOQItem) => void; onUpdateBOQItem?: (itemId: string, updatedItem: Partial) => void; onImportBOQItems: (items: BOQItem[]) => void; userRole: UserRole; } type SortField = 'id' | 'rate' | 'plannedUnitCost' | 'plannedQty' | 'executedQty' | 'progress' | 'revenue' | 'variance' | 'profit' | 'priority'; type SortDirection = 'asc' | 'desc'; type StatusFilter = 'ALL' | 'PENDING' | 'IN_PROGRESS' | 'COMPLETED'; const MasterControl: React.FC = ({ data, onAddDocument, onAddBOQItem, onUpdateBOQItem, onImportBOQItems, userRole }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [isImportModalOpen, setIsImportModalOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [unitFilter, setUnitFilter] = useState('ALL'); const [statusFilter, setStatusFilter] = useState('ALL'); const [priorityFilter, setPriorityFilter] = useState('ALL'); const [sortField, setSortField] = useState('id'); const [sortDirection, setSortDirection] = useState('asc'); const [expandedRows, setExpandedRows] = useState>(new Set()); // Import State const [importTab, setImportTab] = useState<'EXISTING' | 'UPLOAD'>('EXISTING'); const [selectedFileId, setSelectedFileId] = useState(''); const [fileToUpload, setFileToUpload] = useState(null); const [isImporting, setIsImporting] = useState(false); const [importStatus, setImportStatus] = useState(null); // Editing state for breakdown const [editId, setEditId] = useState(null); const [editMat, setEditMat] = useState('0'); const [editLab, setEditLab] = useState('0'); const [editEqp, setEditEqp] = useState('0'); const [editOH, setEditOH] = useState('0'); const canEditBOQ = userRole === 'DIRECTOR' || userRole === 'MANAGER'; // Form State for Adding Item const [description, setDescription] = useState(''); const [unit, setUnit] = useState(Unit.CUM); const [rate, setRate] = useState(''); const [plannedUnitCost, setPlannedUnitCost] = useState(''); const [itemPriority, setItemPriority] = useState('MEDIUM'); const [plannedQty, setPlannedQty] = useState(''); // Breakdown states for new item const [plannedMat, setPlannedMat] = useState('0'); const [plannedLab, setPlannedLab] = useState('0'); const [plannedEqp, setPlannedEqp] = useState('0'); const [plannedOH, setPlannedOH] = useState('0'); const [isSuggesting, setIsSuggesting] = useState(false); const [aiAppliedFields, setAiAppliedFields] = useState(false); // Auto-calculate plannedUnitCost when breakdown changes for NEW item useEffect(() => { const total = Number(plannedMat) + Number(plannedLab) + Number(plannedEqp) + Number(plannedOH); setPlannedUnitCost(total.toString()); }, [plannedMat, plannedLab, plannedEqp, plannedOH]); // Use all documents for import selection to give user full flexibility const availableDocs = useMemo(() => { // Filter for documents that are likely to be BOQs (PDF, Excel, etc.) return data.documents.filter(d => ['PDF', 'XLSX', 'CSV', 'DOC', 'DOCX'].includes(d.type) || ['CONTRACT', 'REPORT', 'BILL'].includes(d.category) ); }, [data.documents]); const handleAddItem = (e: React.FormEvent) => { e.preventDefault(); const newItem: BOQItem = { id: `${(data.boq.length + 1) * 10}-NEW`, description, unit, rate: Number(rate), priority: itemPriority, plannedUnitCost: Number(plannedUnitCost), plannedBreakdown: { material: Number(plannedMat), labor: Number(plannedLab), equipment: Number(plannedEqp), overhead: Number(plannedOH) }, plannedQty: Number(plannedQty), executedQty: 0 }; onAddBOQItem(newItem); setIsModalOpen(false); resetForm(); }; const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { setFileToUpload(e.target.files[0]); } }; const handleImport = async () => { if (importTab === 'EXISTING' && !selectedFileId) return; if (importTab === 'UPLOAD' && !fileToUpload) return; setIsImporting(true); try { let docName = ''; if (importTab === 'UPLOAD' && fileToUpload) { setImportStatus('Uploading & Scanning...'); // 1. Create and Add Document const newDoc: ProjectDocument = { id: `D${Date.now()}`, name: fileToUpload.name, type: fileToUpload.name.split('.').pop()?.toUpperCase() || 'PDF', category: 'CONTRACT', module: 'MASTER', uploadDate: new Date().toISOString().split('T')[0], size: `${(fileToUpload.size / (1024 * 1024)).toFixed(2)} MB`, url: URL.createObjectURL(fileToUpload), isAnalyzed: true }; onAddDocument(newDoc); docName = newDoc.name; // Artificial delay for UX await new Promise(resolve => setTimeout(resolve, 1500)); } else { const file = data.documents.find(d => d.id === selectedFileId); if (!file) return; docName = file.name; setImportStatus('Analyzing existing document...'); } // 2. Parse Items via AI Service const items = await parseBOQDocument(docName); setImportStatus(`Found ${items.length} BOQ items. Syncing...`); await new Promise(resolve => setTimeout(resolve, 1000)); // 3. Import Items onImportBOQItems(items); resetImport(); } catch (e) { setImportStatus('Failed to parse document.'); } finally { setIsImporting(false); } }; const resetImport = () => { setIsImportModalOpen(false); setSelectedFileId(''); setFileToUpload(null); setImportTab('EXISTING'); setImportStatus(null); }; const resetForm = () => { setDescription(''); setRate(''); setPlannedUnitCost(''); setPlannedQty(''); setPlannedMat('0'); setPlannedLab('0'); setPlannedEqp('0'); setPlannedOH('0'); setItemPriority('MEDIUM'); setAiAppliedFields(false); }; const handleSuggestCost = async () => { if (!description) return; setIsSuggesting(true); const suggestion = await suggestPlannedUnitCost(description, unit, data.boq); setIsSuggesting(false); if (suggestion) { setPlannedMat(suggestion.breakdown.material.toString()); setPlannedLab(suggestion.breakdown.labor.toString()); setPlannedEqp(suggestion.breakdown.equipment.toString()); setPlannedOH(suggestion.breakdown.overhead.toString()); setAiAppliedFields(true); // Reset highlight after 3 seconds setTimeout(() => setAiAppliedFields(false), 3000); } }; const startEditing = (item: BOQItem) => { setEditId(item.id); setEditMat(item.plannedBreakdown?.material?.toString() || '0'); setEditLab(item.plannedBreakdown?.labor?.toString() || '0'); setEditEqp(item.plannedBreakdown?.equipment?.toString() || '0'); setEditOH(item.plannedBreakdown?.overhead?.toString() || '0'); }; const cancelEditing = () => { setEditId(null); }; const saveEditing = (id: string) => { if (!onUpdateBOQItem) return; const total = Number(editMat) + Number(editLab) + Number(editEqp) + Number(editOH); onUpdateBOQItem(id, { plannedUnitCost: total, plannedBreakdown: { material: Number(editMat), labor: Number(editLab), equipment: Number(editEqp), overhead: Number(editOH) } }); setEditId(null); }; const handleLinkDocument = (itemId: string, docId: string) => { if (onUpdateBOQItem) { onUpdateBOQItem(itemId, { linkedDocId: docId || undefined }); } }; const toggleRow = (id: string) => { const newExpanded = new Set(expandedRows); if (newExpanded.has(id)) { newExpanded.delete(id); if (editId === id) setEditId(null); } else { newExpanded.add(id); } setExpandedRows(newExpanded); }; const handleSort = (field: SortField) => { if (sortField === field) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); } else { setSortField(field); setSortDirection('asc'); } }; const clearFilters = () => { setSearchTerm(''); setUnitFilter('ALL'); setStatusFilter('ALL'); setPriorityFilter('ALL'); }; const hasActiveFilters = searchTerm !== '' || unitFilter !== 'ALL' || statusFilter !== 'ALL' || priorityFilter !== 'ALL'; const getPriorityWeight = (p?: Priority) => { if (p === 'HIGH') return 3; if (p === 'MEDIUM') return 2; if (p === 'LOW') return 1; return 0; }; // Group documents by category for the dropdown const groupedDocuments = useMemo(() => { const groups: Record = {}; data.documents.forEach(doc => { if (!groups[doc.category]) groups[doc.category] = []; groups[doc.category].push(doc); }); return groups; }, [data.documents]); const filteredAndSortedBOQ = useMemo(() => { return data.boq .filter(item => { const matchesSearch = item.description.toLowerCase().includes(searchTerm.toLowerCase()) || item.id.toLowerCase().includes(searchTerm.toLowerCase()); const matchesUnit = unitFilter === 'ALL' || item.unit === unitFilter; const matchesPriority = priorityFilter === 'ALL' || item.priority === priorityFilter; let currentStatus: StatusFilter = 'PENDING'; if (item.executedQty >= item.plannedQty) currentStatus = 'COMPLETED'; else if (item.executedQty > 0) currentStatus = 'IN_PROGRESS'; const matchesStatus = statusFilter === 'ALL' || currentStatus === statusFilter; return matchesSearch && matchesUnit && matchesStatus && matchesPriority; }) .sort((a, b) => { let valA: any, valB: any; const getVariance = (item: BOQItem) => { if (item.executedQty === 0 || !item.costAnalysis) return 0; return item.plannedUnitCost - item.costAnalysis.unitCost; }; const getProfit = (item: BOQItem) => { if (item.executedQty === 0) return 0; const cost = item.costAnalysis?.unitCost || item.plannedUnitCost; return (item.rate - cost) * item.executedQty; }; switch (sortField) { case 'rate': valA = a.rate; valB = b.rate; break; case 'plannedUnitCost': valA = a.plannedUnitCost; valB = b.plannedUnitCost; break; case 'plannedQty': valA = a.plannedQty; valB = b.plannedQty; break; case 'executedQty': valA = a.executedQty; valB = b.executedQty; break; case 'variance': valA = getVariance(a); valB = getVariance(b); break; case 'profit': valA = getProfit(a); valB = getProfit(b); break; case 'priority': valA = getPriorityWeight(a.priority); valB = getPriorityWeight(b.priority); break; case 'progress': valA = (a.executedQty / a.plannedQty) || 0; valB = (b.executedQty / b.plannedQty) || 0; break; case 'revenue': valA = a.rate * a.plannedQty; valB = b.rate * b.plannedQty; break; default: valA = a.id; valB = b.id; } if (valA < valB) return sortDirection === 'asc' ? -1 : 1; if (valA > valB) return sortDirection === 'asc' ? 1 : -1; return 0; }); }, [data.boq, searchTerm, unitFilter, statusFilter, priorityFilter, sortField, sortDirection]); const SortIcon = ({ field }: { field: SortField }) => { if (sortField !== field) return ; return sortDirection === 'asc' ? : ; }; const getPriorityColor = (p?: Priority) => { switch(p) { case 'HIGH': return 'bg-red-50 text-red-600 border-red-100'; case 'MEDIUM': return 'bg-amber-50 text-amber-600 border-amber-100'; case 'LOW': return 'bg-blue-50 text-blue-600 border-blue-100'; default: return 'bg-slate-50 text-slate-600 border-slate-100'; } }; return (

Master Control (Baseline)

Fixed Contract Data, BOQ & Budget

Project Name

{data.name}

Contract Duration

{data.startDate} to {data.endDate}

Total Contract Value

৳{data.contractValue.toLocaleString()}

{/* Table Toolbar */}

Bill of Quantities (BOQ)

Rev 1.0
setSearchTerm(e.target.value)} className="w-full pl-9 pr-4 py-1.5 text-sm bg-white border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all" />
{hasActiveFilters && ( )}
{canEditBOQ && ( <> )}
{filteredAndSortedBOQ.map((item) => { const progress = Math.min(100, (item.executedQty / item.plannedQty) * 100) || 0; const isCompleted = item.executedQty >= item.plannedQty; const isInProgress = item.executedQty > 0 && !isCompleted; const isExpanded = expandedRows.has(item.id); const isEditing = editId === item.id; // Finance Analysis const hasActualCost = item.executedQty > 0 && !!item.costAnalysis; const actualCost = item.costAnalysis?.unitCost || 0; const costVariance = hasActualCost ? (item.plannedUnitCost - actualCost) : 0; const margin = hasActualCost ? (item.rate - actualCost) : (item.rate - item.plannedUnitCost); const profitContribution = item.executedQty > 0 ? (margin * item.executedQty) : 0; const currentTotal = isEditing ? (Number(editMat) + Number(editLab) + Number(editEqp) + Number(editOH)) : item.plannedUnitCost; return ( toggleRow(item.id)} > {isExpanded && ( e.stopPropagation()}> )} ); })}
handleSort('id')}> ID Description & Status handleSort('priority')}> Priority Unit handleSort('rate')}> Rate (৳) handleSort('variance')}> Cost Var. handleSort('profit')}> Profit Cont. handleSort('plannedQty')}> Planned handleSort('executedQty')}> Executed handleSort('progress')}> Progress handleSort('revenue')}> Total Rev (৳)
{isExpanded ? : } {item.id}
{item.description}
{isCompleted ? ( Completed ) : isInProgress ? ( In Progress ) : ( Pending )} Budget: ৳{item.plannedUnitCost.toLocaleString()} {/* Linked Doc Indicator */} {item.linkedDocId && ( Doc )}
{item.priority || 'N/A'}
{item.unit} {item.rate.toLocaleString(undefined, { minimumFractionDigits: 2 })} {hasActualCost ? (
= 0 ? 'text-emerald-600' : 'text-red-600'}`}> {costVariance >= 0 ? '+' : ''}{costVariance.toFixed(2)}
) : ( N/A )}
{item.executedQty > 0 ? (
= 0 ? 'text-blue-700' : 'text-red-700'}`}> ৳{profitContribution.toLocaleString(undefined, { maximumFractionDigits: 0 })}
) : ( 0 )}
{item.plannedQty.toLocaleString()} {item.executedQty.toLocaleString()}
= 100 ? 'text-emerald-600' : progress > 0 ? 'text-blue-600' : 'text-slate-400'}`}> {progress.toFixed(1)}%
= 100 ? 'bg-emerald-500' : 'bg-blue-500'}`} style={{ width: `${progress}%` }} >
{(item.rate * item.plannedQty).toLocaleString(undefined, { minimumFractionDigits: 2 })}
Item Details & Cost Breakdown
{!isEditing && canEditBOQ && ( )} {isEditing && (
)}
{/* Document Link Section */}
{item.linkedDocId && ( d.id === item.linkedDocId)?.url} download={data.documents.find(d => d.id === item.linkedDocId)?.name} className="text-blue-600 hover:text-blue-800 p-2 hover:bg-blue-50 rounded-full transition-colors" title="Download Linked Document" > )}
{/* Component Inputs/Views */} {[ { label: 'Material', value: item.plannedBreakdown?.material || 0, editValue: editMat, setEdit: setEditMat, color: 'bg-blue-500' }, { label: 'Labor', value: item.plannedBreakdown?.labor || 0, editValue: editLab, setEdit: setEditLab, color: 'bg-amber-500' }, { label: 'Equipment', value: item.plannedBreakdown?.equipment || 0, editValue: editEqp, setEdit: setEditEqp, color: 'bg-emerald-500' }, { label: 'Overhead', value: item.plannedBreakdown?.overhead || 0, editValue: editOH, setEdit: setEditOH, color: 'bg-violet-500' }, ].map((comp, idx) => { const percent = (isEditing ? Number(comp.editValue) : comp.value) / (currentTotal || 1) * 100 || 0; return (

{comp.label}

{percent.toFixed(0)}%
{isEditing ? (
comp.setEdit(e.target.value)} className="w-full pl-7 pr-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm font-mono font-bold" />
) : (

৳{comp.value.toLocaleString()}

)}
); })}
Calculated Planned Unit Cost:
{isEditing && currentTotal !== item.plannedUnitCost && ( item.plannedUnitCost ? 'bg-red-100 text-red-600' : 'bg-emerald-100 text-emerald-600'}`}> {currentTotal > item.plannedUnitCost ? 'Increased' : 'Decreased'} by ৳{Math.abs(currentTotal - item.plannedUnitCost).toLocaleString()} )} ৳{currentTotal.toLocaleString(undefined, { minimumFractionDigits: 2 })}

Change Order Management

Track extra work requests and contract variations

{/* Import BOQ Modal */} {isImportModalOpen && (

Import BOQ Items

{/* Tabs */}

AI will extract items, quantities, and rates from the document to populate the master schedule.

{importTab === 'EXISTING' ? (
{availableDocs.length === 0 && (

No compatible files found (PDF/Excel).

)}
) : (
{fileToUpload ? : }

{fileToUpload ? fileToUpload.name : "Click to browse or drag file"}

{!fileToUpload &&

PDF, Excel, or Word

}
)} {importStatus && (
{isImporting ? : } {importStatus}
)}
)} {/* Add Item Modal */} {isModalOpen && (

Add New BOQ Item

{isSuggesting && (
AI Estimating...
)}