diff --git a/apps/web/components/orders/components/datetime-select/components/time-select.tsx b/apps/web/components/orders/components/datetime-select/components/time-select.tsx index 4798c9f..fe6a39b 100644 --- a/apps/web/components/orders/components/datetime-select/components/time-select.tsx +++ b/apps/web/components/orders/components/datetime-select/components/time-select.tsx @@ -7,19 +7,29 @@ import { Button } from '@repo/ui/components/ui/button'; export function TimeSelect() { const masterId = useOrderStore((store) => store.masterId); const date = useOrderStore((store) => store.date); + const serviceId = useOrderStore((store) => store.serviceId); - const { data: { times } = {}, isLoading } = useAvailableTimeSlotsQuery({ - filters: { - date: { - eq: date, - }, - master: { - documentId: { - eq: masterId, + const { data: { times } = {}, isLoading } = useAvailableTimeSlotsQuery( + { + filters: { + date: { + eq: date, + }, + master: { + documentId: { + eq: masterId, + }, }, }, }, - }); + { + service: { + documentId: { + eq: serviceId, + }, + }, + }, + ); if (isLoading || !times) return null; diff --git a/apps/web/hooks/api/slots.ts b/apps/web/hooks/api/slots.ts index 301a342..eee437c 100644 --- a/apps/web/hooks/api/slots.ts +++ b/apps/web/hooks/api/slots.ts @@ -33,11 +33,12 @@ export const useSlotQuery = (variables: Parameters[0]) => { }; export const useAvailableTimeSlotsQuery = ( - variables: Parameters[0], + ...variables: Parameters ) => { return useQuery({ - queryFn: () => getAvailableTimeSlots(variables), + queryFn: () => getAvailableTimeSlots(...variables), queryKey: ['available-time-slots', variables], + staleTime: 15 * 1_000, }); }; diff --git a/packages/graphql/api/slots.ts b/packages/graphql/api/slots.ts index 908f811..7ebcd3a 100644 --- a/packages/graphql/api/slots.ts +++ b/packages/graphql/api/slots.ts @@ -1,8 +1,9 @@ import { getClientWithToken } from '../apollo/client'; import * as GQL from '../types'; -import { formatDate, formatTime } from '../utils/datetime-format'; +import { formatDate, formatTime, getMinutes } from '../utils/datetime-format'; import { BaseService } from './base'; import { CustomersService } from './customers'; +import { ServicesService } from './services'; import { type VariablesOf } from '@graphql-typed-document-node/core'; import dayjs from 'dayjs'; @@ -20,8 +21,8 @@ export class SlotsService extends BaseService { ...variables, date: formatDate(variables.input.date).db(), master: customer?.documentId, - time_end: formatTime(variables.input.time_end).db(), - time_start: formatTime(variables.input.time_start).db(), + time_end: variables.input.time_end && formatTime(variables.input.time_end).db(), + time_start: variables.input.time_start && formatTime(variables.input.time_start).db(), }, }); @@ -45,13 +46,13 @@ export class SlotsService extends BaseService { return mutationResult.data; } - async getAvailableTimeSlots(variables: VariablesOf) { + async getAvailableTimeSlots( + variables: VariablesOf, + context: { service: GQL.ServiceFiltersInput }, + ) { const { query } = await getClientWithToken(); - const { - data: { slots: openedSlots }, - error, - } = await query({ + const getSlotsResult = await query({ query: GQL.GetSlotsOrdersDocument, variables: { filters: { @@ -62,36 +63,33 @@ export class SlotsService extends BaseService { }, }); - if (error) throw new Error(error.message); + if (getSlotsResult.error) throw new Error(getSlotsResult.error.message); + + const servicesService = new ServicesService(this.customer); + + if (!context?.service?.documentId?.eq) throw new Error('Missing service id'); + + const { service } = await servicesService.getService({ + documentId: context.service.documentId.eq, + }); + + if (!service) throw new Error('Service not found'); + + const duration = getMinutes(service.duration); + + const openedSlots = getSlotsResult.data.slots; const times: Array<{ slotId: string; time: string }> = []; for (const slot of openedSlots) { if (!slot?.time_start || !slot?.time_end) continue; - const orders = slot.orders ?? []; + let startTime = dayjs(`${slot.date} ${slot.time_start}`); + const endTime = dayjs(`${slot.date} ${slot.time_end}`).subtract(duration, 'minutes'); - let currentTime = dayjs(`${slot.date} ${slot.time_start}`); - const endTime = dayjs(`${slot.date} ${slot.time_end}`); - - while (currentTime.isBefore(endTime)) { - const slotStart = currentTime; - const slotEnd = currentTime.add(30, 'minute'); - - const overlaps = orders.some((order) => { - if (!order?.time_start || !order?.time_end) return false; - - const orderStart = dayjs(`${slot.date} ${order.time_start}`); - const orderEnd = dayjs(`${slot.date} ${order.time_end}`); - - return slotStart.isBefore(orderEnd) && slotEnd.isAfter(orderStart); - }); - - if (!overlaps) { - times.push({ slotId: slot.documentId, time: slotStart.format('HH:mm') }); - } - - currentTime = slotEnd; + while (startTime.valueOf() <= endTime.valueOf()) { + times.push({ slotId: slot.documentId, time: startTime.format('HH:mm') }); + startTime = startTime.add(15, 'minutes'); } } diff --git a/packages/graphql/graphql.config.cjs b/packages/graphql/graphql.config.cjs index f575ce9..1c82209 100644 --- a/packages/graphql/graphql.config.cjs +++ b/packages/graphql/graphql.config.cjs @@ -9,6 +9,7 @@ module.exports = { onlyOperationTypes: true, scalars: { Long: 'number', + Time: 'string', }, useTypeImports: true, }, diff --git a/packages/graphql/types/operations.generated.ts b/packages/graphql/types/operations.generated.ts index 2ff3726..91ebddc 100644 --- a/packages/graphql/types/operations.generated.ts +++ b/packages/graphql/types/operations.generated.ts @@ -17,7 +17,7 @@ export type Scalars = { DateTime: { input: any; output: any; } JSON: { input: any; output: any; } Long: { input: number; output: number; } - Time: { input: any; output: any; } + Time: { input: string; output: string; } }; export type BlockFiltersInput = { @@ -695,67 +695,67 @@ export type UpdateCustomerMutationVariables = Exact<{ export type UpdateCustomerMutation = { __typename?: 'Mutation', updateCustomer?: { __typename?: 'Customer', active?: boolean | null | undefined, documentId: string, name: string, phone: string, photoUrl?: string | null | undefined, role: Enum_Customer_Role, telegramId?: number | null | undefined } | null | undefined }; -export type OrderFieldsFragment = { __typename?: 'Order', documentId: string, time_start?: any | null | undefined, time_end?: any | null | undefined, state?: Enum_Order_State | null | undefined, order_number?: number | null | undefined, services: Array<{ __typename?: 'Service', documentId: string, name?: string | null | undefined } | null | undefined>, client?: { __typename?: 'Customer', name: string, documentId: string, photoUrl?: string | null | undefined } | null | undefined }; +export type OrderFieldsFragment = { __typename?: 'Order', documentId: string, time_start?: string | null | undefined, time_end?: string | null | undefined, state?: Enum_Order_State | null | undefined, order_number?: number | null | undefined, services: Array<{ __typename?: 'Service', documentId: string, name?: string | null | undefined } | null | undefined>, client?: { __typename?: 'Customer', name: string, documentId: string, photoUrl?: string | null | undefined } | null | undefined }; export type GetOrderQueryVariables = Exact<{ documentId: Scalars['ID']['input']; }>; -export type GetOrderQuery = { __typename?: 'Query', order?: { __typename?: 'Order', documentId: string, time_start?: any | null | undefined, time_end?: any | null | undefined, state?: Enum_Order_State | null | undefined, order_number?: number | null | undefined, services: Array<{ __typename?: 'Service', documentId: string, name?: string | null | undefined } | null | undefined>, client?: { __typename?: 'Customer', name: string, documentId: string, photoUrl?: string | null | undefined } | null | undefined } | null | undefined }; +export type GetOrderQuery = { __typename?: 'Query', order?: { __typename?: 'Order', documentId: string, time_start?: string | null | undefined, time_end?: string | null | undefined, state?: Enum_Order_State | null | undefined, order_number?: number | null | undefined, services: Array<{ __typename?: 'Service', documentId: string, name?: string | null | undefined } | null | undefined>, client?: { __typename?: 'Customer', name: string, documentId: string, photoUrl?: string | null | undefined } | null | undefined } | null | undefined }; export type CreateOrderMutationVariables = Exact<{ input: OrderInput; }>; -export type CreateOrderMutation = { __typename?: 'Mutation', createOrder?: { __typename?: 'Order', documentId: string, time_start?: any | null | undefined, time_end?: any | null | undefined, state?: Enum_Order_State | null | undefined, order_number?: number | null | undefined, services: Array<{ __typename?: 'Service', documentId: string, name?: string | null | undefined } | null | undefined>, client?: { __typename?: 'Customer', name: string, documentId: string, photoUrl?: string | null | undefined } | null | undefined } | null | undefined }; +export type CreateOrderMutation = { __typename?: 'Mutation', createOrder?: { __typename?: 'Order', documentId: string, time_start?: string | null | undefined, time_end?: string | null | undefined, state?: Enum_Order_State | null | undefined, order_number?: number | null | undefined, services: Array<{ __typename?: 'Service', documentId: string, name?: string | null | undefined } | null | undefined>, client?: { __typename?: 'Customer', name: string, documentId: string, photoUrl?: string | null | undefined } | null | undefined } | null | undefined }; -export type ServiceFieldsFragment = { __typename?: 'Service', documentId: string, name?: string | null | undefined, duration: any }; +export type ServiceFieldsFragment = { __typename?: 'Service', documentId: string, name?: string | null | undefined, duration: string }; export type GetServicesQueryVariables = Exact<{ filters?: InputMaybe; }>; -export type GetServicesQuery = { __typename?: 'Query', services: Array<{ __typename?: 'Service', documentId: string, name?: string | null | undefined, duration: any } | null | undefined> }; +export type GetServicesQuery = { __typename?: 'Query', services: Array<{ __typename?: 'Service', documentId: string, name?: string | null | undefined, duration: string } | null | undefined> }; export type GetServiceQueryVariables = Exact<{ documentId: Scalars['ID']['input']; }>; -export type GetServiceQuery = { __typename?: 'Query', service?: { __typename?: 'Service', documentId: string, name?: string | null | undefined, duration: any } | null | undefined }; +export type GetServiceQuery = { __typename?: 'Query', service?: { __typename?: 'Service', documentId: string, name?: string | null | undefined, duration: string } | null | undefined }; -export type SlotFieldsFragment = { __typename?: 'Slot', documentId: string, date?: any | null | undefined, time_start: any, time_end: any, state?: Enum_Slot_State | null | undefined }; +export type SlotFieldsFragment = { __typename?: 'Slot', documentId: string, date?: any | null | undefined, time_start: string, time_end: string, state?: Enum_Slot_State | null | undefined }; export type CreateSlotMutationVariables = Exact<{ input: SlotInput; }>; -export type CreateSlotMutation = { __typename?: 'Mutation', createSlot?: { __typename?: 'Slot', documentId: string, date?: any | null | undefined, time_start: any, time_end: any, state?: Enum_Slot_State | null | undefined } | null | undefined }; +export type CreateSlotMutation = { __typename?: 'Mutation', createSlot?: { __typename?: 'Slot', documentId: string, date?: any | null | undefined, time_start: string, time_end: string, state?: Enum_Slot_State | null | undefined } | null | undefined }; export type GetSlotsQueryVariables = Exact<{ filters?: InputMaybe; }>; -export type GetSlotsQuery = { __typename?: 'Query', slots: Array<{ __typename?: 'Slot', documentId: string, date?: any | null | undefined, time_start: any, time_end: any, state?: Enum_Slot_State | null | undefined } | null | undefined> }; +export type GetSlotsQuery = { __typename?: 'Query', slots: Array<{ __typename?: 'Slot', documentId: string, date?: any | null | undefined, time_start: string, time_end: string, state?: Enum_Slot_State | null | undefined } | null | undefined> }; export type GetSlotsOrdersQueryVariables = Exact<{ filters?: InputMaybe; }>; -export type GetSlotsOrdersQuery = { __typename?: 'Query', slots: Array<{ __typename?: 'Slot', documentId: string, date?: any | null | undefined, time_start: any, time_end: any, state?: Enum_Slot_State | null | undefined, orders: Array<{ __typename?: 'Order', documentId: string, time_start?: any | null | undefined, time_end?: any | null | undefined } | null | undefined> } | null | undefined> }; +export type GetSlotsOrdersQuery = { __typename?: 'Query', slots: Array<{ __typename?: 'Slot', documentId: string, date?: any | null | undefined, time_start: string, time_end: string, state?: Enum_Slot_State | null | undefined, orders: Array<{ __typename?: 'Order', documentId: string, time_start?: string | null | undefined, time_end?: string | null | undefined } | null | undefined> } | null | undefined> }; export type GetSlotQueryVariables = Exact<{ documentId: Scalars['ID']['input']; }>; -export type GetSlotQuery = { __typename?: 'Query', slot?: { __typename?: 'Slot', documentId: string, date?: any | null | undefined, time_start: any, time_end: any, state?: Enum_Slot_State | null | undefined, orders: Array<{ __typename?: 'Order', documentId: string, time_start?: any | null | undefined, time_end?: any | null | undefined } | null | undefined>, master?: { __typename?: 'Customer', documentId: string } | null | undefined } | null | undefined }; +export type GetSlotQuery = { __typename?: 'Query', slot?: { __typename?: 'Slot', documentId: string, date?: any | null | undefined, time_start: string, time_end: string, state?: Enum_Slot_State | null | undefined, orders: Array<{ __typename?: 'Order', documentId: string, time_start?: string | null | undefined, time_end?: string | null | undefined } | null | undefined>, master?: { __typename?: 'Customer', documentId: string } | null | undefined } | null | undefined }; export type UpdateSlotMutationVariables = Exact<{ documentId: Scalars['ID']['input']; @@ -763,7 +763,7 @@ export type UpdateSlotMutationVariables = Exact<{ }>; -export type UpdateSlotMutation = { __typename?: 'Mutation', updateSlot?: { __typename?: 'Slot', documentId: string, date?: any | null | undefined, time_start: any, time_end: any, state?: Enum_Slot_State | null | undefined } | null | undefined }; +export type UpdateSlotMutation = { __typename?: 'Mutation', updateSlot?: { __typename?: 'Slot', documentId: string, date?: any | null | undefined, time_start: string, time_end: string, state?: Enum_Slot_State | null | undefined } | null | undefined }; export type DeleteSlotMutationVariables = Exact<{ documentId: Scalars['ID']['input']; diff --git a/packages/graphql/utils/datetime-format.ts b/packages/graphql/utils/datetime-format.ts index e33377b..790b161 100644 --- a/packages/graphql/utils/datetime-format.ts +++ b/packages/graphql/utils/datetime-format.ts @@ -34,6 +34,11 @@ export function formatTime(time: string) { }; } +export function getMinutes(time: string) { + const [hours = '00', minutes = '00'] = time.split(':'); + return Number.parseInt(hours, 10) * 60 + Number.parseInt(minutes, 10); +} + export function sumTime(time1: string, time2: string) { const [hours1 = '00', minutes1 = '00'] = time1.split(':'); const [hours2 = '00', minutes2 = '00'] = time2.split(':');