diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index 0baac10..db3977f 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -8,6 +8,11 @@ import { OtpVerification } from './components/pages/OtpVerification'
import { AuthCallback } from './components/pages/AuthCallback'
import { AdminLayout } from './components/admin/AdminLayout'
import { AdminDashboard } from './components/admin/pages/AdminDashboard'
+import { AdminPlans } from './components/admin/pages/AdminPlans'
+import { AdminPaymentMethods } from './components/admin/pages/AdminPaymentMethods'
+import { AdminPayments } from './components/admin/pages/AdminPayments'
+import { AdminUsers } from './components/admin/pages/AdminUsers'
+import { AdminSettings } from './components/admin/pages/AdminSettings'
import { Loader2 } from 'lucide-react'
function ProtectedRoute({ children }: { children: React.ReactNode }) {
@@ -64,6 +69,11 @@ export default function App() {
{/* Admin Routes */}
}>
} />
+ } />
+ } />
+ } />
+ } />
+ } />
{/* Protected Routes */}
diff --git a/apps/web/src/components/admin/pages/AdminPaymentMethods.tsx b/apps/web/src/components/admin/pages/AdminPaymentMethods.tsx
new file mode 100644
index 0000000..c981a36
--- /dev/null
+++ b/apps/web/src/components/admin/pages/AdminPaymentMethods.tsx
@@ -0,0 +1,12 @@
+export function AdminPaymentMethods() {
+ return (
+
+
+ Payment Methods
+
+
+ Manage payment methods (Coming soon)
+
+
+ )
+}
diff --git a/apps/web/src/components/admin/pages/AdminPayments.tsx b/apps/web/src/components/admin/pages/AdminPayments.tsx
new file mode 100644
index 0000000..955ff05
--- /dev/null
+++ b/apps/web/src/components/admin/pages/AdminPayments.tsx
@@ -0,0 +1,12 @@
+export function AdminPayments() {
+ return (
+
+
+ Payment Verification
+
+
+ Verify pending payments (Coming soon)
+
+
+ )
+}
diff --git a/apps/web/src/components/admin/pages/AdminPlans.tsx b/apps/web/src/components/admin/pages/AdminPlans.tsx
new file mode 100644
index 0000000..1beed3d
--- /dev/null
+++ b/apps/web/src/components/admin/pages/AdminPlans.tsx
@@ -0,0 +1,248 @@
+import { useEffect, useState } from 'react'
+import axios from 'axios'
+import { Plus, Edit, Trash2, Eye, EyeOff, GripVertical } from 'lucide-react'
+
+const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'
+
+interface Plan {
+ id: string
+ name: string
+ slug: string
+ description: string
+ price: string
+ currency: string
+ durationType: string
+ durationDays: number | null
+ trialDays: number
+ features: any
+ badge: string | null
+ badgeColor: string | null
+ sortOrder: number
+ isActive: boolean
+ isVisible: boolean
+ isFeatured: boolean
+ _count: {
+ subscriptions: number
+ }
+}
+
+export function AdminPlans() {
+ const [plans, setPlans] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [showModal, setShowModal] = useState(false)
+ const [editingPlan, setEditingPlan] = useState(null)
+
+ useEffect(() => {
+ fetchPlans()
+ }, [])
+
+ const fetchPlans = async () => {
+ try {
+ const token = localStorage.getItem('token')
+ const response = await axios.get(`${API_URL}/api/admin/plans`, {
+ headers: { Authorization: `Bearer ${token}` },
+ })
+ setPlans(response.data)
+ } catch (error) {
+ console.error('Failed to fetch plans:', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleDelete = async (id: string) => {
+ if (!confirm('Are you sure you want to delete this plan?')) return
+
+ try {
+ const token = localStorage.getItem('token')
+ await axios.delete(`${API_URL}/api/admin/plans/${id}`, {
+ headers: { Authorization: `Bearer ${token}` },
+ })
+ fetchPlans()
+ } catch (error) {
+ console.error('Failed to delete plan:', error)
+ alert('Failed to delete plan')
+ }
+ }
+
+ const toggleVisibility = async (plan: Plan) => {
+ try {
+ const token = localStorage.getItem('token')
+ await axios.put(
+ `${API_URL}/api/admin/plans/${plan.id}`,
+ { isVisible: !plan.isVisible },
+ { headers: { Authorization: `Bearer ${token}` } }
+ )
+ fetchPlans()
+ } catch (error) {
+ console.error('Failed to update plan:', error)
+ }
+ }
+
+ const formatPrice = (price: string, currency: string) => {
+ const amount = parseInt(price)
+ if (amount === 0) return 'Free'
+ return new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: currency,
+ minimumFractionDigits: 0,
+ }).format(amount)
+ }
+
+ if (loading) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
+
+ Plans Management
+
+
+ Manage subscription plans
+
+
+
+
+
+ {/* Plans Grid */}
+
+ {plans.map((plan) => (
+
+ {/* Header */}
+
+
+
+
+
+ {plan.name}
+
+
+ {plan.badge && (
+
+ {plan.badge}
+
+ )}
+
+
+ {plan.description}
+
+
+
+ {/* Price */}
+
+
+ {formatPrice(plan.price, plan.currency)}
+
+
+ {plan.durationType === 'lifetime'
+ ? 'Lifetime access'
+ : plan.durationType === 'monthly'
+ ? 'per month'
+ : plan.durationType === 'yearly'
+ ? 'per year'
+ : plan.durationType}
+
+ {plan.trialDays > 0 && (
+
+ {plan.trialDays} days free trial
+
+ )}
+
+
+ {/* Stats */}
+
+
+
+ Subscriptions:
+
+
+ {plan._count.subscriptions}
+
+
+
+
Status:
+
+ {plan.isActive && (
+
+ Active
+
+ )}
+ {plan.isVisible ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Actions */}
+
+
+
+
+
+
+ ))}
+
+
+ {plans.length === 0 && (
+
+ )}
+
+ )
+}
diff --git a/apps/web/src/components/admin/pages/AdminSettings.tsx b/apps/web/src/components/admin/pages/AdminSettings.tsx
new file mode 100644
index 0000000..76e0db2
--- /dev/null
+++ b/apps/web/src/components/admin/pages/AdminSettings.tsx
@@ -0,0 +1,12 @@
+export function AdminSettings() {
+ return (
+
+
+ App Settings
+
+
+ Manage app configuration (Coming soon)
+
+
+ )
+}
diff --git a/apps/web/src/components/admin/pages/AdminUsers.tsx b/apps/web/src/components/admin/pages/AdminUsers.tsx
new file mode 100644
index 0000000..deeb220
--- /dev/null
+++ b/apps/web/src/components/admin/pages/AdminUsers.tsx
@@ -0,0 +1,234 @@
+import { useEffect, useState } from 'react'
+import axios from 'axios'
+import { Search, UserX, UserCheck, Crown } from 'lucide-react'
+
+const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'
+
+interface User {
+ id: string
+ email: string
+ name: string | null
+ role: string
+ emailVerified: boolean
+ createdAt: string
+ lastLoginAt: string | null
+ suspendedAt: string | null
+ _count: {
+ wallets: number
+ transactions: number
+ }
+}
+
+export function AdminUsers() {
+ const [users, setUsers] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [search, setSearch] = useState('')
+
+ useEffect(() => {
+ fetchUsers()
+ }, [search])
+
+ const fetchUsers = async () => {
+ try {
+ const token = localStorage.getItem('token')
+ const params = search ? `?search=${encodeURIComponent(search)}` : ''
+ const response = await axios.get(`${API_URL}/api/admin/users${params}`, {
+ headers: { Authorization: `Bearer ${token}` },
+ })
+ setUsers(response.data)
+ } catch (error) {
+ console.error('Failed to fetch users:', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleSuspend = async (userId: string, suspend: boolean) => {
+ const reason = suspend
+ ? prompt('Enter suspension reason:')
+ : null
+
+ if (suspend && !reason) return
+
+ try {
+ const token = localStorage.getItem('token')
+ const endpoint = suspend ? 'suspend' : 'unsuspend'
+ await axios.post(
+ `${API_URL}/api/admin/users/${userId}/${endpoint}`,
+ suspend ? { reason } : {},
+ { headers: { Authorization: `Bearer ${token}` } }
+ )
+ fetchUsers()
+ } catch (error) {
+ console.error('Failed to update user:', error)
+ alert('Failed to update user')
+ }
+ }
+
+ const handleGrantPro = async (userId: string) => {
+ const days = prompt('Enter duration in days (e.g., 30 for monthly):')
+ if (!days || isNaN(parseInt(days))) return
+
+ try {
+ const token = localStorage.getItem('token')
+ await axios.post(
+ `${API_URL}/api/admin/users/${userId}/grant-pro`,
+ {
+ planSlug: 'pro-monthly',
+ durationDays: parseInt(days),
+ },
+ { headers: { Authorization: `Bearer ${token}` } }
+ )
+ alert('Pro access granted successfully!')
+ fetchUsers()
+ } catch (error) {
+ console.error('Failed to grant pro access:', error)
+ alert('Failed to grant pro access')
+ }
+ }
+
+ if (loading) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
+ Users Management
+
+
+ Manage user accounts and permissions
+
+
+
+ {/* Search */}
+
+
+
+ setSearch(e.target.value)}
+ className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+ />
+
+
+
+ {/* Users Table */}
+
+
+
+
+
+ |
+ User
+ |
+
+ Role
+ |
+
+ Stats
+ |
+
+ Status
+ |
+
+ Actions
+ |
+
+
+
+ {users.map((user) => (
+
+
+
+
+ {user.name?.[0] || user.email[0].toUpperCase()}
+
+
+
+ {user.name || 'No name'}
+
+
+ {user.email}
+
+
+
+ |
+
+
+ {user.role}
+
+ |
+
+ {user._count.wallets} wallets
+ {user._count.transactions} transactions
+ |
+
+ {user.suspendedAt ? (
+
+ Suspended
+
+ ) : user.emailVerified ? (
+
+ Active
+
+ ) : (
+
+ Unverified
+
+ )}
+ |
+
+ {user.suspendedAt ? (
+
+ ) : (
+
+ )}
+
+ |
+
+ ))}
+
+
+
+
+
+ {users.length === 0 && (
+
+ )}
+
+ )
+}