3711 lines
147 KiB
Markdown
3711 lines
147 KiB
Markdown
# 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
|
|
<!-- routes/(app)/dashboard/+page.svelte -->
|
|
<script>
|
|
export let data; // { member, events, stats }
|
|
const { member } = data;
|
|
const isBoard = member.role === 'board' || member.role === 'admin';
|
|
const isAdmin = member.role === 'admin';
|
|
</script>
|
|
|
|
<!-- Everyone sees these -->
|
|
<WelcomeCard {member} />
|
|
<DuesStatusCard {member} />
|
|
<UpcomingEventsCard events={data.upcomingEvents} />
|
|
|
|
<!-- Board and Admin see these -->
|
|
{#if isBoard}
|
|
<Separator title="Board Tools" />
|
|
<MemberStatsCard stats={data.memberStats} />
|
|
<PendingMembersCard members={data.pendingMembers} />
|
|
<DuesOverviewCard overview={data.duesOverview} />
|
|
{/if}
|
|
|
|
<!-- Admin only sees these -->
|
|
{#if isAdmin}
|
|
<Separator title="Admin" />
|
|
<SystemHealthCard health={data.systemHealth} />
|
|
<RecentActivityCard activity={data.recentActivity} />
|
|
<QuickActionsCard />
|
|
{/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<T>(
|
|
supabase: SupabaseClient,
|
|
category: string,
|
|
key: string,
|
|
defaultValue: T
|
|
): Promise<T> {
|
|
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<Record<string, any>> {
|
|
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<void> {
|
|
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}}!',
|
|
'<!DOCTYPE html>
|
|
<html>
|
|
<head><style>body{font-family:Arial,sans-serif;}</style></head>
|
|
<body>
|
|
<div style="max-width:600px;margin:0 auto;padding:20px;">
|
|
<img src="{{logo_url}}" alt="Monaco USA" style="max-width:150px;">
|
|
<h1>Welcome to Monaco USA!</h1>
|
|
<p>Dear {{member_name}},</p>
|
|
<p>Thank you for joining Monaco USA! Your Member ID is <strong>{{member_id}}</strong>.</p>
|
|
<p>To complete your membership, please pay your annual dues of <strong>€{{dues_amount}}</strong>.</p>
|
|
<h3>Payment Details:</h3>
|
|
<ul>
|
|
<li>Bank: {{bank_name}}</li>
|
|
<li>IBAN: {{iban}}</li>
|
|
<li>Account Holder: {{account_holder}}</li>
|
|
<li>Reference: {{member_id}}</li>
|
|
</ul>
|
|
<p><a href="{{portal_link}}" style="background:#dc2626;color:white;padding:10px 20px;text-decoration:none;border-radius:5px;">Access Your Portal</a></p>
|
|
<p>Best regards,<br>Monaco USA Team</p>
|
|
</div>
|
|
</body>
|
|
</html>',
|
|
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',
|
|
'<!DOCTYPE html>
|
|
<html>
|
|
<body style="font-family:Arial,sans-serif;">
|
|
<div style="max-width:600px;margin:0 auto;padding:20px;">
|
|
<h1>Dues Reminder</h1>
|
|
<p>Dear {{member_name}},</p>
|
|
<p>This is a friendly reminder that your Monaco USA membership dues of <strong>€{{dues_amount}}</strong> are due on <strong>{{due_date}}</strong> ({{days_until_due}} days from now).</p>
|
|
<h3>Payment Details:</h3>
|
|
<ul>
|
|
<li>IBAN: {{iban}}</li>
|
|
<li>Account Holder: {{account_holder}}</li>
|
|
<li>Reference: {{member_id}}</li>
|
|
</ul>
|
|
<p><a href="{{portal_link}}/payments" style="background:#dc2626;color:white;padding:10px 20px;text-decoration:none;border-radius:5px;">View Payment Details</a></p>
|
|
<p>Thank you for being a valued member!</p>
|
|
</div>
|
|
</body>
|
|
</html>',
|
|
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',
|
|
'<!DOCTYPE html>
|
|
<html>
|
|
<body style="font-family:Arial,sans-serif;">
|
|
<div style="max-width:600px;margin:0 auto;padding:20px;">
|
|
<h1 style="color:#dc2626;">Payment Overdue</h1>
|
|
<p>Dear {{member_name}},</p>
|
|
<p>Your Monaco USA membership dues of <strong>€{{dues_amount}}</strong> are now <strong>{{days_overdue}} days overdue</strong>.</p>
|
|
<p>Please make your payment as soon as possible to maintain your membership benefits.</p>
|
|
{{#if grace_period_remaining}}
|
|
<p><strong>Note:</strong> You have {{grace_period_remaining}} days remaining in your grace period before your membership is set to inactive.</p>
|
|
{{/if}}
|
|
<h3>Payment Details:</h3>
|
|
<ul>
|
|
<li>IBAN: {{iban}}</li>
|
|
<li>Account Holder: {{account_holder}}</li>
|
|
<li>Reference: {{member_id}}</li>
|
|
</ul>
|
|
<p><a href="{{portal_link}}/payments" style="background:#dc2626;color:white;padding:10px 20px;text-decoration:none;border-radius:5px;">Pay Now</a></p>
|
|
</div>
|
|
</body>
|
|
</html>',
|
|
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',
|
|
'<!DOCTYPE html>
|
|
<html>
|
|
<body style="font-family:Arial,sans-serif;">
|
|
<div style="max-width:600px;margin:0 auto;padding:20px;">
|
|
<h1 style="color:#10b981;">Payment Received!</h1>
|
|
<p>Dear {{member_name}},</p>
|
|
<p>Thank you! We have received your membership dues payment.</p>
|
|
<h3>Payment Details:</h3>
|
|
<ul>
|
|
<li>Amount: €{{amount_paid}}</li>
|
|
<li>Payment Date: {{payment_date}}</li>
|
|
<li>Next Due Date: {{next_due_date}}</li>
|
|
<li>Reference: {{payment_reference}}</li>
|
|
</ul>
|
|
<p>Your membership is now active until {{next_due_date}}.</p>
|
|
<p><a href="{{portal_link}}/payments" style="background:#10b981;color:white;padding:10px 20px;text-decoration:none;border-radius:5px;">View Payment History</a></p>
|
|
</div>
|
|
</body>
|
|
</html>',
|
|
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}}',
|
|
'<!DOCTYPE html>
|
|
<html>
|
|
<body style="font-family:Arial,sans-serif;">
|
|
<div style="max-width:600px;margin:0 auto;padding:20px;">
|
|
<h1>You''re Registered!</h1>
|
|
<p>Dear {{member_name}},</p>
|
|
<p>Your RSVP for <strong>{{event_title}}</strong> has been confirmed.</p>
|
|
<h3>Event Details:</h3>
|
|
<ul>
|
|
<li><strong>Date:</strong> {{event_date}}</li>
|
|
<li><strong>Time:</strong> {{event_time}}</li>
|
|
<li><strong>Location:</strong> {{event_location}}</li>
|
|
{{#if guest_count}}<li><strong>Additional Guests:</strong> {{guest_count}}</li>{{/if}}
|
|
</ul>
|
|
{{#if is_paid}}
|
|
<h3>Payment Required:</h3>
|
|
<p>Total: €{{total_amount}}</p>
|
|
<ul>
|
|
<li>IBAN: {{iban}}</li>
|
|
<li>Reference: {{payment_reference}}</li>
|
|
</ul>
|
|
{{/if}}
|
|
<p><a href="{{portal_link}}/events/{{event_id}}" style="background:#dc2626;color:white;padding:10px 20px;text-decoration:none;border-radius:5px;">View Event</a></p>
|
|
</div>
|
|
</body>
|
|
</html>',
|
|
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}}',
|
|
'<!DOCTYPE html>
|
|
<html>
|
|
<body style="font-family:Arial,sans-serif;">
|
|
<div style="max-width:600px;margin:0 auto;padding:20px;">
|
|
<h1>Event Reminder</h1>
|
|
<p>Dear {{member_name}},</p>
|
|
<p>This is a reminder that <strong>{{event_title}}</strong> is {{time_until_event}}.</p>
|
|
<h3>Event Details:</h3>
|
|
<ul>
|
|
<li><strong>Date:</strong> {{event_date}}</li>
|
|
<li><strong>Time:</strong> {{event_time}}</li>
|
|
<li><strong>Location:</strong> {{event_location}}</li>
|
|
</ul>
|
|
{{#if event_description}}
|
|
<p>{{event_description}}</p>
|
|
{{/if}}
|
|
<p>We look forward to seeing you there!</p>
|
|
</div>
|
|
</body>
|
|
</html>',
|
|
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<string, any>
|
|
) {
|
|
// 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 │ │
|
|
│ ├──────────────────────┼───────────────────────────────────┤ │
|
|
│ │ <h1>Dues Reminder</h1│ │ │
|
|
│ │ <p>Dear {{member_nam │ ┌─────────────────────────────┐ │ │
|
|
│ │ e}},</p> │ │ Dues Reminder │ │ │
|
|
│ │ <p>This is a friendl │ │ │ │ │
|
|
│ │ y reminder that your │ │ Dear John Doe, │ │ │
|
|
│ │ Monaco USA membershi │ │ │ │ │
|
|
│ │ p dues...</p> │ │ 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 |
|