add fixes to claims and seasons
This commit is contained in:
+279
-25
@@ -326,12 +326,53 @@ app.patch("/api/admin/:famId/assigned-chores/:id", requireAdmin, async (c) => {
|
||||
|
||||
app.delete("/api/admin/:famId/assigned-chores/:id", requireAdmin, async (c) => {
|
||||
try {
|
||||
const { id } = c.req.param();
|
||||
const { famId, id } = c.req.param();
|
||||
const comps = await pb.getList("completions", `famId = '${famId}'`);
|
||||
const toDelete = (comps.items || []).filter((c: any) => c.assignedChoreId === id);
|
||||
for (const comp of toDelete) {
|
||||
await pb.delete("completions", comp.id);
|
||||
}
|
||||
await pb.delete("assigned_chores", id);
|
||||
return c.json({ ok: true });
|
||||
} catch (err) { return handleError(c, err); }
|
||||
});
|
||||
|
||||
// ── Admin CRUD: Seasons ──────────────────────────
|
||||
|
||||
app.get("/api/admin/:famId/seasons", requireAdmin, async (c) => {
|
||||
try {
|
||||
const { famId } = c.req.param();
|
||||
const data = await pb.getList("seasons", `famId = '${famId}'`);
|
||||
return c.json(data.items);
|
||||
} catch (err) { return handleError(c, err); }
|
||||
});
|
||||
|
||||
app.post("/api/admin/:famId/seasons", requireAdmin, async (c) => {
|
||||
try {
|
||||
const { famId } = c.req.param();
|
||||
const body = await c.req.json();
|
||||
const record = await pb.create("seasons", { famId, ...body });
|
||||
return c.json(record);
|
||||
} catch (err) { return handleError(c, err); }
|
||||
});
|
||||
|
||||
app.patch("/api/admin/:famId/seasons/:id", requireAdmin, async (c) => {
|
||||
try {
|
||||
const { id } = c.req.param();
|
||||
const body = await c.req.json();
|
||||
const record = await pb.update("seasons", id, body);
|
||||
return c.json(record);
|
||||
} catch (err) { return handleError(c, err); }
|
||||
});
|
||||
|
||||
app.delete("/api/admin/:famId/seasons/:id", requireAdmin, async (c) => {
|
||||
try {
|
||||
const { id } = c.req.param();
|
||||
await pb.delete("seasons", id);
|
||||
return c.json({ ok: true });
|
||||
} catch (err) { return handleError(c, err); }
|
||||
});
|
||||
|
||||
// ── Admin: Regenerate invite code ────────────────────────
|
||||
|
||||
app.get("/api/admin/:famId/fam", requireAdmin, async (c) => {
|
||||
@@ -397,9 +438,9 @@ app.get("/api/admin/:famId/weekly-summary", requireAdmin, async (c) => {
|
||||
|
||||
let rewardPointsList: any[] = [];
|
||||
try {
|
||||
const pointRewards = await pb.getList("rewards", `famId = '${famId}' && rewardType = 'points' && claimed = true`);
|
||||
const pointRewards = await pb.getList("rewards", `famId = '${famId}' && rewardType = 'points' && status = 'claimed'`);
|
||||
rewardPointsList = pointRewards.items;
|
||||
} catch {} // schema may not have rewardType/claimed yet
|
||||
} catch {} // schema may not have rewardType/status yet
|
||||
|
||||
const assignedList = assigned.items;
|
||||
const completionsList = completions.items;
|
||||
@@ -446,12 +487,19 @@ app.get("/api/admin/:famId/weekly-summary", requireAdmin, async (c) => {
|
||||
return sum + (chore?.type === "money" ? Number(chore.value) : 0);
|
||||
}, 0);
|
||||
|
||||
// Include claimed cash rewards in money earned
|
||||
let bonusMoney = 0;
|
||||
try {
|
||||
const cashRewards = await pb.getList("rewards", `famId = '${famId}' && memberId = '${m.id}' && rewardType = 'cash' && status = 'claimed'`);
|
||||
bonusMoney = cashRewards.items.reduce((sum: number, r: any) => sum + Number(r.value), 0);
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
memberId: m.id,
|
||||
memberName: m.name,
|
||||
memberColor: m.color,
|
||||
pointsEarned: weekPoints + bonusPoints,
|
||||
moneyEarned: weekMoney,
|
||||
moneyEarned: weekMoney + bonusMoney,
|
||||
choresCompleted: memberCompletions.length,
|
||||
totalChores: memberAssignments.length,
|
||||
dayPoints,
|
||||
@@ -590,10 +638,10 @@ app.get("/api/admin/:famId/bonus-configs/progress", requireAdmin, async (c) => {
|
||||
current: teamCurrent,
|
||||
criteriaValue: cfg.criteriaValue || 0,
|
||||
reward: teamReward
|
||||
? { id: teamReward.id, claimed: teamReward.claimed }
|
||||
? { id: teamReward.id, status: teamReward.status }
|
||||
: null,
|
||||
state: teamReward
|
||||
? (teamReward.claimed ? "claimed" : "unclaimed")
|
||||
? teamReward.status
|
||||
: "pending",
|
||||
achieved: teamReward ? true : false,
|
||||
});
|
||||
@@ -626,10 +674,10 @@ app.get("/api/admin/:famId/bonus-configs/progress", requireAdmin, async (c) => {
|
||||
current,
|
||||
criteriaValue: cfg.criteriaValue || 0,
|
||||
reward: memberReward
|
||||
? { id: memberReward.id, claimed: memberReward.claimed }
|
||||
? { id: memberReward.id, status: memberReward.status }
|
||||
: null,
|
||||
state: memberReward
|
||||
? (memberReward.claimed ? "claimed" : "unclaimed")
|
||||
? memberReward.status
|
||||
: "pending",
|
||||
achieved: memberReward ? true : false,
|
||||
});
|
||||
@@ -730,7 +778,7 @@ async function evaluateFam(famId: string): Promise<void> {
|
||||
famId, memberId: m.id, bonusConfigId: cfg.id,
|
||||
label, value: Number(cfg.rewardValue) || 0,
|
||||
rewardType: cfg.rewardType,
|
||||
claimed: cfg.rewardType === "points",
|
||||
status: cfg.rewardType === "points" ? "claimed" : "unclaimed",
|
||||
claimedAt: cfg.rewardType === "points" ? now : null,
|
||||
date: now.slice(0, 10),
|
||||
});
|
||||
@@ -755,17 +803,17 @@ async function evaluateFam(famId: string): Promise<void> {
|
||||
const winner = eligible.sort((a, b) => b.current - a.current)[0];
|
||||
if (winner && !existingRewards.length) {
|
||||
const label = cfg.rewardType === "cash"
|
||||
? `${cfg.name} – £${(Number(cfg.rewardValue) / 100).toFixed(2)}`
|
||||
: `${cfg.name} – ${cfg.rewardValue}`;
|
||||
const now = new Date().toISOString();
|
||||
await pb.create("rewards", {
|
||||
famId, memberId: winner.memberId, bonusConfigId: cfg.id,
|
||||
label, value: Number(cfg.rewardValue) || 0,
|
||||
rewardType: cfg.rewardType,
|
||||
claimed: cfg.rewardType === "points",
|
||||
claimedAt: cfg.rewardType === "points" ? now : null,
|
||||
date: now.slice(0, 10),
|
||||
});
|
||||
? `${cfg.name} – £${(Number(cfg.rewardValue) / 100).toFixed(2)}`
|
||||
: `${cfg.name} – ${cfg.rewardValue}`;
|
||||
const now = new Date().toISOString();
|
||||
await pb.create("rewards", {
|
||||
famId, memberId: m.id, bonusConfigId: cfg.id,
|
||||
label, value: Number(cfg.rewardValue) || 0,
|
||||
rewardType: cfg.rewardType,
|
||||
status: cfg.rewardType === "points" ? "claimed" : "unclaimed",
|
||||
claimedAt: cfg.rewardType === "points" ? now : null,
|
||||
date: now.slice(0, 10),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -825,7 +873,7 @@ app.post("/api/admin/:famId/bonus-configs/:id/trigger", requireAdmin, async (c)
|
||||
// Check occurrence limits per target member
|
||||
for (const m of targetMembers) {
|
||||
const memberRewards = existingRewards.items.filter((r: any) => r.memberId === m.id);
|
||||
if (cfg.occurrence === "once" && memberRewards.some((r: any) => !r.claimed)) {
|
||||
if (cfg.occurrence === "once" && memberRewards.some((r: any) => r.status === "unclaimed")) {
|
||||
return c.json({ error: `Already issued and pending for ${m.name}` }, 400);
|
||||
}
|
||||
if (cfg.occurrence === "recurring" && cfg.period) {
|
||||
@@ -849,7 +897,7 @@ app.post("/api/admin/:famId/bonus-configs/:id/trigger", requireAdmin, async (c)
|
||||
famId, memberId: m.id, bonusConfigId: cfg.id,
|
||||
label, value: Number(cfg.rewardValue) || 0,
|
||||
rewardType: cfg.rewardType,
|
||||
claimed: cfg.rewardType === "points",
|
||||
status: cfg.rewardType === "points" ? "claimed" : "unclaimed",
|
||||
claimedAt: cfg.rewardType === "points" ? now : null,
|
||||
date: now.slice(0, 10),
|
||||
});
|
||||
@@ -896,6 +944,21 @@ app.post("/api/admin/:famId/bonus-configs/evaluate", requireAdmin, async (c) =>
|
||||
} catch (err) { return handleError(c, err); }
|
||||
});
|
||||
|
||||
// ── Member: Get seasons ─────────────────────────────────
|
||||
|
||||
app.get("/api/members/seasons", async (c) => {
|
||||
try {
|
||||
const deviceToken = c.req.header("x-device-token");
|
||||
if (!deviceToken) return c.json({ error: "x-device-token required" }, 400);
|
||||
const hashHex = crypto.createHash("sha256").update(deviceToken).digest("hex");
|
||||
const members = await pb.getList("members", `deviceToken = '${hashHex}'`);
|
||||
const member = members.items?.[0];
|
||||
if (!member) return c.json({ error: "Invalid device token" }, 401);
|
||||
const seasons = await pb.getList("seasons", `famId = '${member.famId}'`);
|
||||
return c.json({ seasons: seasons.items, famId: member.famId });
|
||||
} catch (err) { return handleError(c, err); }
|
||||
});
|
||||
|
||||
// ── Member: Get chores (for kanban) ──────────────────────
|
||||
|
||||
app.post("/api/members/my-chores", requireDeviceToken, async (c) => {
|
||||
@@ -969,11 +1032,35 @@ app.post("/api/admin/:famId/rewards/:id/claim", requireAdmin, async (c) => {
|
||||
try {
|
||||
const { id } = c.req.param();
|
||||
const now = new Date().toISOString();
|
||||
const record = await pb.update("rewards", id, { claimed: true, claimedAt: now });
|
||||
const record = await pb.update("rewards", id, { status: "claimed", claimedAt: now });
|
||||
return c.json(record);
|
||||
} catch (err) { return handleError(c, err); }
|
||||
});
|
||||
|
||||
// ── Admin: Issue all requested rewards for a member ─────
|
||||
|
||||
app.post("/api/admin/:famId/rewards/issue-all", requireAdmin, async (c) => {
|
||||
try {
|
||||
const { famId } = c.req.param();
|
||||
const body = await c.req.json();
|
||||
const { memberId, message } = body;
|
||||
if (!memberId) return c.json({ error: "memberId required" }, 400);
|
||||
const now = new Date().toISOString();
|
||||
// Find all requested rewards for this member
|
||||
const rewards = await pb.getList("rewards", `famId = '${famId}' && memberId = '${memberId}' && status = 'requested'`);
|
||||
let count = 0;
|
||||
for (const r of rewards.items) {
|
||||
await pb.update("rewards", r.id, { status: "claimed", claimedAt: now });
|
||||
count++;
|
||||
}
|
||||
// Send notification to member if message provided
|
||||
if (message && count > 0) {
|
||||
await pb.create("notifications", { famId, memberId, message, read: false });
|
||||
}
|
||||
return c.json({ count });
|
||||
} catch (err) { return handleError(c, err); }
|
||||
});
|
||||
|
||||
// ── Admin: Revoke a completion ────────────────────────
|
||||
|
||||
app.post("/api/admin/:famId/completions/:id/revoke", requireAdmin, async (c) => {
|
||||
@@ -1042,16 +1129,40 @@ app.post("/api/members/rewards/:id/claim", requireDeviceToken, async (c) => {
|
||||
const famId = c.get("famId");
|
||||
const memberId = c.get("memberId");
|
||||
const now = new Date().toISOString();
|
||||
const record = await pb.update("rewards", id, { claimed: true, claimedAt: now });
|
||||
const record = await pb.update("rewards", id, { status: "requested", requestedAt: now });
|
||||
// Create notification for admin
|
||||
const members = await pb.getList("members", `famId = '${famId}' && id = '${memberId}'`);
|
||||
const memberName = members.items?.[0]?.name || "Unknown";
|
||||
const msg = `Claim made by ${memberName}`;
|
||||
const msg = `Claim requested by ${memberName}`;
|
||||
await pb.create("notifications", { famId, memberId, message: msg, read: false });
|
||||
return c.json(record);
|
||||
} catch (err) { return handleError(c, err); }
|
||||
});
|
||||
|
||||
// ── Member: Request all unclaimed rewards ────────────────
|
||||
|
||||
app.post("/api/members/rewards/request-all", requireDeviceToken, async (c) => {
|
||||
try {
|
||||
const famId = c.get("famId");
|
||||
const memberId = c.get("memberId");
|
||||
const now = new Date().toISOString();
|
||||
// Find all unclaimed rewards for this member
|
||||
const rewards = await pb.getList("rewards", `famId = '${famId}' && memberId = '${memberId}' && status = 'unclaimed'`);
|
||||
let count = 0;
|
||||
for (const r of rewards.items) {
|
||||
await pb.update("rewards", r.id, { status: "requested", requestedAt: now });
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
const members = await pb.getList("members", `famId = '${famId}' && id = '${memberId}'`);
|
||||
const memberName = members.items?.[0]?.name || "Unknown";
|
||||
const msg = `${count} claim(s) requested by ${memberName}`;
|
||||
await pb.create("notifications", { famId, memberId, message: msg, read: false });
|
||||
}
|
||||
return c.json({ count });
|
||||
} catch (err) { return handleError(c, err); }
|
||||
});
|
||||
|
||||
// ── Member: Notifications ──────────────────────────────
|
||||
|
||||
app.get("/api/members/notifications", requireDeviceToken, async (c) => {
|
||||
@@ -1071,6 +1182,149 @@ app.post("/api/members/notifications/:id/dismiss", requireDeviceToken, async (c)
|
||||
} catch (err) { return handleError(c, err); }
|
||||
});
|
||||
|
||||
// ── Admin: Complete week (snapshot to weekly_history) ────
|
||||
|
||||
app.post("/api/admin/:famId/complete-week", requireAdmin, async (c) => {
|
||||
try {
|
||||
const { famId } = c.req.param();
|
||||
const payday = await getFamPayday(famId);
|
||||
const ws = weekStart(payday);
|
||||
|
||||
// Evaluate weekly bonus configs first
|
||||
await evaluateFam(famId);
|
||||
|
||||
// Fetch data for summary
|
||||
const [members, assigned, completions] = await Promise.all([
|
||||
pb.getList("members", `famId = '${famId}'`),
|
||||
pb.getList("assigned_chores", `famId = '${famId}'`),
|
||||
pb.getList("completions", `famId = '${famId}' && date >= '${ws}'`),
|
||||
]);
|
||||
|
||||
let rewardPointsList: any[] = [];
|
||||
let rewardCashList: any[] = [];
|
||||
try {
|
||||
const [pointsRewards, cashRewards] = await Promise.all([
|
||||
pb.getList("rewards", `famId = '${famId}' && rewardType = 'points' && status = 'claimed'`),
|
||||
pb.getList("rewards", `famId = '${famId}' && rewardType = 'cash' && status = 'claimed'`),
|
||||
]);
|
||||
rewardPointsList = pointsRewards.items;
|
||||
rewardCashList = cashRewards.items;
|
||||
} catch {}
|
||||
|
||||
const assignedList = assigned.items;
|
||||
const historyRecords = [];
|
||||
|
||||
for (const m of members.items) {
|
||||
const memberCompletions = completions.items.filter((c: any) => c.memberId === m.id);
|
||||
|
||||
const weekPoints = memberCompletions.reduce((sum: number, c: any) => {
|
||||
const chore = assignedList.find((a: any) => a.id === c.assignedChoreId);
|
||||
return sum + (chore?.type === "points" ? Number(chore.value) : 0);
|
||||
}, 0);
|
||||
|
||||
const weekMoney = memberCompletions.reduce((sum: number, c: any) => {
|
||||
const chore = assignedList.find((a: any) => a.id === c.assignedChoreId);
|
||||
return sum + (chore?.type === "money" ? Number(chore.value) : 0);
|
||||
}, 0);
|
||||
|
||||
const bonusPoints = rewardPointsList
|
||||
.filter((r: any) => r.memberId === m.id)
|
||||
.reduce((sum: number, r: any) => sum + Number(r.value), 0);
|
||||
|
||||
const bonusMoney = rewardCashList
|
||||
.filter((r: any) => r.memberId === m.id)
|
||||
.reduce((sum: number, r: any) => sum + Number(r.value), 0);
|
||||
|
||||
// Upsert weekly_history
|
||||
const existing = await pb.getList("weekly_history", `famId = '${famId}' && memberId = '${m.id}' && weekStart = '${ws}'`);
|
||||
const recordData = {
|
||||
famId,
|
||||
memberId: m.id,
|
||||
weekStart: ws,
|
||||
pointsEarned: weekPoints + bonusPoints,
|
||||
moneyEarned: weekMoney + bonusMoney,
|
||||
choresCompleted: memberCompletions.length,
|
||||
bonusEarned: bonusPoints,
|
||||
};
|
||||
|
||||
if (existing.items?.length > 0) {
|
||||
await pb.update("weekly_history", existing.items[0].id, recordData);
|
||||
} else {
|
||||
const record = await pb.create("weekly_history", recordData);
|
||||
historyRecords.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
return c.json({ weekStart: ws, historyRecords, memberCount: members.items.length });
|
||||
} catch (err) { return handleError(c, err); }
|
||||
});
|
||||
|
||||
// ── Admin: Generate test data ────────────────────────────
|
||||
|
||||
app.post("/api/admin/:famId/debug/generate-data", requireAdmin, async (c) => {
|
||||
try {
|
||||
const { famId } = c.req.param();
|
||||
const body = await c.req.json().catch(() => ({}));
|
||||
const days = body.days || 7;
|
||||
|
||||
const [members, templates] = await Promise.all([
|
||||
pb.getList("members", `famId = '${famId}'`),
|
||||
pb.getList("chore_templates", `famId = '${famId}'`),
|
||||
]);
|
||||
|
||||
if (!members.items.length) return c.json({ error: "No members found" }, 400);
|
||||
if (!templates.items.length) return c.json({ error: "No templates found" }, 400);
|
||||
|
||||
let completionsCreated = 0;
|
||||
const today = new Date();
|
||||
|
||||
for (let d = 0; d < days; d++) {
|
||||
const date = new Date(today);
|
||||
date.setDate(date.getDate() - d);
|
||||
const dateStr = date.toISOString().slice(0, 10);
|
||||
|
||||
for (const m of members.items) {
|
||||
// Randomly complete 50-100% of templates
|
||||
const completionRate = 0.5 + Math.random() * 0.5;
|
||||
for (const t of templates.items) {
|
||||
if (Math.random() > completionRate) continue;
|
||||
|
||||
// Create assigned_chores if needed
|
||||
let assigned = await pb.getList("assigned_chores", `famId = '${famId}' && memberId = '${m.id}' && templateId = '${t.id}'`);
|
||||
let assignedId;
|
||||
if (assigned.items?.length > 0) {
|
||||
assignedId = assigned.items[0].id;
|
||||
} else {
|
||||
const record = await pb.create("assigned_chores", {
|
||||
famId,
|
||||
memberId: m.id,
|
||||
templateId: t.id,
|
||||
frequency: t.defaultFrequency || "daily",
|
||||
type: t.defaultType || "points",
|
||||
value: t.defaultValue || 10,
|
||||
});
|
||||
assignedId = record.id;
|
||||
}
|
||||
|
||||
// Check if already completed
|
||||
const existing = await pb.getList("completions", `famId = '${famId}' && memberId = '${m.id}' && assignedChoreId = '${assignedId}' && date = '${dateStr}'`);
|
||||
if (existing.items?.length > 0) continue;
|
||||
|
||||
await pb.create("completions", {
|
||||
famId,
|
||||
memberId: m.id,
|
||||
assignedChoreId: assignedId,
|
||||
date: dateStr,
|
||||
});
|
||||
completionsCreated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.json({ completionsCreated, days });
|
||||
} catch (err) { return handleError(c, err); }
|
||||
});
|
||||
|
||||
// ── Start server ─────────────────────────────────────────
|
||||
|
||||
const port = parseInt(process.env.PROXY_PORT || "3456", 10);
|
||||
|
||||
@@ -553,5 +553,199 @@ export async function migrate(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 16. Add seasons json field to fams ──
|
||||
{
|
||||
const c = await getCollection("fams");
|
||||
if (c) {
|
||||
const hasField = c.fields.some((f: any) => f.name === "seasons");
|
||||
if (!hasField) {
|
||||
console.log("[migrate] Adding seasons to fams...");
|
||||
c.fields.push({ name: "seasons", type: "json" });
|
||||
await updateCollection(c.id, {
|
||||
name: "fams", type: "base",
|
||||
listRule: c.listRule, viewRule: c.viewRule,
|
||||
createRule: c.createRule, updateRule: c.updateRule,
|
||||
deleteRule: c.deleteRule, fields: c.fields,
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ fams.seasons already exists`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 17. Add seasonIds json field to assigned_chores ──
|
||||
{
|
||||
const c = await getCollection("assigned_chores");
|
||||
if (c) {
|
||||
const hasField = c.fields.some((f: any) => f.name === "seasonIds");
|
||||
if (!hasField) {
|
||||
console.log("[migrate] Adding seasonIds to assigned_chores...");
|
||||
c.fields.push({ name: "seasonIds", type: "json" });
|
||||
await updateCollection(c.id, {
|
||||
name: "assigned_chores", type: "base",
|
||||
listRule: c.listRule, viewRule: c.viewRule,
|
||||
createRule: c.createRule, updateRule: c.updateRule,
|
||||
deleteRule: c.deleteRule, fields: c.fields,
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ assigned_chores.seasonIds already exists`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 18. Create seasons collection if missing ──
|
||||
{
|
||||
const existing = await getCollection("seasons");
|
||||
if (!existing) {
|
||||
console.log("[migrate] Creating seasons collection...");
|
||||
const t = await auth();
|
||||
const famsCol = await getCollection("fams");
|
||||
const res = await fetch(`${PB_ENDPOINT}/api/collections`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` },
|
||||
body: JSON.stringify({
|
||||
name: "seasons", type: "base",
|
||||
listRule: "", viewRule: "",
|
||||
createRule: null, updateRule: null, deleteRule: null,
|
||||
fields: [
|
||||
{ name: "famId", type: "relation", required: true, maxSelect: 1, collectionId: famsCol?.id || "" },
|
||||
{ name: "name", type: "text", required: true },
|
||||
{ name: "color", type: "text" },
|
||||
{ name: "active", type: "bool" },
|
||||
{ name: "autoDisable", type: "date" },
|
||||
{ name: "autoStart", type: "date" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
if (res.ok) console.log(" ✓ Created seasons collection");
|
||||
else console.log(` ↳ seasons collection creation skipped or already exists`);
|
||||
} else {
|
||||
console.log(` ↳ seasons collection already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 19. Add active bool to existing seasons collection ──
|
||||
{
|
||||
const c = await getCollection("seasons");
|
||||
if (c) {
|
||||
const hasActive = c.fields.some((f: any) => f.name === "active");
|
||||
if (!hasActive) {
|
||||
console.log("[migrate] Adding active bool to seasons...");
|
||||
c.fields.push({ name: "active", type: "bool" });
|
||||
await updateCollection(c.id, {
|
||||
name: "seasons", type: "base",
|
||||
listRule: c.listRule, viewRule: c.viewRule,
|
||||
createRule: c.createRule, updateRule: c.updateRule,
|
||||
deleteRule: c.deleteRule, fields: c.fields,
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ seasons.active already exists`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 20. Backfill active=true from fams.seasons array ──
|
||||
{
|
||||
const t = await auth();
|
||||
const fams = await getCollection("fams");
|
||||
if (fams) {
|
||||
const hasSeasonsField = fams.fields.some((f: any) => f.name === "seasons");
|
||||
if (hasSeasonsField) {
|
||||
console.log("[migrate] Backfilling season active flags from fams.seasons...");
|
||||
const allFams = await fetch(`${PB_ENDPOINT}/api/collections/fams/records?perPage=1000`, {
|
||||
headers: { Authorization: `Bearer ${t}` },
|
||||
});
|
||||
const famData = await allFams.json();
|
||||
for (const fam of famData.items || []) {
|
||||
const activeIds = fam.seasons || [];
|
||||
if (activeIds.length > 0) {
|
||||
for (const sid of activeIds) {
|
||||
await fetch(`${PB_ENDPOINT}/api/collections/seasons/records/${sid}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` },
|
||||
body: JSON.stringify({ active: true }),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(` ↳ backfilled ${famData.items?.length || 0} fams`);
|
||||
} else {
|
||||
console.log(` ↳ fams.seasons already removed`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 21. Remove seasons field from fams ──
|
||||
{
|
||||
const c = await getCollection("fams");
|
||||
if (c) {
|
||||
const hasField = c.fields.some((f: any) => f.name === "seasons");
|
||||
if (hasField) {
|
||||
console.log("[migrate] Removing seasons field from fams...");
|
||||
c.fields = c.fields.filter((f: any) => f.name !== "seasons");
|
||||
await updateCollection(c.id, {
|
||||
name: "fams", type: "base",
|
||||
listRule: c.listRule, viewRule: c.viewRule,
|
||||
createRule: c.createRule, updateRule: c.updateRule,
|
||||
deleteRule: c.deleteRule, fields: c.fields,
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ fams.seasons already removed`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 22. Convert rewards.claimed boolean to status select field ──
|
||||
{
|
||||
const c = await getCollection("rewards");
|
||||
if (c) {
|
||||
const hasClaimedField = c.fields.some((f: any) => f.name === "claimed");
|
||||
const hasStatusField = c.fields.some((f: any) => f.name === "status");
|
||||
if (hasClaimedField && !hasStatusField) {
|
||||
console.log("[migrate] Converting rewards.claimed to status field...");
|
||||
|
||||
// Fetch all rewards to backfill status
|
||||
const t = await auth();
|
||||
let page = 1;
|
||||
let total = 0;
|
||||
while (true) {
|
||||
const res = await fetch(`${PB_ENDPOINT}/api/collections/rewards/records?page=${page}&perPage=100`, {
|
||||
headers: { Authorization: `Bearer ${t}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
for (const r of data.items || []) {
|
||||
const status = r.claimed ? "claimed" : "unclaimed";
|
||||
await fetch(`${PB_ENDPOINT}/api/collections/rewards/records/${r.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` },
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
total++;
|
||||
}
|
||||
if (!data.items || data.items.length < 100) break;
|
||||
page++;
|
||||
}
|
||||
console.log(` ↳ backfilled ${total} rewards with status`);
|
||||
|
||||
// Remove claimed field and add status + requestedAt fields
|
||||
c.fields = c.fields.filter((f: any) => f.name !== "claimed");
|
||||
if (!c.fields.some((f: any) => f.name === "status")) {
|
||||
c.fields.push({ name: "status", type: "select", required: true, values: ["unclaimed", "requested", "claimed"], maxSelect: 1 });
|
||||
}
|
||||
if (!c.fields.some((f: any) => f.name === "requestedAt")) {
|
||||
c.fields.push({ name: "requestedAt", type: "date", required: false });
|
||||
}
|
||||
await updateCollection(c.id, {
|
||||
name: "rewards", type: "base",
|
||||
listRule: c.listRule, viewRule: c.viewRule,
|
||||
createRule: c.createRule, updateRule: c.updateRule,
|
||||
deleteRule: c.deleteRule, fields: c.fields,
|
||||
});
|
||||
} else if (!hasClaimedField) {
|
||||
console.log(` ↳ rewards.claimed already converted to status`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[migrate] Done");
|
||||
}
|
||||
|
||||
+5
-5
@@ -56,14 +56,14 @@ export const pb = {
|
||||
|
||||
async delete(collection: string, id: string) {
|
||||
const res = await request("DELETE", `/api/collections/${collection}/records/${id}`);
|
||||
if (!res.ok) throw new Error(`PB delete ${collection}: ${res.status}`);
|
||||
const body = await res.text();
|
||||
if (!res.ok) throw new Error(`PB delete ${collection}: ${res.status} ${body}`);
|
||||
},
|
||||
|
||||
async getList(collection: string, filter = "") {
|
||||
const params = new URLSearchParams();
|
||||
if (filter) params.set("filter", filter);
|
||||
params.set("perPage", "200");
|
||||
const res = await request("GET", `/api/collections/${collection}/records?${params}`);
|
||||
let path = `/api/collections/${collection}/records?perPage=1000`;
|
||||
if (filter) path += `&filter=${encodeURIComponent(filter)}`;
|
||||
const res = await request("GET", path);
|
||||
const json = await res.json();
|
||||
if (!res.ok) throw new Error(`PB list ${collection}: ${JSON.stringify(json)}`);
|
||||
return json;
|
||||
|
||||
Reference in New Issue
Block a user