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:
parent
fe2d8c4e9b
commit
005d4467bc
|
|
@ -54,6 +54,23 @@ import {
|
||||||
|
|
||||||
const DELIVERY_TIME_OPTIONS = ["Первая половина дня", "Вторая половина дня"];
|
const DELIVERY_TIME_OPTIONS = ["Первая половина дня", "Вторая половина дня"];
|
||||||
const WEEK_DAY_LABELS = ["ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ", "ВС"];
|
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 = {
|
const DELIVERY_TIME_ALIASES = {
|
||||||
"До обеда": "Первая половина дня",
|
"До обеда": "Первая половина дня",
|
||||||
"После обеда": "Вторая половина дня",
|
"После обеда": "Вторая половина дня",
|
||||||
|
|
@ -558,6 +575,7 @@ export const OrderDetailPanel = ({
|
||||||
const [pickupDate, setPickupDate] = React.useState(order?.pickupDate || "");
|
const [pickupDate, setPickupDate] = React.useState(order?.pickupDate || "");
|
||||||
const [pickupTimeSlot, setPickupTimeSlot] = React.useState(DELIVERY_TIME_OPTIONS[0]);
|
const [pickupTimeSlot, setPickupTimeSlot] = React.useState(DELIVERY_TIME_OPTIONS[0]);
|
||||||
const [deliveryAddress, setDeliveryAddress] = React.useState(order?.deliveryAddress || order?.customerAddress || "");
|
const [deliveryAddress, setDeliveryAddress] = React.useState(order?.deliveryAddress || order?.customerAddress || "");
|
||||||
|
const [confirmAction, setConfirmAction] = React.useState(null);
|
||||||
const minSelectableDateKey = React.useMemo(() => getNextSelectableDateKey(), []);
|
const minSelectableDateKey = React.useMemo(() => getNextSelectableDateKey(), []);
|
||||||
const [currentMonth, setCurrentMonth] = React.useState(() => {
|
const [currentMonth, setCurrentMonth] = React.useState(() => {
|
||||||
const existingDeliveryDate = fromDateKey(order?.deliveryDate);
|
const existingDeliveryDate = fromDateKey(order?.deliveryDate);
|
||||||
|
|
@ -709,11 +727,10 @@ export const OrderDetailPanel = ({
|
||||||
: "Доставка";
|
: "Доставка";
|
||||||
const dateLabel = isPickup ? "Дата самовывоза" : "Дата доставки";
|
const dateLabel = isPickup ? "Дата самовывоза" : "Дата доставки";
|
||||||
const timeLabel = isPickup ? "Время самовывоза" : "Время доставки";
|
const timeLabel = isPickup ? "Время самовывоза" : "Время доставки";
|
||||||
const addressLabel = isPickup ? "Адрес самовывоза" : "Адрес доставки";
|
const addressLabel = isPickup ? "Адрес клиента" : "Адрес доставки";
|
||||||
// For pickup orders, hide the delivery address if it's just a placeholder like "Самовывоз"
|
|
||||||
const effectiveAddress = isPickup
|
const effectiveAddress = isPickup
|
||||||
? (order.deliveryAddress && order.deliveryAddress !== "Самовывоз" && order.deliveryAddress !== "самовывоз" ? order.deliveryAddress : "")
|
? (order.customerAddress || "")
|
||||||
: order.deliveryAddress;
|
: (order.deliveryAddress || "");
|
||||||
return (
|
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 className="grid gap-3 rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4 md:grid-cols-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -809,10 +826,12 @@ export const OrderDetailPanel = ({
|
||||||
<p className="text-xs text-[var(--color-text-muted)]">Готово</p>
|
<p className="text-xs text-[var(--color-text-muted)]">Готово</p>
|
||||||
<p className="font-medium !text-[var(--color-text)]">{order.readyCount ?? 0}</p>
|
<p className="font-medium !text-[var(--color-text)]">{order.readyCount ?? 0}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{(order.notReadyCount ?? 0) > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-[var(--color-text-muted)]">Не готово</p>
|
<p className="text-xs text-[var(--color-text-muted)]">Не готово</p>
|
||||||
<p className="font-medium !text-[var(--color-text)]">{order.notReadyCount ?? 0}</p>
|
<p className="font-medium !text-[var(--color-text)]">{order.notReadyCount}</p>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-[var(--color-text-muted)]">Обновлена</p>
|
<p className="text-xs text-[var(--color-text-muted)]">Обновлена</p>
|
||||||
<p className="font-medium !text-[var(--color-text)]">{formatDateTime(order.updatedAt)}</p>
|
<p className="font-medium !text-[var(--color-text)]">{formatDateTime(order.updatedAt)}</p>
|
||||||
|
|
@ -908,7 +927,7 @@ export const OrderDetailPanel = ({
|
||||||
</div>
|
</div>
|
||||||
) : deliveryType === "delivery" ? (
|
) : deliveryType === "delivery" ? (
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:relative md:z-10">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Дата доставки"
|
aria-label="Дата доставки"
|
||||||
|
|
@ -920,7 +939,7 @@ export const OrderDetailPanel = ({
|
||||||
<span aria-hidden="true" className="text-[var(--color-text-muted)]">▾</span>
|
<span aria-hidden="true" className="text-[var(--color-text-muted)]">▾</span>
|
||||||
</button>
|
</button>
|
||||||
{isCalendarOpen ? (
|
{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 className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
|
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
|
||||||
|
|
@ -1039,6 +1058,7 @@ export const OrderDetailPanel = ({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Дата самовывоза"
|
aria-label="Дата самовывоза"
|
||||||
|
|
@ -1050,7 +1070,7 @@ export const OrderDetailPanel = ({
|
||||||
<span aria-hidden="true" className="text-[var(--color-text-muted)]">▾</span>
|
<span aria-hidden="true" className="text-[var(--color-text-muted)]">▾</span>
|
||||||
</button>
|
</button>
|
||||||
{isCalendarOpen ? (
|
{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 className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">Календарь самовывоза</p>
|
<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>
|
<p className="mt-2 text-xs text-[var(--color-text-muted)]">Выходные отмечены пунктиром и недоступны.</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
</div>
|
||||||
<div className="grid gap-2 sm:grid-cols-2">
|
<div className="grid gap-2 sm:grid-cols-2">
|
||||||
{DELIVERY_TIME_OPTIONS.map((option) => (
|
{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(""); }}>
|
<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
|
<Button
|
||||||
className="w-full md:w-[180px] md:flex-none md:self-start"
|
className="w-full md:w-[180px] md:flex-none md:self-start"
|
||||||
onClick={handleSaveDeliveryChoice}
|
onClick={() => setConfirmAction({ type: 'delivery' })}
|
||||||
disabled={isSavingDeliveryChoice}
|
disabled={isSavingDeliveryChoice}
|
||||||
>
|
>
|
||||||
{isSavingDeliveryChoice ? "Сохраняем..." : "Согласовать"}
|
{isSavingDeliveryChoice ? "Сохраняем..." : "Согласовать"}
|
||||||
|
|
@ -1107,7 +1128,7 @@ export const OrderDetailPanel = ({
|
||||||
) : null}
|
) : 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">
|
<Panel className="space-y-4 p-5">
|
||||||
<div>
|
<div>
|
||||||
<strong>Назначение водителя</strong>
|
<strong>Назначение водителя</strong>
|
||||||
|
|
@ -1159,7 +1180,7 @@ export const OrderDetailPanel = ({
|
||||||
</Select>
|
</Select>
|
||||||
<Button
|
<Button
|
||||||
className="md:px-4 md:py-2 md:whitespace-nowrap md:self-start"
|
className="md:px-4 md:py-2 md:whitespace-nowrap md:self-start"
|
||||||
onClick={handleAssignDriver}
|
onClick={() => setConfirmAction({ type: 'driver' })}
|
||||||
disabled={isSavingDeliveryChoice || !selectedDriverId}
|
disabled={isSavingDeliveryChoice || !selectedDriverId}
|
||||||
>
|
>
|
||||||
{isSavingDeliveryChoice ? "Назначаем..." : "Назначить"}
|
{isSavingDeliveryChoice ? "Назначаем..." : "Назначить"}
|
||||||
|
|
@ -1206,17 +1227,7 @@ export const OrderDetailPanel = ({
|
||||||
setFormMessage(statusOption.hint || "");
|
setFormMessage(statusOption.hint || "");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setFormMessage("");
|
setConfirmAction({ type: 'status', status: statusOption.value });
|
||||||
onChangeDeliveryStatus({
|
|
||||||
orderGroupId: order.id,
|
|
||||||
status: statusOption.value,
|
|
||||||
}).then((response) => {
|
|
||||||
if (!response.success) {
|
|
||||||
setFormMessage(response.error || "Не удалось обновить статус");
|
|
||||||
} else {
|
|
||||||
setFormMessage("");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
disabled={isSavingDeliveryChoice}
|
disabled={isSavingDeliveryChoice}
|
||||||
>
|
>
|
||||||
|
|
@ -1427,6 +1438,37 @@ export const OrderDetailPanel = ({
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
) : null}
|
) : 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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,18 @@ import {
|
||||||
|
|
||||||
const MAX_VISIBLE_INVOICES = 2;
|
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 buildGroupSummary = (group) => {
|
||||||
const orderCountLabel = `${group.ordersCount || 0} ${group.ordersCount === 1 ? "заказ" : group.ordersCount < 5 ? "заказа" : "заказов"}`;
|
const orderCountLabel = `${group.ordersCount || 0} ${group.ordersCount === 1 ? "заказ" : group.ordersCount < 5 ? "заказа" : "заказов"}`;
|
||||||
const parts = [orderCountLabel];
|
const parts = [orderCountLabel];
|
||||||
if (group.deliveryDate) {
|
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);
|
parts.push(datePart);
|
||||||
}
|
}
|
||||||
if (group.assignedDriverName) {
|
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)]">
|
<td className="max-w-[260px] px-5 py-4 text-sm text-[var(--color-text-muted)]">
|
||||||
{renderOrderNumbers(group)}
|
{renderOrderNumbers(group)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4">
|
<td className="px-5 py-4 text-center">
|
||||||
<Badge tone={getOrderGroupStatusTone(group)}>{getOrderGroupDisplayStatusLabel(group)}</Badge>
|
<Badge tone={getOrderGroupStatusTone(group)}>{getOrderGroupDisplayStatusLabel(group)}</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4 text-sm">
|
<td className="px-5 py-4 text-sm">
|
||||||
|
|
@ -173,7 +180,7 @@ export const OrdersTable = ({
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4 text-sm">
|
<td className="px-5 py-4 text-sm">
|
||||||
{group.deliveryDate ? (
|
{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>
|
<span className="text-[var(--color-text-muted)]">—</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue