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:
@@ -86,6 +86,7 @@ export default async function ProjectDetailPage({
|
||||
payments={payments}
|
||||
acceptedTotal={project.accepted_total ?? "0"}
|
||||
clientId={id}
|
||||
projectId={id}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
Reference in New Issue
Block a user