diff --git a/apps/bot/src/index.ts b/apps/bot/src/index.ts index 76562c2..2e9c648 100644 --- a/apps/bot/src/index.ts +++ b/apps/bot/src/index.ts @@ -16,7 +16,8 @@ import { message } from 'telegraf/filters'; const bot = new Telegraf(environment.BOT_TOKEN); bot.start(async (context) => { - const customer = await getCustomer({ telegramId: context.from.id }); + const data = await getCustomer({ telegramId: context.from.id }); + const customer = data?.data?.customers?.at(0); if (customer) { return context.reply( @@ -33,7 +34,8 @@ bot.start(async (context) => { }); bot.command('addcontact', async (context) => { - const customer = await getCustomer({ telegramId: context.from.id }); + const data = await getCustomer({ telegramId: context.from.id }); + const customer = data?.data?.customers?.at(0); if (!customer) { return context.reply( @@ -50,7 +52,8 @@ bot.command('addcontact', async (context) => { }); bot.command('becomemaster', async (context) => { - const customer = await getCustomer({ telegramId: context.from.id }); + const data = await getCustomer({ telegramId: context.from.id }); + const customer = data?.data?.customers?.at(0); if (!customer) { return context.reply('Сначала поделитесь своим номером телефона.', KEYBOARD_SHARE_PHONE); @@ -73,7 +76,9 @@ bot.command('becomemaster', async (context) => { }); bot.on(message('contact'), async (context) => { - const customer = await getCustomer({ telegramId: context.from.id }); + const data = await getCustomer({ telegramId: context.from.id }); + const customer = data?.data?.customers?.at(0); + const isRegistration = !customer; const { contact } = context.message; diff --git a/apps/web/actions/contacts.ts b/apps/web/actions/contacts.ts index 8d9f530..3b1bcf4 100644 --- a/apps/web/actions/contacts.ts +++ b/apps/web/actions/contacts.ts @@ -9,11 +9,9 @@ export async function getClients() { const { user } = session; - const getCustomerClientsResponse = await getCustomerClients({ telegramId: user?.telegramId }); + const response = await getCustomerClients({ telegramId: user?.telegramId }); - return { - clients: getCustomerClientsResponse?.clients, - }; + return response.data?.customers?.at(0); } export async function getMasters() { @@ -22,9 +20,7 @@ export async function getMasters() { const { user } = session; - const getCustomerMastersResponse = await getCustomerMasters({ telegramId: user?.telegramId }); + const response = await getCustomerMasters({ telegramId: user?.telegramId }); - return { - masters: getCustomerMastersResponse?.masters, - }; + return response.data?.customers?.at(0); } diff --git a/apps/web/actions/orders.ts b/apps/web/actions/orders.ts new file mode 100644 index 0000000..f1891b0 --- /dev/null +++ b/apps/web/actions/orders.ts @@ -0,0 +1,4 @@ +'use server'; +import * as api from '@repo/graphql/api'; + +export const getOrder = api.getOrder; diff --git a/apps/web/actions/profile.ts b/apps/web/actions/profile.ts index 7d6043b..e92a3c2 100644 --- a/apps/web/actions/profile.ts +++ b/apps/web/actions/profile.ts @@ -3,17 +3,17 @@ import { authOptions } from '@/config/auth'; import { getCustomer, updateCustomerProfile } from '@repo/graphql/api'; import { type CustomerInput, type GetCustomerQueryVariables } from '@repo/graphql/types'; import { getServerSession } from 'next-auth/next'; -import { revalidatePath } from 'next/cache'; export async function getProfile(input?: GetCustomerQueryVariables) { const session = await getServerSession(authOptions); if (!session) throw new Error('Missing session'); const { user } = session; - const telegramId = input?.telegramId || user?.telegramId; - const customer = await getCustomer({ telegramId }); + const { data } = await getCustomer({ telegramId }); + const customer = data?.customers?.at(0); + return customer; } @@ -23,13 +23,12 @@ export async function updateProfile(input: CustomerInput) { const { user } = session; - const customer = await getCustomer({ telegramId: user?.telegramId }); + const { data } = await getCustomer({ telegramId: user?.telegramId }); + const customer = data.customers.at(0); if (!customer) throw new Error('Customer not found'); await updateCustomerProfile({ data: input, - documentId: customer.documentId, + documentId: customer?.documentId, }); - - revalidatePath('/profile'); } diff --git a/apps/web/actions/slots.ts b/apps/web/actions/slots.ts new file mode 100644 index 0000000..b2b440b --- /dev/null +++ b/apps/web/actions/slots.ts @@ -0,0 +1,60 @@ +'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 } from '@/utils/date'; +import * as api from '@repo/graphql/api'; +import type * as GQL from '@repo/graphql/types'; + +type AddSlotInput = Omit; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type FixTypescriptCringe = ApolloTypes.FetchResult; + +export async function addSlot(input: AddSlotInput) { + const customer = await getProfile(); + + return api.createSlot({ + ...input, + date: formatDate(input.date).db(), + master: customer?.documentId, + time_end: formatTime(input.time_end).db(), + time_start: formatTime(input.time_start).db(), + }); +} + +export async function getSlots(input: GQL.GetSlotsQueryVariables) { + const customer = await getProfile(); + + if (!customer?.documentId) throw new Error('Customer not found'); + + return api.getSlots({ + filters: { + ...input.filters, + master: { + documentId: { + eq: customer.documentId, + }, + }, + }, + }); +} + +export async function updateSlot(input: GQL.UpdateSlotMutationVariables) { + const customer = await getProfile(); + + if (!customer?.documentId) throw new Error('Customer not found'); + + return api.updateSlot({ + ...input, + data: { + ...input.data, + date: input.data?.date ? formatDate(input.data.date).db() : undefined, + time_end: input.data?.time_end ? formatTime(input.data.time_end).db() : undefined, + time_start: input.data?.time_start ? formatTime(input.data.time_start).db() : undefined, + }, + }); +} + +export const getSlot = api.getSlot; +export const deleteSlot = api.deleteSlot; diff --git a/apps/web/app/(main)/contacts/page.tsx b/apps/web/app/(main)/contacts/page.tsx index 2bd43e6..34c2045 100644 --- a/apps/web/app/(main)/contacts/page.tsx +++ b/apps/web/app/(main)/contacts/page.tsx @@ -8,7 +8,7 @@ export default function ContactsPage() {
-

Контакты

+

Контакты

diff --git a/apps/web/app/(main)/layout.tsx b/apps/web/app/(main)/layout.tsx index b3c37f7..9369ed8 100644 --- a/apps/web/app/(main)/layout.tsx +++ b/apps/web/app/(main)/layout.tsx @@ -4,7 +4,7 @@ import { type PropsWithChildren } from 'react'; export default async function Layout({ children }: Readonly) { return ( <> -
{children}
+
{children}
); diff --git a/apps/web/app/(main)/orders/add/page.tsx b/apps/web/app/(main)/orders/add/page.tsx new file mode 100644 index 0000000..bf5b793 --- /dev/null +++ b/apps/web/app/(main)/orders/add/page.tsx @@ -0,0 +1,3 @@ +export default function AddOrdersPage() { + return 'Add Orders'; +} diff --git a/apps/web/app/(main)/orders/page.tsx b/apps/web/app/(main)/orders/page.tsx new file mode 100644 index 0000000..b339e29 --- /dev/null +++ b/apps/web/app/(main)/orders/page.tsx @@ -0,0 +1,3 @@ +export default function OrdersPage() { + return 'Orders'; +} diff --git a/apps/web/app/(main)/profile/[telegramId]/page.tsx b/apps/web/app/(main)/profile/[telegramId]/page.tsx index c94b1b4..dac16f6 100644 --- a/apps/web/app/(main)/profile/[telegramId]/page.tsx +++ b/apps/web/app/(main)/profile/[telegramId]/page.tsx @@ -1,6 +1,6 @@ -import { getProfile } from '@/actions/profile'; +import { Container } from '@/components/layout'; import { PageHeader } from '@/components/navigation'; -import { ProfileCard } from '@/components/profile/profile-card'; +import { ContactDataCard, PersonCard } from '@/components/profile'; import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; type Props = { params: Promise<{ telegramId: string }> }; @@ -11,15 +11,13 @@ export default async function ProfilePage(props: Readonly) { const queryClient = new QueryClient(); - await queryClient.prefetchQuery({ - queryFn: () => getProfile({ telegramId }), - queryKey: telegramId ? ['profile', 'telegramId', telegramId] : ['profile'], - }); - return ( - + + + + ); } diff --git a/apps/web/app/(main)/profile/page.tsx b/apps/web/app/(main)/profile/page.tsx index 1fbf2a9..372caa5 100644 --- a/apps/web/app/(main)/profile/page.tsx +++ b/apps/web/app/(main)/profile/page.tsx @@ -1,18 +1,17 @@ -import { getProfile } from '@/actions/profile'; -import { ProfileCard } from '@/components/profile/profile-card'; +import { Container } from '@/components/layout'; +import { LinksCard, PersonCard, ProfileDataCard } from '@/components/profile'; import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; export default async function ProfilePage() { const queryClient = new QueryClient(); - await queryClient.prefetchQuery({ - queryFn: () => getProfile(), - queryKey: ['profile'], - }); - return ( - + + + + + ); } diff --git a/apps/web/app/(main)/profile/schedule/layout.tsx b/apps/web/app/(main)/profile/schedule/layout.tsx new file mode 100644 index 0000000..4e7490d --- /dev/null +++ b/apps/web/app/(main)/profile/schedule/layout.tsx @@ -0,0 +1,6 @@ +import { ScheduleContextProvider } from '@/context/schedule'; +import { type PropsWithChildren } from 'react'; + +export default async function Layout({ children }: Readonly) { + return {children}; +} diff --git a/apps/web/app/(main)/profile/schedule/page.tsx b/apps/web/app/(main)/profile/schedule/page.tsx new file mode 100644 index 0000000..dc86121 --- /dev/null +++ b/apps/web/app/(main)/profile/schedule/page.tsx @@ -0,0 +1,15 @@ +import { Container } from '@/components/layout'; +import { PageHeader } from '@/components/navigation'; +import { DaySlotsList, ScheduleCalendar } from '@/components/schedule'; + +export default function SchedulePage() { + return ( + <> + + + + + + + ); +} diff --git a/apps/web/app/(main)/profile/schedule/slots/[documentId]/page.tsx b/apps/web/app/(main)/profile/schedule/slots/[documentId]/page.tsx new file mode 100644 index 0000000..c7eaf20 --- /dev/null +++ b/apps/web/app/(main)/profile/schedule/slots/[documentId]/page.tsx @@ -0,0 +1,23 @@ +import { Container } from '@/components/layout'; +import { PageHeader } from '@/components/navigation'; +import { SlotButtons, SlotDateTime, SlotOrdersList } from '@/components/schedule'; +import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; + +type Props = { params: Promise<{ documentId: string }> }; + +export default async function ProfilePage(props: Readonly) { + const parameters = await props.params; + + const queryClient = new QueryClient(); + + return ( + + + + + + + + + ); +} diff --git a/apps/web/app/(main)/records/add/page.tsx b/apps/web/app/(main)/records/add/page.tsx deleted file mode 100644 index 4b8196a..0000000 --- a/apps/web/app/(main)/records/add/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function AddRecordsPage() { - return 'Add Records'; -} diff --git a/apps/web/app/(main)/records/page.tsx b/apps/web/app/(main)/records/page.tsx deleted file mode 100644 index 061b3a7..0000000 --- a/apps/web/app/(main)/records/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function RecordsPage() { - return 'Records'; -} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 6c91944..e083a00 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,12 +1,17 @@ import { AuthProvider } from '@/providers/auth'; +import { ErrorProvider } from '@/providers/error'; import { QueryProvider } from '@/providers/query'; import { ThemeProvider } from '@/providers/theme-provider'; import { I18nProvider } from '@/utils/i18n/provider'; import '@repo/ui/globals.css'; +import { cn } from '@repo/ui/lib/utils'; import { type Metadata } from 'next'; import { getLocale } from 'next-intl/server'; +import { Inter } from 'next/font/google'; import { type PropsWithChildren } from 'react'; +const inter = Inter({ subsets: ['latin', 'cyrillic'] }); + export const metadata: Metadata = { title: 'Запишись.онлайн', }; @@ -16,14 +21,16 @@ export default async function RootLayout({ children }: Readonly - - - - - {children} - - - + + + + + + {children} + + + + ); diff --git a/apps/web/components/common/divider.tsx b/apps/web/components/common/divider.tsx new file mode 100644 index 0000000..569f7b1 --- /dev/null +++ b/apps/web/components/common/divider.tsx @@ -0,0 +1,13 @@ +import { cn } from '@repo/ui/lib/utils'; + +type Props = { + readonly className?: string; +}; + +export function HorizontalDivider({ className }: Props) { + return ( +
+
+
+ ); +} diff --git a/apps/web/components/contacts/dropdown-filter.tsx b/apps/web/components/contacts/dropdown-filter.tsx index 62645f9..bc29eda 100644 --- a/apps/web/components/contacts/dropdown-filter.tsx +++ b/apps/web/components/contacts/dropdown-filter.tsx @@ -10,7 +10,7 @@ import { import { ChevronDown } from 'lucide-react'; import { use } from 'react'; -const filterLabels: Record = { +const filterLabels: Order = { all: 'Все', clients: 'Клиенты', masters: 'Мастера', diff --git a/apps/web/components/layout/container.tsx b/apps/web/components/layout/container.tsx new file mode 100644 index 0000000..3c339f0 --- /dev/null +++ b/apps/web/components/layout/container.tsx @@ -0,0 +1,9 @@ +import { cn } from '@repo/ui/lib/utils'; +import { type PropsWithChildren } from 'react'; + +export function Container({ + children, + className, +}: Readonly & { readonly className?: string }) { + return
{children}
; +} diff --git a/apps/web/components/layout/index.ts b/apps/web/components/layout/index.ts new file mode 100644 index 0000000..85ee15b --- /dev/null +++ b/apps/web/components/layout/index.ts @@ -0,0 +1 @@ +export * from './container'; diff --git a/apps/web/components/navigation/header/index.tsx b/apps/web/components/navigation/header/index.tsx index 1dbc698..21424e9 100644 --- a/apps/web/components/navigation/header/index.tsx +++ b/apps/web/components/navigation/header/index.tsx @@ -1,14 +1,13 @@ 'use client'; import { BackButton } from './back-button'; -import { PageTitle } from './page-title'; type Props = { title: string | undefined }; export function PageHeader(props: Readonly) { return ( -
+
- + {props.title}
); } diff --git a/apps/web/components/navigation/header/page-title.tsx b/apps/web/components/navigation/header/page-title.tsx deleted file mode 100644 index 967c468..0000000 --- a/apps/web/components/navigation/header/page-title.tsx +++ /dev/null @@ -1,5 +0,0 @@ -type Props = { readonly title: string | undefined }; - -export function PageTitle(props: Readonly) { - return {props?.title}; -} diff --git a/apps/web/components/navigation/navbar/index.tsx b/apps/web/components/navigation/navbar/index.tsx index 380c1af..fcb25e4 100644 --- a/apps/web/components/navigation/navbar/index.tsx +++ b/apps/web/components/navigation/navbar/index.tsx @@ -10,11 +10,11 @@ export function BottomNav() { if (!isFirstLevel) return null; return ( -