([]);
+ const [loading, setLoading] = useState(true);
+ const [generating, setGenerating] = useState(false);
+ const [newCode, setNewCode] = useState("");
+ const [error, setError] = useState("");
+
+ const fetchCodes = async () => {
+ setLoading(true);
+ try {
+ const res = await fetch("/api/admin/codes");
+ if (res.ok) {
+ setCodes(await res.json());
+ }
+ } catch {
+ // ignore
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchCodes();
+ }, []);
+
+ const handleGenerate = async () => {
+ setGenerating(true);
+ setError("");
+ setNewCode("");
+ try {
+ const res = await fetch("/api/admin/codes", { method: "POST" });
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data.error || "Грешка при генерирање");
+ }
+ setNewCode(data.code);
+ fetchCodes();
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Грешка при генерирање");
+ } finally {
+ setGenerating(false);
+ }
+ };
+
+ const handleDelete = async (id: string) => {
+ if (!confirm("Дали сте сигурни?")) return;
+ try {
+ await fetch(`/api/admin/codes/${id}`, { method: "DELETE" });
+ fetchCodes();
+ } catch {
+ // ignore
+ }
+ };
+
+ return (
+
+
+ Кодови
+
+
+
+ {newCode && (
+
+ Нов код:
+ {newCode}
+ Копирајте го кодот. Ќе биде прикажан само еднаш.
+
+ )}
+
+ {error && (
+ {error}
+ )}
+
+
+ {loading ? (
+ Вчитување...
+ ) : codes.length === 0 ? (
+ Нема генерирани кодови
+ ) : (
+
+
+
+ Код
+ Креиран од
+ Креиран
+ Статус
+ Акции
+
+
+
+ {codes.map((item) => (
+
+ {item.code}
+ {item.createdBy.username}
+ {new Date(item.createdAt).toLocaleDateString("mk-MK")}
+
+ {item.usedByUserId ? (
+
+ Искористен
+
+ ) : (
+
+ Неискористен
+
+ )}
+
+
+ {!item.usedByUserId && (
+
+ )}
+
+
+ ))}
+
+
+ )}
+
+
+ );
+}
diff --git a/src/app/admin/dashboard/page.tsx b/src/app/admin/dashboard/page.tsx
new file mode 100644
index 0000000..1044b60
--- /dev/null
+++ b/src/app/admin/dashboard/page.tsx
@@ -0,0 +1,29 @@
+import { prisma } from "@/lib/prisma";
+
+export default async function AdminDashboardPage() {
+ const [adminCount, codeCount, usedCodeCount] = await Promise.all([
+ prisma.adminUser.count(),
+ prisma.code.count(),
+ prisma.code.count({ where: { usedByUserId: { not: null } } }),
+ ]);
+
+ return (
+
+ Контролна табла
+
+
+ Администратори
+ {adminCount}
+
+
+ Генерирани кодови
+ {codeCount}
+
+
+ Искористени кодови
+ {usedCodeCount}
+
+
+
+ );
+}
diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx
new file mode 100644
index 0000000..21748ee
--- /dev/null
+++ b/src/app/admin/layout.tsx
@@ -0,0 +1,17 @@
+import { getAdminSession } from "@/lib/admin-session";
+import { redirect } from "next/navigation";
+import AdminSidebar from "./AdminSidebar";
+
+export default async function AdminLayout({ children }: { children: React.ReactNode }) {
+ const session = await getAdminSession();
+ if (!session) {
+ redirect("/admin/login");
+ }
+
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx
new file mode 100644
index 0000000..a3d5c5b
--- /dev/null
+++ b/src/app/admin/users/page.tsx
@@ -0,0 +1,187 @@
+"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([]);
+ 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 (
+
+
+ Администратори
+
+
+
+ {showCreate && (
+
+ )}
+
+
+ {loading ? (
+ Вчитување...
+ ) : users.length === 0 ? (
+ Нема администратори
+ ) : (
+
+
+
+ Корисничко име
+ Улога
+ Креиран
+ Акции
+
+
+
+ {users.map((user) => (
+
+ {user.username}
+ {user.role === "SUPER_ADMIN" ? "SuperAdmin" : "Admin"}
+ {new Date(user.createdAt).toLocaleDateString("mk-MK")}
+
+
+ {user.role !== "SUPER_ADMIN" && (
+
+ )}
+
+
+ ))}
+
+
+ )}
+
+
+ );
+}
diff --git a/src/app/api/admin/codes/[id]/route.ts b/src/app/api/admin/codes/[id]/route.ts
new file mode 100644
index 0000000..d7c8ece
--- /dev/null
+++ b/src/app/api/admin/codes/[id]/route.ts
@@ -0,0 +1,29 @@
+import { NextRequest, NextResponse } from "next/server";
+import { prisma } from "@/lib/prisma";
+import { getAdminSession } from "@/lib/admin-session";
+
+export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+ const session = await getAdminSession();
+ if (!session) {
+ return NextResponse.json({ error: "Неавторизирано" }, { status: 401 });
+ }
+
+ const { id } = await params;
+ const code = await prisma.code.findUnique({ where: { id } });
+ if (!code) {
+ return NextResponse.json({ error: "Кодот не е пронајден" }, { status: 404 });
+ }
+ if (code.usedByUserId) {
+ return NextResponse.json({ error: "Не можете да избришете искористен код" }, { status: 400 });
+ }
+
+ if (session.role !== "SUPER_ADMIN") {
+ const admin = await prisma.adminUser.findUnique({ where: { username: session.username } });
+ if (!admin || code.createdById !== admin.id) {
+ return NextResponse.json({ error: "Немате дозвола" }, { status: 403 });
+ }
+ }
+
+ await prisma.code.delete({ where: { id } });
+ return NextResponse.json({ success: true });
+}
diff --git a/src/app/api/admin/codes/route.ts b/src/app/api/admin/codes/route.ts
new file mode 100644
index 0000000..6465a77
--- /dev/null
+++ b/src/app/api/admin/codes/route.ts
@@ -0,0 +1,44 @@
+import { NextRequest, NextResponse } from "next/server";
+import { randomBytes } from "crypto";
+import { prisma } from "@/lib/prisma";
+import { getAdminSession } from "@/lib/admin-session";
+
+export async function GET() {
+ const session = await getAdminSession();
+ if (!session) {
+ return NextResponse.json({ error: "Неавторизирано" }, { status: 401 });
+ }
+
+ const where = session.role === "SUPER_ADMIN" ? {} : { createdBy: { username: session.username } };
+
+ const codes = await prisma.code.findMany({
+ where,
+ orderBy: { createdAt: "desc" },
+ include: { createdBy: { select: { username: true } } },
+ });
+
+ return NextResponse.json(codes);
+}
+
+export async function POST() {
+ const session = await getAdminSession();
+ if (!session) {
+ return NextResponse.json({ error: "Неавторизирано" }, { status: 401 });
+ }
+
+ const admin = await prisma.adminUser.findUnique({ where: { username: session.username } });
+ if (!admin) {
+ return NextResponse.json({ error: "Администраторот не е пронајден" }, { status: 404 });
+ }
+
+ const code = randomBytes(6).toString("hex").toUpperCase();
+
+ const created = await prisma.code.create({
+ data: {
+ code,
+ createdById: admin.id,
+ },
+ });
+
+ return NextResponse.json(created, { status: 201 });
+}
diff --git a/src/app/api/admin/users/[id]/route.ts b/src/app/api/admin/users/[id]/route.ts
new file mode 100644
index 0000000..751970c
--- /dev/null
+++ b/src/app/api/admin/users/[id]/route.ts
@@ -0,0 +1,54 @@
+import { NextRequest, NextResponse } from "next/server";
+import { hash } from "bcryptjs";
+import { prisma } from "@/lib/prisma";
+import { getAdminSession } from "@/lib/admin-session";
+
+export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+ const session = await getAdminSession();
+ if (!session || session.role !== "SUPER_ADMIN") {
+ return NextResponse.json({ error: "Немате дозвола" }, { status: 403 });
+ }
+
+ const { id } = await params;
+ const user = await prisma.adminUser.findUnique({ where: { id } });
+ if (!user) {
+ return NextResponse.json({ error: "Администраторот не е пронајден" }, { status: 404 });
+ }
+ if (user.role === "SUPER_ADMIN") {
+ return NextResponse.json({ error: "Не можете да избришете SuperAdmin" }, { status: 400 });
+ }
+
+ await prisma.adminUser.delete({ where: { id } });
+ return NextResponse.json({ success: true });
+}
+
+export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+ const session = await getAdminSession();
+ if (!session || session.role !== "SUPER_ADMIN") {
+ return NextResponse.json({ error: "Немате дозвола" }, { status: 403 });
+ }
+
+ try {
+ const { id } = await params;
+ const { password } = await req.json();
+ if (!password || password.length < 6) {
+ return NextResponse.json({ error: "Лозинката мора да има најмалку 6 карактери" }, { status: 400 });
+ }
+
+ const user = await prisma.adminUser.findUnique({ where: { id } });
+ if (!user) {
+ return NextResponse.json({ error: "Администраторот не е пронајден" }, { status: 404 });
+ }
+
+ const passwordHash = await hash(password, 12);
+ await prisma.adminUser.update({
+ where: { id },
+ data: { passwordHash },
+ });
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error("Update admin error:", error);
+ return NextResponse.json({ error: "Внатрешна грешка на серверот" }, { status: 500 });
+ }
+}
diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts
new file mode 100644
index 0000000..49f4194
--- /dev/null
+++ b/src/app/api/admin/users/route.ts
@@ -0,0 +1,49 @@
+import { NextRequest, NextResponse } from "next/server";
+import { hash } from "bcryptjs";
+import { prisma } from "@/lib/prisma";
+import { getAdminSession } from "@/lib/admin-session";
+
+export async function GET() {
+ const session = await getAdminSession();
+ if (!session || session.role !== "SUPER_ADMIN") {
+ return NextResponse.json({ error: "Немате дозвола" }, { status: 403 });
+ }
+
+ const users = await prisma.adminUser.findMany({
+ orderBy: { createdAt: "desc" },
+ });
+
+ return NextResponse.json(users);
+}
+
+export async function POST(req: NextRequest) {
+ const session = await getAdminSession();
+ if (!session || session.role !== "SUPER_ADMIN") {
+ return NextResponse.json({ error: "Немате дозвола" }, { status: 403 });
+ }
+
+ try {
+ const { username, password } = await req.json();
+ if (!username || !password) {
+ return NextResponse.json({ error: "Корисничко име и лозинка се задолжителни" }, { status: 400 });
+ }
+ if (password.length < 6) {
+ return NextResponse.json({ error: "Лозинката мора да има најмалку 6 карактери" }, { status: 400 });
+ }
+
+ const existing = await prisma.adminUser.findUnique({ where: { username } });
+ if (existing) {
+ return NextResponse.json({ error: "Корисничкото име веќе постои" }, { status: 409 });
+ }
+
+ const passwordHash = await hash(password, 12);
+ const user = await prisma.adminUser.create({
+ data: { username, passwordHash, role: "ADMIN" },
+ });
+
+ return NextResponse.json(user, { status: 201 });
+ } catch (error) {
+ console.error("Create admin error:", error);
+ return NextResponse.json({ error: "Внатрешна грешка на серверот" }, { status: 500 });
+ }
+}