Initial state for GitNexus analysis

This commit is contained in:
Zaid Marzguioui
2026-03-29 19:44:57 +02:00
parent 9d7c7f8030
commit 61ff48cb73
155 changed files with 23483 additions and 1722 deletions

View File

@@ -0,0 +1,3 @@
export {};
// Phase 4 components: members-table, member-form, member-detail,
// application-workflow, dues-category-manager, member-statistics-dashboard

View File

@@ -0,0 +1,55 @@
import { z } from 'zod';
export const MembershipStatusEnum = z.enum([
'active', 'inactive', 'pending', 'resigned', 'excluded', 'deceased',
]);
export type MembershipStatus = z.infer<typeof MembershipStatusEnum>;
export const SepaMandateStatusEnum = z.enum([
'active', 'pending', 'revoked', 'expired',
]);
export const CreateMemberSchema = z.object({
accountId: z.string().uuid(),
memberNumber: z.string().optional(),
firstName: z.string().min(1).max(128),
lastName: z.string().min(1).max(128),
dateOfBirth: z.string().optional(),
gender: z.enum(['male', 'female', 'diverse']).optional(),
title: z.string().max(32).optional(),
email: z.string().email().optional().or(z.literal('')),
phone: z.string().max(32).optional(),
mobile: z.string().max(32).optional(),
street: z.string().max(256).optional(),
houseNumber: z.string().max(16).optional(),
postalCode: z.string().max(10).optional(),
city: z.string().max(128).optional(),
country: z.string().max(2).default('DE'),
status: MembershipStatusEnum.default('active'),
entryDate: z.string().default(() => new Date().toISOString().split('T')[0]!),
duesCategoryId: z.string().uuid().optional(),
iban: z.string().max(34).optional(),
bic: z.string().max(11).optional(),
accountHolder: z.string().max(128).optional(),
gdprConsent: z.boolean().default(false),
notes: z.string().optional(),
});
export type CreateMemberInput = z.infer<typeof CreateMemberSchema>;
export const UpdateMemberSchema = CreateMemberSchema.partial().extend({
memberId: z.string().uuid(),
});
export type UpdateMemberInput = z.infer<typeof UpdateMemberSchema>;
export const CreateDuesCategorySchema = z.object({
accountId: z.string().uuid(),
name: z.string().min(1).max(128),
description: z.string().optional(),
amount: z.number().min(0),
interval: z.enum(['monthly', 'quarterly', 'half_yearly', 'yearly']).default('yearly'),
isDefault: z.boolean().default(false),
});
export type CreateDuesCategoryInput = z.infer<typeof CreateDuesCategorySchema>;

View File

@@ -0,0 +1,190 @@
import type { Database } from '@kit/supabase/database';
import type { SupabaseClient } from '@supabase/supabase-js';
import type { CreateMemberInput, UpdateMemberInput } from '../schema/member.schema';
/**
* Factory for the Member Management API.
*/
export function createMemberManagementApi(client: SupabaseClient<Database>) {
return {
async listMembers(accountId: string, opts?: { status?: string; search?: string; page?: number; pageSize?: number }) {
let query = (client).from('members')
.select('*', { count: 'exact' })
.eq('account_id', accountId)
.order('last_name')
.order('first_name');
if (opts?.status) query = query.eq('status', opts.status as Database['public']['Enums']['membership_status']);
if (opts?.search) {
query = query.or(`last_name.ilike.%${opts.search}%,first_name.ilike.%${opts.search}%,email.ilike.%${opts.search}%,member_number.ilike.%${opts.search}%`);
}
const page = opts?.page ?? 1;
const pageSize = opts?.pageSize ?? 25;
query = query.range((page - 1) * pageSize, page * pageSize - 1);
const { data, error, count } = await query;
if (error) throw error;
return { data: data ?? [], total: count ?? 0, page, pageSize };
},
async getMember(memberId: string) {
const { data, error } = await (client).from('members')
.select('*')
.eq('id', memberId)
.single();
if (error) throw error;
return data;
},
async createMember(input: CreateMemberInput, userId: string) {
const { data, error } = await (client).from('members')
.insert({
account_id: input.accountId,
member_number: input.memberNumber,
first_name: input.firstName,
last_name: input.lastName,
date_of_birth: input.dateOfBirth,
gender: input.gender,
title: input.title,
email: input.email,
phone: input.phone,
mobile: input.mobile,
street: input.street,
house_number: input.houseNumber,
postal_code: input.postalCode,
city: input.city,
country: input.country,
status: input.status,
entry_date: input.entryDate,
dues_category_id: input.duesCategoryId,
iban: input.iban,
bic: input.bic,
account_holder: input.accountHolder,
gdpr_consent: input.gdprConsent,
gdpr_consent_date: input.gdprConsent ? new Date().toISOString() : null,
notes: input.notes,
created_by: userId,
updated_by: userId,
})
.select()
.single();
if (error) throw error;
return data;
},
async updateMember(input: UpdateMemberInput, userId: string) {
const updateData: Record<string, unknown> = { updated_by: userId };
if (input.firstName !== undefined) updateData.first_name = input.firstName;
if (input.lastName !== undefined) updateData.last_name = input.lastName;
if (input.email !== undefined) updateData.email = input.email;
if (input.phone !== undefined) updateData.phone = input.phone;
if (input.mobile !== undefined) updateData.mobile = input.mobile;
if (input.street !== undefined) updateData.street = input.street;
if (input.houseNumber !== undefined) updateData.house_number = input.houseNumber;
if (input.postalCode !== undefined) updateData.postal_code = input.postalCode;
if (input.city !== undefined) updateData.city = input.city;
if (input.status !== undefined) updateData.status = input.status;
if (input.duesCategoryId !== undefined) updateData.dues_category_id = input.duesCategoryId;
if (input.iban !== undefined) updateData.iban = input.iban;
if (input.notes !== undefined) updateData.notes = input.notes;
const { data, error } = await (client).from('members')
.update(updateData)
.eq('id', input.memberId)
.select()
.single();
if (error) throw error;
return data;
},
async deleteMember(memberId: string) {
const { error } = await (client).from('members')
.update({ status: 'resigned', exit_date: new Date().toISOString().split('T')[0] })
.eq('id', memberId);
if (error) throw error;
},
async getMemberStatistics(accountId: string) {
const { data, error } = await (client).from('members')
.select('status')
.eq('account_id', accountId);
if (error) throw error;
const stats = { total: 0, active: 0, inactive: 0, pending: 0, resigned: 0 };
for (const m of (data ?? [])) {
stats.total++;
if (m.status === 'active') stats.active++;
else if (m.status === 'inactive') stats.inactive++;
else if (m.status === 'pending') stats.pending++;
else if (m.status === 'resigned') stats.resigned++;
}
return stats;
},
// Dues categories
async listDuesCategories(accountId: string) {
const { data, error } = await (client).from('dues_categories')
.select('*')
.eq('account_id', accountId)
.order('sort_order');
if (error) throw error;
return data ?? [];
},
// Applications
async listApplications(accountId: string, status?: string) {
let query = (client).from('membership_applications')
.select('*')
.eq('account_id', accountId)
.order('created_at', { ascending: false });
if (status) query = query.eq('status', status as Database['public']['Enums']['application_status']);
const { data, error } = await query;
if (error) throw error;
return data ?? [];
},
async approveApplication(applicationId: string, userId: string) {
// Get application
const { data: app, error: appError } = await (client).from('membership_applications')
.select('*')
.eq('id', applicationId)
.single();
if (appError) throw appError;
// Create member from application
const { data: member, error: memberError } = await (client).from('members')
.insert({
account_id: app.account_id,
first_name: app.first_name,
last_name: app.last_name,
email: app.email,
phone: app.phone,
street: app.street,
postal_code: app.postal_code,
city: app.city,
date_of_birth: app.date_of_birth,
status: 'active',
created_by: userId,
updated_by: userId,
})
.select()
.single();
if (memberError) throw memberError;
// Update application
await (client).from('membership_applications')
.update({
status: 'approved',
reviewed_by: userId,
reviewed_at: new Date().toISOString(),
member_id: member.id,
})
.eq('id', applicationId);
return member;
},
};
}