- 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
150 lines
4.2 KiB
TypeScript
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 }
|
|
};
|
|
};
|