Добавлен пакет react-hot-toast для уведомлений и обновлены конфигурации. Исправлены сообщения об ошибках на использование toast вместо alert. Обновлены типы GraphQL для поддержки новых полей в заказах. Оптимизировано форматирование расписания работы ПВЗ с группировкой дней.
This commit is contained in:
27
package-lock.json
generated
27
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -1,4 +1,4 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -1,15 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { Toaster } from 'sonner'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
|
||||
export const ToastProvider = () => {
|
||||
return (
|
||||
<Toaster
|
||||
position="top-right"
|
||||
richColors
|
||||
closeButton
|
||||
expand={false}
|
||||
duration={4000}
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
style: {
|
||||
background: '#fff',
|
||||
color: '#333',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
},
|
||||
success: {
|
||||
iconTheme: {
|
||||
primary: '#10B981',
|
||||
secondary: '#fff',
|
||||
},
|
||||
},
|
||||
error: {
|
||||
iconTheme: {
|
||||
primary: '#EF4444',
|
||||
secondary: '#fff',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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<string, any> | 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: {
|
||||
|
@ -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 типы для категорий автозапчастей
|
||||
|
@ -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<string, number[]> = {};
|
||||
|
||||
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('; ');
|
||||
}
|
||||
|
||||
|
57
tailwind.config.js
Normal file
57
tailwind.config.js
Normal file
@ -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: [],
|
||||
}
|
Reference in New Issue
Block a user