Добавлены новые зависимости для работы с эмодзи и улучшена структура базы данных. Реализована модель сообщений и обновлены компоненты для поддержки новых функций мессенджера. Обновлены запросы и мутации для работы с сообщениями и чатом.

This commit is contained in:
Bivekich
2025-07-16 22:07:38 +03:00
parent 823ef9a28c
commit 205c9eae98
33 changed files with 3288 additions and 229 deletions

View File

@ -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>