diff --git a/src/lib/components/app-sidebar.svelte b/src/lib/components/app-sidebar.svelte
index f05f719..70b70b6 100644
--- a/src/lib/components/app-sidebar.svelte
+++ b/src/lib/components/app-sidebar.svelte
@@ -54,12 +54,20 @@
-
-
-
-
-
- {#snippet child({ props })}
-
- {/snippet}
-
-
-
-
-
diff --git a/src/lib/components/organization-switcher.svelte b/src/lib/components/organization-switcher.svelte
new file mode 100644
index 0000000..26e36a8
--- /dev/null
+++ b/src/lib/components/organization-switcher.svelte
@@ -0,0 +1,350 @@
+
+
+{#snippet organizationFields(prefix: string, organization?: Organization)}
+
+{/snippet}
+
+{#snippet bankFields(prefix: string, organization?: Organization)}
+
+{/snippet}
+
+
+
+
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
+
+
+
+
+
+
+
+ Organization settings
+ Update details for the active organization and invoice payments.
+
+
+
+
+
+
+
+
+
+ Create organization
+ Add organization details for records and invoice payments.
+
+
+
+
+
diff --git a/src/lib/schemas/organizations.schema.ts b/src/lib/schemas/organizations.schema.ts
new file mode 100644
index 0000000..eecbaef
--- /dev/null
+++ b/src/lib/schemas/organizations.schema.ts
@@ -0,0 +1,25 @@
+import { z } from 'zod';
+import { idSchema, optionalText, requiredText } from './shared.schema';
+
+const organizationDetailsSchema = z.object({
+ name: requiredText('Organization name'),
+ workspace: requiredText('Workspace', 120).default('Blueprint Workspace'),
+ addressLine1: optionalText(160),
+ addressLine2: optionalText(160),
+ city: optionalText(100),
+ region: optionalText(100),
+ postcode: optionalText(24),
+ country: requiredText('Country', 80).default('United Kingdom'),
+ bankName: optionalText(120),
+ bankAccountName: optionalText(120),
+ bankAccountNumber: optionalText(40),
+ bankSortCode: optionalText(40),
+ bankIban: optionalText(64),
+ bankSwift: optionalText(32)
+});
+
+export const organizationCreateSchema = organizationDetailsSchema.extend({
+ redirectTo: optionalText(500).default('/dashboard')
+});
+
+export const organizationEditSchema = organizationDetailsSchema.extend(idSchema.shape);
diff --git a/src/routes/dashboard/organizations/+server.ts b/src/routes/dashboard/organizations/+server.ts
new file mode 100644
index 0000000..b27b278
--- /dev/null
+++ b/src/routes/dashboard/organizations/+server.ts
@@ -0,0 +1,50 @@
+import { redirect, error } from '@sveltejs/kit';
+import { db } from '$lib/server/db';
+import { organizations } from '$lib/server/db/schema';
+import { organizationCreateSchema } from '$lib/schemas/organizations.schema';
+import { setActiveOrganization } from '$lib/server/organizations';
+import type { RequestHandler } from './$types';
+
+function safeRedirect(value: string) {
+ return value.startsWith('/') && !value.startsWith('//') ? value : '/dashboard';
+}
+
+export const POST: RequestHandler = async ({ locals, request }) => {
+ if (!locals.user) {
+ redirect(303, '/login');
+ }
+
+ const formData = await request.formData();
+ const parsed = organizationCreateSchema.safeParse(Object.fromEntries(formData));
+
+ if (!parsed.success) {
+ error(400, 'Check the organization details and try again.');
+ }
+
+ const id = crypto.randomUUID();
+ const now = new Date();
+
+ await db.insert(organizations).values({
+ id,
+ name: parsed.data.name,
+ workspace: parsed.data.workspace,
+ addressLine1: parsed.data.addressLine1 || null,
+ addressLine2: parsed.data.addressLine2 || null,
+ city: parsed.data.city || null,
+ region: parsed.data.region || null,
+ postcode: parsed.data.postcode || null,
+ country: parsed.data.country,
+ bankName: parsed.data.bankName || null,
+ bankAccountName: parsed.data.bankAccountName || null,
+ bankAccountNumber: parsed.data.bankAccountNumber || null,
+ bankSortCode: parsed.data.bankSortCode || null,
+ bankIban: parsed.data.bankIban || null,
+ bankSwift: parsed.data.bankSwift || null,
+ createdAt: now,
+ updatedAt: now
+ });
+
+ await setActiveOrganization(locals, id);
+
+ redirect(303, safeRedirect(parsed.data.redirectTo));
+};
diff --git a/src/routes/dashboard/organizations/current/+server.ts b/src/routes/dashboard/organizations/current/+server.ts
new file mode 100644
index 0000000..b854ecc
--- /dev/null
+++ b/src/routes/dashboard/organizations/current/+server.ts
@@ -0,0 +1,56 @@
+import { and, eq, isNull } from 'drizzle-orm';
+import { error, redirect } from '@sveltejs/kit';
+import { db } from '$lib/server/db';
+import { organizations } from '$lib/server/db/schema';
+import { loadOrganizationContext } from '$lib/server/organizations';
+import { organizationEditSchema } from '$lib/schemas/organizations.schema';
+import type { RequestHandler } from './$types';
+
+function safeRedirect(value: FormDataEntryValue | null) {
+ return typeof value === 'string' && value.startsWith('/') && !value.startsWith('//')
+ ? value
+ : '/dashboard';
+}
+
+export const POST: RequestHandler = async ({ locals, request }) => {
+ if (!locals.user) {
+ redirect(303, '/login');
+ }
+
+ const { activeOrganizationId } = await loadOrganizationContext(locals);
+ const formData = await request.formData();
+ const parsed = organizationEditSchema.safeParse(Object.fromEntries(formData));
+
+ if (!parsed.success || parsed.data.id !== activeOrganizationId) {
+ error(400, 'Check the organization details and try again.');
+ }
+
+ await db
+ .update(organizations)
+ .set({
+ name: parsed.data.name,
+ workspace: parsed.data.workspace,
+ addressLine1: parsed.data.addressLine1 || null,
+ addressLine2: parsed.data.addressLine2 || null,
+ city: parsed.data.city || null,
+ region: parsed.data.region || null,
+ postcode: parsed.data.postcode || null,
+ country: parsed.data.country,
+ bankName: parsed.data.bankName || null,
+ bankAccountName: parsed.data.bankAccountName || null,
+ bankAccountNumber: parsed.data.bankAccountNumber || null,
+ bankSortCode: parsed.data.bankSortCode || null,
+ bankIban: parsed.data.bankIban || null,
+ bankSwift: parsed.data.bankSwift || null,
+ updatedAt: new Date()
+ })
+ .where(
+ and(
+ eq(organizations.id, activeOrganizationId),
+ eq(organizations.id, parsed.data.id),
+ isNull(organizations.archivedAt)
+ )
+ );
+
+ redirect(303, safeRedirect(formData.get('redirectTo')));
+};
diff --git a/src/routes/dashboard/switch-organization/+server.ts b/src/routes/dashboard/switch-organization/+server.ts
new file mode 100644
index 0000000..bcf7727
--- /dev/null
+++ b/src/routes/dashboard/switch-organization/+server.ts
@@ -0,0 +1,18 @@
+import { redirect } from '@sveltejs/kit';
+import { setActiveOrganization } from '$lib/server/organizations';
+import type { RequestHandler } from './$types';
+
+export const POST: RequestHandler = async ({ locals, request }) => {
+ const formData = await request.formData();
+ const organizationId = formData.get('organizationId');
+ const redirectTo = formData.get('redirectTo');
+
+ if (typeof organizationId === 'string') {
+ await setActiveOrganization(locals, organizationId);
+ }
+
+ redirect(
+ 303,
+ typeof redirectTo === 'string' && redirectTo.startsWith('/') ? redirectTo : '/dashboard'
+ );
+};