diff --git a/frontend/src/lib/server/migrate.ts b/frontend/src/lib/server/migrate.ts index eda4e65..6a84399 100644 --- a/frontend/src/lib/server/migrate.ts +++ b/frontend/src/lib/server/migrate.ts @@ -55,6 +55,48 @@ async function updateCollection(id: string, col: any): Promise { console.log(` ✓ Updated collection: ${col.name || id}`); } +async function createRecord(collection: string, data: any): Promise { + const t = await auth(); + const res = await fetch(`${PB_ENDPOINT}/api/collections/${collection}/records`, { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` }, + body: JSON.stringify(data), + }); + if (!res.ok) { + const d = await res.json(); + throw new Error(`Create record ${collection} failed: ${JSON.stringify(d)}`); + } +} + +// Seed two default seasons (Holidays, Term time) per family so the family-admin +// season picker always has options. Idempotent by name. +async function ensureDefaultSeasons(): Promise { + const famsCol = await getCollection("fams"); + if (!famsCol) return; + const t = await auth(); + const famsRes = await fetch(`${PB_ENDPOINT}/api/collections/fams/records?perPage=200`, { + headers: { Authorization: `Bearer ${t}` }, + }); + const fams = (await famsRes.json()).items || []; + const defaults = [ + { name: "Holidays", color: "#f59e0b", active: true }, + { name: "Term time", color: "#3b82f6", active: true }, + ]; + for (const fam of fams) { + const sRes = await fetch( + `${PB_ENDPOINT}/api/collections/seasons/records?filter=(famId='${fam.id}')&fields=name&perPage=200`, + { headers: { Authorization: `Bearer ${t}` } } + ); + const have = new Set(((await sRes.json()).items || []).map((s: any) => s.name)); + for (const d of defaults) { + if (!have.has(d.name)) { + await createRecord("seasons", { famId: fam.id, ...d }); + console.log(` ✓ Seeded season ${d.name} for fam ${fam.id}`); + } + } + } +} + // Apply the custom fields + rules the app relies on to PB's native `users` // auth collection (created automatically on first serve). Children live here as // role='child'; username is a password-auth identity so the server can @@ -332,6 +374,7 @@ export async function migrate(): Promise { await ensureAssignedChoreFields(); await ensureAccessCodes(); await ensurePlatform(); + await ensureDefaultSeasons(); console.log("[migrate] Done"); } @@ -353,6 +396,12 @@ async function ensureAssignedChoreFields(): Promise { typeField.values = [...typeField.values, "emoji"]; changed = true; } + // Direct todos (created by a parent, not from a template) have no templateId. + const tplField = col.fields.find((f: any) => f.name === "templateId"); + if (tplField && tplField.required) { + tplField.required = false; + changed = true; + } if (changed) { await updateCollection(col.id, { ...col, fields: [...col.fields] }); } diff --git a/frontend/src/lib/server/services/completions.ts b/frontend/src/lib/server/services/completions.ts index d47fc61..22e393f 100644 --- a/frontend/src/lib/server/services/completions.ts +++ b/frontend/src/lib/server/services/completions.ts @@ -69,6 +69,7 @@ export async function toggle(pb: any, famId: string, memberId: string, body: { a value: Number(chore.value), rewardType: 'cash', status: 'unclaimed', + claimable: 'payday', date }); rewardId = reward.id; diff --git a/frontend/src/lib/templateIcons.ts b/frontend/src/lib/templateIcons.ts index 876e8d0..354ab02 100644 --- a/frontend/src/lib/templateIcons.ts +++ b/frontend/src/lib/templateIcons.ts @@ -69,15 +69,33 @@ export const TEMPLATE_COLORS: TemplateColor[] = [ ]; export function accentBg(color?: string): string { - if (!color) return 'transparent'; + // Opaque pastel (desaturated toward white) so it doesn't blend with the + // container background behind assigned-chore cards. Uncolored items get a + // soft purple pastel (not grey) so they stay visible. + const base = color || '#8b5cf6'; + const hex = base.replace('#', ''); + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + const t = 0.78; // amount of white to mix in → pastel + const mix = (c: number) => Math.round(c + (255 - c) * t); + return `rgb(${mix(r)}, ${mix(g)}, ${mix(b)})`; +} + +function isLight(color: string): boolean { const hex = color.replace('#', ''); const r = parseInt(hex.slice(0, 2), 16); const g = parseInt(hex.slice(2, 4), 16); const b = parseInt(hex.slice(4, 6), 16); - return `rgba(${r}, ${g}, ${b}, 0.3)`; + const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return lum > 0.8; } export function outlineColor(color?: string): string { - return color || '#e5e7eb'; + // Uncolored or very light colors get a dark purple outline for contrast. + const fallback = '#4c1d95'; + if (!color) return fallback; + if (isLight(color)) return fallback; + return color; } diff --git a/frontend/src/routes/[fam]/+layout.svelte b/frontend/src/routes/[fam]/+layout.svelte index 77e7197..c56c275 100644 --- a/frontend/src/routes/[fam]/+layout.svelte +++ b/frontend/src/routes/[fam]/+layout.svelte @@ -147,7 +147,7 @@ s.active !== false)} announcement={disabled && !activating ? accessMessage(accessReason, isParent) : ''} > + {#each list as s} + + {/each} + +{/snippet} + {#if assignModal} -
(assignModal = null)} role="presentation"> +
{ if (e.target === e.currentTarget) assignModal = null; }} role="presentation">