Merged in release/2021-08-20 (pull request #181)

release/2021-08-20

Approved-by: Patrick Fic
This commit is contained in:
Patrick Fic
2021-08-13 23:38:45 +00:00
52 changed files with 4576 additions and 981 deletions

View File

@@ -109,6 +109,17 @@ export default function AccountingPayablesTableComponent({ loading, bills }) {
<Checkbox disabled checked={record.is_credit_memo} />
),
},
{
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
},
{
title: t("general.labels.actions"),
dataIndex: "actions",

View File

@@ -108,7 +108,17 @@ export default function AccountingPayablesTableComponent({
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
),
},
{
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
},
{
title: t("general.labels.actions"),
dataIndex: "actions",

View File

@@ -114,11 +114,21 @@ export default function AccountingReceivablesTableComponent({ loading, jobs }) {
);
},
},
{
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => (
<Space wrap>

View File

@@ -72,9 +72,11 @@ export function BillEnterModalLinesComponent({
quantity: opt.part_qty || 1,
actual_price: opt.cost,
cost_center: opt.part_type
? responsibilityCenters.defaults.costs[
? responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[
opt.part_type
] || null
] ||
null)
: null,
};
}

View File

@@ -0,0 +1,83 @@
import { Button, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import Dinero from "dinero.js";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(DmsAllocationsSummary);
export function DmsAllocationsSummary({ socket, bodyshop, jobId }) {
const { t } = useTranslation();
const [allocationsSummary, setAllocationsSummary] = useState([]);
const columns = [
{
title: t("jobs.fields.dms.center"),
dataIndex: "center",
key: "center",
},
{
title: t("jobs.fields.dms.sale"),
dataIndex: "sale",
key: "sale",
render: (text, record) => Dinero(record.sale).toFormat(),
},
{
title: t("jobs.fields.dms.cost"),
dataIndex: "cost",
key: "cost",
render: (text, record) => Dinero(record.cost).toFormat(),
},
{
title: t("jobs.fields.dms.sale_dms_acctnumber"),
dataIndex: "sale_dms_acctnumber",
key: "sale_dms_acctnumber",
render: (text, record) =>
record.profitCenter && record.profitCenter.dms_acctnumber,
},
{
title: t("jobs.fields.dms.cost_dms_acctnumber"),
dataIndex: "cost_dms_acctnumber",
key: "cost_dms_acctnumber",
render: (text, record) =>
record.costCenter && record.costCenter.dms_acctnumber,
},
{
title: t("jobs.fields.dms.dms_wip_acctnumber"),
dataIndex: "dms_wip_acctnumber",
key: "dms_wip_acctnumber",
render: (text, record) =>
record.costCenter && record.costCenter.dms_wip_acctnumber,
},
];
return (
<Table
title={() => (
<Button
onClick={() => {
socket.emit("cdk-calculate-allocations", jobId, (ack) =>
setAllocationsSummary(ack)
);
}}
>
Get
</Button>
)}
pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns}
rowKey="center"
dataSource={allocationsSummary}
/>
);
}

View File

@@ -0,0 +1,115 @@
import React, { useState } from "react";
import { Modal, Button, Table, Input } from "antd";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsCdkMakes);
export function DmsCdkMakes({ bodyshop, form, socket }) {
const [makesList, setMakesList] = useState([]);
const [searchText, setSearchText] = useState("");
const [loading, setLoading] = useState(false);
const [visible, setVisible] = useState(false);
const [selectedModel, setSelectedModel] = useState(null);
const { t } = useTranslation();
const columns = [
{
title: t("jobs.fields.dms.makeFullName"),
dataIndex: "makeFullName",
key: "makeFullName",
},
{
title: t("jobs.fields.dms.modelFullName"),
dataIndex: "modelFullName",
key: "modelFullName",
},
{
title: t("jobs.fields.dms.makeCode"),
dataIndex: "makeCode",
key: "makeCode",
},
{
title: t("jobs.fields.dms.modelCode"),
dataIndex: "modelCode",
key: "modelCode",
},
];
const filteredMakes =
searchText !== "" && searchText
? makesList.filter(
(make) =>
searchText
.split(" ")
.some((v) =>
make.makeFullName.toLowerCase().includes(v.toLowerCase())
) ||
searchText
.split(" ")
.some((v) =>
make.modelFullName.toLowerCase().includes(v.toLowerCase())
)
)
: makesList;
return (
<div>
<Modal width={"90%"} visible={visible} onCancel={() => setVisible(false)}>
<Table
title={() => (
<Input.Search
onSearch={(val) => setSearchText(val)}
placeholder={t("general.labels.search")}
/>
)}
columns={columns}
loading={loading}
id="id"
dataSource={filteredMakes}
onRow={(record) => {
return {
onClick: setSelectedModel(record),
};
}}
rowSelection={{
onSelect: (record, selected, ...props) => {
console.log(
"🚀 ~ file: dms-cdk-makes.component.jsx ~ line 85 ~ record, selected, ...props",
record,
selected,
...props
);
setSelectedModel(record);
},
type: "radio",
selectedRowKeys: [selectedModel && selectedModel.id],
}}
/>
</Modal>
<Button
onClick={() => {
setVisible(true);
setLoading(true);
socket.emit("cdk-get-makes", bodyshop.cdk_dealerid, (makes) => {
console.log("Called back", makes);
setMakesList(makes);
setLoading(false);
});
}}
>
Get Makes
</Button>
</div>
);
}

View File

@@ -0,0 +1,77 @@
import { Button, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { socket } from "../../pages/dms/dms.container";
import PhoneFormatter from "../../utils/PhoneFormatter";
import { alphaSort } from "../../utils/sorters";
export default function DmsCustomerSelector() {
const { t } = useTranslation();
const [customerList, setcustomerList] = useState([]);
const [visible, setVisible] = useState(false);
const [selectedCustomer, setSelectedCustomer] = useState(null);
socket.on("cdk-select-customer", (customerList, callback) => {
setVisible(true);
setcustomerList(customerList);
});
const onOk = () => {
setVisible(false);
socket.emit("cdk-selected-customer", selectedCustomer);
};
const columns = [
{
title: t("dms.fields.name1"),
dataIndex: ["name1", "fullName"],
key: "name1",
sorter: (a, b) => alphaSort(a.name1?.fullName, b.name1?.fullName),
},
{
title: t("dms.fields.name2"),
dataIndex: ["name2", "fullName"],
key: "name2",
sorter: (a, b) => alphaSort(a.name2?.fullName, b.name2?.fullName),
},
{
title: t("dms.fields.phone"),
dataIndex: ["contactInfo", "mainTelephoneNumber", "value"],
key: "phone",
render: (record, value) => (
<PhoneFormatter>
{record.contactInfo?.mainTelephoneNumber?.value}
</PhoneFormatter>
),
},
{
title: t("dms.fields.address"),
//dataIndex: ["name2", "fullName"],
key: "address",
render: (record, value) =>
`${record.address?.addressLine[0]}, ${record.address?.city} ${record.address?.stateOrProvince} ${record.address?.postalCode}`,
},
];
if (!visible) return <></>;
return (
<Table
title={() => (
<div>
<Button onClick={onOk}>Select</Button>
</div>
)}
pagination={{ position: "top" }}
columns={columns}
rowKey={(record) => record.id.value}
dataSource={customerList}
//onChange={handleTableChange}
rowSelection={{
onSelect: (props) => {
setSelectedCustomer(props.id.value);
},
type: "radio",
selectedRowKeys: [selectedCustomer],
}}
/>
);
}

View File

@@ -0,0 +1,55 @@
import { Divider, Space, Tag, Timeline } from "antd";
import moment from "moment";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents);
export function DmsLogEvents({ socket, logs, bodyshop }) {
return (
<Timeline pending reverse={true}>
{logs.map((log, idx) => (
<Timeline.Item key={idx} color={LogLevelHierarchy(log.level)}>
<Space wrap align="start" style={{}}>
<Tag color={LogLevelHierarchy(log.level)}>{log.level}</Tag>
<span>{moment(log.timestamp).format("MM/DD/YYYY HH:MM:ss")}</span>
<Divider type="vertical" />
<span>{log.message}</span>
</Space>
</Timeline.Item>
))}
</Timeline>
);
}
function LogLevelHierarchy(level) {
switch (level) {
case "TRACE":
return "pink";
case "DEBUG":
return "orange";
case "INFO":
return "blue";
case "WARNING":
return "yellow";
case "ERROR":
return "red";
default:
return 0;
}
}

View File

@@ -0,0 +1,150 @@
import { DeleteFilled } from "@ant-design/icons";
import { Button, Form, Input } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
export function DmsPostForm({ bodyshop, socket, jobId }) {
const [form] = Form.useForm();
const { t } = useTranslation();
return (
<Form form={form} layout="vertical">
<LayoutFormRow>
<Form.Item
name="journal"
label={t("jobs.fields.dms.journal")}
initialValue={
bodyshop.cdk_configuration &&
bodyshop.cdk_configuration.default_journal
}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="dms_make"
label={t("jobs.fields.dms.dms_make")}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="dms_make"
label={t("jobs.fields.dms.dms_make")}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<DmsCdkMakes form={form} socket={socket} />
</LayoutFormRow>
<Form.List name={["payers"]}>
{(fields, { add, remove }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<LayoutFormRow>
<Form.Item
label={t("jobs.fields.dms.payer.name")}
key={`${index}name`}
name={[field.name, "name"]}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.account")}
key={`${index}account`}
name={[field.name, "account"]}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.amount")}
key={`${index}amount`}
name={[field.name, "amount"]}
rules={[
{
required: true,
},
]}
>
<CurrencyInput />
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.controlnumber")}
key={`${index}controlnumber`}
name={[field.name, "controlnumber"]}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
</LayoutFormRow>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</Form>
);
}

View File

@@ -72,7 +72,7 @@ export function JobLineStatusPopup({ bodyshop, jobline, disabled }) {
);
return (
<div
style={{ width: "100%", minHeight: "2rem", cursor: "pointer" }}
style={{ width: "100%", minHeight: "1rem", cursor: "pointer" }}
onClick={() => !disabled && setEditing(true)}
>
{jobline.status}

View File

@@ -1,4 +1,4 @@
import { Button } from "antd";
import { Button, Dropdown, Menu } from "antd";
import _ from "lodash";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -12,11 +12,8 @@ const mapStateToProps = createStructuredSelector({
export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
const { t } = useTranslation();
const handleAllocate = () => {
logImEXEvent("jobs_close_allocate_auto");
const { defaults } = bodyshop.md_responsibility_centers;
const handleAllocate = (defaults) => {
form.setFieldsValue({
joblines: joblines.map((jl) => {
const ret = _.cloneDeep(jl);
@@ -48,8 +45,36 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
});
};
return (
<Button onClick={handleAllocate} disabled={disabled}>
const handleAutoAllocateClick = () => {
logImEXEvent("jobs_close_allocate_auto");
const { defaults } = bodyshop.md_responsibility_centers;
handleAllocate(defaults);
};
const handleMenuClick = ({ item, key, keyPath, domEvent }) => {
logImEXEvent("jobs_close_allocate_auto_dms");
handleAllocate(
bodyshop.md_responsibility_centers.dms_defaults.find(
(x) => x.name === key
)
);
};
const overlay = bodyshop.cdk_dealerid && (
<Menu onClick={handleMenuClick}>
{bodyshop.md_responsibility_centers.dms_defaults.map((mapping) => (
<Menu.Item key={mapping.name}>{mapping.name}</Menu.Item>
))}
</Menu>
);
return bodyshop.cdk_dealerid ? (
<Dropdown overlay={overlay}>
<Button disabled={disabled}>{t("jobs.actions.dmsautoallocate")}</Button>
</Dropdown>
) : (
<Button onClick={handleAutoAllocateClick} disabled={disabled}>
{t("jobs.actions.autoallocate")}
</Button>
);

View File

@@ -10,7 +10,7 @@ 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 _ from "lodash";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
@@ -113,7 +113,7 @@ export function LaborAllocationsTable({
color: record.difference >= 0 ? "green" : "red",
}}
>
{record.difference}
{_.round(record.difference, 1)}
</strong>
),
},

View File

@@ -17,7 +17,7 @@ import {
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
import _ from "lodash";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
@@ -52,17 +52,20 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
const theValue = {
date: moment(val).format("D dd"),
paintHrs: dayhrs.painthrs,
bodyHrs: dayhrs.bodyhrs,
accTargetHrs: Utils.AsOfDateTargetHours(
bodyshop.scoreboard_target.dailyBodyTarget +
bodyshop.scoreboard_target.dailyPaintTarget,
val
paintHrs: _.round(dayhrs.painthrs, 1),
bodyHrs: _.round(dayhrs.bodyhrs),
accTargetHrs: _.round(
Utils.AsOfDateTargetHours(
bodyshop.scoreboard_target.dailyBodyTarget +
bodyshop.scoreboard_target.dailyPaintTarget,
val
)
),
accHrs:
accHrs: _.round(
acc.length > 0
? acc[acc.length - 1].accHrs + dayhrs.painthrs + dayhrs.bodyhrs
: dayhrs.painthrs + dayhrs.bodyhrs,
: dayhrs.painthrs + dayhrs.bodyhrs
),
};
return [...acc, theValue];

View File

@@ -1,6 +1,9 @@
import { Button, Card, Tabs } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ShopInfoGeneral from "./shop-info.general.component";
import ShopInfoIntakeChecklistComponent from "./shop-info.intake.component";
import ShopInfoLaborRates from "./shop-info.laborrates.component";
@@ -11,7 +14,15 @@ import ShopInfoROStatusComponent from "./shop-info.rostatus.component";
import ShopInfoSchedulingComponent from "./shop-info.scheduling.component";
import ShopInfoSpeedPrint from "./shop-info.speedprint.component";
export default function ShopInfoComponent({ form, saveLoading }) {
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent);
export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
const { t } = useTranslation();
return (
<Card
@@ -53,6 +64,7 @@ export default function ShopInfoComponent({ form, saveLoading }) {
>
<ShopInfoResponsibilityCenterComponent form={form} />
</Tabs.TabPane>
<Tabs.TabPane key="checklists" tab={t("bodyshop.labels.checklists")}>
<ShopInfoIntakeChecklistComponent form={form} />
</Tabs.TabPane>

View File

@@ -17,7 +17,7 @@ import RbacWrapper, {
} from "../rbac-wrapper/rbac-wrapper.component";
import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
import { Link } from "react-router-dom";
import _ from "lodash";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
authLevel: selectAuthLevel,
@@ -268,8 +268,12 @@ export function TimeTicketList({
<Table.Summary.Cell />
<Table.Summary.Cell />
<Table.Summary.Cell />
<Table.Summary.Cell>{totals.productivehrs}</Table.Summary.Cell>
<Table.Summary.Cell>{totals.actualhrs}</Table.Summary.Cell>
<Table.Summary.Cell>
{_.round(totals.productivehrs, 1)}
</Table.Summary.Cell>
<Table.Summary.Cell>
{_.round(totals.actualhrs, 1)}
</Table.Summary.Cell>
<Table.Summary.Cell>
{totals.actualhrs === 0 || !totals.actualhrs
? "∞"

View File

@@ -48,7 +48,9 @@ export function TimeTicketModalComponent({
{emps &&
emps.rates.map((item) => (
<Select.Option key={item.cost_center}>
{item.cost_center}
{item.cost_center === "timetickets.labels.shift"
? t(item.cost_center)
: item.cost_center}
</Select.Option>
))}
</Select>

View File

@@ -24,6 +24,10 @@ export const QUERY_JOBS_FOR_EXPORT = gql`
clm_total
clm_no
ins_co_nm
exportlogs {
id
successful
}
}
}
`;
@@ -37,6 +41,10 @@ export const QUERY_BILLS_FOR_EXPORT = gql`
invoice_number
is_credit_memo
total
exportlogs {
id
successful
}
job {
id
ro_number
@@ -73,6 +81,10 @@ export const QUERY_PAYMENTS_FOR_EXPORT = gql`
transactionid
paymentnum
date
exportlogs {
id
successful
}
}
}
`;

View File

@@ -93,6 +93,7 @@ export const QUERY_BODYSHOP = gql`
features
attach_pdf_to_email
tt_allow_post_to_invoiced
cdk_configuration
employees {
id
active
@@ -182,6 +183,7 @@ export const UPDATE_SHOP = gql`
cdk_dealerid
attach_pdf_to_email
tt_allow_post_to_invoiced
cdk_configuration
employees {
id
first_name

View File

@@ -1875,3 +1875,100 @@ export const FIND_JOBS_BY_CLAIM = gql`
}
}
`;
export const QUERY_JOB_EXPORT_DMS = gql`
query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
jobs_by_pk(id: $id) {
ro_number
invoice_allocation
ins_co_id
id
ded_amt
ded_status
depreciation_taxes
other_amount_payable
towing_payable
storage_payable
adjustment_bottom_line
federal_tax_rate
state_tax_rate
local_tax_rate
tax_tow_rt
tax_str_rt
tax_paint_mat_rt
tax_sub_rt
tax_lbr_rt
tax_levies_rt
parts_tax_rates
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
date_exported
date_invoiced
voided
scheduled_completion
actual_completion
scheduled_delivery
actual_delivery
scheduled_in
actual_in
bills {
id
federal_tax_rate
local_tax_rate
state_tax_rate
is_credit_memo
billlines {
actual_cost
cost_center
id
quantity
}
}
joblines(where: { removed: { _eq: false } }) {
id
removed
tax_part
line_desc
prt_dsmk_p
prt_dsmk_m
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
profitcenter_labor
profitcenter_part
prt_dsmk_p
}
}
}
`;

View File

@@ -2,7 +2,10 @@ import { gql } from "@apollo/client";
export const SUBSCRIPTION_SCOREBOARD = gql`
subscription SUBSCRIPTION_SCOREBOARD($start: date!, $end: date!) {
scoreboard(where: { _and: { date: { _gte: $start, _lte: $end } } }) {
scoreboard(
where: { _and: { date: { _gte: $start, _lte: $end } } }
order_by: { date: asc }
) {
id
painthrs
bodyhrs

View File

@@ -1,16 +1,25 @@
import { Result, Timeline, Space, Tag, Divider, Button } from "antd";
//import { useQuery } from "@apollo/client";
import { Button, Col, Result, Row, Select, Space } from "antd";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import SocketIO from "socket.io-client";
//import AlertComponent from "../../components/alert/alert.component";
import DmsAllocationsSummary from "../../components/dms-allocations-summary/dms-allocations-summary.component";
import DmsCustomerSelector from "../../components/dms-customer-selector/dms-customer-selector.component";
import DmsLogEvents from "../../components/dms-log-events/dms-log-events.component";
import DmsPostForm from "../../components/dms-post-form/dms-post-form.component";
//import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { auth } from "../../firebase/firebase.utils";
//import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
import SocketIO from "socket.io-client";
import { auth } from "../../firebase/firebase.utils";
import moment from "moment";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -38,7 +47,15 @@ export const socket = SocketIO(
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation();
const [logLevel, setLogLevel] = useState("TRACE");
const [logs, setLogs] = useState([]);
const search = queryString.parse(useLocation().search);
const { jobId } = search;
// const { loading, error } = useQuery(QUERY_JOB_EXPORT_DMS, {
// variables: { id: jobId },
// skip: true, //!jobId,
// });
useEffect(() => {
document.title = t("titles.dms");
@@ -55,6 +72,19 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
socket.on("connected", () => {
console.log("Connected again.");
});
socket.on("reconnect", () => {
console.log("Connected again.");
setLogs((logs) => {
return [
...logs,
{
timestamp: new Date(),
level: "WARNING",
message: "Reconnected to CDK Export Service",
},
];
});
});
socket.on("log-event", (payload) => {
setLogs((logs) => {
@@ -63,74 +93,68 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
});
socket.connect();
socket.emit("set-log-level", "TRACE");
socket.emit("set-log-level", logLevel);
return () => {
socket.removeAllListeners();
socket.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!bodyshop.cdk_dealerid) return <Result status="404" />;
if (!jobId || !bodyshop.cdk_dealerid) return <Result status="404" />;
const dmsType = determineDmsType(bodyshop);
// if (loading) return <LoadingSpinner />;
// if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
<Button
onClick={() => {
socket.emit(
`${dmsType}-export-job`,
"752a4f5f-22ab-414b-b182-98d4e62227ef"
);
}}
>
Export
</Button>
<Space>
<Button
onClick={() => {
socket.emit(
`${dmsType}-export-job`,
"752a4f5f-22ab-414b-b182-98d4e62227ef"
);
}}
>
Export
</Button>
<Select
placeholder="Log Level"
value={logLevel}
onChange={(value) => {
setLogLevel(value);
socket.emit("set-log-level", value);
}}
>
<Select.Option key="TRACE">TRACE</Select.Option>
<Select.Option key="DEBUG">DEBUG</Select.Option>
<Select.Option key="INFO">INFO</Select.Option>
<Select.Option key="WARNING">WARNING</Select.Option>
<Select.Option key="ERROR">ERROR</Select.Option>
</Select>
<Button onClick={() => setLogs([])}>Clear Logs</Button>
</Space>
<Row gutter={32}>
<Col span={18}>
<DmsAllocationsSummary socket={socket} jobId={jobId} />
<DmsPostForm socket={socket} jobId={jobId} />
</Col>
<Col span={6}>
<div style={{ maxHeight: "500px", overflowY: "auto" }}>
<DmsLogEvents socket={socket} logs={logs} />
</div>
</Col>
</Row>
<Button
onClick={() => {
setLogs([]);
socket.disconnect();
socket.connect();
}}
>
reconnect
</Button>
<Timeline pending={socket.connected && "Processing..."} reverse={true}>
{logs.map((log, idx) => (
<Timeline.Item key={idx} color={LogLevelHierarchy(log.level)}>
<Space wrap align="start" style={{}}>
<Tag color={LogLevelHierarchy(log.level)}>{log.level}</Tag>
<span>{moment(log.timestamp).format("MM/DD/YYYY HH:MM:ss")}</span>
<Divider type="vertical" />
<span>{log.message}</span>
</Space>
</Timeline.Item>
))}
</Timeline>
<DmsCustomerSelector />
</div>
);
}
function LogLevelHierarchy(level) {
switch (level) {
case "TRACE":
return "pink";
case "DEBUG":
return "orange";
case "INFO":
return "blue";
case "WARNING":
return "yellow";
case "ERROR":
return "red";
default:
return 0;
}
}
const determineDmsType = (bodyshop) => {
if (bodyshop.cdk_dealerid) return "cdk";
else {

View File

@@ -1,6 +0,0 @@
import DmsActions from "./dms.types";
export const endLoading = (options) => ({
// type: DmsActions.END_LOADING,
payload: options,
});

View File

@@ -1,20 +0,0 @@
import DmsActionTypes from "./dms.types";
const INITIAL_STATE = {
eventLog: [],
};
const dmsReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
// case ApplicationActionTypes.SET_SELECTED_HEADER:
// return {
// ...state,
// selectedHeader: action.payload,
// };
default:
return state;
}
};
export default dmsReducer;

View File

@@ -1,14 +0,0 @@
import { all, call } from "redux-saga/effects";
//import DmsActionTypes from "./dms.types";
export function* onCalculateScheduleLoad() {
// yield takeLatest(
// DmsActionTypes.CALCULATE_SCHEDULE_LOAD,
// calculateScheduleLoad
// );
}
export function* calculateScheduleLoad({ payload: end }) {}
export function* dmsSagas() {
yield all([call()]);
}

View File

@@ -1,8 +0,0 @@
import { createSelector } from "reselect";
const selectDms = (state) => state.dms;
export const selectEventLog = createSelector(
[selectDms],
(dms) => dms.eventLog
);

View File

@@ -1,4 +0,0 @@
const DmsActionTypes = {
ADD_EVENT: "ADD_EVENT",
};
export default DmsActionTypes;

View File

@@ -231,6 +231,12 @@
"deliver": {
"templates": "Delivery Templates"
},
"dms": {
"default_journal": "Default Journal",
"dms_acctnumber": "DMS Account #",
"dms_wip_acctnumber": "DMS W.I.P. Account #",
"mappingname": "DMS Mapping Name"
},
"email": "General Shop Email",
"enforce_class": "Enforce Class on Conversion?",
"enforce_referral": "Enforce Referrals",
@@ -467,6 +473,10 @@
"defaultcostsmapping": "Default Costs Mapping",
"defaultprofitsmapping": "Default Profits Mapping",
"deliverchecklist": "Delivery Checklist",
"dms": {
"cdk_dealerid": "CDK Dealer ID",
"title": "DMS"
},
"employees": "Employees",
"insurancecos": "Insurance Companies",
"intakechecklist": "Intake Checklist",
@@ -815,6 +825,9 @@
"exportlogs": {
"fields": {
"createdat": "Created At"
},
"labels": {
"attempts": "Export Attempts"
}
},
"general": {
@@ -1051,6 +1064,7 @@
"changestatus": "Change Status",
"convert": "Convert",
"deliver": "Deliver",
"dmsautoallocate": "DMS Auto Allocate",
"export": "Export",
"exportcustdata": "Export Customer Data",
"exportselected": "Export Selected",
@@ -1130,6 +1144,14 @@
"ded_amt": "Deductible",
"ded_status": "Deductible Status",
"depreciation_taxes": "Depreciation/Taxes",
"dms": {
"center": "Center",
"cost": "Cost",
"cost_dms_acctnumber": "Cost DMS Acct #",
"dms_wip_acctnumber": "Cost WIP DMS Acct #",
"sale": "Sale",
"sale_dms_acctnumber": "Sale DMS Acct #"
},
"driveable": "Driveable",
"employee_body": "Body",
"employee_csr": "Customer Service Rep.",
@@ -2090,6 +2112,11 @@
"updated": "Scoreboard updated."
}
},
"scoredboard": {
"successes": {
"updated": "Scoreboard entry updated."
}
},
"tech": {
"fields": {
"employeeid": "Employee ID",

View File

@@ -231,6 +231,12 @@
"deliver": {
"templates": ""
},
"dms": {
"default_journal": "",
"dms_acctnumber": "",
"dms_wip_acctnumber": "",
"mappingname": ""
},
"email": "",
"enforce_class": "",
"enforce_referral": "",
@@ -467,6 +473,10 @@
"defaultcostsmapping": "",
"defaultprofitsmapping": "",
"deliverchecklist": "",
"dms": {
"cdk_dealerid": "",
"title": ""
},
"employees": "",
"insurancecos": "",
"intakechecklist": "",
@@ -815,6 +825,9 @@
"exportlogs": {
"fields": {
"createdat": ""
},
"labels": {
"attempts": ""
}
},
"general": {
@@ -1051,6 +1064,7 @@
"changestatus": "Cambiar Estado",
"convert": "Convertir",
"deliver": "",
"dmsautoallocate": "",
"export": "",
"exportcustdata": "",
"exportselected": "",
@@ -1130,6 +1144,14 @@
"ded_amt": "Deducible",
"ded_status": "Estado deducible",
"depreciation_taxes": "Depreciación / Impuestos",
"dms": {
"center": "",
"cost": "",
"cost_dms_acctnumber": "",
"dms_wip_acctnumber": "",
"sale": "",
"sale_dms_acctnumber": ""
},
"driveable": "",
"employee_body": "",
"employee_csr": "Representante de servicio al cliente.",
@@ -2090,6 +2112,11 @@
"updated": ""
}
},
"scoredboard": {
"successes": {
"updated": ""
}
},
"tech": {
"fields": {
"employeeid": "",

View File

@@ -231,6 +231,12 @@
"deliver": {
"templates": ""
},
"dms": {
"default_journal": "",
"dms_acctnumber": "",
"dms_wip_acctnumber": "",
"mappingname": ""
},
"email": "",
"enforce_class": "",
"enforce_referral": "",
@@ -467,6 +473,10 @@
"defaultcostsmapping": "",
"defaultprofitsmapping": "",
"deliverchecklist": "",
"dms": {
"cdk_dealerid": "",
"title": ""
},
"employees": "",
"insurancecos": "",
"intakechecklist": "",
@@ -815,6 +825,9 @@
"exportlogs": {
"fields": {
"createdat": ""
},
"labels": {
"attempts": ""
}
},
"general": {
@@ -1051,6 +1064,7 @@
"changestatus": "Changer le statut",
"convert": "Convertir",
"deliver": "",
"dmsautoallocate": "",
"export": "",
"exportcustdata": "",
"exportselected": "",
@@ -1130,6 +1144,14 @@
"ded_amt": "Déductible",
"ded_status": "Statut de franchise",
"depreciation_taxes": "Amortissement / taxes",
"dms": {
"center": "",
"cost": "",
"cost_dms_acctnumber": "",
"dms_wip_acctnumber": "",
"sale": "",
"sale_dms_acctnumber": ""
},
"driveable": "",
"employee_body": "",
"employee_csr": "représentant du service à la clientèle",
@@ -2090,6 +2112,11 @@
"updated": ""
}
},
"scoredboard": {
"successes": {
"updated": ""
}
},
"tech": {
"fields": {
"employeeid": "",