Добавлены настройки для удаленных изображений в конфигурацию Next.js. Удалены неиспользуемые заголовки и улучшен интерфейс компонентов панели управления и мессенджера, включая добавление новых заголовков с кнопками и сокращенных названий компаний. Оптимизированы стили и структура кода в компонентах.

This commit is contained in:
Bivekich
2025-07-17 12:26:08 +03:00
parent 44867c8aee
commit abd0df1fe7
10 changed files with 375 additions and 124 deletions

37
deploy.sh Normal file
View File

@ -0,0 +1,37 @@
#!/bin/bash
echo "🚀 Starting deployment to new.sferav.ru..."
# Остановка предыдущей версии
echo "⏹️ Stopping previous version..."
docker-compose -f docker-compose.prod.yml down
# Очистка неиспользуемых образов
echo "🧹 Cleaning up unused images..."
docker image prune -f
# Сборка и запуск новой версии
echo "🔨 Building and starting new version..."
docker-compose -f docker-compose.prod.yml up -d --build
# Ожидание запуска
echo "⏳ Waiting for application to start..."
sleep 10
# Проверка здоровья
echo "🏥 Checking application health..."
for i in {1..30}; do
if curl -f http://127.0.0.1:3017/api/health > /dev/null 2>&1; then
echo "✅ Application is healthy!"
break
fi
echo "⏳ Attempt $i/30 - waiting for health check..."
sleep 2
done
# Проверка статуса контейнера
echo "📊 Container status:"
docker-compose -f docker-compose.prod.yml ps
echo "🎉 Deployment completed!"
echo "🌐 Application is available at: https://new.sferav.ru"

36
docker-compose.prod.yml Normal file
View File

@ -0,0 +1,36 @@
services:
app:
build:
context: .
args:
- DATABASE_URL=${DATABASE_URL}
- SMS_AERO_EMAIL=${SMS_AERO_EMAIL}
- SMS_AERO_API_KEY=${SMS_AERO_API_KEY}
- SMS_AERO_API_URL=${SMS_AERO_API_URL}
- DADATA_API_KEY=${DADATA_API_KEY}
- DADATA_API_URL=${DADATA_API_URL}
- WILDBERRIES_API_URL=${WILDBERRIES_API_URL}
- OZON_API_URL=${OZON_API_URL}
- JWT_SECRET=${JWT_SECRET}
- SMS_DEV_MODE=${SMS_DEV_MODE}
ports:
- "127.0.0.1:3017:3000" # Привязка только к localhost
env_file:
- .env
environment:
- NODE_ENV=production
- PORT=3000
- HOSTNAME=0.0.0.0
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
timeout: 10s
interval: 30s
retries: 3
start_period: 40s
# Логирование
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

View File

@ -2,6 +2,16 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: 'standalone',
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 's3.twcstorage.ru',
port: '',
pathname: '/**',
},
],
},
};
export default nextConfig;

View File

@ -40,15 +40,6 @@ export function DashboardHome() {
<Sidebar />
<main className="flex-1 ml-64">
<div className="p-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">
Добро пожаловать!
</h1>
<p className="text-white/80">
Главная панель управления {getCabinetType()}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Информация об организации */}
<Card className="bg-white/10 backdrop-blur border-white/20 p-6">

View File

@ -554,62 +554,6 @@ export function UserSettings() {
<Sidebar />
<main className="flex-1 ml-56 px-6 py-4 overflow-hidden">
<div className="h-full w-full flex flex-col">
{/* Заголовок - фиксированная высота */}
<div className="flex items-center justify-between mb-4 flex-shrink-0">
<div>
<h1 className="text-xl font-bold text-white mb-1">Настройки профиля</h1>
<p className="text-white/70 text-sm">Управление информацией о профиле и организации</p>
</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>
{/* Сообщения о сохранении */}
{saveMessage && (
<Alert className={`mb-4 ${saveMessage.type === 'success' ? 'border-green-500 bg-green-500/10' : 'border-red-500 bg-red-500/10'}`}>
@ -658,6 +602,64 @@ export function UserSettings() {
{/* Профиль пользователя */}
<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">
@ -831,12 +833,56 @@ export function UserSettings() {
{/* Организация и юридические данные */}
<TabsContent value="organization" className="flex-1 overflow-hidden">
<Card className="glass-card p-6 h-full overflow-hidden">
<div className="flex items-center gap-3 mb-4">
<Building2 className="h-5 w-5 text-blue-400" />
<h3 className="text-lg font-semibold text-white">Организация и юридические данные</h3>
{(formData.inn || user?.organization?.inn) && (
<CheckCircle className="h-5 w-5 text-green-400 ml-auto" />
)}
{/* Заголовок вкладки с кнопками */}
<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>
{/* Общая подпись про реестр */}
@ -1010,12 +1056,56 @@ export function UserSettings() {
{(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">
<CreditCard className="h-5 w-5 text-red-400" />
<h3 className="text-lg font-semibold text-white">Финансовые данные</h3>
{formData.bankName && formData.bik && formData.accountNumber && formData.corrAccount && (
<CheckCircle className="h-5 w-5 text-green-400 ml-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">
@ -1073,12 +1163,56 @@ export function UserSettings() {
{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 gap-3 mb-6">
<Key className="h-5 w-5 text-green-400" />
<h3 className="text-lg font-semibold text-white">API ключи маркетплейсов</h3>
{user?.organization?.apiKeys?.length > 0 && (
<CheckCircle className="h-5 w-5 text-green-400 ml-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">
@ -1123,9 +1257,15 @@ export function UserSettings() {
{/* Инструменты */}
<TabsContent value="tools" className="flex-1 overflow-hidden">
<Card className="glass-card p-6 h-full overflow-auto">
<div className="flex items-center gap-3 mb-6">
<Key className="h-5 w-5 text-green-400" />
<h3 className="text-lg font-semibold text-white">Инструменты</h3>
{/* Заголовок вкладки */}
<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') && (

View File

@ -15,14 +15,6 @@ export function MarketDashboard() {
<Sidebar />
<main className="flex-1 ml-56 px-6 py-4 overflow-hidden">
<div className="h-full w-full flex flex-col">
{/* Заголовок - фиксированная высота */}
<div className="flex items-center justify-between mb-4 flex-shrink-0">
<div>
<h1 className="text-xl font-bold text-white mb-1">Маркет</h1>
<p className="text-white/70 text-sm">Управление контрагентами и поиск партнеров</p>
</div>
</div>
{/* Основной контент с табами */}
<div className="flex-1 overflow-hidden">
<Tabs defaultValue="counterparties" className="h-full flex flex-col">

View File

@ -122,6 +122,37 @@ export function MessengerChat({ counterparty }: MessengerChatProps) {
return name.charAt(0).toUpperCase()
}
const getShortCompanyName = (fullName: string) => {
if (!fullName) return 'Полное название не указано'
// Словарь для замены полных форм на сокращенные
const legalFormReplacements: { [key: string]: string } = {
'Общество с ограниченной ответственностью': 'ООО',
'Открытое акционерное общество': 'ОАО',
'Закрытое акционерное общество': 'ЗАО',
'Публичное акционерное общество': 'ПАО',
'Непубличное акционерное общество': 'НАО',
'Акционерное общество': 'АО',
'Индивидуальный предприниматель': 'ИП',
'Товарищество с ограниченной ответственностью': 'ТОО',
'Частное предприятие': 'ЧП',
'Субъект предпринимательской деятельности': 'СПД'
}
let result = fullName
// Заменяем полные формы на сокращенные
for (const [fullForm, shortForm] of Object.entries(legalFormReplacements)) {
const regex = new RegExp(`^${fullForm}\\s+`, 'i')
if (regex.test(result)) {
result = result.replace(regex, `${shortForm} `)
break
}
}
return result
}
const getTypeLabel = (type: string) => {
switch (type) {
case 'FULFILLMENT':
@ -273,17 +304,17 @@ export function MessengerChat({ counterparty }: MessengerChatProps) {
</Avatar>
<div>
<h3 className="text-white font-medium">
{getOrganizationName(counterparty)}
</h3>
<p className="text-white/60 text-sm mb-1">
{getManagerName(counterparty)}
</p>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-3">
<h3 className="text-white font-medium">
{getOrganizationName(counterparty)}
</h3>
<Badge className={`${getTypeColor(counterparty.type)} text-xs`}>
{getTypeLabel(counterparty.type)}
</Badge>
</div>
<p className="text-white/60 text-sm">
{getShortCompanyName(counterparty.fullName || '')}
</p>
</div>
</div>

View File

@ -48,6 +48,37 @@ export function MessengerConversations({
return name.charAt(0).toUpperCase()
}
const getShortCompanyName = (fullName: string) => {
if (!fullName) return 'Полное название не указано'
// Словарь для замены полных форм на сокращенные
const legalFormReplacements: { [key: string]: string } = {
'Общество с ограниченной ответственностью': 'ООО',
'Открытое акционерное общество': 'ОАО',
'Закрытое акционерное общество': 'ЗАО',
'Публичное акционерное общество': 'ПАО',
'Непубличное акционерное общество': 'НАО',
'Акционерное общество': 'АО',
'Индивидуальный предприниматель': 'ИП',
'Товарищество с ограниченной ответственностью': 'ТОО',
'Частное предприятие': 'ЧП',
'Субъект предпринимательской деятельности': 'СПД'
}
let result = fullName
// Заменяем полные формы на сокращенные
for (const [fullForm, shortForm] of Object.entries(legalFormReplacements)) {
const regex = new RegExp(`^${fullForm}\\s+`, 'i')
if (regex.test(result)) {
result = result.replace(regex, `${shortForm} `)
break
}
}
return result
}
const getTypeLabel = (type: string) => {
switch (type) {
case 'FULFILLMENT':
@ -154,18 +185,17 @@ export function MessengerConversations({
</Avatar>
<div className="flex-1 min-w-0">
<h4 className="text-white font-medium text-sm leading-tight truncate mb-1">
{getOrganizationName(org)}
</h4>
<div className="flex items-center space-x-2 mb-2">
<Badge className={`${getTypeColor(org.type)} text-xs`}>
<div className="flex items-center justify-between mb-1">
<h4 className="text-white font-medium text-sm leading-tight truncate">
{getOrganizationName(org)}
</h4>
<Badge className={`${getTypeColor(org.type)} text-xs flex-shrink-0 ml-2`}>
{getTypeLabel(org.type)}
</Badge>
</div>
<p className="text-white/60 text-xs truncate">
{getManagerName(org)}
{getShortCompanyName(org.fullName || '')}
</p>
</div>
</div>

View File

@ -65,14 +65,6 @@ export function MessengerDashboard() {
<Sidebar />
<main className="flex-1 ml-56 px-6 py-4 overflow-hidden">
<div className="h-full w-full flex flex-col">
{/* Заголовок - фиксированная высота */}
<div className="flex items-center justify-between mb-4 flex-shrink-0">
<div>
<h1 className="text-xl font-bold text-white mb-1">Мессенджер</h1>
<p className="text-white/70 text-sm">Общение с контрагентами</p>
</div>
</div>
{/* Основной контент - сетка из 2 колонок */}
<div className="flex-1 overflow-hidden">
<div className="grid grid-cols-[350px_1fr] gap-4 h-full">

View File

@ -12,14 +12,6 @@ export function ServicesDashboard() {
<Sidebar />
<main className="flex-1 ml-56 px-6 py-4 overflow-hidden">
<div className="h-full w-full flex flex-col">
{/* Заголовок - фиксированная высота */}
<div className="flex items-center justify-between mb-4 flex-shrink-0">
<div>
<h1 className="text-xl font-bold text-white mb-1">Услуги</h1>
<p className="text-white/70 text-sm">Управление услугами, расходниками и логистикой</p>
</div>
</div>
{/* Основной контент с табами */}
<div className="flex-1 overflow-hidden">
<Tabs defaultValue="services" className="h-full flex flex-col">