Добавлены модели товаров и корзины для оптовиков, реализованы соответствующие мутации и запросы в GraphQL. Обновлен API для загрузки файлов с учетом новых типов данных. Улучшена обработка ошибок и добавлены новые функции для работы с категориями товаров.

This commit is contained in:
Bivekich
2025-07-17 16:36:07 +03:00
parent 6a94d51032
commit f377fbab5f
21 changed files with 3958 additions and 34 deletions

View File

@ -634,4 +634,187 @@ export const DELETE_SUPPLY = gql`
mutation DeleteSupply($id: ID!) {
deleteSupply(id: $id)
}
`
`
// Мутации для товаров оптовика
export const CREATE_PRODUCT = gql`
mutation CreateProduct($input: ProductInput!) {
createProduct(input: $input) {
success
message
product {
id
name
article
description
price
quantity
category {
id
name
}
brand
color
size
weight
dimensions
material
images
mainImage
isActive
createdAt
updatedAt
}
}
}
`
export const UPDATE_PRODUCT = gql`
mutation UpdateProduct($id: ID!, $input: ProductInput!) {
updateProduct(id: $id, input: $input) {
success
message
product {
id
name
article
description
price
quantity
category {
id
name
}
brand
color
size
weight
dimensions
material
images
mainImage
isActive
createdAt
updatedAt
}
}
}
`
export const DELETE_PRODUCT = gql`
mutation DeleteProduct($id: ID!) {
deleteProduct(id: $id)
}
`
// Мутации для корзины
export const ADD_TO_CART = gql`
mutation AddToCart($productId: ID!, $quantity: Int = 1) {
addToCart(productId: $productId, quantity: $quantity) {
success
message
cart {
id
totalPrice
totalItems
items {
id
quantity
totalPrice
isAvailable
availableQuantity
product {
id
name
article
price
quantity
images
mainImage
organization {
id
name
fullName
}
}
}
}
}
}
`
export const UPDATE_CART_ITEM = gql`
mutation UpdateCartItem($productId: ID!, $quantity: Int!) {
updateCartItem(productId: $productId, quantity: $quantity) {
success
message
cart {
id
totalPrice
totalItems
items {
id
quantity
totalPrice
isAvailable
availableQuantity
product {
id
name
article
price
quantity
images
mainImage
organization {
id
name
fullName
}
}
}
}
}
}
`
export const REMOVE_FROM_CART = gql`
mutation RemoveFromCart($productId: ID!) {
removeFromCart(productId: $productId) {
success
message
cart {
id
totalPrice
totalItems
items {
id
quantity
totalPrice
isAvailable
availableQuantity
product {
id
name
article
price
quantity
images
mainImage
organization {
id
name
fullName
}
}
}
}
}
}
`
export const CLEAR_CART = gql`
mutation ClearCart {
clearCart
}
`

View File

@ -77,6 +77,34 @@ export const GET_MY_SUPPLIES = gql`
}
`
export const GET_MY_PRODUCTS = gql`
query GetMyProducts {
myProducts {
id
name
article
description
price
quantity
category {
id
name
}
brand
color
size
weight
dimensions
material
images
mainImage
isActive
createdAt
updatedAt
}
}
`
// Запросы для контрагентов
export const SEARCH_ORGANIZATIONS = gql`
query SearchOrganizations($type: OrganizationType, $search: String) {
@ -300,4 +328,112 @@ export const GET_CONVERSATIONS = gql`
updatedAt
}
}
`
export const GET_CATEGORIES = gql`
query GetCategories {
categories {
id
name
createdAt
updatedAt
}
}
`
export const GET_ALL_PRODUCTS = gql`
query GetAllProducts($search: String, $category: String) {
allProducts(search: $search, category: $category) {
id
name
article
description
price
quantity
category {
id
name
}
brand
color
size
weight
dimensions
material
images
mainImage
isActive
createdAt
updatedAt
organization {
id
inn
name
fullName
type
address
phones
emails
users {
id
avatar
managerName
}
}
}
}
`
export const GET_MY_CART = gql`
query GetMyCart {
myCart {
id
totalPrice
totalItems
items {
id
quantity
totalPrice
isAvailable
availableQuantity
createdAt
updatedAt
product {
id
name
article
description
price
quantity
brand
color
size
images
mainImage
isActive
category {
id
name
}
organization {
id
inn
name
fullName
type
address
phones
emails
users {
id
avatar
managerName
}
}
}
}
createdAt
updatedAt
}
}
`

View File

@ -483,6 +483,161 @@ export const resolvers = {
include: { organization: true },
orderBy: { createdAt: 'desc' }
})
},
// Мои товары (для оптовиков)
myProducts: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что это оптовик
if (currentUser.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Товары доступны только для оптовиков')
}
return await prisma.product.findMany({
where: { organizationId: currentUser.organization.id },
include: {
category: true,
organization: true
},
orderBy: { createdAt: 'desc' }
})
},
// Все товары всех оптовиков для маркета
allProducts: async (_: unknown, args: { search?: string; category?: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
}
const where: Record<string, unknown> = {
isActive: true, // Показываем только активные товары
organization: {
type: 'WHOLESALE' // Только товары оптовиков
}
}
if (args.search) {
where.OR = [
{ name: { contains: args.search, mode: 'insensitive' } },
{ article: { contains: args.search, mode: 'insensitive' } },
{ description: { contains: args.search, mode: 'insensitive' } },
{ brand: { contains: args.search, mode: 'insensitive' } }
]
}
if (args.category) {
where.categoryId = args.category
}
return await prisma.product.findMany({
where,
include: {
category: true,
organization: {
include: {
users: true
}
}
},
orderBy: { createdAt: 'desc' },
take: 100 // Ограничиваем количество результатов
})
},
// Все категории
categories: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
}
return await prisma.category.findMany({
orderBy: { name: 'asc' }
})
},
// Корзина пользователя
myCart: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Найти или создать корзину для организации
let cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id },
include: {
items: {
include: {
product: {
include: {
category: true,
organization: {
include: {
users: true
}
}
}
}
}
},
organization: true
}
})
if (!cart) {
cart = await prisma.cart.create({
data: {
organizationId: currentUser.organization.id
},
include: {
items: {
include: {
product: {
include: {
category: true,
organization: {
include: {
users: true
}
}
}
}
}
},
organization: true
}
})
}
return cart
}
},
@ -592,7 +747,7 @@ export const resolvers = {
registerFulfillmentOrganization: async (
_: unknown,
args: { input: { phone: string; inn: string } },
args: { input: { phone: string; inn: string; type: 'FULFILLMENT' | 'LOGIST' | 'WHOLESALE' } },
context: Context
) => {
if (!context.user) {
@ -601,7 +756,7 @@ export const resolvers = {
})
}
const { inn } = args.input
const { inn, type } = args.input
// Валидируем ИНН
if (!dadataService.validateInn(inn)) {
@ -675,7 +830,7 @@ export const resolvers = {
revenue: organizationData.revenue,
taxSystem: organizationData.taxSystem,
type: 'FULFILLMENT',
type: type,
dadataData: JSON.parse(JSON.stringify(organizationData.rawData))
}
})
@ -695,7 +850,7 @@ export const resolvers = {
return {
success: true,
message: 'Фулфилмент организация успешно зарегистрирована',
message: 'Организация успешно зарегистрирована',
user: updatedUser
}
@ -2096,6 +2251,599 @@ export const resolvers = {
console.error('Error deleting supply:', error)
return false
}
},
// Создать товар
createProduct: async (_: unknown, args: {
input: {
name: string;
article: string;
description?: string;
price: number;
quantity: number;
categoryId?: string;
brand?: string;
color?: string;
size?: string;
weight?: number;
dimensions?: string;
material?: string;
images?: string[];
mainImage?: string;
isActive?: boolean;
}
}, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что это оптовик
if (currentUser.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Товары доступны только для оптовиков')
}
// Проверяем уникальность артикула в рамках организации
const existingProduct = await prisma.product.findFirst({
where: {
article: args.input.article,
organizationId: currentUser.organization.id
}
})
if (existingProduct) {
return {
success: false,
message: 'Товар с таким артикулом уже существует'
}
}
try {
const product = await prisma.product.create({
data: {
name: args.input.name,
article: args.input.article,
description: args.input.description,
price: args.input.price,
quantity: args.input.quantity,
categoryId: args.input.categoryId,
brand: args.input.brand,
color: args.input.color,
size: args.input.size,
weight: args.input.weight,
dimensions: args.input.dimensions,
material: args.input.material,
images: args.input.images || [],
mainImage: args.input.mainImage,
isActive: args.input.isActive ?? true,
organizationId: currentUser.organization.id
},
include: {
category: true,
organization: true
}
})
return {
success: true,
message: 'Товар успешно создан',
product
}
} catch (error) {
console.error('Error creating product:', error)
return {
success: false,
message: 'Ошибка при создании товара'
}
}
},
// Обновить товар
updateProduct: async (_: unknown, args: {
id: string;
input: {
name: string;
article: string;
description?: string;
price: number;
quantity: number;
categoryId?: string;
brand?: string;
color?: string;
size?: string;
weight?: number;
dimensions?: string;
material?: string;
images?: string[];
mainImage?: string;
isActive?: boolean;
}
}, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что товар принадлежит текущей организации
const existingProduct = await prisma.product.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id
}
})
if (!existingProduct) {
throw new GraphQLError('Товар не найден или нет доступа')
}
// Проверяем уникальность артикула (если он изменился)
if (args.input.article !== existingProduct.article) {
const duplicateProduct = await prisma.product.findFirst({
where: {
article: args.input.article,
organizationId: currentUser.organization.id,
NOT: { id: args.id }
}
})
if (duplicateProduct) {
return {
success: false,
message: 'Товар с таким артикулом уже существует'
}
}
}
try {
const product = await prisma.product.update({
where: { id: args.id },
data: {
name: args.input.name,
article: args.input.article,
description: args.input.description,
price: args.input.price,
quantity: args.input.quantity,
categoryId: args.input.categoryId,
brand: args.input.brand,
color: args.input.color,
size: args.input.size,
weight: args.input.weight,
dimensions: args.input.dimensions,
material: args.input.material,
images: args.input.images || [],
mainImage: args.input.mainImage,
isActive: args.input.isActive ?? true
},
include: {
category: true,
organization: true
}
})
return {
success: true,
message: 'Товар успешно обновлен',
product
}
} catch (error) {
console.error('Error updating product:', error)
return {
success: false,
message: 'Ошибка при обновлении товара'
}
}
},
// Удалить товар
deleteProduct: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что товар принадлежит текущей организации
const existingProduct = await prisma.product.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id
}
})
if (!existingProduct) {
throw new GraphQLError('Товар не найден или нет доступа')
}
try {
await prisma.product.delete({
where: { id: args.id }
})
return true
} catch (error) {
console.error('Error deleting product:', error)
return false
}
},
// Добавить товар в корзину
addToCart: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что товар существует и активен
const product = await prisma.product.findFirst({
where: {
id: args.productId,
isActive: true
},
include: {
organization: true
}
})
if (!product) {
return {
success: false,
message: 'Товар не найден или неактивен'
}
}
// Проверяем, что пользователь не пытается добавить свой собственный товар
if (product.organizationId === currentUser.organization.id) {
return {
success: false,
message: 'Нельзя добавлять собственные товары в корзину'
}
}
// Найти или создать корзину
let cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id }
})
if (!cart) {
cart = await prisma.cart.create({
data: {
organizationId: currentUser.organization.id
}
})
}
try {
// Проверяем, есть ли уже такой товар в корзине
const existingCartItem = await prisma.cartItem.findUnique({
where: {
cartId_productId: {
cartId: cart.id,
productId: args.productId
}
}
})
if (existingCartItem) {
// Обновляем количество
const newQuantity = existingCartItem.quantity + args.quantity
if (newQuantity > product.quantity) {
return {
success: false,
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`
}
}
await prisma.cartItem.update({
where: { id: existingCartItem.id },
data: { quantity: newQuantity }
})
} else {
// Создаем новый элемент корзины
if (args.quantity > product.quantity) {
return {
success: false,
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`
}
}
await prisma.cartItem.create({
data: {
cartId: cart.id,
productId: args.productId,
quantity: args.quantity
}
})
}
// Возвращаем обновленную корзину
const updatedCart = await prisma.cart.findUnique({
where: { id: cart.id },
include: {
items: {
include: {
product: {
include: {
category: true,
organization: {
include: {
users: true
}
}
}
}
}
},
organization: true
}
})
return {
success: true,
message: 'Товар добавлен в корзину',
cart: updatedCart
}
} catch (error) {
console.error('Error adding to cart:', error)
return {
success: false,
message: 'Ошибка при добавлении в корзину'
}
}
},
// Обновить количество товара в корзине
updateCartItem: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
const cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id }
})
if (!cart) {
return {
success: false,
message: 'Корзина не найдена'
}
}
// Проверяем, что товар существует в корзине
const cartItem = await prisma.cartItem.findUnique({
where: {
cartId_productId: {
cartId: cart.id,
productId: args.productId
}
},
include: {
product: true
}
})
if (!cartItem) {
return {
success: false,
message: 'Товар не найден в корзине'
}
}
if (args.quantity <= 0) {
return {
success: false,
message: 'Количество должно быть больше 0'
}
}
if (args.quantity > cartItem.product.quantity) {
return {
success: false,
message: `Недостаточно товара в наличии. Доступно: ${cartItem.product.quantity}`
}
}
try {
await prisma.cartItem.update({
where: { id: cartItem.id },
data: { quantity: args.quantity }
})
// Возвращаем обновленную корзину
const updatedCart = await prisma.cart.findUnique({
where: { id: cart.id },
include: {
items: {
include: {
product: {
include: {
category: true,
organization: {
include: {
users: true
}
}
}
}
}
},
organization: true
}
})
return {
success: true,
message: 'Количество товара обновлено',
cart: updatedCart
}
} catch (error) {
console.error('Error updating cart item:', error)
return {
success: false,
message: 'Ошибка при обновлении корзины'
}
}
},
// Удалить товар из корзины
removeFromCart: async (_: unknown, args: { productId: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
const cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id }
})
if (!cart) {
return {
success: false,
message: 'Корзина не найдена'
}
}
try {
await prisma.cartItem.delete({
where: {
cartId_productId: {
cartId: cart.id,
productId: args.productId
}
}
})
// Возвращаем обновленную корзину
const updatedCart = await prisma.cart.findUnique({
where: { id: cart.id },
include: {
items: {
include: {
product: {
include: {
category: true,
organization: {
include: {
users: true
}
}
}
}
}
},
organization: true
}
})
return {
success: true,
message: 'Товар удален из корзины',
cart: updatedCart
}
} catch (error) {
console.error('Error removing from cart:', error)
return {
success: false,
message: 'Ошибка при удалении из корзины'
}
}
},
// Очистить корзину
clearCart: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
const cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id }
})
if (!cart) {
return false
}
try {
await prisma.cartItem.deleteMany({
where: { cartId: cart.id }
})
return true
} catch (error) {
console.error('Error clearing cart:', error)
return false
}
}
},
@ -2114,6 +2862,29 @@ export const resolvers = {
}
},
Cart: {
totalPrice: (parent: { items: Array<{ product: { price: number }, quantity: number }> }) => {
return parent.items.reduce((total, item) => {
return total + (Number(item.product.price) * item.quantity)
}, 0)
},
totalItems: (parent: { items: Array<{ quantity: number }> }) => {
return parent.items.reduce((total, item) => total + item.quantity, 0)
}
},
CartItem: {
totalPrice: (parent: { product: { price: number }, quantity: number }) => {
return Number(parent.product.price) * parent.quantity
},
isAvailable: (parent: { product: { quantity: number, isActive: boolean }, quantity: number }) => {
return parent.product.isActive && parent.product.quantity >= parent.quantity
},
availableQuantity: (parent: { product: { quantity: number } }) => {
return parent.product.quantity
}
},
User: {
organization: async (parent: { organizationId?: string; organization?: unknown }) => {
// Если организация уже загружена через include, возвращаем её

View File

@ -28,6 +28,18 @@ export const typeDefs = gql`
# Расходники организации
mySupplies: [Supply!]!
# Товары оптовика
myProducts: [Product!]!
# Все товары всех оптовиков для маркета
allProducts(search: String, category: String): [Product!]!
# Все категории
categories: [Category!]!
# Корзина пользователя
myCart: Cart
}
type Mutation {
@ -77,6 +89,17 @@ export const typeDefs = gql`
createSupply(input: SupplyInput!): SupplyResponse!
updateSupply(id: ID!, input: SupplyInput!): SupplyResponse!
deleteSupply(id: ID!): Boolean!
# Работа с товарами (для оптовиков)
createProduct(input: ProductInput!): ProductResponse!
updateProduct(id: ID!, input: ProductInput!): ProductResponse!
deleteProduct(id: ID!): Boolean!
# Работа с корзиной
addToCart(productId: ID!, quantity: Int = 1): CartResponse!
updateCartItem(productId: ID!, quantity: Int!): CartResponse!
removeFromCart(productId: ID!): CartResponse!
clearCart: Boolean!
}
# Типы данных
@ -160,6 +183,7 @@ export const typeDefs = gql`
input FulfillmentRegistrationInput {
phone: String!
inn: String!
type: OrganizationType!
}
input SellerRegistrationInput {
@ -349,6 +373,89 @@ export const typeDefs = gql`
supply: Supply
}
# Типы для категорий товаров
type Category {
id: ID!
name: String!
createdAt: String!
updatedAt: String!
}
# Типы для товаров оптовика
type Product {
id: ID!
name: String!
article: String!
description: String
price: Float!
quantity: Int!
category: Category
brand: String
color: String
size: String
weight: Float
dimensions: String
material: String
images: [String!]!
mainImage: String
isActive: Boolean!
createdAt: String!
updatedAt: String!
organization: Organization!
}
input ProductInput {
name: String!
article: String!
description: String
price: Float!
quantity: Int!
categoryId: ID
brand: String
color: String
size: String
weight: Float
dimensions: String
material: String
images: [String!]
mainImage: String
isActive: Boolean
}
type ProductResponse {
success: Boolean!
message: String!
product: Product
}
# Типы для корзины
type Cart {
id: ID!
items: [CartItem!]!
totalPrice: Float!
totalItems: Int!
createdAt: String!
updatedAt: String!
organization: Organization!
}
type CartItem {
id: ID!
product: Product!
quantity: Int!
totalPrice: Float!
isAvailable: Boolean!
availableQuantity: Int!
createdAt: String!
updatedAt: String!
}
type CartResponse {
success: Boolean!
message: String!
cart: Cart
}
# JSON скаляр
scalar JSON
`