feat: init payments + split payment in project workspace

- initProjectPayments: crea Acconto 50%/Saldo 50% se il progetto non ha pagamenti
- splitPayment: divide un pagamento in due rate con importi custom
- SplitPaymentForm: form client interattivo con anteprima live della rata 2
- PaymentsTab: stato vuoto con CTA + pulsante Dividi per ogni riga
- projectId prop opzionale per attivare le feature solo nel contesto progetto
This commit is contained in:
2026-05-23 10:18:06 +02:00
parent 0571040d2f
commit 75097ccfa4
4 changed files with 215 additions and 34 deletions
+1
View File
@@ -86,6 +86,7 @@ export default async function ProjectDetailPage({
payments={payments}
acceptedTotal={project.accepted_total ?? "0"}
clientId={id}
projectId={id}
/>
</TabsContent>
+56 -1
View File
@@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { db } from "@/db";
import { projects, clients } from "@/db/schema";
import { projects, clients, payments } from "@/db/schema";
import { eq } from "drizzle-orm";
import { nanoid } from "nanoid";
@@ -51,5 +51,60 @@ export async function unarchiveProject(projectId: string): Promise<void> {
export async function updateProjectAcceptedTotal(projectId: string, acceptedTotal: string): Promise<void> {
await requireAdmin();
await db.update(projects).set({ accepted_total: acceptedTotal }).where(eq(projects.id, projectId));
revalidatePath(`/admin/projects/${projectId}`);
}
// Creates Acconto 50% + Saldo 50% stubs for a project that has no payments yet.
export async function initProjectPayments(projectId: string): Promise<void> {
await requireAdmin();
const rows = await db
.select({ accepted_total: projects.accepted_total })
.from(projects)
.where(eq(projects.id, projectId))
.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" },
]);
revalidatePath(`/admin/projects/${projectId}`);
}
// Splits one payment into two with custom amounts. The original row keeps
// its label (+ " Rata 1"), a new row is created with the remainder.
export async function splitPayment(
paymentId: string,
projectId: string,
firstAmountStr: string
): Promise<void> {
await requireAdmin();
const first = parseFloat(firstAmountStr);
if (isNaN(first) || first <= 0) throw new Error("Importo non valido");
const rows = await db.select().from(payments).where(eq(payments.id, paymentId)).limit(1);
if (!rows[0]) throw new Error("Pagamento non trovato");
const original = rows[0];
const total = parseFloat(original.amount);
const second = parseFloat((total - first).toFixed(2));
if (second <= 0) throw new Error("Il primo importo deve essere inferiore al totale");
// Strip any existing " Rata N" suffix so re-splits stay clean.
const baseLabel = original.label.replace(/ Rata \d+$/, "");
await db
.update(payments)
.set({ amount: first.toFixed(2), label: `${baseLabel} Rata 1` })
.where(eq(payments.id, paymentId));
await db.insert(payments).values({
project_id: projectId,
label: `${baseLabel} Rata 2`,
amount: second.toFixed(2),
status: original.status,
});
revalidatePath(`/admin/projects/${projectId}`);
}