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
+48
View File
@@ -291,6 +291,54 @@ export const resourceRouter = createTRPCRouter({
return { resources, total, page, limit, nextCursor };
}),
/** Lightweight resource card for hover tooltips on the timeline. */
getHoverCard: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
const resource = await ctx.db.resource.findUnique({
where: { id: input.id },
select: {
id: true,
displayName: true,
eid: true,
email: true,
chapter: true,
lcrCents: true,
ucrCents: true,
currency: true,
chargeabilityTarget: true,
skills: true,
availability: true,
isActive: true,
areaRole: { select: { id: true, name: true, color: true } },
country: { select: { name: true, code: true } },
managementLevel: { select: { name: true } },
resourceType: true,
},
});
if (!resource) {
throw new TRPCError({ code: "NOT_FOUND", message: "Resource not found" });
}
const directory = await getAnonymizationDirectory(ctx.db);
const anon = anonymizeResource(resource, directory);
return {
id: anon.id,
displayName: anon.displayName ?? "",
eid: anon.eid ?? "",
chapter: resource.chapter,
lcrCents: resource.lcrCents,
ucrCents: resource.ucrCents,
currency: resource.currency,
chargeabilityTarget: resource.chargeabilityTarget,
skills: resource.skills as Record<string, unknown>[],
isActive: resource.isActive,
resourceType: resource.resourceType,
areaRole: resource.areaRole,
country: resource.country,
managementLevel: resource.managementLevel,
};
}),
getById: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {