4565 lines
134 KiB
TypeScript
4565 lines
134 KiB
TypeScript
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<string, unknown> = {
|
||
// Больше не исключаем собственную организацию
|
||
};
|
||
|
||
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<string, unknown> = {
|
||
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,
|
||
};
|