PRD โ Time Reporting: model + UX update โ
Status: Draft for Anton's review Author: Eric L Date: 8 June 2026 (v2 โ incorporating WBS proposal alignment) Decision needed by: Sprint planning, week 24
Summary โ
The current Time Reporting work (VUL-319 + VUL-734 + VUL-896) spans three distinct purposes that are not currently distinguished in the backlog. This blurs ownership, makes stories ambiguous, and risks reimplementation across teams.
This PRD reframes the work around three layers, each with a clear owner, consumer, and boundary. It then proposes an eleven-entity data model anchored on a unified Resource registry, a deterministic rule engine, and a complete mobile + desktop UX โ all mapped to the right layer.
One sentence: A single Time Reporting service (Layer 1) derives cost, payroll, and billable streams from one raw shift; company-level workflows (Layer 2) and project-level views (Layer 3) consume the service through one API. No duplicated logic.
WBS alignment (v2 update): This PRD is now aligned with the parallel wbs-proposal.md. Labour CostLines under the project-side WBS materialise from the time service's engine output (write-through on clock-out). The two efforts share twelve resolved decisions (D1โD12, see ยง Alignment with WBS proposal below). The Personnel entity from v1 has been replaced by a unified Resource entity in coresetup, used by planning, time, quotation, and project execution.
Layer 1 โ Time Reporting Service (technical foundation) โ
The standalone microservice that owns the data, the registries, and the engine. Already partially specified in VUL-896.
What lives here โ
| Capability | Description |
|---|---|
Raw time entries (TimeEntry) | Polymorphic โ entity_type + entity_id lets entries reference work_package, work_order, absence, or future consumers. From VUL-896. |
| Registries (time-specific) | SalaryType, Ruleset |
| Rule Engine | Deterministic split of a raw shift into SalaryType segments per the Resource's Ruleset. Materialises labour CostLines on the project side (write-through on clock-out, D5). |
| Derivation API | Reads registries + engine output, produces three streams: cost rows, payroll rows, billable rows. Snapshot rule (D3): materialised values are immutable; registry changes only affect new entries unless an admin explicitly re-derives. |
| Work Schedule Template | Contracted-hours baseline per Resource โ feeds variance detection. |
| Variance log | Append-only daily variance records (actual vs. contracted). |
What doesn't live here (but L1 depends on) โ
| Capability | Where it lives | L1 dependency |
|---|---|---|
| Resource registry (Resource, ResourceType, ResourceSubtype) | Coresetup (D2) | L1 reads Resource for clock-in identity + cost lookup. Shared with planning, quotation, project execution. |
| User โ Resource link | Coresetup (User entity) | L1 reads User.resource_id to resolve session identity โ Resource โ defaults for mobile clock-in. |
| Agreement | Contracts service | L1 reads agreements to drive billable routing through Article. |
| Article + Subtype binding | Coresetup (Article register) | L1 reads articles to route billable rows. New field Article.subtype_id binds time articles to a Subtype. |
Project WorkPackage and CostLine | Projects service (per wbs-proposal.md) | L1 writes labour CostLines into the project WBS on clock-out (D4, D5). |
Why a standalone service โ
VUL-896 already states the why, summarised:
- Work Order microservice + existing Work Package consumer need the same time data
- Absence entries have no parent entity โ breaks any embedded-in-Projects model
- Payroll, invoicing, cost control, dashboards all need one consistent source of truth
- Cost of extraction now (no time data exists in active records) โ zero. Cost later โ high.
Layer 1 stories โ
| Story | New / Existing | Notes |
|---|---|---|
| VUL-896 Time Reporting service extraction | Existing | Anchor โ establishes the service boundary |
VUL-350 Raw Time Block (TimeEntry) | Rewrite | Replace total_hours / billable_hours decimal model with start/end/break/appointed_subtype + polymorphic entity reference (entity_type=work_package per D6) |
| VUL-854 Work Schedule Template | Existing โ keep | Baseline source for variance |
| VUL-352 Variance Detection | Existing โ keep | L1 capability, surfaced in L2 |
| Resource registry (coresetup) | New | Resource + ResourceType (system, D9: LABOR/EQUIPMENT/MATERIAL/SUBCONTRACTOR/SERVICE/OTHER) + ResourceSubtype (tenant, with preset library). User gets nullable resource_id FK. Cross-service: planning + quotation + projects + time all read from here. |
| SalaryType + Ruleset registries | New | Tier multipliers + locale-aware salary codes; rule groups (Overtime, Weekend, Normal in MVP). Lives in the time service. |
| Rule Engine + derivation + CostLine materialisation | New | The 1โN derivation logic; reads registries + Agreement + Article binding; writes labour CostLines under the project WBS with snapshot fields (D3); materialises on clock-out (D5). |
| Agreement registry + resolution chain | New | Lives in contracts service, not time service. Project โ Contract โ Framework โ Company default. |
| Article โ Subtype binding | New | Schema change in coresetup. Adds subtype_id to Article (nullable, backwards-compatible). |
| ResourceSubtype preset library (sv-SE + nl-NL) | New | Seed catalogue of craftsman trades + equipment classes per market. Onboarding tick-list. Versioned so Rise can update presets without overwriting customer customisations. |
Layer 2 โ Company-level UX (HR / payroll surface) โ
The pan-company workflows: attestation, variance review, registry configuration, salary export. Cross-employee, cross-project. Owned by the payroll / HR admin role.
What lives here โ
| Capability | Description |
|---|---|
| Payroll attestation workbench | Period-level bulk attest per Resource; variance pills surface exceptions; ready-to-export preview |
| Variance review | Drill-down per Resource into days under/over schedule |
| Registry config | Admin UI for SalaryType, ResourceSubtype, Ruleset, Agreement, Resource competencies |
| Salary export | Generates per-employee ร date ร salary code rows to Fortnox (SE) / Exact Online / Nmbrs (NL) |
| Billable feed | Emits T&M billable rows for invoicing consumers |
Layer 2 stories โ
| Story | New / Existing | Notes |
|---|---|---|
| VUL-353 Payroll & invoicing export | Rewrite | Add canonical schema. Pulls from L1 derivation API. |
| VUL-351 Approval workflow | Trim | Single-step PM approval, weekly bulk |
| Payroll attestation workbench | New | The desktop workbench drawn in time-views.html |
| Billable feed to invoice draft | New | Split from VUL-353; routes billable rows to T&M invoice drafts |
Layer 3 โ Project-level UX (PM / field worker surface) โ
The user-facing surfaces where time and materials are logged and where projects consume the data. Owned by Anton + frontend.
What lives here โ
| Capability | Description |
|---|---|
| Mobile clock-in/out | Three reporting patterns (pre-select, skip + allocate, switch mid-shift) + end-of-day confirmation with draggable timeline. See eric-time-reporting-complete.html. |
| Mobile material logging | Already covered by VUL-855 + VUL-764 + VUL-752 โ needs reconciliation with the new time model |
| PM Labor cost & billable view | Project-level desktop view โ hours, cost, billable, margin per Resource + activity. See time-views.html. |
| T&M invoice draft | Pulls billable rows from L2 feed, presents as draft lines |
| My Week | Eric's personal mobile dashboard โ articles produced, no prices |
| AI NL time entry | Voice / text โ raw time block, engine handles the rest |
Layer 3 stories โ
| Story | New / Existing | Notes |
|---|---|---|
| VUL-773 AI NL time entry | Rework | Produces raw entries; engine derives the split. No more pre-computed allocations. |
| VUL-855 Combined T&M entry point | Description update in VUL-734 | Time section writes through L1's Raw Time Block API instead of its own decimal model |
| Mobile time reporting flow | New | Clock-in/out + 3 patterns + end-of-day allocation. Single sprint. |
| PM Labor cost & billable view | New | Desktop view from the dashboard mockup |
| My Week โ articles view | New | Personal week view, articles only, no prices |
| VUL-355 Time Analytics | Defer to Full | Tag full |
| VUL-852 My Day suggestions | Defer to Full | Tag full |
| VUL-853 My Day AI layer | Defer to Full | Tag full |
| VUL-851 Spike: event storage | Existing โ keep | Pedro's go/no-go |
The data model โ
Eleven entities. Resource-centred: one row per physical thing (person, machine, material lot), shared across services.
| Entity | Layer / Service | Role |
|---|---|---|
| User | Auth โ coresetup | Login identity. New nullable FK resource_id bridges auth session โ Resource. Worker users have it set; admins/portal users null. |
| Resource | Coresetup | The unified registry. One row per physical thing. Replaces the previous Personnel. Carries type + subtype + (LABOR-only) hourly_cost, ruleset, competencies. |
| ResourceType | Coresetup โ system-defined | Level 1 classification. ~5โ6 fixed values: LABOR, EQUIPMENT, MATERIAL, SUBCONTRACTOR, SERVICE, OTHER. No admin CRUD. |
| ResourceSubtype | Coresetup โ tenant-defined | Level 2 classification. Plumber, Electrician, Excavator 14โ18T, Concrete, etc. Rise ships a preset library per market; companies extend freely. |
| SalaryType | Time service | Tier register (NORMAL, OB1, OB2, OT). Multiplier drives cost; salary_code drives payroll export per locale. |
| Ruleset | Time service | Rules turning a raw shift into SalaryType segments. Assigned per Resource (LABOR only). |
| TimeEntry | Time service (VUL-896) | Polymorphic raw shift. entity_type + entity_id lets entries reference work_package, work_order, absence. |
| WorkSchedule | Time service (VUL-854) | Contracted-hours template per Resource. Feeds variance. |
| VarianceLog | Time service (VUL-352) | Append-only daily variance. Surfaced in Attestation + Variance review. |
| Agreement | Contracts service | Billing rules + price overrides + validity. Resolution chain: Project โ Contract โ Framework โ Company default. |
| Article | Coresetup | Catalog item with default_billable_rate and new subtype_id field binding time articles to a ResourceSubtype. |
Entity-relationship diagram โ
The eleven entities and their references, grouped by owning service. CostLine is shown only as the engine's write-through target โ it is owned by the projects service (per wbs-proposal.md), not the time service.
erDiagram
%% โโโ Coresetup โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
User |o--o| Resource : "resource_id (worker bridge, nullable)"
ResourceType ||--o{ ResourceSubtype : "type_id"
ResourceType ||--o{ Resource : "type_id"
ResourceSubtype ||--o{ Resource : "subtype_id"
ResourceSubtype }o--o{ Resource : "competencies (M:N, LABOR)"
ResourceSubtype |o--o{ Article : "subtype_id (binds time article)"
%% โโโ Time service โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Ruleset |o--o{ Resource : "ruleset_id (LABOR only)"
Ruleset }o--o{ SalaryType : "splits a shift into tier segments"
Resource ||--o{ TimeEntry : "resource_id"
ResourceSubtype |o--o{ TimeEntry : "appointed_subtype_id"
Resource ||--o{ WorkSchedule : "contracted hours"
Resource ||--o{ VarianceLog : "actual vs contracted"
WorkSchedule ||--o{ VarianceLog : "baseline"
%% โโโ Contracts service โโโโโโโโโโโโโโโโโโโโโโโ
Agreement }o--o{ Article : "billing rule -> article (+ price override)"
%% โโโ Engine output โ projects service (external) โโโ
TimeEntry ||--o{ CostLine : "materialised on clock-out (D4, D5)"
SalaryType ||--o{ CostLine : "tier (labour)"
Article ||--o{ CostLine : "billable line (labour)"
User {
Guid id PK
Guid resource_id FK "nullable โ worker users only"
string email
}
Resource {
Guid id PK
Guid type_id FK "to ResourceType"
Guid subtype_id FK "to ResourceSubtype"
Guid ruleset_id FK "to Ruleset โ LABOR only, nullable"
string number
string name
decimal hourly_cost "LABOR only, nullable"
enum status "active|archived"
}
ResourceType {
Guid id PK
enum code "LABOR|EQUIPMENT|MATERIAL|SUBCONTRACTOR|SERVICE|OTHER"
string name
int sort_order
}
ResourceSubtype {
Guid id PK
Guid type_id FK "to ResourceType"
string name
string preset_key "nullable โ preset origin"
decimal default_hourly_cost "nullable"
decimal default_billable_rate "nullable"
enum status "active|archived"
}
SalaryType {
Guid id PK
string code "NORMAL|OB1|OB2|OT"
decimal multiplier "drives cost"
string salary_code "per locale โ drives payroll"
}
Ruleset {
Guid id PK
string name
json rules "shift to SalaryType segments"
}
TimeEntry {
Guid id PK
Guid resource_id FK "to Resource"
Guid appointed_subtype_id FK "to ResourceSubtype, nullable"
string entity_type "work_package|work_order|absence"
Guid entity_id "polymorphic โ null for absence"
timestamp start_time
timestamp end_time
int break_minutes
enum status "draft|submitted|approved"
}
WorkSchedule {
Guid id PK
Guid resource_id FK "to Resource"
decimal contracted_hours
string period
}
VarianceLog {
Guid id PK
Guid resource_id FK "to Resource"
date date
decimal actual_hours
decimal contracted_hours
decimal variance
}
Agreement {
Guid id PK
string scope "Project|Contract|Framework|Company"
json billing_rules "subtype + tier to article + override"
date valid_from
date valid_to
}
Article {
Guid id PK
Guid subtype_id FK "to ResourceSubtype, nullable (new)"
string code
decimal default_billable_rate
}
CostLine {
Guid id PK
Guid work_package_id FK "projects service โ external"
enum cost_kind
Guid resource_ref FK "to Resource"
Guid time_entry_ref FK "to TimeEntry (labour)"
}Cost resolution โ
Cost per segment = (Resource.hourly_cost ?? Subtype.default_hourly_cost) ร SalaryType.multiplierPayroll resolution โ
Payroll row = SalaryType.salary_code_{locale} ร hours per segmentBillable resolution โ
For each segment (per appointed_subtype + salary_type tier):
Find matching BillingRule in resolved Agreement
Route segment to bound Article (via Article.subtype_id)
Apply Agreement's price override or Article's default_billable_rate
Materialise as Labour CostLine under the project's WorkPackage (D4, D5)Snapshot rule (D3) โ
When a labour CostLine is materialised, the engine writes both the FK references AND a snapshot of the resolved values at that moment (snapCostRate, snapMultiplier, snapSellingRate). The engine never silently rewrites historical CostLines. Changes to Resource.hourly_cost, Article.default_billable_rate, or Agreement.billing_rules only affect CostLines materialised after the change. An admin can explicitly trigger "Recompute since date X" โ confirmation-gated. This prevents a typo correction from retroactively changing last quarter's invoiced revenue.
Alignment with WBS proposal โ
The parallel wbs-proposal.md defines the project-execution WBS (Project โ WorkPackage โ CostLine) that materialised labour rows land in. Twelve decisions (D1โD12) reconcile the two efforts:
| # | Decision | Resolution |
|---|---|---|
| D1 | Typed sub-lines vs flat CostLine | Flat โ CostLine carries all type-specific fields conditional on costKind; there are no separate per-kind sub-line entities. |
| D2 | Where Resource lives | coresetup โ shared registry. |
| D3 | hourlyRate modelling | Drop the single hourlyRate ref. Replace with appointedSubtypeRef + salaryTypeRef + articleRef. Engine writes snapshot values. Never updates reported data unless prompted. |
| D4 | Bridge from TimeEntry to CostLine | Back-reference โ each materialised labour CostLine carries timeEntryRef. Mirrors how material CostLines carry purchaseOrderReferences[]. Full audit trail. |
| D5 | When engine writes | Write-through on clock-out (status=submitted). Lines remain editable until PM approves the week. |
| D6 | TimeEntry.entity_type value | Resolved: work_package. WorkOrder is a separate execution entity, not a node inside the WBS. |
| D7 | Two entry flows | Both supported. Time-first (mobile clock-in โ TimeEntry โ engine โ CostLine) for SE small-contractor reality. Work-first (PM pre-fills concept CostLines โ mechanic fills date+employee) for NL formal-execution. Same data outcome. |
| D8 | Concept labour CostLines | Yes, they exist. PM can plan "8h of Electrician work" before any clock-in. Realised lines link to concept via originCalcLine. Enables planned-vs-actual at line level. |
| D9 | costKind enum values | labour, material, subcontracting, equipment, service, other โ both service (predictable outsourced) and other (truly uncategorisable). |
| D10 | TimeEntry vs RawTimeEntry naming | TimeEntry โ matches the WBS proposal's existing diagrams. "Raw" prefix was only meaningful when there were derived sibling tables, which there aren't. |
| D11 | Fixed-price WorkPackages | Engine still derives billable rows for fixed-price, controlled by company-level toggle derive_billable_on_fixed (default ON). Lines are informational for fixed price; the WorkPackage's fixed sellingPriceTotals drives the invoice. |
| D12 | priceDetermination simplification | Accept WBS proposal as-is for MVP (Fixed price / T&M / warranty / additional). Optional suggestion: simplify to Fixed / Billable + flags for warranty / additional / T&M-with-cap. Defer to WBS implementation team. |
These decisions resolve all alignment open questions between this PRD and the WBS proposal. The WBS proposal has been updated to reference them inline.
What the field worker actually sees โ
The whole point of the architecture is to push complexity into one-time setup and keep daily reporting near-trivial.
Eric's normal workday โ 4 taps. Clock in (1) โ optional appointed-subtype override (1) โ clock out (1) โ submit (1). Everything else comes from setup or context. See eric-time-reporting-complete.html for the full UX walkthrough.
The model never appears in his UI. He doesn't see OB1, OB2, salary codes, billable rates, or agreements. He sees: project, activity, hours. The engine handles the rest.
MVP / Full split โ
MVP โ what must ship for SE pilot (week 26):
- All Layer 1 stories
- All Layer 2 stories
- Mobile time reporting flow (Layer 3)
- PM Labor view (Layer 3)
- My Week (Layer 3)
- VUL-855 description update
- 5 existing rewrites + 3 keeps
Full โ deferred, tagged full:
- VUL-355 Time analytics
- VUL-852 / VUL-853 My Day + My Day AI
- Special Rules group (holidays) โ acceptance criterion on Rule Engine story
- Night Work group โ acceptance criterion on Rule Engine story
- Per-Article billable tier overrides โ acceptance criterion on Agreement story
- Per-diem and km automation โ acceptance criterion on Mobile flow story
The model accommodates all Full-tier items without migration.
Changes to existing epics โ
VUL-319 (Time Reporting) โ scope expands and reorganises โ
Scope today: registration / approval / integration with billability derived from contract compensation form. Largely Layer 3-flavoured but with backend mixed in.
Scope proposed: the whole three-layer effort. The epic description gets rewritten to reflect L1 / L2 / L3 explicitly and to anchor VUL-896 inside it.
Stories disposition:
- 5 rewrites (VUL-319, 350, 351, 353, 773)
- 3 keeps (VUL-352, 854, 851)
- 3 defers (VUL-355, 852, 853)
- 10 new (5 L1 incl. Resource registry + preset library, 2 L2, 3 L3)
- VUL-896 anchored as the L1 parent
VUL-734 (Material & Cost Reporting) โ one story description updated โ
Only one change: VUL-855's time section description updated to write through L1's TimeEntry API instead of its own total_hours / billable_hours model. The engine materialises labour CostLine under the project's WorkPackage (per WBS proposal alignment, D4 + D5). VUL-734's overall scope is unchanged.
Project execution WBS โ coordinated, not blocked โ
The wbs-proposal.md initiative defines the project-execution WBS that consumes labour from this PRD. The two efforts are aligned via D1โD12 (see ยง Alignment with WBS proposal). No backlog changes triggered in projects-side epics by this PRD โ but the WBS proposal team needs to adopt D1:B (collapsed sub-lines) and D3:A (drop hourlyRate ref) before their implementation lands.
Decisions Anton needs to make โ
Most of the v1 decisions are resolved by D1โD12 (see ยง Alignment with WBS proposal). Three remain open for Anton:
Mobile flow story scope โ bundle 3 patterns + end-of-day allocation + tier preview into one story (proposed), or split? My estimate: one sprint with a stretch goal.
Layer 2 attestation workbench scope โ one story for workbench + variance review, or split? Proposed: one story; variance review is a tab inside the workbench.
D12 priceDetermination simplification โ adopt the WBS proposal's enum as-is (
Fixed price | T&M | warranty | additional), or push the optional simplification (Fixed | Billable+ flags)? Proposed: accept as-is for MVP, flag the simplification as a future cleanup. The decision affects how Layer 2 / Layer 3 readWorkPackage.priceDetermination.
Decisions for Pedro (backend lead) โ
Most resolved by D1โD12. Two remain:
VUL-896 story tree โ does VUL-896 swallow the registries + engine as part of its own scope, or are they sibling stories that depend on VUL-896? Proposed: siblings. VUL-896 = service extraction + polymorphic
TimeEntry; new stories = registries + engine + CostLine materialisation. Cleaner story-level estimates.Article-Subtype binding sequencing โ adding
subtype_idto Article is a coresetup schema change. Confirm with Mladjan / Filip nothing in VUL-660 (Quote Canvas import) collides. 5-minute Slack check; blocking if it surfaces a conflict.
Joint decisions (Eric + Pedro + WBS proposal author) โ
These are not blocking but benefit from a sync before either initiative goes final:
D1:B sign-off โ โ signed off (WBS author, June 2026). The WBS proposal now documents the flat
CostLineshape (collapsed sub-lines, no labour sub-line) as the migration target. Both documents are aligned.entity_typenaming (D6) โ โ resolved:work_package(aligned with the WBS proposal'sWorkPackagenaming).D11 toggle scope โ
derive_billable_on_fixedcompany-level toggle: confirm default behaviour and admin UI placement.
Risks โ
| Risk | Likelihood | Mitigation |
|---|---|---|
| Model rewrite blocks Quote Canvas integration | Medium | 1-hour design review with Pedro + Mladjan before merging the Article-Subtype binding story |
| Mobile flow scope creep into Full tier | High | Acceptance criteria explicitly mark MVP vs Full; stretch goals separate |
| Fortnox payroll schema rejects tiered rows | Low | Schema spike before Layer 2 development; fallback to flattening at export |
| WBS proposal author rejects D1:B (collapsed sub-lines) | โ Resolved | D1:B signed off by the WBS author (June 2026); both documents are aligned on the flat CostLine. Risk closed โ no fallback to D1:A needed. |
| Engine snapshot rule (D3) misunderstood as caching | Low | Document explicitly that snapshots are immutable values, not cache; admin "Recompute since X" is the only re-derive path |
| Service extraction (VUL-896) creates cross-team coupling | Medium | All consumers go through one API; no internal database access. Standard microservice discipline. |
Backlog impact โ
| Before | After |
|---|---|
| VUL-319: 10 stories | VUL-319: ~18 stories (12 active MVP + 3 deferred Full + cross-layer additions) |
| VUL-734: 5 stories | VUL-734: 5 stories (only VUL-855 description updated) |
| VUL-896 floating in backlog | VUL-896 anchored as L1 parent in VUL-319 |
| Net | +8 stories, but with clear layer ownership and no duplication risk |
The +8 buys: working OB/OT splits, accurate payroll export, working T&M billable feed, three-pattern mobile UX, PM + payroll desktop views, AND clean service architecture that doesn't have to be undone later.
What I want from this PRD โ
- Anton's sign-off on the L2 / L3 stories and the VUL-855 reconciliation
- Pedro's sign-off on the L1 story tree and Agreement / Article-Subtype boundaries
- A 30-minute three-person sync (Eric + Anton + Pedro) before sprint planning to nail down the ordering and the cross-service questions
- Commitment to ship MVP for week 26 SE pilot
Reference โ
- Companion document:
wbs-proposal.mdโ project execution WBS, consumer of labour CostLines materialised by this PRD's engine - Architecture map:
time-reporting-architecture.htmlโ L1 / L2 / L3 overview with artifact linkage - Data & schema:
time-reporting-data-flow.htmlโ eleven entities, schema diagram, Eric's shift walkthrough, view โ data matrix - Design artifact:
eric-time-reporting-complete.htmlโ full consolidated UX - Desktop views:
time-views.htmlโ PM Labor view (L3) + Payroll attestation workbench (L2) - Rule engine specification: Confluence
Rule engine-driven time reporting - Existing Jira: VUL-319, VUL-350, VUL-351, VUL-352, VUL-353, VUL-355, VUL-734, VUL-751, VUL-752, VUL-764, VUL-773, VUL-851, VUL-852, VUL-853, VUL-854, VUL-855, VUL-896
