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' },
],
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. 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_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. 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: '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;
@@ -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 (
<SpotlightCard className="p-5 sm:p-6">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
<SectionHeading
icon={MessageSquare}
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
/>
<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 === 'internal'} onClick={() => setChannel('internal')} icon={Lock} label="Internal" />
<ChannelTab active={channel === 'group'} onClick={() => setChannel('group')} icon={Users} label="Team" />
<ChannelTab active={channel === 'direct'} onClick={() => setChannel('direct')} icon={UserCheck} label="Direct" />
</div>
</div>
@@ -893,8 +899,14 @@ const TaskThreadCard = ({ task, disabled, onSend }) => {
>
{messages.length === 0 ? (
<div className="text-center py-10 text-zinc-500">
<MessageSquare size={20} className="mx-auto mb-2 opacity-50" />
<p className="text-sm">No {channel === 'internal' ? 'internal notes' : 'messages'} yet.</p>
{isDirect
? <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>
) : messages.map(m => (
<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">
<div className={`flex items-center gap-2 rounded-xl border bg-zinc-50 dark:bg-white/5 px-3 py-2
${channel === 'internal'
? 'border-amber-300 dark:border-amber-500/30'
${isDirect
? 'border-indigo-300 dark:border-indigo-500/30'
: '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
type="text"
value={draft}
onChange={(e) => 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"
/>
<button
type="submit"
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"
>
<Send size={14} />
</button>
</div>
{channel === 'internal' && (
<p className="mt-2 text-[11px] text-amber-600 dark:text-amber-400 flex items-center gap-1.5">
<Lock size={10} /> Internal notes are visible to your company team only not the homeowner.
{isDirect ? (
<p className="mt-2 text-[11px] text-indigo-600 dark:text-indigo-400 flex items-center gap-1.5">
<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>
)}
</form>
@@ -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 }) => {
<div className="flex items-end gap-2 max-w-[88%]">
{!mine && (
<div className={`w-7 h-7 rounded-full text-[11px] font-bold flex items-center justify-center shrink-0
${channel === 'internal'
? 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400'
${isDirect
? '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'
}`}>
{message.senderName?.charAt(0)?.toUpperCase() || '?'}
</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 && (
<div className={`text-[10px] font-bold uppercase tracking-wider ${meta} mb-0.5`}>
{message.senderName} · {message.senderRole}