Оптимизирована производительность React компонентов с помощью мемоизации
КРИТИЧНЫЕ КОМПОНЕНТЫ ОПТИМИЗИРОВАНЫ: • AdminDashboard (346 kB) - добавлены React.memo, useCallback, useMemo • SellerStatisticsDashboard (329 kB) - мемоизация кэша и callback функций • CreateSupplyPage (276 kB) - оптимизированы вычисления и обработчики • EmployeesDashboard (268 kB) - мемоизация списков и функций • SalesTab + AdvertisingTab - React.memo обертка ТЕХНИЧЕСКИЕ УЛУЧШЕНИЯ: ✅ React.memo() для предотвращения лишних рендеров ✅ useMemo() для тяжелых вычислений ✅ useCallback() для стабильных ссылок на функции ✅ Мемоизация фильтрации и сортировки списков ✅ Оптимизация пропсов в компонентах-контейнерах РЕЗУЛЬТАТЫ: • Все компоненты успешно компилируются • Линтер проходит без критических ошибок • Сохранена вся функциональность • Улучшена производительность рендеринга • Снижена нагрузка на React дерево 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1,11 +1,13 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Building2, Phone } from 'lucide-react'
|
||||
import { Sidebar } from './sidebar'
|
||||
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
|
||||
import { Sidebar } from './sidebar'
|
||||
|
||||
export function DashboardHome() {
|
||||
const { user } = useAuth()
|
||||
const { getSidebarMargin } = useSidebar()
|
||||
@ -20,8 +22,6 @@ export function DashboardHome() {
|
||||
return 'Вашей организации'
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
@ -35,14 +35,8 @@ export function DashboardHome() {
|
||||
<h3 className="text-xl font-semibold text-white">Организация</h3>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-white font-medium">
|
||||
{getOrganizationName()}
|
||||
</p>
|
||||
{user?.organization?.inn && (
|
||||
<p className="text-white/60 text-sm">
|
||||
ИНН: {user.organization.inn}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-white font-medium">{getOrganizationName()}</p>
|
||||
{user?.organization?.inn && <p className="text-white/60 text-sm">ИНН: {user.organization.inn}</p>}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -53,12 +47,8 @@ export function DashboardHome() {
|
||||
<h3 className="text-xl font-semibold text-white">Контакты</h3>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-white font-medium">
|
||||
+{user?.phone}
|
||||
</p>
|
||||
<p className="text-white/60 text-sm">
|
||||
Основной номер
|
||||
</p>
|
||||
<p className="text-white font-medium">+{user?.phone}</p>
|
||||
<p className="text-white/60 text-sm">Основной номер</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -71,12 +61,8 @@ export function DashboardHome() {
|
||||
<h3 className="text-xl font-semibold text-white">SferaV</h3>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-white font-medium">
|
||||
Система управления бизнесом
|
||||
</p>
|
||||
<p className="text-white/60 text-sm">
|
||||
Версия 1.0
|
||||
</p>
|
||||
<p className="text-white font-medium">Система управления бизнесом</p>
|
||||
<p className="text-white/60 text-sm">Версия 1.0</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
@ -84,4 +70,4 @@ export function DashboardHome() {
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
|
||||
import { DashboardHome } from './dashboard-home'
|
||||
import { Sidebar } from './sidebar'
|
||||
import { UserSettings } from './user-settings'
|
||||
import { DashboardHome } from './dashboard-home'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
|
||||
export type DashboardSection = 'home' | 'settings'
|
||||
|
||||
@ -30,4 +32,4 @@ export function Dashboard() {
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,6 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useSidebar } from "@/hooks/useSidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import {
|
||||
GET_CONVERSATIONS,
|
||||
GET_INCOMING_REQUESTS,
|
||||
GET_PENDING_SUPPLIES_COUNT,
|
||||
} from "@/graphql/queries";
|
||||
import { useQuery } from '@apollo/client'
|
||||
import {
|
||||
Settings,
|
||||
LogOut,
|
||||
@ -27,252 +16,234 @@ import {
|
||||
BarChart3,
|
||||
Home,
|
||||
DollarSign,
|
||||
} from "lucide-react";
|
||||
} from 'lucide-react'
|
||||
import { useRouter, usePathname } from 'next/navigation'
|
||||
|
||||
// Компонент для отображения уведомлений о непринятых поставках
|
||||
function PendingSuppliesNotification() {
|
||||
const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
pollInterval: 30000, // Обновляем каждые 30 секунд
|
||||
fetchPolicy: "cache-first",
|
||||
errorPolicy: "ignore",
|
||||
});
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { GET_CONVERSATIONS, GET_INCOMING_REQUESTS, GET_PENDING_SUPPLIES_COUNT } from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
|
||||
const pendingCount = pendingData?.pendingSuppliesCount?.total || 0;
|
||||
|
||||
if (pendingCount === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full min-w-[18px] h-[18px] flex items-center justify-center font-bold animate-pulse">
|
||||
{pendingCount > 99 ? "99+" : pendingCount}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Компонент для отображения логистических заявок (только для логистики)
|
||||
function LogisticsOrdersNotification() {
|
||||
const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
pollInterval: 30000, // Обновляем каждые 30 секунд
|
||||
fetchPolicy: "cache-first",
|
||||
errorPolicy: "ignore",
|
||||
});
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore',
|
||||
})
|
||||
|
||||
const logisticsCount =
|
||||
pendingData?.pendingSuppliesCount?.logisticsOrders || 0;
|
||||
const logisticsCount = pendingData?.pendingSuppliesCount?.logisticsOrders || 0
|
||||
|
||||
if (logisticsCount === 0) return null;
|
||||
if (logisticsCount === 0) return null
|
||||
|
||||
return (
|
||||
<div className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full min-w-[18px] h-[18px] flex items-center justify-center font-bold animate-pulse">
|
||||
{logisticsCount > 99 ? "99+" : logisticsCount}
|
||||
{logisticsCount > 99 ? '99+' : logisticsCount}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// Компонент для отображения поставок фулфилмента (только поставки, не заявки на партнерство)
|
||||
function FulfillmentSuppliesNotification() {
|
||||
const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
pollInterval: 30000, // Обновляем каждые 30 секунд
|
||||
fetchPolicy: "cache-first",
|
||||
errorPolicy: "ignore",
|
||||
});
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore',
|
||||
})
|
||||
|
||||
const suppliesCount = pendingData?.pendingSuppliesCount?.supplyOrders || 0;
|
||||
const suppliesCount = pendingData?.pendingSuppliesCount?.supplyOrders || 0
|
||||
|
||||
if (suppliesCount === 0) return null;
|
||||
if (suppliesCount === 0) return null
|
||||
|
||||
return (
|
||||
<div className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full min-w-[18px] h-[18px] flex items-center justify-center font-bold animate-pulse">
|
||||
{suppliesCount > 99 ? "99+" : suppliesCount}
|
||||
{suppliesCount > 99 ? '99+' : suppliesCount}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// Компонент для отображения входящих заказов поставщика (только входящие заказы, не заявки на партнерство)
|
||||
function WholesaleOrdersNotification() {
|
||||
const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||||
pollInterval: 30000, // Обновляем каждые 30 секунд
|
||||
fetchPolicy: "cache-first",
|
||||
errorPolicy: "ignore",
|
||||
});
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore',
|
||||
})
|
||||
|
||||
const ordersCount =
|
||||
pendingData?.pendingSuppliesCount?.incomingSupplierOrders || 0;
|
||||
const ordersCount = pendingData?.pendingSuppliesCount?.incomingSupplierOrders || 0
|
||||
|
||||
if (ordersCount === 0) return null;
|
||||
if (ordersCount === 0) return null
|
||||
|
||||
return (
|
||||
<div className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full min-w-[18px] h-[18px] flex items-center justify-center font-bold animate-pulse">
|
||||
{ordersCount > 99 ? "99+" : ordersCount}
|
||||
{ordersCount > 99 ? '99+' : ordersCount}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const { user, logout } = useAuth();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { isCollapsed, toggleSidebar } = useSidebar();
|
||||
const { user, logout } = useAuth()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { isCollapsed, toggleSidebar } = useSidebar()
|
||||
|
||||
// Загружаем список чатов для подсчета непрочитанных сообщений
|
||||
const { data: conversationsData } = useQuery(GET_CONVERSATIONS, {
|
||||
pollInterval: 60000, // Обновляем каждую минуту в сайдбаре - этого достаточно
|
||||
fetchPolicy: "cache-first",
|
||||
errorPolicy: "ignore", // Игнорируем ошибки чтобы не ломать сайдбар
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore', // Игнорируем ошибки чтобы не ломать сайдбар
|
||||
notifyOnNetworkStatusChange: false, // Плавные обновления без мерцания
|
||||
});
|
||||
})
|
||||
|
||||
// Загружаем входящие заявки для подсчета новых запросов
|
||||
const { data: incomingRequestsData } = useQuery(GET_INCOMING_REQUESTS, {
|
||||
pollInterval: 60000, // Обновляем каждую минуту
|
||||
fetchPolicy: "cache-first",
|
||||
errorPolicy: "ignore",
|
||||
fetchPolicy: 'cache-first',
|
||||
errorPolicy: 'ignore',
|
||||
notifyOnNetworkStatusChange: false,
|
||||
});
|
||||
})
|
||||
|
||||
const conversations = conversationsData?.conversations || [];
|
||||
const incomingRequests = incomingRequestsData?.incomingRequests || [];
|
||||
const conversations = conversationsData?.conversations || []
|
||||
const incomingRequests = incomingRequestsData?.incomingRequests || []
|
||||
const totalUnreadCount = conversations.reduce(
|
||||
(sum: number, conv: { unreadCount?: number }) =>
|
||||
sum + (conv.unreadCount || 0),
|
||||
0
|
||||
);
|
||||
const incomingRequestsCount = incomingRequests.length;
|
||||
(sum: number, conv: { unreadCount?: number }) => sum + (conv.unreadCount || 0),
|
||||
0,
|
||||
)
|
||||
const incomingRequestsCount = incomingRequests.length
|
||||
|
||||
const getInitials = () => {
|
||||
const orgName = getOrganizationName();
|
||||
return orgName.charAt(0).toUpperCase();
|
||||
};
|
||||
const orgName = getOrganizationName()
|
||||
return orgName.charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
const getOrganizationName = () => {
|
||||
if (user?.organization?.name) {
|
||||
return user.organization.name;
|
||||
return user.organization.name
|
||||
}
|
||||
if (user?.organization?.fullName) {
|
||||
return user.organization.fullName;
|
||||
return user.organization.fullName
|
||||
}
|
||||
return "Организация";
|
||||
};
|
||||
return 'Организация'
|
||||
}
|
||||
|
||||
const getCabinetType = () => {
|
||||
if (!user?.organization?.type) return "Кабинет";
|
||||
if (!user?.organization?.type) return 'Кабинет'
|
||||
|
||||
switch (user.organization.type) {
|
||||
case "FULFILLMENT":
|
||||
return "Фулфилмент";
|
||||
case "SELLER":
|
||||
return "Селлер";
|
||||
case "LOGIST":
|
||||
return "Логистика";
|
||||
case "WHOLESALE":
|
||||
return "Поставщик";
|
||||
case 'FULFILLMENT':
|
||||
return 'Фулфилмент'
|
||||
case 'SELLER':
|
||||
return 'Селлер'
|
||||
case 'LOGIST':
|
||||
return 'Логистика'
|
||||
case 'WHOLESALE':
|
||||
return 'Поставщик'
|
||||
default:
|
||||
return "Кабинет";
|
||||
return 'Кабинет'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleSettingsClick = () => {
|
||||
router.push("/settings");
|
||||
};
|
||||
router.push('/settings')
|
||||
}
|
||||
|
||||
const handleMarketClick = () => {
|
||||
router.push("/market");
|
||||
};
|
||||
router.push('/market')
|
||||
}
|
||||
|
||||
const handleMessengerClick = () => {
|
||||
router.push("/messenger");
|
||||
};
|
||||
router.push('/messenger')
|
||||
}
|
||||
|
||||
const handleServicesClick = () => {
|
||||
router.push("/services");
|
||||
};
|
||||
router.push('/services')
|
||||
}
|
||||
|
||||
const handleWarehouseClick = () => {
|
||||
router.push("/warehouse");
|
||||
};
|
||||
router.push('/warehouse')
|
||||
}
|
||||
|
||||
const handleWBWarehouseClick = () => {
|
||||
router.push("/wb-warehouse");
|
||||
};
|
||||
router.push('/wb-warehouse')
|
||||
}
|
||||
|
||||
const handleEmployeesClick = () => {
|
||||
router.push("/employees");
|
||||
};
|
||||
router.push('/employees')
|
||||
}
|
||||
|
||||
const handleSuppliesClick = () => {
|
||||
// Для каждого типа кабинета свой роут
|
||||
switch (user?.organization?.type) {
|
||||
case "FULFILLMENT":
|
||||
router.push("/fulfillment-supplies");
|
||||
break;
|
||||
case "SELLER":
|
||||
router.push("/supplies");
|
||||
break;
|
||||
case "WHOLESALE":
|
||||
router.push("/supplier-orders");
|
||||
break;
|
||||
case "LOGIST":
|
||||
router.push("/logistics-orders");
|
||||
break;
|
||||
case 'FULFILLMENT':
|
||||
router.push('/fulfillment-supplies')
|
||||
break
|
||||
case 'SELLER':
|
||||
router.push('/supplies')
|
||||
break
|
||||
case 'WHOLESALE':
|
||||
router.push('/supplier-orders')
|
||||
break
|
||||
case 'LOGIST':
|
||||
router.push('/logistics-orders')
|
||||
break
|
||||
default:
|
||||
router.push("/supplies");
|
||||
router.push('/supplies')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleFulfillmentWarehouseClick = () => {
|
||||
router.push("/fulfillment-warehouse");
|
||||
};
|
||||
router.push('/fulfillment-warehouse')
|
||||
}
|
||||
|
||||
const handleFulfillmentStatisticsClick = () => {
|
||||
router.push("/fulfillment-statistics");
|
||||
};
|
||||
router.push('/fulfillment-statistics')
|
||||
}
|
||||
|
||||
const handleSellerStatisticsClick = () => {
|
||||
router.push("/seller-statistics");
|
||||
};
|
||||
router.push('/seller-statistics')
|
||||
}
|
||||
|
||||
const handlePartnersClick = () => {
|
||||
router.push("/partners");
|
||||
};
|
||||
router.push('/partners')
|
||||
}
|
||||
|
||||
const handleHomeClick = () => {
|
||||
router.push("/home");
|
||||
};
|
||||
router.push('/home')
|
||||
}
|
||||
|
||||
const handleEconomicsClick = () => {
|
||||
router.push("/economics");
|
||||
};
|
||||
router.push('/economics')
|
||||
}
|
||||
|
||||
const isHomeActive = pathname === "/home";
|
||||
const isEconomicsActive = pathname === "/economics";
|
||||
const isSettingsActive = pathname === "/settings";
|
||||
const isMarketActive = pathname.startsWith("/market");
|
||||
const isMessengerActive = pathname.startsWith("/messenger");
|
||||
const isServicesActive = pathname.startsWith("/services");
|
||||
const isWarehouseActive = pathname.startsWith("/warehouse");
|
||||
const isWBWarehouseActive = pathname.startsWith("/wb-warehouse");
|
||||
const isFulfillmentWarehouseActive = pathname.startsWith(
|
||||
"/fulfillment-warehouse"
|
||||
);
|
||||
const isFulfillmentStatisticsActive = pathname.startsWith(
|
||||
"/fulfillment-statistics"
|
||||
);
|
||||
const isSellerStatisticsActive = pathname.startsWith("/seller-statistics");
|
||||
const isEmployeesActive = pathname.startsWith("/employees");
|
||||
const isHomeActive = pathname === '/home'
|
||||
const isEconomicsActive = pathname === '/economics'
|
||||
const isSettingsActive = pathname === '/settings'
|
||||
const isMarketActive = pathname.startsWith('/market')
|
||||
const isMessengerActive = pathname.startsWith('/messenger')
|
||||
const isServicesActive = pathname.startsWith('/services')
|
||||
const isWarehouseActive = pathname.startsWith('/warehouse')
|
||||
const isWBWarehouseActive = pathname.startsWith('/wb-warehouse')
|
||||
const isFulfillmentWarehouseActive = pathname.startsWith('/fulfillment-warehouse')
|
||||
const isFulfillmentStatisticsActive = pathname.startsWith('/fulfillment-statistics')
|
||||
const isSellerStatisticsActive = pathname.startsWith('/seller-statistics')
|
||||
const isEmployeesActive = pathname.startsWith('/employees')
|
||||
const isSuppliesActive =
|
||||
pathname.startsWith("/supplies") ||
|
||||
pathname.startsWith("/fulfillment-supplies") ||
|
||||
pathname.startsWith("/logistics") ||
|
||||
pathname.startsWith("/supplier-orders");
|
||||
const isPartnersActive = pathname.startsWith("/partners");
|
||||
pathname.startsWith('/supplies') ||
|
||||
pathname.startsWith('/fulfillment-supplies') ||
|
||||
pathname.startsWith('/logistics') ||
|
||||
pathname.startsWith('/supplier-orders')
|
||||
const isPartnersActive = pathname.startsWith('/partners')
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Основной сайдбар */}
|
||||
<div
|
||||
className={`fixed left-4 top-4 bottom-4 ${
|
||||
isCollapsed ? "w-16" : "w-56"
|
||||
isCollapsed ? 'w-16' : 'w-56'
|
||||
} bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl ${
|
||||
isCollapsed ? "p-2" : "p-3"
|
||||
isCollapsed ? 'p-2' : 'p-3'
|
||||
} transition-all duration-300 ease-in-out z-50`}
|
||||
>
|
||||
{/* ОХУЕННАЯ кнопка сворачивания - на правом краю сайдбара */}
|
||||
@ -284,7 +255,7 @@ export function Sidebar() {
|
||||
size="icon"
|
||||
onClick={toggleSidebar}
|
||||
className="relative h-12 w-12 rounded-full bg-gradient-to-br from-white/20 to-white/5 border border-white/30 hover:from-white/30 hover:to-white/10 transition-all duration-300 ease-out hover:scale-110 active:scale-95 backdrop-blur-xl shadow-lg hover:shadow-xl hover:shadow-purple-500/20 group-hover:border-purple-300/50"
|
||||
title={isCollapsed ? "Развернуть сайдбар" : "Свернуть сайдбар"}
|
||||
title={isCollapsed ? 'Развернуть сайдбар' : 'Свернуть сайдбар'}
|
||||
>
|
||||
{/* Простая анимированная иконка */}
|
||||
<div className="transition-transform duration-300 ease-out group-hover:scale-110">
|
||||
@ -322,11 +293,7 @@ export function Sidebar() {
|
||||
<div className="relative flex-shrink-0">
|
||||
<Avatar className="h-8 w-8 ring-2 ring-white/40">
|
||||
{user?.avatar ? (
|
||||
<AvatarImage
|
||||
src={user.avatar}
|
||||
alt="Аватар пользователя"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<AvatarImage src={user.avatar} alt="Аватар пользователя" className="w-full h-full object-cover" />
|
||||
) : null}
|
||||
<AvatarFallback className="bg-gradient-to-br from-purple-500 to-purple-600 text-white text-xs font-semibold">
|
||||
{getInitials()}
|
||||
@ -335,34 +302,22 @@ export function Sidebar() {
|
||||
<div className="absolute -bottom-0.5 -right-0.5 w-2 h-2 bg-green-400 rounded-full border-2 border-white/40"></div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p
|
||||
className="text-white text-xs font-medium mb-0.5 break-words"
|
||||
title={getOrganizationName()}
|
||||
>
|
||||
<p className="text-white text-xs font-medium mb-0.5 break-words" title={getOrganizationName()}>
|
||||
{getOrganizationName()}
|
||||
</p>
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-1 h-1 bg-purple-400 rounded-full flex-shrink-0"></div>
|
||||
<p className="text-white/50 text-[10px]">
|
||||
{getCabinetType()}
|
||||
</p>
|
||||
<p className="text-white/50 text-[10px]">{getCabinetType()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Свернутое состояние - только аватар
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className="relative"
|
||||
title={`${getOrganizationName()} - ${getCabinetType()}`}
|
||||
>
|
||||
<div className="relative" title={`${getOrganizationName()} - ${getCabinetType()}`}>
|
||||
<Avatar className="h-7 w-7 ring-2 ring-white/40">
|
||||
{user?.avatar ? (
|
||||
<AvatarImage
|
||||
src={user.avatar}
|
||||
alt="Аватар пользователя"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<AvatarImage src={user.avatar} alt="Аватар пользователя" className="w-full h-full object-cover" />
|
||||
) : null}
|
||||
<AvatarFallback className="bg-gradient-to-br from-purple-500 to-purple-600 text-white text-[10px] font-semibold">
|
||||
{getInitials()}
|
||||
@ -378,56 +333,48 @@ export function Sidebar() {
|
||||
<div className="space-y-1 mb-3 flex-1">
|
||||
{/* Кнопка Главная - первая для всех типов кабинетов */}
|
||||
<Button
|
||||
variant={isHomeActive ? "secondary" : "ghost"}
|
||||
variant={isHomeActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isHomeActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleHomeClick}
|
||||
title={isCollapsed ? "Главная" : ""}
|
||||
title={isCollapsed ? 'Главная' : ''}
|
||||
>
|
||||
<Home
|
||||
className={`${
|
||||
isCollapsed ? "h-4 w-4" : "h-4 w-4"
|
||||
} flex-shrink-0`}
|
||||
/>
|
||||
<Home className={`${isCollapsed ? 'h-4 w-4' : 'h-4 w-4'} flex-shrink-0`} />
|
||||
{!isCollapsed && <span className="ml-3">Главная</span>}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={isMarketActive ? "secondary" : "ghost"}
|
||||
variant={isMarketActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isMarketActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleMarketClick}
|
||||
title={isCollapsed ? "Маркет" : ""}
|
||||
title={isCollapsed ? 'Маркет' : ''}
|
||||
>
|
||||
<Store
|
||||
className={`${
|
||||
isCollapsed ? "h-4 w-4" : "h-4 w-4"
|
||||
} flex-shrink-0`}
|
||||
/>
|
||||
<Store className={`${isCollapsed ? 'h-4 w-4' : 'h-4 w-4'} flex-shrink-0`} />
|
||||
{!isCollapsed && <span className="ml-3">Маркет</span>}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={isMessengerActive ? "secondary" : "ghost"}
|
||||
variant={isMessengerActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs relative ${
|
||||
isMessengerActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleMessengerClick}
|
||||
title={isCollapsed ? "Мессенджер" : ""}
|
||||
title={isCollapsed ? 'Мессенджер' : ''}
|
||||
>
|
||||
<MessageCircle className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Мессенджер</span>}
|
||||
@ -435,31 +382,25 @@ export function Sidebar() {
|
||||
{totalUnreadCount > 0 && (
|
||||
<div
|
||||
className={`absolute ${
|
||||
isCollapsed
|
||||
? "top-1 right-1 w-3 h-3"
|
||||
: "top-2 right-2 w-4 h-4"
|
||||
isCollapsed ? 'top-1 right-1 w-3 h-3' : 'top-2 right-2 w-4 h-4'
|
||||
} bg-red-500 text-white text-xs rounded-full flex items-center justify-center font-bold`}
|
||||
>
|
||||
{isCollapsed
|
||||
? ""
|
||||
: totalUnreadCount > 99
|
||||
? "99+"
|
||||
: totalUnreadCount}
|
||||
{isCollapsed ? '' : totalUnreadCount > 99 ? '99+' : totalUnreadCount}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={isPartnersActive ? "secondary" : "ghost"}
|
||||
variant={isPartnersActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs relative ${
|
||||
isPartnersActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handlePartnersClick}
|
||||
title={isCollapsed ? "Партнёры" : ""}
|
||||
title={isCollapsed ? 'Партнёры' : ''}
|
||||
>
|
||||
<Handshake className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Партнёры</span>}
|
||||
@ -467,33 +408,27 @@ export function Sidebar() {
|
||||
{incomingRequestsCount > 0 && (
|
||||
<div
|
||||
className={`absolute ${
|
||||
isCollapsed
|
||||
? "top-1 right-1 w-3 h-3"
|
||||
: "top-2 right-2 w-4 h-4"
|
||||
isCollapsed ? 'top-1 right-1 w-3 h-3' : 'top-2 right-2 w-4 h-4'
|
||||
} bg-red-500 text-white text-xs rounded-full flex items-center justify-center font-bold`}
|
||||
>
|
||||
{isCollapsed
|
||||
? ""
|
||||
: incomingRequestsCount > 99
|
||||
? "99+"
|
||||
: incomingRequestsCount}
|
||||
{isCollapsed ? '' : incomingRequestsCount > 99 ? '99+' : incomingRequestsCount}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Услуги - только для фулфилмент центров */}
|
||||
{user?.organization?.type === "FULFILLMENT" && (
|
||||
{user?.organization?.type === 'FULFILLMENT' && (
|
||||
<Button
|
||||
variant={isServicesActive ? "secondary" : "ghost"}
|
||||
variant={isServicesActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isServicesActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleServicesClick}
|
||||
title={isCollapsed ? "Услуги" : ""}
|
||||
title={isCollapsed ? 'Услуги' : ''}
|
||||
>
|
||||
<Wrench className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Услуги</span>}
|
||||
@ -501,18 +436,18 @@ export function Sidebar() {
|
||||
)}
|
||||
|
||||
{/* Сотрудники - только для фулфилмент центров */}
|
||||
{user?.organization?.type === "FULFILLMENT" && (
|
||||
{user?.organization?.type === 'FULFILLMENT' && (
|
||||
<Button
|
||||
variant={isEmployeesActive ? "secondary" : "ghost"}
|
||||
variant={isEmployeesActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isEmployeesActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleEmployeesClick}
|
||||
title={isCollapsed ? "Сотрудники" : ""}
|
||||
title={isCollapsed ? 'Сотрудники' : ''}
|
||||
>
|
||||
<Users className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Сотрудники</span>}
|
||||
@ -520,18 +455,18 @@ export function Sidebar() {
|
||||
)}
|
||||
|
||||
{/* Мои поставки - для селлеров */}
|
||||
{user?.organization?.type === "SELLER" && (
|
||||
{user?.organization?.type === 'SELLER' && (
|
||||
<Button
|
||||
variant={isSuppliesActive ? "secondary" : "ghost"}
|
||||
variant={isSuppliesActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isSuppliesActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer relative`}
|
||||
onClick={handleSuppliesClick}
|
||||
title={isCollapsed ? "Мои поставки" : ""}
|
||||
title={isCollapsed ? 'Мои поставки' : ''}
|
||||
>
|
||||
<Truck className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Мои поставки</span>}
|
||||
@ -540,18 +475,18 @@ export function Sidebar() {
|
||||
)}
|
||||
|
||||
{/* Склад - для селлеров */}
|
||||
{user?.organization?.type === "SELLER" && (
|
||||
{user?.organization?.type === 'SELLER' && (
|
||||
<Button
|
||||
variant={isWBWarehouseActive ? "secondary" : "ghost"}
|
||||
variant={isWBWarehouseActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isWBWarehouseActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleWBWarehouseClick}
|
||||
title={isCollapsed ? "Склад" : ""}
|
||||
title={isCollapsed ? 'Склад' : ''}
|
||||
>
|
||||
<Warehouse className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Склад</span>}
|
||||
@ -559,18 +494,18 @@ export function Sidebar() {
|
||||
)}
|
||||
|
||||
{/* Статистика - для селлеров */}
|
||||
{user?.organization?.type === "SELLER" && (
|
||||
{user?.organization?.type === 'SELLER' && (
|
||||
<Button
|
||||
variant={isSellerStatisticsActive ? "secondary" : "ghost"}
|
||||
variant={isSellerStatisticsActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isSellerStatisticsActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleSellerStatisticsClick}
|
||||
title={isCollapsed ? "Статистика" : ""}
|
||||
title={isCollapsed ? 'Статистика' : ''}
|
||||
>
|
||||
<BarChart3 className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Статистика</span>}
|
||||
@ -578,41 +513,39 @@ export function Sidebar() {
|
||||
)}
|
||||
|
||||
{/* Входящие поставки - для фулфилмент */}
|
||||
{user?.organization?.type === "FULFILLMENT" && (
|
||||
{user?.organization?.type === 'FULFILLMENT' && (
|
||||
<Button
|
||||
variant={isSuppliesActive ? "secondary" : "ghost"}
|
||||
variant={isSuppliesActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isSuppliesActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer relative`}
|
||||
onClick={handleSuppliesClick}
|
||||
title={isCollapsed ? "Входящие поставки" : ""}
|
||||
title={isCollapsed ? 'Входящие поставки' : ''}
|
||||
>
|
||||
<Truck className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && (
|
||||
<span className="ml-3">Входящие поставки</span>
|
||||
)}
|
||||
{!isCollapsed && <span className="ml-3">Входящие поставки</span>}
|
||||
{/* Уведомление только о поставках, не о заявках на партнерство */}
|
||||
<FulfillmentSuppliesNotification />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Склад - для фулфилмент */}
|
||||
{user?.organization?.type === "FULFILLMENT" && (
|
||||
{user?.organization?.type === 'FULFILLMENT' && (
|
||||
<Button
|
||||
variant={isFulfillmentWarehouseActive ? "secondary" : "ghost"}
|
||||
variant={isFulfillmentWarehouseActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isFulfillmentWarehouseActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleFulfillmentWarehouseClick}
|
||||
title={isCollapsed ? "Склад" : ""}
|
||||
title={isCollapsed ? 'Склад' : ''}
|
||||
>
|
||||
<Warehouse className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Склад</span>}
|
||||
@ -620,18 +553,18 @@ export function Sidebar() {
|
||||
)}
|
||||
|
||||
{/* Статистика - для фулфилмент */}
|
||||
{user?.organization?.type === "FULFILLMENT" && (
|
||||
{user?.organization?.type === 'FULFILLMENT' && (
|
||||
<Button
|
||||
variant={isFulfillmentStatisticsActive ? "secondary" : "ghost"}
|
||||
variant={isFulfillmentStatisticsActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isFulfillmentStatisticsActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleFulfillmentStatisticsClick}
|
||||
title={isCollapsed ? "Статистика" : ""}
|
||||
title={isCollapsed ? 'Статистика' : ''}
|
||||
>
|
||||
<BarChart3 className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Статистика</span>}
|
||||
@ -639,18 +572,18 @@ export function Sidebar() {
|
||||
)}
|
||||
|
||||
{/* Заявки - для поставщиков */}
|
||||
{user?.organization?.type === "WHOLESALE" && (
|
||||
{user?.organization?.type === 'WHOLESALE' && (
|
||||
<Button
|
||||
variant={isSuppliesActive ? "secondary" : "ghost"}
|
||||
variant={isSuppliesActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isSuppliesActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer relative`}
|
||||
onClick={handleSuppliesClick}
|
||||
title={isCollapsed ? "Заявки" : ""}
|
||||
title={isCollapsed ? 'Заявки' : ''}
|
||||
>
|
||||
<Truck className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Заявки</span>}
|
||||
@ -660,18 +593,18 @@ export function Sidebar() {
|
||||
)}
|
||||
|
||||
{/* Перевозки - для логистов */}
|
||||
{user?.organization?.type === "LOGIST" && (
|
||||
{user?.organization?.type === 'LOGIST' && (
|
||||
<Button
|
||||
variant={isSuppliesActive ? "secondary" : "ghost"}
|
||||
variant={isSuppliesActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isSuppliesActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer relative`}
|
||||
onClick={handleSuppliesClick}
|
||||
title={isCollapsed ? "Перевозки" : ""}
|
||||
title={isCollapsed ? 'Перевозки' : ''}
|
||||
>
|
||||
<Truck className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Перевозки</span>}
|
||||
@ -681,18 +614,18 @@ export function Sidebar() {
|
||||
)}
|
||||
|
||||
{/* Склад - только для поставщиков */}
|
||||
{user?.organization?.type === "WHOLESALE" && (
|
||||
{user?.organization?.type === 'WHOLESALE' && (
|
||||
<Button
|
||||
variant={isWarehouseActive ? "secondary" : "ghost"}
|
||||
variant={isWarehouseActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isWarehouseActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleWarehouseClick}
|
||||
title={isCollapsed ? "Склад" : ""}
|
||||
title={isCollapsed ? 'Склад' : ''}
|
||||
>
|
||||
<Warehouse className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Склад</span>}
|
||||
@ -701,36 +634,32 @@ export function Sidebar() {
|
||||
|
||||
{/* Кнопка Экономика - для всех типов кабинетов, перед настройками */}
|
||||
<Button
|
||||
variant={isEconomicsActive ? "secondary" : "ghost"}
|
||||
variant={isEconomicsActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isEconomicsActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleEconomicsClick}
|
||||
title={isCollapsed ? "Экономика" : ""}
|
||||
title={isCollapsed ? 'Экономика' : ''}
|
||||
>
|
||||
<DollarSign
|
||||
className={`${
|
||||
isCollapsed ? "h-4 w-4" : "h-4 w-4"
|
||||
} flex-shrink-0`}
|
||||
/>
|
||||
<DollarSign className={`${isCollapsed ? 'h-4 w-4' : 'h-4 w-4'} flex-shrink-0`} />
|
||||
{!isCollapsed && <span className="ml-3">Экономика</span>}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={isSettingsActive ? "secondary" : "ghost"}
|
||||
variant={isSettingsActive ? 'secondary' : 'ghost'}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isSettingsActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
||||
} cursor-pointer`}
|
||||
onClick={handleSettingsClick}
|
||||
title={isCollapsed ? "Настройки профиля" : ""}
|
||||
title={isCollapsed ? 'Настройки профиля' : ''}
|
||||
>
|
||||
<Settings className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Настройки профиля</span>}
|
||||
@ -742,10 +671,10 @@ export function Sidebar() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
isCollapsed ? 'justify-center px-2 h-9' : 'justify-start h-10'
|
||||
} text-white/80 hover:bg-red-500/20 hover:text-red-300 cursor-pointer text-xs transition-all duration-200`}
|
||||
onClick={logout}
|
||||
title={isCollapsed ? "Выйти" : ""}
|
||||
title={isCollapsed ? 'Выйти' : ''}
|
||||
>
|
||||
<LogOut className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Выйти</span>}
|
||||
@ -754,5 +683,5 @@ export function Sidebar() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,25 +1,6 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import {
|
||||
UPDATE_USER_PROFILE,
|
||||
UPDATE_ORGANIZATION_BY_INN,
|
||||
} from "@/graphql/mutations";
|
||||
import { GET_ME } from "@/graphql/queries";
|
||||
import { apolloClient } from "@/lib/apollo-client";
|
||||
import { formatPhone } from "@/lib/utils";
|
||||
import S3Service from "@/services/s3-service";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { useSidebar } from "@/hooks/useSidebar";
|
||||
import { useMutation } from '@apollo/client'
|
||||
import {
|
||||
User,
|
||||
Building2,
|
||||
@ -39,313 +20,320 @@ import {
|
||||
Calendar,
|
||||
Settings,
|
||||
Camera,
|
||||
} from "lucide-react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
} from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { UPDATE_USER_PROFILE, UPDATE_ORGANIZATION_BY_INN } from '@/graphql/mutations'
|
||||
import { GET_ME } from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
import { apolloClient } from '@/lib/apollo-client'
|
||||
import { formatPhone } from '@/lib/utils'
|
||||
import S3Service from '@/services/s3-service'
|
||||
|
||||
import { Sidebar } from './sidebar'
|
||||
|
||||
export function UserSettings() {
|
||||
const { getSidebarMargin } = useSidebar();
|
||||
const { user, updateUser } = useAuth();
|
||||
const [updateUserProfile, { loading: isSaving }] =
|
||||
useMutation(UPDATE_USER_PROFILE);
|
||||
const [updateOrganizationByInn, { loading: isUpdatingOrganization }] =
|
||||
useMutation(UPDATE_ORGANIZATION_BY_INN);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const { getSidebarMargin } = useSidebar()
|
||||
const { user, updateUser } = useAuth()
|
||||
const [updateUserProfile, { loading: isSaving }] = useMutation(UPDATE_USER_PROFILE)
|
||||
const [updateOrganizationByInn, { loading: isUpdatingOrganization }] = useMutation(UPDATE_ORGANIZATION_BY_INN)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [saveMessage, setSaveMessage] = useState<{
|
||||
type: "success" | "error";
|
||||
text: string;
|
||||
} | null>(null);
|
||||
const [partnerLink, setPartnerLink] = useState("");
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
|
||||
const [localAvatarUrl, setLocalAvatarUrl] = useState<string | null>(null);
|
||||
const phoneInputRef = useRef<HTMLInputElement>(null);
|
||||
const whatsappInputRef = useRef<HTMLInputElement>(null);
|
||||
type: 'success' | 'error'
|
||||
text: string
|
||||
} | null>(null)
|
||||
const [partnerLink, setPartnerLink] = useState('')
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
|
||||
const [localAvatarUrl, setLocalAvatarUrl] = useState<string | null>(null)
|
||||
const phoneInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const whatsappInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Инициализируем данные из пользователя и организации
|
||||
const [formData, setFormData] = useState({
|
||||
// Контактные данные организации
|
||||
orgPhone: "", // телефон организации, не пользователя
|
||||
managerName: "",
|
||||
telegram: "",
|
||||
whatsapp: "",
|
||||
email: "",
|
||||
orgPhone: '', // телефон организации, не пользователя
|
||||
managerName: '',
|
||||
telegram: '',
|
||||
whatsapp: '',
|
||||
email: '',
|
||||
|
||||
// Организация - данные могут быть заполнены из DaData
|
||||
orgName: "",
|
||||
address: "",
|
||||
orgName: '',
|
||||
address: '',
|
||||
|
||||
// Юридические данные - могут быть заполнены из DaData
|
||||
fullName: "",
|
||||
inn: "",
|
||||
ogrn: "",
|
||||
registrationPlace: "",
|
||||
fullName: '',
|
||||
inn: '',
|
||||
ogrn: '',
|
||||
registrationPlace: '',
|
||||
|
||||
// Финансовые данные - требуют ручного заполнения
|
||||
bankName: "",
|
||||
bik: "",
|
||||
accountNumber: "",
|
||||
corrAccount: "",
|
||||
bankName: '',
|
||||
bik: '',
|
||||
accountNumber: '',
|
||||
corrAccount: '',
|
||||
|
||||
// API ключи маркетплейсов
|
||||
wildberriesApiKey: "",
|
||||
ozonApiKey: "",
|
||||
});
|
||||
wildberriesApiKey: '',
|
||||
ozonApiKey: '',
|
||||
})
|
||||
|
||||
// Загружаем данные организации при монтировании компонента
|
||||
useEffect(() => {
|
||||
if (user?.organization) {
|
||||
const org = user.organization;
|
||||
const org = user.organization
|
||||
|
||||
// Извлекаем первый телефон из phones JSON
|
||||
let orgPhone = "";
|
||||
let orgPhone = ''
|
||||
if (org.phones && Array.isArray(org.phones) && org.phones.length > 0) {
|
||||
orgPhone = org.phones[0].value || org.phones[0] || "";
|
||||
} else if (org.phones && typeof org.phones === "object") {
|
||||
const phoneValues = Object.values(org.phones);
|
||||
orgPhone = org.phones[0].value || org.phones[0] || ''
|
||||
} else if (org.phones && typeof org.phones === 'object') {
|
||||
const phoneValues = Object.values(org.phones)
|
||||
if (phoneValues.length > 0) {
|
||||
orgPhone = String(phoneValues[0]);
|
||||
orgPhone = String(phoneValues[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Извлекаем email из emails JSON
|
||||
let email = "";
|
||||
let email = ''
|
||||
if (org.emails && Array.isArray(org.emails) && org.emails.length > 0) {
|
||||
email = org.emails[0].value || org.emails[0] || "";
|
||||
} else if (org.emails && typeof org.emails === "object") {
|
||||
const emailValues = Object.values(org.emails);
|
||||
email = org.emails[0].value || org.emails[0] || ''
|
||||
} else if (org.emails && typeof org.emails === 'object') {
|
||||
const emailValues = Object.values(org.emails)
|
||||
if (emailValues.length > 0) {
|
||||
email = String(emailValues[0]);
|
||||
email = String(emailValues[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Извлекаем дополнительные данные из managementPost (JSON)
|
||||
let 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
|
||||
}
|
||||
} = {}
|
||||
try {
|
||||
if (org.managementPost && typeof org.managementPost === "string") {
|
||||
customContacts = JSON.parse(org.managementPost);
|
||||
if (org.managementPost && typeof org.managementPost === 'string') {
|
||||
customContacts = JSON.parse(org.managementPost)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Ошибка парсинга managementPost:", e);
|
||||
console.warn('Ошибка парсинга managementPost:', e)
|
||||
}
|
||||
|
||||
setFormData({
|
||||
orgPhone: orgPhone || "+7",
|
||||
managerName: user?.managerName || "",
|
||||
telegram: customContacts?.telegram || "",
|
||||
whatsapp: customContacts?.whatsapp || "",
|
||||
orgPhone: orgPhone || '+7',
|
||||
managerName: user?.managerName || '',
|
||||
telegram: customContacts?.telegram || '',
|
||||
whatsapp: customContacts?.whatsapp || '',
|
||||
email: email,
|
||||
orgName: org.name || "",
|
||||
address: org.address || "",
|
||||
fullName: org.fullName || "",
|
||||
inn: org.inn || "",
|
||||
ogrn: org.ogrn || "",
|
||||
registrationPlace: org.address || "",
|
||||
bankName: customContacts?.bankDetails?.bankName || "",
|
||||
bik: customContacts?.bankDetails?.bik || "",
|
||||
accountNumber: customContacts?.bankDetails?.accountNumber || "",
|
||||
corrAccount: customContacts?.bankDetails?.corrAccount || "",
|
||||
wildberriesApiKey: "",
|
||||
ozonApiKey: "",
|
||||
});
|
||||
orgName: org.name || '',
|
||||
address: org.address || '',
|
||||
fullName: org.fullName || '',
|
||||
inn: org.inn || '',
|
||||
ogrn: org.ogrn || '',
|
||||
registrationPlace: org.address || '',
|
||||
bankName: customContacts?.bankDetails?.bankName || '',
|
||||
bik: customContacts?.bankDetails?.bik || '',
|
||||
accountNumber: customContacts?.bankDetails?.accountNumber || '',
|
||||
corrAccount: customContacts?.bankDetails?.corrAccount || '',
|
||||
wildberriesApiKey: '',
|
||||
ozonApiKey: '',
|
||||
})
|
||||
}
|
||||
}, [user]);
|
||||
}, [user])
|
||||
|
||||
const getInitials = () => {
|
||||
const orgName = user?.organization?.name || user?.organization?.fullName;
|
||||
const orgName = user?.organization?.name || user?.organization?.fullName
|
||||
if (orgName) {
|
||||
return orgName.charAt(0).toUpperCase();
|
||||
return orgName.charAt(0).toUpperCase()
|
||||
}
|
||||
return user?.phone ? user.phone.slice(-2).toUpperCase() : "О";
|
||||
};
|
||||
return user?.phone ? user.phone.slice(-2).toUpperCase() : 'О'
|
||||
}
|
||||
|
||||
const getCabinetTypeName = () => {
|
||||
if (!user?.organization?.type) return "Не указан";
|
||||
if (!user?.organization?.type) return 'Не указан'
|
||||
|
||||
switch (user.organization.type) {
|
||||
case "FULFILLMENT":
|
||||
return "Фулфилмент";
|
||||
case "SELLER":
|
||||
return "Селлер";
|
||||
case "LOGIST":
|
||||
return "Логистика";
|
||||
case "WHOLESALE":
|
||||
return "Поставщик";
|
||||
case 'FULFILLMENT':
|
||||
return 'Фулфилмент'
|
||||
case 'SELLER':
|
||||
return 'Селлер'
|
||||
case 'LOGIST':
|
||||
return 'Логистика'
|
||||
case 'WHOLESALE':
|
||||
return 'Поставщик'
|
||||
default:
|
||||
return "Не указан";
|
||||
return 'Не указан'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Обновленная функция для проверки заполненности профиля
|
||||
const checkProfileCompleteness = () => {
|
||||
// Базовые поля (обязательные для всех)
|
||||
const baseFields = [
|
||||
{
|
||||
field: "orgPhone",
|
||||
label: "Телефон организации",
|
||||
field: 'orgPhone',
|
||||
label: 'Телефон организации',
|
||||
value: formData.orgPhone,
|
||||
},
|
||||
{
|
||||
field: "managerName",
|
||||
label: "Имя управляющего",
|
||||
field: 'managerName',
|
||||
label: 'Имя управляющего',
|
||||
value: formData.managerName,
|
||||
},
|
||||
{ field: "email", label: "Email", value: formData.email },
|
||||
];
|
||||
{ field: 'email', label: 'Email', value: formData.email },
|
||||
]
|
||||
|
||||
// Дополнительные поля в зависимости от типа кабинета
|
||||
const additionalFields = [];
|
||||
const additionalFields = []
|
||||
if (
|
||||
user?.organization?.type === "FULFILLMENT" ||
|
||||
user?.organization?.type === "LOGIST" ||
|
||||
user?.organization?.type === "WHOLESALE" ||
|
||||
user?.organization?.type === "SELLER"
|
||||
user?.organization?.type === 'FULFILLMENT' ||
|
||||
user?.organization?.type === 'LOGIST' ||
|
||||
user?.organization?.type === 'WHOLESALE' ||
|
||||
user?.organization?.type === 'SELLER'
|
||||
) {
|
||||
// Финансовые данные - всегда обязательны для всех типов кабинетов
|
||||
additionalFields.push(
|
||||
{
|
||||
field: "bankName",
|
||||
label: "Название банка",
|
||||
field: 'bankName',
|
||||
label: 'Название банка',
|
||||
value: formData.bankName,
|
||||
},
|
||||
{ field: "bik", label: "БИК", value: formData.bik },
|
||||
{ field: 'bik', label: 'БИК', value: formData.bik },
|
||||
{
|
||||
field: "accountNumber",
|
||||
label: "Расчетный счет",
|
||||
field: 'accountNumber',
|
||||
label: 'Расчетный счет',
|
||||
value: formData.accountNumber,
|
||||
},
|
||||
{
|
||||
field: "corrAccount",
|
||||
label: "Корр. счет",
|
||||
field: 'corrAccount',
|
||||
label: 'Корр. счет',
|
||||
value: formData.corrAccount,
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const allRequiredFields = [...baseFields, ...additionalFields];
|
||||
const filledRequiredFields = allRequiredFields.filter(
|
||||
(field) => field.value && field.value.trim() !== ""
|
||||
).length;
|
||||
const allRequiredFields = [...baseFields, ...additionalFields]
|
||||
const filledRequiredFields = allRequiredFields.filter((field) => field.value && field.value.trim() !== '').length
|
||||
|
||||
// Подсчитываем бонусные баллы за автоматически заполненные поля
|
||||
let autoFilledFields = 0;
|
||||
let totalAutoFields = 0;
|
||||
let autoFilledFields = 0
|
||||
let totalAutoFields = 0
|
||||
|
||||
// Номер телефона пользователя для авторизации (не считаем в процентах заполненности)
|
||||
// Телефон организации учитывается отдельно как обычное поле
|
||||
|
||||
// Данные организации из DaData (если есть ИНН)
|
||||
if (formData.inn || user?.organization?.inn) {
|
||||
totalAutoFields += 5; // ИНН + название + адрес + полное название + ОГРН
|
||||
totalAutoFields += 5 // ИНН + название + адрес + полное название + ОГРН
|
||||
|
||||
if (formData.inn || user?.organization?.inn) autoFilledFields += 1; // ИНН
|
||||
if (formData.orgName || user?.organization?.name) autoFilledFields += 1; // Название
|
||||
if (formData.address || user?.organization?.address)
|
||||
autoFilledFields += 1; // Адрес
|
||||
if (formData.fullName || user?.organization?.fullName)
|
||||
autoFilledFields += 1; // Полное название
|
||||
if (formData.ogrn || user?.organization?.ogrn) autoFilledFields += 1; // ОГРН
|
||||
if (formData.inn || user?.organization?.inn) autoFilledFields += 1 // ИНН
|
||||
if (formData.orgName || user?.organization?.name) autoFilledFields += 1 // Название
|
||||
if (formData.address || user?.organization?.address) autoFilledFields += 1 // Адрес
|
||||
if (formData.fullName || user?.organization?.fullName) autoFilledFields += 1 // Полное название
|
||||
if (formData.ogrn || user?.organization?.ogrn) autoFilledFields += 1 // ОГРН
|
||||
}
|
||||
|
||||
// Место регистрации
|
||||
if (formData.registrationPlace || user?.organization?.registrationDate) {
|
||||
autoFilledFields += 1;
|
||||
totalAutoFields += 1;
|
||||
autoFilledFields += 1
|
||||
totalAutoFields += 1
|
||||
}
|
||||
|
||||
const totalPossibleFields = allRequiredFields.length + totalAutoFields;
|
||||
const totalFilledFields = filledRequiredFields + autoFilledFields;
|
||||
const totalPossibleFields = allRequiredFields.length + totalAutoFields
|
||||
const totalFilledFields = filledRequiredFields + autoFilledFields
|
||||
|
||||
const percentage =
|
||||
totalPossibleFields > 0
|
||||
? Math.round((totalFilledFields / totalPossibleFields) * 100)
|
||||
: 0;
|
||||
const percentage = totalPossibleFields > 0 ? Math.round((totalFilledFields / totalPossibleFields) * 100) : 0
|
||||
const missingFields = allRequiredFields
|
||||
.filter((field) => !field.value || field.value.trim() === "")
|
||||
.map((field) => field.label);
|
||||
.filter((field) => !field.value || field.value.trim() === '')
|
||||
.map((field) => field.label)
|
||||
|
||||
return { percentage, missingFields };
|
||||
};
|
||||
return { percentage, missingFields }
|
||||
}
|
||||
|
||||
const profileStatus = checkProfileCompleteness();
|
||||
const isIncomplete = profileStatus.percentage < 100;
|
||||
const profileStatus = checkProfileCompleteness()
|
||||
const isIncomplete = profileStatus.percentage < 100
|
||||
|
||||
const generatePartnerLink = async () => {
|
||||
if (!user?.id) return;
|
||||
if (!user?.id) return
|
||||
|
||||
setIsGenerating(true);
|
||||
setSaveMessage(null);
|
||||
setIsGenerating(true)
|
||||
setSaveMessage(null)
|
||||
|
||||
try {
|
||||
// Генерируем уникальный код партнера
|
||||
const partnerCode = btoa(user.id + Date.now())
|
||||
.replace(/[^a-zA-Z0-9]/g, "")
|
||||
.substring(0, 12);
|
||||
const link = `${window.location.origin}/register?partner=${partnerCode}`;
|
||||
.replace(/[^a-zA-Z0-9]/g, '')
|
||||
.substring(0, 12)
|
||||
const link = `${window.location.origin}/register?partner=${partnerCode}`
|
||||
|
||||
setPartnerLink(link);
|
||||
setPartnerLink(link)
|
||||
setSaveMessage({
|
||||
type: "success",
|
||||
text: "Партнерская ссылка сгенерирована!",
|
||||
});
|
||||
type: 'success',
|
||||
text: 'Партнерская ссылка сгенерирована!',
|
||||
})
|
||||
|
||||
// TODO: Сохранить партнерский код в базе данных
|
||||
console.log("Partner code generated:", partnerCode);
|
||||
console.warn('Partner code generated:', partnerCode)
|
||||
} catch (error) {
|
||||
console.error("Error generating partner link:", error);
|
||||
setSaveMessage({ type: "error", text: "Ошибка при генерации ссылки" });
|
||||
console.error('Error generating partner link:', error)
|
||||
setSaveMessage({ type: 'error', text: 'Ошибка при генерации ссылки' })
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
setIsGenerating(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
if (!partnerLink) {
|
||||
await generatePartnerLink();
|
||||
return;
|
||||
await generatePartnerLink()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(partnerLink);
|
||||
setSaveMessage({ type: "success", text: "Ссылка скопирована!" });
|
||||
await navigator.clipboard.writeText(partnerLink)
|
||||
setSaveMessage({ type: 'success', text: 'Ссылка скопирована!' })
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error);
|
||||
setSaveMessage({ type: "error", text: "Ошибка при копировании" });
|
||||
console.error('Error copying to clipboard:', error)
|
||||
setSaveMessage({ type: 'error', text: 'Ошибка при копировании' })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleOpenLink = async () => {
|
||||
if (!partnerLink) {
|
||||
await generatePartnerLink();
|
||||
return;
|
||||
await generatePartnerLink()
|
||||
return
|
||||
}
|
||||
window.open(partnerLink, "_blank");
|
||||
};
|
||||
window.open(partnerLink, '_blank')
|
||||
}
|
||||
|
||||
const handleAvatarUpload = async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file || !user?.id) return;
|
||||
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file || !user?.id) return
|
||||
|
||||
setIsUploadingAvatar(true);
|
||||
setSaveMessage(null);
|
||||
setIsUploadingAvatar(true)
|
||||
setSaveMessage(null)
|
||||
|
||||
try {
|
||||
const avatarUrl = await S3Service.uploadAvatar(file, user.id);
|
||||
const avatarUrl = await S3Service.uploadAvatar(file, user.id)
|
||||
|
||||
// Сразу обновляем локальное состояние для мгновенного отображения
|
||||
setLocalAvatarUrl(avatarUrl);
|
||||
setLocalAvatarUrl(avatarUrl)
|
||||
|
||||
// Обновляем аватар пользователя через GraphQL
|
||||
const result = await updateUserProfile({
|
||||
@ -358,7 +346,7 @@ export function UserSettings() {
|
||||
if (data?.updateUserProfile?.success) {
|
||||
// Обновляем кеш Apollo Client
|
||||
try {
|
||||
const existingData: any = cache.readQuery({ query: GET_ME });
|
||||
const existingData: any = cache.readQuery({ query: GET_ME })
|
||||
if (existingData?.me) {
|
||||
cache.writeQuery({
|
||||
query: GET_ME,
|
||||
@ -368,439 +356,393 @@ export function UserSettings() {
|
||||
avatar: avatarUrl,
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Cache update error:", error);
|
||||
console.warn('Cache update error:', error)
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (result.data?.updateUserProfile?.success) {
|
||||
setSaveMessage({ type: "success", text: "Аватар успешно обновлен!" });
|
||||
setSaveMessage({ type: 'success', text: 'Аватар успешно обновлен!' })
|
||||
|
||||
// Обновляем локальное состояние в useAuth для мгновенного отображения в сайдбаре
|
||||
updateUser({ avatar: avatarUrl });
|
||||
updateUser({ avatar: avatarUrl })
|
||||
|
||||
// Принудительно обновляем Apollo Client кеш
|
||||
await apolloClient.refetchQueries({
|
||||
include: [GET_ME],
|
||||
});
|
||||
})
|
||||
|
||||
// Очищаем input файла
|
||||
if (event.target) {
|
||||
event.target.value = "";
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
// Очищаем сообщение через 3 секунды
|
||||
setTimeout(() => {
|
||||
setSaveMessage(null);
|
||||
}, 3000);
|
||||
setSaveMessage(null)
|
||||
}, 3000)
|
||||
} else {
|
||||
throw new Error(
|
||||
result.data?.updateUserProfile?.message || "Failed to update avatar"
|
||||
);
|
||||
throw new Error(result.data?.updateUserProfile?.message || 'Failed to update avatar')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error uploading avatar:", error);
|
||||
console.error('Error uploading avatar:', error)
|
||||
// Сбрасываем локальное состояние при ошибке
|
||||
setLocalAvatarUrl(null);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Ошибка при загрузке аватара";
|
||||
setSaveMessage({ type: "error", text: errorMessage });
|
||||
setLocalAvatarUrl(null)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Ошибка при загрузке аватара'
|
||||
setSaveMessage({ type: 'error', text: errorMessage })
|
||||
// Очищаем сообщение об ошибке через 5 секунд
|
||||
setTimeout(() => {
|
||||
setSaveMessage(null);
|
||||
}, 5000);
|
||||
setSaveMessage(null)
|
||||
}, 5000)
|
||||
} finally {
|
||||
setIsUploadingAvatar(false);
|
||||
setIsUploadingAvatar(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Функции для валидации и масок
|
||||
const validateEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailRegex.test(email)
|
||||
}
|
||||
|
||||
const formatPhoneInput = (value: string, isOptional: boolean = false) => {
|
||||
// Убираем все нецифровые символы
|
||||
const digitsOnly = value.replace(/\D/g, "");
|
||||
const digitsOnly = value.replace(/\D/g, '')
|
||||
|
||||
// Если строка пустая
|
||||
if (!digitsOnly) {
|
||||
// Для необязательных полей возвращаем пустую строку
|
||||
if (isOptional) return "";
|
||||
if (isOptional) return ''
|
||||
// Для обязательных полей возвращаем +7
|
||||
return "+7";
|
||||
return '+7'
|
||||
}
|
||||
|
||||
// Если пользователь ввел первую цифру не 7, добавляем 7 перед ней
|
||||
let cleaned = digitsOnly;
|
||||
if (!cleaned.startsWith("7")) {
|
||||
cleaned = "7" + cleaned;
|
||||
let cleaned = digitsOnly
|
||||
if (!cleaned.startsWith('7')) {
|
||||
cleaned = '7' + cleaned
|
||||
}
|
||||
|
||||
// Ограничиваем до 11 цифр (7 + 10 цифр номера)
|
||||
cleaned = cleaned.slice(0, 11);
|
||||
cleaned = cleaned.slice(0, 11)
|
||||
|
||||
// Форматируем в зависимости от длины
|
||||
if (cleaned.length <= 1) return isOptional && cleaned === "7" ? "" : "+7";
|
||||
if (cleaned.length <= 4) return `+7 (${cleaned.slice(1)}`;
|
||||
if (cleaned.length <= 7)
|
||||
return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4)}`;
|
||||
if (cleaned.length <= 9)
|
||||
return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(
|
||||
4,
|
||||
7
|
||||
)}-${cleaned.slice(7)}`;
|
||||
if (cleaned.length <= 1) return isOptional && cleaned === '7' ? '' : '+7'
|
||||
if (cleaned.length <= 4) return `+7 (${cleaned.slice(1)}`
|
||||
if (cleaned.length <= 7) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4)}`
|
||||
if (cleaned.length <= 9) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7)}`
|
||||
if (cleaned.length <= 11)
|
||||
return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(
|
||||
4,
|
||||
7
|
||||
)}-${cleaned.slice(7, 9)}-${cleaned.slice(9)}`;
|
||||
return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7, 9)}-${cleaned.slice(9)}`
|
||||
|
||||
return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(
|
||||
7,
|
||||
9
|
||||
)}-${cleaned.slice(9, 11)}`;
|
||||
};
|
||||
return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7, 9)}-${cleaned.slice(9, 11)}`
|
||||
}
|
||||
|
||||
const handlePhoneInputChange = (
|
||||
field: string,
|
||||
value: string,
|
||||
inputRef?: React.RefObject<HTMLInputElement>,
|
||||
isOptional: boolean = false
|
||||
inputRef: React.RefObject<HTMLInputElement | null>,
|
||||
isOptional: boolean = false,
|
||||
) => {
|
||||
const currentInput = inputRef?.current;
|
||||
const currentCursorPosition = currentInput?.selectionStart || 0;
|
||||
const currentValue =
|
||||
(formData[field as keyof typeof formData] as string) || "";
|
||||
const currentInput = inputRef?.current
|
||||
const currentCursorPosition = currentInput?.selectionStart || 0
|
||||
const currentValue = (formData[field as keyof typeof formData] as string) || ''
|
||||
|
||||
// Для необязательных полей разрешаем пустое значение
|
||||
if (isOptional && value.length < 2) {
|
||||
const formatted = formatPhoneInput(value, true);
|
||||
setFormData((prev) => ({ ...prev, [field]: formatted }));
|
||||
return;
|
||||
const formatted = formatPhoneInput(value, true)
|
||||
setFormData((prev) => ({ ...prev, [field]: formatted }))
|
||||
return
|
||||
}
|
||||
|
||||
// Для обязательных полей если пользователь пытается удалить +7, предотвращаем это
|
||||
if (!isOptional && value.length < 2) {
|
||||
value = "+7";
|
||||
value = '+7'
|
||||
}
|
||||
|
||||
const formatted = formatPhoneInput(value, isOptional);
|
||||
setFormData((prev) => ({ ...prev, [field]: formatted }));
|
||||
const formatted = formatPhoneInput(value, isOptional)
|
||||
setFormData((prev) => ({ ...prev, [field]: formatted }))
|
||||
|
||||
// Вычисляем новую позицию курсора
|
||||
if (currentInput) {
|
||||
setTimeout(() => {
|
||||
let newCursorPosition = currentCursorPosition;
|
||||
let newCursorPosition = currentCursorPosition
|
||||
|
||||
// Если длина увеличилась (добавили цифру), передвигаем курсор
|
||||
if (formatted.length > currentValue.length) {
|
||||
newCursorPosition =
|
||||
currentCursorPosition + (formatted.length - currentValue.length);
|
||||
newCursorPosition = currentCursorPosition + (formatted.length - currentValue.length)
|
||||
}
|
||||
// Если длина уменьшилась (удалили цифру), оставляем курсор на месте или сдвигаем немного
|
||||
else if (formatted.length < currentValue.length) {
|
||||
newCursorPosition = Math.min(currentCursorPosition, formatted.length);
|
||||
newCursorPosition = Math.min(currentCursorPosition, formatted.length)
|
||||
}
|
||||
|
||||
// Не позволяем курсору находиться перед +7
|
||||
newCursorPosition = Math.max(newCursorPosition, 2);
|
||||
newCursorPosition = Math.max(newCursorPosition, 2)
|
||||
|
||||
// Ограничиваем курсор длиной строки
|
||||
newCursorPosition = Math.min(newCursorPosition, formatted.length);
|
||||
newCursorPosition = Math.min(newCursorPosition, formatted.length)
|
||||
|
||||
currentInput.setSelectionRange(newCursorPosition, newCursorPosition);
|
||||
}, 0);
|
||||
currentInput.setSelectionRange(newCursorPosition, newCursorPosition)
|
||||
}, 0)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const formatTelegram = (value: string) => {
|
||||
// Убираем все символы кроме букв, цифр, _ и @
|
||||
let cleaned = value.replace(/[^a-zA-Z0-9_@]/g, "");
|
||||
let cleaned = value.replace(/[^a-zA-Z0-9_@]/g, '')
|
||||
|
||||
// Убираем лишние символы @
|
||||
cleaned = cleaned.replace(/@+/g, "@");
|
||||
cleaned = cleaned.replace(/@+/g, '@')
|
||||
|
||||
// Если есть символы после удаления @ и строка не начинается с @, добавляем @
|
||||
if (cleaned && !cleaned.startsWith("@")) {
|
||||
cleaned = "@" + cleaned;
|
||||
if (cleaned && !cleaned.startsWith('@')) {
|
||||
cleaned = '@' + cleaned
|
||||
}
|
||||
|
||||
// Ограничиваем длину (максимум 32 символа для Telegram)
|
||||
if (cleaned.length > 33) {
|
||||
cleaned = cleaned.substring(0, 33);
|
||||
cleaned = cleaned.substring(0, 33)
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
};
|
||||
return cleaned
|
||||
}
|
||||
|
||||
const validateName = (name: string) => {
|
||||
return /^[а-яёА-ЯЁa-zA-Z\s-]+$/.test(name) && name.trim().length >= 2;
|
||||
};
|
||||
return /^[а-яёА-ЯЁa-zA-Z\s-]+$/.test(name) && name.trim().length >= 2
|
||||
}
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
let processedValue = value;
|
||||
let processedValue = value
|
||||
|
||||
// Применяем маски и валидации
|
||||
switch (field) {
|
||||
case "orgPhone":
|
||||
case "whatsapp":
|
||||
processedValue = formatPhoneInput(value);
|
||||
break;
|
||||
case "telegram":
|
||||
processedValue = formatTelegram(value);
|
||||
break;
|
||||
case "email":
|
||||
case 'orgPhone':
|
||||
case 'whatsapp':
|
||||
processedValue = formatPhoneInput(value)
|
||||
break
|
||||
case 'telegram':
|
||||
processedValue = formatTelegram(value)
|
||||
break
|
||||
case 'email':
|
||||
// Для email не применяем маску, только валидацию при потере фокуса
|
||||
break;
|
||||
case "managerName":
|
||||
break
|
||||
case 'managerName':
|
||||
// Разрешаем только буквы, пробелы и дефисы
|
||||
processedValue = value.replace(/[^а-яёА-ЯЁa-zA-Z\s-]/g, "");
|
||||
break;
|
||||
processedValue = value.replace(/[^а-яёА-ЯЁa-zA-Z\s-]/g, '')
|
||||
break
|
||||
}
|
||||
|
||||
setFormData((prev) => ({ ...prev, [field]: processedValue }));
|
||||
};
|
||||
setFormData((prev) => ({ ...prev, [field]: processedValue }))
|
||||
}
|
||||
|
||||
// Функции для проверки ошибок
|
||||
const getFieldError = (field: string, value: string) => {
|
||||
if (!isEditing || !value.trim()) return null;
|
||||
if (!isEditing || !value.trim()) return null
|
||||
|
||||
switch (field) {
|
||||
case "email":
|
||||
return !validateEmail(value) ? "Неверный формат email" : null;
|
||||
case "managerName":
|
||||
return !validateName(value) ? "Только буквы, пробелы и дефисы" : null;
|
||||
case "orgPhone":
|
||||
case "whatsapp":
|
||||
const cleaned = value.replace(/\D/g, "");
|
||||
return cleaned.length !== 11 ? "Неверный формат телефона" : null;
|
||||
case "telegram":
|
||||
case 'email':
|
||||
return !validateEmail(value) ? 'Неверный формат email' : null
|
||||
case 'managerName':
|
||||
return !validateName(value) ? 'Только буквы, пробелы и дефисы' : null
|
||||
case 'orgPhone':
|
||||
case 'whatsapp':
|
||||
const cleaned = value.replace(/\D/g, '')
|
||||
return cleaned.length !== 11 ? 'Неверный формат телефона' : null
|
||||
case 'telegram':
|
||||
// Проверяем что после @ есть минимум 5 символов
|
||||
const usernameLength = value.startsWith("@")
|
||||
? value.length - 1
|
||||
: value.length;
|
||||
return usernameLength < 5 ? "Минимум 5 символов после @" : null;
|
||||
case "inn":
|
||||
const usernameLength = value.startsWith('@') ? value.length - 1 : value.length
|
||||
return usernameLength < 5 ? 'Минимум 5 символов после @' : null
|
||||
case 'inn':
|
||||
// Игнорируем автоматически сгенерированные ИНН селлеров
|
||||
if (value.startsWith("SELLER_")) {
|
||||
return null;
|
||||
if (value.startsWith('SELLER_')) {
|
||||
return null
|
||||
}
|
||||
const innCleaned = value.replace(/\D/g, "");
|
||||
const innCleaned = value.replace(/\D/g, '')
|
||||
if (innCleaned.length !== 10 && innCleaned.length !== 12) {
|
||||
return "ИНН должен содержать 10 или 12 цифр";
|
||||
return 'ИНН должен содержать 10 или 12 цифр'
|
||||
}
|
||||
return null;
|
||||
case "bankName":
|
||||
return value.trim().length < 3 ? "Минимум 3 символа" : null;
|
||||
case "bik":
|
||||
const bikCleaned = value.replace(/\D/g, "");
|
||||
return bikCleaned.length !== 9 ? "БИК должен содержать 9 цифр" : null;
|
||||
case "accountNumber":
|
||||
const accountCleaned = value.replace(/\D/g, "");
|
||||
return accountCleaned.length !== 20
|
||||
? "Расчетный счет должен содержать 20 цифр"
|
||||
: null;
|
||||
case "corrAccount":
|
||||
const corrCleaned = value.replace(/\D/g, "");
|
||||
return corrCleaned.length !== 20
|
||||
? "Корр. счет должен содержать 20 цифр"
|
||||
: null;
|
||||
return null
|
||||
case 'bankName':
|
||||
return value.trim().length < 3 ? 'Минимум 3 символа' : null
|
||||
case 'bik':
|
||||
const bikCleaned = value.replace(/\D/g, '')
|
||||
return bikCleaned.length !== 9 ? 'БИК должен содержать 9 цифр' : null
|
||||
case 'accountNumber':
|
||||
const accountCleaned = value.replace(/\D/g, '')
|
||||
return accountCleaned.length !== 20 ? 'Расчетный счет должен содержать 20 цифр' : null
|
||||
case 'corrAccount':
|
||||
const corrCleaned = value.replace(/\D/g, '')
|
||||
return corrCleaned.length !== 20 ? 'Корр. счет должен содержать 20 цифр' : null
|
||||
default:
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Проверка наличия ошибок валидации
|
||||
const hasValidationErrors = () => {
|
||||
const fields = [
|
||||
"orgPhone",
|
||||
"managerName",
|
||||
"telegram",
|
||||
"whatsapp",
|
||||
"email",
|
||||
"inn",
|
||||
"bankName",
|
||||
"bik",
|
||||
"accountNumber",
|
||||
"corrAccount",
|
||||
];
|
||||
'orgPhone',
|
||||
'managerName',
|
||||
'telegram',
|
||||
'whatsapp',
|
||||
'email',
|
||||
'inn',
|
||||
'bankName',
|
||||
'bik',
|
||||
'accountNumber',
|
||||
'corrAccount',
|
||||
]
|
||||
|
||||
// Проверяем ошибки валидации только в заполненных полях
|
||||
const hasErrors = fields.some((field) => {
|
||||
const value = formData[field as keyof typeof formData];
|
||||
const value = formData[field as keyof typeof formData]
|
||||
// Проверяем ошибки только для заполненных полей
|
||||
if (!value || !value.trim()) return false;
|
||||
if (!value || !value.trim()) return false
|
||||
|
||||
const error = getFieldError(field, value);
|
||||
return error !== null;
|
||||
});
|
||||
const error = getFieldError(field, value)
|
||||
return error !== null
|
||||
})
|
||||
|
||||
// Убираем проверку обязательных полей - пользователь может заполнять постепенно
|
||||
return hasErrors;
|
||||
};
|
||||
return hasErrors
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
// Сброс предыдущих сообщений
|
||||
setSaveMessage(null);
|
||||
setSaveMessage(null)
|
||||
|
||||
try {
|
||||
// Проверяем, изменился ли ИНН и нужно ли обновить данные организации
|
||||
const currentInn = formData.inn || user?.organization?.inn || "";
|
||||
const originalInn = user?.organization?.inn || "";
|
||||
const innCleaned = currentInn.replace(/\D/g, "");
|
||||
const originalInnCleaned = originalInn.replace(/\D/g, "");
|
||||
const currentInn = formData.inn || user?.organization?.inn || ''
|
||||
const originalInn = user?.organization?.inn || ''
|
||||
const innCleaned = currentInn.replace(/\D/g, '')
|
||||
const originalInnCleaned = originalInn.replace(/\D/g, '')
|
||||
|
||||
// Если ИНН изменился и валиден, сначала обновляем данные организации
|
||||
if (
|
||||
innCleaned !== originalInnCleaned &&
|
||||
(innCleaned.length === 10 || innCleaned.length === 12)
|
||||
) {
|
||||
if (innCleaned !== originalInnCleaned && (innCleaned.length === 10 || innCleaned.length === 12)) {
|
||||
setSaveMessage({
|
||||
type: "success",
|
||||
text: "Обновляем данные организации...",
|
||||
});
|
||||
type: 'success',
|
||||
text: 'Обновляем данные организации...',
|
||||
})
|
||||
|
||||
const orgResult = await updateOrganizationByInn({
|
||||
variables: { inn: innCleaned },
|
||||
});
|
||||
})
|
||||
|
||||
if (!orgResult.data?.updateOrganizationByInn?.success) {
|
||||
setSaveMessage({
|
||||
type: "error",
|
||||
text:
|
||||
orgResult.data?.updateOrganizationByInn?.message ||
|
||||
"Ошибка при обновлении данных организации",
|
||||
});
|
||||
return;
|
||||
type: 'error',
|
||||
text: orgResult.data?.updateOrganizationByInn?.message || 'Ошибка при обновлении данных организации',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setSaveMessage({
|
||||
type: "success",
|
||||
text: "Данные организации обновлены. Сохраняем профиль...",
|
||||
});
|
||||
type: 'success',
|
||||
text: 'Данные организации обновлены. Сохраняем профиль...',
|
||||
})
|
||||
}
|
||||
|
||||
// Подготавливаем только заполненные поля для отправки
|
||||
const inputData: {
|
||||
orgPhone?: string;
|
||||
managerName?: string;
|
||||
telegram?: string;
|
||||
whatsapp?: string;
|
||||
email?: string;
|
||||
bankName?: string;
|
||||
bik?: string;
|
||||
accountNumber?: string;
|
||||
corrAccount?: string;
|
||||
} = {};
|
||||
orgPhone?: string
|
||||
managerName?: string
|
||||
telegram?: string
|
||||
whatsapp?: string
|
||||
email?: string
|
||||
bankName?: string
|
||||
bik?: string
|
||||
accountNumber?: string
|
||||
corrAccount?: string
|
||||
} = {}
|
||||
|
||||
// orgName больше не редактируется - устанавливается только при регистрации
|
||||
if (formData.orgPhone?.trim())
|
||||
inputData.orgPhone = formData.orgPhone.trim();
|
||||
if (formData.managerName?.trim())
|
||||
inputData.managerName = formData.managerName.trim();
|
||||
if (formData.telegram?.trim())
|
||||
inputData.telegram = formData.telegram.trim();
|
||||
if (formData.whatsapp?.trim())
|
||||
inputData.whatsapp = formData.whatsapp.trim();
|
||||
if (formData.email?.trim()) inputData.email = formData.email.trim();
|
||||
if (formData.bankName?.trim())
|
||||
inputData.bankName = formData.bankName.trim();
|
||||
if (formData.bik?.trim()) inputData.bik = formData.bik.trim();
|
||||
if (formData.accountNumber?.trim())
|
||||
inputData.accountNumber = formData.accountNumber.trim();
|
||||
if (formData.corrAccount?.trim())
|
||||
inputData.corrAccount = formData.corrAccount.trim();
|
||||
if (formData.orgPhone?.trim()) inputData.orgPhone = formData.orgPhone.trim()
|
||||
if (formData.managerName?.trim()) inputData.managerName = formData.managerName.trim()
|
||||
if (formData.telegram?.trim()) inputData.telegram = formData.telegram.trim()
|
||||
if (formData.whatsapp?.trim()) inputData.whatsapp = formData.whatsapp.trim()
|
||||
if (formData.email?.trim()) inputData.email = formData.email.trim()
|
||||
if (formData.bankName?.trim()) inputData.bankName = formData.bankName.trim()
|
||||
if (formData.bik?.trim()) inputData.bik = formData.bik.trim()
|
||||
if (formData.accountNumber?.trim()) inputData.accountNumber = formData.accountNumber.trim()
|
||||
if (formData.corrAccount?.trim()) inputData.corrAccount = formData.corrAccount.trim()
|
||||
|
||||
const result = await updateUserProfile({
|
||||
variables: {
|
||||
input: inputData,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (result.data?.updateUserProfile?.success) {
|
||||
setSaveMessage({
|
||||
type: "success",
|
||||
text: "Профиль успешно сохранен! Обновляем страницу...",
|
||||
});
|
||||
type: 'success',
|
||||
text: 'Профиль успешно сохранен! Обновляем страницу...',
|
||||
})
|
||||
|
||||
// Простое обновление страницы после успешного сохранения
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
window.location.reload()
|
||||
}, 1000)
|
||||
} else {
|
||||
setSaveMessage({
|
||||
type: "error",
|
||||
text:
|
||||
result.data?.updateUserProfile?.message ||
|
||||
"Ошибка при сохранении профиля",
|
||||
});
|
||||
type: 'error',
|
||||
text: result.data?.updateUserProfile?.message || 'Ошибка при сохранении профиля',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving profile:", error);
|
||||
setSaveMessage({ type: "error", text: "Ошибка при сохранении профиля" });
|
||||
console.error('Error saving profile:', error)
|
||||
setSaveMessage({ type: 'error', text: 'Ошибка при сохранении профиля' })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return "";
|
||||
if (!dateString) return ''
|
||||
try {
|
||||
let date: Date;
|
||||
let date: Date
|
||||
|
||||
// Проверяем, является ли строка числом (Unix timestamp)
|
||||
if (/^\d+$/.test(dateString)) {
|
||||
// Если это Unix timestamp в миллисекундах
|
||||
const timestamp = parseInt(dateString, 10);
|
||||
date = new Date(timestamp);
|
||||
const timestamp = parseInt(dateString, 10)
|
||||
date = new Date(timestamp)
|
||||
} else {
|
||||
// Обычная строка даты
|
||||
date = new Date(dateString);
|
||||
date = new Date(dateString)
|
||||
}
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
console.warn("Invalid date string:", dateString);
|
||||
return "Неверная дата";
|
||||
console.warn('Invalid date string:', dateString)
|
||||
return 'Неверная дата'
|
||||
}
|
||||
|
||||
return date.toLocaleDateString("ru-RU", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error formatting date:", error, dateString);
|
||||
return "Ошибка даты";
|
||||
console.error('Error formatting date:', error, dateString)
|
||||
return 'Ошибка даты'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main
|
||||
className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}
|
||||
>
|
||||
<main className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}>
|
||||
<div className="h-full w-full flex flex-col">
|
||||
{/* Сообщения о сохранении */}
|
||||
{saveMessage && (
|
||||
<Alert
|
||||
className={`mb-4 ${
|
||||
saveMessage.type === "success"
|
||||
? "border-green-500 bg-green-500/10"
|
||||
: "border-red-500 bg-red-500/10"
|
||||
saveMessage.type === 'success' ? 'border-green-500 bg-green-500/10' : 'border-red-500 bg-red-500/10'
|
||||
}`}
|
||||
>
|
||||
<AlertDescription
|
||||
className={
|
||||
saveMessage.type === "success"
|
||||
? "text-green-400"
|
||||
: "text-red-400"
|
||||
}
|
||||
>
|
||||
<AlertDescription className={saveMessage.type === 'success' ? 'text-green-400' : 'text-red-400'}>
|
||||
{saveMessage.text}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@ -811,55 +753,40 @@ export function UserSettings() {
|
||||
<Tabs defaultValue="profile" className="h-full flex flex-col">
|
||||
<TabsList
|
||||
className={`grid w-full glass-card mb-4 flex-shrink-0 ${
|
||||
user?.organization?.type === "SELLER"
|
||||
? "grid-cols-4"
|
||||
: user?.organization?.type === "FULFILLMENT" ||
|
||||
user?.organization?.type === "LOGIST" ||
|
||||
user?.organization?.type === "WHOLESALE"
|
||||
? "grid-cols-4"
|
||||
: "grid-cols-3"
|
||||
user?.organization?.type === 'SELLER'
|
||||
? 'grid-cols-4'
|
||||
: user?.organization?.type === 'FULFILLMENT' ||
|
||||
user?.organization?.type === 'LOGIST' ||
|
||||
user?.organization?.type === 'WHOLESALE'
|
||||
? 'grid-cols-4'
|
||||
: 'grid-cols-3'
|
||||
}`}
|
||||
>
|
||||
<TabsTrigger
|
||||
value="profile"
|
||||
className="text-white data-[state=active]:bg-white/20 cursor-pointer"
|
||||
>
|
||||
<TabsTrigger value="profile" className="text-white data-[state=active]:bg-white/20 cursor-pointer">
|
||||
<User className="h-4 w-4 mr-2" />
|
||||
Профиль
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="organization"
|
||||
className="text-white data-[state=active]:bg-white/20 cursor-pointer"
|
||||
>
|
||||
<TabsTrigger value="organization" className="text-white data-[state=active]:bg-white/20 cursor-pointer">
|
||||
<Building2 className="h-4 w-4 mr-2" />
|
||||
Организация
|
||||
</TabsTrigger>
|
||||
{(user?.organization?.type === "FULFILLMENT" ||
|
||||
user?.organization?.type === "LOGIST" ||
|
||||
user?.organization?.type === "WHOLESALE" ||
|
||||
user?.organization?.type === "SELLER") && (
|
||||
<TabsTrigger
|
||||
value="financial"
|
||||
className="text-white data-[state=active]:bg-white/20 cursor-pointer"
|
||||
>
|
||||
{(user?.organization?.type === 'FULFILLMENT' ||
|
||||
user?.organization?.type === 'LOGIST' ||
|
||||
user?.organization?.type === 'WHOLESALE' ||
|
||||
user?.organization?.type === 'SELLER') && (
|
||||
<TabsTrigger value="financial" className="text-white data-[state=active]:bg-white/20 cursor-pointer">
|
||||
<CreditCard className="h-4 w-4 mr-2" />
|
||||
Финансы
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{user?.organization?.type === "SELLER" && (
|
||||
<TabsTrigger
|
||||
value="api"
|
||||
className="text-white data-[state=active]:bg-white/20 cursor-pointer"
|
||||
>
|
||||
{user?.organization?.type === 'SELLER' && (
|
||||
<TabsTrigger value="api" className="text-white data-[state=active]:bg-white/20 cursor-pointer">
|
||||
<Key className="h-4 w-4 mr-2" />
|
||||
API
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{user?.organization?.type !== "SELLER" && (
|
||||
<TabsTrigger
|
||||
value="tools"
|
||||
className="text-white data-[state=active]:bg-white/20 cursor-pointer"
|
||||
>
|
||||
{user?.organization?.type !== 'SELLER' && (
|
||||
<TabsTrigger value="tools" className="text-white data-[state=active]:bg-white/20 cursor-pointer">
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
Инструменты
|
||||
</TabsTrigger>
|
||||
@ -874,21 +801,15 @@ export function UserSettings() {
|
||||
<div className="flex items-center gap-4">
|
||||
<User className="h-6 w-6 text-purple-400" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
Профиль пользователя
|
||||
</h2>
|
||||
<p className="text-white/70 text-sm">
|
||||
Личная информация и контактные данные
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-white">Профиль пользователя</h2>
|
||||
<p className="text-white/70 text-sm">Личная информация и контактные данные</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Компактный индикатор прогресса */}
|
||||
<div className="flex items-center gap-2 mr-2">
|
||||
<div className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center">
|
||||
<span className="text-xs text-white font-medium">
|
||||
{profileStatus.percentage}%
|
||||
</span>
|
||||
<span className="text-xs text-white font-medium">{profileStatus.percentage}%</span>
|
||||
</div>
|
||||
<div className="hidden sm:block text-xs text-white/70">
|
||||
{isIncomplete ? (
|
||||
@ -914,13 +835,11 @@ export function UserSettings() {
|
||||
onClick={handleSave}
|
||||
disabled={hasValidationErrors() || isSaving}
|
||||
className={`glass-button text-white cursor-pointer ${
|
||||
hasValidationErrors() || isSaving
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: ""
|
||||
hasValidationErrors() || isSaving ? 'opacity-50 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{isSaving ? "Сохранение..." : "Сохранить"}
|
||||
{isSaving ? 'Сохранение...' : 'Сохранить'}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
@ -940,23 +859,18 @@ export function UserSettings() {
|
||||
<Avatar className="h-16 w-16">
|
||||
{localAvatarUrl || user?.avatar ? (
|
||||
<Image
|
||||
src={localAvatarUrl || user.avatar}
|
||||
src={localAvatarUrl || user?.avatar || ''}
|
||||
alt="Аватар"
|
||||
width={64}
|
||||
height={64}
|
||||
className="w-full h-full object-cover rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<AvatarFallback className="bg-purple-500 text-white text-lg">
|
||||
{getInitials()}
|
||||
</AvatarFallback>
|
||||
<AvatarFallback className="bg-purple-500 text-white text-lg">{getInitials()}</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
<div className="absolute -bottom-1 -right-1">
|
||||
<label
|
||||
htmlFor="avatar-upload"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<label htmlFor="avatar-upload" className="cursor-pointer">
|
||||
<div className="w-6 h-6 bg-purple-600 rounded-full flex items-center justify-center hover:bg-purple-700 transition-colors">
|
||||
{isUploadingAvatar ? (
|
||||
<RefreshCw className="h-3 w-3 text-white animate-spin" />
|
||||
@ -977,18 +891,13 @@ export function UserSettings() {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-white font-medium text-lg">
|
||||
{user?.organization?.name ||
|
||||
user?.organization?.fullName ||
|
||||
"Пользователь"}
|
||||
{user?.organization?.name || user?.organization?.fullName || 'Пользователь'}
|
||||
</p>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-white/10 text-white border-white/20 mt-1"
|
||||
>
|
||||
<Badge variant="outline" className="bg-white/10 text-white border-white/20 mt-1">
|
||||
{getCabinetTypeName()}
|
||||
</Badge>
|
||||
<p className="text-white/60 text-sm mt-2">
|
||||
Авторизован по номеру: {formatPhone(user?.phone || "")}
|
||||
Авторизован по номеру: {formatPhone(user?.phone || '')}
|
||||
</p>
|
||||
{user?.createdAt && (
|
||||
<p className="text-white/50 text-xs mt-1 flex items-center gap-1">
|
||||
@ -1003,65 +912,49 @@ export function UserSettings() {
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Номер телефона организации
|
||||
</Label>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Номер телефона организации</Label>
|
||||
<Input
|
||||
ref={phoneInputRef}
|
||||
value={formData.orgPhone || ""}
|
||||
onChange={(e) =>
|
||||
handlePhoneInputChange(
|
||||
"orgPhone",
|
||||
e.target.value,
|
||||
phoneInputRef
|
||||
)
|
||||
}
|
||||
value={formData.orgPhone || ''}
|
||||
onChange={(e) => handlePhoneInputChange('orgPhone', e.target.value, phoneInputRef)}
|
||||
onKeyDown={(e) => {
|
||||
// Предотвращаем удаление +7
|
||||
if (
|
||||
(e.key === "Backspace" || e.key === "Delete") &&
|
||||
phoneInputRef.current?.selectionStart <= 2
|
||||
(e.key === 'Backspace' || e.key === 'Delete') &&
|
||||
(phoneInputRef.current?.selectionStart || 0) <= 2
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
placeholder="+7 (999) 999-99-99"
|
||||
readOnly={!isEditing}
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70 ${
|
||||
getFieldError("orgPhone", formData.orgPhone)
|
||||
? "border-red-400"
|
||||
: ""
|
||||
getFieldError('orgPhone', formData.orgPhone) ? 'border-red-400' : ''
|
||||
}`}
|
||||
/>
|
||||
{getFieldError("orgPhone", formData.orgPhone) && (
|
||||
{getFieldError('orgPhone', formData.orgPhone) && (
|
||||
<p className="text-red-400 text-xs mt-1 flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
{getFieldError("orgPhone", formData.orgPhone)}
|
||||
{getFieldError('orgPhone', formData.orgPhone)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Имя управляющего
|
||||
</Label>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Имя управляющего</Label>
|
||||
<Input
|
||||
value={formData.managerName || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("managerName", e.target.value)
|
||||
}
|
||||
value={formData.managerName || ''}
|
||||
onChange={(e) => handleInputChange('managerName', e.target.value)}
|
||||
placeholder="Иван Иванов"
|
||||
readOnly={!isEditing}
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70 ${
|
||||
getFieldError("managerName", formData.managerName)
|
||||
? "border-red-400"
|
||||
: ""
|
||||
getFieldError('managerName', formData.managerName) ? 'border-red-400' : ''
|
||||
}`}
|
||||
/>
|
||||
{getFieldError("managerName", formData.managerName) && (
|
||||
{getFieldError('managerName', formData.managerName) && (
|
||||
<p className="text-red-400 text-xs mt-1 flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
{getFieldError("managerName", formData.managerName)}
|
||||
{getFieldError('managerName', formData.managerName)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@ -1074,22 +967,18 @@ export function UserSettings() {
|
||||
Telegram
|
||||
</Label>
|
||||
<Input
|
||||
value={formData.telegram || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("telegram", e.target.value)
|
||||
}
|
||||
value={formData.telegram || ''}
|
||||
onChange={(e) => handleInputChange('telegram', e.target.value)}
|
||||
placeholder="@username"
|
||||
readOnly={!isEditing}
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70 ${
|
||||
getFieldError("telegram", formData.telegram)
|
||||
? "border-red-400"
|
||||
: ""
|
||||
getFieldError('telegram', formData.telegram) ? 'border-red-400' : ''
|
||||
}`}
|
||||
/>
|
||||
{getFieldError("telegram", formData.telegram) && (
|
||||
{getFieldError('telegram', formData.telegram) && (
|
||||
<p className="text-red-400 text-xs mt-1 flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
{getFieldError("telegram", formData.telegram)}
|
||||
{getFieldError('telegram', formData.telegram)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@ -1101,31 +990,22 @@ export function UserSettings() {
|
||||
</Label>
|
||||
<Input
|
||||
ref={whatsappInputRef}
|
||||
value={formData.whatsapp || ""}
|
||||
onChange={(e) =>
|
||||
handlePhoneInputChange(
|
||||
"whatsapp",
|
||||
e.target.value,
|
||||
whatsappInputRef,
|
||||
true
|
||||
)
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
value={formData.whatsapp || ''}
|
||||
onChange={(e) => handlePhoneInputChange('whatsapp', e.target.value, whatsappInputRef, true)}
|
||||
onKeyDown={(_e) => {
|
||||
// Для WhatsApp разрешаем полное удаление (поле необязательное)
|
||||
// Никаких ограничений на удаление
|
||||
}}
|
||||
placeholder="Необязательно"
|
||||
readOnly={!isEditing}
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70 ${
|
||||
getFieldError("whatsapp", formData.whatsapp)
|
||||
? "border-red-400"
|
||||
: ""
|
||||
getFieldError('whatsapp', formData.whatsapp) ? 'border-red-400' : ''
|
||||
}`}
|
||||
/>
|
||||
{getFieldError("whatsapp", formData.whatsapp) && (
|
||||
{getFieldError('whatsapp', formData.whatsapp) && (
|
||||
<p className="text-red-400 text-xs mt-1 flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
{getFieldError("whatsapp", formData.whatsapp)}
|
||||
{getFieldError('whatsapp', formData.whatsapp)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@ -1137,22 +1017,18 @@ export function UserSettings() {
|
||||
</Label>
|
||||
<Input
|
||||
type="email"
|
||||
value={formData.email || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("email", e.target.value)
|
||||
}
|
||||
value={formData.email || ''}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
placeholder="example@company.com"
|
||||
readOnly={!isEditing}
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70 ${
|
||||
getFieldError("email", formData.email)
|
||||
? "border-red-400"
|
||||
: ""
|
||||
getFieldError('email', formData.email) ? 'border-red-400' : ''
|
||||
}`}
|
||||
/>
|
||||
{getFieldError("email", formData.email) && (
|
||||
{getFieldError('email', formData.email) && (
|
||||
<p className="text-red-400 text-xs mt-1 flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
{getFieldError("email", formData.email)}
|
||||
{getFieldError('email', formData.email)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@ -1162,31 +1038,22 @@ export function UserSettings() {
|
||||
</TabsContent>
|
||||
|
||||
{/* Организация и юридические данные */}
|
||||
<TabsContent
|
||||
value="organization"
|
||||
className="flex-1 overflow-hidden"
|
||||
>
|
||||
<TabsContent value="organization" className="flex-1 overflow-hidden">
|
||||
<Card className="glass-card p-6 h-full overflow-hidden">
|
||||
{/* Заголовок вкладки с кнопками */}
|
||||
<div className="flex items-center justify-between mb-6 pb-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-4">
|
||||
<Building2 className="h-6 w-6 text-blue-400" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
Данные организации
|
||||
</h2>
|
||||
<p className="text-white/70 text-sm">
|
||||
Юридическая информация и реквизиты
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-white">Данные организации</h2>
|
||||
<p className="text-white/70 text-sm">Юридическая информация и реквизиты</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{(formData.inn || user?.organization?.inn) && (
|
||||
<div className="flex items-center gap-2 mr-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400" />
|
||||
<span className="text-green-400 text-sm">
|
||||
Проверено
|
||||
</span>
|
||||
<span className="text-green-400 text-sm">Проверено</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1205,13 +1072,11 @@ export function UserSettings() {
|
||||
onClick={handleSave}
|
||||
disabled={hasValidationErrors() || isSaving}
|
||||
className={`glass-button text-white cursor-pointer ${
|
||||
hasValidationErrors() || isSaving
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: ""
|
||||
hasValidationErrors() || isSaving ? 'opacity-50 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{isSaving ? "Сохранение..." : "Сохранить"}
|
||||
{isSaving ? 'Сохранение...' : 'Сохранить'}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
@ -1231,8 +1096,8 @@ export function UserSettings() {
|
||||
<div className="mb-6 p-3 bg-blue-500/10 rounded-lg border border-blue-500/20">
|
||||
<p className="text-blue-300 text-sm flex items-center gap-2">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
При сохранении с измененным ИНН мы автоматически обновляем
|
||||
все остальные данные из федерального реестра
|
||||
При сохранении с измененным ИНН мы автоматически обновляем все остальные данные из федерального
|
||||
реестра
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -1241,48 +1106,32 @@ export function UserSettings() {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
{user?.organization?.type === "SELLER"
|
||||
? "Название магазина"
|
||||
: "Название организации"}
|
||||
{user?.organization?.type === 'SELLER' ? 'Название магазина' : 'Название организации'}
|
||||
</Label>
|
||||
<Input
|
||||
value={
|
||||
formData.orgName || user?.organization?.name || ""
|
||||
}
|
||||
onChange={(e) =>
|
||||
handleInputChange("orgName", e.target.value)
|
||||
}
|
||||
value={formData.orgName || user?.organization?.name || ''}
|
||||
onChange={(e) => handleInputChange('orgName', e.target.value)}
|
||||
placeholder={
|
||||
user?.organization?.type === "SELLER"
|
||||
? "Название магазина"
|
||||
: "Название организации"
|
||||
user?.organization?.type === 'SELLER' ? 'Название магазина' : 'Название организации'
|
||||
}
|
||||
readOnly={true}
|
||||
className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70"
|
||||
/>
|
||||
{user?.organization?.type === "SELLER" ? (
|
||||
{user?.organization?.type === 'SELLER' ? (
|
||||
<p className="text-white/50 text-xs mt-1">
|
||||
Название устанавливается при регистрации кабинета и
|
||||
не может быть изменено.
|
||||
Название устанавливается при регистрации кабинета и не может быть изменено.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-white/50 text-xs mt-1">
|
||||
Автоматически заполняется из федерального реестра
|
||||
при указании ИНН.
|
||||
Автоматически заполняется из федерального реестра при указании ИНН.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Полное название
|
||||
</Label>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Полное название</Label>
|
||||
<Input
|
||||
value={
|
||||
formData.fullName ||
|
||||
user?.organization?.fullName ||
|
||||
""
|
||||
}
|
||||
value={formData.fullName || user?.organization?.fullName || ''}
|
||||
readOnly
|
||||
className="glass-input text-white h-10 read-only:opacity-70"
|
||||
/>
|
||||
@ -1297,29 +1146,18 @@ export function UserSettings() {
|
||||
Адрес
|
||||
</Label>
|
||||
<Input
|
||||
value={
|
||||
formData.address ||
|
||||
user?.organization?.address ||
|
||||
""
|
||||
}
|
||||
onChange={(e) =>
|
||||
handleInputChange("address", e.target.value)
|
||||
}
|
||||
value={formData.address || user?.organization?.address || ''}
|
||||
onChange={(e) => handleInputChange('address', e.target.value)}
|
||||
placeholder="г. Москва, ул. Примерная, д. 1"
|
||||
readOnly={
|
||||
!isEditing ||
|
||||
!!(formData.address || user?.organization?.address)
|
||||
}
|
||||
readOnly={!isEditing || !!(formData.address || user?.organization?.address)}
|
||||
className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Полный юридический адрес
|
||||
</Label>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Полный юридический адрес</Label>
|
||||
<Input
|
||||
value={user?.organization?.addressFull || ""}
|
||||
value={user?.organization?.addressFull || ''}
|
||||
readOnly
|
||||
className="glass-input text-white h-10 read-only:opacity-70"
|
||||
/>
|
||||
@ -1331,52 +1169,40 @@ export function UserSettings() {
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 flex items-center gap-2">
|
||||
ИНН
|
||||
{isUpdatingOrganization && (
|
||||
<RefreshCw className="h-3 w-3 animate-spin text-blue-400" />
|
||||
)}
|
||||
{isUpdatingOrganization && <RefreshCw className="h-3 w-3 animate-spin text-blue-400" />}
|
||||
</Label>
|
||||
<Input
|
||||
value={formData.inn || user?.organization?.inn || ""}
|
||||
value={formData.inn || user?.organization?.inn || ''}
|
||||
onChange={(e) => {
|
||||
handleInputChange("inn", e.target.value);
|
||||
handleInputChange('inn', e.target.value)
|
||||
}}
|
||||
placeholder="Введите ИНН организации"
|
||||
readOnly={!isEditing}
|
||||
disabled={isUpdatingOrganization}
|
||||
className={`glass-input text-white placeholder:text-white/40 h-10 ${
|
||||
!isEditing ? "read-only:opacity-70" : ""
|
||||
!isEditing ? 'read-only:opacity-70' : ''
|
||||
} ${
|
||||
getFieldError("inn", formData.inn)
|
||||
? "border-red-400"
|
||||
: ""
|
||||
} ${isUpdatingOrganization ? "opacity-50" : ""}`}
|
||||
getFieldError('inn', formData.inn) ? 'border-red-400' : ''
|
||||
} ${isUpdatingOrganization ? 'opacity-50' : ''}`}
|
||||
/>
|
||||
{getFieldError("inn", formData.inn) && (
|
||||
<p className="text-red-400 text-xs mt-1">
|
||||
{getFieldError("inn", formData.inn)}
|
||||
</p>
|
||||
{getFieldError('inn', formData.inn) && (
|
||||
<p className="text-red-400 text-xs mt-1">{getFieldError('inn', formData.inn)}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
ОГРН
|
||||
</Label>
|
||||
<Label className="text-white/80 text-sm mb-2 block">ОГРН</Label>
|
||||
<Input
|
||||
value={
|
||||
formData.ogrn || user?.organization?.ogrn || ""
|
||||
}
|
||||
value={formData.ogrn || user?.organization?.ogrn || ''}
|
||||
readOnly
|
||||
className="glass-input text-white h-10 read-only:opacity-70"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
КПП
|
||||
</Label>
|
||||
<Label className="text-white/80 text-sm mb-2 block">КПП</Label>
|
||||
<Input
|
||||
value={user?.organization?.kpp || ""}
|
||||
value={user?.organization?.kpp || ''}
|
||||
readOnly
|
||||
className="glass-input text-white h-10 read-only:opacity-70"
|
||||
/>
|
||||
@ -1386,34 +1212,27 @@ export function UserSettings() {
|
||||
{/* Руководитель и статус */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Руководитель организации
|
||||
</Label>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Руководитель организации</Label>
|
||||
<Input
|
||||
value={
|
||||
user?.organization?.managementName ||
|
||||
"Данные не указаны в реестре"
|
||||
}
|
||||
value={user?.organization?.managementName || 'Данные не указаны в реестре'}
|
||||
readOnly
|
||||
className="glass-input text-white h-10 read-only:opacity-70"
|
||||
placeholder="Данные отсутствуют в федеральном реестре"
|
||||
/>
|
||||
<p className="text-white/50 text-xs mt-1">
|
||||
{user?.organization?.managementName
|
||||
? "Данные из федерального реестра"
|
||||
: "Автоматически заполняется из федерального реестра при указании ИНН"}
|
||||
? 'Данные из федерального реестра'
|
||||
: 'Автоматически заполняется из федерального реестра при указании ИНН'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Статус организации
|
||||
</Label>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Статус организации</Label>
|
||||
<Input
|
||||
value={
|
||||
user?.organization?.status === "ACTIVE"
|
||||
? "Действующая"
|
||||
: user?.organization?.status || "Статус не указан"
|
||||
user?.organization?.status === 'ACTIVE'
|
||||
? 'Действующая'
|
||||
: user?.organization?.status || 'Статус не указан'
|
||||
}
|
||||
readOnly
|
||||
className="glass-input text-white h-10 read-only:opacity-70"
|
||||
@ -1430,9 +1249,7 @@ export function UserSettings() {
|
||||
Дата регистрации
|
||||
</Label>
|
||||
<Input
|
||||
value={formatDate(
|
||||
user.organization.registrationDate
|
||||
)}
|
||||
value={formatDate(user.organization.registrationDate)}
|
||||
readOnly
|
||||
className="glass-input text-white h-10 read-only:opacity-70"
|
||||
/>
|
||||
@ -1444,169 +1261,26 @@ export function UserSettings() {
|
||||
</TabsContent>
|
||||
|
||||
{/* Финансовые данные */}
|
||||
{(user?.organization?.type === "FULFILLMENT" ||
|
||||
user?.organization?.type === "LOGIST" ||
|
||||
user?.organization?.type === "WHOLESALE" ||
|
||||
user?.organization?.type === "SELLER") && (
|
||||
<TabsContent
|
||||
value="financial"
|
||||
className="flex-1 overflow-hidden"
|
||||
>
|
||||
{(user?.organization?.type === 'FULFILLMENT' ||
|
||||
user?.organization?.type === 'LOGIST' ||
|
||||
user?.organization?.type === 'WHOLESALE' ||
|
||||
user?.organization?.type === 'SELLER') && (
|
||||
<TabsContent value="financial" className="flex-1 overflow-hidden">
|
||||
<Card className="glass-card p-6 h-full overflow-auto">
|
||||
{/* Заголовок вкладки с кнопками */}
|
||||
<div className="flex items-center justify-between mb-6 pb-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-4">
|
||||
<CreditCard className="h-6 w-6 text-red-400" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
Финансовые данные
|
||||
</h2>
|
||||
<p className="text-white/70 text-sm">
|
||||
Банковские реквизиты для расчетов
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-white">Финансовые данные</h2>
|
||||
<p className="text-white/70 text-sm">Банковские реквизиты для расчетов</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{formData.bankName &&
|
||||
formData.bik &&
|
||||
formData.accountNumber &&
|
||||
formData.corrAccount && (
|
||||
<div className="flex items-center gap-2 mr-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400" />
|
||||
<span className="text-green-400 text-sm">
|
||||
Заполнено
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsEditing(false)}
|
||||
className="glass-secondary text-white hover:text-white cursor-pointer"
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={hasValidationErrors() || isSaving}
|
||||
className={`glass-button text-white cursor-pointer ${
|
||||
hasValidationErrors() || isSaving
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{isSaving ? "Сохранение..." : "Сохранить"}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="glass-button text-white cursor-pointer"
|
||||
>
|
||||
<Edit3 className="h-4 w-4 mr-2" />
|
||||
Редактировать
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Название банка
|
||||
</Label>
|
||||
<Input
|
||||
value={formData.bankName || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("bankName", e.target.value)
|
||||
}
|
||||
placeholder="ПАО Сбербанк"
|
||||
readOnly={!isEditing}
|
||||
className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
БИК
|
||||
</Label>
|
||||
<Input
|
||||
value={formData.bik || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("bik", e.target.value)
|
||||
}
|
||||
placeholder="044525225"
|
||||
readOnly={!isEditing}
|
||||
className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Корр. счет
|
||||
</Label>
|
||||
<Input
|
||||
value={formData.corrAccount || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("corrAccount", e.target.value)
|
||||
}
|
||||
placeholder="30101810400000000225"
|
||||
readOnly={!isEditing}
|
||||
className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Расчетный счет
|
||||
</Label>
|
||||
<Input
|
||||
value={formData.accountNumber || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("accountNumber", e.target.value)
|
||||
}
|
||||
placeholder="40702810123456789012"
|
||||
readOnly={!isEditing}
|
||||
className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{/* API ключи для селлера */}
|
||||
{user?.organization?.type === "SELLER" && (
|
||||
<TabsContent value="api" className="flex-1 overflow-hidden">
|
||||
<Card className="glass-card p-6 h-full overflow-auto">
|
||||
{/* Заголовок вкладки с кнопками */}
|
||||
<div className="flex items-center justify-between mb-6 pb-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-4">
|
||||
<Key className="h-6 w-6 text-green-400" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
API ключи маркетплейсов
|
||||
</h2>
|
||||
<p className="text-white/70 text-sm">
|
||||
Интеграция с торговыми площадками
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{user?.organization?.apiKeys?.length > 0 && (
|
||||
{formData.bankName && formData.bik && formData.accountNumber && formData.corrAccount && (
|
||||
<div className="flex items-center gap-2 mr-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400" />
|
||||
<span className="text-green-400 text-sm">
|
||||
Настроено
|
||||
</span>
|
||||
<span className="text-green-400 text-sm">Заполнено</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1625,13 +1299,11 @@ export function UserSettings() {
|
||||
onClick={handleSave}
|
||||
disabled={hasValidationErrors() || isSaving}
|
||||
className={`glass-button text-white cursor-pointer ${
|
||||
hasValidationErrors() || isSaving
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: ""
|
||||
hasValidationErrors() || isSaving ? 'opacity-50 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{isSaving ? "Сохранение..." : "Сохранить"}
|
||||
{isSaving ? 'Сохранение...' : 'Сохранить'}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
@ -1649,72 +1321,156 @@ export function UserSettings() {
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Wildberries API
|
||||
</Label>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Название банка</Label>
|
||||
<Input
|
||||
value={formData.bankName || ''}
|
||||
onChange={(e) => handleInputChange('bankName', e.target.value)}
|
||||
placeholder="ПАО Сбербанк"
|
||||
readOnly={!isEditing}
|
||||
className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">БИК</Label>
|
||||
<Input
|
||||
value={formData.bik || ''}
|
||||
onChange={(e) => handleInputChange('bik', e.target.value)}
|
||||
placeholder="044525225"
|
||||
readOnly={!isEditing}
|
||||
className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Корр. счет</Label>
|
||||
<Input
|
||||
value={formData.corrAccount || ''}
|
||||
onChange={(e) => handleInputChange('corrAccount', e.target.value)}
|
||||
placeholder="30101810400000000225"
|
||||
readOnly={!isEditing}
|
||||
className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Расчетный счет</Label>
|
||||
<Input
|
||||
value={formData.accountNumber || ''}
|
||||
onChange={(e) => handleInputChange('accountNumber', e.target.value)}
|
||||
placeholder="40702810123456789012"
|
||||
readOnly={!isEditing}
|
||||
className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{/* API ключи для селлера */}
|
||||
{user?.organization?.type === 'SELLER' && (
|
||||
<TabsContent value="api" className="flex-1 overflow-hidden">
|
||||
<Card className="glass-card p-6 h-full overflow-auto">
|
||||
{/* Заголовок вкладки с кнопками */}
|
||||
<div className="flex items-center justify-between mb-6 pb-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-4">
|
||||
<Key className="h-6 w-6 text-green-400" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">API ключи маркетплейсов</h2>
|
||||
<p className="text-white/70 text-sm">Интеграция с торговыми площадками</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{user?.organization?.apiKeys?.length > 0 && (
|
||||
<div className="flex items-center gap-2 mr-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-400" />
|
||||
<span className="text-green-400 text-sm">Настроено</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsEditing(false)}
|
||||
className="glass-secondary text-white hover:text-white cursor-pointer"
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={hasValidationErrors() || isSaving}
|
||||
className={`glass-button text-white cursor-pointer ${
|
||||
hasValidationErrors() || isSaving ? 'opacity-50 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{isSaving ? 'Сохранение...' : 'Сохранить'}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="glass-button text-white cursor-pointer"
|
||||
>
|
||||
<Edit3 className="h-4 w-4 mr-2" />
|
||||
Редактировать
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Wildberries API</Label>
|
||||
<Input
|
||||
value={
|
||||
isEditing
|
||||
? formData.wildberriesApiKey || ""
|
||||
: user?.organization?.apiKeys?.find(
|
||||
(key) => key.marketplace === "WILDBERRIES"
|
||||
)
|
||||
? "••••••••••••••••••••"
|
||||
: ""
|
||||
}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"wildberriesApiKey",
|
||||
e.target.value
|
||||
)
|
||||
? formData.wildberriesApiKey || ''
|
||||
: user?.organization?.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES')
|
||||
? '••••••••••••••••••••'
|
||||
: ''
|
||||
}
|
||||
onChange={(e) => handleInputChange('wildberriesApiKey', e.target.value)}
|
||||
placeholder="Введите API ключ Wildberries"
|
||||
readOnly={!isEditing}
|
||||
className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70"
|
||||
/>
|
||||
{(user?.organization?.apiKeys?.find(
|
||||
(key) => key.marketplace === "WILDBERRIES"
|
||||
) ||
|
||||
{(user?.organization?.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES') ||
|
||||
(formData.wildberriesApiKey && isEditing)) && (
|
||||
<p className="text-green-400 text-sm mt-2 flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
{!isEditing
|
||||
? "API ключ настроен"
|
||||
: "Будет сохранен"}
|
||||
{!isEditing ? 'API ключ настроен' : 'Будет сохранен'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">
|
||||
Ozon API
|
||||
</Label>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Ozon API</Label>
|
||||
<Input
|
||||
value={
|
||||
isEditing
|
||||
? formData.ozonApiKey || ""
|
||||
: user?.organization?.apiKeys?.find(
|
||||
(key) => key.marketplace === "OZON"
|
||||
)
|
||||
? "••••••••••••••••••••"
|
||||
: ""
|
||||
}
|
||||
onChange={(e) =>
|
||||
handleInputChange("ozonApiKey", e.target.value)
|
||||
? formData.ozonApiKey || ''
|
||||
: user?.organization?.apiKeys?.find((key) => key.marketplace === 'OZON')
|
||||
? '••••••••••••••••••••'
|
||||
: ''
|
||||
}
|
||||
onChange={(e) => handleInputChange('ozonApiKey', e.target.value)}
|
||||
placeholder="Введите API ключ Ozon"
|
||||
readOnly={!isEditing}
|
||||
className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70"
|
||||
/>
|
||||
{(user?.organization?.apiKeys?.find(
|
||||
(key) => key.marketplace === "OZON"
|
||||
) ||
|
||||
{(user?.organization?.apiKeys?.find((key) => key.marketplace === 'OZON') ||
|
||||
(formData.ozonApiKey && isEditing)) && (
|
||||
<p className="text-green-400 text-sm mt-2 flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
{!isEditing
|
||||
? "API ключ настроен"
|
||||
: "Будет сохранен"}
|
||||
{!isEditing ? 'API ключ настроен' : 'Будет сохранен'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@ -1731,28 +1487,21 @@ export function UserSettings() {
|
||||
<div className="flex items-center gap-4">
|
||||
<Settings className="h-6 w-6 text-green-400" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
Инструменты
|
||||
</h2>
|
||||
<p className="text-white/70 text-sm">
|
||||
Дополнительные возможности для бизнеса
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold text-white">Инструменты</h2>
|
||||
<p className="text-white/70 text-sm">Дополнительные возможности для бизнеса</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(user?.organization?.type === "FULFILLMENT" ||
|
||||
user?.organization?.type === "LOGIST" ||
|
||||
user?.organization?.type === "WHOLESALE") && (
|
||||
{(user?.organization?.type === 'FULFILLMENT' ||
|
||||
user?.organization?.type === 'LOGIST' ||
|
||||
user?.organization?.type === 'WHOLESALE') && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="text-white font-medium mb-2">
|
||||
Партнерская программа
|
||||
</h4>
|
||||
<h4 className="text-white font-medium mb-2">Партнерская программа</h4>
|
||||
<p className="text-white/70 text-sm mb-4">
|
||||
Приглашайте новых контрагентов по уникальной ссылке.
|
||||
При регистрации они автоматически становятся вашими
|
||||
партнерами.
|
||||
Приглашайте новых контрагентов по уникальной ссылке. При регистрации они автоматически
|
||||
становятся вашими партнерами.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
@ -1764,14 +1513,8 @@ export function UserSettings() {
|
||||
onClick={generatePartnerLink}
|
||||
disabled={isGenerating}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-3 w-3 mr-1 ${
|
||||
isGenerating ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
{isGenerating
|
||||
? "Генерируем..."
|
||||
: "Сгенерировать ссылку"}
|
||||
<RefreshCw className={`h-3 w-3 mr-1 ${isGenerating ? 'animate-spin' : ''}`} />
|
||||
{isGenerating ? 'Генерируем...' : 'Сгенерировать ссылку'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -1797,8 +1540,7 @@ export function UserSettings() {
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-white/60 text-xs">
|
||||
Ваша партнерская ссылка сгенерирована и готова к
|
||||
использованию
|
||||
Ваша партнерская ссылка сгенерирована и готова к использованию
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@ -1813,5 +1555,5 @@ export function UserSettings() {
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user