+
{/* 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 */}
+ {/* Charts Row */}
+
+ {/* Budget vs Actual */}
+
+ Budget vs Actual Spend
+ Per project (in $k)
+
+
+
+
+
+ } />
+
+
+
+
+
+
+
+ {/* Project Status Distribution */}
+
+ Project Status
+ Distribution
+
+
+
+
+ {statusChartData.map((entry, i) => (
+ |
+ ))}
+
+
+
+
+
+
+
+
+ {/* 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}
+
+
+ ))}
+ >
+ )}
navigate('/owner/vendors')}
- className="w-full mt-6 py-2 text-xs font-bold uppercase tracking-wider text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white border border-zinc-200 dark:border-white/10 hover:bg-zinc-100 dark:hover:bg-white/5 rounded-xl transition-all"
+ onClick={() => navigate('/owner/projects')}
+ className="w-full mt-6 py-2 text-xs font-bold uppercase tracking-wider text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white border border-zinc-200 dark:border-white/10 hover:bg-zinc-100 dark:hover:bg-white/5 rounded-xl transition-all flex items-center justify-center gap-2"
>
- View All Activity
+ View All Projects
@@ -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 */}
+ setSelectedPerson(null)}
+ className="lg:hidden flex items-center gap-2 px-4 py-3 text-sm font-bold text-blue-600 dark:text-blue-400 border-b border-zinc-200 dark:border-white/5 bg-zinc-50/50 dark:bg-white/5"
+ >
+
+ Back to People
+
{/* 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 */}
+ setSelectedVendor(null)}
+ className="lg:hidden flex items-center gap-2 px-4 py-3 text-sm font-bold text-blue-600 dark:text-blue-400 border-b border-zinc-200 dark:border-white/5 bg-zinc-50/50 dark:bg-white/5"
+ >
+
+ Back to Vendors
+
{/* 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
+ setSelectedItem(null)} className="p-2 rounded-lg bg-zinc-100 dark:bg-white/10 text-zinc-500 hover:text-red-500 transition-colors">
+
+
+
+
{task.name}
+ {task.status.replace('_', ' ')}
+
+
+ {[['Project', task.projectAddress], ['Due Date', task.dueDate], ['Assigned To', task.assignedTo], ['Project Status', task.projectStatus]].map(([l, v], i) => (
+
+ ))}
+
+
+
+ setSelectedItem(null)} className="px-4 py-2 text-sm font-bold rounded-xl border border-zinc-200 dark:border-white/10 hover:bg-zinc-100 dark:hover:bg-white/5 transition-colors">Close
+
+
+
,
+ 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
+ setSelectedItem(null)} className="p-2 rounded-lg bg-zinc-100 dark:bg-white/10 text-zinc-500 hover:text-red-500 transition-colors">
+
+
+
+
{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) => (
+
+ ))}
+
+
+
+ setSelectedItem(null)} className="px-4 py-2 text-sm font-bold rounded-xl border border-zinc-200 dark:border-white/10 hover:bg-zinc-100 dark:hover:bg-white/5 transition-colors">Close
+
+
+
,
+ 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 */}
Upload Photo
-
+ openDetailModal('clock_in', 'Clock-In History')}
+ className="flex flex-col items-center justify-center p-4 rounded-xl bg-zinc-100 dark:bg-white/10 text-zinc-900 dark:text-white hover:bg-zinc-200 dark:hover:bg-white/20 transition-all active:scale-95"
+ >
Clock In
@@ -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' && (
-
{
- e.stopPropagation();
- handleTaskClick(task);
- }}
- >
+ { e.stopPropagation(); handleTaskClick(task); }}>
Update
)}
@@ -183,15 +352,13 @@ const SubContractorDashboard = () => {
)) : (
-
+
)}
- {/* 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}
-
-
+ openDetailModal('payment_history', 'Payment History')}
+ className="w-full mt-8 py-3 rounded-xl bg-zinc-100 dark:bg-white/10 hover:bg-zinc-200 dark:hover:bg-white/20 text-zinc-900 dark:text-white font-bold text-sm transition-colors"
+ >
View Payment History
@@ -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
+ navigate('/vendor/orders')} className="text-sm text-blue-500 hover:text-blue-400 font-medium">View All
+
{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.
-
- View Details
-
+
View Details
-
- {/* Compliance */}
@@ -201,9 +241,7 @@ const VendorDashboard = () => {
Your General Liability policy expires in 45 days.
-
- Upload Renewal
-
+
Upload Renewal
@@ -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 */}
+
@@ -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?
);
};