-
{card.title}
+
handleCardClick(card)}>{card.title}
{card.vendorCode}
{formatCurrency(price)}
@@ -718,13 +1025,183 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
-
Поиск товаров
-
- Введите запрос в поле поиска, чтобы найти товары в вашем каталоге Wildberries
-
+
Карточки товаров Wildberries
+ {user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')?.isActive ? (
+ <>
+
+ Введите запрос в поле поиска, чтобы найти товары в вашем каталоге Wildberries, или загрузите все доступные карточки
+
+
+ >
+ ) : (
+ <>
+
+ Для работы с реальными карточками товаров необходимо настроить API ключ Wildberries в настройках организации
+
+
+ Сейчас показаны демонстрационные товары. Для тестирования используйте поиск или загрузите все.
+
+
+ >
+ )}
)}
+
+ {/* Модальное окно с детальной информацией о товаре */}
+
)
}
\ No newline at end of file
diff --git a/src/graphql/mutations.ts b/src/graphql/mutations.ts
index 6aefb40..412d5d5 100644
--- a/src/graphql/mutations.ts
+++ b/src/graphql/mutations.ts
@@ -177,6 +177,7 @@ export const ADD_MARKETPLACE_API_KEY = gql`
apiKey {
id
marketplace
+ apiKey
isActive
validationData
}
diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts
index 57d67a1..c6969b0 100644
--- a/src/graphql/queries.ts
+++ b/src/graphql/queries.ts
@@ -40,8 +40,11 @@ export const GET_ME = gql`
apiKeys {
id
marketplace
+ apiKey
isActive
validationData
+ createdAt
+ updatedAt
}
}
}
@@ -253,6 +256,7 @@ export const GET_ORGANIZATION = gql`
apiKeys {
id
marketplace
+ apiKey
isActive
validationData
createdAt
diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts
index 663f4ed..500bcd3 100644
--- a/src/graphql/resolvers.ts
+++ b/src/graphql/resolvers.ts
@@ -96,7 +96,7 @@ const generateToken = (payload: AuthTokenPayload): string => {
const verifyToken = (token: string): AuthTokenPayload => {
try {
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload;
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
throw new GraphQLError("Недействительный токен", {
extensions: { code: "UNAUTHENTICATED" },
@@ -168,7 +168,7 @@ function parseLiteral(ast: unknown): unknown {
fields?: unknown[];
values?: unknown[];
};
-
+
switch (astNode.kind) {
case Kind.STRING:
case Kind.BOOLEAN:
@@ -290,7 +290,7 @@ export const resolvers = {
// Получаем исходящие заявки для добавления флага hasOutgoingRequest
const outgoingRequests = await prisma.counterpartyRequest.findMany({
- where: {
+ where: {
senderId: currentUser.organization.id,
status: "PENDING",
},
@@ -301,7 +301,7 @@ export const resolvers = {
// Получаем входящие заявки для добавления флага hasIncomingRequest
const incomingRequests = await prisma.counterpartyRequest.findMany({
- where: {
+ where: {
receiverId: currentUser.organization.id,
status: "PENDING",
},
@@ -365,7 +365,7 @@ export const resolvers = {
const counterparties = await prisma.counterparty.findMany({
where: { organizationId: currentUser.organization.id },
- include: {
+ include: {
counterparty: {
include: {
users: true,
@@ -396,7 +396,7 @@ export const resolvers = {
}
return await prisma.counterpartyRequest.findMany({
- where: {
+ where: {
receiverId: currentUser.organization.id,
status: "PENDING",
},
@@ -436,7 +436,7 @@ export const resolvers = {
}
return await prisma.counterpartyRequest.findMany({
- where: {
+ where: {
senderId: currentUser.organization.id,
status: { in: ["PENDING", "REJECTED"] },
},
@@ -505,7 +505,7 @@ export const resolvers = {
receiverOrganization: {
include: {
users: true,
- },
+ },
},
},
orderBy: { createdAt: "asc" },
@@ -670,7 +670,7 @@ export const resolvers = {
return await prisma.product.findMany({
where: { organizationId: currentUser.organization.id },
- include: {
+ include: {
category: true,
organization: true,
},
@@ -712,7 +712,7 @@ export const resolvers = {
return await prisma.product.findMany({
where,
- include: {
+ include: {
category: true,
organization: {
include: {
@@ -897,7 +897,7 @@ export const resolvers = {
}
const employee = await prisma.employee.findFirst({
- where: {
+ where: {
id: args.id,
organizationId: currentUser.organization.id,
},
@@ -984,7 +984,7 @@ export const resolvers = {
args.phone,
args.code
);
-
+
if (!verificationResult.success) {
return {
success: false,
@@ -1043,7 +1043,7 @@ export const resolvers = {
};
console.log("verifySmsCode - Returning result:", {
- success: result.success,
+ success: result.success,
hasToken: !!result.token,
hasUser: !!result.user,
message: result.message,
@@ -1147,28 +1147,28 @@ export const resolvers = {
addressFull: organizationData.addressFull,
ogrn: organizationData.ogrn,
ogrnDate: organizationData.ogrnDate,
-
+
// Статус организации
status: organizationData.status,
actualityDate: organizationData.actualityDate,
registrationDate: organizationData.registrationDate,
liquidationDate: organizationData.liquidationDate,
-
+
// Руководитель
managementName: organizationData.managementName,
managementPost: organizationData.managementPost,
-
+
// ОПФ
opfCode: organizationData.opfCode,
opfFull: organizationData.opfFull,
opfShort: organizationData.opfShort,
-
+
// Коды статистики
okato: organizationData.okato,
oktmo: organizationData.oktmo,
okpo: organizationData.okpo,
okved: organizationData.okved,
-
+
// Контакты
phones: organizationData.phones
? JSON.parse(JSON.stringify(organizationData.phones))
@@ -1176,12 +1176,12 @@ export const resolvers = {
emails: organizationData.emails
? JSON.parse(JSON.stringify(organizationData.emails))
: null,
-
+
// Финансовые данные
employeeCount: organizationData.employeeCount,
revenue: organizationData.revenue,
taxSystem: organizationData.taxSystem,
-
+
type: type,
dadataData: JSON.parse(JSON.stringify(organizationData.rawData)),
},
@@ -1284,7 +1284,7 @@ export const resolvers = {
const tradeMark = validationResults[0]?.data?.tradeMark;
const sellerName = validationResults[0]?.data?.sellerName;
const shopName = tradeMark || sellerName || "Магазин";
-
+
const organization = await prisma.organization.create({
data: {
inn:
@@ -1427,7 +1427,7 @@ export const resolvers = {
where: { id: existingKey.id },
data: {
apiKey,
- validationData: JSON.parse(JSON.stringify(validationResult.data)),
+ validationData: JSON.parse(JSON.stringify(validationResult.data)),
isActive: true,
},
});
@@ -1453,7 +1453,7 @@ export const resolvers = {
message: "API ключ успешно добавлен",
apiKey: newKey,
};
- }
+ }
} catch (error) {
console.error("Error adding marketplace API key:", error);
return {
@@ -1526,7 +1526,7 @@ export const resolvers = {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
- include: {
+ include: {
organization: {
include: {
apiKeys: true,
@@ -1541,7 +1541,7 @@ export const resolvers = {
try {
const { input } = args;
-
+
// Обновляем данные пользователя (аватар, имя управляющего)
const userUpdateData: { avatar?: string; managerName?: string } = {};
if (input.avatar) {
@@ -1550,14 +1550,14 @@ export const resolvers = {
if (input.managerName) {
userUpdateData.managerName = input.managerName;
}
-
+
if (Object.keys(userUpdateData).length > 0) {
await prisma.user.update({
where: { id: context.user.id },
data: userUpdateData,
});
}
-
+
// Подготавливаем данные для обновления организации
const updateData: {
phones?: object;
@@ -1565,20 +1565,20 @@ export const resolvers = {
managementName?: string;
managementPost?: string;
} = {};
-
+
// Название организации больше не обновляется через профиль
// Для селлеров устанавливается при регистрации, для остальных - при смене ИНН
-
+
// Обновляем контактные данные в JSON поле phones
if (input.orgPhone) {
updateData.phones = [{ value: input.orgPhone, type: "main" }];
}
-
- // Обновляем email в JSON поле emails
+
+ // Обновляем email в JSON поле emails
if (input.email) {
updateData.emails = [{ value: input.email, type: "main" }];
}
-
+
// Сохраняем дополнительные контакты в custom полях
// Пока добавим их как дополнительные JSON поля
const customContacts: {
@@ -1592,13 +1592,13 @@ export const resolvers = {
corrAccount?: string;
};
} = {};
-
+
// managerName теперь сохраняется в поле пользователя, а не в JSON
-
+
if (input.telegram) {
customContacts.telegram = input.telegram;
}
-
+
if (input.whatsapp) {
customContacts.whatsapp = input.whatsapp;
}
@@ -1616,7 +1616,7 @@ export const resolvers = {
corrAccount: input.corrAccount,
};
}
-
+
// Если есть дополнительные контакты, сохраним их в поле managementPost временно
// В идеале нужно добавить отдельную таблицу для контактов
if (Object.keys(customContacts).length > 0) {
@@ -1635,7 +1635,7 @@ export const resolvers = {
// Получаем обновленного пользователя
const updatedUser = await prisma.user.findUnique({
where: { id: context.user.id },
- include: {
+ include: {
organization: {
include: {
apiKeys: true,
@@ -1671,7 +1671,7 @@ export const resolvers = {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
- include: {
+ include: {
organization: {
include: {
apiKeys: true,
@@ -1769,7 +1769,7 @@ export const resolvers = {
// Получаем обновленного пользователя
const updatedUser = await prisma.user.findUnique({
where: { id: context.user.id },
- include: {
+ include: {
organization: {
include: {
apiKeys: true,
@@ -2930,22 +2930,22 @@ export const resolvers = {
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;
+ 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
@@ -3005,7 +3005,7 @@ export const resolvers = {
isActive: args.input.isActive ?? true,
organizationId: currentUser.organization.id,
},
- include: {
+ include: {
category: true,
organization: true,
},
@@ -3029,23 +3029,23 @@ export const resolvers = {
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;
+ 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
@@ -3115,7 +3115,7 @@ export const resolvers = {
mainImage: args.input.mainImage,
isActive: args.input.isActive ?? true,
},
- include: {
+ include: {
category: true,
organization: true,
},
@@ -3400,7 +3400,7 @@ export const resolvers = {
if (existingCartItem) {
// Обновляем количество
const newQuantity = existingCartItem.quantity + args.quantity;
-
+
if (newQuantity > product.quantity) {
return {
success: false,
@@ -3940,7 +3940,7 @@ export const resolvers = {
try {
const employee = await prisma.employee.update({
- where: {
+ where: {
id: args.id,
organizationId: currentUser.organization.id,
},
@@ -4002,7 +4002,7 @@ export const resolvers = {
try {
await prisma.employee.delete({
- where: {
+ where: {
id: args.id,
organizationId: currentUser.organization.id,
},
@@ -4150,7 +4150,7 @@ export const resolvers = {
if (parent.users) {
return parent.users;
}
-
+
// Иначе загружаем отдельно
return await prisma.user.findMany({
where: { organizationId: parent.id },
@@ -4197,7 +4197,7 @@ export const resolvers = {
if (parent.organization) {
return parent.organization;
}
-
+
// Иначе загружаем отдельно если есть organizationId
if (parent.organizationId) {
return await prisma.organization.findUnique({
@@ -4514,14 +4514,14 @@ const adminQueries = {
const limit = args.limit || 50;
const offset = args.offset || 0;
-
+
// Строим условие поиска
const whereCondition: Prisma.UserWhereInput = args.search
? {
OR: [
{ phone: { contains: args.search, mode: "insensitive" } },
{ managerName: { contains: args.search, mode: "insensitive" } },
- {
+ {
organization: {
OR: [
{ name: { contains: args.search, mode: "insensitive" } },
@@ -4589,7 +4589,7 @@ const adminMutations = {
args.password,
admin.password
);
-
+
if (!isPasswordValid) {
return {
success: false,
@@ -4605,7 +4605,7 @@ const adminMutations = {
// Создать токен
const token = jwt.sign(
- {
+ {
adminId: admin.id,
username: admin.username,
type: "admin",
diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts
index 854b3eb..a464a08 100644
--- a/src/graphql/typedefs.ts
+++ b/src/graphql/typedefs.ts
@@ -246,6 +246,7 @@ export const typeDefs = gql`
type ApiKey {
id: ID!
marketplace: MarketplaceType!
+ apiKey: String!
isActive: Boolean!
validationData: JSON
createdAt: DateTime!
diff --git a/src/services/wildberries-service.ts b/src/services/wildberries-service.ts
index 5c05712..e1d2696 100644
--- a/src/services/wildberries-service.ts
+++ b/src/services/wildberries-service.ts
@@ -43,7 +43,7 @@ interface WildberriesCardsResponse {
}
interface WildberriesCardFilter {
- sort?: {
+ settings?: {
cursor?: {
limit?: number
nmID?: number
@@ -55,61 +55,55 @@ interface WildberriesCardFilter {
objectIDs?: number[]
tagIDs?: number[]
brandIDs?: number[]
+ colorIDs?: number[]
+ sizeIDs?: number[]
}
}
}
export class WildberriesService {
- private static baseUrl = 'https://marketplace-api.wildberries.ru'
private static contentUrl = 'https://content-api.wildberries.ru'
+ private static publicUrl = 'https://public-api.wildberries.ru'
+ private static supplierUrl = 'https://suppliers-api.wildberries.ru'
/**
- * Получить список складов WB
- */
- static async getWarehouses(apiKey: string): Promise
{
- try {
- const response = await fetch(`${this.baseUrl}/api/v2/warehouses`, {
- method: 'GET',
- headers: {
- 'Authorization': apiKey,
- 'Content-Type': 'application/json',
- },
- })
-
- if (!response.ok) {
- throw new Error(`WB API Error: ${response.status} ${response.statusText}`)
- }
-
- const data: WildberriesWarehousesResponse = await response.json()
- return data.data || []
- } catch (error) {
- console.error('Error fetching WB warehouses:', error)
- throw new Error('Ошибка получения складов Wildberries')
- }
- }
-
- /**
- * Получить карточки товаров
+ * Получение карточек товаров через Content API v2
*/
static async getCards(apiKey: string, filter?: WildberriesCardFilter): Promise {
try {
- const response = await fetch(`${this.contentUrl}/content/v1/cards/cursor/list`, {
+ console.log('Calling WB Content API v2 with filter:', filter)
+
+ const response = await fetch(`${this.contentUrl}/content/v2/get/cards/list`, {
method: 'POST',
headers: {
'Authorization': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify(filter || {
- sort: {
+ settings: {
cursor: {
limit: 100
+ },
+ filter: {
+ withPhoto: -1
}
}
})
})
+ console.log(`${this.contentUrl}/content/v2/get/cards/list`, response.status, response.statusText)
+
if (!response.ok) {
- throw new Error(`WB API Error: ${response.status} ${response.statusText}`)
+ const errorText = await response.text()
+ let errorData
+ try {
+ errorData = JSON.parse(errorText)
+ } catch {
+ errorData = { message: errorText }
+ }
+
+ console.log('WB API Error Response:', errorData)
+ throw new Error(`WB API Error: ${response.status} - ${response.statusText}`)
}
const data: WildberriesCardsResponse = await response.json()
@@ -121,16 +115,17 @@ export class WildberriesService {
}
/**
- * Поиск карточек товаров по тексту
+ * Поиск карточек товаров
*/
- static async searchCards(apiKey: string, searchText: string, limit: number = 100): Promise {
+ static async searchCards(apiKey: string, searchTerm: string, limit = 50): Promise {
const filter: WildberriesCardFilter = {
- sort: {
+ settings: {
cursor: {
limit
},
filter: {
- textSearch: searchText
+ textSearch: searchTerm,
+ withPhoto: -1
}
}
}
@@ -139,63 +134,70 @@ export class WildberriesService {
}
/**
- * Валидация API ключа WB
+ * Получение всех карточек товаров с пагинацией
+ */
+ static async getAllCards(apiKey: string, limit = 100): Promise {
+ const filter: WildberriesCardFilter = {
+ settings: {
+ cursor: {
+ limit
+ },
+ filter: {
+ withPhoto: -1
+ }
+ }
+ }
+
+ return this.getCards(apiKey, filter)
+ }
+
+ /**
+ * Получение складов WB
+ */
+ static async getWarehouses(apiKey: string): Promise {
+ try {
+ const response = await fetch(`${this.supplierUrl}/api/v3/warehouses`, {
+ headers: {
+ 'Authorization': apiKey,
+ }
+ })
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+
+ const data: WildberriesWarehousesResponse = await response.json()
+ return data.data || []
+ } catch (error) {
+ console.error('Error fetching warehouses:', error)
+ return []
+ }
+ }
+
+ /**
+ * Проверка валидности API ключа
*/
static async validateApiKey(apiKey: string): Promise {
try {
- await this.getWarehouses(apiKey)
- return true
+ const response = await fetch(`${this.contentUrl}/content/v2/get/cards/list`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': apiKey,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ settings: {
+ cursor: {
+ limit: 1
+ }
+ }
+ })
+ })
+
+ return response.ok
} catch (error) {
- console.error('WB API key validation failed:', error)
+ console.error('Error validating API key:', error)
return false
}
}
-
- /**
- * Получить информацию о поставке
- */
- static async getSupplyInfo(apiKey: string, supplyId: string): Promise {
- try {
- const response = await fetch(`${this.baseUrl}/api/v3/supplies/${supplyId}`, {
- method: 'GET',
- headers: {
- 'Authorization': apiKey,
- 'Content-Type': 'application/json',
- },
- })
-
- if (!response.ok) {
- throw new Error(`WB API Error: ${response.status} ${response.statusText}`)
- }
-
- return await response.json()
- } catch (error) {
- console.error('Error fetching WB supply info:', error)
- throw new Error('Ошибка получения информации о поставке')
- }
- }
-
- /**
- * Получить список поставок
- */
- static async getSupplies(apiKey: string, limit: number = 1000, next: number = 0): Promise {
- try {
- const response = await fetch(`${this.baseUrl}/api/v3/supplies?limit=${limit}&next=${next}`, {
- method: 'GET',
- headers: {
- 'Authorization': apiKey,
- 'Content-Type': 'application/json',
- },
- })
-
- if (!response.ok) {
- throw new Error(`WB API Error: ${response.status} ${response.statusText}`)
- }
-
- return await response.json()
- } catch (error) {
- console.error('Error fetching WB supplies:', error)
- throw new Error('Ошибка получения списка поставок')
- }
- }
}
\ No newline at end of file