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,154 @@
|
||||
import {
|
||||
addPhase,
|
||||
addTask,
|
||||
updateTaskStatus,
|
||||
updatePhaseStatus,
|
||||
} from "@/app/admin/clients/[id]/actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { ClientFullDetail } from "@/lib/admin-queries";
|
||||
|
||||
type Props = {
|
||||
phases: ClientFullDetail["phases"];
|
||||
clientId: string;
|
||||
};
|
||||
|
||||
const taskStatusOptions = [
|
||||
{ value: "todo", label: "Da fare" },
|
||||
{ value: "in_progress", label: "In corso" },
|
||||
{ value: "done", label: "Fatto" },
|
||||
];
|
||||
|
||||
const phaseStatusOptions = [
|
||||
{ value: "upcoming", label: "In arrivo" },
|
||||
{ value: "active", label: "Attiva" },
|
||||
{ value: "done", label: "Completata" },
|
||||
];
|
||||
|
||||
export async function PhasesTab({ phases, clientId }: Props) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Add phase form */}
|
||||
<form
|
||||
action={async (fd: FormData) => {
|
||||
"use server";
|
||||
await addPhase(clientId, fd);
|
||||
}}
|
||||
className="flex gap-2"
|
||||
>
|
||||
<Input
|
||||
name="title"
|
||||
placeholder="Nome nuova fase..."
|
||||
className="max-w-xs"
|
||||
required
|
||||
/>
|
||||
<Button type="submit" variant="outline" size="sm">
|
||||
+ Fase
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* Phases list */}
|
||||
{phases.length === 0 && (
|
||||
<p className="text-sm text-gray-400">Nessuna fase ancora.</p>
|
||||
)}
|
||||
{phases.map((phase) => (
|
||||
<div
|
||||
key={phase.id}
|
||||
className="border border-gray-200 rounded-lg p-4 bg-white"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-semibold text-gray-900">{phase.title}</h3>
|
||||
<form
|
||||
action={async (fd: FormData) => {
|
||||
"use server";
|
||||
await updatePhaseStatus(
|
||||
phase.id,
|
||||
clientId,
|
||||
fd.get("status") as string
|
||||
);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<select
|
||||
name="status"
|
||||
defaultValue={phase.status}
|
||||
className="text-xs border border-gray-200 rounded px-2 py-1 bg-white"
|
||||
>
|
||||
{phaseStatusOptions.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button type="submit" variant="ghost" size="sm" className="text-xs">
|
||||
Salva
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Tasks */}
|
||||
<div className="space-y-2 mb-3">
|
||||
{phase.tasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="flex items-center justify-between pl-3 border-l-2 border-gray-100"
|
||||
>
|
||||
<span className="text-sm text-gray-800">{task.title}</span>
|
||||
<form
|
||||
action={async (fd: FormData) => {
|
||||
"use server";
|
||||
await updateTaskStatus(
|
||||
task.id,
|
||||
clientId,
|
||||
fd.get("status") as string
|
||||
);
|
||||
}}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<select
|
||||
name="status"
|
||||
defaultValue={task.status}
|
||||
className="text-xs border border-gray-200 rounded px-2 py-1 bg-white"
|
||||
>
|
||||
{taskStatusOptions.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-xs px-1"
|
||||
>
|
||||
✓
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add task form */}
|
||||
<form
|
||||
action={async (fd: FormData) => {
|
||||
"use server";
|
||||
await addTask(phase.id, clientId, fd);
|
||||
}}
|
||||
className="flex gap-2 mt-2"
|
||||
>
|
||||
<Input
|
||||
name="title"
|
||||
placeholder="Nuovo task..."
|
||||
className="text-sm max-w-xs"
|
||||
required
|
||||
/>
|
||||
<Button type="submit" variant="ghost" size="sm" className="text-xs">
|
||||
+ Task
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user