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,105 @@
|
||||
import { postAdminComment } from "@/app/admin/clients/[id]/actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { Comment } from "@/db/schema";
|
||||
import type { ClientFullDetail } from "@/lib/admin-queries";
|
||||
|
||||
type Props = {
|
||||
comments: Comment[];
|
||||
phases: ClientFullDetail["phases"];
|
||||
clientId: string;
|
||||
};
|
||||
|
||||
export async function CommentsTab({ comments, phases, clientId }: Props) {
|
||||
// Build entity label map for display
|
||||
const entityLabels: Record<string, string> = {};
|
||||
for (const phase of phases) {
|
||||
for (const task of phase.tasks) {
|
||||
entityLabels[task.id] = `Task: ${task.title}`;
|
||||
for (const d of task.deliverables) {
|
||||
entityLabels[d.id] = `Deliverable: ${d.title}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build list of entities the admin can reply on
|
||||
const entities: Array<{ id: string; type: string; label: string }> = [];
|
||||
for (const phase of phases) {
|
||||
for (const task of phase.tasks) {
|
||||
entities.push({ id: task.id, type: "task", label: `Task: ${task.title}` });
|
||||
for (const d of task.deliverables) {
|
||||
entities.push({
|
||||
id: d.id,
|
||||
type: "deliverable",
|
||||
label: `Deliverable: ${d.title}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-lg">
|
||||
{/* Comment list */}
|
||||
{comments.length === 0 && (
|
||||
<p className="text-sm text-gray-400">Nessun commento ancora.</p>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
{comments.map((c) => (
|
||||
<div
|
||||
key={c.id}
|
||||
className={`flex gap-3 ${c.author === "admin" ? "flex-row-reverse" : ""}`}
|
||||
>
|
||||
<div
|
||||
className={`rounded-lg px-3 py-2 text-sm max-w-xs ${
|
||||
c.author === "admin"
|
||||
? "bg-gray-900 text-white"
|
||||
: "bg-white border border-gray-200 text-gray-800"
|
||||
}`}
|
||||
>
|
||||
<p className="text-xs font-medium mb-1 opacity-60">
|
||||
{c.author === "admin" ? "iamcavalli" : "Cliente"} —{" "}
|
||||
{entityLabels[c.entity_id] ?? c.entity_id}
|
||||
</p>
|
||||
<p>{c.body}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Admin reply form */}
|
||||
{entities.length > 0 && (
|
||||
<form
|
||||
action={async (fd: FormData) => {
|
||||
"use server";
|
||||
await postAdminComment(clientId, fd);
|
||||
}}
|
||||
className="bg-white border border-gray-200 rounded-lg p-4 space-y-3"
|
||||
>
|
||||
<h3 className="font-medium text-gray-900 text-sm">
|
||||
Rispondi come admin
|
||||
</h3>
|
||||
<select
|
||||
name="entity"
|
||||
className="w-full text-sm border border-gray-200 rounded px-2 py-1.5 bg-white"
|
||||
required
|
||||
>
|
||||
{entities.map((e) => (
|
||||
<option key={e.id} value={`${e.type}:${e.id}`}>
|
||||
{e.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Textarea
|
||||
name="body"
|
||||
placeholder="Scrivi un commento..."
|
||||
rows={3}
|
||||
required
|
||||
/>
|
||||
<Button type="submit" size="sm">
|
||||
Invia risposta
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user