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
+67 -33
View File
@@ -2,15 +2,18 @@ import {
updatePaymentStatus,
updateAcceptedTotal,
} from "@/app/admin/clients/[id]/actions";
import { initProjectPayments } from "@/app/admin/projects/project-actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { SplitPaymentForm } from "@/components/admin/SplitPaymentForm";
import type { Payment } from "@/db/schema";
type Props = {
payments: Payment[];
acceptedTotal: string;
clientId: string;
projectId?: string; // set only from project detail page — enables init & split
};
const statusLabels: Record<string, string> = {
@@ -19,7 +22,7 @@ const statusLabels: Record<string, string> = {
saldato: "Saldato",
};
export async function PaymentsTab({ payments, acceptedTotal, clientId }: Props) {
export async function PaymentsTab({ payments, acceptedTotal, clientId, projectId }: Props) {
return (
<div className="space-y-6 max-w-md">
{/* Accepted total */}
@@ -53,45 +56,76 @@ export async function PaymentsTab({ payments, acceptedTotal, clientId }: Props)
</p>
</div>
{/* Payment rows */}
{payments.map((p) => (
<div
key={p.id}
className="bg-white border border-gray-200 rounded-lg p-4"
>
<div className="flex items-center justify-between mb-2">
<h3 className="font-medium text-gray-900">{p.label}</h3>
<span className="text-sm text-gray-600">
{" "}
{parseFloat(p.amount).toLocaleString("it-IT", {
minimumFractionDigits: 2,
})}
</span>
</div>
{/* Empty state — project has no payments yet */}
{projectId && payments.length === 0 && (
<div className="bg-white border border-dashed border-gray-300 rounded-lg p-6 text-center">
<p className="text-sm text-[#71717a] mb-4">
Nessun pagamento configurato. Crea le rate standard Acconto e Saldo.
</p>
<form
action={async (fd: FormData) => {
action={async () => {
"use server";
await updatePaymentStatus(p.id, clientId, fd.get("status") as string);
await initProjectPayments(projectId);
}}
className="flex items-center gap-2"
>
<select
name="status"
defaultValue={p.status}
className="text-sm border border-gray-200 rounded px-2 py-1.5 bg-white flex-1"
>
{Object.entries(statusLabels).map(([val, label]) => (
<option key={val} value={val}>
{label}
</option>
))}
</select>
<Button type="submit" size="sm" variant="outline">
Aggiorna
<Button type="submit" size="sm">
Crea Acconto &amp; Saldo
</Button>
</form>
</div>
))}
)}
{/* Payment rows */}
{payments.map((p) => {
const amount = parseFloat(p.amount);
return (
<div
key={p.id}
className="bg-white border border-gray-200 rounded-lg p-4"
>
<div className="flex items-center justify-between mb-2">
<h3 className="font-medium text-gray-900">{p.label}</h3>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">
{" "}
{amount.toLocaleString("it-IT", {
minimumFractionDigits: 2,
})}
</span>
{projectId && amount > 0 && (
<SplitPaymentForm
paymentId={p.id}
projectId={projectId}
currentAmount={amount}
/>
)}
</div>
</div>
<form
action={async (fd: FormData) => {
"use server";
await updatePaymentStatus(p.id, clientId, fd.get("status") as string);
}}
className="flex items-center gap-2"
>
<select
name="status"
defaultValue={p.status}
className="text-sm border border-gray-200 rounded px-2 py-1.5 bg-white flex-1"
>
{Object.entries(statusLabels).map(([val, label]) => (
<option key={val} value={val}>
{label}
</option>
))}
</select>
<Button type="submit" size="sm" variant="outline">
Aggiorna
</Button>
</form>
</div>
);
})}
</div>
);
}