subcontractor: add progress updates and task activity timeline

This commit is contained in:
Satyam Rastogi
2026-05-21 15:30:19 +05:30
parent ad39848b0f
commit 3a038ffc6f
2 changed files with 706 additions and 62 deletions
+248 -1
View File
@@ -5152,6 +5152,43 @@ const MOCK_SUBCONTRACTOR_TASKS = [
{ id: 'sct_001_f1', description: 'Labour — Ridge Cap Replacement', type: 'Labour / Service Fee', amount: 350, createdAt: '2026-05-16T13:20:00Z' },
{ id: 'sct_001_f2', description: 'Mobilization to site', type: 'Mobilization Fee', amount: 75, createdAt: '2026-05-16T13:22:00Z' },
],
activities: [
{
id: 'sct_001_a1', type: 'status_change', status: 'Assigned',
note: 'Task created and assigned. Materials list shared with crew.',
photos: [],
actorId: 'owner_001', actorName: 'Justin Johnson', actorRole: 'Owner',
at: '2026-05-15T09:12:00Z',
},
{
id: 'sct_001_a2', type: 'status_change', status: 'In Progress',
note: 'On site, sourced replacement Owens Corning Estate Gray. Starting on the south slope.',
photos: [
{ id: 'sct_001_a2_p1', url: '/assets/images/properties/Hail_Damaged_Shingles.jpg', name: 'arrival.jpg' },
],
actorId: 'sub_002', actorName: 'Maya Patel', actorRole: 'Subcontractor',
at: '2026-05-16T08:30:00Z',
},
{
id: 'sct_001_a3', type: 'progress_update', status: 'In Progress',
note: 'First two damaged ridge tiles removed. Decking underneath is clean — no rot.',
photos: [
{ id: 'sct_001_a3_p1', url: '/assets/images/properties/Storm_Worn_Roof.jpg', name: 'decking-clean.jpg' },
],
actorId: 'sub_002', actorName: 'Maya Patel', actorRole: 'Subcontractor',
at: '2026-05-16T10:45:00Z',
},
{
id: 'sct_001_a4', type: 'progress_update', status: 'In Progress',
note: 'Replacement ridge caps installed on lower row. Sealing next.',
photos: [
{ id: 'sct_001_a4_p1', url: '/assets/images/properties/Cracked_Storm_Shingles.jpg', name: 'lower-row.jpg' },
{ id: 'sct_001_a4_p2', url: '/assets/images/properties/Overlapping_Roof_Shingles.jpg', name: 'wide-shot.jpg' },
],
actorId: 'sub_002', actorName: 'Maya Patel', actorRole: 'Subcontractor',
at: '2026-05-16T14:20:00Z',
},
],
projectId: 'proj_004',
paymentStatus: 'Unpaid',
paymentRequestedAt: '2026-05-16T17:30:00Z',
@@ -5197,6 +5234,53 @@ const MOCK_SUBCONTRACTOR_TASKS = [
fees: [
{ id: 'sct_002_f1', description: 'Labour — Day 1 disconnect / prep', type: 'Labour / Service Fee', amount: 425, createdAt: '2026-05-12T17:00:00Z' },
],
activities: [
{
id: 'sct_002_a1', type: 'status_change', status: 'Assigned',
note: 'Task created and assigned. Permit pulled, materials staged.',
photos: [],
actorId: 'owner_001', actorName: 'Justin Johnson', actorRole: 'Owner',
at: '2026-05-10T15:40:00Z',
},
{
id: 'sct_002_a2', type: 'status_change', status: 'In Progress',
note: 'Oncor on site for disconnect. Cutting power to existing 100A panel and starting tear-out.',
photos: [
{ id: 'sct_002_a2_p1', url: '/assets/images/properties/Beige_Two_Story_House.jpg', name: 'oncor-disconnect.jpg' },
],
actorId: 'sub_001', actorName: 'Carlos Subcontractor', actorRole: 'Subcontractor',
at: '2026-05-12T08:20:00Z',
},
{
id: 'sct_002_a3', type: 'progress_update', status: 'In Progress',
note: 'Old 100A panel removed. Service entrance wires capped and labelled. Mast looks good — no signs of corrosion.',
photos: [
{ id: 'sct_002_a3_p1', url: '/assets/images/properties/Modern_White_Suburban_Home.jpg', name: 'mast-check.jpg' },
{ id: 'sct_002_a3_p2', url: '/assets/images/properties/Brick_Front_Porch_Home.jpg', name: 'old-panel-out.jpg' },
],
actorId: 'sub_001', actorName: 'Carlos Subcontractor', actorRole: 'Subcontractor',
at: '2026-05-12T10:30:00Z',
},
{
id: 'sct_002_a4', type: 'progress_update', status: 'In Progress',
note: 'New 200A Square D panel mounted and bonded. Two new ground rods driven and connected.',
photos: [
{ id: 'sct_002_a4_p1', url: '/assets/images/properties/Sunny_Suburban_House.jpg', name: 'new-panel-mount.jpg' },
],
actorId: 'sub_001', actorName: 'Carlos Subcontractor', actorRole: 'Subcontractor',
at: '2026-05-12T13:15:00Z',
},
{
id: 'sct_002_a5', type: 'progress_update', status: 'In Progress',
note: 'Migrated 20 of 24 home circuits to the new panel. Tested with multimeter — all live and balanced across legs. Remaining 4 circuits (HVAC + dryer) to be wired tomorrow before re-energizing.',
photos: [
{ id: 'sct_002_a5_p1', url: '/assets/images/properties/Luxury_Driveway_Residence.jpg', name: 'circuits-migrated.jpg' },
{ id: 'sct_002_a5_p2', url: '/assets/images/properties/Overlapping_Roof_Shingles.jpg', name: 'panel-labels.jpg' },
],
actorId: 'sub_001', actorName: 'Carlos Subcontractor', actorRole: 'Subcontractor',
at: '2026-05-12T16:00:00Z',
},
],
projectId: 'proj_003',
paymentStatus: 'Unpaid',
paymentRequestedAt: '2026-05-12T17:00:00Z',
@@ -5319,6 +5403,33 @@ const MOCK_SUBCONTRACTOR_TASKS = [
fees: [
{ id: 'sct_005_f1', description: 'Inspection — initial visit', type: 'Inspection Fee', amount: 125, createdAt: '2026-05-14T13:00:00Z' },
],
activities: [
{
id: 'sct_005_a1', type: 'status_change', status: 'Assigned',
note: 'Task created. Customer reports unusually high attic temps.',
photos: [],
actorId: 'ADM01', actorName: 'Admin One', actorRole: 'Admin',
at: '2026-05-12T10:00:00Z',
},
{
id: 'sct_005_a2', type: 'status_change', status: 'In Progress',
note: 'Initial inspection pass — soffit baffles appear blocked. Need attic access for confirmation.',
photos: [
{ id: 'sct_005_a2_p1', url: '/assets/images/properties/Roof_Inspection_Angle.jpg', name: 'south-gable.jpg' },
],
actorId: 'sub_002', actorName: 'Maya Patel', actorRole: 'Subcontractor',
at: '2026-05-14T11:00:00Z',
},
{
id: 'sct_005_a3', type: 'status_change', status: 'On Hold',
note: 'Work stopped — homeowner needs to clear stored boxes blocking attic access before I can finish.',
photos: [
{ id: 'sct_005_a3_p1', url: '/assets/images/properties/Beige_Two_Story_House.jpg', name: 'access-blocked.jpg' },
],
actorId: 'sub_002', actorName: 'Maya Patel', actorRole: 'Subcontractor',
at: '2026-05-15T16:00:00Z',
},
],
projectId: 'proj_012',
paymentStatus: 'Unpaid',
paymentRequestedAt: '2026-05-14T13:00:00Z',
@@ -5449,6 +5560,33 @@ const MOCK_SUBCONTRACTOR_TASKS = [
{ id: 'sct_008_f1', description: 'Labour — Kitchen GFCI install (2 outlets + island)', type: 'Labour / Service Fee', amount: 220, createdAt: '2026-05-19T12:35:00Z' },
{ id: 'sct_008_f2', description: 'Mobilization to site', type: 'Mobilization Fee', amount: 75, createdAt: '2026-05-19T08:00:00Z' },
],
activities: [
{
id: 'sct_008_a1', type: 'status_change', status: 'Assigned',
note: 'Task created and assigned to Carlos. Insurance covering code upgrade.',
photos: [],
actorId: 'ADM01', actorName: 'Admin One', actorRole: 'Admin',
at: '2026-05-16T13:00:00Z',
},
{
id: 'sct_008_a2', type: 'status_change', status: 'In Progress',
note: 'Arrived on site. Verified wiring is copper (no aluminum). Starting kitchen counter outlets first.',
photos: [
{ id: 'sct_008_a2_p1', url: '/assets/images/properties/Modern_White_Suburban_Home.jpg', name: 'kitchen-before.jpg' },
],
actorId: 'sub_001', actorName: 'Carlos Subcontractor', actorRole: 'Subcontractor',
at: '2026-05-19T10:15:00Z',
},
{
id: 'sct_008_a3', type: 'progress_update', status: 'In Progress',
note: 'Both kitchen counter outlets swapped to Leviton 20A GFCI. Tested with plug-in tester — passed.',
photos: [
{ id: 'sct_008_a3_p1', url: '/assets/images/properties/Sunny_Suburban_House.jpg', name: 'gfci-installed.jpg' },
],
actorId: 'sub_001', actorName: 'Carlos Subcontractor', actorRole: 'Subcontractor',
at: '2026-05-19T12:30:00Z',
},
],
projectId: 'proj_011',
paymentStatus: 'Unpaid',
paymentRequestedAt: '2026-05-19T12:35:00Z',
@@ -5456,6 +5594,43 @@ const MOCK_SUBCONTRACTOR_TASKS = [
updatedAt: '2026-05-19T18:00:00Z',
},
// -------------------------------------------------------------------
// sct_010 Carlos (sub_001) newly assigned, awaiting acknowledgement
// -------------------------------------------------------------------
{
id: 'sct_010',
companyId: 'lynkeduppro',
companyName: 'LynkedUp Pro Roofing',
subcontractorId: 'sub_001',
subcontractorName: 'Carlos Subcontractor',
assignedBy: 'owner_001',
assignedByName: 'Justin Johnson',
title: 'Install EV charger circuit — garage',
location: '3917 Arizona Pl, Plano, TX 75023',
description: 'Homeowner purchased a Tesla Wall Connector. Run a dedicated 60A 240V circuit from the main panel to the garage wall. Coordinate with homeowner for outage window. Permit required.',
dueDate: '2026-05-29',
priority: 'medium',
status: 'Assigned',
photos: [
{ id: 'sct_010_p1', name: 'Garage_Wall.jpg', url: '/assets/images/properties/Modern_White_Suburban_Home.jpg', annotation: 'Wall Connector mounting location' },
{ id: 'sct_010_p2', name: 'Panel_Location.jpg', url: '/assets/images/properties/Brick_Front_Porch_Home.jpg', annotation: 'Main panel — check available breaker slots' },
],
statusHistory: [
{ id: 'sct_010_h1', status: 'Assigned', actorId: 'owner_001', actorName: 'Justin Johnson', comment: 'Task created and assigned. Please confirm ETA.', at: '2026-05-20T09:00:00Z' },
],
thread: [
{ id: 'sct_010_t1', channel: 'external', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Carlos — homeowner is hoping to have this in before next weekend. Can you take a look at the panel and confirm slot availability?', at: '2026-05-20T09:02:00Z' },
],
expenses: [],
fees: [],
activities: [],
projectId: 'proj_011',
paymentStatus: 'Unpaid',
paymentRequestedAt: null,
createdAt: '2026-05-20T09:00:00Z',
updatedAt: '2026-05-20T09:02:00Z',
},
// -------------------------------------------------------------------
// sct_009 Jennifer Wu (sub_004) On Hold plumbing
// -------------------------------------------------------------------
@@ -5952,13 +6127,19 @@ export const MockStoreProvider = ({ children }) => {
return task;
};
const setSubcontractorTaskStatus = (taskId, nextStatus, comment, actor) => {
const setSubcontractorTaskStatus = (taskId, nextStatus, comment, actor, photos = []) => {
const now = new Date().toISOString();
let didChange = false;
let companyId = null;
let companyName = null;
let assignedByUserId = null;
const normalizedPhotos = (photos || []).map((p, i) => ({
id: p.id || `${Date.now()}_${i}_${Math.random().toString(36).slice(2, 7)}`,
url: p.url,
name: p.name || '',
}));
setSubcontractorTasks(prev => prev.map(t => {
if (t.id !== taskId) return t;
if (t.status === nextStatus) return t;
@@ -5974,10 +6155,22 @@ export const MockStoreProvider = ({ children }) => {
comment: comment?.trim() || '',
at: now,
};
const activityEntry = {
id: `a_${Date.now()}`,
type: 'status_change',
status: nextStatus,
note: comment?.trim() || '',
photos: normalizedPhotos,
actorId: actor?.id || t.subcontractorId,
actorName: actor?.name || t.subcontractorName,
actorRole: actor?.role || 'Subcontractor',
at: now,
};
return {
...t,
status: nextStatus,
statusHistory: [...(t.statusHistory || []), entry],
activities: [...(t.activities || []), activityEntry],
updatedAt: now,
};
}));
@@ -6085,6 +6278,59 @@ export const MockStoreProvider = ({ children }) => {
toast.success('Fee submitted');
};
const addSubcontractorTaskActivity = (taskId, { note, photos = [] }, actor) => {
const trimmed = note?.trim() || '';
if (!trimmed || !photos.length) return;
const now = new Date().toISOString();
const normalizedPhotos = photos.map((p, i) => ({
id: p.id || `${Date.now()}_${i}_${Math.random().toString(36).slice(2, 7)}`,
url: p.url,
name: p.name || '',
}));
let companyId = null;
let companyName = null;
let assignedByUserId = null;
let subId = null;
setSubcontractorTasks(prev => prev.map(t => {
if (t.id !== taskId) return t;
companyId = t.companyId;
companyName = t.companyName;
assignedByUserId = t.assignedBy;
subId = t.subcontractorId;
const entry = {
id: `a_${Date.now()}`,
type: 'progress_update',
status: t.status,
note: trimmed,
photos: normalizedPhotos,
actorId: actor?.id || t.subcontractorId,
actorName: actor?.name || t.subcontractorName,
actorRole: actor?.role || 'Subcontractor',
at: now,
};
return {
...t,
activities: [...(t.activities || []), entry],
updatedAt: now,
};
}));
const recipientUserId = actor?.id === assignedByUserId ? subId : assignedByUserId;
if (recipientUserId) {
addNotification({
recipientUserId,
recipientRole: actor?.id === assignedByUserId ? 'SUBCONTRACTOR' : 'OWNER',
type: 'progress_update',
message: `New progress update from ${actor?.name || 'subcontractor'}`,
taskId,
fromCompanyId: companyId,
fromCompanyName: companyName,
});
}
toast.success('Progress update posted');
};
const updateSubcontractorTask = (taskId, data) => {
let updatedSubId = null;
let prevSubId = null;
@@ -6421,6 +6667,7 @@ export const MockStoreProvider = ({ children }) => {
addSubcontractorTaskMessage,
addSubcontractorTaskExpense,
addSubcontractorTaskFee,
addSubcontractorTaskActivity,
notifications,
addNotification,
markNotificationRead,