From 35ff7a2e059fcf3cd1a6289947a2e2566d61707c Mon Sep 17 00:00:00 2001 From: Satyam Rastogi Date: Fri, 29 May 2026 17:45:35 +0530 Subject: [PATCH] feat(projects): Inspections tab (damage + final, issues, rework loop, photo upload) --- src/pages/owner/OwnerProjectDetail.jsx | 253 ++++++++++++++++++++++++- 1 file changed, 252 insertions(+), 1 deletion(-) diff --git a/src/pages/owner/OwnerProjectDetail.jsx b/src/pages/owner/OwnerProjectDetail.jsx index e50a2e5..1c20dcf 100644 --- a/src/pages/owner/OwnerProjectDetail.jsx +++ b/src/pages/owner/OwnerProjectDetail.jsx @@ -13,7 +13,7 @@ import { ArrowLeft, CheckCircle, Clock, AlertTriangle, PauseCircle, ShieldAlert, FileText, DollarSign, GitPullRequest, AlertCircle, Activity, Milestone, Shield, ChevronRight, Phone, MessageSquare, Mail, AlertOctagon, TrendingUp, Key, Zap, Camera, Plus, Map, User, Crosshair, Image as ImageIcon, MapPin, - Upload, Trash2, Eye, Pencil, X as XIcon, FolderOpen, Download, File, StickyNote, Banknote, ChevronLeft, ChevronDown as ChevronDownIcon, Info, Settings, Building2, User as UserIcon, Briefcase, Users, HardHat, Wrench, UserCheck, Wallet + Upload, Trash2, Eye, Pencil, X as XIcon, FolderOpen, Download, File, StickyNote, Banknote, ChevronLeft, ChevronDown as ChevronDownIcon, Info, Settings, Building2, User as UserIcon, Briefcase, Users, HardHat, Wrench, UserCheck, Wallet, ShieldCheck } from 'lucide-react'; import ChangeOrderDrawer from '../../components/owner/ChangeOrderDrawer'; import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal'; @@ -74,6 +74,7 @@ const tabs = [ { id: 'milestones', label: 'Milestones', icon: Milestone }, { id: 'invoices', label: 'Invoices', icon: DollarSign }, { id: 'risks', label: 'Risk & Issues', icon: AlertCircle }, + { id: 'inspections', label: 'Inspections', icon: ShieldCheck }, { id: 'activity', label: 'Activity', icon: Clock }, { id: 'docs', label: 'Docs', icon: FolderOpen }, ]; @@ -192,6 +193,7 @@ const OwnerProjectDetail = () => { removeUserCommissionOverride, updateProject, addJobTeamMember, removeJobTeamMember, updateJobTeamMember, advanceLifecycleStage, + addInspection, addInspectionIssue, resolveInspectionIssue, } = useMockStore(); const navigate = useNavigate(); const { can } = usePermissions(); @@ -215,6 +217,11 @@ const OwnerProjectDetail = () => { const [commissionConfig, setCommissionConfig] = useState(null); const [commissionSettingsOpen, setCommissionSettingsOpen] = useState(false); + // ── Inspections state ───────────────────────────────────────────────────── + // issuePhotoMap: { [issueId]: [{id, url, caption}] } — session-only local photos + const [issuePhotoMap, setIssuePhotoMap] = useState({}); + const photoInputRefs = useRef({}); + // ── Docs state ──────────────────────────────────────────────────────────── const [docs, setDocs] = useState(() => seedDocsMock(projectId)); const [viewDoc, setViewDoc] = useState(null); // doc object being viewed @@ -1792,6 +1799,250 @@ const OwnerProjectDetail = () => { )} + {/* INSPECTIONS TAB */} + {activeTab === 'inspections' && (() => { + const inspections = project.inspections || []; + const isOwnerAdmin = ['OWNER', 'ADMIN'].includes(user?.role); + + // Find latest final inspection for re-inspection scheduling + const finalInspections = inspections.filter(ins => ins.kind === 'final'); + const latestFinal = finalInspections.length > 0 + ? finalInspections.reduce((latest, ins) => (ins.round || 1) > (latest.round || 1) ? ins : latest, finalInspections[0]) + : null; + const showScheduleReinspection = isOwnerAdmin && latestFinal && latestFinal.result === 'fail'; + + const handlePhotoAttach = (issueId, file) => { + if (!file) return; + const url = URL.createObjectURL(file); + const photo = { id: Date.now(), url, caption: file.name }; + setIssuePhotoMap(prev => ({ + ...prev, + [issueId]: [...(prev[issueId] || []), photo], + })); + }; + + return ( +
+ {/* Header + Schedule Re-inspection */} +
+
+ +

Inspections

+ + {inspections.length} + +
+ {showScheduleReinspection && ( + + )} +
+ + {inspections.length === 0 ? ( + +
+ +

No inspections recorded yet.

+

Damage verifications and final inspections will appear here.

+
+
+ ) : ( + inspections.map((inspection) => { + const isDamage = inspection.kind === 'damage'; + const kindLabel = isDamage ? 'Damage Verification' : 'Final Inspection'; + const resultBadge = inspection.result === 'pass' + ? 'text-emerald-600 dark:text-[#39ff14] bg-emerald-50 dark:bg-[#39ff14]/10 border border-emerald-200 dark:border-[#39ff14]/20' + : inspection.result === 'fail' + ? 'text-red-600 dark:text-[#ff003c] bg-red-50 dark:bg-[#ff003c]/10 border border-red-200 dark:border-[#ff003c]/20' + : 'text-zinc-500 bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10'; + const resultLabel = inspection.result === 'pass' ? 'Pass' : inspection.result === 'fail' ? 'Fail' : 'Pending'; + const spotlightColor = isDamage ? 'rgba(253, 169, 19, 0.08)' : 'rgba(0, 240, 255, 0.08)'; + const headerColor = isDamage ? NEON_GOLD : NEON_BLUE; + const issues = inspection.issues || []; + + return ( + + {/* Inspection header */} +
+
+ +
+
+ {kindLabel} + {(inspection.round || 1) > 1 && ( + + Round {inspection.round} + + )} + + {resultLabel} + +
+
+ {inspection.inspectorName && ( +

+ Inspector:{' '} + {inspection.inspectorName} + {isDamage && inspection.inspectorRole && ` — ${inspection.inspectorRole}`} +

+ )} + {!isDamage && inspection.inspectorContact && ( +

+ {inspection.inspectorContact.phone && ( + {inspection.inspectorContact.phone} + )} + {inspection.inspectorContact.email && ( + {inspection.inspectorContact.email} + )} +

+ )} + {inspection.inspectionDate && ( +

Date:{' '}{inspection.inspectionDate}

+ )} +
+ {inspection.feedbackNote && ( +

+ {inspection.feedbackNote} +

+ )} +
+
+ {/* Add Issue button — OWNER/ADMIN only */} + {isOwnerAdmin && ( + + )} +
+ + {/* Issues list */} + {issues.length > 0 ? ( +
+

+ Issues ({issues.length}) +

+ {issues.map((issue) => { + const localPhotos = issuePhotoMap[issue.id] || []; + const allPhotos = [...(issue.photos || []), ...localPhotos]; + const isOpen = issue.status === 'open'; + return ( +
+
+
+
+ {issue.description} + + {isOpen ? 'Open' : 'Fixed'} + +
+ {issue.note && ( +

{issue.note}

+ )} +
+ {/* Actions: Mark Fixed + Attach Photo — OWNER/ADMIN only */} + {isOwnerAdmin && ( +
+ {isOpen && ( + + )} + {/* Hidden file input for photo attach */} + + { photoInputRefs.current[issue.id] = el; }} + onChange={(e) => { + handlePhotoAttach(issue.id, e.target.files?.[0]); + e.target.value = ''; + }} + /> +
+ )} +
+ {/* Photo thumbnails */} + {allPhotos.length > 0 && ( +
+ {allPhotos.map((photo) => ( +
+ {photo.caption + {photo.caption && ( +
+ {photo.caption} +
+ )} +
+ ))} +
+ )} +
+ ); + })} +
+ ) : ( +

No issues recorded for this inspection.

+ )} +
+ ); + }) + )} +
+ ); + })()} + {/* ACTIVITY TAB */} {/* ── DOCS TAB ── */} {activeTab === 'docs' && (