feat: document edit inline + client dashboard sidebar layout

- actions.ts: add updateDocument server action (label + url, Zod validated)
- DocumentRow: Client Component with hover-reveal edit/remove buttons,
  inline edit form with pre-filled fields and cancel/save
- DocumentsTab: use DocumentRow, remove variant dependency
- client-dashboard: two-column layout (sidebar left on lg+):
  sidebar = payments + documents + notes (sticky top)
  main = brief + phases toggle (timeline / kanban)
  mobile: main first, sidebar below (order-1/order-2)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simone Cavalli
2026-05-16 12:24:49 +02:00
parent 7af917fe80
commit 3582e26970
4 changed files with 199 additions and 84 deletions
+114
View File
@@ -0,0 +1,114 @@
"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { updateDocument, deleteDocument } from "@/app/admin/clients/[id]/actions";
import type { Document } from "@/db/schema";
export function DocumentRow({
doc,
clientId,
}: {
doc: Document;
clientId: string;
}) {
const [editing, setEditing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [, startTransition] = useTransition();
const router = useRouter();
function handleSave(fd: FormData) {
setError(null);
startTransition(async () => {
try {
await updateDocument(doc.id, clientId, fd);
setEditing(false);
router.refresh();
} catch (e) {
setError(e instanceof Error ? e.message : "Errore nel salvataggio");
}
});
}
function handleDelete() {
startTransition(async () => {
await deleteDocument(doc.id, clientId);
router.refresh();
});
}
if (editing) {
return (
<form
action={handleSave}
className="bg-white border-2 border-[#1A463C]/30 rounded-lg px-4 py-3 space-y-2"
>
<Input
name="label"
defaultValue={doc.label}
placeholder="Nome / etichetta"
required
className="text-sm"
/>
<Input
name="url"
defaultValue={doc.url}
type="url"
placeholder="https://..."
required
className="text-sm"
/>
{error && <p className="text-xs text-red-600">{error}</p>}
<div className="flex gap-2 pt-1">
<Button type="submit" size="sm">
Salva
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => { setEditing(false); setError(null); }}
className="text-[#71717a]"
>
Annulla
</Button>
</div>
</form>
);
}
return (
<div className="flex items-center justify-between bg-white border border-[#e5e7eb] rounded-lg px-4 py-3 group">
<a
href={doc.url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-[#1A463C] hover:underline font-medium truncate min-w-0 mr-3"
>
{doc.label}
</a>
<div className="flex items-center gap-1 shrink-0">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setEditing(true)}
className="text-xs text-[#71717a] hover:text-[#1A463C] opacity-0 group-hover:opacity-100 transition-opacity"
>
Modifica
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleDelete}
className="text-xs text-red-400 hover:text-red-600 opacity-0 group-hover:opacity-100 transition-opacity"
>
Rimuovi
</Button>
</div>
</div>
);
}
+6 -32
View File
@@ -1,4 +1,5 @@
import { addDocument, deleteDocument } from "@/app/admin/clients/[id]/actions";
import { addDocument } from "@/app/admin/clients/[id]/actions";
import { DocumentRow } from "@/components/admin/DocumentRow";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -14,9 +15,9 @@ export async function DocumentsTab({ documents, clientId }: Props) {
"use server";
await addDocument(clientId, fd);
}}
className="bg-white border border-gray-200 rounded-lg p-4 space-y-3"
className="bg-white border border-[#e5e7eb] rounded-lg p-4 space-y-3"
>
<h3 className="font-medium text-gray-900">Aggiungi documento</h3>
<h3 className="font-medium text-[#1a1a1a]">Aggiungi documento</h3>
<div className="space-y-1">
<Label htmlFor="doc-label">Nome / etichetta</Label>
<Input
@@ -42,38 +43,11 @@ export async function DocumentsTab({ documents, clientId }: Props) {
</form>
{documents.length === 0 && (
<p className="text-sm text-gray-400">Nessun documento ancora.</p>
<p className="text-sm text-[#71717a]">Nessun documento ancora.</p>
)}
<div className="space-y-2">
{documents.map((doc) => (
<div
key={doc.id}
className="flex items-center justify-between bg-white border border-gray-200 rounded-lg px-4 py-3"
>
<a
href={doc.url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:underline"
>
{doc.label}
</a>
<form
action={async () => {
"use server";
await deleteDocument(doc.id, clientId);
}}
>
<Button
type="submit"
variant="ghost"
size="sm"
className="text-red-500 hover:text-red-700 text-xs"
>
Rimuovi
</Button>
</form>
</div>
<DocumentRow key={doc.id} doc={doc} clientId={clientId} />
))}
</div>
</div>