Compare commits

...

95 Commits

Author SHA1 Message Date
Patrick Fic
d2b2a5399d IO-1327 Resolve dinero error on bill posting 2021-08-20 08:07:01 -07:00
Patrick Fic
ba5400d04a IO-1307 Resolve selection issues on export tables. 2021-08-18 13:18:30 -07:00
Patrick Fic
0190e737c1 IO-1297 resolve decimal issue on time ticket list. 2021-08-18 12:58:26 -07:00
Patrick Fic
8f52efbaca IO-1289 Resolve scoreboard UI issues. 2021-08-13 16:33:36 -07:00
Patrick Fic
d6dc5db185 IO-1296 Resolve missing translation. 2021-08-13 16:20:45 -07:00
Patrick Fic
58d1859640 IO-1306 Show Export Attempts on export screens. 2021-08-13 16:17:16 -07:00
Patrick Fic
dba648aea2 IO-1303 Adjust jobline status alignment. 2021-08-13 15:52:02 -07:00
Patrick Fic
12f6206f88 IO-1224 IO-1297 IO-1298 Adjust decimal place displays in various locations 2021-08-13 15:48:12 -07:00
Patrick Fic
7d9fd06b6d IO-233 CDK Get Makes & Taxes calculation. 2021-08-13 15:08:44 -07:00
Patrick Fic
b68de683b0 Merged in hotfix/2021-08-12 (pull request #179)
Merged in hotfix/2021-08-12 (pull request #178)

Approved-by: Patrick Fic
2021-08-12 21:30:32 +00:00
Patrick Fic
ef79ccc299 Additional Checklist resolutions. 2021-08-12 13:20:56 -07:00
Patrick Fic
8ff2a6e6c4 Resolve delete checklist item. 2021-08-12 13:16:26 -07:00
Patrick Fic
be2cfb908a IO-1312 Resolve delivery checklist issue. 2021-08-12 13:01:20 -07:00
Patrick Fic
77819b06fe Merged in hotfix/2021-08-11 (pull request #169)
hotfix/2021-08-11

Approved-by: Patrick Fic
2021-08-11 20:03:30 +00:00
Patrick Fic
4e936b4cff Merged in hotfix/2021-08-11 (pull request #168)
hotfix/2021-08-11

Approved-by: Patrick Fic
2021-08-11 20:03:10 +00:00
Patrick Fic
f45f351678 IO-1310 resolve production list issue. 2021-08-11 12:57:46 -07:00
Patrick Fic
311171fa0a Merged in feature/2021-08-13 (pull request #166)
Merge into CDK Branch

Approved-by: Patrick Fic
2021-08-11 18:22:20 +00:00
Patrick Fic
7e05f03f4d Resolve merge conflicts. 2021-08-11 11:21:49 -07:00
Patrick Fic
18618efdc0 Include sentry username 2021-08-11 11:16:22 -07:00
Patrick Fic
fe5f4f2727 IO-1300 Resolve null line desc. 2021-08-11 11:09:16 -07:00
Patrick Fic
fd7c907b8f IO-233 Beging get vehicle makes 2021-08-11 11:08:03 -07:00
Patrick Fic
e86160e530 IO-1307 Resolve table check error on export tables. 2021-08-11 08:06:51 -07:00
Patrick Fic
9f7e0d611a IO-1300 Resolve line desc to lower case error. 2021-08-11 07:59:32 -07:00
Patrick Fic
0b523efa95 IO-1304 Resolve ins co nm update on import. 2021-08-11 07:58:01 -07:00
Patrick Fic
6b64499e24 IO-233 Resolv merge issues & move allocations to ws 2021-08-10 14:30:08 -07:00
Patrick Fic
e78e628bec Merge branch 'feature/cdk' into feature/2021-08-13 2021-08-10 13:58:58 -07:00
Patrick Fic
ed8eb51c2f IO-233 CDK Allocations Summary 2021-08-10 13:42:53 -07:00
Patrick Fic
124d68ef68 IO-233 CDK Shop Config 2021-08-10 08:40:38 -07:00
Patrick Fic
3e802c1465 Merged in feature/2021-08-06 (pull request #162)
Feature/2021 08 06
2021-08-06 19:31:18 +00:00
Patrick Fic
ac18e78897 Merged in feature/2021-08-06 (pull request #161)
Resolve CI Error
2021-08-05 16:27:15 +00:00
Patrick Fic
e37ee39e88 Resolve CI Error 2021-08-05 09:25:59 -07:00
Patrick Fic
fed16a4aa3 Merged in feature/2021-08-06 (pull request #160)
Feature/2021 08 06
2021-08-05 16:18:03 +00:00
Patrick Fic
8c94dfce9e Time ticket sort update & remove io event logging. 2021-08-05 09:11:53 -07:00
Patrick Fic
69c13dd052 IO-1286 Resolve supplementing issues. 2021-08-05 09:10:09 -07:00
Patrick Fic
a5fe54164e IO-1285 Resolve production not saving sort order. 2021-08-04 09:30:53 -07:00
Patrick Fic
5ecd5a5a5c IO-1122 Add permission to text to intake checklist. 2021-08-04 09:10:23 -07:00
Patrick Fic
ed45347c23 Merged in feature/2021-07-30 (pull request #159)
feature/2021-07-30

Approved-by: Patrick Fic
2021-08-04 00:42:40 +00:00
Patrick Fic
3e0385479f IO-1234 Remove excess resp. center setup. 2021-08-03 17:28:32 -07:00
Patrick Fic
36f833be91 IO-1281 Prevent posting of tickets to closed RO 2021-08-03 16:52:06 -07:00
Patrick Fic
8fb39f9ea4 Merged in feature/2021-07-30 (pull request #158)
feature/2021-07-30

Approved-by: Patrick Fic
2021-08-03 18:46:24 +00:00
Patrick Fic
5b84ebbc25 IO-1284 Job Admin updates 2021-08-03 11:45:36 -07:00
Patrick Fic
b0ec7867b5 IO-1281 System setting for posting time tickets to closed RO 2021-08-03 11:27:03 -07:00
Patrick Fic
4e4c59ce4d IO-1262 Add SMS reminder to schedule. 2021-08-03 11:14:49 -07:00
Patrick Fic
e9bf1c05ad IO-594 Adjust Autohouse FIle Name 2021-08-03 10:56:33 -07:00
Patrick Fic
7ac1fa5abf Merged in feature/2021-07-30 (pull request #157)
Feature/2021 07 30
2021-07-30 22:25:33 +00:00
Patrick Fic
a489ac1d26 Merged in feature/2021-07-30 (pull request #156)
Remove FCM token.

Approved-by: Patrick Fic
2021-07-30 22:21:27 +00:00
Patrick Fic
4d7a7442ce Remove FCM token. 2021-07-30 15:21:04 -07:00
Patrick Fic
a19bce5a37 Merged in feature/2021-07-30 (pull request #155)
feature/2021-07-30

Approved-by: Patrick Fic
2021-07-30 22:11:48 +00:00
Patrick Fic
35782244bf Merge branch 'feature/2021-07-30' of bitbucket.org:snaptsoft/bodyshop into feature/2021-07-30 2021-07-30 15:11:21 -07:00
Patrick Fic
7407429344 Final phone resolution. 2021-07-30 15:11:16 -07:00
Patrick Fic
55c532f6e2 Merged in feature/2021-07-30 (pull request #154)
Resolve more Phone Lib issues.
2021-07-30 17:24:36 +00:00
Patrick Fic
d3c8b5d731 Resolve more Phone Lib issues. 2021-07-30 10:23:57 -07:00
Patrick Fic
cf09f98d7e Merged in feature/2021-07-30 (pull request #153)
Resolve break with Phone Package.
2021-07-30 16:46:38 +00:00
Patrick Fic
d8e8a8e4e9 Resolve break with Phone Package. 2021-07-30 09:45:59 -07:00
Patrick Fic
65210dea2f Merged in feature/2021-07-30 (pull request #152)
Add several reports to report center.

Approved-by: Patrick Fic
2021-07-29 21:06:18 +00:00
Patrick Fic
0c9b850872 Add several reports to report center. 2021-07-29 14:05:46 -07:00
Patrick Fic
99486830b7 Merged in feature/2021-07-30 (pull request #151)
feature/2021-07-30

Approved-by: Patrick Fic
2021-07-29 20:24:45 +00:00
Patrick Fic
74a62a46d3 IO-1280 Sorting on Available jobs. 2021-07-29 13:24:14 -07:00
Patrick Fic
d306041bcf IO-992 Audit trail bugfixes. 2021-07-29 13:18:28 -07:00
Patrick Fic
ae8a924cd6 IO-1273 Resolve dashboard error 2 2021-07-28 16:31:32 -07:00
Patrick Fic
6ab1b9f787 Merged in feature/2021-07-30 (pull request #150)
feature/2021-07-30

Approved-by: Patrick Fic
2021-07-28 22:27:36 +00:00
Patrick Fic
46ddc440fe IO-992 WIP Job Audits 2021-07-28 15:25:01 -07:00
Patrick Fic
6bf8eacfbd IO-1280 Resolve available jobs sort. 2021-07-28 15:24:58 -07:00
Patrick Fic
b2fa4f220d IO-1277 Protect production note on checklist. 2021-07-28 12:13:17 -07:00
Patrick Fic
2f175c304c IO-1276 Remove ability to return IH invoice. 2021-07-28 12:02:43 -07:00
Patrick Fic
79714e5708 IO-992 Job Audit additions. 2021-07-28 11:58:31 -07:00
Patrick Fic
7c5aa9c913 IO-1275 Finish appointment notes. 2021-07-28 11:34:35 -07:00
Patrick Fic
59b8bae182 IO-1275 WIP Appointment notes. 2021-07-28 11:00:02 -07:00
Patrick Fic
35323ba624 IO-1273 Graceful error on job totals. 2021-07-27 11:19:23 -07:00
Patrick Fic
8ca3741a52 IO-1274 Change Password on profilel 2021-07-26 16:54:31 -07:00
Patrick Fic
c3c021774e Merged in feature/2021-07-30 (pull request #149)
feature/2021-07-30

Approved-by: Patrick Fic
2021-07-22 23:30:47 +00:00
Patrick Fic
6b811d635b IO-992 Job Audit Logs 2021-07-22 16:29:41 -07:00
Patrick Fic
97aecd3ddc IO-594 add SSH key support for AH 2021-07-22 08:40:05 -07:00
Patrick Fic
c4fdef445e Merged in feature/2021-07-23 (pull request #148)
Feature/2021 07 23
2021-07-22 00:23:07 +00:00
Patrick Fic
e642087360 Merged in feature/2021-07-23 (pull request #147)
IO-594 Add AH Settings
2021-07-21 20:18:47 +00:00
Patrick Fic
6ce2d2723b Merged in feature/2021-07-23 (pull request #146)
Feature/2021 07 23
2021-07-21 18:21:44 +00:00
Patrick Fic
79e11dda4c Merged in feature/2021-07-23 (pull request #145)
IO-594 Create schedulable AH export.
2021-07-20 23:26:16 +00:00
Patrick Fic
f75f88840f Merged in feature/2021-07-23 (pull request #144)
Feature/2021 07 23
2021-07-20 17:56:01 +00:00
Patrick Fic
11cfef904b Merged in feature/2021-07-16 (pull request #143)
Feature/2021 07 16
2021-07-16 21:51:15 +00:00
Patrick Fic
a45dcd307b Merged in feature/2021-07-16 (pull request #142)
Feature/2021 07 16
2021-07-16 21:45:25 +00:00
Patrick Fic
7f4a36038e Merged in feature/2021-07-16 (pull request #141)
IO-1189 IO-1190 IO-1259 Added gen doc keys.
2021-07-16 17:15:46 +00:00
Patrick Fic
12307cbd56 Merged in feature/2021-07-16 (pull request #140)
IO-1264 Add CSR reports
2021-07-16 16:26:46 +00:00
Patrick Fic
c9ed8a9360 Merged in feature/2021-07-16 (pull request #139)
Feature/2021 07 16
2021-07-16 15:32:01 +00:00
Patrick Fic
120d6f9f5f Merged in feature/2021-07-16 (pull request #138)
feature/2021-07-16

Approved-by: Patrick Fic
2021-07-15 22:11:58 +00:00
Patrick Fic
6f64cb71f2 Merged in feature/2021-07-16 (pull request #137)
feature/2021-07-16
2021-07-15 18:42:45 +00:00
Patrick Fic
7a6a834998 Merged in feature/2021-07-16 (pull request #136)
feature/2021-07-16

Approved-by: Patrick Fic
2021-07-13 19:32:15 +00:00
Patrick Fic
7999895323 Merged in feature/2021-07-09 (pull request #135)
Feature/2021 07 09
2021-07-09 20:52:55 +00:00
Patrick Fic
71ef3dadc5 Merged in feature/2021-07-09 (pull request #132)
Feature/2021 07 09
2021-07-08 01:06:58 +00:00
Patrick Fic
f0d6c5e1b1 IO-233 CDK WIP 2021-07-06 09:31:01 -07:00
Patrick Fic
991df9c48f Merged in hotfix/2021-07-02 (pull request #128)
hotfix/2021-07-02

Approved-by: Patrick Fic
2021-07-02 20:34:59 +00:00
Patrick Fic
84b39f3d2b IO-233 WIP CDK 2021-07-02 12:53:18 -07:00
Patrick Fic
4ab0947cc8 IO-233 Add customer insert actions. 2021-06-30 16:04:01 -07:00
Patrick Fic
8cbef14ea3 Merged in hotfix/2021-06-30 (pull request #126)
Hotfix/2021 06 30
2021-06-30 20:47:05 +00:00
Patrick Fic
105ecd4221 IO-233 CDK 2021-06-30 13:41:00 -07:00
Patrick Fic
3176cfcc56 Merged in hotfix/2021-06-30 (pull request #124)
Hotfix/2021 06 30
2021-06-30 20:17:34 +00:00
155 changed files with 8334 additions and 6973 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -4,39 +4,39 @@
"private": true,
"proxy": "http://localhost:5000",
"dependencies": {
"@apollo/client": "^3.3.17",
"@craco/craco": "^5.9.0",
"@fingerprintjs/fingerprintjs": "^3.1.2",
"@apollo/client": "^3.3.21",
"@craco/craco": "^6.2.0",
"@fingerprintjs/fingerprintjs": "^3.2.0",
"@lourenci/react-kanban": "^2.1.0",
"@sentry/react": "^6.3.6",
"@sentry/tracing": "^6.3.6",
"@sentry/react": "^6.10.0",
"@sentry/tracing": "^6.10.0",
"@stripe/react-stripe-js": "^1.4.0",
"@stripe/stripe-js": "^1.14.0",
"@tanem/react-nprogress": "^3.0.65",
"antd": "^4.15.5",
"@stripe/stripe-js": "^1.16.0",
"@tanem/react-nprogress": "^3.0.74",
"antd": "^4.16.8",
"apollo-link-logger": "^2.0.0",
"axios": "^0.21.1",
"craco-less": "^1.17.1",
"dinero.js": "^1.8.1",
"dotenv": "^9.0.2",
"craco-less": "^1.18.0",
"dinero.js": "^1.9.0",
"dotenv": "^10.0.0",
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0",
"exifr": "^7.0.0",
"firebase": "^8.6.0",
"graphql": "^15.5.0",
"i18next": "^20.2.2",
"i18next-browser-languagedetector": "^6.1.1",
"jsoneditor": "^9.4.1",
"exifr": "^7.1.2",
"firebase": "^8.7.1",
"graphql": "^15.5.1",
"i18next": "^20.3.4",
"i18next-browser-languagedetector": "^6.1.2",
"jsoneditor": "^9.5.2",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.9.17",
"libphonenumber-js": "^1.9.22",
"logrocket": "^1.2.0",
"markerjs2": "^2.8.1",
"markerjs2": "^2.9.0",
"moment-business-days": "^1.2.0",
"phone": "^2.4.21",
"phone": "^3.1.2",
"preval.macro": "^5.0.0",
"prop-types": "^15.7.2",
"query-string": "^7.0.0",
"rc-queue-anim": "^1.8.5",
"query-string": "^7.0.1",
"rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6",
"react": "^17.0.1",
"react-big-calendar": "^0.33.2",
@@ -45,26 +45,26 @@
"react-drag-listview": "^0.1.8",
"react-grid-gallery": "^0.5.5",
"react-grid-layout": "^1.2.5",
"react-i18next": "^11.8.15",
"react-i18next": "^11.11.3",
"react-icons": "^4.2.0",
"react-number-format": "^4.5.5",
"react-number-format": "^4.6.4",
"react-redux": "^7.2.4",
"react-resizable": "^3.0.1",
"react-resizable": "^3.0.4",
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.3",
"recharts": "^2.0.7",
"recharts": "^2.0.10",
"redux": "^4.1.0",
"redux-persist": "^6.0.0",
"redux-saga": "^1.1.3",
"redux-state-sync": "^3.1.2",
"reselect": "^4.0.0",
"sass": "^1.32.13",
"socket.io-client": "^4.1.2",
"sass": "^1.35.2",
"socket.io-client": "^4.1.3",
"styled-components": "^5.3.0",
"subscriptions-transport-ws": "^0.9.18",
"web-vitals": "^1.1.2",
"web-vitals": "^2.1.0",
"workbox-background-sync": "^6.1.5",
"workbox-broadcast-update": "^6.1.5",
"workbox-cacheable-response": "^6.1.5",

View File

@@ -109,6 +109,17 @@ export default function AccountingPayablesTableComponent({ loading, bills }) {
<Checkbox disabled checked={record.is_credit_memo} />
),
},
{
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
@@ -121,6 +132,7 @@ export default function AccountingPayablesTableComponent({ loading, bills }) {
billId={record.id}
disabled={transInProgress || !!record.exported}
loadingCallback={setTransInProgress}
setSelectedBills={setSelectedBills}
/>
</div>
),

View File

@@ -108,7 +108,17 @@ export default function AccountingPayablesTableComponent({
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
),
},
{
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
@@ -120,6 +130,7 @@ export default function AccountingPayablesTableComponent({
paymentId={record.id}
disabled={transInProgress || !!record.exportedat}
loadingCallback={setTransInProgress}
setSelectedPayments={setSelectedPayments}
/>
),
},

View File

@@ -114,17 +114,28 @@ export default function AccountingReceivablesTableComponent({ loading, jobs }) {
);
},
},
{
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => (
<Space wrap>
<JobExportButton
jobId={record.id}
disabled={!!record.date_exported}
setSelectedJobs={setSelectedJobs}
/>
<Link to={`/manage/jobs/${record.id}/close`}>
<Button>{t("jobs.labels.viewallocations")}</Button>

View File

@@ -26,6 +26,8 @@ import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-bu
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -33,6 +35,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
@@ -40,7 +44,10 @@ export default connect(
mapDispatchToProps
)(BillDetailEditcontainer);
export function BillDetailEditcontainer({ setPartsOrderContext }) {
export function BillDetailEditcontainer({
setPartsOrderContext,
insertAuditTrail,
}) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const { t } = useTranslation();
@@ -134,6 +141,12 @@ export function BillDetailEditcontainer({ setPartsOrderContext }) {
});
await Promise.all(updates);
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();

View File

@@ -11,12 +11,14 @@ import {
QUERY_JOB_LBR_ADJUSTMENTS,
UPDATE_JOB,
} from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import BillFormContainer from "../bill-form/bill-form.container";
import { handleUpload } from "../documents-upload/documents-upload.utility";
@@ -27,6 +29,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
function BillEnterModalContainer({
@@ -34,6 +38,7 @@ function BillEnterModalContainer({
toggleModalVisible,
bodyshop,
currentUser,
insertAuditTrail,
}) {
const [form] = Form.useForm();
const { t } = useTranslation();
@@ -83,7 +88,7 @@ function BillEnterModalContainer({
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
});
console.log("adjustmentsToInsert", adjustmentsToInsert);
const adjKeys = Object.keys(adjustmentsToInsert);
if (adjKeys.length > 0) {
//Query the adjustments, merge, and update them.
@@ -116,7 +121,12 @@ function BillEnterModalContainer({
message: JSON.stringify(jobUpdate.errors),
}),
});
return;
}
insertAuditTrail({
jobid: values.jobid,
operation: AuditTrailMapping.jobmodifylbradj(),
});
}
if (!!r1.errors) {
@@ -172,6 +182,12 @@ function BillEnterModalContainer({
});
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
insertAuditTrail({
jobid: values.jobid,
billid: billId,
operation: AuditTrailMapping.billposted(remainingValues.invoice_number),
});
if (enterAgain) {
form.resetFields();
form.setFieldsValue({ billlines: [] });

View File

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

View File

@@ -1,13 +1,8 @@
import Dinero from "dinero.js";
export const CalculateBillTotal = (invoice) => {
const {
total,
billlines,
federal_tax_rate,
local_tax_rate,
state_tax_rate,
} = invoice;
const { total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate } =
invoice;
//TODO Determine why this recalculates so many times.
let subtotal = Dinero({ amount: 0 });
@@ -20,8 +15,7 @@ export const CalculateBillTotal = (invoice) => {
billlines.forEach((i) => {
if (!!i) {
const itemTotal = Dinero({
amount:
Math.round(((i.actual_cost || 0) * 100 + Number.EPSILON) * 100) / 100,
amount: Math.round((i.actual_cost || 0) * 100),
}).multiply(i.quantity || 1);
subtotal = subtotal.add(itemTotal);

View File

@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
@@ -14,6 +15,7 @@ import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
//jobRO: selectJobReadOnly,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
@@ -26,6 +28,7 @@ const mapDispatchToProps = (dispatch) => ({
});
export function BillsListTableComponent({
bodyshop,
job,
billsQuery,
handleOnRowClick,
@@ -52,7 +55,9 @@ export function BillsListTableComponent({
)}
<BillDeleteButton bill={record} />
<Button
disabled={record.is_credit_memo}
disabled={
record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid
}
onClick={() =>
setPartsOrderContext({
actions: {},

View File

@@ -34,7 +34,9 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
let dailySales;
if (!!jobsByDate[val]) {
dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => {
return dayAcc.add(Dinero(dayVal.job_totals.totals.subtotal));
return dayAcc.add(
Dinero((dayVal.job_totals && dayVal.job_totals.totals.subtotal) || 0)
);
}, Dinero());
} else {
dailySales = Dinero();

View File

@@ -13,7 +13,14 @@ export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
const dollars =
data.projected_monthly_sales &&
data.projected_monthly_sales.reduce(
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
(acc, val) =>
acc.add(
Dinero(
val.job_totals &&
val.job_totals.totals &&
val.job_totals.totals.subtotal
)
),
Dinero()
);
return (

View File

@@ -14,7 +14,8 @@ export default function DashboardTotalProductionDollars({
const dollars =
data.production_jobs &&
data.production_jobs.reduce(
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
(acc, val) =>
acc.add(Dinero(val.job_totals && val.job_totals.totals.subtotal)),
Dinero()
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,50 @@
import { Button, Popover, Space } from "antd";
import { AlertFilled } from "@ant-design/icons";
import {
Button,
Divider,
Dropdown,
Menu,
notification,
Popover,
Space,
} from "antd";
import parsePhoneNumber from "libphonenumber-js";
import moment from "moment";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import {
openChatByPhone,
setMessage,
} from "../../redux/messaging/messaging.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component";
import ScheduleAtChange from "./job-at-change.component";
import ScheduleEventColor from "./schedule-event.color.component";
import queryString from "query-string";
import ScheduleEventNote from "./schedule-event.note.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setScheduleContext: (context) =>
dispatch(setModalContext({ context: context, modal: "schedule" })),
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
setMessage: (text) => dispatch(setMessage(text)),
});
export function ScheduleEventComponent({
bodyshop,
setMessage,
openChatByPhone,
event,
refetch,
handleCancel,
@@ -38,7 +64,7 @@ export function ScheduleEventComponent({
);
const popoverContent = (
<div>
<div style={{ maxWidth: "40vw" }}>
{!event.isintake ? (
<strong>{event.title}</strong>
) : (
@@ -75,17 +101,19 @@ export function ScheduleEventComponent({
{(event.job && event.job.ownr_ea) || ""}
</DataLabel>
<DataLabel label={t("jobs.fields.ownr_ph1")}>
<PhoneFormatter>
{(event.job && event.job.ownr_ph1) || ""}
</PhoneFormatter>
<ChatOpenButton
phone={event.job && event.job.ownr_ph1}
jobid={event.job.id}
/>
</DataLabel>
<DataLabel label={t("jobs.fields.alt_transport")}>
{(event.job && event.job.alt_transport) || ""}
<ScheduleAtChange job={event && event.job} />
</DataLabel>
<ScheduleEventNote event={event} />
</div>
) : null}
<Divider />
<Space wrap>
{event.job ? (
<Link to={`/manage/jobs/${event.job && event.job.id}`}>
@@ -106,23 +134,62 @@ export function ScheduleEventComponent({
{t("appointments.actions.preview")}
</Button>
) : null}
<Button
onClick={() => {
const Template = TemplateList("job").appointment_reminder;
GenerateDocument(
{
name: Template.key,
variables: { id: event.job.id },
},
{ to: event.job && event.job.ownr_ea, subject: Template.subject },
"e",
event.job && event.job.id
);
}}
disabled={event.arrived}
<Dropdown
overlay={
<Menu>
<Menu.Item
onClick={() => {
const Template = TemplateList("job").appointment_reminder;
GenerateDocument(
{
name: Template.key,
variables: { id: event.job.id },
},
{
to: event.job && event.job.ownr_ea,
subject: Template.subject,
},
"e",
event.job && event.job.id
);
}}
disabled={event.arrived}
>
{t("general.labels.email")}
</Menu.Item>
<Menu.Item
onClick={() => {
const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
if (p && p.isValid()) {
openChatByPhone({
phone_num: p.formatInternational(),
jobid: event.job.id,
});
setMessage(
t("appointments.labels.reminder", {
shopname: bodyshop.shopname,
date: moment(event.start).format("MM/DD/YYYY"),
time: moment(event.start).format("HH:MM a"),
})
);
setVisible(false);
} else {
notification["error"]({
message: t("messaging.error.invalidphone"),
});
}
}}
disabled={event.arrived || !bodyshop.messagingservicesid}
>
{t("general.labels.sms")}
</Menu.Item>
</Menu>
}
>
{t("appointments.actions.sendreminder")}
</Button>
<Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown>
<Button onClick={() => handleCancel(event.id)} disabled={event.arrived}>
{t("appointments.actions.cancel")}
</Button>
@@ -161,6 +228,7 @@ export function ScheduleEventComponent({
const RegularEvent = event.isintake ? (
<div style={{ display: "flex", flexWrap: "wrap" }}>
<Space>
{event.note && <AlertFilled className="production-alert" />}
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
<span>{`${(event.job && event.job.ownr_fn) || ""} ${
(event.job && event.job.ownr_ln) || ""
@@ -202,4 +270,7 @@ export function ScheduleEventComponent({
</Popover>
);
}
export default connect(null, mapDispatchToProps)(ScheduleEventComponent);
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScheduleEventComponent);

View File

@@ -0,0 +1,74 @@
import { EditFilled, SaveFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Input, notification, Space } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DataLabel from "../data-label/data-label.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function ScheduleEventNote({ event }) {
const [editing, setEditing] = useState(false);
const [note, setNote] = useState(event.note || "");
const [loading, setLoading] = useState(false);
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
const { t } = useTranslation();
const toggleEdit = async () => {
if (editing) {
//Await the update
setLoading(true);
const result = await updateAppointment({
variables: {
appid: event.id,
app: { note },
},
});
if (!!!result.errors) {
// notification["success"]({ message: t("appointments.successes.saved") });
} else {
notification["error"]({
message: t("jobs.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setEditing(false);
} else {
setEditing(true);
}
setLoading(false);
};
return (
<DataLabel label={t("appointments.fields.note")}>
<Space flex>
{!editing ? (
event.note || ""
) : (
<Input.TextArea
rows={3}
value={note}
onChange={(e) => setNote(e.target.value)}
style={{ maxWidth: "8vw" }}
/>
)}
<Button onClick={toggleEdit} loading={loading}>
{editing ? <SaveFilled /> : <EditFilled />}
</Button>
</Space>
</DataLabel>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventNote);

View File

@@ -0,0 +1,46 @@
import { useQuery } from "@apollo/client";
import { Card, Table } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
import { DateTimeFormatter } from "../../utils/DateFormatter";
export default function JobAuditTrail({ jobId }) {
const { t } = useTranslation();
const { loading, data } = useQuery(QUERY_AUDIT_TRAIL, {
variables: { jobid: jobId },
skip: !jobId,
});
const columns = [
{
title: t("audit.fields.created"),
dataIndex: "created",
key: "created",
render: (text, record) => (
<DateTimeFormatter>{record.created}</DateTimeFormatter>
),
},
{
title: t("audit.fields.useremail"),
dataIndex: "useremail",
key: "useremail",
},
{
title: t("audit.fields.operation"),
dataIndex: "operation",
key: "operation",
},
];
return (
<Card title={t("jobs.labels.audit")}>
<Table
loading={loading}
columns={columns}
rowKey="id"
dataSource={data ? data.audit_trail : []}
/>
</Card>
);
}

View File

@@ -16,16 +16,21 @@ import {
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
import moment from "moment-business-days";
import { insertAuditTrail } from "../../../../redux/application/application.actions";
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export function JobChecklistForm({
insertAuditTrail,
formItems,
bodyshop,
currentUser,
@@ -37,6 +42,8 @@ export function JobChecklistForm({
const [intakeJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false);
const [markAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_AS_ARRIVED);
const [updateOwner] = useMutation(UPDATE_OWNER);
const { jobId } = useParams();
const history = useHistory();
const search = queryString.parse(useLocation().search);
@@ -59,6 +66,12 @@ export function JobChecklistForm({
production_vars: {
...job.production_vars,
...values.production_vars,
note:
values.production_vars &&
values.production_vars.note &&
values.production_vars.note !== ""
? job.production_vars && values.production_vars.note
: job.production_vars && job.production_vars.note,
},
}),
...(type === "intake" && {
@@ -104,11 +117,40 @@ export function JobChecklistForm({
});
}
}
if (type === "intake" && job.owner && job.owner.id) {
//Updae Owner Allow to Text
const updateOwnerResult = await updateOwner({
variables: {
ownerId: job.owner.id,
owner: { allow_text_message: values.allow_text_message },
},
});
if (!!updateOwnerResult.errors) {
notification["error"]({
message: t("checklist.errors.complete", {
error: JSON.stringify(result.errors),
}),
});
}
}
setLoading(false);
if (!!!result.errors) {
notification["success"]({ message: t("checklist.successes.completed") });
history.push(`/manage/jobs/${jobId}`);
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobchecklist(
type,
(type === "deliver" && values.removeFromProduction && false) ||
(type === "intake" && values.addToProduction),
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered)
),
});
} else {
notification["error"]({
message: t("checklist.errors.complete", {
@@ -135,6 +177,7 @@ export function JobChecklistForm({
initialValues={{
...(type === "intake" && {
addToProduction: true,
allow_text_message: job.owner && job.owner.allow_text_message,
scheduled_completion:
(job && job.scheduled_completion) ||
moment().businessAdd(
@@ -170,6 +213,14 @@ export function JobChecklistForm({
>
<Switch disabled={readOnly} />
</Form.Item>
<Form.Item
name="allow_text_message"
valuePropName="checked"
label={t("checklist.labels.allow_text_message")}
disabled={readOnly}
>
<Switch disabled={readOnly} />
</Form.Item>
<Form.Item
name="scheduled_completion"
label={t("jobs.fields.scheduled_completion")}

View File

@@ -37,8 +37,8 @@ export function JobEmployeeAssignments({
});
const [visibility, setVisibility] = useState(false);
const onChange = (e) => {
setAssignment({ ...assignment, employeeid: e });
const onChange = (value, option) => {
setAssignment({ ...assignment, employeeid: value, name: option.name });
};
const popContent = (
@@ -56,7 +56,11 @@ export function JobEmployeeAssignments({
}
>
{bodyshop.employees.map((emp) => (
<Select.Option value={emp.id} key={emp.id}>
<Select.Option
value={emp.id}
key={emp.id}
name={`${emp.first_name} ${emp.last_name}`}
>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}

View File

@@ -6,14 +6,34 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB_ASSIGNMENTS } from "../../graphql/jobs.queries";
import JobEmployeeAssignmentsComponent from "./job-employee-assignments.component";
export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobEmployeeAssignmentsContainer);
export function JobEmployeeAssignmentsContainer({
job,
refetch,
insertAuditTrail,
}) {
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOB_ASSIGNMENTS);
const [loading, setLoading] = useState(false);
const handleAdd = async (assignment) => {
setLoading(true);
const { operation, employeeid } = assignment;
const { operation, employeeid, name } = assignment;
logImEXEvent("job_assign_employee", { operation });
let empAssignment = determineFieldName(operation);
@@ -23,6 +43,11 @@ export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
});
if (refetch) refetch();
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentchange(operation, name),
});
if (!!result.errors) {
notification["error"]({
message: t("jobs.errors.assigning", {
@@ -48,6 +73,10 @@ export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
}),
});
}
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentremoved(operation),
});
setLoading(false);
};

View File

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

View File

@@ -15,6 +15,7 @@ const JobSearchSelect = (
{
disabled,
convertedOnly = false,
notInvoiced = false,
notExported = true,
clm_no = false,
...restProps
@@ -30,6 +31,7 @@ const JobSearchSelect = (
variables: {
...(convertedOnly ? { isConverted: true } : {}),
...(notExported ? { notExported: true } : {}),
...(notInvoiced ? { notInvoiced: true } : {}),
},
}
: {}),

View File

@@ -6,8 +6,8 @@ import { useTranslation } from "react-i18next";
export default function JobAdminDeleteIntake({ job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [updateJob] = useMutation(gql`
mutation UPDATE_JOB($jobId: uuid!) {
const [deleteIntake] = useMutation(gql`
mutation DELETE_INTAKE($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { intakechecklist: null }
@@ -18,9 +18,39 @@ export default function JobAdminDeleteIntake({ job }) {
}
`);
const [DELETE_DELIVERY] = useMutation(gql`
mutation DELETE_DELIVERY($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { deliverchecklist: null }
) {
id
deliverchecklist
}
}
`);
const handleDelete = async (values) => {
setLoading(true);
const result = await updateJob({
const result = await deleteIntake({
variables: { jobId: job.id },
});
if (!!!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
} else {
notification["error"]({
message: t("jobs.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
};
const handleDeleteDelivery = async (values) => {
setLoading(true);
const result = await DELETE_DELIVERY({
variables: { jobId: job.id },
});
@@ -34,12 +64,16 @@ export default function JobAdminDeleteIntake({ job }) {
});
}
setLoading(false);
//Get the owner details, populate it all back into the job.
};
return (
<Button loading={loading} onClick={handleDelete}>
{t("jobs.labels.deleteintake")}
</Button>
<>
<Button loading={loading} onClick={handleDelete}>
{t("jobs.labels.deleteintake")}
</Button>
<Button loading={loading} onClick={handleDeleteDelivery}>
{t("jobs.labels.deletedelivery")}
</Button>
</>
);
}

View File

@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import moment from "moment";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
@@ -21,8 +22,8 @@ export default connect(
export function JobAdminMarkReexport({ bodyshop, job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [updateJob] = useMutation(gql`
mutation UPDATE_JOB($jobId: uuid!) {
const [markJobForReexport] = useMutation(gql`
mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: null
@@ -30,14 +31,84 @@ export function JobAdminMarkReexport({ bodyshop, job }) {
}
) {
id
intakechecklist
date_exported
status
date_invoiced
}
}
`);
const handleUpdate = async (values) => {
const [markJobExported] = useMutation(gql`
mutation MARK_JOB_AS_EXPORTED($jobId: uuid!, $date_exported: timestamptz!) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: $date_exported
status: "${bodyshop.md_ro_statuses.default_exported}"
}
) {
id
date_exported
date_invoiced
status
}
}
`);
const [markJobUninvoiced] = useMutation(gql`
mutation MARK_JOB_AS_UNINVOICED($jobId: uuid!, ) {
update_jobs_by_pk(
pk_columns: { id: $jobId }
_set: { date_exported: null
date_invoiced: null
status: "${bodyshop.md_ro_statuses.default_delivered}"
}
) {
id
date_exported
date_invoiced
status
}
}
`);
const handleMarkForExport = async () => {
setLoading(true);
const result = await updateJob({
const result = await markJobForReexport({
variables: { jobId: job.id },
});
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
} else {
notification["error"]({
message: t("jobs.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
};
const handleMarkExported = async () => {
setLoading(true);
const result = await markJobExported({
variables: { jobId: job.id, date_exported: moment() },
});
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
} else {
notification["error"]({
message: t("jobs.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
};
const handleUninvoice = async () => {
setLoading(true);
const result = await markJobUninvoiced({
variables: { jobId: job.id },
});
@@ -51,16 +122,31 @@ export function JobAdminMarkReexport({ bodyshop, job }) {
});
}
setLoading(false);
//Get the owner details, populate it all back into the job.
};
return (
<Button
loading={loading}
disabled={!job.date_exported}
onClick={handleUpdate}
>
{t("jobs.labels.markforreexport")}
</Button>
<>
<Button
loading={loading}
disabled={!job.date_exported}
onClick={handleMarkForExport}
>
{t("jobs.labels.markforreexport")}
</Button>
<Button
loading={loading}
disabled={job.date_exported}
onClick={handleMarkExported}
>
{t("jobs.actions.markasexported")}
</Button>
<Button
loading={loading}
disabled={!job.date_invoiced || job.date_exported}
onClick={handleUninvoice}
>
{t("jobs.actions.uninvoice")}
</Button>
</>
);
}

View File

@@ -57,7 +57,7 @@ export function JobsAvailableComponent({
title: t("jobs.fields.cieca_id"),
dataIndex: "cieca_id",
key: "cieca_id",
sorter: (a, b) => alphaSort(a, b),
sorter: (a, b) => alphaSort(a.cieca_id, b.cieca_id),
sortOrder:
state.sortedInfo.columnKey === "cieca_id" && state.sortedInfo.order,
},
@@ -68,9 +68,10 @@ export function JobsAvailableComponent({
//width: "8%",
// onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a, b),
sorter: (a, b) =>
alphaSort(a.job && a.job.ro_number, b.job && b.job.ro_number),
sortOrder:
state.sortedInfo.columnKey === "cieca_id" && state.sortedInfo.order,
state.sortedInfo.columnKey === "job_id" && state.sortedInfo.order,
render: (text, record) =>
record.job ? (
<Link to={`/manage/jobs/${record.job.id}`}>
@@ -87,7 +88,7 @@ export function JobsAvailableComponent({
dataIndex: "ownr_name",
key: "ownr_name",
ellipsis: true,
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sorter: (a, b) => alphaSort(a.ownr_name, b.ownr_name),
sortOrder:
state.sortedInfo.columnKey === "ownr_name" && state.sortedInfo.order,
},

View File

@@ -25,10 +25,12 @@ import {
import { INSERT_NEW_JOB, UPDATE_JOB } from "../../graphql/jobs.queries";
import { INSERT_NEW_NOTE } from "../../graphql/notes.queries";
import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import AlertComponent from "../alert/alert.component";
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
@@ -42,8 +44,15 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
export function JobsAvailableContainer({ bodyshop, currentUser }) {
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export function JobsAvailableContainer({
bodyshop,
currentUser,
insertAuditTrail,
}) {
const { loading, error, data, refetch } = useQuery(QUERY_AVAILABLE_JOBS, {
fetchPolicy: "network-only",
});
@@ -66,7 +75,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
const client = useApolloClient();
const estDataLazyLoad = useLazyQuery(QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK);
const [loadEstData, estData] = estDataLazyLoad;
const [loadEstData, estDataRaw] = estDataLazyLoad;
const importOptionsState = useState({ overrideHeaders: false });
const importOptions = importOptionsState[0];
@@ -79,13 +88,9 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
setOwnerModalVisible(false);
setInsertLoading(true);
if (
!(
estData.data &&
estData.data.available_jobs_by_pk &&
estData.data.available_jobs_by_pk.est_data
)
) {
const estData = replaceEmpty(estDataRaw.data.available_jobs_by_pk);
if (!(estData && estData.est_data)) {
//We don't have the right data. Error!
setInsertLoading(false);
notification["error"]({
@@ -97,28 +102,25 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
const newTotals = (
await Axios.post("/job/totals", {
job: {
...estData.data.available_jobs_by_pk.est_data,
joblines: estData.data.available_jobs_by_pk.est_data.joblines.data,
...estData.est_data,
joblines: estData.est_data.joblines.data,
},
})
).data;
let existingVehicles;
if (
estData.data.available_jobs_by_pk.est_data.vehicle &&
estData.data.available_jobs_by_pk.est_data.vin
) {
if (estData.est_data.vehicle && estData.est_data.vin) {
//There's vehicle data, need to double check the VIN.
existingVehicles = await client.query({
query: SEARCH_VEHICLE_BY_VIN,
variables: {
vin: estData.data.available_jobs_by_pk.est_data.vehicle.data.v_vin,
vin: estData.est_data.vehicle.data.v_vin,
},
});
}
const newJob = {
...estData.data.available_jobs_by_pk.est_data,
...estData.est_data,
clm_total: Dinero(newTotals.totals.total_repairs).toFormat("0.00"),
owner_owing: Dinero(newTotals.totals.custPayable.total).toFormat("0.00"),
job_totals: newTotals,
@@ -157,8 +159,13 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
});
//Job has been inserted. Clean up the available jobs record.
insertAuditTrail({
jobid: r.data.insert_jobs.returning[0].id,
operation: AuditTrailMapping.jobimported(),
});
deleteJob({
variables: { id: estData.data.available_jobs_by_pk.id },
variables: { id: estData.id },
}).then((r) => {
refetch();
setInsertLoading(false);
@@ -180,14 +187,8 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
setJobModalVisible(false);
setInsertLoading(true);
if (
!(
estData.data &&
estData.data.available_jobs_by_pk &&
estData.data.available_jobs_by_pk.est_data
)
) {
const estData = replaceEmpty(estDataRaw.data.available_jobs_by_pk);
if (!(estData && estData.est_data)) {
//We don't have the right data. Error!
setInsertLoading(false);
notification["error"]({
@@ -195,18 +196,19 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
});
} else {
//create upsert job
let supp = _.cloneDeep(estData.data.available_jobs_by_pk.est_data);
let supp = _.cloneDeep(estData.est_data);
delete supp.owner;
delete supp.vehicle;
if (importOptions.overrideHeaders) {
delete supp.ins_co_nm;
if (!importOptions.overrideHeaders) {
HeaderFields.forEach((item) => delete supp[item]);
}
let suppDelta = await GetSupplementDelta(
client,
selectedJob,
estData.data.available_jobs_by_pk.est_data.joblines.data
estData.est_data.joblines.data
);
delete supp.joblines;
@@ -265,7 +267,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
//Job has been inserted. Clean up the available jobs record.
deleteJob({
variables: { id: estData.data.available_jobs_by_pk.id },
variables: { id: estData.id },
}).then((r) => {
refetch();
setInsertLoading(false);
@@ -283,17 +285,21 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
],
},
});
insertAuditTrail({
jobid: selectedJob,
operation: AuditTrailMapping.jobsupplement(),
});
}
};
const owner =
estData.data &&
estData.data.available_jobs_by_pk &&
estData.data.available_jobs_by_pk.est_data &&
estData.data.available_jobs_by_pk.est_data.owner &&
estData.data.available_jobs_by_pk.est_data.owner.data &&
!estData.data.available_jobs_by_pk.issupplement
? estData.data.available_jobs_by_pk.est_data.owner.data
estDataRaw.data &&
estDataRaw.data.available_jobs_by_pk &&
estDataRaw.data.available_jobs_by_pk.est_data &&
estDataRaw.data.available_jobs_by_pk.est_data.owner &&
estDataRaw.data.available_jobs_by_pk.est_data.owner.data &&
!estDataRaw.data.available_jobs_by_pk.issupplement
? estDataRaw.data.available_jobs_by_pk.est_data.owner.data
: null;
const onOwnerModalCancel = () => {
@@ -331,8 +337,8 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
message={t("jobs.labels.creating_new_job")}
>
<OwnerFindModalContainer
loading={estData.loading}
error={estData.error}
loading={estDataRaw.loading}
error={estDataRaw.error}
owner={owner}
selectedOwner={selectedOwner}
setSelectedOwner={setSelectedOwner}
@@ -341,8 +347,8 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
onCancel={onOwnerModalCancel}
/>
<JobsFindModalContainer
loading={estData.loading}
error={estData.error}
loading={estDataRaw.loading}
error={estDataRaw.error}
selectedJob={selectedJob}
setSelectedJob={setSelectedJob}
importOptionsState={importOptionsState}
@@ -368,4 +374,16 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
</LoadingSpinner>
);
}
export default connect(mapStateToProps, null)(JobsAvailableContainer);
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsAvailableContainer);
function replaceEmpty(someObj, replaceValue = null) {
const replacer = (key, value) => (value === "" ? replaceValue : value);
//^ because you seem to want to replace (strings) "null" or "undefined" too
console.log(someObj);
const temp = JSON.stringify(someObj, replacer);
console.log(`temp`, temp);
return JSON.parse(temp);
}

View File

@@ -6,18 +6,21 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export function JobsChangeStatus({ job, bodyshop, jobRO }) {
export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
const { t } = useTranslation();
const [availableStatuses, setAvailableStatuses] = useState([]);
@@ -29,6 +32,10 @@ export function JobsChangeStatus({ job, bodyshop, jobRO }) {
})
.then((r) => {
notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobstatuschange(status),
});
// refetch();
})
.catch((error) => {

View File

@@ -1,4 +1,4 @@
import { Button } from "antd";
import { Button, Dropdown, Menu } from "antd";
import _ from "lodash";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -12,11 +12,8 @@ const mapStateToProps = createStructuredSelector({
export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
const { t } = useTranslation();
const handleAllocate = () => {
logImEXEvent("jobs_close_allocate_auto");
const { defaults } = bodyshop.md_responsibility_centers;
const handleAllocate = (defaults) => {
form.setFieldsValue({
joblines: joblines.map((jl) => {
const ret = _.cloneDeep(jl);
@@ -32,7 +29,7 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
}
//Verify that this is also manually updated in server/job-costing
if (!jl.part_type && !jl.mod_lbr_ty) {
const lineDesc = jl.line_desc.toLowerCase();
const lineDesc = jl.line_desc ? jl.line_desc.toLowerCase() : "";
if (lineDesc.includes("shop materials")) {
ret.profitcenter_part = defaults.profits["MASH"];
} else if (lineDesc.includes("paint/materials")) {
@@ -48,8 +45,36 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
});
};
return (
<Button onClick={handleAllocate} disabled={disabled}>
const handleAutoAllocateClick = () => {
logImEXEvent("jobs_close_allocate_auto");
const { defaults } = bodyshop.md_responsibility_centers;
handleAllocate(defaults);
};
const handleMenuClick = ({ item, key, keyPath, domEvent }) => {
logImEXEvent("jobs_close_allocate_auto_dms");
handleAllocate(
bodyshop.md_responsibility_centers.dms_defaults.find(
(x) => x.name === key
)
);
};
const overlay = bodyshop.cdk_dealerid && (
<Menu onClick={handleMenuClick}>
{bodyshop.md_responsibility_centers.dms_defaults.map((mapping) => (
<Menu.Item key={mapping.name}>{mapping.name}</Menu.Item>
))}
</Menu>
);
return bodyshop.cdk_dealerid ? (
<Dropdown overlay={overlay}>
<Button disabled={disabled}>{t("jobs.actions.dmsautoallocate")}</Button>
</Dropdown>
) : (
<Button onClick={handleAutoAllocateClick} disabled={disabled}>
{t("jobs.actions.autoallocate")}
</Button>
);

View File

@@ -24,6 +24,7 @@ export function JobsCloseExportButton({
currentUser,
jobId,
disabled,
setSelectedJobs,
}) {
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOB);
@@ -147,6 +148,11 @@ export function JobsCloseExportButton({
}),
});
}
if (setSelectedJobs) {
setSelectedJobs((selectedJobs) => {
return selectedJobs.filter((i) => i !== jobId);
});
}
}
setLoading(false);

View File

@@ -13,8 +13,10 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -22,10 +24,17 @@ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
export function JobsConvertButton({
bodyshop,
job,
refetch,
jobRO,
insertAuditTrail,
}) {
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO);
@@ -43,6 +52,14 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
notification["success"]({
message: t("jobs.successes.converted"),
});
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobconverted(
res.data.update_jobs.returning[0].ro_number
),
});
setVisible(false);
}
setLoading(false);

View File

@@ -1,7 +1,10 @@
import { notification } from "antd";
import i18n from "i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { store } from "../../redux/store";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
export default function AddToProduction(
apolloClient,
@@ -21,6 +24,13 @@ export default function AddToProduction(
notification["success"]({
message: i18n.t("jobs.successes.save"),
});
store.dispatch(
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobinproductionchange(!remove),
})
);
if (completionCallback) completionCallback();
})
.catch((error) => {

View File

@@ -142,7 +142,10 @@ export function JobsDetailHeaderActions({
</Menu.Item>
<Menu.Item
key="entertimetickets"
disabled={!job.converted}
disabled={
!job.converted ||
(!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced)
}
onClick={() => {
logImEXEvent("job_header_enter_time_ticekts");

View File

@@ -10,7 +10,7 @@ import { alphaSort } from "../../utils/sorters";
import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component";
import "./labor-allocations-table.styles.scss";
import { CalculateAllocationsTotals } from "./labor-allocations-table.utility";
import _ from "lodash";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
@@ -113,7 +113,7 @@ export function LaborAllocationsTable({
color: record.difference >= 0 ? "green" : "red",
}}
>
{record.difference}
{_.round(record.difference, 1)}
</strong>
),
},

View File

@@ -9,6 +9,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries";
import { INSERT_NEW_PARTS_ORDERS } from "../../graphql/parts-orders.queries";
import { QUERY_ALL_VENDORS_FOR_ORDER } from "../../graphql/vendors.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setEmailOptions } from "../../redux/email/email.actions";
import {
setModalContext,
@@ -19,6 +20,7 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import AlertComponent from "../alert/alert.component";
@@ -36,6 +38,8 @@ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("partsOrder")),
setBillEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "billEnter" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export function PartsOrderModalContainer({
@@ -45,6 +49,7 @@ export function PartsOrderModalContainer({
bodyshop,
setEmailOptions,
setBillEnterContext,
insertAuditTrail,
}) {
const { t } = useTranslation();
@@ -109,6 +114,18 @@ export function PartsOrderModalContainer({
: bodyshop.md_order_statuses.default_ordered || "Ordered*",
},
});
insertAuditTrail({
jobid: jobId,
operation: isReturn
? AuditTrailMapping.jobspartsreturn(
insertResult.data.insert_parts_orders.returning[0].order_number
)
: AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number
),
});
if (!!jobLinesResult.errors) {
notification["error"]({
message: t("parts_orders.errors.creating"),

View File

@@ -25,6 +25,7 @@ export function PayableExportButton({
billId,
disabled,
loadingCallback,
setSelectedBills,
}) {
const { t } = useTranslation();
const [updateBill] = useMutation(UPDATE_BILLS);
@@ -142,6 +143,11 @@ export function PayableExportButton({
}),
});
}
if (setSelectedBills) {
setSelectedBills((selectedBills) => {
return selectedBills.filter((i) => i !== billId);
});
}
}
if (!!loadingCallback) loadingCallback(false);

View File

@@ -24,6 +24,7 @@ export function PaymentExportButton({
paymentId,
disabled,
loadingCallback,
setSelectedPayments,
}) {
const { t } = useTranslation();
const [updatePayment] = useMutation(UPDATE_PAYMENTS);
@@ -141,6 +142,12 @@ export function PaymentExportButton({
}),
});
}
if (setSelectedPayments) {
setSelectedPayments((selectedBills) => {
return selectedBills.filter((i) => i !== paymentId);
});
}
}
if (!!loadingCallback) loadingCallback(false);

View File

@@ -14,12 +14,25 @@ import IndefiniteLoading from "../indefinite-loading/indefinite-loading.componen
import { logImEXEvent } from "../../firebase/firebase.utils";
import ProductionBoardFilters from "../production-board-filters/production-board-filters.component";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
});
export function ProductionBoardKanbanComponent({ data, bodyshop, technician }) {
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export function ProductionBoardKanbanComponent({
data,
bodyshop,
technician,
insertAuditTrail,
}) {
const [boardLanes, setBoardLanes] = useState({
columns: [{ id: "Loading...", title: "Loading...", cards: [] }],
});
@@ -104,6 +117,11 @@ export function ProductionBoardKanbanComponent({ data, bodyshop, technician }) {
newChildCardNewParent
),
});
insertAuditTrail({
jobid: card.id,
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
});
if (update.errors) {
notification["error"]({
message: t("production.errors.boardupdate", {
@@ -130,4 +148,7 @@ export function ProductionBoardKanbanComponent({ data, bodyshop, technician }) {
</div>
);
}
export default connect(mapStateToProps, null)(ProductionBoardKanbanComponent);
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProductionBoardKanbanComponent);

View File

@@ -16,24 +16,32 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const iconStyle = { marginLeft: ".3rem" };
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export function ProductionListEmpAssignment({ bodyshop, record, type }) {
export function ProductionListEmpAssignment({
insertAuditTrail,
bodyshop,
record,
type,
}) {
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false);
const handleAdd = async (assignment) => {
setLoading(true);
const { operation, employeeid } = assignment;
const { operation, employeeid, name } = assignment;
logImEXEvent("job_assign_employee", { operation });
let empAssignment = determineFieldName(operation);
@@ -44,6 +52,11 @@ export function ProductionListEmpAssignment({ bodyshop, record, type }) {
awaitRefetchQueries: true,
});
insertAuditTrail({
jobid: record.id,
operation: AuditTrailMapping.jobassignmentchange(empAssignment, name),
});
if (!!result.errors) {
notification["error"]({
message: t("jobs.errors.assigning", {
@@ -64,6 +77,11 @@ export function ProductionListEmpAssignment({ bodyshop, record, type }) {
awaitRefetchQueries: true,
});
insertAuditTrail({
jobid: record.id,
operation: AuditTrailMapping.jobassignmentremoved(empAssignment),
});
if (!!result.errors) {
notification["error"]({
message: t("jobs.errors.assigning", {
@@ -80,8 +98,8 @@ export function ProductionListEmpAssignment({ bodyshop, record, type }) {
});
const [visibility, setVisibility] = useState(false);
const onChange = (e) => {
setAssignment({ ...assignment, employeeid: e });
const onChange = (e, option) => {
setAssignment({ ...assignment, employeeid: e, name: option.name });
};
const popContent = (
@@ -99,7 +117,11 @@ export function ProductionListEmpAssignment({ bodyshop, record, type }) {
}
>
{bodyshop.employees.map((emp) => (
<Select.Option value={emp.id} key={emp.id}>
<Select.Option
value={emp.id}
key={emp.id}
name={`${emp.first_name} ${emp.last_name}`}
>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}

View File

@@ -6,12 +6,21 @@ import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function ProductionListColumnStatus({ record, bodyshop }) {
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export function ProductionListColumnStatus({
record,
bodyshop,
insertAuditTrail,
}) {
const [updateJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false);
@@ -28,6 +37,11 @@ export function ProductionListColumnStatus({ record, bodyshop }) {
},
},
});
insertAuditTrail({
jobid: record.id,
operation: AuditTrailMapping.jobstatuschange(key),
});
setLoading(false);
};
@@ -52,4 +66,7 @@ export function ProductionListColumnStatus({ record, bodyshop }) {
</Dropdown>
);
}
export default connect(mapStateToProps, null)(ProductionListColumnStatus);
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProductionListColumnStatus);

View File

@@ -25,6 +25,7 @@ export function ProductionListTable({
currentUser,
state,
setColumns,
setState,
}) {
const { t } = useTranslation();
const [updateDefaultProdView] = useMutation(UPDATE_ACTIVE_PROD_LIST_VIEW);
@@ -43,6 +44,10 @@ export function ProductionListTable({
};
})
);
setState(
bodyshop.production_config.filter((pc) => pc.name === value)[0].columns
.tableState
);
const assoc = bodyshop.associations.find(
(a) => a.useremail === currentUser.email
@@ -77,6 +82,8 @@ export function ProductionListTable({
};
})
);
setState(bodyshop.production_config[0].columns.tableState);
};
return (

View File

@@ -41,9 +41,9 @@ export function ProductionListTable({
const [state, setState] = useState(
(bodyshop.production_config &&
bodyshop.production_config.find((p) => p.name === defaultView)
?.tableState) ||
bodyshop.production_config[0]?.tableState || {
bodyshop.production_config.find((p) => p.name === defaultView)?.columns
.tableState) ||
bodyshop.production_config[0]?.columns.tableState || {
sortedInfo: {},
filteredInfo: { text: "" },
}
@@ -173,6 +173,7 @@ export function ProductionListTable({
<ProductionListTableViewSelect
state={state}
setState={setState}
setColumns={setColumns}
/>

View File

@@ -1,11 +1,16 @@
import { Button, Form, Input } from "antd";
import { Button, Form, Input, notification } from "antd";
import { LockOutlined } from "@ant-design/icons";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { updateUserDetails } from "../../redux/user/user.actions";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {
logImEXEvent,
updateCurrentPassword,
} from "../../firebase/firebase.utils";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -28,33 +33,96 @@ export default connect(
});
};
const handleChangePassword = async ({ password }) => {
logImEXEvent("password_update");
try {
await updateCurrentPassword(password);
notification.success({
message: t("user.successess.passwordchanged"),
});
} catch (error) {
notification.error({
message: error.message,
});
}
};
return (
<div>
<Form
onFinish={handleFinish}
autoComplete={"no"}
initialValues={currentUser}
layout="vertical"
>
<Form.Item
label={t("user.fields.displayname")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name="displayName"
>
<Input />
</Form.Item>
<Form.Item label={t("user.fields.photourl")} name="photoURL">
<Input />
</Form.Item>
<LayoutFormRow>
<Form.Item
label={t("user.fields.displayname")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name="displayName"
>
<Input />
</Form.Item>
<Form.Item label={t("user.fields.photourl")} name="photoURL">
<Input />
</Form.Item>
</LayoutFormRow>
<Button type="primary" key="submit" htmlType="submit">
{t("user.actions.updateprofile")}
</Button>
</Form>
<Form
onFinish={handleChangePassword}
autoComplete={"no"}
initialValues={currentUser}
layout="vertical"
>
<LayoutFormRow>
<Form.Item label={t("general.labels.newpassword")} name="password">
<Input
prefix={<LockOutlined />}
type="password"
placeholder={t("general.labels.password")}
/>
</Form.Item>
<Form.Item
label={t("general.labels.confirmpassword")}
name="password-confirm"
dependencies={["password"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
validator(rule, value) {
if (!value || getFieldValue("password") === value) {
return Promise.resolve();
}
return Promise.reject(
t("general.labels.passwordsdonotmatch")
);
},
}),
]}
>
<Input
prefix={<LockOutlined />}
type="password"
placeholder={t("general.labels.password")}
/>
</Form.Item>
</LayoutFormRow>
<Button type="primary" key="submit" htmlType="submit">
{t("user.actions.changepassword")}
</Button>
</Form>
</div>
);
});

View File

@@ -61,8 +61,12 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
{
name: values.key,
variables: {
...(start ? { start: moment(start).format("YYYY-MM-DD") } : {}),
...(end ? { end: moment(end).format("YYYY-MM-DD") } : {}),
...(start
? { start: moment(start).startOf("day").format("YYYY-MM-DD") }
: {}),
...(end
? { end: moment(end).endOf("day").format("YYYY-MM-DD") }
: {}),
...(id ? { id: id } : {}),
},
},

View File

@@ -1,4 +1,14 @@
import { Button, Card, Col, Form, Row, Select, Switch } from "antd";
import {
Button,
Col,
Form,
Input,
Row,
Select,
Space,
Switch,
Typography,
} from "antd";
import axios from "axios";
import moment from "moment";
import React, { useState } from "react";
@@ -91,31 +101,34 @@ export function ScheduleJobModalComponent({
<DateTimePicker onlyFuture />
</Form.Item>
</LayoutFormRow>
<Card title={t("appointments.labels.smartscheduling")}>
<Typography.Title level={4}>
{t("appointments.labels.smartscheduling")}
</Typography.Title>
{
// smartOptions.length > 0 && (
// <div>{t("appointments.labels.suggesteddates")}</div>
// )
}
<Space wrap>
<Button onClick={handleSmartScheduling} loading={loading}>
{t("appointments.actions.calculate")}
</Button>
{smartOptions.length > 0 && (
<div>{t("appointments.labels.suggesteddates")}</div>
)}
<div
className="imex-flex-row imex-flex-row__flex-space-around"
style={{ flexWrap: "wrap" }}
>
{smartOptions.map((d, idx) => (
<Button
className="imex-flex-row__margin"
key={idx}
onClick={() => {
form.setFieldsValue({ start: new moment(d).add(8, "hours") });
handleDateBlur();
}}
>
<DateFormatter>{d}</DateFormatter>
</Button>
))}
</div>
</Card>
{smartOptions.map((d, idx) => (
<Button
className="imex-flex-row__margin"
key={idx}
onClick={() => {
form.setFieldsValue({ start: new moment(d).add(8, "hours") });
handleDateBlur();
}}
>
<DateFormatter>{d}</DateFormatter>
</Button>
))}
</Space>
<LayoutFormRow grow>
<Form.Item
name="notifyCustomer"
@@ -124,12 +137,9 @@ export function ScheduleJobModalComponent({
>
<Switch />
</Form.Item>
<Form.Item shouldUpdate>
{() => (
<Form.Item name="email" label={t("jobs.fields.ownr_ea")}>
<EmailInput disabled={!form.getFieldValue("notifyCustomer")} />
</Form.Item>
)}
<Form.Item name="email" label={t("jobs.fields.ownr_ea")}>
<EmailInput disabled={!form.getFieldValue("notifyCustomer")} />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow grow>
@@ -158,6 +168,9 @@ export function ScheduleJobModalComponent({
))}
</Select>
</Form.Item>
<Form.Item name={"note"} label={t("appointments.fields.note")}>
<Input />
</Form.Item>
</LayoutFormRow>
{t("appointments.labels.history")}
<ScheduleExistingAppointmentsList

View File

@@ -105,6 +105,7 @@ export function ScheduleJobModalContainer({
start: moment(values.start),
end: moment(values.start).add(bodyshop.appt_length || 60, "minutes"),
color: values.color,
note:values.note
},
jobId: jobId,
altTransport: values.alt_transport,

View File

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

View File

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

View File

@@ -444,6 +444,13 @@ export default function ShopInfoGeneral({ form }) {
>
<Switch />
</Form.Item>
<Form.Item
name={["tt_allow_post_to_invoiced"]}
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
valuePropName="checked"
>
<Switch />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.labels.messagingpresets")}>
<Form.List name={["md_messaging_presets"]}>

View File

@@ -30,7 +30,10 @@ export function TechClockInComponent({ form, bodyshop, technician }) {
},
]}
>
<JobSearchSelect />
<JobSearchSelect
convertedOnly={!bodyshop.tt_allow_post_to_invoiced}
notExported={!bodyshop.tt_allow_post_to_invoiced}
/>
</Form.Item>
<Form.Item

View File

@@ -1,9 +1,10 @@
import { Card, Space, Table } from "antd";
import { EditFilled } from "@ant-design/icons";
import { Card, Space, Table } from "antd";
import moment from "moment";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import {
selectAuthLevel,
@@ -16,8 +17,6 @@ import RbacWrapper, {
HasRbacAccess,
} from "../rbac-wrapper/rbac-wrapper.component";
import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
import { Link } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
authLevel: selectAuthLevel,
@@ -105,7 +104,8 @@ export function TimeTicketList({
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sorter: (a, b) =>
alphaSort(a.job && a.job.ro_number, b.job && b.job.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) =>
@@ -267,8 +267,12 @@ export function TimeTicketList({
<Table.Summary.Cell />
<Table.Summary.Cell />
<Table.Summary.Cell />
<Table.Summary.Cell>{totals.productivehrs}</Table.Summary.Cell>
<Table.Summary.Cell>{totals.actualhrs}</Table.Summary.Cell>
<Table.Summary.Cell>
{totals.productivehrs.toFixed(1)}
</Table.Summary.Cell>
<Table.Summary.Cell>
{totals.actualhrs.toFixed(1)}
</Table.Summary.Cell>
<Table.Summary.Cell>
{totals.actualhrs === 0 || !totals.actualhrs
? "∞"

View File

@@ -48,7 +48,9 @@ export function TimeTicketModalComponent({
{emps &&
emps.rates.map((item) => (
<Select.Option key={item.cost_center}>
{item.cost_center}
{item.cost_center === "timetickets.labels.shift"
? t(item.cost_center)
: item.cost_center}
</Select.Option>
))}
</Select>
@@ -82,7 +84,10 @@ export function TimeTicketModalComponent({
},
]}
>
<JobSearchSelect convertedOnly notExported={false} />
<JobSearchSelect
convertedOnly={!bodyshop.tt_allow_post_to_invoiced}
notExported={!bodyshop.tt_allow_post_to_invoiced}
/>
</Form.Item>
)}
</Form.Item>

View File

@@ -35,6 +35,24 @@ export const updateCurrentUser = (userDetails) => {
});
};
export const updateCurrentPassword = async (password) => {
const currentUser = await getCurrentUser();
return currentUser.updatePassword(password);
// return new Promise((resolve, reject) => {
// const unsubscribe = auth.onAuthStateChanged(
// (userAuth) => {
// userAuth.updatePassword(password).then((r) => {
// unsubscribe();
// resolve(userAuth);
// });
// },
// (error) => reject(error)
// );
// });
};
let messaging;
try {
messaging = firebase.messaging();

View File

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

View File

@@ -20,6 +20,7 @@ export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
isintake
block
color
note
job {
alt_transport
ro_number
@@ -69,6 +70,7 @@ export const INSERT_APPOINTMENT_BLOCK = gql`
title
isintake
block
note
}
}
}
@@ -90,6 +92,7 @@ export const INSERT_APPOINTMENT = gql`
isintake
block
color
note
}
}
update_jobs(
@@ -116,6 +119,7 @@ export const QUERY_APPOINTMENT_BY_DATE = gql`
isintake
block
color
note
job {
alt_transport
ro_number
@@ -168,6 +172,7 @@ export const UPDATE_APPOINTMENT = gql`
isintake
block
color
note
}
}
}
@@ -198,6 +203,7 @@ export const QUERY_APPOINTMENTS_BY_JOBID = gql`
canceled
created_at
block
note
}
}
`;

View File

@@ -1,18 +1,31 @@
import { gql } from "@apollo/client";
export const QUERY_AUDIT_TRAIL = gql`
query QUERY_AUDIT_TRAIL($id: uuid!) {
audit_trail(where: { recordid: { _eq: $id } }) {
query QUERY_AUDIT_TRAIL($jobid: uuid!) {
audit_trail(
where: { jobid: { _eq: $jobid } }
order_by: { created: desc }
) {
useremail
tabname
schemaname
recordid
jobid
operation
old_val
new_val
id
created
bodyshopid
}
}
`;
export const INSERT_AUDIT_TRAIL = gql`
mutation INSERT_AUDIT_TRAIL($auditObj: audit_trail_insert_input!) {
insert_audit_trail_one(object: $auditObj) {
id
jobid
billid
bodyshopid
created
operation
useremail
}
}
`;

View File

@@ -92,6 +92,8 @@ export const QUERY_BODYSHOP = gql`
cdk_dealerid
features
attach_pdf_to_email
tt_allow_post_to_invoiced
cdk_configuration
employees {
id
active
@@ -180,6 +182,8 @@ export const UPDATE_SHOP = gql`
md_jobline_presets
cdk_dealerid
attach_pdf_to_email
tt_allow_post_to_invoiced
cdk_configuration
employees {
id
first_name
@@ -206,6 +210,10 @@ export const QUERY_INTAKE_CHECKLIST = gql`
scheduled_delivery
intakechecklist
status
owner {
allow_text_message
id
}
labhrs: joblines_aggregate(
where: {
_and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }]

View File

@@ -1025,6 +1025,7 @@ export const SEARCH_JOBS_FOR_AUTOCOMPLETE = gql`
$search: String
$isConverted: Boolean
$notExported: Boolean
$notInvoiced: Boolean
) {
search_jobs(
args: { search: $search }
@@ -1033,6 +1034,7 @@ export const SEARCH_JOBS_FOR_AUTOCOMPLETE = gql`
_and: {
converted: { _eq: $isConverted }
date_exported: { _is_null: $notExported }
date_invoiced: { _is_null: $notInvoiced }
}
}
) {
@@ -1847,6 +1849,10 @@ export const QUERY_JOB_CHECKLISTS = gql`
scheduled_delivery
actual_delivery
production_vars
owner {
id
allow_text_message
}
bodyshop {
id
intakechecklist
@@ -1869,3 +1875,100 @@ export const FIND_JOBS_BY_CLAIM = gql`
}
}
`;
export const QUERY_JOB_EXPORT_DMS = gql`
query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
jobs_by_pk(id: $id) {
ro_number
invoice_allocation
ins_co_id
id
ded_amt
ded_status
depreciation_taxes
other_amount_payable
towing_payable
storage_payable
adjustment_bottom_line
federal_tax_rate
state_tax_rate
local_tax_rate
tax_tow_rt
tax_str_rt
tax_paint_mat_rt
tax_sub_rt
tax_lbr_rt
tax_levies_rt
parts_tax_rates
job_totals
rate_la1
rate_la2
rate_la3
rate_la4
rate_laa
rate_lab
rate_lad
rate_lae
rate_laf
rate_lag
rate_lam
rate_lar
rate_las
rate_lau
rate_ma2s
rate_ma2t
rate_ma3s
rate_mabl
rate_macs
rate_mahw
rate_mapa
rate_mash
rate_matd
status
date_exported
date_invoiced
voided
scheduled_completion
actual_completion
scheduled_delivery
actual_delivery
scheduled_in
actual_in
bills {
id
federal_tax_rate
local_tax_rate
state_tax_rate
is_credit_memo
billlines {
actual_cost
cost_center
id
quantity
}
}
joblines(where: { removed: { _eq: false } }) {
id
removed
tax_part
line_desc
prt_dsmk_p
prt_dsmk_m
part_type
oem_partno
db_price
act_price
part_qty
mod_lbr_ty
db_hrs
mod_lb_hrs
lbr_op
lbr_amt
op_code_desc
profitcenter_labor
profitcenter_part
prt_dsmk_p
}
}
}
`;

View File

@@ -5,6 +5,7 @@ export const INSERT_NEW_PARTS_ORDERS = gql`
insert_parts_orders(objects: $po) {
returning {
id
order_number
}
}
}

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { useQuery } from "@apollo/client";
import { Card, Col, Result, Row, Space } from "antd";
import { Card, Col, Result, Row, Space, Typography } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -84,6 +84,9 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader }) {
return (
<RbacWrapper action="jobs:admin">
<Typography.Title level={4} style={{ color: "tomato" }}>
{t("jobs.labels.adminwarning")}
</Typography.Title>
<Row gutter={[16, 16]}>
<Col {...colSpan}>
<Card style={cardStyle}>

View File

@@ -5,6 +5,7 @@ import Icon, {
FileImageFilled,
PrinterFilled,
ToolFilled,
HistoryOutlined,
} from "@ant-design/icons";
import {
Button,
@@ -45,6 +46,9 @@ import ScheduleJobModalContainer from "../../components/schedule-job-modal/sched
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { insertAuditTrail } from "../../redux/application/application.actions";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -53,6 +57,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "printCenter" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export function JobsDetailPage({
setPrintCenterContext,
@@ -60,6 +66,7 @@ export function JobsDetailPage({
job,
mutationUpdateJob,
handleSubmit,
insertAuditTrail,
refetch,
}) {
const { t } = useTranslation();
@@ -81,6 +88,7 @@ export function JobsDetailPage({
const handleFinish = async (values) => {
setLoading(true);
const result = await mutationUpdateJob({
variables: {
jobId: job.id,
@@ -105,6 +113,62 @@ export function JobsDetailPage({
notification["success"]({
message: t("jobs.successes.savetitle"),
});
const changedAuditFields = form.getFieldsValue(
[
"scheduled_in",
"actual_in",
"scheduled_completion",
"actual_completion",
"scheduled_delivery",
"actual_delivery",
"date_invoiced",
"ins_co_nm",
"ded_amt",
"ded_status",
"date_exported",
"special_coverage_policy",
"ca_gst_registrant",
"ca_bc_pvrt",
"scheduled_in",
"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_mapa",
"rate_mahw",
"rate_mash",
"rate_matd",
],
(meta) => meta && meta.touched
);
Object.keys(changedAuditFields).forEach((key) => {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobfieldchange(
key,
changedAuditFields[key] instanceof moment
? moment(changedAuditFields[key]).format("MM/DD/YYYY hh:mm a")
: changedAuditFields[key]
),
});
});
await refetch();
form.setFieldsValue(transormJobToForm(job));
form.resetFields();
@@ -279,6 +343,17 @@ export function JobsDetailPage({
>
<JobNotesContainer jobId={job.id} />
</Tabs.TabPane>
<Tabs.TabPane
tab={
<span>
<HistoryOutlined />
{t("jobs.labels.audit")}
</span>
}
key="audit"
>
<JobAuditTrail jobId={job.id} />
</Tabs.TabPane>
</Tabs>
</Form>
</div>

View File

@@ -53,3 +53,8 @@ export const setOnline = (isOnline) => ({
type: ApplicationActionTypes.SET_ONLINE_STATUS,
payload: isOnline,
});
export const insertAuditTrail = ({ jobid, billid, operation }) => ({
type: ApplicationActionTypes.INSERT_AUDIT_TRAIL,
payload: { jobid, billid, operation },
});

View File

@@ -1,6 +1,7 @@
import moment from "moment";
import { all, call, put, select, takeLatest } from "redux-saga/effects";
import { QUERY_SCHEDULE_LOAD_DATA } from "../../graphql/appointments.queries";
import { INSERT_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
import client from "../../utils/GraphQLClient";
import { CalculateLoad, CheckJobBucket } from "../../utils/SSSUtils";
import {
@@ -125,6 +126,56 @@ export function* calculateScheduleLoad({ payload: end }) {
}
}
export function* applicationSagas() {
yield all([call(onCalculateScheduleLoad)]);
export function* onInsertAuditTrail() {
yield takeLatest(
ApplicationActionTypes.INSERT_AUDIT_TRAIL,
insertAuditTrailSaga
);
}
export function* insertAuditTrailSaga({
payload: { jobid, billid, operation },
}) {
const state = yield select();
const bodyshop = state.user.bodyshop;
const currentUser = state.user.currentUser;
console.log(
"Inserting audit trail for",
bodyshop.shopname,
currentUser.email,
jobid,
billid,
operation
);
const variables = {
auditObj: {
bodyshopid: bodyshop.id,
jobid,
billid,
operation,
useremail: currentUser.email,
},
};
yield client.mutate({
mutation: INSERT_AUDIT_TRAIL,
variables,
update(cache, { data }) {
cache.modify({
fields: {
audit_trail(existingAuditTrail, { readField }) {
const newAuditTrail = cache.writeQuery({
data: data.insert_audit_trail_one,
query: INSERT_AUDIT_TRAIL,
variables,
});
return [...existingAuditTrail, newAuditTrail];
},
},
});
},
});
}
export function* applicationSagas() {
yield all([call(onCalculateScheduleLoad), call(onInsertAuditTrail)]);
}

View File

@@ -10,5 +10,6 @@ const ApplicationActionTypes = {
SET_JOB_READONLY: "SET_JOB_READONLY",
SET_PARTNER_VERSION: "SET_PARTNER_VERSION",
SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
};
export default ApplicationActionTypes;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ export function* openChatByPhone({ payload }) {
data: { conversations },
} = yield client.query({
query: CONVERSATION_ID_BY_PHONE,
variables: { phone: phone(phone_num)[0] },
variables: { phone: phone(phone_num).phoneNumber },
fetchPolicy: "network-only",
});
@@ -53,7 +53,7 @@ export function* openChatByPhone({ payload }) {
variables: {
conversation: [
{
phone_num: phone(phone_num)[0],
phone_num: phone(phone_num).phoneNumber,
bodyshopid: bodyshop.id,
job_conversations: jobid ? { data: { jobid: jobid } } : null,
},

View File

@@ -28,6 +28,7 @@ import {
validatePasswordResetSuccess,
} from "./user.actions";
import UserActionTypes from "./user.types";
import * as Sentry from "@sentry/browser";
export function* onEmailSignInStart() {
yield takeLatest(UserActionTypes.EMAIL_SIGN_IN_START, signInWithEmail);
@@ -100,8 +101,12 @@ export function* onUpdateUserDetails() {
}
export function* updateUserDetails(userDetails) {
try {
yield updateCurrentUser(userDetails.payload);
yield put(updateUserDetailsSuccess(userDetails.payload));
const updatedDetails = yield updateCurrentUser(userDetails.payload);
console.log(
"🚀 ~ file: user.sagas.js ~ line 104 ~ updatedDetails",
updatedDetails
);
yield put(updateUserDetailsSuccess(updatedDetails));
notification.open({
type: "success",
message: i18next.t("profile.successes.updated"),
@@ -174,6 +179,11 @@ export function* signInSuccessSaga({ payload }) {
]);
console.log("Setting $crisp segments", ["user"]);
window.$crisp.push(["set", "session:segments", [["user"]]]);
Sentry.setUser({
email: payload.email,
username: payload.displayName || payload.email,
});
} catch (error) {
console.log("Error updating Crisp settings.", error);
}

View File

@@ -37,6 +37,7 @@
"fields": {
"alt_transport": "Alt. Trans.",
"color": "Appointment Color",
"note": "Appt. Note",
"time": "Appointment Time",
"title": "Title"
},
@@ -52,6 +53,7 @@
"nocompletingjobs": "No jobs scheduled for completion.",
"nodateselected": "No date has been selected.",
"priorappointments": "Previous Appointments",
"reminder": "This is {{shopname}} reminding you about an appointment on {{date}} at {{time}}. Please let us know if you are not able to make the appointment. We look forward to seeing you soon. ",
"scheduledfor": "Scheduled appointment for: ",
"smartscheduling": "Smart Scheduling",
"suggesteddates": "Suggested Dates"
@@ -82,6 +84,24 @@
"values": "Values"
}
},
"audit_trail": {
"messages": {
"billposted": "Bill with invoice number {{invoice_number}} posted.",
"billupdated": "Bill with invoice number {{invoice_number}} updated.",
"jobassignmentchange": "Employee {{name}} assigned to {{operation}}",
"jobassignmentremoved": "Employee assignment removed for {{operation}}",
"jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.",
"jobconverted": "Job converted and assigned number {{ro_number}}.",
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
"jobimported": "Job imported.",
"jobinproductionchange": "Job production status set to {{inproduction}}",
"jobmodifylbradj": "Labor adjustments modified.",
"jobspartsorder": "Parts order {{order_number}} added to job.",
"jobspartsreturn": "Parts return {{order_number}} added to job.",
"jobstatuschange": "Job status changed to {{status}}.",
"jobsupplement": "Job supplement imported."
}
},
"billlines": {
"actions": {
"newline": "New Line"
@@ -211,6 +231,12 @@
"deliver": {
"templates": "Delivery Templates"
},
"dms": {
"default_journal": "Default Journal",
"dms_acctnumber": "DMS Account #",
"dms_wip_acctnumber": "DMS W.I.P. Account #",
"mappingname": "DMS Mapping Name"
},
"email": "General Shop Email",
"enforce_class": "Enforce Class on Conversion?",
"enforce_referral": "Enforce Referrals",
@@ -426,6 +452,7 @@
"production_statuses": "Production Statuses"
},
"target_touchtime": "Target Touch Time",
"tt_allow_post_to_invoiced": "Allow Time Tickets to be posted to Invoiced & Exported Jobs",
"use_fippa": "Use FIPPA for Names on Generated Documents?",
"website": "Website",
"zip_post": "Zip/Postal Code"
@@ -446,6 +473,10 @@
"defaultcostsmapping": "Default Costs Mapping",
"defaultprofitsmapping": "Default Profits Mapping",
"deliverchecklist": "Delivery Checklist",
"dms": {
"cdk_dealerid": "CDK Dealer ID",
"title": "DMS"
},
"employees": "Employees",
"insurancecos": "Insurance Companies",
"intakechecklist": "Intake Checklist",
@@ -491,6 +522,7 @@
},
"labels": {
"addtoproduction": "Add Job to Production?",
"allow_text_message": "Permission to Text?",
"checklist": "Checklist",
"printpack": "Job Intake Print Pack",
"removefromproduction": "Remove job from production?"
@@ -793,6 +825,9 @@
"exportlogs": {
"fields": {
"createdat": "Created At"
},
"labels": {
"attempts": "Export Attempts"
}
},
"general": {
@@ -857,6 +892,7 @@
"message": "Message",
"monday": "Monday",
"na": "N/A",
"newpassword": "New Password",
"no": "No",
"nointernet": "It looks like you're not connected to the internet.",
"nointernet_sub": "Please check your connection and try again. ",
@@ -877,6 +913,7 @@
"sendagain": "Send Again",
"sendby": "Send By",
"signin": "Sign In",
"sms": "SMS",
"sub_status": {
"expired": "The subscription for this shop has expired. Please contact technical support to reactivate the subscription. "
},
@@ -1027,6 +1064,7 @@
"changestatus": "Change Status",
"convert": "Convert",
"deliver": "Deliver",
"dmsautoallocate": "DMS Auto Allocate",
"export": "Export",
"exportcustdata": "Export Customer Data",
"exportselected": "Export Selected",
@@ -1036,6 +1074,7 @@
"intake": "Intake",
"manualnew": "Create New Job Manually",
"mark": "Mark",
"markasexported": "Mark as Exported",
"markpstexempt": "Mark Job PST Exempt",
"markpstexemptconfirm": "Are you sure you want to do this? To undo this, you must manually update all PST rates.",
"postbills": "Post Bills",
@@ -1046,6 +1085,7 @@
"schedule": "Schedule",
"sendcsi": "Send CSI",
"sync": "Sync",
"uninvoice": "Uninvoice",
"unvoid": "Unvoid Job",
"viewchecklist": "View Checklists",
"viewdetail": "View Details"
@@ -1104,6 +1144,14 @@
"ded_amt": "Deductible",
"ded_status": "Deductible Status",
"depreciation_taxes": "Depreciation/Taxes",
"dms": {
"center": "Center",
"cost": "Cost",
"cost_dms_acctnumber": "Cost DMS Acct #",
"dms_wip_acctnumber": "Cost WIP DMS Acct #",
"sale": "Sale",
"sale_dms_acctnumber": "Sale DMS Acct #"
},
"driveable": "Driveable",
"employee_body": "Body",
"employee_csr": "Customer Service Rep.",
@@ -1227,6 +1275,7 @@
"servicecar": "Service Car",
"servicing_dealer": "Servicing Dealer",
"servicing_dealer_contact": "Servicing Dealer Contact",
"special_coverage_policy": "Special Coverage Policy",
"specialcoveragepolicy": "Special Coverage Policy",
"state_tax_rate": "Provincial/State Tax Rate",
"status": "Job Status",
@@ -1264,6 +1313,7 @@
"additionaltotal": "Additional Total",
"adjustmentrate": "Adjustment Rate",
"adjustments": "Adjustments",
"adminwarning": "Use the functionality on this page at your own risk. You are responsible for any and all changes to your data.",
"allocations": "Allocations",
"alreadyclosed": "This job has already been closed.",
"appointmentconfirmation": "Send confirmation to customer?",
@@ -1316,7 +1366,8 @@
"waived": "Waived"
},
"deleteconfirm": "Are you sure you want to delete this job? This cannot be undone. ",
"deleteintake": "Delete Intake",
"deletedelivery": "Delete Delivery Checklist",
"deleteintake": "Delete Intake Checklist",
"deliverchecklist": "Deliver Checklist",
"difference": "Difference",
"diskscan": "Scan Disk for Estimates",
@@ -1953,12 +2004,15 @@
"bills": "Bills",
"exportlogs": "Export Logs",
"jobs": "Jobs",
"parts_orders": "Parts Orders",
"payments": "Payments",
"scoreboard": "Scoreboard",
"timetickets": "Timetickets"
},
"vendor": "Vendor"
},
"templates": {
"anticipated_revenue": "Anticipated Revenue",
"attendance_detail": "Attendance (All Employees)",
"attendance_employee": "Employee Attendance",
"attendance_summary": "Attendance Summary (All Employees)",
@@ -1994,11 +2048,13 @@
"job_costing_ro_date_summary": "Job Costing by RO - Summary",
"job_costing_ro_estimator": "Job Costing by Estimator",
"job_costing_ro_ins_co": "Job Costing by RO Source",
"lag_time": "Lag Time",
"open_orders": "Open Orders by Date",
"open_orders_csr": "Open Orders by CSR",
"open_orders_estimator": "Open Orders by Estimator",
"open_orders_ins_co": "Open Orders by Insurance Company",
"parts_backorder": "Backordered Parts",
"parts_not_recieved": "Parts Not Received",
"payments_by_date": "Payments by Date",
"payments_by_date_type": "Payments by Date and Type",
"production_by_csr": "Production by CSR",
@@ -2015,6 +2071,8 @@
"purchases_grouped_by_vendor_detailed": "Purchases Grouped by Vendor - Detailed",
"purchases_grouped_by_vendor_summary": "Purchases Grouped by Vendor - Summary",
"schedule": "Appointment Schedule",
"scoreboard_detail": "Scoreboard Detail",
"scoreboard_summary": "Scoreboard Summary",
"supplement_ratio_ins_co": "Supplement Ratio by Source",
"thank_you_date": "Thank You Letters",
"timetickets": "Time Tickets",
@@ -2054,6 +2112,11 @@
"updated": "Scoreboard updated."
}
},
"scoredboard": {
"successes": {
"updated": "Scoreboard entry updated."
}
},
"tech": {
"fields": {
"employeeid": "Employee ID",
@@ -2221,6 +2284,7 @@
},
"user": {
"actions": {
"changepassword": "Change Password",
"signout": "Sign Out",
"updateprofile": "Update Profile"
},
@@ -2235,6 +2299,9 @@
},
"labels": {
"actions": "Actions"
},
"successess": {
"passwordchanged": "Password changed successfully. "
}
},
"vehicles": {

View File

@@ -37,6 +37,7 @@
"fields": {
"alt_transport": "",
"color": "",
"note": "",
"time": "",
"title": "Título"
},
@@ -52,6 +53,7 @@
"nocompletingjobs": "",
"nodateselected": "No se ha seleccionado ninguna fecha.",
"priorappointments": "Nombramientos previos",
"reminder": "",
"scheduledfor": "Cita programada para:",
"smartscheduling": "",
"suggesteddates": ""
@@ -82,6 +84,24 @@
"values": ""
}
},
"audit_trail": {
"messages": {
"billposted": "",
"billupdated": "",
"jobassignmentchange": "",
"jobassignmentremoved": "",
"jobchecklist": "",
"jobconverted": "",
"jobfieldchanged": "",
"jobimported": "",
"jobinproductionchange": "",
"jobmodifylbradj": "",
"jobspartsorder": "",
"jobspartsreturn": "",
"jobstatuschange": "",
"jobsupplement": ""
}
},
"billlines": {
"actions": {
"newline": ""
@@ -211,6 +231,12 @@
"deliver": {
"templates": ""
},
"dms": {
"default_journal": "",
"dms_acctnumber": "",
"dms_wip_acctnumber": "",
"mappingname": ""
},
"email": "",
"enforce_class": "",
"enforce_referral": "",
@@ -426,6 +452,7 @@
"production_statuses": ""
},
"target_touchtime": "",
"tt_allow_post_to_invoiced": "",
"use_fippa": "",
"website": "",
"zip_post": ""
@@ -446,6 +473,10 @@
"defaultcostsmapping": "",
"defaultprofitsmapping": "",
"deliverchecklist": "",
"dms": {
"cdk_dealerid": "",
"title": ""
},
"employees": "",
"insurancecos": "",
"intakechecklist": "",
@@ -491,6 +522,7 @@
},
"labels": {
"addtoproduction": "",
"allow_text_message": "",
"checklist": "",
"printpack": "",
"removefromproduction": ""
@@ -793,6 +825,9 @@
"exportlogs": {
"fields": {
"createdat": ""
},
"labels": {
"attempts": ""
}
},
"general": {
@@ -857,6 +892,7 @@
"message": "",
"monday": "",
"na": "N / A",
"newpassword": "",
"no": "",
"nointernet": "",
"nointernet_sub": "",
@@ -877,6 +913,7 @@
"sendagain": "",
"sendby": "",
"signin": "",
"sms": "",
"sub_status": {
"expired": ""
},
@@ -1027,6 +1064,7 @@
"changestatus": "Cambiar Estado",
"convert": "Convertir",
"deliver": "",
"dmsautoallocate": "",
"export": "",
"exportcustdata": "",
"exportselected": "",
@@ -1036,6 +1074,7 @@
"intake": "",
"manualnew": "",
"mark": "",
"markasexported": "",
"markpstexempt": "",
"markpstexemptconfirm": "",
"postbills": "Contabilizar facturas",
@@ -1046,6 +1085,7 @@
"schedule": "Programar",
"sendcsi": "",
"sync": "",
"uninvoice": "",
"unvoid": "",
"viewchecklist": "",
"viewdetail": ""
@@ -1104,6 +1144,14 @@
"ded_amt": "Deducible",
"ded_status": "Estado deducible",
"depreciation_taxes": "Depreciación / Impuestos",
"dms": {
"center": "",
"cost": "",
"cost_dms_acctnumber": "",
"dms_wip_acctnumber": "",
"sale": "",
"sale_dms_acctnumber": ""
},
"driveable": "",
"employee_body": "",
"employee_csr": "Representante de servicio al cliente.",
@@ -1227,6 +1275,7 @@
"servicecar": "Auto de servicio",
"servicing_dealer": "Distribuidor de servicio",
"servicing_dealer_contact": "Servicio Contacto con el concesionario",
"special_coverage_policy": "Política de cobertura especial",
"specialcoveragepolicy": "Política de cobertura especial",
"state_tax_rate": "",
"status": "Estado del trabajo",
@@ -1264,6 +1313,7 @@
"additionaltotal": "",
"adjustmentrate": "",
"adjustments": "",
"adminwarning": "",
"allocations": "",
"alreadyclosed": "",
"appointmentconfirmation": "¿Enviar confirmación al cliente?",
@@ -1316,6 +1366,7 @@
"waived": ""
},
"deleteconfirm": "",
"deletedelivery": "",
"deleteintake": "",
"deliverchecklist": "",
"difference": "",
@@ -1953,12 +2004,15 @@
"bills": "",
"exportlogs": "",
"jobs": "",
"parts_orders": "",
"payments": "",
"scoreboard": "",
"timetickets": ""
},
"vendor": ""
},
"templates": {
"anticipated_revenue": "",
"attendance_detail": "",
"attendance_employee": "",
"attendance_summary": "",
@@ -1994,11 +2048,13 @@
"job_costing_ro_date_summary": "",
"job_costing_ro_estimator": "",
"job_costing_ro_ins_co": "",
"lag_time": "",
"open_orders": "",
"open_orders_csr": "",
"open_orders_estimator": "",
"open_orders_ins_co": "",
"parts_backorder": "",
"parts_not_recieved": "",
"payments_by_date": "",
"payments_by_date_type": "",
"production_by_csr": "",
@@ -2015,6 +2071,8 @@
"purchases_grouped_by_vendor_detailed": "",
"purchases_grouped_by_vendor_summary": "",
"schedule": "",
"scoreboard_detail": "",
"scoreboard_summary": "",
"supplement_ratio_ins_co": "",
"thank_you_date": "",
"timetickets": "",
@@ -2054,6 +2112,11 @@
"updated": ""
}
},
"scoredboard": {
"successes": {
"updated": ""
}
},
"tech": {
"fields": {
"employeeid": "",
@@ -2221,6 +2284,7 @@
},
"user": {
"actions": {
"changepassword": "",
"signout": "desconectar",
"updateprofile": "Actualización del perfil"
},
@@ -2235,6 +2299,9 @@
},
"labels": {
"actions": ""
},
"successess": {
"passwordchanged": ""
}
},
"vehicles": {

View File

@@ -37,6 +37,7 @@
"fields": {
"alt_transport": "",
"color": "",
"note": "",
"time": "",
"title": "Titre"
},
@@ -52,6 +53,7 @@
"nocompletingjobs": "",
"nodateselected": "Aucune date n'a été sélectionnée.",
"priorappointments": "Rendez-vous précédents",
"reminder": "",
"scheduledfor": "Rendez-vous prévu pour:",
"smartscheduling": "",
"suggesteddates": ""
@@ -82,6 +84,24 @@
"values": ""
}
},
"audit_trail": {
"messages": {
"billposted": "",
"billupdated": "",
"jobassignmentchange": "",
"jobassignmentremoved": "",
"jobchecklist": "",
"jobconverted": "",
"jobfieldchanged": "",
"jobimported": "",
"jobinproductionchange": "",
"jobmodifylbradj": "",
"jobspartsorder": "",
"jobspartsreturn": "",
"jobstatuschange": "",
"jobsupplement": ""
}
},
"billlines": {
"actions": {
"newline": ""
@@ -211,6 +231,12 @@
"deliver": {
"templates": ""
},
"dms": {
"default_journal": "",
"dms_acctnumber": "",
"dms_wip_acctnumber": "",
"mappingname": ""
},
"email": "",
"enforce_class": "",
"enforce_referral": "",
@@ -426,6 +452,7 @@
"production_statuses": ""
},
"target_touchtime": "",
"tt_allow_post_to_invoiced": "",
"use_fippa": "",
"website": "",
"zip_post": ""
@@ -446,6 +473,10 @@
"defaultcostsmapping": "",
"defaultprofitsmapping": "",
"deliverchecklist": "",
"dms": {
"cdk_dealerid": "",
"title": ""
},
"employees": "",
"insurancecos": "",
"intakechecklist": "",
@@ -491,6 +522,7 @@
},
"labels": {
"addtoproduction": "",
"allow_text_message": "",
"checklist": "",
"printpack": "",
"removefromproduction": ""
@@ -793,6 +825,9 @@
"exportlogs": {
"fields": {
"createdat": ""
},
"labels": {
"attempts": ""
}
},
"general": {
@@ -857,6 +892,7 @@
"message": "",
"monday": "",
"na": "N / A",
"newpassword": "",
"no": "",
"nointernet": "",
"nointernet_sub": "",
@@ -877,6 +913,7 @@
"sendagain": "",
"sendby": "",
"signin": "",
"sms": "",
"sub_status": {
"expired": ""
},
@@ -1027,6 +1064,7 @@
"changestatus": "Changer le statut",
"convert": "Convertir",
"deliver": "",
"dmsautoallocate": "",
"export": "",
"exportcustdata": "",
"exportselected": "",
@@ -1036,6 +1074,7 @@
"intake": "",
"manualnew": "",
"mark": "",
"markasexported": "",
"markpstexempt": "",
"markpstexemptconfirm": "",
"postbills": "Poster des factures",
@@ -1046,6 +1085,7 @@
"schedule": "Programme",
"sendcsi": "",
"sync": "",
"uninvoice": "",
"unvoid": "",
"viewchecklist": "",
"viewdetail": ""
@@ -1104,6 +1144,14 @@
"ded_amt": "Déductible",
"ded_status": "Statut de franchise",
"depreciation_taxes": "Amortissement / taxes",
"dms": {
"center": "",
"cost": "",
"cost_dms_acctnumber": "",
"dms_wip_acctnumber": "",
"sale": "",
"sale_dms_acctnumber": ""
},
"driveable": "",
"employee_body": "",
"employee_csr": "représentant du service à la clientèle",
@@ -1227,6 +1275,7 @@
"servicecar": "Voiture de service",
"servicing_dealer": "Concessionnaire",
"servicing_dealer_contact": "Contacter le concessionnaire",
"special_coverage_policy": "Politique de couverture spéciale",
"specialcoveragepolicy": "Politique de couverture spéciale",
"state_tax_rate": "",
"status": "Statut de l'emploi",
@@ -1264,6 +1313,7 @@
"additionaltotal": "",
"adjustmentrate": "",
"adjustments": "",
"adminwarning": "",
"allocations": "",
"alreadyclosed": "",
"appointmentconfirmation": "Envoyer une confirmation au client?",
@@ -1316,6 +1366,7 @@
"waived": ""
},
"deleteconfirm": "",
"deletedelivery": "",
"deleteintake": "",
"deliverchecklist": "",
"difference": "",
@@ -1953,12 +2004,15 @@
"bills": "",
"exportlogs": "",
"jobs": "",
"parts_orders": "",
"payments": "",
"scoreboard": "",
"timetickets": ""
},
"vendor": ""
},
"templates": {
"anticipated_revenue": "",
"attendance_detail": "",
"attendance_employee": "",
"attendance_summary": "",
@@ -1994,11 +2048,13 @@
"job_costing_ro_date_summary": "",
"job_costing_ro_estimator": "",
"job_costing_ro_ins_co": "",
"lag_time": "",
"open_orders": "",
"open_orders_csr": "",
"open_orders_estimator": "",
"open_orders_ins_co": "",
"parts_backorder": "",
"parts_not_recieved": "",
"payments_by_date": "",
"payments_by_date_type": "",
"production_by_csr": "",
@@ -2015,6 +2071,8 @@
"purchases_grouped_by_vendor_detailed": "",
"purchases_grouped_by_vendor_summary": "",
"schedule": "",
"scoreboard_detail": "",
"scoreboard_summary": "",
"supplement_ratio_ins_co": "",
"thank_you_date": "",
"timetickets": "",
@@ -2054,6 +2112,11 @@
"updated": ""
}
},
"scoredboard": {
"successes": {
"updated": ""
}
},
"tech": {
"fields": {
"employeeid": "",
@@ -2221,6 +2284,7 @@
},
"user": {
"actions": {
"changepassword": "",
"signout": "Déconnexion",
"updateprofile": "Mettre à jour le profil"
},
@@ -2235,6 +2299,9 @@
},
"labels": {
"actions": ""
},
"successess": {
"passwordchanged": ""
}
},
"vehicles": {

View File

@@ -0,0 +1,31 @@
import i18n from "i18next";
const AuditTrailMapping = {
jobstatuschange: (status) =>
i18n.t("audit_trail.messages.jobstatuschange", { status }),
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"),
jobimported: () => i18n.t("audit_trail.messages.jobimported"),
jobconverted: (ro_number) =>
i18n.t("audit_trail.messages.jobconverted", { ro_number }),
jobfieldchange: (field, value) =>
i18n.t("audit_trail.messages.jobfieldchanged", { field, value }),
jobspartsorder: (order_number) =>
i18n.t("audit_trail.messages.jobspartsorder", { order_number }),
jobspartsreturn: (order_number) =>
i18n.t("audit_trail.messages.jobspartsreturn", { order_number }),
jobmodifylbradj: () => i18n.t("audit_trail.messages.jobmodifylbradj", {}),
billposted: (invoice_number) =>
i18n.t("audit_trail.messages.billposted", { invoice_number }),
billupdated: (invoice_number) =>
i18n.t("audit_trail.messages.billupdated", { invoice_number }),
jobassignmentchange: (operation, name) =>
i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }),
jobassignmentremoved: (operation) =>
i18n.t("audit_trail.messages.jobassignmentremoved", { operation }),
jobinproductionchange: (inproduction) =>
i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }),
jobchecklist: (type, inproduction, status) =>
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
};
export default AuditTrailMapping;

View File

@@ -6,7 +6,6 @@ import { WebSocketLink } from "@apollo/client/link/ws";
import { getMainDefinition } from "@apollo/client/utilities";
//import { split } from "apollo-link";
import apolloLogger from "apollo-link-logger";
import axios from "axios";
import { auth } from "../firebase/firebase.utils";
import errorLink from "../graphql/apollo-error-handling";
@@ -48,7 +47,7 @@ const roundTripLink = new ApolloLink((operation, forward) => {
});
const TrackExecutionTime = async (operationName, time) => {
await axios.post("/ioevent", { operationName, time, dbevent: true });
//await axios.post("/ioevent", { operationName, time, dbevent: true });
};
const subscriptionMiddleware = {

View File

@@ -1190,6 +1190,66 @@ export const TemplateList = (type, context) => {
field: i18n.t("jobs.fields.date_open"),
},
},
lag_time: {
title: i18n.t("reportcenter.templates.lag_time"),
description: "",
subject: i18n.t("reportcenter.templates.lag_time"),
key: "lag_time",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
},
parts_not_recieved: {
title: i18n.t("reportcenter.templates.parts_not_recieved"),
description: "",
subject: i18n.t("reportcenter.templates.parts_not_recieved"),
key: "parts_not_recieved",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.parts_orders"),
field: i18n.t("parts_orders.fields.order_date"),
},
},
scoreboard_detail: {
title: i18n.t("reportcenter.templates.scoreboard_detail"),
description: "",
subject: i18n.t("reportcenter.templates.scoreboard_detail"),
key: "scoreboard_detail",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.scoreboard"),
field: i18n.t("scoreboard.fields.date"),
},
},
scoreboard_summary: {
title: i18n.t("reportcenter.templates.scoreboard_summary"),
description: "",
subject: i18n.t("reportcenter.templates.scoreboard_summary"),
key: "scoreboard_summary",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.scoreboard"),
field: i18n.t("scoreboard.fields.date"),
},
},
anticipated_revenue: {
title: i18n.t("reportcenter.templates.anticipated_revenue"),
description: "",
subject: i18n.t("reportcenter.templates.anticipated_revenue"),
key: "anticipated_revenue",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.scheduled_completion"), // Also date invoice.
},
},
}
: {}),
...(!type || type === "courtesycarcontract"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
- args:
permission:
allow_aggregations: false
columns:
- id
- new_val
- old_val
- operation
- schemaname
- tabname
- useremail
- created
- bodyshopid
- recordid
computed_fields: []
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: audit_trail
schema: public
type: create_select_permission

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: audit_trail
schema: public
type: drop_select_permission

View File

@@ -0,0 +1,10 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."audit_trail" ADD COLUMN "schemaname" text;
type: run_sql
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."audit_trail" ALTER COLUMN "schemaname" DROP NOT NULL;
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."audit_trail" DROP COLUMN "schemaname" CASCADE;
type: run_sql

View File

@@ -0,0 +1,10 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."audit_trail" ADD COLUMN "tabname" text;
type: run_sql
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."audit_trail" ALTER COLUMN "tabname" DROP NOT NULL;
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."audit_trail" DROP COLUMN "tabname" CASCADE;
type: run_sql

View File

@@ -0,0 +1,10 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."audit_trail" ADD COLUMN "recordid" uuid;
type: run_sql
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."audit_trail" ALTER COLUMN "recordid" DROP NOT NULL;
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."audit_trail" DROP COLUMN "recordid" CASCADE;
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."audit_trail" DROP COLUMN "jobid";
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."audit_trail" ADD COLUMN "jobid" uuid NULL;
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."audit_trail" DROP COLUMN "billid";
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."audit_trail" ADD COLUMN "billid" uuid NULL;
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: alter table "public"."audit_trail" drop constraint "audit_trail_billid_fkey";
type: run_sql

View File

@@ -0,0 +1,10 @@
- args:
cascade: false
read_only: false
sql: |-
alter table "public"."audit_trail"
add constraint "audit_trail_billid_fkey"
foreign key ("billid")
references "public"."bills"
("id") on update cascade on delete cascade;
type: run_sql

View File

@@ -0,0 +1,12 @@
- args:
cascade: false
read_only: false
sql: |-
alter table "public"."audit_trail" drop constraint "audit_trail_billid_fkey",
add constraint "audit_trail_billid_fkey"
foreign key ("billid")
references "public"."bills"
("id")
on update cascade
on delete cascade;
type: run_sql

Some files were not shown because too many files have changed in this diff Show More