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,185 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { ClientView } from "@/lib/client-view";
|
||||
import type { Comment } from "@/db/schema";
|
||||
import { ApproveButton } from "@/components/client/ApproveButton";
|
||||
import { CommentList } from "@/components/client/CommentList";
|
||||
import { CommentForm } from "@/components/client/CommentForm";
|
||||
|
||||
type Task = ClientView["phases"][number]["tasks"][number] & {
|
||||
phaseTitle: string;
|
||||
};
|
||||
|
||||
const COLUMNS: { id: "todo" | "in_progress" | "done"; label: string; dotClass: string; headerClass: string }[] = [
|
||||
{
|
||||
id: "todo",
|
||||
label: "Da fare",
|
||||
dotClass: "bg-[#d4d4d8]",
|
||||
headerClass: "text-[#71717a]",
|
||||
},
|
||||
{
|
||||
id: "in_progress",
|
||||
label: "In corso",
|
||||
dotClass: "bg-[#DEF168]",
|
||||
headerClass: "text-[#1A463C]",
|
||||
},
|
||||
{
|
||||
id: "done",
|
||||
label: "Fatto",
|
||||
dotClass: "bg-[#1A463C]",
|
||||
headerClass: "text-[#1A463C]",
|
||||
},
|
||||
{ id: "todo", label: "Da fare", dotClass: "bg-[#d4d4d8]", headerClass: "text-[#71717a]" },
|
||||
{ id: "in_progress", label: "In corso", dotClass: "bg-[#DEF168]", headerClass: "text-[#1A463C]" },
|
||||
{ id: "done", label: "Fatto", dotClass: "bg-[#1A463C]", headerClass: "text-[#1A463C]" },
|
||||
];
|
||||
|
||||
function TaskCard({
|
||||
task,
|
||||
token,
|
||||
comments,
|
||||
}: {
|
||||
task: Task;
|
||||
token: string;
|
||||
comments: Comment[];
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const taskComments = comments.filter((c) => c.entity_id === task.id);
|
||||
const hasDeliverables = task.deliverables.length > 0;
|
||||
const hasActivity = taskComments.length > 0 || hasDeliverables;
|
||||
|
||||
function TaskCard({ task, token }: { task: Task; token: string }) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg border border-[#e5e7eb] overflow-hidden shadow-sm">
|
||||
<button
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="w-full text-left px-3 py-2.5 hover:bg-[#f9f9f9] transition-colors"
|
||||
>
|
||||
<p className="text-[10px] font-medium text-[#71717a] uppercase tracking-wide mb-1 truncate">
|
||||
{task.phaseTitle}
|
||||
</p>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p
|
||||
className={`text-sm font-medium leading-snug ${
|
||||
task.status === "done"
|
||||
? "line-through text-[#71717a]"
|
||||
: "text-[#1a1a1a]"
|
||||
}`}
|
||||
>
|
||||
{task.title}
|
||||
</p>
|
||||
{hasActivity && (
|
||||
<span className="text-[10px] text-[#71717a] mt-0.5 shrink-0">
|
||||
{expanded ? "▲" : "▼"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{task.description && !expanded && (
|
||||
<p className="text-xs text-[#71717a] mt-0.5 leading-snug line-clamp-2">
|
||||
{task.description}
|
||||
</p>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="border-t border-[#f4f4f5] px-3 pb-3 pt-2 space-y-3">
|
||||
{task.description && (
|
||||
<p className="text-xs text-[#71717a] leading-snug">{task.description}</p>
|
||||
)}
|
||||
|
||||
{hasDeliverables && (
|
||||
<div className="space-y-2">
|
||||
{task.deliverables.map((d) => {
|
||||
const delivComments = comments.filter((c) => c.entity_id === d.id);
|
||||
return (
|
||||
<div
|
||||
key={d.id}
|
||||
className="bg-[#f9f9f9] rounded-lg px-3 py-2 space-y-2"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs font-medium text-[#1a1a1a] truncate">
|
||||
{d.title}
|
||||
</span>
|
||||
{(d.status === "pending" ||
|
||||
d.status === "submitted" ||
|
||||
d.approved_at !== null) && (
|
||||
<ApproveButton
|
||||
deliverableId={d.id}
|
||||
token={token}
|
||||
approvedAt={d.approved_at}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<CommentList comments={delivComments} />
|
||||
<CommentForm
|
||||
token={token}
|
||||
entityType="deliverable"
|
||||
entityId={d.id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CommentList comments={taskComments} />
|
||||
<CommentForm token={token} entityType="task" entityId={task.id} />
|
||||
</div>
|
||||
<div className="bg-white rounded-lg border border-[#e5e7eb] px-3 py-2.5 shadow-sm">
|
||||
<p className="text-[10px] font-medium text-[#71717a] uppercase tracking-wide mb-1 truncate">
|
||||
{task.phaseTitle}
|
||||
</p>
|
||||
<p className={`text-sm font-medium leading-snug mb-2 ${task.status === "done" ? "line-through text-[#71717a]" : "text-[#1a1a1a]"}`}>
|
||||
{task.title}
|
||||
</p>
|
||||
{task.description && (
|
||||
<p className="text-xs text-[#71717a] mb-2 leading-snug">{task.description}</p>
|
||||
)}
|
||||
{task.deliverables.length > 0 && (
|
||||
<ul className="space-y-1.5 mt-2 pt-2 border-t border-[#f4f4f5]">
|
||||
{task.deliverables.map((d) => (
|
||||
<li key={d.id} className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs text-[#666666] truncate">{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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ClientKanban({
|
||||
phases,
|
||||
token,
|
||||
comments,
|
||||
}: {
|
||||
phases: ClientView["phases"];
|
||||
token: string;
|
||||
comments: Comment[];
|
||||
}) {
|
||||
export function ClientKanban({ phases, token }: { phases: ClientView["phases"]; token: string }) {
|
||||
const allTasks: Task[] = phases.flatMap((phase) =>
|
||||
phase.tasks.map((task) => ({ ...task, phaseTitle: phase.title }))
|
||||
);
|
||||
|
||||
const tasksByStatus = {
|
||||
todo: allTasks.filter((t) => t.status === "todo"),
|
||||
todo: allTasks.filter((t) => t.status === "todo"),
|
||||
in_progress: allTasks.filter((t) => t.status === "in_progress"),
|
||||
done: allTasks.filter((t) => t.status === "done"),
|
||||
done: allTasks.filter((t) => t.status === "done"),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
{COLUMNS.map((col) => (
|
||||
<div
|
||||
key={col.id}
|
||||
className="flex flex-col rounded-xl border border-[#e5e7eb] bg-[#f9f9f9] min-h-[200px]"
|
||||
>
|
||||
<div
|
||||
className={`px-4 py-3 flex items-center justify-between ${col.headerClass}`}
|
||||
>
|
||||
<div key={col.id} className="flex flex-col rounded-xl border border-[#e5e7eb] bg-[#f9f9f9] min-h-[200px]">
|
||||
<div className={`px-4 py-3 flex items-center justify-between ${col.headerClass}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${col.dotClass}`} />
|
||||
<span className="text-xs font-bold uppercase tracking-wider">
|
||||
{col.label}
|
||||
</span>
|
||||
<span className="text-xs font-bold uppercase tracking-wider">{col.label}</span>
|
||||
</div>
|
||||
<span className="text-xs font-semibold tabular-nums bg-white rounded-full px-2 py-0.5 border border-[#e5e7eb]">
|
||||
{tasksByStatus[col.id].length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-3 space-y-2">
|
||||
{tasksByStatus[col.id].map((task) => (
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
token={token}
|
||||
comments={comments}
|
||||
/>
|
||||
<TaskCard key={task.id} task={task} token={token} />
|
||||
))}
|
||||
{tasksByStatus[col.id].length === 0 && (
|
||||
<p className="text-xs text-[#d4d4d8] italic text-center py-10">
|
||||
Nessun task
|
||||
</p>
|
||||
<p className="text-xs text-[#d4d4d8] italic text-center py-10">Nessun task</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user