From 5dfef524e2a8fb418a3dbec378bb3f7df94a1194 Mon Sep 17 00:00:00 2001 From: vchikalkin Date: Mon, 8 Sep 2025 12:51:35 +0300 Subject: [PATCH] refactor(contact): remove customer master role checks and simplify contact addition - Updated the `addContact` function to allow all users to add contacts, removing the previous restriction that only masters could do so. - Deleted the `become-master` feature and related utility functions, streamlining the codebase. - Adjusted command settings to reflect the removal of the master role functionality. - Refactored components and hooks to eliminate dependencies on the master role, enhancing user experience and simplifying logic. --- apps/bot/src/bot/conversations/add-contact.ts | 7 +- apps/bot/src/bot/features/become-master.ts | 42 -------- apps/bot/src/bot/features/index.ts | 1 - apps/bot/src/bot/settings/commands.ts | 2 +- apps/bot/src/utils/customer.ts | 5 - .../app/(main)/profile/[telegramId]/page.tsx | 19 +--- apps/web/app/(main)/profile/page.tsx | 7 +- .../schedule/slots/[documentId]/page.tsx | 13 +-- apps/web/components/orders/order-buttons.tsx | 28 ++--- .../orders/orders-list/date-select.tsx | 24 ++--- .../components/orders/orders-list/index.tsx | 18 ++-- .../orders/orders-list/orders-list.tsx | 11 +- .../components/profile/links-card/index.tsx | 9 +- .../profile/links-card/link-button.tsx | 5 +- apps/web/components/profile/orders-list.tsx | 41 +++---- .../profile/services/services-list.tsx | 29 +++-- .../schedule/day-slots-list/index.tsx | 26 +---- .../components/schedule/slot-orders-list.tsx | 7 +- apps/web/components/shared/book-button.tsx | 16 +-- apps/web/components/shared/contact-row.tsx | 5 +- apps/web/components/shared/order-card.tsx | 18 ++-- apps/web/hooks/api/customers.ts | 10 +- apps/web/stores/order/hooks.tsx | 26 ++--- packages/graphql/api/orders.ts | 40 +------ packages/graphql/api/slots.ts | 2 +- packages/graphql/operations/customers.graphql | 4 + .../graphql/types/operations.generated.ts | 100 +++++++++--------- packages/utils/src/customer.ts | 4 +- 28 files changed, 171 insertions(+), 348 deletions(-) delete mode 100644 apps/bot/src/bot/features/become-master.ts delete mode 100644 apps/bot/src/utils/customer.ts diff --git a/apps/bot/src/bot/conversations/add-contact.ts b/apps/bot/src/bot/conversations/add-contact.ts index aa37d6f..d7ea928 100644 --- a/apps/bot/src/bot/conversations/add-contact.ts +++ b/apps/bot/src/bot/conversations/add-contact.ts @@ -1,14 +1,13 @@ /* eslint-disable id-length */ import { type Context } from '@/bot/context'; import { KEYBOARD_REMOVE, KEYBOARD_SHARE_BOT, KEYBOARD_SHARE_PHONE } from '@/config/keyboards'; -import { isCustomerMaster } from '@/utils/customer'; import { isValidPhoneNumber, normalizePhoneNumber } from '@/utils/phone'; import { type Conversation } from '@grammyjs/conversations'; import { CustomersService } from '@repo/graphql/api/customers'; import { RegistrationService } from '@repo/graphql/api/registration'; export async function addContact(conversation: Conversation, ctx: Context) { - // Проверяем, что пользователь является мастером + // Все пользователи могут добавлять контакты const telegramId = ctx.from?.id; if (!telegramId) { return ctx.reply(await conversation.external(({ t }) => t('err-generic'))); @@ -24,10 +23,6 @@ export async function addContact(conversation: Conversation, c ); } - if (!isCustomerMaster(customer)) { - return ctx.reply(await conversation.external(({ t }) => t('msg-not-master'))); - } - // Просим отправить контакт клиента await ctx.reply(await conversation.external(({ t }) => t('msg-send-client-contact'))); diff --git a/apps/bot/src/bot/features/become-master.ts b/apps/bot/src/bot/features/become-master.ts deleted file mode 100644 index 1dbca9d..0000000 --- a/apps/bot/src/bot/features/become-master.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { type Context } from '@/bot/context'; -import { logHandle } from '@/bot/helpers/logging'; -import { KEYBOARD_SHARE_PHONE } from '@/config/keyboards'; -import { isCustomerMaster } from '@/utils/customer'; -import { CustomersService } from '@repo/graphql/api/customers'; -import { Enum_Customer_Role } from '@repo/graphql/types'; -import { Composer } from 'grammy'; - -const composer = new Composer(); - -const feature = composer.chatType('private'); - -feature.command('becomemaster', logHandle('command-become-master'), async (ctx) => { - const telegramId = ctx.from.id; - const customerService = new CustomersService({ telegramId }); - const { customer } = await customerService.getCustomer({ telegramId }); - - if (!customer) { - return ctx.reply(ctx.t('msg-need-phone'), { ...KEYBOARD_SHARE_PHONE, parse_mode: 'HTML' }); - } - - if (isCustomerMaster(customer)) { - return ctx.reply(ctx.t('msg-already-master'), { parse_mode: 'HTML' }); - } - - // Обновляем роль клиента на мастер - const response = await customerService - .updateCustomer({ - data: { role: Enum_Customer_Role.Master }, - }) - .catch((error) => { - ctx.reply(ctx.t('err-with-details', { error: String(error) }), { parse_mode: 'HTML' }); - }); - - if (response) { - return ctx.reply(ctx.t('msg-become-master'), { parse_mode: 'HTML' }); - } - - return ctx.reply(ctx.t('err-generic'), { parse_mode: 'HTML' }); -}); - -export { composer as becomeMaster }; diff --git a/apps/bot/src/bot/features/index.ts b/apps/bot/src/bot/features/index.ts index 6caad36..dcc3d11 100644 --- a/apps/bot/src/bot/features/index.ts +++ b/apps/bot/src/bot/features/index.ts @@ -1,5 +1,4 @@ export * from './add-contact'; -export * from './become-master'; export * from './help'; export * from './registration'; export * from './share-bot'; diff --git a/apps/bot/src/bot/settings/commands.ts b/apps/bot/src/bot/settings/commands.ts index dbd78ca..e10e9a0 100644 --- a/apps/bot/src/bot/settings/commands.ts +++ b/apps/bot/src/bot/settings/commands.ts @@ -5,7 +5,7 @@ import { type LanguageCode } from '@grammyjs/types'; import { type Api, type Bot, type RawApi } from 'grammy'; export async function setCommands({ api }: Bot>) { - const commands = createCommands(['start', 'addcontact', 'becomemaster', 'sharebot', 'help']); + const commands = createCommands(['start', 'addcontact', 'sharebot', 'help']); for (const command of commands) { addLocalizations(command); diff --git a/apps/bot/src/utils/customer.ts b/apps/bot/src/utils/customer.ts deleted file mode 100644 index 499e79c..0000000 --- a/apps/bot/src/utils/customer.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as GQL from '@repo/graphql/types'; - -export function isCustomerMaster(customer: GQL.CustomerFieldsFragment) { - return customer?.role === GQL.Enum_Customer_Role.Master; -} diff --git a/apps/web/app/(main)/profile/[telegramId]/page.tsx b/apps/web/app/(main)/profile/[telegramId]/page.tsx index ead4f58..c3a032c 100644 --- a/apps/web/app/(main)/profile/[telegramId]/page.tsx +++ b/apps/web/app/(main)/profile/[telegramId]/page.tsx @@ -3,9 +3,7 @@ import { getSessionUser } from '@/actions/session'; import { Container } from '@/components/layout'; import { PageHeader } from '@/components/navigation'; import { ContactDataCard, PersonCard, ProfileOrdersList } from '@/components/profile'; -import { MasterServicesList } from '@/components/profile/services'; -import { BookButton } from '@/components/shared/book-button'; -import { isCustomerMaster } from '@repo/utils/customer'; +import { ReadonlyServicesList } from '@/components/profile/services'; import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; // Тип параметров страницы @@ -32,27 +30,14 @@ export default async function ProfilePage(props: Readonly) { // Проверка наличия данных if (!profile || !currentUser) return null; - // Определяем роли и id - const isMaster = isCustomerMaster(currentUser); - const isProfileMaster = isCustomerMaster(profile); - const masterId = isMaster ? currentUser.documentId : profile.documentId; - const clientId = isMaster ? profile.documentId : currentUser.documentId; - return ( - {isProfileMaster && } + - {masterId && clientId && ( - - )} ); diff --git a/apps/web/app/(main)/profile/page.tsx b/apps/web/app/(main)/profile/page.tsx index b198f1a..a03039b 100644 --- a/apps/web/app/(main)/profile/page.tsx +++ b/apps/web/app/(main)/profile/page.tsx @@ -2,25 +2,22 @@ import { getCustomer } from '@/actions/api/customers'; import { getSessionUser } from '@/actions/session'; import { Container } from '@/components/layout'; import { LinksCard, PersonCard, ProfileDataCard, SubscriptionInfoBar } from '@/components/profile'; -import { isCustomerMaster } from '@repo/utils/customer'; import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; export default async function ProfilePage() { const queryClient = new QueryClient(); const { telegramId } = await getSessionUser(); - const { customer } = await queryClient.fetchQuery({ + await queryClient.prefetchQuery({ queryFn: () => getCustomer({ telegramId }), queryKey: ['customer', telegramId], }); - const isMaster = customer && isCustomerMaster(customer); - return ( - {isMaster ? : null} + diff --git a/apps/web/app/(main)/profile/schedule/slots/[documentId]/page.tsx b/apps/web/app/(main)/profile/schedule/slots/[documentId]/page.tsx index e31e5a0..d248303 100644 --- a/apps/web/app/(main)/profile/schedule/slots/[documentId]/page.tsx +++ b/apps/web/app/(main)/profile/schedule/slots/[documentId]/page.tsx @@ -1,6 +1,4 @@ -import { getCustomer } from '@/actions/api/customers'; import { getSlot } from '@/actions/api/slots'; -import { getSessionUser } from '@/actions/session'; import { Container } from '@/components/layout'; import { PageHeader } from '@/components/navigation'; import { SlotButtons, SlotDateTime, SlotOrdersList } from '@/components/schedule'; @@ -21,22 +19,13 @@ export default async function SlotPage(props: Readonly) { queryKey: ['slot', documentId], }); - // Получаем текущего пользователя - const sessionUser = await getSessionUser(); - const { customer: currentUser } = await queryClient.fetchQuery({ - queryFn: () => getCustomer({ telegramId: sessionUser.telegramId }), - queryKey: ['customer', sessionUser.telegramId], - }); - - const masterId = currentUser?.documentId; - return ( - {masterId && } +
diff --git a/apps/web/components/orders/order-buttons.tsx b/apps/web/components/orders/order-buttons.tsx index 9390c7b..8c5c5d1 100644 --- a/apps/web/components/orders/order-buttons.tsx +++ b/apps/web/components/orders/order-buttons.tsx @@ -1,9 +1,10 @@ +/* eslint-disable complexity */ /* eslint-disable canonical/id-match */ 'use client'; import FloatingActionPanel from '../shared/action-panel'; import { type OrderComponentProps } from './types'; -import { useIsMaster } from '@/hooks/api/customers'; +import { useCustomerQuery } from '@/hooks/api/customers'; import { useOrderMutation, useOrderQuery } from '@/hooks/api/orders'; import { usePushWithData } from '@/hooks/url'; import { Enum_Order_State } from '@repo/graphql/types'; @@ -12,13 +13,16 @@ import { isBeforeNow } from '@repo/utils/datetime-format'; export function OrderButtons({ documentId }: Readonly) { const push = usePushWithData(); - const isMaster = useIsMaster(); - + const { data: { customer } = {} } = useCustomerQuery(); const { data: { order } = {} } = useOrderQuery({ documentId }); const { isPending, mutate: updateOrder } = useOrderMutation({ documentId }); - if (!order) return null; + if (!order || !customer) return null; + + // Проверяем роль относительно конкретного заказа + const isOrderMaster = order.slot?.master?.documentId === customer.documentId; + const isOrderClient = order.client?.documentId === customer.documentId; const isCreated = order?.state === Enum_Order_State.Created; const isApproved = order?.state === Enum_Order_State.Approved; @@ -27,21 +31,21 @@ export function OrderButtons({ documentId }: Readonly) { const isCancelled = order?.state === Enum_Order_State.Cancelled; function handleApprove() { - if (isMaster) { + if (isOrderMaster) { updateOrder({ data: { state: Enum_Order_State.Approved } }); } } function handleCancel() { - if (isMaster) { + if (isOrderMaster) { updateOrder({ data: { state: Enum_Order_State.Cancelled } }); - } else { + } else if (isOrderClient) { updateOrder({ data: { state: Enum_Order_State.Cancelling } }); } } function handleOnComplete() { - if (isMaster) { + if (isOrderMaster) { updateOrder({ data: { state: Enum_Order_State.Completed } }); } } @@ -52,11 +56,11 @@ export function OrderButtons({ documentId }: Readonly) { const isOrderStale = order?.datetime_start && isBeforeNow(order?.datetime_start); - const canCancel = !isOrderStale && (isCreated || (isMaster && isCancelling) || isApproved); - const canComplete = isMaster && isApproved; - const canConfirm = !isOrderStale && isMaster && isCreated; + const canCancel = !isOrderStale && (isCreated || (isOrderMaster && isCancelling) || isApproved); + const canComplete = isOrderMaster && isApproved; + const canConfirm = !isOrderStale && isOrderMaster && isCreated; const canRepeat = isCancelled || isCompleted; - const canReturn = !isOrderStale && isMaster && isCancelled; + const canReturn = !isOrderStale && isOrderMaster && isCancelled; return ( { diff --git a/apps/web/components/orders/orders-list/index.tsx b/apps/web/components/orders/orders-list/index.tsx index 3c98819..5e86a3d 100644 --- a/apps/web/components/orders/orders-list/index.tsx +++ b/apps/web/components/orders/orders-list/index.tsx @@ -2,7 +2,7 @@ import { OrdersList as OrdersListComponent } from './orders-list'; import { useCurrentAndNext } from './utils'; -import { useCustomerQuery, useIsMaster } from '@/hooks/api/customers'; +import { useCustomerQuery } from '@/hooks/api/customers'; import { useOrdersInfiniteQuery } from '@/hooks/api/orders'; import { useDateTimeStore } from '@/stores/datetime'; import type * as GQL from '@repo/graphql/types'; @@ -10,7 +10,6 @@ import { getDateUTCRange } from '@repo/utils/datetime-format'; export function OrdersList() { const { data: { customer } = {} } = useCustomerQuery(); - const isMaster = useIsMaster(); const selectedDate = useDateTimeStore((store) => store.date); const { endOfDay, startOfDay } = getDateUTCRange(selectedDate).day(); @@ -22,10 +21,13 @@ export function OrdersList() { } = useOrdersInfiniteQuery( { filters: { - client: isMaster ? undefined : { documentId: { eq: customer?.documentId } }, + // Показываем все записи где пользователь является клиентом или мастером + or: [ + { client: { documentId: { eq: customer?.documentId } } }, + { slot: { master: { documentId: { eq: customer?.documentId } } } }, + ], slot: { datetime_start: selectedDate ? { gte: startOfDay, lt: endOfDay } : undefined, - master: isMaster ? { documentId: { eq: customer?.documentId } } : undefined, }, }, }, @@ -37,17 +39,15 @@ export function OrdersList() { const { current, next } = useCurrentAndNext(orders); - if (!orders?.length || isLoading) return null; - return ( fetchNextPage()} + onLoadMore={fetchNextPage} orders={orders} - title={isMaster ? 'Записи клиентов' : 'Ваши записи'} + title={selectedDate ? `Записи на ${selectedDate.toLocaleDateString()}` : 'Все записи'} /> ); } diff --git a/apps/web/components/orders/orders-list/orders-list.tsx b/apps/web/components/orders/orders-list/orders-list.tsx index d7b3e55..7d8bb96 100644 --- a/apps/web/components/orders/orders-list/orders-list.tsx +++ b/apps/web/components/orders/orders-list/orders-list.tsx @@ -1,14 +1,17 @@ import { getOrderStatus, getStatusText, type OrderStatus } from './utils'; +import { DataNotFound } from '@/components/shared/alert'; import { OrderCard } from '@/components/shared/order-card'; import type * as GQL from '@repo/graphql/types'; import { Button } from '@repo/ui/components/ui/button'; +import { LoadingSpinner } from '@repo/ui/components/ui/spinner'; import { cn } from '@repo/ui/lib/utils'; type Order = GQL.OrderFieldsFragment; -type OrdersListProps = Pick[0], 'avatarSource'> & { +type OrdersListProps = { readonly current: null | Order; readonly hasNextPage?: boolean; + readonly isLoading?: boolean; readonly next: null | Order; readonly onLoadMore?: () => void; readonly orders: Order[]; @@ -16,9 +19,9 @@ type OrdersListProps = Pick[0], 'avatarSource'> & { }; export function OrdersList({ - avatarSource, current, hasNextPage = false, + isLoading, next, onLoadMore, orders, @@ -27,6 +30,8 @@ export function OrdersList({ return (

{title}

+ {isLoading && } + {!isLoading && !orders.length ? : null} {orders?.map((order) => { if (!order) return null; @@ -34,7 +39,7 @@ export function OrdersList({ return ( - + ); })} diff --git a/apps/web/components/profile/links-card/index.tsx b/apps/web/components/profile/links-card/index.tsx index b6ff717..c14c22c 100644 --- a/apps/web/components/profile/links-card/index.tsx +++ b/apps/web/components/profile/links-card/index.tsx @@ -1,24 +1,19 @@ 'use client'; import { LinkButton } from './link-button'; -import { useIsMaster } from '@/hooks/api/customers'; export function LinksCard() { - const isMaster = useIsMaster(); - return (
); diff --git a/apps/web/components/profile/links-card/link-button.tsx b/apps/web/components/profile/links-card/link-button.tsx index 1dc6786..6bd65c6 100644 --- a/apps/web/components/profile/links-card/link-button.tsx +++ b/apps/web/components/profile/links-card/link-button.tsx @@ -4,12 +4,9 @@ type Props = { readonly description?: string; readonly href: string; readonly text: string; - readonly visible?: boolean; }; -export function LinkButton({ description, href, text, visible }: Props) { - if (!visible) return null; - +export function LinkButton({ description, href, text }: Props) { return (
diff --git a/apps/web/components/profile/orders-list.tsx b/apps/web/components/profile/orders-list.tsx index 1133a6d..9f24cf0 100644 --- a/apps/web/components/profile/orders-list.tsx +++ b/apps/web/components/profile/orders-list.tsx @@ -1,15 +1,15 @@ 'use client'; +import { DataNotFound } from '../shared/alert'; import { OrderCard } from '../shared/order-card'; import { type ProfileProps } from './types'; -import { useCustomerQuery, useIsMaster } from '@/hooks/api/customers'; +import { useCustomerQuery } from '@/hooks/api/customers'; import { useOrdersInfiniteQuery } from '@/hooks/api/orders'; import { Button } from '@repo/ui/components/ui/button'; +import { LoadingSpinner } from '@repo/ui/components/ui/spinner'; export function ProfileOrdersList({ telegramId }: Readonly) { const { data: { customer } = {} } = useCustomerQuery(); - const isMaster = useIsMaster(); - const { data: { customer: profile } = {} } = useCustomerQuery({ telegramId }); const { @@ -20,18 +20,17 @@ export function ProfileOrdersList({ telegramId }: Readonly) { } = useOrdersInfiniteQuery( { filters: { - client: { - documentId: { - eq: isMaster ? profile?.documentId : customer?.documentId, + // Показываем все записи между текущим пользователем и профилем + or: [ + { + client: { documentId: { eq: customer?.documentId } }, + slot: { master: { documentId: { eq: profile?.documentId } } }, }, - }, - slot: { - master: { - documentId: { - eq: isMaster ? customer?.documentId : profile?.documentId, - }, + { + client: { documentId: { eq: profile?.documentId } }, + slot: { master: { documentId: { eq: customer?.documentId } } }, }, - }, + ], }, }, { enabled: Boolean(profile?.documentId) && Boolean(customer?.documentId) }, @@ -39,22 +38,12 @@ export function ProfileOrdersList({ telegramId }: Readonly) { const orders = pages?.flatMap((page) => page.orders) ?? []; - if (!orders?.length || isLoading) return null; - return (

Недавние записи

- {orders?.map( - (order) => - order && ( - - ), - )} + {isLoading && } + {!isLoading && !orders.length ? : null} + {orders?.map((order) => order && )} {hasNextPage && (