Унификация UI раздела Партнеры и создание системы документирования
🎨 Унификация UI: - Полная унификация визуала вкладок Рефералы и Мои контрагенты - Исправлены React Hooks ошибки в sidebar.tsx - Убрана лишняя обертка glass-card в partners-dashboard.tsx - Исправлена цветовая схема (purple → yellow) - Табличный формат вместо карточного grid-layout - Компактные блоки статистики (4 метрики в ряд) - Правильная прозрачность glass-morphism эффектов 📚 Документация: - Переименован referral-system-rules.md → partners-rules.md - Детальные UI/UX правила в partners-rules.md - Правила унификации в visual-design-rules.md - Обновлен current-session.md - Создан development-diary.md 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -17,6 +17,7 @@ import {
|
||||
X,
|
||||
Copy,
|
||||
Gift,
|
||||
TrendingUp,
|
||||
} from 'lucide-react'
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
@ -37,7 +38,6 @@ import {
|
||||
} from '@/graphql/queries'
|
||||
|
||||
import { OrganizationAvatar } from './organization-avatar'
|
||||
import { OrganizationCard } from './organization-card'
|
||||
|
||||
interface Organization {
|
||||
id: string
|
||||
@ -119,7 +119,7 @@ export function MarketCounterparties() {
|
||||
}
|
||||
await navigator.clipboard.writeText(partnerLink)
|
||||
toast.success('Партнерская ссылка скопирована!', {
|
||||
description: 'Поделитесь ей для прямого делового сотрудничества'
|
||||
description: 'Поделитесь ей для прямого делового сотрудничества',
|
||||
})
|
||||
} catch {
|
||||
toast.error('Не удалось скопировать ссылку')
|
||||
@ -321,34 +321,121 @@ export function MarketCounterparties() {
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="counterparties" className="flex-1 overflow-hidden mt-3 flex flex-col">
|
||||
{/* Блок с партнерской ссылкой */}
|
||||
<Card className="glass-card p-4 mb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-purple-500/20 border border-purple-500/30">
|
||||
<Gift className="h-5 w-5 text-purple-400" />
|
||||
<div className="h-full flex flex-col space-y-4">
|
||||
{/* Компактный блок с партнерской ссылкой */}
|
||||
<Card className="glass-card p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
|
||||
<Gift className="h-4 w-4 text-yellow-400" />
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-white">Партнерская ссылка</h3>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-white font-medium">Пригласить партнера</h3>
|
||||
<p className="text-white/60 text-sm">Прямое деловое сотрудничество с автоматическим добавлением в партнеры</p>
|
||||
<div className="text-xs text-white/60">
|
||||
Прямое деловое сотрудничество с автоматическим добавлением
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={copyPartnerLink}
|
||||
className="glass-button hover:bg-white/20 transition-all duration-200"
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
Копировать ссылку
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 px-3 py-2 glass-input rounded-lg text-white/60 font-mono text-sm truncate">
|
||||
{partnerLinkData?.myPartnerLink || 'http://localhost:3000/register?partner=LOADING'}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={copyPartnerLink}
|
||||
className="glass-button hover:bg-white/20 transition-all duration-200 px-3"
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
Копировать
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Компактная панель фильтров */}
|
||||
<div className="glass-card p-3 mb-3 space-y-3">
|
||||
<div className="flex flex-col xl:flex-row gap-3">
|
||||
{/* Поиск */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="relative">
|
||||
{/* Компактная статистика */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<Card className="glass-card p-3 hover:bg-white/5 transition-all duration-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-blue-500/20 border border-blue-500/30">
|
||||
<Users className="h-4 w-4 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-white/60 uppercase tracking-wide">Партнеров</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
{counterpartiesLoading ? (
|
||||
<span className="inline-block h-6 w-8 bg-white/10 rounded animate-pulse" />
|
||||
) : (
|
||||
counterparties.length
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card p-3 hover:bg-white/5 transition-all duration-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
|
||||
<ArrowDownCircle className="h-4 w-4 text-yellow-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-white/60 uppercase tracking-wide">Заявок</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
{incomingLoading ? (
|
||||
<span className="inline-block h-6 w-8 bg-white/10 rounded animate-pulse" />
|
||||
) : (
|
||||
incomingRequests.length
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card p-3 hover:bg-white/5 transition-all duration-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-green-500/20 border border-green-500/30">
|
||||
<TrendingUp className="h-4 w-4 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-white/60 uppercase tracking-wide">За месяц</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
{counterpartiesLoading ? (
|
||||
<span className="inline-block h-6 w-8 bg-white/10 rounded animate-pulse" />
|
||||
) : (
|
||||
counterparties.filter(org => {
|
||||
const monthAgo = new Date();
|
||||
monthAgo.setMonth(monthAgo.getMonth() - 1);
|
||||
return new Date(org.createdAt) > monthAgo;
|
||||
}).length
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card p-3 hover:bg-white/5 transition-all duration-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
|
||||
<ArrowUpCircle className="h-4 w-4 text-yellow-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-white/60 uppercase tracking-wide">Исходящих</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
{outgoingLoading ? (
|
||||
<span className="inline-block h-6 w-8 bg-white/10 rounded animate-pulse" />
|
||||
) : (
|
||||
outgoingRequests.length
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Компактные фильтры */}
|
||||
<Card className="glass-card p-3">
|
||||
<div className="flex flex-col xl:flex-row gap-3">
|
||||
{/* Поиск */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40" />
|
||||
<GlassInput
|
||||
placeholder="Поиск..."
|
||||
@ -356,16 +443,16 @@ export function MarketCounterparties() {
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 h-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Фильтры и сортировка */}
|
||||
<div className="flex gap-2">
|
||||
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||
<SelectTrigger className="glass-input text-white border-white/20 h-9 min-w-[120px]">
|
||||
<Filter className="h-3 w-3 mr-1" />
|
||||
<SelectValue placeholder="Тип" />
|
||||
</SelectTrigger>
|
||||
{/* Фильтры и сортировка */}
|
||||
<div className="flex gap-2">
|
||||
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||
<SelectTrigger className="glass-input text-white border-white/20 h-9 min-w-[120px]">
|
||||
<Filter className="h-3 w-3 mr-1" />
|
||||
<SelectValue placeholder="Тип" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="glass-card border-white/20">
|
||||
<SelectItem value="all">Все</SelectItem>
|
||||
<SelectItem value="FULFILLMENT">Фулфилмент</SelectItem>
|
||||
@ -437,102 +524,137 @@ export function MarketCounterparties() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Список контрагентов */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{counterpartiesLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-white/60">Загрузка...</div>
|
||||
</div>
|
||||
) : filteredAndSortedCounterparties.length === 0 ? (
|
||||
<div className="glass-card p-8">
|
||||
<div className="text-center">
|
||||
{counterparties.length === 0 ? (
|
||||
<>
|
||||
<Users className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||
<p className="text-white/60">У вас пока нет контрагентов</p>
|
||||
<p className="text-white/40 text-sm mt-2">Перейдите на другие вкладки, чтобы найти партнеров</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Search className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||
<p className="text-white/60">Ничего не найдено</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
Попробуйте изменить параметры поиска или фильтрации
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Таблица контрагентов */}
|
||||
<Card className="glass-card flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="p-6 space-y-3">
|
||||
{/* Заголовок таблицы */}
|
||||
<div className="p-4 rounded-xl bg-gradient-to-r from-white/5 to-white/10 border border-white/10">
|
||||
<div className="grid grid-cols-12 gap-4 text-sm font-medium text-white/80">
|
||||
<div className="col-span-2 flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4 text-blue-400" />
|
||||
<span>Дата добавления</span>
|
||||
</div>
|
||||
<div className="col-span-3 flex items-center gap-2">
|
||||
<Building className="h-4 w-4 text-green-400" />
|
||||
<span>Организация</span>
|
||||
</div>
|
||||
<div className="col-span-1 text-center flex items-center justify-center">
|
||||
<span>Тип</span>
|
||||
</div>
|
||||
<div className="col-span-3 flex items-center gap-2">
|
||||
<Phone className="h-4 w-4 text-purple-400" />
|
||||
<span>Контакты</span>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center gap-2">
|
||||
<MapPin className="h-4 w-4 text-orange-400" />
|
||||
<span>Адрес</span>
|
||||
</div>
|
||||
<div className="col-span-1 text-center flex items-center justify-center">
|
||||
<span>Действия</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredAndSortedCounterparties.map((organization: Organization) => (
|
||||
<div key={organization.id} className="glass-card p-4 w-full hover:bg-white/5 transition-colors">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<OrganizationAvatar organization={organization} size="md" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-col space-y-2 mb-3">
|
||||
<h4 className="text-white font-medium text-lg leading-tight">
|
||||
{organization.name || organization.fullName}
|
||||
</h4>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge className={getTypeBadgeStyles(organization.type)}>
|
||||
{getTypeLabel(organization.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Строки таблицы */}
|
||||
{counterpartiesLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-white/60">Загрузка...</div>
|
||||
</div>
|
||||
) : filteredAndSortedCounterparties.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64">
|
||||
{counterparties.length === 0 ? (
|
||||
<>
|
||||
<Users className="h-12 w-12 text-white/20 mb-2" />
|
||||
<p className="text-white/60">У вас пока нет контрагентов</p>
|
||||
<p className="text-white/40 text-sm mt-1">Перейдите на другие вкладки, чтобы найти партнеров</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Search className="h-12 w-12 text-white/20 mb-2" />
|
||||
<p className="text-white/60">Ничего не найдено</p>
|
||||
<p className="text-white/40 text-sm mt-1">
|
||||
Попробуйте изменить параметры поиска или фильтрации
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
filteredAndSortedCounterparties.map((organization: Organization) => (
|
||||
<div key={organization.id} className="p-4 rounded-xl bg-white/5 hover:bg-white/10 transition-all duration-200 border border-white/10">
|
||||
<div className="grid grid-cols-12 gap-4 items-center">
|
||||
<div className="col-span-2 text-white/80">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-3 w-3 text-white/40" />
|
||||
<span className="text-sm">{formatDate(organization.createdAt)}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center text-white/60 text-sm">
|
||||
<Building className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||
<span>ИНН: {organization.inn}</span>
|
||||
</div>
|
||||
|
||||
{organization.address && (
|
||||
<div className="flex items-start text-white/60 text-sm">
|
||||
<MapPin className="h-3 w-3 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span className="line-clamp-2">{organization.address}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{organization.phones && organization.phones.length > 0 && (
|
||||
<div className="flex items-center text-white/60 text-sm">
|
||||
<Phone className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||
<span>{organization.phones[0].value}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{organization.emails && organization.emails.length > 0 && (
|
||||
<div className="flex items-center text-white/60 text-sm">
|
||||
<Mail className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||
<span className="truncate">{organization.emails[0].value}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center text-white/40 text-xs pt-2">
|
||||
<Calendar className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||
<span>Добавлен {formatDate(organization.createdAt)}</span>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<OrganizationAvatar organization={organization} size="sm" />
|
||||
<div>
|
||||
<p className="text-white font-medium text-sm">
|
||||
{organization.name || organization.fullName}
|
||||
</p>
|
||||
<p className="text-white/60 text-xs flex items-center gap-1">
|
||||
<Building className="h-3 w-3" />
|
||||
{organization.inn}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 text-center">
|
||||
<Badge className={getTypeBadgeStyles(organization.type) + ' text-xs'}>
|
||||
{getTypeLabel(organization.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<div className="space-y-1">
|
||||
{organization.phones && organization.phones.length > 0 && (
|
||||
<div className="flex items-center text-white/60 text-xs">
|
||||
<Phone className="h-3 w-3 mr-2" />
|
||||
<span>{organization.phones[0].value}</span>
|
||||
</div>
|
||||
)}
|
||||
{organization.emails && organization.emails.length > 0 && (
|
||||
<div className="flex items-center text-white/60 text-xs">
|
||||
<Mail className="h-3 w-3 mr-2" />
|
||||
<span className="truncate">{organization.emails[0].value}</span>
|
||||
</div>
|
||||
)}
|
||||
{!organization.phones?.length && !organization.emails?.length && (
|
||||
<span className="text-white/40 text-xs">Нет контактов</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
{organization.address ? (
|
||||
<p className="text-white/60 text-xs line-clamp-2">{organization.address}</p>
|
||||
) : (
|
||||
<span className="text-white/40 text-xs">Не указан</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-1 text-center">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleRemoveCounterparty(organization.id)}
|
||||
className="hover:bg-red-500/20 text-white/60 hover:text-red-300 h-8 w-8 p-0"
|
||||
title="Удалить из контрагентов"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleRemoveCounterparty(organization.id)}
|
||||
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border-red-500/30 cursor-pointer w-full"
|
||||
>
|
||||
Удалить из контрагентов
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
|
Reference in New Issue
Block a user