From f8d0299490a677fb1895e0691ff3f7104f31258a Mon Sep 17 00:00:00 2001 From: Karl Ludwig Weise Date: Thu, 8 Oct 2026 21:41:25 +0200 Subject: [PATCH] Settings: Splitting up settings form into many smaller forms like for channels --- package-lock.json | 12 +- project.inlang/messages/de.json | 70 +++- project.inlang/messages/en.json | 71 +++- .../components/nav-secondary.svelte | 2 +- .../templates/form-split/form-split.svelte | 16 +- src/lib/const/routes.ts | 8 +- src/lib/stores/tenants.ts | 9 + src/routes/(pages)/dashboard/+page.svelte | 2 +- .../(pages)/dashboard/account/+page.svelte | 91 ++--- .../edit-settings-form-address.svelte | 125 +++++++ .../edit-settings-form-advanced.svelte | 152 ++++++++ .../edit-settings-form-general.svelte | 204 +++++++++++ .../edit-settings-form-links.svelte | 117 ++++++ .../edit-settings-form.svelte | 343 ------------------ .../(components)/edit-settings-form/index.ts | 12 +- .../(components)/edit-settings-form/schema.ts | 53 +-- .../dashboard/settings/+layout.server.ts | 98 +++++ .../dashboard/settings/+page.server.ts | 186 +++------- .../(pages)/dashboard/settings/+page.svelte | 61 +++- .../dashboard/settings/address/+page.svelte | 43 +++ .../dashboard/settings/advanced/+page.svelte | 43 +++ .../dashboard/settings/general/+page.svelte | 43 +++ .../dashboard/settings/links/+page.svelte | 43 +++ .../(pages)/dashboard/tenants/+page.svelte | 2 +- 24 files changed, 1197 insertions(+), 609 deletions(-) create mode 100644 src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form-address.svelte create mode 100644 src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form-advanced.svelte create mode 100644 src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form-general.svelte create mode 100644 src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form-links.svelte delete mode 100644 src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form.svelte create mode 100644 src/routes/(pages)/dashboard/settings/+layout.server.ts create mode 100644 src/routes/(pages)/dashboard/settings/address/+page.svelte create mode 100644 src/routes/(pages)/dashboard/settings/advanced/+page.svelte create mode 100644 src/routes/(pages)/dashboard/settings/general/+page.svelte create mode 100644 src/routes/(pages)/dashboard/settings/links/+page.svelte diff --git a/package-lock.json b/package-lock.json index 593a46a..f8c6c95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/project.inlang/messages/de.json b/project.inlang/messages/de.json index b5b0c3b..cefa317 100644 --- a/project.inlang/messages/de.json +++ b/project.inlang/messages/de.json @@ -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" }, diff --git a/project.inlang/messages/en.json b/project.inlang/messages/en.json index d1a900a..915bf61 100644 --- a/project.inlang/messages/en.json +++ b/project.inlang/messages/en.json @@ -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" }, diff --git a/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte b/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte index 45b6fe5..cdaba5c 100644 --- a/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte +++ b/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte @@ -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"], diff --git a/src/lib/components/templates/form-split/form-split.svelte b/src/lib/components/templates/form-split/form-split.svelte index 2a47f5b..e3925b0 100644 --- a/src/lib/components/templates/form-split/form-split.svelte +++ b/src/lib/components/templates/form-split/form-split.svelte @@ -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();
@@ -21,8 +28,13 @@ {description} {/if} + {#if hint} +
+ {@render hint?.()} +
+ {/if}
-
+
{@render children?.()}
diff --git a/src/lib/const/routes.ts b/src/lib/const/routes.ts index 9a8196b..0b5498b 100644 --- a/src/lib/const/routes.ts +++ b/src/lib/const/routes.ts @@ -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", diff --git a/src/lib/stores/tenants.ts b/src/lib/stores/tenants.ts index c4f1a72..a17a784 100644 --- a/src/lib/stores/tenants.ts +++ b/src/lib/stores/tenants.ts @@ -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, diff --git a/src/routes/(pages)/dashboard/+page.svelte b/src/routes/(pages)/dashboard/+page.svelte index a4b677c..9897d4e 100644 --- a/src/routes/(pages)/dashboard/+page.svelte +++ b/src/routes/(pages)/dashboard/+page.svelte @@ -107,7 +107,7 @@ { label: m["dashboard.onboarding.sections.settings.action"](), onClick: () => { - goto(resolve(ROUTES.DASHBOARD.SETTINGS)); + goto(resolve(ROUTES.DASHBOARD.SETTINGS.MAIN)); }, }, ], diff --git a/src/routes/(pages)/dashboard/account/+page.svelte b/src/routes/(pages)/dashboard/account/+page.svelte index 8f1740b..6ef96d0 100644 --- a/src/routes/(pages)/dashboard/account/+page.svelte +++ b/src/routes/(pages)/dashboard/account/+page.svelte @@ -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"; - - {#snippet child({ props })} - - - - {m["account.general.title"]()} - - - {m["account.general.description"]()} - - - - - - - {/snippet} - - - - {#snippet child({ props })} - - - - {m["account.passkeys.title"]()} - - - {m["account.passkeys.description"]()} - - - - - - - {/snippet} - + + + {#if $auth.user?.role === "GLOBAL_ADMIN"} - - {#snippet child({ props })} - - - - {m["account.change-passphrase.title"]()} - - - {m["account.change-passphrase.description"]()} - - - - - - - {/snippet} - + {/if} diff --git a/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form-address.svelte b/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form-address.svelte new file mode 100644 index 0000000..e356981 --- /dev/null +++ b/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form-address.svelte @@ -0,0 +1,125 @@ + + + + + +
+
+ + + {#snippet children({ props })} + {m["settings.form.fields.street.title"]()} + + {/snippet} + + + + + + {#snippet children({ props })} + {m["settings.form.fields.number.title"]()} + + {/snippet} + + + +
+ + + {#snippet children({ props })} + {m["settings.form.fields.additionalAddressInfo.title"]()} + + {/snippet} + + + +
+ + + {#snippet children({ props })} + {m["settings.form.fields.zip.title"]()} + + {/snippet} + + + + + + {#snippet children({ props })} + {m["settings.form.fields.city.title"]()} + + {/snippet} + + + +
+
+
+ +
+ + {m["save"]()} + +
+
diff --git a/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form-advanced.svelte b/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form-advanced.svelte new file mode 100644 index 0000000..799e38d --- /dev/null +++ b/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form-advanced.svelte @@ -0,0 +1,152 @@ + + + + + + + + {#snippet children({ props })} + {m["settings.form.fields.autoDeleteDays.title"]()} + ($formData.settings.autoDeleteDays = parseInt(v))} + > + + {$formData.settings.autoDeleteDays + ? autoDeleteDaysOptions.find((x) => x.key === $formData.settings.autoDeleteDays) + ?.label + : m["settings.form.fields.autoDeleteDays.placeholder"]()} + + + {#each autoDeleteDaysOptions as option (option.key)} + + {option.label} + + {/each} + + + {/snippet} + + + + + + + + {#snippet children({ props })} + {m["settings.form.fields.requirePhone.title"]()} + { + $formData.settings.requirePhone = v; + }} + /> + {/snippet} + + + + + +
+ + {m["save"]()} + +
+
+ +{#snippet hint()} + +{/snippet} diff --git a/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form-general.svelte b/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form-general.svelte new file mode 100644 index 0000000..c96f704 --- /dev/null +++ b/src/routes/(pages)/dashboard/settings/(components)/edit-settings-form/edit-settings-form-general.svelte @@ -0,0 +1,204 @@ + + + + + +
+ + + {#snippet children({ props })} + {m["settings.general.languages.languages.title"]()} + ($formData.languages = v)} + > + + {$formData.languages.length > 0 + ? $formData.languages + .map((id) => availableLocales.find((x) => x.key === id)?.label) + .join(", ") + : m["settings.general.languages.languages.placeholder"]()} + + + {#each availableLocales as locale (locale.key)} + {locale.label} + {/each} + + + + {m["settings.general.languages.languages.description"]()} + + {/snippet} + + + + + + {#snippet children({ props })} + {m["settings.general.languages.defaultLanguage.title"]()} + ($formData.defaultLanguage = v)} + > + + {$formData.defaultLanguage + ? availableLocales.find((x) => x.key === $formData.defaultLanguage)?.label + : m["settings.general.languages.defaultLanguage.placeholder"]()} + + + {#each $formData.languages as language (language)} + + {availableLocales.find((x) => x.key === language)?.label} + + {/each} + + + + {m["settings.general.languages.defaultLanguage.description"]()} + + {/snippet} + + + +
+
+ +
+ + + {#snippet children({ props })} + {m["settings.general.branding.logo.title"]()} + + {/snippet} + + + + + + {#snippet children({ props })} + {m["form.name"]()} + + {/snippet} + + + {m["settings.general.branding.longName.description"]()} + + + + + {#snippet children({ locale })} + + + {#snippet children({ props })} + {m["settings.general.branding.descriptions.title"]()} +