feat: Тип column replaces Проблема, red highlight for problem rows, delivery type filter, clean status options

- OrdersTable: replaced 'Проблема' column with 'Тип' (Самовывоз/Доставка)
- Problem rows highlighted red (bg rgba danger 10%)
- Mobile cards also show Тип label
- OrderFilters: added deliveryType dropdown (Доставка/Самовывоз)
- filterOrderGroups: filter by deliveryType
- useOrderGroups: deliveryType in default filters state
- Cleaned ORDER_GROUP_DISPLAY_STATUS_OPTIONS: removed 'Повторное SMS' and 'SMS отправлены' (not used)
This commit is contained in:
root 2026-06-18 15:00:58 +00:00
parent 8d3d8dc0fe
commit 54386e9710
4 changed files with 90 additions and 65 deletions

View File

@ -105,6 +105,16 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
</select> </select>
)} )}
<select
value={filters.deliveryType || ""}
onChange={(e) => updateFilter("deliveryType", e.target.value)}
className="h-[46px] rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 text-sm outline-none focus:border-[var(--color-accent)]"
>
<option value="">Тип получения</option>
<option value="delivery">Доставка</option>
<option value="pickup">Самовывоз</option>
</select>
<Input <Input
className="h-[46px] py-0" className="h-[46px] py-0"
placeholder="Поиск по группе, клиенту или телефону" placeholder="Поиск по группе, клиенту или телефону"

View File

@ -54,6 +54,12 @@ const renderOrderNumbers = (group) => {
return `${visible.join(", ")} +${remaining}`; return `${visible.join(", ")} +${remaining}`;
}; };
const getDeliveryTypeLabel = (deliveryType) => {
if (deliveryType === "pickup") return "Самовывоз";
if (deliveryType === "delivery") return "Доставка";
return "—";
};
const renderMobileOrderNumbers = (group) => { const renderMobileOrderNumbers = (group) => {
const numbers = group.allBillNumbers || group.orderNumbers; const numbers = group.allBillNumbers || group.orderNumbers;
if (!Array.isArray(numbers) || !numbers.length) { if (!Array.isArray(numbers) || !numbers.length) {
@ -111,47 +117,56 @@ export const OrdersTable = ({
Группы не найдены. Попробуйте изменить поиск или статус. Группы не найдены. Попробуйте изменить поиск или статус.
</div> </div>
) : null} ) : null}
{orderGroups.map((group) => ( {orderGroups.map((group) => {
<Button const hasProblem = group.hasDeliveryProblem;
key={group.id} const baseClass = "w-full rounded-[22px] border text-left transition";
variant="ghost" const selectedClass = selectedOrderGroupId === group.id
onClick={() => onOpenOrder(group.id)} ? "border-[var(--color-accent)] bg-[var(--color-accent-soft)]"
className={["w-full rounded-[22px] border text-left transition", : hasProblem
selectedOrderGroupId === group.id ? "border-[var(--color-accent)] bg-[var(--color-accent-soft)]" : "border-[var(--color-border)] bg-[var(--color-surface-strong)]", ? "border-[var(--color-danger)] bg-[rgba(201,61,61,0.1)]"
].join(" ")} : "border-[var(--color-border)] bg-[var(--color-surface-strong)]";
>
<div className="space-y-1">
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3">
<div className="min-w-0 truncate font-medium">
{group.displayTitle || group.customerName || group.groupKey}
</div>
<Badge className="self-start" tone={getOrderGroupStatusTone(group)}>
{getOrderGroupDisplayStatusLabel(group)}
</Badge>
</div>
<div className="text-sm text-[var(--color-text-muted)]">
{group.displaySubtitle || [group.customerPhone, group.customerDate].filter(Boolean).join(" · ")}
</div>
</div>
<div className="mt-3 text-sm text-[var(--color-text-muted)]">{buildGroupSummary(group)}</div> return (
<div className="mt-2 text-sm text-[var(--color-text-muted)]">{renderMobileOrderNumbers(group)}</div> <Button
{group.hasDeliveryProblem && ( key={group.id}
<div className="mt-2 rounded-lg border border-[var(--color-warning)] bg-[var(--color-warning-soft)] px-2 py-1 text-xs"> variant="ghost"
<span className="font-medium text-[var(--color-warning)]"> Проблема с доставкой</span> onClick={() => onOpenOrder(group.id)}
{group.deliveryProblemNote && ( className={[baseClass, selectedClass].join(" ")}
<div className="mt-0.5 text-[var(--color-text-muted)]">{group.deliveryProblemNote}</div> >
)} <div className="space-y-1">
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3">
<div className="min-w-0 truncate font-medium">
{group.displayTitle || group.customerName || group.groupKey}
</div>
<Badge className="self-start" tone={getOrderGroupStatusTone(group)}>
{getOrderGroupDisplayStatusLabel(group)}
</Badge>
</div>
<div className="text-sm text-[var(--color-text-muted)]">
{group.displaySubtitle || [group.customerPhone, group.customerDate].filter(Boolean).join(" · ")}
</div>
</div> </div>
)}
{!group.hasDeliveryProblem && Array.isArray(group?.driverShipmentData) && group.driverShipmentData.length > 0 && ( <div className="mt-3 text-sm text-[var(--color-text-muted)]">{buildGroupSummary(group)}</div>
<div className="mt-2 text-xs text-[var(--color-accent)]"> Доставка без проблем</div> <div className="mt-2 text-sm text-[var(--color-text-muted)]">{renderMobileOrderNumbers(group)}</div>
)} <div className="mt-2 text-sm text-[var(--color-text-muted)]">Тип: {getDeliveryTypeLabel(group.deliveryType)}</div>
<div className="mt-3 text-xs text-[var(--color-text-muted)]"> {group.hasDeliveryProblem && (
{formatDateTime(group.updatedAt)} <div className="mt-2 rounded-lg border border-[var(--color-warning)] bg-[var(--color-warning-soft)] px-2 py-1 text-xs">
</div> <span className="font-medium text-[var(--color-warning)]"> Проблема с доставкой</span>
</Button> {group.deliveryProblemNote && (
))} <div className="mt-0.5 text-[var(--color-text-muted)]">{group.deliveryProblemNote}</div>
)}
</div>
)}
{!group.hasDeliveryProblem && Array.isArray(group?.driverShipmentData) && group.driverShipmentData.length > 0 && (
<div className="mt-2 text-xs text-[var(--color-accent)]"> Доставка без проблем</div>
)}
<div className="mt-3 text-xs text-[var(--color-text-muted)]">
{formatDateTime(group.updatedAt)}
</div>
</Button>
);
})}
</div> </div>
<div className="hidden overflow-x-auto md:block"> <div className="hidden overflow-x-auto md:block">
@ -167,16 +182,24 @@ export const OrdersTable = ({
<div className="px-5 py-4 font-medium">Статус</div> <div className="px-5 py-4 font-medium">Статус</div>
<div className="px-5 py-4 font-medium">Водитель</div> <div className="px-5 py-4 font-medium">Водитель</div>
<div className="px-5 py-4 font-medium">Дата доставки</div> <div className="px-5 py-4 font-medium">Дата доставки</div>
<div className="px-5 py-4 font-medium">Проблема</div> <div className="px-5 py-4 font-medium">Тип</div>
<div className="px-5 py-4 font-medium">Обновлён</div> <div className="px-5 py-4 font-medium">Обновлён</div>
</div> </div>
{orderGroups.map((group) => ( {orderGroups.map((group) => {
<Button const hasProblem = group.hasDeliveryProblem;
key={group.id} const rowClassName = `grid grid-cols-[minmax(180px,2.5fr)_minmax(100px,1.2fr)_minmax(100px,1fr)_minmax(80px,1fr)_minmax(120px,1.2fr)_minmax(80px,0.8fr)_minmax(130px,1.2fr)] gap-0 w-full border-t border-[var(--color-border)] text-left transition ${
variant="ghost" hasProblem
className={`grid grid-cols-[minmax(180px,2.5fr)_minmax(100px,1.2fr)_minmax(100px,1fr)_minmax(80px,1fr)_minmax(120px,1.2fr)_minmax(80px,0.8fr)_minmax(130px,1.2fr)] gap-0 w-full border-t border-[var(--color-border)] text-left transition hover:bg-[var(--color-accent-soft)] ${selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : ""}`} ? "bg-[rgba(201,61,61,0.1)] hover:bg-[rgba(201,61,61,0.15)]"
onClick={() => onOpenOrder(group.id)} : "hover:bg-[var(--color-accent-soft)]"
> } ${selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : ""}`;
return (
<Button
key={group.id}
variant="ghost"
className={rowClassName}
onClick={() => onOpenOrder(group.id)}
>
<div className="px-5 py-4"> <div className="px-5 py-4">
<div className="font-medium">{group.displayTitle || group.customerName || group.groupKey}</div> <div className="font-medium">{group.displayTitle || group.customerName || group.groupKey}</div>
<div className="mt-1 text-sm text-[var(--color-text-muted)]"> <div className="mt-1 text-sm text-[var(--color-text-muted)]">
@ -201,27 +224,15 @@ export const OrdersTable = ({
)} )}
</div> </div>
<div className="px-5 py-4 text-sm"> <div className="px-5 py-4 text-sm">
{group.hasDeliveryProblem ? ( {getDeliveryTypeLabel(group.deliveryType)}
<div>
<span className="text-[var(--color-warning)] font-medium"> Проблема</span>
{group.deliveryProblemNote && (
<div className="mt-0.5 text-xs text-[var(--color-text-muted)] line-clamp-2">
{group.deliveryProblemNote}
</div>
)}
</div>
) : Array.isArray(group?.driverShipmentData) && group.driverShipmentData.length > 0 ? (
<span className="text-[var(--color-accent)]"> Ок</span>
) : (
<span className="text-[var(--color-text-muted)]"></span>
)}
</div> </div>
<div className="px-5 py-4 text-sm text-[var(--color-text-muted)]"> <div className="px-5 py-4 text-sm text-[var(--color-text-muted)]">
{formatDateTime(group.updatedAt)} {formatDateTime(group.updatedAt)}
</div> </div>
</Button> </Button>
))} );
</div> })}
</div>
)} )}
</div> </div>
</Panel> </Panel>

View File

@ -14,6 +14,7 @@ export const useOrderGroups = () => {
const [filters, setFilters] = React.useState({ const [filters, setFilters] = React.useState({
query: "", query: "",
displayStatus: "all", displayStatus: "all",
deliveryType: "",
}); });
const [selectedOrderGroupId, setSelectedOrderGroupId] = React.useState(null); const [selectedOrderGroupId, setSelectedOrderGroupId] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(true); const [isLoading, setIsLoading] = React.useState(true);

View File

@ -305,6 +305,11 @@ export const filterOrderGroups = (groups, filters = {}) => {
} }
} }
const deliveryTypeFilter = (filters.deliveryType || "").trim().toLowerCase();
if (deliveryTypeFilter && group.deliveryType !== deliveryTypeFilter) {
return false;
}
if (!query) { if (!query) {
return true; return true;
} }
@ -333,8 +338,6 @@ export const ORDER_GROUP_DISPLAY_STATUS_OPTIONS = [
{ value: "delivery:paid_storage", label: DELIVERY_GROUP_STATUS_LABELS.paid_storage }, { value: "delivery:paid_storage", label: DELIVERY_GROUP_STATUS_LABELS.paid_storage },
{ value: "delivery:cancelled", label: DELIVERY_GROUP_STATUS_LABELS.cancelled }, { value: "delivery:cancelled", label: DELIVERY_GROUP_STATUS_LABELS.cancelled },
{ value: "status:manual_required", label: "Требует ручной обработки" }, { value: "status:manual_required", label: "Требует ручной обработки" },
{ value: "status:second_sms_sent", label: "Повторное SMS" },
{ value: "status:sms_sent", label: "SMS отправлены" },
{ value: "status:ready_for_notification", label: "Готово к уведомлению" }, { value: "status:ready_for_notification", label: "Готово к уведомлению" },
]; ];