582 lines
24 KiB
JavaScript
582 lines
24 KiB
JavaScript
import { CRIMEAN_CITIES } from "../../constants/cities.js";
|
||
import React from "react";
|
||
import {
|
||
getOrderGroupDeliveryHalfDay,
|
||
getOrderGroupDeliveryStatusLabel,
|
||
getOrderGroupDeliveryStatusTone,
|
||
DRIVER_VISIBLE_DELIVERY_STATUSES,
|
||
isOrderGroupVisibleToDriver,
|
||
groupOrderGroupsByDate,
|
||
parseGroupDate,
|
||
ORDER_GROUP_DELIVERY_HALF_DAY_OPTIONS,
|
||
} from "../../services/orderGroupViews";
|
||
import { Badge } from "../UI/Badge";
|
||
import { Button } from "../UI/Button";
|
||
import { Input } from "../UI/Input";
|
||
import { Select } from "../UI/Select";
|
||
import { Panel } from "../UI/Panel";
|
||
import { SkeletonPage } from "../UI/Loading";
|
||
|
||
const CHEVRON_DOWN = (
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<path d="M4 6l4 4 4-4" />
|
||
</svg>
|
||
);
|
||
const CHEVRON_RIGHT = (
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<path d="M6 4l4 4-4 4" />
|
||
</svg>
|
||
);
|
||
|
||
const extractCity = (address) => {
|
||
if (!address || typeof address !== "string") return null;
|
||
const trimmed = address.trim();
|
||
if (!trimmed) return null;
|
||
|
||
const cityMatch = trimmed.match(/(?:г\.\s+|гор\.\s+|пос\.\s+|с\.\s+|дер\.\s+|пгт\.\s+|город\s+|село\s+|г\s+)([А-ЯЁA-Z][а-яёa-zA-Z\s\-]+?)(?:\s*[,;.]|\s|$)/i);
|
||
if (cityMatch) {
|
||
return cityMatch[1].trim();
|
||
}
|
||
|
||
|
||
for (const city of CRIMEAN_CITIES) {
|
||
if (trimmed.toLowerCase().includes(city.toLowerCase())) {
|
||
return city;
|
||
}
|
||
}
|
||
|
||
// Бахчисарайский р-н → Бахчисарай
|
||
const district = trimmed.match(/([А-ЯЁа-яё]+)ский\s*(?:р-н|район)/i);
|
||
if (district) {
|
||
const base = district[1];
|
||
for (const city of CRIMEAN_CITIES) {
|
||
if (city.toLowerCase().startsWith(base.toLowerCase())) return city;
|
||
}
|
||
}
|
||
|
||
// no match → null (caller falls back to Севастополь)
|
||
return null;
|
||
};
|
||
|
||
const normalizeCity = (address) => {
|
||
const city = extractCity(address);
|
||
return city || "Севастополь";
|
||
};
|
||
|
||
const DRIVER_DELIVERY_STATUS_OPTIONS = [
|
||
{ value: "all", label: "Все статусы" },
|
||
...DRIVER_VISIBLE_DELIVERY_STATUSES.map((status) => ({
|
||
value: status,
|
||
label: status === "driver_assigned" ? "Назначено вам" : getOrderGroupDeliveryStatusLabel(status),
|
||
})),
|
||
];
|
||
|
||
const pluralGroups = (n) => {
|
||
if (n === 1) return "группа";
|
||
if (n >= 2 && n < 5) return "группы";
|
||
return "групп";
|
||
};
|
||
|
||
/** Count items by status, return array of {status, label, tone, count} */
|
||
const countByStatus = (items) => {
|
||
const map = new Map();
|
||
for (const item of items) {
|
||
const s = item.deliveryStatus || item.delivery_status || "unknown";
|
||
map.set(s, (map.get(s) || 0) + 1);
|
||
}
|
||
const result = [];
|
||
for (const [status, count] of map) {
|
||
result.push({
|
||
status,
|
||
label: status === "driver_assigned" ? "Назначено" : getOrderGroupDeliveryStatusLabel(status),
|
||
tone: getOrderGroupDeliveryStatusTone(status),
|
||
count,
|
||
});
|
||
}
|
||
// Sort: delivered last (green = done), others by severity
|
||
const order = ["problem", "cancelled", "on_route", "loaded", "driver_assigned", "paid_storage", "delivered", "picked_up"];
|
||
result.sort((a, b) => {
|
||
const ia = order.indexOf(a.status);
|
||
const ib = order.indexOf(b.status);
|
||
if (ia === -1 && ib === -1) return a.status.localeCompare(b.status);
|
||
if (ia === -1) return 1;
|
||
if (ib === -1) return -1;
|
||
return ia - ib;
|
||
});
|
||
return result;
|
||
};
|
||
|
||
export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUser, isLoading = false }) => {
|
||
const [filters, setFilters] = React.useState({
|
||
selectedDate: "",
|
||
deliveryStatus: "all",
|
||
selectedCity: "",
|
||
searchQuery: "",
|
||
halfDay: "all",
|
||
});
|
||
const [collapsedDates, setCollapsedDates] = React.useState({});
|
||
|
||
const hasActiveFilters = filters.selectedDate || filters.deliveryStatus !== "all" || filters.selectedCity || filters.searchQuery || filters.halfDay !== "all";
|
||
|
||
const resetAllFilters = () => {
|
||
setFilters({ selectedDate: "", deliveryStatus: "all", selectedCity: "", searchQuery: "", halfDay: "all" });
|
||
};
|
||
|
||
const toggleDate = (date) => {
|
||
setCollapsedDates((prev) => ({ ...prev, [date]: !prev[date] }));
|
||
};
|
||
|
||
const normalizePhoneForTel = (phone) => {
|
||
const cleaned = String(phone || "").trim();
|
||
if (!cleaned) return "";
|
||
if (cleaned.startsWith("+7")) return cleaned;
|
||
if (cleaned.startsWith("8")) return "+7" + cleaned.slice(1);
|
||
return "+7" + cleaned;
|
||
};
|
||
|
||
const driverOrderGroups = React.useMemo(
|
||
() => orderGroups.filter((group) => {
|
||
const isVisible = isOrderGroupVisibleToDriver(group);
|
||
const isAssignedToMe = currentUser && group.assignedDriverId === currentUser.id;
|
||
return isVisible && isAssignedToMe;
|
||
}),
|
||
[orderGroups, currentUser],
|
||
);
|
||
|
||
const dateDeliveryMap = React.useMemo(() => {
|
||
const map = new Map();
|
||
driverOrderGroups.forEach((group) => {
|
||
const date = group.deliveryDate;
|
||
if (date) {
|
||
map.set(date, (map.get(date) || 0) + 1);
|
||
}
|
||
});
|
||
return map;
|
||
}, [driverOrderGroups]);
|
||
|
||
const sortedDeliveryDates = React.useMemo(() => {
|
||
return Array.from(dateDeliveryMap.keys()).sort();
|
||
}, [dateDeliveryMap]);
|
||
|
||
const cityDeliveryMap = React.useMemo(() => {
|
||
const map = new Map();
|
||
driverOrderGroups.forEach((group) => {
|
||
const city = normalizeCity(group.deliveryAddress || group.delivery_address);
|
||
map.set(city, (map.get(city) || 0) + 1);
|
||
});
|
||
return map;
|
||
}, [driverOrderGroups]);
|
||
|
||
const sortedCities = React.useMemo(() => {
|
||
return Array.from(cityDeliveryMap.keys()).sort((a, b) => {
|
||
if (a === "Севастополь") return -1;
|
||
if (b === "Севастополь") return 1;
|
||
return a.localeCompare(b, "ru");
|
||
});
|
||
}, [cityDeliveryMap]);
|
||
|
||
const getSearchHaystack = (group) => {
|
||
return [
|
||
group.groupKey,
|
||
group.displayTitle,
|
||
group.customerName,
|
||
group.customerPhone,
|
||
group.customerDate,
|
||
Array.isArray(group.orderNumbers) ? group.orderNumbers.join(" ") : "",
|
||
group.deliveryAddress || group.delivery_address || "",
|
||
group.city || "",
|
||
].filter(Boolean).join(" ").toLowerCase();
|
||
};
|
||
|
||
const filteredOrderGroups = React.useMemo(() => {
|
||
let result = [...driverOrderGroups];
|
||
if (filters.selectedDate) {
|
||
result = result.filter((group) => group.deliveryDate === filters.selectedDate);
|
||
}
|
||
if (filters.deliveryStatus !== "all") {
|
||
result = result.filter((group) => (group.deliveryStatus || group.delivery_status) === filters.deliveryStatus);
|
||
}
|
||
if (filters.selectedCity) {
|
||
result = result.filter((group) => {
|
||
const city = normalizeCity(group.deliveryAddress || group.delivery_address);
|
||
return city === filters.selectedCity;
|
||
});
|
||
}
|
||
if (filters.halfDay !== "all") {
|
||
result = result.filter((group) => {
|
||
const groupHalfDay = getOrderGroupDeliveryHalfDay(group);
|
||
if (filters.halfDay === "unknown") {
|
||
return !groupHalfDay;
|
||
}
|
||
const labelMap = { morning: "Первая половина дня", afternoon: "Вторая половина дня" };
|
||
return groupHalfDay === labelMap[filters.halfDay];
|
||
});
|
||
}
|
||
const query = (filters.searchQuery || "").trim().toLowerCase();
|
||
if (query) {
|
||
result = result.filter((group) => getSearchHaystack(group).includes(query));
|
||
}
|
||
// Sort: problems first, then by status priority
|
||
const statusPriority = ["problem", "on_route", "loaded", "driver_assigned", "agreed", "delivered", "picked_up", "paid_storage"];
|
||
result.sort((a, b) => {
|
||
// has_delivery_problem always first
|
||
const aProblem = a.hasDeliveryProblem || a.has_delivery_problem;
|
||
const bProblem = b.hasDeliveryProblem || b.has_delivery_problem;
|
||
if (aProblem && !bProblem) return -1;
|
||
if (!aProblem && bProblem) return 1;
|
||
const sa = a.deliveryStatus || a.delivery_status || "unknown";
|
||
const sb = b.deliveryStatus || b.delivery_status || "unknown";
|
||
const ia = statusPriority.indexOf(sa);
|
||
const ib = statusPriority.indexOf(sb);
|
||
if (ia === -1 && ib === -1) return 0;
|
||
if (ia === -1) return 1;
|
||
if (ib === -1) return -1;
|
||
return ia - ib;
|
||
});
|
||
return result;
|
||
}, [driverOrderGroups, filters.selectedDate, filters.deliveryStatus, filters.selectedCity, filters.searchQuery, filters.halfDay]);
|
||
|
||
const groupedOrderGroups = React.useMemo(
|
||
() => groupOrderGroupsByDate(filteredOrderGroups),
|
||
[filteredOrderGroups],
|
||
);
|
||
|
||
const deliveryCountLabel = `${filteredOrderGroups.length} ${
|
||
filteredOrderGroups.length === 1 ? "доставка" : filteredOrderGroups.length < 5 ? "доставки" : "доставок"
|
||
}`;
|
||
|
||
const isDateSelected = (date) => filters.selectedDate === date;
|
||
|
||
// Compute per-date status summary for collapsed badges
|
||
const dateStatusSummary = React.useMemo(() => {
|
||
const summary = {};
|
||
for (const dg of groupedOrderGroups) {
|
||
summary[dg.date] = countByStatus(dg.items);
|
||
}
|
||
return summary;
|
||
}, [groupedOrderGroups]);
|
||
|
||
if (isLoading) {
|
||
return <SkeletonPage panels={3} />;
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-4 fs-zone-card">
|
||
<Panel className="space-y-3 p-5">
|
||
<div className="space-y-4">
|
||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||
<div>
|
||
<div className="flex flex-wrap items-center gap-3">
|
||
<h3 className="text-lg font-semibold">Мои доставки</h3>
|
||
<Badge tone="neutral">{deliveryCountLabel}</Badge>
|
||
</div>
|
||
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
|
||
Показываем только назначенные вам группы доставки. Выберите дату и город.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
||
<label className="flex min-w-0 flex-col gap-2">
|
||
<span className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
|
||
Дата
|
||
</span>
|
||
<Input
|
||
type="date"
|
||
value={filters.selectedDate}
|
||
onChange={(event) => setFilters((current) => ({ ...current, selectedDate: event.target.value }))}
|
||
/>
|
||
</label>
|
||
<label className="flex min-w-0 flex-col gap-2">
|
||
<span className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
|
||
Статус
|
||
</span>
|
||
<Select
|
||
value={filters.deliveryStatus}
|
||
onChange={(event) =>
|
||
setFilters((current) => ({ ...current, deliveryStatus: event.target.value }))
|
||
}
|
||
>
|
||
{DRIVER_DELIVERY_STATUS_OPTIONS.map((option) => (
|
||
<option key={option.value} value={option.value}>
|
||
{option.label}
|
||
</option>
|
||
))}
|
||
</Select>
|
||
</label>
|
||
</div>
|
||
|
||
<div className="grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
||
<label className="flex min-w-0 flex-col gap-2">
|
||
<span className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
|
||
Поиск
|
||
</span>
|
||
<Input
|
||
type="text"
|
||
placeholder="Имя, адрес, телефон, номер заказа..."
|
||
value={filters.searchQuery}
|
||
onChange={(event) => setFilters((current) => ({ ...current, searchQuery: event.target.value }))}
|
||
/>
|
||
</label>
|
||
<label className="flex min-w-0 flex-col gap-2">
|
||
<span className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
|
||
Время дня
|
||
</span>
|
||
<Select
|
||
value={filters.halfDay}
|
||
onChange={(event) => setFilters((current) => ({ ...current, halfDay: event.target.value }))}
|
||
>
|
||
{ORDER_GROUP_DELIVERY_HALF_DAY_OPTIONS.map((option) => (
|
||
<option key={option.value} value={option.value}>
|
||
{option.label}
|
||
</option>
|
||
))}
|
||
</Select>
|
||
</label>
|
||
</div>
|
||
|
||
{hasActiveFilters && (
|
||
<div className="pt-1">
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={resetAllFilters}
|
||
className="text-xs text-[var(--color-text-muted)] hover:text-[var(--color-danger)]"
|
||
>
|
||
✕ Сбросить все фильтры
|
||
</Button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Date pills */}
|
||
{sortedDeliveryDates.length > 0 && (
|
||
<div className="flex flex-wrap gap-2 pt-2">
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => setFilters((current) => ({ ...current, selectedDate: "" }))}
|
||
className={[
|
||
"rounded-full px-3 py-1.5 text-xs font-medium",
|
||
!filters.selectedDate
|
||
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-text)]"
|
||
: "text-[var(--color-text-muted)]",
|
||
].join(" ")}
|
||
>
|
||
Все даты
|
||
</Button>
|
||
{sortedDeliveryDates.map((date) => {
|
||
const count = dateDeliveryMap.get(date) || 0;
|
||
const selected = isDateSelected(date);
|
||
return (
|
||
<Button
|
||
key={date}
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => setFilters((current) => ({ ...current, selectedDate: date }))}
|
||
className={[
|
||
"flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium",
|
||
selected
|
||
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-text)]"
|
||
: "text-[var(--color-text-muted)]",
|
||
].join(" ")}
|
||
>
|
||
<span>{parseGroupDate(date)?.toLocaleDateString("ru-RU", { day: "numeric", month: "short" }) || "—"}</span>
|
||
{count > 0 && (
|
||
<span className="rounded-full bg-[var(--color-accent)] px-1.5 py-0.5 text-[10px] font-bold text-white">
|
||
{count}
|
||
</span>
|
||
)}
|
||
</Button>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{/* City pills */}
|
||
{sortedCities.length > 1 && (
|
||
<div className="flex flex-wrap gap-2">
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => setFilters((current) => ({ ...current, selectedCity: "" }))}
|
||
className={[
|
||
"rounded-full px-3 py-1.5 text-xs font-medium",
|
||
!filters.selectedCity
|
||
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-text)]"
|
||
: "text-[var(--color-text-muted)]",
|
||
].join(" ")}
|
||
>
|
||
Все города
|
||
</Button>
|
||
{sortedCities.map((city) => {
|
||
const count = cityDeliveryMap.get(city) || 0;
|
||
const selected = filters.selectedCity === city;
|
||
return (
|
||
<Button
|
||
key={city}
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => setFilters((current) => ({ ...current, selectedCity: city }))}
|
||
className={[
|
||
"flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium",
|
||
selected
|
||
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-text)]"
|
||
: "text-[var(--color-text-muted)]",
|
||
].join(" ")}
|
||
>
|
||
<span>{city}</span>
|
||
{count > 0 && (
|
||
<span className="rounded-full bg-[var(--color-accent)] px-1.5 py-0.5 text-[10px] font-bold text-white">
|
||
{count}
|
||
</span>
|
||
)}
|
||
</Button>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</Panel>
|
||
|
||
{groupedOrderGroups.length ? (
|
||
groupedOrderGroups.map((group) => {
|
||
const isCollapsed = collapsedDates[group.date];
|
||
const statusCounts = dateStatusSummary[group.date] || [];
|
||
|
||
// Group items by delivery status within each date
|
||
const statusBuckets = new Map();
|
||
for (const item of group.items) {
|
||
const s = item.deliveryStatus || item.delivery_status || "unknown";
|
||
const label = s === "driver_assigned" ? "Назначено вам" : getOrderGroupDeliveryStatusLabel(s);
|
||
const tone = getOrderGroupDeliveryStatusTone(s);
|
||
if (!statusBuckets.has(s)) {
|
||
statusBuckets.set(s, { label, tone, items: [] });
|
||
}
|
||
statusBuckets.get(s).items.push(item);
|
||
}
|
||
|
||
const statusOrder = ["agreed", "driver_assigned", "loaded", "on_route", "delivered", "picked_up", "paid_storage", "problem"];
|
||
const sortedBuckets = Array.from(statusBuckets.entries()).sort(([a], [b]) => {
|
||
const ia = statusOrder.indexOf(a);
|
||
const ib = statusOrder.indexOf(b);
|
||
if (ia === -1 && ib === -1) return a.localeCompare(b);
|
||
if (ia === -1) return 1;
|
||
if (ib === -1) return -1;
|
||
return ia - ib;
|
||
});
|
||
|
||
return (
|
||
<Panel key={group.date} className="space-y-4 p-5">
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
className="flex w-full items-center gap-3 text-left"
|
||
onClick={() => toggleDate(group.date)}
|
||
>
|
||
<span className="shrink-0 text-[var(--color-text-muted)] transition-transform">
|
||
{isCollapsed ? CHEVRON_RIGHT : CHEVRON_DOWN}
|
||
</span>
|
||
<div className="min-w-0 flex-1">
|
||
<h4 className="text-lg font-semibold capitalize">
|
||
{parseGroupDate(group.date)?.toLocaleDateString("ru-RU", {
|
||
day: "numeric",
|
||
month: "long",
|
||
weekday: "long",
|
||
}) || "Без даты"}
|
||
</h4>
|
||
</div>
|
||
<div className="flex shrink-0 flex-wrap items-center gap-1.5">
|
||
{statusCounts.map(({ status, label, tone, count }) => (
|
||
<Badge key={status} tone={tone}>{count} {label}</Badge>
|
||
))}
|
||
</div>
|
||
</Button>
|
||
|
||
{!isCollapsed && (
|
||
<div className="space-y-4 pt-1">
|
||
{sortedBuckets.map(([statusValue, { label, tone, items }]) => (
|
||
<div key={statusValue} className="space-y-2">
|
||
<div className="flex items-center gap-2">
|
||
<Badge tone={tone}>{label}</Badge>
|
||
<span className="text-xs text-[var(--color-text-muted)]">{items.length} {pluralGroups(items.length)}</span>
|
||
</div>
|
||
{/* Table layout — № | Фамилия | Телефон | Город | Часть дня */}
|
||
<div className="overflow-x-auto">
|
||
<div className="min-w-[640px]">
|
||
<div className="grid grid-cols-[minmax(90px,1fr)_minmax(120px,1.5fr)_minmax(130px,1.2fr)_minmax(100px,1fr)_minmax(120px,1fr)] gap-0 border-b border-[var(--color-border)] bg-[var(--color-surface-strong)] text-xs uppercase tracking-[0.1em] text-[var(--color-text-muted)]">
|
||
<div className="px-3 py-1.5 font-medium">№ счёта</div>
|
||
<div className="px-3 py-1.5 font-medium">Клиент</div>
|
||
<div className="px-3 py-1.5 font-medium">Телефон</div>
|
||
<div className="px-3 py-1.5 font-medium">Город</div>
|
||
<div className="px-3 py-1.5 font-medium">Часть дня</div>
|
||
</div>
|
||
{items.map((item) => {
|
||
const hasProblem = item.hasDeliveryProblem || item.has_delivery_problem;
|
||
const phoneTel = normalizePhoneForTel(item.customerPhone);
|
||
const halfDayLabel = getOrderGroupDeliveryHalfDay(item);
|
||
const city = normalizeCity(item.deliveryAddress || item.delivery_address);
|
||
const orderNum = (Array.isArray(item.orderNumbers) && item.orderNumbers.length > 0)
|
||
? item.orderNumbers.join(", ")
|
||
: (item.groupKey || "—");
|
||
return (
|
||
<button
|
||
key={item.id}
|
||
type="button"
|
||
className={`grid grid-cols-[minmax(90px,1fr)_minmax(120px,1.5fr)_minmax(130px,1.2fr)_minmax(100px,1fr)_minmax(120px,1fr)] gap-0 w-full border-t border-[var(--color-border)] text-left transition hover:bg-[var(--color-accent-soft)] ${hasProblem ? "bg-[rgba(239,68,68,0.04)]" : ""}`}
|
||
onClick={() => onOpenOrder?.(item.id)}
|
||
>
|
||
<div className="px-3 py-2 text-xs">
|
||
<span className="font-semibold text-[var(--color-accent)]">{orderNum}</span>
|
||
{hasProblem && <span className="ml-1 text-[10px] font-bold text-[var(--color-danger)]">⚠</span>}
|
||
</div>
|
||
<div className="px-3 py-2 text-xs font-medium text-[var(--color-text)]">
|
||
{item.customerName || item.displayTitle || item.groupKey}
|
||
</div>
|
||
<div className="px-3 py-2 text-xs">
|
||
{item.customerPhone && phoneTel ? (
|
||
<a
|
||
href={`tel:${phoneTel}`}
|
||
onClick={(e) => e.stopPropagation()}
|
||
className="text-[var(--color-accent)] hover:underline"
|
||
>
|
||
{item.customerPhone}
|
||
</a>
|
||
) : (
|
||
<span className="text-[var(--color-text-muted)]">{item.customerPhone || "—"}</span>
|
||
)}
|
||
</div>
|
||
<div className="px-3 py-2 text-xs text-[var(--color-text-muted)]">
|
||
{city}
|
||
</div>
|
||
<div className="px-3 py-2 text-xs text-[var(--color-text-muted)]">
|
||
{halfDayLabel || "—"}
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</Panel>
|
||
);
|
||
})
|
||
) : (
|
||
<Panel className="p-6">
|
||
<h4 className="text-lg font-semibold">Доставки не найдены</h4>
|
||
<p className="mt-2 text-sm text-[var(--color-text-muted)]">
|
||
Сейчас у вас нет назначенных групп доставки.
|
||
</p>
|
||
</Panel>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|