import type { Pipeline } from '@lr/shared'; import { ArrowDown, ArrowUp, Lock, Plus, Trash2 } from 'lucide-react'; import { useEffect, useState } from 'react'; import { api } from '../api.ts'; import { ErrorNote } from '../components/ui.tsx'; import { useAction } from '../hooks.ts'; import { usePipeline, type Status } from './api.ts'; import { useCrm } from './i18n.ts'; import { Dialog } from './parts.tsx'; /** Название, которое сохраняется при уходе из поля или по Enter. */ function NameField({ value, label, onSave }: { value: string; label: string; onSave: (name: string) => void }) { const [text, setText] = useState(value); useEffect(() => setText(value), [value]); const commit = () => { const name = text.trim(); if (name && name !== value) onSave(name); else setText(value); }; return ( setText(e.target.value)} onBlur={commit} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }} /> ); } function IconButton({ label, onClick, disabled, danger, children }: { label: string; onClick: () => void; disabled?: boolean; danger?: boolean; children: React.ReactNode }) { return ( ); } function AddRow({ placeholder, onAdd, pending }: { placeholder: string; onAdd: (name: string, done: () => void) => void; pending: boolean }) { const { c } = useCrm(); const [name, setName] = useState(''); return (
{ e.preventDefault(); if (name.trim()) onAdd(name.trim(), () => setName('')); }}> setName(e.target.value)} />
); } const move = (list: T[], from: number, to: number) => { const next = [...list]; next.splice(to, 0, ...next.splice(from, 1)); return next; }; /** * Настройка воронки руководителем: статусы и причины отказа. Системные статусы можно только * переименовать — на них завязаны пул, аналитика и биллинг. */ export function PipelineSettings({ pipeline }: { pipeline: Pipeline }) { const { c, t } = useCrm(); const x = c.pl; const info = usePipeline(pipeline); const call = (path: string, method: string, body?: object) => api(path, { method, body }); const act = useAction((v: { path: string; method: string; body?: object }) => call(v.path, v.method, v.body)); const [removing, setRemoving] = useState(null); const [moveTo, setMoveTo] = useState(''); const statuses = info.data?.statuses ?? []; const reasons = info.data?.reasons ?? []; const middle = statuses.filter((s) => !s.code); const reorder = (from: number, to: number) => act.mutate({ path: `/pipelines/${pipeline}/order`, method: 'POST', body: { ids: move(middle, from, to).map((s) => s.id) } }); const reorderReasons = (from: number, to: number) => act.mutate({ path: `/pipelines/${pipeline}/reasons-order`, method: 'POST', body: { ids: move(reasons, from, to).map((r) => r.id) } }); const remove = (s: Status) => { if (s.count > 0) { setMoveTo(''); setRemoving(s); } else if (confirm(x.confirmStatus(s.name))) act.mutate({ path: `/pipeline-statuses/${s.id}`, method: 'DELETE' }); }; // Карточки удаляемого статуса переходят в другой рабочий статус. const targets = statuses.filter((s) => s.id !== removing?.id && (!s.code || s.code === 'taken')); return (

{x.title[pipeline]}

{x.hint}

{x.statuses}

    {statuses.map((s) => { const i = middle.findIndex((m) => m.id === s.id); const system = Boolean(s.code); return (
  • reorder(i, i - 1)}> reorder(i, i + 1)}>
    act.mutate({ path: `/pipeline-statuses/${s.id}`, method: 'PATCH', body: { name } })} /> {pipeline === 'object' && !['new', 'deal', 'rejected'].includes(s.code ?? '') && ( )} {system ? ( ) : ( remove(s)}> )}
  • ); })}
act.mutate({ path: `/pipelines/${pipeline}/statuses`, method: 'POST', body: { name } }, { onSuccess: done })} />

{x.reasons}

    {reasons.map((r, i) => (
  • reorderReasons(i, i - 1)}> reorderReasons(i, i + 1)}>
    act.mutate({ path: `/reject-reasons/${r.id}`, method: 'PATCH', body: { name } })} /> {r.code ? ( ) : ( confirm(x.confirmReason(r.name)) && act.mutate({ path: `/reject-reasons/${r.id}`, method: 'DELETE' })}> )}
  • ))}
act.mutate({ path: `/pipelines/${pipeline}/reasons`, method: 'POST', body: { name } }, { onSuccess: done })} /> {removing && ( { setRemoving(null); act.reset(); }}>
{ e.preventDefault(); act.mutate({ path: `/pipeline-statuses/${removing.id}?moveTo=${moveTo}`, method: 'DELETE' }, { onSuccess: () => setRemoving(null) }); }} >

{x.removeHint(removing.count)}

)}
); }