29876/frontend/src/pages/dashboard.tsx
2025-03-14 09:22:53 +00:00

613 lines
22 KiB
TypeScript

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 LayoutAuthenticated from '../layouts/Authenticated';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import BaseIcon from '../components/BaseIcon';
import { getPageTitle } from '../config';
import Link from 'next/link';
import { hasPermission } from '../helpers/userPermissions';
import { fetchWidgets } from '../stores/roles/rolesSlice';
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
const Dashboard = () => {
const dispatch = useAppDispatch();
const iconsColor = useAppSelector((state) => state.style.iconsColor);
const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
const [users, setUsers] = React.useState('Loading...');
const [accounts, setAccounts] = React.useState('Loading...');
const [contacts, setContacts] = React.useState('Loading...');
const [leads, setLeads] = React.useState('Loading...');
const [models, setModels] = React.useState('Loading...');
const [opportunities, setOpportunities] = React.useState('Loading...');
const [projects, setProjects] = React.useState('Loading...');
const [reports, setReports] = React.useState('Loading...');
const [skills, setSkills] = React.useState('Loading...');
const [storyboards, setStoryboards] = React.useState('Loading...');
const [roles, setRoles] = React.useState('Loading...');
const [permissions, setPermissions] = React.useState('Loading...');
const [organisations, setOrganisations] = React.useState('Loading...');
const [widgetsRole, setWidgetsRole] = React.useState({
role: { value: '', label: '' },
});
const { currentUser } = useAppSelector((state) => state.auth);
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
const organizationId = currentUser?.Organisations?.id;
async function loadData() {
const entities = [
'users',
'accounts',
'contacts',
'leads',
'models',
'opportunities',
'projects',
'reports',
'skills',
'storyboards',
'roles',
'permissions',
'organisations',
];
const fns = [
setUsers,
setAccounts,
setContacts,
setLeads,
setModels,
setOpportunities,
setProjects,
setReports,
setSkills,
setStoryboards,
setRoles,
setPermissions,
setOrganisations,
];
const requests = entities.map((entity, index) => {
if (hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
return axios.get(`/${entity.toLowerCase()}/count`);
} else {
fns[index](null);
return Promise.resolve({ data: { count: null } });
}
});
Promise.allSettled(requests).then((results) => {
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
fns[i](result.value.data.count);
} else {
fns[i](result.reason.message);
}
});
});
}
async function getWidgets(roleId) {
await dispatch(fetchWidgets(roleId));
}
React.useEffect(() => {
if (!currentUser) return;
loadData().then();
setWidgetsRole({
role: {
value: currentUser?.app_role?.id,
label: currentUser?.app_role?.name,
},
});
}, [currentUser]);
React.useEffect(() => {
if (!currentUser || !widgetsRole?.role?.value) return;
getWidgets(widgetsRole?.role?.value || '').then();
}, [widgetsRole?.role?.value]);
return (
<>
<Head>
<title>{getPageTitle('Dashboard')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant}
title='Overview'
main
>
{''}
</SectionTitleLineWithButton>
{hasPermission(currentUser, 'CREATE_ROLES') && (
<WidgetCreator
currentUser={currentUser}
isFetchingQuery={isFetchingQuery}
setWidgetsRole={setWidgetsRole}
widgetsRole={widgetsRole}
/>
)}
{!!rolesWidgets.length &&
hasPermission(currentUser, 'CREATE_ROLES') && (
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
</p>
)}
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
{(isFetchingQuery || loading) && (
<div
className={` ${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}
>
<BaseIcon
className={`${iconsColor} animate-spin mr-5`}
w='w-16'
h='h-16'
size={48}
path={icon.mdiLoading}
/>{' '}
Loading widgets...
</div>
)}
{rolesWidgets &&
rolesWidgets.map((widget) => (
<SmartWidget
key={widget.id}
userId={currentUser?.id}
widget={widget}
roleId={widgetsRole?.role?.value || ''}
admin={hasPermission(currentUser, 'CREATE_ROLES')}
/>
))}
</div>
{!!rolesWidgets.length && <hr className='my-6 ' />}
<div
id='dashboard'
className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'
>
{hasPermission(currentUser, 'READ_USERS') && (
<Link href={'/users/users-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Users
</div>
<div className='text-3xl leading-tight font-semibold'>
{users}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiAccountGroup || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_ACCOUNTS') && (
<Link href={'/accounts/accounts-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Accounts
</div>
<div className='text-3xl leading-tight font-semibold'>
{accounts}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiDomain || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_CONTACTS') && (
<Link href={'/contacts/contacts-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Contacts
</div>
<div className='text-3xl leading-tight font-semibold'>
{contacts}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiAccountCircle || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_LEADS') && (
<Link href={'/leads/leads-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Leads
</div>
<div className='text-3xl leading-tight font-semibold'>
{leads}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiAccountPlus || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_MODELS') && (
<Link href={'/models/models-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Models
</div>
<div className='text-3xl leading-tight font-semibold'>
{models}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiCubeOutline || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_OPPORTUNITIES') && (
<Link href={'/opportunities/opportunities-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Opportunities
</div>
<div className='text-3xl leading-tight font-semibold'>
{opportunities}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiBriefcaseOutline || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_PROJECTS') && (
<Link href={'/projects/projects-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Projects
</div>
<div className='text-3xl leading-tight font-semibold'>
{projects}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiProjectorScreen || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_REPORTS') && (
<Link href={'/reports/reports-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Reports
</div>
<div className='text-3xl leading-tight font-semibold'>
{reports}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiFileDocumentOutline || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_SKILLS') && (
<Link href={'/skills/skills-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Skills
</div>
<div className='text-3xl leading-tight font-semibold'>
{skills}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiStarCircleOutline || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_STORYBOARDS') && (
<Link href={'/storyboards/storyboards-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Storyboards
</div>
<div className='text-3xl leading-tight font-semibold'>
{storyboards}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiStoryboardOutline || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_ROLES') && (
<Link href={'/roles/roles-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Roles
</div>
<div className='text-3xl leading-tight font-semibold'>
{roles}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={
icon.mdiShieldAccountVariantOutline || icon.mdiTable
}
/>
</div>
</div>
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_PERMISSIONS') && (
<Link href={'/permissions/permissions-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Permissions
</div>
<div className='text-3xl leading-tight font-semibold'>
{permissions}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiShieldAccountOutline || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_ORGANISATIONS') && (
<Link href={'/organisations/organisations-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Organisations
</div>
<div className='text-3xl leading-tight font-semibold'>
{organisations}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
</div>
</SectionMain>
</>
);
};
Dashboard.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default Dashboard;