feat(02-03): build /admin/clients/[id] workspace with tabbed layout and all tab components

- Create /admin/clients/[id]/page.tsx — Server Component using Radix Tabs (Fasi & Task, Pagamenti, Documenti, Commenti)
- Create PhasesTab: phases list with add-phase form, task lists with add-task form, status selects for phases and tasks
- Create PaymentsTab: accepted_total editor (splits to 50% on each payment), payment status selects with paid_at on saldato
- Create DocumentsTab: add document (label + URL) form, document list with delete action
- Create CommentsTab: chronological comment display (admin vs cliente style), admin reply form with entity selector
- All mutations via inline Server Action closures bound to action= props; revalidatePath ensures fresh data
This commit is contained in:
Simone Cavalli
2026-05-15 21:16:10 +02:00
parent 7733566f5b
commit 59a46d37fa
5 changed files with 509 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
import {
updatePaymentStatus,
updateAcceptedTotal,
} from "@/app/admin/clients/[id]/actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type { Payment } from "@/db/schema";
type Props = {
payments: Payment[];
acceptedTotal: string;
clientId: string;
};
const statusLabels: Record<string, string> = {
da_saldare: "Da saldare",
inviata: "Inviata",
saldato: "Saldato",
};
export async function PaymentsTab({ payments, acceptedTotal, clientId }: Props) {
return (
<div className="space-y-6 max-w-md">
{/* Accepted total */}
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h3 className="font-medium text-gray-900 mb-3">Totale preventivo</h3>
<form
action={async (fd: FormData) => {
"use server";
await updateAcceptedTotal(clientId, fd);
}}
className="flex items-end gap-3"
>
<div className="space-y-1 flex-1">
<Label htmlFor="accepted_total">Importo ()</Label>
<Input
id="accepted_total"
name="accepted_total"
type="number"
step="0.01"
min="0"
defaultValue={acceptedTotal}
className="max-w-xs"
/>
</div>
<Button type="submit" size="sm">
Salva
</Button>
</form>
<p className="text-xs text-gray-400 mt-2">
Le rate Acconto e Saldo vengono aggiornate automaticamente al 50% ciascuna.
</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>
<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>
);
}