236 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* eslint-disable canonical/id-match */
/* eslint-disable @typescript-eslint/naming-convention */
import { getClientWithToken } from '../apollo/client';
import * as GQL from '../types';
import { BaseService } from './base';
import { ServicesService } from './services';
import { type VariablesOf } from '@graphql-typed-document-node/core';
import { getMinutes } from '@repo/utils/datetime-format';
import dayjs from 'dayjs';
export const ERRORS = {
HAS_ORDERS: 'Слот имеет активные заказы',
INVALID_TIME: 'Некорректное время',
MISSING_DATETIME_END: 'Не указана дата окончания',
MISSING_DATETIME_START: 'Не указана дата начала',
MISSING_SERVICE_ID: 'Не указана услуга',
NO_PERMISSION: 'Нет доступа',
OVERLAPPING_TIME: 'Время пересекается с другими слотами',
SERVICE_NOT_FOUND: 'Слот не найден',
SLOT_NOT_FOUND: 'Слот не найден',
};
const FORBIDDEN_ORDER_STATES: GQL.Enum_Order_State[] = [
GQL.Enum_Order_State.Scheduled,
GQL.Enum_Order_State.Approved,
GQL.Enum_Order_State.Completed,
GQL.Enum_Order_State.Cancelled,
];
export class SlotsService extends BaseService {
async createSlot(variables: VariablesOf<typeof GQL.CreateSlotDocument>) {
const { customer } = await this._getUser();
const { mutate } = await getClientWithToken();
const mutationResult = await mutate({
mutation: GQL.CreateSlotDocument,
variables: {
...variables,
input: {
...variables.input,
master: customer?.documentId,
},
},
});
const error = mutationResult.errors?.at(0);
if (error) throw new Error(error.message);
return mutationResult.data;
}
async deleteSlot(variables: VariablesOf<typeof GQL.DeleteSlotDocument>) {
await this.checkPermission(variables);
const { slot } = await this.getSlot({ documentId: variables.documentId });
if (slot?.orders?.length) {
throw new Error(ERRORS.HAS_ORDERS);
}
const { mutate } = await getClientWithToken();
const mutationResult = await mutate({
mutation: GQL.DeleteSlotDocument,
variables,
});
const error = mutationResult.errors?.at(0);
if (error) throw new Error(error.message);
return mutationResult.data;
}
async getAvailableTimeSlots(
variables: VariablesOf<typeof GQL.GetSlotsDocument>,
context: { service: GQL.ServiceFiltersInput },
) {
if (!variables.filters?.datetime_start) throw new Error(ERRORS.MISSING_DATETIME_START);
if (!context?.service?.documentId?.eq) throw new Error(ERRORS.MISSING_SERVICE_ID);
const { query } = await getClientWithToken();
const getSlotsResult = await query({
query: GQL.GetSlotsOrdersDocument,
variables: {
filters: {
...variables.filters,
state: { eq: GQL.Enum_Slot_State.Open },
},
},
});
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.SERVICE_NOT_FOUND);
const serviceDuration = getMinutes(service.duration);
const slots = getSlotsResult.data.slots;
const times: Array<{ slotId: string; time: string }> = [];
const now = dayjs();
for (const slot of slots) {
if (!slot?.datetime_start || !slot?.datetime_end) continue;
let datetimeStart = dayjs(slot.datetime_start);
const datetimeEnd = dayjs(slot.datetime_end).subtract(serviceDuration, 'minutes');
while (datetimeStart.valueOf() <= datetimeEnd.valueOf()) {
const slotStartTime = datetimeStart;
const potentialDatetimeEnd = datetimeStart.add(serviceDuration, 'minutes');
const hasConflict = slot.orders.some(
(order) =>
order?.state &&
![GQL.Enum_Order_State.Cancelled].includes(order.state) &&
slotStartTime.isBefore(dayjs(order.datetime_end)) &&
potentialDatetimeEnd.isAfter(dayjs(order.datetime_start)),
);
if (!hasConflict && datetimeStart.isAfter(now, 'minute')) {
times.push({ slotId: slot.documentId, time: datetimeStart.toISOString() });
}
datetimeStart = datetimeStart.add(15, 'minutes');
}
}
return { slots, times };
}
async getSlot(variables: VariablesOf<typeof GQL.GetSlotDocument>) {
const { query } = await getClientWithToken();
const result = await query({
query: GQL.GetSlotDocument,
variables,
});
return result.data;
}
async getSlots(variables: VariablesOf<typeof GQL.GetSlotsDocument>) {
const { query } = await getClientWithToken();
const result = await query({
query: GQL.GetSlotsDocument,
variables,
});
return result.data;
}
async updateSlot(variables: VariablesOf<typeof GQL.UpdateSlotDocument>) {
await this.checkPermission(variables);
await this.checkIsTimeChanging(variables);
const { mutate } = await getClientWithToken();
const mutationResult = await mutate({
mutation: GQL.UpdateSlotDocument,
variables,
});
const error = mutationResult.errors?.at(0);
if (error) throw new Error(error.message);
return mutationResult.data;
}
private async checkIsTimeChanging(variables: VariablesOf<typeof GQL.UpdateSlotDocument>) {
const { slot } = await this.getSlot({ documentId: variables.documentId });
if (!slot) throw new Error(ERRORS.SLOT_NOT_FOUND);
const datetime_start = variables.data.datetime_start;
const datetime_end = variables.data.datetime_end;
const isTimeChanging = datetime_start || datetime_end;
if (!isTimeChanging) return;
if (!datetime_start) throw new Error(ERRORS.MISSING_DATETIME_START);
if (!datetime_end) throw new Error(ERRORS.MISSING_DATETIME_END);
// Проверка валидности времени
if (new Date(datetime_end) <= new Date(datetime_start)) {
throw new Error(ERRORS.INVALID_TIME);
}
const orders = slot?.orders;
if (
orders?.length &&
orders?.some((order) => order?.state && FORBIDDEN_ORDER_STATES.includes(order.state))
) {
throw new Error(ERRORS.HAS_ORDERS);
}
const { documentId } = slot;
const overlappingEntities = await this.getSlots({
filters: {
datetime_end: { gt: datetime_start },
datetime_start: { lt: datetime_end },
documentId: { not: { eq: documentId } },
},
});
if (overlappingEntities?.slots?.length) {
throw new Error(ERRORS.OVERLAPPING_TIME);
}
}
private async checkPermission(
variables: Pick<VariablesOf<typeof GQL.GetSlotDocument>, 'documentId'>,
) {
const { customer } = await this._getUser();
const { slot } = await this.getSlot({ documentId: variables.documentId });
if (!slot) throw new Error(ERRORS.SLOT_NOT_FOUND);
if (slot?.master?.documentId !== customer?.documentId) throw new Error(ERRORS.NO_PERMISSION);
}
}