diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..30cf57e
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml
new file mode 100644
index 0000000..644cdf0
--- /dev/null
+++ b/.idea/go.imports.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..50ae804
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workorders.iml b/.idea/workorders.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/workorders.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..9777ebc
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,1294 @@
+# Work Order System — Technical Build Plan
+**Stack:** Go (backend API) · Pure JavaScript + Web Components (frontend) · SQL Server (MSSQL) · Docker
+
+---
+
+## Table of Contents
+
+1. [Architecture Overview](#1-architecture-overview)
+2. [Project Structure](#2-project-structure)
+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)
+7. [File & Photo Handling](#7-file--photo-handling)
+8. [Maps & Location](#8-maps--location)
+9. [Authentication & Authorization](#9-authentication--authorization)
+10. [Docker Deployment](#10-docker-deployment)
+11. [Phased Build Plan](#11-phased-build-plan)
+12. [Development Conventions](#12-development-conventions)
+
+---
+
+## 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 │
+└─────────────────────────────────────────────────────────┘
+```
+
+**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.
+- **Multipart uploads** stored to local volume (or swap for Azure Blob / S3 later).
+- **JWT** stateless auth — fits well with a SPA + API split.
+
+---
+
+## 2. Project Structure
+
+```
+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
+```
+
+---
+
+## 3. Database Schema
+
+### Core Tables
+
+```sql
+-- ── Work Orders ───────────────────────────────────────────────────────────────
+CREATE TABLE work_orders (
+ id INT IDENTITY PRIMARY KEY,
+ wo_number AS ('WO-' + RIGHT('000000' + CAST(id AS VARCHAR), 6)) PERSISTED,
+ title NVARCHAR(200) NOT NULL,
+ description NVARCHAR(MAX),
+ instructions NVARCHAR(MAX), -- Rich text / markdown
+ status NVARCHAR(30) NOT NULL DEFAULT 'draft',
+ -- draft | assigned | scheduled | in_progress | pending_review | closed
+ priority NVARCHAR(10) NOT NULL DEFAULT 'normal',
+ -- low | normal | high | urgent
+ scheduled_start DATETIME2,
+ scheduled_end DATETIME2,
+ actual_start DATETIME2,
+ actual_end DATETIME2,
+ -- Location
+ site_name NVARCHAR(200),
+ address NVARCHAR(400),
+ lat DECIMAL(10,7),
+ lng DECIMAL(10,7),
+ access_notes NVARCHAR(MAX), -- Gate codes, access roads
+ -- Polymorphic parent
+ parent_type NVARCHAR(50), -- 'project' | 'ticket' | 'service_order' | NULL
+ parent_id INT,
+ -- Audit
+ created_by INT NOT NULL,
+ created_at DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
+ updated_at DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
+ closed_at DATETIME2,
+ closed_by INT,
+ CONSTRAINT chk_wo_status CHECK (status IN ('draft','assigned','scheduled','in_progress','pending_review','closed')),
+ CONSTRAINT chk_wo_priority CHECK (priority IN ('low','normal','high','urgent'))
+);
+
+-- ── Resource Registry (master lists) ─────────────────────────────────────────
+CREATE TABLE resource_people (
+ id INT IDENTITY PRIMARY KEY,
+ name NVARCHAR(100) NOT NULL,
+ role NVARCHAR(100),
+ email NVARCHAR(200),
+ phone NVARCHAR(30),
+ active BIT NOT NULL DEFAULT 1
+);
+
+CREATE TABLE resource_vehicles (
+ id INT IDENTITY PRIMARY KEY,
+ unit_number NVARCHAR(50) NOT NULL,
+ description NVARCHAR(200),
+ vehicle_type NVARCHAR(100),
+ active BIT NOT NULL DEFAULT 1
+);
+
+CREATE TABLE resource_equipment (
+ id INT IDENTITY PRIMARY KEY,
+ name NVARCHAR(200) NOT NULL,
+ asset_tag NVARCHAR(100),
+ category NVARCHAR(100),
+ active BIT NOT NULL DEFAULT 1
+);
+
+CREATE TABLE resource_materials (
+ id INT IDENTITY PRIMARY KEY,
+ name NVARCHAR(200) NOT NULL,
+ unit NVARCHAR(30), -- 'ft', 'each', 'box'
+ part_number NVARCHAR(100),
+ active BIT NOT NULL DEFAULT 1
+);
+
+-- ── 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,
+ resource_type NVARCHAR(20) NOT NULL, -- 'person' | 'vehicle' | 'equipment' | 'material'
+ resource_id INT NOT NULL,
+ quantity DECIMAL(10,2), -- For materials
+ notes NVARCHAR(500),
+ assigned_at DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
+ CONSTRAINT chk_resource_type CHECK (resource_type IN ('person','vehicle','equipment','material'))
+);
+
+-- ── Checklist Steps ───────────────────────────────────────────────────────────
+CREATE TABLE wo_steps (
+ id INT IDENTITY PRIMARY KEY,
+ wo_id INT NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE,
+ step_order INT NOT NULL,
+ title NVARCHAR(200) NOT NULL,
+ description NVARCHAR(MAX),
+ required BIT NOT NULL DEFAULT 1,
+ completed BIT NOT NULL DEFAULT 0,
+ completed_by INT,
+ completed_at DATETIME2,
+ notes NVARCHAR(MAX) -- Field notes added at completion
+);
+
+-- ── Attachments (photos, docs) ────────────────────────────────────────────────
+CREATE TABLE wo_attachments (
+ id INT IDENTITY PRIMARY KEY,
+ wo_id INT NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE,
+ step_id INT REFERENCES wo_steps(id), -- Optional: attach to a specific step
+ file_name NVARCHAR(500) NOT NULL,
+ file_path NVARCHAR(1000) NOT NULL, -- Relative path under /uploads
+ file_type NVARCHAR(100), -- MIME type
+ file_size BIGINT,
+ caption NVARCHAR(500),
+ phase NVARCHAR(20), -- 'before' | 'during' | 'after'
+ lat DECIMAL(10,7), -- Geo-tag from EXIF or device
+ lng DECIMAL(10,7),
+ uploaded_by INT NOT NULL,
+ uploaded_at DATETIME2 NOT NULL DEFAULT GETUTCDATE()
+);
+
+-- ── Accounting Codes ──────────────────────────────────────────────────────────
+CREATE TABLE wo_accounting (
+ id INT IDENTITY PRIMARY KEY,
+ wo_id INT NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE,
+ code_type NVARCHAR(50) NOT NULL, -- 'gl_account' | 'cost_center' | 'wbs' | 'billing_ref'
+ code_value NVARCHAR(200) NOT NULL,
+ description NVARCHAR(500),
+ CONSTRAINT uq_wo_accounting UNIQUE (wo_id, code_type)
+);
+
+-- ── Audit Log ─────────────────────────────────────────────────────────────────
+CREATE TABLE wo_audit_log (
+ id INT IDENTITY PRIMARY KEY,
+ wo_id INT NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE,
+ action NVARCHAR(100) NOT NULL,
+ old_value NVARCHAR(MAX),
+ new_value NVARCHAR(MAX),
+ performed_by INT NOT NULL,
+ performed_at DATETIME2 NOT NULL DEFAULT GETUTCDATE()
+);
+
+-- ── 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);
+CREATE INDEX ix_wo_resources ON wo_resources (wo_id, resource_type);
+CREATE INDEX ix_wo_attachments ON wo_attachments (wo_id);
+```
+
+---
+
+## 4. Go Backend
+
+### `cmd/server/main.go`
+
+```go
+package main
+
+import (
+ "log"
+ "net/http"
+
+ "github.com/yourorg/workorder/internal/api"
+ "github.com/yourorg/workorder/internal/config"
+ "github.com/yourorg/workorder/internal/repository"
+)
+
+func main() {
+ cfg := config.Load() // reads .env / environment variables
+
+ db, err := repository.Connect(cfg.DBDSN)
+ if err != nil {
+ log.Fatalf("db connect: %v", err)
+ }
+ defer db.Close()
+
+ r := api.NewRouter(cfg, db)
+
+ log.Printf("listening on %s", cfg.Addr)
+ if err := http.ListenAndServe(cfg.Addr, r); err != nil {
+ log.Fatal(err)
+ }
+}
+```
+
+### `internal/config/config.go`
+
+```go
+package config
+
+import "os"
+
+type Config struct {
+ Addr string
+ DBDSN string
+ JWTSecret string
+ UploadPath string
+ BaseURL string
+}
+
+func Load() *Config {
+ return &Config{
+ Addr: env("ADDR", ":8080"),
+ DBDSN: env("DB_DSN", ""),
+ JWTSecret: env("JWT_SECRET", "change-me"),
+ UploadPath: env("UPLOAD_PATH", "./uploads"),
+ BaseURL: env("BASE_URL", "http://localhost:8080"),
+ }
+}
+
+func env(key, fallback string) string {
+ if v := os.Getenv(key); v != "" {
+ return v
+ }
+ return fallback
+}
+```
+
+### `internal/api/router.go`
+
+```go
+package api
+
+import (
+ "net/http"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/go-chi/chi/v5/middleware"
+ "github.com/yourorg/workorder/internal/api/handlers"
+ mw "github.com/yourorg/workorder/internal/api/middleware"
+ "github.com/yourorg/workorder/internal/config"
+ "github.com/jmoiron/sqlx"
+)
+
+func NewRouter(cfg *config.Config, db *sqlx.DB) http.Handler {
+ r := chi.NewRouter()
+
+ // Global middleware
+ r.Use(middleware.Logger)
+ r.Use(middleware.Recoverer)
+ r.Use(middleware.RealIP)
+ r.Use(mw.CORS)
+
+ // Serve frontend static files
+ r.Handle("/*", http.FileServer(http.Dir("./web")))
+
+ // Auth routes (no JWT required)
+ r.Post("/api/auth/login", handlers.NewAuthHandler(cfg, db).Login)
+ r.Post("/api/auth/refresh", handlers.NewAuthHandler(cfg, db).Refresh)
+
+ // Protected API routes
+ r.Group(func(r chi.Router) {
+ r.Use(mw.JWT(cfg.JWTSecret))
+
+ wo := handlers.NewWorkOrderHandler(db)
+ r.Get("/api/work-orders", wo.List)
+ r.Post("/api/work-orders", wo.Create)
+ r.Get("/api/work-orders/{id}", wo.Get)
+ r.Put("/api/work-orders/{id}", wo.Update)
+ r.Delete("/api/work-orders/{id}", wo.Delete)
+ r.Put("/api/work-orders/{id}/status", wo.UpdateStatus)
+
+ res := handlers.NewResourceHandler(db)
+ r.Get("/api/work-orders/{id}/resources", res.List)
+ r.Post("/api/work-orders/{id}/resources", res.Assign)
+ r.Delete("/api/work-orders/{id}/resources/{rid}", res.Remove)
+
+ step := handlers.NewStepHandler(db)
+ r.Get("/api/work-orders/{id}/steps", step.List)
+ r.Post("/api/work-orders/{id}/steps", step.Create)
+ r.Put("/api/work-orders/{id}/steps/{sid}", step.Update)
+ r.Post("/api/work-orders/{id}/steps/{sid}/complete", step.Complete)
+ r.Delete("/api/work-orders/{id}/steps/{sid}", step.Delete)
+
+ att := handlers.NewAttachmentHandler(cfg, db)
+ r.Get("/api/work-orders/{id}/attachments", att.List)
+ r.Post("/api/work-orders/{id}/attachments", att.Upload)
+ r.Delete("/api/work-orders/{id}/attachments/{aid}", att.Delete)
+ r.Handle("/uploads/*", http.StripPrefix("/uploads/", http.FileServer(http.Dir(cfg.UploadPath))))
+
+ acc := handlers.NewAccountingHandler(db)
+ r.Get("/api/work-orders/{id}/accounting", acc.Get)
+ r.Put("/api/work-orders/{id}/accounting", acc.Upsert)
+
+ // Resource registries (master lists for assignment pickers)
+ r.Get("/api/registry/people", handlers.RegistryPeople(db))
+ r.Get("/api/registry/vehicles", handlers.RegistryVehicles(db))
+ r.Get("/api/registry/equipment", handlers.RegistryEquipment(db))
+ r.Get("/api/registry/materials", handlers.RegistryMaterials(db))
+ })
+
+ return r
+}
+```
+
+### `internal/model/workorder.go`
+
+```go
+package model
+
+import "time"
+
+type WorkOrder struct {
+ ID int `db:"id" json:"id"`
+ WONumber string `db:"wo_number" json:"wo_number"`
+ Title string `db:"title" json:"title"`
+ Description string `db:"description" json:"description"`
+ Instructions string `db:"instructions" json:"instructions"`
+ Status string `db:"status" json:"status"`
+ Priority string `db:"priority" json:"priority"`
+ ScheduledStart *time.Time `db:"scheduled_start" json:"scheduled_start"`
+ ScheduledEnd *time.Time `db:"scheduled_end" json:"scheduled_end"`
+ ActualStart *time.Time `db:"actual_start" json:"actual_start"`
+ ActualEnd *time.Time `db:"actual_end" json:"actual_end"`
+ SiteName string `db:"site_name" json:"site_name"`
+ Address string `db:"address" json:"address"`
+ Lat *float64 `db:"lat" json:"lat"`
+ Lng *float64 `db:"lng" json:"lng"`
+ AccessNotes string `db:"access_notes" json:"access_notes"`
+ ParentType *string `db:"parent_type" json:"parent_type"`
+ ParentID *int `db:"parent_id" json:"parent_id"`
+ CreatedBy int `db:"created_by" json:"created_by"`
+ CreatedAt time.Time `db:"created_at" json:"created_at"`
+ UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
+}
+
+// Enriched view returned by GET /work-orders/{id}
+type WorkOrderDetail struct {
+ WorkOrder
+ Resources []AssignedResource `json:"resources"`
+ Steps []Step `json:"steps"`
+ Attachments []Attachment `json:"attachments"`
+ Accounting []AccountingCode `json:"accounting"`
+}
+
+type WorkOrderListItem struct {
+ ID int `db:"id" json:"id"`
+ WONumber string `db:"wo_number" json:"wo_number"`
+ Title string `db:"title" json:"title"`
+ Status string `db:"status" json:"status"`
+ Priority string `db:"priority" json:"priority"`
+ SiteName string `db:"site_name" json:"site_name"`
+ ScheduledStart *time.Time `db:"scheduled_start" json:"scheduled_start"`
+ StepCount int `db:"step_count" json:"step_count"`
+ StepsDone int `db:"steps_done" json:"steps_done"`
+ PhotoCount int `db:"photo_count" json:"photo_count"`
+}
+```
+
+### `internal/repository/workorder.go`
+
+```go
+package repository
+
+import (
+ "context"
+ "github.com/jmoiron/sqlx"
+ "github.com/yourorg/workorder/internal/model"
+)
+
+type WorkOrderRepo struct{ db *sqlx.DB }
+
+func NewWorkOrderRepo(db *sqlx.DB) *WorkOrderRepo {
+ return &WorkOrderRepo{db: db}
+}
+
+func (r *WorkOrderRepo) List(ctx context.Context, status, search string) ([]model.WorkOrderListItem, error) {
+ query := `
+ SELECT
+ wo.id, wo.wo_number, wo.title, wo.status, wo.priority,
+ wo.site_name, wo.scheduled_start,
+ COUNT(s.id) AS step_count,
+ SUM(CAST(s.completed AS INT)) AS steps_done,
+ COUNT(a.id) AS photo_count
+ FROM work_orders wo
+ LEFT JOIN wo_steps s ON s.wo_id = wo.id
+ LEFT JOIN wo_attachments a ON a.wo_id = wo.id
+ WHERE (@status = '' OR wo.status = @status)
+ AND (@search = '' OR wo.title LIKE '%' + @search + '%'
+ OR wo.wo_number LIKE '%' + @search + '%'
+ OR wo.site_name LIKE '%' + @search + '%')
+ GROUP BY wo.id, wo.wo_number, wo.title, wo.status,
+ wo.priority, wo.site_name, wo.scheduled_start
+ ORDER BY wo.scheduled_start DESC, wo.id DESC`
+
+ rows := []model.WorkOrderListItem{}
+ err := r.db.SelectContext(ctx, &rows, query,
+ sqlx.Named("status", status),
+ sqlx.Named("search", search),
+ )
+ return rows, err
+}
+
+func (r *WorkOrderRepo) GetByID(ctx context.Context, id int) (*model.WorkOrder, error) {
+ var wo model.WorkOrder
+ err := r.db.GetContext(ctx, &wo,
+ `SELECT * FROM work_orders WHERE id = @p1`, id)
+ if err != nil {
+ return nil, err
+ }
+ return &wo, nil
+}
+
+func (r *WorkOrderRepo) Create(ctx context.Context, wo *model.WorkOrder) (int, error) {
+ var id int
+ err := r.db.QueryRowContext(ctx, `
+ INSERT INTO work_orders
+ (title, description, instructions, status, priority,
+ scheduled_start, scheduled_end, site_name, address, lat, lng,
+ access_notes, parent_type, parent_id, created_by, updated_at)
+ OUTPUT INSERTED.id
+ VALUES
+ (@title, @description, @instructions, @status, @priority,
+ @scheduled_start, @scheduled_end, @site_name, @address, @lat, @lng,
+ @access_notes, @parent_type, @parent_id, @created_by, GETUTCDATE())`,
+ sqlx.Named("title", wo.Title),
+ // ... remaining named params
+ ).Scan(&id)
+ return id, err
+}
+
+func (r *WorkOrderRepo) UpdateStatus(ctx context.Context, id int, status string, userID int) error {
+ _, err := r.db.ExecContext(ctx, `
+ UPDATE work_orders
+ SET status = @p1, updated_at = GETUTCDATE(),
+ actual_start = CASE WHEN @p1 = 'in_progress' AND actual_start IS NULL
+ THEN GETUTCDATE() ELSE actual_start END,
+ actual_end = CASE WHEN @p1 = 'closed' THEN GETUTCDATE() ELSE actual_end END,
+ closed_at = CASE WHEN @p1 = 'closed' THEN GETUTCDATE() ELSE closed_at END,
+ closed_by = CASE WHEN @p1 = 'closed' THEN @p2 ELSE closed_by END
+ WHERE id = @p3`, status, userID, id)
+ return err
+}
+```
+
+---
+
+## 5. REST API Design
+
+| Method | Path | Description |
+|--------|------|-------------|
+| POST | `/api/auth/login` | Login, returns JWT |
+| GET | `/api/work-orders?status=&search=&page=` | Paginated list |
+| POST | `/api/work-orders` | Create work order |
+| GET | `/api/work-orders/{id}` | Full detail (WO + resources + steps + photos + accounting) |
+| PUT | `/api/work-orders/{id}` | Update work order fields |
+| PUT | `/api/work-orders/{id}/status` | Status transition with audit |
+| GET | `/api/work-orders/{id}/resources` | List assigned resources |
+| POST | `/api/work-orders/{id}/resources` | Assign a resource |
+| DELETE | `/api/work-orders/{id}/resources/{rid}` | Remove assignment |
+| GET | `/api/work-orders/{id}/steps` | List checklist steps |
+| POST | `/api/work-orders/{id}/steps` | Add a step |
+| PUT | `/api/work-orders/{id}/steps/{sid}` | Edit step |
+| POST | `/api/work-orders/{id}/steps/{sid}/complete` | Check off step |
+| POST | `/api/work-orders/{id}/attachments` | Upload photo/file (multipart) |
+| GET | `/api/work-orders/{id}/attachments` | List attachments |
+| PUT | `/api/work-orders/{id}/accounting` | Upsert accounting codes |
+| GET | `/api/registry/people` | Lookup: people for assignment picker |
+| GET | `/api/registry/vehicles` | Lookup: vehicles |
+| GET | `/api/registry/equipment` | Lookup: equipment |
+| GET | `/api/registry/materials` | Lookup: materials |
+
+### Standard Response Envelope
+
+```json
+{
+ "data": { ... },
+ "meta": { "page": 1, "per_page": 25, "total": 142 },
+ "error": null
+}
+```
+
+### Status Transition Rules (enforced server-side)
+
+```
+draft → assigned → scheduled → in_progress → pending_review → closed
+ ↘ (re-open) ↗
+```
+
+---
+
+## 6. Frontend — Web Components
+
+### Design Principles
+
+- **No build step** — plain `
+
+
+```
+
+Zero build tooling. Zero npm for the frontend. Open in browser and go.
diff --git a/WorkOrderSystemProposal.pptx b/WorkOrderSystemProposal.pptx
new file mode 100644
index 0000000..eec13fb
Binary files /dev/null and b/WorkOrderSystemProposal.pptx differ