feat: timeline multi-select, demand popover, resource hover card, merged tooltips, dark mode fixes

Major timeline enhancements:
- Right-click drag multi-selection with floating action bar (batch delete/assign)
- DemandPopover for demand strip details (replaces broken "Loading" modal)
- ResourceHoverCard on name hover showing skills, rates, role, chapter
- Merged heatmap+vacation tooltips into unified TimelineTooltip component
- Fixed overbooking blink animation (date normalization, z-index ordering)
- Fixed dark mode sticky column bleed-through in project view
- System roles admin page, notification task management, performance review docs

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-18 23:43:51 +01:00
parent d0f04f13f8
commit ddec3a927a
67 changed files with 4930 additions and 1166 deletions
+36 -3
View File
@@ -15,16 +15,47 @@ export interface TRPCContext {
session: Session | null;
db: typeof prisma;
dbUser: { id: string; systemRole: string; permissionOverrides: unknown } | null;
roleDefaults: Record<string, PermissionKey[]> | null;
}
// Cache role defaults for 60 seconds to avoid DB hit on every request
let _roleDefaultsCache: Record<string, PermissionKey[]> | null = null;
let _roleDefaultsCacheTime = 0;
const ROLE_DEFAULTS_TTL = 60_000;
export async function loadRoleDefaults(): Promise<Record<string, PermissionKey[]>> {
const now = Date.now();
if (_roleDefaultsCache && now - _roleDefaultsCacheTime < ROLE_DEFAULTS_TTL) {
return _roleDefaultsCache;
}
const configs = await prisma.systemRoleConfig.findMany({
select: { role: true, defaultPermissions: true },
});
const map: Record<string, PermissionKey[]> = {};
for (const c of configs) {
map[c.role] = c.defaultPermissions as PermissionKey[];
}
_roleDefaultsCache = map;
_roleDefaultsCacheTime = now;
return map;
}
/** Invalidate the role defaults cache (call after updating SystemRoleConfig) */
export function invalidateRoleDefaultsCache(): void {
_roleDefaultsCache = null;
_roleDefaultsCacheTime = 0;
}
export function createTRPCContext(opts: {
session: Session | null;
dbUser?: { id: string; systemRole: string; permissionOverrides: unknown } | null;
roleDefaults?: Record<string, PermissionKey[]> | null;
}): TRPCContext {
return {
session: opts.session,
db: prisma,
dbUser: opts.dbUser ?? null,
roleDefaults: opts.roleDefaults ?? null,
};
}
@@ -86,7 +117,8 @@ export const managerProcedure = protectedProcedure.use(({ ctx, next }) => {
}
const permissions = resolvePermissions(
user.systemRole as SystemRole,
user.permissionOverrides as import("@planarchy/shared").PermissionOverrides | null
user.permissionOverrides as import("@planarchy/shared").PermissionOverrides | null,
ctx.roleDefaults ?? undefined,
);
return next({ ctx: { ...ctx, user, permissions } });
});
@@ -104,7 +136,8 @@ export const controllerProcedure = protectedProcedure.use(({ ctx, next }) => {
}
const permissions = resolvePermissions(
user.systemRole as SystemRole,
user.permissionOverrides as import("@planarchy/shared").PermissionOverrides | null
user.permissionOverrides as import("@planarchy/shared").PermissionOverrides | null,
ctx.roleDefaults ?? undefined,
);
return next({ ctx: { ...ctx, user, permissions } });
});
@@ -117,7 +150,7 @@ export const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
if (!user || user.systemRole !== SystemRole.ADMIN) {
throw new TRPCError({ code: "FORBIDDEN", message: "Admin role required" });
}
const permissions = resolvePermissions(SystemRole.ADMIN, null);
const permissions = resolvePermissions(SystemRole.ADMIN, null, ctx.roleDefaults ?? undefined);
return next({ ctx: { ...ctx, user, permissions } });
});