import React, { useState, useMemo } from 'react'; import { ProjectState, ProjectDocument, DPR, UserRole, MaterialConsumption, Unit } from '../types'; import DocumentManager from './DocumentManager'; import { MapPin, Users, Calendar, PlusCircle, X, ClipboardCheck, Lock, Sparkles, Loader2, FileText, CheckCircle2, Package, ArrowDownLeft, ArrowUpRight, Edit2, Save, HardHat, BarChart3, AlertCircle, Mic } from 'lucide-react'; import { extractDPRData } from '../services/localAnalysisService'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, Cell } from 'recharts'; interface SiteExecutionProps { data: ProjectState; onAddDocument: (doc: ProjectDocument) => void; onAddDPR: (dpr: DPR) => void; onReceiveMaterial: (materialId: string, qty: number, rate?: number) => void; onUpdatePDRemarks: (type: 'MATERIAL' | 'SUBCONTRACTOR', id: string, remarks: string) => void; userRole: UserRole; } const SiteExecution: React.FC = ({ data, onAddDocument, onAddDPR, onReceiveMaterial, onUpdatePDRemarks, userRole }) => { const [isDprModalOpen, setIsDprModalOpen] = useState(false); const [isReceiveModalOpen, setIsReceiveModalOpen] = useState(false); const [isAiLoading, setIsAiLoading] = useState(false); const [aiPopulatedFields, setAiPopulatedFields] = useState>(new Set()); const [selectedReportId, setSelectedReportId] = useState(''); const [editingRemarksId, setEditingRemarksId] = useState(null); const [tempRemarks, setTempRemarks] = useState(''); const [storeView, setStoreView] = useState<'INVENTORY' | 'HISTORY'>('INVENTORY'); const canAddDPR = userRole === 'ENGINEER' || userRole === 'DIRECTOR'; const canUploadDoc = userRole === 'ENGINEER' || userRole === 'DIRECTOR'; const canManageStore = userRole === 'ENGINEER' || userRole === 'MANAGER' || userRole === 'DIRECTOR'; const isDirector = userRole === 'DIRECTOR'; // DPR Form State const [activityDate, setActivityDate] = useState(new Date().toISOString().split('T')[0]); const [activityDesc, setActivityDesc] = useState(''); const [location, setLocation] = useState(''); const [laborCount, setLaborCount] = useState(0); const [remarks, setRemarks] = useState(''); const [linkedBoqId, setLinkedBoqId] = useState(''); const [subContractorId, setSubContractorId] = useState(''); const [workDoneQty, setWorkDoneQty] = useState(0); const [materialsConsumed, setMaterialsConsumed] = useState([]); // Receive Material Form State const [receiveMatId, setReceiveMatId] = useState(''); const [receiveQty, setReceiveQty] = useState(''); const [receiveRate, setReceiveRate] = useState(''); const reportDocs = useMemo(() => data.documents .filter(d => d.category === 'REPORT') .sort((a, b) => new Date(b.uploadDate).getTime() - new Date(a.uploadDate).getTime()), [data.documents] ); // Prepare Data for Material Chart const stockChartData = useMemo(() => { return data.materials.map(m => ({ name: m.name.length > 12 ? m.name.substring(0, 10) + '..' : m.name, fullName: m.name, Received: m.totalReceived, Consumed: m.totalConsumed, unit: m.unit })); }, [data.materials]); // Derive Consumption History Log const consumptionHistory = useMemo(() => { const history: { id: string; date: string; materialName: string; qty: number; unit: string; activity: string }[] = []; data.dprs.forEach(dpr => { if (dpr.materialsUsed) { dpr.materialsUsed.forEach(usage => { const mat = data.materials.find(m => m.id === usage.materialId); if (mat) { history.push({ id: `${dpr.id}-${mat.id}`, date: dpr.date, materialName: mat.name, qty: usage.qty, unit: mat.unit, activity: dpr.activity }); } }); } }); return history.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); }, [data.dprs, data.materials]); const handleCreateDPR = (e: React.FormEvent) => { e.preventDefault(); // Validate Material Stock const invalidConsumption = materialsConsumed.find(c => { const mat = data.materials.find(m => m.id === c.materialId); return mat && c.qty > mat.currentStock; }); if (invalidConsumption) { const mat = data.materials.find(m => m.id === invalidConsumption.materialId); alert(`Insufficient stock for ${mat?.name}. Available: ${mat?.currentStock} ${mat?.unit}, Requested: ${invalidConsumption.qty} ${mat?.unit}. Please receive material first.`); return; } let finalDesc = activityDesc; if (linkedBoqId && !finalDesc) { const boqItem = data.boq.find(b => b.id === linkedBoqId); if (boqItem) finalDesc = boqItem.description; } const newDPR: DPR = { id: `DPR-${Date.now()}`, date: activityDate, activity: finalDesc || 'Site Activity', location: location || 'Site', laborCount: Number(laborCount), remarks, linkedBoqId: linkedBoqId || undefined, subContractorId: subContractorId || undefined, workDoneQty: Number(workDoneQty) > 0 ? Number(workDoneQty) : undefined, materialsUsed: materialsConsumed.filter(m => m.qty > 0) }; onAddDPR(newDPR); setIsDprModalOpen(false); resetForm(); }; const handleReceiveSubmit = (e: React.FormEvent) => { e.preventDefault(); if(receiveMatId && receiveQty) { onReceiveMaterial(receiveMatId, Number(receiveQty), receiveRate ? Number(receiveRate) : undefined); setIsReceiveModalOpen(false); setReceiveMatId(''); setReceiveQty(''); setReceiveRate(''); } }; const resetForm = () => { setActivityDesc(''); setLocation(''); setLaborCount(0); setRemarks(''); setLinkedBoqId(''); setSubContractorId(''); setWorkDoneQty(0); setAiPopulatedFields(new Set()); setSelectedReportId(''); setMaterialsConsumed([]); }; // Speech Recognition State const [isRecording, setIsRecording] = useState(false); const handleVoiceRecord = () => { const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition; if (!SpeechRecognition) { alert("Voice recognition is not supported in this browser."); return; } const recognition = new SpeechRecognition(); recognition.continuous = false; recognition.interimResults = false; recognition.lang = 'en-US'; recognition.onstart = () => { setIsRecording(true); }; recognition.onresult = async (event: any) => { setIsRecording(false); const transcript = event.results[0][0].transcript; if (transcript) { setIsAiLoading(true); // Extract DPR data locally from the voice transcript. const extracted = await extractDPRData("Voice Note", data.boq, transcript, 'text/plain'); handleExtractionResult(extracted); } }; recognition.onerror = (event: any) => { console.error("Speech recognition error", event.error); setIsRecording(false); if (event.error === 'not-allowed') { alert("Please grant microphone permissions to use voice dictation."); } else { // Fallback simulation for preview environment without mic access alert(`Microphone error: ${event.error}. Note: If inside an iframe without mic access, please open in a new tab.`); } }; recognition.start(); }; const handleExtractionResult = (extracted: any) => { setIsAiLoading(false); if (extracted) { const newPopulated = new Set(); if (extracted.date) { setActivityDate(extracted.date); newPopulated.add('date'); } if (extracted.activity) { setActivityDesc(extracted.activity); newPopulated.add('activity'); } if (extracted.location) { setLocation(extracted.location); newPopulated.add('location'); } if (extracted.laborCount) { setLaborCount(extracted.laborCount); newPopulated.add('labor'); } if (extracted.remarks) { setRemarks(extracted.remarks); newPopulated.add('remarks'); } if (extracted.linkedBoqId) { setLinkedBoqId(extracted.linkedBoqId); newPopulated.add('boq'); } if (extracted.workDoneQty) { setWorkDoneQty(extracted.workDoneQty); newPopulated.add('qty'); } if (extracted.subContractorName) { const match = data.subContractors?.find(s => s.name.toLowerCase().includes(extracted.subContractorName!.toLowerCase()) || extracted.subContractorName!.toLowerCase().includes(s.name.toLowerCase()) ); if (match) { setSubContractorId(match.id); newPopulated.add('subcontractor'); } } if (extracted.materials && extracted.materials.length > 0) { const mappedMaterials = extracted.materials.map((m: any) => { const match = data.materials.find(ex => ex.name.toLowerCase().includes(m.name.toLowerCase())); return match ? { materialId: match.id, qty: m.qty } : null; }).filter(Boolean) as MaterialConsumption[]; if (mappedMaterials.length > 0) { setMaterialsConsumed(mappedMaterials); newPopulated.add('materials'); } } setAiPopulatedFields(newPopulated); } }; const handleAiAutoFill = async () => { const reportToAnalyze = selectedReportId ? reportDocs.find(d => d.id === selectedReportId) : reportDocs[0]; if (!reportToAnalyze) { alert("Please upload or select a site report to analyze."); return; } setIsAiLoading(true); const extracted = await extractDPRData(reportToAnalyze.name, data.boq); handleExtractionResult(extracted); }; const addConsumptionRow = () => { if (data.materials.length > 0) { setMaterialsConsumed([...materialsConsumed, { materialId: data.materials[0].id, qty: 0 }]); } }; const updateConsumption = (index: number, field: keyof MaterialConsumption, value: any) => { const updated = [...materialsConsumed]; updated[index] = { ...updated[index], [field]: value }; setMaterialsConsumed(updated); }; const removeConsumptionRow = (index: number) => { setMaterialsConsumed(materialsConsumed.filter((_, i) => i !== index)); }; const saveRemarks = (type: 'MATERIAL' | 'SUBCONTRACTOR', id: string) => { onUpdatePDRemarks(type, id, tempRemarks); setEditingRemarksId(null); }; return (

Site Execution

Track Progress, Remaining Works & Daily Reports

{canAddDPR ? ( ) : (
Read Only (Role: {userRole})
)}
{/* Site Store Section */}

Site Store

{canManageStore && ( )}
{storeView === 'INVENTORY' ? ( <> {/* Material Chart */} {stockChartData.length > 0 && (
[`${value.toLocaleString()} ${props.payload.unit}`, name]} labelFormatter={(label) => { const item = stockChartData.find(d => d.name === label); return item ? item.fullName : label; }} />
)}
{data.materials.map(mat => (

{mat.name}

{mat.unit}
Total Recv {mat.totalReceived.toLocaleString()}
Consumed {mat.totalConsumed.toLocaleString()}
Stock {mat.currentStock.toLocaleString()}
{/* Director Remarks */}
{editingRemarksId === mat.id ? (
setTempRemarks(e.target.value)} className="w-full text-[10px] border border-indigo-300 rounded px-1 py-0.5 outline-none" autoFocus />
) : (

{mat.pdRemarks ? `Note: ${mat.pdRemarks}` : (isDirector ? "Add PD Note..." : "")}

{isDirector && ( )}
)}
))} {data.materials.length === 0 && (
No materials tracked.
)}
) : (
{consumptionHistory.map(row => ( ))} {consumptionHistory.length === 0 && ( )}
Date Material Qty Activity
{row.date} {row.materialName} {row.qty} {row.unit} {row.activity}
No consumption records found in DPRs.
)}
{/* Engaged Sub-Contractors Section */}

Engaged Sub-Contractors

Automated from Progress
{data.subContractors && data.subContractors.map(sc => (

{sc.name}

{sc.specialization}

Due: ৳{sc.currentLiability.toLocaleString()}
Work Done Value ৳{sc.totalWorkValue.toLocaleString()}
Paid / Billed ৳{sc.totalBilled.toLocaleString()}
{/* Director Remarks */}
{editingRemarksId === sc.id ? (
setTempRemarks(e.target.value)} className="w-full text-[10px] border border-orange-300 rounded px-1 py-0.5 outline-none" autoFocus />
) : (

{sc.pdRemarks ? `Note: ${sc.pdRemarks}` : (isDirector ? "Add PD Note..." : "")}

{isDirector && ( )}
)}
))} {(!data.subContractors || data.subContractors.length === 0) && (
No sub-contractors engaged.
)}

Physical Progress & Remaining Works

Live from Site
{data.boq.map((item) => { const percent = Math.min(100, Math.round((item.executedQty / item.plannedQty) * 100)); const remaining = Math.max(0, item.plannedQty - item.executedQty); return ( ); })}
Item Description Planned Qty Executed Qty Remaining Qty Progress % Status
{item.description}
ID: {item.id}
{item.plannedQty.toLocaleString()} {item.unit} {item.executedQty.toLocaleString()} {item.unit} {remaining.toLocaleString()} {item.unit}
{percent}%
= 100 ? 'bg-emerald-500' : 'bg-blue-600'}`} style={{ width: `${percent}%` }}>
{percent >= 100 ? ( Completed ) : percent > 0 ? ( In Progress ) : ( Pending )}
{/* DPRs */}

Daily Progress Reports (DPR) Log

{data.dprs.map((dpr) => (

{dpr.activity}

#{dpr.id}
{dpr.date}
{dpr.location}
{dpr.laborCount} Workers
{dpr.workDoneQty && dpr.linkedBoqId && (
+ {dpr.workDoneQty} Work Done Added
)} {dpr.subContractorId && (
Engaged: {data.subContractors?.find(s => s.id === dpr.subContractorId)?.name}
)} {dpr.materialsUsed && dpr.materialsUsed.length > 0 && (
{dpr.materialsUsed.map((usage, idx) => { const matName = data.materials.find(m => m.id === usage.materialId)?.name || usage.materialId; return ( Consumed: {usage.qty} {data.materials.find(m => m.id === usage.materialId)?.unit} of {matName} ); })}
)} {dpr.remarks && (
"{dpr.remarks}"
)}
))} {data.dprs.length === 0 && (
No daily reports logged yet.
)}
{/* Documents */}
{/* Receive Material Modal */} {isReceiveModalOpen && (

Receive Material (Inward)

setReceiveQty(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm outline-none" placeholder="e.g. 500" />
setReceiveRate(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm outline-none" placeholder="Update Unit Price" />
)} {/* DPR Modal */} {isDprModalOpen && (

Add Daily Progress Report

AI Smart Auto-Fill
{isAiLoading && (
Analyzing Report...
)}
{aiPopulatedFields.size > 0 && (
Successfully updated {aiPopulatedFields.size} fields
)}
{ setActivityDate(e.target.value); const next = new Set(aiPopulatedFields); next.delete('date'); setAiPopulatedFields(next); }} className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm transition-all ${aiPopulatedFields.has('date') ? 'border-indigo-300 bg-indigo-50/30' : 'border-slate-300'}`} />
{ setLaborCount(Number(e.target.value)); const next = new Set(aiPopulatedFields); next.delete('labor'); setAiPopulatedFields(next); }} className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm transition-all ${aiPopulatedFields.has('labor') ? 'border-indigo-300 bg-indigo-50/30' : 'border-slate-300'}`} />
{(linkedBoqId || aiPopulatedFields.has('qty')) && (
{ setWorkDoneQty(Number(e.target.value)); const next = new Set(aiPopulatedFields); next.delete('qty'); setAiPopulatedFields(next); }} className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm transition-all ${aiPopulatedFields.has('qty') ? 'border-indigo-300 bg-white' : 'border-blue-200 bg-white'}`} /> {data.boq.find(b => b.id === linkedBoqId)?.unit}
{subContractorId && linkedBoqId && (
* Liability will be automatically created: ৳{(Number(workDoneQty) * (data.subContractors?.find(s => s.id === subContractorId)?.agreedRates.find(r => r.boqId === linkedBoqId)?.rate || 0)).toLocaleString()}
)}
)} {/* Material Consumption Section */}
{aiPopulatedFields.has('materials') && }
{materialsConsumed.map((row, idx) => { const mat = data.materials.find(m => m.id === row.materialId); const isError = mat ? row.qty > mat.currentStock : false; return (
updateConsumption(idx, 'qty', Number(e.target.value))} className={`w-full text-xs border rounded px-2 py-1.5 ${isError ? 'border-red-500 bg-red-50' : (aiPopulatedFields.has('materials') ? 'border-indigo-200 bg-indigo-50/20' : 'border-slate-300')}`} placeholder="Qty" /> {isError && Over Limit}
{mat?.unit}
); })} {materialsConsumed.length === 0 &&

No materials added.

}
{ setActivityDesc(e.target.value); const next = new Set(aiPopulatedFields); next.delete('activity'); setAiPopulatedFields(next); }} placeholder={linkedBoqId ? "Auto-filled from BOQ if empty" : "e.g., Site Clearing, Mobilization"} className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm transition-all ${aiPopulatedFields.has('activity') ? 'border-indigo-300 bg-indigo-50/30' : 'border-slate-300'}`} />
{ setLocation(e.target.value); const next = new Set(aiPopulatedFields); next.delete('location'); setAiPopulatedFields(next); }} placeholder="e.g., Chainage 10+500" className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm transition-all ${aiPopulatedFields.has('location') ? 'border-indigo-300 bg-indigo-50/30' : 'border-slate-300'}`} />