subcontrcator: post a status update section- removed photo required option

This commit is contained in:
Satyam Rastogi
2026-05-21 20:49:10 +05:30
parent e648547f1b
commit 490d16b80a
2 changed files with 62 additions and 40 deletions
+14 -4
View File
@@ -6280,9 +6280,9 @@ export const MockStoreProvider = ({ children }) => {
const addSubcontractorTaskActivity = (taskId, { note, photos = [] }, actor) => { const addSubcontractorTaskActivity = (taskId, { note, photos = [] }, actor) => {
const trimmed = note?.trim() || ''; const trimmed = note?.trim() || '';
if (!trimmed || !photos.length) return; if (!trimmed) return;
const now = new Date().toISOString(); const now = new Date().toISOString();
const normalizedPhotos = photos.map((p, i) => ({ const normalizedPhotos = (photos || []).map((p, i) => ({
id: p.id || `${Date.now()}_${i}_${Math.random().toString(36).slice(2, 7)}`, id: p.id || `${Date.now()}_${i}_${Math.random().toString(36).slice(2, 7)}`,
url: p.url, url: p.url,
name: p.name || '', name: p.name || '',
@@ -6298,7 +6298,7 @@ export const MockStoreProvider = ({ children }) => {
companyName = t.companyName; companyName = t.companyName;
assignedByUserId = t.assignedBy; assignedByUserId = t.assignedBy;
subId = t.subcontractorId; subId = t.subcontractorId;
const entry = { const activityEntry = {
id: `a_${Date.now()}`, id: `a_${Date.now()}`,
type: 'progress_update', type: 'progress_update',
status: t.status, status: t.status,
@@ -6309,9 +6309,19 @@ export const MockStoreProvider = ({ children }) => {
actorRole: actor?.role || 'Subcontractor', actorRole: actor?.role || 'Subcontractor',
at: now, at: now,
}; };
const historyEntry = {
id: `h_${Date.now()}`,
status: t.status,
actorId: actor?.id || t.subcontractorId,
actorName: actor?.name || t.subcontractorName,
comment: trimmed,
at: now,
isProgressUpdate: true,
};
return { return {
...t, ...t,
activities: [...(t.activities || []), entry], activities: [...(t.activities || []), activityEntry],
statusHistory: [...(t.statusHistory || []), historyEntry],
updatedAt: now, updatedAt: now,
}; };
})); }));
@@ -348,28 +348,28 @@ const TaskHeaderCard = ({ task, overdue, onLogExpense, onLogFee }) => {
const STATUS_COPY = { const STATUS_COPY = {
Assigned: { Assigned: {
title: 'Acknowledge assignment', title: 'Acknowledge assignment',
hint: 'Confirm you have received this task and attach a photo of the site or scope of work.', hint: 'Confirm you have received this task and share any notes about the site or scope of work.',
notePlaceholder: 'e.g. Acknowledged — heading to site tomorrow morning.', notePlaceholder: 'e.g. Acknowledged — heading to site tomorrow morning.',
photoHint: 'Site or arrival photo', photoHint: 'Site or arrival photo',
accent: 'amber', accent: 'amber',
}, },
'In Progress': { 'In Progress': {
title: 'Start work', title: 'Start work',
hint: 'Add a brief note about what you are starting and one or more "before" photos.', hint: 'Add a brief note about what you are starting and attach any "before" photos if you have them.',
notePlaceholder: 'e.g. On site. Materials staged. Starting south slope ridge cap removal.', notePlaceholder: 'e.g. On site. Materials staged. Starting south slope ridge cap removal.',
photoHint: 'Before / starting photo', photoHint: 'Before / starting photo',
accent: 'blue', accent: 'blue',
}, },
'On Hold': { 'On Hold': {
title: 'Put task on hold', title: 'Put task on hold',
hint: 'Tell us why work has stopped, and attach a supporting photo (weather, materials, access, etc.).', hint: 'Tell us why work has stopped and add any supporting photos (weather, materials, access, etc.) if applicable.',
notePlaceholder: 'e.g. Work stopped due to rain. Will resume tomorrow.', notePlaceholder: 'e.g. Work stopped due to rain. Will resume tomorrow.',
photoHint: 'Supporting photo (e.g. rain, blocked access)', photoHint: 'Supporting photo (e.g. rain, blocked access)',
accent: 'orange', accent: 'orange',
}, },
Completed: { Completed: {
title: 'Mark as completed', title: 'Mark as completed',
hint: 'Add a final completion note and at least one photo showing the finished work.', hint: 'Add a final completion note and attach any photos of the finished work.',
notePlaceholder: 'e.g. House painting completed successfully. Walked through with homeowner.', notePlaceholder: 'e.g. House painting completed successfully. Walked through with homeowner.',
photoHint: 'Completion / final-look photo(s)', photoHint: 'Completion / final-look photo(s)',
accent: 'emerald', accent: 'emerald',
@@ -396,12 +396,10 @@ const StatusManagementCard = ({ task, statuses, disabled, onChange, onPostProgre
const copy = pendingStatus ? STATUS_COPY[pendingStatus] || STATUS_COPY.Assigned : null; const copy = pendingStatus ? STATUS_COPY[pendingStatus] || STATUS_COPY.Assigned : null;
const accentBtnCls = copy ? ACCENT_BTN[copy.accent] : ACCENT_BTN.blue; const accentBtnCls = copy ? ACCENT_BTN[copy.accent] : ACCENT_BTN.blue;
const noteOk = statusNote.trim().length > 0; const noteOk = statusNote.trim().length > 0;
const photoOk = statusPhotos.length > 0; const canConfirm = noteOk && !isSubmittingStatus;
const canConfirm = noteOk && photoOk && !isSubmittingStatus;
const progressNoteOk = progressNote.trim().length > 0; const progressNoteOk = progressNote.trim().length > 0;
const progressPhotoOk = progressPhotos.length > 0; const canPostProgress = progressNoteOk && !isSubmittingProgress;
const canPostProgress = progressNoteOk && progressPhotoOk && !isSubmittingProgress;
const handlePick = (s) => { const handlePick = (s) => {
if (disabled) return; if (disabled) return;
@@ -443,15 +441,34 @@ const StatusManagementCard = ({ task, statuses, disabled, onChange, onPostProgre
}, 350); }, 350);
}; };
const canPostUpdates = task.status === 'In Progress' || task.status === 'On Hold'; const canPostUpdates = ['Assigned', 'In Progress', 'On Hold', 'Completed'].includes(task.status);
const updateAccent = task.status === 'On Hold' ? 'orange' : 'blue'; const updateStatusCfg = STATUS_CONFIG[task.status] || STATUS_CONFIG.Assigned;
const updateBadgeCls = task.status === 'On Hold' const updateBadgeCls = updateStatusCfg.cls;
? 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-400' const UpdateIcon = updateStatusCfg.icon || Play;
: 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400'; const postBtnCls = (() => {
const UpdateIcon = task.status === 'On Hold' ? PauseCircle : Play; switch (task.status) {
const postBtnCls = task.status === 'On Hold' case 'Assigned': return 'bg-amber-600 hover:bg-amber-500 shadow-amber-500/20';
? 'bg-orange-600 hover:bg-orange-500 shadow-orange-500/20' case 'On Hold': return 'bg-orange-600 hover:bg-orange-500 shadow-orange-500/20';
: 'bg-blue-600 hover:bg-blue-500 shadow-blue-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 materials — supplier ETA Friday."';
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.';
}
})();
const sortedActivities = useMemo(() => { const sortedActivities = useMemo(() => {
return [...(task.activities || [])].sort((a, b) => new Date(b.at) - new Date(a.at)); return [...(task.activities || [])].sort((a, b) => new Date(b.at) - new Date(a.at));
@@ -462,7 +479,7 @@ const StatusManagementCard = ({ task, statuses, disabled, onChange, onPostProgre
<SectionHeading <SectionHeading
icon={RefreshCcw} icon={RefreshCcw}
title="Update Task Status" title="Update Task Status"
hint="Move this task through the workflow. A note and at least one photo are required for every status change." 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"> <div className="grid grid-cols-2 md:grid-cols-4 gap-2 sm:gap-3 mt-4">
@@ -520,7 +537,10 @@ const StatusManagementCard = ({ task, statuses, disabled, onChange, onPostProgre
</div> </div>
<div> <div>
<RequiredLabel ok={photoOk}>Photo upload <span className="text-zinc-400 font-normal normal-case"> {copy.photoHint}</span></RequiredLabel> <label className="text-xs font-bold uppercase tracking-wider text-zinc-600 dark:text-zinc-300 mb-1.5 flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-zinc-400" />
<span>Photo upload <span className="text-zinc-400 font-normal normal-case"> {copy.photoHint}</span></span>
</label>
<PhotoPicker <PhotoPicker
photos={statusPhotos} photos={statusPhotos}
onChange={setStatusPhotos} onChange={setStatusPhotos}
@@ -532,11 +552,7 @@ const StatusManagementCard = ({ task, statuses, disabled, onChange, onPostProgre
{!canConfirm && !isSubmittingStatus && ( {!canConfirm && !isSubmittingStatus && (
<div className="flex items-start gap-2 text-[11px] text-amber-700 dark:text-amber-400"> <div className="flex items-start gap-2 text-[11px] text-amber-700 dark:text-amber-400">
<AlertTriangle size={12} className="mt-0.5 shrink-0" /> <AlertTriangle size={12} className="mt-0.5 shrink-0" />
<span> <span>Add a note to enable the status update.</span>
{(!noteOk && !photoOk) && 'Add a note and at least one photo to enable the status update.'}
{(!noteOk && photoOk) && 'Add a note to enable the status update.'}
{(noteOk && !photoOk) && 'Upload at least one photo to enable the status update.'}
</span>
</div> </div>
)} )}
@@ -564,8 +580,7 @@ const StatusManagementCard = ({ task, statuses, disabled, onChange, onPostProgre
</div> </div>
)} )}
{/* Continuous progress updates while In Progress or On Hold */} {!pendingStatus && canPostUpdates && (
{!pendingStatus && canPostUpdates && !disabled && (
<div className="mt-4 p-4 rounded-xl bg-zinc-50 dark:bg-white/[0.03] border border-zinc-200 dark:border-white/10 space-y-4"> <div className="mt-4 p-4 rounded-xl bg-zinc-50 dark:bg-white/[0.03] border border-zinc-200 dark:border-white/10 space-y-4">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider ${updateBadgeCls}`}> <span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider ${updateBadgeCls}`}>
@@ -573,11 +588,7 @@ const StatusManagementCard = ({ task, statuses, disabled, onChange, onPostProgre
</span> </span>
<h4 className="text-sm font-bold text-zinc-900 dark:text-white">Post a progress update</h4> <h4 className="text-sm font-bold text-zinc-900 dark:text-white">Post a progress update</h4>
</div> </div>
<p className="text-[12px] text-zinc-500"> <p className="text-[12px] text-zinc-500">{progressCopy}</p>
{task.status === 'On Hold'
? 'Keep the admin posted while this task is paused — share why it is still on hold or what changed. Each update needs a note and at least one photo.'
: 'Keep the admin in the loop with ongoing updates while you work. Each update needs a note and at least one photo.'}
</p>
<div> <div>
<RequiredLabel ok={progressNoteOk}>Progress note</RequiredLabel> <RequiredLabel ok={progressNoteOk}>Progress note</RequiredLabel>
@@ -586,20 +597,21 @@ const StatusManagementCard = ({ task, statuses, disabled, onChange, onPostProgre
onChange={(e) => setProgressNote(e.target.value)} onChange={(e) => setProgressNote(e.target.value)}
rows={2} rows={2}
disabled={isSubmittingProgress} disabled={isSubmittingProgress}
placeholder={task.status === 'On Hold' placeholder={progressPlaceholder}
? 'e.g. "Still waiting on materials — supplier ETA Friday."'
: 'e.g. "First wall painting completed."'}
className="w-full text-sm rounded-lg bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-white/10 px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500/40 transition-colors disabled:opacity-60" className="w-full text-sm rounded-lg bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-white/10 px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500/40 transition-colors disabled:opacity-60"
/> />
</div> </div>
<div> <div>
<RequiredLabel ok={progressPhotoOk}>Photos</RequiredLabel> <label className="text-xs font-bold uppercase tracking-wider text-zinc-600 dark:text-zinc-300 mb-1.5 flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-zinc-400" />
<span>Photos</span>
</label>
<PhotoPicker <PhotoPicker
photos={progressPhotos} photos={progressPhotos}
onChange={setProgressPhotos} onChange={setProgressPhotos}
disabled={isSubmittingProgress} disabled={isSubmittingProgress}
ctaLabel={task.status === 'On Hold' ? 'Tap to upload supporting photo(s)' : 'Tap to upload progress photo(s)'} ctaLabel="Tap to upload photo(s)"
/> />
</div> </div>