Добавлено новое поле типа товара в модель Product и соответствующие изменения в компонентах, формах и GraphQL запросах. Реализована логика выбора типа товара в интерфейсе, обновлены резолверы и типы для поддержки нового поля. Улучшена обработка данных и интерфейс для отображения типа товара.
This commit is contained in:
@ -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
|
||||||
|
@ -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 && (
|
||||||
|
@ -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">
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -111,6 +111,7 @@ export const GET_MY_PRODUCTS = gql`
|
|||||||
description
|
description
|
||||||
price
|
price
|
||||||
quantity
|
quantity
|
||||||
|
type
|
||||||
category {
|
category {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
@ -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("Ошибка при удалении категории");
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Логистические мутации
|
// Логистические мутации
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user