From 4b135ce67f288ee4f62081a4dc39f23c5b3676ae Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Wed, 19 Aug 2026 22:30:58 +0200 Subject: [PATCH] refactor(progetti): via il tab Commenti e il timer dalla lista MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Due rimozioni chieste esplicitamente, che tolgono due doppioni. Il tab "Commenti" del progetto duplicava /admin/conversazioni. Non era una scorciatoia: era la seconda copia. `buildEntityMap()` in conversations-queries cammina clienti -> progetti -> fasi -> task -> deliverable e raccoglie TUTTI i commenti con l'etichetta dell'entita' di origine, quindi l'inbox e' un sovrainsieme stretto di quel tab. La lettura non perde niente. Una cosa la perde, e va detta: dal tab si poteva rispondere sulla singola entita', mentre `replyToConversation` salva sempre sul thread generale. Non e' una regressione introdotta qui — e' una scelta di prodotto gia' presa e gia' annotata in conversazioni/actions.ts — ma da oggi e' l'unica via, e il commento la' sopra ora lo dice. Il timer nella lista progetti era l'altro doppione: si avvia e si ferma dentro il progetto, dove c'e' il contesto per sapere su cosa stai lavorando. Toglierlo elimina anche una query per pagina (la scansione delle entry aperte). Cade di conseguenza il codice rimasto senza chiamanti: CommentsTab.tsx, `postAdminComment`, il campo `comments` di ProjectFullDetail con la sua query, e i due campi activeTimer* di ProjectWithPayments. `totalTrackedSeconds` resta: serve al calcolo del EUR/h, che in lista ci sta ancora. Build e lint puliti. Co-Authored-By: Claude Opus 5 --- src/app/admin/clients/[id]/actions.ts | 21 +---- src/app/admin/conversazioni/actions.ts | 6 +- src/app/admin/projects/[id]/page.tsx | 7 -- src/app/admin/projects/page.tsx | 1 - src/components/admin/ProjectRow.tsx | 13 --- src/components/admin/tabs/CommentsTab.tsx | 109 ---------------------- src/lib/admin-queries.ts | 32 +------ 7 files changed, 15 insertions(+), 174 deletions(-) delete mode 100644 src/components/admin/tabs/CommentsTab.tsx diff --git a/src/app/admin/clients/[id]/actions.ts b/src/app/admin/clients/[id]/actions.ts index 413abe3..3506fd2 100644 --- a/src/app/admin/clients/[id]/actions.ts +++ b/src/app/admin/clients/[id]/actions.ts @@ -18,7 +18,6 @@ import { payments, clients, projects, - comments, client_emails, otp_codes, } from "@/db/schema"; @@ -368,21 +367,11 @@ export async function updateAcceptedTotal(id: string, formData: FormData) { } } -// ── COMMENTS (admin reply) ──────────────────────────────────────────────────── - -export async function postAdminComment(id: string, formData: FormData) { - await requireAdmin(); - const entity = formData.get("entity") as string; - const body = (formData.get("body") as string)?.trim(); - if (!body || !entity) throw new Error("Dati mancanti"); - const [entity_type, entity_id] = entity.split(":"); - if (!entity_type || !entity_id) throw new Error("Formato entity non valido"); - const allowedTypes = ["task", "deliverable", "phase", "general"]; - if (!allowedTypes.includes(entity_type)) throw new Error("entity_type non valido"); - await db.insert(comments).values({ entity_type, entity_id, author: "admin", body }); - const { path } = await resolveEntity(id); - revalidatePath(path); -} +// ── COMMENTS ────────────────────────────────────────────────────────────────── +// La risposta dell'admin vive in `replyToConversation` +// (src/app/admin/conversazioni/actions.ts): unica inbox, un solo punto di +// scrittura. Qui c'era `postAdminComment`, che rispondeva sulla singola entità +// dal tab Commenti del progetto — tab rimosso, action con lui. // ── ACCESSI PORTALE (whitelist OTP) ────────────────────────────────────────── // La whitelist è l'unico modo per entrare nel portale: nessuna auto-registrazione. diff --git a/src/app/admin/conversazioni/actions.ts b/src/app/admin/conversazioni/actions.ts index 1d0f1bc..a5984fe 100644 --- a/src/app/admin/conversazioni/actions.ts +++ b/src/app/admin/conversazioni/actions.ts @@ -15,7 +15,11 @@ async function requireAdmin() { /** * Admin reply from the Conversazioni inbox. Per project decision, replies are * saved as a "general" comment on the client (entity_id = clientId), so they - * surface in the client's general chat and in the client detail CommentsTab. + * surface in the client's general chat. + * + * Questa è l'UNICA via di risposta dell'admin da quando il tab Commenti del + * progetto è stato rimosso: i messaggi su fase/task/deliverable si leggono qui + * con la loro etichetta, ma la risposta torna sempre sul thread generale. */ export async function replyToConversation(clientId: string, formData: FormData) { await requireAdmin(); diff --git a/src/app/admin/projects/[id]/page.tsx b/src/app/admin/projects/[id]/page.tsx index 9303190..83ca08e 100644 --- a/src/app/admin/projects/[id]/page.tsx +++ b/src/app/admin/projects/[id]/page.tsx @@ -5,7 +5,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { PhasesTab } from "@/components/admin/tabs/PhasesTab"; import { PaymentsTab } from "@/components/admin/tabs/PaymentsTab"; import { DocumentsTab } from "@/components/admin/tabs/DocumentsTab"; -import { CommentsTab } from "@/components/admin/tabs/CommentsTab"; import { TimerTab } from "@/components/admin/tabs/TimerTab"; import { OffersTab } from "@/components/admin/tabs/OffersTab"; import { PhasesViewToggle } from "@/components/admin/kanban/PhasesViewToggle"; @@ -34,7 +33,6 @@ export default async function ProjectDetailPage({ payments, documents, notes, - comments, activeTimerEntryId, activeTimerStartedAt, totalTrackedSeconds, @@ -72,7 +70,6 @@ export default async function ProjectDetailPage({ Pagamenti Documenti Note - Commenti Timer Offerte @@ -115,10 +112,6 @@ export default async function ProjectDetailPage({ - - - - Valore Totale Acconto Saldo - Timer Redditività (€/H) diff --git a/src/components/admin/ProjectRow.tsx b/src/components/admin/ProjectRow.tsx index af448ae..076cc6e 100644 --- a/src/components/admin/ProjectRow.tsx +++ b/src/components/admin/ProjectRow.tsx @@ -1,5 +1,4 @@ import Link from "next/link"; -import { TimerCell } from "@/components/admin/TimerCell"; import type { ProjectWithPayments } from "@/lib/admin-queries"; const statusConfig: Record = { @@ -80,18 +79,6 @@ export function ProjectRow({ project }: { project: ProjectWithPayments }) { )} - -
- -
- - {eurPerHour === null ? ( diff --git a/src/components/admin/tabs/CommentsTab.tsx b/src/components/admin/tabs/CommentsTab.tsx deleted file mode 100644 index 41666f5..0000000 --- a/src/components/admin/tabs/CommentsTab.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { postAdminComment } from "@/app/admin/clients/[id]/actions"; -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; -import type { Comment } from "@/db/schema"; -import type { ClientFullDetail } from "@/lib/admin-queries"; - -type Props = { - comments: Comment[]; - phases: ClientFullDetail["phases"]; - clientId: string; -}; - -export async function CommentsTab({ comments, phases, clientId }: Props) { - // Build entity label map for display (phases, tasks, deliverables, and general) - const entityLabels: Record = { - [clientId]: "Messaggio generale", - }; - for (const phase of phases) { - entityLabels[phase.id] = `Fase: ${phase.title}`; - for (const task of phase.tasks) { - entityLabels[task.id] = `Task: ${task.title}`; - for (const d of task.deliverables) { - entityLabels[d.id] = `Deliverable: ${d.title}`; - } - } - } - - // Build list of entities the admin can reply on - const entities: Array<{ id: string; type: string; label: string }> = [ - { id: clientId, type: "general", label: "Messaggio generale" }, - ]; - for (const phase of phases) { - entities.push({ id: phase.id, type: "phase", label: `Fase: ${phase.title}` }); - for (const task of phase.tasks) { - entities.push({ id: task.id, type: "task", label: `Task: ${task.title}` }); - for (const d of task.deliverables) { - entities.push({ - id: d.id, - type: "deliverable", - label: `Deliverable: ${d.title}`, - }); - } - } - } - - return ( -
- {/* Comment list */} - {comments.length === 0 && ( -

Nessun commento ancora.

- )} -
- {comments.map((c) => ( -
-
-

- {c.author === "admin" ? "iamcavalli" : "Cliente"} —{" "} - {entityLabels[c.entity_id] ?? c.entity_id} -

-

{c.body}

-
-
- ))} -
- - {/* Admin reply form */} -
{ - "use server"; - await postAdminComment(clientId, fd); - }} - className="bg-white border border-gray-200 rounded-lg p-4 space-y-3" - > -

- Rispondi come admin -

- -