fix: optimistic DnD updates, focus trap, film_thickness coercion, closed_at reset
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,10 @@ const KanbanBoard = () => {
|
||||
|
||||
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }));
|
||||
|
||||
const [localOverrides, setLocalOverrides] = useState({});
|
||||
|
||||
const findOrderStatus = (orderId) => {
|
||||
if (localOverrides[orderId]) return localOverrides[orderId];
|
||||
for (const status of STATUSES) {
|
||||
if (ordersByStatus[status]?.find((o) => o.id === orderId)) return status;
|
||||
}
|
||||
@@ -27,7 +30,8 @@ const KanbanBoard = () => {
|
||||
|
||||
const handleDragStart = ({ active }) => {
|
||||
const status = findOrderStatus(active.id);
|
||||
if (status) setActiveOrder(ordersByStatus[status].find((o) => o.id === active.id));
|
||||
if (status) setActiveOrder(ordersByStatus[status].find((o) => o.id === active.id) ||
|
||||
Object.values(ordersByStatus).flat().find((o) => o.id === active.id));
|
||||
};
|
||||
|
||||
const handleDragEnd = async ({ active, over }) => {
|
||||
@@ -38,10 +42,38 @@ const KanbanBoard = () => {
|
||||
const sourceStatus = findOrderStatus(active.id);
|
||||
|
||||
if (targetStatus && sourceStatus !== targetStatus) {
|
||||
await updateStatus(active.id, targetStatus, session?.user?.email);
|
||||
setLocalOverrides((prev) => ({ ...prev, [active.id]: targetStatus }));
|
||||
const { error } = await updateStatus(active.id, targetStatus, session?.user?.email);
|
||||
if (error) {
|
||||
console.error('Status update failed:', error);
|
||||
setLocalOverrides((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[active.id];
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
setLocalOverrides((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[active.id];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const effectiveOrdersByStatus = STATUSES.reduce((acc, status) => {
|
||||
acc[status] = (ordersByStatus[status] || []).filter(
|
||||
(o) => !localOverrides[o.id] || localOverrides[o.id] === status
|
||||
);
|
||||
// Add orders that were moved here optimistically
|
||||
const overrideOrders = Object.entries(localOverrides)
|
||||
.filter(([, s]) => s === status)
|
||||
.map(([id]) => Object.values(ordersByStatus).flat().find((o) => o.id === id))
|
||||
.filter(Boolean);
|
||||
acc[status] = [...acc[status], ...overrideOrders];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '60px', color: 'var(--text-muted)', fontFamily: 'var(--font-heading)' }}>
|
||||
@@ -58,7 +90,7 @@ const KanbanBoard = () => {
|
||||
<KanbanColumn
|
||||
key={status}
|
||||
status={status}
|
||||
orders={ordersByStatus[status] || []}
|
||||
orders={effectiveOrdersByStatus[status] || []}
|
||||
onCardClick={setSelectedOrder}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -57,6 +57,17 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
|
||||
}
|
||||
}, [order?.id]);
|
||||
|
||||
const modalRef = React.useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKey = (e) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
document.addEventListener('keydown', handleKey);
|
||||
modalRef.current?.focus();
|
||||
return () => document.removeEventListener('keydown', handleKey);
|
||||
}, [onClose]);
|
||||
|
||||
const set = (e) => setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -76,7 +87,13 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
|
||||
} else {
|
||||
const statusChanged = form.status !== order.status;
|
||||
const updates = { ...form };
|
||||
if (statusChanged && form.status === 'закрыт') updates.closed_at = new Date().toISOString();
|
||||
if (statusChanged) {
|
||||
if (form.status === 'закрыт') {
|
||||
updates.closed_at = new Date().toISOString();
|
||||
} else if (order.status === 'закрыт') {
|
||||
updates.closed_at = null;
|
||||
}
|
||||
}
|
||||
|
||||
const { error } = await supabase.from('orders').update(updates).eq('id', order.id);
|
||||
if (error) { setError(error.message); setSaving(false); return; }
|
||||
@@ -110,6 +127,8 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
ref={modalRef}
|
||||
tabIndex={-1}
|
||||
className="glass-panel"
|
||||
style={{ width: '100%', maxWidth: '620px', maxHeight: '90vh', overflow: 'auto', padding: '30px', margin: '20px' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -157,7 +176,7 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="modal-thickness" style={labelStyle}>Толщина плёнки</label>
|
||||
<select id="modal-thickness" name="film_thickness" value={form.film_thickness} onChange={set} style={{ ...inputStyle, cursor: 'pointer' }}>
|
||||
<select id="modal-thickness" name="film_thickness" value={form.film_thickness} onChange={(e) => setForm((prev) => ({ ...prev, film_thickness: Number(e.target.value) }))} style={{ ...inputStyle, cursor: 'pointer' }}>
|
||||
<option value={200}>200 микрон</option>
|
||||
<option value={300}>300 микрон</option>
|
||||
</select>
|
||||
@@ -205,7 +224,7 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
|
||||
{events.map((e) => (
|
||||
<div key={e.id} style={{ fontSize: '0.8rem', color: 'var(--text-muted)', display: 'flex', gap: '10px', alignItems: 'baseline' }}>
|
||||
<span style={{ color: 'var(--accent-blue)', fontWeight: 600, flexShrink: 0 }}>
|
||||
{new Date(e.created_at).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
|
||||
{new Date(e.created_at).toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
<span style={{ flex: 1 }}>{e.description}</span>
|
||||
{e.created_by && (
|
||||
|
||||
Reference in New Issue
Block a user