spomeni/src/app/admin/users/page.tsx
dimitar 9ca66fc753 feat(admin): add admin panel with dashboard, users, and codes management
- Add admin layout with sidebar navigation and session guard
- Create AdminSidebar client component with role-based nav links
- Add dashboard page showing stats (admin count, code counts)
- Add users management page (SuperAdmin only): list, create, delete,
  and reset passwords for admin users
- Add codes management page: list all codes, generate new codes,
  delete unused codes
- Add API routes for admin user CRUD (GET, POST, DELETE, PUT)
- Add API routes for code management (GET, POST, DELETE)
- All UI in Macedonian
2026-07-29 18:56:49 +02:00

188 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

"use client";
import { useEffect, useState } from "react";
interface AdminUser {
id: string;
username: string;
role: string;
createdAt: string;
}
export default function AdminUsersPage() {
const [users, setUsers] = useState<AdminUser[]>([]);
const [loading, setLoading] = useState(true);
const [showCreate, setShowCreate] = useState(false);
const [newUsername, setNewUsername] = useState("");
const [newPassword, setNewPassword] = useState("");
const [error, setError] = useState("");
const fetchUsers = async () => {
setLoading(true);
try {
const res = await fetch("/api/admin/users");
if (res.ok) {
setUsers(await res.json());
}
} catch {
// ignore
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchUsers();
}, []);
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
try {
const res = await fetch("/api/admin/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: newUsername, password: newPassword }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Грешка при креирање");
}
setShowCreate(false);
setNewUsername("");
setNewPassword("");
fetchUsers();
} catch (err) {
setError(err instanceof Error ? err.message : "Грешка при креирање");
}
};
const handleDelete = async (id: string) => {
if (!confirm("Дали сте сигурни дека сакате да го избришете овој администратор?")) return;
try {
const res = await fetch(`/api/admin/users/${id}`, { method: "DELETE" });
if (res.ok) {
fetchUsers();
}
} catch {
// ignore
}
};
const handleResetPassword = async (id: string) => {
const newPw = prompt("Внесете нова лозинка:");
if (!newPw || newPw.length < 6) {
alert("Лозинката мора да има најмалку 6 карактери");
return;
}
try {
const res = await fetch(`/api/admin/users/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: newPw }),
});
if (res.ok) {
alert("Лозинката е променета");
} else {
const data = await res.json();
alert(data.error || "Грешка");
}
} catch {
alert("Грешка");
}
};
return (
<div>
<div className="mb-8 flex items-center justify-between">
<h1 className="text-2xl font-semibold text-stone-900">Администратори</h1>
<button
onClick={() => setShowCreate(!showCreate)}
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light"
>
{showCreate ? "Откажи" : "Креирај администратор"}
</button>
</div>
{showCreate && (
<form onSubmit={handleCreate} className="mb-8 rounded-lg bg-white p-6 shadow-sm">
<div className="mb-4 grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium text-stone-700">Корисничко име</label>
<input
type="text"
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
className="mt-1 block w-full rounded-lg border border-stone-200 px-3 py-2.5 text-stone-900 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-stone-700">Лозинка</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="mt-1 block w-full rounded-lg border border-stone-200 px-3 py-2.5 text-stone-900 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
required
minLength={6}
/>
</div>
</div>
{error && <p className="mb-4 text-sm text-red-600">{error}</p>}
<button
type="submit"
className="rounded-lg bg-primary px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light"
>
Креирај
</button>
</form>
)}
<div className="rounded-lg bg-white shadow-sm">
{loading ? (
<p className="p-6 text-sm text-stone-500">Вчитување...</p>
) : users.length === 0 ? (
<p className="p-6 text-sm text-stone-500">Нема администратори</p>
) : (
<table className="w-full text-left text-sm">
<thead className="border-b border-stone-200">
<tr>
<th className="px-6 py-3 font-medium text-stone-500">Корисничко име</th>
<th className="px-6 py-3 font-medium text-stone-500">Улога</th>
<th className="px-6 py-3 font-medium text-stone-500">Креиран</th>
<th className="px-6 py-3 font-medium text-stone-500">Акции</th>
</tr>
</thead>
<tbody className="divide-y divide-stone-100">
{users.map((user) => (
<tr key={user.id} className="hover:bg-stone-50">
<td className="px-6 py-4 text-stone-900">{user.username}</td>
<td className="px-6 py-4 text-stone-600">{user.role === "SUPER_ADMIN" ? "SuperAdmin" : "Admin"}</td>
<td className="px-6 py-4 text-stone-600">{new Date(user.createdAt).toLocaleDateString("mk-MK")}</td>
<td className="space-x-2 px-6 py-4">
<button
onClick={() => handleResetPassword(user.id)}
className="text-sm text-primary hover:underline"
>
Промени лозинка
</button>
{user.role !== "SUPER_ADMIN" && (
<button
onClick={() => handleDelete(user.id)}
className="text-sm text-red-600 hover:underline"
>
Избриши
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}