Auto commit: 2026-01-08T12:06:16.184Z

This commit is contained in:
Flatlogic Bot 2026-01-08 12:06:16 +00:00
parent 9bb4dbe70c
commit 66ae691b92

View File

@ -1,373 +1,155 @@
import * as icon from '@mdi/js'; import * as icon from '@mdi/js'
import Head from 'next/head' import Head from 'next/head'
import React from 'react' import React, { ReactElement } from 'react'
import axios from 'axios'; import axios from 'axios'
import type { ReactElement } from 'react'
import LayoutAuthenticated from '../layouts/Authenticated' import LayoutAuthenticated from '../layouts/Authenticated'
import SectionMain from '../components/SectionMain' import SectionMain from '../components/SectionMain'
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton' import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
import BaseIcon from "../components/BaseIcon"; import BaseIcon from '../components/BaseIcon'
import { getPageTitle } from '../config' import { getPageTitle } from '../config'
import Link from "next/link"; import Link from 'next/link'
import { useAppSelector } from '../stores/hooks'
import { hasPermission } from '../helpers/userPermissions'
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 Dashboard = () => {
const dispatch = useAppDispatch(); const iconsColor = useAppSelector((state) => state.style.iconsColor)
const iconsColor = useAppSelector((state) => state.style.iconsColor); const corners = useAppSelector((state) => state.style.corners)
const corners = useAppSelector((state) => state.style.corners); const cardsStyle = useAppSelector((state) => state.style.cardsStyle)
const cardsStyle = useAppSelector((state) => state.style.cardsStyle); const { currentUser } = useAppSelector((state) => state.auth)
const loadingMessage = 'Loading...'; const loadingMessage = 'Loading...'
const [leads, setLeads] = React.useState(loadingMessage)
const [projects, setProjects] = React.useState(loadingMessage)
const [tasks, setTasks] = React.useState(loadingMessage)
const [users, setUsers] = React.useState(loadingMessage); async function loadData() {
const [roles, setRoles] = React.useState(loadingMessage); const entities = ['leads', 'projects', 'tasks']
const [permissions, setPermissions] = React.useState(loadingMessage); const fns = [setLeads, setProjects, setTasks]
const [leads, setLeads] = React.useState(loadingMessage);
const [projects, setProjects] = React.useState(loadingMessage);
const [tasks, setTasks] = React.useState(loadingMessage);
const [comments, setComments] = React.useState(loadingMessage);
const [attachments, setAttachments] = React.useState(loadingMessage);
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 } })
}
})
const [widgetsRole, setWidgetsRole] = React.useState({ Promise.allSettled(requests).then((results) => {
role: { value: '', label: '' }, results.forEach((result, i) => {
}); if (result.status === 'fulfilled') {
const { currentUser } = useAppSelector((state) => state.auth); fns[i](result.value.data.count)
const { isFetchingQuery } = useAppSelector((state) => state.openAi); } else {
fns[i](result.reason.message)
}
})
})
}
const { rolesWidgets, loading } = useAppSelector((state) => state.roles); React.useEffect(() => {
if (!currentUser) return
loadData().then()
async function loadData() { }, [currentUser])
const entities = ['users','roles','permissions','leads','projects','tasks','comments','attachments',];
const fns = [setUsers,setRoles,setPermissions,setLeads,setProjects,setTasks,setComments,setAttachments,];
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 ( return (
<> <>
<Head> <Head>
<title> <title>{getPageTitle('Dashboard')}</title>
{getPageTitle('Overview')}
</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton <SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant} icon={icon.mdiChartTimelineVariant}
title='Overview' title="TeamFlow PM Dashboard"
main> main
>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator <div id="dashboard" className="grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6">
currentUser={currentUser} {hasPermission(currentUser, 'READ_LEADS') && (
isFetchingQuery={isFetchingQuery} <Link href={'/leads/leads-list'}>
setWidgetsRole={setWidgetsRole} <div
widgetsRole={widgetsRole} className={`${
/>} corners !== 'rounded-full' ? corners : 'rounded-3xl'
{!!rolesWidgets.length && } dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
hasPermission(currentUser, 'CREATE_ROLES') && ( >
<p className=' text-gray-500 dark:text-gray-400 mb-4'> <div className="flex justify-between align-center">
{`${widgetsRole?.role?.label || 'Users'}'s widgets`} <div>
</p> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
)} Potential Leads
</div>
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'> <div className="text-3xl leading-tight font-semibold">{leads}</div>
{(isFetchingQuery || loading) && ( </div>
<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`}> <div>
<BaseIcon <BaseIcon
className={`${iconsColor} animate-spin mr-5`} className={`${iconsColor}`}
w='w-16' w="w-16"
h='h-16' h="h-16"
size={48} size={48}
path={icon.mdiLoading} path={icon.mdiAccountMultiple}
/>{' '}
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>
</div>
{!!rolesWidgets.length && <hr className='my-6 text-pastelEmeraldTheme-mainBG ' />}
<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> </div>
</Link>} </div>
</Link>
)}
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}> {hasPermission(currentUser, 'READ_PROJECTS') && (
<div <Link href={'/projects/projects-list'}>
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`} <div
> className={`${
<div className="flex justify-between align-center"> corners !== 'rounded-full' ? corners : 'rounded-3xl'
<div> } dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> >
Roles <div className="flex justify-between align-center">
</div> <div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
{roles} Active 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.mdiShieldAccountVariantOutline || icon.mdiTable}
/>
</div>
</div> </div>
<div className="text-3xl leading-tight font-semibold">{projects}</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
path={icon.mdiFolder}
/>
</div>
</div> </div>
</Link>} </div>
</Link>
)}
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}> {hasPermission(currentUser, 'READ_TASKS') && (
<div <Link href={'/tasks/tasks-list'}>
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`} <div
> className={`${
<div className="flex justify-between align-center"> corners !== 'rounded-full' ? corners : 'rounded-3xl'
<div> } dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> >
Permissions <div className="flex justify-between align-center">
</div> <div>
<div className="text-3xl leading-tight font-semibold"> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
{permissions} Open Tasks
</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>
<div className="text-3xl leading-tight font-semibold">{tasks}</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
path={icon.mdiFormatListBulleted}
/>
</div>
</div> </div>
</Link>} </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={'mdiAccountMultiple' in icon ? icon['mdiAccountMultiple' as keyof typeof icon] : icon.mdiTable || 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={'mdiFolder' in icon ? icon['mdiFolder' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_TASKS') && <Link href={'/tasks/tasks-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">
Tasks
</div>
<div className="text-3xl leading-tight font-semibold">
{tasks}
</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={'mdiFormatListBulleted' in icon ? icon['mdiFormatListBulleted' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_COMMENTS') && <Link href={'/comments/comments-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">
Comments
</div>
<div className="text-3xl leading-tight font-semibold">
{comments}
</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={'mdiCommentText' in icon ? icon['mdiCommentText' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_ATTACHMENTS') && <Link href={'/attachments/attachments-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">
Attachments
</div>
<div className="text-3xl leading-tight font-semibold">
{attachments}
</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={'mdiPaperclip' in icon ? icon['mdiPaperclip' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
</div> </div>
</SectionMain> </SectionMain>
</> </>
@ -379,3 +161,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) {
} }
export default Dashboard export default Dashboard