chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -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];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user