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
80
backend/src/lib/task-core.js
Normal file
80
backend/src/lib/task-core.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { pool, getConfig } from '../db.js'
|
||||
import { notifyAssignment, notifyUrgent } from './mailer.js'
|
||||
|
||||
export const PRIORITIES = ['low', 'medium', 'high', 'urgent']
|
||||
export const STATUSES = ['submitted', 'in_progress', 'hold_parts', 'hold_scheduled', 'temporary_fix', 'fixed']
|
||||
|
||||
// Legal state transitions. temporary_fix still counts as open.
|
||||
export const TRANSITIONS = {
|
||||
submitted: ['in_progress', 'hold_parts', 'hold_scheduled', 'temporary_fix', 'fixed'],
|
||||
in_progress: ['submitted', 'hold_parts', 'hold_scheduled', 'temporary_fix', 'fixed'],
|
||||
hold_parts: ['submitted', 'in_progress', 'hold_scheduled', 'temporary_fix', 'fixed'],
|
||||
hold_scheduled: ['submitted', 'in_progress', 'hold_parts', 'temporary_fix', 'fixed'],
|
||||
temporary_fix: ['submitted', 'in_progress', 'fixed'],
|
||||
fixed: ['submitted'], // reopen only
|
||||
}
|
||||
|
||||
export async function logEvent(taskId, eventType, { fromStatus = null, toStatus = null, note = null, userName = null } = {}) {
|
||||
await pool.query(
|
||||
`INSERT INTO task_events (task_id, event_type, from_status, to_status, note, user_name)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[taskId, eventType, fromStatus, toStatus, note, userName]
|
||||
)
|
||||
}
|
||||
|
||||
// Shared by the tasks route and the recurring-template scheduler.
|
||||
// input.assigned_* fall back to the configured defaults when absent.
|
||||
export async function createTask(input, actor) {
|
||||
const config = await getConfig()
|
||||
|
||||
let assignedType = input.assigned_type || config.default_assigned_type || 'staff'
|
||||
let assignedTo = input.assigned_to ?? null
|
||||
let assignedToName = input.assigned_to_name ?? null
|
||||
let contractorId = input.contractor_id ?? null
|
||||
|
||||
if (!input.assigned_type && assignedTo == null && contractorId == null) {
|
||||
if (assignedType === 'contractor' && config.default_contractor_id) {
|
||||
contractorId = config.default_contractor_id
|
||||
} else if (config.default_assignee) {
|
||||
assignedType = 'staff'
|
||||
assignedTo = config.default_assignee
|
||||
assignedToName = config.default_assignee_name || config.default_assignee
|
||||
}
|
||||
}
|
||||
if (assignedType === 'contractor') { assignedTo = null; assignedToName = null }
|
||||
else contractorId = null
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO tasks (title, description, location_id, asset_id, template_id, priority, unusable,
|
||||
due_date, assigned_type, assigned_to, assigned_to_name, contractor_id,
|
||||
created_by, created_by_name)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING *`,
|
||||
[
|
||||
input.title, input.description || null, input.location_id, input.asset_id || null,
|
||||
input.template_id || null, input.priority || 'medium', input.unusable === true,
|
||||
input.due_date || null, assignedType, assignedTo, assignedToName, contractorId,
|
||||
actor.email, actor.name,
|
||||
]
|
||||
)
|
||||
const task = rows[0]
|
||||
|
||||
await logEvent(task.id, 'created', { toStatus: 'submitted', userName: actor.name })
|
||||
|
||||
const { rows: locRows } = await pool.query('SELECT name FROM locations WHERE id = $1', [task.location_id])
|
||||
const locationName = locRows[0]?.name || `#${task.location_id}`
|
||||
|
||||
// Notifications are fire-and-forget; mailer swallows errors.
|
||||
if (config.notify_on_assign) {
|
||||
if (task.assigned_type === 'staff' && task.assigned_to && task.assigned_to !== actor.email) {
|
||||
notifyAssignment(task, locationName, task.assigned_to)
|
||||
} else if (task.assigned_type === 'contractor' && task.contractor_id) {
|
||||
const { rows: c } = await pool.query('SELECT email FROM contractors WHERE id = $1', [task.contractor_id])
|
||||
if (c[0]?.email) notifyAssignment(task, locationName, c[0].email)
|
||||
}
|
||||
}
|
||||
if (config.notify_on_urgent && task.priority === 'urgent' && config.urgent_notify_email) {
|
||||
notifyUrgent(task, locationName, config.urgent_notify_email)
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue