Files
delivery-tracker/frontend/src/pages/Dashboard.tsx

249 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;