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:
Simone Cavalli
2026-05-16 12:49:14 +02:00
parent c467ef300b
commit 549cf0b592
7 changed files with 307 additions and 348 deletions
+10 -19
View File
@@ -6,7 +6,7 @@ import { clients, comments, tasks, phases, deliverables } from "@/db/schema";
const commentSchema = z.object({
token: z.string().min(1),
entity_type: z.enum(["task", "deliverable"]),
entity_type: z.enum(["task", "deliverable", "general"]),
entity_id: z.string().min(1),
body: z.string().min(1, "Il commento non può essere vuoto").max(2000),
});
@@ -38,59 +38,50 @@ export async function POST(request: NextRequest) {
const clientId = clientRows[0].id;
// Verify entity belongs to this client (prevent cross-client comment injection)
if (entity_type === "task") {
if (entity_type === "general") {
// General messages: entity_id must be the client's own id
if (entity_id !== clientId) {
return NextResponse.json({ error: "Entity non valida" }, { status: 403 });
}
} else if (entity_type === "task") {
const phasesForClient = await db
.select({ id: phases.id })
.from(phases)
.where(eq(phases.client_id, clientId));
const phaseIds = phasesForClient.map((p) => p.id);
if (phaseIds.length === 0) {
return NextResponse.json({ error: "Nessuna fase trovata" }, { status: 404 });
}
const taskRows = await db
.select({ id: tasks.id })
.from(tasks)
.where(inArray(tasks.phase_id, phaseIds));
const taskCheck = taskRows.find((r) => r.id === entity_id);
if (!taskCheck) {
if (!taskRows.find((r) => r.id === entity_id)) {
return NextResponse.json({ error: "Task non trovato" }, { status: 404 });
}
} else {
// deliverable — verify via task → phase → client chain
// deliverable
const phasesForClient = await db
.select({ id: phases.id })
.from(phases)
.where(eq(phases.client_id, clientId));
const phaseIds = phasesForClient.map((p) => p.id);
if (phaseIds.length === 0) {
return NextResponse.json({ error: "Nessuna fase trovata" }, { status: 404 });
}
const taskRows = await db
.select({ id: tasks.id })
.from(tasks)
.where(inArray(tasks.phase_id, phaseIds));
const taskIds = taskRows.map((r) => r.id);
if (taskIds.length === 0) {
return NextResponse.json({ error: "Nessun task trovato" }, { status: 404 });
}
const delivRows = await db
.select({ id: deliverables.id })
.from(deliverables)
.where(inArray(deliverables.task_id, taskIds));
const delivCheck = delivRows.find((r) => r.id === entity_id);
if (!delivCheck) {
if (!delivRows.find((r) => r.id === entity_id)) {
return NextResponse.json({ error: "Deliverable non trovato" }, { status: 404 });
}
}
+2 -2
View File
@@ -42,12 +42,12 @@ export default async function ClientPage({
notFound();
}
// Fetch comments for all tasks and deliverables in this client's data
// Fetch comments: tasks, deliverables, and general (entity_id = clientId)
const allTaskIds = view.phases.flatMap((p) => p.tasks.map((t) => t.id));
const allDeliverableIds = view.phases.flatMap((p) =>
p.tasks.flatMap((t) => t.deliverables.map((d) => d.id))
);
const allEntityIds = [...allTaskIds, ...allDeliverableIds];
const allEntityIds = [view.client.id, ...allTaskIds, ...allDeliverableIds];
const allComments: Comment[] =
allEntityIds.length > 0