subcontrcator: post a status update section- removed photo required option
This commit is contained in:
+14
-4
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user