Apply 7 UI fixes: center badges, hide not-ready, customer address for pickup, compact calendars, confirm modals, hide driver panel for pickup, format dates

This commit is contained in:
root 2026-06-12 12:49:48 +00:00
parent fe2d8c4e9b
commit 005d4467bc
2 changed files with 77 additions and 28 deletions

View File

@ -54,6 +54,23 @@ import {
const DELIVERY_TIME_OPTIONS = ["Первая половина дня", "Вторая половина дня"];
const WEEK_DAY_LABELS = ["ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ", "ВС"];
const STATUS_LABELS = { pending_confirmation: 'Ожидает согласования', agreed: 'Согласовано', driver_assigned: 'Назначен водитель', loaded: 'Загружено', on_route: 'В пути', delivered: 'Доставлено', pickup: 'Самовывоз', requires_address: 'Требуется адрес', problem: 'Проблема', cancelled: 'Отменено' };
const ConfirmModal = ({ open, title, message, onConfirm, onCancel }) => {
if (!open) return null;
return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/40" onClick={onCancel}>
<div className="mx-4 w-full max-w-sm rounded-[24px] bg-[var(--color-surface-strong)] p-6 shadow-xl" onClick={e => e.stopPropagation()}>
{title && <h3 className="text-lg font-semibold">{title}</h3>}
{message && <p className="mt-2 text-sm text-[var(--color-text-muted)]">{message}</p>}
<div className="mt-5 flex justify-end gap-3">
<button type="button" className="rounded-xl border border-[var(--color-border)] px-4 py-2 text-sm font-medium text-[var(--color-text-muted)] hover:bg-[var(--color-surface)]" onClick={onCancel}>Отмена</button>
<button type="button" className="rounded-xl bg-[var(--color-accent)] px-4 py-2 text-sm font-medium text-white hover:opacity-90" onClick={onConfirm}>Подтвердить</button>
</div>
</div>
</div>
);
};
const DELIVERY_TIME_ALIASES = {
"До обеда": "Первая половина дня",
"После обеда": "Вторая половина дня",
@ -558,6 +575,7 @@ export const OrderDetailPanel = ({
const [pickupDate, setPickupDate] = React.useState(order?.pickupDate || "");
const [pickupTimeSlot, setPickupTimeSlot] = React.useState(DELIVERY_TIME_OPTIONS[0]);
const [deliveryAddress, setDeliveryAddress] = React.useState(order?.deliveryAddress || order?.customerAddress || "");
const [confirmAction, setConfirmAction] = React.useState(null);
const minSelectableDateKey = React.useMemo(() => getNextSelectableDateKey(), []);
const [currentMonth, setCurrentMonth] = React.useState(() => {
const existingDeliveryDate = fromDateKey(order?.deliveryDate);
@ -709,11 +727,10 @@ export const OrderDetailPanel = ({
: "Доставка";
const dateLabel = isPickup ? "Дата самовывоза" : "Дата доставки";
const timeLabel = isPickup ? "Время самовывоза" : "Время доставки";
const addressLabel = isPickup ? "Адрес самовывоза" : "Адрес доставки";
// For pickup orders, hide the delivery address if it's just a placeholder like "Самовывоз"
const addressLabel = isPickup ? "Адрес клиента" : "Адрес доставки";
const effectiveAddress = isPickup
? (order.deliveryAddress && order.deliveryAddress !== "Самовывоз" && order.deliveryAddress !== "самовывоз" ? order.deliveryAddress : "")
: order.deliveryAddress;
? (order.customerAddress || "")
: (order.deliveryAddress || "");
return (
<div className="grid gap-3 rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4 md:grid-cols-4">
<div>
@ -809,10 +826,12 @@ export const OrderDetailPanel = ({
<p className="text-xs text-[var(--color-text-muted)]">Готово</p>
<p className="font-medium !text-[var(--color-text)]">{order.readyCount ?? 0}</p>
</div>
<div>
<p className="text-xs text-[var(--color-text-muted)]">Не готово</p>
<p className="font-medium !text-[var(--color-text)]">{order.notReadyCount ?? 0}</p>
</div>
{(order.notReadyCount ?? 0) > 0 ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">Не готово</p>
<p className="font-medium !text-[var(--color-text)]">{order.notReadyCount}</p>
</div>
) : null}
<div>
<p className="text-xs text-[var(--color-text-muted)]">Обновлена</p>
<p className="font-medium !text-[var(--color-text)]">{formatDateTime(order.updatedAt)}</p>
@ -908,7 +927,7 @@ export const OrderDetailPanel = ({
</div>
) : deliveryType === "delivery" ? (
<div className="flex flex-col gap-3 md:flex-row md:items-start md:relative md:z-10">
<div className="space-y-3 md:relative md:z-30 md:min-w-0 md:flex-1 md:pr-4">
<div className="relative space-y-3 md:min-w-0 md:flex-1 md:pr-4">
<button
type="button"
aria-label="Дата доставки"
@ -920,7 +939,7 @@ export const OrderDetailPanel = ({
<span aria-hidden="true" className="text-[var(--color-text-muted)]"></span>
</button>
{isCalendarOpen ? (
<div className="rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface)] p-4 shadow-soft md:absolute md:left-0 md:top-full md:z-50 md:mt-3 md:w-[min(460px,calc(100vw-3rem))]">
<div className="rounded-[20px] border border-[var(--color-border)] bg-[var(--color-surface)] p-3 shadow-soft absolute left-0 top-full z-50 mt-2 w-[300px]">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
@ -1039,6 +1058,7 @@ export const OrderDetailPanel = ({
</div>
) : (
<div className="space-y-3">
<div className="relative">
<button
type="button"
aria-label="Дата самовывоза"
@ -1050,7 +1070,7 @@ export const OrderDetailPanel = ({
<span aria-hidden="true" className="text-[var(--color-text-muted)]"></span>
</button>
{isCalendarOpen ? (
<div className="rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface)] p-4 shadow-soft md:relative md:z-50">
<div className="rounded-[20px] border border-[var(--color-border)] bg-[var(--color-surface)] p-3 shadow-soft absolute left-0 top-full z-50 mt-2 w-[300px]">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">Календарь самовывоза</p>
@ -1084,6 +1104,7 @@ export const OrderDetailPanel = ({
<p className="mt-2 text-xs text-[var(--color-text-muted)]">Выходные отмечены пунктиром и недоступны.</p>
</div>
) : null}
</div>
<div className="grid gap-2 sm:grid-cols-2">
{DELIVERY_TIME_OPTIONS.map((option) => (
<button key={option} type="button" aria-pressed={pickupTimeSlot === option} className={["min-h-[54px] rounded-2xl border px-4 text-left text-sm font-medium transition", pickupTimeSlot === option ? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] !text-[var(--color-text)]" : "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)] hover:!text-[var(--color-text)]"].join(" ")} onClick={() => { setPickupTimeSlot(option); setFormMessage(""); }}>
@ -1095,7 +1116,7 @@ export const OrderDetailPanel = ({
)}
<Button
className="w-full md:w-[180px] md:flex-none md:self-start"
onClick={handleSaveDeliveryChoice}
onClick={() => setConfirmAction({ type: 'delivery' })}
disabled={isSavingDeliveryChoice}
>
{isSavingDeliveryChoice ? "Сохраняем..." : "Согласовать"}
@ -1107,7 +1128,7 @@ export const OrderDetailPanel = ({
) : null}
{canManageDelivery && ["manager", "logistician", "admin", "mega_admin"].includes(userRole) ? (
{canManageDelivery && ["manager", "logistician", "admin", "mega_admin"].includes(userRole) && (order.deliveryType !== "pickup" && order.deliveryStatus !== "pickup" && order.delivery_status !== "pickup") ? (
<Panel className="space-y-4 p-5">
<div>
<strong>Назначение водителя</strong>
@ -1159,7 +1180,7 @@ export const OrderDetailPanel = ({
</Select>
<Button
className="md:px-4 md:py-2 md:whitespace-nowrap md:self-start"
onClick={handleAssignDriver}
onClick={() => setConfirmAction({ type: 'driver' })}
disabled={isSavingDeliveryChoice || !selectedDriverId}
>
{isSavingDeliveryChoice ? "Назначаем..." : "Назначить"}
@ -1206,17 +1227,7 @@ export const OrderDetailPanel = ({
setFormMessage(statusOption.hint || "");
return;
}
setFormMessage("");
onChangeDeliveryStatus({
orderGroupId: order.id,
status: statusOption.value,
}).then((response) => {
if (!response.success) {
setFormMessage(response.error || "Не удалось обновить статус");
} else {
setFormMessage("");
}
});
setConfirmAction({ type: 'status', status: statusOption.value });
}}
disabled={isSavingDeliveryChoice}
>
@ -1427,6 +1438,37 @@ export const OrderDetailPanel = ({
</div>
</Panel>
) : null}
<ConfirmModal
open={confirmAction?.type === 'delivery'}
title="Согласовать доставку?"
message={deliveryType === 'pickup' ? `Подтвердите самовывоз ${pickupDate ? formatDateForDisplay(pickupDate) : ''} ${pickupTimeSlot || ''}` : `Подтвердите доставку ${deliveryDate ? formatDateForDisplay(deliveryDate) : ''} ${deliveryTime || ''}`}
onConfirm={() => { setConfirmAction(null); handleSaveDeliveryChoice(); }}
onCancel={() => setConfirmAction(null)}
/>
<ConfirmModal
open={confirmAction?.type === 'driver'}
title="Назначить водителя?"
message={`Назначить ${drivers.find(d => d.id === selectedDriverId)?.name || 'выбранного водителя'} на эту группу?`}
onConfirm={() => { setConfirmAction(null); handleAssignDriver(); }}
onCancel={() => setConfirmAction(null)}
/>
<ConfirmModal
open={confirmAction?.type === 'status'}
title="Изменить статус?"
message={`Установить статус «${STATUS_LABELS[confirmAction?.status] || confirmAction?.status}»?`}
onConfirm={() => {
const status = confirmAction.status;
setConfirmAction(null);
onChangeDeliveryStatus({ orderGroupId: order.id, status }).then((response) => {
if (!response.success) setFormMessage(response.error || 'Не удалось обновить статус');
else setFormMessage('');
});
}}
onCancel={() => setConfirmAction(null)}
/>
</div>
);
};

View File

@ -10,11 +10,18 @@ import {
const MAX_VISIBLE_INVOICES = 2;
const fmtDate = (d) => {
if (!d) return '';
const [y, m, day] = d.split('-');
if (!y || !m || !day) return d;
return `${day}.${m}.${y}`;
};
const buildGroupSummary = (group) => {
const orderCountLabel = `${group.ordersCount || 0} ${group.ordersCount === 1 ? "заказ" : group.ordersCount < 5 ? "заказа" : "заказов"}`;
const parts = [orderCountLabel];
if (group.deliveryDate) {
const datePart = group.deliveryTime ? `${group.deliveryDate} · ${group.deliveryTime}` : group.deliveryDate;
const datePart = group.deliveryTime ? `${fmtDate(group.deliveryDate)} · ${group.deliveryTime}` : fmtDate(group.deliveryDate);
parts.push(datePart);
}
if (group.assignedDriverName) {
@ -165,7 +172,7 @@ export const OrdersTable = ({
<td className="max-w-[260px] px-5 py-4 text-sm text-[var(--color-text-muted)]">
{renderOrderNumbers(group)}
</td>
<td className="px-5 py-4">
<td className="px-5 py-4 text-center">
<Badge tone={getOrderGroupStatusTone(group)}>{getOrderGroupDisplayStatusLabel(group)}</Badge>
</td>
<td className="px-5 py-4 text-sm">
@ -173,7 +180,7 @@ export const OrdersTable = ({
</td>
<td className="px-5 py-4 text-sm">
{group.deliveryDate ? (
<span>{group.deliveryDate}{group.deliveryTime ? <span className="text-[var(--color-text-muted)]"> · {group.deliveryTime}</span> : ""}</span>
<span>{fmtDate(group.deliveryDate)}{group.deliveryTime ? <span className="text-[var(--color-text-muted)]"> · {group.deliveryTime}</span> : ""}</span>
) : (
<span className="text-[var(--color-text-muted)]"></span>
)}