diff --git a/src/admin/orders/KanbanBoard.jsx b/src/admin/orders/KanbanBoard.jsx
new file mode 100644
index 0000000..6020651
--- /dev/null
+++ b/src/admin/orders/KanbanBoard.jsx
@@ -0,0 +1,92 @@
+import React, { useState } from 'react';
+import { DndContext, closestCorners, DragOverlay, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
+import { useOutletContext } from 'react-router-dom';
+import { useOrders } from './useOrders';
+import { useAuth } from '../auth/useAuth';
+import KanbanColumn from './KanbanColumn';
+import OrderCard from './OrderCard';
+import OrderModal from './OrderModal';
+
+const STATUSES = ['новая', 'замер', 'согласование', 'монтаж', 'закрыт'];
+
+const KanbanBoard = () => {
+ const { ordersByStatus, loading, updateStatus } = useOrders();
+ const { session } = useAuth();
+ const { showNewOrder, setShowNewOrder } = useOutletContext() || {};
+ const [activeOrder, setActiveOrder] = useState(null);
+ const [selectedOrder, setSelectedOrder] = useState(null);
+
+ const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }));
+
+ const findOrderStatus = (orderId) => {
+ for (const status of STATUSES) {
+ if (ordersByStatus[status]?.find((o) => o.id === orderId)) return status;
+ }
+ return null;
+ };
+
+ const handleDragStart = ({ active }) => {
+ const status = findOrderStatus(active.id);
+ if (status) setActiveOrder(ordersByStatus[status].find((o) => o.id === active.id));
+ };
+
+ const handleDragEnd = async ({ active, over }) => {
+ setActiveOrder(null);
+ if (!over) return;
+
+ const targetStatus = STATUSES.includes(over.id) ? over.id : findOrderStatus(over.id);
+ const sourceStatus = findOrderStatus(active.id);
+
+ if (targetStatus && sourceStatus !== targetStatus) {
+ await updateStatus(active.id, targetStatus, session?.user?.email);
+ }
+ };
+
+ if (loading) {
+ return (
+
+ Загрузка заказов...
+
+ );
+ }
+
+ return (
+ <>
+
+
+ {STATUSES.map((status) => (
+
+ ))}
+
+
+ {activeOrder && {}} />}
+
+
+
+ {selectedOrder && (
+ setSelectedOrder(null)}
+ onSaved={() => setSelectedOrder(null)}
+ />
+ )}
+
+ {showNewOrder && (
+ setShowNewOrder(false)}
+ onSaved={() => setShowNewOrder(false)}
+ />
+ )}
+ >
+ );
+};
+
+export default KanbanBoard;
diff --git a/src/admin/orders/KanbanColumn.jsx b/src/admin/orders/KanbanColumn.jsx
new file mode 100644
index 0000000..16384c3
--- /dev/null
+++ b/src/admin/orders/KanbanColumn.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { useDroppable } from '@dnd-kit/core';
+import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
+import OrderCard from './OrderCard';
+
+const STATUS_COLORS = {
+ 'новая': '#f59e0b',
+ 'замер': '#0ea5e9',
+ 'согласование': '#8b5cf6',
+ 'монтаж': '#f97316',
+ 'закрыт': '#22c55e',
+};
+
+const KanbanColumn = ({ status, orders, onCardClick }) => {
+ const { setNodeRef, isOver } = useDroppable({ id: status });
+
+ return (
+
+ {/* Header */}
+
+
+ {status}
+
+
+ {orders.length}
+
+
+
+ {/* Drop zone */}
+
+ o.id)} strategy={verticalListSortingStrategy}>
+ {orders.map((order) => (
+ onCardClick(order)} />
+ ))}
+
+
+
+ );
+};
+
+export default KanbanColumn;
diff --git a/src/admin/orders/OrderCard.jsx b/src/admin/orders/OrderCard.jsx
new file mode 100644
index 0000000..7f3e571
--- /dev/null
+++ b/src/admin/orders/OrderCard.jsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import { useSortable } from '@dnd-kit/sortable';
+import { CSS } from '@dnd-kit/utilities';
+import { GripVertical } from 'lucide-react';
+
+const OBJECT_TYPE_LABELS = {
+ industrial: 'Промышленный',
+ commercial: 'Коммерческий',
+ infrastructure: 'Инфраструктура',
+ residential: 'Частный',
+ other: 'Другое',
+};
+
+const OrderCard = ({ order, onClick }) => {
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
+ id: order.id,
+ });
+
+ return (
+
+
e.stopPropagation()}
+ >
+
+
+
+
+ {order.name}
+
+
+ {order.object_type && (
+
+ {OBJECT_TYPE_LABELS[order.object_type] || order.object_type}
+
+ )}
+ {order.area_sqm && (
+ {order.area_sqm} м²
+ )}
+
+ {(order.final_cost || order.estimated_cost) && (
+
+ {Number(order.final_cost || order.estimated_cost).toLocaleString('ru-RU')} ₽
+
+ )}
+
+ {new Date(order.created_at).toLocaleDateString('ru-RU')}
+
+
+
+ );
+};
+
+export default OrderCard;
diff --git a/src/admin/orders/OrderModal.jsx b/src/admin/orders/OrderModal.jsx
new file mode 100644
index 0000000..47d6185
--- /dev/null
+++ b/src/admin/orders/OrderModal.jsx
@@ -0,0 +1,224 @@
+import React, { useState, useEffect } from 'react';
+import { X, Save } from 'lucide-react';
+import { supabase } from '../../lib/supabase';
+import { useAuth } from '../auth/useAuth';
+
+const STATUSES = ['новая', 'замер', 'согласование', 'монтаж', 'закрыт'];
+const OBJECT_TYPES = [
+ { value: 'industrial', label: 'Промышленный объект' },
+ { value: 'commercial', label: 'Коммерческая штаб-квартира' },
+ { value: 'infrastructure', label: 'Критическая инфраструктура' },
+ { value: 'residential', label: 'Частная резиденция' },
+ { value: 'other', label: 'Другое' },
+];
+
+const inputStyle = {
+ width: '100%', padding: '10px 12px',
+ background: 'var(--bg-lighter)',
+ border: '1.5px solid var(--border-light)',
+ borderRadius: '6px',
+ color: 'var(--text-main)', fontSize: '0.92rem',
+ outline: 'none', fontFamily: 'var(--font-body)',
+ boxSizing: 'border-box',
+};
+
+const labelStyle = {
+ display: 'block', marginBottom: '5px',
+ fontSize: '0.75rem', fontWeight: 700,
+ color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.4px',
+};
+
+const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
+ const { session } = useAuth();
+ const [form, setForm] = useState({
+ name: order?.name || '',
+ phone: order?.phone || '',
+ object_type: order?.object_type || '',
+ area_sqm: order?.area_sqm ?? '',
+ film_thickness: order?.film_thickness ?? 200,
+ estimated_cost: order?.estimated_cost ?? '',
+ final_cost: order?.final_cost ?? '',
+ notes: order?.notes || '',
+ assigned_to: order?.assigned_to || '',
+ status: order?.status || 'новая',
+ });
+ const [events, setEvents] = useState([]);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ if (order?.id) {
+ supabase
+ .from('order_events')
+ .select('*')
+ .eq('order_id', order.id)
+ .order('created_at', { ascending: false })
+ .then(({ data }) => setEvents(data || []));
+ }
+ }, [order?.id]);
+
+ const set = (e) => setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
+
+ const handleSave = async () => {
+ if (!form.name.trim() || !form.phone.trim()) {
+ setError('Имя и телефон обязательны');
+ return;
+ }
+ setSaving(true);
+ setError('');
+ const userEmail = session?.user?.email;
+
+ if (mode === 'create') {
+ const { error } = await supabase
+ .from('orders')
+ .insert({ ...form, source: 'manual' });
+ if (error) { setError(error.message); setSaving(false); return; }
+ } else {
+ const statusChanged = form.status !== order.status;
+ const updates = { ...form };
+ if (statusChanged && form.status === 'закрыт') updates.closed_at = new Date().toISOString();
+
+ const { error } = await supabase.from('orders').update(updates).eq('id', order.id);
+ if (error) { setError(error.message); setSaving(false); return; }
+
+ const { error: evtErr } = await supabase.from('order_events').insert({
+ order_id: order.id,
+ created_by: userEmail,
+ event_type: statusChanged ? 'status_change' : 'edit',
+ description: statusChanged
+ ? `Статус изменён: ${order.status} → ${form.status}`
+ : 'Данные заказа обновлены',
+ });
+ if (evtErr) console.error('Failed to log order event:', evtErr);
+ }
+ setSaving(false);
+ onSaved?.();
+ onClose();
+ };
+
+ return (
+
+
e.stopPropagation()}
+ >
+ {/* Header */}
+
+
+ {mode === 'create' ? 'Новая заявка' : order?.name}
+
+
+
+
+ {/* Fields */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {error &&
{error}
}
+
+
+
+
+
+ {/* Event log */}
+ {mode === 'edit' && events.length > 0 && (
+
+
+ История изменений
+
+
+ {events.map((e) => (
+
+
+ {new Date(e.created_at).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
+
+ {e.description}
+ {e.created_by && (
+ {e.created_by}
+ )}
+
+ ))}
+
+
+ )}
+
+
+ );
+};
+
+export default OrderModal;