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