From 371c52fe9d804fbb6e9e5675b942c859c6ef6712 Mon Sep 17 00:00:00 2001 From: Satyam Rastogi Date: Thu, 21 May 2026 15:50:19 +0530 Subject: [PATCH] subcontractor: task thread section having teams and direct chat options now --- src/data/mockStore.jsx | 54 +++++++------- .../SubcontractorTaskDetailPage.jsx | 72 ++++++++++++------- 2 files changed, 75 insertions(+), 51 deletions(-) diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index 9c2fbed..5608142 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -5133,13 +5133,13 @@ const MOCK_SUBCONTRACTOR_TASKS = [ { id: 'sct_001_h2', status: 'In Progress', actorId: 'sub_002', actorName: 'Maya Patel', comment: 'On site, sourced materials.', at: '2026-05-16T08:30:00Z' }, ], thread: [ - { id: 'sct_001_t1', channel: 'external', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Please source them — match Owens Corning Estate Gray as best you can.', at: '2026-05-15T09:14:00Z' }, - { id: 'sct_001_t2', channel: 'external', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'Got it. I’ll be there on May 20. Do you have shingles in storage or should I source them?', at: '2026-05-15T10:30:00Z', mine: true }, - { id: 'sct_001_t3', channel: 'external', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Source them and put it on the task expenses. Homeowner is OK to reimburse.', at: '2026-05-15T11:00:00Z' }, - { id: 'sct_001_t4', channel: 'external', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'Sounds good — picking up at Home Depot tomorrow morning.', at: '2026-05-15T11:08:00Z', mine: true }, - { id: 'sct_001_t5', channel: 'internal', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Heads up team: homeowner is sensitive about noise — keep work to daylight hours only. No early-morning starts.', at: '2026-05-15T11:05:00Z' }, - { id: 'sct_001_t6', channel: 'internal', senderId: 'ADM01', senderName: 'Admin One', senderRole: 'Admin', body: 'Permits are not required for this scope. Skip the city portal step.', at: '2026-05-15T12:10:00Z' }, - { id: 'sct_001_t7', channel: 'internal', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'Noted. Will text homeowner when crew is 30 min out.', at: '2026-05-15T12:25:00Z', mine: true }, + { id: 'sct_001_t1', channel: 'group', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Please source them — match Owens Corning Estate Gray as best you can.', at: '2026-05-15T09:14:00Z' }, + { id: 'sct_001_t2', channel: 'group', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'Got it. I’ll be there on May 20. Do you have shingles in storage or should I source them?', at: '2026-05-15T10:30:00Z', mine: true }, + { id: 'sct_001_t3', channel: 'group', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Source them and put it on the task expenses. Homeowner is OK to reimburse.', at: '2026-05-15T11:00:00Z' }, + { id: 'sct_001_t4', channel: 'group', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'Sounds good — picking up at Home Depot tomorrow morning.', at: '2026-05-15T11:08:00Z', mine: true }, + { id: 'sct_001_t5', channel: 'direct', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Heads up team: homeowner is sensitive about noise — keep work to daylight hours only. No early-morning starts.', at: '2026-05-15T11:05:00Z' }, + { id: 'sct_001_t6', channel: 'group', senderId: 'ADM01', senderName: 'Admin One', senderRole: 'Admin', body: 'Permits are not required for this scope. Skip the city portal step.', at: '2026-05-15T12:10:00Z' }, + { id: 'sct_001_t7', channel: 'direct', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'Noted. Will text homeowner when crew is 30 min out.', at: '2026-05-15T12:25:00Z', mine: true }, ], expenses: [ { id: 'sct_001_e1', description: 'Owens Corning Estate Gray (single pkg)', category: 'Materials', amount: 64.5, notes: 'From Home Depot, kept receipt.', receiptUrl: 'mock://receipts/oc-estate-gray.pdf', createdAt: '2026-05-16T13:10:00Z' }, @@ -5222,9 +5222,9 @@ const MOCK_SUBCONTRACTOR_TASKS = [ { id: 'sct_002_h2', status: 'In Progress', actorId: 'sub_001', actorName: 'Carlos Subcontractor', comment: 'On site, started disconnect.', at: '2026-05-12T08:20:00Z' }, ], thread: [ - { id: 'sct_002_t1', channel: 'external', senderId: 'sub_001', senderName: 'Carlos Subcontractor', senderRole: 'Subcontractor', body: 'Oncor disconnect is confirmed for May 12, 8 AM.', at: '2026-05-11T16:00:00Z', mine: true }, - { id: 'sct_002_t2', channel: 'external', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Perfect. Homeowner will be on site.', at: '2026-05-11T16:30:00Z' }, - { id: 'sct_002_t3', channel: 'internal', senderId: 'ADM01', senderName: 'Admin One', senderRole: 'Admin', body: 'Inspection slot booked with the city for May 15.', at: '2026-05-12T09:30:00Z' }, + { id: 'sct_002_t1', channel: 'group', senderId: 'sub_001', senderName: 'Carlos Subcontractor', senderRole: 'Subcontractor', body: 'Oncor disconnect is confirmed for May 12, 8 AM.', at: '2026-05-11T16:00:00Z', mine: true }, + { id: 'sct_002_t2', channel: 'group', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Perfect. Homeowner will be on site.', at: '2026-05-11T16:30:00Z' }, + { id: 'sct_002_t3', channel: 'group', senderId: 'ADM01', senderName: 'Admin One', senderRole: 'Admin', body: 'Inspection slot booked with the city for May 15.', at: '2026-05-12T09:30:00Z' }, ], expenses: [ { id: 'sct_002_e1', description: '200A panel — Square D', category: 'Materials', amount: 312.0, notes: '', receiptUrl: '', createdAt: '2026-05-11T09:00:00Z' }, @@ -5314,7 +5314,7 @@ const MOCK_SUBCONTRACTOR_TASKS = [ { id: 'sct_003_h3', status: 'Completed', actorId: 'sub_003', actorName: 'Dwayne Holland', comment: 'Walked through with homeowner.', at: '2026-05-14T17:30:00Z' }, ], thread: [ - { id: 'sct_003_t1', channel: 'external', senderId: 'sub_003', senderName: 'Dwayne Holland', senderRole: 'Subcontractor', body: 'Wrapped up — homeowner signed off. Photos uploaded.', at: '2026-05-14T17:25:00Z', mine: true }, + { id: 'sct_003_t1', channel: 'group', senderId: 'sub_003', senderName: 'Dwayne Holland', senderRole: 'Subcontractor', body: 'Wrapped up — homeowner signed off. Photos uploaded.', at: '2026-05-14T17:25:00Z', mine: true }, ], expenses: [ { id: 'sct_003_e1', description: 'Paint — SW Repose Gray (1 gal)', category: 'Materials', amount: 52.5, notes: '', receiptUrl: '', createdAt: '2026-05-13T07:30:00Z' }, @@ -5355,9 +5355,9 @@ const MOCK_SUBCONTRACTOR_TASKS = [ { id: 'sct_004_h1', status: 'Assigned', actorId: 'owner_001', actorName: 'Justin Johnson', comment: 'Storm-response priority. Please confirm ETA.', at: '2026-05-18T22:40:00Z' }, ], thread: [ - { id: 'sct_004_t1', channel: 'external', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Can you make it first thing tomorrow morning?', at: '2026-05-18T22:42:00Z' }, - { id: 'sct_004_t2', channel: 'external', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'Yes — on site by 7:30 AM. Bringing tarps and battens.', at: '2026-05-18T23:05:00Z', mine: true }, - { id: 'sct_004_t3', channel: 'internal', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Customer is filing an insurance claim — make sure to document everything with photos.', at: '2026-05-18T23:10:00Z' }, + { id: 'sct_004_t1', channel: 'group', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Can you make it first thing tomorrow morning?', at: '2026-05-18T22:42:00Z' }, + { id: 'sct_004_t2', channel: 'group', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'Yes — on site by 7:30 AM. Bringing tarps and battens.', at: '2026-05-18T23:05:00Z', mine: true }, + { id: 'sct_004_t3', channel: 'direct', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Customer is filing an insurance claim — make sure to document everything with photos.', at: '2026-05-18T23:10:00Z' }, ], expenses: [], fees: [], @@ -5394,8 +5394,8 @@ const MOCK_SUBCONTRACTOR_TASKS = [ { id: 'sct_005_h3', status: 'On Hold', actorId: 'sub_002', actorName: 'Maya Patel', comment: 'Waiting on homeowner to clear stored boxes from access path.', at: '2026-05-15T16:00:00Z' }, ], thread: [ - { id: 'sct_005_t1', channel: 'external', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'Access is blocked — homeowner needs to move some storage before I can complete the inspection.', at: '2026-05-15T15:55:00Z', mine: true }, - { id: 'sct_005_t2', channel: 'internal', senderId: 'ADM01', senderName: 'Admin One', senderRole: 'Admin', body: 'I will reach out to the homeowner and reschedule.', at: '2026-05-15T16:20:00Z' }, + { id: 'sct_005_t1', channel: 'group', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'Access is blocked — homeowner needs to move some storage before I can complete the inspection.', at: '2026-05-15T15:55:00Z', mine: true }, + { id: 'sct_005_t2', channel: 'direct', senderId: 'ADM01', senderName: 'Admin One', senderRole: 'Admin', body: 'I will reach out to the homeowner and reschedule.', at: '2026-05-15T16:20:00Z' }, ], expenses: [ { id: 'sct_005_e1', description: 'Infrared thermometer (rental)', category: 'Equipment Rental', amount: 35.0, notes: '', receiptUrl: '', createdAt: '2026-05-14T10:30:00Z' }, @@ -5494,7 +5494,7 @@ const MOCK_SUBCONTRACTOR_TASKS = [ { id: 'sct_007_h3', status: 'Completed', actorId: 'sub_002', actorName: 'Maya Patel', comment: 'Closed out, photos in.', at: '2026-05-07T16:45:00Z' }, ], thread: [ - { id: 'sct_007_t1', channel: 'external', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'All done — closed out. Final photos uploaded.', at: '2026-05-07T16:40:00Z', mine: true }, + { id: 'sct_007_t1', channel: 'group', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'All done — closed out. Final photos uploaded.', at: '2026-05-07T16:40:00Z', mine: true }, ], expenses: [ { id: 'sct_007_e1', description: 'OSB sheathing 4x8 (2 sheets)', category: 'Materials', amount: 76.0, notes: '', receiptUrl: '', createdAt: '2026-05-05T07:00:00Z' }, @@ -5540,14 +5540,14 @@ const MOCK_SUBCONTRACTOR_TASKS = [ { id: 'sct_008_h2', status: 'In Progress', actorId: 'sub_001', actorName: 'Carlos Subcontractor', comment: 'On site, kitchen done; bath next.', at: '2026-05-19T10:15:00Z' }, ], thread: [ - { id: 'sct_008_t1', channel: 'external', senderId: 'ADM01', senderName: 'Admin One', senderRole: 'Admin', body: 'Insurance is reimbursing the code upgrade — keep receipts for the GFCI devices.', at: '2026-05-16T13:05:00Z' }, - { id: 'sct_008_t2', channel: 'external', senderId: 'sub_001', senderName: 'Carlos Subcontractor', senderRole: 'Subcontractor', body: 'Confirmed — Leviton 20A devices, will keep receipts. Targeting May 19 for the visit.', at: '2026-05-16T14:20:00Z', mine: true }, - { id: 'sct_008_t3', channel: 'external', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Homeowner has 2 small kids — please bring outlet covers as a courtesy.', at: '2026-05-17T09:00:00Z' }, - { id: 'sct_008_t4', channel: 'external', senderId: 'sub_001', senderName: 'Carlos Subcontractor', senderRole: 'Subcontractor', body: 'Will do. Already in my truck.', at: '2026-05-17T09:05:00Z', mine: true }, - { id: 'sct_008_t5', channel: 'external', senderId: 'sub_001', senderName: 'Carlos Subcontractor', senderRole: 'Subcontractor', body: 'Kitchen done. Both counter outlets and the island are on GFCI now. Starting bath after lunch.', at: '2026-05-19T12:30:00Z', mine: true }, - { id: 'sct_008_t6', channel: 'internal', senderId: 'ADM01', senderName: 'Admin One', senderRole: 'Admin', body: 'No permit needed for like-for-like swap. Skip the city portal.', at: '2026-05-16T13:10:00Z' }, - { id: 'sct_008_t7', channel: 'internal', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Carlos — if you find any aluminum branch wiring, stop and flag it. We will scope as a change order.', at: '2026-05-17T10:00:00Z' }, - { id: 'sct_008_t8', channel: 'internal', senderId: 'sub_001', senderName: 'Carlos Subcontractor', senderRole: 'Subcontractor', body: 'All copper, no aluminum. Proceeding as scoped.', at: '2026-05-19T10:30:00Z', mine: true }, + { id: 'sct_008_t1', channel: 'group', senderId: 'ADM01', senderName: 'Admin One', senderRole: 'Admin', body: 'Insurance is reimbursing the code upgrade — keep receipts for the GFCI devices.', at: '2026-05-16T13:05:00Z' }, + { id: 'sct_008_t2', channel: 'group', senderId: 'sub_001', senderName: 'Carlos Subcontractor', senderRole: 'Subcontractor', body: 'Confirmed — Leviton 20A devices, will keep receipts. Targeting May 19 for the visit.', at: '2026-05-16T14:20:00Z', mine: true }, + { id: 'sct_008_t3', channel: 'group', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Homeowner has 2 small kids — please bring outlet covers as a courtesy.', at: '2026-05-17T09:00:00Z' }, + { id: 'sct_008_t4', channel: 'group', senderId: 'sub_001', senderName: 'Carlos Subcontractor', senderRole: 'Subcontractor', body: 'Will do. Already in my truck.', at: '2026-05-17T09:05:00Z', mine: true }, + { id: 'sct_008_t5', channel: 'group', senderId: 'sub_001', senderName: 'Carlos Subcontractor', senderRole: 'Subcontractor', body: 'Kitchen done. Both counter outlets and the island are on GFCI now. Starting bath after lunch.', at: '2026-05-19T12:30:00Z', mine: true }, + { id: 'sct_008_t6', channel: 'direct', senderId: 'ADM01', senderName: 'Admin One', senderRole: 'Admin', body: 'No permit needed for like-for-like swap. Skip the city portal.', at: '2026-05-16T13:10:00Z' }, + { id: 'sct_008_t7', channel: 'group', senderId: 'owner_001', senderName: 'Justin Johnson', senderRole: 'Owner', body: 'Carlos — if you find any aluminum branch wiring, stop and flag it. We will scope as a change order.', at: '2026-05-17T10:00:00Z' }, + { id: 'sct_008_t8', channel: 'direct', senderId: 'sub_001', senderName: 'Carlos Subcontractor', senderRole: 'Subcontractor', body: 'All copper, no aluminum. Proceeding as scoped.', at: '2026-05-19T10:30:00Z', mine: true }, ], expenses: [ { id: 'sct_008_e1', description: 'Leviton 20A GFCI receptacles (qty 4)', category: 'Materials', amount: 92.0, notes: 'SmartlockPro tamper-resistant.', receiptUrl: 'mock://receipts/leviton-gfci.pdf', createdAt: '2026-05-18T16:00:00Z' }, @@ -5619,7 +5619,7 @@ const MOCK_SUBCONTRACTOR_TASKS = [ { 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' }, + { id: 'sct_010_t1', channel: 'group', 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: [], @@ -6192,7 +6192,7 @@ export const MockStoreProvider = ({ children }) => { } }; - const addSubcontractorTaskMessage = (taskId, { body, channel = 'external', sender }) => { + const addSubcontractorTaskMessage = (taskId, { body, channel = 'group', sender }) => { if (!body?.trim()) return; const now = new Date().toISOString(); let companyId = null; diff --git a/src/pages/subcontractor/SubcontractorTaskDetailPage.jsx b/src/pages/subcontractor/SubcontractorTaskDetailPage.jsx index 85f532e..c32fbfc 100644 --- a/src/pages/subcontractor/SubcontractorTaskDetailPage.jsx +++ b/src/pages/subcontractor/SubcontractorTaskDetailPage.jsx @@ -8,7 +8,7 @@ import { ArrowLeft, ChevronRight, ClipboardList, MapPin, Calendar, Building2, User, Hash, DollarSign, Receipt, Camera, Image as ImageIcon, MessageSquare, Lock, Send, Clock, PauseCircle, CheckCircle, RefreshCcw, AlertCircle, X, Plus, Upload, - Activity, ImagePlus, Loader2, Play, Trash2, AlertTriangle, + Activity, ImagePlus, Loader2, Play, Trash2, AlertTriangle, Users, UserCheck, } from 'lucide-react'; // --------------------------------------------------------------------------- @@ -850,10 +850,12 @@ const ActivityTimelineRow = ({ activity, isLast, onPreview }) => { // Section 3 — Task Thread / Communication // =========================================================================== const TaskThreadCard = ({ task, disabled, onSend }) => { - const [channel, setChannel] = useState('external'); // external | internal + const [channel, setChannel] = useState('group'); // group | direct const [draft, setDraft] = useState(''); const scrollerRef = useRef(null); + const supervisorName = task.assignedByName || 'your supervisor'; + const messages = useMemo( () => (task.thread || []).filter(m => m.channel === channel), [task.thread, channel], @@ -872,18 +874,22 @@ const TaskThreadCard = ({ task, disabled, onSend }) => { setDraft(''); }; + const isDirect = channel === 'direct'; + return (
- setChannel('external')} icon={MessageSquare} label="External" /> - setChannel('internal')} icon={Lock} label="Internal" /> + setChannel('group')} icon={Users} label="Team" /> + setChannel('direct')} icon={UserCheck} label="Direct" />
@@ -893,8 +899,14 @@ const TaskThreadCard = ({ task, disabled, onSend }) => { > {messages.length === 0 ? (
- -

No {channel === 'internal' ? 'internal notes' : 'messages'} yet.

+ {isDirect + ? + : } +

+ {isDirect + ? `No direct messages with ${supervisorName} yet.` + : 'No team messages yet.'} +

) : messages.map(m => ( @@ -903,31 +915,38 @@ const TaskThreadCard = ({ task, disabled, onSend }) => {
- {channel === 'internal' && } + {isDirect && } setDraft(e.target.value)} disabled={disabled} - placeholder={channel === 'internal' ? 'Add an internal note…' : 'Start typing…'} + placeholder={isDirect + ? `Message ${supervisorName} privately…` + : 'Message the team…'} className="flex-1 bg-transparent border-0 outline-none text-sm placeholder:text-zinc-400 disabled:opacity-50" />
- {channel === 'internal' && ( -

- Internal notes are visible to your company team only — not the homeowner. + {isDirect ? ( +

+ Direct messages are private between you and {supervisorName}. The rest of the team cannot see them. +

+ ) : ( +

+ Visible to everyone on the project team. The homeowner is not part of this chat.

)}
@@ -952,13 +971,18 @@ const ChannelTab = ({ active, onClick, icon: Icon, label }) => ( const MessageBubble = ({ message, channel }) => { const mine = !!message.mine; - const bubbleBase = channel === 'internal' - ? 'bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20' - : (mine ? 'bg-blue-600 text-white' : 'bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10'); + const isDirect = channel === 'direct'; + const bubbleBase = isDirect + ? (mine + ? 'bg-indigo-600 text-white' + : 'bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20') + : (mine + ? 'bg-blue-600 text-white' + : 'bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10'); const meta = mine - ? 'text-blue-100/80' - : channel === 'internal' - ? 'text-amber-700 dark:text-amber-400' + ? (isDirect ? 'text-indigo-100/80' : 'text-blue-100/80') + : isDirect + ? 'text-indigo-700 dark:text-indigo-400' : 'text-zinc-500'; return ( @@ -966,14 +990,14 @@ const MessageBubble = ({ message, channel }) => {
{!mine && (
{message.senderName?.charAt(0)?.toUpperCase() || '?'}
)} -
+
{!mine && (
{message.senderName} · {message.senderRole}