added labor allocationtable

This commit is contained in:
jfrye122
2023-05-14 11:59:34 -04:00
parent 66fa8196af
commit 6d852453b0
8 changed files with 591 additions and 45 deletions

View File

@@ -77,12 +77,13 @@ export default connect(null, null)(CostCenterSelect);
const styles = StyleSheet.create({
container: {
padding: 16,
marginVertical: 4,
marginHorizontal: 16,
justifyContent: "center",
alignContent: "center",
},
dropdown: {
height: 50,
height: 48,
borderColor: "gray",
borderWidth: 0.5,
borderRadius: 8,

View File

@@ -1,23 +1,183 @@
import React from "react";
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { FlatList, RefreshControl, StyleSheet, Text, View } from "react-native";
import _ from "lodash";
import { Card, DataTable } from "react-native-paper";
import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs.queries";
import ErrorDisplay from "../error-display/error-display.component";
import { useQuery } from "@apollo/client";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectCurrentEmployee } from "../../redux/employee/employee.selectors";
import { CalculateAllocationsTotals } from "../../util/labor-allocations-table.utility";
export default function LaborAllocationsTable({ job, loading, refetch }) {
const { t } = useTranslation();
if (!job) {
<Card>
<Text>Job is not defined.</Text>
</Card>;
}
const onRefresh = async () => {
return refetch();
};
return (
<View style={{ flex: 1 }}>
<DataTable>
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectCurrentEmployee,
});
export function LaborAllocationsTable({ jobId, bodyshop, technician }) {
console.log("LaborAllocationsTable, jobId", jobId);
//, loading, refetch
//const jobid = jobid !== undefined ? jobid : "";
const { t } = useTranslation();
const onRefresh = async () => {
console.log("LaborAllocationsTable refetch");
return refetch();
};
//maybe use this
const { loading, error, data, refetch } = useQuery(GET_LINE_TICKET_BY_PK, {
variables: { id: jobId },
skip: !!!jobId,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
// console.log("LaborAllocationsTable, data", data);
if (error) return <ErrorDisplay errorMessage={error.message} />;
// let joblines = [];
// let timetickets = [];
// let adjustments = [];
const [totals, setTotals] = useState([]);
const [state, setState] = useState({
sortedInfo: {
columnKey: "cost_center",
field: "cost_center",
order: "ascend",
},
filteredInfo: {},
});
useEffect(() => {
console.log("LaborAllocationsTable useEffect on data change");
// joblines = data?.joblines ? data.joblines : [];
// timetickets = data?.timetickets ? data.timetickets : [];
// adjustments = data?.adjustments ? data.adjustments : [];
}, [data]);
useEffect(() => {
console.log(
"LaborAllocationsTable useEffect on [joblines, timetickets, bodyshop, adjustments, jobId] change",
data?.joblines,
data?.adjustments
);
if (!!data?.joblines && !!data?.timetickets && !!bodyshop)
setTotals(
CalculateAllocationsTotals(
bodyshop,
data?.joblines,
data?.timetickets,
data?.adjustments
)
);
if (!jobId) setTotals([]);
}, [data?.joblines, data?.timetickets, bodyshop, data?.adjustments, jobId]);
// const convertedLines = useMemo(
// () => data?.joblines && data?.joblines.filter((j) => j.convertedtolbr),
// [data?.joblines]
// );
const summary =
totals &&
totals.reduce(
(acc, val) => {
acc.hrs_total += val.total;
acc.hrs_claimed += val.claimed;
acc.adjustments += val.adjustments;
acc.difference += val.difference;
return acc;
},
{ hrs_total: 0, hrs_claimed: 0, adjustments: 0, difference: 0 }
);
console.log("labor summary is:", summary);
return (
<View style={{ flexGrow: 1 }}>
{typeof data !== "undefined" ? (
<View style={{ flexGrow: 1 }}>
<DataTable>
<DataTable.Header>
<DataTable.Title style={{ flex: 2 }}>Cost Center</DataTable.Title>
<DataTable.Title numeric style={{ flex: 2 }}>
Hours Total
</DataTable.Title>
<DataTable.Title numeric style={{ flex: 2 }}>
Hours Claimed
</DataTable.Title>
<DataTable.Title numeric style={{ flex: 2 }}>
Adjustments
</DataTable.Title>
<DataTable.Title numeric style={{ flex: 2 }}>
Difference
</DataTable.Title>
</DataTable.Header>
</DataTable>
<DataTable>
<FlatList
data={totals}
refreshControl={
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
}
keyExtractor={(item) => item.cost_center}
renderItem={(object) => (
<DataTable.Row>
<DataTable.Cell style={{ flex: 2 }}>
<Text>
{object.item.cost_center}
{object.item.mod_lbr_ty}
</Text>
</DataTable.Cell>
<DataTable.Cell style={{ flex: 2 }}>
<Text>{object.item.total && object.item.total.toFixed(1)}</Text>
</DataTable.Cell>
<DataTable.Cell style={{ flex: 2 }}>
<Text>{object.item.claimed && object.item.claimed.toFixed(1)}</Text>
</DataTable.Cell>
<DataTable.Cell style={{ flex: 2 }}>
<Text>
{object.item.adjustments && object.item.adjustments.toFixed(1)}
{/* {!technician && (
<LaborAllocationsAdjustmentEdit
jobId={jobId}
adjustments={adjustments}
mod_lbr_ty={record.opcode}
refetchQueryNames={["GET_LINE_TICKET_BY_PK"]}
>
<EditFilled />
</LaborAllocationsAdjustmentEdit>
)} */}
</Text>
</DataTable.Cell>
<DataTable.Cell style={{ flex: 2 }}>
<Text style={{
color: object.item.difference >= 0 ? "green" : "red",
}}>
{/* <strong
style={{
color: object.difference >= 0 ? "green" : "red",
}}
>
{_.round(object.difference, 1)}
</strong> */}
{_.round(object.item.difference, 1)}
</Text>
</DataTable.Cell>
</DataTable.Row>
)}
/>
</DataTable>
</View>
) : null}
{/* <DataTable>
<DataTable.Header>
<DataTable.Title style={{ flex: 4 }}>
{t("jobdetail.labels.lines_desc")}
@@ -35,9 +195,9 @@ export default function LaborAllocationsTable({ job, loading, refetch }) {
{t("jobdetail.labels.lines_qty")}
</DataTable.Title>
</DataTable.Header>
</DataTable>
<FlatList
</DataTable> */}
{/* <FlatList
data={job.joblines}
refreshControl={
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
@@ -64,10 +224,13 @@ export default function LaborAllocationsTable({ job, loading, refetch }) {
</DataTable.Cell>
</DataTable.Row>
)}
/>
</View>
);
}
const localStyles = StyleSheet.create({});
/> */}
{/* use "totals" for the rows in the table */}
{/* use "summary" for the totals at the bottom */}
</View>
);
}
const localStyles = StyleSheet.create({});
export default connect(mapStateToProps, null)(LaborAllocationsTable);

View File

@@ -14,6 +14,8 @@ import {
} from "../../redux/employee/employee.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useCallback } from "react";
// import LaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.component";
import LaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.component";
//TODO add props needed for call
const mapStateToProps = createStructuredSelector({
@@ -34,15 +36,15 @@ export function TimeTicketCreate({
const [date2, setDate2] = useState(new Date());
const [currentSCC, setCurrentSCC] = useState(null);
const [currentSJob, setCurrentSJob] = useState(null);
// const [currentSJob, setCurrentSJob] = useState(null);
const [currentSJobId, setCurrentSJobId] = useState(null);
const wrapperSetCurrentSJobState = useCallback(
(val) => {
setCurrentSJob(val);
},
[setCurrentSJob]
);
// const wrapperSetCurrentSJobState = useCallback(
// (val) => {
// setCurrentSJob(val);
// },
// [setCurrentSJob]
// );
const showDatePicker = () => {
setDatePickerVisibility(true);
@@ -56,19 +58,20 @@ export function TimeTicketCreate({
hideDatePicker();
};
const formSubmit = (values) => {
Dialog.alert({ content: <pre>{JSON.stringify(values, null, 2)}</pre> });
console.log("values", values);
//Dialog.alert({ content: <pre>{JSON.stringify(values, null, 2)}</pre> });
//TODO update with start call for create time ticket
};
return (
<View style={localStyles.content}>
<ScrollView>
<ScrollView contentContainerStyle={{ flexGrow: 1 }}>
<Formik
initialValues={{
jobid: "",
jobid: {currentSJobId},
ticketdate: date2.toLocaleDateString(),
employee: currentEmployee.technician.first_name,
costcenter: "",
costcenter: {currentSCC},
productivehours: "",
actualhours: "",
}}
@@ -146,8 +149,13 @@ export function TimeTicketCreate({
</Formik>
{/* Below is for list of jobs/tickets */}
<View style={localStyles.bottomTimeTicketContainer}></View>
</ScrollView>
<View style={{ flexGrow: 1 }}>
<LaborAllocationsTable jobId={currentSJobId?.value} />
</View>
</View>
);
}
@@ -159,12 +167,13 @@ const localStyles = StyleSheet.create({
display: "flex",
flex: 1,
},
topTimeTicketContainer: {},
bottomTimeTicketContainer: {},
topContainer: {},
bottomContainer: {},
input: {},
dateButton: {
margin: 16,
height: 40,
marginVertical: 4,
marginHorizontal: 16,
height: 48,
justifyContent: "center",
alignContent: "center",
borderColor: "blue",
@@ -177,7 +186,8 @@ const localStyles = StyleSheet.create({
alignContent: "center",
},
inputStyle: {
margin: 16,
marginVertical: 4,
marginHorizontal: 16,
height: 48,
fontSize: 16,
},

View File

@@ -923,6 +923,54 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
}
`;
export const GET_LINE_TICKET_BY_PK = gql`
query GET_LINE_TICKET_BY_PK($id: uuid!) {
jobs_by_pk(id: $id) {
id
lbr_adjustments
converted
}
joblines(where: { jobid: { _eq: $id }, removed: { _eq: false } }) {
id
line_desc
part_type
oem_partno
db_price
act_price
part_qty
mod_lbr_ty
db_hrs
mod_lb_hrs
lbr_op
lbr_amt
op_code_desc
convertedtolbr
convertedtolbr_data
}
timetickets(where: { jobid: { _eq: $id } }) {
actualhrs
ciecacode
cost_center
date
id
jobid
employeeid
memo
flat_rate
clockon
clockoff
rate
employee {
id
first_name
last_name
employee_number
}
productivehrs
}
}
`;
export const generate_UPDATE_JOB_KANBAN = (
oldChildId,
oldChildNewParent,

View File

@@ -360,6 +360,96 @@
"titles": {
"createtimeticket": "Create a Time Ticket"
}
},
"joblines": {
"actions": {
"converttolabor": "Convert amount to Labor.",
"new": "New Line"
},
"errors": {
"creating": "Error encountered while creating job line. {{message}}",
"updating": "Error encountered updating job line. {{message}}"
},
"fields": {
"act_price": "Retail Price",
"ah_detail_line": "Mark as Detail Labor Line (Autohouse Only)",
"db_price": "List Price",
"lbr_types": {
"LA1": "LA1",
"LA2": "LA2",
"LA3": "LA3",
"LA4": "LA4",
"LAA": "Aluminum",
"LAB": "Body",
"LAD": "Diagnostic",
"LAE": "Electrical",
"LAF": "Frame",
"LAG": "Glass",
"LAM": "Mechanical",
"LAR": "Refinish",
"LAS": "Structural",
"LAU": "User Defined"
},
"line_desc": "Line Desc.",
"line_ind": "S#",
"line_no": "Line #",
"location": "Location",
"mod_lb_hrs": "Hrs",
"mod_lbr_ty": "Labor Type",
"notes": "Notes",
"oem_partno": "OEM Part #",
"op_code_desc": "Op Code Description",
"part_qty": "Qty.",
"part_type": "Part Type",
"part_types": {
"CCC": "CC Cleaning",
"CCD": "CC Damage Waiver",
"CCDR": "CC Daily Rate",
"CCF": "CC Refuel",
"CCM": "CC Mileage",
"PAA": "Aftermarket",
"PAC": "Rechromed",
"PAE": "Existing",
"PAG": "Glass",
"PAL": "LKQ",
"PAM": "Remanufactured",
"PAN": "New/OEM",
"PAO": "Other",
"PAP": "OEM Partial",
"PAR": "Recored",
"PAS": "Sublet",
"PASL": "Sublet (L)"
},
"profitcenter_labor": "Profit Center: Labor",
"profitcenter_part": "Profit Center: Part",
"prt_dsmk_m": "Line Discount/Markup $",
"prt_dsmk_p": "Line Discount/Markup %",
"status": "Status",
"tax_part": "Tax Part",
"total": "Total",
"unq_seq": "Seq #"
},
"labels": {
"adjustmenttobeadded": "Adjustment to be added: {{adjustment}}",
"billref": "Latest Bill",
"convertedtolabor": "This line has been converted to labor. Ensure you adjust the profit center for the amount accordingly.",
"edit": "Edit Line",
"ioucreated": "IOU",
"new": "New Line",
"nostatus": "No Status",
"presets": "Jobline Presets"
},
"successes": {
"created": "Job line created successfully.",
"saved": "Job line saved.",
"updated": "Job line updated successfully."
},
"validations": {
"ahdetailonlyonuserdefinedtypes": "Detail line indicator can only be set for LA1, LA2, LA3, LA4, and LAU labor types.",
"hrsrequirediflbrtyp": "Labor hours are required if a labor type is selected. Clear the labor type if there are no labor hours.",
"requiredifparttype": "Required if a part type has been specified.",
"zeropriceexistingpart": "This line cannot have any price since it uses an existing part."
}
}
}
}

View File

@@ -360,6 +360,96 @@
"titles": {
"createtimeticket": ""
}
},
"joblines": {
"actions": {
"converttolabor": "",
"new": ""
},
"errors": {
"creating": "",
"updating": ""
},
"fields": {
"act_price": "Precio actual",
"ah_detail_line": "",
"db_price": "Precio de base de datos",
"lbr_types": {
"LA1": "",
"LA2": "",
"LA3": "",
"LA4": "",
"LAA": "",
"LAB": "",
"LAD": "",
"LAE": "",
"LAF": "",
"LAG": "",
"LAM": "",
"LAR": "",
"LAS": "",
"LAU": ""
},
"line_desc": "Descripción de línea",
"line_ind": "S#",
"line_no": "",
"location": "",
"mod_lb_hrs": "Horas laborales",
"mod_lbr_ty": "Tipo de trabajo",
"notes": "",
"oem_partno": "OEM parte #",
"op_code_desc": "",
"part_qty": "",
"part_type": "Tipo de parte",
"part_types": {
"CCC": "",
"CCD": "",
"CCDR": "",
"CCF": "",
"CCM": "",
"PAA": "",
"PAC": "",
"PAE": "",
"PAG": "",
"PAL": "",
"PAM": "",
"PAN": "",
"PAO": "",
"PAP": "",
"PAR": "",
"PAS": "",
"PASL": ""
},
"profitcenter_labor": "",
"profitcenter_part": "",
"prt_dsmk_m": "",
"prt_dsmk_p": "",
"status": "Estado",
"tax_part": "",
"total": "",
"unq_seq": "Seq #"
},
"labels": {
"adjustmenttobeadded": "",
"billref": "",
"convertedtolabor": "",
"edit": "Línea de edición",
"ioucreated": "",
"new": "Nueva línea",
"nostatus": "",
"presets": ""
},
"successes": {
"created": "",
"saved": "",
"updated": ""
},
"validations": {
"ahdetailonlyonuserdefinedtypes": "",
"hrsrequirediflbrtyp": "",
"requiredifparttype": "",
"zeropriceexistingpart": ""
}
}
}
}

View File

@@ -360,6 +360,96 @@
"titles": {
"createtimeticket": ""
}
},
"joblines": {
"actions": {
"converttolabor": "",
"new": ""
},
"errors": {
"creating": "",
"updating": ""
},
"fields": {
"act_price": "Prix actuel",
"ah_detail_line": "",
"db_price": "Prix de la base de données",
"lbr_types": {
"LA1": "",
"LA2": "",
"LA3": "",
"LA4": "",
"LAA": "",
"LAB": "",
"LAD": "",
"LAE": "",
"LAF": "",
"LAG": "",
"LAM": "",
"LAR": "",
"LAS": "",
"LAU": ""
},
"line_desc": "Description de la ligne",
"line_ind": "S#",
"line_no": "",
"location": "",
"mod_lb_hrs": "Heures de travail",
"mod_lbr_ty": "Type de travail",
"notes": "",
"oem_partno": "Pièce OEM #",
"op_code_desc": "",
"part_qty": "",
"part_type": "Type de pièce",
"part_types": {
"CCC": "",
"CCD": "",
"CCDR": "",
"CCF": "",
"CCM": "",
"PAA": "",
"PAC": "",
"PAE": "",
"PAG": "",
"PAL": "",
"PAM": "",
"PAN": "",
"PAO": "",
"PAP": "",
"PAR": "",
"PAS": "",
"PASL": ""
},
"profitcenter_labor": "",
"profitcenter_part": "",
"prt_dsmk_m": "",
"prt_dsmk_p": "",
"status": "Statut",
"tax_part": "",
"total": "",
"unq_seq": "Seq #"
},
"labels": {
"adjustmenttobeadded": "",
"billref": "",
"convertedtolabor": "",
"edit": "Ligne d'édition",
"ioucreated": "",
"new": "Nouvelle ligne",
"nostatus": "",
"presets": ""
},
"successes": {
"created": "",
"saved": "",
"updated": ""
},
"validations": {
"ahdetailonlyonuserdefinedtypes": "",
"hrsrequirediflbrtyp": "",
"requiredifparttype": "",
"zeropriceexistingpart": ""
}
}
}
}

View File

@@ -0,0 +1,54 @@
import i18next from "i18next";
import React from "react";
export const CalculateAllocationsTotals = (
bodyshop,
joblines,
timetickets,
adjustments = []
) => {
// console.log("🚀 ~ file: adjustments", adjustments);
// console.log("🚀 ~ file: bodyshop", bodyshop);
console.log("🚀 ~ file: joblines", joblines);
// console.log("🚀 ~ file: timetickets", timetickets);
const responsibilitycenters = bodyshop.md_responsibility_centers;
const jobCodes = joblines.map((item) => item.mod_lbr_ty);
console.log("jobCodes :", jobCodes);
//.filter((value, index, self) => self.indexOf(value) === index && !!value);
const ticketCodes = timetickets.map((item) => item.ciecacode);
console.log("ticketCodes :", ticketCodes);
//.filter((value, index, self) => self.indexOf(value) === index && !!value);
const adjustmentCodes = Object.keys(adjustments);
console.log("adjustmentCodes :", adjustmentCodes);
const allCodes = [...jobCodes, ...ticketCodes, ...adjustmentCodes].filter(
(value, index, self) => self.indexOf(value) === index && !!value
);
console.log("allCodes :", allCodes);
const r = allCodes.reduce((acc, value) => {
const r = {
opcode: value,
cost_center:
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? i18next.t(
`joblines.fields.lbr_types.${value && value.toUpperCase()}`
)
: responsibilitycenters.defaults.costs[value],
mod_lbr_ty: value,
total: joblines.reduce((acc2, val2) => {
return val2.mod_lbr_ty === value ? acc2 + val2.mod_lb_hrs : acc2;
}, 0),
adjustments: adjustments[value] || 0,
claimed: timetickets.reduce((acc3, val3) => {
return val3.ciecacode === value ? acc3 + val3.productivehrs : acc3;
}, 0),
};
r.difference = r.total + r.adjustments - r.claimed;
acc.push(r);
return acc;
}, []);
console.log(" r is :", r);
return r;
};