From d501ad33546cfe3721c7bf2fedd1964dadd1b035 Mon Sep 17 00:00:00 2001 From: Bivekich Date: Sun, 29 Jun 2025 03:36:20 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=20react-hot-toast=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B8=20=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8?= =?UTF-8?q?=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8.=20=D0=98=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=81=D0=BE?= =?UTF-8?q?=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BE=D0=B1=20?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=D1=85=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20toast=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=BE=20alert.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=82=D0=B8=D0=BF=D1=8B=20GraphQL=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=B5=D0=B9=20=D0=B2=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=B0?= =?UTF-8?q?=D1=85.=20=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BE=20=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B0=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B0=D1=81=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D0=9F=D0=92=D0=97=20?= =?UTF-8?q?=D1=81=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=BE=D0=B9=20=D0=B4=D0=BD=D0=B5=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 27 + package.json | 1 + postcss.config.mjs | 2 +- src/app/dashboard/catalog/page.tsx | 3 +- src/app/dashboard/invoices/page.tsx | 7 +- src/app/globals.css | 6 +- src/components/providers/ToastProvider.tsx | 27 +- src/lib/graphql/resolvers.ts | 588 ++++++++------------- src/lib/graphql/typeDefs.ts | 8 +- src/lib/yandex-delivery-service.ts | 53 +- tailwind.config.js | 57 ++ 11 files changed, 403 insertions(+), 376 deletions(-) create mode 100644 tailwind.config.js diff --git a/package-lock.json b/package-lock.json index 3f1868c..bffb7ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.57.0", + "react-hot-toast": "^2.5.2", "sonner": "^2.0.5", "tailwind-merge": "^3.3.0", "tailwindcss": "^4", @@ -8428,6 +8429,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -11166,6 +11176,23 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-hot-toast": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", + "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index d0672db..15114c8 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.57.0", + "react-hot-toast": "^2.5.2", "sonner": "^2.0.5", "tailwind-merge": "^3.3.0", "tailwindcss": "^4", diff --git a/postcss.config.mjs b/postcss.config.mjs index 7f764a5..4c12660 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,4 +1,4 @@ -/** @type {import('tailwindcss').Config} */ +/** @type {import('postcss-load-config').Config} */ export default { plugins: { '@tailwindcss/postcss': {}, diff --git a/src/app/dashboard/catalog/page.tsx b/src/app/dashboard/catalog/page.tsx index 0fe1095..8bb2403 100644 --- a/src/app/dashboard/catalog/page.tsx +++ b/src/app/dashboard/catalog/page.tsx @@ -24,6 +24,7 @@ import { ImportProductsModal } from '@/components/catalog/ImportProductsModal' import { Pagination } from '@/components/ui/pagination' import { GET_CATEGORIES, GET_PRODUCTS, GET_PRODUCTS_COUNT } from '@/lib/graphql/queries' import { EXPORT_PRODUCTS } from '@/lib/graphql/mutations' +import toast from 'react-hot-toast' @@ -106,7 +107,7 @@ export default function CatalogPage() { } } catch (error) { console.error('Ошибка экспорта:', error) - alert('Не удалось экспортировать товары') + toast.error('Не удалось экспортировать товары') } finally { setExportLoading(false) } diff --git a/src/app/dashboard/invoices/page.tsx b/src/app/dashboard/invoices/page.tsx index b8f3f99..a282b3b 100644 --- a/src/app/dashboard/invoices/page.tsx +++ b/src/app/dashboard/invoices/page.tsx @@ -31,6 +31,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' +import toast from 'react-hot-toast' const GET_BALANCE_INVOICES = gql` query GetBalanceInvoices { @@ -127,7 +128,7 @@ export default function InvoicesPage() { }, onError: (error) => { console.error('Ошибка обновления статуса счета:', error) - alert('Ошибка обновления статуса: ' + error.message) + toast.error('Ошибка обновления статуса: ' + error.message) } }) @@ -175,11 +176,11 @@ export default function InvoicesPage() { window.URL.revokeObjectURL(url) document.body.removeChild(a) } else { - alert('Ошибка получения PDF: ' + (data?.getInvoicePDF?.error || 'Неизвестная ошибка')) + toast.error('Ошибка получения PDF: ' + (data?.getInvoicePDF?.error || 'Неизвестная ошибка')) } } catch (error) { console.error('Ошибка скачивания PDF:', error) - alert('Ошибка скачивания PDF: ' + (error as Error).message) + toast.error('Ошибка скачивания PDF: ' + (error as Error).message) } } diff --git a/src/app/globals.css b/src/app/globals.css index dc98be7..e00f07c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,8 +1,8 @@ -@import "tailwindcss"; +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; @import "tw-animate-css"; -@custom-variant dark (&:is(.dark *)); - @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); diff --git a/src/components/providers/ToastProvider.tsx b/src/components/providers/ToastProvider.tsx index 0345557..d68bad0 100644 --- a/src/components/providers/ToastProvider.tsx +++ b/src/components/providers/ToastProvider.tsx @@ -1,15 +1,32 @@ "use client" -import { Toaster } from 'sonner' +import { Toaster } from 'react-hot-toast' export const ToastProvider = () => { return ( ) } \ No newline at end of file diff --git a/src/lib/graphql/resolvers.ts b/src/lib/graphql/resolvers.ts index 8882fe3..5162081 100644 --- a/src/lib/graphql/resolvers.ts +++ b/src/lib/graphql/resolvers.ts @@ -377,6 +377,8 @@ interface CreateOrderInput { items: OrderItemInput[] deliveryAddress?: string comment?: string + paymentMethod?: string + legalEntityId?: string } interface OrderItemInput { @@ -535,18 +537,18 @@ export const resolvers = { }) }, relatedProducts: async (parent: { id: string }) => { - const product = await prisma.product.findUnique({ - where: { id: parent.id }, - include: { relatedProducts: { include: { images: { orderBy: { order: 'asc' } } } } } - }) - return product?.relatedProducts || [] + const product = await prisma.product.findUnique({ + where: { id: parent.id }, + include: { relatedProducts: { include: { images: { orderBy: { order: 'asc' } } } } } + }) + return product?.relatedProducts || [] }, accessoryProducts: async (parent: { id: string }) => { - const product = await prisma.product.findUnique({ - where: { id: parent.id }, - include: { accessoryProducts: { include: { images: { orderBy: { order: 'asc' } } } } } - }) - return product?.accessoryProducts || [] + const product = await prisma.product.findUnique({ + where: { id: parent.id }, + include: { accessoryProducts: { include: { images: { orderBy: { order: 'asc' } } } } } + }) + return product?.accessoryProducts || [] } }, @@ -2754,66 +2756,6 @@ export const resolvers = { } }, - partsIndexCatalogParams: async (_: unknown, { - catalogId, - groupId, - lang = 'ru', - engineId, - generationId, - params, - q - }: { - catalogId: string; - groupId: string; - lang?: 'ru' | 'en'; - engineId?: string; - generationId?: string; - params?: string; - q?: string; - }) => { - try { - console.log('🔍 GraphQL resolver partsIndexCatalogParams вызван с параметрами:', { - catalogId, - groupId, - lang, - q - }) - - // Преобразуем строку params в объект если передан - let parsedParams: Record | undefined; - if (params) { - try { - parsedParams = JSON.parse(params); - } catch (error) { - console.warn('⚠️ Не удалось разобрать параметры фильтрации:', params); - } - } - - const parameters = await partsIndexService.getCatalogParams(catalogId, groupId, { - lang, - engineId, - generationId, - params: parsedParams, - q - }) - - if (!parameters) { - console.warn('⚠️ Не удалось получить параметры каталога') - return { - list: [], - paramsQuery: {} - } - } - - console.log('✅ Получены параметры каталога:', parameters.list.length) - - return parameters - } catch (error) { - console.error('❌ Ошибка в GraphQL resolver partsIndexCatalogParams:', error) - throw new Error('Не удалось получить параметры каталога') - } - }, - partsIndexSearchByArticle: async (_: unknown, { articleNumber, brandName, @@ -4178,248 +4120,6 @@ export const resolvers = { } }, - updateProduct: async (_: unknown, { - id, - input, - images = [], - characteristics = [], - options = [] - }: { - id: string; - input: ProductInput; - images?: ProductImageInput[]; - characteristics?: CharacteristicInput[]; - options?: ProductOptionInput[] - }, context: Context) => { - try { - console.log('updateProduct вызван с опциями:', JSON.stringify(options, null, 2)) - - if (!context.userId) { - throw new Error('Пользователь не авторизован') - } - - const { categoryIds, ...productData } = input - - // Удаляем старые изображения, характеристики и опции - await prisma.productImage.deleteMany({ - where: { productId: id } - }) - - await prisma.productCharacteristic.deleteMany({ - where: { productId: id } - }) - - await prisma.productOption.deleteMany({ - where: { productId: id } - }) - - // Обновляем основные данные товара - const finalData = { ...productData } - if (input.name && !input.slug) { - finalData.slug = createSlug(input.name) - } - - await prisma.product.update({ - where: { id }, - data: { - ...finalData, - categories: categoryIds ? { - set: categoryIds.map((catId: string) => ({ id: catId })) - } : undefined, - images: { - create: images.map((img, index) => ({ - ...img, - order: img.order ?? index - })) - } - } - }) - - // Добавляем новые характеристики - for (const char of characteristics) { - let characteristic = await prisma.characteristic.findUnique({ - where: { name: char.name } - }) - - if (!characteristic) { - characteristic = await prisma.characteristic.create({ - data: { name: char.name } - }) - } - - await prisma.productCharacteristic.create({ - data: { - productId: id, - characteristicId: characteristic.id, - value: char.value - } - }) - } - - // Добавляем новые опции - console.log(`Обрабатываем ${options.length} опций для товара ${id}`) - for (const optionInput of options) { - console.log('Обрабатываем опцию:', optionInput) - - // Создаём или находим опцию - let option = await prisma.option.findUnique({ - where: { name: optionInput.name } - }) - - if (!option) { - console.log('Создаем новую опцию:', optionInput.name) - option = await prisma.option.create({ - data: { - name: optionInput.name, - type: optionInput.type - } - }) - } else { - console.log('Найдена существующая опция:', option.id) - } - - // Создаём значения опции и связываем с товаром - for (const valueInput of optionInput.values) { - console.log('Обрабатываем значение опции:', valueInput) - - // Создаём или находим значение опции - let optionValue = await prisma.optionValue.findFirst({ - where: { - optionId: option.id, - value: valueInput.value - } - }) - - if (!optionValue) { - console.log('Создаем новое значение опции') - optionValue = await prisma.optionValue.create({ - data: { - optionId: option.id, - value: valueInput.value, - price: valueInput.price || 0 - } - }) - } else { - console.log('Найдено существующее значение опции:', optionValue.id) - } - - // Связываем товар с опцией и значением - console.log('Создаем связь товар-опция-значение') - await prisma.productOption.create({ - data: { - productId: id, - optionId: option.id, - optionValueId: optionValue.id - } - }) - } - } - console.log('Все опции обработаны') - - // Получаем обновленный товар со всеми связанными данными - const product = await prisma.product.findUnique({ - where: { id }, - include: { - categories: true, - images: { orderBy: { order: 'asc' } }, - options: { - include: { - option: { include: { values: true } }, - optionValue: true - } - }, - characteristics: { include: { characteristic: true } }, - relatedProducts: { include: { images: { orderBy: { order: 'asc' } } } }, - accessoryProducts: { include: { images: { orderBy: { order: 'asc' } } } } - } - }) - - // Создаем запись в истории товара - if (context.userId && product) { - await prisma.productHistory.create({ - data: { - productId: id, - action: 'UPDATE', - changes: JSON.stringify({ - name: input.name, - article: input.article, - description: input.description, - wholesalePrice: input.wholesalePrice, - retailPrice: input.retailPrice, - stock: input.stock, - isVisible: input.isVisible, - categories: categoryIds, - images: images.length, - characteristics: characteristics.length, - options: options.length - }), - userId: context.userId - } - }) - } - - // Логируем действие - if (context.headers) { - const { ipAddress, userAgent } = getClientInfo(context.headers) - await createAuditLog({ - userId: context.userId, - action: AuditAction.PRODUCT_UPDATE, - details: `Товар "${product?.name}"`, - ipAddress, - userAgent - }) - } - - return product - } catch (error) { - console.error('Ошибка обновления товара:', error) - if (error instanceof Error) { - throw error - } - throw new Error('Не удалось обновить товар') - } - }, - - deleteProduct: async (_: unknown, { id }: { id: string }, context: Context) => { - try { - if (!context.userId) { - throw new Error('Пользователь не авторизован') - } - - const product = await prisma.product.findUnique({ - where: { id } - }) - - if (!product) { - throw new Error('Товар не найден') - } - - await prisma.product.delete({ - where: { id } - }) - - // Логируем действие - if (context.headers) { - const { ipAddress, userAgent } = getClientInfo(context.headers) - await createAuditLog({ - userId: context.userId, - action: AuditAction.PRODUCT_DELETE, - details: `Товар "${product.name}"`, - ipAddress, - userAgent - }) - } - - return true - } catch (error) { - console.error('Ошибка удаления товара:', error) - if (error instanceof Error) { - throw error - } - throw new Error('Не удалось удалить товар') - } - }, - updateProductVisibility: async (_: unknown, { id, isVisible }: { id: string; isVisible: boolean }, context: Context) => { try { if (!context.userId) { @@ -4668,7 +4368,7 @@ export const resolvers = { }) } - return { + return { url: uploadResult.url, filename, count: products.length @@ -4965,26 +4665,6 @@ export const resolvers = { } }, - deleteOption: async (_: unknown, { id }: { id: string }, context: Context) => { - try { - if (!context.userId) { - throw new Error('Пользователь не авторизован') - } - - await prisma.option.delete({ - where: { id } - }) - - return true - } catch (error) { - console.error('Ошибка удаления опции:', error) - if (error instanceof Error) { - throw error - } - throw new Error('Не удалось удалить опцию') - } - }, - // Клиенты createClient: async (_: unknown, { input, vehicles = [], discounts = [] }: { input: ClientInput; vehicles?: ClientVehicleInput[]; discounts?: ClientDiscountInput[] @@ -5755,26 +5435,6 @@ export const resolvers = { } }, - deleteClientVehicle: async (_: unknown, { id }: { id: string }, context: Context) => { - try { - if (!context.userId) { - throw new Error('Пользователь не авторизован') - } - - await prisma.clientVehicle.delete({ - where: { id } - }) - - return true - } catch (error) { - console.error('Ошибка удаления транспорта:', error) - if (error instanceof Error) { - throw error - } - throw new Error('Не удалось удалить транспорт') - } - }, - // Адреса доставки createClientDeliveryAddress: async (_: unknown, { clientId, input }: { clientId: string; input: ClientDeliveryAddressInput }, context: Context) => { try { @@ -6308,7 +5968,7 @@ export const resolvers = { console.log('✅ PDF успешно сгенерирован'); return { - success: true, + success: true, pdfBase64, filename }; @@ -6571,7 +6231,7 @@ export const resolvers = { }) const sessionId = Math.random().toString(36).substring(7) - + return { exists: !!client, client, @@ -6595,7 +6255,7 @@ export const resolvers = { console.log(`У номера ${phone} уже есть активный код, осталось ${ttl} секунд`) return { - success: true, + success: true, sessionId: finalSessionId, message: `Код уже отправлен. Попробуйте через ${ttl} секунд.` } @@ -7448,6 +7108,41 @@ export const resolvers = { ? clientId.substring(7) : clientId + // Проверяем баланс для оплаты с баланса + if (input.paymentMethod === 'balance') { + console.log('createOrder: проверяем баланс для оплаты с баланса') + + // Сначала ищем дефолтный активный контракт, если нет - любой активный + let contract = await prisma.clientContract.findFirst({ + where: { + clientId: cleanClientId, + isActive: true, + isDefault: true + } + }) + + if (!contract) { + // Если дефолтного нет, ищем любой активный + contract = await prisma.clientContract.findFirst({ + where: { + clientId: cleanClientId, + isActive: true + } + }) + } + + if (!contract) { + throw new Error('Активный контракт не найден') + } + + const availableBalance = (contract.balance || 0) + (contract.creditLimit || 0) + console.log(`createOrder: доступный баланс: ${availableBalance}, сумма заказа: ${totalAmount}`) + + if (availableBalance < totalAmount) { + throw new Error('Недостаточно средств на балансе для оплаты заказа') + } + } + const order = await prisma.order.create({ data: { orderNumber, @@ -7458,7 +7153,7 @@ export const resolvers = { totalAmount, finalAmount: totalAmount, // Пока без скидок deliveryAddress: input.deliveryAddress, - comment: input.comment, + comment: `${input.comment || ''}${input.paymentMethod ? ` | Способ оплаты: ${input.paymentMethod}` : ''}${input.legalEntityId ? ` | ЮЛ ID: ${input.legalEntityId}` : ''}`, items: { create: input.items.map(item => ({ productId: item.productId, @@ -7483,6 +7178,52 @@ export const resolvers = { } }) + // Если оплата с баланса, списываем средства и устанавливаем статус "Оплачен" + if (input.paymentMethod === 'balance') { + console.log('createOrder: списываем средства с баланса') + + // Ищем тот же контракт, который использовали для проверки баланса + let contractToUpdate = await prisma.clientContract.findFirst({ + where: { + clientId: cleanClientId, + isActive: true, + isDefault: true + } + }) + + if (!contractToUpdate) { + contractToUpdate = await prisma.clientContract.findFirst({ + where: { + clientId: cleanClientId, + isActive: true + } + }) + } + + if (contractToUpdate) { + await prisma.clientContract.update({ + where: { + id: contractToUpdate.id + }, + data: { + balance: { + decrement: totalAmount + } + } + }) + + console.log(`createOrder: списано ${totalAmount} ₽ с баланса контракта ${contractToUpdate.contractNumber}`) + + // Обновляем статус заказа на "Оплачен" + await prisma.order.update({ + where: { id: order.id }, + data: { status: 'PAID' } + }) + + console.log('createOrder: статус заказа изменен на PAID') + } + } + console.log('createOrder: заказ создан:', order.orderNumber) return order } catch (error) { @@ -7609,6 +7350,139 @@ export const resolvers = { } }, + // Resolver для подтверждения платежа + confirmPayment: async (_: unknown, { orderId }: { orderId: string }, context: Context) => { + try { + console.log('confirmPayment: подтверждение платежа для заказа:', orderId) + + // Находим заказ + const order = await prisma.order.findUnique({ + where: { id: orderId }, + include: { + client: true, + items: { + include: { + product: true + } + }, + payments: true + } + }) + + if (!order) { + throw new Error('Заказ не найден') + } + + // Если заказ уже оплачен, просто возвращаем его + if (order.status === 'PAID') { + console.log('confirmPayment: заказ уже оплачен') + return order + } + + // Обновляем статус заказа на "Оплачен" + const updatedOrder = await prisma.order.update({ + where: { id: orderId }, + data: { status: 'PAID' }, + include: { + client: true, + items: { + include: { + product: true + } + }, + payments: true + } + }) + + console.log('confirmPayment: статус заказа изменен на PAID') + return updatedOrder + + } catch (error) { + console.error('Ошибка подтверждения платежа:', error) + if (error instanceof Error) { + throw error + } + throw new Error('Не удалось подтвердить платеж') + } + }, + + // Resolver для создания платежа + createPayment: async (_: unknown, { input }: { input: CreatePaymentInput }, context: Context) => { + try { + console.log('createPayment: создание платежа для заказа:', input.orderId) + + // Находим заказ + const order = await prisma.order.findUnique({ + where: { id: input.orderId }, + include: { items: true } + }) + + if (!order) { + throw new Error('Заказ не найден') + } + + // Если заказ уже оплачен с баланса, не создаем платеж в ЮКассе + if (order.status === 'PAID') { + console.log('createPayment: заказ уже оплачен с баланса') + // Возвращаем успешный результат без создания платежа в ЮКассе + return { + payment: null, + confirmationUrl: null, + success: true, + message: 'Заказ уже оплачен с баланса' + } + } + + // Создаем платеж в ЮКассе + const { yooKassaService } = await import('../yookassa-service') + + const payment = await yooKassaService.createPayment({ + amount: order.finalAmount, + currency: 'RUB', + description: input.description || `Оплата заказа ${order.orderNumber}`, + returnUrl: input.returnUrl, + metadata: { orderId: order.id } + }) + + console.log('createPayment: платеж создан в ЮКассе:', payment.id) + + // Маппинг статусов YooKassa на GraphQL enum + const mapYooKassaStatus = (status: string) => { + switch (status) { + case 'pending': return 'PENDING' + case 'waiting_for_capture': return 'WAITING_FOR_CAPTURE' + case 'succeeded': return 'SUCCEEDED' + case 'canceled': return 'CANCELED' + default: return 'PENDING' + } + } + + return { + payment: { + id: payment.id, + orderId: order.id, + yookassaPaymentId: payment.id, + status: mapYooKassaStatus(payment.status), + amount: parseFloat(payment.amount.value), + currency: payment.amount.currency, + description: payment.description, + confirmationUrl: payment.confirmation?.confirmation_url || null, + createdAt: new Date().toISOString() + }, + confirmationUrl: payment.confirmation?.confirmation_url || null, + success: true, + message: 'Платеж успешно создан' + } + + } catch (error) { + console.error('Ошибка создания платежа:', error) + if (error instanceof Error) { + throw error + } + throw new Error('Не удалось создать платеж') + } + }, + // Мутация для получения офферов доставки getDeliveryOffers: async (_: unknown, { input }: { input: { diff --git a/src/lib/graphql/typeDefs.ts b/src/lib/graphql/typeDefs.ts index 310b113..5f0b90d 100644 --- a/src/lib/graphql/typeDefs.ts +++ b/src/lib/graphql/typeDefs.ts @@ -1777,6 +1777,8 @@ export const typeDefs = gql` items: [OrderItemInput!]! deliveryAddress: String comment: String + paymentMethod: String + legalEntityId: String } input OrderItemInput { @@ -1798,8 +1800,10 @@ export const typeDefs = gql` # Результат создания платежа type CreatePaymentResult { - payment: Payment! - confirmationUrl: String! + payment: Payment + confirmationUrl: String + success: Boolean! + message: String } # PartsAPI типы для категорий автозапчастей diff --git a/src/lib/yandex-delivery-service.ts b/src/lib/yandex-delivery-service.ts index edd5c1e..5f05b03 100644 --- a/src/lib/yandex-delivery-service.ts +++ b/src/lib/yandex-delivery-service.ts @@ -205,7 +205,7 @@ class YandexDeliveryService { } /** - * Форматирование расписания работы ПВЗ + * Форматирование расписания работы ПВЗ с группировкой дней */ formatSchedule(schedule: YandexPickupPoint['schedule']): string { if (!schedule.restrictions || schedule.restrictions.length === 0) { @@ -214,12 +214,57 @@ class YandexDeliveryService { const dayNames = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; - return schedule.restrictions.map(restriction => { - const days = restriction.days.map(day => dayNames[day - 1]).join(', '); + // Группируем ограничения по времени + const timeGroups: Record = {}; + + schedule.restrictions.forEach(restriction => { const timeFrom = `${restriction.time_from.hours.toString().padStart(2, '0')}:${restriction.time_from.minutes.toString().padStart(2, '0')}`; const timeTo = `${restriction.time_to.hours.toString().padStart(2, '0')}:${restriction.time_to.minutes.toString().padStart(2, '0')}`; + const timeRange = `${timeFrom}-${timeTo}`; - return `${days}: ${timeFrom}-${timeTo}`; + if (!timeGroups[timeRange]) { + timeGroups[timeRange] = []; + } + timeGroups[timeRange].push(...restriction.days); + }); + + // Форматируем каждую группу времени + return Object.entries(timeGroups).map(([timeRange, days]) => { + // Убираем дубликаты и сортируем дни + const uniqueDays = [...new Set(days)].sort((a, b) => a - b); + + // Группируем последовательные дни в диапазоны + const dayRanges: string[] = []; + let rangeStart = uniqueDays[0]; + let rangeEnd = uniqueDays[0]; + + for (let i = 1; i < uniqueDays.length; i++) { + const currentDay = uniqueDays[i]; + const prevDay = uniqueDays[i - 1]; + + // Если дни идут подряд, расширяем диапазон + if (currentDay === prevDay + 1) { + rangeEnd = currentDay; + } else { + // Завершаем текущий диапазон и начинаем новый + if (rangeStart === rangeEnd) { + dayRanges.push(dayNames[rangeStart - 1]); + } else { + dayRanges.push(`${dayNames[rangeStart - 1]}-${dayNames[rangeEnd - 1]}`); + } + rangeStart = currentDay; + rangeEnd = currentDay; + } + } + + // Добавляем последний диапазон + if (rangeStart === rangeEnd) { + dayRanges.push(dayNames[rangeStart - 1]); + } else { + dayRanges.push(`${dayNames[rangeStart - 1]}-${dayNames[rangeEnd - 1]}`); + } + + return `${dayRanges.join(', ')}: ${timeRange}`; }).join('; '); } diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..accf6d0 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,57 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + fontFamily: { + sans: ["var(--font-geist-sans)", "sans-serif"], + mono: ["var(--font-geist-mono)", "monospace"], + }, + }, + }, + plugins: [], +} \ No newline at end of file