import React, { useState, useEffect } from 'react'; import { NotificationProvider } from './contexts/NotificationContext'; import Layout from './components/Layout'; import Dashboard from './components/Dashboard'; import MasterControl from './components/MasterControl'; import SiteExecution from './components/SiteExecution'; import FinancialControl from './components/FinancialControl'; import LiabilityTracker from './components/LiabilityTracker'; import DocumentManager from './components/DocumentManager'; import ProjectList from './components/ProjectList'; import Auth from './components/Auth'; import TaskManager from './components/TaskManager'; import MemberManager from './components/MemberManager'; import GanttChart from './components/GanttChart'; import FinancialAnalytics from './components/FinancialAnalytics'; import Procurement from './components/Procurement'; import SubcontractorPortal from './components/SubcontractorPortal'; import QCSafety from './components/QCSafety'; import Reporting from './components/Reporting'; import PhotoLogs from './components/PhotoLogs'; import EquipmentManager from './components/EquipmentManager'; import AttendanceManager from './components/AttendanceManager'; import ChangeOrderManager from './components/ChangeOrderManager'; import SustainabilityTracker from './components/SustainabilityTracker'; import BimViewer from './components/BimViewer'; import ClientPortal from './components/ClientPortal'; import VendorAnalytics from './components/VendorAnalytics'; import LocalAssistant from './components/LocalAssistant'; import { CommentSection } from './components/Collaboration'; import { MOCK_PROJECTS } from './constants'; import { ProjectState, ProjectDocument, DPR, UserRole, BOQItem, AiSuggestion, Material, Bill, ExtractedDPR, User, Task } from './types'; import { parseBOQDocument, analyzeDocumentContent, processWhatsAppMessage } from './services/localAnalysisService'; import { MessageSquare, Send, Loader2, Smartphone, AlertCircle, LayoutDashboard, PlusCircle } from 'lucide-react'; import { useLocalCollection } from './hooks/useLocalCollection'; const stripUndefined = (obj: any): any => { if (Array.isArray(obj)) return obj.map(stripUndefined); if (typeof obj === 'object' && obj !== null) { const res: any = {}; for (const key in obj) { if (obj[key] !== undefined) res[key] = stripUndefined(obj[key]); } return res; } return obj; }; const App: React.FC = () => { const [user, setUser] = useState(null); const [isAuthReady, setIsAuthReady] = useState(false); const [activeTab, setActiveTab] = useState('dashboard'); const authRefreshKey = user?.uid || localStorage.getItem('auth_token') || 'guest'; const { data: projectsData, add: addProject, update: updateProjectStorage } = useLocalCollection('projects', authRefreshKey); const [activeProjectId, setActiveProjectId] = useState(null); const [activeProjectRole, setActiveProjectRole] = useState(null); const [activeProjectTasks, setActiveProjectTasks] = useState([]); const [activeProjectMembers, setActiveProjectMembers] = useState([]); const [isSimulatingWhatsApp, setIsSimulatingWhatsApp] = useState(false); const [whatsappMessage, setWhatsappMessage] = useState(''); const [selectedDocId, setSelectedDocId] = useState(null); // Connection Test omitted since Firebase is removed // Auth Listener replaced with short initialization useEffect(() => { setIsAuthReady(true); }, []); // Sync Projects const projects = projectsData; useEffect(() => { if (projects.length === 0 && user) { MOCK_PROJECTS.forEach(p => { addProject({ ...p, ownerUid: user.uid } as any); }); } }, [user, projects.length, addProject]); // Project Role Listener useEffect(() => { if (user && activeProjectId) { // Local mode: give them the role they logged in with for everything setActiveProjectRole(user.role); } }, [user, activeProjectId]); useEffect(() => { if (user) { setActiveProjectMembers([user]); } }, [user]); useEffect(() => { if (!activeProjectId && projects.length > 0) { setActiveProjectId(projects[0].id); } }, [activeProjectId, projects]); // Tasks local fetch useEffect(() => { const fetchTasks = async () => { try { const token = localStorage.getItem('auth_token'); const res = await fetch(`/api/collections/tasks_${activeProjectId}`, { headers: token ? { 'Authorization': `Bearer ${token}` } : {} }); if (!res.ok) throw new Error("Tasks fetch failed"); const data = await res.json(); setActiveProjectTasks(data || []); } catch (e) { console.error("Failed to fetch tasks", e); } }; if (activeProjectId) { fetchTasks(); } }, [activeProjectId]); const activeProject = projects.find(p => p.id === activeProjectId); const handleLogout = async () => { localStorage.removeItem('local_user_uid'); localStorage.removeItem('auth_token'); setUser(null); setActiveProjectId(null); }; const handleCreateProject = async (newProject: Partial) => { if (!user) return; const id = `P${Date.now()}`; const project: ProjectState = { ...newProject as ProjectState, id, ownerUid: user.uid, memberUids: [user.uid], aiSuggestions: [], materials: [], subContractors: [], documents: [], dprs: [], boq: [], bills: [], liabilities: [], milestones: [], purchaseOrders: [], qualityChecks: [], safetyChecks: [], photoLogs: [], equipment: [], attendance: [], changeOrders: [], vendors: [], weatherForecast: [], bimModels: [] }; addProject(stripUndefined(project)); setActiveProjectId(id); }; const handleUpdateProject = async (projectId: string, updater: (proj: ProjectState) => ProjectState) => { const project = projects.find(p => p.id === projectId); if (!project) return; const updated = updater(project); updateProjectStorage(projectId, stripUndefined(updated)); }; const handleAddDocument = async (newDoc: ProjectDocument) => { if (!activeProjectId || !activeProject) return; // 1. Add Document immediately handleUpdateProject(activeProjectId, (project) => ({ ...project, documents: [newDoc, ...project.documents] })); // 2. Trigger Auto-Analysis based on Doc Type try { let mimeType = 'application/pdf'; if (newDoc.type === 'JPG' || newDoc.type === 'PNG') mimeType = 'image/jpeg'; const suggestions = await analyzeDocumentContent(newDoc.name, newDoc.category, activeProject.boq, newDoc.content, mimeType); if (suggestions && suggestions.length > 0) { handleUpdateProject(activeProjectId, (project) => ({ ...project, documents: project.documents.map(d => d.id === newDoc.id ? { ...d, isAnalyzed: true } : d), aiSuggestions: [...suggestions.map(s => ({ ...s, docId: newDoc.id })), ...project.aiSuggestions] })); } } catch (e) { console.error("Auto-analysis failed", e); } }; const handleAnalyzeDocument = (docId: string, suggestions: AiSuggestion[]) => { if (!activeProjectId) return; handleUpdateProject(activeProjectId, (project) => ({ ...project, documents: project.documents.map(d => d.id === docId ? { ...d, isAnalyzed: true } : d), aiSuggestions: [...suggestions, ...project.aiSuggestions] })); setActiveTab('dashboard'); // Switch to dashboard to see results }; const handleImportBOQItems = (items: BOQItem[]) => { if (!activeProjectId) return; handleUpdateProject(activeProjectId, (project) => ({ ...project, boq: [...project.boq, ...items] // Append new items. In real app, this might merge or replace. })); }; const handleApplySuggestion = async (suggestionId: string) => { if (!activeProjectId || !activeProject) return; const suggestion = activeProject.aiSuggestions.find(s => s.id === suggestionId); if (!suggestion) return; if (suggestion.type === 'BOQ_IMPORT') { const relatedDoc = activeProject.documents.find(d => d.id === suggestion.docId); if (relatedDoc) { let mimeType = 'application/pdf'; if (relatedDoc.type === 'JPG' || relatedDoc.type === 'PNG') mimeType = 'image/jpeg'; const items = await parseBOQDocument(relatedDoc.name, relatedDoc.content, mimeType); handleImportBOQItems(items); } handleUpdateProject(activeProjectId, (project) => ({ ...project, aiSuggestions: project.aiSuggestions.map(s => s.id === suggestionId ? { ...s, status: 'APPLIED' as const } : s) })); return; } if (suggestion.type === 'DPR_ENTRY' && suggestion.value) { const dprData = suggestion.value as ExtractedDPR; // Resolve IDs let subId = undefined; if (dprData.subContractorName) { subId = activeProject.subContractors?.find(s => s.name.toLowerCase().includes(dprData.subContractorName!.toLowerCase()) )?.id; } let materialsUsed = []; if (dprData.materials) { materialsUsed = dprData.materials.map(m => { const mat = activeProject.materials.find(ex => ex.name.toLowerCase().includes(m.name.toLowerCase())); return mat ? { materialId: mat.id, qty: m.qty } : null; }).filter(Boolean) as any; } const newDPR: DPR = { id: `DPR-AI-${Date.now()}`, date: dprData.date || new Date().toISOString().split('T')[0], activity: dprData.activity || 'Reported Activity', location: dprData.location || 'Site', laborCount: dprData.laborCount || 0, remarks: dprData.remarks || '', linkedBoqId: dprData.linkedBoqId, workDoneQty: dprData.workDoneQty, subContractorId: subId, materialsUsed: materialsUsed }; handleAddDPR(newDPR); handleUpdateProject(activeProjectId, (project) => ({ ...project, aiSuggestions: project.aiSuggestions.map(s => s.id === suggestionId ? { ...s, status: 'APPLIED' as const } : s) })); return; } handleUpdateProject(activeProjectId, (project) => { let updatedProject = { ...project }; // Update data based on suggestion type if (suggestion.type === 'QUANTITY_UPDATE' && suggestion.linkedId && suggestion.value) { updatedProject.boq = project.boq.map(b => b.id === suggestion.linkedId ? { ...b, executedQty: b.executedQty + suggestion.value } : b); } else if (suggestion.type === 'BILL_DETECTION' && suggestion.value) { const billVal = suggestion.value as any; // could be object or number const amount = typeof billVal === 'object' ? billVal.amount : billVal; const newBill = { id: `BILL-AI-${Date.now()}`, type: 'VENDOR_INVOICE' as const, entityName: suggestion.title.split('from ')[1] || 'Unknown Vendor', amount: Number(amount), date: new Date().toISOString().split('T')[0], status: 'PENDING' as const }; updatedProject.bills = [newBill, ...project.bills]; } updatedProject.aiSuggestions = project.aiSuggestions.map(s => s.id === suggestionId ? { ...s, status: 'APPLIED' as const } : s); return updatedProject; }); }; const handleDismissSuggestion = (suggestionId: string) => { if (!activeProjectId) return; handleUpdateProject(activeProjectId, (project) => ({ ...project, aiSuggestions: project.aiSuggestions.map(s => s.id === suggestionId ? { ...s, status: 'DISMISSED' as const } : s) })); }; const handleAddDPR = (newDPR: DPR) => { if (!activeProjectId) return; handleUpdateProject(activeProjectId, (project) => { const updatedDPRs = [newDPR, ...project.dprs]; let updatedBOQ = project.boq; let updatedSubContractors = project.subContractors; let updatedLiabilities = project.liabilities; // 1. Update BOQ Executed Qty if (newDPR.linkedBoqId && newDPR.workDoneQty) { updatedBOQ = project.boq.map(item => { if (item.id === newDPR.linkedBoqId) { return { ...item, executedQty: item.executedQty + (newDPR.workDoneQty || 0) }; } return item; }); // 2. Automated Sub-contractor Progress Tracking if (newDPR.subContractorId && newDPR.workDoneQty) { const sub = project.subContractors.find(s => s.id === newDPR.subContractorId); if (sub) { // Find agreed rate for this BOQ item const rateInfo = sub.agreedRates.find(r => r.boqId === newDPR.linkedBoqId); const rate = rateInfo ? rateInfo.rate : 0; const workValue = newDPR.workDoneQty * rate; if (workValue > 0) { // Update SC stats updatedSubContractors = project.subContractors.map(s => { if (s.id === sub.id) { return { ...s, totalWorkValue: s.totalWorkValue + workValue, currentLiability: s.currentLiability + workValue }; } return s; }); // Create Liability Entry automatically const newLiability = { id: `L-AUTO-${Date.now()}`, description: `Unbilled Work: ${sub.name} (${newDPR.date})`, type: 'UNBILLED_WORK' as const, amount: workValue, dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] // Net 30 default }; updatedLiabilities = [newLiability, ...project.liabilities]; } } } } // 3. Update Material Stock let updatedMaterials = project.materials; if (newDPR.materialsUsed && newDPR.materialsUsed.length > 0) { updatedMaterials = project.materials.map(mat => { const used = newDPR.materialsUsed?.find(u => u.materialId === mat.id); if (used) { return { ...mat, totalConsumed: mat.totalConsumed + used.qty, currentStock: mat.currentStock - used.qty }; } return mat; }); } return { ...project, dprs: updatedDPRs, boq: updatedBOQ, materials: updatedMaterials, subContractors: updatedSubContractors, liabilities: updatedLiabilities }; }); }; const handleReceiveMaterial = (materialId: string, receivedQty: number, newRate?: number) => { if (!activeProjectId) return; handleUpdateProject(activeProjectId, (project) => ({ ...project, materials: project.materials.map(mat => { if (mat.id === materialId) { const newTotalReceived = mat.totalReceived + receivedQty; const newStock = mat.currentStock + receivedQty; // Weighted Average Rate Calculation const oldVal = mat.currentStock * mat.averageRate; const newVal = receivedQty * (newRate || mat.averageRate); const newAvgRate = (oldVal + newVal) / newStock; return { ...mat, totalReceived: newTotalReceived, currentStock: newStock, averageRate: newRate ? newAvgRate : mat.averageRate }; } return mat; }) })); }; const handleAddBill = (newBill: Bill) => { if (!activeProjectId) return; handleUpdateProject(activeProjectId, (project) => ({ ...project, bills: [newBill, ...project.bills] })); }; const handleBillItemizedUpdate = (items: { boqId: string; amount: number }[]) => { if (!activeProjectId) return; handleUpdateProject(activeProjectId, (project) => ({ ...project, boq: project.boq.map(b => { const update = items.find(i => i.boqId === b.id); if (update) { return { ...b, billedAmount: (b.billedAmount || 0) + update.amount }; } return b; }) })); }; const handleUpdatePDRemarks = (entityType: 'MATERIAL' | 'BILL' | 'DPR' | 'SUBCONTRACTOR', entityId: string, remarks: string) => { if (!activeProjectId) return; handleUpdateProject(activeProjectId, (project) => { if (entityType === 'MATERIAL') { return { ...project, materials: project.materials.map(m => m.id === entityId ? { ...m, pdRemarks: remarks } : m) }; } if (entityType === 'BILL') { return { ...project, bills: project.bills.map(b => b.id === entityId ? { ...b, pdRemarks: remarks } : b) }; } if (entityType === 'SUBCONTRACTOR') { return { ...project, subContractors: project.subContractors.map(s => s.id === entityId ? { ...s, pdRemarks: remarks } : s) }; } return project; }); }; const handleAddBOQItem = (newItem: BOQItem) => { if (!activeProjectId) return; handleUpdateProject(activeProjectId, (project) => ({ ...project, boq: [...project.boq, newItem] })); }; const handleUpdateBOQItem = (itemId: string, updatedItem: Partial) => { if (!activeProjectId) return; handleUpdateProject(activeProjectId, (project) => ({ ...project, boq: project.boq.map(item => item.id === itemId ? { ...item, ...updatedItem } : item) })); }; const handleSimulateWhatsApp = async () => { if (!activeProjectId || !activeProject || !whatsappMessage.trim()) return; setIsSimulatingWhatsApp(true); try { const extracted = await processWhatsAppMessage(whatsappMessage, activeProject.boq); if (extracted) { // Create an AI Suggestion based on WhatsApp message const newSuggestion: AiSuggestion = { id: `WA-SUG-${Date.now()}`, docId: 'WHATSAPP', type: 'DPR_ENTRY', title: 'WhatsApp Progress Update', description: `Extracted from message: "${whatsappMessage.substring(0, 50)}..."`, value: extracted, status: 'PENDING' }; handleUpdateProject(activeProjectId, (project) => ({ ...project, aiSuggestions: [newSuggestion, ...project.aiSuggestions] })); setWhatsappMessage(''); setActiveTab('dashboard'); } } catch (err) { console.error("WhatsApp simulation failed", err); } finally { setIsSimulatingWhatsApp(false); } }; if (!isAuthReady) { return (
); } if (!user) { return ; } if (!activeProject) { return (
{}} // Disabled for real users />
); } const renderContent = () => { if (!activeProjectId || !activeProject) { return (

No Project Selected

Please select a project from the sidebar to view its dashboard and manage construction activities.

); } switch (activeTab) { case 'dashboard': return ( handleUpdateProject(activeProjectId, updater)} /> ); case 'master': return ; case 'site': return ; case 'finance': return ; case 'analytics': return (

Sustainability & Waste Tracking

); case 'procurement': return (

Vendor Performance

); case 'equipment': return ; case 'labor': return ; case 'subcontractors': return ; case 'qc-safety': return ; case 'gantt': return ; case 'bim': return ; case 'photos': return ; case 'reports': return ; case 'client': return ; case 'liability': return ; case 'documents': return (
{selectedDocId && (
)}
); case 'tasks': return (
); case 'team': return (
); default: return handleUpdateProject(activeProjectId, updater)} />; } }; return ( setActiveProjectId(null)} projectName={activeProject.name} user={{ ...user, role: activeProjectRole || user.role }} onLogout={handleLogout} >
{renderContent()}
{/* Collaboration Sidebar */}

WhatsApp DPR Simulation

Paste a message from your site WhatsApp group to automatically extract progress data.