From 9625aafd73f5093f2f50deda29b2eeca5ec6a80b Mon Sep 17 00:00:00 2001 From: Satyam Rastogi Date: Fri, 29 May 2026 19:44:33 +0530 Subject: [PATCH] feat(subtasks): owner table category/assigned-date columns, owner modal stage timeline + review gate, subcontractor picker stages --- .../subcontractor/TaskViewModal.jsx | 264 +++++++++++++++++- src/pages/owner/SubcontractorTasksPage.jsx | 28 +- .../SubcontractorTaskDetailPage.jsx | 123 ++++++-- 3 files changed, 385 insertions(+), 30 deletions(-) diff --git a/src/components/subcontractor/TaskViewModal.jsx b/src/components/subcontractor/TaskViewModal.jsx index 5b0b368..9af952d 100644 --- a/src/components/subcontractor/TaskViewModal.jsx +++ b/src/components/subcontractor/TaskViewModal.jsx @@ -1,6 +1,8 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; -import { X, ClipboardList, MapPin, Calendar, User, Building2, Camera } from 'lucide-react'; +import { X, ClipboardList, MapPin, Calendar, User, Building2, Camera, Clock, CheckCircle, AlertTriangle } from 'lucide-react'; +import { useMockStore } from '../../data/mockStore'; +import { useAuth } from '../../context/AuthContext'; const PRIORITY_STYLES = { low: { label: 'Low', cls: 'bg-zinc-200 text-zinc-700 dark:bg-zinc-700 dark:text-zinc-300' }, @@ -9,11 +11,25 @@ const PRIORITY_STYLES = { }; const STATUS_STYLES = { - Assigned: 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400', - 'In Progress': 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400', - 'On Hold': 'bg-slate-100 text-slate-600 dark:bg-slate-500/20 dark:text-slate-400', - Completed: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400', - Cancelled: 'bg-zinc-200 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300', + Assigned: 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400', + 'Pre-Work Inspection': 'bg-purple-100 text-purple-700 dark:bg-purple-500/20 dark:text-purple-400', + 'In Progress': 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400', + 'On Hold': 'bg-slate-100 text-slate-600 dark:bg-slate-500/20 dark:text-slate-400', + 'Post-Work Review': 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-400', + Completed: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400', + 'Rework Needed': 'bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-400', + Cancelled: 'bg-zinc-200 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300', +}; + +const STATUS_DOT = { + Assigned: 'bg-amber-500', + 'Pre-Work Inspection': 'bg-purple-500', + 'In Progress': 'bg-blue-500', + 'On Hold': 'bg-slate-500', + 'Post-Work Review': 'bg-orange-500', + Completed: 'bg-emerald-500', + 'Rework Needed': 'bg-red-500', + Cancelled: 'bg-zinc-500', }; const formatDate = (iso) => { @@ -22,21 +38,85 @@ const formatDate = (iso) => { catch { return iso; } }; +const formatDateTime = (iso) => { + if (!iso) return '—'; + try { + const d = new Date(iso); + const dateStr = d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); + const timeStr = d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); + return `${dateStr} · ${timeStr}`; + } catch { return iso; } +}; + const TaskViewModal = ({ isOpen, onClose, task }) => { + const { user } = useAuth(); + const { setSubcontractorTaskStatus } = useMockStore(); + + const [reviewNote, setReviewNote] = useState(''); + const [reworkNote, setReworkNote] = useState(''); + const [showReworkInput, setShowReworkInput] = useState(false); + const [submitting, setSubmitting] = useState(false); + useEffect(() => { const handler = (e) => { if (e.key === 'Escape') onClose(); }; if (isOpen) window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [isOpen, onClose]); + // Reset review state when task changes + useEffect(() => { + setReviewNote(''); + setReworkNote(''); + setShowReworkInput(false); + setSubmitting(false); + }, [task?.id]); + if (!isOpen || !task) return null; const priority = PRIORITY_STYLES[task.priority] || PRIORITY_STYLES.medium; + const isOwnerOrAdmin = ['OWNER', 'ADMIN'].includes(user?.role); + const isPostWorkReview = task.status === 'Post-Work Review'; + + // Find the latest "Rework Needed" history entry for the callout + const statusHistory = task.statusHistory || []; + const reworkEntry = [...statusHistory] + .reverse() + .find(h => h.status === 'Rework Needed'); + const showReworkCallout = task.status === 'Rework Needed' || !!reworkEntry; + + const handleApprove = () => { + if (submitting) return; + setSubmitting(true); + const note = reviewNote.trim() || 'Approved — passed review'; + setSubcontractorTaskStatus( + task.id, + 'Completed', + note, + { id: user.id, name: user.name, role: user.role }, + ); + setSubmitting(false); + setReviewNote(''); + }; + + const handleRework = () => { + if (!reworkNote.trim() || submitting) return; + setSubmitting(true); + setSubcontractorTaskStatus( + task.id, + 'Rework Needed', + reworkNote.trim(), + { id: user.id, name: user.name, role: user.role }, + ); + setSubmitting(false); + setReworkNote(''); + setShowReworkInput(false); + }; return createPortal(
+ {/* Header */}
@@ -84,6 +164,176 @@ const TaskViewModal = ({ isOpen, onClose, task }) => {
)} + {/* Rework Needed callout — prominent amber banner */} + {showReworkCallout && reworkEntry && ( +
+
+ + + Rework Requested + + {reworkEntry.actorName && ( + + by {reworkEntry.actorName} + + )} +
+ {reworkEntry.comment && ( +

+ {reworkEntry.comment} +

+ )} + {reworkEntry.photos && reworkEntry.photos.length > 0 && ( +
+ {reworkEntry.photos.map((p) => ( +
+ {p.name +
+ ))} +
+ )} +
{formatDateTime(reworkEntry.at)}
+
+ )} + + {/* Owner review gate — shown only when status is Post-Work Review AND user is OWNER/ADMIN */} + {isPostWorkReview && isOwnerOrAdmin && ( +
+
+ +

+ Owner Review Required +

+
+

+ This task is awaiting your review. Approve it to mark as Completed, or send it back for rework with a note explaining what needs to be fixed. +

+ + {/* Optional approval note */} +
+ +