Cambios
This commit is contained in:
parent
fbb8138a91
commit
f8a0ee671e
|
|
@ -35,3 +35,5 @@ yarn-error.log*
|
|||
!.yarn/versions
|
||||
.pnp.*
|
||||
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
const locale = {
|
||||
APPLICATIONS: 'Applications',
|
||||
EXAMPLE: 'Example',
|
||||
DASHBOARD: 'Tablero'
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
|
@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Team Member Type
|
||||
*/
|
||||
type TeamMemberType = {
|
||||
id: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export default TeamMemberType;
|
||||
80
yarn.lock
80
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue