Rebuild admin pages around DataTable/AdminModal; restructure categories/groups and v2 groups API

Remove the generic $lib/svelte-admin CRUD layer and AdminTable.svelte; each
admin route now owns its own form/modal logic. Add DataTable.svelte,
superform-errors.ts, and profile-specific modal/field components.

Flatten proclamation categories/groups and drop subscription levels; add the
v2 groups API; update v1/v2 endpoints, serializers, auth, and regenerated
Prisma client with the new migrations.

Document the overhaul plan in docs/ADMIN_OVERHAUL.md.
This commit is contained in:
NeilClack
2026-09-10 09:38:01 -05:00
parent f4e736c92d
commit 91d162c746
127 changed files with 7182 additions and 9785 deletions
@@ -1,78 +1,87 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, deleteRecord, superFormErrorsToFieldErrors } from '$lib/svelte-admin/crud';
import { prisma } from '$lib/db';
import { superValidate } from 'sveltekit-superforms/server';
import { zod4 } from 'sveltekit-superforms/adapters';
import * as Schemas from '$lib/svelte-admin/schemas';
import { z } from 'zod';
import { superFormErrorsToFieldErrors } from '$lib/superform-errors';
const config = getModelConfig('proclamations')!;
function viewerRoleId(locals: App.Locals): number {
return (locals.user as { roleId?: number } | null)?.roleId ?? 4;
}
const ProclamationSchema = z.object({
name: z.string().min(1, 'Required'),
solfeggioFrequencyId: z.coerce.number().int(),
categoryId: z.coerce.number().int(),
groupId: z.coerce.number().int(),
content: z.string().optional().default('')
});
function isSuper(locals: App.Locals): boolean {
return viewerRoleId(locals) <= 1;
return ((locals.user as { roleId?: number } | null)?.roleId ?? 4) <= 1;
}
export const load: PageServerLoad = async ({ locals }) => {
const base = await loadRecords(locals, config);
const [proclamations, frequencies, categories, groups] = await Promise.all([
prisma.proclamation.findMany({
include: { category: true, group: true, solfeggioFrequency: true },
orderBy: { name: 'asc' }
}),
prisma.solfeggioFrequency.findMany({
select: { id: true, name: true, value: true },
orderBy: { name: 'asc' }
}),
prisma.proclamationCategory.findMany({
select: { id: true, name: true },
orderBy: { name: 'asc' }
}),
prisma.proclamationGroup.findMany({
select: { id: true, name: true },
orderBy: { sortOrder: 'asc' }
})
]);
const records = proclamations as unknown as Record<string, unknown>[];
// Strip content from non-Super users
if (!isSuper(locals)) {
for (const record of base.records) {
for (const record of records) {
delete record.content;
}
}
const categories = await prisma.proclamationCategory.findMany({
select: { id: true, name: true, parentId: true },
orderBy: { name: 'asc' }
});
return { ...base, categories };
return { records, frequencies, categories, groups };
};
export const actions = {
upsert: async ({ request, locals }) => {
if (!locals.user) {
return { success: false, error: 'Unauthorized' };
return { success: false, action: 'upsert', error: 'Unauthorized' };
}
if (!isSuper(locals)) {
return { success: false, error: 'Only Super admins can edit proclamations' };
return {
success: false,
action: 'upsert',
error: 'Only Super admins can edit proclamations'
};
}
const formData = await request.formData();
const form = await superValidate(formData, zod4(Schemas.Proclamation));
const form = await superValidate(formData, zod4(ProclamationSchema));
if (!form.valid) {
const { error, fieldErrors } = superFormErrorsToFieldErrors(
const { error: formError, fieldErrors } = superFormErrorsToFieldErrors(
form.errors as Record<string, unknown>
);
return { success: false, error, fieldErrors };
return { success: false, action: 'upsert', error: formError, fieldErrors };
}
const rawId = formData.get('id')?.toString();
const { parentCategoryId, ...rest } = form.data;
const data: Record<string, unknown> = {
...rest,
categoryId: rest.categoryId ?? parentCategoryId
};
const data: Record<string, unknown> = { ...form.data };
try {
if (rawId) {
const existing = await prisma.proclamation.findUnique({
where: { id: rawId }
});
const existing = await prisma.proclamation.findUnique({ where: { id: rawId } });
if (!existing) {
return { success: false, error: 'Record not found' };
return { success: false, action: 'upsert', error: 'Record not found' };
}
await prisma.proclamation.update({
where: { id: rawId },
data: data as any
});
await prisma.proclamation.update({ where: { id: rawId }, data: data as any });
} else {
await prisma.proclamation.create({ data: data as any });
}
@@ -80,18 +89,41 @@ export const actions = {
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
return {
success: false,
action: 'upsert',
error: 'A record with this name already exists.'
};
}
throw e;
}
return { success: true };
return { success: true, action: 'upsert' };
},
delete: async ({ request, locals }) => {
if (!isSuper(locals)) {
return { success: false, error: 'Only Super admins can delete proclamations' };
return {
success: false,
action: 'delete',
error: 'Only Super admins can delete proclamations'
};
}
return deleteRecord(request, config, locals);
const formData = await request.formData();
const rawId = formData.get('id')?.toString();
if (!rawId) return { success: false, action: 'delete', error: 'Missing id' };
try {
await prisma.proclamation.delete({ where: { id: rawId } });
} catch (e: unknown) {
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2003') {
return {
success: false,
action: 'delete',
error: 'Cannot delete: this proclamation is assigned to users.'
};
}
throw e;
}
return { success: true, action: 'delete' };
}
};