Compare commits
2 Commits
18e1f3ffb1
...
52f01f5b12
Author | SHA1 | Date | |
---|---|---|---|
52f01f5b12 | |||
d501ad3354 |
27
package-lock.json
generated
27
package-lock.json
generated
@ -61,6 +61,7 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.57.0",
|
"react-hook-form": "^7.57.0",
|
||||||
|
"react-hot-toast": "^2.5.2",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.5",
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.3.0",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
@ -8428,6 +8429,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
@ -11166,6 +11176,23 @@
|
|||||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
"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": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
@ -71,6 +71,7 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.57.0",
|
"react-hook-form": "^7.57.0",
|
||||||
|
"react-hot-toast": "^2.5.2",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.5",
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.3.0",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('postcss-load-config').Config} */
|
||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
'@tailwindcss/postcss': {},
|
'@tailwindcss/postcss': {},
|
||||||
|
@ -24,6 +24,7 @@ import { ImportProductsModal } from '@/components/catalog/ImportProductsModal'
|
|||||||
import { Pagination } from '@/components/ui/pagination'
|
import { Pagination } from '@/components/ui/pagination'
|
||||||
import { GET_CATEGORIES, GET_PRODUCTS, GET_PRODUCTS_COUNT } from '@/lib/graphql/queries'
|
import { GET_CATEGORIES, GET_PRODUCTS, GET_PRODUCTS_COUNT } from '@/lib/graphql/queries'
|
||||||
import { EXPORT_PRODUCTS } from '@/lib/graphql/mutations'
|
import { EXPORT_PRODUCTS } from '@/lib/graphql/mutations'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -106,7 +107,7 @@ export default function CatalogPage() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка экспорта:', error)
|
console.error('Ошибка экспорта:', error)
|
||||||
alert('Не удалось экспортировать товары')
|
toast.error('Не удалось экспортировать товары')
|
||||||
} finally {
|
} finally {
|
||||||
setExportLoading(false)
|
setExportLoading(false)
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
const GET_BALANCE_INVOICES = gql`
|
const GET_BALANCE_INVOICES = gql`
|
||||||
query GetBalanceInvoices {
|
query GetBalanceInvoices {
|
||||||
@ -127,7 +128,7 @@ export default function InvoicesPage() {
|
|||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error('Ошибка обновления статуса счета:', error)
|
console.error('Ошибка обновления статуса счета:', error)
|
||||||
alert('Ошибка обновления статуса: ' + error.message)
|
toast.error('Ошибка обновления статуса: ' + error.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -175,11 +176,11 @@ export default function InvoicesPage() {
|
|||||||
window.URL.revokeObjectURL(url)
|
window.URL.revokeObjectURL(url)
|
||||||
document.body.removeChild(a)
|
document.body.removeChild(a)
|
||||||
} else {
|
} else {
|
||||||
alert('Ошибка получения PDF: ' + (data?.getInvoicePDF?.error || 'Неизвестная ошибка'))
|
toast.error('Ошибка получения PDF: ' + (data?.getInvoicePDF?.error || 'Неизвестная ошибка'))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка скачивания PDF:', error)
|
console.error('Ошибка скачивания PDF:', error)
|
||||||
alert('Ошибка скачивания PDF: ' + (error as Error).message)
|
toast.error('Ошибка скачивания PDF: ' + (error as Error).message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
|
@ -1,15 +1,32 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Toaster } from 'sonner'
|
import { Toaster } from 'react-hot-toast'
|
||||||
|
|
||||||
export const ToastProvider = () => {
|
export const ToastProvider = () => {
|
||||||
return (
|
return (
|
||||||
<Toaster
|
<Toaster
|
||||||
position="top-right"
|
position="top-right"
|
||||||
richColors
|
toastOptions={{
|
||||||
closeButton
|
duration: 4000,
|
||||||
expand={false}
|
style: {
|
||||||
duration={4000}
|
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[]
|
items: OrderItemInput[]
|
||||||
deliveryAddress?: string
|
deliveryAddress?: string
|
||||||
comment?: string
|
comment?: string
|
||||||
|
paymentMethod?: string
|
||||||
|
legalEntityId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OrderItemInput {
|
interface OrderItemInput {
|
||||||
@ -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, {
|
partsIndexSearchByArticle: async (_: unknown, {
|
||||||
articleNumber,
|
articleNumber,
|
||||||
brandName,
|
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) => {
|
updateProductVisibility: async (_: unknown, { id, isVisible }: { id: string; isVisible: boolean }, context: Context) => {
|
||||||
try {
|
try {
|
||||||
if (!context.userId) {
|
if (!context.userId) {
|
||||||
@ -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 = [] }: {
|
createClient: async (_: unknown, { input, vehicles = [], discounts = [] }: {
|
||||||
input: ClientInput; vehicles?: ClientVehicleInput[]; discounts?: ClientDiscountInput[]
|
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) => {
|
createClientDeliveryAddress: async (_: unknown, { clientId, input }: { clientId: string; input: ClientDeliveryAddressInput }, context: Context) => {
|
||||||
try {
|
try {
|
||||||
@ -7448,6 +7108,41 @@ export const resolvers = {
|
|||||||
? clientId.substring(7)
|
? clientId.substring(7)
|
||||||
: clientId
|
: 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({
|
const order = await prisma.order.create({
|
||||||
data: {
|
data: {
|
||||||
orderNumber,
|
orderNumber,
|
||||||
@ -7458,7 +7153,7 @@ export const resolvers = {
|
|||||||
totalAmount,
|
totalAmount,
|
||||||
finalAmount: totalAmount, // Пока без скидок
|
finalAmount: totalAmount, // Пока без скидок
|
||||||
deliveryAddress: input.deliveryAddress,
|
deliveryAddress: input.deliveryAddress,
|
||||||
comment: input.comment,
|
comment: `${input.comment || ''}${input.paymentMethod ? ` | Способ оплаты: ${input.paymentMethod}` : ''}${input.legalEntityId ? ` | ЮЛ ID: ${input.legalEntityId}` : ''}`,
|
||||||
items: {
|
items: {
|
||||||
create: input.items.map(item => ({
|
create: input.items.map(item => ({
|
||||||
productId: item.productId,
|
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)
|
console.log('createOrder: заказ создан:', order.orderNumber)
|
||||||
return order
|
return order
|
||||||
} catch (error) {
|
} 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 }: {
|
getDeliveryOffers: async (_: unknown, { input }: {
|
||||||
input: {
|
input: {
|
||||||
|
@ -1777,6 +1777,8 @@ export const typeDefs = gql`
|
|||||||
items: [OrderItemInput!]!
|
items: [OrderItemInput!]!
|
||||||
deliveryAddress: String
|
deliveryAddress: String
|
||||||
comment: String
|
comment: String
|
||||||
|
paymentMethod: String
|
||||||
|
legalEntityId: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input OrderItemInput {
|
input OrderItemInput {
|
||||||
@ -1798,8 +1800,10 @@ export const typeDefs = gql`
|
|||||||
|
|
||||||
# Результат создания платежа
|
# Результат создания платежа
|
||||||
type CreatePaymentResult {
|
type CreatePaymentResult {
|
||||||
payment: Payment!
|
payment: Payment
|
||||||
confirmationUrl: String!
|
confirmationUrl: String
|
||||||
|
success: Boolean!
|
||||||
|
message: String
|
||||||
}
|
}
|
||||||
|
|
||||||
# PartsAPI типы для категорий автозапчастей
|
# PartsAPI типы для категорий автозапчастей
|
||||||
|
@ -205,7 +205,7 @@ class YandexDeliveryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Форматирование расписания работы ПВЗ
|
* Форматирование расписания работы ПВЗ с группировкой дней
|
||||||
*/
|
*/
|
||||||
formatSchedule(schedule: YandexPickupPoint['schedule']): string {
|
formatSchedule(schedule: YandexPickupPoint['schedule']): string {
|
||||||
if (!schedule.restrictions || schedule.restrictions.length === 0) {
|
if (!schedule.restrictions || schedule.restrictions.length === 0) {
|
||||||
@ -214,12 +214,57 @@ class YandexDeliveryService {
|
|||||||
|
|
||||||
const dayNames = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
|
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 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 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('; ');
|
}).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