mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-10-15 12:05:05 +02:00
Settings: Splitting up settings form into many smaller forms like for channels
This commit is contained in:
Generated
+6
-6
@@ -5348,9 +5348,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/joi": {
|
||||
"version": "17.13.7",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.7.tgz",
|
||||
"integrity": "sha512-MF80Dm5Y2veNy8QWVx9Bj3ui4mo7+VPSPsR1M+oaHXV0Gx6zGX9a2F+OZG3Blby9tOlzU9Rs5FUimlEhbKtfnQ==",
|
||||
"version": "17.13.8",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.8.tgz",
|
||||
"integrity": "sha512-iPKOGmiRw1jxf/JOPwxmCcUQAOdF359mdzYiP2DJ+TMX0YK2zjK3D+zYOaGjpumWxOFF/l2xVWjRVK5bGSLdEw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
@@ -6923,9 +6923,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
|
||||
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
||||
@@ -367,6 +367,7 @@
|
||||
"weekdays": "Wochentage"
|
||||
}
|
||||
},
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"actions": "Aktionen",
|
||||
"view": "Ansehen",
|
||||
@@ -733,14 +734,17 @@
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"loading": "Lade Einstellungen...",
|
||||
"form": {
|
||||
"sections": {
|
||||
"general": "Allgemein",
|
||||
"address": "Adresse",
|
||||
"links": "Links",
|
||||
"advanced": "Erweitert"
|
||||
},
|
||||
"fields": {
|
||||
"success": "Einstellungen aktualisiert",
|
||||
"error": "Fehler beim Speichern der Einstellungen",
|
||||
"overview": {
|
||||
"title": "Einstellungen"
|
||||
},
|
||||
"general": {
|
||||
"title": "Allgemein",
|
||||
"description": "Name, Beschreibung und Sprachen",
|
||||
"languages": {
|
||||
"title": "Sprachen",
|
||||
"description": "Definiere Sprachen, die Du unterstützen möchtest und lege eine Standardsprache fest.",
|
||||
"languages": {
|
||||
"title": "Sprachen",
|
||||
"description": "Wähle die Sprachen, die Klienten angezeigt bekommen sollen. Du musst mindestens Eine auswählen.",
|
||||
@@ -753,7 +757,11 @@
|
||||
"errors": {
|
||||
"notInLanguages": "Muss eine der ausgewählten Sprachen sein"
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
"title": "Branding",
|
||||
"description": "Lege Name, Beschreibung und Logo für deine Organisation fest.",
|
||||
"longName": {
|
||||
"title": "Name",
|
||||
"description": "Der öffentliche Name Deiner Organisation."
|
||||
@@ -764,7 +772,49 @@
|
||||
"descriptions": {
|
||||
"title": "Beschreibung (optional)",
|
||||
"description": "Kurze Beschreibung deiner Organisation. Wird auf der Startseite angezeigt."
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"address": {
|
||||
"title": "Adresse",
|
||||
"description": "Die vollständige Adresse deiner Organisation für Termine",
|
||||
"address": {
|
||||
"title": "Besucheradresse",
|
||||
"description": "Diese Adresse wird in den Termininformationen für deine Klienten angezeigt."
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"title": "Links",
|
||||
"description": "Webseite- und rechtliche Links",
|
||||
"website": {
|
||||
"title": "Webseite",
|
||||
"description": "Wenn du eine Webseite angibst, wird ein Link auf der Startseite angezeigt."
|
||||
},
|
||||
"legal": {
|
||||
"title": "Rechtliche Links",
|
||||
"description": "Rechtliche Links werden in der Fußzeile angezeigt, wenn sie gesetzt sind."
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Erweitert",
|
||||
"description": "Compliance und erforderliche Kundendaten",
|
||||
"compliance": {
|
||||
"title": "Compliance",
|
||||
"description": "Stelle sicher, dass deine Organisation den gesetzlichen Anforderungen entspricht. Rechtliche Links können im Abschnitt Links gesetzt werden."
|
||||
},
|
||||
"requirements": {
|
||||
"title": "Bedingungen",
|
||||
"description": "Gib' an welche Informationen für alle öffentlich gebuchten Termine erforderlich sind."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"sections": {
|
||||
"general": "Allgemein",
|
||||
"address": "Adresse",
|
||||
"links": "Links",
|
||||
"advanced": "Erweitert"
|
||||
},
|
||||
"fields": {
|
||||
"street": {
|
||||
"title": "Straße"
|
||||
},
|
||||
|
||||
@@ -375,6 +375,7 @@
|
||||
"weekdays": "Weekdays"
|
||||
}
|
||||
},
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"actions": "Actions",
|
||||
"view": "View",
|
||||
@@ -741,14 +742,17 @@
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"loading": "Loading settings...",
|
||||
"form": {
|
||||
"sections": {
|
||||
"general": "General",
|
||||
"address": "Address",
|
||||
"links": "Links",
|
||||
"advanced": "Advanced"
|
||||
},
|
||||
"fields": {
|
||||
"success": "Settings updated",
|
||||
"error": "Error saving settings",
|
||||
"overview": {
|
||||
"title": "Settings"
|
||||
},
|
||||
"general": {
|
||||
"title": "General",
|
||||
"description": "Name, Description and Language settings",
|
||||
"languages": {
|
||||
"title": "Languages",
|
||||
"description": "Define the languages you want to support and set a default language.",
|
||||
"languages": {
|
||||
"title": "Languages",
|
||||
"description": "Define the languages your customers will see. You need to select at least one.",
|
||||
@@ -761,7 +765,11 @@
|
||||
"errors": {
|
||||
"notInLanguages": "Default language must be one of the selected languages"
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
"title": "Branding",
|
||||
"description": "Set name, description and logo for your organization.",
|
||||
"longName": {
|
||||
"title": "Name",
|
||||
"description": "Public name of your organization."
|
||||
@@ -772,7 +780,50 @@
|
||||
"descriptions": {
|
||||
"title": "Description (optional)",
|
||||
"description": "Short description of your organization. Will be shown on the home page."
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"address": {
|
||||
"title": "Address",
|
||||
"description": "Your organizations full address for appointments",
|
||||
"address": {
|
||||
"title": "Visitor Address",
|
||||
"description": "This address will be included in appointment information for your clients."
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"title": "Links",
|
||||
"description": "Website and legal links",
|
||||
"website": {
|
||||
"title": "Website",
|
||||
"description": "If you set this, a link will be shown on the home page."
|
||||
},
|
||||
"legal": {
|
||||
"title": "Legal",
|
||||
"description": "Legal links are shown in the footer, when set."
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced",
|
||||
"description": "Compliance and required client data",
|
||||
"compliance": {
|
||||
"title": "Compliance",
|
||||
"description": "Ensure your organization complies with legal requirements. Legal links can be set in the Links section."
|
||||
},
|
||||
"requirements": {
|
||||
"title": "Requirements",
|
||||
"description": "Specify globally required client data for publically booked appointments."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"sections": {
|
||||
"general": "General",
|
||||
"address": "Address",
|
||||
"links": "Links",
|
||||
"advanced": "Advanced"
|
||||
},
|
||||
"fields": {
|
||||
|
||||
"street": {
|
||||
"title": "Street"
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
{
|
||||
title: m["nav.settings"](),
|
||||
url: ROUTES.DASHBOARD.SETTINGS,
|
||||
url: ROUTES.DASHBOARD.SETTINGS.MAIN,
|
||||
isTenantOnly: true,
|
||||
icon: SettingsIcon,
|
||||
roles: ["GLOBAL_ADMIN", "TENANT_ADMIN"],
|
||||
|
||||
@@ -6,9 +6,16 @@
|
||||
let {
|
||||
title,
|
||||
description,
|
||||
hint,
|
||||
class: className,
|
||||
children,
|
||||
}: { title: string; description: string; class?: string; children?: Snippet } = $props();
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
hint?: Snippet;
|
||||
class?: string;
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class={cn("flex flex-col gap-3 md:grid md:grid-cols-3 md:gap-20", className)}>
|
||||
@@ -21,8 +28,13 @@
|
||||
{description}
|
||||
</Text>
|
||||
{/if}
|
||||
{#if hint}
|
||||
<div class="mt-4">
|
||||
{@render hint?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<div class="col-span-2 mt-2">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,13 @@ export const ROUTES = {
|
||||
CHANNEL_SLOTS: "/dashboard/channels/[channelId]/slots",
|
||||
CHANNEL_PUBLIC_BOOKING: "/dashboard/channels/[channelId]/public-booking",
|
||||
ABSENCES: "/dashboard/absences",
|
||||
SETTINGS: "/dashboard/settings",
|
||||
SETTINGS: {
|
||||
MAIN: "/dashboard/settings",
|
||||
GENERAL: "/dashboard/settings/general",
|
||||
ADDRESS: "/dashboard/settings/address",
|
||||
LINKS: "/dashboard/settings/links",
|
||||
ADVANCED: "/dashboard/settings/advanced",
|
||||
},
|
||||
ACCOUNT: {
|
||||
MAIN: "/dashboard/account",
|
||||
GENERAL: "/dashboard/account/general",
|
||||
|
||||
@@ -103,6 +103,15 @@ const triggerOnboardingNotifications = (
|
||||
curState: TenantState,
|
||||
) => {
|
||||
if (newCurrentTenant && newCurrentTenant?.setupState !== "READY") {
|
||||
// In the progress on configuring settings?
|
||||
if (
|
||||
newCurrentTenant?.setupState === "SETTINGS" &&
|
||||
curState.currentTenant?.setupState === "SETTINGS"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// In the progress on configuring the first channel?
|
||||
if (newCurrentTenant?.setupState === "CHANNELS") {
|
||||
const hasCompleteChannel = get(channels).channels.some(
|
||||
(c) => c.agentIds?.length > 0 && c.slotTemplates?.length > 0 && c.pause === false,
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
{
|
||||
label: m["dashboard.onboarding.sections.settings.action"](),
|
||||
onClick: () => {
|
||||
goto(resolve(ROUTES.DASHBOARD.SETTINGS));
|
||||
goto(resolve(ROUTES.DASHBOARD.SETTINGS.MAIN));
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
import { MaxPageWidth } from "$lib/components/layouts/max-page-width";
|
||||
import { SidebarLayout } from "$lib/components/layouts/sidebar-layout";
|
||||
import { PageHeadline } from "$lib/components/templates/page-headline";
|
||||
import { GroupItem } from "$lib/components/ui/group-item";
|
||||
import * as Item from "$lib/components/ui/item";
|
||||
import { Text } from "$lib/components/ui/typography";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { ChevronRight } from "@lucide/svelte/icons";
|
||||
</script>
|
||||
|
||||
<SidebarLayout
|
||||
@@ -22,75 +21,27 @@
|
||||
<MaxPageWidth maxWidth="md" class="flex flex-col gap-6">
|
||||
<PageHeadline title={m["account.overview.title"]()} />
|
||||
<Item.Group class="gap-2">
|
||||
<Item.Root variant="outline">
|
||||
{#snippet child({ props })}
|
||||
<a href={resolve(ROUTES.DASHBOARD.ACCOUNT.GENERAL)} {...props}>
|
||||
<Item.Content>
|
||||
<Item.Title>
|
||||
<Text style="md">{m["account.general.title"]()}</Text>
|
||||
</Item.Title>
|
||||
<Item.Description>
|
||||
<Text style="md">{m["account.general.description"]()}</Text>
|
||||
</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<ChevronRight class="size-4" />
|
||||
</Item.Actions>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Item.Root>
|
||||
<!-- <Item.Root variant="outline">
|
||||
{#snippet child({ props })}
|
||||
<a href={resolve(ROUTES.DASHBOARD.ACCOUNT.CHANGE_EMAIL)} {...props}>
|
||||
<Item.Content>
|
||||
<Item.Title>
|
||||
<Text style="md">{m["account.change-email.title"]()}</Text>
|
||||
</Item.Title>
|
||||
<Item.Description>
|
||||
<Text style="md">{m["account.change-email.description"]()}</Text>
|
||||
</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<ChevronRight class="size-4" />
|
||||
</Item.Actions>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Item.Root> -->
|
||||
<Item.Root variant="outline">
|
||||
{#snippet child({ props })}
|
||||
<a href={resolve(ROUTES.DASHBOARD.ACCOUNT.PASSKEYS)} {...props}>
|
||||
<Item.Content>
|
||||
<Item.Title>
|
||||
<Text style="md">{m["account.passkeys.title"]()}</Text>
|
||||
</Item.Title>
|
||||
<Item.Description>
|
||||
<Text style="md">{m["account.passkeys.description"]()}</Text>
|
||||
</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<ChevronRight class="size-4" />
|
||||
</Item.Actions>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Item.Root>
|
||||
<GroupItem
|
||||
title={m["account.general.title"]()}
|
||||
description={m["account.general.description"]()}
|
||||
href={resolve(ROUTES.DASHBOARD.ACCOUNT.GENERAL)}
|
||||
/>
|
||||
<!-- <GroupItem
|
||||
title={m["account.change-email.title"]()}
|
||||
description={m["account.change-email.description"]()}
|
||||
href={resolve(ROUTES.DASHBOARD.ACCOUNT.CHANGE_EMAIL)}
|
||||
/> -->
|
||||
<GroupItem
|
||||
title={m["account.passkeys.title"]()}
|
||||
description={m["account.passkeys.description"]()}
|
||||
href={resolve(ROUTES.DASHBOARD.ACCOUNT.PASSKEYS)}
|
||||
/>
|
||||
{#if $auth.user?.role === "GLOBAL_ADMIN"}
|
||||
<Item.Root variant="outline">
|
||||
{#snippet child({ props })}
|
||||
<a href={resolve(ROUTES.DASHBOARD.ACCOUNT.CHANGE_PASSPHRASE)} {...props}>
|
||||
<Item.Content>
|
||||
<Item.Title>
|
||||
<Text style="md">{m["account.change-passphrase.title"]()}</Text>
|
||||
</Item.Title>
|
||||
<Item.Description>
|
||||
<Text style="md">{m["account.change-passphrase.description"]()}</Text>
|
||||
</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<ChevronRight class="size-4" />
|
||||
</Item.Actions>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Item.Root>
|
||||
<GroupItem
|
||||
title={m["account.change-passphrase.title"]()}
|
||||
description={m["account.change-passphrase.description"]()}
|
||||
href={resolve(ROUTES.DASHBOARD.ACCOUNT.CHANGE_PASSPHRASE)}
|
||||
/>
|
||||
{/if}
|
||||
</Item.Group>
|
||||
</MaxPageWidth>
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
<script lang="ts">
|
||||
import { m } from "$i18n/messages.js";
|
||||
import { FormSplit } from "$lib/components/templates/form-split";
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { tenants } from "$lib/stores/tenants";
|
||||
import type { TTenantSettings } from "$lib/types/tenant";
|
||||
import { untrack } from "svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
import { zod4Client as zodClient } from "sveltekit-superforms/adapters";
|
||||
import { formSchema } from "./schema";
|
||||
|
||||
let { entity }: { entity: TTenantSettings } = $props();
|
||||
|
||||
const form = superForm(
|
||||
untrack(() => ({
|
||||
id: entity.id,
|
||||
address: entity.address,
|
||||
})),
|
||||
{
|
||||
dataType: "json",
|
||||
validators: zodClient(formSchema),
|
||||
onResult: async (event) => {
|
||||
auth.refreshLastActive();
|
||||
if (event.result.type === "success") {
|
||||
tenants.reload();
|
||||
toast.success(m["settings.success"]());
|
||||
} else if (event.result.type === "failure") {
|
||||
toast.error(m["settings.error"]());
|
||||
}
|
||||
isSubmitting = false;
|
||||
},
|
||||
onSubmit: () => (isSubmitting = true),
|
||||
},
|
||||
);
|
||||
|
||||
let isSubmitting = $state(false);
|
||||
|
||||
const { form: formData, enhance } = form;
|
||||
</script>
|
||||
|
||||
<Form.Root
|
||||
{enhance}
|
||||
action={`${ROUTES.DASHBOARD.SETTINGS.MAIN}?/edit`}
|
||||
class="flex flex-col gap-10"
|
||||
>
|
||||
<Form.Field {form} name="id" class="hidden">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Input {...props} bind:value={$formData.id} type="hidden" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<FormSplit
|
||||
title={m["settings.address.address.title"]()}
|
||||
description={m["settings.address.address.description"]()}
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex justify-between gap-3">
|
||||
<Form.Field {form} name="address.street" class="flex-4/5">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.street.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.address.street} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="address.number" class="flex-1/5">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.number.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.address.number} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
</div>
|
||||
<Form.Field {form} name="address.additionalAddressInfo">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.additionalAddressInfo.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.address.additionalAddressInfo} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<div class="flex justify-between gap-3">
|
||||
<Form.Field {form} name="address.zip" class="flex-2/5">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.zip.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.address.zip} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="address.city" class="flex-3/5">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.city.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.address.city} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
</div>
|
||||
</div>
|
||||
</FormSplit>
|
||||
|
||||
<div class="mt-6 flex flex-col gap-4">
|
||||
<Form.Button
|
||||
size="lg"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
class="sm:ml-auto"
|
||||
>
|
||||
{m["save"]()}
|
||||
</Form.Button>
|
||||
</div>
|
||||
</Form.Root>
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from "$app/paths";
|
||||
import { m } from "$i18n/messages.js";
|
||||
import { FormSplit } from "$lib/components/templates/form-split";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { CheckboxWithLabel } from "$lib/components/ui/checkbox-with-label";
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { tenants } from "$lib/stores/tenants";
|
||||
import type { TTenantSettings } from "$lib/types/tenant";
|
||||
import { ChevronRight } from "@lucide/svelte";
|
||||
import { untrack } from "svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
import { zod4Client as zodClient } from "sveltekit-superforms/adapters";
|
||||
import { formSchema } from "./schema";
|
||||
|
||||
let { entity }: { entity: TTenantSettings } = $props();
|
||||
|
||||
const autoDeleteDaysOptions = [
|
||||
{ key: 30, label: m["settings.form.fields.autoDeleteDays.options.30"]() },
|
||||
{ key: 60, label: m["settings.form.fields.autoDeleteDays.options.60"]() },
|
||||
{ key: 90, label: m["settings.form.fields.autoDeleteDays.options.90"]() },
|
||||
{ key: 180, label: m["settings.form.fields.autoDeleteDays.options.180"]() },
|
||||
{ key: 365, label: m["settings.form.fields.autoDeleteDays.options.365"]() },
|
||||
];
|
||||
|
||||
const form = superForm(
|
||||
untrack(() => ({
|
||||
id: entity.id,
|
||||
settings: {
|
||||
autoDeleteDays: entity.settings.autoDeleteDays,
|
||||
requirePhone: entity.settings.requirePhone || false,
|
||||
},
|
||||
})),
|
||||
{
|
||||
dataType: "json",
|
||||
validators: zodClient(formSchema),
|
||||
onResult: async (event) => {
|
||||
auth.refreshLastActive();
|
||||
if (event.result.type === "success") {
|
||||
tenants.reload();
|
||||
toast.success(m["settings.success"]());
|
||||
} else if (event.result.type === "failure") {
|
||||
toast.error(m["settings.error"]());
|
||||
}
|
||||
isSubmitting = false;
|
||||
},
|
||||
onSubmit: () => (isSubmitting = true),
|
||||
},
|
||||
);
|
||||
|
||||
let isSubmitting = $state(false);
|
||||
|
||||
const { form: formData, enhance } = form;
|
||||
</script>
|
||||
|
||||
<Form.Root
|
||||
{enhance}
|
||||
action={`${ROUTES.DASHBOARD.SETTINGS.MAIN}?/edit`}
|
||||
class="flex flex-col gap-10"
|
||||
>
|
||||
<Form.Field {form} name="id" class="hidden">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Input {...props} bind:value={$formData.id} type="hidden" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<FormSplit
|
||||
title={m["settings.advanced.compliance.title"]()}
|
||||
description={m["settings.advanced.compliance.description"]()}
|
||||
{hint}
|
||||
>
|
||||
<Form.Field {form} name="settings.autoDeleteDays">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.autoDeleteDays.title"]()}</Form.Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={$formData.settings.autoDeleteDays.toString()}
|
||||
name={props.name}
|
||||
onValueChange={(v) => ($formData.settings.autoDeleteDays = parseInt(v))}
|
||||
>
|
||||
<Select.Trigger {...props} class="w-full">
|
||||
{$formData.settings.autoDeleteDays
|
||||
? autoDeleteDaysOptions.find((x) => x.key === $formData.settings.autoDeleteDays)
|
||||
?.label
|
||||
: m["settings.form.fields.autoDeleteDays.placeholder"]()}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each autoDeleteDaysOptions as option (option.key)}
|
||||
<Select.Item value={option.key.toString()}>
|
||||
{option.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
</FormSplit>
|
||||
<FormSplit
|
||||
title={m["settings.advanced.requirements.title"]()}
|
||||
description={m["settings.advanced.requirements.description"]()}
|
||||
>
|
||||
<Form.Field {form} name="settings.requirePhone">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.requirePhone.title"]()}</Form.Label>
|
||||
<CheckboxWithLabel
|
||||
{...props}
|
||||
bind:value={$formData.settings.requirePhone}
|
||||
label={m["settings.form.fields.requirePhone.label"]()}
|
||||
onCheckedChange={(v) => {
|
||||
$formData.settings.requirePhone = v;
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
</FormSplit>
|
||||
|
||||
<div class="mt-6 flex flex-col gap-4">
|
||||
<Form.Button
|
||||
size="lg"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
class="sm:ml-auto"
|
||||
>
|
||||
{m["save"]()}
|
||||
</Form.Button>
|
||||
</div>
|
||||
</Form.Root>
|
||||
|
||||
{#snippet hint()}
|
||||
<Button
|
||||
variant="link"
|
||||
size="xs"
|
||||
href={resolve(ROUTES.DASHBOARD.SETTINGS.LINKS)}
|
||||
class="text-normal m-0 p-0"
|
||||
>
|
||||
<ChevronRight class="size-3 p-0" />
|
||||
{m["settings.links.legal.title"]()}
|
||||
</Button>
|
||||
{/snippet}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
<script lang="ts">
|
||||
import { m } from "$i18n/messages.js";
|
||||
import { FormSplit } from "$lib/components/templates/form-split";
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { InputCroppedImageBlob } from "$lib/components/ui/input-cropped-image-blob";
|
||||
import { LanguageTabs } from "$lib/components/ui/language-tabs";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { supportedLocales, translatedLocales } from "$lib/const/locales";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { tenants } from "$lib/stores/tenants";
|
||||
import type { TTenantSettings } from "$lib/types/tenant";
|
||||
import DefaultOrgIcon from "@lucide/svelte/icons/landmark";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
import { zod4Client as zodClient } from "sveltekit-superforms/adapters";
|
||||
import { formSchema } from "./schema";
|
||||
import { untrack } from "svelte";
|
||||
|
||||
let { entity }: { entity: TTenantSettings } = $props();
|
||||
|
||||
const availableLocales = supportedLocales.map((it) => ({
|
||||
key: it,
|
||||
label: translatedLocales[it as keyof typeof translatedLocales],
|
||||
}));
|
||||
|
||||
const form = superForm(
|
||||
untrack(() => ({
|
||||
id: entity.id,
|
||||
languages: entity.languages,
|
||||
defaultLanguage: entity.defaultLanguage,
|
||||
longName: entity.longName,
|
||||
logo: entity.logo,
|
||||
descriptions: supportedLocales.reduce(
|
||||
(acc, locale) => {
|
||||
return { ...acc, [locale]: entity.descriptions[locale] || "" };
|
||||
},
|
||||
{} as { [key: string]: string },
|
||||
),
|
||||
})),
|
||||
{
|
||||
dataType: "json",
|
||||
validators: zodClient(formSchema),
|
||||
onResult: async (event) => {
|
||||
auth.refreshLastActive();
|
||||
if (event.result.type === "success") {
|
||||
tenants.reload();
|
||||
toast.success(m["settings.success"]());
|
||||
} else if (event.result.type === "failure") {
|
||||
toast.error(m["settings.error"]());
|
||||
}
|
||||
isSubmitting = false;
|
||||
},
|
||||
onSubmit: () => (isSubmitting = true),
|
||||
},
|
||||
);
|
||||
|
||||
let isSubmitting = $state(false);
|
||||
|
||||
const { form: formData, enhance } = form;
|
||||
</script>
|
||||
|
||||
<Form.Root
|
||||
{enhance}
|
||||
action={`${ROUTES.DASHBOARD.SETTINGS.MAIN}?/edit`}
|
||||
class="flex flex-col gap-10"
|
||||
>
|
||||
<Form.Field {form} name="id" class="hidden">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Input {...props} bind:value={$formData.id} type="hidden" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<FormSplit
|
||||
title={m["settings.general.languages.title"]()}
|
||||
description={m["settings.general.languages.description"]()}
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Form.Field {form} name="languages">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.general.languages.languages.title"]()}</Form.Label>
|
||||
<Select.Root
|
||||
type="multiple"
|
||||
bind:value={$formData.languages}
|
||||
name={props.name}
|
||||
onValueChange={(v) => ($formData.languages = v)}
|
||||
>
|
||||
<Select.Trigger {...props} class="w-full">
|
||||
{$formData.languages.length > 0
|
||||
? $formData.languages
|
||||
.map((id) => availableLocales.find((x) => x.key === id)?.label)
|
||||
.join(", ")
|
||||
: m["settings.general.languages.languages.placeholder"]()}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each availableLocales as locale (locale.key)}
|
||||
<Select.Item value={locale.key}>{locale.label}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<Form.Description>
|
||||
{m["settings.general.languages.languages.description"]()}
|
||||
</Form.Description>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="defaultLanguage">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.general.languages.defaultLanguage.title"]()}</Form.Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
bind:value={$formData.defaultLanguage}
|
||||
name={props.name}
|
||||
onValueChange={(v) => ($formData.defaultLanguage = v)}
|
||||
>
|
||||
<Select.Trigger {...props} class="w-full">
|
||||
{$formData.defaultLanguage
|
||||
? availableLocales.find((x) => x.key === $formData.defaultLanguage)?.label
|
||||
: m["settings.general.languages.defaultLanguage.placeholder"]()}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each $formData.languages as language (language)}
|
||||
<Select.Item value={language}>
|
||||
{availableLocales.find((x) => x.key === language)?.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<Form.Description>
|
||||
{m["settings.general.languages.defaultLanguage.description"]()}
|
||||
</Form.Description>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
</div>
|
||||
</FormSplit>
|
||||
<FormSplit
|
||||
title={m["settings.general.branding.title"]()}
|
||||
description={m["settings.general.branding.description"]()}
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Form.Field {form} name="logo">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.general.branding.logo.title"]()}</Form.Label>
|
||||
<InputCroppedImageBlob
|
||||
{...props}
|
||||
bind:value={$formData.logo}
|
||||
FallbackIcon={DefaultOrgIcon}
|
||||
/>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="longName">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["form.name"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.longName} type="text" autocomplete="off" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.Description>
|
||||
{m["settings.general.branding.longName.description"]()}
|
||||
</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<LanguageTabs languages={$formData.languages}>
|
||||
{#snippet children({ locale })}
|
||||
<Form.Field {form} name={`descriptions.${locale}`}>
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.general.branding.descriptions.title"]()}</Form.Label>
|
||||
<Textarea {...props} bind:value={$formData.descriptions[locale]} maxlength={200} />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.Description
|
||||
>{m["settings.general.branding.descriptions.description"]()}</Form.Description
|
||||
>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
{/snippet}
|
||||
</LanguageTabs>
|
||||
</div>
|
||||
</FormSplit>
|
||||
|
||||
<div class="mt-6 flex flex-col gap-4">
|
||||
<Form.Button
|
||||
size="lg"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
class="sm:ml-auto"
|
||||
>
|
||||
{m["save"]()}
|
||||
</Form.Button>
|
||||
</div>
|
||||
</Form.Root>
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
import { m } from "$i18n/messages.js";
|
||||
import { FormSplit } from "$lib/components/templates/form-split";
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { tenants } from "$lib/stores/tenants";
|
||||
import type { TTenantSettings } from "$lib/types/tenant";
|
||||
import { untrack } from "svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
import { zod4Client as zodClient } from "sveltekit-superforms/adapters";
|
||||
import { formSchema } from "./schema";
|
||||
|
||||
let { entity }: { entity: TTenantSettings } = $props();
|
||||
|
||||
const form = superForm(
|
||||
untrack(() => ({
|
||||
id: entity.id,
|
||||
links: entity.links,
|
||||
})),
|
||||
{
|
||||
dataType: "json",
|
||||
validators: zodClient(formSchema),
|
||||
onResult: async (event) => {
|
||||
auth.refreshLastActive();
|
||||
if (event.result.type === "success") {
|
||||
tenants.reload();
|
||||
toast.success(m["settings.success"]());
|
||||
} else if (event.result.type === "failure") {
|
||||
toast.error(m["settings.error"]());
|
||||
}
|
||||
isSubmitting = false;
|
||||
},
|
||||
onSubmit: () => (isSubmitting = true),
|
||||
},
|
||||
);
|
||||
|
||||
let isSubmitting = $state(false);
|
||||
|
||||
const { form: formData, enhance } = form;
|
||||
</script>
|
||||
|
||||
<Form.Root
|
||||
{enhance}
|
||||
action={`${ROUTES.DASHBOARD.SETTINGS.MAIN}?/edit`}
|
||||
class="flex flex-col gap-10"
|
||||
>
|
||||
<Form.Field {form} name="id" class="hidden">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Input {...props} bind:value={$formData.id} type="hidden" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<FormSplit
|
||||
title={m["settings.links.website.title"]()}
|
||||
description={m["settings.links.website.description"]()}
|
||||
>
|
||||
<Form.Field {form} name="links.website" class="flex-3/5">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.website.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.links.website} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.Description>
|
||||
{m["settings.form.fields.website.description"]()}
|
||||
</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
</FormSplit>
|
||||
<FormSplit
|
||||
title={m["settings.links.legal.title"]()}
|
||||
description={m["settings.links.legal.description"]()}
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Form.Field {form} name="links.imprint" class="flex-3/5">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.imprint.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.links.imprint} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.Description>
|
||||
{m["settings.form.fields.imprint.description"]()}
|
||||
</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="links.privacyStatement" class="flex-3/5">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.privacyStatement.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.links.privacyStatement} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.Description>
|
||||
{m["settings.form.fields.privacyStatement.description"]()}
|
||||
</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
</div>
|
||||
</FormSplit>
|
||||
|
||||
<div class="mt-6 flex flex-col gap-4">
|
||||
<Form.Button
|
||||
size="lg"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
class="sm:ml-auto"
|
||||
>
|
||||
{m["save"]()}
|
||||
</Form.Button>
|
||||
</div>
|
||||
</Form.Root>
|
||||
-343
@@ -1,343 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { m } from "$i18n/messages.js";
|
||||
import { FormGrid, FormGridItem } from "$lib/components/templates/form-grid";
|
||||
import { CheckboxWithLabel } from "$lib/components/ui/checkbox-with-label";
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { InputCroppedImageBlob } from "$lib/components/ui/input-cropped-image-blob";
|
||||
import { LanguageTabs } from "$lib/components/ui/language-tabs";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { supportedLocales, translatedLocales } from "$lib/const/locales";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { tenants } from "$lib/stores/tenants";
|
||||
import type { TTenantSettings } from "$lib/types/tenant";
|
||||
import DefaultOrgIcon from "@lucide/svelte/icons/landmark";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
import { zod4Client as zodClient } from "sveltekit-superforms/adapters";
|
||||
import { formSchema } from "./schema";
|
||||
|
||||
let { entity }: { entity: TTenantSettings } = $props();
|
||||
|
||||
const autoDeleteDaysOptions = [
|
||||
{ key: 30, label: m["settings.form.fields.autoDeleteDays.options.30"]() },
|
||||
{ key: 60, label: m["settings.form.fields.autoDeleteDays.options.60"]() },
|
||||
{ key: 90, label: m["settings.form.fields.autoDeleteDays.options.90"]() },
|
||||
{ key: 180, label: m["settings.form.fields.autoDeleteDays.options.180"]() },
|
||||
{ key: 365, label: m["settings.form.fields.autoDeleteDays.options.365"]() },
|
||||
];
|
||||
const availableLocales = supportedLocales.map((it) => ({
|
||||
key: it,
|
||||
label: translatedLocales[it as keyof typeof translatedLocales],
|
||||
}));
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(
|
||||
{
|
||||
id: entity.id,
|
||||
languages: entity.languages,
|
||||
defaultLanguage: entity.defaultLanguage,
|
||||
shortName: entity.shortName,
|
||||
longName: entity.longName,
|
||||
logo: entity.logo,
|
||||
descriptions: supportedLocales.reduce(
|
||||
(acc, locale) => {
|
||||
return { ...acc, [locale]: entity.descriptions[locale] || "" };
|
||||
},
|
||||
{} as { [key: string]: string },
|
||||
),
|
||||
address: entity.address,
|
||||
links: entity.links,
|
||||
settings: {
|
||||
autoDeleteDays: entity.settings.autoDeleteDays,
|
||||
requirePhone: entity.settings.requirePhone || false,
|
||||
},
|
||||
},
|
||||
{
|
||||
dataType: "json",
|
||||
validators: zodClient(formSchema),
|
||||
onResult: async (event) => {
|
||||
auth.refreshLastActive();
|
||||
if (event.result.type === "success") {
|
||||
tenants.reload();
|
||||
toast.success(m["tenants.edit.success"]());
|
||||
} else if (event.result.type === "failure") {
|
||||
toast.error(m["tenants.edit.error"]());
|
||||
}
|
||||
isSubmitting = false;
|
||||
},
|
||||
onSubmit: () => (isSubmitting = true),
|
||||
},
|
||||
);
|
||||
|
||||
let isSubmitting = $state(false);
|
||||
|
||||
const { form: formData, enhance } = form;
|
||||
</script>
|
||||
|
||||
<Form.Root {enhance} action="?/edit">
|
||||
<Form.Field {form} name="id" class="hidden">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Input {...props} bind:value={$formData.id} type="hidden" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<FormGrid class="grid-flow-dense">
|
||||
<FormGridItem
|
||||
title={m["settings.form.sections.general"]()}
|
||||
hideTitleOnMobile={true}
|
||||
class="row-span-3"
|
||||
>
|
||||
<Form.Field {form} name="languages">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.languages.title"]()}</Form.Label>
|
||||
<Select.Root
|
||||
type="multiple"
|
||||
bind:value={$formData.languages}
|
||||
name={props.name}
|
||||
onValueChange={(v) => ($formData.languages = v)}
|
||||
>
|
||||
<Select.Trigger {...props} class="w-full">
|
||||
{$formData.languages.length > 0
|
||||
? $formData.languages
|
||||
.map((id) => availableLocales.find((x) => x.key === id)?.label)
|
||||
.join(", ")
|
||||
: m["settings.form.fields.languages.placeholder"]()}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each availableLocales as locale (locale.key)}
|
||||
<Select.Item value={locale.key}>{locale.label}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<Form.Description>
|
||||
{m["settings.form.fields.languages.description"]()}
|
||||
</Form.Description>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="defaultLanguage">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.defaultLanguage.title"]()}</Form.Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
bind:value={$formData.defaultLanguage}
|
||||
name={props.name}
|
||||
onValueChange={(v) => ($formData.defaultLanguage = v)}
|
||||
>
|
||||
<Select.Trigger {...props} class="w-full">
|
||||
{$formData.defaultLanguage
|
||||
? availableLocales.find((x) => x.key === $formData.defaultLanguage)?.label
|
||||
: m["settings.form.fields.defaultLanguage.placeholder"]()}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each $formData.languages as language (language)}
|
||||
<Select.Item value={language}>
|
||||
{availableLocales.find((x) => x.key === language)?.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<Form.Description>
|
||||
{m["settings.form.fields.defaultLanguage.description"]()}
|
||||
</Form.Description>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="longName">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["form.name"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.longName} type="text" autocomplete="off" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.Description>
|
||||
{m["settings.form.fields.longName.description"]()}
|
||||
</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="logo">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.logo.title"]()}</Form.Label>
|
||||
<InputCroppedImageBlob
|
||||
{...props}
|
||||
bind:value={$formData.logo}
|
||||
FallbackIcon={DefaultOrgIcon}
|
||||
/>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<LanguageTabs languages={$formData.languages}>
|
||||
{#snippet children({ locale })}
|
||||
<Form.Field {form} name={`descriptions.${locale}`}>
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.descriptions.title"]()}</Form.Label>
|
||||
<Textarea {...props} bind:value={$formData.descriptions[locale]} maxlength={200} />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.Description
|
||||
>{m["settings.form.fields.descriptions.description"]()}</Form.Description
|
||||
>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
{/snippet}
|
||||
</LanguageTabs>
|
||||
</FormGridItem>
|
||||
<FormGridItem title={m["settings.form.sections.address"]()}>
|
||||
<div class="flex justify-between gap-3">
|
||||
<Form.Field {form} name="address.street" class="flex-4/5">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.street.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.address.street} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="address.number" class="flex-1/5">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.number.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.address.number} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
</div>
|
||||
<Form.Field {form} name="address.additionalAddressInfo">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.additionalAddressInfo.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.address.additionalAddressInfo} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<div class="flex justify-between gap-3">
|
||||
<Form.Field {form} name="address.zip" class="flex-2/5">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.zip.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.address.zip} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="address.city" class="flex-3/5">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.city.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.address.city} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
</div>
|
||||
</FormGridItem>
|
||||
<FormGridItem title={m["settings.form.sections.links"]()}>
|
||||
<Form.Field {form} name="links.website" class="flex-3/5">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.website.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.links.website} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.Description>
|
||||
{m["settings.form.fields.website.description"]()}
|
||||
</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="links.imprint" class="flex-3/5">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.imprint.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.links.imprint} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.Description>
|
||||
{m["settings.form.fields.imprint.description"]()}
|
||||
</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="links.privacyStatement" class="flex-3/5">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.privacyStatement.title"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.links.privacyStatement} type="text" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.Description>
|
||||
{m["settings.form.fields.privacyStatement.description"]()}
|
||||
</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
</FormGridItem>
|
||||
<FormGridItem title={m["settings.form.sections.advanced"]()} class="xl:col-start-3">
|
||||
<Form.Field {form} name="settings.autoDeleteDays">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.autoDeleteDays.title"]()}</Form.Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={$formData.settings.autoDeleteDays.toString()}
|
||||
name={props.name}
|
||||
onValueChange={(v) => ($formData.settings.autoDeleteDays = parseInt(v))}
|
||||
>
|
||||
<Select.Trigger {...props} class="w-full">
|
||||
{$formData.settings.autoDeleteDays
|
||||
? autoDeleteDaysOptions.find((x) => x.key === $formData.settings.autoDeleteDays)
|
||||
?.label
|
||||
: m["settings.form.fields.autoDeleteDays.placeholder"]()}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each autoDeleteDaysOptions as option (option.key)}
|
||||
<Select.Item value={option.key.toString()}>
|
||||
{option.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="settings.requirePhone">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["settings.form.fields.requirePhone.title"]()}</Form.Label>
|
||||
<CheckboxWithLabel
|
||||
{...props}
|
||||
bind:value={$formData.settings.requirePhone}
|
||||
label={m["settings.form.fields.requirePhone.label"]()}
|
||||
onCheckedChange={(v) => {
|
||||
$formData.settings.requirePhone = v;
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
</FormGridItem>
|
||||
</FormGrid>
|
||||
|
||||
<div class="mt-6 flex flex-col gap-4">
|
||||
<Form.Button
|
||||
size="lg"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
class="sm:ml-auto"
|
||||
>
|
||||
{m["settings.form.action"]()}
|
||||
</Form.Button>
|
||||
</div>
|
||||
</Form.Root>
|
||||
@@ -1,5 +1,13 @@
|
||||
import EditSettingsForm from "./edit-settings-form.svelte";
|
||||
import EditSettingsFormGeneral from "./edit-settings-form-general.svelte";
|
||||
import EditSettingsFormAddress from "./edit-settings-form-address.svelte";
|
||||
import EditSettingsFormAdvanced from "./edit-settings-form-advanced.svelte";
|
||||
import EditSettingsFormLinks from "./edit-settings-form-links.svelte";
|
||||
|
||||
export { EditSettingsForm };
|
||||
export {
|
||||
EditSettingsFormGeneral,
|
||||
EditSettingsFormAddress,
|
||||
EditSettingsFormAdvanced,
|
||||
EditSettingsFormLinks,
|
||||
};
|
||||
export { formSchema } from "./schema";
|
||||
export type { FormSchema } from "./schema";
|
||||
|
||||
@@ -22,34 +22,39 @@ const optionalUrl = (errorMessage: string) =>
|
||||
export const formSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
languages: z.array(z.string()).min(1, { message: m["form.errors.languages"]() }),
|
||||
defaultLanguage: z.string().min(1, { message: m["form.errors.language"]() }),
|
||||
shortName: z.string(),
|
||||
longName: z.string().min(1, { message: m["form.errors.longname"]() }),
|
||||
logo: z.string().optional().or(z.literal("")),
|
||||
descriptions: z.record(z.string(), z.string()).optional(),
|
||||
address: z.object({
|
||||
street: z.string().min(1, { message: m["form.errors.street"]() }),
|
||||
number: z.string().min(1, { message: m["form.errors.houseNo"]() }),
|
||||
additionalAddressInfo: z.string().optional().or(z.literal("")),
|
||||
zip: z.string().min(4, { message: m["form.errors.zip"]() }),
|
||||
city: z.string().min(1, { message: m["form.errors.city"]() }),
|
||||
}),
|
||||
links: z.object({
|
||||
website: optionalUrl(m["form.errors.url"]()),
|
||||
imprint: optionalUrl(m["form.errors.url"]()),
|
||||
privacyStatement: optionalUrl(m["form.errors.url"]()),
|
||||
}),
|
||||
settings: z.object({
|
||||
autoDeleteDays: z.number().min(30, { message: m["form.errors.deleteAfterDays"]() }),
|
||||
requirePhone: z.boolean().optional().default(false),
|
||||
}),
|
||||
languages: z.array(z.string()).min(1, { message: m["form.errors.languages"]() }).optional(),
|
||||
defaultLanguage: z.string().min(1, { message: m["form.errors.language"]() }).optional(),
|
||||
longName: z.string().min(1, { message: m["form.errors.longname"]() }).optional(),
|
||||
logo: z.string().optional().or(z.literal("")).optional(),
|
||||
descriptions: z.record(z.string(), z.string()).optional().optional(),
|
||||
address: z
|
||||
.object({
|
||||
street: z.string().min(1, { message: m["form.errors.street"]() }),
|
||||
number: z.string().min(1, { message: m["form.errors.houseNo"]() }),
|
||||
additionalAddressInfo: z.string().optional().or(z.literal("")),
|
||||
zip: z.string().min(4, { message: m["form.errors.zip"]() }),
|
||||
city: z.string().min(1, { message: m["form.errors.city"]() }),
|
||||
})
|
||||
.optional(),
|
||||
links: z
|
||||
.object({
|
||||
website: optionalUrl(m["form.errors.url"]()),
|
||||
imprint: optionalUrl(m["form.errors.url"]()),
|
||||
privacyStatement: optionalUrl(m["form.errors.url"]()),
|
||||
})
|
||||
.optional(),
|
||||
settings: z
|
||||
.object({
|
||||
autoDeleteDays: z.number().min(30, { message: m["form.errors.deleteAfterDays"]() }),
|
||||
requirePhone: z.boolean().optional().default(false),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.superRefine(({ languages, defaultLanguage }, ctx) => {
|
||||
if (!languages.includes(defaultLanguage)) {
|
||||
if (languages && defaultLanguage && !languages.includes(defaultLanguage)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: m["settings.form.fields.defaultLanguage.errors.notInLanguages"](),
|
||||
message: m["settings.general.languages.defaultLanguage.errors.notInLanguages"](),
|
||||
path: ["defaultLanguage"],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { ROUTES } from "$lib/const/routes.js";
|
||||
import logger from "$lib/logger";
|
||||
import type { TTenantSettings } from "$lib/types/tenant";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
|
||||
const log = logger.setContext(import.meta.filename);
|
||||
|
||||
export const load = async (event) => {
|
||||
event.depends(`channel:details`);
|
||||
|
||||
// To edit seetings you must have connected tenant
|
||||
if (!event.locals.user?.tenantId) {
|
||||
log.warn("No tenant ID found for user while loading settings");
|
||||
throw redirect(302, ROUTES.DASHBOARD.MAIN);
|
||||
}
|
||||
|
||||
const base = event
|
||||
.fetch(`/api/tenants/${event.locals.user?.tenantId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "same-origin",
|
||||
})
|
||||
.then(async (res) => {
|
||||
// Logout if session expired
|
||||
if (res.status === 401) {
|
||||
redirect(302, ROUTES.LOGOUT);
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await res.json();
|
||||
return {
|
||||
id: body.tenant.id,
|
||||
languages: body.tenant.languages,
|
||||
defaultLanguage: body.tenant.defaultLanguage,
|
||||
shortName: body.tenant.shortName,
|
||||
longName: body.tenant.longName,
|
||||
logo: body.tenant.logo || "",
|
||||
descriptions: body.tenant.descriptions,
|
||||
links: {
|
||||
website: body.tenant.links.website || "",
|
||||
imprint: body.tenant.links.imprint || "",
|
||||
privacyStatement: body.tenant.links.privacyStatement || "",
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
log.error("Failed to parse settings base response", { error });
|
||||
}
|
||||
});
|
||||
|
||||
const config = event
|
||||
.fetch(`/api/tenants/${event.locals.user?.tenantId}/config`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "same-origin",
|
||||
})
|
||||
.then(async (res) => {
|
||||
// Logout if session expired
|
||||
if (res.status === 401) {
|
||||
redirect(302, ROUTES.LOGOUT);
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await res.json();
|
||||
return {
|
||||
address: {
|
||||
street: body["address.street"] || "",
|
||||
number: body["address.number"] || "",
|
||||
additionalAddressInfo: body["address.additionalAddressInfo"] || "",
|
||||
zip: body["address.zip"] || "",
|
||||
city: body["address.city"] || "",
|
||||
},
|
||||
settings: {
|
||||
autoDeleteDays: body.autoDeleteDays || 90,
|
||||
requirePhone: body.requirePhone || false,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
log.error("Failed to parse settings config response", { error });
|
||||
}
|
||||
});
|
||||
|
||||
const item = Promise.all([base, config]).then(([base, config]) => {
|
||||
return {
|
||||
...base,
|
||||
...config,
|
||||
} as TTenantSettings;
|
||||
});
|
||||
|
||||
return {
|
||||
streamed: {
|
||||
item,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,103 +1,12 @@
|
||||
import { ROUTES } from "$lib/const/routes.js";
|
||||
import logger from "$lib/logger";
|
||||
import { fail, redirect, type Actions } from "@sveltejs/kit";
|
||||
import { removeEmptyTranslations } from "$lib/utils/localizations";
|
||||
import { fail, type Actions } from "@sveltejs/kit";
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
import { zod4 as zod } from "sveltekit-superforms/adapters";
|
||||
import { formSchema as editFormSchema } from "./(components)/edit-settings-form";
|
||||
import { removeEmptyTranslations } from "$lib/utils/localizations";
|
||||
|
||||
const log = logger.setContext(import.meta.filename);
|
||||
|
||||
export const load = async (event) => {
|
||||
// To edit seetings you must have connected tenant
|
||||
if (!event.locals.user?.tenantId) {
|
||||
log.warn("No tenant ID found for user while loading settings");
|
||||
throw redirect(302, ROUTES.DASHBOARD.MAIN);
|
||||
}
|
||||
|
||||
const base = event
|
||||
.fetch(`/api/tenants/${event.locals.user?.tenantId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "same-origin",
|
||||
})
|
||||
.then(async (res) => {
|
||||
// Logout if session expired
|
||||
if (res.status === 401) {
|
||||
redirect(302, ROUTES.LOGOUT);
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await res.json();
|
||||
return {
|
||||
id: body.tenant.id,
|
||||
languages: body.tenant.languages,
|
||||
defaultLanguage: body.tenant.defaultLanguage,
|
||||
shortName: body.tenant.shortName,
|
||||
longName: body.tenant.longName,
|
||||
logo: body.tenant.logo || "",
|
||||
descriptions: body.tenant.descriptions,
|
||||
links: {
|
||||
website: body.tenant.links.website || "",
|
||||
imprint: body.tenant.links.imprint || "",
|
||||
privacyStatement: body.tenant.links.privacyStatement || "",
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
log.error("Failed to parse settings base response", { error });
|
||||
}
|
||||
});
|
||||
|
||||
const config = event
|
||||
.fetch(`/api/tenants/${event.locals.user?.tenantId}/config`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "same-origin",
|
||||
})
|
||||
.then(async (res) => {
|
||||
// Logout if session expired
|
||||
if (res.status === 401) {
|
||||
redirect(302, ROUTES.LOGOUT);
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await res.json();
|
||||
return {
|
||||
address: {
|
||||
street: body["address.street"] || "",
|
||||
number: body["address.number"] || "",
|
||||
additionalAddressInfo: body["address.additionalAddressInfo"] || "",
|
||||
zip: body["address.zip"] || "",
|
||||
city: body["address.city"] || "",
|
||||
},
|
||||
settings: {
|
||||
autoDeleteDays: body.autoDeleteDays || 90,
|
||||
requirePhone: body.requirePhone || false,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
log.error("Failed to parse settings config response", { error });
|
||||
}
|
||||
});
|
||||
|
||||
const item = Promise.all([base, config]).then(([base, config]) => {
|
||||
return {
|
||||
...base,
|
||||
...config,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
streamed: {
|
||||
item,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
edit: async (event) => {
|
||||
const form = await superValidate(event, zod(editFormSchema));
|
||||
@@ -110,44 +19,67 @@ export const actions: Actions = {
|
||||
});
|
||||
}
|
||||
|
||||
const base = await event.fetch(`/api/tenants/${form.data.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
languages: form.data.languages,
|
||||
defaultLanguage: form.data.defaultLanguage,
|
||||
longName: form.data.longName,
|
||||
logo: form.data.logo,
|
||||
descriptions: removeEmptyTranslations(form.data.descriptions),
|
||||
links: form.data.links,
|
||||
}),
|
||||
});
|
||||
const requests = [];
|
||||
if (form.data.longName || form.data.links) {
|
||||
const base = await event.fetch(`/api/tenants/${form.data.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
...(form.data.longName
|
||||
? {
|
||||
languages: form.data.languages,
|
||||
defaultLanguage: form.data.defaultLanguage,
|
||||
longName: form.data.longName,
|
||||
logo: form.data.logo,
|
||||
descriptions: removeEmptyTranslations(form.data.descriptions),
|
||||
}
|
||||
: undefined),
|
||||
...(form.data.links
|
||||
? {
|
||||
links: form.data.links,
|
||||
}
|
||||
: undefined),
|
||||
}),
|
||||
});
|
||||
requests.push(base);
|
||||
}
|
||||
|
||||
const config = await event.fetch(`/api/tenants/${form.data.id}/config`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
"address.street": form.data.address.street,
|
||||
"address.number": form.data.address.number,
|
||||
"address.additionalAddressInfo": form.data.address.additionalAddressInfo,
|
||||
"address.zip": form.data.address.zip,
|
||||
"address.city": form.data.address.city,
|
||||
autoDeleteDays: form.data.settings.autoDeleteDays,
|
||||
requirePhone: form.data.settings.requirePhone ?? false,
|
||||
}),
|
||||
});
|
||||
if (form.data.address || form.data.settings) {
|
||||
const config = await event.fetch(`/api/tenants/${form.data.id}/config`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
...(form.data.address
|
||||
? {
|
||||
"address.street": form.data.address.street,
|
||||
"address.number": form.data.address.number,
|
||||
"address.additionalAddressInfo": form.data.address.additionalAddressInfo,
|
||||
"address.zip": form.data.address.zip,
|
||||
"address.city": form.data.address.city,
|
||||
}
|
||||
: undefined),
|
||||
...(form.data.settings
|
||||
? {
|
||||
autoDeleteDays: form.data.settings.autoDeleteDays,
|
||||
requirePhone: form.data.settings.requirePhone ?? false,
|
||||
}
|
||||
: undefined),
|
||||
}),
|
||||
});
|
||||
requests.push(config);
|
||||
}
|
||||
|
||||
const resp = await Promise.all([base, config]).then(([base, config]) => {
|
||||
if (base.status < 400 && config.status < 400) {
|
||||
const resp = await Promise.all(requests).then((all) => {
|
||||
if (all.every((res) => res.status < 400)) {
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, bodies: { base: base, config: config } };
|
||||
return { success: false, bodies: { base: all[0], config: all[1] } };
|
||||
});
|
||||
|
||||
if (resp.success) {
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from "$app/paths";
|
||||
import { m } from "$i18n/messages";
|
||||
import { MaxPageWidth } from "$lib/components/layouts/max-page-width";
|
||||
import { SidebarLayout } from "$lib/components/layouts/sidebar-layout";
|
||||
import { LoadingItemGroup } from "$lib/components/templates/loading/index.js";
|
||||
import { PageHeadline } from "$lib/components/templates/page-headline";
|
||||
import { GroupItem } from "$lib/components/ui/group-item";
|
||||
import * as Item from "$lib/components/ui/item";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { Card } from "$lib/components/ui/card";
|
||||
import { Headline } from "$lib/components/ui/typography";
|
||||
import { EditSettingsForm } from "./(components)/edit-settings-form";
|
||||
import { LoadingCenter } from "$lib/components/templates/loading";
|
||||
import type { TTenantSettings } from "$lib/types/tenant";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
const isAddressComplete = (address: TTenantSettings["address"]) => {
|
||||
return (
|
||||
address &&
|
||||
address.street !== "" &&
|
||||
address.number !== "" &&
|
||||
address.zip !== "" &&
|
||||
address.city !== ""
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -19,18 +31,45 @@
|
||||
breakcrumbs={[
|
||||
{
|
||||
label: m["nav.settings"](),
|
||||
href: ROUTES.DASHBOARD.SETTINGS,
|
||||
href: ROUTES.DASHBOARD.SETTINGS.MAIN,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<MaxPageWidth maxWidth="lg">
|
||||
<MaxPageWidth maxWidth="lg" class="flex flex-col gap-6">
|
||||
{#await data.streamed.item}
|
||||
<LoadingCenter title={m["settings.loading"]()} class="h-[80vh] py-20" />
|
||||
<LoadingItemGroup title={m["settings.loading"]()} />
|
||||
{:then item}
|
||||
<Card>
|
||||
<Headline style="h4" level="h1">Settings</Headline>
|
||||
<EditSettingsForm entity={item} />
|
||||
</Card>
|
||||
{#if item}
|
||||
<PageHeadline title={m["settings.overview.title"]()} />
|
||||
<Item.Group class="gap-2">
|
||||
<GroupItem
|
||||
title={m["settings.general.title"]()}
|
||||
description={m["settings.general.description"]()}
|
||||
badges={item.longName === ""
|
||||
? [{ variant: "destructive", label: m["required"]() }]
|
||||
: undefined}
|
||||
href={resolve(ROUTES.DASHBOARD.SETTINGS.GENERAL)}
|
||||
/>
|
||||
<GroupItem
|
||||
title={m["settings.address.title"]()}
|
||||
description={m["settings.address.description"]()}
|
||||
badges={!isAddressComplete(item.address)
|
||||
? [{ variant: "destructive", label: m["required"]() }]
|
||||
: undefined}
|
||||
href={resolve(ROUTES.DASHBOARD.SETTINGS.ADDRESS)}
|
||||
/>
|
||||
<GroupItem
|
||||
title={m["settings.links.title"]()}
|
||||
description={m["settings.links.description"]()}
|
||||
href={resolve(ROUTES.DASHBOARD.SETTINGS.LINKS)}
|
||||
/>
|
||||
<GroupItem
|
||||
title={m["settings.advanced.title"]()}
|
||||
description={m["settings.advanced.description"]()}
|
||||
href={resolve(ROUTES.DASHBOARD.SETTINGS.ADVANCED)}
|
||||
/>
|
||||
</Item.Group>
|
||||
{/if}
|
||||
{/await}
|
||||
</MaxPageWidth>
|
||||
</SidebarLayout>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { m } from "$i18n/messages";
|
||||
import { MaxPageWidth } from "$lib/components/layouts/max-page-width";
|
||||
import { SidebarLayout } from "$lib/components/layouts/sidebar-layout";
|
||||
import { LoadingDetail } from "$lib/components/templates/loading";
|
||||
import { PageHeadline } from "$lib/components/templates/page-headline";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { EditSettingsFormAddress } from "../(components)/edit-settings-form";
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m["settings.title"]()}: {m["settings.address.title"]()} - OpenReception</title>
|
||||
</svelte:head>
|
||||
|
||||
<SidebarLayout
|
||||
breakcrumbs={[
|
||||
{
|
||||
label: m["nav.settings"](),
|
||||
href: ROUTES.DASHBOARD.SETTINGS.MAIN,
|
||||
},
|
||||
{
|
||||
label: m["settings.address.title"](),
|
||||
href: ROUTES.DASHBOARD.SETTINGS.ADDRESS,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<MaxPageWidth maxWidth="lg" class="flex flex-col gap-6">
|
||||
{#await data.streamed.item}
|
||||
<LoadingDetail title={m["settings.loading"]()} />
|
||||
{:then item}
|
||||
{#if item}
|
||||
<PageHeadline
|
||||
title={m["settings.address.title"]()}
|
||||
description={m["settings.address.description"]()}
|
||||
backHref={ROUTES.DASHBOARD.SETTINGS.MAIN}
|
||||
/>
|
||||
<EditSettingsFormAddress entity={item} />
|
||||
{/if}
|
||||
{/await}
|
||||
</MaxPageWidth>
|
||||
</SidebarLayout>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { m } from "$i18n/messages";
|
||||
import { MaxPageWidth } from "$lib/components/layouts/max-page-width";
|
||||
import { SidebarLayout } from "$lib/components/layouts/sidebar-layout";
|
||||
import { LoadingDetail } from "$lib/components/templates/loading";
|
||||
import { PageHeadline } from "$lib/components/templates/page-headline";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { EditSettingsFormAdvanced } from "../(components)/edit-settings-form";
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m["settings.title"]()}: {m["settings.advanced.title"]()} - OpenReception</title>
|
||||
</svelte:head>
|
||||
|
||||
<SidebarLayout
|
||||
breakcrumbs={[
|
||||
{
|
||||
label: m["nav.settings"](),
|
||||
href: ROUTES.DASHBOARD.SETTINGS.MAIN,
|
||||
},
|
||||
{
|
||||
label: m["settings.advanced.title"](),
|
||||
href: ROUTES.DASHBOARD.SETTINGS.ADVANCED,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<MaxPageWidth maxWidth="lg" class="flex flex-col gap-6">
|
||||
{#await data.streamed.item}
|
||||
<LoadingDetail title={m["settings.loading"]()} />
|
||||
{:then item}
|
||||
{#if item}
|
||||
<PageHeadline
|
||||
title={m["settings.advanced.title"]()}
|
||||
description={m["settings.advanced.description"]()}
|
||||
backHref={ROUTES.DASHBOARD.SETTINGS.MAIN}
|
||||
/>
|
||||
<EditSettingsFormAdvanced entity={item} />
|
||||
{/if}
|
||||
{/await}
|
||||
</MaxPageWidth>
|
||||
</SidebarLayout>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { m } from "$i18n/messages";
|
||||
import { MaxPageWidth } from "$lib/components/layouts/max-page-width";
|
||||
import { SidebarLayout } from "$lib/components/layouts/sidebar-layout";
|
||||
import { LoadingDetail } from "$lib/components/templates/loading";
|
||||
import { PageHeadline } from "$lib/components/templates/page-headline";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { EditSettingsFormGeneral } from "../(components)/edit-settings-form";
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m["settings.title"]()}: {m["settings.general.title"]()} - OpenReception</title>
|
||||
</svelte:head>
|
||||
|
||||
<SidebarLayout
|
||||
breakcrumbs={[
|
||||
{
|
||||
label: m["nav.settings"](),
|
||||
href: ROUTES.DASHBOARD.SETTINGS.MAIN,
|
||||
},
|
||||
{
|
||||
label: m["settings.general.title"](),
|
||||
href: ROUTES.DASHBOARD.SETTINGS.GENERAL,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<MaxPageWidth maxWidth="lg" class="flex flex-col gap-6">
|
||||
{#await data.streamed.item}
|
||||
<LoadingDetail title={m["settings.loading"]()} />
|
||||
{:then item}
|
||||
{#if item}
|
||||
<PageHeadline
|
||||
title={m["settings.general.title"]()}
|
||||
description={m["settings.general.description"]()}
|
||||
backHref={ROUTES.DASHBOARD.SETTINGS.MAIN}
|
||||
/>
|
||||
<EditSettingsFormGeneral entity={item} />
|
||||
{/if}
|
||||
{/await}
|
||||
</MaxPageWidth>
|
||||
</SidebarLayout>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { m } from "$i18n/messages";
|
||||
import { MaxPageWidth } from "$lib/components/layouts/max-page-width";
|
||||
import { SidebarLayout } from "$lib/components/layouts/sidebar-layout";
|
||||
import { LoadingDetail } from "$lib/components/templates/loading";
|
||||
import { PageHeadline } from "$lib/components/templates/page-headline";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { EditSettingsFormLinks } from "../(components)/edit-settings-form";
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m["settings.title"]()}: {m["settings.links.title"]()} - OpenReception</title>
|
||||
</svelte:head>
|
||||
|
||||
<SidebarLayout
|
||||
breakcrumbs={[
|
||||
{
|
||||
label: m["nav.settings"](),
|
||||
href: ROUTES.DASHBOARD.SETTINGS.MAIN,
|
||||
},
|
||||
{
|
||||
label: m["settings.links.title"](),
|
||||
href: ROUTES.DASHBOARD.SETTINGS.LINKS,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<MaxPageWidth maxWidth="lg" class="flex flex-col gap-6">
|
||||
{#await data.streamed.item}
|
||||
<LoadingDetail title={m["settings.loading"]()} />
|
||||
{:then item}
|
||||
{#if item}
|
||||
<PageHeadline
|
||||
title={m["settings.links.title"]()}
|
||||
description={m["settings.links.description"]()}
|
||||
backHref={ROUTES.DASHBOARD.SETTINGS.MAIN}
|
||||
/>
|
||||
<EditSettingsFormLinks entity={item} />
|
||||
{/if}
|
||||
{/await}
|
||||
</MaxPageWidth>
|
||||
</SidebarLayout>
|
||||
@@ -92,7 +92,6 @@
|
||||
type: "action",
|
||||
icon: EditIcon,
|
||||
label: m["edit"](),
|
||||
isMainAction: true,
|
||||
onClick: () => {
|
||||
curItem = item;
|
||||
openDialog("edit");
|
||||
@@ -101,6 +100,7 @@
|
||||
{
|
||||
type: "action",
|
||||
icon: SelectIcon,
|
||||
isMainAction: true,
|
||||
label: m["select"](),
|
||||
onClick: () => tenantsStore.setCurrentTenant(item.id),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user