Added dashboard framework. Components are not yet created nor is the query finalized. BOD-79
Missed in previous commit. BOD-79
This commit is contained in:
@@ -4684,6 +4684,152 @@
|
|||||||
</folder_node>
|
</folder_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>dashboard</name>
|
||||||
|
<children>
|
||||||
|
<folder_node>
|
||||||
|
<name>actions</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>addcomponent</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>errors</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>updatinglayout</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>titles</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>monthlyrevenuegraph</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>productiondollars</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>productionhours</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>projectedmonthlysales</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>documents</name>
|
<name>documents</name>
|
||||||
<children>
|
<children>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import axios from "axios";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { auth } from "../../firebase/firebase.utils";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -97,6 +97,13 @@ export default connect(
|
|||||||
<button onClick={handle}>Hit with Header.</button>
|
<button onClick={handle}>Hit with Header.</button>
|
||||||
<button onClick={handleLocal}>Hit Localhost with Header.</button>
|
<button onClick={handleLocal}>Hit Localhost with Header.</button>
|
||||||
<button onClick={handleQbxml}>Qbxml</button>
|
<button onClick={handleQbxml}>Qbxml</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
logImEXEvent("IMEXEVENT", { somethignArThare: 5 });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Log an ImEX Event.
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { Card } from "antd";
|
||||||
|
import moment from "moment";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Area,
|
||||||
|
Bar,
|
||||||
|
CartesianGrid,
|
||||||
|
ComposedChart,
|
||||||
|
Legend,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis
|
||||||
|
} from "recharts";
|
||||||
|
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||||
|
|
||||||
|
export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const jobsByDate = {
|
||||||
|
"2020-07-5": [{ clm_total: 1224 }],
|
||||||
|
"2020-07-8": [{ clm_total: 987 }, { clm_total: 8755 }],
|
||||||
|
"2020-07-12": [{ clm_total: 684 }, { clm_total: 12022 }],
|
||||||
|
"2020-07-21": [{ clm_total: 15000 }],
|
||||||
|
"2020-07-28": [{ clm_total: 122 }, { clm_total: 4522 }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const listOfDays = Utils.ListOfDaysInCurrentMonth();
|
||||||
|
|
||||||
|
const chartData = listOfDays.reduce((acc, val) => {
|
||||||
|
//Sum up the current day.
|
||||||
|
let dailySales;
|
||||||
|
if (!!jobsByDate[val]) {
|
||||||
|
dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => {
|
||||||
|
return dayAcc + dayVal.clm_total;
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
dailySales = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const theValue = {
|
||||||
|
date: moment(val).format("D dd"),
|
||||||
|
dailySales,
|
||||||
|
accSales:
|
||||||
|
acc.length > 0 ? acc[acc.length - 1].accSales + dailySales : dailySales,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [...acc, theValue];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={t("dashboard.titles.monthlyrevenuegraph")} {...cardProps}>
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<ComposedChart
|
||||||
|
data={chartData}
|
||||||
|
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid stroke="#f5f5f5" />
|
||||||
|
<XAxis dataKey="date" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
name="Accumulated Sales"
|
||||||
|
dataKey="accSales"
|
||||||
|
fill="#8884d8"
|
||||||
|
stroke="#8884d8"
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
name="Daily Sales"
|
||||||
|
dataKey="dailySales"
|
||||||
|
//stackId="day"
|
||||||
|
barSize={20}
|
||||||
|
fill="#413ea0"
|
||||||
|
/>
|
||||||
|
</ComposedChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
|
||||||
|
import { Card, Statistic } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const aboveTargetMonthlySales = false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card {...cardProps}>
|
||||||
|
<Statistic
|
||||||
|
title={t("dashboard.titles.projectedmonthlysales")}
|
||||||
|
value={222000.0}
|
||||||
|
precision={2}
|
||||||
|
prefix={
|
||||||
|
<div>
|
||||||
|
{aboveTargetMonthlySales ? (
|
||||||
|
<ArrowUpOutlined />
|
||||||
|
) : (
|
||||||
|
<ArrowDownOutlined />
|
||||||
|
)}
|
||||||
|
$
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
valueStyle={{ color: aboveTargetMonthlySales ? "green" : "red" }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card, Statistic } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
export default function DashboardTotalProductionDollars({
|
||||||
|
data,
|
||||||
|
...cardProps
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const aboveTargetProductionDollars = false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card {...cardProps}>
|
||||||
|
<Statistic
|
||||||
|
title={t("dashboard.titles.productiondollars")}
|
||||||
|
value={175000.0}
|
||||||
|
precision={2}
|
||||||
|
prefix={
|
||||||
|
<div>
|
||||||
|
{aboveTargetProductionDollars ? (
|
||||||
|
<ArrowUpOutlined />
|
||||||
|
) : (
|
||||||
|
<ArrowDownOutlined />
|
||||||
|
)}
|
||||||
|
$
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
valueStyle={{ color: aboveTargetProductionDollars ? "green" : "red" }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card, Statistic } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
export default function DashboardTotalProductionHours({ data, ...cardProps }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const aboveTargetHours = true;
|
||||||
|
return (
|
||||||
|
<Card {...cardProps}>
|
||||||
|
<Statistic
|
||||||
|
title={t("dashboard.titles.productionhours")}
|
||||||
|
value={750}
|
||||||
|
prefix={aboveTargetHours ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
|
||||||
|
valueStyle={{ color: aboveTargetHours ? "green" : "red" }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,64 +1,176 @@
|
|||||||
import { Card } from "antd";
|
import Icon from "@ant-design/icons";
|
||||||
|
import { Button, Dropdown, Menu, notification } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { useMutation, useQuery } from "react-apollo";
|
||||||
import { Responsive, WidthProvider } from "react-grid-layout";
|
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||||
import styled from "styled-components";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { MdClose } from "react-icons/md";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
|
||||||
|
import { QUERY_DASHBOARD_DETAILS } from "../../graphql/bodyshop.queries";
|
||||||
|
import {
|
||||||
|
selectBodyshop,
|
||||||
|
selectCurrentUser,
|
||||||
|
} from "../../redux/user/user.selectors";
|
||||||
|
import DashboardMonthlyRevenueGraph from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
|
||||||
|
import DashboardProjectedMonthlySales from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
|
||||||
|
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
|
||||||
|
import DashboardTotalProductionHours from "../dashboard-components/total-production-hours/total-production-hours.component";
|
||||||
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
//Combination of the following:
|
//Combination of the following:
|
||||||
// /node_modules/react-grid-layout/css/styles.css
|
// /node_modules/react-grid-layout/css/styles.css
|
||||||
// /node_modules/react-resizable/css/styles.css
|
// /node_modules/react-resizable/css/styles.css
|
||||||
import "./dashboard-grid.styles.css";
|
import "./dashboard-grid.styles.css";
|
||||||
|
import "./dashboard-grid.styles.scss";
|
||||||
const Sdiv = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
height: 80%;
|
|
||||||
width: 80%;
|
|
||||||
top: 10%;
|
|
||||||
left: 10%;
|
|
||||||
// background-color: #ffcc00;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
||||||
|
|
||||||
export default function DashboardGridComponent() {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
currentUser: selectCurrentUser,
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||||
|
const { loading, error, data } = useQuery(QUERY_DASHBOARD_DETAILS);
|
||||||
|
const { t } = useTranslation();
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
layout: [
|
layout: bodyshop.associations[0].user.dashboardlayout || [
|
||||||
{ i: "1", x: 0, y: 0, w: 2, h: 2 },
|
{ i: "ProductionDollars", x: 0, y: 0, w: 2, h: 2 },
|
||||||
{ i: "2", x: 2, y: 0, w: 2, h: 2 },
|
// { i: "ProductionHours", x: 2, y: 0, w: 2, h: 2 },
|
||||||
{ i: "3", x: 4, y: 0, w: 2, h: 2 }
|
],
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
|
||||||
|
|
||||||
const defaultProps = {
|
const handleLayoutChange = async (newLayout) => {
|
||||||
className: "layout",
|
setState({ ...state, layout: newLayout });
|
||||||
breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }
|
const result = await updateLayout({
|
||||||
// cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 },
|
variables: { email: currentUser.email, layout: newLayout },
|
||||||
// rowHeight: 100
|
});
|
||||||
|
|
||||||
|
if (!!result.errors) {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("dashboard.errors.updatinglayout", {
|
||||||
|
message: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveComponent = (key) => {
|
||||||
|
const idxToRemove = state.layout.findIndex((i) => i.i === key);
|
||||||
|
const newLayout = state.layout;
|
||||||
|
newLayout.splice(idxToRemove, 1);
|
||||||
|
console.log(newLayout);
|
||||||
|
handleLayoutChange(newLayout);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddComponent = (e) => {
|
||||||
|
handleLayoutChange([
|
||||||
|
...state.layout,
|
||||||
|
{
|
||||||
|
i: e.key,
|
||||||
|
x: (state.layout.length * 2) % (state.cols || 12),
|
||||||
|
y: Infinity, // puts it at the bottom
|
||||||
|
w: componentList[e.key].w || 2,
|
||||||
|
h: componentList[e.key].h || 2,
|
||||||
|
},
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// We're using the cols coming back from this to calculate where to add new items.
|
|
||||||
const onBreakpointChange = (breakpoint, cols) => {
|
const onBreakpointChange = (breakpoint, cols) => {
|
||||||
console.log("breakpoint, cols", breakpoint, cols);
|
setState({ ...state, breakpoint: breakpoint, cols: cols });
|
||||||
// setState({ ...state, breakpoint: breakpoint, cols: cols });
|
|
||||||
};
|
};
|
||||||
if (true) return null;
|
|
||||||
|
const existingLayoutKeys = state.layout.map((i) => i.i);
|
||||||
|
const addComponentOverlay = (
|
||||||
|
<Menu onClick={handleAddComponent}>
|
||||||
|
{Object.keys(componentList).map((key) => (
|
||||||
|
<Menu.Item
|
||||||
|
key={key}
|
||||||
|
value={key}
|
||||||
|
disabled={existingLayoutKeys.includes(key)}
|
||||||
|
>
|
||||||
|
{componentList[key].label}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sdiv>
|
<div>
|
||||||
The Grid.
|
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
|
||||||
|
<Button>{t("dashboard.actions.addcomponent")}</Button>
|
||||||
|
</Dropdown>
|
||||||
<ResponsiveReactGridLayout
|
<ResponsiveReactGridLayout
|
||||||
{...defaultProps}
|
className="layout"
|
||||||
|
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||||
|
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
||||||
|
width="100%"
|
||||||
|
onLayoutChange={handleLayoutChange}
|
||||||
onBreakpointChange={onBreakpointChange}
|
onBreakpointChange={onBreakpointChange}
|
||||||
width='100%'
|
>
|
||||||
onLayoutChange={layout => {
|
|
||||||
console.log("layout", layout);
|
|
||||||
setState({ ...state, layout });
|
|
||||||
}}>
|
|
||||||
{state.layout.map((item, index) => {
|
{state.layout.map((item, index) => {
|
||||||
|
const TheComponent = componentList[item.i].component;
|
||||||
return (
|
return (
|
||||||
<Card style={{ width: "100px" }} key={item.i} data-grid={item}>
|
<div key={item.i} data-grid={item}>
|
||||||
A Card {index}
|
<LoadingSkeleton loading={loading}>
|
||||||
</Card>
|
<Icon
|
||||||
|
component={MdClose}
|
||||||
|
key={item.i}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: "2",
|
||||||
|
right: ".25rem",
|
||||||
|
top: ".25rem",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => handleRemoveComponent(item.i)}
|
||||||
|
/>
|
||||||
|
<TheComponent
|
||||||
|
className="dashboard-card"
|
||||||
|
size="small"
|
||||||
|
style={{ height: "100%", width: "100%" }}
|
||||||
|
/>
|
||||||
|
</LoadingSkeleton>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ResponsiveReactGridLayout>
|
</ResponsiveReactGridLayout>
|
||||||
</Sdiv>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(DashboardGridComponent);
|
||||||
|
|
||||||
|
const componentList = {
|
||||||
|
ProductionDollars: {
|
||||||
|
label: "Production Dollars",
|
||||||
|
component: DashboardTotalProductionDollars,
|
||||||
|
w: 2,
|
||||||
|
h: 1,
|
||||||
|
},
|
||||||
|
ProductionHours: {
|
||||||
|
label: "Production Hours",
|
||||||
|
component: DashboardTotalProductionHours,
|
||||||
|
w: 2,
|
||||||
|
h: 1,
|
||||||
|
},
|
||||||
|
ProjectedMonthlySales: {
|
||||||
|
label: "Projected Monthly Sales",
|
||||||
|
component: DashboardProjectedMonthlySales,
|
||||||
|
w: 2,
|
||||||
|
h: 1,
|
||||||
|
},
|
||||||
|
MonthlyRevenueGraph: {
|
||||||
|
label: "Monthly Sales Graph",
|
||||||
|
component: DashboardMonthlyRevenueGraph,
|
||||||
|
w: 2,
|
||||||
|
h: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -124,3 +124,5 @@
|
|||||||
.react-resizable-hide > .react-resizable-handle {
|
.react-resizable-hide > .react-resizable-handle {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.dashboard-card {
|
||||||
|
// background-color: green;
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
// background-color: red;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,39 +8,45 @@ class ErrorBoundary extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
hasErrored: false,
|
hasErrored: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
info: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromError(error) {
|
static getDerivedStateFromError(error) {
|
||||||
return { hasErrored: true, error };
|
console.log("ErrorBoundary -> getDerivedStateFromError -> error", error);
|
||||||
|
return { hasErrored: true, error: error };
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error, info) {
|
componentDidCatch(error, info) {
|
||||||
console.log("Exception Caught by Error Boundary.", error, info);
|
console.log("Exception Caught by Error Boundary.", error, info);
|
||||||
|
this.setState({ ...this.state, error, info });
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
console.log("this.state", this.state);
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
if (this.state.hasErrored === true) {
|
if (this.state.hasErrored === true) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Result
|
<Result
|
||||||
status='500'
|
status="500"
|
||||||
title={t("general.labels.exceptiontitle")}
|
title={t("general.labels.exceptiontitle")}
|
||||||
subTitle={t("general.messages.exception")}
|
subTitle={t("general.messages.exception")}
|
||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
type='primary'
|
type="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{t("general.actions.refresh")}
|
{t("general.actions.refresh")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
alert("Not implemented yet.");
|
alert("Not implemented yet.");
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{t("general.actions.submitticket")}
|
{t("general.actions.submitticket")}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -50,7 +56,10 @@ class ErrorBoundary extends React.Component {
|
|||||||
<Col offset={6} span={12}>
|
<Col offset={6} span={12}>
|
||||||
<Collapse bordered={false}>
|
<Collapse bordered={false}>
|
||||||
<Collapse.Panel header={t("general.labels.errors")}>
|
<Collapse.Panel header={t("general.labels.errors")}>
|
||||||
{JSON.stringify(this.state.error || "")}
|
<div>
|
||||||
|
<strong>{this.state.error.message}</strong>
|
||||||
|
</div>
|
||||||
|
<div>{this.state.error.stack}</div>
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -4,5 +4,9 @@ import "./loading-skeleton.styles.scss";
|
|||||||
import { Skeleton } from "antd";
|
import { Skeleton } from "antd";
|
||||||
|
|
||||||
export default function LoadingSkeleton(props) {
|
export default function LoadingSkeleton(props) {
|
||||||
return <Skeleton {...props} className='loading-skeleton' active />;
|
return (
|
||||||
|
<Skeleton {...props} className="loading-skeleton" active>
|
||||||
|
{props.children}
|
||||||
|
</Skeleton>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,33 @@
|
|||||||
|
import moment from "moment";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
import {
|
import {
|
||||||
ComposedChart,
|
|
||||||
Line,
|
|
||||||
Area,
|
Area,
|
||||||
Bar,
|
Bar,
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
CartesianGrid,
|
CartesianGrid, ComposedChart,
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
ResponsiveContainer,
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Legend, Line,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ResponsiveContainer, Tooltip, XAxis,
|
||||||
|
YAxis
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
|
|
||||||
import moment from "moment";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import gql from "graphql-tag";
|
|||||||
export const QUERY_BODYSHOP = gql`
|
export const QUERY_BODYSHOP = gql`
|
||||||
query QUERY_BODYSHOP {
|
query QUERY_BODYSHOP {
|
||||||
bodyshops(where: { associations: { active: { _eq: true } } }) {
|
bodyshops(where: { associations: { active: { _eq: true } } }) {
|
||||||
|
associations {
|
||||||
|
user {
|
||||||
|
authid
|
||||||
|
email
|
||||||
|
dashboardlayout
|
||||||
|
}
|
||||||
|
}
|
||||||
address1
|
address1
|
||||||
address2
|
address2
|
||||||
city
|
city
|
||||||
@@ -114,3 +121,36 @@ export const QUERY_STRIPE_ID = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const QUERY_DASHBOARD_DETAILS = gql`
|
||||||
|
query QUERY_DASHBOARD_DETAILS {
|
||||||
|
query
|
||||||
|
QUERY_DASHBOARD_DETAILS {
|
||||||
|
jobs {
|
||||||
|
id
|
||||||
|
clm_total
|
||||||
|
scheduled_completion
|
||||||
|
date_invoiced
|
||||||
|
ins_co_nm
|
||||||
|
}
|
||||||
|
compJobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||||
|
id
|
||||||
|
scheduled_completion
|
||||||
|
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAB" } }) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
mod_lb_hrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" } }) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
mod_lb_hrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -828,6 +828,8 @@ export const QUERY_ALL_JOBS_PAGINATED = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const QUERY_JOB_CLOSE_DETAILS = gql`
|
export const QUERY_JOB_CLOSE_DETAILS = gql`
|
||||||
query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
|
query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
|
||||||
jobs_by_pk(id: $id) {
|
jobs_by_pk(id: $id) {
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ export const UPSERT_USER = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const UPDATE_DASHBOARD_LAYOUT = gql`
|
||||||
|
mutation UPDATE_DASHBOARD_LAYOUT($email: String!, $layout: jsonb!) {
|
||||||
|
update_users_by_pk(
|
||||||
|
pk_columns: { email: $email }
|
||||||
|
_set: { dashboardlayout: $layout }
|
||||||
|
) {
|
||||||
|
email
|
||||||
|
dashboardlayout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const UPDATE_FCM_TOKEN = gql`
|
export const UPDATE_FCM_TOKEN = gql`
|
||||||
mutation UPDATE_FCM_TOKEN($authEmail: String!, $token: jsonb!) {
|
mutation UPDATE_FCM_TOKEN($authEmail: String!, $token: jsonb!) {
|
||||||
update_users(
|
update_users(
|
||||||
|
|||||||
@@ -1,34 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import DashboardGridComponent from "../../components/dashboard-grid/dashboard-grid.component";
|
import DashboardGridComponent from "../../components/dashboard-grid/dashboard-grid.component";
|
||||||
import Test from "../../components/_test/test.component";
|
|
||||||
import { analytics, logImEXEvent } from "../../firebase/firebase.utils";
|
|
||||||
|
|
||||||
export default function ManageRootPageComponent() {
|
export default function ManageRootPageComponent() {
|
||||||
//const client = useApolloClient();
|
//const client = useApolloClient();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Test />
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
logImEXEvent("IMEXEVENT", { somethignArThare: 5 });
|
|
||||||
}}>
|
|
||||||
LogImEXEvent
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
console.log("Things.");
|
|
||||||
analytics.logEvent("start_game", {
|
|
||||||
level: "10",
|
|
||||||
difficulty: "expert",
|
|
||||||
});
|
|
||||||
analytics.logEvent("select_content", {
|
|
||||||
content_type: "image",
|
|
||||||
content_id: "P12453",
|
|
||||||
items: [{ name: "Kittens" }],
|
|
||||||
});
|
|
||||||
}}>
|
|
||||||
Click me to start an event
|
|
||||||
</button>
|
|
||||||
<DashboardGridComponent />
|
<DashboardGridComponent />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import ManageRootPageComponent from "./manage-root.page.component";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { setBreadcrumbs } from "../../redux/application/application.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import ManageRootPageComponent from "./manage-root.page.component";
|
||||||
|
|
||||||
export default function ManageRootPageContainer() {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function ManageRootPageContainer({ setBreadcrumbs, bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = t("titles.manageroot");
|
document.title = t("titles.manageroot");
|
||||||
}, [t]);
|
setBreadcrumbs([]);
|
||||||
|
}, [t, setBreadcrumbs]);
|
||||||
|
|
||||||
return <ManageRootPageComponent />;
|
return <ManageRootPageComponent />;
|
||||||
}
|
}
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(ManageRootPageContainer);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import PrintCenterModalContainer from "../../components/print-center-modal/print
|
|||||||
import { QUERY_STRIPE_ID } from "../../graphql/bodyshop.queries";
|
import { QUERY_STRIPE_ID } from "../../graphql/bodyshop.queries";
|
||||||
import { selectInstanceConflict } from "../../redux/user/user.selectors";
|
import { selectInstanceConflict } from "../../redux/user/user.selectors";
|
||||||
import "./manage.page.styles.scss";
|
import "./manage.page.styles.scss";
|
||||||
|
import TestComponent from "../../components/_test/test.component";
|
||||||
|
|
||||||
const ManageRootPage = lazy(() =>
|
const ManageRootPage = lazy(() =>
|
||||||
import("../manage-root/manage-root.page.container")
|
import("../manage-root/manage-root.page.container")
|
||||||
@@ -167,6 +168,11 @@ export function Manage({ match, conflict }) {
|
|||||||
<PaymentModalContainer />
|
<PaymentModalContainer />
|
||||||
</Elements>
|
</Elements>
|
||||||
<Route exact path={`${match.path}`} component={ManageRootPage} />
|
<Route exact path={`${match.path}`} component={ManageRootPage} />
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={`${match.path}/ttt`}
|
||||||
|
component={TestComponent}
|
||||||
|
/>
|
||||||
<Route exact path={`${match.path}/jobs`} component={JobsPage} />
|
<Route exact path={`${match.path}/jobs`} component={JobsPage} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -305,6 +305,20 @@
|
|||||||
"submittedsub": "Your input is highly appreciated."
|
"submittedsub": "Your input is highly appreciated."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"actions": {
|
||||||
|
"addcomponent": "Add Component"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"updatinglayout": "Error saving updated layout {{message}}"
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"monthlyrevenuegraph": "Monthly Revenue Graph",
|
||||||
|
"productiondollars": "Total dollars in production",
|
||||||
|
"productionhours": "Total hours in production",
|
||||||
|
"projectedmonthlysales": "Projected Monthly Sales"
|
||||||
|
}
|
||||||
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"delete": "Delete Selected Documents",
|
"delete": "Delete Selected Documents",
|
||||||
|
|||||||
@@ -305,6 +305,20 @@
|
|||||||
"submittedsub": ""
|
"submittedsub": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"actions": {
|
||||||
|
"addcomponent": ""
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"updatinglayout": ""
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"monthlyrevenuegraph": "",
|
||||||
|
"productiondollars": "",
|
||||||
|
"productionhours": "",
|
||||||
|
"projectedmonthlysales": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"delete": "",
|
"delete": "",
|
||||||
|
|||||||
@@ -305,6 +305,20 @@
|
|||||||
"submittedsub": ""
|
"submittedsub": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"actions": {
|
||||||
|
"addcomponent": ""
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"updatinglayout": ""
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"monthlyrevenuegraph": "",
|
||||||
|
"productiondollars": "",
|
||||||
|
"productionhours": "",
|
||||||
|
"projectedmonthlysales": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"delete": "",
|
"delete": "",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."users" DROP COLUMN "dashboardlayout";
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."users" ADD COLUMN "dashboardlayout" jsonb NOT NULL
|
||||||
|
DEFAULT jsonb_build_array();
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
- args:
|
||||||
|
role: user
|
||||||
|
table:
|
||||||
|
name: users
|
||||||
|
schema: public
|
||||||
|
type: drop_select_permission
|
||||||
|
- args:
|
||||||
|
permission:
|
||||||
|
allow_aggregations: false
|
||||||
|
columns:
|
||||||
|
- authid
|
||||||
|
- created_at
|
||||||
|
- email
|
||||||
|
- fcmtokens
|
||||||
|
- updated_at
|
||||||
|
computed_fields: []
|
||||||
|
filter:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
role: user
|
||||||
|
table:
|
||||||
|
name: users
|
||||||
|
schema: public
|
||||||
|
type: create_select_permission
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
- args:
|
||||||
|
role: user
|
||||||
|
table:
|
||||||
|
name: users
|
||||||
|
schema: public
|
||||||
|
type: drop_select_permission
|
||||||
|
- args:
|
||||||
|
permission:
|
||||||
|
allow_aggregations: false
|
||||||
|
columns:
|
||||||
|
- authid
|
||||||
|
- created_at
|
||||||
|
- dashboardlayout
|
||||||
|
- email
|
||||||
|
- fcmtokens
|
||||||
|
- updated_at
|
||||||
|
computed_fields: []
|
||||||
|
filter:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
role: user
|
||||||
|
table:
|
||||||
|
name: users
|
||||||
|
schema: public
|
||||||
|
type: create_select_permission
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
- args:
|
||||||
|
role: user
|
||||||
|
table:
|
||||||
|
name: users
|
||||||
|
schema: public
|
||||||
|
type: drop_update_permission
|
||||||
|
- args:
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- authid
|
||||||
|
- email
|
||||||
|
- fcmtokens
|
||||||
|
filter:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
set: {}
|
||||||
|
role: user
|
||||||
|
table:
|
||||||
|
name: users
|
||||||
|
schema: public
|
||||||
|
type: create_update_permission
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
- args:
|
||||||
|
role: user
|
||||||
|
table:
|
||||||
|
name: users
|
||||||
|
schema: public
|
||||||
|
type: drop_update_permission
|
||||||
|
- args:
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- authid
|
||||||
|
- dashboardlayout
|
||||||
|
- email
|
||||||
|
- fcmtokens
|
||||||
|
filter:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
set: {}
|
||||||
|
role: user
|
||||||
|
table:
|
||||||
|
name: users
|
||||||
|
schema: public
|
||||||
|
type: create_update_permission
|
||||||
@@ -3684,6 +3684,7 @@ tables:
|
|||||||
columns:
|
columns:
|
||||||
- authid
|
- authid
|
||||||
- created_at
|
- created_at
|
||||||
|
- dashboardlayout
|
||||||
- email
|
- email
|
||||||
- fcmtokens
|
- fcmtokens
|
||||||
- updated_at
|
- updated_at
|
||||||
@@ -3695,6 +3696,7 @@ tables:
|
|||||||
permission:
|
permission:
|
||||||
columns:
|
columns:
|
||||||
- authid
|
- authid
|
||||||
|
- dashboardlayout
|
||||||
- email
|
- email
|
||||||
- fcmtokens
|
- fcmtokens
|
||||||
filter:
|
filter:
|
||||||
|
|||||||
Reference in New Issue
Block a user