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

165 lines
5.3 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 */
import { getClientWithToken } from '../apollo/client';
import * as GQL from '../types';
import { Enum_Customer_Role, Enum_Slot_State } from '../types';
import { BaseService } from './base';
import { CustomersService } from './customers';
import { NotifyService } from './notify';
import { ServicesService } from './services';
import { SlotsService } from './slots';
import { type VariablesOf } from '@graphql-typed-document-node/core';
import { isCustomerMaster } from '@repo/utils/customer';
import { formatTime, sumTime } from '@repo/utils/datetime-format';
const ERRORS = {
INVALID_CLIENT: 'Invalid client',
INVALID_MASTER: 'Invalid master',
INVALID_SERVICE_DURATION: 'Invalid service duration',
MISSING_CLIENT: 'Missing client id',
MISSING_ORDER: 'Order not found',
MISSING_SERVICE_ID: 'Missing service id',
MISSING_SERVICES: 'Missing services',
MISSING_SLOT: 'Missing slot id',
MISSING_START_TIME: 'Missing time start',
MISSING_USER: 'User not found',
NO_PERMISSION: 'No permission',
SLOT_CLOSED: 'Slot is closed',
};
export class OrdersService extends BaseService {
async createOrder(variables: {
input: Omit<VariablesOf<typeof GQL.CreateOrderDocument>['input'], 'time_end'>;
}) {
if (!variables.input.slot) throw new Error(ERRORS.MISSING_SLOT);
if (!variables.input.client) throw new Error(ERRORS.MISSING_CLIENT);
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.time_start) throw new Error(ERRORS.MISSING_START_TIME);
const customersService = new CustomersService(this.customer);
const slotsService = new SlotsService(this.customer);
const servicesService = new ServicesService(this.customer);
const { customer } = await customersService.getCustomer(this.customer);
const { slot } = await slotsService.getSlot({ documentId: variables.input.slot });
if (slot?.state === Enum_Slot_State.Closed) {
throw new Error(ERRORS.SLOT_CLOSED);
}
if (customer?.role === Enum_Customer_Role.Client) {
if (customer.documentId !== variables.input.client) {
throw new Error(ERRORS.INVALID_CLIENT);
}
const masters = await customersService.getMasters(this.customer);
const masterId = slot?.master?.documentId;
if (!masters.customers.some((master) => master?.documentId === masterId)) {
throw new Error(ERRORS.INVALID_MASTER);
}
}
if (
customer?.role === Enum_Customer_Role.Master &&
slot?.master?.documentId !== customer.documentId
) {
throw new Error(ERRORS.INVALID_MASTER);
}
const { service } = await servicesService.getService({
documentId: variables.input.services[0],
});
if (!service?.duration) throw new Error(ERRORS.INVALID_SERVICE_DURATION);
const endTime = sumTime(variables.input.time_start, service?.duration);
const { mutate } = await getClientWithToken();
const mutationResult = await mutate({
mutation: GQL.CreateOrderDocument,
variables: {
...variables,
input: {
...variables.input,
time_end: formatTime(endTime).db(),
},
},
});
const error = mutationResult.errors?.at(0);
if (error) throw new Error(error.message);
// Уведомление об создании заказа
const notifyService = new NotifyService(this.customer);
notifyService.orderCreated(variables);
return mutationResult.data;
}
async getOrder(variables: VariablesOf<typeof GQL.GetOrderDocument>) {
const { query } = await getClientWithToken();
const result = await query({
query: GQL.GetOrderDocument,
variables,
});
return result.data;
}
async getOrders(variables: VariablesOf<typeof GQL.GetOrdersDocument>) {
const { query } = await getClientWithToken();
const result = await query({
query: GQL.GetOrdersDocument,
variables,
});
return result.data;
}
async updateOrder(variables: VariablesOf<typeof GQL.UpdateOrderDocument>) {
const customersService = new CustomersService(this.customer);
const { customer } = await customersService.getCustomer(this.customer);
if (!customer) throw new Error(ERRORS.MISSING_USER);
const { query } = await getClientWithToken();
const {
data: { order },
} = await query({
query: GQL.GetOrderDocument,
variables: { documentId: variables.documentId },
});
if (!order) throw new Error(ERRORS.MISSING_ORDER);
const isMaster = isCustomerMaster(customer);
const hasPermission =
(isMaster && order.slot?.master?.documentId === customer.documentId) ||
(!isMaster && order.client?.documentId === customer.documentId);
if (!hasPermission) throw new Error(ERRORS.NO_PERMISSION);
const { mutate } = await getClientWithToken();
const mutationResult = await mutate({
mutation: GQL.UpdateOrderDocument,
variables,
});
const error = mutationResult.errors?.at(0);
if (error) throw new Error(error.message);
// Уведомление об изменении заказа
const notifyService = new NotifyService(this.customer);
notifyService.orderUpdated(variables);
return mutationResult.data;
}
}