Add account hierarchy framework with migrations, RLS policies, and UI components

This commit is contained in:
T. Zehetbauer
2026-03-31 22:18:04 +02:00
parent 7e7da0b465
commit 59546ad6d2
262 changed files with 11671 additions and 3927 deletions

View File

@@ -1,9 +1,11 @@
'use server';
import { z } from 'zod';
import { authActionClient } from '@kit/next/safe-action';
import { getLogger } from '@kit/shared/logger';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import {
CreateEventSchema,
EventRegistrationSchema,
@@ -44,7 +46,10 @@ export const createHolidayPass = authActionClient
const logger = await getLogger();
const api = createEventManagementApi(client);
logger.info({ name: 'event.createHolidayPass' }, 'Creating holiday pass...');
logger.info(
{ name: 'event.createHolidayPass' },
'Creating holiday pass...',
);
const result = await api.createHolidayPass(input);
logger.info({ name: 'event.createHolidayPass' }, 'Holiday pass created');
return { success: true, data: result };

View File

@@ -1,6 +1,7 @@
import type { Database } from '@kit/supabase/database';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '@kit/supabase/database';
import type { CreateEventInput } from '../schema/event.schema';
/* eslint-disable @typescript-eslint/no-explicit-any */
@@ -10,16 +11,28 @@ export function createEventManagementApi(client: SupabaseClient<Database>) {
const db = client;
return {
async listEvents(accountId: string, opts?: { status?: string; page?: number }) {
let query = client.from('events').select('*', { count: 'exact' })
.eq('account_id', accountId).order('event_date', { ascending: false });
async listEvents(
accountId: string,
opts?: { status?: string; page?: number },
) {
let query = client
.from('events')
.select('*', { count: 'exact' })
.eq('account_id', accountId)
.order('event_date', { ascending: false });
if (opts?.status) query = query.eq('status', opts.status);
const page = opts?.page ?? 1;
query = query.range((page - 1) * PAGE_SIZE, page * PAGE_SIZE - 1);
const { data, error, count } = await query;
if (error) throw error;
const total = count ?? 0;
return { data: data ?? [], total, page, pageSize: PAGE_SIZE, totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)) };
return {
data: data ?? [],
total,
page,
pageSize: PAGE_SIZE,
totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)),
};
},
async getRegistrationCounts(eventIds: string[]) {
@@ -40,71 +53,131 @@ export function createEventManagementApi(client: SupabaseClient<Database>) {
},
async getEvent(eventId: string) {
const { data, error } = await client.from('events').select('*').eq('id', eventId).single();
const { data, error } = await client
.from('events')
.select('*')
.eq('id', eventId)
.single();
if (error) throw error;
return data;
},
async createEvent(input: CreateEventInput) {
const { data, error } = await client.from('events').insert({
account_id: input.accountId, name: input.name, description: input.description,
event_date: input.eventDate, event_time: input.eventTime, end_date: input.endDate,
location: input.location, capacity: input.capacity, min_age: input.minAge,
max_age: input.maxAge, fee: input.fee, status: input.status,
registration_deadline: input.registrationDeadline,
contact_name: input.contactName, contact_email: input.contactEmail, contact_phone: input.contactPhone,
}).select().single();
const { data, error } = await client
.from('events')
.insert({
account_id: input.accountId,
name: input.name,
description: input.description,
event_date: input.eventDate,
event_time: input.eventTime,
end_date: input.endDate,
location: input.location,
capacity: input.capacity,
min_age: input.minAge,
max_age: input.maxAge,
fee: input.fee,
status: input.status,
registration_deadline: input.registrationDeadline,
contact_name: input.contactName,
contact_email: input.contactEmail,
contact_phone: input.contactPhone,
})
.select()
.single();
if (error) throw error;
return data;
},
async registerForEvent(input: { eventId: string; firstName: string; lastName: string; email?: string; parentName?: string }) {
async registerForEvent(input: {
eventId: string;
firstName: string;
lastName: string;
email?: string;
parentName?: string;
}) {
// Check capacity
const event = await this.getEvent(input.eventId);
if (event.capacity) {
const { count } = await client.from('event_registrations').select('*', { count: 'exact', head: true })
.eq('event_id', input.eventId).in('status', ['pending', 'confirmed']);
const { count } = await client
.from('event_registrations')
.select('*', { count: 'exact', head: true })
.eq('event_id', input.eventId)
.in('status', ['pending', 'confirmed']);
if ((count ?? 0) >= event.capacity) {
throw new Error('Event is full');
}
}
const { data, error } = await client.from('event_registrations').insert({
event_id: input.eventId, first_name: input.firstName, last_name: input.lastName,
email: input.email, parent_name: input.parentName, status: 'confirmed',
}).select().single();
const { data, error } = await client
.from('event_registrations')
.insert({
event_id: input.eventId,
first_name: input.firstName,
last_name: input.lastName,
email: input.email,
parent_name: input.parentName,
status: 'confirmed',
})
.select()
.single();
if (error) throw error;
return data;
},
async getRegistrations(eventId: string) {
const { data, error } = await client.from('event_registrations').select('*')
.eq('event_id', eventId).order('created_at');
const { data, error } = await client
.from('event_registrations')
.select('*')
.eq('event_id', eventId)
.order('created_at');
if (error) throw error;
return data ?? [];
},
// Holiday passes
async listHolidayPasses(accountId: string) {
const { data, error } = await client.from('holiday_passes').select('*')
.eq('account_id', accountId).order('year', { ascending: false });
const { data, error } = await client
.from('holiday_passes')
.select('*')
.eq('account_id', accountId)
.order('year', { ascending: false });
if (error) throw error;
return data ?? [];
},
async getPassActivities(passId: string) {
const { data, error } = await client.from('holiday_pass_activities').select('*')
.eq('pass_id', passId).order('activity_date');
const { data, error } = await client
.from('holiday_pass_activities')
.select('*')
.eq('pass_id', passId)
.order('activity_date');
if (error) throw error;
return data ?? [];
},
async createHolidayPass(input: { accountId: string; name: string; year: number; description?: string; price?: number; validFrom?: string; validUntil?: string }) {
const { data, error } = await client.from('holiday_passes').insert({
account_id: input.accountId, name: input.name, year: input.year,
description: input.description, price: input.price ?? 0,
valid_from: input.validFrom, valid_until: input.validUntil,
}).select().single();
async createHolidayPass(input: {
accountId: string;
name: string;
year: number;
description?: string;
price?: number;
validFrom?: string;
validUntil?: string;
}) {
const { data, error } = await client
.from('holiday_passes')
.insert({
account_id: input.accountId,
name: input.name,
year: input.year,
description: input.description,
price: input.price ?? 0,
valid_from: input.validFrom,
valid_until: input.validUntil,
})
.select()
.single();
if (error) throw error;
return data;
},