subcontractor: task thread section having teams and direct chat options now
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user