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:
parent
b7554b89c8
commit
64dfec1355
@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -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 />,
|
||||
};
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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} />
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 }),
|
||||
});
|
||||
23
apps/web/stores/lib/slices/services-slice.ts
Normal file
23
apps/web/stores/lib/slices/services-slice.ts
Normal 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 }),
|
||||
});
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
}));
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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:00–12:00, сервис = 1 час, шаг = 15 мин
|
||||
// Все возможные начала интервалов: от 10:00 до 11:00 включительно (всего 5)
|
||||
// Слот: 10:00–12: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());
|
||||
});
|
||||
|
||||
@ -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) =>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user