f5551e33c7
Co-Authored-By: claude-flow <ruv@ruv.net>
440 lines
12 KiB
Markdown
440 lines
12 KiB
Markdown
# 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 }
|
|
```
|