done ui enhancement
This commit is contained in:
Generated
+1
-16
@@ -71,7 +71,6 @@
|
||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/generator": "^7.28.6",
|
||||
@@ -1794,7 +1793,6 @@
|
||||
"integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -1805,7 +1803,6 @@
|
||||
"integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -1865,7 +1862,6 @@
|
||||
"integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.53.0",
|
||||
"@typescript-eslint/types": "8.53.0",
|
||||
@@ -2113,7 +2109,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2236,7 +2231,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -2606,7 +2600,6 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -3118,7 +3111,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
@@ -3317,8 +3309,7 @@
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/leaflet-control-geocoder": {
|
||||
"version": "3.3.1",
|
||||
@@ -3865,7 +3856,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -3943,7 +3933,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -3953,7 +3942,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -4279,7 +4267,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -4374,7 +4361,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -4505,7 +4491,6 @@
|
||||
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ExternalLink, Box, Search, Rocket } from 'lucide-react';
|
||||
import { moduleApi, type Module } from '../modules/user/UserModuleApi';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CustomInput from '../../components/custom/CustomInput';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { user } = useAuth();
|
||||
@@ -83,31 +84,27 @@ const Dashboard = () => {
|
||||
}, [modules, searchTerm, t]);
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-10 min-h-screen bg-gray-50/50">
|
||||
<div className="space-y-6 min-h-screen">
|
||||
{/* Hero Section */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6 animate-fade-in-up">
|
||||
<div>
|
||||
<h1 className="text-4xl font-extrabold text-(--text-primary) tracking-tight mb-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold text-(--text-primary)">
|
||||
{t('dashboard:title')}, {user?.first_name || 'Admin'}! 👋
|
||||
</h1>
|
||||
<p className="text-lg text-(--text-secondary)">
|
||||
<p className="text-sm text-(--text-secondary)">
|
||||
{t('dashboard:subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full md:w-auto">
|
||||
<div className="relative group w-full md:w-64">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Search className="h-5 w-5 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('dashboard:searchPlaceholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="block w-full pl-10 pr-3 py-2.5 bg-white border border-gray-200 rounded-xl leading-5 text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm hover:shadow-md"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-80">
|
||||
<CustomInput
|
||||
placeholder={t('dashboard:searchPlaceholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
leftIcon={<Search size={18} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,11 +113,12 @@ const Dashboard = () => {
|
||||
<div className="space-y-12">
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="bg-white border border-gray-100 rounded-2xl p-6 h-48 animate-pulse shadow-sm">
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
|
||||
<div key={i} className="bg-(--card-bg) border border-(--card-border) rounded-2xl p-6 h-48 animate-pulse shadow-sm flex flex-col">
|
||||
<div className="h-12 w-12 bg-gray-200 rounded-xl mb-4"></div>
|
||||
<div className="h-6 bg-gray-200 rounded w-3/4 mb-3"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2 mb-auto"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/4 mt-4"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { History, RefreshCcw } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CustomButton, CustomColumnFilter } from "../../components/custom";
|
||||
import { CustomColumnFilter, CustomButton } from "../../components/custom";
|
||||
import { DataTable } from "../../components/custom/CustomTable";
|
||||
import type { ColumnDef } from "../../components/custom/CustomTable";
|
||||
import {
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
SHARED_ADMIN_TABLE_PAGE_SIZE_STORAGE_KEY,
|
||||
resolveStoredTablePageSize,
|
||||
} from "../../lib/tablePageSize";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
|
||||
interface AuditLog extends Record<string, unknown> {
|
||||
id: string;
|
||||
@@ -267,11 +267,14 @@ const LogsPage = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold text-(--text-primary) flex items-center gap-2">
|
||||
<History size={24} /> {t("title")}
|
||||
</h1>
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold text-(--text-primary)">
|
||||
{t("title")}
|
||||
</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Track and audit all system activities and user actions.</p>
|
||||
</div>
|
||||
<CustomButton
|
||||
variant="outlined"
|
||||
onClick={fetchLogs}
|
||||
|
||||
@@ -63,7 +63,7 @@ const ModuleList = () => {
|
||||
|
||||
if (loading && modules.length === 0) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-8 bg-gray-300 rounded w-1/4"></div>
|
||||
<div className="h-64 bg-gray-300 rounded"></div>
|
||||
@@ -73,11 +73,11 @@ const ModuleList = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-(--text-primary)">{t('registry.title')}</h1>
|
||||
<p className="text-(--text-secondary) mt-1">{t('registry.subtitle')}</p>
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold text-(--text-primary)">{t('registry.title')}</h1>
|
||||
<p className="text-sm text-(--text-secondary)">{t('registry.subtitle')}</p>
|
||||
</div>
|
||||
<CustomButton
|
||||
onClick={handleCreate}
|
||||
@@ -108,11 +108,10 @@ const ModuleList = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`flex items-center gap-1 transition-opacity ${
|
||||
togglingId === module.id
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'cursor-pointer hover:text-blue-600'
|
||||
}`}
|
||||
className={`flex items-center gap-1 transition-opacity ${togglingId === module.id
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'cursor-pointer hover:text-blue-600'
|
||||
}`}
|
||||
title={module.status === 'active' ? 'Deactivate' : 'Activate'}
|
||||
onClick={async () => {
|
||||
if (togglingId) return;
|
||||
|
||||
@@ -10,7 +10,7 @@ const TenantModuleAssignment = () => {
|
||||
const [availableModules, setAvailableModules] = useState<Module[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showAssignDialog, setShowAssignDialog] = useState(false);
|
||||
const [selectedModule, setSelectedModule] = useState('');
|
||||
const [selectedModule, setSelectedModule] = useState('');
|
||||
const [selectedEnv, setSelectedEnv] = useState('prod');
|
||||
|
||||
const fetchData = async () => {
|
||||
|
||||
@@ -239,8 +239,8 @@ const ProfilePage: React.FC = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">{t('title')}</h1>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold text-[var(--text-primary)]">{t('title')}</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('subTitle')}
|
||||
</p>
|
||||
|
||||
@@ -196,12 +196,12 @@ const AddRoles = () => {
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<CustomBackButton to="/roles" tooltip={t('backToRoles')} />
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-gray-900">{t('add')}</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('subTitle')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold text-[var(--text-primary)]">{t('add')}</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('subTitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -464,8 +464,9 @@ const AllRoles = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">{t('title')}</h1>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold text-[var(--text-primary)]">{t('title')}</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Define and manage user roles and their associated permissions.</p>
|
||||
</div>
|
||||
<ProtectedComponent requiredAccess="admin.role.create">
|
||||
<Link to="/roles/add">
|
||||
@@ -476,16 +477,13 @@ const AllRoles = () => {
|
||||
|
||||
|
||||
|
||||
{isLoading ? (
|
||||
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
|
||||
<CustomLoader />
|
||||
</div>
|
||||
) : errorMessage ? (
|
||||
{errorMessage ? (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : (
|
||||
<CustomTable
|
||||
isLoading={isLoading}
|
||||
data={roles}
|
||||
columns={columns}
|
||||
columnVisibilityEnabled
|
||||
|
||||
@@ -54,12 +54,11 @@ const SettingsPage: React.FC = () => {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-(--text-primary)">
|
||||
{t('settings.title')}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 mb-6">
|
||||
<h1 className="text-2xl font-bold text-(--text-primary)">
|
||||
{t('settings.title')}
|
||||
</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Manage your account settings and application preferences.</p>
|
||||
</div>
|
||||
|
||||
{/* Language Settings Section */}
|
||||
|
||||
@@ -123,12 +123,14 @@ const AddSubscriptions = () => {
|
||||
<div className="flex items-center gap-4">
|
||||
<CustomBackButton to="/subscriptions" tooltip="Back to Subscriptions" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-gray-900">
|
||||
Add Subscription Plan
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
Create a new plan with pricing and access permissions.
|
||||
</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold text-[var(--text-primary)]">
|
||||
Add Subscription Plan
|
||||
</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Create a new plan with pricing and access permissions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -529,10 +529,9 @@ const AllSubscriptions = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">
|
||||
Subscription Plans
|
||||
</h1>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold text-[var(--text-primary)]">Subscription Plans</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Create and manage different subscription tiers and their permissions.</p>
|
||||
</div>
|
||||
<ProtectedComponent requiredAccess="superadmin.plan.create">
|
||||
<Link to="/subscriptions/add">
|
||||
@@ -541,16 +540,13 @@ const AllSubscriptions = () => {
|
||||
</ProtectedComponent>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
|
||||
<CustomLoader />
|
||||
</div>
|
||||
) : errorMessage ? (
|
||||
{errorMessage ? (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : (
|
||||
<CustomTable
|
||||
isLoading={isLoading}
|
||||
data={plans}
|
||||
columns={columns}
|
||||
columnVisibilityEnabled
|
||||
|
||||
@@ -239,16 +239,16 @@ const AddTenants = () => {
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<CustomBackButton to="/tenants" tooltip="Back to Tenants" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-gray-900">
|
||||
{canCreateTenant ? "Add Tenant" : "Tenant Details"}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
{canCreateTenant
|
||||
? "Create a new tenant with domain and subscription plan."
|
||||
: "Your account is assigned to this tenant."}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold text-[var(--text-primary)]">
|
||||
{canCreateTenant ? "Add Tenant" : "Tenant Details"}
|
||||
</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{canCreateTenant
|
||||
? "Create a new tenant with domain and subscription plan."
|
||||
: "Your account is assigned to this tenant."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
CustomModal,
|
||||
CustomTable,
|
||||
CustomStatus,
|
||||
CustomLoader,
|
||||
CustomActionMenu,
|
||||
CustomActionItem,
|
||||
} from "../../../components/custom";
|
||||
@@ -643,8 +642,9 @@ const AllTenants = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">Tenants</h1>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold text-[var(--text-primary)]">Tenants</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Manage and monitor all tenant accounts in the system.</p>
|
||||
</div>
|
||||
<ProtectedComponent requiredAccess="superadmin.tenant.create">
|
||||
<Link to="/tenants/add">
|
||||
@@ -653,16 +653,13 @@ const AllTenants = () => {
|
||||
</ProtectedComponent>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
|
||||
<CustomLoader />
|
||||
</div>
|
||||
) : errorMessage ? (
|
||||
{errorMessage ? (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : (
|
||||
<CustomTable
|
||||
isLoading={isLoading}
|
||||
data={tenants}
|
||||
columns={columns}
|
||||
columnVisibilityEnabled
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Plus, Edit2, Trash2 } from "lucide-react";
|
||||
import { CustomButton } from "../../../components/custom";
|
||||
import DataTable, { type ColumnDef } from "../../../components/custom/CustomTable";
|
||||
import { CustomActionMenu, CustomActionItem, CustomStatus, CustomConfirmationModal } from "../../../components/custom";
|
||||
import { Loader } from "../../../components/custom/CustomLoader";
|
||||
|
||||
import { paletteApi } from "../PaletteApi";
|
||||
import type { ColorPalette, ColorPalettePayload } from "../ThemeTypes";
|
||||
import PaletteForm from "./PaletteForm";
|
||||
@@ -169,27 +169,24 @@ const AllPalettes: React.FC = () => {
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-(--text-primary)">{t('theme:title')}</h1>
|
||||
{/* <p className="text-sm text-(--text-secondary) mt-1">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold text-(--text-primary)">{t('theme:title')}</h1>
|
||||
<p className="text-sm text-(--text-secondary)">
|
||||
Manage system color themes and default palettes.
|
||||
</p> */}
|
||||
</p>
|
||||
</div>
|
||||
<CustomButton onClick={handleCreate} leftIcon={<Plus size={18} />}>
|
||||
{t('theme:actions.create')}
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
|
||||
<Loader />
|
||||
</div>
|
||||
) : errorMessage ? (
|
||||
{errorMessage ? (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : (
|
||||
<DataTable
|
||||
isLoading={isLoading}
|
||||
data={palettes}
|
||||
columns={columns}
|
||||
columnVisibilityEnabled
|
||||
|
||||
@@ -268,12 +268,12 @@ const AddUsers = () => {
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<CustomBackButton to="/users" tooltip={t('backToUsers')} />
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-(--text-primary)">{t('add')}</h1>
|
||||
<p className="text-sm text-(--text-secondary)">
|
||||
{t('subTitle')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold text-(--text-primary)">{t('add')}</h1>
|
||||
<p className="text-sm text-(--text-secondary)">
|
||||
{t('subTitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
CustomInput,
|
||||
CustomModal,
|
||||
CustomTable,
|
||||
CustomLoader,
|
||||
|
||||
CustomActionMenu,
|
||||
CustomActionItem,
|
||||
CustomStatus,
|
||||
@@ -587,8 +587,9 @@ const AllUsers = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">{t('title')}</h1>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-bold text-[var(--text-primary)]">{t('title')}</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Manage system users, their roles, and account status.</p>
|
||||
</div>
|
||||
<ProtectedComponent requiredAccess="admin.user.create">
|
||||
<Link to="/users/add">
|
||||
@@ -599,16 +600,13 @@ const AllUsers = () => {
|
||||
|
||||
|
||||
|
||||
{isLoading ? (
|
||||
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
|
||||
<CustomLoader />
|
||||
</div>
|
||||
) : errorMessage ? (
|
||||
{errorMessage ? (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : (
|
||||
<CustomTable
|
||||
isLoading={isLoading}
|
||||
data={users}
|
||||
columns={columns}
|
||||
columnVisibilityEnabled
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
persistTablePageSize,
|
||||
resolveStoredTablePageSize,
|
||||
} from "../../lib/tablePageSize";
|
||||
import Loader from "./CustomLoader";
|
||||
|
||||
function cn(...parts: Array<string | false | null | undefined>) {
|
||||
return parts.filter(Boolean).join(" ");
|
||||
@@ -701,12 +702,8 @@ export function DataTable<T extends Record<string, unknown>>(
|
||||
colSpan={visibleColumns.length}
|
||||
className="px-6 py-24 text-center text-[var(--text-secondary)]"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<svg className="animate-spin h-8 w-8 text-blue-500 mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<p>{t('common.loading')}</p>
|
||||
<div className="flex flex-col items-center justify-center min-h-[100px]">
|
||||
<Loader />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import {
|
||||
Menu,
|
||||
ChevronDown,
|
||||
LogOut,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useSidebar } from "../../context/SidebarContext";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { authApi } from "../../application/authentication/AuthApi";
|
||||
import { clearAuthCookies } from "../../lib/authCookies";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
const AppHeader: React.FC = () => {
|
||||
const { t } = useTranslation(['profile', 'common']);
|
||||
const navigate = useNavigate();
|
||||
const { user, } = useAuth();
|
||||
const { user } = useAuth();
|
||||
const { toggleMobileSidebar } = useSidebar();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
||||
const userMenuRef = useRef<HTMLDivElement>(null);
|
||||
@@ -50,12 +52,12 @@ const AppHeader: React.FC = () => {
|
||||
: "U";
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-40 flex w-full bg-[var(--header-bg)] border-b border-[var(--header-border)] transition-colors duration-200">
|
||||
<header className="sticky top-0 z-40 flex w-full bg-white/80 backdrop-blur-md border-b border-gray-200 transition-all duration-200">
|
||||
<div className="flex flex-grow items-center gap-4 px-4 py-3 lg:px-6">
|
||||
{/* LEFT: Mobile Toggle & Brand (Mobile only) */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className="flex items-center justify-center p-2 text-[var(--header-text)] hover:bg-[var(--header-border)] rounded-lg lg:hidden transition-colors"
|
||||
className="flex items-center justify-center p-2 text-gray-500 hover:bg-gray-100 rounded-lg lg:hidden transition-colors"
|
||||
onClick={toggleMobileSidebar}
|
||||
aria-label="Toggle Sidebar"
|
||||
>
|
||||
@@ -63,67 +65,12 @@ const AppHeader: React.FC = () => {
|
||||
</button>
|
||||
|
||||
<Link to="/" className="lg:hidden flex items-center gap-2">
|
||||
<span className="font-bold text-[var(--header-text)] text-lg">SaaS</span>
|
||||
<span className="font-bold text-gray-900 text-lg">SaaS</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Logistics Network Pattern - Multi-layer Design */}
|
||||
<div className="hidden lg:flex items-center overflow-hidden">
|
||||
{/* ... existing SVG content ... */}
|
||||
<svg className="w-80 h-10" viewBox="0 0 320 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Background grid pattern */}
|
||||
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
|
||||
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="currentColor" strokeOpacity="0.1" strokeWidth="0.5" className="text-[var(--header-text)]" />
|
||||
</pattern>
|
||||
<rect width="320" height="40" fill="url(#grid)" />
|
||||
|
||||
{/* Multiple route paths */}
|
||||
<path d="M0 30 C40 30, 60 10, 100 10 S160 30, 200 20 S260 10, 320 15" stroke="currentColor" strokeOpacity="0.2" strokeWidth="2" fill="none" className="text-[var(--header-text)]" />
|
||||
<path d="M0 20 C50 15, 80 25, 120 20 S180 10, 220 15 S280 25, 320 20" stroke="currentColor" strokeOpacity="0.3" strokeWidth="1.5" fill="none" className="text-[var(--header-text)]" />
|
||||
<path d="M0 10 C30 15, 70 5, 110 12 S170 20, 210 12 S270 8, 320 12" stroke="currentColor" strokeOpacity="0.4" strokeWidth="1" strokeDasharray="4 2" fill="none" className="text-[var(--header-text)]" />
|
||||
|
||||
{/* Hub nodes with glow effect */}
|
||||
<circle cx="40" cy="25" r="6" fill="currentColor" fillOpacity="0.1" stroke="currentColor" strokeOpacity="0.4" strokeWidth="1.5" className="text-[var(--header-text)]" />
|
||||
<circle cx="40" cy="25" r="3" fill="currentColor" className="text-[var(--primary)]" />
|
||||
|
||||
<circle cx="120" cy="15" r="8" fill="currentColor" fillOpacity="0.1" stroke="currentColor" strokeOpacity="0.5" strokeWidth="2" className="text-[var(--header-text)]" />
|
||||
<circle cx="120" cy="15" r="4" fill="currentColor" className="text-[var(--primary)]" />
|
||||
|
||||
<circle cx="200" cy="18" r="6" fill="currentColor" fillOpacity="0.1" stroke="currentColor" strokeOpacity="0.4" strokeWidth="1.5" className="text-[var(--header-text)]" />
|
||||
<circle cx="200" cy="18" r="3" fill="currentColor" className="text-[var(--primary)]" />
|
||||
|
||||
<circle cx="280" cy="16" r="5" fill="currentColor" fillOpacity="0.1" stroke="currentColor" strokeOpacity="0.3" strokeWidth="1" className="text-[var(--header-text)]" />
|
||||
<circle cx="280" cy="16" r="2.5" fill="currentColor" className="text-[var(--primary)]" />
|
||||
|
||||
{/* Package icons */}
|
||||
<g transform="translate(70, 22)">
|
||||
<rect width="10" height="8" rx="1" fill="currentColor" fillOpacity="0.2" stroke="currentColor" strokeWidth="0.8" className="text-[var(--header-text)]" />
|
||||
<line x1="0" y1="3" x2="10" y2="3" stroke="currentColor" strokeWidth="0.5" className="text-[var(--header-text)]" />
|
||||
<line x1="5" y1="3" x2="5" y2="8" stroke="currentColor" strokeWidth="0.5" className="text-[var(--header-text)]" />
|
||||
</g>
|
||||
|
||||
<g transform="translate(160, 8)">
|
||||
<rect width="10" height="8" rx="1" fill="currentColor" fillOpacity="0.2" stroke="currentColor" strokeWidth="0.8" className="text-(--header-text)" />
|
||||
<line x1="0" y1="3" x2="10" y2="3" stroke="currentColor" strokeWidth="0.5" className="text-(--header-text)" />
|
||||
<line x1="5" y1="3" x2="5" y2="8" stroke="currentColor" strokeWidth="0.5" className="text-(--header-text)" />
|
||||
</g>
|
||||
|
||||
<g transform="translate(240, 20)">
|
||||
<rect width="8" height="6" rx="1" fill="currentColor" fillOpacity="0.2" stroke="currentColor" strokeWidth="0.8" className="text-(--header-text)" />
|
||||
<line x1="0" y1="2" x2="8" y2="2" stroke="currentColor" strokeWidth="0.5" className="text-(--header-text)" />
|
||||
</g>
|
||||
|
||||
{/* Connecting dots */}
|
||||
<circle cx="20" cy="22" r="1.5" fill="currentColor" fillOpacity="0.5" className="text-(--header-text)" />
|
||||
<circle cx="90" cy="12" r="1.5" fill="currentColor" fillOpacity="0.5" className="text-(--header-text)" />
|
||||
<circle cx="150" cy="22" r="1.5" fill="currentColor" fillOpacity="0.5" className="text-(--header-text)" />
|
||||
<circle cx="230" cy="14" r="1.5" fill="currentColor" fillOpacity="0.5" className="text-(--header-text)" />
|
||||
<circle cx="300" cy="18" r="1.5" fill="currentColor" fillOpacity="0.5" className="text-(--header-text)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* RIGHT: Actions */}
|
||||
<div className="flex items-center gap-2 sm:gap-4 ms-auto">
|
||||
<div className="flex items-center gap-2 sm:gap-4 ltr:ml-auto rtl:mr-auto">
|
||||
|
||||
{/* User Menu */}
|
||||
<div className="relative" ref={userMenuRef}>
|
||||
@@ -132,11 +79,10 @@ const AppHeader: React.FC = () => {
|
||||
onClick={() => setIsUserMenuOpen((prev) => !prev)}
|
||||
className="
|
||||
flex items-center gap-2
|
||||
pl-2 pr-1 py-1.5
|
||||
rounded-lg
|
||||
hover:bg-(--header-border)
|
||||
transition-colors
|
||||
border border-transparent hover:border-(--header-border)
|
||||
p-1 pr-3
|
||||
rounded-full border border-gray-200
|
||||
hover:border-gray-300 hover:bg-gray-50
|
||||
transition-all bg-white/50
|
||||
"
|
||||
aria-expanded={isUserMenuOpen}
|
||||
>
|
||||
@@ -144,12 +90,12 @@ const AppHeader: React.FC = () => {
|
||||
{initials}
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block text-left me-1">
|
||||
<p className="text-sm font-medium text-(--header-text) leading-none">{fullName}</p>
|
||||
<div className="hidden md:block ltr:text-left rtl:text-right ltr:mr-1 rtl:ml-1 leading-tight">
|
||||
<p className="text-sm font-semibold text-gray-700">{fullName}</p>
|
||||
</div>
|
||||
|
||||
<ChevronDown
|
||||
size={16}
|
||||
size={14}
|
||||
className={`text-gray-400 transition-transform duration-200 ${isUserMenuOpen ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
@@ -159,33 +105,45 @@ const AppHeader: React.FC = () => {
|
||||
{isUserMenuOpen && (
|
||||
<div
|
||||
className="
|
||||
absolute end-0 mt-2 w-56
|
||||
rounded-xl border border-(--card-border)
|
||||
bg-(--card-bg)
|
||||
shadow-md
|
||||
absolute ltr:right-0 rtl:left-0 mt-2 w-64
|
||||
rounded-xl border border-gray-100
|
||||
bg-white
|
||||
shadow-xl
|
||||
overflow-hidden
|
||||
animate-in fade-in zoom-in-95 duration-150
|
||||
origin-top-right
|
||||
ltr:origin-top-right rtl:origin-top-left
|
||||
z-50
|
||||
"
|
||||
>
|
||||
<div className="px-4 py-3 border-b border-(--card-border) bg-(--background)">
|
||||
<p className="text-xs text-(--text-secondary) truncate mt-0.5">
|
||||
<div className="px-4 py-3 border-b border-gray-100 bg-gray-50/50">
|
||||
<p className="text-sm font-semibold text-gray-900 truncate">
|
||||
{user?.email}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 truncate mt-0.5 uppercase tracking-wider">
|
||||
{user?.role?.role_name || "User"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="py-1">
|
||||
<Link
|
||||
to="/profile"
|
||||
onClick={() => setIsUserMenuOpen(false)}
|
||||
className="flex w-full items-center gap-2 px-4 py-2 text-sm text-(--text-primary) hover:bg-(--background-secondary) transition-colors"
|
||||
className="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg bg-gray-100 flex items-center justify-center text-gray-500">
|
||||
<ChevronDown size={16} className="-rotate-90" />
|
||||
</div>
|
||||
{t('profile:header.profileSettings')}
|
||||
</Link>
|
||||
|
||||
<div className="h-px bg-gray-100 my-1 mx-2"></div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex w-full items-center gap-2 px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors"
|
||||
className="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 transition-colors"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg bg-red-50 flex items-center justify-center text-red-600">
|
||||
<LogOut size={16} />
|
||||
</div>
|
||||
{t('profile:header.signOut')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,15 @@ import { SidebarProvider, useSidebar } from "../../context/SidebarContext";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import AppHeader from "./AppHeader";
|
||||
import AppSidebar from "./AppSidebar";
|
||||
import Backdrop from "./Backdrop";
|
||||
|
||||
const LayoutContent: React.FC = () => {
|
||||
const { isExpanded } = useSidebar();
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--background)] transition-colors duration-200 flex flex-col">
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col transition-all duration-300">
|
||||
<AppSidebar />
|
||||
<Backdrop />
|
||||
|
||||
<div
|
||||
className={`
|
||||
@@ -20,9 +20,10 @@ const LayoutContent: React.FC = () => {
|
||||
>
|
||||
<AppHeader />
|
||||
|
||||
<main className="flex-1 p-4 md:p-6 lg:p-6 animate-fade-in w-full max-w-[1920px] mx-auto">
|
||||
{/* Outlet Container - Ensures content is constrained but responsive */}
|
||||
<Outlet />
|
||||
<main className="flex-1 p-4 sm:p-6 lg:p-8 animate-in fade-in duration-500">
|
||||
<div className="max-w-[1600px] mx-auto w-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CreditCard,
|
||||
FileText,
|
||||
} from "lucide-react";
|
||||
import { useSidebar } from "../../context/SidebarContext";
|
||||
import { usePermission } from "../../lib/usePermission";
|
||||
@@ -82,7 +83,7 @@ const navItems: NavItem[] = [
|
||||
access: "modules.view",
|
||||
},
|
||||
{
|
||||
icon: <LayoutGrid size={22} />,
|
||||
icon: <FileText size={22} />,
|
||||
name: "Logs",
|
||||
path: "/logs",
|
||||
access: "admin.logs.read",
|
||||
@@ -124,6 +125,7 @@ const AppSidebar: React.FC = () => {
|
||||
'Settings': 'settings',
|
||||
'Modules': 'modules',
|
||||
'Logs': 'logs',
|
||||
'Subscriptions': 'subscriptions',
|
||||
};
|
||||
|
||||
const key = keyMap[name];
|
||||
@@ -131,7 +133,6 @@ const AppSidebar: React.FC = () => {
|
||||
return t(`nav.${key}`, name);
|
||||
}
|
||||
|
||||
// Try other namespaces if not in nav map
|
||||
return t([
|
||||
`modules:${name}`,
|
||||
`dashboard:${name}`,
|
||||
@@ -140,22 +141,6 @@ const AppSidebar: React.FC = () => {
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
const isActive = useCallback(
|
||||
(path: string) => location.pathname === path || location.pathname.startsWith(path + "/"),
|
||||
[location.pathname]
|
||||
);
|
||||
|
||||
const isSubmenuRouteActive = useCallback(
|
||||
(submenu?: SubMenuItem[]) =>
|
||||
submenu?.some(
|
||||
(subItem) =>
|
||||
location.pathname === subItem.path ||
|
||||
location.pathname.startsWith(subItem.path + "/")
|
||||
) ?? false,
|
||||
[location.pathname]
|
||||
);
|
||||
|
||||
const visibleNavItems = useMemo(
|
||||
() => navItems.filter((item) => {
|
||||
if (!item.access) return true;
|
||||
@@ -169,11 +154,55 @@ const AppSidebar: React.FC = () => {
|
||||
[can]
|
||||
);
|
||||
|
||||
const isActive = useCallback(
|
||||
(path: string) => location.pathname === path || location.pathname.startsWith(path + "/"),
|
||||
[location.pathname]
|
||||
);
|
||||
|
||||
const groupedNavItems = useMemo(() => {
|
||||
const groups: { label: string; items: NavItem[] }[] = [];
|
||||
const addedPaths = new Set<string>();
|
||||
|
||||
const getItems = (names: string[]) => {
|
||||
const filtered = visibleNavItems.filter(item =>
|
||||
names.includes(item.name) && !addedPaths.has(item.path)
|
||||
);
|
||||
filtered.forEach(item => addedPaths.add(item.path));
|
||||
return filtered;
|
||||
};
|
||||
|
||||
// 1. Overview
|
||||
const dashboardItems = getItems(["Dashboard"]);
|
||||
if (dashboardItems.length > 0) {
|
||||
groups.push({ label: t('nav.groups.overview', 'Overview'), items: dashboardItems });
|
||||
}
|
||||
|
||||
// 2. Management
|
||||
const managementItems = getItems(["Tenants", "Subscriptions", "Roles", "Users"]);
|
||||
if (managementItems.length > 0) {
|
||||
groups.push({ label: t('nav.groups.management', 'Management'), items: managementItems });
|
||||
}
|
||||
|
||||
// 3. System
|
||||
const systemItems = getItems(["Themes", "Modules", "Logs", "Settings"]);
|
||||
if (systemItems.length > 0) {
|
||||
groups.push({ label: t('nav.groups.system', 'System'), items: systemItems });
|
||||
}
|
||||
|
||||
// Catch-all
|
||||
const remaining = visibleNavItems.filter(item => !addedPaths.has(item.path));
|
||||
if (remaining.length > 0) {
|
||||
groups.push({ label: t('nav.groups.other', 'Other'), items: remaining });
|
||||
}
|
||||
|
||||
return groups;
|
||||
}, [visibleNavItems, t]);
|
||||
|
||||
const [openSubmenus, setOpenSubmenus] = useState<Record<string, boolean>>({});
|
||||
|
||||
const toggleSubmenu = (name: string) => {
|
||||
if (!isExpanded && !isMobile) {
|
||||
toggleSidebar(); // Auto expand when clicking submenu in collapsed state
|
||||
toggleSidebar();
|
||||
}
|
||||
setOpenSubmenus(prev => ({ ...prev, [name]: !prev[name] }));
|
||||
};
|
||||
@@ -192,25 +221,15 @@ const AppSidebar: React.FC = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sidebar width logic
|
||||
const sidebarWidth = isExpanded ? "w-[260px]" : "w-[80px]";
|
||||
const mobileClasses = isMobileOpen ? "translate-x-0" : "-translate-x-full";
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile Backdrop */}
|
||||
{isMobileOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-40 lg:hidden backdrop-blur-sm transition-opacity duration-300 animate-fade-in"
|
||||
onClick={closeMobileSidebar}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
<aside
|
||||
className={`
|
||||
fixed top-0 left-0 z-50 h-screen bg-[var(--sidebar-bg)] text-[var(--sidebar-text)] flex flex-col
|
||||
transition-all duration-300 ease-in-out border-r border-[var(--sidebar-border)]
|
||||
transition-all duration-300 ease-in-out border-r border-[var(--sidebar-border)]
|
||||
lg:translate-x-0 overflow-x-hidden
|
||||
${isMobile ? "w-[280px]" : sidebarWidth}
|
||||
${isMobile ? mobileClasses : ""}
|
||||
@@ -218,8 +237,7 @@ const AppSidebar: React.FC = () => {
|
||||
`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className={`h-[70px] flex items-center px-4 border-b border-(--sidebar-border)/50 ${!isExpanded && !isMobile ? 'justify-center' : 'justify-between'}`}>
|
||||
{/* Logo - Only show when expanded or mobile */}
|
||||
<div className={`h-[70px] flex items-center px-4 border-b border-white/5 ${!isExpanded && !isMobile ? 'justify-center' : 'justify-between'}`}>
|
||||
{(isExpanded || isMobile) && (
|
||||
<Link to="/dashboard" className="flex items-center gap-3 overflow-hidden ml-1" onClick={isMobile ? closeMobileSidebar : undefined}>
|
||||
{user?.role?.accesses?.includes("superadmin.tenant.read") ? (
|
||||
@@ -235,163 +253,166 @@ const AppSidebar: React.FC = () => {
|
||||
className="h-8 w-auto object-contain"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="shrink-0 text-blue-400">
|
||||
<Package size={32} />
|
||||
</div>
|
||||
<span className="text-xl font-bold text-(--sidebar-active-text) whitespace-nowrap">
|
||||
SaaS
|
||||
</span>
|
||||
</>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="shrink-0 w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center text-white font-bold">S</div>
|
||||
<span className="text-xl font-bold text-white whitespace-nowrap">SaaS</span>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Desktop Toggle Button */}
|
||||
{!isMobile && (
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className={`p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-white/10 transition-colors ${!isExpanded ? "" : ""}`}
|
||||
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-white/10 transition-colors"
|
||||
aria-label="Toggle Sidebar"
|
||||
>
|
||||
<PanelLeft size={20} className={!isExpanded ? "rotate-180" : ""} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Mobile Close Button */}
|
||||
{isMobile && (
|
||||
<button onClick={closeMobileSidebar} className="p-1 rounded-md text-slate-400 hover:text-white hover:bg-white/10">
|
||||
<button
|
||||
onClick={closeMobileSidebar}
|
||||
className="p-1 rounded-md text-slate-400 hover:text-white hover:bg-white/10"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 overflow-y-auto overflow-x-hidden py-6 px-3 custom-scrollbar">
|
||||
<ul className="space-y-2">
|
||||
{visibleNavItems.map((nav) => {
|
||||
const active = isActive(nav.path);
|
||||
const hasSubmenu = nav.submenu && nav.submenu.length > 0;
|
||||
const isMenuOpen = openSubmenus[nav.name] || isSubmenuRouteActive(nav.submenu);
|
||||
<nav className="flex-1 overflow-y-auto overflow-x-hidden py-4 px-3 custom-scrollbar">
|
||||
<div className="flex flex-col gap-6">
|
||||
{groupedNavItems.map((group, idx) => (
|
||||
<div key={idx} className="space-y-1.5">
|
||||
{(isExpanded || isMobile) && (
|
||||
<p className="px-3 text-[10px] font-bold text-slate-500 uppercase tracking-[0.15em] mb-2 mt-2">
|
||||
{group.label}
|
||||
</p>
|
||||
)}
|
||||
|
||||
if (hasSubmenu) {
|
||||
return (
|
||||
<li key={nav.name}>
|
||||
<button
|
||||
onClick={() => toggleSubmenu(nav.name)}
|
||||
className={`
|
||||
w-full relative flex items-center gap-3 px-3 py-3 rounded-lg transition-all duration-200 group
|
||||
${isMenuOpen
|
||||
? "bg-white/5 text-(--sidebar-active-text)"
|
||||
: "text-(--sidebar-text) hover:bg-(--sidebar-hover-bg)"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
{nav.icon}
|
||||
</div>
|
||||
<ul className="space-y-1.5">
|
||||
{group.items.map((nav) => {
|
||||
const active = isActive(nav.path);
|
||||
const hasSubmenu = nav.submenu && nav.submenu.length > 0;
|
||||
const isMenuOpen = openSubmenus[nav.name];
|
||||
|
||||
{(isExpanded || isMobile) && (
|
||||
<>
|
||||
<span className="flex-1 text-left whitespace-nowrap font-medium">
|
||||
if (hasSubmenu) {
|
||||
return (
|
||||
<li key={nav.name}>
|
||||
<button
|
||||
onClick={() => toggleSubmenu(nav.name)}
|
||||
className={`
|
||||
w-full relative flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200 group
|
||||
${isMenuOpen
|
||||
? "bg-white/5 text-white"
|
||||
: "text-slate-400 hover:bg-white/5 hover:text-white"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
{nav.icon}
|
||||
</div>
|
||||
|
||||
{(isExpanded || isMobile) && (
|
||||
<>
|
||||
<span className="flex-1 text-left whitespace-nowrap font-medium text-sm">
|
||||
{getNavTranslation(nav.name)}
|
||||
</span>
|
||||
<span className="flex-shrink-0 transition-transform duration-200">
|
||||
{isMenuOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className={`overflow-hidden transition-all duration-300 ${isMenuOpen && (isExpanded || isMobile) ? "max-h-[800px] opacity-100 mt-1" : "max-h-0 opacity-0"}`}>
|
||||
<ul className="space-y-1 pl-4 mt-1 border-l border-white/5 ml-4">
|
||||
{nav.submenu?.filter(subItem => !subItem.access || can(subItem.access)).map((subItem) => {
|
||||
const subActive = isActive(subItem.path || "");
|
||||
return (
|
||||
<li key={subItem.name}>
|
||||
<Link
|
||||
to={subItem.path || "#"}
|
||||
onClick={isMobile ? closeMobileSidebar : undefined}
|
||||
className={`
|
||||
flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors block
|
||||
${subActive
|
||||
? "text-white font-bold"
|
||||
: "text-slate-500 hover:text-slate-300 hover:bg-white/5"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{subItem.icon && (
|
||||
<div className="flex-shrink-0 scale-75">
|
||||
{subItem.icon}
|
||||
</div>
|
||||
)}
|
||||
<span className="truncate">{getNavTranslation(subItem.name)}</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={nav.name}>
|
||||
<Link
|
||||
to={nav.path}
|
||||
onClick={isMobile ? closeMobileSidebar : undefined}
|
||||
className={`
|
||||
relative flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200 group
|
||||
${active
|
||||
? "bg-white text-slate-900"
|
||||
: "text-slate-400 hover:bg-white/5 hover:text-white"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
{nav.icon}
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={`
|
||||
whitespace-nowrap font-semibold text-sm transition-all duration-300
|
||||
${(isExpanded || isMobile) ? "opacity-100 w-auto" : "opacity-0 w-0 hidden"}
|
||||
`}
|
||||
>
|
||||
{getNavTranslation(nav.name)}
|
||||
</span>
|
||||
<span className="flex-shrink-0 transition-transform duration-200">
|
||||
{isMenuOpen ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{!isExpanded && !isMobile && (
|
||||
<div className="absolute left-full ml-4 px-2 py-1 bg-slate-800 text-white text-xs rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-50 whitespace-nowrap pointer-events-none">
|
||||
{getNavTranslation(nav.name)}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Submenu Items */}
|
||||
<div className={`overflow-hidden transition-all duration-300 ${isMenuOpen && (isExpanded || isMobile) ? "max-h-96 opacity-100 mt-1" : "max-h-0 opacity-0"}`}>
|
||||
<ul className="space-y-1 pl-9">
|
||||
{nav.submenu?.filter(subItem => !subItem.access || can(subItem.access)).map((subItem) => {
|
||||
const subActive = isActive(subItem.path || "");
|
||||
return (
|
||||
<li key={subItem.name}>
|
||||
<Link
|
||||
to={subItem.path || "#"}
|
||||
onClick={isMobile ? closeMobileSidebar : undefined}
|
||||
className={`
|
||||
flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors block
|
||||
${subActive
|
||||
? "bg-(--sidebar-active-bg) text-(--sidebar-active-text) font-medium"
|
||||
: "text-(--sidebar-text-muted) hover:text-(--sidebar-text) hover:bg-(--sidebar-hover-bg)"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{subItem.icon && (
|
||||
<div className="flex-shrink-0 scale-90">
|
||||
{subItem.icon}
|
||||
</div>
|
||||
)}
|
||||
<span className="truncate">{getNavTranslation(subItem.name)}</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={nav.name}>
|
||||
<Link
|
||||
to={nav.path}
|
||||
onClick={isMobile ? closeMobileSidebar : undefined}
|
||||
className={`
|
||||
relative flex items-center gap-3 px-3 py-3 rounded-lg transition-all duration-200 group
|
||||
${active
|
||||
? "bg-(--sidebar-active-bg) text-(--sidebar-active-text)"
|
||||
: "text-(--sidebar-text) hover:bg-(--sidebar-hover-bg)"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
{nav.icon}
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={`
|
||||
whitespace-nowrap font-medium transition-all duration-300
|
||||
${(isExpanded || isMobile) ? "opacity-100 w-auto" : "opacity-0 w-0 hidden"}
|
||||
`}
|
||||
>
|
||||
{getNavTranslation(nav.name)}
|
||||
</span>
|
||||
|
||||
{/* Tooltip for collapsed mode */}
|
||||
{!isExpanded && !isMobile && (
|
||||
<div className="absolute left-full ml-4 px-2 py-1 bg-slate-800 text-white text-xs rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-50 whitespace-nowrap pointer-events-none">
|
||||
{getNavTranslation(nav.name)}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{!isExpanded && !isMobile && (
|
||||
<div className="absolute left-full ml-4 px-2 py-1 bg-slate-800 text-white text-xs rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-50 whitespace-nowrap pointer-events-none">
|
||||
{getNavTranslation(nav.name)}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-slate-700/50">
|
||||
{/* Logout */}
|
||||
<div className="p-4 border-t border-white/5">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={`flex items-center gap-3 w-full px-3 py-2 text-red-400 hover:text-red-300 hover:bg-red-500/10 rounded-lg transition-colors overflow-hidden ${(isExpanded || isMobile) ? "justify-start" : "justify-center"}`}
|
||||
className={`group flex items-center gap-3 w-full px-3 py-2.5 text-slate-400 hover:text-white hover:bg-white/5 rounded-xl transition-all overflow-hidden ${(isExpanded || isMobile) ? "justify-start" : "justify-center"}`}
|
||||
title="Logout"
|
||||
>
|
||||
<LogOut size={20} />
|
||||
<div className="flex-shrink-0 transition-colors group-hover:text-white">
|
||||
<LogOut size={20} />
|
||||
</div>
|
||||
{(isExpanded || isMobile) && (
|
||||
<span className="font-medium">{t('actions.signout')}</span>
|
||||
<span className="font-medium text-sm">Sign Out</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ const Backdrop: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-gray-900/50 lg:hidden"
|
||||
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm lg:hidden animate-in fade-in duration-300"
|
||||
onClick={toggleMobileSidebar}
|
||||
/>
|
||||
);
|
||||
|
||||
+12
-2
@@ -1,4 +1,4 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,100..900;1,100..900&display=swap");
|
||||
@import "tailwindcss";
|
||||
|
||||
@keyframes fadeIn {
|
||||
@@ -40,16 +40,26 @@
|
||||
--text-secondary: #64748B;
|
||||
--card-bg: #FFFFFF;
|
||||
--card-border: #E2E8F0;
|
||||
|
||||
/* Optional: smoother focus ring for any inputs inside your custom components */
|
||||
--ui-ring: rgba(17, 24, 39, 0.12); /* gray-900/12 */
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-family: "Archivo", sans-serif;
|
||||
@apply text-base antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Optional: smoother focus ring for any inputs inside your custom components */
|
||||
.ui-focus :is(input, textarea, select, button):focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--ui-ring);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* Hide number input arrows/spinners */
|
||||
.no-spinner::-webkit-inner-spin-button,
|
||||
.no-spinner::-webkit-outer-spin-button {
|
||||
|
||||
@@ -7,6 +7,10 @@ export default defineConfig(({ mode }) => {
|
||||
console.log(`\n 🔌 Backend API URL: \x1b[36m${env.VITE_API_BASE_URL}\x1b[0m\n`);
|
||||
|
||||
return {
|
||||
server: {
|
||||
port: 5174,
|
||||
|
||||
},
|
||||
plugins: [react(), tailwindcss()],
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user