# Calculation Reference How every number in Planarchy is derived. All monetary values are integer cents. All percentages are 0-100 integers unless noted. --- ## 1. Allocation Costs **Source:** `packages/engine/src/allocation/calculator.ts` | Metric | Formula | Notes | |--------|---------|-------| | Daily Cost | `round(hoursPerDay x lcrCents)` | LCR = Labor Cost Rate (cents/hour) | | Total Cost | `round(lcrCents x hoursPerDay x workingDays)` | Mon-Fri only, minus vacation days | | Working Days | Calendar days minus weekends minus vacations | Sat included only if `includeSaturday=true` | **Inputs:** - `lcrCents` — from Resource record (integer cents per hour) - `hoursPerDay` — from Allocation record - `startDate` / `endDate` — allocation period **Edge cases:** - Null LCR -> cost = 0 - Recurring allocations use `getRecurringHoursForDay()` per-day override - Availability-aware: capped at resource's available hours for that weekday - Vacation days block hours completely (reduce to 0 for that day) --- ## 2. Chargeability ### 2a. Allocation-level Chargeability **Source:** `packages/engine/src/allocation/chargeability.ts` ``` availableHours = SUM(availability[weekday] for each working day in period) bookedHours = SUM(min(allocation.hoursPerDay, availability[day]) for each overlapping day) chargeability = availableHours > 0 ? min(100, round(bookedHours / availableHours x 100)) : 0 ``` - Capped at 100 (overbooking not reflected) - Only counts days where resource has availability > 0 ### 2b. Forecast Chargeability (Chargeability Report) **Source:** `packages/engine/src/chargeability/calculator.ts` Per resource per month: ``` For each assignment overlapping the month: hours = hoursPerDay x workingDays(in overlap) categoryHours[categoryCode] += hours chg = min(1, chargeableHours / SAH) bd = min(1, bdHours / SAH) mdi = min(1, mdiHours / SAH) ... unassigned = min(1, max(0, SAH - totalAssigned) / SAH) ``` Categories come from the project's `utilizationCategory.code`. All ratios sum to <= 1.0. ### 2c. Group Chargeability (FTE-weighted) ``` groupChg = SUM(fte_i x chg_i) / SUM(fte_i) ``` Returns 0-1 ratio. Used for chapter/team aggregations. --- ## 3. Standard Available Hours (SAH) **Source:** `packages/engine/src/sah/calculator.ts` SAH is the denominator for all chargeability ratios. ``` For each day in period: if weekend -> weekendDays++ else if holiday -> publicHolidayDays++ else if absent -> absenceDays++ else: hoursForDay = getDailyHours(day, baseHours, scheduleRules) x FTE totalHours += hoursForDay netWorkingDays++ SAH = round(totalHours x 100) / 100 ``` **Schedule rules** (e.g. Spain): - Friday: reduced hours (`fridayHours`) - Summer period: reduced hours (`summerHours`) - Default: `regularHours` **Components returned:** | Field | Meaning | |-------|---------| | calendarDays | Total days (inclusive) | | weekendDays | Sat + Sun | | grossWorkingDays | calendarDays - weekendDays | | publicHolidayDays | Holidays falling on working days | | absenceDays | Vacations/absences on working days | | netWorkingDays | Gross - holidays - absences | | effectiveHoursPerDay | Weighted average including FTE | | standardAvailableHours | **SAH** (total for period) | --- ## 4. Budget **Source:** `packages/engine/src/budget/monitor.ts` ``` For each allocation: totalCents = dailyCostCents x workingDays(Mon-Fri) CONFIRMED/ACTIVE -> confirmedCents += totalCents PROPOSED -> proposedCents += totalCents allocatedCents = confirmedCents + proposedCents remainingCents = budgetCents - allocatedCents utilizationPct = budgetCents > 0 ? (allocatedCents / budgetCents) x 100 : 0 winWeightedCents = round(allocatedCents x winProbability / 100) ``` **Warning thresholds** (`packages/shared/src/constants/index.ts`): | Level | Threshold | |-------|-----------| | INFO | >= 70% | | WARNING | >= 85% | | CRITICAL | >= 95% or exceeded | --- ## 5. Vacation Balance **Source:** `packages/api/src/router/entitlement.ts` ### Day counting ``` countDays(start, end, isHalfDay) = isHalfDay ? 0.5 : round((end - start) / 86400000) + 1 ``` Calendar days, inclusive. Half-day always = 0.5. ### Carryover (lazy, on first access) ``` carryover = max(0, prev.entitledDays - prev.usedDays - prev.pendingDays) new.entitledDays = defaultDays + carryover ``` ### Balance sync ``` usedDays = SUM(countDays(...)) where status=APPROVED and type in [ANNUAL, OTHER] pendingDays = SUM(countDays(...)) where status=PENDING and type in [ANNUAL, OTHER] remaining = max(0, entitledDays - usedDays - pendingDays) sickDays = SUM(countDays(...)) where status=APPROVED and type=SICK (informational, does not consume balance) ``` --- ## 6. Estimates **Source:** `packages/engine/src/estimate/metrics.ts` ### Per demand line ``` costTotalCents = round(hours x costRateCents) priceTotalCents = round(hours x billRateCents) ``` ### Estimate summary ``` totalHours = SUM(line.hours) totalCostCents = SUM(line.costTotalCents) totalPriceCents = SUM(line.priceTotalCents) marginCents = totalPriceCents - totalCostCents marginPercent = totalPriceCents > 0 ? round(marginCents / totalPriceCents x 100) : 0 ``` --- ## 7. Effort Rules **Source:** `packages/engine/src/estimate/effort-rules.ts` Converts scope items into demand lines: ``` unitCount = case unitMode: "per_frame" -> frameCount ?? 1 "per_item" -> itemCount ?? 1 "flat" -> 1 hours = round(unitCount x hoursPerUnit x 100) / 100 ``` Rules match by `scopeType` (case-insensitive). Sorted by `sortOrder`. Unmatched scope items generate warnings. Lines are aggregated by `discipline + "::" + chapter`. --- ## 8. Experience Multipliers **Source:** `packages/engine/src/estimate/experience-multiplier.ts` ### Rule matching (hierarchical specificity) | Specificity | Filters present | Score | |-------------|-----------------|-------| | Most specific | chapter + location + level | 7 | | | chapter + location | 6 | | | chapter + level | 5 | | | chapter only | 4 | | | location + level | 3 | | | location only | 2 | | | level only | 1 | | Least specific | none (global fallback) | 0 | Highest score wins. Ties broken by order. ### Rate adjustment ``` adjustedCostRate = round(costRateCents x costMultiplier) adjustedBillRate = round(billRateCents x billMultiplier) ``` ### Shoring ratio ``` if shoringRatio > 0: onsiteHours = hours x (1 - shoringRatio) offshoreHours = hours x shoringRatio x (1 + additionalEffortRatio) adjustedHours = round((onsiteHours + offshoreHours) x 100) / 100 ``` --- ## 9. Staffing Suggestions **Source:** `packages/staffing/src/skill-matcher.ts` ### Component scores (each 0-100) **Skill Score:** ``` if no required skills: 100 else: requiredScore = SUM((proficiency/5) x (70/requiredCount)) for matched skills if preferred skills exist: preferredScore = SUM((proficiency/5) x (30/preferredCount)) for matched score = min(100, round(requiredScore + preferredScore)) else: score = min(100, round(requiredScore / 70 x 100)) ``` **Availability Score:** ``` no conflicts: 100 else: max(0, 100 - conflictDays x 10) ``` **Cost Score:** ``` no budget: 50 (neutral) within budget: 100 over budget: max(0, round(100 - (ratio - 1) x 100)) where ratio = resourceLCR / budgetLCR ``` **Utilization Score:** ``` gap = target - current gap >= 20: 100 gap >= 0: 60 + gap x 2 gap >= -20: max(0, 60 + gap x 2) gap < -20: 0 ``` ### Combined score (weights) | Dimension | Weight | |-----------|--------| | Skill | 40% | | Availability | 30% | | Cost | 20% | | Utilization | 10% | ``` total = round(skill x 0.4 + availability x 0.3 + cost x 0.2 + utilization x 0.1) ``` Results sorted descending by total score. --- ## 10. Resource Value Score **Source:** `packages/staffing/src/value-scorer.ts` Five dimensions, each normalized to 0-100: | Dimension | Formula | Weight | |-----------|---------|--------| | Skill Depth | `round(avgProficiency / 5 x 100)` | 30% | | Skill Breadth | `min(100, skillCount x 10)` | 15% | | Cost Efficiency | `round((1 - lcrCents/maxLcrCents) x 100)` | 25% | | Chargeability | `max(0, 100 - abs(target - current) x 2)` | 15% | | Experience | `min(100, round(avgYears x 10))` | 15% | ``` total = clamp(0, 100, round( depth x 0.30 + breadth x 0.15 + costEff x 0.25 + chg x 0.15 + exp x 0.15 )) ``` Recomputed on demand with 90-day lookback for current chargeability. --- ## 11. Dashboard Metrics **Source:** `packages/application/src/use-cases/dashboard/` ### Overview ``` totalCostCents = SUM(dailyCostCents x inclusiveDays) for budget bookings avgUtilizationPct = totalBudgetCents > 0 ? round(totalCost / totalBudget x 100) : 0 ``` ### Peak Times ``` For each allocation, for each day in period: bucket[weekKey or monthKey][group] += hoursPerDay capacityHours = SUM(avgDailyAvailability) x (week=5, month=22) ``` Groups: by project (shortCode), chapter, or resource name. ### Demand Analysis ``` demandFteFactor = percentage > 0 ? percentage/100 : (hoursPerDay/8) allocatedHours = SUM(hoursPerDay x inclusiveDays) requiredFTEs = SUM(headcount x demandFteFactor) ``` --- ## 12. Column Definitions **Source:** `packages/shared/src/constants/columns.ts` ### Allocation Columns | Key | Label | Default | Hideable | Derivation | |-----|-------|---------|----------|------------| | resource | Resource | visible | no | `assignment.resource.displayName` | | project | Project | visible | no | `project.shortCode + project.name` | | role | Role | visible | yes | `allocation.role` (text label) | | dates | Dates | visible | yes | `startDate -> endDate` formatted | | hoursPerDay | h/day | visible | yes | `allocation.hoursPerDay` | | cost | Daily Cost | hidden | yes | `round(hoursPerDay x lcrCents) / 100` in EUR | | status | Status | visible | yes | PROPOSED / CONFIRMED / ACTIVE / COMPLETED / CANCELLED | ### Resource Columns | Key | Label | Default | Derivation | |-----|-------|---------|------------| | displayName | Name | visible | Direct from DB | | eid | EID | visible | Employee ID (anonymized when enabled) | | chapter | Chapter | visible | Organizational unit | | roles | Roles | visible | From ResourceRole join table | | chargeability | Chargeability | visible | See Section 2a | | lcr | LCR | hidden | Labor Cost Rate (cents/hour), requires VIEW_COSTS | | valueScore | Score | hidden | See Section 10 | | isActive | Status | visible | Boolean flag | ### Project Columns | Key | Label | Default | Derivation | |-----|-------|---------|------------| | shortCode | Code | visible | Direct from DB | | name | Name | visible | Direct from DB | | status | Status | visible | DRAFT / ACTIVE / COMPLETED / CANCELLED | | orderType | Type | visible | CHARGEABLE / INTERNAL / INVESTMENT | | dates | Dates | visible | `startDate -> endDate` | | budget | Budget | hidden | `project.budgetCents / 100` in EUR | | allocations | Allocations | visible | Count of linked assignments + demands | --- ## 13. Rounding & Precision Rules | Domain | Rule | |--------|------| | Money | Integer cents everywhere. `round(value)` at calculation boundary | | Hours | 2 decimal places: `round(x * 100) / 100` | | Percentages | Integer 0-100: `round(ratio * 100)` | | Scores | Integer 0-100: `round(weighted_sum)`, clamped | | Days | Integer (or 0.5 for half-day vacations) | | FTE | Decimal (e.g. 0.8), used as multiplier on SAH | --- ## 14. Constants **Source:** `packages/shared/src/constants/index.ts` ``` DEFAULT_WORKING_HOURS_PER_DAY = 8 DEFAULT_AVAILABILITY = { monday-friday: 8h each saturday/sunday: 0h } BUDGET_WARNING_THRESHOLDS = { INFO: 70, WARNING: 85, CRITICAL: 95 } STAFFING_SCORE_WEIGHTS = { SKILL: 0.4, AVAILABILITY: 0.3, COST: 0.2, UTILIZATION: 0.1 } VALUE_SCORE_WEIGHTS = { SKILL_DEPTH: 0.30, BREADTH: 0.15, COST_EFF: 0.25, CHG: 0.15, EXP: 0.15 } ```