feat: payments plans, phase auto-cascade, transcripts, manual timer

- Pagamenti: totale ereditato dalla somma accepted_total delle offerte
  attive (con override) + selettore schema rate 1/2/3 step; nuova colonna
  payments.percent per rescalare gli importi preservando label/stato
- Fasi & Task: recomputePhaseStatus auto-cascade (task -> fase) su
  updateTaskStatus/addTask, fix UI stale, etichette Da iniziare/In corso/Completata
- Pagina pubblica: colori fasi (grigio/blu/#1A463C) + fasi collassabili
- Transcript del cliente visibili in tab Documenti admin e dashboard pubblica
- Timer: inserimento manuale (+30/+60, minuti liberi, data) + elimina entry
- CLAUDE.md: procedura Deploy & DB Access; push su main automatico

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 10:38:56 +02:00
parent 988f6f425a
commit 186bb9ea19
21 changed files with 1037 additions and 196 deletions
+30 -2
View File
@@ -1,6 +1,6 @@
import { eq, inArray, asc, sql } from "drizzle-orm";
import { eq, inArray, asc, desc, sql } from "drizzle-orm";
import { db } from "@/db";
import { clients, projects, phases, tasks, deliverables, payments, documents, notes, comments, project_offers, offer_micros, offer_micro_services, offer_services } from "@/db/schema";
import { clients, projects, phases, tasks, deliverables, payments, documents, notes, comments, project_offers, offer_micros, offer_micro_services, offer_services, clientTranscripts } from "@/db/schema";
/**
* ClientView: Legacy shape used by ClientDashboard component.
@@ -58,6 +58,13 @@ export interface ClientView {
cumulative_price: string;
accepted_total: string | null;
}>;
transcripts: Array<{
id: string;
title: string | null;
call_date: string;
content: string;
created_at: string;
}>;
}
export interface ProjectView {
@@ -121,6 +128,13 @@ export interface ProjectView {
cumulative_price: string; // sum of assigned offer_services.price
accepted_total: string | null;
}>;
transcripts: Array<{
id: string;
title: string | null;
call_date: string;
content: string;
created_at: Date;
}>;
}
export interface ClientProjectSummary {
@@ -308,6 +322,19 @@ export async function getProjectView(projectId: string): Promise<ProjectView | n
accepted_total: o.accepted_total ? String(o.accepted_total) : null,
}));
// Transcripts linked to the project's client (post lead→client conversion)
const transcriptsRows = await db
.select({
id: clientTranscripts.id,
title: clientTranscripts.title,
call_date: clientTranscripts.call_date,
content: clientTranscripts.content,
created_at: clientTranscripts.created_at,
})
.from(clientTranscripts)
.where(eq(clientTranscripts.client_id, project.client_id))
.orderBy(desc(clientTranscripts.call_date));
// Comments scoped to tasks and deliverables of this project
const allEntityIds = [...taskIds, ...deliverablesRows.map((d) => d.id)];
const commentsRows =
@@ -352,5 +379,6 @@ export async function getProjectView(projectId: string): Promise<ProjectView | n
comments: commentsRows,
global_progress_pct,
activeOffers: activeOffers.length > 0 ? activeOffers : undefined,
transcripts: transcriptsRows,
};
}