admin: category drag-and-drop reorder + modal-based license key management

- Add sortOrder column to ProclamationCategory with migration
- Implement drag-and-drop reorder in AdminTable with persist-to-DB
- Refactor AdminTable to use $derived + dragOverride (no $effect sync)
- Replace window.location.reload with invalidateAll for reorder

- Convert license key create to modal with subscription level dropdown
- Add LicenseKeyForm.svelte following the CategoryForm pattern
- Add replace action (delete + generate new key with same level)
- Remove deactivateOld checkbox; each key has its own level
- Use use:enhance with update() to refresh list after create/replace
This commit is contained in:
NeilClack
2026-07-08 12:59:55 -05:00
parent 93d941569e
commit 6266be0edf
31 changed files with 21102 additions and 23217 deletions
@@ -8,7 +8,20 @@ import { error } from '@sveltejs/kit';
const config = getModelConfig('license-keys')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
const records = await loadRecords(locals, config);
const subscriptionLevels = await prisma.subscriptionLevel.findMany({
select: { id: true, name: true },
orderBy: { name: 'asc' }
});
return {
...records,
subscriptionLevels: subscriptionLevels.map((s: { id: number; name: string }) => ({
label: s.name,
value: s.name
}))
};
};
export const actions = {
@@ -17,45 +30,39 @@ export const actions = {
const formData = await request.formData();
const email = formData.get('email')?.toString();
const deactivateOld = formData.get('deactivateOld') === 'on';
const level = formData.get('level')?.toString();
if (!email) {
return { success: false, error: 'Email is required' };
}
if (!level) {
return { success: false, error: 'Subscription level is required' };
}
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
return { success: false, error: 'No user found with that email' };
}
// Deactivate old keys if requested
if (deactivateOld) {
await prisma.licenseKey.updateMany({
where: { userId: user.id, active: true },
data: { active: false }
});
}
// Generate 16-char hex UUID grouped as 4-4-4-4
const hex = crypto.randomUUID().replace(/-/g, '').slice(0, 16).toUpperCase();
const key = `${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}`;
const licenseKey = await prisma.licenseKey.create({
await prisma.licenseKey.create({
data: {
key,
active: true,
level: 'standard',
level,
userId: user.id
}
});
// Send email with license key
try {
const profile = await prisma.profile.findUnique({ where: { userId: user.id } });
await sendMail({
to: email,
subject: 'Your License Key',
data: {
firstName: user.name ?? 'Valued Customer',
firstName: profile?.firstName ?? user.name ?? 'Valued Customer',
licenseKey: key
}
});
@@ -63,6 +70,32 @@ export const actions = {
console.error('Failed to send license key email:', e);
}
return { success: true, error: undefined };
},
replace: async ({ request, locals }) => {
if (!locals.user) throw error(401, 'Unauthorized');
const formData = await request.formData();
const keyId = formData.get('id')?.toString();
if (!keyId) return { success: false, error: 'Missing key id' };
const existing = await prisma.licenseKey.findUnique({ where: { id: keyId } });
if (!existing) return { success: false, error: 'License key not found' };
await prisma.licenseKey.delete({ where: { id: keyId } });
const hex = crypto.randomUUID().replace(/-/g, '').slice(0, 16).toUpperCase();
const key = `${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}`;
await prisma.licenseKey.create({
data: {
key,
active: true,
level: existing.level,
userId: existing.userId
}
});
return { success: true, error: undefined };
}
};