diff --git a/src/App.jsx b/src/App.jsx index 9362115..1692295 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -17,6 +17,8 @@ import OwnerSnapshot from './pages/owner/OwnerSnapshot'; import PeopleDirectory from './pages/owner/PeopleDirectory'; import VendorDirectory from './pages/owner/VendorDirectory'; import DocumentManagement from './pages/owner/DocumentManagement'; +import OwnerProjectList from './pages/owner/OwnerProjectList'; +import OwnerProjectDetail from './pages/owner/OwnerProjectDetail'; import PlaceholderDashboard from './pages/PlaceholderDashboard'; import ProjectList from './pages/contractor/ProjectList'; import AiAssistantPage from './pages/AiAssistantPage'; @@ -107,11 +109,19 @@ function App() { } /> + + + + } /> + + + + } /> -
- Detailed Market Intelligence Map (Coming Soon) -
+ } /> @@ -141,6 +151,14 @@ function App() { } /> + + + + } + /> {/* Contractor Routes */} { + const { meetings, properties, salesHistory, users, vendors, personnel, documents, projects, owners, orders, vendorInvoices } = store; + const TODAY = '2026-02-18'; + const todayDate = new Date(TODAY); -COMPANY INFO: -- **Phone**: 866-259-6533 -- **Experience**: 20+ years of craftsmanship. -- **Offer**: First 3 diagnostic exams are FREE. -- **Discounts**: 15% off for Veterans & Seniors. + const BASE = `You are the LynkedUp Pro AI Assistant — an intelligent operations concierge for a construction & roofing management platform. +Company: LynkedUp Pro | Phone: 866-259-6533 | 20+ years experience | Free first 3 diagnostic exams | 15% off Veterans & Seniors. +Today's Date: ${TODAY}. Tone: Professional, data-driven, concise. Use markdown formatting. Never fabricate data — only reference what's provided below. -TONE: Professional, efficient, and data-aware. `; -const getEmployeeContext = (user, meetings, properties, salesHistory, users) => { - // --- ADMIN SPECIFIC CONTEXT --- + if (!user) { + // Guest / unauthenticated + return BASE + `ROLE: EXPERT ROOFING CONSULTANT (Guest) + +KNOWLEDGE BASE: +- Active Leak: Water saturates insulation/decking → mold in 24-48h. Urgency: map moisture path immediately. +- Missing Shingles: Underlayment exposed to UV → destroyed in days. Risk: insurance may deny for "neglect". +- Hail Dents: Fractured fiberglass mat → reduces roof life 3-5 years. Document now or insurance calls it "wear and tear". +- Granule Loss: Asphalt baking in Texas sun → roof failure in 12-18 months. +- Ice Dams: Heat loss from attic → assess intake vents, not just patch. +- Sagging: Structural fatigue or long-term water intrusion → do NOT walk on area. + +STRATEGY: Validate observation → explain hidden consequence of waiting → offer zero-risk solution (free inspection). +HOOK: "Insurance adjusters use specific criteria. We know exactly what they look for." | "We're in Plano — project manager can stop by in 30 minutes for free moisture check." +Contact: 866-259-6533`; + } + + // ──────────────── OWNER ──────────────── + if (user.role === 'OWNER') { + const ownerProfile = (owners || []).find(o => o.id === user.id); + const myProjects = projects.filter(p => p.ownerId === user.id); + const activeProj = myProjects.filter(p => p.status === 'active'); + const completedProj = myProjects.filter(p => p.status === 'completed'); + const onHoldProj = myProjects.filter(p => p.status === 'on_hold'); + const delayedProj = myProjects.filter(p => p.status === 'delayed'); + const totalBudget = myProjects.reduce((s, p) => s + (p.budget || 0), 0); + const totalSpent = myProjects.reduce((s, p) => s + (p.spent || 0), 0); + const pendingInvoices = myProjects.reduce((s, p) => s + (p.invoices || []).filter(i => i.status === 'pending').reduce((is, i) => is + i.amount, 0), 0); + + // Vendor compliance overview (owners see all vendors) + const activeVendors = vendors.filter(v => v.status === 'active'); + const nonCompliant = vendors.filter(v => v.status !== 'active'); + const vendorSummary = vendors.slice(0, 6).map(v => { + const coiStatus = v.compliance?.coi?.status || 'unknown'; + const w9Status = v.compliance?.w9?.status || 'unknown'; + return ` - ${v.vendorName}: COI=${coiStatus}, W9=${w9Status}, Rating=${v.performance?.rating || 'N/A'}/5, Spend=$${(v.spend?.totalSpend || 0).toLocaleString()}`; + }).join('\n'); + + // Documents overview + const pendingDocs = documents.filter(d => d.status === 'pending_review').length; + const expiredDocs = documents.filter(d => d.status === 'expired').length; + const expiringSoon = documents.filter(d => { + if (!d.expirationDate || d.status === 'expired') return false; + const diff = (new Date(d.expirationDate) - todayDate) / (1000 * 60 * 60 * 24); + return diff > 0 && diff <= 30; + }).length; + + // Personnel overview + const activeStaff = personnel.filter(p => p.status === 'active').length; + const totalStaff = personnel.length; + + // Project details + const projDetails = myProjects.map(p => { + const variance = p.budget > 0 ? Math.round(((p.spent - p.budget) / p.budget) * 100) : 0; + const nextMs = (p.milestones || []).find(m => m.status !== 'completed'); + return ` - ${p.address} (${p.projectType}): ${p.status.toUpperCase()}, ${p.completionPercentage || 0}% done, Budget $${p.budget.toLocaleString()}, Spent $${p.spent.toLocaleString()} (${variance > 0 ? '+' : ''}${variance}%), Next: ${nextMs ? `${nextMs.name} by ${nextMs.dueDate}` : 'N/A'}`; + }).join('\n'); + + // Upcoming milestones across all owner projects + const upcomingMs = myProjects.flatMap(p => (p.milestones || []).filter(m => { + const diff = (new Date(m.dueDate) - todayDate) / (1000 * 60 * 60 * 24); + return diff >= -3 && diff <= 30 && m.status !== 'completed'; + }).map(m => ({ ...m, address: p.address }))).sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate)); + + const msText = upcomingMs.slice(0, 8).map(m => ` - ${m.name} @ ${m.address} — due ${m.dueDate} (${m.status})`).join('\n'); + + const dashSummary = ownerProfile?.dashboardSummary; + const riskScore = dashSummary?.riskScore ?? ownerProfile?.riskScore ?? 'N/A'; + const roiProjection = dashSummary?.roiProjection ?? 'N/A'; + + return BASE + `ROLE: OWNER — ${user.name} (${ownerProfile?.companyName || user.companyName || 'N/A'}) + +EXECUTIVE SUMMARY: +- Total Projects: ${myProjects.length} (Active: ${activeProj.length}, Completed: ${completedProj.length}, On Hold: ${onHoldProj.length}, Delayed: ${delayedProj.length}) +- Total Budget: $${totalBudget.toLocaleString()} | Total Spent: $${totalSpent.toLocaleString()} | Remaining: $${(totalBudget - totalSpent).toLocaleString()} +- Pending Invoice Payouts: $${pendingInvoices.toLocaleString()} +- Risk Score: ${riskScore}/100 (lower is better) | ROI Projection: ${roiProjection}% +- Active Staff: ${activeStaff}/${totalStaff} + +MY PROJECTS: +${projDetails || ' None.'} + +UPCOMING MILESTONES (Next 30 Days): +${msText || ' None due.'} + +VENDOR COMPLIANCE (Cross-Role: Owner sees ALL vendors): +- Active: ${activeVendors.length} | Non-Compliant: ${nonCompliant.length} +${vendorSummary} + +DOCUMENT STATUS: +- Pending Review: ${pendingDocs} | Expired: ${expiredDocs} | Expiring Soon (30d): ${expiringSoon} + +INSTRUCTIONS: +- You are the Business Intelligence Architect for this owner. +- Answer questions about projects, budgets, vendor compliance, documents, personnel, and strategic risks. +- When asked "which vendor is non-compliant?", list them by name with specific issues (COI/W9 status). +- When asked about a specific project, provide detailed breakdown from the data above. +- PRIORITIZE RISK ALERTS when compliance issues or over-budget projects exist.`; + } + + // ──────────────── ADMIN ──────────────── if (user.role === 'ADMIN') { - const unassignedCount = properties.filter(p => !p.assignedAgentId && p.canvassingStatus === 'Lead').length; - // Last contact > 24h ago logic - const hotLeadCount = properties.filter(p => p.canvassingStatus === 'Hot Lead' && (!p.lastContactDate || new Date(p.lastContactDate) < new Date(Date.now() - 86400000))).length; - const rescheduleCount = meetings.filter(m => m.status === 'Cancelled' || m.status === 'Missed').length; - const signatureCount = properties.filter(p => p.pendingSignature).length; - const totalUrgent = unassignedCount + hotLeadCount + rescheduleCount + signatureCount; + // Urgent action center + const unassigned = properties.filter(p => !p.assignedAgentId && p.canvassingStatus === 'Lead').length; + const hotLeadCount = properties.filter(p => p.canvassingStatus === 'Hot Lead').length; + const rescheduleNeeded = meetings.filter(m => m.status === 'Cancelled' || m.status === 'Missed').length; + const pendingSigs = properties.filter(p => p.pendingSignature).length; - // --- LEADERBOARD SNAPSHOT (Current Month) --- - const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1); - - // Aggregate Sales by Agent + // Leaderboard const agentStats = {}; salesHistory.forEach(tx => { - if (new Date(tx.date) >= startOfMonth && tx.status === 'closed_won') { + if (tx.status === 'closed_won') { if (!agentStats[tx.agentId]) agentStats[tx.agentId] = { revenue: 0, volume: 0 }; agentStats[tx.agentId].revenue += tx.amount; agentStats[tx.agentId].volume += 1; } }); + const leaderboard = Object.entries(agentStats).map(([id, stats]) => { + const agent = users.find(u => u.id === id); + return { name: agent?.name || id, ...stats }; + }).sort((a, b) => b.revenue - a.revenue).slice(0, 5); + const lbText = leaderboard.map((s, i) => ` ${i + 1}. ${s.name} — $${s.revenue.toLocaleString()} (${s.volume} deals)`).join('\n'); - // Convert to Array & Sort - const sortedStats = Object.keys(agentStats).map(agentId => { - const agent = users.find(u => u.id === agentId); - return { - name: agent ? agent.name : agentId, - revenue: agentStats[agentId].revenue, - volume: agentStats[agentId].volume - }; - }).sort((a, b) => b.revenue - a.revenue).slice(0, 3); + // Pipeline + const totalProps = properties.length; + const statusBreakdown = {}; + properties.forEach(p => { statusBreakdown[p.canvassingStatus] = (statusBreakdown[p.canvassingStatus] || 0) + 1; }); + const pipelineText = Object.entries(statusBreakdown).map(([k, v]) => ` - ${k}: ${v}`).join('\n'); - const leaderboardText = sortedStats.map((s, i) => `${i + 1}. ${s.name} - $${s.revenue.toLocaleString()}`).join('\n'); + // Meetings today + const todayMeetings = meetings.filter(m => m.date === TODAY); - return ` -ROLE: ADMIN (${user.name}) + // All projects overview (admin sees everything) + const allActive = projects.filter(p => p.status === 'active').length; + const allBudget = projects.reduce((s, p) => s + (p.budget || 0), 0); + const allSpent = projects.reduce((s, p) => s + (p.spent || 0), 0); -*** URGENT ACTION CENTER SUMMARY (${totalUrgent} Items) *** -1. UNASSIGNED LEADS: ${unassignedCount} - - Action: Assign agents to these leads immediately. -2. HOT LEADS (Inactive > 24h): ${hotLeadCount} - - Action: These high-value leads need a follow-up call. -3. RESCHEDULE NEEDED: ${rescheduleCount} - - Action: Re-book these cancelled/missed appointments. -4. PENDING SIGNATURES: ${signatureCount} - - Action: Send reminders to close these deals. + // Vendor summary (admin sees all) + const nonCompliant = vendors.filter(v => v.status !== 'active'); -GENERAL DATA ACCESS: -- Total Properties: ${properties.length} -- Total Revenue (All Agents): $${meetings.filter(m => m.status === 'Converted').reduce((sum, m) => sum + (m.dealValue || 0), 0).toLocaleString()} + // Personnel + const activeStaff = personnel.filter(p => p.status === 'active'); -LEADERBOARD SNAPSHOT (Top 3): -${leaderboardText} -- Full details at /admin/leaderboard - `; - } + // Documents + const pendingDocs = documents.filter(d => d.status === 'pending_review').length; - // --- FIELD AGENT CONTEXT (Existing Logic) --- - // 1. Agenda (Today + Upcoming) - const today = new Date().toISOString().split('T')[0]; - const myMeetings = meetings - .filter(m => m.agentId === user.id) - .sort((a, b) => new Date(a.date) - new Date(b.date)); + return BASE + `ROLE: ADMIN — ${user.name} - // Simple segment: Today vs Upcoming - const agendaText = myMeetings.map(m => { - const isToday = m.date === today; - return `- [${m.date} @ ${m.time}] ${m.status.toUpperCase()} with ${m.customerName} (${m.propertyId})${isToday ? ' **(TODAY)**' : ''} - Issue: "${m.issueDescription || 'N/A'}" - Comments: "${m.customerComments || 'None'}" - Outcome: ${m.outcome || 'Pending'}`; - }).join('\n'); +URGENT ACTION CENTER: +- Unassigned Leads: ${unassigned} +- Hot Leads Needing Follow-Up: ${hotLeadCount} +- Reschedule Needed (Cancelled/Missed): ${rescheduleNeeded} +- Pending Signatures: ${pendingSigs} +- Documents Pending Review: ${pendingDocs} - // 2. Hot Leads (Top 5) - const hotLeads = properties - .filter(p => p.canvassingStatus === 'Hot Lead') - .slice(0, 5) - .map(p => `- ${p.propertyData.propertyAddress} (Value: $${p.propertyData.currentEstimatedMarketValue.toLocaleString()})`) - .join('\n'); +SALES PIPELINE (${totalProps} properties): +${pipelineText} - // 3. Golden Leads (Older + High Value) - const goldenLeads = properties - .filter(p => p.propertyData.yearBuilt < 2000 && p.propertyData.currentEstimatedMarketValue > 400000) - .slice(0, 3) - .map(p => `- ${p.propertyData.propertyAddress} (Built: ${p.propertyData.yearBuilt})`) - .join('\n'); +LEADERBOARD (All-Time Closed Won): +${lbText || ' No data.'} - // 4. Pending Signatures - const pendingSigs = properties - .filter(p => p.propertyData.pendingSignature) - .map(p => `- ${p.propertyData.propertyAddress} (Owner: ${p.ownerData.fullName})`) - .join('\n'); +TODAY'S MEETINGS (${todayMeetings.length}): +${todayMeetings.slice(0, 5).map(m => ` - ${m.time} with ${m.customerName} (${m.status})`).join('\n') || ' None scheduled.'} - // 5. Revenue & Performance Snapshot - const myCompleted = meetings.filter(m => m.agentId === user.id && (m.status === 'Converted' || m.status === 'Completed')); - const myWins = meetings.filter(m => m.agentId === user.id && m.status === 'Converted'); - const totalRevenue = myWins.reduce((sum, m) => sum + (m.dealValue || 0), 0); - const winRate = myCompleted.length > 0 ? Math.round((myWins.length / myCompleted.length) * 100) : 0; +PROJECTS OVERVIEW (Cross-Role: Admin sees ALL): +- Active: ${allActive}/${projects.length} | Total Budget: $${allBudget.toLocaleString()} | Spent: $${allSpent.toLocaleString()} - // 6. Neighborhood Intelligence - const avgValue = Math.round(properties.reduce((sum, p) => sum + p.propertyData.currentEstimatedMarketValue, 0) / properties.length); - const commonRoof = "Architectural Shingle"; // Hardcoded for demo, or could calculate mode +VENDOR COMPLIANCE: +- Total Vendors: ${vendors.length} | Non-Compliant: ${nonCompliant.length} +${nonCompliant.map(v => ` - ${v.vendorName}: Status=${v.status}`).join('\n') || ' All compliant.'} - return ` -ROLE: FIELD AGENT / ADMIN (${user.name}) - -YOUR DATA ACCESS: -1. AGENDA: -${agendaText || "No meetings found."} - -2. HOT LEADS (Priority): -${hotLeads || "None."} - -3. GOLDEN LEADS (High Value / Old): -${goldenLeads || "None."} - -4. PENDING SIGNATURES (Follow Up Needed): -${pendingSigs || "None."} - -5. MY PERFORMANCE: -- Total Revenue (YTD): $${totalRevenue.toLocaleString()} -- Win Rate: ${winRate}% (${myWins.length} wins / ${myCompleted.length} completed) - -6. TERRITORY INSIGHTS: -- Avg Home Value: $${avgValue.toLocaleString()} -- Most Common Roof: ${commonRoof} +STAFF: ${activeStaff.length} active / ${personnel.length} total INSTRUCTIONS: -- If asked about "agenda", "schedule", or "next appointment", summarize the meetings. -- If asked about "leads", suggest the designated Hot or Golden leads. -- If asked about "performance" or "stats", share your win rate and revenue. -- If asked about "neighborhood" or "territory", share the average value and roof types. -`; -}; - -const getCustomerContext = (user, meetings, properties) => { - // 1. My Property - // In mock data, customers are linked via 'propertyId' or name matching. - const myProperty = properties.find(p => p.propertyData.propertyId === user.propertyId) - || properties.find(p => p.ownerData.fullName === user.name); - - let propDetails = "Property details not found."; - let preventiveTips = "Standard seasonal maintenance recommended."; - let warrantyStatus = "Standard Manufacturer Warranty (Expired)"; - - if (myProperty) { - propDetails = ` - - Address: ${myProperty.propertyData.propertyAddress} - - Roof Type: ${myProperty.propertyData.roofMaterial} - - Roof Condition: ${myProperty.propertyData.roofCondition} - - Storm Risk Score: ${myProperty.propertyData.neighborhoodRating}/10 - - Est. Value: $${myProperty.propertyData.currentEstimatedMarketValue.toLocaleString()} - - Last Inspection: ${myProperty.propertyData.lastInspectionDate || 'Unknown'} - `; - - // Dynamic Tips - if (myProperty.propertyData.roofCondition === 'Needs Repair') { - preventiveTips = "URGENT: Schedule an inspection immediately to prevent water damage."; - } else if (myProperty.propertyData.treeCoverage > 50) { - preventiveTips = "Consider trimming overhanging branches to protect shingles."; - } - - // Mock Warranty Logic - const yearBuilt = myProperty.propertyData.yearBuilt; - if (yearBuilt > 2015) warrantyStatus = "Active Workmanship Warranty (Valid until 2030)"; +- You manage the entire operation. Answer questions about pipeline, agents, revenue, meetings, and compliance. +- When asked "who's the top performer?", use the leaderboard data. +- When asked about a specific agent, cross-reference meetings + salesHistory. +- You can also answer questions about projects, vendors, and documents (full cross-role access).`; } - // 2. History & Upcoming - const myHistory = meetings - .filter(m => m.customerId === user.id || m.customerName === user.name) - .sort((a, b) => new Date(b.date) - new Date(a.date)); // Newest first + // ──────────────── FIELD AGENT ──────────────── + if (user.role === 'FIELD_AGENT') { + const myMeetings = meetings.filter(m => m.agentId === user.id).sort((a, b) => new Date(a.date) - new Date(b.date)); + const todayMeetings = myMeetings.filter(m => m.date === TODAY); + const upcomingMeetings = myMeetings.filter(m => new Date(m.date) >= todayDate); - const historyText = myHistory.map(m => { - const isFuture = new Date(m.date) > new Date(); - return `- [${m.date}] ${isFuture ? 'UPCOMING' : 'COMPLETED'} (${m.status}) - Agent: ${m.agentName || 'Assigned Rep'} - Note: ${m.notes} - ${m.dealValue ? `Cost: $${m.dealValue}` : ''}`; - }).join('\n'); + const agendaText = upcomingMeetings.slice(0, 8).map(m => { + const isToday = m.date === TODAY; + return ` - [${m.date} @ ${m.time}] ${m.status} with ${m.customerName}${isToday ? ' **(TODAY)**' : ''} — ${m.issueDescription || 'General'}`; + }).join('\n'); - // 3. Total Spend - const totalSpend = myHistory.reduce((sum, m) => sum + (m.dealValue || 0), 0); + const hotLeads = properties.filter(p => p.canvassingStatus === 'Hot Lead').slice(0, 5) + .map(p => ` - ${p.propertyData?.propertyAddress || p.address || 'N/A'} (Value: $${(p.propertyData?.currentEstimatedMarketValue || 0).toLocaleString()})`).join('\n'); - return ` -ROLE: HOMEOWNER / CUSTOMER (${user.name}) + const goldenLeads = properties.filter(p => p.propertyData?.yearBuilt < 2000 && p.propertyData?.currentEstimatedMarketValue > 400000).slice(0, 3) + .map(p => ` - ${p.propertyData.propertyAddress} (Built: ${p.propertyData.yearBuilt})`).join('\n'); -YOUR DATA: -1. PROPERTY PROFILE: + const pendingSigs = properties.filter(p => p.pendingSignature || p.propertyData?.pendingSignature) + .map(p => ` - ${p.propertyData?.propertyAddress || 'N/A'} (Owner: ${p.ownerData?.fullName || 'N/A'})`).join('\n'); + + const myWins = meetings.filter(m => m.agentId === user.id && m.status === 'Converted'); + const myCompleted = meetings.filter(m => m.agentId === user.id && (m.status === 'Converted' || m.status === 'Completed')); + const totalRevenue = myWins.reduce((s, m) => s + (m.dealValue || 0), 0); + const winRate = myCompleted.length > 0 ? Math.round((myWins.length / myCompleted.length) * 100) : 0; + + return BASE + `ROLE: FIELD AGENT — ${user.name} + +MY AGENDA (${todayMeetings.length} today, ${upcomingMeetings.length} upcoming): +${agendaText || ' No upcoming meetings.'} + +HOT LEADS (Priority): +${hotLeads || ' None.'} + +GOLDEN LEADS (High Value / Older Homes): +${goldenLeads || ' None.'} + +PENDING SIGNATURES: +${pendingSigs || ' None.'} + +MY PERFORMANCE: +- Revenue (YTD): $${totalRevenue.toLocaleString()} +- Win Rate: ${winRate}% (${myWins.length} wins / ${myCompleted.length} completed) + +TERRITORY: ${properties.length} properties tracked in DFW. + +INSTRUCTIONS: +- Help with agenda, leads, performance stats, and territory insights. +- When asked "what's next?", show the next upcoming meeting. +- When asked about leads, suggest Hot or Golden leads. +- Be efficient and action-oriented.`; + } + + // ──────────────── CONTRACTOR ──────────────── + if (user.role === 'CONTRACTOR') { + const myProjects = projects.filter(p => p.contractorId === user.id || p.contractorId === 'con_001'); + const activeProj = myProjects.filter(p => p.status === 'active'); + const totalBudget = myProjects.reduce((s, p) => s + (p.budget || 0), 0); + const totalSpent = myProjects.reduce((s, p) => s + (p.spent || 0), 0); + const pendingInvoices = myProjects.reduce((s, p) => s + (p.invoices || []).filter(i => i.status === 'pending').reduce((is, i) => is + i.amount, 0), 0); + + const projDetails = myProjects.map(p => { + const nextMs = (p.milestones || []).find(m => m.status !== 'completed'); + return ` - ${p.address} (${p.projectType}): ${p.status.toUpperCase()}, ${p.completionPercentage || 0}% done, Budget $${p.budget.toLocaleString()}, Spent $${p.spent.toLocaleString()}, Next: ${nextMs ? `${nextMs.name} by ${nextMs.dueDate}` : 'All complete'}`; + }).join('\n'); + + // Upcoming milestones + const upcomingMs = myProjects.flatMap(p => (p.milestones || []).filter(m => { + const diff = (new Date(m.dueDate) - todayDate) / (1000 * 60 * 60 * 24); + return diff >= -3 && diff <= 14 && m.status !== 'completed'; + }).map(m => ({ ...m, address: p.address }))).sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate)); + + const msText = upcomingMs.map(m => ` - ${m.name} @ ${m.address} — due ${m.dueDate} (${m.status})`).join('\n'); + + // My documents / compliance + const myDocs = documents.filter(d => d.entityId === user.id || d.entityId === 'con_001'); + const expiredDocs = myDocs.filter(d => d.status === 'expired'); + + // Cross-role: Contractor sees subcontractors on their projects + const subIds = [...new Set(myProjects.flatMap(p => p.subcontractorIds || []))]; + const subUsers = users.filter(u => subIds.includes(u.id)); + + // Cross-role: Contractor sees vendors delivering to their projects + const vendorNames = [...new Set(myProjects.flatMap(p => (p.milestones || []).filter(m => m.vendorId).map(m => { + const v = vendors.find(vv => vv.id === m.vendorId); + return v ? v.vendorName : m.vendorId; + })))]; + + return BASE + `ROLE: CONTRACTOR — ${user.name} + +MY PROJECTS (${myProjects.length} total, ${activeProj.length} active): +${projDetails || ' None assigned.'} + +CRITICAL TASKS (Next 14 Days): +${msText || ' No critical tasks due.'} + +FINANCIALS: +- Total Budget: $${totalBudget.toLocaleString()} | Spent: $${totalSpent.toLocaleString()} +- Pending Invoice Payouts: $${pendingInvoices.toLocaleString()} + +COMPLIANCE: +${expiredDocs.length > 0 ? `- WARNING: ${expiredDocs.length} expired documents need renewal: ${expiredDocs.map(d => d.name).join(', ')}` : '- All documents current.'} + +CROSS-ROLE VISIBILITY: +- My Subcontractors: ${subUsers.map(s => s.name).join(', ') || 'None assigned'} +- Vendors on My Projects: ${vendorNames.join(', ') || 'None'} + +INSTRUCTIONS: +- Help track project progress, deadlines, invoices, and crew coordination. +- Only discuss THIS contractor's projects — never reveal other contractors' data. +- When asked about a subcontractor, reference their tasks on shared projects.`; + } + + // ──────────────── SUBCONTRACTOR ──────────────── + if (user.role === 'SUBCONTRACTOR') { + const myId = user.id || 'sub_001'; + const myProjects = projects.filter(p => (p.subcontractorIds || []).includes(myId)); + + // Only milestones assigned to this sub + const myTasks = myProjects.flatMap(p => (p.milestones || []).filter(m => m.assignedTo === myId).map(m => ({ ...m, address: p.address, projectId: p.id }))); + const activeTasks = myTasks.filter(t => t.status === 'in_progress'); + const pendingTasks = myTasks.filter(t => t.status === 'pending'); + const completedTasks = myTasks.filter(t => t.status === 'completed'); + + const taskDetails = myTasks.filter(t => t.status !== 'completed').map(t => ` - ${t.name} @ ${t.address} — ${t.status} (due ${t.dueDate})`).join('\n'); + + // Pending payouts (from invoices on projects I'm on) + const pendingPay = myProjects.reduce((s, p) => s + (p.invoices || []).filter(i => i.status === 'pending' && (i.payeeTo === myId || i.payeeTo === 'sub_001')).reduce((is, i) => is + i.amount, 0), 0); + + // My compliance docs + const myDocs = documents.filter(d => d.entityId === myId); + const expiredDocs = myDocs.filter(d => d.status === 'expired'); + + // Cross-role: Sub sees the contractor managing their projects + const contractorIds = [...new Set(myProjects.map(p => p.contractorId))]; + const contractorUsers = users.filter(u => contractorIds.includes(u.id)); + + return BASE + `ROLE: SUBCONTRACTOR — ${user.name} + +MY TASKS (${activeTasks.length} active, ${pendingTasks.length} pending, ${completedTasks.length} completed): +${taskDetails || ' No open tasks.'} + +ASSIGNED PROJECTS: ${myProjects.map(p => `${p.address} (${p.status})`).join(', ') || 'None'} + +FINANCIALS: +- Pending Payouts: $${pendingPay.toLocaleString()} + +COMPLIANCE: +${expiredDocs.length > 0 ? `- WARNING: ${expiredDocs.length} expired docs: ${expiredDocs.map(d => d.name).join(', ')}` : '- All documents current.'} + +CROSS-ROLE VISIBILITY: +- My General Contractor: ${contractorUsers.map(c => c.name).join(', ') || 'N/A'} + +INSTRUCTIONS: +- Help track task progress, deadlines, and payouts. +- Only show THIS subcontractor's tasks — never reveal other subs' data or full project budgets. +- When asked "what's next?", show the nearest pending/in-progress task by due date.`; + } + + // ──────────────── VENDOR ──────────────── + if (user.role === 'VENDOR') { + // Map user to vendor record + const vendorMap = { 'ven_001': 'v3', 'ven_002': 'v1' }; + const vendorId = vendorMap[user.id] || 'v1'; + const myVendor = vendors.find(v => v.id === vendorId) || vendors[0]; + + const coiStatus = myVendor.compliance?.coi?.status || 'unknown'; + const coiExpiry = myVendor.compliance?.coi?.expirationDate || 'N/A'; + const w9Status = myVendor.compliance?.w9?.status || 'unknown'; + const rating = myVendor.performance?.rating || 'N/A'; + const onTime = myVendor.performance?.onTimeDelivery || 'N/A'; + const defectRate = myVendor.performance?.defectRate || 'N/A'; + + // Orders for this vendor + const myOrders = (orders || []).filter(o => o.vendorId === vendorId); + const activeOrders = myOrders.filter(o => o.status === 'shipped' || o.status === 'processing'); + const orderText = myOrders.slice(0, 5).map(o => ` - PO#${o.id}: ${o.items?.map(i => i.name).join(', ') || 'Items'} — ${o.status.toUpperCase()}, $${(o.total || 0).toLocaleString()}, ETA: ${o.estimatedDelivery || 'N/A'}`).join('\n'); + + // Invoices for this vendor + const myInvoices = (vendorInvoices || []).filter(i => i.vendorId === vendorId); + const pendingInv = myInvoices.filter(i => i.status === 'pending'); + const totalPending = pendingInv.reduce((s, i) => s + (i.amount || 0), 0); + const invText = myInvoices.slice(0, 5).map(i => ` - INV#${i.id}: $${(i.amount || 0).toLocaleString()} — ${i.status.toUpperCase()} (${i.dueDate || 'N/A'})`).join('\n'); + + // Spend + const totalSpend = myVendor.spend?.totalSpend || 0; + const ytdSpend = myVendor.spend?.ytdSpend || totalSpend; + + return BASE + `ROLE: VENDOR — ${user.name} (${myVendor.vendorName}) + +COMPANY PROFILE: +- Category: ${myVendor.category || 'N/A'} | Status: ${myVendor.status || 'N/A'} +- Rating: ${rating}/5.0 | On-Time Delivery: ${onTime}% | Defect Rate: ${defectRate}% + +COMPLIANCE: +- COI: ${coiStatus} (expires ${coiExpiry}) +- W-9: ${w9Status} +${coiStatus === 'expired' || w9Status === 'expired' ? '⚠️ WARNING: Expired documents — upload renewed versions to avoid order holds.' : ''} + +ORDERS (${myOrders.length} total, ${activeOrders.length} active): +${orderText || ' No orders.'} + +INVOICES: +- Pending: ${pendingInv.length} ($${totalPending.toLocaleString()}) +${invText || ' No invoices.'} + +SPEND: +- Total All-Time: $${totalSpend.toLocaleString()} +- YTD: $${ytdSpend.toLocaleString()} + +INSTRUCTIONS: +- Help track orders, invoices, compliance status, and deliveries. +- Only show THIS vendor's data — never reveal other vendors' info or full project financials. +- When asked about compliance, explain exactly what needs renewal and urgency. +- When asked "what orders are pending?", list active orders with ETAs.`; + } + + // ──────────────── CUSTOMER ──────────────── + if (user.role === 'CUSTOMER') { + const myProperty = properties.find(p => p.propertyData?.propertyId === user.propertyId) + || properties.find(p => p.ownerData?.fullName === user.name); + + let propDetails = ' Property details not found.'; + let tips = 'Standard seasonal maintenance recommended.'; + let warranty = 'Standard Manufacturer Warranty (check status with your rep)'; + + if (myProperty) { + const pd = myProperty.propertyData; + propDetails = ` - Address: ${pd.propertyAddress} + - Roof Type: ${pd.roofMaterial} | Condition: ${pd.roofCondition} + - Storm Risk: ${pd.neighborhoodRating}/10 + - Est. Value: $${pd.currentEstimatedMarketValue?.toLocaleString() || 'N/A'} + - Last Inspection: ${pd.lastInspectionDate || 'Unknown'}`; + + if (pd.roofCondition === 'Needs Repair') tips = 'URGENT: Schedule an inspection immediately to prevent water damage.'; + else if (pd.treeCoverage > 50) tips = 'Consider trimming overhanging branches to protect shingles.'; + + if (pd.yearBuilt > 2015) warranty = 'Active Workmanship Warranty (Valid until 2030)'; + } + + const myMeetings = meetings.filter(m => m.customerId === user.id || m.customerName === user.name) + .sort((a, b) => new Date(b.date) - new Date(a.date)); + const meetingText = myMeetings.slice(0, 5).map(m => { + const isFuture = new Date(m.date) > todayDate; + return ` - [${m.date}] ${isFuture ? 'UPCOMING' : 'COMPLETED'} (${m.status}) — ${m.notes || 'No notes'}`; + }).join('\n'); + + const totalSpend = myMeetings.reduce((s, m) => s + (m.dealValue || 0), 0); + + return BASE + `ROLE: HOMEOWNER — ${user.name} + +MY PROPERTY: ${propDetails} -2. SERVICE HISTORY: -${historyText || "No records found."} +SERVICE HISTORY: +${meetingText || ' No records found.'} -3. FINANCIALS: -- Total Spend to Date: $${totalSpend.toLocaleString()} -- Warranty Status: ${warrantyStatus} +FINANCIALS: +- Total Spend: $${totalSpend.toLocaleString()} +- Warranty: ${warranty} -4. PERSONALIZED ADVICE: -- Tip: ${preventiveTips} +ADVICE: ${tips} INSTRUCTIONS: - Answer questions about "my roof", "my appointment", "warranty", or "spending". -- Be polite and reassuring. -`; -}; - -const getGuestContext = () => { - return ` -ROLE: EXPERT ROOFING CONSULTANT (20+ Years Experience) - -YOUR KNOWLEDGE BASE (Use this to diagnose, reassure, but EDUCATE ON HIDDEN RISKS): - -1. COMMON ISSUES & RISK FACTORS: -- **Active Leak**: "A bucket stops the floor damage, but water is currently saturating your insulation and decking." -> URGENCY: "Mold begins to grow in wet insulation within 24-48 hours. We need to map the moisture path immediately." -- **Missing Shingles**: "It looks minor, but your underlayment is now exposed to UV rays which destroy it in days." -> RISK: "Leaving this can lead to a denied insurance claim later for 'neglect'." -- **Hail Dents**: "These look like small bruises, but they have fractured the fiberglass mat." -> INVISIBLE DAMAGE: "This reduces roof life by 3-5 years. If not documented now, insurance will call it 'wear and tear' later." -- **Granule Loss**: "Your roof is shedding its sunscreen." -> REALITY: "The asphalt is now baking in the Texas sun. This is a leading indicator of roof failure within 12-18 months." - -2. COMPLEX ISSUE TRIAGE: -- **Ice Dams**: "Heat loss from your attic is melting snow." -> SOLUTION: "We need to assess your intake vents, not just patch the roof." -- **Sagging**: "This indicates structural fatigue or long-term water intrusion." -> ACTION: "Do not walk on this area. Needs professional stabilization." - -3. THE "WHY US" HOOK: -- "Insurance adjusters use specific criteria to approve claims. We know exactly what they look for." -- "A DIY patch often voids the manufacturer warranty. Our repairs are certified." -- "We are in Plano right now—I can have a project manager stop by in 30 minutes to check the moisture levels for free." - -INSTRUCTIONS: -- **Tone**: Professional, Authoritative, "Doctor-like". -- **Strategy**: Validate the user's observation -> Explain the *hidden* consequence of waiting -> Offer the *Zero-Risk* solution (Free Inspection). -- **Contact**: "Call 866-259-6533. We can be there today." -`; -}; - -const getOwnerContext = (user, storeData) => { - const { meetings, properties, salesHistory, vendors, personnel, documents, projects } = storeData; - - // 1. Financial High-Level - const totalRevenue = salesHistory.filter(s => s.status === 'closed_won').reduce((sum, s) => sum + s.amount, 0); - const pendingPayouts = projects.reduce((sum, p) => sum + p.invoices.filter(i => i.status === 'pending').reduce((is, i) => is + i.amount, 0), 0); - - // 2. Urgent Items - const expiringDocs = documents.filter(d => { - const exp = new Date(d.expirationDate); - const now = new Date(); - const diffDays = Math.ceil((exp - now) / (1000 * 60 * 60 * 24)); - return diffDays > 0 && diffDays <= 30; - }).length; - - const pendingDocs = documents.filter(d => d.status === 'pending_review' || d.status === 'pending').length; - const nonCompliantVendors = vendors.filter(v => v.status !== 'active').length; - - // 3. Operational Snapshot - const activeProjects = projects.filter(p => p.status === 'active').length; - const activePersonnel = personnel.filter(p => p.status === 'active').length; - - return ` -ROLE: OWNER (${user.name}) - -*** EXECUTIVE DASHBOARD SUMMARY *** - -1. FINANCIAL HEALTH: - - Total Revenue (YTD): $${totalRevenue.toLocaleString()} - - Pending Invoice Payouts: $${pendingPayouts.toLocaleString()} - -2. RISK & COMPLIANCE (Action Required): - - Documents Pending Review: ${pendingDocs} - - Documents Expiring Soon (30d): ${expiringDocs} - - Non-Compliant Vendors: ${nonCompliantVendors} - -3. OPERATIONS: - - Active Projects: ${activeProjects} - - Active Staff: ${activePersonnel} - -INSTRUCTIONS: -- You are the Business Intelligence Architect. -- Answer questions about revenue, burn rate, compliance risks, and high-level strategy. -- If asked about specific details (e.g., "Which vendor is non-compliant?"), you can query the valid filtered lists provided implicitly in your logic or advise checking the specific dashboard page. -- PRIORITIZE RISK ALERTS: If compliance is low, warn the owner. -`; -}; - -const getContractorContext = (user, storeData) => { - const { projects, documents } = storeData; - - // 1. Filter Projects for THIS Contractor - const myProjects = projects.filter(p => p.contractorId === user.id || p.subcontractorIds?.includes(user.id)); - - if (myProjects.length === 0) { - return ` -ROLE: CONTRACTOR (${user.name}) -CONTEXT: You currently have no active projects assigned. -INSTRUCTIONS: Assist with general onboarding or account management questions. -`; +- Be polite, reassuring, and helpful. +- Offer to schedule a free inspection if concerns arise: 866-259-6533.`; } - // 2. Project Details - const projectSummaries = myProjects.map(p => { - const milestones = p.milestones.filter(m => m.assignedTo === user.id); - const nextMilestone = milestones.find(m => m.status !== 'completed'); - return `- Project #${p.id} (${p.projectType}): Status ${p.status} - Next Deadline: ${nextMilestone ? `${nextMilestone.name} by ${nextMilestone.dueDate}` : 'None'} - Budget: $${p.budget.toLocaleString()}`; - }).join('\n'); - - // 3. My Compliance - const myDocs = documents.filter(d => d.entityId === user.id); - const missingOrExpired = myDocs.filter(d => d.status === 'expired' || d.status === 'missing'); - - return ` -ROLE: CONTRACTOR (${user.name}) - -YOUR ASSIGNMENTS: -${projectSummaries} - -YOUR COMPLIANCE STATUS: -${missingOrExpired.length > 0 ? `WARNING: You have ${missingOrExpired.length} documents needing attention.` : "All compliance documents are up to date."} - -INSTRUCTIONS: -- ONLY discuss projects listed above. -- Do NOT reveal company-wide financial data or other contractors' projects. -- Help the contractor track their deadlines and payments. -`; + // ──────────────── FALLBACK ──────────────── + return BASE + `ROLE: ${user.role} — ${user.name} +You are logged in but your role context is being set up. I can help with general questions about LynkedUp Pro.`; }; -// ... (getEmployeeContext and getCustomerContext remain, updated signatures to use storeData object for cleaner passing) - const Chatbot = (props) => { const { user, isAuthenticated } = useAuth(); // Pull ALL data needed for context generation const storeData = useMockStore(); // Destructure specifically for local usage if needed, but we pass the whole object to context functions - const { addMeeting, meetings, properties, salesHistory, users, vendors, personnel, documents, projects } = storeData; + const { addMeeting, meetings, properties, salesHistory, users, vendors, personnel, documents, projects, owners, orders, vendorInvoices } = storeData; const [isOpen, setIsOpen] = useState(false); const [isMinimized, setIsMinimized] = useState(false); @@ -391,14 +514,20 @@ const Chatbot = (props) => { let greeting = 'Hello! Welcome to LynkedUp Pro. How can I help you with your roofing needs today?'; if (user) { greeting = `Welcome back, ${user.name}! `; - if (user.role === 'FIELD_AGENT' || user.role === 'ADMIN') { - greeting += "I have your latest territory data and agenda ready. What do you need?"; + if (user.role === 'ADMIN') { + greeting += "Operations HQ is live. Ask me about pipeline, agents, compliance, or revenue."; + } else if (user.role === 'FIELD_AGENT') { + greeting += "I have your territory data and agenda ready. What do you need?"; } else if (user.role === 'CUSTOMER') { greeting += "I have your property records open. How can I assist?"; } else if (user.role === 'OWNER') { - greeting += "Executive Dashboard is live. Ask me about revenue, compliance, or operational risks."; - } else if (user.role === 'CONTRACTOR' || user.role === 'SUBCONTRACTOR') { - greeting += "I have your project assignments loaded. Need to check deadlines or invoices?"; + greeting += "Executive Dashboard is live. Ask me about projects, budgets, vendors, or compliance."; + } else if (user.role === 'CONTRACTOR') { + greeting += "Command Center loaded. Need project status, deadlines, or invoice info?"; + } else if (user.role === 'SUBCONTRACTOR') { + greeting += "Your tasks are loaded. Ask about deadlines, payouts, or compliance."; + } else if (user.role === 'VENDOR') { + greeting += "Your orders and compliance data are ready. Ask about invoices, deliveries, or documents."; } } setMessages([{ role: 'assistant', content: greeting }]); @@ -431,69 +560,10 @@ const Chatbot = (props) => { // --- REAL AI REQUEST --- - // --- NEW: Enhanced Context Builders for Phase 7 --- - - const getOwnerContext = () => { - // Summarize financials - // Real app: Calculate from store data - return "Role: OWNER. Context: You have access to deep financial insights. Total Revenue: $4.2M. Active Projects: 12. Urgent Items: 3 (1 expired insurance, 2 pending invoices)."; - }; - - const getContractorContext = () => { - const myProjects = storeData.projects.filter(p => p.contractorId === 'con_001'); - const activeCount = myProjects.filter(p => p.status === 'active').length; - return `Role: CONTRACTOR. Context: You are managing ${activeCount} active projects. Total budget volume: $${myProjects.reduce((s, p) => s + p.budget, 0).toLocaleString()}. Key deadline: 2604 Dunwick Dr (Roof Tear-off completed).`; - }; - - const getVendorContext = () => { - // Mock ID 'v3' for 'abc_supply' or 'v1' for generic - const myId = user.id === 'ven_001' ? 'v3' : 'v1'; - const me = storeData.vendors.find(v => v.id === myId) || storeData.vendors[0]; - const pending = me.spend?.pendingInvoices || 0; - return `Role: VENDOR. Context: You are ${me.vendorName}. Pending Invoices: $${pending.toLocaleString()}. Compliance Status: ${me.compliance.coi.status}. Performance Rating: ${me.performance.rating}/5.0.`; - }; - - const getSubContractorContext = () => { - // Mock ID 'sub_001' - const myTasks = storeData.projects.flatMap(p => p.milestones.filter(m => m.assignedTo === 'sub_001')); - const pendingPay = 6500; // Mock calculation from dashboard logic - return `Role: SUBCONTRACTOR. Context: You have ${myTasks.filter(t => t.status === 'in_progress').length} active tasks. Pending Payouts: $${pendingPay.toLocaleString()}. Next Task: Electrical Guard Install at 2604 Dunwick.`; - }; - - // -------------------------------------------------- - - const generateContext = () => { - // Base context with general knowledge - let context = `You are the LynkedUp Pro AI Assistant. Current User: ${user?.name} (${user?.role}). - - System Capabilities: - - We track 5,000+ properties in DFW. - - We manage canvassing, inspections, and project execution. - - We use 'Zinc' design system (Dark/Light mode). - - Today's Date: 2026-02-16. - `; - - if (user?.role === 'OWNER') { - context += getOwnerContext(); - } else if (user?.role === 'CONTRACTOR') { - context += getContractorContext(); - } else if (user?.role === 'VENDOR') { - context += getVendorContext(); - } else if (user?.role === 'SUBCONTRACTOR') { - context += getSubContractorContext(); - } else if (user?.role === 'FIELD_AGENT') { - context += `Role: FIELD_AGENT. Focus: Canvassing and Leads. Your Map View is active.`; - } - - // Add current page context if needed (e.g. if on a specific project page) - // For now, we keep it high-level role based. - - return context; - }; + // --- Generate deep, role-aware context --- // 1. Build Dynamic Context - let systemContext = generateContext(); + let systemContext = generateRoleContext(user, storeData); // 2. Call Groq const chatCompletion = await groq.chat.completions.create({ diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx index b092654..6ddee14 100644 --- a/src/components/Layout.jsx +++ b/src/components/Layout.jsx @@ -134,6 +134,7 @@ const Layout = () => { case 'OWNER': return [ { to: "/owner/snapshot", icon: LayoutDashboard, label: "Dashboard" }, + { to: "/owner/projects", icon: Briefcase, label: "Projects" }, { to: "/owner/vendors", icon: Users, label: "Vendors" }, { to: "/owner/people", icon: User, label: "People" }, { to: "/owner/documents", icon: FileText, label: "Documents" }, @@ -149,6 +150,7 @@ const Layout = () => { { to: "/admin/dashboard", icon: LayoutDashboard, label: "Dashboard" }, { to: "/admin/schedule", icon: Calendar, label: "Schedule" }, { to: "/admin/leaderboard", icon: Trophy, label: "Leaderboard" }, + { to: "/admin/maps", icon: Map, label: "Territory Map" }, ...commonItems ]; case 'CONTRACTOR': diff --git a/src/components/StatCard.jsx b/src/components/StatCard.jsx index eb879d9..e5c8d4f 100644 --- a/src/components/StatCard.jsx +++ b/src/components/StatCard.jsx @@ -68,14 +68,14 @@ export const StatCard = ({ label, value, suffix = '', icon: Icon, trend, trendLa const bgColor = colorClass.split(' ')[1]; return ( - +

{label}

0 diff --git a/src/components/contractor/FinancialSummaryModal.jsx b/src/components/contractor/FinancialSummaryModal.jsx index 6715a7f..198452a 100644 --- a/src/components/contractor/FinancialSummaryModal.jsx +++ b/src/components/contractor/FinancialSummaryModal.jsx @@ -138,9 +138,37 @@ const FinancialSummaryModal = ({ isOpen, onClose, role, data }) => {
- {/* Table */} -
- + {/* Mobile Card View */} +
+ {sortedData.length > 0 ? ( +
+ {sortedData.map((item, idx) => ( +
+
+
+

{item.description || item.project}

+ {item.project &&

{item.project}

} +
+ {formatCurrency(item.amount)} +
+
+ {item.date} + {item.status} +
+
+ ))} +
+ ) : ( +
No transactions found.
+ )} +
+ + {/* Desktop Table View */} +
+
{[ @@ -152,7 +180,7 @@ const FinancialSummaryModal = ({ isOpen, onClose, role, data }) => { @@ -162,22 +190,22 @@ const FinancialSummaryModal = ({ isOpen, onClose, role, data }) => { {sortedData.length > 0 ? sortedData.map((item, idx) => ( - - - - diff --git a/src/components/contractor/TaskDetailsModal.jsx b/src/components/contractor/TaskDetailsModal.jsx index 79fe8cf..bdd8bc5 100644 --- a/src/components/contractor/TaskDetailsModal.jsx +++ b/src/components/contractor/TaskDetailsModal.jsx @@ -91,7 +91,7 @@ const TaskDetailsModal = ({ isOpen, onClose, task, onUpdate }) => { {/* Body */} -
+
{/* Task Info Card */}

{task.name}

@@ -153,7 +153,7 @@ const TaskDetailsModal = ({ isOpen, onClose, task, onUpdate }) => {
{/* Footer Actions */} -
+
{task.status !== 'completed' ? ( <> ))}
-
+
{filteredDocs.length === 0 ? (
@@ -59,29 +83,56 @@ const DocumentReviewQueue = () => {
) : ( filteredDocs.map(doc => ( -
-
-
-
- +
+
+
+
+ {getDocTypeIcon(doc.documentType)}
-
-

{doc.title}

+
+

{doc.name}

- {doc.status} + {doc.status === 'pending_review' ? 'Pending Review' : doc.status}

- {doc.category} + {getDocTypeLabel(doc.documentType)} ID: {doc.id}

-

Submitted by: Vendor #{doc.relatedEntityId} on {new Date(doc.uploadDate).toLocaleDateString()}

+

+ Uploaded by: {doc.uploadedBy} + {doc.entityName && <> for {doc.entityName}} + {doc.uploadedAt && <> on {new Date(doc.uploadedAt).toLocaleDateString()}} +

+ {doc.aiConfidence && ( +

+ AI Confidence: = 0.9 ? 'text-emerald-500' : doc.aiConfidence >= 0.8 ? 'text-amber-500' : 'text-red-500'}`}> + {Math.round(doc.aiConfidence * 100)}% + +

+ )} {doc.expirationDate && ( -

- Expires: {new Date(doc.expirationDate).toLocaleDateString()} +

+ + {new Date(doc.expirationDate) < new Date('2026-02-18') ? 'EXPIRED: ' : 'Expires: '} + {new Date(doc.expirationDate).toLocaleDateString()} +

+ )} + {doc.status === 'expired' && ( +

+ + Vendor must upload renewed document

)}
@@ -90,15 +141,21 @@ const DocumentReviewQueue = () => {
- -
- {doc.status === 'pending' && ( + {doc.status === 'pending_review' && (
)} + + {doc.status === 'expired' && ( + + )}
diff --git a/src/components/owner/ChangeOrderDrawer.jsx b/src/components/owner/ChangeOrderDrawer.jsx new file mode 100644 index 0000000..831e943 --- /dev/null +++ b/src/components/owner/ChangeOrderDrawer.jsx @@ -0,0 +1,103 @@ +import React, { useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import { X, GitPullRequest, DollarSign, Calendar, FileText } from 'lucide-react'; + +const ChangeOrderDrawer = ({ isOpen, onClose, changeOrder }) => { + useEffect(() => { + const handleEsc = (e) => { if (e.key === 'Escape') onClose(); }; + if (isOpen) window.addEventListener('keydown', handleEsc); + return () => window.removeEventListener('keydown', handleEsc); + }, [isOpen, onClose]); + + useEffect(() => { + document.body.style.overflow = isOpen ? 'hidden' : ''; + return () => { document.body.style.overflow = ''; }; + }, [isOpen]); + + if (!isOpen || !changeOrder) return null; + + const co = changeOrder; + + const formatCurrency = (amt) => + new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(amt); + + const getStatusColor = (s) => { + if (s === 'approved') return 'text-emerald-600 bg-emerald-100 dark:text-emerald-400 dark:bg-emerald-500/10'; + if (s === 'pending') return 'text-amber-600 bg-amber-100 dark:text-amber-400 dark:bg-amber-500/10'; + return 'text-red-600 bg-red-100 dark:text-red-400 dark:bg-red-500/10'; + }; + + return createPortal( +
+
+
+ {/* Mobile Drag Handle */} +
+
+
+ {/* Header */} +
+
+
+ +
+
+

Change Order

+

{co.id}

+
+
+ +
+ + {/* Content */} +
+
+

{co.title}

+ + {co.status} + +
+ + {co.description && ( +
+
Description
+

{co.description}

+
+ )} + +
+
+
+ + Amount +
+
{formatCurrency(co.amount)}
+
+
+
+ + Submitted +
+
{co.dateSubmitted}
+
+
+ + {/* Cost Impact */} +
+
Cost Impact
+

+ This change order {co.status === 'approved' ? 'has added' : 'would add'}{' '} + {formatCurrency(co.amount)}{' '} + to the project budget. +

+
+
+
+
, + document.body + ); +}; + +export default ChangeOrderDrawer; diff --git a/src/components/owner/FinancialDetailsModal.jsx b/src/components/owner/FinancialDetailsModal.jsx index bd13883..a1ccc72 100644 --- a/src/components/owner/FinancialDetailsModal.jsx +++ b/src/components/owner/FinancialDetailsModal.jsx @@ -3,8 +3,9 @@ import { createPortal } from 'react-dom'; import { X, ArrowUpRight, ArrowDownRight, Filter, Download, Search, CheckCircle, Clock, AlertCircle } from 'lucide-react'; import { useMockStore } from '../../data/mockStore'; -const FinancialDetailsModal = ({ isOpen, onClose, type }) => { - const { projects, vendors } = useMockStore(); +const FinancialDetailsModal = ({ isOpen, onClose, type, ownerId }) => { + const { projects: allProjects, vendors } = useMockStore(); + const projects = ownerId ? allProjects.filter(p => p.ownerId === ownerId) : allProjects; const [searchTerm, setSearchTerm] = useState(''); const [sortConfig, setSortConfig] = useState({ key: 'amount', direction: 'desc' }); @@ -152,9 +153,9 @@ const FinancialDetailsModal = ({ isOpen, onClose, type }) => {
{/* Header */} -
-
-
handleSort(col.key)} - className={`px-3 sm:px-6 py-3 sm:py-4 text-[10px] sm:text-xs font-bold uppercase tracking-wider text-zinc-500 cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-300 transition-colors ${col.align === 'right' ? 'text-right' : ''}`} + className={`px-6 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-300 transition-colors ${col.align === 'right' ? 'text-right' : ''}`} > {col.label}
+ {item.date} -
{item.description || item.project}
+
+
{item.description || item.project}
{item.project &&
{item.project}
}
- + {item.status} + {formatCurrency(item.amount)}
@@ -258,7 +285,7 @@ const FinancialDetailsModal = ({ isOpen, onClose, type }) => { {/* Footer Summary */} -
+
Showing {sortedItems.length} records
Net Total: diff --git a/src/components/owner/FinancialKPICards.jsx b/src/components/owner/FinancialKPICards.jsx index 86c3708..e38bf3c 100644 --- a/src/components/owner/FinancialKPICards.jsx +++ b/src/components/owner/FinancialKPICards.jsx @@ -3,18 +3,17 @@ import { useMockStore } from '../../data/mockStore'; import { DollarSign, TrendingUp, TrendingDown, CreditCard } from 'lucide-react'; import { SpotlightCard } from '../SpotlightCard'; -const FinancialKPICards = ({ onCardClick }) => { +const FinancialKPICards = ({ onCardClick, ownerId }) => { const { projects, vendors } = useMockStore(); - // Removed direct navigate, now delegates to parent - // Calculate KPIs derived from mock data + // Calculate KPIs derived from mock data — filtered by ownerId when provided const metrics = useMemo(() => { - let totalRevenue = 0; // Derived from active project budgets (simplified for demo) - let outstandingAR = 0; // Invoices sent but not paid - let pendingPayouts = 0; // Vendor invoices pending - let vendorBillsDue = 0; + const ownerProjects = ownerId ? projects.filter(p => p.ownerId === ownerId) : projects; - projects.forEach(project => { + let totalRevenue = 0; + let pendingPayouts = 0; + + ownerProjects.forEach(project => { if (project.status === 'active' || project.status === 'completed') { totalRevenue += project.budget; } @@ -32,11 +31,11 @@ const FinancialKPICards = ({ onCardClick }) => { return { totalRevenue, - outstandingAR: totalRevenue * 0.3, // Mocking AR as 30% of revenue for demo + outstandingAR: totalRevenue * 0.3, pendingPayouts, totalVendorSpend }; - }, [projects, vendors]); + }, [projects, vendors, ownerId]); const cards = [ { diff --git a/src/components/owner/InvoiceDetailModal.jsx b/src/components/owner/InvoiceDetailModal.jsx new file mode 100644 index 0000000..2bdbf25 --- /dev/null +++ b/src/components/owner/InvoiceDetailModal.jsx @@ -0,0 +1,92 @@ +import React, { useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import { X, DollarSign, CheckCircle, Clock, AlertCircle } from 'lucide-react'; + +const InvoiceDetailModal = ({ isOpen, onClose, invoice }) => { + useEffect(() => { + const handleEsc = (e) => { if (e.key === 'Escape') onClose(); }; + if (isOpen) window.addEventListener('keydown', handleEsc); + return () => window.removeEventListener('keydown', handleEsc); + }, [isOpen, onClose]); + + if (!isOpen || !invoice) return null; + + const inv = invoice; + + const formatCurrency = (amt) => + new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amt); + + const statusConfig = { + paid: { label: 'Paid', color: 'text-emerald-600 bg-emerald-100 dark:text-emerald-400 dark:bg-emerald-500/10', icon: CheckCircle }, + pending: { label: 'Pending', color: 'text-amber-600 bg-amber-100 dark:text-amber-400 dark:bg-amber-500/10', icon: Clock }, + overdue: { label: 'Overdue', color: 'text-red-600 bg-red-100 dark:text-red-400 dark:bg-red-500/10', icon: AlertCircle }, + }; + + const cfg = statusConfig[inv.status] || statusConfig.pending; + const StatusIcon = cfg.icon; + + return createPortal( +
+
+
+ {/* Mobile Drag Handle */} +
+
+
+ {/* Header */} +
+
+
+ +
+
+

Invoice Detail

+

{inv.id}

+
+
+ +
+ + {/* Content */} +
+ {/* Amount & Status */} +
+
+
{formatCurrency(inv.amount)}
+
+ + {cfg.label} + +
+ + {/* Details Grid */} +
+ {[ + ['Submitted By', inv.submittedBy], + ['Due Date', inv.dueDate], + ['Date Paid', inv.datePaid || 'Not yet paid'], + ['Status', inv.status], + ].map(([label, value], i) => ( +
+
{label}
+
{value}
+
+ ))} +
+
+ + {/* Footer */} +
+ +
+
+
, + document.body + ); +}; + +export default InvoiceDetailModal; diff --git a/src/components/owner/UrgentItemsPanel.jsx b/src/components/owner/UrgentItemsPanel.jsx index 61f2fdf..e8ebd7f 100644 --- a/src/components/owner/UrgentItemsPanel.jsx +++ b/src/components/owner/UrgentItemsPanel.jsx @@ -3,9 +3,11 @@ import { AlertCircle, Clock, FileWarning, Wallet, CheckCircle, AlertTriangle, Ch import { SpotlightCard } from '../SpotlightCard'; import { useMockStore } from '../../data/mockStore'; -const UrgentItemsPanel = ({ onActionClick }) => { +const UrgentItemsPanel = ({ onActionClick, ownerId }) => { const { documents, vendors, projects } = useMockStore(); - // Removed direct navigate + + // Filter projects by owner when provided + const ownerProjects = ownerId ? projects.filter(p => p.ownerId === ownerId) : projects; // Logic to find urgent items const expiringDocs = documents.filter(d => { @@ -22,7 +24,7 @@ const UrgentItemsPanel = ({ onActionClick }) => { v.compliance.coi.status === 'expired' ); - const pendingInvoices = projects.flatMap(p => + const pendingInvoices = ownerProjects.flatMap(p => (p.invoices || []).filter(i => i.status === 'pending') ); diff --git a/src/components/vendor/VendorFinancialSummaryModal.jsx b/src/components/vendor/VendorFinancialSummaryModal.jsx index 782f9f7..b661fe7 100644 --- a/src/components/vendor/VendorFinancialSummaryModal.jsx +++ b/src/components/vendor/VendorFinancialSummaryModal.jsx @@ -139,9 +139,39 @@ const VendorFinancialSummaryModal = ({ isOpen, onClose, data }) => {
- {/* Table */} -
-
+ {/* Mobile Card View */} +
+ {sortedData.length > 0 ? ( +
+ {sortedData.map((invoice, idx) => ( +
+
+
+

{invoice.project}

+

{invoice.id}

+
+ {formatCurrency(invoice.amount)} +
+
+ {invoice.status} + Due: {invoice.dueDate} +
+
+ ))} +
+ ) : ( +
No invoices found.
+ )} +
+ + {/* Desktop Table View */} +
+
{[ @@ -155,7 +185,7 @@ const VendorFinancialSummaryModal = ({ isOpen, onClose, data }) => { @@ -165,21 +195,21 @@ const VendorFinancialSummaryModal = ({ isOpen, onClose, data }) => { {sortedData.length > 0 ? sortedData.map((invoice, idx) => ( - - - - - - diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index d5ad855..ae73a01 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -665,18 +665,30 @@ const MOCK_USERS = [ { id: 'a3', type: 'employee', empId: 'ADM03', email: 'admin3@plano.com', password: 'password', role: 'ADMIN', name: 'Arthur Director' }, // --- NEW ROLES (Owners Box) --- - // Owner + // Owner 1 — Active, strong financials, one project under dispute { id: 'own_001', type: 'owner', - username: 'justin', // For login - email: 'justin@lynkeduppro.com', - password: 'password', // Keeping simple for dev + username: 'justin', + email: 'justin@johnsondev.com', + password: 'password', role: 'OWNER', - name: 'Justin Owner', - companyName: 'LynkedUp Pro', + name: 'Justin Johnson', + companyName: 'Johnson Development Group', phone: '972-555-1234' }, + // Owner 2 — High Risk, vendor payment delays, over-budget project + { + id: 'own_002', + type: 'owner', + username: 'diana', + email: 'diana@reevesconstruction.com', + password: 'password', + role: 'OWNER', + name: 'Diana Reeves', + companyName: 'Reeves Construction Partners', + phone: '214-555-5678' + }, // Contractor { id: 'con_001', @@ -688,7 +700,7 @@ const MOCK_USERS = [ name: 'Mike Contractor', companyName: 'Texas Builders LLC', licenseNumber: 'TX-GC-123456', - phone: '214-555-5678' + phone: '214-555-4567' }, // Subcontractor { @@ -704,19 +716,6 @@ const MOCK_USERS = [ phone: '469-555-9012' }, // Vendor (Supplier) - { - id: 'ven_001', - type: 'vendor', - username: 'abc_supply', - email: 'sales@abcsupply.com', - password: 'password', - role: 'VENDOR', - name: 'ABC Supply Co.', - companyName: 'ABC Supply', - tradeType: 'materials', - phone: '972-555-8888' - }, - // Vendor (Supplier) { id: 'ven_001', type: 'vendor', @@ -887,6 +886,123 @@ function generateMockMeetings() { const MOCK_MEETINGS = generateMockMeetings(); +// --- OWNER PROFILES --- + +const MOCK_OWNERS = [ + { + id: 'own_001', + companyName: 'Johnson Development Group', + primaryContact: 'Justin Johnson', + email: 'justin@johnsondev.com', + phone: '972-555-1234', + companyType: 'Real Estate Developer', + portfolioSize: 6, + activeProjects: 4, + completedProjects: 1, + onHoldProjects: 1, + totalInvestmentValue: 187000, + riskScore: 28, + status: 'Active', + location: { city: 'Plano', state: 'TX' }, + memberSince: '2023-06-15', + lastActivityDate: '2026-02-15', + dashboardSummary: { + totalBudget: 187000, + committedCost: 156200, + actualSpent: 131500, + roiProjection: 12.4, + pendingApprovals: 3, + openChangeOrders: 2, + openRFIs: 1, + disputes: 1, + upcomingMilestones: [ + { name: 'Shingle Install Complete', project: '2604 Dunwick Dr', dueDate: '2026-02-14', projectId: 'proj_001' }, + { name: 'Electrical Hookup', project: '6613 Phoenix Pl', dueDate: '2026-02-14', projectId: 'proj_003' }, + { name: 'Flooring and Trim', project: '2612 Dunwick Dr', dueDate: '2026-02-18', projectId: 'proj_004' }, + { name: 'Panel Replacement', project: '3913 Arizona Pl', dueDate: '2026-02-20', projectId: 'proj_006' } + ], + cashFlowForecast: [ + { month: 'Mar 2026', inflow: 45000, outflow: 38000 }, + { month: 'Apr 2026', inflow: 32000, outflow: 28000 }, + { month: 'May 2026', inflow: 18000, outflow: 15000 }, + { month: 'Jun 2026', inflow: 12000, outflow: 8000 }, + { month: 'Jul 2026', inflow: 8000, outflow: 5000 }, + { month: 'Aug 2026', inflow: 5000, outflow: 3000 } + ], + monthlySpend: [ + { month: 'Mar 2025', amount: 0 }, + { month: 'Apr 2025', amount: 0 }, + { month: 'May 2025', amount: 0 }, + { month: 'Jun 2025', amount: 0 }, + { month: 'Jul 2025', amount: 0 }, + { month: 'Aug 2025', amount: 0 }, + { month: 'Sep 2025', amount: 0 }, + { month: 'Oct 2025', amount: 0 }, + { month: 'Nov 2025', amount: 8500 }, + { month: 'Dec 2025', amount: 33200 }, + { month: 'Jan 2026', amount: 52800 }, + { month: 'Feb 2026', amount: 37000 } + ] + } + }, + { + id: 'own_002', + companyName: 'Reeves Construction Partners', + primaryContact: 'Diana Reeves', + email: 'diana@reevesconstruction.com', + phone: '214-555-5678', + companyType: 'Institutional', + portfolioSize: 6, + activeProjects: 3, + completedProjects: 1, + onHoldProjects: 2, + totalInvestmentValue: 430000, + riskScore: 72, + status: 'High Risk', + location: { city: 'Plano', state: 'TX' }, + memberSince: '2024-01-10', + lastActivityDate: '2026-02-12', + dashboardSummary: { + totalBudget: 430000, + committedCost: 385000, + actualSpent: 291300, + roiProjection: 4.8, + pendingApprovals: 5, + openChangeOrders: 5, + openRFIs: 3, + disputes: 2, + upcomingMilestones: [ + { name: 'Section A - TPO Install', project: '6909 Custer Rd', dueDate: '2026-02-28', projectId: 'proj_007' }, + { name: 'Final Grade and Drainage', project: '2112 Winslow Dr', dueDate: '2026-02-10', projectId: 'proj_008' }, + { name: 'Floor 2 Installation', project: '7224 Independence Pkwy', dueDate: '2026-02-14', projectId: 'proj_009' }, + { name: 'Waterproofing Application', project: 'Legacy Town Center', dueDate: '2026-02-12', projectId: 'proj_011' } + ], + cashFlowForecast: [ + { month: 'Mar 2026', inflow: 85000, outflow: 92000 }, + { month: 'Apr 2026', inflow: 72000, outflow: 68000 }, + { month: 'May 2026', inflow: 55000, outflow: 45000 }, + { month: 'Jun 2026', inflow: 40000, outflow: 32000 }, + { month: 'Jul 2026', inflow: 25000, outflow: 18000 }, + { month: 'Aug 2026', inflow: 15000, outflow: 10000 } + ], + monthlySpend: [ + { month: 'Mar 2025', amount: 0 }, + { month: 'Apr 2025', amount: 0 }, + { month: 'May 2025', amount: 0 }, + { month: 'Jun 2025', amount: 0 }, + { month: 'Jul 2025', amount: 0 }, + { month: 'Aug 2025', amount: 0 }, + { month: 'Sep 2025', amount: 14200 }, + { month: 'Oct 2025', amount: 35000 }, + { month: 'Nov 2025', amount: 52000 }, + { month: 'Dec 2025', amount: 68400 }, + { month: 'Jan 2026', amount: 74200 }, + { month: 'Feb 2026', amount: 47500 } + ] + } + } +]; + // --- NEW OWNERS BOX DATA SCHEMAS --- const MOCK_PERSONNEL = [ @@ -897,7 +1013,7 @@ const MOCK_PERSONNEL = [ email: 'jesus@lynkeduppro.com', phone: '214-555-0101', team: 'Sales Team A', - manager: 'Justin Owner', + manager: 'Justin Johnson', sensitiveData: { ssn: '123-45-6789', bankAccount: '9876543210' @@ -962,16 +1078,16 @@ const MOCK_VENDORS = [ } }, spend: { - totalSpend: 154000, - lastPayment: { date: '2026-02-01', amount: 12500 }, - ytdSpend: 45000, - pendingInvoices: 2500 + totalSpend: 218000, + lastPayment: { date: '2026-02-08', amount: 18500 }, + ytdSpend: 67000, + pendingInvoices: 4200 }, performance: { rating: 4.8, onTimeRate: 0.95, - jobsCompleted: 34, - activeJobs: 2 + jobsCompleted: 42, + activeJobs: 3 }, status: 'active' }, @@ -998,16 +1114,16 @@ const MOCK_VENDORS = [ } }, spend: { - totalSpend: 4500, - lastPayment: { date: '2025-12-15', amount: 800 }, + totalSpend: 9200, + lastPayment: { date: '2025-12-15', amount: 1800 }, ytdSpend: 0, - pendingInvoices: 0 + pendingInvoices: 3400 }, performance: { rating: 4.2, onTimeRate: 0.88, - jobsCompleted: 5, - activeJobs: 0 + jobsCompleted: 8, + activeJobs: 1 }, status: 'non_compliant' }, @@ -1030,16 +1146,124 @@ const MOCK_VENDORS = [ coi: { uploaded: true, status: 'compliant', expirationDate: '2027-01-01' } }, spend: { - totalSpend: 250000, - lastPayment: { date: '2026-02-10', amount: 15000 }, - ytdSpend: 85000, - pendingInvoices: 12000 + totalSpend: 312000, + lastPayment: { date: '2026-02-12', amount: 22400 }, + ytdSpend: 98000, + pendingInvoices: 15600 }, performance: { rating: 5.0, onTimeRate: 0.99, - jobsCompleted: 100, // Deliveries - activeJobs: 5 + jobsCompleted: 124, + activeJobs: 7 + }, + status: 'active' + }, + { + id: 'v4', + vendorName: 'Lone Star Plumbing', + vendorType: 'plumbing', + primaryContact: { + name: 'Ray Gutierrez', + email: 'ray@lonestarplumbing.com', + phone: '469-555-0304' + }, + sensitiveData: { + ein: '33-2211009', + bankAccount: '4455667788' + }, + compliance: { + w9: { uploaded: true, status: 'approved' }, + subcontractorAgreement: { uploaded: true, status: 'approved' }, + coi: { + uploaded: true, + expirationDate: '2026-03-05', + status: 'compliant' + } + }, + spend: { + totalSpend: 86000, + lastPayment: { date: '2026-01-20', amount: 7200 }, + ytdSpend: 14500, + pendingInvoices: 8900 + }, + performance: { + rating: 4.5, + onTimeRate: 0.91, + jobsCompleted: 19, + activeJobs: 2 + }, + status: 'active' + }, + { + id: 'v5', + vendorName: 'DFW Foundation Pros', + vendorType: 'foundation', + primaryContact: { + name: 'Tom Atkins', + email: 'tom@dfwfoundation.com', + phone: '214-555-0405' + }, + sensitiveData: { + ein: '44-5566778', + bankAccount: '7788991100' + }, + compliance: { + w9: { uploaded: false, status: 'pending' }, + subcontractorAgreement: { uploaded: true, status: 'approved' }, + coi: { + uploaded: true, + expirationDate: '2026-09-15', + status: 'compliant' + } + }, + spend: { + totalSpend: 142000, + lastPayment: { date: '2026-01-10', amount: 24000 }, + ytdSpend: 24000, + pendingInvoices: 18500 + }, + performance: { + rating: 3.9, + onTimeRate: 0.82, + jobsCompleted: 11, + activeJobs: 2 + }, + status: 'non_compliant' + }, + { + id: 'v6', + vendorName: 'North Texas HVAC Solutions', + vendorType: 'hvac', + primaryContact: { + name: 'Linda Foster', + email: 'linda@ntxhvac.com', + phone: '972-555-0506' + }, + sensitiveData: { + ein: '66-7788990', + bankAccount: '2233445566' + }, + compliance: { + w9: { uploaded: true, status: 'approved' }, + subcontractorAgreement: { uploaded: true, status: 'approved' }, + coi: { + uploaded: true, + expirationDate: '2026-11-20', + status: 'compliant' + } + }, + spend: { + totalSpend: 178000, + lastPayment: { date: '2026-02-05', amount: 9800 }, + ytdSpend: 32000, + pendingInvoices: 6700 + }, + performance: { + rating: 4.6, + onTimeRate: 0.93, + jobsCompleted: 27, + activeJobs: 3 }, status: 'active' } @@ -1051,10 +1275,10 @@ const MOCK_ORDERS = [ vendorId: 'v3', projectId: 'proj_001', projectAddress: '2604 Dunwick Dr, Plano, TX 75023', - orderDate: '2026-02-10', - dueDate: '2026-02-20', - deliveryDate: null, - status: 'shipped', + orderDate: '2026-01-18', + dueDate: '2026-01-28', + deliveryDate: '2026-01-27', + status: 'delivered', items: [ { id: 'item_001', @@ -1076,8 +1300,8 @@ const MOCK_ORDERS = [ } ], subtotal: 3100.00, - tax: 248.00, - total: 3348.00, + tax: 255.75, + total: 3355.75, deliveryAddress: { street: '2604 Dunwick Dr', city: 'Plano', @@ -1086,27 +1310,28 @@ const MOCK_ORDERS = [ instructions: 'Deliver to side yard, call foreman on arrival' }, timeline: [ - { date: '2026-02-10', event: 'Order Placed', status: 'completed' }, - { date: '2026-02-11', event: 'Order Confirmed', status: 'completed' }, - { date: '2026-02-15', event: 'Shipped', status: 'completed' }, - { date: '2026-02-20', event: 'Estimated Delivery', status: 'pending' } + { date: '2026-01-18', event: 'Order Placed', status: 'completed' }, + { date: '2026-01-19', event: 'Order Confirmed', status: 'completed' }, + { date: '2026-01-24', event: 'Shipped', status: 'completed' }, + { date: '2026-01-27', event: 'Delivered', status: 'completed' } ], documents: [ - { id: 'doc_001', type: 'PO', name: 'PO-2604-Dunwick.pdf', url: '#' }, - { id: 'doc_002', type: 'Invoice', name: 'INV-001.pdf', url: '#' } + { id: 'odoc_001', type: 'PO', name: 'PO-2604-Dunwick.pdf', url: '#' }, + { id: 'odoc_002', type: 'Invoice', name: 'INV-ORD001.pdf', url: '#' } ], + // TODO: cross-reference — Vendor: ABC Supply Co., Project Contractor: Texas Builders LLC notes: 'Rush order for weather window', - contactPerson: 'John Smith', - contactPhone: '469-555-0123' + contactPerson: 'Juan Perez', + contactPhone: '469-555-0201' }, { id: 'ORD-002', vendorId: 'v3', projectId: 'proj_002', - projectAddress: 'Plano, TX 75023', - orderDate: '2026-02-05', - dueDate: '2026-02-15', - deliveryDate: '2026-02-14', + projectAddress: '2617 Rothland Ln, Plano, TX 75023', + orderDate: '2025-11-10', + dueDate: '2025-11-20', + deliveryDate: '2025-11-19', status: 'delivered', items: [ { @@ -1117,83 +1342,329 @@ const MOCK_ORDERS = [ unitPrice: 12.50, total: 1875.00, specifications: 'White, 5-inch K-style' + }, + { + id: 'item_004', + description: 'Downspout Extensions', + quantity: 8, + unit: 'pieces', + unitPrice: 28.00, + total: 224.00, + specifications: 'Aluminum, matching white' } ], - subtotal: 1875.00, - tax: 150.00, - total: 2025.00, + subtotal: 2099.00, + tax: 173.17, + total: 2272.17, deliveryAddress: { - street: 'Gutter Replacement Project', + street: '2617 Rothland Ln', city: 'Plano', state: 'TX', zip: '75023', - instructions: 'Standard delivery' + instructions: 'Leave at garage side' }, timeline: [ - { date: '2026-02-05', event: 'Order Placed', status: 'completed' }, - { date: '2026-02-06', event: 'Order Confirmed', status: 'completed' }, - { date: '2026-02-12', event: 'Shipped', status: 'completed' }, - { date: '2026-02-14', event: 'Delivered', status: 'completed' } + { date: '2025-11-10', event: 'Order Placed', status: 'completed' }, + { date: '2025-11-11', event: 'Order Confirmed', status: 'completed' }, + { date: '2025-11-17', event: 'Shipped', status: 'completed' }, + { date: '2025-11-19', event: 'Delivered', status: 'completed' } ], documents: [ - { id: 'doc_003', type: 'PO', name: 'PO-Gutter-Project.pdf', url: '#' }, - { id: 'doc_004', type: 'Delivery Receipt', name: 'DR-002.pdf', url: '#' } + { id: 'odoc_003', type: 'PO', name: 'PO-2617-Rothland.pdf', url: '#' }, + { id: 'odoc_004', type: 'Delivery Receipt', name: 'DR-ORD002.pdf', url: '#' } ], - notes: null, - contactPerson: 'Mike Johnson', - contactPhone: '469-555-0456' + // TODO: cross-reference — Vendor: ABC Supply Co., Project Contractor: Texas Builders LLC + notes: 'Completed project delivery - all materials accounted for', + contactPerson: 'Sales Desk', + contactPhone: '972-555-8888' }, { id: 'ORD-003', - vendorId: 'v3', + vendorId: 'v6', projectId: 'proj_003', - projectAddress: 'Dallas, TX 75201', - orderDate: '2026-02-12', - dueDate: '2026-02-25', + projectAddress: '6613 Phoenix Pl, Plano, TX 75023', + orderDate: '2026-01-25', + dueDate: '2026-02-08', + deliveryDate: null, + status: 'shipped', + items: [ + { + id: 'item_005', + description: 'Commercial HVAC Unit - 5 Ton', + quantity: 2, + unit: 'units', + unitPrice: 4200.00, + total: 8400.00, + specifications: 'Carrier 14 SEER2, R-410A' + }, + { + id: 'item_006', + description: 'Ductwork - Flex', + quantity: 200, + unit: 'linear feet', + unitPrice: 6.50, + total: 1300.00, + specifications: 'R-8 insulated, 8-inch diameter' + } + ], + subtotal: 9700.00, + tax: 800.25, + total: 10500.25, + deliveryAddress: { + street: '6613 Phoenix Pl', + city: 'Plano', + state: 'TX', + zip: '75023', + instructions: 'Equipment pad on south side of building' + }, + timeline: [ + { date: '2026-01-25', event: 'Order Placed', status: 'completed' }, + { date: '2026-01-27', event: 'Order Confirmed', status: 'completed' }, + { date: '2026-02-03', event: 'Shipped', status: 'completed' }, + { date: '2026-02-08', event: 'Estimated Delivery', status: 'pending' } + ], + documents: [ + { id: 'odoc_005', type: 'PO', name: 'PO-6613-Phoenix-HVAC.pdf', url: '#' } + ], + // TODO: cross-reference — Vendor: North Texas HVAC Solutions, Project Contractor: Texas Builders LLC + notes: 'HVAC units backordered 5 days, pushed delivery date', + contactPerson: 'Linda Foster', + contactPhone: '972-555-0506' + }, + { + id: 'ORD-004', + vendorId: 'v5', + projectId: 'proj_008', + projectAddress: '2112 Winslow Dr, Plano, TX 75023', + orderDate: '2025-12-02', + dueDate: '2025-12-15', + deliveryDate: '2025-12-14', + status: 'delivered', + items: [ + { + id: 'item_007', + description: 'Steel Piers - Foundation', + quantity: 18, + unit: 'piers', + unitPrice: 420.00, + total: 7560.00, + specifications: 'Galvanized steel, 12-foot depth rated' + }, + { + id: 'item_008', + description: 'Hydraulic Jacks - Rental', + quantity: 6, + unit: 'units', + unitPrice: 180.00, + total: 1080.00, + specifications: '20-ton capacity, 30-day rental' + } + ], + subtotal: 8640.00, + tax: 712.80, + total: 9352.80, + deliveryAddress: { + street: '2112 Winslow Dr', + city: 'Plano', + state: 'TX', + zip: '75023', + instructions: 'Loading dock access required, heavy equipment' + }, + timeline: [ + { date: '2025-12-02', event: 'Order Placed', status: 'completed' }, + { date: '2025-12-03', event: 'Order Confirmed', status: 'completed' }, + { date: '2025-12-10', event: 'Shipped', status: 'completed' }, + { date: '2025-12-14', event: 'Delivered', status: 'completed' } + ], + documents: [ + { id: 'odoc_006', type: 'PO', name: 'PO-Winslow-Foundation.pdf', url: '#' }, + { id: 'odoc_007', type: 'Invoice', name: 'INV-ORD004.pdf', url: '#' }, + { id: 'odoc_008', type: 'Delivery Receipt', name: 'DR-ORD004.pdf', url: '#' } + ], + // TODO: cross-reference — Vendor: DFW Foundation Pros, Project Contractor: Texas Builders LLC + notes: 'Foundation repair materials - additional piers ordered due to scope expansion', + contactPerson: 'Tom Atkins', + contactPhone: '214-555-0405' + }, + { + id: 'ORD-005', + vendorId: 'v3', + projectId: 'proj_007', + projectAddress: '6909 Custer Rd, Plano, TX 75023', + orderDate: '2026-02-01', + dueDate: '2026-02-14', deliveryDate: null, status: 'confirmed', items: [ { - id: 'item_004', - description: 'Roofing Nails - Coil', - quantity: 20, - unit: 'boxes', - unitPrice: 35.00, - total: 700.00, - specifications: '1-1/4 inch, galvanized' + id: 'item_009', + description: 'Commercial TPO Membrane', + quantity: 120, + unit: 'rolls', + unitPrice: 95.00, + total: 11400.00, + specifications: '60 mil, white, 10-ft wide' }, { - id: 'item_005', - description: 'Flashing - Step', - quantity: 100, - unit: 'pieces', - unitPrice: 4.50, - total: 450.00, - specifications: 'Aluminum, 8-inch' + id: 'item_010', + description: 'Roof Insulation Boards', + quantity: 300, + unit: 'sheets', + unitPrice: 18.50, + total: 5550.00, + specifications: 'Polyiso, 2-inch, 4x8' + }, + { + id: 'item_011', + description: 'Mechanical Fasteners', + quantity: 40, + unit: 'boxes', + unitPrice: 52.00, + total: 2080.00, + specifications: 'FM-approved, #14 x 3-inch' } ], - subtotal: 1150.00, - tax: 92.00, - total: 1242.00, + subtotal: 19030.00, + tax: 1569.98, + total: 20599.98, deliveryAddress: { - street: 'Commercial Building', - city: 'Dallas', + street: '6909 Custer Rd', + city: 'Plano', state: 'TX', - zip: '75201', - instructions: 'Loading dock access required' + zip: '75023', + instructions: 'Crane lift to rooftop, coordinate 48hrs in advance' }, timeline: [ - { date: '2026-02-12', event: 'Order Placed', status: 'completed' }, - { date: '2026-02-13', event: 'Order Confirmed', status: 'completed' }, - { date: '2026-02-20', event: 'Estimated Ship Date', status: 'pending' }, - { date: '2026-02-25', event: 'Estimated Delivery', status: 'pending' } + { date: '2026-02-01', event: 'Order Placed', status: 'completed' }, + { date: '2026-02-03', event: 'Order Confirmed', status: 'completed' }, + { date: '2026-02-10', event: 'Estimated Ship Date', status: 'pending' }, + { date: '2026-02-14', event: 'Estimated Delivery', status: 'pending' } ], documents: [ - { id: 'doc_005', type: 'PO', name: 'PO-Dallas-Commercial.pdf', url: '#' } + { id: 'odoc_009', type: 'PO', name: 'PO-Custer-Commercial-Roof.pdf', url: '#' } ], - notes: 'Confirm delivery time 24hrs in advance', - contactPerson: 'Sarah Williams', - contactPhone: '214-555-0789' + // TODO: cross-reference — Vendor: ABC Supply Co., Project Contractor: Texas Builders LLC + notes: 'Large commercial order - confirm crane availability before ship', + contactPerson: 'Sales Desk', + contactPhone: '972-555-8888' + }, + { + id: 'ORD-006', + vendorId: 'v4', + projectId: 'proj_012', + projectAddress: 'Dallas/Plano Marriott at Legacy Town Center, 7121 Bishop Rd, Plano, TX 75024', + orderDate: '2026-01-15', + dueDate: '2026-01-30', + deliveryDate: '2026-01-29', + status: 'delivered', + items: [ + { + id: 'item_012', + description: 'Commercial Grade PVC Pipe - 4 inch', + quantity: 400, + unit: 'linear feet', + unitPrice: 8.75, + total: 3500.00, + specifications: 'Schedule 40, SDR-26' + }, + { + id: 'item_013', + description: 'Ball Valves - Brass', + quantity: 24, + unit: 'pieces', + unitPrice: 42.00, + total: 1008.00, + specifications: '2-inch, full port, lead-free' + }, + { + id: 'item_014', + description: 'Water Heater - Commercial Tankless', + quantity: 3, + unit: 'units', + unitPrice: 2800.00, + total: 8400.00, + specifications: 'Rinnai CU199iN, 199K BTU' + } + ], + subtotal: 12908.00, + tax: 1064.91, + total: 13972.91, + deliveryAddress: { + street: '7121 Bishop Rd', + city: 'Plano', + state: 'TX', + zip: '75024', + instructions: 'Service entrance, coordinate with building management' + }, + timeline: [ + { date: '2026-01-15', event: 'Order Placed', status: 'completed' }, + { date: '2026-01-16', event: 'Order Confirmed', status: 'completed' }, + { date: '2026-01-24', event: 'Shipped', status: 'completed' }, + { date: '2026-01-29', event: 'Delivered', status: 'completed' } + ], + documents: [ + { id: 'odoc_010', type: 'PO', name: 'PO-Marriott-Plumbing.pdf', url: '#' }, + { id: 'odoc_011', type: 'Delivery Receipt', name: 'DR-ORD006.pdf', url: '#' }, + { id: 'odoc_012', type: 'Invoice', name: 'INV-ORD006.pdf', url: '#' } + ], + // TODO: cross-reference — Vendor: Lone Star Plumbing, Project Contractor: Texas Builders LLC + notes: 'Plumbing overhaul materials - project currently on hold pending permit review', + contactPerson: 'Ray Gutierrez', + contactPhone: '469-555-0304' + }, + { + id: 'ORD-007', + vendorId: 'v3', + projectId: 'proj_004', + projectAddress: '2612 Dunwick Dr, Plano, TX 75023', + orderDate: '2026-02-15', + dueDate: '2026-02-28', + deliveryDate: null, + status: 'pending', + items: [ + { id: 'item_015', description: 'Interior Drywall Sheets', quantity: 80, unit: 'sheets', unitPrice: 14.50, total: 1160.00, specifications: '4x8, 1/2-inch, fire-rated' }, + { id: 'item_016', description: 'Joint Compound', quantity: 12, unit: 'buckets', unitPrice: 22.00, total: 264.00, specifications: 'All-purpose, 5-gallon' } + ], + subtotal: 1424.00, + tax: 117.48, + total: 1541.48, + deliveryAddress: { street: '2612 Dunwick Dr', city: 'Plano', state: 'TX', zip: '75023', instructions: 'Front entrance, stack in garage' }, + timeline: [ + { date: '2026-02-15', event: 'Order Placed', status: 'completed' }, + { date: '2026-02-28', event: 'Estimated Delivery', status: 'pending' } + ], + documents: [{ id: 'odoc_013', type: 'PO', name: 'PO-Dunwick-Drywall.pdf', url: '#' }], + notes: 'Awaiting vendor confirmation', + contactPerson: 'Sales Desk', + contactPhone: '972-555-8888' + }, + { + id: 'ORD-008', + vendorId: 'v3', + projectId: 'proj_009', + projectAddress: '7224 Independence Pkwy, Plano, TX 75025', + orderDate: '2026-02-10', + dueDate: '2026-02-22', + deliveryDate: null, + status: 'shipped', + items: [ + { id: 'item_017', description: 'Window Sealant - Exterior Grade', quantity: 48, unit: 'tubes', unitPrice: 8.50, total: 408.00, specifications: 'Silicone, clear, UV-resistant' }, + { id: 'item_018', description: 'Flashing Tape - Self-Adhesive', quantity: 20, unit: 'rolls', unitPrice: 35.00, total: 700.00, specifications: '6-inch, peel-and-stick, 75-ft' } + ], + subtotal: 1108.00, + tax: 91.41, + total: 1199.41, + deliveryAddress: { street: '7224 Independence Pkwy', city: 'Plano', state: 'TX', zip: '75025', instructions: 'Leave at side entrance' }, + timeline: [ + { date: '2026-02-10', event: 'Order Placed', status: 'completed' }, + { date: '2026-02-11', event: 'Order Confirmed', status: 'completed' }, + { date: '2026-02-17', event: 'Shipped', status: 'completed' }, + { date: '2026-02-22', event: 'Estimated Delivery', status: 'pending' } + ], + documents: [{ id: 'odoc_014', type: 'PO', name: 'PO-Independence-Sealant.pdf', url: '#' }], + notes: 'In transit via FedEx Freight', + contactPerson: 'Sales Desk', + contactPhone: '972-555-8888' } ]; @@ -1203,13 +1674,13 @@ const MOCK_VENDOR_INVOICES = [ vendorId: 'v3', orderId: 'ORD-002', projectId: 'proj_002', - project: 'Plano, TX 75023', - invoiceDate: '2026-02-14', - dueDate: '2026-03-01', - amount: 2025.00, + project: '2617 Rothland Ln, Plano, TX 75023', + invoiceDate: '2025-11-22', + dueDate: '2025-12-07', + amount: 2272.17, status: 'paid', paymentTerms: 'Net 15', - description: 'Gutter System Delivery' + description: 'Gutter System and Downspout Materials' }, { id: 'INV-2026-002', @@ -1217,47 +1688,127 @@ const MOCK_VENDOR_INVOICES = [ orderId: 'ORD-001', projectId: 'proj_001', project: '2604 Dunwick Dr, Plano, TX 75023', - invoiceDate: '2026-02-15', - dueDate: '2026-03-02', - amount: 3348.00, + invoiceDate: '2026-01-28', + dueDate: '2026-02-12', + amount: 3355.75, status: 'pending', paymentTerms: 'Net 15', - description: 'Roofing Materials Delivery' + description: 'Roofing Shingles and Underlayment Delivery' }, { id: 'INV-2026-003', - vendorId: 'v3', + vendorId: 'v6', orderId: 'ORD-003', projectId: 'proj_003', - project: 'Dallas, TX 75201', - invoiceDate: '2026-02-13', - dueDate: '2026-02-28', - amount: 1242.00, + project: '6613 Phoenix Pl, Plano, TX 75023', + invoiceDate: '2026-02-04', + dueDate: '2026-02-19', + amount: 10500.25, status: 'pending', paymentTerms: 'Net 15', - description: 'Roofing Accessories' + description: 'HVAC Units and Ductwork Delivery' + }, + { + id: 'INV-2026-004', + vendorId: 'v5', + orderId: 'ORD-004', + projectId: 'proj_008', + project: '2112 Winslow Dr, Plano, TX 75023', + invoiceDate: '2025-12-16', + dueDate: '2025-12-31', + amount: 9352.80, + status: 'paid', + paymentTerms: 'Net 15', + description: 'Foundation Steel Piers and Equipment Rental' + }, + { + id: 'INV-2026-005', + vendorId: 'v3', + orderId: 'ORD-005', + projectId: 'proj_007', + project: '6909 Custer Rd, Plano, TX 75023', + invoiceDate: '2026-02-03', + dueDate: '2026-02-18', + amount: 20599.98, + status: 'pending', + paymentTerms: 'Net 15', + description: 'Commercial TPO Roofing Materials' + }, + { + id: 'INV-2026-006', + vendorId: 'v4', + orderId: 'ORD-006', + projectId: 'proj_012', + project: 'Dallas/Plano Marriott, 7121 Bishop Rd, Plano, TX 75024', + invoiceDate: '2026-01-30', + dueDate: '2026-02-14', + amount: 13972.91, + status: 'pending', + paymentTerms: 'Net 15', + description: 'Commercial Plumbing Materials and Water Heaters' + }, + { + id: 'INV-2026-007', + vendorId: 'v1', + orderId: 'ORD-LABOR-001', + projectId: 'proj_004', + project: '2612 Dunwick Dr, Plano, TX 75023', + invoiceDate: '2026-02-10', + dueDate: '2026-02-25', + amount: 8750.00, + status: 'pending', + paymentTerms: 'Net 15', + description: 'Interior Renovation - Roofing Crew Labor Week 3' + }, + { + id: 'INV-2026-008', + vendorId: 'v3', + orderId: 'ORD-MAT-006', + projectId: 'proj_006', + project: '3913 Arizona Pl, Plano, TX 75023', + invoiceDate: '2026-02-07', + dueDate: '2026-02-22', + amount: 2850.00, + status: 'pending', + paymentTerms: 'Net 15', + description: 'Electrical Panel and Circuit Breaker Materials' }, { id: 'INV-2025-045', - vendorId: 'v3', - orderId: 'ORD-OLD-001', - projectId: 'proj_old_001', - project: 'Frisco, TX 75034', - invoiceDate: '2025-12-20', - dueDate: '2026-01-05', - amount: 4500.00, + vendorId: 'v1', + orderId: 'ORD-HIST-001', + projectId: 'proj_010', + project: '2100 Legacy Dr, Plano, TX 75023', + invoiceDate: '2025-10-15', + dueDate: '2025-10-30', + amount: 14200.00, status: 'paid', paymentTerms: 'Net 15', - description: 'Complete Roofing Package' + description: 'Parking Lot Resurfacing Crew Labor' + }, + { + id: 'INV-2025-046', + vendorId: 'v3', + orderId: 'ORD-HIST-002', + projectId: 'proj_010', + project: '2100 Legacy Dr, Plano, TX 75023', + invoiceDate: '2025-10-02', + dueDate: '2025-10-17', + amount: 8400.00, + status: 'paid', + paymentTerms: 'Net 15', + description: 'Asphalt and Sealant Materials' } ]; const MOCK_DOCUMENTS = [ { id: 'd1', + name: 'Jesus Gonzales - Driver License', entityId: 'p1', entityType: 'personnel', entityName: 'Jesus Gonzales', + vendorId: 'v1', documentType: 'DL', fileName: 'JG_DriverLicense.pdf', fileUrl: '#', @@ -1271,9 +1822,11 @@ const MOCK_DOCUMENTS = [ }, { id: 'd2', + name: 'Sparky Electric - Certificate of Insurance', entityId: 'v2', entityType: 'vendor', entityName: 'Sparky Electric', + vendorId: 'v2', documentType: 'COI', fileName: 'Sparky_COI_2025.pdf', fileUrl: '#', @@ -1287,136 +1840,799 @@ const MOCK_DOCUMENTS = [ }, { id: 'd3', + name: 'Sarah Sales - Pay Plan Agreement', entityId: 'p2', entityType: 'personnel', entityName: 'Sarah Sales', + vendorId: 'v1', documentType: 'PAY_PLAN', fileName: 'Sarah_PayPlan_Signed.pdf', fileUrl: '#', uploadedBy: 'Sarah Sales', uploadedAt: '2026-02-14', - expirationDate: null, + expirationDate: '2027-02-14', status: 'pending_review', aiConfidence: 0.88, reviewedBy: null, reviewedAt: null + }, + { + id: 'd4', + name: 'Lone Star Plumbing - Certificate of Insurance', + entityId: 'v4', + entityType: 'vendor', + entityName: 'Lone Star Plumbing', + vendorId: 'v4', + documentType: 'COI', + fileName: 'LoneStarPlumbing_COI_2026.pdf', + fileUrl: '#', + uploadedBy: 'Ray Gutierrez', + uploadedAt: '2025-03-10', + expirationDate: '2026-03-05', + status: 'approved', + aiConfidence: 0.96, + reviewedBy: 'Admin', + reviewedAt: '2025-03-11' + }, + { + id: 'd5', + name: 'DFW Foundation Pros - W9 Form', + entityId: 'v5', + entityType: 'vendor', + entityName: 'DFW Foundation Pros', + vendorId: 'v5', + documentType: 'W9', + fileName: 'DFW_Foundation_W9.pdf', + fileUrl: '#', + uploadedBy: 'Tom Atkins', + uploadedAt: '2026-01-20', + expirationDate: '2027-01-20', + status: 'pending_review', + aiConfidence: 0.82, + reviewedBy: null, + reviewedAt: null + }, + { + id: 'd6', + name: 'Texas Roofing Crew A - Subcontractor Agreement', + entityId: 'v1', + entityType: 'vendor', + entityName: 'Texas Roofing Crew A', + vendorId: 'v1', + documentType: 'SUB_AGREEMENT', + fileName: 'TexasRoofing_SubAgreement_2026.pdf', + fileUrl: '#', + uploadedBy: 'Juan Perez', + uploadedAt: '2025-12-01', + expirationDate: '2026-12-01', + status: 'approved', + aiConfidence: 0.97, + reviewedBy: 'Admin', + reviewedAt: '2025-12-02' + }, + { + id: 'd7', + name: 'North Texas HVAC - Certificate of Insurance', + entityId: 'v6', + entityType: 'vendor', + entityName: 'North Texas HVAC Solutions', + vendorId: 'v6', + documentType: 'COI', + fileName: 'NTXHVAC_COI_2026.pdf', + fileUrl: '#', + uploadedBy: 'Linda Foster', + uploadedAt: '2025-11-25', + expirationDate: '2026-11-20', + status: 'approved', + aiConfidence: 0.94, + reviewedBy: 'Admin', + reviewedAt: '2025-11-26' + }, + { + id: 'd8', + name: 'ABC Supply Co. - Certificate of Insurance', + entityId: 'v3', + entityType: 'vendor', + entityName: 'ABC Supply Co.', + vendorId: 'v3', + documentType: 'COI', + fileName: 'ABCSupply_COI_2027.pdf', + fileUrl: '#', + uploadedBy: 'Sales Desk', + uploadedAt: '2026-01-05', + expirationDate: '2027-01-01', + status: 'approved', + aiConfidence: 0.99, + reviewedBy: 'Admin', + reviewedAt: '2026-01-06' + }, + { + id: 'd9', + name: 'Sparky Electric - W9 Form', + entityId: 'v2', + entityType: 'vendor', + entityName: 'Sparky Electric', + vendorId: 'v2', + documentType: 'W9', + fileName: 'Sparky_W9_2026.pdf', + fileUrl: '#', + uploadedBy: 'Mike Spark', + uploadedAt: '2026-02-01', + expirationDate: '2027-02-01', + status: 'approved', + aiConfidence: 0.91, + reviewedBy: 'Admin', + reviewedAt: '2026-02-02' + }, + { + id: 'd10', + name: 'DFW Foundation Pros - Certificate of Insurance', + entityId: 'v5', + entityType: 'vendor', + entityName: 'DFW Foundation Pros', + vendorId: 'v5', + documentType: 'COI', + fileName: 'DFW_Foundation_COI_2026.pdf', + fileUrl: '#', + uploadedBy: 'Tom Atkins', + uploadedAt: '2025-09-20', + expirationDate: '2026-09-15', + status: 'approved', + aiConfidence: 0.93, + reviewedBy: 'Admin', + reviewedAt: '2025-09-21' } ]; const MOCK_PROJECTS = [ + // ===================================================================== + // OWNER 1: Marcus Whitfield (own_001) — 6 Projects + // Active owner, strong financials, one project under dispute + // ===================================================================== + + // Project 1: Active, on track, healthy budget { - id: 'proj_1', + id: 'proj_001', + ownerId: 'own_001', propertyId: 'P-2602', address: '2604 Dunwick Dr, Plano, TX 75023', contractorId: 'con_001', subcontractorIds: ['sub_001'], projectType: 'Roof Replacement', status: 'active', - budget: 25000, - spent: 12500, - margin: 0.35, - startDate: '2026-02-01', - endDate: '2026-02-15', - completionPercentage: 65, + phase: 'Structural', + budget: 45000, + approvedBudget: 45000, + committedCost: 38500, + actualCost: 22500, + spent: 22500, + variancePercent: -50.0, + margin: 0.40, + startDate: '2026-01-20', + endDate: '2026-03-05', + completionPercentage: 55, + healthScore: 82, + openRFIs: 0, + changeOrderCount: 0, + pendingInvoiceCount: 2, + budgetBreakdown: [ + { category: 'Materials - Shingles', allocated: 15000, committed: 14200, actual: 8500 }, + { category: 'Materials - Underlayment', allocated: 3500, committed: 3355, actual: 3355 }, + { category: 'Labor - Tear Off', allocated: 8000, committed: 8000, actual: 8000 }, + { category: 'Labor - Install', allocated: 12000, committed: 7200, actual: 0 }, + { category: 'Permits & Inspection', allocated: 2500, committed: 2500, actual: 2645 }, + { category: 'Cleanup & Haul-Away', allocated: 4000, committed: 3245, actual: 0 } + ], + changeOrders: [], + rfis: [], + riskLog: [ + { id: 'risk_001_1', description: 'Weather delay risk during February', severity: 'Medium', likelihood: 'Likely', mitigation: 'Monitor 10-day forecast, flex schedule for rain days', status: 'monitoring' }, + { id: 'risk_001_2', description: 'Material price increase from supplier', severity: 'Low', likelihood: 'Unlikely', mitigation: 'Price locked with PO', status: 'mitigated' } + ], + issueLog: [], + paymentSchedule: [ + { id: 'ps_001_1', milestone: 'Mobilization (20%)', amount: 9000, dueDate: '2026-01-22', status: 'paid' }, + { id: 'ps_001_2', milestone: 'Tear-off Complete (30%)', amount: 13500, dueDate: '2026-02-01', status: 'paid' }, + { id: 'ps_001_3', milestone: 'Install Complete (30%)', amount: 13500, dueDate: '2026-02-20', status: 'pending' }, + { id: 'ps_001_4', milestone: 'Final Inspection (20%)', amount: 9000, dueDate: '2026-03-05', status: 'pending' } + ], + activityTimeline: [ + { date: '2026-02-12', action: 'Invoice Submitted', user: 'ABC Supply Co.', details: 'INV-001-2 for roofing materials - $3,355.75' }, + { date: '2026-02-08', action: 'Milestone Completed', user: 'Texas Roofing Crew A', details: 'Dry-In Inspection passed' }, + { date: '2026-02-01', action: 'Payment Released', user: 'Justin Johnson', details: 'Tear-off milestone payment - $12,000' }, + { date: '2026-01-27', action: 'Material Delivered', user: 'ABC Supply Co.', details: 'Shingles and underlayment delivered to site' }, + { date: '2026-01-20', action: 'Project Started', user: 'Texas Builders LLC', details: 'Roof replacement project kicked off' } + ], + // TODO: cross-reference — Contractor: Texas Builders LLC, Vendors: ABC Supply Co., Texas Roofing Crew A milestones: [ - { - id: 'ms_1', - name: 'Material Delivery', - dueDate: '2026-02-02', - status: 'completed', - assignedTo: 'ven_001' // Supplier - }, - { - id: 'ms_2', - name: 'Roof Tear-off', - dueDate: '2026-02-04', - status: 'completed', - assignedTo: 'con_001' - }, - { - id: 'ms_3', - name: 'Dry-In Inspection', - dueDate: '2026-02-06', - status: 'completed', - assignedTo: 'con_001' - }, - { - id: 'ms_4', - name: 'Shingle Install', - dueDate: '2026-02-10', - status: 'in_progress', - assignedTo: 'con_001' - }, - { - id: 'ms_5', - name: 'Final Inspection', - dueDate: '2026-02-14', - status: 'pending', - assignedTo: 'con_001' - } + { id: 'ms_001_1', name: 'Material Delivery', dueDate: '2026-01-25', status: 'completed', assignedTo: 'v3' }, + { id: 'ms_001_2', name: 'Roof Tear-off', dueDate: '2026-01-30', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_001_3', name: 'Dry-In Inspection', dueDate: '2026-02-03', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_001_4', name: 'Shingle Install', dueDate: '2026-02-14', status: 'in_progress', assignedTo: 'v1' }, + { id: 'ms_001_5', name: 'Final Inspection', dueDate: '2026-03-01', status: 'pending', assignedTo: 'con_001' } ], invoices: [ - { - id: 'inv_1', - amount: 10000, - submittedBy: 'con_001', - status: 'paid', - dueDate: '2026-02-05', - datePaid: '2026-02-06' - }, - { - id: 'inv_2', - amount: 5000, - submittedBy: 'sub_001', - status: 'pending', - dueDate: '2026-02-10' - }, - { - id: 'inv_3', - amount: 12000, - submittedBy: 'ven_001', // ABC Supply - status: 'pending', - dueDate: '2026-02-12' - } + { id: 'inv_001_1', amount: 12000, submittedBy: 'con_001', status: 'paid', dueDate: '2026-02-01', datePaid: '2026-02-02' }, + { id: 'inv_001_2', amount: 3355.75, submittedBy: 'v3', status: 'pending', dueDate: '2026-02-12' }, + { id: 'inv_001_3', amount: 7200, submittedBy: 'v1', status: 'pending', dueDate: '2026-02-20' } ], documents: [ - { name: 'Scope of Work', url: '#', type: 'contract' }, - { name: 'Permit #22-401', url: '#', type: 'permit' }, - { name: 'Material Order', url: '#', type: 'invoice' } + { name: 'Scope of Work - Roof Replacement', url: '#', type: 'contract' }, + { name: 'Permit #26-0401', url: '#', type: 'permit' }, + { name: 'Material Order PO-2604', url: '#', type: 'invoice' } ] }, + + // Project 2: Completed, zero variance (perfectly on budget) { - id: 'proj_2', + id: 'proj_002', + ownerId: 'own_001', propertyId: 'P-2608', address: '2617 Rothland Ln, Plano, TX 75023', contractorId: 'con_001', subcontractorIds: [], projectType: 'Gutter Replacement', - status: 'scheduled', - budget: 4500, - spent: 0, - margin: 0.40, - startDate: '2026-02-20', - endDate: '2026-02-22', - completionPercentage: 0, + status: 'completed', + phase: 'Handover', + budget: 8500, + approvedBudget: 8500, + committedCost: 8500, + actualCost: 8500, + spent: 8500, + variancePercent: 0, + margin: 0.35, + startDate: '2025-11-15', + endDate: '2025-12-10', + completionPercentage: 100, + healthScore: 100, + openRFIs: 0, + changeOrderCount: 0, + pendingInvoiceCount: 0, + // TODO: cross-reference — Contractor: Texas Builders LLC, Vendors: ABC Supply Co. milestones: [ - { - id: 'ms_2_1', - name: 'Material Delivery', - dueDate: '2026-02-19', - status: 'pending', - assignedTo: 'ven_001' - }, - { - id: 'ms_2_2', - name: 'Installation', - dueDate: '2026-02-20', - status: 'pending', - assignedTo: 'con_001' - } + { id: 'ms_002_1', name: 'Old Gutter Removal', dueDate: '2025-11-18', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_002_2', name: 'Material Delivery', dueDate: '2025-11-20', status: 'completed', assignedTo: 'v3' }, + { id: 'ms_002_3', name: 'Seamless Gutter Install', dueDate: '2025-11-28', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_002_4', name: 'Downspout Routing', dueDate: '2025-12-03', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_002_5', name: 'Final Walkthrough', dueDate: '2025-12-08', status: 'completed', assignedTo: 'con_001' } ], - invoices: [], - documents: [] + invoices: [ + { id: 'inv_002_1', amount: 2272.17, submittedBy: 'v3', status: 'paid', dueDate: '2025-11-25', datePaid: '2025-11-26' }, + { id: 'inv_002_2', amount: 4200, submittedBy: 'con_001', status: 'paid', dueDate: '2025-12-05', datePaid: '2025-12-06' }, + { id: 'inv_002_3', amount: 2027.83, submittedBy: 'con_001', status: 'paid', dueDate: '2025-12-10', datePaid: '2025-12-10' } + ], + documents: [ + { name: 'Scope of Work - Gutter', url: '#', type: 'contract' }, + { name: 'Completion Certificate', url: '#', type: 'certificate' }, + { name: 'Final Invoice Package', url: '#', type: 'invoice' }, + { name: 'Warranty Registration', url: '#', type: 'warranty' } + ] + }, + + // Project 3: Delayed with missed milestone + { + id: 'proj_003', + ownerId: 'own_001', + propertyId: 'P-2610', + address: '6613 Phoenix Pl, Plano, TX 75023', + contractorId: 'con_001', + subcontractorIds: ['sub_001'], + projectType: 'HVAC Installation', + status: 'active', + phase: 'MEP', + budget: 32000, + approvedBudget: 32000, + committedCost: 28200, + actualCost: 18400, + spent: 18400, + variancePercent: -42.5, + margin: 0.30, + startDate: '2026-01-06', + endDate: '2026-02-28', + completionPercentage: 42, + healthScore: 58, + openRFIs: 1, + changeOrderCount: 0, + pendingInvoiceCount: 2, + // TODO: cross-reference — Contractor: Texas Builders LLC, Vendors: North Texas HVAC Solutions, ABC Supply Co. + milestones: [ + { id: 'ms_003_1', name: 'Site Preparation', dueDate: '2026-01-10', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_003_2', name: 'Ductwork Rough-In', dueDate: '2026-01-20', status: 'completed', assignedTo: 'v6' }, + { id: 'ms_003_3', name: 'Equipment Delivery', dueDate: '2026-01-30', status: 'completed', assignedTo: 'v6' }, + { id: 'ms_003_4', name: 'Unit Installation', dueDate: '2026-02-07', status: 'in_progress', assignedTo: 'v6' }, + { id: 'ms_003_5', name: 'Electrical Hookup', dueDate: '2026-02-14', status: 'pending', assignedTo: 'sub_001' }, + { id: 'ms_003_6', name: 'System Test and Commissioning', dueDate: '2026-02-25', status: 'pending', assignedTo: 'v6' } + ], + invoices: [ + { id: 'inv_003_1', amount: 8200, submittedBy: 'con_001', status: 'paid', dueDate: '2026-01-15', datePaid: '2026-01-16' }, + { id: 'inv_003_2', amount: 10500.25, submittedBy: 'v6', status: 'pending', dueDate: '2026-02-19' }, + { id: 'inv_003_3', amount: 3400, submittedBy: 'v2', status: 'pending', dueDate: '2026-02-22' } + ], + documents: [ + { name: 'HVAC Design Plans', url: '#', type: 'plans' }, + { name: 'Mechanical Permit #26-0187', url: '#', type: 'permit' }, + { name: 'Equipment Spec Sheets', url: '#', type: 'specification' } + ] + }, + + // Project 4: Active, over budget (spent > budget) + { + id: 'proj_004', + ownerId: 'own_001', + propertyId: 'P-2600', + address: '2612 Dunwick Dr, Plano, TX 75023', + contractorId: 'con_001', + subcontractorIds: ['sub_001'], + projectType: 'Interior Renovation', + status: 'active', + phase: 'Finishing', + budget: 55000, + approvedBudget: 55000, + committedCost: 62300, + actualCost: 62300, + spent: 62300, + variancePercent: 13.3, + margin: -0.13, + startDate: '2025-12-01', + endDate: '2026-02-28', + completionPercentage: 78, + healthScore: 35, + openRFIs: 0, + changeOrderCount: 2, + pendingInvoiceCount: 2, + // TODO: cross-reference — Contractor: Texas Builders LLC, Vendors: ABC Supply Co., Texas Roofing Crew A, Sparky Electric + milestones: [ + { id: 'ms_004_1', name: 'Demo and Abatement', dueDate: '2025-12-10', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_004_2', name: 'Framing and Structural', dueDate: '2025-12-22', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_004_3', name: 'Electrical Rough-In', dueDate: '2026-01-08', status: 'completed', assignedTo: 'sub_001' }, + { id: 'ms_004_4', name: 'Plumbing Rough-In', dueDate: '2026-01-15', status: 'completed', assignedTo: 'v4' }, + { id: 'ms_004_5', name: 'Drywall and Paint', dueDate: '2026-02-05', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_004_6', name: 'Flooring and Trim', dueDate: '2026-02-18', status: 'in_progress', assignedTo: 'con_001' }, + { id: 'ms_004_7', name: 'Final Punch List', dueDate: '2026-02-25', status: 'pending', assignedTo: 'con_001' } + ], + invoices: [ + { id: 'inv_004_1', amount: 18000, submittedBy: 'con_001', status: 'paid', dueDate: '2025-12-15', datePaid: '2025-12-16' }, + { id: 'inv_004_2', amount: 15500, submittedBy: 'con_001', status: 'paid', dueDate: '2026-01-10', datePaid: '2026-01-12' }, + { id: 'inv_004_3', amount: 12800, submittedBy: 'v3', status: 'paid', dueDate: '2026-01-20', datePaid: '2026-01-22' }, + { id: 'inv_004_3b', amount: 4800, submittedBy: 'sub_001', status: 'paid', dueDate: '2026-01-12', datePaid: '2026-01-14' }, + { id: 'inv_004_4', amount: 8750, submittedBy: 'v1', status: 'pending', dueDate: '2026-02-25' }, + { id: 'inv_004_5', amount: 7250, submittedBy: 'con_001', status: 'pending', dueDate: '2026-02-28' } + ], + documents: [ + { name: 'Interior Renovation Contract', url: '#', type: 'contract' }, + { name: 'Building Permit #25-1892', url: '#', type: 'permit' }, + { name: 'Change Order #1 - Scope Expansion', url: '#', type: 'change_order' }, + { name: 'Change Order #2 - Material Upgrade', url: '#', type: 'change_order' }, + { name: 'Asbestos Abatement Report', url: '#', type: 'report' } + ] + }, + + // Project 5: On Hold with pending dispute + { + id: 'proj_005', + ownerId: 'own_001', + propertyId: 'P-2601', + address: '2608 Dunwick Dr, Plano, TX 75023', + contractorId: 'con_001', + subcontractorIds: [], + projectType: 'Siding Replacement', + status: 'on_hold', + phase: 'Structural', + budget: 28000, + approvedBudget: 28000, + committedCost: 20700, + actualCost: 14200, + spent: 14200, + variancePercent: -49.3, + margin: 0.25, + startDate: '2026-01-06', + endDate: '2026-02-20', + completionPercentage: 38, + healthScore: 42, + openRFIs: 0, + changeOrderCount: 0, + pendingInvoiceCount: 1, + // TODO: cross-reference — Contractor: Texas Builders LLC, Vendors: ABC Supply Co. + milestones: [ + { id: 'ms_005_1', name: 'Scaffolding Setup', dueDate: '2026-01-08', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_005_2', name: 'Old Siding Removal', dueDate: '2026-01-15', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_005_3', name: 'Moisture Barrier Install', dueDate: '2026-01-22', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_005_4', name: 'New Siding Install', dueDate: '2026-02-05', status: 'pending', assignedTo: 'con_001' }, + { id: 'ms_005_5', name: 'Trim and Caulking', dueDate: '2026-02-15', status: 'pending', assignedTo: 'con_001' } + ], + invoices: [ + { id: 'inv_005_1', amount: 8400, submittedBy: 'con_001', status: 'paid', dueDate: '2026-01-12', datePaid: '2026-01-13' }, + { id: 'inv_005_2', amount: 5800, submittedBy: 'v3', status: 'paid', dueDate: '2026-01-20', datePaid: '2026-01-21' }, + { id: 'inv_005_3', amount: 6500, submittedBy: 'con_001', status: 'pending', dueDate: '2026-02-10' } + ], + documents: [ + { name: 'Siding Replacement Contract', url: '#', type: 'contract' }, + { name: 'Dispute Notice - Material Defect', url: '#', type: 'dispute' }, + { name: 'Manufacturer Warranty Claim', url: '#', type: 'warranty' }, + { name: 'Site Photos - Defective Panels', url: '#', type: 'photos' } + ] + }, + + // Project 6: Active, on track, early stage + { + id: 'proj_006', + ownerId: 'own_001', + propertyId: 'P-2625', + address: '3913 Arizona Pl, Plano, TX 75023', + contractorId: 'con_001', + subcontractorIds: ['sub_001'], + projectType: 'Electrical Upgrade', + status: 'active', + phase: 'MEP', + budget: 18500, + approvedBudget: 18500, + committedCost: 12400, + actualCost: 5600, + spent: 5600, + variancePercent: -69.7, + margin: 0.38, + startDate: '2026-02-03', + endDate: '2026-03-15', + completionPercentage: 22, + healthScore: 88, + openRFIs: 0, + changeOrderCount: 0, + pendingInvoiceCount: 1, + // TODO: cross-reference — Contractor: Texas Builders LLC, Vendors: Sparky Electric + milestones: [ + { id: 'ms_006_1', name: 'Panel Assessment', dueDate: '2026-02-05', status: 'completed', assignedTo: 'sub_001' }, + { id: 'ms_006_2', name: 'Permit Approval', dueDate: '2026-02-10', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_006_3', name: 'Panel Replacement', dueDate: '2026-02-20', status: 'in_progress', assignedTo: 'sub_001' }, + { id: 'ms_006_4', name: 'Circuit Re-routing', dueDate: '2026-03-01', status: 'pending', assignedTo: 'sub_001' }, + { id: 'ms_006_5', name: 'City Inspection', dueDate: '2026-03-12', status: 'pending', assignedTo: 'con_001' } + ], + invoices: [ + { id: 'inv_006_1', amount: 2200, submittedBy: 'sub_001', status: 'paid', dueDate: '2026-02-08', datePaid: '2026-02-09' }, + { id: 'inv_006_2', amount: 3400, submittedBy: 'sub_001', status: 'pending', dueDate: '2026-02-22' } + ], + documents: [ + { name: 'Electrical Upgrade Proposal', url: '#', type: 'contract' }, + { name: 'Electrical Permit #26-0223', url: '#', type: 'permit' }, + { name: 'Load Calculation Report', url: '#', type: 'report' } + ] + }, + + // ===================================================================== + // OWNER 2: Diana Reeves (own_002) — 6 Projects + // High risk owner, vendor payment delays, over-budget project + // ===================================================================== + + // Project 7: Active, on track, healthy budget (commercial) + { + id: 'proj_007', + ownerId: 'own_002', + propertyId: 'P-2640', + address: '6909 Custer Rd, Plano, TX 75023', + contractorId: 'con_001', + subcontractorIds: ['sub_001'], + projectType: 'Commercial Roof Replacement', + status: 'active', + phase: 'Structural', + budget: 125000, + approvedBudget: 125000, + committedCost: 98000, + actualCost: 48000, + spent: 48000, + variancePercent: -61.6, + margin: 0.35, + startDate: '2026-01-27', + endDate: '2026-04-15', + completionPercentage: 32, + healthScore: 78, + openRFIs: 0, + changeOrderCount: 0, + pendingInvoiceCount: 2, + budgetBreakdown: [ + { category: 'Materials - TPO Membrane', allocated: 38000, committed: 35400, actual: 11400 }, + { category: 'Materials - Insulation', allocated: 18000, committed: 16800, actual: 5550 }, + { category: 'Materials - Fasteners/Adhesive', allocated: 8000, committed: 7200, actual: 2080 }, + { category: 'Labor - Section A', allocated: 22000, committed: 18500, actual: 18500 }, + { category: 'Labor - Section B', allocated: 22000, committed: 8000, actual: 0 }, + { category: 'Crane Rental', allocated: 8000, committed: 5600, actual: 4200 }, + { category: 'Permits & Inspection', allocated: 4000, committed: 3500, actual: 3270 }, + { category: 'Safety Equipment', allocated: 5000, committed: 3000, actual: 3000 } + ], + changeOrders: [ + { id: 'CO-007-1', title: 'Additional Drainage Scuppers', amount: 4200, status: 'pending', dateSubmitted: '2026-02-10', description: 'Building management requested 4 additional scuppers on east elevation for improved water management' } + ], + rfis: [ + { id: 'RFI-007-1', subject: 'Deck Condition Under Section B', status: 'open', submittedBy: 'Texas Roofing Crew A', dateOpened: '2026-02-14', dateClosed: null }, + { id: 'RFI-007-2', subject: 'Flashing Detail at Parapet Wall', status: 'closed', submittedBy: 'Texas Builders LLC', dateOpened: '2026-02-03', dateClosed: '2026-02-06' } + ], + riskLog: [ + { id: 'risk_007_1', description: 'Roof deck deterioration may require additional repairs', severity: 'High', likelihood: 'Possible', mitigation: 'Structural assessment scheduled before Section B tear-off', status: 'open' }, + { id: 'risk_007_2', description: 'High winds could delay membrane installation', severity: 'Medium', likelihood: 'Likely', mitigation: 'Schedule membrane work on low-wind days, use mechanical fasteners as backup', status: 'monitoring' }, + { id: 'risk_007_3', description: 'Supply chain delay on TPO rolls', severity: 'Low', likelihood: 'Unlikely', mitigation: 'Full order confirmed and 60% already delivered', status: 'mitigated' } + ], + issueLog: [ + { id: 'issue_007_1', title: 'Crane access blocked by tenant parking', priority: 'Medium', status: 'resolved', assignedTo: 'Texas Builders LLC', dateReported: '2026-02-01' }, + { id: 'issue_007_2', title: 'Section A tear-off revealed wet insulation', priority: 'High', status: 'open', assignedTo: 'Texas Roofing Crew A', dateReported: '2026-02-14' } + ], + paymentSchedule: [ + { id: 'ps_007_1', milestone: 'Mobilization (20%)', amount: 25000, dueDate: '2026-01-30', status: 'paid' }, + { id: 'ps_007_2', milestone: 'Section A Complete (25%)', amount: 31250, dueDate: '2026-03-01', status: 'pending' }, + { id: 'ps_007_3', milestone: 'Section B Complete (25%)', amount: 31250, dueDate: '2026-03-30', status: 'pending' }, + { id: 'ps_007_4', milestone: 'Final Inspection (20%)', amount: 25000, dueDate: '2026-04-10', status: 'pending' }, + { id: 'ps_007_5', milestone: 'Warranty Registration (10%)', amount: 12500, dueDate: '2026-04-15', status: 'pending' } + ], + activityTimeline: [ + { date: '2026-02-14', action: 'RFI Submitted', user: 'Texas Roofing Crew A', details: 'RFI-007-1: Deck condition under Section B needs assessment' }, + { date: '2026-02-14', action: 'Issue Reported', user: 'Texas Roofing Crew A', details: 'Wet insulation found during Section A tear-off' }, + { date: '2026-02-10', action: 'Change Order Submitted', user: 'Texas Builders LLC', details: 'CO-007-1: Additional drainage scuppers requested - $4,200' }, + { date: '2026-02-06', action: 'RFI Closed', user: 'Texas Builders LLC', details: 'RFI-007-2: Flashing detail resolved per manufacturer spec' }, + { date: '2026-02-03', action: 'Milestone Completed', user: 'ABC Supply Co.', details: 'Material procurement complete - all TPO delivered' }, + { date: '2026-01-30', action: 'Payment Released', user: 'Diana Reeves', details: 'Mobilization payment - $25,000' }, + { date: '2026-01-27', action: 'Project Started', user: 'Texas Builders LLC', details: 'Commercial roof replacement kicked off' } + ], + // TODO: cross-reference — Contractor: Texas Builders LLC, Vendors: ABC Supply Co., Texas Roofing Crew A + milestones: [ + { id: 'ms_007_1', name: 'Roof Survey and Assessment', dueDate: '2026-01-30', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_007_2', name: 'Material Procurement', dueDate: '2026-02-07', status: 'completed', assignedTo: 'v3' }, + { id: 'ms_007_3', name: 'Section A - Tear Off', dueDate: '2026-02-14', status: 'completed', assignedTo: 'v1' }, + { id: 'ms_007_4', name: 'Section A - TPO Install', dueDate: '2026-02-28', status: 'in_progress', assignedTo: 'v1' }, + { id: 'ms_007_5', name: 'Section B - Tear Off', dueDate: '2026-03-15', status: 'pending', assignedTo: 'v1' }, + { id: 'ms_007_6', name: 'Section B - TPO Install', dueDate: '2026-03-30', status: 'pending', assignedTo: 'v1' }, + { id: 'ms_007_7', name: 'Final Inspection and Warranty', dueDate: '2026-04-10', status: 'pending', assignedTo: 'con_001' } + ], + invoices: [ + { id: 'inv_007_1', amount: 25000, submittedBy: 'con_001', status: 'paid', dueDate: '2026-02-05', datePaid: '2026-02-06' }, + { id: 'inv_007_2', amount: 20599.98, submittedBy: 'v3', status: 'pending', dueDate: '2026-02-18' }, + { id: 'inv_007_3', amount: 18500, submittedBy: 'v1', status: 'pending', dueDate: '2026-02-28' } + ], + documents: [ + { name: 'Commercial Roofing Contract', url: '#', type: 'contract' }, + { name: 'Commercial Building Permit #26-0312', url: '#', type: 'permit' }, + { name: 'TPO Manufacturer Spec', url: '#', type: 'specification' }, + { name: 'Roof Survey Report', url: '#', type: 'report' }, + { name: 'Safety Plan - Fall Protection', url: '#', type: 'safety' } + ] + }, + + // Project 8: Active, over budget (spent > budget) + { + id: 'proj_008', + ownerId: 'own_002', + propertyId: 'P-2641', + address: '2112 Winslow Dr, Plano, TX 75023', + contractorId: 'con_001', + subcontractorIds: [], + projectType: 'Foundation Repair', + status: 'active', + phase: 'Finishing', + budget: 75000, + approvedBudget: 75000, + committedCost: 91500, + actualCost: 91500, + spent: 91500, + variancePercent: 22.0, + margin: -0.22, + startDate: '2025-11-18', + endDate: '2026-02-28', + completionPercentage: 88, + healthScore: 25, + openRFIs: 0, + changeOrderCount: 2, + pendingInvoiceCount: 2, + // TODO: cross-reference — Contractor: Texas Builders LLC, Vendors: DFW Foundation Pros, ABC Supply Co. + milestones: [ + { id: 'ms_008_1', name: 'Structural Engineering Report', dueDate: '2025-11-22', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_008_2', name: 'Pier Installation - East', dueDate: '2025-12-06', status: 'completed', assignedTo: 'v5' }, + { id: 'ms_008_3', name: 'Pier Installation - West', dueDate: '2025-12-20', status: 'completed', assignedTo: 'v5' }, + { id: 'ms_008_4', name: 'Slab Lifting', dueDate: '2026-01-10', status: 'completed', assignedTo: 'v5' }, + { id: 'ms_008_5', name: 'Plumbing Re-test', dueDate: '2026-01-25', status: 'completed', assignedTo: 'v4' }, + { id: 'ms_008_6', name: 'Final Grade and Drainage', dueDate: '2026-02-10', status: 'in_progress', assignedTo: 'con_001' }, + { id: 'ms_008_7', name: 'Engineering Re-certification', dueDate: '2026-02-25', status: 'pending', assignedTo: 'con_001' } + ], + invoices: [ + { id: 'inv_008_1', amount: 30000, submittedBy: 'con_001', status: 'paid', dueDate: '2025-12-01', datePaid: '2025-12-03' }, + { id: 'inv_008_2', amount: 9352.80, submittedBy: 'v5', status: 'paid', dueDate: '2025-12-20', datePaid: '2025-12-22' }, + { id: 'inv_008_3', amount: 24000, submittedBy: 'v5', status: 'paid', dueDate: '2026-01-15', datePaid: '2026-01-18' }, + { id: 'inv_008_4', amount: 18500, submittedBy: 'v5', status: 'pending', dueDate: '2026-02-15' }, + { id: 'inv_008_5', amount: 9647.20, submittedBy: 'con_001', status: 'pending', dueDate: '2026-02-28' } + ], + documents: [ + { name: 'Foundation Repair Contract', url: '#', type: 'contract' }, + { name: 'Structural Engineering Report', url: '#', type: 'report' }, + { name: 'Pier Layout Diagram', url: '#', type: 'plans' }, + { name: 'Change Order #1 - Additional Piers', url: '#', type: 'change_order' }, + { name: 'Change Order #2 - Plumbing Reroute', url: '#', type: 'change_order' }, + { name: 'Soil Analysis Report', url: '#', type: 'report' } + ] + }, + + // Project 9: Delayed with missed milestone + { + id: 'proj_009', + ownerId: 'own_002', + propertyId: 'P-2642', + address: '7224 Independence Pkwy, Plano, TX 75025', + contractorId: 'con_001', + subcontractorIds: ['sub_001'], + projectType: 'Window Replacement', + status: 'active', + budget: 42000, + spent: 25800, + margin: 0.28, + startDate: '2026-01-13', + endDate: '2026-03-10', + completionPercentage: 50, + phase: 'Finishing', + approvedBudget: 42000, + committedCost: 32000, + actualCost: 25800, + variancePercent: -38.6, + healthScore: 55, + openRFIs: 0, + changeOrderCount: 0, + pendingInvoiceCount: 1, + milestones: [ + { id: 'ms_009_1', name: 'Window Measurements', dueDate: '2026-01-16', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_009_2', name: 'Custom Window Fabrication', dueDate: '2026-01-30', status: 'completed', assignedTo: 'v3' }, + { id: 'ms_009_3', name: 'Floor 1 Installation', dueDate: '2026-02-07', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_009_4', name: 'Floor 2 Installation', dueDate: '2026-02-14', status: 'in_progress', assignedTo: 'con_001' }, + { id: 'ms_009_5', name: 'Exterior Trim and Sealant', dueDate: '2026-02-25', status: 'pending', assignedTo: 'con_001' }, + { id: 'ms_009_6', name: 'Energy Audit Verification', dueDate: '2026-03-05', status: 'pending', assignedTo: 'con_001' } + ], + invoices: [ + { id: 'inv_009_1', amount: 14000, submittedBy: 'v3', status: 'paid', dueDate: '2026-02-01', datePaid: '2026-02-03' }, + { id: 'inv_009_2', amount: 8200, submittedBy: 'con_001', status: 'paid', dueDate: '2026-02-10', datePaid: '2026-02-11' }, + { id: 'inv_009_3', amount: 9800, submittedBy: 'con_001', status: 'pending', dueDate: '2026-02-25' } + ], + documents: [ + { name: 'Window Replacement Agreement', url: '#', type: 'contract' }, + { name: 'Custom Window Order Confirmation', url: '#', type: 'purchase_order' }, + { name: 'Energy Rating Certificates', url: '#', type: 'certificate' } + ] + }, + + // Project 10: Completed, zero variance + { + id: 'proj_010', + ownerId: 'own_002', + propertyId: 'P-2656', + address: '2100 Legacy Dr, Plano, TX 75023', + contractorId: 'con_001', + subcontractorIds: [], + projectType: 'Parking Lot Resurface', + status: 'completed', + budget: 35000, + spent: 35000, + margin: 0.30, + startDate: '2025-09-15', + endDate: '2025-10-28', + completionPercentage: 100, + phase: 'Handover', + approvedBudget: 35000, + committedCost: 35000, + actualCost: 35000, + variancePercent: 0.0, + healthScore: 100, + openRFIs: 0, + changeOrderCount: 0, + pendingInvoiceCount: 0, + milestones: [ + { id: 'ms_010_1', name: 'Surface Milling', dueDate: '2025-09-20', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_010_2', name: 'Base Repair', dueDate: '2025-09-28', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_010_3', name: 'Asphalt Paving', dueDate: '2025-10-08', status: 'completed', assignedTo: 'v1' }, + { id: 'ms_010_4', name: 'Sealcoat Application', dueDate: '2025-10-18', status: 'completed', assignedTo: 'v1' }, + { id: 'ms_010_5', name: 'Striping and Signage', dueDate: '2025-10-25', status: 'completed', assignedTo: 'con_001' } + ], + invoices: [ + { id: 'inv_010_1', amount: 8400, submittedBy: 'v3', status: 'paid', dueDate: '2025-09-25', datePaid: '2025-09-26' }, + { id: 'inv_010_2', amount: 14200, submittedBy: 'v1', status: 'paid', dueDate: '2025-10-12', datePaid: '2025-10-13' }, + { id: 'inv_010_3', amount: 12400, submittedBy: 'con_001', status: 'paid', dueDate: '2025-10-28', datePaid: '2025-10-28' } + ], + documents: [ + { name: 'Paving Contract', url: '#', type: 'contract' }, + { name: 'Completion Certificate', url: '#', type: 'certificate' }, + { name: 'ADA Compliance Verification', url: '#', type: 'report' }, + { name: 'Final Payment Release', url: '#', type: 'invoice' } + ] + }, + + // Project 11: Disputed with open change orders and unresolved issues + { + id: 'proj_011', + ownerId: 'own_002', + propertyId: 'P-2645', + address: 'Legacy Town Center, 5760 Daniel Rd, Plano, TX 75024', + contractorId: 'con_001', + subcontractorIds: ['sub_001'], + projectType: 'Exterior Facade Restoration', + status: 'disputed', + budget: 95000, + spent: 68400, + margin: 0.20, + startDate: '2025-12-08', + endDate: '2026-03-20', + completionPercentage: 62, + phase: 'MEP', + approvedBudget: 95000, + committedCost: 88000, + actualCost: 68400, + variancePercent: -28.0, + healthScore: 30, + openRFIs: 2, + changeOrderCount: 3, + pendingInvoiceCount: 2, + milestones: [ + { id: 'ms_011_1', name: 'Scaffolding and Protection', dueDate: '2025-12-12', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_011_2', name: 'Surface Cleaning and Prep', dueDate: '2025-12-22', status: 'completed', assignedTo: 'con_001' }, + { id: 'ms_011_3', name: 'Masonry Repair - Phase 1', dueDate: '2026-01-10', status: 'completed', assignedTo: 'v1' }, + { id: 'ms_011_4', name: 'Masonry Repair - Phase 2', dueDate: '2026-01-28', status: 'completed', assignedTo: 'v1' }, + { id: 'ms_011_5', name: 'Waterproofing Application', dueDate: '2026-02-12', status: 'in_progress', assignedTo: 'con_001' }, + { id: 'ms_011_6', name: 'Window Sealant Replacement', dueDate: '2026-02-28', status: 'pending', assignedTo: 'v4' }, + { id: 'ms_011_7', name: 'Final Restoration and Paint', dueDate: '2026-03-15', status: 'pending', assignedTo: 'con_001' } + ], + invoices: [ + { id: 'inv_011_1', amount: 22000, submittedBy: 'con_001', status: 'paid', dueDate: '2025-12-18', datePaid: '2025-12-20' }, + { id: 'inv_011_2', amount: 18400, submittedBy: 'v1', status: 'paid', dueDate: '2026-01-15', datePaid: '2026-01-18' }, + { id: 'inv_011_3', amount: 15200, submittedBy: 'v3', status: 'paid', dueDate: '2026-01-30', datePaid: '2026-02-02' }, + { id: 'inv_011_4', amount: 12800, submittedBy: 'con_001', status: 'pending', dueDate: '2026-02-20' }, + { id: 'inv_011_5', amount: 8900, submittedBy: 'v4', status: 'pending', dueDate: '2026-03-05' } + ], + documents: [ + { name: 'Facade Restoration Master Contract', url: '#', type: 'contract' }, + { name: 'Building Permit #25-2101', url: '#', type: 'permit' }, + { name: 'Change Order #1 - Hidden Water Damage', url: '#', type: 'change_order' }, + { name: 'Change Order #2 - Additional Masonry', url: '#', type: 'change_order' }, + { name: 'Change Order #3 - Structural Reinforcement', url: '#', type: 'change_order' }, + { name: 'Dispute Filing - Scope Disagreement', url: '#', type: 'dispute' }, + { name: 'RFI #1 - Original vs Current Scope', url: '#', type: 'rfi' }, + { name: 'RFI #2 - Waterproofing Method', url: '#', type: 'rfi' } + ] + }, + + // Project 12: On Hold + { + id: 'proj_012', + ownerId: 'own_002', + propertyId: 'P-2644', + address: 'Dallas/Plano Marriott at Legacy Town Center, 7121 Bishop Rd, Plano, TX 75024', + contractorId: 'con_001', + subcontractorIds: [], + projectType: 'Plumbing Overhaul', + status: 'on_hold', + budget: 58000, + spent: 22600, + margin: 0.32, + startDate: '2026-01-13', + endDate: '2026-04-01', + completionPercentage: 28, + phase: 'Foundation', + approvedBudget: 58000, + committedCost: 34000, + actualCost: 22600, + variancePercent: -61.0, + healthScore: 45, + openRFIs: 0, + changeOrderCount: 0, + pendingInvoiceCount: 2, + milestones: [ + { id: 'ms_012_1', name: 'Plumbing Assessment', dueDate: '2026-01-17', status: 'completed', assignedTo: 'v4' }, + { id: 'ms_012_2', name: 'Material Procurement', dueDate: '2026-01-25', status: 'completed', assignedTo: 'v4' }, + { id: 'ms_012_3', name: 'Main Line Replacement', dueDate: '2026-02-05', status: 'completed', assignedTo: 'v4' }, + { id: 'ms_012_4', name: 'Water Heater Installation', dueDate: '2026-02-18', status: 'pending', assignedTo: 'v4' }, + { id: 'ms_012_5', name: 'Branch Line Reroute', dueDate: '2026-03-05', status: 'pending', assignedTo: 'v4' }, + { id: 'ms_012_6', name: 'Pressure Testing', dueDate: '2026-03-20', status: 'pending', assignedTo: 'v4' }, + { id: 'ms_012_7', name: 'City Inspection', dueDate: '2026-03-28', status: 'pending', assignedTo: 'con_001' } + ], + invoices: [ + { id: 'inv_012_1', amount: 8600, submittedBy: 'v4', status: 'paid', dueDate: '2026-01-22', datePaid: '2026-01-24' }, + { id: 'inv_012_2', amount: 13972.91, submittedBy: 'v4', status: 'pending', dueDate: '2026-02-14' }, + { id: 'inv_012_3', amount: 12000, submittedBy: 'con_001', status: 'pending', dueDate: '2026-02-28' } + ], + documents: [ + { name: 'Plumbing Overhaul Contract', url: '#', type: 'contract' }, + { name: 'Plumbing Permit #26-0156', url: '#', type: 'permit' }, + { name: 'Scope Hold Notice - Permit Review', url: '#', type: 'notice' }, + { name: 'Material Delivery Receipt', url: '#', type: 'receipt' } + ] } ]; @@ -1431,6 +2647,7 @@ export const MockStoreProvider = ({ children }) => { const [salesHistory, setSalesHistory] = useState(MOCK_SALES_HISTORY); // New Data States + const [owners, setOwners] = useState(MOCK_OWNERS); const [personnel, setPersonnel] = useState(MOCK_PERSONNEL); const [vendors, setVendors] = useState(MOCK_VENDORS); const [documents, setDocuments] = useState(MOCK_DOCUMENTS); @@ -1517,6 +2734,7 @@ export const MockStoreProvider = ({ children }) => { salesHistory, // Exported for Leaderboard // New Owners Box Data + owners, personnel, vendors, documents, diff --git a/src/pages/AdminSchedule.jsx b/src/pages/AdminSchedule.jsx index 5fadb4b..5475b6b 100644 --- a/src/pages/AdminSchedule.jsx +++ b/src/pages/AdminSchedule.jsx @@ -371,6 +371,7 @@ const AdminSchedule = () => { + igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew? ); }; diff --git a/src/pages/LeaderboardPage.jsx b/src/pages/LeaderboardPage.jsx index 6fa08da..523318d 100644 --- a/src/pages/LeaderboardPage.jsx +++ b/src/pages/LeaderboardPage.jsx @@ -221,6 +221,7 @@ const LeaderboardPage = () => { + igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew? ); }; diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index 6367797..d3d66e0 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -278,6 +278,7 @@ const Login = () => { + igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew? ); }; diff --git a/src/pages/Maps.jsx b/src/pages/Maps.jsx index b49a532..1634165 100644 --- a/src/pages/Maps.jsx +++ b/src/pages/Maps.jsx @@ -414,6 +414,7 @@ function Maps() { loadingAddr={loadingAddr} onSave={handleSave} /> + igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew? ); } diff --git a/src/pages/NotFound.jsx b/src/pages/NotFound.jsx index 50ce549..9a7fb02 100644 --- a/src/pages/NotFound.jsx +++ b/src/pages/NotFound.jsx @@ -50,6 +50,7 @@ const NotFound = () => {
LynkedUpPro Infrastructure
+ igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew? ); }; diff --git a/src/pages/contractor/ContractorDashboard.jsx b/src/pages/contractor/ContractorDashboard.jsx index 0bd2d8a..cfe0da3 100644 --- a/src/pages/contractor/ContractorDashboard.jsx +++ b/src/pages/contractor/ContractorDashboard.jsx @@ -4,44 +4,51 @@ import { useAuth } from '../../context/AuthContext'; import { useMockStore } from '../../data/mockStore'; import { StatCard } from '../../components/StatCard'; import { SpotlightCard } from '../../components/SpotlightCard'; -import { Hammer, CheckSquare, Clock, AlertTriangle, Calendar, DollarSign } from 'lucide-react'; +import { Hammer, CheckSquare, Clock, AlertTriangle, Calendar, DollarSign, PauseCircle } from 'lucide-react'; import ProjectDetailsModal from '../../components/contractor/ProjectDetailsModal'; import FinancialSummaryModal from '../../components/contractor/FinancialSummaryModal'; +import TaskDetailsModal from '../../components/contractor/TaskDetailsModal'; const ContractorDashboard = () => { const { user } = useAuth(); const { projects } = useMockStore(); // 1. Identify current contractor - // Filter projects where this contractor is the Lead - const myProjects = projects.filter(p => p.contractorId === user?.id || p.contractorId === 'con_001'); // Default to con_001 for mock + const myProjects = projects.filter(p => p.contractorId === user?.id || p.contractorId === 'con_001'); // 2. Aggregate Data const activeProjects = myProjects.filter(p => p.status === 'active').length; - const upcomingProjects = myProjects.filter(p => p.status === 'scheduled').length; + const onHoldProjects = myProjects.filter(p => p.status === 'on_hold').length; + const completedProjects = myProjects.filter(p => p.status === 'completed').length; // Calculate total budget managed const totalBudget = myProjects.reduce((sum, p) => sum + (p.budget || 0), 0); const totalSpent = myProjects.reduce((sum, p) => sum + (p.spent || 0), 0); const budgetUtilization = totalBudget > 0 ? Math.round((totalSpent / totalBudget) * 100) : 0; - // Upcoming Milestones (next 7 days) - const upcomingMilestones = myProjects.flatMap(p => - p.milestones.filter(m => { - const dueDate = new Date(m.dueDate); - const today = new Date('2026-02-05'); // Fixed context date - const diffTime = Math.abs(dueDate - today); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - return diffDays <= 7 && m.status !== 'completed'; - }).map(m => ({ ...m, projectAddress: p.address })) + // Pending invoice amounts + const pendingInvoiceAmount = myProjects.reduce((sum, p) => + sum + (p.invoices || []).filter(i => i.status === 'pending').reduce((s, i) => s + i.amount, 0), 0 ); + // Upcoming Milestones (next 14 days from context date) + const upcomingMilestones = myProjects.flatMap(p => + (p.milestones || []).filter(m => { + const dueDate = new Date(m.dueDate); + const today = new Date('2026-02-18'); // Current context date + const diffDays = (dueDate - today) / (1000 * 60 * 60 * 24); + return diffDays >= -3 && diffDays <= 14 && m.status !== 'completed'; + }).map(m => ({ ...m, projectAddress: p.address, projectId: p.id })) + ).sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate)); + const navigate = useNavigate(); // Modal state const [selectedProject, setSelectedProject] = useState(null); const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); const [isFinancialModalOpen, setIsFinancialModalOpen] = useState(false); + const [selectedTask, setSelectedTask] = useState(null); + const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); // Financial data for modal const financialData = { @@ -49,7 +56,7 @@ const ContractorDashboard = () => { spent: totalSpent, remaining: totalBudget - totalSpent, paid: totalSpent, - pending: 0, + pending: pendingInvoiceAmount, items: myProjects.map(p => ({ date: p.startDate, description: p.address, @@ -64,6 +71,11 @@ const ContractorDashboard = () => { setIsProjectModalOpen(true); }; + const handleTaskClick = (task) => { + setSelectedTask(task); + setIsTaskModalOpen(true); + }; + return (
{/* Header */} @@ -89,18 +101,24 @@ const ContractorDashboard = () => { {/* KPI Cards */}
- - +
navigate('/contractor/projects')} className="cursor-pointer"> + +
+
navigate('/contractor/projects')} className="cursor-pointer"> + +
setIsFinancialModalOpen(true)} className="cursor-pointer"> { suffix="%" />
- +
{ if (upcomingMilestones.length > 0) handleTaskClick(upcomingMilestones[0]); }} className="cursor-pointer"> + +
{/* Main Content */} @@ -141,8 +162,14 @@ const ContractorDashboard = () => {

{proj.address}

{proj.projectType} • Started {new Date(proj.startDate).toLocaleDateString()}

- - On Track + proj.budget + ? 'bg-red-100 dark:bg-red-500/20 text-red-600 dark:text-red-400' + : proj.completionPercentage >= 70 + ? 'bg-emerald-100 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400' + : 'bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400' + }`}> + {proj.spent > proj.budget ? 'Over Budget' : proj.completionPercentage >= 70 ? 'On Track' : 'In Progress'} @@ -185,15 +212,37 @@ const ContractorDashboard = () => { {/* Right Panel: Tasks & Alerts */}
-

Critical Tasks (Next 7 Days)

+

Critical Tasks (Next 14 Days)

{upcomingMilestones.length > 0 ? upcomingMilestones.map((ms, idx) => ( -
-
+
handleTaskClick(ms)} + className={`flex items-start gap-3 p-3 rounded-lg transition-colors cursor-pointer group ${ + new Date(ms.dueDate) < new Date('2026-02-18') + ? 'bg-red-50 dark:bg-red-500/5 hover:bg-red-100 dark:hover:bg-red-500/10' + : ms.status === 'in_progress' + ? 'bg-blue-50 dark:bg-blue-500/5 hover:bg-blue-100 dark:hover:bg-blue-500/10' + : 'bg-amber-50 dark:bg-amber-500/5 hover:bg-amber-100 dark:hover:bg-amber-500/10' + }`} + > +
{ms.name}

{ms.projectAddress}

-

Due: {ms.dueDate}

+
+

Due: {ms.dueDate}

+ {ms.status.replace('_', ' ')} +
)) : ( @@ -233,6 +282,12 @@ const ContractorDashboard = () => { role="CONTRACTOR" data={financialData} /> + setIsTaskModalOpen(false)} + task={selectedTask} + /> + igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?
); }; diff --git a/src/pages/contractor/ProjectList.jsx b/src/pages/contractor/ProjectList.jsx index ac8396f..9b0a98b 100644 --- a/src/pages/contractor/ProjectList.jsx +++ b/src/pages/contractor/ProjectList.jsx @@ -136,6 +136,7 @@ const ProjectList = () => { onClose={() => setIsProjectModalOpen(false)} project={selectedProject} /> + igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?
); }; diff --git a/src/pages/owner/DocumentManagement.jsx b/src/pages/owner/DocumentManagement.jsx index e6a0ede..7dd6425 100644 --- a/src/pages/owner/DocumentManagement.jsx +++ b/src/pages/owner/DocumentManagement.jsx @@ -1,42 +1,64 @@ -import React from 'react'; +import React, { useMemo } from 'react'; +import { useMockStore } from '../../data/mockStore'; import DocumentReviewQueue from '../../components/documents/DocumentReviewQueue'; -import { FileText, Shield, AlertTriangle, CheckCircle, Clock } from 'lucide-react'; +import { FileText, Shield, AlertTriangle, CheckCircle, Clock, XCircle } from 'lucide-react'; import { SpotlightCard } from '../../components/SpotlightCard'; +import { toast } from 'sonner'; const DocumentManagement = () => { + const { documents } = useMockStore(); + + // Live stats from actual data + const stats = useMemo(() => { + const pendingReview = documents.filter(d => d.status === 'pending_review').length; + const expired = documents.filter(d => d.status === 'expired').length; + const expiringSoon = documents.filter(d => { + if (!d.expirationDate || d.status === 'expired') return false; + const expDate = new Date(d.expirationDate); + const today = new Date('2026-02-18'); + const diffDays = (expDate - today) / (1000 * 60 * 60 * 24); + return diffDays > 0 && diffDays <= 30; + }).length; + const approved = documents.filter(d => d.status === 'approved').length; + return { pendingReview, expired, expiringSoon, approved, total: documents.length }; + }, [documents]); + return ( -
+
{/* Ambient Background Glows */}
-
+

Document Control

Review, approve, and manage critical business documents.

-
+
Audit Log
-
-
+
{/* Main Content: Review Queue */}
- -
+ +
@@ -45,7 +67,7 @@ const DocumentManagement = () => {

Documents requiring your attention

-
+
@@ -53,28 +75,44 @@ const DocumentManagement = () => { {/* Sidebar: Compliance Overview */}
- +

Quick Stats

- Pending Approval - 3 + + Pending Review + + {stats.pendingReview}
- Expiring Soon (30d) - 1 + + Expired + + {stats.expired}
+ + Expiring Soon (30d) + + {stats.expiringSoon} +
+
+ + Approved + + {stats.approved} +
+
Total Documents - 124 + {stats.total}
- +

Compliance Tip @@ -86,6 +124,7 @@ const DocumentManagement = () => {

+ igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?
); }; diff --git a/src/pages/owner/OwnerSnapshot.jsx b/src/pages/owner/OwnerSnapshot.jsx index 8303736..1079304 100644 --- a/src/pages/owner/OwnerSnapshot.jsx +++ b/src/pages/owner/OwnerSnapshot.jsx @@ -1,40 +1,131 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; +import { useMockStore } from '../../data/mockStore'; import FinancialKPICards from '../../components/owner/FinancialKPICards'; import UrgentItemsPanel from '../../components/owner/UrgentItemsPanel'; import FinancialDetailsModal from '../../components/owner/FinancialDetailsModal'; import ActionCenterModal from '../../components/owner/ActionCenterModal'; import { SpotlightCard } from '../../components/SpotlightCard'; +import { + BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend +} from 'recharts'; +import { + Briefcase, TrendingUp, AlertTriangle, Shield, ChevronRight, ArrowUpRight +} from 'lucide-react'; const OwnerSnapshot = () => { const { user } = useAuth(); const navigate = useNavigate(); + const { owners, projects } = useMockStore(); + + // Find the current owner profile + const owner = useMemo(() => + owners?.find(o => o.id === user?.id) || null + , [owners, user]); + + // Filter projects for this owner + const ownerProjects = useMemo(() => + projects.filter(p => p.ownerId === user?.id) + , [projects, user]); // Modal States const [financialModal, setFinancialModal] = useState({ isOpen: false, type: 'revenue' }); const [actionModal, setActionModal] = useState({ isOpen: false, filter: 'all' }); - // Handlers const handleFinancialClick = (type) => { setFinancialModal({ isOpen: true, type }); }; const handleActionClick = (id) => { - // Map IDs to filter types - const filterMap = { - 'docs': 'docs', - 'vendors': 'vendors', - 'invoices': 'invoices', - 'all': 'all' - }; + const filterMap = { docs: 'docs', vendors: 'vendors', invoices: 'invoices', all: 'all' }; setActionModal({ isOpen: true, filter: filterMap[id] || 'all' }); }; - // Get time of day for greeting + // Greeting const hour = new Date().getHours(); const greeting = hour < 12 ? 'Good Morning' : hour < 18 ? 'Good Afternoon' : 'Good Evening'; + // --- Derived Stats --- + const projectStats = useMemo(() => { + const active = ownerProjects.filter(p => p.status === 'active').length; + const completed = ownerProjects.filter(p => p.status === 'completed').length; + const delayed = ownerProjects.filter(p => p.status === 'delayed').length; + const onHold = ownerProjects.filter(p => p.status === 'on_hold').length; + const disputed = ownerProjects.filter(p => p.status === 'disputed').length; + const totalBudget = ownerProjects.reduce((sum, p) => sum + (p.approvedBudget || p.budget || 0), 0); + const totalSpent = ownerProjects.reduce((sum, p) => sum + (p.actualCost || p.spent || 0), 0); + const avgHealth = ownerProjects.length > 0 + ? Math.round(ownerProjects.reduce((sum, p) => sum + (p.healthScore || 0), 0) / ownerProjects.length) + : 0; + return { active, completed, delayed, onHold, disputed, totalBudget, totalSpent, avgHealth, total: ownerProjects.length }; + }, [ownerProjects]); + + // --- Chart Data --- + const statusChartData = useMemo(() => [ + { name: 'Active', value: projectStats.active, color: '#22c55e' }, + { name: 'Completed', value: projectStats.completed, color: '#3b82f6' }, + { name: 'Delayed', value: projectStats.delayed, color: '#f59e0b' }, + { name: 'On Hold', value: projectStats.onHold, color: '#a855f7' }, + { name: 'Disputed', value: projectStats.disputed, color: '#ef4444' }, + ].filter(d => d.value > 0), [projectStats]); + + const budgetChartData = useMemo(() => + ownerProjects.slice(0, 6).map(p => ({ + name: p.projectType.length > 14 ? p.projectType.slice(0, 14) + '...' : p.projectType, + budget: (p.approvedBudget || p.budget || 0) / 1000, + spent: (p.actualCost || p.spent || 0) / 1000, + })) + , [ownerProjects]); + + const monthlySpendData = owner?.dashboardSummary?.monthlySpend || []; + + // Activity feed from owner's projects + const activityFeed = useMemo(() => { + const items = []; + ownerProjects.forEach(p => { + if (p.activityTimeline) { + p.activityTimeline.forEach(a => items.push({ ...a, projectType: p.projectType, projectId: p.id })); + } + }); + // Sort by date desc, take top 5 + items.sort((a, b) => new Date(b.date) - new Date(a.date)); + return items.slice(0, 5); + }, [ownerProjects]); + + const formatCurrency = (val) => { + if (val >= 1000000) return `$${(val / 1000000).toFixed(1)}M`; + if (val >= 1000) return `$${(val / 1000).toFixed(0)}k`; + return `$${val}`; + }; + + const getRiskColor = (score) => { + if (score <= 30) return 'text-emerald-500 bg-emerald-100 dark:bg-emerald-500/10'; + if (score <= 60) return 'text-amber-500 bg-amber-100 dark:bg-amber-500/10'; + return 'text-red-500 bg-red-100 dark:bg-red-500/10'; + }; + + const getHealthColor = (score) => { + if (score >= 70) return 'text-emerald-500'; + if (score >= 40) return 'text-amber-500'; + return 'text-red-500'; + }; + + const CustomTooltip = ({ active, payload, label }) => { + if (active && payload?.length) { + return ( +
+

{label}

+ {payload.map((p, i) => ( +

+ {p.name}: ${p.value}k +

+ ))} +
+ ); + } + return null; + }; return (
@@ -44,44 +135,136 @@ const OwnerSnapshot = () => {
-
+
{/* Header Section */}
-

+

{greeting}, {user?.name?.split(' ')[0] || 'Owner'}

-

Here's your business snapshot for today.

+

+ {owner ? `${owner.companyName} — ${owner.portfolioSize} projects in portfolio` : "Here's your business snapshot for today."} +

-
+
-
+ {/* Quick Stats Row */} +
+ {[ + { label: 'Total Projects', value: projectStats.total, icon: Briefcase, color: 'text-blue-500', onClick: () => navigate('/owner/projects') }, + { label: 'Active', value: projectStats.active, icon: TrendingUp, color: 'text-emerald-500', onClick: () => navigate('/owner/projects') }, + { label: 'Avg Health', value: `${projectStats.avgHealth}%`, icon: Shield, color: getHealthColor(projectStats.avgHealth), onClick: () => navigate('/owner/projects') }, + { label: 'Risk Score', value: owner?.riskScore ?? '—', icon: AlertTriangle, color: owner ? getRiskColor(owner.riskScore).split(' ')[0] : 'text-zinc-500', onClick: () => handleActionClick('all') }, + ].map((stat, i) => ( + +
+
+ +
+
+
{stat.value}
+
{stat.label}
+
+
+
+ ))} +
+ {/* Financial KPIs */}

Financial Overview

- +
+ {/* Charts Row */} +
+ {/* Budget vs Actual */} + +

Budget vs Actual Spend

+

Per project (in $k)

+
+ + + + + } /> + + + + +
+
+ + {/* Project Status Distribution */} + +

Project Status

+

Distribution

+
+ + + + {statusChartData.map((entry, i) => ( + + ))} + + + {value}} + /> + + +
+
+
+ + {/* Monthly Spend Trend */} + {monthlySpendData.length > 0 && ( + +

Monthly Spend Trend

+

Last 12 months (in $k)

+
+ + + + + } /> + + + +
+
+ )} + {/* Main Content Grid */}
- {/* Urgent Items Panel (Takes up 2 columns on large screens) */} + {/* Urgent Items Panel */}
- +
- {/* Activity Feed / Notifications Placeholder (Right Column) */} + {/* Activity Feed */}
@@ -89,28 +272,47 @@ const OwnerSnapshot = () => {

Recent Activity

- {[1, 2, 3].map((i) => ( + {activityFeed.length > 0 ? activityFeed.map((item, i) => (
-
+
-

New invoice submitted by Texas Builders LLC

- 2 hours ago +

+ {item.action} + {item.details && — {item.details}} +

+ + {item.date} · {item.projectType} +
- ))} -
-
-
-

Payment received for Project P-2602

- 5 hours ago -
-
+ )) : ( + // Fallback when no activityTimeline data + <> + {[ + { msg: `${projectStats.active} active projects in progress`, time: 'Current', color: 'bg-blue-400' }, + { msg: `${formatCurrency(projectStats.totalSpent)} spent of ${formatCurrency(projectStats.totalBudget)} total budget`, time: 'Overview', color: 'bg-emerald-400' }, + { msg: `${projectStats.completed} projects completed successfully`, time: 'All time', color: 'bg-purple-400' }, + ].map((item, i) => ( +
+
+
+

{item.msg}

+ {item.time} +
+
+ ))} + + )}
@@ -123,15 +325,16 @@ const OwnerSnapshot = () => { isOpen={financialModal.isOpen} onClose={() => setFinancialModal({ ...financialModal, isOpen: false })} type={financialModal.type} + ownerId={user?.id} /> setActionModal({ ...actionModal, isOpen: false })} defaultFilter={actionModal.filter} - // Force re-render on filter change if needed, generally okay as props update key={actionModal.filter} /> -
+ igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew? +
); }; diff --git a/src/pages/owner/PeopleDirectory.jsx b/src/pages/owner/PeopleDirectory.jsx index 8526807..028f4d1 100644 --- a/src/pages/owner/PeopleDirectory.jsx +++ b/src/pages/owner/PeopleDirectory.jsx @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import { useMockStore } from '../../data/mockStore'; -import { Search, Filter, Shield, User, Briefcase, Zap } from 'lucide-react'; +import { Search, Filter, Shield, User, Briefcase, Zap, ArrowLeft } from 'lucide-react'; import MaskedData from '../../components/MaskedData'; import ComplianceChecklist from '../../components/people/ComplianceChecklist'; import { SpotlightCard } from '../../components/SpotlightCard'; @@ -10,6 +10,11 @@ const PeopleDirectory = () => { const [filter, setFilter] = useState('all'); // all, employee, contractor, subcontractor const [search, setSearch] = useState(''); const [selectedPerson, setSelectedPerson] = useState(null); + const detailRef = useRef(null); + + const handleSelectPerson = (person) => { + setSelectedPerson(person); + }; // Combine and normalize data for display const allPeople = [ @@ -30,7 +35,7 @@ const PeopleDirectory = () => { }); return ( -
+
{/* Ambient Background Glows */}
@@ -45,7 +50,7 @@ const PeopleDirectory = () => {
{/* Left Sidebar: List & Search */} - +
@@ -93,7 +98,7 @@ const PeopleDirectory = () => { {filteredPeople.map((person) => (
setSelectedPerson(person)} + onClick={() => handleSelectPerson(person)} className={`p-3 rounded-xl border transition-all cursor-pointer group ${selectedPerson?.id === person.id ? 'bg-blue-50 dark:bg-blue-500/10 border-blue-200 dark:border-blue-500/20 shadow-sm' : 'bg-transparent border-transparent hover:bg-zinc-100 dark:hover:bg-white/5' @@ -119,15 +124,23 @@ const PeopleDirectory = () => { {/* Right Panel: Details */} -
+
{selectedPerson ? ( + {/* Mobile Back Button */} + {/* Header */} -
+
-

{selectedPerson.name}

+

{selectedPerson.name}

{
-
-
+
+
{/* Sensitive Data Section */} -
+

@@ -227,6 +240,7 @@ const PeopleDirectory = () => {

+ igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?
); }; diff --git a/src/pages/owner/VendorDirectory.jsx b/src/pages/owner/VendorDirectory.jsx index 621cdd8..c7470cf 100644 --- a/src/pages/owner/VendorDirectory.jsx +++ b/src/pages/owner/VendorDirectory.jsx @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { useMockStore } from '../../data/mockStore'; -import { Search, Filter, Briefcase, DollarSign, FileText, AlertTriangle, CheckCircle } from 'lucide-react'; +import { Search, Filter, Briefcase, DollarSign, FileText, AlertTriangle, CheckCircle, ArrowLeft } from 'lucide-react'; import MaskedData from '../../components/MaskedData'; import ComplianceChecklist from '../../components/people/ComplianceChecklist'; import { SpotlightCard } from '../../components/SpotlightCard'; @@ -10,6 +10,12 @@ const VendorDirectory = () => { const [filterType, setFilterType] = useState('all'); // all, roofing, electrical, plumbing, hvac const [search, setSearch] = useState(''); const [selectedVendor, setSelectedVendor] = useState(null); + const detailRef = useRef(null); + + // On mobile, scroll to detail when vendor selected + const handleSelectVendor = (vendor) => { + setSelectedVendor(vendor); + }; const filteredVendors = vendors.filter(vendor => { const matchesSearch = vendor.vendorName.toLowerCase().includes(search.toLowerCase()) || @@ -24,7 +30,7 @@ const VendorDirectory = () => { const totalSpend = filteredVendors.reduce((acc, v) => acc + (v.spend?.totalSpend || 0), 0); return ( -
+
{/* Ambient Background Glows */}
@@ -46,7 +52,7 @@ const VendorDirectory = () => {
{/* Left Sidebar: Vendor List */} - +
@@ -79,7 +85,7 @@ const VendorDirectory = () => { {filteredVendors.map((vendor) => (
setSelectedVendor(vendor)} + onClick={() => handleSelectVendor(vendor)} className={`p-3 rounded-xl border transition-all cursor-pointer group ${selectedVendor?.id === vendor.id ? 'bg-blue-50 dark:bg-blue-500/10 border-blue-200 dark:border-blue-500/20 shadow-sm' : 'bg-transparent border-transparent hover:bg-zinc-100 dark:hover:bg-white/5' @@ -109,15 +115,23 @@ const VendorDirectory = () => { {/* Right Panel: Vendor Details */} -
+
{selectedVendor ? ( + {/* Mobile Back Button */} + {/* Header */} -
+
-

{selectedVendor.vendorName}

+

{selectedVendor.vendorName}

{
-
-
+
+
{/* Financials & Compliance */} -
+

@@ -211,6 +225,7 @@ const VendorDirectory = () => {

+ igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?
); }; diff --git a/src/pages/subcontractor/SubContractorDashboard.jsx b/src/pages/subcontractor/SubContractorDashboard.jsx index acddb5f..859847a 100644 --- a/src/pages/subcontractor/SubContractorDashboard.jsx +++ b/src/pages/subcontractor/SubContractorDashboard.jsx @@ -1,47 +1,235 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; import { useAuth } from '../../context/AuthContext'; import { useMockStore } from '../../data/mockStore'; import { StatCard } from '../../components/StatCard'; import { SpotlightCard } from '../../components/SpotlightCard'; -import { CheckSquare, Clock, Wallet, MapPin, Camera } from 'lucide-react'; +import { CheckSquare, Clock, Wallet, MapPin, Camera, X, Calendar, DollarSign, ChevronRight } from 'lucide-react'; import TaskDetailsModal from '../../components/contractor/TaskDetailsModal'; import FinancialSummaryModal from '../../components/contractor/FinancialSummaryModal'; +// --- Reusable list modal for filtered tasks / clock-in / payment history --- +const SubDetailModal = ({ isOpen, onClose, title, type, tasks, invoices, clockIns, onTaskClick }) => { + const [selectedItem, setSelectedItem] = useState(null); + + React.useEffect(() => { + const handleEsc = (e) => { if (e.key === 'Escape') { if (selectedItem) setSelectedItem(null); else onClose(); } }; + if (isOpen) window.addEventListener('keydown', handleEsc); + return () => window.removeEventListener('keydown', handleEsc); + }, [isOpen, onClose, selectedItem]); + + if (!isOpen) return null; + + const formatCurrency = (amt) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amt); + + const getStatusColor = (s) => { + if (s === 'completed' || s === 'paid') return 'text-emerald-600 bg-emerald-100 dark:text-emerald-400 dark:bg-emerald-500/10'; + if (s === 'in_progress') return 'text-blue-600 bg-blue-100 dark:text-blue-400 dark:bg-blue-500/10'; + if (s === 'pending') return 'text-amber-600 bg-amber-100 dark:text-amber-400 dark:bg-amber-500/10'; + return 'text-zinc-500 bg-zinc-100 dark:text-zinc-400 dark:bg-white/5'; + }; + + // If a specific task is selected, show detail view + if (selectedItem && (type === 'tasks_in_progress' || type === 'pending_tasks' || type === 'jobs_completed')) { + const task = selectedItem; + return createPortal( +
+
setSelectedItem(null)} /> +
+
+

Task Detail

+ +
+
+
+

{task.name}

+ {task.status.replace('_', ' ')} +
+
+ {[['Project', task.projectAddress], ['Due Date', task.dueDate], ['Assigned To', task.assignedTo], ['Project Status', task.projectStatus]].map(([l, v], i) => ( +
+
{l}
+
{v}
+
+ ))} +
+
+
+ +
+
+
, + document.body + ); + } + + // If a specific invoice is selected in payment history + if (selectedItem && type === 'payment_history') { + const inv = selectedItem; + return createPortal( +
+
setSelectedItem(null)} /> +
+
+

Payment Detail

+ +
+
+
+
{formatCurrency(inv.amount)}
+ {inv.status} +
+
+ {[['Invoice ID', inv.id], ['Due Date', inv.dueDate], ['Date Paid', inv.datePaid || 'Not yet'], ['Submitted By', inv.submittedBy]].map(([l, v], i) => ( +
+
{l}
+
{v}
+
+ ))} +
+
+
+ +
+
+
, + document.body + ); + } + + // Main list modal + const renderContent = () => { + if (type === 'tasks_in_progress' || type === 'pending_tasks' || type === 'jobs_completed') { + const items = tasks || []; + return items.length > 0 ? ( +
+ {items.map((task, i) => ( +
setSelectedItem(task)} className="flex items-center justify-between p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-200 dark:border-white/5 hover:border-blue-500/30 transition-all cursor-pointer group"> +
+
+
+

{task.name}

+

{task.projectAddress}

+
+
+
+
+

Due: {task.dueDate}

+ {task.status.replace('_', ' ')} +
+ +
+
+ ))} +
+ ) :

No tasks found.

; + } + + if (type === 'clock_in') { + const items = clockIns || []; + return items.length > 0 ? ( +
+ {items.map((entry, i) => ( +
+
+
+
+

{entry.date}

+

{entry.project}

+
+
+
+

{entry.clockIn} — {entry.clockOut || 'Active'}

+

{entry.hours} hrs

+
+
+ ))} +
+ ) :

No clock-in records.

; + } + + if (type === 'payment_history') { + const items = invoices || []; + return items.length > 0 ? ( +
+ {items.map((inv, i) => ( +
setSelectedItem(inv)} className="flex items-center justify-between p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-200 dark:border-white/5 hover:border-emerald-500/30 transition-all cursor-pointer group"> +
+
+
+

Invoice #{inv.id}

+

Due: {inv.dueDate}

+
+
+
+
+

{formatCurrency(inv.amount)}

+ {inv.status} +
+ +
+
+ ))} +
+ ) :

No payment records.

; + } + }; + + return createPortal( +
+
+
+
+

{title}

+ +
+
+ {renderContent()} +
+
+
, + document.body + ); +}; + const SubContractorDashboard = () => { const { user } = useAuth(); const { projects } = useMockStore(); - // 1. Identify current subcontractor (e.g. 'sub_001') const subId = user?.id || 'sub_001'; - // 2. Aggregate Tasks (Milestones assigned to me) + // Aggregate Tasks const myTasks = projects.flatMap(p => p.milestones.filter(m => m.assignedTo === subId) - .map(m => ({ - ...m, - projectAddress: p.address, - projectId: p.id, - projectStatus: p.status - })) + .map(m => ({ ...m, projectAddress: p.address, projectId: p.id, projectStatus: p.status })) ); - const activeTasks = myTasks.filter(t => t.status === 'in_progress').length; - const pendingTasks = myTasks.filter(t => t.status === 'pending').length; - const completedTasks = myTasks.filter(t => t.status === 'completed').length; + const inProgressTasks = myTasks.filter(t => t.status === 'in_progress'); + const pendingTasks = myTasks.filter(t => t.status === 'pending'); + const completedTasks = myTasks.filter(t => t.status === 'completed'); - // 3. Financials (Invoices submitted by me) + // Financials const myInvoices = projects.flatMap(p => - p.invoices.filter(i => i.submittedBy === subId) + (p.invoices || []).filter(i => i.submittedBy === subId) ); const pendingPay = myInvoices.filter(i => i.status === 'pending').reduce((sum, i) => sum + i.amount, 0); const totalEarnings = myInvoices.filter(i => i.status === 'paid').reduce((sum, i) => sum + i.amount, 0); - // Modal state + // Mock clock-in data + const clockInData = useMemo(() => [ + { date: '2026-02-18', project: '2604 Dunwick Dr — Roof Replacement', clockIn: '7:00 AM', clockOut: null, hours: 'Active' }, + { date: '2026-02-17', project: '6613 Phoenix Pl — HVAC Upgrade', clockIn: '7:15 AM', clockOut: '4:30 PM', hours: '9.25' }, + { date: '2026-02-16', project: '2604 Dunwick Dr — Roof Replacement', clockIn: '6:45 AM', clockOut: '3:45 PM', hours: '9.0' }, + { date: '2026-02-15', project: '3913 Arizona Pl — Electrical Panel', clockIn: '7:00 AM', clockOut: '4:00 PM', hours: '9.0' }, + { date: '2026-02-14', project: '2612 Dunwick Dr — Interior Renovation', clockIn: '7:30 AM', clockOut: '5:00 PM', hours: '9.5' }, + ], []); + + // Modal states + const [detailModal, setDetailModal] = useState({ isOpen: false, type: null, title: '' }); const [selectedTask, setSelectedTask] = useState(null); const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); const [isFinancialModalOpen, setIsFinancialModalOpen] = useState(false); - // Financial data for modal const financialData = { total: totalEarnings + pendingPay, paid: totalEarnings, @@ -64,7 +252,10 @@ const SubContractorDashboard = () => { const handleTaskUpdate = (taskId, actionType) => { console.log(`Task ${taskId} updated with action: ${actionType}`); - // In a real app, this would update the backend + }; + + const openDetailModal = (type, title) => { + setDetailModal({ isOpen: true, type, title }); }; return ( @@ -73,17 +264,20 @@ const SubContractorDashboard = () => {

Field Task Center

- You have {activeTasks} active tasks and ${pendingPay.toLocaleString()} in pending payouts. + You have {inProgressTasks.length} active tasks and ${pendingPay.toLocaleString()} in pending payouts.

- {/* Quick Actions (Mobile First) */} + {/* Quick Actions */}
- @@ -99,32 +293,18 @@ const SubContractorDashboard = () => { {/* KPI Cards */}
- - -
setIsFinancialModalOpen(true)} className="cursor-pointer"> - +
openDetailModal('tasks_in_progress', 'Tasks In Progress')} className="cursor-pointer"> + +
+
openDetailModal('pending_tasks', 'Pending Tasks')} className="cursor-pointer"> + +
+
setIsFinancialModalOpen(true)} className="cursor-pointer"> + +
+
openDetailModal('jobs_completed', 'Jobs Completed')} className="cursor-pointer"> +
-
{/* Main Task List */} @@ -134,13 +314,10 @@ const SubContractorDashboard = () => {

My Assignments

-
{myTasks.length > 0 ? myTasks.map((task, idx) => (
handleTaskClick(task)}>
- - {/* Left: Task Info */}
{
- - {/* Right: Status & Action */}

Due: {task.dueDate}

{task.status.replace('_', ' ')}
{task.status !== 'completed' && ( - )} @@ -183,15 +352,13 @@ const SubContractorDashboard = () => {
)) : ( -
-

No tasks assigned.

-
+

No tasks assigned.

)}
- {/* Mobile Earnings Widget */} + {/* Earnings Widget */}

Earnings Tracker

@@ -200,7 +367,6 @@ const SubContractorDashboard = () => {

Total Earned (YTD)

${totalEarnings.toLocaleString()}

-
Paid Invoices @@ -211,8 +377,10 @@ const SubContractorDashboard = () => { {myInvoices.filter(i => i.status === 'pending').length}
- -
@@ -221,18 +389,23 @@ const SubContractorDashboard = () => {
{/* Modals */} - setIsTaskModalOpen(false)} - task={selectedTask} - onUpdate={handleTaskUpdate} - /> - setIsFinancialModalOpen(false)} - role="SUBCONTRACTOR" - data={financialData} + setIsTaskModalOpen(false)} task={selectedTask} onUpdate={handleTaskUpdate} /> + setIsFinancialModalOpen(false)} role="SUBCONTRACTOR" data={financialData} /> + setDetailModal({ isOpen: false, type: null, title: '' })} + title={detailModal.title} + type={detailModal.type} + tasks={ + detailModal.type === 'tasks_in_progress' ? inProgressTasks : + detailModal.type === 'pending_tasks' ? pendingTasks : + detailModal.type === 'jobs_completed' ? completedTasks : [] + } + invoices={myInvoices} + clockIns={clockInData} + onTaskClick={handleTaskClick} /> + igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?
); }; diff --git a/src/pages/vendor/VendorDashboard.jsx b/src/pages/vendor/VendorDashboard.jsx index 11caf4f..9894659 100644 --- a/src/pages/vendor/VendorDashboard.jsx +++ b/src/pages/vendor/VendorDashboard.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; import { useMockStore } from '../../data/mockStore'; @@ -23,40 +23,48 @@ const VendorDashboard = () => { const [isPerformanceModalOpen, setIsPerformanceModalOpen] = useState(false); // 1. Identify current vendor - // For mock purposes, if logged in as 'abc_supply', we map to 'v3' - // In real app, user.vendorId would track this. const vendorId = user.id === 'ven_001' ? 'v3' : 'v1'; const vendorData = vendors.find(v => v.id === vendorId) || vendors[0]; - // 2. Aggregate Data - // Find projects where this vendor is assigned to milestones or has invoices - // A. Milestones + // 2. Aggregate Data from ACTUAL vendor invoices (not vendorData.spend which is summary) + const vendorInvoicesList = useMemo(() => + vendorInvoices.filter(inv => inv.vendorId === vendorId) + , [vendorInvoices, vendorId]); + + const pendingInvoiceAmount = useMemo(() => + vendorInvoicesList.filter(inv => inv.status === 'pending').reduce((sum, inv) => sum + inv.amount, 0) + , [vendorInvoicesList]); + + const pendingInvoiceCount = useMemo(() => + vendorInvoicesList.filter(inv => inv.status === 'pending').length + , [vendorInvoicesList]); + + const paidInvoiceAmount = useMemo(() => + vendorInvoicesList.filter(inv => inv.status === 'paid').reduce((sum, inv) => sum + inv.amount, 0) + , [vendorInvoicesList]); + + const totalEarningsYTD = paidInvoiceAmount; // Only count actually paid invoices + + // Orders + const vendorOrders = useMemo(() => orders.filter(o => o.vendorId === vendorId), [orders, vendorId]); + const activeOrderCount = vendorOrders.filter(o => o.status !== 'delivered').length; + + // Milestones const activeDeliveries = projects.flatMap(p => p.milestones) .filter(m => m.assignedTo === vendorId && m.status === 'pending').length; - const completedDeliveries = projects.flatMap(p => p.milestones) - .filter(m => m.assignedTo === vendorId && m.status === 'completed').length; - - // B. Financials - const pendingInvoices = vendorData.spend?.pendingInvoices || 0; - const totalEarnings = vendorData.spend?.totalSpend || 0; - - // C. Compliance + // Compliance const complianceStatus = vendorData.compliance?.coi?.status === 'compliant' ? 'Verified' : 'Action Required'; - // D. Financial Data for Modal - const vendorOrders = orders.filter(o => o.vendorId === vendorId); - const vendorInvoicesList = vendorInvoices.filter(inv => inv.vendorId === vendorId); - const paidInvoices = vendorInvoicesList.filter(inv => inv.status === 'paid').reduce((sum, inv) => sum + inv.amount, 0); - const pendingPayments = vendorInvoicesList.filter(inv => inv.status === 'pending').reduce((sum, inv) => sum + inv.amount, 0); + // Financial Data for Modal — DISTINCT: Pending vs Paid const financialData = { - totalEarnings: vendorData.spend?.ytdSpend || 0, - paidInvoices, - pendingPayments, + totalEarnings: totalEarningsYTD, + paidInvoices: paidInvoiceAmount, + pendingPayments: pendingInvoiceAmount, invoices: vendorInvoicesList }; - // E. Click Handlers + // Click Handlers const handleOrderClick = (order) => { setSelectedOrder(order); setIsOrderModalOpen(true); @@ -92,23 +100,24 @@ const VendorDashboard = () => {
setIsFinancialModalOpen(true)} className="cursor-pointer"> +
+
navigate('/vendor/orders')} className="cursor-pointer"> +
- navigate('/vendor/orders')} - className="cursor-pointer hover:border-blue-500/50 transition-colors" - />
setIsFinancialModalOpen(true)} className="cursor-pointer"> {
+ {/* Earnings Summary Card */} + +

Earnings Summary

+
+
+
Paid (YTD)
+
${paidInvoiceAmount.toLocaleString()}
+
{vendorInvoicesList.filter(i => i.status === 'paid').length} invoices
+
+
+
Pending
+
${pendingInvoiceAmount.toLocaleString()}
+
{pendingInvoiceCount} invoices
+
+
+
Active Orders
+
{activeOrderCount}
+
{vendorOrders.filter(o => o.status === 'shipped').length} shipped, {vendorOrders.filter(o => o.status === 'pending').length} pending
+
+
+
Total Volume
+
${(paidInvoiceAmount + pendingInvoiceAmount).toLocaleString()}
+
{vendorInvoicesList.length} total invoices
+
+
+
+ {/* Main Content Area */}
- {/* Active Projects / Deliveries */} + {/* Active Orders */}
-

Active Orders & Deliveries

+
+

Active Orders & Deliveries

+ +
{vendorOrders.map(order => (
{ onClick={() => handleOrderClick(order)} className="flex items-center justify-between p-4 rounded-xl bg-zinc-50 dark:bg-white/5 border border-zinc-100 dark:border-white/5 hover:border-blue-500/30 transition-colors cursor-pointer group">
-
+

{order.id}

-

- {order.projectAddress} -

+

{order.projectAddress}

- Due: {order.dueDate} + ${order.total.toLocaleString()}

- {order.status} @@ -176,7 +221,6 @@ const VendorDashboard = () => {

Action Center

- {/* Invoices */}
@@ -185,14 +229,10 @@ const VendorDashboard = () => {

Invoice #INV-2024-001 was flagged for missing PO number.

- +
- - {/* Compliance */}
@@ -201,9 +241,7 @@ const VendorDashboard = () => {

Your General Liability policy expires in 45 days.

- +
@@ -215,7 +253,7 @@ const VendorDashboard = () => {
  • On-Time Delivery Rate - {vendorData.performance?.onTimeRate * 100}% + {(vendorData.performance?.onTimeRate || 0) * 100}%
  • Average Payment Window @@ -223,7 +261,7 @@ const VendorDashboard = () => {
  • Active Contracts - 3 + {vendorOrders.length}
@@ -231,26 +269,11 @@ const VendorDashboard = () => {
{/* Modals */} - setIsOrderModalOpen(false)} - order={selectedOrder} - /> - setIsFinancialModalOpen(false)} - data={financialData} - /> - setIsComplianceModalOpen(false)} - vendorData={vendorData} - /> - setIsPerformanceModalOpen(false)} - vendorData={vendorData} - /> + setIsOrderModalOpen(false)} order={selectedOrder} /> + setIsFinancialModalOpen(false)} data={financialData} /> + setIsComplianceModalOpen(false)} vendorData={vendorData} /> + setIsPerformanceModalOpen(false)} vendorData={vendorData} /> + igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?
); }; diff --git a/src/pages/vendor/VendorOrders.jsx b/src/pages/vendor/VendorOrders.jsx index b87b6f8..b09ad6b 100644 --- a/src/pages/vendor/VendorOrders.jsx +++ b/src/pages/vendor/VendorOrders.jsx @@ -71,7 +71,49 @@ const VendorOrders = () => {
-
+ {/* Mobile Card View */} +
+ {filteredOrders.length > 0 ? filteredOrders.map(order => ( +
handleOrderClick(order)} + className="p-4 active:bg-zinc-50 dark:active:bg-white/5 transition-colors cursor-pointer" + > +
+
+

+ {order.projectAddress || order.project} +

+

{order.id}

+
+ +
+
+ + {order.status === 'delivered' && } + {order.status === 'shipped' && } + {(order.status === 'pending' || order.status === 'confirmed') && } + {order.status} + + + {order.items ? `${order.items.length} items` : order.item} + +
+
+ Due: {order.dueDate || 'N/A'} + ${(order.total || order.amount).toLocaleString()} +
+
+ )) : ( +
+ +

No orders found matching this filter.

+
+ )} +
+ + {/* Desktop Table View */} +
handleSort(col.key)} - className={`px-3 sm:px-6 py-3 sm:py-4 text-[10px] sm:text-xs font-bold uppercase tracking-wider text-zinc-500 cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-300 transition-colors ${col.align === 'right' ? 'text-right' : ''}`} + className={`px-6 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-300 transition-colors ${col.align === 'right' ? 'text-right' : ''}`} > {col.label}
+ {invoice.id} -
{invoice.project}
+
+
{invoice.project}
{invoice.description &&
{invoice.description}
}
+ {invoice.invoiceDate} + {invoice.dueDate} - + { {invoice.status} + {formatCurrency(invoice.amount)}
@@ -128,7 +170,7 @@ const VendorOrders = () => {
{filteredOrders.length === 0 && ( -
+

No orders found matching this filter.

@@ -141,6 +183,7 @@ const VendorOrders = () => { onClose={() => setIsOrderModalOpen(false)} order={selectedOrder} /> + igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?
); };