mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-09-24 01:44:51 +02:00
Merge branch 'main' of github.com:open-reception/appointment-booking-software into 148-provide-apis-for-cancelling-appointments-staff-and-client-wise
This commit is contained in:
@@ -6,7 +6,24 @@ on:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
test-and-build:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -24,9 +41,21 @@ jobs:
|
||||
|
||||
- name: Run type checking
|
||||
run: npm run check
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm run test:unit -- --run
|
||||
@@ -41,6 +70,22 @@ jobs:
|
||||
SMTP_FROM_EMAIL: "noreply@example.com"
|
||||
JWT_SECRET: "test"
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build application
|
||||
run: npm run build
|
||||
env:
|
||||
|
||||
Generated
+491
-641
File diff suppressed because it is too large
Load Diff
+22
-22
@@ -38,38 +38,38 @@
|
||||
"db:clean": "npm run docker:dev:down && npm run db:drop && npm run docker:dev:up"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.3.0",
|
||||
"@eslint/js": "^9.29.0",
|
||||
"@inlang/paraglide-js": "2.3.2",
|
||||
"@internationalized/date": "^3.8.2",
|
||||
"@lucide/svelte": "^0.544.0",
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@sveltejs/adapter-auto": "^6.1.0",
|
||||
"@sveltejs/kit": "^2.22.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/svelte": "^5.2.8",
|
||||
"@eslint/compat": "^2.0.1",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@inlang/paraglide-js": "2.9.1",
|
||||
"@internationalized/date": "^3.10.1",
|
||||
"@lucide/svelte": "^0.562.0",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/kit": "^2.50.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/svelte": "^5.3.1",
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/node": "^24",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"bits-ui": "^2.11.4",
|
||||
"bits-ui": "^2.15.4",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-svelte": "^3.9.3",
|
||||
"eslint-plugin-svelte": "^3.14.0",
|
||||
"formsnap": "^2.0.1",
|
||||
"globals": "^16.2.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.13",
|
||||
"svelte": "^5.34.7",
|
||||
"svelte-check": "^4.2.2",
|
||||
"svelte-sonner": "^1.0.5",
|
||||
"sveltekit-superforms": "^2.28.0",
|
||||
"prettier-plugin-svelte": "^3.4.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"svelte": "^5.46.4",
|
||||
"svelte-check": "^4.3.5",
|
||||
"svelte-sonner": "^1.0.7",
|
||||
"sveltekit-superforms": "^2.29.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-variants": "^3.1.1",
|
||||
"tailwindcss": "^4.1.10",
|
||||
@@ -84,7 +84,7 @@
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@noble/post-quantum": "^0.4.1",
|
||||
"@simplewebauthn/server": "^11.0.0",
|
||||
"@sveltejs/adapter-node": "^5.2.12",
|
||||
"@sveltejs/adapter-node": "^5.5.1",
|
||||
"argon2": "^0.43.0",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"cropperjs": "^2.0.1",
|
||||
|
||||
@@ -1 +1,19 @@
|
||||
cache
|
||||
# IF GIT SHOWED THAT THIS FILE CHANGED
|
||||
#
|
||||
# 1. RUN THE FOLLOWING COMMAND
|
||||
#
|
||||
# ---
|
||||
# git rm --cached '**/*.inlang/.gitignore'
|
||||
# ---
|
||||
#
|
||||
# 2. COMMIT THE CHANGE
|
||||
#
|
||||
# ---
|
||||
# git commit -m "fix: remove tracked .gitignore from inlang project"
|
||||
# ---
|
||||
#
|
||||
# Inlang handles the gitignore itself starting with version ^2.5.
|
||||
#
|
||||
# everything is ignored except settings.json
|
||||
*
|
||||
!settings.json
|
||||
@@ -844,7 +844,8 @@
|
||||
"description": "Nur Sie und {name} können diese Informationen sehen. Die Daten sind Ende-zu-Ende verschlüsselt."
|
||||
},
|
||||
"email": {
|
||||
"description": "Wir senden Ihnen eine Bestätigungs-E-Mail mit den Termindetails."
|
||||
"notifyMe": "Benachrichtigen Sie mich über Änderungen per E-Mail",
|
||||
"tooltip": "Wenn aktiviert, wird Ihre Mail vorübergehend im Logbuch des Mailservers auftauchen. Wenn deaktiviert müssen Sie sich auf dieser Seite Einloggen, um Änderungen einzusehen."
|
||||
},
|
||||
"phone": {
|
||||
"description": "Format: +491234567890. {name} kontaktiert Sie ggf. telefonisch, falls es Fragen/Änderungen zu Ihrem Termin gibt.",
|
||||
|
||||
@@ -853,7 +853,8 @@
|
||||
"description": "Only you and {name} will be able to access this information. It is end-to-end encrypted."
|
||||
},
|
||||
"email": {
|
||||
"description": "We will use this to send you the appointment details."
|
||||
"notifyMe": "Notify me of changes by e-mail",
|
||||
"tooltip": "If enabled, your e-mail address will appear temporarily in server logs of the mailserver. If disabled, you will have to login on this page to check for updates."
|
||||
},
|
||||
"phone": {
|
||||
"description": "Format: +1234567890. {name} may contact you by phone, if there are questions or changes regarding your appointment.",
|
||||
|
||||
@@ -3,6 +3,27 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { handle } from "./hooks.server";
|
||||
import { mockCookies } from "$lib/tests/const";
|
||||
|
||||
// Mock the sequence function to avoid the request store issue
|
||||
vi.mock("@sveltejs/kit/hooks", () => ({
|
||||
sequence: (...handlers: any[]) => {
|
||||
// Return a simple sequential handler that doesn't require request store
|
||||
return async ({ event, resolve }: any) => {
|
||||
let currentResolve = resolve;
|
||||
|
||||
// Chain handlers in reverse order
|
||||
for (let i = handlers.length - 1; i >= 0; i--) {
|
||||
const handler = handlers[i];
|
||||
const previousResolve = currentResolve;
|
||||
currentResolve = async (event: any) => {
|
||||
return handler({ event, resolve: previousResolve });
|
||||
};
|
||||
}
|
||||
|
||||
return currentResolve(event);
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the startup service
|
||||
vi.mock("$lib/server/services/startup-service", () => ({
|
||||
StartupService: {
|
||||
@@ -72,6 +93,8 @@ describe("hooks.server", () => {
|
||||
isDataRequest: false,
|
||||
isSubRequest: false,
|
||||
platform: {} as any,
|
||||
tracing: { enabled: false, root: "", current: "" },
|
||||
isRemoteRequest: false,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -142,6 +165,8 @@ describe("hooks.server", () => {
|
||||
isDataRequest: false,
|
||||
isSubRequest: false,
|
||||
platform: {} as any,
|
||||
tracing: { enabled: false, root: "", current: "" },
|
||||
isRemoteRequest: false,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -189,6 +214,8 @@ describe("hooks.server", () => {
|
||||
isDataRequest: false,
|
||||
isSubRequest: false,
|
||||
platform: {} as any,
|
||||
tracing: { enabled: false, root: "", current: "" },
|
||||
isRemoteRequest: false,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -71,6 +71,7 @@ interface EncryptedData {
|
||||
export interface AppointmentData {
|
||||
name: string;
|
||||
email: string;
|
||||
shareEmail: boolean;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
@@ -319,7 +320,7 @@ export class UnifiedAppointmentCrypto {
|
||||
appointmentDate,
|
||||
duration,
|
||||
emailHash: this.emailHash,
|
||||
clientEmail: appointmentData.email,
|
||||
clientEmail: appointmentData.shareEmail ? appointmentData.email : undefined,
|
||||
clientLanguage,
|
||||
clientPublicKey: this.clientKeyPair?.publicKey,
|
||||
privateKeyShare: await this.getPrivateKeyShare(),
|
||||
@@ -336,7 +337,7 @@ export class UnifiedAppointmentCrypto {
|
||||
channelId,
|
||||
appointmentDate,
|
||||
duration,
|
||||
clientEmail: appointmentData.email,
|
||||
clientEmail: appointmentData.shareEmail ? appointmentData.email : undefined,
|
||||
clientLanguage,
|
||||
encryptedAppointment,
|
||||
};
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton isActive={isCurrentSection(item.url)} tooltipContent={item.title}>
|
||||
{#snippet child({ props })}
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||
<a href={item.url} {...props}>
|
||||
<item.icon />
|
||||
<Text style="md" class="ml-2">{item.title}</Text>
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
{#if $auth.user && item.roles.includes($auth.user?.role)}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton isActive={isCurrentSection(item.url)} tooltipContent={item.title}>
|
||||
<!-- eslint-disable svelte/no-navigation-without-resolve -->
|
||||
{#snippet child({ props })}
|
||||
<a
|
||||
href={item.url}
|
||||
@@ -55,6 +56,7 @@
|
||||
{/if}
|
||||
</a>
|
||||
{/snippet}
|
||||
<!-- eslint-enable svelte/no-navigation-without-resolve -->
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
{/if}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { nameToAvatarFallback } from "$lib/utils/name";
|
||||
import { LanguageSwitch } from "$lib/components/templates/language-switch";
|
||||
import { m } from "$i18n/messages";
|
||||
import { resolve } from "$app/paths";
|
||||
|
||||
const sidebar = useSidebar();
|
||||
</script>
|
||||
@@ -61,13 +62,13 @@
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<LanguageSwitch class="w-full" triggerClass="w-[100%] [&>svg]:ml-auto" />
|
||||
<DropdownMenu.Item onclick={() => goto(ROUTES.DASHBOARD.ACCOUNT.MAIN)}>
|
||||
<DropdownMenu.Item onclick={() => goto(resolve(ROUTES.DASHBOARD.ACCOUNT.MAIN))}>
|
||||
<AccountIcon />
|
||||
{m["nav.account"]()}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onclick={() => goto(ROUTES.LOGOUT)}>
|
||||
<DropdownMenu.Item onclick={() => goto(resolve(ROUTES.LOGOUT))}>
|
||||
<LogOutIcon />
|
||||
{m["nav.logout"]()}
|
||||
</DropdownMenu.Item>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import UnknownTenantIcon from "@lucide/svelte/icons/landmark";
|
||||
import Loader from "@lucide/svelte/icons/loader-2";
|
||||
import { cn } from "$lib/utils";
|
||||
import { resolve } from "$app/paths";
|
||||
|
||||
const sidebar = useSidebar();
|
||||
|
||||
@@ -116,7 +117,10 @@
|
||||
{/each}
|
||||
{#if $tenants?.tenants.length > maxTenantsToShow}
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item class="gap-2 p-2" onclick={() => goto(ROUTES.DASHBOARD.TENANTS)}>
|
||||
<DropdownMenu.Item
|
||||
class="gap-2 p-2"
|
||||
onclick={() => goto(resolve(ROUTES.DASHBOARD.TENANTS))}
|
||||
>
|
||||
<div
|
||||
class="flex size-6 items-center justify-center rounded-md border bg-transparent"
|
||||
>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
size?: VariantProps<typeof variants>["size"];
|
||||
} = $props();
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const isSmaller = size === "sm";
|
||||
</script>
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<!-- eslint-disable svelte/no-navigation-without-resolve -->
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
@@ -70,6 +71,7 @@
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
<!-- eslint-enable svelte/no-navigation-without-resolve -->
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
|
||||
@@ -1,31 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { Checkbox } from "../checkbox";
|
||||
import * as Popover from "$lib/components/ui/popover/index.js";
|
||||
import { cn } from "$lib/utils";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { Text } from "../typography";
|
||||
import type { OnChangeFn } from "vaul-svelte";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { Text } from "../typography";
|
||||
import { Info } from "@lucide/svelte/icons";
|
||||
import { buttonVariants } from "../button";
|
||||
|
||||
let {
|
||||
class: className,
|
||||
value = $bindable(false),
|
||||
label,
|
||||
tooltip,
|
||||
onCheckedChange,
|
||||
...restProps
|
||||
}: HTMLAttributes<HTMLButtonElement> & {
|
||||
value: boolean;
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
id?: string;
|
||||
onCheckedChange?: OnChangeFn<boolean>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<label class={cn("flex items-start gap-2", className)}>
|
||||
<Checkbox
|
||||
{...restProps}
|
||||
checked={value}
|
||||
value={value ? "true" : "false"}
|
||||
{onCheckedChange}
|
||||
class="mt-0.5"
|
||||
/>
|
||||
<Text style="sm" class="font-normal select-none">{label}</Text>
|
||||
<label class={cn("flex items-center gap-2", className)}>
|
||||
<div class="flex items-start gap-2">
|
||||
<Checkbox
|
||||
{...restProps}
|
||||
checked={value}
|
||||
value={value ? "true" : "false"}
|
||||
{onCheckedChange}
|
||||
class="mt-0.5"
|
||||
/>
|
||||
<Text style="sm" class="font-normal select-none">{label}</Text>
|
||||
</div>
|
||||
{#if tooltip}
|
||||
<Popover.Root>
|
||||
<Popover.Trigger class={cn(buttonVariants({ variant: "ghost", size: "xs" }), "p-1!")}>
|
||||
<Info size="sm" />
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-65 leading-1">
|
||||
<Text style="xs">
|
||||
{tooltip}
|
||||
</Text>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{cell.char}
|
||||
{cell.char ? "*" : null}
|
||||
{#if cell.hasFakeCaret}
|
||||
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div class="animate-caret-blink bg-foreground h-4 w-px duration-1000"></div>
|
||||
|
||||
@@ -7,7 +7,13 @@
|
||||
let { value = $bindable(), ...restProps }: Props = $props();
|
||||
</script>
|
||||
|
||||
<InputOTP.Root {...restProps} maxlength={6} bind:value>
|
||||
<InputOTP.Root
|
||||
{...restProps}
|
||||
maxlength={6}
|
||||
type="password"
|
||||
pushPasswordManagerStrategy="none"
|
||||
bind:value
|
||||
>
|
||||
{#snippet children({ cells })}
|
||||
<InputOTP.Group>
|
||||
{#each cells.slice(0, 3) as cell (cell)}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
import Root from "./popover.svelte";
|
||||
import Close from "./popover-close.svelte";
|
||||
import Content from "./popover-content.svelte";
|
||||
import Trigger from "./popover-trigger.svelte";
|
||||
const Root = PopoverPrimitive.Root;
|
||||
const Close = PopoverPrimitive.Close;
|
||||
import Portal from "./popover-portal.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
Close,
|
||||
Portal,
|
||||
//
|
||||
Root as Popover,
|
||||
Content as PopoverContent,
|
||||
Trigger as PopoverTrigger,
|
||||
Close as PopoverClose,
|
||||
Portal as PopoverPortal,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} />
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
import PopoverPortal from "./popover-portal.svelte";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -10,20 +12,20 @@
|
||||
portalProps,
|
||||
...restProps
|
||||
}: PopoverPrimitive.ContentProps & {
|
||||
portalProps?: PopoverPrimitive.PortalProps;
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Portal {...portalProps}>
|
||||
<PopoverPortal {...portalProps}>
|
||||
<PopoverPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="popover-content"
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--bits-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--bits-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
</PopoverPortal>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: PopoverPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Root bind:open {...restProps} />
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
export function openDialog(id: string) {
|
||||
responsiveDialogs.update((state) => {
|
||||
const newState = new Map(state);
|
||||
const newState = new SvelteMap(state);
|
||||
newState.set(id, true);
|
||||
return newState;
|
||||
});
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
export function closeDialog(id: string) {
|
||||
responsiveDialogs.update((state) => {
|
||||
const newState = new Map(state);
|
||||
const newState = new SvelteMap(state);
|
||||
if (newState.has(id)) {
|
||||
newState.set(id, false);
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
import * as Drawer from "$lib/components/ui/drawer";
|
||||
import { onDestroy, type Snippet } from "svelte";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { MediaQuery } from "svelte/reactivity";
|
||||
import { MediaQuery, SvelteMap } from "svelte/reactivity";
|
||||
import { HorizontalPagePadding } from "../page";
|
||||
import { ScrollArea } from "../scroll-area";
|
||||
import { cn } from "$lib/utils";
|
||||
@@ -80,7 +80,7 @@
|
||||
|
||||
onDestroy(() => {
|
||||
responsiveDialogs.update((state) => {
|
||||
const newState = new Map(state);
|
||||
const newState = new SvelteMap(state);
|
||||
newState.delete(id);
|
||||
return newState;
|
||||
});
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
return segments;
|
||||
}
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const segments = processTranslation(translation, interpolations);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -28,4 +28,4 @@ export const ROUTES = {
|
||||
CHANGE_PASSPHRASE: "/dashboard/account/change-passphrase",
|
||||
},
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
cancelUrl: string;
|
||||
} = $props();
|
||||
|
||||
setLocale(locale);
|
||||
$effect(() => {
|
||||
setLocale(locale);
|
||||
});
|
||||
</script>
|
||||
|
||||
<EmailLayout>
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
cancelUrl: string;
|
||||
} = $props();
|
||||
|
||||
setLocale(locale);
|
||||
$effect(() => {
|
||||
setLocale(locale);
|
||||
});
|
||||
</script>
|
||||
|
||||
<EmailLayout>
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
};
|
||||
} = $props();
|
||||
|
||||
setLocale(locale);
|
||||
$effect(() => {
|
||||
setLocale(locale);
|
||||
});
|
||||
</script>
|
||||
|
||||
<EmailLayout>
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
expirationMinutes: number;
|
||||
} = $props();
|
||||
|
||||
setLocale(locale);
|
||||
$effect(() => {
|
||||
setLocale(locale);
|
||||
});
|
||||
</script>
|
||||
|
||||
<EmailLayout>
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
loginUrl: string;
|
||||
} = $props();
|
||||
|
||||
setLocale(locale);
|
||||
$effect(() => {
|
||||
setLocale(locale);
|
||||
});
|
||||
</script>
|
||||
|
||||
<EmailLayout>
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
expirationMinutes: number;
|
||||
} = $props();
|
||||
|
||||
setLocale(locale);
|
||||
$effect(() => {
|
||||
setLocale(locale);
|
||||
});
|
||||
</script>
|
||||
|
||||
<EmailLayout>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
</script>
|
||||
|
||||
<div class="button-container">
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||
<a class="button" {href}>{@render children?.()}</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export interface ClientTunnelData {
|
||||
appointmentDate: string;
|
||||
duration: number;
|
||||
emailHash: string;
|
||||
clientEmail: string;
|
||||
clientEmail?: string;
|
||||
clientLanguage?: string;
|
||||
clientPublicKey: string;
|
||||
privateKeyShare: string;
|
||||
@@ -543,19 +543,21 @@ export class AppointmentService {
|
||||
});
|
||||
|
||||
// Send email notification to client (async, don't wait)
|
||||
this.sendAppointmentNotification(
|
||||
result.appointment.id,
|
||||
clientData.channelId,
|
||||
clientData.clientEmail,
|
||||
clientData.clientLanguage || "de",
|
||||
result.requiresConfirmation,
|
||||
).catch((error) => {
|
||||
log.error("Failed to send appointment notification", {
|
||||
tunnelId: clientData.tunnelId,
|
||||
appointmentId: result.appointment.id,
|
||||
error: String(error),
|
||||
if (clientData.clientEmail) {
|
||||
this.sendAppointmentNotification(
|
||||
result.appointment.id,
|
||||
clientData.channelId,
|
||||
clientData.clientEmail,
|
||||
clientData.clientLanguage || "de",
|
||||
result.requiresConfirmation,
|
||||
).catch((error) => {
|
||||
log.error("Failed to send appointment notification", {
|
||||
tunnelId: clientData.tunnelId,
|
||||
appointmentId: result.appointment.id,
|
||||
error: String(error),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { goto } from "$app/navigation";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { agents } from "./agents";
|
||||
import { channels } from "./channels";
|
||||
import { resolve } from "$app/paths";
|
||||
|
||||
const log = logger.setContext("TenantsStore");
|
||||
|
||||
@@ -54,7 +55,7 @@ const createTenantsStore = () => {
|
||||
|
||||
// Redirect to dashboard main if tenant changed to avaoid showing data from previous tenant
|
||||
if (tenantId !== curTenant) {
|
||||
goto(ROUTES.DASHBOARD.MAIN);
|
||||
goto(resolve(ROUTES.DASHBOARD.MAIN));
|
||||
}
|
||||
},
|
||||
reload: async () => {
|
||||
@@ -81,7 +82,7 @@ const createTenantsStore = () => {
|
||||
description: m["dashboard.onboarding.notification.ongoing.description"](),
|
||||
action: {
|
||||
label: m["dashboard.onboarding.notification.ongoing.action"](),
|
||||
onClick: () => goto(ROUTES.DASHBOARD.MAIN),
|
||||
onClick: () => goto(resolve(ROUTES.DASHBOARD.MAIN)),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -92,7 +93,7 @@ const createTenantsStore = () => {
|
||||
description: m["dashboard.onboarding.notification.done.description"](),
|
||||
action: {
|
||||
label: m["dashboard.onboarding.notification.done.action"](),
|
||||
onClick: () => goto(ROUTES.MAIN),
|
||||
onClick: () => goto(resolve(ROUTES.MAIN)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export type TPublicAppointment = {
|
||||
};
|
||||
data?: {
|
||||
name: string;
|
||||
shareEmail: boolean;
|
||||
email: string;
|
||||
phone?: string;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
|
||||
@@ -19,7 +20,7 @@ export const refreshSession = async () => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
auth.setRefreshing(false);
|
||||
goto(ROUTES.LOGOUT);
|
||||
goto(resolve(ROUTES.LOGOUT));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -42,7 +43,7 @@ export const refreshUserData = async () => {
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
goto(ROUTES.LOGOUT);
|
||||
goto(resolve(ROUTES.LOGOUT));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
+33
-15
@@ -1,16 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { m } from "$i18n/messages.js";
|
||||
import * as Alert from "$lib/components/ui/alert";
|
||||
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 { Text } from "$lib/components/ui/typography";
|
||||
import { publicStore } from "$lib/stores/public";
|
||||
import type { TPublicAppointment } from "$lib/types/public";
|
||||
import { Lock } from "@lucide/svelte";
|
||||
import { get } from "svelte/store";
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
import { zod4Client as zodClient } from "sveltekit-superforms/adapters";
|
||||
import { Lock } from "@lucide/svelte";
|
||||
import { publicStore } from "$lib/stores/public";
|
||||
import { createFormSchema } from "./schema";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
const { proceed }: { proceed: (a: Partial<TPublicAppointment>) => void } = $props();
|
||||
const tenant = $derived($publicStore.tenant);
|
||||
@@ -19,6 +20,7 @@
|
||||
const form = superForm(
|
||||
{
|
||||
name: "",
|
||||
shareEmail: false,
|
||||
email: "",
|
||||
phone: get(publicStore).tenant?.requirePhone ? "" : undefined,
|
||||
},
|
||||
@@ -36,6 +38,7 @@
|
||||
...appointment,
|
||||
data: {
|
||||
name: validation.data.name,
|
||||
shareEmail: validation.data.shareEmail,
|
||||
email: validation.data.email,
|
||||
phone: validation.data.phone,
|
||||
},
|
||||
@@ -74,18 +77,33 @@
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="email">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["form.email"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.email} type="email" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
<Form.Description class="mt-1">
|
||||
{m["public.steps.data.email.description"]()}
|
||||
</Form.Description>
|
||||
</Form.Field>
|
||||
<div>
|
||||
<Form.Field {form} name="email">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["form.email"]()}</Form.Label>
|
||||
<Input {...props} bind:value={$formData.email} type="email" />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="shareEmail">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<CheckboxWithLabel
|
||||
{...props}
|
||||
bind:value={$formData.shareEmail}
|
||||
label={m["public.steps.data.email.notifyMe"]()}
|
||||
tooltip={m["public.steps.data.email.tooltip"]()}
|
||||
onCheckedChange={(v) => {
|
||||
$formData.shareEmail = v;
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
</div>
|
||||
<Form.Field {form} name="phone">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { m } from "$i18n/messages";
|
||||
import { getLocale } from "$i18n/runtime";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
@@ -39,7 +40,7 @@
|
||||
)
|
||||
.then(() => {
|
||||
const isRequest = channel.requiresConfirmation;
|
||||
goto(ROUTES.APPOINTMENT_BOOKED, { state: { isRequest } });
|
||||
goto(resolve(ROUTES.APPOINTMENT_BOOKED), { state: { isRequest } });
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(m["public.steps.summary.error"]());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { browser } from "$app/environment";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { publicStore } from "$lib/stores/public";
|
||||
import type { TPublicAppointment, TPublicSchedule } from "$lib/types/public";
|
||||
@@ -74,7 +75,7 @@ export const proceed = (
|
||||
newAppointment: updatedAppointment,
|
||||
}));
|
||||
}
|
||||
goto(`${ROUTES.BOOK_APPOINTMENT}/${newAppointment.channel}`);
|
||||
goto(resolve(`${ROUTES.BOOK_APPOINTMENT}/${newAppointment.channel}`));
|
||||
return { ...newAppointment, step: "SELECT_AGENT" };
|
||||
}
|
||||
default:
|
||||
@@ -83,7 +84,7 @@ export const proceed = (
|
||||
};
|
||||
|
||||
export const resest = () => {
|
||||
goto(ROUTES.BOOK_APPOINTMENT, { invalidateAll: true });
|
||||
goto(resolve(ROUTES.BOOK_APPOINTMENT), { invalidateAll: true });
|
||||
};
|
||||
|
||||
export const fetchSchedule = async (opts: {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Check from "@lucide/svelte/icons/check";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
|
||||
const { data } = $props();
|
||||
</script>
|
||||
@@ -65,7 +66,7 @@
|
||||
size="lg"
|
||||
class="w-full"
|
||||
onclick={() =>
|
||||
goto(ROUTES.SETUP_PASSKEY, {
|
||||
goto(resolve(ROUTES.SETUP_PASSKEY), {
|
||||
state: {
|
||||
id: confirmation.id,
|
||||
email: confirmation.email,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
}: { formId: string; onEvent: EventReporter; data: { form: SuperValidated<Infer<FormSchema>> } } =
|
||||
$props();
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(data.form, {
|
||||
validators: zodClient(formSchema),
|
||||
onResult: async (event) => {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import { formSchema, type FormSchema } from "./schema";
|
||||
import { onMount } from "svelte";
|
||||
import { UnifiedAppointmentCrypto } from "$lib/client/appointment-crypto";
|
||||
import { resolve } from "$app/paths";
|
||||
|
||||
let {
|
||||
data,
|
||||
@@ -37,6 +38,7 @@
|
||||
let kyberKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array } | undefined = $state();
|
||||
let registrationChallenge: string | undefined = $state();
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(data.form, {
|
||||
validators: zodClient(formSchema),
|
||||
onChange: (event) => {
|
||||
@@ -48,7 +50,7 @@
|
||||
if (event.result.type === "success") {
|
||||
toast.success(m["setupPasskey.success"]());
|
||||
await storeStaffKeyPair();
|
||||
await goto(ROUTES.LOGIN);
|
||||
await goto(resolve(ROUTES.LOGIN));
|
||||
} else {
|
||||
toast.error(m["setupPasskey.error"]());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { m } from "$i18n/messages";
|
||||
import { SidebarLayout } from "$lib/components/layouts/sidebar-layout";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
@@ -72,7 +73,7 @@
|
||||
{
|
||||
label: m["dashboard.tenants.setup.sections.tenants.action"](),
|
||||
onClick: () => {
|
||||
goto(ROUTES.DASHBOARD.TENANTS, { state: { action: "add" } });
|
||||
goto(resolve(ROUTES.DASHBOARD.TENANTS), { state: { action: "add" } });
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -103,7 +104,7 @@
|
||||
{
|
||||
label: m["dashboard.onboarding.sections.settings.action"](),
|
||||
onClick: () => {
|
||||
goto(ROUTES.DASHBOARD.SETTINGS);
|
||||
goto(resolve(ROUTES.DASHBOARD.SETTINGS));
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -116,7 +117,7 @@
|
||||
{
|
||||
label: m["dashboard.onboarding.sections.agents.action"](),
|
||||
onClick: () => {
|
||||
goto(ROUTES.DASHBOARD.AGENTS, { state: { action: "add" } });
|
||||
goto(resolve(ROUTES.DASHBOARD.AGENTS), { state: { action: "add" } });
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -130,7 +131,7 @@
|
||||
{
|
||||
label: m["dashboard.onboarding.sections.channels.action"](),
|
||||
onClick: () => {
|
||||
goto(ROUTES.DASHBOARD.CHANNELS, { state: { action: "add" } });
|
||||
goto(resolve(ROUTES.DASHBOARD.CHANNELS), { state: { action: "add" } });
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -144,7 +145,7 @@
|
||||
{
|
||||
label: m["dashboard.onboarding.sections.staff.action"](),
|
||||
onClick: () => {
|
||||
goto(ROUTES.DASHBOARD.STAFF, { state: { action: "add" } });
|
||||
goto(resolve(ROUTES.DASHBOARD.STAFF), { state: { action: "add" } });
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
+1
@@ -14,6 +14,7 @@
|
||||
const agents = $derived($agentsStore.agents ?? []);
|
||||
let { entity, done }: { entity: TAbsence; done: () => void } = $props();
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(
|
||||
{ id: entity.id, agent: entity.agentId },
|
||||
{
|
||||
|
||||
+6
-2
@@ -18,6 +18,8 @@
|
||||
let { entity, done }: { entity: TAbsence; done: () => void } = $props();
|
||||
|
||||
const agents = $derived($agentsStore.agents ?? []);
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(
|
||||
{
|
||||
id: entity.id,
|
||||
@@ -43,8 +45,10 @@
|
||||
);
|
||||
|
||||
let isSubmitting = $state(false);
|
||||
let startDate = toInputDateTime(entity.startDate);
|
||||
let endDate = toInputDateTime(entity.endDate);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let startDate = $state(toInputDateTime(entity.startDate));
|
||||
// svelte-ignore state_referenced_locally
|
||||
let endDate = $state(toInputDateTime(entity.endDate));
|
||||
let isAllDay = $state(
|
||||
startDate.hour === 0 && startDate.minute === 0 && endDate.hour === 0 && endDate.minute === 0,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<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";
|
||||
@@ -22,7 +23,7 @@
|
||||
<Item.Group class="gap-2">
|
||||
<Item.Root variant="outline">
|
||||
{#snippet child({ props })}
|
||||
<a href={ROUTES.DASHBOARD.ACCOUNT.GENERAL} {...props}>
|
||||
<a href={resolve(ROUTES.DASHBOARD.ACCOUNT.GENERAL)} {...props}>
|
||||
<Item.Content>
|
||||
<Item.Title>
|
||||
<Text style="md">{m["account.general.title"]()}</Text>
|
||||
@@ -39,7 +40,7 @@
|
||||
</Item.Root>
|
||||
<Item.Root variant="outline">
|
||||
{#snippet child({ props })}
|
||||
<a href={ROUTES.DASHBOARD.ACCOUNT.CHANGE_EMAIL} {...props}>
|
||||
<a href={resolve(ROUTES.DASHBOARD.ACCOUNT.CHANGE_EMAIL)} {...props}>
|
||||
<Item.Content>
|
||||
<Item.Title>
|
||||
<Text style="md">{m["account.change-email.title"]()}</Text>
|
||||
@@ -56,7 +57,7 @@
|
||||
</Item.Root>
|
||||
<Item.Root variant="outline">
|
||||
{#snippet child({ props })}
|
||||
<a href={ROUTES.DASHBOARD.ACCOUNT.PASSKEYS} {...props}>
|
||||
<a href={resolve(ROUTES.DASHBOARD.ACCOUNT.PASSKEYS)} {...props}>
|
||||
<Item.Content>
|
||||
<Item.Title>
|
||||
<Text style="md">{m["account.passkeys.title"]()}</Text>
|
||||
@@ -74,7 +75,7 @@
|
||||
{#if $auth.user?.role === "GLOBAL_ADMIN"}
|
||||
<Item.Root variant="outline">
|
||||
{#snippet child({ props })}
|
||||
<a href={ROUTES.DASHBOARD.ACCOUNT.CHANGE_PASSPHRASE} {...props}>
|
||||
<a href={resolve(ROUTES.DASHBOARD.ACCOUNT.CHANGE_PASSPHRASE)} {...props}>
|
||||
<Item.Content>
|
||||
<Item.Title>
|
||||
<Text style="md">{m["account.change-passphrase.title"]()}</Text>
|
||||
|
||||
+1
@@ -14,6 +14,7 @@
|
||||
|
||||
let { entity, done }: { entity: TAgent; done: () => void } = $props();
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(
|
||||
{ id: entity.id, name: "" },
|
||||
{
|
||||
|
||||
+4
-2
@@ -17,8 +17,10 @@
|
||||
let { entity, done }: { entity: TAgent; done: () => void } = $props();
|
||||
|
||||
const tenantLocales = get(tenants).currentTenant?.languages ?? [];
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(
|
||||
{
|
||||
$state.snapshot({
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
descriptions: tenantLocales.reduce(
|
||||
@@ -26,7 +28,7 @@
|
||||
{} as { [key: string]: string },
|
||||
),
|
||||
image: entity.image ?? "",
|
||||
},
|
||||
}),
|
||||
{
|
||||
dataType: "json",
|
||||
validators: zodClient(formSchema),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { browser } from "$app/environment";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import type { TCalendar, TCalendarItem } from "$lib/types/calendar";
|
||||
import { toCalendarDateTime, toZoned, type CalendarDate } from "@internationalized/date";
|
||||
@@ -32,7 +33,7 @@ export const fetchCalendar = async (opts: { tenant: string; startDate: CalendarD
|
||||
}
|
||||
} else {
|
||||
if (res.status === 401) {
|
||||
goto(ROUTES.LOGIN);
|
||||
goto(resolve(ROUTES.LOGIN));
|
||||
} else {
|
||||
console.error("Unable to fetch calendar", res.status, res.statusText);
|
||||
}
|
||||
|
||||
+1
@@ -15,6 +15,7 @@
|
||||
|
||||
let { entity, done }: { entity: TChannelWithFullAgents; done: () => void } = $props();
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(
|
||||
{ id: entity.id, name: "" },
|
||||
{
|
||||
|
||||
+2
@@ -23,6 +23,8 @@
|
||||
|
||||
const agents = $derived($agentsStore.agents ?? []);
|
||||
const tenantLocales = get(tenants).currentTenant?.languages ?? [];
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(
|
||||
{
|
||||
id: entity.id,
|
||||
|
||||
+1
@@ -14,6 +14,7 @@
|
||||
|
||||
let { entity, done }: { entity: TChannelWithFullAgents; done: () => void } = $props();
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(
|
||||
{ id: entity.id, pause: !entity.pause },
|
||||
{
|
||||
|
||||
+2
@@ -30,6 +30,8 @@
|
||||
key: it,
|
||||
label: translatedLocales[it as keyof typeof translatedLocales],
|
||||
}));
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(
|
||||
{
|
||||
id: entity.id,
|
||||
|
||||
+1
@@ -14,6 +14,7 @@
|
||||
|
||||
let { entity, done }: { entity: TStaff; done: () => void } = $props();
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(
|
||||
{ id: entity.id, email: "", confirmationState: entity.confirmationState },
|
||||
{
|
||||
|
||||
+2
@@ -14,6 +14,8 @@
|
||||
let { entity, done }: { entity: TStaff; done: () => void } = $props();
|
||||
|
||||
const availableRoles = $derived(roles.filter((it) => it.value !== "GLOBAL_ADMIN"));
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(
|
||||
{
|
||||
id: entity.id,
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@
|
||||
let { data, done }: { done: () => void; data: { form: SuperValidated<Infer<FormSchema>> } } =
|
||||
$props();
|
||||
|
||||
const form = superForm(data.form, {
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm($state.snapshot(data.form), {
|
||||
validators: zodClient(formSchema),
|
||||
onResult: async (event) => {
|
||||
if (event.result.type === "success") {
|
||||
|
||||
+1
@@ -17,6 +17,7 @@
|
||||
|
||||
let { entity, done }: { entity: TTenant; done: () => void } = $props();
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(
|
||||
{ id: entity.id, shortname: "" },
|
||||
{
|
||||
|
||||
+1
@@ -10,6 +10,7 @@
|
||||
|
||||
let { entity, done }: { entity: TTenant; done: () => void } = $props();
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm(
|
||||
{ id: entity.id, shortName: entity.shortName },
|
||||
{
|
||||
|
||||
@@ -5,20 +5,21 @@
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import type { EventReporter } from "$lib/components/ui/form/form-root.svelte";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Passkey } from "$lib/components/ui/passkey";
|
||||
import type { PasskeyState } from "$lib/components/ui/passkey/state.svelte";
|
||||
import { Text } from "$lib/components/ui/typography";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import logger from "$lib/logger";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { arrayBufferToBase64, fetchChallenge, getCredential } from "$lib/utils/passkey";
|
||||
import { onMount } from "svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
import { zod4Client as zodClient } from "sveltekit-superforms/adapters";
|
||||
import { baseSchema, formSchema } from "./schema";
|
||||
import { onMount } from "svelte";
|
||||
import { Passkey } from "$lib/components/ui/passkey";
|
||||
import { Text } from "$lib/components/ui/typography";
|
||||
import type { PasskeyState } from "$lib/components/ui/passkey/state.svelte";
|
||||
import { arrayBufferToBase64, fetchChallenge, getCredential } from "$lib/utils/passkey";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import logger from "$lib/logger";
|
||||
import { resolve } from "$app/paths";
|
||||
|
||||
let { formId, onEvent }: { formId: string; onEvent: EventReporter } = $props();
|
||||
|
||||
@@ -42,7 +43,7 @@
|
||||
onResult: async (event) => {
|
||||
if (event.result.type === "success") {
|
||||
auth.setUser(event.result.data?.user);
|
||||
await goto(ROUTES.DASHBOARD.MAIN);
|
||||
await goto(resolve(ROUTES.DASHBOARD.MAIN));
|
||||
} else {
|
||||
toast.error(m["login.error"]());
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export const load = async (event) => {
|
||||
|
||||
// Check if global admin exists
|
||||
const adminExists = await UserService.adminExists();
|
||||
// @ts-expect-error any string should be tested against blocklist
|
||||
if (blocklist.includes(cleanedId) && adminExists) {
|
||||
redirect(302, ROUTES.LOGIN);
|
||||
}
|
||||
|
||||
@@ -14,13 +14,16 @@
|
||||
import { goto } from "$app/navigation";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { resolve } from "$app/paths";
|
||||
|
||||
let { data }: { data: { form: SuperValidated<Infer<FormSchema>> } } = $props();
|
||||
|
||||
const formId = "resend-confirmation-email";
|
||||
let email = $state("");
|
||||
let isSubmitting = $state(false);
|
||||
const form = superForm(data.form, {
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm($state.snapshot(data.form), {
|
||||
resetForm: false,
|
||||
validators: zodClient(formSchema),
|
||||
onResult: (event) => {
|
||||
@@ -41,7 +44,7 @@
|
||||
|
||||
// This page is only of help, if you have just created an admin account
|
||||
if (!email) {
|
||||
await goto(ROUTES.SETUP.CREATE_ADMIN_ACCOUNT);
|
||||
await goto(resolve(ROUTES.SETUP.CREATE_ADMIN_ACCOUNT));
|
||||
}
|
||||
|
||||
// Set email from previous step
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import type { PasskeyState } from "$lib/components/ui/passkey/state.svelte";
|
||||
import { arrayBufferToBase64, fetchChallenge, generatePasskey } from "$lib/utils/passkey";
|
||||
import logger from "$lib/logger";
|
||||
import { resolve } from "$app/paths";
|
||||
|
||||
let {
|
||||
data,
|
||||
@@ -28,7 +29,8 @@
|
||||
}: { formId: string; onEvent: EventReporter; data: { form: SuperValidated<Infer<FormSchema>> } } =
|
||||
$props();
|
||||
|
||||
const form = superForm(data.form, {
|
||||
// svelte-ignore state_referenced_locally
|
||||
const form = superForm($state.snapshot(data.form), {
|
||||
validators: zodClient(formSchema),
|
||||
onChange: (event) => {
|
||||
if (event.paths.includes("email")) {
|
||||
@@ -38,7 +40,7 @@
|
||||
onResult: async (event) => {
|
||||
if (event.result.type === "success") {
|
||||
toast.success(m["setup.create_admin_account.success"]());
|
||||
await goto(ROUTES.SETUP.CHECK_EMAIL, {
|
||||
await goto(resolve(ROUTES.SETUP.CHECK_EMAIL), {
|
||||
state: { email: event.result.data?.form.data.email },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import { setLocale } from "$i18n/runtime";
|
||||
import { Toaster } from "$lib/components/ui/sonner/index.js";
|
||||
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||
import { ModeWatcher, setMode } from "mode-watcher";
|
||||
import "../app.css";
|
||||
import { setLocale } from "$i18n/runtime";
|
||||
|
||||
let { data, children } = $props();
|
||||
|
||||
@@ -27,4 +28,6 @@
|
||||
|
||||
<ModeWatcher track={false} />
|
||||
<Toaster richColors />
|
||||
{@render children()}
|
||||
<Tooltip.Provider delayDuration={0}>
|
||||
{@render children()}
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -146,6 +146,8 @@ describe("POST /api/admin/tenant", () => {
|
||||
isSubRequest: false,
|
||||
fetch: fetch,
|
||||
setHeaders: vi.fn(),
|
||||
tracing: { enabled: false, root: "", current: "" },
|
||||
isRemoteRequest: false,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import logger from "$lib/logger";
|
||||
import { checkPermission } from "$lib/server/utils/permissions";
|
||||
|
||||
const requestSchema = z.object({
|
||||
clientEmail: z.string().email().optional(),
|
||||
clientEmail: z.email().optional(),
|
||||
clientLanguage: z.string().optional().default("en"),
|
||||
});
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const requestSchema = z.object({
|
||||
agentId: z.string(),
|
||||
appointmentDate: z.string(),
|
||||
duration: z.number().int().positive(),
|
||||
clientEmail: z.string().email(),
|
||||
clientEmail: z.email().optional(),
|
||||
clientLanguage: z.string().optional().default("en"),
|
||||
encryptedAppointment: z.object({
|
||||
encryptedPayload: z.string(),
|
||||
@@ -121,7 +121,6 @@ registerOpenAPIRoute("/tenants/{id}/appointments/add-to-tunnel", "POST", {
|
||||
"tunnelId",
|
||||
"channelId",
|
||||
"appointmentDate",
|
||||
"clientEmail",
|
||||
"encryptedAppointment",
|
||||
],
|
||||
},
|
||||
@@ -304,12 +303,6 @@ export const POST: RequestHandler = async ({ request, params }) => {
|
||||
if (!tenant) {
|
||||
logger.warn("Cannot send email: Tenant not found", { tenantId });
|
||||
} else {
|
||||
// Create client data object from request
|
||||
const clientData = {
|
||||
email: validatedData.clientEmail,
|
||||
language: validatedData.clientLanguage,
|
||||
};
|
||||
|
||||
// Get channel title for the email
|
||||
const channelTitle = await getChannelTitle(
|
||||
tenantId,
|
||||
@@ -318,20 +311,28 @@ export const POST: RequestHandler = async ({ request, params }) => {
|
||||
);
|
||||
|
||||
// Send appropriate email based on whether confirmation is required
|
||||
if (requiresConfirmation) {
|
||||
await sendAppointmentRequestEmail(clientData, tenant, result, channelTitle);
|
||||
logger.info("Appointment request email sent", {
|
||||
tunnelId: validatedData.tunnelId,
|
||||
appointmentId: result.id,
|
||||
tenantId,
|
||||
});
|
||||
} else {
|
||||
await sendAppointmentCreatedEmail(clientData, tenant, result, channelTitle);
|
||||
logger.info("Appointment confirmation email sent", {
|
||||
tunnelId: validatedData.tunnelId,
|
||||
appointmentId: result.id,
|
||||
tenantId,
|
||||
});
|
||||
if (validatedData.clientEmail) {
|
||||
// Create client data object from request
|
||||
const clientData = {
|
||||
email: validatedData.clientEmail,
|
||||
language: validatedData.clientLanguage,
|
||||
};
|
||||
|
||||
if (requiresConfirmation) {
|
||||
await sendAppointmentRequestEmail(clientData, tenant, result, channelTitle);
|
||||
logger.info("Appointment request email sent", {
|
||||
tunnelId: validatedData.tunnelId,
|
||||
appointmentId: result.id,
|
||||
tenantId,
|
||||
});
|
||||
} else {
|
||||
await sendAppointmentCreatedEmail(clientData, tenant, result, channelTitle);
|
||||
logger.info("Appointment confirmation email sent", {
|
||||
tunnelId: validatedData.tunnelId,
|
||||
appointmentId: result.id,
|
||||
tenantId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (emailError) {
|
||||
|
||||
@@ -12,7 +12,7 @@ const requestSchema = z.object({
|
||||
appointmentDate: z.string(),
|
||||
duration: z.number().int().positive(),
|
||||
emailHash: z.string(),
|
||||
clientEmail: z.string().email(),
|
||||
clientEmail: z.email().optional(),
|
||||
clientLanguage: z.string().optional().default("en"),
|
||||
clientPublicKey: z.string(),
|
||||
privateKeyShare: z.string(),
|
||||
@@ -74,7 +74,7 @@ registerOpenAPIRoute("/tenants/{id}/appointments/create-new-client", "POST", {
|
||||
clientEmail: {
|
||||
type: "string",
|
||||
format: "email",
|
||||
description: "Client email address for sending confirmation",
|
||||
description: "Client email address for sending confirmation. Optional.",
|
||||
example: "client@example.com",
|
||||
},
|
||||
clientLanguage: {
|
||||
@@ -138,7 +138,6 @@ registerOpenAPIRoute("/tenants/{id}/appointments/create-new-client", "POST", {
|
||||
"channelId",
|
||||
"appointmentDate",
|
||||
"emailHash",
|
||||
"clientEmail",
|
||||
"clientPublicKey",
|
||||
"privateKeyShare",
|
||||
"encryptedAppointment",
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { RequestEvent } from "@sveltejs/kit";
|
||||
|
||||
const ACCESS_TOKEN_COOKIE_NAME = "access_token";
|
||||
export const getAccessToken = (
|
||||
// @ts-expect-error exact route names are not relevant here
|
||||
event: RequestEvent<Partial<Record<string, string>>, string | null>,
|
||||
): string | null => {
|
||||
let accessToken: string | null = null;
|
||||
|
||||
Reference in New Issue
Block a user