102 lines
3.1 KiB
TypeScript
102 lines
3.1 KiB
TypeScript
'use server';
|
|
|
|
import { revalidatePath } from 'next/cache';
|
|
import { redirect } from 'next/navigation';
|
|
import { z } from 'zod';
|
|
import { prisma } from '@/lib/prisma';
|
|
import { requireAdmin } from '@/lib/requireAdmin';
|
|
|
|
// datetime-local inputs return "YYYY-MM-DDTHH:mm" in the browser's local
|
|
// timezone. We treat them as local time and convert via Date constructor
|
|
// (which interprets the string in the server's timezone — for a single-host
|
|
// deploy that's fine; document if you ever shard across timezones).
|
|
const slotSchema = z
|
|
.object({
|
|
labelSv: z.string().trim().min(1).max(200),
|
|
labelEn: z.string().trim().min(1).max(200),
|
|
startsAt: z.string().trim().min(1),
|
|
endsAt: z.string().trim().min(1),
|
|
capacity: z.coerce.number().int().min(0).max(10000),
|
|
active: z.preprocess(
|
|
(v) => v === 'on' || v === 'true' || v === true,
|
|
z.boolean(),
|
|
),
|
|
})
|
|
.refine((d) => new Date(d.startsAt) < new Date(d.endsAt), {
|
|
path: ['endsAt'],
|
|
message: 'invalidTime',
|
|
});
|
|
|
|
type Parsed = z.infer<typeof slotSchema>;
|
|
|
|
function toDbFields(d: Parsed) {
|
|
return {
|
|
labelSv: d.labelSv,
|
|
labelEn: d.labelEn,
|
|
startsAt: new Date(d.startsAt),
|
|
endsAt: new Date(d.endsAt),
|
|
capacity: d.capacity,
|
|
active: d.active,
|
|
};
|
|
}
|
|
|
|
function fromFormData(fd: FormData): Parsed {
|
|
return slotSchema.parse({
|
|
labelSv: fd.get('labelSv') ?? '',
|
|
labelEn: fd.get('labelEn') ?? '',
|
|
startsAt: fd.get('startsAt') ?? '',
|
|
endsAt: fd.get('endsAt') ?? '',
|
|
capacity: fd.get('capacity') ?? 0,
|
|
active: fd.get('active') ?? false,
|
|
});
|
|
}
|
|
|
|
export async function createPickupSlot(formData: FormData) {
|
|
await requireAdmin();
|
|
const data = fromFormData(formData);
|
|
await prisma.pickupSlot.create({ data: toDbFields(data) });
|
|
revalidatePath('/admin/pickup-slots');
|
|
revalidatePath('/');
|
|
redirect('/admin/pickup-slots');
|
|
}
|
|
|
|
export async function updatePickupSlot(formData: FormData) {
|
|
await requireAdmin();
|
|
const id = String(formData.get('id') ?? '');
|
|
if (!id) return;
|
|
const data = fromFormData(formData);
|
|
await prisma.pickupSlot.update({ where: { id }, data: toDbFields(data) });
|
|
revalidatePath('/admin/pickup-slots');
|
|
revalidatePath(`/admin/pickup-slots/${id}`);
|
|
revalidatePath('/');
|
|
redirect('/admin/pickup-slots');
|
|
}
|
|
|
|
export async function togglePickupSlotActive(formData: FormData) {
|
|
await requireAdmin();
|
|
const id = String(formData.get('id') ?? '');
|
|
if (!id) return;
|
|
const cur = await prisma.pickupSlot.findUnique({ where: { id } });
|
|
if (!cur) return;
|
|
await prisma.pickupSlot.update({
|
|
where: { id },
|
|
data: { active: !cur.active },
|
|
});
|
|
revalidatePath('/admin/pickup-slots');
|
|
revalidatePath('/');
|
|
}
|
|
|
|
export async function deletePickupSlot(formData: FormData) {
|
|
await requireAdmin();
|
|
const id = String(formData.get('id') ?? '');
|
|
if (!id) return;
|
|
const inUse = await prisma.booking.count({ where: { pickupSlotId: id } });
|
|
if (inUse > 0) {
|
|
redirect(`/admin/pickup-slots/${id}?error=in-use&count=${inUse}`);
|
|
}
|
|
await prisma.pickupSlot.delete({ where: { id } });
|
|
revalidatePath('/admin/pickup-slots');
|
|
revalidatePath('/');
|
|
redirect('/admin/pickup-slots');
|
|
}
|