feat(04-02): admin projects list + client detail project cards
- NavBar: add Progetti (/admin/projects) and Impostazioni links - ProjectRow: project table row with €/h calc (accepted_total / tracked hours) - /admin/projects: table listing all projects with value, payments, timer, €/h - /admin/projects/new: create form with client select + ?client_id pre-selection - project-actions: createProject, archiveProject, unarchiveProject, updateProjectAcceptedTotal - /admin/clients/[id]: rewritten to show project cards grid instead of tabbed workspace - getAllClientsWithPayments: add projectBrands[] and ltv (sum of project accepted_totals) - ClientRow: show project brand names under client name, LTV column replaces Totale Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,11 @@ export function ClientRow({ client }: { client: ClientWithPayments }) {
|
||||
>
|
||||
{client.name}
|
||||
</Link>
|
||||
<p className="text-xs text-[#71717a]">{client.brand_name}</p>
|
||||
{client.projectBrands && client.projectBrands.length > 0 ? (
|
||||
<p className="text-xs text-[#71717a] mt-0.5">{client.projectBrands.join(" | ")}</p>
|
||||
) : (
|
||||
<p className="text-xs text-[#71717a]">{client.brand_name}</p>
|
||||
)}
|
||||
{client.archived && (
|
||||
<span className="text-[10px] text-[#71717a] bg-[#f4f4f5] px-1.5 py-0.5 rounded-full">
|
||||
Archiviato
|
||||
@@ -30,7 +34,7 @@ export function ClientRow({ client }: { client: ClientWithPayments }) {
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-[#1a1a1a]">
|
||||
€{parseFloat(client.accepted_total).toLocaleString("it-IT", { minimumFractionDigits: 2 })}
|
||||
€{parseFloat(client.ltv).toLocaleString("it-IT", { minimumFractionDigits: 2 })}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{acconto && (
|
||||
|
||||
@@ -12,12 +12,18 @@ export function NavBar() {
|
||||
<Link href="/admin" className="text-sm text-white/70 hover:text-white transition-colors">
|
||||
Clienti
|
||||
</Link>
|
||||
<Link href="/admin/projects" className="text-sm text-white/70 hover:text-white transition-colors">
|
||||
Progetti
|
||||
</Link>
|
||||
<Link href="/admin/analytics" className="text-sm text-white/70 hover:text-white transition-colors">
|
||||
Statistiche
|
||||
</Link>
|
||||
<Link href="/admin/catalog" className="text-sm text-white/70 hover:text-white transition-colors">
|
||||
Catalogo
|
||||
</Link>
|
||||
<Link href="/admin/impostazioni" className="text-sm text-white/70 hover:text-white transition-colors">
|
||||
Impostazioni
|
||||
</Link>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import Link from "next/link";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { TimerCell } from "@/components/admin/TimerCell";
|
||||
import type { ProjectWithPayments } from "@/lib/admin-queries";
|
||||
|
||||
const statusConfig: Record<string, { label: string; className: string }> = {
|
||||
da_saldare: { label: "Da saldare", className: "bg-red-100 text-red-700 border-transparent" },
|
||||
inviata: { label: "Inviata", className: "bg-[#DEF168]/30 text-[#1A463C] border-transparent" },
|
||||
saldato: { label: "Saldato", className: "bg-[#1A463C]/10 text-[#1A463C] border-transparent font-medium" },
|
||||
};
|
||||
|
||||
export function ProjectRow({ project }: { project: ProjectWithPayments }) {
|
||||
const acconto = project.payments.find((p) => p.label.toLowerCase().includes("acconto"));
|
||||
const saldo = project.payments.find((p) => p.label.toLowerCase().includes("saldo"));
|
||||
|
||||
const hours = project.totalTrackedSeconds / 3600;
|
||||
const eurPerHour = hours > 0 ? parseFloat(project.accepted_total) / hours : null;
|
||||
|
||||
return (
|
||||
<tr className={`border-b border-[#f4f4f5] hover:bg-[#f9f9f9] transition-colors ${project.archived ? "opacity-60" : ""}`}>
|
||||
<td className="py-3 px-4">
|
||||
<Link
|
||||
href={`/admin/projects/${project.id}`}
|
||||
className="font-medium text-[#1a1a1a] hover:text-[#1A463C] hover:underline"
|
||||
>
|
||||
{project.name}
|
||||
</Link>
|
||||
<p className="text-xs text-[#71717a]">{project.client.name}</p>
|
||||
{project.archived && (
|
||||
<span className="text-[10px] text-[#71717a] bg-[#f4f4f5] px-1.5 py-0.5 rounded-full">
|
||||
Archiviato
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-[#1a1a1a]">
|
||||
€{parseFloat(project.accepted_total).toLocaleString("it-IT", { minimumFractionDigits: 2 })}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{acconto && (
|
||||
<Badge className={statusConfig[acconto.status]?.className ?? "border-transparent bg-[#f4f4f5] text-[#71717a]"}>
|
||||
Acconto: {statusConfig[acconto.status]?.label ?? acconto.status}
|
||||
</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{saldo && (
|
||||
<Badge className={statusConfig[saldo.status]?.className ?? "border-transparent bg-[#f4f4f5] text-[#71717a]"}>
|
||||
Saldo: {statusConfig[saldo.status]?.label ?? saldo.status}
|
||||
</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<TimerCell
|
||||
clientId={project.id}
|
||||
activeEntryId={project.activeTimerEntryId}
|
||||
activeStartedAt={project.activeTimerStartedAt}
|
||||
totalTrackedSeconds={project.totalTrackedSeconds}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-[#71717a] tabular-nums">
|
||||
{eurPerHour !== null ? `€${eurPerHour.toFixed(2)}/h` : "—"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user