refactor(order-form): update service handling to support multiple services

- Renamed `ServiceSelect` to `ServicesSelect` for clarity.
- Updated state management to handle multiple service IDs instead of a single service ID.
- Adjusted related components (`DateSelect`, `TimeSelect`, `SubmitButton`, and `NextButton`) to accommodate the new services structure.
- Removed the deprecated `service-select.tsx` file and refactored related logic in the order store and API to support multiple services.
- Enhanced error handling in the slots service to validate multiple services correctly.
This commit is contained in:
vchikalkin 2025-08-19 19:14:14 +03:00
parent b7554b89c8
commit 64dfec1355
13 changed files with 221 additions and 133 deletions

View File

@ -14,7 +14,7 @@ export function DateSelect() {
const setTime = useOrderStore((store) => store.setTime);
const setSlot = useOrderStore((store) => store.setSlotId);
const masterId = useOrderStore((store) => store.masterId);
const serviceId = useOrderStore((store) => store.serviceId);
const serviceIds = useOrderStore((store) => store.serviceIds);
useEffect(() => {
if (!selectedDate) {
@ -28,7 +28,7 @@ export function DateSelect() {
{
filters: {
datetime_start: {
gte: dayjs(selectedMonthDate).startOf('month').startOf('day').toISOString(),
gte: dayjs().startOf('day').toISOString(),
lte: dayjs(selectedMonthDate).endOf('month').endOf('day').toISOString(),
},
master: {
@ -39,11 +39,7 @@ export function DateSelect() {
},
},
{
service: {
documentId: {
eq: serviceId,
},
},
services: serviceIds,
},
);
@ -80,7 +76,7 @@ export function DateTimeSelect() {
export function TimeSelect() {
const masterId = useOrderStore((store) => store.masterId);
const date = useOrderStore((store) => store.date);
const serviceId = useOrderStore((store) => store.serviceId);
const serviceIds = useOrderStore((store) => store.serviceIds);
const { data: { times } = {}, isLoading } = useAvailableTimeSlotsQuery(
{
@ -97,11 +93,7 @@ export function TimeSelect() {
},
},
{
service: {
documentId: {
eq: serviceId,
},
},
services: serviceIds,
},
);

View File

@ -5,7 +5,7 @@ import { ClientsGrid, MastersGrid } from './contacts-grid';
import { DateTimeSelect } from './datetime-select';
import { NextButton } from './next-button';
import { ErrorPage, SuccessPage } from './result';
import { ServiceSelect } from './service-select';
import { ServicesSelect } from './services-select';
import { SubmitButton } from './submit-button';
import { useGetUrlData } from '@/hooks/url';
import { OrderStoreProvider, useInitOrderStore, useOrderStore } from '@/stores/order';
@ -19,7 +19,7 @@ const STEP_COMPONENTS: Record<string, JSX.Element> = {
'datetime-select': <DateTimeSelect />,
error: <ErrorPage />,
'master-select': <MastersGrid />,
'service-select': <ServiceSelect />,
'service-select': <ServicesSelect />,
success: <SuccessPage />,
};

View File

@ -4,14 +4,14 @@ import { useOrderStore } from '@/stores/order';
import { Button } from '@repo/ui/components/ui/button';
export function NextButton() {
const { clientId, date, masterId, nextStep, serviceId, step, time } = useOrderStore(
const { clientId, date, masterId, nextStep, serviceIds, step, time } = useOrderStore(
(store) => store,
);
const isDisabled =
(step === 'master-select' && !masterId) ||
(step === 'client-select' && !clientId) ||
(step === 'service-select' && !serviceId) ||
(step === 'service-select' && !serviceIds.length) ||
(step === 'datetime-select' && (!date || !time));
return (

View File

@ -1,3 +1,4 @@
/* eslint-disable consistent-return */
'use client';
import { DataNotFound } from '@/components/shared/alert';
@ -7,7 +8,7 @@ import { useOrderStore } from '@/stores/order';
import { type ServiceFieldsFragment } from '@repo/graphql/types';
import { cn } from '@repo/ui/lib/utils';
export function ServiceSelect() {
export function ServicesSelect() {
const masterId = useOrderStore((store) => store.masterId);
const { data: { services } = {} } = useServicesQuery({
@ -35,14 +36,19 @@ export function ServiceSelect() {
}
function ServiceCardRadio({ documentId, ...props }: Readonly<ServiceFieldsFragment>) {
const serviceId = useOrderStore((store) => store.serviceId);
const setServiceId = useOrderStore((store) => store.setServiceId);
const serviceIds = useOrderStore((store) => store.serviceIds);
const addServiceId = useOrderStore((store) => store.addServiceId);
const removeServiceId = useOrderStore((store) => store.removeServiceId);
const selected = serviceId === documentId;
const selected = serviceIds.includes(documentId);
function handleOnSelect() {
setServiceId(documentId);
}
const handleOnSelect = () => {
if (selected) {
return removeServiceId(documentId);
} else {
addServiceId(documentId);
}
};
return (
<label
@ -55,8 +61,8 @@ function ServiceCardRadio({ documentId, ...props }: Readonly<ServiceFieldsFragme
checked={selected}
className="hidden"
name="service"
onChange={() => handleOnSelect()}
type="radio"
onChange={handleOnSelect}
type="checkbox"
value={documentId}
/>
<ServiceCard {...props} />

View File

@ -7,8 +7,8 @@ import { LoadingSpinner } from '@repo/ui/components/ui/spinner';
import { useEffect } from 'react';
export function SubmitButton() {
const { clientId, date, serviceId, setStep, slotId, time } = useOrderStore((store) => store);
const isDisabled = !clientId || !serviceId || !date || !time || !slotId;
const { clientId, date, serviceIds, setStep, slotId, time } = useOrderStore((store) => store);
const isDisabled = !clientId || !serviceIds.length || !date || !time || !slotId;
const { isError, isPending, isSuccess, mutate: createOrder } = useOrderCreate();
@ -19,7 +19,7 @@ export function SubmitButton() {
input: {
client: clientId,
datetime_start: time,
services: [serviceId],
services: serviceIds,
slot: slotId,
},
});

View File

@ -1,6 +1,6 @@
export * from './client-slice';
export * from './datetime-slice';
export * from './master-slice';
export * from './service-slice';
export * from './services-slice';
export * from './slot-slice';
export * from './steps-slice';

View File

@ -1,11 +0,0 @@
import { type StateCreator } from 'zustand';
export type ServiceSlice = {
serviceId: null | string;
setServiceId: (id: null | string) => void;
};
export const createServiceSlice: StateCreator<ServiceSlice> = (set) => ({
serviceId: null,
setServiceId: (id) => set({ serviceId: id }),
});

View File

@ -0,0 +1,23 @@
import { type StateCreator } from 'zustand';
export type ServiceSlice = {
addServiceId: (id: string) => void;
clearServiceIds: () => void;
removeServiceId: (id: string) => void;
serviceIds: string[];
setServiceIds: (ids: string[]) => void;
};
export const createServicesSlice: StateCreator<ServiceSlice> = (set) => ({
addServiceId: (id) =>
set((state) => ({
serviceIds: state.serviceIds.includes(id) ? state.serviceIds : [...state.serviceIds, id],
})),
clearServiceIds: () => set({ serviceIds: [] }),
removeServiceId: (id) =>
set((state) => ({
serviceIds: state.serviceIds.filter((serviceId) => serviceId !== id),
})),
serviceIds: [],
setServiceIds: (ids) => set({ serviceIds: ids }),
});

View File

@ -4,6 +4,7 @@ import { useOrderStore } from './context';
import { type Steps } from './types';
import { useCustomerQuery, useIsMaster } from '@/hooks/api/customers';
import { type OrderFieldsFragment } from '@repo/graphql/types';
import { sift } from 'radashi';
import { useEffect, useRef } from 'react';
const STEPS: Steps[] = [
@ -23,7 +24,7 @@ export function useInitOrderStore(initData: null | OrderFieldsFragment) {
const setMasterId = useOrderStore((store) => store.setMasterId);
const setClientId = useOrderStore((store) => store.setClientId);
const setServiceId = useOrderStore((store) => store.setServiceId);
const setServiceIds = useOrderStore((store) => store.setServiceIds);
const setStep = useOrderStore((store) => store.setStep);
const setStepsSequence = useOrderStore((store) => store._setStepSequence);
const step = useOrderStore((store) => store.step);
@ -38,13 +39,13 @@ export function useInitOrderStore(initData: null | OrderFieldsFragment) {
if (initData) {
const masterId = initData.slot?.master?.documentId;
const clientId = initData.client?.documentId;
const serviceId = initData.services?.[0]?.documentId;
const serviceIds = sift(initData.services).map(({ documentId }) => documentId);
if (masterId) setMasterId(masterId);
if (clientId) setClientId(clientId);
if (serviceId) setServiceId(serviceId);
if (serviceIds) setServiceIds(serviceIds);
if (masterId && clientId && serviceId) {
if (masterId && clientId && serviceIds.length) {
setStep('datetime-select');
} else if (masterId && clientId) {
setStep('service-select');
@ -69,7 +70,7 @@ export function useInitOrderStore(initData: null | OrderFieldsFragment) {
isMaster,
setClientId,
setMasterId,
setServiceId,
setServiceIds,
setStep,
setStepsSequence,
step,

View File

@ -2,7 +2,7 @@ import {
createClientSlice,
createDateTimeSlice,
createMasterSlice,
createServiceSlice,
createServicesSlice,
createSlotSlice,
createStepsSlice,
} from '../lib/slices';
@ -14,7 +14,7 @@ export function createOrderStore() {
...createClientSlice(...args),
...createDateTimeSlice(...args),
...createMasterSlice(...args),
...createServiceSlice(...args),
...createServicesSlice(...args),
...createSlotSlice(...args),
...createStepsSlice(...args),
}));

View File

@ -48,19 +48,32 @@ export class OrdersService extends BaseService {
// Проверки на существование обязательных полей для предотвращения ошибок типов
if (!variables.input.slot) throw new Error(ERRORS.MISSING_SLOT);
if (!variables.input.services?.length) throw new Error(ERRORS.MISSING_SERVICES);
if (!variables.input.services[0]) throw new Error(ERRORS.MISSING_SERVICE_ID);
if (!variables.input.client) throw new Error(ERRORS.MISSING_CLIENT);
const servicesService = new ServicesService(this._user);
const { service } = await servicesService.getService({
documentId: variables.input.services[0],
});
// Получаем все услуги по их идентификаторам
const services: Array<{ duration: string }> = [];
for (const serviceId of variables.input.services) {
if (!serviceId) throw new Error(ERRORS.MISSING_SERVICE_ID);
if (!service?.duration) throw new Error(ERRORS.INVALID_SERVICE_DURATION);
const { service } = await servicesService.getService({
documentId: serviceId,
});
if (!service?.duration) throw new Error(ERRORS.INVALID_SERVICE_DURATION);
services.push(service);
}
// Суммируем длительности всех услуг
const totalServiceDuration = services.reduce(
(sum, service) => sum + getMinutes(service.duration),
0,
);
const datetimeEnd = dayjs(variables.input.datetime_start)
.add(getMinutes(service.duration), 'minute')
.add(totalServiceDuration, 'minute')
.toISOString();
await this.checkBeforeCreate({

View File

@ -82,21 +82,31 @@ describe('SlotsService', () => {
};
const mockContext = {
service: {
documentId: { eq: 'service-123' },
},
services: ['service-123', 'service-456'],
};
const mockService = {
const mockService1 = {
active: true,
documentId: 'service-123',
duration: '01:00:00', // 1 час
master: mockCustomer,
name: 'Test Service',
name: 'Test Service 1',
};
const mockGetServiceResult = {
service: mockService,
const mockService2 = {
active: true,
documentId: 'service-456',
duration: '00:30:00', // 30 минут
master: mockCustomer,
name: 'Test Service 2',
};
const mockGetServiceResult1 = {
service: mockService1,
};
const mockGetServiceResult2 = {
service: mockService2,
};
const mockSlotWithOrders = {
@ -143,43 +153,40 @@ describe('SlotsService', () => {
query: mockQuery,
});
// Мокаем ServicesService.getService
const mockGetService = vi.fn().mockResolvedValue(mockGetServiceResult);
// Мокаем ServicesService.getService для двух сервисов
const mockGetService = vi
.fn()
.mockResolvedValueOnce(mockGetServiceResult1) // Первый вызов для service-123
.mockResolvedValueOnce(mockGetServiceResult2); // Второй вызов для service-456
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
const result = slotsService.getAvailableTimeSlots(mockVariables, mockContext);
const result = await slotsService.getAvailableTimeSlots(mockVariables, mockContext);
expect(result).resolves.toEqual({
expect(result).toEqual({
slots: [mockSlotWithOrders, mockSlotWithoutOrders],
times: [
{
slotId: 'slot-123',
time: now.add(1, 'hour').toISOString(), // 11:00
slotId: 'slot-124',
time: now.add(3, 'hour').toISOString(), // 18:00
},
{
slotId: 'slot-124',
time: now.add(3, 'hour').toISOString(), // 13:00
time: now.add(3, 'hour').add(15, 'minute').toISOString(), // 18:15
},
{
slotId: 'slot-124',
time: now.add(3, 'hour').add(15, 'minute').toISOString(), // 13:15
},
{
slotId: 'slot-124',
time: now.add(3, 'hour').add(30, 'minute').toISOString(), // 13:30
},
{
slotId: 'slot-124',
time: now.add(3, 'hour').add(45, 'minute').toISOString(), // 13:45
},
{
slotId: 'slot-124',
time: now.add(4, 'hour').toISOString(), // 14:00
time: now.add(3, 'hour').add(30, 'minute').toISOString(), // 18:30
},
],
});
// Проверяем, что getService был вызван для каждого сервиса
expect(mockGetService).toHaveBeenCalledTimes(2);
expect(mockGetService).toHaveBeenNthCalledWith(1, { documentId: 'service-123' });
expect(mockGetService).toHaveBeenNthCalledWith(2, { documentId: 'service-456' });
});
it('should throw error when datetime_start is missing', async () => {
@ -192,14 +199,24 @@ describe('SlotsService', () => {
await expect(result).rejects.toThrow(ERRORS.MISSING_DATETIME_START);
});
it('should throw error when service documentId is missing', async () => {
const contextWithoutService = {
service: {},
it('should throw error when services array is missing', async () => {
const contextWithoutServices = {
services: [],
};
const result = slotsService.getAvailableTimeSlots(mockVariables, contextWithoutService);
const result = slotsService.getAvailableTimeSlots(mockVariables, contextWithoutServices);
await expect(result).rejects.toThrow(ERRORS.MISSING_SERVICE_ID);
await expect(result).rejects.toThrow(ERRORS.MISSING_SERVICES_IDS);
});
it('should throw error when services array is null', async () => {
const contextWithNullServices = {
services: null,
};
const result = slotsService.getAvailableTimeSlots(mockVariables, contextWithNullServices);
await expect(result).rejects.toThrow(ERRORS.MISSING_SERVICES_IDS);
});
it('should throw error when service is not found', async () => {
@ -215,9 +232,9 @@ describe('SlotsService', () => {
query: mockQuery,
});
// Мокаем ServicesService.getService возвращающий null
// Мокаем ServicesService.getService возвращающий null для первого сервиса
const mockGetService = vi.fn().mockResolvedValue({
data: { service: null },
service: null,
});
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
@ -225,7 +242,7 @@ describe('SlotsService', () => {
const result = slotsService.getAvailableTimeSlots(mockVariables, mockContext);
await expect(result).rejects.toThrow(ERRORS.SERVICE_NOT_FOUND);
await expect(result).rejects.toThrow(ERRORS.NOT_FOUND_SERVICE);
});
it('should filter out times that conflict with orders', async () => {
@ -241,8 +258,12 @@ describe('SlotsService', () => {
query: mockQuery,
});
// Мокаем ServicesService.getService
const mockGetService = vi.fn().mockResolvedValue(mockGetServiceResult);
// Мокаем ServicesService.getService для двух сервисов
const mockGetService = vi
.fn()
.mockResolvedValueOnce(mockGetServiceResult1)
.mockResolvedValueOnce(mockGetServiceResult2);
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
@ -256,18 +277,24 @@ describe('SlotsService', () => {
});
it('should include times from cancelled orders', async () => {
vi.setSystemTime(now.toDate()); // синхронизируем "текущее" время
// Устанавливаем текущее время на 5 минут раньше начала слота
const currentTime = now.subtract(5, 'minute');
vi.setSystemTime(currentTime.toDate());
const slotWithCancelledOrder = {
...mockSlotWithOrders,
datetime_end: now.add(2, 'hour').toISOString(), // 12:00
datetime_start: now.toISOString(), // 10:00
documentId: 'slot-cancelled',
master: mockCustomer,
orders: [
{
datetime_end: now.add(1, 'hour').toISOString(),
datetime_start: now.toISOString(),
datetime_end: now.add(1, 'hour').toISOString(), // 11:00
datetime_start: now.toISOString(), // 10:00
documentId: 'order-123',
state: GQL.Enum_Order_State.Cancelled,
},
],
state: GQL.Enum_Slot_State.Open,
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
@ -284,21 +311,26 @@ describe('SlotsService', () => {
query: mockQuery,
});
const mockGetService = vi.fn().mockResolvedValue(mockGetServiceResult);
const mockGetService = vi
.fn()
.mockResolvedValueOnce(mockGetServiceResult1)
.mockResolvedValueOnce(mockGetServiceResult2);
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
const result = await slotsService.getAvailableTimeSlots(mockVariables, mockContext);
// Слот: 10:0012:00, сервис = 1 час, шаг = 15 мин
// Все возможные начала интервалов: от 10:00 до 11:00 включительно (всего 5)
// Слот: 10:0012:00, общая длительность сервисов = 1.5 часа (90 мин)
// datetimeEnd для генерации = 12:00 - 90 мин = 10:30
// Шаг = 15 мин
// Доступные времена: 10:00, 10:15, 10:30 (но 10:00 не пройдет проверку isAfter(now, 'minute'))
// Текущее время = 9:55, поэтому 10:00 > 9:55 на 5 минут > 1 минуту
const expectedTimes = [
// now.toISOString(), // 10:00
now.toISOString(), // 10:00
now.add(15, 'minute').toISOString(), // 10:15
now.add(30, 'minute').toISOString(), // 10:30
now.add(45, 'minute').toISOString(), // 10:45
now.add(1, 'hour').toISOString(), // 11:00
];
const actualTimes = result.times
@ -306,6 +338,9 @@ describe('SlotsService', () => {
.map((time) => time.time);
expect(actualTimes).toEqual(expectedTimes);
// Восстанавливаем исходное время
vi.setSystemTime(now.toDate());
});
it('should filter out times in the past', async () => {
@ -332,8 +367,12 @@ describe('SlotsService', () => {
query: mockQuery,
});
// Мокаем ServicesService.getService
const mockGetService = vi.fn().mockResolvedValue(mockGetServiceResult);
// Мокаем ServicesService.getService для двух сервисов
const mockGetService = vi
.fn()
.mockResolvedValueOnce(mockGetServiceResult1)
.mockResolvedValueOnce(mockGetServiceResult2);
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
@ -368,8 +407,12 @@ describe('SlotsService', () => {
query: mockQuery,
});
// Мокаем ServicesService.getService
const mockGetService = vi.fn().mockResolvedValue(mockGetServiceResult);
// Мокаем ServicesService.getService для двух сервисов
const mockGetService = vi
.fn()
.mockResolvedValueOnce(mockGetServiceResult1)
.mockResolvedValueOnce(mockGetServiceResult2);
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
@ -411,8 +454,12 @@ describe('SlotsService', () => {
query: mockQuery,
});
// Мокаем ServicesService.getService
const mockGetService = vi.fn().mockResolvedValue(mockGetServiceResult);
// Мокаем ServicesService.getService для двух сервисов
const mockGetService = vi
.fn()
.mockResolvedValueOnce(mockGetServiceResult1)
.mockResolvedValueOnce(mockGetServiceResult2);
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
@ -443,12 +490,17 @@ describe('SlotsService', () => {
await expect(result).rejects.toThrow('GraphQL error');
});
it('should calculate service duration correctly', async () => {
const serviceWithDuration = {
...mockService,
it('should calculate total service duration correctly', async () => {
const serviceWithDuration1 = {
...mockService1,
duration: '00:30:00', // 30 минут
};
const serviceWithDuration2 = {
...mockService2,
duration: '00:45:00', // 45 минут
};
const mockQuery = vi.fn().mockImplementation(({ query }) => {
if (query === GQL.GetSlotsOrdersDocument) {
return Promise.resolve({
@ -463,24 +515,26 @@ describe('SlotsService', () => {
query: mockQuery,
});
// Мокаем ServicesService.getService с другой длительностью
const mockGetService = vi.fn().mockResolvedValue({
service: serviceWithDuration,
});
// Мокаем ServicesService.getService с разными длительностями
const mockGetService = vi
.fn()
.mockResolvedValueOnce({ service: serviceWithDuration1 })
.mockResolvedValueOnce({ service: serviceWithDuration2 });
mockServicesService.mockImplementation(() => ({
getService: mockGetService,
}));
const result = await slotsService.getAvailableTimeSlots(mockVariables, mockContext);
// Проверяем, что времена генерируются с учетом длительности услуги
// Проверяем, что времена генерируются с учетом общей длительности услуг (30 + 45 = 75 минут)
expect(result.times.length).toBeGreaterThan(0);
// Последнее время должно быть не позже чем datetime_end минус длительность услуги
// Последнее время должно быть не позже чем datetime_end минус общая длительность услуг
const lastTime = dayjs(result.times[result.times.length - 1].time);
const slotEnd = dayjs(mockSlotWithoutOrders.datetime_end);
const serviceDuration = dayjs.duration('00:30:00');
const maxTime = slotEnd.subtract(serviceDuration);
const totalServiceDuration = dayjs.duration('01:15:00'); // 75 минут
const maxTime = slotEnd.subtract(totalServiceDuration);
expect(lastTime.valueOf()).toBeLessThanOrEqual(maxTime.valueOf());
});

View File

@ -12,7 +12,7 @@ export const ERRORS = {
INVALID_TIME: 'Некорректное время',
MISSING_DATETIME_END: 'Не указана дата окончания',
MISSING_DATETIME_START: 'Не указана дата начала',
MISSING_SERVICE_ID: 'Не указана услуга',
MISSING_SERVICES_IDS: 'Не указаны услуги',
NO_PAST_SLOT: 'Нельзя создать слот в прошлом',
NO_PERMISSION: 'Нет доступа',
NOT_FOUND_MASTER: 'Мастер не найден',
@ -71,10 +71,30 @@ export class SlotsService extends BaseService {
async getAvailableTimeSlots(
variables: VariablesOf<typeof GQL.GetSlotsDocument>,
context: { service: GQL.ServiceFiltersInput },
context: { services: string[] },
) {
if (!variables.filters?.datetime_start) throw new Error(ERRORS.MISSING_DATETIME_START);
if (!context?.service?.documentId?.eq) throw new Error(ERRORS.MISSING_SERVICE_ID);
if (!context?.services?.length) throw new Error(ERRORS.MISSING_SERVICES_IDS);
const servicesService = new ServicesService(this._user);
// Получаем все услуги по массиву id`
const services: Array<{ duration: string }> = [];
for (const serviceId of context.services) {
const { service } = await servicesService.getService({
documentId: serviceId,
});
if (!service) throw new Error(ERRORS.NOT_FOUND_SERVICE);
services.push(service);
}
// Суммируем длительности всех услуг
const totalServiceDuration = services.reduce(
(sum, service) => sum + getMinutes(service.duration),
0,
);
const { query } = await getClientWithToken();
@ -90,16 +110,6 @@ export class SlotsService extends BaseService {
if (getSlotsResult.error) throw new Error(getSlotsResult.error.message);
const servicesService = new ServicesService(this._user);
const { service } = await servicesService.getService({
documentId: context.service.documentId.eq,
});
if (!service) throw new Error(ERRORS.NOT_FOUND_SERVICE);
const serviceDuration = getMinutes(service.duration);
const slots = getSlotsResult.data.slots;
const times: Array<{ slotId: string; time: string }> = [];
@ -110,11 +120,11 @@ export class SlotsService extends BaseService {
if (!slot?.datetime_start || !slot?.datetime_end) continue;
let datetimeStart = dayjs(slot.datetime_start);
const datetimeEnd = dayjs(slot.datetime_end).subtract(serviceDuration, 'minutes');
const datetimeEnd = dayjs(slot.datetime_end).subtract(totalServiceDuration, 'minutes');
while (datetimeStart.valueOf() <= datetimeEnd.valueOf()) {
const slotStartTime = datetimeStart;
const potentialDatetimeEnd = datetimeStart.add(serviceDuration, 'minutes');
const potentialDatetimeEnd = datetimeStart.add(totalServiceDuration, 'minutes');
const hasConflict = slot.orders.some(
(order) =>