import { KANBAN_GROUPS } from "../../shared/constants.js";
import { DropdownField } from "../../shared/DropdownField.jsx";
import { FilterIcon, RefreshIcon } from "../../shared/icons.jsx";
import { fallbackStatusGroup, fmtKanbanDate, resolveDeadlineTone, statusLabel } from "../../shared/utils.js";
export function KanbanBoard({
loading,
columns,
rows,
role,
actorId,
filters,
onRefresh,
onOpenFilter,
onRemoveFilter,
onEditFilter,
getFilterChipLabel,
onOpenSort,
sortActive,
onOpenRequest,
onClaimRequest,
onMoveRequest,
status,
FilterToolbarComponent,
StatusLineComponent,
}) {
const { useMemo, useState } = React;
const [draggingId, setDraggingId] = useState("");
const [dragOverGroup, setDragOverGroup] = useState("");
const safeColumns = Array.isArray(columns) && columns.length ? columns : KANBAN_GROUPS;
const grouped = useMemo(() => {
const map = {};
safeColumns.forEach((column) => {
map[String(column.key)] = [];
});
(rows || []).forEach((row) => {
const group = String(row?.status_group || fallbackStatusGroup(row?.status_code));
if (!map[group]) map[group] = [];
map[group].push(row);
});
return map;
}, [rows, safeColumns]);
const rowMap = useMemo(() => {
const map = new Map();
(rows || []).forEach((row) => {
if (!row?.id) return;
map.set(String(row.id), row);
});
return map;
}, [rows]);
const onDropToGroup = (event, groupKey) => {
event.preventDefault();
const requestId = String(event.dataTransfer.getData("text/plain") || draggingId || "");
setDragOverGroup("");
setDraggingId("");
if (!requestId) return;
const row = rowMap.get(requestId);
if (!row) return;
onMoveRequest(row, String(groupKey || ""));
};
const FilterToolbar = FilterToolbarComponent;
const StatusLine = StatusLineComponent;
return (
Канбан заявок
Группировка по группам статусов и серверная фильтрация карточек.
{FilterToolbar ? (
) : null}
{safeColumns.map((column) => {
const key = String(column.key || "");
const cards = grouped[key] || [];
const isOver = dragOverGroup === key;
return (
{
event.preventDefault();
setDragOverGroup(key);
}}
onDragLeave={(event) => {
if (event.currentTarget.contains(event.relatedTarget)) return;
setDragOverGroup((prev) => (prev === key ? "" : prev));
}}
onDrop={(event) => onDropToGroup(event, key)}
>
{column.label || key}
{Number(column.total ?? cards.length)}
{cards.length ? (
cards.map((row) => {
const requestId = String(row.id || "");
const isUnassigned = !String(row.assigned_lawyer_id || "").trim();
const canClaim = role === "LAWYER" && isUnassigned;
const canMove =
role === "ADMIN" ||
(!isUnassigned && String(row.assigned_lawyer_id || "").trim() === String(actorId || "").trim());
const transitionOptions = Array.isArray(row.available_transitions) ? row.available_transitions : [];
const deadline = row.sla_deadline_at || row.case_deadline_at || "";
const deadlineTone = resolveDeadlineTone(deadline);
const unreadTypes = new Set();
if (role === "LAWYER") {
if (row.lawyer_has_unread_updates && row.lawyer_unread_event_type) unreadTypes.add(String(row.lawyer_unread_event_type).toUpperCase());
} else {
if (row.client_has_unread_updates && row.client_unread_event_type) unreadTypes.add(String(row.client_unread_event_type).toUpperCase());
if (row.lawyer_has_unread_updates && row.lawyer_unread_event_type) unreadTypes.add(String(row.lawyer_unread_event_type).toUpperCase());
}
const hasUnreadMessage = unreadTypes.has("MESSAGE");
const hasUnreadAttachment = unreadTypes.has("ATTACHMENT");
return (
onOpenRequest(requestId, event)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
onOpenRequest(requestId, event);
}
}}
onDragStart={(event) => {
if (!canMove) {
event.preventDefault();
return;
}
setDraggingId(requestId);
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", requestId);
}}
onDragEnd={() => {
setDraggingId("");
setDragOverGroup("");
}}
>
{row.status_name || statusLabel(row.status_code)}
{String(row.description || "Описание не заполнено")}
{row.client_name || "-"}
{fmtKanbanDate(row.created_at)}
{row.topic_code || "-"}
{row.assigned_lawyer_name || (isUnassigned ? "Не назначено" : row.assigned_lawyer_id || "-")}
💬
📎
{deadline ? fmtKanbanDate(deadline) : "—"}
event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
>
{canClaim ? (
) : null}
{canMove && transitionOptions.length ? (
event.stopPropagation()}>
{
const targetStatus = String(nextValue || "");
if (!targetStatus) return;
onMoveRequest(row, "", targetStatus);
}}
options={transitionOptions.map((transition) => ({
value: String(transition.to_status),
label: String(transition.to_status_name || transition.to_status),
}))}
/>
) : null}
);
})
) : (
Пусто
)}
);
})}
{StatusLine ?
: null}
);
}
export default KanbanBoard;