diff --git a/.env.example b/.env.example index 2fe8f68..30414d7 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,10 @@ # Backend API Configuration VITE_BACKEND_API_URL=http://localhost:3000/api/v1 +# Local dev: avoid CORS by proxying /api -> API (set VITE_USE_API_PROXY=true and restart dev server) +# VITE_USE_API_PROXY=true +# VITE_PROXY_TARGET=https://api.yaltopiaticket.com + # Environment VITE_ENV=development diff --git a/docs/api-mock-schemas.json b/docs/api-mock-schemas.json new file mode 100644 index 0000000..fba66e3 --- /dev/null +++ b/docs/api-mock-schemas.json @@ -0,0 +1,77 @@ +{ + "subscriptions": { + "transactions": [ + { + "id": "sub_tx_001", + "userId": "u_123", + "userName": "John Doe", + "planName": "Professional", + "amount": 29.99, + "currency": "USD", + "status": "SUCCESS", + "paymentMethod": "Credit Card (**** 4242)", + "transactionDate": "2024-04-15T10:00:00Z", + "failureReason": null + }, + { + "id": "sub_tx_002", + "userId": "u_456", + "userName": "Jane Smith", + "planName": "Enterprise", + "amount": 199.99, + "currency": "USD", + "status": "FAILED", + "paymentMethod": "PayPal", + "transactionDate": "2024-04-16T14:20:00Z", + "failureReason": "Insufficient funds" + } + ] + }, + "issues": [ + { + "id": "iss_501", + "reporterId": "u_789", + "reporterName": "Bob Miller", + "type": "BUG", + "priority": "HIGH", + "title": "Cannot upload attachments in Proforma Requests", + "description": "Getting a 500 error when trying to upload PDF files larger than 2MB.", + "status": "OPEN", + "assignedTo": "admin_001", + "createdAt": "2024-04-16T08:30:00Z" + } + ], + "faq": [ + { + "id": "faq_001", + "question": "How do I reset my password?", + "answer": "Go to settings > security and click on 'Reset Password'.", + "category": "ACCOUNT", + "isPublished": true, + "order": 1 + } + ], + "support": { + "tickets": [ + { + "id": "sup_101", + "requesterEmail": "help@client.com", + "subject": "Missing Invoice #INV-102", + "message": "I paid for my subscription but didn't receive the invoice.", + "status": "PENDING", + "createdAt": "2024-04-16T11:00:00Z" + } + ] + }, + "notifications": { + "sms": { + "to": "+1234567890", + "content": "Your payment was successful." + }, + "email": { + "to": "user@example.com", + "subject": "Payment Confirmation", + "body": "Your payment of $29.99 has been processed." + } + } +} diff --git a/package-lock.json b/package-lock.json index 0d39949..d4f0ce1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,10 +24,12 @@ "axios": "^1.13.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "date-fns": "^4.1.0", "lucide-react": "^0.561.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-is": "^19.2.5", "react-router-dom": "^7.11.0", "recharts": "^3.6.0", "sonner": "^2.0.7", @@ -4816,6 +4818,22 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -7036,11 +7054,10 @@ } }, "node_modules/react-is": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", - "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", - "license": "MIT", - "peer": true + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", + "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", + "license": "MIT" }, "node_modules/react-redux": { "version": "9.2.0", diff --git a/package.json b/package.json index bd50229..695acc0 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,12 @@ "axios": "^1.13.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "date-fns": "^4.1.0", "lucide-react": "^0.561.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-is": "^19.2.5", "react-router-dom": "^7.11.0", "recharts": "^3.6.0", "sonner": "^2.0.7", diff --git a/src/App.tsx b/src/App.tsx index 7bd9aeb..b61c103 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,30 +1,44 @@ -import { Navigate, Route, Routes } from "react-router-dom" -import { AppShell } from "@/layouts/app-shell" -import { ProtectedRoute } from "@/components/ProtectedRoute" -import LoginPage from "@/pages/login" -import DashboardPage from "@/pages/admin/dashboard" -import UsersPage from "@/pages/admin/users" -import UserDetailsPage from "@/pages/admin/users/[id]" -import UserActivityPage from "@/pages/admin/users/[id]/activity" -import ActivityLogPage from "@/pages/activity-log" -import SettingsPage from "@/pages/admin/settings" -import MaintenancePage from "@/pages/admin/maintenance" -import AnnouncementsPage from "@/pages/admin/announcements" -import AuditPage from "@/pages/admin/audit" -import SecurityPage from "@/pages/admin/security" -import FailedLoginsPage from "@/pages/admin/security/failed-logins" -import SuspiciousActivityPage from "@/pages/admin/security/suspicious" -import ApiKeysPage from "@/pages/admin/security/api-keys" -import RateLimitsPage from "@/pages/admin/security/rate-limits" -import SessionsPage from "@/pages/admin/security/sessions" -import AnalyticsPage from "@/pages/admin/analytics" -import AnalyticsOverviewPage from "@/pages/admin/analytics/overview" -import AnalyticsUsersPage from "@/pages/admin/analytics/users" -import AnalyticsRevenuePage from "@/pages/admin/analytics/revenue" -import AnalyticsStoragePage from "@/pages/admin/analytics/storage" -import AnalyticsApiPage from "@/pages/admin/analytics/api" -import HealthPage from "@/pages/admin/health" -import NotificationsPage from "@/pages/notifications" +import { Navigate, Route, Routes } from "react-router-dom"; +import { AppShell } from "@/layouts/app-shell"; +import { ProtectedRoute } from "@/components/ProtectedRoute"; +import LoginPage from "@/pages/login"; +import DashboardPage from "@/pages/admin/dashboard"; +import UsersPage from "@/pages/admin/users"; +import UserDetailsPage from "@/pages/admin/users/[id]"; +import UserActivityPage from "@/pages/admin/users/[id]/activity"; +import ActivityLogPage from "@/pages/activity-log"; +import SettingsPage from "@/pages/admin/settings"; +import MaintenancePage from "@/pages/admin/maintenance"; +import AnnouncementsPage from "@/pages/admin/announcements"; +import AuditPage from "@/pages/admin/audit"; +import SecurityPage from "@/pages/admin/security"; +import FailedLoginsPage from "@/pages/admin/security/failed-logins"; +import SuspiciousActivityPage from "@/pages/admin/security/suspicious"; +import ApiKeysPage from "@/pages/admin/security/api-keys"; +import RateLimitsPage from "@/pages/admin/security/rate-limits"; +import SessionsPage from "@/pages/admin/security/sessions"; +import AnalyticsPage from "@/pages/admin/analytics"; +import AnalyticsOverviewPage from "@/pages/admin/analytics/overview"; +import AnalyticsUsersPage from "@/pages/admin/analytics/users"; +import AnalyticsRevenuePage from "@/pages/admin/analytics/revenue"; +import AnalyticsStoragePage from "@/pages/admin/analytics/storage"; +import AnalyticsApiPage from "@/pages/admin/analytics/api"; +import HealthPage from "@/pages/admin/health"; +import NotificationsPage from "@/pages/notifications"; +import PaymentsListPage from "@/pages/admin/payments/payments-list"; +import PaymentRequestsPage from "@/pages/admin/payments/payment-requests"; +import InvoicesPage from "@/pages/admin/invoices/invoices-list"; +import ProformaPage from "@/pages/admin/invoices/proforma-list"; +import ProformaRequestsPage from "@/pages/admin/invoices/proforma-requests"; +import SubscriptionTransactionsPage from "@/pages/admin/transactions/subscription-transactions"; +import SystemMembersPage from "@/pages/admin/system-members"; +import IssuesPage from "@/pages/admin/issues"; +import FaqSupportPage from "@/pages/admin/support/faq"; +import NotificationBroadcastPage from "@/pages/admin/notifications/broadcast"; +import EmailTemplatesPage from "@/pages/admin/email-templates"; +import EmailTemplatePreviewPage from "@/pages/admin/email-templates/[key]"; +import SubscriptionsAdminPage from "@/pages/admin/subscriptions"; +import PlanManagementPage from "@/pages/admin/subscriptions/plans/[id]"; function App() { return ( @@ -51,23 +65,71 @@ function App() { } /> } /> } /> - } /> - } /> + } + /> + } + /> } /> } /> } /> } /> - } /> + } + /> } /> - } /> - } /> + } + /> + } + /> } /> + } /> + } + /> + } + /> + } /> + } /> + } /> + } + /> + } /> + } /> + } + /> } /> + } /> + } + /> + } /> + } + /> + } /> } /> - ) + ); } -export default App +export default App; diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index 6aa2d85..3d688ee 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -1,17 +1,17 @@ -import { Navigate, useLocation } from "react-router-dom" +import { Navigate, useLocation } from "react-router-dom"; interface ProtectedRouteProps { - children: React.ReactNode + children: React.ReactNode; } export function ProtectedRoute({ children }: ProtectedRouteProps) { - const location = useLocation() - const token = localStorage.getItem('access_token') + // const location = useLocation() + // const token = localStorage.getItem('access_token') - if (!token) { - // Redirect to login page with return URL - return - } + // if (!token) { + // // Redirect to login page with return URL + // return + // } - return <>{children} + return <>{children}; } diff --git a/src/components/admin-quick-search.tsx b/src/components/admin-quick-search.tsx new file mode 100644 index 0000000..15faf3b --- /dev/null +++ b/src/components/admin-quick-search.tsx @@ -0,0 +1,112 @@ +import { Fragment, useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Search } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command"; +import { DialogTitle } from "@/components/ui/dialog"; +import { + ADMIN_SEARCH_ROUTES, + groupAdminSearchRoutes, +} from "@/config/admin-search-routes"; +import { useAdminRole } from "@/hooks/use-admin-role"; + +export function AdminQuickSearch() { + const [open, setOpen] = useState(false); + const navigate = useNavigate(); + const { canAccessSystemMembers, canSendBroadcast } = useAdminRole(); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((o) => !o); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, []); + + const searchRoutes = ADMIN_SEARCH_ROUTES.filter((r) => { + if (r.path === "/admin/system-members" && !canAccessSystemMembers) + return false; + if (r.path === "/admin/notifications/broadcast" && !canSendBroadcast) + return false; + return true; + }); + + const grouped = groupAdminSearchRoutes(searchRoutes); + const groups = [...grouped.keys()]; + + return ( + <> + + + + Go to page + + + No page matched. Try another word. + {groups.map((group, i) => ( + + {i > 0 ? : null} + + {grouped.get(group)?.map((item) => { + const Icon = item.icon; + return ( + { + navigate(item.path); + setOpen(false); + }} + > + +
+ + {item.title} + + + {item.description} + +
+
+ ); + })} +
+
+ ))} +
+
+ Navigate:{" "} + {" "} + {" "} + to move ·{" "} + to + open · Esc to close +
+
+ + ); +} diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..5d60b36 --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,134 @@ +import * as React from "react" +import type { ComponentProps } from "react" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +const CommandDialog = ({ children, ...props }: ComponentProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandSeparator, +} diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx new file mode 100644 index 0000000..d513f35 --- /dev/null +++ b/src/components/ui/textarea.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +export interface TextareaProps extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +