add fixes to claims and seasons

This commit is contained in:
JCEEE
2026-07-30 11:12:35 +01:00
parent 57ef30d3a7
commit b254564436
24 changed files with 2517 additions and 704 deletions
+77 -19
View File
@@ -1,23 +1,81 @@
import { pbAdmin } from '$lib/server/pb-admin';
import { redirect, fail } from '@sveltejs/kit';
import { PB_EMAIL, PB_PASSWORD } from '../../../../config.ts';
import type { Actions, PageServerLoad } from './$types';
export async function load() {
const fams = await pbAdmin.getList('fams');
const famsWithStats = await Promise.all(fams.map(async (fam: any) => {
const [members, rewards] = await Promise.all([
pbAdmin.getList('members', `famId = '${fam.id}'`),
pbAdmin.getList('rewards', `famId = '${fam.id}'`),
]);
return {
id: fam.id, name: fam.name, slug: fam.slug, inviteCode: fam.inviteCode,
memberCount: members.length,
claimedRewards: (rewards as any[]).filter((r: any) => r.claimed).length,
totalRewards: rewards.length,
};
}));
export const load: PageServerLoad = async ({ cookies }) => {
const session = cookies.get('platform_session');
if (!session) {
return { authenticated: false, fams: [], totalFams: 0, totalMembers: 0, totalRewards: 0 };
}
const totalFams = fams.length;
const totalMembers = famsWithStats.reduce((s: number, f: any) => s + f.memberCount, 0);
const totalRewards = famsWithStats.reduce((s: number, f: any) => s + f.totalRewards, 0);
try {
const fams = await pbAdmin.getList('fams');
const famsWithStats = await Promise.all(fams.map(async (fam: any) => {
const [members, rewards, famAdmins] = await Promise.all([
pbAdmin.getList('members', `famId = '${fam.id}'`),
pbAdmin.getList('rewards', `famId = '${fam.id}'`),
pbAdmin.getList('fam_admins', `famId = '${fam.id}'`),
]);
return {
id: fam.id, name: fam.name, slug: fam.slug, inviteCode: fam.inviteCode,
memberCount: members.length,
requestedRewards: (rewards as any[]).filter((r: any) => r.status === 'requested').length,
totalRewards: rewards.length,
parentEmail: (famAdmins as any[])?.[0]?.email || '',
featureFlags: fam.featureFlags || {},
};
}));
return { fams: famsWithStats, totalFams, totalMembers, totalRewards };
}
const totalFams = fams.length;
const totalMembers = famsWithStats.reduce((s: number, f: any) => s + f.memberCount, 0);
const totalRewards = famsWithStats.reduce((s: number, f: any) => s + f.totalRewards, 0);
return { authenticated: true, fams: famsWithStats, totalFams, totalMembers, totalRewards };
} catch {
return { authenticated: false, fams: [], totalFams: 0, totalMembers: 0, totalRewards: 0 };
}
};
export const actions: Actions = {
login: async ({ request, cookies }) => {
const fd = await request.formData();
const email = fd.get('email') as string;
const password = fd.get('password') as string;
if (email === PB_EMAIL && password === PB_PASSWORD) {
cookies.set('platform_session', 'authenticated', {
path: '/',
httpOnly: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24, // 24 hours
});
return { success: true };
}
return fail(400, { error: 'Invalid credentials' });
},
logout: async ({ cookies }) => {
cookies.delete('platform_session', { path: '/' });
throw redirect(303, '/admin');
},
toggleFeatureFlag: async ({ request, cookies }) => {
const session = cookies.get('platform_session');
if (!session) return fail(401, { error: 'Not authenticated' });
const fd = await request.formData();
const famId = fd.get('famId') as string;
const flag = fd.get('flag') as string;
try {
const fam = await pbAdmin.getOne('fams', famId);
const flags = fam.featureFlags || {};
flags[flag] = !flags[flag];
await pbAdmin.update('fams', famId, { featureFlags: flags });
return { success: true };
} catch (e) {
return fail(500, { error: e instanceof Error ? e.message : 'Failed to update' });
}
},
};
+218 -29
View File
@@ -1,37 +1,226 @@
<script lang="ts">
let { data } = $props();
import { enhance } from '$app/forms';
import { ViewHeader, CardGrid, Card, Button } from '$lib/components';
let { data, form } = $props();
</script>
<h1>Super Admin</h1>
{#if !data.authenticated}
<div class="login-container">
<div class="login-card">
<h1>Platform Admin</h1>
<p class="subtitle">Sign in to access the admin dashboard</p>
<div class="stats">
<div class="card"><strong>{data.totalFams}</strong> Families</div>
<div class="card"><strong>{data.totalMembers}</strong> Members</div>
<div class="card"><strong>{data.totalRewards}</strong> Rewards</div>
</div>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<h2>Families</h2>
<table>
<thead><tr><th>Name</th><th>Slug</th><th>Members</th><th>Rewards</th><th>Invite Code</th></tr></thead>
<tbody>
{#each data.fams as fam}
<tr>
<td><a href="/{fam.slug}/admin">{fam.name}</a></td>
<td>{fam.slug}</td>
<td>{fam.memberCount}</td>
<td>{fam.claimedRewards}/{fam.totalRewards}</td>
<td><code>{fam.inviteCode}</code></td>
</tr>
{/each}
</tbody>
</table>
<form method="POST" action="?/login" use:enhance>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required />
</div>
<Button type="submit" variant="primary" size="lg">Sign In</Button>
</form>
</div>
</div>
{:else}
<ViewHeader title="Platform Admin" subtitle="Manage families and platform settings" />
<CardGrid>
<Card cols={1} title="Overview" accent="#6366f1">
<div class="stats">
<div class="stat">
<span class="stat-value">{data.totalFams}</span>
<span class="stat-label">Families</span>
</div>
<div class="stat">
<span class="stat-value">{data.totalMembers}</span>
<span class="stat-label">Members</span>
</div>
<div class="stat">
<span class="stat-value">{data.totalRewards}</span>
<span class="stat-label">Rewards</span>
</div>
</div>
</Card>
<Card cols={2} title="Families" accent="#059669">
<table class="fam-table">
<thead>
<tr>
<th>Name</th>
<th>Parent</th>
<th>Members</th>
<th>Claims</th>
<th>Debug</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each data.fams as fam}
<tr>
<td>
<a href="/{fam.slug}/admin">{fam.name}</a>
<span class="slug">/{fam.slug}</span>
</td>
<td>{fam.parentEmail || '—'}</td>
<td>{fam.memberCount}</td>
<td>
{#if fam.requestedRewards > 0}
<span class="badge pending">{fam.requestedRewards} pending</span>
{:else}
<span class="badge none">None</span>
{/if}
</td>
<td>
<form method="POST" action="?/toggleFeatureFlag" use:enhance>
<input type="hidden" name="famId" value={fam.id} />
<input type="hidden" name="flag" value="debugMode" />
<button
type="submit"
class="toggle-btn"
class:active={fam.featureFlags?.debugMode}
>
{fam.featureFlags?.debugMode ? 'ON' : 'OFF'}
</button>
</form>
</td>
<td>
<a href="/{fam.slug}/admin" class="link">Dashboard</a>
<a href="/{fam.slug}" class="link">View</a>
</td>
</tr>
{/each}
</tbody>
</table>
</Card>
</CardGrid>
<form method="POST" action="?/logout" class="logout-form">
<Button type="submit" variant="ghost" size="sm">Sign Out</Button>
</form>
{/if}
<style>
.stats { display: flex; gap: 1rem; margin: 1rem 0; }
.card { padding: 1rem; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; flex: 1; text-align: center; }
.card strong { font-size: 1.5rem; display: block; }
table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
th, td { padding: 0.5rem; border: 1px solid #e5e7eb; text-align: left; }
th { background: #f9fafb; font-weight: 600; }
code { background: #eee; padding: 0.1rem 0.3rem; border-radius: 3px; font-size: 0.85em; }
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #f3f4f6;
}
.login-card {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
.login-card h1 {
margin: 0 0 0.5rem;
font-size: 1.5rem;
}
.subtitle {
color: #6b7280;
margin: 0 0 1.5rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
font-size: 0.9rem;
}
.form-group input {
width: 100%;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.9rem;
}
.error {
color: #dc2626;
background: #fef2f2;
padding: 0.5rem;
border-radius: 6px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.stats {
display: flex;
gap: 2rem;
}
.stat {
text-align: center;
}
.stat-value {
display: block;
font-size: 2rem;
font-weight: 700;
color: #1f2937;
}
.stat-label {
font-size: 0.85rem;
color: #6b7280;
}
.fam-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.fam-table th, .fam-table td {
padding: 0.5rem;
border-bottom: 1px solid #e5e7eb;
text-align: left;
}
.fam-table th {
font-weight: 600;
color: #374151;
}
.slug {
color: #9ca3af;
font-size: 0.8rem;
margin-left: 0.25rem;
}
.badge {
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 10px;
font-weight: 500;
}
.badge.pending { background: #fef3c7; color: #92400e; }
.badge.none { background: #f3f4f6; color: #6b7280; }
.toggle-btn {
background: #e5e7eb;
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.75rem;
font-weight: 600;
}
.toggle-btn.active {
background: #059669;
color: white;
}
.link {
color: #2563eb;
text-decoration: none;
margin-right: 0.5rem;
font-size: 0.85rem;
}
.link:hover { text-decoration: underline; }
.logout-form {
position: fixed;
top: 1rem;
right: 1rem;
}
</style>