diff --git a/src/app/globals.css b/src/app/globals.css index 86bbb38..759c9ba 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,6 +3,38 @@ @custom-variant dark (&:is(.dark *)); +/* Кастомные скроллбары */ +.scrollbar-thin { + scrollbar-width: thin; +} + +.scrollbar-thumb-white\/20 { + scrollbar-color: rgba(255, 255, 255, 0.2) transparent; +} + +.scrollbar-track-transparent { + scrollbar-color: rgba(255, 255, 255, 0.2) transparent; +} + +/* Webkit скроллбары для браузеров на базе Chromium */ +.scrollbar-thin::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.scrollbar-thin::-webkit-scrollbar-track { + background: transparent; +} + +.scrollbar-thin::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, 0.2); + border-radius: 3px; +} + +.scrollbar-thin::-webkit-scrollbar-thumb:hover { + background-color: rgba(255, 255, 255, 0.3); +} + @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); diff --git a/src/components/supplies/wb-product-cards.tsx b/src/components/supplies/wb-product-cards.tsx index b369c89..eba7330 100644 --- a/src/components/supplies/wb-product-cards.tsx +++ b/src/components/supplies/wb-product-cards.tsx @@ -30,7 +30,8 @@ import { import { WildberriesService } from '@/services/wildberries-service' import { useAuth } from '@/hooks/useAuth' import { useQuery, useMutation } from '@apollo/client' -import { GET_MY_COUNTERPARTIES } from '@/graphql/queries' +import { apolloClient } from '@/lib/apollo-client' +import { GET_MY_COUNTERPARTIES, GET_COUNTERPARTY_SERVICES, GET_COUNTERPARTY_SUPPLIES } from '@/graphql/queries' import { CREATE_WILDBERRIES_SUPPLY } from '@/graphql/mutations' import { toast } from 'sonner' @@ -58,12 +59,12 @@ interface WildberriesCard { interface SelectedCard { card: WildberriesCard selectedQuantity: number - selectedMarket: string - selectedPlace: string - sellerName: string - sellerPhone: string + customPrice: number // Пользовательская цена за все количество + selectedFulfillmentOrg: string // ID выбранной FF организации + selectedFulfillmentServices: string[] // ID выбранных услуг FF (множественный выбор) + selectedConsumableOrg: string // ID выбранной организации расходников + selectedConsumableServices: string[] // ID выбранных расходников (множественный выбор) deliveryDate: string - selectedServices: string[] } interface FulfillmentService { @@ -74,6 +75,13 @@ interface FulfillmentService { organizationName: string } +interface Organization { + id: string + name?: string + fullName?: string + type: string +} + interface WBProductCardsProps { onBack: () => void onComplete: (selectedCards: SelectedCard[]) => void @@ -88,6 +96,8 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { const [selectedCards, setSelectedCards] = useState([]) const [showSummary, setShowSummary] = useState(false) const [fulfillmentServices, setFulfillmentServices] = useState([]) + const [organizationServices, setOrganizationServices] = useState<{[orgId: string]: Array<{id: string, name: string, description?: string, price: number}>}>({}) + const [organizationSupplies, setOrganizationSupplies] = useState<{[orgId: string]: Array<{id: string, name: string, description?: string, price: number}>}>({}) const [selectedCardForDetails, setSelectedCardForDetails] = useState(null) const [currentImageIndex, setCurrentImageIndex] = useState(0) @@ -261,6 +271,60 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { // Загружаем контрагентов-фулфилментов const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES) + + // Автоматически загружаем услуги и расходники для уже выбранных организаций + useEffect(() => { + selectedCards.forEach(sc => { + if (sc.selectedFulfillmentOrg && !organizationServices[sc.selectedFulfillmentOrg]) { + loadOrganizationServices(sc.selectedFulfillmentOrg) + } + if (sc.selectedConsumableOrg && !organizationSupplies[sc.selectedConsumableOrg]) { + loadOrganizationSupplies(sc.selectedConsumableOrg) + } + }) + }, [selectedCards]) + + // Функция для загрузки услуг организации + const loadOrganizationServices = async (organizationId: string) => { + if (organizationServices[organizationId]) return // Уже загружены + + try { + const response = await apolloClient.query({ + query: GET_COUNTERPARTY_SERVICES, + variables: { organizationId } + }) + + if (response.data?.counterpartyServices) { + setOrganizationServices(prev => ({ + ...prev, + [organizationId]: response.data.counterpartyServices + })) + } + } catch (error) { + console.error('Ошибка загрузки услуг организации:', error) + } + } + + // Функция для загрузки расходников организации + const loadOrganizationSupplies = async (organizationId: string) => { + if (organizationSupplies[organizationId]) return // Уже загружены + + try { + const response = await apolloClient.query({ + query: GET_COUNTERPARTY_SUPPLIES, + variables: { organizationId } + }) + + if (response.data?.counterpartySupplies) { + setOrganizationSupplies(prev => ({ + ...prev, + [organizationId]: response.data.counterpartySupplies + })) + } + } catch (error) { + console.error('Ошибка загрузки расходников организации:', error) + } + } // Мутация для создания поставки const [createSupply, { loading: creatingSupply }] = useMutation(CREATE_WILDBERRIES_SUPPLY, { @@ -286,48 +350,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { { value: 'food-city', label: 'Фуд Сити' } ] - useEffect(() => { - // Загружаем услуги фулфилмента из контрагентов - if (counterpartiesData?.myCounterparties) { - interface Organization { - id: string - name?: string - fullName?: string - type: string - } - const fulfillmentOrganizations = counterpartiesData.myCounterparties.filter( - (org: Organization) => org.type === 'FULFILLMENT' - ) - - // В реальном приложении здесь был бы запрос услуг для каждой организации - const mockServices: FulfillmentService[] = fulfillmentOrganizations.flatMap((org: Organization) => [ - { - id: `${org.id}-packaging`, - name: 'Упаковка товаров', - description: 'Профессиональная упаковка товаров', - price: 50, - organizationName: org.name || org.fullName - }, - { - id: `${org.id}-labeling`, - name: 'Маркировка товаров', - description: 'Нанесение этикеток и штрих-кодов', - price: 30, - organizationName: org.name || org.fullName - }, - { - id: `${org.id}-quality-check`, - name: 'Контроль качества', - description: 'Проверка качества товаров', - price: 100, - organizationName: org.name || org.fullName - } - ]) - - setFulfillmentServices(mockServices) - } - }, [counterpartiesData]) // Автоматически загружаем товары при открытии компонента useEffect(() => { @@ -491,12 +514,12 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { const newSelectedCard: SelectedCard = { card, selectedQuantity: value as number, - selectedMarket: '', - selectedPlace: '', - sellerName: '', - sellerPhone: '', - deliveryDate: '', - selectedServices: [] + customPrice: 0, + selectedFulfillmentOrg: '', + selectedFulfillmentServices: [], + selectedConsumableOrg: '', + selectedConsumableServices: [], + deliveryDate: '' } return [...prev, newSelectedCard] } @@ -518,14 +541,50 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { }).format(amount) } + // Функция для получения цены услуги по ID + const getServicePrice = (orgId: string, serviceId: string): number => { + const services = organizationServices[orgId] + if (!services) return 0 + const service = services.find(s => s.id === serviceId) + return service ? service.price : 0 + } + + // Функция для получения цены расходника по ID + const getSupplyPrice = (orgId: string, supplyId: string): number => { + const supplies = organizationSupplies[orgId] + if (!supplies) return 0 + const supply = supplies.find(s => s.id === supplyId) + return supply ? supply.price : 0 + } + + // Функция для расчета стоимости услуг и расходников за 1 штуку + const calculateAdditionalCostPerUnit = (sc: SelectedCard): number => { + let servicesCost = 0 + let suppliesCost = 0 + + // Стоимость услуг фулфилмента + if (sc.selectedFulfillmentOrg && sc.selectedFulfillmentServices.length > 0) { + servicesCost = sc.selectedFulfillmentServices.reduce((sum, serviceId) => { + return sum + getServicePrice(sc.selectedFulfillmentOrg, serviceId) + }, 0) + } + + // Стоимость расходных материалов + if (sc.selectedConsumableOrg && sc.selectedConsumableServices.length > 0) { + suppliesCost = sc.selectedConsumableServices.reduce((sum, supplyId) => { + return sum + getSupplyPrice(sc.selectedConsumableOrg, supplyId) + }, 0) + } + + return servicesCost + suppliesCost + } + const getTotalAmount = () => { return selectedCards.reduce((sum, sc) => { - const cardPrice = sc.card.sizes[0]?.discountedPrice || sc.card.sizes[0]?.price || 0 - const servicesPrice = sc.selectedServices.reduce((serviceSum, serviceId) => { - const service = fulfillmentServices.find(s => s.id === serviceId) - return serviceSum + (service?.price || 0) - }, 0) - return sum + (cardPrice + servicesPrice) * sc.selectedQuantity + const additionalCostPerUnit = calculateAdditionalCostPerUnit(sc) + const totalCostPerUnit = (sc.customPrice / sc.selectedQuantity) + additionalCostPerUnit + const totalCostForAllItems = totalCostPerUnit * sc.selectedQuantity + return sum + totalCostForAllItems }, 0) } @@ -533,11 +592,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { return selectedCards.reduce((sum, sc) => sum + sc.selectedQuantity, 0) } - const applyServicesToAll = (serviceIds: string[]) => { - setSelectedCards(prev => - prev.map(sc => ({ ...sc, selectedServices: serviceIds })) - ) - } + // Функция больше не нужна, так как услуги выбираются индивидуально const handleCardClick = (card: WildberriesCard) => { setSelectedCardForDetails(card) @@ -570,17 +625,14 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { vendorCode: sc.card.vendorCode, title: sc.card.title, brand: sc.card.brand, - price: sc.card.sizes[0]?.price || 0, - discountedPrice: sc.card.sizes[0]?.discountedPrice || null, - quantity: sc.card.sizes[0]?.quantity || 0, selectedQuantity: sc.selectedQuantity, - selectedMarket: sc.selectedMarket, - selectedPlace: sc.selectedPlace, - sellerName: sc.sellerName, - sellerPhone: sc.sellerPhone, - deliveryDate: sc.deliveryDate || null, - mediaFiles: sc.card.mediaFiles, - selectedServices: sc.selectedServices + customPrice: sc.customPrice, + selectedFulfillmentOrg: sc.selectedFulfillmentOrg, + selectedFulfillmentServices: sc.selectedFulfillmentServices, + selectedConsumableOrg: sc.selectedConsumableOrg, + selectedConsumableServices: sc.selectedConsumableServices, + deliveryDate: sc.deliveryDate || null, + mediaFiles: sc.card.mediaFiles })) } @@ -625,72 +677,245 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
{selectedCards.map((sc) => { - const cardPrice = sc.card.sizes[0]?.discountedPrice || sc.card.sizes[0]?.price || 0 - const servicesPrice = sc.selectedServices.reduce((sum, serviceId) => { - const service = fulfillmentServices.find(s => s.id === serviceId) - return sum + (service?.price || 0) - }, 0) - const totalPrice = (cardPrice + servicesPrice) * sc.selectedQuantity + const fulfillmentOrgs = (counterpartiesData?.myCounterparties || []).filter((org: Organization) => org.type === 'FULFILLMENT') + const consumableOrgs = (counterpartiesData?.myCounterparties || []).filter((org: Organization) => org.type === 'FULFILLMENT') return (
{sc.card.title} -
+

{sc.card.title}

{sc.card.vendorCode}

-
-
- Количество: - {sc.selectedQuantity} +
+ {/* Количество и цена */} +
+
+ + updateCardSelection(sc.card, 'selectedQuantity', parseInt(e.target.value) || 0)} + className="bg-white/5 border-white/20 text-white mt-1" + min="1" + /> +
+
+ + { + const pricePerUnit = e.target.value === '' ? 0 : parseFloat(e.target.value) || 0 + const totalPrice = pricePerUnit * sc.selectedQuantity + updateCardSelection(sc.card, 'customPrice', totalPrice) + }} + className="bg-white/5 border-white/20 text-white mt-1" + placeholder="Введите цену за 1 штуку" + /> + + {/* Показываем расчет дополнительных расходов */} + {(() => { + const additionalCost = calculateAdditionalCostPerUnit(sc) + if (additionalCost > 0) { + return ( +
+
Дополнительные расходы за 1 шт:
+ {sc.selectedFulfillmentServices.length > 0 && ( +
+ Услуги: {sc.selectedFulfillmentServices.map(serviceId => { + const price = getServicePrice(sc.selectedFulfillmentOrg, serviceId) + const services = organizationServices[sc.selectedFulfillmentOrg] + const service = services?.find(s => s.id === serviceId) + return service ? `${service.name} (${price}₽)` : '' + }).join(', ')} +
+ )} + {sc.selectedConsumableServices.length > 0 && ( +
+ Расходники: {sc.selectedConsumableServices.map(supplyId => { + const price = getSupplyPrice(sc.selectedConsumableOrg, supplyId) + const supplies = organizationSupplies[sc.selectedConsumableOrg] + const supply = supplies?.find(s => s.id === supplyId) + return supply ? `${supply.name} (${price}₽)` : '' + }).join(', ')} +
+ )} +
+ Итого доп. расходы: {formatCurrency(additionalCost)} +
+
+ Полная стоимость за 1 шт: {formatCurrency((sc.customPrice / sc.selectedQuantity) + additionalCost)} +
+
+ ) + } + return null + })()} +
+
+ + updateCardSelection(sc.card, 'deliveryDate', e.target.value)} + className="bg-white/5 border-white/20 text-white mt-1" + /> +
-
- Рынок: - {markets.find(m => m.value === sc.selectedMarket)?.label || 'Не выбран'} -
-
- Место: - {sc.selectedPlace || 'Не указано'} -
-
- Продавец: - {sc.sellerName || 'Не указан'} -
-
- Телефон: - {sc.sellerPhone || 'Не указан'} -
-
- Дата поставки: - {sc.deliveryDate || 'Не выбрана'} + + {/* Услуги */} +
+
+ + +
+ + {sc.selectedFulfillmentOrg && ( +
+ +
+ {organizationServices[sc.selectedFulfillmentOrg] ? ( + organizationServices[sc.selectedFulfillmentOrg].length > 0 ? ( + organizationServices[sc.selectedFulfillmentOrg].map((service) => { + const isSelected = sc.selectedFulfillmentServices.includes(service.id) + return ( + + ) + }) + ) : ( +
+ У данной организации нет услуг +
+ ) + ) : ( +
+ Загрузка услуг... +
+ )} +
+
+ )} + +
+ + +
+ + {sc.selectedConsumableOrg && ( +
+ +
+ {organizationSupplies[sc.selectedConsumableOrg] ? ( + organizationSupplies[sc.selectedConsumableOrg].length > 0 ? ( + organizationSupplies[sc.selectedConsumableOrg].map((supply) => { + const isSelected = sc.selectedConsumableServices.includes(supply.id) + return ( + + ) + }) + ) : ( +
+ У данной организации нет расходников +
+ ) + ) : ( +
+ Загрузка расходников... +
+ )} +
+
+ )}
- {sc.selectedServices.length > 0 && ( -
-

Услуги:

-
- {sc.selectedServices.map(serviceId => { - const service = fulfillmentServices.find(s => s.id === serviceId) - return service ? ( - - {service.name} ({formatCurrency(service.price)}) - - ) : null - })} -
-
- )} -
- {formatCurrency(totalPrice)} + + {formatCurrency(sc.customPrice)} + + {sc.selectedQuantity > 0 && sc.customPrice > 0 && ( +

+ ~{formatCurrency(sc.customPrice / sc.selectedQuantity)} за шт. +

+ )}
@@ -815,12 +1040,9 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { const selectedQuantity = getSelectedQuantity(card) const isSelected = selectedQuantity > 0 const selectedCard = selectedCards.find(sc => sc.card.nmID === card.nmID) - const mainSize = card.sizes[0] - const maxQuantity = mainSize?.quantity || 0 - const price = mainSize?.discountedPrice || mainSize?.price || 0 return ( - +
{/* Изображение и основная информация */}
@@ -833,13 +1055,23 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { onClick={() => handleCardClick(card)} /> - {/* Количество в наличии */} + {/* Индикатор товара WB */}
- 10 ? 'bg-green-500/80' : maxQuantity > 0 ? 'bg-yellow-500/80' : 'bg-red-500/80'} text-white border-0 backdrop-blur text-xs`}> - {maxQuantity} + + ◉ WB
+ {/* Индикатор выбранного товара */} + {isSelected && ( +
+ + + В корзине + +
+ )} + {/* Overlay с кнопкой */}
@@ -857,80 +1090,78 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
{/* Заголовок и бренд */}
-
- +
+ {card.brand} + №{card.nmID}

handleCardClick(card)}> {card.title}

-

{card.vendorCode}

+

Артикул: {card.vendorCode}

- {/* Цена */} -
-
- {formatCurrency(price)} + {/* Информация о товаре */} +
+
+ Добавьте в поставку для настройки
{/* Управление количеством */} -
-
+
+
+ Добавить в поставку: +
+
- { - const value = e.target.value.replace(/[^0-9]/g, '') - const numValue = Math.max(0, Math.min(maxQuantity, parseInt(value) || 0)) - updateCardSelection(card, 'selectedQuantity', numValue) - }} - onFocus={(e) => e.target.select()} - className="h-7 w-12 text-center bg-white/10 border border-white/20 text-white text-sm rounded focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> +
+ { + const value = e.target.value.replace(/[^0-9]/g, '') + const numValue = Math.max(0, parseInt(value) || 0) + updateCardSelection(card, 'selectedQuantity', numValue) + }} + onFocus={(e) => e.target.select()} + className="h-8 w-full text-center bg-white/10 border border-white/20 text-white text-sm rounded focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + placeholder="0" + /> +
- - {selectedQuantity > 0 && ( - - - {selectedQuantity} - - )}
- {/* Сумма для выбранного товара */} + {/* Указание что настройки в корзине */} {selectedQuantity > 0 && ( -
-
- {formatCurrency(price * selectedQuantity)} +
+
+ В корзине: {selectedQuantity} шт +

Настройте цену и услуги в корзине

)}
- -
) @@ -992,14 +1223,14 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { {/* Модальное окно с детальной информацией о товаре */} !open && closeDetailsModal()}> - + Детальная информация о товаре {selectedCardForDetails && ( -
+
{/* Изображения */} -
+
-
+
{currentImageIndex + 1} из {selectedCardForDetails.mediaFiles?.length || 0}
@@ -1031,107 +1262,114 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
{/* Миниатюры изображений */} - {selectedCardForDetails.mediaFiles?.length > 1 && ( -
- {selectedCardForDetails.mediaFiles?.map((image, index) => ( - {`${selectedCardForDetails.title} setCurrentImageIndex(index)} - /> - ))} + {selectedCardForDetails.mediaFiles?.length > 1 && ( +
+
+ {selectedCardForDetails.mediaFiles?.map((image, index) => ( + {`${selectedCardForDetails.title} setCurrentImageIndex(index)} + /> + ))} +
)}
{/* Информация о товаре */} -
+
-

{selectedCardForDetails.title}

-

Артикул: {selectedCardForDetails.vendorCode}

+

{selectedCardForDetails.title}

+

Артикул: {selectedCardForDetails.vendorCode}

-
-
- Бренд: - {selectedCardForDetails.brand} +
+
+ Бренд: + {selectedCardForDetails.brand}
-
- Категория: - {selectedCardForDetails.object} +
+ Категория: + {selectedCardForDetails.object}
-
- Родительская категория: - {selectedCardForDetails.parent} +
+ Родительская: + {selectedCardForDetails.parent}
-
- Страна производства: - {selectedCardForDetails.countryProduction} +
+ Страна: + {selectedCardForDetails.countryProduction}
{selectedCardForDetails.description && (
-

Описание

-

{selectedCardForDetails.description}

+

Описание

+
+

+ {selectedCardForDetails.description} +

+
)} {/* Размеры и цены */}
-

Доступные варианты

+

Доступные варианты

{selectedCardForDetails.sizes.map((size) => ( -
+
{size.wbSize} - 10 ? 'bg-green-500/20 text-green-300' : size.quantity > 0 ? 'bg-yellow-500/20 text-yellow-300' : 'bg-red-500/20 text-red-300'}`}> + 10 ? 'bg-green-500/30 text-green-200' : size.quantity > 0 ? 'bg-yellow-500/30 text-yellow-200' : 'bg-red-500/30 text-red-200'} font-medium`}> {size.quantity} шт.
Размер: {size.techSize}
-
{formatCurrency(size.discountedPrice || size.price)}
+
{formatCurrency(size.discountedPrice || size.price)}
{size.discountedPrice && size.discountedPrice < size.price && (
{formatCurrency(size.price)}
)}
- ))} -
-
+ ))} +
+
- {/* Кнопки действий в модальном окне */} -
- - -
-
-
- )} - -
+ {/* Кнопки действий в модальном окне */} +
+ + +
+
+
+ )} + +
diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index 94c6f9c..32251d1 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -612,6 +612,39 @@ export const GET_MY_WILDBERRIES_SUPPLIES = gql` } ` +// Запросы для получения услуг и расходников от конкретных организаций-контрагентов +export const GET_COUNTERPARTY_SERVICES = gql` + query GetCounterpartyServices($organizationId: ID!) { + counterpartyServices(organizationId: $organizationId) { + id + name + description + price + imageUrl + createdAt + updatedAt + } + } +` + +export const GET_COUNTERPARTY_SUPPLIES = gql` + query GetCounterpartySupplies($organizationId: ID!) { + counterpartySupplies(organizationId: $organizationId) { + id + name + description + price + quantity + unit + category + status + imageUrl + createdAt + updatedAt + } + } +` + // Админ запросы export const ADMIN_ME = gql` query AdminMe { diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index c82fa99..f4e8a07 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -810,6 +810,104 @@ export const resolvers = { }); }, + // Публичные услуги контрагента (для фулфилмента) + counterpartyServices: async ( + _: unknown, + args: { organizationId: string }, + context: Context + ) => { + if (!context.user) { + throw new GraphQLError("Требуется авторизация", { + extensions: { code: "UNAUTHENTICATED" }, + }); + } + + const currentUser = await prisma.user.findUnique({ + where: { id: context.user.id }, + include: { organization: true }, + }); + + if (!currentUser?.organization) { + throw new GraphQLError("У пользователя нет организации"); + } + + // Проверяем, что запрашиваемая организация является контрагентом + const counterparty = await prisma.counterparty.findFirst({ + where: { + organizationId: currentUser.organization.id, + counterpartyId: args.organizationId, + }, + }); + + if (!counterparty) { + throw new GraphQLError("Организация не является вашим контрагентом"); + } + + // Проверяем, что это фулфилмент центр + const targetOrganization = await prisma.organization.findUnique({ + where: { id: args.organizationId }, + }); + + if (!targetOrganization || targetOrganization.type !== "FULFILLMENT") { + throw new GraphQLError("Услуги доступны только у фулфилмент центров"); + } + + return await prisma.service.findMany({ + where: { organizationId: args.organizationId }, + include: { organization: true }, + orderBy: { createdAt: "desc" }, + }); + }, + + // Публичные расходники контрагента (для оптовиков) + counterpartySupplies: async ( + _: unknown, + args: { organizationId: string }, + context: Context + ) => { + if (!context.user) { + throw new GraphQLError("Требуется авторизация", { + extensions: { code: "UNAUTHENTICATED" }, + }); + } + + const currentUser = await prisma.user.findUnique({ + where: { id: context.user.id }, + include: { organization: true }, + }); + + if (!currentUser?.organization) { + throw new GraphQLError("У пользователя нет организации"); + } + + // Проверяем, что запрашиваемая организация является контрагентом + const counterparty = await prisma.counterparty.findFirst({ + where: { + organizationId: currentUser.organization.id, + counterpartyId: args.organizationId, + }, + }); + + if (!counterparty) { + throw new GraphQLError("Организация не является вашим контрагентом"); + } + + // Проверяем, что это фулфилмент центр (у них есть расходники) + const targetOrganization = await prisma.organization.findUnique({ + where: { id: args.organizationId }, + }); + + if (!targetOrganization || targetOrganization.type !== "FULFILLMENT") { + throw new GraphQLError("Расходники доступны только у фулфилмент центров"); + } + + return await prisma.supply.findMany({ + where: { organizationId: args.organizationId }, + include: { organization: true }, + orderBy: { createdAt: "desc" }, + }); + }, + // Корзина пользователя myCart: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { @@ -4312,6 +4410,32 @@ export const resolvers = { where: { organizationId: parent.id }, }); }, + services: async (parent: { id: string; services?: unknown[] }) => { + // Если услуги уже загружены через include, возвращаем их + if (parent.services) { + return parent.services; + } + + // Иначе загружаем отдельно + return await prisma.service.findMany({ + where: { organizationId: parent.id }, + include: { organization: true }, + orderBy: { createdAt: "desc" }, + }); + }, + supplies: async (parent: { id: string; supplies?: unknown[] }) => { + // Если расходники уже загружены через include, возвращаем их + if (parent.supplies) { + return parent.supplies; + } + + // Иначе загружаем отдельно + return await prisma.supply.findMany({ + where: { organizationId: parent.id }, + include: { organization: true }, + orderBy: { createdAt: "desc" }, + }); + }, }, Cart: { diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts index 0829742..5dd5ca7 100644 --- a/src/graphql/typedefs.ts +++ b/src/graphql/typedefs.ts @@ -66,6 +66,12 @@ export const typeDefs = gql` month: Int! ): [EmployeeSchedule!]! + # Публичные услуги контрагента (для фулфилмента) + counterpartyServices(organizationId: ID!): [Service!]! + + # Публичные расходники контрагента (для оптовиков) + counterpartySupplies(organizationId: ID!): [Supply!]! + # Админ запросы adminMe: Admin allUsers(search: String, limit: Int, offset: Int): UsersResponse! @@ -235,6 +241,8 @@ export const typeDefs = gql` emails: JSON users: [User!]! apiKeys: [ApiKey!]! + services: [Service!]! + supplies: [Supply!]! isCounterparty: Boolean isCurrentUser: Boolean hasOutgoingRequest: Boolean