Add lead time to recurring templates — generate before actually due
task_templates.lead_days (default 0): scheduler now spawns the task when (next_due - lead_days) <= today instead of next_due <= today, but due_date on the created task is still the real next_due — so overdue and priority handling are unaffected, the task just becomes visible earlier for prep/booking. Small jobs (AC filter clean) can stay at 0; bigger ones needing advance notice (septic tank empty-out, contractor booking) can set 2-3 days or 30 days as needed. - db.js: lead_days INT NOT NULL DEFAULT 0 (inline for fresh installs, ALTER for existing). - scheduler.js: WHERE (next_due - lead_days) <= $1 — next_due itself and the fixed/floating rollover logic are untouched by this. - templates.js: accept + validate lead_days (>= 0) on create/update. - Recurring.tsx: lead time field in the template form with contextual hint text, plus a small 'generates Nd early' note on list rows that have one set. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
763e7be6ca
commit
d9b01a5981
5 changed files with 35 additions and 9 deletions
|
|
@ -94,6 +94,7 @@ export async function initDb() {
|
|||
interval_value INT NOT NULL DEFAULT 1,
|
||||
interval_unit TEXT NOT NULL DEFAULT 'months', -- days | weeks | months
|
||||
next_due DATE NOT NULL,
|
||||
lead_days INT NOT NULL DEFAULT 0, -- generate the task this many days before next_due (due_date stays the real date)
|
||||
template_notes TEXT NOT NULL DEFAULT '', -- carried onto every future occurrence
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
schedule_mode TEXT NOT NULL DEFAULT 'fixed', -- fixed (anchored to next_due) | floating (anchored to completion)
|
||||
|
|
@ -190,6 +191,9 @@ export async function initDb() {
|
|||
await pool.query(`
|
||||
ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS group_id INT REFERENCES template_groups(id) ON DELETE SET NULL
|
||||
`)
|
||||
await pool.query(`
|
||||
ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS lead_days INT NOT NULL DEFAULT 0
|
||||
`)
|
||||
|
||||
await seedDefaults()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ import { addInterval, isoDate } from './recurrence.js'
|
|||
const CHECK_INTERVAL_MS = 60 * 60 * 1000 // hourly
|
||||
|
||||
// Spawn one task per due template.
|
||||
// - lead_days brings generation forward of next_due (e.g. a septic tank
|
||||
// empty-out wants 30 days' warning to book a contractor; an AC filter
|
||||
// clean might only need 2-3) — the spawned task's due_date is still the
|
||||
// real next_due, so overdue/priority handling is unaffected, it just
|
||||
// appears in the task list earlier.
|
||||
// - Fixed templates: advance next_due past today immediately, so a backlog
|
||||
// after downtime produces a single task, not one per missed occurrence.
|
||||
// - Floating templates: leave next_due untouched and mark the template
|
||||
|
|
@ -15,7 +20,7 @@ export async function spawnDueTemplates(log = console) {
|
|||
const today = isoDate(new Date())
|
||||
const { rows: due } = await pool.query(
|
||||
`SELECT * FROM task_templates
|
||||
WHERE active = TRUE AND next_due <= $1 AND (schedule_mode != 'floating' OR pending_task_id IS NULL)
|
||||
WHERE active = TRUE AND (next_due - lead_days) <= $1 AND (schedule_mode != 'floating' OR pending_task_id IS NULL)
|
||||
ORDER BY id`,
|
||||
[today]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ export async function templateRoutes(app) {
|
|||
if (b.schedule_mode && !SCHEDULE_MODES.includes(b.schedule_mode)) return reply.status(400).send({ error: 'Invalid schedule_mode' })
|
||||
const interval = parseInt(b.interval_value) || 1
|
||||
if (interval < 1) return reply.status(400).send({ error: 'interval_value must be at least 1' })
|
||||
const leadDays = b.lead_days !== undefined ? parseInt(b.lead_days) || 0 : 0
|
||||
if (leadDays < 0) return reply.status(400).send({ error: 'lead_days cannot be negative' })
|
||||
if (b.group_id) {
|
||||
const { rows: g } = await pool.query('SELECT id FROM template_groups WHERE id = $1', [b.group_id])
|
||||
if (!g.length) return reply.status(400).send({ error: 'Unknown group_id' })
|
||||
|
|
@ -60,9 +62,9 @@ export async function templateRoutes(app) {
|
|||
const { rows } = await pool.query(
|
||||
`INSERT INTO task_templates
|
||||
(title, description, location_id, asset_id, priority, unusable, assigned_type, assigned_to,
|
||||
assigned_to_name, contractor_id, interval_value, interval_unit, next_due, template_notes,
|
||||
assigned_to_name, contractor_id, interval_value, interval_unit, next_due, lead_days, template_notes,
|
||||
schedule_mode, group_id, created_by)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) RETURNING *`,
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18) RETURNING *`,
|
||||
[
|
||||
b.title, b.description || null, b.location_id, b.asset_id || null,
|
||||
b.priority || 'medium', b.unusable === true,
|
||||
|
|
@ -70,7 +72,7 @@ export async function templateRoutes(app) {
|
|||
b.assigned_type === 'contractor' ? null : (b.assigned_to || null),
|
||||
b.assigned_type === 'contractor' ? null : (b.assigned_to_name || null),
|
||||
b.assigned_type === 'contractor' ? (b.contractor_id || null) : null,
|
||||
interval, b.interval_unit || 'months', b.next_due,
|
||||
interval, b.interval_unit || 'months', b.next_due, leadDays,
|
||||
b.template_notes || '', b.schedule_mode || 'fixed', b.group_id || null, req.user.email,
|
||||
]
|
||||
)
|
||||
|
|
@ -85,6 +87,7 @@ export async function templateRoutes(app) {
|
|||
if (b.priority && !PRIORITIES.includes(b.priority)) return reply.status(400).send({ error: 'Invalid priority' })
|
||||
if (b.interval_unit && !INTERVAL_UNITS.includes(b.interval_unit)) return reply.status(400).send({ error: 'Invalid interval_unit' })
|
||||
if (b.schedule_mode && !SCHEDULE_MODES.includes(b.schedule_mode)) return reply.status(400).send({ error: 'Invalid schedule_mode' })
|
||||
if (b.lead_days !== undefined && parseInt(b.lead_days) < 0) return reply.status(400).send({ error: 'lead_days cannot be negative' })
|
||||
if (b.group_id) {
|
||||
const { rows: g } = await pool.query('SELECT id FROM template_groups WHERE id = $1', [b.group_id])
|
||||
if (!g.length) return reply.status(400).send({ error: 'Unknown group_id' })
|
||||
|
|
@ -96,8 +99,8 @@ export async function templateRoutes(app) {
|
|||
title = $1, description = $2, location_id = $3, asset_id = $4, priority = $5, unusable = $6,
|
||||
assigned_type = $7, assigned_to = $8, assigned_to_name = $9, contractor_id = $10,
|
||||
interval_value = $11, interval_unit = $12, next_due = $13, template_notes = $14, active = $15,
|
||||
schedule_mode = $16, group_id = $17
|
||||
WHERE id = $18 RETURNING *`,
|
||||
schedule_mode = $16, group_id = $17, lead_days = $18
|
||||
WHERE id = $19 RETURNING *`,
|
||||
[
|
||||
b.title ?? t.title,
|
||||
b.description !== undefined ? b.description : t.description,
|
||||
|
|
@ -116,6 +119,7 @@ export async function templateRoutes(app) {
|
|||
b.active ?? t.active,
|
||||
b.schedule_mode ?? t.schedule_mode,
|
||||
b.group_id !== undefined ? (b.group_id || null) : t.group_id,
|
||||
b.lead_days !== undefined ? parseInt(b.lead_days) || 0 : t.lead_days,
|
||||
req.params.id,
|
||||
]
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue