Compare commits

...

3 Commits

7 changed files with 3049 additions and 1702 deletions

View File

@ -97,6 +97,8 @@ model Organization {
supplies Supply[]
users User[]
logistics Logistics[]
supplyOrders SupplyOrder[]
partnerSupplyOrders SupplyOrder[] @relation("SupplyOrderPartner")
wildberriesSupplies WildberriesSupply[]
@@map("organizations")
@ -230,6 +232,7 @@ model Product {
organizationId String
cartItems CartItem[]
favorites Favorites[]
supplyOrderItems SupplyOrderItem[]
category Category? @relation(fields: [categoryId], references: [id])
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
@ -404,6 +407,14 @@ enum ScheduleStatus {
ABSENT
}
enum SupplyOrderStatus {
PENDING
CONFIRMED
IN_TRANSIT
DELIVERED
CANCELLED
}
enum WildberriesSupplyStatus {
DRAFT
CREATED
@ -426,3 +437,36 @@ model 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>
<Button
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"
>
<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`
mutation CreateLogistics($input: LogisticsInput!) {

View File

@ -1,190 +1,198 @@
import jwt from 'jsonwebtoken'
import bcrypt from 'bcryptjs'
import { GraphQLError } from 'graphql'
import { GraphQLScalarType, Kind } from 'graphql'
import { prisma } from '@/lib/prisma'
import { SmsService } from '@/services/sms-service'
import { DaDataService } from '@/services/dadata-service'
import { MarketplaceService } from '@/services/marketplace-service'
import { Prisma } from '@prisma/client'
import jwt from "jsonwebtoken";
import bcrypt from "bcryptjs";
import { GraphQLError } from "graphql";
import { GraphQLScalarType, Kind } from "graphql";
import { prisma } from "@/lib/prisma";
import { SmsService } from "@/services/sms-service";
import { DaDataService } from "@/services/dadata-service";
import { MarketplaceService } from "@/services/marketplace-service";
import { Prisma } from "@prisma/client";
// Сервисы
const smsService = new SmsService()
const dadataService = new DaDataService()
const marketplaceService = new MarketplaceService()
const smsService = new SmsService();
const dadataService = new DaDataService();
const marketplaceService = new MarketplaceService();
// Интерфейсы для типизации
interface Context {
user?: {
id: string
phone: string
}
id: string;
phone: string;
};
admin?: {
id: string
username: string
}
id: string;
username: string;
};
}
interface CreateEmployeeInput {
firstName: string
lastName: string
middleName?: string
birthDate?: string
avatar?: string
passportPhoto?: string
passportSeries?: string
passportNumber?: string
passportIssued?: string
passportDate?: string
address?: string
position: string
department?: string
hireDate: string
salary?: number
phone: string
email?: string
telegram?: string
whatsapp?: string
emergencyContact?: string
emergencyPhone?: string
firstName: string;
lastName: string;
middleName?: string;
birthDate?: string;
avatar?: string;
passportPhoto?: string;
passportSeries?: string;
passportNumber?: string;
passportIssued?: string;
passportDate?: string;
address?: string;
position: string;
department?: string;
hireDate: string;
salary?: number;
phone: string;
email?: string;
telegram?: string;
whatsapp?: string;
emergencyContact?: string;
emergencyPhone?: string;
}
interface UpdateEmployeeInput {
firstName?: string
lastName?: string
middleName?: string
birthDate?: string
avatar?: string
passportPhoto?: string
passportSeries?: string
passportNumber?: string
passportIssued?: string
passportDate?: string
address?: string
position?: string
department?: string
hireDate?: string
salary?: number
status?: 'ACTIVE' | 'VACATION' | 'SICK' | 'FIRED'
phone?: string
email?: string
telegram?: string
whatsapp?: string
emergencyContact?: string
emergencyPhone?: string
firstName?: string;
lastName?: string;
middleName?: string;
birthDate?: string;
avatar?: string;
passportPhoto?: string;
passportSeries?: string;
passportNumber?: string;
passportIssued?: string;
passportDate?: string;
address?: string;
position?: string;
department?: string;
hireDate?: string;
salary?: number;
status?: "ACTIVE" | "VACATION" | "SICK" | "FIRED";
phone?: string;
email?: string;
telegram?: string;
whatsapp?: string;
emergencyContact?: string;
emergencyPhone?: string;
}
interface UpdateScheduleInput {
employeeId: string
date: string
status: 'WORK' | 'WEEKEND' | 'VACATION' | 'SICK' | 'ABSENT'
hoursWorked?: number
notes?: string
employeeId: string;
date: string;
status: "WORK" | "WEEKEND" | "VACATION" | "SICK" | "ABSENT";
hoursWorked?: number;
notes?: string;
}
interface AuthTokenPayload {
userId: string
phone: string
userId: string;
phone: string;
}
// JWT утилиты
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
const verifyToken = (token: string): AuthTokenPayload => {
try {
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
throw new GraphQLError('Недействительный токен', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Недействительный токен", {
extensions: { code: "UNAUTHENTICATED" },
});
}
}
};
// Скалярный тип для JSON
const JSONScalar = new GraphQLScalarType({
name: 'JSON',
description: 'JSON custom scalar type',
name: "JSON",
description: "JSON custom scalar type",
serialize(value: unknown) {
return value // значение отправляется клиенту
return value; // значение отправляется клиенту
},
parseValue(value: unknown) {
return value // значение получено от клиента
return value; // значение получено от клиента
},
parseLiteral(ast) {
switch (ast.kind) {
case Kind.STRING:
case Kind.BOOLEAN:
return ast.value
return ast.value;
case Kind.INT:
case Kind.FLOAT:
return parseFloat(ast.value)
return parseFloat(ast.value);
case Kind.OBJECT: {
const value = Object.create(null)
ast.fields.forEach(field => {
value[field.name.value] = parseLiteral(field.value)
})
return value
const value = Object.create(null);
ast.fields.forEach((field) => {
value[field.name.value] = parseLiteral(field.value);
});
return value;
}
case Kind.LIST:
return ast.values.map(parseLiteral)
return ast.values.map(parseLiteral);
default:
return null
return null;
}
}
})
},
});
// Скалярный тип для DateTime
const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
description: 'DateTime custom scalar type',
name: "DateTime",
description: "DateTime custom scalar type",
serialize(value: unknown) {
if (value instanceof Date) {
return value.toISOString() // значение отправляется клиенту как ISO строка
return value.toISOString(); // значение отправляется клиенту как ISO строка
}
return value
return value;
},
parseValue(value: unknown) {
if (typeof value === 'string') {
return new Date(value) // значение получено от клиента, парсим как дату
if (typeof value === "string") {
return new Date(value); // значение получено от клиента, парсим как дату
}
return value
return value;
},
parseLiteral(ast) {
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 {
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) {
case Kind.STRING:
case Kind.BOOLEAN:
return astNode.value
return astNode.value;
case Kind.INT:
case Kind.FLOAT:
return parseFloat(astNode.value as string)
return parseFloat(astNode.value as string);
case Kind.OBJECT: {
const value = Object.create(null)
const value = Object.create(null);
if (astNode.fields) {
astNode.fields.forEach((field: unknown) => {
const fieldNode = field as { name: { value: string }; value: unknown }
value[fieldNode.name.value] = parseLiteral(fieldNode.value)
})
const fieldNode = field as {
name: { value: string };
value: unknown;
};
value[fieldNode.name.value] = parseLiteral(fieldNode.value);
});
}
return value
return value;
}
case Kind.LIST:
return (ast as { values: unknown[] }).values.map(parseLiteral)
return (ast as { values: unknown[] }).values.map(parseLiteral);
default:
return null
return null;
}
}
@ -195,9 +203,9 @@ export const resolvers = {
Query: {
me: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
return await prisma.user.findUnique({
@ -205,497 +213,546 @@ export const resolvers = {
include: {
organization: {
include: {
apiKeys: true
}
}
}
})
apiKeys: true,
},
},
},
});
},
organization: async (_: unknown, args: { id: string }, context: Context) => {
organization: async (
_: unknown,
args: { id: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const organization = await prisma.organization.findUnique({
where: { id: args.id },
include: {
apiKeys: true,
users: true
}
})
users: true,
},
});
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) {
throw new GraphQLError('Нет доступа к этой организации', {
extensions: { code: 'FORBIDDEN' }
})
throw new GraphQLError("Нет доступа к этой организации", {
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
// Получаем текущую организацию пользователя
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Получаем уже существующих контрагентов для добавления флага
const existingCounterparties = await prisma.counterparty.findMany({
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
const outgoingRequests = await prisma.counterpartyRequest.findMany({
where: {
where: {
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
const incomingRequests = await prisma.counterpartyRequest.findMany({
where: {
where: {
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> = {
// Больше не исключаем собственную организацию
}
};
if (args.type) {
where.type = args.type
where.type = args.type;
}
if (args.search) {
where.OR = [
{ name: { contains: args.search, mode: 'insensitive' } },
{ fullName: { contains: args.search, mode: 'insensitive' } },
{ inn: { contains: args.search } }
]
{ name: { contains: args.search, mode: "insensitive" } },
{ fullName: { contains: args.search, mode: "insensitive" } },
{ inn: { contains: args.search } },
];
}
const organizations = await prisma.organization.findMany({
where,
take: 50, // Ограничиваем количество результатов
orderBy: { createdAt: 'desc' },
orderBy: { createdAt: "desc" },
include: {
users: true,
apiKeys: true
}
})
apiKeys: true,
},
});
// Добавляем флаги isCounterparty, isCurrentUser, hasOutgoingRequest и hasIncomingRequest к каждой организации
return organizations.map(org => ({
return organizations.map((org) => ({
...org,
isCounterparty: existingCounterpartyIds.includes(org.id),
isCurrentUser: org.id === currentUser.organization?.id,
hasOutgoingRequest: outgoingRequestIds.includes(org.id),
hasIncomingRequest: incomingRequestIds.includes(org.id)
}))
hasIncomingRequest: incomingRequestIds.includes(org.id),
}));
},
// Мои контрагенты
myCounterparties: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
const counterparties = await prisma.counterparty.findMany({
where: { organizationId: currentUser.organization.id },
include: {
include: {
counterparty: {
include: {
users: true,
apiKeys: true
}
}
}
})
apiKeys: true,
},
},
},
});
return counterparties.map(c => c.counterparty)
return counterparties.map((c) => c.counterparty);
},
// Входящие заявки
incomingRequests: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
return await prisma.counterpartyRequest.findMany({
where: {
where: {
receiverId: currentUser.organization.id,
status: 'PENDING'
status: "PENDING",
},
include: {
sender: {
include: {
users: true,
apiKeys: true
}
apiKeys: true,
},
},
receiver: {
include: {
users: true,
apiKeys: true
}
}
apiKeys: true,
},
},
},
orderBy: { createdAt: 'desc' }
})
orderBy: { createdAt: "desc" },
});
},
// Исходящие заявки
outgoingRequests: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
return await prisma.counterpartyRequest.findMany({
where: {
where: {
senderId: currentUser.organization.id,
status: { in: ['PENDING', 'REJECTED'] }
status: { in: ["PENDING", "REJECTED"] },
},
include: {
sender: {
include: {
users: true,
apiKeys: true
}
apiKeys: true,
},
},
receiver: {
include: {
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
const limit = args.limit || 50
const offset = args.offset || 0
const limit = args.limit || 50;
const offset = args.offset || 0;
const messages = await prisma.message.findMany({
where: {
OR: [
{
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.counterpartyId
receiverOrganizationId: args.counterpartyId,
},
{
senderOrganizationId: args.counterpartyId,
receiverOrganizationId: currentUser.organization.id
}
]
receiverOrganizationId: currentUser.organization.id,
},
],
},
include: {
sender: true,
senderOrganization: {
include: {
users: true
}
users: true,
},
},
receiverOrganization: {
include: {
users: true
}
}
users: true,
},
},
},
orderBy: { createdAt: 'asc' },
orderBy: { createdAt: "asc" },
take: limit,
skip: offset
})
skip: offset,
});
return messages
return messages;
},
// Список чатов (последние сообщения с каждым контрагентом)
conversations: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// TODO: Здесь будет логика получения списка чатов
// Пока возвращаем пустой массив, так как таблица сообщений еще не создана
return []
return [];
},
// Мои услуги
myServices: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Услуги доступны только для фулфилмент центров')
if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError("Услуги доступны только для фулфилмент центров");
}
return await prisma.service.findMany({
where: { organizationId: currentUser.organization.id },
include: { organization: true },
orderBy: { createdAt: 'desc' }
})
orderBy: { createdAt: "desc" },
});
},
// Мои расходники
mySupplies: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
return await prisma.supply.findMany({
where: { organizationId: currentUser.organization.id },
include: { organization: true },
orderBy: { createdAt: 'desc' }
})
orderBy: { createdAt: "desc" },
});
},
// Логистика организации
myLogistics: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
return await prisma.logistics.findMany({
where: { organizationId: currentUser.organization.id },
include: { organization: true },
orderBy: { createdAt: 'desc' }
})
orderBy: { createdAt: "desc" },
});
},
// Мои товары (для оптовиков)
myProducts: async (_: unknown, __: unknown, context: Context) => {
// Мои поставки Wildberries
myWildberriesSupplies: async (
_: unknown,
__: unknown,
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
return await prisma.wildberriesSupply.findMany({
where: { organizationId: currentUser.organization.id },
include: {
organization: true,
cards: true,
},
orderBy: { createdAt: "desc" },
});
},
// Мои товары (для оптовиков)
myProducts: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что это оптовик
if (currentUser.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Товары доступны только для оптовиков')
if (currentUser.organization.type !== "WHOLESALE") {
throw new GraphQLError("Товары доступны только для оптовиков");
}
return await prisma.product.findMany({
where: { organizationId: currentUser.organization.id },
include: {
include: {
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const where: Record<string, unknown> = {
isActive: true, // Показываем только активные товары
organization: {
type: 'WHOLESALE' // Только товары оптовиков
}
}
type: "WHOLESALE", // Только товары оптовиков
},
};
if (args.search) {
where.OR = [
{ name: { contains: args.search, mode: 'insensitive' } },
{ article: { contains: args.search, mode: 'insensitive' } },
{ description: { contains: args.search, mode: 'insensitive' } },
{ brand: { contains: args.search, mode: 'insensitive' } }
]
{ name: { contains: args.search, mode: "insensitive" } },
{ article: { contains: args.search, mode: "insensitive" } },
{ description: { contains: args.search, mode: "insensitive" } },
{ brand: { contains: args.search, mode: "insensitive" } },
];
}
if (args.category) {
where.categoryId = args.category
where.categoryId = args.category;
}
return await prisma.product.findMany({
where,
include: {
include: {
category: true,
organization: {
include: {
users: true
}
}
users: true,
},
},
},
orderBy: { createdAt: 'desc' },
take: 100 // Ограничиваем количество результатов
})
orderBy: { createdAt: "desc" },
take: 100, // Ограничиваем количество результатов
});
},
// Все категории
categories: async (_: unknown, __: unknown, context: Context) => {
if (!context.user && !context.admin) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
return await prisma.category.findMany({
orderBy: { name: 'asc' }
})
orderBy: { name: "asc" },
});
},
// Корзина пользователя
myCart: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Найти или создать корзину для организации
@ -709,21 +766,21 @@ export const resolvers = {
category: true,
organization: {
include: {
users: true
}
}
}
}
}
users: true,
},
},
},
},
},
},
organization: true
}
})
organization: true,
},
});
if (!cart) {
cart = await prisma.cart.create({
data: {
organizationId: currentUser.organization.id
organizationId: currentUser.organization.id,
},
include: {
items: {
@ -733,36 +790,36 @@ export const resolvers = {
category: true,
organization: {
include: {
users: true
}
}
}
}
}
users: true,
},
},
},
},
},
},
organization: true
}
})
organization: true,
},
});
}
return cart
return cart;
},
// Избранные товары пользователя
myFavorites: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Получаем избранные товары
@ -774,210 +831,228 @@ export const resolvers = {
category: true,
organization: {
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) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError("Доступно только для фулфилмент центров");
}
const employees = await prisma.employee.findMany({
where: { organizationId: currentUser.organization.id },
include: {
organization: true
organization: true,
},
orderBy: { createdAt: 'desc' }
})
orderBy: { createdAt: "desc" },
});
return employees
return employees;
},
// Получение сотрудника по ID
employee: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError("Доступно только для фулфилмент центров");
}
const employee = await prisma.employee.findFirst({
where: {
where: {
id: args.id,
organizationId: currentUser.organization.id
organizationId: currentUser.organization.id,
},
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError("Доступно только для фулфилмент центров");
}
// Проверяем что сотрудник принадлежит организации
const employee = await prisma.employee.findFirst({
where: {
id: args.employeeId,
organizationId: currentUser.organization.id
}
})
organizationId: currentUser.organization.id,
},
});
if (!employee) {
throw new GraphQLError('Сотрудник не найден')
throw new GraphQLError("Сотрудник не найден");
}
// Получаем записи табеля за указанный месяц
const startDate = new Date(args.year, args.month, 1)
const endDate = new Date(args.year, args.month + 1, 0)
const startDate = new Date(args.year, args.month, 1);
const endDate = new Date(args.year, args.month + 1, 0);
const scheduleRecords = await prisma.employeeSchedule.findMany({
where: {
employeeId: args.employeeId,
date: {
gte: startDate,
lte: endDate
}
lte: endDate,
},
},
orderBy: {
date: 'asc'
}
})
date: "asc",
},
});
return scheduleRecords
}
return scheduleRecords;
},
},
Mutation: {
sendSmsCode: async (_: unknown, args: { phone: string }) => {
const result = await smsService.sendSmsCode(args.phone)
const result = await smsService.sendSmsCode(args.phone);
return {
success: result.success,
message: result.message || 'SMS код отправлен'
}
message: result.message || "SMS код отправлен",
};
},
verifySmsCode: async (_: unknown, args: { phone: string; code: string }) => {
const verificationResult = await smsService.verifySmsCode(args.phone, args.code)
verifySmsCode: async (
_: unknown,
args: { phone: string; code: string }
) => {
const verificationResult = await smsService.verifySmsCode(
args.phone,
args.code
);
if (!verificationResult.success) {
return {
success: false,
message: verificationResult.message || 'Неверный код'
}
message: verificationResult.message || "Неверный код",
};
}
// Найти или создать пользователя
const formattedPhone = args.phone.replace(/\D/g, '')
const formattedPhone = args.phone.replace(/\D/g, "");
let user = await prisma.user.findUnique({
where: { phone: formattedPhone },
include: {
organization: {
include: {
apiKeys: true
}
}
}
})
apiKeys: true,
},
},
},
});
if (!user) {
user = await prisma.user.create({
data: {
phone: formattedPhone
phone: formattedPhone,
},
include: {
organization: {
include: {
apiKeys: true
}
}
}
})
apiKeys: true,
},
},
},
});
}
const token = generateToken({
userId: user.id,
phone: user.phone
})
phone: user.phone,
});
console.log('verifySmsCode - Generated token:', token ? `${token.substring(0, 20)}...` : 'No token')
console.log('verifySmsCode - Full token:', token)
console.log('verifySmsCode - User object:', { id: user.id, phone: user.phone })
console.log(
"verifySmsCode - Generated token:",
token ? `${token.substring(0, 20)}...` : "No token"
);
console.log("verifySmsCode - Full token:", token);
console.log("verifySmsCode - User object:", {
id: user.id,
phone: user.phone,
});
const result = {
success: true,
message: 'Авторизация успешна',
message: "Авторизация успешна",
token,
user
}
user,
};
console.log('verifySmsCode - Returning result:', {
success: result.success,
console.log("verifySmsCode - Returning result:", {
success: result.success,
hasToken: !!result.token,
hasUser: !!result.user,
message: result.message,
tokenPreview: result.token ? `${result.token.substring(0, 20)}...` : 'No token in result'
})
tokenPreview: result.token
? `${result.token.substring(0, 20)}...`
: "No token in result",
});
return result
return result;
},
verifyInn: async (_: unknown, args: { inn: string }) => {
@ -985,72 +1060,80 @@ export const resolvers = {
if (!dadataService.validateInn(args.inn)) {
return {
success: false,
message: 'Неверный формат ИНН'
}
message: "Неверный формат ИНН",
};
}
// Получаем данные организации из DaData
const organizationData = await dadataService.getOrganizationByInn(args.inn)
const organizationData = await dadataService.getOrganizationByInn(
args.inn
);
if (!organizationData) {
return {
success: false,
message: 'Организация с указанным ИНН не найдена'
}
message: "Организация с указанным ИНН не найдена",
};
}
return {
success: true,
message: 'ИНН найден',
message: "ИНН найден",
organization: {
name: organizationData.name,
fullName: organizationData.fullName,
address: organizationData.address,
isActive: organizationData.isActive
}
}
isActive: organizationData.isActive,
},
};
},
registerFulfillmentOrganization: async (
_: unknown,
args: { input: { phone: string; inn: string; type: 'FULFILLMENT' | 'LOGIST' | 'WHOLESALE' } },
args: {
input: {
phone: string;
inn: string;
type: "FULFILLMENT" | "LOGIST" | "WHOLESALE";
};
},
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const { inn, type } = args.input
const { inn, type } = args.input;
// Валидируем ИНН
if (!dadataService.validateInn(inn)) {
return {
success: false,
message: 'Неверный формат ИНН'
}
message: "Неверный формат ИНН",
};
}
// Получаем данные организации из DaData
const organizationData = await dadataService.getOrganizationByInn(inn)
const organizationData = await dadataService.getOrganizationByInn(inn);
if (!organizationData) {
return {
success: false,
message: 'Организация с указанным ИНН не найдена'
}
message: "Организация с указанным ИНН не найдена",
};
}
try {
// Проверяем, что организация еще не зарегистрирована
const existingOrg = await prisma.organization.findUnique({
where: { inn: organizationData.inn }
})
where: { inn: organizationData.inn },
});
if (existingOrg) {
return {
success: false,
message: 'Организация с таким ИНН уже зарегистрирована'
}
message: "Организация с таким ИНН уже зарегистрирована",
};
}
// Создаем организацию со всеми данными из DaData
@ -1064,41 +1147,45 @@ export const resolvers = {
addressFull: organizationData.addressFull,
ogrn: organizationData.ogrn,
ogrnDate: organizationData.ogrnDate,
// Статус организации
status: organizationData.status,
actualityDate: organizationData.actualityDate,
registrationDate: organizationData.registrationDate,
liquidationDate: organizationData.liquidationDate,
// Руководитель
managementName: organizationData.managementName,
managementPost: organizationData.managementPost,
// ОПФ
opfCode: organizationData.opfCode,
opfFull: organizationData.opfFull,
opfShort: organizationData.opfShort,
// Коды статистики
okato: organizationData.okato,
oktmo: organizationData.oktmo,
okpo: organizationData.okpo,
okved: organizationData.okved,
// Контакты
phones: organizationData.phones ? JSON.parse(JSON.stringify(organizationData.phones)) : null,
emails: organizationData.emails ? JSON.parse(JSON.stringify(organizationData.emails)) : null,
phones: organizationData.phones
? JSON.parse(JSON.stringify(organizationData.phones))
: null,
emails: organizationData.emails
? JSON.parse(JSON.stringify(organizationData.emails))
: null,
// Финансовые данные
employeeCount: organizationData.employeeCount,
revenue: organizationData.revenue,
taxSystem: organizationData.taxSystem,
type: type,
dadataData: JSON.parse(JSON.stringify(organizationData.rawData))
}
})
dadataData: JSON.parse(JSON.stringify(organizationData.rawData)),
},
});
// Привязываем пользователя к организации
const updatedUser = await prisma.user.update({
@ -1107,24 +1194,23 @@ export const resolvers = {
include: {
organization: {
include: {
apiKeys: true
}
}
}
})
apiKeys: true,
},
},
},
});
return {
success: true,
message: 'Организация успешно зарегистрирована',
user: updatedUser
}
message: "Организация успешно зарегистрирована",
user: updatedUser,
};
} catch (error) {
console.error('Error registering fulfillment organization:', error)
console.error("Error registering fulfillment organization:", error);
return {
success: false,
message: 'Ошибка при регистрации организации'
}
message: "Ошибка при регистрации организации",
};
}
},
@ -1132,87 +1218,96 @@ export const resolvers = {
_: unknown,
args: {
input: {
phone: string
wbApiKey?: string
ozonApiKey?: string
ozonClientId?: string
}
phone: string;
wbApiKey?: string;
ozonApiKey?: string;
ozonClientId?: string;
};
},
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const { wbApiKey, ozonApiKey, ozonClientId } = args.input
const { wbApiKey, ozonApiKey, ozonClientId } = args.input;
if (!wbApiKey && !ozonApiKey) {
return {
success: false,
message: 'Необходимо указать хотя бы один API ключ маркетплейса'
}
message: "Необходимо указать хотя бы один API ключ маркетплейса",
};
}
try {
// Валидируем API ключи
const validationResults = []
const validationResults = [];
if (wbApiKey) {
const wbResult = await marketplaceService.validateWildberriesApiKey(wbApiKey)
const wbResult = await marketplaceService.validateWildberriesApiKey(
wbApiKey
);
if (!wbResult.isValid) {
return {
success: false,
message: `Wildberries: ${wbResult.message}`
}
message: `Wildberries: ${wbResult.message}`,
};
}
validationResults.push({
marketplace: 'WILDBERRIES',
marketplace: "WILDBERRIES",
apiKey: wbApiKey,
data: wbResult.data
})
data: wbResult.data,
});
}
if (ozonApiKey && ozonClientId) {
const ozonResult = await marketplaceService.validateOzonApiKey(ozonApiKey, ozonClientId)
const ozonResult = await marketplaceService.validateOzonApiKey(
ozonApiKey,
ozonClientId
);
if (!ozonResult.isValid) {
return {
success: false,
message: `Ozon: ${ozonResult.message}`
}
message: `Ozon: ${ozonResult.message}`,
};
}
validationResults.push({
marketplace: 'OZON',
marketplace: "OZON",
apiKey: ozonApiKey,
data: ozonResult.data
})
data: ozonResult.data,
});
}
// Создаем организацию селлера - используем tradeMark как основное имя
const tradeMark = validationResults[0]?.data?.tradeMark
const sellerName = validationResults[0]?.data?.sellerName
const shopName = tradeMark || sellerName || 'Магазин'
const tradeMark = validationResults[0]?.data?.tradeMark;
const sellerName = validationResults[0]?.data?.sellerName;
const shopName = tradeMark || sellerName || "Магазин";
const organization = await prisma.organization.create({
data: {
inn: (validationResults[0]?.data?.inn as string) || `SELLER_${Date.now()}`,
inn:
(validationResults[0]?.data?.inn as string) ||
`SELLER_${Date.now()}`,
name: shopName, // Используем tradeMark как основное название
fullName: sellerName ? `${sellerName} (${shopName})` : `Интернет-магазин "${shopName}"`,
type: 'SELLER'
}
})
fullName: sellerName
? `${sellerName} (${shopName})`
: `Интернет-магазин "${shopName}"`,
type: "SELLER",
},
});
// Добавляем API ключи
for (const validation of validationResults) {
await prisma.apiKey.create({
data: {
marketplace: validation.marketplace as 'WILDBERRIES' | 'OZON',
marketplace: validation.marketplace as "WILDBERRIES" | "OZON",
apiKey: validation.apiKey,
organizationId: organization.id,
validationData: JSON.parse(JSON.stringify(validation.data))
}
})
validationData: JSON.parse(JSON.stringify(validation.data)),
},
});
}
// Привязываем пользователя к организации
@ -1222,24 +1317,23 @@ export const resolvers = {
include: {
organization: {
include: {
apiKeys: true
}
}
}
})
apiKeys: true,
},
},
},
});
return {
success: true,
message: 'Селлер организация успешно зарегистрирована',
user: updatedUser
}
message: "Селлер организация успешно зарегистрирована",
user: updatedUser,
};
} catch (error) {
console.error('Error registering seller organization:', error)
console.error("Error registering seller organization:", error);
return {
success: false,
message: 'Ошибка при регистрации организации'
}
message: "Ошибка при регистрации организации",
};
}
},
@ -1247,84 +1341,85 @@ export const resolvers = {
_: unknown,
args: {
input: {
marketplace: 'WILDBERRIES' | 'OZON'
apiKey: string
clientId?: string
validateOnly?: boolean
}
marketplace: "WILDBERRIES" | "OZON";
apiKey: string;
clientId?: string;
validateOnly?: boolean;
};
},
context: Context
) => {
// Разрешаем валидацию без авторизации
if (!args.input.validateOnly && !context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const { marketplace, apiKey, clientId, validateOnly } = args.input
const { marketplace, apiKey, clientId, validateOnly } = args.input;
// Валидируем API ключ
const validationResult = await marketplaceService.validateApiKey(
marketplace,
apiKey,
clientId
)
);
if (!validationResult.isValid) {
return {
success: false,
message: validationResult.message
}
message: validationResult.message,
};
}
// Если это только валидация, возвращаем результат без сохранения
if (validateOnly) {
return {
success: true,
message: 'API ключ действителен',
message: "API ключ действителен",
apiKey: {
id: 'validate-only',
id: "validate-only",
marketplace,
isActive: true,
validationData: validationResult,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
}
updatedAt: new Date().toISOString(),
},
};
}
// Для сохранения API ключа нужна авторизация
if (!context.user) {
throw new GraphQLError('Требуется авторизация для сохранения API ключа', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError(
"Требуется авторизация для сохранения API ключа",
{
extensions: { code: "UNAUTHENTICATED" },
}
);
}
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!user?.organization) {
return {
success: false,
message: 'Пользователь не привязан к организации'
}
message: "Пользователь не привязан к организации",
};
}
try {
// Проверяем, что такого ключа еще нет
const existingKey = await prisma.apiKey.findUnique({
where: {
organizationId_marketplace: {
organizationId: user.organization.id,
marketplace
}
}
})
marketplace,
},
},
});
if (existingKey) {
// Обновляем существующий ключ
@ -1332,16 +1427,16 @@ export const resolvers = {
where: { id: existingKey.id },
data: {
apiKey,
validationData: JSON.parse(JSON.stringify(validationResult.data)),
isActive: true
}
})
validationData: JSON.parse(JSON.stringify(validationResult.data)),
isActive: true,
},
});
return {
success: true,
message: 'API ключ успешно обновлен',
apiKey: updatedKey
}
message: "API ключ успешно обновлен",
apiKey: updatedKey,
};
} else {
// Создаем новый ключ
const newKey = await prisma.apiKey.create({
@ -1349,44 +1444,43 @@ export const resolvers = {
marketplace,
apiKey,
organizationId: user.organization.id,
validationData: JSON.parse(JSON.stringify(validationResult.data))
}
})
validationData: JSON.parse(JSON.stringify(validationResult.data)),
},
});
return {
success: true,
message: 'API ключ успешно добавлен',
apiKey: newKey
}
message: "API ключ успешно добавлен",
apiKey: newKey,
};
}
} catch (error) {
console.error('Error adding marketplace API key:', error)
console.error("Error adding marketplace API key:", error);
return {
success: false,
message: 'Ошибка при добавлении API ключа'
}
message: "Ошибка при добавлении API ключа",
};
}
},
removeMarketplaceApiKey: async (
_: unknown,
args: { marketplace: 'WILDBERRIES' | 'OZON' },
args: { marketplace: "WILDBERRIES" | "OZON" },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!user?.organization) {
throw new GraphQLError('Пользователь не привязан к организации')
throw new GraphQLError("Пользователь не привязан к организации");
}
try {
@ -1394,128 +1488,139 @@ export const resolvers = {
where: {
organizationId_marketplace: {
organizationId: user.organization.id,
marketplace: args.marketplace
}
}
})
marketplace: args.marketplace,
},
},
});
return true
return true;
} catch (error) {
console.error('Error removing marketplace API key:', error)
return false
console.error("Error removing marketplace API key:", error);
return false;
}
},
updateUserProfile: async (_: unknown, args: { input: {
avatar?: string
orgPhone?: string
managerName?: string
telegram?: string
whatsapp?: string
email?: string
bankName?: string
bik?: string
accountNumber?: string
corrAccount?: string
} }, context: Context) => {
updateUserProfile: async (
_: unknown,
args: {
input: {
avatar?: string;
orgPhone?: string;
managerName?: string;
telegram?: string;
whatsapp?: string;
email?: string;
bankName?: string;
bik?: string;
accountNumber?: string;
corrAccount?: string;
};
},
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: {
include: {
organization: {
include: {
apiKeys: true
}
}
}
})
apiKeys: true,
},
},
},
});
if (!user?.organization) {
throw new GraphQLError('Пользователь не привязан к организации')
throw new GraphQLError("Пользователь не привязан к организации");
}
try {
const { input } = args
const { input } = args;
// Обновляем данные пользователя (аватар, имя управляющего)
const userUpdateData: { avatar?: string; managerName?: string } = {}
const userUpdateData: { avatar?: string; managerName?: string } = {};
if (input.avatar) {
userUpdateData.avatar = input.avatar
userUpdateData.avatar = input.avatar;
}
if (input.managerName) {
userUpdateData.managerName = input.managerName
userUpdateData.managerName = input.managerName;
}
if (Object.keys(userUpdateData).length > 0) {
await prisma.user.update({
where: { id: context.user.id },
data: userUpdateData
})
data: userUpdateData,
});
}
// Подготавливаем данные для обновления организации
const updateData: {
phones?: object
emails?: object
managementName?: string
managementPost?: string
} = {}
phones?: object;
emails?: object;
managementName?: string;
managementPost?: string;
} = {};
// Название организации больше не обновляется через профиль
// Для селлеров устанавливается при регистрации, для остальных - при смене ИНН
// Обновляем контактные данные в JSON поле phones
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) {
updateData.emails = [{ value: input.email, type: 'main' }]
updateData.emails = [{ value: input.email, type: "main" }];
}
// Сохраняем дополнительные контакты в custom полях
// Пока добавим их как дополнительные JSON поля
const customContacts: {
managerName?: string
telegram?: string
whatsapp?: string
managerName?: string;
telegram?: string;
whatsapp?: string;
bankDetails?: {
bankName?: string
bik?: string
accountNumber?: string
corrAccount?: string
}
} = {}
bankName?: string;
bik?: string;
accountNumber?: string;
corrAccount?: string;
};
} = {};
// managerName теперь сохраняется в поле пользователя, а не в JSON
if (input.telegram) {
customContacts.telegram = input.telegram
customContacts.telegram = input.telegram;
}
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 = {
bankName: input.bankName,
bik: input.bik,
accountNumber: input.accountNumber,
corrAccount: input.corrAccount
}
corrAccount: input.corrAccount,
};
}
// Если есть дополнительные контакты, сохраним их в поле managementPost временно
// В идеале нужно добавить отдельную таблицу для контактов
if (Object.keys(customContacts).length > 0) {
updateData.managementPost = JSON.stringify(customContacts)
updateData.managementPost = JSON.stringify(customContacts);
}
// Обновляем организацию
@ -1523,56 +1628,60 @@ export const resolvers = {
where: { id: user.organization.id },
data: updateData,
include: {
apiKeys: true
}
})
apiKeys: true,
},
});
// Получаем обновленного пользователя
const updatedUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: {
include: {
organization: {
include: {
apiKeys: true
}
}
}
})
apiKeys: true,
},
},
},
});
return {
success: true,
message: 'Профиль успешно обновлен',
user: updatedUser
}
message: "Профиль успешно обновлен",
user: updatedUser,
};
} catch (error) {
console.error('Error updating user profile:', error)
console.error("Error updating user profile:", error);
return {
success: false,
message: 'Ошибка при обновлении профиля'
}
message: "Ошибка при обновлении профиля",
};
}
},
updateOrganizationByInn: async (_: unknown, args: { inn: string }, context: Context) => {
updateOrganizationByInn: async (
_: unknown,
args: { inn: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: {
include: {
organization: {
include: {
apiKeys: true
}
}
}
})
apiKeys: true,
},
},
},
});
if (!user?.organization) {
throw new GraphQLError('Пользователь не привязан к организации')
throw new GraphQLError("Пользователь не привязан к организации");
}
try {
@ -1580,43 +1689,57 @@ export const resolvers = {
if (!dadataService.validateInn(args.inn)) {
return {
success: false,
message: 'Неверный формат ИНН'
}
message: "Неверный формат ИНН",
};
}
// Получаем данные организации из DaData
const organizationData = await dadataService.getOrganizationByInn(args.inn)
const organizationData = await dadataService.getOrganizationByInn(
args.inn
);
if (!organizationData) {
return {
success: false,
message: 'Организация с указанным ИНН не найдена в федеральном реестре'
}
message:
"Организация с указанным ИНН не найдена в федеральном реестре",
};
}
// Проверяем, есть ли уже организация с таким ИНН в базе (кроме текущей)
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 {
success: false,
message: `Организация с ИНН ${organizationData.inn} уже существует в системе`
}
message: `Организация с ИНН ${organizationData.inn} уже существует в системе`,
};
}
// Подготавливаем данные для обновления
const updateData: Prisma.OrganizationUpdateInput = {
kpp: organizationData.kpp,
// Для селлеров не обновляем название организации (это название магазина)
...(user.organization.type !== 'SELLER' && { name: organizationData.name }),
...(user.organization.type !== "SELLER" && {
name: organizationData.name,
}),
fullName: organizationData.fullName,
address: organizationData.address,
addressFull: organizationData.addressFull,
ogrn: organizationData.ogrn,
ogrnDate: organizationData.ogrnDate ? organizationData.ogrnDate.toISOString() : null,
registrationDate: organizationData.registrationDate ? organizationData.registrationDate.toISOString() : null,
liquidationDate: organizationData.liquidationDate ? organizationData.liquidationDate.toISOString() : null,
ogrnDate: organizationData.ogrnDate
? organizationData.ogrnDate.toISOString()
: null,
registrationDate: organizationData.registrationDate
? organizationData.registrationDate.toISOString()
: null,
liquidationDate: organizationData.liquidationDate
? organizationData.liquidationDate.toISOString()
: null,
managementName: organizationData.managementName, // Всегда перезаписываем данными из DaData (может быть null)
managementPost: user.organization.managementPost, // Сохраняем кастомные данные пользователя
opfCode: organizationData.opfCode,
@ -1626,12 +1749,12 @@ export const resolvers = {
oktmo: organizationData.oktmo,
okpo: organizationData.okpo,
okved: organizationData.okved,
status: organizationData.status
}
status: organizationData.status,
};
// Добавляем ИНН только если он отличается от текущего
if (user.organization.inn !== organizationData.inn) {
updateData.inn = organizationData.inn
updateData.inn = organizationData.inn;
}
// Обновляем организацию
@ -1639,70 +1762,74 @@ export const resolvers = {
where: { id: user.organization.id },
data: updateData,
include: {
apiKeys: true
}
})
apiKeys: true,
},
});
// Получаем обновленного пользователя
const updatedUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: {
include: {
organization: {
include: {
apiKeys: true
}
}
}
})
apiKeys: true,
},
},
},
});
return {
success: true,
message: 'Данные организации успешно обновлены',
user: updatedUser
}
message: "Данные организации успешно обновлены",
user: updatedUser,
};
} catch (error) {
console.error('Error updating organization by INN:', error)
console.error("Error updating organization by INN:", error);
return {
success: false,
message: 'Ошибка при обновлении данных организации'
}
message: "Ошибка при обновлении данных организации",
};
}
},
logout: () => {
// В stateless JWT системе logout происходит на клиенте
// Можно добавить 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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
if (currentUser.organization.id === args.organizationId) {
throw new GraphQLError('Нельзя отправить заявку самому себе')
throw new GraphQLError("Нельзя отправить заявку самому себе");
}
// Проверяем, что организация-получатель существует
const receiverOrganization = await prisma.organization.findUnique({
where: { id: args.organizationId }
})
where: { id: args.organizationId },
});
if (!receiverOrganization) {
throw new GraphQLError('Организация не найдена')
throw new GraphQLError("Организация не найдена");
}
try {
@ -1711,55 +1838,59 @@ export const resolvers = {
where: {
senderId_receiverId: {
senderId: currentUser.organization.id,
receiverId: args.organizationId
}
receiverId: args.organizationId,
},
},
update: {
status: 'PENDING',
status: "PENDING",
message: args.message,
updatedAt: new Date()
updatedAt: new Date(),
},
create: {
senderId: currentUser.organization.id,
receiverId: args.organizationId,
message: args.message,
status: 'PENDING'
status: "PENDING",
},
include: {
sender: true,
receiver: true
}
})
receiver: true,
},
});
return {
success: true,
message: 'Заявка отправлена',
request
}
message: "Заявка отправлена",
request,
};
} catch (error) {
console.error('Error sending counterparty request:', error)
console.error("Error sending counterparty request:", error);
return {
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
try {
@ -1768,23 +1899,23 @@ export const resolvers = {
where: { id: args.requestId },
include: {
sender: true,
receiver: true
}
})
receiver: true,
},
});
if (!request) {
throw new GraphQLError('Заявка не найдена')
throw new GraphQLError("Заявка не найдена");
}
if (request.receiverId !== currentUser.organization.id) {
throw new GraphQLError('Нет прав на обработку этой заявки')
throw new GraphQLError("Нет прав на обработку этой заявки");
}
if (request.status !== 'PENDING') {
throw new GraphQLError('Заявка уже обработана')
if (request.status !== "PENDING") {
throw new GraphQLError("Заявка уже обработана");
}
const newStatus = args.accept ? 'ACCEPTED' : 'REJECTED'
const newStatus = args.accept ? "ACCEPTED" : "REJECTED";
// Обновляем статус заявки
const updatedRequest = await prisma.counterpartyRequest.update({
@ -1792,9 +1923,9 @@ export const resolvers = {
data: { status: newStatus },
include: {
sender: true,
receiver: true
}
})
receiver: true,
},
});
// Если заявка принята, создаем связи контрагентов в обе стороны
if (args.accept) {
@ -1803,94 +1934,102 @@ export const resolvers = {
prisma.counterparty.create({
data: {
organizationId: request.receiverId,
counterpartyId: request.senderId
}
counterpartyId: request.senderId,
},
}),
// Добавляем получателя в контрагенты отправителя
prisma.counterparty.create({
data: {
organizationId: request.senderId,
counterpartyId: request.receiverId
}
})
])
counterpartyId: request.receiverId,
},
}),
]);
}
return {
success: true,
message: args.accept ? 'Заявка принята' : 'Заявка отклонена',
request: updatedRequest
}
message: args.accept ? "Заявка принята" : "Заявка отклонена",
request: updatedRequest,
};
} catch (error) {
console.error('Error responding to counterparty request:', error)
console.error("Error responding to counterparty request:", error);
return {
success: false,
message: 'Ошибка при обработке заявки'
}
message: "Ошибка при обработке заявки",
};
}
},
// Отменить заявку
cancelCounterpartyRequest: async (_: unknown, args: { requestId: string }, context: Context) => {
cancelCounterpartyRequest: async (
_: unknown,
args: { requestId: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
try {
const request = await prisma.counterpartyRequest.findUnique({
where: { id: args.requestId }
})
where: { id: args.requestId },
});
if (!request) {
throw new GraphQLError('Заявка не найдена')
throw new GraphQLError("Заявка не найдена");
}
if (request.senderId !== currentUser.organization.id) {
throw new GraphQLError('Можно отменить только свои заявки')
throw new GraphQLError("Можно отменить только свои заявки");
}
if (request.status !== 'PENDING') {
throw new GraphQLError('Можно отменить только ожидающие заявки')
if (request.status !== "PENDING") {
throw new GraphQLError("Можно отменить только ожидающие заявки");
}
await prisma.counterpartyRequest.update({
where: { id: args.requestId },
data: { status: 'CANCELLED' }
})
data: { status: "CANCELLED" },
});
return true
return true;
} catch (error) {
console.error('Error cancelling counterparty request:', error)
return false
console.error("Error cancelling counterparty request:", error);
return false;
}
},
// Удалить контрагента
removeCounterparty: async (_: unknown, args: { organizationId: string }, context: Context) => {
removeCounterparty: async (
_: unknown,
args: { organizationId: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
try {
@ -1899,60 +2038,70 @@ export const resolvers = {
prisma.counterparty.deleteMany({
where: {
organizationId: currentUser.organization.id,
counterpartyId: args.organizationId
}
counterpartyId: args.organizationId,
},
}),
prisma.counterparty.deleteMany({
where: {
organizationId: args.organizationId,
counterpartyId: currentUser.organization.id
}
})
])
counterpartyId: currentUser.organization.id,
},
}),
]);
return true
return true;
} catch (error) {
console.error('Error removing counterparty:', error)
return false
console.error("Error removing counterparty:", error);
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что получатель является контрагентом
const isCounterparty = await prisma.counterparty.findFirst({
where: {
organizationId: currentUser.organization.id,
counterpartyId: args.receiverOrganizationId
}
})
counterpartyId: args.receiverOrganizationId,
},
});
if (!isCounterparty) {
throw new GraphQLError('Можно отправлять сообщения только контрагентам')
throw new GraphQLError(
"Можно отправлять сообщения только контрагентам"
);
}
// Получаем организацию получателя
const receiverOrganization = await prisma.organization.findUnique({
where: { id: args.receiverOrganizationId }
})
where: { id: args.receiverOrganizationId },
});
if (!receiverOrganization) {
throw new GraphQLError('Организация получателя не найдена')
throw new GraphQLError("Организация получателя не найдена");
}
try {
@ -1960,76 +2109,86 @@ export const resolvers = {
const message = await prisma.message.create({
data: {
content: args.content?.trim() || null,
type: args.type || 'TEXT',
type: args.type || "TEXT",
senderId: context.user.id,
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.receiverOrganizationId
receiverOrganizationId: args.receiverOrganizationId,
},
include: {
sender: true,
senderOrganization: {
include: {
users: true
}
users: true,
},
},
receiverOrganization: {
include: {
users: true
}
}
}
})
users: true,
},
},
},
});
return {
success: true,
message: 'Сообщение отправлено',
messageData: message
}
message: "Сообщение отправлено",
messageData: message,
};
} catch (error) {
console.error('Error sending message:', error)
console.error("Error sending message:", error);
return {
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что получатель является контрагентом
const isCounterparty = await prisma.counterparty.findFirst({
where: {
organizationId: currentUser.organization.id,
counterpartyId: args.receiverOrganizationId
}
})
counterpartyId: args.receiverOrganizationId,
},
});
if (!isCounterparty) {
throw new GraphQLError('Можно отправлять сообщения только контрагентам')
throw new GraphQLError(
"Можно отправлять сообщения только контрагентам"
);
}
// Получаем организацию получателя
const receiverOrganization = await prisma.organization.findUnique({
where: { id: args.receiverOrganizationId }
})
where: { id: args.receiverOrganizationId },
});
if (!receiverOrganization) {
throw new GraphQLError('Организация получателя не найдена')
throw new GraphQLError("Организация получателя не найдена");
}
try {
@ -2037,217 +2196,256 @@ export const resolvers = {
const message = await prisma.message.create({
data: {
content: null,
type: 'VOICE',
type: "VOICE",
voiceUrl: args.voiceUrl,
voiceDuration: args.voiceDuration,
senderId: context.user.id,
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.receiverOrganizationId
receiverOrganizationId: args.receiverOrganizationId,
},
include: {
sender: true,
senderOrganization: {
include: {
users: true
}
users: true,
},
},
receiverOrganization: {
include: {
users: true
}
}
}
})
users: true,
},
},
},
});
return {
success: true,
message: 'Голосовое сообщение отправлено',
messageData: message
}
message: "Голосовое сообщение отправлено",
messageData: message,
};
} catch (error) {
console.error('Error sending voice message:', error)
console.error("Error sending voice message:", error);
return {
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что получатель является контрагентом
const isCounterparty = await prisma.counterparty.findFirst({
where: {
organizationId: currentUser.organization.id,
counterpartyId: args.receiverOrganizationId
}
})
counterpartyId: args.receiverOrganizationId,
},
});
if (!isCounterparty) {
throw new GraphQLError('Можно отправлять сообщения только контрагентам')
throw new GraphQLError(
"Можно отправлять сообщения только контрагентам"
);
}
try {
const message = await prisma.message.create({
data: {
content: null,
type: 'IMAGE',
type: "IMAGE",
fileUrl: args.fileUrl,
fileName: args.fileName,
fileSize: args.fileSize,
fileType: args.fileType,
senderId: context.user.id,
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.receiverOrganizationId
receiverOrganizationId: args.receiverOrganizationId,
},
include: {
sender: true,
senderOrganization: {
include: {
users: true
}
users: true,
},
},
receiverOrganization: {
include: {
users: true
}
}
}
})
users: true,
},
},
},
});
return {
success: true,
message: 'Изображение отправлено',
messageData: message
}
message: "Изображение отправлено",
messageData: message,
};
} catch (error) {
console.error('Error sending image:', error)
console.error("Error sending image:", error);
return {
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что получатель является контрагентом
const isCounterparty = await prisma.counterparty.findFirst({
where: {
organizationId: currentUser.organization.id,
counterpartyId: args.receiverOrganizationId
}
})
counterpartyId: args.receiverOrganizationId,
},
});
if (!isCounterparty) {
throw new GraphQLError('Можно отправлять сообщения только контрагентам')
throw new GraphQLError(
"Можно отправлять сообщения только контрагентам"
);
}
try {
const message = await prisma.message.create({
data: {
content: null,
type: 'FILE',
type: "FILE",
fileUrl: args.fileUrl,
fileName: args.fileName,
fileSize: args.fileSize,
fileType: args.fileType,
senderId: context.user.id,
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.receiverOrganizationId
receiverOrganizationId: args.receiverOrganizationId,
},
include: {
sender: true,
senderOrganization: {
include: {
users: true
}
users: true,
},
},
receiverOrganization: {
include: {
users: true
}
}
}
})
users: true,
},
},
},
});
return {
success: true,
message: 'Файл отправлен',
messageData: message
}
message: "Файл отправлен",
messageData: message,
};
} catch (error) {
console.error('Error sending file:', error)
console.error("Error sending file:", error);
return {
success: false,
message: 'Ошибка при отправке файла'
}
message: "Ошибка при отправке файла",
};
}
},
// Отметить сообщения как прочитанные
markMessagesAsRead: async (_: unknown, args: { conversationId: string }, context: Context) => {
markMessagesAsRead: async (
_: unknown,
args: { conversationId: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
// 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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Услуги доступны только для фулфилмент центров')
if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError("Услуги доступны только для фулфилмент центров");
}
try {
@ -2257,52 +2455,64 @@ export const resolvers = {
description: args.input.description,
price: args.input.price,
imageUrl: args.input.imageUrl,
organizationId: currentUser.organization.id
organizationId: currentUser.organization.id,
},
include: { organization: true }
})
include: { organization: true },
});
return {
success: true,
message: 'Услуга успешно создана',
service
}
message: "Услуга успешно создана",
service,
};
} catch (error) {
console.error('Error creating service:', error)
console.error("Error creating service:", error);
return {
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что услуга принадлежит текущей организации
const existingService = await prisma.service.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id
}
})
organizationId: currentUser.organization.id,
},
});
if (!existingService) {
throw new GraphQLError('Услуга не найдена или нет доступа')
throw new GraphQLError("Услуга не найдена или нет доступа");
}
try {
@ -2312,86 +2522,103 @@ export const resolvers = {
name: args.input.name,
description: args.input.description,
price: args.input.price,
imageUrl: args.input.imageUrl
imageUrl: args.input.imageUrl,
},
include: { organization: true }
})
include: { organization: true },
});
return {
success: true,
message: 'Услуга успешно обновлена',
service
}
message: "Услуга успешно обновлена",
service,
};
} catch (error) {
console.error('Error updating service:', error)
console.error("Error updating service:", error);
return {
success: false,
message: 'Ошибка при обновлении услуги'
}
message: "Ошибка при обновлении услуги",
};
}
},
// Удалить услугу
deleteService: async (_: unknown, args: { id: string }, context: Context) => {
deleteService: async (
_: unknown,
args: { id: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что услуга принадлежит текущей организации
const existingService = await prisma.service.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id
}
})
organizationId: currentUser.organization.id,
},
});
if (!existingService) {
throw new GraphQLError('Услуга не найдена или нет доступа')
throw new GraphQLError("Услуга не найдена или нет доступа");
}
try {
await prisma.service.delete({
where: { id: args.id }
})
where: { id: args.id },
});
return true
return true;
} catch (error) {
console.error('Error deleting service:', error)
return false
console.error("Error deleting service:", error);
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Расходники доступны только для фулфилмент центров')
if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError(
"Расходники доступны только для фулфилмент центров"
);
}
try {
@ -2402,52 +2629,64 @@ export const resolvers = {
price: args.input.price,
quantity: 0, // Временно устанавливаем 0, так как поле убрано из интерфейса
imageUrl: args.input.imageUrl,
organizationId: currentUser.organization.id
organizationId: currentUser.organization.id,
},
include: { organization: true }
})
include: { organization: true },
});
return {
success: true,
message: 'Расходник успешно создан',
supply
}
message: "Расходник успешно создан",
supply,
};
} catch (error) {
console.error('Error creating supply:', error)
console.error("Error creating supply:", error);
return {
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что расходник принадлежит текущей организации
const existingSupply = await prisma.supply.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id
}
})
organizationId: currentUser.organization.id,
},
});
if (!existingSupply) {
throw new GraphQLError('Расходник не найден или нет доступа')
throw new GraphQLError("Расходник не найден или нет доступа");
}
try {
@ -2458,119 +2697,292 @@ export const resolvers = {
description: args.input.description,
price: args.input.price,
quantity: 0, // Временно устанавливаем 0, так как поле убрано из интерфейса
imageUrl: args.input.imageUrl
imageUrl: args.input.imageUrl,
},
include: { organization: true }
})
include: { organization: true },
});
return {
success: true,
message: 'Расходник успешно обновлен',
supply
}
message: "Расходник успешно обновлен",
supply,
};
} catch (error) {
console.error('Error updating supply:', error)
console.error("Error updating supply:", error);
return {
success: false,
message: 'Ошибка при обновлении расходника'
}
message: "Ошибка при обновлении расходника",
};
}
},
// Удалить расходник
deleteSupply: async (_: unknown, args: { id: string }, context: Context) => {
deleteSupply: async (
_: unknown,
args: { id: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что расходник принадлежит текущей организации
const existingSupply = await prisma.supply.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id
}
})
organizationId: currentUser.organization.id,
},
});
if (!existingSupply) {
throw new GraphQLError('Расходник не найден или нет доступа')
throw new GraphQLError("Расходник не найден или нет доступа");
}
try {
await prisma.supply.delete({
where: { id: args.id }
})
where: { id: args.id },
});
return true
return true;
} catch (error) {
console.error('Error deleting supply:', error)
return false
console.error("Error deleting supply:", error);
return false;
}
},
// Создать товар
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) => {
// Создать заказ поставки расходников
createSupplyOrder: async (
_: unknown,
args: {
input: {
partnerId: string;
deliveryDate: string;
items: Array<{ productId: string; quantity: number }>;
};
},
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
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') {
throw new GraphQLError('Товары доступны только для оптовиков')
if (currentUser.organization.type !== "WHOLESALE") {
throw new GraphQLError("Товары доступны только для оптовиков");
}
// Проверяем уникальность артикула в рамках организации
const existingProduct = await prisma.product.findFirst({
where: {
article: args.input.article,
organizationId: currentUser.organization.id
}
})
organizationId: currentUser.organization.id,
},
});
if (existingProduct) {
return {
success: false,
message: 'Товар с таким артикулом уже существует'
}
message: "Товар с таким артикулом уже существует",
};
}
try {
@ -2591,74 +3003,78 @@ export const resolvers = {
images: args.input.images || [],
mainImage: args.input.mainImage,
isActive: args.input.isActive ?? true,
organizationId: currentUser.organization.id
organizationId: currentUser.organization.id,
},
include: {
include: {
category: true,
organization: true
}
})
organization: true,
},
});
return {
success: true,
message: 'Товар успешно создан',
product
}
message: "Товар успешно создан",
product,
};
} catch (error) {
console.error('Error creating product:', error)
console.error("Error creating product:", error);
return {
success: false,
message: 'Ошибка при создании товара'
}
message: "Ошибка при создании товара",
};
}
},
// Обновить товар
updateProduct: async (_: unknown, args: {
id: string;
input: {
name: string;
article: string;
description?: string;
price: number;
quantity: number;
categoryId?: string;
brand?: string;
color?: string;
size?: string;
weight?: number;
dimensions?: string;
material?: string;
images?: string[];
mainImage?: string;
isActive?: boolean;
}
}, context: Context) => {
updateProduct: async (
_: unknown,
args: {
id: string;
input: {
name: string;
article: string;
description?: string;
price: number;
quantity: number;
categoryId?: string;
brand?: string;
color?: string;
size?: string;
weight?: number;
dimensions?: string;
material?: string;
images?: string[];
mainImage?: string;
isActive?: boolean;
};
},
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что товар принадлежит текущей организации
const existingProduct = await prisma.product.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id
}
})
organizationId: currentUser.organization.id,
},
});
if (!existingProduct) {
throw new GraphQLError('Товар не найден или нет доступа')
throw new GraphQLError("Товар не найден или нет доступа");
}
// Проверяем уникальность артикула (если он изменился)
@ -2667,15 +3083,15 @@ export const resolvers = {
where: {
article: args.input.article,
organizationId: currentUser.organization.id,
NOT: { id: args.id }
}
})
NOT: { id: args.id },
},
});
if (duplicateProduct) {
return {
success: false,
message: 'Товар с таким артикулом уже существует'
}
message: "Товар с таким артикулом уже существует",
};
}
}
@ -2697,141 +3113,153 @@ export const resolvers = {
material: args.input.material,
images: args.input.images || [],
mainImage: args.input.mainImage,
isActive: args.input.isActive ?? true
isActive: args.input.isActive ?? true,
},
include: {
include: {
category: true,
organization: true
}
})
organization: true,
},
});
return {
success: true,
message: 'Товар успешно обновлен',
product
}
message: "Товар успешно обновлен",
product,
};
} catch (error) {
console.error('Error updating product:', error)
console.error("Error updating product:", error);
return {
success: false,
message: 'Ошибка при обновлении товара'
}
message: "Ошибка при обновлении товара",
};
}
},
// Удалить товар
deleteProduct: async (_: unknown, args: { id: string }, context: Context) => {
deleteProduct: async (
_: unknown,
args: { id: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что товар принадлежит текущей организации
const existingProduct = await prisma.product.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id
}
})
organizationId: currentUser.organization.id,
},
});
if (!existingProduct) {
throw new GraphQLError('Товар не найден или нет доступа')
throw new GraphQLError("Товар не найден или нет доступа");
}
try {
await prisma.product.delete({
where: { id: args.id }
})
where: { id: args.id },
});
return true
return true;
} catch (error) {
console.error('Error deleting product:', error)
return false
console.error("Error deleting product:", error);
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
// Проверяем уникальность названия категории
const existingCategory = await prisma.category.findUnique({
where: { name: args.input.name }
})
where: { name: args.input.name },
});
if (existingCategory) {
return {
success: false,
message: 'Категория с таким названием уже существует'
}
message: "Категория с таким названием уже существует",
};
}
try {
const category = await prisma.category.create({
data: {
name: args.input.name
}
})
name: args.input.name,
},
});
return {
success: true,
message: 'Категория успешно создана',
category
}
message: "Категория успешно создана",
category,
};
} catch (error) {
console.error('Error creating category:', error)
console.error("Error creating category:", error);
return {
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
// Проверяем существование категории
const existingCategory = await prisma.category.findUnique({
where: { id: args.id }
})
where: { id: args.id },
});
if (!existingCategory) {
return {
success: false,
message: 'Категория не найдена'
}
message: "Категория не найдена",
};
}
// Проверяем уникальность нового названия (если изменилось)
if (args.input.name !== existingCategory.name) {
const duplicateCategory = await prisma.category.findUnique({
where: { name: args.input.name }
})
where: { name: args.input.name },
});
if (duplicateCategory) {
return {
success: false,
message: 'Категория с таким названием уже существует'
}
message: "Категория с таким названием уже существует",
};
}
}
@ -2839,113 +3267,123 @@ export const resolvers = {
const category = await prisma.category.update({
where: { id: args.id },
data: {
name: args.input.name
}
})
name: args.input.name,
},
});
return {
success: true,
message: 'Категория успешно обновлена',
category
}
message: "Категория успешно обновлена",
category,
};
} catch (error) {
console.error('Error updating category:', error)
console.error("Error updating category:", error);
return {
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
// Проверяем существование категории
const existingCategory = await prisma.category.findUnique({
where: { id: args.id },
include: { products: true }
})
include: { products: true },
});
if (!existingCategory) {
throw new GraphQLError('Категория не найдена')
throw new GraphQLError("Категория не найдена");
}
// Проверяем, есть ли товары в этой категории
if (existingCategory.products.length > 0) {
throw new GraphQLError('Нельзя удалить категорию, в которой есть товары')
throw new GraphQLError(
"Нельзя удалить категорию, в которой есть товары"
);
}
try {
await prisma.category.delete({
where: { id: args.id }
})
where: { id: args.id },
});
return true
return true;
} catch (error) {
console.error('Error deleting category:', error)
return false
console.error("Error deleting category:", error);
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что товар существует и активен
const product = await prisma.product.findFirst({
where: {
id: args.productId,
isActive: true
isActive: true,
},
include: {
organization: true
}
})
organization: true,
},
});
if (!product) {
return {
success: false,
message: 'Товар не найден или неактивен'
}
message: "Товар не найден или неактивен",
};
}
// Проверяем, что пользователь не пытается добавить свой собственный товар
if (product.organizationId === currentUser.organization.id) {
return {
success: false,
message: 'Нельзя добавлять собственные товары в корзину'
}
message: "Нельзя добавлять собственные товары в корзину",
};
}
// Найти или создать корзину
let cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id }
})
where: { organizationId: currentUser.organization.id },
});
if (!cart) {
cart = await prisma.cart.create({
data: {
organizationId: currentUser.organization.id
}
})
organizationId: currentUser.organization.id,
},
});
}
try {
@ -2954,42 +3392,42 @@ export const resolvers = {
where: {
cartId_productId: {
cartId: cart.id,
productId: args.productId
}
}
})
productId: args.productId,
},
},
});
if (existingCartItem) {
// Обновляем количество
const newQuantity = existingCartItem.quantity + args.quantity
const newQuantity = existingCartItem.quantity + args.quantity;
if (newQuantity > product.quantity) {
return {
success: false,
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`
}
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`,
};
}
await prisma.cartItem.update({
where: { id: existingCartItem.id },
data: { quantity: newQuantity }
})
data: { quantity: newQuantity },
});
} else {
// Создаем новый элемент корзины
if (args.quantity > product.quantity) {
return {
success: false,
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`
}
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`,
};
}
await prisma.cartItem.create({
data: {
cartId: cart.id,
productId: args.productId,
quantity: args.quantity
}
})
quantity: args.quantity,
},
});
}
// Возвращаем обновленную корзину
@ -3003,57 +3441,61 @@ export const resolvers = {
category: true,
organization: {
include: {
users: true
}
}
}
}
}
users: true,
},
},
},
},
},
},
organization: true
}
})
organization: true,
},
});
return {
success: true,
message: 'Товар добавлен в корзину',
cart: updatedCart
}
message: "Товар добавлен в корзину",
cart: updatedCart,
};
} catch (error) {
console.error('Error adding to cart:', error)
console.error("Error adding to cart:", error);
return {
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
const cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id }
})
where: { organizationId: currentUser.organization.id },
});
if (!cart) {
return {
success: false,
message: 'Корзина не найдена'
}
message: "Корзина не найдена",
};
}
// Проверяем, что товар существует в корзине
@ -3061,40 +3503,40 @@ export const resolvers = {
where: {
cartId_productId: {
cartId: cart.id,
productId: args.productId
}
productId: args.productId,
},
},
include: {
product: true
}
})
product: true,
},
});
if (!cartItem) {
return {
success: false,
message: 'Товар не найден в корзине'
}
message: "Товар не найден в корзине",
};
}
if (args.quantity <= 0) {
return {
success: false,
message: 'Количество должно быть больше 0'
}
message: "Количество должно быть больше 0",
};
}
if (args.quantity > cartItem.product.quantity) {
return {
success: false,
message: `Недостаточно товара в наличии. Доступно: ${cartItem.product.quantity}`
}
message: `Недостаточно товара в наличии. Доступно: ${cartItem.product.quantity}`,
};
}
try {
await prisma.cartItem.update({
where: { id: cartItem.id },
data: { quantity: args.quantity }
})
data: { quantity: args.quantity },
});
// Возвращаем обновленную корзину
const updatedCart = await prisma.cart.findUnique({
@ -3107,57 +3549,61 @@ export const resolvers = {
category: true,
organization: {
include: {
users: true
}
}
}
}
}
users: true,
},
},
},
},
},
},
organization: true
}
})
organization: true,
},
});
return {
success: true,
message: 'Количество товара обновлено',
cart: updatedCart
}
message: "Количество товара обновлено",
cart: updatedCart,
};
} catch (error) {
console.error('Error updating cart item:', error)
console.error("Error updating cart item:", error);
return {
success: false,
message: 'Ошибка при обновлении корзины'
}
message: "Ошибка при обновлении корзины",
};
}
},
// Удалить товар из корзины
removeFromCart: async (_: unknown, args: { productId: string }, context: Context) => {
removeFromCart: async (
_: unknown,
args: { productId: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
const cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id }
})
where: { organizationId: currentUser.organization.id },
});
if (!cart) {
return {
success: false,
message: 'Корзина не найдена'
}
message: "Корзина не найдена",
};
}
try {
@ -3165,10 +3611,10 @@ export const resolvers = {
where: {
cartId_productId: {
cartId: cart.id,
productId: args.productId
}
}
})
productId: args.productId,
},
},
});
// Возвращаем обновленную корзину
const updatedCart = await prisma.cart.findUnique({
@ -3181,109 +3627,113 @@ export const resolvers = {
category: true,
organization: {
include: {
users: true
}
}
}
}
}
users: true,
},
},
},
},
},
},
organization: true
}
})
organization: true,
},
});
return {
success: true,
message: 'Товар удален из корзины',
cart: updatedCart
}
message: "Товар удален из корзины",
cart: updatedCart,
};
} catch (error) {
console.error('Error removing from cart:', error)
console.error("Error removing from cart:", error);
return {
success: false,
message: 'Ошибка при удалении из корзины'
}
message: "Ошибка при удалении из корзины",
};
}
},
// Очистить корзину
clearCart: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
const cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id }
})
where: { organizationId: currentUser.organization.id },
});
if (!cart) {
return false
return false;
}
try {
await prisma.cartItem.deleteMany({
where: { cartId: cart.id }
})
where: { cartId: cart.id },
});
return true
return true;
} catch (error) {
console.error('Error clearing cart:', error)
return false
console.error("Error clearing cart:", error);
return false;
}
},
// Добавить товар в избранное
addToFavorites: async (_: unknown, args: { productId: string }, context: Context) => {
addToFavorites: async (
_: unknown,
args: { productId: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что товар существует и активен
const product = await prisma.product.findFirst({
where: {
id: args.productId,
isActive: true
isActive: true,
},
include: {
organization: true
}
})
organization: true,
},
});
if (!product) {
return {
success: false,
message: 'Товар не найден или неактивен'
}
message: "Товар не найден или неактивен",
};
}
// Проверяем, что пользователь не пытается добавить свой собственный товар
if (product.organizationId === currentUser.organization.id) {
return {
success: false,
message: 'Нельзя добавлять собственные товары в избранное'
}
message: "Нельзя добавлять собственные товары в избранное",
};
}
try {
@ -3292,25 +3742,25 @@ export const resolvers = {
where: {
organizationId_productId: {
organizationId: currentUser.organization.id,
productId: args.productId
}
}
})
productId: args.productId,
},
},
});
if (existingFavorite) {
return {
success: false,
message: 'Товар уже в избранном'
}
message: "Товар уже в избранном",
};
}
// Добавляем товар в избранное
await prisma.favorites.create({
data: {
organizationId: currentUser.organization.id,
productId: args.productId
}
})
productId: args.productId,
},
});
// Возвращаем обновленный список избранного
const favorites = await prisma.favorites.findMany({
@ -3321,44 +3771,48 @@ export const resolvers = {
category: true,
organization: {
include: {
users: true
}
}
}
}
users: true,
},
},
},
},
},
orderBy: { createdAt: 'desc' }
})
orderBy: { createdAt: "desc" },
});
return {
success: true,
message: 'Товар добавлен в избранное',
favorites: favorites.map(favorite => favorite.product)
}
message: "Товар добавлен в избранное",
favorites: favorites.map((favorite) => favorite.product),
};
} catch (error) {
console.error('Error adding to favorites:', error)
console.error("Error adding to favorites:", error);
return {
success: false,
message: 'Ошибка при добавлении в избранное'
}
message: "Ошибка при добавлении в избранное",
};
}
},
// Удалить товар из избранного
removeFromFavorites: async (_: unknown, args: { productId: string }, context: Context) => {
removeFromFavorites: async (
_: unknown,
args: { productId: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
try {
@ -3366,9 +3820,9 @@ export const resolvers = {
await prisma.favorites.deleteMany({
where: {
organizationId: currentUser.organization.id,
productId: args.productId
}
})
productId: args.productId,
},
});
// Возвращаем обновленный список избранного
const favorites = await prisma.favorites.findMany({
@ -3379,48 +3833,52 @@ export const resolvers = {
category: true,
organization: {
include: {
users: true
}
}
}
}
users: true,
},
},
},
},
},
orderBy: { createdAt: 'desc' }
})
orderBy: { createdAt: "desc" },
});
return {
success: true,
message: 'Товар удален из избранного',
favorites: favorites.map(favorite => favorite.product)
}
message: "Товар удален из избранного",
favorites: favorites.map((favorite) => favorite.product),
};
} catch (error) {
console.error('Error removing from favorites:', error)
console.error("Error removing from favorites:", error);
return {
success: false,
message: 'Ошибка при удалении из избранного'
}
message: "Ошибка при удалении из избранного",
};
}
},
// Создать сотрудника
createEmployee: async (_: unknown, args: { input: CreateEmployeeInput }, context: Context) => {
createEmployee: async (
_: unknown,
args: { input: CreateEmployeeInput },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError("Доступно только для фулфилмент центров");
}
try {
@ -3428,136 +3886,158 @@ export const resolvers = {
data: {
...args.input,
organizationId: currentUser.organization.id,
birthDate: args.input.birthDate ? new Date(args.input.birthDate) : undefined,
passportDate: args.input.passportDate ? new Date(args.input.passportDate) : undefined,
hireDate: new Date(args.input.hireDate)
birthDate: args.input.birthDate
? new Date(args.input.birthDate)
: undefined,
passportDate: args.input.passportDate
? new Date(args.input.passportDate)
: undefined,
hireDate: new Date(args.input.hireDate),
},
include: {
organization: true
}
})
organization: true,
},
});
return {
success: true,
message: 'Сотрудник успешно добавлен',
employee
}
message: "Сотрудник успешно добавлен",
employee,
};
} catch (error) {
console.error('Error creating employee:', error)
console.error("Error creating employee:", error);
return {
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError("Доступно только для фулфилмент центров");
}
try {
const employee = await prisma.employee.update({
where: {
where: {
id: args.id,
organizationId: currentUser.organization.id
organizationId: currentUser.organization.id,
},
data: {
...args.input,
birthDate: args.input.birthDate ? new Date(args.input.birthDate) : undefined,
passportDate: args.input.passportDate ? new Date(args.input.passportDate) : undefined,
hireDate: args.input.hireDate ? new Date(args.input.hireDate) : undefined
birthDate: args.input.birthDate
? new Date(args.input.birthDate)
: undefined,
passportDate: args.input.passportDate
? new Date(args.input.passportDate)
: undefined,
hireDate: args.input.hireDate
? new Date(args.input.hireDate)
: undefined,
},
include: {
organization: true
}
})
organization: true,
},
});
return {
success: true,
message: 'Сотрудник успешно обновлен',
employee
}
message: "Сотрудник успешно обновлен",
employee,
};
} catch (error) {
console.error('Error updating employee:', error)
console.error("Error updating employee:", error);
return {
success: false,
message: 'Ошибка при обновлении сотрудника'
}
message: "Ошибка при обновлении сотрудника",
};
}
},
// Удалить сотрудника
deleteEmployee: async (_: unknown, args: { id: string }, context: Context) => {
deleteEmployee: async (
_: unknown,
args: { id: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError("Доступно только для фулфилмент центров");
}
try {
await prisma.employee.delete({
where: {
where: {
id: args.id,
organizationId: currentUser.organization.id
}
})
organizationId: currentUser.organization.id,
},
});
return true
return true;
} catch (error) {
console.error('Error deleting employee:', error)
return false
console.error("Error deleting employee:", error);
return false;
}
},
// Обновить табель сотрудника
updateEmployeeSchedule: async (_: unknown, args: { input: UpdateScheduleInput }, context: Context) => {
updateEmployeeSchedule: async (
_: unknown,
args: { input: UpdateScheduleInput },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError("Доступно только для фулфилмент центров");
}
try {
@ -3565,12 +4045,12 @@ export const resolvers = {
const employee = await prisma.employee.findFirst({
where: {
id: args.input.employeeId,
organizationId: currentUser.organization.id
}
})
organizationId: currentUser.organization.id,
},
});
if (!employee) {
throw new GraphQLError('Сотрудник не найден')
throw new GraphQLError("Сотрудник не найден");
}
// Создаем или обновляем запись табеля
@ -3578,73 +4058,89 @@ export const resolvers = {
where: {
employeeId_date: {
employeeId: args.input.employeeId,
date: new Date(args.input.date)
}
date: new Date(args.input.date),
},
},
create: {
employeeId: args.input.employeeId,
date: new Date(args.input.date),
status: args.input.status,
hoursWorked: args.input.hoursWorked,
notes: args.input.notes
notes: args.input.notes,
},
update: {
status: args.input.status,
hoursWorked: args.input.hoursWorked,
notes: args.input.notes
}
})
notes: args.input.notes,
},
});
return true
return true;
} catch (error) {
console.error('Error updating employee schedule:', error)
return false
console.error("Error updating employee schedule:", error);
return false;
}
},
// Создать поставку Wildberries
createWildberriesSupply: async (_: unknown, args: { input: { cards: Array<{ price: number; discountedPrice?: number; selectedQuantity: number; selectedServices?: string[] }> } }, context: Context) => {
createWildberriesSupply: async (
_: unknown,
args: {
input: {
cards: Array<{
price: number;
discountedPrice?: number;
selectedQuantity: number;
selectedServices?: string[];
}>;
};
},
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
try {
// Пока что просто логируем данные, так как таблицы еще нет
console.log('Создание поставки Wildberries с данными:', args.input)
const totalAmount = args.input.cards.reduce((sum: number, card) => {
const cardPrice = card.discountedPrice || card.price
const servicesPrice = (card.selectedServices?.length || 0) * 50
return sum + (cardPrice + servicesPrice) * card.selectedQuantity
}, 0)
console.log("Создание поставки Wildberries с данными:", args.input);
const totalItems = args.input.cards.reduce((sum: number, card) => sum + card.selectedQuantity, 0)
const totalAmount = args.input.cards.reduce((sum: number, card) => {
const cardPrice = card.discountedPrice || card.price;
const servicesPrice = (card.selectedServices?.length || 0) * 50;
return sum + (cardPrice + servicesPrice) * card.selectedQuantity;
}, 0);
const totalItems = args.input.cards.reduce(
(sum: number, card) => sum + card.selectedQuantity,
0
);
// Временная заглушка - вернем success без создания в БД
return {
success: true,
message: `Поставка создана успешно! Товаров: ${totalItems}, Сумма: ${totalAmount} руб.`,
supply: null // Временно null
}
supply: null, // Временно null
};
} catch (error) {
console.error('Error creating Wildberries supply:', error)
console.error("Error creating Wildberries supply:", error);
return {
success: false,
message: 'Ошибка при создании поставки Wildberries'
}
message: "Ошибка при создании поставки Wildberries",
};
}
}
},
},
// Резолверы типов
@ -3652,158 +4148,180 @@ export const resolvers = {
users: async (parent: { id: string; users?: unknown[] }) => {
// Если пользователи уже загружены через include, возвращаем их
if (parent.users) {
return parent.users
return parent.users;
}
// Иначе загружаем отдельно
return await prisma.user.findMany({
where: { organizationId: parent.id }
})
}
where: { organizationId: parent.id },
});
},
},
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 total + (Number(item.product.price) * item.quantity)
}, 0)
return total + Number(item.product.price) * item.quantity;
}, 0);
},
totalItems: (parent: { items: Array<{ quantity: number }> }) => {
return parent.items.reduce((total, item) => total + item.quantity, 0)
}
return parent.items.reduce((total, item) => total + item.quantity, 0);
},
},
CartItem: {
totalPrice: (parent: { product: { price: number }, quantity: number }) => {
return Number(parent.product.price) * parent.quantity
totalPrice: (parent: { product: { price: number }; quantity: number }) => {
return Number(parent.product.price) * parent.quantity;
},
isAvailable: (parent: { product: { quantity: number, isActive: boolean }, quantity: number }) => {
return parent.product.isActive && parent.product.quantity >= parent.quantity
isAvailable: (parent: {
product: { quantity: number; isActive: boolean };
quantity: number;
}) => {
return (
parent.product.isActive && parent.product.quantity >= parent.quantity
);
},
availableQuantity: (parent: { product: { quantity: number } }) => {
return parent.product.quantity
}
return parent.product.quantity;
},
},
User: {
organization: async (parent: { organizationId?: string; organization?: unknown }) => {
organization: async (parent: {
organizationId?: string;
organization?: unknown;
}) => {
// Если организация уже загружена через include, возвращаем её
if (parent.organization) {
return parent.organization
return parent.organization;
}
// Иначе загружаем отдельно если есть organizationId
if (parent.organizationId) {
return await prisma.organization.findUnique({
where: { id: parent.organizationId },
include: {
apiKeys: true,
users: true
}
})
users: true,
},
});
}
return null
}
return null;
},
},
Message: {
type: (parent: { type?: string | null }) => {
return parent.type || 'TEXT'
return parent.type || "TEXT";
},
createdAt: (parent: { createdAt: Date | string }) => {
if (parent.createdAt instanceof Date) {
return parent.createdAt.toISOString()
return parent.createdAt.toISOString();
}
return parent.createdAt
return parent.createdAt;
},
updatedAt: (parent: { updatedAt: Date | string }) => {
if (parent.updatedAt instanceof Date) {
return parent.updatedAt.toISOString()
return parent.updatedAt.toISOString();
}
return parent.updatedAt
}
return parent.updatedAt;
},
},
Employee: {
birthDate: (parent: { birthDate?: Date | string | null }) => {
if (!parent.birthDate) return null
if (!parent.birthDate) return null;
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 }) => {
if (!parent.passportDate) return null
if (!parent.passportDate) return null;
if (parent.passportDate instanceof Date) {
return parent.passportDate.toISOString()
return parent.passportDate.toISOString();
}
return parent.passportDate
return parent.passportDate;
},
hireDate: (parent: { hireDate: Date | string }) => {
if (parent.hireDate instanceof Date) {
return parent.hireDate.toISOString()
return parent.hireDate.toISOString();
}
return parent.hireDate
return parent.hireDate;
},
createdAt: (parent: { createdAt: Date | string }) => {
if (parent.createdAt instanceof Date) {
return parent.createdAt.toISOString()
return parent.createdAt.toISOString();
}
return parent.createdAt
return parent.createdAt;
},
updatedAt: (parent: { updatedAt: Date | string }) => {
if (parent.updatedAt instanceof Date) {
return parent.updatedAt.toISOString()
return parent.updatedAt.toISOString();
}
return parent.updatedAt
}
return parent.updatedAt;
},
},
EmployeeSchedule: {
date: (parent: { date: Date | string }) => {
if (parent.date instanceof Date) {
return parent.date.toISOString()
return parent.date.toISOString();
}
return parent.date
return parent.date;
},
createdAt: (parent: { createdAt: Date | string }) => {
if (parent.createdAt instanceof Date) {
return parent.createdAt.toISOString()
return parent.createdAt.toISOString();
}
return parent.createdAt
return parent.createdAt;
},
updatedAt: (parent: { updatedAt: Date | string }) => {
if (parent.updatedAt instanceof Date) {
return parent.updatedAt.toISOString()
return parent.updatedAt.toISOString();
}
return parent.updatedAt
return parent.updatedAt;
},
employee: async (parent: { employeeId: string }) => {
return await prisma.employee.findUnique({
where: { id: parent.employeeId }
})
}
}
}
where: { id: parent.employeeId },
});
},
},
};
// Логистические мутации
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
try {
@ -3814,44 +4332,57 @@ const logisticsMutations = {
priceUnder1m3: args.input.priceUnder1m3,
priceOver1m3: args.input.priceOver1m3,
description: args.input.description,
organizationId: currentUser.organization.id
organizationId: currentUser.organization.id,
},
include: {
organization: true
}
})
organization: true,
},
});
console.log('✅ Logistics created:', logistics.id)
console.log("✅ Logistics created:", logistics.id);
return {
success: true,
message: 'Логистический маршрут создан',
logistics
}
message: "Логистический маршрут создан",
logistics,
};
} catch (error) {
console.error('❌ Error creating logistics:', error)
console.error("❌ Error creating logistics:", error);
return {
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) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
try {
@ -3859,12 +4390,12 @@ const logisticsMutations = {
const existingLogistics = await prisma.logistics.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id
}
})
organizationId: currentUser.organization.id,
},
});
if (!existingLogistics) {
throw new GraphQLError('Логистический маршрут не найден')
throw new GraphQLError("Логистический маршрут не найден");
}
const logistics = await prisma.logistics.update({
@ -3874,44 +4405,48 @@ const logisticsMutations = {
toLocation: args.input.toLocation,
priceUnder1m3: args.input.priceUnder1m3,
priceOver1m3: args.input.priceOver1m3,
description: args.input.description
description: args.input.description,
},
include: {
organization: true
}
})
organization: true,
},
});
console.log('✅ Logistics updated:', logistics.id)
console.log("✅ Logistics updated:", logistics.id);
return {
success: true,
message: 'Логистический маршрут обновлен',
logistics
}
message: "Логистический маршрут обновлен",
logistics,
};
} catch (error) {
console.error('❌ Error updating logistics:', error)
console.error("❌ Error updating logistics:", error);
return {
success: false,
message: 'Ошибка при обновлении логистического маршрута'
}
message: "Ошибка при обновлении логистического маршрута",
};
}
},
// Удалить логистический маршрут
deleteLogistics: async (_: unknown, args: { id: string }, context: Context) => {
deleteLogistics: async (
_: unknown,
args: { id: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true }
})
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
throw new GraphQLError("У пользователя нет организации");
}
try {
@ -3919,192 +4454,202 @@ const logisticsMutations = {
const existingLogistics = await prisma.logistics.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id
}
})
organizationId: currentUser.organization.id,
},
});
if (!existingLogistics) {
throw new GraphQLError('Логистический маршрут не найден')
throw new GraphQLError("Логистический маршрут не найден");
}
await prisma.logistics.delete({
where: { id: args.id }
})
where: { id: args.id },
});
console.log('✅ Logistics deleted:', args.id)
return true
console.log("✅ Logistics deleted:", args.id);
return true;
} catch (error) {
console.error('❌ Error deleting logistics:', error)
return false
console.error("❌ Error deleting logistics:", error);
return false;
}
}
}
},
};
// Добавляем логистические мутации к основным резолверам
resolvers.Mutation = {
...resolvers.Mutation,
...logisticsMutations
}
...logisticsMutations,
};
// Админ резолверы
const adminQueries = {
adminMe: async (_: unknown, __: unknown, context: Context) => {
if (!context.admin) {
throw new GraphQLError('Требуется авторизация администратора', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация администратора", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const admin = await prisma.admin.findUnique({
where: { id: context.admin.id }
})
where: { id: context.admin.id },
});
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) {
throw new GraphQLError('Требуется авторизация администратора', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация администратора", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const limit = args.limit || 50
const offset = args.offset || 0
const limit = args.limit || 50;
const offset = args.offset || 0;
// Строим условие поиска
const whereCondition: Prisma.UserWhereInput = args.search
? {
OR: [
{ phone: { contains: args.search, mode: 'insensitive' } },
{ managerName: { contains: args.search, mode: 'insensitive' } },
{
{ phone: { contains: args.search, mode: "insensitive" } },
{ managerName: { contains: args.search, mode: "insensitive" } },
{
organization: {
OR: [
{ name: { contains: args.search, mode: 'insensitive' } },
{ fullName: { contains: args.search, mode: 'insensitive' } },
{ inn: { contains: args.search, mode: 'insensitive' } }
]
}
}
]
{ name: { contains: args.search, mode: "insensitive" } },
{ fullName: { contains: args.search, mode: "insensitive" } },
{ inn: { contains: args.search, mode: "insensitive" } },
],
},
},
],
}
: {}
: {};
// Получаем пользователей с пагинацией
const [users, total] = await Promise.all([
prisma.user.findMany({
where: whereCondition,
include: {
organization: true
organization: true,
},
take: limit,
skip: offset,
orderBy: { createdAt: 'desc' }
orderBy: { createdAt: "desc" },
}),
prisma.user.count({
where: whereCondition
})
])
where: whereCondition,
}),
]);
return {
users,
total,
hasMore: offset + limit < total
}
}
}
hasMore: offset + limit < total,
};
},
};
const adminMutations = {
adminLogin: async (_: unknown, args: { username: string; password: string }) => {
adminLogin: async (
_: unknown,
args: { username: string; password: string }
) => {
try {
// Найти администратора
const admin = await prisma.admin.findUnique({
where: { username: args.username }
})
where: { username: args.username },
});
if (!admin) {
return {
success: false,
message: 'Неверные учетные данные'
}
message: "Неверные учетные данные",
};
}
// Проверить активность
if (!admin.isActive) {
return {
success: false,
message: 'Аккаунт заблокирован'
}
message: "Аккаунт заблокирован",
};
}
// Проверить пароль
const isPasswordValid = await bcrypt.compare(args.password, admin.password)
const isPasswordValid = await bcrypt.compare(
args.password,
admin.password
);
if (!isPasswordValid) {
return {
success: false,
message: 'Неверные учетные данные'
}
message: "Неверные учетные данные",
};
}
// Обновить время последнего входа
await prisma.admin.update({
where: { id: admin.id },
data: { lastLogin: new Date() }
})
data: { lastLogin: new Date() },
});
// Создать токен
const token = jwt.sign(
{
{
adminId: admin.id,
username: admin.username,
type: 'admin'
type: "admin",
},
process.env.JWT_SECRET!,
{ expiresIn: '24h' }
)
{ expiresIn: "24h" }
);
return {
success: true,
message: 'Успешная авторизация',
message: "Успешная авторизация",
token,
admin: {
...admin,
password: undefined // Не возвращаем пароль
}
}
password: undefined, // Не возвращаем пароль
},
};
} catch (error) {
console.error('Admin login error:', error)
console.error("Admin login error:", error);
return {
success: false,
message: 'Ошибка авторизации'
}
message: "Ошибка авторизации",
};
}
},
adminLogout: async (_: unknown, __: unknown, context: Context) => {
if (!context.admin) {
throw new GraphQLError('Требуется авторизация администратора', {
extensions: { code: 'UNAUTHENTICATED' }
})
throw new GraphQLError("Требуется авторизация администратора", {
extensions: { code: "UNAUTHENTICATED" },
});
}
return true
}
}
return true;
},
};
// Добавляем админ запросы и мутации к основным резолверам
resolvers.Query = {
...resolvers.Query,
...adminQueries
}
...adminQueries,
};
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`
scalar DateTime
@ -6,31 +6,34 @@ export const typeDefs = gql`
type Query {
me: User
organization(id: ID!): Organization
# Поиск организаций по типу для добавления в контрагенты
searchOrganizations(type: OrganizationType, search: String): [Organization!]!
searchOrganizations(
type: OrganizationType
search: String
): [Organization!]!
# Мои контрагенты
myCounterparties: [Organization!]!
# Входящие заявки
incomingRequests: [CounterpartyRequest!]!
# Исходящие заявки
outgoingRequests: [CounterpartyRequest!]!
# Сообщения с контрагентом
messages(counterpartyId: ID!, limit: Int, offset: Int): [Message!]!
# Список чатов (последние сообщения с каждым контрагентом)
conversations: [Conversation!]!
# Услуги организации
myServices: [Service!]!
# Расходники организации
mySupplies: [Supply!]!
# Логистика организации
myLogistics: [Logistics!]!
@ -39,26 +42,30 @@ export const typeDefs = gql`
# Товары оптовика
myProducts: [Product!]!
# Все товары всех оптовиков для маркета
allProducts(search: String, category: String): [Product!]!
# Все категории
categories: [Category!]!
# Корзина пользователя
myCart: Cart
# Избранные товары пользователя
myFavorites: [Product!]!
# Сотрудники организации
myEmployees: [Employee!]!
employee(id: ID!): Employee
# Табель сотрудника за месяц
employeeSchedule(employeeId: ID!, year: Int!, month: Int!): [EmployeeSchedule!]!
employeeSchedule(
employeeId: ID!
year: Int!
month: Int!
): [EmployeeSchedule!]!
# Админ запросы
adminMe: Admin
allUsers(search: String, limit: Int, offset: Int): UsersResponse!
@ -68,75 +75,108 @@ export const typeDefs = gql`
# Авторизация через SMS
sendSmsCode(phone: String!): SmsResponse!
verifySmsCode(phone: String!, code: String!): AuthResponse!
# Валидация ИНН
verifyInn(inn: String!): InnValidationResponse!
# Обновление профиля пользователя
updateUserProfile(input: UpdateUserProfileInput!): UpdateUserProfileResponse!
updateUserProfile(
input: UpdateUserProfileInput!
): UpdateUserProfileResponse!
# Обновление данных организации по ИНН
updateOrganizationByInn(inn: String!): UpdateOrganizationResponse!
# Регистрация организации
registerFulfillmentOrganization(input: FulfillmentRegistrationInput!): AuthResponse!
registerFulfillmentOrganization(
input: FulfillmentRegistrationInput!
): AuthResponse!
registerSellerOrganization(input: SellerRegistrationInput!): AuthResponse!
# Работа с API ключами
addMarketplaceApiKey(input: MarketplaceApiKeyInput!): ApiKeyResponse!
removeMarketplaceApiKey(marketplace: MarketplaceType!): Boolean!
# Выход из системы
logout: Boolean!
# Работа с контрагентами
sendCounterpartyRequest(organizationId: ID!, message: String): CounterpartyRequestResponse!
respondToCounterpartyRequest(requestId: ID!, accept: Boolean!): CounterpartyRequestResponse!
sendCounterpartyRequest(
organizationId: ID!
message: String
): CounterpartyRequestResponse!
respondToCounterpartyRequest(
requestId: ID!
accept: Boolean!
): CounterpartyRequestResponse!
cancelCounterpartyRequest(requestId: ID!): Boolean!
removeCounterparty(organizationId: ID!): Boolean!
# Работа с сообщениями
sendMessage(receiverOrganizationId: ID!, content: String, 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!
sendMessage(
receiverOrganizationId: ID!
content: String
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!
# Работа с услугами
createService(input: ServiceInput!): ServiceResponse!
updateService(id: ID!, input: ServiceInput!): ServiceResponse!
deleteService(id: ID!): Boolean!
# Работа с расходниками
createSupply(input: SupplyInput!): SupplyResponse!
updateSupply(id: ID!, input: SupplyInput!): SupplyResponse!
deleteSupply(id: ID!): Boolean!
# Заказы поставок расходников
createSupplyOrder(input: SupplyOrderInput!): SupplyOrderResponse!
# Работа с логистикой
createLogistics(input: LogisticsInput!): LogisticsResponse!
updateLogistics(id: ID!, input: LogisticsInput!): LogisticsResponse!
deleteLogistics(id: ID!): Boolean!
# Работа с товарами (для оптовиков)
createProduct(input: ProductInput!): ProductResponse!
updateProduct(id: ID!, input: ProductInput!): ProductResponse!
deleteProduct(id: ID!): Boolean!
# Работа с категориями
createCategory(input: CategoryInput!): CategoryResponse!
updateCategory(id: ID!, input: CategoryInput!): CategoryResponse!
deleteCategory(id: ID!): Boolean!
# Работа с корзиной
addToCart(productId: ID!, quantity: Int = 1): CartResponse!
updateCartItem(productId: ID!, quantity: Int!): CartResponse!
removeFromCart(productId: ID!): CartResponse!
clearCart: Boolean!
# Работа с избранным
addToFavorites(productId: ID!): FavoritesResponse!
removeFromFavorites(productId: ID!): FavoritesResponse!
# Работа с сотрудниками
createEmployee(input: CreateEmployeeInput!): EmployeeResponse!
updateEmployee(id: ID!, input: UpdateEmployeeInput!): EmployeeResponse!
@ -216,14 +256,14 @@ export const typeDefs = gql`
input UpdateUserProfileInput {
# Аватар пользователя
avatar: String
# Контактные данные организации
orgPhone: String
managerName: String
telegram: String
whatsapp: String
email: String
# Банковские данные
bankName: String
bik: String
@ -422,6 +462,55 @@ export const typeDefs = gql`
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 {
id: ID!
@ -778,4 +867,4 @@ export const typeDefs = gql`
message: String!
supply: WildberriesSupply
}
`
`;