chore(repo): checkpoint current capakraken implementation state

This commit is contained in:
2026-03-29 12:47:12 +02:00
parent beae1a5d6e
commit 47e4d701ff
94 changed files with 4283 additions and 1710 deletions
+4 -2
View File
@@ -108,8 +108,10 @@ export function useDashboardLayout() {
const onLayoutChange = useCallback(
(layout: { i: string; x: number; y: number; w: number; h: number }[]) => {
setConfig((prev) => {
const layoutMap = new Map(layout.map((item) => [item.i, item]));
const previousWidgetMap = new Map(prev.widgets.map((widget) => [widget.id, widget]));
const updatedWidgets = prev.widgets.map((w) => {
const item = layout.find((l) => l.i === w.id);
const item = layoutMap.get(w.id);
if (!item) return w;
return { ...w, x: item.x, y: item.y, w: item.w, h: item.h };
});
@@ -118,7 +120,7 @@ export function useDashboardLayout() {
// react-grid-layout fires onLayoutChange on mount too — we skip that
// to avoid overwriting saved positions with compacted coordinates.
const changed = updatedWidgets.some((w) => {
const orig = prev.widgets.find((o) => o.id === w.id);
const orig = previousWidgetMap.get(w.id);
return orig && (w.x !== orig.x || w.y !== orig.y || w.w !== orig.w || w.h !== orig.h);
});
+99
View File
@@ -0,0 +1,99 @@
"use client";
import { useMemo } from "react";
import { trpc } from "~/lib/trpc/client.js";
export interface ClientReference {
id: string;
name: string;
code: string | null;
isActive?: boolean;
}
export interface CountryReference {
id: string;
name: string;
code: string;
isActive?: boolean;
}
export interface RoleReference {
id: string;
name: string;
isActive?: boolean;
}
export interface ReferenceDataSelection {
clients?: boolean;
countries?: boolean;
roles?: boolean;
chapters?: boolean;
}
const LOOKUP_STALE_TIME_MS = 300_000;
export function useReferenceData(selection: ReferenceDataSelection = {}) {
const shouldLoadClients = selection.clients === true;
const shouldLoadCountries = selection.countries === true;
const shouldLoadRoles = selection.roles === true;
const shouldLoadChapters = selection.chapters === true;
const { data: clientsRaw } = trpc.clientEntity.list.useQuery(
{ isActive: true },
{ staleTime: LOOKUP_STALE_TIME_MS, enabled: shouldLoadClients },
);
const { data: countriesRaw } = trpc.country.list.useQuery(
{ isActive: true },
{ staleTime: LOOKUP_STALE_TIME_MS, enabled: shouldLoadCountries },
);
const { data: rolesRaw } = trpc.role.list.useQuery(
{ isActive: true },
{ staleTime: LOOKUP_STALE_TIME_MS, enabled: shouldLoadRoles },
);
const { data: chaptersRaw } = trpc.resource.chapters.useQuery(undefined, {
staleTime: LOOKUP_STALE_TIME_MS,
enabled: shouldLoadChapters,
});
const clients = useMemo<ClientReference[]>(() => {
if (!shouldLoadClients) return [];
const list = (
Array.isArray(clientsRaw) ? clientsRaw : ((clientsRaw as { clients?: ClientReference[] } | undefined)?.clients ?? [])
) as ClientReference[];
return [...list]
.filter((client) => client.isActive !== false)
.sort((left, right) => left.name.localeCompare(right.name));
}, [clientsRaw, shouldLoadClients]);
const countries = useMemo<CountryReference[]>(() => {
if (!shouldLoadCountries) return [];
const list = (Array.isArray(countriesRaw) ? countriesRaw : []) as CountryReference[];
return [...list]
.filter((country) => country.isActive !== false)
.sort((left, right) => left.name.localeCompare(right.name));
}, [countriesRaw, shouldLoadCountries]);
const roles = useMemo<RoleReference[]>(() => {
if (!shouldLoadRoles) return [];
const list = (Array.isArray(rolesRaw) ? rolesRaw : []) as RoleReference[];
return [...list]
.filter((role) => role.isActive !== false)
.sort((left, right) => left.name.localeCompare(right.name));
}, [rolesRaw, shouldLoadRoles]);
const chapters = useMemo<string[]>(() => {
if (!shouldLoadChapters) return [];
const list = (Array.isArray(chaptersRaw) ? chaptersRaw : []) as string[];
return [...list].sort((left, right) => left.localeCompare(right));
}, [chaptersRaw, shouldLoadChapters]);
return {
clients,
countries,
roles,
chapters,
};
}
+13 -36
View File
@@ -1,59 +1,36 @@
/**
* Shared hook for loading filter options used across dashboard widgets.
* Loads clients, countries, roles, and chapters once with long cache TTL.
* Loads only the requested lookup sets and exposes them as filter options.
*/
"use client";
import { useMemo } from "react";
import { trpc } from "~/lib/trpc/client.js";
import { useReferenceData, type ReferenceDataSelection } from "~/hooks/useReferenceData.js";
export interface FilterOption {
value: string;
label: string;
}
export function useWidgetFilterOptions() {
const { data: clientsRaw } = trpc.clientEntity.list.useQuery(
{ isActive: true },
{ staleTime: 300_000 },
);
const { data: countriesRaw } = trpc.country.list.useQuery(
{ isActive: true },
{ staleTime: 300_000 },
);
const { data: rolesRaw } = trpc.role.list.useQuery(
{ isActive: true },
{ staleTime: 300_000 },
);
export function useWidgetFilterOptions(selection: ReferenceDataSelection = {}) {
const { clients: clientRows, countries: countryRows, roles: roleRows, chapters: chapterRows } =
useReferenceData(selection);
const clients = useMemo<FilterOption[]>(() => {
const list = (Array.isArray(clientsRaw) ? clientsRaw : (clientsRaw as any)?.clients ?? []) as Array<{ id: string; name: string }>;
return list.map((c) => ({ value: c.id, label: c.name }));
}, [clientsRaw]);
return clientRows.map((client) => ({ value: client.id, label: client.name }));
}, [clientRows]);
const countries = useMemo<FilterOption[]>(() => {
const list = (Array.isArray(countriesRaw) ? countriesRaw : []) as Array<{ id: string; name: string }>;
return list.map((c) => ({ value: c.id, label: c.name }));
}, [countriesRaw]);
return countryRows.map((country) => ({ value: country.id, label: country.name }));
}, [countryRows]);
const roles = useMemo<FilterOption[]>(() => {
const list = (Array.isArray(rolesRaw) ? rolesRaw : []) as Array<{ id: string; name: string }>;
return list.map((r) => ({ value: r.id, label: r.name }));
}, [rolesRaw]);
return roleRows.map((role) => ({ value: role.id, label: role.name }));
}, [roleRows]);
// Chapters are derived from roles or can be hardcoded common ones
const chapters = useMemo<FilterOption[]>(() => {
const common = [
"Digital Content Production",
"Project Management",
"Art Direction",
"CGI-Dev",
"Product Data Management",
];
return common.map((c) => ({ value: c, label: c }));
}, []);
return chapterRows.map((chapter) => ({ value: chapter, label: chapter }));
}, [chapterRows]);
return { clients, countries, roles, chapters };
}