feat: Add Multi-Gateway Configuration Module with Payment Settings Tab
This commit is contained in:
270
MASTER_API_INVENTORY.md
Normal file
270
MASTER_API_INVENTORY.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# Eventify Master API Inventory
|
||||||
|
|
||||||
|
> **Version:** 1.0 | **Last Updated:** 2026-02-09 | **Status:** Definitive Reference
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Auth & Identity
|
||||||
|
|
||||||
|
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| AUTH-01 | `/api/v1/auth/login/otp/request` | POST | Inbound | App/Web -> Server | Request OTP for phone login | High |
|
||||||
|
| AUTH-02 | `/api/v1/auth/login/otp/verify` | POST | Inbound | App/Web -> Server | Verify OTP and issue tokens | High |
|
||||||
|
| AUTH-03 | `/api/v1/auth/login/password` | POST | Inbound | App/Web -> Server | Email/password login | High |
|
||||||
|
| AUTH-04 | `/api/v1/auth/register` | POST | Inbound | App/Web -> Server | New user registration | High |
|
||||||
|
| AUTH-05 | `/api/v1/auth/logout` | POST | Inbound | App/Web -> Server | Invalidate session/tokens | Medium |
|
||||||
|
| AUTH-06 | `/api/v1/auth/refresh` | POST | Inbound | App/Web -> Server | Refresh access token | High |
|
||||||
|
| AUTH-07 | `/api/v1/auth/forgot-password` | POST | Inbound | App/Web -> Server | Initiate password reset | Medium |
|
||||||
|
| AUTH-08 | `/api/v1/auth/reset-password` | POST | Inbound | App/Web -> Server | Complete password reset | Medium |
|
||||||
|
| AUTH-09 | `/api/v1/auth/mfa/setup` | POST | Inbound | App/Web -> Server | Enable 2FA (TOTP/SMS) | Medium |
|
||||||
|
| AUTH-10 | `/api/v1/auth/mfa/verify` | POST | Inbound | App/Web -> Server | Verify 2FA code | High |
|
||||||
|
| AUTH-11 | `/api/v1/auth/oauth/google` | GET | Inbound | App/Web -> Server | Google OAuth redirect | Medium |
|
||||||
|
| AUTH-12 | `/api/v1/auth/oauth/google/callback` | GET | Inbound | Google -> Server | Google OAuth callback | Medium |
|
||||||
|
| AUTH-13 | `/api/v1/auth/oauth/apple` | GET | Inbound | App -> Server | Apple Sign-In redirect | Medium |
|
||||||
|
| AUTH-14 | `/api/v1/auth/oauth/apple/callback` | POST | Inbound | Apple -> Server | Apple Sign-In callback | Medium |
|
||||||
|
| AUTH-15 | `/api/v1/auth/sessions` | GET | Inbound | App/Web -> Server | List active sessions | Low |
|
||||||
|
| AUTH-16 | `/api/v1/auth/sessions/:id` | DELETE | Inbound | App/Web -> Server | Revoke specific session | Medium |
|
||||||
|
| AUTH-17 | `/api/v1/auth/sessions/all` | DELETE | Inbound | App/Web -> Server | Revoke all sessions (except current) | Medium |
|
||||||
|
| AUTH-18 | `/api/v1/partner/auth/login` | POST | Inbound | Partner -> Server | Partner/Organizer login | High |
|
||||||
|
| AUTH-19 | `/api/v1/admin/auth/login` | POST | Inbound | Control Center -> Server | Admin login | High |
|
||||||
|
| AUTH-20 | `/api/v1/admin/auth/impersonate/:userId` | POST | Inbound | Control Center -> Server | Impersonate user session | High |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. User Entity (CRUD & Profile)
|
||||||
|
|
||||||
|
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| USER-01 | `/api/v1/users/me` | GET | Inbound | App/Web -> Server | Get current user profile | High |
|
||||||
|
| USER-02 | `/api/v1/users/me` | PATCH | Inbound | App/Web -> Server | Update profile (name, bio, avatar) | Medium |
|
||||||
|
| USER-03 | `/api/v1/users/me/phone` | PATCH | Inbound | App/Web -> Server | Update phone number | Medium |
|
||||||
|
| USER-04 | `/api/v1/users/me/email` | PATCH | Inbound | App/Web -> Server | Update email address | Medium |
|
||||||
|
| USER-05 | `/api/v1/users/me/preferences` | PATCH | Inbound | App/Web -> Server | Update notification/privacy preferences | Low |
|
||||||
|
| USER-06 | `/api/v1/users/me/avatar` | POST | Inbound | App/Web -> Server | Upload avatar image | Low |
|
||||||
|
| USER-07 | `/api/v1/users/me/delete` | POST | Inbound | App/Web -> Server | Request account deletion (GDPR) | Medium |
|
||||||
|
| USER-08 | `/api/v1/users/:id` | GET | Inbound | App/Web -> Server | Get public user profile | Low |
|
||||||
|
| USER-09 | `/api/v1/users/:id/follow` | POST | Inbound | App -> Server | Follow another user | Low |
|
||||||
|
| USER-10 | `/api/v1/users/:id/unfollow` | POST | Inbound | App -> Server | Unfollow user | Low |
|
||||||
|
| USER-11 | `/api/v1/users/me/followers` | GET | Inbound | App -> Server | List followers | Low |
|
||||||
|
| USER-12 | `/api/v1/users/me/following` | GET | Inbound | App -> Server | List following | Low |
|
||||||
|
| USER-13 | `/api/v1/admin/users` | GET | Inbound | Control Center -> Server | List all users (paginated, filterable) | High |
|
||||||
|
| USER-14 | `/api/v1/admin/users/:id` | GET | Inbound | Control Center -> Server | Get user details (admin view) | High |
|
||||||
|
| USER-15 | `/api/v1/admin/users/:id` | PATCH | Inbound | Control Center -> Server | Update user (admin override) | High |
|
||||||
|
| USER-16 | `/api/v1/admin/users/:id/suspend` | POST | Inbound | Control Center -> Server | Suspend user account | High |
|
||||||
|
| USER-17 | `/api/v1/admin/users/:id/ban` | POST | Inbound | Control Center -> Server | Ban user permanently | High |
|
||||||
|
| USER-18 | `/api/v1/admin/users/:id/reinstate` | POST | Inbound | Control Center -> Server | Reinstate suspended/banned user | High |
|
||||||
|
| USER-19 | `/api/v1/admin/users/:id/notes` | POST | Inbound | Control Center -> Server | Add internal admin note | Low |
|
||||||
|
| USER-20 | `/api/v1/admin/users/:id/tags` | PATCH | Inbound | Control Center -> Server | Update user tags | Low |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Events (Discovery, CRUD, Publishing)
|
||||||
|
|
||||||
|
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| EVT-01 | `/api/v1/events` | GET | Inbound | App/Web -> Server | List/search events (public) | High |
|
||||||
|
| EVT-02 | `/api/v1/events/:id` | GET | Inbound | App/Web -> Server | Get event details | High |
|
||||||
|
| EVT-03 | `/api/v1/events/:id/tickets` | GET | Inbound | App/Web -> Server | Get ticket types for event | High |
|
||||||
|
| EVT-04 | `/api/v1/events/featured` | GET | Inbound | App/Web -> Server | Get featured/promoted events | Medium |
|
||||||
|
| EVT-05 | `/api/v1/events/nearby` | GET | Inbound | App -> Server | Get events near user (geo) | Medium |
|
||||||
|
| EVT-06 | `/api/v1/events/categories` | GET | Inbound | App/Web -> Server | List event categories | Low |
|
||||||
|
| EVT-07 | `/api/v1/events/:id/similar` | GET | Inbound | App/Web -> Server | Get similar events | Low |
|
||||||
|
| EVT-08 | `/api/v1/partner/events` | GET | Inbound | Partner -> Server | List organizer's events | High |
|
||||||
|
| EVT-09 | `/api/v1/partner/events` | POST | Inbound | Partner -> Server | Create new event | High |
|
||||||
|
| EVT-10 | `/api/v1/partner/events/:id` | GET | Inbound | Partner -> Server | Get event details (owner) | High |
|
||||||
|
| EVT-11 | `/api/v1/partner/events/:id` | PATCH | Inbound | Partner -> Server | Update event | High |
|
||||||
|
| EVT-12 | `/api/v1/partner/events/:id` | DELETE | Inbound | Partner -> Server | Delete/cancel event | High |
|
||||||
|
| EVT-13 | `/api/v1/partner/events/:id/publish` | POST | Inbound | Partner -> Server | Publish event (make live) | High |
|
||||||
|
| EVT-14 | `/api/v1/partner/events/:id/unpublish` | POST | Inbound | Partner -> Server | Unpublish/draft event | Medium |
|
||||||
|
| EVT-15 | `/api/v1/partner/events/:id/duplicate` | POST | Inbound | Partner -> Server | Clone event | Low |
|
||||||
|
| EVT-16 | `/api/v1/partner/events/:id/tickets` | POST | Inbound | Partner -> Server | Create ticket type | High |
|
||||||
|
| EVT-17 | `/api/v1/partner/events/:id/tickets/:ticketId` | PATCH | Inbound | Partner -> Server | Update ticket type | High |
|
||||||
|
| EVT-18 | `/api/v1/partner/events/:id/tickets/:ticketId` | DELETE | Inbound | Partner -> Server | Delete ticket type | Medium |
|
||||||
|
| EVT-19 | `/api/v1/admin/events` | GET | Inbound | Control Center -> Server | List all events (admin) | High |
|
||||||
|
| EVT-20 | `/api/v1/admin/events/:id` | PATCH | Inbound | Control Center -> Server | Override event details | High |
|
||||||
|
| EVT-21 | `/api/v1/admin/events/:id/approve` | POST | Inbound | Control Center -> Server | Approve pending event | High |
|
||||||
|
| EVT-22 | `/api/v1/admin/events/:id/reject` | POST | Inbound | Control Center -> Server | Reject pending event | High |
|
||||||
|
| EVT-23 | `/api/v1/admin/events/:id/feature` | POST | Inbound | Control Center -> Server | Feature event on homepage | Medium |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Orders & Tickets (Commerce)
|
||||||
|
|
||||||
|
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| ORD-01 | `/api/v1/orders` | POST | Inbound | App/Web -> Server | Create order (cart checkout) | High |
|
||||||
|
| ORD-02 | `/api/v1/orders/:id` | GET | Inbound | App/Web -> Server | Get order details | High |
|
||||||
|
| ORD-03 | `/api/v1/orders/:id/pay` | POST | Inbound | App/Web -> Server | Initiate payment for order | High |
|
||||||
|
| ORD-04 | `/api/v1/orders/:id/cancel` | POST | Inbound | App/Web -> Server | Cancel pending order | Medium |
|
||||||
|
| ORD-05 | `/api/v1/users/me/orders` | GET | Inbound | App/Web -> Server | List user's orders | High |
|
||||||
|
| ORD-06 | `/api/v1/users/me/tickets` | GET | Inbound | App/Web -> Server | List user's tickets (wallet) | High |
|
||||||
|
| ORD-07 | `/api/v1/tickets/:id` | GET | Inbound | App/Web -> Server | Get ticket details (for wallet) | High |
|
||||||
|
| ORD-08 | `/api/v1/tickets/:id/qr` | GET | Inbound | App -> Server | Get QR code data for ticket | High |
|
||||||
|
| ORD-09 | `/api/v1/tickets/:id/transfer` | POST | Inbound | App -> Server | Transfer ticket to another user | Medium |
|
||||||
|
| ORD-10 | `/api/v1/tickets/:id/accept-transfer` | POST | Inbound | App -> Server | Accept incoming ticket transfer | Medium |
|
||||||
|
| ORD-11 | `/api/v1/partner/orders` | GET | Inbound | Partner -> Server | List orders for organizer's events | High |
|
||||||
|
| ORD-12 | `/api/v1/partner/orders/:id` | GET | Inbound | Partner -> Server | Get order details (organizer) | High |
|
||||||
|
| ORD-13 | `/api/v1/partner/orders/:id/refund` | POST | Inbound | Partner -> Server | Initiate refund | High |
|
||||||
|
| ORD-14 | `/api/v1/partner/attendees` | GET | Inbound | Partner -> Server | List attendees for event | High |
|
||||||
|
| ORD-15 | `/api/v1/admin/orders` | GET | Inbound | Control Center -> Server | List all orders (admin) | High |
|
||||||
|
| ORD-16 | `/api/v1/admin/orders/:id` | GET | Inbound | Control Center -> Server | Get order details (admin) | High |
|
||||||
|
| ORD-17 | `/api/v1/admin/orders/:id/refund` | POST | Inbound | Control Center -> Server | Force refund (admin override) | High |
|
||||||
|
| ORD-18 | `/api/v1/admin/orders/:id/receipt` | GET | Inbound | Control Center -> Server | Generate/download receipt | Medium |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Payments & Payouts
|
||||||
|
|
||||||
|
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| PAY-01 | `/api/v1/payments/razorpay/create-order` | POST | Inbound | App/Web -> Server | Create Razorpay order | High |
|
||||||
|
| PAY-02 | `/api/v1/payments/razorpay/verify` | POST | Inbound | App/Web -> Server | Verify Razorpay payment signature | High |
|
||||||
|
| PAY-03 | `/api/v1/payments/stripe/create-intent` | POST | Inbound | App/Web -> Server | Create Stripe PaymentIntent | High |
|
||||||
|
| PAY-04 | `/api/v1/payments/stripe/confirm` | POST | Inbound | App/Web -> Server | Confirm Stripe payment | High |
|
||||||
|
| PAY-05 | `/api/v1/webhooks/razorpay` | POST | Inbound | Razorpay -> Server | Razorpay webhook events | High |
|
||||||
|
| PAY-06 | `/api/v1/webhooks/stripe` | POST | Inbound | Stripe -> Server | Stripe webhook events | High |
|
||||||
|
| PAY-07 | `/api/v1/partner/payouts` | GET | Inbound | Partner -> Server | List payout history | High |
|
||||||
|
| PAY-08 | `/api/v1/partner/payouts/:id` | GET | Inbound | Partner -> Server | Get payout details | High |
|
||||||
|
| PAY-09 | `/api/v1/partner/bank-account` | GET | Inbound | Partner -> Server | Get linked bank account | High |
|
||||||
|
| PAY-10 | `/api/v1/partner/bank-account` | POST | Inbound | Partner -> Server | Link bank account for payouts | High |
|
||||||
|
| PAY-11 | `/api/v1/partner/bank-account` | PATCH | Inbound | Partner -> Server | Update bank account | High |
|
||||||
|
| PAY-12 | `/api/v1/admin/payouts` | GET | Inbound | Control Center -> Server | List all payouts (admin) | High |
|
||||||
|
| PAY-13 | `/api/v1/admin/payouts/:id/approve` | POST | Inbound | Control Center -> Server | Approve payout | High |
|
||||||
|
| PAY-14 | `/api/v1/admin/payouts/:id/hold` | POST | Inbound | Control Center -> Server | Hold payout | High |
|
||||||
|
| PAY-15 | `/api/v1/admin/payouts/:id/release` | POST | Inbound | Control Center -> Server | Release held payout | High |
|
||||||
|
| PAY-16 | `Razorpay Payout API` | POST | Outbound | Server -> Razorpay | Initiate payout to organizer | High |
|
||||||
|
| PAY-17 | `Stripe Connect Payout` | POST | Outbound | Server -> Stripe | Transfer funds to connected account | High |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Operations (Check-in, Scanning, Staff)
|
||||||
|
|
||||||
|
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| OPS-01 | `/api/v1/partner/scan/validate` | POST | Inbound | Partner App -> Server | Validate scanned QR code | High |
|
||||||
|
| OPS-02 | `/api/v1/partner/scan/checkin` | POST | Inbound | Partner App -> Server | Check-in attendee | High |
|
||||||
|
| OPS-03 | `/api/v1/partner/scan/checkout` | POST | Inbound | Partner App -> Server | Check-out attendee (optional) | Low |
|
||||||
|
| OPS-04 | `/api/v1/partner/events/:id/checkin-stats` | GET | Inbound | Partner -> Server | Get live check-in statistics | Medium |
|
||||||
|
| OPS-05 | `/api/v1/partner/staff` | GET | Inbound | Partner -> Server | List event staff | Medium |
|
||||||
|
| OPS-06 | `/api/v1/partner/staff` | POST | Inbound | Partner -> Server | Invite staff member | Medium |
|
||||||
|
| OPS-07 | `/api/v1/partner/staff/:id` | DELETE | Inbound | Partner -> Server | Remove staff member | Medium |
|
||||||
|
| OPS-08 | `/api/v1/partner/staff/:id/permissions` | PATCH | Inbound | Partner -> Server | Update staff permissions | Medium |
|
||||||
|
| OPS-09 | `/api/v1/staff/auth/login` | POST | Inbound | Staff App -> Server | Staff member login | High |
|
||||||
|
| OPS-10 | `/api/v1/staff/events` | GET | Inbound | Staff App -> Server | List assigned events | Medium |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Communication (Email, Push, SMS, Chat)
|
||||||
|
|
||||||
|
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| COM-01 | `Resend API - Transactional` | POST | Outbound | Server -> Resend | Send order confirmation email | High |
|
||||||
|
| COM-02 | `Resend API - Marketing` | POST | Outbound | Server -> Resend | Send marketing/promo email | Low |
|
||||||
|
| COM-03 | `FCM - Push Notification` | POST | Outbound | Server -> FCM | Send push notification to user | Medium |
|
||||||
|
| COM-04 | `APNs - Push Notification` | POST | Outbound | Server -> APNs | Send iOS push notification | Medium |
|
||||||
|
| COM-05 | `Twilio SMS` | POST | Outbound | Server -> Twilio | Send SMS (OTP, alerts) | High |
|
||||||
|
| COM-06 | `/api/v1/users/me/devices` | POST | Inbound | App -> Server | Register device for push | Medium |
|
||||||
|
| COM-07 | `/api/v1/users/me/devices/:id` | DELETE | Inbound | App -> Server | Unregister device | Low |
|
||||||
|
| COM-08 | `/api/v1/webhooks/resend` | POST | Inbound | Resend -> Server | Email delivery status webhook | Medium |
|
||||||
|
| COM-09 | `/api/v1/webhooks/twilio` | POST | Inbound | Twilio -> Server | SMS delivery status webhook | Medium |
|
||||||
|
| COM-10 | `/api/v1/admin/notifications/send` | POST | Inbound | Control Center -> Server | Send notification to user | Medium |
|
||||||
|
| COM-11 | `/api/v1/admin/notifications/broadcast` | POST | Inbound | Control Center -> Server | Broadcast to user segment | Medium |
|
||||||
|
| COM-12 | `/api/v1/chat/conversations` | GET | Inbound | App -> Server | List user's conversations | Low |
|
||||||
|
| COM-13 | `/api/v1/chat/conversations/:id/messages` | GET | Inbound | App -> Server | Get messages in conversation | Low |
|
||||||
|
| COM-14 | `/api/v1/chat/conversations/:id/messages` | POST | Inbound | App -> Server | Send message | Low |
|
||||||
|
| COM-15 | `WebSocket /ws/chat` | WS | Bidirectional | App <-> Server | Real-time chat connection | Low |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Data & Analytics
|
||||||
|
|
||||||
|
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| DAT-01 | `/api/v1/partner/analytics/overview` | GET | Inbound | Partner -> Server | Get sales overview dashboard | Medium |
|
||||||
|
| DAT-02 | `/api/v1/partner/analytics/sales` | GET | Inbound | Partner -> Server | Get detailed sales data | Medium |
|
||||||
|
| DAT-03 | `/api/v1/partner/analytics/attendees` | GET | Inbound | Partner -> Server | Get attendee demographics | Low |
|
||||||
|
| DAT-04 | `/api/v1/partner/analytics/traffic` | GET | Inbound | Partner -> Server | Get page view/traffic data | Low |
|
||||||
|
| DAT-05 | `/api/v1/partner/export/attendees` | GET | Outbound | Server -> Browser | Export attendee list (CSV) | Medium |
|
||||||
|
| DAT-06 | `/api/v1/partner/export/orders` | GET | Outbound | Server -> Browser | Export orders (CSV) | Medium |
|
||||||
|
| DAT-07 | `/api/v1/admin/analytics/overview` | GET | Inbound | Control Center -> Server | Platform-wide analytics | Medium |
|
||||||
|
| DAT-08 | `/api/v1/admin/analytics/revenue` | GET | Inbound | Control Center -> Server | Revenue breakdown | High |
|
||||||
|
| DAT-09 | `/api/v1/admin/analytics/users` | GET | Inbound | Control Center -> Server | User growth metrics | Medium |
|
||||||
|
| DAT-10 | `/api/v1/admin/export/users` | GET | Outbound | Server -> Browser | Export users (CSV) | Medium |
|
||||||
|
| DAT-11 | `/api/v1/admin/export/orders` | GET | Outbound | Server -> Browser | Export orders (CSV) | Medium |
|
||||||
|
| DAT-12 | `/api/v1/admin/export/payouts` | GET | Outbound | Server -> Browser | Export payouts (CSV) | Medium |
|
||||||
|
| DAT-13 | `/api/v1/admin/audit-logs` | GET | Inbound | Control Center -> Server | Get admin activity logs | High |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Third-Party Integrations
|
||||||
|
|
||||||
|
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| INT-01 | `AWS S3 - Presigned URL` | POST | Outbound | Server -> S3 | Generate upload URL | Medium |
|
||||||
|
| INT-02 | `/api/v1/uploads/presigned` | GET | Inbound | App/Web -> Server | Request presigned URL for upload | Medium |
|
||||||
|
| INT-03 | `Google Maps Geocoding API` | GET | Outbound | Server -> Google | Geocode event address | Low |
|
||||||
|
| INT-04 | `Google Maps Places API` | GET | Outbound | Server -> Google | Autocomplete venue search | Low |
|
||||||
|
| INT-05 | `/api/v1/geo/ip` | GET | Inbound | App/Web -> Server | Get location from IP | Low |
|
||||||
|
| INT-06 | `MaxMind GeoIP` | GET | Internal | Server -> MaxMind DB | IP to location lookup | Low |
|
||||||
|
| INT-07 | `/api/v1/calendar/google/export` | GET | Inbound | App/Web -> Server | Generate Google Calendar link | Low |
|
||||||
|
| INT-08 | `/api/v1/calendar/ical/export` | GET | Inbound | App/Web -> Server | Generate iCal file | Low |
|
||||||
|
| INT-09 | `Sentry - Error Reporting` | POST | Outbound | Server -> Sentry | Log errors | Medium |
|
||||||
|
| INT-10 | `Slack Webhook` | POST | Outbound | Server -> Slack | Alert on critical events | Medium |
|
||||||
|
| INT-11 | `PagerDuty API` | POST | Outbound | Server -> PagerDuty | Trigger on-call alerts | High |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Support & Moderation
|
||||||
|
|
||||||
|
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| SUP-01 | `/api/v1/admin/tickets` | GET | Inbound | Control Center -> Server | List support tickets | Medium |
|
||||||
|
| SUP-02 | `/api/v1/admin/tickets/:id` | GET | Inbound | Control Center -> Server | Get ticket details | Medium |
|
||||||
|
| SUP-03 | `/api/v1/admin/tickets` | POST | Inbound | Control Center -> Server | Create escalation ticket | Medium |
|
||||||
|
| SUP-04 | `/api/v1/admin/tickets/:id/reply` | POST | Inbound | Control Center -> Server | Reply to ticket | Medium |
|
||||||
|
| SUP-05 | `/api/v1/admin/tickets/:id/close` | POST | Inbound | Control Center -> Server | Close ticket | Medium |
|
||||||
|
| SUP-06 | `/api/v1/admin/tickets/:id/assign` | POST | Inbound | Control Center -> Server | Assign ticket to agent | Medium |
|
||||||
|
| SUP-07 | `/api/v1/users/me/tickets` | GET | Inbound | App/Web -> Server | List user's support tickets | Low |
|
||||||
|
| SUP-08 | `/api/v1/users/me/tickets` | POST | Inbound | App/Web -> Server | Create support ticket | Low |
|
||||||
|
| SUP-09 | `/api/v1/admin/moderation/reports` | GET | Inbound | Control Center -> Server | List content reports | Medium |
|
||||||
|
| SUP-10 | `/api/v1/admin/moderation/reports/:id/action` | POST | Inbound | Control Center -> Server | Take action on report | Medium |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. System & Utilities
|
||||||
|
|
||||||
|
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| SYS-01 | `/api/v1/health` | GET | Inbound | Any -> Server | Health check | High |
|
||||||
|
| SYS-02 | `/api/v1/health/db` | GET | Inbound | Internal -> Server | Database connectivity check | High |
|
||||||
|
| SYS-03 | `/api/v1/health/redis` | GET | Inbound | Internal -> Server | Redis connectivity check | High |
|
||||||
|
| SYS-04 | `/api/v1/config/mobile` | GET | Inbound | App -> Server | Get mobile app config (feature flags) | Medium |
|
||||||
|
| SYS-05 | `/api/v1/config/web` | GET | Inbound | Web -> Server | Get web config (feature flags) | Medium |
|
||||||
|
| SYS-06 | `/api/v1/version` | GET | Inbound | Any -> Server | Get API version info | Low |
|
||||||
|
| SYS-07 | `Cron: Payout Processor` | CRON | Internal | Scheduler -> Server | Process pending payouts | High |
|
||||||
|
| SYS-08 | `Cron: Reminder Emails` | CRON | Internal | Scheduler -> Server | Send event reminder emails | Medium |
|
||||||
|
| SYS-09 | `Cron: Expired Orders Cleanup` | CRON | Internal | Scheduler -> Server | Cancel expired pending orders | Medium |
|
||||||
|
| SYS-10 | `Cron: Analytics Aggregation` | CRON | Internal | Scheduler -> Server | Pre-compute analytics | Low |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary Statistics
|
||||||
|
|
||||||
|
| Category | Total Endpoints |
|
||||||
|
| :--- | :---: |
|
||||||
|
| Auth & Identity | 20 |
|
||||||
|
| User Entity | 20 |
|
||||||
|
| Events | 23 |
|
||||||
|
| Orders & Tickets | 18 |
|
||||||
|
| Payments & Payouts | 17 |
|
||||||
|
| Operations | 10 |
|
||||||
|
| Communication | 15 |
|
||||||
|
| Data & Analytics | 13 |
|
||||||
|
| Third-Party Integrations | 11 |
|
||||||
|
| Support & Moderation | 10 |
|
||||||
|
| System & Utilities | 10 |
|
||||||
|
| **TOTAL** | **167** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Note:** This inventory represents the definitive API surface for the Eventify ecosystem. All development should reference this document for endpoint naming, method conventions, and integration points.
|
||||||
219
src/features/settings/components/GatewayConfigSheet.tsx
Normal file
219
src/features/settings/components/GatewayConfigSheet.tsx
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { GatewayProvider, GatewayCredentials } from '@/lib/types/settings';
|
||||||
|
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { verifyGatewayCredentials, saveGatewayConfig } from '@/lib/actions/payment-settings';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Lock, CheckBase, Loader2, Copy } from 'lucide-react';
|
||||||
|
import { decrypt } from '@/lib/payment-encryption';
|
||||||
|
|
||||||
|
interface GatewayConfigSheetProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
provider: GatewayProvider;
|
||||||
|
initialConfig: GatewayCredentials;
|
||||||
|
onSave: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GatewayConfigSheet({ open, onOpenChange, provider, initialConfig, onSave }: GatewayConfigSheetProps) {
|
||||||
|
const [config, setConfig] = useState<GatewayCredentials>(initialConfig);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [verifying, setVerifying] = useState(false);
|
||||||
|
|
||||||
|
// Reset config when provider changes
|
||||||
|
useEffect(() => {
|
||||||
|
// Decrypt sensitive fields for editing
|
||||||
|
const decrypted = { ...initialConfig };
|
||||||
|
if (decrypted.salt) decrypted.salt = decrypt(decrypted.salt);
|
||||||
|
setConfig(decrypted);
|
||||||
|
}, [provider, initialConfig, open]);
|
||||||
|
|
||||||
|
const handleVerify = async () => {
|
||||||
|
setVerifying(true);
|
||||||
|
try {
|
||||||
|
const res = await verifyGatewayCredentials(provider, config);
|
||||||
|
if (res.success) {
|
||||||
|
toast.success('Credentials Verified', { description: res.message });
|
||||||
|
} else {
|
||||||
|
toast.error('Verification Failed', { description: res.message });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Verification Error');
|
||||||
|
} finally {
|
||||||
|
setVerifying(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await saveGatewayConfig(provider, config);
|
||||||
|
if (res.success) {
|
||||||
|
toast.success(`Saved ${provider} configuration`);
|
||||||
|
onSave();
|
||||||
|
onOpenChange(false);
|
||||||
|
} else {
|
||||||
|
toast.error(res.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to save configuration');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
|
<SheetContent className="w-[400px] sm:w-[540px] overflow-y-auto">
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle className="flex items-center gap-2 capitalize">
|
||||||
|
{provider} Configuration
|
||||||
|
</SheetTitle>
|
||||||
|
<SheetDescription>
|
||||||
|
Configure API keys and secrets for {provider}.
|
||||||
|
</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
<div className="py-6 space-y-6">
|
||||||
|
{/* Environment Toggle */}
|
||||||
|
<div className="flex items-center justify-between p-4 border rounded-lg bg-muted/30">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label>Mode</Label>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{config.mode === 'live' ? 'Production / Live Traffic' : 'Sandbox / Test Mode'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={config.mode === 'live'}
|
||||||
|
onCheckedChange={(c) => setConfig({ ...config, mode: c ? 'live' : 'test' })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="credentials">
|
||||||
|
<TabsList className="w-full">
|
||||||
|
<TabsTrigger value="credentials" className="flex-1">Credentials</TabsTrigger>
|
||||||
|
<TabsTrigger value="features" className="flex-1">Features</TabsTrigger>
|
||||||
|
<TabsTrigger value="webhooks" className="flex-1">Webhooks</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="credentials" className="space-y-4 pt-4">
|
||||||
|
{/* Dynamic inputs based on provider */}
|
||||||
|
{(provider === 'razorpay' || provider === 'easebuzz') && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Key ID / Merchant Key</Label>
|
||||||
|
<Input
|
||||||
|
value={config.keyId || config.merchantId || ''}
|
||||||
|
onChange={(e) => setConfig({ ...config, keyId: e.target.value, merchantId: e.target.value })}
|
||||||
|
placeholder="rzp_test_..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(provider === 'stripe') && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Public Key</Label>
|
||||||
|
<Input
|
||||||
|
value={config.publicKey || ''}
|
||||||
|
onChange={(e) => setConfig({ ...config, publicKey: e.target.value })}
|
||||||
|
placeholder="pk_test_..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(provider === 'payu' || provider === 'easebuzz') && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Salt / Secret Key</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={config.salt || ''}
|
||||||
|
onChange={(e) => setConfig({ ...config, salt: e.target.value })}
|
||||||
|
placeholder="Enter secret salt"
|
||||||
|
className="pr-10"
|
||||||
|
/>
|
||||||
|
<Lock className="absolute right-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button variant="secondary" className="w-full mt-4" onClick={handleVerify} disabled={verifying}>
|
||||||
|
{verifying && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
Test Credentials
|
||||||
|
</Button>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="features" className="space-y-4 pt-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-center space-x-2 border p-3 rounded-md">
|
||||||
|
<Switch
|
||||||
|
id="feat-nb"
|
||||||
|
checked={config.features.netbanking}
|
||||||
|
onCheckedChange={(c) => setConfig({ ...config, features: { ...config.features, netbanking: c } })}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="feat-nb">Netbanking</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 border p-3 rounded-md">
|
||||||
|
<Switch
|
||||||
|
id="feat-upi"
|
||||||
|
checked={config.features.upi}
|
||||||
|
onCheckedChange={(c) => setConfig({ ...config, features: { ...config.features, upi: c } })}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="feat-upi">UPI</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 border p-3 rounded-md">
|
||||||
|
<Switch
|
||||||
|
id="feat-cards"
|
||||||
|
checked={config.features.cards}
|
||||||
|
onCheckedChange={(c) => setConfig({ ...config, features: { ...config.features, cards: c } })}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="feat-cards">Cards</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 border p-3 rounded-md">
|
||||||
|
<Switch
|
||||||
|
id="feat-emi"
|
||||||
|
checked={config.features.emi}
|
||||||
|
onCheckedChange={(c) => setConfig({ ...config, features: { ...config.features, emi: c } })}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="feat-emi">EMI</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="webhooks" className="space-y-4 pt-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Webhook URL</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input readOnly value={`https://api.eventify.com/webhooks/${provider}`} />
|
||||||
|
<Button variant="outline" size="icon">
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Add this URL to your provider dashboard.</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Webhook Secret</Label>
|
||||||
|
<Input
|
||||||
|
value={config.webhookSecret || ''}
|
||||||
|
onChange={(e) => setConfig({ ...config, webhookSecret: e.target.value })}
|
||||||
|
placeholder="whsec_..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SheetFooter>
|
||||||
|
<Button onClick={handleSave} disabled={loading} className="w-full sm:w-auto">
|
||||||
|
{loading ? 'Saving...' : 'Save Configuration'}
|
||||||
|
</Button>
|
||||||
|
</SheetFooter>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
143
src/features/settings/components/SettingsLayout.tsx
Normal file
143
src/features/settings/components/SettingsLayout.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { getSystemSettings } from '@/lib/actions/settings';
|
||||||
|
import { GlobalSettings, DEFAULT_SETTINGS } from '@/lib/types/settings';
|
||||||
|
import { OrganizationSettings } from './tabs/OrganizationSettings';
|
||||||
|
import { PublicAppConfigTab } from './tabs/PublicAppConfig';
|
||||||
|
import { PartnerGovernanceTab } from './tabs/PartnerGovernance';
|
||||||
|
import { SystemHealthTab } from './tabs/SystemHealth';
|
||||||
|
import { PaymentConfigTab } from './tabs/PaymentConfig';
|
||||||
|
import { Loader2, Settings, Smartphone, Building2, Handshake, Server, CreditCard } from 'lucide-react';
|
||||||
|
|
||||||
|
export function SettingsLayout() {
|
||||||
|
const [settings, setSettings] = useState<GlobalSettings | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadSettings() {
|
||||||
|
try {
|
||||||
|
const res = await getSystemSettings();
|
||||||
|
if (res.success) {
|
||||||
|
setSettings(res.data);
|
||||||
|
} else {
|
||||||
|
setSettings(DEFAULT_SETTINGS); // Fallback
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setSettings(DEFAULT_SETTINGS);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadSettings();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUpdate = (section: keyof GlobalSettings, data: any) => {
|
||||||
|
if (!settings) return;
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
[section]: { ...settings[section], ...data }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading || !settings) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-[60vh] text-muted-foreground">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin mb-4" />
|
||||||
|
<p>Loading system configuration...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full space-y-6 container mx-auto p-6 max-w-7xl">
|
||||||
|
<div className="flex items-center justify-between pb-2">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">System Settings</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Global configuration for User App, Partner Dashboard, and Backoffice.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="organization" orientation="vertical" className="flex-1 w-full flex flex-col md:flex-row gap-8">
|
||||||
|
<Card className="md:w-64 flex-shrink-0 h-fit bg-muted/30 p-2 border shadow-sm">
|
||||||
|
<TabsList className="bg-transparent flex flex-col h-auto items-start w-full space-y-1 p-0">
|
||||||
|
<TabsTrigger
|
||||||
|
value="organization"
|
||||||
|
className="w-full justify-start px-3 py-2.5 data-[state=active]:bg-background data-[state=active]:shadow-sm rounded-md transition-all font-medium"
|
||||||
|
>
|
||||||
|
<Building2 className="h-4 w-4 mr-2" />
|
||||||
|
Organization
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="payment"
|
||||||
|
className="w-full justify-start px-3 py-2.5 data-[state=active]:bg-background data-[state=active]:shadow-sm rounded-md transition-all font-medium"
|
||||||
|
>
|
||||||
|
<CreditCard className="h-4 w-4 mr-2" />
|
||||||
|
Payment Gateways
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="public-app"
|
||||||
|
className="w-full justify-start px-3 py-2.5 data-[state=active]:bg-background data-[state=active]:shadow-sm rounded-md transition-all font-medium"
|
||||||
|
>
|
||||||
|
<Smartphone className="h-4 w-4 mr-2" />
|
||||||
|
Public App
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="partner-governance"
|
||||||
|
className="w-full justify-start px-3 py-2.5 data-[state=active]:bg-background data-[state=active]:shadow-sm rounded-md transition-all font-medium"
|
||||||
|
>
|
||||||
|
<Handshake className="h-4 w-4 mr-2" />
|
||||||
|
Partner Governance
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="system-health"
|
||||||
|
className="w-full justify-start px-3 py-2.5 data-[state=active]:bg-background data-[state=active]:shadow-sm rounded-md transition-all font-medium"
|
||||||
|
>
|
||||||
|
<Server className="h-4 w-4 mr-2" />
|
||||||
|
System Health
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<TabsContent value="organization" className="mt-0 space-y-4 animate-in fade-in-50 slide-in-from-bottom-2 duration-300">
|
||||||
|
<OrganizationSettings
|
||||||
|
orgConfig={settings.organization}
|
||||||
|
securityConfig={settings.security}
|
||||||
|
onUpdate={handleUpdate}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="payment" className="mt-0 space-y-4 animate-in fade-in-50 slide-in-from-bottom-2 duration-300">
|
||||||
|
<PaymentConfigTab
|
||||||
|
config={settings.payment}
|
||||||
|
onUpdate={(data) => handleUpdate('payment', data)}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="public-app" className="mt-0 space-y-4 animate-in fade-in-50 slide-in-from-bottom-2 duration-300">
|
||||||
|
<PublicAppConfigTab
|
||||||
|
config={settings.publicApp}
|
||||||
|
onUpdate={(data) => handleUpdate('publicApp', data)}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="partner-governance" className="mt-0 space-y-4 animate-in fade-in-50 slide-in-from-bottom-2 duration-300">
|
||||||
|
<PartnerGovernanceTab
|
||||||
|
config={settings.partner}
|
||||||
|
onUpdate={(data) => handleUpdate('partner', data)}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="system-health" className="mt-0 space-y-4 animate-in fade-in-50 slide-in-from-bottom-2 duration-300">
|
||||||
|
<SystemHealthTab
|
||||||
|
config={settings.system}
|
||||||
|
onUpdate={(data) => handleUpdate('system', data)}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
158
src/features/settings/components/tabs/OrganizationSettings.tsx
Normal file
158
src/features/settings/components/tabs/OrganizationSettings.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { OrganizationConfig, SecurityConfig } from '@/lib/types/settings';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { updateSystemSetting } from '@/lib/actions/settings';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Building2, ShieldCheck, Mail, Globe } from 'lucide-react';
|
||||||
|
|
||||||
|
interface OrganizationSettingsProps {
|
||||||
|
orgConfig: OrganizationConfig;
|
||||||
|
securityConfig: SecurityConfig;
|
||||||
|
onUpdate: (section: 'organization' | 'security', data: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OrganizationSettings({ orgConfig, securityConfig, onUpdate }: OrganizationSettingsProps) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Internal state for form usage
|
||||||
|
const [orgState, setOrgState] = useState(orgConfig);
|
||||||
|
const [secState, setSecState] = useState(securityConfig);
|
||||||
|
|
||||||
|
const handleSaveOrg = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await updateSystemSetting('organization', orgState);
|
||||||
|
if (res.success) {
|
||||||
|
toast.success('Organization profile updated');
|
||||||
|
onUpdate('organization', res.updatedSettings.organization);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to update organization settings');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveSecurity = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await updateSystemSetting('security', secState);
|
||||||
|
if (res.success) {
|
||||||
|
toast.success('Security policy updated');
|
||||||
|
onUpdate('security', res.updatedSettings.security);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to update security settings');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Building2 className="h-5 w-5 text-primary" />
|
||||||
|
Organization Profile
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
General branding and contact details for the control center and emails.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Brand Name</Label>
|
||||||
|
<Input
|
||||||
|
value={orgState.brandName}
|
||||||
|
onChange={(e) => setOrgState({ ...orgState, brandName: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Support Email</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
className="pl-9"
|
||||||
|
value={orgState.supportEmail}
|
||||||
|
onChange={(e) => setOrgState({ ...orgState, supportEmail: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Legal Address</Label>
|
||||||
|
<Input
|
||||||
|
value={orgState.legalAddress}
|
||||||
|
onChange={(e) => setOrgState({ ...orgState, legalAddress: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-4">
|
||||||
|
<Button onClick={handleSaveOrg} disabled={loading}>
|
||||||
|
{loading ? 'Saving...' : 'Save Profile'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-orange-200 bg-orange-50/10">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<ShieldCheck className="h-5 w-5 text-orange-600" />
|
||||||
|
Security Policy
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enforce security rules for all Control Center staff members.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between p-4 border rounded-lg bg-card">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label className="text-base">Enforce 2FA</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Require Two-Factor Authentication for all admin accounts.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={secState.enforce2FA}
|
||||||
|
onCheckedChange={(checked) => setSecState({ ...secState, enforce2FA: checked })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Session Timeout (Minutes)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={secState.sessionTimeoutMinutes}
|
||||||
|
onChange={(e) => setSecState({ ...secState, sessionTimeoutMinutes: parseInt(e.target.value) })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Password Expiration (Days)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={secState.passwordExpirationDays}
|
||||||
|
onChange={(e) => setSecState({ ...secState, passwordExpirationDays: parseInt(e.target.value) })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button variant="outline" onClick={handleSaveSecurity} disabled={loading} className="border-orange-200 text-orange-700 hover:bg-orange-50 hover:text-orange-800">
|
||||||
|
{loading ? 'Saving...' : 'Update Security Policy'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
173
src/features/settings/components/tabs/PartnerGovernance.tsx
Normal file
173
src/features/settings/components/tabs/PartnerGovernance.tsx
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { PartnerConfig } from '@/lib/types/settings';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { updateSystemSetting } from '@/lib/actions/settings';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Handshake, Banknote, FileCheck } from 'lucide-react';
|
||||||
|
|
||||||
|
interface PartnerGovernanceProps {
|
||||||
|
config: PartnerConfig;
|
||||||
|
onUpdate: (data: PartnerConfig) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PartnerGovernanceTab({ config, onUpdate }: PartnerGovernanceProps) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [state, setState] = useState(config);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await updateSystemSetting('partner', state);
|
||||||
|
if (res.success && res.updatedSettings.partner) {
|
||||||
|
toast.success('Partner governance rules updated');
|
||||||
|
onUpdate(res.updatedSettings.partner);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to update partner settings');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleKycDoc = (doc: 'pan' | 'gst' | 'aadhaar' | 'cheque') => {
|
||||||
|
const current = state.allowedKycDocs;
|
||||||
|
const updated = current.includes(doc)
|
||||||
|
? current.filter(d => d !== doc)
|
||||||
|
: [...current, doc];
|
||||||
|
|
||||||
|
setState({ ...state, allowedKycDocs: updated });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
|
||||||
|
{/* Onboarding Rules */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Handshake className="h-5 w-5 text-indigo-500" />
|
||||||
|
Onboarding & Approval
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Set rules for new organizer accounts and events.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between py-2 border-b">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label>Require KYC Approval</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">Block payouts until verified.</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={state.requireKyc}
|
||||||
|
onCheckedChange={(c) => setState({ ...state, requireKyc: c })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label>Manual Event Approval</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">Admins must approve live events.</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={state.manualEventApproval}
|
||||||
|
onCheckedChange={(c) => setState({ ...state, manualEventApproval: c })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Commission Model */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Banknote className="h-5 w-5 text-green-600" />
|
||||||
|
Commission & Payouts
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Define the default revenue share model.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Default Commission (%)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={state.defaultCommissionPercent}
|
||||||
|
onChange={(e) => setState({ ...state, defaultCommissionPercent: Number(e.target.value) })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Payout Schedule</Label>
|
||||||
|
<Select
|
||||||
|
value={state.payoutSchedule}
|
||||||
|
onValueChange={(v: any) => setState({ ...state, payoutSchedule: v })}
|
||||||
|
>
|
||||||
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="daily">Daily (T+1)</SelectItem>
|
||||||
|
<SelectItem value="weekly">Weekly</SelectItem>
|
||||||
|
<SelectItem value="monthly">Monthly</SelectItem>
|
||||||
|
<SelectItem value="manual">Manual Request</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Min. Payout Amount (₹)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={state.minPayoutAmount}
|
||||||
|
onChange={(e) => setState({ ...state, minPayoutAmount: Number(e.target.value) })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* KYC Requirements */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<FileCheck className="h-5 w-5 text-slate-500" />
|
||||||
|
KYC Requirements
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Select documents required for organizer verification.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{['pan', 'gst', 'aadhaar', 'cheque'].map((doc) => (
|
||||||
|
<div key={doc} className="flex items-center space-x-2 border p-3 rounded-md">
|
||||||
|
<Checkbox
|
||||||
|
id={`doc-${doc}`}
|
||||||
|
checked={state.allowedKycDocs.includes(doc as any)}
|
||||||
|
onCheckedChange={() => toggleKycDoc(doc as any)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`doc-${doc}`} className="uppercase text-xs font-semibold">
|
||||||
|
{doc}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={handleSave} disabled={loading}>
|
||||||
|
{loading ? 'Saving...' : 'Update Governance Rules'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
196
src/features/settings/components/tabs/PaymentConfig.tsx
Normal file
196
src/features/settings/components/tabs/PaymentConfig.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { PaymentConfig, GatewayProvider, GatewayCredentials } from '@/lib/types/settings';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { GatewayConfigSheet } from '@/features/settings/components/GatewayConfigSheet';
|
||||||
|
import { updateRoutingRules } from '@/lib/actions/payment-settings';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { CreditCard, Globe, Settings2, ShieldCheck } from 'lucide-react';
|
||||||
|
|
||||||
|
interface PaymentConfigTabProps {
|
||||||
|
config: PaymentConfig;
|
||||||
|
onUpdate: (data: PaymentConfig) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROVIDERS: { id: GatewayProvider; name: string }[] = [
|
||||||
|
{ id: 'razorpay', name: 'Razorpay' },
|
||||||
|
{ id: 'stripe', name: 'Stripe' },
|
||||||
|
{ id: 'payu', name: 'PayU' },
|
||||||
|
{ id: 'easebuzz', name: 'Easebuzz' },
|
||||||
|
{ id: 'worldline', name: 'Worldline' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function PaymentConfigTab({ config, onUpdate }: PaymentConfigTabProps) {
|
||||||
|
const [selectedProvider, setSelectedProvider] = useState<GatewayProvider | null>(null);
|
||||||
|
const [sheetOpen, setSheetOpen] = useState(false);
|
||||||
|
const [routingState, setRoutingState] = useState(config);
|
||||||
|
|
||||||
|
const handleOpenConfig = (provider: GatewayProvider) => {
|
||||||
|
setSelectedProvider(provider);
|
||||||
|
setSheetOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRoutingUpdate = async (key: keyof PaymentConfig, value: string) => {
|
||||||
|
const newState = { ...routingState, [key]: value };
|
||||||
|
setRoutingState(newState);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await updateRoutingRules({
|
||||||
|
defaultGateway: newState.defaultGateway,
|
||||||
|
fallbackGateway: newState.fallbackGateway,
|
||||||
|
internationalGateway: newState.internationalGateway
|
||||||
|
});
|
||||||
|
if (res.success) {
|
||||||
|
toast.success('Routing rules updated');
|
||||||
|
// Optimistic update handled by local state, but we should propagate up
|
||||||
|
onUpdate(newState);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to update routing');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
|
||||||
|
{/* Gateway Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{PROVIDERS.map((provider) => {
|
||||||
|
const gwConfig = config.gateways[provider.id];
|
||||||
|
const isActive = gwConfig?.enabled;
|
||||||
|
const isLive = gwConfig?.mode === 'live';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={provider.id} className={`group hover:shadow-md transition-shadow ${isActive ? 'border-primary/50 bg-primary/5' : 'opacity-75'}`}>
|
||||||
|
<CardHeader className="flex flex-row items-start justify-between pb-2">
|
||||||
|
<CardTitle className="text-lg font-bold">{provider.name}</CardTitle>
|
||||||
|
{isActive ? (
|
||||||
|
<Badge variant={isLive ? 'default' : 'secondary'} className={isLive ? 'bg-emerald-600' : ''}>
|
||||||
|
{isLive ? 'LIVE' : 'TEST'}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline">Disabled</Badge>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-sm text-muted-foreground space-y-1">
|
||||||
|
{isActive ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ShieldCheck className="h-4 w-4 text-emerald-500" />
|
||||||
|
Verified
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
{gwConfig.features.upi && <Badge variant="outline" className="text-[10px]">UPI</Badge>}
|
||||||
|
{gwConfig.features.cards && <Badge variant="outline" className="text-[10px]">Cards</Badge>}
|
||||||
|
{gwConfig.features.emi && <Badge variant="outline" className="text-[10px]">EMI</Badge>}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
Not configured
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button
|
||||||
|
variant={isActive ? "outline" : "secondary"}
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => handleOpenConfig(provider.id)}
|
||||||
|
>
|
||||||
|
<Settings2 className="h-4 w-4 mr-2" />
|
||||||
|
Configure
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Routing Logic */}
|
||||||
|
<Card className="border-indigo-200 bg-indigo-50/10">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-indigo-700">
|
||||||
|
<Globe className="h-5 w-5" />
|
||||||
|
Smart Routing Logic
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Define how transactions are routed across active gateways.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid md:grid-cols-3 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Default Gateway</Label>
|
||||||
|
<Select
|
||||||
|
value={routingState.defaultGateway}
|
||||||
|
onValueChange={(v) => handleRoutingUpdate('defaultGateway', v)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{PROVIDERS.map(p => <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">Used for 90% of traffic.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Fallback Gateway</Label>
|
||||||
|
<Select
|
||||||
|
value={routingState.fallbackGateway}
|
||||||
|
onValueChange={(v) => handleRoutingUpdate('fallbackGateway', v)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{PROVIDERS.map(p => <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">Used if default fails.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>International (Non-INR)</Label>
|
||||||
|
<Select
|
||||||
|
value={routingState.internationalGateway}
|
||||||
|
onValueChange={(v) => handleRoutingUpdate('internationalGateway', v)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{PROVIDERS.map(p => <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">Cards issued outside India.</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Config Sheet Instance */}
|
||||||
|
{selectedProvider && config.gateways[selectedProvider] && (
|
||||||
|
<GatewayConfigSheet
|
||||||
|
open={sheetOpen}
|
||||||
|
onOpenChange={setSheetOpen}
|
||||||
|
provider={selectedProvider}
|
||||||
|
initialConfig={config.gateways[selectedProvider]}
|
||||||
|
onSave={() => {
|
||||||
|
// Refresh parent data? Handled via Actions and upper state update usually,
|
||||||
|
// but for now we rely on the parent's polling or manual refresh,
|
||||||
|
// effectively we might want to trigger a callback here.
|
||||||
|
// Ideally onUpdate would trigger a re-fetch.
|
||||||
|
window.location.reload(); // Quick dirty refresh to sync state in this demo
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
193
src/features/settings/components/tabs/PublicAppConfig.tsx
Normal file
193
src/features/settings/components/tabs/PublicAppConfig.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { PublicAppConfig } from '@/lib/types/settings';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { updateSystemSetting } from '@/lib/actions/settings';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Smartphone, Zap, Percent, Link as LinkIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
interface PublicAppConfigProps {
|
||||||
|
config: PublicAppConfig;
|
||||||
|
onUpdate: (data: PublicAppConfig) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PublicAppConfigTab({ config, onUpdate }: PublicAppConfigProps) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [state, setState] = useState(config);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await updateSystemSetting('publicApp', state);
|
||||||
|
if (res.success && res.updatedSettings.publicApp) {
|
||||||
|
toast.success('Public App configuration updated');
|
||||||
|
onUpdate(res.updatedSettings.publicApp);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to update app config');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to update deeply nested feature flags
|
||||||
|
const toggleFeature = (key: keyof PublicAppConfig['betaFeatures']) => {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
betaFeatures: {
|
||||||
|
...prev.betaFeatures,
|
||||||
|
[key]: !prev.betaFeatures[key]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
|
||||||
|
{/* Feature Flags */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Zap className="h-5 w-5 text-yellow-500" />
|
||||||
|
Feature Flags
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Toggle beta features for end-users instantly.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between py-2 border-b">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label>Crypto Payments</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">Accept ETH/SOL via Solana Pay</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={state.betaFeatures.cryptoPayments}
|
||||||
|
onCheckedChange={() => toggleFeature('cryptoPayments')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-2 border-b">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label>AI Recommendations</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">Show "Events you might like"</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={state.betaFeatures.aiRecommendations}
|
||||||
|
onCheckedChange={() => toggleFeature('aiRecommendations')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label>Social Login</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">Enable Google/Apple Auth</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={state.betaFeatures.socialLogin}
|
||||||
|
onCheckedChange={() => toggleFeature('socialLogin')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Commercials */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Percent className="h-5 w-5 text-emerald-600" />
|
||||||
|
Booking Fees & Tax
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Set the global platform fee charged to users.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Platform Fee (Flat ₹)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={state.fees.platformFeeFlat}
|
||||||
|
onChange={(e) => setState({
|
||||||
|
...state,
|
||||||
|
fees: { ...state.fees, platformFeeFlat: Number(e.target.value) }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Added to every ticket purchase.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>GST / Tax Rate (%)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={state.fees.taxRatePercent}
|
||||||
|
onChange={(e) => setState({
|
||||||
|
...state,
|
||||||
|
fees: { ...state.fees, taxRatePercent: Number(e.target.value) }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Links */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<LinkIcon className="h-5 w-5 text-blue-500" />
|
||||||
|
Support Links
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Update external URLs for help, terms, and privacy policies.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Help Center URL</Label>
|
||||||
|
<Input
|
||||||
|
value={state.links.helpCenterUrl}
|
||||||
|
onChange={(e) => setState({
|
||||||
|
...state,
|
||||||
|
links: { ...state.links, helpCenterUrl: e.target.value }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Terms & Conditions URL</Label>
|
||||||
|
<Input
|
||||||
|
value={state.links.termsUrl}
|
||||||
|
onChange={(e) => setState({
|
||||||
|
...state,
|
||||||
|
links: { ...state.links, termsUrl: e.target.value }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Privacy Policy URL</Label>
|
||||||
|
<Input
|
||||||
|
value={state.links.privacyUrl}
|
||||||
|
onChange={(e) => setState({
|
||||||
|
...state,
|
||||||
|
links: { ...state.links, privacyUrl: e.target.value }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end sticky bottom-6 z-10">
|
||||||
|
<Button size="lg" onClick={handleSave} disabled={loading} className="shadow-lg">
|
||||||
|
{loading ? 'Publishing Updates...' : 'Publish Public App Config'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
244
src/features/settings/components/tabs/SystemHealth.tsx
Normal file
244
src/features/settings/components/tabs/SystemHealth.tsx
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { SystemConfig } from '@/lib/types/settings';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
|
import { updateSystemSetting, purgeSystemCache } from '@/lib/actions/settings';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Server, CreditCard, Radio, Trash2, AlertTriangle, Activity } from 'lucide-react';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
|
interface SystemHealthProps {
|
||||||
|
config: SystemConfig;
|
||||||
|
onUpdate: (data: SystemConfig) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SystemHealthTab({ config, onUpdate }: SystemHealthProps) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [state, setState] = useState(config);
|
||||||
|
const [purgeLoading, setPurgeLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await updateSystemSetting('system', state);
|
||||||
|
if (res.success && res.updatedSettings.system) {
|
||||||
|
toast.success('System configuration updated');
|
||||||
|
onUpdate(res.updatedSettings.system);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to update system config');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePurgeCache = async () => {
|
||||||
|
setPurgeLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await purgeSystemCache();
|
||||||
|
if (res.success) {
|
||||||
|
toast.success('Cache purged successfully', {
|
||||||
|
description: 'Changes are propagating to edge locations.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to purge cache');
|
||||||
|
} finally {
|
||||||
|
setPurgeLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
|
||||||
|
{/* System Status Banner */}
|
||||||
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">API Status</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-2.5 w-2.5 rounded-full bg-emerald-500 animate-pulse" />
|
||||||
|
<span className="text-2xl font-bold">Operational</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Uptime: 99.99%</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">Cache Status</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-2xl font-bold">Healthy</span>
|
||||||
|
<Badge variant="outline">TTL: {state.cache.ttl}s</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Last Purged: {state.cache.lastPurgedAt ? new Date(state.cache.lastPurgedAt).toLocaleTimeString() : 'Never'}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">Database</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<span className="text-2xl font-bold">Connected</span>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Latency: 24ms</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Payment Gateways */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<CreditCard className="h-5 w-5 text-primary" />
|
||||||
|
Payment Gateways
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Manage gateway keys and active modes (Test vs Live).
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{/* Stripe */}
|
||||||
|
<div className="p-4 border rounded-lg bg-card/50">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h4 className="font-semibold">Stripe</h4>
|
||||||
|
{state.gateways.stripe.mode === 'live' ?
|
||||||
|
<Badge className="bg-emerald-500">Live</Badge> :
|
||||||
|
<Badge variant="secondary">Test Mode</Badge>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={state.gateways.stripe.enabled}
|
||||||
|
onCheckedChange={(c) => setState({
|
||||||
|
...state,
|
||||||
|
gateways: { ...state.gateways, stripe: { ...state.gateways.stripe, enabled: c } }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>Public Key</Label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={state.gateways.stripe.publicKey}
|
||||||
|
readOnly // For demo safety
|
||||||
|
className="font-mono text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-4">
|
||||||
|
<Label className="text-xs">Mode:</Label>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
checked={state.gateways.stripe.mode === 'live'}
|
||||||
|
onCheckedChange={(c) => setState({
|
||||||
|
...state,
|
||||||
|
gateways: { ...state.gateways, stripe: { ...state.gateways.stripe, mode: c ? 'live' : 'test' } }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<span className={state.gateways.stripe.mode === 'live' ? 'font-bold text-red-600' : 'text-muted-foreground'}>
|
||||||
|
{state.gateways.stripe.mode === 'live' ? 'LIVE TRAFFIC' : 'Test Mode'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Razorpay */}
|
||||||
|
<div className="p-4 border rounded-lg bg-card/50">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h4 className="font-semibold">Razorpay</h4>
|
||||||
|
{state.gateways.razorpay.mode === 'live' ?
|
||||||
|
<Badge className="bg-emerald-500">Live</Badge> :
|
||||||
|
<Badge variant="secondary">Test Mode</Badge>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={state.gateways.razorpay.enabled}
|
||||||
|
onCheckedChange={(c) => setState({
|
||||||
|
...state,
|
||||||
|
gateways: { ...state.gateways, razorpay: { ...state.gateways.razorpay, enabled: c } }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>Key ID</Label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={state.gateways.razorpay.keyId}
|
||||||
|
readOnly
|
||||||
|
className="font-mono text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="justify-end border-t pt-4">
|
||||||
|
<Button onClick={handleSave} disabled={loading}>Save Gateway Config</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Danger Zone */}
|
||||||
|
<h3 className="text-lg font-semibold text-red-600 flex items-center gap-2 mt-8">
|
||||||
|
<AlertTriangle className="h-5 w-5" />
|
||||||
|
Danger Zone
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<Card className="border-red-200 bg-red-50/10">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base text-red-700">Maintenance Mode</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Disable the public facing app temporarily. Only admins can access.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex justify-between items-center">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>Status</Label>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{false ? <span className="text-red-600">Active - App Offline</span> : <span className="text-emerald-600">Inactive - App Live</span>}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="destructive">Enable Maintenance</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-orange-200 bg-orange-50/10">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base text-orange-700">Cache Control</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Force purge the CDN cache for all static pages.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex justify-between items-center">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>Metrics</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">Items cached: ~14.2k</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="border-orange-300 text-orange-700 hover:bg-orange-100"
|
||||||
|
onClick={handlePurgeCache}
|
||||||
|
disabled={purgeLoading}
|
||||||
|
>
|
||||||
|
{purgeLoading ? (
|
||||||
|
<Activity className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
)}
|
||||||
|
Purge Cache
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
79
src/lib/actions/payment-settings.ts
Normal file
79
src/lib/actions/payment-settings.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { GatewayProvider, GatewayCredentials, PaymentConfig, GlobalSettings } from '../types/settings';
|
||||||
|
import { updateSystemSetting, getSystemSettings } from './settings';
|
||||||
|
import { encrypt, decrypt } from '../payment-encryption';
|
||||||
|
|
||||||
|
export async function verifyGatewayCredentials(
|
||||||
|
provider: GatewayProvider,
|
||||||
|
credentials: Partial<GatewayCredentials>
|
||||||
|
): Promise<{ success: boolean; message: string }> {
|
||||||
|
// Simulate API call to provider
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
if (provider === 'payu' && (!credentials.merchantId || !credentials.salt)) {
|
||||||
|
return { success: false, message: 'Invalid PayU credentials: Merchant ID and Salt are required.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === 'razorpay' && !credentials.keyId) {
|
||||||
|
return { success: false, message: 'Invalid Razorpay Key ID.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success simulation
|
||||||
|
return { success: true, message: `Successfully connected to ${provider.toUpperCase()} [${credentials.mode} mode]` };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveGatewayConfig(
|
||||||
|
provider: GatewayProvider,
|
||||||
|
config: GatewayCredentials
|
||||||
|
): Promise<{ success: boolean; message: string }> {
|
||||||
|
|
||||||
|
// Fetch current settings first
|
||||||
|
const currentRes = await getSystemSettings();
|
||||||
|
if (!currentRes.success) return { success: false, message: 'Failed to retrieve current settings' };
|
||||||
|
|
||||||
|
const settings = currentRes.data;
|
||||||
|
|
||||||
|
// Encrypt sensitive fields
|
||||||
|
const encryptedConfig = { ...config };
|
||||||
|
if (config.salt) encryptedConfig.salt = encrypt(config.salt);
|
||||||
|
// Note: We don't encrypt public keys usually, but secrets yes.
|
||||||
|
// For demo simplicity, we encrypt 'salt' and assume others are public/safe or handled similarly.
|
||||||
|
|
||||||
|
// Update specific gateway in payment config
|
||||||
|
const newPaymentConfig: PaymentConfig = {
|
||||||
|
...settings.payment,
|
||||||
|
gateways: {
|
||||||
|
...settings.payment.gateways,
|
||||||
|
[provider]: encryptedConfig
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use our main update action
|
||||||
|
const updateRes = await updateSystemSetting('payment', newPaymentConfig);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: updateRes.success,
|
||||||
|
message: `Saved configuration for ${provider}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateRoutingRules(
|
||||||
|
rules: Pick<PaymentConfig, 'defaultGateway' | 'fallbackGateway' | 'internationalGateway'>
|
||||||
|
): Promise<{ success: boolean; message: string }> {
|
||||||
|
// Fetch current settings first
|
||||||
|
const currentRes = await getSystemSettings();
|
||||||
|
if (!currentRes.success) return { success: false, message: 'Failed to retrieve settings' };
|
||||||
|
|
||||||
|
const settings = currentRes.data;
|
||||||
|
|
||||||
|
const newPaymentConfig: PaymentConfig = {
|
||||||
|
...settings.payment,
|
||||||
|
...rules
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRes = await updateSystemSetting('payment', newPaymentConfig);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: updateRes.success,
|
||||||
|
message: 'Routing logic updated successfully'
|
||||||
|
};
|
||||||
|
}
|
||||||
95
src/lib/actions/settings.ts
Normal file
95
src/lib/actions/settings.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { GlobalSettings, DEFAULT_SETTINGS } from '../types/settings';
|
||||||
|
|
||||||
|
// Simulate a database/store with a singleton object
|
||||||
|
// In a real app, this would be a DB table 'system_config'
|
||||||
|
let MOCK_DB_SETTINGS: GlobalSettings = { ...DEFAULT_SETTINGS };
|
||||||
|
|
||||||
|
// Load from localStorage if available (client-side only trick for persistence in this demo)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const saved = localStorage.getItem('mock_system_settings');
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(saved);
|
||||||
|
MOCK_DB_SETTINGS = { ...DEFAULT_SETTINGS, ...parsed };
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse saved settings", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSystemSettings(): Promise<{ success: boolean; data: GlobalSettings }> {
|
||||||
|
// Simulate network delay
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
// In a real app, we would fetch from DB here
|
||||||
|
// return db.systemConfig.findFirst();
|
||||||
|
|
||||||
|
// For now, return our in-memory/local storage mock
|
||||||
|
// We try to read from localStorage again to sync tabs
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const saved = localStorage.getItem('mock_system_settings');
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(saved);
|
||||||
|
// Deep merge or at least top-level merge to ensure new sections (like 'payment') are added
|
||||||
|
MOCK_DB_SETTINGS = {
|
||||||
|
...DEFAULT_SETTINGS,
|
||||||
|
...parsed,
|
||||||
|
};
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: MOCK_DB_SETTINGS };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSystemSetting<K extends keyof GlobalSettings>(
|
||||||
|
section: K,
|
||||||
|
data: Partial<GlobalSettings[K]>
|
||||||
|
): Promise<{ success: boolean; message: string; updatedSettings: GlobalSettings }> {
|
||||||
|
// Simulate network delay
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
|
||||||
|
// Update the specific section
|
||||||
|
MOCK_DB_SETTINGS = {
|
||||||
|
...MOCK_DB_SETTINGS,
|
||||||
|
[section]: {
|
||||||
|
...MOCK_DB_SETTINGS[section],
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Persist to localStorage for demo
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('mock_system_settings', JSON.stringify(MOCK_DB_SETTINGS));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[AUDIT] System Setting Updated: ${section}`, data);
|
||||||
|
|
||||||
|
// In Next.js we would maintain revalidatePath here
|
||||||
|
// revalidatePath('/settings');
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Settings updated successfully.',
|
||||||
|
updatedSettings: MOCK_DB_SETTINGS
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function purgeSystemCache(): Promise<{ success: boolean; message: string }> {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
console.log(`[AUDIT] System Cache Purged by Admin`);
|
||||||
|
|
||||||
|
// Update last purged time
|
||||||
|
MOCK_DB_SETTINGS.system.cache.lastPurgedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('mock_system_settings', JSON.stringify(MOCK_DB_SETTINGS));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'System cache purged. Changes are now live on Public App.'
|
||||||
|
};
|
||||||
|
}
|
||||||
33
src/lib/payment-encryption.ts
Normal file
33
src/lib/payment-encryption.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// In a real Node.js environment, we would use 'crypto'
|
||||||
|
// import crypto from 'crypto';
|
||||||
|
|
||||||
|
// Since this is a Vite client-side demo, we'll mock the encryption
|
||||||
|
// to simulate the security layer.
|
||||||
|
|
||||||
|
export function encrypt(text: string): string {
|
||||||
|
if (!text) return '';
|
||||||
|
try {
|
||||||
|
// Simple base64 encoding simulation + prefix
|
||||||
|
return `enc_${btoa(text)}`;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Encryption failed", e);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrypt(text: string): string {
|
||||||
|
if (!text) return '';
|
||||||
|
if (!text.startsWith('enc_')) return text; // Return as-is if not encrypted
|
||||||
|
try {
|
||||||
|
const payload = text.replace('enc_', '');
|
||||||
|
return atob(payload);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Decryption failed", e);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function maskKey(key: string): string {
|
||||||
|
if (!key || key.length < 8) return '********';
|
||||||
|
return `******${key.slice(-4)}`;
|
||||||
|
}
|
||||||
173
src/lib/types/settings.ts
Normal file
173
src/lib/types/settings.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
export interface OrganizationConfig {
|
||||||
|
brandName: string;
|
||||||
|
supportEmail: string;
|
||||||
|
legalAddress: string;
|
||||||
|
logoUrl?: string; // For emails
|
||||||
|
socialLinks: {
|
||||||
|
twitter?: string;
|
||||||
|
linkedin?: string;
|
||||||
|
instagram?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityConfig {
|
||||||
|
enforce2FA: boolean;
|
||||||
|
sessionTimeoutMinutes: number;
|
||||||
|
passwordExpirationDays: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublicAppConfig {
|
||||||
|
maintenanceMode: boolean;
|
||||||
|
betaFeatures: {
|
||||||
|
cryptoPayments: boolean;
|
||||||
|
aiRecommendations: boolean;
|
||||||
|
socialLogin: boolean;
|
||||||
|
};
|
||||||
|
fees: {
|
||||||
|
platformFeeFlat: number; // e.g. 20 (INR)
|
||||||
|
taxRatePercent: number; // e.g. 18 (GST)
|
||||||
|
};
|
||||||
|
banners: {
|
||||||
|
id: string;
|
||||||
|
imageUrl: string;
|
||||||
|
linkUrl: string;
|
||||||
|
active: boolean;
|
||||||
|
}[];
|
||||||
|
links: {
|
||||||
|
helpCenterUrl: string;
|
||||||
|
termsUrl: string;
|
||||||
|
privacyUrl: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PartnerConfig {
|
||||||
|
requireKyc: boolean;
|
||||||
|
manualEventApproval: boolean;
|
||||||
|
defaultCommissionPercent: number;
|
||||||
|
payoutSchedule: 'daily' | 'weekly' | 'monthly' | 'manual';
|
||||||
|
minPayoutAmount: number;
|
||||||
|
allowedKycDocs: ('pan' | 'gst' | 'aadhaar' | 'cheque')[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GatewayProvider = 'razorpay' | 'stripe' | 'payu' | 'easebuzz' | 'worldline';
|
||||||
|
|
||||||
|
export interface GatewayCredentials {
|
||||||
|
enabled: boolean;
|
||||||
|
mode: 'test' | 'live';
|
||||||
|
merchantId?: string;
|
||||||
|
publicKey?: string; // For Stripe
|
||||||
|
keyId?: string; // For Razorpay
|
||||||
|
salt?: string; // For PayU/Easebuzz
|
||||||
|
webhookSecret?: string;
|
||||||
|
features: {
|
||||||
|
netbanking: boolean;
|
||||||
|
upi: boolean;
|
||||||
|
cards: boolean;
|
||||||
|
emi: boolean;
|
||||||
|
wallet: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentConfig {
|
||||||
|
defaultGateway: GatewayProvider;
|
||||||
|
fallbackGateway: GatewayProvider;
|
||||||
|
internationalGateway: GatewayProvider;
|
||||||
|
gateways: Record<GatewayProvider, GatewayCredentials>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GlobalSettings {
|
||||||
|
organization: OrganizationConfig;
|
||||||
|
security: SecurityConfig;
|
||||||
|
publicApp: PublicAppConfig;
|
||||||
|
partner: PartnerConfig;
|
||||||
|
system: SystemConfig;
|
||||||
|
payment: PaymentConfig; // New section
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SETTINGS: GlobalSettings = {
|
||||||
|
organization: {
|
||||||
|
brandName: 'Eventify',
|
||||||
|
supportEmail: 'support@eventify.com',
|
||||||
|
legalAddress: '123 Tech Park, Bangalore, India',
|
||||||
|
socialLinks: {}
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
enforce2FA: false,
|
||||||
|
sessionTimeoutMinutes: 60,
|
||||||
|
passwordExpirationDays: 90
|
||||||
|
},
|
||||||
|
publicApp: {
|
||||||
|
maintenanceMode: false,
|
||||||
|
betaFeatures: {
|
||||||
|
cryptoPayments: false,
|
||||||
|
aiRecommendations: true,
|
||||||
|
socialLogin: true
|
||||||
|
},
|
||||||
|
fees: {
|
||||||
|
platformFeeFlat: 20,
|
||||||
|
taxRatePercent: 18
|
||||||
|
},
|
||||||
|
banners: [],
|
||||||
|
links: {
|
||||||
|
helpCenterUrl: 'https://help.eventify.com',
|
||||||
|
termsUrl: 'https://eventify.com/terms',
|
||||||
|
privacyUrl: 'https://eventify.com/privacy'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
partner: {
|
||||||
|
requireKyc: true,
|
||||||
|
manualEventApproval: false,
|
||||||
|
defaultCommissionPercent: 5,
|
||||||
|
payoutSchedule: 'weekly',
|
||||||
|
minPayoutAmount: 1000,
|
||||||
|
allowedKycDocs: ['pan', 'gst', 'cheque']
|
||||||
|
},
|
||||||
|
system: {
|
||||||
|
gateways: { // Legacy structure, will migrate to 'payment'
|
||||||
|
stripe: { enabled: true, mode: 'test', publicKey: 'pk_test_...' },
|
||||||
|
razorpay: { enabled: true, mode: 'test', keyId: 'rzp_test_...' }
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
ttl: 3600
|
||||||
|
}
|
||||||
|
},
|
||||||
|
payment: {
|
||||||
|
defaultGateway: 'razorpay',
|
||||||
|
fallbackGateway: 'stripe',
|
||||||
|
internationalGateway: 'stripe',
|
||||||
|
gateways: {
|
||||||
|
razorpay: {
|
||||||
|
enabled: true,
|
||||||
|
mode: 'test',
|
||||||
|
keyId: 'rzp_test_123456',
|
||||||
|
features: { netbanking: true, upi: true, cards: true, emi: false, wallet: true }
|
||||||
|
},
|
||||||
|
stripe: {
|
||||||
|
enabled: true,
|
||||||
|
mode: 'test',
|
||||||
|
publicKey: 'pk_test_123456',
|
||||||
|
features: { netbanking: false, upi: false, cards: true, emi: false, wallet: false }
|
||||||
|
},
|
||||||
|
payu: {
|
||||||
|
enabled: false,
|
||||||
|
mode: 'test',
|
||||||
|
merchantId: '',
|
||||||
|
salt: '',
|
||||||
|
features: { netbanking: true, upi: true, cards: true, emi: true, wallet: true }
|
||||||
|
},
|
||||||
|
easebuzz: {
|
||||||
|
enabled: false,
|
||||||
|
mode: 'test',
|
||||||
|
merchantId: '',
|
||||||
|
salt: '',
|
||||||
|
features: { netbanking: true, upi: true, cards: true, emi: true, wallet: true }
|
||||||
|
},
|
||||||
|
worldline: {
|
||||||
|
enabled: false,
|
||||||
|
mode: 'test',
|
||||||
|
merchantId: '',
|
||||||
|
features: { netbanking: true, upi: true, cards: true, emi: false, wallet: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,32 +1,10 @@
|
|||||||
import { AppLayout } from '@/components/layout/AppLayout';
|
import { AppLayout } from '@/components/layout/AppLayout';
|
||||||
import { OrganizationProfileCard } from '@/features/settings/components/OrganizationProfileCard';
|
import { SettingsLayout } from '@/features/settings/components/SettingsLayout';
|
||||||
import { PayoutBankingCard } from '@/features/settings/components/PayoutBankingCard';
|
|
||||||
import { TeamSecurityCard } from '@/features/settings/components/TeamSecurityCard';
|
|
||||||
import { DeveloperSection } from '@/features/settings/components/DeveloperSection';
|
|
||||||
import { PaymentGatewayCard } from '@/features/settings/components/PaymentGatewayCard';
|
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
return (
|
return (
|
||||||
<AppLayout
|
<AppLayout>
|
||||||
title="Settings"
|
<SettingsLayout />
|
||||||
description="Manage platform configuration and critical operations."
|
|
||||||
>
|
|
||||||
<div className="space-y-6 max-w-[1200px]">
|
|
||||||
{/* Top Grid: Identity & Banking */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-auto lg:h-[400px]">
|
|
||||||
<OrganizationProfileCard />
|
|
||||||
<PayoutBankingCard />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Middle Grid: Gateways & Security */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-auto">
|
|
||||||
<PaymentGatewayCard />
|
|
||||||
<TeamSecurityCard />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Collapsible Developer Section */}
|
|
||||||
<DeveloperSection />
|
|
||||||
</div>
|
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user