WIP Payroll. Added line assignment & begin server side calc.
This commit is contained in:
@@ -14067,6 +14067,48 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>scheduledintoday</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>scheduledouttoday</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>
|
||||||
</children>
|
</children>
|
||||||
@@ -19398,6 +19440,27 @@
|
|||||||
<folder_node>
|
<folder_node>
|
||||||
<name>actions</name>
|
<name>actions</name>
|
||||||
<children>
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>assign_team</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>
|
<concept_node>
|
||||||
<name>converttolabor</name>
|
<name>converttolabor</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -19555,6 +19618,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>assigned_team</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>
|
<concept_node>
|
||||||
<name>db_price</name>
|
<name>db_price</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -23928,6 +24012,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>dms_unsold</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>
|
<concept_node>
|
||||||
<name>dms_wip_acctnumber</name>
|
<name>dms_wip_acctnumber</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -43600,6 +43705,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>jobs_scheduled_completion</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>
|
<concept_node>
|
||||||
<name>lag_time</name>
|
<name>lag_time</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ import JobSendPartPriceChangeComponent from "../job-send-parts-price-change/job-
|
|||||||
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
||||||
import JobLinesExpander from "./job-lines-expander.component";
|
import JobLinesExpander from "./job-lines-expander.component";
|
||||||
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
|
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
|
||||||
|
import JoblineTeamAssignment from "../job-line-team-assignment/job-line-team-assignmnent.component";
|
||||||
import JobLineDispatchButton from "../job-line-dispatch-button/job-line-dispatch-button.component";
|
import JobLineDispatchButton from "../job-line-dispatch-button/job-line-dispatch-button.component";
|
||||||
|
import JobLineBulkAssignComponent from "../job-line-bulk-assign/job-line-bulk-assign.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -287,6 +289,14 @@ export function JobLinesComponent({
|
|||||||
state.sortedInfo.columnKey === "line_ind" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "line_ind" && state.sortedInfo.order,
|
||||||
responsive: ["md"],
|
responsive: ["md"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.assigned_team"),
|
||||||
|
dataIndex: "assigned_team",
|
||||||
|
key: "assigned_team",
|
||||||
|
render: (text, record) => (
|
||||||
|
<JoblineTeamAssignment disabled={jobRO} jobline={record} />
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("joblines.fields.notes"),
|
title: t("joblines.fields.notes"),
|
||||||
dataIndex: "notes",
|
dataIndex: "notes",
|
||||||
@@ -405,7 +415,11 @@ export function JobLinesComponent({
|
|||||||
setSelectedLines(
|
setSelectedLines(
|
||||||
_.uniq([
|
_.uniq([
|
||||||
...selectedLines,
|
...selectedLines,
|
||||||
...jobLines.filter((item) => markedTypes.includes(item.part_type)),
|
...jobLines.filter(
|
||||||
|
(item) =>
|
||||||
|
markedTypes.includes(item.part_type) ||
|
||||||
|
markedTypes.includes(item.mod_lbr_ty)
|
||||||
|
),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -418,6 +432,10 @@ export function JobLinesComponent({
|
|||||||
<Menu.Item key="PAL">{t("joblines.fields.part_types.PAL")}</Menu.Item>
|
<Menu.Item key="PAL">{t("joblines.fields.part_types.PAL")}</Menu.Item>
|
||||||
<Menu.Item key="PAS">{t("joblines.fields.part_types.PAS")}</Menu.Item>
|
<Menu.Item key="PAS">{t("joblines.fields.part_types.PAS")}</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
<Menu.Item key="LAB">{t("joblines.fields.lbr_types.LAB")}</Menu.Item>
|
||||||
|
<Menu.Item key="LAR">{t("joblines.fields.lbr_types.LAR")}</Menu.Item>
|
||||||
|
<Menu.Item key="LAM">{t("joblines.fields.lbr_types.LAM")}</Menu.Item>
|
||||||
|
<Menu.Divider />
|
||||||
<Menu.Item key="clear">{t("general.labels.clear")}</Menu.Item>
|
<Menu.Item key="clear">{t("general.labels.clear")}</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
@@ -446,6 +464,11 @@ export function JobLinesComponent({
|
|||||||
setSelectedLines={setSelectedLines}
|
setSelectedLines={setSelectedLines}
|
||||||
job={job}
|
job={job}
|
||||||
/>
|
/>
|
||||||
|
<JobLineBulkAssignComponent
|
||||||
|
selectedLines={selectedLines}
|
||||||
|
setSelectedLines={setSelectedLines}
|
||||||
|
job={job}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
(job && !job.converted) ||
|
(job && !job.converted) ||
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { Button, Form, Popover, Select, Space, notification } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { UPDATE_LINE_BULK_ASSIGN } from "../../graphql/jobs-lines.queries";
|
||||||
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
|
import {
|
||||||
|
selectBodyshop,
|
||||||
|
selectCurrentUser,
|
||||||
|
} from "../../redux/user/user.selectors";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
jobRO: selectJobReadOnly,
|
||||||
|
currentUser: selectCurrentUser,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(JoblineBulkAssign);
|
||||||
|
|
||||||
|
export function JoblineBulkAssign({
|
||||||
|
setSelectedLines,
|
||||||
|
selectedLines,
|
||||||
|
|
||||||
|
bodyshop,
|
||||||
|
jobRO,
|
||||||
|
job,
|
||||||
|
currentUser,
|
||||||
|
}) {
|
||||||
|
console.log(
|
||||||
|
"🚀 ~ file: job-line-bulk-assign.component.jsx:36 ~ selectedLines:",
|
||||||
|
selectedLines
|
||||||
|
);
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [assignLines] = useMutation(UPDATE_LINE_BULK_ASSIGN);
|
||||||
|
|
||||||
|
const handleConvert = async (values) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await assignLines({
|
||||||
|
variables: {
|
||||||
|
jobline: {
|
||||||
|
assigned_team: values.assigned_team,
|
||||||
|
},
|
||||||
|
ids: selectedLines.map((l) => l.id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (result.errors) {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("parts_dispatch.errors.creating", {
|
||||||
|
error: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setVisible(false);
|
||||||
|
} catch (error) {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("parts_dispatch.errors.creating", {
|
||||||
|
error: error,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const popMenu = (
|
||||||
|
<div>
|
||||||
|
<Form layout="vertical" form={form} onFinish={handleConvert}>
|
||||||
|
<Form.Item
|
||||||
|
name={"assigned_team"}
|
||||||
|
label={t("joblines.fields.assigned_team")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
style={{ width: 200 }}
|
||||||
|
optionFilterProp="children"
|
||||||
|
filterOption={(input, option) =>
|
||||||
|
option.props.children
|
||||||
|
.toLowerCase()
|
||||||
|
.indexOf(input.toLowerCase()) >= 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{bodyshop.employee_teams.map((team) => (
|
||||||
|
<Select.Option value={team.id} key={team.id} name={team.name}>
|
||||||
|
{team.name}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Space wrap>
|
||||||
|
<Button type="danger" onClick={() => form.submit()} loading={loading}>
|
||||||
|
{t("general.actions.save")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setVisible(false)}>
|
||||||
|
{t("general.actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={visible} content={popMenu}>
|
||||||
|
<Button
|
||||||
|
disabled={selectedLines.length === 0 || jobRO}
|
||||||
|
loading={loading}
|
||||||
|
onClick={() => setVisible(true)}
|
||||||
|
>
|
||||||
|
{t("joblines.actions.assign_team", { count: selectedLines.length })}
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { notification, Select } from "antd";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
//currentUser: selectCurrentUser
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function JoblineTeamAssignment({ bodyshop, jobline, disabled }) {
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [assignedTeam, setAssignedTeam] = useState(jobline.assigned_team);
|
||||||
|
const [updateJob] = useMutation(UPDATE_JOB_LINE);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editing) setAssignedTeam(jobline.assigned_team);
|
||||||
|
}, [editing, jobline.assigned_team]);
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
setAssignedTeam(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (e) => {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await updateJob({
|
||||||
|
variables: {
|
||||||
|
lineId: jobline.id,
|
||||||
|
line: { assigned_team: assignedTeam },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
assignedTeam === null ||
|
||||||
|
assignedTeam === undefined ||
|
||||||
|
assignedTeam === ""
|
||||||
|
) {
|
||||||
|
alert("TODO - implement calculation to reduce assigned hours if needed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!!result.errors) {
|
||||||
|
notification["success"]({ message: t("joblines.successes.saved") });
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("joblines.errors.saving", {
|
||||||
|
error: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
setEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editing)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<LoadingSpinner loading={loading}>
|
||||||
|
<Select
|
||||||
|
autoFocus
|
||||||
|
allowClear
|
||||||
|
dropdownMatchSelectWidth={100}
|
||||||
|
value={assignedTeam}
|
||||||
|
onSelect={handleChange}
|
||||||
|
onBlur={handleSave}
|
||||||
|
onClear={() => handleChange(null)}
|
||||||
|
>
|
||||||
|
{Object.values(bodyshop.employee_teams).map((s, idx) => (
|
||||||
|
<Select.Option key={idx} value={s.id}>
|
||||||
|
{s.name}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</LoadingSpinner>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const team = bodyshop.employee_teams.find(
|
||||||
|
(tm) => tm.id === jobline.assigned_team
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ width: "100%", minHeight: "1rem", cursor: "pointer" }}
|
||||||
|
onClick={() => !disabled && setEditing(true)}
|
||||||
|
>
|
||||||
|
{team?.name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(JoblineTeamAssignment);
|
||||||
@@ -5,6 +5,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import LaborAllocationsTableComponent from "../labor-allocations-table/labor-allocations-table.component";
|
import LaborAllocationsTableComponent from "../labor-allocations-table/labor-allocations-table.component";
|
||||||
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
||||||
|
import PayrollLaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.payroll.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -78,6 +79,14 @@ export function JobsDetailLaborContainer({
|
|||||||
adjustments={adjustments}
|
adjustments={adjustments}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col {...adjSpan}>
|
||||||
|
<PayrollLaborAllocationsTable
|
||||||
|
jobId={jobId}
|
||||||
|
joblines={joblines}
|
||||||
|
timetickets={timetickets}
|
||||||
|
adjustments={adjustments}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
import { EditFilled } from "@ant-design/icons";
|
||||||
|
import { Button, Card, Col, Row, Space, Table, Typography } from "antd";
|
||||||
|
import _ from "lodash";
|
||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component";
|
||||||
|
import "./labor-allocations-table.styles.scss";
|
||||||
|
import { CalculateAllocationsTotals } from "./labor-allocations-table.utility";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
technician: selectTechnician,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function PayrollLaborAllocationsTable({
|
||||||
|
jobId,
|
||||||
|
joblines,
|
||||||
|
timetickets,
|
||||||
|
bodyshop,
|
||||||
|
adjustments,
|
||||||
|
technician,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [totals, setTotals] = useState([]);
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {
|
||||||
|
columnKey: "cost_center",
|
||||||
|
field: "cost_center",
|
||||||
|
order: "ascend",
|
||||||
|
},
|
||||||
|
filteredInfo: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!!joblines && !!timetickets && !!bodyshop);
|
||||||
|
|
||||||
|
setTotals(
|
||||||
|
CalculateAllocationsTotals(bodyshop, joblines, timetickets, adjustments)
|
||||||
|
);
|
||||||
|
if (!jobId) setTotals([]);
|
||||||
|
}, [joblines, timetickets, bodyshop, adjustments, jobId]);
|
||||||
|
|
||||||
|
const convertedLines = useMemo(
|
||||||
|
() => joblines && joblines.filter((j) => j.convertedtolbr),
|
||||||
|
[joblines]
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("timetickets.fields.cost_center"),
|
||||||
|
dataIndex: "cost_center",
|
||||||
|
key: "cost_center",
|
||||||
|
defaultSortOrder: "cost_center",
|
||||||
|
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => `${record.cost_center} (${record.mod_lbr_ty})`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.hrs_total"),
|
||||||
|
dataIndex: "total",
|
||||||
|
key: "total",
|
||||||
|
sorter: (a, b) => a.total - b.total,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => record.total.toFixed(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.hrs_claimed"),
|
||||||
|
dataIndex: "hrs_claimed",
|
||||||
|
key: "hrs_claimed",
|
||||||
|
sorter: (a, b) => a.claimed - b.claimed,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "claimed" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => record.claimed && record.claimed.toFixed(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.adjustments"),
|
||||||
|
dataIndex: "adjustments",
|
||||||
|
key: "adjustments",
|
||||||
|
sorter: (a, b) => a.adjustments - b.adjustments,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "adjustments" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<Space wrap>
|
||||||
|
{record.adjustments.toFixed(1)}
|
||||||
|
{!technician && (
|
||||||
|
<LaborAllocationsAdjustmentEdit
|
||||||
|
jobId={jobId}
|
||||||
|
adjustments={adjustments}
|
||||||
|
mod_lbr_ty={record.opcode}
|
||||||
|
refetchQueryNames={["GET_LINE_TICKET_BY_PK"]}
|
||||||
|
>
|
||||||
|
<EditFilled />
|
||||||
|
</LaborAllocationsAdjustmentEdit>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.labels.difference"),
|
||||||
|
dataIndex: "difference",
|
||||||
|
|
||||||
|
key: "difference",
|
||||||
|
sorter: (a, b) => a.difference - b.difference,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "difference" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<strong
|
||||||
|
style={{
|
||||||
|
color: record.difference >= 0 ? "green" : "red",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{_.round(record.difference, 1)}
|
||||||
|
</strong>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const convertedTableCols = [
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.line_desc"),
|
||||||
|
dataIndex: "line_desc",
|
||||||
|
key: "line_desc",
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.op_code_desc"),
|
||||||
|
dataIndex: "op_code_desc",
|
||||||
|
key: "op_code_desc",
|
||||||
|
ellipsis: true,
|
||||||
|
render: (text, record) =>
|
||||||
|
`${record.op_code_desc || ""}${
|
||||||
|
record.alt_partm ? ` ${record.alt_partm}` : ""
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.act_price"),
|
||||||
|
dataIndex: "act_price",
|
||||||
|
key: "act_price",
|
||||||
|
ellipsis: true,
|
||||||
|
render: (text, record) => (
|
||||||
|
<>
|
||||||
|
<CurrencyFormatter>
|
||||||
|
{record.db_ref === "900510" || record.db_ref === "900511"
|
||||||
|
? record.prt_dsmk_m
|
||||||
|
: record.act_price}
|
||||||
|
</CurrencyFormatter>
|
||||||
|
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
|
||||||
|
<span
|
||||||
|
style={{ marginLeft: ".2rem" }}
|
||||||
|
>{`(${record.prt_dsmk_p}%)`}</span>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.part_qty"),
|
||||||
|
dataIndex: "part_qty",
|
||||||
|
key: "part_qty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.mod_lbr_ty"),
|
||||||
|
dataIndex: "conv_mod_lbr_ty",
|
||||||
|
key: "conv_mod_lbr_ty",
|
||||||
|
render: (text, record) =>
|
||||||
|
record.convertedtolbr_data && record.convertedtolbr_data.mod_lbr_ty,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.mod_lb_hrs"),
|
||||||
|
dataIndex: "conv_mod_lb_hrs",
|
||||||
|
key: "conv_mod_lb_hrs",
|
||||||
|
render: (text, record) =>
|
||||||
|
record.convertedtolbr_data &&
|
||||||
|
record.convertedtolbr_data.mod_lb_hrs &&
|
||||||
|
record.convertedtolbr_data.mod_lb_hrs.toFixed(1),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={24}>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
console.log(
|
||||||
|
await axios.post("/payroll/payall", {
|
||||||
|
jobid: jobId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Calc
|
||||||
|
</Button>
|
||||||
|
<Card title={t("jobs.labels.laborallocations")}>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
rowKey={(record) => `${record.cost_center} ${record.mod_lbr_ty}`}
|
||||||
|
pagination={false}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
dataSource={totals}
|
||||||
|
scroll={{
|
||||||
|
x: true,
|
||||||
|
}}
|
||||||
|
summary={() => (
|
||||||
|
<Table.Summary.Row>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
<Typography.Title level={4}>
|
||||||
|
{t("general.labels.totals")}
|
||||||
|
</Typography.Title>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{summary.hrs_total.toFixed(1)}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{summary.hrs_claimed.toFixed(1)}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{summary.adjustments.toFixed(1)}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{summary.difference.toFixed(1)}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</Table.Summary.Row>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
{convertedLines && convertedLines.length > 0 && (
|
||||||
|
<Col span={24}>
|
||||||
|
<Card title={t("jobs.labels.convertedtolabor")}>
|
||||||
|
<Table
|
||||||
|
columns={convertedTableCols}
|
||||||
|
rowKey="id"
|
||||||
|
pagination={false}
|
||||||
|
dataSource={convertedLines}
|
||||||
|
scroll={{
|
||||||
|
x: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(mapStateToProps, null)(PayrollLaborAllocationsTable);
|
||||||
@@ -119,6 +119,19 @@ export const QUERY_BODYSHOP = gql`
|
|||||||
tt_enforce_hours_for_tech_console
|
tt_enforce_hours_for_tech_console
|
||||||
md_tasks_presets
|
md_tasks_presets
|
||||||
use_paint_scale_data
|
use_paint_scale_data
|
||||||
|
employee_teams(
|
||||||
|
order_by: { name: asc }
|
||||||
|
where: { active: { _eq: true } }
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
employee_team_members {
|
||||||
|
id
|
||||||
|
employeeid
|
||||||
|
labor_rates
|
||||||
|
percentage
|
||||||
|
}
|
||||||
|
}
|
||||||
employees {
|
employees {
|
||||||
user_email
|
user_email
|
||||||
id
|
id
|
||||||
@@ -235,6 +248,19 @@ export const UPDATE_SHOP = gql`
|
|||||||
enforce_conversion_category
|
enforce_conversion_category
|
||||||
tt_enforce_hours_for_tech_console
|
tt_enforce_hours_for_tech_console
|
||||||
md_tasks_presets
|
md_tasks_presets
|
||||||
|
employee_teams(
|
||||||
|
order_by: { name: asc }
|
||||||
|
where: { active: { _eq: true } }
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
employee_team_members {
|
||||||
|
id
|
||||||
|
employeeid
|
||||||
|
labor_rates
|
||||||
|
percentage
|
||||||
|
}
|
||||||
|
}
|
||||||
employees {
|
employees {
|
||||||
id
|
id
|
||||||
first_name
|
first_name
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ export const GET_LINE_TICKET_BY_PK = gql`
|
|||||||
op_code_desc
|
op_code_desc
|
||||||
convertedtolbr
|
convertedtolbr
|
||||||
convertedtolbr_data
|
convertedtolbr_data
|
||||||
|
|
||||||
}
|
}
|
||||||
timetickets(where: { jobid: { _eq: $id } }) {
|
timetickets(where: { jobid: { _eq: $id } }) {
|
||||||
actualhrs
|
actualhrs
|
||||||
@@ -244,6 +243,7 @@ export const UPDATE_JOB_LINE = gql`
|
|||||||
removed
|
removed
|
||||||
convertedtolbr
|
convertedtolbr
|
||||||
convertedtolbr_data
|
convertedtolbr_data
|
||||||
|
assigned_team
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,3 +348,19 @@ export const UPDATE_LINE_PPC = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const UPDATE_LINE_BULK_ASSIGN = gql`
|
||||||
|
mutation UPDATE_LINE_BULK_ASSIGN(
|
||||||
|
$ids: [uuid!]!
|
||||||
|
$jobline: joblines_set_input
|
||||||
|
) {
|
||||||
|
update_joblines_many(
|
||||||
|
updates: { _set: $jobline, where: { id: { _in: $ids } } }
|
||||||
|
) {
|
||||||
|
returning {
|
||||||
|
id
|
||||||
|
assigned_team
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -733,6 +733,7 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
employeeid
|
employeeid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
assigned_team
|
||||||
billlines(limit: 1, order_by: { bill: { date: desc } }) {
|
billlines(limit: 1, order_by: { bill: { date: desc } }) {
|
||||||
id
|
id
|
||||||
quantity
|
quantity
|
||||||
|
|||||||
@@ -1212,6 +1212,7 @@
|
|||||||
},
|
},
|
||||||
"joblines": {
|
"joblines": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"assign_team": "Assign Team",
|
||||||
"converttolabor": "Convert amount to Labor.",
|
"converttolabor": "Convert amount to Labor.",
|
||||||
"dispatchparts": "Dispatch Parts ({{count}})",
|
"dispatchparts": "Dispatch Parts ({{count}})",
|
||||||
"new": "New Line"
|
"new": "New Line"
|
||||||
@@ -1223,6 +1224,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"act_price": "Retail Price",
|
"act_price": "Retail Price",
|
||||||
"ah_detail_line": "Mark as Detail Labor Line (Autohouse Only)",
|
"ah_detail_line": "Mark as Detail Labor Line (Autohouse Only)",
|
||||||
|
"assigned_team": "Team",
|
||||||
"db_price": "List Price",
|
"db_price": "List Price",
|
||||||
"lbr_types": {
|
"lbr_types": {
|
||||||
"LA1": "LA1",
|
"LA1": "LA1",
|
||||||
@@ -1455,8 +1457,8 @@
|
|||||||
"cost_dms_acctnumber": "Cost DMS Acct #",
|
"cost_dms_acctnumber": "Cost DMS Acct #",
|
||||||
"dms_make": "DMS Make",
|
"dms_make": "DMS Make",
|
||||||
"dms_model": "DMS Model",
|
"dms_model": "DMS Model",
|
||||||
"dms_wip_acctnumber": "Cost WIP DMS Acct #",
|
|
||||||
"dms_unsold": "New, Unsold Vehicle",
|
"dms_unsold": "New, Unsold Vehicle",
|
||||||
|
"dms_wip_acctnumber": "Cost WIP DMS Acct #",
|
||||||
"id": "DMS ID",
|
"id": "DMS ID",
|
||||||
"inservicedate": "In Service Date",
|
"inservicedate": "In Service Date",
|
||||||
"journal": "Journal #",
|
"journal": "Journal #",
|
||||||
@@ -2590,8 +2592,8 @@
|
|||||||
"job_costing_ro_ins_co": "Job Costing by RO Source",
|
"job_costing_ro_ins_co": "Job Costing by RO Source",
|
||||||
"jobs_completed_not_invoiced": "Jobs Completed not Invoiced",
|
"jobs_completed_not_invoiced": "Jobs Completed not Invoiced",
|
||||||
"jobs_invoiced_not_exported": "Jobs Invoiced not Exported",
|
"jobs_invoiced_not_exported": "Jobs Invoiced not Exported",
|
||||||
"jobs_scheduled_completion": "Jobs Scheduled Completion",
|
|
||||||
"jobs_reconcile": "Parts/Sublet/Labor Reconciliation",
|
"jobs_reconcile": "Parts/Sublet/Labor Reconciliation",
|
||||||
|
"jobs_scheduled_completion": "Jobs Scheduled Completion",
|
||||||
"lag_time": "Lag Time",
|
"lag_time": "Lag Time",
|
||||||
"open_orders": "Open Orders by Date",
|
"open_orders": "Open Orders by Date",
|
||||||
"open_orders_csr": "Open Orders by CSR",
|
"open_orders_csr": "Open Orders by CSR",
|
||||||
|
|||||||
@@ -1212,6 +1212,7 @@
|
|||||||
},
|
},
|
||||||
"joblines": {
|
"joblines": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"assign_team": "",
|
||||||
"converttolabor": "",
|
"converttolabor": "",
|
||||||
"dispatchparts": "",
|
"dispatchparts": "",
|
||||||
"new": ""
|
"new": ""
|
||||||
@@ -1223,6 +1224,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"act_price": "Precio actual",
|
"act_price": "Precio actual",
|
||||||
"ah_detail_line": "",
|
"ah_detail_line": "",
|
||||||
|
"assigned_team": "",
|
||||||
"db_price": "Precio de base de datos",
|
"db_price": "Precio de base de datos",
|
||||||
"lbr_types": {
|
"lbr_types": {
|
||||||
"LA1": "",
|
"LA1": "",
|
||||||
@@ -1455,8 +1457,8 @@
|
|||||||
"cost_dms_acctnumber": "",
|
"cost_dms_acctnumber": "",
|
||||||
"dms_make": "",
|
"dms_make": "",
|
||||||
"dms_model": "",
|
"dms_model": "",
|
||||||
"dms_wip_acctnumber": "",
|
|
||||||
"dms_unsold": "",
|
"dms_unsold": "",
|
||||||
|
"dms_wip_acctnumber": "",
|
||||||
"id": "",
|
"id": "",
|
||||||
"inservicedate": "",
|
"inservicedate": "",
|
||||||
"journal": "",
|
"journal": "",
|
||||||
@@ -2590,8 +2592,8 @@
|
|||||||
"job_costing_ro_ins_co": "",
|
"job_costing_ro_ins_co": "",
|
||||||
"jobs_completed_not_invoiced": "",
|
"jobs_completed_not_invoiced": "",
|
||||||
"jobs_invoiced_not_exported": "",
|
"jobs_invoiced_not_exported": "",
|
||||||
"jobs_scheduled_completion": "",
|
|
||||||
"jobs_reconcile": "",
|
"jobs_reconcile": "",
|
||||||
|
"jobs_scheduled_completion": "",
|
||||||
"lag_time": "",
|
"lag_time": "",
|
||||||
"open_orders": "",
|
"open_orders": "",
|
||||||
"open_orders_csr": "",
|
"open_orders_csr": "",
|
||||||
|
|||||||
@@ -1212,6 +1212,7 @@
|
|||||||
},
|
},
|
||||||
"joblines": {
|
"joblines": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"assign_team": "",
|
||||||
"converttolabor": "",
|
"converttolabor": "",
|
||||||
"dispatchparts": "",
|
"dispatchparts": "",
|
||||||
"new": ""
|
"new": ""
|
||||||
@@ -1223,6 +1224,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"act_price": "Prix actuel",
|
"act_price": "Prix actuel",
|
||||||
"ah_detail_line": "",
|
"ah_detail_line": "",
|
||||||
|
"assigned_team": "",
|
||||||
"db_price": "Prix de la base de données",
|
"db_price": "Prix de la base de données",
|
||||||
"lbr_types": {
|
"lbr_types": {
|
||||||
"LA1": "",
|
"LA1": "",
|
||||||
@@ -1455,8 +1457,8 @@
|
|||||||
"cost_dms_acctnumber": "",
|
"cost_dms_acctnumber": "",
|
||||||
"dms_make": "",
|
"dms_make": "",
|
||||||
"dms_model": "",
|
"dms_model": "",
|
||||||
"dms_wip_acctnumber": "",
|
|
||||||
"dms_unsold": "",
|
"dms_unsold": "",
|
||||||
|
"dms_wip_acctnumber": "",
|
||||||
"id": "",
|
"id": "",
|
||||||
"inservicedate": "",
|
"inservicedate": "",
|
||||||
"journal": "",
|
"journal": "",
|
||||||
@@ -2590,8 +2592,8 @@
|
|||||||
"job_costing_ro_ins_co": "",
|
"job_costing_ro_ins_co": "",
|
||||||
"jobs_completed_not_invoiced": "",
|
"jobs_completed_not_invoiced": "",
|
||||||
"jobs_invoiced_not_exported": "",
|
"jobs_invoiced_not_exported": "",
|
||||||
"jobs_scheduled_completion": "",
|
|
||||||
"jobs_reconcile": "",
|
"jobs_reconcile": "",
|
||||||
|
"jobs_scheduled_completion": "",
|
||||||
"lag_time": "",
|
"lag_time": "",
|
||||||
"open_orders": "",
|
"open_orders": "",
|
||||||
"open_orders_csr": "",
|
"open_orders_csr": "",
|
||||||
|
|||||||
@@ -261,6 +261,14 @@ app.post(
|
|||||||
intellipay.postback
|
intellipay.postback
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const payroll = require("./server/payroll/payroll");
|
||||||
|
app.post(
|
||||||
|
"/payroll/calculatelabortotals",
|
||||||
|
fb.validateFirebaseIdToken,
|
||||||
|
payroll.calculateLaborTotals
|
||||||
|
);
|
||||||
|
app.post("/payroll/payall", fb.validateFirebaseIdToken, payroll.payall);
|
||||||
|
|
||||||
var ioevent = require("./server/ioevent/ioevent");
|
var ioevent = require("./server/ioevent/ioevent");
|
||||||
app.post("/ioevent", ioevent.default);
|
app.post("/ioevent", ioevent.default);
|
||||||
// app.post("/newlog", (req, res) => {
|
// app.post("/newlog", (req, res) => {
|
||||||
|
|||||||
@@ -1822,3 +1822,92 @@ exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports.QUERY_JOB_PAYROLL_DATA = `query QUERY_JOB_PAYROLL_DATA($id: uuid!) {
|
||||||
|
jobs_by_pk(id: $id) {
|
||||||
|
bodyshop{
|
||||||
|
id
|
||||||
|
employee_teams{
|
||||||
|
id
|
||||||
|
name
|
||||||
|
employee_team_members{
|
||||||
|
id
|
||||||
|
employee{
|
||||||
|
id
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
|
}
|
||||||
|
percentage
|
||||||
|
labor_rates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timetickets{
|
||||||
|
id
|
||||||
|
employeeid
|
||||||
|
rate
|
||||||
|
productivehrs
|
||||||
|
actualhrs
|
||||||
|
ciecacode
|
||||||
|
}
|
||||||
|
lbr_adjustments
|
||||||
|
ro_number
|
||||||
|
id
|
||||||
|
job_totals
|
||||||
|
rate_la1
|
||||||
|
rate_la2
|
||||||
|
rate_la3
|
||||||
|
rate_la4
|
||||||
|
rate_laa
|
||||||
|
rate_lab
|
||||||
|
rate_lad
|
||||||
|
rate_lae
|
||||||
|
rate_laf
|
||||||
|
rate_lag
|
||||||
|
rate_lam
|
||||||
|
rate_lar
|
||||||
|
rate_las
|
||||||
|
rate_lau
|
||||||
|
rate_ma2s
|
||||||
|
rate_ma2t
|
||||||
|
rate_ma3s
|
||||||
|
rate_mabl
|
||||||
|
rate_macs
|
||||||
|
rate_mahw
|
||||||
|
rate_mapa
|
||||||
|
rate_mash
|
||||||
|
rate_matd
|
||||||
|
status
|
||||||
|
materials
|
||||||
|
joblines(where: { removed: { _eq: false } }){
|
||||||
|
id
|
||||||
|
line_no
|
||||||
|
unq_seq
|
||||||
|
line_ind
|
||||||
|
line_desc
|
||||||
|
part_type
|
||||||
|
line_ref
|
||||||
|
oem_partno
|
||||||
|
db_price
|
||||||
|
act_price
|
||||||
|
part_qty
|
||||||
|
mod_lbr_ty
|
||||||
|
db_hrs
|
||||||
|
mod_lb_hrs
|
||||||
|
lbr_op
|
||||||
|
lbr_amt
|
||||||
|
op_code_desc
|
||||||
|
status
|
||||||
|
notes
|
||||||
|
location
|
||||||
|
tax_part
|
||||||
|
db_ref
|
||||||
|
manual_line
|
||||||
|
prt_dsmk_p
|
||||||
|
prt_dsmk_m
|
||||||
|
misc_amt
|
||||||
|
misc_tax
|
||||||
|
assigned_team
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|||||||
59
server/payroll/calculate-totals.js
Normal file
59
server/payroll/calculate-totals.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const Dinero = require("dinero.js");
|
||||||
|
const queries = require("../graphql-client/queries");
|
||||||
|
const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
// Dinero.defaultCurrency = "USD";
|
||||||
|
// Dinero.globalLocale = "en-CA";
|
||||||
|
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||||
|
|
||||||
|
exports.calculateLaborTotals = async function (req, res) {
|
||||||
|
const BearerToken = req.headers.authorization;
|
||||||
|
const { jobid } = req.body;
|
||||||
|
logger.log("job-payroll-labor-totals", "DEBUG", req.user.email, jobid, null);
|
||||||
|
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
|
||||||
|
headers: {
|
||||||
|
Authorization: BearerToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { jobs_by_pk: job } = await client
|
||||||
|
.setHeaders({ Authorization: BearerToken })
|
||||||
|
.request(queries.QUERY_JOB_PAYROLL_DATA, {
|
||||||
|
id: jobid,
|
||||||
|
});
|
||||||
|
|
||||||
|
//iterate over each ticket, building a hash of team -> employee to calculate total assigned hours.
|
||||||
|
|
||||||
|
const assignmentHash = { unassigned: 0 };
|
||||||
|
job.joblines.forEach((jobline) => {
|
||||||
|
if (jobline.mod_lb_hrs > 0) {
|
||||||
|
//Check if the line is assigned. If not, keep track of it as an unassigned line by type.
|
||||||
|
if (jobline.assigned_team === null) {
|
||||||
|
assignmentHash.unassigned[jobline.mod_lbr_ty] =
|
||||||
|
assignmentHash.unassigned[jobline.mod_lbr_ty] + jobline.mod_lb_hrs;
|
||||||
|
} else {
|
||||||
|
//Line is assigned.
|
||||||
|
if (!assignmentHash[jobline.assigned_team]) {
|
||||||
|
assignmentHash[jobline.assigned_team] = 0;
|
||||||
|
}
|
||||||
|
assignmentHash[jobline.assigned_team] =
|
||||||
|
assignmentHash[jobline.assigned_team] + jobline.mod_lb_hrs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res.json(assignmentHash);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(
|
||||||
|
"job-payroll-labor-totals-error",
|
||||||
|
"ERROR",
|
||||||
|
req.user.email,
|
||||||
|
jobid,
|
||||||
|
{
|
||||||
|
jobid: jobid,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
res.status(503).send();
|
||||||
|
}
|
||||||
|
};
|
||||||
249
server/payroll/pay-all.js
Normal file
249
server/payroll/pay-all.js
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
const Dinero = require("dinero.js");
|
||||||
|
const queries = require("../graphql-client/queries");
|
||||||
|
const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||||
|
const _ = require("lodash");
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
// Dinero.defaultCurrency = "USD";
|
||||||
|
// Dinero.globalLocale = "en-CA";
|
||||||
|
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||||
|
|
||||||
|
exports.payall = async function (req, res) {
|
||||||
|
const BearerToken = req.headers.authorization;
|
||||||
|
const { jobid } = req.body;
|
||||||
|
logger.log("job-payroll-pay-all", "DEBUG", req.user.email, jobid, null);
|
||||||
|
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
|
||||||
|
headers: {
|
||||||
|
Authorization: BearerToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { jobs_by_pk: job } = await client
|
||||||
|
.setHeaders({ Authorization: BearerToken })
|
||||||
|
.request(queries.QUERY_JOB_PAYROLL_DATA, {
|
||||||
|
id: jobid,
|
||||||
|
});
|
||||||
|
|
||||||
|
//iterate over each ticket, building a hash of team -> employee to calculate total assigned hours.
|
||||||
|
|
||||||
|
const assignmentHash = { unassigned: 0 };
|
||||||
|
const employeeHash = {}; // employeeid => Cieca labor type => rate => hours. Contains how many hours each person should be paid.
|
||||||
|
job.joblines.forEach((jobline) => {
|
||||||
|
if (jobline.mod_lb_hrs > 0) {
|
||||||
|
//Check if the line is assigned. If not, keep track of it as an unassigned line by type.
|
||||||
|
if (jobline.assigned_team === null) {
|
||||||
|
assignmentHash.unassigned =
|
||||||
|
assignmentHash.unassigned + jobline.mod_lb_hrs;
|
||||||
|
} else {
|
||||||
|
//Line is assigned.
|
||||||
|
if (!assignmentHash[jobline.assigned_team]) {
|
||||||
|
assignmentHash[jobline.assigned_team] = 0;
|
||||||
|
}
|
||||||
|
assignmentHash[jobline.assigned_team] =
|
||||||
|
assignmentHash[jobline.assigned_team] + jobline.mod_lb_hrs;
|
||||||
|
|
||||||
|
//Create the assignment breakdown.
|
||||||
|
const theTeam = job.bodyshop.employee_teams.find(
|
||||||
|
(team) => team.id === jobline.assigned_team
|
||||||
|
);
|
||||||
|
|
||||||
|
theTeam.employee_team_members.forEach((tm) => {
|
||||||
|
//Figure out how many hours they are owed at this line, and at what rate.
|
||||||
|
console.log(tm);
|
||||||
|
if (!employeeHash[tm.employee.id]) {
|
||||||
|
employeeHash[tm.employee.id] = {};
|
||||||
|
}
|
||||||
|
if (!employeeHash[tm.employee.id][jobline.mod_lbr_ty]) {
|
||||||
|
employeeHash[tm.employee.id][jobline.mod_lbr_ty] = {};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!employeeHash[tm.employee.id][jobline.mod_lbr_ty][
|
||||||
|
tm.labor_rates[jobline.mod_lbr_ty]
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
|
||||||
|
tm.labor_rates[jobline.mod_lbr_ty]
|
||||||
|
] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoursOwed = (tm.percentage / 100) * jobline.mod_lb_hrs;
|
||||||
|
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
|
||||||
|
tm.labor_rates[jobline.mod_lbr_ty]
|
||||||
|
] =
|
||||||
|
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
|
||||||
|
tm.labor_rates[jobline.mod_lbr_ty]
|
||||||
|
] + hoursOwed;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ticketHash = {}; // employeeid => Cieca labor type => rate => hours.
|
||||||
|
//Calculate how much each employee has been paid so far.
|
||||||
|
job.timetickets.forEach((ticket) => {
|
||||||
|
if (!ticketHash[ticket.employeeid]) {
|
||||||
|
ticketHash[ticket.employeeid] = {};
|
||||||
|
}
|
||||||
|
if (!ticketHash[ticket.employeeid][ticket.ciecacode]) {
|
||||||
|
ticketHash[ticket.employeeid][ticket.ciecacode] = {};
|
||||||
|
}
|
||||||
|
if (!ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate]) {
|
||||||
|
ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] = 0;
|
||||||
|
}
|
||||||
|
ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] =
|
||||||
|
ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] +
|
||||||
|
ticket.productivehrs;
|
||||||
|
//Add the rate
|
||||||
|
});
|
||||||
|
|
||||||
|
if (assignmentHash.unassigned > 0) {
|
||||||
|
res.json({ success: false, error: "Unassigned hours." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Calculate how much time each tech should have by labor type.
|
||||||
|
const comparison = compare(employeeHash, ticketHash);
|
||||||
|
|
||||||
|
const ticketsToInsert = [];
|
||||||
|
|
||||||
|
//Check the ones that are different first. Source of truth will be the employee hash.
|
||||||
|
comparison.different.forEach((differentKey) => {
|
||||||
|
const empVal = employeeHash[differentKey];
|
||||||
|
const ticketVal = ticketHash[differentKey];
|
||||||
|
|
||||||
|
ticketsToInsert.push({
|
||||||
|
jobid: job.id,
|
||||||
|
employeeid: differentKey.split(".")[0],
|
||||||
|
productivehrs: empVal - ticketVal,
|
||||||
|
rate: differentKey.split(".")[2],
|
||||||
|
memo: "Adjustment between expected and entered values. ",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
comparison.missing_from_first
|
||||||
|
.filter((differentKey) => differentKey.split(".").length == 3)
|
||||||
|
.forEach((differentKey) => {
|
||||||
|
const empVal = employeeHash[differentKey];
|
||||||
|
const ticketVal = ticketHash[differentKey];
|
||||||
|
|
||||||
|
ticketsToInsert.push({
|
||||||
|
jobid: job.id,
|
||||||
|
employeeid: differentKey.split(".")[0],
|
||||||
|
productivehrs: empVal - ticketVal * -1,
|
||||||
|
rate: differentKey.split(".")[2],
|
||||||
|
memo: "Entered ticket reversed to match system payroll.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
comparison.missing_from_second
|
||||||
|
.filter((differentKey) => differentKey.split(".").length == 3)
|
||||||
|
.forEach((differentKey) => {
|
||||||
|
const empVal = employeeHash[differentKey];
|
||||||
|
const ticketVal = ticketHash[differentKey];
|
||||||
|
|
||||||
|
ticketsToInsert.push({
|
||||||
|
jobid: job.id,
|
||||||
|
employeeid: differentKey.split(".")[0],
|
||||||
|
productivehrs: empVal - ticketVal * -1,
|
||||||
|
rate: differentKey.split(".")[2],
|
||||||
|
memo: "Entered ticket reversed to match system payroll.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
assignmentHash,
|
||||||
|
employeeHash,
|
||||||
|
diff: getObjectDiff(employeeHash, ticketHash),
|
||||||
|
compare: compare(employeeHash, ticketHash),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(
|
||||||
|
"job-payroll-labor-totals-error",
|
||||||
|
"ERROR",
|
||||||
|
req.user.email,
|
||||||
|
jobid,
|
||||||
|
{
|
||||||
|
jobid: jobid,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
res.status(503).send();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getObjectDiff(obj1, obj2) {
|
||||||
|
const diff = Object.keys(obj1).reduce((result, key) => {
|
||||||
|
if (!obj2.hasOwnProperty(key)) {
|
||||||
|
result.push(key);
|
||||||
|
} else if (_.isEqual(obj1[key], obj2[key])) {
|
||||||
|
const resultKeyIndex = result.indexOf(key);
|
||||||
|
result.splice(resultKeyIndex, 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, Object.keys(obj2));
|
||||||
|
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
var compare = function (a, b) {
|
||||||
|
var result = {
|
||||||
|
different: [],
|
||||||
|
missing_from_first: [],
|
||||||
|
missing_from_second: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
_.reduce(
|
||||||
|
a,
|
||||||
|
function (result, value, key) {
|
||||||
|
if (b.hasOwnProperty(key)) {
|
||||||
|
if (_.isEqual(value, b[key])) {
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
if (typeof a[key] != typeof {} || typeof b[key] != typeof {}) {
|
||||||
|
//dead end.
|
||||||
|
result.different.push(key);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
var deeper = compare(a[key], b[key]);
|
||||||
|
result.different = result.different.concat(
|
||||||
|
_.map(deeper.different, (sub_path) => {
|
||||||
|
return key + "." + sub_path;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
result.missing_from_second = result.missing_from_second.concat(
|
||||||
|
_.map(deeper.missing_from_second, (sub_path) => {
|
||||||
|
return key + "." + sub_path;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
result.missing_from_first = result.missing_from_first.concat(
|
||||||
|
_.map(deeper.missing_from_first, (sub_path) => {
|
||||||
|
return key + "." + sub_path;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.missing_from_second.push(key);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result
|
||||||
|
);
|
||||||
|
|
||||||
|
_.reduce(
|
||||||
|
b,
|
||||||
|
function (result, value, key) {
|
||||||
|
if (a.hasOwnProperty(key)) {
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
result.missing_from_first.push(key);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
2
server/payroll/payroll.js
Normal file
2
server/payroll/payroll.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
exports.calculateLaborTotals = require("./calculate-totals").calculateLaborTotals;
|
||||||
|
exports.payall = require("./pay-all").payall;
|
||||||
Reference in New Issue
Block a user