Autosave: 20260407-151714
This commit is contained in:
parent
e34c261c25
commit
818e7d5818
BIN
assets/pasted-20260407-150901-3e5110b3.png
Normal file
BIN
assets/pasted-20260407-150901-3e5110b3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
@ -135,6 +135,11 @@ const menuAside: MenuAsideItem[] = [
|
||||
icon: 'mdiFolderMultipleImage' in icon ? icon['mdiFolderMultipleImage' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_ASSETS'
|
||||
},
|
||||
{
|
||||
href: '/mai-server',
|
||||
label: 'MAi Server',
|
||||
icon: icon.mdiServer,
|
||||
},
|
||||
{
|
||||
href: '/profile',
|
||||
label: 'Profile',
|
||||
|
||||
336
frontend/src/pages/mai-server.tsx
Normal file
336
frontend/src/pages/mai-server.tsx
Normal file
@ -0,0 +1,336 @@
|
||||
import * as icon from '@mdi/js';
|
||||
import Head from 'next/head';
|
||||
import React from 'react';
|
||||
import axios from 'axios';
|
||||
import type { ReactElement } from 'react';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import CardBox from '../components/CardBox';
|
||||
import FormField from '../components/FormField';
|
||||
import SectionMain from '../components/SectionMain';
|
||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
||||
import { getPageTitle } from '../config';
|
||||
import { hasPermission } from '../helpers/userPermissions';
|
||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
|
||||
type MetricState = Record<'profiles' | 'conversations' | 'messages' | 'announcements', string>;
|
||||
|
||||
type MetricConfig = {
|
||||
key: keyof MetricState;
|
||||
title: string;
|
||||
detail: string;
|
||||
endpoint: string;
|
||||
permission: string;
|
||||
iconPath: string;
|
||||
};
|
||||
|
||||
const moduleLabels = [
|
||||
'Ai',
|
||||
'AS Ai',
|
||||
'MAI',
|
||||
'MAI SERVER ALIYO MOMOT',
|
||||
'D MOTHERBOARD',
|
||||
'D RAM',
|
||||
'D ROM',
|
||||
'D WLAN',
|
||||
'ALIYO MOMOT BANK ALIYO MOMOT',
|
||||
];
|
||||
|
||||
const readinessItems = [
|
||||
{ label: 'Register', description: 'Account onboarding is available through the existing sign up flow.' },
|
||||
{ label: 'Authentification', description: 'Session access can be enabled directly from this control page.' },
|
||||
{ label: 'Profile', description: 'Profile data stays synced with the authenticated application user.' },
|
||||
{ label: 'Password', description: 'Password reset and recovery reuse the built-in auth endpoints.' },
|
||||
{ label: 'Country', description: 'Regional profile details can be completed from the profile screen.' },
|
||||
];
|
||||
|
||||
const adminItems = [
|
||||
{ label: 'Admin', description: 'Administrator-level visibility is preserved through the role model.' },
|
||||
{ label: 'Profile database', description: 'Profile records are available through the existing user management module.' },
|
||||
{ label: 'iSecurity', description: 'Email verification and JWT session protection stay active for secure access.' },
|
||||
{ label: 'iManager', description: 'Operational shortcuts stay routed through dashboard, profile, and entity pages.' },
|
||||
{ label: 'iAuthentifier', description: 'Authentication status is surfaced clearly for the current session.' },
|
||||
];
|
||||
|
||||
const metricConfigs: MetricConfig[] = [
|
||||
{
|
||||
key: 'profiles',
|
||||
title: 'Profiles',
|
||||
detail: 'Registered profile records',
|
||||
endpoint: '/users/count',
|
||||
permission: 'READ_USERS',
|
||||
iconPath: icon.mdiAccountGroup,
|
||||
},
|
||||
{
|
||||
key: 'conversations',
|
||||
title: 'Marketing conversations',
|
||||
detail: 'Lead and outreach threads',
|
||||
endpoint: '/conversations/count',
|
||||
permission: 'READ_CONVERSATIONS',
|
||||
iconPath: icon.mdiChartTimelineVariant,
|
||||
},
|
||||
{
|
||||
key: 'messages',
|
||||
title: 'Message flow',
|
||||
detail: 'Communication volume',
|
||||
endpoint: '/messages/count',
|
||||
permission: 'READ_MESSAGES',
|
||||
iconPath: icon.mdiMessageTextOutline,
|
||||
},
|
||||
{
|
||||
key: 'announcements',
|
||||
title: 'Marketing broadcasts',
|
||||
detail: 'Announcement and campaign records',
|
||||
endpoint: '/announcements/count',
|
||||
permission: 'READ_ANNOUNCEMENTS',
|
||||
iconPath: icon.mdiBullhornOutline,
|
||||
},
|
||||
];
|
||||
|
||||
const MaiServerPage = () => {
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
||||
const corners = useAppSelector((state) => state.style.corners);
|
||||
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
|
||||
|
||||
const [isAuthenticationEnabled, setIsAuthenticationEnabled] = React.useState(false);
|
||||
const [metrics, setMetrics] = React.useState<MetricState>({
|
||||
profiles: '—',
|
||||
conversations: '—',
|
||||
messages: '—',
|
||||
announcements: '—',
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentUser) return;
|
||||
|
||||
const loadMetrics = async () => {
|
||||
const requests = metricConfigs.map(async (metric) => {
|
||||
if (!hasPermission(currentUser, metric.permission)) {
|
||||
return { key: metric.key, value: 'Private' };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(metric.endpoint);
|
||||
return { key: metric.key, value: String(response.data?.count ?? 0) };
|
||||
} catch (error) {
|
||||
console.error(`Failed to load ${metric.key} metric`, error);
|
||||
return { key: metric.key, value: 'Unavailable' };
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(requests);
|
||||
|
||||
setMetrics((previous) => {
|
||||
const next = { ...previous };
|
||||
|
||||
results.forEach((result) => {
|
||||
next[result.key] = result.value;
|
||||
});
|
||||
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
loadMetrics();
|
||||
}, [currentUser]);
|
||||
|
||||
const fullName = [currentUser?.firstName, currentUser?.lastName].filter(Boolean).join(' ');
|
||||
const displayName = fullName || currentUser?.email || 'Authenticated user';
|
||||
const emailAddress = currentUser?.email || 'No email connected';
|
||||
const provider = currentUser?.provider || 'local';
|
||||
const roleName = currentUser?.app_role?.name || 'Workspace member';
|
||||
const countryValueRaw = currentUser?.country || currentUser?.organization?.country;
|
||||
const countryName = typeof countryValueRaw === 'string' && countryValueRaw ? countryValueRaw : 'Not configured';
|
||||
const emailVerificationStatus = currentUser?.emailVerified ? 'Verified' : 'Pending';
|
||||
const authenticationStatus = isAuthenticationEnabled ? 'Enabled for this session' : 'Standby';
|
||||
const authenticationTone = isAuthenticationEnabled ? 'text-green-600 dark:text-green-400' : 'text-amber-600 dark:text-amber-400';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Aliyo Momot MAi Server')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={icon.mdiServer} title="Aliyo Momot MAi Server" main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
<CardBox className="mb-6 overflow-hidden">
|
||||
<div className="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.3em] text-gray-500 dark:text-gray-400">
|
||||
MAi server Aliyo Momot 1 and 2 · Server login
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
Branded authentication, profiles, email access, and marketing visibility in one place.
|
||||
</h2>
|
||||
<p className="mt-3 max-w-3xl text-sm text-gray-600 dark:text-gray-300">
|
||||
This separate in-app dashboard keeps the existing login flow intact while giving you a dedicated
|
||||
Aliyo Momot MAi Server experience for server entry readiness, profile context, email visibility, and
|
||||
high-level marketing statistics.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row lg:flex-col xl:flex-row">
|
||||
<BaseButton
|
||||
color={isAuthenticationEnabled ? 'success' : 'contrast'}
|
||||
icon={isAuthenticationEnabled ? icon.mdiCheckCircle : icon.mdiShieldCheck}
|
||||
label={isAuthenticationEnabled ? 'Authentication Enabled' : 'Enable Authentication'}
|
||||
onClick={() => setIsAuthenticationEnabled((previous) => !previous)}
|
||||
/>
|
||||
<BaseButton color="lightDark" icon={icon.mdiAccountCircle} label="Open Profile" href="/profile" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
{moduleLabels.map((label) => (
|
||||
<div
|
||||
key={label}
|
||||
className={`${corners !== 'rounded-full' ? corners : 'rounded-3xl'} border border-gray-200 bg-white/70 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-gray-600 dark:border-dark-700 dark:bg-dark-800/80 dark:text-gray-300`}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<div className="mb-6 grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-4">
|
||||
{metricConfigs.map((metric) => (
|
||||
<CardBox key={metric.key}>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-500 dark:text-gray-400">{metric.title}</p>
|
||||
<p className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">{metrics[metric.key]}</p>
|
||||
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">{metric.detail}</p>
|
||||
</div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
path={metric.iconPath}
|
||||
size={42}
|
||||
w="w-12"
|
||||
h="h-12"
|
||||
/>
|
||||
</div>
|
||||
</CardBox>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
|
||||
<CardBox>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<BaseIcon className={`${iconsColor}`} path={icon.mdiAccountCircle} size={30} />
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">Register & profile</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Profile, password, and regional setup blocks</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{readinessItems.map((item) => (
|
||||
<div key={item.label} className="flex items-start gap-3 border-b border-gray-100 pb-3 last:border-b-0 dark:border-dark-700">
|
||||
<BaseIcon path={icon.mdiCheckCircle} className="mt-0.5 text-green-600 dark:text-green-400" size={18} />
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 dark:text-white">{item.label}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={`mt-4 ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} ${cardsStyle} border border-dashed border-gray-300 p-4 dark:border-dark-700`}>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Current user</p>
|
||||
<p className="mt-2 text-lg font-semibold text-gray-900 dark:text-white">{displayName}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">{roleName}</p>
|
||||
<p className="mt-3 text-sm text-gray-500 dark:text-gray-400">Country / region: {countryName}</p>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<CardBox>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<BaseIcon className={`${iconsColor}`} path={icon.mdiDatabase} size={30} />
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">Admin profile database</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Administrative controls inspired by the shared mockup</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{adminItems.map((item) => (
|
||||
<div key={item.label} className="flex items-start gap-3 border-b border-gray-100 pb-3 last:border-b-0 dark:border-dark-700">
|
||||
<BaseIcon path={icon.mdiCog} className="mt-0.5 text-blue-600 dark:text-blue-400" size={18} />
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 dark:text-white">{item.label}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div className={`${corners !== 'rounded-full' ? corners : 'rounded-3xl'} border border-gray-200 p-4 dark:border-dark-700`}>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Server entry database</p>
|
||||
<p className="mt-2 text-lg font-semibold text-gray-900 dark:text-white">Protected</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Access remains tied to your authenticated app session.</p>
|
||||
</div>
|
||||
<div className={`${corners !== 'rounded-full' ? corners : 'rounded-3xl'} border border-gray-200 p-4 dark:border-dark-700`}>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Email database</p>
|
||||
<p className="mt-2 text-lg font-semibold text-gray-900 dark:text-white">{emailVerificationStatus}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Primary email readiness for outreach and verification.</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<CardBox>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<BaseIcon className={`${iconsColor}`} path={icon.mdiLanConnect} size={30} />
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">Server login</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Email and authentication details for the current operator</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormField label="Email">
|
||||
<input value={emailAddress} readOnly />
|
||||
</FormField>
|
||||
<FormField label="Password">
|
||||
<input type="password" value="••••••••••••" readOnly />
|
||||
</FormField>
|
||||
<FormField label="Authentication status">
|
||||
<input value={authenticationStatus} readOnly />
|
||||
</FormField>
|
||||
<FormField label="Provider">
|
||||
<input value={provider} readOnly />
|
||||
</FormField>
|
||||
|
||||
<div className={`mb-4 ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} border px-4 py-3 dark:border-dark-700`}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Live status</p>
|
||||
<p className={`mt-1 text-sm font-semibold ${authenticationTone}`}>{authenticationStatus}</p>
|
||||
</div>
|
||||
<BaseIcon
|
||||
path={isAuthenticationEnabled ? icon.mdiCheckCircle : icon.mdiAlertCircle}
|
||||
className={authenticationTone}
|
||||
size={22}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<BaseButton color="contrast" icon={icon.mdiShieldCheck} label="Enable Authentication" onClick={() => setIsAuthenticationEnabled(true)} />
|
||||
<BaseButton color="lightDark" icon={icon.mdiEmailOutline} label="Profile Email" href="/profile" />
|
||||
<BaseButton color="whiteDark" icon={icon.mdiOpenInNew} label="Forgot Password" href="/forgot" />
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MaiServerPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||
};
|
||||
|
||||
export default MaiServerPage;
|
||||
Loading…
x
Reference in New Issue
Block a user