Vlad Chikalkin c5799a7f00
Feature/orders (#25)
* add contacts scroller

* add service select

* add calendar & time picker

* context/order: add masterId

* Revert "context/order: add masterId"

This reverts commit d5d07d7b2f5b6673a621a30b00ad087c60675a3f.

* components/order-form: add back button

* disable submit button if no customer selected

* disable submit button if no service selected

* service component: comment span

* save selected date to context

* fix calendar padding

* hooks/slot: rename index -> master

* slot list: render immediately

* fix step components rendering

* add check icon for masters

* Revert "add check icon for masters"

This reverts commit cc81a9a504918ebbffcca8d035c7c4984f109957.

* prepare for split contacts grid into masters/clients grid

* create MastersGrid & master-select step

* optimize useCustomerContacts

* add ClientsGrid & 'client-select' step

* add self to masters list & border avatar

* context/order: split into files

* hooks/profile: allow pass empty args to useProfileQuery/useProfileMutation

* context/order: skip client-select in client steps

* packages: upgrade next@15.3.0

* .vscode: add launch.json

* back-button: fix steps using

* contacts: skip client step for client

* fix react types

* ServiceSelect: fix padding

* Revert "contacts: skip client step for client"

This reverts commit db9af07dab9df9428561a1952f5a2c91c5b9d88d.

* fix steps for client & master

* split datetime-select into files

* improve useSlots hook

* migrate from order context to zustand store

* pass order store via context

* fix submit button not working

* skip master select for master & client select for client

* select time feature & get final order values

* apps/web: rename actions/service -> actions/services

* create order works!

* split next-button into two buttons

* add result pages (success, error)

* packages/graphql: add eslint

* merge branch 'refactor-api' (#23)

* refactor customer api

* refactor slots api

* hooks/customers: use invalidateQueries

* refactor services api

* optimize hooks queryKey

* refactor orders api

* typo refactor hooks

* fix telegramId type (number)

* fix bot with new api

* rename customers masters & clients query

* fix useClientsQuery & useMastersQuery query

* new line after 'use client' & 'use server' directives

* move getAvailableTimeSlots to server

* getAvailableTimeSlots: add filter by orders

* take into service duration when computing times

* fix GetSlotsOrders order

* take into existing orders when computing times

* fix build

* app/orders: fill page with content

* stores/order: split into slices

* components/orders: remove nested components dirs

* move order store -> orders\order-store

* replace ScheduleTimeContext with ScheduleStore

* fix slots queries

* context: rename contexts properly

* finally organized stores & context

* move order-card & time-range to @/components/shared

* Refactor/components folder structure (#24)

* refactor components/navigation

* refactor components/orders

* refactor components/profile

* refactor components/schedule

* remove components/common/spinner

* add launch.json

* add horizontal calendar

* remove context/date.tsx

* optimize orders list fetching

* add numberOfDaysBefore param

* fix orders list in slot page

* graphql/api: remove throw new Error

* horizontal-calendar: switch months by arrow buttons

* SlotCard: use SlotComponentProps type

* stores/schedule: export useScheduleStore

* SlotPage: add page header title

* contacts: mark inactive contacts

* prefetchQuery customer profile pages

* fix create slot

* packages: radash -> radashi

* fix queries, using formatDate & formatTime on client

* graphql: remove rename operations files

* fix create order query

* fix show actual slot status after slot update

* order page

* slot page: replace buttons with floating panel

* fix blur & colors

* fix floating panel overflows content

* hide ClientsOrdersList for non masters

* hooks/services: rename input -> variables

* move OrderCard types close to component

* exact types for Slot components & page

* app/profile: show shared orders

* order-services: fix types

* order page: add buttons

* order-card: add colors

* add order status alert

* fix badges & alerts

* take into account cancelled and completed orders in the slot list

* action panel: hide if no handlers

* highlight days with slots in schedule calendar

* highlight days in horizontal calendar

* remove getSlotsOrders fn

* show masters avatar in orders list

* fix auth redirects

* fix orders list for client

* create useIsMaster hook to prevent duplication

* order: revert cancel button for master

* FloatingActionPanel: block buttons while pending request

* hooks: invalidate orders & slots after mutate & delete

* order: revert approve button for master

* api/orders: protect update order

* order-card: show date

* order-card: add showDate variables in props

* order: add repeat button

* disable dashboard button

* apps/bot: beautify messages

* order: notify to telegram messages

* orderUpdate: add status info
2025-06-27 13:44:17 +03:00

149 lines
4.2 KiB
TypeScript

import { getClientWithToken } from '../apollo/client';
import * as GQL from '../types';
import { BaseService } from './base';
import { CustomersService } from './customers';
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 class SlotsService extends BaseService {
async createSlot(variables: VariablesOf<typeof GQL.CreateSlotDocument>) {
const customerService = new CustomersService(this.customer);
const { customer } = await customerService.getCustomer(this.customer);
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>) {
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 },
) {
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.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 serviceDuration = 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;
let startTime = dayjs(`${slot.date} ${slot.time_start}`);
const endTime = dayjs(`${slot.date} ${slot.time_end}`).subtract(serviceDuration, 'minutes');
while (startTime.valueOf() <= endTime.valueOf()) {
const slotStartTime = startTime;
const potentialEndTime = startTime.add(serviceDuration, 'minutes');
const hasConflict = slot.orders.some(
(order) =>
order?.state &&
![GQL.Enum_Order_State.Cancelled, GQL.Enum_Order_State.Completed].includes(
order.state,
) &&
slotStartTime.isBefore(dayjs(`${slot.date} ${order.time_end}`)) &&
potentialEndTime.isAfter(dayjs(`${slot.date} ${order.time_start}`)),
);
if (!hasConflict) {
times.push({ slotId: slot.documentId, time: startTime.format('HH:mm') });
}
startTime = startTime.add(15, 'minutes');
}
}
return { 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>) {
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;
}
}