@@ -147,7 +150,9 @@ export default function AdminLayout({
? 'Tata Letak Kursi Armada'
: pathname.startsWith('/admin/settings')
? 'Pengaturan Brand & Pembayaran'
- : 'Kelola Pemesanan Tiket'}
+ : pathname.startsWith('/admin/profile')
+ ? 'Pengaturan Profil Admin'
+ : 'Kelola Pemesanan Tiket'}
{new Date().toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
diff --git a/src/app/admin/profile/AdminProfile.module.css b/src/app/admin/profile/AdminProfile.module.css
new file mode 100644
index 0000000..4433aca
--- /dev/null
+++ b/src/app/admin/profile/AdminProfile.module.css
@@ -0,0 +1,91 @@
+.container {
+ animation: fadeIn 0.4s ease-out forwards;
+}
+
+.profileGrid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 30px;
+}
+
+@media (max-width: 900px) {
+ .profileGrid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.sectionTitle {
+ font-size: 1.15rem;
+ font-weight: 600;
+ color: var(--text-white);
+ margin-bottom: 24px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ border-bottom: 1px solid var(--border-light);
+ padding-bottom: 12px;
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.alert {
+ padding: 14px 16px;
+ border-radius: 8px;
+ font-size: 0.9rem;
+ margin-bottom: 25px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.alertSuccess {
+ background: rgba(16, 185, 129, 0.15);
+ color: var(--accent-emerald);
+ border: 1px solid rgba(16, 185, 129, 0.3);
+}
+
+.alertError {
+ background: rgba(244, 63, 94, 0.15);
+ color: var(--accent-rose);
+ border: 1px solid rgba(244, 63, 94, 0.3);
+}
+
+.loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 400px;
+ color: var(--text-secondary);
+}
+
+.spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid rgba(255, 255, 255, 0.1);
+ border-radius: 50%;
+ border-top-color: var(--primary);
+ animation: spin 1s ease-in-out infinite;
+ margin-bottom: 15px;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
diff --git a/src/app/admin/profile/page.tsx b/src/app/admin/profile/page.tsx
new file mode 100644
index 0000000..88e786d
--- /dev/null
+++ b/src/app/admin/profile/page.tsx
@@ -0,0 +1,253 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { FaUser, FaLock, FaCheckCircle, FaExclamationCircle } from 'react-icons/fa';
+import styles from './AdminProfile.module.css';
+
+interface User {
+ id: number;
+ name: string;
+ email: string;
+ phone: string;
+ role: string;
+ createdAt: string;
+}
+
+export default function AdminProfile() {
+ const router = useRouter();
+ const [user, setUser] = useState
(null);
+ const [loading, setLoading] = useState(true);
+
+ // Profile form state
+ const [name, setName] = useState('');
+ const [phone, setPhone] = useState('');
+
+ // Password form state
+ const [currentPassword, setCurrentPassword] = useState('');
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+
+ const [savingProfile, setSavingProfile] = useState(false);
+ const [savingPassword, setSavingPassword] = useState(false);
+ const [successMsg, setSuccessMsg] = useState('');
+ const [errorMsg, setErrorMsg] = useState('');
+
+ useEffect(() => {
+ async function fetchProfile() {
+ try {
+ const res = await fetch('/api/user/profile');
+ if (!res.ok) {
+ router.push('/login?redirect=/admin/profile');
+ return;
+ }
+ const data = await res.json();
+ if (data.user.role !== 'ADMIN') {
+ router.push('/');
+ return;
+ }
+ setUser(data.user);
+ setName(data.user.name);
+ setPhone(data.user.phone);
+ } catch (err) {
+ console.error('Fetch profile error:', err);
+ setErrorMsg('Gagal memuat profil administrator.');
+ } finally {
+ setLoading(false);
+ }
+ }
+ fetchProfile();
+ }, [router]);
+
+ const handleUpdateProfile = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setSavingProfile(true);
+ setSuccessMsg('');
+ setErrorMsg('');
+
+ try {
+ const res = await fetch('/api/user/profile', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, phone }),
+ });
+
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data.error || 'Gagal memperbarui profil.');
+ }
+
+ setUser(data.user);
+ setSuccessMsg('Profil admin berhasil diperbarui!');
+ setTimeout(() => setSuccessMsg(''), 3000);
+ } catch (err: any) {
+ setErrorMsg(err.message || 'Terjadi kesalahan.');
+ } finally {
+ setSavingProfile(false);
+ }
+ };
+
+ const handleChangePassword = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setSavingPassword(true);
+ setSuccessMsg('');
+ setErrorMsg('');
+
+ if (newPassword !== confirmPassword) {
+ setErrorMsg('Konfirmasi password baru tidak cocok.');
+ setSavingPassword(false);
+ return;
+ }
+
+ try {
+ const res = await fetch('/api/user/profile', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ currentPassword, newPassword }),
+ });
+
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data.error || 'Gagal mengubah password.');
+ }
+
+ setSuccessMsg('Password admin berhasil diubah!');
+ setCurrentPassword('');
+ setNewPassword('');
+ setConfirmPassword('');
+ setTimeout(() => setSuccessMsg(''), 3000);
+ } catch (err: any) {
+ setErrorMsg(err.message || 'Terjadi kesalahan.');
+ } finally {
+ setSavingPassword(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
Memuat profil admin...
+
+ );
+ }
+
+ if (!user) return null;
+
+ return (
+
+ {successMsg && (
+
+ {successMsg}
+
+ )}
+
+ {errorMsg && (
+
+ {errorMsg}
+
+ )}
+
+
+ {/* Profile Info Card */}
+
+
+ Informasi Akun Admin
+
+
+
+
+ {/* Change Password Card */}
+
+
+ Ubah Password Keamanan
+
+
+
+
+
+ );
+}
diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts
new file mode 100644
index 0000000..2d9d38e
--- /dev/null
+++ b/src/app/api/user/profile/route.ts
@@ -0,0 +1,93 @@
+import { NextResponse } from 'next/server';
+import { prisma } from '@/lib/db';
+import { getSessionUser } from '@/lib/auth';
+import bcrypt from 'bcryptjs';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ try {
+ const session = await getSessionUser();
+ if (!session) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const user = await prisma.user.findUnique({
+ where: { id: session.userId },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ phone: true,
+ role: true,
+ createdAt: true,
+ },
+ });
+
+ if (!user) {
+ return NextResponse.json({ error: 'User tidak ditemukan.' }, { status: 404 });
+ }
+
+ return NextResponse.json({ user });
+ } catch (error) {
+ console.error('Fetch profile error:', error);
+ return NextResponse.json({ error: 'Gagal mengambil data profil.' }, { status: 500 });
+ }
+}
+
+export async function PUT(request: Request) {
+ try {
+ const session = await getSessionUser();
+ if (!session) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { name, phone, currentPassword, newPassword } = await request.json();
+
+ const user = await prisma.user.findUnique({
+ where: { id: session.userId },
+ });
+
+ if (!user) {
+ return NextResponse.json({ error: 'User tidak ditemukan.' }, { status: 404 });
+ }
+
+ const updateData: any = {};
+
+ if (name) updateData.name = name;
+ if (phone) updateData.phone = phone;
+
+ // Handle password change if requested
+ if (currentPassword && newPassword) {
+ const isMatch = await bcrypt.compare(currentPassword, user.password);
+ if (!isMatch) {
+ return NextResponse.json({ error: 'Password saat ini salah.' }, { status: 400 });
+ }
+
+ if (newPassword.length < 6) {
+ return NextResponse.json({ error: 'Password baru minimal harus 6 karakter.' }, { status: 400 });
+ }
+
+ updateData.password = await bcrypt.hash(newPassword, 10);
+ } else if (currentPassword || newPassword) {
+ return NextResponse.json({ error: 'Untuk mengubah password, isi kedua field password saat ini dan password baru.' }, { status: 400 });
+ }
+
+ const updatedUser = await prisma.user.update({
+ where: { id: session.userId },
+ data: updateData,
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ phone: true,
+ role: true,
+ },
+ });
+
+ return NextResponse.json({ success: true, user: updatedUser });
+ } catch (error) {
+ console.error('Update profile error:', error);
+ return NextResponse.json({ error: 'Gagal memperbarui profil.' }, { status: 500 });
+ }
+}
diff --git a/src/app/dashboard/profile/page.tsx b/src/app/dashboard/profile/page.tsx
new file mode 100644
index 0000000..abd9519
--- /dev/null
+++ b/src/app/dashboard/profile/page.tsx
@@ -0,0 +1,268 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import Link from 'next/link';
+import { FaUser, FaLock, FaCheckCircle, FaExclamationCircle } from 'react-icons/fa';
+import styles from './profile.module.css';
+
+interface User {
+ id: number;
+ name: string;
+ email: string;
+ phone: string;
+ role: string;
+ createdAt: string;
+}
+
+export default function UserProfile() {
+ const router = useRouter();
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ // Profile update form state
+ const [name, setName] = useState('');
+ const [phone, setPhone] = useState('');
+
+ // Password change form state
+ const [currentPassword, setCurrentPassword] = useState('');
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+
+ const [savingProfile, setSavingProfile] = useState(false);
+ const [savingPassword, setSavingPassword] = useState(false);
+ const [successMsg, setSuccessMsg] = useState('');
+ const [errorMsg, setErrorMsg] = useState('');
+
+ useEffect(() => {
+ async function fetchProfile() {
+ try {
+ const res = await fetch('/api/user/profile');
+ if (!res.ok) {
+ router.push('/login?redirect=/dashboard/profile');
+ return;
+ }
+ const data = await res.json();
+ setUser(data.user);
+ setName(data.user.name);
+ setPhone(data.user.phone);
+ } catch (err) {
+ console.error('Fetch profile error:', err);
+ setErrorMsg('Gagal memuat profil pengguna.');
+ } finally {
+ setLoading(false);
+ }
+ }
+ fetchProfile();
+ }, [router]);
+
+ const handleUpdateProfile = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setSavingProfile(true);
+ setSuccessMsg('');
+ setErrorMsg('');
+
+ try {
+ const res = await fetch('/api/user/profile', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, phone }),
+ });
+
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data.error || 'Gagal memperbarui profil.');
+ }
+
+ setUser(data.user);
+ setSuccessMsg('Profil berhasil diperbarui!');
+ setTimeout(() => setSuccessMsg(''), 3000);
+ } catch (err: any) {
+ setErrorMsg(err.message || 'Terjadi kesalahan.');
+ } finally {
+ setSavingProfile(false);
+ }
+ };
+
+ const handleChangePassword = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setSavingPassword(true);
+ setSuccessMsg('');
+ setErrorMsg('');
+
+ if (newPassword !== confirmPassword) {
+ setErrorMsg('Konfirmasi password baru tidak cocok.');
+ setSavingPassword(false);
+ return;
+ }
+
+ try {
+ const res = await fetch('/api/user/profile', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ currentPassword, newPassword }),
+ });
+
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data.error || 'Gagal mengubah password.');
+ }
+
+ setSuccessMsg('Password berhasil diubah!');
+ setCurrentPassword('');
+ setNewPassword('');
+ setConfirmPassword('');
+ setTimeout(() => setSuccessMsg(''), 3000);
+ } catch (err: any) {
+ setErrorMsg(err.message || 'Terjadi kesalahan.');
+ } finally {
+ setSavingPassword(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
Memuat profil Anda...
+
+ );
+ }
+
+ if (!user) return null;
+
+ return (
+
+
+
Pengaturan Profil
+
Kelola informasi pribadi dan keamanan akun Anda.
+
+
+ {successMsg && (
+
+ {successMsg}
+
+ )}
+
+ {errorMsg && (
+
+ {errorMsg}
+
+ )}
+
+ {/* Profile Info Form */}
+
+
+ Informasi Profil
+
+
+
+
+ {/* Password Change Form */}
+
+
+ Ubah Password Keamanan
+
+
+
+
+ );
+}
diff --git a/src/app/dashboard/profile/profile.module.css b/src/app/dashboard/profile/profile.module.css
new file mode 100644
index 0000000..651d445
--- /dev/null
+++ b/src/app/dashboard/profile/profile.module.css
@@ -0,0 +1,160 @@
+.container {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 40px 20px;
+ animation: fadeIn 0.4s ease-out forwards;
+}
+
+.header {
+ margin-bottom: 30px;
+}
+
+.title {
+ font-size: 2rem;
+ font-weight: 700;
+ color: var(--text-white);
+ margin-bottom: 8px;
+}
+
+.subtitle {
+ color: var(--text-secondary);
+ font-size: 0.95rem;
+}
+
+.profileCard {
+ background: var(--bg-card);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border: 1px solid var(--border-light);
+ border-radius: 16px;
+ padding: 30px;
+ margin-bottom: 30px;
+ box-shadow: var(--shadow-lg);
+}
+
+.sectionTitle {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--text-white);
+ margin-bottom: 20px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ border-bottom: 1px solid var(--border-light);
+ padding-bottom: 10px;
+}
+
+.infoGrid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+@media (max-width: 600px) {
+ .infoGrid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.infoItem {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.infoLabel {
+ font-size: 0.8rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--text-muted);
+}
+
+.infoValue {
+ font-size: 1rem;
+ font-weight: 500;
+ color: var(--text-primary);
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.formGrid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+}
+
+@media (max-width: 600px) {
+ .formGrid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 15px;
+ margin-top: 10px;
+}
+
+.alert {
+ padding: 14px 16px;
+ border-radius: 8px;
+ font-size: 0.9rem;
+ margin-bottom: 20px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.alertSuccess {
+ background: rgba(16, 185, 129, 0.15);
+ color: var(--accent-emerald);
+ border: 1px solid rgba(16, 185, 129, 0.3);
+}
+
+.alertError {
+ background: rgba(244, 63, 94, 0.15);
+ color: var(--accent-rose);
+ border: 1px solid rgba(244, 63, 94, 0.3);
+}
+
+.loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 400px;
+ color: var(--text-secondary);
+}
+
+.spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid rgba(255, 255, 255, 0.1);
+ border-radius: 50%;
+ border-top-color: var(--primary);
+ animation: spin 1s ease-in-out infinite;
+ margin-bottom: 15px;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index c06f5dc..2150504 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -94,6 +94,11 @@ export default function Header({ settings }: { settings?: any }) {
Dashboard Saya
)}
+ {user && (
+
+ Profil Saya
+
+ )}
{user?.role === 'ADMIN' && (
Admin Panel
@@ -104,7 +109,7 @@ export default function Header({ settings }: { settings?: any }) {
{user ? (
-
+
Halo, {user.name}