feat(subtasks): owner table category/assigned-date columns, owner modal stage timeline + review gate, subcontractor picker stages

This commit is contained in:
Satyam Rastogi
2026-05-29 19:44:33 +05:30
parent 1e8b72f18d
commit 9625aafd73
3 changed files with 385 additions and 30 deletions
+26 -2
View File
@@ -332,11 +332,13 @@ const SubcontractorTasksPage = () => {
<thead className="bg-zinc-50 dark:bg-[#18181b] border-b border-zinc-200 dark:border-white/10">
<tr>
<Th>Task</Th>
<Th>Task Category</Th>
<Th>Subcontractor</Th>
<Th>Location</Th>
<Th>Due</Th>
<Th>Priority</Th>
<Th>Status</Th>
<Th>Assigned Date</Th>
<Th>Created</Th>
<Th align="right">Actions</Th>
</tr>
@@ -344,7 +346,7 @@ const SubcontractorTasksPage = () => {
<tbody className="divide-y divide-zinc-100 dark:divide-white/5">
{paginated.length === 0 ? (
<tr>
<td colSpan={8}>
<td colSpan={10}>
<EmptyState />
</td>
</tr>
@@ -482,6 +484,15 @@ const TaskRow = ({ task, onView, onEdit, onCancel, onReassign, openMenu, setOpen
<div className="text-[11px] text-zinc-500 font-mono mt-0.5">{task.id}</div>
</button>
</td>
<td className="px-5 py-4 whitespace-nowrap">
{task.category ? (
<span className="px-2 py-0.5 rounded-full text-xs font-semibold bg-violet-100 text-violet-700 dark:bg-violet-500/20 dark:text-violet-300">
{task.category}
</span>
) : (
<span className="text-zinc-400 text-xs"></span>
)}
</td>
<td className="px-5 py-4">
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-full bg-blue-500/10 text-blue-500 flex items-center justify-center text-xs font-bold shrink-0">
@@ -508,6 +519,7 @@ const TaskRow = ({ task, onView, onEdit, onCancel, onReassign, openMenu, setOpen
</td>
<td className="px-5 py-4"><PriorityBadge priority={task.priority} /></td>
<td className="px-5 py-4"><StatusBadge status={task.status} /></td>
<td className="px-5 py-4 whitespace-nowrap text-sm text-zinc-500">{formatDate(task.assignedDate)}</td>
<td className="px-5 py-4 whitespace-nowrap text-sm text-zinc-500">{formatDate(task.createdAt)}</td>
<td className="px-5 py-4 text-right relative">
<div className="inline-flex items-center gap-1">
@@ -584,12 +596,24 @@ const TaskCard = ({ task, onView, onEdit, onCancel, onReassign, canAssign }) =>
<span className="flex items-center gap-1"><Users size={11} />{task.subcontractorName}</span>
<span className="flex items-center gap-1"><MapPin size={11} /><span className="truncate max-w-[160px]">{task.location}</span></span>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex flex-wrap items-center gap-2 mb-2">
<PriorityBadge priority={task.priority} />
{task.category && (
<span className="px-2 py-0.5 rounded-full text-xs font-semibold bg-violet-100 text-violet-700 dark:bg-violet-500/20 dark:text-violet-300">
{task.category}
</span>
)}
<span className={`flex items-center gap-1 text-[11px] font-mono ${overdue ? 'text-red-500 font-bold' : 'text-zinc-500'}`}>
<Calendar size={11} /> Due {formatDate(task.dueDate)}{overdue ? ' · OVERDUE' : ''}
</span>
</div>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-zinc-500">
{task.assignedDate && (
<span className="flex items-center gap-1">
<Clock size={11} /> Assigned {formatDate(task.assignedDate)}
</span>
)}
</div>
</button>
<div className="mt-3 flex flex-wrap gap-2 pt-2 border-t border-zinc-100 dark:border-white/5">
<CardActionButton onClick={onView} icon={Eye}>View</CardActionButton>
@@ -21,11 +21,14 @@ const PRIORITY_STYLES = {
};
const STATUS_CONFIG = {
Assigned: { cls: 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400', icon: Clock, dot: 'bg-amber-500' },
'In Progress': { cls: 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400', icon: PauseCircle, dot: 'bg-blue-500' },
'On Hold': { cls: 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-400', icon: PauseCircle, dot: 'bg-orange-500' },
Completed: { cls: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400', icon: CheckCircle, dot: 'bg-emerald-500' },
Cancelled: { cls: 'bg-zinc-200 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300', icon: X, dot: 'bg-zinc-500' },
Assigned: { cls: 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400', icon: Clock, dot: 'bg-amber-500' },
'Pre-Work Inspection': { cls: 'bg-violet-100 text-violet-700 dark:bg-violet-500/20 dark:text-violet-400', icon: AlertCircle, dot: 'bg-violet-500' },
'In Progress': { cls: 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400', icon: PauseCircle, dot: 'bg-blue-500' },
'On Hold': { cls: 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-400', icon: PauseCircle, dot: 'bg-orange-500' },
'Post-Work Review': { cls: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-400', icon: Send, dot: 'bg-indigo-500' },
'Rework Needed': { cls: 'bg-amber-100 text-amber-800 dark:bg-amber-500/20 dark:text-amber-300', icon: AlertTriangle, dot: 'bg-amber-600' },
Completed: { cls: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400', icon: CheckCircle, dot: 'bg-emerald-500' },
Cancelled: { cls: 'bg-zinc-200 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300', icon: X, dot: 'bg-zinc-500' },
};
const formatDate = (iso) => {
@@ -353,6 +356,13 @@ const STATUS_COPY = {
photoHint: 'Site or arrival photo',
accent: 'amber',
},
'Pre-Work Inspection': {
title: 'Log pre-work site inspection',
hint: 'Record your site-walk before starting. Note existing conditions, scope clarifications, and attach inspection photos.',
notePlaceholder: 'e.g. Site walk done. Existing shingles in poor shape on north side — photographed. Ready to start.',
photoHint: 'Inspection / site-condition photos',
accent: 'violet',
},
'In Progress': {
title: 'Start work',
hint: 'Add a brief note about what you are starting and attach any "before" photos if you have them.',
@@ -367,6 +377,13 @@ const STATUS_COPY = {
photoHint: 'Supporting photo (e.g. rain, blocked access)',
accent: 'orange',
},
'Post-Work Review': {
title: 'Submit for owner review',
hint: 'Work is complete on your end — submit it to the owner for final review and sign-off. Attach "after" photos and a completion note.',
notePlaceholder: 'e.g. All work completed. Cleaned up site. Final photos attached — ready for owner inspection.',
photoHint: 'Completion / after photos (required for review)',
accent: 'indigo',
},
Completed: {
title: 'Mark as completed',
hint: 'Add a final completion note and attach any photos of the finished work.',
@@ -378,8 +395,10 @@ const STATUS_COPY = {
const ACCENT_BTN = {
amber: 'bg-amber-600 hover:bg-amber-500 shadow-amber-500/20',
violet: 'bg-violet-600 hover:bg-violet-500 shadow-violet-500/20',
blue: 'bg-blue-600 hover:bg-blue-500 shadow-blue-500/20',
orange: 'bg-orange-600 hover:bg-orange-500 shadow-orange-500/20',
indigo: 'bg-indigo-600 hover:bg-indigo-500 shadow-indigo-500/20',
emerald: 'bg-emerald-600 hover:bg-emerald-500 shadow-emerald-500/20',
};
@@ -441,32 +460,41 @@ const StatusManagementCard = ({ task, statuses, disabled, onChange, onPostProgre
}, 350);
};
const canPostUpdates = ['Assigned', 'In Progress', 'On Hold', 'Completed'].includes(task.status);
const canPostUpdates = ['Assigned', 'Pre-Work Inspection', 'In Progress', 'On Hold', 'Post-Work Review', 'Rework Needed'].includes(task.status);
const updateStatusCfg = STATUS_CONFIG[task.status] || STATUS_CONFIG.Assigned;
const updateBadgeCls = updateStatusCfg.cls;
const UpdateIcon = updateStatusCfg.icon || Play;
const postBtnCls = (() => {
switch (task.status) {
case 'Assigned': return 'bg-amber-600 hover:bg-amber-500 shadow-amber-500/20';
case 'On Hold': return 'bg-orange-600 hover:bg-orange-500 shadow-orange-500/20';
case 'Completed': return 'bg-emerald-600 hover:bg-emerald-500 shadow-emerald-500/20';
default: return 'bg-blue-600 hover:bg-blue-500 shadow-blue-500/20';
case 'Assigned': return 'bg-amber-600 hover:bg-amber-500 shadow-amber-500/20';
case 'Pre-Work Inspection': return 'bg-violet-600 hover:bg-violet-500 shadow-violet-500/20';
case 'On Hold': return 'bg-orange-600 hover:bg-orange-500 shadow-orange-500/20';
case 'Post-Work Review': return 'bg-indigo-600 hover:bg-indigo-500 shadow-indigo-500/20';
case 'Rework Needed': return 'bg-amber-700 hover:bg-amber-600 shadow-amber-500/20';
case 'Completed': return 'bg-emerald-600 hover:bg-emerald-500 shadow-emerald-500/20';
default: return 'bg-blue-600 hover:bg-blue-500 shadow-blue-500/20';
}
})();
const progressPlaceholder = (() => {
switch (task.status) {
case 'Assigned': return 'e.g. "Scheduled for Friday, reviewed site plan."';
case 'On Hold': return 'e.g. "Still waiting on materialssupplier ETA Friday."';
case 'Completed': return 'e.g. "Follow-up walkthrough done, no issues noted."';
default: return 'e.g. "First wall painting completed."';
case 'Assigned': return 'e.g. "Scheduled for Friday, reviewed site plan."';
case 'Pre-Work Inspection': return 'e.g. "Noted existing damage on east wallphotographed. Materials ordered."';
case 'On Hold': return 'e.g. "Still waiting on materials — supplier ETA Friday."';
case 'Post-Work Review': return 'e.g. "All punch items cleared. Awaiting owner walk-through confirmation."';
case 'Rework Needed': return 'e.g. "Addressing the rework items — additional materials sourced."';
case 'Completed': return 'e.g. "Follow-up walkthrough done, no issues noted."';
default: return 'e.g. "First wall painting completed."';
}
})();
const progressCopy = (() => {
switch (task.status) {
case 'Assigned': return 'Share early notes about the upcoming work — site prep, schedule changes, anything the admin should know before you start.';
case 'On Hold': return 'Keep the admin posted while this task is paused — share why it is still on hold or what changed.';
case 'Completed': return 'Add a follow-up note after completion — punch-list items, homeowner feedback, or anything that came up after sign-off.';
default: return 'Keep the admin in the loop with ongoing updates while you work.';
case 'Assigned': return 'Share early notes about the upcoming work — site prep, schedule changes, anything the admin should know before you start.';
case 'Pre-Work Inspection': return 'Log any additional observations from your site inspection — existing conditions, scope questions, or equipment notes.';
case 'On Hold': return 'Keep the admin posted while this task is paused — share why it is still on hold or what changed.';
case 'Post-Work Review': return 'Post any follow-up notes while awaiting owner review — clarifications, additional photos, or punch-list responses.';
case 'Rework Needed': return 'Share your progress addressing the rework — the owner can track your updates while you make corrections.';
case 'Completed': return 'Add a follow-up note after completion — punch-list items, homeowner feedback, or anything that came up after sign-off.';
default: return 'Keep the admin in the loop with ongoing updates while you work.';
}
})();
@@ -474,6 +502,14 @@ const StatusManagementCard = ({ task, statuses, disabled, onChange, onPostProgre
return [...(task.activities || [])].sort((a, b) => new Date(b.at) - new Date(a.at));
}, [task.activities]);
// Latest rework feedback from owner — shown when status is 'Rework Needed'
const reworkFeedback = useMemo(() => {
if (task.status !== 'Rework Needed') return null;
const history = task.statusHistory || [];
// Find the most recent 'Rework Needed' entry (reverse iterate for latest-first)
return [...history].reverse().find(h => h.status === 'Rework Needed') || null;
}, [task.status, task.statusHistory]);
return (
<SpotlightCard className="p-5 sm:p-6">
<SectionHeading
@@ -482,7 +518,45 @@ const StatusManagementCard = ({ task, statuses, disabled, onChange, onPostProgre
hint="Move this task through the workflow. Add a note describing the change, and attach photos if you have them."
/>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 sm:gap-3 mt-4">
{/* Rework callout — only shown when status is 'Rework Needed' */}
{reworkFeedback && (
<div className="mt-4 p-4 rounded-xl bg-amber-50 dark:bg-amber-500/10 border border-amber-300 dark:border-amber-500/40 space-y-2">
<div className="flex items-center gap-2">
<AlertTriangle size={14} className="text-amber-600 dark:text-amber-400 shrink-0" />
<span className="text-xs font-bold uppercase tracking-wider text-amber-700 dark:text-amber-400">
Rework Requested
</span>
{reworkFeedback.actorName && (
<span className="text-[11px] text-amber-700/80 dark:text-amber-400/80 ml-auto font-semibold">
by {reworkFeedback.actorName}
</span>
)}
</div>
{reworkFeedback.comment ? (
<p className="text-sm text-amber-900 dark:text-amber-200 whitespace-pre-line">
{reworkFeedback.comment}
</p>
) : (
<p className="text-sm italic text-amber-700/70 dark:text-amber-400/70">
No specific feedback provided contact the owner for details.
</p>
)}
{reworkFeedback.photos && reworkFeedback.photos.length > 0 && (
<div className="grid grid-cols-3 sm:grid-cols-4 gap-2 mt-2">
{reworkFeedback.photos.map((p) => (
<div key={p.id} className="aspect-square rounded-lg overflow-hidden border border-amber-200 dark:border-amber-500/30 bg-amber-100 dark:bg-amber-500/10">
<img src={p.url} alt={p.name || 'Rework photo'} className="w-full h-full object-cover" />
</div>
))}
</div>
)}
<p className="text-[11px] text-amber-700/70 dark:text-amber-400/60 pt-1">
Address the feedback above, then set status to <strong>In Progress</strong> and later <strong>Post-Work Review</strong> to resubmit.
</p>
</div>
)}
<div className="flex flex-wrap gap-2 sm:gap-3 mt-4">
{statuses.map(s => {
const cfg = STATUS_CONFIG[s] || STATUS_CONFIG.Assigned;
const Icon = cfg.icon;
@@ -494,7 +568,7 @@ const StatusManagementCard = ({ task, statuses, disabled, onChange, onPostProgre
type="button"
onClick={() => handlePick(s)}
disabled={disabled || isSubmittingStatus}
className={`group relative flex items-center justify-center gap-2 px-3 py-3 rounded-xl text-sm font-bold border transition-all
className={`group relative flex items-center justify-center gap-2 px-3 py-3 rounded-xl text-sm font-bold border transition-all flex-1 min-w-[calc(50%-0.375rem)] sm:min-w-[calc(33%-0.375rem)] md:min-w-0
${isCurrent
? 'border-blue-500 bg-blue-500/10 text-blue-600 dark:text-blue-400 shadow-lg shadow-blue-500/10'
: isPending
@@ -504,7 +578,14 @@ const StatusManagementCard = ({ task, statuses, disabled, onChange, onPostProgre
disabled:opacity-40 disabled:cursor-not-allowed`}
>
<Icon size={14} />
<span>{s}</span>
<span className="text-center leading-tight">
{s}
{s === 'Post-Work Review' && (
<span className="block text-[10px] font-normal normal-case opacity-70 leading-tight mt-0.5">
Submit for owner review
</span>
)}
</span>
{isCurrent && <span className="absolute top-1 right-1 w-2 h-2 rounded-full bg-blue-500 animate-pulse" />}
</button>
);