import { CURRENCIES, DEAL_TYPES, PROPERTY_TYPES, REGIONS, YEREVAN_DISTRICTS } from '@lr/shared'; import { useQuery } from '@tanstack/react-query'; import { Columns3, EyeOff, Link2, List, Plus, Search, SlidersHorizontal, Star } from 'lucide-react'; import { useEffect, useState } from 'react'; import { Link, useNavigate, useParams } from 'react-router'; import { api, type Me } from '../api.ts'; import { ErrorNote, PageTitle } from '../components/ui.tsx'; import { useAction } from '../hooks.ts'; import { qs, useDepartments, useFilters, useMembers, usePipeline, useRates, type Lead } from './api.ts'; import { useCrm } from './i18n.ts'; import { Chips } from './FiltersPage.tsx'; import { LeadDrawer } from './LeadDrawer.tsx'; import { Dialog, Kanban, StatusChip, useSpecs, useStatusChange } from './parts.tsx'; type Tab = 'pool' | 'mine' | 'all'; function ManualLeadDialog({ onClose }: { onClose: (id?: number) => void }) { const { c, t } = useCrm(); const d = c.dlg; const [f, setF] = useState>({ currency: 'USD', dealType: 'sale', propertyType: 'apartment' }); const create = useAction(() => { const body = Object.fromEntries(Object.entries(f).filter(([, v]) => v !== '')); return api<{ id: number }>('/leads', { method: 'POST', body }); }); const input = (key: string, label: string, props: React.InputHTMLAttributes = {}) => (
setF({ ...f, [key]: e.target.value })} {...props} />
); const select = (key: string, label: string, options: [string, string][]) => (
); return ( onClose()} wide>
{ e.preventDefault(); create.mutate(undefined, { onSuccess: (r) => onClose(r.id) }); }}> {input('title', d.title, { required: true, maxLength: 200, autoFocus: true })}
{select('dealType', c.filters.dealType, DEAL_TYPES.map((x) => [x, c.deal[x]]))} {select('propertyType', c.filters.types, PROPERTY_TYPES.map((x) => [x, c.ptype[x]]))} {select('region', c.filters.region, [['', '—'], ...REGIONS.map((r) => [r, r] as [string, string])])} {input('district', c.col.place, { maxLength: 60 })}
{input('address', d.address, { maxLength: 300 })}
{input('price', d.price, { type: 'number', min: 0 })} {select('currency', d.currency, CURRENCIES.map((x) => [x, x]))} {input('rooms', d.rooms, { type: 'number', min: 0, max: 50 })} {input('area', d.area, { type: 'number', min: 0, step: 'any' })}
{input('authorName', d.author, { maxLength: 100 })} {input('phone', d.phone, { type: 'tel', maxLength: 40 })}
{input('url', d.link, { type: 'url', maxLength: 1000 })}
); } type ByUrlResult = { result: 'taken' | 'created' | 'mine'; id: number } | { result: 'busy'; id: number | null; assigneeName: string | null }; /** * Объект по ссылке на List.am: свободный из CRM закрепляется за агентом, занятый — показывает чей, * отсутствующий — скачивается с List.am и заводится на агента. */ function ByUrlDialog({ onClose, onOpen }: { onClose: () => void; onOpen: (id: number) => void }) { const { c, t } = useCrm(); const [url, setUrl] = useState(''); const check = useAction(() => api('/leads/by-url', { method: 'POST', body: { url } }), ['leads']); const r = check.data; return (
{ e.preventDefault(); check.mutate(); }}>
{ setUrl(e.target.value); check.reset(); }} />

{c.x.byUrlHint}

{check.isPending &&

{c.x.byUrlLoading}

} {r && (

{r.result === 'busy' ? c.x.busy(r.assigneeName) : c.x[r.result]}

)}
{r?.id ? ( ) : ( )}
); } /** Звёздочка «Избранное» в строке списка. */ function FavoriteButton({ lead, className = '' }: { lead: Lead; className?: string }) { const { c } = useCrm(); const toggle = useAction(() => api(`/leads/${lead.id}/favorite`, { method: 'POST', body: { on: !lead.favorite } }), ['leads']); return ( ); } /** Точка у объекта, карточку которого пользователь ещё не открывал. */ function UnseenDot({ lead }: { lead: Lead }) { const { c } = useCrm(); if (lead.seen !== false) return null; return ; } type PriceCurrency = 'AMD' | 'USD'; function Thumb({ lead, className }: { lead: Lead; className: string }) { return lead.photos[0] ? ( ) : (