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 (
);
}
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}
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 && (
)}
);
}