diff --git a/components/studio/AdminPanel.tsx b/components/studio/AdminPanel.tsx new file mode 100644 index 0000000000000000000000000000000000000000..aa1968717de94e6fe999c299cfb07dcbdb734855 --- /dev/null +++ b/components/studio/AdminPanel.tsx @@ -0,0 +1,355 @@ +import { useCallback, useEffect, useState } from "react"; +import { Check, Copy, FolderOpen, KeyRound, MessageSquare, RotateCcw, Trash2, UserPlus, Users } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Badge } from "@/components/ui/badge"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + createUser, + deleteUser, + listUsers, + resetPassword, + roleLabel, + statusLabel, + type StoredUser, +} from "@/lib/auth-service"; +import { + countProjectsByOwner, + formatStamp, + listProjects, + removeProjectsOfOwner, + type StoredProject, +} from "@/lib/studio-storage"; +import { listFeedback, markFeedbackRead, removeFeedback, type FeedbackItem } from "@/lib/feedback-service"; + +type Props = { + open: boolean; + onOpenChange: (v: boolean) => void; +}; + +export default function AdminPanel({ open, onOpenChange }: Props) { + const [users, setUsers] = useState([]); + const [counts, setCounts] = useState>({}); + const [createOpen, setCreateOpen] = useState(false); + const [newLogin, setNewLogin] = useState(""); + const [createError, setCreateError] = useState(null); + const [credentials, setCredentials] = useState<{ username: string; password: string } | null>(null); + const [toDelete, setToDelete] = useState(null); + const [viewing, setViewing] = useState<{ user: StoredUser; items: StoredProject[] } | null>(null); + const [feedback, setFeedback] = useState([]); + + const refresh = useCallback(() => { + setUsers(listUsers()); + setCounts(countProjectsByOwner()); + setFeedback(listFeedback()); + }, []); + + useEffect(() => { + if (open) refresh(); + }, [open, refresh]); + + const totalProjects = Object.values(counts).reduce((a, b) => a + b, 0); + const activeUsers = users.filter((u) => u.status === "active").length; + const pendingUsers = users.length - activeUsers; + + const copy = (value: string) => { + void navigator.clipboard?.writeText(value); + }; + + const submitCreate = () => { + const result = createUser(newLogin); + if (!result.ok) { + setCreateError(result.error); + return; + } + setCreateError(null); + setCreateOpen(false); + setNewLogin(""); + setCredentials({ username: result.user.username, password: result.tempPassword }); + refresh(); + }; + + return ( + <> + + + + + Панель админа + + Управление пользователями студии и обзор сохранённых схем. + + +
+ {[ + { label: "Пользователей", value: users.length }, + { label: "Активных", value: activeUsers }, + { label: "Ожидают входа", value: pendingUsers }, + { label: "Всего схем", value: totalProjects }, + { label: "Новых пожеланий", value: feedback.filter((f) => !f.read).length }, + ].map((s) => ( +
+

{s.label}

+

{s.value}

+
+ ))} +
+ +
+

+ Пожелания пользователей +

+ {feedback.length === 0 ? ( +

Пока нет сообщений.

+ ) : ( +
    + {feedback.map((f) => ( +
  • +
    +

    + {f.username} · {formatStamp(f.createdAt)} + {!f.read && новое} +

    +

    {f.text}

    +
    +
    + {!f.read && ( + + )} + +
    +
  • + ))} +
+ )} +
+ + +
+ +
+ +
+ + + + Логин + Роль + Создан + Статус + Схемы + Действия + + + + {users.map((u) => ( + + {u.username} + {roleLabel(u.role)} + {formatStamp(u.createdAt)} + + {statusLabel(u.status)} + + {counts[u.id] ?? 0} + +
+ + + +
+
+
+ ))} +
+
+
+
+
+ + {/* Создание пользователя */} + + + + Создать пользователя + + Укажите логин — система выдаст одноразовый временный пароль для первого входа. + + +
+ + { + setNewLogin(e.target.value); + setCreateError(null); + }} + className="h-12" + /> + {createError &&

{createError}

} +
+ +
+
+ + {/* Показ временного пароля */} + !v && setCredentials(null)}> + + + + Временный пароль + + + Передайте пароль пользователю «{credentials?.username}». При первом входе он задаст постоянный пароль. + + +
+ + {credentials?.password} + + +
+ +
+
+ + {/* Проекты пользователя */} + !v && setViewing(null)}> + + + Проекты «{viewing?.user.username}» + Схемы, сохранённые этим пользователем. + + {viewing && viewing.items.length === 0 ? ( +

У пользователя пока нет схем.

+ ) : ( +
+ {viewing?.items.map((p) => ( +
+ {`Превью +
+

{p.title}

+

+ {formatStamp(p.updatedAt)} · элементов: {p.elements} +

+
+
+ ))} +
+ )} +
+
+ + !v && setToDelete(null)}> + + + Удалить пользователя? + + Пользователь «{toDelete?.username}» и все его схемы будут удалены безвозвратно. + + + + Отмена + { + if (toDelete) { + removeProjectsOfOwner(toDelete.id); + deleteUser(toDelete.id); + } + setToDelete(null); + refresh(); + }} + > + Удалить + + + + + + ); +} diff --git a/components/studio/FeedbackDialog.tsx b/components/studio/FeedbackDialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..51561befd68f8d9173db55524cf1b020d830b800 --- /dev/null +++ b/components/studio/FeedbackDialog.tsx @@ -0,0 +1,52 @@ +import { useState } from "react"; +import { MessageSquarePlus, Send } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Textarea } from "@/components/ui/textarea"; +import { addFeedback } from "@/lib/feedback-service"; +import type { StoredUser } from "@/lib/auth-service"; + +type Props = { + open: boolean; + onOpenChange: (v: boolean) => void; + user: StoredUser; +}; + +export default function FeedbackDialog({ open, onOpenChange, user }: Props) { + const [text, setText] = useState(""); + const [sent, setSent] = useState(false); + + const submit = () => { + if (text.trim().length < 3) return; + addFeedback(user.id, user.username, text); + setText(""); + setSent(true); + window.setTimeout(() => { + setSent(false); + onOpenChange(false); + }, 1200); + }; + + return ( + + + + + Чего не хватает? + + Напишите пожелание — оно придёт администратору студии. + +