- Status filter replaced by folder tabs (All open / Overdue / In progress / Temp fix / On hold) with live count badges; tabs, priority and category filters now applied client-side so counts always match - Priority filter badges with counts; sort is now a dropdown list - New hold_booked status (contractor booked), grouped with in progress, with optional booked date stored in hold_until; hold_until cleared when leaving the date-bearing holds Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
83 lines
4 KiB
JavaScript
83 lines
4 KiB
JavaScript
import { pool, getConfig } from '../db.js'
|
|
import { notifyAssignment, notifyUrgent } from './mailer.js'
|
|
import { notifyAssignmentPush } from './push.js'
|
|
|
|
export const PRIORITIES = ['low', 'medium', 'high', 'urgent']
|
|
export const STATUSES = ['submitted', 'in_progress', 'hold_parts', 'hold_booked', 'hold_scheduled', 'temporary_fix', 'fixed']
|
|
|
|
// Legal state transitions. temporary_fix still counts as open.
|
|
export const TRANSITIONS = {
|
|
submitted: ['in_progress', 'hold_parts', 'hold_booked', 'hold_scheduled', 'temporary_fix', 'fixed'],
|
|
in_progress: ['submitted', 'hold_parts', 'hold_booked', 'hold_scheduled', 'temporary_fix', 'fixed'],
|
|
hold_parts: ['submitted', 'in_progress', 'hold_booked', 'hold_scheduled', 'temporary_fix', 'fixed'],
|
|
hold_booked: ['submitted', 'in_progress', 'hold_parts', 'hold_scheduled', 'temporary_fix', 'fixed'],
|
|
hold_scheduled: ['submitted', 'in_progress', 'hold_parts', 'hold_booked', '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, photoId = null } = {}) {
|
|
await pool.query(
|
|
`INSERT INTO task_events (task_id, event_type, from_status, to_status, note, user_name, photo_id)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
[taskId, eventType, fromStatus, toStatus, note, userName, photoId]
|
|
)
|
|
}
|
|
|
|
// 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)
|
|
notifyAssignmentPush(task, locationName)
|
|
} 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
|
|
}
|