7c57a22a3c
Rename the centre switcher to organization switcher and wire it to loaded organizations. Add dropdown actions for switching organizations, creating a new organization, and editing the active organization in dialogs. Persist organization changes through dashboard endpoints while preserving the current redirect path.
351 lines
11 KiB
Svelte
351 lines
11 KiB
Svelte
<script lang="ts">
|
|
import Building2Icon from '@lucide/svelte/icons/building-2';
|
|
import CheckIcon from '@lucide/svelte/icons/check';
|
|
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
|
|
import LandmarkIcon from '@lucide/svelte/icons/landmark';
|
|
import PlusIcon from '@lucide/svelte/icons/plus';
|
|
import SettingsIcon from '@lucide/svelte/icons/settings';
|
|
import { page } from '$app/state';
|
|
import { Button } from '$lib/components/ui/button/index.js';
|
|
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
|
import * as Field from '$lib/components/ui/field/index.js';
|
|
import { Input } from '$lib/components/ui/input/index.js';
|
|
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
|
|
|
type Organization = {
|
|
id: string;
|
|
name: string;
|
|
workspace: string;
|
|
addressLine1?: string | null;
|
|
addressLine2?: string | null;
|
|
city?: string | null;
|
|
region?: string | null;
|
|
postcode?: string | null;
|
|
country?: string | null;
|
|
bankName?: string | null;
|
|
bankAccountName?: string | null;
|
|
bankAccountNumber?: string | null;
|
|
bankSortCode?: string | null;
|
|
bankIban?: string | null;
|
|
bankSwift?: string | null;
|
|
};
|
|
|
|
let {
|
|
organizations,
|
|
activeOrganization
|
|
}: {
|
|
organizations: Organization[];
|
|
activeOrganization: Organization;
|
|
} = $props();
|
|
|
|
let settingsOpen = $state(false);
|
|
let createOpen = $state(false);
|
|
const redirectTo = $derived(page.url.pathname + page.url.search);
|
|
</script>
|
|
|
|
{#snippet organizationFields(prefix: string, organization?: Organization)}
|
|
<div class="grid gap-3">
|
|
<div class="grid gap-2 sm:grid-cols-2">
|
|
<Field.Field>
|
|
<Field.Label for={`${prefix}-name`}>Name</Field.Label>
|
|
<Input
|
|
id={`${prefix}-name`}
|
|
name="name"
|
|
type="text"
|
|
value={organization?.name ?? ''}
|
|
required
|
|
/>
|
|
</Field.Field>
|
|
<Field.Field>
|
|
<Field.Label for={`${prefix}-workspace`}>Workspace</Field.Label>
|
|
<Input
|
|
id={`${prefix}-workspace`}
|
|
name="workspace"
|
|
type="text"
|
|
value={organization?.workspace ?? 'Blueprint Workspace'}
|
|
required
|
|
/>
|
|
</Field.Field>
|
|
</div>
|
|
|
|
<Field.Field>
|
|
<Field.Label for={`${prefix}-address-line-1`}>Address</Field.Label>
|
|
<Input
|
|
id={`${prefix}-address-line-1`}
|
|
name="addressLine1"
|
|
type="text"
|
|
value={organization?.addressLine1 ?? ''}
|
|
/>
|
|
</Field.Field>
|
|
<Field.Field>
|
|
<Field.Label for={`${prefix}-address-line-2`}>Address line 2</Field.Label>
|
|
<Input
|
|
id={`${prefix}-address-line-2`}
|
|
name="addressLine2"
|
|
type="text"
|
|
value={organization?.addressLine2 ?? ''}
|
|
/>
|
|
</Field.Field>
|
|
<div class="grid gap-2 sm:grid-cols-2">
|
|
<Field.Field>
|
|
<Field.Label for={`${prefix}-city`}>City</Field.Label>
|
|
<Input id={`${prefix}-city`} name="city" type="text" value={organization?.city ?? ''} />
|
|
</Field.Field>
|
|
<Field.Field>
|
|
<Field.Label for={`${prefix}-region`}>Region</Field.Label>
|
|
<Input
|
|
id={`${prefix}-region`}
|
|
name="region"
|
|
type="text"
|
|
value={organization?.region ?? ''}
|
|
/>
|
|
</Field.Field>
|
|
</div>
|
|
<div class="grid gap-2 sm:grid-cols-2">
|
|
<Field.Field>
|
|
<Field.Label for={`${prefix}-postcode`}>Postcode</Field.Label>
|
|
<Input
|
|
id={`${prefix}-postcode`}
|
|
name="postcode"
|
|
type="text"
|
|
value={organization?.postcode ?? ''}
|
|
/>
|
|
</Field.Field>
|
|
<Field.Field>
|
|
<Field.Label for={`${prefix}-country`}>Country</Field.Label>
|
|
<Input
|
|
id={`${prefix}-country`}
|
|
name="country"
|
|
type="text"
|
|
value={organization?.country ?? 'United Kingdom'}
|
|
required
|
|
/>
|
|
</Field.Field>
|
|
</div>
|
|
</div>
|
|
{/snippet}
|
|
|
|
{#snippet bankFields(prefix: string, organization?: Organization)}
|
|
<div class="grid gap-3">
|
|
<div class="flex items-center gap-2 text-sm font-medium">
|
|
<LandmarkIcon class="size-4" />
|
|
<span>Bank details</span>
|
|
</div>
|
|
<div class="grid gap-2 sm:grid-cols-2">
|
|
<Field.Field>
|
|
<Field.Label for={`${prefix}-bank-name`}>Bank name</Field.Label>
|
|
<Input
|
|
id={`${prefix}-bank-name`}
|
|
name="bankName"
|
|
type="text"
|
|
value={organization?.bankName ?? ''}
|
|
/>
|
|
</Field.Field>
|
|
<Field.Field>
|
|
<Field.Label for={`${prefix}-bank-account-name`}>Account name</Field.Label>
|
|
<Input
|
|
id={`${prefix}-bank-account-name`}
|
|
name="bankAccountName"
|
|
type="text"
|
|
value={organization?.bankAccountName ?? ''}
|
|
/>
|
|
</Field.Field>
|
|
</div>
|
|
<div class="grid gap-2 sm:grid-cols-2">
|
|
<Field.Field>
|
|
<Field.Label for={`${prefix}-bank-account-number`}>Account number</Field.Label>
|
|
<Input
|
|
id={`${prefix}-bank-account-number`}
|
|
name="bankAccountNumber"
|
|
type="text"
|
|
value={organization?.bankAccountNumber ?? ''}
|
|
/>
|
|
</Field.Field>
|
|
<Field.Field>
|
|
<Field.Label for={`${prefix}-bank-sort-code`}>Sort code</Field.Label>
|
|
<Input
|
|
id={`${prefix}-bank-sort-code`}
|
|
name="bankSortCode"
|
|
type="text"
|
|
value={organization?.bankSortCode ?? ''}
|
|
/>
|
|
</Field.Field>
|
|
</div>
|
|
<div class="grid gap-2 sm:grid-cols-2">
|
|
<Field.Field>
|
|
<Field.Label for={`${prefix}-bank-iban`}>IBAN</Field.Label>
|
|
<Input
|
|
id={`${prefix}-bank-iban`}
|
|
name="bankIban"
|
|
type="text"
|
|
value={organization?.bankIban ?? ''}
|
|
/>
|
|
</Field.Field>
|
|
<Field.Field>
|
|
<Field.Label for={`${prefix}-bank-swift`}>SWIFT/BIC</Field.Label>
|
|
<Input
|
|
id={`${prefix}-bank-swift`}
|
|
name="bankSwift"
|
|
type="text"
|
|
value={organization?.bankSwift ?? ''}
|
|
/>
|
|
</Field.Field>
|
|
</div>
|
|
</div>
|
|
{/snippet}
|
|
|
|
<Sidebar.Menu>
|
|
<Sidebar.MenuItem>
|
|
<DropdownMenu.Root>
|
|
<DropdownMenu.Trigger>
|
|
{#snippet child({ props })}
|
|
<Sidebar.MenuButton
|
|
size="lg"
|
|
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
{...props}
|
|
>
|
|
<div
|
|
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
|
|
>
|
|
<Building2Icon class="size-4" />
|
|
</div>
|
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
|
<span class="truncate font-semibold">{activeOrganization.name}</span>
|
|
<span class="truncate text-xs">{activeOrganization.workspace}</span>
|
|
</div>
|
|
<ChevronsUpDownIcon class="ms-auto" />
|
|
</Sidebar.MenuButton>
|
|
{/snippet}
|
|
</DropdownMenu.Trigger>
|
|
<DropdownMenu.Content class="w-(--bits-dropdown-menu-anchor-width)" align="start">
|
|
{#each organizations as organization (organization.id)}
|
|
<form method="POST" action="/dashboard/switch-organization">
|
|
<input type="hidden" name="organizationId" value={organization.id} />
|
|
<input type="hidden" name="redirectTo" value={redirectTo} />
|
|
<DropdownMenu.Item
|
|
class="gap-2 focus:bg-sidebar-accent focus:text-sidebar-accent-foreground data-[selected=true]:bg-sidebar-accent data-[selected=true]:text-sidebar-accent-foreground"
|
|
data-selected={organization.id === activeOrganization.id}
|
|
>
|
|
<button type="submit" class="contents">
|
|
<div
|
|
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground group-focus/dropdown-menu-item:bg-sidebar-primary group-focus/dropdown-menu-item:text-sidebar-primary-foreground"
|
|
>
|
|
<Building2Icon class="size-4 !text-sidebar-primary-foreground" />
|
|
</div>
|
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
|
<span class="truncate font-semibold">{organization.name}</span>
|
|
<span class="truncate text-xs text-muted-foreground"
|
|
>{organization.workspace}</span
|
|
>
|
|
</div>
|
|
{#if organization.id === activeOrganization.id}
|
|
<CheckIcon class="ms-auto" />
|
|
{/if}
|
|
</button>
|
|
</DropdownMenu.Item>
|
|
</form>
|
|
{/each}
|
|
<DropdownMenu.Separator />
|
|
<DropdownMenu.Item
|
|
class="gap-2 focus:bg-sidebar-accent focus:text-sidebar-accent-foreground"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="contents"
|
|
onclick={() => {
|
|
settingsOpen = true;
|
|
}}
|
|
>
|
|
<div
|
|
class="flex aspect-square size-8 items-center justify-center rounded-lg border bg-background text-foreground"
|
|
>
|
|
<SettingsIcon class="size-4" />
|
|
</div>
|
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
|
<span class="truncate font-semibold">Organization settings</span>
|
|
<span class="truncate text-xs text-muted-foreground">Edit current organization</span>
|
|
</div>
|
|
</button>
|
|
</DropdownMenu.Item>
|
|
<DropdownMenu.Item
|
|
class="gap-2 focus:bg-sidebar-accent focus:text-sidebar-accent-foreground"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="contents"
|
|
onclick={() => {
|
|
createOpen = true;
|
|
}}
|
|
>
|
|
<div
|
|
class="flex aspect-square size-8 items-center justify-center rounded-lg border bg-background text-foreground"
|
|
>
|
|
<PlusIcon class="size-4" />
|
|
</div>
|
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
|
<span class="truncate font-semibold">Create organization</span>
|
|
<span class="truncate text-xs text-muted-foreground">Add a new workspace</span>
|
|
</div>
|
|
</button>
|
|
</DropdownMenu.Item>
|
|
</DropdownMenu.Content>
|
|
</DropdownMenu.Root>
|
|
</Sidebar.MenuItem>
|
|
</Sidebar.Menu>
|
|
|
|
<Dialog.Root bind:open={settingsOpen}>
|
|
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
|
|
<Dialog.Header>
|
|
<Dialog.Title>Organization settings</Dialog.Title>
|
|
<Dialog.Description
|
|
>Update details for the active organization and invoice payments.</Dialog.Description
|
|
>
|
|
</Dialog.Header>
|
|
|
|
<form method="POST" action="/dashboard/organizations/current" class="grid gap-5">
|
|
<input type="hidden" name="id" value={activeOrganization.id} />
|
|
<input type="hidden" name="redirectTo" value={redirectTo} />
|
|
|
|
{@render organizationFields('organization-settings', activeOrganization)}
|
|
{@render bankFields('organization-settings', activeOrganization)}
|
|
|
|
<Dialog.Footer>
|
|
<Dialog.Close>
|
|
{#snippet child({ props })}
|
|
<Button variant="outline" {...props}>Cancel</Button>
|
|
{/snippet}
|
|
</Dialog.Close>
|
|
<Button type="submit">Save settings</Button>
|
|
</Dialog.Footer>
|
|
</form>
|
|
</Dialog.Content>
|
|
</Dialog.Root>
|
|
|
|
<Dialog.Root bind:open={createOpen}>
|
|
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
|
|
<Dialog.Header>
|
|
<Dialog.Title>Create organization</Dialog.Title>
|
|
<Dialog.Description
|
|
>Add organization details for records and invoice payments.</Dialog.Description
|
|
>
|
|
</Dialog.Header>
|
|
|
|
<form method="POST" action="/dashboard/organizations" class="grid gap-5">
|
|
<input type="hidden" name="redirectTo" value={redirectTo} />
|
|
|
|
{@render organizationFields('organization-create')}
|
|
{@render bankFields('organization-create')}
|
|
|
|
<Dialog.Footer>
|
|
<Dialog.Close>
|
|
{#snippet child({ props })}
|
|
<Button variant="outline" {...props}>Cancel</Button>
|
|
{/snippet}
|
|
</Dialog.Close>
|
|
<Button type="submit">Create organization</Button>
|
|
</Dialog.Footer>
|
|
</form>
|
|
</Dialog.Content>
|
|
</Dialog.Root>
|