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 {