@shopbite-de/storefront 1.4.1 → 1.5.1
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.
- package/api-types/storeApiSchema.json +4503 -4363
- package/api-types/storeApiTypes.d.ts +73 -0
- package/app/app.vue +11 -2
- package/app/components/Checkout/DeliveryTimeSelect.vue +16 -5
- package/app/components/Header.vue +2 -3
- package/app/composables/useBusinessHours.ts +156 -0
- package/app/composables/useDeliveryTime.ts +34 -10
- package/app/composables/useHolidays.ts +49 -0
- package/package.json +2 -3
- package/test/nuxt/useBusinessHours.test.ts +58 -0
- package/test/nuxt/useDeliveryTime.test.ts +106 -3
- package/app/utils/businessHours.ts +0 -114
- package/app/utils/holidays.ts +0 -24
- package/app/utils/storeHours.ts +0 -8
|
@@ -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 {
|
|
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="
|
|
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>
|
|
@@ -84,9 +84,8 @@ const cartQuickViewOpen = ref(false);
|
|
|
84
84
|
<template #title>
|
|
85
85
|
<NuxtLink to="/" class="-m-1.5 p-1.5">
|
|
86
86
|
<span class="sr-only">{{ siteName }}</span>
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
dark="/dark/Logo.svg"
|
|
87
|
+
<NuxtImg
|
|
88
|
+
src="/light/Logo.svg"
|
|
90
89
|
class="h-12 w-auto"
|
|
91
90
|
/>
|
|
92
91
|
</NuxtLink>
|
|
@@ -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
|
-
|
|
50
|
-
|
|
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.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"main": "nuxt.config.ts",
|
|
5
5
|
"description": "Shopware storefront for food delivery shops",
|
|
6
6
|
"keywords": [
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
"@vue/compiler-dom": "^3.5.26",
|
|
49
49
|
"@vue/server-renderer": "^3.5.26",
|
|
50
50
|
"@vue/test-utils": "^2.4.6",
|
|
51
|
-
"dotenv-cli": "^11.0.0",
|
|
52
51
|
"eslint": "^9.39.1",
|
|
53
52
|
"happy-dom": "^20.0.10",
|
|
54
53
|
"jsdom": "^27.2.0",
|
|
@@ -72,7 +71,7 @@
|
|
|
72
71
|
"prettier": "prettier --check \"**/*.{ts,tsx,md,vue}\"",
|
|
73
72
|
"prettier:fix": "prettier --check \"**/*.{ts,tsx,md,vue}\" --write",
|
|
74
73
|
"generate-types": "shopware-api-gen generate --apiType=store",
|
|
75
|
-
"load-schema": "
|
|
74
|
+
"load-schema": "pnpx @shopware/api-gen loadSchema --apiType=store",
|
|
76
75
|
"lint:fix": "npm run prettier:fix && npm run eslint:fix"
|
|
77
76
|
}
|
|
78
77
|
}
|
|
@@ -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
|
+
});
|