feat: calendar — open/edit orders and add new order with pre-filled date
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { ChevronLeft, ChevronRight, Plus } from 'lucide-react';
|
||||
import { useCalendar } from './useCalendar';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile';
|
||||
import OrderModal from '../orders/OrderModal';
|
||||
|
||||
const MONTH_NAMES = [
|
||||
'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
|
||||
@@ -41,10 +42,11 @@ function isSameDay(a, b) {
|
||||
a.getDate() === b.getDate();
|
||||
}
|
||||
|
||||
function OrderChip({ order }) {
|
||||
function OrderChip({ order, onClick }) {
|
||||
const color = getStatusColor(order.status);
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => { e.stopPropagation(); onClick(order); }}
|
||||
title={`${order.name} — ${order.status}`}
|
||||
style={{
|
||||
background: color + '1A',
|
||||
@@ -58,6 +60,7 @@ function OrderChip({ order }) {
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '100%',
|
||||
lineHeight: 1.4,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{order.name}
|
||||
@@ -65,7 +68,7 @@ function OrderChip({ order }) {
|
||||
);
|
||||
}
|
||||
|
||||
function DayCell({ date, orders, isCurrentMonth, isToday, isSelected, onSelect, isMobile }) {
|
||||
function DayCell({ date, orders, isCurrentMonth, isToday, isSelected, onSelect, onOrderClick, isMobile }) {
|
||||
const dayOrders = orders.filter((o) => o.scheduled_at && isSameDay(new Date(o.scheduled_at), date));
|
||||
const visible = dayOrders.slice(0, 2);
|
||||
const extra = dayOrders.length - visible.length;
|
||||
@@ -110,7 +113,7 @@ function DayCell({ date, orders, isCurrentMonth, isToday, isSelected, onSelect,
|
||||
</span>
|
||||
|
||||
{!isMobile && visible.map((o) => (
|
||||
<OrderChip key={o.id} order={o} />
|
||||
<OrderChip key={o.id} order={o} onClick={onOrderClick} />
|
||||
))}
|
||||
|
||||
{isMobile && dayOrders.length > 0 && (
|
||||
@@ -137,25 +140,23 @@ function DayCell({ date, orders, isCurrentMonth, isToday, isSelected, onSelect,
|
||||
);
|
||||
}
|
||||
|
||||
function DayDetail({ date, orders }) {
|
||||
function DayDetail({ date, orders, onOrderClick, onAdd }) {
|
||||
const dayOrders = orders.filter((o) => o.scheduled_at && isSameDay(new Date(o.scheduled_at), date));
|
||||
|
||||
if (dayOrders.length === 0) {
|
||||
return (
|
||||
<div style={{ color: 'var(--text-muted, #8C9097)', fontSize: '0.85rem' }}>
|
||||
На этот день записей нет
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{dayOrders.length === 0 && (
|
||||
<div style={{ color: 'var(--text-muted, #8C9097)', fontSize: '0.85rem' }}>
|
||||
На этот день записей нет
|
||||
</div>
|
||||
)}
|
||||
{dayOrders.map((o) => {
|
||||
const color = getStatusColor(o.status);
|
||||
const time = new Date(o.scheduled_at).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
||||
return (
|
||||
<div
|
||||
key={o.id}
|
||||
onClick={() => onOrderClick(o)}
|
||||
className="glass-panel"
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
@@ -164,7 +165,11 @@ function DayDetail({ date, orders }) {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '12px',
|
||||
cursor: 'pointer',
|
||||
transition: 'box-shadow 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.boxShadow = ''}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontFamily: 'var(--font-heading)', fontWeight: 700, fontSize: '0.92rem', marginBottom: '2px' }}>
|
||||
@@ -194,6 +199,17 @@ function DayDetail({ date, orders }) {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -203,8 +219,13 @@ const CalendarPage = () => {
|
||||
const [year, setYear] = useState(today.getFullYear());
|
||||
const [month, setMonth] = useState(today.getMonth());
|
||||
const [selectedDate, setSelectedDate] = useState(today);
|
||||
const [editOrder, setEditOrder] = useState(null);
|
||||
const [createDate, setCreateDate] = useState(null);
|
||||
const isMobile = useIsMobile();
|
||||
const { orders, loading } = useCalendar(year, month);
|
||||
const { orders, loading, refetch } = useCalendar(year, month);
|
||||
|
||||
const handleModalClose = () => { setEditOrder(null); setCreateDate(null); };
|
||||
const handleModalSaved = () => { handleModalClose(); refetch(); };
|
||||
|
||||
const prevMonth = () => {
|
||||
if (month === 0) { setYear((y) => y - 1); setMonth(11); }
|
||||
@@ -317,6 +338,7 @@ const CalendarPage = () => {
|
||||
isToday={isSameDay(date, today)}
|
||||
isSelected={isSameDay(date, selectedDate)}
|
||||
onSelect={setSelectedDate}
|
||||
onOrderClick={setEditOrder}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
))}
|
||||
@@ -333,8 +355,33 @@ const CalendarPage = () => {
|
||||
}}>
|
||||
{selectedDateLabel}
|
||||
</h3>
|
||||
{loading ? null : <DayDetail date={selectedDate} orders={orders} />}
|
||||
{loading ? null : (
|
||||
<DayDetail
|
||||
date={selectedDate}
|
||||
orders={orders}
|
||||
onOrderClick={setEditOrder}
|
||||
onAdd={() => setCreateDate(selectedDate)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editOrder && (
|
||||
<OrderModal
|
||||
order={editOrder}
|
||||
mode="edit"
|
||||
onClose={handleModalClose}
|
||||
onSaved={handleModalSaved}
|
||||
/>
|
||||
)}
|
||||
|
||||
{createDate && (
|
||||
<OrderModal
|
||||
order={{ scheduled_at: createDate.toISOString() }}
|
||||
mode="create"
|
||||
onClose={handleModalClose}
|
||||
onSaved={handleModalSaved}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
export function useCalendar(year, month) {
|
||||
const [orders, setOrders] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [trigger, setTrigger] = useState(0);
|
||||
|
||||
const refetch = useCallback(() => setTrigger((t) => t + 1), []);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
setLoading(true);
|
||||
const from = new Date(year, month, 1).toISOString();
|
||||
const to = new Date(year, month + 1, 0, 23, 59, 59).toISOString();
|
||||
|
||||
@@ -16,10 +21,11 @@ export function useCalendar(year, month) {
|
||||
.lte('scheduled_at', to)
|
||||
.order('scheduled_at', { ascending: true })
|
||||
.then(({ data }) => {
|
||||
setOrders(data || []);
|
||||
setLoading(false);
|
||||
if (isMounted) { setOrders(data || []); setLoading(false); }
|
||||
});
|
||||
}, [year, month]);
|
||||
|
||||
return { orders, loading };
|
||||
return () => { isMounted = false; };
|
||||
}, [year, month, trigger]);
|
||||
|
||||
return { orders, loading, refetch };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user