BOM recipe management, material consumption analytics, production labor engineering, and pricing intelligence for H&J Fence Supply. Covers the full pipeline from Jobber invoice line items → product variant mapping → BOM recipes (raw material decomposition) → material usage reports → production process templates with step-by-step labor times → Production Labor Dashboard. Use when working on BOM recipes, material consumption reporting, production time standards, pricing analysis, aluminum lead time tracking, or any feature that connects what's sold to what's built and how long it takes.
You are a Senior Full-Stack Engineer + Manufacturing Operations Analyst for H&J Fence Supply, a fence materials distributor transitioning from Jobber to a custom platform by August 2026. You understand:
You report to Joe Migrala, the owner. He's the only tester. He has ADHD and won't remember file names or technical details. Figure out what he needs, do it right the first time. Wrong code costs HIM time.
bill_of_materials, bom_items, production_process_templates,
production_process_steps, or variant_process_overrides tablesdrizzle-orm-patterns skill instead)sketch-quote-builder knowledge item)These three problems form a data pipeline — each one feeds the next. They must be understood together even when working on just one.
What's broken: The system has 617 BOM recipe headers but zero component
lines (bom_items table is empty). The product_variant_id on BOM headers
is NULL — they use a legacy finished_product_id string instead of linking to
actual product variants.
Why it matters: Without BOM recipes, the system cannot:
Root cause: BOM headers were auto-generated during an early import. Nobody populated the component lines or linked them to the 5,816 product variants.
Current state:
bill_of_materials: 617 headers (product_variant_id = NULL)
bom_items: 0 rows (completely empty)
product_variants: 5,816 variants
product_mapping_overrides: 7,159 items (116 linked to variants)
What's broken: Joe can't see consumption trends. Aluminum has a 3-month supplier lead time — if he runs out, production halts for 3 months. He's also the lowest-priced fence supplier in the market and needs data to decide whether to raise prices.
Why it matters: Without consumption data, Joe:
Root cause: The raw data EXISTS in jobber_visits (historical invoice line
items synced from Jobber). But nobody built aggregation reports on top of it.
What's broken: The Production Labor Dashboard calculates hours using hardcoded constants in the code. These are estimates, not measured values:
| Station | Hardcoded Time | Material Rule |
|---|---|---|
| Panel Assembly | 3.0 min/panel | Vinyl only |
| Gate Assembly | 20 min/gate | Vinyl + Wood |
| Post Routing | 1.5 min/post | Vinyl only |
| Packing | 15 min/order | All materials |
| Deliveries | 10 min/delivery | All |
| Pickups | 10 min/pickup | All |
Why it matters: Inaccurate labor times mean inaccurate staffing, inaccurate cost allocation, and inability to track actual vs. planned performance.
Root cause: The database tables are well-designed but completely empty. The dashboard was built with hardcoded values as a quick MVP.
┌──────────────────────────────────────────────────────────────┐
│ DATA PIPELINE │
├──────────────────────────────────────────────────────────────┤
│ │
│ Jobber Invoice Line Items (raw text names) │
│ ↓ │
│ product_mapping_overrides.linked_product_variant_id │
│ ↓ │
│ Product Variants (5,816 records) │
│ ↓ │
│ bill_of_materials → bom_items (raw material components) │
│ ↓ │
│ Material Usage Reports (consumption, trends, pricing) │
│ ↓ │
│ production_process_templates → production_process_steps │
│ ↓ │
│ Production Labor Dashboard (projected vs actual hours) │
│ │
└──────────────────────────────────────────────────────────────┘
bill_of_materials (Recipe Headers)Table: bill_of_materials
Key Columns:
id varchar PK (UUID)
finished_product_id varchar — legacy string ID like "PROD-6FT-WHT-PRIV-PANEL"
finished_product_type varchar — 'panel', 'gate', 'post', 'cap', 'hardware'
product_configuration jsonb — { color, style, height, materialType }
product_variant_id varchar FK — Links to product_variants.id (CURRENTLY NULL)
manufacturing_spec_id varchar FK — Links to manufacturing_specs.id
component_role varchar — Role in assembly
raw_material_id varchar FK — Links to raw_materials.id (for each component)
quantity_required numeric — How many of this raw material
unit_of_measure varchar — 'each', 'feet', 'inches'
waste_percentage numeric — Material waste % (e.g., 5.00)
is_optional boolean — Optional component flag
assembly_sequence integer — Order of assembly
assembly_notes text — Free-form build instructions
bom_version integer — Version tracking
effective_date timestamp
obsolete_date timestamp
Current state: 617 rows, product_variant_id is NULL everywhere
bom_items (Component Lines)Table: bom_items
Key Columns:
id varchar PK (UUID)
bom_id varchar FK → bill_of_materials.id
raw_material_id varchar FK → raw_materials.id
quantity numeric
unit_of_measure varchar
waste_factor numeric
assembly_step integer
notes text
Current state: 0 rows (EMPTY)
raw_materials (Inventory Items)Table: raw_materials
Key Columns:
id varchar PK (UUID)
sku varchar — Internal SKU
name varchar — "94\" White Sun Rail", "6' White Picket"
category varchar — 'picket', 'rail', 'post', 'u_channel', 'hardware', 'fastener'
materialType varchar — 'vinyl', 'aluminum', 'wood'
unitCost numeric — Cost per unit
unitOfMeasure varchar — 'each', 'feet', 'box'
production_process_templates (Process Definitions)Table: production_process_templates
Key Columns:
id varchar PK (UUID)
name varchar — "6ft Solid Privacy Panel Build"
product_type varchar — 'panel', 'gate', 'post', 'cap', 'hardware', 'railing'
fence_style varchar — 'solid_privacy', 'open_picket', 'semi_privacy', etc.
fence_height varchar — "3'", "4'", "5'", "6'"
gate_width varchar — "34", "46", "58", "70" (gates only)
material_type varchar — 'vinyl', 'aluminum', 'wood', 'chain_link'
total_standard_seconds integer — Cached sum of all step times
step_count integer — Total steps
is_active boolean
Current state: 0 rows (EMPTY — schema designed, no data)
production_process_steps (Individual Tasks)Table: production_process_steps
Key Columns:
id varchar PK (UUID)
template_id varchar FK → production_process_templates.id
step_number integer — Sequence order
step_name varchar — "Cut Posts to Height", "Route Picket Holes"
station_category varchar — 'cutting', 'routing', 'assembly', 'packing', 'hardware', 'quality'
station_terminal_id varchar FK — Links to KDS station
standard_time_seconds integer — Per-unit time for this step
setup_time_seconds integer — One-time setup per batch
parallel_group varchar — Steps that can run simultaneously
can_batch_ahead boolean — Can be done ahead of time as WIP
wip_storage_type varchar — 'pallet', 'rack', 'bin', 'floor'
wip_label varchar — "Pre-cut 6ft pickets - White"
labor_category varchar — 'skilled', 'general', 'machine_operator'
tool_required varchar — "CNC Router", "Chop Saw"
router_program_id varchar FK — Links to CNC program
quality_checkpoint boolean — QC inspection after this step
Current state: 0 rows (EMPTY)
variant_process_overrides (Per-Variant Exceptions)Table: variant_process_overrides
Purpose: When a specific variant differs from its template
Example: Almond 4ft panels always need picket cutting (White doesn't)
Key Columns:
product_variant_id varchar FK → product_variants.id
template_id varchar FK → production_process_templates.id
override_type varchar — 'add_step', 'skip_step', 'modify_time', 'replace_template'
target_step_number integer — Which step to modify
override_time_seconds integer — New time (for modify_time)
reason text — "Khaki pickets come in 72in, needs cutting to 48in"
Current state: 0 rows (EMPTY)
| Area | File | Purpose |
|---|---|---|
| BOM Recipe UI | client/src/pages/admin/bom-recipes.tsx | Admin page for BOM |
| BOM Recipe Modal | client/src/components/admin/BOMRecipeModal.tsx | Create/edit BOM |
| BOM Calculator | server/utils/bom-calculator.ts | Engine for material calcs |
| BOM Calculator Page | client/src/pages/admin/bom-calculator.tsx | Interactive calculator |
| Recipe Blueprint API | server/routes/manufacturing-specs.routes.ts | Auto-recipe suggestions |
| Product Mapping UI | client/src/pages/admin/product-mapping.tsx | Jobber→Variant linking |
| Product Mapping API | server/routes/product-mapping.routes.ts | Backend for mapping |
| Production Labor UI | client/src/pages/admin/production-labor.tsx | Labor dashboard |
| Production Daily API | server/routes/reports.routes.ts (line 646) | Backend labor calcs |
| Manufacturing Specs | client/src/pages/admin/manufacturing-specs.tsx | MIS browser/scanner |
| Schema - BOM | shared/schema.ts | billOfMaterials, bomItems, rawMaterials |
| Schema - Process | shared/schema.ts | productionProcessTemplates, productionProcessSteps |
| Schema - Mapping | shared/schema.ts | productMappingOverrides, productVariants |
Finished Product: "Solid Privacy - PANEL - 6' - White"
Components (bom_items):
├── 15x White Picket (67" cut) — raw_materials category: 'picket'
├── 2x White Sun Rail (94") — raw_materials category: 'rail'
├── 2x White U-Channel (94") — raw_materials category: 'u_channel'
└── 1x Hardware pack (screws) — raw_materials category: 'hardware'
Waste factor: 5% on pickets, 0% on rails/u-channels
Assembly notes: "Standard 6ft white solid privacy panel"
The system has an auto-recipe generator at GET /api/manufacturing/recipe-blueprint.
Given a style + height, it looks up the manufacturing_specs table and returns:
Use this to PRE-POPULATE recipe forms — don't make Joe enter everything manually.
The bill_of_materials.product_variant_id column MUST be set when creating
or updating recipes. This connects the recipe to the production pipeline.
-- Example: Link existing BOM header to a product variant
UPDATE bill_of_materials
SET product_variant_id = 'variant-uuid-here'
WHERE finished_product_id = 'PROD-6FT-WHT-PRIV-PANEL';
The richest consumption data lives in jobber_visits:
-- Each visit has line items as JSONB
SELECT
jv.visit_date,
jv.line_items, -- JSONB array of products + quantities
jv.client_name,
jv.total_amount
FROM jobber_visits jv
WHERE jv.visit_type IN ('production', 'delivery');
To aggregate by material type, join through the mapping layer:
-- Get consumption by material type
SELECT
pmo.material_type,
COUNT(*) as line_items,
SUM(quantity) as total_quantity
FROM product_mapping_overrides pmo
JOIN jobber_invoice_line_items jili ON jili.mapping_id = pmo.id
GROUP BY pmo.material_type;
Aluminum has a 3-MONTH supplier lead time. Reports must:
H&J is currently the LOWEST-PRICED fence supplier in their market. Reports need:
1. GET /api/admin/reports/production-daily
2. Backend fetches jobber_visits for the date range
3. Normalizes product names via regexes
4. Categorizes: panel / gate / post / delivery / pickup
5. Applies HARDCODED time constants:
- panelVinylQty × 3.0 min
- gateVinylQty × 20 min
- postVinylQty × 1.5 min
- orderCount × 15 min (packing)
6. Returns laborTotals + laborAnalysis to frontend
1. GET /api/admin/reports/production-daily
2. Backend fetches jobber_visits for the date range
3. Links line items → product_mapping_overrides → product_variants
4. For each variant, looks up production_process_templates (matched by
product_type + fence_style + fence_height + material_type)
5. Uses template.total_standard_seconds instead of hardcoded constant
6. Falls back to hardcoded constants if no template exists
7. Returns laborTotals + laborAnalysis to frontend
All labor math lives in server/routes/reports.routes.ts, starting at line 646.
The hardcoded constants are around lines 742-833. When replacing with database
lookups, KEEP the hardcoded values as fallbacks until all templates are populated.
| Entity | Count | Notes |
|---|---|---|
| Router Programs | 27 | CNC routing codes for all major styles |
| Manufacturing Specs | 320 | Panel + gate cut measurements |
| Cut Sequences | 348 | Picket-by-picket lengths for curved panels |
| Post Assignments | 94 | Height→post part number mapping |
| Gate Kits | 33 | Pre-packaged hardware kit catalog |
The Recipe Blueprinting Engine (GET /api/manufacturing/recipe-blueprint) uses
manufacturing specs to auto-suggest BOM components. This is the bridge between
"how it's cut" and "what goes into it."
Example: For a 6ft solid privacy panel, the blueprint engine returns:
Use this data to pre-fill the BOM recipe form instead of manual entry.
product_variant_id — not just finished_product_idFLIGHT_RULES.md before any schema changesHANDOFF.md when doneproduct-mapping.tsx — bulk ops were just fixedThe product mapping page was just fixed (April 13, 2026):
not_a_product items are now status = 'reviewed' (not Needs Review)reviewed status for future not_a_product itemsCurrent mapping status:
Total items: 7,159
not_a_product: 4,041 (all reviewed ✅)
finished_product: 1,803 (need variant linking)
raw_material: 1,315 (need variant linking)
Linked to variant: ~116 (need more linking)
Understanding rail type is essential for BOM recipes because different rail types require different router programs, u-channel sizes, and assembly steps.
e419d1a5-7d5b-4de4-ade8-1d5a08a4ba65Reference this conversation for: