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

This commit is contained in:
Veronika Smirnova
2025-07-28 10:21:22 +03:00
parent 9f84316e00
commit 03af965050
8 changed files with 115 additions and 49 deletions

View File

@ -232,6 +232,7 @@ model Product {
description String? description String?
price Decimal @db.Decimal(12, 2) price Decimal @db.Decimal(12, 2)
quantity Int @default(0) quantity Int @default(0)
type ProductType @default(PRODUCT)
categoryId String? categoryId String?
brand String? brand String?
color String? color String?
@ -438,6 +439,11 @@ enum WildberriesSupplyStatus {
CANCELLED CANCELLED
} }
enum ProductType {
PRODUCT
CONSUMABLE
}
model Logistics { model Logistics {
id String @id @default(cuid()) id String @id @default(cuid())
fromLocation String fromLocation String

View File

@ -18,6 +18,7 @@ interface Product {
description: string description: string
price: number price: number
quantity: number quantity: number
type: 'PRODUCT' | 'CONSUMABLE'
category: { id: string; name: string } | null category: { id: string; name: string } | null
brand: string brand: string
color: string color: string
@ -180,13 +181,26 @@ export function ProductCard({ product, onEdit, onDeleted }: ProductCardProps) {
{/* Дополнительная информация */} {/* Дополнительная информация */}
<div className="space-y-1"> <div className="space-y-1">
{product.category && ( <div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-2"> {/* Тип товара */}
<Badge
variant="outline"
className={`text-xs ${
product.type === 'PRODUCT'
? 'bg-blue-500/20 text-blue-300 border-blue-400/30'
: 'bg-orange-500/20 text-orange-300 border-orange-400/30'
}`}
>
{product.type === 'PRODUCT' ? 'Товар' : 'Расходник'}
</Badge>
{/* Категория */}
{product.category && (
<Badge variant="outline" className="glass-secondary text-white/60 border-white/20 text-xs"> <Badge variant="outline" className="glass-secondary text-white/60 border-white/20 text-xs">
{product.category.name} {product.category.name}
</Badge> </Badge>
</div> )}
)} </div>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{product.brand && ( {product.brand && (

View File

@ -20,6 +20,7 @@ interface Product {
description: string description: string
price: number price: number
quantity: number quantity: number
type: 'PRODUCT' | 'CONSUMABLE'
category: { id: string; name: string } | null category: { id: string; name: string } | null
brand: string brand: string
color: string color: string
@ -45,6 +46,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
description: product?.description || '', description: product?.description || '',
price: product?.price || 0, price: product?.price || 0,
quantity: product?.quantity || 0, quantity: product?.quantity || 0,
type: product?.type || 'PRODUCT' as 'PRODUCT' | 'CONSUMABLE',
categoryId: product?.category?.id || 'none', categoryId: product?.category?.id || 'none',
brand: product?.brand || '', brand: product?.brand || '',
color: product?.color || '', color: product?.color || '',
@ -183,6 +185,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
description: formData.description || undefined, description: formData.description || undefined,
price: formData.price, price: formData.price,
quantity: formData.quantity, quantity: formData.quantity,
type: formData.type,
categoryId: formData.categoryId && formData.categoryId !== 'none' ? formData.categoryId : undefined, categoryId: formData.categoryId && formData.categoryId !== 'none' ? formData.categoryId : undefined,
brand: formData.brand || undefined, brand: formData.brand || undefined,
color: formData.color || undefined, color: formData.color || undefined,
@ -258,6 +261,28 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
/> />
</div> </div>
<div className="mt-4">
<Label className="text-white/80 text-sm mb-2 block">
Тип <span className="text-red-400">*</span>
</Label>
<Select
value={formData.type}
onValueChange={(value) => handleInputChange('type', value)}
>
<SelectTrigger className="glass-input text-white h-10">
<SelectValue placeholder="Выберите тип" />
</SelectTrigger>
<SelectContent className="glass-card">
<SelectItem value="PRODUCT" className="text-white">
Товар
</SelectItem>
<SelectItem value="CONSUMABLE" className="text-white">
Расходник
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4 mt-4"> <div className="grid grid-cols-2 gap-4 mt-4">
<div> <div>
<Label className="text-white/80 text-sm mb-2 block"> <Label className="text-white/80 text-sm mb-2 block">

View File

@ -20,6 +20,7 @@ interface Product {
description: string description: string
price: number price: number
quantity: number quantity: number
type: 'PRODUCT' | 'CONSUMABLE'
category: { id: string; name: string } | null category: { id: string; name: string } | null
brand: string brand: string
color: string color: string

View File

@ -804,6 +804,7 @@ export const CREATE_PRODUCT = gql`
description description
price price
quantity quantity
type
category { category {
id id
name name
@ -836,6 +837,7 @@ export const UPDATE_PRODUCT = gql`
description description
price price
quantity quantity
type
category { category {
id id
name name

View File

@ -111,6 +111,7 @@ export const GET_MY_PRODUCTS = gql`
description description
price price
quantity quantity
type
category { category {
id id
name name

View File

@ -202,6 +202,10 @@ export const resolvers = {
JSON: JSONScalar, JSON: JSONScalar,
DateTime: DateTimeScalar, DateTime: DateTimeScalar,
Product: {
type: (parent: any) => parent.type || "PRODUCT",
},
Query: { Query: {
me: async (_: unknown, __: unknown, context: Context) => { me: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
@ -1248,13 +1252,13 @@ export const resolvers = {
try { try {
const categories = await prisma.category.findMany({ const categories = await prisma.category.findMany({
orderBy: { orderBy: {
name: 'asc' name: "asc",
} },
}); });
return categories; return categories;
} catch (error) { } catch (error) {
console.error('Ошибка получения категорий:', error); console.error("Ошибка получения категорий:", error);
throw new GraphQLError('Не удалось получить категории'); throw new GraphQLError("Не удалось получить категории");
} }
}, },
}, },
@ -1652,8 +1656,8 @@ export const resolvers = {
console.log(`🔍 Validating ${marketplace} API key:`, { console.log(`🔍 Validating ${marketplace} API key:`, {
keyLength: apiKey.length, keyLength: apiKey.length,
keyPreview: apiKey.substring(0, 20) + '...', keyPreview: apiKey.substring(0, 20) + "...",
validateOnly validateOnly,
}); });
// Валидируем API ключ // Валидируем API ключ
@ -1666,7 +1670,10 @@ export const resolvers = {
console.log(`✅ Validation result for ${marketplace}:`, validationResult); console.log(`✅ Validation result for ${marketplace}:`, validationResult);
if (!validationResult.isValid) { if (!validationResult.isValid) {
console.log(`❌ Validation failed for ${marketplace}:`, validationResult.message); console.log(
`❌ Validation failed for ${marketplace}:`,
validationResult.message
);
return { return {
success: false, success: false,
message: validationResult.message, message: validationResult.message,
@ -3396,6 +3403,7 @@ export const resolvers = {
description?: string; description?: string;
price: number; price: number;
quantity: number; quantity: number;
type?: "PRODUCT" | "CONSUMABLE";
categoryId?: string; categoryId?: string;
brand?: string; brand?: string;
color?: string; color?: string;
@ -3453,6 +3461,7 @@ export const resolvers = {
description: args.input.description, description: args.input.description,
price: args.input.price, price: args.input.price,
quantity: args.input.quantity, quantity: args.input.quantity,
type: args.input.type || "PRODUCT",
categoryId: args.input.categoryId, categoryId: args.input.categoryId,
brand: args.input.brand, brand: args.input.brand,
color: args.input.color, color: args.input.color,
@ -3496,6 +3505,7 @@ export const resolvers = {
description?: string; description?: string;
price: number; price: number;
quantity: number; quantity: number;
type?: "PRODUCT" | "CONSUMABLE";
categoryId?: string; categoryId?: string;
brand?: string; brand?: string;
color?: string; color?: string;
@ -3564,6 +3574,7 @@ export const resolvers = {
description: args.input.description, description: args.input.description,
price: args.input.price, price: args.input.price,
quantity: args.input.quantity, quantity: args.input.quantity,
...(args.input.type && { type: args.input.type }),
categoryId: args.input.categoryId, categoryId: args.input.categoryId,
brand: args.input.brand, brand: args.input.brand,
color: args.input.color, color: args.input.color,
@ -5049,39 +5060,36 @@ export const resolvers = {
// Мутации для категорий // Мутации для категорий
const categoriesMutations = { const categoriesMutations = {
// Создать категорию // Создать категорию
createCategory: async ( createCategory: async (_: unknown, args: { input: { name: string } }) => {
_: unknown,
args: { input: { name: string } }
) => {
try { try {
// Проверяем есть ли уже категория с таким именем // Проверяем есть ли уже категория с таким именем
const existingCategory = await prisma.category.findUnique({ const existingCategory = await prisma.category.findUnique({
where: { name: args.input.name } where: { name: args.input.name },
}); });
if (existingCategory) { if (existingCategory) {
return { return {
success: false, success: false,
message: 'Категория с таким названием уже существует' message: "Категория с таким названием уже существует",
}; };
} }
const category = await prisma.category.create({ const category = await prisma.category.create({
data: { data: {
name: args.input.name name: args.input.name,
} },
}); });
return { return {
success: true, success: true,
message: 'Категория успешно создана', message: "Категория успешно создана",
category category,
}; };
} catch (error) { } catch (error) {
console.error('Ошибка создания категории:', error); console.error("Ошибка создания категории:", error);
return { return {
success: false, success: false,
message: 'Ошибка при создании категории' message: "Ошибка при создании категории",
}; };
} }
}, },
@ -5089,94 +5097,93 @@ const categoriesMutations = {
// Обновить категорию // Обновить категорию
updateCategory: async ( updateCategory: async (
_: unknown, _: unknown,
args: { id: string, input: { name: string } } args: { id: string; input: { name: string } }
) => { ) => {
try { try {
// Проверяем существует ли категория // Проверяем существует ли категория
const existingCategory = await prisma.category.findUnique({ const existingCategory = await prisma.category.findUnique({
where: { id: args.id } where: { id: args.id },
}); });
if (!existingCategory) { if (!existingCategory) {
return { return {
success: false, success: false,
message: 'Категория не найдена' message: "Категория не найдена",
}; };
} }
// Проверяем не занято ли имя другой категорией // Проверяем не занято ли имя другой категорией
const duplicateCategory = await prisma.category.findFirst({ const duplicateCategory = await prisma.category.findFirst({
where: { where: {
name: args.input.name, name: args.input.name,
id: { not: args.id } id: { not: args.id },
} },
}); });
if (duplicateCategory) { if (duplicateCategory) {
return { return {
success: false, success: false,
message: 'Категория с таким названием уже существует' message: "Категория с таким названием уже существует",
}; };
} }
const category = await prisma.category.update({ const category = await prisma.category.update({
where: { id: args.id }, where: { id: args.id },
data: { data: {
name: args.input.name name: args.input.name,
} },
}); });
return { return {
success: true, success: true,
message: 'Категория успешно обновлена', message: "Категория успешно обновлена",
category category,
}; };
} catch (error) { } catch (error) {
console.error('Ошибка обновления категории:', error); console.error("Ошибка обновления категории:", error);
return { return {
success: false, success: false,
message: 'Ошибка при обновлении категории' message: "Ошибка при обновлении категории",
}; };
} }
}, },
// Удалить категорию // Удалить категорию
deleteCategory: async ( deleteCategory: async (_: unknown, args: { id: string }) => {
_: unknown,
args: { id: string }
) => {
try { try {
// Проверяем существует ли категория // Проверяем существует ли категория
const existingCategory = await prisma.category.findUnique({ const existingCategory = await prisma.category.findUnique({
where: { id: args.id } where: { id: args.id },
}); });
if (!existingCategory) { if (!existingCategory) {
throw new GraphQLError('Категория не найдена'); throw new GraphQLError("Категория не найдена");
} }
// Проверяем есть ли товары в этой категории // Проверяем есть ли товары в этой категории
const productsCount = await prisma.product.count({ const productsCount = await prisma.product.count({
where: { categoryId: args.id } where: { categoryId: args.id },
}); });
if (productsCount > 0) { if (productsCount > 0) {
throw new GraphQLError('Нельзя удалить категорию, в которой есть товары'); throw new GraphQLError(
"Нельзя удалить категорию, в которой есть товары"
);
} }
await prisma.category.delete({ await prisma.category.delete({
where: { id: args.id } where: { id: args.id },
}); });
return true; return true;
} catch (error) { } catch (error) {
console.error('Ошибка удаления категории:', error); console.error("Ошибка удаления категории:", error);
if (error instanceof GraphQLError) { if (error instanceof GraphQLError) {
throw error; throw error;
} }
throw new GraphQLError('Ошибка при удалении категории'); throw new GraphQLError("Ошибка при удалении категории");
} }
} },
}; };
// Логистические мутации // Логистические мутации

View File

@ -181,7 +181,10 @@ export const typeDefs = gql`
# Заказы поставок расходников # Заказы поставок расходников
createSupplyOrder(input: SupplyOrderInput!): SupplyOrderResponse! createSupplyOrder(input: SupplyOrderInput!): SupplyOrderResponse!
updateSupplyOrderStatus(id: ID!, status: SupplyOrderStatus!): SupplyOrderResponse! updateSupplyOrderStatus(
id: ID!
status: SupplyOrderStatus!
): SupplyOrderResponse!
# Работа с логистикой # Работа с логистикой
createLogistics(input: LogisticsInput!): LogisticsResponse! createLogistics(input: LogisticsInput!): LogisticsResponse!
@ -392,6 +395,11 @@ export const typeDefs = gql`
OZON OZON
} }
enum ProductType {
PRODUCT
CONSUMABLE
}
enum CounterpartyRequestStatus { enum CounterpartyRequestStatus {
PENDING PENDING
ACCEPTED ACCEPTED
@ -629,6 +637,7 @@ export const typeDefs = gql`
description: String description: String
price: Float! price: Float!
quantity: Int! quantity: Int!
type: ProductType
category: Category category: Category
brand: String brand: String
color: String color: String
@ -650,6 +659,7 @@ export const typeDefs = gql`
description: String description: String
price: Float! price: Float!
quantity: Int! quantity: Int!
type: ProductType
categoryId: ID categoryId: ID
brand: String brand: String
color: String color: String