38697-vm/frontend/src/pages/dashboard.tsx
2026-02-23 00:22:54 +00:00

259 lines
14 KiB
TypeScript

import * as icon from '@mdi/js';
import Head from 'next/head'
import React, { useCallback, useMemo } 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 moment from 'moment';
import { hasPermission } from "../helpers/userPermissions";
import { fetchWidgets } from '../stores/roles/rolesSlice';
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
import UserAvatar from '../components/UserAvatar';
import CardBox from '../components/CardBox';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { fetch as fetchActivityItems } from '../stores/activity_feed_items/activity_feed_itemsSlice';
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 loadingMessage = 'Loading...';
const [projects, setProjects] = React.useState(loadingMessage);
const [trials, setTrials] = React.useState(loadingMessage);
const [form_submissions, setForm_submissions] = React.useState(loadingMessage);
const [users, setUsers] = React.useState(loadingMessage);
const { activity_feed_items, loading: activityLoading } = useAppSelector((state) => state.activity_feed_items);
const { currentUser } = useAppSelector((state) => state.auth);
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
const loadCounts = useCallback(async () => {
const entities = ['projects', 'trials', 'form_submissions', 'users'];
const fns = [setProjects, setTrials, setForm_submissions, setUsers];
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 results = await Promise.allSettled(requests);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
fns[i](result.value.data.count);
} else {
fns[i](result.reason.message);
}
});
}, [currentUser]);
const currentUserMemo = useMemo(() => currentUser ? JSON.stringify(currentUser.id) : null, [currentUser]);
React.useEffect(() => {
if (!currentUser) return;
loadCounts().then();
if (hasPermission(currentUser, 'READ_ACTIVITY_FEED_ITEMS')) {
dispatch(fetchActivityItems({ query: '?limit=10&orderBy=occurred_at_DESC' }));
}
}, [currentUserMemo, dispatch, loadCounts]);
const getItemIcon = (type: string) => {
switch (type) {
case 'project_created': return icon.mdiFolderPlusOutline;
case 'trial_created': return icon.mdiFlaskPlusOutline;
case 'submission_created': return icon.mdiClipboardCheckOutline;
case 'user_created': return icon.mdiAccountPlusOutline;
case 'attachment_added': return icon.mdiPaperclip;
default: return icon.mdiBellOutline;
}
}
return (
<>
<Head>
<title>{getPageTitle('Home')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={icon.mdiHomeOutline} title='Home' main>
{''}
</SectionTitleLineWithButton>
{/* Summary Cards */}
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6'>
<Link href={'/projects/projects-list'}>
<CardBox className="hover:shadow-lg transition-shadow cursor-pointer">
<div className="flex justify-between items-center">
<div>
<p className="text-gray-500 dark:text-gray-400">Projects</p>
<h3 className="text-3xl font-bold">{projects}</h3>
</div>
<BaseIcon path={icon.mdiFolderOutline} size={48} className={iconsColor} />
</div>
</CardBox>
</Link>
<Link href={'/trials/trials-list'}>
<CardBox className="hover:shadow-lg transition-shadow cursor-pointer">
<div className="flex justify-between items-center">
<div>
<p className="text-gray-500 dark:text-gray-400">Trials</p>
<h3 className="text-3xl font-bold">{trials}</h3>
</div>
<BaseIcon path={icon.mdiTestTube} size={48} className={iconsColor} />
</div>
</CardBox>
</Link>
<Link href={'/form_submissions/form_submissions-list'}>
<CardBox className="hover:shadow-lg transition-shadow cursor-pointer">
<div className="flex justify-between items-center">
<div>
<p className="text-gray-500 dark:text-gray-400">Submissions</p>
<h3 className="text-3xl font-bold">{form_submissions}</h3>
</div>
<BaseIcon path={icon.mdiClipboardTextOutline} size={48} className={iconsColor} />
</div>
</CardBox>
</Link>
<Link href={'/users/users-list'}>
<CardBox className="hover:shadow-lg transition-shadow cursor-pointer">
<div className="flex justify-between items-center">
<div>
<p className="text-gray-500 dark:text-gray-400">Team Members</p>
<h3 className="text-3xl font-bold">{users}</h3>
</div>
<BaseIcon path={icon.mdiAccountGroup} size={48} className={iconsColor} />
</div>
</CardBox>
</Link>
</div>
{/* Activity Feed Section */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<SectionTitleLineWithButton icon={icon.mdiTimelineTextOutline} title="Recent Activity" />
{activityLoading && <p>Loading feed...</p>}
{!activityLoading && activity_feed_items?.length === 0 && (
<CardBox>
<p className="text-center text-gray-500 py-8">No recent activity found.</p>
</CardBox>
)}
<div className="space-y-4">
{activity_feed_items?.map((item: any) => (
<CardBox key={item.id} className="hover:border-emerald-500 transition-colors border-l-4 border-l-emerald-500">
<div className="flex items-start space-x-4">
<div className="flex-shrink-0">
<UserAvatar
username={item.actor_user?.firstName || 'User'}
image={item.actor_user?.avatar}
className="w-12 h-12"
/>
</div>
<div className="flex-grow">
<div className="flex justify-between items-start">
<div>
<span className="font-bold text-lg">
{item.actor_user?.firstName} {item.actor_user?.lastName}
</span>
<span className="text-gray-500 ml-2">
{item.title}
</span>
</div>
<div className="text-xs text-gray-400 flex items-center">
<BaseIcon path={icon.mdiClockOutline} size={14} className="mr-1" />
{moment(item.occurred_at).fromNow()}
</div>
</div>
<p className="text-gray-600 dark:text-gray-300 mt-1">
{item.summary}
</p>
{item.thumbnail && item.thumbnail[0] && (
<div className="mt-3">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={item.thumbnail[0].publicUrl}
alt="Activity thumbnail"
className="rounded-lg max-h-48 w-auto object-cover border dark:border-gray-700"
/>
</div>
)}
<div className="mt-3 flex items-center justify-between">
<div className="flex items-center space-x-2">
<BaseIcon
path={getItemIcon(item.item_type)}
size={18}
className="text-emerald-600"
/>
<span className="text-xs font-medium uppercase tracking-wider text-emerald-600">
{item.item_type.replace('_', ' ')}
</span>
</div>
{item.link_path && (
<Link href={item.link_path} className="text-sm text-emerald-600 hover:underline font-medium">
View Details
</Link>
)}
</div>
</div>
</div>
</CardBox>
))}
</div>
</div>
<div className="space-y-6">
<SectionTitleLineWithButton icon={icon.mdiChartPie} title="Quick Stats" />
<CardBox>
<div className="space-y-4">
<div className="flex justify-between items-center pb-2 border-b dark:border-gray-700">
<span className="text-gray-500">Active Projects</span>
<span className="font-bold">{projects}</span>
</div>
<div className="flex justify-between items-center pb-2 border-b dark:border-gray-700">
<span className="text-gray-500">Ongoing Trials</span>
<span className="font-bold">{trials}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-500">New Submissions</span>
<span className="font-bold text-emerald-600">+{form_submissions}</span>
</div>
</div>
</CardBox>
<SectionTitleLineWithButton icon={icon.mdiInformationOutline} title="System Info" />
<CardBox className="bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800">
<p className="text-sm text-emerald-800 dark:text-emerald-200">
<strong>Trial Tracker</strong> is running in multi-tenant mode.
Farm-level data isolation is active for your account.
</p>
</CardBox>
</div>
</div>
</SectionMain>
</>
)
}
Dashboard.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
}
export default Dashboard