Добавлены модели и мутации для управления заказами поставок расходников. Обновлены схемы GraphQL с новыми типами и полями для SupplyOrder и SupplyOrderItem. Реализована логика создания заказов поставок с соответствующими полями и статусами. Обновлены компоненты интерфейса для улучшения навигации и взаимодействия с новыми функциями.

This commit is contained in:
Veronika Smirnova
2025-07-21 12:45:11 +03:00
parent 39c1499f72
commit 96a328b3ac
7 changed files with 2982 additions and 1680 deletions

View File

@ -97,6 +97,8 @@ model Organization {
supplies Supply[] supplies Supply[]
users User[] users User[]
logistics Logistics[] logistics Logistics[]
supplyOrders SupplyOrder[]
partnerSupplyOrders SupplyOrder[] @relation("SupplyOrderPartner")
@@map("organizations") @@map("organizations")
} }
@ -229,6 +231,7 @@ model Product {
organizationId String organizationId String
cartItems CartItem[] cartItems CartItem[]
favorites Favorites[] favorites Favorites[]
supplyOrderItems SupplyOrderItem[]
category Category? @relation(fields: [categoryId], references: [id]) category Category? @relation(fields: [categoryId], references: [id])
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
@ -363,6 +366,14 @@ enum ScheduleStatus {
ABSENT ABSENT
} }
enum SupplyOrderStatus {
PENDING
CONFIRMED
IN_TRANSIT
DELIVERED
CANCELLED
}
model Logistics { model Logistics {
id String @id @default(cuid()) id String @id @default(cuid())
fromLocation String fromLocation String
@ -377,3 +388,36 @@ model Logistics {
@@map("logistics") @@map("logistics")
} }
model SupplyOrder {
id String @id @default(cuid())
partnerId String
deliveryDate DateTime
status SupplyOrderStatus @default(PENDING)
totalAmount Decimal @db.Decimal(12, 2)
totalItems Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organizationId String
items SupplyOrderItem[]
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
partner Organization @relation("SupplyOrderPartner", fields: [partnerId], references: [id])
@@map("supply_orders")
}
model SupplyOrderItem {
id String @id @default(cuid())
supplyOrderId String
productId String
quantity Int
price Decimal @db.Decimal(12, 2)
totalPrice Decimal @db.Decimal(12, 2)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
supplyOrder SupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id])
@@unique([supplyOrderId, productId])
@@map("supply_order_items")
}

View File

@ -0,0 +1,10 @@
import { AuthGuard } from "@/components/auth-guard";
import { MaterialsOrderForm } from "@/components/fulfillment-supplies/materials-supplies/materials-order-form";
export default function MaterialsOrderPage() {
return (
<AuthGuard>
<MaterialsOrderForm />
</AuthGuard>
);
}

View File

@ -0,0 +1,610 @@
"use client";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { useQuery, useMutation } from "@apollo/client";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar";
import {
ArrowLeft,
Building2,
MapPin,
Phone,
Mail,
Star,
Search,
Calendar,
Package,
Plus,
Minus,
ShoppingCart,
} from "lucide-react";
import { GET_MY_COUNTERPARTIES, GET_ALL_PRODUCTS } from "@/graphql/queries";
import { CREATE_SUPPLY_ORDER } from "@/graphql/mutations";
import { OrganizationAvatar } from "@/components/market/organization-avatar";
import { toast } from "sonner";
import Image from "next/image";
interface Partner {
id: string;
inn: string;
name?: string;
fullName?: string;
type: "FULFILLMENT" | "SELLER" | "LOGIST" | "WHOLESALE";
address?: string;
phones?: Array<{ value: string }>;
emails?: Array<{ value: string }>;
users?: Array<{ id: string; avatar?: string; managerName?: string }>;
createdAt: string;
}
interface Product {
id: string;
name: string;
article: string;
description?: string;
price: number;
quantity: number;
category?: { id: string; name: string };
brand?: string;
color?: string;
size?: string;
weight?: number;
dimensions?: string;
material?: string;
images: string[];
mainImage?: string;
isActive: boolean;
organization: {
id: string;
inn: string;
name?: string;
fullName?: string;
};
}
interface SelectedProduct extends Product {
selectedQuantity: number;
}
export function MaterialsOrderForm() {
const router = useRouter();
const { getSidebarMargin } = useSidebar();
const [selectedPartner, setSelectedPartner] = useState<Partner | null>(null);
const [selectedProducts, setSelectedProducts] = useState<SelectedProduct[]>(
[]
);
const [searchQuery, setSearchQuery] = useState("");
const [deliveryDate, setDeliveryDate] = useState("");
// Загружаем контрагентов-оптовиков
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(
GET_MY_COUNTERPARTIES
);
// Загружаем товары для выбранного партнера
const { data: productsData, loading: productsLoading } = useQuery(
GET_ALL_PRODUCTS,
{
skip: !selectedPartner,
variables: { search: null, category: null },
}
);
// Мутация для создания заказа поставки
const [createSupplyOrder, { loading: isCreatingOrder }] = useMutation(CREATE_SUPPLY_ORDER);
// Фильтруем только оптовиков из партнеров
const wholesalePartners = (counterpartiesData?.myCounterparties || []).filter(
(org: Partner) => org.type === "WHOLESALE"
);
// Фильтруем партнеров по поисковому запросу
const filteredPartners = wholesalePartners.filter(
(partner: Partner) =>
partner.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
partner.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
partner.inn?.toLowerCase().includes(searchQuery.toLowerCase())
);
// Фильтруем товары по выбранному партнеру
const partnerProducts = selectedPartner
? (productsData?.allProducts || []).filter(
(product: Product) => product.organization.id === selectedPartner.id
)
: [];
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB",
minimumFractionDigits: 0,
}).format(amount);
};
const updateProductQuantity = (productId: string, quantity: number) => {
const product = partnerProducts.find((p: Product) => p.id === productId);
if (!product) return;
setSelectedProducts((prev) => {
const existing = prev.find((p) => p.id === productId);
if (quantity === 0) {
return prev.filter((p) => p.id !== productId);
}
if (existing) {
return prev.map((p) =>
p.id === productId ? { ...p, selectedQuantity: quantity } : p
);
} else {
return [...prev, { ...product, selectedQuantity: quantity }];
}
});
};
const getSelectedQuantity = (productId: string): number => {
const selected = selectedProducts.find((p) => p.id === productId);
return selected ? selected.selectedQuantity : 0;
};
const getTotalAmount = () => {
return selectedProducts.reduce(
(sum, product) => sum + product.price * product.selectedQuantity,
0
);
};
const getTotalItems = () => {
return selectedProducts.reduce(
(sum, product) => sum + product.selectedQuantity,
0
);
};
const handleCreateOrder = async () => {
if (!selectedPartner || selectedProducts.length === 0 || !deliveryDate) {
toast.error("Заполните все обязательные поля");
return;
}
try {
const result = await createSupplyOrder({
variables: {
input: {
partnerId: selectedPartner.id,
deliveryDate: deliveryDate,
items: selectedProducts.map(product => ({
productId: product.id,
quantity: product.selectedQuantity
}))
}
}
});
if (result.data?.createSupplyOrder?.success) {
toast.success("Заказ поставки создан успешно!");
router.push("/fulfillment-supplies");
} else {
toast.error(result.data?.createSupplyOrder?.message || "Ошибка при создании заказа");
}
} catch (error) {
console.error("Error creating supply order:", error);
toast.error("Ошибка при создании заказа поставки");
}
};
const renderStars = (rating: number = 4.5) => {
return Array.from({ length: 5 }, (_, i) => (
<Star
key={i}
className={`h-3 w-3 ${
i < Math.floor(rating)
? "text-yellow-400 fill-current"
: "text-gray-400"
}`}
/>
));
};
// Если выбран партнер и есть товары, показываем товары
if (selectedPartner && partnerProducts.length > 0) {
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}
>
<div className="h-full w-full flex flex-col">
{/* Заголовок */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedPartner(null)}
className="text-white/60 hover:text-white hover:bg-white/10"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Назад к партнерам
</Button>
<div>
<h1 className="text-2xl font-bold text-white">
Товары партнера
</h1>
<p className="text-white/60">
{selectedPartner.name || selectedPartner.fullName}
</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => router.push("/fulfillment-supplies")}
className="text-white/60 hover:text-white hover:bg-white/10"
>
Отмена
</Button>
</div>
<div className="flex-1 overflow-hidden grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Список товаров */}
<div className="lg:col-span-2 overflow-hidden">
<Card className="glass-card h-full overflow-hidden">
<div className="p-4 h-full flex flex-col">
<h3 className="text-lg font-semibold text-white mb-4">
Доступные товары
</h3>
<div className="flex-1 overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{partnerProducts.map((product: Product) => {
const selectedQuantity = getSelectedQuantity(
product.id
);
return (
<Card
key={product.id}
className="glass-secondary p-4"
>
<div className="space-y-3">
{/* Изображение товара */}
{product.mainImage && (
<div className="relative h-32 w-full bg-white/5 rounded overflow-hidden">
<Image
src={product.mainImage}
alt={product.name}
fill
className="object-cover"
/>
</div>
)}
{/* Информация о товаре */}
<div>
<h4 className="text-white font-medium text-sm">
{product.name}
</h4>
<p className="text-white/60 text-xs">
Артикул: {product.article}
</p>
{product.description && (
<p className="text-white/60 text-xs mt-1 line-clamp-2">
{product.description}
</p>
)}
</div>
{/* Цена и наличие */}
<div className="flex items-center justify-between">
<div>
<div className="text-white font-bold">
{formatCurrency(product.price)}
</div>
<div className="text-white/60 text-xs">
В наличии: {product.quantity}
</div>
</div>
</div>
{/* Выбор количества */}
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={() =>
updateProductQuantity(
product.id,
Math.max(0, selectedQuantity - 1)
)
}
disabled={selectedQuantity === 0}
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10"
>
<Minus className="h-4 w-4" />
</Button>
<Input
type="number"
value={selectedQuantity}
onChange={(e) => {
const value = Math.max(
0,
Math.min(
product.quantity,
parseInt(e.target.value) || 0
)
);
updateProductQuantity(product.id, value);
}}
className="h-8 w-16 text-center bg-white/10 border-white/20 text-white"
min={0}
max={product.quantity}
/>
<Button
variant="ghost"
size="sm"
onClick={() =>
updateProductQuantity(
product.id,
Math.min(
product.quantity,
selectedQuantity + 1
)
)
}
disabled={
selectedQuantity >= product.quantity
}
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10"
>
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
</Card>
);
})}
</div>
</div>
</div>
</Card>
</div>
{/* Сводка заказа */}
<div className="overflow-hidden">
<Card className="glass-card h-full overflow-hidden">
<div className="p-4 h-full flex flex-col">
<h3 className="text-lg font-semibold text-white mb-4">
Сводка заказа
</h3>
{/* Дата поставки */}
<div className="mb-4">
<label className="block text-white/80 text-sm mb-2">
<Calendar className="h-4 w-4 inline mr-2" />
Дата поставки
</label>
<Input
type="date"
value={deliveryDate}
onChange={(e) => setDeliveryDate(e.target.value)}
className="bg-white/10 border-white/20 text-white"
required
/>
</div>
{/* Выбранные товары */}
<div className="flex-1 overflow-y-auto mb-4">
{selectedProducts.length === 0 ? (
<div className="text-center py-8">
<Package className="h-12 w-12 text-white/20 mx-auto mb-2" />
<p className="text-white/60 text-sm">
Товары не выбраны
</p>
</div>
) : (
<div className="space-y-2">
{selectedProducts.map((product) => (
<Card
key={product.id}
className="glass-secondary p-3"
>
<div className="space-y-1">
<div className="text-white text-sm font-medium">
{product.name}
</div>
<div className="flex justify-between text-xs text-white/60">
<span>
{product.selectedQuantity} шт ×{" "}
{formatCurrency(product.price)}
</span>
<span className="text-white font-medium">
{formatCurrency(
product.price * product.selectedQuantity
)}
</span>
</div>
</div>
</Card>
))}
</div>
)}
</div>
{/* Итого */}
<div className="border-t border-white/20 pt-4 space-y-2">
<div className="flex justify-between text-white/80">
<span>Товаров:</span>
<span>{getTotalItems()} шт</span>
</div>
<div className="flex justify-between text-white font-bold text-lg">
<span>Итого:</span>
<span>{formatCurrency(getTotalAmount())}</span>
</div>
</div>
{/* Кнопка создания заказа */}
<Button
onClick={handleCreateOrder}
disabled={selectedProducts.length === 0 || !deliveryDate || isCreatingOrder}
className="w-full mt-4 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white"
>
<ShoppingCart className="h-4 w-4 mr-2" />
{isCreatingOrder ? "Создание заказа..." : "Создать заказ поставки"}
</Button>
</div>
</Card>
</div>
</div>
</div>
</main>
</div>
);
}
// Основная форма выбора партнера
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}
>
<div className="h-full w-full flex flex-col">
{/* Заголовок */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<Button
variant="ghost"
size="sm"
onClick={() => router.push("/fulfillment-supplies")}
className="text-white/60 hover:text-white hover:bg-white/10"
>
<ArrowLeft className="h-4 w-4 mr-2" />К поставкам
</Button>
<div>
<h1 className="text-2xl font-bold text-white">
Заказ расходников
</h1>
<p className="text-white/60">
Выберите партнера-оптовика для заказа расходников
</p>
</div>
</div>
</div>
{/* Поиск */}
<div className="mb-6">
<div className="relative max-w-md">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-white/40" />
<Input
placeholder="Поиск партнеров..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-white/10 border-white/20 text-white placeholder:text-white/40"
/>
</div>
</div>
{/* Список партнеров */}
<Card className="glass-card flex-1 overflow-hidden">
<div className="p-6 h-full flex flex-col">
{counterpartiesLoading ? (
<div className="flex-1 flex items-center justify-center">
<div className="text-white/60">Загрузка партнеров...</div>
</div>
) : filteredPartners.length === 0 ? (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<Building2 className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">
{wholesalePartners.length === 0
? "У вас пока нет партнеров-оптовиков"
: "Партнеры не найдены"}
</p>
<p className="text-white/40 text-sm mt-2">
Добавьте партнеров в разделе "Партнеры"
</p>
</div>
</div>
) : (
<div className="flex-1 overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredPartners.map((partner: Partner) => (
<Card
key={partner.id}
className="glass-secondary p-4 cursor-pointer transition-all hover:bg-white/15 hover:border-white/30 hover:scale-105"
onClick={() => setSelectedPartner(partner)}
>
<div className="space-y-3">
{/* Заголовок карточки */}
<div className="flex items-start space-x-3">
<OrganizationAvatar
organization={partner}
size="sm"
/>
<div className="flex-1 min-w-0">
<h3 className="text-white font-semibold text-sm mb-1 truncate">
{partner.name || partner.fullName}
</h3>
<div className="flex items-center space-x-1 mb-2">
{renderStars()}
<span className="text-white/60 text-xs ml-1">
4.5
</span>
</div>
</div>
</div>
{/* Информация */}
<div className="space-y-1">
{partner.address && (
<div className="flex items-center space-x-2">
<MapPin className="h-3 w-3 text-gray-400" />
<span className="text-white/80 text-xs truncate">
{partner.address}
</span>
</div>
)}
{partner.phones && partner.phones.length > 0 && (
<div className="flex items-center space-x-2">
<Phone className="h-3 w-3 text-gray-400" />
<span className="text-white/80 text-xs">
{partner.phones[0].value}
</span>
</div>
)}
{partner.emails && partner.emails.length > 0 && (
<div className="flex items-center space-x-2">
<Mail className="h-3 w-3 text-gray-400" />
<span className="text-white/80 text-xs truncate">
{partner.emails[0].value}
</span>
</div>
)}
</div>
{/* ИНН */}
<div className="pt-2 border-t border-white/10">
<p className="text-white/60 text-xs">
ИНН: {partner.inn}
</p>
</div>
</div>
</Card>
))}
</div>
</div>
)}
</div>
</Card>
</div>
</main>
</div>
);
}

View File

@ -175,6 +175,7 @@ export function MaterialsSuppliesTab() {
</div> </div>
<Button <Button
size="sm" size="sm"
onClick={() => window.location.href = '/fulfillment-supplies/materials/order'}
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white text-xs" className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white text-xs"
> >
<Plus className="h-3 w-3 mr-1" /> <Plus className="h-3 w-3 mr-1" />

View File

@ -634,6 +634,54 @@ export const DELETE_SUPPLY = gql`
} }
` `
// Мутация для заказа поставки расходников
export const CREATE_SUPPLY_ORDER = gql`
mutation CreateSupplyOrder($input: SupplyOrderInput!) {
createSupplyOrder(input: $input) {
success
message
order {
id
partnerId
deliveryDate
status
totalAmount
totalItems
createdAt
partner {
id
inn
name
fullName
address
phones
emails
}
items {
id
quantity
price
totalPrice
product {
id
name
article
description
price
quantity
images
mainImage
category {
id
name
}
}
}
}
}
}
`
// Мутации для логистики // Мутации для логистики
export const CREATE_LOGISTICS = gql` export const CREATE_LOGISTICS = gql`
mutation CreateLogistics($input: LogisticsInput!) { mutation CreateLogistics($input: LogisticsInput!) {

View File

@ -1,190 +1,198 @@
import jwt from 'jsonwebtoken' import jwt from "jsonwebtoken";
import bcrypt from 'bcryptjs' import bcrypt from "bcryptjs";
import { GraphQLError } from 'graphql' import { GraphQLError } from "graphql";
import { GraphQLScalarType, Kind } from 'graphql' import { GraphQLScalarType, Kind } from "graphql";
import { prisma } from '@/lib/prisma' import { prisma } from "@/lib/prisma";
import { SmsService } from '@/services/sms-service' import { SmsService } from "@/services/sms-service";
import { DaDataService } from '@/services/dadata-service' import { DaDataService } from "@/services/dadata-service";
import { MarketplaceService } from '@/services/marketplace-service' import { MarketplaceService } from "@/services/marketplace-service";
import { Prisma } from '@prisma/client' import { Prisma } from "@prisma/client";
// Сервисы // Сервисы
const smsService = new SmsService() const smsService = new SmsService();
const dadataService = new DaDataService() const dadataService = new DaDataService();
const marketplaceService = new MarketplaceService() const marketplaceService = new MarketplaceService();
// Интерфейсы для типизации // Интерфейсы для типизации
interface Context { interface Context {
user?: { user?: {
id: string id: string;
phone: string phone: string;
} };
admin?: { admin?: {
id: string id: string;
username: string username: string;
} };
} }
interface CreateEmployeeInput { interface CreateEmployeeInput {
firstName: string firstName: string;
lastName: string lastName: string;
middleName?: string middleName?: string;
birthDate?: string birthDate?: string;
avatar?: string avatar?: string;
passportPhoto?: string passportPhoto?: string;
passportSeries?: string passportSeries?: string;
passportNumber?: string passportNumber?: string;
passportIssued?: string passportIssued?: string;
passportDate?: string passportDate?: string;
address?: string address?: string;
position: string position: string;
department?: string department?: string;
hireDate: string hireDate: string;
salary?: number salary?: number;
phone: string phone: string;
email?: string email?: string;
telegram?: string telegram?: string;
whatsapp?: string whatsapp?: string;
emergencyContact?: string emergencyContact?: string;
emergencyPhone?: string emergencyPhone?: string;
} }
interface UpdateEmployeeInput { interface UpdateEmployeeInput {
firstName?: string firstName?: string;
lastName?: string lastName?: string;
middleName?: string middleName?: string;
birthDate?: string birthDate?: string;
avatar?: string avatar?: string;
passportPhoto?: string passportPhoto?: string;
passportSeries?: string passportSeries?: string;
passportNumber?: string passportNumber?: string;
passportIssued?: string passportIssued?: string;
passportDate?: string passportDate?: string;
address?: string address?: string;
position?: string position?: string;
department?: string department?: string;
hireDate?: string hireDate?: string;
salary?: number salary?: number;
status?: 'ACTIVE' | 'VACATION' | 'SICK' | 'FIRED' status?: "ACTIVE" | "VACATION" | "SICK" | "FIRED";
phone?: string phone?: string;
email?: string email?: string;
telegram?: string telegram?: string;
whatsapp?: string whatsapp?: string;
emergencyContact?: string emergencyContact?: string;
emergencyPhone?: string emergencyPhone?: string;
} }
interface UpdateScheduleInput { interface UpdateScheduleInput {
employeeId: string employeeId: string;
date: string date: string;
status: 'WORK' | 'WEEKEND' | 'VACATION' | 'SICK' | 'ABSENT' status: "WORK" | "WEEKEND" | "VACATION" | "SICK" | "ABSENT";
hoursWorked?: number hoursWorked?: number;
notes?: string notes?: string;
} }
interface AuthTokenPayload { interface AuthTokenPayload {
userId: string userId: string;
phone: string phone: string;
} }
// JWT утилиты // JWT утилиты
const generateToken = (payload: AuthTokenPayload): string => { const generateToken = (payload: AuthTokenPayload): string => {
return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '30d' }) return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: "30d" });
} };
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const verifyToken = (token: string): AuthTokenPayload => { const verifyToken = (token: string): AuthTokenPayload => {
try { try {
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload;
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) { } catch (error) {
throw new GraphQLError('Недействительный токен', { throw new GraphQLError("Недействительный токен", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
} };
// Скалярный тип для JSON // Скалярный тип для JSON
const JSONScalar = new GraphQLScalarType({ const JSONScalar = new GraphQLScalarType({
name: 'JSON', name: "JSON",
description: 'JSON custom scalar type', description: "JSON custom scalar type",
serialize(value: unknown) { serialize(value: unknown) {
return value // значение отправляется клиенту return value; // значение отправляется клиенту
}, },
parseValue(value: unknown) { parseValue(value: unknown) {
return value // значение получено от клиента return value; // значение получено от клиента
}, },
parseLiteral(ast) { parseLiteral(ast) {
switch (ast.kind) { switch (ast.kind) {
case Kind.STRING: case Kind.STRING:
case Kind.BOOLEAN: case Kind.BOOLEAN:
return ast.value return ast.value;
case Kind.INT: case Kind.INT:
case Kind.FLOAT: case Kind.FLOAT:
return parseFloat(ast.value) return parseFloat(ast.value);
case Kind.OBJECT: { case Kind.OBJECT: {
const value = Object.create(null) const value = Object.create(null);
ast.fields.forEach(field => { ast.fields.forEach((field) => {
value[field.name.value] = parseLiteral(field.value) value[field.name.value] = parseLiteral(field.value);
}) });
return value return value;
} }
case Kind.LIST: case Kind.LIST:
return ast.values.map(parseLiteral) return ast.values.map(parseLiteral);
default: default:
return null return null;
} }
} },
}) });
// Скалярный тип для DateTime // Скалярный тип для DateTime
const DateTimeScalar = new GraphQLScalarType({ const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime', name: "DateTime",
description: 'DateTime custom scalar type', description: "DateTime custom scalar type",
serialize(value: unknown) { serialize(value: unknown) {
if (value instanceof Date) { if (value instanceof Date) {
return value.toISOString() // значение отправляется клиенту как ISO строка return value.toISOString(); // значение отправляется клиенту как ISO строка
} }
return value return value;
}, },
parseValue(value: unknown) { parseValue(value: unknown) {
if (typeof value === 'string') { if (typeof value === "string") {
return new Date(value) // значение получено от клиента, парсим как дату return new Date(value); // значение получено от клиента, парсим как дату
} }
return value return value;
}, },
parseLiteral(ast) { parseLiteral(ast) {
if (ast.kind === Kind.STRING) { if (ast.kind === Kind.STRING) {
return new Date(ast.value) // AST значение как дата return new Date(ast.value); // AST значение как дата
} }
return null return null;
} },
}) });
function parseLiteral(ast: unknown): unknown { function parseLiteral(ast: unknown): unknown {
const astNode = ast as { kind: string; value?: unknown; fields?: unknown[]; values?: unknown[] } const astNode = ast as {
kind: string;
value?: unknown;
fields?: unknown[];
values?: unknown[];
};
switch (astNode.kind) { switch (astNode.kind) {
case Kind.STRING: case Kind.STRING:
case Kind.BOOLEAN: case Kind.BOOLEAN:
return astNode.value return astNode.value;
case Kind.INT: case Kind.INT:
case Kind.FLOAT: case Kind.FLOAT:
return parseFloat(astNode.value as string) return parseFloat(astNode.value as string);
case Kind.OBJECT: { case Kind.OBJECT: {
const value = Object.create(null) const value = Object.create(null);
if (astNode.fields) { if (astNode.fields) {
astNode.fields.forEach((field: unknown) => { astNode.fields.forEach((field: unknown) => {
const fieldNode = field as { name: { value: string }; value: unknown } const fieldNode = field as {
value[fieldNode.name.value] = parseLiteral(fieldNode.value) name: { value: string };
}) value: unknown;
};
value[fieldNode.name.value] = parseLiteral(fieldNode.value);
});
} }
return value return value;
} }
case Kind.LIST: case Kind.LIST:
return (ast as { values: unknown[] }).values.map(parseLiteral) return (ast as { values: unknown[] }).values.map(parseLiteral);
default: default:
return null return null;
} }
} }
@ -195,9 +203,9 @@ export const resolvers = {
Query: { Query: {
me: async (_: unknown, __: unknown, context: Context) => { me: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
return await prisma.user.findUnique({ return await prisma.user.findUnique({
@ -205,142 +213,154 @@ export const resolvers = {
include: { include: {
organization: { organization: {
include: { include: {
apiKeys: true apiKeys: true,
} },
} },
} },
}) });
}, },
organization: async (_: unknown, args: { id: string }, context: Context) => { organization: async (
_: unknown,
args: { id: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const organization = await prisma.organization.findUnique({ const organization = await prisma.organization.findUnique({
where: { id: args.id }, where: { id: args.id },
include: { include: {
apiKeys: true, apiKeys: true,
users: true users: true,
} },
}) });
if (!organization) { if (!organization) {
throw new GraphQLError('Организация не найдена') throw new GraphQLError("Организация не найдена");
} }
// Проверяем, что пользователь имеет доступ к этой организации // Проверяем, что пользователь имеет доступ к этой организации
const hasAccess = organization.users.some(user => user.id === context.user!.id) const hasAccess = organization.users.some(
(user) => user.id === context.user!.id
);
if (!hasAccess) { if (!hasAccess) {
throw new GraphQLError('Нет доступа к этой организации', { throw new GraphQLError("Нет доступа к этой организации", {
extensions: { code: 'FORBIDDEN' } extensions: { code: "FORBIDDEN" },
}) });
} }
return organization return organization;
}, },
// Поиск организаций по типу для добавления в контрагенты // Поиск организаций по типу для добавления в контрагенты
searchOrganizations: async (_: unknown, args: { type?: string; search?: string }, context: Context) => { searchOrganizations: async (
_: unknown,
args: { type?: string; search?: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
// Получаем текущую организацию пользователя // Получаем текущую организацию пользователя
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Получаем уже существующих контрагентов для добавления флага // Получаем уже существующих контрагентов для добавления флага
const existingCounterparties = await prisma.counterparty.findMany({ const existingCounterparties = await prisma.counterparty.findMany({
where: { organizationId: currentUser.organization.id }, where: { organizationId: currentUser.organization.id },
select: { counterpartyId: true } select: { counterpartyId: true },
}) });
const existingCounterpartyIds = existingCounterparties.map(c => c.counterpartyId) const existingCounterpartyIds = existingCounterparties.map(
(c) => c.counterpartyId
);
// Получаем исходящие заявки для добавления флага hasOutgoingRequest // Получаем исходящие заявки для добавления флага hasOutgoingRequest
const outgoingRequests = await prisma.counterpartyRequest.findMany({ const outgoingRequests = await prisma.counterpartyRequest.findMany({
where: { where: {
senderId: currentUser.organization.id, senderId: currentUser.organization.id,
status: 'PENDING' status: "PENDING",
}, },
select: { receiverId: true } select: { receiverId: true },
}) });
const outgoingRequestIds = outgoingRequests.map(r => r.receiverId) const outgoingRequestIds = outgoingRequests.map((r) => r.receiverId);
// Получаем входящие заявки для добавления флага hasIncomingRequest // Получаем входящие заявки для добавления флага hasIncomingRequest
const incomingRequests = await prisma.counterpartyRequest.findMany({ const incomingRequests = await prisma.counterpartyRequest.findMany({
where: { where: {
receiverId: currentUser.organization.id, receiverId: currentUser.organization.id,
status: 'PENDING' status: "PENDING",
}, },
select: { senderId: true } select: { senderId: true },
}) });
const incomingRequestIds = incomingRequests.map(r => r.senderId) const incomingRequestIds = incomingRequests.map((r) => r.senderId);
const where: Record<string, unknown> = { const where: Record<string, unknown> = {
// Больше не исключаем собственную организацию // Больше не исключаем собственную организацию
} };
if (args.type) { if (args.type) {
where.type = args.type where.type = args.type;
} }
if (args.search) { if (args.search) {
where.OR = [ where.OR = [
{ name: { contains: args.search, mode: 'insensitive' } }, { name: { contains: args.search, mode: "insensitive" } },
{ fullName: { contains: args.search, mode: 'insensitive' } }, { fullName: { contains: args.search, mode: "insensitive" } },
{ inn: { contains: args.search } } { inn: { contains: args.search } },
] ];
} }
const organizations = await prisma.organization.findMany({ const organizations = await prisma.organization.findMany({
where, where,
take: 50, // Ограничиваем количество результатов take: 50, // Ограничиваем количество результатов
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: "desc" },
include: { include: {
users: true, users: true,
apiKeys: true apiKeys: true,
} },
}) });
// Добавляем флаги isCounterparty, isCurrentUser, hasOutgoingRequest и hasIncomingRequest к каждой организации // Добавляем флаги isCounterparty, isCurrentUser, hasOutgoingRequest и hasIncomingRequest к каждой организации
return organizations.map(org => ({ return organizations.map((org) => ({
...org, ...org,
isCounterparty: existingCounterpartyIds.includes(org.id), isCounterparty: existingCounterpartyIds.includes(org.id),
isCurrentUser: org.id === currentUser.organization?.id, isCurrentUser: org.id === currentUser.organization?.id,
hasOutgoingRequest: outgoingRequestIds.includes(org.id), hasOutgoingRequest: outgoingRequestIds.includes(org.id),
hasIncomingRequest: incomingRequestIds.includes(org.id) hasIncomingRequest: incomingRequestIds.includes(org.id),
})) }));
}, },
// Мои контрагенты // Мои контрагенты
myCounterparties: async (_: unknown, __: unknown, context: Context) => { myCounterparties: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
const counterparties = await prisma.counterparty.findMany({ const counterparties = await prisma.counterparty.findMany({
@ -349,306 +369,314 @@ export const resolvers = {
counterparty: { counterparty: {
include: { include: {
users: true, users: true,
apiKeys: true apiKeys: true,
} },
} },
} },
}) });
return counterparties.map(c => c.counterparty) return counterparties.map((c) => c.counterparty);
}, },
// Входящие заявки // Входящие заявки
incomingRequests: async (_: unknown, __: unknown, context: Context) => { incomingRequests: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
return await prisma.counterpartyRequest.findMany({ return await prisma.counterpartyRequest.findMany({
where: { where: {
receiverId: currentUser.organization.id, receiverId: currentUser.organization.id,
status: 'PENDING' status: "PENDING",
}, },
include: { include: {
sender: { sender: {
include: { include: {
users: true, users: true,
apiKeys: true apiKeys: true,
} },
}, },
receiver: { receiver: {
include: { include: {
users: true, users: true,
apiKeys: true apiKeys: true,
} },
} },
}, },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: "desc" },
}) });
}, },
// Исходящие заявки // Исходящие заявки
outgoingRequests: async (_: unknown, __: unknown, context: Context) => { outgoingRequests: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
return await prisma.counterpartyRequest.findMany({ return await prisma.counterpartyRequest.findMany({
where: { where: {
senderId: currentUser.organization.id, senderId: currentUser.organization.id,
status: { in: ['PENDING', 'REJECTED'] } status: { in: ["PENDING", "REJECTED"] },
}, },
include: { include: {
sender: { sender: {
include: { include: {
users: true, users: true,
apiKeys: true apiKeys: true,
} },
}, },
receiver: { receiver: {
include: { include: {
users: true, users: true,
apiKeys: true apiKeys: true,
} },
} },
}, },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: "desc" },
}) });
}, },
// Сообщения с контрагентом // Сообщения с контрагентом
messages: async (_: unknown, args: { counterpartyId: string; limit?: number; offset?: number }, context: Context) => { messages: async (
_: unknown,
args: { counterpartyId: string; limit?: number; offset?: number },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
const limit = args.limit || 50 const limit = args.limit || 50;
const offset = args.offset || 0 const offset = args.offset || 0;
const messages = await prisma.message.findMany({ const messages = await prisma.message.findMany({
where: { where: {
OR: [ OR: [
{ {
senderOrganizationId: currentUser.organization.id, senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.counterpartyId receiverOrganizationId: args.counterpartyId,
}, },
{ {
senderOrganizationId: args.counterpartyId, senderOrganizationId: args.counterpartyId,
receiverOrganizationId: currentUser.organization.id receiverOrganizationId: currentUser.organization.id,
} },
] ],
}, },
include: { include: {
sender: true, sender: true,
senderOrganization: { senderOrganization: {
include: { include: {
users: true users: true,
} },
}, },
receiverOrganization: { receiverOrganization: {
include: { include: {
users: true users: true,
} },
} },
}, },
orderBy: { createdAt: 'asc' }, orderBy: { createdAt: "asc" },
take: limit, take: limit,
skip: offset skip: offset,
}) });
return messages return messages;
}, },
// Список чатов (последние сообщения с каждым контрагентом) // Список чатов (последние сообщения с каждым контрагентом)
conversations: async (_: unknown, __: unknown, context: Context) => { conversations: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// TODO: Здесь будет логика получения списка чатов // TODO: Здесь будет логика получения списка чатов
// Пока возвращаем пустой массив, так как таблица сообщений еще не создана // Пока возвращаем пустой массив, так как таблица сообщений еще не создана
return [] return [];
}, },
// Мои услуги // Мои услуги
myServices: async (_: unknown, __: unknown, context: Context) => { myServices: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что это фулфилмент центр // Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') { if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError('Услуги доступны только для фулфилмент центров') throw new GraphQLError("Услуги доступны только для фулфилмент центров");
} }
return await prisma.service.findMany({ return await prisma.service.findMany({
where: { organizationId: currentUser.organization.id }, where: { organizationId: currentUser.organization.id },
include: { organization: true }, include: { organization: true },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: "desc" },
}) });
}, },
// Мои расходники // Мои расходники
mySupplies: async (_: unknown, __: unknown, context: Context) => { mySupplies: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
return await prisma.supply.findMany({ return await prisma.supply.findMany({
where: { organizationId: currentUser.organization.id }, where: { organizationId: currentUser.organization.id },
include: { organization: true }, include: { organization: true },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: "desc" },
}) });
}, },
// Логистика организации // Логистика организации
myLogistics: async (_: unknown, __: unknown, context: Context) => { myLogistics: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
return await prisma.logistics.findMany({ return await prisma.logistics.findMany({
where: { organizationId: currentUser.organization.id }, where: { organizationId: currentUser.organization.id },
include: { organization: true }, include: { organization: true },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: "desc" },
}) });
}, },
// Мои товары (для оптовиков) // Мои товары (для оптовиков)
myProducts: async (_: unknown, __: unknown, context: Context) => { myProducts: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что это оптовик // Проверяем, что это оптовик
if (currentUser.organization.type !== 'WHOLESALE') { if (currentUser.organization.type !== "WHOLESALE") {
throw new GraphQLError('Товары доступны только для оптовиков') throw new GraphQLError("Товары доступны только для оптовиков");
} }
return await prisma.product.findMany({ return await prisma.product.findMany({
where: { organizationId: currentUser.organization.id }, where: { organizationId: currentUser.organization.id },
include: { include: {
category: true, category: true,
organization: true organization: true,
}, },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: "desc" },
}) });
}, },
// Все товары всех оптовиков для маркета // Все товары всех оптовиков для маркета
allProducts: async (_: unknown, args: { search?: string; category?: string }, context: Context) => { allProducts: async (
_: unknown,
args: { search?: string; category?: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const where: Record<string, unknown> = { const where: Record<string, unknown> = {
isActive: true, // Показываем только активные товары isActive: true, // Показываем только активные товары
organization: { organization: {
type: 'WHOLESALE' // Только товары оптовиков type: "WHOLESALE", // Только товары оптовиков
} },
} };
if (args.search) { if (args.search) {
where.OR = [ where.OR = [
{ name: { contains: args.search, mode: 'insensitive' } }, { name: { contains: args.search, mode: "insensitive" } },
{ article: { contains: args.search, mode: 'insensitive' } }, { article: { contains: args.search, mode: "insensitive" } },
{ description: { contains: args.search, mode: 'insensitive' } }, { description: { contains: args.search, mode: "insensitive" } },
{ brand: { contains: args.search, mode: 'insensitive' } } { brand: { contains: args.search, mode: "insensitive" } },
] ];
} }
if (args.category) { if (args.category) {
where.categoryId = args.category where.categoryId = args.category;
} }
return await prisma.product.findMany({ return await prisma.product.findMany({
@ -657,43 +685,43 @@ export const resolvers = {
category: true, category: true,
organization: { organization: {
include: { include: {
users: true users: true,
} },
} },
}, },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: "desc" },
take: 100 // Ограничиваем количество результатов take: 100, // Ограничиваем количество результатов
}) });
}, },
// Все категории // Все категории
categories: async (_: unknown, __: unknown, context: Context) => { categories: async (_: unknown, __: unknown, context: Context) => {
if (!context.user && !context.admin) { if (!context.user && !context.admin) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
return await prisma.category.findMany({ return await prisma.category.findMany({
orderBy: { name: 'asc' } orderBy: { name: "asc" },
}) });
}, },
// Корзина пользователя // Корзина пользователя
myCart: async (_: unknown, __: unknown, context: Context) => { myCart: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Найти или создать корзину для организации // Найти или создать корзину для организации
@ -707,21 +735,21 @@ export const resolvers = {
category: true, category: true,
organization: { organization: {
include: { include: {
users: true users: true,
} },
} },
} },
} },
} },
}, },
organization: true organization: true,
} },
}) });
if (!cart) { if (!cart) {
cart = await prisma.cart.create({ cart = await prisma.cart.create({
data: { data: {
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
}, },
include: { include: {
items: { items: {
@ -731,36 +759,36 @@ export const resolvers = {
category: true, category: true,
organization: { organization: {
include: { include: {
users: true users: true,
} },
} },
} },
} },
} },
}, },
organization: true organization: true,
} },
}) });
} }
return cart return cart;
}, },
// Избранные товары пользователя // Избранные товары пользователя
myFavorites: async (_: unknown, __: unknown, context: Context) => { myFavorites: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Получаем избранные товары // Получаем избранные товары
@ -772,210 +800,228 @@ export const resolvers = {
category: true, category: true,
organization: { organization: {
include: { include: {
users: true users: true,
} },
} },
} },
} },
}, },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: "desc" },
}) });
return favorites.map(favorite => favorite.product) return favorites.map((favorite) => favorite.product);
}, },
// Сотрудники организации // Сотрудники организации
myEmployees: async (_: unknown, __: unknown, context: Context) => { myEmployees: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
if (currentUser.organization.type !== 'FULFILLMENT') { if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError('Доступно только для фулфилмент центров') throw new GraphQLError("Доступно только для фулфилмент центров");
} }
const employees = await prisma.employee.findMany({ const employees = await prisma.employee.findMany({
where: { organizationId: currentUser.organization.id }, where: { organizationId: currentUser.organization.id },
include: { include: {
organization: true organization: true,
}, },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: "desc" },
}) });
return employees return employees;
}, },
// Получение сотрудника по ID // Получение сотрудника по ID
employee: async (_: unknown, args: { id: string }, context: Context) => { employee: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
if (currentUser.organization.type !== 'FULFILLMENT') { if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError('Доступно только для фулфилмент центров') throw new GraphQLError("Доступно только для фулфилмент центров");
} }
const employee = await prisma.employee.findFirst({ const employee = await prisma.employee.findFirst({
where: { where: {
id: args.id, id: args.id,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
}, },
include: { include: {
organization: true organization: true,
} },
}) });
return employee return employee;
}, },
// Получить табель сотрудника за месяц // Получить табель сотрудника за месяц
employeeSchedule: async (_: unknown, args: { employeeId: string; year: number; month: number }, context: Context) => { employeeSchedule: async (
_: unknown,
args: { employeeId: string; year: number; month: number },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
if (currentUser.organization.type !== 'FULFILLMENT') { if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError('Доступно только для фулфилмент центров') throw new GraphQLError("Доступно только для фулфилмент центров");
} }
// Проверяем что сотрудник принадлежит организации // Проверяем что сотрудник принадлежит организации
const employee = await prisma.employee.findFirst({ const employee = await prisma.employee.findFirst({
where: { where: {
id: args.employeeId, id: args.employeeId,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
} },
}) });
if (!employee) { if (!employee) {
throw new GraphQLError('Сотрудник не найден') throw new GraphQLError("Сотрудник не найден");
} }
// Получаем записи табеля за указанный месяц // Получаем записи табеля за указанный месяц
const startDate = new Date(args.year, args.month, 1) const startDate = new Date(args.year, args.month, 1);
const endDate = new Date(args.year, args.month + 1, 0) const endDate = new Date(args.year, args.month + 1, 0);
const scheduleRecords = await prisma.employeeSchedule.findMany({ const scheduleRecords = await prisma.employeeSchedule.findMany({
where: { where: {
employeeId: args.employeeId, employeeId: args.employeeId,
date: { date: {
gte: startDate, gte: startDate,
lte: endDate lte: endDate,
} },
}, },
orderBy: { orderBy: {
date: 'asc' date: "asc",
} },
}) });
return scheduleRecords return scheduleRecords;
} },
}, },
Mutation: { Mutation: {
sendSmsCode: async (_: unknown, args: { phone: string }) => { sendSmsCode: async (_: unknown, args: { phone: string }) => {
const result = await smsService.sendSmsCode(args.phone) const result = await smsService.sendSmsCode(args.phone);
return { return {
success: result.success, success: result.success,
message: result.message || 'SMS код отправлен' message: result.message || "SMS код отправлен",
} };
}, },
verifySmsCode: async (_: unknown, args: { phone: string; code: string }) => { verifySmsCode: async (
const verificationResult = await smsService.verifySmsCode(args.phone, args.code) _: unknown,
args: { phone: string; code: string }
) => {
const verificationResult = await smsService.verifySmsCode(
args.phone,
args.code
);
if (!verificationResult.success) { if (!verificationResult.success) {
return { return {
success: false, success: false,
message: verificationResult.message || 'Неверный код' message: verificationResult.message || "Неверный код",
} };
} }
// Найти или создать пользователя // Найти или создать пользователя
const formattedPhone = args.phone.replace(/\D/g, '') const formattedPhone = args.phone.replace(/\D/g, "");
let user = await prisma.user.findUnique({ let user = await prisma.user.findUnique({
where: { phone: formattedPhone }, where: { phone: formattedPhone },
include: { include: {
organization: { organization: {
include: { include: {
apiKeys: true apiKeys: true,
} },
} },
} },
}) });
if (!user) { if (!user) {
user = await prisma.user.create({ user = await prisma.user.create({
data: { data: {
phone: formattedPhone phone: formattedPhone,
}, },
include: { include: {
organization: { organization: {
include: { include: {
apiKeys: true apiKeys: true,
} },
} },
} },
}) });
} }
const token = generateToken({ const token = generateToken({
userId: user.id, userId: user.id,
phone: user.phone phone: user.phone,
}) });
console.log('verifySmsCode - Generated token:', token ? `${token.substring(0, 20)}...` : 'No token') console.log(
console.log('verifySmsCode - Full token:', token) "verifySmsCode - Generated token:",
console.log('verifySmsCode - User object:', { id: user.id, phone: user.phone }) 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 = { const result = {
success: true, success: true,
message: 'Авторизация успешна', message: "Авторизация успешна",
token, token,
user user,
} };
console.log('verifySmsCode - Returning result:', { console.log("verifySmsCode - Returning result:", {
success: result.success, success: result.success,
hasToken: !!result.token, hasToken: !!result.token,
hasUser: !!result.user, hasUser: !!result.user,
message: result.message, message: result.message,
tokenPreview: result.token ? `${result.token.substring(0, 20)}...` : 'No token in result' tokenPreview: result.token
}) ? `${result.token.substring(0, 20)}...`
: "No token in result",
});
return result return result;
}, },
verifyInn: async (_: unknown, args: { inn: string }) => { verifyInn: async (_: unknown, args: { inn: string }) => {
@ -983,72 +1029,80 @@ export const resolvers = {
if (!dadataService.validateInn(args.inn)) { if (!dadataService.validateInn(args.inn)) {
return { return {
success: false, success: false,
message: 'Неверный формат ИНН' message: "Неверный формат ИНН",
} };
} }
// Получаем данные организации из DaData // Получаем данные организации из DaData
const organizationData = await dadataService.getOrganizationByInn(args.inn) const organizationData = await dadataService.getOrganizationByInn(
args.inn
);
if (!organizationData) { if (!organizationData) {
return { return {
success: false, success: false,
message: 'Организация с указанным ИНН не найдена' message: "Организация с указанным ИНН не найдена",
} };
} }
return { return {
success: true, success: true,
message: 'ИНН найден', message: "ИНН найден",
organization: { organization: {
name: organizationData.name, name: organizationData.name,
fullName: organizationData.fullName, fullName: organizationData.fullName,
address: organizationData.address, address: organizationData.address,
isActive: organizationData.isActive isActive: organizationData.isActive,
} },
} };
}, },
registerFulfillmentOrganization: async ( registerFulfillmentOrganization: async (
_: unknown, _: unknown,
args: { input: { phone: string; inn: string; type: 'FULFILLMENT' | 'LOGIST' | 'WHOLESALE' } }, args: {
input: {
phone: string;
inn: string;
type: "FULFILLMENT" | "LOGIST" | "WHOLESALE";
};
},
context: Context context: Context
) => { ) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const { inn, type } = args.input const { inn, type } = args.input;
// Валидируем ИНН // Валидируем ИНН
if (!dadataService.validateInn(inn)) { if (!dadataService.validateInn(inn)) {
return { return {
success: false, success: false,
message: 'Неверный формат ИНН' message: "Неверный формат ИНН",
} };
} }
// Получаем данные организации из DaData // Получаем данные организации из DaData
const organizationData = await dadataService.getOrganizationByInn(inn) const organizationData = await dadataService.getOrganizationByInn(inn);
if (!organizationData) { if (!organizationData) {
return { return {
success: false, success: false,
message: 'Организация с указанным ИНН не найдена' message: "Организация с указанным ИНН не найдена",
} };
} }
try { try {
// Проверяем, что организация еще не зарегистрирована // Проверяем, что организация еще не зарегистрирована
const existingOrg = await prisma.organization.findUnique({ const existingOrg = await prisma.organization.findUnique({
where: { inn: organizationData.inn } where: { inn: organizationData.inn },
}) });
if (existingOrg) { if (existingOrg) {
return { return {
success: false, success: false,
message: 'Организация с таким ИНН уже зарегистрирована' message: "Организация с таким ИНН уже зарегистрирована",
} };
} }
// Создаем организацию со всеми данными из DaData // Создаем организацию со всеми данными из DaData
@ -1085,8 +1139,12 @@ export const resolvers = {
okved: organizationData.okved, okved: organizationData.okved,
// Контакты // Контакты
phones: organizationData.phones ? JSON.parse(JSON.stringify(organizationData.phones)) : null, phones: organizationData.phones
emails: organizationData.emails ? JSON.parse(JSON.stringify(organizationData.emails)) : null, ? JSON.parse(JSON.stringify(organizationData.phones))
: null,
emails: organizationData.emails
? JSON.parse(JSON.stringify(organizationData.emails))
: null,
// Финансовые данные // Финансовые данные
employeeCount: organizationData.employeeCount, employeeCount: organizationData.employeeCount,
@ -1094,9 +1152,9 @@ export const resolvers = {
taxSystem: organizationData.taxSystem, taxSystem: organizationData.taxSystem,
type: type, type: type,
dadataData: JSON.parse(JSON.stringify(organizationData.rawData)) dadataData: JSON.parse(JSON.stringify(organizationData.rawData)),
} },
}) });
// Привязываем пользователя к организации // Привязываем пользователя к организации
const updatedUser = await prisma.user.update({ const updatedUser = await prisma.user.update({
@ -1105,24 +1163,23 @@ export const resolvers = {
include: { include: {
organization: { organization: {
include: { include: {
apiKeys: true apiKeys: true,
} },
} },
} },
}) });
return { return {
success: true, success: true,
message: 'Организация успешно зарегистрирована', message: "Организация успешно зарегистрирована",
user: updatedUser user: updatedUser,
} };
} catch (error) { } catch (error) {
console.error('Error registering fulfillment organization:', error) console.error("Error registering fulfillment organization:", error);
return { return {
success: false, success: false,
message: 'Ошибка при регистрации организации' message: "Ошибка при регистрации организации",
} };
} }
}, },
@ -1130,87 +1187,96 @@ export const resolvers = {
_: unknown, _: unknown,
args: { args: {
input: { input: {
phone: string phone: string;
wbApiKey?: string wbApiKey?: string;
ozonApiKey?: string ozonApiKey?: string;
ozonClientId?: string ozonClientId?: string;
} };
}, },
context: Context context: Context
) => { ) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const { wbApiKey, ozonApiKey, ozonClientId } = args.input const { wbApiKey, ozonApiKey, ozonClientId } = args.input;
if (!wbApiKey && !ozonApiKey) { if (!wbApiKey && !ozonApiKey) {
return { return {
success: false, success: false,
message: 'Необходимо указать хотя бы один API ключ маркетплейса' message: "Необходимо указать хотя бы один API ключ маркетплейса",
} };
} }
try { try {
// Валидируем API ключи // Валидируем API ключи
const validationResults = [] const validationResults = [];
if (wbApiKey) { if (wbApiKey) {
const wbResult = await marketplaceService.validateWildberriesApiKey(wbApiKey) const wbResult = await marketplaceService.validateWildberriesApiKey(
wbApiKey
);
if (!wbResult.isValid) { if (!wbResult.isValid) {
return { return {
success: false, success: false,
message: `Wildberries: ${wbResult.message}` message: `Wildberries: ${wbResult.message}`,
} };
} }
validationResults.push({ validationResults.push({
marketplace: 'WILDBERRIES', marketplace: "WILDBERRIES",
apiKey: wbApiKey, apiKey: wbApiKey,
data: wbResult.data data: wbResult.data,
}) });
} }
if (ozonApiKey && ozonClientId) { if (ozonApiKey && ozonClientId) {
const ozonResult = await marketplaceService.validateOzonApiKey(ozonApiKey, ozonClientId) const ozonResult = await marketplaceService.validateOzonApiKey(
ozonApiKey,
ozonClientId
);
if (!ozonResult.isValid) { if (!ozonResult.isValid) {
return { return {
success: false, success: false,
message: `Ozon: ${ozonResult.message}` message: `Ozon: ${ozonResult.message}`,
} };
} }
validationResults.push({ validationResults.push({
marketplace: 'OZON', marketplace: "OZON",
apiKey: ozonApiKey, apiKey: ozonApiKey,
data: ozonResult.data data: ozonResult.data,
}) });
} }
// Создаем организацию селлера - используем tradeMark как основное имя // Создаем организацию селлера - используем tradeMark как основное имя
const tradeMark = validationResults[0]?.data?.tradeMark const tradeMark = validationResults[0]?.data?.tradeMark;
const sellerName = validationResults[0]?.data?.sellerName const sellerName = validationResults[0]?.data?.sellerName;
const shopName = tradeMark || sellerName || 'Магазин' const shopName = tradeMark || sellerName || "Магазин";
const organization = await prisma.organization.create({ const organization = await prisma.organization.create({
data: { data: {
inn: (validationResults[0]?.data?.inn as string) || `SELLER_${Date.now()}`, inn:
(validationResults[0]?.data?.inn as string) ||
`SELLER_${Date.now()}`,
name: shopName, // Используем tradeMark как основное название name: shopName, // Используем tradeMark как основное название
fullName: sellerName ? `${sellerName} (${shopName})` : `Интернет-магазин "${shopName}"`, fullName: sellerName
type: 'SELLER' ? `${sellerName} (${shopName})`
} : `Интернет-магазин "${shopName}"`,
}) type: "SELLER",
},
});
// Добавляем API ключи // Добавляем API ключи
for (const validation of validationResults) { for (const validation of validationResults) {
await prisma.apiKey.create({ await prisma.apiKey.create({
data: { data: {
marketplace: validation.marketplace as 'WILDBERRIES' | 'OZON', marketplace: validation.marketplace as "WILDBERRIES" | "OZON",
apiKey: validation.apiKey, apiKey: validation.apiKey,
organizationId: organization.id, organizationId: organization.id,
validationData: JSON.parse(JSON.stringify(validation.data)) validationData: JSON.parse(JSON.stringify(validation.data)),
} },
}) });
} }
// Привязываем пользователя к организации // Привязываем пользователя к организации
@ -1220,24 +1286,23 @@ export const resolvers = {
include: { include: {
organization: { organization: {
include: { include: {
apiKeys: true apiKeys: true,
} },
} },
} },
}) });
return { return {
success: true, success: true,
message: 'Селлер организация успешно зарегистрирована', message: "Селлер организация успешно зарегистрирована",
user: updatedUser user: updatedUser,
} };
} catch (error) { } catch (error) {
console.error('Error registering seller organization:', error) console.error("Error registering seller organization:", error);
return { return {
success: false, success: false,
message: 'Ошибка при регистрации организации' message: "Ошибка при регистрации организации",
} };
} }
}, },
@ -1245,84 +1310,85 @@ export const resolvers = {
_: unknown, _: unknown,
args: { args: {
input: { input: {
marketplace: 'WILDBERRIES' | 'OZON' marketplace: "WILDBERRIES" | "OZON";
apiKey: string apiKey: string;
clientId?: string clientId?: string;
validateOnly?: boolean validateOnly?: boolean;
} };
}, },
context: Context context: Context
) => { ) => {
// Разрешаем валидацию без авторизации // Разрешаем валидацию без авторизации
if (!args.input.validateOnly && !context.user) { if (!args.input.validateOnly && !context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const { marketplace, apiKey, clientId, validateOnly } = args.input const { marketplace, apiKey, clientId, validateOnly } = args.input;
// Валидируем API ключ // Валидируем API ключ
const validationResult = await marketplaceService.validateApiKey( const validationResult = await marketplaceService.validateApiKey(
marketplace, marketplace,
apiKey, apiKey,
clientId clientId
) );
if (!validationResult.isValid) { if (!validationResult.isValid) {
return { return {
success: false, success: false,
message: validationResult.message message: validationResult.message,
} };
} }
// Если это только валидация, возвращаем результат без сохранения // Если это только валидация, возвращаем результат без сохранения
if (validateOnly) { if (validateOnly) {
return { return {
success: true, success: true,
message: 'API ключ действителен', message: "API ключ действителен",
apiKey: { apiKey: {
id: 'validate-only', id: "validate-only",
marketplace, marketplace,
isActive: true, isActive: true,
validationData: validationResult, validationData: validationResult,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString() updatedAt: new Date().toISOString(),
} },
} };
} }
// Для сохранения API ключа нужна авторизация // Для сохранения API ключа нужна авторизация
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация для сохранения API ключа', { throw new GraphQLError(
extensions: { code: 'UNAUTHENTICATED' } "Требуется авторизация для сохранения API ключа",
}) {
extensions: { code: "UNAUTHENTICATED" },
}
);
} }
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!user?.organization) { if (!user?.organization) {
return { return {
success: false, success: false,
message: 'Пользователь не привязан к организации' message: "Пользователь не привязан к организации",
} };
} }
try { try {
// Проверяем, что такого ключа еще нет // Проверяем, что такого ключа еще нет
const existingKey = await prisma.apiKey.findUnique({ const existingKey = await prisma.apiKey.findUnique({
where: { where: {
organizationId_marketplace: { organizationId_marketplace: {
organizationId: user.organization.id, organizationId: user.organization.id,
marketplace marketplace,
} },
} },
}) });
if (existingKey) { if (existingKey) {
// Обновляем существующий ключ // Обновляем существующий ключ
@ -1330,16 +1396,16 @@ export const resolvers = {
where: { id: existingKey.id }, where: { id: existingKey.id },
data: { data: {
apiKey, apiKey,
validationData: JSON.parse(JSON.stringify(validationResult.data)), validationData: JSON.parse(JSON.stringify(validationResult.data)),
isActive: true isActive: true,
} },
}) });
return { return {
success: true, success: true,
message: 'API ключ успешно обновлен', message: "API ключ успешно обновлен",
apiKey: updatedKey apiKey: updatedKey,
} };
} else { } else {
// Создаем новый ключ // Создаем новый ключ
const newKey = await prisma.apiKey.create({ const newKey = await prisma.apiKey.create({
@ -1347,44 +1413,43 @@ export const resolvers = {
marketplace, marketplace,
apiKey, apiKey,
organizationId: user.organization.id, organizationId: user.organization.id,
validationData: JSON.parse(JSON.stringify(validationResult.data)) validationData: JSON.parse(JSON.stringify(validationResult.data)),
} },
}) });
return { return {
success: true, success: true,
message: 'API ключ успешно добавлен', message: "API ключ успешно добавлен",
apiKey: newKey apiKey: newKey,
} };
} }
} catch (error) { } catch (error) {
console.error('Error adding marketplace API key:', error) console.error("Error adding marketplace API key:", error);
return { return {
success: false, success: false,
message: 'Ошибка при добавлении API ключа' message: "Ошибка при добавлении API ключа",
} };
} }
}, },
removeMarketplaceApiKey: async ( removeMarketplaceApiKey: async (
_: unknown, _: unknown,
args: { marketplace: 'WILDBERRIES' | 'OZON' }, args: { marketplace: "WILDBERRIES" | "OZON" },
context: Context context: Context
) => { ) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!user?.organization) { if (!user?.organization) {
throw new GraphQLError('Пользователь не привязан к организации') throw new GraphQLError("Пользователь не привязан к организации");
} }
try { try {
@ -1392,34 +1457,40 @@ export const resolvers = {
where: { where: {
organizationId_marketplace: { organizationId_marketplace: {
organizationId: user.organization.id, organizationId: user.organization.id,
marketplace: args.marketplace marketplace: args.marketplace,
} },
} },
}) });
return true return true;
} catch (error) { } catch (error) {
console.error('Error removing marketplace API key:', error) console.error("Error removing marketplace API key:", error);
return false return false;
} }
}, },
updateUserProfile: async (_: unknown, args: { input: { updateUserProfile: async (
avatar?: string _: unknown,
orgPhone?: string args: {
managerName?: string input: {
telegram?: string avatar?: string;
whatsapp?: string orgPhone?: string;
email?: string managerName?: string;
bankName?: string telegram?: string;
bik?: string whatsapp?: string;
accountNumber?: string email?: string;
corrAccount?: string bankName?: string;
} }, context: Context) => { bik?: string;
accountNumber?: string;
corrAccount?: string;
};
},
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
@ -1427,93 +1498,98 @@ export const resolvers = {
include: { include: {
organization: { organization: {
include: { include: {
apiKeys: true apiKeys: true,
} },
} },
} },
}) });
if (!user?.organization) { if (!user?.organization) {
throw new GraphQLError('Пользователь не привязан к организации') throw new GraphQLError("Пользователь не привязан к организации");
} }
try { try {
const { input } = args const { input } = args;
// Обновляем данные пользователя (аватар, имя управляющего) // Обновляем данные пользователя (аватар, имя управляющего)
const userUpdateData: { avatar?: string; managerName?: string } = {} const userUpdateData: { avatar?: string; managerName?: string } = {};
if (input.avatar) { if (input.avatar) {
userUpdateData.avatar = input.avatar userUpdateData.avatar = input.avatar;
} }
if (input.managerName) { if (input.managerName) {
userUpdateData.managerName = input.managerName userUpdateData.managerName = input.managerName;
} }
if (Object.keys(userUpdateData).length > 0) { if (Object.keys(userUpdateData).length > 0) {
await prisma.user.update({ await prisma.user.update({
where: { id: context.user.id }, where: { id: context.user.id },
data: userUpdateData data: userUpdateData,
}) });
} }
// Подготавливаем данные для обновления организации // Подготавливаем данные для обновления организации
const updateData: { const updateData: {
phones?: object phones?: object;
emails?: object emails?: object;
managementName?: string managementName?: string;
managementPost?: string managementPost?: string;
} = {} } = {};
// Название организации больше не обновляется через профиль // Название организации больше не обновляется через профиль
// Для селлеров устанавливается при регистрации, для остальных - при смене ИНН // Для селлеров устанавливается при регистрации, для остальных - при смене ИНН
// Обновляем контактные данные в JSON поле phones // Обновляем контактные данные в JSON поле phones
if (input.orgPhone) { if (input.orgPhone) {
updateData.phones = [{ value: input.orgPhone, type: 'main' }] updateData.phones = [{ value: input.orgPhone, type: "main" }];
} }
// Обновляем email в JSON поле emails // Обновляем email в JSON поле emails
if (input.email) { if (input.email) {
updateData.emails = [{ value: input.email, type: 'main' }] updateData.emails = [{ value: input.email, type: "main" }];
} }
// Сохраняем дополнительные контакты в custom полях // Сохраняем дополнительные контакты в custom полях
// Пока добавим их как дополнительные JSON поля // Пока добавим их как дополнительные JSON поля
const customContacts: { const customContacts: {
managerName?: string managerName?: string;
telegram?: string telegram?: string;
whatsapp?: string whatsapp?: string;
bankDetails?: { bankDetails?: {
bankName?: string bankName?: string;
bik?: string bik?: string;
accountNumber?: string accountNumber?: string;
corrAccount?: string corrAccount?: string;
} };
} = {} } = {};
// managerName теперь сохраняется в поле пользователя, а не в JSON // managerName теперь сохраняется в поле пользователя, а не в JSON
if (input.telegram) { if (input.telegram) {
customContacts.telegram = input.telegram customContacts.telegram = input.telegram;
} }
if (input.whatsapp) { if (input.whatsapp) {
customContacts.whatsapp = input.whatsapp customContacts.whatsapp = input.whatsapp;
} }
if (input.bankName || input.bik || input.accountNumber || input.corrAccount) { if (
input.bankName ||
input.bik ||
input.accountNumber ||
input.corrAccount
) {
customContacts.bankDetails = { customContacts.bankDetails = {
bankName: input.bankName, bankName: input.bankName,
bik: input.bik, bik: input.bik,
accountNumber: input.accountNumber, accountNumber: input.accountNumber,
corrAccount: input.corrAccount corrAccount: input.corrAccount,
} };
} }
// Если есть дополнительные контакты, сохраним их в поле managementPost временно // Если есть дополнительные контакты, сохраним их в поле managementPost временно
// В идеале нужно добавить отдельную таблицу для контактов // В идеале нужно добавить отдельную таблицу для контактов
if (Object.keys(customContacts).length > 0) { if (Object.keys(customContacts).length > 0) {
updateData.managementPost = JSON.stringify(customContacts) updateData.managementPost = JSON.stringify(customContacts);
} }
// Обновляем организацию // Обновляем организацию
@ -1521,9 +1597,9 @@ export const resolvers = {
where: { id: user.organization.id }, where: { id: user.organization.id },
data: updateData, data: updateData,
include: { include: {
apiKeys: true apiKeys: true,
} },
}) });
// Получаем обновленного пользователя // Получаем обновленного пользователя
const updatedUser = await prisma.user.findUnique({ const updatedUser = await prisma.user.findUnique({
@ -1531,31 +1607,35 @@ export const resolvers = {
include: { include: {
organization: { organization: {
include: { include: {
apiKeys: true apiKeys: true,
} },
} },
} },
}) });
return { return {
success: true, success: true,
message: 'Профиль успешно обновлен', message: "Профиль успешно обновлен",
user: updatedUser user: updatedUser,
} };
} catch (error) { } catch (error) {
console.error('Error updating user profile:', error) console.error("Error updating user profile:", error);
return { return {
success: false, success: false,
message: 'Ошибка при обновлении профиля' message: "Ошибка при обновлении профиля",
} };
} }
}, },
updateOrganizationByInn: async (_: unknown, args: { inn: string }, context: Context) => { updateOrganizationByInn: async (
_: unknown,
args: { inn: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
@ -1563,14 +1643,14 @@ export const resolvers = {
include: { include: {
organization: { organization: {
include: { include: {
apiKeys: true apiKeys: true,
} },
} },
} },
}) });
if (!user?.organization) { if (!user?.organization) {
throw new GraphQLError('Пользователь не привязан к организации') throw new GraphQLError("Пользователь не привязан к организации");
} }
try { try {
@ -1578,43 +1658,57 @@ export const resolvers = {
if (!dadataService.validateInn(args.inn)) { if (!dadataService.validateInn(args.inn)) {
return { return {
success: false, success: false,
message: 'Неверный формат ИНН' message: "Неверный формат ИНН",
} };
} }
// Получаем данные организации из DaData // Получаем данные организации из DaData
const organizationData = await dadataService.getOrganizationByInn(args.inn) const organizationData = await dadataService.getOrganizationByInn(
args.inn
);
if (!organizationData) { if (!organizationData) {
return { return {
success: false, success: false,
message: 'Организация с указанным ИНН не найдена в федеральном реестре' message:
} "Организация с указанным ИНН не найдена в федеральном реестре",
};
} }
// Проверяем, есть ли уже организация с таким ИНН в базе (кроме текущей) // Проверяем, есть ли уже организация с таким ИНН в базе (кроме текущей)
const existingOrganization = await prisma.organization.findUnique({ const existingOrganization = await prisma.organization.findUnique({
where: { inn: organizationData.inn } where: { inn: organizationData.inn },
}) });
if (existingOrganization && existingOrganization.id !== user.organization.id) { if (
existingOrganization &&
existingOrganization.id !== user.organization.id
) {
return { return {
success: false, success: false,
message: `Организация с ИНН ${organizationData.inn} уже существует в системе` message: `Организация с ИНН ${organizationData.inn} уже существует в системе`,
} };
} }
// Подготавливаем данные для обновления // Подготавливаем данные для обновления
const updateData: Prisma.OrganizationUpdateInput = { const updateData: Prisma.OrganizationUpdateInput = {
kpp: organizationData.kpp, kpp: organizationData.kpp,
// Для селлеров не обновляем название организации (это название магазина) // Для селлеров не обновляем название организации (это название магазина)
...(user.organization.type !== 'SELLER' && { name: organizationData.name }), ...(user.organization.type !== "SELLER" && {
name: organizationData.name,
}),
fullName: organizationData.fullName, fullName: organizationData.fullName,
address: organizationData.address, address: organizationData.address,
addressFull: organizationData.addressFull, addressFull: organizationData.addressFull,
ogrn: organizationData.ogrn, ogrn: organizationData.ogrn,
ogrnDate: organizationData.ogrnDate ? organizationData.ogrnDate.toISOString() : null, ogrnDate: organizationData.ogrnDate
registrationDate: organizationData.registrationDate ? organizationData.registrationDate.toISOString() : null, ? organizationData.ogrnDate.toISOString()
liquidationDate: organizationData.liquidationDate ? organizationData.liquidationDate.toISOString() : null, : null,
registrationDate: organizationData.registrationDate
? organizationData.registrationDate.toISOString()
: null,
liquidationDate: organizationData.liquidationDate
? organizationData.liquidationDate.toISOString()
: null,
managementName: organizationData.managementName, // Всегда перезаписываем данными из DaData (может быть null) managementName: organizationData.managementName, // Всегда перезаписываем данными из DaData (может быть null)
managementPost: user.organization.managementPost, // Сохраняем кастомные данные пользователя managementPost: user.organization.managementPost, // Сохраняем кастомные данные пользователя
opfCode: organizationData.opfCode, opfCode: organizationData.opfCode,
@ -1624,12 +1718,12 @@ export const resolvers = {
oktmo: organizationData.oktmo, oktmo: organizationData.oktmo,
okpo: organizationData.okpo, okpo: organizationData.okpo,
okved: organizationData.okved, okved: organizationData.okved,
status: organizationData.status status: organizationData.status,
} };
// Добавляем ИНН только если он отличается от текущего // Добавляем ИНН только если он отличается от текущего
if (user.organization.inn !== organizationData.inn) { if (user.organization.inn !== organizationData.inn) {
updateData.inn = organizationData.inn updateData.inn = organizationData.inn;
} }
// Обновляем организацию // Обновляем организацию
@ -1637,9 +1731,9 @@ export const resolvers = {
where: { id: user.organization.id }, where: { id: user.organization.id },
data: updateData, data: updateData,
include: { include: {
apiKeys: true apiKeys: true,
} },
}) });
// Получаем обновленного пользователя // Получаем обновленного пользователя
const updatedUser = await prisma.user.findUnique({ const updatedUser = await prisma.user.findUnique({
@ -1647,60 +1741,64 @@ export const resolvers = {
include: { include: {
organization: { organization: {
include: { include: {
apiKeys: true apiKeys: true,
} },
} },
} },
}) });
return { return {
success: true, success: true,
message: 'Данные организации успешно обновлены', message: "Данные организации успешно обновлены",
user: updatedUser user: updatedUser,
} };
} catch (error) { } catch (error) {
console.error('Error updating organization by INN:', error) console.error("Error updating organization by INN:", error);
return { return {
success: false, success: false,
message: 'Ошибка при обновлении данных организации' message: "Ошибка при обновлении данных организации",
} };
} }
}, },
logout: () => { logout: () => {
// В stateless JWT системе logout происходит на клиенте // В stateless JWT системе logout происходит на клиенте
// Можно добавить blacklist токенов, если нужно // Можно добавить blacklist токенов, если нужно
return true return true;
}, },
// Отправить заявку на добавление в контрагенты // Отправить заявку на добавление в контрагенты
sendCounterpartyRequest: async (_: unknown, args: { organizationId: string; message?: string }, context: Context) => { sendCounterpartyRequest: async (
_: unknown,
args: { organizationId: string; message?: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
if (currentUser.organization.id === args.organizationId) { if (currentUser.organization.id === args.organizationId) {
throw new GraphQLError('Нельзя отправить заявку самому себе') throw new GraphQLError("Нельзя отправить заявку самому себе");
} }
// Проверяем, что организация-получатель существует // Проверяем, что организация-получатель существует
const receiverOrganization = await prisma.organization.findUnique({ const receiverOrganization = await prisma.organization.findUnique({
where: { id: args.organizationId } where: { id: args.organizationId },
}) });
if (!receiverOrganization) { if (!receiverOrganization) {
throw new GraphQLError('Организация не найдена') throw new GraphQLError("Организация не найдена");
} }
try { try {
@ -1709,55 +1807,59 @@ export const resolvers = {
where: { where: {
senderId_receiverId: { senderId_receiverId: {
senderId: currentUser.organization.id, senderId: currentUser.organization.id,
receiverId: args.organizationId receiverId: args.organizationId,
} },
}, },
update: { update: {
status: 'PENDING', status: "PENDING",
message: args.message, message: args.message,
updatedAt: new Date() updatedAt: new Date(),
}, },
create: { create: {
senderId: currentUser.organization.id, senderId: currentUser.organization.id,
receiverId: args.organizationId, receiverId: args.organizationId,
message: args.message, message: args.message,
status: 'PENDING' status: "PENDING",
}, },
include: { include: {
sender: true, sender: true,
receiver: true receiver: true,
} },
}) });
return { return {
success: true, success: true,
message: 'Заявка отправлена', message: "Заявка отправлена",
request request,
} };
} catch (error) { } catch (error) {
console.error('Error sending counterparty request:', error) console.error("Error sending counterparty request:", error);
return { return {
success: false, success: false,
message: 'Ошибка при отправке заявки' message: "Ошибка при отправке заявки",
} };
} }
}, },
// Ответить на заявку контрагента // Ответить на заявку контрагента
respondToCounterpartyRequest: async (_: unknown, args: { requestId: string; accept: boolean }, context: Context) => { respondToCounterpartyRequest: async (
_: unknown,
args: { requestId: string; accept: boolean },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
try { try {
@ -1766,23 +1868,23 @@ export const resolvers = {
where: { id: args.requestId }, where: { id: args.requestId },
include: { include: {
sender: true, sender: true,
receiver: true receiver: true,
} },
}) });
if (!request) { if (!request) {
throw new GraphQLError('Заявка не найдена') throw new GraphQLError("Заявка не найдена");
} }
if (request.receiverId !== currentUser.organization.id) { if (request.receiverId !== currentUser.organization.id) {
throw new GraphQLError('Нет прав на обработку этой заявки') throw new GraphQLError("Нет прав на обработку этой заявки");
} }
if (request.status !== 'PENDING') { if (request.status !== "PENDING") {
throw new GraphQLError('Заявка уже обработана') throw new GraphQLError("Заявка уже обработана");
} }
const newStatus = args.accept ? 'ACCEPTED' : 'REJECTED' const newStatus = args.accept ? "ACCEPTED" : "REJECTED";
// Обновляем статус заявки // Обновляем статус заявки
const updatedRequest = await prisma.counterpartyRequest.update({ const updatedRequest = await prisma.counterpartyRequest.update({
@ -1790,9 +1892,9 @@ export const resolvers = {
data: { status: newStatus }, data: { status: newStatus },
include: { include: {
sender: true, sender: true,
receiver: true receiver: true,
} },
}) });
// Если заявка принята, создаем связи контрагентов в обе стороны // Если заявка принята, создаем связи контрагентов в обе стороны
if (args.accept) { if (args.accept) {
@ -1801,94 +1903,102 @@ export const resolvers = {
prisma.counterparty.create({ prisma.counterparty.create({
data: { data: {
organizationId: request.receiverId, organizationId: request.receiverId,
counterpartyId: request.senderId counterpartyId: request.senderId,
} },
}), }),
// Добавляем получателя в контрагенты отправителя // Добавляем получателя в контрагенты отправителя
prisma.counterparty.create({ prisma.counterparty.create({
data: { data: {
organizationId: request.senderId, organizationId: request.senderId,
counterpartyId: request.receiverId counterpartyId: request.receiverId,
} },
}) }),
]) ]);
} }
return { return {
success: true, success: true,
message: args.accept ? 'Заявка принята' : 'Заявка отклонена', message: args.accept ? "Заявка принята" : "Заявка отклонена",
request: updatedRequest request: updatedRequest,
} };
} catch (error) { } catch (error) {
console.error('Error responding to counterparty request:', error) console.error("Error responding to counterparty request:", error);
return { return {
success: false, success: false,
message: 'Ошибка при обработке заявки' message: "Ошибка при обработке заявки",
} };
} }
}, },
// Отменить заявку // Отменить заявку
cancelCounterpartyRequest: async (_: unknown, args: { requestId: string }, context: Context) => { cancelCounterpartyRequest: async (
_: unknown,
args: { requestId: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
try { try {
const request = await prisma.counterpartyRequest.findUnique({ const request = await prisma.counterpartyRequest.findUnique({
where: { id: args.requestId } where: { id: args.requestId },
}) });
if (!request) { if (!request) {
throw new GraphQLError('Заявка не найдена') throw new GraphQLError("Заявка не найдена");
} }
if (request.senderId !== currentUser.organization.id) { if (request.senderId !== currentUser.organization.id) {
throw new GraphQLError('Можно отменить только свои заявки') throw new GraphQLError("Можно отменить только свои заявки");
} }
if (request.status !== 'PENDING') { if (request.status !== "PENDING") {
throw new GraphQLError('Можно отменить только ожидающие заявки') throw new GraphQLError("Можно отменить только ожидающие заявки");
} }
await prisma.counterpartyRequest.update({ await prisma.counterpartyRequest.update({
where: { id: args.requestId }, where: { id: args.requestId },
data: { status: 'CANCELLED' } data: { status: "CANCELLED" },
}) });
return true return true;
} catch (error) { } catch (error) {
console.error('Error cancelling counterparty request:', error) console.error("Error cancelling counterparty request:", error);
return false return false;
} }
}, },
// Удалить контрагента // Удалить контрагента
removeCounterparty: async (_: unknown, args: { organizationId: string }, context: Context) => { removeCounterparty: async (
_: unknown,
args: { organizationId: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
try { try {
@ -1897,60 +2007,70 @@ export const resolvers = {
prisma.counterparty.deleteMany({ prisma.counterparty.deleteMany({
where: { where: {
organizationId: currentUser.organization.id, organizationId: currentUser.organization.id,
counterpartyId: args.organizationId counterpartyId: args.organizationId,
} },
}), }),
prisma.counterparty.deleteMany({ prisma.counterparty.deleteMany({
where: { where: {
organizationId: args.organizationId, organizationId: args.organizationId,
counterpartyId: currentUser.organization.id counterpartyId: currentUser.organization.id,
} },
}) }),
]) ]);
return true return true;
} catch (error) { } catch (error) {
console.error('Error removing counterparty:', error) console.error("Error removing counterparty:", error);
return false return false;
} }
}, },
// Отправить сообщение // Отправить сообщение
sendMessage: async (_: unknown, args: { receiverOrganizationId: string; content?: string; type?: 'TEXT' | 'VOICE' }, context: Context) => { sendMessage: async (
_: unknown,
args: {
receiverOrganizationId: string;
content?: string;
type?: "TEXT" | "VOICE";
},
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что получатель является контрагентом // Проверяем, что получатель является контрагентом
const isCounterparty = await prisma.counterparty.findFirst({ const isCounterparty = await prisma.counterparty.findFirst({
where: { where: {
organizationId: currentUser.organization.id, organizationId: currentUser.organization.id,
counterpartyId: args.receiverOrganizationId counterpartyId: args.receiverOrganizationId,
} },
}) });
if (!isCounterparty) { if (!isCounterparty) {
throw new GraphQLError('Можно отправлять сообщения только контрагентам') throw new GraphQLError(
"Можно отправлять сообщения только контрагентам"
);
} }
// Получаем организацию получателя // Получаем организацию получателя
const receiverOrganization = await prisma.organization.findUnique({ const receiverOrganization = await prisma.organization.findUnique({
where: { id: args.receiverOrganizationId } where: { id: args.receiverOrganizationId },
}) });
if (!receiverOrganization) { if (!receiverOrganization) {
throw new GraphQLError('Организация получателя не найдена') throw new GraphQLError("Организация получателя не найдена");
} }
try { try {
@ -1958,76 +2078,86 @@ export const resolvers = {
const message = await prisma.message.create({ const message = await prisma.message.create({
data: { data: {
content: args.content?.trim() || null, content: args.content?.trim() || null,
type: args.type || 'TEXT', type: args.type || "TEXT",
senderId: context.user.id, senderId: context.user.id,
senderOrganizationId: currentUser.organization.id, senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.receiverOrganizationId receiverOrganizationId: args.receiverOrganizationId,
}, },
include: { include: {
sender: true, sender: true,
senderOrganization: { senderOrganization: {
include: { include: {
users: true users: true,
} },
}, },
receiverOrganization: { receiverOrganization: {
include: { include: {
users: true users: true,
} },
} },
} },
}) });
return { return {
success: true, success: true,
message: 'Сообщение отправлено', message: "Сообщение отправлено",
messageData: message messageData: message,
} };
} catch (error) { } catch (error) {
console.error('Error sending message:', error) console.error("Error sending message:", error);
return { return {
success: false, success: false,
message: 'Ошибка при отправке сообщения' message: "Ошибка при отправке сообщения",
} };
} }
}, },
// Отправить голосовое сообщение // Отправить голосовое сообщение
sendVoiceMessage: async (_: unknown, args: { receiverOrganizationId: string; voiceUrl: string; voiceDuration: number }, context: Context) => { sendVoiceMessage: async (
_: unknown,
args: {
receiverOrganizationId: string;
voiceUrl: string;
voiceDuration: number;
},
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что получатель является контрагентом // Проверяем, что получатель является контрагентом
const isCounterparty = await prisma.counterparty.findFirst({ const isCounterparty = await prisma.counterparty.findFirst({
where: { where: {
organizationId: currentUser.organization.id, organizationId: currentUser.organization.id,
counterpartyId: args.receiverOrganizationId counterpartyId: args.receiverOrganizationId,
} },
}) });
if (!isCounterparty) { if (!isCounterparty) {
throw new GraphQLError('Можно отправлять сообщения только контрагентам') throw new GraphQLError(
"Можно отправлять сообщения только контрагентам"
);
} }
// Получаем организацию получателя // Получаем организацию получателя
const receiverOrganization = await prisma.organization.findUnique({ const receiverOrganization = await prisma.organization.findUnique({
where: { id: args.receiverOrganizationId } where: { id: args.receiverOrganizationId },
}) });
if (!receiverOrganization) { if (!receiverOrganization) {
throw new GraphQLError('Организация получателя не найдена') throw new GraphQLError("Организация получателя не найдена");
} }
try { try {
@ -2035,217 +2165,256 @@ export const resolvers = {
const message = await prisma.message.create({ const message = await prisma.message.create({
data: { data: {
content: null, content: null,
type: 'VOICE', type: "VOICE",
voiceUrl: args.voiceUrl, voiceUrl: args.voiceUrl,
voiceDuration: args.voiceDuration, voiceDuration: args.voiceDuration,
senderId: context.user.id, senderId: context.user.id,
senderOrganizationId: currentUser.organization.id, senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.receiverOrganizationId receiverOrganizationId: args.receiverOrganizationId,
}, },
include: { include: {
sender: true, sender: true,
senderOrganization: { senderOrganization: {
include: { include: {
users: true users: true,
} },
}, },
receiverOrganization: { receiverOrganization: {
include: { include: {
users: true users: true,
} },
} },
} },
}) });
return { return {
success: true, success: true,
message: 'Голосовое сообщение отправлено', message: "Голосовое сообщение отправлено",
messageData: message messageData: message,
} };
} catch (error) { } catch (error) {
console.error('Error sending voice message:', error) console.error("Error sending voice message:", error);
return { return {
success: false, success: false,
message: 'Ошибка при отправке голосового сообщения' message: "Ошибка при отправке голосового сообщения",
} };
} }
}, },
// Отправить изображение // Отправить изображение
sendImageMessage: async (_: unknown, args: { receiverOrganizationId: string; fileUrl: string; fileName: string; fileSize: number; fileType: string }, context: Context) => { sendImageMessage: async (
_: unknown,
args: {
receiverOrganizationId: string;
fileUrl: string;
fileName: string;
fileSize: number;
fileType: string;
},
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что получатель является контрагентом // Проверяем, что получатель является контрагентом
const isCounterparty = await prisma.counterparty.findFirst({ const isCounterparty = await prisma.counterparty.findFirst({
where: { where: {
organizationId: currentUser.organization.id, organizationId: currentUser.organization.id,
counterpartyId: args.receiverOrganizationId counterpartyId: args.receiverOrganizationId,
} },
}) });
if (!isCounterparty) { if (!isCounterparty) {
throw new GraphQLError('Можно отправлять сообщения только контрагентам') throw new GraphQLError(
"Можно отправлять сообщения только контрагентам"
);
} }
try { try {
const message = await prisma.message.create({ const message = await prisma.message.create({
data: { data: {
content: null, content: null,
type: 'IMAGE', type: "IMAGE",
fileUrl: args.fileUrl, fileUrl: args.fileUrl,
fileName: args.fileName, fileName: args.fileName,
fileSize: args.fileSize, fileSize: args.fileSize,
fileType: args.fileType, fileType: args.fileType,
senderId: context.user.id, senderId: context.user.id,
senderOrganizationId: currentUser.organization.id, senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.receiverOrganizationId receiverOrganizationId: args.receiverOrganizationId,
}, },
include: { include: {
sender: true, sender: true,
senderOrganization: { senderOrganization: {
include: { include: {
users: true users: true,
} },
}, },
receiverOrganization: { receiverOrganization: {
include: { include: {
users: true users: true,
} },
} },
} },
}) });
return { return {
success: true, success: true,
message: 'Изображение отправлено', message: "Изображение отправлено",
messageData: message messageData: message,
} };
} catch (error) { } catch (error) {
console.error('Error sending image:', error) console.error("Error sending image:", error);
return { return {
success: false, success: false,
message: 'Ошибка при отправке изображения' message: "Ошибка при отправке изображения",
} };
} }
}, },
// Отправить файл // Отправить файл
sendFileMessage: async (_: unknown, args: { receiverOrganizationId: string; fileUrl: string; fileName: string; fileSize: number; fileType: string }, context: Context) => { sendFileMessage: async (
_: unknown,
args: {
receiverOrganizationId: string;
fileUrl: string;
fileName: string;
fileSize: number;
fileType: string;
},
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что получатель является контрагентом // Проверяем, что получатель является контрагентом
const isCounterparty = await prisma.counterparty.findFirst({ const isCounterparty = await prisma.counterparty.findFirst({
where: { where: {
organizationId: currentUser.organization.id, organizationId: currentUser.organization.id,
counterpartyId: args.receiverOrganizationId counterpartyId: args.receiverOrganizationId,
} },
}) });
if (!isCounterparty) { if (!isCounterparty) {
throw new GraphQLError('Можно отправлять сообщения только контрагентам') throw new GraphQLError(
"Можно отправлять сообщения только контрагентам"
);
} }
try { try {
const message = await prisma.message.create({ const message = await prisma.message.create({
data: { data: {
content: null, content: null,
type: 'FILE', type: "FILE",
fileUrl: args.fileUrl, fileUrl: args.fileUrl,
fileName: args.fileName, fileName: args.fileName,
fileSize: args.fileSize, fileSize: args.fileSize,
fileType: args.fileType, fileType: args.fileType,
senderId: context.user.id, senderId: context.user.id,
senderOrganizationId: currentUser.organization.id, senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.receiverOrganizationId receiverOrganizationId: args.receiverOrganizationId,
}, },
include: { include: {
sender: true, sender: true,
senderOrganization: { senderOrganization: {
include: { include: {
users: true users: true,
} },
}, },
receiverOrganization: { receiverOrganization: {
include: { include: {
users: true users: true,
} },
} },
} },
}) });
return { return {
success: true, success: true,
message: 'Файл отправлен', message: "Файл отправлен",
messageData: message messageData: message,
} };
} catch (error) { } catch (error) {
console.error('Error sending file:', error) console.error("Error sending file:", error);
return { return {
success: false, success: false,
message: 'Ошибка при отправке файла' message: "Ошибка при отправке файла",
} };
} }
}, },
// Отметить сообщения как прочитанные // Отметить сообщения как прочитанные
markMessagesAsRead: async (_: unknown, args: { conversationId: string }, context: Context) => { markMessagesAsRead: async (
_: unknown,
args: { conversationId: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
// TODO: Здесь будет логика обновления статуса сообщений // TODO: Здесь будет логика обновления статуса сообщений
// Пока возвращаем успешный ответ // Пока возвращаем успешный ответ
return true return true;
}, },
// Создать услугу // Создать услугу
createService: async (_: unknown, args: { input: { name: string; description?: string; price: number; imageUrl?: string } }, context: Context) => { createService: async (
_: unknown,
args: {
input: {
name: string;
description?: string;
price: number;
imageUrl?: string;
};
},
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что это фулфилмент центр // Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') { if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError('Услуги доступны только для фулфилмент центров') throw new GraphQLError("Услуги доступны только для фулфилмент центров");
} }
try { try {
@ -2255,52 +2424,64 @@ export const resolvers = {
description: args.input.description, description: args.input.description,
price: args.input.price, price: args.input.price,
imageUrl: args.input.imageUrl, imageUrl: args.input.imageUrl,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
}, },
include: { organization: true } include: { organization: true },
}) });
return { return {
success: true, success: true,
message: 'Услуга успешно создана', message: "Услуга успешно создана",
service service,
} };
} catch (error) { } catch (error) {
console.error('Error creating service:', error) console.error("Error creating service:", error);
return { return {
success: false, success: false,
message: 'Ошибка при создании услуги' message: "Ошибка при создании услуги",
} };
} }
}, },
// Обновить услугу // Обновить услугу
updateService: async (_: unknown, args: { id: string; input: { name: string; description?: string; price: number; imageUrl?: string } }, context: Context) => { updateService: async (
_: unknown,
args: {
id: string;
input: {
name: string;
description?: string;
price: number;
imageUrl?: string;
};
},
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что услуга принадлежит текущей организации // Проверяем, что услуга принадлежит текущей организации
const existingService = await prisma.service.findFirst({ const existingService = await prisma.service.findFirst({
where: { where: {
id: args.id, id: args.id,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
} },
}) });
if (!existingService) { if (!existingService) {
throw new GraphQLError('Услуга не найдена или нет доступа') throw new GraphQLError("Услуга не найдена или нет доступа");
} }
try { try {
@ -2310,86 +2491,103 @@ export const resolvers = {
name: args.input.name, name: args.input.name,
description: args.input.description, description: args.input.description,
price: args.input.price, price: args.input.price,
imageUrl: args.input.imageUrl imageUrl: args.input.imageUrl,
}, },
include: { organization: true } include: { organization: true },
}) });
return { return {
success: true, success: true,
message: 'Услуга успешно обновлена', message: "Услуга успешно обновлена",
service service,
} };
} catch (error) { } catch (error) {
console.error('Error updating service:', error) console.error("Error updating service:", error);
return { return {
success: false, success: false,
message: 'Ошибка при обновлении услуги' message: "Ошибка при обновлении услуги",
} };
} }
}, },
// Удалить услугу // Удалить услугу
deleteService: async (_: unknown, args: { id: string }, context: Context) => { deleteService: async (
_: unknown,
args: { id: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что услуга принадлежит текущей организации // Проверяем, что услуга принадлежит текущей организации
const existingService = await prisma.service.findFirst({ const existingService = await prisma.service.findFirst({
where: { where: {
id: args.id, id: args.id,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
} },
}) });
if (!existingService) { if (!existingService) {
throw new GraphQLError('Услуга не найдена или нет доступа') throw new GraphQLError("Услуга не найдена или нет доступа");
} }
try { try {
await prisma.service.delete({ await prisma.service.delete({
where: { id: args.id } where: { id: args.id },
}) });
return true return true;
} catch (error) { } catch (error) {
console.error('Error deleting service:', error) console.error("Error deleting service:", error);
return false return false;
} }
}, },
// Создать расходник // Создать расходник
createSupply: async (_: unknown, args: { input: { name: string; description?: string; price: number; imageUrl?: string } }, context: Context) => { createSupply: async (
_: unknown,
args: {
input: {
name: string;
description?: string;
price: number;
imageUrl?: string;
};
},
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что это фулфилмент центр // Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') { if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError('Расходники доступны только для фулфилмент центров') throw new GraphQLError(
"Расходники доступны только для фулфилмент центров"
);
} }
try { try {
@ -2400,52 +2598,64 @@ export const resolvers = {
price: args.input.price, price: args.input.price,
quantity: 0, // Временно устанавливаем 0, так как поле убрано из интерфейса quantity: 0, // Временно устанавливаем 0, так как поле убрано из интерфейса
imageUrl: args.input.imageUrl, imageUrl: args.input.imageUrl,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
}, },
include: { organization: true } include: { organization: true },
}) });
return { return {
success: true, success: true,
message: 'Расходник успешно создан', message: "Расходник успешно создан",
supply supply,
} };
} catch (error) { } catch (error) {
console.error('Error creating supply:', error) console.error("Error creating supply:", error);
return { return {
success: false, success: false,
message: 'Ошибка при создании расходника' message: "Ошибка при создании расходника",
} };
} }
}, },
// Обновить расходник // Обновить расходник
updateSupply: async (_: unknown, args: { id: string; input: { name: string; description?: string; price: number; imageUrl?: string } }, context: Context) => { updateSupply: async (
_: unknown,
args: {
id: string;
input: {
name: string;
description?: string;
price: number;
imageUrl?: string;
};
},
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что расходник принадлежит текущей организации // Проверяем, что расходник принадлежит текущей организации
const existingSupply = await prisma.supply.findFirst({ const existingSupply = await prisma.supply.findFirst({
where: { where: {
id: args.id, id: args.id,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
} },
}) });
if (!existingSupply) { if (!existingSupply) {
throw new GraphQLError('Расходник не найден или нет доступа') throw new GraphQLError("Расходник не найден или нет доступа");
} }
try { try {
@ -2456,119 +2666,292 @@ export const resolvers = {
description: args.input.description, description: args.input.description,
price: args.input.price, price: args.input.price,
quantity: 0, // Временно устанавливаем 0, так как поле убрано из интерфейса quantity: 0, // Временно устанавливаем 0, так как поле убрано из интерфейса
imageUrl: args.input.imageUrl imageUrl: args.input.imageUrl,
}, },
include: { organization: true } include: { organization: true },
}) });
return { return {
success: true, success: true,
message: 'Расходник успешно обновлен', message: "Расходник успешно обновлен",
supply supply,
} };
} catch (error) { } catch (error) {
console.error('Error updating supply:', error) console.error("Error updating supply:", error);
return { return {
success: false, success: false,
message: 'Ошибка при обновлении расходника' message: "Ошибка при обновлении расходника",
} };
} }
}, },
// Удалить расходник // Удалить расходник
deleteSupply: async (_: unknown, args: { id: string }, context: Context) => { deleteSupply: async (
_: unknown,
args: { id: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что расходник принадлежит текущей организации // Проверяем, что расходник принадлежит текущей организации
const existingSupply = await prisma.supply.findFirst({ const existingSupply = await prisma.supply.findFirst({
where: { where: {
id: args.id, id: args.id,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
} },
}) });
if (!existingSupply) { if (!existingSupply) {
throw new GraphQLError('Расходник не найден или нет доступа') throw new GraphQLError("Расходник не найден или нет доступа");
} }
try { try {
await prisma.supply.delete({ await prisma.supply.delete({
where: { id: args.id } where: { id: args.id },
}) });
return true return true;
} catch (error) { } catch (error) {
console.error('Error deleting supply:', error) console.error("Error deleting supply:", error);
return false return false;
} }
}, },
// Создать товар // Создать заказ поставки расходников
createProduct: async (_: unknown, args: { createSupplyOrder: async (
input: { _: unknown,
name: string; args: {
article: string; input: {
description?: string; partnerId: string;
price: number; deliveryDate: string;
quantity: number; items: Array<{ productId: string; quantity: number }>;
categoryId?: string; };
brand?: string; },
color?: string; context: Context
size?: string; ) => {
weight?: number;
dimensions?: string;
material?: string;
images?: string[];
mainImage?: string;
isActive?: boolean;
}
}, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') 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') { if (currentUser.organization.type !== "WHOLESALE") {
throw new GraphQLError('Товары доступны только для оптовиков') throw new GraphQLError("Товары доступны только для оптовиков");
} }
// Проверяем уникальность артикула в рамках организации // Проверяем уникальность артикула в рамках организации
const existingProduct = await prisma.product.findFirst({ const existingProduct = await prisma.product.findFirst({
where: { where: {
article: args.input.article, article: args.input.article,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
} },
}) });
if (existingProduct) { if (existingProduct) {
return { return {
success: false, success: false,
message: 'Товар с таким артикулом уже существует' message: "Товар с таким артикулом уже существует",
} };
} }
try { try {
@ -2589,74 +2972,78 @@ export const resolvers = {
images: args.input.images || [], images: args.input.images || [],
mainImage: args.input.mainImage, mainImage: args.input.mainImage,
isActive: args.input.isActive ?? true, isActive: args.input.isActive ?? true,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
}, },
include: { include: {
category: true, category: true,
organization: true organization: true,
} },
}) });
return { return {
success: true, success: true,
message: 'Товар успешно создан', message: "Товар успешно создан",
product product,
} };
} catch (error) { } catch (error) {
console.error('Error creating product:', error) console.error("Error creating product:", error);
return { return {
success: false, success: false,
message: 'Ошибка при создании товара' message: "Ошибка при создании товара",
} };
} }
}, },
// Обновить товар // Обновить товар
updateProduct: async (_: unknown, args: { updateProduct: async (
id: string; _: unknown,
input: { args: {
name: string; id: string;
article: string; input: {
description?: string; name: string;
price: number; article: string;
quantity: number; description?: string;
categoryId?: string; price: number;
brand?: string; quantity: number;
color?: string; categoryId?: string;
size?: string; brand?: string;
weight?: number; color?: string;
dimensions?: string; size?: string;
material?: string; weight?: number;
images?: string[]; dimensions?: string;
mainImage?: string; material?: string;
isActive?: boolean; images?: string[];
} mainImage?: string;
}, context: Context) => { isActive?: boolean;
};
},
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что товар принадлежит текущей организации // Проверяем, что товар принадлежит текущей организации
const existingProduct = await prisma.product.findFirst({ const existingProduct = await prisma.product.findFirst({
where: { where: {
id: args.id, id: args.id,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
} },
}) });
if (!existingProduct) { if (!existingProduct) {
throw new GraphQLError('Товар не найден или нет доступа') throw new GraphQLError("Товар не найден или нет доступа");
} }
// Проверяем уникальность артикула (если он изменился) // Проверяем уникальность артикула (если он изменился)
@ -2665,15 +3052,15 @@ export const resolvers = {
where: { where: {
article: args.input.article, article: args.input.article,
organizationId: currentUser.organization.id, organizationId: currentUser.organization.id,
NOT: { id: args.id } NOT: { id: args.id },
} },
}) });
if (duplicateProduct) { if (duplicateProduct) {
return { return {
success: false, success: false,
message: 'Товар с таким артикулом уже существует' message: "Товар с таким артикулом уже существует",
} };
} }
} }
@ -2695,141 +3082,153 @@ export const resolvers = {
material: args.input.material, material: args.input.material,
images: args.input.images || [], images: args.input.images || [],
mainImage: args.input.mainImage, mainImage: args.input.mainImage,
isActive: args.input.isActive ?? true isActive: args.input.isActive ?? true,
}, },
include: { include: {
category: true, category: true,
organization: true organization: true,
} },
}) });
return { return {
success: true, success: true,
message: 'Товар успешно обновлен', message: "Товар успешно обновлен",
product product,
} };
} catch (error) { } catch (error) {
console.error('Error updating product:', error) console.error("Error updating product:", error);
return { return {
success: false, success: false,
message: 'Ошибка при обновлении товара' message: "Ошибка при обновлении товара",
} };
} }
}, },
// Удалить товар // Удалить товар
deleteProduct: async (_: unknown, args: { id: string }, context: Context) => { deleteProduct: async (
_: unknown,
args: { id: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что товар принадлежит текущей организации // Проверяем, что товар принадлежит текущей организации
const existingProduct = await prisma.product.findFirst({ const existingProduct = await prisma.product.findFirst({
where: { where: {
id: args.id, id: args.id,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
} },
}) });
if (!existingProduct) { if (!existingProduct) {
throw new GraphQLError('Товар не найден или нет доступа') throw new GraphQLError("Товар не найден или нет доступа");
} }
try { try {
await prisma.product.delete({ await prisma.product.delete({
where: { id: args.id } where: { id: args.id },
}) });
return true return true;
} catch (error) { } catch (error) {
console.error('Error deleting product:', error) console.error("Error deleting product:", error);
return false return false;
} }
}, },
// Создать категорию // Создать категорию
createCategory: async (_: unknown, args: { input: { name: string } }, context: Context) => { createCategory: async (
_: unknown,
args: { input: { name: string } },
context: Context
) => {
if (!context.user && !context.admin) { if (!context.user && !context.admin) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
// Проверяем уникальность названия категории // Проверяем уникальность названия категории
const existingCategory = await prisma.category.findUnique({ const existingCategory = await prisma.category.findUnique({
where: { name: args.input.name } where: { name: args.input.name },
}) });
if (existingCategory) { if (existingCategory) {
return { return {
success: false, success: false,
message: 'Категория с таким названием уже существует' message: "Категория с таким названием уже существует",
} };
} }
try { try {
const category = await prisma.category.create({ const category = await prisma.category.create({
data: { data: {
name: args.input.name name: args.input.name,
} },
}) });
return { return {
success: true, success: true,
message: 'Категория успешно создана', message: "Категория успешно создана",
category category,
} };
} catch (error) { } catch (error) {
console.error('Error creating category:', error) console.error("Error creating category:", error);
return { return {
success: false, success: false,
message: 'Ошибка при создании категории' message: "Ошибка при создании категории",
} };
} }
}, },
// Обновить категорию // Обновить категорию
updateCategory: async (_: unknown, args: { id: string; input: { name: string } }, context: Context) => { updateCategory: async (
_: unknown,
args: { id: string; input: { name: string } },
context: Context
) => {
if (!context.user && !context.admin) { if (!context.user && !context.admin) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
// Проверяем существование категории // Проверяем существование категории
const existingCategory = await prisma.category.findUnique({ const existingCategory = await prisma.category.findUnique({
where: { id: args.id } where: { id: args.id },
}) });
if (!existingCategory) { if (!existingCategory) {
return { return {
success: false, success: false,
message: 'Категория не найдена' message: "Категория не найдена",
} };
} }
// Проверяем уникальность нового названия (если изменилось) // Проверяем уникальность нового названия (если изменилось)
if (args.input.name !== existingCategory.name) { if (args.input.name !== existingCategory.name) {
const duplicateCategory = await prisma.category.findUnique({ const duplicateCategory = await prisma.category.findUnique({
where: { name: args.input.name } where: { name: args.input.name },
}) });
if (duplicateCategory) { if (duplicateCategory) {
return { return {
success: false, success: false,
message: 'Категория с таким названием уже существует' message: "Категория с таким названием уже существует",
} };
} }
} }
@ -2837,113 +3236,123 @@ export const resolvers = {
const category = await prisma.category.update({ const category = await prisma.category.update({
where: { id: args.id }, where: { id: args.id },
data: { data: {
name: args.input.name name: args.input.name,
} },
}) });
return { return {
success: true, success: true,
message: 'Категория успешно обновлена', message: "Категория успешно обновлена",
category category,
} };
} catch (error) { } catch (error) {
console.error('Error updating category:', error) console.error("Error updating category:", error);
return { return {
success: false, success: false,
message: 'Ошибка при обновлении категории' message: "Ошибка при обновлении категории",
} };
} }
}, },
// Удалить категорию // Удалить категорию
deleteCategory: async (_: unknown, args: { id: string }, context: Context) => { deleteCategory: async (
_: unknown,
args: { id: string },
context: Context
) => {
if (!context.user && !context.admin) { if (!context.user && !context.admin) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
// Проверяем существование категории // Проверяем существование категории
const existingCategory = await prisma.category.findUnique({ const existingCategory = await prisma.category.findUnique({
where: { id: args.id }, where: { id: args.id },
include: { products: true } include: { products: true },
}) });
if (!existingCategory) { if (!existingCategory) {
throw new GraphQLError('Категория не найдена') throw new GraphQLError("Категория не найдена");
} }
// Проверяем, есть ли товары в этой категории // Проверяем, есть ли товары в этой категории
if (existingCategory.products.length > 0) { if (existingCategory.products.length > 0) {
throw new GraphQLError('Нельзя удалить категорию, в которой есть товары') throw new GraphQLError(
"Нельзя удалить категорию, в которой есть товары"
);
} }
try { try {
await prisma.category.delete({ await prisma.category.delete({
where: { id: args.id } where: { id: args.id },
}) });
return true return true;
} catch (error) { } catch (error) {
console.error('Error deleting category:', error) console.error("Error deleting category:", error);
return false return false;
} }
}, },
// Добавить товар в корзину // Добавить товар в корзину
addToCart: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => { addToCart: async (
_: unknown,
args: { productId: string; quantity: number },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что товар существует и активен // Проверяем, что товар существует и активен
const product = await prisma.product.findFirst({ const product = await prisma.product.findFirst({
where: { where: {
id: args.productId, id: args.productId,
isActive: true isActive: true,
}, },
include: { include: {
organization: true organization: true,
} },
}) });
if (!product) { if (!product) {
return { return {
success: false, success: false,
message: 'Товар не найден или неактивен' message: "Товар не найден или неактивен",
} };
} }
// Проверяем, что пользователь не пытается добавить свой собственный товар // Проверяем, что пользователь не пытается добавить свой собственный товар
if (product.organizationId === currentUser.organization.id) { if (product.organizationId === currentUser.organization.id) {
return { return {
success: false, success: false,
message: 'Нельзя добавлять собственные товары в корзину' message: "Нельзя добавлять собственные товары в корзину",
} };
} }
// Найти или создать корзину // Найти или создать корзину
let cart = await prisma.cart.findUnique({ let cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id } where: { organizationId: currentUser.organization.id },
}) });
if (!cart) { if (!cart) {
cart = await prisma.cart.create({ cart = await prisma.cart.create({
data: { data: {
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
} },
}) });
} }
try { try {
@ -2952,42 +3361,42 @@ export const resolvers = {
where: { where: {
cartId_productId: { cartId_productId: {
cartId: cart.id, cartId: cart.id,
productId: args.productId productId: args.productId,
} },
} },
}) });
if (existingCartItem) { if (existingCartItem) {
// Обновляем количество // Обновляем количество
const newQuantity = existingCartItem.quantity + args.quantity const newQuantity = existingCartItem.quantity + args.quantity;
if (newQuantity > product.quantity) { if (newQuantity > product.quantity) {
return { return {
success: false, success: false,
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}` message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`,
} };
} }
await prisma.cartItem.update({ await prisma.cartItem.update({
where: { id: existingCartItem.id }, where: { id: existingCartItem.id },
data: { quantity: newQuantity } data: { quantity: newQuantity },
}) });
} else { } else {
// Создаем новый элемент корзины // Создаем новый элемент корзины
if (args.quantity > product.quantity) { if (args.quantity > product.quantity) {
return { return {
success: false, success: false,
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}` message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`,
} };
} }
await prisma.cartItem.create({ await prisma.cartItem.create({
data: { data: {
cartId: cart.id, cartId: cart.id,
productId: args.productId, productId: args.productId,
quantity: args.quantity quantity: args.quantity,
} },
}) });
} }
// Возвращаем обновленную корзину // Возвращаем обновленную корзину
@ -3001,57 +3410,61 @@ export const resolvers = {
category: true, category: true,
organization: { organization: {
include: { include: {
users: true users: true,
} },
} },
} },
} },
} },
}, },
organization: true organization: true,
} },
}) });
return { return {
success: true, success: true,
message: 'Товар добавлен в корзину', message: "Товар добавлен в корзину",
cart: updatedCart cart: updatedCart,
} };
} catch (error) { } catch (error) {
console.error('Error adding to cart:', error) console.error("Error adding to cart:", error);
return { return {
success: false, success: false,
message: 'Ошибка при добавлении в корзину' message: "Ошибка при добавлении в корзину",
} };
} }
}, },
// Обновить количество товара в корзине // Обновить количество товара в корзине
updateCartItem: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => { updateCartItem: async (
_: unknown,
args: { productId: string; quantity: number },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
const cart = await prisma.cart.findUnique({ const cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id } where: { organizationId: currentUser.organization.id },
}) });
if (!cart) { if (!cart) {
return { return {
success: false, success: false,
message: 'Корзина не найдена' message: "Корзина не найдена",
} };
} }
// Проверяем, что товар существует в корзине // Проверяем, что товар существует в корзине
@ -3059,40 +3472,40 @@ export const resolvers = {
where: { where: {
cartId_productId: { cartId_productId: {
cartId: cart.id, cartId: cart.id,
productId: args.productId productId: args.productId,
} },
}, },
include: { include: {
product: true product: true,
} },
}) });
if (!cartItem) { if (!cartItem) {
return { return {
success: false, success: false,
message: 'Товар не найден в корзине' message: "Товар не найден в корзине",
} };
} }
if (args.quantity <= 0) { if (args.quantity <= 0) {
return { return {
success: false, success: false,
message: 'Количество должно быть больше 0' message: "Количество должно быть больше 0",
} };
} }
if (args.quantity > cartItem.product.quantity) { if (args.quantity > cartItem.product.quantity) {
return { return {
success: false, success: false,
message: `Недостаточно товара в наличии. Доступно: ${cartItem.product.quantity}` message: `Недостаточно товара в наличии. Доступно: ${cartItem.product.quantity}`,
} };
} }
try { try {
await prisma.cartItem.update({ await prisma.cartItem.update({
where: { id: cartItem.id }, where: { id: cartItem.id },
data: { quantity: args.quantity } data: { quantity: args.quantity },
}) });
// Возвращаем обновленную корзину // Возвращаем обновленную корзину
const updatedCart = await prisma.cart.findUnique({ const updatedCart = await prisma.cart.findUnique({
@ -3105,57 +3518,61 @@ export const resolvers = {
category: true, category: true,
organization: { organization: {
include: { include: {
users: true users: true,
} },
} },
} },
} },
} },
}, },
organization: true organization: true,
} },
}) });
return { return {
success: true, success: true,
message: 'Количество товара обновлено', message: "Количество товара обновлено",
cart: updatedCart cart: updatedCart,
} };
} catch (error) { } catch (error) {
console.error('Error updating cart item:', error) console.error("Error updating cart item:", error);
return { return {
success: false, success: false,
message: 'Ошибка при обновлении корзины' message: "Ошибка при обновлении корзины",
} };
} }
}, },
// Удалить товар из корзины // Удалить товар из корзины
removeFromCart: async (_: unknown, args: { productId: string }, context: Context) => { removeFromCart: async (
_: unknown,
args: { productId: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
const cart = await prisma.cart.findUnique({ const cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id } where: { organizationId: currentUser.organization.id },
}) });
if (!cart) { if (!cart) {
return { return {
success: false, success: false,
message: 'Корзина не найдена' message: "Корзина не найдена",
} };
} }
try { try {
@ -3163,10 +3580,10 @@ export const resolvers = {
where: { where: {
cartId_productId: { cartId_productId: {
cartId: cart.id, cartId: cart.id,
productId: args.productId productId: args.productId,
} },
} },
}) });
// Возвращаем обновленную корзину // Возвращаем обновленную корзину
const updatedCart = await prisma.cart.findUnique({ const updatedCart = await prisma.cart.findUnique({
@ -3179,109 +3596,113 @@ export const resolvers = {
category: true, category: true,
organization: { organization: {
include: { include: {
users: true users: true,
} },
} },
} },
} },
} },
}, },
organization: true organization: true,
} },
}) });
return { return {
success: true, success: true,
message: 'Товар удален из корзины', message: "Товар удален из корзины",
cart: updatedCart cart: updatedCart,
} };
} catch (error) { } catch (error) {
console.error('Error removing from cart:', error) console.error("Error removing from cart:", error);
return { return {
success: false, success: false,
message: 'Ошибка при удалении из корзины' message: "Ошибка при удалении из корзины",
} };
} }
}, },
// Очистить корзину // Очистить корзину
clearCart: async (_: unknown, __: unknown, context: Context) => { clearCart: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
const cart = await prisma.cart.findUnique({ const cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id } where: { organizationId: currentUser.organization.id },
}) });
if (!cart) { if (!cart) {
return false return false;
} }
try { try {
await prisma.cartItem.deleteMany({ await prisma.cartItem.deleteMany({
where: { cartId: cart.id } where: { cartId: cart.id },
}) });
return true return true;
} catch (error) { } catch (error) {
console.error('Error clearing cart:', error) console.error("Error clearing cart:", error);
return false return false;
} }
}, },
// Добавить товар в избранное // Добавить товар в избранное
addToFavorites: async (_: unknown, args: { productId: string }, context: Context) => { addToFavorites: async (
_: unknown,
args: { productId: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
// Проверяем, что товар существует и активен // Проверяем, что товар существует и активен
const product = await prisma.product.findFirst({ const product = await prisma.product.findFirst({
where: { where: {
id: args.productId, id: args.productId,
isActive: true isActive: true,
}, },
include: { include: {
organization: true organization: true,
} },
}) });
if (!product) { if (!product) {
return { return {
success: false, success: false,
message: 'Товар не найден или неактивен' message: "Товар не найден или неактивен",
} };
} }
// Проверяем, что пользователь не пытается добавить свой собственный товар // Проверяем, что пользователь не пытается добавить свой собственный товар
if (product.organizationId === currentUser.organization.id) { if (product.organizationId === currentUser.organization.id) {
return { return {
success: false, success: false,
message: 'Нельзя добавлять собственные товары в избранное' message: "Нельзя добавлять собственные товары в избранное",
} };
} }
try { try {
@ -3290,25 +3711,25 @@ export const resolvers = {
where: { where: {
organizationId_productId: { organizationId_productId: {
organizationId: currentUser.organization.id, organizationId: currentUser.organization.id,
productId: args.productId productId: args.productId,
} },
} },
}) });
if (existingFavorite) { if (existingFavorite) {
return { return {
success: false, success: false,
message: 'Товар уже в избранном' message: "Товар уже в избранном",
} };
} }
// Добавляем товар в избранное // Добавляем товар в избранное
await prisma.favorites.create({ await prisma.favorites.create({
data: { data: {
organizationId: currentUser.organization.id, organizationId: currentUser.organization.id,
productId: args.productId productId: args.productId,
} },
}) });
// Возвращаем обновленный список избранного // Возвращаем обновленный список избранного
const favorites = await prisma.favorites.findMany({ const favorites = await prisma.favorites.findMany({
@ -3319,44 +3740,48 @@ export const resolvers = {
category: true, category: true,
organization: { organization: {
include: { include: {
users: true users: true,
} },
} },
} },
} },
}, },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: "desc" },
}) });
return { return {
success: true, success: true,
message: 'Товар добавлен в избранное', message: "Товар добавлен в избранное",
favorites: favorites.map(favorite => favorite.product) favorites: favorites.map((favorite) => favorite.product),
} };
} catch (error) { } catch (error) {
console.error('Error adding to favorites:', error) console.error("Error adding to favorites:", error);
return { return {
success: false, success: false,
message: 'Ошибка при добавлении в избранное' message: "Ошибка при добавлении в избранное",
} };
} }
}, },
// Удалить товар из избранного // Удалить товар из избранного
removeFromFavorites: async (_: unknown, args: { productId: string }, context: Context) => { removeFromFavorites: async (
_: unknown,
args: { productId: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
try { try {
@ -3364,9 +3789,9 @@ export const resolvers = {
await prisma.favorites.deleteMany({ await prisma.favorites.deleteMany({
where: { where: {
organizationId: currentUser.organization.id, organizationId: currentUser.organization.id,
productId: args.productId productId: args.productId,
} },
}) });
// Возвращаем обновленный список избранного // Возвращаем обновленный список избранного
const favorites = await prisma.favorites.findMany({ const favorites = await prisma.favorites.findMany({
@ -3377,48 +3802,52 @@ export const resolvers = {
category: true, category: true,
organization: { organization: {
include: { include: {
users: true users: true,
} },
} },
} },
} },
}, },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: "desc" },
}) });
return { return {
success: true, success: true,
message: 'Товар удален из избранного', message: "Товар удален из избранного",
favorites: favorites.map(favorite => favorite.product) favorites: favorites.map((favorite) => favorite.product),
} };
} catch (error) { } catch (error) {
console.error('Error removing from favorites:', error) console.error("Error removing from favorites:", error);
return { return {
success: false, success: false,
message: 'Ошибка при удалении из избранного' message: "Ошибка при удалении из избранного",
} };
} }
}, },
// Создать сотрудника // Создать сотрудника
createEmployee: async (_: unknown, args: { input: CreateEmployeeInput }, context: Context) => { createEmployee: async (
_: unknown,
args: { input: CreateEmployeeInput },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
if (currentUser.organization.type !== 'FULFILLMENT') { if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError('Доступно только для фулфилмент центров') throw new GraphQLError("Доступно только для фулфилмент центров");
} }
try { try {
@ -3426,136 +3855,158 @@ export const resolvers = {
data: { data: {
...args.input, ...args.input,
organizationId: currentUser.organization.id, organizationId: currentUser.organization.id,
birthDate: args.input.birthDate ? new Date(args.input.birthDate) : undefined, birthDate: args.input.birthDate
passportDate: args.input.passportDate ? new Date(args.input.passportDate) : undefined, ? new Date(args.input.birthDate)
hireDate: new Date(args.input.hireDate) : undefined,
passportDate: args.input.passportDate
? new Date(args.input.passportDate)
: undefined,
hireDate: new Date(args.input.hireDate),
}, },
include: { include: {
organization: true organization: true,
} },
}) });
return { return {
success: true, success: true,
message: 'Сотрудник успешно добавлен', message: "Сотрудник успешно добавлен",
employee employee,
} };
} catch (error) { } catch (error) {
console.error('Error creating employee:', error) console.error("Error creating employee:", error);
return { return {
success: false, success: false,
message: 'Ошибка при создании сотрудника' message: "Ошибка при создании сотрудника",
} };
} }
}, },
// Обновить сотрудника // Обновить сотрудника
updateEmployee: async (_: unknown, args: { id: string; input: UpdateEmployeeInput }, context: Context) => { updateEmployee: async (
_: unknown,
args: { id: string; input: UpdateEmployeeInput },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
if (currentUser.organization.type !== 'FULFILLMENT') { if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError('Доступно только для фулфилмент центров') throw new GraphQLError("Доступно только для фулфилмент центров");
} }
try { try {
const employee = await prisma.employee.update({ const employee = await prisma.employee.update({
where: { where: {
id: args.id, id: args.id,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
}, },
data: { data: {
...args.input, ...args.input,
birthDate: args.input.birthDate ? new Date(args.input.birthDate) : undefined, birthDate: args.input.birthDate
passportDate: args.input.passportDate ? new Date(args.input.passportDate) : undefined, ? new Date(args.input.birthDate)
hireDate: args.input.hireDate ? new Date(args.input.hireDate) : undefined : undefined,
passportDate: args.input.passportDate
? new Date(args.input.passportDate)
: undefined,
hireDate: args.input.hireDate
? new Date(args.input.hireDate)
: undefined,
}, },
include: { include: {
organization: true organization: true,
} },
}) });
return { return {
success: true, success: true,
message: 'Сотрудник успешно обновлен', message: "Сотрудник успешно обновлен",
employee employee,
} };
} catch (error) { } catch (error) {
console.error('Error updating employee:', error) console.error("Error updating employee:", error);
return { return {
success: false, success: false,
message: 'Ошибка при обновлении сотрудника' message: "Ошибка при обновлении сотрудника",
} };
} }
}, },
// Удалить сотрудника // Удалить сотрудника
deleteEmployee: async (_: unknown, args: { id: string }, context: Context) => { deleteEmployee: async (
_: unknown,
args: { id: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
if (currentUser.organization.type !== 'FULFILLMENT') { if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError('Доступно только для фулфилмент центров') throw new GraphQLError("Доступно только для фулфилмент центров");
} }
try { try {
await prisma.employee.delete({ await prisma.employee.delete({
where: { where: {
id: args.id, id: args.id,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
} },
}) });
return true return true;
} catch (error) { } catch (error) {
console.error('Error deleting employee:', error) console.error("Error deleting employee:", error);
return false return false;
} }
}, },
// Обновить табель сотрудника // Обновить табель сотрудника
updateEmployeeSchedule: async (_: unknown, args: { input: UpdateScheduleInput }, context: Context) => { updateEmployeeSchedule: async (
_: unknown,
args: { input: UpdateScheduleInput },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
if (currentUser.organization.type !== 'FULFILLMENT') { if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError('Доступно только для фулфилмент центров') throw new GraphQLError("Доступно только для фулфилмент центров");
} }
try { try {
@ -3563,12 +4014,12 @@ export const resolvers = {
const employee = await prisma.employee.findFirst({ const employee = await prisma.employee.findFirst({
where: { where: {
id: args.input.employeeId, id: args.input.employeeId,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
} },
}) });
if (!employee) { if (!employee) {
throw new GraphQLError('Сотрудник не найден') throw new GraphQLError("Сотрудник не найден");
} }
// Создаем или обновляем запись табеля // Создаем или обновляем запись табеля
@ -3576,29 +4027,29 @@ export const resolvers = {
where: { where: {
employeeId_date: { employeeId_date: {
employeeId: args.input.employeeId, employeeId: args.input.employeeId,
date: new Date(args.input.date) date: new Date(args.input.date),
} },
}, },
create: { create: {
employeeId: args.input.employeeId, employeeId: args.input.employeeId,
date: new Date(args.input.date), date: new Date(args.input.date),
status: args.input.status, status: args.input.status,
hoursWorked: args.input.hoursWorked, hoursWorked: args.input.hoursWorked,
notes: args.input.notes notes: args.input.notes,
}, },
update: { update: {
status: args.input.status, status: args.input.status,
hoursWorked: args.input.hoursWorked, hoursWorked: args.input.hoursWorked,
notes: args.input.notes notes: args.input.notes,
} },
}) });
return true return true;
} catch (error) { } catch (error) {
console.error('Error updating employee schedule:', error) console.error("Error updating employee schedule:", error);
return false return false;
} }
} },
}, },
// Резолверы типов // Резолверы типов
@ -3606,44 +4057,54 @@ export const resolvers = {
users: async (parent: { id: string; users?: unknown[] }) => { users: async (parent: { id: string; users?: unknown[] }) => {
// Если пользователи уже загружены через include, возвращаем их // Если пользователи уже загружены через include, возвращаем их
if (parent.users) { if (parent.users) {
return parent.users return parent.users;
} }
// Иначе загружаем отдельно // Иначе загружаем отдельно
return await prisma.user.findMany({ return await prisma.user.findMany({
where: { organizationId: parent.id } where: { organizationId: parent.id },
}) });
} },
}, },
Cart: { Cart: {
totalPrice: (parent: { items: Array<{ product: { price: number }, quantity: number }> }) => { totalPrice: (parent: {
items: Array<{ product: { price: number }; quantity: number }>;
}) => {
return parent.items.reduce((total, item) => { return parent.items.reduce((total, item) => {
return total + (Number(item.product.price) * item.quantity) return total + Number(item.product.price) * item.quantity;
}, 0) }, 0);
}, },
totalItems: (parent: { items: Array<{ quantity: number }> }) => { totalItems: (parent: { items: Array<{ quantity: number }> }) => {
return parent.items.reduce((total, item) => total + item.quantity, 0) return parent.items.reduce((total, item) => total + item.quantity, 0);
} },
}, },
CartItem: { CartItem: {
totalPrice: (parent: { product: { price: number }, quantity: number }) => { totalPrice: (parent: { product: { price: number }; quantity: number }) => {
return Number(parent.product.price) * parent.quantity return Number(parent.product.price) * parent.quantity;
}, },
isAvailable: (parent: { product: { quantity: number, isActive: boolean }, quantity: number }) => { isAvailable: (parent: {
return parent.product.isActive && parent.product.quantity >= parent.quantity product: { quantity: number; isActive: boolean };
quantity: number;
}) => {
return (
parent.product.isActive && parent.product.quantity >= parent.quantity
);
}, },
availableQuantity: (parent: { product: { quantity: number } }) => { availableQuantity: (parent: { product: { quantity: number } }) => {
return parent.product.quantity return parent.product.quantity;
} },
}, },
User: { User: {
organization: async (parent: { organizationId?: string; organization?: unknown }) => { organization: async (parent: {
organizationId?: string;
organization?: unknown;
}) => {
// Если организация уже загружена через include, возвращаем её // Если организация уже загружена через include, возвращаем её
if (parent.organization) { if (parent.organization) {
return parent.organization return parent.organization;
} }
// Иначе загружаем отдельно если есть organizationId // Иначе загружаем отдельно если есть organizationId
@ -3652,112 +4113,124 @@ export const resolvers = {
where: { id: parent.organizationId }, where: { id: parent.organizationId },
include: { include: {
apiKeys: true, apiKeys: true,
users: true users: true,
} },
}) });
} }
return null return null;
} },
}, },
Message: { Message: {
type: (parent: { type?: string | null }) => { type: (parent: { type?: string | null }) => {
return parent.type || 'TEXT' return parent.type || "TEXT";
}, },
createdAt: (parent: { createdAt: Date | string }) => { createdAt: (parent: { createdAt: Date | string }) => {
if (parent.createdAt instanceof Date) { if (parent.createdAt instanceof Date) {
return parent.createdAt.toISOString() return parent.createdAt.toISOString();
} }
return parent.createdAt return parent.createdAt;
}, },
updatedAt: (parent: { updatedAt: Date | string }) => { updatedAt: (parent: { updatedAt: Date | string }) => {
if (parent.updatedAt instanceof Date) { if (parent.updatedAt instanceof Date) {
return parent.updatedAt.toISOString() return parent.updatedAt.toISOString();
} }
return parent.updatedAt return parent.updatedAt;
} },
}, },
Employee: { Employee: {
birthDate: (parent: { birthDate?: Date | string | null }) => { birthDate: (parent: { birthDate?: Date | string | null }) => {
if (!parent.birthDate) return null if (!parent.birthDate) return null;
if (parent.birthDate instanceof Date) { if (parent.birthDate instanceof Date) {
return parent.birthDate.toISOString() return parent.birthDate.toISOString();
} }
return parent.birthDate return parent.birthDate;
}, },
passportDate: (parent: { passportDate?: Date | string | null }) => { passportDate: (parent: { passportDate?: Date | string | null }) => {
if (!parent.passportDate) return null if (!parent.passportDate) return null;
if (parent.passportDate instanceof Date) { if (parent.passportDate instanceof Date) {
return parent.passportDate.toISOString() return parent.passportDate.toISOString();
} }
return parent.passportDate return parent.passportDate;
}, },
hireDate: (parent: { hireDate: Date | string }) => { hireDate: (parent: { hireDate: Date | string }) => {
if (parent.hireDate instanceof Date) { if (parent.hireDate instanceof Date) {
return parent.hireDate.toISOString() return parent.hireDate.toISOString();
} }
return parent.hireDate return parent.hireDate;
}, },
createdAt: (parent: { createdAt: Date | string }) => { createdAt: (parent: { createdAt: Date | string }) => {
if (parent.createdAt instanceof Date) { if (parent.createdAt instanceof Date) {
return parent.createdAt.toISOString() return parent.createdAt.toISOString();
} }
return parent.createdAt return parent.createdAt;
}, },
updatedAt: (parent: { updatedAt: Date | string }) => { updatedAt: (parent: { updatedAt: Date | string }) => {
if (parent.updatedAt instanceof Date) { if (parent.updatedAt instanceof Date) {
return parent.updatedAt.toISOString() return parent.updatedAt.toISOString();
} }
return parent.updatedAt return parent.updatedAt;
} },
}, },
EmployeeSchedule: { EmployeeSchedule: {
date: (parent: { date: Date | string }) => { date: (parent: { date: Date | string }) => {
if (parent.date instanceof Date) { if (parent.date instanceof Date) {
return parent.date.toISOString() return parent.date.toISOString();
} }
return parent.date return parent.date;
}, },
createdAt: (parent: { createdAt: Date | string }) => { createdAt: (parent: { createdAt: Date | string }) => {
if (parent.createdAt instanceof Date) { if (parent.createdAt instanceof Date) {
return parent.createdAt.toISOString() return parent.createdAt.toISOString();
} }
return parent.createdAt return parent.createdAt;
}, },
updatedAt: (parent: { updatedAt: Date | string }) => { updatedAt: (parent: { updatedAt: Date | string }) => {
if (parent.updatedAt instanceof Date) { if (parent.updatedAt instanceof Date) {
return parent.updatedAt.toISOString() return parent.updatedAt.toISOString();
} }
return parent.updatedAt return parent.updatedAt;
}, },
employee: async (parent: { employeeId: string }) => { employee: async (parent: { employeeId: string }) => {
return await prisma.employee.findUnique({ return await prisma.employee.findUnique({
where: { id: parent.employeeId } where: { id: parent.employeeId },
}) });
} },
} },
} };
// Логистические мутации // Логистические мутации
const logisticsMutations = { const logisticsMutations = {
// Создать логистический маршрут // Создать логистический маршрут
createLogistics: async (_: unknown, args: { input: { fromLocation: string; toLocation: string; priceUnder1m3: number; priceOver1m3: number; description?: string } }, context: Context) => { createLogistics: async (
_: unknown,
args: {
input: {
fromLocation: string;
toLocation: string;
priceUnder1m3: number;
priceOver1m3: number;
description?: string;
};
},
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
try { try {
@ -3768,44 +4241,57 @@ const logisticsMutations = {
priceUnder1m3: args.input.priceUnder1m3, priceUnder1m3: args.input.priceUnder1m3,
priceOver1m3: args.input.priceOver1m3, priceOver1m3: args.input.priceOver1m3,
description: args.input.description, description: args.input.description,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
}, },
include: { include: {
organization: true organization: true,
} },
}) });
console.log('✅ Logistics created:', logistics.id) console.log("✅ Logistics created:", logistics.id);
return { return {
success: true, success: true,
message: 'Логистический маршрут создан', message: "Логистический маршрут создан",
logistics logistics,
} };
} catch (error) { } catch (error) {
console.error('❌ Error creating logistics:', error) console.error("❌ Error creating logistics:", error);
return { return {
success: false, success: false,
message: 'Ошибка при создании логистического маршрута' message: "Ошибка при создании логистического маршрута",
} };
} }
}, },
// Обновить логистический маршрут // Обновить логистический маршрут
updateLogistics: async (_: unknown, args: { id: string; input: { fromLocation: string; toLocation: string; priceUnder1m3: number; priceOver1m3: number; description?: string } }, context: Context) => { updateLogistics: async (
_: unknown,
args: {
id: string;
input: {
fromLocation: string;
toLocation: string;
priceUnder1m3: number;
priceOver1m3: number;
description?: string;
};
},
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
try { try {
@ -3813,12 +4299,12 @@ const logisticsMutations = {
const existingLogistics = await prisma.logistics.findFirst({ const existingLogistics = await prisma.logistics.findFirst({
where: { where: {
id: args.id, id: args.id,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
} },
}) });
if (!existingLogistics) { if (!existingLogistics) {
throw new GraphQLError('Логистический маршрут не найден') throw new GraphQLError("Логистический маршрут не найден");
} }
const logistics = await prisma.logistics.update({ const logistics = await prisma.logistics.update({
@ -3828,44 +4314,48 @@ const logisticsMutations = {
toLocation: args.input.toLocation, toLocation: args.input.toLocation,
priceUnder1m3: args.input.priceUnder1m3, priceUnder1m3: args.input.priceUnder1m3,
priceOver1m3: args.input.priceOver1m3, priceOver1m3: args.input.priceOver1m3,
description: args.input.description description: args.input.description,
}, },
include: { include: {
organization: true organization: true,
} },
}) });
console.log('✅ Logistics updated:', logistics.id) console.log("✅ Logistics updated:", logistics.id);
return { return {
success: true, success: true,
message: 'Логистический маршрут обновлен', message: "Логистический маршрут обновлен",
logistics logistics,
} };
} catch (error) { } catch (error) {
console.error('❌ Error updating logistics:', error) console.error("❌ Error updating logistics:", error);
return { return {
success: false, success: false,
message: 'Ошибка при обновлении логистического маршрута' message: "Ошибка при обновлении логистического маршрута",
} };
} }
}, },
// Удалить логистический маршрут // Удалить логистический маршрут
deleteLogistics: async (_: unknown, args: { id: string }, context: Context) => { deleteLogistics: async (
_: unknown,
args: { id: string },
context: Context
) => {
if (!context.user) { if (!context.user) {
throw new GraphQLError('Требуется авторизация', { throw new GraphQLError("Требуется авторизация", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const currentUser = await prisma.user.findUnique({ const currentUser = await prisma.user.findUnique({
where: { id: context.user.id }, where: { id: context.user.id },
include: { organization: true } include: { organization: true },
}) });
if (!currentUser?.organization) { if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации') throw new GraphQLError("У пользователя нет организации");
} }
try { try {
@ -3873,192 +4363,202 @@ const logisticsMutations = {
const existingLogistics = await prisma.logistics.findFirst({ const existingLogistics = await prisma.logistics.findFirst({
where: { where: {
id: args.id, id: args.id,
organizationId: currentUser.organization.id organizationId: currentUser.organization.id,
} },
}) });
if (!existingLogistics) { if (!existingLogistics) {
throw new GraphQLError('Логистический маршрут не найден') throw new GraphQLError("Логистический маршрут не найден");
} }
await prisma.logistics.delete({ await prisma.logistics.delete({
where: { id: args.id } where: { id: args.id },
}) });
console.log('✅ Logistics deleted:', args.id) console.log("✅ Logistics deleted:", args.id);
return true return true;
} catch (error) { } catch (error) {
console.error('❌ Error deleting logistics:', error) console.error("❌ Error deleting logistics:", error);
return false return false;
} }
} },
} };
// Добавляем логистические мутации к основным резолверам // Добавляем логистические мутации к основным резолверам
resolvers.Mutation = { resolvers.Mutation = {
...resolvers.Mutation, ...resolvers.Mutation,
...logisticsMutations ...logisticsMutations,
} };
// Админ резолверы // Админ резолверы
const adminQueries = { const adminQueries = {
adminMe: async (_: unknown, __: unknown, context: Context) => { adminMe: async (_: unknown, __: unknown, context: Context) => {
if (!context.admin) { if (!context.admin) {
throw new GraphQLError('Требуется авторизация администратора', { throw new GraphQLError("Требуется авторизация администратора", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const admin = await prisma.admin.findUnique({ const admin = await prisma.admin.findUnique({
where: { id: context.admin.id } where: { id: context.admin.id },
}) });
if (!admin) { if (!admin) {
throw new GraphQLError('Администратор не найден') throw new GraphQLError("Администратор не найден");
} }
return admin return admin;
}, },
allUsers: async (_: unknown, args: { search?: string; limit?: number; offset?: number }, context: Context) => { allUsers: async (
_: unknown,
args: { search?: string; limit?: number; offset?: number },
context: Context
) => {
if (!context.admin) { if (!context.admin) {
throw new GraphQLError('Требуется авторизация администратора', { throw new GraphQLError("Требуется авторизация администратора", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
const limit = args.limit || 50 const limit = args.limit || 50;
const offset = args.offset || 0 const offset = args.offset || 0;
// Строим условие поиска // Строим условие поиска
const whereCondition: Prisma.UserWhereInput = args.search const whereCondition: Prisma.UserWhereInput = args.search
? { ? {
OR: [ OR: [
{ phone: { contains: args.search, mode: 'insensitive' } }, { phone: { contains: args.search, mode: "insensitive" } },
{ managerName: { contains: args.search, mode: 'insensitive' } }, { managerName: { contains: args.search, mode: "insensitive" } },
{ {
organization: { organization: {
OR: [ OR: [
{ name: { contains: args.search, mode: 'insensitive' } }, { name: { contains: args.search, mode: "insensitive" } },
{ fullName: { contains: args.search, mode: 'insensitive' } }, { fullName: { contains: args.search, mode: "insensitive" } },
{ inn: { contains: args.search, mode: 'insensitive' } } { inn: { contains: args.search, mode: "insensitive" } },
] ],
} },
} },
] ],
} }
: {} : {};
// Получаем пользователей с пагинацией // Получаем пользователей с пагинацией
const [users, total] = await Promise.all([ const [users, total] = await Promise.all([
prisma.user.findMany({ prisma.user.findMany({
where: whereCondition, where: whereCondition,
include: { include: {
organization: true organization: true,
}, },
take: limit, take: limit,
skip: offset, skip: offset,
orderBy: { createdAt: 'desc' } orderBy: { createdAt: "desc" },
}), }),
prisma.user.count({ prisma.user.count({
where: whereCondition where: whereCondition,
}) }),
]) ]);
return { return {
users, users,
total, total,
hasMore: offset + limit < total hasMore: offset + limit < total,
} };
} },
} };
const adminMutations = { const adminMutations = {
adminLogin: async (_: unknown, args: { username: string; password: string }) => { adminLogin: async (
_: unknown,
args: { username: string; password: string }
) => {
try { try {
// Найти администратора // Найти администратора
const admin = await prisma.admin.findUnique({ const admin = await prisma.admin.findUnique({
where: { username: args.username } where: { username: args.username },
}) });
if (!admin) { if (!admin) {
return { return {
success: false, success: false,
message: 'Неверные учетные данные' message: "Неверные учетные данные",
} };
} }
// Проверить активность // Проверить активность
if (!admin.isActive) { if (!admin.isActive) {
return { return {
success: false, success: false,
message: 'Аккаунт заблокирован' message: "Аккаунт заблокирован",
} };
} }
// Проверить пароль // Проверить пароль
const isPasswordValid = await bcrypt.compare(args.password, admin.password) const isPasswordValid = await bcrypt.compare(
args.password,
admin.password
);
if (!isPasswordValid) { if (!isPasswordValid) {
return { return {
success: false, success: false,
message: 'Неверные учетные данные' message: "Неверные учетные данные",
} };
} }
// Обновить время последнего входа // Обновить время последнего входа
await prisma.admin.update({ await prisma.admin.update({
where: { id: admin.id }, where: { id: admin.id },
data: { lastLogin: new Date() } data: { lastLogin: new Date() },
}) });
// Создать токен // Создать токен
const token = jwt.sign( const token = jwt.sign(
{ {
adminId: admin.id, adminId: admin.id,
username: admin.username, username: admin.username,
type: 'admin' type: "admin",
}, },
process.env.JWT_SECRET!, process.env.JWT_SECRET!,
{ expiresIn: '24h' } { expiresIn: "24h" }
) );
return { return {
success: true, success: true,
message: 'Успешная авторизация', message: "Успешная авторизация",
token, token,
admin: { admin: {
...admin, ...admin,
password: undefined // Не возвращаем пароль password: undefined, // Не возвращаем пароль
} },
} };
} catch (error) { } catch (error) {
console.error('Admin login error:', error) console.error("Admin login error:", error);
return { return {
success: false, success: false,
message: 'Ошибка авторизации' message: "Ошибка авторизации",
} };
} }
}, },
adminLogout: async (_: unknown, __: unknown, context: Context) => { adminLogout: async (_: unknown, __: unknown, context: Context) => {
if (!context.admin) { if (!context.admin) {
throw new GraphQLError('Требуется авторизация администратора', { throw new GraphQLError("Требуется авторизация администратора", {
extensions: { code: 'UNAUTHENTICATED' } extensions: { code: "UNAUTHENTICATED" },
}) });
} }
return true return true;
} },
} };
// Добавляем админ запросы и мутации к основным резолверам // Добавляем админ запросы и мутации к основным резолверам
resolvers.Query = { resolvers.Query = {
...resolvers.Query, ...resolvers.Query,
...adminQueries ...adminQueries,
} };
resolvers.Mutation = { resolvers.Mutation = {
...resolvers.Mutation, ...resolvers.Mutation,
...adminMutations ...adminMutations,
} };

View File

@ -1,4 +1,4 @@
import { gql } from 'graphql-tag' import { gql } from "graphql-tag";
export const typeDefs = gql` export const typeDefs = gql`
scalar DateTime scalar DateTime
@ -8,7 +8,10 @@ export const typeDefs = gql`
organization(id: ID!): Organization organization(id: ID!): Organization
# Поиск организаций по типу для добавления в контрагенты # Поиск организаций по типу для добавления в контрагенты
searchOrganizations(type: OrganizationType, search: String): [Organization!]! searchOrganizations(
type: OrganizationType
search: String
): [Organization!]!
# Мои контрагенты # Мои контрагенты
myCounterparties: [Organization!]! myCounterparties: [Organization!]!
@ -54,7 +57,11 @@ export const typeDefs = gql`
employee(id: ID!): Employee employee(id: ID!): Employee
# Табель сотрудника за месяц # Табель сотрудника за месяц
employeeSchedule(employeeId: ID!, year: Int!, month: Int!): [EmployeeSchedule!]! employeeSchedule(
employeeId: ID!
year: Int!
month: Int!
): [EmployeeSchedule!]!
# Админ запросы # Админ запросы
adminMe: Admin adminMe: Admin
@ -70,13 +77,17 @@ export const typeDefs = gql`
verifyInn(inn: String!): InnValidationResponse! verifyInn(inn: String!): InnValidationResponse!
# Обновление профиля пользователя # Обновление профиля пользователя
updateUserProfile(input: UpdateUserProfileInput!): UpdateUserProfileResponse! updateUserProfile(
input: UpdateUserProfileInput!
): UpdateUserProfileResponse!
# Обновление данных организации по ИНН # Обновление данных организации по ИНН
updateOrganizationByInn(inn: String!): UpdateOrganizationResponse! updateOrganizationByInn(inn: String!): UpdateOrganizationResponse!
# Регистрация организации # Регистрация организации
registerFulfillmentOrganization(input: FulfillmentRegistrationInput!): AuthResponse! registerFulfillmentOrganization(
input: FulfillmentRegistrationInput!
): AuthResponse!
registerSellerOrganization(input: SellerRegistrationInput!): AuthResponse! registerSellerOrganization(input: SellerRegistrationInput!): AuthResponse!
# Работа с API ключами # Работа с API ключами
@ -87,16 +98,42 @@ export const typeDefs = gql`
logout: Boolean! logout: Boolean!
# Работа с контрагентами # Работа с контрагентами
sendCounterpartyRequest(organizationId: ID!, message: String): CounterpartyRequestResponse! sendCounterpartyRequest(
respondToCounterpartyRequest(requestId: ID!, accept: Boolean!): CounterpartyRequestResponse! organizationId: ID!
message: String
): CounterpartyRequestResponse!
respondToCounterpartyRequest(
requestId: ID!
accept: Boolean!
): CounterpartyRequestResponse!
cancelCounterpartyRequest(requestId: ID!): Boolean! cancelCounterpartyRequest(requestId: ID!): Boolean!
removeCounterparty(organizationId: ID!): Boolean! removeCounterparty(organizationId: ID!): Boolean!
# Работа с сообщениями # Работа с сообщениями
sendMessage(receiverOrganizationId: ID!, content: String, type: MessageType = TEXT): MessageResponse! sendMessage(
sendVoiceMessage(receiverOrganizationId: ID!, voiceUrl: String!, voiceDuration: Int!): MessageResponse! receiverOrganizationId: ID!
sendImageMessage(receiverOrganizationId: ID!, fileUrl: String!, fileName: String!, fileSize: Int!, fileType: String!): MessageResponse! content: String
sendFileMessage(receiverOrganizationId: ID!, fileUrl: String!, fileName: String!, fileSize: Int!, fileType: String!): MessageResponse! type: MessageType = TEXT
): MessageResponse!
sendVoiceMessage(
receiverOrganizationId: ID!
voiceUrl: String!
voiceDuration: Int!
): MessageResponse!
sendImageMessage(
receiverOrganizationId: ID!
fileUrl: String!
fileName: String!
fileSize: Int!
fileType: String!
): MessageResponse!
sendFileMessage(
receiverOrganizationId: ID!
fileUrl: String!
fileName: String!
fileSize: Int!
fileType: String!
): MessageResponse!
markMessagesAsRead(conversationId: ID!): Boolean! markMessagesAsRead(conversationId: ID!): Boolean!
# Работа с услугами # Работа с услугами
@ -109,6 +146,9 @@ export const typeDefs = gql`
updateSupply(id: ID!, input: SupplyInput!): SupplyResponse! updateSupply(id: ID!, input: SupplyInput!): SupplyResponse!
deleteSupply(id: ID!): Boolean! deleteSupply(id: ID!): Boolean!
# Заказы поставок расходников
createSupplyOrder(input: SupplyOrderInput!): SupplyOrderResponse!
# Работа с логистикой # Работа с логистикой
createLogistics(input: LogisticsInput!): LogisticsResponse! createLogistics(input: LogisticsInput!): LogisticsResponse!
updateLogistics(id: ID!, input: LogisticsInput!): LogisticsResponse! updateLogistics(id: ID!, input: LogisticsInput!): LogisticsResponse!
@ -414,6 +454,55 @@ export const typeDefs = gql`
supply: Supply supply: Supply
} }
# Типы для заказов поставок расходников
type SupplyOrder {
id: ID!
partnerId: ID!
partner: Organization!
deliveryDate: DateTime!
status: SupplyOrderStatus!
totalAmount: Float!
totalItems: Int!
items: [SupplyOrderItem!]!
createdAt: DateTime!
updatedAt: DateTime!
organization: Organization!
}
type SupplyOrderItem {
id: ID!
productId: ID!
product: Product!
quantity: Int!
price: Float!
totalPrice: Float!
}
enum SupplyOrderStatus {
PENDING
CONFIRMED
IN_TRANSIT
DELIVERED
CANCELLED
}
input SupplyOrderInput {
partnerId: ID!
deliveryDate: DateTime!
items: [SupplyOrderItemInput!]!
}
input SupplyOrderItemInput {
productId: ID!
quantity: Int!
}
type SupplyOrderResponse {
success: Boolean!
message: String!
order: SupplyOrder
}
# Типы для логистики # Типы для логистики
type Logistics { type Logistics {
id: ID! id: ID!
@ -693,4 +782,4 @@ export const typeDefs = gql`
total: Int! total: Int!
hasMore: Boolean! hasMore: Boolean!
} }
` `;