diff --git a/src/components/admin/StopWordsPanel.jsx b/src/components/admin/StopWordsPanel.jsx new file mode 100644 index 0000000..a0b702c --- /dev/null +++ b/src/components/admin/StopWordsPanel.jsx @@ -0,0 +1,126 @@ +import React from "react"; +import { Panel } from "../UI/Panel"; +import { Button } from "../UI/Button"; +import { supabase } from "../../supabaseClient"; + +export const StopWordsPanel = () => { + const [words, setWords] = React.useState([]); + const [newWord, setNewWord] = React.useState(""); + const [isLoading, setIsLoading] = React.useState(true); + const [error, setError] = React.useState(""); + const [deletingId, setDeletingId] = React.useState(null); + + const loadWords = React.useCallback(async () => { + setIsLoading(true); + setError(""); + const { data, error: fetchError } = await supabase + .from("stop_words") + .select("id, word, created_at") + .order("word", { ascending: true }); + if (fetchError) { + setError(fetchError.message); + } else { + setWords(data || []); + } + setIsLoading(false); + }, []); + + React.useEffect(() => { loadWords(); }, [loadWords]); + + const handleAdd = async () => { + const trimmed = newWord.trim().toLowerCase(); + if (!trimmed) return; + if (words.some((w) => w.word === trimmed)) { + setError("Такое слово уже есть"); + return; + } + setError(""); + const { error: insertError } = await supabase + .from("stop_words") + .insert({ word: trimmed }); + if (insertError) { + setError(insertError.message); + return; + } + setNewWord(""); + await loadWords(); + }; + + const handleDelete = async (id) => { + setDeletingId(id); + const { error: deleteError } = await supabase + .from("stop_words") + .delete() + .eq("id", id); + if (deleteError) { + setError(deleteError.message); + } else { + await loadWords(); + } + setDeletingId(null); + }; + + const handleKeyDown = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + handleAdd(); + } + }; + + return ( + +
+

Стоп-слова

+

+ Позиции с этими словами не показываются клиентам в карточке доставки. + Добавляйте слова-маркеры: «сверление», «обмер» и т.д. +

+
+ +
+ setNewWord(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Новое стоп-слово" + className="flex-1 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-2.5 text-sm !text-[var(--color-text)] outline-none transition focus:border-[var(--color-accent)]" + maxLength={100} + /> + +
+ + {error && ( +

{error}

+ )} + + {isLoading ? ( +

Загрузка...

+ ) : !words.length ? ( +

Стоп-слов пока нет. Добавьте первое.

+ ) : ( +
+ {words.map((w) => ( + + {w.word} + + + ))} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/components/client/OrderCompositionPanel.jsx b/src/components/client/OrderCompositionPanel.jsx index 92f7d97..2e5bcf1 100644 --- a/src/components/client/OrderCompositionPanel.jsx +++ b/src/components/client/OrderCompositionPanel.jsx @@ -60,14 +60,26 @@ 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)); +}; + export const OrderCompositionPanel = ({ invitation = {} }) => { + const stopWords = invitation.stopWords || []; const rawItems = invitation.orderItems || invitation.items || []; - const products = flattenOrderProducts(rawItems); + const allProducts = flattenOrderProducts(rawItems); + const products = stopWords.length + ? allProducts.filter((p) => !matchesStopWord(p.name, stopWords)) + : allProducts; + + const filteredCount = allProducts.length - products.length; const reference = getInvitationReferenceLabel(invitation); const [isExpanded, setIsExpanded] = React.useState(false); - if (products.length === 0) return null; + if (products.length === 0 && filteredCount === 0) return null; return ( @@ -106,8 +118,13 @@ export const OrderCompositionPanel = ({ invitation = {} }) => { ) : null} ))} + {products.length === 0 && filteredCount > 0 && ( +

+ Все позиции исключены из отображения. +

+ )} )}
); -}; +}; \ No newline at end of file diff --git a/src/pages/DashboardPage.jsx b/src/pages/DashboardPage.jsx index ab4d399..a5cb88d 100644 --- a/src/pages/DashboardPage.jsx +++ b/src/pages/DashboardPage.jsx @@ -6,6 +6,7 @@ import { OrdersTable } from "../components/orders/OrdersTable"; import { AdminDashboard } from "../components/admin/AdminDashboard"; import UserManagementPanel from "../components/admin/UserManagementPanel"; import ErrorLogPanel from "../components/admin/ErrorLogPanel"; +import { StopWordsPanel } from "../components/admin/StopWordsPanel"; import { Panel } from "../components/UI/Panel"; import { ProductGuidePanel } from "../components/dashboard/ProductGuidePanel"; import { useAuth } from "../context/AuthContext"; @@ -20,6 +21,7 @@ const MEGA_ADMIN_NAV = [ { key: "orders", label: "Группы", description: "Реестр групп доставки.", badge: null }, { key: "users", label: "Пользователи", description: "Управление пользователями и ролями.", badge: null }, { key: "errors", label: "Ошибки", description: "Журнал ошибок приложения.", badge: null }, + { key: "stop_words", label: "Стоп-слова", description: "Слова, исключаемые из клиентской карточки.", badge: null }, ]; const ROLE_SECTION = { @@ -91,6 +93,7 @@ export const DashboardPage = () => { { key: "analytics", label: "Аналитика", description: "Статистика доставки.", badge: null }, { key: "orders", label: "Группы", description: "Реестр групп доставки.", badge: String(allOrderGroups.length || orderGroups.length || 0) }, { key: "users", label: "Пользователи", description: "Управление пользователями.", badge: null }, + { key: "stop_words", label: "Стоп-слова", description: "Слова, исключаемые из карточки.", badge: null }, ] : userRole === "logistician" ? [ @@ -113,6 +116,7 @@ export const DashboardPage = () => { if (activeSection === "guide") return ; if (activeSection === "analytics") return
; if (activeSection === "users") return
; + if (activeSection === "stop_words") return
; if (activeSection === "errors") return
; if (userRole === "driver") {