Maintenance log book app — initial scaffold
Multi-department fault log: NewBook-synced room locations + manual locations with categories, six-state task flow (submitted/in progress/ hold-parts/hold-later/temporary fix/fixed), photos per stage, priorities with unusable flag and per-task NewBook out-of-order push, costs on resolve, comment/audit thread, recurring task templates with note-to-template carryover, asset register, contractor register with document attachments, staff/contractor allocation, occupancy-aware summary filter, searchable history with CSV export, email notifications. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
6ca395097e
47 changed files with 6727 additions and 0 deletions
195
backend/src/db.js
Normal file
195
backend/src/db.js
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import pg from 'pg'
|
||||
|
||||
const { Pool } = pg
|
||||
export const pool = new Pool({ connectionString: process.env.DATABASE_URL })
|
||||
|
||||
export async function initDb() {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value JSONB NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Location categories: Rooms, Kitchen, Public Areas, External, Garden, ...
|
||||
-- is_rooms marks the category whose locations sync from NewBook and
|
||||
-- participate in the occupancy filter / out-of-order push.
|
||||
CREATE TABLE IF NOT EXISTS location_categories (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
is_rooms BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS locations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
category_id INT NOT NULL REFERENCES location_categories(id),
|
||||
source TEXT NOT NULL DEFAULT 'manual', -- manual | newbook
|
||||
newbook_site_id TEXT UNIQUE,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assets (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
location_id INT NOT NULL REFERENCES locations(id),
|
||||
make_model TEXT,
|
||||
serial_no TEXT,
|
||||
install_date DATE,
|
||||
notes TEXT,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contractors (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
company TEXT,
|
||||
phone TEXT,
|
||||
email TEXT,
|
||||
address TEXT,
|
||||
notes TEXT,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contractor_docs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
contractor_id INT NOT NULL REFERENCES contractors(id) ON DELETE CASCADE,
|
||||
file_name TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
mime_type TEXT,
|
||||
file_size INT,
|
||||
doc_type TEXT, -- e.g. Liability insurance, Gas Safe cert
|
||||
expiry_date DATE,
|
||||
uploaded_by TEXT,
|
||||
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Recurring task templates. Scheduler spawns a task when next_due arrives.
|
||||
CREATE TABLE IF NOT EXISTS task_templates (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
location_id INT NOT NULL REFERENCES locations(id),
|
||||
asset_id INT REFERENCES assets(id),
|
||||
priority TEXT NOT NULL DEFAULT 'medium',
|
||||
unusable BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
assigned_type TEXT NOT NULL DEFAULT 'staff', -- staff | contractor
|
||||
assigned_to TEXT,
|
||||
assigned_to_name TEXT,
|
||||
contractor_id INT REFERENCES contractors(id),
|
||||
interval_value INT NOT NULL DEFAULT 1,
|
||||
interval_unit TEXT NOT NULL DEFAULT 'months', -- days | weeks | months
|
||||
next_due DATE NOT NULL,
|
||||
template_notes TEXT NOT NULL DEFAULT '', -- carried onto every future occurrence
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_by TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
location_id INT NOT NULL REFERENCES locations(id),
|
||||
asset_id INT REFERENCES assets(id),
|
||||
template_id INT REFERENCES task_templates(id),
|
||||
priority TEXT NOT NULL DEFAULT 'medium', -- low | medium | high | urgent
|
||||
status TEXT NOT NULL DEFAULT 'submitted',
|
||||
unusable BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
newbook_blocked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
hold_until DATE,
|
||||
due_date DATE,
|
||||
assigned_type TEXT NOT NULL DEFAULT 'staff', -- staff | contractor
|
||||
assigned_to TEXT,
|
||||
assigned_to_name TEXT,
|
||||
contractor_id INT REFERENCES contractors(id),
|
||||
created_by TEXT,
|
||||
created_by_name TEXT,
|
||||
completed_by TEXT,
|
||||
completed_by_name TEXT,
|
||||
completed_at TIMESTAMPTZ,
|
||||
cost NUMERIC(10,2),
|
||||
cost_notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS tasks_status_idx ON tasks (status);
|
||||
CREATE INDEX IF NOT EXISTS tasks_location_idx ON tasks (location_id);
|
||||
CREATE INDEX IF NOT EXISTS tasks_assigned_idx ON tasks (assigned_to);
|
||||
CREATE INDEX IF NOT EXISTS tasks_completed_idx ON tasks (completed_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_photos (
|
||||
id SERIAL PRIMARY KEY,
|
||||
task_id INT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
file_name TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
mime_type TEXT,
|
||||
file_size INT,
|
||||
stage TEXT NOT NULL DEFAULT 'report', -- report | progress | resolution
|
||||
uploaded_by TEXT,
|
||||
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Audit trail + comment thread per task.
|
||||
CREATE TABLE IF NOT EXISTS task_events (
|
||||
id SERIAL PRIMARY KEY,
|
||||
task_id INT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
event_type TEXT NOT NULL, -- created | status_change | reassigned | comment | photo | cost | newbook_block | newbook_unblock | reopened | edited
|
||||
from_status TEXT,
|
||||
to_status TEXT,
|
||||
note TEXT,
|
||||
user_name TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS task_events_task_idx ON task_events (task_id, created_at);
|
||||
`)
|
||||
|
||||
await seedDefaults()
|
||||
}
|
||||
|
||||
async function seedDefaults() {
|
||||
const categories = [
|
||||
{ name: 'Rooms', sort: 1, is_rooms: true },
|
||||
{ name: 'Kitchen', sort: 2, is_rooms: false },
|
||||
{ name: 'Public Areas', sort: 3, is_rooms: false },
|
||||
{ name: 'External', sort: 4, is_rooms: false },
|
||||
{ name: 'Garden', sort: 5, is_rooms: false },
|
||||
]
|
||||
for (const c of categories) {
|
||||
await pool.query(
|
||||
`INSERT INTO location_categories (name, sort_order, is_rooms)
|
||||
VALUES ($1, $2, $3) ON CONFLICT (name) DO NOTHING`,
|
||||
[c.name, c.sort, c.is_rooms]
|
||||
)
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
default_assigned_type: 'staff',
|
||||
default_assignee: '', // staff email
|
||||
default_assignee_name: '',
|
||||
default_contractor_id: null,
|
||||
urgent_notify_email: '',
|
||||
notify_on_assign: true,
|
||||
notify_on_urgent: true,
|
||||
newbook_block_status: 'Maintenance',
|
||||
newbook_unblock_status: 'Dirty',
|
||||
}
|
||||
for (const [key, value] of Object.entries(defaults)) {
|
||||
await pool.query(
|
||||
`INSERT INTO config (key, value) VALUES ($1, $2) ON CONFLICT (key) DO NOTHING`,
|
||||
[key, JSON.stringify(value)]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getConfig() {
|
||||
const { rows } = await pool.query('SELECT key, value FROM config')
|
||||
return Object.fromEntries(rows.map(r => [r.key, r.value]))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue