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 = ( ); const CHEVRON_RIGHT = ( ); 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 ; } return (

Мои доставки

{deliveryCountLabel}

Показываем только назначенные вам группы доставки. Выберите дату и город.

{hasActiveFilters && (
)} {/* Date pills */} {sortedDeliveryDates.length > 0 && (
{sortedDeliveryDates.map((date) => { const count = dateDeliveryMap.get(date) || 0; const selected = isDateSelected(date); return ( ); })}
)} {/* City pills */} {sortedCities.length > 1 && (
{sortedCities.map((city) => { const count = cityDeliveryMap.get(city) || 0; const selected = filters.selectedCity === city; return ( ); })}
)}
{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 ( {!isCollapsed && (
{sortedBuckets.map(([statusValue, { label, tone, items }]) => (
{label} {items.length} {pluralGroups(items.length)}
{/* Table layout — № | Фамилия | Телефон | Город | Часть дня */}
№ счёта
Клиент
Телефон
Город
Часть дня
{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 ( ); })}
))}
)}
); }) ) : (

Доставки не найдены

Сейчас у вас нет назначенных групп доставки.

)}
); };