186 lines
7.0 KiB
JavaScript
186 lines
7.0 KiB
JavaScript
import React from "react";
|
||
import { Button } from "../UI/Button";
|
||
import { Panel } from "../UI/Panel";
|
||
|
||
const WEEK_DAYS = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
|
||
|
||
const startOfMonth = (date) => new Date(date.getFullYear(), date.getMonth(), 1);
|
||
const endOfMonth = (date) => new Date(date.getFullYear(), date.getMonth() + 1, 0);
|
||
const addMonths = (date, amount) => new Date(date.getFullYear(), date.getMonth() + amount, 1);
|
||
const formatDayKey = (date) => date.toISOString().slice(0, 10);
|
||
const labelMonth = (date) =>
|
||
date.toLocaleDateString("ru-RU", { month: "long", year: "numeric" });
|
||
|
||
const resolveOrderDay = (order) => order.deliverySlots[0]?.date || order.scheduledDelivery.slice(0, 10);
|
||
|
||
const buildCalendarDays = (currentMonth) => {
|
||
const firstDay = startOfMonth(currentMonth);
|
||
const lastDay = endOfMonth(currentMonth);
|
||
const firstWeekDay = (firstDay.getDay() + 6) % 7;
|
||
const totalDays = lastDay.getDate();
|
||
const cells = [];
|
||
|
||
for (let index = 0; index < firstWeekDay; index += 1) {
|
||
cells.push(null);
|
||
}
|
||
|
||
for (let day = 1; day <= totalDays; day += 1) {
|
||
cells.push(new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day));
|
||
}
|
||
|
||
while (cells.length % 7 !== 0) {
|
||
cells.push(null);
|
||
}
|
||
|
||
return cells;
|
||
};
|
||
|
||
export const OrdersCalendarView = ({ orders, onOpenOrder }) => {
|
||
const initialMonth = React.useMemo(() => {
|
||
if (!orders.length) {
|
||
return startOfMonth(new Date());
|
||
}
|
||
|
||
const firstOrderDate = new Date(`${resolveOrderDay(orders[0])}T00:00:00`);
|
||
return startOfMonth(firstOrderDate);
|
||
}, [orders]);
|
||
|
||
const [currentMonth, setCurrentMonth] = React.useState(initialMonth);
|
||
|
||
React.useEffect(() => {
|
||
setCurrentMonth(initialMonth);
|
||
}, [initialMonth]);
|
||
|
||
const calendarDays = React.useMemo(() => buildCalendarDays(currentMonth), [currentMonth]);
|
||
const ordersByDay = React.useMemo(
|
||
() =>
|
||
orders.reduce((accumulator, order) => {
|
||
const key = resolveOrderDay(order);
|
||
accumulator[key] = accumulator[key] || [];
|
||
accumulator[key].push(order);
|
||
return accumulator;
|
||
}, {}),
|
||
[orders],
|
||
);
|
||
const agendaDays = React.useMemo(
|
||
() =>
|
||
Object.entries(ordersByDay)
|
||
.sort(([left], [right]) => new Date(left) - new Date(right))
|
||
.map(([key, dayOrders]) => ({ key, dayOrders })),
|
||
[ordersByDay],
|
||
);
|
||
|
||
return (
|
||
<Panel className="space-y-5 p-6">
|
||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||
<div>
|
||
<h3 className="text-lg font-semibold">Календарь доставок</h3>
|
||
<p className="mt-2 text-sm text-[var(--color-text-muted)]">
|
||
Месячный вид по датам доставки. Клик по заказу открывает карточку.
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Button size="sm" variant="secondary" onClick={() => setCurrentMonth(addMonths(currentMonth, -1))}>
|
||
Назад
|
||
</Button>
|
||
<div className="min-w-[180px] text-center text-sm font-medium capitalize">
|
||
{labelMonth(currentMonth)}
|
||
</div>
|
||
<Button size="sm" variant="secondary" onClick={() => setCurrentMonth(addMonths(currentMonth, 1))}>
|
||
Вперёд
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-3 md:hidden">
|
||
<div className="text-xs uppercase tracking-[0.14em] text-[var(--color-text-muted)]">Заказы по дням</div>
|
||
{agendaDays.map(({ key, dayOrders }) => (
|
||
<div key={key} className="rounded-[22px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4">
|
||
<div className="mb-3 flex items-center justify-between gap-2">
|
||
<div className="text-sm font-semibold">
|
||
{new Date(`${key}T00:00:00`).toLocaleDateString("ru-RU", {
|
||
day: "numeric",
|
||
month: "long",
|
||
})}
|
||
</div>
|
||
<div className="text-xs text-[var(--color-text-muted)]">{dayOrders.length}</div>
|
||
</div>
|
||
<div className="space-y-2">
|
||
{dayOrders.map((order) => (
|
||
<button
|
||
key={order.id}
|
||
type="button"
|
||
onClick={() => onOpenOrder(order.id)}
|
||
className="w-full rounded-[16px] bg-[var(--color-surface)] px-3 py-3 text-left text-sm hover:bg-[var(--color-accent-soft)]"
|
||
>
|
||
<div className="font-medium">{order.orderNumber}</div>
|
||
<div className="mt-1 text-xs text-[var(--color-text-muted)]">{order.customer.name}</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="hidden md:block">
|
||
<div className="grid grid-cols-7 gap-3 text-xs uppercase tracking-[0.12em] text-[var(--color-text-muted)]">
|
||
{WEEK_DAYS.map((day) => (
|
||
<div key={day} className="px-2">
|
||
{day}
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="mt-3 grid grid-cols-7 gap-3">
|
||
{calendarDays.map((day, index) => {
|
||
if (!day) {
|
||
return (
|
||
<div
|
||
key={`empty-${index}`}
|
||
className="min-h-[132px] rounded-[24px] border border-dashed border-[var(--color-border)] bg-[var(--color-surface)]/50"
|
||
/>
|
||
);
|
||
}
|
||
|
||
const key = formatDayKey(day);
|
||
const dayOrders = ordersByDay[key] || [];
|
||
|
||
return (
|
||
<div
|
||
key={key}
|
||
className="min-h-[132px] rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-3"
|
||
>
|
||
<div className="mb-3 flex items-center justify-between gap-2">
|
||
<div className="text-sm font-semibold">{day.getDate()}</div>
|
||
<div className="text-xs text-[var(--color-text-muted)]">{dayOrders.length || ""}</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
{dayOrders.slice(0, 2).map((order) => (
|
||
<button
|
||
key={order.id}
|
||
type="button"
|
||
onClick={() => onOpenOrder(order.id)}
|
||
className="w-full rounded-[16px] bg-[var(--color-surface)] px-3 py-2 text-left text-sm hover:bg-[var(--color-accent-soft)]"
|
||
>
|
||
<div className="font-medium">{order.orderNumber}</div>
|
||
<div className="mt-1 text-xs text-[var(--color-text-muted)]">
|
||
{order.customer.name}
|
||
</div>
|
||
</button>
|
||
))}
|
||
{dayOrders.length > 2 ? (
|
||
<div className="px-1 text-xs text-[var(--color-text-muted)]">
|
||
Ещё {dayOrders.length - 2}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</Panel>
|
||
);
|
||
};
|