diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 7555e85..170c16f 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -9,7 +9,7 @@ import { supabaseAdmin } from '$lib/server/supabase'; // Cache for setup check to avoid hitting DB on every request let setupCheckCache: { needsSetup: boolean; checkedAt: number } | null = null; -const SETUP_CACHE_TTL = 60000; // 1 minute cache +const SETUP_CACHE_TTL = 300000; // 5 minute cache /** * Invalidate the setup check cache @@ -110,8 +110,20 @@ const supabaseHandle: Handle = async ({ event, resolve }) => { const setupCheckHandle: Handle = async ({ event, resolve }) => { const path = event.url.pathname; - // Always allow access to setup page and static assets - if (path === '/setup' || path.startsWith('/api/') || path.startsWith('/auth/')) { + // Always allow access to setup, auth, api, login, signup, public pages, and static assets + if ( + path === '/setup' || + path.startsWith('/api/') || + path.startsWith('/auth/') || + path === '/login' || + path === '/signup' || + path === '/forgot-password' || + path === '/logout' || + path.startsWith('/public/') || + path.startsWith('/join') || + path.startsWith('/_app/') || + path.includes('.') + ) { return resolve(event); } diff --git a/src/routes/(app)/admin/bulk-email/+page.server.ts b/src/routes/(app)/admin/bulk-email/+page.server.ts index 39d3214..f9c29fa 100644 --- a/src/routes/(app)/admin/bulk-email/+page.server.ts +++ b/src/routes/(app)/admin/bulk-email/+page.server.ts @@ -14,14 +14,15 @@ export const load: PageServerLoad = async ({ locals }) => { .limit(20); // Get recipient counts for filter preview + // Use members_with_dues view which has status_name from the joined membership_statuses table const { data: members } = await supabaseAdmin - .from('members') - .select('id, role, status') + .from('members_with_dues') + .select('id, role, status_name, email') .not('email', 'is', null); const recipientCounts = { all: members?.length || 0, - active: members?.filter((m: any) => m.status === 'active').length || 0, + active: members?.filter((m: any) => m.status_name === 'active').length || 0, board: members?.filter((m: any) => m.role === 'board' || m.role === 'admin').length || 0, admin: members?.filter((m: any) => m.role === 'admin').length || 0 }; @@ -52,14 +53,14 @@ export const actions: Actions = { return fail(400, { error: 'Email body must be at least 10 characters' }); } - // Build recipient query + // Build recipient query - use view for status filtering let query = supabaseAdmin - .from('members') - .select('id, email, first_name, last_name, role, status'); + .from('members_with_dues') + .select('id, email, first_name, last_name, role, status_name'); switch (target) { case 'active': - query = query.eq('status', 'active'); + query = query.eq('status_name', 'active'); break; case 'board': query = query.in('role', ['board', 'admin']); diff --git a/src/routes/(app)/admin/members/+page.server.ts b/src/routes/(app)/admin/members/+page.server.ts index 1cac1ec..6be6ada 100644 --- a/src/routes/(app)/admin/members/+page.server.ts +++ b/src/routes/(app)/admin/members/+page.server.ts @@ -37,12 +37,20 @@ export const load: PageServerLoad = async ({ locals, url }) => { filteredMembers = filteredMembers.filter((m: any) => m.status_name === statusFilter); } - // Load membership statuses for dropdown - const { data: statuses } = await supabaseAdmin + // Load membership statuses for dropdown (deduplicate by display_name) + const { data: rawStatuses } = await supabaseAdmin .from('membership_statuses') .select('*') .order('sort_order', { ascending: true }); + const seen = new Set(); + const statuses = (rawStatuses || []).filter((s: any) => { + const key = s.display_name.toLowerCase(); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + // Calculate stats const stats = { total: members?.length || 0, diff --git a/src/routes/(app)/board/members/+page.server.ts b/src/routes/(app)/board/members/+page.server.ts index 860ca58..69f8473 100644 --- a/src/routes/(app)/board/members/+page.server.ts +++ b/src/routes/(app)/board/members/+page.server.ts @@ -1,4 +1,5 @@ import type { PageServerLoad } from './$types'; +import { supabaseAdmin } from '$lib/server/supabase'; export const load: PageServerLoad = async ({ locals, url }) => { const searchQuery = url.searchParams.get('search') || ''; @@ -9,8 +10,8 @@ export const load: PageServerLoad = async ({ locals, url }) => { const { member } = await locals.safeGetSession(); const isPrivileged = member?.role === 'admin' || member?.role === 'board'; - // Build the query - select only needed columns to avoid exposing sensitive fields - let query = locals.supabase + // Use admin client to bypass RLS on the view (dues_payments RLS blocks the join) + let query = supabaseAdmin .from('members_with_dues') .select('id, member_id, first_name, last_name, email, phone, role, status_name, type_name, avatar_url, created_at, nationality, address, directory_privacy') .order('last_name', { ascending: true }); @@ -40,7 +41,7 @@ export const load: PageServerLoad = async ({ locals, url }) => { } // Get membership statuses for filter dropdown - const { data: statuses } = await locals.supabase + const { data: statuses } = await supabaseAdmin .from('membership_statuses') .select('*') .order('sort_order', { ascending: true }); diff --git a/supabase/migrations/026_fix_duplicate_statuses.sql b/supabase/migrations/026_fix_duplicate_statuses.sql new file mode 100644 index 0000000..8895e13 --- /dev/null +++ b/supabase/migrations/026_fix_duplicate_statuses.sql @@ -0,0 +1,24 @@ +-- Fix duplicate membership statuses +-- The database has both "Active" (capitalized name) and "active" (lowercase) entries. +-- The seed data only creates "active" - the "Active" entry was added manually. +-- We need to: +-- 1. Move any members pointing to the duplicate "Active" status to the original "active" status +-- 2. Delete the duplicate "Active" entry + +-- First, update any members that reference the duplicate "Active" (capitalized) status +-- to point to the original "active" (lowercase) status +UPDATE public.members +SET membership_status_id = ( + SELECT id FROM public.membership_statuses WHERE name = 'active' LIMIT 1 +) +WHERE membership_status_id IN ( + SELECT id FROM public.membership_statuses WHERE name = 'Active' +); + +-- Delete the duplicate "Active" (capitalized) status entry +DELETE FROM public.membership_statuses WHERE name = 'Active'; + +-- Ensure the remaining "active" status has correct sort_order +UPDATE public.membership_statuses +SET sort_order = 2, display_name = 'Active', color = '#22c55e' +WHERE name = 'active';