Compare commits

..

67 Commits

Author SHA1 Message Date
Allan Carr
6319fd20fa IO-2407 In House Invoice Audit Log correction for Invoice Number 2023-09-20 12:16:03 -07:00
Patrick Fic
64851047bf Merged in release/2023-09-15 (pull request #980)
Release/2023 09 15
2023-09-15 17:40:34 +00:00
Patrick Fic
73fac34ef4 Merge branch 'feature/intellipay' into release/2023-09-15 2023-09-15 10:38:51 -07:00
Allan Carr
fcfa1a9be8 Merged in feature/IO-2395-Payment-Expansion-Formating (pull request #976)
IO-2395 Adjust Payment Number Label
2023-09-15 16:14:16 +00:00
Allan Carr
41849644f3 IO-2395 Adjust Payment Number Label 2023-09-15 09:15:34 -07:00
Allan Carr
f36fb06dd6 Merged in feature/IO-2368-QBO-Successful-Export (pull request #972)
IO-2368 Confirm that there are successful transaction
2023-09-15 16:04:15 +00:00
Allan Carr
af4c4a4fa3 Merged in feature/IO-2395-Payment-Expansion-Formating (pull request #973)
IO-2395 Payment Expansion Formatting
2023-09-15 16:03:59 +00:00
Patrick Fic
d6045a9334 Merge branch 'feature/IO-2399-import-status' into release/2023-09-15 2023-09-14 11:19:05 -07:00
Patrick Fic
dcc29f23d4 Respect default import status on new creation. 2023-09-14 11:18:40 -07:00
Patrick Fic
638a9fc76b Add CIECA PFT to job. 2023-09-11 11:45:35 -07:00
Allan Carr
53e3b3fa03 IO-2395 Payment Expansion Formatting 2023-09-11 11:15:50 -07:00
Allan Carr
56c1b6f992 IO-2368 Confirm that there are successful transaction 2023-09-11 09:07:38 -07:00
Allan Carr
22f9a7ee3d Merged in release/2023-09-01 (pull request #971)
Release/2023 09 01
2023-09-08 20:50:50 +00:00
Allan Carr
cfcad472fd Merged in feature/IO-2391-IP-Address-for-Check (pull request #969)
IO-2391 Remote IP comment out till proxy for X-Forwarded-For can be figured out from AWS Load balancer
2023-09-08 20:17:19 +00:00
Allan Carr
1a622f1b2c IO-2391 Remote IP comment out till proxy for X-Forwarded-For can be figured out from AWS Load balancer 2023-09-08 13:18:20 -07:00
Allan Carr
87e3adf579 IO-2368 Move Cache update to function and just pass in keys array 2023-09-08 10:17:42 -07:00
Allan Carr
aa7a4ccdd0 Merged in feature/IO-2368-QBO-Successful-Export (pull request #967)
IO-2368 Move Cache update to function and just pass in keys array
2023-09-08 17:16:50 +00:00
Allan Carr
28dc10f5a1 Merged in feature/IO-2368-QBO-Successful-Export (pull request #966)
IO-2368 Update Cache on success
2023-09-08 00:23:57 +00:00
Allan Carr
b2d615b9c1 Merged in feature/IO-2392-Ticket-Date-UTCOffset (pull request #965)
IO-2392 Ticket Date UTCOffset
2023-09-08 00:22:28 +00:00
Allan Carr
1e40a22762 IO-2392 Ticket Date UTCOffset 2023-09-07 17:23:08 -07:00
Allan Carr
d1407162d9 IO-2368 Update Cache on success 2023-09-07 17:21:21 -07:00
Allan Carr
7a1984d037 IO-2368 Update Cache on QBD posting success 2023-09-06 16:23:58 -07:00
Allan Carr
9bcc449f20 Merged in feature/IO-2368-QBO-Successful-Export (pull request #964)
IO-2368 Update Cache on QBD posting success
2023-09-06 23:23:53 +00:00
Allan Carr
bc7d0ef171 IO-2391 Add IP address to Server API Check 2023-09-06 09:06:05 -07:00
Allan Carr
3e9b046476 Merged in feature/IO-2391-IP-Address-for-Check (pull request #963)
IO-2391 Add IP address to Server API Check
2023-09-06 16:05:57 +00:00
Allan Carr
e0ccd62c82 Merged in feature/IO-1559-ClaimsCorp-Datapump (pull request #956)
IO-1559 ClaimsCorp Datapump
2023-09-01 15:47:25 +00:00
Allan Carr
fd0970aef2 Merged in feature/IO-2365-Correct-LAU-Transation (pull request #961)
IO-2365 LAU Translation adjust from Detail to User Defined
2023-09-01 15:47:08 +00:00
Allan Carr
b673bcae7a IO-2365 LAU Translation adjust from Detail to User Defined 2023-09-01 08:46:52 -07:00
Allan Carr
63673548a0 IO-1559 ClaimsCorp Datapump 2023-08-29 17:51:34 -07:00
Patrick Fic
29b74a8c0e Resolve issue when selecting lines. 2023-08-29 14:12:38 -07:00
Patrick Fic
2658626c7e Add CIECA PFL info. 2023-08-28 12:05:36 -07:00
Allan Carr
763b199646 Merged in release/2023-08-25 (pull request #954)
Release/2023 08 25
2023-08-25 19:49:35 +00:00
Allan Carr
17905fa844 Merged in feature/IO-2389-Add-Metadata-of-Transaction-Wip (pull request #953)
IO-2389 Metadata to ExportLog
2023-08-25 18:05:01 +00:00
Allan Carr
74a0b78a71 IO-2389 Metadata to ExportLog 2023-08-25 11:05:16 -07:00
Allan Carr
797a423702 Merged in feature/IO-2368-QBO-Successful-Export (pull request #945)
IO-2368 Successful Export Notification for QBO
2023-08-25 17:24:38 +00:00
Allan Carr
2fce8c9644 Merged in feature/IO-2385-Tech-Console-Attendance-Report (pull request #946)
IO-2385 Enabled Emailing of TimeTicket & Attendance from Tech Console
2023-08-25 17:24:23 +00:00
Patrick Fic
9cd39c1c3e Resolve manual appointment old data after submission. 2023-08-25 10:23:22 -07:00
Patrick Fic
6264a2f45c Merge branch 'feature/intellipay' into release/2023-08-25 2023-08-23 13:54:39 -07:00
Allan Carr
9319f492dd IO-2385 Enabled Emailing of TimeTicket & Attendance from Tech Console
Modify print center so that it will/wont display email option if technician is set and production board detail so that it wont display the remove from production / add to scoreboard if technician is set
2023-08-22 13:29:28 -07:00
Allan Carr
8f04c5a12c IO-2368 Successful Export Notification for QBO 2023-08-22 09:15:10 -07:00
Allan Carr
436a41405d Merged in release/2023-08-18 (pull request #944)
Release/2023 08 18
2023-08-18 21:41:04 +00:00
Allan Carr
a2150009db IO-2385 Tech Console to print Attendance Report from Shift Clock 2023-08-18 13:59:42 -07:00
Allan Carr
e1c785322f Merged in feature/IO-2385-Tech-Console-Attendance-Report (pull request #943)
IO-2385 Tech Console to print Attendance Report from Shift Clock
2023-08-18 20:59:23 +00:00
John Allen Delos Reyes
c1d71720ab Merged in feature/IO-2380-parts-order-receive-modal (pull request #934)
Feature/IO-2380 parts order receive modal

Approved-by: Patrick Fic
2023-08-18 18:16:04 +00:00
Allan Carr
89ff7740e2 Merged in feature/IO-2325-Ticket-Create-By (pull request #942)
IO-2325 Add in Created by from Action menu
2023-08-18 16:02:56 +00:00
Allan Carr
4e69fe819e IO-2325 Add in Created by from Action menu 2023-08-18 09:03:13 -07:00
Allan Carr
a8cc3fa190 Merged in feature/IO-2384-Owner-Related-Job-Default-Sort-Order (pull request #939)
IO-2384 Default Sort Order for Related Jobs
2023-08-17 23:19:03 +00:00
Allan Carr
10fceb7ddf IO-2384 Extend to Vehicle Related ROs 2023-08-17 16:18:56 -07:00
Allan Carr
6c1a0cff8d IO-2384 Default Sort Order for Related Jobs 2023-08-17 16:05:33 -07:00
Allan Carr
d92d2cca9a Merged in feature/IO-2247-Dashboard-Components (pull request #938)
IO-2247 Don't push manual appointments into component if they don't have a job associated.
2023-08-17 16:32:37 +00:00
Allan Carr
ea54820bc0 IO-2247 Don't push manual appointments into component if they don't have a job associated. 2023-08-17 09:32:32 -07:00
Allan Carr
62a800a2c0 Merged in feature/IO-2381-Disabled-Cancel-Appt-Button (pull request #937)
IO-2381 Disabled Cancel Appt Button
2023-08-17 01:37:20 +00:00
Allan Carr
9c408d8bf5 IO-2381 Disabled Cancel Appt Button 2023-08-16 18:37:19 -07:00
Allan Carr
45ad09c100 Merged in feature/IO-2383-Timeticket-Employee-Rangefilter (pull request #935)
IO-2383 TimeTicket Employee Range Filter
2023-08-17 00:25:11 +00:00
Allan Carr
dd5cafcd42 IO-2383 TimeTicket Employee Range Filter 2023-08-16 17:25:32 -07:00
swtmply
ddd816e7ca IO-2380 added translation to fields 2023-08-17 06:15:46 +08:00
swtmply
d646e5f285 IO-2380 added part number and price 2023-08-16 11:43:21 +08:00
Allan Carr
ba683a2e8a Merged in release/2023-08-11 (pull request #931)
Release/2023 08 11
2023-08-11 20:36:11 +00:00
Allan Carr
5209c12b89 Merged in feature/IO-2325-Ticket-Create-By (pull request #928)
IO-2325 Shift Clock Created By
2023-08-11 15:36:06 +00:00
Allan Carr
bf7aa17f65 IO-2325 Shift Clock Created By 2023-08-11 08:35:57 -07:00
Allan Carr
cd6e0dcde3 Merged in feature/IO-2325-Ticket-Create-By (pull request #926)
IO-2325 Time Ticket Creation Date
2023-08-10 19:06:58 +00:00
Allan Carr
a2822f5592 Merged in feature/IO-2376-Inactive-Employee-Login (pull request #925)
IO-2376 Prevent Inactive Employee from logging in
2023-08-10 19:06:49 +00:00
Allan Carr
ca129fa4a0 IO-2325 Time Ticket Creation Date 2023-08-10 12:06:47 -07:00
Allan Carr
eee135f4ef IO-2376 Prevent Inactive Employee from logging in 2023-08-09 18:01:28 -07:00
Allan Carr
de92b2d47e Merged in feature/IO-2375-Job-Costing-by-CSR-Filter-Label (pull request #924)
IO-2375 Filter label for Job Costing by CSR correction
2023-08-09 22:26:09 +00:00
Allan Carr
5d7384aa8b IO-2375 Filter label for Job Costing by CSR correction 2023-08-09 15:25:58 -07:00
Patrick Fic
bf18e687da Schema updates for parts dispatch. 2023-08-04 12:36:08 -07:00
61 changed files with 1893 additions and 281 deletions

View File

@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")), toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, billid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, billid, operation })),
}); });
const Templates = TemplateList("job_special"); const Templates = TemplateList("job_special");
@@ -316,7 +316,7 @@ function BillEnterModalContainer({
insertAuditTrail({ insertAuditTrail({
jobid: values.jobid, jobid: values.jobid,
billid: billId, billid: billId,
operation: AuditTrailMapping.billposted(remainingValues.invoice_number), operation: AuditTrailMapping.billposted(r1.data.insert_bills.returning[0].invoice_number),
}); });
if (enterAgain) { if (enterAgain) {

View File

@@ -23,36 +23,40 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
const appt = []; // Flatten Data const appt = []; // Flatten Data
data.scheduled_in_today.forEach((item) => { data.scheduled_in_today.forEach((item) => {
var i = { if (item.job) {
canceled: item.canceled, var i = {
id: item.id, canceled: item.canceled,
alt_transport: item.job.alt_transport, id: item.id,
clm_no: item.job.clm_no, alt_transport: item.job.alt_transport,
jobid: item.job.jobid, clm_no: item.job.clm_no,
ins_co_nm: item.job.ins_co_nm, jobid: item.job.jobid,
iouparent: item.job.iouparent, ins_co_nm: item.job.ins_co_nm,
ownerid: item.job.ownerid, iouparent: item.job.iouparent,
ownr_co_nm: item.job.ownr_co_nm, ownerid: item.job.ownerid,
ownr_ea: item.job.ownr_ea, ownr_co_nm: item.job.ownr_co_nm,
ownr_fn: item.job.ownr_fn, ownr_ea: item.job.ownr_ea,
ownr_ln: item.job.ownr_ln, ownr_fn: item.job.ownr_fn,
ownr_ph1: item.job.ownr_ph1, ownr_ln: item.job.ownr_ln,
ownr_ph2: item.job.ownr_ph2, ownr_ph1: item.job.ownr_ph1,
production_vars: item.job.production_vars, ownr_ph2: item.job.ownr_ph2,
ro_number: item.job.ro_number, production_vars: item.job.production_vars,
suspended: item.job.suspended, ro_number: item.job.ro_number,
v_make_desc: item.job.v_make_desc, suspended: item.job.suspended,
v_model_desc: item.job.v_model_desc, v_make_desc: item.job.v_make_desc,
v_model_yr: item.job.v_model_yr, v_model_desc: item.job.v_model_desc,
v_vin: item.job.v_vin, v_model_yr: item.job.v_model_yr,
vehicleid: item.job.vehicleid, v_vin: item.job.v_vin,
note: item.note, vehicleid: item.job.vehicleid,
start: moment(item.start).format("hh:mm a"), note: item.note,
title: item.title, start: moment(item.start).format("hh:mm a"),
}; title: item.title,
appt.push(i); };
appt.push(i);
}
});
appt.sort(function (a, b) {
return new moment(a.start) - new moment(b.start);
}); });
appt.sort ( function (a, b) { return new Date(a.start) - new Date(b.start); });
const columns = [ const columns = [
{ {
@@ -182,7 +186,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
}; };
return ( return (
<Card title={t("dashboard.titles.scheduledintoday", {date: moment().startOf("day").format("MM/DD/YYYY")})} {...cardProps}> <Card
title={t("dashboard.titles.scheduledintoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
})}
{...cardProps}
>
<div style={{ height: "100%" }}> <div style={{ height: "100%" }}>
<Table <Table
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -1,10 +1,9 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import Icon, { import Icon, {
BankFilled, BankFilled,
BarChartOutlined, BarChartOutlined,
CarFilled, CarFilled,
ClockCircleFilled,
CheckCircleOutlined, CheckCircleOutlined,
ClockCircleFilled,
DashboardFilled, DashboardFilled,
DollarCircleFilled, DollarCircleFilled,
ExportOutlined, ExportOutlined,
@@ -26,6 +25,7 @@ import Icon, {
UnorderedListOutlined, UnorderedListOutlined,
UserOutlined, UserOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Layout, Menu } from "antd"; import { Layout, Menu } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -274,7 +274,11 @@ function Header({
onClick={() => { onClick={() => {
setTimeTicketContext({ setTimeTicketContext({
actions: {}, actions: {},
context: {}, context: {
created_by: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email,
},
}); });
}} }}
> >

View File

@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR } from "../../graphql/vendors.queries"; import { SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR } from "../../graphql/vendors.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { GenerateDocument } from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
@@ -13,13 +14,14 @@ import VendorSearchSelect from "../vendor-search-select/vendor-search-select.com
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
technician: selectTechnician,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(Jobd3RdPartyModal); export default connect(mapStateToProps, mapDispatchToProps)(Jobd3RdPartyModal);
export function Jobd3RdPartyModal({ bodyshop, jobId, job }) { export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -212,7 +214,9 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job }) {
]} ]}
> >
<Radio.Group> <Radio.Group>
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio> {!technician ? (
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
) : null}
<Radio value={"p"}>{t("parts_orders.labels.print")}</Radio> <Radio value={"p"}>{t("parts_orders.labels.print")}</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>

View File

@@ -29,11 +29,11 @@ import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component"; import DataLabel from "../data-label/data-label.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component"; import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
import ScheduleAtChange from "./job-at-change.component"; import ScheduleAtChange from "./job-at-change.component";
import ScheduleEventColor from "./schedule-event.color.component"; import ScheduleEventColor from "./schedule-event.color.component";
import ScheduleEventNote from "./schedule-event.note.component"; import ScheduleEventNote from "./schedule-event.note.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -208,46 +208,56 @@ export function ScheduleEventComponent({
<Button>{t("appointments.actions.sendreminder")}</Button> <Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown> </Dropdown>
) : null} ) : null}
<Popover {event.arrived ? (
trigger="click"
disabled={event.arrived}
content={
<Form
layout="vertical"
onFinish={({ lost_sale_reason }) => {
handleCancel({ id: event.id, lost_sale_reason });
}}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button htmlType="submit">
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
<Button <Button
// onClick={() => handleCancel(event.id)} // onClick={() => handleCancel(event.id)}
disabled={event.arrived} disabled={event.arrived}
> >
{t("appointments.actions.cancel")} {t("appointments.actions.cancel")}
</Button> </Button>
</Popover> ) : (
<Popover
trigger="click"
disabled={event.arrived}
content={
<Form
layout="vertical"
onFinish={({ lost_sale_reason }) => {
handleCancel({ id: event.id, lost_sale_reason });
}}
>
<Form.Item
name="lost_sale_reason"
label={t("jobs.fields.lost_sale_reason")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
label: lsr,
value: lsr,
}))}
/>
</Form.Item>
<Button htmlType="submit">
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
<Button
// onClick={() => handleCancel(event.id)}
disabled={event.arrived}
>
{t("appointments.actions.cancel")}
</Button>
</Popover>
)}
{event.isintake ? ( {event.isintake ? (
<Button <Button
disabled={event.arrived} disabled={event.arrived}

View File

@@ -401,7 +401,7 @@ export function JobLinesComponent({
const markedTypes = [e.key]; const markedTypes = [e.key];
if (e.key === "PAN") markedTypes.push("PAP"); if (e.key === "PAN") markedTypes.push("PAP");
if (e.key === "PAS") markedTypes.push("PASL"); if (e.key === "PAS") markedTypes.push("PASL");
setSelectedLines( setSelectedLines((selectedLines) =>
_.uniq([ _.uniq([
...selectedLines, ...selectedLines,
...jobLines.filter((item) => markedTypes.includes(item.part_type)), ...jobLines.filter((item) => markedTypes.includes(item.part_type)),
@@ -614,8 +614,17 @@ export function JobLinesComponent({
onSelectAll: (selected, selectedRows, changeRows) => { onSelectAll: (selected, selectedRows, changeRows) => {
setSelectedLines(selectedRows); setSelectedLines(selectedRows);
}, },
onSelect: (record, selected, selectedRows, nativeEvent) => onSelect: (record, selected, selectedRows, nativeEvent) => {
setSelectedLines(selectedRows), if (selected) {
setSelectedLines((selectedLines) =>
_.uniqBy([...selectedLines, record], "id")
);
} else {
setSelectedLines((selectedLines) =>
selectedLines.filter((l) => l.id !== record.id)
);
}
},
}} }}
/> />
</div> </div>

View File

@@ -3,7 +3,7 @@ import {
useApolloClient, useApolloClient,
useLazyQuery, useLazyQuery,
useMutation, useMutation,
useQuery useQuery,
} from "@apollo/client"; } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react"; import { useTreatments } from "@splitsoftware/splitio-react";
import { Col, notification, Row } from "antd"; import { Col, notification, Row } from "antd";
@@ -20,7 +20,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { import {
DELETE_AVAILABLE_JOB, DELETE_AVAILABLE_JOB,
QUERY_AVAILABLE_JOBS, QUERY_AVAILABLE_JOBS,
QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK,
} from "../../graphql/available-jobs.queries"; } from "../../graphql/available-jobs.queries";
import { INSERT_NEW_JOB, UPDATE_JOB } from "../../graphql/jobs.queries"; import { INSERT_NEW_JOB, UPDATE_JOB } from "../../graphql/jobs.queries";
import { INSERT_NEW_NOTE } from "../../graphql/notes.queries"; import { INSERT_NEW_NOTE } from "../../graphql/notes.queries";
@@ -28,7 +28,7 @@ import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import confirmDialog from "../../utils/asyncConfirm"; import confirmDialog from "../../utils/asyncConfirm";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
@@ -135,6 +135,7 @@ export function JobsAvailableContainer({
owner_owing: Dinero(newTotals.totals.custPayable.total).toFormat("0.00"), owner_owing: Dinero(newTotals.totals.custPayable.total).toFormat("0.00"),
job_totals: newTotals, job_totals: newTotals,
date_open: moment(), date_open: moment(),
status: bodyshop.md_ro_statuses.default_imported,
notes: { notes: {
data: { data: {
created_by: currentUser.email, created_by: currentUser.email,

View File

@@ -4,22 +4,35 @@ import axios from "axios";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { useHistory } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { auth } from "../../firebase/firebase.utils"; import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import { logImEXEvent } from "../../firebase/firebase.utils"; import client from "../../utils/GraphQLClient";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { useHistory } from "react-router-dom";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
function updateJobCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
fields: {
jobs(existingJobs = []) {
return existingJobs.filter(
(jobRef) => jobRef.__ref.includes(items) === false
);
},
},
});
}
export function JobsCloseExportButton({ export function JobsCloseExportButton({
bodyshop, bodyshop,
currentUser, currentUser,
@@ -101,6 +114,9 @@ export function JobsCloseExportButton({
//Check to see if any of them failed. If they didn't don't execute the update. //Check to see if any of them failed. If they didn't don't execute the update.
const failedTransactions = PartnerResponse.data.filter((r) => !r.success); const failedTransactions = PartnerResponse.data.filter((r) => !r.success);
const successfulTransactions = PartnerResponse.data.filter(
(r) => r.success
);
if (failedTransactions.length > 0) { if (failedTransactions.length > 0) {
//Uh oh. At least one was no good. //Uh oh. At least one was no good.
failedTransactions.forEach((ft) => { failedTransactions.forEach((ft) => {
@@ -159,12 +175,15 @@ export function JobsCloseExportButton({
}, },
}); });
if (!jobUpdateResponse.errors) { if (!!!jobUpdateResponse.errors) {
notification.open({ notification.open({
type: "success", type: "success",
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
);
} else { } else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.exporting", { message: t("jobs.errors.exporting", {
@@ -173,13 +192,31 @@ export function JobsCloseExportButton({
}); });
} }
} }
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
updateJobCache([
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "jobid"
: "id"
]
)
),
]);
}
if (setSelectedJobs) { if (setSelectedJobs) {
setSelectedJobs((selectedJobs) => { setSelectedJobs((selectedJobs) => {
return selectedJobs.filter((i) => i !== jobId); return selectedJobs.filter((i) => i !== jobId);
}); });
} }
} }
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false); setLoading(false);
}; };

View File

@@ -140,63 +140,67 @@ export function JobsDetailHeaderActions({
<Menu.Item <Menu.Item
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled} disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
> >
<Popover {job.status !== bodyshop.md_ro_statuses.default_scheduled ? (
trigger="click" t("menus.jobsactions.cancelallappointments")
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled} ) : (
content={ <Popover
<Form trigger="click"
layout="vertical" disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
onFinish={async ({ lost_sale_reason }) => { content={
const jobUpdate = await cancelAllAppointments({ <Form
variables: { layout="vertical"
jobid: job.id, onFinish={async ({ lost_sale_reason }) => {
job: { const jobUpdate = await cancelAllAppointments({
date_scheduled: null, variables: {
scheduled_in: null, jobid: job.id,
scheduled_completion: null, job: {
lost_sale_reason, date_scheduled: null,
status: bodyshop.md_ro_statuses.default_imported, scheduled_in: null,
scheduled_completion: null,
lost_sale_reason,
status: bodyshop.md_ro_statuses.default_imported,
},
}, },
},
});
if (!jobUpdate.errors) {
notification["success"]({
message: t("appointments.successes.canceled"),
}); });
return; if (!jobUpdate.errors) {
} notification["success"]({
}} message: t("appointments.successes.canceled"),
> });
<Form.Item return;
name="lost_sale_reason" }
label={t("jobs.fields.lost_sale_reason")} }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
> >
<Select <Form.Item
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({ name="lost_sale_reason"
label: lsr, label={t("jobs.fields.lost_sale_reason")}
value: lsr, rules={[
}))} {
/> required: true,
</Form.Item> //message: t("general.validation.required"),
<Button },
htmlType="submit" ]}
disabled={ >
job.status !== bodyshop.md_ro_statuses.default_scheduled <Select
} options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
> label: lsr,
{t("appointments.actions.cancel")} value: lsr,
</Button> }))}
</Form> />
} </Form.Item>
> <Button
{t("menus.jobsactions.cancelallappointments")} htmlType="submit"
</Popover> disabled={
job.status !== bodyshop.md_ro_statuses.default_scheduled
}
>
{t("appointments.actions.cancel")}
</Button>
</Form>
}
>
{t("menus.jobsactions.cancelallappointments")}
</Popover>
)}
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
disabled={ disabled={
@@ -242,7 +246,12 @@ export function JobsDetailHeaderActions({
setTimeTicketContext({ setTimeTicketContext({
actions: {}, actions: {},
context: { jobId: job.id }, context: {
jobId: job.id,
created_by: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email,
},
}); });
}} }}
> >

View File

@@ -13,12 +13,26 @@ import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
function updateJobCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
fields: {
jobs(existingJobs = []) {
return existingJobs.filter(
(jobRef) => jobRef.__ref.includes(items) === false
);
},
},
});
}
export function JobsExportAllButton({ export function JobsExportAllButton({
bodyshop, bodyshop,
currentUser, currentUser,
@@ -96,7 +110,9 @@ export function JobsExportAllButton({
Object.keys(groupedData).map(async (key) => { Object.keys(groupedData).map(async (key) => {
//Check to see if any of them failed. If they didn't don't execute the update. //Check to see if any of them failed. If they didn't don't execute the update.
const failedTransactions = groupedData[key].filter((r) => !r.success); const failedTransactions = groupedData[key].filter((r) => !r.success);
const successfulTransactions = groupedData[key].filter(
(r) => r.success
);
if (failedTransactions.length > 0) { if (failedTransactions.length > 0) {
//Uh oh. At least one was no good. //Uh oh. At least one was no good.
failedTransactions.forEach((ft) => { failedTransactions.forEach((ft) => {
@@ -155,12 +171,17 @@ export function JobsExportAllButton({
}, },
}); });
if (!jobUpdateResponse.errors) { if (!!!jobUpdateResponse.errors) {
notification.open({ notification.open({
type: "success", type: "success",
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map(
(job) => job.id
)
);
} else { } else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.exporting", { message: t("jobs.errors.exporting", {
@@ -169,14 +190,31 @@ export function JobsExportAllButton({
}); });
} }
} }
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
updateJobCache([
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "jobid"
: "id"
]
)
),
]);
}
} }
}) })
); );
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (!!completedCallback) completedCallback([]); if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
setLoading(false); setLoading(false);
}; };

View File

@@ -6,6 +6,7 @@ import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort, statusSort } from "../../utils/sorters";
import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component"; import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -15,6 +16,15 @@ const mapStateToProps = createStructuredSelector({
function OwnerDetailJobsComponent({ bodyshop, owner }) { function OwnerDetailJobsComponent({ bodyshop, owner }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]); const [selectedJobs, setSelectedJobs] = useState([]);
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [ const columns = [
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
@@ -26,6 +36,9 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
{record.ro_number || t("general.labels.na")} {record.ro_number || t("general.labels.na")}
</Link> </Link>
), ),
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
}, },
{ {
title: t("jobs.fields.vehicle"), title: t("jobs.fields.vehicle"),
@@ -46,11 +59,17 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
title: t("jobs.fields.clm_no"), title: t("jobs.fields.clm_no"),
dataIndex: "clm_no", dataIndex: "clm_no",
key: "clm_no", key: "clm_no",
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
}, },
{ {
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
}, },
{ {
@@ -60,6 +79,9 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
render: (text, record) => ( render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter> <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
), ),
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
}, },
]; ];
@@ -80,6 +102,7 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
scroll={{ x: true }} scroll={{ x: true }}
rowKey="id" rowKey="id"
dataSource={owner.jobs} dataSource={owner.jobs}
onChange={handleTableChange}
rowSelection={{ rowSelection={{
onSelect: (record, selected, selectedRows) => { onSelect: (record, selected, selectedRows) => {
setSelectedJobs(selectedRows ? selectedRows.map((i) => i.id) : []); setSelectedJobs(selectedRows ? selectedRows.map((i) => i.id) : []);

View File

@@ -113,6 +113,8 @@ export function PartsOrderListTableComponent({
id: pol.id, id: pol.id,
line_desc: pol.line_desc, line_desc: pol.line_desc,
quantity: pol.quantity, quantity: pol.quantity,
act_price: pol.act_price,
oem_partno: pol.oem_partno,
}; };
}), }),
}, },

View File

@@ -79,6 +79,20 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item
label={t("joblines.fields.oem_partno")}
key={`${index}oem_partno`}
name={[field.name, "oem_partno"]}
>
<Input disabled />
</Form.Item>
<Form.Item
label={t("joblines.fields.act_price")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<Input disabled />
</Form.Item>
<Form.Item <Form.Item
label={t("joblines.fields.location")} label={t("joblines.fields.location")}
key={`${index}location`} key={`${index}location`}

View File

@@ -1,26 +1,39 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, notification } from "antd"; import { Button, notification } from "antd";
import axios from "axios"; import axios from "axios";
import _ from "lodash";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { auth } from "../../firebase/firebase.utils"; import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_BILLS } from "../../graphql/bills.queries"; import { UPDATE_BILLS } from "../../graphql/bills.queries";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import { logImEXEvent } from "../../firebase/firebase.utils"; import client from "../../utils/GraphQLClient";
import _ from "lodash";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { Link } from "react-router-dom";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
function updateBillCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
fields: {
bills(existingJobs = []) {
return existingJobs.filter(
(billRef) => billRef.__ref.includes(items) === false
);
},
},
});
}
export function PayableExportAll({ export function PayableExportAll({
bodyshop, bodyshop,
currentUser, currentUser,
@@ -97,7 +110,9 @@ export function PayableExportAll({
proms.push( proms.push(
(async () => { (async () => {
const failedTransactions = groupedData[key].filter((r) => !r.success); const failedTransactions = groupedData[key].filter((r) => !r.success);
const successfulTransactions = groupedData[key].filter(
(r) => r.success
);
if (failedTransactions.length > 0) { if (failedTransactions.length > 0) {
//Uh oh. At least one was no good. //Uh oh. At least one was no good.
failedTransactions.map((ft) => failedTransactions.map((ft) =>
@@ -143,7 +158,15 @@ export function PayableExportAll({
const billUpdateResponse = await updateBill({ const billUpdateResponse = await updateBill({
variables: { variables: {
billIdList: [key], billIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo
? "billid"
: "id"
]
),
bill: { bill: {
exported: true, exported: true,
exported_at: new Date(), exported_at: new Date(),
@@ -156,6 +179,11 @@ export function PayableExportAll({
key: "billsuccessexport", key: "billsuccessexport",
message: t("bills.successes.exported"), message: t("bills.successes.exported"),
}); });
updateBillCache(
billUpdateResponse.data.update_bills.returning.map(
(bill) => bill.id
)
);
} else { } else {
notification["error"]({ notification["error"]({
message: t("bills.errors.exporting", { message: t("bills.errors.exporting", {
@@ -164,6 +192,26 @@ export function PayableExportAll({
}); });
} }
} }
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
updateBillCache([
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo
? "billid"
: "id"
]
)
),
]);
}
} }
})() })()
); );
@@ -172,8 +220,6 @@ export function PayableExportAll({
await Promise.all(proms); await Promise.all(proms);
if (!!completedCallback) completedCallback([]); if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false); setLoading(false);
}; };

View File

@@ -4,22 +4,35 @@ import axios from "axios";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { auth } from "../../firebase/firebase.utils"; import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_BILLS } from "../../graphql/bills.queries"; import { UPDATE_BILLS } from "../../graphql/bills.queries";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import { logImEXEvent } from "../../firebase/firebase.utils"; import client from "../../utils/GraphQLClient";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { Link } from "react-router-dom";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
function updateBillCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
fields: {
bills(existingJobs = []) {
return existingJobs.filter(
(billRef) => billRef.__ref.includes(items) === false
);
},
},
});
}
export function PayableExportButton({ export function PayableExportButton({
bodyshop, bodyshop,
currentUser, currentUser,
@@ -159,6 +172,11 @@ export function PayableExportButton({
key: "billsuccessexport", key: "billsuccessexport",
message: t("bills.successes.exported"), message: t("bills.successes.exported"),
}); });
updateBillCache(
billUpdateResponse.data.update_bills.returning.map(
(bill) => bill.id
)
);
} else { } else {
notification["error"]({ notification["error"]({
message: t("bills.errors.exporting", { message: t("bills.errors.exporting", {
@@ -167,7 +185,25 @@ export function PayableExportButton({
}); });
} }
} }
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch(); if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
updateBillCache([
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "billid"
: "id"
]
)
),
]);
}
if (setSelectedBills) { if (setSelectedBills) {
setSelectedBills((selectedBills) => { setSelectedBills((selectedBills) => {

View File

@@ -1,10 +1,4 @@
import React, { useState } from "react";
import { useMutation, useQuery } from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client";
import {
GET_REFUNDABLE_AMOUNT_BY_JOBID,
INSERT_PAYMENT_RESPONSE,
QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID,
} from "../../graphql/payment_response.queries";
import { import {
Button, Button,
Descriptions, Descriptions,
@@ -13,10 +7,18 @@ import {
Space, Space,
notification, notification,
} from "antd"; } from "antd";
import moment from "moment";
import axios from "axios"; import axios from "axios";
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries"; import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import {
GET_REFUNDABLE_AMOUNT_BY_JOBID,
INSERT_PAYMENT_RESPONSE,
QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID,
} from "../../graphql/payment_response.queries";
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter";
const { confirm } = Modal; const { confirm } = Modal;
@@ -144,10 +146,10 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
{payment_response?.response?.nameOnCard ?? ""} {payment_response?.response?.nameOnCard ?? ""}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.amount")}> <Descriptions.Item label={t("job_payments.titles.amount")}>
{record.amount} <CurrencyFormatter>{record.amount}</CurrencyFormatter>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.dateOfPayment")}> <Descriptions.Item label={t("job_payments.titles.dateOfPayment")}>
{moment(record.created_at).format("YYYY-MM-DD HH:mm:ss")} {<DateTimeFormatter>{record.created_at}</DateTimeFormatter>}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.transactionid")}> <Descriptions.Item label={t("job_payments.titles.transactionid")}>
{record.transactionid} {record.transactionid}
@@ -158,6 +160,9 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
<Descriptions.Item label={t("job_payments.titles.paymenttype")}> <Descriptions.Item label={t("job_payments.titles.paymenttype")}>
{record.type} {record.type}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("job_payments.titles.paymentnum")}>
{record.paymentnum}
</Descriptions.Item>
{payment_response && ( {payment_response && (
<Descriptions.Item label={t("job_payments.titles.refundamount")}> <Descriptions.Item label={t("job_payments.titles.refundamount")}>
<Space> <Space>

View File

@@ -13,12 +13,26 @@ import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
function updatePaymentCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
fields: {
payments(existingJobs = []) {
return existingJobs.filter(
(paymentRef) => paymentRef.__ref.includes(items) === false
);
},
},
});
}
export function PaymentExportButton({ export function PaymentExportButton({
bodyshop, bodyshop,
currentUser, currentUser,
@@ -157,6 +171,11 @@ export function PaymentExportButton({
key: "paymentsuccessexport", key: "paymentsuccessexport",
message: t("payments.successes.exported"), message: t("payments.successes.exported"),
}); });
updatePaymentCache(
paymentUpdateResponse.data.update_payments.returning.map(
(payment) => payment.id
)
);
} else { } else {
notification["error"]({ notification["error"]({
message: t("payments.errors.exporting", { message: t("payments.errors.exporting", {
@@ -172,7 +191,25 @@ export function PaymentExportButton({
}); });
} }
} }
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch(); if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
updatePaymentCache([
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "paymentid"
: "id"
]
)
),
]);
}
if (!!loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
setLoading(false); setLoading(false);
}; };

View File

@@ -13,11 +13,25 @@ import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
function updatePaymentCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
fields: {
payments(existingJobs = []) {
return existingJobs.filter(
(paymentRef) => paymentRef.__ref.includes(items) === false
);
},
},
});
}
export function PaymentsExportAllButton({ export function PaymentsExportAllButton({
bodyshop, bodyshop,
currentUser, currentUser,
@@ -25,7 +39,7 @@ export function PaymentsExportAllButton({
disabled, disabled,
loadingCallback, loadingCallback,
completedCallback, completedCallback,
refetch refetch,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updatePayments] = useMutation(UPDATE_PAYMENTS); const [updatePayments] = useMutation(UPDATE_PAYMENTS);
@@ -84,7 +98,9 @@ export function PaymentsExportAllButton({
proms.push( proms.push(
(async () => { (async () => {
const failedTransactions = groupedData[key].filter((r) => !r.success); const failedTransactions = groupedData[key].filter((r) => !r.success);
const successfulTransactions = groupedData[key].filter(
(r) => r.success
);
if (failedTransactions.length > 0) { if (failedTransactions.length > 0) {
//Uh oh. At least one was no good. //Uh oh. At least one was no good.
failedTransactions.map((ft) => failedTransactions.map((ft) =>
@@ -130,7 +146,15 @@ export function PaymentsExportAllButton({
}); });
const paymentUpdateResponse = await updatePayments({ const paymentUpdateResponse = await updatePayments({
variables: { variables: {
paymentIdList: [key], paymentIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo
? "paymentid"
: "id"
]
),
payment: { payment: {
exportedat: new Date(), exportedat: new Date(),
}, },
@@ -142,6 +166,11 @@ export function PaymentsExportAllButton({
key: "paymentsuccessexport", key: "paymentsuccessexport",
message: t("payments.successes.exported"), message: t("payments.successes.exported"),
}); });
updatePaymentCache(
paymentUpdateResponse.data.update_payments.returning.map(
(payment) => payment.id
)
);
} else { } else {
notification["error"]({ notification["error"]({
message: t("payments.errors.exporting", { message: t("payments.errors.exporting", {
@@ -150,6 +179,26 @@ export function PaymentsExportAllButton({
}); });
} }
} }
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
updatePaymentCache([
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo
? "paymentid"
: "id"
]
)
),
]);
}
} }
})() })()
); );
@@ -157,7 +206,6 @@ export function PaymentsExportAllButton({
await Promise.all(proms); await Promise.all(proms);
if (!!completedCallback) completedCallback([]); if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false); setLoading(false);
}; };

View File

@@ -5,11 +5,13 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { setEmailOptions } from "../../redux/email/email.actions"; import { setEmailOptions } from "../../redux/email/email.actions";
import { selectPrintCenter } from "../../redux/modals/modals.selectors"; import { selectPrintCenter } from "../../redux/modals/modals.selectors";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { GenerateDocument } from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
printCenterModal: selectPrintCenter, printCenterModal: selectPrintCenter,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
technician: selectTechnician,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setEmailOptions: (e) => dispatch(setEmailOptions(e)), setEmailOptions: (e) => dispatch(setEmailOptions(e)),
@@ -22,6 +24,7 @@ export function PrintCenterItemComponent({
id, id,
bodyshop, bodyshop,
disabled, disabled,
technician,
}) { }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { context } = printCenterModal; const { context } = printCenterModal;
@@ -44,19 +47,24 @@ export function PrintCenterItemComponent({
<Space wrap> <Space wrap>
{item.title} {item.title}
<PrinterOutlined onClick={renderToNewWindow} /> <PrinterOutlined onClick={renderToNewWindow} />
<MailOutlined {!technician ? (
onClick={() => { <MailOutlined
GenerateDocument( onClick={() => {
{ GenerateDocument(
name: item.key, {
variables: { id: id }, name: item.key,
}, variables: { id: id },
{ to: context.job && context.job.ownr_ea, subject: item.subject }, },
"e", {
id to: context.job && context.job.ownr_ea,
); subject: item.subject,
}} },
/> "e",
id
);
}}
/>
) : null}
{loading && <Spin />} {loading && <Spin />}
</Space> </Space>
</li> </li>

View File

@@ -1,31 +1,33 @@
import { PrinterFilled } from "@ant-design/icons";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { Descriptions, Drawer, Space, PageHeader, Button } from "antd"; import { Button, Descriptions, Drawer, PageHeader, Space } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom"; import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries"; import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import StartChatButton from "../chat-open-button/chat-open-button.component"; import StartChatButton from "../chat-open-button/chat-open-button.component";
import JobAtChange from "../job-at-change/job-at-change.component";
import JobDetailCardsDocumentsComponent from "../job-detail-cards/job-detail-cards.documents.component"; import JobDetailCardsDocumentsComponent from "../job-detail-cards/job-detail-cards.documents.component";
import JobDetailCardsNotesComponent from "../job-detail-cards/job-detail-cards.notes.component"; import JobDetailCardsNotesComponent from "../job-detail-cards/job-detail-cards.notes.component";
import JobDetailCardsPartsComponent from "../job-detail-cards/job-detail-cards.parts.component"; import JobDetailCardsPartsComponent from "../job-detail-cards/job-detail-cards.parts.component";
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container"; import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import ProductionRemoveButton from "../production-remove-button/production-remove-button.component";
import JobAtChange from "../job-at-change/job-at-change.component";
import { PrinterFilled } from "@ant-design/icons";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component"; import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component";
import { selectBodyshop } from "../../redux/user/user.selectors"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ProductionRemoveButton from "../production-remove-button/production-remove-button.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
technician: selectTechnician,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) => setPrintCenterContext: (context) =>
@@ -40,6 +42,7 @@ export function ProductionListDetail({
bodyshop, bodyshop,
jobs, jobs,
setPrintCenterContext, setPrintCenterContext,
technician,
}) { }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const history = useHistory(); const history = useHistory();
@@ -66,7 +69,9 @@ export function ProductionListDetail({
title={theJob.ro_number} title={theJob.ro_number}
extra={ extra={
<Space wrap> <Space wrap>
<ProductionRemoveButton jobId={theJob.id} />{" "} {!technician ? (
<ProductionRemoveButton jobId={theJob.id} />
) : null}
<Button <Button
onClick={() => { onClick={() => {
setPrintCenterContext({ setPrintCenterContext({
@@ -82,7 +87,9 @@ export function ProductionListDetail({
<PrinterFilled /> <PrinterFilled />
{t("jobs.actions.printCenter")} {t("jobs.actions.printCenter")}
</Button> </Button>
<ScoreboardAddButton job={data ? data.jobs_by_pk : {}} /> {!technician ? (
<ScoreboardAddButton job={data ? data.jobs_by_pk : {}} />
) : null}
</Space> </Space>
} }
/> />

View File

@@ -59,11 +59,12 @@ export function ScheduleManualEvent({ bodyshop, event }) {
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"], refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"],
}); });
} }
form.resetFields();
setVisibility(false);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} finally { } finally {
setLoading(false); setLoading(false);
setVisibility(false);
} }
}; };

View File

@@ -1,21 +1,26 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Card, Form, notification, Space } from "antd"; import { Button, Card, Form, notification, Space } from "antd";
import axios from "axios"; import axios from "axios";
import moment from "moment";
import momenttz from "moment-timezone";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { INSERT_NEW_TIME_TICKET } from "../../graphql/timetickets.queries"; import { INSERT_NEW_TIME_TICKET } from "../../graphql/timetickets.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import TechClockInComponent from "./tech-job-clock-in-form.component";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
import moment from "moment";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
import TechClockInComponent from "./tech-job-clock-in-form.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
technician: selectTechnician, technician: selectTechnician,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setTimeTicketContext: (context) => setTimeTicketContext: (context) =>
@@ -25,9 +30,10 @@ export function TechClockInContainer({
setTimeTicketContext, setTimeTicketContext,
technician, technician,
bodyshop, bodyshop,
currentUser,
}) { }) {
console.log( console.log(
"🚀 ~ file: tech-job-clock-in-form.container.jsx:29 ~ technician:", "🚀 ~ file: tech-job-clock-in-form.container.jsx:30 ~ technician:",
technician technician
); );
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -44,14 +50,20 @@ export function TechClockInContainer({
const handleFinish = async (values) => { const handleFinish = async (values) => {
setLoading(true); setLoading(true);
const theTime = (await axios.post("/utils/time")).data; const theTime = (await axios.post("/utils/time")).data;
const result = await insertTimeTicket({ const result = await insertTimeTicket({
variables: { variables: {
timeTicketInput: [ timeTicketInput: [
{ {
bodyshopid: bodyshop.id, bodyshopid: bodyshop.id,
employeeid: technician.id, employeeid: technician.id,
date: moment(theTime).format("YYYY-MM-DD"), date:
typeof bodyshop.timezone === "string"
? momenttz.tz(theTime, bodyshop.timezone).format("YYYY-MM-DD")
: typeof bodyshop.timezone === "number"
? moment(theTime)
.format("YYYY-MM-DD")
.utcOffset(bodyshop.timezone)
: moment(theTime).format("YYYY-MM-DD"),
clockon: moment(theTime), clockon: moment(theTime),
jobid: values.jobid, jobid: values.jobid,
cost_center: values.cost_center, cost_center: values.cost_center,
@@ -66,6 +78,12 @@ export function TechClockInContainer({
values.cost_center values.cost_center
); );
}), }),
created_by: currentUser.email.concat(
" | ",
technician.employee_number
.concat(" ", technician.first_name, " ", technician.last_name)
.trim()
),
}, },
], ],
}, },
@@ -100,13 +118,24 @@ export function TechClockInContainer({
employeeid: technician.id, employeeid: technician.id,
flat_rate: emps.flat_rate, flat_rate: emps.flat_rate,
}, },
created_by: currentUser.email.concat(
" | ",
technician.employee_number
.concat(
" ",
technician.first_name,
" ",
technician.last_name
)
.trim()
),
}, },
}); });
}} }}
> >
{t("timetickets.actions.enter")} {t("timetickets.actions.enter")}
</Button> </Button>
<TechJobPrintTickets /> <TechJobPrintTickets attendacePrint={false} />
<Button <Button
type="primary" type="primary"
onClick={() => form.submit()} onClick={() => form.submit()}

View File

@@ -1,4 +1,4 @@
import { Button, Card, DatePicker, Form, Popover, Space } from "antd"; import { Button, Card, DatePicker, Form, Popover, Radio, Space } from "antd";
import moment from "moment"; import moment from "moment";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -21,12 +21,13 @@ export default connect(
mapDispatchToProps mapDispatchToProps
)(TechJobPrintTickets); )(TechJobPrintTickets);
export function TechJobPrintTickets({ technician, event }) { export function TechJobPrintTickets({ technician, event, attendacePrint }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [visibility, setVisibility] = useState(false); const [visibility, setVisibility] = useState(false);
const Templates = TemplateList("report_center");
useEffect(() => { useEffect(() => {
if (visibility && event) { if (visibility && event) {
@@ -44,7 +45,10 @@ export function TechJobPrintTickets({ technician, event }) {
try { try {
await GenerateDocument( await GenerateDocument(
{ {
name: TemplateList().timetickets_employee.key, name:
attendacePrint === true
? Templates.attendance_employee.key
: Templates.timetickets_employee.key,
variables: { variables: {
...(start ...(start
? { start: moment(start).startOf("day").format("YYYY-MM-DD") } ? { start: moment(start).startOf("day").format("YYYY-MM-DD") }
@@ -60,9 +64,12 @@ export function TechJobPrintTickets({ technician, event }) {
}, },
{ {
to: technician.email, to: technician.email,
subject: TemplateList().timetickets_employee.subject, subject:
attendacePrint === true
? Templates.attendance_employee.subject
: Templates.timetickets_employee.subject,
}, },
"p" values.sendby // === "email" ? "e" : "p"
); );
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@@ -92,10 +99,25 @@ export function TechJobPrintTickets({ technician, event }) {
format={"MM/DD/YYYY"} format={"MM/DD/YYYY"}
/> />
</Form.Item> </Form.Item>
<Form.Item dependencies={["dates"]}>
{() => {
return (
<Form.Item
label={t("general.labels.sendby")}
name="sendby"
initialValue="p"
>
<Radio.Group>
<Radio value="e">{t("general.labels.email")}</Radio>
<Radio value="p">{t("general.labels.print")}</Radio>
</Radio.Group>
</Form.Item>
);
}}
</Form.Item>
<Space wrap> <Space wrap>
<Button type="primary" onClick={() => form.submit()}> <Button type="primary" onClick={() => form.submit()}>
{t("general.actions.print")} {t("reportcenter.actions.generate")}
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
@@ -118,7 +140,7 @@ export function TechJobPrintTickets({ technician, event }) {
return ( return (
<Popover content={overlay} visible={visibility}> <Popover content={overlay} visible={visibility}>
<Button loading={loading} onClick={handleClick}> <Button loading={loading} onClick={handleClick}>
{t("general.actions.print")} {t("general.labels.reports")}
</Button> </Button>
</Popover> </Popover>
); );

View File

@@ -9,9 +9,10 @@ import { createStructuredSelector } from "reselect";
import { import {
selectAuthLevel, selectAuthLevel,
selectBodyshop, selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import RbacWrapper, { import RbacWrapper, {
HasRbacAccess, HasRbacAccess,
@@ -20,6 +21,7 @@ import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
authLevel: selectAuthLevel, authLevel: selectAuthLevel,
currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -29,6 +31,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketList);
export function TimeTicketList({ export function TimeTicketList({
bodyshop, bodyshop,
authLevel, authLevel,
currentUser,
disabled, disabled,
loading, loading,
timetickets, timetickets,
@@ -193,7 +196,15 @@ export function TimeTicketList({
} }
}, },
}, },
{
title: t("timetickets.fields.created_by"),
dataIndex: "created_by",
key: "created_by",
sorter: (a, b) => alphaSort(a.created_by, b.created_by),
sortOrder:
state.sortedInfo.columnKey === "created_by" && state.sortedInfo.order,
render: (text, record) => record.created_by,
},
{ {
title: t("general.labels.actions"), title: t("general.labels.actions"),
dataIndex: "actions", dataIndex: "actions",
@@ -254,7 +265,12 @@ export function TimeTicketList({
(techConsole ? null : ( (techConsole ? null : (
<TimeTicketEnterButton <TimeTicketEnterButton
actions={{ refetch }} actions={{ refetch }}
context={{ jobId: jobId }} context={{
jobId: jobId,
created_by: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email,
}}
disabled={disabled} disabled={disabled}
> >
{t("timetickets.actions.enter")} {t("timetickets.actions.enter")}

View File

@@ -1,5 +1,5 @@
import { useMutation, useQuery } from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client";
import { Button, Form, Modal, notification, PageHeader, Space } from "antd"; import { Button, Form, Modal, PageHeader, Space, notification } from "antd";
import moment from "moment"; import moment from "moment";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -77,6 +77,7 @@ export function TimeTicketModalContainer({
)[0].rate )[0].rate
: null, : null,
bodyshopid: bodyshop.id, bodyshopid: bodyshop.id,
created_by: timeTicketModal.context.created_by,
}, },
], ],
}, },

View File

@@ -4,48 +4,67 @@ import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import DataLabel from "../data-label/data-label.component"; import DataLabel from "../data-label/data-label.component";
import TechClockOffButton from "../tech-job-clock-out-button/tech-job-clock-out-button.component"; import TechClockOffButton from "../tech-job-clock-out-button/tech-job-clock-out-button.component";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
export default function TimeTicketShiftActive({ timetickets, refetch }) { export default function TimeTicketShiftActive({
timetickets,
refetch,
isTechConsole,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div> <div>
{timetickets.length > 0 ? ( {timetickets.length > 0 ? (
<div> <div
<Typography.Title level={2}> style={{
{t("timetickets.labels.shiftalreadyclockedon")} display: "flex",
</Typography.Title> justifyContent: "space-between",
<List flexDirection: "column",
grid={{ height: "100%",
gutter: 32, }}
xs: 1, >
sm: 2, <div style={{ display: "flex", justifyContent: "space-between" }}>
md: 3, <Typography.Title level={2}>
lg: 4, {t("timetickets.labels.shiftalreadyclockedon")}
xl: 5, </Typography.Title>
xxl: 6, {isTechConsole ? (
}} <TechJobPrintTickets attendacePrint={true} />
dataSource={timetickets || []} ) : null}
renderItem={(ticket) => ( </div>
<List.Item> <div style={{ flexGrow: 1 }}>
<Card <List
title={t(ticket.memo)} grid={{
actions={[ gutter: 32,
<TechClockOffButton xs: 1,
jobId={ticket.jobid} sm: 2,
timeTicketId={ticket.id} md: 3,
completedCallback={refetch} lg: 4,
isShiftTicket xl: 5,
/>, xxl: 6,
]} }}
> dataSource={timetickets || []}
<DataLabel label={t("timetickets.fields.clockon")}> renderItem={(ticket) => (
<DateTimeFormatter>{ticket.clockon}</DateTimeFormatter> <List.Item>
</DataLabel> <Card
</Card> title={t(ticket.memo)}
</List.Item> actions={[
)} <TechClockOffButton
></List> jobId={ticket.jobid}
timeTicketId={ticket.id}
completedCallback={refetch}
isShiftTicket
/>,
]}
>
<DataLabel label={t("timetickets.fields.clockon")}>
<DateTimeFormatter>{ticket.clockon}</DateTimeFormatter>
</DataLabel>
</Card>
</List.Item>
)}
></List>
</div>
</div> </div>
) : null} ) : null}
</div> </div>

View File

@@ -1,7 +1,8 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Form, notification } from "antd"; import { Button, Form, Space, notification } from "antd";
import axios from "axios"; import axios from "axios";
import moment from "moment"; import moment from "moment";
import momenttz from "moment-timezone";
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -12,6 +13,7 @@ import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
import TimeTicektShiftComponent from "./time-ticket-shift-form.component"; import TimeTicektShiftComponent from "./time-ticket-shift-form.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
@@ -29,6 +31,10 @@ export function TimeTicektShiftContainer({
isTechConsole, isTechConsole,
checkIfAlreadyClocked, checkIfAlreadyClocked,
}) { }) {
console.log(
"🚀 ~ file: time-ticket-shift-form.container.jsx:28 ~ technician:",
technician
);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [insertTimeTicket] = useMutation(INSERT_NEW_TIME_TICKET); const [insertTimeTicket] = useMutation(INSERT_NEW_TIME_TICKET);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -63,8 +69,30 @@ export function TimeTicektShiftContainer({
employeeid: isTechConsole ? technician.id : employeeId, employeeid: isTechConsole ? technician.id : employeeId,
cost_center: "timetickets.labels.shift", cost_center: "timetickets.labels.shift",
clockon: theTime, clockon: theTime,
date: theTime, date:
typeof bodyshop.timezone === "string"
? momenttz.tz(theTime, bodyshop.timezone).format("YYYY-MM-DD")
: typeof bodyshop.timezone === "number"
? moment(theTime)
.utcOffset(bodyshop.timezone)
.format("YYYY-MM-DD")
: moment(theTime).format("YYYY-MM-DD"),
memo: values.memo, memo: values.memo,
created_by: isTechConsole
? currentUser.email.concat(
" | ",
technician.employee_number
.concat(
" ",
technician.first_name,
" ",
technician.last_name
)
.trim()
)
: currentUser.displayName
? currentUser.email.concat(" | ", currentUser.displayName)
: currentUser.email,
}, },
], ],
}, },
@@ -98,9 +126,14 @@ export function TimeTicektShiftContainer({
initialValues={{ cost_center: t("timetickets.labels.shift") }} initialValues={{ cost_center: t("timetickets.labels.shift") }}
> >
<TimeTicektShiftComponent form={form} /> <TimeTicektShiftComponent form={form} />
<Button htmlType="submit" loading={loading}> <Space wrap>
{t("timetickets.actions.clockin")} <Button htmlType="submit" loading={loading} type="primary">
</Button> {t("timetickets.actions.clockin")}
</Button>
{isTechConsole === true ? (
<TechJobPrintTickets attendacePrint={true} />
) : null}
</Space>
</Form> </Form>
</div> </div>
); );

View File

@@ -76,6 +76,7 @@ export function TimeTicketShiftContainer({
<TimeTicketShiftActive <TimeTicketShiftActive
timetickets={data ? data.timetickets : []} timetickets={data ? data.timetickets : []}
refetch={refetch} refetch={refetch}
isTechConsole={isTechConsole}
/> />
) : ( ) : (
<TimeTicketShiftFormContainer <TimeTicketShiftFormContainer

View File

@@ -6,8 +6,9 @@ import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component"; import { alphaSort, statusSort } from "../../utils/sorters";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -16,6 +17,14 @@ const mapStateToProps = createStructuredSelector({
export function VehicleDetailJobsComponent({ vehicle, bodyshop }) { export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]); const [selectedJobs, setSelectedJobs] = useState([]);
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [ const columns = [
{ {
@@ -28,6 +37,9 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
{record.ro_number || t("general.labels.na")} {record.ro_number || t("general.labels.na")}
</Link> </Link>
), ),
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
}, },
{ {
title: t("jobs.fields.owner"), title: t("jobs.fields.owner"),
@@ -43,11 +55,17 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
title: t("jobs.fields.clm_no"), title: t("jobs.fields.clm_no"),
dataIndex: "clm_no", dataIndex: "clm_no",
key: "clm_no", key: "clm_no",
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
}, },
{ {
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
}, },
{ {
@@ -57,6 +75,9 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
render: (text, record) => ( render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter> <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
), ),
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
}, },
]; ];
@@ -76,6 +97,7 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
rowKey="id" rowKey="id"
scroll={{ x: true }} scroll={{ x: true }}
dataSource={vehicle.jobs} dataSource={vehicle.jobs}
onChange={handleTableChange}
rowSelection={{ rowSelection={{
onSelect: (record, selected, selectedRows) => { onSelect: (record, selected, selectedRows) => {
setSelectedJobs(selectedRows ? selectedRows.map((i) => i.id) : []); setSelectedJobs(selectedRows ? selectedRows.map((i) => i.id) : []);

View File

@@ -5,6 +5,7 @@ export const INSERT_NEW_BILL = gql`
insert_bills(objects: $bill) { insert_bills(objects: $bill) {
returning { returning {
id id
invoice_number
} }
} }
} }

View File

@@ -57,6 +57,7 @@ export const GET_LINE_TICKET_BY_PK = gql`
actualhrs actualhrs
ciecacode ciecacode
cost_center cost_center
created_by
date date
id id
jobid jobid

View File

@@ -574,7 +574,6 @@ export const GET_JOB_BY_PK = gql`
est_co_nm est_co_nm
est_ct_fn est_ct_fn
est_ct_ln est_ct_ln
est_ph1 est_ph1
est_ea est_ea
selling_dealer selling_dealer
@@ -746,6 +745,7 @@ export const GET_JOB_BY_PK = gql`
jobid jobid
amount amount
payer payer
paymentnum
created_at created_at
transactionid transactionid
memo memo

View File

@@ -69,7 +69,7 @@ export const QUERY_OWNER_BY_ID = gql`
preferred_contact preferred_contact
note note
tax_number tax_number
jobs { jobs(order_by: { date_open: desc }) {
id id
ro_number ro_number
clm_no clm_no

View File

@@ -37,6 +37,7 @@ export const QUERY_TIME_TICKETS_IN_RANGE = gql`
clockon clockon
cost_center cost_center
created_at created_at
created_by
date date
id id
rate rate
@@ -80,6 +81,7 @@ export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql`
clockon clockon
cost_center cost_center
created_at created_at
created_by
date date
id id
rate rate
@@ -112,6 +114,7 @@ export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql`
clockon clockon
cost_center cost_center
created_at created_at
created_by
date date
id id
rate rate
@@ -151,6 +154,7 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
clockon clockon
cost_center cost_center
created_at created_at
created_by
date date
id id
rate rate
@@ -181,6 +185,7 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
clockon clockon
cost_center cost_center
created_at created_at
created_by
date date
id id
rate rate
@@ -210,6 +215,7 @@ export const INSERT_NEW_TIME_TICKET = gql`
insert_timetickets(objects: $timeTicketInput) { insert_timetickets(objects: $timeTicketInput) {
returning { returning {
id id
created_by
clockon clockon
clockoff clockoff
employeeid employeeid

View File

@@ -28,11 +28,10 @@ export const QUERY_VEHICLE_BY_ID = gql`
updated_at updated_at
trim_color trim_color
notes notes
jobs { jobs(order_by: { date_open: desc }) {
id id
ro_number ro_number
ownr_fn ownr_fn
ownr_ln ownr_ln
owner { owner {
id id

View File

@@ -1,21 +1,24 @@
import { BackTop, Layout } from "antd"; import { BackTop, Layout } from "antd";
import React, { lazy, Suspense, useEffect } from "react"; import React, { Suspense, lazy, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Redirect, Route, Switch } from "react-router-dom"; import { Redirect, Route, Switch } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import ErrorBoundary from "../../components/error-boundary/error-boundary.component"; import ErrorBoundary from "../../components/error-boundary/error-boundary.component";
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import TechHeader from "../../components/tech-header/tech-header.component"; import TechHeader from "../../components/tech-header/tech-header.component";
import TechSider from "../../components/tech-sider/tech-sider.component"; import TechSider from "../../components/tech-sider/tech-sider.component";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
import "./tech.page.styles.scss";
import UpdateAlert from "../../components/update-alert/update-alert.component"; import UpdateAlert from "../../components/update-alert/update-alert.component";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import "./tech.page.styles.scss";
const TimeTicketModalContainer = lazy(() => const TimeTicketModalContainer = lazy(() =>
import("../../components/time-ticket-modal/time-ticket-modal.container") import("../../components/time-ticket-modal/time-ticket-modal.container")
); );
const EmailOverlayContainer = lazy(() =>
import("../../components/email-overlay/email-overlay.container.jsx")
);
const PrintCenterModalContainer = lazy(() => const PrintCenterModalContainer = lazy(() =>
import("../../components/print-center-modal/print-center-modal.container") import("../../components/print-center-modal/print-center-modal.container")
); );
@@ -69,6 +72,7 @@ export function TechPage({ technician, match }) {
> >
<FeatureWrapper featureName="tech-console"> <FeatureWrapper featureName="tech-console">
<TimeTicketModalContainer /> <TimeTicketModalContainer />
<EmailOverlayContainer />
<PrintCenterModalContainer /> <PrintCenterModalContainer />
<Switch> <Switch>
<Route <Route

View File

@@ -490,7 +490,7 @@
"lam": "Mechanical", "lam": "Mechanical",
"lar": "Refinish", "lar": "Refinish",
"las": "Structural", "las": "Structural",
"lau": "Detail", "lau": "User Defined",
"local_tax": "Local Tax", "local_tax": "Local Tax",
"mapa": "Paint Materials", "mapa": "Paint Materials",
"mash": "Shop Materials", "mash": "Shop Materials",
@@ -1093,6 +1093,7 @@
"passwordsdonotmatch": "The passwords you have entered do not match.", "passwordsdonotmatch": "The passwords you have entered do not match.",
"print": "Print", "print": "Print",
"refresh": "Refresh", "refresh": "Refresh",
"reports": "Reports",
"required": "Required", "required": "Required",
"saturday": "Saturday", "saturday": "Saturday",
"search": "Search...", "search": "Search...",
@@ -1210,6 +1211,7 @@
"payer": "Payer", "payer": "Payer",
"payername": "Payer Name", "payername": "Payer Name",
"paymentid": "Payment Reference ID", "paymentid": "Payment Reference ID",
"paymentnum": "Payment Number",
"paymenttype": "Payment Type", "paymenttype": "Payment Type",
"refundamount": "Refund Amount", "refundamount": "Refund Amount",
"transactionid": "Transaction ID" "transactionid": "Transaction ID"
@@ -2736,6 +2738,7 @@
"clockon": "Clocked In", "clockon": "Clocked In",
"committed": "", "committed": "",
"cost_center": "Cost Center", "cost_center": "Cost Center",
"created_by": "Created By",
"date": "Ticket Date", "date": "Ticket Date",
"efficiency": "Efficiency", "efficiency": "Efficiency",
"employee": "Employee", "employee": "Employee",

View File

@@ -1093,6 +1093,7 @@
"passwordsdonotmatch": "", "passwordsdonotmatch": "",
"print": "", "print": "",
"refresh": "", "refresh": "",
"reports": "",
"required": "", "required": "",
"saturday": "", "saturday": "",
"search": "Buscar...", "search": "Buscar...",
@@ -1210,6 +1211,7 @@
"payer": "", "payer": "",
"payername": "", "payername": "",
"paymentid": "", "paymentid": "",
"paymentnum": "",
"paymenttype": "", "paymenttype": "",
"refundamount": "", "refundamount": "",
"transactionid": "" "transactionid": ""
@@ -2736,6 +2738,7 @@
"clockon": "", "clockon": "",
"committed": "", "committed": "",
"cost_center": "", "cost_center": "",
"created_by": "",
"date": "", "date": "",
"efficiency": "", "efficiency": "",
"employee": "", "employee": "",

View File

@@ -1093,6 +1093,7 @@
"passwordsdonotmatch": "", "passwordsdonotmatch": "",
"print": "", "print": "",
"refresh": "", "refresh": "",
"reports": "",
"required": "", "required": "",
"saturday": "", "saturday": "",
"search": "Chercher...", "search": "Chercher...",
@@ -1210,6 +1211,7 @@
"payer": "", "payer": "",
"payername": "", "payername": "",
"paymentid": "", "paymentid": "",
"paymentnum": "",
"paymenttype": "", "paymenttype": "",
"refundamount": "", "refundamount": "",
"transactionid": "" "transactionid": ""
@@ -2736,6 +2738,7 @@
"clockon": "", "clockon": "",
"committed": "", "committed": "",
"cost_center": "", "cost_center": "",
"created_by": "",
"date": "", "date": "",
"efficiency": "", "efficiency": "",
"employee": "", "employee": "",

View File

@@ -1040,7 +1040,7 @@ export const TemplateList = (type, context) => {
disabled: false, disabled: false,
rangeFilter: { rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"), object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_open"), field: i18n.t("jobs.fields.date_invoiced"),
}, },
group: "jobs", group: "jobs",
}, },
@@ -1135,6 +1135,10 @@ export const TemplateList = (type, context) => {
key: "timetickets_employee", key: "timetickets_employee",
idtype: "employee", idtype: "employee",
disabled: false, disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.timetickets"),
field: i18n.t("timetickets.fields.date"),
},
group: "payroll", group: "payroll",
}, },
attendance_detail: { attendance_detail: {

View File

@@ -3273,6 +3273,8 @@
- ca_gst_registrant - ca_gst_registrant
- cat_no - cat_no
- category - category
- cieca_pfl
- cieca_pft
- cieca_stl - cieca_stl
- cieca_ttl - cieca_ttl
- ciecaid - ciecaid
@@ -3314,6 +3316,7 @@
- date_repairstarted - date_repairstarted
- date_scheduled - date_scheduled
- date_towin - date_towin
- date_void
- ded_amt - ded_amt
- ded_note - ded_note
- ded_status - ded_status
@@ -3495,7 +3498,6 @@
- v_model_yr - v_model_yr
- v_vin - v_vin
- vehicleid - vehicleid
- date_void
- voided - voided
select_permissions: select_permissions:
- role: user - role: user
@@ -3539,6 +3541,8 @@
- ca_gst_registrant - ca_gst_registrant
- cat_no - cat_no
- category - category
- cieca_pfl
- cieca_pft
- cieca_stl - cieca_stl
- cieca_ttl - cieca_ttl
- ciecaid - ciecaid
@@ -3580,6 +3584,7 @@
- date_repairstarted - date_repairstarted
- date_scheduled - date_scheduled
- date_towin - date_towin
- date_void
- ded_amt - ded_amt
- ded_note - ded_note
- ded_status - ded_status
@@ -3762,7 +3767,6 @@
- v_model_yr - v_model_yr
- v_vin - v_vin
- vehicleid - vehicleid
- date_void
- voided - voided
filter: filter:
bodyshop: bodyshop:
@@ -3816,6 +3820,8 @@
- ca_gst_registrant - ca_gst_registrant
- cat_no - cat_no
- category - category
- cieca_pfl
- cieca_pft
- cieca_stl - cieca_stl
- cieca_ttl - cieca_ttl
- ciecaid - ciecaid
@@ -3857,6 +3863,7 @@
- date_repairstarted - date_repairstarted
- date_scheduled - date_scheduled
- date_towin - date_towin
- date_void
- ded_amt - ded_amt
- ded_note - ded_note
- ded_status - ded_status
@@ -4039,7 +4046,6 @@
- v_model_yr - v_model_yr
- v_vin - v_vin
- vehicleid - vehicleid
- date_void
- voided - voided
filter: filter:
bodyshop: bodyshop:
@@ -4649,6 +4655,7 @@
_eq: X-Hasura-User-Id _eq: X-Hasura-User-Id
- active: - active:
_eq: true _eq: true
allow_aggregations: true
update_permissions: update_permissions:
- role: user - role: user
permission: permission:
@@ -4726,6 +4733,7 @@
_eq: X-Hasura-User-Id _eq: X-Hasura-User-Id
- active: - active:
_eq: true _eq: true
allow_aggregations: true
update_permissions: update_permissions:
- role: user - role: user
permission: permission:
@@ -5554,6 +5562,7 @@
- committed_at - committed_at
- cost_center - cost_center
- created_at - created_at
- created_by
- date - date
- employeeid - employeeid
- flat_rate - flat_rate
@@ -5578,6 +5587,7 @@
- committed_at - committed_at
- cost_center - cost_center
- created_at - created_at
- created_by
- date - date
- employeeid - employeeid
- flat_rate - flat_rate
@@ -5611,6 +5621,7 @@
- committed_at - committed_at
- cost_center - cost_center
- created_at - created_at
- created_by
- date - date
- employeeid - employeeid
- flat_rate - flat_rate

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."parts_dispatch_employeeid";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "parts_dispatch_employeeid" on
"public"."parts_dispatch" using btree ("employeeid");

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."parts_dispatch_dispatchid";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "parts_dispatch_dispatchid" on
"public"."parts_dispatch_lines" using btree ("partsdispatchid");

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."parts_dispatch_line_accepted_at";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "parts_dispatch_line_accepted_at" on
"public"."parts_dispatch_lines" using btree ("accepted_at");

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."timetickets" add column "created_by" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."timetickets" add column "created_by" text
null;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."jobs" add column "cieca_pfl" jsonb
-- null default jsonb_build_object();

View File

@@ -0,0 +1,2 @@
alter table "public"."jobs" add column "cieca_pfl" jsonb
null default jsonb_build_object();

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "claimscorpid" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "claimscorpid" text
null;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."jobs" add column "cieca_pft" jsonb
-- null default jsonb_build_object();

View File

@@ -0,0 +1,2 @@
alter table "public"."jobs" add column "cieca_pft" jsonb
null default jsonb_build_object();

View File

@@ -42,13 +42,13 @@ const io = new Server(server, {
}); });
exports.io = io; exports.io = io;
require("./server/web-sockets/web-socket"); require("./server/web-sockets/web-socket");
// app.set('trust proxy', true)
//app.use(fb.validateFirebaseIdToken); // app.use(fb.validateFirebaseIdToken);
app.use(compression()); app.use(compression());
app.use(cookieParser()); app.use(cookieParser());
app.use(bodyParser.json({ limit: "50mb" })); app.use(bodyParser.json({ limit: "50mb" }));
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true })); app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
//app.use(enforce.HTTPS({ trustProtoHeader: true })); // app.use(enforce.HTTPS({ trustProtoHeader: true }));
app.use( app.use(
cors({ credentials: true, exposedHeaders: ["set-cookie"] }) cors({ credentials: true, exposedHeaders: ["set-cookie"] })
// cors({ // cors({
@@ -71,10 +71,17 @@ app.get("/test", async function (req, res) {
const commit = require("child_process").execSync( const commit = require("child_process").execSync(
"git rev-parse --short HEAD" "git rev-parse --short HEAD"
); );
// console.log(app.get('trust proxy'));
// console.log("remoteAddress", req.socket.remoteAddress);
// console.log("X-Forwarded-For", req.header('x-forwarded-for'));
logger.log("test-api-status", "DEBUG", "api", { commit }); logger.log("test-api-status", "DEBUG", "api", { commit });
// sendEmail.sendServerEmail({
// subject: `API Check - ${process.env.NODE_ENV}`,
// text: `Server API check has come in. Remote IP: ${req.socket.remoteAddress}, X-Forwarded-For: ${req.header('x-forwarded-for')}`,
// });
sendEmail.sendServerEmail({ sendEmail.sendServerEmail({
subject: `API Check - ${process.env.NODE_ENV}`, subject: `API Check - ${process.env.NODE_ENV}`,
text: `Server API check has come in. `, text: `Server API check has come in.`,
}); });
res.status(200).send(`OK - ${commit}`); res.status(200).send(`OK - ${commit}`);
}); });
@@ -216,6 +223,7 @@ app.post("/qbo/payments", fb.validateFirebaseIdToken, qbo.payments);
var data = require("./server/data/data"); var data = require("./server/data/data");
app.post("/data/ah", data.autohouse); app.post("/data/ah", data.autohouse);
app.post("/data/cc", data.claimscorp);
app.post("/record-handler/arms", data.arms); app.post("/record-handler/arms", data.arms);
var taskHandler = require("./server/tasks/tasks"); var taskHandler = require("./server/tasks/tasks");

View File

@@ -643,6 +643,7 @@ async function InsertAccountPostingData(socket) {
wips.push(item); wips.push(item);
}); });
socket.transWips = wips;
const { data: AccountPostingChange } = await axios.post( const { data: AccountPostingChange } = await axios.post(
PBS_ENDPOINTS.AccountingPostingChange, PBS_ENDPOINTS.AccountingPostingChange,
@@ -697,6 +698,7 @@ async function MarkJobExported(socket, jobid) {
jobid: jobid, jobid: jobid,
successful: true, successful: true,
useremail: socket.user.email, useremail: socket.user.email,
metadata: socket.transWips,
}, },
bill: { bill: {
exported: true, exported: true,

847
server/data/claimscorp.js Normal file
View File

@@ -0,0 +1,847 @@
const path = require("path");
const queries = require("../graphql-client/queries");
const Dinero = require("dinero.js");
const moment = require("moment-timezone");
var builder = require("xmlbuilder2");
const _ = require("lodash");
const logger = require("../utils/logger");
const fs = require("fs");
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
let Client = require("ssh2-sftp-client");
const client = require("../graphql-client/graphql-client").client;
const { sendServerEmail } = require("../email/sendemail");
const CCDineroFormat = "0,0.00";
const AhDateFormat = "MMDDYYYY";
const repairOpCodes = ["OP4", "OP9", "OP10"];
const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"];
const ftpSetup = {
host: process.env.CLAIMSCORP_HOST,
port: process.env.CLAIMSCORP_PORT,
username: process.env.CLAIMSCORP_USER,
password: process.env.CLAIMSCORP_PASSWORD,
debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data),
algorithms: {
serverHostKey: ["ssh-rsa", "ssh-dss"],
},
};
exports.default = async (req, res) => {
//Query for the List of Bodyshop Clients.
logger.log("claimscorp-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_CLAIMSCORP_SHOPS);
const specificShopIds = req.body.bodyshopIds; // ['uuid]
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
res.sendStatus(401);
return;
}
const allxmlsToUpload = [];
const allErrors = [];
try {
for (const bodyshop of specificShopIds
? bodyshops.filter((b) => specificShopIds.includes(b.id))
: bodyshops) {
logger.log("claimscorp-start-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname,
});
const erroredJobs = [];
try {
const { jobs, bodyshops_by_pk } = await client.request(
queries.CLAIMSCORP_QUERY,
{
bodyshopid: bodyshop.id,
start: start
? moment(start).startOf("day")
: moment().subtract(5, "days").startOf("day"),
...(end && { end: moment(end).startOf("day") }),
}
);
const claimsCorpObject = {
ClaimsCorpExport: {
ShopID: bodyshops_by_pk.claimscorpid,
ShopName: bodyshops_by_pk.shopname,
RO: jobs.map((j) =>
CreateRepairOrderTag(
{ ...j, bodyshop: bodyshops_by_pk },
function ({ job, error }) {
erroredJobs.push({ job: job, error: error.toString() });
}
)
),
},
};
if (erroredJobs.length > 0) {
logger.log("claimscorp-failed-jobs", "ERROR", "api", bodyshop.id, {
count: erroredJobs.length,
jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)),
});
}
var ret = builder
.create(
{
// version: "1.0",
// encoding: "UTF-8",
//keepNullNodes: true,
},
claimsCorpObject
)
.end({ allowEmptyTags: true });
allxmlsToUpload.push({
count: claimsCorpObject.ClaimsCorpExport.RO.length,
xml: ret,
filename: `${bodyshop.claimscorpid}-MIS-${moment().format(
"YYYYMMDDTHHMMss"
)}.xml`,
});
logger.log("claimscorp-end-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname,
});
} catch (error) {
//Error at the shop level.
logger.log("claimscorp-error-shop", "ERROR", "api", bodyshop.id, {
...error,
});
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
claimscorpid: bodyshop.claimscorpid,
fatal: true,
errors: [error.toString()],
});
} finally {
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
claimscorpid: bodyshop.claimscorpid,
errors: erroredJobs.map((ej) => ({
ro_number: ej.job?.ro_number,
jobid: ej.job?.id,
error: ej.error,
})),
});
}
}
if (skipUpload) {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
res.json(allxmlsToUpload);
sendServerEmail({
subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`,
});
return;
}
let sftp = new Client();
sftp.on("error", (errors) =>
logger.log("claimscorp-sftp-error", "ERROR", "api", null, {
...errors,
})
);
try {
//Connect to the FTP and upload all.
await sftp.connect(ftpSetup);
for (const xmlObj of allxmlsToUpload) {
logger.log("claimscorp-sftp-upload", "DEBUG", "api", null, {
filename: xmlObj.filename,
});
const uploadResult = await sftp.put(
Buffer.from(xmlObj.xml),
`/${xmlObj.filename}`
);
logger.log("claimscorp-sftp-upload-result", "DEBUG", "api", null, {
uploadResult,
});
}
//***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml
} catch (error) {
logger.log("claimscorp-sftp-error", "ERROR", "api", null, {
...error,
});
} finally {
sftp.end();
}
sendServerEmail({
subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`,
});
res.sendStatus(200);
} catch (error) {
res.status(200).json(error);
}
};
const CreateRepairOrderTag = (job, errorCallback) => {
//Level 2
if (!job.job_totals) {
errorCallback({
jobid: job.id,
job: job,
ro_number: job.ro_number,
error: { toString: () => "No job totals for RO." },
});
return {};
}
const repairCosts = CreateCosts(job);
//Calculate detail only lines.
const detailAdjustments = job.joblines
.filter((jl) => jl.ah_detail_line && jl.mod_lbr_ty)
.reduce(
(acc, val) => {
return {
hours: acc.hours + val.mod_lb_hrs,
amount: acc.amount.add(
Dinero({
amount: Math.round(
(job.job_totals.rates[val.mod_lbr_ty.toLowerCase()].rate || 0) *
val.mod_lb_hrs *
100
),
})
),
};
},
{ hours: 0, amount: Dinero() }
);
try {
const ret = {
RONumber: job.ro_number,
Customer: {
CustomerZip: (job.ownr_zip && job.ownr_zip.substring(0, 3)) || "",
CustomerState: job.ownr_st || "",
},
Vehicle: {
Year: job.v_model_yr
? parseInt(job.v_model_yr.match(/\d/g))
? parseInt(job.v_model_yr.match(/\d/g).join(""), 10)
: ""
: "",
Make: job.v_make_desc || "",
Model: job.v_model_desc || "",
BodyStyle: (job.vehicle && job.vehicle.v_bstyle) || "",
Color: job.v_color || "",
VIN: job.v_vin || "",
},
Carrier: {
InsuranceCo: job.ins_co_nm || "",
CompanyName: job.ins_co_nm || "",
},
Claim: job.clm_no || "",
Contacts: {
PC: job.employee_csr_rel
? `${
job.employee_csr_rel.last_name
? job.employee_csr_rel.last_name
: ""
}${job.employee_csr_rel.last_name ? ", " : ""}${
job.employee_csr_rel.first_name
? job.employee_csr_rel.first_name
: ""
}`
: "",
Phone1: "",
Phone2: "",
EstimatorName: `${job.est_ct_ln ? job.est_ct_ln : ""}${
job.est_ct_ln ? ", " : ""
}${job.est_ct_fn ? job.est_ct_fn : ""}`,
BodyTechnician: job.employee_body_rel
? `${
job.employee_body_rel.last_name
? job.employee_body_rel.last_name
: ""
}${job.employee_body_rel.last_name ? ", " : ""}${
job.employee_body_rel.first_name
? job.employee_body_rel.first_name
: ""
}`
: "",
PaintTechnician: job.employee_refinish_rel
? `${
job.employee_refinish_rel.last_name
? job.employee_refinish_rel.last_name
: ""
}${job.employee_refinish_rel.last_name ? ", " : ""}${
job.employee_refinish_rel.first_name
? job.employee_refinish_rel.first_name
: ""
}`
: "",
},
Dates: {
DateCreated:
(job.date_estimated &&
moment(job.date_estimated).format(AhDateFormat)) ||
"",
DateofLoss:
(job.loss_date && moment(job.loss_date).format(AhDateFormat)) || "",
DateFNOL: "",
DateContact: "",
DateEstimated:
(job.date_estimated &&
moment(job.date_estimated).format(AhDateFormat)) ||
"",
DateScheduled:
(job.scheduled_in &&
moment(job.scheduled_in)
.tz(job.bodyshop.timezone)
.format(AhDateFormat)) ||
"",
DateArrived:
(job.actual_in &&
moment(job.actual_in)
.tz(job.bodyshop.timezone)
.format(AhDateFormat)) ||
"",
DateFirstPartsOrdered:
(job.parts_orders &&
job.parts_orders[0] &&
moment(job.parts_orders[0].created_at)
.tz(job.bodyshop.timezone)
.format(AhDateFormat)) ||
"",
StartDate: job.date_repairstarted
? (job.date_repairstarted &&
moment(job.date_repairstarted)
.tz(job.bodyshop.timezone)
.format(AhDateFormat)) ||
""
: (job.date_repairstarted &&
moment(job.actual_in)
.tz(job.bodyshop.timezone)
.format(AhDateFormat)) ||
"",
BodyStart: "",
BodyEnd: "",
FrameStart: "",
FrameEnd: "",
PrepStart: "",
PrepEnd: "",
SprayStart: "",
SprayEnd: "",
DateReady:
(job.actual_completion &&
moment(job.actual_completion)
.tz(job.bodyshop.timezone)
.format(AhDateFormat)) ||
"",
DateScheduledDelivery:
(job.scheduled_delivery &&
moment(job.scheduled_delivery)
.tz(job.bodyshop.timezone)
.format(AhDateFormat)) ||
"",
DateDelivered:
(job.actual_delivery &&
moment(job.actual_delivery)
.tz(job.bodyshop.timezone)
.format(AhDateFormat)) ||
"",
DateClosed:
(job.date_invoiced &&
moment(job.date_invoiced)
.tz(job.bodyshop.timezone)
.format(AhDateFormat)) ||
"",
BilledDate: "",
PaidInFullDate: "",
RoStatus: job.tlos_ind
? "TOT"
: StatusMapping(job.status, job.bodyshop.md_ro_statuses),
},
Sales: {
Body: Dinero(job.job_totals.rates.lab.total)
.add(Dinero(job.job_totals.rates.laa.total))
.add(Dinero(job.job_totals.rates.lad.total))
.add(Dinero(job.job_totals.rates.las.total))
.toFormat(CCDineroFormat),
Refinish: Dinero(job.job_totals.rates.lar.total).toFormat(
CCDineroFormat
),
Prep: Dinero().toFormat(CCDineroFormat),
Frame: Dinero(job.job_totals.rates.laf.total).toFormat(CCDineroFormat),
Mechanical: Dinero(job.job_totals.rates.lam.total).toFormat(
CCDineroFormat
),
Glass: Dinero(job.job_totals.rates.lag.total).toFormat(CCDineroFormat),
Elec: Dinero(job.job_totals.rates.lae.total).toFormat(CCDineroFormat),
Detail: detailAdjustments.amount.toFormat(CCDineroFormat),
Reassem: Dinero().toFormat(CCDineroFormat),
OtherLabor: Dinero(job.job_totals.rates.la1.total)
.add(Dinero(job.job_totals.rates.la2.total))
.add(Dinero(job.job_totals.rates.la3.total))
.add(Dinero(job.job_totals.rates.la4.total))
.add(Dinero(job.job_totals.rates.lau.total))
.subtract(detailAdjustments.amount)
.toFormat(CCDineroFormat),
BMatl: Dinero(job.job_totals.rates.mash.total).toFormat(CCDineroFormat),
PMatl: Dinero(job.job_totals.rates.mapa.total).toFormat(CCDineroFormat),
OEM: Dinero(
job.job_totals.parts.parts.list.PAN &&
job.job_totals.parts.parts.list.PAN.total
)
.add(
Dinero(
job.job_totals.parts.parts.list.PAP &&
job.job_totals.parts.parts.list.PAP.total
)
)
.toFormat(CCDineroFormat),
LKQ: Dinero(
job.job_totals.parts.parts.list.PAL &&
job.job_totals.parts.parts.list.PAL.total
).toFormat(CCDineroFormat),
AM: Dinero(
job.job_totals.parts.parts.list.PAA &&
job.job_totals.parts.parts.list.PAA.total
).toFormat(CCDineroFormat),
MechParts: Dinero().toFormat(CCDineroFormat),
OtherParts: Dinero(
job.job_totals.parts.parts.list.PAO &&
job.job_totals.parts.parts.list.PAO.total
).toFormat(CCDineroFormat),
OtherSales: Dinero(job.job_totals.additional.storage).toFormat(
CCDineroFormat
),
Sublet: Dinero(job.job_totals.parts.sublets.total).toFormat(
CCDineroFormat
),
Towing: Dinero(job.job_totals.additional.towing).toFormat(
CCDineroFormat
),
Rental:
job.job_totals.additional.additionalCostItems.includes(
"ATS Amount"
) === true
? Dinero(
job.job_totals.additional.additionalCostItems[
job.job_totals.additional.additionalCostItems.indexOf(
"ATS Amount"
)
].total
).toFormat(CCDineroFormat)
: Dinero().toFormat(CCDineroFormat),
HazWaste: Dinero().toFormat(CCDineroFormat),
Discounts: Dinero(job.job_totals.additional.adjustments).toFormat(
CCDineroFormat
),
Tax: Dinero(job.job_totals.totals.local_tax)
.add(Dinero(job.job_totals.totals.state_tax))
.add(Dinero(job.job_totals.totals.federal_tax))
.add(Dinero(job.job_totals.additional.pvrt))
.toFormat(CCDineroFormat),
NetSaleTotal: Dinero(job.job_totals.totals.subtotal).toFormat(
CCDineroFormat
),
SaleTotal: Dinero(job.job_totals.totals.total_repairs).toFormat(
CCDineroFormat
),
},
SaleHours: {
Body: job.job_totals.rates.lab.hours.toFixed(2),
BodyRepairHours: job.joblines
.filter((line) => repairOpCodes.includes(line.lbr_op))
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
.toFixed(2),
BodyReplaceHours: job.joblines
.filter((line) => replaceOpCodes.includes(line.lbr_op))
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
.toFixed(2),
Paint: job.job_totals.rates.lar.hours.toFixed(2),
Prep: "0.00",
FrameHours: job.job_totals.rates.laf.hours.toFixed(2),
MechanicalHours: job.job_totals.rates.lam.hours.toFixed(2),
GlassHours: job.job_totals.rates.lag.hours.toFixed(2),
ElectricalHours: job.job_totals.rates.lae.hours.toFixed(2),
DetailHours: detailAdjustments.hours,
Reassem: "0.00",
Other: (
job.job_totals.rates.la1.hours +
job.job_totals.rates.la2.hours +
job.job_totals.rates.la3.hours +
job.job_totals.rates.la4.hours +
job.job_totals.rates.lau.hours -
detailAdjustments.hours
).toFixed(2),
TotalHours: job.joblines
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
.toFixed(2),
},
Costs: {
Body: repairCosts.BodyLaborTotalCost.toFormat(CCDineroFormat),
Paint: repairCosts.RefinishLaborTotalCost.toFormat(CCDineroFormat),
Prep: Dinero().toFormat(CCDineroFormat),
Frame: Dinero(job.job_totals.rates.laf.total).toFormat(CCDineroFormat),
Mech: repairCosts.MechanicalLaborTotalCost.toFormat(CCDineroFormat),
Glass: repairCosts.GlassLaborTotalCost.toFormat(CCDineroFormat),
Elec: repairCosts.ElectricalLaborTotalCost.toFormat(CCDineroFormat),
Detail: Dinero().toFormat(CCDineroFormat),
Reassem: Dinero().toFormat(CCDineroFormat),
OtherLabor: repairCosts.LaborMiscTotalCost.toFormat(CCDineroFormat),
Bmatl: repairCosts.BMTotalCost.toFormat(CCDineroFormat),
Pmatl: repairCosts.PMTotalCost.toFormat(CCDineroFormat),
OEM: repairCosts.PartsOemCost.toFormat(CCDineroFormat),
LKQ: repairCosts.PartsRecycledCost.toFormat(CCDineroFormat),
AM: repairCosts.PartsAMCost.toFormat(CCDineroFormat),
MechParts: Dinero().toFormat(CCDineroFormat),
OtherParts: Dinero().toFormat(CCDineroFormat), //Check Synergy
OtherCosts: repairCosts.PartsOtherCost.toFormat(CCDineroFormat),
Sublet: repairCosts.SubletTotalCost.toFormat(CCDineroFormat),
Towing: repairCosts.TowingTotalCost.toFormat(CCDineroFormat),
Storage: repairCosts.StorageTotalCost.toFormat(CCDineroFormat),
Rental: Dinero().toFormat(CCDineroFormat),
HazWaste: Dinero().toFormat(CCDineroFormat),
CostTotal: repairCosts.TotalCost.toFormat(CCDineroFormat),
},
CostHours: {
Body: repairCosts.BodyLaborTotalHrs.toFixed(2),
Paint: repairCosts.RefinishLaborTotalHrs.toFixed(2),
Prep: "0.00",
Frame: repairCosts.FrameLaborTotalHrs.toFixed(2),
Mech: repairCosts.MechanicalLaborTotalHrs.toFixed(2),
Glass: repairCosts.GlassLaborTotalHrs.toFixed(2),
Elec: repairCosts.ElectricalLaborTotalHrs.toFixed(2),
Detail: "0.00",
Other: repairCosts.LaborMiscTotalHrs.toFixed(2),
CostTotalHours: repairCosts.TotalHrs.toFixed(2),
},
};
return ret;
} catch (error) {
logger.log("claimscorp-job-calculate-error", "ERROR", "api", null, {
error,
});
errorCallback({ jobid: job.id, ro_number: job.ro_number, error });
}
};
const CreateCosts = (job) => {
//Create a mapping based on AH Requirements
//For DMS, the keys in the object below are the CIECA part types.
const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => {
//At the bill level.
bill_val.billlines.map((line_val) => {
//At the bill line level.
if (!bill_acc[line_val.cost_center])
bill_acc[line_val.cost_center] = Dinero();
bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add(
Dinero({
amount: Math.round((line_val.actual_cost || 0) * 100),
})
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1)
);
return null;
});
return bill_acc;
}, {});
//If the hourly rates for job costing are set, add them in.
if (
job.bodyshop.jc_hourly_rates &&
(job.bodyshop.jc_hourly_rates.mapa ||
typeof job.bodyshop.jc_hourly_rates.mapa === "number" ||
isNaN(job.bodyshop.jc_hourly_rates.mapa) === false)
) {
if (
!billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
]
)
billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = Dinero();
if (job.bodyshop.use_paint_scale_data === true) {
if (job.mixdata.length > 0) {
billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = Dinero({
amount: Math.round(
((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100
),
});
} else {
billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
].add(
Dinero({
amount: Math.round(
(job.bodyshop.jc_hourly_rates &&
job.bodyshop.jc_hourly_rates.mapa * 100) ||
0
),
}).multiply(job.job_totals.rates.mapa.hours)
);
}
} else {
billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
].add(
Dinero({
amount: Math.round(
(job.bodyshop.jc_hourly_rates &&
job.bodyshop.jc_hourly_rates.mapa * 100) ||
0
),
}).multiply(job.job_totals.rates.mapa.hours)
);
}
}
if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) {
if (
!billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MASH
]
)
billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MASH
] = Dinero();
billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MASH
] = billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MASH
].add(
Dinero({
amount: Math.round(
(job.bodyshop.jc_hourly_rates &&
job.bodyshop.jc_hourly_rates.mash * 100) ||
0
),
}).multiply(job.job_totals.rates.mash.hours)
);
}
//Uses CIECA Labor types.
const ticketTotalsByCostCenter = job.timetickets.reduce(
(ticket_acc, ticket_val) => {
//At the invoice level.
if (!ticket_acc[ticket_val.cost_center])
ticket_acc[ticket_val.cost_center] = Dinero();
ticket_acc[ticket_val.cost_center] = ticket_acc[
ticket_val.cost_center
].add(
Dinero({
amount: Math.round((ticket_val.rate || 0) * 100),
}).multiply(
(ticket_val.flat_rate
? ticket_val.productivehrs
: ticket_val.actualhrs) || 0
)
);
return ticket_acc;
},
{}
);
const ticketHrsByCostCenter = job.timetickets.reduce(
(ticket_acc, ticket_val) => {
//At the invoice level.
if (!ticket_acc[ticket_val.cost_center])
ticket_acc[ticket_val.cost_center] = 0;
ticket_acc[ticket_val.cost_center] =
ticket_acc[ticket_val.cost_center] +
(ticket_val.flat_rate
? ticket_val.productivehrs
: ticket_val.actualhrs) || 0;
return ticket_acc;
},
{}
);
//CIECA STANDARD MAPPING OBJECT.
const ciecaObj = {
ATS: "ATS",
LA1: "LA1",
LA2: "LA2",
LA3: "LA3",
LA4: "LA4",
LAA: "LAA",
LAB: "LAB",
LAD: "LAD",
LAE: "LAE",
LAF: "LAF",
LAG: "LAG",
LAM: "LAM",
LAR: "LAR",
LAS: "LAS",
LAU: "LAU",
PAA: "PAA",
PAC: "PAC",
PAG: "PAG",
PAL: "PAL",
PAM: "PAM",
PAN: "PAN",
PAO: "PAO",
PAP: "PAP",
PAR: "PAR",
PAS: "PAS",
TOW: "TOW",
MAPA: "MAPA",
MASH: "MASH",
PASL: "PASL",
};
const defaultCosts =
job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber
? ciecaObj
: job.bodyshop.md_responsibility_centers.defaults.costs;
return {
PartsTotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => {
if (
key !== defaultCosts.PAS &&
key !== defaultCosts.PASL &&
key !== defaultCosts.MAPA &&
key !== defaultCosts.MASH &&
key !== defaultCosts.TOW
)
return acc.add(billTotalsByCostCenters[key]);
return acc;
}, Dinero()),
PartsOemCost: (billTotalsByCostCenters[defaultCosts.PAN] || Dinero()).add(
billTotalsByCostCenters[defaultCosts.PAP] || Dinero()
),
PartsAMCost: billTotalsByCostCenters[defaultCosts.PAA] || Dinero(),
PartsReconditionedCost:
billTotalsByCostCenters[defaultCosts.PAM] || Dinero(),
PartsRecycledCost: billTotalsByCostCenters[defaultCosts.PAL] || Dinero(),
PartsOtherCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(),
SubletTotalCost:
billTotalsByCostCenters[defaultCosts.PAS] ||
Dinero(billTotalsByCostCenters[defaultCosts.PASL] || Dinero()),
BodyLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAB] || Dinero(),
BodyLaborTotalHrs: ticketHrsByCostCenter[defaultCosts.LAB] || 0,
RefinishLaborTotalCost:
ticketTotalsByCostCenter[defaultCosts.LAR] || Dinero(),
RefinishLaborTotalHrs: ticketHrsByCostCenter[defaultCosts.LAR] || 0,
MechanicalLaborTotalCost:
ticketTotalsByCostCenter[defaultCosts.LAM] || Dinero(),
MechanicalLaborTotalHrs: ticketHrsByCostCenter[defaultCosts.LAM] || 0,
StructuralLaborTotalCost:
ticketTotalsByCostCenter[defaultCosts.LAS] || Dinero(),
StructuralLaborTotalHrs: ticketHrsByCostCenter[defaultCosts.LAS] || 0,
ElectricalLaborTotalCost:
ticketTotalsByCostCenter[defaultCosts.LAE] || Dinero(),
ElectricalLaborTotalHrs: ticketHrsByCostCenter[defaultCosts.LAE] || 0,
FrameLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAF] || Dinero(),
FrameLaborTotalHrs: ticketHrsByCostCenter[defaultCosts.LAF] || 0,
GlassLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAG] || Dinero(),
GlassLaborTotalHrs: ticketHrsByCostCenter[defaultCosts.LAG] || 0,
DetailLaborTotalCost: Dinero(),
// ticketTotalsByCostCenter[defaultCosts.LAD] || Dinero(),
LaborMiscTotalCost: (ticketTotalsByCostCenter[defaultCosts.LA1] || Dinero())
.add(ticketTotalsByCostCenter[defaultCosts.LA2] || Dinero())
.add(ticketTotalsByCostCenter[defaultCosts.LA2] || Dinero())
.add(ticketTotalsByCostCenter[defaultCosts.LA3] || Dinero())
.add(ticketTotalsByCostCenter[defaultCosts.LA4] || Dinero())
.add(ticketTotalsByCostCenter[defaultCosts.LAU] || Dinero()),
LaborMiscTotalHrs:
(ticketHrsByCostCenter[defaultCosts.LA1] || 0) +
(ticketHrsByCostCenter[defaultCosts.LA2] || 0) +
(ticketHrsByCostCenter[defaultCosts.LA3] || 0) +
(ticketHrsByCostCenter[defaultCosts.LA4] || 0) +
(ticketHrsByCostCenter[defaultCosts.LAU] || 0),
PMTotalCost: billTotalsByCostCenters[defaultCosts.MAPA] || Dinero(),
BMTotalCost: billTotalsByCostCenters[defaultCosts.MASH] || Dinero(),
MiscTotalCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(),
TowingTotalCost: billTotalsByCostCenters[defaultCosts.TOW] || Dinero(),
StorageTotalCost: Dinero(),
DetailTotal: Dinero(),
DetailTotalCost: Dinero(),
SalesTaxTotalCost: Dinero(),
LabourTotalCost: Object.keys(ticketTotalsByCostCenter).reduce(
(acc, key) => {
return acc.add(ticketTotalsByCostCenter[key]);
},
Dinero()
),
TotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => {
return acc.add(billTotalsByCostCenters[key]);
}, Dinero()),
TotalHrs: job.timetickets.reduce((acc, ticket_val) => {
return (
acc +
(ticket_val.flat_rate
? ticket_val.productivehrs
: ticket_val.actualhrs) || 0
);
}, 0),
};
};
const StatusMapping = (status, md_ro_statuses) => {
//Possible return statuses CLO, CAN, OPN
const {
default_imported,
default_open,
default_scheduled,
default_arrived,
default_completed,
default_delivered,
default_invoiced,
default_exported,
default_void,
} = md_ro_statuses;
if (
status === default_open ||
status === default_imported ||
status === default_scheduled ||
status === default_arrived ||
status === default_completed ||
status === default_delivered ||
md_ro_statuses.production_statuses.includes(status)
)
return "OPN";
else if (status === default_invoiced || status === default_exported)
return "CLO";
else if (status === default_void) return "CAN";
else return "UNDEFINED";
};

View File

@@ -1,2 +1,3 @@
exports.autohouse = require("./autohouse").default; exports.autohouse = require("./autohouse").default;
exports.arms = require("./arms").default; exports.claimscorp = require("./claimscorp").default;
exports.arms = require("./arms").default;

View File

@@ -653,6 +653,7 @@ exports.QUERY_EMPLOYEE_PIN = `query QUERY_EMPLOYEE_PIN($shopId: uuid!, $employee
employee_number employee_number
id id
pin pin
active
} }
}`; }`;
@@ -837,6 +838,179 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop
} }
`; `;
exports.CLAIMSCORP_QUERY = `query CLAIMSCORP_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) {
bodyshops_by_pk(id: $bodyshopid){
id
shopname
address1
city
state
zip_post
country
phone
md_ro_statuses
md_order_statuses
claimscorpid
md_responsibility_centers
jc_hourly_rates
cdk_dealerid
pbs_serialnumber
use_paint_scale_data
timezone
}
jobs(where: {_and: [{converted: {_eq: true}}, {updated_at: {_gt: $start}}, {updated_at: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}]}) {
id
created_at
ro_number
status
est_ct_fn
est_ct_ln
ownr_st
ownr_zip
tlos_ind
v_color
v_model_yr
v_model_desc
v_make_desc
v_vin
vehicle {
v_bstyle
}
ins_co_nm
clm_no
loss_date
asgn_date
date_estimated
date_open
scheduled_in
actual_in
scheduled_completion
actual_completion
scheduled_delivery
actual_delivery
date_invoiced
date_exported
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_matd
rate_mapa
rate_mash
job_totals
parts_tax_rates
date_repairstarted
joblines(where: {removed: {_eq: false}}) {
id
line_no
line_ind
status
line_ind
db_price
act_price
mod_lb_hrs
mod_lbr_ty
line_desc
prt_dsmk_m
prt_dsmk_p
part_qty
part_type
oem_partno
lbr_op
profitcenter_part
profitcenter_labor
ah_detail_line
parts_order_lines(order_by: {parts_order: {order_date: desc_nulls_last}} limit: 1){
parts_order{
id
order_date
}
}
billlines(order_by: {bill: {date: desc_nulls_last}} limit: 1) {
actual_cost
actual_price
quantity
bill {
vendor {
name
}
invoice_number
date
}
}
}
bills {
id
federal_tax_rate
local_tax_rate
state_tax_rate
is_credit_memo
billlines {
actual_cost
cost_center
id
quantity
}
}
employee_body_rel {
first_name
last_name
employee_number
id
}
employee_csr_rel {
first_name
last_name
employee_number
id
}
employee_prep_rel {
first_name
last_name
employee_number
id
}
employee_refinish_rel {
first_name
last_name
employee_number
id
}
parts_orders(limit: 1, order_by: {created_at: desc}) {
created_at
}
timetickets {
id
rate
cost_center
actualhrs
productivehrs
flat_rate
}
mixdata(limit: 1, order_by: {updated_at: desc}) {
jobid
totalliquidcost
}
}
}
`;
exports.ENTEGRAL_EXPORT = ` exports.ENTEGRAL_EXPORT = `
query ENTEGRAL_EXPORT($bodyshopid: uuid!) { query ENTEGRAL_EXPORT($bodyshopid: uuid!) {
jobs(where: {_and: [{converted: {_eq: true}}, {shopid: {_eq: $bodyshopid}}]}) { jobs(where: {_and: [{converted: {_eq: true}}, {shopid: {_eq: $bodyshopid}}]}) {
@@ -1383,6 +1557,27 @@ exports.GET_AUTOHOUSE_SHOPS = `query GET_AUTOHOUSE_SHOPS {
} }
`; `;
exports.GET_CLAIMSCORP_SHOPS = `query GET_CLAIMSCORP_SHOPS {
bodyshops(where: {claimscorpid: {_is_null: false}}){
id
shopname
address1
city
state
zip_post
country
phone
md_ro_statuses
md_order_statuses
claimscorpid
md_responsibility_centers
jc_hourly_rates
imexshopid
timezone
}
}
`;
exports.GET_ENTEGRAL_SHOPS = `query GET_AUTOHOUSE_SHOPS { exports.GET_ENTEGRAL_SHOPS = `query GET_AUTOHOUSE_SHOPS {
bodyshops(where: {entegral_id: {_is_null: false}}){ bodyshops(where: {entegral_id: {_is_null: false}}){
id id

View File

@@ -23,7 +23,7 @@ exports.techLogin = async (req, res) => {
let technician; let technician;
if (result.employees && result.employees[0]) { if (result.employees && result.employees[0]) {
const dbRecord = result.employees[0]; const dbRecord = result.employees[0];
if (dbRecord.pin === pin) { if (dbRecord.pin === pin && dbRecord.active === true) {
valid = true; valid = true;
delete dbRecord.pin; delete dbRecord.pin;
technician = dbRecord; technician = dbRecord;