chore(repo): initialize planarchy workspace
This commit is contained in:
BIN
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
@@ -0,0 +1,322 @@
|
||||
# Plan: Chargeability Report
|
||||
|
||||
**Date:** 2026-03-13
|
||||
**Status:** Draft
|
||||
**Depends on:** Country/SAH, OrgUnit hierarchy, Utilization Categories, Client/WBS, Resource extensions
|
||||
|
||||
## Problem
|
||||
|
||||
The bi-weekly chargeability report is currently produced in Excel. Planarchy needs a **live reporting section** in the app that updates in real-time as assignments, resources, and SAH change. The report is not a static file — it is an interactive page that can be **exported** as Excel or PDF on demand.
|
||||
|
||||
Core requirements:
|
||||
|
||||
- Live FTE-weighted chargeability percentages grouped by org unit hierarchy
|
||||
- Historical actuals (SAP-imported or tracked) alongside forward forecasts
|
||||
- Region filtering (GER only vs ALL resource types)
|
||||
- Target vs actual comparison per management level and chapter
|
||||
- Business Development % as a separate metric
|
||||
- Change tracking (delta vs previous forecast snapshot)
|
||||
- Excel and PDF export of the current view
|
||||
|
||||
## Report Structure (from Excel)
|
||||
|
||||
The report screenshot shows this layout:
|
||||
|
||||
```
|
||||
Chg Report Content Prod. // - All
|
||||
Status as of: {date}
|
||||
|
||||
| Region | Org Unit (L6) | Chapter (L7) | Ressource Type | FTE | Target ACN | tracked Chg % (SAP) | predicted Chg % (forecast months) |
|
||||
```
|
||||
|
||||
### Row Hierarchy
|
||||
|
||||
**GER section** (Production Studios only):
|
||||
|
||||
```
|
||||
GER | Content Production | Content Production | Total | FTE | Target | Jan | Feb | Mar | Apr | ...
|
||||
GER | CGI Content | Art Direction | | FTE | Target | ... |
|
||||
GER | CGI Content | Capability Dev | | FTE | Target | ... |
|
||||
GER | CGI Content | CGI Production | | FTE | Target | ... |
|
||||
GER | CGI Content | Product Data Mgmt | | FTE | Target | ... |
|
||||
GER | CGI Content | Program/Delivery | | FTE | Target | ... |
|
||||
GER | CGI Content | CGI Content | Total | FTE | Target | ... | ← L6 subtotal
|
||||
GER | CGI Technology | CGI Development | | FTE | Target | ... |
|
||||
GER | CGI Technology | IT Development | | FTE | Target | ... |
|
||||
GER | CGI Technology | CGI Technology | Total | FTE | Target | ... |
|
||||
... (Creative Content Production, VFX)
|
||||
GER | Content Prod. | Total | Chg Germany | FTE | Target | ... | ← grand total
|
||||
GER | Content Prod. | Total | BD Germany | | | ... | ← BD% row
|
||||
```
|
||||
|
||||
**PLUS row**: additional CHG for Near&Offshore + ACN FTEs
|
||||
|
||||
**ALL row**: combined total
|
||||
|
||||
**Change tracking**: previous forecast date + delta rows
|
||||
|
||||
### Column Structure
|
||||
|
||||
| Column Group | Type | Source |
|
||||
|---|---|---|
|
||||
| FTE | number | Sum of resource FTE for the group |
|
||||
| Target ACN | % | Official target per management level, FTE-weighted |
|
||||
| Historical months (SAP) | tracked Chg % | Imported actual chargeability from SAP/period data |
|
||||
| Forecast months | predicted Chg % | FTE-weighted average of individual resource forecasts |
|
||||
|
||||
### Key Formulas
|
||||
|
||||
**FTE per chapter:**
|
||||
```
|
||||
SUM(resource.fte) WHERE resource.orgUnit.level7 = chapter
|
||||
AND resource.resourceType = filter
|
||||
```
|
||||
|
||||
**Chargeability per chapter (forecast):**
|
||||
```
|
||||
SUM(resource.fte * resource.forecastChargeability[month])
|
||||
/ SUM(resource.fte)
|
||||
```
|
||||
This is an FTE-weighted average.
|
||||
|
||||
**Target per chapter:**
|
||||
```
|
||||
SUM(resource.fte * resource.managementLevel.targetPercentage)
|
||||
/ SUM(resource.fte)
|
||||
```
|
||||
|
||||
**Unassigned hours (implicit):**
|
||||
```
|
||||
SAH - sum(all categorized hours)
|
||||
```
|
||||
|
||||
**BD% (Germany):**
|
||||
```
|
||||
SUM(resource.fte * resource.bdPercentage[month])
|
||||
/ SUM(resource.fte)
|
||||
```
|
||||
|
||||
## Data Requirements
|
||||
|
||||
### What Planarchy needs to have (per resource, per month)
|
||||
|
||||
| Data Point | Source | Notes |
|
||||
|---|---|---|
|
||||
| FTE | Resource.fte | May vary monthly |
|
||||
| Org Unit (L5/L6/L7) | Resource.orgUnit + tree | Drives row grouping |
|
||||
| Country / Metro City | Resource.country | Drives region filter (GER vs ALL) |
|
||||
| Resource Type | Derived or stored | Production Studios / Near&Offshore / Accenture |
|
||||
| Management Level | Resource.managementLevel | Drives target % |
|
||||
| Target % | ManagementLevel.targetPercentage | Official chargeability target |
|
||||
| Forecast Chargeability | **Derived from assignments** | Hours assigned to Chg projects / SAH |
|
||||
| Forecast BD% | **Derived from assignments** | Hours assigned to BD projects / SAH |
|
||||
| Tracked Chargeability | **Imported from SAP** or tracked in-app | Actual period data |
|
||||
|
||||
### Forecast Chargeability Derivation
|
||||
|
||||
This is the key insight: **predicted chargeability can be derived from what Planarchy already knows**:
|
||||
|
||||
```
|
||||
forecastChg(resource, month) =
|
||||
sum(assignment.hoursPerDay * workingDays)
|
||||
WHERE assignment.project.utilizationCategory = 'Chg'
|
||||
AND assignment overlaps month
|
||||
/ SAH(resource, month)
|
||||
```
|
||||
|
||||
Similarly for BD, MD&I, etc. — each utilization category's hours divided by SAH.
|
||||
|
||||
This means the chargeability report is a **query over existing assignments + SAH**, not a separate data entry.
|
||||
|
||||
### Tracked (Actual) Chargeability
|
||||
|
||||
For historical data, two options:
|
||||
|
||||
1. **Import from SAP**: bulk import of period data (P-1, P-2, etc.) as snapshots
|
||||
2. **Track in-app**: if Planarchy becomes the system of record for time tracking
|
||||
|
||||
Recommendation: Start with SAP import. Add a `ChargeabilitySnapshot` model for imported actuals.
|
||||
|
||||
```prisma
|
||||
model ChargeabilitySnapshot {
|
||||
id String @id @default(cuid())
|
||||
periodDate DateTime // e.g. 2026-02-28
|
||||
periodType String // "PTD" or "MTD"
|
||||
orgUnitId String // L7 chapter
|
||||
resourceType String? // filter dimension
|
||||
fte Float
|
||||
chargeability Float // 0.0 to 1.0
|
||||
bdPercentage Float?
|
||||
mdiPercentage Float?
|
||||
moPercentage Float?
|
||||
recoveryRate Float?
|
||||
importedAt DateTime @default(now())
|
||||
|
||||
@@unique([periodDate, periodType, orgUnitId, resourceType])
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
Location: `packages/api/src/router/chargeability-report.ts`
|
||||
|
||||
| Procedure | Access | Description |
|
||||
|---|---|---|
|
||||
| `getReport` | manager | Full chargeability report for a date range and region filter |
|
||||
| `getResourceForecast` | manager | Per-resource monthly forecast data (for the ChgFC-equivalent) |
|
||||
| `importActuals` | admin | Bulk import SAP period data |
|
||||
| `getSnapshots` | manager | List imported actuals for a period |
|
||||
| `getChangeTracking` | manager | Delta between current forecast and a previous snapshot date |
|
||||
|
||||
### `getReport` Response Shape
|
||||
|
||||
```typescript
|
||||
interface ChargeabilityReportRow {
|
||||
region: string; // "GER" or "ALL"
|
||||
orgUnitL6: string;
|
||||
chapter: string; // L7 name
|
||||
resourceType?: string; // for ALL section sub-rows
|
||||
isSubtotal: boolean;
|
||||
fte: number;
|
||||
targetACN: number; // FTE-weighted target %
|
||||
months: {
|
||||
month: string; // "2026-03"
|
||||
dataType: 'SAP' | 'MTD'; // actual vs forecast
|
||||
fte: number;
|
||||
chargeabilityPercent: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface ChargeabilityReport {
|
||||
statusDate: string;
|
||||
gerRows: ChargeabilityReportRow[];
|
||||
bdGermany: { month: string; bdPercent: number }[];
|
||||
plusRow: { fte: number; months: { month: string; chargeabilityPercent: number }[] };
|
||||
allTotalRow: ChargeabilityReportRow;
|
||||
changeTracking?: {
|
||||
previousDate: string;
|
||||
gerDelta: { month: string; delta: number }[];
|
||||
allDelta: { month: string; delta: number }[];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Engine: Chargeability Calculator
|
||||
|
||||
Location: `packages/engine/src/chargeability/calculator.ts`
|
||||
|
||||
Pure functions for:
|
||||
|
||||
```typescript
|
||||
// FTE-weighted chargeability for a group
|
||||
function calculateGroupChargeability(
|
||||
resources: { fte: number; chargeability: number }[]
|
||||
): number;
|
||||
|
||||
// Derive forecast chargeability from assignments + SAH
|
||||
function deriveResourceForecast(
|
||||
assignments: { hoursPerDay: number; startDate: Date; endDate: Date; utilizationCategory: string }[],
|
||||
sah: SAHResult,
|
||||
month: { start: Date; end: Date }
|
||||
): { chg: number; bd: number; mdi: number; mo: number; pdr: number; unassigned: number };
|
||||
|
||||
// FTE-weighted target for a group
|
||||
function calculateGroupTarget(
|
||||
resources: { fte: number; targetPercentage: number }[]
|
||||
): number;
|
||||
```
|
||||
|
||||
## UI
|
||||
|
||||
### Live Reporting Section (`/reports/chargeability`)
|
||||
|
||||
This is an **interactive, live-updating page** — not a static export. Data refreshes whenever assignments, resource attributes, or SAH inputs change.
|
||||
|
||||
**Layout:**
|
||||
- Header: title, status date (auto = now), region toggle (GER / ALL)
|
||||
- Filter bar: resource type toggles (Production Studios / Accenture / Near&Offshore), date range (which months to display)
|
||||
- Main table: matching the Excel screenshot structure (see Row Hierarchy above)
|
||||
- Color coding: green background = actual months (SAP data), blue = current month, white = forecast months
|
||||
- Subtotal rows: bold, darker background
|
||||
- BD% row: bottom of GER section
|
||||
- Change tracking section: collapsible, shows delta vs a selectable previous snapshot date
|
||||
- Sticky first columns (Region, Org Unit, Chapter, Resource Type, FTE, Target) with horizontal scroll for months
|
||||
|
||||
**Interactivity:**
|
||||
- Click on a chapter row → drill down to individual resources in that chapter
|
||||
- Click on a resource → navigate to resource detail page
|
||||
- Click on a cell → tooltip showing the contributing assignments and their hours
|
||||
- Compare mode: select two snapshot dates and see a side-by-side delta view
|
||||
|
||||
**Real-time updates:**
|
||||
- SSE integration: report subscribes to assignment/resource change events
|
||||
- When an assignment is created/modified/deleted, affected rows recalculate
|
||||
- Debounced refresh (not per-keystroke, but within seconds of a change)
|
||||
|
||||
### Export (on demand)
|
||||
|
||||
Export buttons in the page header:
|
||||
|
||||
**Excel export:**
|
||||
- Matches the current Excel report format for stakeholder familiarity
|
||||
- Includes all visible rows and columns based on current filter state
|
||||
- Separate sheets for GER and ALL views
|
||||
- Formulas preserved where possible (e.g. subtotals as SUM formulas)
|
||||
- Uses the existing PDF/Excel export infrastructure from Phase 6
|
||||
|
||||
**PDF export:**
|
||||
- Landscape layout matching the on-screen table
|
||||
- Includes header with status date and active filters
|
||||
- Page breaks per L6 section
|
||||
- Color coding preserved
|
||||
|
||||
## Dependencies
|
||||
|
||||
All other plans must land first:
|
||||
1. **Country/SAH** — provides available hours denominator
|
||||
2. **OrgUnit hierarchy** — provides row grouping
|
||||
3. **Utilization Categories** — provides hour bucketing (Chg, BD, etc.)
|
||||
4. **Client/WBS** — provides project attribution
|
||||
5. **Resource extensions** — provides management level, resource type, client unit
|
||||
6. **Management Level model** — provides target percentages
|
||||
|
||||
## Phased Delivery
|
||||
|
||||
### Phase A: Live forecast report
|
||||
- Interactive page at `/reports/chargeability`
|
||||
- Derive chargeability from assignments + SAH (live query, no static file)
|
||||
- Show FTE and predicted Chg% per chapter per month
|
||||
- Region filter (GER/ALL) and resource type toggles
|
||||
- Subtotals and BD% row
|
||||
- SSE subscription for real-time updates
|
||||
|
||||
### Phase B: Target comparison + drill-down
|
||||
- Add management level targets
|
||||
- Show Target ACN column with variance highlighting
|
||||
- Click-to-drill-down into individual resources per chapter
|
||||
- Cell tooltips showing contributing assignments
|
||||
|
||||
### Phase C: Historical actuals + snapshots
|
||||
- SAP actuals import mechanism
|
||||
- Show tracked Chg% for past periods (green columns)
|
||||
- Mixed actual/forecast column display
|
||||
- Snapshot save/compare for change tracking
|
||||
|
||||
### Phase D: Export + client views
|
||||
- Excel export matching stakeholder format (with formulas)
|
||||
- PDF export (landscape, color-coded)
|
||||
- Client-specific report views (e.g. BMW-only filter)
|
||||
- Compare mode (two snapshots side-by-side)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Chargeability report page with GER/ALL toggle
|
||||
- [ ] Rows grouped by L6 → L7 with subtotals
|
||||
- [ ] FTE-weighted chargeability percentages per chapter per month
|
||||
- [ ] Forecast derived from assignments + SAH (not manual entry)
|
||||
- [ ] Target column from management level target percentages
|
||||
- [ ] Resource type sub-rows in ALL section
|
||||
- [ ] BD% row for Germany
|
||||
- [ ] SAP actuals import mechanism
|
||||
- [ ] Change tracking (delta vs previous snapshot)
|
||||
- [ ] Excel export
|
||||
@@ -0,0 +1,149 @@
|
||||
# Plan: Client Model with WBS Hierarchy
|
||||
|
||||
**Date:** 2026-03-13
|
||||
**Status:** Draft
|
||||
**Depends on:** -
|
||||
|
||||
## Problem
|
||||
|
||||
Projects need to be linked to clients for chargeability reporting, budget tracking, and organizational grouping. The Dispo Categories file defines a two-level client hierarchy:
|
||||
|
||||
- **WBS Master Client** — the parent organization (e.g. "BMW", "VOLKSWAGEN")
|
||||
- **WBS Client Name** — the legal entity (e.g. "BMW AG", "Dr. Ing. h.c. F. Porsche AG")
|
||||
|
||||
Currently Planarchy has no Client model. Projects exist independently without client attribution.
|
||||
|
||||
## Data
|
||||
|
||||
35 WBS Master Clients with their legal sub-entities. Examples:
|
||||
|
||||
| WBS Master Client | WBS Client Names |
|
||||
|---|---|
|
||||
| BMW | BMW AG |
|
||||
| VOLKSWAGEN | Audi Business Innovation GmbH, Dr. Ing. h.c. F. Porsche AG, MAN Truck & Bus SE, Volkswagen AG |
|
||||
| DAIMLER | antoni garage GmbH & Co. KG, Mercedes-Benz AG |
|
||||
| EXOR-STELLANTIS | AUTOMOBILES PEUGEOT, FCA Italy S.p.A., Ferrari S.p.A, MASERATI SPA A SOCIO UNICO |
|
||||
|
||||
## Schema
|
||||
|
||||
### New: `Client` model (self-referencing for parent/child)
|
||||
|
||||
```prisma
|
||||
model Client {
|
||||
id String @id @default(cuid())
|
||||
name String // Display name
|
||||
code String? @unique // Optional short code (e.g. "BMW", "VW")
|
||||
parentId String?
|
||||
parent Client? @relation("ClientTree", fields: [parentId], references: [id])
|
||||
children Client[] @relation("ClientTree")
|
||||
isActive Boolean @default(true)
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
projects Project[]
|
||||
|
||||
@@unique([parentId, name])
|
||||
}
|
||||
```
|
||||
|
||||
Design choice: self-referencing tree instead of separate `MasterClient` + `ClientEntity` tables. This supports:
|
||||
- Two levels today (Master → Entity)
|
||||
- Potential deeper nesting in the future
|
||||
- Simple queries via `parentId IS NULL` for top-level clients
|
||||
|
||||
### Project model extension
|
||||
|
||||
```prisma
|
||||
model Project {
|
||||
// ... existing fields ...
|
||||
|
||||
clientId String?
|
||||
client Client? @relation(fields: [clientId], references: [id])
|
||||
}
|
||||
```
|
||||
|
||||
A project links to a WBS Client Name (child level). The master client is derived via `client.parent`.
|
||||
|
||||
## Shared Types
|
||||
|
||||
```typescript
|
||||
// packages/shared/src/types/client.ts
|
||||
|
||||
interface Client {
|
||||
id: string;
|
||||
name: string;
|
||||
code?: string;
|
||||
parentId?: string;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
interface ClientWithChildren extends Client {
|
||||
children: Client[];
|
||||
}
|
||||
|
||||
interface ClientTree extends Client {
|
||||
children: ClientTree[];
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
Location: `packages/api/src/router/client.ts`
|
||||
|
||||
| Procedure | Access | Description |
|
||||
|---|---|---|
|
||||
| `list` | protected | Flat list, optionally filtered by parentId |
|
||||
| `getTree` | protected | Nested tree for UI |
|
||||
| `getById` | protected | Single client with parent/children |
|
||||
| `create` | manager | Create client (top-level or child) |
|
||||
| `update` | manager | Edit name, code, re-parent |
|
||||
| `deactivate` | manager | Soft-delete |
|
||||
|
||||
Manager-level access (not just admin) since project managers typically need to manage client relationships.
|
||||
|
||||
## UI
|
||||
|
||||
### Client Management (`/clients` or `/admin/clients`)
|
||||
|
||||
- Tree view showing Master Clients → WBS Client Names
|
||||
- CRUD at both levels
|
||||
- Search/filter for quick lookup
|
||||
- Badge showing number of linked projects per client
|
||||
|
||||
### Project: Client Assignment
|
||||
|
||||
- `ProjectModal.tsx` gets a cascading or searchable client picker:
|
||||
1. Select or search Master Client
|
||||
2. Select WBS Client Name under that master
|
||||
- `ProjectWizard.tsx` Step 1 includes client selection
|
||||
- Project list shows client name column
|
||||
|
||||
### Client Unit (Resource Attribute)
|
||||
|
||||
The chargeability report also has a "Client Unit" dimension on resources (BMW, Daimler, Porsche, etc.). This represents which client a resource is primarily assigned to. This is separate from project-client linking:
|
||||
|
||||
- **Project.clientId** → which client owns this project (WBS billing entity)
|
||||
- **Resource client unit** → which client group the resource primarily serves (for reporting)
|
||||
|
||||
Resource client unit can be derived from the resource's primary assignments or set manually. This is covered in the resource extensions plan, not here.
|
||||
|
||||
## Seed Data
|
||||
|
||||
Seed the 35 master clients and their sub-entities from the Dispo Categories file. See `MandatoryDispoCategories_V3.xlsx` → `Project-Attr` sheet, rows 5-40.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Create `Client` table with seed data (35 masters + ~50 sub-entities)
|
||||
2. Add `clientId` to Project (nullable)
|
||||
3. Admin links existing projects to clients
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `Client` model with self-referencing parent/child tree
|
||||
- [ ] Project linked to Client (WBS Client Name level)
|
||||
- [ ] Client management UI with tree view
|
||||
- [ ] Project modal/wizard includes client picker
|
||||
- [ ] Seed data for all 35 master clients and sub-entities
|
||||
- [ ] `getTree` API returns nested structure
|
||||
@@ -0,0 +1,210 @@
|
||||
# Plan: Country-Driven Standard Available Hours (SAH) and FTE
|
||||
|
||||
**Date:** 2026-03-13
|
||||
**Status:** Draft
|
||||
**Depends on:** OrgUnit hierarchy, Resource model extensions
|
||||
|
||||
## Problem
|
||||
|
||||
Planarchy currently uses a flat `hoursPerDay` on allocations. The chargeability reporting model requires:
|
||||
|
||||
- Country-specific daily working hours (8h Germany, 9h India, variable Spain)
|
||||
- Public holidays per country AND metro city
|
||||
- FTE as a multiplier that reduces available hours (FTE 0.5 = 4h/day in an 8h country)
|
||||
- SAH (Standard Available Hours) = net hours after deducting holidays and absences
|
||||
|
||||
Without SAH, FTE-based chargeability calculations cannot be accurate.
|
||||
|
||||
## Concepts
|
||||
|
||||
### Standard Available Hours (SAH)
|
||||
|
||||
> "Die Standard Available Hrs (SAH) bezeichnen die Netto-Arbeitszeit, also die Zeit, die nach Abzug aller Fehlzeiten tatsaechlich fuer die produktive Arbeit zur Verfuegung steht."
|
||||
|
||||
**Formula per resource per period:**
|
||||
|
||||
```
|
||||
grossHoursPerDay = country.dailyWorkingHours (with Spain schedule rules)
|
||||
effectiveHoursPerDay = grossHoursPerDay * resource.fte
|
||||
workingDaysInPeriod = calendarDays - weekends - publicHolidays(country, metroCity)
|
||||
absenceDays = vacation + illness + other absence
|
||||
SAH = effectiveHoursPerDay * (workingDaysInPeriod - absenceDays)
|
||||
```
|
||||
|
||||
### Daily Working Hours by Country
|
||||
|
||||
| Country | Daily Hours | Special Rules |
|
||||
|---|---|---|
|
||||
| Costa Rica | 8h | - |
|
||||
| Germany | 8h | - |
|
||||
| Hungary | 8h | - |
|
||||
| India | 9h | - |
|
||||
| Italy | 8h | - |
|
||||
| Portugal | 8h | - |
|
||||
| Spain | variable | Fridays always 6.5h; Mon-Thu 6.5h during 1 Jul - 15 Sep, otherwise 9h |
|
||||
| United Kingdom | 8h | - |
|
||||
|
||||
### FTE Impact
|
||||
|
||||
- FTE is stored with at least 2 decimal places (e.g. 0.50, 0.80)
|
||||
- FTE can change per month (contract changes, joiners/leavers)
|
||||
- Part-time 50% in Germany: 8h * 0.50 = 4h effective daily hours
|
||||
- Part-time 80% in India: 9h * 0.80 = 7.2h effective daily hours
|
||||
|
||||
## Schema Changes
|
||||
|
||||
### New: `Country` model
|
||||
|
||||
```prisma
|
||||
model Country {
|
||||
id String @id @default(cuid())
|
||||
code String @unique // ISO 3166-1 alpha-2 (DE, IN, ES, ...)
|
||||
name String // Display name
|
||||
dailyWorkingHours Float @default(8.0) // Base daily hours
|
||||
scheduleRules Json? @db.JsonB // For Spain-style variable schedules
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
metroCities MetroCity[]
|
||||
resources Resource[]
|
||||
}
|
||||
```
|
||||
|
||||
### New: `MetroCity` model
|
||||
|
||||
```prisma
|
||||
model MetroCity {
|
||||
id String @id @default(cuid())
|
||||
name String // e.g. "Munich", "Stuttgart"
|
||||
countryId String
|
||||
country Country @relation(fields: [countryId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
resources Resource[]
|
||||
|
||||
@@unique([countryId, name])
|
||||
}
|
||||
```
|
||||
|
||||
### Resource model extensions
|
||||
|
||||
```prisma
|
||||
model Resource {
|
||||
// ... existing fields ...
|
||||
|
||||
fte Float @default(1.0) // Already exists, ensure 2+ decimals
|
||||
countryId String?
|
||||
country Country? @relation(fields: [countryId], references: [id])
|
||||
metroCityId String?
|
||||
metroCity MetroCity? @relation(fields: [metroCityId], references: [id])
|
||||
}
|
||||
```
|
||||
|
||||
Note: Resource already has `postalCode` and `federalState` from Phase 9 (Vacation Pro).
|
||||
The new `countryId` + `metroCityId` are complementary — `federalState` stays for German public holiday derivation, `countryId` drives daily hours and SAH.
|
||||
|
||||
## Engine: SAH Calculator
|
||||
|
||||
Location: `packages/engine/src/sah/calculator.ts`
|
||||
|
||||
```typescript
|
||||
interface SAHInput {
|
||||
country: { dailyWorkingHours: number; scheduleRules?: SpainScheduleRule };
|
||||
fte: number;
|
||||
periodStart: Date;
|
||||
periodEnd: Date;
|
||||
publicHolidays: Date[]; // from country + metro city
|
||||
absenceDays: Date[]; // vacation, illness, other
|
||||
}
|
||||
|
||||
interface SAHResult {
|
||||
grossWorkingDays: number;
|
||||
publicHolidayDays: number;
|
||||
absenceDays: number;
|
||||
netWorkingDays: number;
|
||||
effectiveHoursPerDay: number;
|
||||
standardAvailableHours: number;
|
||||
}
|
||||
|
||||
function calculateSAH(input: SAHInput): SAHResult;
|
||||
```
|
||||
|
||||
Spain schedule handling:
|
||||
|
||||
```typescript
|
||||
interface SpainScheduleRule {
|
||||
type: 'spain';
|
||||
fridayHours: number; // 6.5
|
||||
summerPeriod: { from: string; to: string }; // "07-01" to "09-15"
|
||||
summerHours: number; // 6.5
|
||||
regularHours: number; // 9.0
|
||||
}
|
||||
|
||||
function getSpainDailyHours(date: Date, rule: SpainScheduleRule): number;
|
||||
```
|
||||
|
||||
## Public Holidays
|
||||
|
||||
Phase 9 already has `computeEaster()` and `getPublicHolidays(year, state?)` for German states (Bavaria).
|
||||
This needs extension:
|
||||
|
||||
- Move to a country-aware public holiday system
|
||||
- Support metro city specific holidays (e.g. Munich Assumption Day)
|
||||
- Seed known holidays per country from a reference table
|
||||
- Admin UI to manage/override holidays per country+city
|
||||
|
||||
Existing Phase 9 German holiday logic becomes the "Germany" implementation within the broader system.
|
||||
|
||||
## FTE Monthly Tracking
|
||||
|
||||
The chargeability report shows FTE can vary by month per resource. Options:
|
||||
|
||||
1. **Simple:** Single `fte` field on Resource, changed when contract changes (current approach)
|
||||
2. **Monthly:** `ResourceFTEHistory` model tracking FTE per month with effective date
|
||||
|
||||
Recommendation: Start with option 1 (single FTE field). Add history tracking as a follow-up if month-over-month FTE change reporting is needed. The current Resource model already has `fte`.
|
||||
|
||||
## UI Changes
|
||||
|
||||
### Admin: Country + Metro City Management
|
||||
|
||||
- `/admin/countries` page
|
||||
- CRUD for countries with daily working hours and schedule rules
|
||||
- Nested metro city management per country
|
||||
- Seed data for the 8 known countries + their metro cities
|
||||
|
||||
### Resource: Country + Metro City Assignment
|
||||
|
||||
- `ResourceModal.tsx` gets Country dropdown (required) and Metro City dropdown (filtered by country)
|
||||
- Auto-suggest metro city based on existing `postalCode` where possible
|
||||
|
||||
### Dashboard/Reports: SAH Display
|
||||
|
||||
- Resource detail page shows SAH for current period
|
||||
- Chargeability report uses SAH as the denominator
|
||||
|
||||
## Migration
|
||||
|
||||
1. Create Country + MetroCity tables with seed data
|
||||
2. Backfill existing resources: derive country from `federalState` (Germany) or leave null
|
||||
3. Admin assigns countries to remaining resources
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Public holiday system must be extended to all countries (not just German states)
|
||||
- Vacation/absence system (Phase 9) feeds into SAH absence deduction
|
||||
- Chargeability report (separate plan) consumes SAH as primary metric
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Country model with daily working hours and optional schedule rules
|
||||
- [ ] Metro City model linked to Country
|
||||
- [ ] Resource linked to Country + Metro City
|
||||
- [ ] SAH calculator in engine: `calculateSAH()` pure function
|
||||
- [ ] Spain variable schedule correctly handled
|
||||
- [ ] FTE reduces effective daily hours: `effectiveHours = dailyHours * fte`
|
||||
- [ ] Public holidays extended beyond Germany to all 8 countries
|
||||
- [ ] Admin UI for Country + Metro City CRUD
|
||||
- [ ] Resource modal with Country + Metro City selection
|
||||
- [ ] Seed data for all 8 countries and their metro cities
|
||||
@@ -0,0 +1,177 @@
|
||||
# Plan: Org Unit Hierarchy (Level 5 / 6 / 7)
|
||||
|
||||
**Date:** 2026-03-13
|
||||
**Status:** Draft
|
||||
**Depends on:** -
|
||||
|
||||
## Problem
|
||||
|
||||
The chargeability report groups resources by a 3-level organizational hierarchy:
|
||||
|
||||
```
|
||||
Level 5: Content Production (department)
|
||||
Level 6: CGI Content | CGI Technology | Creative Content Production | VFX (division)
|
||||
Level 7: Art Direction | CGI Production | 3D | IT Development | ... (chapter/team)
|
||||
```
|
||||
|
||||
Every resource must be mapped to an Org Unit Level 7. Level 7 rolls up to Level 6, which rolls up to Level 5. The names of org units can change over time, so they must be editable.
|
||||
|
||||
Planarchy already has a `Role` model, but roles represent skills/functions (e.g. "3D Artist"), not organizational placement. A person's org unit and their role are different dimensions.
|
||||
|
||||
## Current Data
|
||||
|
||||
### Level 5 → Level 6
|
||||
|
||||
| Level 5 | Level 6 |
|
||||
|---|---|
|
||||
| Content Production | CGI Content |
|
||||
| Content Production | CGI Technology |
|
||||
| Content Production | Creative Content Production |
|
||||
| Content Production | VFX |
|
||||
|
||||
### Level 6 → Level 7
|
||||
|
||||
| Level 6 | Level 7 |
|
||||
|---|---|
|
||||
| CGI Content | Art Direction |
|
||||
| CGI Content | Capability Development |
|
||||
| CGI Content | CGI Production |
|
||||
| CGI Content | Product Data Management |
|
||||
| CGI Content | Program/Delivery Mgmt |
|
||||
| CGI Technology | CGI Development |
|
||||
| CGI Technology | IT Development |
|
||||
| Creative Content Production | *(direct, no sub-teams)* |
|
||||
| VFX | 2D & Art Direction |
|
||||
| VFX | 3D |
|
||||
| VFX | Program/Delivery Mgmt & Other |
|
||||
|
||||
## Schema
|
||||
|
||||
### New: `OrgUnit` model (self-referencing tree)
|
||||
|
||||
```prisma
|
||||
model OrgUnit {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
shortName String? // optional abbreviation
|
||||
level Int // 5, 6, or 7
|
||||
parentId String?
|
||||
parent OrgUnit? @relation("OrgUnitTree", fields: [parentId], references: [id])
|
||||
children OrgUnit[] @relation("OrgUnitTree")
|
||||
sortOrder Int @default(0)
|
||||
isActive Boolean @default(true) // soft-delete for renamed/retired units
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
resources Resource[] // resources assigned to this org unit (level 7)
|
||||
|
||||
@@unique([parentId, name])
|
||||
}
|
||||
```
|
||||
|
||||
### Resource model extension
|
||||
|
||||
```prisma
|
||||
model Resource {
|
||||
// ... existing fields ...
|
||||
|
||||
orgUnitId String?
|
||||
orgUnit OrgUnit? @relation(fields: [orgUnitId], references: [id])
|
||||
}
|
||||
```
|
||||
|
||||
Constraint: A resource should only be assigned to a Level 7 org unit. The UI enforces this; the schema allows any level for flexibility.
|
||||
|
||||
## Shared Types
|
||||
|
||||
```typescript
|
||||
// packages/shared/src/types/org-unit.ts
|
||||
|
||||
interface OrgUnit {
|
||||
id: string;
|
||||
name: string;
|
||||
shortName?: string;
|
||||
level: number; // 5, 6, or 7
|
||||
parentId?: string;
|
||||
sortOrder: number;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
interface OrgUnitTree extends OrgUnit {
|
||||
children: OrgUnitTree[];
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
Location: `packages/api/src/router/org-unit.ts`
|
||||
|
||||
| Procedure | Access | Description |
|
||||
|---|---|---|
|
||||
| `list` | protected | Returns flat list, optionally filtered by level or parentId |
|
||||
| `getTree` | protected | Returns nested tree structure for UI rendering |
|
||||
| `create` | admin | Create org unit with parent reference |
|
||||
| `update` | admin | Rename, re-parent, change sort order |
|
||||
| `deactivate` | admin | Soft-delete (sets `isActive = false`) |
|
||||
|
||||
## UI
|
||||
|
||||
### Admin: Org Unit Management (`/admin/org-units`)
|
||||
|
||||
- Tree view showing L5 → L6 → L7 hierarchy
|
||||
- Inline editing of names (since names can change)
|
||||
- Add/remove units at each level
|
||||
- Drag-and-drop reordering within a level
|
||||
- Deactivate (not hard delete) to preserve historical data
|
||||
|
||||
### Resource: Org Unit Assignment
|
||||
|
||||
- `ResourceModal.tsx` gets cascading dropdowns:
|
||||
1. Level 5 (pre-filtered or single value)
|
||||
2. Level 6 (filtered by selected L5)
|
||||
3. Level 7 (filtered by selected L6) — this is what gets stored on the resource
|
||||
- Alternative: single tree picker showing full path
|
||||
|
||||
### Chargeability Report
|
||||
|
||||
The report groups by L6 → L7 (see `plan-chargeability-report.md`). The OrgUnit tree drives the row structure of the report.
|
||||
|
||||
## Seed Data
|
||||
|
||||
Seed the initial hierarchy from the Dispo Categories file:
|
||||
|
||||
```
|
||||
Content Production (L5)
|
||||
├── CGI Content (L6)
|
||||
│ ├── Art Direction (L7)
|
||||
│ ├── Capability Development (L7)
|
||||
│ ├── CGI Production (L7)
|
||||
│ ├── Product Data Management (L7)
|
||||
│ └── Program/Delivery Mgmt (L7)
|
||||
├── CGI Technology (L6)
|
||||
│ ├── CGI Development (L7)
|
||||
│ └── IT Development (L7)
|
||||
├── Creative Content Production (L6)
|
||||
│ └── Creative Content Production (L7)
|
||||
└── VFX (L6)
|
||||
├── 2D & Art Direction (L7)
|
||||
├── 3D (L7)
|
||||
└── Program/Delivery Mgmt & Other (L7)
|
||||
```
|
||||
|
||||
## Migration
|
||||
|
||||
1. Create `OrgUnit` table and seed data
|
||||
2. Add `orgUnitId` to Resource (nullable)
|
||||
3. Admin assigns org units to existing resources
|
||||
4. No automated backfill possible — org unit assignment is business knowledge
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `OrgUnit` model with self-referencing parent/child tree
|
||||
- [ ] Resource linked to OrgUnit (level 7)
|
||||
- [ ] Admin UI: tree view with CRUD for all levels
|
||||
- [ ] Resource modal: cascading L5 → L6 → L7 dropdowns
|
||||
- [ ] Org unit names are editable without breaking historical references
|
||||
- [ ] Seed data for the known hierarchy
|
||||
- [ ] `getTree` API returns nested structure for UI
|
||||
@@ -0,0 +1,98 @@
|
||||
# Dispo v2 Implementation Plan — Overview
|
||||
|
||||
**Date:** 2026-03-13
|
||||
**Status:** Draft
|
||||
**Source:** `MandatoryDispoCategories_V3.xlsx` + `20260309_Bi-Weekly_Chargeability_Reporting_Content_Production_V0.943_4Hartmut.xlsx`
|
||||
|
||||
## Goal
|
||||
|
||||
Extend Planarchy to support chargeability reporting with country-specific SAH (Standard Available Hours), FTE-based capacity, organizational hierarchy, utilization categories, client/WBS management, and a native chargeability report replacing the current Excel workflow.
|
||||
|
||||
## Plan Documents
|
||||
|
||||
| # | Plan | File | Core Deliverable |
|
||||
|---|---|---|---|
|
||||
| 1 | Country, SAH & FTE | [plan-country-sah-fte.md](plan-country-sah-fte.md) | Country/MetroCity models, SAH calculator, FTE-scaled daily hours |
|
||||
| 2 | Org Unit Hierarchy | [plan-org-unit-hierarchy.md](plan-org-unit-hierarchy.md) | 3-level OrgUnit tree (L5→L6→L7), resource assignment, admin UI |
|
||||
| 3 | Utilization Categories | [plan-utilization-categories.md](plan-utilization-categories.md) | UtilizationCategory model on projects (Chg, BD, MD&I, M&O, PD&R, Absence) |
|
||||
| 4 | Client & WBS | [plan-client-wbs-model.md](plan-client-wbs-model.md) | Client tree (Master→Entity), project-client linking |
|
||||
| 5 | Resource Extensions | [plan-resource-extensions.md](plan-resource-extensions.md) | EID attributes, ManagementLevel, ResourceType, Chg Responsibility, derivation rules |
|
||||
| 6 | Chargeability Report | [plan-chargeability-report.md](plan-chargeability-report.md) | Native report replacing Excel, forecast from assignments + SAH, SAP import for actuals |
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### SAH as capacity basis
|
||||
Standard Available Hours = `(dailyHours * FTE) * (workingDays - publicHolidays - absence)`.
|
||||
Country drives daily hours (8h most, 9h India, variable Spain). FTE reduces proportionally.
|
||||
|
||||
### Resource Type derivation (Option A)
|
||||
Store only 5 base types in DB: Employee, Freelancer, Apprentice, Intern, Student.
|
||||
Derive reporting types at query time:
|
||||
- **Production Studios** = `chgResponsibility = true` AND country = Germany
|
||||
- **Near&Offshore** = country NOT Germany AND type = Employee/Freelancer
|
||||
- **Accenture** = `chgResponsibility = false`
|
||||
- **Long-term absence** = derived from absence system
|
||||
|
||||
Derivation rules are configurable in admin (which countries map to which reporting type).
|
||||
|
||||
### Utilization on projects, not allocations
|
||||
Each project carries a utilization category (Chg, BD, MD&I, etc.). Hours assigned to a project inherit its category for reporting. Unassigned hours = SAH minus all categorized hours.
|
||||
|
||||
### Forecast chargeability = derived metric
|
||||
`forecastChg = hours on Chg projects / SAH`. No manual chargeability entry — it comes from what Planarchy already knows about assignments.
|
||||
|
||||
## Dependency Order
|
||||
|
||||
```
|
||||
1. Country/SAH ─────────────────┐
|
||||
2. OrgUnit Hierarchy ───────────┤
|
||||
3. Utilization Categories ──────┼──→ 5. Resource Extensions ──→ 6. Chargeability Report
|
||||
4. Client/WBS ──────────────────┘
|
||||
```
|
||||
|
||||
Plans 1-4 are independent and can be implemented in parallel.
|
||||
Plan 5 (Resource Extensions) depends on 1-4 for the FK targets.
|
||||
Plan 6 (Chargeability Report) depends on all others.
|
||||
|
||||
## New Prisma Models Summary
|
||||
|
||||
| Model | Purpose |
|
||||
|---|---|
|
||||
| `Country` | Country with daily working hours and schedule rules |
|
||||
| `MetroCity` | City within a country (for public holidays) |
|
||||
| `OrgUnit` | Self-referencing 3-level org hierarchy |
|
||||
| `UtilizationCategory` | Project classification for hour bucketing |
|
||||
| `Client` | Self-referencing client hierarchy (Master → Entity) |
|
||||
| `ManagementLevelGroup` | Career level grouping with target chargeability % |
|
||||
| `ManagementLevel` | Specific level within a group |
|
||||
| `ChargeabilitySnapshot` | Imported SAP actuals for historical reporting |
|
||||
|
||||
## Resource Model Changes Summary
|
||||
|
||||
New fields on `Resource`:
|
||||
- `enterpriseId` (String, unique)
|
||||
- `countryId` → Country
|
||||
- `metroCityId` → MetroCity
|
||||
- `orgUnitId` → OrgUnit (L7)
|
||||
- `managementLevelGroupId` → ManagementLevelGroup
|
||||
- `managementLevelId` → ManagementLevel
|
||||
- `resourceType` (enum: EMPLOYEE, FREELANCER, APPRENTICE, INTERN, STUDENT)
|
||||
- `chgResponsibility` (Boolean, default true)
|
||||
- `rolledOff` (Boolean)
|
||||
- `departed` (Boolean)
|
||||
- `clientUnitId` → Client (primary client for reporting)
|
||||
- `lcrCents`, `ucrCents` (Int, placeholder for cost rates)
|
||||
|
||||
## Project Model Changes Summary
|
||||
|
||||
New fields on `Project`:
|
||||
- `utilizationCategoryId` → UtilizationCategory
|
||||
- `clientId` → Client (WBS Client Name)
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Resource Type derivation rules**: The country→reporting-type mapping should be admin-configurable. Exact admin UI TBD.
|
||||
2. **Win Probability**: The Dispo file mentions it "should contain the value from MMS". Is this relevant for Planarchy? If so, it's a field on Project.
|
||||
3. **LCR/UCR**: Cost rate definitions are not yet available. Placeholder fields are included.
|
||||
4. **SAP import format**: What format do SAP period exports come in? CSV? API? Needs clarification for the import mechanism.
|
||||
5. **FTE history**: Currently single `fte` field. Monthly FTE tracking may be needed if contract changes happen mid-month.
|
||||
@@ -0,0 +1,206 @@
|
||||
# Plan: Resource Model Extensions (EID Attributes)
|
||||
|
||||
**Date:** 2026-03-13
|
||||
**Status:** Draft
|
||||
**Depends on:** Country/SAH, OrgUnit hierarchy
|
||||
|
||||
## Problem
|
||||
|
||||
The Dispo Categories file defines a rich set of EID (employee) attributes that Planarchy's Resource model currently does not cover. These attributes are needed for chargeability reporting, resource filtering, and organizational grouping.
|
||||
|
||||
## Current Resource Model (relevant fields)
|
||||
|
||||
```
|
||||
Resource {
|
||||
id, name, email, skills (JSONB), isActive, postalCode, federalState,
|
||||
portfolioUrl, roleId, aiSummary, fte, userId, ...
|
||||
}
|
||||
```
|
||||
|
||||
## Required New Attributes
|
||||
|
||||
### Attributes that need DB storage
|
||||
|
||||
| Attribute | Type | Source | Notes |
|
||||
|---|---|---|---|
|
||||
| Enterprise ID | String | manual/import | ACN-style username (e.g. "a.kasperovich") |
|
||||
| Country | FK → Country | manual | See plan-country-sah-fte.md |
|
||||
| Metro City | FK → MetroCity | manual | See plan-country-sah-fte.md |
|
||||
| Org Unit (L7) | FK → OrgUnit | manual | See plan-org-unit-hierarchy.md |
|
||||
| Management Level Group | FK → ManagementLevel | manual | See below |
|
||||
| Management Level | derived from group | - | Sub-level within group |
|
||||
| FTE | Float | manual | Already exists, ensure 2+ decimal precision |
|
||||
| Resource Type | Enum | manual | Apprentice, Employee, Freelancer, Intern, Student |
|
||||
| Chg Responsibility | Boolean | manual | Default: true. Drives "Accenture" resource type derivation |
|
||||
| Rolled Off | Boolean | manual | Status flag, default: false |
|
||||
| Departed | Boolean | manual | Status flag, default: false |
|
||||
| Client Unit | FK → Client? or String | manual | Primary client assignment for reporting |
|
||||
|
||||
### Attributes that are derived (no DB input)
|
||||
|
||||
| Attribute | Derivation Rule |
|
||||
|---|---|
|
||||
| Long-term absence | Derived from vacation/absence system (extended leave) |
|
||||
| Chapter | Derived from OrgUnit L7 → name |
|
||||
| Department | Derived from OrgUnit L6 → name |
|
||||
| MV Ressource Type (reporting) | Derived: see resource type derivation rules |
|
||||
|
||||
### Resource Type Derivation for Reporting
|
||||
|
||||
The chargeability report uses a "MV Ressource Type" that differs from the stored Resource Type:
|
||||
|
||||
| Reporting Type | Derivation Rule |
|
||||
|---|---|
|
||||
| Production Studios | `chgResponsibility = true` AND country is Germany |
|
||||
| Near&Offshore | Country is NOT Germany AND resource type is Employee/Freelancer |
|
||||
| Accenture | `chgResponsibility = false` (regardless of country) |
|
||||
| Long-term absence | Has active long-term absence flag |
|
||||
|
||||
These are computed at query time, not stored. An admin UI can make the country→reporting-type mapping configurable.
|
||||
|
||||
## Management Level Model
|
||||
|
||||
### New: `ManagementLevelGroup` model
|
||||
|
||||
```prisma
|
||||
model ManagementLevelGroup {
|
||||
id String @id @default(cuid())
|
||||
name String @unique // "Analyst", "Consultant", "Manager", etc.
|
||||
targetPercentage Float // Official chargeability target (e.g. 0.805)
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
levels ManagementLevel[]
|
||||
resources Resource[]
|
||||
}
|
||||
```
|
||||
|
||||
### New: `ManagementLevel` model
|
||||
|
||||
```prisma
|
||||
model ManagementLevel {
|
||||
id String @id @default(cuid())
|
||||
name String @unique // "10-Senior Analyst", "11-Analyst", etc.
|
||||
groupId String
|
||||
group ManagementLevelGroup @relation(fields: [groupId], references: [id])
|
||||
|
||||
resources Resource[]
|
||||
}
|
||||
```
|
||||
|
||||
### Seed Data
|
||||
|
||||
| Group | Target % | Levels |
|
||||
|---|---|---|
|
||||
| Accenture Leadership | 36.5% | *(levels 1-4, names TBD)* |
|
||||
| Senior Manager | 54.6% | 5-Associate Director, 6-Senior Manager |
|
||||
| Manager | 74.7% | 7-Manager |
|
||||
| Consultant | 80.8% | 8-Associate Manager, 9-Team Lead/Consultant |
|
||||
| Analyst | 80.5% | 10-Senior Analyst, 11-Analyst |
|
||||
| Associate | 77.0% | 12-Associate, 13-New Associate |
|
||||
|
||||
## Schema Changes on Resource
|
||||
|
||||
```prisma
|
||||
model Resource {
|
||||
// ... existing fields ...
|
||||
|
||||
enterpriseId String? @unique // ACN enterprise ID
|
||||
countryId String? // FK → Country
|
||||
country Country? @relation(...)
|
||||
metroCityId String? // FK → MetroCity
|
||||
metroCity MetroCity? @relation(...)
|
||||
orgUnitId String? // FK → OrgUnit (L7)
|
||||
orgUnit OrgUnit? @relation(...)
|
||||
managementLevelGroupId String? // FK → ManagementLevelGroup
|
||||
managementLevelGroup ManagementLevelGroup? @relation(...)
|
||||
managementLevelId String? // FK → ManagementLevel
|
||||
managementLevel ManagementLevel? @relation(...)
|
||||
resourceType ResourceType @default(EMPLOYEE)
|
||||
chgResponsibility Boolean @default(true)
|
||||
rolledOff Boolean @default(false)
|
||||
departed Boolean @default(false)
|
||||
clientUnitId String? // Primary client for reporting
|
||||
clientUnit Client? @relation("ResourceClientUnit", fields: [clientUnitId], references: [id])
|
||||
}
|
||||
|
||||
enum ResourceType {
|
||||
EMPLOYEE
|
||||
FREELANCER
|
||||
APPRENTICE
|
||||
INTERN
|
||||
STUDENT
|
||||
}
|
||||
```
|
||||
|
||||
## UI Changes
|
||||
|
||||
### Resource Modal Extensions
|
||||
|
||||
Add to `ResourceModal.tsx`:
|
||||
|
||||
| Field | UI Element | Notes |
|
||||
|---|---|---|
|
||||
| Enterprise ID | Text input | Optional, unique |
|
||||
| Country | Dropdown | Required for SAH |
|
||||
| Metro City | Dropdown (filtered by country) | Optional |
|
||||
| Org Unit | Cascading L5→L6→L7 picker | Stores L7 |
|
||||
| Management Level Group | Dropdown | Drives target % |
|
||||
| Management Level | Dropdown (filtered by group) | Specific level |
|
||||
| Resource Type | Dropdown (5 values) | Default: Employee |
|
||||
| Chg Responsibility | Toggle | Default: on |
|
||||
| Client Unit | Client picker | Primary client for reporting |
|
||||
| Rolled Off | Toggle | Status |
|
||||
| Departed | Toggle | Status |
|
||||
|
||||
### Admin: Management Level Management (`/admin/management-levels`)
|
||||
|
||||
- Table of management level groups with target percentages
|
||||
- Nested levels within each group
|
||||
- Editable target % (changes over time)
|
||||
- Add/edit levels
|
||||
|
||||
### Resource List Enhancements
|
||||
|
||||
- New columns: Enterprise ID, Country, Org Unit, Management Level, Client Unit
|
||||
- Filters for: Country, Org Unit (L6/L7), Management Level Group, Resource Type, Client Unit
|
||||
- Status filter: active / rolled off / departed
|
||||
|
||||
## LCR and UCR
|
||||
|
||||
The Dispo file mentions LCR (Local Cost Rate) and UCR (Unit Cost Rate) but has no defined values yet. These are likely cost rates per resource used in budget calculations.
|
||||
|
||||
Recommendation: Add placeholder fields now, define schema when values become available.
|
||||
|
||||
```prisma
|
||||
model Resource {
|
||||
// ...
|
||||
lcrCents Int? // Local Cost Rate in cents per hour
|
||||
ucrCents Int? // Unit Cost Rate in cents per hour
|
||||
}
|
||||
```
|
||||
|
||||
Follows the integer-cents pattern from CLAUDE.md.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Add new columns to Resource (all nullable initially)
|
||||
2. Create ManagementLevelGroup + ManagementLevel tables with seed data
|
||||
3. Add ResourceType enum
|
||||
4. Admin populates existing resources via batch import or manual assignment
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Enterprise ID field on Resource (unique, optional)
|
||||
- [ ] Resource linked to Country, MetroCity, OrgUnit, ManagementLevel
|
||||
- [ ] ResourceType enum with 5 values
|
||||
- [ ] chgResponsibility, rolledOff, departed boolean flags
|
||||
- [ ] Client Unit FK for primary client assignment
|
||||
- [ ] ManagementLevelGroup with target percentages
|
||||
- [ ] ManagementLevel with group membership
|
||||
- [ ] Resource modal with all new fields
|
||||
- [ ] Resource list with new columns and filters
|
||||
- [ ] Management level admin UI
|
||||
- [ ] LCR/UCR placeholder fields (integer cents)
|
||||
- [ ] Reporting resource type derivation logic
|
||||
@@ -0,0 +1,158 @@
|
||||
# Plan: Utilization Categories on Projects
|
||||
|
||||
**Date:** 2026-03-13
|
||||
**Status:** Draft
|
||||
**Depends on:** -
|
||||
|
||||
## Problem
|
||||
|
||||
The chargeability report categorizes all work into utilization buckets. Currently Planarchy projects have no utilization classification. Every project needs a utilization category so that hours booked against it flow into the correct reporting bucket.
|
||||
|
||||
## Utilization Categories
|
||||
|
||||
From the Dispo Categories file (adapted to ACN naming):
|
||||
|
||||
| Short | Name | Description |
|
||||
|---|---|---|
|
||||
| Chg | Chargeable | Billable client project work |
|
||||
| BD | Business Development | Sales, proposals, presales activities |
|
||||
| MD&I | Market Development and Initiative | R&D, innovation, market development |
|
||||
| M&O | Management and Operations | Internal admin, management overhead |
|
||||
| PD&R | Personal Development and Recruitment | Training, hiring, onboarding |
|
||||
| Absence | Absence & Non Standard | Reduces Standard Available Hours: vacation, illness, non-standard leave |
|
||||
|
||||
Notes from the Dispo file:
|
||||
- "Absent" and "Not available" are merged into "Absence & Non Standard"
|
||||
- "Unassigned" hours are calculated automatically (SAH minus all categorized hours)
|
||||
- Categories follow ACN naming convention
|
||||
|
||||
## Schema
|
||||
|
||||
### New: `UtilizationCategory` model
|
||||
|
||||
```prisma
|
||||
model UtilizationCategory {
|
||||
id String @id @default(cuid())
|
||||
code String @unique // "Chg", "BD", "MD&I", etc.
|
||||
name String // Full display name
|
||||
description String? // Editable explanation
|
||||
sortOrder Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
isDefault Boolean @default(false) // One category can be the default for new projects
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
projects Project[]
|
||||
}
|
||||
```
|
||||
|
||||
### Project model extension
|
||||
|
||||
```prisma
|
||||
model Project {
|
||||
// ... existing fields ...
|
||||
|
||||
utilizationCategoryId String?
|
||||
utilizationCategory UtilizationCategory? @relation(fields: [utilizationCategoryId], references: [id])
|
||||
}
|
||||
```
|
||||
|
||||
Why on Project (not on Allocation/Assignment):
|
||||
- The Dispo model maps categories to projects, not to individual time entries
|
||||
- A project is either "Chargeable" or "Business Development" — the category is a project-level attribute
|
||||
- Hours assigned to a project inherit the project's utilization category for reporting
|
||||
|
||||
## Shared Types
|
||||
|
||||
```typescript
|
||||
// packages/shared/src/types/utilization-category.ts
|
||||
|
||||
interface UtilizationCategory {
|
||||
id: string;
|
||||
code: string; // "Chg", "BD", "MD&I", "M&O", "PD&R", "Absence"
|
||||
name: string;
|
||||
description?: string;
|
||||
sortOrder: number;
|
||||
isActive: boolean;
|
||||
isDefault: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
Location: `packages/api/src/router/utilization-category.ts`
|
||||
|
||||
| Procedure | Access | Description |
|
||||
|---|---|---|
|
||||
| `list` | protected | Returns all active categories (sorted) |
|
||||
| `getById` | protected | Single category |
|
||||
| `create` | admin | Create new category |
|
||||
| `update` | admin | Edit code, name, description, sort order |
|
||||
| `deactivate` | admin | Soft-delete |
|
||||
|
||||
## UI
|
||||
|
||||
### Admin: Utilization Category Management (`/admin/utilization-categories`)
|
||||
|
||||
- Table listing all categories with code, name, description
|
||||
- Inline editing of name and description
|
||||
- Add new categories (for future extensibility)
|
||||
- Reorder via sort order
|
||||
- Mark one as default for new projects
|
||||
|
||||
### Project: Category Assignment
|
||||
|
||||
- `ProjectModal.tsx` gets a `UtilizationCategory` dropdown (required or defaults to "Chg")
|
||||
- `ProjectWizard.tsx` Step 1 includes category selection
|
||||
- Project list shows category badge
|
||||
|
||||
### Chargeability Report
|
||||
|
||||
Hours are bucketed by looking up `project.utilizationCategory.code`:
|
||||
- `Chg` hours → chargeability numerator
|
||||
- `BD` hours → business development column
|
||||
- `MD&I` hours → market development column
|
||||
- `M&O` hours → management overhead column
|
||||
- `PD&R` hours → personal development column
|
||||
- `Absence` hours → reduce SAH (already handled by vacation system, this is the project-side booking)
|
||||
|
||||
**Unassigned hours** = SAH - sum of all categorized hours (calculated, not stored)
|
||||
|
||||
## Absence Category and Vacation System
|
||||
|
||||
The "Absence & Non Standard" category overlaps with Phase 9 Vacation Pro. Relationship:
|
||||
|
||||
- Vacation/illness from the vacation system reduces SAH directly (date-based)
|
||||
- An "Absence" project may additionally exist for booking non-standard absence hours
|
||||
- The chargeability calculator deducts both: vacation days (from vacation system) and hours booked to Absence-category projects
|
||||
|
||||
This avoids double-counting: vacation days reduce available days in SAH, while Absence-project hours capture remaining non-standard time.
|
||||
|
||||
## Seed Data
|
||||
|
||||
```sql
|
||||
INSERT INTO "UtilizationCategory" (code, name, description, "sortOrder", "isDefault") VALUES
|
||||
('Chg', 'Chargeable', 'Billable client project work', 1, true),
|
||||
('BD', 'Business Development', 'Sales, proposals, presales activities', 2, false),
|
||||
('MD&I', 'Market Development and Initiative', 'R&D, innovation, market development', 3, false),
|
||||
('M&O', 'Management and Operations', 'Internal admin, management overhead', 4, false),
|
||||
('PD&R', 'Personal Development and Recruitment','Training, hiring, onboarding', 5, false),
|
||||
('Absence', 'Absence & Non Standard', 'Vacation, illness, non-standard leave (reduces SAH)', 6, false);
|
||||
```
|
||||
|
||||
## Migration
|
||||
|
||||
1. Create `UtilizationCategory` table with seed data
|
||||
2. Add `utilizationCategoryId` to Project (nullable initially)
|
||||
3. Default all existing projects to "Chg" (or let admin assign manually)
|
||||
4. Make required once all projects are assigned
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `UtilizationCategory` model with code, name, description
|
||||
- [ ] Project linked to UtilizationCategory
|
||||
- [ ] Admin UI for category CRUD (code, name, description editable)
|
||||
- [ ] Project modal/wizard includes category dropdown
|
||||
- [ ] Seed data for all 6 categories
|
||||
- [ ] Chargeability report buckets hours by project category
|
||||
- [ ] Unassigned hours computed as SAH minus all categorized hours
|
||||
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
* Generates skill matrix Excel files for all Planarchy resources.
|
||||
* Format matches skillmatrix_formular_example.xlsx exactly.
|
||||
*/
|
||||
import { createRequire } from "module";
|
||||
import { writeFileSync, mkdirSync } from "fs";
|
||||
const require = createRequire(import.meta.url);
|
||||
const XLSX = require("/home/hartmut/Documents/Copilot/planarchy/node_modules/.pnpm/xlsx@0.18.5/node_modules/xlsx/xlsx.js");
|
||||
|
||||
const OUT_DIR = "/home/hartmut/Documents/Copilot/planarchy/samples/skillmatrix_dummydata";
|
||||
mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
// ─── Skill Definitions ─────────────────────────────────────────────────────
|
||||
|
||||
const SOFTWARE_SKILLS = [
|
||||
// 3D Editing
|
||||
{ category: "3D Editing", item: "3ds max" },
|
||||
{ category: "3D Editing", item: "Maya" },
|
||||
{ category: "3D Editing", item: "Blender" },
|
||||
{ category: "3D Editing", item: "Modo" },
|
||||
{ category: "3D Editing", item: "Cinema4D" },
|
||||
{ category: "3D Editing", item: "Houdini" },
|
||||
{ category: "3D Editing", item: "ZBrush" },
|
||||
// 2D Creation
|
||||
{ category: "2D Creation", item: "Photoshop" },
|
||||
{ category: "2D Creation", item: "Illustrator" },
|
||||
{ category: "2D Creation", item: "After Effects" },
|
||||
{ category: "2D Creation", item: "Substance Painter" },
|
||||
{ category: "2D Creation", item: "Substance Designer" },
|
||||
{ category: "2D Creation", item: "Procreate" },
|
||||
// Automotive Pipeline
|
||||
{ category: "Automotive Pipeline", item: "VPB" },
|
||||
{ category: "Automotive Pipeline", item: "Data Pipeline" },
|
||||
{ category: "Automotive Pipeline", item: "EFC" },
|
||||
{ category: "Automotive Pipeline", item: "VMDS" },
|
||||
{ category: "Automotive Pipeline", item: "Batch Export Tools" },
|
||||
{ category: "Automotive Pipeline", item: "JT Format Tools" },
|
||||
// Game Engines
|
||||
{ category: "Game Engines", item: "Unreal Engine" },
|
||||
{ category: "Game Engines", item: "Unity" },
|
||||
{ category: "Game Engines", item: "CryEngine" },
|
||||
// Development
|
||||
{ category: "Development", item: "Python" },
|
||||
{ category: "Development", item: "JavaScript" },
|
||||
{ category: "Development", item: "TypeScript" },
|
||||
{ category: "Development", item: "C++" },
|
||||
{ category: "Development", item: "MEL / MaxScript" },
|
||||
{ category: "Development", item: "Node.js" },
|
||||
{ category: "Development", item: "Git" },
|
||||
// Rendering
|
||||
{ category: "Rendering", item: "V-Ray" },
|
||||
{ category: "Rendering", item: "Arnold" },
|
||||
{ category: "Rendering", item: "Corona" },
|
||||
{ category: "Rendering", item: "Octane" },
|
||||
{ category: "Rendering", item: "Redshift" },
|
||||
{ category: "Rendering", item: "Cycles" },
|
||||
{ category: "Rendering", item: "Marmoset Toolbag" },
|
||||
// Collaboration
|
||||
{ category: "Collaboration & PM", item: "Jira" },
|
||||
{ category: "Collaboration & PM", item: "Confluence" },
|
||||
{ category: "Collaboration & PM", item: "MS Project" },
|
||||
{ category: "Collaboration & PM", item: "Excel" },
|
||||
{ category: "Collaboration & PM", item: "PowerPoint" },
|
||||
{ category: "Collaboration & PM", item: "Shotgun / ShotGrid" },
|
||||
];
|
||||
|
||||
const TECHNICAL_SKILLS = [
|
||||
// Industry experience
|
||||
{ category: "Industry Experience", item: "Automotive" },
|
||||
{ category: "Industry Experience", item: "Arch viz / Real estate" },
|
||||
{ category: "Industry Experience", item: "Environments" },
|
||||
{ category: "Industry Experience", item: "Product viz" },
|
||||
{ category: "Industry Experience", item: "Character creation" },
|
||||
{ category: "Industry Experience", item: "VFX" },
|
||||
{ category: "Industry Experience", item: "Games" },
|
||||
// Output formats
|
||||
{ category: "Output Format", item: "Offline rendering" },
|
||||
{ category: "Output Format", item: "Real-time / Configurator" },
|
||||
{ category: "Output Format", item: "Interactive (AR/VR)" },
|
||||
{ category: "Output Format", item: "360° / Panorama" },
|
||||
{ category: "Output Format", item: "Motion picture" },
|
||||
// File formats
|
||||
{ category: "File Formats", item: "FBX" },
|
||||
{ category: "File Formats", item: "OBJ / MTL" },
|
||||
{ category: "File Formats", item: "USD / USDZ" },
|
||||
{ category: "File Formats", item: "Alembic" },
|
||||
{ category: "File Formats", item: "glTF / glb" },
|
||||
{ category: "File Formats", item: "JT" },
|
||||
{ category: "File Formats", item: "3MF" },
|
||||
// Asset types
|
||||
{ category: "Asset Types", item: "Hard surface / Vehicles" },
|
||||
{ category: "Asset Types", item: "Organic / Characters" },
|
||||
{ category: "Asset Types", item: "Interior / Props" },
|
||||
{ category: "Asset Types", item: "Exterior / Architecture" },
|
||||
{ category: "Asset Types", item: "Materials & Textures" },
|
||||
{ category: "Asset Types", item: "Lighting & HDRI" },
|
||||
// Quality & Pipeline
|
||||
{ category: "Quality & Pipeline", item: "Asset QA" },
|
||||
{ category: "Quality & Pipeline", item: "Performance optimization" },
|
||||
{ category: "Quality & Pipeline", item: "LOD creation" },
|
||||
{ category: "Quality & Pipeline", item: "Data validation" },
|
||||
{ category: "Quality & Pipeline", item: "Bug tracking" },
|
||||
{ category: "Quality & Pipeline", item: "Review & Feedback" },
|
||||
];
|
||||
|
||||
// ─── Profile Templates per Chapter ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Seeded random number generator for reproducible results.
|
||||
*/
|
||||
function seededRandom(seed) {
|
||||
let s = seed;
|
||||
return () => {
|
||||
s = (s * 16807 + 0) % 2147483647;
|
||||
return (s - 1) / 2147483646;
|
||||
};
|
||||
}
|
||||
|
||||
function rand(rng, min, max) {
|
||||
return Math.floor(rng() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function pick(rng, ...choices) {
|
||||
return choices[Math.floor(rng() * choices.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns skill levels for all software+technical skills based on chapter profile.
|
||||
* Each skill gets a property 0-4 (0=no experience) and optionally a main skillset marker.
|
||||
*/
|
||||
function buildSkillProfile(resource, seed) {
|
||||
const rng = seededRandom(seed);
|
||||
const chapter = resource.chapter ?? "Digital Content Production";
|
||||
|
||||
// Determine area of expertise and portfolio based on chapter
|
||||
let areaOfExpertise, yearsOfExperience, portfolioUrl;
|
||||
|
||||
if (chapter === "Project Management") {
|
||||
areaOfExpertise = "Project Manager";
|
||||
yearsOfExperience = rand(rng, 4, 15);
|
||||
portfolioUrl = "";
|
||||
} else if (chapter === "Art Direction") {
|
||||
areaOfExpertise = "Art Director";
|
||||
yearsOfExperience = rand(rng, 5, 18);
|
||||
portfolioUrl = `https://www.artstation.com/${resource.eid.split(".")[0]}`;
|
||||
} else if (chapter === "Product Data Management") {
|
||||
areaOfExpertise = "PDM Specialist";
|
||||
yearsOfExperience = rand(rng, 3, 12);
|
||||
portfolioUrl = "";
|
||||
} else if (chapter === "CGI-Dev") {
|
||||
areaOfExpertise = pick(rng, "Unreal Dev", "Frontend Dev", "3D Tech Tester");
|
||||
yearsOfExperience = rand(rng, 2, 12);
|
||||
portfolioUrl = `https://github.com/${resource.eid.split(".")[0]}`;
|
||||
} else {
|
||||
// Digital Content Production
|
||||
areaOfExpertise = pick(rng, "3D Artist", "3D Lead", "3D Generalist");
|
||||
yearsOfExperience = rand(rng, 2, 14);
|
||||
portfolioUrl = `https://www.artstation.com/${resource.eid.split(".")[0]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns proficiency 0-4 for a given skill based on chapter profile.
|
||||
*/
|
||||
function getSkillLevel(category, item) {
|
||||
const c = chapter;
|
||||
const base = rng();
|
||||
|
||||
// Project Management chapter
|
||||
if (c === "Project Management") {
|
||||
if (category === "Collaboration & PM") return rand(rng, 3, 4);
|
||||
if (item === "Photoshop" || item === "Illustrator") return rand(rng, 1, 3);
|
||||
if (item === "3ds max" || item === "Maya") return rand(rng, 0, 1);
|
||||
if (item === "Unreal Engine" || item === "Unity") return rand(rng, 0, 1);
|
||||
if (category === "Automotive Pipeline") return rand(rng, 2, 4);
|
||||
if (category === "Industry Experience") return rand(rng, 1, 3);
|
||||
if (category === "Output Format") return rand(rng, 1, 2);
|
||||
if (category === "Quality & Pipeline") return rand(rng, 2, 4);
|
||||
if (category === "File Formats") return rand(rng, 1, 2);
|
||||
if (category === "Asset Types") return rand(rng, 0, 1);
|
||||
return base > 0.7 ? rand(rng, 1, 2) : 0;
|
||||
}
|
||||
|
||||
// Art Direction
|
||||
if (c === "Art Direction") {
|
||||
if (item === "Photoshop") return rand(rng, 3, 4);
|
||||
if (item === "Illustrator") return rand(rng, 3, 4);
|
||||
if (item === "After Effects") return rand(rng, 2, 4);
|
||||
if (item === "Cinema4D" || item === "Blender") return rand(rng, 2, 3);
|
||||
if (item === "3ds max" || item === "Maya") return rand(rng, 2, 4);
|
||||
if (item === "ZBrush") return rand(rng, 1, 3);
|
||||
if (item === "Substance Painter") return rand(rng, 2, 3);
|
||||
if (category === "Automotive Pipeline") return rand(rng, 2, 3);
|
||||
if (category === "Industry Experience") return rand(rng, 2, 4);
|
||||
if (item === "Real-time / Configurator") return rand(rng, 2, 3);
|
||||
if (item === "Offline rendering") return rand(rng, 3, 4);
|
||||
if (category === "Asset Types") return rand(rng, 2, 4);
|
||||
if (category === "Rendering") return rand(rng, 2, 4);
|
||||
if (category === "Collaboration & PM") return rand(rng, 1, 3);
|
||||
return base > 0.5 ? rand(rng, 1, 2) : 0;
|
||||
}
|
||||
|
||||
// Product Data Management
|
||||
if (c === "Product Data Management") {
|
||||
if (category === "Automotive Pipeline") return rand(rng, 3, 4);
|
||||
if (category === "File Formats") return rand(rng, 3, 4);
|
||||
if (item === "Asset QA" || item === "Data validation") return rand(rng, 3, 4);
|
||||
if (item === "Performance optimization" || item === "LOD creation") return rand(rng, 2, 4);
|
||||
if (item === "Excel" || item === "PowerPoint") return rand(rng, 3, 4);
|
||||
if (item === "Jira" || item === "Confluence") return rand(rng, 2, 3);
|
||||
if (item === "FBX" || item === "OBJ / MTL") return rand(rng, 3, 4);
|
||||
if (item === "JT" || item === "3MF") return rand(rng, 3, 4);
|
||||
if (item === "Automotive") return rand(rng, 3, 4);
|
||||
if (item === "Hard surface / Vehicles") return rand(rng, 3, 4);
|
||||
if (category === "3D Editing") return rand(rng, 1, 3);
|
||||
if (category === "Industry Experience") return rand(rng, 2, 3);
|
||||
if (category === "Quality & Pipeline") return rand(rng, 2, 4);
|
||||
return base > 0.6 ? rand(rng, 1, 2) : 0;
|
||||
}
|
||||
|
||||
// CGI-Dev
|
||||
if (c === "CGI-Dev") {
|
||||
if (item === "Unreal Engine") return rand(rng, 3, 4);
|
||||
if (item === "Unity") return rand(rng, 2, 4);
|
||||
if (item === "Python") return rand(rng, 3, 4);
|
||||
if (item === "JavaScript" || item === "TypeScript") return rand(rng, 3, 4);
|
||||
if (item === "C++") return rand(rng, 2, 4);
|
||||
if (item === "Node.js") return rand(rng, 2, 3);
|
||||
if (item === "Git") return rand(rng, 3, 4);
|
||||
if (item === "MEL / MaxScript") return rand(rng, 2, 3);
|
||||
if (item === "Real-time / Configurator") return rand(rng, 3, 4);
|
||||
if (item === "Interactive (AR/VR)") return rand(rng, 2, 4);
|
||||
if (category === "Automotive Pipeline") return rand(rng, 2, 3);
|
||||
if (item === "Asset QA" || item === "Performance optimization") return rand(rng, 3, 4);
|
||||
if (item === "LOD creation") return rand(rng, 2, 4);
|
||||
if (item === "glTF / glb" || item === "FBX") return rand(rng, 3, 4);
|
||||
if (item === "Automotive" || item === "Games") return rand(rng, 3, 4);
|
||||
if (category === "3D Editing") return rand(rng, 1, 3);
|
||||
if (category === "2D Creation") return rand(rng, 0, 2);
|
||||
if (category === "Rendering") return rand(rng, 1, 2);
|
||||
return base > 0.5 ? rand(rng, 1, 2) : 0;
|
||||
}
|
||||
|
||||
// Digital Content Production (3D Artists)
|
||||
if (item === "3ds max") return rand(rng, 3, 4);
|
||||
if (item === "Maya") return rand(rng, 2, 4);
|
||||
if (item === "Blender") return rand(rng, 1, 3);
|
||||
if (item === "Cinema4D") return rand(rng, 0, 3);
|
||||
if (item === "Houdini") return rand(rng, 0, 2);
|
||||
if (item === "ZBrush") return rand(rng, 1, 3);
|
||||
if (item === "Photoshop") return rand(rng, 3, 4);
|
||||
if (item === "Substance Painter") return rand(rng, 2, 4);
|
||||
if (item === "Substance Designer") return rand(rng, 1, 3);
|
||||
if (item === "V-Ray") return rand(rng, 2, 4);
|
||||
if (item === "Arnold") return rand(rng, 1, 3);
|
||||
if (item === "Redshift") return rand(rng, 1, 3);
|
||||
if (item === "Corona") return rand(rng, 0, 2);
|
||||
if (category === "Automotive Pipeline") return rand(rng, 2, 4);
|
||||
if (item === "Automotive") return rand(rng, 2, 4);
|
||||
if (item === "Hard surface / Vehicles") return rand(rng, 3, 4);
|
||||
if (item === "Materials & Textures") return rand(rng, 3, 4);
|
||||
if (item === "FBX" || item === "OBJ / MTL") return rand(rng, 3, 4);
|
||||
if (item === "Offline rendering") return rand(rng, 3, 4);
|
||||
if (item === "Real-time / Configurator") return rand(rng, 2, 3);
|
||||
if (category === "Quality & Pipeline") return rand(rng, 2, 3);
|
||||
if (category === "Asset Types") return rand(rng, 2, 4);
|
||||
if (category === "Collaboration & PM") return rand(rng, 1, 3);
|
||||
return base > 0.6 ? rand(rng, 1, 2) : 0;
|
||||
}
|
||||
|
||||
// Build skill rows
|
||||
const softwareRows = SOFTWARE_SKILLS.map(({ category, item }) => ({
|
||||
category,
|
||||
item,
|
||||
property: String(getSkillLevel(category, item)),
|
||||
"main skillset": "please select",
|
||||
}));
|
||||
|
||||
const technicalRows = TECHNICAL_SKILLS.map(({ category, item }) => ({
|
||||
category,
|
||||
item,
|
||||
property: String(getSkillLevel(category, item)),
|
||||
"main skillset": "",
|
||||
}));
|
||||
|
||||
// Pick 2 main skills: the top 2 Software Skills with property > 0
|
||||
const eligibleMain = softwareRows
|
||||
.filter((r) => parseInt(r.property) >= 3)
|
||||
.sort((a, b) => parseInt(b.property) - parseInt(a.property))
|
||||
.slice(0, 2);
|
||||
|
||||
eligibleMain.forEach((row, idx) => {
|
||||
row["main skillset"] = String(idx + 1); // "1" for first, "2" for second
|
||||
});
|
||||
|
||||
return {
|
||||
areaOfExpertise,
|
||||
yearsOfExperience,
|
||||
portfolioUrl,
|
||||
softwareRows,
|
||||
technicalRows,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Excel Generation ───────────────────────────────────────────────────────
|
||||
|
||||
function createWorkbook(resource, profile) {
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
// Sheet 1: Introduction (minimal placeholder)
|
||||
const introData = [
|
||||
{ Introduction: "Skill Matrix — auto-generated for testing" },
|
||||
];
|
||||
const wsIntro = XLSX.utils.json_to_sheet(introData);
|
||||
XLSX.utils.book_append_sheet(wb, wsIntro, "Introduction");
|
||||
|
||||
// Sheet 2: Employee Information
|
||||
const empData = [
|
||||
{ "__EMPTY": "", item: "full name", property: resource.displayName },
|
||||
{ "__EMPTY": "", item: "area of expertise/main focus", property: profile.areaOfExpertise },
|
||||
{ "__EMPTY": "", item: "years of experience", property: String(profile.yearsOfExperience) + ".0" },
|
||||
{
|
||||
"__EMPTY": "",
|
||||
item: "Creative Portfolio",
|
||||
property: profile.portfolioUrl || "https://www.example.com/" + resource.eid,
|
||||
},
|
||||
];
|
||||
const wsEmp = XLSX.utils.json_to_sheet(empData);
|
||||
XLSX.utils.book_append_sheet(wb, wsEmp, "Employee Information");
|
||||
|
||||
// Sheet 3: Software Skills
|
||||
const wsSoftware = XLSX.utils.json_to_sheet(profile.softwareRows);
|
||||
XLSX.utils.book_append_sheet(wb, wsSoftware, "Software Skills");
|
||||
|
||||
// Sheet 4: Technical Skillset
|
||||
const wsTechnical = XLSX.utils.json_to_sheet(profile.technicalRows);
|
||||
XLSX.utils.book_append_sheet(wb, wsTechnical, "Technical Skillset");
|
||||
|
||||
// Sheet 5: Setup (minimal)
|
||||
const setupData = [
|
||||
{
|
||||
"area of expertise": profile.areaOfExpertise,
|
||||
"Career Level": "10",
|
||||
"skill level": "3",
|
||||
"main toolset": "1",
|
||||
"type of work": "Automotive",
|
||||
},
|
||||
];
|
||||
const wsSetup = XLSX.utils.json_to_sheet(setupData);
|
||||
XLSX.utils.book_append_sheet(wb, wsSetup, "Setup");
|
||||
|
||||
return wb;
|
||||
}
|
||||
|
||||
// ─── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const resources = [
|
||||
{ eid: "anna.marie", displayName: "Anna Marie", chapter: "Product Data Management" },
|
||||
{ eid: "bruce.banner", displayName: "Bruce Banner", chapter: "Project Management" },
|
||||
{ eid: "carl.lucas", displayName: "Carl Lucas", chapter: "CGI-Dev" },
|
||||
{ eid: "carol.danvers", displayName: "Carol Danvers", chapter: "CGI-Dev" },
|
||||
{ eid: "charles.xavier", displayName: "Charles Xavier", chapter: "Project Management" },
|
||||
{ eid: "clint.barton", displayName: "Clint Barton", chapter: "Product Data Management" },
|
||||
{ eid: "danny.rand", displayName: "Danny Rand", chapter: "CGI-Dev" },
|
||||
{ eid: "drax.drax", displayName: "Drax Drax", chapter: "Art Direction" },
|
||||
{ eid: "erik.lehnsherr", displayName: "Erik Lehnsherr", chapter: "Digital Content Production" },
|
||||
{ eid: "frank.castle", displayName: "Frank Castle", chapter: "CGI-Dev" },
|
||||
{ eid: "gamora.gamora", displayName: "Gamora Gamora", chapter: "Project Management" },
|
||||
{ eid: "groot.groot", displayName: "Groot Groot", chapter: "Art Direction" },
|
||||
{ eid: "hank.mccoy", displayName: "Hank Mccoy", chapter: "Product Data Management" },
|
||||
{ eid: "james.barnes", displayName: "James Barnes", chapter: "Digital Content Production" },
|
||||
{ eid: "jean.grey", displayName: "Jean Grey", chapter: "Digital Content Production" },
|
||||
{ eid: "jessica.drew", displayName: "Jessica Drew", chapter: "Digital Content Production" },
|
||||
{ eid: "jessica.jones", displayName: "Jessica Jones", chapter: "CGI-Dev" },
|
||||
{ eid: "kamala.khan", displayName: "Kamala Khan", chapter: "CGI-Dev" },
|
||||
{ eid: "logan.howlett", displayName: "Logan Howlett", chapter: "Digital Content Production" },
|
||||
{ eid: "matt.murdock", displayName: "Matt Murdock", chapter: "CGI-Dev" },
|
||||
{ eid: "miles.morales", displayName: "Miles Morales", chapter: "Digital Content Production" },
|
||||
{ eid: "natasha.romanoff", displayName: "Natasha Romanoff", chapter: "Project Management" },
|
||||
{ eid: "ororo.munroe", displayName: "Ororo Munroe", chapter: "Project Management" },
|
||||
{ eid: "peter.parker", displayName: "Peter Parker", chapter: "Digital Content Production" },
|
||||
{ eid: "peter.quill", displayName: "Peter Quill", chapter: "Product Data Management" },
|
||||
{ eid: "rocket.rocket", displayName: "Rocket Rocket", chapter: "Art Direction" },
|
||||
{ eid: "sam.wilson", displayName: "Sam Wilson", chapter: "Digital Content Production" },
|
||||
{ eid: "scott.summers", displayName: "Scott Summers", chapter: "Project Management" },
|
||||
{ eid: "stephen.strange", displayName: "Stephen Strange", chapter: "Digital Content Production" },
|
||||
{ eid: "steve.rogers", displayName: "Steve Rogers", chapter: "Project Management" },
|
||||
{ eid: "tchalla.tchalla", displayName: "Tchalla Tchalla", chapter: "Art Direction" },
|
||||
{ eid: "thor.odinson", displayName: "Thor Odinson", chapter: "Digital Content Production" },
|
||||
{ eid: "tony.stark", displayName: "Tony Stark", chapter: "Digital Content Production" },
|
||||
{ eid: "vision.vision", displayName: "Vision Vision", chapter: "Art Direction" },
|
||||
{ eid: "wanda.maximoff", displayName: "Wanda Maximoff", chapter: "Digital Content Production" },
|
||||
{ eid: "wong.wong", displayName: "Wong Wong", chapter: "Digital Content Production" },
|
||||
];
|
||||
|
||||
let count = 0;
|
||||
for (const resource of resources) {
|
||||
// Use a hash-like seed from the eid string
|
||||
const seed = resource.eid.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0) * 31 + 1;
|
||||
const profile = buildSkillProfile(resource, seed);
|
||||
const wb = createWorkbook(resource, profile);
|
||||
|
||||
const filename = `${resource.eid}.xlsx`;
|
||||
const filepath = `${OUT_DIR}/${filename}`;
|
||||
XLSX.writeFile(wb, filepath);
|
||||
|
||||
const mainSkills = profile.softwareRows
|
||||
.filter((r) => r["main skillset"] === "1" || r["main skillset"] === "2")
|
||||
.map((r) => r.item);
|
||||
|
||||
console.log(`✓ ${filename} — ${profile.areaOfExpertise}, ${profile.yearsOfExperience}y, main: [${mainSkills.join(", ")}]`);
|
||||
count++;
|
||||
}
|
||||
|
||||
console.log(`\nGenerated ${count} skill matrix files in ${OUT_DIR}`);
|
||||
Reference in New Issue
Block a user