Files
CapaKraken/docs/calculation-reference.md
T

12 KiB

Calculation Reference

How every number in CapaKraken 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 }