@shopbite-de/storefront 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -268,6 +268,24 @@ export type Schemas = {
268
268
  type: "page" | "link" | "folder";
269
269
  };
270
270
  BreadcrumbCollection: components["schemas"]["Breadcrumb"][];
271
+ BusinessHour: {
272
+ apiAlias?: string;
273
+ closingTime?: string;
274
+ /** Format: date-time */
275
+ createdAt?: string;
276
+ dayOfWeek?: number;
277
+ /** Format: uuid */
278
+ id?: string;
279
+ openingTime?: string;
280
+ /** Format: uuid */
281
+ salesChannelId?: string;
282
+ translated: [];
283
+ /** Format: date-time */
284
+ updatedAt?: string | null;
285
+ };
286
+ BusinessHourStruct: {
287
+ businessHours?: components["schemas"]["BusinessHour"][];
288
+ };
271
289
  CalculatedPrice: {
272
290
  /** @enum {string} */
273
291
  apiAlias: "calculated_price";
@@ -1519,6 +1537,25 @@ export type Schemas = {
1519
1537
  /** Format: date-time */
1520
1538
  readonly updatedAt?: string;
1521
1539
  };
1540
+ Holiday: {
1541
+ apiAlias?: string;
1542
+ /** Format: date-time */
1543
+ createdAt?: string;
1544
+ /** Format: date-time */
1545
+ end?: string;
1546
+ /** Format: uuid */
1547
+ id?: string;
1548
+ /** Format: uuid */
1549
+ salesChannelId?: string;
1550
+ /** Format: date-time */
1551
+ start?: string;
1552
+ translated: [];
1553
+ /** Format: date-time */
1554
+ updatedAt?: string | null;
1555
+ };
1556
+ HolidayStruct: {
1557
+ holidays?: components["schemas"]["Holiday"][];
1558
+ };
1522
1559
  ImportExportFile: {
1523
1560
  /** Format: date-time */
1524
1561
  readonly createdAt?: string;
@@ -4546,6 +4583,30 @@ export type Schemas = {
4546
4583
  /** Whether checkout is enabled for ShopBite */
4547
4584
  isCheckoutEnabled: boolean;
4548
4585
  };
4586
+ ShopbiteBusinessHour: {
4587
+ closingTime: string;
4588
+ /** Format: date-time */
4589
+ readonly createdAt?: string;
4590
+ /** Format: int64 */
4591
+ dayOfWeek: number;
4592
+ id: string;
4593
+ openingTime: string;
4594
+ salesChannelId: string;
4595
+ /** Format: date-time */
4596
+ readonly updatedAt?: string;
4597
+ };
4598
+ ShopbiteHoliday: {
4599
+ /** Format: date-time */
4600
+ readonly createdAt?: string;
4601
+ /** Format: date-time */
4602
+ end: string;
4603
+ id: string;
4604
+ salesChannelId: string;
4605
+ /** Format: date-time */
4606
+ start: string;
4607
+ /** Format: date-time */
4608
+ readonly updatedAt?: string;
4609
+ };
4549
4610
  SimpleFilter: {
4550
4611
  field: string;
4551
4612
  /** @enum {string} */
@@ -7004,12 +7065,24 @@ export type operations = {
7004
7065
  } & components["schemas"]["EntitySearchResult"];
7005
7066
  responseCode: 200;
7006
7067
  };
7068
+ "shopbite.business-hour.get get /shopbite/business-hour": {
7069
+ contentType?: "application/json";
7070
+ accept?: "application/json";
7071
+ response: components["schemas"]["BusinessHourStruct"];
7072
+ responseCode: 200;
7073
+ };
7007
7074
  "shopbite.config.get get /shopbite/config": {
7008
7075
  contentType?: "application/json";
7009
7076
  accept?: "application/json";
7010
7077
  response: components["schemas"]["ShopBiteConfig"];
7011
7078
  responseCode: 200;
7012
7079
  };
7080
+ "shopbite.holiday.get get /shopbite/holiday": {
7081
+ contentType?: "application/json";
7082
+ accept?: "application/json";
7083
+ response: components["schemas"]["HolidayStruct"];
7084
+ responseCode: 200;
7085
+ };
7013
7086
  "readSitemap get /sitemap": {
7014
7087
  contentType?: "application/json";
7015
7088
  accept?: "application/json";
package/app/app.vue CHANGED
@@ -17,6 +17,15 @@ Sentry.init({
17
17
  dsn: runtimeConfig.public.sentry.dsn,
18
18
  });
19
19
 
20
+ const {
21
+ getNextOpeningTime,
22
+ isStoreOpen,
23
+ refresh: refreshBusinessHours,
24
+ } = useBusinessHours();
25
+ const { isClosedHoliday, refresh: refreshHolidays } = useHolidays();
26
+
27
+ await Promise.all([refreshBusinessHours(), refreshHolidays()]);
28
+
20
29
  // Initialize session context
21
30
  const { data: sessionContextData } = await useAsyncData(
22
31
  "session-context",
@@ -59,12 +68,12 @@ const TOAST_CONFIG = {
59
68
  } as const;
60
69
 
61
70
  function displayStoreStatus() {
62
- const isOpen = isStoreOpen();
71
+ const isOpen = isStoreOpen(undefined, isClosedHoliday);
63
72
 
64
73
  if (isOpen) {
65
74
  toast.add(TOAST_CONFIG.open);
66
75
  } else {
67
- const nextOpening = getNextOpeningTime(ref(new Date()));
76
+ const nextOpening = getNextOpeningTime(ref(new Date()), isClosedHoliday);
68
77
  toast.add({
69
78
  ...TOAST_CONFIG.closed,
70
79
  description: nextOpening
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, watch, onMounted, onUnmounted, computed } from "vue";
3
3
  import { useDeliveryTime } from "~/composables/useDeliveryTime";
4
- import { isClosedHoliday } from "~/utils/holidays";
4
+ import { useHolidays } from "~/composables/useHolidays";
5
5
 
6
6
  const props = defineProps<{
7
7
  modelValue?: string;
@@ -44,13 +44,15 @@ const {
44
44
  validate,
45
45
  } = useDeliveryTime(now);
46
46
 
47
+ const { isClosedHoliday } = useHolidays();
48
+
47
49
  const validationError = computed<string | null>(() =>
48
50
  selected.value ? validate(selected.value) : null,
49
51
  );
50
52
 
51
53
  const isValid = computed<boolean>(() => {
52
54
  if (!selected.value) return false;
53
- if (isClosedHoliday(now.value)) return false;
55
+ if (isClosedHoliday(now.value) === true) return false;
54
56
  return validate(selected.value) === null;
55
57
  });
56
58
 
@@ -130,7 +132,7 @@ function handleTimeInput(event: Event): void {
130
132
  </script>
131
133
 
132
134
  <template>
133
- <div v-if="!isClosedHoliday(now)" class="flex flex-col gap-2 mt-4">
135
+ <div v-if="isClosedHoliday(now) === false" class="flex flex-col gap-2 mt-4">
134
136
  <div class="flex flex-row items-center justify-between gap-4">
135
137
  <div>Wunschlieferung- oder Abholzeit ab:</div>
136
138
  <client-only>
@@ -138,7 +140,7 @@ function handleTimeInput(event: Event): void {
138
140
  type="time"
139
141
  :min="minTime ?? undefined"
140
142
  :max="maxTime ?? undefined"
141
- :disabled="isClosedToday || isClosedHoliday(now)"
143
+ :disabled="isClosedToday || isClosedHoliday(now) === true"
142
144
  :value="selected"
143
145
  step="300"
144
146
  class="border rounded px-2 py-1"
@@ -165,7 +167,7 @@ function handleTimeInput(event: Event): void {
165
167
  variant="subtle"
166
168
  />
167
169
  </div>
168
- <div v-else>
170
+ <div v-else-if="isClosedHoliday(now) === true">
169
171
  <UBadge
170
172
  variant="subtle"
171
173
  class="w-full"
@@ -174,4 +176,13 @@ function handleTimeInput(event: Event): void {
174
176
  label="Geschlossen wegen Betriebsferien"
175
177
  />
176
178
  </div>
179
+ <div v-else>
180
+ <UBadge
181
+ variant="subtle"
182
+ class="w-full"
183
+ icon="i-lucide-loader"
184
+ color="neutral"
185
+ label="Lade Öffnungszeiten..."
186
+ />
187
+ </div>
177
188
  </template>
@@ -0,0 +1,156 @@
1
+ import { useShopwareContext } from "#imports";
2
+ import type { Ref } from "vue";
3
+ import { setTime } from "~/utils/time";
4
+
5
+ export type ServiceInterval = { start: Date; end: Date };
6
+
7
+ export function useBusinessHours() {
8
+ const { apiClient } = useShopwareContext();
9
+
10
+ const { data, pending, refresh } = useAsyncData(
11
+ "business-hours",
12
+ async () => {
13
+ const response = await apiClient.invoke(
14
+ "shopbite.business-hour.get get /shopbite/business-hour",
15
+ );
16
+
17
+ return response.data.businessHours;
18
+ },
19
+ { immediate: false },
20
+ );
21
+
22
+ const getServiceIntervals = (date: Date): Array<ServiceInterval> => {
23
+ if (!data.value) return [];
24
+
25
+ const dayOfWeek = date.getDay();
26
+ const dayBusinessHours = data.value.filter(
27
+ (bh) => bh.dayOfWeek === dayOfWeek,
28
+ );
29
+
30
+ return dayBusinessHours
31
+ .map((bh) => {
32
+ if (!bh.openingTime || !bh.closingTime) return null;
33
+ const [startH, startM] = bh.openingTime.split(":").map(Number);
34
+ const [endH, endM] = bh.closingTime.split(":").map(Number);
35
+
36
+ return {
37
+ start: setTime(date, startH, startM),
38
+ end: setTime(date, endH, endM),
39
+ };
40
+ })
41
+ .filter((interval): interval is ServiceInterval => interval !== null);
42
+ };
43
+
44
+ const getEarliestSelectableTime = (
45
+ currentTime: Date,
46
+ currentDeliveryTime: number | null,
47
+ ): Date => {
48
+ const earliest = new Date(currentTime);
49
+ earliest.setMinutes(earliest.getMinutes() + (currentDeliveryTime ?? 30));
50
+ earliest.setSeconds(0, 0);
51
+ return earliest;
52
+ };
53
+
54
+ const findActiveInterval = (
55
+ currentTime: Date,
56
+ currentDeliveryTime: number | null,
57
+ ): ServiceInterval | null => {
58
+ const intervals = getServiceIntervals(currentTime);
59
+ const earliest = getEarliestSelectableTime(
60
+ currentTime,
61
+ currentDeliveryTime ?? 30,
62
+ );
63
+
64
+ if (intervals.length === 0) return null;
65
+
66
+ const current = intervals.find(
67
+ (interval) => earliest >= interval.start && earliest <= interval.end,
68
+ );
69
+ if (current) return current;
70
+
71
+ return intervals.find((interval) => interval.start > earliest) ?? null;
72
+ };
73
+
74
+ const getNextOpeningTime = (
75
+ now: Ref<Date>,
76
+ isClosedHoliday: (date: Date) => boolean | undefined,
77
+ ): string | null => {
78
+ const currentDate = now.value;
79
+ const isHoliday = isClosedHoliday(currentDate);
80
+
81
+ // If we don't know if today is a holiday, we can't reliably say we are open/closed
82
+ if (isHoliday === undefined) return null;
83
+
84
+ // Try up to 60 days ahead to find next opening (covers long holiday periods)
85
+ for (let i = 0; i < 60; i++) {
86
+ const checkDate = new Date(currentDate);
87
+ checkDate.setDate(checkDate.getDate() + i);
88
+ checkDate.setHours(12, 0, 0, 0); // Set to midday to avoid timezone issues
89
+
90
+ // Skip holidays
91
+ if (isClosedHoliday(checkDate) === true) continue;
92
+
93
+ const intervals = getServiceIntervals(checkDate);
94
+ if (intervals.length === 0) continue;
95
+
96
+ // For today, check if there's still an opening coming
97
+ if (i === 0) {
98
+ for (const interval of intervals) {
99
+ if (interval.start.getTime() > currentDate.getTime()) {
100
+ const hours = interval.start.getHours().toString().padStart(2, "0");
101
+ const minutes = interval.start
102
+ .getMinutes()
103
+ .toString()
104
+ .padStart(2, "0");
105
+ return `${hours}:${minutes} Uhr`;
106
+ }
107
+ }
108
+ continue; // Today's openings have passed, check next days
109
+ }
110
+
111
+ const nextOpen = intervals[0].start;
112
+ const day = nextOpen.getDate().toString().padStart(2, "0");
113
+ const month = (nextOpen.getMonth() + 1).toString().padStart(2, "0");
114
+ const dayName = [
115
+ "Sonntag",
116
+ "Montag",
117
+ "Dienstag",
118
+ "Mittwoch",
119
+ "Donnerstag",
120
+ "Freitag",
121
+ "Samstag",
122
+ ][nextOpen.getDay()];
123
+ const hours = nextOpen.getHours().toString().padStart(2, "0");
124
+ const minutes = nextOpen.getMinutes().toString().padStart(2, "0");
125
+
126
+ if (i === 1) {
127
+ return `morgen um ${hours}:${minutes} Uhr`;
128
+ }
129
+ return `${dayName}, ${day}.${month}. um ${hours}:${minutes} Uhr`;
130
+ }
131
+
132
+ return null;
133
+ };
134
+
135
+ const isStoreOpen = (
136
+ date: Date = new Date(),
137
+ isClosedHoliday: (date: Date) => boolean | undefined,
138
+ ): boolean => {
139
+ if (isClosedHoliday(date) === true) return false;
140
+
141
+ const intervals = getServiceIntervals(date);
142
+ if (intervals.length === 0) return false;
143
+ return intervals.some(({ start, end }) => date >= start && date <= end);
144
+ };
145
+
146
+ return {
147
+ businessHours: data,
148
+ isLoading: pending,
149
+ refresh,
150
+ isStoreOpen,
151
+ getServiceIntervals,
152
+ getEarliestSelectableTime,
153
+ findActiveInterval,
154
+ getNextOpeningTime,
155
+ };
156
+ }
@@ -1,12 +1,4 @@
1
1
  import { computed, type Ref } from "vue";
2
- import { toTimeString, parseTimeString } from "~/utils/time";
3
- import {
4
- getEarliestSelectableTime,
5
- findActiveInterval,
6
- getServiceIntervals,
7
- isTuesday,
8
- type ServiceInterval,
9
- } from "~/utils/businessHours";
10
2
 
11
3
  function isTimeWithinBounds(
12
4
  time: string,
@@ -19,6 +11,14 @@ function isTimeWithinBounds(
19
11
 
20
12
  export function useDeliveryTime(now: Ref<Date>) {
21
13
  const { deliveryTime } = useShopBiteConfig();
14
+ const {
15
+ getServiceIntervals,
16
+ getEarliestSelectableTime,
17
+ findActiveInterval,
18
+ businessHours,
19
+ } = useBusinessHours();
20
+ const { isClosedHoliday } = useHolidays();
21
+
22
22
  const earliest = computed<Date>(() =>
23
23
  getEarliestSelectableTime(now.value, deliveryTime.value),
24
24
  );
@@ -28,6 +28,7 @@ export function useDeliveryTime(now: Ref<Date>) {
28
28
  );
29
29
 
30
30
  const minTime = computed<string | null>(() => {
31
+ if (isClosedHoliday(now.value) !== false) return null;
31
32
  const interval = activeInterval.value;
32
33
  if (!interval) return null;
33
34
  const minDate = new Date(
@@ -37,6 +38,7 @@ export function useDeliveryTime(now: Ref<Date>) {
37
38
  });
38
39
 
39
40
  const maxTime = computed<string | null>(() => {
41
+ if (isClosedHoliday(now.value) !== false) return null;
40
42
  const interval = activeInterval.value;
41
43
  return interval ? toTimeString(interval.end) : null;
42
44
  });
@@ -44,11 +46,33 @@ export function useDeliveryTime(now: Ref<Date>) {
44
46
  const isClosedToday = computed<boolean>(() => activeInterval.value === null);
45
47
 
46
48
  const helperText = computed<string>(() => {
49
+ const isHoliday = isClosedHoliday(now.value);
50
+ if (isHoliday === undefined) {
51
+ return "Lade Informationen...";
52
+ }
53
+ if (isHoliday) {
54
+ return "Wegen Betriebsferien geschlossen.";
55
+ }
56
+
47
57
  const interval = activeInterval.value;
48
58
  if (!interval) {
49
- if (isTuesday(now.value)) {
50
- return "Heute (Dienstag) ist Ruhetag. Bitte an einem anderen Tag bestellen.";
59
+ const dayOfWeek = now.value.getDay();
60
+ const hasOpeningsToday =
61
+ businessHours.value?.some((bh) => bh.dayOfWeek === dayOfWeek) ?? false;
62
+
63
+ if (!hasOpeningsToday) {
64
+ const dayName = [
65
+ "Sonntag",
66
+ "Montag",
67
+ "Dienstag",
68
+ "Mittwoch",
69
+ "Donnerstag",
70
+ "Freitag",
71
+ "Samstag",
72
+ ][dayOfWeek];
73
+ return `Heute (${dayName}) ist Ruhetag. Bitte an einem anderen Tag bestellen.`;
51
74
  }
75
+
52
76
  const intervals = getServiceIntervals(now.value);
53
77
  const lastInterval = intervals.at(-1);
54
78
  if (lastInterval && earliest.value > lastInterval.end) {
@@ -0,0 +1,49 @@
1
+ import { useShopwareContext } from "#imports";
2
+
3
+ export function useHolidays() {
4
+ const { apiClient } = useShopwareContext();
5
+
6
+ const { data, pending, refresh } = useAsyncData(
7
+ "holidays",
8
+ async () => {
9
+ const response = await apiClient.invoke(
10
+ "shopbite.holiday.get get /shopbite/holiday",
11
+ );
12
+
13
+ return response.data.holidays;
14
+ },
15
+ { immediate: false },
16
+ );
17
+
18
+ /**
19
+ * Checks if a given date (default today) is a closed holiday.
20
+ * Returns undefined if holiday data is not yet loaded.
21
+ */
22
+ const isClosedHoliday = (date?: Date): boolean | undefined => {
23
+ if (!data.value) return undefined;
24
+ const checkDate = date ?? new Date();
25
+ const formattedDate = formatDateYYYYMMDD(checkDate);
26
+
27
+ return data.value.some((holiday) => {
28
+ if (!holiday.start || !holiday.end) return false;
29
+ const start = formatDateYYYYMMDD(new Date(holiday.start));
30
+ const end = formatDateYYYYMMDD(new Date(holiday.end));
31
+ return formattedDate >= start && formattedDate <= end;
32
+ });
33
+ };
34
+
35
+ return {
36
+ holidays: data,
37
+ isClosedHoliday,
38
+ isLoading: pending,
39
+ refresh,
40
+ };
41
+ }
42
+
43
+ function formatDateYYYYMMDD(date: Date): string {
44
+ const year = date.getFullYear();
45
+ const month = String(date.getMonth() + 1).padStart(2, "0");
46
+ const day = String(date.getDate()).padStart(2, "0");
47
+
48
+ return `${year}-${month}-${day}`;
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopbite-de/storefront",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "main": "nuxt.config.ts",
5
5
  "description": "Shopware storefront for food delivery shops",
6
6
  "keywords": [
@@ -72,7 +72,7 @@
72
72
  "prettier": "prettier --check \"**/*.{ts,tsx,md,vue}\"",
73
73
  "prettier:fix": "prettier --check \"**/*.{ts,tsx,md,vue}\" --write",
74
74
  "generate-types": "shopware-api-gen generate --apiType=store",
75
- "load-schema": "bunx @shopware/api-gen loadSchema --apiType=store",
75
+ "load-schema": "pnpx @shopware/api-gen loadSchema --apiType=store",
76
76
  "lint:fix": "npm run prettier:fix && npm run eslint:fix"
77
77
  }
78
78
  }
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { mockNuxtImport } from "@nuxt/test-utils/runtime";
3
+ import { ref } from "vue";
4
+ import { useBusinessHours } from "~/composables/useBusinessHours";
5
+
6
+ // Mock useAsyncData
7
+ mockNuxtImport("useAsyncData", () => {
8
+ return (key: string, handler: () => Promise<any>) => {
9
+ const data = ref(null);
10
+ const pending = ref(false);
11
+ const refresh = vi.fn(async () => {
12
+ data.value = await handler();
13
+ });
14
+ return { data, pending, refresh };
15
+ };
16
+ });
17
+
18
+ const mockBusinessHoursData = [
19
+ { dayOfWeek: 1, openingTime: "11:30", closingTime: "14:30" },
20
+ { dayOfWeek: 1, openingTime: "17:30", closingTime: "23:00" },
21
+ ];
22
+
23
+ mockNuxtImport("useShopwareContext", () => () => ({
24
+ apiClient: {
25
+ invoke: vi.fn().mockResolvedValue({
26
+ data: { businessHours: mockBusinessHoursData },
27
+ }),
28
+ },
29
+ }));
30
+
31
+ describe("useBusinessHours", () => {
32
+ beforeEach(() => {
33
+ vi.clearAllMocks();
34
+ });
35
+
36
+ it("should identify if store is open", async () => {
37
+ const { isStoreOpen, refresh } = useBusinessHours();
38
+ await refresh();
39
+
40
+ const isClosedHoliday = vi.fn().mockReturnValue(false);
41
+
42
+ // Monday at 12:00 (Open)
43
+ const openTime = new Date("2023-10-23T12:00:00"); // Monday
44
+ expect(isStoreOpen(openTime, isClosedHoliday)).toBe(true);
45
+
46
+ // Monday at 15:00 (Closed - between intervals)
47
+ const closedTime = new Date("2023-10-23T15:00:00");
48
+ expect(isStoreOpen(closedTime, isClosedHoliday)).toBe(false);
49
+
50
+ // Monday at 10:00 (Closed - before intervals)
51
+ const beforeTime = new Date("2023-10-23T10:00:00");
52
+ expect(isStoreOpen(beforeTime, isClosedHoliday)).toBe(false);
53
+
54
+ // Holiday (Closed)
55
+ isClosedHoliday.mockReturnValue(true);
56
+ expect(isStoreOpen(openTime, isClosedHoliday)).toBe(false);
57
+ });
58
+ });
@@ -3,14 +3,110 @@ import { mockNuxtImport } from "@nuxt/test-utils/runtime";
3
3
  import { ref } from "vue";
4
4
  import { useDeliveryTime } from "~/composables/useDeliveryTime";
5
5
 
6
- const { mockDeliveryTime } = vi.hoisted(() => ({
7
- mockDeliveryTime: { value: 45 }, // Use object to keep reference
8
- }));
6
+ const { mockDeliveryTime, mockBusinessHours, mockHolidays } = vi.hoisted(
7
+ () => ({
8
+ mockDeliveryTime: { value: 45 },
9
+ mockBusinessHours: {
10
+ value: [
11
+ { dayOfWeek: 1, openingTime: "11:30", closingTime: "14:30" },
12
+ { dayOfWeek: 1, openingTime: "17:30", closingTime: "23:00" },
13
+ { dayOfWeek: 3, openingTime: "11:30", closingTime: "14:30" },
14
+ { dayOfWeek: 3, openingTime: "17:30", closingTime: "23:00" },
15
+ { dayOfWeek: 4, openingTime: "11:30", closingTime: "14:30" },
16
+ { dayOfWeek: 4, openingTime: "17:30", closingTime: "23:00" },
17
+ { dayOfWeek: 5, openingTime: "11:30", closingTime: "14:30" },
18
+ { dayOfWeek: 5, openingTime: "17:30", closingTime: "23:00" },
19
+ { dayOfWeek: 6, openingTime: "17:30", closingTime: "23:30" },
20
+ { dayOfWeek: 0, openingTime: "11:30", closingTime: "14:30" },
21
+ { dayOfWeek: 0, openingTime: "17:30", closingTime: "23:00" },
22
+ ],
23
+ },
24
+ mockHolidays: { value: [] },
25
+ }),
26
+ );
9
27
 
10
28
  mockNuxtImport("useShopBiteConfig", () => () => ({
11
29
  deliveryTime: mockDeliveryTime,
12
30
  }));
13
31
 
32
+ mockNuxtImport("useBusinessHours", () => () => ({
33
+ businessHours: mockBusinessHours,
34
+ getServiceIntervals: (date: Date) => {
35
+ const dayOfWeek = date.getDay();
36
+ return mockBusinessHours.value
37
+ .filter((bh) => bh.dayOfWeek === dayOfWeek)
38
+ .map((bh) => {
39
+ const [startH, startM] = bh.openingTime.split(":").map(Number);
40
+ const [endH, endM] = bh.closingTime.split(":").map(Number);
41
+ const start = new Date(date);
42
+ start.setHours(startH, startM, 0, 0);
43
+ const end = new Date(date);
44
+ end.setHours(endH, endM, 0, 0);
45
+ return { start, end };
46
+ });
47
+ },
48
+ getEarliestSelectableTime: (
49
+ currentTime: Date,
50
+ currentDeliveryTime: number | null,
51
+ ) => {
52
+ const earliest = new Date(currentTime);
53
+ earliest.setMinutes(earliest.getMinutes() + (currentDeliveryTime ?? 30));
54
+ earliest.setSeconds(0, 0);
55
+ return earliest;
56
+ },
57
+ findActiveInterval: (
58
+ currentTime: Date,
59
+ currentDeliveryTime: number | null,
60
+ ) => {
61
+ const intervals = [
62
+ { dayOfWeek: 1, openingTime: "11:30", closingTime: "14:30" },
63
+ { dayOfWeek: 1, openingTime: "17:30", closingTime: "23:00" },
64
+ { dayOfWeek: 3, openingTime: "11:30", closingTime: "14:30" },
65
+ { dayOfWeek: 3, openingTime: "17:30", closingTime: "23:00" },
66
+ { dayOfWeek: 4, openingTime: "11:30", closingTime: "14:30" },
67
+ { dayOfWeek: 4, openingTime: "17:30", closingTime: "23:00" },
68
+ { dayOfWeek: 5, openingTime: "11:30", closingTime: "14:30" },
69
+ { dayOfWeek: 5, openingTime: "17:30", closingTime: "23:00" },
70
+ { dayOfWeek: 6, openingTime: "17:30", closingTime: "23:30" },
71
+ { dayOfWeek: 0, openingTime: "11:30", closingTime: "14:30" },
72
+ { dayOfWeek: 0, openingTime: "17:30", closingTime: "23:00" },
73
+ ]
74
+ .filter((bh) => bh.dayOfWeek === currentTime.getDay())
75
+ .map((bh) => {
76
+ const [startH, startM] = bh.openingTime.split(":").map(Number);
77
+ const [endH, endM] = bh.closingTime.split(":").map(Number);
78
+ const start = new Date(currentTime);
79
+ start.setHours(startH, startM, 0, 0);
80
+ const end = new Date(currentTime);
81
+ end.setHours(endH, endM, 0, 0);
82
+ return { start, end };
83
+ });
84
+
85
+ const earliest = new Date(currentTime);
86
+ earliest.setMinutes(earliest.getMinutes() + (currentDeliveryTime ?? 30));
87
+ earliest.setSeconds(0, 0);
88
+
89
+ if (intervals.length === 0) return null;
90
+ const current = intervals.find(
91
+ (i) => earliest >= i.start && earliest <= i.end,
92
+ );
93
+ if (current) return current;
94
+ return intervals.find((i) => i.start > earliest) ?? null;
95
+ },
96
+ }));
97
+
98
+ mockNuxtImport("useHolidays", () => () => ({
99
+ isClosedHoliday: (date: Date) => {
100
+ if (!mockHolidays.value) return undefined;
101
+ const formattedDate = date.toISOString().split("T")[0];
102
+ return mockHolidays.value.some((h: any) => {
103
+ const start = h.start.split("T")[0];
104
+ const end = h.end.split("T")[0];
105
+ return formattedDate >= start && formattedDate <= end;
106
+ });
107
+ },
108
+ }));
109
+
14
110
  describe("useDeliveryTime", () => {
15
111
  const now = ref(new Date("2023-10-27T12:00:00")); // A Friday
16
112
 
@@ -65,4 +161,11 @@ describe("useDeliveryTime", () => {
65
161
  );
66
162
  }
67
163
  });
164
+
165
+ it("should return loading message when holidays are not loaded", () => {
166
+ mockHolidays.value = null;
167
+ const { helperText } = useDeliveryTime(now);
168
+ expect(helperText.value).toBe("Lade Informationen...");
169
+ mockHolidays.value = []; // Reset for other tests
170
+ });
68
171
  });