feat: filter bar in kanban — by manager, object type, overdue

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 11:36:36 +03:00
parent 066007e72d
commit b69ea40f35
+101 -2
View File
@@ -10,8 +10,92 @@ import OrderModal from './OrderModal';
const STATUSES = ['новая', 'замер', 'согласование', 'монтаж', 'закрыт'];
function KanbanFilters({ filters, orders, onChange }) {
const managers = [...new Set(orders.map((o) => o.assigned_to).filter(Boolean))].sort();
const objectTypes = {
industrial: 'Промышленный',
commercial: 'Коммерческий',
infrastructure: 'Инфраструктура',
residential: 'Частный',
other: 'Другое',
};
const hasFilters = filters.manager || filters.objectType || filters.overdueOnly;
return (
<div style={{
display: 'flex', alignItems: 'center', gap: '10px',
flexWrap: 'wrap', marginBottom: '16px',
}}>
{/* Manager filter */}
<select
value={filters.manager}
onChange={(e) => onChange({ ...filters, manager: e.target.value })}
style={{
padding: '7px 12px', borderRadius: '8px', fontSize: '0.85rem',
border: filters.manager ? '1.5px solid #4361EE' : '1.5px solid #E9ECEF',
background: '#fff', color: filters.manager ? '#4361EE' : '#8C9097',
cursor: 'pointer', fontFamily: 'var(--font-body)', outline: 'none',
}}
>
<option value="">Все менеджеры</option>
{managers.map((m) => <option key={m} value={m}>{m}</option>)}
</select>
{/* Object type filter */}
<select
value={filters.objectType}
onChange={(e) => onChange({ ...filters, objectType: e.target.value })}
style={{
padding: '7px 12px', borderRadius: '8px', fontSize: '0.85rem',
border: filters.objectType ? '1.5px solid #4361EE' : '1.5px solid #E9ECEF',
background: '#fff', color: filters.objectType ? '#4361EE' : '#8C9097',
cursor: 'pointer', fontFamily: 'var(--font-body)', outline: 'none',
}}
>
<option value="">Все типы</option>
{Object.entries(objectTypes).map(([v, l]) => <option key={v} value={v}>{l}</option>)}
</select>
{/* Overdue toggle */}
<button
onClick={() => onChange({ ...filters, overdueOnly: !filters.overdueOnly })}
style={{
padding: '7px 14px', borderRadius: '8px', fontSize: '0.85rem', fontWeight: 600,
border: filters.overdueOnly ? '1.5px solid #ef4444' : '1.5px solid #E9ECEF',
background: filters.overdueOnly ? '#fef2f2' : '#fff',
color: filters.overdueOnly ? '#ef4444' : '#8C9097',
cursor: 'pointer', fontFamily: 'var(--font-heading)',
}}
>
Просроченные
</button>
{/* Clear button */}
{hasFilters && (
<button
onClick={() => onChange({ manager: '', objectType: '', overdueOnly: false })}
style={{
padding: '7px 12px', borderRadius: '8px', fontSize: '0.82rem',
border: '1.5px solid #E9ECEF', background: '#fff',
color: '#8C9097', cursor: 'pointer', fontFamily: 'var(--font-body)',
}}
>
Сбросить
</button>
)}
{hasFilters && (
<span style={{ fontSize: '0.78rem', color: '#8C9097', marginLeft: '4px' }}>
Фильтр активен
</span>
)}
</div>
);
}
const KanbanBoard = () => {
const { ordersByStatus, loading, updateStatus } = useOrders();
const { orders, ordersByStatus, loading, updateStatus } = useOrders();
const { session } = useAuth();
const { showNewOrder, setShowNewOrder } = useOutletContext() || {};
const isMobile = useIsMobile();
@@ -21,6 +105,7 @@ const KanbanBoard = () => {
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }));
const [localOverrides, setLocalOverrides] = useState({});
const [filters, setFilters] = useState({ manager: '', objectType: '', overdueOnly: false });
const findOrderStatus = (orderId) => {
if (localOverrides[orderId]) return localOverrides[orderId];
@@ -76,6 +161,19 @@ const KanbanBoard = () => {
return acc;
}, {});
const filteredOrdersByStatus = STATUSES.reduce((acc, status) => {
acc[status] = (effectiveOrdersByStatus[status] || []).filter((o) => {
if (filters.manager && o.assigned_to !== filters.manager) return false;
if (filters.objectType && o.object_type !== filters.objectType) return false;
if (filters.overdueOnly) {
const overdue = o.scheduled_at && new Date(o.scheduled_at) < new Date() && o.status !== 'закрыт';
if (!overdue) return false;
}
return true;
});
return acc;
}, {});
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '60px', color: 'var(--text-muted)', fontFamily: 'var(--font-heading)' }}>
@@ -86,6 +184,7 @@ const KanbanBoard = () => {
return (
<>
<KanbanFilters filters={filters} orders={orders} onChange={setFilters} />
<DndContext sensors={sensors} collisionDetection={closestCorners} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<div style={{
display: 'flex',
@@ -100,7 +199,7 @@ const KanbanBoard = () => {
<KanbanColumn
key={status}
status={status}
orders={effectiveOrdersByStatus[status] || []}
orders={filteredOrdersByStatus[status] || []}
onCardClick={setSelectedOrder}
isMobile={isMobile}
/>