163 lines
6.7 KiB
TypeScript
163 lines
6.7 KiB
TypeScript
import { useState } from 'react';
|
||
import { Search, Plus, MessageSquare, Star, Clock } from 'lucide-react';
|
||
import { useCommunicationsStore } from '../services/communicationsService';
|
||
import { Badge, Button } from '@omnichannel/ui';
|
||
|
||
interface ContactsViewProps {
|
||
onAddContactModal: () => void;
|
||
}
|
||
|
||
export function ContactsView({ onAddContactModal }: ContactsViewProps) {
|
||
const { conversations, setActiveTab, setActiveChatId } = useCommunicationsStore();
|
||
const [filterType, setFilterType] = useState<'all' | 'clients' | 'vip'>('all');
|
||
const [searchQuery, setSearchQuery] = useState('');
|
||
|
||
const contactsList = conversations.map((c) => ({
|
||
...c.contact,
|
||
conversationId: c.id,
|
||
}));
|
||
|
||
const filteredContacts = contactsList.filter((cnt) => {
|
||
if (filterType === 'clients' && !cnt.isClient) return false;
|
||
if (filterType === 'vip' && !cnt.isVip) return false;
|
||
if (searchQuery.trim()) {
|
||
const q = searchQuery.toLowerCase();
|
||
return (
|
||
cnt.name.toLowerCase().includes(q) ||
|
||
cnt.company.toLowerCase().includes(q) ||
|
||
cnt.role.toLowerCase().includes(q)
|
||
);
|
||
}
|
||
return true;
|
||
});
|
||
|
||
const handleOpenChat = (conversationId: string) => {
|
||
setActiveChatId(conversationId);
|
||
setActiveTab('messengers');
|
||
};
|
||
|
||
return (
|
||
<div className="flex-1 min-h-0 flex flex-col p-4 sm:p-6 bg-[var(--krona-bg-workspace)] overflow-y-auto">
|
||
{/* Header controls */}
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
||
<div className="flex items-center gap-3">
|
||
<div className="relative w-64 sm:w-80">
|
||
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-[var(--krona-text-muted)]" />
|
||
<input
|
||
type="text"
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
placeholder="Поиск по контактам..."
|
||
className="w-full h-10 pl-9 pr-3 rounded-xl bg-[var(--krona-surface)] border border-[var(--krona-border)] text-sm text-[var(--krona-text-primary)] placeholder-[var(--krona-text-muted)] focus:outline-none focus:border-[var(--krona-primary)]"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-1 bg-[var(--krona-surface)] p-1 rounded-xl border border-[var(--krona-border)] text-xs font-semibold">
|
||
<button
|
||
type="button"
|
||
onClick={() => setFilterType('all')}
|
||
className={`px-3 py-1.5 rounded-lg transition-colors ${
|
||
filterType === 'all'
|
||
? 'bg-[var(--krona-primary)] text-white'
|
||
: 'text-[var(--krona-text-secondary)] hover:text-[var(--krona-text-primary)]'
|
||
}`}
|
||
>
|
||
Все
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setFilterType('clients')}
|
||
className={`px-3 py-1.5 rounded-lg transition-colors ${
|
||
filterType === 'clients'
|
||
? 'bg-[var(--krona-primary)] text-white'
|
||
: 'text-[var(--krona-text-secondary)] hover:text-[var(--krona-text-primary)]'
|
||
}`}
|
||
>
|
||
Клиенты
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setFilterType('vip')}
|
||
className={`px-3 py-1.5 rounded-lg transition-colors ${
|
||
filterType === 'vip'
|
||
? 'bg-[var(--krona-primary)] text-white'
|
||
: 'text-[var(--krona-text-secondary)] hover:text-[var(--krona-text-primary)]'
|
||
}`}
|
||
>
|
||
VIP
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<Button
|
||
variant="primary"
|
||
onClick={onAddContactModal}
|
||
leftIcon={<Plus className="w-4 h-4" />}
|
||
>
|
||
Добавить контакт
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Contacts Cards Grid */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||
{filteredContacts.map((cnt) => (
|
||
<div
|
||
key={cnt.id}
|
||
className="p-5 rounded-2xl bg-[var(--krona-surface)] border border-[var(--krona-border-subtle)] shadow-xs flex flex-col justify-between gap-4 hover:border-[var(--krona-border)] transition-all"
|
||
>
|
||
<div>
|
||
<div className="flex items-start justify-between gap-2">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-12 h-12 rounded-full bg-[var(--krona-bg-workspace)] border border-[var(--krona-border)] flex items-center justify-center font-bold text-sm text-[var(--krona-primary)]">
|
||
{cnt.initials}
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center gap-1.5">
|
||
<h3 className="font-bold text-sm text-[var(--krona-text-primary)]">
|
||
{cnt.name}
|
||
</h3>
|
||
{cnt.isVip && <Star className="w-3.5 h-3.5 text-amber-500 fill-amber-500" />}
|
||
</div>
|
||
<span className="text-xs text-[var(--krona-text-secondary)] block">
|
||
{cnt.company} • {cnt.role}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-1">
|
||
{cnt.isClient && <Badge variant="success" size="sm">Клиент</Badge>}
|
||
{cnt.isVip && <Badge variant="warning" size="sm">VIP</Badge>}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Channels snippet */}
|
||
<div className="mt-4 space-y-1.5 text-xs text-[var(--krona-text-secondary)]">
|
||
{cnt.channels.slice(0, 2).map((ch, idx) => (
|
||
<div key={idx} className="flex items-center justify-between">
|
||
<span className="font-medium capitalize">{ch.label}:</span>
|
||
<span className="text-[var(--krona-text-primary)]">{ch.value}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="pt-3 border-t border-[var(--krona-border-subtle)] flex items-center justify-between gap-2">
|
||
<span className="text-[11px] text-[var(--krona-text-muted)] flex items-center gap-1">
|
||
<Clock className="w-3 h-3" />
|
||
{cnt.lastActivity}
|
||
</span>
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
onClick={() => handleOpenChat(cnt.conversationId)}
|
||
leftIcon={<MessageSquare className="w-3.5 h-3.5 text-[var(--krona-primary)]" />}
|
||
>
|
||
В чат
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|