)}
{financialSummary.pendingPayments > 0 && (
-
-
+
+
⏳ {financialSummary.pendingPayments} pending payment{financialSummary.pendingPayments !== 1 ? 's' : ''} awaiting processing
diff --git a/Frontend/src/pages/admin/APIKeyManagementPage.tsx b/Frontend/src/pages/admin/APIKeyManagementPage.tsx
new file mode 100644
index 00000000..08f05684
--- /dev/null
+++ b/Frontend/src/pages/admin/APIKeyManagementPage.tsx
@@ -0,0 +1,308 @@
+import React, { useEffect, useState } from 'react';
+import { Plus, Trash2, Copy, Eye, EyeOff, Edit } from 'lucide-react';
+import apiKeyService, { APIKey, CreateAPIKeyData, UpdateAPIKeyData } from '../../features/integrations/services/apiKeyService';
+import { toast } from 'react-toastify';
+import Loading from '../../shared/components/Loading';
+import { formatDate } from '../../shared/utils/format';
+
+const APIKeyManagementPage: React.FC = () => {
+ const [apiKeys, setApiKeys] = useState
([]);
+ const [loading, setLoading] = useState(true);
+ const [showModal, setShowModal] = useState(false);
+ const [editingKey, setEditingKey] = useState(null);
+ const [newKey, setNewKey] = useState(null);
+ const [formData, setFormData] = useState({
+ name: '',
+ scopes: [],
+ description: '',
+ rate_limit: 100,
+ });
+
+ const availableScopes = [
+ 'read:bookings',
+ 'write:bookings',
+ 'read:payments',
+ 'write:payments',
+ 'read:invoices',
+ 'write:invoices',
+ 'read:users',
+ 'write:users',
+ ];
+
+ useEffect(() => {
+ fetchAPIKeys();
+ }, []);
+
+ const fetchAPIKeys = async () => {
+ try {
+ setLoading(true);
+ const response = await apiKeyService.getAPIKeys();
+ setApiKeys(response.data.api_keys || []);
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load API keys');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ try {
+ if (editingKey) {
+ await apiKeyService.updateAPIKey(editingKey.id, formData as UpdateAPIKeyData);
+ toast.success('API key updated successfully');
+ setShowModal(false);
+ setEditingKey(null);
+ resetForm();
+ fetchAPIKeys();
+ } else {
+ const response = await apiKeyService.createAPIKey(formData);
+ toast.success('API key created successfully');
+ setNewKey(response.data.key);
+ setShowModal(false);
+ resetForm();
+ fetchAPIKeys();
+ }
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || `Unable to ${editingKey ? 'update' : 'create'} API key`);
+ }
+ };
+
+ const handleEdit = (key: APIKey) => {
+ setEditingKey(key);
+ setFormData({
+ name: key.name,
+ scopes: key.scopes,
+ description: key.description || '',
+ rate_limit: key.rate_limit,
+ expires_at: key.expires_at || undefined,
+ });
+ setShowModal(true);
+ };
+
+ const handleRevoke = async (id: number) => {
+ if (!confirm('Are you sure you want to revoke this API key?')) return;
+
+ try {
+ await apiKeyService.revokeAPIKey(id);
+ toast.success('API key revoked');
+ fetchAPIKeys();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to revoke API key');
+ }
+ };
+
+ const copyToClipboard = (text: string) => {
+ navigator.clipboard.writeText(text);
+ toast.success('Copied to clipboard');
+ };
+
+ const resetForm = () => {
+ setFormData({
+ name: '',
+ scopes: [],
+ description: '',
+ rate_limit: 100,
+ });
+ setEditingKey(null);
+ };
+
+ const toggleScope = (scope: string) => {
+ setFormData({
+ ...formData,
+ scopes: formData.scopes.includes(scope)
+ ? formData.scopes.filter(s => s !== scope)
+ : [...formData.scopes, scope],
+ });
+ };
+
+ if (loading && apiKeys.length === 0) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+ API Key Management
+
+
+
Manage API keys for third-party access
+
+
setShowModal(true)}
+ className="w-full sm:w-auto px-4 sm:px-6 py-2 sm:py-2.5 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-xl font-semibold hover:from-blue-700 hover:to-blue-800 shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center gap-2 text-xs sm:text-sm"
+ >
+
+ Create API Key
+
+
+
+ {newKey && (
+
+
API Key Created (save this - it won't be shown again):
+
+
{newKey}
+
+ copyToClipboard(newKey)}
+ className="p-2 sm:p-2.5 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg sm:rounded-xl hover:from-blue-700 hover:to-blue-800 shadow-md hover:shadow-lg transition-all duration-200"
+ >
+
+
+ setNewKey(null)}
+ className="p-2 sm:p-2.5 bg-slate-200 hover:bg-slate-300 rounded-lg sm:rounded-xl transition-colors"
+ >
+ ✕
+
+
+
+
+ )}
+
+
+ {apiKeys.length === 0 ? (
+
+
No API keys configured
+
+ ) : (
+
+ {apiKeys.map((key) => (
+
+
+
+
+
{key.name}
+
+ {key.key_prefix}...
+
+
+ {key.is_active ? 'Active' : 'Revoked'}
+
+
+
+ Scopes: {key.scopes.join(', ')}
+
+
+ Rate Limit: {key.rate_limit}/min • Created: {formatDate(key.created_at)}
+ {key.last_used_at && ` • Last Used: ${formatDate(key.last_used_at)}`}
+
+
+
+ {key.is_active && (
+ <>
+ handleEdit(key)}
+ className="p-2 sm:p-2.5 text-blue-600 hover:bg-blue-50 rounded-lg sm:rounded-xl transition-colors flex-shrink-0"
+ title="Edit API key"
+ >
+
+
+ handleRevoke(key.id)}
+ className="px-3 sm:px-4 py-2 bg-gradient-to-r from-red-600 to-red-700 text-white rounded-lg sm:rounded-xl hover:from-red-700 hover:to-red-800 shadow-lg hover:shadow-xl transition-all duration-200 text-xs sm:text-sm font-semibold flex items-center justify-center gap-2"
+ >
+
+ Revoke
+
+ >
+ )}
+
+
+
+ ))}
+
+ )}
+
+
+ {showModal && (
+
+
+
+ {editingKey ? 'Edit API Key' : 'Create API Key'}
+
+
+
+
+ )}
+
+ );
+};
+
+export default APIKeyManagementPage;
+
diff --git a/Frontend/src/pages/admin/ApprovalManagementPage.tsx b/Frontend/src/pages/admin/ApprovalManagementPage.tsx
new file mode 100644
index 00000000..e775190d
--- /dev/null
+++ b/Frontend/src/pages/admin/ApprovalManagementPage.tsx
@@ -0,0 +1,264 @@
+import React, { useEffect, useState } from 'react';
+import { CheckCircle, XCircle, Clock, Eye, Filter, Trash2 } from 'lucide-react';
+import approvalService, { ApprovalRequest } from '../../features/system/services/approvalService';
+import { toast } from 'react-toastify';
+import Loading from '../../shared/components/Loading';
+import Pagination from '../../shared/components/Pagination';
+import { formatDate } from '../../shared/utils/format';
+
+const ApprovalManagementPage: React.FC = () => {
+ const [approvals, setApprovals] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [selectedApproval, setSelectedApproval] = useState(null);
+ const [showDetails, setShowDetails] = useState(false);
+ const [processingId, setProcessingId] = useState(null);
+ const [filters, setFilters] = useState({
+ approval_type: '',
+ });
+ const [currentPage, setCurrentPage] = useState(1);
+ const [totalPages, setTotalPages] = useState(1);
+ const itemsPerPage = 20;
+
+ useEffect(() => {
+ fetchApprovals();
+ }, [filters, currentPage]);
+
+ const fetchApprovals = async () => {
+ try {
+ setLoading(true);
+ const response = await approvalService.getPendingApprovals({
+ ...filters,
+ page: currentPage,
+ limit: itemsPerPage,
+ });
+ setApprovals(response.data.approvals || []);
+ if (response.data.pagination) {
+ setTotalPages(response.data.pagination.totalPages || 1);
+ }
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load approvals');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleApprove = async (id: number) => {
+ try {
+ setProcessingId(id);
+ await approvalService.approveRequest(id);
+ toast.success('Approval request approved');
+ fetchApprovals();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to approve request');
+ } finally {
+ setProcessingId(null);
+ }
+ };
+
+ const handleReject = async (id: number) => {
+ const reason = prompt('Enter rejection reason:');
+ if (!reason) return;
+
+ try {
+ setProcessingId(id);
+ await approvalService.rejectRequest(id, reason);
+ toast.success('Approval request rejected');
+ fetchApprovals();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to reject request');
+ } finally {
+ setProcessingId(null);
+ }
+ };
+
+ const handleCancel = async (id: number) => {
+ if (!confirm('Are you sure you want to cancel this approval request?')) return;
+
+ try {
+ setProcessingId(id);
+ await approvalService.cancelRequest(id);
+ toast.success('Approval request cancelled');
+ fetchApprovals();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to cancel request');
+ } finally {
+ setProcessingId(null);
+ }
+ };
+
+ const getStatusIcon = (status: string) => {
+ switch (status) {
+ case 'approved':
+ return ;
+ case 'rejected':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ if (loading && approvals.length === 0) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+ Approval Management
+
+
+
Review and manage pending approval requests
+
+
+
+
+
+
+ {
+ setFilters({ ...filters, approval_type: e.target.value });
+ setCurrentPage(1);
+ }}
+ className="w-full px-3 sm:px-4 py-2 sm:py-2.5 bg-white border-2 border-slate-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 text-slate-700 font-medium shadow-sm hover:shadow-md text-sm sm:text-base"
+ >
+ All Types
+ Invoice Update
+ Payment Refund
+ Invoice Mark Paid
+ Financial Adjustment
+
+
+
+
+ {approvals.length === 0 ? (
+
+ ) : (
+
+ {approvals.map((approval) => (
+
+
+
+
+ {getStatusIcon(approval.status)}
+
+ {approval.approval_type.replace('_', ' ').toUpperCase()}
+
+
+ {approval.priority}
+
+
+
+ Resource: {approval.resource_type} #{approval.resource_id}
+
+
+ Requested: {formatDate(approval.requested_at)}
+
+ {approval.notes && (
+
{approval.notes}
+ )}
+
+
+ {
+ setSelectedApproval(approval);
+ setShowDetails(true);
+ }}
+ className="p-2 sm:p-2.5 text-blue-600 hover:bg-blue-50 rounded-lg sm:rounded-xl transition-colors flex-shrink-0"
+ >
+
+
+ {approval.status === 'pending' && (
+ <>
+ handleApprove(approval.id)}
+ disabled={processingId === approval.id}
+ className="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-gradient-to-r from-emerald-600 to-emerald-700 text-white rounded-lg sm:rounded-xl hover:from-emerald-700 hover:to-emerald-800 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 text-xs sm:text-sm font-semibold"
+ >
+ Approve
+
+ handleReject(approval.id)}
+ disabled={processingId === approval.id}
+ className="flex-1 sm:flex-none px-3 sm:px-4 py-2 bg-gradient-to-r from-red-600 to-red-700 text-white rounded-lg sm:rounded-xl hover:from-red-700 hover:to-red-800 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 text-xs sm:text-sm font-semibold"
+ >
+ Reject
+
+ >
+ )}
+
+
+
+ ))}
+
+ )}
+
+ {totalPages > 1 && (
+
+ )}
+
+
+ {showDetails && selectedApproval && (
+
+
+
+
Approval Details
+ setShowDetails(false)}
+ className="text-slate-500 hover:text-slate-700 p-2 hover:bg-slate-100 rounded-lg transition-colors"
+ >
+ ✕
+
+
+
+
+
Type:
+
{selectedApproval.approval_type}
+
+
+
Status:
+
{selectedApproval.status}
+
+
+
Request Data:
+
+ {JSON.stringify(selectedApproval.request_data, null, 2)}
+
+
+ {selectedApproval.current_data && (
+
+
Current Data:
+
+ {JSON.stringify(selectedApproval.current_data, null, 2)}
+
+
+ )}
+
+
+
+ )}
+
+ );
+};
+
+export default ApprovalManagementPage;
+
diff --git a/Frontend/src/pages/admin/BackupManagementPage.tsx b/Frontend/src/pages/admin/BackupManagementPage.tsx
new file mode 100644
index 00000000..29be9fa1
--- /dev/null
+++ b/Frontend/src/pages/admin/BackupManagementPage.tsx
@@ -0,0 +1,201 @@
+import React, { useEffect, useState } from 'react';
+import { Download, Trash2, Plus, HardDrive, AlertTriangle } from 'lucide-react';
+import backupService, { Backup } from '../../features/system/services/backupService';
+import { toast } from 'react-toastify';
+import Loading from '../../shared/components/Loading';
+import { formatDate } from '../../shared/utils/format';
+
+const BackupManagementPage: React.FC = () => {
+ const [backups, setBackups] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [creating, setCreating] = useState(false);
+ const [backupAvailable, setBackupAvailable] = useState(null);
+
+ useEffect(() => {
+ fetchBackups();
+ checkBackupStatus();
+ }, []);
+
+ const checkBackupStatus = async () => {
+ try {
+ const response = await backupService.checkStatus();
+ setBackupAvailable(response.data.available);
+ if (!response.data.available) {
+ toast.warning(response.data.message || 'Backup service is not available', { autoClose: 8000 });
+ }
+ } catch (error: any) {
+ console.error('Error checking backup status:', error);
+ setBackupAvailable(false);
+ }
+ };
+
+ const fetchBackups = async () => {
+ try {
+ setLoading(true);
+ const response = await backupService.listBackups();
+ setBackups(response.data.backups || []);
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load backups');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleCreateBackup = async () => {
+ if (!confirm('Create a new database backup?')) return;
+
+ try {
+ setCreating(true);
+ await backupService.createBackup();
+ toast.success('Backup created successfully');
+ fetchBackups();
+ } catch (error: any) {
+ const errorDetail = error.response?.data?.detail;
+ if (error.response?.status === 503 && errorDetail?.requires_installation) {
+ // Show a more detailed error for missing mysqldump
+ toast.error(
+
+
Backup service unavailable
+
{errorDetail.message || errorDetail}
+
,
+ { autoClose: 10000 }
+ );
+ } else {
+ toast.error(error.response?.data?.detail?.message || error.response?.data?.detail || error.response?.data?.message || 'Unable to create backup');
+ }
+ } finally {
+ setCreating(false);
+ }
+ };
+
+ const handleDownload = async (filename: string) => {
+ try {
+ const blob = await backupService.downloadBackup(filename);
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+ toast.success('Backup downloaded');
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to download backup');
+ }
+ };
+
+ const handleCleanup = async () => {
+ if (!confirm('Clean up old backups beyond retention period?')) return;
+
+ try {
+ const response = await backupService.cleanupOldBackups();
+ toast.success(`Removed ${response.data.removed_count} old backup(s)`);
+ fetchBackups();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to cleanup backups');
+ }
+ };
+
+ if (loading && backups.length === 0) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+ Backup Management
+
+
+
Manage database backups
+
+
+
+ Cleanup Old
+
+
+
+ {creating ? 'Creating...' : 'Create Backup'}
+
+
+
+
+ {/* Warning Banner */}
+ {backupAvailable === false && (
+
+
+
+
+
Backup Service Unavailable
+
+ mysqldump is not installed. Please install MySQL client tools to enable backups:
+
+
+
+ Ubuntu/Debian: sudo apt-get install mysql-client
+ CentOS/RHEL: sudo yum install mysql
+ macOS: brew install mysql-client
+
+
+
+
+
+ )}
+
+
+ {backups.length === 0 ? (
+
+
+
No backups available
+
+ ) : (
+
+ {backups.map((backup) => (
+
+
+
+
+
+
{backup.filename}
+
+ Size: {backup.size_mb} MB • Database: {backup.database}
+
+
+ Created: {formatDate(backup.created_at)}
+
+
+
+
handleDownload(backup.filename)}
+ className="w-full sm:w-auto px-3 sm:px-4 py-2 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg sm:rounded-xl hover:from-blue-700 hover:to-blue-800 shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center gap-2 text-xs sm:text-sm font-semibold"
+ >
+
+ Download
+
+
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+export default BackupManagementPage;
+
diff --git a/Frontend/src/pages/admin/GDPRManagementPage.tsx b/Frontend/src/pages/admin/GDPRManagementPage.tsx
new file mode 100644
index 00000000..6cdf78ce
--- /dev/null
+++ b/Frontend/src/pages/admin/GDPRManagementPage.tsx
@@ -0,0 +1,147 @@
+import React, { useEffect, useState } from 'react';
+import { Download, Trash2, FileText, Eye } from 'lucide-react';
+import gdprService, { GDPRRequest } from '../../features/compliance/services/gdprService';
+import { toast } from 'react-toastify';
+import Loading from '../../shared/components/Loading';
+import { formatDate } from '../../shared/utils/format';
+
+const GDPRManagementPage: React.FC = () => {
+ const [requests, setRequests] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [selectedRequest, setSelectedRequest] = useState(null);
+
+ useEffect(() => {
+ fetchRequests();
+ }, []);
+
+ const fetchRequests = async () => {
+ try {
+ setLoading(true);
+ const response = await gdprService.getAllRequests();
+ setRequests(response.data.requests || []);
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load GDPR requests');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDownloadExport = async (request: GDPRRequest) => {
+ if (!request.verification_token) {
+ toast.error('Verification token not available');
+ return;
+ }
+
+ try {
+ const blob = await gdprService.downloadExport(request.id, request.verification_token);
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `user_data_export_${request.id}.json`;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+ toast.success('Export downloaded');
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to download export');
+ }
+ };
+
+ const handleDelete = async (id: number) => {
+ if (!confirm('Are you sure you want to delete this GDPR request?')) return;
+
+ try {
+ await gdprService.deleteRequest(id);
+ toast.success('GDPR request deleted successfully');
+ fetchRequests();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to delete GDPR request');
+ }
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+ GDPR Compliance
+
+
+
Manage data export and deletion requests
+
+
+
+ {requests.length === 0 ? (
+
+ ) : (
+
+ {requests.map((request) => (
+
+
+
+
+ {request.request_type === 'data_export' ? (
+
+ ) : (
+
+ )}
+
+ {request.request_type.replace('_', ' ').toUpperCase()}
+
+
+ {request.status}
+
+
+
+ User: {request.user_email}
+
+
+ Created: {formatDate(request.created_at)}
+ {request.processed_at && ` • Processed: ${formatDate(request.processed_at)}`}
+
+
+
+ {request.request_type === 'data_export' && request.status === 'completed' && (
+ handleDownloadExport(request)}
+ className="px-3 sm:px-4 py-2 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg sm:rounded-xl hover:from-blue-700 hover:to-blue-800 shadow-lg hover:shadow-xl transition-all duration-200 text-xs sm:text-sm font-semibold flex items-center justify-center gap-2"
+ >
+
+ Download
+
+ )}
+ handleDelete(request.id)}
+ className="p-2 sm:p-2.5 text-red-600 hover:bg-red-50 rounded-lg sm:rounded-xl transition-colors flex-shrink-0"
+ title="Delete request"
+ >
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+export default GDPRManagementPage;
+
diff --git a/Frontend/src/pages/admin/WebhookManagementPage.tsx b/Frontend/src/pages/admin/WebhookManagementPage.tsx
new file mode 100644
index 00000000..7dad4729
--- /dev/null
+++ b/Frontend/src/pages/admin/WebhookManagementPage.tsx
@@ -0,0 +1,279 @@
+import React, { useEffect, useState } from 'react';
+import { Plus, Trash2, Eye, Activity, Edit } from 'lucide-react';
+import webhookService, { Webhook, CreateWebhookData, UpdateWebhookData } from '../../features/integrations/services/webhookService';
+import { toast } from 'react-toastify';
+import Loading from '../../shared/components/Loading';
+import { formatDate } from '../../shared/utils/format';
+
+const WebhookManagementPage: React.FC = () => {
+ const [webhooks, setWebhooks] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [showModal, setShowModal] = useState(false);
+ const [editingWebhook, setEditingWebhook] = useState(null);
+ const [selectedWebhook, setSelectedWebhook] = useState(null);
+ const [formData, setFormData] = useState({
+ name: '',
+ url: '',
+ events: [],
+ description: '',
+ retry_count: 3,
+ timeout_seconds: 30,
+ });
+
+ const availableEvents = [
+ 'booking.created',
+ 'booking.updated',
+ 'booking.cancelled',
+ 'payment.completed',
+ 'payment.failed',
+ 'invoice.created',
+ 'invoice.paid',
+ 'user.created',
+ 'user.updated',
+ ];
+
+ useEffect(() => {
+ fetchWebhooks();
+ }, []);
+
+ const fetchWebhooks = async () => {
+ try {
+ setLoading(true);
+ const response = await webhookService.getWebhooks();
+ setWebhooks(response.data.webhooks || []);
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load webhooks');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ try {
+ if (editingWebhook) {
+ await webhookService.updateWebhook(editingWebhook.id, formData as UpdateWebhookData);
+ toast.success('Webhook updated successfully');
+ } else {
+ const response = await webhookService.createWebhook(formData);
+ toast.success('Webhook created successfully');
+ // Show secret to user (only shown once)
+ if (response.data.webhook.secret) {
+ alert(`Webhook secret (save this - it won't be shown again): ${response.data.webhook.secret}`);
+ }
+ }
+ setShowModal(false);
+ setEditingWebhook(null);
+ resetForm();
+ fetchWebhooks();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || `Unable to ${editingWebhook ? 'update' : 'create'} webhook`);
+ }
+ };
+
+ const handleEdit = (webhook: Webhook) => {
+ setEditingWebhook(webhook);
+ setFormData({
+ name: webhook.name,
+ url: webhook.url,
+ events: webhook.events,
+ description: webhook.description || '',
+ retry_count: webhook.retry_count,
+ timeout_seconds: webhook.timeout_seconds,
+ });
+ setShowModal(true);
+ };
+
+ const handleDelete = async (id: number) => {
+ if (!confirm('Are you sure you want to delete this webhook?')) return;
+
+ try {
+ await webhookService.deleteWebhook(id);
+ toast.success('Webhook deleted successfully');
+ fetchWebhooks();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to delete webhook');
+ }
+ };
+
+ const resetForm = () => {
+ setFormData({
+ name: '',
+ url: '',
+ events: [],
+ description: '',
+ retry_count: 3,
+ timeout_seconds: 30,
+ });
+ setEditingWebhook(null);
+ };
+
+ const toggleEvent = (event: string) => {
+ setFormData({
+ ...formData,
+ events: formData.events.includes(event)
+ ? formData.events.filter(e => e !== event)
+ : [...formData.events, event],
+ });
+ };
+
+ if (loading && webhooks.length === 0) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+ Webhook Management
+
+
+
Manage webhooks for external integrations
+
+
setShowModal(true)}
+ className="w-full sm:w-auto px-4 sm:px-6 py-2 sm:py-2.5 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-xl font-semibold hover:from-blue-700 hover:to-blue-800 shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center gap-2 text-xs sm:text-sm"
+ >
+
+ Create Webhook
+
+
+
+
+ {webhooks.length === 0 ? (
+
+
No webhooks configured
+
+ ) : (
+
+ {webhooks.map((webhook) => (
+
+
+
+
+
+
{webhook.name}
+
+ {webhook.status}
+
+
+
{webhook.url}
+
+ Events: {webhook.events.join(', ')}
+
+
+ Created: {formatDate(webhook.created_at)}
+
+
+
+ handleEdit(webhook)}
+ className="p-2 sm:p-2.5 text-blue-600 hover:bg-blue-50 rounded-lg sm:rounded-xl transition-colors flex-shrink-0"
+ title="Edit webhook"
+ >
+
+
+ handleDelete(webhook.id)}
+ className="p-2 sm:p-2.5 text-red-600 hover:bg-red-50 rounded-lg sm:rounded-xl transition-colors flex-shrink-0"
+ title="Delete webhook"
+ >
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ {showModal && (
+
+
+
+ {editingWebhook ? 'Edit Webhook' : 'Create Webhook'}
+
+
+
+ Name
+ setFormData({ ...formData, name: e.target.value })}
+ className="w-full px-3 sm:px-4 py-2 bg-white border-2 border-slate-200 rounded-xl focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all duration-200 text-slate-700 font-medium shadow-sm hover:shadow-md text-sm sm:text-base"
+ required
+ />
+
+
+ URL
+ setFormData({ ...formData, url: e.target.value })}
+ className="w-full px-3 sm:px-4 py-2 bg-white border-2 border-slate-200 rounded-xl focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all duration-200 text-slate-700 font-medium shadow-sm hover:shadow-md text-sm sm:text-base"
+ required
+ />
+
+
+
Events
+
+ {availableEvents.map((event) => (
+
+ toggleEvent(event)}
+ className="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500"
+ />
+ {event}
+
+ ))}
+
+
+
+ Description
+ setFormData({ ...formData, description: e.target.value })}
+ className="w-full px-3 sm:px-4 py-2 bg-white border-2 border-slate-200 rounded-xl focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all duration-200 text-slate-700 font-medium shadow-sm hover:shadow-md text-sm sm:text-base"
+ rows={3}
+ />
+
+
+ {
+ setShowModal(false);
+ resetForm();
+ }}
+ className="flex-1 sm:flex-none px-4 py-2 border-2 border-slate-300 rounded-xl hover:bg-slate-50 transition-all duration-200 text-slate-700 font-medium text-sm sm:text-base"
+ >
+ Cancel
+
+
+ {editingWebhook ? 'Update' : 'Create'}
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default WebhookManagementPage;
+
diff --git a/Frontend/src/pages/customer/DashboardPage.tsx b/Frontend/src/pages/customer/DashboardPage.tsx
index 8edabc22..864a35a3 100644
--- a/Frontend/src/pages/customer/DashboardPage.tsx
+++ b/Frontend/src/pages/customer/DashboardPage.tsx
@@ -64,15 +64,15 @@ const DashboardPage: React.FC = () => {
const getPaymentStatusColor = (status: string) => {
switch (status) {
case 'completed':
- return 'bg-green-100 text-green-800';
+ return 'bg-gradient-to-r from-emerald-50 to-green-50 text-emerald-800 border-emerald-200';
case 'pending':
- return 'bg-yellow-100 text-yellow-800';
+ return 'bg-gradient-to-r from-amber-50 to-yellow-50 text-amber-800 border-amber-200';
case 'failed':
- return 'bg-red-100 text-red-800';
+ return 'bg-gradient-to-r from-rose-50 to-red-50 text-rose-800 border-rose-200';
case 'refunded':
- return 'bg-gray-100 text-gray-800';
+ return 'bg-gradient-to-r from-slate-50 to-gray-50 text-slate-700 border-slate-200';
default:
- return 'bg-gray-100 text-gray-800';
+ return 'bg-gradient-to-r from-slate-50 to-gray-50 text-slate-700 border-slate-200';
}
};
@@ -98,7 +98,7 @@ const DashboardPage: React.FC = () => {
if (error || !stats) {
return (
-
+
{
}
return (
-
-
-
- Dashboard
-
-
+
+
+
+
Overview of your activity and bookings
{}
-
+
{}
-
-
-
-
+
+
+
+
{stats.booking_change_percentage !== 0 && (
-
0 ? 'text-green-600' : 'text-red-600'
+ 0
+ ? 'text-emerald-700 bg-emerald-50 border border-emerald-200'
+ : 'text-rose-700 bg-rose-50 border border-rose-200'
}`}>
{stats.booking_change_percentage > 0 ? (
-
+
) : (
-
+
)}
{Math.abs(stats.booking_change_percentage).toFixed(1)}%
)}
-
+
Total Bookings
-
+
{stats.total_bookings}
{}
-
-
-
-
+
+
+
+
{stats.spending_change_percentage !== 0 && (
-
0 ? 'text-green-600' : 'text-red-600'
+ 0
+ ? 'text-emerald-700 bg-emerald-50 border border-emerald-200'
+ : 'text-rose-700 bg-rose-50 border border-rose-200'
}`}>
{stats.spending_change_percentage > 0 ? (
-
+
) : (
-
+
)}
{Math.abs(stats.spending_change_percentage).toFixed(1)}%
)}
-
+
Total Spending
-
+
{formatCurrency(stats.total_spending)}
{}
-
-
-
-
+
+
+
+
{stats.currently_staying > 0 && (
-
+
Active
)}
-
+
Currently Staying
-
+
{stats.currently_staying}
{}
-
-
-
-
+
+
-
+
Upcoming Bookings
-
+
{stats.upcoming_bookings.length}
{}
-
+
{}
-
-
+
+
+
Recent Activity
{stats.recent_activity && stats.recent_activity.length > 0 ? (
-
+
{stats.recent_activity.map((activity, index) => (
navigate(`/bookings/${activity.booking_id}`)}
>
-
-
+
-
-
+
+
{activity.action}
-
+
{activity.room?.room_number || activity.booking_number}
-
+
{formatRelativeTime(new Date(activity.created_at))}
@@ -259,35 +267,36 @@ const DashboardPage: React.FC = () => {
{}
-
-
+
+
+
Upcoming Bookings
{stats.upcoming_bookings && stats.upcoming_bookings.length > 0 ? (
-
+
{stats.upcoming_bookings.map((booking) => (
navigate(`/bookings/${booking.id}`)}
>
-
-
+
+
Room {booking.room?.room_number || 'N/A'}
-
+
{formatDate(booking.check_in_date, 'medium')}
-
+
{formatCurrency(booking.total_price)}
-
{booking.status.charAt(0).toUpperCase() + booking.status.slice(1).replace('_', ' ')}
@@ -307,16 +316,17 @@ const DashboardPage: React.FC = () => {
{}
-
-
-
+
+
+
+
Payment History
navigate('/bookings')}
- className="text-sm text-blue-600 hover:text-blue-700 font-medium"
+ className="text-xs sm:text-sm text-blue-600 hover:text-blue-700 font-semibold hover:underline transition-colors"
>
- View All
+ View All →
{loadingPayments ? (
@@ -324,34 +334,34 @@ const DashboardPage: React.FC = () => {
) : recentPayments && recentPayments.length > 0 ? (
-
+
{recentPayments.map((payment) => (
navigate(`/bookings/${payment.booking_id}`)}
>
-
-
-
+
+
+
-
+
{formatCurrency(payment.amount)}
-
-
+
+
{getPaymentMethodLabel(payment.payment_method)}
{payment.payment_date && (
-
+
• {formatDate(payment.payment_date, 'short')}
)}
-
+
{payment.payment_status.charAt(0).toUpperCase() + payment.payment_status.slice(1)}
diff --git a/Frontend/src/pages/customer/GDPRPage.tsx b/Frontend/src/pages/customer/GDPRPage.tsx
new file mode 100644
index 00000000..db5cbf57
--- /dev/null
+++ b/Frontend/src/pages/customer/GDPRPage.tsx
@@ -0,0 +1,205 @@
+import React, { useEffect, useState } from 'react';
+import { Download, Trash2, FileText, AlertCircle } from 'lucide-react';
+import gdprService, { GDPRRequest } from '../../features/compliance/services/gdprService';
+import { toast } from 'react-toastify';
+import Loading from '../../shared/components/Loading';
+import { formatDate } from '../../shared/utils/format';
+
+const GDPRPage: React.FC = () => {
+ const [requests, setRequests] = useState
([]);
+ const [loading, setLoading] = useState(true);
+ const [requesting, setRequesting] = useState(false);
+
+ useEffect(() => {
+ fetchRequests();
+ }, []);
+
+ const fetchRequests = async () => {
+ try {
+ setLoading(true);
+ const response = await gdprService.getMyRequests();
+ setRequests(response.data.requests || []);
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load GDPR requests');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleRequestExport = async () => {
+ if (!confirm('Request a copy of your personal data? You will receive an email when ready.')) return;
+
+ try {
+ setRequesting(true);
+ await gdprService.requestDataExport();
+ toast.success('Data export request created. You will receive an email when ready.');
+ fetchRequests();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to create export request');
+ } finally {
+ setRequesting(false);
+ }
+ };
+
+ const handleRequestDeletion = async () => {
+ if (!confirm('WARNING: This will permanently delete your account and all associated data. This action cannot be undone. Are you absolutely sure?')) return;
+ if (!confirm('This is your last chance. Are you 100% certain you want to delete all your data?')) return;
+
+ try {
+ setRequesting(true);
+ const response = await gdprService.requestDataDeletion();
+ toast.success('Deletion request created. Please check your email to confirm.');
+ fetchRequests();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to create deletion request');
+ } finally {
+ setRequesting(false);
+ }
+ };
+
+ const handleDownload = async (request: GDPRRequest) => {
+ if (!request.verification_token) {
+ toast.error('Verification token not available');
+ return;
+ }
+
+ try {
+ const blob = await gdprService.downloadExport(request.id, request.verification_token);
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `user_data_export_${request.id}.json`;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+ toast.success('Export downloaded');
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to download export');
+ }
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+ Data Privacy (GDPR)
+
+
+
+ Manage your personal data and privacy rights
+
+
+
+
+ {/* Data Export */}
+
+
+
+
+
Export Your Data
+
+ Request a copy of all your personal data stored in our system. You will receive an email with a download link when your data is ready.
+
+
+ {requesting ? 'Requesting...' : 'Request Data Export'}
+
+
+
+
+
+ {/* Data Deletion */}
+
+
+
+
+
Delete Your Data
+
+
+
+
+
Warning: This action cannot be undone
+
+ Requesting data deletion will permanently remove your account and all associated data including bookings, payments, and personal information. This action is irreversible.
+
+
+
+
+
+ {requesting ? 'Requesting...' : 'Request Data Deletion'}
+
+
+
+
+
+ {/* Request History */}
+ {requests.length > 0 && (
+
+
Request History
+
+ {requests.map((request) => (
+
+
+
+
+ {request.request_type === 'data_export' ? (
+
+ ) : (
+
+ )}
+
+ {request.request_type === 'data_export' ? 'Data Export' : 'Data Deletion'}
+
+
+ {request.status}
+
+
+
+ Created: {formatDate(request.created_at)}
+ {request.processed_at && ` • Processed: ${formatDate(request.processed_at)}`}
+
+
+ {request.request_type === 'data_export' && request.status === 'completed' && (
+
handleDownload(request)}
+ className="w-full sm:w-auto px-3 sm:px-4 py-2 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg sm:rounded-xl hover:from-blue-700 hover:to-blue-800 shadow-lg hover:shadow-xl transition-all duration-200 text-xs sm:text-sm font-semibold"
+ >
+ Download
+
+ )}
+
+
+ ))}
+
+
+ )}
+
+
+
+ );
+};
+
+export default GDPRPage;
+
diff --git a/Frontend/src/pages/customer/ProfilePage.tsx b/Frontend/src/pages/customer/ProfilePage.tsx
index db81c224..36c089d9 100644
--- a/Frontend/src/pages/customer/ProfilePage.tsx
+++ b/Frontend/src/pages/customer/ProfilePage.tsx
@@ -18,16 +18,21 @@ import {
Eye,
EyeOff,
RefreshCw,
- KeyRound
+ KeyRound,
+ LogOut,
+ Monitor
} from 'lucide-react';
import { toast } from 'react-toastify';
import authService from '../../features/auth/services/authService';
+import sessionService from '../../features/auth/services/sessionService';
import useAuthStore from '../../store/useAuthStore';
import Loading from '../../shared/components/Loading';
import EmptyState from '../../shared/components/EmptyState';
import { useAsync } from '../../shared/hooks/useAsync';
import { useGlobalLoading } from '../../shared/contexts/LoadingContext';
import { normalizeImageUrl } from '../../shared/utils/imageUtils';
+import { formatDate } from '../../shared/utils/format';
+import { UserSession } from '../../features/auth/services/sessionService';
const profileValidationSchema = yup.object().shape({
name: yup
@@ -55,7 +60,11 @@ const passwordValidationSchema = yup.object().shape({
newPassword: yup
.string()
.required('New password is required')
- .min(6, 'Password must be at least 6 characters'),
+ .min(8, 'Password must be at least 8 characters')
+ .matches(/[A-Z]/, 'Password must contain at least one uppercase letter')
+ .matches(/[a-z]/, 'Password must contain at least one lowercase letter')
+ .matches(/[0-9]/, 'Password must contain at least one number')
+ .matches(/[!@#$%^&*(),.?":{}|<>]/, 'Password must contain at least one special character (!@#$%^&*(),.?":{}|<>)'),
confirmPassword: yup
.string()
.required('Please confirm your password')
@@ -68,7 +77,7 @@ type PasswordFormData = yup.InferType;
const ProfilePage: React.FC = () => {
const { userInfo, setUser } = useAuthStore();
const { setLoading } = useGlobalLoading();
- const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'mfa'>('profile');
+ const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'mfa' | 'sessions'>('profile');
const [avatarPreview, setAvatarPreview] = useState(null);
const [showPassword, setShowPassword] = useState<{
current: boolean;
@@ -402,6 +411,17 @@ const ProfilePage: React.FC = () => {
Two-Factor Authentication
+ setActiveTab('sessions')}
+ className={`py-3 sm:py-4 px-2 sm:px-1 border-b-2 font-medium text-xs sm:text-sm transition-colors whitespace-nowrap ${
+ activeTab === 'sessions'
+ ? 'border-[#d4af37] text-[#d4af37]'
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
+ }`}
+ >
+
+ Active Sessions
+
@@ -967,9 +987,133 @@ const ProfilePage: React.FC = () => {
)}
)}
+
+ {/* Sessions Tab */}
+ {activeTab === 'sessions' && (
+
+ )}
);
};
+const SessionsTab: React.FC = () => {
+ const [sessions, setSessions] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ fetchSessions();
+ }, []);
+
+ const fetchSessions = async () => {
+ try {
+ setLoading(true);
+ const response = await sessionService.getMySessions();
+ setSessions(response.data.sessions || []);
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load sessions');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleRevoke = async (sessionId: number) => {
+ if (!confirm('Are you sure you want to revoke this session?')) return;
+
+ try {
+ await sessionService.revokeSession(sessionId);
+ toast.success('Session revoked');
+ fetchSessions();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to revoke session');
+ }
+ };
+
+ const handleRevokeAll = async () => {
+ if (!confirm('Are you sure you want to revoke all other sessions?')) return;
+
+ try {
+ await sessionService.revokeAllSessions();
+ toast.success('All other sessions revoked');
+ fetchSessions();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to revoke sessions');
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Active Sessions
+
+
+ Manage your active sessions across different devices
+
+
+ {sessions.length > 1 && (
+
+ Revoke All Others
+
+ )}
+
+
+ {sessions.length === 0 ? (
+
+ ) : (
+
+ {sessions.map((session) => (
+
+
+
+
+
+
+ {session.user_agent || 'Unknown Device'}
+
+
+ IP: {session.ip_address || 'Unknown'}
+
+
+ Last Activity: {formatDate(session.last_activity)}
+
+
+ Expires: {formatDate(session.expires_at)}
+
+
+
+
handleRevoke(session.id)}
+ className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 flex items-center gap-2 text-sm"
+ >
+
+ Revoke
+
+
+
+ ))}
+
+ )}
+
+ );
+};
+
export default ProfilePage;
diff --git a/Frontend/src/pages/customer/SessionManagementPage.tsx b/Frontend/src/pages/customer/SessionManagementPage.tsx
new file mode 100644
index 00000000..20c78e60
--- /dev/null
+++ b/Frontend/src/pages/customer/SessionManagementPage.tsx
@@ -0,0 +1,135 @@
+import React, { useEffect, useState } from 'react';
+import { LogOut, Monitor, Smartphone, Tablet } from 'lucide-react';
+import sessionService, { UserSession } from '../../features/auth/services/sessionService';
+import { toast } from 'react-toastify';
+import Loading from '../../shared/components/Loading';
+import { formatDate } from '../../shared/utils/format';
+
+const SessionManagementPage: React.FC = () => {
+ const [sessions, setSessions] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ fetchSessions();
+ }, []);
+
+ const fetchSessions = async () => {
+ try {
+ setLoading(true);
+ const response = await sessionService.getMySessions();
+ setSessions(response.data.sessions || []);
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load sessions');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleRevoke = async (sessionId: number) => {
+ if (!confirm('Are you sure you want to revoke this session?')) return;
+
+ try {
+ await sessionService.revokeSession(sessionId);
+ toast.success('Session revoked');
+ fetchSessions();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to revoke session');
+ }
+ };
+
+ const handleRevokeAll = async () => {
+ if (!confirm('Are you sure you want to revoke all other sessions?')) return;
+
+ try {
+ await sessionService.revokeAllSessions();
+ toast.success('All other sessions revoked');
+ fetchSessions();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to revoke sessions');
+ }
+ };
+
+ const getDeviceIcon = (userAgent?: string) => {
+ if (!userAgent) return ;
+ if (userAgent.includes('Mobile')) return ;
+ if (userAgent.includes('Tablet')) return ;
+ return ;
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+ Active Sessions
+
+
+
Manage your active sessions
+
+ {sessions.length > 1 && (
+
+ Revoke All Other Sessions
+
+ )}
+
+
+
+ {sessions.length === 0 ? (
+
+ ) : (
+
+ {sessions.map((session) => (
+
+
+
+
+ {getDeviceIcon(session.user_agent)}
+
+
+
+ {session.user_agent || 'Unknown Device'}
+
+
+ IP: {session.ip_address || 'Unknown'}
+
+
+ Last Activity: {formatDate(session.last_activity)}
+
+
+ Expires: {formatDate(session.expires_at)}
+
+
+
+
handleRevoke(session.id)}
+ className="w-full sm:w-auto px-3 sm:px-4 py-2 bg-gradient-to-r from-red-600 to-red-700 text-white rounded-lg sm:rounded-xl hover:from-red-700 hover:to-red-800 shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center gap-2 text-xs sm:text-sm font-semibold"
+ >
+
+ Revoke
+
+
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+export default SessionManagementPage;
+
diff --git a/Frontend/src/pages/staff/DashboardPage.tsx b/Frontend/src/pages/staff/DashboardPage.tsx
index e617672c..eede02ce 100644
--- a/Frontend/src/pages/staff/DashboardPage.tsx
+++ b/Frontend/src/pages/staff/DashboardPage.tsx
@@ -173,7 +173,7 @@ const StaffDashboardPage: React.FC = () => {
if (error || !stats) {
return (
-
+
{
}
return (
-
+
{}
-
-
-
-
-
+
+
+
-
Hotel operations overview and management
+
Hotel operations overview and management
{}
-
-
{}
-
+
{}
-
+
-
-
Total Revenue
-
+
+
Total Revenue
+
{formatCurrency(stats?.total_revenue || 0)}
-
-
-
+
+
{stats?.total_payments || 0} completed payments
{}
-
+
-
-
Total Bookings
-
+
+
Total Bookings
+
{stats?.total_bookings || 0}
-
-
-
+
+
{stats?.active_bookings || 0} active bookings
{}
-
+
-
-
Available Rooms
-
+
+
Available Rooms
+
{stats?.available_rooms || 0}
-
-
-
+
+
{stats?.total_rooms || 0} total rooms
{}
-
+
-
-
Pending Payments
-
+
+
Pending Payments
+
{stats?.pending_payments || 0}
-
-
-
+
+
Require attention
@@ -311,17 +312,17 @@ const StaffDashboardPage: React.FC = () => {
{}
-
+
{}
-
-
-
+
+
+
Recent Bookings
navigate('/staff/bookings')}
- className="text-sm text-blue-600 hover:text-blue-700 font-semibold"
+ className="text-xs sm:text-sm text-blue-600 hover:text-blue-700 font-semibold hover:underline transition-colors"
>
View All →
@@ -331,28 +332,28 @@ const StaffDashboardPage: React.FC = () => {
) : recentBookings.length === 0 ? (
) : (
-
+
{recentBookings.map((booking) => (
navigate(`/staff/bookings/${booking.id}`)}
>
-
-
-
-
{booking.booking_number}
+
+
+
+ {booking.booking_number}
{getBookingStatusBadge(booking.status || 'pending')}
-
+
{booking.user?.name || 'Guest'} • Room {booking.room?.room_number || 'N/A'}
{booking.check_in_date && formatDate(booking.check_in_date)} - {booking.check_out_date && formatDate(booking.check_out_date)}
-
-
{formatCurrency(booking.total_price || 0)}
+
+
{formatCurrency(booking.total_price || 0)}
@@ -362,15 +363,15 @@ const StaffDashboardPage: React.FC = () => {
{}
-
-
-
+
+
+
Recent Payments
navigate('/staff/payments')}
- className="text-sm text-blue-600 hover:text-blue-700 font-semibold"
+ className="text-xs sm:text-sm text-blue-600 hover:text-blue-700 font-semibold hover:underline transition-colors"
>
View All →
@@ -380,29 +381,29 @@ const StaffDashboardPage: React.FC = () => {
) : recentPayments.length === 0 ? (
) : (
-
+
{recentPayments.map((payment) => (
-
-
-
-
+
+
+
+
{payment.transaction_id || `PAY-${payment.id}`}
{getPaymentStatusBadge(payment.payment_status)}
-
+
{payment.booking?.booking_number || 'N/A'} • {payment.payment_method || 'N/A'}
{payment.payment_date && formatDate(payment.payment_date)}
-
-
{formatCurrency(payment.amount || 0)}
+
+
{formatCurrency(payment.amount || 0)}
diff --git a/Frontend/src/shared/components/SidebarAdmin.tsx b/Frontend/src/shared/components/SidebarAdmin.tsx
index 31c00d85..c4623ce7 100644
--- a/Frontend/src/shared/components/SidebarAdmin.tsx
+++ b/Frontend/src/shared/components/SidebarAdmin.tsx
@@ -30,7 +30,13 @@ import {
Star,
AlertCircle,
FileCheck,
- ClipboardCheck
+ ClipboardCheck,
+ CheckCircle2,
+ Download,
+ Webhook,
+ Key,
+ HardDrive,
+ Activity
} from 'lucide-react';
import useAuthStore from '../../store/useAuthStore';
import { useResponsive } from '../../hooks';
@@ -272,6 +278,31 @@ const SidebarAdmin: React.FC = ({
icon: ClipboardCheck,
label: 'Compliance'
},
+ {
+ path: '/admin/approvals',
+ icon: CheckCircle2,
+ label: 'Approvals'
+ },
+ {
+ path: '/admin/gdpr',
+ icon: Download,
+ label: 'GDPR'
+ },
+ {
+ path: '/admin/webhooks',
+ icon: Webhook,
+ label: 'Webhooks'
+ },
+ {
+ path: '/admin/api-keys',
+ icon: Key,
+ label: 'API Keys'
+ },
+ {
+ path: '/admin/backups',
+ icon: HardDrive,
+ label: 'Backups'
+ },
]
},
];