- {loading ? (
-
- ) : !hasWBApiKey ? (
-
-
- Настройте API Wildberries
- Для просмотра остатков добавьте API ключ Wildberries в настройках
-
-
- ) : filteredStocks.length === 0 ? (
-
-
- Товары не найдены
- Попробуйте изменить параметры поиска
-
- ) : (
-
-
-
- {/* Таблица товаров */}
-
- {filteredStocks.map((item, index) => (
-
- ))}
+ {/* Основной контент */}
+
+
+
+
+
+
+ Склад Wildberries
+
+
+ Управление товарами на складах Wildberries
+
+
+
+
- )}
-
+
+ {/* Поиск */}
+
+
+
+
+
+ {/* Контент с таблицей */}
+
+ {!initialized || loading || cacheLoading ? (
+
+
+
+ ) : filteredStocks.length === 0 ? (
+
+
+
+
+ {searchTerm ? 'Товары не найдены' : 'Нет данных о товарах'}
+
+ {!searchTerm && (
+
+ )}
+
+
+ ) : (
+
+
+ {/* Заголовок таблицы */}
+
+
+ {/* Строки товаров */}
+ {filteredStocks.map((item) => (
+
+ ))}
+
+
+ )}
+
+
)
}
\ No newline at end of file
diff --git a/src/graphql/mutations.ts b/src/graphql/mutations.ts
index 76ddfc2..7bbefd0 100644
--- a/src/graphql/mutations.ts
+++ b/src/graphql/mutations.ts
@@ -1033,6 +1033,70 @@ export const REMOVE_FROM_FAVORITES = gql`
}
`;
+// Мутации для внешней рекламы
+export const CREATE_EXTERNAL_AD = gql`
+ mutation CreateExternalAd($input: ExternalAdInput!) {
+ createExternalAd(input: $input) {
+ success
+ message
+ externalAd {
+ id
+ name
+ url
+ cost
+ date
+ nmId
+ clicks
+ organizationId
+ createdAt
+ updatedAt
+ }
+ }
+ }
+`;
+
+export const UPDATE_EXTERNAL_AD = gql`
+ mutation UpdateExternalAd($id: ID!, $input: ExternalAdInput!) {
+ updateExternalAd(id: $id, input: $input) {
+ success
+ message
+ externalAd {
+ id
+ name
+ url
+ cost
+ date
+ nmId
+ clicks
+ organizationId
+ createdAt
+ updatedAt
+ }
+ }
+ }
+`;
+
+export const DELETE_EXTERNAL_AD = gql`
+ mutation DeleteExternalAd($id: ID!) {
+ deleteExternalAd(id: $id) {
+ success
+ message
+ externalAd {
+ id
+ }
+ }
+ }
+`;
+
+export const UPDATE_EXTERNAL_AD_CLICKS = gql`
+ mutation UpdateExternalAdClicks($id: ID!, $clicks: Int!) {
+ updateExternalAdClicks(id: $id, clicks: $clicks) {
+ success
+ message
+ }
+ }
+`;
+
// Мутации для категорий
export const CREATE_CATEGORY = gql`
mutation CreateCategory($input: CategoryInput!) {
@@ -1248,3 +1312,25 @@ export const UPDATE_SUPPLY_ORDER_STATUS = gql`
}
}
`;
+
+// Мутации для кеша склада WB
+export const SAVE_WB_WAREHOUSE_CACHE = gql`
+ mutation SaveWBWarehouseCache($input: WBWarehouseCacheInput!) {
+ saveWBWarehouseCache(input: $input) {
+ success
+ message
+ fromCache
+ cache {
+ id
+ organizationId
+ cacheDate
+ data
+ totalProducts
+ totalStocks
+ totalReserved
+ createdAt
+ updatedAt
+ }
+ }
+ }
+`;
diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts
index 40db734..418acdd 100644
--- a/src/graphql/queries.ts
+++ b/src/graphql/queries.ts
@@ -829,6 +829,27 @@ export const GET_WILDBERRIES_CAMPAIGNS_LIST = gql`
}
`
+export const GET_EXTERNAL_ADS = gql`
+ query GetExternalAds($dateFrom: String!, $dateTo: String!) {
+ getExternalAds(dateFrom: $dateFrom, dateTo: $dateTo) {
+ success
+ message
+ externalAds {
+ id
+ name
+ url
+ cost
+ date
+ nmId
+ clicks
+ organizationId
+ createdAt
+ updatedAt
+ }
+ }
+ }
+`
+
// Админ запросы
export const ADMIN_ME = gql`
query AdminMe {
@@ -932,3 +953,25 @@ export const GET_PENDING_SUPPLIES_COUNT = gql`
}
}
`;
+
+// Запросы для кеша склада WB
+export const GET_WB_WAREHOUSE_DATA = gql`
+ query GetWBWarehouseData {
+ getWBWarehouseData {
+ success
+ message
+ fromCache
+ cache {
+ id
+ organizationId
+ cacheDate
+ data
+ totalProducts
+ totalStocks
+ totalReserved
+ createdAt
+ updatedAt
+ }
+ }
+ }
+`;
diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts
index 11e1f9b..37fbc6f 100644
--- a/src/graphql/resolvers.ts
+++ b/src/graphql/resolvers.ts
@@ -5017,6 +5017,59 @@ export const resolvers = {
};
}
},
+
+ updateExternalAdClicks: async (
+ _: unknown,
+ { id, clicks }: { id: string; clicks: number },
+ context: Context
+ ) => {
+ if (!context.user) {
+ throw new GraphQLError("Требуется авторизация", {
+ extensions: { code: "UNAUTHENTICATED" },
+ });
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ });
+
+ if (!user?.organization) {
+ throw new GraphQLError("Организация не найдена");
+ }
+
+ // Проверяем, что реклама принадлежит организации пользователя
+ const existingAd = await prisma.externalAd.findFirst({
+ where: {
+ id,
+ organizationId: user.organization.id,
+ },
+ });
+
+ if (!existingAd) {
+ throw new GraphQLError("Внешняя реклама не найдена");
+ }
+
+ await prisma.externalAd.update({
+ where: { id },
+ data: { clicks },
+ });
+
+ return {
+ success: true,
+ message: "Клики успешно обновлены",
+ externalAd: null,
+ };
+ } catch (error) {
+ console.error("Error updating external ad clicks:", error);
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : "Ошибка обновления кликов",
+ externalAd: null,
+ };
+ }
+ },
},
// Резолверы типов
@@ -6019,14 +6072,394 @@ const wildberriesQueries = {
},
};
+// Резолверы для внешней рекламы
+const externalAdQueries = {
+ getExternalAds: async (
+ _: unknown,
+ { dateFrom, dateTo }: { dateFrom: string; dateTo: string },
+ context: Context
+ ) => {
+ if (!context.user) {
+ throw new GraphQLError("Требуется авторизация", {
+ extensions: { code: "UNAUTHENTICATED" },
+ });
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ });
+
+ if (!user?.organization) {
+ throw new GraphQLError("Организация не найдена");
+ }
+
+ const externalAds = await prisma.externalAd.findMany({
+ where: {
+ organizationId: user.organization.id,
+ date: {
+ gte: new Date(dateFrom),
+ lte: new Date(dateTo + 'T23:59:59.999Z'),
+ },
+ },
+ orderBy: {
+ date: 'desc',
+ },
+ });
+
+ return {
+ success: true,
+ message: null,
+ externalAds: externalAds.map(ad => ({
+ ...ad,
+ cost: parseFloat(ad.cost.toString()),
+ date: ad.date.toISOString().split('T')[0],
+ createdAt: ad.createdAt.toISOString(),
+ updatedAt: ad.updatedAt.toISOString(),
+ })),
+ };
+ } catch (error) {
+ console.error("Error fetching external ads:", error);
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : "Ошибка получения внешней рекламы",
+ externalAds: [],
+ };
+ }
+ },
+};
+
+const externalAdMutations = {
+ createExternalAd: async (
+ _: unknown,
+ { input }: { input: { name: string; url: string; cost: number; date: string; nmId: string } },
+ context: Context
+ ) => {
+ if (!context.user) {
+ throw new GraphQLError("Требуется авторизация", {
+ extensions: { code: "UNAUTHENTICATED" },
+ });
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ });
+
+ if (!user?.organization) {
+ throw new GraphQLError("Организация не найдена");
+ }
+
+ const externalAd = await prisma.externalAd.create({
+ data: {
+ name: input.name,
+ url: input.url,
+ cost: input.cost,
+ date: new Date(input.date),
+ nmId: input.nmId,
+ organizationId: user.organization.id,
+ },
+ });
+
+ return {
+ success: true,
+ message: "Внешняя реклама успешно создана",
+ externalAd: {
+ ...externalAd,
+ cost: parseFloat(externalAd.cost.toString()),
+ date: externalAd.date.toISOString().split('T')[0],
+ createdAt: externalAd.createdAt.toISOString(),
+ updatedAt: externalAd.updatedAt.toISOString(),
+ },
+ };
+ } catch (error) {
+ console.error("Error creating external ad:", error);
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : "Ошибка создания внешней рекламы",
+ externalAd: null,
+ };
+ }
+ },
+
+ updateExternalAd: async (
+ _: unknown,
+ { id, input }: { id: string; input: { name: string; url: string; cost: number; date: string; nmId: string } },
+ context: Context
+ ) => {
+ if (!context.user) {
+ throw new GraphQLError("Требуется авторизация", {
+ extensions: { code: "UNAUTHENTICATED" },
+ });
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ });
+
+ if (!user?.organization) {
+ throw new GraphQLError("Организация не найдена");
+ }
+
+ // Проверяем, что реклама принадлежит организации пользователя
+ const existingAd = await prisma.externalAd.findFirst({
+ where: {
+ id,
+ organizationId: user.organization.id,
+ },
+ });
+
+ if (!existingAd) {
+ throw new GraphQLError("Внешняя реклама не найдена");
+ }
+
+ const externalAd = await prisma.externalAd.update({
+ where: { id },
+ data: {
+ name: input.name,
+ url: input.url,
+ cost: input.cost,
+ date: new Date(input.date),
+ nmId: input.nmId,
+ },
+ });
+
+ return {
+ success: true,
+ message: "Внешняя реклама успешно обновлена",
+ externalAd: {
+ ...externalAd,
+ cost: parseFloat(externalAd.cost.toString()),
+ date: externalAd.date.toISOString().split('T')[0],
+ createdAt: externalAd.createdAt.toISOString(),
+ updatedAt: externalAd.updatedAt.toISOString(),
+ },
+ };
+ } catch (error) {
+ console.error("Error updating external ad:", error);
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : "Ошибка обновления внешней рекламы",
+ externalAd: null,
+ };
+ }
+ },
+
+ deleteExternalAd: async (
+ _: unknown,
+ { id }: { id: string },
+ context: Context
+ ) => {
+ if (!context.user) {
+ throw new GraphQLError("Требуется авторизация", {
+ extensions: { code: "UNAUTHENTICATED" },
+ });
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ });
+
+ if (!user?.organization) {
+ throw new GraphQLError("Организация не найдена");
+ }
+
+ // Проверяем, что реклама принадлежит организации пользователя
+ const existingAd = await prisma.externalAd.findFirst({
+ where: {
+ id,
+ organizationId: user.organization.id,
+ },
+ });
+
+ if (!existingAd) {
+ throw new GraphQLError("Внешняя реклама не найдена");
+ }
+
+ await prisma.externalAd.delete({
+ where: { id },
+ });
+
+ return {
+ success: true,
+ message: "Внешняя реклама успешно удалена",
+ externalAd: null,
+ };
+ } catch (error) {
+ console.error("Error deleting external ad:", error);
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : "Ошибка удаления внешней рекламы",
+ externalAd: null,
+ };
+ }
+ },
+
+};
+
+// Резолверы для кеша склада WB
+const wbWarehouseCacheQueries = {
+ getWBWarehouseData: async (
+ _: unknown,
+ __: unknown,
+ context: Context
+ ) => {
+ if (!context.user) {
+ throw new GraphQLError("Требуется авторизация", {
+ extensions: { code: "UNAUTHENTICATED" },
+ });
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ });
+
+ if (!user?.organization) {
+ throw new GraphQLError("Организация не найдена");
+ }
+
+ // Получаем текущую дату без времени
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ // Ищем кеш за сегодня
+ const cache = await prisma.wBWarehouseCache.findFirst({
+ where: {
+ organizationId: user.organization.id,
+ cacheDate: today,
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ });
+
+ if (cache) {
+ // Возвращаем данные из кеша
+ return {
+ success: true,
+ message: "Данные получены из кеша",
+ cache: {
+ ...cache,
+ cacheDate: cache.cacheDate.toISOString().split('T')[0],
+ createdAt: cache.createdAt.toISOString(),
+ updatedAt: cache.updatedAt.toISOString(),
+ },
+ fromCache: true,
+ };
+ } else {
+ // Кеша нет, нужно загрузить данные из API
+ return {
+ success: true,
+ message: "Кеш не найден, требуется загрузка из API",
+ cache: null,
+ fromCache: false,
+ };
+ }
+ } catch (error) {
+ console.error("Error getting WB warehouse cache:", error);
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : "Ошибка получения кеша склада WB",
+ cache: null,
+ fromCache: false,
+ };
+ }
+ },
+};
+
+const wbWarehouseCacheMutations = {
+ saveWBWarehouseCache: async (
+ _: unknown,
+ { input }: { input: { data: string; totalProducts: number; totalStocks: number; totalReserved: number } },
+ context: Context
+ ) => {
+ if (!context.user) {
+ throw new GraphQLError("Требуется авторизация", {
+ extensions: { code: "UNAUTHENTICATED" },
+ });
+ }
+
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id: context.user.id },
+ include: { organization: true },
+ });
+
+ if (!user?.organization) {
+ throw new GraphQLError("Организация не найдена");
+ }
+
+ // Получаем текущую дату без времени
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ // Используем upsert для создания или обновления кеша
+ const cache = await prisma.wBWarehouseCache.upsert({
+ where: {
+ organizationId_cacheDate: {
+ organizationId: user.organization.id,
+ cacheDate: today,
+ },
+ },
+ update: {
+ data: input.data,
+ totalProducts: input.totalProducts,
+ totalStocks: input.totalStocks,
+ totalReserved: input.totalReserved,
+ },
+ create: {
+ organizationId: user.organization.id,
+ cacheDate: today,
+ data: input.data,
+ totalProducts: input.totalProducts,
+ totalStocks: input.totalStocks,
+ totalReserved: input.totalReserved,
+ },
+ });
+
+ return {
+ success: true,
+ message: "Кеш склада WB успешно сохранен",
+ cache: {
+ ...cache,
+ cacheDate: cache.cacheDate.toISOString().split('T')[0],
+ createdAt: cache.createdAt.toISOString(),
+ updatedAt: cache.updatedAt.toISOString(),
+ },
+ fromCache: false,
+ };
+ } catch (error) {
+ console.error("Error saving WB warehouse cache:", error);
+ return {
+ success: false,
+ message: error instanceof Error ? error.message : "Ошибка сохранения кеша склада WB",
+ cache: null,
+ fromCache: false,
+ };
+ }
+ },
+};
+
// Добавляем админ запросы и мутации к основным резолверам
resolvers.Query = {
...resolvers.Query,
...adminQueries,
...wildberriesQueries,
+ ...externalAdQueries,
+ ...wbWarehouseCacheQueries,
};
resolvers.Mutation = {
...resolvers.Mutation,
...adminMutations,
+ ...externalAdMutations,
+ ...wbWarehouseCacheMutations,
};
diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts
index fd9708e..d0d388b 100644
--- a/src/graphql/typedefs.ts
+++ b/src/graphql/typedefs.ts
@@ -108,6 +108,12 @@ export const typeDefs = gql`
# Список кампаний Wildberries
getWildberriesCampaignsList: WildberriesCampaignsListResponse!
+
+ # Типы для внешней рекламы
+ getExternalAds(dateFrom: String!, dateTo: String!): ExternalAdsResponse!
+
+ # Типы для кеша склада WB
+ getWBWarehouseData: WBWarehouseCacheResponse!
}
type Mutation {
@@ -244,6 +250,12 @@ export const typeDefs = gql`
# Админ мутации
adminLogin(username: String!, password: String!): AdminAuthResponse!
adminLogout: Boolean!
+
+ # Типы для внешней рекламы
+ createExternalAd(input: ExternalAdInput!): ExternalAdResponse!
+ updateExternalAd(id: ID!, input: ExternalAdInput!): ExternalAdResponse!
+ deleteExternalAd(id: ID!): ExternalAdResponse!
+ updateExternalAdClicks(id: ID!, clicks: Int!): ExternalAdResponse!
}
# Типы данных
@@ -1147,4 +1159,84 @@ export const typeDefs = gql`
advertId: Int!
changeTime: String!
}
+
+ # Типы для внешней рекламы
+ type ExternalAd {
+ id: ID!
+ name: String!
+ url: String!
+ cost: Float!
+ date: String!
+ nmId: String!
+ clicks: Int!
+ organizationId: String!
+ createdAt: String!
+ updatedAt: String!
+ }
+
+ input ExternalAdInput {
+ name: String!
+ url: String!
+ cost: Float!
+ date: String!
+ nmId: String!
+ }
+
+ type ExternalAdResponse {
+ success: Boolean!
+ message: String
+ externalAd: ExternalAd
+ }
+
+ type ExternalAdsResponse {
+ success: Boolean!
+ message: String
+ externalAds: [ExternalAd!]!
+ }
+
+ extend type Query {
+ getExternalAds(dateFrom: String!, dateTo: String!): ExternalAdsResponse!
+ }
+
+ extend type Mutation {
+ createExternalAd(input: ExternalAdInput!): ExternalAdResponse!
+ updateExternalAd(id: ID!, input: ExternalAdInput!): ExternalAdResponse!
+ deleteExternalAd(id: ID!): ExternalAdResponse!
+ updateExternalAdClicks(id: ID!, clicks: Int!): ExternalAdResponse!
+ }
+
+ # Типы для кеша склада WB
+ type WBWarehouseCache {
+ id: ID!
+ organizationId: String!
+ cacheDate: String!
+ data: String! # JSON строка с данными
+ totalProducts: Int!
+ totalStocks: Int!
+ totalReserved: Int!
+ createdAt: String!
+ updatedAt: String!
+ }
+
+ type WBWarehouseCacheResponse {
+ success: Boolean!
+ message: String
+ cache: WBWarehouseCache
+ fromCache: Boolean! # Указывает, получены ли данные из кеша
+ }
+
+ input WBWarehouseCacheInput {
+ data: String! # JSON строка с данными склада
+ totalProducts: Int!
+ totalStocks: Int!
+ totalReserved: Int!
+ }
+
+ extend type Query {
+ getWBWarehouseData: WBWarehouseCacheResponse!
+ }
+
+ extend type Mutation {
+ saveWBWarehouseCache(input: WBWarehouseCacheInput!): WBWarehouseCacheResponse!
+ }
`;
diff --git a/src/lib/click-storage.ts b/src/lib/click-storage.ts
new file mode 100644
index 0000000..80272f9
--- /dev/null
+++ b/src/lib/click-storage.ts
@@ -0,0 +1,30 @@
+// Общее хранилище кликов для всех API роутов
+class ClickStorage {
+ private static instance: ClickStorage
+ private storage = new Map
()
+
+ static getInstance(): ClickStorage {
+ if (!ClickStorage.instance) {
+ ClickStorage.instance = new ClickStorage()
+ }
+ return ClickStorage.instance
+ }
+
+ recordClick(linkId: string): number {
+ const currentClicks = this.storage.get(linkId) || 0
+ const newClicks = currentClicks + 1
+ this.storage.set(linkId, newClicks)
+ console.log(`Click recorded for ${linkId}: ${newClicks} total`)
+ return newClicks
+ }
+
+ getClicks(linkId: string): number {
+ return this.storage.get(linkId) || 0
+ }
+
+ getAllClicks(): Record {
+ return Object.fromEntries(this.storage)
+ }
+}
+
+export const clickStorage = ClickStorage.getInstance()
\ No newline at end of file