Initial state for GitNexus analysis
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
export {};
|
||||
// Phase 4 components: members-table, member-form, member-detail,
|
||||
// application-workflow, dues-category-manager, member-statistics-dashboard
|
||||
@@ -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>;
|
||||
190
packages/features/member-management/src/server/api.ts
Normal file
190
packages/features/member-management/src/server/api.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user