diff --git a/deploy/init.sql b/deploy/init.sql index b53d9a6..9bc4d61 100644 --- a/deploy/init.sql +++ b/deploy/init.sql @@ -660,8 +660,8 @@ GRANT SELECT, INSERT, UPDATE, DELETE ON public.documents TO authenticated; GRANT SELECT ON public.document_categories TO authenticated; GRANT SELECT, INSERT, UPDATE, DELETE ON public.document_folders TO authenticated; --- Settings (public settings viewable) -GRANT SELECT ON public.app_settings TO authenticated; +-- Settings (admin can manage, all authenticated can read) +GRANT SELECT, INSERT, UPDATE, DELETE ON public.app_settings TO authenticated; -- Email (admin can manage templates, users can view own logs) GRANT SELECT, UPDATE ON public.email_templates TO authenticated; @@ -1409,6 +1409,73 @@ ALTER TABLE public.members ADD COLUMN IF NOT EXISTS onboarding_completed_at TIME CREATE INDEX IF NOT EXISTS idx_members_payment_deadline ON public.members(payment_deadline) WHERE payment_deadline IS NOT NULL; +-- ============================================ +-- MIGRATION 017: In-App Notifications +-- ============================================ + +-- In-app notifications table +CREATE TABLE public.notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + member_id UUID NOT NULL REFERENCES public.members(id) ON DELETE CASCADE, + type TEXT NOT NULL CHECK (type IN ('welcome', 'event', 'payment', 'membership', 'system', 'announcement')), + title TEXT NOT NULL, + message TEXT NOT NULL, + link TEXT, + read_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_notifications_member ON public.notifications(member_id); +CREATE INDEX idx_notifications_unread ON public.notifications(member_id) WHERE read_at IS NULL; +CREATE INDEX idx_notifications_created ON public.notifications(created_at DESC); + +ALTER TABLE public.notifications ENABLE ROW LEVEL SECURITY; + +-- Members can view their own notifications +CREATE POLICY "Members can view own notifications" + ON public.notifications FOR SELECT + TO authenticated + USING (member_id = auth.uid()); + +-- Members can update their own notifications (mark as read) +CREATE POLICY "Members can update own notifications" + ON public.notifications FOR UPDATE + TO authenticated + USING (member_id = auth.uid()); + +-- Admin can manage all notifications +CREATE POLICY "Admin can manage all notifications" + ON public.notifications FOR ALL + TO authenticated + USING ( + EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin') + ); + +-- Grant permissions +GRANT SELECT, UPDATE ON public.notifications TO authenticated; +GRANT ALL ON public.notifications TO service_role; + +-- Trigger to create welcome notification for new members +CREATE OR REPLACE FUNCTION create_welcome_notification() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO public.notifications (member_id, type, title, message, link) + VALUES ( + NEW.id, + 'welcome', + 'Welcome to Monaco USA!', + 'Thank you for joining our community. Complete your profile and explore upcoming events.', + '/profile' + ); + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +CREATE TRIGGER on_member_created_notification + AFTER INSERT ON public.members + FOR EACH ROW + EXECUTE FUNCTION create_welcome_notification(); + -- ============================================ -- GRANT SERVICE_ROLE ACCESS TO ALL TABLES -- ============================================ diff --git a/src/lib/components/layout/Header.svelte b/src/lib/components/layout/Header.svelte index 3b01046..7add357 100644 --- a/src/lib/components/layout/Header.svelte +++ b/src/lib/components/layout/Header.svelte @@ -1,6 +1,7 @@ + + + +
+ + + + + {#if isOpen} + + {/if} +
diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte index a6711e0..8e9f2e2 100644 --- a/src/lib/components/ui/button/button.svelte +++ b/src/lib/components/ui/button/button.svelte @@ -50,16 +50,27 @@ size = 'default', disabled = false, type = 'button', + href, children, ...restProps - }: ButtonProps & { children?: import('svelte').Snippet } = $props(); + }: ButtonProps & { children?: import('svelte').Snippet; href?: string } = $props(); - +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/src/routes/api/notifications/+server.ts b/src/routes/api/notifications/+server.ts new file mode 100644 index 0000000..4334753 --- /dev/null +++ b/src/routes/api/notifications/+server.ts @@ -0,0 +1,25 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ locals }) => { + const { session, user } = await locals.safeGetSession(); + + if (!session || !user) { + return json({ error: 'Not authenticated' }, { status: 401 }); + } + + // Fetch notifications for the current user + const { data: notifications, error } = await locals.supabase + .from('notifications') + .select('*') + .eq('member_id', user.id) + .order('created_at', { ascending: false }) + .limit(50); + + if (error) { + console.error('Failed to fetch notifications:', error); + return json({ error: 'Failed to fetch notifications' }, { status: 500 }); + } + + return json({ notifications: notifications || [] }); +}; diff --git a/src/routes/api/notifications/[id]/read/+server.ts b/src/routes/api/notifications/[id]/read/+server.ts new file mode 100644 index 0000000..45d5624 --- /dev/null +++ b/src/routes/api/notifications/[id]/read/+server.ts @@ -0,0 +1,30 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ locals, params }) => { + const { session, user } = await locals.safeGetSession(); + + if (!session || !user) { + return json({ error: 'Not authenticated' }, { status: 401 }); + } + + const notificationId = params.id; + + if (!notificationId) { + return json({ error: 'Notification ID required' }, { status: 400 }); + } + + // Mark notification as read (RLS ensures user can only update their own) + const { error } = await locals.supabase + .from('notifications') + .update({ read_at: new Date().toISOString() }) + .eq('id', notificationId) + .eq('member_id', user.id); + + if (error) { + console.error('Failed to mark notification as read:', error); + return json({ error: 'Failed to update notification' }, { status: 500 }); + } + + return json({ success: true }); +}; diff --git a/src/routes/api/notifications/read-all/+server.ts b/src/routes/api/notifications/read-all/+server.ts new file mode 100644 index 0000000..1c7acaa --- /dev/null +++ b/src/routes/api/notifications/read-all/+server.ts @@ -0,0 +1,24 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ locals }) => { + const { session, user } = await locals.safeGetSession(); + + if (!session || !user) { + return json({ error: 'Not authenticated' }, { status: 401 }); + } + + // Mark all unread notifications as read for the current user + const { error } = await locals.supabase + .from('notifications') + .update({ read_at: new Date().toISOString() }) + .eq('member_id', user.id) + .is('read_at', null); + + if (error) { + console.error('Failed to mark all notifications as read:', error); + return json({ error: 'Failed to update notifications' }, { status: 500 }); + } + + return json({ success: true }); +}; diff --git a/src/routes/setup/+page.server.ts b/src/routes/setup/+page.server.ts index e2e5a4c..981582b 100644 --- a/src/routes/setup/+page.server.ts +++ b/src/routes/setup/+page.server.ts @@ -1,6 +1,7 @@ import { fail, redirect } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; import { supabaseAdmin } from '$lib/server/supabase'; +import { sendEmail, wrapInMonacoTemplate } from '$lib/server/email'; export const load: PageServerLoad = async () => { // Check if any users exist in the system @@ -203,6 +204,51 @@ export const actions: Actions = { }); } + // Send welcome email to admin + try { + const portalUrl = url.origin; + const welcomeContent = ` +

+ Welcome, ${firstName}! +

+

+ Congratulations! You've successfully set up the Monaco USA Portal as the founding administrator. +

+

+ As the admin, you can now: +

+ +
+ Sign In to Portal +
+

+ If you have any questions, please reach out to the development team. +

+ `; + + await sendEmail({ + to: email, + subject: 'Welcome to Monaco USA Portal - Admin Setup Complete', + html: wrapInMonacoTemplate({ + title: 'Welcome, Administrator!', + content: welcomeContent + }), + recipientId: authData.user.id, + recipientName: `${firstName} ${lastName}`, + emailType: 'welcome', + sentBy: 'system' + }); + } catch (emailError) { + console.error('Failed to send admin welcome email:', emailError); + // Non-critical - continue anyway since the account was created successfully + } + // Success - redirect to login throw redirect(303, '/login?setup=complete'); }