249 lines
9.5 KiB
TypeScript
249 lines
9.5 KiB
TypeScript
import { useState, useEffect, useMemo } from 'react';
|
||
import { Plus, Printer, ChevronRight, CalendarDays } from 'lucide-react';
|
||
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isToday, getDay } from 'date-fns';
|
||
import { ru } from 'date-fns/locale';
|
||
import { useDeliveryStore } from '../stores/deliveryStore';
|
||
import type { Delivery } from '../types';
|
||
import { pickupLocationLabels } from '../types';
|
||
import { Button } from '../components/ui/Button';
|
||
import { Card } from '../components/ui/Card';
|
||
|
||
interface DashboardProps {
|
||
onDateSelect: (date: string) => void;
|
||
onAddDelivery: () => void;
|
||
}
|
||
|
||
const Dashboard = ({ onDateSelect, onAddDelivery }: DashboardProps) => {
|
||
const deliveryCounts = useDeliveryStore(state => state.deliveryCounts);
|
||
const fetchDeliveryCounts = useDeliveryStore(state => state.fetchDeliveryCounts);
|
||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||
|
||
// Fetch counts on mount
|
||
useEffect(() => {
|
||
fetchDeliveryCounts();
|
||
}, [fetchDeliveryCounts]);
|
||
|
||
const days = useMemo(() => {
|
||
const monthStart = startOfMonth(currentMonth);
|
||
const monthEnd = endOfMonth(currentMonth);
|
||
return eachDayOfInterval({ start: monthStart, end: monthEnd });
|
||
}, [currentMonth]);
|
||
|
||
const getCountForDate = (date: Date) => {
|
||
const dateStr = format(date, 'dd-MM-yyyy');
|
||
return deliveryCounts[dateStr] || 0;
|
||
};
|
||
|
||
const handlePrintDay = (date: Date) => {
|
||
const dateStr = format(date, 'dd-MM-yyyy');
|
||
const fetchDeliveriesByDate = useDeliveryStore.getState().fetchDeliveriesByDate;
|
||
|
||
// Fetch and print
|
||
fetchDeliveriesByDate(dateStr).then(() => {
|
||
const deliveries = useDeliveryStore.getState().deliveries;
|
||
printDeliveries(date, deliveries);
|
||
});
|
||
};
|
||
|
||
const printDeliveries = (date: Date, dayDeliveries: Delivery[]) => {
|
||
|
||
const printWindow = window.open('', '_blank');
|
||
if (!printWindow) return;
|
||
|
||
const html = `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Доставки на ${format(date, 'dd MMMM yyyy', { locale: ru })}</title>
|
||
<style>
|
||
body { font-family: system-ui, sans-serif; margin: 20px; }
|
||
h1 { font-size: 18px; margin-bottom: 16px; }
|
||
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||
th, td { text-align: left; padding: 6px; border-bottom: 1px solid #ddd; }
|
||
th { font-weight: 600; background: #f5f5f5; }
|
||
.address-details { font-size: 11px; color: #666; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>Доставки на ${format(date, 'dd MMMM yyyy', { locale: ru })}</h1>
|
||
<table>
|
||
<tr>
|
||
<th>Загрузка</th>
|
||
<th>Товар</th>
|
||
<th>Клиент</th>
|
||
<th>Адрес</th>
|
||
<th>Телефон</th>
|
||
<th>Услуги</th>
|
||
<th>Комментарий</th>
|
||
</tr>
|
||
${dayDeliveries.map((d: Delivery) => `
|
||
<tr>
|
||
<td>${d.pickupLocation2 ? pickupLocationLabels[d.pickupLocation] + ' + ' + pickupLocationLabels[d.pickupLocation2] : pickupLocationLabels[d.pickupLocation]}</td>
|
||
<td>${d.productName}${d.productName2 ? '<br><small>+ ' + d.productName2 + '</small>' : ''}</td>
|
||
<td>${d.customerName}</td>
|
||
<td>
|
||
ул. ${d.street}, д. ${d.house}${d.apartment ? ', кв. ' + d.apartment : ''}
|
||
${d.entrance || d.floor ? '<br><span class="address-details">' + (d.entrance ? 'Подъезд ' + d.entrance : '') + (d.entrance && d.floor ? ', ' : '') + (d.floor ? 'этаж ' + d.floor : '') + '</span>' : ''}
|
||
</td>
|
||
<td>${d.phone}</td>
|
||
<td>${d.serviceInfo || '-'}</td>
|
||
<td>${d.comment || '-'}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</table>
|
||
</body>
|
||
</html>
|
||
`;
|
||
|
||
printWindow.document.write(html);
|
||
printWindow.document.close();
|
||
printWindow?.print();
|
||
};
|
||
|
||
const navigateMonth = (direction: 'prev' | 'next') => {
|
||
setCurrentMonth(prev => {
|
||
const newDate = new Date(prev);
|
||
newDate.setMonth(prev.getMonth() + (direction === 'next' ? 1 : -1));
|
||
return newDate;
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-[#1b1b1d]">Панель управления</h1>
|
||
<p className="text-[#75777d] mt-1">Выберите дату для просмотра доставок</p>
|
||
</div>
|
||
<Button onClick={onAddDelivery}>
|
||
<Plus size={18} className="mr-2" />
|
||
Новая доставка
|
||
</Button>
|
||
</div>
|
||
|
||
<Card className="p-6">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h2 className="text-lg font-semibold text-[#1b1b1d] flex items-center gap-2">
|
||
<CalendarDays size={20} className="text-[#1B263B]" />
|
||
{format(currentMonth, 'LLLL yyyy', { locale: ru })}
|
||
</h2>
|
||
<div className="flex gap-2">
|
||
<Button variant="ghost" size="sm" onClick={() => navigateMonth('prev')}>
|
||
←
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => setCurrentMonth(new Date())}
|
||
>
|
||
Сегодня
|
||
</Button>
|
||
<Button variant="ghost" size="sm" onClick={() => navigateMonth('next')}>
|
||
→
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-7 gap-1 mb-2">
|
||
{['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].map(day => (
|
||
<div key={day} className="text-center text-xs font-medium text-[#75777d] py-2">
|
||
{day}
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="grid grid-cols-7 gap-1">
|
||
{Array.from({ length: (getDay(startOfMonth(currentMonth)) + 6) % 7 }).map((_, i) => (
|
||
<div key={`empty-${i}`} className="p-3 min-h-[80px]" />
|
||
))}
|
||
{days.map((day) => {
|
||
const count = getCountForDate(day);
|
||
const isTodayDate = isToday(day);
|
||
|
||
return (
|
||
<button
|
||
key={day.toISOString()}
|
||
onClick={() => onDateSelect(format(day, 'dd-MM-yyyy'))}
|
||
className={`
|
||
relative p-3 rounded-lg text-left transition-all min-h-[80px]
|
||
${isTodayDate ? 'bg-[#1B263B] text-white' : 'hover:bg-[#f5f3f5]'}
|
||
${!isTodayDate && count > 0 ? 'bg-[#ffdcc3]/30' : ''}
|
||
`}
|
||
>
|
||
<div className={`text-sm font-medium ${isTodayDate ? 'text-white' : 'text-[#1b1b1d]'}`}>
|
||
{format(day, 'd')}
|
||
</div>
|
||
{count > 0 && (
|
||
<div className={`mt-1 text-[10px] truncate w-full ${isTodayDate ? 'text-white/80' : 'text-[#F28C28]'}`}>
|
||
{count} {count === 1 ? 'доставка' : count < 5 ? 'доставки' : 'доставок'}
|
||
</div>
|
||
)}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</Card>
|
||
|
||
<div className="space-y-3">
|
||
<h3 className="font-semibold text-[#1b1b1d]">Ближайшие даты с доставками</h3>
|
||
{days
|
||
.filter(day => getCountForDate(day) > 0)
|
||
.slice(0, 7)
|
||
.map(day => {
|
||
const count = getCountForDate(day);
|
||
return (
|
||
<Card key={day.toISOString()} className="flex items-center justify-between p-4">
|
||
<div className="flex items-center gap-4">
|
||
<div className="text-center min-w-[60px]">
|
||
<div className="text-2xl font-bold text-[#1B263B]">
|
||
{format(day, 'd')}
|
||
</div>
|
||
<div className="text-xs text-[#75777d] uppercase">
|
||
{format(day, 'MMM', { locale: ru })}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div className="font-medium text-[#1b1b1d]">
|
||
{count} {count === 1 ? 'доставка' : count < 5 ? 'доставки' : 'доставок'}
|
||
</div>
|
||
<div className="text-sm text-[#75777d]">
|
||
{format(day, 'EEEE', { locale: ru })}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handlePrintDay(day)}
|
||
>
|
||
<Printer size={16} />
|
||
</Button>
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
onClick={() => onDateSelect(format(day, 'dd-MM-yyyy'))}
|
||
>
|
||
Открыть
|
||
<ChevronRight size={16} className="ml-1" />
|
||
</Button>
|
||
</div>
|
||
</Card>
|
||
);
|
||
})}
|
||
|
||
{days.filter(day => getCountForDate(day) > 0).length === 0 && (
|
||
<Card className="p-8 text-center">
|
||
<p className="text-[#75777d]">Нет запланированных доставок</p>
|
||
<Button onClick={onAddDelivery} variant="ghost" className="mt-2">
|
||
Создать первую доставку
|
||
</Button>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default Dashboard;
|