added todos | payday time settings | refactor payment scheduling

This commit is contained in:
JCEEE
2026-08-04 08:34:14 +01:00
parent ccbbd302d4
commit 85ea97ae96
16 changed files with 2877 additions and 659 deletions
+178 -1
View File
@@ -311,6 +311,27 @@ export async function migrate(): Promise<void> {
} else {
console.log(` ↳ settings schema is current`);
}
// Ensure simulateEow debug flag exists (read-only EOW preview toggle)
const simField = settingsCol.fields.some(
(f: any) => f.name === "simulateEow",
);
if (!simField) {
console.log("[migrate] Adding settings.simulateEow (debug toggle)...");
settingsCol.fields.push({ name: "simulateEow", type: "bool" });
await updateCollection(settingsCol.id, {
name: "settings",
type: "base",
listRule: settingsCol.listRule,
viewRule: settingsCol.viewRule,
createRule: settingsCol.createRule,
updateRule: settingsCol.updateRule,
deleteRule: settingsCol.deleteRule,
fields: settingsCol.fields,
});
} else {
console.log(` ↳ settings.simulateEow already exists`);
}
}
// ── 4. Create notifications collection if missing ──
@@ -383,6 +404,119 @@ export async function migrate(): Promise<void> {
}
}
// ── 5b. Add lastIssued (payday heartbeat) field to fams if missing ──
{
const fc = famsCol || (await getCollection("fams"));
if (fc) {
const hasLastIssued = fc.fields.some((f: any) => f.name === "lastIssued");
if (!hasLastIssued) {
console.log("[migrate] Adding lastIssued field to fams...");
fc.fields.push({ name: "lastIssued", type: "text" });
await updateCollection(fc.id, {
name: "fams",
type: "base",
listRule: fc.listRule,
viewRule: fc.viewRule,
createRule: fc.createRule,
updateRule: fc.updateRule,
deleteRule: fc.deleteRule,
fields: fc.fields,
});
} else {
console.log(` ↳ fams.lastIssued already exists`);
}
}
}
// ── 5c. Add paydayTime (HH:MM) field to fams if missing ──
{
const fc = famsCol || (await getCollection("fams"));
if (fc) {
const hasPaydayTime = fc.fields.some((f: any) => f.name === "paydayTime");
if (!hasPaydayTime) {
console.log("[migrate] Adding paydayTime field to fams...");
fc.fields.push({ name: "paydayTime", type: "text" });
await updateCollection(fc.id, {
name: "fams",
type: "base",
listRule: fc.listRule,
viewRule: fc.viewRule,
createRule: fc.createRule,
updateRule: fc.updateRule,
deleteRule: fc.deleteRule,
fields: fc.fields,
});
} else {
console.log(` ↳ fams.paydayTime already exists`);
}
}
}
// ── 5d. Add timezone (IANA name or "auto") field to fams if missing ──
{
const fc = famsCol || (await getCollection("fams"));
if (fc) {
const hasTz = fc.fields.some((f: any) => f.name === "timezone");
if (!hasTz) {
console.log("[migrate] Adding timezone field to fams...");
fc.fields.push({
name: "timezone",
type: "text",
max: 64,
pattern: "",
autogeneratePattern: "",
primaryKey: false,
system: false,
required: false,
unique: false,
hidden: false,
presentable: false,
noDecimal: false,
});
await updateCollection(fc.id, {
name: "fams",
type: "base",
listRule: fc.listRule,
viewRule: fc.viewRule,
createRule: fc.createRule,
updateRule: fc.updateRule,
deleteRule: fc.deleteRule,
fields: fc.fields,
});
} else {
console.log(` ↳ fams.timezone already exists`);
}
}
}
// ── 5e. Backfill fams.timezone = "auto" for any existing fams ──
try {
const t = await auth();
const res = await fetch(
`${PB_ENDPOINT}/api/collections/fams/records?perPage=200&filter=${encodeURIComponent(
`timezone = "" || timezone = null`,
)}`,
{ headers: { Authorization: `Bearer ${t}` } },
);
const data = await res.json();
const fams = data?.items || [];
for (const f of fams) {
await fetch(`${PB_ENDPOINT}/api/collections/fams/records/${f.id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${t}`,
},
body: JSON.stringify({ timezone: "auto" }),
});
}
if (fams.length) {
console.log(` ✓ Backfilled timezone="auto" for ${fams.length} fam${fams.length > 1 ? "s" : ""}`);
}
} catch (err) {
console.log(" ↳ timezone backfill skipped:", err instanceof Error ? err.message : err);
}
// ── 6. Backfill completions.date — strip timestamps to YYYY-MM-DD ──
// PB v0.25 doesn't allow changing field type (date→text), so we keep it as `date`
// and just normalise existing records. The frontend reads with .slice(0, 10) either way.
@@ -1164,5 +1298,48 @@ export async function migrate(): Promise<void> {
}
}
console.log("[migrate] Done");
// ── 7. Add todo fields to assigned_chores if missing ──
const assignedCol = await getCollection("assigned_chores");
if (assignedCol) {
let needsUpdate = false;
// 7a. Make templateId non-required (todos don't use templates)
const tplField = assignedCol.fields.find((f: any) => f.name === "templateId");
if (tplField && tplField.required) {
console.log("[migrate] Making assigned_chores.templateId non-required...");
tplField.required = false;
needsUpdate = true;
}
// 7b. Add isTodo, startDate, completeBy fields
const hasIsTodo = assignedCol.fields.some((f: any) => f.name === "isTodo");
if (!hasIsTodo) {
console.log("[migrate] Adding todo fields to assigned_chores...");
assignedCol.fields.push(
{ name: "isTodo", type: "bool" },
{ name: "startDate", type: "text" },
{ name: "completeBy", type: "text" }
);
needsUpdate = true;
} else {
console.log(` ↳ assigned_chores.isTodo already exists`);
}
if (needsUpdate) {
await updateCollection(assignedCol.id, {
name: "assigned_chores",
type: "base",
listRule: assignedCol.listRule,
viewRule: assignedCol.viewRule,
createRule: assignedCol.createRule,
updateRule: assignedCol.updateRule,
deleteRule: assignedCol.deleteRule,
fields: assignedCol.fields,
});
}
} else {
console.log(` ↳ assigned_chores collection not found (will be created by seed)`);
}
console.log("[migrate] Done");
}