chore(repo): initialize planarchy workspace

This commit is contained in:
2026-03-14 14:31:09 +01:00
commit dd55d0e78b
769 changed files with 166461 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
import type { ColumnDef } from "../types/columns.js";
export const RESOURCE_COLUMNS: ColumnDef[] = [
{ key: "displayName", label: "Name", defaultVisible: true, hideable: false },
{ key: "eid", label: "EID", defaultVisible: true, hideable: true },
{ key: "chapter", label: "Chapter", defaultVisible: true, hideable: true },
{ key: "roles", label: "Roles", defaultVisible: true, hideable: true },
{ key: "chargeability", label: "Chargeability", defaultVisible: true, hideable: true, sortable: true },
{ key: "lcr", label: "LCR", defaultVisible: false, hideable: true },
{ key: "valueScore", label: "Score", defaultVisible: false, hideable: true },
{ key: "isActive", label: "Status", defaultVisible: true, hideable: true },
];
export const PROJECT_COLUMNS: ColumnDef[] = [
{ key: "shortCode", label: "Code", defaultVisible: true, hideable: false },
{ key: "name", label: "Name", defaultVisible: true, hideable: false },
{ key: "status", label: "Status", defaultVisible: true, hideable: true },
{ key: "orderType", label: "Type", defaultVisible: true, hideable: true },
{ key: "dates", label: "Dates", defaultVisible: true, hideable: true },
{ key: "budget", label: "Budget", defaultVisible: false, hideable: true },
{ key: "allocations", label: "Allocations", defaultVisible: true, hideable: true },
{ key: "responsible", label: "Responsible", defaultVisible: false, hideable: true },
];
export const ALLOCATION_COLUMNS: ColumnDef[] = [
{ key: "resource", label: "Resource", defaultVisible: true, hideable: false },
{ key: "project", label: "Project", defaultVisible: true, hideable: false },
{ key: "dates", label: "Dates", defaultVisible: true, hideable: true },
{ key: "hoursPerDay", label: "h/day", defaultVisible: true, hideable: true },
{ key: "status", label: "Status", defaultVisible: true, hideable: true },
{ key: "cost", label: "Daily Cost", defaultVisible: false, hideable: true },
{ key: "role", label: "Role", defaultVisible: true, hideable: true },
];
@@ -0,0 +1,178 @@
import type { WeekdayAvailability } from "../types/resource.js";
export const DISPO_RESOURCE_CHAPTER_BY_TOKEN = {
"2D": "Digital Content Production",
"3D": "Digital Content Production",
PM: "Project Management",
AD: "Art Direction",
} as const;
export const DISPO_ASSIGNMENT_ROLE_BY_TOKEN = {
"2D": "2D Artist",
"3D": "3D Artist",
PM: "Project Manager",
AD: "Art Director",
} as const;
export const DISPO_UTILIZATION_CODE_BY_TOKEN = {
CH: "Chg",
MO: "M&O",
MD: "MD&I",
PD: "PD&R",
AB: "Absence",
NA: "Absence",
UN: null,
} as const;
export const DISPO_UTILIZATION_CATEGORIES = [
{
code: "Chg",
name: "Chargeable",
description: "Billable client project work",
sortOrder: 1,
isDefault: true,
},
{
code: "BD",
name: "Business Development",
description: "Sales, proposals, presales activities",
sortOrder: 2,
isDefault: false,
},
{
code: "MD&I",
name: "Market Development & Initiatives",
description: "R&D, innovation, market development",
sortOrder: 3,
isDefault: false,
},
{
code: "M&O",
name: "Management & Operations",
description: "Internal admin, management overhead",
sortOrder: 4,
isDefault: false,
},
{
code: "PD&R",
name: "People Development & Recruitment",
description: "Training, hiring, onboarding",
sortOrder: 5,
isDefault: false,
},
{
code: "Absence",
name: "Absence & Non Standard",
description: "Vacation, illness, non-standard leave (reduces SAH)",
sortOrder: 6,
isDefault: false,
},
] as const;
export const DISPO_INTERNAL_PROJECT_BUCKETS = [
{
sourceToken: "MO",
shortCode: "INT-MO",
name: "Management & Operations",
utilizationCategoryCode: "M&O",
},
{
sourceToken: "MD",
shortCode: "INT-MD",
name: "Market Development & Initiatives",
utilizationCategoryCode: "MD&I",
},
{
sourceToken: "PD",
shortCode: "INT-PD",
name: "People Development & Recruitment",
utilizationCategoryCode: "PD&R",
},
] as const;
export const DISPO_REQUIRED_ROLE_SEEDS = [
{
name: "2D Artist",
description: "Canonical Dispo assignment role for 2D delivery work",
},
{
name: "3D Artist",
description: "Canonical Dispo assignment role for 3D delivery work",
},
{
name: "Project Manager",
description: "Canonical Dispo assignment role for project management delivery",
},
{
name: "Art Director",
description: "Canonical Dispo assignment role for art direction delivery",
},
] as const;
export type DispoRoleToken = keyof typeof DISPO_ASSIGNMENT_ROLE_BY_TOKEN;
export type DispoChapterToken = keyof typeof DISPO_RESOURCE_CHAPTER_BY_TOKEN;
export type DispoUtilizationToken = keyof typeof DISPO_UTILIZATION_CODE_BY_TOKEN;
export type DispoInternalBucketDefinition = (typeof DISPO_INTERNAL_PROJECT_BUCKETS)[number];
function normalizeDispoTokenValue(value: string): string {
return value.trim().replace(/\s+/g, " ").toUpperCase();
}
function roundToTwoDecimals(value: number): number {
return Math.round(value * 100) / 100;
}
export function normalizeCanonicalResourceIdentity(value: string): string {
return value.trim().toLowerCase();
}
export function resolveCanonicalResourceIdentity(enterpriseId?: string | null, eid?: string | null) {
const normalizedEnterpriseId = enterpriseId ? normalizeCanonicalResourceIdentity(enterpriseId) : null;
const normalizedEid = eid ? normalizeCanonicalResourceIdentity(eid) : null;
const conflict = Boolean(
normalizedEnterpriseId &&
normalizedEid &&
normalizedEnterpriseId !== normalizedEid,
);
return {
canonicalId: conflict ? null : normalizedEnterpriseId ?? normalizedEid,
normalizedEnterpriseId,
normalizedEid,
conflict,
};
}
export function normalizeDispoChapterToken(token?: string | null): string | null {
if (!token) return null;
const normalizedToken = normalizeDispoTokenValue(token) as DispoChapterToken;
return DISPO_RESOURCE_CHAPTER_BY_TOKEN[normalizedToken] ?? null;
}
export function normalizeDispoRoleToken(token?: string | null): string | null {
if (!token) return null;
const normalizedToken = normalizeDispoTokenValue(token) as DispoRoleToken;
return DISPO_ASSIGNMENT_ROLE_BY_TOKEN[normalizedToken] ?? null;
}
export function normalizeDispoUtilizationToken(token?: string | null): string | null {
if (!token) return null;
const normalizedToken = normalizeDispoTokenValue(token) as DispoUtilizationToken;
return DISPO_UTILIZATION_CODE_BY_TOKEN[normalizedToken] ?? null;
}
export function createWeekdayAvailabilityFromFte(
fte: number,
dailyWorkingHours = 8,
): WeekdayAvailability {
const safeFte = Math.min(Math.max(fte, 0), 1);
const hoursPerDay = roundToTwoDecimals(dailyWorkingHours * safeFte);
return {
monday: hoursPerDay,
tuesday: hoursPerDay,
wednesday: hoursPerDay,
thursday: hoursPerDay,
friday: hoursPerDay,
};
}
@@ -0,0 +1,141 @@
/**
* German federal states (Bundesländer) with abbreviations.
*/
export const GERMAN_FEDERAL_STATES: Record<string, string> = {
BB: "Brandenburg",
BE: "Berlin",
BW: "Baden-Württemberg",
BY: "Bayern",
HB: "Bremen",
HE: "Hessen",
HH: "Hamburg",
MV: "Mecklenburg-Vorpommern",
NI: "Niedersachsen",
NW: "Nordrhein-Westfalen",
RP: "Rheinland-Pfalz",
SH: "Schleswig-Holstein",
SL: "Saarland",
SN: "Sachsen",
ST: "Sachsen-Anhalt",
TH: "Thüringen",
};
/**
* PLZ prefix → state mapping (first 2 digits, ~85% accuracy).
* Source: DE postal code allocation by Deutsche Post.
*/
const PLZ_PREFIX_MAP: Record<number, string> = {
1: "BE", // 1xxxx mostly Berlin
2: "BE", // 02xxx Cottbus area, approximation
3: "BB", // 03xxx
4: "MV", // 04xxx close enough
5: "SN", // 04-05xxx Sachsen
6: "ST", // 06xxx Sachsen-Anhalt
7: "TH", // 07xxx Thüringen
8: "SN", // 08xxx Sachsen (Chemnitz region)
9: "BY", // 09xxx Bayern (Ingolstadt south)
10: "BE",
11: "BE",
12: "BE",
13: "BE",
14: "BB",
15: "BB",
16: "BB",
17: "MV",
18: "MV",
19: "MV",
20: "HH",
21: "HH",
22: "HH",
23: "SH",
24: "SH",
25: "SH",
26: "NI",
27: "NI",
28: "HB",
29: "NI",
30: "NI",
31: "NI",
32: "NW",
33: "NW",
34: "HE",
35: "HE",
36: "HE",
37: "NI",
38: "NI",
39: "ST",
40: "NW",
41: "NW",
42: "NW",
44: "NW",
45: "NW",
46: "NW",
47: "NW",
48: "NW",
49: "NI",
50: "NW",
51: "NW",
52: "NW",
53: "NW",
54: "RP",
55: "RP",
56: "RP",
57: "NW",
58: "NW",
59: "NW",
60: "HE",
61: "HE",
63: "HE",
64: "HE",
65: "HE",
66: "SL",
67: "RP",
68: "BW",
69: "BW",
70: "BW",
71: "BW",
72: "BW",
73: "BW",
74: "BW",
75: "BW",
76: "BW",
77: "BW",
78: "BW",
79: "BW",
80: "BY",
81: "BY",
82: "BY",
83: "BY",
84: "BY",
85: "BY",
86: "BY",
87: "BY",
88: "BW",
89: "BY",
90: "BY",
91: "BY",
92: "BY",
93: "BY",
94: "BY",
95: "BY",
96: "BY",
97: "BY",
98: "TH",
99: "TH",
};
/**
* Infer federal state abbreviation from a German postal code (5 digits).
* Returns undefined if the format is invalid or no match found.
*/
export function inferStateFromPostalCode(plz: string): string | undefined {
const code = plz.trim().replace(/\s/g, "");
if (!/^\d{5}$/.test(code)) return undefined;
// Try 2-digit prefix first, then 1-digit as fallback
const prefix2 = parseInt(code.slice(0, 2), 10);
if (PLZ_PREFIX_MAP[prefix2] !== undefined) return PLZ_PREFIX_MAP[prefix2];
const prefix1 = parseInt(code.slice(0, 1), 10);
return PLZ_PREFIX_MAP[prefix1];
}
+61
View File
@@ -0,0 +1,61 @@
export * from "./germanStates.js";
export * from "./publicHolidays.js";
export * from "./columns.js";
export * from "./dispo-import.js";
export const BUDGET_WARNING_THRESHOLDS = {
INFO: 70,
WARNING: 85,
CRITICAL: 95,
} as const;
export const DEFAULT_WORKING_HOURS_PER_DAY = 8;
export const DEFAULT_AVAILABILITY = {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
} as const;
export const VALUE_SCORE_WEIGHTS = {
SKILL_DEPTH: 0.30,
SKILL_BREADTH: 0.15,
COST_EFFICIENCY: 0.25,
CHARGEABILITY: 0.15,
EXPERIENCE: 0.15,
} as const;
export const SCORE_WEIGHTS = {
SKILL: 0.4,
AVAILABILITY: 0.3,
COST: 0.2,
UTILIZATION: 0.1,
} as const;
export const PAGINATION_DEFAULTS = {
PAGE: 1,
LIMIT: 50,
MAX_LIMIT: 500,
} as const;
export const SSE_EVENT_TYPES = {
ALLOCATION_CREATED: "allocation.created",
ALLOCATION_UPDATED: "allocation.updated",
ALLOCATION_DELETED: "allocation.deleted",
PROJECT_SHIFTED: "project.shifted",
BUDGET_WARNING: "budget.warning",
VACATION_CREATED: "vacation.created",
VACATION_UPDATED: "vacation.updated",
VACATION_DELETED: "vacation.deleted",
ROLE_CREATED: "role.created",
ROLE_UPDATED: "role.updated",
ROLE_DELETED: "role.deleted",
NOTIFICATION_CREATED: "notification:created",
PING: "ping",
} as const;
export type SseEventType = (typeof SSE_EVENT_TYPES)[keyof typeof SSE_EVENT_TYPES];
export const SSE_NOTIFICATION_CREATED = SSE_EVENT_TYPES.NOTIFICATION_CREATED;
@@ -0,0 +1,129 @@
/**
* German public holiday calculator.
* Supports federal holidays + Bavaria (BY) specific holidays.
*
* Easter-based dates use the Gauss/Meeus algorithm.
*/
export interface PublicHoliday {
date: string; // ISO "YYYY-MM-DD"
name: string;
federal: boolean; // true = all states; false = state-specific
states?: string[]; // which state abbreviations observe this holiday
}
/**
* Compute Easter Sunday date for a given year (Gregorian calendar).
* Uses the Anonymous Gregorian algorithm.
*/
function computeEaster(year: number): Date {
const a = year % 19;
const b = Math.floor(year / 100);
const c = year % 100;
const d = Math.floor(b / 4);
const e = b % 4;
const f = Math.floor((b + 8) / 25);
const g = Math.floor((b - f + 1) / 3);
const h = (19 * a + b - d - g + 15) % 30;
const i = Math.floor(c / 4);
const k = c % 4;
const l = (32 + 2 * e + 2 * i - h - k) % 7;
const m = Math.floor((a + 11 * h + 22 * l) / 451);
const month = Math.floor((h + l - 7 * m + 114) / 31); // 1-based
const day = ((h + l - 7 * m + 114) % 31) + 1;
return new Date(Date.UTC(year, month - 1, day));
}
function addDays(date: Date, days: number): Date {
const d = new Date(date);
d.setUTCDate(d.getUTCDate() + days);
return d;
}
function fmt(date: Date): string {
return date.toISOString().slice(0, 10);
}
function fixed(year: number, month: number, day: number): string {
return fmt(new Date(Date.UTC(year, month - 1, day)));
}
/**
* Return all public holidays for a given year and optional state.
* When state is omitted, returns federal holidays only.
* When state is provided (e.g. "BY"), returns federal + state-specific holidays.
*/
export function getPublicHolidays(year: number, state?: string): PublicHoliday[] {
const easter = computeEaster(year);
const holidays: PublicHoliday[] = [
// Federal holidays (all states)
{ date: fixed(year, 1, 1), name: "Neujahr", federal: true },
{ date: fixed(year, 5, 1), name: "Tag der Arbeit", federal: true },
{ date: fixed(year, 10, 3), name: "Tag der Deutschen Einheit", federal: true },
{ date: fixed(year, 12, 25), name: "1. Weihnachtstag", federal: true },
{ date: fixed(year, 12, 26), name: "2. Weihnachtstag", federal: true },
// Easter-based federal holidays
{ date: fmt(addDays(easter, -2)), name: "Karfreitag", federal: true },
{ date: fmt(easter), name: "Ostersonntag", federal: true },
{ date: fmt(addDays(easter, 1)), name: "Ostermontag", federal: true },
{ date: fmt(addDays(easter, 39)), name: "Christi Himmelfahrt", federal: true },
{ date: fmt(addDays(easter, 49)), name: "Pfingstsonntag", federal: true },
{ date: fmt(addDays(easter, 50)), name: "Pfingstmontag", federal: true },
// Bavaria-specific (BY)
{
date: fixed(year, 1, 6),
name: "Heilige Drei Könige",
federal: false,
states: ["BY", "BW", "ST"],
},
{
date: fmt(addDays(easter, 60)),
name: "Fronleichnam",
federal: false,
states: ["BY", "BW", "HE", "NW", "RP", "SL"],
},
{
date: fixed(year, 8, 15),
name: "Mariä Himmelfahrt",
federal: false,
states: ["BY", "SL"],
},
{
date: fixed(year, 11, 1),
name: "Allerheiligen",
federal: false,
states: ["BY", "BW", "NW", "RP", "SL"],
},
// Other state-specific (not BY but included for completeness)
{
date: fixed(year, 10, 31),
name: "Reformationstag",
federal: false,
states: ["BB", "HB", "HH", "MV", "NI", "SH", "SN", "ST", "TH"],
},
{
date: fixed(year, 11, 18),
name: "Buß- und Bettag",
federal: false,
states: ["SN"],
},
];
if (!state) {
return holidays.filter((h) => h.federal);
}
return holidays.filter((h) => h.federal || h.states?.includes(state));
}
/**
* Check if a given date (ISO string or Date) is a public holiday.
*/
export function isPublicHoliday(date: Date | string, state?: string): boolean {
const d = typeof date === "string" ? date : date.toISOString().slice(0, 10);
const year = parseInt(d.slice(0, 4), 10);
return getPublicHolidays(year, state).some((h) => h.date === d);
}