subcontractor: task thread section having teams and direct chat options now

This commit is contained in:
Satyam Rastogi
2026-05-21 15:50:19 +05:30
parent 3a038ffc6f
commit 371c52fe9d
2 changed files with 75 additions and 51 deletions
+27 -27
View File
@@ -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' }, { 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: [ 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_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: 'external', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'Got it. Ill 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_t2', channel: 'group', senderId: 'sub_002', senderName: 'Maya Patel', senderRole: 'Subcontractor', body: 'Got it. Ill 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_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: '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_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: '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_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: '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_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: '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_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: [ 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' }, { 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' }, { 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: [ 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_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: '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_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: '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_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: [ expenses: [
{ id: 'sct_002_e1', description: '200A panel — Square D', category: 'Materials', amount: 312.0, notes: '', receiptUrl: '', createdAt: '2026-05-11T09:00:00Z' }, { 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' }, { id: 'sct_003_h3', status: 'Completed', actorId: 'sub_003', actorName: 'Dwayne Holland', comment: 'Walked through with homeowner.', at: '2026-05-14T17:30:00Z' },
], ],
thread: [ 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: [ 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' }, { 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' }, { 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: [ 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_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: '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_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: '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_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: [], expenses: [],
fees: [], 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' }, { 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: [ 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_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: '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_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: [ expenses: [
{ id: 'sct_005_e1', description: 'Infrared thermometer (rental)', category: 'Equipment Rental', amount: 35.0, notes: '', receiptUrl: '', createdAt: '2026-05-14T10:30:00Z' }, { 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' }, { id: 'sct_007_h3', status: 'Completed', actorId: 'sub_002', actorName: 'Maya Patel', comment: 'Closed out, photos in.', at: '2026-05-07T16:45:00Z' },
], ],
thread: [ 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: [ expenses: [
{ id: 'sct_007_e1', description: 'OSB sheathing 4x8 (2 sheets)', category: 'Materials', amount: 76.0, notes: '', receiptUrl: '', createdAt: '2026-05-05T07:00:00Z' }, { 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' }, { 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: [ 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_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: '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_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: '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_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: '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_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: '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_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: '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_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: '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_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: '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_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: [ 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' }, { 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' }, { 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: [ 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: [], expenses: [],
fees: [], 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; if (!body?.trim()) return;
const now = new Date().toISOString(); const now = new Date().toISOString();
let companyId = null; let companyId = null;
@@ -8,7 +8,7 @@ import {
ArrowLeft, ChevronRight, ClipboardList, MapPin, Calendar, Building2, User, Hash, ArrowLeft, ChevronRight, ClipboardList, MapPin, Calendar, Building2, User, Hash,
DollarSign, Receipt, Camera, Image as ImageIcon, MessageSquare, Lock, Send, DollarSign, Receipt, Camera, Image as ImageIcon, MessageSquare, Lock, Send,
Clock, PauseCircle, CheckCircle, RefreshCcw, AlertCircle, X, Plus, Upload, 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'; } from 'lucide-react';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -850,10 +850,12 @@ const ActivityTimelineRow = ({ activity, isLast, onPreview }) => {
// Section 3 — Task Thread / Communication // Section 3 — Task Thread / Communication
// =========================================================================== // ===========================================================================
const TaskThreadCard = ({ task, disabled, onSend }) => { const TaskThreadCard = ({ task, disabled, onSend }) => {
const [channel, setChannel] = useState('external'); // external | internal const [channel, setChannel] = useState('group'); // group | direct
const [draft, setDraft] = useState(''); const [draft, setDraft] = useState('');
const scrollerRef = useRef(null); const scrollerRef = useRef(null);
const supervisorName = task.assignedByName || 'your supervisor';
const messages = useMemo( const messages = useMemo(
() => (task.thread || []).filter(m => m.channel === channel), () => (task.thread || []).filter(m => m.channel === channel),
[task.thread, channel], [task.thread, channel],
@@ -872,18 +874,22 @@ const TaskThreadCard = ({ task, disabled, onSend }) => {
setDraft(''); setDraft('');
}; };
const isDirect = channel === 'direct';
return ( return (
<SpotlightCard className="p-5 sm:p-6"> <SpotlightCard className="p-5 sm:p-6">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3"> <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
<SectionHeading <SectionHeading
icon={MessageSquare} icon={MessageSquare}
title="Task Thread" title="Task Thread"
hint="Stay in sync with the admin and homeowner. Use Internal for notes the homeowner shouldn't see." hint={isDirect
? `Private 1:1 chat with your supervisor (${supervisorName}). Only the two of you see these messages.`
: 'Team chat for everyone working on this project — admin, owner / project lead, and you. The homeowner is not in this thread.'}
inline inline
/> />
<div className="inline-flex rounded-xl bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 p-1 shrink-0"> <div className="inline-flex rounded-xl bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 p-1 shrink-0">
<ChannelTab active={channel === 'external'} onClick={() => setChannel('external')} icon={MessageSquare} label="External" /> <ChannelTab active={channel === 'group'} onClick={() => setChannel('group')} icon={Users} label="Team" />
<ChannelTab active={channel === 'internal'} onClick={() => setChannel('internal')} icon={Lock} label="Internal" /> <ChannelTab active={channel === 'direct'} onClick={() => setChannel('direct')} icon={UserCheck} label="Direct" />
</div> </div>
</div> </div>
@@ -893,8 +899,14 @@ const TaskThreadCard = ({ task, disabled, onSend }) => {
> >
{messages.length === 0 ? ( {messages.length === 0 ? (
<div className="text-center py-10 text-zinc-500"> <div className="text-center py-10 text-zinc-500">
<MessageSquare size={20} className="mx-auto mb-2 opacity-50" /> {isDirect
<p className="text-sm">No {channel === 'internal' ? 'internal notes' : 'messages'} yet.</p> ? <UserCheck size={20} className="mx-auto mb-2 opacity-50" />
: <Users size={20} className="mx-auto mb-2 opacity-50" />}
<p className="text-sm">
{isDirect
? `No direct messages with ${supervisorName} yet.`
: 'No team messages yet.'}
</p>
</div> </div>
) : messages.map(m => ( ) : messages.map(m => (
<MessageBubble key={m.id} message={m} channel={channel} /> <MessageBubble key={m.id} message={m} channel={channel} />
@@ -903,31 +915,38 @@ const TaskThreadCard = ({ task, disabled, onSend }) => {
<form onSubmit={submit} className="mt-4 pt-4 border-t border-zinc-200 dark:border-white/10"> <form onSubmit={submit} className="mt-4 pt-4 border-t border-zinc-200 dark:border-white/10">
<div className={`flex items-center gap-2 rounded-xl border bg-zinc-50 dark:bg-white/5 px-3 py-2 <div className={`flex items-center gap-2 rounded-xl border bg-zinc-50 dark:bg-white/5 px-3 py-2
${channel === 'internal' ${isDirect
? 'border-amber-300 dark:border-amber-500/30' ? 'border-indigo-300 dark:border-indigo-500/30'
: 'border-zinc-200 dark:border-white/10' : 'border-zinc-200 dark:border-white/10'
}`}> }`}>
{channel === 'internal' && <Lock size={14} className="text-amber-500 shrink-0" />} {isDirect && <UserCheck size={14} className="text-indigo-500 shrink-0" />}
<input <input
type="text" type="text"
value={draft} value={draft}
onChange={(e) => setDraft(e.target.value)} onChange={(e) => setDraft(e.target.value)}
disabled={disabled} 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" className="flex-1 bg-transparent border-0 outline-none text-sm placeholder:text-zinc-400 disabled:opacity-50"
/> />
<button <button
type="submit" type="submit"
disabled={!draft.trim() || disabled} disabled={!draft.trim() || disabled}
className="p-2 rounded-lg bg-blue-600 hover:bg-blue-500 text-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors" className={`p-2 rounded-lg text-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors
${isDirect ? 'bg-indigo-600 hover:bg-indigo-500' : 'bg-blue-600 hover:bg-blue-500'}`}
aria-label="Send message" aria-label="Send message"
> >
<Send size={14} /> <Send size={14} />
</button> </button>
</div> </div>
{channel === 'internal' && ( {isDirect ? (
<p className="mt-2 text-[11px] text-amber-600 dark:text-amber-400 flex items-center gap-1.5"> <p className="mt-2 text-[11px] text-indigo-600 dark:text-indigo-400 flex items-center gap-1.5">
<Lock size={10} /> Internal notes are visible to your company team only not the homeowner. <Lock size={10} /> Direct messages are private between you and {supervisorName}. The rest of the team cannot see them.
</p>
) : (
<p className="mt-2 text-[11px] text-zinc-500 flex items-center gap-1.5">
<Users size={10} /> Visible to everyone on the project team. The homeowner is not part of this chat.
</p> </p>
)} )}
</form> </form>
@@ -952,13 +971,18 @@ const ChannelTab = ({ active, onClick, icon: Icon, label }) => (
const MessageBubble = ({ message, channel }) => { const MessageBubble = ({ message, channel }) => {
const mine = !!message.mine; const mine = !!message.mine;
const bubbleBase = channel === 'internal' const isDirect = channel === 'direct';
? 'bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20' const bubbleBase = isDirect
: (mine ? 'bg-blue-600 text-white' : 'bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10'); ? (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 const meta = mine
? 'text-blue-100/80' ? (isDirect ? 'text-indigo-100/80' : 'text-blue-100/80')
: channel === 'internal' : isDirect
? 'text-amber-700 dark:text-amber-400' ? 'text-indigo-700 dark:text-indigo-400'
: 'text-zinc-500'; : 'text-zinc-500';
return ( return (
@@ -966,14 +990,14 @@ const MessageBubble = ({ message, channel }) => {
<div className="flex items-end gap-2 max-w-[88%]"> <div className="flex items-end gap-2 max-w-[88%]">
{!mine && ( {!mine && (
<div className={`w-7 h-7 rounded-full text-[11px] font-bold flex items-center justify-center shrink-0 <div className={`w-7 h-7 rounded-full text-[11px] font-bold flex items-center justify-center shrink-0
${channel === 'internal' ${isDirect
? 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400' ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-400'
: 'bg-blue-100 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400' : 'bg-blue-100 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400'
}`}> }`}>
{message.senderName?.charAt(0)?.toUpperCase() || '?'} {message.senderName?.charAt(0)?.toUpperCase() || '?'}
</div> </div>
)} )}
<div className={`rounded-2xl px-3 py-2 ${bubbleBase} ${channel === 'internal' ? 'text-zinc-900 dark:text-amber-50' : ''}`}> <div className={`rounded-2xl px-3 py-2 ${bubbleBase}`}>
{!mine && ( {!mine && (
<div className={`text-[10px] font-bold uppercase tracking-wider ${meta} mb-0.5`}> <div className={`text-[10px] font-bold uppercase tracking-wider ${meta} mb-0.5`}>
{message.senderName} · {message.senderRole} {message.senderName} · {message.senderRole}