Dashbaord fixes

This commit is contained in:
AlacrisDevs
2026-02-07 22:29:09 +02:00
parent d22847f555
commit 23693db9ec
4 changed files with 509 additions and 43 deletions

View File

@@ -19,6 +19,7 @@
required?: boolean; required?: boolean;
autocomplete?: AutoFill; autocomplete?: AutoFill;
icon?: string; icon?: string;
name?: string;
oninput?: (e: Event) => void; oninput?: (e: Event) => void;
onchange?: (e: Event) => void; onchange?: (e: Event) => void;
onkeydown?: (e: KeyboardEvent) => void; onkeydown?: (e: KeyboardEvent) => void;
@@ -35,6 +36,7 @@
required = false, required = false,
autocomplete, autocomplete,
icon, icon,
name,
oninput, oninput,
onchange, onchange,
onkeydown, onkeydown,
@@ -79,6 +81,7 @@
{disabled} {disabled}
{required} {required}
{autocomplete} {autocomplete}
{name}
{oninput} {oninput}
{onchange} {onchange}
{onkeydown} {onkeydown}

View File

@@ -1,19 +1,15 @@
import { error, redirect } from '@sveltejs/kit'; import { error, redirect, fail } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad, Actions } from './$types';
// Cast helper for columns not yet in generated types // Cast helper for columns not yet in generated types
function db(supabase: any) { function db(supabase: any) {
return supabase as any; return supabase as any;
} }
export const load: PageServerLoad = async ({ locals }) => { async function requireAdmin(locals: any) {
const { session, user } = await locals.safeGetSession(); const { session, user } = await locals.safeGetSession();
if (!session || !user) redirect(303, '/login');
if (!session || !user) {
redirect(303, '/login');
}
// Check platform admin status
const { data: profile } = await db(locals.supabase) const { data: profile } = await db(locals.supabase)
.from('profiles') .from('profiles')
.select('is_platform_admin') .select('is_platform_admin')
@@ -24,6 +20,12 @@ export const load: PageServerLoad = async ({ locals }) => {
error(403, 'Access denied. Platform admin only.'); error(403, 'Access denied. Platform admin only.');
} }
return { session, user };
}
export const load: PageServerLoad = async ({ locals }) => {
const { user } = await requireAdmin(locals);
// Fetch all platform data in parallel // Fetch all platform data in parallel
const [ const [
orgsResult, orgsResult,
@@ -45,8 +47,7 @@ export const load: PageServerLoad = async ({ locals }) => {
.order('created_at', { ascending: false }), .order('created_at', { ascending: false }),
db(locals.supabase) db(locals.supabase)
.from('org_members') .from('org_members')
.select('id, user_id, org_id, role') .select('id, user_id, org_id, role'),
.order('created_at', { ascending: false }),
]); ]);
const organizations = orgsResult.data ?? []; const organizations = orgsResult.data ?? [];
@@ -83,6 +84,7 @@ export const load: PageServerLoad = async ({ locals }) => {
} }
return { return {
currentUserId: user.id,
organizations: organizations.map((o: any) => ({ organizations: organizations.map((o: any) => ({
...o, ...o,
memberCount: orgMemberCounts[o.id] || 0, memberCount: orgMemberCounts[o.id] || 0,
@@ -90,6 +92,7 @@ export const load: PageServerLoad = async ({ locals }) => {
})), })),
profiles, profiles,
events, events,
orgMembers,
stats: { stats: {
totalUsers: profiles.length, totalUsers: profiles.length,
totalOrgs: organizations.length, totalOrgs: organizations.length,
@@ -102,3 +105,152 @@ export const load: PageServerLoad = async ({ locals }) => {
}, },
}; };
}; };
export const actions: Actions = {
deleteOrg: async ({ request, locals }) => {
await requireAdmin(locals);
const formData = await request.formData();
const orgId = formData.get('id') as string;
if (!orgId) return fail(400, { error: 'Missing org ID' });
const { error: err } = await db(locals.supabase)
.from('organizations')
.delete()
.eq('id', orgId);
if (err) return fail(500, { error: err.message });
return { success: true };
},
deleteUser: async ({ request, locals }) => {
await requireAdmin(locals);
const formData = await request.formData();
const userId = formData.get('id') as string;
if (!userId) return fail(400, { error: 'Missing user ID' });
// Delete profile (cascades from auth.users FK)
const { error: err } = await db(locals.supabase)
.from('profiles')
.delete()
.eq('id', userId);
if (err) return fail(500, { error: err.message });
return { success: true };
},
deleteEvent: async ({ request, locals }) => {
await requireAdmin(locals);
const formData = await request.formData();
const eventId = formData.get('id') as string;
if (!eventId) return fail(400, { error: 'Missing event ID' });
const { error: err } = await db(locals.supabase)
.from('events')
.delete()
.eq('id', eventId);
if (err) return fail(500, { error: err.message });
return { success: true };
},
updateEventStatus: async ({ request, locals }) => {
await requireAdmin(locals);
const formData = await request.formData();
const eventId = formData.get('id') as string;
const status = formData.get('status') as string;
if (!eventId || !status) return fail(400, { error: 'Missing event ID or status' });
const { error: err } = await db(locals.supabase)
.from('events')
.update({ status })
.eq('id', eventId);
if (err) return fail(500, { error: err.message });
return { success: true };
},
toggleAdmin: async ({ request, locals }) => {
await requireAdmin(locals);
const formData = await request.formData();
const userId = formData.get('id') as string;
const isAdmin = formData.get('is_admin') === 'true';
if (!userId) return fail(400, { error: 'Missing user ID' });
const { error: err } = await db(locals.supabase)
.from('profiles')
.update({ is_platform_admin: isAdmin })
.eq('id', userId);
if (err) return fail(500, { error: err.message });
return { success: true };
},
updateOrg: async ({ request, locals }) => {
await requireAdmin(locals);
const formData = await request.formData();
const orgId = formData.get('id') as string;
const name = formData.get('name') as string;
const slug = formData.get('slug') as string;
if (!orgId) return fail(400, { error: 'Missing org ID' });
const updates: Record<string, string> = {};
if (name) updates.name = name;
if (slug) updates.slug = slug;
const { error: err } = await db(locals.supabase)
.from('organizations')
.update(updates)
.eq('id', orgId);
if (err) return fail(500, { error: err.message });
return { success: true };
},
updateEvent: async ({ request, locals }) => {
await requireAdmin(locals);
const formData = await request.formData();
const eventId = formData.get('id') as string;
const name = formData.get('name') as string;
const slug = formData.get('slug') as string;
const status = formData.get('status') as string;
const startDate = formData.get('start_date') as string;
const endDate = formData.get('end_date') as string;
if (!eventId) return fail(400, { error: 'Missing event ID' });
const updates: Record<string, string | null> = {};
if (name) updates.name = name;
if (slug) updates.slug = slug;
if (status) updates.status = status;
if (startDate) updates.start_date = startDate;
if (endDate) updates.end_date = endDate;
const { error: err } = await db(locals.supabase)
.from('events')
.update(updates)
.eq('id', eventId);
if (err) return fail(500, { error: err.message });
return { success: true };
},
updateUser: async ({ request, locals }) => {
await requireAdmin(locals);
const formData = await request.formData();
const userId = formData.get('id') as string;
const fullName = formData.get('full_name') as string;
const email = formData.get('email') as string;
if (!userId) return fail(400, { error: 'Missing user ID' });
const updates: Record<string, any> = {};
if (fullName !== null) updates.full_name = fullName;
if (email) updates.email = email;
const { error: err } = await db(locals.supabase)
.from('profiles')
.update(updates)
.eq('id', userId);
if (err) return fail(500, { error: err.message });
return { success: true };
},
};

View File

@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Button, Badge, Avatar, Card, StatCard, TabBar, Input } from "$lib/components/ui"; import { enhance } from "$app/forms";
import { invalidateAll } from "$app/navigation";
import { Button, Badge, Avatar, StatCard, TabBar, Input, Modal } from "$lib/components/ui";
let { data } = $props(); let { data } = $props();
@@ -8,6 +10,13 @@
let userSearch = $state(""); let userSearch = $state("");
let eventSearch = $state(""); let eventSearch = $state("");
// Modal state
let confirmModal = $state<{ type: string; id: string; name: string } | null>(null);
let editOrgModal = $state<any>(null);
let editUserModal = $state<any>(null);
let editEventModal = $state<any>(null);
let actionLoading = $state(false);
const filteredOrgs = $derived( const filteredOrgs = $derived(
orgSearch orgSearch
? data.organizations.filter((o: any) => ? data.organizations.filter((o: any) =>
@@ -44,6 +53,11 @@
}); });
} }
function formatDateInput(dateStr: string | null) {
if (!dateStr) return "";
return new Date(dateStr).toISOString().split("T")[0];
}
function timeAgo(dateStr: string | null) { function timeAgo(dateStr: string | null) {
if (!dateStr) return "—"; if (!dateStr) return "—";
const diff = Date.now() - new Date(dateStr).getTime(); const diff = Date.now() - new Date(dateStr).getTime();
@@ -64,10 +78,33 @@
draft: "text-light/40 bg-light/5", draft: "text-light/40 bg-light/5",
}; };
// Find org name by id const eventStatuses = ["planning", "active", "completed", "archived"];
const orgMap = $derived( const orgMap = $derived(
Object.fromEntries(data.organizations.map((o: any) => [o.id, o])), Object.fromEntries(data.organizations.map((o: any) => [o.id, o])),
); );
// Count orgs per user
const userOrgCounts = $derived(() => {
const counts: Record<string, number> = {};
for (const m of data.orgMembers) {
counts[m.user_id] = (counts[m.user_id] || 0) + 1;
}
return counts;
});
function enhanceAction() {
return ({ result }: any) => {
actionLoading = false;
if (result.type === "success") {
confirmModal = null;
editOrgModal = null;
editUserModal = null;
editEventModal = null;
invalidateAll();
}
};
}
</script> </script>
<svelte:head> <svelte:head>
@@ -104,13 +141,12 @@
<div class="max-w-7xl mx-auto px-6 py-6 space-y-6"> <div class="max-w-7xl mx-auto px-6 py-6 space-y-6">
<!-- Stats Overview --> <!-- Stats Overview -->
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-3"> <div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-3">
<StatCard label="Total Users" value={data.stats.totalUsers} icon="group" /> <StatCard label="Total Users" value={data.stats.totalUsers} icon="group" />
<StatCard label="Organizations" value={data.stats.totalOrgs} icon="business" /> <StatCard label="Organizations" value={data.stats.totalOrgs} icon="business" />
<StatCard label="Total Events" value={data.stats.totalEvents} icon="event" /> <StatCard label="Total Events" value={data.stats.totalEvents} icon="event" />
<StatCard label="Memberships" value={data.stats.totalMemberships} icon="badge" /> <StatCard label="New Users (7d)" value={data.stats.newUsersLast7d} icon="person_add" />
<StatCard label="New (7d)" value={data.stats.newUsersLast7d} icon="person_add" /> <StatCard label="New Users (30d)" value={data.stats.newUsersLast30d} icon="trending_up" />
<StatCard label="New (30d)" value={data.stats.newUsersLast30d} icon="trending_up" />
<StatCard label="Active Events" value={data.stats.activeEvents} icon="play_circle" /> <StatCard label="Active Events" value={data.stats.activeEvents} icon="play_circle" />
<StatCard label="Planning" value={data.stats.planningEvents} icon="edit_calendar" /> <StatCard label="Planning" value={data.stats.planningEvents} icon="edit_calendar" />
</div> </div>
@@ -134,13 +170,7 @@
<div class="bg-dark/30 border border-light/5 rounded-xl p-5"> <div class="bg-dark/30 border border-light/5 rounded-xl p-5">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 class="font-heading text-body text-white">Recent Organizations</h3> <h3 class="font-heading text-body text-white">Recent Organizations</h3>
<button <button type="button" class="text-[11px] text-primary hover:text-primary/80 transition-colors" onclick={() => (activeTab = "organizations")}>View all →</button>
type="button"
class="text-[11px] text-primary hover:text-primary/80 transition-colors"
onclick={() => (activeTab = "organizations")}
>
View all →
</button>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
{#each data.organizations.slice(0, 5) as org} {#each data.organizations.slice(0, 5) as org}
@@ -168,13 +198,7 @@
<div class="bg-dark/30 border border-light/5 rounded-xl p-5"> <div class="bg-dark/30 border border-light/5 rounded-xl p-5">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 class="font-heading text-body text-white">Recent Users</h3> <h3 class="font-heading text-body text-white">Recent Users</h3>
<button <button type="button" class="text-[11px] text-primary hover:text-primary/80 transition-colors" onclick={() => (activeTab = "users")}>View all →</button>
type="button"
class="text-[11px] text-primary hover:text-primary/80 transition-colors"
onclick={() => (activeTab = "users")}
>
View all →
</button>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
{#each data.profiles.slice(0, 5) as profile} {#each data.profiles.slice(0, 5) as profile}
@@ -204,13 +228,7 @@
<div class="bg-dark/30 border border-light/5 rounded-xl p-5 lg:col-span-2"> <div class="bg-dark/30 border border-light/5 rounded-xl p-5 lg:col-span-2">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 class="font-heading text-body text-white">Recent Events</h3> <h3 class="font-heading text-body text-white">Recent Events</h3>
<button <button type="button" class="text-[11px] text-primary hover:text-primary/80 transition-colors" onclick={() => (activeTab = "events")}>View all →</button>
type="button"
class="text-[11px] text-primary hover:text-primary/80 transition-colors"
onclick={() => (activeTab = "events")}
>
View all →
</button>
</div> </div>
{#if data.events.length > 0} {#if data.events.length > 0}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
@@ -252,6 +270,29 @@
<p class="text-body-sm text-light/30 text-center py-4">No events yet</p> <p class="text-body-sm text-light/30 text-center py-4">No events yet</p>
{/if} {/if}
</div> </div>
<!-- Event Status Legend -->
<div class="bg-dark/30 border border-light/5 rounded-xl p-5 lg:col-span-2">
<h3 class="font-heading text-body text-white mb-3">Event Status Guide</h3>
<div class="grid md:grid-cols-4 gap-4">
<div class="flex items-start gap-3">
<span class="text-[10px] px-2 py-0.5 rounded-full shrink-0 text-amber-400 bg-amber-400/10">planning</span>
<p class="text-[11px] text-light/40">Event is being prepared. Team is setting up departments, tasks, and logistics.</p>
</div>
<div class="flex items-start gap-3">
<span class="text-[10px] px-2 py-0.5 rounded-full shrink-0 text-emerald-400 bg-emerald-400/10">active</span>
<p class="text-[11px] text-light/40">Event is currently live/happening. All systems go.</p>
</div>
<div class="flex items-start gap-3">
<span class="text-[10px] px-2 py-0.5 rounded-full shrink-0 text-blue-400 bg-blue-400/10">completed</span>
<p class="text-[11px] text-light/40">Event has finished. Data is preserved for review.</p>
</div>
<div class="flex items-start gap-3">
<span class="text-[10px] px-2 py-0.5 rounded-full shrink-0 text-light/40 bg-light/5">archived</span>
<p class="text-[11px] text-light/40">Event is hidden from normal views. Can be restored.</p>
</div>
</div>
</div>
</div> </div>
{:else if activeTab === "organizations"} {:else if activeTab === "organizations"}
@@ -268,6 +309,7 @@
<th class="text-[10px] text-light/40 font-body py-3 px-4">Members</th> <th class="text-[10px] text-light/40 font-body py-3 px-4">Members</th>
<th class="text-[10px] text-light/40 font-body py-3 px-4">Events</th> <th class="text-[10px] text-light/40 font-body py-3 px-4">Events</th>
<th class="text-[10px] text-light/40 font-body py-3 px-4">Created</th> <th class="text-[10px] text-light/40 font-body py-3 px-4">Created</th>
<th class="text-[10px] text-light/40 font-body py-3 px-4 text-right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -287,11 +329,31 @@
<Badge variant="primary" size="sm">{org.eventCount}</Badge> <Badge variant="primary" size="sm">{org.eventCount}</Badge>
</td> </td>
<td class="py-3 px-4 text-[11px] text-light/30">{formatDate(org.created_at)}</td> <td class="py-3 px-4 text-[11px] text-light/30">{formatDate(org.created_at)}</td>
<td class="py-3 px-4">
<div class="flex items-center justify-end gap-1">
<button
type="button"
class="p-1.5 text-light/30 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
title="Edit organization"
onclick={() => (editOrgModal = { id: org.id, name: org.name, slug: org.slug })}
>
<span class="material-symbols-rounded" style="font-size: 16px;">edit</span>
</button>
<button
type="button"
class="p-1.5 text-light/30 hover:text-red-400 hover:bg-red-400/10 rounded-lg transition-colors"
title="Delete organization"
onclick={() => (confirmModal = { type: "deleteOrg", id: org.id, name: org.name })}
>
<span class="material-symbols-rounded" style="font-size: 16px;">delete</span>
</button>
</div>
</td>
</tr> </tr>
{/each} {/each}
{#if filteredOrgs.length === 0} {#if filteredOrgs.length === 0}
<tr> <tr>
<td colspan="5" class="py-8 text-center text-body-sm text-light/30"> <td colspan="6" class="py-8 text-center text-body-sm text-light/30">
{orgSearch ? "No organizations match your search" : "No organizations yet"} {orgSearch ? "No organizations match your search" : "No organizations yet"}
</td> </td>
</tr> </tr>
@@ -313,8 +375,10 @@
<tr class="border-b border-light/5 bg-dark/20"> <tr class="border-b border-light/5 bg-dark/20">
<th class="text-[10px] text-light/40 font-body py-3 px-4">User</th> <th class="text-[10px] text-light/40 font-body py-3 px-4">User</th>
<th class="text-[10px] text-light/40 font-body py-3 px-4">Email</th> <th class="text-[10px] text-light/40 font-body py-3 px-4">Email</th>
<th class="text-[10px] text-light/40 font-body py-3 px-4">Organizations</th>
<th class="text-[10px] text-light/40 font-body py-3 px-4">Role</th> <th class="text-[10px] text-light/40 font-body py-3 px-4">Role</th>
<th class="text-[10px] text-light/40 font-body py-3 px-4">Joined</th> <th class="text-[10px] text-light/40 font-body py-3 px-4">Joined</th>
<th class="text-[10px] text-light/40 font-body py-3 px-4 text-right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -327,6 +391,9 @@
</div> </div>
</td> </td>
<td class="py-3 px-4 text-body-sm text-light/40">{profile.email}</td> <td class="py-3 px-4 text-body-sm text-light/40">{profile.email}</td>
<td class="py-3 px-4">
<Badge variant="default" size="sm">{userOrgCounts()[profile.id] ?? 0}</Badge>
</td>
<td class="py-3 px-4"> <td class="py-3 px-4">
{#if profile.is_platform_admin} {#if profile.is_platform_admin}
<Badge variant="error" size="sm">Platform Admin</Badge> <Badge variant="error" size="sm">Platform Admin</Badge>
@@ -335,11 +402,50 @@
{/if} {/if}
</td> </td>
<td class="py-3 px-4 text-[11px] text-light/30">{formatDate(profile.created_at)}</td> <td class="py-3 px-4 text-[11px] text-light/30">{formatDate(profile.created_at)}</td>
<td class="py-3 px-4">
<div class="flex items-center justify-end gap-1">
<button
type="button"
class="p-1.5 text-light/30 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
title="Edit user"
onclick={() => (editUserModal = { id: profile.id, full_name: profile.full_name ?? "", email: profile.email, is_platform_admin: profile.is_platform_admin })}
>
<span class="material-symbols-rounded" style="font-size: 16px;">edit</span>
</button>
{#if profile.is_platform_admin && profile.id !== data.currentUserId}
<form method="POST" action="?/toggleAdmin" use:enhance={enhanceAction}>
<input type="hidden" name="id" value={profile.id} />
<input type="hidden" name="is_admin" value="false" />
<button type="submit" class="p-1.5 text-amber-400 hover:text-amber-300 hover:bg-amber-400/10 rounded-lg transition-colors" title="Remove admin">
<span class="material-symbols-rounded" style="font-size: 16px; font-variation-settings: 'FILL' 1;">shield</span>
</button>
</form>
{:else if !profile.is_platform_admin}
<form method="POST" action="?/toggleAdmin" use:enhance={enhanceAction}>
<input type="hidden" name="id" value={profile.id} />
<input type="hidden" name="is_admin" value="true" />
<button type="submit" class="p-1.5 text-light/30 hover:text-amber-400 hover:bg-amber-400/10 rounded-lg transition-colors" title="Make admin">
<span class="material-symbols-rounded" style="font-size: 16px;">shield</span>
</button>
</form>
{/if}
{#if profile.id !== data.currentUserId}
<button
type="button"
class="p-1.5 text-light/30 hover:text-red-400 hover:bg-red-400/10 rounded-lg transition-colors"
title="Delete user"
onclick={() => (confirmModal = { type: "deleteUser", id: profile.id, name: profile.full_name ?? profile.email })}
>
<span class="material-symbols-rounded" style="font-size: 16px;">delete</span>
</button>
{/if}
</div>
</td>
</tr> </tr>
{/each} {/each}
{#if filteredUsers.length === 0} {#if filteredUsers.length === 0}
<tr> <tr>
<td colspan="4" class="py-8 text-center text-body-sm text-light/30"> <td colspan="6" class="py-8 text-center text-body-sm text-light/30">
{userSearch ? "No users match your search" : "No users yet"} {userSearch ? "No users match your search" : "No users yet"}
</td> </td>
</tr> </tr>
@@ -365,6 +471,7 @@
<th class="text-[10px] text-light/40 font-body py-3 px-4">Start</th> <th class="text-[10px] text-light/40 font-body py-3 px-4">Start</th>
<th class="text-[10px] text-light/40 font-body py-3 px-4">End</th> <th class="text-[10px] text-light/40 font-body py-3 px-4">End</th>
<th class="text-[10px] text-light/40 font-body py-3 px-4">Created</th> <th class="text-[10px] text-light/40 font-body py-3 px-4">Created</th>
<th class="text-[10px] text-light/40 font-body py-3 px-4 text-right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -380,18 +487,47 @@
{orgMap[event.org_id]?.name ?? "—"} {orgMap[event.org_id]?.name ?? "—"}
</td> </td>
<td class="py-3 px-4"> <td class="py-3 px-4">
<span class="text-[10px] px-2 py-0.5 rounded-full capitalize {statusColors[event.status] ?? 'text-light/40 bg-light/5'}"> <form method="POST" action="?/updateEventStatus" use:enhance={enhanceAction} class="inline">
{event.status} <input type="hidden" name="id" value={event.id} />
</span> <select
name="status"
class="text-[10px] px-2 py-0.5 rounded-full capitalize cursor-pointer border-0 appearance-none bg-transparent {statusColors[event.status] ?? 'text-light/40 bg-light/5'}"
onchange={(e) => e.currentTarget.form?.requestSubmit()}
>
{#each eventStatuses as s}
<option value={s} selected={event.status === s} class="bg-surface text-white">{s}</option>
{/each}
</select>
</form>
</td> </td>
<td class="py-3 px-4 text-[11px] text-light/40">{formatDate(event.start_date)}</td> <td class="py-3 px-4 text-[11px] text-light/40">{formatDate(event.start_date)}</td>
<td class="py-3 px-4 text-[11px] text-light/40">{formatDate(event.end_date)}</td> <td class="py-3 px-4 text-[11px] text-light/40">{formatDate(event.end_date)}</td>
<td class="py-3 px-4 text-[11px] text-light/30">{timeAgo(event.created_at)}</td> <td class="py-3 px-4 text-[11px] text-light/30">{timeAgo(event.created_at)}</td>
<td class="py-3 px-4">
<div class="flex items-center justify-end gap-1">
<button
type="button"
class="p-1.5 text-light/30 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
title="Edit event"
onclick={() => (editEventModal = { id: event.id, name: event.name, slug: event.slug, status: event.status, start_date: formatDateInput(event.start_date), end_date: formatDateInput(event.end_date) })}
>
<span class="material-symbols-rounded" style="font-size: 16px;">edit</span>
</button>
<button
type="button"
class="p-1.5 text-light/30 hover:text-red-400 hover:bg-red-400/10 rounded-lg transition-colors"
title="Delete event"
onclick={() => (confirmModal = { type: "deleteEvent", id: event.id, name: event.name })}
>
<span class="material-symbols-rounded" style="font-size: 16px;">delete</span>
</button>
</div>
</td>
</tr> </tr>
{/each} {/each}
{#if filteredEvents.length === 0} {#if filteredEvents.length === 0}
<tr> <tr>
<td colspan="6" class="py-8 text-center text-body-sm text-light/30"> <td colspan="7" class="py-8 text-center text-body-sm text-light/30">
{eventSearch ? "No events match your search" : "No events yet"} {eventSearch ? "No events match your search" : "No events yet"}
</td> </td>
</tr> </tr>
@@ -404,3 +540,122 @@
{/if} {/if}
</div> </div>
</div> </div>
<!-- Delete Confirmation Modal -->
<Modal isOpen={!!confirmModal} onClose={() => (confirmModal = null)} title="Confirm Delete">
{#if confirmModal}
<p class="text-light/70 text-body-sm mb-2">
Are you sure you want to delete <strong class="text-white">{confirmModal.name}</strong>?
</p>
<p class="text-red-400/70 text-[11px] mb-4">This action cannot be undone. All associated data will be permanently removed.</p>
<form
method="POST"
action="?/{confirmModal.type}"
use:enhance={() => { actionLoading = true; return enhanceAction(); }}
>
<input type="hidden" name="id" value={confirmModal.id} />
<div class="flex gap-3 justify-end">
<Button variant="secondary" onclick={() => (confirmModal = null)}>Cancel</Button>
<Button variant="danger" type="submit" loading={actionLoading}>Delete</Button>
</div>
</form>
{/if}
</Modal>
<!-- Edit Organization Modal -->
<Modal isOpen={!!editOrgModal} onClose={() => (editOrgModal = null)} title="Edit Organization">
{#if editOrgModal}
<form
method="POST"
action="?/updateOrg"
use:enhance={() => { actionLoading = true; return enhanceAction(); }}
class="space-y-4"
>
<input type="hidden" name="id" value={editOrgModal.id} />
<Input label="Name" name="name" bind:value={editOrgModal.name} />
<Input label="Slug" name="slug" bind:value={editOrgModal.slug} />
<div class="flex gap-3 justify-end">
<Button variant="secondary" onclick={() => (editOrgModal = null)}>Cancel</Button>
<Button type="submit" loading={actionLoading}>Save</Button>
</div>
</form>
{/if}
</Modal>
<!-- Edit User Modal -->
<Modal isOpen={!!editUserModal} onClose={() => (editUserModal = null)} title="Edit User">
{#if editUserModal}
<form
method="POST"
action="?/updateUser"
use:enhance={() => { actionLoading = true; return enhanceAction(); }}
class="space-y-4"
>
<input type="hidden" name="id" value={editUserModal.id} />
<Input label="Full Name" name="full_name" bind:value={editUserModal.full_name} />
<Input label="Email" name="email" bind:value={editUserModal.email} />
<div class="flex items-center gap-3 py-2">
<label class="flex items-center gap-2 text-body-sm text-light/60 cursor-pointer">
<input type="checkbox" bind:checked={editUserModal.is_platform_admin} class="accent-primary" />
Platform Admin
</label>
</div>
<div class="flex gap-3 justify-end">
<Button variant="secondary" onclick={() => (editUserModal = null)}>Cancel</Button>
<Button type="submit" loading={actionLoading}>Save</Button>
</div>
</form>
{/if}
</Modal>
<!-- Edit Event Modal -->
<Modal isOpen={!!editEventModal} onClose={() => (editEventModal = null)} title="Edit Event">
{#if editEventModal}
<form
method="POST"
action="?/updateEvent"
use:enhance={() => { actionLoading = true; return enhanceAction(); }}
class="space-y-4"
>
<input type="hidden" name="id" value={editEventModal.id} />
<Input label="Name" name="name" bind:value={editEventModal.name} />
<Input label="Slug" name="slug" bind:value={editEventModal.slug} />
<div>
<label class="block text-body-sm text-light/60 mb-1">Status</label>
<select
name="status"
bind:value={editEventModal.status}
class="w-full bg-dark/50 border border-light/10 rounded-xl px-3 py-2 text-body-sm text-white focus:outline-none focus:border-primary/50"
>
{#each eventStatuses as s}
<option value={s} class="bg-surface">{s.charAt(0).toUpperCase() + s.slice(1)}</option>
{/each}
</select>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-body-sm text-light/60 mb-1">Start Date</label>
<input
type="date"
name="start_date"
bind:value={editEventModal.start_date}
class="w-full bg-dark/50 border border-light/10 rounded-xl px-3 py-2 text-body-sm text-white focus:outline-none focus:border-primary/50"
/>
</div>
<div>
<label class="block text-body-sm text-light/60 mb-1">End Date</label>
<input
type="date"
name="end_date"
bind:value={editEventModal.end_date}
class="w-full bg-dark/50 border border-light/10 rounded-xl px-3 py-2 text-body-sm text-white focus:outline-none focus:border-primary/50"
/>
</div>
</div>
<div class="flex gap-3 justify-end">
<Button variant="secondary" onclick={() => (editEventModal = null)}>Cancel</Button>
<Button type="submit" loading={actionLoading}>Save</Button>
</div>
</form>
{/if}
</Modal>

View File

@@ -0,0 +1,56 @@
-- Platform admins can read/write all data across the platform
-- This bypasses org-membership-based RLS for users with is_platform_admin = true
-- Helper function to check if current user is a platform admin
CREATE OR REPLACE FUNCTION is_platform_admin()
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM profiles
WHERE id = auth.uid()
AND is_platform_admin = true
);
$$ LANGUAGE sql SECURITY DEFINER STABLE;
-- Organizations: platform admins can do everything
CREATE POLICY "Platform admins full access to organizations" ON organizations
USING (is_platform_admin()) WITH CHECK (is_platform_admin());
-- Org Members: platform admins can see all memberships
CREATE POLICY "Platform admins full access to org_members" ON org_members
USING (is_platform_admin()) WITH CHECK (is_platform_admin());
-- Profiles: platform admins can update any profile
CREATE POLICY "Platform admins can update profiles" ON profiles FOR UPDATE
USING (is_platform_admin()) WITH CHECK (is_platform_admin());
-- Events: platform admins can do everything
CREATE POLICY "Platform admins full access to events" ON events
USING (is_platform_admin()) WITH CHECK (is_platform_admin());
-- Event members: platform admins can do everything
CREATE POLICY "Platform admins full access to event_members" ON event_members
USING (is_platform_admin()) WITH CHECK (is_platform_admin());
-- Documents: platform admins can do everything
CREATE POLICY "Platform admins full access to documents" ON documents
USING (is_platform_admin()) WITH CHECK (is_platform_admin());
-- Kanban boards: platform admins can do everything
CREATE POLICY "Platform admins full access to kanban_boards" ON kanban_boards
USING (is_platform_admin()) WITH CHECK (is_platform_admin());
-- Calendar events: platform admins can do everything
CREATE POLICY "Platform admins full access to calendar_events" ON calendar_events
USING (is_platform_admin()) WITH CHECK (is_platform_admin());
-- Org roles: platform admins can do everything
CREATE POLICY "Platform admins full access to org_roles" ON org_roles
USING (is_platform_admin()) WITH CHECK (is_platform_admin());
-- Org invites: platform admins can do everything
CREATE POLICY "Platform admins full access to org_invites" ON org_invites
USING (is_platform_admin()) WITH CHECK (is_platform_admin());
-- Event departments: platform admins can do everything
CREATE POLICY "Platform admins full access to event_departments" ON event_departments
USING (is_platform_admin()) WITH CHECK (is_platform_admin());