Compare commits

...

10 Commits

Author SHA1 Message Date
vchikalkin
1528cc25b8 create order works! 2025-05-08 19:30:00 +03:00
vchikalkin
24fb2103f7 apps/web: rename actions/service -> actions/services 2025-05-08 16:47:02 +03:00
vchikalkin
3738c4e2a9 select time feature & get final order values 2025-05-07 17:33:35 +03:00
vchikalkin
7fcf67eece skip master select for master & client select for client 2025-05-06 13:14:48 +03:00
vchikalkin
b5306357c8 fix submit button not working 2025-05-06 10:57:46 +03:00
vchikalkin
7e886172f2 pass order store via context 2025-04-30 18:58:46 +03:00
vchikalkin
e6f2e6ccaf migrate from order context to zustand store 2025-04-29 17:48:11 +03:00
vchikalkin
2bc7607800 improve useSlots hook 2025-04-16 17:19:22 +03:00
vchikalkin
7e143b3054 split datetime-select into files 2025-04-16 16:03:58 +03:00
vchikalkin
1e6718508a fix steps for client & master 2025-04-16 15:22:25 +03:00
40 changed files with 526 additions and 321 deletions

View File

@ -1,4 +1,69 @@
/* eslint-disable canonical/id-match */
'use server';
// eslint-disable-next-line sonarjs/no-internal-api-use
import type * as ApolloTypes from '../../../packages/graphql/node_modules/@apollo/client/core';
import { getProfile } from './profile';
import { formatDate, formatTime, sumTime } from '@/utils/date';
import * as api from '@repo/graphql/api';
import { Enum_Customer_Role, Enum_Slot_State } from '@repo/graphql/types';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type FixTypescriptCringe = ApolloTypes.FetchResult;
export const getOrder = api.getOrder;
type OrderInput = {
clientId: string;
date: Date;
serviceId: string;
slotId: string;
time: string;
};
export async function createOrder(input: OrderInput) {
const customer = await getProfile();
if (!input.slotId) {
throw new Error('Missing slot');
}
const { data } = await api.getSlot({ documentId: input.slotId });
if (data.slot?.state === Enum_Slot_State.Closed) {
throw new Error('Slot is closed');
}
if (customer.role === Enum_Customer_Role.Client) {
if (customer.documentId !== input.clientId) {
throw new Error('Invalid client');
}
const masterId = data.slot?.master?.documentId;
const masters = await api.getCustomerMasters(customer);
if (!masters.data.customers.some((master) => master?.documentId === masterId)) {
throw new Error('Invalid master');
}
}
if (
customer.role === Enum_Customer_Role.Master &&
data.slot?.master?.documentId !== customer.documentId
) {
throw new Error('Invalid master');
}
const service = await api.getService({ documentId: input.serviceId });
const endTime = sumTime(input.time, service?.data?.service?.duration);
const payload = {
client: input.clientId,
date: formatDate(input.date).db(),
services: [input.serviceId],
slot: input.slotId,
time_end: formatTime(endTime).db(),
time_start: formatTime(input.time).db(),
};
return api.createOrder(payload);
}

View File

@ -1,8 +1,8 @@
'use server';
// eslint-disable-next-line sonarjs/no-internal-api-use
import type * as ApolloTypes from '../../../packages/graphql/node_modules/@apollo/client/core';
import { getProfile } from './profile';
import * as api from '@repo/graphql/api/service';
// eslint-disable-next-line sonarjs/no-internal-api-use
import type * as ApolloTypes from '@repo/graphql/node_modules/@apollo/client/core';
import { type GetServicesQueryVariables } from '@repo/graphql/types';
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -28,12 +28,12 @@ export async function getSlots(input: GQL.GetSlotsQueryVariables) {
return api.getSlots({
filters: {
...input.filters,
master: {
documentId: {
eq: customer.documentId,
},
},
...input.filters,
},
});
}

View File

@ -1,13 +1,13 @@
'use client';
import { OrderContext } from '@/context/order';
import { useOrderStore } from '@/stores/order';
import { Button } from '@repo/ui/components/ui/button';
import { use } from 'react';
export function BackButton() {
const { prevStep, step } = use(OrderContext);
const step = useOrderStore((store) => store.step);
const previousStep = useOrderStore((store) => store.prevStep);
function handleOnClick() {
prevStep();
previousStep();
}
if (['master-select', 'success'].includes(step)) return null;

View File

@ -2,14 +2,15 @@
import { ContactsGridBase } from './components';
import { LoadingSpinner } from '@/components/common/spinner';
import { ContactsFilterProvider } from '@/context/contacts-filter';
import { OrderContext } from '@/context/order';
import { useCustomerContacts } from '@/hooks/contacts';
import { useOrderStore } from '@/stores/order';
import { withContext } from '@/utils/context';
import { use, useEffect } from 'react';
import { useEffect } from 'react';
export const MastersGrid = withContext(ContactsFilterProvider)(function () {
const { contacts, isLoading, setFilter } = useCustomerContacts({ includeSelf: true });
const { masterId, setMasterId } = use(OrderContext);
const masterId = useOrderStore((store) => store.masterId);
const setMasterId = useOrderStore((store) => store.setMasterId);
useEffect(() => {
setFilter('masters');
@ -29,7 +30,8 @@ export const MastersGrid = withContext(ContactsFilterProvider)(function () {
export const ClientsGrid = withContext(ContactsFilterProvider)(function () {
const { contacts, isLoading, setFilter } = useCustomerContacts();
const { clientId, setClientId } = use(OrderContext);
const clientId = useOrderStore((store) => store.clientId);
const setClientId = useOrderStore((store) => store.setClientId);
useEffect(() => {
setFilter('clients');

View File

@ -1,79 +0,0 @@
'use client';
import { OrderContext } from '@/context/order';
import { Button } from '@repo/ui/components/ui/button';
import { Calendar } from '@repo/ui/components/ui/calendar';
import dayjs, { type Dayjs } from 'dayjs';
import { use } from 'react';
type TimeSlotsProps = {
endTime?: string;
interval?: number;
startTime?: string;
};
export function DateSelect() {
const { date: selectedDate, setDate } = use(OrderContext);
return (
<Calendar
className="bg-background"
disabled={(date) => {
return dayjs().isAfter(dayjs(date), 'day');
}}
mode="single"
onSelect={(date) => {
if (date) setDate(date);
}}
selected={selectedDate}
/>
);
}
const generateTimeSlots = (start: string, end: string, interval: number): Dayjs[] => {
const times: Dayjs[] = [];
let currentTime = dayjs(start, 'HH:mm');
const endTime = dayjs(end, 'HH:mm');
while (currentTime.isBefore(endTime) || currentTime.isSame(endTime)) {
times.push(currentTime);
currentTime = currentTime.add(interval, 'minute');
}
return times;
};
export function TimeSelect({
endTime = '2025-03-10T20:00:00',
interval = 30,
startTime = '2025-03-10T09:00:00',
}: Readonly<TimeSlotsProps>) {
const timeSlots = generateTimeSlots(startTime, endTime, interval);
const morning = timeSlots.filter((time) => time.hour() < 12);
const afternoon = timeSlots.filter((time) => time.hour() >= 12 && time.hour() < 18);
const evening = timeSlots.filter((time) => time.hour() >= 18);
return (
<div className="space-y-2">
<TimeSlotsButtons times={morning} title="Утро" />
<TimeSlotsButtons times={afternoon} title="День" />
<TimeSlotsButtons times={evening} title="Вечер" />
</div>
);
}
function TimeSlotsButtons({ times, title }: Readonly<{ times: Dayjs[]; title: string }>) {
if (!times.length) return null;
return (
<div className="space-y-2">
<h2 className="text-lg font-semibold">{title}</h2>
<div className="grid grid-cols-3 gap-2">
{times.map((time) => (
<Button className="mb-2" key={time.toString()} variant="outline">
{time.format('HH:mm')}
</Button>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,27 @@
'use client';
import { useOrderStore } from '@/stores/order';
import { Calendar } from '@repo/ui/components/ui/calendar';
import dayjs from 'dayjs';
export function DateSelect() {
const selectedDate = useOrderStore((store) => store.date);
const setDate = useOrderStore((store) => store.setDate);
const setTime = useOrderStore((store) => store.setTime);
const setSlot = useOrderStore((store) => store.setSlotId);
return (
<Calendar
className="bg-background"
disabled={(date) => {
return dayjs().isAfter(dayjs(date), 'day');
}}
mode="single"
onSelect={(date) => {
if (date) setDate(date);
setTime(null);
setSlot(null);
}}
selected={selectedDate}
/>
);
}

View File

@ -0,0 +1,75 @@
/* eslint-disable canonical/id-match */
'use client';
import { useSlots } from '@/hooks/slots';
import { useOrderStore } from '@/stores/order';
import { Enum_Slot_State, type SlotFieldsFragment } from '@repo/graphql/types';
import { Button } from '@repo/ui/components/ui/button';
import dayjs, { type Dayjs } from 'dayjs';
import { sift } from 'radash';
const generateTimeSlots = (slots: SlotFieldsFragment[]): Array<{ slotId: string; time: Dayjs }> => {
const times: Array<{ slotId: string; time: Dayjs }> = [];
for (const slot of slots) {
let currentTime = dayjs(`${slot.date} ${slot.time_start}`);
const endTime = dayjs(`${slot.date} ${slot.time_end}`);
while (currentTime.isBefore(endTime) || currentTime.isSame(endTime)) {
times.push({ slotId: slot.documentId, time: currentTime });
currentTime = currentTime.add(30, 'minute');
}
}
return times;
};
export function TimeSelect() {
const masterId = useOrderStore((store) => store.masterId);
const date = useOrderStore((store) => store.date);
const { data } = useSlots({ date, masterId });
const openedSlots = data?.data.slots.filter((slot) => slot?.state === Enum_Slot_State.Open);
const timeSlots = generateTimeSlots(openedSlots ? sift(openedSlots) : []);
const morning = timeSlots.filter((time) => time.time.hour() < 12);
const afternoon = timeSlots.filter((time) => time.time.hour() >= 12 && time.time.hour() < 18);
const evening = timeSlots.filter((time) => time.time.hour() >= 18);
return (
<div className="space-y-2">
<TimeSlotsButtons times={morning} title="Утро" />
<TimeSlotsButtons times={afternoon} title="День" />
<TimeSlotsButtons times={evening} title="Вечер" />
</div>
);
}
function TimeSlotsButtons({
times,
title,
}: Readonly<{ times: Array<{ slotId: string; time: Dayjs }>; title: string }>) {
const setTime = useOrderStore((store) => store.setTime);
const setSlot = useOrderStore((store) => store.setSlotId);
if (!times.length) return null;
return (
<div className="space-y-2">
<h2 className="text-lg font-semibold">{title}</h2>
<div className="grid grid-cols-3 gap-2">
{times.map(({ slotId, time }) => (
<Button
className="mb-2"
key={time.toString()}
onClick={() => {
setTime(time.format('HH:mm'));
setSlot(slotId);
}}
variant="outline"
>
{time.format('HH:mm')}
</Button>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,11 @@
import { DateSelect } from './components/date-select';
import { TimeSelect } from './components/time-select';
export function DateTimeSelect() {
return (
<div className="space-y-4">
<DateSelect />
<TimeSelect />
</div>
);
}

View File

@ -1,5 +1,5 @@
export * from './back-button';
export * from './contacts-grid';
export * from './datetime-select';
export * from './next-button';
export * from './service-select';
export * from './submit-button';

View File

@ -0,0 +1,58 @@
/* eslint-disable sonarjs/no-duplicate-string */
'use client';
import { useOrderCreate } from '@/hooks/orders';
import { useOrderStore } from '@/stores/order';
import { Button } from '@repo/ui/components/ui/button';
import { useEffect } from 'react';
export function NextButton() {
const { clientId, date, nextStep, serviceId, setStep, slotId, step, time } = useOrderStore(
(store) => store,
);
const disabled = useButtonDisabled();
const { isPending, isSuccess, mutate: createOrder } = useOrderCreate();
useEffect(() => {
if (isSuccess) setStep('success');
}, [isSuccess, setStep]);
function handleOnClick() {
if (step !== 'datetime-select') return nextStep();
if (!clientId || !date || !serviceId || !slotId || !time) return null;
return createOrder({ clientId, date, serviceId, slotId, time });
}
if (isPending) {
return (
<Button className="w-full" disabled type="button">
Загрузка...
</Button>
);
}
return (
<Button className="w-full" disabled={disabled} onClick={() => handleOnClick()} type="button">
{step === 'datetime-select' ? 'Завершить' : 'Далее'}
</Button>
);
}
function useButtonDisabled() {
const step = useOrderStore((state) => state.step);
const clientId = useOrderStore((state) => state.clientId);
const masterId = useOrderStore((state) => state.masterId);
const serviceId = useOrderStore((state) => state.serviceId);
const date = useOrderStore((state) => state.date);
const time = useOrderStore((state) => state.time);
return (
(step === 'master-select' && !masterId) ||
(step === 'client-select' && !clientId) ||
(step === 'service-select' && !serviceId) ||
(step === 'datetime-select' && (!date || !time))
);
}

View File

@ -1,9 +1,8 @@
'use client';
import { OrderContext } from '@/context/order';
import { useServicesQuery } from '@/hooks/service';
import { useServicesQuery } from '@/hooks/services';
import { useOrderStore } from '@/stores/order';
import { type ServiceFieldsFragment } from '@repo/graphql/types';
import { cn } from '@repo/ui/lib/utils';
import { use } from 'react';
export function ServiceSelect() {
const { data } = useServicesQuery();
@ -18,7 +17,8 @@ export function ServiceSelect() {
}
function ServiceCard({ documentId, name }: Readonly<ServiceFieldsFragment>) {
const { serviceId, setServiceId } = use(OrderContext);
const serviceId = useOrderStore((store) => store.serviceId);
const setServiceId = useOrderStore((store) => store.setServiceId);
const selected = serviceId === documentId;

View File

@ -1,32 +0,0 @@
'use client';
import { OrderContext } from '@/context/order';
import { Button } from '@repo/ui/components/ui/button';
import { use } from 'react';
export function SubmitButton() {
const { nextStep, step } = use(OrderContext);
function handleOnClick() {
if (step !== 'success') {
nextStep();
}
}
const disabled = useButtonDisabled();
return (
<Button className="w-full" disabled={disabled} onClick={() => handleOnClick()} type="button">
{step === 'success' ? 'Создать' : 'Продолжить'}
</Button>
);
}
function useButtonDisabled() {
const { clientId, masterId, serviceId, step } = use(OrderContext);
return (
(step === 'master-select' && !masterId) ||
(step === 'client-select' && !clientId) ||
(step === 'service-select' && !serviceId)
);
}

View File

@ -1,43 +1,42 @@
'use client';
import { LoadingSpinner } from '../common/spinner';
import {
BackButton,
ClientsGrid,
DateSelect,
DateTimeSelect,
MastersGrid,
NextButton,
ServiceSelect,
SubmitButton,
TimeSelect,
} from './components';
import { OrderContext, OrderContextProvider } from '@/context/order';
import { OrderStoreProvider, useOrderStore } from '@/stores/order';
import { useInitOrderStore } from '@/stores/order/hooks';
import { withContext } from '@/utils/context';
import { type JSX, use } from 'react';
import { type JSX } from 'react';
const STEP_COMPONENTS: Record<string, JSX.Element> = {
'client-select': <ClientsGrid />,
'datetime-select': (
<>
<DateSelect />
<TimeSelect />
</>
),
'datetime-select': <DateTimeSelect />,
'master-select': <MastersGrid />,
'service-select': <ServiceSelect />,
};
function getStepComponent(step: string) {
return STEP_COMPONENTS[step] ?? null;
}
export const OrderForm = withContext(OrderStoreProvider)(function () {
useInitOrderStore();
export const OrderForm = withContext(OrderContextProvider)(function () {
const { step } = use(OrderContext);
const step = useOrderStore((store) => store.step);
if (step === 'loading') return <LoadingSpinner />;
return (
<div className="space-y-4 [&>*]:px-4">
{getStepComponent(step)}
<div className="space-y-2">
<SubmitButton />
<NextButton />
<BackButton />
</div>
</div>
);
});
function getStepComponent(step: string) {
return STEP_COMPONENTS[step] ?? null;
}

View File

@ -1,7 +1,7 @@
/* eslint-disable canonical/id-match */
'use client';
import { ReadonlyTimeRange } from './time-range';
import { useSlotQuery } from '@/hooks/slots/master';
import { useSlotQuery } from '@/hooks/slots';
import { Enum_Slot_State, type SlotFieldsFragment } from '@repo/graphql/types';
import { Badge } from '@repo/ui/components/ui/badge';
import { cn } from '@repo/ui/lib/utils';

View File

@ -1,6 +1,6 @@
'use client';
import { type SlotComponentProps } from '../types';
import { useSlotQuery } from '@/hooks/slots/master';
import { useSlotQuery } from '@/hooks/slots';
import { formatDate } from '@/utils/date';
export function SlotDate({ documentId }: Readonly<SlotComponentProps>) {

View File

@ -3,7 +3,7 @@
import { ScheduleTimeContext } from '../context';
import { type SlotComponentProps } from '../types';
import { EditableTimeRangeForm, ReadonlyTimeRange } from './time-range';
import { useSlotMutation, useSlotQuery } from '@/hooks/slots/master';
import { useSlotMutation, useSlotQuery } from '@/hooks/slots';
import { Button } from '@repo/ui/components/ui/button';
import { PencilLine } from 'lucide-react';
import { use, useEffect } from 'react';

View File

@ -3,7 +3,7 @@
import { EditableTimeRangeForm } from './components/time-range';
import { ScheduleTimeContext, ScheduleTimeContextProvider } from './context';
import { ScheduleContext } from '@/context/schedule';
import { useSlotAdd } from '@/hooks/slots/master';
import { useSlotAdd } from '@/hooks/slots';
import { withContext } from '@/utils/context';
import { Enum_Slot_State } from '@repo/graphql/types';
import { Button } from '@repo/ui/components/ui/button';
@ -15,7 +15,7 @@ export const DaySlotAddForm = withContext(ScheduleTimeContextProvider)(function
const { selectedDate } = use(ScheduleContext);
const { isPending, mutate: addSlot } = useSlotAdd();
const { isPending, mutate: addSlot } = useSlotAdd({ date: selectedDate });
const handleSubmit = (event: FormEvent) => {
event.preventDefault();

View File

@ -2,10 +2,13 @@
import { SlotCard } from './components/slot-card';
import { DaySlotAddForm } from './day-slot-add-form';
import { LoadingSpinner } from '@/components/common/spinner';
import { useSlots } from '@/hooks/slots/master';
import { ScheduleContext } from '@/context/schedule';
import { useSlots } from '@/hooks/slots';
import { use } from 'react';
export function DaySlotsList() {
const { data, isLoading } = useSlots();
const { selectedDate } = use(ScheduleContext);
const { data, isLoading } = useSlots({ date: selectedDate });
const slots = data?.data.slots;
if (isLoading) return <LoadingSpinner />;

View File

@ -2,15 +2,20 @@
/* eslint-disable canonical/id-match */
'use client';
import { type SlotComponentProps } from './types';
import { useSlotDelete, useSlotMutation, useSlotQuery } from '@/hooks/slots/master';
import { ScheduleContext } from '@/context/schedule';
import { useSlotDelete, useSlotMutation, useSlotQuery } from '@/hooks/slots';
import { Enum_Slot_State } from '@repo/graphql/types';
import { Button } from '@repo/ui/components/ui/button';
import { useRouter } from 'next/navigation';
import { use } from 'react';
export function SlotButtons({ documentId }: Readonly<SlotComponentProps>) {
const { data } = useSlotQuery({ documentId });
const { mutate: updateSlot } = useSlotMutation({ documentId });
const { mutate: deleteSlot } = useSlotDelete({ documentId });
const { selectedDate } = use(ScheduleContext);
const { mutate: deleteSlot } = useSlotDelete({ date: selectedDate, documentId });
const router = useRouter();

View File

@ -1,7 +1,7 @@
'use client';
import { OrderCard } from './components/order-card';
import { type SlotComponentProps } from './types';
import { useSlotQuery } from '@/hooks/slots/master';
import { useSlotQuery } from '@/hooks/slots';
export function SlotOrdersList({ documentId }: Readonly<SlotComponentProps>) {
const { data } = useSlotQuery({ documentId });

View File

@ -1,23 +0,0 @@
'use client';
import { getStepsReducer } from './reducer';
import { type Steps } from './types';
import { useProfileQuery } from '@/hooks/profile';
import { useReducer, useState } from 'react';
export function useEntityState() {
const [entityId, setEntityId] = useState<null | string>(null);
return { entityId, setEntityId };
}
export function useStep() {
const { data: profile } = useProfileQuery();
const stepsReducer = getStepsReducer(profile);
const [state, dispatch] = useReducer(stepsReducer, { step: 'master-select' });
const setStep = (payload: Steps) => dispatch({ payload, type: 'SET_STEP' });
const nextStep = () => dispatch({ type: 'NEXT_STEP' });
const previousStep = () => dispatch({ type: 'PREV_STEP' });
return { nextStep, prevStep: previousStep, setStep, ...state };
}

View File

@ -1,52 +0,0 @@
'use client';
import { useEntityState, useStep } from './hooks';
import { type ContextType } from './types';
import { createContext, type PropsWithChildren, useMemo, useState } from 'react';
export const OrderContext = createContext<ContextType>({} as ContextType);
export function OrderContextProvider({ children }: Readonly<PropsWithChildren>) {
const { entityId: masterId, setEntityId: setMasterId } = useEntityState();
const { entityId: serviceId, setEntityId: setServiceId } = useEntityState();
const { entityId: clientId, setEntityId: setClientId } = useEntityState();
const [date, setDate] = useState<Date>(new Date());
const [time, setTime] = useState<null | string>(null);
const { nextStep, prevStep, setStep, step } = useStep();
const value = useMemo(
() => ({
clientId,
date,
masterId,
nextStep,
prevStep,
serviceId,
setClientId,
setDate,
setMasterId,
setServiceId,
setStep,
setTime,
step,
time,
}),
[
clientId,
date,
masterId,
nextStep,
prevStep,
serviceId,
setClientId,
setMasterId,
setServiceId,
setStep,
step,
time,
],
);
return <OrderContext value={value}>{children}</OrderContext>;
}

View File

@ -1,48 +0,0 @@
/* eslint-disable canonical/id-match */
import { type Action, type State, type Steps } from './types';
import { type CustomerFieldsFragment, Enum_Customer_Role } from '@repo/graphql/types';
const masterSteps: Steps[] = [
'master-select',
'client-select',
'service-select',
'datetime-select',
'success',
];
const clientSteps = masterSteps.filter((step) => step !== 'client-select');
export function getStepsReducer(profile: CustomerFieldsFragment | undefined) {
if (profile?.role === Enum_Customer_Role.Master) return createStepsReducer(masterSteps);
return createStepsReducer(clientSteps);
}
function createStepsReducer(stepsSequence: Steps[]) {
return function (state: State, action: Action): State {
switch (action.type) {
case 'NEXT_STEP': {
const currentIndex = stepsSequence.indexOf(state.step);
const nextIndex = currentIndex + 1;
const nextStep = stepsSequence[nextIndex];
return nextStep ? { ...state, step: nextStep } : state;
}
case 'PREV_STEP': {
const currentIndex = stepsSequence.indexOf(state.step);
const previousIndex = currentIndex - 1;
const previousStep = stepsSequence[previousIndex];
return previousStep ? { ...state, step: previousStep } : state;
}
case 'SET_STEP': {
return { ...state, step: action.payload };
}
default:
return state;
}
};
}

View File

@ -1,8 +1,8 @@
'use client';
import { getOrder } from '@/actions/orders';
import { createOrder, getOrder } from '@/actions/orders';
// eslint-disable-next-line sonarjs/no-internal-api-use
import type * as ApolloTypes from '@repo/graphql/node_modules/@apollo/client/core';
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type FixTypescriptCringe = ApolloTypes.FetchResult;
@ -16,3 +16,8 @@ export const useOrderQuery = ({ documentId }: Props) =>
queryFn: () => getOrder({ documentId }),
queryKey: ['orders', 'get', documentId],
});
export const useOrderCreate = () =>
useMutation({
mutationFn: createOrder,
});

View File

@ -1,5 +1,5 @@
'use client';
import { getServices } from '@/actions/service';
import { getServices } from '@/actions/services';
// eslint-disable-next-line sonarjs/no-internal-api-use
import type * as ApolloTypes from '@repo/graphql/node_modules/@apollo/client/core';
import { type GetServicesQueryVariables } from '@repo/graphql/types';

View File

@ -1,42 +1,46 @@
'use client';
import { addSlot, deleteSlot, getSlot, getSlots, updateSlot } from '@/actions/slots';
import { ScheduleContext } from '@/context/schedule';
import { formatDate } from '@/utils/date';
// eslint-disable-next-line sonarjs/no-internal-api-use
import type * as ApolloTypes from '@repo/graphql/node_modules/@apollo/client/core';
import { useMutation, useQuery } from '@tanstack/react-query';
import { use } from 'react';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type FixTypescriptCringe = ApolloTypes.FetchResult;
export const useSlots = () => {
const { selectedDate } = use(ScheduleContext);
type SlotAddInput = {
date: Date;
};
type SlotMutationInput = {
documentId: string;
};
type SlotQueryInput = {
date: Date;
masterId?: null | string;
};
export const useSlots = ({ date, masterId }: SlotQueryInput) => {
return useQuery({
queryFn: () =>
getSlots({
filters: {
date: {
eq: formatDate(selectedDate).db(),
},
date: { eq: formatDate(date).db() },
master: masterId ? { documentId: { eq: masterId } } : undefined,
},
}),
queryKey: ['slots', 'list', selectedDate],
queryKey: ['slots', 'master', masterId, 'list', date],
});
};
type Props = {
documentId: string;
};
export const useSlotQuery = ({ documentId }: Props) =>
export const useSlotQuery = ({ documentId }: SlotMutationInput) =>
useQuery({
queryFn: () => getSlot({ documentId }),
queryKey: ['slots', 'get', documentId],
});
export const useSlotMutation = ({ documentId }: Props) => {
export const useSlotMutation = ({ documentId }: SlotMutationInput) => {
const { refetch } = useSlotQuery({ documentId });
return useMutation({
@ -46,8 +50,8 @@ export const useSlotMutation = ({ documentId }: Props) => {
});
};
export const useSlotAdd = () => {
const { refetch } = useSlots();
export const useSlotAdd = ({ date }: SlotAddInput) => {
const { refetch } = useSlots({ date });
return useMutation({
mutationFn: addSlot,
@ -56,8 +60,8 @@ export const useSlotAdd = () => {
});
};
export const useSlotDelete = ({ documentId }: Props) => {
const { refetch } = useSlots();
export const useSlotDelete = ({ date, documentId }: SlotAddInput & SlotMutationInput) => {
const { refetch } = useSlots({ date });
return useMutation({
mutationFn: () => deleteSlot({ documentId }),

View File

@ -28,7 +28,8 @@
"react": "catalog:",
"react-dom": "catalog:",
"use-debounce": "^10.0.4",
"zod": "catalog:"
"zod": "catalog:",
"zustand": "^5.0.3"
},
"devDependencies": {
"@playwright/test": "^1.49.1",

View File

@ -0,0 +1,16 @@
'use client';
import { createOrderStore } from './store';
import { createContext, type PropsWithChildren, useRef } from 'react';
export type OrderStoreApi = ReturnType<typeof createOrderStore>;
export const OrderStoreContext = createContext<OrderStoreApi | undefined>(undefined);
export function OrderStoreProvider({ children }: Readonly<PropsWithChildren>) {
const storeRef = useRef<null | OrderStoreApi>(null);
if (storeRef.current === null) {
storeRef.current = createOrderStore();
}
return <OrderStoreContext value={storeRef.current}>{children}</OrderStoreContext>;
}

View File

@ -0,0 +1,55 @@
/* eslint-disable canonical/id-match */
'use client';
import { OrderStoreContext } from './context';
import { type OrderStore, type Steps } from './types';
import { useProfileQuery } from '@/hooks/profile';
import { Enum_Customer_Role } from '@repo/graphql/types';
import { useContext, useEffect } from 'react';
import { useStore } from 'zustand';
export const useOrderStore = <T,>(selector: (store: OrderStore) => T): T => {
const orderStoreContext = useContext(OrderStoreContext);
if (!orderStoreContext) {
throw new Error(`useOrderStore must be used within OrderStoreProvider`);
}
return useStore(orderStoreContext, selector);
};
const STEPS: Steps[] = [
'master-select',
'client-select',
'service-select',
'datetime-select',
'success',
];
export const MASTER_STEPS: Steps[] = STEPS.filter((step) => step !== 'master-select');
export const CLIENT_STEPS: Steps[] = STEPS.filter((step) => step !== 'client-select');
export function useInitOrderStore() {
const { data } = useProfileQuery();
const setMasterId = useOrderStore((store) => store.setMasterId);
const setClientId = useOrderStore((store) => store.setClientId);
const setStep = useOrderStore((store) => store.setStep);
const setStepsSequence = useOrderStore((store) => store._setStepSequence);
useEffect(() => {
const role = data?.role;
if (role === Enum_Customer_Role.Master && data) {
setMasterId(data?.documentId);
}
if (role === Enum_Customer_Role.Client && data) {
setClientId(data?.documentId);
}
const steps = role === Enum_Customer_Role.Master ? MASTER_STEPS : CLIENT_STEPS;
const initialStep = steps[0] as Steps;
setStepsSequence(steps);
setStep(initialStep);
}, [data, setClientId, setMasterId, setStep, setStepsSequence]);
}

View File

@ -0,0 +1,2 @@
export * from './context';
export { useOrderStore } from './hooks';

View File

@ -0,0 +1,39 @@
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable canonical/id-match */
import { type OrderStore } from './types';
import { createStore } from 'zustand';
export function createOrderStore() {
return createStore<OrderStore>((set, get) => ({
_setStepSequence: (steps) => set({ _stepSequence: steps }),
_stepSequence: [],
clientId: null,
date: new Date(),
masterId: null,
nextStep: () => {
const { _stepSequence, step } = get();
const index = _stepSequence.indexOf(step);
const next = _stepSequence[index + 1];
if (next) set({ step: next });
},
prevStep: () => {
const { _stepSequence, step } = get();
const index = _stepSequence.indexOf(step);
const previous = _stepSequence[index - 1];
if (previous) set({ step: previous });
},
serviceId: null,
setClientId: (id) => set({ clientId: id }),
setDate: (date) => set({ date }),
setMasterId: (id) => set({ masterId: id }),
setServiceId: (id) => set({ serviceId: id }),
setSlotId: (id) => set({ slotId: id }),
setStep: (step) => set({ step }),
setTime: (time) => set({ time }),
slotId: null,
step: 'loading',
time: null,
}));
}

View File

@ -1,30 +1,29 @@
export type Action =
| { payload: Steps; type: 'SET_STEP' }
| { type: 'NEXT_STEP' }
| { type: 'PREV_STEP' };
export type ContextType = {
export type OrderStore = {
_setStepSequence: (steps: Steps[]) => void;
_stepSequence: Steps[];
clientId: null | string;
date: Date;
masterId: null | string;
nextStep: () => void;
prevStep: () => void;
serviceId: null | string;
setClientId: (clientId: null | string) => void;
setClientId: (id: null | string) => void;
setDate: (date: Date) => void;
setMasterId: (customerId: null | string) => void;
setServiceId: (serviceId: null | string) => void;
setMasterId: (id: null | string) => void;
setServiceId: (id: null | string) => void;
setSlotId: (slot: null | string) => void;
setStep: (step: Steps) => void;
setTime: (time: string) => void;
setTime: (time: null | string) => void;
slotId: null | string;
step: Steps;
time: null | string;
};
export type State = { step: Steps };
export type Steps =
| 'client-select'
| 'datetime-select'
| 'error'
| 'loading'
| 'master-select'
| 'service-select'
| 'success';

View File

@ -33,3 +33,19 @@ export function formatTime(time: string) {
user: () => `${hours}:${minutes}`,
};
}
export function sumTime(time1: string, time2: string) {
const [hours1 = '00', minutes1 = '00'] = time1.split(':');
const [hours2 = '00', minutes2 = '00'] = time2.split(':');
let totalMinutes = Number.parseInt(minutes1, 10) + Number.parseInt(minutes2, 10);
let totalHours = Number.parseInt(hours1, 10) + Number.parseInt(hours2, 10);
totalHours += Math.floor(totalMinutes / 60);
totalMinutes %= 60;
const paddedHours = totalHours.toString().padStart(2, '0');
const paddedMinutes = totalMinutes.toString().padStart(2, '0');
return `${paddedHours}:${paddedMinutes}`;
}

View File

@ -2,11 +2,20 @@
import { getClientWithToken } from '../apollo/client';
import * as GQL from '../types';
export async function getOrder(input: GQL.GetOrderQueryVariables) {
export async function getOrder(variables: GQL.GetOrderQueryVariables) {
const { query } = await getClientWithToken();
return query({
query: GQL.GetOrderDocument,
variables: input,
variables,
});
}
export async function createOrder(input: GQL.CreateOrderMutationVariables['input']) {
const { mutate } = await getClientWithToken();
return mutate({
mutation: GQL.CreateOrderDocument,
variables: { input },
});
}

View File

@ -20,3 +20,9 @@ query GetOrder($documentId: ID!) {
...OrderFields
}
}
mutation CreateOrder($input: OrderInput!) {
createOrder(data: $input) {
...OrderFields
}
}

View File

@ -1,6 +1,7 @@
fragment ServiceFields on Service {
documentId
name
duration
}
query GetServices($filters: ServiceFiltersInput) {

View File

@ -24,6 +24,9 @@ query GetSlot($documentId: ID!) {
orders(sort: "time_start:asc") {
documentId
}
master {
documentId
}
...SlotFields
}
}

View File

@ -336,6 +336,7 @@ export type OrderFiltersInput = {
block?: InputMaybe<BlockFiltersInput>;
client?: InputMaybe<CustomerFiltersInput>;
createdAt?: InputMaybe<DateTimeFilterInput>;
date?: InputMaybe<DateFilterInput>;
documentId?: InputMaybe<IdFilterInput>;
not?: InputMaybe<OrderFiltersInput>;
or?: InputMaybe<Array<InputMaybe<OrderFiltersInput>>>;
@ -354,6 +355,7 @@ export type OrderFiltersInput = {
export type OrderInput = {
block?: InputMaybe<Scalars['ID']['input']>;
client?: InputMaybe<Scalars['ID']['input']>;
date?: InputMaybe<Scalars['Date']['input']>;
order_number?: InputMaybe<Scalars['Int']['input']>;
price?: InputMaybe<Scalars['Int']['input']>;
publishedAt?: InputMaybe<Scalars['DateTime']['input']>;
@ -423,6 +425,7 @@ export type ServiceFiltersInput = {
and?: InputMaybe<Array<InputMaybe<ServiceFiltersInput>>>;
createdAt?: InputMaybe<DateTimeFilterInput>;
documentId?: InputMaybe<IdFilterInput>;
duration?: InputMaybe<TimeFilterInput>;
master?: InputMaybe<CustomerFiltersInput>;
name?: InputMaybe<StringFilterInput>;
not?: InputMaybe<ServiceFiltersInput>;
@ -433,6 +436,7 @@ export type ServiceFiltersInput = {
};
export type ServiceInput = {
duration?: InputMaybe<Scalars['Time']['input']>;
master?: InputMaybe<Scalars['ID']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
orders?: InputMaybe<Array<InputMaybe<Scalars['ID']['input']>>>;
@ -699,21 +703,28 @@ export type GetOrderQueryVariables = Exact<{
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 ServiceFieldsFragment = { __typename?: 'Service', documentId: string, name?: string | 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 ServiceFieldsFragment = { __typename?: 'Service', documentId: string, name?: string | null | undefined, duration: any };
export type GetServicesQueryVariables = Exact<{
filters?: InputMaybe<ServiceFiltersInput>;
}>;
export type GetServicesQuery = { __typename?: 'Query', services: Array<{ __typename?: 'Service', documentId: string, name?: string | null | undefined } | null | undefined> };
export type GetServicesQuery = { __typename?: 'Query', services: Array<{ __typename?: 'Service', documentId: string, name?: string | null | undefined, duration: any } | null | undefined> };
export type GetServiceQueryVariables = Exact<{
documentId: Scalars['ID']['input'];
}>;
export type GetServiceQuery = { __typename?: 'Query', service?: { __typename?: 'Service', documentId: string, name?: string | null | undefined } | null | undefined };
export type GetServiceQuery = { __typename?: 'Query', service?: { __typename?: 'Service', documentId: string, name?: string | null | undefined, duration: any } | 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 };
@ -736,7 +747,7 @@ export type GetSlotQueryVariables = Exact<{
}>;
export type GetSlotQuery = { __typename?: 'Query', slot?: { __typename?: 'Slot', state?: Enum_Slot_State | null | undefined, documentId: string, date?: any | null | undefined, time_start: any, time_end: any, orders: Array<{ __typename?: 'Order', documentId: string } | null | undefined> } | null | undefined };
export type GetSlotQuery = { __typename?: 'Query', slot?: { __typename?: 'Slot', state?: Enum_Slot_State | null | undefined, documentId: string, date?: any | null | undefined, time_start: any, time_end: any, orders: Array<{ __typename?: 'Order', documentId: string } | null | undefined>, master?: { __typename?: 'Customer', documentId: string } | null | undefined } | null | undefined };
export type UpdateSlotMutationVariables = Exact<{
documentId: Scalars['ID']['input'];
@ -755,7 +766,7 @@ export type DeleteSlotMutation = { __typename?: 'Mutation', deleteSlot?: { __typ
export const CustomerFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CustomerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Customer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"telegramId"}}]}}]} as unknown as DocumentNode<CustomerFieldsFragment, unknown>;
export const OrderFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"time_start"}},{"kind":"Field","name":{"kind":"Name","value":"time_end"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"order_number"}},{"kind":"Field","name":{"kind":"Name","value":"services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"client"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}}]}}]}}]} as unknown as DocumentNode<OrderFieldsFragment, unknown>;
export const ServiceFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServiceFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Service"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode<ServiceFieldsFragment, unknown>;
export const ServiceFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServiceFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Service"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}}]}}]} as unknown as DocumentNode<ServiceFieldsFragment, unknown>;
export const SlotFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SlotFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Slot"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"time_start"}},{"kind":"Field","name":{"kind":"Name","value":"time_end"}},{"kind":"Field","name":{"kind":"Name","value":"state"}}]}}]} as unknown as DocumentNode<SlotFieldsFragment, unknown>;
export const RegisterDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Register"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"password"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"register"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"username"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identifier"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"password"},"value":{"kind":"Variable","name":{"kind":"Name","value":"password"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jwt"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]}}]} as unknown as DocumentNode<RegisterMutation, RegisterMutationVariables>;
export const LoginDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Login"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"password"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"login"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"identifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identifier"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"password"},"value":{"kind":"Variable","name":{"kind":"Name","value":"password"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jwt"}}]}}]}}]} as unknown as DocumentNode<LoginMutation, LoginMutationVariables>;
@ -765,10 +776,11 @@ export const GetCustomerMastersDocument = {"kind":"Document","definitions":[{"ki
export const GetCustomerClientsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCustomerClients"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"phone"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"telegramId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Long"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"or"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"phone"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"phone"}}}]}}]},{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"telegramId"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"telegramId"}}}]}}]}]}},{"kind":"ObjectField","name":{"kind":"Name","value":"and"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"BooleanValue","value":true}}]}}]}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"clients"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CustomerFields"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CustomerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Customer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"telegramId"}}]}}]} as unknown as DocumentNode<GetCustomerClientsQuery, GetCustomerClientsQueryVariables>;
export const UpdateCustomerProfileDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCustomerProfile"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CustomerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateCustomer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"documentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CustomerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CustomerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Customer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"telegramId"}}]}}]} as unknown as DocumentNode<UpdateCustomerProfileMutation, UpdateCustomerProfileMutationVariables>;
export const GetOrderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOrder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"documentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"time_start"}},{"kind":"Field","name":{"kind":"Name","value":"time_end"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"order_number"}},{"kind":"Field","name":{"kind":"Name","value":"services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"client"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}}]}}]}}]} as unknown as DocumentNode<GetOrderQuery, GetOrderQueryVariables>;
export const GetServicesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetServices"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ServiceFiltersInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"services"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServiceFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServiceFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Service"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode<GetServicesQuery, GetServicesQueryVariables>;
export const GetServiceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetService"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"service"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"documentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServiceFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServiceFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Service"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode<GetServiceQuery, GetServiceQueryVariables>;
export const CreateOrderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOrder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"OrderInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOrder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"time_start"}},{"kind":"Field","name":{"kind":"Name","value":"time_end"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"order_number"}},{"kind":"Field","name":{"kind":"Name","value":"services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"client"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}}]}}]}}]} as unknown as DocumentNode<CreateOrderMutation, CreateOrderMutationVariables>;
export const GetServicesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetServices"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ServiceFiltersInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"services"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServiceFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServiceFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Service"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}}]}}]} as unknown as DocumentNode<GetServicesQuery, GetServicesQueryVariables>;
export const GetServiceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetService"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"service"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"documentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServiceFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServiceFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Service"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}}]}}]} as unknown as DocumentNode<GetServiceQuery, GetServiceQueryVariables>;
export const CreateSlotDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateSlot"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SlotInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createSlot"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SlotFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SlotFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Slot"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"time_start"}},{"kind":"Field","name":{"kind":"Name","value":"time_end"}},{"kind":"Field","name":{"kind":"Name","value":"state"}}]}}]} as unknown as DocumentNode<CreateSlotMutation, CreateSlotMutationVariables>;
export const GetSlotsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSlots"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"SlotFiltersInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slots"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"StringValue","value":"time_start:asc","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SlotFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SlotFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Slot"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"time_start"}},{"kind":"Field","name":{"kind":"Name","value":"time_end"}},{"kind":"Field","name":{"kind":"Name","value":"state"}}]}}]} as unknown as DocumentNode<GetSlotsQuery, GetSlotsQueryVariables>;
export const GetSlotDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSlot"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slot"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"documentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"orders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"StringValue","value":"time_start:asc","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"SlotFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SlotFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Slot"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"time_start"}},{"kind":"Field","name":{"kind":"Name","value":"time_end"}},{"kind":"Field","name":{"kind":"Name","value":"state"}}]}}]} as unknown as DocumentNode<GetSlotQuery, GetSlotQueryVariables>;
export const GetSlotDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSlot"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slot"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"documentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"orders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"StringValue","value":"time_start:asc","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"master"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"SlotFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SlotFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Slot"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"time_start"}},{"kind":"Field","name":{"kind":"Name","value":"time_end"}},{"kind":"Field","name":{"kind":"Name","value":"state"}}]}}]} as unknown as DocumentNode<GetSlotQuery, GetSlotQueryVariables>;
export const UpdateSlotDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSlot"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SlotInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateSlot"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"documentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SlotFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SlotFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Slot"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"time_start"}},{"kind":"Field","name":{"kind":"Name","value":"time_end"}},{"kind":"Field","name":{"kind":"Name","value":"state"}}]}}]} as unknown as DocumentNode<UpdateSlotMutation, UpdateSlotMutationVariables>;
export const DeleteSlotDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSlot"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteSlot"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"documentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}}]}}]}}]} as unknown as DocumentNode<DeleteSlotMutation, DeleteSlotMutationVariables>;

26
pnpm-lock.yaml generated
View File

@ -183,6 +183,9 @@ importers:
zod:
specifier: 'catalog:'
version: 3.24.1
zustand:
specifier: ^5.0.3
version: 5.0.3(@types/react@19.1.2)(react@19.1.0)
devDependencies:
'@playwright/test':
specifier: ^1.49.1
@ -6170,6 +6173,24 @@ packages:
zod@3.24.1:
resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
zustand@5.0.3:
resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
immer: '>=9.0.6'
react: '>=18.0.0'
use-sync-external-store: '>=1.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
use-sync-external-store:
optional: true
snapshots:
'@alloc/quick-lru@5.2.0': {}
@ -12821,3 +12842,8 @@ snapshots:
zen-observable@0.8.15: {}
zod@3.24.1: {}
zustand@5.0.3(@types/react@19.1.2)(react@19.1.0):
optionalDependencies:
'@types/react': 19.1.2
react: 19.1.0