# Monaco USA Portal 2026 - Complete Rebuild ## Project Overview Rebuild the Monaco USA member portal from scratch in `monacousa-portal-2026/` with modern architecture, beautiful UI, and improved functionality. --- # DETAILED FEATURE SPECIFICATIONS ## 1. MEMBER SYSTEM (Detailed) ### 1.1 Member ID Format - **Format**: `MUSA-XXXX` (sequential 4-digit number) - **Examples**: MUSA-0001, MUSA-0042, MUSA-1234 - **Auto-generated** on member creation - **Immutable** once assigned - **Unique constraint** in database ### 1.2 Membership Statuses (Admin-Configurable) Admin can create, edit, and delete statuses via Settings. **Default Statuses (seeded on first run):** | Status | Color | Description | Is Default | |--------|-------|-------------|------------| | `pending` | Yellow | New member, awaiting dues payment | Yes (for new signups) | | `active` | Green | Dues paid, full access | No | | `inactive` | Gray | Lapsed membership or suspended | No | | `expired` | Red | Membership terminated | No | **Status Configuration Table:** ```sql CREATE TABLE public.membership_statuses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, color TEXT NOT NULL DEFAULT '#6b7280', -- Tailwind gray-500 description TEXT, is_default BOOLEAN DEFAULT FALSE, -- Used for new signups sort_order INTEGER DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW() ); ``` ### 1.3 Roles/Tiers **Fixed 3-tier system (not configurable):** | Role | Access Level | Capabilities | |------|--------------|--------------| | `member` | Basic | View own profile, events, pay dues | | `board` | Elevated | + Member directory, record payments, manage events | | `admin` | Full | + User management, system settings, all data | ### 1.4 Required Member Fields All fields marked as required during signup: | Field | Type | Validation | Notes | |-------|------|------------|-------| | `first_name` | Text | Min 2 chars | Required | | `last_name` | Text | Min 2 chars | Required | | `email` | Email | Valid email format | Required, unique | | `phone` | Text | International format | Required | | `date_of_birth` | Date | Must be 18+ years old | Required | | `address` | Text | Min 10 chars | Required | | `nationality` | Array | At least 1 country | Required, multiple allowed | ### 1.5 Optional Member Fields | Field | Type | Notes | |-------|------|-------| | `avatar_url` | Text | Supabase Storage path | | `membership_type_id` | UUID | Links to membership_types table | | `notes` | Text | Admin-only notes about member | ### 1.6 Nationality Handling - **Multiple nationalities allowed** - Stored as PostgreSQL `TEXT[]` array - Uses ISO 3166-1 alpha-2 country codes: `['FR', 'US', 'MC']` - UI shows country flags + names - Searchable/filterable in directory ### 1.7 Profile Features - **Profile photo**: Upload via Supabase Storage - Max size: 5MB - Formats: JPG, PNG, WebP - Auto-resized to 256x256 - Stored at: `avatars/{member_id}/profile.{ext}` - **No bio field** (simplified profile) - Members can edit: name, phone, address, nationality, photo ### 1.8 Member Directory **Visibility controlled by admin settings:** ```sql CREATE TABLE public.directory_settings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), field_name TEXT NOT NULL UNIQUE, visible_to_members BOOLEAN DEFAULT FALSE, visible_to_board BOOLEAN DEFAULT TRUE, visible_to_admin BOOLEAN DEFAULT TRUE, updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Default visibility settings INSERT INTO directory_settings (field_name, visible_to_members, visible_to_board) VALUES ('first_name', true, true), ('last_name', true, true), ('avatar_url', true, true), ('nationality', true, true), ('email', false, true), ('phone', false, true), ('address', false, true), ('date_of_birth', false, true), ('member_since', true, true), ('membership_status', false, true); ``` ### 1.9 Member Signup Flow ``` ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ /signup │────▶│ Create Auth │────▶│ Email Verify│ │ Form │ │ User + Member│ │ Link Sent │ └─────────────┘ └──────────────┘ └─────────────┘ │ ▼ ┌──────────────┐ ┌─────────────┐ │ Status = │────▶│ Wait for │ │ 'pending' │ │ Dues Payment│ └──────────────┘ └─────────────┘ │ ▼ ┌─────────────┐ │ Board/Admin │ │ Records Dues│ └─────────────┘ │ ▼ ┌─────────────┐ │ Status = │ │ 'active' │ └─────────────┘ ``` **Key Points:** - Email verification required - Status starts as `pending` - Member gains `active` status ONLY when first dues payment recorded - Pending members can log in but see limited dashboard ### 1.10 Admin Member Management **Two ways to add members:** **Option A: Direct Add** 1. Admin fills out member form 2. Admin sets temporary password OR sends password setup email 3. Member record created with chosen status 4. Member can log in immediately **Option B: Invite** 1. Admin enters email + basic info 2. System sends invitation email with signup link 3. Invitee completes signup form 4. Status set based on invite settings ### 1.11 Membership Types (Admin-Configurable) Admin can create membership tiers with different pricing: ```sql CREATE TABLE public.membership_types ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL UNIQUE, -- 'regular', 'student', 'senior' display_name TEXT NOT NULL, -- 'Regular Member', 'Student' annual_dues DECIMAL(10,2) NOT NULL, -- 50.00, 25.00, etc. description TEXT, is_default BOOLEAN DEFAULT FALSE, -- Default for new signups is_active BOOLEAN DEFAULT TRUE, -- Can be assigned sort_order INTEGER DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW() ); -- Default membership types INSERT INTO membership_types (name, display_name, annual_dues, is_default) VALUES ('regular', 'Regular Member', 50.00, true), ('student', 'Student', 25.00, false), ('senior', 'Senior (65+)', 35.00, false), ('family', 'Family', 75.00, false), ('honorary', 'Honorary Member', 0.00, false); ``` ### 1.12 Complete Member Schema ```sql CREATE TABLE public.members ( -- Identity id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, member_id TEXT UNIQUE NOT NULL, -- MUSA-0001 format (auto-generated) -- Required Personal Info first_name TEXT NOT NULL, last_name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, phone TEXT NOT NULL, date_of_birth DATE NOT NULL, address TEXT NOT NULL, nationality TEXT[] NOT NULL DEFAULT '{}', -- Membership role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('member', 'board', 'admin')), membership_status_id UUID REFERENCES public.membership_statuses(id), membership_type_id UUID REFERENCES public.membership_types(id), member_since DATE DEFAULT CURRENT_DATE, -- Profile avatar_url TEXT, -- Admin notes TEXT, -- Admin-only notes -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Auto-generate member_id trigger CREATE OR REPLACE FUNCTION generate_member_id() RETURNS TRIGGER AS $$ DECLARE next_num INTEGER; BEGIN SELECT COALESCE(MAX(CAST(SUBSTRING(member_id FROM 6) AS INTEGER)), 0) + 1 INTO next_num FROM public.members; NEW.member_id := 'MUSA-' || LPAD(next_num::TEXT, 4, '0'); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER set_member_id BEFORE INSERT ON public.members FOR EACH ROW WHEN (NEW.member_id IS NULL) EXECUTE FUNCTION generate_member_id(); ``` --- ## 2. DUES/PAYMENTS SYSTEM (Detailed) ### 2.1 Dues Cycle - **Due date calculation**: Payment date + 365 days - **Example**: Payment on Jan 15, 2026 → Due Jan 15, 2027 - **No proration**: Full annual dues regardless of join date ### 2.2 Payment Methods **Bank transfer only** (no online payments): - IBAN tracking - Reference number for matching - Manual recording by Board/Admin ### 2.3 Payment Recording **Who can record payments:** - Board members - Admins **Standard payment data tracked:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `member_id` | UUID | Yes | Which member | | `amount` | Decimal | Yes | Payment amount (€) | | `payment_date` | Date | Yes | When payment was made | | `due_date` | Date | Yes | When this payment period ends (auto-calculated) | | `reference` | Text | No | Bank transfer reference | | `payment_method` | Text | Yes | Always 'bank_transfer' for now | | `recorded_by` | UUID | Yes | Board/Admin who recorded | | `notes` | Text | No | Optional notes | ### 2.4 Dues Settings (Admin-Configurable) ```sql CREATE TABLE public.dues_settings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), setting_key TEXT UNIQUE NOT NULL, setting_value TEXT NOT NULL, description TEXT, updated_at TIMESTAMPTZ DEFAULT NOW(), updated_by UUID REFERENCES public.members(id) ); -- Default settings INSERT INTO dues_settings (setting_key, setting_value, description) VALUES ('reminder_days_before', '30,7', 'Days before due date to send reminders (comma-separated)'), ('grace_period_days', '30', 'Days after due date before auto-inactive'), ('overdue_reminder_interval', '14', 'Days between overdue reminder emails'), ('payment_iban', 'MC58 1756 9000 0104 0050 1001 860', 'IBAN for dues payment'), ('payment_account_holder', 'ASSOCIATION MONACO USA', 'Account holder name'), ('payment_instructions', 'Please include your Member ID in the reference', 'Payment instructions'); ``` ### 2.5 Automatic Reminders **Reminder Schedule (configurable via settings):** 1. **30 days before** due date: "Your dues are coming up" 2. **7 days before** due date: "Reminder: dues due in 1 week" 3. **On due date**: "Your dues are now due" 4. **Every 14 days overdue**: "Your dues are overdue" (until grace period ends) **Email Content Includes:** - Member name - Amount due (from membership_type) - Due date - IBAN and account holder - Payment reference suggestion (Member ID) - Link to portal **Technical Implementation:** - Supabase Edge Function runs daily - Checks all members for reminder triggers - Logs sent emails in `email_logs` table - Respects settings for intervals ### 2.6 Overdue Handling **Grace Period Flow:** ``` Due Date Passed │ ▼ ┌─────────────────────────────────────────┐ │ GRACE PERIOD (configurable, default 30 days) │ │ - Status remains 'active' │ │ - Overdue reminders sent │ │ - Flagged in dashboard │ └─────────────────────────────────────────┘ │ ▼ (grace period ends) ┌─────────────────────────────────────────┐ │ AUTO STATUS CHANGE │ │ - Status → 'inactive' │ │ - Final notification email │ │ - Member loses active access │ └─────────────────────────────────────────┘ ``` **Supabase Edge Function for Auto-Update:** ```typescript // Runs daily via cron async function updateOverdueMembers() { const gracePeriodDays = await getSetting('grace_period_days'); const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - gracePeriodDays); // Find members past grace period const { data: overdueMembers } = await supabase .from('members_with_dues') .select('*') .eq('membership_status', 'active') .lt('current_due_date', cutoffDate.toISOString()); // Update each to inactive for (const member of overdueMembers) { await supabase .from('members') .update({ membership_status_id: inactiveStatusId }) .eq('id', member.id); // Send final notification await sendEmail(member.email, 'membership_lapsed', { ... }); } } ``` ### 2.7 Payment History (Member Visible) Members can see their complete payment history: **Display includes:** - Payment date - Amount paid - Due date (period covered) - Reference number - Payment method **Members CANNOT see:** - Who recorded the payment - Internal notes - Other members' payments ### 2.8 Dues Dashboard (Board/Admin) **Overview Stats:** - Total members with current dues - Members with dues due soon (next 30 days) - Overdue members count - Total collected this year **Filterable Member List:** - Filter by: status (current, due soon, overdue, never paid) - Sort by: due date, days overdue, member name - Quick actions: Record payment, Send reminder **Individual Member View:** - Full payment history - Current dues status - Quick record payment form - Send manual reminder button ### 2.9 Complete Dues Schema ```sql -- Dues payments table CREATE TABLE public.dues_payments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), member_id UUID NOT NULL REFERENCES public.members(id) ON DELETE CASCADE, amount DECIMAL(10,2) NOT NULL, currency TEXT DEFAULT 'EUR', payment_date DATE NOT NULL, due_date DATE NOT NULL, -- Calculated: payment_date + 1 year payment_method TEXT DEFAULT 'bank_transfer', reference TEXT, -- Bank transfer reference notes TEXT, -- Internal notes recorded_by UUID NOT NULL REFERENCES public.members(id), created_at TIMESTAMPTZ DEFAULT NOW() ); -- Trigger to auto-calculate due_date CREATE OR REPLACE FUNCTION calculate_due_date() RETURNS TRIGGER AS $$ BEGIN NEW.due_date := NEW.payment_date + INTERVAL '1 year'; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER set_due_date BEFORE INSERT ON public.dues_payments FOR EACH ROW WHEN (NEW.due_date IS NULL) EXECUTE FUNCTION calculate_due_date(); -- After payment: update member status to active CREATE OR REPLACE FUNCTION update_member_status_on_payment() RETURNS TRIGGER AS $$ DECLARE active_status_id UUID; BEGIN -- Get active status ID SELECT id INTO active_status_id FROM public.membership_statuses WHERE name = 'active'; -- Update member status UPDATE public.members SET membership_status_id = active_status_id, updated_at = NOW() WHERE id = NEW.member_id; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER activate_member_on_payment AFTER INSERT ON public.dues_payments FOR EACH ROW EXECUTE FUNCTION update_member_status_on_payment(); -- Computed view for dues status CREATE VIEW public.members_with_dues AS SELECT m.*, ms.name as status_name, ms.display_name as status_display_name, ms.color as status_color, mt.display_name as membership_type_name, mt.annual_dues, dp.last_payment_date, dp.current_due_date, CASE WHEN dp.current_due_date IS NULL THEN 'never_paid' WHEN dp.current_due_date < CURRENT_DATE THEN 'overdue' WHEN dp.current_due_date < CURRENT_DATE + INTERVAL '30 days' THEN 'due_soon' ELSE 'current' END as dues_status, CASE WHEN dp.current_due_date < CURRENT_DATE THEN (CURRENT_DATE - dp.current_due_date)::INTEGER ELSE NULL END as days_overdue, CASE WHEN dp.current_due_date >= CURRENT_DATE THEN (dp.current_due_date - CURRENT_DATE)::INTEGER ELSE NULL END as days_until_due FROM public.members m LEFT JOIN public.membership_statuses ms ON m.membership_status_id = ms.id LEFT JOIN public.membership_types mt ON m.membership_type_id = mt.id LEFT JOIN LATERAL ( SELECT payment_date as last_payment_date, due_date as current_due_date FROM public.dues_payments WHERE member_id = m.id ORDER BY due_date DESC LIMIT 1 ) dp ON true; ``` ### 2.10 Email Templates for Dues **Types:** 1. `dues_reminder` - Upcoming dues reminder 2. `dues_due_today` - Dues due today 3. `dues_overdue` - Overdue reminder 4. `dues_lapsed` - Membership lapsed (grace period ended) 5. `dues_received` - Payment confirmation **Template Variables:** - `{{member_name}}` - Full name - `{{member_id}}` - MUSA-XXXX - `{{amount}}` - Due amount - `{{due_date}}` - Formatted date - `{{days_until_due}}` or `{{days_overdue}}` - `{{iban}}` - Payment IBAN - `{{account_holder}}` - Account name - `{{portal_link}}` - Link to portal --- ## 3. EVENTS SYSTEM (Detailed) ### 3.1 Event Types (Admin-Configurable) ```sql CREATE TABLE public.event_types ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, color TEXT NOT NULL DEFAULT '#3b82f6', -- Tailwind blue-500 icon TEXT, -- Lucide icon name description TEXT, is_active BOOLEAN DEFAULT TRUE, sort_order INTEGER DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW() ); -- Default event types INSERT INTO event_types (name, display_name, color, icon) VALUES ('social', 'Social Event', '#10b981', 'party-popper'), ('meeting', 'Meeting', '#6366f1', 'users'), ('fundraiser', 'Fundraiser', '#f59e0b', 'heart-handshake'), ('workshop', 'Workshop', '#8b5cf6', 'graduation-cap'), ('gala', 'Gala/Formal', '#ec4899', 'sparkles'), ('other', 'Other', '#6b7280', 'calendar'); ``` ### 3.2 Event Visibility **Visibility Options:** | Level | Who Can See | Description | |-------|-------------|-------------| | `public` | Anyone | Visible on public events page (no login) | | `members` | All logged-in members | Default for most events | | `board` | Board + Admin only | Board meetings, internal events | | `admin` | Admin only | Administrative events | ### 3.3 Event Pricing **Pricing Model:** - Each event can be free or paid - Paid events have **member price** and **non-member price** - Member pricing determined by `membership_type_id` (if tiered pricing enabled) - Non-members pay non-member price always **Pricing Fields:** ```sql is_paid BOOLEAN DEFAULT FALSE, member_price DECIMAL(10,2) DEFAULT 0, non_member_price DECIMAL(10,2) DEFAULT 0, pricing_notes TEXT -- "Includes dinner and drinks" ``` ### 3.4 Guest/+1 Handling **Per-Event Configuration:** - `max_guests_per_member` - 0, 1, 2, 3, or unlimited - Each RSVP tracks guest count and guest names - Guests count toward total capacity - Non-members can bring guests too (if enabled) ### 3.5 Non-Member (Public) RSVP **Flow for public events:** ``` ┌─────────────────┐ ┌──────────────────┐ │ Public Events │────▶│ Event Detail │ │ Page (no login) │ │ (public visible) │ └─────────────────┘ └──────────────────┘ │ ▼ ┌──────────────────┐ │ RSVP Form │ │ (no account) │ │ - Name │ │ - Email │ │ - Phone │ │ - Guest count │ │ - Guest names │ └──────────────────┘ │ ▼ ┌──────────────────┐ │ Payment Info │ │ (if paid event) │ │ - IBAN shown │ │ - Reference # │ └──────────────────┘ │ ▼ ┌──────────────────┐ │ RSVP Confirmed │ │ (pending payment)│ │ Email sent │ └──────────────────┘ ``` **Non-Member RSVP Table:** ```sql CREATE TABLE public.event_rsvps_public ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), event_id UUID NOT NULL REFERENCES public.events(id) ON DELETE CASCADE, -- Contact info (required) full_name TEXT NOT NULL, email TEXT NOT NULL, phone TEXT, -- RSVP details status TEXT NOT NULL DEFAULT 'confirmed' CHECK (status IN ('confirmed', 'declined', 'maybe', 'waitlist', 'cancelled')), guest_count INTEGER DEFAULT 0, guest_names TEXT[], -- Payment (for paid events) payment_status TEXT DEFAULT 'not_required' CHECK (payment_status IN ('not_required', 'pending', 'paid')), payment_reference TEXT, payment_amount DECIMAL(10,2), -- Attendance attended BOOLEAN DEFAULT FALSE, -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(event_id, email) -- One RSVP per email per event ); ``` ### 3.6 RSVP Status Options **For Members and Non-Members:** | Status | Description | |--------|-------------| | `confirmed` | Attending the event | | `declined` | Not attending | | `maybe` | Tentative/undecided | | `waitlist` | Event full, on waitlist | | `cancelled` | Cancelled RSVP | ### 3.7 Capacity & Waitlist **Capacity Management:** - `max_attendees` - Total spots (null = unlimited) - Includes members + guests + non-members + their guests - When full, new RSVPs go to waitlist **Auto-Promote Waitlist:** ```typescript // Trigger when RSVP is cancelled or declined async function promoteFromWaitlist(eventId: string) { // Get event capacity const event = await getEvent(eventId); const currentCount = await getCurrentAttendeeCount(eventId); if (event.max_attendees && currentCount >= event.max_attendees) { return; // Still full } // Get oldest waitlist entry const waitlisted = await supabase .from('event_rsvps') .select('*') .eq('event_id', eventId) .eq('status', 'waitlist') .order('created_at', { ascending: true }) .limit(1) .single(); if (waitlisted) { // Promote to confirmed await supabase .from('event_rsvps') .update({ status: 'confirmed' }) .eq('id', waitlisted.id); // Send notification email await sendEmail(waitlisted.member.email, 'waitlist_promoted', { event_title: event.title, event_date: event.start_datetime }); } } ``` ### 3.8 Attendance Tracking **Check-in System:** - Board/Admin can mark attendance after event - Checkbox per RSVP: attended yes/no - Track attendance rate per event - Member attendance history viewable ```sql -- Add to RSVPs attended BOOLEAN DEFAULT FALSE, checked_in_at TIMESTAMPTZ, checked_in_by UUID REFERENCES public.members(id) ``` ### 3.9 Calendar Views **Available Views:** 1. **Month** - Traditional calendar grid 2. **Week** - Weekly schedule view 3. **Day** - Single day detailed view 4. **List** - Upcoming events list **Using FullCalendar (SvelteKit compatible):** ```typescript import Calendar from '@event-calendar/core'; import TimeGrid from '@event-calendar/time-grid'; import DayGrid from '@event-calendar/day-grid'; import List from '@event-calendar/list'; ``` ### 3.10 Event Schema ```sql CREATE TABLE public.events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Basic Info title TEXT NOT NULL, description TEXT, event_type_id UUID REFERENCES public.event_types(id), -- Date/Time start_datetime TIMESTAMPTZ NOT NULL, end_datetime TIMESTAMPTZ NOT NULL, all_day BOOLEAN DEFAULT FALSE, timezone TEXT DEFAULT 'Europe/Monaco', -- Location location TEXT, location_url TEXT, -- Google Maps link, etc. -- Capacity max_attendees INTEGER, -- null = unlimited max_guests_per_member INTEGER DEFAULT 1, -- Pricing is_paid BOOLEAN DEFAULT FALSE, member_price DECIMAL(10,2) DEFAULT 0, non_member_price DECIMAL(10,2) DEFAULT 0, pricing_notes TEXT, -- Visibility visibility TEXT NOT NULL DEFAULT 'members' CHECK (visibility IN ('public', 'members', 'board', 'admin')), -- Status status TEXT NOT NULL DEFAULT 'published' CHECK (status IN ('draft', 'published', 'cancelled', 'completed')), -- Media cover_image_url TEXT, -- Event banner/cover image -- Meta created_by UUID NOT NULL REFERENCES public.members(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Member RSVPs CREATE TABLE public.event_rsvps ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), event_id UUID NOT NULL REFERENCES public.events(id) ON DELETE CASCADE, member_id UUID NOT NULL REFERENCES public.members(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'confirmed' CHECK (status IN ('confirmed', 'declined', 'maybe', 'waitlist', 'cancelled')), guest_count INTEGER DEFAULT 0, guest_names TEXT[], notes TEXT, -- Payment (for paid events) payment_status TEXT DEFAULT 'not_required' CHECK (payment_status IN ('not_required', 'pending', 'paid')), payment_reference TEXT, payment_amount DECIMAL(10,2), -- Attendance attended BOOLEAN DEFAULT FALSE, checked_in_at TIMESTAMPTZ, checked_in_by UUID REFERENCES public.members(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(event_id, member_id) ); -- View for event with counts CREATE VIEW public.events_with_counts AS SELECT e.*, et.display_name as event_type_name, et.color as event_type_color, et.icon as event_type_icon, COALESCE(member_rsvps.confirmed_count, 0) + COALESCE(member_rsvps.guest_count, 0) + COALESCE(public_rsvps.confirmed_count, 0) + COALESCE(public_rsvps.guest_count, 0) as total_attendees, COALESCE(member_rsvps.confirmed_count, 0) as member_count, COALESCE(public_rsvps.confirmed_count, 0) as non_member_count, COALESCE(member_rsvps.waitlist_count, 0) + COALESCE(public_rsvps.waitlist_count, 0) as waitlist_count, CASE WHEN e.max_attendees IS NULL THEN FALSE WHEN (COALESCE(member_rsvps.confirmed_count, 0) + COALESCE(member_rsvps.guest_count, 0) + COALESCE(public_rsvps.confirmed_count, 0) + COALESCE(public_rsvps.guest_count, 0)) >= e.max_attendees THEN TRUE ELSE FALSE END as is_full FROM public.events e LEFT JOIN public.event_types et ON e.event_type_id = et.id LEFT JOIN LATERAL ( SELECT COUNT(*) FILTER (WHERE status = 'confirmed') as confirmed_count, COALESCE(SUM(guest_count) FILTER (WHERE status = 'confirmed'), 0) as guest_count, COUNT(*) FILTER (WHERE status = 'waitlist') as waitlist_count FROM public.event_rsvps WHERE event_id = e.id ) member_rsvps ON true LEFT JOIN LATERAL ( SELECT COUNT(*) FILTER (WHERE status = 'confirmed') as confirmed_count, COALESCE(SUM(guest_count) FILTER (WHERE status = 'confirmed'), 0) as guest_count, COUNT(*) FILTER (WHERE status = 'waitlist') as waitlist_count FROM public.event_rsvps_public WHERE event_id = e.id ) public_rsvps ON true; ``` ### 3.11 Event Permissions | Action | Member | Board | Admin | |--------|--------|-------|-------| | View public events | - | - | - | | View member events | ✓ | ✓ | ✓ | | View board events | - | ✓ | ✓ | | View admin events | - | - | ✓ | | RSVP to events | ✓ | ✓ | ✓ | | Create events | - | ✓ | ✓ | | Edit own events | - | ✓ | ✓ | | Edit any event | - | - | ✓ | | Delete events | - | - | ✓ | | Manage RSVPs | - | ✓ | ✓ | | Track attendance | - | ✓ | ✓ | ### 3.12 Event Email Notifications **Email Types:** 1. `event_created` - New event announcement (for public/member events) 2. `event_reminder` - Reminder before event (configurable: 1 day, 1 hour) 3. `event_updated` - Event details changed 4. `event_cancelled` - Event cancelled 5. `rsvp_confirmation` - RSVP received 6. `waitlist_promoted` - Promoted from waitlist 7. `event_payment_reminder` - Payment reminder for paid events **Template Variables:** - `{{event_title}}`, `{{event_date}}`, `{{event_time}}` - `{{event_location}}`, `{{event_description}}` - `{{member_name}}`, `{{guest_count}}` - `{{payment_amount}}`, `{{payment_iban}}` - `{{rsvp_status}}`, `{{portal_link}}` --- ## 4. AUTH & DASHBOARDS (Detailed) ### 4.1 Authentication Method **Email/Password only** (no social login): - Standard email + password signup/login - Email verification required - Password reset via email - Remember me option (extended session) ### 4.2 Login Page Design **Branded login with:** - Monaco USA logo - Association tagline - Login form (email, password, remember me) - Links: Forgot password, Sign up - Glass-morphism styling - Responsive (mobile-friendly) ### 4.3 Auth Flow ``` ┌──────────────────────────────────────────────────────────────┐ │ SIGNUP FLOW │ ├──────────────────────────────────────────────────────────────┤ │ /signup │ │ ├── Full form (all required fields) │ │ ├── Supabase Auth: signUp(email, password) │ │ ├── Create member record (status: pending) │ │ ├── Send verification email │ │ └── Show "Check your email" message │ │ │ │ /auth/callback (email verification link) │ │ ├── Verify email token │ │ ├── Update email_verified = true │ │ └── Redirect to /login with success message │ └──────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────┐ │ LOGIN FLOW │ ├──────────────────────────────────────────────────────────────┤ │ /login │ │ ├── Email + Password form │ │ ├── Supabase Auth: signInWithPassword() │ │ ├── Set session cookie (via Supabase SSR) │ │ ├── Fetch member record │ │ └── Redirect to /dashboard │ └──────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────┐ │ PASSWORD RESET │ ├──────────────────────────────────────────────────────────────┤ │ /forgot-password │ │ ├── Email input form │ │ ├── Supabase Auth: resetPasswordForEmail() │ │ └── Show "Check your email" message │ │ │ │ /auth/reset-password (from email link) │ │ ├── New password form │ │ ├── Supabase Auth: updateUser({ password }) │ │ └── Redirect to /login with success │ └──────────────────────────────────────────────────────────────┘ ``` ### 4.4 Session Management **Supabase SSR Configuration:** ```typescript // src/hooks.server.ts export const handle: Handle = async ({ event, resolve }) => { event.locals.supabase = createServerClient( PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { cookies: { getAll: () => event.cookies.getAll(), setAll: (cookies) => cookies.forEach(({ name, value, options }) => event.cookies.set(name, value, { ...options, path: '/' }) ) } } ); event.locals.safeGetSession = async () => { const { data: { session } } = await event.locals.supabase.auth.getSession(); if (!session) return { session: null, user: null, member: null }; const { data: { user } } = await event.locals.supabase.auth.getUser(); if (!user) return { session: null, user: null, member: null }; // Fetch member record const { data: member } = await event.locals.supabase .from('members_with_dues') .select('*') .eq('id', user.id) .single(); return { session, user, member }; }; return resolve(event); }; ``` ### 4.5 Navigation Structure **Desktop: Collapsible Sidebar** ``` ┌─────────────────────────────────────────────────────┐ │ ┌─────┐ │ │ │ │ Dashboard │ │ │LOGO │ ───────────────────────────────────── │ │ │ │ │ │ └─────┘ [Sidebar Navigation] [Content] │ │ │ │ 📊 Dashboard │ │ 👤 My Profile │ │ 📅 Events │ │ 💳 Payments │ │ │ │ ── Board ──────── (if board/admin) │ │ 👥 Members │ │ 📋 Dues Management │ │ 📅 Event Management │ │ │ │ ── Admin ──────── (if admin) │ │ ⚙️ Settings │ │ 👥 User Management │ │ 📄 Documents │ │ │ │ ───────────────── │ │ 🚪 Logout │ └─────────────────────────────────────────────────────┘ ``` **Mobile: Bottom Navigation Bar** ``` ┌─────────────────────────────────────┐ │ │ │ [Main Content] │ │ │ │ │ ├─────────────────────────────────────┤ │ 🏠 📅 👤 ⚙️ ☰ │ │ Home Events Profile Settings More │ └─────────────────────────────────────┘ ``` ### 4.6 Unified Dashboard with Role Sections **Single `/dashboard` route with role-based sections:** ```svelte {#if isBoard} {/if} {#if isAdmin} {/if} ``` ### 4.7 Member Dashboard Section **Components:** 1. **Welcome Card** - Greeting with name, membership status badge 2. **Dues Status Card** - Current status, next due date, quick pay info 3. **Upcoming Events Card** - Next 3-5 events with RSVP status 4. **Profile Quick View** - Photo, basic info, edit link **Data Loaded:** ```typescript // routes/(app)/dashboard/+page.server.ts export const load = async ({ locals }) => { const { member } = await locals.safeGetSession(); const upcomingEvents = await getUpcomingEventsForMember(member.id, 5); return { member, upcomingEvents }; }; ``` ### 4.8 Board Dashboard Section **Additional Components (visible to board/admin):** 1. **Member Stats Card** - Total, active, pending, inactive counts 2. **Pending Members Card** - New signups awaiting approval/payment 3. **Dues Overview Card** - Current, due soon, overdue breakdown 4. **Recent RSVPs Card** - Latest event RSVPs **Board Stats:** ```typescript interface BoardStats { totalMembers: number; activeMembers: number; pendingMembers: number; inactiveMembers: number; duesSoon: number; // Due in next 30 days duesOverdue: number; // Past due date upcomingEvents: number; pendingRsvps: number; } ``` ### 4.9 Admin Dashboard Section **Additional Components (admin only):** 1. **System Health Card** - Supabase status, email status 2. **Recent Activity Card** - Latest logins, signups, payments 3. **Quick Actions Card** - Add member, create event, send broadcast 4. **Alerts Card** - Issues requiring attention **Admin Stats:** ```typescript interface AdminStats extends BoardStats { totalUsers: number; // Auth users recentLogins: number; // Last 24 hours failedLogins: number; // Last 24 hours emailsSent: number; // This month storageUsed: number; // MB } ``` ### 4.10 Route Protection **Layout-level guards using SvelteKit:** ```typescript // routes/(app)/+layout.server.ts import { redirect } from '@sveltejs/kit'; export const load = async ({ locals }) => { const { session, member } = await locals.safeGetSession(); if (!session) { throw redirect(303, '/login'); } return { member }; }; // routes/(app)/board/+layout.server.ts export const load = async ({ locals, parent }) => { const { member } = await parent(); if (member.role !== 'board' && member.role !== 'admin') { throw redirect(303, '/dashboard'); } return {}; }; // routes/(app)/admin/+layout.server.ts export const load = async ({ locals, parent }) => { const { member } = await parent(); if (member.role !== 'admin') { throw redirect(303, '/dashboard'); } return {}; }; ``` ### 4.11 Responsive Breakpoints | Breakpoint | Width | Layout | |------------|-------|--------| | Mobile | < 640px | Bottom nav, stacked cards | | Tablet | 640-1024px | Collapsed sidebar rail, 2-column | | Desktop | > 1024px | Full sidebar, 3-column grid | ### 4.12 Dashboard Glass-Morphism Design **Glass Card Base Style:** ```css .glass-card { background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); } .glass-card-dark { background: rgba(0, 0, 0, 0.3); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); } ``` **Monaco Red Accent:** ```css :root { --monaco-red: #dc2626; --monaco-red-light: #fee2e2; --monaco-red-dark: #991b1b; } ``` --- ## 5. DOCUMENT STORAGE (Detailed) ### 5.1 Document Categories (Admin-Configurable) ```sql CREATE TABLE public.document_categories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, description TEXT, icon TEXT, -- Lucide icon name sort_order INTEGER DEFAULT 0, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW() ); -- Default categories INSERT INTO document_categories (name, display_name, icon) VALUES ('meeting_minutes', 'Meeting Minutes', 'file-text'), ('governance', 'Governance & Bylaws', 'scale'), ('legal', 'Legal Documents', 'briefcase'), ('financial', 'Financial Reports', 'dollar-sign'), ('member_resources', 'Member Resources', 'book-open'), ('forms', 'Forms & Templates', 'clipboard'), ('other', 'Other Documents', 'file'); ``` ### 5.2 Upload Permissions **Who can upload:** - Board members - Administrators **Members cannot upload** - they can only view documents shared with them. ### 5.3 Document Visibility (Per-Document) **Visibility Options:** | Level | Who Can View | |-------|--------------| | `public` | Anyone (no login required) | | `members` | All logged-in members | | `board` | Board + Admin only | | `admin` | Admin only | **Custom permissions** can also specify specific member IDs for restricted access. ### 5.4 Document Schema ```sql CREATE TABLE public.documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Basic Info title TEXT NOT NULL, description TEXT, category_id UUID REFERENCES public.document_categories(id), -- File Info (Supabase Storage) file_path TEXT NOT NULL, -- Storage path file_name TEXT NOT NULL, -- Original filename file_size INTEGER NOT NULL, -- Bytes mime_type TEXT NOT NULL, -- 'application/pdf', etc. -- Visibility visibility TEXT NOT NULL DEFAULT 'members' CHECK (visibility IN ('public', 'members', 'board', 'admin')), -- Optional: Specific member access (for restricted docs) allowed_member_ids UUID[], -- If set, only these members can view -- Version tracking version INTEGER DEFAULT 1, replaces_document_id UUID REFERENCES public.documents(id), -- Metadata uploaded_by UUID NOT NULL REFERENCES public.members(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Document access log (for audit) CREATE TABLE public.document_access_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), document_id UUID NOT NULL REFERENCES public.documents(id) ON DELETE CASCADE, accessed_by UUID REFERENCES public.members(id), -- null if public access access_type TEXT NOT NULL CHECK (access_type IN ('view', 'download')), ip_address TEXT, accessed_at TIMESTAMPTZ DEFAULT NOW() ); ``` ### 5.5 File Storage (Supabase Storage) **Bucket Configuration:** ```typescript // Storage bucket: 'documents' // Path structure: documents/{category}/{year}/{filename} // Example paths: // documents/meeting_minutes/2026/board-meeting-2026-01-15.pdf // documents/governance/bylaws-v2.pdf // documents/financial/2025/annual-report-2025.pdf ``` **Upload Limits:** - Max file size: 50MB - Allowed types: PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, TXT, JPG, PNG ### 5.6 Document UI Features **Document Library View:** - Filter by category - Filter by visibility level - Search by title/description - Sort by date, name, category - Grid or list view toggle **Document Card:** ``` ┌────────────────────────────────────────┐ │ 📄 [Category Icon] │ │ │ │ Board Meeting Minutes - January 2026 │ │ Meeting minutes from the monthly... │ │ │ │ 📅 Jan 15, 2026 | 📎 PDF | 1.2 MB │ │ │ │ [View] [Download] 👁️ Members │ └────────────────────────────────────────┘ ``` **Upload Form (Board/Admin):** - Title (required) - Description (optional) - Category (required, dropdown) - Visibility (required) - Custom access (optional, member multi-select) - File upload (drag & drop) ### 5.7 Document Permissions (RLS) ```sql -- RLS Policies for documents ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY; -- Public documents viewable by anyone CREATE POLICY "Public documents are viewable" ON public.documents FOR SELECT USING (visibility = 'public'); -- Member documents viewable by authenticated users CREATE POLICY "Member documents viewable by members" ON public.documents FOR SELECT TO authenticated USING ( visibility = 'members' OR visibility = 'public' OR (visibility = 'board' AND EXISTS ( SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin') )) OR (visibility = 'admin' AND EXISTS ( SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin' )) OR (allowed_member_ids IS NOT NULL AND auth.uid() = ANY(allowed_member_ids)) ); -- Board/Admin can manage documents CREATE POLICY "Board can upload documents" ON public.documents FOR INSERT TO authenticated WITH CHECK ( EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin')) ); CREATE POLICY "Uploader or admin can update documents" ON public.documents FOR UPDATE TO authenticated USING ( uploaded_by = auth.uid() OR EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin') ); CREATE POLICY "Admin can delete documents" ON public.documents FOR DELETE TO authenticated USING ( EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin') ); ``` ### 5.8 Version History **Document versioning:** - When replacing a document, create new record with `replaces_document_id` - Previous versions remain accessible (archived) - View version history for any document ```sql -- Get version history for a document SELECT d.*, m.first_name, m.last_name FROM public.documents d JOIN public.members m ON d.uploaded_by = m.id WHERE d.id = :document_id OR d.replaces_document_id = :document_id OR d.id IN ( SELECT replaces_document_id FROM public.documents WHERE id = :document_id ) ORDER BY d.version DESC; ``` ### 5.9 Meeting Minutes Special Handling **For meeting minutes category:** - Date field (meeting date) - Attendees list (optional) - Agenda reference (optional) - Quick template for consistency ```sql -- Optional meeting minutes metadata ALTER TABLE public.documents ADD COLUMN meeting_date DATE; ALTER TABLE public.documents ADD COLUMN meeting_attendees UUID[]; ``` --- ## 6. ADMIN SETTINGS SYSTEM (Detailed) ### 6.1 Settings Architecture Overview **Centralized configuration** for all customizable aspects of the portal, accessible only to Admins via `/admin/settings`. **Settings Categories:** 1. **Organization** - Association branding and info 2. **Membership** - Statuses, types, and pricing 3. **Dues** - Payment settings and reminders 4. **Events** - Event types and defaults 5. **Documents** - Categories and storage 6. **Directory** - Visibility controls 7. **Email** - SMTP and template settings 8. **System** - Technical settings ### 6.2 Settings Storage (Unified Table) ```sql -- Flexible key-value settings with JSON support CREATE TABLE public.app_settings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), category TEXT NOT NULL, -- 'organization', 'dues', 'email', etc. setting_key TEXT NOT NULL, -- 'payment_iban', 'reminder_days', etc. setting_value JSONB NOT NULL, -- Supports strings, numbers, arrays, objects setting_type TEXT NOT NULL DEFAULT 'text' -- 'text', 'number', 'boolean', 'json', 'array' CHECK (setting_type IN ('text', 'number', 'boolean', 'json', 'array')), display_name TEXT NOT NULL, -- Human-readable label description TEXT, -- Help text for admins is_public BOOLEAN DEFAULT FALSE, -- If true, accessible without auth updated_at TIMESTAMPTZ DEFAULT NOW(), updated_by UUID REFERENCES public.members(id), UNIQUE(category, setting_key) ); -- Audit log for settings changes CREATE TABLE public.settings_audit_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), setting_id UUID NOT NULL REFERENCES public.app_settings(id), old_value JSONB, new_value JSONB NOT NULL, changed_by UUID NOT NULL REFERENCES public.members(id), changed_at TIMESTAMPTZ DEFAULT NOW(), change_reason TEXT ); -- RLS: Only admins can read/write settings ALTER TABLE public.app_settings ENABLE ROW LEVEL SECURITY; CREATE POLICY "Only admins can manage settings" ON public.app_settings FOR ALL TO authenticated USING ( EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin') OR is_public = TRUE ); ``` ### 6.3 Default Settings (Seeded on First Run) ```sql -- Organization Settings INSERT INTO app_settings (category, setting_key, setting_value, setting_type, display_name, description, is_public) VALUES ('organization', 'association_name', '"Monaco USA"', 'text', 'Association Name', 'Official name of the association', true), ('organization', 'tagline', '"Americans in Monaco"', 'text', 'Tagline', 'Association tagline shown on login', true), ('organization', 'contact_email', '"contact@monacousa.org"', 'text', 'Contact Email', 'Public contact email address', true), ('organization', 'address', '"Monaco"', 'text', 'Address', 'Association physical address', true), ('organization', 'logo_url', '"/logo.png"', 'text', 'Logo URL', 'Path to association logo', true), ('organization', 'primary_color', '"#dc2626"', 'text', 'Primary Color', 'Brand primary color (hex)', true), -- Dues Settings ('dues', 'payment_iban', '"MC58 1756 9000 0104 0050 1001 860"', 'text', 'Payment IBAN', 'Bank IBAN for dues payment', false), ('dues', 'payment_account_holder', '"ASSOCIATION MONACO USA"', 'text', 'Account Holder', 'Bank account holder name', false), ('dues', 'payment_bank_name', '"Credit Foncier de Monaco"', 'text', 'Bank Name', 'Name of the bank', false), ('dues', 'payment_instructions', '"Please include your Member ID (MUSA-XXXX) in the reference"', 'text', 'Payment Instructions', 'Instructions shown to members', false), ('dues', 'reminder_days_before', '[30, 7, 1]', 'array', 'Reminder Days', 'Days before due date to send reminders', false), ('dues', 'grace_period_days', '30', 'number', 'Grace Period', 'Days after due date before auto-inactive', false), ('dues', 'overdue_reminder_interval', '14', 'number', 'Overdue Reminder Interval', 'Days between overdue reminder emails', false), ('dues', 'auto_inactive_enabled', 'true', 'boolean', 'Auto Inactive', 'Automatically set members inactive after grace period', false), -- Event Settings ('events', 'default_max_guests', '2', 'number', 'Default Max Guests', 'Default maximum guests per RSVP', false), ('events', 'reminder_hours_before', '[24, 1]', 'array', 'Event Reminder Hours', 'Hours before event to send reminders', false), ('events', 'allow_public_rsvp', 'true', 'boolean', 'Allow Public RSVP', 'Allow non-members to RSVP to public events', false), ('events', 'auto_close_rsvp_hours', '0', 'number', 'Auto Close RSVP', 'Hours before event to close RSVP (0 = never)', false), -- Directory Settings ('directory', 'member_visible_fields', '["first_name", "last_name", "avatar_url", "nationality", "member_since"]', 'array', 'Member Visible Fields', 'Fields visible to regular members', false), ('directory', 'board_visible_fields', '["first_name", "last_name", "avatar_url", "nationality", "email", "phone", "address", "date_of_birth", "member_since", "membership_status"]', 'array', 'Board Visible Fields', 'Fields visible to board members', false), ('directory', 'show_membership_status', 'false', 'boolean', 'Show Status to Members', 'Show membership status in directory for regular members', false), -- System Settings ('system', 'maintenance_mode', 'false', 'boolean', 'Maintenance Mode', 'Put the portal in maintenance mode', false), ('system', 'maintenance_message', '"The portal is currently undergoing maintenance. Please check back soon."', 'text', 'Maintenance Message', 'Message shown during maintenance', false), ('system', 'session_timeout_hours', '168', 'number', 'Session Timeout', 'Hours until session expires (default: 7 days)', false), ('system', 'max_upload_size_mb', '50', 'number', 'Max Upload Size', 'Maximum file upload size in MB', false), ('system', 'allowed_file_types', '["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "jpg", "jpeg", "png", "webp"]', 'array', 'Allowed File Types', 'Allowed file extensions for uploads', false); ``` ### 6.4 Settings UI Layout **Navigation Tabs:** ``` ┌──────────────────────────────────────────────────────────────────┐ │ ⚙️ Settings │ ├──────────────────────────────────────────────────────────────────┤ │ [Organization] [Membership] [Dues] [Events] [Documents] │ │ [Directory] [Email] [System] │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Organization Settings │ │ │ │ ───────────────────────────────────────────────────────── │ │ │ │ │ │ │ │ Association Name │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ │ Monaco USA │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ Official name of the association │ │ │ │ │ │ │ │ Tagline │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ │ Americans in Monaco │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ Association tagline shown on login │ │ │ │ │ │ │ │ Primary Color │ │ │ │ ┌────────┐ ┌──────────────────────────────────────────┐ │ │ │ │ │ 🎨 │ │ #dc2626 │ │ │ │ │ └────────┘ └──────────────────────────────────────────┘ │ │ │ │ │ │ │ │ [Save Changes] │ │ │ └─────────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘ ``` ### 6.5 Membership Settings Tab **Manages configurable membership statuses and types:** ``` ┌──────────────────────────────────────────────────────────────────┐ │ Membership Settings │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ MEMBERSHIP STATUSES │ │ ───────────────────────────────────────────────────────────── │ │ │ │ ┌───────────┬─────────────┬──────────┬────────────┬──────────┐ │ │ │ Name │ Display │ Color │ Is Default │ Actions │ │ │ ├───────────┼─────────────┼──────────┼────────────┼──────────┤ │ │ │ pending │ Pending │ 🟡 Yellow│ ✓ │ ✏️ 🗑️ │ │ │ │ active │ Active │ 🟢 Green │ │ ✏️ 🗑️ │ │ │ │ inactive │ Inactive │ ⚪ Gray │ │ ✏️ 🗑️ │ │ │ │ expired │ Expired │ 🔴 Red │ │ ✏️ 🗑️ │ │ │ └───────────┴─────────────┴──────────┴────────────┴──────────┘ │ │ │ │ [+ Add Status] │ │ │ │ ───────────────────────────────────────────────────────────── │ │ │ │ MEMBERSHIP TYPES │ │ ───────────────────────────────────────────────────────────── │ │ │ │ ┌───────────┬───────────────┬──────────┬────────────┬────────┐ │ │ │ Name │ Display │ Annual € │ Is Default │Actions │ │ │ ├───────────┼───────────────┼──────────┼────────────┼────────┤ │ │ │ regular │ Regular │ €50.00 │ ✓ │ ✏️ 🗑️ │ │ │ │ student │ Student │ €25.00 │ │ ✏️ 🗑️ │ │ │ │ senior │ Senior (65+) │ €35.00 │ │ ✏️ 🗑️ │ │ │ │ family │ Family │ €75.00 │ │ ✏️ 🗑️ │ │ │ │ honorary │ Honorary │ €0.00 │ │ ✏️ 🗑️ │ │ │ └───────────┴───────────────┴──────────┴────────────┴────────┘ │ │ │ │ [+ Add Membership Type] │ │ │ └──────────────────────────────────────────────────────────────────┘ ``` ### 6.6 Event Types Settings **Admin can manage event types with colors and icons:** ``` ┌──────────────────────────────────────────────────────────────────┐ │ Event Types │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┬───────────────┬────────────┬────────┬────────┐ │ │ │ Name │ Display │ Color │ Icon │Actions │ │ │ ├─────────────┼───────────────┼────────────┼────────┼────────┤ │ │ │ social │ Social Event │ 🟢 #10b981 │ 🎉 │ ✏️ 🗑️ │ │ │ │ meeting │ Meeting │ 🔵 #6366f1 │ 👥 │ ✏️ 🗑️ │ │ │ │ fundraiser │ Fundraiser │ 🟠 #f59e0b │ 💝 │ ✏️ 🗑️ │ │ │ │ workshop │ Workshop │ 🟣 #8b5cf6 │ 🎓 │ ✏️ 🗑️ │ │ │ │ gala │ Gala/Formal │ 🌸 #ec4899 │ ✨ │ ✏️ 🗑️ │ │ │ │ other │ Other │ ⚫ #6b7280 │ 📅 │ ✏️ 🗑️ │ │ │ └─────────────┴───────────────┴────────────┴────────┴────────┘ │ │ │ │ [+ Add Event Type] │ │ │ └──────────────────────────────────────────────────────────────────┘ ``` ### 6.7 Document Categories Settings ``` ┌──────────────────────────────────────────────────────────────────┐ │ Document Categories │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────┬─────────────────────┬────────┬────────────┐ │ │ │ Name │ Display │ Icon │ Actions │ │ │ ├─────────────────┼─────────────────────┼────────┼────────────┤ │ │ │ meeting_minutes │ Meeting Minutes │ 📄 │ ✏️ 🗑️ │ │ │ │ governance │ Governance & Bylaws │ ⚖️ │ ✏️ 🗑️ │ │ │ │ legal │ Legal Documents │ 💼 │ ✏️ 🗑️ │ │ │ │ financial │ Financial Reports │ 💰 │ ✏️ 🗑️ │ │ │ │ member_resources│ Member Resources │ 📚 │ ✏️ 🗑️ │ │ │ │ forms │ Forms & Templates │ 📋 │ ✏️ 🗑️ │ │ │ │ other │ Other Documents │ 📁 │ ✏️ 🗑️ │ │ │ └─────────────────┴─────────────────────┴────────┴────────────┘ │ │ │ │ [+ Add Category] │ │ │ └──────────────────────────────────────────────────────────────────┘ ``` ### 6.8 Directory Visibility Settings **Admin controls what fields are visible to different roles:** ``` ┌──────────────────────────────────────────────────────────────────┐ │ Directory Visibility │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ Configure which member fields are visible in the directory. │ │ Admin always sees all fields. │ │ │ │ ┌─────────────────┬──────────────────┬──────────────────┐ │ │ │ Field │ Visible to │ Visible to │ │ │ │ │ Members │ Board │ │ │ ├─────────────────┼──────────────────┼──────────────────┤ │ │ │ First Name │ ☑️ Always shown │ ☑️ Always shown │ │ │ │ Last Name │ ☑️ Always shown │ ☑️ Always shown │ │ │ │ Profile Photo │ ☑️ │ ☑️ │ │ │ │ Nationality │ ☑️ │ ☑️ │ │ │ │ Email │ ☐ │ ☑️ │ │ │ │ Phone │ ☐ │ ☑️ │ │ │ │ Address │ ☐ │ ☑️ │ │ │ │ Date of Birth │ ☐ │ ☑️ │ │ │ │ Member Since │ ☑️ │ ☑️ │ │ │ │ Status │ ☐ │ ☑️ │ │ │ │ Membership Type │ ☐ │ ☑️ │ │ │ └─────────────────┴──────────────────┴──────────────────┘ │ │ │ │ [Save Visibility Settings] │ │ │ └──────────────────────────────────────────────────────────────────┘ ``` ### 6.9 System Settings Tab ``` ┌──────────────────────────────────────────────────────────────────┐ │ System Settings │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ MAINTENANCE │ │ ───────────────────────────────────────────────────────────── │ │ │ │ ☐ Enable Maintenance Mode │ │ │ │ Maintenance Message: │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ The portal is currently undergoing maintenance. │ │ │ │ Please check back soon. │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ───────────────────────────────────────────────────────────── │ │ │ │ SECURITY │ │ ───────────────────────────────────────────────────────────── │ │ │ │ Session Timeout (hours): │ │ ┌────────────┐ │ │ │ 168 │ (7 days) │ │ └────────────┘ │ │ │ │ ───────────────────────────────────────────────────────────── │ │ │ │ FILE UPLOADS │ │ ───────────────────────────────────────────────────────────── │ │ │ │ Max Upload Size (MB): │ │ ┌────────────┐ │ │ │ 50 │ │ │ └────────────┘ │ │ │ │ Allowed File Types: │ │ [PDF] [DOC] [DOCX] [XLS] [XLSX] [PPT] [PPTX] │ │ [TXT] [JPG] [PNG] [WEBP] [+ Add Type] │ │ │ │ [Save System Settings] │ │ │ └──────────────────────────────────────────────────────────────────┘ ``` ### 6.10 Settings Access Pattern ```typescript // src/lib/server/settings.ts // Get a single setting with type safety export async function getSetting( supabase: SupabaseClient, category: string, key: string, defaultValue: T ): Promise { const { data } = await supabase .from('app_settings') .select('setting_value') .eq('category', category) .eq('setting_key', key) .single(); return data?.setting_value ?? defaultValue; } // Get all settings for a category export async function getCategorySettings( supabase: SupabaseClient, category: string ): Promise> { const { data } = await supabase .from('app_settings') .select('setting_key, setting_value') .eq('category', category); return Object.fromEntries( (data ?? []).map(s => [s.setting_key, s.setting_value]) ); } // Update a setting (admin only) export async function updateSetting( supabase: SupabaseClient, category: string, key: string, value: any, userId: string ): Promise { await supabase .from('app_settings') .update({ setting_value: value, updated_at: new Date().toISOString(), updated_by: userId }) .eq('category', category) .eq('setting_key', key); } ``` ### 6.11 Settings Permissions | Action | Member | Board | Admin | |--------|--------|-------|-------| | View public settings | ✓ | ✓ | ✓ | | View all settings | - | - | ✓ | | Edit settings | - | - | ✓ | | Manage statuses | - | - | ✓ | | Manage membership types | - | - | ✓ | | Manage event types | - | - | ✓ | | Manage document categories | - | - | ✓ | | View settings audit log | - | - | ✓ | --- ## 7. EMAIL SYSTEM (Detailed) ### 7.1 Email Architecture **Provider**: Supabase Edge Functions + external SMTP (Resend, SendGrid, or Mailgun) **Why external SMTP:** - Supabase built-in email is limited to auth emails only - External SMTP provides better deliverability, tracking, and templates - Resend recommended for simplicity and modern API ### 7.2 Email Provider Configuration ```sql -- Email settings (stored in app_settings) INSERT INTO app_settings (category, setting_key, setting_value, setting_type, display_name, description) VALUES ('email', 'provider', '"resend"', 'text', 'Email Provider', 'Email service provider (resend, sendgrid, mailgun)'), ('email', 'api_key', '""', 'text', 'API Key', 'Email provider API key (stored securely)'), ('email', 'from_address', '"noreply@monacousa.org"', 'text', 'From Address', 'Default sender email address'), ('email', 'from_name', '"Monaco USA"', 'text', 'From Name', 'Default sender name'), ('email', 'reply_to', '"contact@monacousa.org"', 'text', 'Reply-To Address', 'Reply-to email address'), ('email', 'enable_tracking', 'true', 'boolean', 'Enable Tracking', 'Track email opens and clicks'), ('email', 'batch_size', '50', 'number', 'Batch Size', 'Max emails per batch send'), ('email', 'rate_limit_per_hour', '100', 'number', 'Rate Limit', 'Maximum emails per hour'); ``` ### 7.3 Email Types & Triggers | Email Type | Trigger | Recipients | Automated | |------------|---------|------------|-----------| | `welcome` | New signup verified | New member | Yes | | `email_verification` | Signup | New member | Yes (Supabase) | | `password_reset` | Password reset request | Member | Yes (Supabase) | | `dues_reminder` | X days before due | Member | Yes (cron) | | `dues_due_today` | Due date | Member | Yes (cron) | | `dues_overdue` | Every X days overdue | Member | Yes (cron) | | `dues_lapsed` | Grace period ends | Member | Yes (cron) | | `dues_received` | Payment recorded | Member | Yes | | `event_created` | New event published | All/visibility | Optional | | `event_reminder` | X hours before event | RSVP'd members | Yes (cron) | | `event_updated` | Event details changed | RSVP'd members | Yes | | `event_cancelled` | Event cancelled | RSVP'd members | Yes | | `rsvp_confirmation` | RSVP submitted | Member | Yes | | `waitlist_promoted` | Spot opens up | Waitlisted member | Yes | | `member_invite` | Admin invites member | Invitee | Manual | | `broadcast` | Admin sends message | Selected members | Manual | ### 7.4 Email Templates Schema ```sql CREATE TABLE public.email_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Template identification template_key TEXT UNIQUE NOT NULL, -- 'dues_reminder', 'welcome', etc. template_name TEXT NOT NULL, -- 'Dues Reminder Email' category TEXT NOT NULL, -- 'dues', 'events', 'system' -- Template content subject TEXT NOT NULL, -- Subject line with {{variables}} body_html TEXT NOT NULL, -- HTML body with {{variables}} body_text TEXT, -- Plain text fallback -- Settings is_active BOOLEAN DEFAULT TRUE, is_system BOOLEAN DEFAULT FALSE, -- System templates can't be deleted -- Metadata variables_schema JSONB, -- Available variables documentation preview_data JSONB, -- Sample data for preview created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), updated_by UUID REFERENCES public.members(id) ); -- Default email templates INSERT INTO email_templates (template_key, template_name, category, subject, body_html, is_system, variables_schema) VALUES -- Welcome Email ('welcome', 'Welcome Email', 'system', 'Welcome to Monaco USA, {{member_name}}!', '
Monaco USA

Welcome to Monaco USA!

Dear {{member_name}},

Thank you for joining Monaco USA! Your Member ID is {{member_id}}.

To complete your membership, please pay your annual dues of €{{dues_amount}}.

Payment Details:

  • Bank: {{bank_name}}
  • IBAN: {{iban}}
  • Account Holder: {{account_holder}}
  • Reference: {{member_id}}

Access Your Portal

Best regards,
Monaco USA Team

', TRUE, '{"member_name":"string","member_id":"string","dues_amount":"number","bank_name":"string","iban":"string","account_holder":"string","portal_link":"string","logo_url":"string"}' ), -- Dues Reminder ('dues_reminder', 'Dues Reminder', 'dues', 'Your Monaco USA dues are due in {{days_until_due}} days', '

Dues Reminder

Dear {{member_name}},

This is a friendly reminder that your Monaco USA membership dues of €{{dues_amount}} are due on {{due_date}} ({{days_until_due}} days from now).

Payment Details:

  • IBAN: {{iban}}
  • Account Holder: {{account_holder}}
  • Reference: {{member_id}}

View Payment Details

Thank you for being a valued member!

', TRUE, '{"member_name":"string","member_id":"string","dues_amount":"number","due_date":"date","days_until_due":"number","iban":"string","account_holder":"string","portal_link":"string"}' ), -- Dues Overdue ('dues_overdue', 'Dues Overdue Notice', 'dues', 'OVERDUE: Your Monaco USA dues are {{days_overdue}} days past due', '

Payment Overdue

Dear {{member_name}},

Your Monaco USA membership dues of €{{dues_amount}} are now {{days_overdue}} days overdue.

Please make your payment as soon as possible to maintain your membership benefits.

{{#if grace_period_remaining}}

Note: You have {{grace_period_remaining}} days remaining in your grace period before your membership is set to inactive.

{{/if}}

Payment Details:

  • IBAN: {{iban}}
  • Account Holder: {{account_holder}}
  • Reference: {{member_id}}

Pay Now

', TRUE, '{"member_name":"string","member_id":"string","dues_amount":"number","days_overdue":"number","grace_period_remaining":"number","iban":"string","account_holder":"string","portal_link":"string"}' ), -- Dues Received ('dues_received', 'Payment Confirmation', 'dues', 'Thank you! Your Monaco USA dues payment has been received', '

Payment Received!

Dear {{member_name}},

Thank you! We have received your membership dues payment.

Payment Details:

  • Amount: €{{amount_paid}}
  • Payment Date: {{payment_date}}
  • Next Due Date: {{next_due_date}}
  • Reference: {{payment_reference}}

Your membership is now active until {{next_due_date}}.

View Payment History

', TRUE, '{"member_name":"string","amount_paid":"number","payment_date":"date","next_due_date":"date","payment_reference":"string","portal_link":"string"}' ), -- Event RSVP Confirmation ('rsvp_confirmation', 'RSVP Confirmation', 'events', 'You''re registered: {{event_title}}', '

You''re Registered!

Dear {{member_name}},

Your RSVP for {{event_title}} has been confirmed.

Event Details:

  • Date: {{event_date}}
  • Time: {{event_time}}
  • Location: {{event_location}}
  • {{#if guest_count}}
  • Additional Guests: {{guest_count}}
  • {{/if}}
{{#if is_paid}}

Payment Required:

Total: €{{total_amount}}

  • IBAN: {{iban}}
  • Reference: {{payment_reference}}
{{/if}}

View Event

', TRUE, '{"member_name":"string","event_title":"string","event_date":"date","event_time":"string","event_location":"string","guest_count":"number","is_paid":"boolean","total_amount":"number","iban":"string","payment_reference":"string","event_id":"string","portal_link":"string"}' ), -- Event Reminder ('event_reminder', 'Event Reminder', 'events', 'Reminder: {{event_title}} is {{time_until_event}}', '

Event Reminder

Dear {{member_name}},

This is a reminder that {{event_title}} is {{time_until_event}}.

Event Details:

  • Date: {{event_date}}
  • Time: {{event_time}}
  • Location: {{event_location}}
{{#if event_description}}

{{event_description}}

{{/if}}

We look forward to seeing you there!

', TRUE, '{"member_name":"string","event_title":"string","event_date":"date","event_time":"string","event_location":"string","event_description":"string","time_until_event":"string"}' ); ``` ### 7.5 Email Logging Schema ```sql -- Enhanced email logs with tracking CREATE TABLE public.email_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Recipients recipient_id UUID REFERENCES public.members(id), recipient_email TEXT NOT NULL, recipient_name TEXT, -- Email details template_key TEXT REFERENCES public.email_templates(template_key), subject TEXT NOT NULL, email_type TEXT NOT NULL, -- Status tracking status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'sent', 'delivered', 'opened', 'clicked', 'bounced', 'failed')), -- Provider data provider TEXT, -- 'resend', 'sendgrid', etc. provider_message_id TEXT, -- External message ID for tracking -- Engagement tracking opened_at TIMESTAMPTZ, clicked_at TIMESTAMPTZ, -- Error handling error_message TEXT, retry_count INTEGER DEFAULT 0, -- Metadata template_variables JSONB, -- Variables used in template sent_by UUID REFERENCES public.members(id), -- For manual sends -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), sent_at TIMESTAMPTZ, delivered_at TIMESTAMPTZ ); -- Index for common queries CREATE INDEX idx_email_logs_recipient ON public.email_logs(recipient_id); CREATE INDEX idx_email_logs_status ON public.email_logs(status); CREATE INDEX idx_email_logs_type ON public.email_logs(email_type); CREATE INDEX idx_email_logs_created ON public.email_logs(created_at DESC); ``` ### 7.6 Automated Email Scheduler (Supabase Edge Function) ```typescript // supabase/functions/email-scheduler/index.ts import { createClient } from '@supabase/supabase-js'; import { Resend } from 'resend'; // Runs daily via pg_cron Deno.serve(async (req) => { const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ); const today = new Date(); today.setHours(0, 0, 0, 0); // 1. Get settings const settings = await getSettings(supabase); const reminderDays = settings.reminder_days_before as number[]; const gracePeriod = settings.grace_period_days as number; // 2. Find members needing reminders const { data: membersWithDues } = await supabase .from('members_with_dues') .select('*') .in('dues_status', ['current', 'due_soon', 'overdue']); for (const member of membersWithDues || []) { const dueDate = new Date(member.current_due_date); const daysUntil = Math.ceil((dueDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); const daysOverdue = member.days_overdue || 0; // Check if we need to send a reminder if (daysUntil > 0 && reminderDays.includes(daysUntil)) { // Send upcoming reminder await sendEmail(supabase, 'dues_reminder', member, { days_until_due: daysUntil }); } else if (daysUntil === 0) { // Due today await sendEmail(supabase, 'dues_due_today', member, {}); } else if (daysOverdue > 0 && daysOverdue <= gracePeriod) { // Overdue but in grace period if (daysOverdue % settings.overdue_reminder_interval === 0) { await sendEmail(supabase, 'dues_overdue', member, { days_overdue: daysOverdue, grace_period_remaining: gracePeriod - daysOverdue }); } } else if (daysOverdue === gracePeriod + 1) { // Grace period just ended await sendEmail(supabase, 'dues_lapsed', member, {}); } } // 3. Send event reminders await sendEventReminders(supabase, settings); return new Response(JSON.stringify({ success: true }), { headers: { 'Content-Type': 'application/json' } }); }); async function sendEmail( supabase: any, templateKey: string, member: any, extraVariables: Record ) { // Get template const { data: template } = await supabase .from('email_templates') .select('*') .eq('template_key', templateKey) .eq('is_active', true) .single(); if (!template) return; // Build variables const variables = { member_name: `${member.first_name} ${member.last_name}`, member_id: member.member_id, dues_amount: member.annual_dues || 50, due_date: member.current_due_date, portal_link: Deno.env.get('PORTAL_URL'), ...extraVariables }; // Add payment settings const settings = await getSettings(supabase); variables.iban = settings.payment_iban; variables.account_holder = settings.payment_account_holder; variables.bank_name = settings.payment_bank_name; // Render template const subject = renderTemplate(template.subject, variables); const html = renderTemplate(template.body_html, variables); // Send via provider const resend = new Resend(Deno.env.get('RESEND_API_KEY')); const result = await resend.emails.send({ from: `${settings.from_name} <${settings.from_address}>`, to: member.email, subject, html }); // Log email await supabase.from('email_logs').insert({ recipient_id: member.id, recipient_email: member.email, recipient_name: variables.member_name, template_key: templateKey, subject, email_type: templateKey, status: result.error ? 'failed' : 'sent', provider: 'resend', provider_message_id: result.data?.id, error_message: result.error?.message, template_variables: variables, sent_at: new Date().toISOString() }); } ``` ### 7.7 Email Settings UI (Admin) ``` ┌──────────────────────────────────────────────────────────────────┐ │ Email Settings │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ PROVIDER CONFIGURATION │ │ ───────────────────────────────────────────────────────────── │ │ │ │ Email Provider: │ │ ┌────────────────────────────────────────┐ │ │ │ Resend ▼ │ │ │ └────────────────────────────────────────┘ │ │ │ │ API Key: │ │ ┌────────────────────────────────────────┐ │ │ │ re_••••••••••••••• │ [Test Connection] │ │ └────────────────────────────────────────┘ │ │ │ │ From Address: │ │ ┌────────────────────────────────────────┐ │ │ │ noreply@monacousa.org │ │ │ └────────────────────────────────────────┘ │ │ │ │ From Name: │ │ ┌────────────────────────────────────────┐ │ │ │ Monaco USA │ │ │ └────────────────────────────────────────┘ │ │ │ │ Reply-To: │ │ ┌────────────────────────────────────────┐ │ │ │ contact@monacousa.org │ │ │ └────────────────────────────────────────┘ │ │ │ │ ───────────────────────────────────────────────────────────── │ │ │ │ TRACKING │ │ ───────────────────────────────────────────────────────────── │ │ │ │ ☑️ Enable open tracking │ │ ☑️ Enable click tracking │ │ │ │ [Save Email Settings] │ │ │ └──────────────────────────────────────────────────────────────────┘ ``` ### 7.8 Email Templates Editor (Admin) ``` ┌──────────────────────────────────────────────────────────────────┐ │ Email Templates │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────────┬────────────────┬─────────┬──────────────┐ │ │ │ Template │ Category │ Active │ Actions │ │ │ ├───────────────────┼────────────────┼─────────┼──────────────┤ │ │ │ Welcome Email │ System │ ✓ │ ✏️ 👁️ 📧 │ │ │ │ Dues Reminder │ Dues │ ✓ │ ✏️ 👁️ 📧 │ │ │ │ Dues Due Today │ Dues │ ✓ │ ✏️ 👁️ 📧 │ │ │ │ Dues Overdue │ Dues │ ✓ │ ✏️ 👁️ 📧 │ │ │ │ Dues Lapsed │ Dues │ ✓ │ ✏️ 👁️ 📧 │ │ │ │ Dues Received │ Dues │ ✓ │ ✏️ 👁️ 📧 │ │ │ │ RSVP Confirmation │ Events │ ✓ │ ✏️ 👁️ 📧 │ │ │ │ Event Reminder │ Events │ ✓ │ ✏️ 👁️ 📧 │ │ │ │ Event Cancelled │ Events │ ✓ │ ✏️ 👁️ 📧 │ │ │ │ Waitlist Promoted │ Events │ ✓ │ ✏️ 👁️ 📧 │ │ │ └───────────────────┴────────────────┴─────────┴──────────────┘ │ │ │ │ Legend: ✏️ Edit | 👁️ Preview | 📧 Send Test │ │ │ │ [+ Create Custom Template] │ │ │ └──────────────────────────────────────────────────────────────────┘ ``` ### 7.9 Template Editor View ``` ┌──────────────────────────────────────────────────────────────────┐ │ Edit Template: Dues Reminder │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ Template Name: │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Dues Reminder │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ Subject Line: │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Your Monaco USA dues are due in {{days_until_due}} days │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────┬───────────────────────────────────┐ │ │ │ HTML Editor │ Preview │ │ │ ├──────────────────────┼───────────────────────────────────┤ │ │ │

Dues ReminderDear {{member_nam │ ┌─────────────────────────────┐ │ │ │ │ e}},

│ │ Dues Reminder │ │ │ │ │

This is a friendl │ │ │ │ │ │ │ y reminder that your │ │ Dear John Doe, │ │ │ │ │ Monaco USA membershi │ │ │ │ │ │ │ p dues...

│ │ This is a friendly │ │ │ │ │ ... │ │ reminder that your Monaco │ │ │ │ │ │ │ USA membership dues of │ │ │ │ │ │ │ €50.00 are due on... │ │ │ │ │ │ └─────────────────────────────┘ │ │ │ └──────────────────────┴───────────────────────────────────┘ │ │ │ │ AVAILABLE VARIABLES: │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ {{member_name}} {{member_id}} {{dues_amount}} {{due_date}}│ │ │ │ {{days_until_due}} {{iban}} {{account_holder}} │ │ │ │ {{portal_link}} │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ [Cancel] [Send Test Email] [Save Changes] │ │ │ └──────────────────────────────────────────────────────────────────┘ ``` ### 7.10 Email Logs View (Admin) ``` ┌──────────────────────────────────────────────────────────────────┐ │ Email Logs │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ Filter: [All Types ▼] [All Status ▼] [Last 30 days ▼] [Search] │ │ │ │ ┌────────┬────────────────┬──────────────┬────────┬──────────┐ │ │ │ Date │ Recipient │ Subject │ Type │ Status │ │ │ ├────────┼────────────────┼──────────────┼────────┼──────────┤ │ │ │ Jan 9 │ john@email.com │ Your Monaco │ dues_ │ 📬 Opened │ │ │ │ 14:30 │ John Doe │ USA dues... │ remind │ │ │ │ ├────────┼────────────────┼──────────────┼────────┼──────────┤ │ │ │ Jan 9 │ jane@email.com │ You're regis │ rsvp_ │ ✅ Sent │ │ │ │ 10:15 │ Jane Smith │ tered: Gala │ conf │ │ │ │ ├────────┼────────────────┼──────────────┼────────┼──────────┤ │ │ │ Jan 8 │ bob@email.com │ OVERDUE: You │ dues_ │ 🔴 Bounce │ │ │ │ 09:00 │ Bob Wilson │ r Monaco... │ overdu │ │ │ │ └────────┴────────────────┴──────────────┴────────┴──────────┘ │ │ │ │ Status Legend: │ │ ✅ Sent | 📬 Opened | 🔗 Clicked | 🔴 Bounced | ❌ Failed │ │ │ │ Stats: 156 sent this month | 78% open rate | 2 bounces │ │ │ └──────────────────────────────────────────────────────────────────┘ ``` ### 7.11 Manual Broadcast Feature (Admin) **Admin can send broadcast emails to selected members:** ``` ┌──────────────────────────────────────────────────────────────────┐ │ Send Broadcast Email │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ Recipients: │ │ ○ All active members (45) │ │ ○ All members (52) │ │ ○ Board members only (5) │ │ ● Select specific members │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 🔍 Search members... │ │ │ ├──────────────────────────────────────────────────────────┤ │ │ │ ☑️ John Doe (john@email.com) │ │ │ │ ☑️ Jane Smith (jane@email.com) │ │ │ │ ☐ Bob Wilson (bob@email.com) │ │ │ │ ... │ │ │ └──────────────────────────────────────────────────────────┘ │ │ Selected: 2 members │ │ │ │ Subject: │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Important Update from Monaco USA │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ Message: │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ [Rich text editor with formatting options] │ │ │ │ │ │ │ │ Dear {{member_name}}, │ │ │ │ │ │ │ │ We wanted to inform you about... │ │ │ │ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ [Preview] [Send Test to Myself] [Send to 2 Recipients] │ │ │ └──────────────────────────────────────────────────────────────────┘ ``` ### 7.12 Email Cron Jobs (pg_cron in Supabase) ```sql -- Enable pg_cron extension CREATE EXTENSION IF NOT EXISTS pg_cron; -- Schedule daily email checks (runs at 9 AM Monaco time) SELECT cron.schedule( 'daily-email-scheduler', '0 9 * * *', -- Every day at 9:00 AM $$ SELECT net.http_post( url := 'https://your-project.supabase.co/functions/v1/email-scheduler', headers := '{"Authorization": "Bearer ' || current_setting('app.service_role_key') || '"}'::jsonb, body := '{}'::jsonb ); $$ ); -- Schedule event reminders (runs every hour) SELECT cron.schedule( 'hourly-event-reminders', '0 * * * *', -- Every hour $$ SELECT net.http_post( url := 'https://your-project.supabase.co/functions/v1/event-reminders', headers := '{"Authorization": "Bearer ' || current_setting('app.service_role_key') || '"}'::jsonb, body := '{}'::jsonb ); $$ ); ``` ### 7.13 Email Permissions | Action | Member | Board | Admin | |--------|--------|-------|-------| | Receive automated emails | ✓ | ✓ | ✓ | | View own email history | ✓ | ✓ | ✓ | | View all email logs | - | - | ✓ | | Edit email templates | - | - | ✓ | | Send broadcast emails | - | - | ✓ | | Send manual reminders | - | ✓ | ✓ | | Configure email settings | - | - | ✓ | | Test email connection | - | - | ✓ | --- ## Phase 1: Current System Analysis (COMPLETED) ### Current Tech Stack | Layer | Technology | |-------|------------| | Framework | Nuxt 3 (Vue 3) - SSR disabled, CSR-only | | UI Components | Vuetify 3 + Tailwind CSS + Custom SCSS | | Database | NocoDB (REST API over PostgreSQL) | | Authentication | Keycloak (OAuth2/OIDC) | | File Storage | MinIO (S3-compatible) | | Email | Nodemailer + Handlebars templates | | State | Vue Composition API (no Pinia) | ### Current Features Inventory #### 1. Member Management - **Data Model**: Member with 20+ fields (name, email, phone, DOB, nationality, address, etc.) - **Member ID Format**: `MUSA-YYYY-XXXX` (auto-generated) - **Status States**: Active, Inactive, Pending, Expired - **Portal Tiers**: admin, board, user - **Profile Images**: Stored in MinIO #### 2. Dues/Subscription System - **Calculation**: Due 1 year after last payment - **Fields Tracked**: `membership_date_paid`, `payment_due_date`, `current_year_dues_paid` - **Reminders**: 30-day advance warning, overdue notifications - **Auto-Status**: Members 1+ year overdue marked Inactive - **Rates**: €50 regular, €25 student, €35 senior, €75 family, €200 corporate #### 3. Events System - **Event Types**: meeting, social, fundraiser, workshop, board-only - **RSVP System**: confirmed, declined, pending, waitlist - **Guest Management**: Extra guests per RSVP - **Pricing**: Member vs non-member pricing - **Visibility**: public, board-only, admin-only - **Calendar**: FullCalendar integration with iCal feed #### 4. Authentication & Authorization - **Login Methods**: OAuth (Keycloak) + Direct login (ROPC) - **Role System**: Keycloak realm roles (monaco-admin, monaco-board, monaco-user) - **Session**: Server-side with HTTP-only cookies (7-30 days) - **Rate Limiting**: 5 attempts/15min, 1-hour IP block - **Signup Flow**: Form → reCAPTCHA → Keycloak user → NocoDB member → Verification email #### 5. Three Dashboard Types - **Admin**: Full system control, user management, settings, all features - **Board**: Member directory, dues management, events, meetings, governance - **Member**: Personal profile, events, payments, resources ### Current Pain Points (to address in rebuild) 1. CSR-only limits SEO and initial load performance 2. NocoDB adds complexity vs direct database access 3. String booleans ("true"/"false") cause type issues 4. No payment history table (only last payment tracked) 5. Vuetify + Tailwind overlap creates CSS conflicts 6. Large monolithic layout files (700-800+ lines each) --- ## Phase 2: New System Requirements ### Core Requirements (confirmed) - [x] Beautiful, modern, responsive frontend (not generic Vue look) - [x] Member tracking with subscription/dues management - [x] Dues due 1 year after last payment - [x] Event calendar with RSVP system - [x] Board members can create/manage events - [x] Event features: capacity, +1 guests, member/non-member pricing - [x] Three dashboard types: Admin, Board, Member - [x] Signup system similar to current - [x] **Manual payment tracking only** (no Stripe integration) - [x] **Email notifications** (dues reminders, event updates) - [x] **Document storage** (meeting minutes, governance docs) ### Deployment Strategy - **Replace entirely** - Switch over when ready (no parallel systems) - **Manual data entry** - Members will be entered manually (no migration scripts) --- ## FRONTEND FRAMEWORK OPTIONS ### Tier 1: Modern & Distinctive (Recommended) #### 1. **Qwik + QwikCity** ⭐ MOST INNOVATIVE | Aspect | Details | |--------|---------| | **What it is** | Resumable framework - HTML loads instantly, JS loads on-demand | | **Unique Feature** | Zero hydration - fastest possible load times | | **Learning Curve** | Medium (JSX-like but different mental model) | | **Ecosystem** | Growing - Auth.js, Drizzle, Modular Forms integrations | | **Benchmark Score** | 93.8 (highest among all frameworks) | **Pros:** - Blazing fast initial load (no hydration delay) - Feels like React/JSX but with better performance model - Built-in form handling with Zod validation - Server functions with `"use server"` directive - Excellent TypeScript support - Unique - won't look like every other site **Cons:** - Smaller community than React/Vue - Fewer pre-built component libraries - Newer framework (less battle-tested in production) - Some patterns feel unfamiliar at first **Best For:** Performance-focused apps where first impression matters --- #### 2. **SolidStart (Solid.js)** ⭐ MOST PERFORMANT REACTIVITY | Aspect | Details | |--------|---------| | **What it is** | Fine-grained reactive framework with meta-framework | | **Unique Feature** | No Virtual DOM - direct DOM updates via signals | | **Learning Curve** | Medium (React-like JSX, different reactivity) | | **Ecosystem** | Good - Ark UI, Kobalte for components | | **Benchmark Score** | 92.2 | **Pros:** - Smallest bundle sizes in the industry - React-like syntax (easy transition) - True reactivity (no re-renders, just updates) - Server functions and data loading built-in - Growing rapidly in popularity - Unique performance characteristics **Cons:** - Smaller ecosystem than React - Fewer tutorials and resources - Some React patterns don't translate directly - Component libraries less mature **Best For:** Highly interactive dashboards with lots of real-time updates --- #### 3. **SvelteKit** ⭐ BEST DEVELOPER EXPERIENCE | Aspect | Details | |--------|---------| | **What it is** | Compiler-based framework with full-stack capabilities | | **Unique Feature** | No virtual DOM, compiles to vanilla JS | | **Learning Curve** | Low (closest to vanilla HTML/CSS/JS) | | **Ecosystem** | Strong - Skeleton UI, Melt UI, Shadcn-Svelte | | **Benchmark Score** | 91.0 | **Pros:** - Simplest syntax - looks like enhanced HTML - Smallest learning curve - Excellent built-in animations/transitions - Strong TypeScript integration - Great form handling - Active, helpful community - Svelte 5 runes make state even simpler **Cons:** - Different mental model from React/Vue - Smaller job market (if that matters) - Some advanced patterns less documented - Breaking changes between Svelte 4 and 5 **Best For:** Clean, maintainable code with minimal boilerplate --- #### 4. **Astro + React/Vue/Svelte Islands** ⭐ MOST FLEXIBLE | Aspect | Details | |--------|---------| | **What it is** | Content-focused framework with "islands" of interactivity | | **Unique Feature** | Mix multiple frameworks, zero JS by default | | **Learning Curve** | Low-Medium | | **Ecosystem** | Excellent - use ANY UI library | | **Benchmark Score** | 90.2 | **Pros:** - Use React, Vue, Svelte, or Solid components together - Zero JavaScript shipped by default - Excellent for content + interactive sections - Built-in image optimization - Great Supabase integration documented - View Transitions API support **Cons:** - Not ideal for highly interactive SPAs - Island architecture adds complexity - More configuration for full interactivity - Less unified than single-framework approach **Best For:** Marketing site + member portal hybrid --- ### Tier 2: Battle-Tested Mainstream #### 5. **Next.js 15 (React)** | Aspect | Details | |--------|---------| | **What it is** | Most popular React meta-framework | | **Unique Feature** | App Router, Server Components, huge ecosystem | | **Learning Curve** | Medium-High (lots of concepts) | | **Ecosystem** | Largest - shadcn/ui, Radix, everything | | **Benchmark Score** | N/A (didn't query) | **Pros:** - Largest ecosystem and community - Most job opportunities - shadcn/ui provides beautiful, customizable components - Excellent documentation - Vercel hosting optimized **Cons:** - Can feel "generic" - many sites use it - Complex mental model (Server vs Client components) - Heavier than alternatives - Vercel-centric development --- #### 6. **Remix** | Aspect | Details | |--------|---------| | **What it is** | Full-stack React framework focused on web standards | | **Unique Feature** | Nested routing, progressive enhancement | | **Learning Curve** | Medium | | **Ecosystem** | Good - React ecosystem compatible | | **Benchmark Score** | 89.4 | **Pros:** - Web standards focused (works without JS) - Excellent data loading patterns - Great error handling - Form handling is first-class - Can deploy anywhere (not Vercel-locked) **Cons:** - Smaller community than Next.js - Less "magic" means more manual work - Merged with React Router (transition period) --- #### 7. **TanStack Start (React)** | Aspect | Details | |--------|---------| | **What it is** | New full-stack framework from TanStack team | | **Unique Feature** | Type-safe from database to UI | | **Learning Curve** | Medium | | **Ecosystem** | TanStack Query, Form, Router built-in | | **Benchmark Score** | 80.7 | **Pros:** - Built by TanStack (Query, Router, Form authors) - End-to-end type safety - Modern patterns throughout - Excellent data fetching built-in **Cons:** - Very new (beta/early stage) - Smaller community - Less documentation - Rapidly evolving API --- #### 8. **Nuxt 4 (Vue 3)** | Aspect | Details | |--------|---------| | **What it is** | Latest Vue meta-framework | | **Unique Feature** | Familiar from current system | | **Learning Curve** | Low (you know it) | | **Ecosystem** | Good - Nuxt UI, PrimeVue | **Pros:** - Familiar - no learning curve - Can reuse some current code/patterns - Strong conventions - Good TypeScript support now **Cons:** - User specifically wants to avoid "generic Vue look" - Similar limitations to current system - Less innovative than alternatives --- #### 9. **Angular 19** | Aspect | Details | |--------|---------| | **What it is** | Google's enterprise framework | | **Unique Feature** | Signals, standalone components, full framework | | **Learning Curve** | High | | **Ecosystem** | Enterprise-grade | | **Benchmark Score** | 90.3 | **Pros:** - Complete framework (no decisions to make) - Excellent for large applications - Strong typing throughout - Signals in Angular 19 are modern **Cons:** - Steeper learning curve - More verbose - "Enterprise" feel may not fit small org - Overkill for this scale --- ### Tier 3: Experimental/Niche #### 10. **Leptos (Rust)** | Aspect | Details | |--------|---------| | **What it is** | Full-stack Rust framework | | **Unique Feature** | WASM-based, extremely fast | | **Benchmark Score** | 89.7 | **Pros:** - Blazing fast (Rust + WASM) - Type safety at compile time - Innovative approach **Cons:** - Requires learning Rust - Small ecosystem - Harder to find developers - Overkill for this use case --- #### 11. **Hono + HTMX** | Aspect | Details | |--------|---------| | **What it is** | Lightweight backend + hypermedia frontend | | **Unique Feature** | Server-rendered, minimal JS | | **Benchmark Score** | 92.8 | **Pros:** - Extremely lightweight - Simple mental model - Works on edge (Cloudflare Workers) - Fast development **Cons:** - Less rich interactivity - Different paradigm (hypermedia) - Limited complex UI patterns - Manual work for dashboards --- ## UI COMPONENT LIBRARY OPTIONS ### For React-based Frameworks (Next.js, Remix, TanStack) | Library | Style | Customizable | Notes | |---------|-------|--------------|-------| | **shadcn/ui** | Modern, clean | Fully (copy/paste) | Most popular, highly customizable | | **Radix Themes** | Polished | Theme-based | Beautiful defaults, less work | | **Radix Primitives** | Unstyled | Fully | Build completely custom | | **Ark UI** | Unstyled | Fully | Works with multiple frameworks | | **Park UI** | Pre-styled Ark | Moderate | Ark + beautiful defaults | ### For Solid.js | Library | Style | Notes | |---------|-------|-------| | **Kobalte** | Unstyled | Radix-like primitives for Solid | | **Ark UI Solid** | Unstyled | Same Ark, Solid version | | **Solid UI** | Various | Community components | ### For Svelte | Library | Style | Notes | |---------|-------|-------| | **shadcn-svelte** | Modern | Port of shadcn for Svelte | | **Skeleton UI** | Tailwind | Full design system | | **Melt UI** | Unstyled | Primitives for Svelte | | **Bits UI** | Unstyled | Headless components | ### For Qwik | Library | Style | Notes | |---------|-------|-------| | **Qwik UI** | Official | Growing component library | | **Custom + Tailwind** | Any | Build from scratch | ### For Vue/Nuxt | Library | Style | Notes | |---------|-------|-------| | **shadcn-vue** | Modern | Port of shadcn for Vue | | **Radix Vue** | Unstyled | Radix primitives for Vue | | **Nuxt UI** | Tailwind | Official Nuxt components | | **PrimeVue** | Various | Comprehensive but generic | --- ## DATABASE OPTIONS - DETAILED COMPARISON ### Option 1: **Supabase** ⭐ RECOMMENDED | Aspect | Details | |--------|---------| | **Type** | PostgreSQL + Auth + Storage + Realtime | | **Hosting** | Managed cloud or self-hosted | | **Pricing** | Free tier, then $25/mo | **Pros:** - All-in-one: Database + Auth + File Storage + Realtime - PostgreSQL (industry standard, powerful) - Row-level security built-in - Excellent TypeScript support - Auto-generated APIs - Real-time subscriptions - Built-in auth (replaces Keycloak) - Dashboard for data management - Can self-host if needed **Cons:** - Vendor lock-in (mitigated by self-host option) - Learning curve for RLS policies - Free tier has limits - Less control than raw PostgreSQL **Best For:** Rapid development with full-stack features --- ### Option 2: **PostgreSQL + Prisma** | Aspect | Details | |--------|---------| | **Type** | Direct database + Type-safe ORM | | **Hosting** | Any PostgreSQL host (Neon, Railway, etc.) | | **Pricing** | Database hosting costs only | **Pros:** - Full control over database - Prisma schema is very readable - Excellent TypeScript types - Migrations handled automatically - Works with any PostgreSQL - Large community **Cons:** - Need separate auth solution - Need separate file storage - More setup work - Prisma can be slow for complex queries **Best For:** Maximum control and flexibility --- ### Option 3: **PostgreSQL + Drizzle ORM** | Aspect | Details | |--------|---------| | **Type** | Direct database + Lightweight ORM | | **Hosting** | Any PostgreSQL host | | **Pricing** | Database hosting costs only | **Pros:** - Closer to SQL (less abstraction) - Faster than Prisma - Smaller bundle size - TypeScript-first - Better for complex queries - Growing rapidly **Cons:** - Newer, smaller community - Less documentation - Need separate auth/storage - More manual migration work **Best For:** Performance-critical apps, SQL-comfortable teams --- ### Option 4: **PlanetScale + Drizzle** | Aspect | Details | |--------|---------| | **Type** | Serverless MySQL | | **Hosting** | Managed cloud only | | **Pricing** | Free tier, then usage-based | **Pros:** - Serverless scaling - Branching (like git for databases) - No connection limits - Fast globally **Cons:** - MySQL not PostgreSQL - No foreign keys (by design) - Vendor lock-in - Can get expensive at scale **Best For:** Serverless deployments, edge functions --- ### Option 5: **Keep NocoDB** | Aspect | Details | |--------|---------| | **Type** | Spreadsheet-like interface over database | | **Hosting** | Self-hosted or cloud | **Pros:** - Already configured - Non-technical users can edit data - Flexible schema changes - API already exists **Cons:** - Adds complexity layer - String booleans issue - Less type safety - Performance overhead - Limited query capabilities **Best For:** Non-technical admin users need direct access --- ## AUTHENTICATION OPTIONS - DETAILED COMPARISON ### Option 1: **Supabase Auth** ⭐ IF USING SUPABASE | Aspect | Details | |--------|---------| | **Type** | Built into Supabase | | **Providers** | Email, OAuth (Google, GitHub, etc.), Magic Link | **Pros:** - Integrated with Supabase (one platform) - Row-level security integration - Simple setup - Built-in user management - Social logins included - Magic link support **Cons:** - Tied to Supabase - Less customizable than Keycloak - No SAML/enterprise SSO on free tier --- ### Option 2: **Keep Keycloak** | Aspect | Details | |--------|---------| | **Type** | Self-hosted identity provider | | **Providers** | Everything (OIDC, SAML, social, etc.) | **Pros:** - Already configured and working - Enterprise-grade features - Full control - SAML support - Custom themes - User federation **Cons:** - Complex to maintain - Heavy resource usage - Overkill for small org - Requires Java expertise - Self-hosted burden --- ### Option 3: **Better Auth** ⭐ MODERN CHOICE | Aspect | Details | |--------|---------| | **Type** | Framework-agnostic TypeScript auth | | **Providers** | Email, OAuth, Magic Link, Passkeys | **Pros:** - Modern, TypeScript-first - Works with any framework - Plugin system for features - Session management built-in - Two-factor auth support - Lightweight **Cons:** - Newer (less battle-tested) - Self-implemented - Need own user storage --- ### Option 4: **Auth.js (NextAuth)** | Aspect | Details | |--------|---------| | **Type** | Framework-agnostic auth library | | **Providers** | 50+ OAuth providers | **Pros:** - Massive provider support - Well documented - Active development - Works with Qwik, SvelteKit, etc. **Cons:** - Complex configuration - Database adapter setup - v5 migration issues - Can be finicky --- ### Option 5: **Clerk** | Aspect | Details | |--------|---------| | **Type** | Auth-as-a-service | | **Providers** | Everything + beautiful UI | **Pros:** - Beautiful pre-built components - Zero config setup - Great DX - Organizations/teams built-in **Cons:** - Expensive at scale - Vendor lock-in - Less control - Monthly costs --- ### Option 6: **Lucia Auth** | Aspect | Details | |--------|---------| | **Type** | Low-level auth library | | **Note** | Being deprecated in favor of guides | **Pros:** - Full control - Lightweight - Educational **Cons:** - Being sunset - More DIY work --- ## CHOSEN STACK (FINAL) | Layer | Technology | Rationale | |-------|------------|-----------| | **Framework** | SvelteKit 2 | Best DX, simple syntax, excellent performance | | **UI Components** | shadcn-svelte + Bits UI | Beautiful, customizable, accessible | | **Styling** | Tailwind CSS 4 | Utility-first, works great with shadcn | | **Database** | Supabase (PostgreSQL) | All-in-one, managed, real-time capable | | **Auth** | Supabase Auth | Integrated with database, simple setup | | **File Storage** | Supabase Storage | Profile images, documents | | **Design** | Glass-morphism (evolved) | Modern, distinctive, refined | | **Language** | TypeScript | Type safety throughout | --- ## Phase 3: Architecture Design ### Project Structure ``` monacousa-portal-2026/ ├── src/ │ ├── lib/ │ │ ├── components/ # Reusable UI components │ │ │ ├── ui/ # shadcn-svelte base components │ │ │ ├── dashboard/ # Dashboard widgets │ │ │ ├── members/ # Member-related components │ │ │ ├── events/ # Event components │ │ │ └── layout/ # Layout components (sidebar, header) │ │ ├── server/ # Server-only utilities │ │ │ ├── supabase.ts # Supabase server client │ │ │ └── auth.ts # Auth helpers │ │ ├── stores/ # Svelte stores for state │ │ ├── utils/ # Shared utilities │ │ │ ├── types.ts # TypeScript types │ │ │ ├── constants.ts # App constants │ │ │ └── helpers.ts # Helper functions │ │ └── supabase.ts # Supabase client (browser) │ │ │ ├── routes/ │ │ ├── +layout.svelte # Root layout │ │ ├── +layout.server.ts # Root server load (auth) │ │ ├── +page.svelte # Landing page │ │ │ │ │ ├── (auth)/ # Auth group (guest only) │ │ │ ├── login/ │ │ │ ├── signup/ │ │ │ ├── forgot-password/ │ │ │ └── callback/ # OAuth callback │ │ │ │ │ ├── (app)/ # Protected app group │ │ │ ├── +layout.svelte # App layout with sidebar │ │ │ ├── +layout.server.ts # Auth guard │ │ │ │ │ │ │ ├── dashboard/ # User dashboard │ │ │ ├── profile/ # User profile │ │ │ ├── events/ # Events calendar/list │ │ │ ├── payments/ # Dues/payments view │ │ │ │ │ │ │ ├── board/ # Board-only routes │ │ │ │ ├── +layout.server.ts # Board guard │ │ │ │ ├── dashboard/ │ │ │ │ ├── members/ │ │ │ │ ├── events/ # Event management │ │ │ │ └── meetings/ │ │ │ │ │ │ │ └── admin/ # Admin-only routes │ │ │ ├── +layout.server.ts # Admin guard │ │ │ ├── dashboard/ │ │ │ ├── members/ │ │ │ ├── users/ │ │ │ ├── events/ │ │ │ └── settings/ │ │ │ │ │ └── api/ # API routes (if needed) │ │ │ ├── hooks.server.ts # Server hooks (Supabase SSR) │ └── app.d.ts # TypeScript declarations │ ├── static/ # Static assets ├── supabase/ # Supabase local dev │ └── migrations/ # Database migrations ├── tests/ # Test files ├── svelte.config.js ├── tailwind.config.ts ├── vite.config.ts └── package.json ``` --- ### Database Schema (Supabase/PostgreSQL) ```sql -- USERS (managed by Supabase Auth) -- auth.users table is automatic -- MEMBERS (extends auth.users) CREATE TABLE public.members ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, member_id TEXT UNIQUE NOT NULL, -- MUSA-2026-0001 format first_name TEXT NOT NULL, last_name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, phone TEXT, date_of_birth DATE, address TEXT, nationality TEXT[], -- Array of country codes ['FR', 'US'] -- Membership role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('member', 'board', 'admin')), membership_status TEXT NOT NULL DEFAULT 'pending' CHECK (membership_status IN ('active', 'inactive', 'pending', 'expired')), member_since DATE DEFAULT CURRENT_DATE, -- Profile avatar_url TEXT, bio TEXT, -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- DUES/PAYMENTS (tracks payment history) CREATE TABLE public.dues_payments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), member_id UUID NOT NULL REFERENCES public.members(id) ON DELETE CASCADE, amount DECIMAL(10,2) NOT NULL, currency TEXT DEFAULT 'EUR', payment_date DATE NOT NULL, due_date DATE NOT NULL, -- When this payment period ends payment_method TEXT, -- 'bank_transfer', 'cash', etc. reference TEXT, -- Transaction reference notes TEXT, recorded_by UUID REFERENCES public.members(id), -- Who recorded this payment created_at TIMESTAMPTZ DEFAULT NOW() ); -- EVENTS CREATE TABLE public.events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL, description TEXT, event_type TEXT NOT NULL CHECK (event_type IN ('social', 'meeting', 'fundraiser', 'workshop', 'other')), start_datetime TIMESTAMPTZ NOT NULL, end_datetime TIMESTAMPTZ NOT NULL, location TEXT, -- Capacity & Pricing max_attendees INTEGER, max_guests_per_member INTEGER DEFAULT 1, member_price DECIMAL(10,2) DEFAULT 0, non_member_price DECIMAL(10,2) DEFAULT 0, -- Visibility visibility TEXT NOT NULL DEFAULT 'members' CHECK (visibility IN ('public', 'members', 'board', 'admin')), status TEXT NOT NULL DEFAULT 'published' CHECK (status IN ('draft', 'published', 'cancelled', 'completed')), -- Metadata created_by UUID NOT NULL REFERENCES public.members(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- EVENT RSVPs CREATE TABLE public.event_rsvps ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), event_id UUID NOT NULL REFERENCES public.events(id) ON DELETE CASCADE, member_id UUID NOT NULL REFERENCES public.members(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'confirmed' CHECK (status IN ('confirmed', 'declined', 'waitlist', 'cancelled')), guest_count INTEGER DEFAULT 0, guest_names TEXT[], payment_status TEXT DEFAULT 'not_required' CHECK (payment_status IN ('not_required', 'pending', 'paid')), attended BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(event_id, member_id) ); -- DOCUMENTS (meeting minutes, governance, etc.) CREATE TABLE public.documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL, description TEXT, category TEXT NOT NULL CHECK (category IN ('meeting_minutes', 'governance', 'financial', 'other')), file_path TEXT NOT NULL, -- Supabase Storage path file_name TEXT NOT NULL, file_size INTEGER, mime_type TEXT, visibility TEXT NOT NULL DEFAULT 'board' CHECK (visibility IN ('members', 'board', 'admin')), uploaded_by UUID NOT NULL REFERENCES public.members(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- EMAIL NOTIFICATIONS LOG CREATE TABLE public.email_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), recipient_id UUID REFERENCES public.members(id), recipient_email TEXT NOT NULL, email_type TEXT NOT NULL, -- 'dues_reminder', 'event_invite', etc. subject TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'sent' CHECK (status IN ('sent', 'failed', 'bounced')), sent_at TIMESTAMPTZ DEFAULT NOW(), error_message TEXT ); -- COMPUTED VIEW: Member with dues status CREATE VIEW public.members_with_dues AS SELECT m.*, dp.payment_date as last_payment_date, dp.due_date as current_due_date, CASE WHEN dp.due_date IS NULL THEN 'never_paid' WHEN dp.due_date < CURRENT_DATE THEN 'overdue' WHEN dp.due_date < CURRENT_DATE + INTERVAL '30 days' THEN 'due_soon' ELSE 'current' END as dues_status, CASE WHEN dp.due_date < CURRENT_DATE THEN CURRENT_DATE - dp.due_date ELSE NULL END as days_overdue FROM public.members m LEFT JOIN LATERAL ( SELECT payment_date, due_date FROM public.dues_payments WHERE member_id = m.id ORDER BY due_date DESC LIMIT 1 ) dp ON true; -- ROW LEVEL SECURITY ALTER TABLE public.members ENABLE ROW LEVEL SECURITY; ALTER TABLE public.dues_payments ENABLE ROW LEVEL SECURITY; ALTER TABLE public.events ENABLE ROW LEVEL SECURITY; ALTER TABLE public.event_rsvps ENABLE ROW LEVEL SECURITY; -- Members: Users can read all, update own, admins can do anything CREATE POLICY "Members are viewable by authenticated users" ON public.members FOR SELECT TO authenticated USING (true); CREATE POLICY "Users can update own profile" ON public.members FOR UPDATE TO authenticated USING (auth.uid() = id); CREATE POLICY "Admins can insert members" ON public.members FOR INSERT TO authenticated WITH CHECK ( EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin') OR auth.uid() = id -- Self-registration ); -- Events: Based on visibility CREATE POLICY "Events viewable based on visibility" ON public.events FOR SELECT TO authenticated USING ( visibility = 'members' OR visibility = 'public' OR (visibility = 'board' AND EXISTS ( SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin') )) OR (visibility = 'admin' AND EXISTS ( SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin' )) ); -- Board/Admin can manage events CREATE POLICY "Board can manage events" ON public.events FOR ALL TO authenticated USING ( EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin')) ); ``` --- ### Authentication Flow ``` 1. SIGNUP FLOW ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ /signup │────▶│ Supabase Auth│────▶│ Email Verify│ │ Form │ │ signUp() │ │ Link Sent │ └─────────────┘ └──────────────┘ └─────────────┘ │ ▼ ┌──────────────┐ │ Create Member│ │ Record (RLS) │ └──────────────┘ 2. LOGIN FLOW ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ /login │────▶│ Supabase Auth│────▶│ Set Session │ │ Form │ │ signIn() │ │ Cookie │ └─────────────┘ └──────────────┘ └─────────────┘ │ ▼ ┌──────────────┐ │ Redirect to │ │ Dashboard │ └──────────────┘ 3. PROTECTED ROUTES ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ Request │────▶│ hooks.server │────▶│ Check Role │ │ /admin/* │ │ getSession() │ │ in members │ └─────────────┘ └──────────────┘ └─────────────┘ │ │ ▼ ▼ ┌──────────────┐ ┌─────────────┐ │ Valid? │ │ Redirect or │ │ Yes → Render │ │ 403 Error │ └──────────────┘ └─────────────┘ ``` --- ### UI Component Library Using **shadcn-svelte** with custom glass-morphism theme: ```typescript // tailwind.config.ts - Glass theme extensions export default { theme: { extend: { colors: { monaco: { 50: '#fef2f2', 100: '#fee2e2', 500: '#ef4444', 600: '#dc2626', // Primary 700: '#b91c1c', 900: '#7f1d1d', } }, backdropBlur: { xs: '2px', }, boxShadow: { 'glass': '0 8px 32px rgba(0, 0, 0, 0.1)', 'glass-lg': '0 25px 50px rgba(0, 0, 0, 0.15)', } } } } ``` **Custom Glass Components:** - `GlassCard` - Frosted glass container - `GlassSidebar` - Navigation sidebar - `GlassButton` - Glass-effect buttons - `GlassInput` - Form inputs with glass styling - `StatCard` - Dashboard stat display - `EventCard` - Event display card - `MemberCard` - Member profile card - `DuesStatusBadge` - Dues status indicator --- ### Key Features Implementation #### 1. Member Management - View all members (admin/board) - Edit member details - Upload profile photos (Supabase Storage) - Track membership status - Filter by status, nationality, dues #### 2. Dues Tracking - Payment history table - Auto-calculate due dates (1 year from payment) - Visual status indicators - Overdue notifications - Manual payment recording #### 3. Event System - Calendar view (FullCalendar or custom) - List view with filters - RSVP with guest management - Attendance tracking - Event creation (board/admin) #### 4. Three Dashboards | Dashboard | Features | |-----------|----------| | **Member** | Profile, upcoming events, dues status, quick actions | | **Board** | Member stats, pending applications, dues overview, event management | | **Admin** | System stats, user management, all member data, settings | #### 5. Email Notifications - Dues reminder emails (30 days before, on due date, overdue) - Event invitation/updates - Welcome email on signup - Password reset emails (Supabase built-in) #### 6. Document Storage - Upload meeting minutes, governance docs - Organize by category - Visibility controls (members/board/admin) - Download/preview functionality --- ## Phase 4: Implementation Roadmap ### Stage 1: Foundation (Week 1-2) 1. Initialize SvelteKit project with TypeScript 2. Set up Tailwind CSS 4 + shadcn-svelte 3. Configure Supabase project 4. Create database schema + migrations 5. Implement Supabase SSR hooks 6. Build base layout components ### Stage 2: Authentication (Week 2-3) 1. Login/Signup pages 2. Email verification flow 3. Password reset 4. Protected route guards 5. Role-based access control ### Stage 3: Core Features (Week 3-5) 1. Member dashboard 2. Profile management 3. Member directory (board/admin) 4. Dues tracking system 5. Payment recording ### Stage 4: Events (Week 5-6) 1. Event listing/calendar 2. Event detail view 3. RSVP system 4. Event creation (board) 5. Attendance tracking ### Stage 5: Admin Features (Week 6-7) 1. Admin dashboard 2. User management 3. System settings 4. Data export ### Stage 6: Polish (Week 7-8) 1. Glass-morphism styling refinement 2. Responsive design 3. Performance optimization 4. Testing 5. Documentation --- ## Verification Plan ### Development Testing ```bash # Start Supabase local npx supabase start # Run dev server npm run dev # Type checking npm run check ``` ### Manual Testing Checklist - [ ] User can sign up and receive verification email - [ ] User can log in and see dashboard - [ ] Member can view/edit profile - [ ] Member can view events and RSVP - [ ] Board member can access board dashboard - [ ] Board member can create/manage events - [ ] Board member can view member directory - [ ] Board member can record dues payments - [ ] Admin can access all features - [ ] Admin can manage user roles - [ ] Role-based routing works correctly - [ ] Responsive on mobile/tablet/desktop ### Browser Testing - Chrome, Firefox, Safari, Edge - iOS Safari, Android Chrome --- ## Data Entry Strategy Since members will be entered manually (no automated migration): ### Admin Setup 1. Create first admin account via Supabase dashboard 2. Manually set `role = 'admin'` in members table 3. Admin can then add other members through the portal ### Member Entry Options 1. **Admin adds members** - Admin creates accounts for existing members 2. **Self-registration** - Members sign up themselves 3. **Invite system** - Admin sends email invites with signup links ### Initial Launch Checklist - [ ] Admin account created and verified - [ ] Board member accounts created - [ ] Test member account for verification - [ ] Email templates configured (Supabase) --- ## Files to Create | Path | Purpose | |------|---------| | `monacousa-portal-2026/` | New project root | | `src/hooks.server.ts` | Supabase SSR setup | | `src/lib/supabase.ts` | Client initialization | | `src/lib/server/supabase.ts` | Server client | | `src/routes/+layout.svelte` | Root layout | | `src/routes/(auth)/login/+page.svelte` | Login page | | `src/routes/(auth)/signup/+page.svelte` | Signup page | | `src/routes/(app)/+layout.svelte` | App layout | | `src/routes/(app)/dashboard/+page.svelte` | Member dashboard | | `supabase/migrations/001_schema.sql` | Database schema |