feat: calendar — assign existing order to date via picker

This commit is contained in:
2026-05-11 08:54:21 +03:00
parent 4d4e84b5ed
commit 9d060a2dc9
+188 -14
View File
@@ -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>
);
};