Auto commit: 2026-01-08T12:06:16.184Z
This commit is contained in:
parent
9bb4dbe70c
commit
66ae691b92
@ -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
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user