diff --git a/src/components/client/ChatSection.tsx b/src/components/client/ChatSection.tsx
new file mode 100644
index 0000000..d1c3664
--- /dev/null
+++ b/src/components/client/ChatSection.tsx
@@ -0,0 +1,213 @@
+"use client";
+
+import { useState, useTransition, useRef, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import type { ClientView } from "@/lib/client-view";
+import type { Comment } from "@/db/schema";
+
+type Entity = { id: string; type: "general" | "task" | "deliverable"; label: string };
+
+function buildEntityList(clientId: string, phases: ClientView["phases"]): Entity[] {
+ const entities: Entity[] = [
+ { id: clientId, type: "general", label: "Messaggio generale" },
+ ];
+ for (const phase of phases) {
+ for (const task of phase.tasks) {
+ entities.push({
+ id: task.id,
+ type: "task",
+ label: `${phase.title} — ${task.title}`,
+ });
+ for (const d of task.deliverables) {
+ entities.push({
+ id: d.id,
+ type: "deliverable",
+ label: `${task.title} — ${d.title}`,
+ });
+ }
+ }
+ }
+ return entities;
+}
+
+function buildLabelMap(entities: Entity[]): Map {
+ const map = new Map();
+ for (const e of entities) map.set(e.id, e.label);
+ return map;
+}
+
+function formatTime(ts: Date | string): string {
+ const d = typeof ts === "string" ? new Date(ts) : ts;
+ return d.toLocaleString("it-IT", {
+ day: "2-digit",
+ month: "short",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+}
+
+export function ChatSection({
+ clientId,
+ phases,
+ token,
+ comments,
+}: {
+ clientId: string;
+ phases: ClientView["phases"];
+ token: string;
+ comments: Comment[];
+}) {
+ const entities = buildEntityList(clientId, phases);
+ const labelMap = buildLabelMap(entities);
+
+ const [body, setBody] = useState("");
+ const [selectedEntityId, setSelectedEntityId] = useState(clientId);
+ const [error, setError] = useState(null);
+ const [, startTransition] = useTransition();
+ const router = useRouter();
+ const bottomRef = useRef(null);
+
+ // Sort all comments chronologically
+ const sorted = [...comments].sort(
+ (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
+ );
+
+ useEffect(() => {
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [comments.length]);
+
+ function handleSend(e: React.FormEvent) {
+ e.preventDefault();
+ const trimmed = body.trim();
+ if (!trimmed) return;
+
+ const selectedEntity = entities.find((en) => en.id === selectedEntityId);
+ if (!selectedEntity) return;
+
+ setError(null);
+ startTransition(async () => {
+ try {
+ const res = await fetch("/api/client/comment", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ token,
+ entity_type: selectedEntity.type,
+ entity_id: selectedEntity.id,
+ body: trimmed,
+ }),
+ });
+ if (!res.ok) {
+ const data = await res.json();
+ setError(data.error ?? "Errore nell'invio");
+ return;
+ }
+ setBody("");
+ router.refresh();
+ } catch {
+ setError("Errore di rete");
+ }
+ });
+ }
+
+ return (
+
+ {/* Chat feed */}
+
+ {sorted.length === 0 && (
+
+ Nessun messaggio ancora. Scrivi qui sotto per iniziare.
+
+ )}
+ {sorted.map((c) => {
+ const isClient = c.author === "client";
+ const entityLabel = labelMap.get(c.entity_id);
+ const showTag = c.entity_id !== clientId && entityLabel;
+
+ return (
+
+ {/* Author + tag */}
+
+
+ {isClient ? "Tu" : "iamcavalli"}
+
+ {showTag && (
+
+ {entityLabel}
+
+ )}
+
+
+ {/* Bubble */}
+
+ {c.body}
+
+
+ {/* Timestamp */}
+
{formatTime(c.created_at)}
+
+ );
+ })}
+
+
+
+ {/* Divider */}
+
+
+ {/* Input area */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/client/kanban/ClientKanban.tsx b/src/components/client/kanban/ClientKanban.tsx
index d8e5114..f2b2436 100644
--- a/src/components/client/kanban/ClientKanban.tsx
+++ b/src/components/client/kanban/ClientKanban.tsx
@@ -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 (
-
-
-
- {expanded && (
-
- {task.description && (
-
{task.description}
- )}
-
- {hasDeliverables && (
-
- {task.deliverables.map((d) => {
- const delivComments = comments.filter((c) => c.entity_id === d.id);
- return (
-
-
-
- {d.title}
-
- {(d.status === "pending" ||
- d.status === "submitted" ||
- d.approved_at !== null) && (
-
- )}
-
-
-
-
- );
- })}
-
- )}
-
-
-
-
+
+
+ {task.phaseTitle}
+
+
+ {task.title}
+
+ {task.description && (
+
{task.description}
+ )}
+ {task.deliverables.length > 0 && (
+
+ {task.deliverables.map((d) => (
+ -
+ {d.title}
+ {(d.status === "pending" || d.status === "submitted" || d.approved_at !== null) && (
+
+ )}
+
+ ))}
+
)}
);
}
-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 (
{COLUMNS.map((col) => (
-
-
+
+
-
- {col.label}
-
+ {col.label}
{tasksByStatus[col.id].length}
-
{tasksByStatus[col.id].map((task) => (
-
+
))}
{tasksByStatus[col.id].length === 0 && (
-
- Nessun task
-
+
Nessun task
)}
diff --git a/src/components/client/kanban/PhaseViewToggle.tsx b/src/components/client/kanban/PhaseViewToggle.tsx
index 9c04b24..788941d 100644
--- a/src/components/client/kanban/PhaseViewToggle.tsx
+++ b/src/components/client/kanban/PhaseViewToggle.tsx
@@ -3,18 +3,15 @@
import { useState, type ReactNode } from "react";
import { ClientKanban } from "./ClientKanban";
import type { ClientView } from "@/lib/client-view";
-import type { Comment } from "@/db/schema";
export function PhaseViewToggle({
timelineView,
phases,
token,
- comments,
}: {
timelineView: ReactNode;
phases: ClientView["phases"];
token: string;
- comments: Comment[];
}) {
const [view, setView] = useState<"timeline" | "kanban">("timeline");
@@ -26,9 +23,7 @@ export function PhaseViewToggle({
- {view === "timeline" ? (
- timelineView
- ) : (
-
- )}
+ {view === "timeline" ? timelineView :
}
);
}
\ No newline at end of file
diff --git a/src/components/phase-timeline.tsx b/src/components/phase-timeline.tsx
index c264aa0..f563a82 100644
--- a/src/components/phase-timeline.tsx
+++ b/src/components/phase-timeline.tsx
@@ -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 (
-