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:
Egor Pozharov
2026-04-14 13:14:28 +06:00
parent 11e12f964d
commit 4e0899d3ce
54 changed files with 779 additions and 0 deletions

View 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>
);
};