import jwt from "jsonwebtoken"; import bcrypt from "bcryptjs"; import { GraphQLError } from "graphql"; import { GraphQLScalarType, Kind } from "graphql"; import { prisma } from "@/lib/prisma"; import { SmsService } from "@/services/sms-service"; import { DaDataService } from "@/services/dadata-service"; import { MarketplaceService } from "@/services/marketplace-service"; import { Prisma } from "@prisma/client"; // Сервисы const smsService = new SmsService(); const dadataService = new DaDataService(); const marketplaceService = new MarketplaceService(); // Интерфейсы для типизации interface Context { user?: { id: string; phone: string; }; admin?: { id: string; username: string; }; } interface CreateEmployeeInput { firstName: string; lastName: string; middleName?: string; birthDate?: string; avatar?: string; passportPhoto?: string; passportSeries?: string; passportNumber?: string; passportIssued?: string; passportDate?: string; address?: string; position: string; department?: string; hireDate: string; salary?: number; phone: string; email?: string; telegram?: string; whatsapp?: string; emergencyContact?: string; emergencyPhone?: string; } interface UpdateEmployeeInput { firstName?: string; lastName?: string; middleName?: string; birthDate?: string; avatar?: string; passportPhoto?: string; passportSeries?: string; passportNumber?: string; passportIssued?: string; passportDate?: string; address?: string; position?: string; department?: string; hireDate?: string; salary?: number; status?: "ACTIVE" | "VACATION" | "SICK" | "FIRED"; phone?: string; email?: string; telegram?: string; whatsapp?: string; emergencyContact?: string; emergencyPhone?: string; } interface UpdateScheduleInput { employeeId: string; date: string; status: "WORK" | "WEEKEND" | "VACATION" | "SICK" | "ABSENT"; hoursWorked?: number; notes?: string; } interface AuthTokenPayload { userId: string; phone: string; } // JWT утилиты const generateToken = (payload: AuthTokenPayload): string => { return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: "30d" }); }; // eslint-disable-next-line @typescript-eslint/no-unused-vars 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 } catch (error) { throw new GraphQLError("Недействительный токен", { extensions: { code: "UNAUTHENTICATED" }, }); } }; // Скалярный тип для JSON const JSONScalar = new GraphQLScalarType({ name: "JSON", description: "JSON custom scalar type", serialize(value: unknown) { return value; // значение отправляется клиенту }, parseValue(value: unknown) { return value; // значение получено от клиента }, parseLiteral(ast) { switch (ast.kind) { case Kind.STRING: case Kind.BOOLEAN: return ast.value; case Kind.INT: case Kind.FLOAT: return parseFloat(ast.value); case Kind.OBJECT: { const value = Object.create(null); ast.fields.forEach((field) => { value[field.name.value] = parseLiteral(field.value); }); return value; } case Kind.LIST: return ast.values.map(parseLiteral); default: return null; } }, }); // Скалярный тип для DateTime const DateTimeScalar = new GraphQLScalarType({ name: "DateTime", description: "DateTime custom scalar type", serialize(value: unknown) { if (value instanceof Date) { return value.toISOString(); // значение отправляется клиенту как ISO строка } return value; }, parseValue(value: unknown) { if (typeof value === "string") { return new Date(value); // значение получено от клиента, парсим как дату } return value; }, parseLiteral(ast) { if (ast.kind === Kind.STRING) { return new Date(ast.value); // AST значение как дата } return null; }, }); function parseLiteral(ast: unknown): unknown { const astNode = ast as { kind: string; value?: unknown; fields?: unknown[]; values?: unknown[]; }; switch (astNode.kind) { case Kind.STRING: case Kind.BOOLEAN: return astNode.value; case Kind.INT: case Kind.FLOAT: return parseFloat(astNode.value as string); case Kind.OBJECT: { const value = Object.create(null); if (astNode.fields) { astNode.fields.forEach((field: unknown) => { const fieldNode = field as { name: { value: string }; value: unknown; }; value[fieldNode.name.value] = parseLiteral(fieldNode.value); }); } return value; } case Kind.LIST: return (ast as { values: unknown[] }).values.map(parseLiteral); default: return null; } } export const resolvers = { JSON: JSONScalar, DateTime: DateTimeScalar, Query: { me: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } return await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: { include: { apiKeys: true, }, }, }, }); }, organization: async ( _: unknown, args: { id: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const organization = await prisma.organization.findUnique({ where: { id: args.id }, include: { apiKeys: true, users: true, }, }); if (!organization) { throw new GraphQLError("Организация не найдена"); } // Проверяем, что пользователь имеет доступ к этой организации const hasAccess = organization.users.some( (user) => user.id === context.user!.id ); if (!hasAccess) { throw new GraphQLError("Нет доступа к этой организации", { extensions: { code: "FORBIDDEN" }, }); } return organization; }, // Поиск организаций по типу для добавления в контрагенты searchOrganizations: async ( _: unknown, args: { type?: string; search?: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } // Получаем текущую организацию пользователя const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Получаем уже существующих контрагентов для добавления флага const existingCounterparties = await prisma.counterparty.findMany({ where: { organizationId: currentUser.organization.id }, select: { counterpartyId: true }, }); const existingCounterpartyIds = existingCounterparties.map( (c) => c.counterpartyId ); // Получаем исходящие заявки для добавления флага hasOutgoingRequest const outgoingRequests = await prisma.counterpartyRequest.findMany({ where: { senderId: currentUser.organization.id, status: "PENDING", }, select: { receiverId: true }, }); const outgoingRequestIds = outgoingRequests.map((r) => r.receiverId); // Получаем входящие заявки для добавления флага hasIncomingRequest const incomingRequests = await prisma.counterpartyRequest.findMany({ where: { receiverId: currentUser.organization.id, status: "PENDING", }, select: { senderId: true }, }); const incomingRequestIds = incomingRequests.map((r) => r.senderId); const where: Record = { // Больше не исключаем собственную организацию }; if (args.type) { where.type = args.type; } if (args.search) { where.OR = [ { name: { contains: args.search, mode: "insensitive" } }, { fullName: { contains: args.search, mode: "insensitive" } }, { inn: { contains: args.search } }, ]; } const organizations = await prisma.organization.findMany({ where, take: 50, // Ограничиваем количество результатов orderBy: { createdAt: "desc" }, include: { users: true, apiKeys: true, }, }); // Добавляем флаги isCounterparty, isCurrentUser, hasOutgoingRequest и hasIncomingRequest к каждой организации return organizations.map((org) => ({ ...org, isCounterparty: existingCounterpartyIds.includes(org.id), isCurrentUser: org.id === currentUser.organization?.id, hasOutgoingRequest: outgoingRequestIds.includes(org.id), hasIncomingRequest: incomingRequestIds.includes(org.id), })); }, // Мои контрагенты myCounterparties: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } const counterparties = await prisma.counterparty.findMany({ where: { organizationId: currentUser.organization.id }, include: { counterparty: { include: { users: true, apiKeys: true, }, }, }, }); return counterparties.map((c) => c.counterparty); }, // Входящие заявки incomingRequests: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } return await prisma.counterpartyRequest.findMany({ where: { receiverId: currentUser.organization.id, status: "PENDING", }, include: { sender: { include: { users: true, apiKeys: true, }, }, receiver: { include: { users: true, apiKeys: true, }, }, }, orderBy: { createdAt: "desc" }, }); }, // Исходящие заявки outgoingRequests: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } return await prisma.counterpartyRequest.findMany({ where: { senderId: currentUser.organization.id, status: { in: ["PENDING", "REJECTED"] }, }, include: { sender: { include: { users: true, apiKeys: true, }, }, receiver: { include: { users: true, apiKeys: true, }, }, }, orderBy: { createdAt: "desc" }, }); }, // Сообщения с контрагентом messages: async ( _: unknown, args: { counterpartyId: string; limit?: number; offset?: number }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } const limit = args.limit || 50; const offset = args.offset || 0; const messages = await prisma.message.findMany({ where: { OR: [ { senderOrganizationId: currentUser.organization.id, receiverOrganizationId: args.counterpartyId, }, { senderOrganizationId: args.counterpartyId, receiverOrganizationId: currentUser.organization.id, }, ], }, include: { sender: true, senderOrganization: { include: { users: true, }, }, receiverOrganization: { include: { users: true, }, }, }, orderBy: { createdAt: "asc" }, take: limit, skip: offset, }); return messages; }, // Список чатов (последние сообщения с каждым контрагентом) conversations: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // TODO: Здесь будет логика получения списка чатов // Пока возвращаем пустой массив, так как таблица сообщений еще не создана return []; }, // Мои услуги myServices: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что это фулфилмент центр if (currentUser.organization.type !== "FULFILLMENT") { throw new GraphQLError("Услуги доступны только для фулфилмент центров"); } return await prisma.service.findMany({ where: { organizationId: currentUser.organization.id }, include: { organization: true }, orderBy: { createdAt: "desc" }, }); }, // Мои расходники mySupplies: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } return await prisma.supply.findMany({ where: { organizationId: currentUser.organization.id }, include: { organization: true }, orderBy: { createdAt: "desc" }, }); }, // Логистика организации myLogistics: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } return await prisma.logistics.findMany({ where: { organizationId: currentUser.organization.id }, include: { organization: true }, orderBy: { createdAt: "desc" }, }); }, // Мои товары (для оптовиков) myProducts: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что это оптовик if (currentUser.organization.type !== "WHOLESALE") { throw new GraphQLError("Товары доступны только для оптовиков"); } return await prisma.product.findMany({ where: { organizationId: currentUser.organization.id }, include: { category: true, organization: true, }, orderBy: { createdAt: "desc" }, }); }, // Все товары всех оптовиков для маркета allProducts: async ( _: unknown, args: { search?: string; category?: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const where: Record = { isActive: true, // Показываем только активные товары organization: { type: "WHOLESALE", // Только товары оптовиков }, }; if (args.search) { where.OR = [ { name: { contains: args.search, mode: "insensitive" } }, { article: { contains: args.search, mode: "insensitive" } }, { description: { contains: args.search, mode: "insensitive" } }, { brand: { contains: args.search, mode: "insensitive" } }, ]; } if (args.category) { where.categoryId = args.category; } return await prisma.product.findMany({ where, include: { category: true, organization: { include: { users: true, }, }, }, orderBy: { createdAt: "desc" }, take: 100, // Ограничиваем количество результатов }); }, // Все категории categories: async (_: unknown, __: unknown, context: Context) => { if (!context.user && !context.admin) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } return await prisma.category.findMany({ orderBy: { name: "asc" }, }); }, // Корзина пользователя myCart: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Найти или создать корзину для организации let cart = await prisma.cart.findUnique({ where: { organizationId: currentUser.organization.id }, include: { items: { include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, }, organization: true, }, }); if (!cart) { cart = await prisma.cart.create({ data: { organizationId: currentUser.organization.id, }, include: { items: { include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, }, organization: true, }, }); } return cart; }, // Избранные товары пользователя myFavorites: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Получаем избранные товары const favorites = await prisma.favorites.findMany({ where: { organizationId: currentUser.organization.id }, include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, orderBy: { createdAt: "desc" }, }); return favorites.map((favorite) => favorite.product); }, // Сотрудники организации myEmployees: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } if (currentUser.organization.type !== "FULFILLMENT") { throw new GraphQLError("Доступно только для фулфилмент центров"); } const employees = await prisma.employee.findMany({ where: { organizationId: currentUser.organization.id }, include: { organization: true, }, orderBy: { createdAt: "desc" }, }); return employees; }, // Получение сотрудника по ID employee: async (_: unknown, args: { id: string }, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } if (currentUser.organization.type !== "FULFILLMENT") { throw new GraphQLError("Доступно только для фулфилмент центров"); } const employee = await prisma.employee.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, include: { organization: true, }, }); return employee; }, // Получить табель сотрудника за месяц employeeSchedule: async ( _: unknown, args: { employeeId: string; year: number; month: number }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } if (currentUser.organization.type !== "FULFILLMENT") { throw new GraphQLError("Доступно только для фулфилмент центров"); } // Проверяем что сотрудник принадлежит организации const employee = await prisma.employee.findFirst({ where: { id: args.employeeId, organizationId: currentUser.organization.id, }, }); if (!employee) { throw new GraphQLError("Сотрудник не найден"); } // Получаем записи табеля за указанный месяц const startDate = new Date(args.year, args.month, 1); const endDate = new Date(args.year, args.month + 1, 0); const scheduleRecords = await prisma.employeeSchedule.findMany({ where: { employeeId: args.employeeId, date: { gte: startDate, lte: endDate, }, }, orderBy: { date: "asc", }, }); return scheduleRecords; }, }, Mutation: { sendSmsCode: async (_: unknown, args: { phone: string }) => { const result = await smsService.sendSmsCode(args.phone); return { success: result.success, message: result.message || "SMS код отправлен", }; }, verifySmsCode: async ( _: unknown, args: { phone: string; code: string } ) => { const verificationResult = await smsService.verifySmsCode( args.phone, args.code ); if (!verificationResult.success) { return { success: false, message: verificationResult.message || "Неверный код", }; } // Найти или создать пользователя const formattedPhone = args.phone.replace(/\D/g, ""); let user = await prisma.user.findUnique({ where: { phone: formattedPhone }, include: { organization: { include: { apiKeys: true, }, }, }, }); if (!user) { user = await prisma.user.create({ data: { phone: formattedPhone, }, include: { organization: { include: { apiKeys: true, }, }, }, }); } const token = generateToken({ userId: user.id, phone: user.phone, }); console.log( "verifySmsCode - Generated token:", token ? `${token.substring(0, 20)}...` : "No token" ); console.log("verifySmsCode - Full token:", token); console.log("verifySmsCode - User object:", { id: user.id, phone: user.phone, }); const result = { success: true, message: "Авторизация успешна", token, user, }; console.log("verifySmsCode - Returning result:", { success: result.success, hasToken: !!result.token, hasUser: !!result.user, message: result.message, tokenPreview: result.token ? `${result.token.substring(0, 20)}...` : "No token in result", }); return result; }, verifyInn: async (_: unknown, args: { inn: string }) => { // Валидируем ИНН if (!dadataService.validateInn(args.inn)) { return { success: false, message: "Неверный формат ИНН", }; } // Получаем данные организации из DaData const organizationData = await dadataService.getOrganizationByInn( args.inn ); if (!organizationData) { return { success: false, message: "Организация с указанным ИНН не найдена", }; } return { success: true, message: "ИНН найден", organization: { name: organizationData.name, fullName: organizationData.fullName, address: organizationData.address, isActive: organizationData.isActive, }, }; }, registerFulfillmentOrganization: async ( _: unknown, args: { input: { phone: string; inn: string; type: "FULFILLMENT" | "LOGIST" | "WHOLESALE"; }; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const { inn, type } = args.input; // Валидируем ИНН if (!dadataService.validateInn(inn)) { return { success: false, message: "Неверный формат ИНН", }; } // Получаем данные организации из DaData const organizationData = await dadataService.getOrganizationByInn(inn); if (!organizationData) { return { success: false, message: "Организация с указанным ИНН не найдена", }; } try { // Проверяем, что организация еще не зарегистрирована const existingOrg = await prisma.organization.findUnique({ where: { inn: organizationData.inn }, }); if (existingOrg) { return { success: false, message: "Организация с таким ИНН уже зарегистрирована", }; } // Создаем организацию со всеми данными из DaData const organization = await prisma.organization.create({ data: { inn: organizationData.inn, kpp: organizationData.kpp, name: organizationData.name, fullName: organizationData.fullName, address: organizationData.address, 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)) : null, 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)), }, }); // Привязываем пользователя к организации const updatedUser = await prisma.user.update({ where: { id: context.user.id }, data: { organizationId: organization.id }, include: { organization: { include: { apiKeys: true, }, }, }, }); return { success: true, message: "Организация успешно зарегистрирована", user: updatedUser, }; } catch (error) { console.error("Error registering fulfillment organization:", error); return { success: false, message: "Ошибка при регистрации организации", }; } }, registerSellerOrganization: async ( _: unknown, args: { input: { phone: string; wbApiKey?: string; ozonApiKey?: string; ozonClientId?: string; }; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const { wbApiKey, ozonApiKey, ozonClientId } = args.input; if (!wbApiKey && !ozonApiKey) { return { success: false, message: "Необходимо указать хотя бы один API ключ маркетплейса", }; } try { // Валидируем API ключи const validationResults = []; if (wbApiKey) { const wbResult = await marketplaceService.validateWildberriesApiKey( wbApiKey ); if (!wbResult.isValid) { return { success: false, message: `Wildberries: ${wbResult.message}`, }; } validationResults.push({ marketplace: "WILDBERRIES", apiKey: wbApiKey, data: wbResult.data, }); } if (ozonApiKey && ozonClientId) { const ozonResult = await marketplaceService.validateOzonApiKey( ozonApiKey, ozonClientId ); if (!ozonResult.isValid) { return { success: false, message: `Ozon: ${ozonResult.message}`, }; } validationResults.push({ marketplace: "OZON", apiKey: ozonApiKey, data: ozonResult.data, }); } // Создаем организацию селлера - используем tradeMark как основное имя const tradeMark = validationResults[0]?.data?.tradeMark; const sellerName = validationResults[0]?.data?.sellerName; const shopName = tradeMark || sellerName || "Магазин"; const organization = await prisma.organization.create({ data: { inn: (validationResults[0]?.data?.inn as string) || `SELLER_${Date.now()}`, name: shopName, // Используем tradeMark как основное название fullName: sellerName ? `${sellerName} (${shopName})` : `Интернет-магазин "${shopName}"`, type: "SELLER", }, }); // Добавляем API ключи for (const validation of validationResults) { await prisma.apiKey.create({ data: { marketplace: validation.marketplace as "WILDBERRIES" | "OZON", apiKey: validation.apiKey, organizationId: organization.id, validationData: JSON.parse(JSON.stringify(validation.data)), }, }); } // Привязываем пользователя к организации const updatedUser = await prisma.user.update({ where: { id: context.user.id }, data: { organizationId: organization.id }, include: { organization: { include: { apiKeys: true, }, }, }, }); return { success: true, message: "Селлер организация успешно зарегистрирована", user: updatedUser, }; } catch (error) { console.error("Error registering seller organization:", error); return { success: false, message: "Ошибка при регистрации организации", }; } }, addMarketplaceApiKey: async ( _: unknown, args: { input: { marketplace: "WILDBERRIES" | "OZON"; apiKey: string; clientId?: string; validateOnly?: boolean; }; }, context: Context ) => { // Разрешаем валидацию без авторизации if (!args.input.validateOnly && !context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const { marketplace, apiKey, clientId, validateOnly } = args.input; // Валидируем API ключ const validationResult = await marketplaceService.validateApiKey( marketplace, apiKey, clientId ); if (!validationResult.isValid) { return { success: false, message: validationResult.message, }; } // Если это только валидация, возвращаем результат без сохранения if (validateOnly) { return { success: true, message: "API ключ действителен", apiKey: { id: "validate-only", marketplace, isActive: true, validationData: validationResult, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, }; } // Для сохранения API ключа нужна авторизация if (!context.user) { throw new GraphQLError( "Требуется авторизация для сохранения API ключа", { extensions: { code: "UNAUTHENTICATED" }, } ); } const user = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!user?.organization) { return { success: false, message: "Пользователь не привязан к организации", }; } try { // Проверяем, что такого ключа еще нет const existingKey = await prisma.apiKey.findUnique({ where: { organizationId_marketplace: { organizationId: user.organization.id, marketplace, }, }, }); if (existingKey) { // Обновляем существующий ключ const updatedKey = await prisma.apiKey.update({ where: { id: existingKey.id }, data: { apiKey, validationData: JSON.parse(JSON.stringify(validationResult.data)), isActive: true, }, }); return { success: true, message: "API ключ успешно обновлен", apiKey: updatedKey, }; } else { // Создаем новый ключ const newKey = await prisma.apiKey.create({ data: { marketplace, apiKey, organizationId: user.organization.id, validationData: JSON.parse(JSON.stringify(validationResult.data)), }, }); return { success: true, message: "API ключ успешно добавлен", apiKey: newKey, }; } } catch (error) { console.error("Error adding marketplace API key:", error); return { success: false, message: "Ошибка при добавлении API ключа", }; } }, removeMarketplaceApiKey: async ( _: unknown, args: { marketplace: "WILDBERRIES" | "OZON" }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const user = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!user?.organization) { throw new GraphQLError("Пользователь не привязан к организации"); } try { await prisma.apiKey.delete({ where: { organizationId_marketplace: { organizationId: user.organization.id, marketplace: args.marketplace, }, }, }); return true; } catch (error) { console.error("Error removing marketplace API key:", error); return false; } }, updateUserProfile: async ( _: unknown, args: { input: { avatar?: string; orgPhone?: string; managerName?: string; telegram?: string; whatsapp?: string; email?: string; bankName?: string; bik?: string; accountNumber?: string; corrAccount?: string; }; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const user = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: { include: { apiKeys: true, }, }, }, }); if (!user?.organization) { throw new GraphQLError("Пользователь не привязан к организации"); } try { const { input } = args; // Обновляем данные пользователя (аватар, имя управляющего) const userUpdateData: { avatar?: string; managerName?: string } = {}; if (input.avatar) { userUpdateData.avatar = input.avatar; } 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; emails?: object; managementName?: string; managementPost?: string; } = {}; // Название организации больше не обновляется через профиль // Для селлеров устанавливается при регистрации, для остальных - при смене ИНН // Обновляем контактные данные в JSON поле phones if (input.orgPhone) { updateData.phones = [{ value: input.orgPhone, type: "main" }]; } // Обновляем email в JSON поле emails if (input.email) { updateData.emails = [{ value: input.email, type: "main" }]; } // Сохраняем дополнительные контакты в custom полях // Пока добавим их как дополнительные JSON поля const customContacts: { managerName?: string; telegram?: string; whatsapp?: string; bankDetails?: { bankName?: string; bik?: string; accountNumber?: string; corrAccount?: string; }; } = {}; // managerName теперь сохраняется в поле пользователя, а не в JSON if (input.telegram) { customContacts.telegram = input.telegram; } if (input.whatsapp) { customContacts.whatsapp = input.whatsapp; } if ( input.bankName || input.bik || input.accountNumber || input.corrAccount ) { customContacts.bankDetails = { bankName: input.bankName, bik: input.bik, accountNumber: input.accountNumber, corrAccount: input.corrAccount, }; } // Если есть дополнительные контакты, сохраним их в поле managementPost временно // В идеале нужно добавить отдельную таблицу для контактов if (Object.keys(customContacts).length > 0) { updateData.managementPost = JSON.stringify(customContacts); } // Обновляем организацию await prisma.organization.update({ where: { id: user.organization.id }, data: updateData, include: { apiKeys: true, }, }); // Получаем обновленного пользователя const updatedUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: { include: { apiKeys: true, }, }, }, }); return { success: true, message: "Профиль успешно обновлен", user: updatedUser, }; } catch (error) { console.error("Error updating user profile:", error); return { success: false, message: "Ошибка при обновлении профиля", }; } }, updateOrganizationByInn: async ( _: unknown, args: { inn: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const user = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: { include: { apiKeys: true, }, }, }, }); if (!user?.organization) { throw new GraphQLError("Пользователь не привязан к организации"); } try { // Валидируем ИНН if (!dadataService.validateInn(args.inn)) { return { success: false, message: "Неверный формат ИНН", }; } // Получаем данные организации из DaData const organizationData = await dadataService.getOrganizationByInn( args.inn ); if (!organizationData) { return { success: false, message: "Организация с указанным ИНН не найдена в федеральном реестре", }; } // Проверяем, есть ли уже организация с таким ИНН в базе (кроме текущей) const existingOrganization = await prisma.organization.findUnique({ where: { inn: organizationData.inn }, }); if ( existingOrganization && existingOrganization.id !== user.organization.id ) { return { success: false, message: `Организация с ИНН ${organizationData.inn} уже существует в системе`, }; } // Подготавливаем данные для обновления const updateData: Prisma.OrganizationUpdateInput = { kpp: organizationData.kpp, // Для селлеров не обновляем название организации (это название магазина) ...(user.organization.type !== "SELLER" && { name: organizationData.name, }), fullName: organizationData.fullName, address: organizationData.address, addressFull: organizationData.addressFull, ogrn: organizationData.ogrn, ogrnDate: organizationData.ogrnDate ? organizationData.ogrnDate.toISOString() : null, registrationDate: organizationData.registrationDate ? organizationData.registrationDate.toISOString() : null, liquidationDate: organizationData.liquidationDate ? organizationData.liquidationDate.toISOString() : null, managementName: organizationData.managementName, // Всегда перезаписываем данными из DaData (может быть null) managementPost: user.organization.managementPost, // Сохраняем кастомные данные пользователя opfCode: organizationData.opfCode, opfFull: organizationData.opfFull, opfShort: organizationData.opfShort, okato: organizationData.okato, oktmo: organizationData.oktmo, okpo: organizationData.okpo, okved: organizationData.okved, status: organizationData.status, }; // Добавляем ИНН только если он отличается от текущего if (user.organization.inn !== organizationData.inn) { updateData.inn = organizationData.inn; } // Обновляем организацию await prisma.organization.update({ where: { id: user.organization.id }, data: updateData, include: { apiKeys: true, }, }); // Получаем обновленного пользователя const updatedUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: { include: { apiKeys: true, }, }, }, }); return { success: true, message: "Данные организации успешно обновлены", user: updatedUser, }; } catch (error) { console.error("Error updating organization by INN:", error); return { success: false, message: "Ошибка при обновлении данных организации", }; } }, logout: () => { // В stateless JWT системе logout происходит на клиенте // Можно добавить blacklist токенов, если нужно return true; }, // Отправить заявку на добавление в контрагенты sendCounterpartyRequest: async ( _: unknown, args: { organizationId: string; message?: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } if (currentUser.organization.id === args.organizationId) { throw new GraphQLError("Нельзя отправить заявку самому себе"); } // Проверяем, что организация-получатель существует const receiverOrganization = await prisma.organization.findUnique({ where: { id: args.organizationId }, }); if (!receiverOrganization) { throw new GraphQLError("Организация не найдена"); } try { // Создаем или обновляем заявку const request = await prisma.counterpartyRequest.upsert({ where: { senderId_receiverId: { senderId: currentUser.organization.id, receiverId: args.organizationId, }, }, update: { status: "PENDING", message: args.message, updatedAt: new Date(), }, create: { senderId: currentUser.organization.id, receiverId: args.organizationId, message: args.message, status: "PENDING", }, include: { sender: true, receiver: true, }, }); return { success: true, message: "Заявка отправлена", request, }; } catch (error) { console.error("Error sending counterparty request:", error); return { success: false, message: "Ошибка при отправке заявки", }; } }, // Ответить на заявку контрагента respondToCounterpartyRequest: async ( _: unknown, args: { requestId: string; accept: boolean }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } try { // Найти заявку и проверить права const request = await prisma.counterpartyRequest.findUnique({ where: { id: args.requestId }, include: { sender: true, receiver: true, }, }); if (!request) { throw new GraphQLError("Заявка не найдена"); } if (request.receiverId !== currentUser.organization.id) { throw new GraphQLError("Нет прав на обработку этой заявки"); } if (request.status !== "PENDING") { throw new GraphQLError("Заявка уже обработана"); } const newStatus = args.accept ? "ACCEPTED" : "REJECTED"; // Обновляем статус заявки const updatedRequest = await prisma.counterpartyRequest.update({ where: { id: args.requestId }, data: { status: newStatus }, include: { sender: true, receiver: true, }, }); // Если заявка принята, создаем связи контрагентов в обе стороны if (args.accept) { await prisma.$transaction([ // Добавляем отправителя в контрагенты получателя prisma.counterparty.create({ data: { organizationId: request.receiverId, counterpartyId: request.senderId, }, }), // Добавляем получателя в контрагенты отправителя prisma.counterparty.create({ data: { organizationId: request.senderId, counterpartyId: request.receiverId, }, }), ]); } return { success: true, message: args.accept ? "Заявка принята" : "Заявка отклонена", request: updatedRequest, }; } catch (error) { console.error("Error responding to counterparty request:", error); return { success: false, message: "Ошибка при обработке заявки", }; } }, // Отменить заявку cancelCounterpartyRequest: async ( _: unknown, args: { requestId: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } try { const request = await prisma.counterpartyRequest.findUnique({ where: { id: args.requestId }, }); if (!request) { throw new GraphQLError("Заявка не найдена"); } if (request.senderId !== currentUser.organization.id) { throw new GraphQLError("Можно отменить только свои заявки"); } if (request.status !== "PENDING") { throw new GraphQLError("Можно отменить только ожидающие заявки"); } await prisma.counterpartyRequest.update({ where: { id: args.requestId }, data: { status: "CANCELLED" }, }); return true; } catch (error) { console.error("Error cancelling counterparty request:", error); return false; } }, // Удалить контрагента removeCounterparty: async ( _: unknown, args: { organizationId: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } try { // Удаляем связь в обе стороны await prisma.$transaction([ prisma.counterparty.deleteMany({ where: { organizationId: currentUser.organization.id, counterpartyId: args.organizationId, }, }), prisma.counterparty.deleteMany({ where: { organizationId: args.organizationId, counterpartyId: currentUser.organization.id, }, }), ]); return true; } catch (error) { console.error("Error removing counterparty:", error); return false; } }, // Отправить сообщение sendMessage: async ( _: unknown, args: { receiverOrganizationId: string; content?: string; type?: "TEXT" | "VOICE"; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что получатель является контрагентом const isCounterparty = await prisma.counterparty.findFirst({ where: { organizationId: currentUser.organization.id, counterpartyId: args.receiverOrganizationId, }, }); if (!isCounterparty) { throw new GraphQLError( "Можно отправлять сообщения только контрагентам" ); } // Получаем организацию получателя const receiverOrganization = await prisma.organization.findUnique({ where: { id: args.receiverOrganizationId }, }); if (!receiverOrganization) { throw new GraphQLError("Организация получателя не найдена"); } try { // Создаем сообщение const message = await prisma.message.create({ data: { content: args.content?.trim() || null, type: args.type || "TEXT", senderId: context.user.id, senderOrganizationId: currentUser.organization.id, receiverOrganizationId: args.receiverOrganizationId, }, include: { sender: true, senderOrganization: { include: { users: true, }, }, receiverOrganization: { include: { users: true, }, }, }, }); return { success: true, message: "Сообщение отправлено", messageData: message, }; } catch (error) { console.error("Error sending message:", error); return { success: false, message: "Ошибка при отправке сообщения", }; } }, // Отправить голосовое сообщение sendVoiceMessage: async ( _: unknown, args: { receiverOrganizationId: string; voiceUrl: string; voiceDuration: number; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что получатель является контрагентом const isCounterparty = await prisma.counterparty.findFirst({ where: { organizationId: currentUser.organization.id, counterpartyId: args.receiverOrganizationId, }, }); if (!isCounterparty) { throw new GraphQLError( "Можно отправлять сообщения только контрагентам" ); } // Получаем организацию получателя const receiverOrganization = await prisma.organization.findUnique({ where: { id: args.receiverOrganizationId }, }); if (!receiverOrganization) { throw new GraphQLError("Организация получателя не найдена"); } try { // Создаем голосовое сообщение const message = await prisma.message.create({ data: { content: null, type: "VOICE", voiceUrl: args.voiceUrl, voiceDuration: args.voiceDuration, senderId: context.user.id, senderOrganizationId: currentUser.organization.id, receiverOrganizationId: args.receiverOrganizationId, }, include: { sender: true, senderOrganization: { include: { users: true, }, }, receiverOrganization: { include: { users: true, }, }, }, }); return { success: true, message: "Голосовое сообщение отправлено", messageData: message, }; } catch (error) { console.error("Error sending voice message:", error); return { success: false, message: "Ошибка при отправке голосового сообщения", }; } }, // Отправить изображение sendImageMessage: async ( _: unknown, args: { receiverOrganizationId: string; fileUrl: string; fileName: string; fileSize: number; fileType: string; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что получатель является контрагентом const isCounterparty = await prisma.counterparty.findFirst({ where: { organizationId: currentUser.organization.id, counterpartyId: args.receiverOrganizationId, }, }); if (!isCounterparty) { throw new GraphQLError( "Можно отправлять сообщения только контрагентам" ); } try { const message = await prisma.message.create({ data: { content: null, type: "IMAGE", fileUrl: args.fileUrl, fileName: args.fileName, fileSize: args.fileSize, fileType: args.fileType, senderId: context.user.id, senderOrganizationId: currentUser.organization.id, receiverOrganizationId: args.receiverOrganizationId, }, include: { sender: true, senderOrganization: { include: { users: true, }, }, receiverOrganization: { include: { users: true, }, }, }, }); return { success: true, message: "Изображение отправлено", messageData: message, }; } catch (error) { console.error("Error sending image:", error); return { success: false, message: "Ошибка при отправке изображения", }; } }, // Отправить файл sendFileMessage: async ( _: unknown, args: { receiverOrganizationId: string; fileUrl: string; fileName: string; fileSize: number; fileType: string; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что получатель является контрагентом const isCounterparty = await prisma.counterparty.findFirst({ where: { organizationId: currentUser.organization.id, counterpartyId: args.receiverOrganizationId, }, }); if (!isCounterparty) { throw new GraphQLError( "Можно отправлять сообщения только контрагентам" ); } try { const message = await prisma.message.create({ data: { content: null, type: "FILE", fileUrl: args.fileUrl, fileName: args.fileName, fileSize: args.fileSize, fileType: args.fileType, senderId: context.user.id, senderOrganizationId: currentUser.organization.id, receiverOrganizationId: args.receiverOrganizationId, }, include: { sender: true, senderOrganization: { include: { users: true, }, }, receiverOrganization: { include: { users: true, }, }, }, }); return { success: true, message: "Файл отправлен", messageData: message, }; } catch (error) { console.error("Error sending file:", error); return { success: false, message: "Ошибка при отправке файла", }; } }, // Отметить сообщения как прочитанные markMessagesAsRead: async ( _: unknown, args: { conversationId: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } // TODO: Здесь будет логика обновления статуса сообщений // Пока возвращаем успешный ответ return true; }, // Создать услугу createService: async ( _: unknown, args: { input: { name: string; description?: string; price: number; imageUrl?: string; }; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что это фулфилмент центр if (currentUser.organization.type !== "FULFILLMENT") { throw new GraphQLError("Услуги доступны только для фулфилмент центров"); } try { const service = await prisma.service.create({ data: { name: args.input.name, description: args.input.description, price: args.input.price, imageUrl: args.input.imageUrl, organizationId: currentUser.organization.id, }, include: { organization: true }, }); return { success: true, message: "Услуга успешно создана", service, }; } catch (error) { console.error("Error creating service:", error); return { success: false, message: "Ошибка при создании услуги", }; } }, // Обновить услугу updateService: async ( _: unknown, args: { id: string; input: { name: string; description?: string; price: number; imageUrl?: string; }; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что услуга принадлежит текущей организации const existingService = await prisma.service.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }); if (!existingService) { throw new GraphQLError("Услуга не найдена или нет доступа"); } try { const service = await prisma.service.update({ where: { id: args.id }, data: { name: args.input.name, description: args.input.description, price: args.input.price, imageUrl: args.input.imageUrl, }, include: { organization: true }, }); return { success: true, message: "Услуга успешно обновлена", service, }; } catch (error) { console.error("Error updating service:", error); return { success: false, message: "Ошибка при обновлении услуги", }; } }, // Удалить услугу deleteService: async ( _: unknown, args: { id: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что услуга принадлежит текущей организации const existingService = await prisma.service.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }); if (!existingService) { throw new GraphQLError("Услуга не найдена или нет доступа"); } try { await prisma.service.delete({ where: { id: args.id }, }); return true; } catch (error) { console.error("Error deleting service:", error); return false; } }, // Создать расходник createSupply: async ( _: unknown, args: { input: { name: string; description?: string; price: number; imageUrl?: string; }; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что это фулфилмент центр if (currentUser.organization.type !== "FULFILLMENT") { throw new GraphQLError( "Расходники доступны только для фулфилмент центров" ); } try { const supply = await prisma.supply.create({ data: { name: args.input.name, description: args.input.description, price: args.input.price, quantity: 0, // Временно устанавливаем 0, так как поле убрано из интерфейса imageUrl: args.input.imageUrl, organizationId: currentUser.organization.id, }, include: { organization: true }, }); return { success: true, message: "Расходник успешно создан", supply, }; } catch (error) { console.error("Error creating supply:", error); return { success: false, message: "Ошибка при создании расходника", }; } }, // Обновить расходник updateSupply: async ( _: unknown, args: { id: string; input: { name: string; description?: string; price: number; imageUrl?: string; }; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что расходник принадлежит текущей организации const existingSupply = await prisma.supply.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }); if (!existingSupply) { throw new GraphQLError("Расходник не найден или нет доступа"); } try { const supply = await prisma.supply.update({ where: { id: args.id }, data: { name: args.input.name, description: args.input.description, price: args.input.price, quantity: 0, // Временно устанавливаем 0, так как поле убрано из интерфейса imageUrl: args.input.imageUrl, }, include: { organization: true }, }); return { success: true, message: "Расходник успешно обновлен", supply, }; } catch (error) { console.error("Error updating supply:", error); return { success: false, message: "Ошибка при обновлении расходника", }; } }, // Удалить расходник deleteSupply: async ( _: unknown, args: { id: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что расходник принадлежит текущей организации const existingSupply = await prisma.supply.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }); if (!existingSupply) { throw new GraphQLError("Расходник не найден или нет доступа"); } try { await prisma.supply.delete({ where: { id: args.id }, }); return true; } catch (error) { console.error("Error deleting supply:", error); return false; } }, // Создать заказ поставки расходников createSupplyOrder: async ( _: unknown, args: { input: { partnerId: string; deliveryDate: string; items: Array<{ productId: string; quantity: number }>; }; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что это фулфилмент центр if (currentUser.organization.type !== "FULFILLMENT") { throw new GraphQLError( "Заказы поставок доступны только для фулфилмент центров" ); } // Проверяем, что партнер существует и является оптовиком const partner = await prisma.organization.findFirst({ where: { id: args.input.partnerId, type: "WHOLESALE", }, }); if (!partner) { return { success: false, message: "Партнер не найден или не является оптовиком", }; } // Проверяем, что партнер является контрагентом const counterparty = await prisma.counterparty.findFirst({ where: { organizationId: currentUser.organization.id, counterpartyId: args.input.partnerId, }, }); if (!counterparty) { return { success: false, message: "Данная организация не является вашим партнером", }; } // Получаем товары для проверки наличия и цен const productIds = args.input.items.map((item) => item.productId); const products = await prisma.product.findMany({ where: { id: { in: productIds }, organizationId: args.input.partnerId, isActive: true, }, }); if (products.length !== productIds.length) { return { success: false, message: "Некоторые товары не найдены или неактивны", }; } // Проверяем наличие товаров for (const item of args.input.items) { const product = products.find((p) => p.id === item.productId); if (!product) { return { success: false, message: `Товар ${item.productId} не найден`, }; } if (product.quantity < item.quantity) { return { success: false, message: `Недостаточно товара "${product.name}". Доступно: ${product.quantity}, запрошено: ${item.quantity}`, }; } } // Рассчитываем общую сумму и количество let totalAmount = 0; let totalItems = 0; const orderItems = args.input.items.map((item) => { const product = products.find((p) => p.id === item.productId)!; const itemTotal = Number(product.price) * item.quantity; totalAmount += itemTotal; totalItems += item.quantity; return { productId: item.productId, quantity: item.quantity, price: product.price, totalPrice: new Prisma.Decimal(itemTotal), }; }); try { const supplyOrder = await prisma.supplyOrder.create({ data: { partnerId: args.input.partnerId, deliveryDate: new Date(args.input.deliveryDate), totalAmount: new Prisma.Decimal(totalAmount), totalItems: totalItems, organizationId: currentUser.organization.id, items: { create: orderItems, }, }, include: { partner: { include: { users: true, }, }, organization: { include: { users: true, }, }, items: { include: { product: { include: { category: true, organization: true, }, }, }, }, }, }); return { success: true, message: "Заказ поставки создан успешно", order: supplyOrder, }; } catch (error) { console.error("Error creating supply order:", error); return { success: false, message: "Ошибка при создании заказа поставки", }; } }, // Создать товар 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; }; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что это оптовик if (currentUser.organization.type !== "WHOLESALE") { throw new GraphQLError("Товары доступны только для оптовиков"); } // Проверяем уникальность артикула в рамках организации const existingProduct = await prisma.product.findFirst({ where: { article: args.input.article, organizationId: currentUser.organization.id, }, }); if (existingProduct) { return { success: false, message: "Товар с таким артикулом уже существует", }; } try { const product = await prisma.product.create({ data: { name: args.input.name, article: args.input.article, description: args.input.description, price: args.input.price, quantity: args.input.quantity, categoryId: args.input.categoryId, brand: args.input.brand, color: args.input.color, size: args.input.size, weight: args.input.weight, dimensions: args.input.dimensions, material: args.input.material, images: args.input.images || [], mainImage: args.input.mainImage, isActive: args.input.isActive ?? true, organizationId: currentUser.organization.id, }, include: { category: true, organization: true, }, }); return { success: true, message: "Товар успешно создан", product, }; } catch (error) { console.error("Error creating product:", error); return { success: false, message: "Ошибка при создании товара", }; } }, // Обновить товар 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; }; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что товар принадлежит текущей организации const existingProduct = await prisma.product.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }); if (!existingProduct) { throw new GraphQLError("Товар не найден или нет доступа"); } // Проверяем уникальность артикула (если он изменился) if (args.input.article !== existingProduct.article) { const duplicateProduct = await prisma.product.findFirst({ where: { article: args.input.article, organizationId: currentUser.organization.id, NOT: { id: args.id }, }, }); if (duplicateProduct) { return { success: false, message: "Товар с таким артикулом уже существует", }; } } try { const product = await prisma.product.update({ where: { id: args.id }, data: { name: args.input.name, article: args.input.article, description: args.input.description, price: args.input.price, quantity: args.input.quantity, categoryId: args.input.categoryId, brand: args.input.brand, color: args.input.color, size: args.input.size, weight: args.input.weight, dimensions: args.input.dimensions, material: args.input.material, images: args.input.images || [], mainImage: args.input.mainImage, isActive: args.input.isActive ?? true, }, include: { category: true, organization: true, }, }); return { success: true, message: "Товар успешно обновлен", product, }; } catch (error) { console.error("Error updating product:", error); return { success: false, message: "Ошибка при обновлении товара", }; } }, // Удалить товар deleteProduct: async ( _: unknown, args: { id: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что товар принадлежит текущей организации const existingProduct = await prisma.product.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }); if (!existingProduct) { throw new GraphQLError("Товар не найден или нет доступа"); } try { await prisma.product.delete({ where: { id: args.id }, }); return true; } catch (error) { console.error("Error deleting product:", error); return false; } }, // Создать категорию createCategory: async ( _: unknown, args: { input: { name: string } }, context: Context ) => { if (!context.user && !context.admin) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } // Проверяем уникальность названия категории const existingCategory = await prisma.category.findUnique({ where: { name: args.input.name }, }); if (existingCategory) { return { success: false, message: "Категория с таким названием уже существует", }; } try { const category = await prisma.category.create({ data: { name: args.input.name, }, }); return { success: true, message: "Категория успешно создана", category, }; } catch (error) { console.error("Error creating category:", error); return { success: false, message: "Ошибка при создании категории", }; } }, // Обновить категорию updateCategory: async ( _: unknown, args: { id: string; input: { name: string } }, context: Context ) => { if (!context.user && !context.admin) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } // Проверяем существование категории const existingCategory = await prisma.category.findUnique({ where: { id: args.id }, }); if (!existingCategory) { return { success: false, message: "Категория не найдена", }; } // Проверяем уникальность нового названия (если изменилось) if (args.input.name !== existingCategory.name) { const duplicateCategory = await prisma.category.findUnique({ where: { name: args.input.name }, }); if (duplicateCategory) { return { success: false, message: "Категория с таким названием уже существует", }; } } try { const category = await prisma.category.update({ where: { id: args.id }, data: { name: args.input.name, }, }); return { success: true, message: "Категория успешно обновлена", category, }; } catch (error) { console.error("Error updating category:", error); return { success: false, message: "Ошибка при обновлении категории", }; } }, // Удалить категорию deleteCategory: async ( _: unknown, args: { id: string }, context: Context ) => { if (!context.user && !context.admin) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } // Проверяем существование категории const existingCategory = await prisma.category.findUnique({ where: { id: args.id }, include: { products: true }, }); if (!existingCategory) { throw new GraphQLError("Категория не найдена"); } // Проверяем, есть ли товары в этой категории if (existingCategory.products.length > 0) { throw new GraphQLError( "Нельзя удалить категорию, в которой есть товары" ); } try { await prisma.category.delete({ where: { id: args.id }, }); return true; } catch (error) { console.error("Error deleting category:", error); return false; } }, // Добавить товар в корзину addToCart: async ( _: unknown, args: { productId: string; quantity: number }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что товар существует и активен const product = await prisma.product.findFirst({ where: { id: args.productId, isActive: true, }, include: { organization: true, }, }); if (!product) { return { success: false, message: "Товар не найден или неактивен", }; } // Проверяем, что пользователь не пытается добавить свой собственный товар if (product.organizationId === currentUser.organization.id) { return { success: false, message: "Нельзя добавлять собственные товары в корзину", }; } // Найти или создать корзину let cart = await prisma.cart.findUnique({ where: { organizationId: currentUser.organization.id }, }); if (!cart) { cart = await prisma.cart.create({ data: { organizationId: currentUser.organization.id, }, }); } try { // Проверяем, есть ли уже такой товар в корзине const existingCartItem = await prisma.cartItem.findUnique({ where: { cartId_productId: { cartId: cart.id, productId: args.productId, }, }, }); if (existingCartItem) { // Обновляем количество const newQuantity = existingCartItem.quantity + args.quantity; if (newQuantity > product.quantity) { return { success: false, message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`, }; } await prisma.cartItem.update({ where: { id: existingCartItem.id }, data: { quantity: newQuantity }, }); } else { // Создаем новый элемент корзины if (args.quantity > product.quantity) { return { success: false, message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`, }; } await prisma.cartItem.create({ data: { cartId: cart.id, productId: args.productId, quantity: args.quantity, }, }); } // Возвращаем обновленную корзину const updatedCart = await prisma.cart.findUnique({ where: { id: cart.id }, include: { items: { include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, }, organization: true, }, }); return { success: true, message: "Товар добавлен в корзину", cart: updatedCart, }; } catch (error) { console.error("Error adding to cart:", error); return { success: false, message: "Ошибка при добавлении в корзину", }; } }, // Обновить количество товара в корзине updateCartItem: async ( _: unknown, args: { productId: string; quantity: number }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } const cart = await prisma.cart.findUnique({ where: { organizationId: currentUser.organization.id }, }); if (!cart) { return { success: false, message: "Корзина не найдена", }; } // Проверяем, что товар существует в корзине const cartItem = await prisma.cartItem.findUnique({ where: { cartId_productId: { cartId: cart.id, productId: args.productId, }, }, include: { product: true, }, }); if (!cartItem) { return { success: false, message: "Товар не найден в корзине", }; } if (args.quantity <= 0) { return { success: false, message: "Количество должно быть больше 0", }; } if (args.quantity > cartItem.product.quantity) { return { success: false, message: `Недостаточно товара в наличии. Доступно: ${cartItem.product.quantity}`, }; } try { await prisma.cartItem.update({ where: { id: cartItem.id }, data: { quantity: args.quantity }, }); // Возвращаем обновленную корзину const updatedCart = await prisma.cart.findUnique({ where: { id: cart.id }, include: { items: { include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, }, organization: true, }, }); return { success: true, message: "Количество товара обновлено", cart: updatedCart, }; } catch (error) { console.error("Error updating cart item:", error); return { success: false, message: "Ошибка при обновлении корзины", }; } }, // Удалить товар из корзины removeFromCart: async ( _: unknown, args: { productId: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } const cart = await prisma.cart.findUnique({ where: { organizationId: currentUser.organization.id }, }); if (!cart) { return { success: false, message: "Корзина не найдена", }; } try { await prisma.cartItem.delete({ where: { cartId_productId: { cartId: cart.id, productId: args.productId, }, }, }); // Возвращаем обновленную корзину const updatedCart = await prisma.cart.findUnique({ where: { id: cart.id }, include: { items: { include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, }, organization: true, }, }); return { success: true, message: "Товар удален из корзины", cart: updatedCart, }; } catch (error) { console.error("Error removing from cart:", error); return { success: false, message: "Ошибка при удалении из корзины", }; } }, // Очистить корзину clearCart: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } const cart = await prisma.cart.findUnique({ where: { organizationId: currentUser.organization.id }, }); if (!cart) { return false; } try { await prisma.cartItem.deleteMany({ where: { cartId: cart.id }, }); return true; } catch (error) { console.error("Error clearing cart:", error); return false; } }, // Добавить товар в избранное addToFavorites: async ( _: unknown, args: { productId: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что товар существует и активен const product = await prisma.product.findFirst({ where: { id: args.productId, isActive: true, }, include: { organization: true, }, }); if (!product) { return { success: false, message: "Товар не найден или неактивен", }; } // Проверяем, что пользователь не пытается добавить свой собственный товар if (product.organizationId === currentUser.organization.id) { return { success: false, message: "Нельзя добавлять собственные товары в избранное", }; } try { // Проверяем, есть ли уже такой товар в избранном const existingFavorite = await prisma.favorites.findUnique({ where: { organizationId_productId: { organizationId: currentUser.organization.id, productId: args.productId, }, }, }); if (existingFavorite) { return { success: false, message: "Товар уже в избранном", }; } // Добавляем товар в избранное await prisma.favorites.create({ data: { organizationId: currentUser.organization.id, productId: args.productId, }, }); // Возвращаем обновленный список избранного const favorites = await prisma.favorites.findMany({ where: { organizationId: currentUser.organization.id }, include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, orderBy: { createdAt: "desc" }, }); return { success: true, message: "Товар добавлен в избранное", favorites: favorites.map((favorite) => favorite.product), }; } catch (error) { console.error("Error adding to favorites:", error); return { success: false, message: "Ошибка при добавлении в избранное", }; } }, // Удалить товар из избранного removeFromFavorites: async ( _: unknown, args: { productId: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } try { // Удаляем товар из избранного await prisma.favorites.deleteMany({ where: { organizationId: currentUser.organization.id, productId: args.productId, }, }); // Возвращаем обновленный список избранного const favorites = await prisma.favorites.findMany({ where: { organizationId: currentUser.organization.id }, include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, orderBy: { createdAt: "desc" }, }); return { success: true, message: "Товар удален из избранного", favorites: favorites.map((favorite) => favorite.product), }; } catch (error) { console.error("Error removing from favorites:", error); return { success: false, message: "Ошибка при удалении из избранного", }; } }, // Создать сотрудника createEmployee: async ( _: unknown, args: { input: CreateEmployeeInput }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } if (currentUser.organization.type !== "FULFILLMENT") { throw new GraphQLError("Доступно только для фулфилмент центров"); } try { const employee = await prisma.employee.create({ data: { ...args.input, organizationId: currentUser.organization.id, birthDate: args.input.birthDate ? new Date(args.input.birthDate) : undefined, passportDate: args.input.passportDate ? new Date(args.input.passportDate) : undefined, hireDate: new Date(args.input.hireDate), }, include: { organization: true, }, }); return { success: true, message: "Сотрудник успешно добавлен", employee, }; } catch (error) { console.error("Error creating employee:", error); return { success: false, message: "Ошибка при создании сотрудника", }; } }, // Обновить сотрудника updateEmployee: async ( _: unknown, args: { id: string; input: UpdateEmployeeInput }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } if (currentUser.organization.type !== "FULFILLMENT") { throw new GraphQLError("Доступно только для фулфилмент центров"); } try { const employee = await prisma.employee.update({ where: { id: args.id, organizationId: currentUser.organization.id, }, data: { ...args.input, birthDate: args.input.birthDate ? new Date(args.input.birthDate) : undefined, passportDate: args.input.passportDate ? new Date(args.input.passportDate) : undefined, hireDate: args.input.hireDate ? new Date(args.input.hireDate) : undefined, }, include: { organization: true, }, }); return { success: true, message: "Сотрудник успешно обновлен", employee, }; } catch (error) { console.error("Error updating employee:", error); return { success: false, message: "Ошибка при обновлении сотрудника", }; } }, // Удалить сотрудника deleteEmployee: async ( _: unknown, args: { id: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } if (currentUser.organization.type !== "FULFILLMENT") { throw new GraphQLError("Доступно только для фулфилмент центров"); } try { await prisma.employee.delete({ where: { id: args.id, organizationId: currentUser.organization.id, }, }); return true; } catch (error) { console.error("Error deleting employee:", error); return false; } }, // Обновить табель сотрудника updateEmployeeSchedule: async ( _: unknown, args: { input: UpdateScheduleInput }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } if (currentUser.organization.type !== "FULFILLMENT") { throw new GraphQLError("Доступно только для фулфилмент центров"); } try { // Проверяем что сотрудник принадлежит организации const employee = await prisma.employee.findFirst({ where: { id: args.input.employeeId, organizationId: currentUser.organization.id, }, }); if (!employee) { throw new GraphQLError("Сотрудник не найден"); } // Создаем или обновляем запись табеля await prisma.employeeSchedule.upsert({ where: { employeeId_date: { employeeId: args.input.employeeId, date: new Date(args.input.date), }, }, create: { employeeId: args.input.employeeId, date: new Date(args.input.date), status: args.input.status, hoursWorked: args.input.hoursWorked, notes: args.input.notes, }, update: { status: args.input.status, hoursWorked: args.input.hoursWorked, notes: args.input.notes, }, }); return true; } catch (error) { console.error("Error updating employee schedule:", error); return false; } }, }, // Резолверы типов Organization: { users: async (parent: { id: string; users?: unknown[] }) => { // Если пользователи уже загружены через include, возвращаем их if (parent.users) { return parent.users; } // Иначе загружаем отдельно return await prisma.user.findMany({ where: { organizationId: parent.id }, }); }, }, Cart: { totalPrice: (parent: { items: Array<{ product: { price: number }; quantity: number }>; }) => { return parent.items.reduce((total, item) => { return total + Number(item.product.price) * item.quantity; }, 0); }, totalItems: (parent: { items: Array<{ quantity: number }> }) => { return parent.items.reduce((total, item) => total + item.quantity, 0); }, }, CartItem: { totalPrice: (parent: { product: { price: number }; quantity: number }) => { return Number(parent.product.price) * parent.quantity; }, isAvailable: (parent: { product: { quantity: number; isActive: boolean }; quantity: number; }) => { return ( parent.product.isActive && parent.product.quantity >= parent.quantity ); }, availableQuantity: (parent: { product: { quantity: number } }) => { return parent.product.quantity; }, }, User: { organization: async (parent: { organizationId?: string; organization?: unknown; }) => { // Если организация уже загружена через include, возвращаем её if (parent.organization) { return parent.organization; } // Иначе загружаем отдельно если есть organizationId if (parent.organizationId) { return await prisma.organization.findUnique({ where: { id: parent.organizationId }, include: { apiKeys: true, users: true, }, }); } return null; }, }, Message: { type: (parent: { type?: string | null }) => { return parent.type || "TEXT"; }, createdAt: (parent: { createdAt: Date | string }) => { if (parent.createdAt instanceof Date) { return parent.createdAt.toISOString(); } return parent.createdAt; }, updatedAt: (parent: { updatedAt: Date | string }) => { if (parent.updatedAt instanceof Date) { return parent.updatedAt.toISOString(); } return parent.updatedAt; }, }, Employee: { birthDate: (parent: { birthDate?: Date | string | null }) => { if (!parent.birthDate) return null; if (parent.birthDate instanceof Date) { return parent.birthDate.toISOString(); } return parent.birthDate; }, passportDate: (parent: { passportDate?: Date | string | null }) => { if (!parent.passportDate) return null; if (parent.passportDate instanceof Date) { return parent.passportDate.toISOString(); } return parent.passportDate; }, hireDate: (parent: { hireDate: Date | string }) => { if (parent.hireDate instanceof Date) { return parent.hireDate.toISOString(); } return parent.hireDate; }, createdAt: (parent: { createdAt: Date | string }) => { if (parent.createdAt instanceof Date) { return parent.createdAt.toISOString(); } return parent.createdAt; }, updatedAt: (parent: { updatedAt: Date | string }) => { if (parent.updatedAt instanceof Date) { return parent.updatedAt.toISOString(); } return parent.updatedAt; }, }, EmployeeSchedule: { date: (parent: { date: Date | string }) => { if (parent.date instanceof Date) { return parent.date.toISOString(); } return parent.date; }, createdAt: (parent: { createdAt: Date | string }) => { if (parent.createdAt instanceof Date) { return parent.createdAt.toISOString(); } return parent.createdAt; }, updatedAt: (parent: { updatedAt: Date | string }) => { if (parent.updatedAt instanceof Date) { return parent.updatedAt.toISOString(); } return parent.updatedAt; }, employee: async (parent: { employeeId: string }) => { return await prisma.employee.findUnique({ where: { id: parent.employeeId }, }); }, }, }; // Логистические мутации const logisticsMutations = { // Создать логистический маршрут createLogistics: async ( _: unknown, args: { input: { fromLocation: string; toLocation: string; priceUnder1m3: number; priceOver1m3: number; description?: string; }; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } try { const logistics = await prisma.logistics.create({ data: { fromLocation: args.input.fromLocation, toLocation: args.input.toLocation, priceUnder1m3: args.input.priceUnder1m3, priceOver1m3: args.input.priceOver1m3, description: args.input.description, organizationId: currentUser.organization.id, }, include: { organization: true, }, }); console.log("✅ Logistics created:", logistics.id); return { success: true, message: "Логистический маршрут создан", logistics, }; } catch (error) { console.error("❌ Error creating logistics:", error); return { success: false, message: "Ошибка при создании логистического маршрута", }; } }, // Обновить логистический маршрут updateLogistics: async ( _: unknown, args: { id: string; input: { fromLocation: string; toLocation: string; priceUnder1m3: number; priceOver1m3: number; description?: string; }; }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } try { // Проверяем, что маршрут принадлежит организации пользователя const existingLogistics = await prisma.logistics.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }); if (!existingLogistics) { throw new GraphQLError("Логистический маршрут не найден"); } const logistics = await prisma.logistics.update({ where: { id: args.id }, data: { fromLocation: args.input.fromLocation, toLocation: args.input.toLocation, priceUnder1m3: args.input.priceUnder1m3, priceOver1m3: args.input.priceOver1m3, description: args.input.description, }, include: { organization: true, }, }); console.log("✅ Logistics updated:", logistics.id); return { success: true, message: "Логистический маршрут обновлен", logistics, }; } catch (error) { console.error("❌ Error updating logistics:", error); return { success: false, message: "Ошибка при обновлении логистического маршрута", }; } }, // Удалить логистический маршрут deleteLogistics: async ( _: unknown, args: { id: string }, context: Context ) => { if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } try { // Проверяем, что маршрут принадлежит организации пользователя const existingLogistics = await prisma.logistics.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }); if (!existingLogistics) { throw new GraphQLError("Логистический маршрут не найден"); } await prisma.logistics.delete({ where: { id: args.id }, }); console.log("✅ Logistics deleted:", args.id); return true; } catch (error) { console.error("❌ Error deleting logistics:", error); return false; } }, }; // Добавляем логистические мутации к основным резолверам resolvers.Mutation = { ...resolvers.Mutation, ...logisticsMutations, }; // Админ резолверы const adminQueries = { adminMe: async (_: unknown, __: unknown, context: Context) => { if (!context.admin) { throw new GraphQLError("Требуется авторизация администратора", { extensions: { code: "UNAUTHENTICATED" }, }); } const admin = await prisma.admin.findUnique({ where: { id: context.admin.id }, }); if (!admin) { throw new GraphQLError("Администратор не найден"); } return admin; }, allUsers: async ( _: unknown, args: { search?: string; limit?: number; offset?: number }, context: Context ) => { if (!context.admin) { throw new GraphQLError("Требуется авторизация администратора", { extensions: { code: "UNAUTHENTICATED" }, }); } 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" } }, { fullName: { contains: args.search, mode: "insensitive" } }, { inn: { contains: args.search, mode: "insensitive" } }, ], }, }, ], } : {}; // Получаем пользователей с пагинацией const [users, total] = await Promise.all([ prisma.user.findMany({ where: whereCondition, include: { organization: true, }, take: limit, skip: offset, orderBy: { createdAt: "desc" }, }), prisma.user.count({ where: whereCondition, }), ]); return { users, total, hasMore: offset + limit < total, }; }, }; const adminMutations = { adminLogin: async ( _: unknown, args: { username: string; password: string } ) => { try { // Найти администратора const admin = await prisma.admin.findUnique({ where: { username: args.username }, }); if (!admin) { return { success: false, message: "Неверные учетные данные", }; } // Проверить активность if (!admin.isActive) { return { success: false, message: "Аккаунт заблокирован", }; } // Проверить пароль const isPasswordValid = await bcrypt.compare( args.password, admin.password ); if (!isPasswordValid) { return { success: false, message: "Неверные учетные данные", }; } // Обновить время последнего входа await prisma.admin.update({ where: { id: admin.id }, data: { lastLogin: new Date() }, }); // Создать токен const token = jwt.sign( { adminId: admin.id, username: admin.username, type: "admin", }, process.env.JWT_SECRET!, { expiresIn: "24h" } ); return { success: true, message: "Успешная авторизация", token, admin: { ...admin, password: undefined, // Не возвращаем пароль }, }; } catch (error) { console.error("Admin login error:", error); return { success: false, message: "Ошибка авторизации", }; } }, adminLogout: async (_: unknown, __: unknown, context: Context) => { if (!context.admin) { throw new GraphQLError("Требуется авторизация администратора", { extensions: { code: "UNAUTHENTICATED" }, }); } return true; }, }; // Добавляем админ запросы и мутации к основным резолверам resolvers.Query = { ...resolvers.Query, ...adminQueries, }; resolvers.Mutation = { ...resolvers.Mutation, ...adminMutations, };