diff --git a/CLAUDE.md b/CLAUDE.md
index 5daf83b..9727525 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,16 +1,16 @@
-# Work Order System — Build TODO
+# Work Order System — Build TODO
> Hand this file to Claude Code. All frontend files use `.mjs` extensions.
-> Stack: Go · Pure JS + Web Components · SQL Server · Docker
+> Stack: Go · Pure JS + Web Components · SQL Server · Docker
---
## Table of Contents
- [Design System](#design-system)
- [Project Structure](#project-structure)
-- [Phase 1 — Foundation](#phase-1--foundation)
-- [Phase 2 — Field Features](#phase-2--field-features)
-- [Phase 3 — Integrations & Admin](#phase-3--integrations--admin)
-- [Phase 4 — Polish & Optimization](#phase-4--polish--optimization)
+- [Phase 1 — Foundation](#phase-1--foundation)
+- [Phase 2 — Field Features](#phase-2--field-features)
+- [Phase 3 — Integrations & Admin](#phase-3--integrations--admin)
+- [Phase 4 — Polish & Optimization](#phase-4--polish--optimization)
- [Component Inventory](#component-inventory)
- [API Endpoints](#api-endpoints)
- [Database Schema](#database-schema)
@@ -24,10 +24,10 @@
```css
:root {
/* Brand */
- --navy: #0D2137; /* Primary dark — sidebar, headers */
- --teal: #0A7EA4; /* Primary accent — buttons, links, active states */
- --teal-lt: #14B8D4; /* Light accent — hover states, highlights */
- --teal-dk: #075E7A; /* Dark accent — pressed states */
+ --navy: #0D2137; /* Primary dark — sidebar, headers */
+ --teal: #0A7EA4; /* Primary accent — buttons, links, active states */
+ --teal-lt: #14B8D4; /* Light accent — hover states, highlights */
+ --teal-dk: #075E7A; /* Dark accent — pressed states */
/* Surfaces */
--bg: #F0F6FA; /* Page background */
@@ -82,7 +82,7 @@
```
### Typography
-Load via `` in `index.html` — no build step needed:
+Load via `` in `index.html` — no build step needed:
```html
@@ -94,13 +94,13 @@ Load via `` in `index.html` — no build step needed:
--font-body: 'Inter', 'Segoe UI', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'Cascadia Code', monospace;
- --text-xs: 0.70rem; /* 11px — labels, caps */
- --text-sm: 0.813rem; /* 13px — helper, meta */
- --text-base: 0.938rem; /* 15px — body */
- --text-md: 1.063rem; /* 17px — subheadings */
- --text-lg: 1.25rem; /* 20px — section headers */
- --text-xl: 1.5rem; /* 24px — page titles */
- --text-2xl: 2rem; /* 32px — dashboard hero numbers */
+ --text-xs: 0.70rem; /* 11px — labels, caps */
+ --text-sm: 0.813rem; /* 13px — helper, meta */
+ --text-base: 0.938rem; /* 15px — body */
+ --text-md: 1.063rem; /* 17px — subheadings */
+ --text-lg: 1.25rem; /* 20px — section headers */
+ --text-xl: 1.5rem; /* 24px — page titles */
+ --text-2xl: 2rem; /* 32px — dashboard hero numbers */
--weight-normal: 400;
--weight-medium: 500;
@@ -115,11 +115,11 @@ Load via `` in `index.html` — no build step needed:
- Inputs: `--border` border, focus ring `--teal` 2px outline-offset, `--radius-sm` corners
- Tables: header row `--surface-2`, alternating rows, sticky header on scroll
- Status pills: colored dot + label, pill shape, light background tint of the status color
-- Icon + label pattern everywhere — use Lucide icons loaded via CDN (no npm)
+- Icon + label pattern everywhere — use Lucide icons loaded via CDN (no npm)
### Icons
```html
-
+
```
Call `lucide.createIcons()` after each render. Use named icons:
@@ -132,152 +132,152 @@ Call `lucide.createIcons()` after each render. Use named icons:
```
workorder/
-├── CLAUDE.md ← !! READ FIRST — conventions for Claude Code
-├── cmd/server/main.go
-├── internal/
-│ ├── api/
-│ │ ├── router.go
-│ │ ├── middleware/
-│ │ │ ├── auth.go
-│ │ │ ├── cors.go
-│ │ │ └── logger.go
-│ │ └── handlers/
-│ │ ├── auth.go
-│ │ ├── workorder.go
-│ │ ├── step.go
-│ │ ├── resource.go
-│ │ ├── attachment.go
-│ │ ├── accounting.go
-│ │ ├── user.go
-│ │ ├── registry.go ← people/vehicle/equipment/material master lists
-│ │ ├── dashboard.go
-│ │ └── report.go
-│ ├── service/
-│ │ ├── workorder.go
-│ │ ├── notification.go
-│ │ ├── spatial.go
-│ │ └── export.go ← CSV/Excel export
-│ ├── repository/
-│ │ ├── db.go
-│ │ ├── workorder.go
-│ │ ├── step.go
-│ │ ├── resource.go
-│ │ ├── attachment.go
-│ │ ├── accounting.go
-│ │ ├── user.go
-│ │ └── registry.go
-│ ├── model/
-│ │ ├── workorder.go
-│ │ ├── step.go
-│ │ ├── resource.go
-│ │ ├── attachment.go
-│ │ ├── accounting.go
-│ │ ├── user.go
-│ │ └── dashboard.go
-│ └── config/config.go
-│
-├── web/ ← All frontend — served as static files by Go
-│ ├── index.html ← App shell, loads fonts, Lucide, Leaflet
-│ ├── app.mjs ← custom element + client router
-│ │
-│ ├── components/
-│ │ ├── layout/
-│ │ │ ├── app-sidebar.mjs ← Left nav — collapsible, mobile drawer
-│ │ │ ├── app-topbar.mjs ← Top bar — breadcrumb, notifications bell, user avatar
-│ │ │ ├── app-root.mjs ← Shell that wires sidebar + topbar +
-│ │ │ └── app-mobile-nav.mjs ← Bottom tab bar for mobile (≤768px)
-│ │ │
-│ │ ├── work-orders/
-│ │ │ ├── wo-list.mjs ← Searchable, filterable WO list
-│ │ │ ├── wo-kanban.mjs ← Kanban board view (by status column)
-│ │ │ ├── wo-form.mjs ← Create / edit full form
-│ │ │ ├── wo-detail.mjs ← Read-only detail — tabs for each section
-│ │ │ ├── wo-checklist.mjs ← Step checklist with check-off + progress bar
-│ │ │ ├── wo-resource-panel.mjs← Assign people, vehicles, equipment, materials
-│ │ │ ├── wo-photo-panel.mjs ← Photo gallery, capture, before/during/after
-│ │ │ ├── wo-map.mjs ← Leaflet map + directions button
-│ │ │ ├── wo-accounting.mjs ← GL, cost center, WBS, billing ref fields
-│ │ │ ├── wo-timeline.mjs ← Audit/activity log feed
-│ │ │ └── wo-print.mjs ← Print-friendly layout for field packets
-│ │ │
-│ │ ├── dashboard/
-│ │ │ ├── dash-root.mjs ← Dashboard page shell
-│ │ │ ├── dash-kpi-card.mjs ← Reusable stat card (number + trend + icon)
-│ │ │ ├── dash-status-chart.mjs← Doughnut chart — WOs by status
-│ │ │ ├── dash-priority-bar.mjs← Bar chart — WOs by priority
-│ │ │ └── dash-recent-feed.mjs ← Recent activity feed
-│ │ │
-│ │ ├── registry/
-│ │ │ ├── people-list.mjs ← Manage crew / technician records
-│ │ │ ├── people-form.mjs
-│ │ │ ├── vehicle-list.mjs ← Manage fleet / vehicles
-│ │ │ ├── vehicle-form.mjs
-│ │ │ ├── equipment-list.mjs ← Manage tools & equipment
-│ │ │ ├── equipment-form.mjs
-│ │ │ ├── material-list.mjs ← Manage materials / inventory
-│ │ │ └── material-form.mjs
-│ │ │
-│ │ ├── users/
-│ │ │ ├── user-list.mjs ← User management (admin only)
-│ │ │ ├── user-form.mjs ← Create / edit user, assign role
-│ │ │ └── user-profile.mjs ← Current user profile + password change
-│ │ │
-│ │ ├── reports/
-│ │ │ ├── report-root.mjs ← Reports landing page
-│ │ │ ├── report-by-status.mjs
-│ │ │ ├── report-by-cost.mjs
-│ │ │ └── report-export.mjs ← CSV / Excel download triggers
-│ │ │
-│ │ └── shared/
-│ │ ├── ui-badge.mjs ← — status + priority pills
-│ │ ├── ui-button.mjs ← — primary/ghost/danger variants
-│ │ ├── ui-card.mjs ← — surface container
-│ │ ├── ui-dialog.mjs ← — modal with backdrop
-│ │ ├── ui-drawer.mjs ← — slide-in panel (mobile forms)
-│ │ ├── ui-toast.mjs ← — success/error notifications
-│ │ ├── ui-spinner.mjs ← — loading state
-│ │ ├── ui-empty.mjs ← — empty state illustration + CTA
-│ │ ├── ui-confirm.mjs ← — "Are you sure?" dialog
-│ │ ├── ui-search.mjs ← — debounced search input
-│ │ ├── ui-tabs.mjs ← — tab bar + panels
-│ │ ├── ui-avatar.mjs ← — initials or photo avatar
-│ │ └── ui-tooltip.mjs ← — hover tooltip
-│ │
-│ ├── lib/
-│ │ ├── api.mjs ← Fetch wrapper, auth header, error handling
-│ │ ├── router.mjs ← Hash/history client router
-│ │ ├── store.mjs ← Reactive state (lightweight signal pattern)
-│ │ ├── auth.mjs ← JWT storage, decode, role checks
-│ │ ├── format.mjs ← Date, currency, phone formatters
-│ │ ├── validate.mjs ← Form field validators
-│ │ └── utils.mjs ← Misc helpers, debounce, deepMerge
-│ │
-│ └── styles/
-│ ├── global.css ← CSS custom properties (design tokens above)
-│ ├── reset.css ← Modern CSS reset
-│ ├── typography.css ← Base font rules
-│ ├── forms.css ← Shared input/select/textarea styles
-│ ├── tables.css ← Shared table styles
-│ └── print.css ← Print overrides for wo-print.mjs
-│
-├── migrations/
-│ ├── 001_initial.sql
-│ ├── 002_resources.sql
-│ ├── 003_attachments.sql
-│ ├── 004_accounting.sql
-│ ├── 005_users_roles.sql
-│ └── 006_audit_log.sql
-│
-├── uploads/ ← Bind-mounted in Docker
-├── Dockerfile
-├── docker-compose.yml
-├── .env.example
-└── go.mod
+├── CLAUDE.md ↠!! READ FIRST — conventions for Claude Code
+├── cmd/server/main.go
+├── internal/
+│ ├── api/
+│ │ ├── router.go
+│ │ ├── middleware/
+│ │ │ ├── auth.go
+│ │ │ ├── cors.go
+│ │ │ └── logger.go
+│ │ └── handlers/
+│ │ ├── auth.go
+│ │ ├── workorder.go
+│ │ ├── step.go
+│ │ ├── resource.go
+│ │ ├── attachment.go
+│ │ ├── accounting.go
+│ │ ├── user.go
+│ │ ├── registry.go ↠people/vehicle/equipment/material master lists
+│ │ ├── dashboard.go
+│ │ └── report.go
+│ ├── service/
+│ │ ├── workorder.go
+│ │ ├── notification.go
+│ │ ├── spatial.go
+│ │ └── export.go ↠CSV/Excel export
+│ ├── repository/
+│ │ ├── db.go
+│ │ ├── workorder.go
+│ │ ├── step.go
+│ │ ├── resource.go
+│ │ ├── attachment.go
+│ │ ├── accounting.go
+│ │ ├── user.go
+│ │ └── registry.go
+│ ├── model/
+│ │ ├── workorder.go
+│ │ ├── step.go
+│ │ ├── resource.go
+│ │ ├── attachment.go
+│ │ ├── accounting.go
+│ │ ├── user.go
+│ │ └── dashboard.go
+│ └── config/config.go
+│
+├── web/ ↠All frontend — served as static files by Go
+│ ├── index.html ↠App shell, loads fonts, Lucide, Leaflet
+│ ├── app.mjs ↠custom element + client router
+│ │
+│ ├── components/
+│ │ ├── layout/
+│ │ │ ├── app-sidebar.mjs ↠Left nav — collapsible, mobile drawer
+│ │ │ ├── app-topbar.mjs ↠Top bar — breadcrumb, notifications bell, user avatar
+│ │ │ ├── app-root.mjs ↠Shell that wires sidebar + topbar +
+│ │ │ └── app-mobile-nav.mjs ↠Bottom tab bar for mobile (≤768px)
+│ │ │
+│ │ ├── work-orders/
+│ │ │ ├── wo-list.mjs ↠Searchable, filterable WO list
+│ │ │ ├── wo-kanban.mjs ↠Kanban board view (by status column)
+│ │ │ ├── wo-form.mjs ↠Create / edit full form
+│ │ │ ├── wo-detail.mjs ↠Read-only detail — tabs for each section
+│ │ │ ├── wo-checklist.mjs ↠Step checklist with check-off + progress bar
+│ │ │ ├── wo-resource-panel.mjs↠Assign people, vehicles, equipment, materials
+│ │ │ ├── wo-photo-panel.mjs ↠Photo gallery, capture, before/during/after
+│ │ │ ├── wo-map.mjs ↠Leaflet map + directions button
+│ │ │ ├── wo-accounting.mjs ↠GL, cost center, WBS, billing ref fields
+│ │ │ ├── wo-timeline.mjs ↠Audit/activity log feed
+│ │ │ └── wo-print.mjs ↠Print-friendly layout for field packets
+│ │ │
+│ │ ├── dashboard/
+│ │ │ ├── dash-root.mjs ↠Dashboard page shell
+│ │ │ ├── dash-kpi-card.mjs ↠Reusable stat card (number + trend + icon)
+│ │ │ ├── dash-status-chart.mjs↠Doughnut chart — WOs by status
+│ │ │ ├── dash-priority-bar.mjs↠Bar chart — WOs by priority
+│ │ │ └── dash-recent-feed.mjs ↠Recent activity feed
+│ │ │
+│ │ ├── registry/
+│ │ │ ├── people-list.mjs ↠Manage crew / technician records
+│ │ │ ├── people-form.mjs
+│ │ │ ├── vehicle-list.mjs ↠Manage fleet / vehicles
+│ │ │ ├── vehicle-form.mjs
+│ │ │ ├── equipment-list.mjs ↠Manage tools & equipment
+│ │ │ ├── equipment-form.mjs
+│ │ │ ├── material-list.mjs ↠Manage materials / inventory
+│ │ │ └── material-form.mjs
+│ │ │
+│ │ ├── users/
+│ │ │ ├── user-list.mjs ↠User management (admin only)
+│ │ │ ├── user-form.mjs ↠Create / edit user, assign role
+│ │ │ └── user-profile.mjs ↠Current user profile + password change
+│ │ │
+│ │ ├── reports/
+│ │ │ ├── report-root.mjs ↠Reports landing page
+│ │ │ ├── report-by-status.mjs
+│ │ │ ├── report-by-cost.mjs
+│ │ │ └── report-export.mjs ↠CSV / Excel download triggers
+│ │ │
+│ │ └── shared/
+│ │ ├── ui-badge.mjs ↠— status + priority pills
+│ │ ├── ui-button.mjs ↠— primary/ghost/danger variants
+│ │ ├── ui-card.mjs ↠— surface container
+│ │ ├── ui-dialog.mjs ↠— modal with backdrop
+│ │ ├── ui-drawer.mjs ↠— slide-in panel (mobile forms)
+│ │ ├── ui-toast.mjs ↠— success/error notifications
+│ │ ├── ui-spinner.mjs ↠— loading state
+│ │ ├── ui-empty.mjs ↠— empty state illustration + CTA
+│ │ ├── ui-confirm.mjs ↠— "Are you sure?" dialog
+│ │ ├── ui-search.mjs ↠— debounced search input
+│ │ ├── ui-tabs.mjs ↠— tab bar + panels
+│ │ ├── ui-avatar.mjs ↠— initials or photo avatar
+│ │ └── ui-tooltip.mjs ↠— hover tooltip
+│ │
+│ ├── lib/
+│ │ ├── api.mjs ↠Fetch wrapper, auth header, error handling
+│ │ ├── router.mjs ↠Hash/history client router
+│ │ ├── store.mjs ↠Reactive state (lightweight signal pattern)
+│ │ ├── auth.mjs ↠JWT storage, decode, role checks
+│ │ ├── format.mjs ↠Date, currency, phone formatters
+│ │ ├── validate.mjs ↠Form field validators
+│ │ └── utils.mjs ↠Misc helpers, debounce, deepMerge
+│ │
+│ └── styles/
+│ ├── global.css ↠CSS custom properties (design tokens above)
+│ ├── reset.css ↠Modern CSS reset
+│ ├── typography.css ↠Base font rules
+│ ├── forms.css ↠Shared input/select/textarea styles
+│ ├── tables.css ↠Shared table styles
+│ └── print.css ↠Print overrides for wo-print.mjs
+│
+├── migrations/
+│ ├── 001_initial.sql
+│ ├── 002_resources.sql
+│ ├── 003_attachments.sql
+│ ├── 004_accounting.sql
+│ ├── 005_users_roles.sql
+│ └── 006_audit_log.sql
+│
+├── uploads/ ↠Bind-mounted in Docker
+├── Dockerfile
+├── docker-compose.yml
+├── .env.example
+└── go.mod
```
---
-## Phase 1 — Foundation
+## Phase 1 — Foundation
**Goal:** Running app with shell, sidebar, auth, and WO CRUD.
### 1.1 CLAUDE.md (create first)
@@ -290,11 +290,11 @@ workorder/
### 1.2 Go Project Bootstrap
- [ ] `go mod init github.com/yourorg/workorder`
- [ ] Add dependencies: `chi/v5`, `sqlx`, `go-mssqldb`, `golang-jwt/jwt/v5`, `google/uuid`, `joho/godotenv`
-- [ ] `internal/config/config.go` — load from env: `ADDR`, `DB_DSN`, `JWT_SECRET`, `UPLOAD_PATH`, `BASE_URL`
-- [ ] `internal/repository/db.go` — `sqlx.Connect`, pool settings (max open 25, max idle 5, lifetime 5min)
-- [ ] `cmd/server/main.go` — wire config → db → router → `http.ListenAndServe`
+- [ ] `internal/config/config.go` — load from env: `ADDR`, `DB_DSN`, `JWT_SECRET`, `UPLOAD_PATH`, `BASE_URL`
+- [ ] `internal/repository/db.go` — `sqlx.Connect`, pool settings (max open 25, max idle 5, lifetime 5min)
+- [ ] `cmd/server/main.go` — wire config → db → router → `http.ListenAndServe`
-### 1.3 Database — Migration 001
+### 1.3 Database — Migration 001
```sql
-- work_orders table (full schema as planned)
-- Computed wo_number column: 'WO-' + zero-padded id
@@ -321,22 +321,22 @@ workorder/
);
-- Seed one admin user
```
-- [ ] `POST /api/auth/login` — bcrypt compare, return `{ token, user }` with 8hr JWT
-- [ ] `POST /api/auth/refresh` — accepts valid token, returns new token
-- [ ] `GET /api/auth/me` — current user from token claims
+- [ ] `POST /api/auth/login` — bcrypt compare, return `{ token, user }` with 8hr JWT
+- [ ] `POST /api/auth/refresh` — accepts valid token, returns new token
+- [ ] `GET /api/auth/me` — current user from token claims
- [ ] JWT middleware: attach user to `context.Context`, return 401 JSON on failure
- [ ] Role helper: `RequireRole(roles ...string)` middleware
### 1.5 Work Order Backend (CRUD)
-- [ ] `model/workorder.go` — `WorkOrder`, `WorkOrderListItem`, `WorkOrderDetail` structs with `db:` and `json:` tags
+- [ ] `model/workorder.go` — `WorkOrder`, `WorkOrderListItem`, `WorkOrderDetail` structs with `db:` and `json:` tags
- [ ] `repository/workorder.go`
- - `List(ctx, filters)` — status, search, priority, parentType, page/limit
- - `GetByID(ctx, id)` — single WO
- - `Create(ctx, wo)` — insert + return generated ID
- - `Update(ctx, wo)` — partial update, always set `updated_at = GETUTCDATE()`
- - `UpdateStatus(ctx, id, status, userID)` — transition + audit log insert
- - `Delete(ctx, id)` — soft delete (add `deleted_at` column)
- - `GetDetail(ctx, id)` — joins WO + steps + resources + attachments + accounting in one call
+ - `List(ctx, filters)` — status, search, priority, parentType, page/limit
+ - `GetByID(ctx, id)` — single WO
+ - `Create(ctx, wo)` — insert + return generated ID
+ - `Update(ctx, wo)` — partial update, always set `updated_at = GETUTCDATE()`
+ - `UpdateStatus(ctx, id, status, userID)` — transition + audit log insert
+ - `Delete(ctx, id)` — soft delete (add `deleted_at` column)
+ - `GetDetail(ctx, id)` — joins WO + steps + resources + attachments + accounting in one call
- [ ] Handlers: `List`, `Get`, `Create`, `Update`, `Delete`, `UpdateStatus`
- [ ] Paginated response: `{ data: [...], meta: { page, per_page, total } }`
@@ -349,14 +349,14 @@ workorder/
- `
-
+
-
+
```
---
-*Start with Phase 1 in order. Each checkbox is one commit. Don't skip CLAUDE.md — it must exist before any code is written.*
+*Start with Phase 1 in order. Each checkbox is one commit. Don't skip CLAUDE.md — it must exist before any code is written.*
---
-# Work Order System — Technical Build Plan
-**Stack:** Go (backend API) · Pure JavaScript + Web Components (frontend) · SQL Server (MSSQL) · Docker
+# Work Order System — Technical Build Plan
+**Stack:** Go (backend API) · Pure JavaScript + Web Components (frontend) · SQL Server (MSSQL) · Docker
---
@@ -1031,7 +1031,7 @@ require (
3. [Database Schema](#3-database-schema)
4. [Go Backend](#4-go-backend)
5. [REST API Design](#5-rest-api-design)
-6. [Frontend — Web Components](#6-frontend--web-components)
+6. [Frontend — Web Components](#6-frontend--web-components)
7. [File & Photo Handling](#7-file--photo-handling)
8. [Maps & Location](#8-maps--location)
9. [Authentication & Authorization](#9-authentication--authorization)
@@ -1044,40 +1044,40 @@ require (
## 1. Architecture Overview
```
-┌─────────────────────────────────────────────────────────┐
-│ Browser Client │
-│ ┌────────────────────────────────────────────────────┐ │
-│ │ Pure JS + Web Components SPA │ │
-│ │ │ │
-│ └───────────────────┬────────────────────────────────┘ │
-└──────────────────────│──────────────────────────────────┘
- │ REST/JSON over HTTPS
-┌──────────────────────▼──────────────────────────────────┐
-│ Go API Server │
-│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
-│ │ Router │ │ Handlers │ │ Service │ │
-│ │ (chi) │ │ │ │ Layer │ │
-│ └──────────┘ └──────────┘ └──────────┘ │
-│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
-│ │ Auth │ │ Upload │ │ Spatial │ │
-│ │ (JWT) │ │ Handler │ │ Utils │ │
-│ └──────────┘ └──────────┘ └──────────┘ │
-└──────────────────────┬──────────────────────────────────┘
- │ sqlx / database/sql
-┌──────────────────────▼──────────────────────────────────┐
-│ SQL Server (MSSQL) │
-│ work_orders · wo_resources · wo_steps · wo_attachments │
-│ wo_accounting · wo_parents · users · resource_registry │
-└─────────────────────────────────────────────────────────┘
+┌─────────────────────────────────────────────────────────â”
+│ Browser Client │
+│ ┌────────────────────────────────────────────────────┠│
+│ │ Pure JS + Web Components SPA │ │
+│ │ │ │
+│ └───────────────────┬────────────────────────────────┘ │
+└──────────────────────│──────────────────────────────────┘
+ │ REST/JSON over HTTPS
+┌──────────────────────▼──────────────────────────────────â”
+│ Go API Server │
+│ ┌──────────┠┌──────────┠┌──────────┠│
+│ │ Router │ │ Handlers │ │ Service │ │
+│ │ (chi) │ │ │ │ Layer │ │
+│ └──────────┘ └──────────┘ └──────────┘ │
+│ ┌──────────┠┌──────────┠┌──────────┠│
+│ │ Auth │ │ Upload │ │ Spatial │ │
+│ │ (JWT) │ │ Handler │ │ Utils │ │
+│ └──────────┘ └──────────┘ └──────────┘ │
+└──────────────────────┬──────────────────────────────────┘
+ │ sqlx / database/sql
+┌──────────────────────▼──────────────────────────────────â”
+│ SQL Server (MSSQL) │
+│ work_orders · wo_resources · wo_steps · wo_attachments │
+│ wo_accounting · wo_parents · users · resource_registry │
+└─────────────────────────────────────────────────────────┘
```
**Key decisions:**
-- **No framework on the frontend** — Custom Elements v1, Shadow DOM, ES Modules. Works in all modern browsers with zero build step.
-- **chi router** for Go — lightweight, idiomatic, stdlib-compatible middleware.
-- **sqlx** for DB access — thin wrapper over `database/sql`, named params, struct scanning.
-- **SQL Server** — matches your existing infrastructure; geography type for spatial data.
+- **No framework on the frontend** — Custom Elements v1, Shadow DOM, ES Modules. Works in all modern browsers with zero build step.
+- **chi router** for Go — lightweight, idiomatic, stdlib-compatible middleware.
+- **sqlx** for DB access — thin wrapper over `database/sql`, named params, struct scanning.
+- **SQL Server** — matches your existing infrastructure; geography type for spatial data.
- **Multipart uploads** stored to local volume (or swap for Azure Blob / S3 later).
-- **JWT** stateless auth — fits well with a SPA + API split.
+- **JWT** stateless auth — fits well with a SPA + API split.
---
@@ -1085,71 +1085,71 @@ require (
```
workorder/
-├── cmd/
-│ └── server/
-│ └── main.go # Entry point, wires everything together
-├── internal/
-│ ├── api/
-│ │ ├── router.go # chi router setup, middleware chain
-│ │ ├── middleware.go # JWT, CORS, logging, recovery
-│ │ └── handlers/
-│ │ ├── workorder.go # WO CRUD handlers
-│ │ ├── resource.go # People / vehicle / equipment assignment
-│ │ ├── step.go # Checklist steps
-│ │ ├── attachment.go # Photo & file upload
-│ │ ├── accounting.go # Accounting code endpoints
-│ │ ├── parent.go # Parent record linking (polymorphic)
-│ │ └── auth.go # Login / token refresh
-│ ├── service/
-│ │ ├── workorder.go # Business logic layer
-│ │ ├── resource.go
-│ │ ├── notification.go # Email/push when WO assigned
-│ │ └── spatial.go # Geocoding, distance calc
-│ ├── repository/
-│ │ ├── workorder.go # SQL queries — work orders
-│ │ ├── resource.go # SQL queries — resources
-│ │ ├── step.go
-│ │ ├── attachment.go
-│ │ └── db.go # sqlx connection pool setup
-│ ├── model/
-│ │ ├── workorder.go # Structs: WorkOrder, WorkOrderSummary
-│ │ ├── resource.go # Person, Vehicle, Equipment, Material
-│ │ ├── step.go # Step, StepCompletion
-│ │ ├── attachment.go # Attachment
-│ │ ├── accounting.go # AccountingCode
-│ │ └── auth.go # User, Claims
-│ └── config/
-│ └── config.go # Env-based config (DB DSN, JWT secret, upload path)
-├── web/ # Frontend — served as static files by Go
-│ ├── index.html # Shell — loads the app component
-│ ├── app.mjs # component, client-side router
-│ ├── components/
-│ │ ├── wo-list.mjs # Work order list / search
-│ │ ├── wo-form.mjs # Create / edit work order
-│ │ ├── wo-detail.mjs # Read-only detail view
-│ │ ├── wo-checklist.mjs # Step checklist with check-off
-│ │ ├── wo-resource-panel.mjs # Assign people, vehicles, equipment
-│ │ ├── wo-photo-panel.mjs # Photo capture and gallery
-│ │ ├── wo-map.mjs # Embedded map + directions
-│ │ ├── wo-accounting.mjs # Accounting code fields
-│ │ ├── wo-status-badge.mjs # Status pill (reusable)
-│ │ └── wo-timeline.mjs # Activity / audit log
-│ ├── lib/
-│ │ ├── api.mjs # Fetch wrapper, auth header injection
-│ │ ├── router.mjs # Tiny hash/history router
-│ │ ├── store.mjs # Lightweight reactive state (no Redux)
-│ │ └── utils.mjs # Formatters, validators
-│ └── styles/
-│ ├── global.css # CSS custom properties (design tokens)
-│ └── reset.css
-├── uploads/ # Stored attachments (bind-mounted in Docker)
-├── migrations/
-│ ├── 001_initial_schema.sql
-│ ├── 002_resource_registry.sql
-│ └── 003_spatial_fields.sql
-├── docker-compose.yml
-├── Dockerfile
-└── .env.example
+├── cmd/
+│ └── server/
+│ └── main.go # Entry point, wires everything together
+├── internal/
+│ ├── api/
+│ │ ├── router.go # chi router setup, middleware chain
+│ │ ├── middleware.go # JWT, CORS, logging, recovery
+│ │ └── handlers/
+│ │ ├── workorder.go # WO CRUD handlers
+│ │ ├── resource.go # People / vehicle / equipment assignment
+│ │ ├── step.go # Checklist steps
+│ │ ├── attachment.go # Photo & file upload
+│ │ ├── accounting.go # Accounting code endpoints
+│ │ ├── parent.go # Parent record linking (polymorphic)
+│ │ └── auth.go # Login / token refresh
+│ ├── service/
+│ │ ├── workorder.go # Business logic layer
+│ │ ├── resource.go
+│ │ ├── notification.go # Email/push when WO assigned
+│ │ └── spatial.go # Geocoding, distance calc
+│ ├── repository/
+│ │ ├── workorder.go # SQL queries — work orders
+│ │ ├── resource.go # SQL queries — resources
+│ │ ├── step.go
+│ │ ├── attachment.go
+│ │ └── db.go # sqlx connection pool setup
+│ ├── model/
+│ │ ├── workorder.go # Structs: WorkOrder, WorkOrderSummary
+│ │ ├── resource.go # Person, Vehicle, Equipment, Material
+│ │ ├── step.go # Step, StepCompletion
+│ │ ├── attachment.go # Attachment
+│ │ ├── accounting.go # AccountingCode
+│ │ └── auth.go # User, Claims
+│ └── config/
+│ └── config.go # Env-based config (DB DSN, JWT secret, upload path)
+├── web/ # Frontend — served as static files by Go
+│ ├── index.html # Shell — loads the app component
+│ ├── app.mjs # component, client-side router
+│ ├── components/
+│ │ ├── wo-list.mjs # Work order list / search
+│ │ ├── wo-form.mjs # Create / edit work order
+│ │ ├── wo-detail.mjs # Read-only detail view
+│ │ ├── wo-checklist.mjs # Step checklist with check-off
+│ │ ├── wo-resource-panel.mjs # Assign people, vehicles, equipment
+│ │ ├── wo-photo-panel.mjs # Photo capture and gallery
+│ │ ├── wo-map.mjs # Embedded map + directions
+│ │ ├── wo-accounting.mjs # Accounting code fields
+│ │ ├── wo-status-badge.mjs # Status pill (reusable)
+│ │ └── wo-timeline.mjs # Activity / audit log
+│ ├── lib/
+│ │ ├── api.mjs # Fetch wrapper, auth header injection
+│ │ ├── router.mjs # Tiny hash/history router
+│ │ ├── store.mjs # Lightweight reactive state (no Redux)
+│ │ └── utils.mjs # Formatters, validators
+│ └── styles/
+│ ├── global.css # CSS custom properties (design tokens)
+│ └── reset.css
+├── uploads/ # Stored attachments (bind-mounted in Docker)
+├── migrations/
+│ ├── 001_initial_schema.sql
+│ ├── 002_resource_registry.sql
+│ └── 003_spatial_fields.sql
+├── docker-compose.yml
+├── Dockerfile
+└── .env.example
```
---
@@ -1159,7 +1159,7 @@ workorder/
### Core Tables
```sql
--- ── Work Orders ───────────────────────────────────────────────────────────────
+-- ── Work Orders ───────────────────────────────────────────────────────────────
CREATE TABLE work_orders (
id INT IDENTITY PRIMARY KEY,
wo_number AS ('WO-' + RIGHT('000000' + CAST(id AS VARCHAR), 6)) PERSISTED,
@@ -1193,7 +1193,7 @@ CREATE TABLE work_orders (
CONSTRAINT chk_wo_priority CHECK (priority IN ('low','normal','high','urgent'))
);
--- ── Resource Registry (master lists) ─────────────────────────────────────────
+-- ── Resource Registry (master lists) ─────────────────────────────────────────
CREATE TABLE resource_people (
id INT IDENTITY PRIMARY KEY,
name NVARCHAR(100) NOT NULL,
@@ -1227,7 +1227,7 @@ CREATE TABLE resource_materials (
active BIT NOT NULL DEFAULT 1
);
--- ── Work Order Resource Assignments ───────────────────────────────────────────
+-- ── Work Order Resource Assignments ───────────────────────────────────────────
CREATE TABLE wo_resources (
id INT IDENTITY PRIMARY KEY,
wo_id INT NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE,
@@ -1239,7 +1239,7 @@ CREATE TABLE wo_resources (
CONSTRAINT chk_resource_type CHECK (resource_type IN ('person','vehicle','equipment','material'))
);
--- ── Checklist Steps ───────────────────────────────────────────────────────────
+-- ── Checklist Steps ───────────────────────────────────────────────────────────
CREATE TABLE wo_steps (
id INT IDENTITY PRIMARY KEY,
wo_id INT NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE,
@@ -1253,7 +1253,7 @@ CREATE TABLE wo_steps (
notes NVARCHAR(MAX) -- Field notes added at completion
);
--- ── Attachments (photos, docs) ────────────────────────────────────────────────
+-- ── Attachments (photos, docs) ────────────────────────────────────────────────
CREATE TABLE wo_attachments (
id INT IDENTITY PRIMARY KEY,
wo_id INT NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE,
@@ -1270,7 +1270,7 @@ CREATE TABLE wo_attachments (
uploaded_at DATETIME2 NOT NULL DEFAULT GETUTCDATE()
);
--- ── Accounting Codes ──────────────────────────────────────────────────────────
+-- ── Accounting Codes ──────────────────────────────────────────────────────────
CREATE TABLE wo_accounting (
id INT IDENTITY PRIMARY KEY,
wo_id INT NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE,
@@ -1280,7 +1280,7 @@ CREATE TABLE wo_accounting (
CONSTRAINT uq_wo_accounting UNIQUE (wo_id, code_type)
);
--- ── Audit Log ─────────────────────────────────────────────────────────────────
+-- ── Audit Log ─────────────────────────────────────────────────────────────────
CREATE TABLE wo_audit_log (
id INT IDENTITY PRIMARY KEY,
wo_id INT NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE,
@@ -1291,7 +1291,7 @@ CREATE TABLE wo_audit_log (
performed_at DATETIME2 NOT NULL DEFAULT GETUTCDATE()
);
--- ── Indexes ───────────────────────────────────────────────────────────────────
+-- ── Indexes ───────────────────────────────────────────────────────────────────
CREATE INDEX ix_wo_status ON work_orders (status);
CREATE INDEX ix_wo_parent ON work_orders (parent_type, parent_id);
CREATE INDEX ix_wo_scheduled ON work_orders (scheduled_start);
@@ -1625,20 +1625,20 @@ func (r *WorkOrderRepo) UpdateStatus(ctx context.Context, id int, status string,
### Status Transition Rules (enforced server-side)
```
-draft → assigned → scheduled → in_progress → pending_review → closed
- ↘ (re-open) ↗
+draft → assigned → scheduled → in_progress → pending_review → closed
+ ↘ (re-open) ↗
```
---
-## 6. Frontend — Web Components
+## 6. Frontend — Web Components
### Design Principles
-- **No build step** — plain `
-```
-Call `lucide.createIcons()` after each render. Use named icons:
-`clipboard-list`, `users`, `truck`, `wrench`, `map-pin`, `camera`, `file-invoice`,
-`layout-dashboard`, `settings`, `bell`, `chevron-left`, `plus`, `search`, `filter`
-
----
-
-## Project Structure
-
-```
-workorder/
-├── CLAUDE.md ← !! READ FIRST — conventions for Claude Code
-├── cmd/server/main.go
-├── internal/
-│ ├── api/
-│ │ ├── router.go
-│ │ ├── middleware/
-│ │ │ ├── auth.go
-│ │ │ ├── cors.go
-│ │ │ └── logger.go
-│ │ └── handlers/
-│ │ ├── auth.go
-│ │ ├── workorder.go
-│ │ ├── step.go
-│ │ ├── resource.go
-│ │ ├── attachment.go
-│ │ ├── accounting.go
-│ │ ├── user.go
-│ │ ├── registry.go ← people/vehicle/equipment/material master lists
-│ │ ├── dashboard.go
-│ │ └── report.go
-│ ├── service/
-│ │ ├── workorder.go
-│ │ ├── notification.go
-│ │ ├── spatial.go
-│ │ └── export.go ← CSV/Excel export
-│ ├── repository/
-│ │ ├── db.go
-│ │ ├── workorder.go
-│ │ ├── step.go
-│ │ ├── resource.go
-│ │ ├── attachment.go
-│ │ ├── accounting.go
-│ │ ├── user.go
-│ │ └── registry.go
-│ ├── model/
-│ │ ├── workorder.go
-│ │ ├── step.go
-│ │ ├── resource.go
-│ │ ├── attachment.go
-│ │ ├── accounting.go
-│ │ ├── user.go
-│ │ └── dashboard.go
-│ └── config/config.go
-│
-├── web/ ← All frontend — served as static files by Go
-│ ├── index.html ← App shell, loads fonts, Lucide, Leaflet
-│ ├── app.mjs ← custom element + client router
-│ │
-│ ├── components/
-│ │ ├── layout/
-│ │ │ ├── app-sidebar.mjs ← Left nav — collapsible, mobile drawer
-│ │ │ ├── app-topbar.mjs ← Top bar — breadcrumb, notifications bell, user avatar
-│ │ │ ├── app-root.mjs ← Shell that wires sidebar + topbar +
-│ │ │ └── app-mobile-nav.mjs ← Bottom tab bar for mobile (≤768px)
-│ │ │
-│ │ ├── work-orders/
-│ │ │ ├── wo-list.mjs ← Searchable, filterable WO list
-│ │ │ ├── wo-kanban.mjs ← Kanban board view (by status column)
-│ │ │ ├── wo-form.mjs ← Create / edit full form
-│ │ │ ├── wo-detail.mjs ← Read-only detail — tabs for each section
-│ │ │ ├── wo-checklist.mjs ← Step checklist with check-off + progress bar
-│ │ │ ├── wo-resource-panel.mjs← Assign people, vehicles, equipment, materials
-│ │ │ ├── wo-photo-panel.mjs ← Photo gallery, capture, before/during/after
-│ │ │ ├── wo-map.mjs ← Leaflet map + directions button
-│ │ │ ├── wo-accounting.mjs ← GL, cost center, WBS, billing ref fields
-│ │ │ ├── wo-timeline.mjs ← Audit/activity log feed
-│ │ │ └── wo-print.mjs ← Print-friendly layout for field packets
-│ │ │
-│ │ ├── dashboard/
-│ │ │ ├── dash-root.mjs ← Dashboard page shell
-│ │ │ ├── dash-kpi-card.mjs ← Reusable stat card (number + trend + icon)
-│ │ │ ├── dash-status-chart.mjs← Doughnut chart — WOs by status
-│ │ │ ├── dash-priority-bar.mjs← Bar chart — WOs by priority
-│ │ │ └── dash-recent-feed.mjs ← Recent activity feed
-│ │ │
-│ │ ├── registry/
-│ │ │ ├── people-list.mjs ← Manage crew / technician records
-│ │ │ ├── people-form.mjs
-│ │ │ ├── vehicle-list.mjs ← Manage fleet / vehicles
-│ │ │ ├── vehicle-form.mjs
-│ │ │ ├── equipment-list.mjs ← Manage tools & equipment
-│ │ │ ├── equipment-form.mjs
-│ │ │ ├── material-list.mjs ← Manage materials / inventory
-│ │ │ └── material-form.mjs
-│ │ │
-│ │ ├── users/
-│ │ │ ├── user-list.mjs ← User management (admin only)
-│ │ │ ├── user-form.mjs ← Create / edit user, assign role
-│ │ │ └── user-profile.mjs ← Current user profile + password change
-│ │ │
-│ │ ├── reports/
-│ │ │ ├── report-root.mjs ← Reports landing page
-│ │ │ ├── report-by-status.mjs
-│ │ │ ├── report-by-cost.mjs
-│ │ │ └── report-export.mjs ← CSV / Excel download triggers
-│ │ │
-│ │ └── shared/
-│ │ ├── ui-badge.mjs ← — status + priority pills
-│ │ ├── ui-button.mjs ← — primary/ghost/danger variants
-│ │ ├── ui-card.mjs ← — surface container
-│ │ ├── ui-dialog.mjs ← — modal with backdrop
-│ │ ├── ui-drawer.mjs ← — slide-in panel (mobile forms)
-│ │ ├── ui-toast.mjs ← — success/error notifications
-│ │ ├── ui-spinner.mjs ← — loading state
-│ │ ├── ui-empty.mjs ← — empty state illustration + CTA
-│ │ ├── ui-confirm.mjs ← — "Are you sure?" dialog
-│ │ ├── ui-search.mjs ← — debounced search input
-│ │ ├── ui-tabs.mjs ← — tab bar + panels
-│ │ ├── ui-avatar.mjs ← — initials or photo avatar
-│ │ └── ui-tooltip.mjs ← — hover tooltip
-│ │
-│ ├── lib/
-│ │ ├── api.mjs ← Fetch wrapper, auth header, error handling
-│ │ ├── router.mjs ← Hash/history client router
-│ │ ├── store.mjs ← Reactive state (lightweight signal pattern)
-│ │ ├── auth.mjs ← JWT storage, decode, role checks
-│ │ ├── format.mjs ← Date, currency, phone formatters
-│ │ ├── validate.mjs ← Form field validators
-│ │ └── utils.mjs ← Misc helpers, debounce, deepMerge
-│ │
-│ └── styles/
-│ ├── global.css ← CSS custom properties (design tokens above)
-│ ├── reset.css ← Modern CSS reset
-│ ├── typography.css ← Base font rules
-│ ├── forms.css ← Shared input/select/textarea styles
-│ ├── tables.css ← Shared table styles
-│ └── print.css ← Print overrides for wo-print.mjs
-│
-├── migrations/
-│ ├── 001_initial.sql
-│ ├── 002_resources.sql
-│ ├── 003_attachments.sql
-│ ├── 004_accounting.sql
-│ ├── 005_users_roles.sql
-│ └── 006_audit_log.sql
-│
-├── uploads/ ← Bind-mounted in Docker
-├── Dockerfile
-├── docker-compose.yml
-├── .env.example
-└── go.mod
-```
-
----
-
-## Phase 1 — Foundation
-**Goal:** Running app with shell, sidebar, auth, and WO CRUD.
-
-### 1.1 CLAUDE.md (create first)
-- [ ] Document `.mjs` extension convention for all frontend JS modules
-- [ ] Document Go conventions: package names, error wrapping, named DB params
-- [ ] Document CSS convention: always use design token vars, never hardcoded hex
-- [ ] Document component convention: Shadow DOM, `#private` fields, custom events
-- [ ] List all CDN dependencies and their globals (`L` for Leaflet, `lucide`, `Chart`)
-
-### 1.2 Go Project Bootstrap
-- [ ] `go mod init github.com/yourorg/workorder`
-- [ ] Add dependencies: `chi/v5`, `sqlx`, `go-mssqldb`, `golang-jwt/jwt/v5`, `google/uuid`, `joho/godotenv`
-- [ ] `internal/config/config.go` — load from env: `ADDR`, `DB_DSN`, `JWT_SECRET`, `UPLOAD_PATH`, `BASE_URL`
-- [ ] `internal/repository/db.go` — `sqlx.Connect`, pool settings (max open 25, max idle 5, lifetime 5min)
-- [ ] `cmd/server/main.go` — wire config → db → router → `http.ListenAndServe`
-
-### 1.3 Database — Migration 001
-```sql
--- work_orders table (full schema as planned)
--- Computed wo_number column: 'WO-' + zero-padded id
--- status CHECK constraint
--- priority CHECK constraint
--- Indexes on status, parent, scheduled_start
-```
-
-### 1.4 Auth
-- [ ] `migrations/005_users_roles.sql`
- ```sql
- CREATE TABLE users (
- id INT IDENTITY PRIMARY KEY,
- username NVARCHAR(100) NOT NULL UNIQUE,
- email NVARCHAR(200) NOT NULL UNIQUE,
- display_name NVARCHAR(200),
- password_hash NVARCHAR(200) NOT NULL, -- bcrypt
- role NVARCHAR(30) NOT NULL DEFAULT 'viewer',
- -- admin | dispatcher | field_tech | viewer
- avatar_url NVARCHAR(500),
- active BIT NOT NULL DEFAULT 1,
- last_login DATETIME2,
- created_at DATETIME2 NOT NULL DEFAULT GETUTCDATE()
- );
- -- Seed one admin user
- ```
-- [ ] `POST /api/auth/login` — bcrypt compare, return `{ token, user }` with 8hr JWT
-- [ ] `POST /api/auth/refresh` — accepts valid token, returns new token
-- [ ] `GET /api/auth/me` — current user from token claims
-- [ ] JWT middleware: attach user to `context.Context`, return 401 JSON on failure
-- [ ] Role helper: `RequireRole(roles ...string)` middleware
-
-### 1.5 Work Order Backend (CRUD)
-- [ ] `model/workorder.go` — `WorkOrder`, `WorkOrderListItem`, `WorkOrderDetail` structs with `db:` and `json:` tags
-- [ ] `repository/workorder.go`
- - `List(ctx, filters)` — status, search, priority, parentType, page/limit
- - `GetByID(ctx, id)` — single WO
- - `Create(ctx, wo)` — insert + return generated ID
- - `Update(ctx, wo)` — partial update, always set `updated_at = GETUTCDATE()`
- - `UpdateStatus(ctx, id, status, userID)` — transition + audit log insert
- - `Delete(ctx, id)` — soft delete (add `deleted_at` column)
- - `GetDetail(ctx, id)` — joins WO + steps + resources + attachments + accounting in one call
-- [ ] Handlers: `List`, `Get`, `Create`, `Update`, `Delete`, `UpdateStatus`
-- [ ] Paginated response: `{ data: [...], meta: { page, per_page, total } }`
-
-### 1.6 Frontend Shell
-- [ ] `web/index.html`
- - Load Google Fonts (Inter + JetBrains Mono)
- - Load Lucide CDN
- - Load Leaflet CSS + JS CDN
- - Load Chart.js CDN
- - `
-
-
-
-
-
-
-
-```
-
----
-
-*Start with Phase 1 in order. Each checkbox is one commit. Don't skip CLAUDE.md — it must exist before any code is written.*
-
----
-
## Work Order Profiles
### Overview
@@ -3343,33 +2322,102 @@ A **Work Order Profile** is a reusable template that defines the default structu
> **Key idea:** Profile = preset + flexibility. The profile defines the standard; each WO can deviate from it.
+---
+
+### Profile Step Types
+
+Profile steps have a **type** that controls how they are displayed and completed in the field. Types are a fixed code-level constant — not a database table. Each type has optional **type_config** (stored as JSON) that gets set when the step is added to a profile.
+
+#### Built-in Step Types
+
+| Type | Value | Field behavior |
+|------|-------|----------------|
+| Work Step | `work_step` | Standard checkbox — mark done, optional note |
+| Photo | `photo` | Camera capture required; config defines phase and prompt |
+| Inspection | `inspection` | Pass / Fail / N/A buttons; config defines criteria text |
+| Note | `note` | Free-text entry field; config defines the prompt label |
+
+#### type_config JSON by Type
+
+```json
+// work_step — no config needed
+{}
+
+// photo
+{
+ "phase": "before", // "before" | "during" | "after"
+ "caption_prompt": "Photo of existing equipment before any work begins"
+}
+
+// inspection
+{
+ "criteria": "All cable slack coiled and secured per spec"
+}
+
+// note
+{
+ "prompt": "Record any site hazards or access issues observed"
+}
+```
+
+#### Example profile: Fiber Splice Job
+
+```
+1. [photo] Before — site overview { phase: "before", caption_prompt: "Wide shot of work area" }
+2. [work_step] Power down splice enclosure
+3. [work_step] Prep and strip fiber ends
+4. [inspection] Check splice loss < 0.1 dB { criteria: "OTDR reading below 0.1 dB loss" }
+5. [photo] Splice tray loaded and sealed { phase: "during", caption_prompt: "Tray with labeled splices" }
+6. [work_step] Seal enclosure and torque to spec
+7. [note] Record any deviations or notes { prompt: "Deviations from standard procedure?" }
+8. [photo] After — completed enclosure { phase: "after", caption_prompt: "Sealed enclosure, final install" }
+```
+
+---
+
### Database
```sql
--- Profiles
-CREATE TABLE wo_profiles (
- id INT IDENTITY PRIMARY KEY,
- name NVARCHAR(200) NOT NULL,
- description NVARCHAR(MAX),
- category NVARCHAR(100),
- default_priority NVARCHAR(10) NOT NULL DEFAULT 'normal',
- default_duration_hours INT,
- default_instructions NVARCHAR(MAX),
- active BIT NOT NULL DEFAULT 1,
- created_at DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
- updated_at DATETIME2 NOT NULL DEFAULT GETUTCDATE()
-);
+-- Profiles table
+IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'wo_profiles')
+BEGIN
+ CREATE TABLE wo_profiles (
+ id INT IDENTITY PRIMARY KEY,
+ name NVARCHAR(200) NOT NULL,
+ description NVARCHAR(MAX),
+ category NVARCHAR(100),
+ default_priority NVARCHAR(10) NOT NULL DEFAULT 'normal',
+ default_duration_hours INT,
+ default_instructions NVARCHAR(MAX),
+ active BIT NOT NULL DEFAULT 1,
+ created_at DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
+ updated_at DATETIME2 NOT NULL DEFAULT GETUTCDATE()
+ );
+END
-CREATE TABLE wo_profile_steps (
- id INT IDENTITY PRIMARY KEY,
- profile_id INT NOT NULL REFERENCES wo_profiles(id) ON DELETE CASCADE,
- step_order INT NOT NULL,
- title NVARCHAR(200) NOT NULL,
- description NVARCHAR(MAX),
- required BIT NOT NULL DEFAULT 1
-);
+-- Profile steps table (with step_type and type_config)
+IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'wo_profile_steps')
+BEGIN
+ CREATE TABLE wo_profile_steps (
+ id INT IDENTITY PRIMARY KEY,
+ profile_id INT NOT NULL REFERENCES wo_profiles(id) ON DELETE CASCADE,
+ step_order INT NOT NULL,
+ title NVARCHAR(200) NOT NULL,
+ description NVARCHAR(MAX),
+ required BIT NOT NULL DEFAULT 1,
+ step_type NVARCHAR(30) NOT NULL DEFAULT 'work_step',
+ -- 'work_step' | 'photo' | 'inspection' | 'note'
+ type_config NVARCHAR(MAX) -- JSON; shape depends on step_type
+ );
+ CREATE INDEX ix_profile_steps ON wo_profile_steps (profile_id, step_order);
+END
-CREATE INDEX ix_profile_steps ON wo_profile_steps (profile_id, step_order);
+-- WO steps also carry step_type and type_config (copied from profile on apply)
+-- Migration: add columns if they do not exist
+IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('wo_steps') AND name = 'step_type')
+ ALTER TABLE wo_steps ADD step_type NVARCHAR(30) NOT NULL DEFAULT 'work_step';
+IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('wo_steps') AND name = 'type_config')
+ ALTER TABLE wo_steps ADD type_config NVARCHAR(MAX) NULL;
```
### API Endpoints
@@ -3393,39 +2441,9 @@ Request body: `{ "mode": "append" | "replace" }`
- **append** — inserts profile steps after existing steps (step_order continues from current max)
- **replace** — deletes all existing steps, then inserts profile steps starting at order 1
+- Both modes copy `step_type` and `type_config` from the profile step into `wo_steps`
- Both modes update `instructions` if currently blank and `priority` if WO is still `draft`
-### Frontend Components
-
-**`web/components/registry/profile-list.mjs`**
-- List page at `/registry/profiles`
-- Cards: name, category badge, step count, active status
-- Search + category filter, edit/deactivate actions, "New Profile" button
-
-**`web/components/registry/profile-form.mjs`**
-- Modal dialog for create/edit
-- Fields: name (required), description, category, default priority, default duration hours, default instructions
-- Inline step editor: ordered step list with add / edit / reorder / delete
-- Active toggle in edit mode
-
-**WO Form integration (`wo-form.mjs`)**
-- "Load Profile" ghost button at top of form (shown only on new WOs or draft status)
-- Searchable profile picker dialog — on confirm pre-fills priority and instructions (if blank)
-- Steps applied server-side via apply-profile after the WO saves; checklist tab then refreshes
-
-**WO Detail integration (`wo-detail.mjs`)**
-- "Apply Profile" button in header actions alongside Edit / Change Status
-- Two-step dialog: (1) pick profile, (2) if WO has existing steps — prompt append vs replace
-- Calls `POST /api/work-orders/{id}/apply-profile/{profileId}` then refreshes Checklist tab
-
-### Sidebar Nav
-
-Add under Resources section:
-
-```
-Profiles /registry/profiles (layout-template icon)
-```
-
### Go Models
```go
@@ -3438,6 +2456,9 @@ type Profile struct {
DefaultDurationHours *int `db:"default_duration_hours" json:"default_duration_hours"`
DefaultInstructions string `db:"default_instructions" json:"default_instructions"`
Active bool `db:"active" json:"active"`
+ StepCount int `db:"step_count" json:"step_count"`
+ CreatedAt time.Time `db:"created_at" json:"created_at"`
+ UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Steps []ProfileStep `db:"-" json:"steps,omitempty"`
}
@@ -3448,7 +2469,58 @@ type ProfileStep struct {
Title string `db:"title" json:"title"`
Description string `db:"description" json:"description"`
Required bool `db:"required" json:"required"`
+ StepType string `db:"step_type" json:"step_type"` // "work_step"|"photo"|"inspection"|"note"
+ TypeConfig string `db:"type_config" json:"type_config"` // JSON string
}
+
+// Step also gains these two fields (in model/models.go)
+// StepType string `db:"step_type" json:"step_type"`
+// TypeConfig string `db:"type_config" json:"type_config"`
+```
+
+### Frontend Components
+
+**`web/components/registry/profile-list.mjs`**
+- List page at `/registry/profiles`
+- Cards: name, category badge, step count, active status
+- Search + category filter, edit/deactivate actions, "New Profile" button
+
+**`web/components/registry/profile-form.mjs`**
+- Modal dialog for create/edit
+- Fields: name (required), description, category, default priority, default duration hours, default instructions
+- Inline step editor: ordered step list with add / edit / reorder / delete
+- When adding a step: type picker first (work_step / photo / inspection / note icon buttons)
+- After type selected: type-specific config fields appear inline:
+ - `photo` → phase select (before/during/after) + caption prompt input
+ - `inspection` → criteria text input
+ - `note` → prompt label input
+ - `work_step` → no extra fields
+- Active toggle in edit mode
+
+**`web/components/work-orders/wo-checklist.mjs`**
+- Each step renders differently based on `step_type`:
+ - `work_step` — large checkbox + title + optional note textarea
+ - `photo` — camera icon button labeled with `type_config.caption_prompt`; tapping opens photo panel pre-set to the configured phase; completed when a photo is attached to this step
+ - `inspection` — Pass / Fail / N/A button group; completed when any selection is made; result stored in completion notes
+ - `note` — text input labeled with `type_config.prompt`; completed when non-empty text is saved
+- Progress bar counts all step types equally
+
+**WO Form integration (`wo-form.mjs`)**
+- "Load Profile" ghost button at top of form (shown only on new WOs or draft status)
+- Searchable profile picker dialog — on confirm pre-fills priority and instructions (if blank)
+- Steps applied server-side via apply-profile after the WO saves; checklist tab then refreshes
+
+**WO Detail integration (`wo-detail.mjs`)**
+- "Apply Profile" button in header actions alongside Edit / Change Status
+- Two-step dialog: (1) pick profile, (2) if WO has existing steps — prompt append vs replace
+- Calls `POST /api/work-orders/{id}/apply-profile/{profileId}` then refreshes Checklist tab
+
+### Sidebar Nav
+
+Under Resources section:
+
+```
+Profiles /registry/profiles (layout-template icon)
```
### apply-profile Handler (pseudocode)
@@ -3459,7 +2531,7 @@ func (h *ProfileHandler) Apply(w http.ResponseWriter, r *http.Request) {
var body struct { Mode string `json:"mode"` }
json.NewDecoder(r.Body).Decode(&body)
- profile := // load profile + steps
+ profile, steps := // load profile + steps
if body.Mode == "replace" {
db.Exec(`DELETE FROM wo_steps WHERE wo_id = @p1`, woID)
@@ -3468,10 +2540,10 @@ func (h *ProfileHandler) Apply(w http.ResponseWriter, r *http.Request) {
var maxOrder int
db.Get(&maxOrder, `SELECT ISNULL(MAX(step_order), 0) FROM wo_steps WHERE wo_id = @p1`, woID)
- for _, s := range profile.Steps {
- db.Exec(`INSERT INTO wo_steps (wo_id, step_order, title, description, required)
- VALUES (@p1, @p2, @p3, @p4, @p5)`,
- woID, maxOrder+s.StepOrder, s.Title, s.Description, s.Required)
+ for _, s := range steps {
+ db.Exec(`INSERT INTO wo_steps (wo_id, step_order, title, description, required, step_type, type_config)
+ VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7)`,
+ woID, maxOrder+s.StepOrder, s.Title, s.Description, s.Required, s.StepType, s.TypeConfig)
}
db.Exec(`
diff --git a/web/app.mjs b/web/app.mjs
index d45df5d..662fe99 100644
--- a/web/app.mjs
+++ b/web/app.mjs
@@ -92,7 +92,7 @@ function showLoginPage() {
`;
- if (window.lucide) lucide.createIcons({ nodes: [root] });
+ if (window.lucide) lucide.createIcons({ root: root });
root.querySelector('#login-form').addEventListener('submit', async e => {
e.preventDefault();
diff --git a/web/components/layout/app-mobile-nav.mjs b/web/components/layout/app-mobile-nav.mjs
index 24d6334..d66b069 100644
--- a/web/components/layout/app-mobile-nav.mjs
+++ b/web/components/layout/app-mobile-nav.mjs
@@ -35,7 +35,7 @@ class AppMobileNav extends HTMLElement {
${t.label}
`;
}).join('')}`;
- if (window.lucide) lucide.createIcons({ nodes: [this] });
+ if (window.lucide) lucide.createIcons({ root: this });
}
}
customElements.define('app-mobile-nav', AppMobileNav);
diff --git a/web/components/layout/app-root.mjs b/web/components/layout/app-root.mjs
index 85fecec..01ea566 100644
--- a/web/components/layout/app-root.mjs
+++ b/web/components/layout/app-root.mjs
@@ -19,7 +19,7 @@ class AppRoot extends HTMLElement {
if (main) {
main.innerHTML = html;
main.scrollTop = 0;
- if (window.lucide) lucide.createIcons({ nodes: [main] });
+ if (window.lucide) lucide.createIcons({ root: main });
}
}
}
diff --git a/web/components/layout/app-sidebar.mjs b/web/components/layout/app-sidebar.mjs
index 50435ba..d3f436a 100644
--- a/web/components/layout/app-sidebar.mjs
+++ b/web/components/layout/app-sidebar.mjs
@@ -131,7 +131,7 @@ class AppSidebar extends HTMLElement {
`;
- if (window.lucide) lucide.createIcons({ nodes: [this] });
+ if (window.lucide) lucide.createIcons({ root: this });
this.querySelector('#toggle-btn')?.addEventListener('click', () => this.#toggle());
this.querySelector('#logout-btn')?.addEventListener('click', () => {
clearToken();
diff --git a/web/components/layout/app-topbar.mjs b/web/components/layout/app-topbar.mjs
index 014968f..88b5a88 100644
--- a/web/components/layout/app-topbar.mjs
+++ b/web/components/layout/app-topbar.mjs
@@ -85,7 +85,7 @@ class AppTopbar extends HTMLElement {
` : ''}`;
- if (window.lucide) lucide.createIcons({ nodes: [this] });
+ if (window.lucide) lucide.createIcons({ root: this });
this.querySelector('#user-menu-btn')?.addEventListener('click', e => {
e.stopPropagation();
diff --git a/web/components/registry/equipment-form.mjs b/web/components/registry/equipment-form.mjs
index b019bf1..3ca0ecd 100644
--- a/web/components/registry/equipment-form.mjs
+++ b/web/components/registry/equipment-form.mjs
@@ -71,7 +71,7 @@ class EquipmentForm extends HTMLElement {
`;
- if (window.lucide) lucide.createIcons({ nodes: [d] });
+ if (window.lucide) lucide.createIcons({ root: d });
d.querySelector('#dlg-close').addEventListener('click', () => d.close());
d.querySelector('#dlg-cancel').addEventListener('click', () => d.close());
diff --git a/web/components/registry/equipment-list.mjs b/web/components/registry/equipment-list.mjs
index 6d703c8..916d6be 100644
--- a/web/components/registry/equipment-list.mjs
+++ b/web/components/registry/equipment-list.mjs
@@ -1,4 +1,4 @@
-import { api } from '../../lib/api.mjs';
+import { api } from '../../lib/api.mjs';
import './equipment-form.mjs';
class EquipmentList extends HTMLElement {
@@ -64,7 +64,7 @@ class EquipmentList extends HTMLElement {