Добавлены новые зависимости для работы с эмодзи и улучшена структура базы данных. Реализована модель сообщений и обновлены компоненты для поддержки новых функций мессенджера. Обновлены запросы и мутации для работы с сообщениями и чатом.
This commit is contained in:
@ -70,7 +70,11 @@ export function UserSettings() {
|
||||
bankName: '',
|
||||
bik: '',
|
||||
accountNumber: '',
|
||||
corrAccount: ''
|
||||
corrAccount: '',
|
||||
|
||||
// API ключи маркетплейсов
|
||||
wildberriesApiKey: '',
|
||||
ozonApiKey: ''
|
||||
})
|
||||
|
||||
// Загружаем данные организации при монтировании компонента
|
||||
@ -122,7 +126,7 @@ export function UserSettings() {
|
||||
|
||||
setFormData({
|
||||
orgPhone: orgPhone,
|
||||
managerName: customContacts?.managerName || '',
|
||||
managerName: user?.managerName || '',
|
||||
telegram: customContacts?.telegram || '',
|
||||
whatsapp: customContacts?.whatsapp || '',
|
||||
email: email,
|
||||
@ -135,7 +139,9 @@ export function UserSettings() {
|
||||
bankName: customContacts?.bankDetails?.bankName || '',
|
||||
bik: customContacts?.bankDetails?.bik || '',
|
||||
accountNumber: customContacts?.bankDetails?.accountNumber || '',
|
||||
corrAccount: customContacts?.bankDetails?.corrAccount || ''
|
||||
corrAccount: customContacts?.bankDetails?.corrAccount || '',
|
||||
wildberriesApiKey: '',
|
||||
ozonApiKey: ''
|
||||
})
|
||||
}
|
||||
}, [user])
|
||||
@ -176,8 +182,8 @@ export function UserSettings() {
|
||||
|
||||
// Дополнительные поля в зависимости от типа кабинета
|
||||
const additionalFields = []
|
||||
if (user?.organization?.type === 'FULFILLMENT' || user?.organization?.type === 'LOGIST' || user?.organization?.type === 'WHOLESALE') {
|
||||
// Финансовые данные - всегда обязательны для бизнес-кабинетов
|
||||
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 },
|
||||
@ -386,13 +392,30 @@ export function UserSettings() {
|
||||
const cleaned = value.replace(/\D/g, '')
|
||||
return cleaned.length !== 11 ? 'Неверный формат телефона' : null
|
||||
case 'telegram':
|
||||
return value.length < 6 ? 'Минимум 5 символов после @' : null
|
||||
// Проверяем что после @ есть минимум 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
|
||||
}
|
||||
@ -400,11 +423,20 @@ export function UserSettings() {
|
||||
|
||||
// Проверка наличия ошибок валидации
|
||||
const hasValidationErrors = () => {
|
||||
const fields = ['orgPhone', 'managerName', 'telegram', 'whatsapp', 'email', 'inn']
|
||||
return fields.some(field => {
|
||||
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]
|
||||
return getFieldError(field, value)
|
||||
// Проверяем ошибки только для заполненных полей
|
||||
if (!value || !value.trim()) return false
|
||||
|
||||
const error = getFieldError(field, value)
|
||||
return error !== null
|
||||
})
|
||||
|
||||
// Убираем проверку обязательных полей - пользователь может заполнять постепенно
|
||||
return hasErrors
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
@ -437,19 +469,33 @@ export function UserSettings() {
|
||||
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: {
|
||||
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
|
||||
}
|
||||
input: inputData
|
||||
}
|
||||
})
|
||||
|
||||
@ -516,19 +562,18 @@ export function UserSettings() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Компактный индикатор прогресса */}
|
||||
{isIncomplete && (
|
||||
<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">
|
||||
Осталось {profileStatus.missingFields.length} {
|
||||
profileStatus.missingFields.length === 1 ? 'поле' :
|
||||
profileStatus.missingFields.length < 5 ? 'поля' : 'полей'
|
||||
}
|
||||
</div>
|
||||
<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 ? (
|
||||
<>
|
||||
@ -590,10 +635,10 @@ export function UserSettings() {
|
||||
<Building2 className="h-4 w-4 mr-2" />
|
||||
Организация
|
||||
</TabsTrigger>
|
||||
{(user?.organization?.type === 'FULFILLMENT' || user?.organization?.type === 'LOGIST' || user?.organization?.type === 'WHOLESALE') && (
|
||||
{(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' && (
|
||||
@ -602,10 +647,12 @@ export function UserSettings() {
|
||||
API
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="tools" className="text-white data-[state=active]:bg-white/20 cursor-pointer">
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
Инструменты
|
||||
</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>
|
||||
|
||||
{/* Профиль пользователя */}
|
||||
@ -671,7 +718,7 @@ export function UserSettings() {
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Номер телефона организации</Label>
|
||||
<Input
|
||||
value={formData.orgPhone}
|
||||
value={formData.orgPhone || ''}
|
||||
onChange={(e) => handleInputChange('orgPhone', e.target.value)}
|
||||
placeholder="+7 (999) 999-99-99"
|
||||
readOnly={!isEditing}
|
||||
@ -679,23 +726,18 @@ export function UserSettings() {
|
||||
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)}
|
||||
</p>
|
||||
) : !formData.orgPhone && (
|
||||
<p className="text-orange-400 text-xs mt-1 flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
Рекомендуется указать
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Имя управляющего</Label>
|
||||
<Input
|
||||
value={formData.managerName}
|
||||
value={formData.managerName || ''}
|
||||
onChange={(e) => handleInputChange('managerName', e.target.value)}
|
||||
placeholder="Иван Иванов"
|
||||
readOnly={!isEditing}
|
||||
@ -719,7 +761,7 @@ export function UserSettings() {
|
||||
Telegram
|
||||
</Label>
|
||||
<Input
|
||||
value={formData.telegram}
|
||||
value={formData.telegram || ''}
|
||||
onChange={(e) => handleInputChange('telegram', e.target.value)}
|
||||
placeholder="@username"
|
||||
readOnly={!isEditing}
|
||||
@ -741,7 +783,7 @@ export function UserSettings() {
|
||||
WhatsApp
|
||||
</Label>
|
||||
<Input
|
||||
value={formData.whatsapp}
|
||||
value={formData.whatsapp || ''}
|
||||
onChange={(e) => handleInputChange('whatsapp', e.target.value)}
|
||||
placeholder="+7 (999) 999-99-99"
|
||||
readOnly={!isEditing}
|
||||
@ -764,7 +806,7 @@ export function UserSettings() {
|
||||
</Label>
|
||||
<Input
|
||||
type="email"
|
||||
value={formData.email}
|
||||
value={formData.email || ''}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
placeholder="example@company.com"
|
||||
readOnly={!isEditing}
|
||||
@ -807,14 +849,25 @@ 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">
|
||||
{user?.organization?.type === 'SELLER' ? 'Название магазина' : 'Название организации'}
|
||||
</Label>
|
||||
<Input
|
||||
value={formData.orgName || user?.organization?.name || ''}
|
||||
onChange={(e) => handleInputChange('orgName', e.target.value)}
|
||||
placeholder="Название организации"
|
||||
readOnly={!isEditing || !!(formData.orgName || user?.organization?.name)}
|
||||
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>
|
||||
@ -904,27 +957,29 @@ export function UserSettings() {
|
||||
|
||||
{/* Руководитель и статус */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{user?.organization?.managementName && (
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
|
||||
{user?.organization?.status && (
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{/* Дата регистрации */}
|
||||
@ -950,7 +1005,7 @@ export function UserSettings() {
|
||||
|
||||
|
||||
{/* Финансовые данные */}
|
||||
{(user?.organization?.type === 'FULFILLMENT' || user?.organization?.type === 'LOGIST' || user?.organization?.type === 'WHOLESALE') && (
|
||||
{(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 gap-3 mb-6">
|
||||
@ -965,7 +1020,7 @@ export function UserSettings() {
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Название банка</Label>
|
||||
<Input
|
||||
value={formData.bankName}
|
||||
value={formData.bankName || ''}
|
||||
onChange={(e) => handleInputChange('bankName', e.target.value)}
|
||||
placeholder="ПАО Сбербанк"
|
||||
readOnly={!isEditing}
|
||||
@ -977,7 +1032,7 @@ export function UserSettings() {
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">БИК</Label>
|
||||
<Input
|
||||
value={formData.bik}
|
||||
value={formData.bik || ''}
|
||||
onChange={(e) => handleInputChange('bik', e.target.value)}
|
||||
placeholder="044525225"
|
||||
readOnly={!isEditing}
|
||||
@ -988,7 +1043,7 @@ export function UserSettings() {
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Корр. счет</Label>
|
||||
<Input
|
||||
value={formData.corrAccount}
|
||||
value={formData.corrAccount || ''}
|
||||
onChange={(e) => handleInputChange('corrAccount', e.target.value)}
|
||||
placeholder="30101810400000000225"
|
||||
readOnly={!isEditing}
|
||||
@ -1000,7 +1055,7 @@ export function UserSettings() {
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Расчетный счет</Label>
|
||||
<Input
|
||||
value={formData.accountNumber}
|
||||
value={formData.accountNumber || ''}
|
||||
onChange={(e) => handleInputChange('accountNumber', e.target.value)}
|
||||
placeholder="40702810123456789012"
|
||||
readOnly={!isEditing}
|
||||
@ -1028,14 +1083,16 @@ export function UserSettings() {
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Wildberries API</Label>
|
||||
<Input
|
||||
value={user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES') ? '••••••••••••••••••••' : ''}
|
||||
readOnly
|
||||
className="glass-input text-white h-10 read-only:opacity-70"
|
||||
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') && (
|
||||
{(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" />
|
||||
API ключ настроен
|
||||
{!isEditing ? 'API ключ настроен' : 'Будет сохранен'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@ -1043,14 +1100,16 @@ export function UserSettings() {
|
||||
<div>
|
||||
<Label className="text-white/80 text-sm mb-2 block">Ozon API</Label>
|
||||
<Input
|
||||
value={user?.organization?.apiKeys?.find(key => key.marketplace === 'OZON') ? '••••••••••••••••••••' : ''}
|
||||
readOnly
|
||||
className="glass-input text-white h-10 read-only:opacity-70"
|
||||
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') && (
|
||||
{(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" />
|
||||
API ключ настроен
|
||||
{!isEditing ? 'API ключ настроен' : 'Будет сохранен'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user