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
@@ -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}