feat: calendar — assign existing order to date via picker
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Plus } from 'lucide-react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Plus, Link2, Search, X } from 'lucide-react';
|
||||
import { useCalendar } from './useCalendar';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile';
|
||||
import OrderModal from '../orders/OrderModal';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
const MONTH_NAMES = [
|
||||
'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
|
||||
@@ -140,7 +141,156 @@ function DayCell({ date, orders, isCurrentMonth, isToday, isSelected, onSelect,
|
||||
);
|
||||
}
|
||||
|
||||
function DayDetail({ date, orders, onOrderClick, onAdd }) {
|
||||
function OrderPicker({ date, onAssign, onClose }) {
|
||||
const [query, setQuery] = useState('');
|
||||
const [allOrders, setAllOrders] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [assigning, setAssigning] = useState(null);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
supabase.from('orders')
|
||||
.select('id, name, phone, status, scheduled_at')
|
||||
.order('created_at', { ascending: false })
|
||||
.then(({ data }) => { if (isMounted) { setAllOrders(data ?? []); setLoading(false); } });
|
||||
return () => { isMounted = false; };
|
||||
}, []);
|
||||
|
||||
useEffect(() => { inputRef.current?.focus(); }, []);
|
||||
|
||||
const q = query.trim().toLowerCase();
|
||||
const filtered = q
|
||||
? allOrders.filter((o) =>
|
||||
o.name?.toLowerCase().includes(q) || o.phone?.toLowerCase().includes(q)
|
||||
)
|
||||
: allOrders;
|
||||
|
||||
const handleAssign = async (order) => {
|
||||
setAssigning(order.id);
|
||||
// Set time to noon on the selected date
|
||||
const d = new Date(date);
|
||||
d.setHours(12, 0, 0, 0);
|
||||
await supabase.from('orders').update({ scheduled_at: d.toISOString() }).eq('id', order.id);
|
||||
onAssign();
|
||||
};
|
||||
|
||||
const dateLabel = date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long' });
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'fixed', inset: 0, zIndex: 1000,
|
||||
background: 'rgba(0,0,0,0.4)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
padding: '16px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
background: '#fff', borderRadius: '16px', width: '100%', maxWidth: '460px',
|
||||
boxShadow: '0 8px 40px rgba(0,0,0,0.18)', overflow: 'hidden',
|
||||
display: 'flex', flexDirection: 'column', maxHeight: '80vh',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div style={{ padding: '20px 20px 16px', borderBottom: '1px solid #E9ECEF', flexShrink: 0 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-heading)', fontWeight: 700, fontSize: '1rem' }}>
|
||||
Привязать заявку
|
||||
</div>
|
||||
<div style={{ fontSize: '0.78rem', color: '#8C9097', marginTop: '2px' }}>
|
||||
к {dateLabel}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#8C9097', padding: '4px' }}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Search size={15} style={{ position: 'absolute', left: '10px', top: '50%', transform: 'translateY(-50%)', color: '#8C9097', pointerEvents: 'none' }} />
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Имя или телефон..."
|
||||
style={{
|
||||
width: '100%', padding: '9px 12px 9px 34px',
|
||||
border: '1.5px solid #E9ECEF', borderRadius: '8px',
|
||||
fontSize: '0.88rem', outline: 'none', boxSizing: 'border-box',
|
||||
fontFamily: 'var(--font-body)',
|
||||
}}
|
||||
onFocus={(e) => e.target.style.borderColor = '#4361EE'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#E9ECEF'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div style={{ overflowY: 'auto', flex: 1 }}>
|
||||
{loading && (
|
||||
<div style={{ padding: '24px', textAlign: 'center', color: '#8C9097', fontSize: '0.85rem' }}>
|
||||
Загрузка...
|
||||
</div>
|
||||
)}
|
||||
{!loading && filtered.length === 0 && (
|
||||
<div style={{ padding: '24px', textAlign: 'center', color: '#8C9097', fontSize: '0.85rem' }}>
|
||||
Ничего не найдено
|
||||
</div>
|
||||
)}
|
||||
{filtered.map((o) => {
|
||||
const color = getStatusColor(o.status);
|
||||
const busy = assigning === o.id;
|
||||
return (
|
||||
<div
|
||||
key={o.id}
|
||||
onClick={() => !assigning && handleAssign(o)}
|
||||
style={{
|
||||
padding: '12px 20px',
|
||||
display: 'flex', alignItems: 'center', gap: '12px',
|
||||
borderBottom: '1px solid #F3F4F6',
|
||||
cursor: assigning ? 'wait' : 'pointer',
|
||||
opacity: assigning && !busy ? 0.5 : 1,
|
||||
transition: 'background 0.1s',
|
||||
}}
|
||||
onMouseEnter={(e) => { if (!assigning) e.currentTarget.style.background = '#F5F7FA'; }}
|
||||
onMouseLeave={(e) => e.currentTarget.style.background = ''}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: '0.9rem', marginBottom: '2px' }}>{o.name}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#8C9097' }}>
|
||||
{o.phone}
|
||||
{o.scheduled_at && (
|
||||
<span style={{ marginLeft: '8px' }}>
|
||||
· сейчас: {new Date(o.scheduled_at).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span style={{
|
||||
background: color + '1A', color,
|
||||
fontSize: '0.7rem', fontWeight: 700,
|
||||
padding: '2px 8px', borderRadius: '10px',
|
||||
textTransform: 'uppercase', letterSpacing: '0.3px', flexShrink: 0,
|
||||
}}>
|
||||
{busy ? '...' : o.status}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DayDetail({ date, orders, onOrderClick, onAdd, onAssign }) {
|
||||
const dayOrders = orders.filter((o) => o.scheduled_at && isSameDay(new Date(o.scheduled_at), date));
|
||||
|
||||
return (
|
||||
@@ -199,17 +349,31 @@ function DayDetail({ date, orders, onOrderClick, onAdd }) {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
onClick={onAdd}
|
||||
className="btn-primary"
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: '6px',
|
||||
alignSelf: 'flex-start', marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
<Plus size={15} />
|
||||
Добавить заявку
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginTop: '4px' }}>
|
||||
<button
|
||||
onClick={onAdd}
|
||||
className="btn-primary"
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: '6px' }}
|
||||
>
|
||||
<Plus size={15} />
|
||||
Новая заявка
|
||||
</button>
|
||||
<button
|
||||
onClick={onAssign}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: '6px',
|
||||
padding: '8px 16px', borderRadius: '8px', cursor: 'pointer',
|
||||
background: 'transparent', border: '1.5px solid #E9ECEF',
|
||||
fontSize: '0.88rem', fontWeight: 600, color: '#1E1E2D',
|
||||
fontFamily: 'var(--font-heading)',
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.borderColor = '#4361EE'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.borderColor = '#E9ECEF'}
|
||||
>
|
||||
<Link2 size={15} />
|
||||
Привязать заявку
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -221,6 +385,7 @@ const CalendarPage = () => {
|
||||
const [selectedDate, setSelectedDate] = useState(today);
|
||||
const [editOrder, setEditOrder] = useState(null);
|
||||
const [createDate, setCreateDate] = useState(null);
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
const { orders, loading, refetch } = useCalendar(year, month);
|
||||
|
||||
@@ -361,6 +526,7 @@ const CalendarPage = () => {
|
||||
orders={orders}
|
||||
onOrderClick={setEditOrder}
|
||||
onAdd={() => setCreateDate(selectedDate)}
|
||||
onAssign={() => setShowPicker(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -382,6 +548,14 @@ const CalendarPage = () => {
|
||||
onSaved={handleModalSaved}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showPicker && (
|
||||
<OrderPicker
|
||||
date={selectedDate}
|
||||
onAssign={() => { setShowPicker(false); refetch(); }}
|
||||
onClose={() => setShowPicker(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user