From 2f2dae686cbabc2d2bff318e351d668ea865b376 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 12 Jun 2026 15:47:55 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20Phase=203=20resilience=20=E2=80=94?= =?UTF-8?q?=20ErrorBoundary,=20split=20saving=20states,=20extract=20useSto?= =?UTF-8?q?pWords?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ErrorBoundary: Russian UI, compact mode for card-level errors, reload button - ErrorBoundary wraps OrderDetailPanel in GroupDetailPage + DeliverySetDetailPanel - Split isSavingDeliveryChoice into 3 independent states: isSavingDeliveryChoice (delivery tab save) isSavingDriverAssignment (driver assign) isSavingStatusChange (status change) - Extract useStopWords hook + matchesStopWord to shared hooks/useStopWords.js - Remove 3x duplicated matchesStopWord from OrderDetailPanel, DriverShipmentPanel, OrderCompositionPanel - Remove useStopWords + supabase import from OrderDetailPanel --- src/components/ErrorBoundary.jsx | 77 +++++++++++-------- .../client/OrderCompositionPanel.jsx | 7 +- src/components/driver/DriverShipmentPanel.jsx | 7 +- .../logistics/DeliverySetDetailPanel.jsx | 5 +- .../orders/DriverAssignmentPanel.jsx | 8 +- src/components/orders/OrderDetailPanel.jsx | 44 ++++------- src/components/orders/StatusActionPanel.jsx | 4 +- src/hooks/useOrderGroups.js | 12 ++- src/hooks/useStopWords.js | 33 ++++++++ src/pages/GroupDetailPage.jsx | 11 ++- 10 files changed, 120 insertions(+), 88 deletions(-) create mode 100644 src/hooks/useStopWords.js diff --git a/src/components/ErrorBoundary.jsx b/src/components/ErrorBoundary.jsx index 617e83b..5a3c0f2 100644 --- a/src/components/ErrorBoundary.jsx +++ b/src/components/ErrorBoundary.jsx @@ -12,7 +12,6 @@ class ErrorBoundary extends React.Component { } componentDidCatch(error, errorInfo) { - // Extract component stack for richer context const componentInfo = { component: errorInfo?.componentStack || null, props: this.props, @@ -25,47 +24,61 @@ class ErrorBoundary extends React.Component { this.setState({ hasError: false, error: null }); }; + handleReload = () => { + window.location.reload(); + }; + renderDefaultFallback() { + const { compact } = this.props; + + if (compact) { + return ( +
+

Что-то пошло не так

+ +
+ ); + } + return ( -
-

- Something went wrong +
+
⚠️
+

+ Что-то пошло не так

-

- An unexpected error occurred. You can try again. +

+ Произошла непредвиденная ошибка. Попробуйте обновить страницу или вернуться позже.

- + {this.state.error && process.env.NODE_ENV === 'development' && ( +
+            {this.state.error.message}
+          
+ )} +
+ + +
); } render() { if (this.state.hasError) { - // Allow custom fallback render function if (typeof this.props.fallback === 'function') { return this.props.fallback(this.state.error, this.handleRetry); } diff --git a/src/components/client/OrderCompositionPanel.jsx b/src/components/client/OrderCompositionPanel.jsx index 1718063..da7b553 100644 --- a/src/components/client/OrderCompositionPanel.jsx +++ b/src/components/client/OrderCompositionPanel.jsx @@ -3,6 +3,7 @@ import { Badge } from "../UI/Badge"; import { Panel } from "../UI/Panel"; import { getInvitationReferenceLabel } from "./invitationReference"; import { supabase } from "../../supabaseClient"; +import { matchesStopWord } from "../../hooks/useStopWords"; const flattenOrderProducts = (rawItems) => { if (!Array.isArray(rawItems) || rawItems.length === 0) return []; @@ -64,12 +65,6 @@ const flattenOrderProducts = (rawItems) => { return products; }; -const matchesStopWord = (name, stopWords) => { - if (!stopWords || !stopWords.length) return false; - const lower = name.toLowerCase(); - return stopWords.some((sw) => lower.includes(sw.toLowerCase())); -}; - export const OrderCompositionPanel = ({ invitation = {} }) => { const [stopWords, setStopWords] = React.useState([]); const [stopWordsLoaded, setStopWordsLoaded] = React.useState(false); diff --git a/src/components/driver/DriverShipmentPanel.jsx b/src/components/driver/DriverShipmentPanel.jsx index d7bf5e9..cf8e3ef 100644 --- a/src/components/driver/DriverShipmentPanel.jsx +++ b/src/components/driver/DriverShipmentPanel.jsx @@ -3,12 +3,7 @@ import { supabase } from "../../supabaseClient"; import { Badge } from "../UI/Badge"; import { Button } from "../UI/Button"; import { Panel } from "../UI/Panel"; - -const matchesStopWord = (name, stopWords) => { - if (!stopWords || !stopWords.length) return false; - const lower = name.toLowerCase(); - return stopWords.some((sw) => lower.includes(sw.toLowerCase())); -}; +import { matchesStopWord } from "../../hooks/useStopWords"; const parseOrderItems = (order) => { if (!order) return []; diff --git a/src/components/logistics/DeliverySetDetailPanel.jsx b/src/components/logistics/DeliverySetDetailPanel.jsx index f3b403e..69ed6d6 100644 --- a/src/components/logistics/DeliverySetDetailPanel.jsx +++ b/src/components/logistics/DeliverySetDetailPanel.jsx @@ -1,5 +1,6 @@ import { Button } from "../UI/Button"; import { OrderDetailPanel } from "../orders/OrderDetailPanel"; +import ErrorBoundary from "../ErrorBoundary"; export const DeliverySetDetailPanel = ({ deliverySet, onClose }) => { if (!deliverySet) { @@ -8,7 +9,9 @@ export const DeliverySetDetailPanel = ({ deliverySet, onClose }) => { return (
- + + + {onClose ? (
diff --git a/src/components/orders/DriverAssignmentPanel.jsx b/src/components/orders/DriverAssignmentPanel.jsx index af511cb..069f585 100644 --- a/src/components/orders/DriverAssignmentPanel.jsx +++ b/src/components/orders/DriverAssignmentPanel.jsx @@ -8,7 +8,7 @@ const DriverAssignmentPanel = ({ order, userRole, canManageDelivery, - isSavingDeliveryChoice, + isSavingDriverAssignment, selectedDriverId, onDriverSelect, onConfirmDriver, @@ -63,7 +63,7 @@ const DriverAssignmentPanel = ({ onChange={(e) => { onDriverSelect(e.target.value); }} - disabled={isSavingDeliveryChoice} + disabled={isSavingDriverAssignment} > {drivers.map((driver) => ( @@ -73,9 +73,9 @@ const DriverAssignmentPanel = ({
) : null} diff --git a/src/components/orders/OrderDetailPanel.jsx b/src/components/orders/OrderDetailPanel.jsx index ebc0580..102a72d 100644 --- a/src/components/orders/OrderDetailPanel.jsx +++ b/src/components/orders/OrderDetailPanel.jsx @@ -47,7 +47,7 @@ import { DriverShipmentPanel } from "../driver/DriverShipmentPanel"; import { CalendarWidget } from "./CalendarWidget"; import { StatusActionPanel } from "./StatusActionPanel"; import { DriverAssignmentPanel } from "./DriverAssignmentPanel"; -import { supabase } from "../../supabaseClient"; +import { matchesStopWord, useStopWords } from "../../hooks/useStopWords"; import { getOrderGroupDeliveryStatusLabel, getOrderGroupDisplayStatusLabel, @@ -326,27 +326,7 @@ const normalizeDateForInput = (value) => { return ""; }; -const matchesStopWord = (name, stopWords) => { - if (!stopWords || !stopWords.length) return false; - const lower = name.toLowerCase(); - return stopWords.some((sw) => lower.includes(sw.toLowerCase())); -}; - -const useStopWords = () => { - const [stopWords, setStopWords] = React.useState([]); - const [active, setActive] = React.useState(true); - React.useEffect(() => { - if (!supabase) return; - Promise.all([ - supabase.from("stop_words").select("word").then(r => r.data || []), - supabase.from("stop_words_scope").select("scope").eq("id", 1).single().then(r => r.data), - ]).then(([words, scopeRow]) => { - setStopWords(words.map((d) => d.word)); - setActive(scopeRow?.scope !== "client_only"); - }); - }, []); - return { stopWords, active }; -}; +const renderValue = (value) => value || "Нет данных"; const CollapsibleOrderComposition = ({ order }) => { const [isExpanded, setIsExpanded] = React.useState(false); @@ -431,7 +411,7 @@ const CollapsibleOrderComposition = ({ order }) => { ); }; -const PaidStoragePanel = ({ order, onChangeDeliveryStatus, isSavingDeliveryChoice, setFormMessage }) => { +const PaidStoragePanel = ({ order, onChangeDeliveryStatus, isSavingStatusChange, setFormMessage }) => { const [showConfirm, setShowConfirm] = React.useState(false); const isPaidStorage = (order.deliveryStatus || order.delivery_status) === "paid_storage"; @@ -461,7 +441,7 @@ const PaidStoragePanel = ({ order, onChangeDeliveryStatus, isSavingDeliveryChoic } }); }} - disabled={isSavingDeliveryChoice} + disabled={isSavingStatusChange} > Отменить платное хранение @@ -497,14 +477,14 @@ const PaidStoragePanel = ({ order, onChangeDeliveryStatus, isSavingDeliveryChoic } }); }} - disabled={isSavingDeliveryChoice} + disabled={isSavingStatusChange} > Да, перевести @@ -514,7 +494,7 @@ const PaidStoragePanel = ({ order, onChangeDeliveryStatus, isSavingDeliveryChoic @@ -561,6 +541,8 @@ export const OrderDetailPanel = ({ canManageDelivery = false, onSaveManualDeliveryChoice, isSavingDeliveryChoice = false, + isSavingDriverAssignment = false, + isSavingStatusChange = false, drivers = [], onAssignDriver, onChangeDeliveryStatus, @@ -1004,7 +986,7 @@ export const OrderDetailPanel = ({ order={order} userRole={userRole} canManageDelivery={canManageDelivery} - isSavingDeliveryChoice={isSavingDeliveryChoice} + isSavingDriverAssignment={isSavingDriverAssignment} selectedDriverId={selectedDriverId} onDriverSelect={(id) => { setSelectedDriverId(id); setDriverMessage(""); }} onConfirmDriver={() => setConfirmAction({ type: 'driver' })} @@ -1017,7 +999,7 @@ export const OrderDetailPanel = ({ order={order} userRole={userRole} canManageDelivery={canManageDelivery} - isSavingDeliveryChoice={isSavingDeliveryChoice} + isSavingStatusChange={isSavingStatusChange} onConfirmStatus={(action) => { if (action.type === "hint") { setFormMessage(action.hint); @@ -1035,7 +1017,7 @@ export const OrderDetailPanel = ({ ) : null} @@ -1112,7 +1094,7 @@ export const OrderDetailPanel = ({
diff --git a/src/hooks/useOrderGroups.js b/src/hooks/useOrderGroups.js index 451523a..0a63e33 100644 --- a/src/hooks/useOrderGroups.js +++ b/src/hooks/useOrderGroups.js @@ -19,6 +19,8 @@ export const useOrderGroups = () => { const [isLoading, setIsLoading] = React.useState(true); const [loadError, setLoadError] = React.useState(""); const [isSavingDeliveryChoice, setIsSavingDeliveryChoice] = React.useState(false); + const [isSavingDriverAssignment, setIsSavingDriverAssignment] = React.useState(false); + const [isSavingStatusChange, setIsSavingStatusChange] = React.useState(false); React.useEffect(() => { let cancelled = false; @@ -142,7 +144,7 @@ export const useOrderGroups = () => { }, []); const assignDriver = React.useCallback(async ({ orderGroupId, driverId }) => { - setIsSavingDeliveryChoice(true); + setIsSavingDriverAssignment(true); try { const result = await assignDriverToOrderGroup({ orderGroupId, driverId }); @@ -165,12 +167,12 @@ export const useOrderGroups = () => { error: getErrorMessage(error, "Не удалось назначить водителя"), }; } finally { - setIsSavingDeliveryChoice(false); + setIsSavingDriverAssignment(false); } }, []); const changeDeliveryStatus = React.useCallback(async ({ orderGroupId, status, details }) => { - setIsSavingDeliveryChoice(true); + setIsSavingStatusChange(true); try { const result = await updateDeliveryStatus({ orderGroupId, status, details }); if (result.error) { @@ -189,7 +191,7 @@ export const useOrderGroups = () => { error: getErrorMessage(error, "Не удалось обновить статус"), }; } finally { - setIsSavingDeliveryChoice(false); + setIsSavingStatusChange(false); } }, []); @@ -210,6 +212,8 @@ export const useOrderGroups = () => { assignDriver, changeDeliveryStatus, isSavingDeliveryChoice, + isSavingDriverAssignment, + isSavingStatusChange, isLoading, loadError, }; diff --git a/src/hooks/useStopWords.js b/src/hooks/useStopWords.js new file mode 100644 index 0000000..12146af --- /dev/null +++ b/src/hooks/useStopWords.js @@ -0,0 +1,33 @@ +import React from "react"; +import { supabase } from "../supabaseClient"; + +const matchesStopWord = (name, stopWords) => { + if (!stopWords || !stopWords.length) return false; + const lower = name.toLowerCase(); + return stopWords.some((sw) => lower.includes(sw.toLowerCase())); +}; + +const useStopWords = () => { + const [stopWords, setStopWords] = React.useState([]); + const [active, setActive] = React.useState(true); + + React.useEffect(() => { + if (!supabase) return; + Promise.all([ + supabase.from("stop_words").select("word").then((r) => r.data || []), + supabase + .from("stop_words_scope") + .select("scope") + .eq("id", 1) + .single() + .then((r) => r.data), + ]).then(([words, scopeRow]) => { + setStopWords(words.map((d) => d.word)); + setActive(scopeRow?.scope !== "client_only"); + }); + }, []); + + return { stopWords, active }; +}; + +export { matchesStopWord, useStopWords }; \ No newline at end of file diff --git a/src/pages/GroupDetailPage.jsx b/src/pages/GroupDetailPage.jsx index b033e55..dd2be64 100644 --- a/src/pages/GroupDetailPage.jsx +++ b/src/pages/GroupDetailPage.jsx @@ -6,6 +6,7 @@ import React from "react"; import { Navigate, useNavigate, useParams, useLocation } from "react-router-dom"; import { OrderDetailPanel } from "../components/orders/OrderDetailPanel"; +import ErrorBoundary from "../components/ErrorBoundary"; import { Button } from "../components/UI/Button"; import { Panel } from "../components/UI/Panel"; import { SkeletonPanel } from "../components/UI/Loading"; @@ -30,6 +31,8 @@ export const GroupDetailPage = () => { setSelectedOrderGroupId, saveManualDeliveryChoice, isSavingDeliveryChoice, + isSavingDriverAssignment, + isSavingStatusChange, assignDriver, changeDeliveryStatus, isLoading, @@ -97,16 +100,20 @@ export const GroupDetailPage = () => { {isLoading ? ( ) : order ? ( - + + /> + ) : ( Группа не найдена.