feat: event team management with departments and roles - Migration 023: event_roles, event_departments, event_member_departments tables - Auto-seed default roles (Head Organizer, Team Lead, Organizer, Volunteer, Sponsor) and departments (Logistics, IT & Tech, Marketing, Finance, Program, Sponsorship, Design, Volunteers) on event creation - API: full CRUD for roles, departments, member-department assignments - Enhanced fetchEventMembers with role + department resolution - Team page: sidebar with department filter + role legend, list/dept view toggle, add/edit/remove members with role + multi-department + notes - i18n: 25 new keys in EN and ET for team management - svelte-check: 0 errors, vitest: 112/112 passed
This commit is contained in:
@@ -27,9 +27,44 @@ export interface EventMember {
|
||||
event_id: string;
|
||||
user_id: string;
|
||||
role: 'lead' | 'manager' | 'member';
|
||||
role_id: string | null;
|
||||
notes: string | null;
|
||||
assigned_at: string;
|
||||
}
|
||||
|
||||
export interface EventRole {
|
||||
id: string;
|
||||
event_id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
sort_order: number;
|
||||
is_default: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface EventDepartment {
|
||||
id: string;
|
||||
event_id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
description: string | null;
|
||||
sort_order: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface EventMemberDepartment {
|
||||
id: string;
|
||||
event_member_id: string;
|
||||
department_id: string;
|
||||
assigned_at: string;
|
||||
}
|
||||
|
||||
export interface EventMemberWithDetails extends EventMember {
|
||||
profile?: { id: string; email: string; full_name: string | null; avatar_url: string | null };
|
||||
event_role?: EventRole;
|
||||
departments: EventDepartment[];
|
||||
}
|
||||
|
||||
export interface EventWithCounts extends Event {
|
||||
member_count: number;
|
||||
}
|
||||
@@ -211,7 +246,7 @@ export async function deleteEvent(
|
||||
export async function fetchEventMembers(
|
||||
supabase: SupabaseClient<Database>,
|
||||
eventId: string
|
||||
): Promise<(EventMember & { profile?: { id: string; email: string; full_name: string | null; avatar_url: string | null } })[]> {
|
||||
): Promise<EventMemberWithDetails[]> {
|
||||
const { data: members, error } = await supabase
|
||||
.from('event_members')
|
||||
.select('*')
|
||||
@@ -234,9 +269,42 @@ export async function fetchEventMembers(
|
||||
|
||||
const profileMap = Object.fromEntries((profiles ?? []).map(p => [p.id, p]));
|
||||
|
||||
// Fetch roles for this event
|
||||
const { data: roles } = await (supabase as any)
|
||||
.from('event_roles')
|
||||
.select('*')
|
||||
.eq('event_id', eventId);
|
||||
const roleMap = Object.fromEntries((roles ?? []).map((r: any) => [r.id, r]));
|
||||
|
||||
// Fetch member-department assignments
|
||||
const memberIds = members.map((m: any) => m.id);
|
||||
const { data: memberDepts } = await (supabase as any)
|
||||
.from('event_member_departments')
|
||||
.select('*')
|
||||
.in('event_member_id', memberIds);
|
||||
|
||||
// Fetch departments for this event
|
||||
const { data: departments } = await (supabase as any)
|
||||
.from('event_departments')
|
||||
.select('*')
|
||||
.eq('event_id', eventId);
|
||||
const deptMap = Object.fromEntries((departments ?? []).map((d: any) => [d.id, d]));
|
||||
|
||||
// Build member-to-departments map
|
||||
const memberDeptMap: Record<string, EventDepartment[]> = {};
|
||||
for (const md of (memberDepts ?? [])) {
|
||||
const dept = deptMap[(md as any).department_id];
|
||||
if (dept) {
|
||||
if (!memberDeptMap[(md as any).event_member_id]) memberDeptMap[(md as any).event_member_id] = [];
|
||||
memberDeptMap[(md as any).event_member_id].push(dept as unknown as EventDepartment);
|
||||
}
|
||||
}
|
||||
|
||||
return members.map((m: any) => ({
|
||||
...m,
|
||||
profile: profileMap[m.user_id] ?? undefined,
|
||||
event_role: m.role_id ? (roleMap[m.role_id] as unknown as EventRole) ?? undefined : undefined,
|
||||
departments: memberDeptMap[m.id] ?? [],
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -244,11 +312,17 @@ export async function addEventMember(
|
||||
supabase: SupabaseClient<Database>,
|
||||
eventId: string,
|
||||
userId: string,
|
||||
role: 'lead' | 'manager' | 'member' = 'member'
|
||||
params: { role?: 'lead' | 'manager' | 'member'; role_id?: string; notes?: string } = {}
|
||||
): Promise<EventMember> {
|
||||
const { data, error } = await supabase
|
||||
const { data, error } = await (supabase as any)
|
||||
.from('event_members')
|
||||
.upsert({ event_id: eventId, user_id: userId, role }, { onConflict: 'event_id,user_id' })
|
||||
.upsert({
|
||||
event_id: eventId,
|
||||
user_id: userId,
|
||||
role: params.role ?? 'member',
|
||||
role_id: params.role_id ?? null,
|
||||
notes: params.notes ?? null,
|
||||
}, { onConflict: 'event_id,user_id' })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
@@ -275,3 +349,202 @@ export async function removeEventMember(
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Event Roles
|
||||
// ============================================================
|
||||
|
||||
export async function fetchEventRoles(
|
||||
supabase: SupabaseClient<Database>,
|
||||
eventId: string
|
||||
): Promise<EventRole[]> {
|
||||
const { data, error } = await (supabase as any)
|
||||
.from('event_roles')
|
||||
.select('*')
|
||||
.eq('event_id', eventId)
|
||||
.order('sort_order');
|
||||
|
||||
if (error) {
|
||||
log.error('fetchEventRoles failed', { error, data: { eventId } });
|
||||
throw error;
|
||||
}
|
||||
return (data ?? []) as unknown as EventRole[];
|
||||
}
|
||||
|
||||
export async function createEventRole(
|
||||
supabase: SupabaseClient<Database>,
|
||||
eventId: string,
|
||||
params: { name: string; color?: string; sort_order?: number }
|
||||
): Promise<EventRole> {
|
||||
const { data, error } = await (supabase as any)
|
||||
.from('event_roles')
|
||||
.insert({
|
||||
event_id: eventId,
|
||||
name: params.name,
|
||||
color: params.color ?? '#6366f1',
|
||||
sort_order: params.sort_order ?? 0,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
log.error('createEventRole failed', { error, data: { eventId, name: params.name } });
|
||||
throw error;
|
||||
}
|
||||
return data as unknown as EventRole;
|
||||
}
|
||||
|
||||
export async function updateEventRole(
|
||||
supabase: SupabaseClient<Database>,
|
||||
roleId: string,
|
||||
params: Partial<Pick<EventRole, 'name' | 'color' | 'sort_order' | 'is_default'>>
|
||||
): Promise<EventRole> {
|
||||
const { data, error } = await (supabase as any)
|
||||
.from('event_roles')
|
||||
.update(params)
|
||||
.eq('id', roleId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
log.error('updateEventRole failed', { error, data: { roleId } });
|
||||
throw error;
|
||||
}
|
||||
return data as unknown as EventRole;
|
||||
}
|
||||
|
||||
export async function deleteEventRole(
|
||||
supabase: SupabaseClient<Database>,
|
||||
roleId: string
|
||||
): Promise<void> {
|
||||
const { error } = await (supabase as any)
|
||||
.from('event_roles')
|
||||
.delete()
|
||||
.eq('id', roleId);
|
||||
|
||||
if (error) {
|
||||
log.error('deleteEventRole failed', { error, data: { roleId } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Event Departments
|
||||
// ============================================================
|
||||
|
||||
export async function fetchEventDepartments(
|
||||
supabase: SupabaseClient<Database>,
|
||||
eventId: string
|
||||
): Promise<EventDepartment[]> {
|
||||
const { data, error } = await (supabase as any)
|
||||
.from('event_departments')
|
||||
.select('*')
|
||||
.eq('event_id', eventId)
|
||||
.order('sort_order');
|
||||
|
||||
if (error) {
|
||||
log.error('fetchEventDepartments failed', { error, data: { eventId } });
|
||||
throw error;
|
||||
}
|
||||
return (data ?? []) as unknown as EventDepartment[];
|
||||
}
|
||||
|
||||
export async function createEventDepartment(
|
||||
supabase: SupabaseClient<Database>,
|
||||
eventId: string,
|
||||
params: { name: string; color?: string; description?: string; sort_order?: number }
|
||||
): Promise<EventDepartment> {
|
||||
const { data, error } = await (supabase as any)
|
||||
.from('event_departments')
|
||||
.insert({
|
||||
event_id: eventId,
|
||||
name: params.name,
|
||||
color: params.color ?? '#00A3E0',
|
||||
description: params.description ?? null,
|
||||
sort_order: params.sort_order ?? 0,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
log.error('createEventDepartment failed', { error, data: { eventId, name: params.name } });
|
||||
throw error;
|
||||
}
|
||||
return data as unknown as EventDepartment;
|
||||
}
|
||||
|
||||
export async function updateEventDepartment(
|
||||
supabase: SupabaseClient<Database>,
|
||||
deptId: string,
|
||||
params: Partial<Pick<EventDepartment, 'name' | 'color' | 'description' | 'sort_order'>>
|
||||
): Promise<EventDepartment> {
|
||||
const { data, error } = await (supabase as any)
|
||||
.from('event_departments')
|
||||
.update(params)
|
||||
.eq('id', deptId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
log.error('updateEventDepartment failed', { error, data: { deptId } });
|
||||
throw error;
|
||||
}
|
||||
return data as unknown as EventDepartment;
|
||||
}
|
||||
|
||||
export async function deleteEventDepartment(
|
||||
supabase: SupabaseClient<Database>,
|
||||
deptId: string
|
||||
): Promise<void> {
|
||||
const { error } = await (supabase as any)
|
||||
.from('event_departments')
|
||||
.delete()
|
||||
.eq('id', deptId);
|
||||
|
||||
if (error) {
|
||||
log.error('deleteEventDepartment failed', { error, data: { deptId } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Member-Department Assignments
|
||||
// ============================================================
|
||||
|
||||
export async function assignMemberDepartment(
|
||||
supabase: SupabaseClient<Database>,
|
||||
eventMemberId: string,
|
||||
departmentId: string
|
||||
): Promise<EventMemberDepartment> {
|
||||
const { data, error } = await (supabase as any)
|
||||
.from('event_member_departments')
|
||||
.upsert(
|
||||
{ event_member_id: eventMemberId, department_id: departmentId },
|
||||
{ onConflict: 'event_member_id,department_id' }
|
||||
)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
log.error('assignMemberDepartment failed', { error, data: { eventMemberId, departmentId } });
|
||||
throw error;
|
||||
}
|
||||
return data as unknown as EventMemberDepartment;
|
||||
}
|
||||
|
||||
export async function unassignMemberDepartment(
|
||||
supabase: SupabaseClient<Database>,
|
||||
eventMemberId: string,
|
||||
departmentId: string
|
||||
): Promise<void> {
|
||||
const { error } = await (supabase as any)
|
||||
.from('event_member_departments')
|
||||
.delete()
|
||||
.eq('event_member_id', eventMemberId)
|
||||
.eq('department_id', departmentId);
|
||||
|
||||
if (error) {
|
||||
log.error('unassignMemberDepartment failed', { error, data: { eventMemberId, departmentId } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
import { fetchEventBySlug, fetchEventMembers } from '$lib/api/events';
|
||||
import { fetchEventBySlug, fetchEventMembers, fetchEventRoles, fetchEventDepartments } from '$lib/api/events';
|
||||
import { createLogger } from '$lib/utils/logger';
|
||||
|
||||
const log = createLogger('page.event-detail');
|
||||
@@ -16,9 +16,13 @@ export const load: LayoutServerLoad = async ({ params, locals, parent }) => {
|
||||
const event = await fetchEventBySlug(locals.supabase, orgId, params.eventSlug);
|
||||
if (!event) error(404, 'Event not found');
|
||||
|
||||
const members = await fetchEventMembers(locals.supabase, event.id);
|
||||
const [members, roles, departments] = await Promise.all([
|
||||
fetchEventMembers(locals.supabase, event.id),
|
||||
fetchEventRoles(locals.supabase, event.id),
|
||||
fetchEventDepartments(locals.supabase, event.id),
|
||||
]);
|
||||
|
||||
return { event, eventMembers: members };
|
||||
return { event, eventMembers: members, eventRoles: roles, eventDepartments: departments };
|
||||
} catch (e: any) {
|
||||
if (e?.status === 404) throw e;
|
||||
log.error('Failed to load event', { error: e, data: { orgId, eventSlug: params.eventSlug } });
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { page } from "$app/stores";
|
||||
import { Avatar } from "$lib/components/ui";
|
||||
import type { Snippet } from "svelte";
|
||||
import type { Event, EventMember } from "$lib/api/events";
|
||||
import type { Event, EventMemberWithDetails, EventRole, EventDepartment } from "$lib/api/events";
|
||||
import * as m from "$lib/paraglide/messages";
|
||||
|
||||
interface Props {
|
||||
@@ -10,14 +10,9 @@
|
||||
org: { id: string; name: string; slug: string };
|
||||
userRole: string;
|
||||
event: Event;
|
||||
eventMembers: (EventMember & {
|
||||
profile?: {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name: string | null;
|
||||
avatar_url: string | null;
|
||||
};
|
||||
})[];
|
||||
eventMembers: EventMemberWithDetails[];
|
||||
eventRoles: EventRole[];
|
||||
eventDepartments: EventDepartment[];
|
||||
};
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import type { Database } from "$lib/supabase/types";
|
||||
import { toasts } from "$lib/stores/ui";
|
||||
import type { Event, EventMember } from "$lib/api/events";
|
||||
import type { Event, EventMemberWithDetails, EventRole, EventDepartment } from "$lib/api/events";
|
||||
import * as m from "$lib/paraglide/messages";
|
||||
|
||||
interface Props {
|
||||
@@ -13,14 +13,9 @@
|
||||
org: { id: string; name: string; slug: string };
|
||||
userRole: string;
|
||||
event: Event;
|
||||
eventMembers: (EventMember & {
|
||||
profile?: {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name: string | null;
|
||||
avatar_url: string | null;
|
||||
};
|
||||
})[];
|
||||
eventMembers: EventMemberWithDetails[];
|
||||
eventRoles: EventRole[];
|
||||
eventDepartments: EventDepartment[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user