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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user