Files
sfera/src/components/dashboard/user-settings.tsx

1331 lines
64 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useAuth } from '@/hooks/useAuth'
import { useMutation } from '@apollo/client'
import { UPDATE_USER_PROFILE, UPDATE_ORGANIZATION_BY_INN } from '@/graphql/mutations'
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 {
User,
Building2,
Phone,
Mail,
MapPin,
CreditCard,
Key,
Edit3,
ExternalLink,
Copy,
CheckCircle,
AlertTriangle,
MessageCircle,
Save,
RefreshCw,
Calendar,
Settings,
Camera
} from 'lucide-react'
import { useState, useEffect } from 'react'
import Image from 'next/image'
export function UserSettings() {
const { user } = 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 [formData, setFormData] = useState({
// Контактные данные организации
orgPhone: '', // телефон организации, не пользователя
managerName: '',
telegram: '',
whatsapp: '',
email: '',
// Организация - данные могут быть заполнены из DaData
orgName: '',
address: '',
// Юридические данные - могут быть заполнены из DaData
fullName: '',
inn: '',
ogrn: '',
registrationPlace: '',
// Финансовые данные - требуют ручного заполнения
bankName: '',
bik: '',
accountNumber: '',
corrAccount: '',
// API ключи маркетплейсов
wildberriesApiKey: '',
ozonApiKey: ''
})
// Загружаем данные организации при монтировании компонента
useEffect(() => {
if (user?.organization) {
const org = user.organization
// Извлекаем первый телефон из phones JSON
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)
if (phoneValues.length > 0) {
orgPhone = String(phoneValues[0])
}
}
// Извлекаем email из emails JSON
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)
if (emailValues.length > 0) {
email = String(emailValues[0])
}
}
// Извлекаем дополнительные данные из managementPost (JSON)
let customContacts: {
managerName?: string
telegram?: string
whatsapp?: string
bankDetails?: {
bankName?: string
bik?: string
accountNumber?: string
corrAccount?: string
}
} = {}
try {
if (org.managementPost && typeof org.managementPost === 'string') {
customContacts = JSON.parse(org.managementPost)
}
} catch (e) {
console.warn('Ошибка парсинга managementPost:', e)
}
setFormData({
orgPhone: orgPhone,
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: ''
})
}
}, [user])
const getInitials = () => {
const orgName = user?.organization?.name || user?.organization?.fullName
if (orgName) {
return orgName.charAt(0).toUpperCase()
}
return user?.phone ? user.phone.slice(-2).toUpperCase() : 'О'
}
const getCabinetTypeName = () => {
if (!user?.organization?.type) return 'Не указан'
switch (user.organization.type) {
case 'FULFILLMENT':
return 'Фулфилмент'
case 'SELLER':
return 'Селлер'
case 'LOGIST':
return 'Логистика'
case 'WHOLESALE':
return 'Оптовик'
default:
return 'Не указан'
}
}
// Обновленная функция для проверки заполненности профиля
const checkProfileCompleteness = () => {
// Базовые поля (обязательные для всех)
const baseFields = [
{ field: 'orgPhone', label: 'Телефон организации', value: formData.orgPhone },
{ field: 'managerName', label: 'Имя управляющего', value: formData.managerName },
{ field: 'email', label: 'Email', value: formData.email }
]
// Дополнительные поля в зависимости от типа кабинета
const additionalFields = []
if (user?.organization?.type === 'FULFILLMENT' || user?.organization?.type === 'LOGIST' || user?.organization?.type === 'WHOLESALE' || user?.organization?.type === 'SELLER') {
// Финансовые данные - всегда обязательны для всех типов кабинетов
additionalFields.push(
{ field: 'bankName', label: 'Название банка', value: formData.bankName },
{ field: 'bik', label: 'БИК', value: formData.bik },
{ field: 'accountNumber', label: 'Расчетный счет', value: formData.accountNumber },
{ field: 'corrAccount', label: 'Корр. счет', value: formData.corrAccount }
)
}
const allRequiredFields = [...baseFields, ...additionalFields]
const filledRequiredFields = allRequiredFields.filter(field => field.value && field.value.trim() !== '').length
// Подсчитываем бонусные баллы за автоматически заполненные поля
let autoFilledFields = 0
let totalAutoFields = 0
// Номер телефона пользователя для авторизации (не считаем в процентах заполненности)
// Телефон организации учитывается отдельно как обычное поле
// Данные организации из DaData (если есть ИНН)
if (formData.inn || user?.organization?.inn) {
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.registrationPlace || user?.organization?.registrationDate) {
autoFilledFields += 1
totalAutoFields += 1
}
const totalPossibleFields = allRequiredFields.length + totalAutoFields
const totalFilledFields = filledRequiredFields + autoFilledFields
const percentage = totalPossibleFields > 0 ? Math.round((totalFilledFields / totalPossibleFields) * 100) : 0
const missingFields = allRequiredFields.filter(field => !field.value || field.value.trim() === '').map(field => field.label)
return { percentage, missingFields }
}
const profileStatus = checkProfileCompleteness()
const isIncomplete = profileStatus.percentage < 100
const generatePartnerLink = async () => {
if (!user?.id) return
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}`
setPartnerLink(link)
setSaveMessage({ type: 'success', text: 'Партнерская ссылка сгенерирована!' })
// TODO: Сохранить партнерский код в базе данных
console.log('Partner code generated:', partnerCode)
} catch (error) {
console.error('Error generating partner link:', error)
setSaveMessage({ type: 'error', text: 'Ошибка при генерации ссылки' })
} finally {
setIsGenerating(false)
}
}
const handleCopyLink = async () => {
if (!partnerLink) {
await generatePartnerLink()
return
}
try {
await navigator.clipboard.writeText(partnerLink)
setSaveMessage({ type: 'success', text: 'Ссылка скопирована!' })
} catch (error) {
console.error('Error copying to clipboard:', error)
setSaveMessage({ type: 'error', text: 'Ошибка при копировании' })
}
}
const handleOpenLink = async () => {
if (!partnerLink) {
await generatePartnerLink()
return
}
window.open(partnerLink, '_blank')
}
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file || !user?.id) return
setIsUploadingAvatar(true)
setSaveMessage(null)
try {
const avatarUrl = await S3Service.uploadAvatar(file, user.id)
// Обновляем аватар пользователя через GraphQL
const result = await updateUserProfile({
variables: {
input: {
avatar: avatarUrl
}
}
})
if (result.data?.updateUserProfile?.success) {
setSaveMessage({ type: 'success', text: 'Аватар успешно обновлен! Обновляем страницу...' })
setTimeout(() => {
window.location.reload()
}, 1000)
} else {
throw new Error('Failed to update avatar')
}
} catch (error) {
console.error('Error uploading avatar:', error)
setSaveMessage({ type: 'error', text: 'Ошибка при загрузке аватара' })
} finally {
setIsUploadingAvatar(false)
}
}
// Функции для валидации и масок
const validateEmail = (email: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
const formatPhoneInput = (value: string) => {
const cleaned = value.replace(/\D/g, '')
if (cleaned.length <= 1) return cleaned
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)}`
return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7, 9)}-${cleaned.slice(9, 11)}`
}
const formatTelegram = (value: string) => {
// Убираем все символы кроме букв, цифр, _ и @
let cleaned = value.replace(/[^a-zA-Z0-9_@]/g, '')
// Убираем лишние символы @
cleaned = cleaned.replace(/@+/g, '@')
// Если есть символы после удаления @ и строка не начинается с @, добавляем @
if (cleaned && !cleaned.startsWith('@')) {
cleaned = '@' + cleaned
}
// Ограничиваем длину (максимум 32 символа для Telegram)
if (cleaned.length > 33) {
cleaned = cleaned.substring(0, 33)
}
return cleaned
}
const validateName = (name: string) => {
return /^[а-яёА-ЯЁa-zA-Z\s-]+$/.test(name) && name.trim().length >= 2
}
const handleInputChange = (field: string, value: string) => {
let processedValue = value
// Применяем маски и валидации
switch (field) {
case 'orgPhone':
case 'whatsapp':
processedValue = formatPhoneInput(value)
break
case 'telegram':
processedValue = formatTelegram(value)
break
case 'email':
// Для email не применяем маску, только валидацию при потере фокуса
break
case 'managerName':
// Разрешаем только буквы, пробелы и дефисы
processedValue = value.replace(/[^а-яёА-ЯЁa-zA-Z\s-]/g, '')
break
}
setFormData(prev => ({ ...prev, [field]: processedValue }))
}
// Функции для проверки ошибок
const getFieldError = (field: string, value: string) => {
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':
// Проверяем что после @ есть минимум 5 символов
const usernameLength = value.startsWith('@') ? value.length - 1 : value.length
return usernameLength < 5 ? 'Минимум 5 символов после @' : null
case 'inn':
// Игнорируем автоматически сгенерированные ИНН селлеров
if (value.startsWith('SELLER_')) {
return null
}
const innCleaned = value.replace(/\D/g, '')
if (innCleaned.length !== 10 && innCleaned.length !== 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
default:
return null
}
}
// Проверка наличия ошибок валидации
const hasValidationErrors = () => {
const fields = ['orgPhone', 'managerName', 'telegram', 'whatsapp', 'email', 'inn', 'bankName', 'bik', 'accountNumber', 'corrAccount']
// Проверяем ошибки валидации только в заполненных полях
const hasErrors = fields.some(field => {
const value = formData[field as keyof typeof formData]
// Проверяем ошибки только для заполненных полей
if (!value || !value.trim()) return false
const error = getFieldError(field, value)
return error !== null
})
// Убираем проверку обязательных полей - пользователь может заполнять постепенно
return hasErrors
}
const handleSave = async () => {
// Сброс предыдущих сообщений
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, '')
// Если ИНН изменился и валиден, сначала обновляем данные организации
if (innCleaned !== originalInnCleaned && (innCleaned.length === 10 || innCleaned.length === 12)) {
setSaveMessage({ type: 'success', text: 'Обновляем данные организации...' })
const orgResult = await updateOrganizationByInn({
variables: { inn: innCleaned }
})
if (!orgResult.data?.updateOrganizationByInn?.success) {
setSaveMessage({
type: 'error',
text: orgResult.data?.updateOrganizationByInn?.message || 'Ошибка при обновлении данных организации'
})
return
}
setSaveMessage({ type: 'success', text: 'Данные организации обновлены. Сохраняем профиль...' })
}
// Подготавливаем только заполненные поля для отправки
const inputData: {
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()
const result = await updateUserProfile({
variables: {
input: inputData
}
})
if (result.data?.updateUserProfile?.success) {
setSaveMessage({ type: 'success', text: 'Профиль успешно сохранен! Обновляем страницу...' })
// Простое обновление страницы после успешного сохранения
setTimeout(() => {
window.location.reload()
}, 1000)
} else {
setSaveMessage({
type: 'error',
text: result.data?.updateUserProfile?.message || 'Ошибка при сохранении профиля'
})
}
} catch (error) {
console.error('Error saving profile:', error)
setSaveMessage({ type: 'error', text: 'Ошибка при сохранении профиля' })
}
}
const formatDate = (dateString?: string) => {
if (!dateString) return ''
try {
let date: Date
// Проверяем, является ли строка числом (Unix timestamp)
if (/^\d+$/.test(dateString)) {
// Если это Unix timestamp в миллисекундах
const timestamp = parseInt(dateString, 10)
date = new Date(timestamp)
} else {
// Обычная строка даты
date = new Date(dateString)
}
if (isNaN(date.getTime())) {
console.warn('Invalid date string:', dateString)
return 'Неверная дата'
}
return date.toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
} catch (error) {
console.error('Error formatting date:', error, dateString)
return 'Ошибка даты'
}
}
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className="flex-1 ml-56 px-6 py-4 overflow-hidden">
<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'}`}>
<AlertDescription className={saveMessage.type === 'success' ? 'text-green-400' : 'text-red-400'}>
{saveMessage.text}
</AlertDescription>
</Alert>
)}
{/* Основной контент с вкладками - заполняет оставшееся пространство */}
<div className="flex-1 overflow-hidden">
<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'
}`}>
<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">
<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">
<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">
<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">
<Settings className="h-4 w-4 mr-2" />
Инструменты
</TabsTrigger>
)}
</TabsList>
{/* Профиль пользователя */}
<TabsContent value="profile" 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">
<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>
</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>
</div>
<div className="hidden sm:block text-xs text-white/70">
{isIncomplete ? (
<>Заполнено {profileStatus.percentage}% профиля</>
) : (
<>Профиль полностью заполнен</>
)}
</div>
</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="flex items-center gap-4 mb-6">
<div className="relative">
<Avatar className="h-16 w-16">
{user?.avatar ? (
<Image
src={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>
)}
</Avatar>
<div className="absolute -bottom-1 -right-1">
<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" />
) : (
<Camera className="h-3 w-3 text-white" />
)}
</div>
</label>
<input
id="avatar-upload"
type="file"
accept="image/*"
onChange={handleAvatarUpload}
className="hidden"
disabled={isUploadingAvatar}
/>
</div>
</div>
<div className="flex-1">
<p className="text-white font-medium text-lg">
{user?.organization?.name || user?.organization?.fullName || 'Пользователь'}
</p>
<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 || '')}
</p>
{user?.createdAt && (
<p className="text-white/50 text-xs mt-1 flex items-center gap-1">
<Calendar className="h-3 w-3" />
Дата регистрации: {formatDate(user.createdAt)}
</p>
)}
</div>
<CheckCircle className="h-5 w-5 text-green-400 ml-auto" />
</div>
<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>
<Input
value={formData.orgPhone || ''}
onChange={(e) => handleInputChange('orgPhone', e.target.value)}
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) && (
<p className="text-red-400 text-xs mt-1 flex items-center gap-1">
<AlertTriangle className="h-3 w-3" />
{getFieldError('orgPhone', formData.orgPhone)}
</p>
)}
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Имя управляющего</Label>
<Input
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) && (
<p className="text-red-400 text-xs mt-1 flex items-center gap-1">
<AlertTriangle className="h-3 w-3" />
{getFieldError('managerName', formData.managerName)}
</p>
)}
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<Label className="text-white/80 text-sm mb-2 flex items-center gap-2">
<MessageCircle className="h-4 w-4 text-blue-400" />
Telegram
</Label>
<Input
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) && (
<p className="text-red-400 text-xs mt-1 flex items-center gap-1">
<AlertTriangle className="h-3 w-3" />
{getFieldError('telegram', formData.telegram)}
</p>
)}
</div>
<div>
<Label className="text-white/80 text-sm mb-2 flex items-center gap-2">
<Phone className="h-4 w-4 text-green-400" />
WhatsApp
</Label>
<Input
value={formData.whatsapp || ''}
onChange={(e) => handleInputChange('whatsapp', e.target.value)}
placeholder="+7 (999) 999-99-99"
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) && (
<p className="text-red-400 text-xs mt-1 flex items-center gap-1">
<AlertTriangle className="h-3 w-3" />
{getFieldError('whatsapp', formData.whatsapp)}
</p>
)}
</div>
<div>
<Label className="text-white/80 text-sm mb-2 flex items-center gap-2">
<Mail className="h-4 w-4 text-red-400" />
Email
</Label>
<Input
type="email"
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) && (
<p className="text-red-400 text-xs mt-1 flex items-center gap-1">
<AlertTriangle className="h-3 w-3" />
{getFieldError('email', formData.email)}
</p>
)}
</div>
</div>
</div>
</Card>
</TabsContent>
{/* Организация и юридические данные */}
<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>
</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>
</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="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>
<div className="space-y-4">
{/* Названия */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-white/80 text-sm mb-2 block">
{user?.organization?.type === 'SELLER' ? 'Название магазина' : 'Название организации'}
</Label>
<Input
value={formData.orgName || user?.organization?.name || ''}
onChange={(e) => handleInputChange('orgName', e.target.value)}
placeholder={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' ? (
<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>
<Input
value={formData.fullName || user?.organization?.fullName || ''}
readOnly
className="glass-input text-white h-10 read-only:opacity-70"
/>
</div>
</div>
{/* Адреса */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-white/80 text-sm mb-2 flex items-center gap-2">
<MapPin className="h-4 w-4" />
Адрес
</Label>
<Input
value={formData.address || user?.organization?.address || ''}
onChange={(e) => handleInputChange('address', e.target.value)}
placeholder="г. Москва, ул. Примерная, д. 1"
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>
<Input
value={user?.organization?.addressFull || ''}
readOnly
className="glass-input text-white h-10 read-only:opacity-70"
/>
</div>
</div>
{/* ИНН, ОГРН, КПП */}
<div className="grid grid-cols-3 gap-4">
<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" />
)}
</Label>
<Input
value={formData.inn || user?.organization?.inn || ''}
onChange={(e) => {
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' : ''
} ${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>
)}
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">ОГРН</Label>
<Input
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>
<Input
value={user?.organization?.kpp || ''}
readOnly
className="glass-input text-white h-10 read-only:opacity-70"
/>
</div>
</div>
{/* Руководитель и статус */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-white/80 text-sm mb-2 block">Руководитель организации</Label>
<Input
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>
<Input
value={user?.organization?.status === 'ACTIVE' ? 'Действующая' : user?.organization?.status || 'Статус не указан'}
readOnly
className="glass-input text-white h-10 read-only:opacity-70"
/>
</div>
</div>
{/* Дата регистрации */}
{user?.organization?.registrationDate && (
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-white/80 text-sm mb-2 flex items-center gap-2">
<Calendar className="h-4 w-4" />
Дата регистрации
</Label>
<Input
value={formatDate(user.organization.registrationDate)}
readOnly
className="glass-input text-white h-10 read-only:opacity-70"
/>
</div>
</div>
)}
</div>
</Card>
</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">
<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>
</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 && (
<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)}
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') || (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 ключ настроен' : 'Будет сохранен'}
</p>
)}
</div>
<div>
<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)}
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') || (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 ключ настроен' : 'Будет сохранен'}
</p>
)}
</div>
</div>
</Card>
</TabsContent>
)}
{/* Инструменты */}
<TabsContent value="tools" 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">
<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>
</div>
</div>
</div>
{(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>
<p className="text-white/70 text-sm mb-4">
Приглашайте новых контрагентов по уникальной ссылке. При регистрации они автоматически становятся вашими партнерами.
</p>
<div className="space-y-3">
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
className="glass-secondary text-white hover:text-white cursor-pointer text-xs px-3 py-2"
onClick={generatePartnerLink}
disabled={isGenerating}
>
<RefreshCw className={`h-3 w-3 mr-1 ${isGenerating ? 'animate-spin' : ''}`} />
{isGenerating ? 'Генерируем...' : 'Сгенерировать ссылку'}
</Button>
</div>
{partnerLink && (
<div className="space-y-2">
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
className="glass-secondary text-white hover:text-white cursor-pointer text-xs px-3 py-2"
onClick={handleOpenLink}
>
<ExternalLink className="h-3 w-3 mr-1" />
Открыть ссылку
</Button>
<Button
size="sm"
variant="outline"
className="glass-secondary text-white hover:text-white cursor-pointer px-2"
onClick={handleCopyLink}
>
<Copy className="h-3 w-3" />
</Button>
</div>
<p className="text-white/60 text-xs">
Ваша партнерская ссылка сгенерирована и готова к использованию
</p>
</div>
)}
</div>
</div>
</div>
)}
</Card>
</TabsContent>
</Tabs>
</div>
</div>
</main>
</div>
)
}