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:
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user