supersam/src/components/admin/SuggestionsPanel.jsx

259 lines
10 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from "react";
import { Panel } from "../UI/Panel";
import { useAuth } from "../../context/AuthContext";
const CATEGORY_OPTIONS = [
{ value: "feature", label: "Новая функция", icon: "✨" },
{ value: "improvement", label: "Улучшение", icon: "🔧" },
{ value: "bug", label: "Проблема", icon: "🐛" },
{ value: "other", label: "Другое", icon: "💬" },
];
const STATUS_MAP = {
new: { label: "Новое", color: "#3b82f6" },
reviewed: { label: "Рассмотрено", color: "#f59e0b" },
accepted: { label: "Принято", color: "#22c55e" },
declined: { label: "Отклонено", color: "#ef4444" },
implemented: { label: "Реализовано", color: "#8b5cf6" },
};
export const SuggestionsPanel = () => {
const { user } = useAuth();
const isAdmin = ["admin", "mega_admin"].includes(user?.role);
const [suggestions, setSuggestions] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [newText, setNewText] = React.useState("");
const [newCategory, setNewCategory] = React.useState("feature");
const [submitting, setSubmitting] = React.useState(false);
const [message, setMessage] = React.useState("");
const getSupabase = React.useCallback(async () => {
const { createClient } = await import("@supabase/supabase-js");
return createClient(
import.meta.env.VITE_SUPABASE_URL || window.__SUPABASE_URL__,
import.meta.env.VITE_SUPABASE_ANON_KEY || window.__SUPABASE_ANON_KEY__
);
}, []);
const fetchSuggestions = React.useCallback(async () => {
try {
const supabase = await getSupabase();
const { data } = await supabase
.from("suggestions")
.select("*")
.order("created_at", { ascending: false });
setSuggestions(data || []);
} catch (e) {
console.error("fetch suggestions error", e);
} finally {
setLoading(false);
}
}, [getSupabase]);
React.useEffect(() => { fetchSuggestions(); }, [fetchSuggestions]);
const handleSubmit = async () => {
if (!newText.trim()) return;
setSubmitting(true);
try {
const supabase = await getSupabase();
const { error } = await supabase.from("suggestions").insert({
author_id: user?.id,
author_name: user?.name || user?.email || "Сотрудник",
author_role: user?.role || "unknown",
content: newText.trim(),
category: newCategory,
});
if (error) throw error;
setNewText("");
setMessage("✅ Предложение отправлено!");
fetchSuggestions();
setTimeout(() => setMessage(""), 3000);
} catch (e) {
setMessage("❌ Ошибка: " + e.message);
} finally {
setSubmitting(false);
}
};
const handleStatusChange = async (id, newStatus, adminComment) => {
try {
const supabase = await getSupabase();
const updates = { status: newStatus, updated_at: new Date().toISOString() };
if (adminComment !== undefined) updates.admin_comment = adminComment;
const { error } = await supabase.from("suggestions").update(updates).eq("id", id);
if (error) throw error;
fetchSuggestions();
} catch (e) {
console.error("update suggestion error", e);
}
};
return (
<div className="space-y-6">
<Panel className="space-y-4 p-5">
<div className="flex items-center gap-3">
<span className="text-xl">💡</span>
<h2 className="text-sm font-semibold uppercase tracking-[0.16em] text-[var(--color-text)]">
Предложить улучшение
</h2>
</div>
<p className="text-sm text-[var(--color-text-muted)]">
Есть идея? Опишите админы рассмотрят.
</p>
<div className="flex flex-wrap gap-2">
{CATEGORY_OPTIONS.map((cat) => (
<button
key={cat.value}
type="button"
onClick={() => setNewCategory(cat.value)}
className={[
"rounded-xl border px-3 py-1.5 text-xs font-medium transition",
newCategory === cat.value
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] !text-[var(--color-text)]"
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)]"
].join(" ")}
>
{cat.icon} {cat.label}
</button>
))}
</div>
<textarea
value={newText}
onChange={(e) => setNewText(e.target.value)}
placeholder="Опишите предложение..."
rows={3}
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] px-4 py-3 text-sm text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)] focus:outline-none resize-none"
/>
<div className="flex items-center gap-3">
<button
type="button"
disabled={!newText.trim() || submitting}
onClick={handleSubmit}
className="rounded-xl bg-[var(--color-accent)] px-5 py-2.5 text-sm font-semibold text-white transition hover:opacity-90 disabled:opacity-40"
>
{submitting ? "Отправка..." : "Отправить"}
</button>
{message && <span className="text-sm text-[var(--color-text-muted)]">{message}</span>}
</div>
</Panel>
<Panel className="space-y-3 p-5">
<h2 className="text-sm font-semibold uppercase tracking-[0.16em] text-[var(--color-text)]">
Все предложения
</h2>
{loading ? (
<p className="text-sm text-[var(--color-text-muted)]">Загрузка...</p>
) : suggestions.length === 0 ? (
<p className="text-sm text-[var(--color-text-muted)]">Пока нет предложений</p>
) : (
<div className="space-y-3">
{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;
return (
<SuggestionCard
key={s.id}
suggestion={s}
category={cat}
status={st}
isAdmin={isAdmin}
onStatusChange={handleStatusChange}
/>
);
})}
</div>
)}
</Panel>
</div>
);
};
const SuggestionCard = ({ suggestion: s, category, status, isAdmin, onStatusChange }) => {
const [editing, setEditing] = React.useState(false);
const [comment, setComment] = React.useState(s.admin_comment || "");
const formatDate = (d) => {
if (!d) return "";
const date = new Date(d);
return date.toLocaleDateString("ru-RU", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
};
return (
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-4 space-y-2">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2">
<span className="text-base">{category.icon}</span>
<span className="text-xs font-medium text-[var(--color-text-muted)]">{category.label}</span>
<span
className="rounded-lg px-2 py-0.5 text-[10px] font-semibold uppercase"
style={{ backgroundColor: status.color + "18", color: status.color }}
>
{status.label}
</span>
</div>
<span className="text-[10px] text-[var(--color-text-muted)] whitespace-nowrap">{formatDate(s.created_at)}</span>
</div>
<p className="text-sm text-[var(--color-text)] whitespace-pre-wrap">{s.content}</p>
<div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)]">
<span className="font-medium">{s.author_name}</span>
<span>·</span>
<span>{s.author_role}</span>
</div>
{s.admin_comment && (
<div className="rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] p-3 text-xs text-[var(--color-text-muted)]">
<span className="font-semibold">💬 Админ:</span> {s.admin_comment}
</div>
)}
{isAdmin && (
<div className="border-t border-[var(--color-border)] pt-2 mt-2 space-y-2">
<div className="flex flex-wrap gap-1.5">
{Object.entries(STATUS_MAP).map(([key, val]) => (
<button
key={key}
type="button"
onClick={() => onStatusChange(s.id, key)}
className={[
"rounded-lg px-2 py-1 text-[10px] font-semibold uppercase transition",
s.status === key ? "text-white" : "opacity-60 hover:opacity-100"
].join(" ")}
style={{
backgroundColor: s.status === key ? val.color : val.color + "22",
color: s.status === key ? "#fff" : val.color
}}
>
{val.label}
</button>
))}
</div>
{editing ? (
<div className="flex gap-2">
<input
type="text"
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Комментарий админа..."
className="flex-1 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-1.5 text-xs text-[var(--color-text)]"
/>
<button
type="button"
onClick={() => { onStatusChange(s.id, s.status, comment); setEditing(false); }}
className="rounded-lg bg-[var(--color-accent)] px-3 py-1.5 text-xs font-semibold text-white"
>
Сохранить
</button>
</div>
) : (
<button
type="button"
onClick={() => setEditing(true)}
className="text-xs text-[var(--color-accent)] hover:underline"
>
Комментарий
</button>
)}
</div>
)}
</div>
);
};