remove payday debug and add multiple ui fixes
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let { variant = 'primary', size = 'md', href, onclick, children, ...rest }: {
|
let { variant = 'primary', size = 'md', href, onclick, children, ...rest }: {
|
||||||
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'success';
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
href?: string;
|
href?: string;
|
||||||
onclick?: () => void;
|
onclick?: () => void;
|
||||||
@@ -46,4 +46,7 @@
|
|||||||
|
|
||||||
.btn-danger { background: #dc2626; color: #fff; border-color: #dc2626; }
|
.btn-danger { background: #dc2626; color: #fff; border-color: #dc2626; }
|
||||||
.btn-danger:hover { background: #b91c1c; }
|
.btn-danger:hover { background: #b91c1c; }
|
||||||
|
|
||||||
|
.btn-success { background: #059669; color: #fff; border-color: #059669; }
|
||||||
|
.btn-success:hover { background: #047857; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { notices, type NoticeType } from '$lib/stores/notices.svelte';
|
import { notices, type NoticeType } from '$lib/stores/notices.svelte';
|
||||||
|
import {
|
||||||
|
checkCircleIcon,
|
||||||
|
alertTriangleIcon,
|
||||||
|
xCircleIcon,
|
||||||
|
infoIcon
|
||||||
|
} from '$lib/components/icons';
|
||||||
|
|
||||||
function icon(type: NoticeType) {
|
function icon(type: NoticeType) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'success': return '✅';
|
case 'success': return checkCircleIcon;
|
||||||
case 'warning': return '⚠️';
|
case 'warning': return alertTriangleIcon;
|
||||||
case 'error': return '❌';
|
case 'error': return xCircleIcon;
|
||||||
default: return 'ℹ️';
|
default: return infoIcon;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -16,7 +22,7 @@
|
|||||||
{#each notices.list as notice (notice.id)}
|
{#each notices.list as notice (notice.id)}
|
||||||
<div class="notice notice-{notice.type}">
|
<div class="notice notice-{notice.type}">
|
||||||
<div class="notice-content">
|
<div class="notice-content">
|
||||||
<div class="notice-icon">{icon(notice.type)}</div>
|
<div class="notice-icon">{@html icon(notice.type)}</div>
|
||||||
<div class="notice-text">
|
<div class="notice-text">
|
||||||
<h4>{notice.title}</h4>
|
<h4>{notice.title}</h4>
|
||||||
{#if notice.message}
|
{#if notice.message}
|
||||||
@@ -73,6 +79,14 @@
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.notice-icon :global(svg) {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
.notice-success .notice-icon { color: #059669; }
|
||||||
|
.notice-warning .notice-icon { color: #d97706; }
|
||||||
|
.notice-error .notice-icon { color: #dc2626; }
|
||||||
|
.notice-info .notice-icon { color: #6366f1; }
|
||||||
.notice-text h4 {
|
.notice-text h4 {
|
||||||
margin: 0 0 0.25rem;
|
margin: 0 0 0.25rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
import { checkCircleIcon } from '$lib/components/icons';
|
||||||
|
|
||||||
interface Tier {
|
interface Tier {
|
||||||
id: 'trial' | 'monthly' | 'yearly';
|
id: 'trial' | 'monthly' | 'yearly';
|
||||||
@@ -84,7 +85,7 @@
|
|||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{#each tier.features as f}
|
{#each tier.features as f}
|
||||||
<li><span class="tick">✓</span>{f}</li>
|
<li><span class="tick">{@html checkCircleIcon}</span>{f}</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</article>
|
</article>
|
||||||
@@ -202,6 +203,7 @@
|
|||||||
gap: 0.45rem;
|
gap: 0.45rem;
|
||||||
}
|
}
|
||||||
.tick { color: #059669; font-weight: 700; flex-shrink: 0; }
|
.tick { color: #059669; font-weight: 700; flex-shrink: 0; }
|
||||||
|
.tick :global(svg) { width: 1.05em; height: 1.05em; vertical-align: -0.15em; }
|
||||||
|
|
||||||
.form-error {
|
.form-error {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export const dashboardIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fi
|
|||||||
export const choresIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>'
|
export const choresIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>'
|
||||||
export const rewardsIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89L17 22l-5-3-5 3 1.523-9.11"/></svg>'
|
export const rewardsIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89L17 22l-5-3-5 3 1.523-9.11"/></svg>'
|
||||||
export const moneyIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>'
|
export const moneyIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>'
|
||||||
|
export const coinsIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="6" rx="8" ry="3"/><path d="M4 6v6c0 1.66 3.58 3 8 3s8-1.34 8-3V6"/><path d="M4 12v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6"/></svg>'
|
||||||
export const handCoinsIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 15h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 17"/><path d="m7 21 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"/><path d="M2 16l1 1h2"/><path d="M4 12l1 1h2"/><path d="M6 8l1 1h2"/><path d="M2 20l1.5-.5"/><circle cx="16" cy="8" r="2"/></svg>'
|
export const handCoinsIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 15h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 17"/><path d="m7 21 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"/><path d="M2 16l1 1h2"/><path d="M4 12l1 1h2"/><path d="M6 8l1 1h2"/><path d="M2 20l1.5-.5"/><circle cx="16" cy="8" r="2"/></svg>'
|
||||||
export const gemIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 3h12l4 6-10 13L2 9Z"/><path d="M11 3 8 9l4 13 4-13-3-6"/><path d="M2 9h20"/></svg>'
|
export const gemIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 3h12l4 6-10 13L2 9Z"/><path d="M11 3 8 9l4 13 4-13-3-6"/><path d="M2 9h20"/></svg>'
|
||||||
export const archiveIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/></svg>'
|
export const archiveIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/></svg>'
|
||||||
@@ -17,3 +18,14 @@ export const chatIcon = '<svg width="20" height="20" viewBox="0 0 24 24" fill="n
|
|||||||
export const sendIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>'
|
export const sendIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>'
|
||||||
export const checkCircleIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>'
|
export const checkCircleIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>'
|
||||||
export const revokeIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>'
|
export const revokeIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>'
|
||||||
|
export const targetIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg>'
|
||||||
|
export const calendarIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>'
|
||||||
|
export const sunIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>'
|
||||||
|
export const lockIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>'
|
||||||
|
export const clockIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>'
|
||||||
|
export const giftIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="8" width="18" height="4"/><path d="M12 8v13M12 8C12 8 9 3 6 5s0 3 3 3M12 8c0 0 3-5 6-3s0 3-3 3"/><path d="M5 12v9h14v-9"/></svg>'
|
||||||
|
export const chevronLeftIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="m15 6 -6 6 6 6"/></svg>'
|
||||||
|
export const chevronRightIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="m9 6 6 6 -6 6"/></svg>'
|
||||||
|
export const alertTriangleIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg>'
|
||||||
|
export const xCircleIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="m15 9-6 6M9 9l6 6"/></svg>'
|
||||||
|
export const infoIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 16v-4M12 8h.01"/></svg>'
|
||||||
|
|||||||
@@ -10,13 +10,6 @@ export async function myChores(pb: any, famId: string, memberId: string) {
|
|||||||
pb.collection('bonus_configs').getFullList({ filter: `famId = '${famId}' && status = 'active'` })
|
pb.collection('bonus_configs').getFullList({ filter: `famId = '${famId}' && status = 'active'` })
|
||||||
]);
|
]);
|
||||||
const { payday, paydayTime, tz } = await famMeta(pb, famId);
|
const { payday, paydayTime, tz } = await famMeta(pb, famId);
|
||||||
let settings: any = {};
|
|
||||||
try {
|
|
||||||
const s = await pb
|
|
||||||
.collection('settings')
|
|
||||||
.getFullList({ filter: `famId = '${famId}'` });
|
|
||||||
settings = s?.[0] || {};
|
|
||||||
} catch {}
|
|
||||||
return {
|
return {
|
||||||
templates,
|
templates,
|
||||||
assigned,
|
assigned,
|
||||||
@@ -25,7 +18,6 @@ export async function myChores(pb: any, famId: string, memberId: string) {
|
|||||||
bonusConfigs,
|
bonusConfigs,
|
||||||
payday,
|
payday,
|
||||||
paydayTime,
|
paydayTime,
|
||||||
timezone: tz,
|
timezone: tz
|
||||||
simulateEow: !!settings.simulateEow
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -11,13 +11,6 @@ export async function myChores(pb: any, famId: string, memberId: string) {
|
|||||||
pb.collection('bonus_configs').getFullList({ filter: `famId = '${famId}' && status = 'active'` })
|
pb.collection('bonus_configs').getFullList({ filter: `famId = '${famId}' && status = 'active'` })
|
||||||
]);
|
]);
|
||||||
const { payday, paydayTime, tz } = await famMeta(pb, famId);
|
const { payday, paydayTime, tz } = await famMeta(pb, famId);
|
||||||
let settings: any = {};
|
|
||||||
try {
|
|
||||||
const s = await pb
|
|
||||||
.collection('settings')
|
|
||||||
.getFullList({ filter: `famId = '${famId}'` });
|
|
||||||
settings = s?.[0] || {};
|
|
||||||
} catch {}
|
|
||||||
return {
|
return {
|
||||||
templates,
|
templates,
|
||||||
assigned,
|
assigned,
|
||||||
@@ -26,8 +19,7 @@ export async function myChores(pb: any, famId: string, memberId: string) {
|
|||||||
bonusConfigs,
|
bonusConfigs,
|
||||||
payday,
|
payday,
|
||||||
paydayTime,
|
paydayTime,
|
||||||
timezone: tz,
|
timezone: tz
|
||||||
simulateEow: !!settings.simulateEow
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,149 +172,6 @@ export async function weeklySummary(pb: any, famId: string) {
|
|||||||
return { weekStart: ws, daysInWeek, summaries };
|
return { weekStart: ws, daysInWeek, summaries };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function eowPreview(pb: any, famId: string) {
|
|
||||||
const { payday, tz } = await famMeta(pb, famId);
|
|
||||||
const ws = weekStart(payday, tz);
|
|
||||||
const we = periodEnd('weekly', ws);
|
|
||||||
|
|
||||||
const [members, assigned, completions, configs, rewards] = await Promise.all([
|
|
||||||
pb.collection('users').getFullList({ filter: `famId = '${famId}' && role = 'child'` }),
|
|
||||||
pb.collection('assigned_chores').getFullList({ filter: `famId = '${famId}'` }),
|
|
||||||
pb.collection('completions').getFullList({ filter: `famId = '${famId}' && date >= '${ws}'` }),
|
|
||||||
pb
|
|
||||||
.collection('bonus_configs')
|
|
||||||
.getFullList({ filter: `famId = '${famId}' && status = 'active'` })
|
|
||||||
.catch(() => []),
|
|
||||||
pb.collection('rewards').getFullList({ filter: `famId = '${famId}'` }).catch(() => [])
|
|
||||||
]);
|
|
||||||
|
|
||||||
let rewardPointsList: any[] = [];
|
|
||||||
let rewardCashList: any[] = [];
|
|
||||||
try {
|
|
||||||
[rewardPointsList, rewardCashList] = await Promise.all([
|
|
||||||
pb.collection('rewards').getFullList({
|
|
||||||
filter: `famId = '${famId}' && rewardType = 'points' && status = 'claimed' && date >= '${ws}'`
|
|
||||||
}),
|
|
||||||
pb.collection('rewards').getFullList({
|
|
||||||
filter: `famId = '${famId}' && rewardType = 'cash' && status = 'claimed' && date >= '${ws}'`
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
const assignedList = assigned;
|
|
||||||
const completionsList = completions;
|
|
||||||
|
|
||||||
const summaries = members.map((m: any) => {
|
|
||||||
const mc = completionsList.filter((c: any) => c.memberId === m.id);
|
|
||||||
const weekPoints = mc.reduce((sum: number, c: any) => {
|
|
||||||
const ch = assignedList.find((a: any) => a.id === c.assignedChoreId);
|
|
||||||
return sum + (ch?.type === 'points' ? Number(ch.value) : 0);
|
|
||||||
}, 0);
|
|
||||||
const weekMoney = mc.reduce((sum: number, c: any) => {
|
|
||||||
const ch = assignedList.find((a: any) => a.id === c.assignedChoreId);
|
|
||||||
return sum + (ch?.type === 'money' ? Number(ch.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);
|
|
||||||
return {
|
|
||||||
memberId: m.id,
|
|
||||||
memberName: m.name || m.username || m.id.slice(0, 6),
|
|
||||||
memberColor: m.color,
|
|
||||||
pointsEarned: weekPoints + bonusPoints,
|
|
||||||
moneyEarned: weekMoney + bonusMoney,
|
|
||||||
choresCompleted: mc.length,
|
|
||||||
bonusEarned: bonusPoints
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const predictedRewards: any[] = [];
|
|
||||||
const existingRewards = rewards;
|
|
||||||
for (const cfg of configs) {
|
|
||||||
if (cfg.type === 'manual') continue;
|
|
||||||
const pStart = cfg.period ? periodStart(cfg.period, payday, tz) : '';
|
|
||||||
const pEnd = cfg.period ? periodEnd(cfg.period, pStart) : '';
|
|
||||||
const periodCompletions = cfg.period
|
|
||||||
? completionsList.filter(
|
|
||||||
(c: any) => (c.date || '').slice(0, 10) >= pStart && (c.date || '').slice(0, 10) <= pEnd
|
|
||||||
)
|
|
||||||
: completionsList;
|
|
||||||
const cfgRewards = existingRewards.filter((r: any) => r.bonusConfigId === cfg.id);
|
|
||||||
|
|
||||||
const tryEval = (sourceComps: any[]) => {
|
|
||||||
if (cfg.type === 'threshold')
|
|
||||||
return sourceComps.reduce((sum: number, c: any) => {
|
|
||||||
const ch = assignedList.find((a: any) => a.id === c.assignedChoreId);
|
|
||||||
return sum + (ch?.type === 'points' ? Number(ch.value) : 0);
|
|
||||||
}, 0);
|
|
||||||
if (cfg.type === 'count') return sourceComps.length;
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (cfg.target === 'individual') {
|
|
||||||
const targets = cfg.memberId ? members.filter((m: any) => m.id === cfg.memberId) : members;
|
|
||||||
for (const m of targets) {
|
|
||||||
if (cfgRewards.some((r: any) => r.memberId === m.id)) continue;
|
|
||||||
const current = tryEval(periodCompletions.filter((c: any) => c.memberId === m.id));
|
|
||||||
if (cfg.criteriaValue > 0 && current >= Number(cfg.criteriaValue))
|
|
||||||
predictedRewards.push({
|
|
||||||
config: cfg.name,
|
|
||||||
memberName: m.name || m.id.slice(0, 6),
|
|
||||||
type: cfg.rewardType,
|
|
||||||
value: Number(cfg.rewardValue) || 0,
|
|
||||||
detail: `${cfg.type} ${current}/${cfg.criteriaValue}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (cfg.target === 'collaborative') {
|
|
||||||
if (cfgRewards.length) continue;
|
|
||||||
const allIds = members.map((m: any) => m.id);
|
|
||||||
const teamComps = periodCompletions.filter((c: any) => allIds.includes(c.memberId));
|
|
||||||
const current = tryEval(teamComps);
|
|
||||||
if (cfg.criteriaValue > 0 && current >= Number(cfg.criteriaValue))
|
|
||||||
predictedRewards.push({
|
|
||||||
config: cfg.name,
|
|
||||||
memberName: 'Everyone',
|
|
||||||
type: cfg.rewardType,
|
|
||||||
value: Number(cfg.rewardValue) || 0,
|
|
||||||
detail: `${cfg.type} ${current}/${cfg.criteriaValue}`
|
|
||||||
});
|
|
||||||
} else if (cfg.target === 'competitive') {
|
|
||||||
if (cfgRewards.length) continue;
|
|
||||||
const scored = members.map((m: any) => ({
|
|
||||||
memberId: m.id,
|
|
||||||
name: m.name,
|
|
||||||
current: tryEval(periodCompletions.filter((c: any) => c.memberId === m.id))
|
|
||||||
}));
|
|
||||||
const qualified = scored.filter((st: any) => st.current >= Number(cfg.criteriaValue));
|
|
||||||
const eligible = qualified.length ? qualified : scored.filter((st: any) => st.current > 0);
|
|
||||||
const winner = eligible.sort((aa: any, bb: any) => bb.current - aa.current)[0];
|
|
||||||
if (winner)
|
|
||||||
predictedRewards.push({
|
|
||||||
config: cfg.name,
|
|
||||||
memberName: winner.name,
|
|
||||||
type: cfg.rewardType,
|
|
||||||
value: Number(cfg.rewardValue) || 0,
|
|
||||||
detail: `winner ${winner.current} pts`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextWeekStart = addDaysStr(ws, 7);
|
|
||||||
|
|
||||||
return {
|
|
||||||
simulateEow: true,
|
|
||||||
weekStart: ws,
|
|
||||||
weekEnd: we,
|
|
||||||
nextWeekStart,
|
|
||||||
summaries,
|
|
||||||
predictedRewards,
|
|
||||||
completionsThisWeek: completionsList.length
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function releaseWeek(pb: any, famId: string) {
|
export async function releaseWeek(pb: any, famId: string) {
|
||||||
const { payday, paydayTime, tz } = await famMeta(pb, famId);
|
const { payday, paydayTime, tz } = await famMeta(pb, famId);
|
||||||
const fams = await pb.collection('fams').getFullList({ filter: `id = '${famId}'` });
|
const fams = await pb.collection('fams').getFullList({ filter: `id = '${famId}'` });
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export function createServices(
|
|||||||
...(timezone !== undefined ? { timezone } : {})
|
...(timezone !== undefined ? { timezone } : {})
|
||||||
}),
|
}),
|
||||||
weeklySummary: (famId: string) => famSvc.weeklySummary(pb, famId),
|
weeklySummary: (famId: string) => famSvc.weeklySummary(pb, famId),
|
||||||
eowPreview: (famId: string) => famSvc.eowPreview(pb, famId),
|
|
||||||
payday: (famId: string) => famSvc.releaseWeek(pb, famId),
|
payday: (famId: string) => famSvc.releaseWeek(pb, famId),
|
||||||
completeWeek: (famId: string) => famSvc.completeWeek(pb, famId),
|
completeWeek: (famId: string) => famSvc.completeWeek(pb, famId),
|
||||||
getProfile: (famId: string) => famSvc.getProfile(pb, famId, uid),
|
getProfile: (famId: string) => famSvc.getProfile(pb, famId, uid),
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ function resolveServerTz(tz?: string): string {
|
|||||||
export async function getSettings(pb: any, famId: string) {
|
export async function getSettings(pb: any, famId: string) {
|
||||||
const list = await pb.collection('settings').getFullList({ filter: `famId = '${famId}'` });
|
const list = await pb.collection('settings').getFullList({ filter: `famId = '${famId}'` });
|
||||||
const s = list?.[0] || {};
|
const s = list?.[0] || {};
|
||||||
return { simulateEow: !!s.simulateEow, webhookUrl: s.webhookUrl || '' };
|
return { webhookUrl: s.webhookUrl || '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSettings(pb: any, famId: string, data: Record<string, unknown>) {
|
export async function updateSettings(pb: any, famId: string, data: Record<string, unknown>) {
|
||||||
const list = await pb.collection('settings').getFullList({ filter: `famId = '${famId}'` });
|
const list = await pb.collection('settings').getFullList({ filter: `famId = '${famId}'` });
|
||||||
const existing = list?.[0];
|
const existing = list?.[0];
|
||||||
const patch: Record<string, unknown> = {};
|
const patch: Record<string, unknown> = {};
|
||||||
if (data.simulateEow !== undefined) patch.simulateEow = !!data.simulateEow;
|
|
||||||
if (data.webhookUrl !== undefined) patch.webhookUrl = String(data.webhookUrl);
|
if (data.webhookUrl !== undefined) patch.webhookUrl = String(data.webhookUrl);
|
||||||
let s;
|
let s;
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -23,5 +22,5 @@ export async function updateSettings(pb: any, famId: string, data: Record<string
|
|||||||
} else {
|
} else {
|
||||||
s = await pb.collection('settings').create({ famId, ...patch });
|
s = await pb.collection('settings').create({ famId, ...patch });
|
||||||
}
|
}
|
||||||
return { simulateEow: !!s.simulateEow, webhookUrl: s.webhookUrl || '' };
|
return { webhookUrl: s.webhookUrl || '' };
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,6 @@ export interface Settings {
|
|||||||
id: string;
|
id: string;
|
||||||
famId: string;
|
famId: string;
|
||||||
webhookUrl?: string;
|
webhookUrl?: string;
|
||||||
simulateEow?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Footer from '$lib/components/Footer.svelte';
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
import { homeIcon } from '$lib/components/icons';
|
import { homeIcon, checkCircleIcon, chevronRightIcon } from '$lib/components/icons';
|
||||||
import { readShortcut, shortcutTarget, type Shortcut } from '$lib/shortcut';
|
import { readShortcut, shortcutTarget, type Shortcut } from '$lib/shortcut';
|
||||||
|
|
||||||
let shortcut = $state<(Shortcut & { target: string }) | null>(null);
|
let shortcut = $state<(Shortcut & { target: string }) | null>(null);
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="return-text">
|
<span class="return-text">
|
||||||
<span class="return-fam">{shortcut.famName}</span>
|
<span class="return-fam">{shortcut.famName}</span>
|
||||||
<span class="return-sub">Jump back in →</span>
|
<span class="return-sub">Jump back in {@html chevronRightIcon}</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -45,9 +45,9 @@
|
|||||||
automatically — no spreadsheets, no nagging.
|
automatically — no spreadsheets, no nagging.
|
||||||
</p>
|
</p>
|
||||||
<ul class="hero-ticks">
|
<ul class="hero-ticks">
|
||||||
<li>Set up in under a minute</li>
|
<li><span class="tick">{@html checkCircleIcon}</span>Set up in under a minute</li>
|
||||||
<li>Kids join with a simple invite code</li>
|
<li><span class="tick">{@html checkCircleIcon}</span>Kids join with a simple invite code</li>
|
||||||
<li>Allowance lands on payday, automatically</li>
|
<li><span class="tick">{@html checkCircleIcon}</span>Allowance lands on payday, automatically</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<p class="card-sub">Free to get going. Takes about a minute.</p>
|
<p class="card-sub">Free to get going. Takes about a minute.</p>
|
||||||
<a class="submit" href="/signup">Create my family</a>
|
<a class="submit" href="/signup">Create my family</a>
|
||||||
<p class="card-alt">
|
<p class="card-alt">
|
||||||
<a href="/pricing">See pricing →</a>
|
<a href="/pricing">See pricing {@html chevronRightIcon}</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="card-alt">
|
<p class="card-alt">
|
||||||
Already have a family? <a href="/login">Log in</a>
|
Already have a family? <a href="/login">Log in</a>
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
<div class="cta">
|
<div class="cta">
|
||||||
<p>Ready to make chores painless?</p>
|
<p>Ready to make chores painless?</p>
|
||||||
<a class="cta-btn" href="/signup">Create your family</a>
|
<a class="cta-btn" href="/signup">Create your family</a>
|
||||||
<p class="card-alt"><a href="/pricing">or see pricing →</a></p>
|
<p class="card-alt"><a href="/pricing">or see pricing {@html chevronRightIcon}</a></p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -230,10 +230,20 @@
|
|||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
}
|
}
|
||||||
.hero-ticks li::before {
|
.hero-ticks li::before {
|
||||||
content: "✓";
|
content: "";
|
||||||
color: #4338ca;
|
}
|
||||||
font-weight: 700;
|
.hero-ticks .tick {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.15rem;
|
||||||
|
height: 1.15rem;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
|
color: #4338ca;
|
||||||
|
}
|
||||||
|
.hero-ticks .tick :global(svg) {
|
||||||
|
width: 1.1em;
|
||||||
|
height: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SIGNUP CARD */
|
/* SIGNUP CARD */
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export async function load(event) {
|
|||||||
s.crud.list('rewards', famId),
|
s.crud.list('rewards', famId),
|
||||||
s.crud.list('bonus-configs', famId),
|
s.crud.list('bonus-configs', famId),
|
||||||
s.crud.list('completions', famId),
|
s.crud.list('completions', famId),
|
||||||
s.settings.get(famId).catch(() => ({ simulateEow: false, webhookUrl: '' }))
|
s.settings.get(famId)
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
role: 'parent',
|
role: 'parent',
|
||||||
@@ -105,8 +105,7 @@ export async function load(event) {
|
|||||||
tallies: {} as Record<string, unknown>,
|
tallies: {} as Record<string, unknown>,
|
||||||
payday: chores.payday,
|
payday: chores.payday,
|
||||||
paydayTime: chores.paydayTime || '18:00',
|
paydayTime: chores.paydayTime || '18:00',
|
||||||
timezone: chores.timezone || 'auto',
|
timezone: chores.timezone || 'auto'
|
||||||
simulateEow: !!chores.simulateEow
|
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return empty;
|
return empty;
|
||||||
@@ -114,33 +113,6 @@ export async function load(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
setEow: async (event) => {
|
|
||||||
if (!event.locals.user) throw redirect(303, '/login');
|
|
||||||
const famId = event.locals.user.famId;
|
|
||||||
const fd = await event.request.formData();
|
|
||||||
const on = fd.get('on') === 'true';
|
|
||||||
try {
|
|
||||||
const s = createServices(pbUser(event), event.locals.user);
|
|
||||||
const result = await s.settings.update(famId, { simulateEow: on });
|
|
||||||
return { simulateEow: result.simulateEow };
|
|
||||||
} catch (e) {
|
|
||||||
return { error: e instanceof Error ? e.message : 'Failed to update settings' };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
previewEow: async (event) => {
|
|
||||||
if (!event.locals.user) throw redirect(303, '/login');
|
|
||||||
const famId = event.locals.user.famId;
|
|
||||||
try {
|
|
||||||
const s = createServices(pbUser(event), event.locals.user);
|
|
||||||
const preview = await s.fam.eowPreview(famId);
|
|
||||||
await s.settings.update(famId, { simulateEow: true });
|
|
||||||
return { preview, simulateEow: true };
|
|
||||||
} catch (e) {
|
|
||||||
return { error: e instanceof Error ? e.message : 'Failed to preview payday' };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
claim: async (event) => {
|
claim: async (event) => {
|
||||||
if (!event.locals.user) throw redirect(303, '/login');
|
if (!event.locals.user) throw redirect(303, '/login');
|
||||||
const famId = event.locals.user.famId;
|
const famId = event.locals.user.famId;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
|||||||
import { famStore } from '$lib/stores/fam.svelte';
|
import { famStore } from '$lib/stores/fam.svelte';
|
||||||
import { ViewHeader, CardGrid, Card } from '$lib/components';
|
import { ViewHeader, CardGrid, Card } from '$lib/components';
|
||||||
import TemplateIcon from '$lib/components/TemplateIcon.svelte';
|
import TemplateIcon from '$lib/components/TemplateIcon.svelte';
|
||||||
import { choresIcon, todoIcon } from '$lib/components/icons';
|
import { choresIcon, todoIcon, checkCircleIcon } from '$lib/components/icons';
|
||||||
import { formatHumanDate } from '$lib/format';
|
import { formatHumanDate } from '$lib/format';
|
||||||
import { TEMPLATE_COLORS, ICON_NAMES, accentBg, outlineColor } from '$lib/templateIcons';
|
import { TEMPLATE_COLORS, ICON_NAMES, accentBg, outlineColor } from '$lib/templateIcons';
|
||||||
import type { ChoreTemplate, AssignedChore, Member, Season, Completion } from '$lib/types';
|
import type { ChoreTemplate, AssignedChore, Member, Season, Completion } from '$lib/types';
|
||||||
@@ -48,8 +48,26 @@
|
|||||||
|
|
||||||
// Curated celebration emojis for todos
|
// Curated celebration emojis for todos
|
||||||
const EMOJI_CHOICES = [
|
const EMOJI_CHOICES = [
|
||||||
'🎉', '⭐', '🏆', '💪', '🧹', '📚', '🦸', '✨', '🌟', '🥇',
|
'🎉',
|
||||||
'🎯', '💚', '🚀', '🦄', '🌈', '🍀', '🎨', '🎸', '⚽', '🍰'
|
'⭐',
|
||||||
|
'🏆',
|
||||||
|
'💪',
|
||||||
|
'🧹',
|
||||||
|
'📚',
|
||||||
|
'🦸',
|
||||||
|
'✨',
|
||||||
|
'🌟',
|
||||||
|
'🥇',
|
||||||
|
'🎯',
|
||||||
|
'💚',
|
||||||
|
'🚀',
|
||||||
|
'🦄',
|
||||||
|
'🌈',
|
||||||
|
'🍀',
|
||||||
|
'🎨',
|
||||||
|
'🎸',
|
||||||
|
'⚽',
|
||||||
|
'🍰'
|
||||||
];
|
];
|
||||||
|
|
||||||
let editFreq = $state('daily');
|
let editFreq = $state('daily');
|
||||||
@@ -68,9 +86,11 @@
|
|||||||
|
|
||||||
// Platform-owned templates only — families assign these, they don't create.
|
// Platform-owned templates only — families assign these, they don't create.
|
||||||
let templates = $derived(
|
let templates = $derived(
|
||||||
((famStore.initialized ? (famStore.templates as ChoreTemplate[]) : (data.templates as ChoreTemplate[])) || []).filter(
|
(
|
||||||
(t: ChoreTemplate) => t.global
|
(famStore.initialized
|
||||||
)
|
? (famStore.templates as ChoreTemplate[])
|
||||||
|
: (data.templates as ChoreTemplate[])) || []
|
||||||
|
).filter((t: ChoreTemplate) => t.global)
|
||||||
);
|
);
|
||||||
let members = $state(
|
let members = $state(
|
||||||
famStore.initialized ? (famStore.members as Member[]) : (data.members as Member[]) || []
|
famStore.initialized ? (famStore.members as Member[]) : (data.members as Member[]) || []
|
||||||
@@ -85,7 +105,7 @@
|
|||||||
// Only active seasons are selectable for assignment / filtering.
|
// Only active seasons are selectable for assignment / filtering.
|
||||||
let activeSeasons = $derived((seasons || []).filter((s) => s.active !== false));
|
let activeSeasons = $derived((seasons || []).filter((s) => s.active !== false));
|
||||||
let completions = $state((data.completions as Completion[]) || []);
|
let completions = $state((data.completions as Completion[]) || []);
|
||||||
|
// $effect(() => console.log(seasons, activeSeasons));
|
||||||
let filteredAssigned = $derived(
|
let filteredAssigned = $derived(
|
||||||
seasonFilter === 'all' ? assigned : assigned.filter((a) => a.seasonIds?.includes(seasonFilter))
|
seasonFilter === 'all' ? assigned : assigned.filter((a) => a.seasonIds?.includes(seasonFilter))
|
||||||
);
|
);
|
||||||
@@ -366,7 +386,10 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- TODOS accordion wrapper -->
|
<!-- TODOS accordion wrapper -->
|
||||||
<div class="accordion-wrapper accordion-todos" class:open={accordionState[m.id]?.todos ?? true}>
|
<div
|
||||||
|
class="accordion-wrapper accordion-todos"
|
||||||
|
class:open={accordionState[m.id]?.todos ?? true}
|
||||||
|
>
|
||||||
<button class="accordion-head" onclick={() => toggleAccordion(m.id, 'todos')}>
|
<button class="accordion-head" onclick={() => toggleAccordion(m.id, 'todos')}>
|
||||||
<span class="accordion-title">
|
<span class="accordion-title">
|
||||||
<span class="accordion-icon">{@html todoIcon}</span>
|
<span class="accordion-icon">{@html todoIcon}</span>
|
||||||
@@ -376,7 +399,9 @@
|
|||||||
>{@html (accordionState[m.id]?.todos ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span
|
>{@html (accordionState[m.id]?.todos ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
<span class="accordion-count" style="background:{m.color}">{todosForMember(m.id).length}</span>
|
<span class="accordion-count" style="background:{m.color}"
|
||||||
|
>{todosForMember(m.id).length}</span
|
||||||
|
>
|
||||||
{#if accordionState[m.id]?.todos ?? true}
|
{#if accordionState[m.id]?.todos ?? true}
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
{#each sortedTodosForMember(m.id) as a}
|
{#each sortedTodosForMember(m.id) as a}
|
||||||
@@ -414,7 +439,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if completed}
|
{#if completed}
|
||||||
<span class="todo-completed-badge">✅ TBC completed</span>
|
<span class="todo-completed-badge">{@html checkCircleIcon} TBC completed</span
|
||||||
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
class="del-btn"
|
class="del-btn"
|
||||||
@@ -440,7 +466,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CHORES accordion wrapper -->
|
<!-- CHORES accordion wrapper -->
|
||||||
<div class="accordion-wrapper accordion-chores" class:open={accordionState[m.id]?.chores ?? true}>
|
<div
|
||||||
|
class="accordion-wrapper accordion-chores"
|
||||||
|
class:open={accordionState[m.id]?.chores ?? true}
|
||||||
|
>
|
||||||
<button class="accordion-head" onclick={() => toggleAccordion(m.id, 'chores')}>
|
<button class="accordion-head" onclick={() => toggleAccordion(m.id, 'chores')}>
|
||||||
<span class="accordion-title">
|
<span class="accordion-title">
|
||||||
<span class="accordion-icon">{@html choresIcon}</span>
|
<span class="accordion-icon">{@html choresIcon}</span>
|
||||||
@@ -450,7 +479,9 @@
|
|||||||
>{@html (accordionState[m.id]?.chores ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span
|
>{@html (accordionState[m.id]?.chores ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
<span class="accordion-count" style="background:{m.color}">{assignedForMember(m.id).length}</span>
|
<span class="accordion-count" style="background:{m.color}"
|
||||||
|
>{assignedForMember(m.id).length}</span
|
||||||
|
>
|
||||||
{#if accordionState[m.id]?.chores ?? true}
|
{#if accordionState[m.id]?.chores ?? true}
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
{#each assignedForMember(m.id) as a}
|
{#each assignedForMember(m.id) as a}
|
||||||
@@ -460,14 +491,20 @@
|
|||||||
onclick={() => openEdit(a)}
|
onclick={() => openEdit(a)}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
style="border-left-color:{outlineColor(a.color)}; background:{accentBg(a.color)}"
|
style="border-left-color:{outlineColor(a.color)}; background:{accentBg(
|
||||||
|
a.color
|
||||||
|
)}"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<TemplateIcon name={a.icon} size={16} color={outlineColor(a.color)} />
|
<TemplateIcon name={a.icon} size={16} color={outlineColor(a.color)} />
|
||||||
<strong>{a.customName || tName}</strong>
|
<strong>{a.customName || tName}</strong>
|
||||||
<span class="badge">{a.frequency}</span>
|
<span class="badge">{a.frequency}</span>
|
||||||
{#each a.seasonIds || [] as sid}
|
{#each a.seasonIds || [] as sid}
|
||||||
<span class="season-dot" title={seasonName(sid)} style="background:{seasonColor(sid)}"></span>
|
<span
|
||||||
|
class="season-dot"
|
||||||
|
title={seasonName(sid)}
|
||||||
|
style="background:{seasonColor(sid)}"
|
||||||
|
></span>
|
||||||
{/each}
|
{/each}
|
||||||
{#if (a.seasonIds || []).length === 0}
|
{#if (a.seasonIds || []).length === 0}
|
||||||
<span class="badge season muted">All year</span>
|
<span class="badge season muted">All year</span>
|
||||||
@@ -509,14 +546,16 @@
|
|||||||
</CardGrid>
|
</CardGrid>
|
||||||
|
|
||||||
<!-- Season pill selector (reused by assign + edit modals) -->
|
<!-- Season pill selector (reused by assign + edit modals) -->
|
||||||
{#snippet seasonPills(list: Season[], selected: string[], set: (v: string[]) => void)}
|
{#snippet seasonPills(list: Season[] = [], selected: string[] = [], set: (v: string[]) => void)}
|
||||||
<div class="season-pills">
|
<div class="season-pills" role="radiogroup" aria-label="Seasons">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="season-pill"
|
class="season-pill"
|
||||||
class:selected={selected.length === 0}
|
class:selected={selected.length === 0}
|
||||||
style="--pill:#9ca3af"
|
style="--pill:#9ca3af"
|
||||||
onclick={() => set([])}>All year</button
|
role="radio"
|
||||||
|
aria-checked={selected.length === 0}
|
||||||
|
onclick={() => set([])}>Any</button
|
||||||
>
|
>
|
||||||
{#each list as s}
|
{#each list as s}
|
||||||
<button
|
<button
|
||||||
@@ -524,6 +563,8 @@
|
|||||||
class="season-pill"
|
class="season-pill"
|
||||||
class:selected={selected[0] === s.id}
|
class:selected={selected[0] === s.id}
|
||||||
style="--pill:{s.color}"
|
style="--pill:{s.color}"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={selected[0] === s.id}
|
||||||
onclick={() => set([s.id])}>{s.name}</button
|
onclick={() => set([s.id])}>{s.name}</button
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -532,16 +573,25 @@
|
|||||||
|
|
||||||
<!-- Assign from Template Modal -->
|
<!-- Assign from Template Modal -->
|
||||||
{#if assignModal}
|
{#if assignModal}
|
||||||
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) assignModal = null; }} role="presentation">
|
<div
|
||||||
|
class="overlay"
|
||||||
|
onclick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) assignModal = null;
|
||||||
|
}}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
|
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
|
||||||
<h3>Assign: {assignModal.t.name}</h3>
|
<h3>Assign: {assignModal.t.name}</h3>
|
||||||
<p class="hint">
|
<p class="hint">
|
||||||
Copies this platform template into {members.find((m) => m.id === assignModal?.memberId)?.name}'s
|
Copies this platform template into {members.find((m) => m.id === assignModal?.memberId)
|
||||||
chores. Amend, then Assign.
|
?.name}'s chores. Amend, then Assign.
|
||||||
</p>
|
</p>
|
||||||
<label>Name <input bind:value={assignName} placeholder="Task name" /></label>
|
<label>Name <input bind:value={assignName} placeholder="Task name" /></label>
|
||||||
<label
|
<label
|
||||||
>Description <input bind:value={assignDescription} placeholder="Optional description" /></label
|
>Description <input
|
||||||
|
bind:value={assignDescription}
|
||||||
|
placeholder="Optional description"
|
||||||
|
/></label
|
||||||
>
|
>
|
||||||
<div class="color-field">
|
<div class="color-field">
|
||||||
<span class="field-label">Colour</span>
|
<span class="field-label">Colour</span>
|
||||||
@@ -556,7 +606,8 @@
|
|||||||
? '#e5e7eb'
|
? '#e5e7eb'
|
||||||
: c.value}"
|
: c.value}"
|
||||||
title={c.label}
|
title={c.label}
|
||||||
onclick={() => (assignColor = c.value)}></button>
|
onclick={() => (assignColor = c.value)}
|
||||||
|
></button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -589,7 +640,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="season-field">
|
<div class="season-field">
|
||||||
<span class="field-label">Seasons</span>
|
<span class="field-label">Seasons</span>
|
||||||
<seasonPills activeSeasons selected={assignSeasonIds} set={(v: string[]) => (assignSeasonIds = v)} />
|
{@render seasonPills(seasons, assignSeasonIds, (v: string[]) => (assignSeasonIds = v))}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button type="button" onclick={() => (assignModal = null)}>Cancel</button>
|
<button type="button" onclick={() => (assignModal = null)}>Cancel</button>
|
||||||
@@ -601,7 +652,13 @@
|
|||||||
|
|
||||||
<!-- Edit Assignment Modal -->
|
<!-- Edit Assignment Modal -->
|
||||||
{#if showEditModal && editingAssignment}
|
{#if showEditModal && editingAssignment}
|
||||||
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) closeEdit(); }} role="presentation">
|
<div
|
||||||
|
class="overlay"
|
||||||
|
onclick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) closeEdit();
|
||||||
|
}}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
|
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
|
||||||
<h3>
|
<h3>
|
||||||
{#if editingAssignment.isTodo}
|
{#if editingAssignment.isTodo}
|
||||||
@@ -619,7 +676,11 @@
|
|||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
>Description
|
>Description
|
||||||
<input name="description" bind:value={editDescription} placeholder="Optional description" /></label
|
<input
|
||||||
|
name="description"
|
||||||
|
bind:value={editDescription}
|
||||||
|
placeholder="Optional description"
|
||||||
|
/></label
|
||||||
>
|
>
|
||||||
|
|
||||||
{#if editingAssignment.isTodo}
|
{#if editingAssignment.isTodo}
|
||||||
@@ -639,7 +700,14 @@
|
|||||||
{:else if editType === 'money'}
|
{:else if editType === 'money'}
|
||||||
<label>
|
<label>
|
||||||
Cash amount (£)
|
Cash amount (£)
|
||||||
<input name="value" type="number" step="0.01" min="0.01" bind:value={editValue} required />
|
<input
|
||||||
|
name="value"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0.01"
|
||||||
|
bind:value={editValue}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
{:else}
|
{:else}
|
||||||
<input type="hidden" name="emoji" value={editEmoji} />
|
<input type="hidden" name="emoji" value={editEmoji} />
|
||||||
@@ -648,7 +716,8 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="link-btn"
|
class="link-btn"
|
||||||
onclick={() => (showTodoEmojiPicker = !showTodoEmojiPicker)}>
|
onclick={() => (showTodoEmojiPicker = !showTodoEmojiPicker)}
|
||||||
|
>
|
||||||
{showTodoEmojiPicker ? 'Hide' : 'Choose'}
|
{showTodoEmojiPicker ? 'Hide' : 'Choose'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -691,20 +760,21 @@
|
|||||||
? '#e5e7eb'
|
? '#e5e7eb'
|
||||||
: c.value}"
|
: c.value}"
|
||||||
title={c.label}
|
title={c.label}
|
||||||
onclick={() => (editColor = c.value)}></button>
|
onclick={() => (editColor = c.value)}
|
||||||
|
></button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="color" value={editColor} />
|
<input type="hidden" name="color" value={editColor} />
|
||||||
</div>
|
</div>
|
||||||
<label
|
<label class="icon-row"
|
||||||
><span class="icon-preview"
|
><span class="icon-preview"
|
||||||
><TemplateIcon name={editIcon} size={22} color={outlineColor(editColor)} /></span
|
><TemplateIcon name={editIcon} size={22} color={outlineColor(editColor)} /></span
|
||||||
>
|
>
|
||||||
<select name="icon" bind:value={editIcon}>
|
<select name="icon" bind:value={editIcon}>
|
||||||
<option value="">None</option>
|
<option value="">None</option>
|
||||||
{#each ICON_NAMES as n}<option value={n}>{n}</option>{/each}
|
{#each ICON_NAMES as n}<option value={n}>{n}</option>{/each}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Frequency
|
Frequency
|
||||||
<select name="frequency" bind:value={editFreq}>
|
<select name="frequency" bind:value={editFreq}>
|
||||||
@@ -725,7 +795,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="season-field">
|
<div class="season-field">
|
||||||
<span class="field-label">Seasons</span>
|
<span class="field-label">Seasons</span>
|
||||||
<seasonPills activeSeasons selected={editSeasonIds} set={(v: string[]) => (editSeasonIds = v)} />
|
{@render seasonPills(seasons, editSeasonIds, (v: string[]) => (editSeasonIds = v))}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
@@ -739,7 +809,13 @@
|
|||||||
|
|
||||||
<!-- Add Todo Modal -->
|
<!-- Add Todo Modal -->
|
||||||
{#if showTodoModal && todoMemberId}
|
{#if showTodoModal && todoMemberId}
|
||||||
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) showTodoModal = false; }} role="presentation">
|
<div
|
||||||
|
class="overlay"
|
||||||
|
onclick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) showTodoModal = false;
|
||||||
|
}}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
|
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
|
||||||
<h3>Add Todo for {members.find((m) => m.id === todoMemberId)?.name || 'Member'}</h3>
|
<h3>Add Todo for {members.find((m) => m.id === todoMemberId)?.name || 'Member'}</h3>
|
||||||
<form
|
<form
|
||||||
@@ -785,13 +861,24 @@
|
|||||||
{:else if todoType === 'money'}
|
{:else if todoType === 'money'}
|
||||||
<label>
|
<label>
|
||||||
Cash amount (£)
|
Cash amount (£)
|
||||||
<input name="value" type="number" step="0.01" min="0.01" bind:value={todoValue} required />
|
<input
|
||||||
|
name="value"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0.01"
|
||||||
|
bind:value={todoValue}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
{:else}
|
{:else}
|
||||||
<input type="hidden" name="emoji" value={todoEmoji} />
|
<input type="hidden" name="emoji" value={todoEmoji} />
|
||||||
<div class="emoji-field">
|
<div class="emoji-field">
|
||||||
<span class="emoji-preview">{todoEmoji}</span>
|
<span class="emoji-preview">{todoEmoji}</span>
|
||||||
<button type="button" class="link-btn" onclick={() => (showTodoEmojiPicker = !showTodoEmojiPicker)}>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="link-btn"
|
||||||
|
onclick={() => (showTodoEmojiPicker = !showTodoEmojiPicker)}
|
||||||
|
>
|
||||||
{showTodoEmojiPicker ? 'Hide' : 'Choose'}
|
{showTodoEmojiPicker ? 'Hide' : 'Choose'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1464,6 +1551,13 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
.todo-completed-badge :global(svg) {
|
||||||
|
width: 0.95em;
|
||||||
|
height: 0.95em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Points badge ── */
|
/* ── Points badge ── */
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { Button } from '$lib/components';
|
import { Button } from '$lib/components';
|
||||||
|
import { homeIcon } from '$lib/components/icons';
|
||||||
|
|
||||||
const famSlug = page.params.fam;
|
const famSlug = page.params.fam;
|
||||||
let name = $state('');
|
let name = $state('');
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
<main class="mx-auto flex min-h-screen max-w-md flex-col items-center justify-center px-6">
|
<main class="mx-auto flex min-h-screen max-w-md flex-col items-center justify-center px-6">
|
||||||
<section class="w-full rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
|
<section class="w-full rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
|
||||||
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-indigo-100 text-2xl">
|
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-indigo-100 text-2xl">
|
||||||
🏠
|
<span class="home-badge">{@html homeIcon}</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-xl font-bold text-slate-900">Join {famSlug}</h1>
|
<h1 class="text-xl font-bold text-slate-900">Join {famSlug}</h1>
|
||||||
<p class="mt-1 text-sm text-slate-500">
|
<p class="mt-1 text-sm text-slate-500">
|
||||||
@@ -63,3 +64,10 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.home-badge :global(svg) {
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { Button } from '$lib/components';
|
import { Button } from '$lib/components';
|
||||||
|
import { homeIcon } from '$lib/components/icons';
|
||||||
|
|
||||||
const famSlug = page.params.fam;
|
const famSlug = page.params.fam;
|
||||||
const username = page.params.username;
|
const username = page.params.username;
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
<main class="mx-auto flex min-h-screen max-w-md flex-col items-center justify-center px-6">
|
<main class="mx-auto flex min-h-screen max-w-md flex-col items-center justify-center px-6">
|
||||||
<section class="w-full rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
|
<section class="w-full rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
|
||||||
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-indigo-100 text-2xl">
|
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-indigo-100 text-2xl">
|
||||||
🏠
|
<span class="home-badge">{@html homeIcon}</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-xl font-bold text-slate-900">Welcome to {famSlug}!</h1>
|
<h1 class="text-xl font-bold text-slate-900">Welcome to {famSlug}!</h1>
|
||||||
<p class="mt-1 text-sm text-slate-500">
|
<p class="mt-1 text-sm text-slate-500">
|
||||||
@@ -54,4 +55,10 @@
|
|||||||
Code is valid for 20 minutes. Ask your parent for a new one if it expires.
|
Code is valid for 20 minutes. Ask your parent for a new one if it expires.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
<style>
|
||||||
|
.home-badge :global(svg) {
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import TemplateIcon from '$lib/components/TemplateIcon.svelte';
|
import TemplateIcon from '$lib/components/TemplateIcon.svelte';
|
||||||
import { ICON_NAMES, TEMPLATE_COLORS } from '$lib/templateIcons';
|
import { ICON_NAMES, TEMPLATE_COLORS } from '$lib/templateIcons';
|
||||||
import Footer from '$lib/components/Footer.svelte';
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
|
import { homeIcon, chevronRightIcon } from '$lib/components/icons';
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data, form } = $props();
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@
|
|||||||
|
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<a class="brand" href="/"><span class="brand-icon">🏠</span> <strong>FamChore</strong></a>
|
<a class="brand" href="/"><span class="brand-icon">{@html homeIcon}</span> <strong>FamChore</strong></a>
|
||||||
<nav>
|
<nav>
|
||||||
{#if data.authenticated}
|
{#if data.authenticated}
|
||||||
<form method="POST" action="?/logout" use:enhance>
|
<form method="POST" action="?/logout" use:enhance>
|
||||||
@@ -311,7 +312,7 @@
|
|||||||
<span class="badge none">None</span>
|
<span class="badge none">None</span>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="row-actions" data-label="Actions"><a href="/{fam.slug}" class="link">View →</a></td>
|
<td class="row-actions" data-label="Actions"><a href="/{fam.slug}" class="link">View {@html chevronRightIcon}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { loadStripe, type StripeEmbeddedCheckout } from '@stripe/stripe-js';
|
import { loadStripe, type StripeEmbeddedCheckout } from '@stripe/stripe-js';
|
||||||
import { Button, PricingPlans } from '$lib/components';
|
import { Button, PricingPlans } from '$lib/components';
|
||||||
import Footer from '$lib/components/Footer.svelte';
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
|
import { homeIcon, chevronLeftIcon, chevronRightIcon } from '$lib/components/icons';
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data, form } = $props();
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@
|
|||||||
|
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<a class="brand" href="/"><span class="brand-icon">🏠</span> <strong>FamChore</strong></a>
|
<a class="brand" href="/"><span class="brand-icon">{@html homeIcon}</span> <strong>FamChore</strong></a>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/login" class="nav-link">Log in</a>
|
<a href="/login" class="nav-link">Log in</a>
|
||||||
<a href="/signup" class="nav-cta">Get started</a>
|
<a href="/signup" class="nav-cta">Get started</a>
|
||||||
@@ -77,13 +78,13 @@
|
|||||||
<div class="checkout-card">
|
<div class="checkout-card">
|
||||||
<div class="checkout-head">
|
<div class="checkout-head">
|
||||||
<h2>Checkout — {checkoutTitle}</h2>
|
<h2>Checkout — {checkoutTitle}</h2>
|
||||||
<Button variant="ghost" size="sm" onclick={resetCheckout}>← Back to plans</Button>
|
<Button variant="ghost" size="sm" onclick={resetCheckout}>{@html chevronLeftIcon} Back to plans</Button>
|
||||||
</div>
|
</div>
|
||||||
<div bind:this={checkoutEl} class="checkout-host"></div>
|
<div bind:this={checkoutEl} class="checkout-host"></div>
|
||||||
<p class="hint">
|
<p class="hint">
|
||||||
You can close and go to your dashboard any time — access unlocks once payment completes.
|
You can close and go to your dashboard any time — access unlocks once payment completes.
|
||||||
</p>
|
</p>
|
||||||
<a href={data.famSlug ? `/${data.famSlug}` : '/'} class="dash-link">Go to dashboard →</a>
|
<a href={data.famSlug ? `/${data.famSlug}` : '/'} class="dash-link">Go to dashboard {@html chevronRightIcon}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
+1
-1
@@ -184,7 +184,7 @@ export const SCHEMA_PLAN: CollectionPlanEntry[] = [
|
|||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
build: (ids) =>
|
build: (ids) =>
|
||||||
col("settings", [rel("famId", ids.fams, true), text("webhookUrl"), bool("simulateEow")], {
|
col("settings", [rel("famId", ids.fams, true), text("webhookUrl")], {
|
||||||
createRule: RULE_PARENT_WRITE,
|
createRule: RULE_PARENT_WRITE,
|
||||||
updateRule: RULE_PARENT_SCOPED,
|
updateRule: RULE_PARENT_SCOPED,
|
||||||
deleteRule: RULE_PARENT_SCOPED,
|
deleteRule: RULE_PARENT_SCOPED,
|
||||||
|
|||||||
Reference in New Issue
Block a user