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