feat: unify client messaging into ChatSection + add general comment type
Removed per-task/deliverable CommentList/CommentForm from PhaseTimeline and ClientKanban. Replaced with a single ChatSection at the bottom of the dashboard that handles general, task, and deliverable messages in a unified chat UI. Added "general" entity_type to the comment API (entity_id = client UUID). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,60 +1,31 @@
|
||||
import type { ClientView } from '@/lib/client-view';
|
||||
import type { Comment } from '@/db/schema';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ApproveButton } from './client/ApproveButton';
|
||||
import { CommentList } from './client/CommentList';
|
||||
import { CommentForm } from './client/CommentForm';
|
||||
|
||||
interface PhaseTimelineProps {
|
||||
phases: ClientView['phases'];
|
||||
token: string;
|
||||
comments: Comment[];
|
||||
}
|
||||
|
||||
function PhaseStatusIcon({ status }: { status: 'upcoming' | 'active' | 'done' }) {
|
||||
if (status === 'done') {
|
||||
return (
|
||||
<svg
|
||||
className="w-5 h-5 text-[#16a34a]"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<svg className="w-5 h-5 text-[#16a34a]" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clipRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (status === 'active') {
|
||||
return (
|
||||
<svg
|
||||
className="w-5 h-5 text-[#0066cc]"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<svg className="w-5 h-5 text-[#1A463C]" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16z" clipRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
className="w-5 h-5 text-[#999999]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg className="w-5 h-5 text-[#999999]" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx={12} cy={12} r={9} />
|
||||
</svg>
|
||||
);
|
||||
@@ -63,45 +34,20 @@ function PhaseStatusIcon({ status }: { status: 'upcoming' | 'active' | 'done' })
|
||||
function TaskStatusIcon({ status }: { status: 'todo' | 'in_progress' | 'done' }) {
|
||||
if (status === 'done') {
|
||||
return (
|
||||
<svg
|
||||
className="w-4 h-4 text-[#16a34a] shrink-0 mt-0.5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<svg className="w-4 h-4 text-[#16a34a] shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clipRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (status === 'in_progress') {
|
||||
return (
|
||||
<svg
|
||||
className="w-4 h-4 text-[#ca8a04] shrink-0 mt-0.5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<svg className="w-4 h-4 text-[#DEF168] shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16z" clipRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
className="w-4 h-4 text-[#999999] shrink-0 mt-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg className="w-4 h-4 text-[#999999] shrink-0 mt-0.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx={12} cy={12} r={9} />
|
||||
</svg>
|
||||
);
|
||||
@@ -115,23 +61,15 @@ const phaseStatusLabel: Record<'upcoming' | 'active' | 'done', string> = {
|
||||
|
||||
const phaseStatusStyle: Record<'upcoming' | 'active' | 'done', string> = {
|
||||
upcoming: 'border-transparent bg-[#999999] text-white',
|
||||
active: 'border-transparent bg-[#0066cc] text-white',
|
||||
active: 'border-transparent bg-[#1A463C] text-white',
|
||||
done: 'border-transparent bg-[#16a34a] text-white',
|
||||
};
|
||||
|
||||
export function PhaseTimeline({ phases, token, comments }: PhaseTimelineProps) {
|
||||
export function PhaseTimeline({ phases, token }: PhaseTimelineProps) {
|
||||
if (phases.length === 0) {
|
||||
return (
|
||||
<p className="text-sm text-[#999999] italic">
|
||||
Nessuna fase ancora configurata.
|
||||
</p>
|
||||
);
|
||||
return <p className="text-sm text-[#999999] italic">Nessuna fase ancora configurata.</p>;
|
||||
}
|
||||
|
||||
// Helper: filter pre-fetched comments by entity id
|
||||
const commentsFor = (entityId: string) =>
|
||||
comments.filter((c) => c.entity_id === entityId);
|
||||
|
||||
return (
|
||||
<div className="space-y-0">
|
||||
{phases.map((phase, index) => {
|
||||
@@ -140,110 +78,56 @@ export function PhaseTimeline({ phases, token, comments }: PhaseTimelineProps) {
|
||||
|
||||
return (
|
||||
<div key={phase.id} className="flex gap-5">
|
||||
{/* Colonna sinistra: indicatore timeline */}
|
||||
<div className="flex flex-col items-center">
|
||||
{/* Cerchio con icona stato */}
|
||||
<div className="w-10 h-10 rounded-full bg-white border-2 border-[#e5e5e5] flex items-center justify-center shrink-0 z-10">
|
||||
<PhaseStatusIcon status={phase.status} />
|
||||
</div>
|
||||
{/* Linea verticale verso la fase successiva */}
|
||||
{!isLast && (
|
||||
<div className="flex-1 w-px bg-[#e5e5e5] my-2" style={{ minHeight: '2rem' }} />
|
||||
)}
|
||||
{!isLast && <div className="flex-1 w-px bg-[#e5e5e5] my-2" style={{ minHeight: '2rem' }} />}
|
||||
</div>
|
||||
|
||||
{/* Colonna destra: contenuto fase */}
|
||||
<div className={`flex-1 ${isLast ? 'pb-0' : 'pb-6'}`}>
|
||||
<Card className="rounded-lg border border-[#e5e5e5] bg-white shadow-none p-5">
|
||||
{/* Header fase */}
|
||||
<div className="flex items-start justify-between gap-3 mb-4">
|
||||
<h3 className="text-base font-bold text-[#1a1a1a] leading-snug">
|
||||
{phase.title}
|
||||
</h3>
|
||||
<h3 className="text-base font-bold text-[#1a1a1a] leading-snug">{phase.title}</h3>
|
||||
<Badge className={`text-xs shrink-0 ${phaseStatusStyle[phase.status]}`}>
|
||||
{phaseStatusLabel[phase.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Barra progresso fase (D-08) */}
|
||||
<div className="mb-5">
|
||||
<div className="flex justify-between items-center mb-1.5">
|
||||
<p className="text-xs text-[#666666] font-medium">
|
||||
{doneCount} di {phase.tasks.length} task
|
||||
</p>
|
||||
<p className="text-xs font-semibold text-[#1a1a1a]">
|
||||
{phase.progress_pct}%
|
||||
</p>
|
||||
<p className="text-xs text-[#666666] font-medium">{doneCount} di {phase.tasks.length} task</p>
|
||||
<p className="text-xs font-semibold text-[#1a1a1a]">{phase.progress_pct}%</p>
|
||||
</div>
|
||||
<Progress value={phase.progress_pct} className="h-1.5" />
|
||||
</div>
|
||||
|
||||
{/* Lista task */}
|
||||
{phase.tasks.length === 0 ? (
|
||||
<p className="text-xs text-[#999999] italic">
|
||||
Nessun task ancora configurato.
|
||||
</p>
|
||||
<p className="text-xs text-[#999999] italic">Nessun task ancora configurato.</p>
|
||||
) : (
|
||||
<ul className="space-y-4">
|
||||
<ul className="space-y-3">
|
||||
{phase.tasks.map((task) => (
|
||||
<li key={task.id} className="flex items-start gap-2.5">
|
||||
<TaskStatusIcon status={task.status} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p
|
||||
className={`text-sm leading-snug ${
|
||||
task.status === 'done'
|
||||
? 'line-through text-[#999999]'
|
||||
: 'text-[#1a1a1a]'
|
||||
}`}
|
||||
>
|
||||
<p className={`text-sm leading-snug ${task.status === 'done' ? 'line-through text-[#999999]' : 'text-[#1a1a1a]'}`}>
|
||||
{task.title}
|
||||
</p>
|
||||
{task.description && (
|
||||
<p className="text-xs text-[#999999] mt-0.5 leading-snug">
|
||||
{task.description}
|
||||
</p>
|
||||
<p className="text-xs text-[#999999] mt-0.5 leading-snug">{task.description}</p>
|
||||
)}
|
||||
|
||||
{/* Deliverable annidati con ApproveButton + CommentiDeliverable */}
|
||||
{task.deliverables.length > 0 && (
|
||||
<ul className="mt-1.5 space-y-3">
|
||||
<ul className="mt-1.5 space-y-2">
|
||||
{task.deliverables.map((d) => (
|
||||
<li
|
||||
key={d.id}
|
||||
className="bg-[#f9f9f9] rounded px-3 py-2"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 mb-2">
|
||||
<span className="text-xs text-[#666666] truncate font-medium">
|
||||
{d.title}
|
||||
</span>
|
||||
{/* ApproveButton: shown for pending/submitted; shows date badge once approved */}
|
||||
{(d.status === 'pending' || d.status === 'submitted' || d.approved_at !== null) && (
|
||||
<ApproveButton
|
||||
deliverableId={d.id}
|
||||
token={token}
|
||||
approvedAt={d.approved_at}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* Comments on this deliverable */}
|
||||
<CommentList comments={commentsFor(d.id)} />
|
||||
<CommentForm
|
||||
token={token}
|
||||
entityType="deliverable"
|
||||
entityId={d.id}
|
||||
/>
|
||||
<li key={d.id} className="bg-[#f9f9f9] rounded px-3 py-2 flex items-center justify-between gap-2">
|
||||
<span className="text-xs text-[#666666] truncate font-medium">{d.title}</span>
|
||||
{(d.status === 'pending' || d.status === 'submitted' || d.approved_at !== null) && (
|
||||
<ApproveButton deliverableId={d.id} token={token} approvedAt={d.approved_at} />
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* Comments on the task itself */}
|
||||
<CommentList comments={commentsFor(task.id)} />
|
||||
<CommentForm
|
||||
token={token}
|
||||
entityType="task"
|
||||
entityId={task.id}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user