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") {