From efe98c09d4859f72bc32cb731b010bc02557fc93 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 16 Jul 2026 12:30:14 +0000 Subject: [PATCH] feat: suggestions voting + logistician access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add suggestion_votes table (DB migration applied) - Vote/unvote with optimistic UI update - Sort suggestions by vote_count desc, then created_at desc - Add 'Предложения' tab to logistician role - Vote button shows count, highlighted when voted --- src/components/admin/SuggestionsPanel.jsx | 89 ++++++++++++++++++++--- src/pages/DashboardPage.jsx | 1 + 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/src/components/admin/SuggestionsPanel.jsx b/src/components/admin/SuggestionsPanel.jsx index b12c14c..abb5756 100644 --- a/src/components/admin/SuggestionsPanel.jsx +++ b/src/components/admin/SuggestionsPanel.jsx @@ -27,6 +27,7 @@ export const SuggestionsPanel = () => { const [newCategory, setNewCategory] = React.useState("feature"); const [submitting, setSubmitting] = React.useState(false); const [message, setMessage] = React.useState(""); + const [myVotes, setMyVotes] = React.useState(new Set()); const getSupabase = React.useCallback(async () => { const { createClient } = await import("@supabase/supabase-js"); @@ -42,14 +43,23 @@ export const SuggestionsPanel = () => { const { data } = await supabase .from("suggestions") .select("*") + .order("vote_count", { ascending: false }) .order("created_at", { ascending: false }); + + // Fetch my votes + const { data: votesData } = await supabase + .from("suggestion_votes") + .select("suggestion_id") + .eq("user_id", user?.id); + setMyVotes(new Set((votesData || []).map((v) => v.suggestion_id))); + setSuggestions(data || []); } catch (e) { console.error("fetch suggestions error", e); } finally { setLoading(false); } - }, [getSupabase]); + }, [getSupabase, user?.id]); React.useEffect(() => { fetchSuggestions(); }, [fetchSuggestions]); @@ -90,6 +100,40 @@ export const SuggestionsPanel = () => { } }; + const handleVote = async (suggestionId) => { + const hasVoted = myVotes.has(suggestionId); + try { + const supabase = await getSupabase(); + if (hasVoted) { + await supabase + .from("suggestion_votes") + .delete() + .eq("suggestion_id", suggestionId) + .eq("user_id", user?.id); + setMyVotes((prev) => { + const next = new Set(prev); + next.delete(suggestionId); + return next; + }); + } else { + await supabase + .from("suggestion_votes") + .insert({ suggestion_id: suggestionId, user_id: user?.id }); + setMyVotes((prev) => new Set(prev).add(suggestionId)); + } + fetchSuggestions(); + } catch (e) { + console.error("vote error", e); + // Revert optimistic update + setMyVotes((prev) => { + const next = new Set(prev); + if (hasVoted) next.add(suggestionId); + else next.delete(suggestionId); + return next; + }); + } + }; + return (
@@ -100,7 +144,7 @@ export const SuggestionsPanel = () => {

- Есть идея? Опишите — админы рассмотрят. + Есть идея? Опишите — другие смогут проголосовать, админы рассмотрят.

{CATEGORY_OPTIONS.map((cat) => ( @@ -140,9 +184,14 @@ export const SuggestionsPanel = () => { -

- Все предложения -

+
+

+ Все предложения +

+ + ↑ Сортировка: по голосам + +
{loading ? (

Загрузка...

) : suggestions.length === 0 ? ( @@ -152,6 +201,7 @@ export const SuggestionsPanel = () => { {suggestions.map((s) => { const cat = CATEGORY_OPTIONS.find((c) => c.value === s.category) || CATEGORY_OPTIONS[3]; const st = STATUS_MAP[s.status] || STATUS_MAP.new; + const hasVoted = myVotes.has(s.id); return ( { status={st} isAdmin={isAdmin} onStatusChange={handleStatusChange} + hasVoted={hasVoted} + onVote={handleVote} /> ); })} @@ -170,7 +222,7 @@ export const SuggestionsPanel = () => { ); }; -const SuggestionCard = ({ suggestion: s, category, status, isAdmin, onStatusChange }) => { +const SuggestionCard = ({ suggestion: s, category, status, isAdmin, onStatusChange, hasVoted, onVote }) => { const [editing, setEditing] = React.useState(false); const [comment, setComment] = React.useState(s.admin_comment || ""); @@ -196,10 +248,27 @@ const SuggestionCard = ({ suggestion: s, category, status, isAdmin, onStatusChan {formatDate(s.created_at)}

{s.content}

-
- {s.author_name} - · - {s.author_role} +
+
+ {s.author_name} + · + {s.author_role} +
+ {/* Vote button */} +
{s.admin_comment && (
diff --git a/src/pages/DashboardPage.jsx b/src/pages/DashboardPage.jsx index 990e665..39dc38f 100644 --- a/src/pages/DashboardPage.jsx +++ b/src/pages/DashboardPage.jsx @@ -141,6 +141,7 @@ export const DashboardPage = () => { : userRole === "logistician" ? [ { key: "logistics", label: "Логистика", description: "Группы доставки по готовности к уведомлению.", badge: String(allOrderGroups.length || orderGroups.length || 0) }, + { key: "suggestions", label: "Предложения", description: "Предложить улучшение.", badge: null }, ] : [ { key: section.key, label: section.label, description: section.description, badge: String(userRole === "driver" ? driverOrderCount : (allOrderGroups.length || orderGroups.length || 0)) },