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:
2026-05-22 08:45:10 +02:00
parent b92b1c447b
commit ef05d647fe
9 changed files with 347 additions and 54 deletions
+22 -2
View File
@@ -42,6 +42,8 @@ export type ClientWithPayments = {
activeTimerEntryId: string | null;
activeTimerStartedAt: Date | null;
totalTrackedSeconds: number;
projectBrands: string[];
ltv: string;
};
export async function getAllClientsWithPayments(
@@ -62,9 +64,16 @@ export async function getAllClientsWithPayments(
// Get all projects for these clients (payments/time_entries now scoped to project)
const clientProjects = await db
.select({ id: projects.id, client_id: projects.client_id })
.select({
id: projects.id,
client_id: projects.client_id,
name: projects.name,
accepted_total: projects.accepted_total,
archived: projects.archived,
})
.from(projects)
.where(inArray(projects.client_id, clientIds));
.where(inArray(projects.client_id, clientIds))
.orderBy(asc(projects.created_at));
const projectIds = clientProjects.map((p) => p.id);
const projectToClient = new Map(clientProjects.map((p) => [p.id, p.client_id]));
@@ -82,6 +91,8 @@ export async function getAllClientsWithPayments(
activeTimerEntryId: null,
activeTimerStartedAt: null,
totalTrackedSeconds: 0,
projectBrands: [],
ltv: "0.00",
}));
}
@@ -133,6 +144,13 @@ export async function getAllClientsWithPayments(
return visible.map((c) => {
const active = clientTimerMap.get(c.id);
const clientPayments = clientPaymentsMap.get(c.id) ?? [];
const clientProjectsList = clientProjects.filter((p) => p.client_id === c.id);
const projectBrands = clientProjectsList
.filter((p) => !p.archived)
.map((p) => p.name);
const ltv = clientProjectsList
.reduce((sum, p) => sum + parseFloat(p.accepted_total ?? "0"), 0)
.toFixed(2);
return {
id: c.id,
name: c.name,
@@ -150,6 +168,8 @@ export async function getAllClientsWithPayments(
activeTimerEntryId: active?.id ?? null,
activeTimerStartedAt: active?.started_at ?? null,
totalTrackedSeconds: clientTotalsMap.get(c.id) ?? 0,
projectBrands,
ltv,
};
});
}