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
+45 -5
View File
@@ -65,6 +65,7 @@ export async function updateProjectAcceptedTotal(projectId: string, acceptedTota
}
// Creates Acconto 50% + Saldo 50% stubs for a project that has no payments yet.
// Kept for backward compatibility; prefer setPaymentPlan for new code.
export async function initProjectPayments(projectId: string): Promise<void> {
await requireAdmin();
const rows = await db
@@ -74,11 +75,50 @@ export async function initProjectPayments(projectId: string): Promise<void> {
.limit(1);
if (!rows[0]) throw new Error("Progetto non trovato");
const total = parseFloat(rows[0].accepted_total ?? "0");
const half = (total / 2).toFixed(2);
await db.insert(payments).values([
{ project_id: projectId, label: "Acconto 50%", amount: half, status: "da_saldare" },
{ project_id: projectId, label: "Saldo 50%", amount: half, status: "da_saldare" },
]);
await setPaymentPlan(projectId, "two", total);
}
// Replaces all payments for a project with a fresh plan at a given total.
// Modes:
// single → 1 row 100% "Pagamento unico (100%)"
// two → 2 rows 50%/50% "Acconto 50% (inizio lavori)" / "Saldo 50% (alla consegna)"
// three → 3 rows 50%/30%/20%
export async function setPaymentPlan(
projectId: string,
mode: "single" | "two" | "three",
total: number
): Promise<void> {
await requireAdmin();
type PaymentDef = { label: string; percent: number };
const plans: Record<string, PaymentDef[]> = {
single: [{ label: "Pagamento unico (100%)", percent: 100 }],
two: [
{ label: "Acconto 50% (inizio lavori)", percent: 50 },
{ label: "Saldo 50% (alla consegna)", percent: 50 },
],
three: [
{ label: "Acconto 50% (inizio lavori)", percent: 50 },
{ label: "30% (post revisioni)", percent: 30 },
{ label: "Saldo 20% (alla consegna)", percent: 20 },
],
};
const plan = plans[mode];
if (!plan) throw new Error("Modalità pagamento non valida");
// Delete existing payments for this project, then insert new ones.
await db.delete(payments).where(eq(payments.project_id, projectId));
await db.insert(payments).values(
plan.map((p) => ({
project_id: projectId,
label: p.label,
percent: p.percent.toFixed(2),
amount: ((total * p.percent) / 100).toFixed(2),
status: "da_saldare" as const,
}))
);
revalidatePath(`/admin/projects/${projectId}`);
}