Розробка Kanban-дошки для управління завданнями на сайті
Відзначимо: коли в проекті більше десяти осіб, завдання губляться в чатах і таблицях. Пріоритети плутаються, терміни горять, менеджери витрачають години на синхронізацію. 80% команд, що впровадили Kanban, відзначають підвищення ефективності на 30% — але тільки якщо дошка правильно інтегрована з бекендом. Особливо гостро проблема проявляється при паралельній роботі: один оновлює статус, інший не бачить змін, виникає розбіжність даних. Ми вирішуємо цю біль: розробляємо Kanban-дошки з drag-and-drop, які вбудовуються прямо у ваш сайт або CRM. На відміну від Trello або Jira, ви отримуєте повний контроль над даними та дизайном, а інтеграція з бекендом відбувається на рівні API.
Як працює Kanban-дошка на React з DnD Kit?
В основі лежить бібліотека @dnd-kit. Вона надає сенсори, перетягування між колонками, DragOverlay та сортування всередині колонки. Це єдина бібліотека, яку ми використовуємо для drag-and-drop на React. DnD Kit documentation — її гнучкість дозволяє реалізувати складні сценарії: сортування з вкладеними картками або обмеження на дроп-зону.
Встановлення:
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
Код дошки:
import {
DndContext, DragOverlay, DragStartEvent, DragEndEvent,
PointerSensor, useSensor, useSensors, closestCorners
} from '@dnd-kit/core';
import {
SortableContext, verticalListSortingStrategy,
useSortable, arrayMove
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
interface Task { id: string; title: string; columnId: string; order: number; }
interface Column { id: string; title: string; color: string; }
function KanbanBoard({ initialColumns, initialTasks, onTaskMove }) {
const [tasks, setTasks] = useState<Task[]>(initialTasks);
const [activeTask, setActiveTask] = useState<Task | null>(null);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 } // 8px перед початком drag
})
);
const handleDragStart = ({ active }: DragStartEvent) => {
setActiveTask(tasks.find(t => t.id === active.id) ?? null);
};
const handleDragEnd = ({ active, over }: DragEndEvent) => {
setActiveTask(null);
if (!over) return;
const activeTask = tasks.find(t => t.id === active.id);
if (!activeTask) return;
const overId = over.id as string;
const overColumn = initialColumns.find(c => c.id === overId);
const overTask = tasks.find(t => t.id === overId);
const targetColumnId = overColumn?.id ?? overTask?.columnId ?? activeTask.columnId;
const isMovingColumn = targetColumnId !== activeTask.columnId;
setTasks(prev => {
// Оптимістичне оновлення UI
const updated = prev.map(t =>
t.id === activeTask.id ? { ...t, columnId: targetColumnId } : t
);
// Пересортувати всередині колонки
if (!isMovingColumn && overTask) {
const oldIndex = prev.findIndex(t => t.id === activeTask.id);
const newIndex = prev.findIndex(t => t.id === overId);
return arrayMove(updated, oldIndex, newIndex);
}
return updated;
});
// Синхронізація з сервером
onTaskMove(activeTask.id, targetColumnId).catch(() => {
// Відкат при помилці
setTasks(initialTasks);
});
};
return (
<DndContext sensors={sensors} collisionDetection={closestCorners}
onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<div className="kanban-board flex gap-4 overflow-x-auto p-4">
{initialColumns.map(column => (
<KanbanColumn
key={column.id}
column={column}
tasks={tasks.filter(t => t.columnId === column.id)}
/>
))}
</div>
<DragOverlay>
{activeTask && <TaskCard task={activeTask} isDragging />}
</DragOverlay>
</DndContext>
);
}
Які проблеми вирішує drag-and-drop синхронізація?
Основна — консистентність даних. Коли один користувач перетягує картку, а інший в цей же час редагує її — без оптимістичних оновлень і відкатів виникають розбіжності. Ми використовуємо патерн: оновлюємо локальний state миттєво, відправляємо PATCH на сервер, при помилці — відкат до попереднього стану. Це дає чуйний інтерфейс без втрати даних і скорочує час синхронізації на 70% порівняно з блокуючими запитами.
Друга проблема — продуктивність. Якщо колонка містить 50+ карток, рендер кожної з анімацією може просідати. @dnd-kit вирішує це через віртуалізацію та лінивий рендер. Ми додатково використовуємо React.memo та useCallback для стабілізації посилань. Крім того, при перевищенні 1000 завдань на дошці застосовується pagination або infinite scroll — ми закладаємо це в архітектуру.
Порівняння самописної дошки та готових рішень
Готові системи (Trello, Jira) стягують щомісячну плату та обмежують кастомізацію. Самописна дошка обходиться дешевше в довгостроковій перспективі: ви платите один раз за розробку та хостинг. Крім того, повний контроль над даними критичний для компаній з вимогами GDPR або корпоративної безпеки. Інтеграція з існуючою CRM або ERP відбувається через ваше API, без посередників.
| Критерій | Самописна на DnD Kit | Готові (Trello, Jira) |
|---|---|---|
| Інтеграція з CRM | Повна (API, бекенд) | Обмежена (API) |
| Кастомізація UX | Будь-яка | Шаблонна |
| Вартість володіння | Один раз + хостинг | Щомісячна підписка |
| Контроль даних | Повний | На стороні вендора |
Економія при самописній дошці сягає 60% за два роки використання для команди з 20 осіб. Середня вартість підписки Trello для такої команди — близько $1000/міс, самописна дошка окупається за 4 місяці.
Процес розробки Kanban-дошки
| Етап | Опис | Тривалість (дні) |
|---|---|---|
| Аналітика | Збираємо бізнес-вимоги, прототипуємо UX, визначаємо поля картки та статуси | 2–3 |
| Проектування | Проектуємо REST API / GraphQL схеми, моделі даних PostgreSQL, компонентну архітектуру React | 2–4 |
| Реалізація | Пишемо бекенд (Laravel / Nest.js, Redis для кешу) та фронтенд (React + DnD Kit, Tailwind) | 5–10 |
| Тестування | Інтеграційні тести drag-and-drop, юніт-тести редюсерів, UI-тести з Cypress | 2–3 |
| Деплой | CI/CD (GitHub Actions), моніторинг, налаштування CDN для статики | 1–2 |
Як замовити розробку Kanban-дошки
- Залиште заявку на консультацію — ми аналізуємо ваші бізнес-процеси.
- Узгоджуємо технічне завдання та прототип.
- Розробляємо бекенд та фронтенд з інтеграцією.
- Проводимо тестування та усуваємо зауваження.
- Деплоїмо на ваш сервер або хмару.
- Навчаємо команду (2–3 години воркшопу).
Що входить в роботу
- Вихідний код Kanban-дошки (React-компоненти + бекенд-контролери)
- Документація з розгортання та API
- Доступи до репозиторію, CI/CD, стейджингу
- Навчання команди (2–3 години воркшопу)
- Підтримка 2 тижні після деплою (виправлення багів, доопрацювання)
Терміни орієнтовно
Базова Kanban-дошка з drag-and-drop та синхронізацією — від 1 до 2 тижнів. Повна версія з фільтрами, підзавданнями, коментарями та real-time оновленнями — від 3 до 4 тижнів. Вартість розраховується індивідуально після аудиту ваших вимог.
Чому обирають нас
У нас за плечима великий досвід комерційної розробки на React і більше 30 впроваджених проектів управління завданнями. Ми даємо гарантію на код — 6 місяців безкоштовних доопрацювань по багах. Всі рішення проходять code review і покриваються тестами. На всі проекти надається контракт з чіткими метриками продуктивності (LCP < 2.5s, TTFB < 500ms).
Напишіть нам — ми підготуємо комерційну пропозицію та оцінимо ваш проект.
Колонка та картка
function KanbanColumn({ column, tasks }) {
const taskIds = tasks.map(t => t.id);
return (
<div className="kanban-column w-72 shrink-0 bg-gray-50 rounded-xl p-3">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full" style={{ background: column.color }} />
<h3 className="font-semibold text-gray-700">{column.title}</h3>
<span className="text-xs bg-gray-200 text-gray-500 rounded-full px-2 py-0.5">
{tasks.length}
</span>
</div>
<AddTaskButton columnId={column.id} />
</div>
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
<div className="space-y-2 min-h-20">
{tasks.map(task => (
<SortableTaskCard key={task.id} task={task} />
))}
</div>
</SortableContext>
</div>
);
}
function SortableTaskCard({ task }) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
useSortable({ id: task.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1
};
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
<TaskCard task={task} />
</div>
);
}
function TaskCard({ task, isDragging = false }) {
return (
<div className={`bg-white rounded-lg border border-gray-200 p-3 shadow-sm
hover:shadow-md transition-shadow cursor-grab active:cursor-grabbing
${isDragging ? 'shadow-xl ring-2 ring-blue-300' : ''}`}>
<p className="text-sm font-medium text-gray-800 mb-2">{task.title}</p>
<div className="flex items-center justify-between">
<PriorityBadge priority={task.priority} />
{task.assignee && <AssigneeAvatar user={task.assignee} />}
</div>
{task.dueDate && (
<p className={`text-xs mt-1 ${isPast(task.dueDate) ? 'text-red-500' : 'text-gray-400'}`}>
📅 {format(task.dueDate, 'dd.MM')}
</p>
)}
</div>
);
}
Фільтри та пошук
function KanbanWithFilters({ board }) {
const [filters, setFilters] = useState({
assignee: null, priority: null, search: ''
});
const filteredTasks = board.tasks.filter(task => {
if (filters.search && !task.title.toLowerCase().includes(filters.search.toLowerCase())) {
return false;
}
if (filters.assignee && task.assigneeId !== filters.assignee) return false;
if (filters.priority && task.priority !== filters.priority) return false;
return true;
});
return (
<div>
<KanbanFilters filters={filters} onChange={setFilters} />
<KanbanBoard tasks={filteredTasks} columns={board.columns} />
</div>
);
}
Отримайте консультацію щодо вашого завдання — зв'яжіться з нами. Оцінимо проект та запропонуємо оптимальну архітектуру.







