- 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
188 lines
6.7 KiB
TypeScript
188 lines
6.7 KiB
TypeScript
"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>
|
||
);
|
||
}
|