Initialize work order management system with database schema, API handlers, web client, and Docker configuration.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/microsoft/go-mssqldb"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrations embed.FS
|
||||
|
||||
func Connect(dsn string) (*sqlx.DB, error) {
|
||||
// Extract base DSN without the database name for initial connection to master
|
||||
masterDSN := strings.Replace(dsn, "database=workorders", "database=master", 1)
|
||||
|
||||
// Retry loop — MSSQL takes time to start
|
||||
var db *sqlx.DB
|
||||
var err error
|
||||
for i := range 20 {
|
||||
db, err = sqlx.Open("sqlserver", masterDSN)
|
||||
if err == nil {
|
||||
if pingErr := db.PingContext(context.Background()); pingErr == nil {
|
||||
break
|
||||
}
|
||||
db.Close()
|
||||
}
|
||||
log.Printf("waiting for MSSQL (%d/20)...", i+1)
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect to master: %w", err)
|
||||
}
|
||||
|
||||
// Create the workorders database if it doesn't exist
|
||||
_, err = db.Exec(`
|
||||
IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'workorders')
|
||||
CREATE DATABASE workorders;
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create database: %w", err)
|
||||
}
|
||||
db.Close()
|
||||
|
||||
// Reconnect to the workorders database
|
||||
db, err = sqlx.Open("sqlserver", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect to workorders: %w", err)
|
||||
}
|
||||
if err = db.PingContext(context.Background()); err != nil {
|
||||
return nil, fmt.Errorf("ping workorders: %w", err)
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(5)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func RunMigrations(db *sqlx.DB) error {
|
||||
// Create migrations tracking table
|
||||
_, err := db.Exec(`
|
||||
IF NOT EXISTS (
|
||||
SELECT * FROM sys.tables WHERE name = 'schema_migrations'
|
||||
)
|
||||
CREATE TABLE schema_migrations (
|
||||
filename NVARCHAR(255) PRIMARY KEY,
|
||||
applied_at DATETIME2 NOT NULL DEFAULT GETUTCDATE()
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create schema_migrations: %w", err)
|
||||
}
|
||||
|
||||
entries, err := migrations.ReadDir("migrations")
|
||||
if err != nil {
|
||||
return fmt.Errorf("read migrations dir: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !strings.HasSuffix(entry.Name(), ".sql") {
|
||||
continue
|
||||
}
|
||||
|
||||
var count int
|
||||
_ = db.QueryRow(`SELECT COUNT(1) FROM schema_migrations WHERE filename = @p1`, entry.Name()).Scan(&count)
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := migrations.ReadFile("migrations/" + entry.Name())
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", entry.Name(), err)
|
||||
}
|
||||
|
||||
// Split on GO statements for multi-batch SQL
|
||||
statements := splitSQL(string(content))
|
||||
for _, stmt := range statements {
|
||||
stmt = strings.TrimSpace(stmt)
|
||||
if stmt == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := db.Exec(stmt); err != nil {
|
||||
return fmt.Errorf("migration %s: %w", entry.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = db.Exec(`INSERT INTO schema_migrations (filename) VALUES (@p1)`, entry.Name())
|
||||
if err != nil {
|
||||
return fmt.Errorf("record migration %s: %w", entry.Name(), err)
|
||||
}
|
||||
log.Printf("applied migration: %s", entry.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitSQL(sql string) []string {
|
||||
var stmts []string
|
||||
for _, part := range strings.Split(sql, "\nGO") {
|
||||
if s := strings.TrimSpace(part); s != "" {
|
||||
stmts = append(stmts, s)
|
||||
}
|
||||
}
|
||||
if len(stmts) == 0 {
|
||||
stmts = []string{sql}
|
||||
}
|
||||
return stmts
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
-- Run once: create database if needed (executed against master)
|
||||
-- The Go migration runner handles this separately.
|
||||
|
||||
-- ── 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),
|
||||
status NVARCHAR(30) NOT NULL DEFAULT 'draft',
|
||||
priority NVARCHAR(10) NOT NULL DEFAULT 'normal',
|
||||
scheduled_start DATETIME2,
|
||||
scheduled_end DATETIME2,
|
||||
actual_start DATETIME2,
|
||||
actual_end DATETIME2,
|
||||
site_name NVARCHAR(200),
|
||||
address NVARCHAR(400),
|
||||
lat DECIMAL(10,7),
|
||||
lng DECIMAL(10,7),
|
||||
access_notes NVARCHAR(MAX),
|
||||
parent_type NVARCHAR(50),
|
||||
parent_id INT,
|
||||
created_by NVARCHAR(200) NOT NULL DEFAULT 'system',
|
||||
created_at DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
|
||||
updated_at DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
|
||||
closed_at DATETIME2,
|
||||
closed_by NVARCHAR(200),
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
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),
|
||||
part_number NVARCHAR(100),
|
||||
active BIT NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
-- ── Work Order Resources ───────────────────────────────────────────────────────
|
||||
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,
|
||||
resource_id INT NOT NULL,
|
||||
quantity DECIMAL(10,2),
|
||||
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 NVARCHAR(200),
|
||||
completed_at DATETIME2,
|
||||
notes NVARCHAR(MAX)
|
||||
);
|
||||
|
||||
-- ── Attachments ───────────────────────────────────────────────────────────────
|
||||
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),
|
||||
file_name NVARCHAR(500) NOT NULL,
|
||||
file_path NVARCHAR(1000) NOT NULL,
|
||||
file_type NVARCHAR(100),
|
||||
file_size BIGINT,
|
||||
caption NVARCHAR(500),
|
||||
phase NVARCHAR(20),
|
||||
lat DECIMAL(10,7),
|
||||
lng DECIMAL(10,7),
|
||||
uploaded_by NVARCHAR(200) 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,
|
||||
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 NVARCHAR(200) 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_resources ON wo_resources (wo_id, resource_type);
|
||||
CREATE INDEX ix_attachments ON wo_attachments (wo_id);
|
||||
|
||||
-- ── Seed registry data ────────────────────────────────────────────────────────
|
||||
INSERT INTO resource_people (name, role, email, phone) VALUES
|
||||
('Alice Johnson', 'Lead Technician', 'alice@example.com', '555-0101'),
|
||||
('Bob Smith', 'Field Tech', 'bob@example.com', '555-0102'),
|
||||
('Carol White', 'Supervisor', 'carol@example.com', '555-0103');
|
||||
|
||||
INSERT INTO resource_vehicles (unit_number, description, vehicle_type) VALUES
|
||||
('TRK-001', 'Ford F-250 Service Truck', 'Pickup'),
|
||||
('VAN-002', 'Transit Connect Work Van', 'Van'),
|
||||
('SUV-003', 'Chevy Tahoe Supervisor', 'SUV');
|
||||
|
||||
INSERT INTO resource_equipment (name, asset_tag, category) VALUES
|
||||
('Bucket Truck', 'EQ-1001', 'Heavy Equipment'),
|
||||
('Portable Generator', 'EQ-1002', 'Power'),
|
||||
('Cable Puller', 'EQ-1003', 'Tools');
|
||||
|
||||
INSERT INTO resource_materials (name, unit, part_number) VALUES
|
||||
('Wire 12 AWG', 'ft', 'WR-12AWG'),
|
||||
('Conduit 1"', 'ft', 'CD-1IN'),
|
||||
('Junction Box', 'each', 'JB-100');
|
||||
Reference in New Issue
Block a user