"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, Upload, Camera } from 'lucide-react' import { useState, useEffect } from 'react' 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: '' }) // Загружаем данные организации при монтировании компонента 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: customContacts?.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 || '' }) } }, [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') { // Финансовые данные - всегда обязательны для бизнес-кабинетов 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) => { 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': return value.length < 6 ? 'Минимум 5 символов после @' : null case 'inn': const innCleaned = value.replace(/\D/g, '') if (innCleaned.length !== 10 && innCleaned.length !== 12) { return 'ИНН должен содержать 10 или 12 цифр' } return null default: return null } } // Проверка наличия ошибок валидации const hasValidationErrors = () => { const fields = ['orgPhone', 'managerName', 'telegram', 'whatsapp', 'email', 'inn'] return fields.some(field => { const value = formData[field as keyof typeof formData] return getFieldError(field, value) }) } 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 result = await updateUserProfile({ variables: { input: { orgPhone: formData.orgPhone, managerName: formData.managerName, telegram: formData.telegram, whatsapp: formData.whatsapp, email: formData.email, bankName: formData.bankName, bik: formData.bik, accountNumber: formData.accountNumber, corrAccount: formData.corrAccount } } }) 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 (
{/* Заголовок - фиксированная высота */}

Настройки профиля

Управление информацией о профиле и организации

{/* Компактный индикатор прогресса */} {isIncomplete && (
{profileStatus.percentage}%
Осталось {profileStatus.missingFields.length} { profileStatus.missingFields.length === 1 ? 'поле' : profileStatus.missingFields.length < 5 ? 'поля' : 'полей' }
)} {isEditing ? ( <> ) : ( )}
{/* Сообщения о сохранении */} {saveMessage && ( {saveMessage.text} )} {/* Основной контент с вкладками - заполняет оставшееся пространство */}
Профиль Организация {(user?.organization?.type === 'FULFILLMENT' || user?.organization?.type === 'LOGIST' || user?.organization?.type === 'WHOLESALE') && ( Финансовые )} {user?.organization?.type === 'SELLER' && ( API )} Инструменты {/* Профиль пользователя */}
{user?.avatar ? ( Аватар ) : ( {getInitials()} )}

{user?.organization?.name || user?.organization?.fullName || 'Пользователь'}

{getCabinetTypeName()}

Авторизован по номеру: {formatPhone(user?.phone || '')}

{user?.createdAt && (

Дата регистрации: {formatDate(user.createdAt)}

)}
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) ? (

{getFieldError('orgPhone', formData.orgPhone)}

) : !formData.orgPhone && (

Рекомендуется указать

)}
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) && (

{getFieldError('managerName', formData.managerName)}

)}
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) && (

{getFieldError('telegram', formData.telegram)}

)}
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) && (

{getFieldError('whatsapp', formData.whatsapp)}

)}
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) && (

{getFieldError('email', formData.email)}

)}
{/* Организация и юридические данные */}

Организация и юридические данные

{(formData.inn || user?.organization?.inn) && ( )}
{/* Общая подпись про реестр */}

При сохранении с измененным ИНН мы автоматически обновляем все остальные данные из федерального реестра

{/* Названия */}
handleInputChange('orgName', e.target.value)} placeholder="Название организации" readOnly={!isEditing || !!(formData.orgName || user?.organization?.name)} className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70" />
{/* Адреса */}
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" />
{/* ИНН, ОГРН, КПП */}
{ 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) && (

{getFieldError('inn', formData.inn)}

)}
{/* Руководитель и статус */}
{user?.organization?.managementName && (
)} {user?.organization?.status && (
)}
{/* Дата регистрации */} {user?.organization?.registrationDate && (
)}
{/* Финансовые данные */} {(user?.organization?.type === 'FULFILLMENT' || user?.organization?.type === 'LOGIST' || user?.organization?.type === 'WHOLESALE') && (

Финансовые данные

{formData.bankName && formData.bik && formData.accountNumber && formData.corrAccount && ( )}
handleInputChange('bankName', e.target.value)} placeholder="ПАО Сбербанк" readOnly={!isEditing} className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70" />
handleInputChange('bik', e.target.value)} placeholder="044525225" readOnly={!isEditing} className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70" />
handleInputChange('corrAccount', e.target.value)} placeholder="30101810400000000225" readOnly={!isEditing} className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70" />
handleInputChange('accountNumber', e.target.value)} placeholder="40702810123456789012" readOnly={!isEditing} className="glass-input text-white placeholder:text-white/40 h-10 read-only:opacity-70" />
)} {/* API ключи для селлера */} {user?.organization?.type === 'SELLER' && (

API ключи маркетплейсов

{user?.organization?.apiKeys?.length > 0 && ( )}
key.marketplace === 'WILDBERRIES') ? '••••••••••••••••••••' : ''} readOnly className="glass-input text-white h-10 read-only:opacity-70" /> {user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES') && (

API ключ настроен

)}
key.marketplace === 'OZON') ? '••••••••••••••••••••' : ''} readOnly className="glass-input text-white h-10 read-only:opacity-70" /> {user?.organization?.apiKeys?.find(key => key.marketplace === 'OZON') && (

API ключ настроен

)}
)} {/* Инструменты */}

Инструменты

{(user?.organization?.type === 'FULFILLMENT' || user?.organization?.type === 'LOGIST' || user?.organization?.type === 'WHOLESALE') && (

Партнерская программа

Приглашайте новых контрагентов по уникальной ссылке. При регистрации они автоматически становятся вашими партнерами.

{partnerLink && (

Ваша партнерская ссылка сгенерирована и готова к использованию

)}
)}
) }