Files
root-org/src/routes/[orgSlug]/+layout.server.ts
AlacrisDevs 2913912cb8 feat: UI overhaul - component library + route layouts with instant headers
- Created 11 reusable UI components: PageHeader, SectionCard, StatCard, StatusBadge, TabBar, MemberList, ActivityFeed, EventCard, ContentSkeleton, QuickLinkGrid, ModuleCard
- Created route-specific +layout.svelte for documents, calendar, kanban, events, settings, account
- Each layout renders PageHeader instantly from parent data, shows ContentSkeleton during navigation
- Removed full-page PageSkeleton from parent layout
- Refactored all pages to use new components instead of inline markup
- Overview page: uses StatCard, SectionCard, EventCard, ActivityFeed, MemberList, QuickLinkGrid
- Events list: uses EventCard, Button components
- Event detail: uses ModuleCard, SectionCard
- Settings/Account/Calendar/Kanban: headers in layouts, toolbars in pages
- Added i18n keys for overview page (EN + ET)
- 0 errors, 112 tests pass
2026-02-07 10:44:53 +02:00

150 lines
4.2 KiB
TypeScript

import { error } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ params, locals }) => {
const { session, user } = await locals.safeGetSession();
if (!session || !user) {
error(401, 'Unauthorized');
}
// Fetch org first (need org.id for subsequent queries)
const { data: org, error: orgError } = await locals.supabase
.from('organizations')
.select('*')
.eq('slug', params.orgSlug)
.single();
if (orgError || !org) {
error(404, 'Organization not found');
}
// Now fetch membership, members, activity, and user profile in parallel (all depend on org.id)
const [membershipResult, membersResult, activityResult, profileResult, docCountResult, folderCountResult, kanbanCountResult, eventCountResult] = await Promise.all([
locals.supabase
.from('org_members')
.select('role, role_id')
.eq('org_id', org.id)
.eq('user_id', user.id)
.single(),
locals.supabase
.from('org_members')
.select('id, user_id, role')
.eq('org_id', org.id)
.limit(10),
locals.supabase
.from('activity_log')
.select(`
id,
action,
entity_type,
entity_id,
entity_name,
created_at,
profiles:user_id (
full_name,
email
)
`)
.eq('org_id', org.id)
.order('created_at', { ascending: false })
.limit(10),
locals.supabase
.from('profiles')
.select('id, email, full_name, avatar_url')
.eq('id', user.id)
.single(),
locals.supabase
.from('documents')
.select('id', { count: 'exact', head: true })
.eq('org_id', org.id)
.eq('type', 'document'),
locals.supabase
.from('documents')
.select('id', { count: 'exact', head: true })
.eq('org_id', org.id)
.eq('type', 'folder'),
locals.supabase
.from('documents')
.select('id', { count: 'exact', head: true })
.eq('org_id', org.id)
.eq('type', 'kanban'),
locals.supabase
.from('events')
.select('id', { count: 'exact', head: true })
.eq('org_id', org.id)
]);
const { data: membership } = membershipResult;
const { data: rawMembers } = membersResult;
const { data: recentActivity } = activityResult;
const { data: profile } = profileResult;
const stats = {
memberCount: (rawMembers ?? []).length,
documentCount: docCountResult.count ?? 0,
folderCount: folderCountResult.count ?? 0,
kanbanCount: kanbanCountResult.count ?? 0,
eventCount: eventCountResult.count ?? 0,
};
if (!membership) {
error(403, 'You are not a member of this organization');
}
// Resolve user's permissions from their custom org_role (if assigned)
let userPermissions: string[] | null = null;
if (membership.role_id) {
const { data: roleData } = await locals.supabase
.from('org_roles')
.select('permissions')
.eq('id', membership.role_id)
.single();
if (roleData?.permissions && Array.isArray(roleData.permissions)) {
userPermissions = roleData.permissions as string[];
}
}
// Fetch profiles separately since org_members.user_id FK points to auth.users, not profiles
const memberUserIds = (rawMembers ?? []).map(m => m.user_id).filter((id): id is string => id !== null);
let memberProfilesMap: Record<string, { id: string; email: string; full_name: string | null; avatar_url: string | null }> = {};
if (memberUserIds.length > 0) {
const { data: memberProfiles } = await locals.supabase
.from('profiles')
.select('id, email, full_name, avatar_url')
.in('id', memberUserIds);
if (memberProfiles) {
memberProfilesMap = Object.fromEntries(memberProfiles.map(p => [p.id, p]));
}
}
const members = (rawMembers ?? []).map(m => ({
...m,
profiles: (m.user_id ? memberProfilesMap[m.user_id] : null) ?? null
}));
// Fetch upcoming events for the overview
const { data: upcomingEvents } = await locals.supabase
.from('events')
.select('id, name, slug, status, start_date, end_date, color, venue_name')
.eq('org_id', org.id)
.in('status', ['planning', 'active'])
.order('start_date', { ascending: true, nullsFirst: false })
.limit(5);
return {
org,
userRole: membership.role,
userPermissions,
members,
recentActivity: recentActivity ?? [],
stats,
upcomingEvents: upcomingEvents ?? [],
user,
profile: profile ?? { id: user.id, email: user.email ?? '', full_name: null, avatar_url: null }
};
};