diff --git a/src/components/shared/SearchBox.tsx b/src/components/shared/SearchBox.tsx new file mode 100644 index 0000000..19b1b83 --- /dev/null +++ b/src/components/shared/SearchBox.tsx @@ -0,0 +1,42 @@ +import type { ReactElement } from 'react'; +import { Search } from 'lucide-react'; +import { useAppTheme } from '@/hooks/useAppTheme'; + +export interface SearchBoxProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + containerClassName?: string; + inputClassName?: string; +} + +export const SearchBox = ({ + value, + onChange, + placeholder = 'Search...', + containerClassName = 'relative w-full sm:w-64', + inputClassName = 'w-full pl-9 pr-4 py-2 border border-[rgba(0,0,0,0.08)] rounded-md text-sm focus:outline-none focus:ring-2 transition-all bg-gray-50/30' +}: SearchBoxProps): ReactElement => { + const { primaryColor } = useAppTheme(); + + return ( +
+ + onChange(e.target.value)} + className={inputClassName} + onFocus={(e) => { + e.currentTarget.style.borderColor = primaryColor; + e.currentTarget.style.boxShadow = `0 0 0 2px ${primaryColor}1A`; + }} + onBlur={(e) => { + e.currentTarget.style.borderColor = 'rgba(0,0,0,0.08)'; + e.currentTarget.style.boxShadow = 'none'; + }} + /> +
+ ); +}; diff --git a/src/components/shared/index.ts b/src/components/shared/index.ts index 1aeaa61..7a7c8f8 100644 --- a/src/components/shared/index.ts +++ b/src/components/shared/index.ts @@ -38,4 +38,4 @@ export { FormSlider } from './FormSlider'; export { RichTextEditor } from './RichTextEditor'; export { FileUploadModal } from './FileUploadModal'; export type { FileUploadModalProps } from './FileUploadModal'; -export { FileShareModal } from './FileShareModal'; \ No newline at end of file +export { FileShareModal } from './FileShareModal';export { SearchBox } from './SearchBox'; diff --git a/src/features/dashboard/components/RecentActivity.tsx b/src/features/dashboard/components/RecentActivity.tsx index 7716773..1cbd76d 100644 --- a/src/features/dashboard/components/RecentActivity.tsx +++ b/src/features/dashboard/components/RecentActivity.tsx @@ -50,9 +50,11 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => { const { primaryColor } = useAppTheme(); const [auditLogs, setAuditLogs] = useState([]); const [isLoading, setIsLoading] = useState(true); - const { tenantId } = useAppSelector((state) => state.auth); + const { tenantId, roles } = useAppSelector((state) => state.auth); const navigate = useNavigate(); + const auditLogPath = roles?.includes('super_admin') ? '/audit-logs' : '/tenant/audit-logs'; + // Default to table variant for a more professional look const activeVariant = variant || 'table'; @@ -84,7 +86,7 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => { size="sm" className="text-[11px] font-bold gap-1 h-7" style={{ color: primaryColor }} - onClick={() => navigate('/tenant/audit-logs')} + onClick={() => navigate(auditLogPath)} > View All @@ -134,7 +136,7 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => { variant="outline" size="sm" className="h-8 text-[11px] gap-1.5 border-[rgba(0,0,0,0.08)] hover:bg-gray-50" - onClick={() => navigate('/tenant/audit-logs')} + onClick={() => navigate(auditLogPath)} > View All diff --git a/src/features/dashboard/components/StatsGrid.tsx b/src/features/dashboard/components/StatsGrid.tsx index dfc5f17..78a942e 100644 --- a/src/features/dashboard/components/StatsGrid.tsx +++ b/src/features/dashboard/components/StatsGrid.tsx @@ -1,8 +1,15 @@ -import { useState, useEffect } from 'react'; -import { Building2, CheckCircle2, Users, TrendingUp, Package, Heart } from 'lucide-react'; -import { StatCard } from './StatCard'; -import type { StatCardData } from '@/types/dashboard'; -import { dashboardService } from '@/services/dashboard-service'; +import { useState, useEffect } from "react"; +import { + Building2, + CheckCircle2, + // Users, + // TrendingUp, + Package, + Heart, +} from "lucide-react"; +import { StatCard } from "./StatCard"; +import type { StatCardData } from "@/types/dashboard"; +import { dashboardService } from "@/services/dashboard-service"; export const StatsGrid = () => { const [statsData, setStatsData] = useState([]); @@ -18,60 +25,64 @@ export const StatsGrid = () => { const { data } = response; const mappedStats: StatCardData[] = [ - { - icon: Building2, + { + icon: Building2, value: data.totalTenants, - label: 'Total Tenants', - badge: { text: `${data.activeTenants} active`, variant: 'green' }, - }, - { - icon: CheckCircle2, + label: "Total Tenants", + badge: { text: `${data.activeTenants} active`, variant: "green" }, + }, + { + icon: CheckCircle2, value: data.activeTenants, - label: 'Active Tenants', + label: "Active Tenants", badge: { - text: data.totalTenants > 0 - ? `${Math.round((data.activeTenants / data.totalTenants) * 100)}% Rate` - : '0% Rate', - variant: 'green', + text: + data.totalTenants > 0 + ? `${Math.round((data.activeTenants / data.totalTenants) * 100)}% Rate` + : "0% Rate", + variant: "green", }, - }, - { - icon: Users, - value: data.totalUsers, - label: 'Total Users', - badge: { text: 'All users', variant: 'gray' }, - }, - { - icon: TrendingUp, - value: data.activeSessions, - label: 'Active Sessions', - badge: { text: 'Live now', variant: 'gray' }, - }, - { - icon: Package, + }, + // { + // icon: Users, + // value: data.totalUsers, + // label: "Total Users", + // badge: { text: "All users", variant: "gray" }, + // }, + // { + // icon: TrendingUp, + // value: data.activeSessions, + // label: "Active Sessions", + // badge: { text: "Live now", variant: "gray" }, + // }, + { + icon: Package, value: data.registeredModules, - label: 'Registered Modules', - badge: { text: 'Total', variant: 'gray' }, - }, - { - icon: Heart, + label: "Registered Modules", + badge: { text: "Total", variant: "gray" }, + }, + { + icon: Heart, value: data.healthyModules, - label: 'Healthy Modules', + label: "Healthy Modules", badge: { - text: data.registeredModules > 0 - ? `${Math.round((data.healthyModules / data.registeredModules) * 100)}% Uptime` - : '0% Uptime', - variant: data.healthyModules === data.registeredModules && data.registeredModules > 0 - ? 'green' - : 'gray', + text: + data.registeredModules > 0 + ? `${Math.round((data.healthyModules / data.registeredModules) * 100)}% Uptime` + : "0% Uptime", + variant: + data.healthyModules === data.registeredModules && + data.registeredModules > 0 + ? "green" + : "gray", }, - }, -]; + }, + ]; setStatsData(mappedStats); } catch (err) { - console.error('Failed to fetch dashboard statistics:', err); - setError('Failed to load statistics. Please try again.'); + console.error("Failed to fetch dashboard statistics:", err); + setError("Failed to load statistics. Please try again."); } finally { setIsLoading(false); } diff --git a/src/pages/superadmin/AuditLogs.tsx b/src/pages/superadmin/AuditLogs.tsx index 762e7d9..70ee158 100644 --- a/src/pages/superadmin/AuditLogs.tsx +++ b/src/pages/superadmin/AuditLogs.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect, useMemo } from 'react'; -import type { ReactElement } from 'react'; -import { Layout } from '@/components/layout/Layout'; +import { useState, useEffect, useMemo } from "react"; +import type { ReactElement } from "react"; +import { Layout } from "@/components/layout/Layout"; import { ViewAuditLogModal, DataTable, @@ -8,63 +8,89 @@ import { FilterDropdown, StatusBadge, type Column, -} from '@/components/shared'; -import { Download, ArrowUpDown, Search, ChevronDown, SlidersHorizontal } from 'lucide-react'; -import { auditLogService } from '@/services/audit-log-service'; -import { tenantService } from '@/services/tenant-service'; -import type { AuditLog } from '@/types/audit-log'; -import type { Tenant } from '@/types/tenant'; -import { cn } from '@/lib/utils'; -import { useAppTheme } from '@/hooks/useAppTheme'; +} from "@/components/shared"; +import { + Download, + ArrowUpDown, + Search, + ChevronDown, + SlidersHorizontal, +} from "lucide-react"; +import { auditLogService } from "@/services/audit-log-service"; +import { tenantService } from "@/services/tenant-service"; +import type { AuditLog } from "@/types/audit-log"; +import type { Tenant } from "@/types/tenant"; +import { cn } from "@/lib/utils"; +import { useAppTheme } from "@/hooks/useAppTheme"; // Helper function to format date const formatDate = (dateString: string): string => { const date = new Date(dateString); - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", }); }; // Helper function to get action badge variant -const getActionVariant = (action: string): 'success' | 'failure' | 'info' | 'process' => { +const getActionVariant = ( + action: string, +): "success" | "failure" | "info" | "process" => { const lowerAction = action.toLowerCase(); - if (lowerAction.includes('create') || lowerAction.includes('register')) return 'success'; - if (lowerAction.includes('update') || lowerAction.includes('version_update') || lowerAction.includes('login')) return 'info'; - if (lowerAction.includes('delete') || lowerAction.includes('deregister')) return 'failure'; - if (lowerAction.includes('read') || lowerAction.includes('get') || lowerAction.includes('status_change')) return 'process'; - return 'info'; + if (lowerAction.includes("create") || lowerAction.includes("register")) + return "success"; + if ( + lowerAction.includes("update") || + lowerAction.includes("version_update") || + lowerAction.includes("login") + ) + return "info"; + if (lowerAction.includes("delete") || lowerAction.includes("deregister")) + return "failure"; + if ( + lowerAction.includes("read") || + lowerAction.includes("get") || + lowerAction.includes("status_change") + ) + return "process"; + return "info"; }; // Helper function to get method badge variant -const getMethodVariant = (method: string | null): 'success' | 'failure' | 'info' | 'process' => { - if (!method) return 'info'; +const getMethodVariant = ( + method: string | null, +): "success" | "failure" | "info" | "process" => { + if (!method) return "info"; const upperMethod = method.toUpperCase(); - if (upperMethod === 'GET') return 'success'; - if (upperMethod === 'POST') return 'info'; - if (upperMethod === 'PUT' || upperMethod === 'PATCH') return 'process'; - if (upperMethod === 'DELETE') return 'failure'; - return 'info'; + if (upperMethod === "GET") return "success"; + if (upperMethod === "POST") return "info"; + if (upperMethod === "PUT" || upperMethod === "PATCH") return "process"; + if (upperMethod === "DELETE") return "failure"; + return "info"; }; // Helper function to get status badge color based on response status const getStatusColor = (status: number | null): string => { - if (!status) return 'text-[#6b7280]'; - if (status >= 200 && status < 300) return 'text-[#10b981]'; - if (status >= 300 && status < 400) return 'text-[#f59e0b]'; - if (status >= 400) return 'text-[#ef4444]'; - return 'text-[#6b7280]'; + if (!status) return "text-[#6b7280]"; + if (status >= 200 && status < 300) return "text-[#10b981]"; + if (status >= 300 && status < 400) return "text-[#f59e0b]"; + if (status >= 400) return "text-[#ef4444]"; + return "text-[#6b7280]"; }; const AuditLogs = (): ReactElement => { const { primaryColor } = useAppTheme(); const [auditLogs, setAuditLogs] = useState([]); const [tenants, setTenants] = useState([]); - const [modules, setModules] = useState>([]); - const [resourceTypes, setResourceTypes] = useState<{ value: string; label: string }[]>([]); + const [modules, setModules] = useState>( + [], + ); + const [resourceTypes, setResourceTypes] = useState< + { value: string; label: string }[] + >([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -89,23 +115,28 @@ const AuditLogs = (): ReactElement => { const [tenantFilter, setTenantFilter] = useState(null); const [methodFilter, setMethodFilter] = useState(null); const [actionFilter, setActionFilter] = useState(null); - const [resourceTypeFilter, setResourceTypeFilter] = useState(null); + const [resourceTypeFilter, setResourceTypeFilter] = useState( + null, + ); const [moduleFilter, setModuleFilter] = useState(null); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); const [orderBy, setOrderBy] = useState(null); - const [search, setSearch] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); + const [search, setSearch] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); const [showMoreFilters, setShowMoreFilters] = useState(false); const hasExtraFilters = useMemo( - () => Boolean(moduleFilter || methodFilter || startDate || endDate || orderBy), - [moduleFilter, methodFilter, startDate, endDate, orderBy] + () => + Boolean(moduleFilter || methodFilter || startDate || endDate || orderBy), + [moduleFilter, methodFilter, startDate, endDate, orderBy], ); // View modal const [viewModalOpen, setViewModalOpen] = useState(false); - const [selectedAuditLogId, setSelectedAuditLogId] = useState(null); + const [selectedAuditLogId, setSelectedAuditLogId] = useState( + null, + ); // Fetch tenants on mount for the selector useEffect(() => { @@ -116,7 +147,7 @@ const AuditLogs = (): ReactElement => { setTenants(response.data); } } catch (err) { - console.error('Failed to load tenants:', err); + console.error("Failed to load tenants:", err); } }; @@ -126,12 +157,12 @@ const AuditLogs = (): ReactElement => { if (response.success) { const options = response.data.map((rt: any) => ({ value: rt.value, - label: rt.label + label: rt.label, })); setResourceTypes(options); } } catch (err) { - console.error('Failed to load resource types:', err); + console.error("Failed to load resource types:", err); } }; @@ -142,7 +173,7 @@ const AuditLogs = (): ReactElement => { setModules(response.data || []); } } catch (err) { - console.error('Failed to load modules:', err); + console.error("Failed to load modules:", err); } }; @@ -174,16 +205,18 @@ const AuditLogs = (): ReactElement => { endDate: endDate || null, search: debouncedSearch || null, }, - orderBy + orderBy, ); if (response.success) { setAuditLogs(response.data); setPagination(response.pagination); } else { - setError('Failed to load audit logs'); + setError("Failed to load audit logs"); } } catch (err: any) { - setError(err?.response?.data?.error?.message || 'Failed to load audit logs'); + setError( + err?.response?.data?.error?.message || "Failed to load audit logs", + ); } finally { setIsLoading(false); } @@ -201,29 +234,44 @@ const AuditLogs = (): ReactElement => { // Fetch audit logs on mount and when pagination/filters change useEffect(() => { fetchAuditLogs(); - }, [currentPage, limit, tenantFilter, methodFilter, actionFilter, resourceTypeFilter, moduleFilter, startDate, endDate, orderBy, debouncedSearch]); + }, [ + currentPage, + limit, + tenantFilter, + methodFilter, + actionFilter, + resourceTypeFilter, + moduleFilter, + startDate, + endDate, + orderBy, + debouncedSearch, + ]); // Handle Export const handleExport = async (): Promise => { try { const response = await auditLogService.export({ - format: 'json', + format: "json", startDate: startDate || undefined, endDate: endDate || undefined, - tenantId: tenantFilter || undefined + tenantId: tenantFilter || undefined, }); if (response.success) { - const blob = new Blob([JSON.stringify(response.data.records, null, 2)], { type: 'application/json' }); + const blob = new Blob( + [JSON.stringify(response.data.records, null, 2)], + { type: "application/json" }, + ); const url = URL.createObjectURL(blob); - const link = document.createElement('a'); + const link = document.createElement("a"); link.href = url; - link.download = `platform-audit-logs-${new Date().toISOString().split('T')[0]}.json`; + link.download = `platform-audit-logs-${new Date().toISOString().split("T")[0]}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } } catch (err: any) { - alert('Export failed: ' + err.message); + alert("Export failed: " + err.message); } }; @@ -242,41 +290,45 @@ const AuditLogs = (): ReactElement => { // Define table columns const columns: Column[] = [ { - key: 'created_at', - label: 'Timestamp', + key: "created_at", + label: "Timestamp", render: (log) => ( - {formatDate(log.created_at)} + + {formatDate(log.created_at)} + ), - mobileLabel: 'Time', + mobileLabel: "Time", }, { - key: 'tenant', - label: 'Tenant', + key: "tenant", + label: "Tenant", render: (log) => ( - {log.tenant ? log.tenant.name : 'N/A'} + {log.tenant ? log.tenant.name : "N/A"} ), }, { - key: 'resource_type', - label: 'Resource Type', + key: "resource_type", + label: "Resource Type", render: (log) => ( - {log.resource_type} + + {log.resource_type} + ), }, { - key: 'module', - label: 'Module', + key: "module", + label: "Module", render: (log) => ( - {log.module?.name || 'Platform'} + {log.module?.name || "Platform"} ), }, { - key: 'action', - label: 'Action', + key: "action", + label: "Action", render: (log) => ( {log.action} @@ -284,36 +336,40 @@ const AuditLogs = (): ReactElement => { ), }, { - key: 'user', - label: 'User', + key: "user", + label: "User", render: (log) => ( - {log.user ? `${log.user.first_name} ${log.user.last_name}` : (log.user_email || 'N/A')} + {log.user + ? `${log.user.first_name} ${log.user.last_name}` + : log.user_email || "N/A"} ), }, { - key: 'request_method', - label: 'Method', + key: "request_method", + label: "Method", render: (log) => ( - {log.request_method ? log.request_method.toUpperCase() : 'N/A'} + {log.request_method ? log.request_method.toUpperCase() : "N/A"} ), }, { - key: 'response_status', - label: 'Status', + key: "response_status", + label: "Status", render: (log) => ( - - {log.response_status || 'N/A'} + + {log.response_status || "N/A"} ), }, { - key: 'actions', - label: 'Actions', - align: 'right', + key: "actions", + label: "Actions", + align: "right", render: (log) => (
@@ -501,10 +573,10 @@ const AuditLogs = (): ReactElement => { { @@ -517,8 +589,8 @@ const AuditLogs = (): ReactElement => { { @@ -533,7 +605,9 @@ const AuditLogs = (): ReactElement => {
- Start Date: + + Start Date: + { />
- End Date: + + End Date: + {
)} - {(startDate || endDate || tenantFilter || actionFilter || resourceTypeFilter || moduleFilter || methodFilter || search || orderBy) && ( + {(startDate || + endDate || + tenantFilter || + actionFilter || + resourceTypeFilter || + moduleFilter || + methodFilter || + search || + orderBy) && (
)} {activeTab === "modules" && id && } - {activeTab === "settings" && tenant && ( - + {activeTab === "settings" && id && ( + )} {activeTab === "license" && } - {activeTab === "audit-logs" && ( - + {activeTab === "audit-logs" && id && ( + )} {activeTab === "billing" && }
@@ -684,362 +627,6 @@ const LicenseTab = ({ tenant: _tenant }: LicenseTabProps): ReactElement => { ); }; -// Audit Logs Tab Component -interface AuditLogsTabProps { - auditLogs: AuditLog[]; - isLoading: boolean; - pagination: { - page: number; - limit: number; - total: number; - totalPages: number; - hasMore: boolean; - }; - currentPage: number; - limit: number; - onPageChange: (page: number) => void; -} - -const AuditLogsTab = ({ - auditLogs, - isLoading, - pagination, - currentPage, - limit, - onPageChange, -}: AuditLogsTabProps): ReactElement => { - const columns: Column[] = [ - { - key: "action", - label: "Action", - render: (log) => ( - {log.action} - ), - }, - { - key: "resource_type", - label: "Resource", - render: (log) => ( - - {log.resource_type} - - ), - }, - { - key: "user", - label: "User", - render: (log) => ( - - {log.user ? `${log.user.first_name} ${log.user.last_name}` : "System"} - - ), - }, - { - key: "request_method", - label: "Method", - render: (log) => ( - - {log.request_method || "N/A"} - - ), - }, - { - key: "response_status", - label: "Status", - render: (log) => ( - = 200 && - log.response_status < 300 - ? "text-green-600" - : log.response_status && log.response_status >= 400 - ? "text-red-600" - : "text-gray-600" - }`} - > - {log.response_status || "N/A"} - - ), - }, - { - key: "created_at", - label: "Date", - render: (log) => ( - - {formatDate(log.created_at)} - - ), - }, - ]; - - return ( -
- {/*
-

Audit Logs

-
*/} - log.id} - isLoading={isLoading} - /> - {pagination.totalPages > 1 && ( - {}} - /> - )} -
- ); -}; - -// Settings Tab Component -interface SettingsTabProps { - tenant: Tenant; -} - -const SettingsTab = ({ tenant: _tenant }: SettingsTabProps): ReactElement => { - const [logoFile, setLogoFile] = useState(null); - const [faviconFile, setFaviconFile] = useState(null); - const [primaryColor, setPrimaryColor] = useState("#112868"); - const [secondaryColor, setSecondaryColor] = useState("#23DCE1"); - const [accentColor, setAccentColor] = useState("#084CC8"); - - const handleLogoChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - // Validate file size (2MB max) - if (file.size > 2 * 1024 * 1024) { - alert("Logo file size must be less than 2MB"); - return; - } - // Validate file type - const validTypes = [ - "image/png", - "image/svg+xml", - "image/jpeg", - "image/jpg", - ]; - if (!validTypes.includes(file.type)) { - alert("Logo must be PNG, SVG, or JPG format"); - return; - } - setLogoFile(file); - } - }; - - const handleFaviconChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - // Validate file size (500KB max) - if (file.size > 500 * 1024) { - alert("Favicon file size must be less than 500KB"); - return; - } - // Validate file type - const validTypes = [ - "image/x-icon", - "image/png", - "image/vnd.microsoft.icon", - ]; - if (!validTypes.includes(file.type)) { - alert("Favicon must be ICO or PNG format"); - return; - } - setFaviconFile(file); - } - }; - - return ( -
- {/* Branding Section */} -
- {/* Section Header */} -
-

Branding

-

- Customize logo, favicon, and colors for this tenant experience. -

-
- - {/* Logo and Favicon Upload */} -
- {/* Company Logo */} -
- - - {logoFile && ( -
- Selected: {logoFile.name} -
- )} - {/* */} -
- - {/* Favicon */} -
- - - {faviconFile && ( -
- Selected: {faviconFile.name} -
- )} -
-
- - {/* Primary Color */} -
- -
-
-
- setPrimaryColor(e.target.value)} - className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent" - placeholder="#112868" - /> -
- setPrimaryColor(e.target.value)} - className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" - /> -
-

- Used for navigation, headers, and key actions. -

-
- - {/* Secondary and Accent Colors */} -
- {/* Secondary Color */} -
- -
-
-
- setSecondaryColor(e.target.value)} - className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent" - placeholder="#23DCE1" - /> -
- setSecondaryColor(e.target.value)} - className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" - /> -
-

- Used for highlights and supporting elements. -

-
- - {/* Accent Color */} -
- -
-
-
- setAccentColor(e.target.value)} - className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent" - placeholder="#084CC8" - /> -
- setAccentColor(e.target.value)} - className="size-10 rounded-md border border-[#d1d5db] cursor-pointer" - /> -
-

- Used for alerts and special notices. -

-
-
-
-
- ); -}; // Billing Tab Component interface BillingTabProps { diff --git a/src/pages/superadmin/Tenants.tsx b/src/pages/superadmin/Tenants.tsx index ddaf62e..a413fd5 100644 --- a/src/pages/superadmin/Tenants.tsx +++ b/src/pages/superadmin/Tenants.tsx @@ -9,6 +9,7 @@ import { DataTable, Pagination, FilterDropdown, + SearchBox, type Column, } from '@/components/shared'; // Note: NewTenantModal, ViewTenantModal, EditTenantModal are now in @/components/superadmin (commented out - using wizard/details/edit pages instead) @@ -16,7 +17,6 @@ import { Plus, ArrowUpDown } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { tenantService } from '@/services/tenant-service'; import type { Tenant } from '@/types/tenant'; - // Helper function to get tenant initials const getTenantInitials = (name: string): string => { const words = name.trim().split(/\s+/); @@ -81,6 +81,10 @@ const Tenants = (): ReactElement => { const [statusFilter, setStatusFilter] = useState(null); const [orderBy, setOrderBy] = useState(null); + // Search state + const [search, setSearch] = useState(''); + const [debouncedSearch, setDebouncedSearch] = useState(''); + // View, Edit, Delete modals // const [viewModalOpen, setViewModalOpen] = useState(false); // Commented out - using details page instead // const [editModalOpen, setEditModalOpen] = useState(false); // Commented out - using edit page instead @@ -93,12 +97,13 @@ const Tenants = (): ReactElement => { page: number, itemsPerPage: number, status: string | null = null, - sortBy: string[] | null = null + sortBy: string[] | null = null, + searchQuery: string | null = null ): Promise => { try { setIsLoading(true); setError(null); - const response = await tenantService.getAll(page, itemsPerPage, status, sortBy); + const response = await tenantService.getAll(page, itemsPerPage, status, sortBy, searchQuery); if (response.success) { setTenants(response.data); setPagination(response.pagination); @@ -113,8 +118,17 @@ const Tenants = (): ReactElement => { }; useEffect(() => { - fetchTenants(currentPage, limit, statusFilter, orderBy); - }, [currentPage, limit, statusFilter, orderBy]); + const timer = setTimeout(() => { + setDebouncedSearch(search); + // We only reset to first page if we are actively searching. + if (search) setCurrentPage(1); + }, 500); + return () => clearTimeout(timer); + }, [search]); + + useEffect(() => { + fetchTenants(currentPage, limit, statusFilter, orderBy, debouncedSearch); + }, [currentPage, limit, statusFilter, orderBy, debouncedSearch]); // Commented out - using wizard instead // const handleCreateTenant = async (data: { @@ -329,8 +343,15 @@ const Tenants = (): ReactElement => {
{/* Table Header with Filters */}
- {/* Filters */} -
+ {/* Search & Filters */} +
+ {/* Global Search */} + + {/* Status Filter */} { const date = new Date(dateString); @@ -59,11 +65,12 @@ const getStatusColor = (status: number | null): string => { return 'text-[#6b7280]'; }; -const AuditLogs = (): ReactElement => { +const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}): ReactElement => { const { primaryColor } = useAppTheme(); const roles = useAppSelector((state) => state.auth.roles); - const tenantId = useAppSelector((state) => state.auth.tenantId); - const isTenantAdmin = roles?.includes('tenant_admin'); + const authTenantId = useAppSelector((state) => state.auth.tenantId); + const tenantId = customTenantId || authTenantId; + const isTenantAdmin = customTenantId ? true : roles?.includes('tenant_admin'); const [auditLogs, setAuditLogs] = useState([]); const [resourceTypes, setResourceTypes] = useState<{ value: string; label: string }[]>([]); @@ -376,16 +383,8 @@ const AuditLogs = (): ReactElement => {
); - return ( - + const content = ( + <> {/* Table Container */}
{/* Table Header with Filters */} @@ -394,24 +393,12 @@ const AuditLogs = (): ReactElement => { {/* Search and Filters */}
{/* Global Search */} -
- - setSearch(e.target.value)} - className="w-full pl-9 pr-4 py-2 border border-[rgba(0,0,0,0.08)] rounded-md text-sm focus:outline-none focus:ring-2 transition-all bg-gray-50/30" - onFocus={(e) => { - e.currentTarget.style.borderColor = primaryColor; - e.currentTarget.style.boxShadow = `0 0 0 2px ${primaryColor}1A`; - }} - onBlur={(e) => { - e.currentTarget.style.borderColor = 'rgba(0,0,0,0.08)'; - e.currentTarget.style.boxShadow = 'none'; - }} - /> -
+ {isTenantAdmin && ( <> @@ -420,12 +407,26 @@ const AuditLogs = (): ReactElement => { label="Action" options={[ { value: 'LOGIN', label: 'LOGIN' }, + { value: 'LOGOUT', label: 'LOGOUT' }, { value: 'CREATE', label: 'CREATE' }, { value: 'UPDATE', label: 'UPDATE' }, { value: 'DELETE', label: 'DELETE' }, { value: 'SUBMIT', label: 'SUBMIT' }, { value: 'APPROVE', label: 'APPROVE' }, { value: 'REJECT', label: 'REJECT' }, + // {value: 'PUBLISH', label: 'PUBLISH'}, + // {value: 'ARCHIVE', label: 'ARCHIVE'}, + // {value: 'CHECKOUT', label: 'CHECKOUT'}, + // {value: 'CHECKIN', label: 'CHECKIN'}, + // {value: 'TRANSITION', label: 'TRANSITION'}, + // {value: 'CANCEL', label: 'CANCEL'}, + // {value: 'COMPLETE', label: 'COMPLETE'}, + // {value: 'ACTIVATE', label: 'ACTIVATE'}, + // {value: 'REVOKE', label: 'REVOKE'}, + // {value: 'STATUS', label: 'STATUS'}, + // {value: 'VOID', label: 'VOID'}, + // {value: 'SEND', label: 'SEND'}, + // {value: 'API_CALL', label: 'API_CALL'}, ]} value={actionFilter} onChange={(value) => { @@ -583,6 +584,24 @@ const AuditLogs = (): ReactElement => { auditLogId={selectedAuditLogId} onLoadAuditLog={loadAuditLog} /> + + ); + + if (hideLayout) { + return content; + } + + return ( + + {content} ); }; diff --git a/src/pages/tenant/Settings.tsx b/src/pages/tenant/Settings.tsx index e6460ff..0e53e2c 100644 --- a/src/pages/tenant/Settings.tsx +++ b/src/pages/tenant/Settings.tsx @@ -15,8 +15,14 @@ const getBaseUrlWithProtocol = (): string => { return import.meta.env.VITE_API_BASE_URL || "http://localhost:3000"; }; -const Settings = (): ReactElement => { - const tenantId = useAppSelector((state) => state.auth.tenantId); +export interface SettingsProps { + customTenantId?: string; + hideLayout?: boolean; +} + +const Settings = ({ customTenantId, hideLayout = false }: SettingsProps = {}): ReactElement => { + const authTenantId = useAppSelector((state) => state.auth.tenantId); + const tenantId = customTenantId || authTenantId; const dispatch = useAppDispatch(); const [isLoading, setIsLoading] = useState(true); @@ -363,16 +369,18 @@ const Settings = (): ReactElement => { if (response.success) { showToast.success("Settings updated successfully"); - // Update theme in Redux - dispatch( - updateTheme({ - logo_file_path: logoFilePath, - favicon_file_path: faviconFilePath, - primary_color: primaryColor, - secondary_color: secondaryColor, - accent_color: accentColor, - }), - ); + // Update theme in Redux for the active session, if not editing a different tenant context + if (!customTenantId || customTenantId === authTenantId) { + dispatch( + updateTheme({ + logo_file_path: logoFilePath, + favicon_file_path: faviconFilePath, + primary_color: primaryColor, + secondary_color: secondaryColor, + accent_color: accentColor, + }), + ); + } // Update local tenant state setTenant({ @@ -431,22 +439,15 @@ const Settings = (): ReactElement => { ); } - return ( - -
- {error && ( -
-

{error}

-
- )} + const content = ( +
+ {error && ( +
+

{error}

+
+ )} - {/* Branding Section */} + {/* Branding Section */}
{/* Section Header */}
@@ -707,6 +708,21 @@ const Settings = (): ReactElement => {
+ ); + + if (hideLayout) { + return content; + } + + return ( + + {content} ); }; diff --git a/src/services/dashboard-service.ts b/src/services/dashboard-service.ts index a30bba3..8de755e 100644 --- a/src/services/dashboard-service.ts +++ b/src/services/dashboard-service.ts @@ -3,8 +3,8 @@ import apiClient from './api-client'; export interface DashboardStatistics { totalTenants: number; activeTenants: number; - totalUsers: number; - activeSessions: number; + // totalUsers: number; + // activeSessions: number; registeredModules: number; healthyModules: number; } diff --git a/src/services/tenant-service.ts b/src/services/tenant-service.ts index b44a561..b07b73e 100644 --- a/src/services/tenant-service.ts +++ b/src/services/tenant-service.ts @@ -72,7 +72,8 @@ export const tenantService = { page: number = 1, limit: number = 20, status?: string | null, - orderBy?: string[] | null + orderBy?: string[] | null, + search?: string | null ): Promise => { const params = new URLSearchParams(); params.append('page', String(page)); @@ -85,6 +86,9 @@ export const tenantService = { params.append('orderBy[]', orderBy[0]); params.append('orderBy[]', orderBy[1]); } + if (search) { + params.append('search', search); + } const response = await apiClient.get(`/tenants?${params.toString()}`); return response.data; },