chore: restructure project into backend and frontend folders
- Move all frontend code to frontend/ directory - Add backend/ with Go project structure (cmd, internal, pkg) - Add docker-compose.yml for orchestration
This commit is contained in:
220
frontend/src/pages/Dashboard.tsx
Normal file
220
frontend/src/pages/Dashboard.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import { useState } from 'react';
|
||||
import { Plus, Printer, ChevronRight, CalendarDays } from 'lucide-react';
|
||||
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isToday } from 'date-fns';
|
||||
import { ru } from 'date-fns/locale';
|
||||
import { useDeliveryStore } from '../stores/deliveryStore';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Card } from '../components/ui/Card';
|
||||
|
||||
interface DashboardProps {
|
||||
onDateSelect: (date: string) => void;
|
||||
onAddDelivery: () => void;
|
||||
}
|
||||
|
||||
export const Dashboard = ({ onDateSelect, onAddDelivery }: DashboardProps) => {
|
||||
const deliveries = useDeliveryStore(state => state.deliveries);
|
||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||
|
||||
const monthStart = startOfMonth(currentMonth);
|
||||
const monthEnd = endOfMonth(currentMonth);
|
||||
const days = eachDayOfInterval({ start: monthStart, end: monthEnd });
|
||||
|
||||
const getCountForDate = (date: Date) => {
|
||||
const dateStr = format(date, 'dd-MM-yyyy');
|
||||
return deliveries.filter(d => d.date === dateStr).length;
|
||||
};
|
||||
|
||||
const handlePrintDay = (date: Date) => {
|
||||
const dateStr = format(date, 'dd-MM-yyyy');
|
||||
const dayDeliveries = deliveries.filter(d => d.date === dateStr);
|
||||
|
||||
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; }
|
||||
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
|
||||
th { font-weight: 600; background: #f5f5f5; }
|
||||
.status-new { background: #ffdcc3; padding: 2px 8px; border-radius: 12px; font-size: 12px; }
|
||||
.status-delivered { background: #dcfce7; padding: 2px 8px; border-radius: 12px; font-size: 12px; }
|
||||
</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>
|
||||
</tr>
|
||||
${dayDeliveries.map(d => `
|
||||
<tr>
|
||||
<td><span class="status-${d.status}">${d.status === 'new' ? 'Новое' : 'Доставлено'}</span></td>
|
||||
<td>${d.pickupLocation === 'warehouse' ? 'Склад' : d.pickupLocation === 'symbat' ? 'Сымбат' : d.pickupLocation === 'nursaya' ? 'Нурсая' : 'Галактика'}</td>
|
||||
<td>${d.productName}</td>
|
||||
<td>${d.address}</td>
|
||||
<td>${d.phone}</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, 'MMMM 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">
|
||||
{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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user