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
@@ -424,6 +424,13 @@ export function TimelineProvider({
const { resourceMap, allocsByResource, resources } = useMemo(() => {
const resourceMap = new Map<string, ResourceBrief>();
const allocsByResource = new Map<string, TimelineAssignmentEntry[]>();
const firstAssignmentByResource = new Map<string, TimelineAssignmentEntry>();
const projectIdsByResource = new Map<string, Set<string>>();
const clientIdsByResource = new Map<string, Set<string>>();
const chapterFilter = new Set(filters.chapters);
const eidFilter = new Set(filters.eids);
const projectFilter = new Set(filters.projectIds);
const clientFilter = new Set(filters.clientIds);
if (eidFilterData?.resources) {
for (const r of eidFilterData.resources as {
@@ -445,6 +452,7 @@ export function TimelineProvider({
for (const entry of visibleAssignments) {
if (!entry.resourceId) continue;
firstAssignmentByResource.set(entry.resourceId, entry);
if (!resourceMap.has(entry.resourceId)) {
resourceMap.set(entry.resourceId, {
id: entry.resource!.id,
@@ -456,13 +464,23 @@ export function TimelineProvider({
const arr = allocsByResource.get(entry.resourceId) ?? [];
arr.push(entry);
allocsByResource.set(entry.resourceId, arr);
const projectIds = projectIdsByResource.get(entry.resourceId) ?? new Set<string>();
projectIds.add(entry.projectId);
projectIdsByResource.set(entry.resourceId, projectIds);
if (typeof entry.project.clientId === "string") {
const clientIds = clientIdsByResource.get(entry.resourceId) ?? new Set<string>();
clientIds.add(entry.project.clientId);
clientIdsByResource.set(entry.resourceId, clientIds);
}
}
// Merge cross-project context allocations so they appear during drag
if (isDragging && contextAllocations.length > 0) {
for (const ca of contextAllocations) {
if (!ca.resourceId) continue;
const existing = visibleAssignments.find((entry) => entry.resourceId === ca.resourceId);
const existing = firstAssignmentByResource.get(ca.resourceId);
if (existing && !resourceMap.has(ca.resourceId)) {
resourceMap.set(ca.resourceId, {
id: existing.resource!.id,
@@ -477,32 +495,35 @@ export function TimelineProvider({
let resources = [...resourceMap.values()].sort((a, b) =>
a.displayName.localeCompare(b.displayName),
);
if (filters.chapters.length > 0) {
resources = resources.filter((r) => r.chapter && filters.chapters.includes(r.chapter));
if (chapterFilter.size > 0) {
resources = resources.filter((r) => r.chapter && chapterFilter.has(r.chapter));
}
if (filters.eids.length > 0) {
resources = resources.filter((r) => filters.eids.includes(r.eid));
if (eidFilter.size > 0) {
resources = resources.filter((r) => eidFilter.has(r.eid));
}
if (filters.projectIds.length > 0) {
resources = resources.filter((r) =>
visibleAssignments.some(
(e) => e.resourceId === r.id && filters.projectIds.includes(e.projectId),
),
);
if (projectFilter.size > 0) {
resources = resources.filter((r) => {
const projectIds = projectIdsByResource.get(r.id);
if (!projectIds) return false;
for (const projectId of projectIds) {
if (projectFilter.has(projectId)) {
return true;
}
}
return false;
});
}
if (filters.clientIds.length > 0) {
resources = resources.filter((r) =>
visibleAssignments.some(
(entry) => {
const clientId = entry.project.clientId;
return (
entry.resourceId === r.id &&
typeof clientId === "string" &&
filters.clientIds.includes(clientId)
);
},
),
);
if (clientFilter.size > 0) {
resources = resources.filter((r) => {
const clientIds = clientIdsByResource.get(r.id);
if (!clientIds) return false;
for (const clientId of clientIds) {
if (clientFilter.has(clientId)) {
return true;
}
}
return false;
});
}
return { resourceMap, allocsByResource, resources };
@@ -520,6 +541,14 @@ export function TimelineProvider({
// ─── Project groups (for project view) ────────────────────────────────────
const projectGroups = useMemo(() => {
const projectGroupMap = new Map<string, ProjectGroup>();
const resourceRowMapByProject = new Map<
string,
Map<string, ProjectGroup["resourceRows"][number]>
>();
const chapterFilter = new Set(filters.chapters);
const eidFilter = new Set(filters.eids);
const clientFilter = new Set(filters.clientIds);
const projectFilter = new Set(filters.projectIds);
const allGroupEntries: TimelineProjectEntry[] = [...visibleAssignments, ...visibleDemands];
for (const entry of allGroupEntries) {
let group = projectGroupMap.get(entry.projectId);
@@ -537,43 +566,37 @@ export function TimelineProvider({
resourceRows: [],
};
projectGroupMap.set(entry.projectId, group);
resourceRowMapByProject.set(entry.projectId, new Map());
}
const currentGroup = group;
if (!currentGroup) continue;
if (entry.kind === "assignment" && entry.resourceId) {
const existingRow = currentGroup.resourceRows.find(
(r) => r.resource.id === entry.resourceId,
);
const rowMap = resourceRowMapByProject.get(entry.projectId);
const existingRow = rowMap?.get(entry.resourceId);
if (existingRow) {
existingRow.allocs.push(entry);
} else {
const res = resourceMap.get(entry.resourceId);
if (res) {
currentGroup.resourceRows.push({ resource: res, allocs: [entry] });
const row = { resource: res, allocs: [entry] };
currentGroup.resourceRows.push(row);
rowMap?.set(entry.resourceId, row);
}
}
}
}
for (const group of projectGroupMap.values()) {
group.resourceRows = group.resourceRows.filter(({ resource, allocs }) => {
if (filters.chapters.length > 0) {
if (!resource.chapter || !filters.chapters.includes(resource.chapter)) {
group.resourceRows = group.resourceRows.filter(({ resource }) => {
if (chapterFilter.size > 0) {
if (!resource.chapter || !chapterFilter.has(resource.chapter)) {
return false;
}
}
if (filters.eids.length > 0 && !filters.eids.includes(resource.eid)) {
if (eidFilter.size > 0 && !eidFilter.has(resource.eid)) {
return false;
}
if (filters.clientIds.length > 0) {
const matchesClient = allocs.some(
(alloc) => {
const clientId = alloc.project.clientId;
return typeof clientId === "string" && filters.clientIds.includes(clientId);
},
);
if (!matchesClient) {
return false;
}
if (clientFilter.size > 0 && (!group.clientId || !clientFilter.has(group.clientId))) {
return false;
}
return true;
});
@@ -584,18 +607,18 @@ export function TimelineProvider({
return [...projectGroupMap.values()]
.sort((a, b) => a.startDate.getTime() - b.startDate.getTime())
.filter((pg) => {
if (filters.projectIds.length > 0 && !filters.projectIds.includes(pg.id)) return false;
if (projectFilter.size > 0 && !projectFilter.has(pg.id)) return false;
if (
filters.clientIds.length > 0 &&
(!pg.clientId || !filters.clientIds.includes(pg.clientId))
clientFilter.size > 0 &&
(!pg.clientId || !clientFilter.has(pg.clientId))
)
return false;
if (
filters.chapters.length > 0 &&
chapterFilter.size > 0 &&
pg.resourceRows.length === 0
)
return false;
if (filters.eids.length > 0 && pg.resourceRows.length === 0)
if (eidFilter.size > 0 && pg.resourceRows.length === 0)
return false;
return true;
});
@@ -4,6 +4,7 @@ import { createPortal } from "react-dom";
import { useMemo, useState, type ReactNode } from "react";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { useAnchoredOverlay } from "~/hooks/useAnchoredOverlay.js";
import { useReferenceData } from "~/hooks/useReferenceData.js";
import { trpc } from "~/lib/trpc/client.js";
import type { TimelineFilters } from "./TimelineFilter.js";
@@ -105,6 +106,7 @@ interface TimelineQuickFiltersProps {
export function TimelineQuickFilters({ filters, onChange }: TimelineQuickFiltersProps) {
const [eidSearch, setEidSearch] = useState("");
const { clients, countries } = useReferenceData({ clients: true, countries: true });
const { data: resourceData } = trpc.resource.list.useQuery(
{ isActive: true, limit: 500 },
{ staleTime: 60_000 },
@@ -113,15 +115,6 @@ export function TimelineQuickFilters({ filters, onChange }: TimelineQuickFilters
{ isActive: true, search: eidSearch, limit: 100 },
{ staleTime: 15_000 },
);
const { data: clientsData } = trpc.clientEntity.list.useQuery(
{ isActive: true },
{ staleTime: 60_000 },
);
const { data: countriesData } = trpc.country.list.useQuery(
{ isActive: true },
{ staleTime: 60_000 },
);
const resources = ((resourceData?.resources as ResourceOption[] | undefined) ?? []).slice();
const eidSuggestions = (
(eidSearchData?.resources as ResourceOption[] | undefined) ??
@@ -140,22 +133,6 @@ export function TimelineQuickFilters({ filters, onChange }: TimelineQuickFilters
[resources],
);
const clients = useMemo(
() =>
((clientsData ?? []) as ClientOption[])
.filter((client) => client.isActive !== false)
.map((client) => ({ id: client.id, name: client.name, code: client.code })),
[clientsData],
);
const countries = useMemo(
() =>
((countriesData ?? []) as Array<{ id: string; code: string; name: string }>)
.map((c) => ({ id: c.id, code: c.code, name: c.name }))
.sort((a, b) => a.name.localeCompare(b.name)),
[countriesData],
);
const resourceMap = useMemo(
() => new Map(resources.map((resource) => [resource.eid, resource])),
[resources],