import { zodResolver } from '@hookform/resolvers/zod' import { type ColumnDef } from '@tanstack/react-table' import { Pencil, Plus, Trash2 } from 'lucide-react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' import { DataTable } from '@/components/data-table/data-table' import { DataTableToolbar } from '@/components/data-table/data-table-toolbar' import { Button } from '@/components/ui/button' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet' import { useToast } from '@/components/ui/use-toast' import { useContactsQuery, useCreateContactMutation, useDeleteContactMutation, useUpdateContactMutation } from '@/features/contacts/hooks' import { useDebounce } from '@/hooks/use-debounce' import { formatDate } from '@/lib/utils' import type { Contact } from '@/types/crm' const contactFormSchema = z.object({ name: z.string().min(2, 'Имя не короче двух символов'), email: z.string().email('Некорректный email').or(z.literal('')).optional(), phone: z.string().min(6, 'Телефон слишком короткий').or(z.literal('')).optional(), ownerId: z.string().optional(), }) interface ContactFormValues extends z.infer {} const defaultValues: ContactFormValues = { name: '', email: '', phone: '', ownerId: '', } const ContactsPage = () => { const [search, setSearch] = useState('') const [ownerFilter, setOwnerFilter] = useState('all') const [drawerOpen, setDrawerOpen] = useState(false) const [editingContact, setEditingContact] = useState(null) const debouncedSearch = useDebounce(search, 400) const ownerIdFilter = ownerFilter === 'all' ? undefined : Number(ownerFilter) const { data: contacts = [], isLoading } = useContactsQuery({ search: debouncedSearch.trim() || undefined, ownerId: Number.isFinite(ownerIdFilter) ? ownerIdFilter : undefined, }) const createContact = useCreateContactMutation() const updateContact = useUpdateContactMutation() const deleteContact = useDeleteContactMutation() const { toast } = useToast() const ownerIds = useMemo(() => { const ids = new Set() contacts.forEach((contact) => { if (contact.owner_id) ids.add(contact.owner_id) }) return Array.from(ids).sort((a, b) => a - b) }, [contacts]) const ownerSelectOptions = useMemo(() => ownerIds.map((ownerId) => ({ value: String(ownerId), label: `Сотрудник #${ownerId}` })), [ownerIds]) const openCreateDrawer = () => { setEditingContact(null) setDrawerOpen(true) } const openEditDrawer = (contact: Contact) => { setEditingContact(contact) setDrawerOpen(true) } const handleDelete = useCallback( async (contact: Contact) => { const confirmed = window.confirm(`Удалить контакт «${contact.name}»?`) if (!confirmed) return try { await deleteContact.mutateAsync(contact.id) toast({ title: 'Контакт удалён', description: 'Запись больше не отображается в списке.' }) } catch (error) { toast({ title: 'Ошибка удаления', description: error instanceof Error ? error.message : 'Попробуйте позже', variant: 'destructive' }) } }, [deleteContact, toast], ) const columns = useMemo[]>( () => [ { accessorKey: 'name', header: 'Контакт', cell: ({ row }) => (

{row.original.name}

{row.original.email ?? '—'}

), }, { accessorKey: 'phone', header: 'Телефон', cell: ({ row }) => row.original.phone ?? '—', }, { accessorKey: 'owner_id', header: 'Владелец', cell: ({ row }) => Сотрудник #{row.original.owner_id}, }, { accessorKey: 'created_at', header: 'Создан', cell: ({ row }) => {formatDate(row.original.created_at)}, }, { id: 'actions', header: '', cell: ({ row }) => (
), }, ], [handleDelete], ) return (

Контакты

Управляйте базой контактов и быстро создавайте новые записи.

Новый контакт } > {ownerIds.length ? ( ) : null} } /> { const payload = { name: values.name, email: values.email ? values.email : null, phone: values.phone ? values.phone : null, owner_id: values.ownerId ? Number(values.ownerId) : undefined, } try { if (editingContact) { await updateContact.mutateAsync({ contactId: editingContact.id, payload }) toast({ title: 'Контакт обновлён', description: 'Изменения сохранены.' }) } else { await createContact.mutateAsync(payload) toast({ title: 'Контакт создан', description: 'Добавлен новый контакт.' }) } setDrawerOpen(false) } catch (error) { toast({ title: 'Ошибка сохранения', description: error instanceof Error ? error.message : 'Попробуйте ещё раз', variant: 'destructive' }) } }} key={editingContact?.id ?? 'create'} />
) } interface ContactFormDrawerProps { open: boolean onOpenChange: (open: boolean) => void contact: Contact | null onSubmit: (values: ContactFormValues) => Promise isSubmitting: boolean ownerOptions: Array<{ value: string; label: string }> } const ContactFormDrawer = ({ open, onOpenChange, contact, onSubmit, isSubmitting, ownerOptions }: ContactFormDrawerProps) => { const form = useForm({ resolver: zodResolver(contactFormSchema), defaultValues, }) useEffect(() => { if (contact) { form.reset({ name: contact.name, email: contact.email ?? '', phone: contact.phone ?? '', ownerId: contact.owner_id ? String(contact.owner_id) : '', }) } else { form.reset(defaultValues) } }, [contact, form]) const handleSubmit = async (values: ContactFormValues) => { await onSubmit(values) form.reset(defaultValues) } return ( {contact ? 'Редактирование контакта' : 'Новый контакт'} Укажите основные данные и при необходимости закрепите владельца.
( Имя )} /> ( E-mail )} /> ( Телефон )} /> ( Владелец Выберите руководителя из списка или оставьте поле пустым, чтобы назначить себя. )} />
) } export default ContactsPage