Добавлено новое поле типа товара в модель 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?
price Decimal @db.Decimal(12, 2)
quantity Int @default(0)
type ProductType @default(PRODUCT)
categoryId String?
brand String?
color String?
@ -438,6 +439,11 @@ enum WildberriesSupplyStatus {
CANCELLED
}
enum ProductType {
PRODUCT
CONSUMABLE
}
model Logistics {
id String @id @default(cuid())
fromLocation String

View File

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

View File

@ -20,6 +20,7 @@ interface Product {
description: string
price: number
quantity: number
type: 'PRODUCT' | 'CONSUMABLE'
category: { id: string; name: string } | null
brand: string
color: string
@ -45,6 +46,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
description: product?.description || '',
price: product?.price || 0,
quantity: product?.quantity || 0,
type: product?.type || 'PRODUCT' as 'PRODUCT' | 'CONSUMABLE',
categoryId: product?.category?.id || 'none',
brand: product?.brand || '',
color: product?.color || '',
@ -183,6 +185,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
description: formData.description || undefined,
price: formData.price,
quantity: formData.quantity,
type: formData.type,
categoryId: formData.categoryId && formData.categoryId !== 'none' ? formData.categoryId : undefined,
brand: formData.brand || undefined,
color: formData.color || undefined,
@ -258,6 +261,28 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
/>
</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>
<Label className="text-white/80 text-sm mb-2 block">

View File

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

View File

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

View File

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

View File

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

View File

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