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