This commit is contained in:
Elizabeth 2024-01-26 17:01:57 -05:00
parent fbb8138a91
commit f8a0ee671e
34 changed files with 2157 additions and 2 deletions

2
.gitignore vendored
View File

@ -35,3 +35,5 @@ yarn-error.log*
!.yarn/versions
.pnp.*
yarn.lock
package-lock.json

View File

@ -15,6 +15,7 @@
"@mui/x-date-pickers": "6.19.0",
"@popperjs/core": "2.11.8",
"@reduxjs/toolkit": "2.0.1",
"apexcharts": "^3.45.2",
"autosuggest-highlight": "3.3.4",
"axios": "1.6.5",
"axios-mock-adapter": "1.21.5",
@ -37,6 +38,7 @@
"prismjs": "1.29.0",
"qs": "6.11.2",
"react": "18.2.0",
"react-apexcharts": "^1.4.1",
"react-app-alias": "2.2.2",
"react-autosuggest": "10.1.0",
"react-dom": "18.2.0",
@ -82,9 +84,9 @@
"@tailwindcss/aspect-ratio": "0.4.2",
"@tailwindcss/typography": "0.5.10",
"@types/autosuggest-highlight": "3.2.3",
"@types/babel-traverse": "6.25.10",
"@types/babel__parser": "7.1.1",
"@types/babel__traverse": "7.20.5",
"@types/babel-traverse": "6.25.10",
"@types/crypto-js": "4.2.1",
"@types/draft-js": "0.11.17",
"@types/draftjs-to-html": "0.8.4",

View File

@ -0,0 +1,7 @@
const locale = {
APPLICATIONS: 'Applications',
EXAMPLE: 'Example',
DASHBOARD: 'Tablero'
};
export default locale;

View File

@ -19,6 +19,14 @@ const navigationConfig: FuseNavItemType[] = [
type: 'item',
icon: 'heroicons-outline:star',
url: 'example'
},
{
id: 'dashboard-component',
title: 'Tablero',
translate: 'TABLERO',
type: 'item',
icon: 'heroicons-outline:star',
url: 'dashboards/project'
}
];

View File

@ -8,8 +8,9 @@ import SignUpConfig from '../main/sign-up/SignUpConfig';
import SignOutConfig from '../main/sign-out/SignOutConfig';
import Error404Page from '../main/404/Error404Page';
import ExampleConfig from '../main/example/ExampleConfig';
import ProjectDashboardAppConfig from '../main/dashboard/project/ProjectDashboardAppConfig';
const routeConfigs: FuseRouteConfigsType = [ExampleConfig, SignOutConfig, SignInConfig, SignUpConfig];
const routeConfigs: FuseRouteConfigsType = [ExampleConfig, SignOutConfig, SignInConfig, SignUpConfig, ProjectDashboardAppConfig];
/**
* The routes of the application.

View File

@ -0,0 +1,51 @@
import { apiService as api } from 'app/store/apiService';
export const addTagTypes = ['project_dashboard_widgets', 'project_dashboard_projects'] as const;
const ProjectDashboardApi = api
.enhanceEndpoints({
addTagTypes
})
.injectEndpoints({
endpoints: (build) => ({
getProjectDashboardWidgets: build.query<
GetProjectDashboardWidgetsApiResponse,
GetProjectDashboardWidgetsApiArg
>({
query: () => ({ url: `/mock-api/dashboards/project/widgets` }),
providesTags: ['project_dashboard_widgets']
}),
getProjectDashboardProjects: build.query<
GetProjectDashboardProjectsApiResponse,
GetProjectDashboardProjectsApiArg
>({
query: () => ({ url: `/mock-api/dashboards/project/projects` }),
providesTags: ['project_dashboard_projects']
})
}),
overrideExisting: false
});
export default ProjectDashboardApi;
export type GetProjectDashboardWidgetsApiResponse = /** status 200 OK */ object;
export type GetProjectDashboardWidgetsApiArg = void;
export type GetProjectDashboardProjectsApiResponse = /** status 200 OK */ ProjectType[];
export type GetProjectDashboardProjectsApiArg = void;
export type ProjectType = {
id: number;
name: string;
};
export const { useGetProjectDashboardWidgetsQuery, useGetProjectDashboardProjectsQuery } = ProjectDashboardApi;
export type ProjectDashboardApiType = {
[ProjectDashboardApi.reducerPath]: ReturnType<typeof ProjectDashboardApi.reducer>;
};
export const selectWidget =
<T>(id: string) =>
(state: ProjectDashboardApiType) => {
const widgets = ProjectDashboardApi.endpoints.getProjectDashboardWidgets.select()(state)?.data;
return widgets?.[id] as T;
};

View File

@ -0,0 +1,86 @@
import FusePageSimple from '@fuse/core/FusePageSimple';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import { useState } from 'react';
import Box from '@mui/material/Box';
import { styled } from '@mui/material/styles';
import * as React from 'react';
import FuseLoading from '@fuse/core/FuseLoading';
import ProjectDashboardAppHeader from './ProjectDashboardAppHeader';
import HomeTab from './tabs/home/HomeTab';
import TeamTab from './tabs/team/TeamTab';
import BudgetTab from './tabs/budget/BudgetTab';
import { useGetProjectDashboardWidgetsQuery } from './ProjectDashboardApi';
const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-header': {
backgroundColor: theme.palette.background.paper,
boxShadow: `inset 0 0 0 1px ${theme.palette.divider}`
}
}));
/**
* The ProjectDashboardApp page.
*/
function ProjectDashboardApp() {
const { isLoading } = useGetProjectDashboardWidgetsQuery();
const [tabValue, setTabValue] = useState(0);
function handleChangeTab(event: React.SyntheticEvent, value: number) {
setTabValue(value);
}
if (isLoading) {
return <FuseLoading />;
}
return (
<Root
header={<ProjectDashboardAppHeader />}
content={
<div className="w-full p-12 pt-16 sm:pt-24 lg:ltr:pr-0 lg:rtl:pl-0">
<Tabs
value={tabValue}
onChange={handleChangeTab}
indicatorColor="secondary"
textColor="inherit"
variant="scrollable"
scrollButtons={false}
className="w-full px-24 -mx-4 min-h-40"
classes={{ indicator: 'flex justify-center bg-transparent w-full h-full' }}
TabIndicatorProps={{
children: (
<Box
sx={{ bgcolor: 'text.disabled' }}
className="w-full h-full rounded-full opacity-20"
/>
)
}}
>
<Tab
className="text-14 font-semibold min-h-40 min-w-64 mx-4 px-12"
disableRipple
label="Home"
/>
<Tab
className="text-14 font-semibold min-h-40 min-w-64 mx-4 px-12"
disableRipple
label="Budget"
/>
<Tab
className="text-14 font-semibold min-h-40 min-w-64 mx-4 px-12"
disableRipple
label="Team"
/>
</Tabs>
{tabValue === 0 && <HomeTab />}
{tabValue === 1 && <BudgetTab />}
{tabValue === 2 && <TeamTab />}
</div>
}
/>
);
}
export default ProjectDashboardApp;

View File

@ -0,0 +1,22 @@
import { lazy } from 'react';
const ProjectDashboardApp = lazy(() => import('./ProjectDashboardApp'));
/**
* The ProjectDashboardApp configuration.
*/
const ProjectDashboardAppConfig = {
settings: {
layout: {
config: {}
}
},
routes: [
{
path: 'dashboards/project',
element: <ProjectDashboardApp />
}
]
};
export default ProjectDashboardAppConfig;

View File

@ -0,0 +1,170 @@
import Avatar from '@mui/material/Avatar';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Typography from '@mui/material/Typography';
import { useState } from 'react';
import _ from '@lodash';
import Button from '@mui/material/Button';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import FuseLoading from '@fuse/core/FuseLoading';
import { darken } from '@mui/material/styles';
import { useSelector } from 'react-redux';
import { selectUser } from 'src/app/auth/user/store/userSlice';
import { useGetProjectDashboardProjectsQuery } from './ProjectDashboardApi';
/**
* The ProjectDashboardAppHeader page.
*/
function ProjectDashboardAppHeader() {
const { isLoading } = useGetProjectDashboardProjectsQuery();
const projects=[
{
"id": 1,
"name": "ACME Corp. Backend App"
},
{
"id": 2,
"name": "ACME Corp. Frontend App"
},
{
"id": 3,
"name": "Creapond"
},
{
"id": 4,
"name": "Withinpixels"
}
]
const user = useSelector(selectUser);
const [selectedProject, setSelectedProject] = useState<{ id: number; menuEl: HTMLElement | null }>({
id: 1,
menuEl: null
});
function handleChangeProject(id: number) {
setSelectedProject({
id,
menuEl: null
});
}
function handleOpenProjectMenu(event: React.MouseEvent<HTMLElement>) {
setSelectedProject({
id: selectedProject.id,
menuEl: event.currentTarget
});
}
function handleCloseProjectMenu() {
setSelectedProject({
id: selectedProject.id,
menuEl: null
});
}
if (isLoading) {
return <FuseLoading />;
}
return (
<div className="flex flex-col w-full px-24 sm:px-32">
<div className="flex flex-col sm:flex-row flex-auto sm:items-center min-w-0 my-32 sm:my-48">
<div className="flex flex-auto items-center min-w-0">
<Avatar
sx={{
background: (theme) => darken(theme.palette.background.default, 0.05),
color: (theme) => theme.palette.text.secondary
}}
className="flex-0 w-64 h-64"
alt="user photo"
src={user?.data?.photoURL}
>
{user?.data?.displayName?.[0]}
</Avatar>
<div className="flex flex-col min-w-0 mx-16">
<Typography className="text-2xl md:text-5xl font-semibold tracking-tight leading-7 md:leading-snug truncate">
{`Welcome back, ${user.data.displayName}!`}
</Typography>
<div className="flex items-center">
<FuseSvgIcon
size={20}
color="action"
>
heroicons-solid:bell
</FuseSvgIcon>
<Typography
className="mx-6 leading-6 truncate"
color="text.secondary"
>
You have 2 new messages and 15 new tasks
</Typography>
</div>
</div>
</div>
<div className="flex items-center mt-24 sm:mt-0 sm:mx-8 space-x-12">
<Button
className="whitespace-nowrap"
variant="contained"
color="primary"
startIcon={<FuseSvgIcon size={20}>heroicons-solid:mail</FuseSvgIcon>}
>
Messages
</Button>
<Button
className="whitespace-nowrap"
variant="contained"
color="secondary"
startIcon={<FuseSvgIcon size={20}>heroicons-solid:cog</FuseSvgIcon>}
>
Settings
</Button>
</div>
</div>
<div className="flex items-center">
<Button
onClick={handleOpenProjectMenu}
className="flex items-center border border-solid border-b-0 rounded-t-xl rounded-b-0 h-40 px-16 text-13 sm:text-16"
sx={{
backgroundColor: (theme) => theme.palette.background.default,
borderColor: (theme) => theme.palette.divider
}}
endIcon={
<FuseSvgIcon
size={20}
color="action"
>
heroicons-solid:chevron-down
</FuseSvgIcon>
}
>
{_.find(projects, ['id', selectedProject.id])?.name}
</Button>
<Menu
id="project-menu"
anchorEl={selectedProject.menuEl}
open={Boolean(selectedProject.menuEl)}
onClose={handleCloseProjectMenu}
>
{/* {projects &&
projects.map((project) => (
<MenuItem
key={project.id}
onClick={() => {
handleChangeProject(project.id);
}}
>
{project.name}
</MenuItem>
))} */}
</Menu>
</div>
</div>
);
}
export default ProjectDashboardAppHeader;

View File

@ -0,0 +1,70 @@
import { motion } from 'framer-motion';
import BudgetDistributionWidget from './widgets/BudgetDistributionWidget';
import WeeklyExpensesWidget from './widgets/WeeklyExpensesWidget';
import MonthlyExpensesWidget from './widgets/MonthlyExpensesWidget';
import YearlyExpensesWidget from './widgets/YearlyExpensesWidget';
import BudgetDetailsWidget from './widgets/BudgetDetailsWidget';
/**
* The BudgetTab component.
*/
function BudgetTab() {
const container = {
show: {
transition: {
staggerChildren: 0.04
}
}
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
};
return (
<motion.div
className="grid grid-cols-1 sm:grid-cols-6 gap-24 w-full min-w-0 p-24"
variants={container}
initial="hidden"
animate="show"
>
<motion.div
variants={item}
className="sm:col-span-3 lg:col-span-4"
>
<BudgetDistributionWidget />
</motion.div>
<div className="sm:col-span-3 lg:col-span-2 grid grid-cols-1 gap-y-24">
<motion.div
variants={item}
className="sm:col-span-2"
>
<WeeklyExpensesWidget />
</motion.div>
<motion.div
variants={item}
className="sm:col-span-2"
>
<MonthlyExpensesWidget />
</motion.div>
<motion.div
variants={item}
className="sm:col-span-2"
>
<YearlyExpensesWidget />
</motion.div>
</div>
<motion.div
variants={item}
className="sm:col-span-6"
>
<BudgetDetailsWidget />
</motion.div>
</motion.div>
);
}
export default BudgetTab;

View File

@ -0,0 +1,118 @@
import Paper from '@mui/material/Paper';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import { memo } from 'react';
import Chip from '@mui/material/Chip';
import { useSelector } from 'react-redux';
import BudgetDetailsDataType from './types/BudgetDetailsDataType';
import { selectWidget } from '../../../ProjectDashboardApi';
/**
* The BudgetDetailsWidget widget.
*/
function BudgetDetailsWidget() {
const widget = useSelector(selectWidget<BudgetDetailsDataType>('budgetDetails'));
if (!widget) {
return null;
}
const { columns, rows } = widget;
return (
<Paper className="flex flex-col flex-auto p-24 shadow rounded-2xl overflow-hidden">
<Typography className="text-lg font-medium tracking-tight leading-6 truncate">Budget Details</Typography>
<div className="table-responsive">
<Table className="w-full min-w-full">
<TableHead>
<TableRow>
{columns.map((column, index) => (
<TableCell key={index}>
<Typography
color="text.secondary"
className="font-semibold text-12 whitespace-nowrap"
>
{column}
</Typography>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, index) => (
<TableRow key={index}>
{Object.entries(row).map(([key, value]) => {
switch (key) {
case 'type': {
return (
<TableCell
key={key}
component="th"
scope="row"
>
<Chip
size="small"
label={value}
/>
</TableCell>
);
}
case 'total':
case 'expensesAmount':
case 'remainingAmount': {
return (
<TableCell
key={key}
component="th"
scope="row"
>
<Typography>
{value.toLocaleString('en-US', {
style: 'currency',
currency: 'USD'
})}
</Typography>
</TableCell>
);
}
case 'expensesPercentage':
case 'remainingPercentage': {
return (
<TableCell
key={key}
component="th"
scope="row"
>
<Typography>{`${value}%`}</Typography>
</TableCell>
);
}
default: {
return (
<TableCell
key={key}
component="th"
scope="row"
>
<Typography>{value}</Typography>
</TableCell>
);
}
}
})}
</TableRow>
))}
</TableBody>
</Table>
</div>
</Paper>
);
}
export default memo(BudgetDetailsWidget);

View File

@ -0,0 +1,105 @@
import Paper from '@mui/material/Paper';
import { useTheme } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import { memo } from 'react';
import ReactApexChart from 'react-apexcharts';
import { ApexOptions } from 'apexcharts';
import { useSelector } from 'react-redux';
import BudgetDistributionDataType from './types/BudgetDistributionDataType';
import { selectWidget } from '../../../ProjectDashboardApi';
/**
* The BudgetDistributionWidget widget.
*/
function BudgetDistributionWidget() {
const widget = useSelector(selectWidget<BudgetDistributionDataType>('budgetDistribution'));
if (!widget) {
return null;
}
const { categories, series } = widget;
const theme = useTheme();
const chartOptions: ApexOptions = {
chart: {
fontFamily: 'inherit',
foreColor: 'inherit',
height: '100%',
type: 'radar',
sparkline: {
enabled: true
}
},
colors: [theme.palette.secondary.main],
dataLabels: {
enabled: true,
formatter: (val: string) => `${val}%`,
textAnchor: 'start',
style: {
fontSize: '13px',
fontWeight: 500
},
background: {
borderWidth: 0,
padding: 4
},
offsetY: -15
},
markers: {
strokeColors: theme.palette.primary.main,
strokeWidth: 4
},
plotOptions: {
radar: {
polygons: {
strokeColors: theme.palette.divider,
connectorColors: theme.palette.divider
}
}
},
stroke: {
width: 2
},
tooltip: {
theme: 'dark',
y: {
formatter: (val) => `${val}%`
}
},
xaxis: {
labels: {
show: true,
style: {
fontSize: '12px',
fontWeight: '500'
}
},
categories
},
yaxis: {
max: (max) => parseInt((max + 10).toFixed(0), 10),
tickAmount: 7
}
};
return (
<Paper className="flex flex-col flex-auto p-24 shadow rounded-2xl overflow-hidden h-full">
<Typography className="text-lg font-medium tracking-tight leading-6 truncate">
Budget Distribution
</Typography>
<div className="flex flex-col flex-auto">
<ReactApexChart
className="flex-auto w-full h-320"
options={chartOptions}
series={series}
type={chartOptions?.chart?.type}
height={chartOptions?.chart?.height}
/>
</div>
</Paper>
);
}
export default memo(BudgetDistributionWidget);

View File

@ -0,0 +1,100 @@
import Paper from '@mui/material/Paper';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import IconButton from '@mui/material/IconButton';
import ReactApexChart from 'react-apexcharts';
import { useTheme } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import { ApexOptions } from 'apexcharts';
import { useSelector } from 'react-redux';
import ExpensesDataType from './types/ExpensesDataType';
import { selectWidget } from '../../../ProjectDashboardApi';
/**
* The MonthlyExpensesWidget widget.
*/
function MonthlyExpensesWidget() {
const widget = useSelector(selectWidget<ExpensesDataType>('monthlyExpenses'));
if (!widget) {
return null;
}
const { amount, series, labels } = widget;
const theme = useTheme();
const chartOptions: ApexOptions = {
chart: {
animations: {
enabled: false
},
fontFamily: 'inherit',
foreColor: 'inherit',
height: '100%',
type: 'line',
sparkline: {
enabled: true
}
},
colors: [theme.palette.success.main],
stroke: {
curve: 'smooth'
},
tooltip: {
theme: 'dark'
},
xaxis: {
type: 'category',
categories: labels
},
yaxis: {
labels: {
formatter: (val) => `$${val}`
}
}
};
return (
<Paper className="flex flex-col flex-auto p-24 shadow rounded-2xl overflow-hidden">
<div className="flex items-start justify-between">
<div className="text-lg font-medium tracking-tight leading-6 truncate">Monthly Expenses</div>
<div className="ml-8 -mt-8 -mr-12">
<IconButton>
<FuseSvgIcon size={20}>heroicons-solid:dots-vertical</FuseSvgIcon>
</IconButton>
</div>
</div>
<div className="flex items-center mt-4">
<div className="flex flex-col">
<div className="text-3xl font-semibold tracking-tight leading-tight">
{amount.toLocaleString('en-US', {
style: 'currency',
currency: 'USD'
})}
</div>
<div className="flex items-center">
<FuseSvgIcon
className="mr-4 text-red-500"
size={20}
>
heroicons-solid:trending-up
</FuseSvgIcon>
<Typography className="font-medium text-sm text-secondary leading-none whitespace-nowrap">
<span className="text-red-500">2%</span>
<span> above projected</span>
</Typography>
</div>
</div>
<div className="flex flex-col flex-auto ml-32">
<ReactApexChart
className="flex-auto w-full h-64"
options={chartOptions}
series={series}
type={chartOptions?.chart?.type}
height={chartOptions?.chart?.height}
/>
</div>
</div>
</Paper>
);
}
export default MonthlyExpensesWidget;

View File

@ -0,0 +1,101 @@
import Paper from '@mui/material/Paper';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import IconButton from '@mui/material/IconButton';
import ReactApexChart from 'react-apexcharts';
import { useTheme } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import { ApexOptions } from 'apexcharts';
import { useSelector } from 'react-redux';
import ExpensesDataType from './types/ExpensesDataType';
import { selectWidget } from '../../../ProjectDashboardApi';
/**
* The MonthlyExpensesWidget widget.
*/
function WeeklyExpensesWidget() {
const widget = useSelector(selectWidget<ExpensesDataType>('weeklyExpenses'));
if (!widget) {
return null;
}
const { amount, series, labels } = widget;
const theme = useTheme();
const chartOptions: ApexOptions = {
chart: {
animations: {
enabled: false
},
fontFamily: 'inherit',
foreColor: 'inherit',
height: '100%',
type: 'line',
sparkline: {
enabled: true
}
},
colors: [theme.palette.secondary.main],
stroke: {
curve: 'smooth'
},
tooltip: {
theme: 'dark'
},
xaxis: {
type: 'category',
categories: labels
},
yaxis: {
labels: {
formatter: (val) => `$${val}`
}
}
};
return (
<Paper className="flex flex-col flex-auto p-24 shadow rounded-2xl overflow-hidden">
<div className="flex items-start justify-between">
<div className="text-lg font-medium tracking-tight leading-6 truncate">Weekly Expenses</div>
<div className="ml-8 -mt-8 -mr-12">
<IconButton>
<FuseSvgIcon size={20}>heroicons-solid:dots-vertical</FuseSvgIcon>
</IconButton>
</div>
</div>
<div className="flex items-center mt-4">
<div className="flex flex-col">
<div className="text-3xl font-semibold tracking-tight leading-tight">
{amount.toLocaleString('en-US', {
style: 'currency',
currency: 'USD'
})}
</div>
<div className="flex items-center">
<FuseSvgIcon
className="mr-4 text-green-500"
size={20}
>
heroicons-solid:trending-down
</FuseSvgIcon>
<Typography className="font-medium text-sm text-secondary leading-none whitespace-nowrap">
<span className="text-green-500">2%</span>
<span> below projected</span>
</Typography>
</div>
</div>
<div className="flex flex-col flex-auto ml-32">
<ReactApexChart
className="flex-auto w-full h-64"
options={chartOptions}
series={series}
type={chartOptions?.chart?.type}
height={chartOptions?.chart?.height}
/>
</div>
</div>
</Paper>
);
}
export default WeeklyExpensesWidget;

View File

@ -0,0 +1,100 @@
import Paper from '@mui/material/Paper';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import IconButton from '@mui/material/IconButton';
import ReactApexChart from 'react-apexcharts';
import { useTheme } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import { ApexOptions } from 'apexcharts';
import { useSelector } from 'react-redux';
import ExpensesDataType from './types/ExpensesDataType';
import { selectWidget } from '../../../ProjectDashboardApi';
/**
* The YearlyExpensesWidget widget.
*/
function YearlyExpensesWidget() {
const widget = useSelector(selectWidget<ExpensesDataType>('yearlyExpenses'));
if (!widget) {
return null;
}
const { amount, series, labels } = widget;
const theme = useTheme();
const chartOptions: ApexOptions = {
chart: {
animations: {
enabled: false
},
fontFamily: 'inherit',
foreColor: 'inherit',
height: '100%',
type: 'line',
sparkline: {
enabled: true
}
},
colors: [theme.palette.error.main],
stroke: {
curve: 'smooth'
},
tooltip: {
theme: 'dark'
},
xaxis: {
type: 'category',
categories: labels
},
yaxis: {
labels: {
formatter: (val) => `$${val}`
}
}
};
return (
<Paper className="flex flex-col flex-auto p-24 shadow rounded-2xl overflow-hidden">
<div className="flex items-start justify-between">
<div className="text-lg font-medium tracking-tight leading-6 truncate">Yearly Expenses</div>
<div className="ml-8 -mt-8 -mr-12">
<IconButton>
<FuseSvgIcon size={20}>heroicons-solid:dots-vertical</FuseSvgIcon>
</IconButton>
</div>
</div>
<div className="flex items-center mt-4">
<div className="flex flex-col">
<div className="text-3xl font-semibold tracking-tight leading-tight">
{amount.toLocaleString('en-US', {
style: 'currency',
currency: 'USD'
})}
</div>
<div className="flex items-center">
<FuseSvgIcon
className="mr-4 text-red-500"
size={20}
>
heroicons-solid:trending-up
</FuseSvgIcon>
<Typography className="font-medium text-sm text-secondary leading-none whitespace-nowrap">
<span className="text-red-500">2%</span>
<span> above projected</span>
</Typography>
</div>
</div>
<div className="flex flex-col flex-auto ml-32">
<ReactApexChart
className="flex-auto w-full h-64"
options={chartOptions}
series={series}
type={chartOptions?.chart?.type}
height={chartOptions?.chart?.height}
/>
</div>
</div>
</Paper>
);
}
export default YearlyExpensesWidget;

View File

@ -0,0 +1,21 @@
/**
* The type definition for a row in the budget details table.
*/
type BudgetDetailsRow = {
type: string;
total: number;
expensesAmount: number;
expensesPercentage: number;
remainingAmount: number;
remainingPercentage: number;
};
/**
* The type definition for the data used to populate the budget details table.
*/
type BudgetDetailsDataType = {
columns: string[];
rows: BudgetDetailsRow[];
};
export default BudgetDetailsDataType;

View File

@ -0,0 +1,17 @@
/**
* The type definition for the data used to populate the budget distribution chart series.
*/
type BudgetDistributionSeriesData = {
name: string;
data: number[];
};
/**
* The type definition for the data used to populate the budget distribution chart.
*/
type BudgetDistributionDataType = {
categories: string[];
series: BudgetDistributionSeriesData[];
};
export default BudgetDistributionDataType;

View File

@ -0,0 +1,18 @@
/**
* The type definition for the data used to populate the expenses chart.
*/
type ExpensesSeriesData = {
name: string;
data: number[];
};
/**
* The type definition for the data used to populate the expenses chart.
*/
type ExpensesDataType = {
amount: number;
labels: string[];
series: ExpensesSeriesData[];
};
export default ExpensesDataType;

View File

@ -0,0 +1,68 @@
import { motion } from 'framer-motion';
import SummaryWidget from './widgets/SummaryWidget';
import OverdueWidget from './widgets/OverdueWidget';
import IssuesWidget from './widgets/IssuesWidget';
import FeaturesWidget from './widgets/FeaturesWidget';
import GithubIssuesWidget from './widgets/GithubIssuesWidget';
import TaskDistributionWidget from './widgets/TaskDistributionWidget';
import ScheduleWidget from './widgets/ScheduleWidget';
/**
* The HomeTab component.
*/
function HomeTab() {
const container = {
show: {
transition: {
staggerChildren: 0.04
}
}
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
};
return (
<motion.div
className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-24 w-full min-w-0 p-24"
variants={container}
initial="hidden"
animate="show"
>
<motion.div variants={item}>
<SummaryWidget />
</motion.div>
<motion.div variants={item}>
<OverdueWidget />
</motion.div>
<motion.div variants={item}>
<IssuesWidget />
</motion.div>
<motion.div variants={item}>
<FeaturesWidget />
</motion.div>
<motion.div
variants={item}
className="sm:col-span-2 md:col-span-4"
>
<GithubIssuesWidget />
</motion.div>
<motion.div
variants={item}
className="sm:col-span-2 md:col-span-4 lg:col-span-2"
>
<TaskDistributionWidget />
</motion.div>
<motion.div
variants={item}
className="sm:col-span-2 md:col-span-4 lg:col-span-2"
>
<ScheduleWidget />
</motion.div>
</motion.div>
);
}
export default HomeTab;

View File

@ -0,0 +1,54 @@
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { memo } from 'react';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import { useSelector } from 'react-redux';
import WidgetDataType from './types/WidgetDataType';
import { selectWidget } from '../../../ProjectDashboardApi';
/**
* The FeaturesWidget widget.
*/
function FeaturesWidget() {
const widget = useSelector(selectWidget<WidgetDataType>('features'));
if (!widget) {
return null;
}
const { data, title } = widget;
return (
<Paper className="flex flex-col flex-auto shadow rounded-2xl overflow-hidden">
<div className="flex items-center justify-between px-8 pt-12">
<Typography
className="px-16 text-lg font-medium tracking-tight leading-6 truncate"
color="text.secondary"
>
{title}
</Typography>
<IconButton
aria-label="more"
size="large"
>
<FuseSvgIcon>heroicons-outline:dots-vertical</FuseSvgIcon>
</IconButton>
</div>
<div className="text-center mt-8">
<Typography className="text-7xl sm:text-8xl font-bold tracking-tight leading-none text-green-500">
{String(data.count)}
</Typography>
<Typography className="text-lg font-medium text-green-600">{data.name}</Typography>
</div>
<Typography
className="flex items-baseline justify-center w-full mt-20 mb-24"
color="text.secondary"
>
<span className="truncate">{data.extra.name}</span>:<b className="px-8">{String(data.extra.count)}</b>
</Typography>
</Paper>
);
}
export default memo(FeaturesWidget);

View File

@ -0,0 +1,249 @@
import Paper from '@mui/material/Paper';
import { lighten, useTheme } from '@mui/material/styles';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import Typography from '@mui/material/Typography';
import { memo, useEffect, useState } from 'react';
import ReactApexChart from 'react-apexcharts';
import Box from '@mui/material/Box';
import { ApexOptions } from 'apexcharts';
import { useSelector } from 'react-redux';
import GithubIssuesDataType from './types/GithubIssuesDataType';
import { selectWidget } from '../../../ProjectDashboardApi';
/**
* The GithubIssuesWidget widget.
*/
function GithubIssuesWidget() {
const theme = useTheme();
const [awaitRender, setAwaitRender] = useState(true);
const [tabValue, setTabValue] = useState(0);
const widget = useSelector(selectWidget<GithubIssuesDataType>('githubIssues'));
if (!widget) {
return null;
}
const { overview, series, ranges, labels } = widget;
const currentRange = Object.keys(ranges)[tabValue];
const chartOptions: ApexOptions = {
chart: {
fontFamily: 'inherit',
foreColor: 'inherit',
height: '100%',
type: 'line',
toolbar: {
show: false
},
zoom: {
enabled: false
}
},
colors: [theme.palette.primary.main, theme.palette.secondary.main],
labels,
dataLabels: {
enabled: true,
enabledOnSeries: [0],
background: {
borderWidth: 0
}
},
grid: {
borderColor: theme.palette.divider
},
legend: {
show: false
},
plotOptions: {
bar: {
columnWidth: '50%'
}
},
states: {
hover: {
filter: {
type: 'darken',
value: 0.75
}
}
},
stroke: {
width: [3, 0]
},
tooltip: {
followCursor: true,
theme: theme.palette.mode
},
xaxis: {
axisBorder: {
show: false
},
axisTicks: {
color: theme.palette.divider
},
labels: {
style: {
colors: theme.palette.text.secondary
}
},
tooltip: {
enabled: false
}
},
yaxis: {
labels: {
offsetX: -16,
style: {
colors: theme.palette.text.secondary
}
}
}
};
useEffect(() => {
setAwaitRender(false);
}, []);
if (awaitRender) {
return null;
}
return (
<Paper className="flex flex-col flex-auto p-24 shadow rounded-2xl overflow-hidden">
<div className="flex flex-col sm:flex-row items-start justify-between">
<Typography className="text-lg font-medium tracking-tight leading-6 truncate">
Github Issues Summary
</Typography>
<div className="mt-12 sm:mt-0 sm:ml-8">
<Tabs
value={tabValue}
onChange={(ev, value: number) => setTabValue(value)}
indicatorColor="secondary"
textColor="inherit"
variant="scrollable"
scrollButtons={false}
className="-mx-4 min-h-40"
classes={{ indicator: 'flex justify-center bg-transparent w-full h-full' }}
TabIndicatorProps={{
children: (
<Box
sx={{ bgcolor: 'text.disabled' }}
className="w-full h-full rounded-full opacity-20"
/>
)
}}
>
{Object.entries(ranges).map(([key, label]) => (
<Tab
className="text-14 font-semibold min-h-40 min-w-64 mx-4 px-12"
disableRipple
key={key}
label={label}
/>
))}
</Tabs>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 grid-flow-row gap-24 w-full mt-32 sm:mt-16">
<div className="flex flex-col flex-auto">
<Typography
className="font-medium"
color="text.secondary"
>
New vs. Closed
</Typography>
<div className="flex flex-col flex-auto">
<ReactApexChart
className="flex-auto w-full"
options={chartOptions}
series={series[currentRange]}
height={320}
/>
</div>
</div>
<div className="flex flex-col">
<Typography
className="font-medium"
color="text.secondary"
>
Overview
</Typography>
<div className="flex-auto grid grid-cols-4 gap-16 mt-24">
<div className="col-span-2 flex flex-col items-center justify-center py-32 px-4 rounded-2xl bg-indigo-50 text-indigo-800">
<Typography className="text-5xl sm:text-7xl font-semibold leading-none tracking-tight">
{overview[currentRange]['new-issues']}
</Typography>
<Typography className="mt-4 text-sm sm:text-lg font-medium">New Issues</Typography>
</div>
<div className="col-span-2 flex flex-col items-center justify-center py-32 px-4 rounded-2xl bg-green-50 text-green-800">
<Typography className="text-5xl sm:text-7xl font-semibold leading-none tracking-tight">
{overview[currentRange]['closed-issues']}
</Typography>
<Typography className="mt-4 text-sm sm:text-lg font-medium">Closed</Typography>
</div>
<Box
sx={{
backgroundColor: (_theme) =>
_theme.palette.mode === 'light'
? lighten(theme.palette.background.default, 0.4)
: lighten(theme.palette.background.default, 0.02)
}}
className="col-span-2 sm:col-span-1 flex flex-col items-center justify-center py-32 px-4 rounded-2xl"
>
<Typography className="text-5xl font-semibold leading-none tracking-tight">
{overview[currentRange].fixed}
</Typography>
<Typography className="mt-4 text-sm font-medium text-center">Fixed</Typography>
</Box>
<Box
sx={{
backgroundColor: (_theme) =>
_theme.palette.mode === 'light'
? lighten(theme.palette.background.default, 0.4)
: lighten(theme.palette.background.default, 0.02)
}}
className="col-span-2 sm:col-span-1 flex flex-col items-center justify-center py-32 px-4 rounded-2xl"
>
<Typography className="text-5xl font-semibold leading-none tracking-tight">
{overview[currentRange]['wont-fix']}
</Typography>
<Typography className="mt-4 text-sm font-medium text-center">Won't Fix</Typography>
</Box>
<Box
sx={{
backgroundColor: (_theme) =>
_theme.palette.mode === 'light'
? lighten(theme.palette.background.default, 0.4)
: lighten(theme.palette.background.default, 0.02)
}}
className="col-span-2 sm:col-span-1 flex flex-col items-center justify-center py-32 px-4 rounded-2xl"
>
<Typography className="text-5xl font-semibold leading-none tracking-tight">
{overview[currentRange]['re-opened']}
</Typography>
<Typography className="mt-4 text-sm font-medium text-center">Re-opened</Typography>
</Box>
<Box
sx={{
backgroundColor: (_theme) =>
_theme.palette.mode === 'light'
? lighten(theme.palette.background.default, 0.4)
: lighten(theme.palette.background.default, 0.02)
}}
className="col-span-2 sm:col-span-1 flex flex-col items-center justify-center py-32 px-4 rounded-2xl"
>
<Typography className="text-5xl font-semibold leading-none tracking-tight">
{overview[currentRange]['needs-triage']}
</Typography>
<Typography className="mt-4 text-sm font-medium text-center">Needs Triage</Typography>
</Box>
</div>
</div>
</div>
</Paper>
);
}
export default memo(GithubIssuesWidget);

View File

@ -0,0 +1,53 @@
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { memo } from 'react';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import { useSelector } from 'react-redux';
import { selectWidget } from '../../../ProjectDashboardApi';
import WidgetDataType from './types/WidgetDataType';
/**
* The IssuesWidget widget.
*/
function IssuesWidget() {
const widget = useSelector(selectWidget<WidgetDataType>('issues'));
if (!widget) {
return null;
}
const { data, title } = widget;
return (
<Paper className="flex flex-col flex-auto shadow rounded-2xl overflow-hidden">
<div className="flex items-center justify-between px-8 pt-12">
<Typography
className="px-16 text-lg font-medium tracking-tight leading-6 truncate"
color="text.secondary"
>
{title}
</Typography>
<IconButton
aria-label="more"
size="large"
>
<FuseSvgIcon>heroicons-outline:dots-vertical</FuseSvgIcon>
</IconButton>
</div>
<div className="text-center mt-8">
<Typography className="text-7xl sm:text-8xl font-bold tracking-tight leading-none text-amber-500">
{String(data.count)}
</Typography>
<Typography className="text-lg font-medium text-amber-600">{data.name}</Typography>
</div>
<Typography
className="flex items-baseline justify-center w-full mt-20 mb-24"
color="text.secondary"
>
<span className="truncate">{data.extra.name}</span>:<b className="px-8">{String(data.extra.count)}</b>
</Typography>
</Paper>
);
}
export default memo(IssuesWidget);

View File

@ -0,0 +1,54 @@
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { memo } from 'react';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import { useSelector } from 'react-redux';
import { selectWidget } from '../../../ProjectDashboardApi';
import WidgetDataType from './types/WidgetDataType';
/**
* The OverdueWidget widget.
*/
function OverdueWidget() {
const widget = useSelector(selectWidget<WidgetDataType>('overdue'));
if (!widget) {
return null;
}
const { data, title } = widget;
return (
<Paper className="flex flex-col flex-auto shadow rounded-2xl overflow-hidden">
<div className="flex items-center justify-between px-8 pt-12">
<Typography
className="px-16 text-lg font-medium tracking-tight leading-6 truncate"
color="text.secondary"
>
{title}
</Typography>
<IconButton
aria-label="more"
size="large"
>
<FuseSvgIcon>heroicons-outline:dots-vertical</FuseSvgIcon>
</IconButton>
</div>
<div className="text-center mt-8">
<Typography className="text-7xl sm:text-8xl font-bold tracking-tight leading-none text-red-500">
{String(data.count)}
</Typography>
<Typography className="text-lg font-medium text-red-600">{data.name}</Typography>
</div>
<Typography
className="flex items-baseline justify-center w-full mt-20 mb-24"
color="text.secondary"
>
<span className="truncate">{data.extra.name}</span>:<b className="px-8">{String(data.extra.count)}</b>
</Typography>
</Paper>
);
}
export default memo(OverdueWidget);

View File

@ -0,0 +1,129 @@
import IconButton from '@mui/material/IconButton';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import ListItemText from '@mui/material/ListItemText';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { memo, useState } from 'react';
import Tabs from '@mui/material/Tabs';
import Box from '@mui/material/Box';
import Tab from '@mui/material/Tab';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import { useSelector } from 'react-redux';
import { selectWidget } from '../../../ProjectDashboardApi';
import ScheduleDataType from './types/ScheduleDataType';
/**
* The ScheduleWidget widget.
*/
function ScheduleWidget() {
const widget = useSelector(selectWidget<ScheduleDataType>('schedule'));
if (!widget) {
return null;
}
const { series, ranges } = widget;
const [tabValue, setTabValue] = useState(0);
const currentRange = Object.keys(ranges)[tabValue];
return (
<Paper className="flex flex-col flex-auto p-24 shadow rounded-2xl overflow-hidden h-full">
<div className="flex flex-col sm:flex-row items-start justify-between">
<Typography className="text-lg font-medium tracking-tight leading-6 truncate">Schedule</Typography>
<div className="mt-12 sm:mt-0 sm:ml-8">
<Tabs
value={tabValue}
onChange={(ev, value: number) => setTabValue(value)}
indicatorColor="secondary"
textColor="inherit"
variant="scrollable"
scrollButtons={false}
className="-mx-16 min-h-40"
classes={{ indicator: 'flex justify-center bg-transparent w-full h-full' }}
TabIndicatorProps={{
children: (
<Box
sx={{ bgcolor: 'text.disabled' }}
className="w-full h-full rounded-full opacity-20"
/>
)
}}
>
{Object.entries(ranges).map(([key, label]) => (
<Tab
className="text-14 font-semibold min-h-40 min-w-64 mx-4 px-12"
disableRipple
key={key}
label={label}
/>
))}
</Tabs>
</div>
</div>
<List className="py-0 mt-8 divide-y">
{series[currentRange].map((item, index) => (
<ListItem
key={index}
className="px-0"
>
<ListItemText
classes={{ root: 'px-8', primary: 'font-medium' }}
primary={item.title}
secondary={
<span className="flex flex-col sm:flex-row sm:items-center -ml-2 mt-8 sm:mt-4 space-y-4 sm:space-y-0 sm:space-x-12">
{item.time && (
<span className="flex items-center">
<FuseSvgIcon
size={20}
color="disabled"
>
heroicons-solid:clock
</FuseSvgIcon>
<Typography
component="span"
className="mx-6 text-md"
color="text.secondary"
>
{item.time}
</Typography>
</span>
)}
{item.location && (
<span className="flex items-center">
<FuseSvgIcon
size={20}
color="disabled"
>
heroicons-solid:location-marker
</FuseSvgIcon>
<Typography
component="span"
className="mx-6 text-md"
color="text.secondary"
>
{item.location}
</Typography>
</span>
)}
</span>
}
/>
<ListItemSecondaryAction>
<IconButton
aria-label="more"
size="large"
>
<FuseSvgIcon>heroicons-solid:chevron-right</FuseSvgIcon>
</IconButton>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</Paper>
);
}
export default memo(ScheduleWidget);

View File

@ -0,0 +1,79 @@
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
import Select, { SelectChangeEvent } from '@mui/material/Select';
import Typography from '@mui/material/Typography';
import { memo, useState } from 'react';
import MenuItem from '@mui/material/MenuItem';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import { useSelector } from 'react-redux';
import { selectWidget } from '../../../ProjectDashboardApi';
import WidgetDataType, { RangeType } from './types/WidgetDataType';
/**
* The SummaryWidget widget.
*/
function SummaryWidget() {
const widget = useSelector(selectWidget<WidgetDataType>('summary'));
if (!widget) {
return null;
}
const { data, ranges, currentRange: currentRangeDefault } = widget;
const [currentRange, setCurrentRange] = useState<RangeType>(currentRangeDefault as RangeType);
function handleChangeRange(event: SelectChangeEvent<string>) {
setCurrentRange(event.target.value as RangeType);
}
return (
<Paper className="flex flex-col flex-auto shadow rounded-2xl overflow-hidden">
<div className="flex items-center justify-between px-8 pt-12">
<Select
className="mx-16"
classes={{ select: 'py-0 flex items-center' }}
value={currentRange}
onChange={handleChangeRange}
inputProps={{
name: 'currentRange'
}}
variant="filled"
size="small"
>
{Object.entries(ranges).map(([key, n]) => {
return (
<MenuItem
key={key}
value={key}
>
{n}
</MenuItem>
);
})}
</Select>
<IconButton
aria-label="more"
size="large"
>
<FuseSvgIcon>heroicons-outline:dots-vertical</FuseSvgIcon>
</IconButton>
</div>
<div className="text-center mt-8">
<Typography className="text-7xl sm:text-8xl font-bold tracking-tight leading-none text-blue-500">
{data.count[currentRange]}
</Typography>
<Typography className="text-lg font-medium text-blue-600 dark:text-blue-500">{data.name}</Typography>
</div>
<Typography
className="flex items-baseline justify-center w-full mt-20 mb-24"
color="text.secondary"
>
<span className="truncate">{data.extra.name}</span>:
<b className="px-8">{data.extra.count[currentRange]}</b>
</Typography>
</Paper>
);
}
export default memo(SummaryWidget);

View File

@ -0,0 +1,167 @@
import Paper from '@mui/material/Paper';
import { lighten, useTheme } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import { memo, useEffect, useState } from 'react';
import ReactApexChart from 'react-apexcharts';
import Tabs from '@mui/material/Tabs';
import Box from '@mui/material/Box';
import Tab from '@mui/material/Tab';
import { ApexOptions } from 'apexcharts';
import { useSelector } from 'react-redux';
import { selectWidget } from '../../../ProjectDashboardApi';
import TaskDistributionDataType from './types/TaskDistributionDataType';
/**
* The TaskDistributionWidget widget.
*/
function TaskDistributionWidget() {
const widget = useSelector(selectWidget<TaskDistributionDataType>('taskDistribution'));
if (!widget) {
return null;
}
const { overview, series, labels, ranges } = widget;
const [tabValue, setTabValue] = useState(0);
const currentRange = Object.keys(ranges)[tabValue];
const [awaitRender, setAwaitRender] = useState(true);
const theme = useTheme();
const chartOptions: ApexOptions = {
chart: {
fontFamily: 'inherit',
foreColor: 'inherit',
height: '100%',
type: 'polarArea',
toolbar: {
show: false
},
zoom: {
enabled: false
}
},
labels,
legend: {
position: 'bottom'
},
plotOptions: {
polarArea: {
spokes: {
connectorColors: theme.palette.divider
},
rings: {
strokeColor: theme.palette.divider
}
}
},
states: {
hover: {
filter: {
type: 'darken',
value: 0.75
}
}
},
stroke: {
width: 2
},
theme: {
monochrome: {
enabled: true,
color: theme.palette.secondary.main,
shadeIntensity: 0.75,
shadeTo: 'dark'
}
},
tooltip: {
followCursor: true,
theme: 'dark'
},
yaxis: {
labels: {
style: {
colors: theme.palette.text.secondary
}
}
}
};
useEffect(() => {
setAwaitRender(false);
}, []);
if (awaitRender) {
return null;
}
return (
<Paper className="flex flex-col flex-auto p-24 shadow rounded-2xl overflow-hidden h-full">
<div className="flex flex-col sm:flex-row items-start justify-between">
<Typography className="text-lg font-medium tracking-tight leading-6 truncate">
Task Distribution
</Typography>
<div className="mt-3 sm:mt-0 sm:ml-2">
<Tabs
value={tabValue}
onChange={(ev, value: number) => setTabValue(value)}
indicatorColor="secondary"
textColor="inherit"
variant="scrollable"
scrollButtons={false}
className="-mx-4 min-h-40"
classes={{ indicator: 'flex justify-center bg-transparent w-full h-full' }}
TabIndicatorProps={{
children: (
<Box
sx={{ bgcolor: 'text.disabled' }}
className="w-full h-full rounded-full opacity-20"
/>
)
}}
>
{Object.entries(ranges).map(([key, label]) => (
<Tab
className="text-14 font-semibold min-h-40 min-w-64 mx-4 px-12"
disableRipple
key={key}
label={label}
/>
))}
</Tabs>
</div>
</div>
<div className="flex flex-col flex-auto mt-6">
<ReactApexChart
className="flex-auto w-full"
options={chartOptions}
series={series[currentRange]}
type={chartOptions?.chart?.type}
/>
</div>
<Box
sx={{
backgroundColor: (_theme) =>
_theme.palette.mode === 'light'
? lighten(theme.palette.background.default, 0.4)
: lighten(theme.palette.background.default, 0.02)
}}
className="grid grid-cols-2 border-t divide-x -m-24 mt-16"
>
<div className="flex flex-col items-center justify-center p-24 sm:p-32">
<div className="text-5xl font-semibold leading-none tracking-tighter">
{overview[currentRange].new}
</div>
<Typography className="mt-4 text-center text-secondary">New tasks</Typography>
</div>
<div className="flex flex-col items-center justify-center p-6 sm:p-8">
<div className="text-5xl font-semibold leading-none tracking-tighter">
{overview[currentRange].completed}
</div>
<Typography className="mt-4 text-center text-secondary">Completed tasks</Typography>
</div>
</Box>
</Paper>
);
}
export default memo(TaskDistributionWidget);

View File

@ -0,0 +1,21 @@
type GithubIssueOverviewData = {
[key: string]: number;
};
type GithubIssueSeriesData = {
name: string;
type: string;
data: number[];
};
/**
* The type definition for the data used to populate the github issues chart.
*/
type GithubIssuesDataType = {
overview: Record<string, GithubIssueOverviewData>;
ranges: Record<string, string>;
labels: string[];
series: Record<string, GithubIssueSeriesData[]>;
};
export default GithubIssuesDataType;

View File

@ -0,0 +1,15 @@
type ScheduleItem = {
title: string;
time: string;
location?: string;
};
/**
* The type definition for the data used to populate the schedule.
*/
type ScheduleDataType = {
ranges: Record<string, string>;
series: Record<string, ScheduleItem[]>;
};
export default ScheduleDataType;

View File

@ -0,0 +1,19 @@
/**
* The type definition for the data used to populate the task distribution chart.
*/
type TaskDistributionOverviewData = {
new: number;
completed: number;
};
/**
* The type definition for the data used to populate the task distribution chart.
*/
type TaskDistributionDataType = {
ranges: Record<string, string>;
overview: Record<string, TaskDistributionOverviewData>;
labels: string[];
series: Record<string, number[]>;
};
export default TaskDistributionDataType;

View File

@ -0,0 +1,25 @@
type ExtraData = {
name: string;
count: Record<RangeType, number>;
};
type WidgetInnerData = {
name: string;
count: Record<RangeType, number>;
extra: ExtraData;
};
/**
* The type definition for the data used to populate the widget.
*/
type WidgetDataType = {
title?: string;
ranges: Record<RangeType, string>;
currentRange?: string;
data: WidgetInnerData;
detail?: string;
};
export type RangeType = 'DY' | 'DT' | 'DTM';
export default WidgetDataType;

View File

@ -0,0 +1,38 @@
import { motion } from 'framer-motion';
import TeamMembersWidget from './widgets/TeamMembersWidget';
/**
* The TeamTab component.
*/
function TeamTab() {
const container = {
show: {
transition: {
staggerChildren: 0.04
}
}
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
};
return (
<motion.div
className="flex flex-wrap p-24"
variants={container}
initial="hidden"
animate="show"
>
<motion.div
variants={item}
className="widget flex w-full"
>
<TeamMembersWidget />
</motion.div>
</motion.div>
);
}
export default TeamTab;

View File

@ -0,0 +1,92 @@
import Typography from '@mui/material/Typography';
import { memo } from 'react';
import Paper from '@mui/material/Paper';
import { motion } from 'framer-motion';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import { useSelector } from 'react-redux';
import { selectWidget } from '../../../ProjectDashboardApi';
import TeamMemberType from './types/TeamMemberType';
/**
* The TeamMembersWidget widget.
*/
function TeamMembersWidget() {
const members = useSelector(selectWidget<TeamMemberType[]>('teamMembers'));
if (!members) {
return null;
}
const container = {
show: {
transition: {
staggerChildren: 0.04
}
}
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
};
return (
<motion.div
variants={container}
initial="hidden"
animate="show"
className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-24 w-full min-w-0"
>
{members.map((member) => (
<Paper
component={motion.div}
variants={item}
className="flex flex-col flex-auto items-center shadow rounded-2xl overflow-hidden"
key={member.id}
>
<div className="flex flex-col flex-auto w-full p-32 text-center">
<div className="w-128 h-128 mx-auto rounded-full overflow-hidden">
<img
className="w-full h-full object-cover"
src={member.avatar}
alt="member"
/>
</div>
<Typography className="mt-24 font-medium">{member.name}</Typography>
<Typography color="text.secondary">{member.title}</Typography>
</div>
<div className="flex items-center w-full border-t divide-x">
<a
className="flex flex-auto items-center justify-center py-16 hover:bg-hover"
href={`mailto:${member.email}`}
role="button"
>
<FuseSvgIcon
size={20}
color="action"
>
heroicons-solid:mail
</FuseSvgIcon>
<Typography className="ml-8">Email</Typography>
</a>
<a
className="flex flex-auto items-center justify-center py-16 hover:bg-hover"
href={`tel${member.phone}`}
role="button"
>
<FuseSvgIcon
size={20}
color="action"
>
heroicons-solid:phone
</FuseSvgIcon>
<Typography className="ml-8">Call</Typography>
</a>
</div>
</Paper>
))}
</motion.div>
);
}
export default memo(TeamMembersWidget);

View File

@ -0,0 +1,13 @@
/**
* Team Member Type
*/
type TeamMemberType = {
id: string;
avatar: string;
name: string;
email: string;
phone: string;
title: string;
};
export default TeamMemberType;

View File

@ -3288,6 +3288,11 @@
"@types/babel__core" "^7.20.5"
react-refresh "^0.14.0"
"@yr/monotone-cubic-spline@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz#7272d89f8e4f6fb7a1600c28c378cc18d3b577b9"
integrity sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==
acorn-jsx@^5.3.1, acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@ -3395,6 +3400,19 @@ anymatch@~3.1.2:
normalize-path "^3.0.0"
picomatch "^2.0.4"
apexcharts@^3.45.2:
version "3.45.2"
resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.45.2.tgz#18a6343da122c12d71ac9f35260aacd31b16fbfb"
integrity sha512-PpuM4sJWy70sUh5U1IFn1m1p45MdHSChLUNnqEoUUUHSU2IHZugFrsVNhov1S8Q0cvfdrCRCvdBtHGSs6PSAWQ==
dependencies:
"@yr/monotone-cubic-spline" "^1.0.3"
svg.draggable.js "^2.2.2"
svg.easing.js "^2.0.0"
svg.filter.js "^2.0.2"
svg.pathmorphing.js "^0.1.3"
svg.resize.js "^1.4.3"
svg.select.js "^3.0.1"
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
@ -7323,6 +7341,13 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
react-apexcharts@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/react-apexcharts/-/react-apexcharts-1.4.1.tgz#95ab31e4d2201308f59f3d2a4b65d10d9d0ea4bb"
integrity sha512-G14nVaD64Bnbgy8tYxkjuXEUp/7h30Q0U33xc3AwtGFijJB9nHqOt1a6eG0WBn055RgRg+NwqbKGtqPxy15d0Q==
dependencies:
prop-types "^15.8.1"
react-app-alias@2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/react-app-alias/-/react-app-alias-2.2.2.tgz#2b486cd21cdba362df9ef71a06ab73e0a8ea660d"
@ -8274,6 +8299,61 @@ svg-parser@^2.0.4:
resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
svg.draggable.js@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz#c514a2f1405efb6f0263e7958f5b68fce50603ba"
integrity sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==
dependencies:
svg.js "^2.0.1"
svg.easing.js@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/svg.easing.js/-/svg.easing.js-2.0.0.tgz#8aa9946b0a8e27857a5c40a10eba4091e5691f12"
integrity sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==
dependencies:
svg.js ">=2.3.x"
svg.filter.js@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/svg.filter.js/-/svg.filter.js-2.0.2.tgz#91008e151389dd9230779fcbe6e2c9a362d1c203"
integrity sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==
dependencies:
svg.js "^2.2.5"
svg.js@>=2.3.x, svg.js@^2.0.1, svg.js@^2.2.5, svg.js@^2.4.0, svg.js@^2.6.5:
version "2.7.1"
resolved "https://registry.yarnpkg.com/svg.js/-/svg.js-2.7.1.tgz#eb977ed4737001eab859949b4a398ee1bb79948d"
integrity sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==
svg.pathmorphing.js@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz#c25718a1cc7c36e852ecabc380e758ac09bb2b65"
integrity sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==
dependencies:
svg.js "^2.4.0"
svg.resize.js@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/svg.resize.js/-/svg.resize.js-1.4.3.tgz#885abd248e0cd205b36b973c4b578b9a36f23332"
integrity sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==
dependencies:
svg.js "^2.6.5"
svg.select.js "^2.1.2"
svg.select.js@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-2.1.2.tgz#e41ce13b1acff43a7441f9f8be87a2319c87be73"
integrity sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==
dependencies:
svg.js "^2.2.5"
svg.select.js@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-3.0.1.tgz#a4198e359f3825739226415f82176a90ea5cc917"
integrity sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==
dependencies:
svg.js "^2.6.5"
swagger2openapi@^7.0.4, swagger2openapi@^7.0.8:
version "7.0.8"
resolved "https://registry.yarnpkg.com/swagger2openapi/-/swagger2openapi-7.0.8.tgz#12c88d5de776cb1cbba758994930f40ad0afac59"