QQQ-41: added app sections, wired all dashboards, implemented widgets that could be, upped version

This commit is contained in:
Tim Chamberlain
2022-09-16 10:27:50 -05:00
parent b3f32572dc
commit f1300d2db9
49 changed files with 33588 additions and 32045 deletions

62374
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,114 +1,115 @@
{ {
"name": "qqq-frontend-material-dashboard", "name": "qqq-frontend-material-dashboard",
"version": "1.0.0", "version": "1.0.0",
"description": "React + TypeScript version of Material Dashboard 2 PRO by Creative Tim", "description": "QQQ Default Dashboard",
"proxy": "http://localhost:8000", "proxy": "http://localhost:8000",
"dependencies": { "dependencies": {
"@asseinfo/react-kanban": "2.2.0", "@asseinfo/react-kanban": "2.2.0",
"@auth0/auth0-react": "1.10.2", "@auth0/auth0-react": "1.10.2",
"@emotion/cache": "11.7.1", "@emotion/cache": "11.7.1",
"@emotion/react": "11.7.1", "@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0", "@emotion/styled": "11.6.0",
"@fullcalendar/daygrid": "5.10.0", "@fullcalendar/daygrid": "5.10.0",
"@fullcalendar/interaction": "5.10.0", "@fullcalendar/interaction": "5.10.0",
"@fullcalendar/react": "5.10.0", "@fullcalendar/react": "5.10.0",
"@fullcalendar/timegrid": "5.10.0", "@fullcalendar/timegrid": "5.10.0",
"@kingsrook/qqq-frontend-core": "1.0.15", "@kingsrook/qqq-frontend-core": "1.0.17",
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.4.1", "@mui/material": "5.4.1",
"@mui/styled-engine": "5.4.1", "@mui/styled-engine": "5.4.1",
"@mui/x-data-grid": "5.13.0", "@mui/x-data-grid": "5.13.0",
"@mui/x-data-grid-pro": "5.13.0", "@mui/x-data-grid-pro": "5.13.0",
"@mui/x-license-pro": "5.12.3", "@mui/x-license-pro": "5.12.3",
"@react-jvectormap/core": "1.0.1", "@react-jvectormap/core": "1.0.1",
"@react-jvectormap/unitedstates": "1.0.1", "@react-jvectormap/unitedstates": "1.0.1",
"@react-jvectormap/world": "1.0.0", "@react-jvectormap/world": "1.0.0",
"@testing-library/jest-dom": "5.16.2", "@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.2", "@testing-library/react": "12.1.2",
"@testing-library/user-event": "13.5.0", "@testing-library/user-event": "13.5.0",
"@types/jest": "27.4.0", "@types/jest": "27.4.0",
"@types/node": "16.11.21", "@types/node": "16.11.21",
"@types/react": "17.0.38", "@types/react": "17.0.38",
"@types/react-dom": "17.0.11", "@types/react-dom": "17.0.11",
"chart.js": "3.4.1", "chart.js": "3.4.1",
"chroma-js": "2.4.2", "chroma-js": "2.4.2",
"datejs": "1.0.0-rc3", "datejs": "1.0.0-rc3",
"dropzone": "5.9.2", "dropzone": "5.9.2",
"flatpickr": "4.6.9", "flatpickr": "4.6.9",
"form-data": "4.0.0", "form-data": "4.0.0",
"formik": "2.2.9", "formik": "2.2.9",
"html-react-parser": "1.4.8", "html-react-parser": "1.4.8",
"http-proxy-middleware": "2.0.6", "http-proxy-middleware": "2.0.6",
"react": "17.0.2", "react": "17.0.2",
"react-chartjs-2": "3.0.4", "react-chartjs-2": "3.0.4",
"react-cookie": "4.1.1", "react-cookie": "4.1.1",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-flatpickr": "3.10.7", "react-flatpickr": "3.10.7",
"react-github-btn": "1.2.1", "react-github-btn": "1.2.1",
"react-images-viewer": "1.7.1", "react-html-parser": "2.0.2",
"react-quill": "1.3.5", "react-images-viewer": "1.7.1",
"react-router": "6.3.0", "react-quill": "1.3.5",
"react-router-dom": "6.2.1", "react-router": "6.3.0",
"react-scripts": "5.0.0", "react-router-dom": "6.2.1",
"react-table": "7.7.0", "react-scripts": "5.0.0",
"stylis": "4.0.13", "react-table": "7.7.0",
"stylis-plugin-rtl": "2.1.1", "stylis": "4.0.13",
"ts-md5": "1.2.11", "stylis-plugin-rtl": "2.1.1",
"uuid": "8.3.2", "ts-md5": "1.2.11",
"web-vitals": "2.1.4", "uuid": "8.3.2",
"yup": "0.32.11" "web-vitals": "2.1.4",
}, "yup": "0.32.11"
"scripts": { },
"build": "react-scripts build", "scripts": {
"clean": "rm -rf node_modules package-lock.json", "build": "react-scripts build",
"eject": "react-scripts eject", "clean": "rm -rf node_modules package-lock.json",
"geff-ham": "rm -rf node_modules/ && rm -rf package-lock-bak.json && npm install --legacy-peer-deps && npm start", "eject": "react-scripts eject",
"install-legacy-peer-deps": "npm install --legacy-peer-deps", "geff-ham": "rm -rf node_modules/ && rm -rf package-lock-bak.json && npm install --legacy-peer-deps && npm start",
"prepublishOnly": "tsc -p ./ --outDir lib/", "install-legacy-peer-deps": "npm install --legacy-peer-deps",
"start": "react-scripts start", "prepublishOnly": "tsc -p ./ --outDir lib/",
"test": "react-scripts test", "start": "react-scripts start",
"cypress:open": "cypress open" "test": "react-scripts test",
}, "cypress:open": "cypress open"
"eslintConfig": { },
"extends": [ "eslintConfig": {
"react-app", "extends": [
"react-app/jest" "react-app",
] "react-app/jest"
}, ]
"browserslist": { },
"production": [ "browserslist": {
">0.2%", "production": [
"not dead", ">0.2%",
"not op_mini all" "not dead",
], "not op_mini all"
"development": [ ],
"last 1 chrome version", "development": [
"last 1 firefox version", "last 1 chrome version",
"last 1 safari version" "last 1 firefox version",
] "last 1 safari version"
}, ]
"devDependencies": { },
"@types/chroma-js": "2.1.3", "devDependencies": {
"@types/dropzone": "5.7.4", "@types/chroma-js": "2.1.3",
"@types/react-flatpickr": "3.8.5", "@types/dropzone": "5.7.4",
"@types/react-table": "7.7.9", "@types/react-flatpickr": "3.8.5",
"@types/uuid": "8.3.4", "@types/react-table": "7.7.9",
"@typescript-eslint/eslint-plugin": "5.10.2", "@types/uuid": "8.3.4",
"@typescript-eslint/parser": "5.10.2", "@typescript-eslint/eslint-plugin": "5.10.2",
"cypress": "10.3.1", "@typescript-eslint/parser": "5.10.2",
"eslint": "8.8.0", "cypress": "10.3.1",
"eslint-config-airbnb": "19.0.4", "eslint": "8.8.0",
"eslint-import-resolver-typescript": "2.5.0", "eslint-config-airbnb": "19.0.4",
"eslint-plugin-import": "2.25.4", "eslint-import-resolver-typescript": "2.5.0",
"eslint-plugin-jsx-a11y": "6.5.1", "eslint-plugin-import": "2.25.4",
"eslint-plugin-react": "7.28.0", "eslint-plugin-jsx-a11y": "6.5.1",
"eslint-plugin-react-hooks": "4.3.0", "eslint-plugin-react": "7.28.0",
"typescript": "^4.7.3" "eslint-plugin-react-hooks": "4.3.0",
}, "typescript": "^4.7.3"
"main": "qqq-frontend-material-dashboard.js", },
"module": "lib/qqq-frontend-material-dashboard.js", "main": "qqq-frontend-material-dashboard.js",
"files": [ "module": "lib/qqq-frontend-material-dashboard.js",
"lib", "files": [
"package.json" "lib",
] "package.json"
]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
public/ups.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -20,6 +20,7 @@
*/ */
import {useAuth0} from "@auth0/auth0-react"; import {useAuth0} from "@auth0/auth0-react";
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType"; import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType";
import {QAppTreeNode} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppTreeNode"; import {QAppTreeNode} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppTreeNode";
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData"; import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
@ -87,10 +88,8 @@ LicenseInfo.setLicenseKey(process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY);
export default function App() export default function App()
{ {
const [, setCookie] = useCookies([SESSION_ID_COOKIE_NAME]); const [, setCookie, removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]);
const { const {user, getAccessTokenSilently, getIdTokenClaims, logout} = useAuth0();
user, getAccessTokenSilently, getIdTokenClaims, logout, loginWithRedirect,
} = useAuth0();
const [loadingToken, setLoadingToken] = useState(false); const [loadingToken, setLoadingToken] = useState(false);
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false); const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
const [profileRoutes, setProfileRoutes] = useState({}); const [profileRoutes, setProfileRoutes] = useState({});
@ -117,7 +116,9 @@ export default function App()
catch (e) catch (e)
{ {
console.log(`Error loading token: ${JSON.stringify(e)}`); console.log(`Error loading token: ${JSON.stringify(e)}`);
removeCookie(SESSION_ID_COOKIE_NAME);
logout(); logout();
return;
} }
})(); })();
}, [loadingToken]); }, [loadingToken]);
@ -159,7 +160,7 @@ export default function App()
} }
const childList: any[] = []; const childList: any[] = [];
if(app.children) if (app.children)
{ {
app.children.forEach((child: QAppTreeNode) => app.children.forEach((child: QAppTreeNode) =>
{ {
@ -213,7 +214,7 @@ export default function App()
const path = `${parentPath}/${app.name}`; const path = `${parentPath}/${app.name}`;
if (app.type === QAppNodeType.APP) if (app.type === QAppNodeType.APP)
{ {
if(app.children) if (app.children)
{ {
app.children.forEach((child: QAppTreeNode) => app.children.forEach((child: QAppTreeNode) =>
{ {
@ -348,10 +349,14 @@ export default function App()
} }
catch (e) catch (e)
{ {
console.log(e); if (e instanceof QException)
if (e.toString().indexOf("status code 401") !== -1)
{ {
logout(); if ((e as QException).message.indexOf("status code 401") !== -1)
{
removeCookie(SESSION_ID_COOKIE_NAME);
logout();
return;
}
} }
} }
})(); })();

View File

@ -20,8 +20,8 @@
*/ */
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QSection";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert} from "@mui/material"; import {Alert} from "@mui/material";
import Avatar from "@mui/material/Avatar"; import Avatar from "@mui/material/Avatar";
@ -30,7 +30,7 @@ import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import {Form, Formik} from "formik"; import {Form, Formik} from "formik";
import React, {useReducer, useState} from "react"; import React, {useReducer, useState} from "react";
import {useParams, useNavigate, useLocation} from "react-router-dom"; import {useLocation, useNavigate, useParams} from "react-router-dom";
import * as Yup from "yup"; import * as Yup from "yup";
import {QCancelButton, QSaveButton} from "qqq/components/QButtons"; import {QCancelButton, QSaveButton} from "qqq/components/QButtons";
import QDynamicForm from "qqq/components/QDynamicForm"; import QDynamicForm from "qqq/components/QDynamicForm";
@ -58,7 +58,7 @@ function EntityForm({table, id}: Props): JSX.Element
const [initialValues, setInitialValues] = useState({} as { [key: string]: string }); const [initialValues, setInitialValues] = useState({} as { [key: string]: string });
const [formFields, setFormFields] = useState(null as Map<string, any>); const [formFields, setFormFields] = useState(null as Map<string, any>);
const [t1sectionName, setT1SectionName] = useState(null as string); const [t1sectionName, setT1SectionName] = useState(null as string);
const [nonT1Sections, setNonT1Sections] = useState([] as QSection[]); const [nonT1Sections, setNonT1Sections] = useState([] as QTableSection[]);
const [alertContent, setAlertContent] = useState(""); const [alertContent, setAlertContent] = useState("");
@ -66,7 +66,7 @@ function EntityForm({table, id}: Props): JSX.Element
const [formValues, setFormValues] = useState({} as { [key: string]: string }); const [formValues, setFormValues] = useState({} as { [key: string]: string });
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData); const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
const [record, setRecord] = useState(null as QRecord); const [record, setRecord] = useState(null as QRecord);
const [tableSections, setTableSections] = useState(null as QSection[]); const [tableSections, setTableSections] = useState(null as QTableSection[]);
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const navigate = useNavigate(); const navigate = useNavigate();
@ -144,7 +144,7 @@ function EntityForm({table, id}: Props): JSX.Element
///////////////////////////////////// /////////////////////////////////////
const dynamicFormFieldsBySection = new Map<string, any>(); const dynamicFormFieldsBySection = new Map<string, any>();
let t1sectionName; let t1sectionName;
const nonT1Sections: QSection[] = []; const nonT1Sections: QTableSection[] = [];
for (let i = 0; i < tableSections.length; i++) for (let i = 0; i < tableSections.length; i++)
{ {
const section = tableSections[i]; const section = tableSections[i];
@ -325,7 +325,7 @@ function EntityForm({table, id}: Props): JSX.Element
} }
</Card> </Card>
</MDBox> </MDBox>
{formFields && nonT1Sections.length ? nonT1Sections.map((section: QSection) => ( {formFields && nonT1Sections.length ? nonT1Sections.map((section: QTableSection) => (
<MDBox key={`edit-card-${section.name}`} pb={3}> <MDBox key={`edit-card-${section.name}`} pb={3}>
<Card id={section.name} sx={{overflow: "visible"}}> <Card id={section.name} sx={{overflow: "visible"}}>
<MDTypography variant="h5" p={3} pb={1}> <MDTypography variant="h5" p={3} pb={1}>

View File

@ -26,16 +26,18 @@ import {ReactNode} from "react";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
interface Props { interface Props
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark"; {
title: string; color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark";
percentage?: { title: string;
color: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "dark" | "white"; percentage?: {
amount: string | number; color: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "dark" | "white";
label: string; amount: string | number;
}; label: string;
icon: ReactNode; };
[key: string]: any; icon: ReactNode;
[key: string]: any;
} }
function ProcessLinkCard({ function ProcessLinkCard({
@ -44,7 +46,7 @@ function ProcessLinkCard({
{ {
return ( return (
<Card> <Card>
<MDBox display="flex" justifyContent="space-between" pt={1} px={2}> <MDBox display="flex" justifyContent="space-between" pt={3} px={2}>
<MDBox <MDBox
variant="gradient" variant="gradient"
bgColor={color} bgColor={color}

View File

@ -26,12 +26,14 @@ import {Link} from "react-router-dom";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
interface Props { interface Props
icon: ReactNode; {
title: string; icon: ReactNode;
route: string | string[]; title: string;
light?: boolean; route: string | string[];
[key: string]: any; light?: boolean;
[key: string]: any;
} }
const ucFirst = (input: string): string => const ucFirst = (input: string): string =>
@ -107,23 +109,16 @@ function QBreadcrumbs({
</MDTypography> </MDTypography>
</Link> </Link>
))} ))}
<MDTypography
variant="button"
fontWeight="regular"
textTransform="capitalize"
color={light ? "white" : "dark"}
sx={{lineHeight: 0}}
>
{routeToLabel(title)}
</MDTypography>
</MuiBreadcrumbs> </MuiBreadcrumbs>
<MDTypography <MDTypography
pt={1}
fontWeight="bold" fontWeight="bold"
textTransform="capitalize" textTransform="capitalize"
variant="h6" variant="h5"
color={light ? "white" : "dark"} color={light ? "white" : "dark"}
noWrap noWrap
> >
{routeToLabel(title)}
</MDTypography> </MDTypography>
</MDBox> </MDBox>
); );

View File

@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {QSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QSection"; import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import {Theme} from "@mui/material/styles"; import {Theme} from "@mui/material/styles";
@ -27,8 +27,9 @@ import React from "react";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
interface Props { interface Props
tableSections: QSection[]; {
tableSections: QTableSection[];
light?: boolean; light?: boolean;
} }
@ -38,7 +39,7 @@ function QRecordSidebar({tableSections, light}: Props): JSX.Element
<Card sx={{borderRadius: ({borders: {borderRadius}}) => borderRadius.lg, position: "sticky", top: "1%"}}> <Card sx={{borderRadius: ({borders: {borderRadius}}) => borderRadius.lg, position: "sticky", top: "1%"}}>
<MDBox component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none"}}> <MDBox component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none"}}>
{ {
tableSections ? tableSections.map((section: QSection, key: number) => ( tableSections ? tableSections.map((section: QTableSection, key: number) => (
<MDBox key={`section-${section.name}`} component="li" pt={key === 0 ? 0 : 1}> <MDBox key={`section-${section.name}`} component="li" pt={key === 0 ? 0 : 1}>
<MDTypography <MDTypography
component="a" component="a"

View File

@ -23,11 +23,11 @@ import Divider from "@mui/material/Divider";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import List from "@mui/material/List"; import List from "@mui/material/List";
import {useEffect, useState, ReactNode, useReducer} from "react"; import {ReactNode, useEffect, useReducer, useState} from "react";
import {useLocation, NavLink} from "react-router-dom"; import {NavLink, useLocation} from "react-router-dom";
import MDBox from "components/MDBox"; import MDBox from "components/MDBox";
import MDTypography from "components/MDTypography"; import MDTypography from "components/MDTypography";
import {useMaterialUIController, setMiniSidenav, setTransparentSidenav, setWhiteSidenav,} from "context"; import {setMiniSidenav, setTransparentSidenav, setWhiteSidenav, useMaterialUIController,} from "context";
import sidenavLogoLabel from "examples/Sidenav/styles/sidenav"; import sidenavLogoLabel from "examples/Sidenav/styles/sidenav";
import AuthenticationButton from "qqq/components/Buttons/AuthenticationButton"; import AuthenticationButton from "qqq/components/Buttons/AuthenticationButton";
import SidenavCollapse from "qqq/components/Sidenav/SidenavCollapse"; import SidenavCollapse from "qqq/components/Sidenav/SidenavCollapse";
@ -334,6 +334,12 @@ function Sidenav({color, icon, logo, companyName, routes, ...rest}: Props): JSX.
} }
/> />
<List>{renderRoutes}</List> <List>{renderRoutes}</List>
<Divider
light={
(!darkMode && !whiteSidenav && !transparentSidenav) ||
(darkMode && !transparentSidenav && whiteSidenav)
}
/>
<AuthenticationButton /> <AuthenticationButton />
</SidenavRoot> </SidenavRoot>
); );

View File

@ -0,0 +1,65 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Theme} from "@mui/material/styles";
import {ReactNode} from "react";
import MDBox from "qqq/components/Temporary/MDBox";
// Declaring prop types for DataTableBodyCell
interface Props
{
children: ReactNode;
noBorder?: boolean;
align?: "left" | "right" | "center";
}
function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
{
return (
<MDBox
component="td"
textAlign={align}
py={1.5}
px={3}
sx={({palette: {light}, typography: {size}, borders: {borderWidth}}: Theme) => ({
fontSize: size.sm,
borderBottom: noBorder ? "none" : `${borderWidth[1]} solid ${light.main}`,
})}
>
<MDBox
display="inline-block"
width="max-content"
color="text"
sx={{verticalAlign: "middle"}}
>
{children}
</MDBox>
</MDBox>
);
}
// Declaring default props for DataTableBodyCell
DataTableBodyCell.defaultProps = {
noBorder: false,
align: "left",
};
export default DataTableBodyCell;

View File

@ -0,0 +1,107 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Icon from "@mui/material/Icon";
import {Theme} from "@mui/material/styles";
import {ReactNode} from "react";
import {useMaterialUIController} from "context";
import MDBox from "qqq/components/Temporary/MDBox";
// Declaring props types for DataTableHeadCell
interface Props
{
width?: string | number;
children: ReactNode;
sorted?: false | "none" | "asce" | "desc";
align?: "left" | "right" | "center";
}
function DataTableHeadCell({width, children, sorted, align, ...rest}: Props): JSX.Element
{
const [controller] = useMaterialUIController();
const {darkMode} = controller;
return (
<MDBox
component="th"
width={width}
py={1.5}
px={3}
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
borderBottom: `${borderWidth[1]} solid ${light.main}`,
})}
>
<MDBox
{...rest}
position="relative"
textAlign={align}
color={darkMode ? "white" : "secondary"}
opacity={0.7}
sx={({typography: {size, fontWeightBold}}: Theme) => ({
fontSize: size.xxs,
fontWeight: fontWeightBold,
textTransform: "uppercase",
cursor: sorted && "pointer",
userSelect: sorted && "none",
})}
>
{children}
{sorted && (
<MDBox
position="absolute"
top={0}
right={align !== "right" ? "16px" : 0}
left={align === "right" ? "-5px" : "unset"}
sx={({typography: {size}}: any) => ({
fontSize: size.lg,
})}
>
<MDBox
position="absolute"
top={-6}
color={sorted === "asce" ? "text" : "secondary"}
opacity={sorted === "asce" ? 1 : 0.5}
>
<Icon>arrow_drop_up</Icon>
</MDBox>
<MDBox
position="absolute"
top={0}
color={sorted === "desc" ? "text" : "secondary"}
opacity={sorted === "desc" ? 1 : 0.5}
>
<Icon>arrow_drop_down</Icon>
</MDBox>
</MDBox>
)}
</MDBox>
</MDBox>
);
}
// Declaring default props for DataTableHeadCell
DataTableHeadCell.defaultProps = {
width: "auto",
sorted: "none",
align: "left",
};
export default DataTableHeadCell;

View File

@ -0,0 +1,360 @@
/* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Autocomplete from "@mui/material/Autocomplete";
import Icon from "@mui/material/Icon";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow";
import parse from "html-react-parser";
import {useEffect, useMemo, useState} from "react";
import {useAsyncDebounce, useGlobalFilter, usePagination, useSortBy, useTable} from "react-table";
import DefaultCell from "layouts/dashboards/sales/components/DefaultCell";
import DataTableBodyCell from "qqq/components/Temporary/DataTable/DataTableBodyCell";
import DataTableHeadCell from "qqq/components/Temporary/DataTable/DataTableHeadCell";
import MDBox from "qqq/components/Temporary/MDBox";
import MDInput from "qqq/components/Temporary/MDInput";
import MDPagination from "qqq/components/Temporary/MDPagination";
import MDTypography from "qqq/components/Temporary/MDTypography";
import ImageCell from "qqq/pages/dashboards/Tables/ImageCell";
export interface TableDataInput
{
columns: { [key: string]: any }[];
rows: { [key: string]: any }[];
}
// Declaring props types for DataTable
interface Props
{
entriesPerPage?:
| false
| {
defaultValue: number;
entries: number[];
};
canSearch?: boolean;
showTotalEntries?: boolean;
table: TableDataInput;
pagination?: {
variant: "contained" | "gradient";
color: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "dark" | "light";
};
isSorted?: boolean;
noEndBorder?: boolean;
}
function DataTable({
entriesPerPage,
canSearch,
showTotalEntries,
table,
pagination,
isSorted,
noEndBorder,
}: Props): JSX.Element
{
let defaultValue: any;
let entries: any[];
if (entriesPerPage)
{
defaultValue = entriesPerPage.defaultValue ? entriesPerPage.defaultValue : "10";
entries = entriesPerPage.entries ? entriesPerPage.entries : ["10", "25", "50", "100"];
}
const columns = useMemo<any>(() => table.columns, [table]);
const data = useMemo<any>(() => table.rows, [table]);
if (!columns || !data)
{
return null;
}
const tableInstance = useTable(
{columns, data, initialState: {pageIndex: 0}},
useGlobalFilter,
useSortBy,
usePagination
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
rows,
page,
pageOptions,
canPreviousPage,
canNextPage,
gotoPage,
nextPage,
previousPage,
setPageSize,
setGlobalFilter,
state: {pageIndex, pageSize, globalFilter},
}: any = tableInstance;
// Set the default value for the entries per page when component mounts
useEffect(() => setPageSize(defaultValue || 10), [defaultValue]);
// Set the entries per page value based on the select value
const setEntriesPerPage = (value: any) => setPageSize(value);
// Render the paginations
const renderPagination = pageOptions.map((option: any) => (
<MDPagination
item
key={option}
onClick={() => gotoPage(Number(option))}
active={pageIndex === option}
>
{option + 1}
</MDPagination>
));
// Handler for the input to set the pagination index
const handleInputPagination = ({target: {value}}: any) =>
value > pageOptions.length || value < 0 ? gotoPage(0) : gotoPage(Number(value));
// Customized page options starting from 1
const customizedPageOptions = pageOptions.map((option: any) => option + 1);
// Setting value for the pagination input
const handleInputPaginationValue = ({target: value}: any) => gotoPage(Number(value.value - 1));
// Search input value state
const [search, setSearch] = useState(globalFilter);
// Search input state handle
const onSearchChange = useAsyncDebounce((value) =>
{
setGlobalFilter(value || undefined);
}, 100);
// A function that sets the sorted value for the table
const setSortedValue = (column: any) =>
{
let sortedValue;
if (isSorted && column.isSorted)
{
sortedValue = column.isSortedDesc ? "desc" : "asce";
}
else if (isSorted)
{
sortedValue = "none";
}
else
{
sortedValue = false;
}
return sortedValue;
};
// Setting the entries starting point
const entriesStart = pageIndex === 0 ? pageIndex + 1 : pageIndex * pageSize + 1;
// Setting the entries ending point
let entriesEnd;
if (pageIndex === 0)
{
entriesEnd = pageSize;
}
else if (pageIndex === pageOptions.length - 1)
{
entriesEnd = rows.length;
}
else
{
entriesEnd = pageSize * (pageIndex + 1);
}
return (
<TableContainer sx={{boxShadow: "none"}}>
{entriesPerPage || canSearch ? (
<MDBox display="flex" justifyContent="space-between" alignItems="center" p={3}>
{entriesPerPage && (
<MDBox display="flex" alignItems="center">
<Autocomplete
disableClearable
value={pageSize.toString()}
options={entries}
onChange={(event, newValue) =>
{
setEntriesPerPage(parseInt(newValue, 10));
}}
size="small"
sx={{width: "5rem"}}
renderInput={(params) => <MDInput {...params} />}
/>
<MDTypography variant="caption" color="secondary">
&nbsp;&nbsp;entries per page
</MDTypography>
</MDBox>
)}
{canSearch && (
<MDBox width="12rem" ml="auto">
<MDInput
placeholder="Search..."
value={search}
size="small"
fullWidth
onChange={({currentTarget}: any) =>
{
setSearch(search);
onSearchChange(currentTarget.value);
}}
/>
</MDBox>
)}
</MDBox>
) : null}
<Table {...getTableProps()}>
<MDBox component="thead">
{headerGroups.map((headerGroup: any, i: number) => (
<TableRow key={i} {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column: any) => (
column.type !== "hidden" && (
<DataTableHeadCell
key={i++}
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
width={column.width ? column.width : "auto"}
align={column.align ? column.align : "left"}
sorted={setSortedValue(column)}
>
{column.render("header")}
</DataTableHeadCell>
)
))}
</TableRow>
))}
</MDBox>
<TableBody {...getTableBodyProps()}>
{page.map((row: any, key: any) =>
{
prepareRow(row);
return (
<TableRow key={key} {...row.getRowProps()}>
{row.cells.map((cell: any) => (
cell.column.type !== "hidden" && (
<DataTableBodyCell
key={key}
noBorder={noEndBorder && rows.length - 1 === key}
align={cell.column.align ? cell.column.align : "left"}
{...cell.getCellProps()}
>
{
cell.column.type === "default" && (
cell.value && "number" === typeof cell.value ? (
<DefaultCell>{cell.value.toLocaleString()}</DefaultCell>
) : (<DefaultCell>{cell.render("Cell")}</DefaultCell>)
)
}
{
cell.column.type === "html" && (
<DefaultCell>{parse(cell.value)}</DefaultCell>
)
}
{
cell.column.type === "image" && row.values["imageTotal"] && (
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} total={row.values["imageTotal"].toLocaleString()} totalType={row.values["imageTotalType"]} />
)
}
{
cell.column.type === "image" && !row.values["imageTotal"] && (
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
)
}
</DataTableBodyCell>
)
))}
</TableRow>
);
})}
</TableBody>
</Table>
<MDBox
display="flex"
flexDirection={{xs: "column", sm: "row"}}
justifyContent="space-between"
alignItems={{xs: "flex-start", sm: "center"}}
p={!showTotalEntries && pageOptions.length === 1 ? 0 : 3}
>
{showTotalEntries && (
<MDBox mb={{xs: 3, sm: 0}}>
<MDTypography variant="button" color="secondary" fontWeight="regular">
Showing {entriesStart} to {entriesEnd} of {rows.length} entries
</MDTypography>
</MDBox>
)}
{pageOptions.length > 1 && (
<MDPagination
variant={pagination.variant ? pagination.variant : "gradient"}
color={pagination.color ? pagination.color : "info"}
>
{canPreviousPage && (
<MDPagination item onClick={() => previousPage()}>
<Icon sx={{fontWeight: "bold"}}>chevron_left</Icon>
</MDPagination>
)}
{renderPagination.length > 6 ? (
<MDBox width="5rem" mx={1}>
<MDInput
inputProps={{type: "number", min: 1, max: customizedPageOptions.length}}
value={customizedPageOptions[pageIndex]}
onChange={(event: any) =>
{
handleInputPagination(event);
handleInputPaginationValue(event);
}}
/>
</MDBox>
) : (
renderPagination
)}
{canNextPage && (
<MDPagination item onClick={() => nextPage()}>
<Icon sx={{fontWeight: "bold"}}>chevron_right</Icon>
</MDPagination>
)}
</MDPagination>
)}
</MDBox>
</TableContainer>
);
}
// Declaring default props for DataTable
DataTable.defaultProps = {
entriesPerPage: {defaultValue: 10, entries: ["5", "10", "15", "20", "25"]},
canSearch: false,
showTotalEntries: true,
pagination: {variant: "gradient", color: "info"},
isSorted: true,
noEndBorder: false,
};
export default DataTable;

View File

@ -26,7 +26,7 @@ import MDTypography from "components/MDTypography";
interface Props interface Props
{ {
variant?: "gradient" | "contained"; variant?: "gradient" | "contained";
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark"; color?: string;
size?: "xs" | "sm" | "md" | "lg"; size?: "xs" | "sm" | "md" | "lg";
badgeContent: string; badgeContent: string;
font?: font?:

View File

@ -0,0 +1,71 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {styled, Theme} from "@mui/material/styles";
import MDButton from "qqq/components/Temporary/MDButton";
// @ts-ignore
export default styled(MDButton)(({theme, ownerState}: { theme?: Theme; ownerState: any }) =>
{
const {borders, functions, typography, palette} = theme;
const {variant, paginationSize, active} = ownerState;
const {borderColor} = borders;
const {pxToRem} = functions;
const {fontWeightRegular, size: fontSize} = typography;
const {light} = palette;
// width, height, minWidth and minHeight values
let sizeValue = pxToRem(36);
if (paginationSize === "small")
{
sizeValue = pxToRem(30);
}
else if (paginationSize === "large")
{
sizeValue = pxToRem(46);
}
return {
borderColor,
margin: `0 ${pxToRem(2)}`,
pointerEvents: active ? "none" : "auto",
fontWeight: fontWeightRegular,
fontSize: fontSize.sm,
width: sizeValue,
minWidth: sizeValue,
height: sizeValue,
minHeight: sizeValue,
"&:hover, &:focus, &:active": {
transform: "none",
boxShadow: (variant !== "gradient" || variant !== "contained") && "none !important",
opacity: "1 !important",
},
"&:hover": {
backgroundColor: light.main,
borderColor,
},
};
});

View File

@ -0,0 +1,104 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {createContext, FC, forwardRef, ReactNode, useContext, useMemo} from "react";
import MDBox from "qqq/components/Temporary/MDBox";
import MDPaginationItemRoot from "qqq/components/Temporary/MDPagination/MDPaginationItemRoot";
// The Pagination main context
const Context = createContext<any>(null);
// Declare props types for MDPagination
interface Props
{
item?: boolean;
variant?: "gradient" | "contained";
color?:
| "white"
| "primary"
| "secondary"
| "info"
| "success"
| "warning"
| "error"
| "light"
| "dark";
size?: "small" | "medium" | "large";
active?: boolean;
children: ReactNode;
[key: string]: any;
}
const MDPagination: FC<Props | any> = forwardRef(
({item, variant, color, size, active, children, ...rest}, ref) =>
{
const context: any = useContext(Context);
const paginationSize = context ? context.size : undefined;
const providerValue = useMemo(
() => ({
variant,
color,
size,
}),
[variant, color, size]
);
return (
<Context.Provider value={providerValue}>
{item ? (
<MDPaginationItemRoot
{...rest}
ref={ref}
variant={active ? context.variant : "outlined"}
color={active ? context.color : "secondary"}
iconOnly
circular
ownerState={{variant, active, paginationSize}}
>
{children}
</MDPaginationItemRoot>
) : (
<MDBox
display="flex"
justifyContent="flex-end"
alignItems="center"
sx={{listStyle: "none"}}
>
{children}
</MDBox>
)}
</Context.Provider>
);
}
);
// Declaring default props for MDPagination
MDPagination.defaultProps = {
item: false,
variant: "gradient",
color: "info",
size: "medium",
active: false,
};
export default MDPagination;

View File

@ -25,19 +25,19 @@ import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {Icon} from "@mui/material"; import {Icon} from "@mui/material";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Divider from "@mui/material/Divider";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import {Link, useLocation} from "react-router-dom"; import {Link, useLocation} from "react-router-dom";
import BaseLayout from "qqq/components/BaseLayout"; import BaseLayout from "qqq/components/BaseLayout";
import ProcessLinkCard from "qqq/components/ProcessLinkCard"; import ProcessLinkCard from "qqq/components/ProcessLinkCard";
import DefaultInfoCard from "qqq/components/Temporary/DefaultInfoCard";
import MDBadgeDot from "qqq/components/Temporary/MDBadgeDot"; import MDBadgeDot from "qqq/components/Temporary/MDBadgeDot";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
import MiniStatisticsCard from "qqq/components/Temporary/MiniStatisticsCard"; import MiniStatisticsCard from "qqq/components/Temporary/MiniStatisticsCard";
import BarChart from "qqq/pages/dashboards/Widgets/BarChart"; import BarChart from "qqq/pages/dashboards/Widgets/BarChart";
import QuickSightChart from "qqq/pages/dashboards/Widgets/QuickSightChart"; import QuickSightChart from "qqq/pages/dashboards/Widgets/QuickSightChart";
import SmallLineChart from "qqq/pages/dashboards/Widgets/SmallLineChart"; import TableCard from "qqq/pages/dashboards/Widgets/TableCard";
import QClient from "qqq/utils/QClient"; import QClient from "qqq/utils/QClient";
import LineChart from "../dashboards/Widgets/LineChart"; import LineChart from "../dashboards/Widgets/LineChart";
@ -118,7 +118,6 @@ function AppHome({app}: Props): JSX.Element
}); });
setTableCounts(tableCounts); setTableCounts(tableCounts);
console.log(app.widgets);
if (app.widgets) if (app.widgets)
{ {
const widgets: any[] = []; const widgets: any[] = [];
@ -127,8 +126,7 @@ function AppHome({app}: Props): JSX.Element
widgets[i] = {}; widgets[i] = {};
setTimeout(async () => setTimeout(async () =>
{ {
const widget = await qController.widget(app.widgets[i]); widgets[i] = await qController.widget(app.widgets[i]);
widgets[i] = widget;
setUpdatedTableCounts(new Date()); setUpdatedTableCounts(new Date());
}, 1); }, 1);
} }
@ -136,47 +134,52 @@ function AppHome({app}: Props): JSX.Element
} }
}, [qInstance, location]); }, [qInstance, location]);
/*
const charts = [
{
type: "barChart",
title: "Parcel Invoice Lines per Month",
barChartData: {
labels: ["Feb 22", "Mar 22", "Apr 22", "May 22", "Jun 22", "Jul 22", "Aug 22"],
datasets: {label: "Parcel Invoice Lines", data: [50000, 22000, 11111, 22333, 40404, 9876, 2355]},
},
},
{
type: "lineChart",
title: "Total Charges by Carrier per Month",
lineChartData: {
labels: ["Feb 22", "Mar 22", "Apr 22", "May 22", "Jun 22", "Jul 22", "Aug 22"],
datasets: [
{label: "UPS", color: "info", data: [50000, 22000, 11111, 22333, 40404, 9876, 2355]},
{label: "FedEx", color: "dark", data: [5000, 22000, 31111, 32333, 20404, 19876, 24355]},
{label: "LSO", color: "error", data: [500, 2200, 1111, 2333, 404, 17876, 2355]},
],
},
},
];
*/
console.log(`Widgets: ${widgets} and tables: ${tables}`);
const haveWidgets = widgets && widgets.length;
const widgetCount = widgets ? widgets.length : 0; const widgetCount = widgets ? widgets.length : 0;
// eslint-disable-next-line no-nested-ternary // eslint-disable-next-line no-nested-ternary
const tileSizeLg = (widgetCount === 0 ? 3 : widgetCount === 1 ? 4 : 6); const tileSizeLg = (widgetCount === 0 ? 3 : widgetCount === 1 ? 4 : 6);
const handleDropdownOnChange = (value: string, index: number) =>
{
alert(value);
setTimeout(async () =>
{
widgets[index] = await qController.widget(app.widgets[index]);
}, 1);
};
return ( return (
<BaseLayout> <BaseLayout>
<MDBox mt={4}> <MDBox mt={4}>
<Grid container spacing={3}> <Grid container spacing={3}>
{ {
widgetCount > 0 ? ( widgetCount > 0 ? (
<Grid item xs={12} lg={widgetCount === 1 ? 3 : 6}> <Grid item xs={12} lg={12}>
<Grid container spacing={3}> <Grid container spacing={3}>
{
widgets.map((chart, i) => (
<Grid key={`${i}`} item xs={12} lg={12}>
<MDBox mb={3}>
{
chart.type === "table" && (
<TableCard
color="info"
title={chart.title}
data={chart}
dropdownOptions={chart.dropdownOptions}
dropdownOnChange={handleDropdownOnChange}
widgetIndex={i}
/>
)
}
</MDBox>
</Grid>
))
}
</Grid>
<Grid item xs={12} lg={widgetCount === 1 ? 3 : 6}>
{ {
widgets.map((chart, i) => ( widgets.map((chart, i) => (
<Grid key={`${i}`} item xs={12} lg={widgetCount === 1 ? 12 : 6}> <Grid key={`${i}`} item xs={12} lg={widgetCount === 1 ? 12 : 6}>
@ -192,7 +195,7 @@ function AppHome({app}: Props): JSX.Element
color="info" color="info"
title={chart.title} title={chart.title}
date={`As of ${new Date().toDateString()}`} date={`As of ${new Date().toDateString()}`}
chart={chart.barChartData} data={chart.chartData}
/> />
) )
} }
@ -226,85 +229,86 @@ function AppHome({app}: Props): JSX.Element
} }
{ {
tables.length > 0 || processes.length > 0 || childApps.length > 0 ? ( app.sections ? (
// eslint-disable-next-line no-nested-ternary
<Grid item xs={12} lg={widgetCount === 0 ? 12 : widgetCount === 1 ? 9 : 6}> <Grid item xs={12} lg={widgetCount === 0 ? 12 : widgetCount === 1 ? 9 : 6}>
{
tables.length ? (
<MDBox mb={3}>
<Card id="basic-info" sx={{overflow: "visible"}}>
<MDBox p={3}>
<MDTypography variant="h5">Tables</MDTypography>
</MDBox>
<Grid container spacing={3} padding={3} paddingTop={0}>
{tables.map((table) => (
<Grid key={table.name} item xs={12} md={12} lg={tileSizeLg}>
<Link to={table.name}>
<MDBox mb={3}>
<MiniStatisticsCard
title={{fontWeight: "bold", text: table.label}}
count={!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "..." : tableCounts.get(table.name).value.toLocaleString()}
percentage={{color: "info", text: (!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "" : (tableCounts.get(table.name).value === 1 ? "total record" : "total records"))}}
icon={{color: "info", component: <Icon>{table.iconName || app.iconName}</Icon>}}
direction="right"
/>
</MDBox>
</Link>
</Grid>
))}
</Grid>
</Card>
</MDBox>
) : null
}
{ {app.sections.map((section) => (
processes.length ? ( <MDBox key={section.name} mb={3}>
<MDBox mb={3}> <Card sx={{overflow: "visible"}}>
<Card id="basic-info" sx={{overflow: "visible"}}> <MDBox p={3}>
<MDBox p={3}> <MDTypography variant="h5">{section.label}</MDTypography>
<MDTypography variant="h5">Processes</MDTypography> </MDBox>
</MDBox> {
<Grid container spacing={3} padding={3} paddingTop={3}> section.processes ? (
{processes.map((process) => ( <MDBox p={3} pl={5} pt={0}>
<Grid key={process.name} item xs={12} md={12} lg={tileSizeLg}> <MDTypography variant="h6">Actions</MDTypography>
<Link to={process.name}> </MDBox>
<ProcessLinkCard ) : null
icon={process.iconName || app.iconName} }
title={process.label} {
/> section.processes ? (
</Link> <Grid container spacing={3} padding={3} paddingTop={0}>
</Grid> {
))} section.processes.map((processName) =>
</Grid> {
</Card> let process = app.childMap.get(processName);
</MDBox> return (
) : null <Grid key={process.name} item xs={12} md={12} lg={tileSizeLg}>
} <Link to={process.name}>
<ProcessLinkCard
{ icon={process.iconName || app.iconName}
childApps.length ? ( title={process.label}
<MDBox mb={3}> />
<Card id="basic-info" sx={{overflow: "visible"}}> </Link>
<MDBox p={3}> </Grid>
<MDTypography variant="h5">Apps</MDTypography> );
</MDBox> })
<Grid container spacing={3} padding={3} paddingTop={3}> }
{childApps.map((childApp) => ( </Grid>
<Grid key={childApp.name} item xs={12} md={12} lg={tileSizeLg}> ) : null
<Link to={childApp.name}> }
<DefaultInfoCard {
icon={childApp.iconName || app.iconName} section.processes && section.tables ? (
title={childApp.label} <Divider />
/> ) : null
</Link> }
</Grid> {
))} section.tables ? (
</Grid> <MDBox p={3} pl={5} pb={0} pt={0}>
</Card> <MDTypography variant="h6">Data</MDTypography>
</MDBox> </MDBox>
) : null ) : null
} }
{
section.tables ? (
<Grid container spacing={3} padding={3} paddingBottom={0} paddingTop={0}>
{
section.tables.map((tableName) =>
{
let table = app.childMap.get(tableName);
return (
<Grid key={table.name} item xs={12} md={12} lg={tileSizeLg}>
<Link to={table.name}>
<MDBox mb={3}>
<MiniStatisticsCard
title={{fontWeight: "bold", text: table.label}}
count={!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "..." : tableCounts.get(table.name).value.toLocaleString()}
percentage={{color: "info", text: (!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "" : (tableCounts.get(table.name).value === 1 ? "total record" : "total records"))}}
icon={{color: "info", component: <Icon>{table.iconName || app.iconName}</Icon>}}
direction="right"
/>
</MDBox>
</Link>
</Grid>
);
})
}
</Grid>
) : null
}
</Card>
</MDBox>
))}
</Grid> </Grid>
) : null ) : null
} }

View File

@ -19,35 +19,36 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Title} from "@mui/icons-material"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {Icon} from "@mui/material"; import {Icon} from "@mui/material";
import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import React, {useState} from "react"; import React, {useEffect, useState} from "react";
import DefaultLineChart from "examples/Charts/LineCharts/DefaultLineChart"; import DashboardLayout from "qqq/components/DashboardLayout";
import DashboardLayout from "examples/LayoutContainers/DashboardLayout";
import DataTable from "examples/Tables/DataTable";
import Footer from "qqq/components/Footer"; import Footer from "qqq/components/Footer";
import Navbar from "qqq/components/Navbar"; import Navbar from "qqq/components/Navbar";
import MDBadgeDot from "qqq/components/Temporary/MDBadgeDot"; import {TableDataInput} from "qqq/components/Temporary/DataTable";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography";
import ShipmentsByWarehouseTable from "qqq/pages/dashboards/Tables/ShipmentsByWarehouseTable"; import ShipmentsByWarehouseTable from "qqq/pages/dashboards/Tables/ShipmentsByWarehouseTable";
import carrierSpendData from "qqq/pages/dashboards/Widgets/Data/CarrierSpendData"; import {GenericChartData} from "qqq/pages/dashboards/Widgets/Data/GenericChartData";
import carrierVolumeLineChartData from "qqq/pages/dashboards/Widgets/Data/CarrierVolumeLineChartData";
import smallShipmentsByWarehouseData from "qqq/pages/dashboards/Widgets/Data/SmallShipmentsByWarehouseData"; import smallShipmentsByWarehouseData from "qqq/pages/dashboards/Widgets/Data/SmallShipmentsByWarehouseData";
import timeInTransitBarChartData from "qqq/pages/dashboards/Widgets/Data/TimeInTransitBarChartData"; import DefaultLineChart, {DefaultLineChartData} from "qqq/pages/dashboards/Widgets/DefaultLineChart";
import ShipmentsByCarrierPieChart from "qqq/pages/dashboards/Widgets/ShipmentsByChannelPieChart"; import HorizontalBarChart from "qqq/pages/dashboards/Widgets/HorizontalBarChart";
import {PieChartData} from "qqq/pages/dashboards/Widgets/PieChart";
import PieChartCard from "qqq/pages/dashboards/Widgets/PieChartCard";
import SimpleStatisticsCard from "qqq/pages/dashboards/Widgets/SimpleStatisticsCard"; import SimpleStatisticsCard from "qqq/pages/dashboards/Widgets/SimpleStatisticsCard";
import HorizontalBarChart from "./Widgets/HorizontalBarChart"; import {StatisticsCardData} from "qqq/pages/dashboards/Widgets/StatisticsCard";
import TableCard from "qqq/pages/dashboards/Widgets/TableCard";
import QClient from "qqq/utils/QClient";
const qController = QClient.getInstance();
function CarrierPerformance(): JSX.Element function CarrierPerformance(): JSX.Element
{ {
const openArrowIcon = "arrow_drop_down"; const openArrowIcon = "arrow_drop_down";
const closeArrowIcon = "arrow_drop_up"; const closeArrowIcon = "arrow_drop_up";
const [shipmentsDropdownValue, setShipmentsDropdownValue] = useState<string>("Last 30 Days"); const [shipmentsDropdownValue, setShipmentsDropdownValue] = useState<string>("Last 30 Days");
const [deliveriesDropdownValue, setDeliveriesDropdownValue] = useState<string>("Last 30 Days"); const [deliveriesDropdownValue, setDeliveriesDropdownValue] = useState<string>("Last 30 Days");
const [failuresDropdownValue, setFailuresDropdownValue] = useState<string>("Last 30 Days"); const [failuresDropdownValue, setFailuresDropdownValue] = useState<string>("Last 30 Days");
@ -64,7 +65,7 @@ function CarrierPerformance(): JSX.Element
{ {
setShipmentsDropdown(currentTarget); setShipmentsDropdown(currentTarget);
setShipmentsDropdownIcon(closeArrowIcon); setShipmentsDropdownIcon(closeArrowIcon);
} };
const closeShipmentsDropdown = ({currentTarget}: any) => const closeShipmentsDropdown = ({currentTarget}: any) =>
{ {
setShipmentsDropdown(null); setShipmentsDropdown(null);
@ -74,8 +75,8 @@ function CarrierPerformance(): JSX.Element
const openDeliveriesDropdown = ({currentTarget}: any) => const openDeliveriesDropdown = ({currentTarget}: any) =>
{ {
setDeliveriesDropdown(currentTarget); setDeliveriesDropdown(currentTarget);
setDeliveriesDropdownIcon(closeArrowIcon) setDeliveriesDropdownIcon(closeArrowIcon);
} };
const closeDeliveriesDropdown = ({currentTarget}: any) => const closeDeliveriesDropdown = ({currentTarget}: any) =>
{ {
setDeliveriesDropdown(null); setDeliveriesDropdown(null);
@ -85,8 +86,8 @@ function CarrierPerformance(): JSX.Element
const openFailuresDropdown = ({currentTarget}: any) => const openFailuresDropdown = ({currentTarget}: any) =>
{ {
setFailuresDropdown(currentTarget); setFailuresDropdown(currentTarget);
setFailuresDropdownIcon(closeArrowIcon) setFailuresDropdownIcon(closeArrowIcon);
} };
const closeFailuresDropdown = ({currentTarget}: any) => const closeFailuresDropdown = ({currentTarget}: any) =>
{ {
setFailuresDropdown(null); setFailuresDropdown(null);
@ -94,7 +95,140 @@ function CarrierPerformance(): JSX.Element
setFailuresDropdownIcon(openArrowIcon); setFailuresDropdownIcon(openArrowIcon);
}; };
// Dropdown menu template for the DefaultStatisticsCard
const [totalShipmentsData, setTotalShipmentsData] = useState({} as StatisticsCardData);
const [successfulDeliveriesData, setSuccessfulDeliveriesData] = useState({} as StatisticsCardData);
const [serviceFailuresData, setServiceFailuresData] = useState({} as StatisticsCardData);
const [shipmentsByCarrierTitle, setShipmentsByCarrierTitle] = useState("");
const [shipmentsByCarrierDescription, setShipmentsByCarrierDescription] = useState("");
const [shipmentsByCarrierData, setShipmentsByCarrierData] = useState({} as PieChartData);
const [carrierVolumeTitle, setCarrierVolumeTitle] = useState("");
const [carrierVolumeData, setCarrierVolumeData] = useState({} as DefaultLineChartData);
const [spendByCarrierTitle, setSpendByCarrierTitle] = useState("");
const [spendByCarrierData, setSpendByCarrierData] = useState({columns: [], rows: []} as TableDataInput);
const [timeInTransitTitle, setTimeInTransitTitle] = useState("");
const [timeInTransitData, setTimeInTransitData] = useState({} as GenericChartData);
const [qInstance, setQInstance] = useState(null as QInstance);
const [dataLoaded, setDataLoaded] = useState(false);
//////////////////////////
// load meta data first //
//////////////////////////
useEffect(() =>
{
(async () =>
{
const newQInstance = await qController.loadMetaData();
setQInstance(newQInstance);
})();
}, []);
///////////////////////////////////////////////////
// once meta data has loaded, load widgets' data //
///////////////////////////////////////////////////
useEffect(() =>
{
if (!qInstance || dataLoaded)
{
return;
}
setDataLoaded(true);
loadYTDShipmentsByCarrierData();
loadTotalShipmentsData();
loadSuccessfulDeliveriesData();
loadServiceFailuresData();
loadCarrierVolumeData();
loadSpendByCarrierData();
loadTimeInTransitData();
}, [qInstance]);
function loadTotalShipmentsData()
{
(async () =>
{
const widgetData = await qController.widget("TotalShipmentsStatisticsCard");
setTotalShipmentsData(widgetData);
})();
}
function loadSuccessfulDeliveriesData()
{
(async () =>
{
const widgetData = await qController.widget("SuccessfulDeliveriesStatisticsCard");
setSuccessfulDeliveriesData(widgetData);
})();
}
function loadServiceFailuresData()
{
(async () =>
{
const widgetData = await qController.widget("ServiceFailuresStatisticsCard");
setServiceFailuresData(widgetData);
})();
}
function loadCarrierVolumeData()
{
(async () =>
{
const widgetData = await qController.widget("CarrierVolumeLineChart");
setCarrierVolumeTitle(widgetData.title);
setCarrierVolumeData(widgetData.chartData);
})();
}
function loadYTDShipmentsByCarrierData()
{
(async () =>
{
const widgetData = await qController.widget("YTDShipmentsByCarrierPieChart");
setShipmentsByCarrierTitle(widgetData.title);
setShipmentsByCarrierDescription(widgetData.description);
setShipmentsByCarrierData(widgetData.chartData);
})();
}
function loadSpendByCarrierData()
{
(async () =>
{
const widgetData = await qController.widget("YTDSpendByCarrierTable");
setSpendByCarrierTitle(widgetData.title);
setSpendByCarrierData(widgetData);
})();
}
function loadTimeInTransitData()
{
(async () =>
{
const widgetData = await qController.widget("TimeInTransitBarChart");
setTimeInTransitTitle(widgetData.title);
///////////////////////////////////////////////////////////////////
// todo: need to make it so all charts can use multiple datasets //
///////////////////////////////////////////////////////////////////
const data = {
labels: widgetData.chartData.labels,
datasets: [
widgetData.chartData.dataset
]
};
setTimeInTransitData(data);
})();
}
const renderMenu = (state: any, open: any, close: any, icon: string) => ( const renderMenu = (state: any, open: any, close: any, icon: string) => (
<span style={{whiteSpace: "nowrap"}}> <span style={{whiteSpace: "nowrap"}}>
<Icon onClick={open} fontSize={"medium"} style={{cursor: "pointer", float: "right"}}>{icon}</Icon> <Icon onClick={open} fontSize={"medium"} style={{cursor: "pointer", float: "right"}}>{icon}</Icon>
@ -121,13 +255,8 @@ function CarrierPerformance(): JSX.Element
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12} sm={4}> <Grid item xs={12} sm={4}>
<SimpleStatisticsCard <SimpleStatisticsCard
title="total shipments" data={totalShipmentsData}
count="50,234" increaseIsGood={true}
percentage={{
color: "success",
value: "+5%",
label: "since last month",
}}
dropdown={{ dropdown={{
action: openShipmentsDropdown, action: openShipmentsDropdown,
menu: renderMenu(shipmentsDropdown, openShipmentsDropdown, closeShipmentsDropdown, shipmentsDropdownIcon), menu: renderMenu(shipmentsDropdown, openShipmentsDropdown, closeShipmentsDropdown, shipmentsDropdownIcon),
@ -137,29 +266,20 @@ function CarrierPerformance(): JSX.Element
</Grid> </Grid>
<Grid item xs={12} sm={4}> <Grid item xs={12} sm={4}>
<SimpleStatisticsCard <SimpleStatisticsCard
title="Successful deliveries" data={successfulDeliveriesData}
count="49,234" increaseIsGood={true}
percentage={{
color: "success",
value: "+12%",
label: "since last month",
}}
dropdown={{ dropdown={{
action: openDeliveriesDropdown, action: openDeliveriesDropdown,
menu: renderMenu(deliveriesDropdown, openDeliveriesDropdown, closeDeliveriesDropdown, deliveriesDropdownIcon), menu: renderMenu(deliveriesDropdown, openDeliveriesDropdown, closeDeliveriesDropdown, deliveriesDropdownIcon),
value: deliveriesDropdownValue, value: deliveriesDropdownValue,
}} }}
/> />
</Grid> </Grid>
<Grid item xs={12} sm={4}> <Grid item xs={12} sm={4}>
<SimpleStatisticsCard <SimpleStatisticsCard
title="service failures" data={serviceFailuresData}
count="832" increaseIsGood={false}
percentage={{
color: "error",
value: "+1.2%",
label: "since last month",
}}
dropdown={{ dropdown={{
action: openFailuresDropdown, action: openFailuresDropdown,
menu: renderMenu(failuresDropdown, openFailuresDropdown, closeFailuresDropdown, failuresDropdownIcon), menu: renderMenu(failuresDropdown, openFailuresDropdown, closeFailuresDropdown, failuresDropdownIcon),
@ -171,54 +291,35 @@ function CarrierPerformance(): JSX.Element
</MDBox> </MDBox>
<MDBox mb={3}> <MDBox mb={3}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12} sm={6} lg={4}>
<ShipmentsByCarrierPieChart />
</Grid>
<Grid item xs={12} sm={6} lg={8}> <Grid item xs={12} sm={6} lg={8}>
<DefaultLineChart <DefaultLineChart
title="Carrier Volume by Month" title={carrierVolumeTitle}
description={ data={carrierVolumeData}
<MDBox display="flex" justifyContent="space-between">
<MDBox display="flex" ml={-1}>
<MDBadgeDot color="dark" size="sm" badgeContent="AxleHire" />
<MDBadgeDot color="info" size="sm" badgeContent="CDL" />
<MDBadgeDot color="primary" size="sm" badgeContent="DHL" />
<MDBadgeDot color="success" size="sm" badgeContent="FedEx" />
<MDBadgeDot color="error" size="sm" badgeContent="LSO" />
<MDBadgeDot color="secondary" size="sm" badgeContent="OnTrac" />
<MDBadgeDot color="warning" size="sm" badgeContent="UPS" />
</MDBox>
</MDBox>
}
chart={carrierVolumeLineChartData}
/> />
</Grid> </Grid>
<Grid item xs={12} md={6} lg={4}>
<MDBox mb={3}>
<PieChartCard
title={shipmentsByCarrierTitle}
description={shipmentsByCarrierDescription}
data={shipmentsByCarrierData}
/>
</MDBox>
</Grid>
</Grid> </Grid>
</MDBox> </MDBox>
<Grid container spacing={3} mb={3}> <Grid container spacing={3} mb={3}>
<Grid item xs={12}> <Grid item xs={12}>
<Card> <TableCard
<MDBox pt={3} px={3}> title={spendByCarrierTitle}
<MDTypography variant="h6" fontWeight="medium"> data={spendByCarrierData}
Spend by Carrier YTD />
</MDTypography>
</MDBox>
<MDBox py={1}>
<DataTable
table={carrierSpendData}
entriesPerPage={false}
showTotalEntries={false}
isSorted={false}
noEndBorder
/>
</MDBox>
</Card>
</Grid> </Grid>
</Grid> </Grid>
<MDBox mb={3}> <MDBox mb={3}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12} lg={8}> <Grid item xs={12} lg={8}>
<HorizontalBarChart title="Time in Transit Last 30 Days" chart={timeInTransitBarChartData} /> <HorizontalBarChart height={250} title={timeInTransitTitle} data={timeInTransitData} />
</Grid> </Grid>
<Grid item xs={12} lg={4}> <Grid item xs={12} lg={4}>
<ShipmentsByWarehouseTable title="Shipments by Warehouse" rows={smallShipmentsByWarehouseData} /> <ShipmentsByWarehouseTable title="Shipments by Warehouse" rows={smallShipmentsByWarehouseData} />

View File

@ -19,28 +19,215 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import {useEffect, useState} from "react";
import DashboardLayout from "qqq/components/DashboardLayout"; import DashboardLayout from "qqq/components/DashboardLayout";
import Footer from "qqq/components/Footer"; import Footer from "qqq/components/Footer";
import Navbar from "qqq/components/Navbar"; import Navbar from "qqq/components/Navbar";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
import edisonWarehouse from "qqq/images/warehouses/edison_nj.jpg";
import pattersonWarehouse from "qqq/images/warehouses/patterson.jpg";
import stocktonWarehouse from "qqq/images/warehouses/stockton.jpg";
import BarChart from "qqq/pages/dashboards/Widgets/BarChart"; import BarChart from "qqq/pages/dashboards/Widgets/BarChart";
import shipmentsByDayBarChartData from "qqq/pages/dashboards/Widgets/Data/ShipmentsByDayBarChartData"; import {GenericChartDataSingleDataset} from "qqq/pages/dashboards/Widgets/Data/GenericChartDataSingleDataset";
import shipmentsByMonthLineChartData from "qqq/pages/dashboards/Widgets/Data/ShipmentsByMonthLineChartData"; import LocationCard, {LocationCardData} from "qqq/pages/dashboards/Widgets/LocationCard";
import ShipmentsByCarrierPieChart from "qqq/pages/dashboards/Widgets/ShipmentsByChannelPieChart"; import {PieChartData} from "qqq/pages/dashboards/Widgets/PieChart";
import PieChartCard from "qqq/pages/dashboards/Widgets/PieChartCard";
import ShipmentsByWarehouse from "qqq/pages/dashboards/Widgets/ShipmentsByWarehouse"; import ShipmentsByWarehouse from "qqq/pages/dashboards/Widgets/ShipmentsByWarehouse";
import SmallLineChart from "qqq/pages/dashboards/Widgets/SmallLineChart"; import SmallLineChart from "qqq/pages/dashboards/Widgets/SmallLineChart";
import StatisticsCard from "qqq/pages/dashboards/Widgets/StatisticsCard"; import StatisticsCard, {StatisticsCardData} from "qqq/pages/dashboards/Widgets/StatisticsCard";
import WarehouseCard from "qqq/pages/dashboards/Widgets/WarehouseCard"; import QClient from "qqq/utils/QClient";
const qController = QClient.getInstance();
function Overview(): JSX.Element function Overview(): JSX.Element
{ {
//////////////////////////////////
// shipments by day widget data //
//////////////////////////////////
const [shipmentsByDayTitle, setShipmentsByDayTitle] = useState("");
const [shipmentsByDayDescription, setShipmentsByDayDescription] = useState("");
const [shipmentsByDayData, setShipmentsByDayData] = useState({} as GenericChartDataSingleDataset);
const [shipmentsByMonthTitle, setShipmentsByMonthTitle] = useState("");
const [shipmentsByMonthDescription, setShipmentsByMonthDescription] = useState("");
const [shipmentsByMonthData, setShipmentsByMonthData] = useState({} as GenericChartDataSingleDataset);
const [shipmentsByCarrierTitle, setShipmentsByCarrierTitle] = useState("");
const [shipmentsByCarrierDescription, setShipmentsByCarrierDescription] = useState("");
const [shipmentsByCarrierData, setShipmentsByCarrierData] = useState({} as PieChartData);
const [todaysShipmentsData, setTodaysShipmentsData] = useState({} as StatisticsCardData);
const [shipmentsInTransitData, setShipmentsInTransitData] = useState({} as StatisticsCardData);
const [openOrdersData, setOpenOrdersData] = useState({} as StatisticsCardData);
const [shippingExceptionsData, setShippingExceptionsData] = useState({} as StatisticsCardData);
const [warehouseData, setWarehouseData] = useState([] as LocationCardData[]);
const [qInstance, setQInstance] = useState(null as QInstance);
//////////////////////////
// load meta data first //
//////////////////////////
useEffect(() =>
{
(async () =>
{
const newQInstance = await qController.loadMetaData();
setQInstance(newQInstance);
})();
}, []);
///////////////////////////////////////////////////
// once meta data has loaded, load widgets' data //
///////////////////////////////////////////////////
useEffect(() =>
{
if (!qInstance)
{
return;
}
loadShipmentsByDayData();
loadShipmentsByMonthData();
loadYTDShipmentsByCarrierData();
loadTodaysShipmentsData();
loadShipmentsInTransitData();
loadOpenOrdersData();
loadShippingExceptionsData();
loadWarehouseData();
}, [qInstance]);
function loadShipmentsByDayData()
{
(async () =>
{
const widgetData = await qController.widget("TotalShipmentsByDayBarChart");
////////////////////////////////////////////////////////////
// calculate average and number of days over that average //
////////////////////////////////////////////////////////////
let dataValues = widgetData.chartData.dataset.data;
let totalShipments = 0;
for (let i = 0; i < dataValues.length; i++)
{
totalShipments += dataValues[i];
}
let daysOverAverage = 0;
let average = Math.floor(totalShipments / 7);
for (let i = 0; i < dataValues.length; i++)
{
if (dataValues[i] > average)
{
daysOverAverage++;
}
}
const description = "Over the last week there have been <strong>" + daysOverAverage.toLocaleString() + (daysOverAverage == 1 ? " day" : " days") + "</strong> with total shipments greater than the daily average of <strong>" + average.toLocaleString() + " shipments</strong>.";
setShipmentsByDayTitle(widgetData.title);
setShipmentsByDayData(widgetData.chartData);
setShipmentsByDayDescription(description);
})();
}
function loadShipmentsByMonthData()
{
(async () =>
{
const widgetData = await qController.widget("TotalShipmentsByMonthLineChart");
/////////////////////////////////////////////
// calculate if 'increasing or decreasing' //
/////////////////////////////////////////////
let dataValues = widgetData.chartData.dataset.data;
let firstHalf = 0;
let secondHalf = 0;
for (let i = 0; i < dataValues.length; i++)
{
if (i < dataValues.length / 2)
{
firstHalf += dataValues[i];
}
else
{
secondHalf += dataValues[i];
}
}
const description = "Total shipments have been <strong>" + ((secondHalf >= firstHalf) ? "increasing" : "decreasing") + "</strong> over the last eight months.";
setShipmentsByMonthTitle(widgetData.title);
setShipmentsByMonthDescription(description);
setShipmentsByMonthData(widgetData.chartData);
})();
}
function loadYTDShipmentsByCarrierData()
{
(async () =>
{
const widgetData = await qController.widget("YTDShipmentsByCarrierPieChart");
setShipmentsByCarrierTitle(widgetData.title);
setShipmentsByCarrierDescription(widgetData.description);
setShipmentsByCarrierData(widgetData.chartData);
})();
}
function loadTodaysShipmentsData()
{
(async () =>
{
const widgetData = await qController.widget("TodaysShipmentsStatisticsCard");
setTodaysShipmentsData(widgetData);
})();
}
function loadShipmentsInTransitData()
{
(async () =>
{
const widgetData = await qController.widget("ShipmentsInTransitStatisticsCard");
setShipmentsInTransitData(widgetData);
})();
}
function loadOpenOrdersData()
{
(async () =>
{
const widgetData = await qController.widget("OpenOrdersStatisticsCard");
setOpenOrdersData(widgetData);
})();
}
function loadShippingExceptionsData()
{
(async () =>
{
const widgetData = await qController.widget("ShippingExceptionsStatisticsCard");
setShippingExceptionsData(widgetData);
})();
}
function loadWarehouseData()
{
(async () =>
{
const widgetData = await qController.widget("WarehouseLocationCards");
setWarehouseData(widgetData);
})();
}
const actionButtons = ( const actionButtons = (
<> <>
<Tooltip title="Refresh" placement="bottom"> <Tooltip title="Refresh" placement="bottom">
@ -74,30 +261,30 @@ function Overview(): JSX.Element
<MDBox mb={3}> <MDBox mb={3}>
<BarChart <BarChart
color="info" color="info"
title="Total Shipments by Day" title={shipmentsByDayTitle}
description={ description={shipmentsByDayDescription}
<span>Over the last week there have been <strong>3 days</strong> with total shipments <strong>greater than</strong> the daily average of <strong>564 shipments</strong>.</span>
}
date="Updated 3 minutes ago" date="Updated 3 minutes ago"
chart={shipmentsByDayBarChartData} data={shipmentsByDayData}
/> />
</MDBox> </MDBox>
</Grid> </Grid>
<Grid item xs={12} md={6} lg={4}> <Grid item xs={12} md={6} lg={4}>
<MDBox mb={3}> <MDBox mb={3}>
<ShipmentsByCarrierPieChart /> <PieChartCard
title={shipmentsByCarrierTitle}
description={shipmentsByCarrierDescription}
data={shipmentsByCarrierData}
/>
</MDBox> </MDBox>
</Grid> </Grid>
<Grid item xs={12} md={6} lg={4}> <Grid item xs={12} md={6} lg={4}>
<MDBox mb={3}> <MDBox mb={3}>
<SmallLineChart <SmallLineChart
color="dark" color="dark"
title="shipments by month" title={shipmentsByMonthTitle}
description={ description={shipmentsByMonthDescription}
<span>Total shipments have been <strong>increasing</strong> over the last eight months.</span> date="Just updatederp"
} chart={shipmentsByMonthData}
date="Just updated"
chart={shipmentsByMonthLineChartData}
/> />
</MDBox> </MDBox>
</Grid> </Grid>
@ -109,13 +296,8 @@ function Overview(): JSX.Element
<MDBox mb={1.5}> <MDBox mb={1.5}>
<StatisticsCard <StatisticsCard
icon="widgets" icon="widgets"
title="Today's Shipments" data={todaysShipmentsData}
count="2,813" increaseIsGood={true}
percentage={{
color: "success",
amount: "+15%",
label: "than lask week",
}}
/> />
</MDBox> </MDBox>
</Grid> </Grid>
@ -123,13 +305,8 @@ function Overview(): JSX.Element
<MDBox mb={1.5}> <MDBox mb={1.5}>
<StatisticsCard <StatisticsCard
icon="local_shipping" icon="local_shipping"
title="Shipments In Transit" data={shipmentsInTransitData}
count="1,023" increaseIsGood={true}
percentage={{
color: "success",
amount: "+1%",
label: "than yesterday",
}}
/> />
</MDBox> </MDBox>
</Grid> </Grid>
@ -138,13 +315,8 @@ function Overview(): JSX.Element
<StatisticsCard <StatisticsCard
color="warning" color="warning"
icon="receipt" icon="receipt"
title="Open Orders" data={openOrdersData}
count="213" increaseIsGood={true}
percentage={{
color: "error",
amount: "+3%",
label: "than last week",
}}
/> />
</MDBox> </MDBox>
</Grid> </Grid>
@ -153,13 +325,8 @@ function Overview(): JSX.Element
<StatisticsCard <StatisticsCard
color="error" color="error"
icon="error" icon="error"
title="Shipping Exceptions" data={shippingExceptionsData}
count="28" increaseIsGood={false}
percentage={{
color: "success",
amount: "-12%",
label: "than yesterday",
}}
/> />
</MDBox> </MDBox>
</Grid> </Grid>
@ -167,48 +334,18 @@ function Overview(): JSX.Element
</MDBox> </MDBox>
<MDBox mt={2}> <MDBox mt={2}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12} md={6} lg={4}> {
<MDBox mt={3}> warehouseData && warehouseData.map((data) => (
<WarehouseCard <Grid item xs={12} md={6} lg={4} key={data.title}>
image={edisonWarehouse} <MDBox mt={3}>
title="Edison, NJ" <LocationCard
description={ locationData={data}
<span>The Edison, NJ warehouse currently has <strong>38 open orders</strong> and <strong>39 ASNs</strong> are expected in the next week.</span> action={actionButtons}
} />
price="99% SLA" </MDBox>
location="Edison, NJ" </Grid>
action={actionButtons} ))
/> }
</MDBox>
</Grid>
<Grid item xs={12} md={6} lg={4}>
<MDBox mt={3}>
<WarehouseCard
image={pattersonWarehouse}
title="Patterson, CA"
description={
<span>The Patterson, CA warehouse shipped <strong>32,032</strong> this year. The delivery SLA is <strong>97.3%</strong>, up <strong>0.8%</strong> from last week.</span>
}
price="98% SLA"
location="Patterson, CA"
action={actionButtons}
/>
</MDBox>
</Grid>
<Grid item xs={12} md={6} lg={4}>
<MDBox mt={3}>
<WarehouseCard
image={stocktonWarehouse}
title="Stockton, CA"
description={
<span>The Stockton, CA warehouse shipped <strong>2,032</strong> packages yesterday. Last week&apos;s failed shipments were down by <strong>12%</strong>.</span>
}
price="95% SLA"
location="Stockton, CA"
action={actionButtons}
/>
</MDBox>
</Grid>
</Grid> </Grid>
</MDBox> </MDBox>
</MDBox> </MDBox>

View File

@ -0,0 +1,57 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Declaring props types for ProductCell
import MDAvatar from "qqq/components/Temporary/MDAvatar";
import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography";
interface Props
{
imageUrl: string;
label: string;
total?: string | number;
totalType?: string;
}
function ImageCell({imageUrl, label, total, totalType}: Props): JSX.Element
{
return (
<MDBox display="flex" alignItems="center" pr={2}>
<MDBox mr={2}>
<MDAvatar src={imageUrl} alt={label} />
</MDBox>
<MDBox display="flex" flexDirection="column">
<MDTypography variant="button" fontWeight="medium">
{label}
</MDTypography>
<MDTypography variant="button" fontWeight="regular" color="secondary">
<MDTypography component="span" variant="button" fontWeight="regular" color="success">
{total}
</MDTypography>{" "}
{totalType}
</MDTypography>
</MDBox>
</MDBox>
);
}
export default ImageCell;

View File

@ -22,31 +22,124 @@
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import {useMemo, ReactNode} from "react"; import parse from "html-react-parser";
import {useMemo} from "react";
import {Bar} from "react-chartjs-2"; import {Bar} from "react-chartjs-2";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
import configs from "qqq/pages/dashboards/Widgets/Configs/BarChartConfig"; import {GenericChartDataSingleDataset} from "qqq/pages/dashboards/Widgets/Data/GenericChartDataSingleDataset";
// Declaring props types for ReportsBarChart
interface Props { ///////////////////////////
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "dark"; // options for bar chart //
title: string; ///////////////////////////
description?: string | ReactNode; const options = {
date: string; responsive: true,
chart: { maintainAspectRatio: false,
labels: string[]; plugins: {
datasets: { legend: {
label: string; display: false,
data: number[]; },
}; },
}; interaction: {
[key: string]: any; intersect: false,
mode: "index",
},
scales: {
y: {
grid: {
drawBorder: false,
display: true,
drawOnChartArea: true,
drawTicks: false,
borderDash: [5, 5],
color: "rgba(255, 255, 255, .2)",
},
ticks: {
suggestedMin: 0,
suggestedMax: 500,
beginAtZero: true,
padding: 10,
font: {
size: 14,
weight: 300,
family: "Roboto",
style: "normal",
lineHeight: 2,
},
color: "#fff",
},
},
x: {
grid: {
drawBorder: false,
display: true,
drawOnChartArea: true,
drawTicks: false,
borderDash: [5, 5],
color: "rgba(255, 255, 255, .2)",
},
ticks: {
display: true,
color: "#f8f9fa",
padding: 10,
font: {
size: 14,
weight: 300,
family: "Roboto",
style: "normal",
lineHeight: 2,
},
},
},
},
};
////////////////////////////////////
// define properties and defaults //
////////////////////////////////////
interface Props
{
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "dark";
title: string;
description?: string;
date: string;
data: GenericChartDataSingleDataset;
} }
function BarChart({color, title, description, date, chart}: Props): JSX.Element BarChart.defaultProps = {
color: "dark",
description: "",
};
function getChartData(labels: any, dataset: any)
{ {
const {data, options} = configs(chart.labels || [], chart.datasets || {}); return {
chartData: {
labels,
datasets: [
{
label: dataset?.label,
tension: 0.4,
borderWidth: 0,
borderRadius: 4,
borderSkipped: false,
backgroundColor: "rgba(255, 255, 255, 0.8)",
data: dataset?.data,
maxBarThickness: 6,
},
],
}
};
}
function BarChart({color, title, description, date, data}: Props): JSX.Element
{
/////////////////////////////////////////////////////////
// enrich data with expected customizations and styles //
/////////////////////////////////////////////////////////
const {chartData} = getChartData(data?.labels, data?.dataset);
return ( return (
<Card sx={{height: "100%"}}> <Card sx={{height: "100%"}}>
@ -63,17 +156,17 @@ function BarChart({color, title, description, date, chart}: Props): JSX.Element
mt={-5} mt={-5}
height="12.5rem" height="12.5rem"
> >
<Bar data={data} options={options} /> <Bar data={chartData} options={options} />
</MDBox> </MDBox>
), ),
[chart, color] [data, color]
)} )}
<MDBox pt={3} pb={1} px={1}> <MDBox pt={3} pb={1} px={1}>
<MDTypography variant="h6" textTransform="capitalize"> <MDTypography variant="h6" textTransform="capitalize">
{title} {title}
</MDTypography> </MDTypography>
<MDTypography component="div" variant="button" color="text" fontWeight="light"> <MDTypography component="div" variant="button" color="text" fontWeight="light">
{description} {parse(description)}
</MDTypography> </MDTypography>
<Divider /> <Divider />
<MDBox display="flex" alignItems="center"> <MDBox display="flex" alignItems="center">
@ -90,10 +183,4 @@ function BarChart({color, title, description, date, chart}: Props): JSX.Element
); );
} }
// Setting default values for the props of ReportsBarChart
BarChart.defaultProps = {
color: "dark",
description: "",
};
export default BarChart; export default BarChart;

View File

@ -19,10 +19,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
function configs(labels: any, datasets: any) function configs(labels: any, datasets: any)
{ {
return { return {
data: { dataderp: {
labels, labels,
datasets: [ datasets: [
{ {

View File

@ -1,87 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import DefaultCell from "layouts/dashboards/sales/components/DefaultCell";
import ProductCell from "layouts/dashboards/sales/components/ProductCell";
import RefundsCell from "layouts/dashboards/sales/components/RefundsCell";
import axlehire from "qqq/images/carrier-logos/axlehire.png"
import cdl from "qqq/images/carrier-logos/cdl.png"
import dhl from "qqq/images/carrier-logos/dhl.png"
import fedex from "qqq/images/carrier-logos/fedex.png"
import lso from "qqq/images/carrier-logos/lso.png"
import ontrac from "qqq/images/carrier-logos/ontrac.png"
import ups from "qqq/images/carrier-logos/ups.png"
const carrierSpendData = {
columns: [
{Header: "carrier", accessor: "product", width: "55%"},
{Header: "total YTD", accessor: "value"},
{Header: "monthly average", accessor: "adsSpent", align: "center"},
{Header: "service failures", accessor: "refunds", align: "center"},
],
rows: [
{
product: <ProductCell image={axlehire} name="AxleHire" orders="921" />,
value: <DefaultCell>$140,925</DefaultCell>,
adsSpent: <DefaultCell>$24,531</DefaultCell>,
refunds: <RefundsCell value={121} icon={{color: "success", name: "keyboard_arrow_up"}} />,
},
{
product: <ProductCell image={cdl} name="CDL" orders="2,421" />,
value: <DefaultCell>$40,600</DefaultCell>,
adsSpent: <DefaultCell>$9,430</DefaultCell>,
refunds: <RefundsCell value={54} icon={{color: "success", name: "keyboard_arrow_up"}} />,
},
{
product: <ProductCell image={dhl} name="DHL" orders="1,391" />,
value: <DefaultCell>$90,233</DefaultCell>,
adsSpent: <DefaultCell>$18.30</DefaultCell>,
refunds: <RefundsCell value={54} icon={{color: "success", name: "keyboard_arrow_up"}} />,
},
{
product: <ProductCell image={fedex} name="FedEx" orders="12,821" />,
value: <DefaultCell>$80,250</DefaultCell>,
adsSpent: <DefaultCell>$4,200</DefaultCell>,
refunds: <RefundsCell value={40} icon={{color: "error", name: "keyboard_arrow_down"}} />,
},
{
product: <ProductCell image={lso} name="LSO" orders="5,921" />,
value: <DefaultCell>$91,300</DefaultCell>,
adsSpent: <DefaultCell>$7,364</DefaultCell>,
refunds: <RefundsCell value={5} icon={{color: "error", name: "keyboard_arrow_down"}} />,
},
{
product: <ProductCell image={ontrac} name="OnTrac" orders="5,921" />,
value: <DefaultCell>$77,300</DefaultCell>,
adsSpent: <DefaultCell>$4,064</DefaultCell>,
refunds: <RefundsCell value={5} icon={{color: "error", name: "keyboard_arrow_down"}} />,
},
{
product: <ProductCell image={ups} name="UPS" orders="8,232" />,
value: <DefaultCell>$130,992</DefaultCell>,
adsSpent: <DefaultCell>$9,500</DefaultCell>,
refunds: <RefundsCell value={13} icon={{color: "success", name: "keyboard_arrow_up"}} />,
},
],
};
export default carrierSpendData;

View File

@ -1,75 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
interface Types {
labels: string[];
datasets: {
label: string;
color: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark";
data: number[];
}[];
}
const carrierVolumeLineChartData: Types = {
labels: ["Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
datasets: [
{
label: " AxleHire",
color: "dark",
data: [500, 200, 110, 150, 440, 670, 100, 150, 300],
},
{
label: " CDL",
color: "info",
data: [1000, 3000, 4000, 1200, 1500, 2200, 2800, 3500, 4500],
},
{
label: " DHL",
color: "primary",
data: [3489, 5932, 4332, 8234, 9239, 10823, 9483, 11909, 11808],
},
{
label: " FedEx",
color: "success",
data: [20388, 21008, 19323, 17934, 18399, 22090, 23909, 25800, 28833],
},
{
label: " LSO",
color: "error",
data: [100, 300, 400, 1200, 1500, 2200, 2800, 2500, 2800],
},
{
label: " OnTrac",
color: "secondary",
data: [3489, 5932, 4332, 8234, 9239, 10823, 9483, 11909, 11808],
},
{
label: " UPS",
color: "warning",
data: [19348, 18008, 20844, 16034, 24000, 23480, 26809, 27888, 27909],
},
],
};
"warning"
export default carrierVolumeLineChartData;

View File

@ -19,9 +19,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
const shipmentsByDayBarChartData = {
labels: ["M", "T", "W", "T", "F", "S", "S"],
datasets: {label: "Sales", data: [503, 202, 1001, 354, 659, 938, 350]},
};
export default shipmentsByDayBarChartData; //////////////////////////////////////
// structure of expected chart data //
//////////////////////////////////////
export interface GenericChartData
{
description?: string;
labels: string[];
datasets: {
label: string;
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark";
data: number[];
}[];
}

View File

@ -19,10 +19,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
const shipmentsByMonthLineChartData =
{
labels: ["Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
datasets: {label: "Mobile apps", data: [50, 40, 300, 320, 500, 350, 200, 230, 500]},
};
export default shipmentsByMonthLineChartData; //////////////////////////////////////
// structure of expected chart data //
//////////////////////////////////////
export interface GenericChartDataSingleDataset
{
labels: string[];
dataset: {
label: string;
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark";
data: number[];
};
}

View File

@ -1,36 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import MDBadgeDot from "components/MDBadgeDot";
import MDBox from "components/MDBox";
const shipmentsByCarrierPieChartData = {
labels: [" AxleHire", " CDL", " DHL", " FedEx", " LSO", " OnTrac", " UPS"],
datasets: {
label: "Projects",
backgroundColors: ["dark", "info", "primary", "success", "error", "secondary", "warning"],
data: [523, 1139, 1933, 3248, 993, 103, 2439]
},
};
export default shipmentsByCarrierPieChartData;

View File

@ -0,0 +1,230 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Card from "@mui/material/Card";
import Icon from "@mui/material/Icon";
import React, {ReactNode, useMemo} from "react";
import {Line} from "react-chartjs-2";
import colors from "assets/theme/base/colors";
import MDBadgeDot from "qqq/components/Temporary/MDBadgeDot";
import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography";
//////////////////////////////////////////
// structure of default line chart data //
//////////////////////////////////////////
export interface DefaultLineChartData
{
labels: string[];
lineLabels?: string[];
datasets: {
label: string;
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark";
data: number[];
}[];
};
/////////////////////
// display options //
/////////////////////
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
interaction: {
intersect: false,
mode: "index",
},
scales: {
y: {
grid: {
drawBorder: false,
display: true,
drawOnChartArea: true,
drawTicks: false,
borderDash: [5, 5],
color: "#c1c4ce5c",
},
ticks: {
display: true,
padding: 10,
color: "#9ca2b7",
font: {
size: 14,
weight: 300,
family: "Roboto",
style: "normal",
lineHeight: 2,
},
},
},
x: {
grid: {
drawBorder: false,
display: true,
drawOnChartArea: true,
drawTicks: true,
borderDash: [5, 5],
color: "#c1c4ce5c",
},
ticks: {
display: true,
color: "#9ca2b7",
padding: 10,
font: {
size: 14,
weight: 300,
family: "Roboto",
style: "normal",
lineHeight: 2,
},
},
},
},
};
/////////////////////////
// inputs and defaults //
/////////////////////////
interface Props
{
icon?: {
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark";
component: ReactNode;
};
title?: string;
height?: string | number;
data: DefaultLineChartData;
[key: string]: any;
}
DefaultLineChart.defaultProps = {
icon: {color: "info", component: ""},
title: "",
height: "19.125rem",
};
function DefaultLineChart({icon, title, height, data}: Props): JSX.Element
{
const allBackgroundColors = ["info", "warning", "primary", "success", "error", "secondary", "dark"];
if (data && data.datasets)
{
data.datasets.forEach((ds, index) =>
{
// @ts-ignore
ds.color = allBackgroundColors[index];
});
}
const chartDatasets = data && data.datasets
? data.datasets.map((dataset) => ({
...dataset,
tension: 0,
pointRadius: 3,
borderWidth: 4,
backgroundColor: "transparent",
fill: true,
pointBackgroundColor: colors[dataset.color]
? colors[dataset.color || "dark"].main
: colors.dark.main,
borderColor: colors[dataset.color]
? colors[dataset.color || "dark"].main
: colors.dark.main,
maxBarThickness: 6,
}))
: [];
let fullData = {};
if (data)
{
fullData = {
labels: data.labels,
datasets: chartDatasets
};
}
const renderChart = (
<MDBox py={2} pr={2} pl={icon.component ? 1 : 2}>
{title ? (
<MDBox display="flex" px={0} pt={0}>
{icon.component && (
<MDBox
width="4rem"
height="4rem"
bgColor={icon.color || "info"}
variant="gradient"
coloredShadow={icon.color || "info"}
borderRadius="xl"
display="flex"
justifyContent="center"
alignItems="center"
color="white"
mt={-5}
mr={2}
>
<Icon fontSize="medium">{icon.component}</Icon>
</MDBox>
)}
<MDBox mt={icon.component ? -2 : 0}>
{title && <MDTypography variant="h6">{title}</MDTypography>}
<MDBox mb={2}>
<MDTypography component="div" variant="button" color="text">
<MDBox display="flex" justifyContent="space-between">
<MDBox display="flex" ml={-1}>
{
data && data.lineLabels ? (
(data.lineLabels.map((label: string, index: number) => (
<MDBox key={index}>
<MDBadgeDot color={allBackgroundColors[index]} size="sm" badgeContent={label} />
</MDBox>
)
))) : null
}
</MDBox>
</MDBox>
</MDTypography>
</MDBox>
</MDBox>
</MDBox>
) : null}
{useMemo(
() => (
<MDBox height={height}>
<Line data={fullData} options={options} />
</MDBox>
),
[data, height]
)}
</MDBox>
);
return title ? <Card>{renderChart}</Card> : renderChart;
}
export default DefaultLineChart;

View File

@ -26,8 +26,72 @@ import {Bar} from "react-chartjs-2";
import colors from "qqq/components/Temporary/colors"; import colors from "qqq/components/Temporary/colors";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
import configs from "qqq/pages/dashboards/Widgets/Configs/HorizontalBarChartConfigs" import {GenericChartData} from "qqq/pages/dashboards/Widgets/Data/GenericChartData";
//////////////////
// configuation //
//////////////////
const options = {
indexAxis: "y",
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
y: {
grid: {
drawBorder: false,
display: true,
drawOnChartArea: true,
drawTicks: false,
borderDash: [5, 5],
color: "#c1c4ce5c",
},
ticks: {
display: true,
padding: 10,
color: "#9ca2b7",
font: {
size: 14,
weight: 300,
family: "Roboto",
style: "normal",
lineHeight: 2,
},
},
},
x: {
grid: {
drawBorder: false,
display: false,
drawOnChartArea: true,
drawTicks: true,
color: "#c1c4ce5c",
},
ticks: {
display: true,
color: "#9ca2b7",
padding: 10,
font: {
size: 14,
weight: 300,
family: "Roboto",
style: "normal",
lineHeight: 2,
},
},
},
},
};
/////////////////////////
// inputs and defaults //
/////////////////////////
interface Props interface Props
{ {
icon?: { icon?: {
@ -37,22 +101,15 @@ interface Props
title?: string; title?: string;
description?: string | ReactNode; description?: string | ReactNode;
height?: string | number; height?: string | number;
chart: { data: GenericChartData;
labels: string[];
datasets: {
label: string;
color: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark";
data: number[];
}[];
};
[key: string]: any; [key: string]: any;
} }
function HorizontalBarChart({icon, title, description, height, chart}: Props): JSX.Element function HorizontalBarChart({icon, title, description, height, data}: Props): JSX.Element
{ {
const chartDatasets = chart.datasets const chartDatasets = data.datasets
? chart.datasets.map((dataset) => ({ ? data.datasets.map((dataset) => ({
...dataset, ...dataset,
weight: 5, weight: 5,
borderWidth: 0, borderWidth: 0,
@ -65,7 +122,14 @@ function HorizontalBarChart({icon, title, description, height, chart}: Props): J
})) }))
: []; : [];
const {data, options} = configs(chart.labels || [], chartDatasets); let fullData = {};
if (data)
{
fullData = {
labels: data.labels,
datasets: chartDatasets
};
}
const renderChart = ( const renderChart = (
<MDBox py={2} pr={2} pl={icon.component ? 1 : 2}> <MDBox py={2} pr={2} pl={icon.component ? 1 : 2}>
@ -102,10 +166,10 @@ function HorizontalBarChart({icon, title, description, height, chart}: Props): J
{useMemo( {useMemo(
() => ( () => (
<MDBox height={height}> <MDBox height={height}>
<Bar data={data} options={options} /> <Bar data={fullData} options={options} />
</MDBox> </MDBox>
), ),
[chart, height] [data, height]
)} )}
</MDBox> </MDBox>
); );

View File

@ -21,32 +21,122 @@
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import {useMemo, ReactNode} from "react"; import {ReactNode, useMemo} from "react";
import {Line} from "react-chartjs-2"; import {Line} from "react-chartjs-2";
import colors from "qqq/components/Temporary/colors"; import colors from "qqq/components/Temporary/colors";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
import configs from "qqq/pages/dashboards/Widgets/Configs/LineChartConfigs"; import configs from "qqq/pages/dashboards/Widgets/Configs/LineChartConfigs";
interface Props {
icon?: { ///////////////////////////////////////////
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark"; // structure of expected line chart data //
component: ReactNode; ///////////////////////////////////////////
}; export interface LineChartData
title?: string; {
description?: string | ReactNode; labels: string[];
height?: string | number; datasets: {
chart: {
labels: string[];
datasets: {
label: string; label: string;
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark"; color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark";
data: number[]; data: number[];
}[]; }[];
}; };
[key: string]: any;
////////////////////////
// line chart options //
////////////////////////
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
interaction: {
intersect: false,
mode: "index",
},
scales: {
y: {
grid: {
drawBorder: false,
display: true,
drawOnChartArea: true,
drawTicks: false,
borderDash: [5, 5],
color: "rgba(255, 255, 255, .2)",
},
ticks: {
display: true,
color: "#f8f9fa",
padding: 10,
font: {
size: 14,
weight: 300,
family: "Roboto",
style: "normal",
lineHeight: 2,
},
},
},
x: {
grid: {
drawBorder: false,
display: false,
drawOnChartArea: false,
drawTicks: false,
borderDash: [5, 5],
},
ticks: {
display: true,
color: "#f8f9fa",
padding: 10,
font: {
size: 14,
weight: 300,
family: "Roboto",
style: "normal",
lineHeight: 2,
},
},
},
},
};
//////////////////////////////////////////
// define input properties and defaults //
//////////////////////////////////////////
interface Props
{
icon?: {
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark";
component: ReactNode;
};
title?: string;
description?: string | ReactNode;
height?: string | number;
chart: {
labels: string[];
datasets: {
label: string;
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark";
data: number[];
}[];
};
[key: string]: any;
} }
LineChart.defaultProps = {
icon: {color: "info", component: ""},
title: "",
description: "",
height: "19.125rem",
};
function LineChart({icon, title, description, height, chart}: Props): JSX.Element function LineChart({icon, title, description, height, chart}: Props): JSX.Element
{ {
const chartDatasets = chart.datasets const chartDatasets = chart.datasets
@ -67,7 +157,7 @@ function LineChart({icon, title, description, height, chart}: Props): JSX.Elemen
})) }))
: []; : [];
const {data, options} = configs(chart.labels || [], chartDatasets); const {data} = configs(chart.labels || [], chartDatasets);
const renderChart = ( const renderChart = (
<MDBox py={2} pr={2} pl={icon.component ? 1 : 2}> <MDBox py={2} pr={2} pl={icon.component ? 1 : 2}>
@ -115,11 +205,5 @@ function LineChart({icon, title, description, height, chart}: Props): JSX.Elemen
return title || description ? <Card>{renderChart}</Card> : renderChart; return title || description ? <Card>{renderChart}</Card> : renderChart;
} }
LineChart.defaultProps = {
icon: {color: "info", component: ""},
title: "",
description: "",
height: "19.125rem",
};
export default LineChart; export default LineChart;

View File

@ -22,22 +22,39 @@
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import parse from "html-react-parser";
import {ReactNode} from "react"; import {ReactNode} from "react";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
interface Props { //////////////////////////////////////////
image: string; // structure of location card data //
title: string; //////////////////////////////////////////
description: string | ReactNode; export interface LocationCardData
price: string; {
location: ReactNode; imageUrl: string;
action?: ReactNode | boolean; title: string;
[key: string]: any; description: string;
footerText: string;
location: string;
} }
function WarehouseCard({image, title, description, price, location, action}: Props): JSX.Element interface Props
{ {
locationData: LocationCardData;
action?: ReactNode | boolean;
[key: string]: any;
}
LocationCard.defaultProps = {
action: false,
};
function LocationCard({locationData, action}: Props): JSX.Element
{
const {imageUrl, title, description, footerText, location} = locationData;
return ( return (
<Card> <Card>
<MDBox <MDBox
@ -50,7 +67,7 @@ function WarehouseCard({image, title, description, price, location, action}: Pro
> >
<MDBox <MDBox
component="img" component="img"
src={image} src={imageUrl}
alt={title} alt={title}
borderRadius="lg" borderRadius="lg"
shadow="md" shadow="md"
@ -68,7 +85,7 @@ function WarehouseCard({image, title, description, price, location, action}: Pro
left={0} left={0}
top="0" top="0"
sx={{ sx={{
backgroundImage: `url(${image})`, backgroundImage: `url(${locationData.imageUrl})`,
transform: "scale(0.94)", transform: "scale(0.94)",
filter: "blur(12px)", filter: "blur(12px)",
backgroundSize: "cover", backgroundSize: "cover",
@ -83,7 +100,7 @@ function WarehouseCard({image, title, description, price, location, action}: Pro
{title} {title}
</MDTypography> </MDTypography>
<MDTypography variant="body2" color="text" sx={{mt: 1.5, mb: 1}}> <MDTypography variant="body2" color="text" sx={{mt: 1.5, mb: 1}}>
{description} {parse(description)}
</MDTypography> </MDTypography>
</MDBox> </MDBox>
<Divider /> <Divider />
@ -97,11 +114,11 @@ function WarehouseCard({image, title, description, price, location, action}: Pro
lineHeight={1} lineHeight={1}
> >
<MDTypography variant="body2" fontWeight="regular" color="text"> <MDTypography variant="body2" fontWeight="regular" color="text">
{price} {footerText}
</MDTypography> </MDTypography>
<MDBox color="text" display="flex" alignItems="center"> <MDBox color="text" display="flex" alignItems="center">
<Icon color="inherit" sx={{m: 0.5}}> <Icon color="inherit" sx={{m: 0.5}}>
place place
</Icon> </Icon>
<MDTypography variant="button" fontWeight="light" color="text"> <MDTypography variant="button" fontWeight="light" color="text">
{location} {location}
@ -112,8 +129,4 @@ function WarehouseCard({image, title, description, price, location, action}: Pro
); );
} }
WarehouseCard.defaultProps = { export default LocationCard;
action: false,
};
export default WarehouseCard;

View File

@ -21,11 +21,26 @@
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import {useMemo, ReactNode} from "react"; import parse from "html-react-parser";
import {ReactNode, useMemo} from "react";
import {Pie} from "react-chartjs-2"; import {Pie} from "react-chartjs-2";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
import configs from "qqq/pages/dashboards/Widgets/Configs/PieChartConfigs" import configs from "qqq/pages/dashboards/Widgets/Configs/PieChartConfigs";
//////////////////////////////////////////
// structure of expected bar chart data //
//////////////////////////////////////////
export interface PieChartData
{
labels: string[];
dataset: {
label: string;
backgroundColors?: string[];
data: number[];
};
}
// Declaring props types for PieChart // Declaring props types for PieChart
interface Props interface Props
@ -35,23 +50,16 @@ interface Props
component: ReactNode; component: ReactNode;
}; };
title?: string; title?: string;
description?: string | ReactNode; description?: string;
height?: string | number; height?: string | number;
chart: { chart: PieChartData;
labels: string[];
datasets: {
label: string;
backgroundColors: string[];
data: number[];
};
};
[key: string]: any; [key: string]: any;
} }
function PieChart({icon, title, description, height, chart}: Props): JSX.Element function PieChart({icon, title, description, height, chart}: Props): JSX.Element
{ {
const {data, options} = configs(chart.labels || [], chart.datasets || {}); const {data, options} = configs(chart?.labels || [], chart?.dataset || {});
const renderChart = ( const renderChart = (
<MDBox py={2} pr={2} pl={icon.component ? 1 : 2}> <MDBox py={2} pr={2} pl={icon.component ? 1 : 2}>
@ -79,7 +87,7 @@ function PieChart({icon, title, description, height, chart}: Props): JSX.Element
{title && <MDTypography variant="h6">{title}</MDTypography>} {title && <MDTypography variant="h6">{title}</MDTypography>}
<MDBox mb={2}> <MDBox mb={2}>
<MDTypography component="div" variant="button" color="text"> <MDTypography component="div" variant="button" color="text">
{description} {parse(description)}
</MDTypography> </MDTypography>
</MDBox> </MDBox>
</MDBox> </MDBox>

View File

@ -22,61 +22,61 @@
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import {useMaterialUIController} from "context"; import parse from "html-react-parser";
import MDBadgeDot from "qqq/components/Temporary/MDBadgeDot"; import MDBadgeDot from "qqq/components/Temporary/MDBadgeDot";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
import shipmentsByCarrierPieChartData from "qqq/pages/dashboards/Widgets/Data/ShipmentsByCarrierPieChartData"; import PieChart, {PieChartData} from "qqq/pages/dashboards/Widgets/PieChart";
import PieChart from "qqq/pages/dashboards/Widgets/PieChart";
function ShipmentsByCarrierPieChart(): JSX.Element // Declaring props types for PieChart
interface Props
{ {
const [controller] = useMaterialUIController(); title?: string;
const {darkMode} = controller; description?: string;
data: PieChartData;
[key: string]: any;
}
function PieChartCard({title, description, data}: Props): JSX.Element
{
const allBackgroundColors = ["info", "warning", "primary", "success", "error", "secondary", "dark"];
if (data && data.dataset)
{
data.dataset.backgroundColors = allBackgroundColors;
}
return ( return (
<Card sx={{height: "100%"}}> <Card sx={{height: "100%"}}>
<MDBox display="flex" justifyContent="space-between" alignItems="center" pt={2} px={2}> <MDBox display="flex" justifyContent="space-between" alignItems="center" pt={2} px={2}>
<MDTypography variant="h6">Shipments By Carrier Year To Date</MDTypography> <MDTypography variant="h6">{title}</MDTypography>
</MDBox> </MDBox>
<MDBox mt={3}> <MDBox mt={3}>
<Grid container alignItems="center"> <Grid container alignItems="center">
<Grid item xs={7}> <Grid item xs={7}>
<PieChart chart={shipmentsByCarrierPieChartData} height="9.5rem" /> <PieChart chart={data} height="9.5rem" />
</Grid> </Grid>
<Grid item xs={5}> <Grid item xs={5}>
<MDBox pr={1}> <MDBox pr={1}>
<MDBox> {
<MDBadgeDot color="dark" size="sm" badgeContent="AxleHire" /> data && data.labels ? (
</MDBox> (data.labels.map((label: string, index: number) => (
<MDBox> <MDBox key={index}>
<MDBadgeDot color="info" size="sm" badgeContent="CDL" /> <MDBadgeDot color={allBackgroundColors[index]} size="sm" badgeContent={label} />
</MDBox> </MDBox>
<MDBox> )
<MDBadgeDot color="primary" size="sm" badgeContent="DHL" /> ))) : null
</MDBox> }
<MDBox>
<MDBadgeDot color="success" size="sm" badgeContent="FedEx" />
</MDBox>
<MDBox>
<MDBadgeDot color="error" size="sm" badgeContent="LSO" />
</MDBox>
<MDBox>
<MDBadgeDot color="secondary" size="sm" badgeContent="OnTrac" />
</MDBox>
<MDBox>
<MDBadgeDot color="warning" size="sm" badgeContent="UPS" />
</MDBox>
</MDBox> </MDBox>
</Grid> </Grid>
</Grid> </Grid>
<Divider /> <Divider />
<Grid container> <Grid container>
<Grid item xs={12}> <Grid item xs={12}>
<MDBox pb={2} px={2} display="flex" flexDirection={{xs: "column", sm: "row"}} mt="auto" > <MDBox pb={2} px={2} display="flex" flexDirection={{xs: "column", sm: "row"}} mt="auto">
<MDTypography variant="button" color="text" fontWeight="light"> <MDTypography variant="button" color="text" fontWeight="light">
<strong>Fedex and UPS</strong> delivered the majority of shipments with a combined percentage of <strong>55%</strong>. {parse(description)}
The fewest shipments were delivered by <strong>AxleHire and OnTrac</strong> combining for <strong>6%</strong>.
</MDTypography> </MDTypography>
</MDBox> </MDBox>
</Grid> </Grid>
@ -86,4 +86,4 @@ function ShipmentsByCarrierPieChart(): JSX.Element
); );
} }
export default ShipmentsByCarrierPieChart; export default PieChartCard;

View File

@ -22,30 +22,46 @@
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import {ReactNode} from "react"; import {ReactNode} from "react";
import {useMaterialUIController} from "context";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
import {StatisticsCardData} from "qqq/pages/dashboards/Widgets/StatisticsCard";
interface Props { interface Props
title: string; {
count: string | number; data: StatisticsCardData;
percentage?: { increaseIsGood: boolean;
color: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "dark" | "white"; dropdown?: {
value: string | number; action: (...args: any) => void;
label: string; menu: ReactNode;
}; value: string;
dropdown?: { };
action: (...args: any) => void;
menu: ReactNode; [key: string]: any;
value: string;
};
[key: string]: any;
} }
function SimpleStatisticsCard({title, count, percentage, dropdown}: Props): JSX.Element function SimpleStatisticsCard({data, increaseIsGood, dropdown}: Props): JSX.Element
{ {
const [controller] = useMaterialUIController(); const {title, count, percentageAmount, percentageLabel} = data;
const {darkMode} = controller;
let percentageString = "";
if (percentageAmount)
{
percentageString = percentageAmount.toLocaleString() + "%";
if (percentageAmount > 0)
{
percentageString = "+" + percentageString;
}
}
let percentColor: string;
if (increaseIsGood)
{
percentColor = (percentageAmount > 0) ? "success" : "warning";
}
else
{
percentColor = (percentageAmount < 0) ? "success" : "warning";
}
return ( return (
<Card> <Card>
@ -63,17 +79,21 @@ function SimpleStatisticsCard({title, count, percentage, dropdown}: Props): JSX.
</MDTypography> </MDTypography>
</MDBox> </MDBox>
<MDBox lineHeight={1}> <MDBox lineHeight={1}>
<MDTypography variant="h5" fontWeight="bold"> {
{count} count ? (
</MDTypography> <MDTypography variant="h5" fontWeight="bold">
<MDTypography variant="button" fontWeight="bold" color={percentage.color}> {count.toLocaleString()}
{percentage.value}&nbsp; </MDTypography>
) : null
}
<MDTypography variant="button" fontWeight="bold" color={percentColor}>
{percentageString}&nbsp;
<MDTypography <MDTypography
variant="button" variant="button"
fontWeight="regular" fontWeight="regular"
color={darkMode ? "text" : "secondary"} color={"secondary"}
> >
{percentage.label} {percentageLabel}
</MDTypography> </MDTypography>
</MDTypography> </MDTypography>
</MDBox> </MDBox>

View File

@ -22,30 +22,42 @@
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import {useMemo, ReactNode} from "react"; import parse from "html-react-parser";
import {useMemo} from "react";
import {Line} from "react-chartjs-2"; import {Line} from "react-chartjs-2";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
import configs from "qqq/pages/dashboards/Widgets/Configs/LineChartConfigs" import configs from "qqq/pages/dashboards/Widgets/Configs/LineChartConfigs";
interface Props { //////////////////////////////////////////
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "dark"; // structure of expected bar chart data //
title: string; //////////////////////////////////////////
description?: string | ReactNode; export interface SmallLineChartData
date: string; {
chart: { labels: string[];
labels: string[]; dataset: {
datasets: {
label: string; label: string;
data: number[]; data: number[];
}; };
}; }
[key: string]: any;
interface Props
{
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "dark";
title: string;
description?: string;
date: string;
chart: SmallLineChartData;
[key: string]: any;
} }
function SmallLineChart({color, title, description, date, chart}: Props): JSX.Element function SmallLineChart({color, title, description, date, chart}: Props): JSX.Element
{ {
const {data, options} = configs(chart.labels || [], chart.datasets || {}); const {data, options} = configs(chart?.labels || [], chart?.dataset || {});
console.log(`DATA: ${JSON.stringify(data)}`);
return ( return (
<Card sx={{height: "100%"}}> <Card sx={{height: "100%"}}>
@ -72,7 +84,7 @@ function SmallLineChart({color, title, description, date, chart}: Props): JSX.El
{title} {title}
</MDTypography> </MDTypography>
<MDTypography component="div" variant="button" color="text" fontWeight="light"> <MDTypography component="div" variant="button" color="text" fontWeight="light">
{description} {parse(description)}
</MDTypography> </MDTypography>
<Divider /> <Divider />
<MDBox display="flex" alignItems="center"> <MDBox display="flex" alignItems="center">

View File

@ -26,22 +26,67 @@ import {ReactNode} from "react";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography"; import MDTypography from "qqq/components/Temporary/MDTypography";
// Declaring props types for CompleStatisticsCard ///////////////////////////////////////////
interface Props { // structure of expected stats card data //
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark"; ///////////////////////////////////////////
title: string; export interface StatisticsCardData
count: string | number; {
percentage?: { title: string;
color: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "dark" | "white"; count: number;
amount: string | number; percentageAmount: number;
label: string; percentageLabel: string;
};
icon: ReactNode;
[key: string]: any;
} }
function StatisticsCard({color, title, count, percentage, icon}: Props): JSX.Element /////////////////////////
// inputs and defaults //
/////////////////////////
interface Props
{ {
data: StatisticsCardData;
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark";
icon: ReactNode;
increaseIsGood: boolean;
dropdown?: {
action: (...args: any) => void;
menu: ReactNode;
value: string;
};
[key: string]: any;
}
StatisticsCard.defaultProps = {
color: "info",
increaseIsGood: true
};
function StatisticsCard({data, color, icon, increaseIsGood}: Props): JSX.Element
{
const {title, count, percentageAmount, percentageLabel} = data;
let percentageString = "";
if (percentageAmount)
{
percentageString = percentageAmount.toLocaleString() + "%";
if (percentageAmount > 0)
{
percentageString = "+" + percentageString;
}
}
let percentColor = "dark";
if (percentageAmount !== 0)
{
if (increaseIsGood)
{
percentColor = (percentageAmount > 0) ? "success" : "warning";
}
else
{
percentColor = (percentageAmount < 0) ? "success" : "warning";
}
}
return ( return (
<Card> <Card>
<MDBox display="flex" justifyContent="space-between" pt={1} px={2}> <MDBox display="flex" justifyContent="space-between" pt={1} px={2}>
@ -66,34 +111,34 @@ function StatisticsCard({color, title, count, percentage, icon}: Props): JSX.Ele
<MDTypography variant="button" fontWeight="light" color="text"> <MDTypography variant="button" fontWeight="light" color="text">
{title} {title}
</MDTypography> </MDTypography>
<MDTypography variant="h4">{count}</MDTypography> {
count !== undefined ? (
<MDTypography variant="h4">{count.toLocaleString()}</MDTypography>
) : null
}
</MDBox> </MDBox>
</MDBox> </MDBox>
<Divider /> <Divider />
<MDBox pb={2} px={2}> {
<MDTypography component="p" variant="button" color="text" display="flex"> percentageAmount !== undefined && percentageAmount !== 0 ? (
<MDTypography <MDBox pb={2} px={2}>
component="span" <MDTypography component="p" variant="button" color="text" display="flex">
variant="button" <MDTypography
fontWeight="bold" component="span"
color={percentage.color} variant="button"
> fontWeight="bold"
{percentage.amount} color={percentColor}
</MDTypography> >
&nbsp;{percentage.label} {percentageString}
</MDTypography> </MDTypography>
</MDBox> &nbsp;{percentageLabel}
</MDTypography>
</MDBox>
) : null
}
</Card> </Card>
); );
} }
StatisticsCard.defaultProps = {
color: "info",
percentage: {
color: "success",
text: "",
label: "",
},
};
export default StatisticsCard; export default StatisticsCard;

View File

@ -0,0 +1,142 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Icon} from "@mui/material";
import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import React, {useEffect, useState} from "react";
import DataTable, {TableDataInput} from "qqq/components/Temporary/DataTable";
import MDBox from "qqq/components/Temporary/MDBox";
import MDTypography from "qqq/components/Temporary/MDTypography";
/////////////////////////
// inputs and defaults //
/////////////////////////
interface Props
{
title: string;
data: TableDataInput;
dropdownOptions?: string[];
dropdownOnChange?: (selectedValue: string, widgetIndex: number) => void;
widgetIndex?: number;
[key: string]: any;
}
function TableCard({title, data, dropdownOptions, dropdownOnChange, widgetIndex}: Props): JSX.Element
{
const openArrowIcon = "arrow_drop_down";
const closeArrowIcon = "arrow_drop_up";
const [dropdown, setDropdown] = useState<string | null>(null);
const [dropdownValue, setDropdownValue] = useState<string>("");
const [dropdownIcon, setDropdownIcon] = useState<string>(openArrowIcon);
const openDropdown = ({currentTarget}: any) =>
{
setDropdown(currentTarget);
setDropdownIcon(closeArrowIcon);
};
const closeDropdown = ({currentTarget}: any) =>
{
setDropdown(null);
setDropdownValue(currentTarget.innerText || dropdownValue);
setDropdownIcon(openArrowIcon);
alert(widgetIndex);
dropdownOnChange(currentTarget.innerText || dropdownValue, widgetIndex);
};
const renderMenu = (state: any, open: any, close: any, icon: string) => (
dropdownOptions && (
<span style={{whiteSpace: "nowrap"}}>
<Icon onClick={open} fontSize={"medium"} style={{cursor: "pointer", float: "right"}}>{icon}</Icon>
<Menu
anchorEl={state}
transformOrigin={{vertical: "top", horizontal: "center"}}
open={Boolean(state)}
onClose={close}
keepMounted
disableAutoFocusItem
>
{
dropdownOptions.map((option) =>
<MenuItem key={option} onClick={close}>{option}</MenuItem>
)
}
</Menu>
</span>
)
);
useEffect(() =>
{
console.log(dropdownOptions);
if (dropdownOptions)
{
setDropdownValue(dropdownOptions[0]);
}
}, [dropdownOptions]);
return (
<Card>
<Grid container>
<Grid item xs={7}>
<MDBox pt={3} px={3}>
<MDTypography variant="h6" fontWeight="medium">
{title}
</MDTypography>
</MDBox>
</Grid>
<Grid item xs={5}>
{dropdownOptions && (
<MDBox p={2} width="100%" textAlign="right" lineHeight={1}>
<MDTypography
variant="caption"
color="secondary"
fontWeight="regular"
sx={{cursor: "pointer"}}
onClick={openDropdown}
>
{dropdownValue}
</MDTypography>
{renderMenu(dropdown, openDropdown, closeDropdown, dropdownIcon)}
</MDBox>
)}
</Grid>
</Grid>
<MDBox py={1}>
<DataTable
table={data}
entriesPerPage={false}
showTotalEntries={false}
isSorted={false}
noEndBorder
/>
</MDBox>
</Card>
);
}
export default TableCard;

View File

@ -20,8 +20,8 @@
*/ */
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QSection";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import Avatar from "@mui/material/Avatar"; import Avatar from "@mui/material/Avatar";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
@ -71,10 +71,10 @@ function ViewContents({id, table}: Props): JSX.Element
const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false); const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false);
const [tableMetaData, setTableMetaData] = useState(null); const [tableMetaData, setTableMetaData] = useState(null);
const [record, setRecord] = useState(null as QRecord); const [record, setRecord] = useState(null as QRecord);
const [tableSections, setTableSections] = useState([] as QSection[]); const [tableSections, setTableSections] = useState([] as QTableSection[]);
const [t1SectionName, setT1SectionName] = useState(null as string); const [t1SectionName, setT1SectionName] = useState(null as string);
const [t1SectionElement, setT1SectionElement] = useState(null as JSX.Element); const [t1SectionElement, setT1SectionElement] = useState(null as JSX.Element);
const [nonT1TableSections, setNonT1TableSections] = useState([] as QSection[]); const [nonT1TableSections, setNonT1TableSections] = useState([] as QTableSection[]);
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]); const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
const [actionsMenu, setActionsMenu] = useState(null); const [actionsMenu, setActionsMenu] = useState(null);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();

View File

@ -19,8 +19,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {QSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QSection";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
/******************************************************************************* /*******************************************************************************
** Utility class for working with QQQ Tables ** Utility class for working with QQQ Tables
@ -28,7 +28,7 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QT
*******************************************************************************/ *******************************************************************************/
class QTableUtils class QTableUtils
{ {
public static getSectionsForRecordSidebar(tableMetaData: QTableMetaData): QSection[] public static getSectionsForRecordSidebar(tableMetaData: QTableMetaData): QTableSection[]
{ {
if (tableMetaData.sections) if (tableMetaData.sections)
{ {
@ -36,7 +36,7 @@ class QTableUtils
} }
else else
{ {
return ([new QSection({ return ([new QTableSection({
iconName: "description", label: "All Fields", name: "allFields", fieldNames: [...tableMetaData.fields.keys()], iconName: "description", label: "All Fields", name: "allFields", fieldNames: [...tableMetaData.fields.keys()],
})]); })]);
} }