Releases 1.33 + 1.34 — Appello Platform
A systematic audit of 61 spec files across two releases, identifying data model conflicts, naming inconsistencies, architectural splits, and integration gaps that must be resolved before implementation begins.
Release 1.33 Phase 1 (LEM Foundation) establishes the rate data models that everything downstream depends on. Labor billable rates are added via a new billableValue field on the existing TradeLevelRate model, giving each trade level a customer-facing rate alongside the internal cost rate. Equipment rates get a more ambitious treatment: a polymorphic EquipmentRateValue system (with EquipmentRateLabel and EquipmentRateSchedule) lets companies define rate cards at the equipment, category, or global level with per-unit pricing (hourly, daily, weekly, monthly, yearly). Material billable rates (LEM-3) add a three-tier pricing cascade (MaterialGroup default markup → Material override → Job override) that resolves what to charge the customer for materials, completing the cost vs. billable separation across all three LEM categories. Together, these form the pricing backbone that Work Orders, Equipment P&L, and Material Consumption all consume.
Release 1.33 Phase 2 (T&M Work Orders) is the hero feature of the release. Work Orders are implemented as a type of Job (not a new entity), using the existing PropertyValue system for jobClassification to distinguish T&M jobs from standard jobs. Each Work Order gets structured line items — WorkOrderLaborItem, WorkOrderMaterialItem, and WorkOrderEquipmentItem — that consume the LEM rates for auto-pricing. The feature includes a full lifecycle: draft creation, line item editing, a signature collection workflow (with the new polymorphic Signature and SignatureRequest models), desktop and mobile form experiences, a T&M Work Order Log dashboard, and conversion pipelines that transform approved Work Orders into Invoices (via JobBill) or Change Orders (via EstimateChangeOrderRecord).
Release 1.33 Phases 3–4 (Equipment Module) round out the release by adding financial and operational depth to the Equipment domain. Equipment time entry (EquipmentTimeEntry) captures hours used per job, feeds into an Equipment P&L report, and supports bulk approval workflows. Equipment invoicing generates customer-facing bills from approved time entries using the LEM-2 rate resolution engine. On the operations side, a maintenance subsystem introduces MaintenanceThreshold, MaintenanceLog, MaintenanceSchedule, and MaintenanceScheduleTemplate for threshold-based and scheduled maintenance tracking. Equipment scheduling with a Gantt-style calendar, inspection checklists tied to form submissions, and mobile QR code scanning (Equipment.qrCode) complete the module.
Release 1.34 Phase 1 (Material Foundation) extends the material domain into a proper supply chain. The three-level model — Material (internal catalog) → ManufacturerProduct (manufacturer part numbers) → DistributorPrice (supplier-specific pricing) — enables multi-supplier price comparison. A first-class PurchaseOrder entity replaces the legacy Job.purchaseOrder string field, supporting both customer-facing POs and supplier POs with shared schema. MaterialConsumptionEntry logs material usage per job per day on both desktop and mobile, feeding directly into the Job Financial Summary. This phase is deliberately foundational — it creates the entities that procurement and operations features consume.
Release 1.34 Phases 2–3 (Procurement & Operations) complete the material lifecycle. Price book import (PriceBookImport) allows bulk CSV/Excel upload of distributor pricing with column mapping and validation. Mobile material requests (MaterialRequest, MaterialRequestItem) create a field-to-office workflow where workers request materials and office staff triages and fulfills. Discount management (DistributorDiscount) supports product, category, and global discounts with date ranges. Supplier purchase orders get a full CRUD lifecycle with PDF generation and sending. Receiving (PurchaseOrderReceipt, ReceiptLineItem) tracks partial and complete deliveries with three-way match capability. Inventory management introduces InventoryLocation, InventoryItem, and an append-only InventoryMovement ledger for stock tracking with QR-based mobile lookup. Vendor credits (VendorCredit, VendorCreditLineItem) handle returns and credit memos. Every financial entity feeds into JobFinancialSummaryService, creating a single source of truth for job-level profitability.
| Model | Type | ID Pattern | Key Fields | Relations |
|---|---|---|---|---|
| 1.33 — LEM Foundation (Phase 1) | ||||
TradeLevelRate |
MOD | VarChar(21) |
+billableValue Decimal(10,2) |
TradeLevel, Instance |
EquipmentRateLabel |
NEW | VarChar(21) |
name, isDefault, instanceId |
Instance, EquipmentRateSchedule[] |
EquipmentRateSchedule |
NEW | VarChar(21) |
labelId, ratePer, effectiveDate |
EquipmentRateLabel, EquipmentRateValue[] |
EquipmentRateValue |
NEW | VarChar(21) |
referenceModel, referenceId, value Decimal |
EquipmentRateSchedule (polymorphic: Equipment | EquipmentCategory | Global) |
| 1.33 — T&M Work Orders (Phase 2) | ||||
WorkOrderLaborItem |
NEW | VarChar(21) |
jobId, tradeLevelId, submittedDuration Int, billableRate |
Job, TradeLevel, User |
WorkOrderMaterialItem |
NEW | VarChar(21) |
jobId, materialId?, quantity, unitCost |
Job, Material? |
WorkOrderEquipmentItem |
NEW | VarChar(21) |
jobId, equipmentId, duration Int, ratePer |
Job, Equipment |
Signature |
NEW | VarChar(21) |
referenceModel, referenceId, signatureData Text, signedAt |
User (polymorphic: WorkOrder | other) |
SignatureRequest |
NEW | VarChar(21) |
referenceModel, referenceId, requestedEmail, status |
User?, Signature? |
WorkOrderOtherItem |
NEW | VarChar(21) |
jobId, name, qty, unitPrice |
Job (mobile spec UIS-20h) |
WorkOrderSigner |
NEW | VarChar(21) |
jobId, name, role, order Int |
Job (mobile spec UIS-20h) |
PropertyValue |
MOD | VarChar(21) |
+jobClassification propertyName |
Instance, PropertyOption |
Job |
MOD | VarChar(21) |
+dateOfWork DateTime? @db.Date |
Existing relations |
EstimateChangeOrderRecord |
MOD | VarChar(21) |
+statusChangedAt DateTime? |
Existing relations |
EstimateChangeOrderRecordValue |
MOD | VarChar(21) |
accountItemId nullable |
Existing relations |
| 1.33 — Equipment Module (Phases 3–4) | ||||
EquipmentTimeEntry |
NEW | Int autoincrement() |
equipmentId, jobId, hours Decimal, status Enum |
Equipment, Job, User |
EquipmentBillingConfig |
NEW | Int autoincrement() |
equipmentId, billingType, defaultRate |
Equipment |
MaintenanceThreshold |
NEW | Int autoincrement() |
equipmentId, metric, thresholdValue |
Equipment, MaintenanceLog[] |
MaintenanceLog |
NEW | Int autoincrement() |
equipmentId, type, description, cost Decimal |
Equipment, MaintenanceThreshold? |
MaintenanceSchedule |
NEW | Int autoincrement() |
equipmentId, frequency, nextDue |
Equipment, MaintenanceScheduleTemplate? |
MaintenanceScheduleTemplate |
NEW | Int autoincrement() |
name, frequency, tasks Json |
Instance, MaintenanceSchedule[] |
DowntimeRecord |
NEW | Int autoincrement() |
equipmentId, startDate, endDate, reason |
Equipment |
EquipmentRate |
NEW | Int autoincrement() |
equipmentId, ratePer, rate Decimal, effectiveFrom |
Equipment (UIS-15C) |
EquipmentRateOverride |
NEW | Int autoincrement() |
equipmentRateId, companyId, overrideRate |
EquipmentRate, Company (UIS-15C) |
EquipmentAttachmentRate |
NEW | Int autoincrement() |
equipmentId, attachmentId, rate Decimal |
Equipment, Equipment (UIS-15C) |
Equipment |
MOD | VarChar(21) |
+qrCode String? |
Existing relations |
EquipmentCategory |
MOD | VarChar(21) |
+defaultInspectionFormId String? |
Existing + FormTemplate? |
| 1.34 — Material Foundation (Phase 1) | ||||
ManufacturerProduct |
NEW | VarChar(21) |
materialId, manufacturerId, partNumber, modelNumber |
Material, Company (Manufacturer) |
DistributorPrice |
NEW | VarChar(21) |
manufacturerProductId, distributorId, listPrice, discountPercent? |
ManufacturerProduct, Company (Distributor) |
PurchaseOrder |
NEW | VarChar(21) |
poNumber, type (Customer|Supplier), companyId, status |
Company, Job, PurchaseOrderLineItem[] |
PurchaseOrderLineItem |
NEW | VarChar(21) |
purchaseOrderId, materialId?, quantity, unitPrice |
PurchaseOrder, Material? |
MaterialConsumptionEntry |
NEW | VarChar(21) |
jobId, materialId, quantity, dateOfWork, status |
Job, Material, User |
| 1.34 — Procurement & Operations (Phases 2–3) | ||||
PriceBookImport |
NEW | VarChar(21) |
distributorId, fileName, status, errorLog |
Company (Distributor), User |
MaterialRequest |
NEW | VarChar(21) |
jobId, requestNumber, status, priority |
Job, User, MaterialRequestItem[] |
MaterialRequestItem |
NEW | VarChar(21) |
materialRequestId, materialId?, quantity, description |
MaterialRequest, Material? |
DistributorDiscount |
NEW | VarChar(21) |
distributorId, scope, discountPercent, validFrom/To |
Company, Material?, MaterialGroup? |
PurchaseOrderReceipt |
NEW | VarChar(21) |
purchaseOrderId, receiptNumber, receivedAt, receivedByUserId |
PurchaseOrder, User, ReceiptLineItem[] |
ReceiptLineItem |
NEW | VarChar(21) |
receiptId, poLineItemId, quantityReceived |
PurchaseOrderReceipt, PurchaseOrderLineItem |
InventoryLocation |
NEW | VarChar(21) |
name, type (Warehouse|JobSite|Vehicle), address |
Instance, InventoryItem[] |
InventoryItem |
NEW | VarChar(21) |
materialId, locationId, quantityOnHand, unitCost |
Material, InventoryLocation |
InventoryMovement |
NEW | VarChar(21) |
materialId, type, quantity, fromLocationId?, toLocationId? |
Material, InventoryLocation (append-only ledger) |
VendorCredit |
NEW | VarChar(21) |
creditNumber, supplierId, status, totalAmount |
Company, PurchaseOrder?, VendorCreditLineItem[] |
VendorCreditLineItem |
NEW | VarChar(21) |
vendorCreditId, materialId?, quantity, unitPrice |
VendorCredit, Material? |
Original Issue
Equipment specs (UIS-15*) used Int @id @default(autoincrement()) for all new models, while everything else used String @id @db.VarChar(21) (nanoid).
Original Issue
UIS-15C/15B defined EquipmentRatePer with only 3 values (HOUR, DAY, WEEK) while LEM-2 defined 5 values. Now aligned.
Original Issue
LEM-2 used a polymorphic EquipmentRateValue pattern while UIS-15C used three separate tables. Now unified on the polymorphic approach.
Original Issue
Three different patterns (PropertyValue, const strings, Prisma enums) across domains. Now each domain's pattern is consistent within itself and the equipment dual-layer approach is intentional.
Category
Naming • Spec Consistency • Schema Convention Violations
Deep Analysis Result
Cross-referencing all 15 spec files against the production Prisma schema revealed 33 field name conflicts — far more than the original 7. The Plan (Emma's) is correct in 19 of 25 scoreable cases. The UIS has a systematic xxxById pattern bug — all 6 user FK fields use the wrong suffix. The existing codebase standard is xxxByUserId (20+ instances: createdByUserId, approvedByUserId, submittedByUserId).
Key Conflict Categories
| Category | UIS Pattern | Plan Pattern | Schema Standard | Winner |
|---|---|---|---|---|
| User FK naming (6 fields) | xxxById | xxxByUserId | xxxByUserId (20+ instances) | Plan |
| Status storage (3 models) | Prisma enum | String + const array | Const-string on newer models | Plan |
| ID generation (2 models) | @default(cuid()) / @default(nanoid()) | @id @db.VarChar(21) | App-generated, no @default | Plan |
| Material entity (1 model) | New Product model | Extend existing Material | Material + MaterialGroup exist | Plan |
| Date fields | dateOfWork, expectedDeliveryDate | workDate, deliveryDate | workDate, nounDate pattern | Plan |
| Multi-tenancy | instanceId present | instanceId omitted | Required on all tenant models | UIS |
| Workflow timestamps | submittedAt, committedAt | Not defined | Common on financial entities | UIS |
Structural Issues in UIS
Product model must NOT be built — it conflicts with the existing Material + MaterialGroup + MaterialAttribute architecture that already ships in production. Plan correctly extends these.@default(cuid()) will break — cuid() generates 25-char IDs, but the constraint is VarChar(21). The codebase generates IDs in application code, not schema defaults.PurchaseOrderStatus, MaterialOrderStatus) — the codebase has moved to const-string patterns for flexibility (no migrations needed to add values).instanceId String @db.VarChar(21) to all new models. (2) Add workflow timestamp fields (submittedAt, approvedAt, sentAt) where approval workflows exist. Do not build UIS's Product model. Do not use @default(cuid()) or @default(nanoid()) in schema.Category
Business Logic • Workflow Design • Financial Controls
Deep Analysis Result
The UIS specs (UIS-25, UIS-25b, UIS-17) contain zero mention of VendorCredit. VendorCredit is defined exclusively in Emma's MAT-8 plan. The original "conflict" is actually a design-philosophy gap: Emma's lightweight 3-state model vs the structured approval pattern used by every other financial entity in Appello.
Pending → Applied → VoidApprovalStatus enum: InReview → Approved | RejectedIndustry Standard
In construction ERP (Procore, Sage 300 CRE, Vista, BuildOps), vendor credits always require approval because they directly affect job profitability, AP reconciliation, and audit compliance. Credits without approval create a control gap — anyone could reduce material costs to inflate job margins.
Missing from Plan
appliedAt, appliedById, voidedAt, voidedById)instanceId for multi-tenancycreditDate (when vendor issued the credit)createFromPO, inventory returns, job cost adjustment). Upgrade the status workflow to match existing Appello patterns:Draft → InReview → Approved → Applied → VoidvendorCredit.approve. Add fields: instanceId, approvedById, approvedAt, appliedById, appliedAt, voidedById, voidedAt, creditDate. Use ApprovalStatus enum or equivalent const-string pattern. Only Approved credits can be applied. Void reverses job cost adjustment.Category
Data Architecture • Competing Designs • Performance • Industry Alignment
Deep Analysis Result
Comprehensive comparison of both architectures reveals Emma's InventoryItem FK approach is the correct choice, reversing the original UIS recommendation. The UIS ledger approach is intellectually elegant but not the industry standard for operational inventory management.
InventoryMovement references materialId + fromLocationId / toLocationId directly. One row per transfer. Stock computed by aggregating all movements. O(n) reads, O(1) writes.
InventoryItem is a "bin card" — one row per (material, location) pair with denormalized quantity. Movements FK to the bin card. Transfer = 2 rows. O(1) reads, O(2) writes.
Performance Comparison
| Operation | UIS Ledger | Plan InventoryItem |
|---|---|---|
| Stock check ("what's at Warehouse A?") | O(n) — full scan of movements | O(1) — single SELECT |
| Low-stock alerts | Requires materialized view | WHERE quantity < minQuantity |
| Transfer recording | 1 row | 2 rows + 2 UPDATE |
| Mobile stock check (offline) | Must sync full movement history | Sync InventoryItem table only |
| Multi-warehouse dashboard | 8 parallel aggregations | Simple SELECT across locations |
Industry Alignment
Every major ERP uses the InventoryItem (bin card) pattern: SAP MM, Oracle Inventory, NetSuite, Acumatica, QuickBooks, Fishbowl, Cin7, DEAR Inventory. The ledger approach is used in event-sourced architectures and some startups, but not in any production ERP that Appello competes with or integrates with.
Scaling Concern
For an ICI trade contractor with 5,000 materials across 10 locations, the ledger grows to 12M+ rows/year of movements to aggregate. The InventoryItem table stays at 50,000 rows — instant queries regardless of history volume.
transferGroupId String? @db.VarChar(21) on InventoryMovement to correlate paired transfer movements.instanceId String @db.VarChar(21) on all three models (multi-tenancy).costCodeId String? @db.VarChar(21) on InventoryMovement (mentioned in MAT-7 checkout logic but missing from model).prisma.$transaction() to prevent half-transfers.InventoryStockLevel for reconciliation verification, not primary stock queries.Category
Status Lifecycle • Approval Workflow • Industry Alignment
Deep Analysis Result
The conflict goes deeper than "Issued" vs "Sent" — the two specs define fundamentally different lifecycle models. The Plan uses a lean 5-state model, while the UIS adds a 3-step internal approval workflow before the PO reaches the supplier.
Draft → Sent → PartiallyReceived → FullyReceived → ClosedDraft → Submitted → Approved → Committed → PartiallyReceived → FullyReceived → Closed → CancelledpurchaseOrder.approve permission.
Industry Standard
Procore (market leader): Draft → Processing → Submitted → PartiallyReceived → Received → Approved → Closed/Void. The Approved milestone is where the PO appears in Committed Costs. Neither "Issued" nor "Sent" exists as a status in any major construction ERP — the industry uses Approved as the milestone state.
Existing Appello Pattern
The codebase uses Submitted → Approved for financial entities (ApprovalStatus enum, JobFinancialForecastStatus). The existing Submitted → Approved pattern aligns with the UIS approach, not the Plan.
Resolution: "Sent" as Timestamp, Not Status
A PO can be Approved (financially committed) without being physically emailed to the vendor. Sent is a transmission event, not a workflow gate. Track it as a timestamp.
Draft → Submitted → Approved → PartiallyReceived → FullyReceived → Closed | CancelledIssued and Committed entirely — Approved covers both concepts. Add sentAt DateTime? field instead of Sent status. The "Send to Supplier" button sets this timestamp without changing workflow state. Add Cancelled from UIS. Store as const-string (Plan pattern), not Prisma enum (UIS pattern).Original Issue
Work Order line items stored durations in seconds (Int) while Equipment time entries used hours (Decimal). Now aligned on Int seconds with computed display values.
Deep Analysis Result
Audited all 30 Decimal fields across 11 MAT plan files and cross-referenced against the existing Prisma schema's 5 different precision levels. Emma's Plan is internally consistent (Decimal(10, 2) for all money/qty, Decimal(5, 2) for percentages) but has two precision risks.
Current Precision Map
| Value Type | Plan Precision | Existing Schema | Risk |
|---|---|---|---|
| Unit prices | Decimal(10, 2) | Decimal(10, 2) | Aligned |
| Quantities | Decimal(10, 2) | Decimal(10, 5) on InvoiceLineItem | Precision loss on unit conversion |
| PO/document totals | Decimal(10, 2) | Decimal(12, 2) on forecasts, Decimal(20, 2) on bids | Large POs may exceed $99.9M |
| Percentages | Decimal(5, 2) | Decimal(5, 2) | Aligned |
| Equipment money | Decimal(12, 2) | Decimal(12, 2) on forecasts | Aligned (Apr 7 fix) |
Decimal(10, 2) — keep as-is.Decimal(10, 4) — upgrade from (10,2) for unit conversion safety (m→ft, kg→lb).Decimal(12, 2) — match forecast pattern.Decimal(5, 2) — keep as-is.PurchaseOrder.amount and VendorCredit.amount to Decimal(12, 2). Remaining (10, 2) fields are acceptable for V1 — widening decimals is a non-destructive ALTER.Some models have isArchived Boolean @default(false), others don't. Missing on: PurchaseOrderLineItem, ReceiptLineItem, VendorCreditLineItem, MaterialRequestItem.
UIS defaults to "Processing". Plan defaults to "Pending".
"Pending" is correct — the file has been uploaded but background processing hasn't started yet.UIS defines errorLog as String? @db.Text. Plan defines it as Json?.
Json — structured error data is more useful for displaying error tables in the UI (row number, column, error message per row).UIS has PartialSuccess but omits Pending. Plan has Pending but omits PartialSuccess.
Pending, Processing, Complete, PartialSuccess, Failed. PartialSuccess is essential for imports where some rows succeed and others fail.MAT-1 says "appropriate permission required (specific permission name TBD)". No explicit permission group has been defined for Purchase Order operations.
purchaseOrder: { view, create, edit, delete }. POs involve financial commitments and should have dedicated access control.Different specs define different URL patterns for the same mobile features:
/material-requests/new/job-sites/[id]/material-request/new/jobs/[jobId]/material-consumption/.../job-sites/[id]/material-consumption/...job-sites/[id]/... pattern consistently. This matches the existing mobile app structure where job site is the primary navigation context.UIS places PDF generation in the API: apps/api/src/templates/SupplierPO.pdf.ts. Plan places it in the Frontend: apps/frontend/.../DownloadablePDF/PurchaseOrder/SupplierPO.tsx.
DownloadablePDF pattern. This matches existing Invoice, Estimate, and Work Order PDF patterns in the codebase.Three separate models handle aspects of the signing workflow:
Signature (WO-3a) — stores actual signature images, polymorphicSignatureRequest (WO-8b) — tracks pending signature requestsWorkOrderSigner (UIS-20h mobile) — separate signer management with orderingWorkOrderSigner overlaps significantly with SignatureRequest. Consider making SignatureRequest handle signing order and signer details, eliminating WorkOrderSigner as a separate model.WorkOrderOtherItem (UIS-20h mobile) has name, qty, unitOfMeasure, qtyOfUnit, unitPrice. WorkOrderMaterialItem (WO-2) with materialId = null (blank line) has description, quantity, unitCost. Both serve similar purposes for ad-hoc/unlisted items.
WorkOrderOtherItem is truly needed or if blank WorkOrderMaterialItem lines (with null materialId) suffice. Fewer models means simpler queries and less conversion logic.Only defined for: WO-8b (signature request), MAT-4 (request submitted), UIS-15D (maintenance thresholds). Missing for: PO issuance, receiving completion, low stock alerts, consumption approval, credit approval.
MAT-9 uses material.edit to gate discount CRUD. But discounts affect pricing across POs and P&L — a user who can edit materials shouldn't necessarily be able to change discount percentages that affect financial calculations.
pricing: { view, edit } or discount: { view, edit } permission group to separate material catalog management from pricing/discount management.The Plan adds manufacturerProductId to the Prisma model for price resolution (linking consumption to a specific manufacturer product and its distributor price). The UIS GraphQL type doesn't include this field.
manufacturerProductId to the GraphQL type and creation input. Without it, consumption entries can't resolve to a specific supplier price, making cost computation ambiguous.DistributorPrice has an inline discountPercent Decimal? @db.Decimal(5, 2). DistributorDiscount (MAT-9) is a separate model for managing discounts. Both can set discount percentages — which takes precedence?
DistributorPrice.discountPercent is the per-row import value from price book CSVs. DistributorDiscount is the managed entity with date ranges and scope. MAT-9 says DistributorDiscount takes precedence when present, with DistributorPrice.discountPercent as fallback. Document this cascade clearly in the resolver.UIS uses Decimal(10, 4) for quantity (supports fractional units). Plan uses Decimal(10, 2) for quantity.
Decimal(10, 4) for quantities. Materials may have fractional units like 2.5 rolls, 0.75 gallons, or 12.375 linear feet. Two decimal places truncates valid quantities.WO-3a adds dateOfWork DateTime? @db.Date to Job. UIS-20h mobile adds workDates Json (array of dates for multi-date Work Orders). These represent the same concept differently.
dateOfWork for the primary date. Only add workDates Json? if multi-date Work Orders are a confirmed v1 requirement. If so, dateOfWork becomes the first date and workDates stores additional dates.Four different computation patterns for "price × quantity" across entities:
PurchaseOrderLineItem.total: query-time computed in resolverDistributorPrice.effectivePrice: query-time computedWorkOrderMaterialItem.total: stored field Decimal(10,2) (pre-computed)MaterialConsumptionEntry costs: computed in JobFinancialSummaryService (aggregate)WO Log: CSV export. CO Log: CSV + PDF export. Inventory, Material Requests, PO List: no export mentioned.
ExportService with CSV and PDF support.Most specs mention "handle empty data" in acceptance criteria but no specs define what the empty state looks like.
EmptyState component with icon, message, and action button. Use consistently across all new list views.approveEquipmentTimeEntries(ids), approveMaterialConsumptionEntries(ids), upsertWorkOrderLineItems — batch operations with no per-item error handling defined.
{ success: [...], failed: [...] } with per-item error messages, or use all-or-nothing transactions. Document the convention.Some models have createdAt/updatedAt only. Some have approvedByUserId/approvedAt. No consistent change history logging.
AuditEntry table) for financial entities where change history matters.A Supplier company page could have 9+ tabs: Overview, Jobs, Contacts, Notes, Files, Products & Pricing (MAT-0), Supplier PO (MAT-5), Credits (MAT-8), Discounts (MAT-9).
Modified by 5 features in parallel: MAT-2a (material consumption), MAT-6 (committed PO costs), MAT-7 (inventory checkout costs), MAT-8 (vendor credits), UIS-15A (equipment costs).
WO Labor/Material/Equipment Editors, PO Line Item editor, VendorCredit Line Item editor — all are conceptually "repeating row with qty + price + description" but built as custom components.
RepeatingLineItemEditor base component with configurable columns, add/remove row actions, and total computation.Six different auto-number prefixes: WO-001, CPO-####, SPO-####, MR-####, VC-####, RCV-###. Each defined independently.
AutoNumberService with prefix + instance scoping. Handles incrementing, padding, and prevents collisions.5 new models (MaintenanceThreshold, MaintenanceLog, MaintenanceSchedule, MaintenanceScheduleTemplate, DowntimeRecord) plus 3 new enums. Phase 4 is already labeled "lower priority, deferrable."
Full PO qty vs received qty vs invoiced qty matching requires receipt tracking, invoice line item linking, and computed columns.
Product > Category > Global with date ranges. Standard for B2B distribution pricing.
Plan flags "FIFO vs weighted average" as an open decision. Both require different movement computation logic.
MAT-4 mentions "Convert to PO" as a future feature on material requests. WO-7 has "Convert to CO/Invoice" as an explicit feature. No shared conversion mutation pattern.
convertEntity(sourceType, sourceId, targetType) with pre-fill logic.61 spec files, 0 mention unit tests, integration tests, or E2E tests.
MAT-2b mentions "offline support" for consumption entries. WO-8 mentions "multiple drafts can be open simultaneously." No other mobile features mention offline capability.
Many queries return arrays: materialConsumptionEntries, inventoryMovements, vendorCredits. No offset/limit/cursor pagination defined.
GetListOptions pattern with pagination for all list queries. Construction companies can accumulate thousands of records per entity.| Entity | States | Storage Pattern | Approval |
|---|---|---|---|
Job |
Existing statuses (Active, Closed, etc.) | PropertyValue | No |
Work Order |
Draft → Submitted → AwaitingSignature → Signed → Approved → Invoiced → Void | PropertyValue | Yes (signature + approval) |
EquipmentTimeEntry |
Pending → Approved / Rejected | Prisma Enum | Yes |
MaterialConsumptionEntry |
Draft → Submitted → Approved | Const String | Yes |
PurchaseOrder |
Draft → Issued → PartiallyReceived → FullyReceived → Closed | Const String | No (v1) |
MaterialRequest |
Submitted → Acknowledged → InProgress → Fulfilled / Cancelled | Const String | No (acknowledge workflow) |
VendorCredit |
Draft → Submitted → Approved → Applied (UIS) Pending → Applied / Void (Plan) |
Const String | TBD |
PriceBookImport |
Pending → Processing → Complete / PartialSuccess / Failed | Const String | No |
| Feature | Desktop | Mobile | Notes |
|---|---|---|---|
| 1.33 — LEM Foundation | |||
| LEM-1 Labor Rates | ✓ | — | Settings — desktop only appropriate |
| LEM-2 Equipment Rates | ✓ | — | Settings — desktop only appropriate |
| 1.33 — T&M Work Orders | |||
| WO-1 Job Classification | ✓ | Via WO-6 | Mobile inherits via Job list |
| WO-2 Line Items | ✓ | Via WO-8 | Mobile form creates items |
| WO-3 Create/Edit | ✓ Flyout | ✓ WO-8 Wizard | Different form patterns |
| WO-4 Detail | ✓ | ✓ WO-6 | Simplified mobile detail |
| WO-5 WO Log | ✓ | ✓ WO-6 | Mobile list view |
| WO-6 CO Log | ✓ | — | GAP — low priority |
| WO-7 Conversion | ✓ | — | Office workflow — appropriate |
| WO-8 Mobile Creation | — | ✓ | Mobile-only |
| 1.33 — Equipment Module | |||
| UIS-15H Equipment Time | ✓ | ✓ | Timesheet integration |
| UIS-15E Equipment Search | ✓ | ✓ | QR scanner |
| UIS-15F Equipment Checklist | ✓ | ✓ | Mobile form submission |
| 1.34 — Material Management | |||
| MAT-0 Supply Chain | ✓ | — | Settings — appropriate |
| MAT-1 Customer PO | ✓ | — | GAP — consider view-only mobile |
| MAT-2a Consumption Desktop | ✓ | — | See MAT-2b |
| MAT-2b Consumption Mobile | — | ✓ | Mobile-only |
| MAT-3 Price Book Import | ✓ | — | Admin — appropriate |
| MAT-4 Material Requests | ✓ Inbox | ✓ Form | Field → Office workflow |
| MAT-5 Supplier PO | ✓ | — | Office workflow — appropriate |
| MAT-6 Receiving | ✓ | — | GAP — mobile receiving would be valuable |
| MAT-7 Inventory | ✓ | QR only | Mobile is QR lookup only |
| MAT-8 Vendor Credits | ✓ | — | Office workflow — appropriate |
| MAT-9 Discounts | ✓ | — | Settings — appropriate |
| Feature | Permission Group | View | Create | Edit | Delete | Special |
|---|---|---|---|---|---|---|
| Work Orders | job |
✓ | ✓ | ✓ | ✓ | viewWageDetails |
| Equipment Time | timesheet (existing) |
✓ | ✓ | ✓ | ✓ | — |
| Equipment Rates | equipment |
✓ | — | ✓ | — | — |
| Equipment Maintenance | equipment |
✓ | — | ✓ | — | view_financial, view_maintenance_cost |
| Material Consumption | materialorder |
— | ✓ | — | — | — |
| Material Requests | materialorder |
— | ✓ | — | — | — |
| Purchase Orders | TBD | TBD | TBD | TBD | TBD | — |
| Inventory | inventory |
✓ | ✓ | ✓ | ✓ | — |
| Vendor Credits | vendorCredit |
✓ | ✓ | ✓ | ✓ | — |
| Discounts | material |
— | — | ✓ | — | — |
| Feature | Modifies JobFinancialSummaryService | Cost Type | Computation |
|---|---|---|---|
| UIS-15A Equipment P&L | ✓ Yes | Equipment costs | SUM(approved entries × resolved rate) |
| MAT-2a Material Consumption | ✓ Yes | Material consumption | SUM(qty × resolved price) at query time |
| MAT-6 Receiving | ✓ Yes | Committed material costs | PO issued amount |
| MAT-7 Inventory Checkout | ✓ Yes | Material cost actuals | Checkout qty × unit cost |
| MAT-8 Vendor Credits | ✓ Yes | Credit adjustment | Negative material cost |
xxxById → xxxByUserId pattern bug (6 fields). Do NOT build UIS Product model — use existing Material. Add instanceId and workflow timestamps from UIS to Plan. (CRITICAL-5 — 33 conflicts, Plan wins 19/25)Draft → InReview → Approved → Applied → Void lifecycle with RBAC gate (vendorCredit.approve). Add audit fields: approvedById/At, appliedById/At, voidedById/At, instanceId, creditDate. (CRITICAL-6 — aligns with ApprovalStatus pattern used by 4 existing entities)transferGroupId, instanceId, costCodeId to InventoryMovement. Wrap transfers in prisma.$transaction(). (CRITICAL-7 — industry-universal bin card pattern)Draft → Submitted → Approved → PartiallyReceived → FullyReceived → Closed | Cancelled. Drop "Issued" and "Committed." Track transmission as sentAt DateTime? instead of a status. Store as const-string, not Prisma enum. (CRITICAL-8 — matches Procore, Sage, and existing Appello ApprovalStatus pattern)Decimal(12, 2) for document totals (PO amount, credit amount), Decimal(10, 4) for quantities (unit conversion safety), Decimal(10, 2) for unit prices, Decimal(5, 2) for percentages. (SHOULD-FIX-1 — 30 fields audited)