import React, { useState, useMemo } from 'react'; import { ProjectState, ProjectDocument, BOQItem, UserRole, Bill, ExtractedBill } from '../types'; import { Download, PlusCircle, CheckCircle2, ChevronDown, ChevronUp, TrendingUp, Wallet, ArrowDownRight, Sparkles, Loader2, Zap, Package, X, Save, Edit2, Hammer, UsersRound, AlertOctagon, FileText } from 'lucide-react'; import DocumentManager from './DocumentManager'; import { extractBillData, suggestActualCostBreakdown, parseRunningBillDetails } from '../services/localAnalysisService'; import jsPDF from 'jspdf'; import autoTable from 'jspdf-autotable'; interface FinancialControlProps { data: ProjectState; onAddDocument: (doc: ProjectDocument) => void; onUpdateBOQItem?: (itemId: string, updatedItem: Partial) => void; onAddBill: (bill: Bill) => void; onUpdatePDRemarks: (type: 'BILL', id: string, remarks: string) => void; onBillItemizedUpdate: (items: { boqId: string; amount: number }[]) => void; userRole: UserRole; } const FinancialControl: React.FC = ({ data, onAddDocument, onUpdateBOQItem, onAddBill, onUpdatePDRemarks, onBillItemizedUpdate, userRole }) => { const [expandedRow, setExpandedRow] = useState(null); const [isAiLoading, setIsAiLoading] = useState(false); const [analyzingItemId, setAnalyzingItemId] = useState(null); const [isBillModalOpen, setIsBillModalOpen] = useState(false); const [editingRemarksId, setEditingRemarksId] = useState(null); const [tempRemarks, setTempRemarks] = useState(''); // Bill Form const [billType, setBillType] = useState('VENDOR_INVOICE'); const [billCategory, setBillCategory] = useState>('OTHER'); const [billEntity, setBillEntity] = useState(''); const [billAmount, setBillAmount] = useState(''); const [billDate, setBillDate] = useState(new Date().toISOString().split('T')[0]); const [aiAutofilled, setAiAutofilled] = useState(false); const [detectedBillDocName, setDetectedBillDocName] = useState(''); const canAddClientBill = userRole === 'MANAGER' || userRole === 'DIRECTOR'; const canAddVendorBill = userRole === 'ACCOUNTANT' || userRole === 'DIRECTOR'; const canUploadDoc = canAddClientBill || canAddVendorBill; const isDirector = userRole === 'DIRECTOR'; const clientBills = data.bills.filter(b => b.type === 'CLIENT_RA'); const vendorBills = data.bills.filter(b => b.type === 'VENDOR_INVOICE' || b.type === 'MATERIAL_EXPENSE' || b.type === 'SUB_CONTRACTOR'); const totalRevenue = clientBills.reduce((acc, b) => acc + b.amount, 0); const totalExpenses = vendorBills.reduce((acc, b) => acc + b.amount, 0); // Material Value Calculation const materialInventoryValue = data.materials.reduce((sum, mat) => sum + (mat.currentStock * mat.averageRate), 0); // --- LIVE SYNC CALCULATIONS --- // 1. Live Material Stats (Automated from DPRs) const materialStats = useMemo(() => { return data.materials.map(m => { const consumedQty = data.dprs.reduce((acc, dpr) => { const usage = dpr.materialsUsed?.find(u => u.materialId === m.id); return acc + (usage ? usage.qty : 0); }, 0); return { ...m, liveConsumedQty: consumedQty, liveExpense: consumedQty * m.averageRate }; }).filter(m => m.liveConsumedQty > 0 || m.totalConsumed > 0); }, [data.materials, data.dprs]); // 2. Live Sub-Contractor Stats (Automated from DPRs vs Bills) const subContractorStats = useMemo(() => { if (!data.subContractors) return []; return data.subContractors.map(sc => { // Calculate accrued liability directly from DPRs for perfect sync const liveWorkValue = data.dprs .filter(d => d.subContractorId === sc.id && d.workDoneQty && d.linkedBoqId) .reduce((sum, d) => { const rate = sc.agreedRates.find(r => r.boqId === d.linkedBoqId)?.rate || 0; return sum + (d.workDoneQty! * rate); }, 0); return { ...sc, liveWorkValue, balance: liveWorkValue - sc.totalBilled }; }); }, [data.subContractors, data.dprs]); // 3. Live BOQ Item Costing const liveItemStats = useMemo(() => { return data.boq.map(item => { const relevantDPRs = data.dprs.filter(d => d.linkedBoqId === item.id); // Calculate Material Expense let materialExp = 0; relevantDPRs.forEach(dpr => { dpr.materialsUsed?.forEach(usage => { const mat = data.materials.find(m => m.id === usage.materialId); if (mat) { materialExp += usage.qty * mat.averageRate; } }); }); // Calculate Sub-Contractor Liability let subContractExp = 0; relevantDPRs.forEach(dpr => { if (dpr.subContractorId && dpr.workDoneQty) { const sc = data.subContractors.find(s => s.id === dpr.subContractorId); const rateObj = sc?.agreedRates.find(r => r.boqId === item.id); if (rateObj) { subContractExp += dpr.workDoneQty * rateObj.rate; } } }); // Estimate Direct Labor let directLaborExp = 0; const avgDailyWage = 800; const dprsWithNoSC = relevantDPRs.filter(d => !d.subContractorId); const totalLaborDays = dprsWithNoSC.reduce((acc, d) => acc + d.laborCount, 0); directLaborExp = totalLaborDays * avgDailyWage; const totalActualCost = materialExp + subContractExp + directLaborExp; const revenue = (item.billedAmount || 0); const workDoneValue = item.executedQty * item.rate; const profit = revenue - totalActualCost; return { ...item, stats: { materialExp, subContractExp, directLaborExp, totalActualCost, profit } }; }); }, [data.boq, data.dprs, data.materials, data.subContractors]); const totalOperationalProfit = liveItemStats.reduce((acc, item) => { const workValue = item.executedQty * item.rate; return acc + (workValue - item.stats.totalActualCost); }, 0); const generateRABill = () => { const doc = new jsPDF(); // Header doc.setFontSize(20); doc.text('Running Account (RA) Bill', 14, 22); doc.setFontSize(10); doc.text(`Project: ${data.name}`, 14, 30); doc.text(`Date: ${new Date().toLocaleDateString()}`, 14, 35); doc.text(`Generated By: BuildTrack AI`, 14, 40); // Table Data const tableData = data.boq .filter(item => item.executedQty > 0) .map((item, index) => [ index + 1, item.description, item.unit, item.plannedQty.toLocaleString(), item.executedQty.toLocaleString(), `৳${item.rate.toLocaleString()}`, `৳${(item.executedQty * item.rate).toLocaleString()}` ]); const totalBillValue = data.boq.reduce((sum, item) => sum + (item.executedQty * item.rate), 0); const previouslyBilled = data.boq.reduce((sum, item) => sum + (item.billedAmount || 0), 0); const netPayable = totalBillValue - previouslyBilled; autoTable(doc, { startY: 50, head: [['Sr.', 'Description', 'Unit', 'Total Qty', 'Executed Qty', 'Rate', 'Amount']], body: tableData, theme: 'grid', styles: { fontSize: 8 }, headStyles: { fillColor: [15, 23, 42] } // Slate-900 }); const finalY = (doc as any).lastAutoTable.finalY || 50; doc.setFontSize(10); doc.text(`Total Value of Work Executed: ৳${totalBillValue.toLocaleString()}`, 14, finalY + 10); doc.text(`Less: Previously Billed: ৳${previouslyBilled.toLocaleString()}`, 14, finalY + 16); doc.setFontSize(12); doc.setFont("helvetica", "bold"); doc.text(`Net Amount Payable: ৳${netPayable.toLocaleString()}`, 14, finalY + 24); doc.save(`RA_Bill_${data.id}_${new Date().toISOString().split('T')[0]}.pdf`); }; const toggleRow = (id: string) => { setExpandedRow(expandedRow === id ? null : id); }; const handleBillUploaded = (extracted: ExtractedBill) => { setAiAutofilled(true); const latestBill = data.documents.find(d => d.category === 'BILL' && d.uploadDate === new Date().toISOString().split('T')[0]); if (latestBill) setDetectedBillDocName(latestBill.name); if (extracted.type) setBillType(extracted.type === 'CLIENT_RA' ? 'CLIENT_RA' : 'VENDOR_INVOICE'); if (extracted.entityName) setBillEntity(extracted.entityName); if (extracted.amount) setBillAmount(extracted.amount.toString()); if (extracted.date) setBillDate(extracted.date); setIsBillModalOpen(true); setTimeout(() => setAiAutofilled(false), 5000); }; const handleAiBillExtraction = async () => { const lastBillDoc = data.documents.find(d => d.category === 'BILL'); if (!lastBillDoc) { alert("No bill documents found to analyze."); return; } setIsAiLoading(true); const extracted = await extractBillData(lastBillDoc.name); setDetectedBillDocName(lastBillDoc.name); setIsAiLoading(false); if (extracted) { alert(`AI Extracted Bill Info:\n\nEntity: ${extracted.entityName}\nAmount: ৳${extracted.amount}\nType: ${extracted.type}\n\nYou can now use these values to populate the form.`); handleBillUploaded(extracted); } }; const handleCreateBill = async (e: React.FormEvent) => { e.preventDefault(); onAddBill({ id: `BILL-${Date.now()}`, type: billType, entityName: billEntity, amount: Number(billAmount), date: billDate, status: 'PENDING', category: billType === 'CLIENT_RA' ? undefined : billCategory }); if (billType === 'CLIENT_RA' && detectedBillDocName) { const confirmItemize = window.confirm("Do you want to automatically distribute this bill amount to BOQ items based on the uploaded document?"); if (confirmItemize) { setIsAiLoading(true); const itemizedUpdates = await parseRunningBillDetails(detectedBillDocName, data.boq); onBillItemizedUpdate(itemizedUpdates); setIsAiLoading(false); alert(`Successfully mapped bill amount to ${itemizedUpdates.length} BOQ items.`); } } setIsBillModalOpen(false); setBillEntity(''); setBillAmount(''); setAiAutofilled(false); setDetectedBillDocName(''); setBillCategory('OTHER'); }; const handleBillTypeChange = (newType: Bill['type']) => { setBillType(newType); if (newType === 'MATERIAL_EXPENSE') setBillCategory('MATERIAL'); else if (newType === 'SUB_CONTRACTOR') setBillCategory('LABOR'); else setBillCategory('OTHER'); }; const saveRemarks = (id: string) => { onUpdatePDRemarks('BILL', id, tempRemarks); setEditingRemarksId(null); }; const BillTable = ({ bills, title }: { bills: typeof data.bills, title: string }) => (

{title}

{bills.map(bill => ( ))}
Bill ID Entity / Description Date Amount Status Notes
{bill.id} {bill.type === 'SUB_CONTRACTOR' &&
Sub-Contract
} {bill.type === 'MATERIAL_EXPENSE' &&
Material
} {bill.category && bill.category !== 'OTHER' && (
{bill.category}
)}
{bill.entityName} {bill.date} ৳{bill.amount.toLocaleString()} {bill.status} {editingRemarksId === bill.id ? (
setTempRemarks(e.target.value)} className="w-full text-xs border border-blue-300 rounded px-1 py-0.5 outline-none" autoFocus />
) : (
{bill.pdRemarks || (isDirector ? "Add note..." : "")} {isDirector && ( )}
)}
); return (

Financial Control

Track Bills, Costs, and Profitability

{canAddVendorBill && ( )} {canAddClientBill && ( )} {canAddClientBill && ( )}

Total Revenue

৳{totalRevenue.toLocaleString()}

Total Billed to Client

Total Expenses

৳{totalExpenses.toLocaleString()}

Vendor + Sub-contract + Materials

Material Inventory Value

৳{materialInventoryValue.toLocaleString()}

Asset Value in Stock

Accrued Profit

= 0 ? 'text-violet-700' : 'text-red-600'}`}> {totalOperationalProfit >= 0 ? '+' : ''}৳{totalOperationalProfit.toLocaleString()}

Work Value - Actual Expense

Live Item-Wise Cost Sheet

Auto-Synced with DPR
{liveItemStats.map((item) => { if (item.executedQty === 0) return null; const totalPL = item.stats.profit; const workDoneValue = item.executedQty * item.rate; const pendingBill = Math.max(0, workDoneValue - (item.billedAmount || 0)); return ( toggleRow(item.id)} > {expandedRow === item.id && ( )} ); })}
Description Selling Rate Material Exp. Sub-Contract Est. Labor Total Actual Billed (PE) Net Profit
{expandedRow === item.id ? : }
{item.description}
{item.id} • Qty: {item.executedQty.toLocaleString()} {item.unit}
৳{item.rate.toLocaleString()} {item.stats.materialExp > 0 ? `৳${item.stats.materialExp.toLocaleString()}` : '-'} {item.stats.subContractExp > 0 ? `৳${item.stats.subContractExp.toLocaleString()}` : '-'} {item.stats.directLaborExp > 0 ? `~৳${item.stats.directLaborExp.toLocaleString()}` : '-'} ৳{item.stats.totalActualCost.toLocaleString()} ৳{(item.billedAmount || 0).toLocaleString()} {pendingBill > 0 &&
Due: ৳{pendingBill.toLocaleString()}
}
= 0 ? 'text-emerald-600' : 'text-red-600'}`}> ৳{totalPL.toLocaleString()}

DPR Source Data Breakdown

Material Consumed
৳{item.stats.materialExp.toLocaleString()}
From Stock Out
Sub-Contract Work
৳{item.stats.subContractExp.toLocaleString()}
Based on Agreed Rate
Direct Labor
৳{item.stats.directLaborExp.toLocaleString()}
Daily Labor Count
Actual Unit Cost ৳{(item.stats.totalActualCost / item.executedQty).toFixed(2)}
Planned Unit Cost ৳{item.plannedUnitCost.toFixed(2)}
Variance = 0 ? 'text-emerald-600' : 'text-red-600'}`}> {item.plannedUnitCost - (item.stats.totalActualCost / item.executedQty) >= 0 ? 'Savings' : 'Overrun'}
{/* COST CENTER BREAKDOWN SECTION */}

Material Cost Breakdown

{materialStats.map(m => ( ))} {materialStats.length === 0 && ( )}
Material Avg Rate Qty Consumed Total Expense
{m.name} ৳{m.averageRate.toLocaleString()} {m.liveConsumedQty} {m.unit} ৳{m.liveExpense.toLocaleString()}
No consumption data

Sub-Contractor Reconciliation

{subContractorStats.map(sc => ( ))} {subContractorStats.length === 0 && ( )}
Sub-Contractor Work Value Billed Balance
{sc.name}
{sc.specialization}
৳{sc.liveWorkValue.toLocaleString()} ৳{sc.totalBilled.toLocaleString()} 0 ? 'text-red-600' : 'text-emerald-600'}`}> ৳{sc.balance.toLocaleString()} {sc.balance > 0 && }
No sub-contractors engaged
{isBillModalOpen && (

{billType === 'CLIENT_RA' ? 'Record Bill Received (PE)' : 'Add Expense / Invoice'}

{aiAutofilled && (
AI Auto-Filled
)}
{billType !== 'CLIENT_RA' && ( <>
)}
setBillEntity(e.target.value)} placeholder="e.g. ABC Constructions Ltd." className={`w-full px-3 py-2 border rounded-lg text-sm transition-all ${aiAutofilled ? 'border-emerald-200 bg-emerald-50/20' : 'border-slate-300'}`} />
setBillAmount(e.target.value)} className={`w-full px-3 py-2 border rounded-lg text-sm transition-all ${aiAutofilled ? 'border-emerald-200 bg-emerald-50/20' : 'border-slate-300'}`} />
setBillDate(e.target.value)} className={`w-full px-3 py-2 border rounded-lg text-sm transition-all ${aiAutofilled ? 'border-emerald-200 bg-emerald-50/20' : 'border-slate-300'}`} />
{billType === 'CLIENT_RA' && detectedBillDocName && (

AI Action: Upon saving, the system will read "{detectedBillDocName}" to automatically distribute the billed amount to individual BOQ items.

)}
)}
); }; export default FinancialControl;