From ced5958eacae3cca3116cb7bd375807e91199bb1 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 10 Jul 2026 14:50:15 +0000 Subject: [PATCH] feat: driver filters + status fixes + logistics problem indicator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DriverDeliveryPlanner: - Text search (name, address, phone, order numbers) - Half-day filter (morning/afternoon/unknown) - 'Reset all filters' button - tel: link on phone in delivery cards - Order numbers badges in cards - Partial delivery warning badge - Problems-first sorting saveShipmentData fix: - Partial shipment with problems → status 'problem' (not 'delivered') - Full shipment → 'delivered' (unchanged) DriverShipmentPanel: - Reset button: confirmation dialog when status is final - Calls onResetStatus to revert delivery status to 'loaded' OrderDetailPanel (driver status section): - 'Return to work' button for delivered/problem/picked_up statuses - Passes onResetStatus + isSavingStatusChange to DriverShipmentPanel LogisticsReadinessBoard: - Warning badge for has_delivery_problem groups in table rows - Shows delivery_problem_note as tooltip --- .../driver/DriverDeliveryPlanner.jsx | 156 +++++++++++++++++- src/components/driver/DriverShipmentPanel.jsx | 46 +++++- .../logistics/LogisticsReadinessBoard.jsx | 12 +- src/components/orders/OrderDetailPanel.jsx | 49 +++++- src/services/supabase/orderGroupRepository.js | 12 +- 5 files changed, 259 insertions(+), 16 deletions(-) diff --git a/src/components/driver/DriverDeliveryPlanner.jsx b/src/components/driver/DriverDeliveryPlanner.jsx index 4fe743f..3435289 100644 --- a/src/components/driver/DriverDeliveryPlanner.jsx +++ b/src/components/driver/DriverDeliveryPlanner.jsx @@ -8,6 +8,7 @@ import { isOrderGroupVisibleToDriver, groupOrderGroupsByDate, parseGroupDate, + ORDER_GROUP_DELIVERY_HALF_DAY_OPTIONS, } from "../../services/orderGroupViews"; import { Badge } from "../UI/Badge"; import { Button } from "../UI/Button"; @@ -110,13 +111,29 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs 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); @@ -158,6 +175,19 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs }); }, [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) { @@ -172,8 +202,39 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs 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]); + }, [driverOrderGroups, filters.selectedDate, filters.deliveryStatus, filters.selectedCity, filters.searchQuery, filters.halfDay]); const groupedOrderGroups = React.useMemo( () => groupOrderGroupsByDate(filteredOrderGroups), @@ -245,6 +306,49 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs +
+ + +
+ + {hasActiveFilters && ( +
+ +
+ )} + {/* Date pills */} {sortedDeliveryDates.length > 0 && (
@@ -402,7 +506,11 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs {items.length} {pluralGroups(items.length)}
- {items.map((item) => ( + {items.map((item) => { + const hasProblem = item.hasDeliveryProblem || item.has_delivery_problem; + const phoneTel = normalizePhoneForTel(item.customerPhone); + const halfDayLabel = getOrderGroupDeliveryHalfDay(item); + return ( - ))} + ); + })}
))} diff --git a/src/components/driver/DriverShipmentPanel.jsx b/src/components/driver/DriverShipmentPanel.jsx index e6d3622..782c4c6 100644 --- a/src/components/driver/DriverShipmentPanel.jsx +++ b/src/components/driver/DriverShipmentPanel.jsx @@ -89,11 +89,12 @@ const parseOrderItems = (order) => { return []; }; -export const DriverShipmentPanel = ({ order, onShipmentChange, onSaveShipment, isSavingShipment }) => { +export const DriverShipmentPanel = ({ order, onShipmentChange, onSaveShipment, isSavingShipment, onResetStatus, isSavingStatusChange }) => { const [stopWords, setStopWords] = React.useState([]); const [scopeActive, setScopeActive] = React.useState(true); const [savedShipment, setSavedShipment] = React.useState([]); const [justSaved, setJustSaved] = React.useState(false); + const [showResetConfirm, setShowResetConfirm] = React.useState(false); React.useEffect(() => { if (!supabase) return; @@ -168,11 +169,27 @@ export const DriverShipmentPanel = ({ order, onShipmentChange, onSaveShipment, i setComments({}); }; + const currentDeliveryStatus = order?.deliveryStatus || order?.delivery_status; + const isStatusFinal = ["delivered", "problem", "picked_up"].includes(currentDeliveryStatus); + const unshipAll = () => { + if (isStatusFinal && onResetStatus) { + setShowResetConfirm(true); + return; + } setShippedItems(new Set()); setComments({}); }; + const confirmResetAll = () => { + setShippedItems(new Set()); + setComments({}); + setShowResetConfirm(false); + if (onResetStatus) { + onResetStatus(); + } + }; + const shippedCount = items.filter((i) => shippedItems.has(i.id)).length; const unshippedCount = items.length - shippedCount; const allShipped = items.length > 0 && shippedCount === items.length; @@ -232,11 +249,36 @@ export const DriverShipmentPanel = ({ order, onShipmentChange, onSaveShipment, i - + {showResetConfirm && ( +
+

+ Сбросить отгрузку и вернуть статус? +

+

+ Текущий статус («{currentDeliveryStatus === "delivered" ? "Доставлено" : currentDeliveryStatus === "problem" ? "Проблема" : "Вывезено"}») будет сброшен. + Отгрузка будет очищена, логист увидит что доставка требует доработки. +

+
+ + +
+
+ )} +
{items.map((item) => { const isShipped = shippedItems.has(item.id); diff --git a/src/components/logistics/LogisticsReadinessBoard.jsx b/src/components/logistics/LogisticsReadinessBoard.jsx index eb5e035..14c4d65 100644 --- a/src/components/logistics/LogisticsReadinessBoard.jsx +++ b/src/components/logistics/LogisticsReadinessBoard.jsx @@ -158,7 +158,17 @@ const renderRow = (group, onSelectSet) => ( {group.assignedDriverName || }
- {getOrderGroupDisplayStatusLabel(group)} +
+ {getOrderGroupDisplayStatusLabel(group)} + {(group.hasDeliveryProblem || group.has_delivery_problem) && ( + + ⚠ Проблема + + )} +
{formatDateTime(group.updatedAt)} diff --git a/src/components/orders/OrderDetailPanel.jsx b/src/components/orders/OrderDetailPanel.jsx index 7996a99..b0b3cc0 100644 --- a/src/components/orders/OrderDetailPanel.jsx +++ b/src/components/orders/OrderDetailPanel.jsx @@ -1088,7 +1088,27 @@ export const OrderDetailPanel = ({ ) : null} {userRole === "driver" && order ? ( - + { + if (onChangeDeliveryStatus) { + onChangeDeliveryStatus({ + orderGroupId: order.id, + status: "loaded", + }).then((response) => { + if (!response.success) { + setFormMessage(response.error || "Не удалось сбросить статус"); + } else { + setFormMessage("Статус сброшен, отгрузка очищена"); + } + }); + } + }} + isSavingStatusChange={isSavingStatusChange} + /> ) : null} {userRole === "driver" && order && onChangeDeliveryStatus ? ( @@ -1135,7 +1155,10 @@ export const OrderDetailPanel = ({ } } - if (statusOptions.length === 0) return null; + // "Return to work" button for final statuses + const canReturn = ["delivered", "picked_up", "problem"].includes(currentStatus); + + if (statusOptions.length === 0 && !canReturn) return null; return statusOptions.map((statusOption) => { const isSelected = pendingStatus?.value === statusOption.value; @@ -1163,6 +1186,28 @@ export const OrderDetailPanel = ({ ); }); })()} + {(() => { + const currentStatus = order.deliveryStatus || order.delivery_status; + const canReturn = ["delivered", "picked_up", "problem"].includes(currentStatus); + if (!canReturn) return null; + return ( + + ); + })()}
{pendingStatus ? (
diff --git a/src/services/supabase/orderGroupRepository.js b/src/services/supabase/orderGroupRepository.js index 68e0001..2131df0 100644 --- a/src/services/supabase/orderGroupRepository.js +++ b/src/services/supabase/orderGroupRepository.js @@ -409,8 +409,16 @@ export const saveShipmentData = async ({ orderGroupId, shipmentData }) => { ? shipmentData.filter((i) => !i.shipped).map((i) => `${i.name}${i.quantity ? ` (${i.quantity}${i.unit ? ` ${i.unit}` : ""})` : ""}${i.comment ? ` — ${i.comment}` : ""}`).join("; ") : null; - // Any shipment data saved = delivered status. Problems go into has_delivery_problem/delivery_problem_note columns. - const newDeliveryStatus = hasAnyShipped ? "delivered" : undefined; + // If all shipped → delivered. If partial (some shipped, some not) → problem. + // If nothing shipped → keep current status (driver just clearing checkboxes). + let newDeliveryStatus; + if (hasAnyShipped && !hasProblem) { + newDeliveryStatus = "delivered"; + } else if (hasAnyShipped && hasProblem) { + newDeliveryStatus = "problem"; + } else { + newDeliveryStatus = undefined; + } const updatePayload = { driver_shipment_data: shipmentData,