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 ( +
+
Loading...
+
+ ) + } + + 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 && ( +
+

No plans found

+
+ )} +
+ ) +} 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 ( +
+
Loading...
+
+ ) + } + + 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 */} +
+
+ + + + + + + + + + + + {users.map((user) => ( + + + + + + + + ))} + +
+ User + + Role + + Stats + + Status + + Actions +
+
+
+ {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 && ( +
+

No users found

+
+ )} +
+ ) +}