Updated parts status graph and formatting. BOD-328
This commit is contained in:
@@ -9860,6 +9860,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>nostatus</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>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
|||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
<Col {...colBreakPoints}>
|
||||||
<JobDetailCardsPartsComponent
|
<JobDetailCardsPartsComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
@@ -163,7 +163,7 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
|||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
<Col {...colBreakPoints}>
|
||||||
<JobDetailCardsDamageComponent
|
<JobDetailCardsDamageComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
|
|||||||
@@ -1,141 +1,17 @@
|
|||||||
import React, { useMemo, useState } from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Pie, PieChart, Sector } from "recharts";
|
import PartsStatusPie from "../parts-status-pie/parts-status-pie.component";
|
||||||
import CardTemplate from "./job-detail-cards.template.component";
|
import CardTemplate from "./job-detail-cards.template.component";
|
||||||
|
|
||||||
export default function JobDetailCardsPartsComponent({ loading, data }) {
|
export default function JobDetailCardsPartsComponent({ loading, data }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { joblines_status } = data;
|
const { joblines_status } = data;
|
||||||
// console.log(
|
|
||||||
// "JobDetailCardsPartsComponent -> joblines_stats",
|
|
||||||
// joblines_status
|
|
||||||
// );
|
|
||||||
|
|
||||||
const memoizedData = useMemo(() => Calculatedata(joblines_status), [
|
|
||||||
joblines_status,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [state, setState] = useState({ activeIndex: 0 });
|
|
||||||
|
|
||||||
const onPieEnter = (data, index) => {
|
|
||||||
setState({
|
|
||||||
activeIndex: index,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
|
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
|
||||||
<PieChart width={400} height={400}>
|
<PartsStatusPie joblines_status={joblines_status} />
|
||||||
<Pie
|
|
||||||
activeIndex={state.activeIndex}
|
|
||||||
activeShape={renderActiveShape}
|
|
||||||
data={memoizedData}
|
|
||||||
cx={200}
|
|
||||||
cy={200}
|
|
||||||
innerRadius={60}
|
|
||||||
outerRadius={80}
|
|
||||||
fill="#8884d8"
|
|
||||||
dataKey="value"
|
|
||||||
onMouseEnter={onPieEnter}
|
|
||||||
/>
|
|
||||||
</PieChart>
|
|
||||||
</CardTemplate>
|
</CardTemplate>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Calculatedata = (data) => {
|
|
||||||
if (data.length > 0) {
|
|
||||||
const statusMapping = {};
|
|
||||||
data.map((i) => {
|
|
||||||
if (!statusMapping[i.status])
|
|
||||||
statusMapping[i.status] = { name: i.status || "No Status*", value: 0 };
|
|
||||||
statusMapping[i.status].value = statusMapping[i.status].value + i.count;
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
return Object.keys(statusMapping).map((key) => {
|
|
||||||
return statusMapping[key];
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
{ name: "Group A", value: 400 },
|
|
||||||
{ name: "Group B", value: 300 },
|
|
||||||
{ name: "Group C", value: 300 },
|
|
||||||
{ name: "Group D", value: 200 },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderActiveShape = (props) => {
|
|
||||||
const RADIAN = Math.PI / 180;
|
|
||||||
const {
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
midAngle,
|
|
||||||
innerRadius,
|
|
||||||
outerRadius,
|
|
||||||
startAngle,
|
|
||||||
endAngle,
|
|
||||||
fill,
|
|
||||||
payload,
|
|
||||||
percent,
|
|
||||||
value,
|
|
||||||
} = props;
|
|
||||||
const sin = Math.sin(-RADIAN * midAngle);
|
|
||||||
const cos = Math.cos(-RADIAN * midAngle);
|
|
||||||
const sx = cx + (outerRadius + 10) * cos;
|
|
||||||
const sy = cy + (outerRadius + 10) * sin;
|
|
||||||
const mx = cx + (outerRadius + 30) * cos;
|
|
||||||
const my = cy + (outerRadius + 30) * sin;
|
|
||||||
const ex = mx + (cos >= 0 ? 1 : -1) * 22;
|
|
||||||
const ey = my;
|
|
||||||
const textAnchor = cos >= 0 ? "start" : "end";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<g>
|
|
||||||
<text x={cx} y={cy} dy={8} textAnchor="middle" fill={fill}>
|
|
||||||
{payload.name}
|
|
||||||
</text>
|
|
||||||
<Sector
|
|
||||||
cx={cx}
|
|
||||||
cy={cy}
|
|
||||||
innerRadius={innerRadius}
|
|
||||||
outerRadius={outerRadius}
|
|
||||||
startAngle={startAngle}
|
|
||||||
endAngle={endAngle}
|
|
||||||
fill={fill}
|
|
||||||
/>
|
|
||||||
<Sector
|
|
||||||
cx={cx}
|
|
||||||
cy={cy}
|
|
||||||
startAngle={startAngle}
|
|
||||||
endAngle={endAngle}
|
|
||||||
innerRadius={outerRadius + 6}
|
|
||||||
outerRadius={outerRadius + 10}
|
|
||||||
fill={fill}
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`}
|
|
||||||
stroke={fill}
|
|
||||||
fill="none"
|
|
||||||
/>
|
|
||||||
<circle cx={ex} cy={ey} r={2} fill={fill} stroke="none" />
|
|
||||||
<text
|
|
||||||
x={ex + (cos >= 0 ? 1 : -1) * 12}
|
|
||||||
y={ey}
|
|
||||||
textAnchor={textAnchor}
|
|
||||||
fill="#333"
|
|
||||||
>{`Count: ${value}`}</text>
|
|
||||||
<text
|
|
||||||
x={ex + (cos >= 0 ? 1 : -1) * 12}
|
|
||||||
y={ey}
|
|
||||||
dy={18}
|
|
||||||
textAnchor={textAnchor}
|
|
||||||
fill="#999"
|
|
||||||
>
|
|
||||||
{`(${(percent * 100).toFixed(2)}%)`}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -6,22 +6,15 @@ import CardTemplate from "./job-detail-cards.template.component";
|
|||||||
|
|
||||||
export default function JobDetailCardsTotalsComponent({ loading, data }) {
|
export default function JobDetailCardsTotalsComponent({ loading, data }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
let totals;
|
|
||||||
|
|
||||||
try {
|
|
||||||
totals = JSON.parse(data.job_totals);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error in CardsTotal component", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardTemplate loading={loading} title={t("jobs.labels.cards.totals")}>
|
<CardTemplate loading={loading} title={t("jobs.labels.cards.totals")}>
|
||||||
{totals ? (
|
{data.job_totals ? (
|
||||||
<div className="imex-flex-row imex-flex-row__flex-space-around">
|
<div className="imex-flex-row imex-flex-row__flex-space-around">
|
||||||
<Statistic
|
<Statistic
|
||||||
className="imex-flex-row__margin-large"
|
className="imex-flex-row__margin-large"
|
||||||
title={t("jobs.labels.total_repairs")}
|
title={t("jobs.labels.total_repairs")}
|
||||||
value={Dinero(totals.totals.total_repairs).toFormat()}
|
value={Dinero(data.job_totals.totals.total_repairs).toFormat()}
|
||||||
/>
|
/>
|
||||||
<Statistic
|
<Statistic
|
||||||
className="imex-flex-row__margin-large"
|
className="imex-flex-row__margin-large"
|
||||||
@@ -33,7 +26,7 @@ export default function JobDetailCardsTotalsComponent({ loading, data }) {
|
|||||||
<Statistic
|
<Statistic
|
||||||
className="imex-flex-row__margin-large"
|
className="imex-flex-row__margin-large"
|
||||||
title={t("jobs.labels.net_repairs")}
|
title={t("jobs.labels.net_repairs")}
|
||||||
value={Dinero(totals.totals.net_repairs).toFormat()}
|
value={Dinero(data.job_totals.totals.net_repairs).toFormat()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,25 +1,93 @@
|
|||||||
import React from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
export default function PartsStatusPie({ partsList }) {
|
export function PartsStatusPie({ bodyshop, joblines_status }) {
|
||||||
return <div>Parts Pie</div>;
|
const { t } = useTranslation();
|
||||||
//const [pieData, setPieData] = useState([]);
|
const pieColor = useCallback(
|
||||||
|
(status) => {
|
||||||
|
if (status === bodyshop.md_order_statuses.default_ordered)
|
||||||
|
return "lightgreen";
|
||||||
|
if (status === bodyshop.md_order_statuses.default_bo) return "crimson";
|
||||||
|
|
||||||
// const result = partsList
|
if (status === bodyshop.md_order_statuses.default_canceled)
|
||||||
// ? partsList.reduce((names, name) => {
|
return "dodgerblue";
|
||||||
// const val = name || "?";
|
|
||||||
// const count = names[val] || 0;
|
|
||||||
// names[val] = count + 1;
|
|
||||||
// return names;
|
|
||||||
// }, {})
|
|
||||||
// : {};
|
|
||||||
|
|
||||||
// const pieData = Object.keys(result).map((i) => {
|
if (status === bodyshop.md_order_statuses.default_returned)
|
||||||
// return {
|
return "powderblue";
|
||||||
// id: i,
|
if (status === bodyshop.md_order_statuses.default_received)
|
||||||
// label: i,
|
return "seagreen";
|
||||||
// value: result[i],
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return <div>{JSON.stringify(pieData)}</div>;
|
return "slategray";
|
||||||
|
},
|
||||||
|
[
|
||||||
|
bodyshop.md_order_statuses.default_ordered,
|
||||||
|
bodyshop.md_order_statuses.default_bo,
|
||||||
|
bodyshop.md_order_statuses.default_canceled,
|
||||||
|
bodyshop.md_order_statuses.default_returned,
|
||||||
|
bodyshop.md_order_statuses.default_received,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const Calculatedata = useCallback(
|
||||||
|
(data) => {
|
||||||
|
if (data.length > 0) {
|
||||||
|
const statusMapping = {};
|
||||||
|
data.map((i) => {
|
||||||
|
if (!statusMapping[i.status])
|
||||||
|
statusMapping[i.status] = {
|
||||||
|
name: i.status || t("joblines.labels.nostatus"),
|
||||||
|
value: 0,
|
||||||
|
color: pieColor(i.status),
|
||||||
|
};
|
||||||
|
statusMapping[i.status].value =
|
||||||
|
statusMapping[i.status].value + i.count;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return Object.keys(statusMapping).map((key) => {
|
||||||
|
return statusMapping[key];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[pieColor, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const memoizedData = useMemo(() => Calculatedata(joblines_status), [
|
||||||
|
joblines_status,
|
||||||
|
Calculatedata,
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log("PartsStatusPie -> memoizedData", memoizedData);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ResponsiveContainer width="100%" height={175}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={memoizedData}
|
||||||
|
innerRadius={40}
|
||||||
|
outerRadius={50}
|
||||||
|
fill="#8884d8"
|
||||||
|
paddingAngle={5}
|
||||||
|
dataKey="value"
|
||||||
|
label={(entry) => `${entry.name} - ${entry.value}`}
|
||||||
|
labelLine
|
||||||
|
>
|
||||||
|
{memoizedData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
export default connect(mapStateToProps, null)(PartsStatusPie);
|
||||||
|
|||||||
@@ -635,7 +635,8 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"edit": "Edit Line",
|
"edit": "Edit Line",
|
||||||
"new": "New Line"
|
"new": "New Line",
|
||||||
|
"nostatus": "No Status"
|
||||||
},
|
},
|
||||||
"successes": {
|
"successes": {
|
||||||
"created": "Job line created successfully.",
|
"created": "Job line created successfully.",
|
||||||
|
|||||||
@@ -635,7 +635,8 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"edit": "Línea de edición",
|
"edit": "Línea de edición",
|
||||||
"new": "Nueva línea"
|
"new": "Nueva línea",
|
||||||
|
"nostatus": ""
|
||||||
},
|
},
|
||||||
"successes": {
|
"successes": {
|
||||||
"created": "",
|
"created": "",
|
||||||
|
|||||||
@@ -635,7 +635,8 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"edit": "Ligne d'édition",
|
"edit": "Ligne d'édition",
|
||||||
"new": "Nouvelle ligne"
|
"new": "Nouvelle ligne",
|
||||||
|
"nostatus": ""
|
||||||
},
|
},
|
||||||
"successes": {
|
"successes": {
|
||||||
"created": "",
|
"created": "",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
- args:
|
||||||
|
cascade: true
|
||||||
|
read_only: false
|
||||||
|
sql: "CREATE OR REPLACE VIEW \"public\".\"joblines_status\" AS \n SELECT j.jobid,\n
|
||||||
|
\ j.status,\n count(1) AS count,\n j.part_type\n FROM joblines j\n
|
||||||
|
\ where j.part_type is not null\n GROUP BY j.jobid, j.status, j.part_type\n
|
||||||
|
\ ;"
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
- args:
|
||||||
|
cascade: true
|
||||||
|
read_only: false
|
||||||
|
sql: "CREATE OR REPLACE VIEW \"public\".\"joblines_status\" AS \n SELECT j.jobid,\n
|
||||||
|
\ j.status,\n count(1) AS count,\n j.part_type\n FROM joblines j\n
|
||||||
|
\ where j.part_type is not null\n GROUP BY j.jobid, j.status, j.part_type\n
|
||||||
|
\ ;"
|
||||||
|
type: run_sql
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
- args:
|
||||||
|
cascade: true
|
||||||
|
read_only: false
|
||||||
|
sql: "CREATE OR REPLACE VIEW \"public\".\"joblines_status\" AS \n SELECT j.jobid,\n
|
||||||
|
\ j.status,\n count(1) AS count,\n j.part_type\n FROM joblines j\n
|
||||||
|
\ where j.part_type is not null and j.part_type <> 'PAE'\n GROUP BY j.jobid,
|
||||||
|
j.status, j.part_type\n ;"
|
||||||
|
type: run_sql
|
||||||
Reference in New Issue
Block a user