Compare commits

..

1 Commits

Author SHA1 Message Date
Patrick Fic
0455e03bfd Remove apollo sentry error. 2024-01-31 10:44:52 -08:00
130 changed files with 1430 additions and 7686 deletions

View File

@@ -42,7 +42,7 @@ jobs:
app-build: app-build:
docker: docker:
- image: cimg/node:16.15.0 - image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client working_directory: ~/repo/client
steps: steps:
@@ -83,7 +83,7 @@ jobs:
test-app-build: test-app-build:
docker: docker:
- image: cimg/node:16.15.0 - image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client working_directory: ~/repo/client
steps: steps:
@@ -159,4 +159,4 @@ workflows:
#- admin-app-build: #- admin-app-build:
#filters: #filters:
#branches: #branches:
#only: master #only: master

View File

@@ -1,16 +0,0 @@
exports.default = {
printWidth: 120,
useTabs: false,
tabWidth: 2,
trailingComma: "es5",
semi: true,
singleQuote: false,
bracketSpacing: true,
arrowParens: "always",
jsxSingleQuote: false,
bracketSameLine: false,
endOfLine: "lf",
importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
};

View File

@@ -1,189 +0,0 @@
# Filters and Sorters
This documentation details the schema required for `.filters` files on the report server. It is used to dynamically
modify the graphQL query and provide the user more power over their reports.
For filters and sorters, valid types include (`type` key in the schema):
- string (default)
- number
- bool or boolean
- date
## Special Notes
- When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected
## High level Schema Overview
```javascript
const schema = {
"filters": [
{
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
"type": "number" // Type of field, can be number or string currently
},
// ... more filters
],
"sorters": [
{
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
"type": "number" // Type of field, can be number or string currently
},
// ... more sorters
],
"dates": {
// This is not yet implemented and will be added in a future release
}
}
```
## Filters
Filters effect the where clause of the graphQL query. They are used to filter the data returned from the server.
A note on special notation used in the `name` field.
## Reflection
Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file.
```json
{
"name": "jobs.status",
"translation": "jobs.fields.status",
"label": "Status",
"type": "string",
"reflector": {
"type": "internal",
"name": "special.job_statuses"
}
}
```
in this example, a reflector with the type 'internal' (all types at the moment require this, and it is used for future functionality), with a name of `special.job_statuses`
The following cases are available
- `special.job_statuses` - This will reflect the statuses of the jobs table `bodyshop.md_ro_statuses.statuses'`
- `special.cost_centers` - This will reflect the cost centers `bodyshop.md_responsibility_centers.costs`
- `special.categories` - This will reflect the categories `bodyshop.md_categories`
- `special.insurance_companies` - This will reflect the insurance companies `bodyshop.md_ins_cos`'
- `special.employee_teams` - This will reflect the employee teams `bodyshop.employee_teams`
- `special.employees` - This will reflect the employees `bodyshop.employees`
- `special.first_names` - This will reflect the first names `bodyshop.employees`
- `special.last_names` - This will reflect the last names `bodyshop.employees`
- `special.referral_sources` - This will reflect the referral sources `bodyshop.md_referral_sources`
- `special.class`- This will reflect the class `bodyshop.md_classes`
- `special.lost_sale_reasons` - This will reflect the lost sale reasons `bodyshop.md_lost_sale_reasons`
- `special.alt_transports` - This will reflect the alternative transports `bodyshop.appt_alt_transport`
- `special.payment_types` - This will reflect the payment types `bodyshop.md_payment_types`
- `special.payment_payers` - This is a special case with a key value set of [Customer, Insurance]
### Path without brackets, multi level
`"name": "jobs.joblines.mod_lb_hrs",`
This will produce a where clause at the `joblines` level of the graphQL query,
```graphql
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
jobs(
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}}
) {
joblines(
order_by: {line_no: asc}
where: {removed: {_eq: false}, mod_lb_hrs: {_lt: 3}}
) {
line_no
mod_lbr_ty
mod_lb_hrs
convertedtolbr
convertedtolbr_data
}
ownr_co_nm
ownr_fn
ownr_ln
plate_no
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_vin
v_color
}
}
```
### Path with brackets,top level
`"name": "[jobs].joblines.mod_lb_hrs",`
This will produce a where clause at the `jobs` level of the graphQL query.
```graphql
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
jobs(
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}, joblines: {mod_lb_hrs: {_gt: 4}}}
) {
joblines(
order_by: {line_no: asc}
where: {removed: {_eq: false}}
) {
line_no
mod_lbr_ty
mod_lb_hrs
convertedtolbr
convertedtolbr_data
}
ownr_co_nm
ownr_fn
ownr_ln
plate_no
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_vin
v_color
}
}
```
## Known Caveats
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs`
is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
- The type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' and is case-sensitive.
- The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used.
- Do not add the ability to filter things that are already filtered as part of the original query, this would be
redundant and could cause issues.
- Do not add the ability to filter on things like FK constraints, must like the above example.
- *INHERITANCE CAVEAT* If you have a filters file on a parent report that has a child that you do not want the filters inherited from, you must place a blank filters file (valid json so {}) on the child report level. This will than fetch the child filters, which are empty and move along, versus inheriting the parent filters.
## Sorters
- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting,
a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs`
would be added at the `joblines` level.
- Most of the reports currently do sorting on a template level, this will need to change to actually see the results
using the sorters.
### Default Sorters
- A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves.
- The `order` key is the order in which the sorters are applied, and the `direction` key is the direction of the sort, either `asc` or `desc`.
```json
{
"name": "jobs.joblines.mod_lb_hrs",
"translation": "jobs.joblines.mod_lb_hrs_1",
"label": "mod_lb_hrs_1",
"type": "number",
"default": {
"order": 1,
"direction": "asc"
}
}
```

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,21 @@
import { Card, Checkbox, Input, Space, Table } from "antd"; import { Input, Table, Checkbox, Card, Space } from "antd";
import queryString from "query-string";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
import PayableExportButton from "../payable-export-button/payable-export-button.component"; import PayableExportButton from "../payable-export-button/payable-export-button.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component"; import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
import { DateFormatter } from "../../utils/DateFormatter";
import queryString from "query-string";
import { logImEXEvent } from "../../firebase/firebase.utils";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -138,6 +138,7 @@ export function AccountingPayablesTableComponent({
title: t("exportlogs.labels.attempts"), title: t("exportlogs.labels.attempts"),
dataIndex: "attempts", dataIndex: "attempts",
key: "attempts", key: "attempts",
render: (text, record) => ( render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} /> <ExportLogsCountDisplay logs={record.exportlogs} />
), ),
@@ -146,6 +147,8 @@ export function AccountingPayablesTableComponent({
title: t("general.labels.actions"), title: t("general.labels.actions"),
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => ( render: (text, record) => (
<PayableExportButton <PayableExportButton
billId={record.id} billId={record.id}

View File

@@ -8,16 +8,14 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay, { import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import PaymentExportButton from "../payment-export-button/payment-export-button.component"; import PaymentExportButton from "../payment-export-button/payment-export-button.component";
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component"; import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component"; import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -77,11 +75,7 @@ export function AccountingPayablesTableComponent({
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
sorter: (a, b) => sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
alphaSort(
OwnerNameDisplayFunction(a.job),
OwnerNameDisplayFunction(b.job)
),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
@@ -100,9 +94,6 @@ export function AccountingPayablesTableComponent({
title: t("payments.fields.amount"), title: t("payments.fields.amount"),
dataIndex: "amount", dataIndex: "amount",
key: "amount", key: "amount",
sorter: (a, b) => a.amount - b.amount,
sortOrder:
state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<CurrencyFormatter>{record.amount}</CurrencyFormatter> <CurrencyFormatter>{record.amount}</CurrencyFormatter>
), ),
@@ -121,21 +112,18 @@ export function AccountingPayablesTableComponent({
title: t("payments.fields.created_at"), title: t("payments.fields.created_at"),
dataIndex: "created_at", dataIndex: "created_at",
key: "created_at", key: "created_at",
sorter: (a, b) => dateSort(a.created_at, b.created_at),
sortOrder:
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<DateTimeFormatter>{record.created_at}</DateTimeFormatter> <DateTimeFormatter>{record.created_at}</DateTimeFormatter>
), ),
}, },
// { {
// title: t("payments.fields.exportedat"), title: t("payments.fields.exportedat"),
// dataIndex: "exportedat", dataIndex: "exportedat",
// key: "exportedat", key: "exportedat",
// render: (text, record) => ( render: (text, record) => (
// <DateTimeFormatter>{record.exportedat}</DateTimeFormatter> <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
// ), ),
// }, },
{ {
title: t("exportlogs.labels.attempts"), title: t("exportlogs.labels.attempts"),
dataIndex: "attempts", dataIndex: "attempts",
@@ -149,6 +137,8 @@ export function AccountingPayablesTableComponent({
title: t("general.labels.actions"), title: t("general.labels.actions"),
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => ( render: (text, record) => (
<PaymentExportButton <PaymentExportButton
paymentId={record.id} paymentId={record.id}

View File

@@ -4,19 +4,17 @@ import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort, dateSort, statusSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component"; import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component"; import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { DateFormatter } from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -65,7 +63,7 @@ export function AccountingReceivablesTableComponent({
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses), sorter: (a, b) => a.status - b.status,
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
}, },
@@ -85,8 +83,7 @@ export function AccountingReceivablesTableComponent({
title: t("jobs.fields.owner"), title: t("jobs.fields.owner"),
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
sorter: (a, b) => sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
@@ -106,15 +103,6 @@ export function AccountingReceivablesTableComponent({
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid}> <Link to={"/manage/vehicles/" + record.vehicleid}>

View File

@@ -3,22 +3,10 @@ import { useMutation } from "@apollo/client";
import { Button, notification, Popconfirm } from "antd"; import { Button, notification, Popconfirm } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { DELETE_BILL } from "../../graphql/bills.queries"; import { DELETE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({}); export default function BillDeleteButton({ bill, callback }) {
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL); const [deleteBill] = useMutation(DELETE_BILL);
@@ -48,11 +36,6 @@ export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") }); notification["success"]({ message: t("bills.successes.deleted") });
insertAuditTrail({
jobid: jobid,
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
type: "billdeleted",
});
if (callback && typeof callback === "function") callback(bill.id); if (callback && typeof callback === "function") callback(bill.id);
} else { } else {

View File

@@ -33,8 +33,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })), dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
@@ -150,7 +150,6 @@ export function BillDetailEditcontainer({
jobid: bill.jobid, jobid: bill.jobid,
billid: search.billid, billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number), operation: AuditTrailMapping.billupdated(bill.invoice_number),
type: "billupdated",
}); });
await refetch(); await refetch();

View File

@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })), dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(

View File

@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")), toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({ jobid, billid, operation, type }) => insertAuditTrail: ({ jobid, billid, operation }) =>
dispatch(insertAuditTrail({ jobid, billid, operation, type })), dispatch(insertAuditTrail({ jobid, billid, operation })),
}); });
const Templates = TemplateList("job_special"); const Templates = TemplateList("job_special");
@@ -94,7 +94,6 @@ function BillEnterModalContainer({
location, location,
outstanding_returns, outstanding_returns,
inventory, inventory,
federal_tax_exempt,
...remainingValues ...remainingValues
} = values; } = values;
@@ -171,7 +170,6 @@ function BillEnterModalContainer({
mod_lbr_ty: key, mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1), hours: adjustmentsToInsert[key].toFixed(1),
}), }),
type: "jobmodifylbradj",
}); });
}); });
@@ -321,7 +319,6 @@ function BillEnterModalContainer({
operation: AuditTrailMapping.billposted( operation: AuditTrailMapping.billposted(
r1.data.insert_bills.returning[0].invoice_number r1.data.insert_bills.returning[0].invoice_number
), ),
type: "billposted",
}); });
if (enterAgain) { if (enterAgain) {

View File

@@ -9,8 +9,8 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component"; import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component"; import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
@@ -58,7 +58,7 @@ export function BillsListTableComponent({
<EditFilled /> <EditFilled />
</Button> </Button>
)} )}
<BillDeleteButton bill={record} jobid={job.id} /> <BillDeleteButton bill={record} />
<BillDetailEditReturnComponent <BillDetailEditReturnComponent
data={{ bills_by_pk: { ...record, jobid: job.id } }} data={{ bills_by_pk: { ...record, jobid: job.id } }}
disabled={ disabled={

View File

@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")), toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
}); });
@@ -102,7 +102,6 @@ const CardPaymentModalComponent = ({
insertAuditTrail({ insertAuditTrail({
jobid: payment.jobid, jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(), operation: AuditTrailMapping.failedpayment(),
type: "failedpayment",
}) })
); );
}); });

View File

@@ -68,30 +68,6 @@ export default function ContractFormComponent({
<FormDateTimePicker /> <FormDateTimePicker />
</Form.Item> </Form.Item>
)} )}
{create && (
<Form.Item
shouldUpdate={(p, c) => p.scheduledreturn !== c.scheduledreturn}
>
{() => {
const insuranceOver =
selectedCar &&
selectedCar.insuranceexpires &&
moment(selectedCar.insuranceexpires)
.endOf("day")
.isBefore(moment(form.getFieldValue("scheduledreturn")));
if (insuranceOver)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.insuranceexpired")}
</span>
</Space>
);
return <></>;
}}
</Form.Item>
)}
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow> <LayoutFormRow grow>
<Form.Item <Form.Item
@@ -114,17 +90,16 @@ export default function ContractFormComponent({
> >
{() => { {() => {
const mileageOver = const mileageOver =
selectedCar && selectedCar.nextservicekm selectedCar &&
? selectedCar.nextservicekm <= form.getFieldValue("kmstart") selectedCar.nextservicekm <= form.getFieldValue("kmstart");
: false;
const dueForService = const dueForService =
selectedCar && selectedCar &&
selectedCar.nextservicedate && selectedCar.nextservicedate &&
moment(selectedCar.nextservicedate) moment(selectedCar.nextservicedate).isBefore(
.endOf("day") moment(form.getFieldValue("scheduledreturn"))
.isSameOrBefore( );
moment(form.getFieldValue("scheduledreturn"))
);
if (mileageOver || dueForService) if (mileageOver || dueForService)
return ( return (
<Space direction="vertical" style={{ color: "tomato" }}> <Space direction="vertical" style={{ color: "tomato" }}>
@@ -142,6 +117,7 @@ export default function ContractFormComponent({
</span> </span>
</Space> </Space>
); );
return <></>; return <></>;
}} }}
</Form.Item> </Form.Item>

View File

@@ -17,18 +17,13 @@ import { DateTimeFormatter } from "../../utils/DateFormatter";
import { GenerateDocument } from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
export default function CourtesyCarsList({ loading, courtesycars, refetch }) { export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: { text: "" },
}); });
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [filter, setFilter] = useLocalStorage(
"filter_courtesy_cars_list",
null
);
const { t } = useTranslation(); const { t } = useTranslation();
const columns = [ const columns = [
@@ -55,7 +50,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => alphaSort(a.status, b.status), sorter: (a, b) => alphaSort(a.status, b.status),
filteredValue: filter?.status || null,
filters: [ filters: [
{ {
text: t("courtesycars.status.in"), text: t("courtesycars.status.in"),
@@ -78,8 +72,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
const { nextservicedate, nextservicekm, mileage, insuranceexpires } = const { nextservicedate, nextservicekm, mileage } = record;
record;
const mileageOver = nextservicekm ? nextservicekm <= mileage : false; const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
@@ -87,25 +80,11 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
nextservicedate && nextservicedate &&
moment(nextservicedate).endOf("day").isSameOrBefore(moment()); moment(nextservicedate).endOf("day").isSameOrBefore(moment());
const insuranceOver =
insuranceexpires &&
moment(insuranceexpires).endOf("day").isBefore(moment());
return ( return (
<Space> <Space>
{t(record.status)} {t(record.status)}
{(mileageOver || dueForService || insuranceOver) && ( {(mileageOver || dueForService) && (
<Tooltip <Tooltip title={t("contracts.labels.cardueforservice")}>
title={
(mileageOver || dueForService) && insuranceOver
? t("contracts.labels.insuranceexpired") +
" / " +
t("contracts.labels.cardueforservice")
: insuranceOver
? t("contracts.labels.insuranceexpired")
: t("contracts.labels.cardueforservice")
}
>
<WarningFilled style={{ color: "tomato" }} /> <WarningFilled style={{ color: "tomato" }} />
</Tooltip> </Tooltip>
)} )}
@@ -118,7 +97,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
dataIndex: "readiness", dataIndex: "readiness",
key: "readiness", key: "readiness",
sorter: (a, b) => alphaSort(a.readiness, b.readiness), sorter: (a, b) => alphaSort(a.readiness, b.readiness),
filteredValue: filter?.readiness || null,
filters: [ filters: [
{ {
text: t("courtesycars.readiness.ready"), text: t("courtesycars.readiness.ready"),
@@ -234,8 +212,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
setFilter(filters);
}; };
const tableData = searchText const tableData = searchText

View File

@@ -5,7 +5,6 @@ import React, { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries"; import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries";
import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import ConfigFormComponents from "../config-form-components/config-form-components.component"; import ConfigFormComponents from "../config-form-components/config-form-components.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
@@ -45,13 +44,6 @@ export default function CsiResponseFormContainer() {
readOnly readOnly
componentList={data.csi_by_pk.csiquestion.config} componentList={data.csi_by_pk.csiquestion.config}
/> />
{data.csi_by_pk.validuntil ? (
<>
{t("csi.fields.validuntil")}
{": "}
<DateFormatter>{data.csi_by_pk.validuntil}</DateFormatter>
</>
) : null}
</Form> </Form>
</Card> </Card>
); );

View File

@@ -5,11 +5,9 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config"; import { alphaSort } from "../../utils/sorters";
import { alphaSort, dateSort } from "../../utils/sorters"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import OwnerNameDisplay, { import {pageLimit} from "../../utils/config";
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
export default function CsiResponseListPaginated({ export default function CsiResponseListPaginated({
refetch, refetch,
@@ -18,23 +16,23 @@ export default function CsiResponseListPaginated({
total, total,
}) { }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const { responseid } = search; const { responseid, page, sortcolumn, sortorder } = search;
const history = useHistory(); const history = useHistory();
const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: { text: "" }, filteredInfo: { text: "" },
page: "",
}); });
const { t } = useTranslation();
const columns = [ const columns = [
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
width: "8%",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder: sortOrder: sortcolumn === "ro_number" && sortorder,
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link to={"/manage/jobs/" + record.job.id}> <Link to={"/manage/jobs/" + record.job.id}>
{record.job.ro_number || t("general.labels.na")} {record.job.ro_number || t("general.labels.na")}
@@ -43,18 +41,15 @@ export default function CsiResponseListPaginated({
}, },
{ {
title: t("jobs.fields.owner"), title: t("jobs.fields.owner"),
dataIndex: "owner_name", dataIndex: "owner",
key: "owner_name", key: "owner",
sorter: (a, b) => ellipsis: true,
alphaSort( sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
OwnerNameDisplayFunction(a.job), width: "25%",
OwnerNameDisplayFunction(b.job) sortOrder: sortcolumn === "owner" && sortorder,
),
sortOrder:
state.sortedInfo.columnKey === "owner_name" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.job.ownerid ? ( return record.job.owner ? (
<Link to={"/manage/owners/" + record.job.ownerid}> <Link to={"/manage/owners/" + record.job.owner.id}>
<OwnerNameDisplay ownerObject={record.job} /> <OwnerNameDisplay ownerObject={record.job} />
</Link> </Link>
) : ( ) : (
@@ -69,9 +64,9 @@ export default function CsiResponseListPaginated({
dataIndex: "completedon", dataIndex: "completedon",
key: "completedon", key: "completedon",
ellipsis: true, ellipsis: true,
sorter: (a, b) => dateSort(a.completedon, b.completedon), sorter: (a, b) => a.completedon - b.completedon,
sortOrder: width: "25%",
state.sortedInfo.columnKey === "completedon" && state.sortedInfo.order, sortOrder: sortcolumn === "completedon" && sortorder,
render: (text, record) => { render: (text, record) => {
return record.completedon ? ( return record.completedon ? (
<DateFormatter>{record.completedon}</DateFormatter> <DateFormatter>{record.completedon}</DateFormatter>
@@ -81,12 +76,11 @@ export default function CsiResponseListPaginated({
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
...state, search.page = pagination.current;
filteredInfo: filters, search.sortcolumn = sorter.columnKey;
sortedInfo: sorter, search.sortorder = sorter.order;
page: pagination.current, history.push({ search: queryString.stringify(search) });
});
}; };
const handleOnRowClick = (record) => { const handleOnRowClick = (record) => {
@@ -114,7 +108,7 @@ export default function CsiResponseListPaginated({
pagination={{ pagination={{
position: "top", position: "top",
pageSize: pageLimit, pageSize: pageLimit,
current: parseInt(state.page || 1), current: parseInt(page || 1),
total: total, total: total,
}} }}
columns={columns} columns={columns}
@@ -128,6 +122,13 @@ export default function CsiResponseListPaginated({
selectedRowKeys: [responseid], selectedRowKeys: [responseid],
type: "radio", type: "radio",
}} }}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
}, // click row
};
}}
/> />
</Card> </Card>
); );

View File

@@ -1,169 +0,0 @@
import {Card, Table, Tag} from "antd";
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
import {useTranslation} from "react-i18next";
import React, {useEffect, useState} from "react";
import moment from "moment";
import DashboardRefreshRequired from "../refresh-required.component";
import axios from "axios";
const fortyFiveDaysAgo = () => moment().subtract(45, 'days').toLocaleString();
export default function JobLifecycleDashboardComponent({data, bodyshop, ...cardProps}) {
const {t} = useTranslation();
const [loading, setLoading] = useState(false);
const [lifecycleData, setLifecycleData] = useState(null);
useEffect(() => {
async function getLifecycleData() {
if (data && data.job_lifecycle) {
setLoading(true);
const response = await axios.post("/job/lifecycle", {
jobids: data.job_lifecycle.map(x => x.id),
statuses: bodyshop.md_order_statuses
});
setLifecycleData(response.data.durations);
setLoading(false);
}
}
getLifecycleData().catch(e => {
console.error(`Error in getLifecycleData: ${e}`);
})
}, [data, bodyshop]);
const columns = [
{
title: t('job_lifecycle.columns.status'),
dataIndex: 'status',
bgColor: 'red',
key: 'status',
render: (text, record) => {
return <Tag color={record.color}>{record.status}</Tag>
}
},
{
title: t('job_lifecycle.columns.human_readable'),
dataIndex: 'humanReadable',
key: 'humanReadable',
},
{
title: t('job_lifecycle.columns.status_count'),
key: 'statusCount',
render: (text, record) => {
return lifecycleData.statusCounts[record.status];
}
},
{
title: t('job_lifecycle.columns.percentage'),
dataIndex: 'percentage',
key: 'percentage',
render: (text, record) => {
return record.percentage.toFixed(2) + '%';
}
},
];
if (!data) return null;
if (!data.job_lifecycle || !lifecycleData) return <DashboardRefreshRequired {...cardProps} />;
const extra = `${t('job_lifecycle.content.calculated_based_on')} ${lifecycleData.jobs} ${t('job_lifecycle.content.jobs_in_since')} ${fortyFiveDaysAgo()}`
return (
<Card title={t("job_lifecycle.titles.dashboard")} {...cardProps}>
<LoadingSkeleton loading={loading}>
<div style={{overflow: 'scroll', height: "100%"}}>
<div id="bar-container" style={{
display: 'flex',
width: '100%',
height: '100px',
textAlign: 'center',
borderRadius: '5px',
borderWidth: '5px',
borderStyle: 'solid',
borderColor: '#f0f2f5',
margin: 0,
padding: 0
}}>
{lifecycleData.summations.map((key, index, array) => {
const isFirst = index === 0;
const isLast = index === array.length - 1;
return (
<div key={key.status} style={{
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
margin: 0,
padding: 0,
borderTop: '1px solid #f0f2f5',
borderBottom: '1px solid #f0f2f5',
borderLeft: isFirst ? '1px solid #f0f2f5' : undefined,
borderRight: isLast ? '1px solid #f0f2f5' : undefined,
backgroundColor: key.color,
width: `${key.percentage}%`
}}
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
>
{key.percentage > 15 ?
<>
<div>{key.roundedPercentage}</div>
<div style={{
backgroundColor: '#f0f2f5',
borderRadius: '5px',
paddingRight: '2px',
paddingLeft: '2px',
fontSize: '0.8rem',
}}>
{key.status}
</div>
</>
: null}
</div>
);
})}
</div>
<Card extra={extra} type='inner' title={t('job_lifecycle.content.legend_title')}
style={{marginTop: '10px'}}>
<div>
{lifecycleData.summations.map((key) => (
<Tag color={key.color} style={{width: '13vh', padding: '4px', margin: '4px'}}>
<div
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
style={{
backgroundColor: '#f0f2f5',
color: '#000',
padding: '4px',
textAlign: 'center'
}}>
{key.status} [{lifecycleData.statusCounts[key.status]}] ({key.roundedPercentage})
</div>
</Tag>
))}
</div>
</Card>
<Card style={{marginTop: "5px"}} type='inner' title={t("job_lifecycle.titles.top_durations")}>
<Table size="small" pagination={false} columns={columns}
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}/>
</Card>
</div>
</LoadingSkeleton>
</Card>
);
}
export const JobLifecycleDashboardGQL = `
job_lifecycle: jobs(where: {
actual_in: {
_gte: "${moment().subtract(45, 'days').toISOString()}"
}
}) {
id
actual_in
} `;

View File

@@ -3,32 +3,21 @@ import {
ExclamationCircleFilled, ExclamationCircleFilled,
PauseCircleOutlined, PauseCircleOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd"; import { Card, Space, Table, Tooltip } from "antd";
import moment from "moment"; import moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { TimeFormatter } from "../../../utils/DateFormatter";
import { onlyUnique } from "../../../utils/arrayHelper";
import { alphaSort, dateSort } from "../../../utils/sorters";
import useLocalStorage from "../../../utils/useLocalStorage";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
import OwnerNameDisplay, { import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
OwnerNameDisplayFunction,
} from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component"; import DashboardRefreshRequired from "../refresh-required.component";
import {pageLimit} from "../../../utils/config";
export default function DashboardScheduledInToday({ data, ...cardProps }) { export default function DashboardScheduledInToday({ data, ...cardProps }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: {},
}); });
const [isTvModeScheduledIn, setIsTvModeScheduledIn] = useLocalStorage(
"isTvModeScheduledIn",
false
);
if (!data) return null; if (!data) return null;
if (!data.scheduled_in_today) if (!data.scheduled_in_today)
return <DashboardRefreshRequired {...cardProps} />; return <DashboardRefreshRequired {...cardProps} />;
@@ -42,12 +31,6 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
alt_transport: item.job.alt_transport, alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no, clm_no: item.job.clm_no,
jobid: item.job.jobid, jobid: item.job.jobid,
joblines_body: item.job.joblines
.filter((l) => l.mod_lbr_ty !== "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
joblines_ref: item.job.joblines
.filter((l) => l.mod_lbr_ty === "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
ins_co_nm: item.job.ins_co_nm, ins_co_nm: item.job.ins_co_nm,
iouparent: item.job.iouparent, iouparent: item.job.iouparent,
ownerid: item.job.ownerid, ownerid: item.job.ownerid,
@@ -66,7 +49,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
v_vin: item.job.v_vin, v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid, vehicleid: item.job.vehicleid,
note: item.note, note: item.note,
start: item.start, start: moment(item.start).format("hh:mm a"),
title: item.title, title: item.title,
}; };
appt.push(i); appt.push(i);
@@ -76,192 +59,11 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
return new moment(a.start) - new moment(b.start); return new moment(a.start) - new moment(b.start);
}); });
const tvFontSize = 16;
const tvFontWeight = "bold";
const tvColumns = [
{
title: t("appointments.fields.time"),
dataIndex: "start",
key: "start",
ellipsis: true,
sorter: (a, b) => dateSort(a.start, b.start),
sortOrder:
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<TimeFormatter>{record.start}</TimeFormatter>
</span>
),
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.jobid}
onClick={(e) => e.stopPropagation()}
>
<Space>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</span>
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
record.v_model_yr || ""
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
);
},
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(appt &&
appt
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.alt_transport}
</span>
),
},
{
title: t("jobs.fields.lab"),
dataIndex: "joblines_body",
key: "joblines_body",
sorter: (a, b) => a.joblines_body - b.joblines_body,
sortOrder:
state.sortedInfo.columnKey === "joblines_body" &&
state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_body.toFixed(1)}
</span>
),
},
{
title: t("jobs.fields.lar"),
dataIndex: "joblines_ref",
key: "joblines_ref",
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
sortOrder:
state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_ref.toFixed(1)}
</span>
),
},
];
const columns = [ const columns = [
{
title: t("appointments.fields.time"),
dataIndex: "start",
key: "start",
ellipsis: true,
sorter: (a, b) => dateSort(a.start, b.start),
sortOrder:
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
render: (text, record) => <TimeFormatter>{record.start}</TimeFormatter>,
},
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link <Link
to={"/manage/jobs/" + record.jobid} to={"/manage/jobs/" + record.jobid}
@@ -289,10 +91,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
sorter: (a, b) => responsive: ["md"],
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.ownerid ? ( return record.ownerid ? (
<Link <Link
@@ -309,16 +108,23 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
}, },
}, },
{ {
title: t("dashboard.labels.phone"), title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph", dataIndex: "ownr_ph1",
key: "ownr_ph", key: "ownr_ph1",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<Space size="small" wrap> <ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} /> ),
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} /> },
</Space> {
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
), ),
}, },
{ {
@@ -328,7 +134,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a> <ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
), ),
}, },
{ {
@@ -336,15 +142,6 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link <Link
@@ -368,80 +165,43 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm), },
sortOrder: {
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order, title: t("appointments.fields.time"),
filters: dataIndex: "start",
(appt && key: "start",
appt ellipsis: true,
.map((j) => j.ins_co_nm) responsive: ["md"],
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Ins. Co.*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
}, },
{ {
title: t("appointments.fields.alt_transport"), title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport", dataIndex: "alt_transport",
key: "alt_transport", key: "alt_transport",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), responsive: ["md"],
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(appt &&
appt
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
}, },
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, sortedInfo: sorter });
}; };
return ( return (
<Card <Card
title={t("dashboard.titles.scheduledindate", { title={t("dashboard.titles.scheduledintoday", {
date: moment().startOf("day").format("MM/DD/YYYY"), date: moment().startOf("day").format("MM/DD/YYYY"),
})} })}
extra={
<Space>
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
<Switch
onClick={() => setIsTvModeScheduledIn(!isTvModeScheduledIn)}
defaultChecked={isTvModeScheduledIn}
/>
</Space>
}
{...cardProps} {...cardProps}
> >
<div style={{ height: "100%" }}> <div style={{ height: "100%" }}>
<Table <Table
onChange={handleTableChange} onChange={handleTableChange}
pagination={false} pagination={{ position: "top", defaultPageSize: pageLimit }}
columns={isTvModeScheduledIn ? tvColumns : columns} columns={columns}
scroll={{ x: true, y: "calc(100% - 2em)" }} scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id" rowKey="id"
style={{ height: "85%" }} style={{ height: "85%" }}
dataSource={appt} dataSource={appt}
size={isTvModeScheduledIn ? "small" : "middle"}
/> />
</div> </div>
</Card> </Card>
@@ -460,10 +220,6 @@ export const DashboardScheduledInTodayGql = `
alt_transport alt_transport
clm_no clm_no
jobid: id jobid: id
joblines(where: {removed: {_eq: false}}) {
mod_lb_hrs
mod_lbr_ty
}
ins_co_nm ins_co_nm
iouparent iouparent
ownerid ownerid

View File

@@ -3,272 +3,37 @@ import {
ExclamationCircleFilled, ExclamationCircleFilled,
PauseCircleOutlined, PauseCircleOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd"; import { Card, Space, Table, Tooltip } from "antd";
import moment from "moment"; import moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { TimeFormatter } from "../../../utils/DateFormatter";
import { onlyUnique } from "../../../utils/arrayHelper";
import { alphaSort, dateSort } from "../../../utils/sorters";
import useLocalStorage from "../../../utils/useLocalStorage";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
import OwnerNameDisplay, { import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
OwnerNameDisplayFunction,
} from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component"; import DashboardRefreshRequired from "../refresh-required.component";
import {pageLimit} from "../../../utils/config";
export default function DashboardScheduledOutToday({ data, ...cardProps }) { export default function DashboardScheduledOutToday({ data, ...cardProps }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: {},
}); });
const [isTvModeScheduledOut, setIsTvModeScheduledOut] = useLocalStorage(
"isTvModeScheduledOut",
false
);
if (!data) return null; if (!data) return null;
if (!data.scheduled_out_today) if (!data.scheduled_out_today)
return <DashboardRefreshRequired {...cardProps} />; return <DashboardRefreshRequired {...cardProps} />;
data.scheduled_out_today.forEach((item) => { data.scheduled_out_today.forEach((item) => {
item.joblines_body = item.joblines item.scheduled_completion= moment(item.scheduled_completion).format("hh:mm a")
? item.joblines
.filter((l) => l.mod_lbr_ty !== "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
: 0;
item.joblines_ref = item.joblines
? item.joblines
.filter((l) => l.mod_lbr_ty === "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
: 0;
}); });
data.scheduled_out_today.sort(function (a, b) { data.scheduled_out_today.sort(function (a, b) {
return new Date(a.scheduled_completion) - new Date(b.scheduled_completion); return new Date(a.scheduled_completion) - new Date(b.scheduled_completion);
}); });
const tvFontSize = 18;
const tvFontWeight = "bold";
const tvColumns = [
{
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
sorter: (a, b) =>
dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder:
state.sortedInfo.columnKey === "scheduled_completion" &&
state.sortedInfo.order,
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
</span>
),
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.jobid}
onClick={(e) => e.stopPropagation()}
>
<Space>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</span>
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
record.v_model_yr || ""
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
);
},
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(data.scheduled_out_today &&
data.scheduled_out_today
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.alt_transport}
</span>
),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters:
(data.scheduled_out_today &&
data.scheduled_out_today
.map((j) => j.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.status),
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.status}
</span>
),
},
{
title: t("jobs.fields.lab"),
dataIndex: "joblines_body",
key: "joblines_body",
sorter: (a, b) => a.joblines_body - b.joblines_body,
sortOrder:
state.sortedInfo.columnKey === "joblines_body" &&
state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_body.toFixed(1)}
</span>
),
},
{
title: t("jobs.fields.lar"),
dataIndex: "joblines_ref",
key: "joblines_ref",
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
sortOrder:
state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_ref.toFixed(1)}
</span>
),
},
];
const columns = [ const columns = [
{
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
sorter: (a, b) =>
dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder:
state.sortedInfo.columnKey === "scheduled_completion" &&
state.sortedInfo.order,
render: (text, record) => (
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
),
},
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link <Link
to={"/manage/jobs/" + record.jobid} to={"/manage/jobs/" + record.jobid}
@@ -296,10 +61,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
sorter: (a, b) => responsive: ["md"],
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.ownerid ? ( return record.ownerid ? (
<Link <Link
@@ -316,16 +78,23 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
}, },
}, },
{ {
title: t("dashboard.labels.phone"), title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph", dataIndex: "ownr_ph1",
key: "ownr_ph", key: "ownr_ph1",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<Space size="small" wrap> <ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} /> ),
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} /> },
</Space> {
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
), ),
}, },
{ {
@@ -335,7 +104,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a> <ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
), ),
}, },
{ {
@@ -343,15 +112,6 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link <Link
@@ -375,80 +135,43 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm), },
sortOrder: {
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order, title: t("jobs.fields.scheduled_completion"),
filters: dataIndex: "scheduled_completion",
(data.scheduled_out_today && key: "scheduled_completion",
data.scheduled_out_today ellipsis: true,
.map((j) => j.ins_co_nm) responsive: ["md"],
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Ins. Co.*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
}, },
{ {
title: t("appointments.fields.alt_transport"), title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport", dataIndex: "alt_transport",
key: "alt_transport", key: "alt_transport",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), responsive: ["md"],
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(data.scheduled_out_today &&
data.scheduled_out_today
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
}, },
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, sortedInfo: sorter });
}; };
return ( return (
<Card <Card
title={t("dashboard.titles.scheduledoutdate", { title={t("dashboard.titles.scheduledouttoday", {
date: moment().startOf("day").format("MM/DD/YYYY"), date: moment().startOf("day").format("MM/DD/YYYY"),
})} })}
extra={
<Space>
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
<Switch
onClick={() => setIsTvModeScheduledOut(!isTvModeScheduledOut)}
defaultChecked={isTvModeScheduledOut}
/>
</Space>
}
{...cardProps} {...cardProps}
> >
<div style={{ height: "100%" }}> <div style={{ height: "100%" }}>
<Table <Table
onChange={handleTableChange} onChange={handleTableChange}
pagination={false} pagination={{ position: "top", defaultPageSize: pageLimit }}
columns={isTvModeScheduledOut ? tvColumns : columns} columns={columns}
scroll={{ x: true, y: "calc(100% - 2em)" }} scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id" rowKey="id"
style={{ height: "85%" }} style={{ height: "85%" }}
dataSource={data.scheduled_out_today} dataSource={data.scheduled_out_today}
size={isTvModeScheduledOut ? "small" : "middle"}
/> />
</div> </div>
</Card> </Card>
@@ -465,10 +188,6 @@ export const DashboardScheduledOutTodayGql = `
alt_transport alt_transport
clm_no clm_no
jobid: id jobid: id
joblines(where: {removed: {_eq: false}}) {
mod_lb_hrs
mod_lbr_ty
}
ins_co_nm ins_co_nm
iouparent iouparent
ownerid ownerid
@@ -481,7 +200,6 @@ export const DashboardScheduledOutTodayGql = `
production_vars production_vars
ro_number ro_number
scheduled_completion scheduled_completion
status
suspended suspended
v_make_desc v_make_desc
v_model_desc v_model_desc

View File

@@ -1,391 +1,386 @@
import Icon, {SyncOutlined} from "@ant-design/icons"; import Icon, { SyncOutlined } from "@ant-design/icons";
import {gql, useMutation, useQuery} from "@apollo/client"; import { gql, useMutation, useQuery } from "@apollo/client";
import {Button, Dropdown, Menu, notification, PageHeader, Space} from "antd"; import { Button, Dropdown, Menu, PageHeader, Space, notification } from "antd";
import i18next from "i18next"; import i18next from "i18next";
import _ from "lodash"; import _ from "lodash";
import moment from "moment"; import moment from "moment";
import React, {useState} from "react"; import React, { useState } from "react";
import {Responsive, WidthProvider} from "react-grid-layout"; import { Responsive, WidthProvider } from "react-grid-layout";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {MdClose} from "react-icons/md"; import { MdClose } from "react-icons/md";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {logImEXEvent} from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import {UPDATE_DASHBOARD_LAYOUT} from "../../graphql/user.queries"; import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import DashboardMonthlyEmployeeEfficiency, { import DashboardMonthlyEmployeeEfficiency, {
DashboardMonthlyEmployeeEfficiencyGql, DashboardMonthlyEmployeeEfficiencyGql,
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component"; } from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component"; import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component"; import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component"; import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
import DashboardMonthlyRevenueGraph, { import DashboardMonthlyRevenueGraph, {
DashboardMonthlyRevenueGraphGql, DashboardMonthlyRevenueGraphGql,
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component"; } from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
import DashboardProjectedMonthlySales, { import DashboardProjectedMonthlySales, {
DashboardProjectedMonthlySalesGql, DashboardProjectedMonthlySalesGql,
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component"; } from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
import DashboardTotalProductionDollars import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
from "../dashboard-components/total-production-dollars/total-production-dollars.component";
import DashboardTotalProductionHours, { import DashboardTotalProductionHours, {
DashboardTotalProductionHoursGql, DashboardTotalProductionHoursGql,
} from "../dashboard-components/total-production-hours/total-production-hours.component"; } from "../dashboard-components/total-production-hours/total-production-hours.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
//Combination of the following: //Combination of the following:
// /node_modules/react-grid-layout/css/styles.css // /node_modules/react-grid-layout/css/styles.css
// /node_modules/react-resizable/css/styles.css // /node_modules/react-resizable/css/styles.css
import DashboardScheduledInToday, { import DashboardScheduledInToday, {
DashboardScheduledInTodayGql, DashboardScheduledInTodayGql,
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component"; } from "../dashboard-components/scheduled-in-today/scheduled-in-today.component";
import DashboardScheduledOutToday, { import DashboardScheduledOutToday, {
DashboardScheduledOutTodayGql, DashboardScheduledOutTodayGql,
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component"; } from "../dashboard-components/scheduled-out-today/scheduled-out-today.component";
import JobLifecycleDashboardComponent, {
JobLifecycleDashboardGQL
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component";
import "./dashboard-grid.styles.scss"; import "./dashboard-grid.styles.scss";
import {GenerateDashboardData} from "./dashboard-grid.utils"; import { GenerateDashboardData } from "./dashboard-grid.utils";
const ResponsiveReactGridLayout = WidthProvider(Responsive); const ResponsiveReactGridLayout = WidthProvider(Responsive);
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export function DashboardGridComponent({currentUser, bodyshop}) { export function DashboardGridComponent({ currentUser, bodyshop }) {
const {t} = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
...(bodyshop.associations[0].user.dashboardlayout ...(bodyshop.associations[0].user.dashboardlayout
? bodyshop.associations[0].user.dashboardlayout ? bodyshop.associations[0].user.dashboardlayout
: {items: [], layout: {}, layouts: []}), : { items: [], layout: {}, layouts: [] }),
});
const { loading, error, data, refetch } = useQuery(
createDashboardQuery(state),
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
);
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
const handleLayoutChange = async (layout, layouts) => {
logImEXEvent("dashboard_change_layout");
setState({ ...state, layout, layouts });
const result = await updateLayout({
variables: {
email: currentUser.email,
layout: { ...state, layout, layouts },
},
}); });
if (!!result.errors) {
notification["error"]({
message: t("dashboard.errors.updatinglayout", {
message: JSON.stringify(result.errors),
}),
});
}
};
const handleRemoveComponent = (key) => {
logImEXEvent("dashboard_remove_component", { name: key });
const idxToRemove = state.items.findIndex((i) => i.i === key);
const {loading, error, data, refetch} = useQuery( const items = _.cloneDeep(state.items);
createDashboardQuery(state),
{fetchPolicy: "network-only", nextFetchPolicy: "network-only"}
);
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT); items.splice(idxToRemove, 1);
setState({ ...state, items });
};
const handleLayoutChange = async (layout, layouts) => { const handleAddComponent = (e) => {
logImEXEvent("dashboard_change_layout"); logImEXEvent("dashboard_add_component", { name: e });
setState({
...state,
items: [
...state.items,
{
i: e.key,
x: (state.items.length * 2) % (state.cols || 12),
y: 99, // puts it at the bottom
w: componentList[e.key].w || 2,
h: componentList[e.key].h || 2,
},
],
});
};
setState({...state, layout, layouts}); const dashboarddata = React.useMemo(
() => GenerateDashboardData(data),
[data]
);
const existingLayoutKeys = state.items.map((i) => i.i);
const addComponentOverlay = (
<Menu onClick={handleAddComponent}>
{Object.keys(componentList).map((key) => (
<Menu.Item
key={key}
value={key}
disabled={existingLayoutKeys.includes(key)}
>
{componentList[key].label}
</Menu.Item>
))}
</Menu>
);
const result = await updateLayout({ if (error) return <AlertComponent message={error.message} type="error" />;
variables: {
email: currentUser.email, return (
layout: {...state, layout, layouts}, <div>
}, <PageHeader
}); extra={
if (!!result.errors) { <Space>
notification["error"]({ <Button onClick={() => refetch()}>
message: t("dashboard.errors.updatinglayout", { <SyncOutlined />
message: JSON.stringify(result.errors), </Button>
}), <Dropdown overlay={addComponentOverlay} trigger={["click"]}>
}); <Button>{t("dashboard.actions.addcomponent")}</Button>
</Dropdown>
</Space>
} }
}; />
const handleRemoveComponent = (key) => {
logImEXEvent("dashboard_remove_component", {name: key});
const idxToRemove = state.items.findIndex((i) => i.i === key);
const items = _.cloneDeep(state.items); <ResponsiveReactGridLayout
className="layout"
items.splice(idxToRemove, 1); breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
setState({...state, items}); cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
}; width="100%"
layouts={state.layouts}
const handleAddComponent = (e) => { onLayoutChange={handleLayoutChange}
logImEXEvent("dashboard_add_component", {name: e}); // onBreakpointChange={onBreakpointChange}
setState({ >
...state, {state.items.map((item, index) => {
items: [ const TheComponent = componentList[item.i].component;
...state.items, return (
{ <div
i: e.key, key={item.i}
x: (state.items.length * 2) % (state.cols || 12), data-grid={{
y: 99, // puts it at the bottom ...item,
w: componentList[e.key].w || 2, minH: componentList[item.i].minH || 1,
h: componentList[e.key].h || 2, minW: componentList[item.i].minW || 1,
}, }}
],
});
};
const dashboarddata = React.useMemo(
() => GenerateDashboardData(data),
[data]
);
const existingLayoutKeys = state.items.map((i) => i.i);
const addComponentOverlay = (
<Menu onClick={handleAddComponent}>
{Object.keys(componentList).map((key) => (
<Menu.Item
key={key}
value={key}
disabled={existingLayoutKeys.includes(key)}
>
{componentList[key].label}
</Menu.Item>
))}
</Menu>
);
if (error) return <AlertComponent message={error.message} type="error"/>;
return (
<div>
<PageHeader
extra={
<Space>
<Button onClick={() => refetch()}>
<SyncOutlined/>
</Button>
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
<Button>{t("dashboard.actions.addcomponent")}</Button>
</Dropdown>
</Space>
}
/>
<ResponsiveReactGridLayout
className="layout"
breakpoints={{lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0}}
cols={{lg: 12, md: 10, sm: 6, xs: 4, xxs: 2}}
width="100%"
layouts={state.layouts}
onLayoutChange={handleLayoutChange}
// onBreakpointChange={onBreakpointChange}
> >
{state.items.map((item, index) => { <LoadingSkeleton loading={loading}>
const TheComponent = componentList[item.i].component; <Icon
return ( component={MdClose}
<div key={item.i}
key={item.i} style={{
data-grid={{ position: "absolute",
...item, zIndex: "2",
minH: componentList[item.i].minH || 1, right: ".25rem",
minW: componentList[item.i].minW || 1, top: ".25rem",
}} cursor: "pointer",
> }}
<LoadingSkeleton loading={loading}> onClick={() => handleRemoveComponent(item.i)}
<Icon />
component={MdClose} <TheComponent className="dashboard-card" data={dashboarddata} />
key={item.i} </LoadingSkeleton>
style={{ </div>
position: "absolute", );
zIndex: "2", })}
right: ".25rem", </ResponsiveReactGridLayout>
top: ".25rem", </div>
cursor: "pointer", );
}}
onClick={() => handleRemoveComponent(item.i)}
/>
<TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboarddata}/>
</LoadingSkeleton>
</div>
);
})}
</ResponsiveReactGridLayout>
</div>
);
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(DashboardGridComponent); )(DashboardGridComponent);
const componentList = { const componentList = {
ProductionDollars: { ProductionDollars: {
label: i18next.t("dashboard.titles.productiondollars"), label: i18next.t("dashboard.titles.productiondollars"),
component: DashboardTotalProductionDollars, component: DashboardTotalProductionDollars,
gqlFragment: null, gqlFragment: null,
w: 1, w: 1,
h: 1, h: 1,
minW: 2, minW: 2,
minH: 1, minH: 1,
}, },
ProductionHours: { ProductionHours: {
label: i18next.t("dashboard.titles.productionhours"), label: i18next.t("dashboard.titles.productionhours"),
component: DashboardTotalProductionHours, component: DashboardTotalProductionHours,
gqlFragment: DashboardTotalProductionHoursGql, gqlFragment: DashboardTotalProductionHoursGql,
w: 3, w: 3,
h: 1, h: 1,
minW: 3, minW: 3,
minH: 1, minH: 1,
}, },
ProjectedMonthlySales: { ProjectedMonthlySales: {
label: i18next.t("dashboard.titles.projectedmonthlysales"), label: i18next.t("dashboard.titles.projectedmonthlysales"),
component: DashboardProjectedMonthlySales, component: DashboardProjectedMonthlySales,
gqlFragment: DashboardProjectedMonthlySalesGql, gqlFragment: DashboardProjectedMonthlySalesGql,
w: 2, w: 2,
h: 1, h: 1,
minW: 2, minW: 2,
minH: 1, minH: 1,
}, },
MonthlyRevenueGraph: { MonthlyRevenueGraph: {
label: i18next.t("dashboard.titles.monthlyrevenuegraph"), label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
component: DashboardMonthlyRevenueGraph, component: DashboardMonthlyRevenueGraph,
gqlFragment: DashboardMonthlyRevenueGraphGql, gqlFragment: DashboardMonthlyRevenueGraphGql,
w: 4, w: 4,
h: 2, h: 2,
minW: 4, minW: 4,
minH: 2, minH: 2,
}, },
MonthlyJobCosting: { MonthlyJobCosting: {
label: i18next.t("dashboard.titles.monthlyjobcosting"), label: i18next.t("dashboard.titles.monthlyjobcosting"),
component: DashboardMonthlyJobCosting, component: DashboardMonthlyJobCosting,
gqlFragment: null, gqlFragment: null,
minW: 6, minW: 6,
minH: 3, minH: 3,
w: 6, w: 6,
h: 3, h: 3,
}, },
MonthlyPartsSales: { MonthlyPartsSales: {
label: i18next.t("dashboard.titles.monthlypartssales"), label: i18next.t("dashboard.titles.monthlypartssales"),
component: DashboardMonthlyPartsSales, component: DashboardMonthlyPartsSales,
gqlFragment: null, gqlFragment: null,
minW: 2, minW: 2,
minH: 2, minH: 2,
w: 2, w: 2,
h: 2, h: 2,
}, },
MonthlyLaborSales: { MonthlyLaborSales: {
label: i18next.t("dashboard.titles.monthlylaborsales"), label: i18next.t("dashboard.titles.monthlylaborsales"),
component: DashboardMonthlyLaborSales, component: DashboardMonthlyLaborSales,
gqlFragment: null, gqlFragment: null,
minW: 2, minW: 2,
minH: 2, minH: 2,
w: 2, w: 2,
h: 2, h: 2,
}, },
// Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings MonthlyEmployeeEfficency: {
MonthlyEmployeeEfficency: { label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"), component: DashboardMonthlyEmployeeEfficiency,
component: DashboardMonthlyEmployeeEfficiency, gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql, minW: 2,
minW: 2, minH: 2,
minH: 2, w: 2,
w: 2, h: 2,
h: 2, },
}, ScheduleInToday: {
ScheduleInToday: { label: i18next.t("dashboard.titles.scheduledintoday", {
label: i18next.t("dashboard.titles.scheduledintoday"), date: moment().startOf("day").format("MM/DD/YYYY"),
component: DashboardScheduledInToday, }),
gqlFragment: DashboardScheduledInTodayGql, component: DashboardScheduledInToday,
minW: 6, gqlFragment: DashboardScheduledInTodayGql,
minH: 2, minW: 10,
w: 10, minH: 2,
h: 3, w: 10,
}, h: 2,
ScheduleOutToday: { },
label: i18next.t("dashboard.titles.scheduledouttoday"), ScheduleOutToday: {
component: DashboardScheduledOutToday, label: i18next.t("dashboard.titles.scheduledouttoday", {
gqlFragment: DashboardScheduledOutTodayGql, date: moment().startOf("day").format("MM/DD/YYYY"),
minW: 6, }),
minH: 2, component: DashboardScheduledOutToday,
w: 10, gqlFragment: DashboardScheduledOutTodayGql,
h: 3, minW: 10,
}, minH: 2,
JobLifecycle: { w: 10,
label: i18next.t("dashboard.titles.joblifecycle"), h: 2,
component: JobLifecycleDashboardComponent, },
gqlFragment: JobLifecycleDashboardGQL,
minW: 6,
minH: 3,
w: 6,
h: 3,
},
}; };
const createDashboardQuery = (state) => { const createDashboardQuery = (state) => {
const componentBasedAdditions = const componentBasedAdditions =
state && state &&
Array.isArray(state.layout) && Array.isArray(state.layout) &&
state.layout state.layout
.map((item, index) => componentList[item.i].gqlFragment || "") .map((item, index) => componentList[item.i].gqlFragment || "")
.join(""); .join("");
return gql` return gql`
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""} query QUERY_DASHBOARD_DETAILS {
monthly_sales: jobs(where: {_and: [ ${componentBasedAdditions || ""}
{ voided: {_eq: false}}, monthly_sales: jobs(where: {_and: [
{date_invoiced: {_gte: "${moment() { voided: {_eq: false}},
{date_invoiced: {_gte: "${moment()
.startOf("month") .startOf("month")
.startOf("day") .startOf("day")
.toISOString()}"}}, {date_invoiced: {_lte: "${moment() .toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month") .endOf("month")
.endOf("day") .endOf("day")
.toISOString()}"}}]}) { .toISOString()}"}}]}) {
id id
ro_number ro_number
date_invoiced date_invoiced
job_totals job_totals
rate_la1 rate_la1
rate_la2 rate_la2
rate_la3 rate_la3
rate_la4 rate_la4
rate_laa rate_laa
rate_lab rate_lab
rate_lad rate_lad
rate_lae rate_lae
rate_laf rate_laf
rate_lag rate_lag
rate_lam rate_lam
rate_lar rate_lar
rate_las rate_las
rate_lau rate_lau
rate_ma2s rate_ma2s
rate_ma2t rate_ma2t
rate_ma3s rate_ma3s
rate_mabl rate_mabl
rate_macs rate_macs
rate_mahw rate_mahw
rate_mapa rate_mapa
rate_mash rate_mash
rate_matd rate_matd
joblines(where: { removed: { _eq: false } }) { joblines(where: { removed: { _eq: false } }) {
id id
mod_lbr_ty mod_lbr_ty
mod_lb_hrs mod_lb_hrs
act_price act_price
part_qty part_qty
part_type part_type
} }
} }
production_jobs: jobs(where: { inproduction: { _eq: true } }) { production_jobs: jobs(where: { inproduction: { _eq: true } }) {
id id
ro_number ro_number
ins_co_nm ins_co_nm
job_totals job_totals
joblines(where: { removed: { _eq: false } }) { joblines(where: { removed: { _eq: false } }) {
id id
mod_lbr_ty mod_lbr_ty
mod_lb_hrs mod_lb_hrs
act_price act_price
part_qty part_qty
part_type part_type
} }
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) { labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
aggregate { aggregate {
sum { sum {
mod_lb_hrs mod_lb_hrs
} }
} }
} }
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) { larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
aggregate { aggregate {
sum { sum {
mod_lb_hrs mod_lb_hrs
} }
} }
} }
} }
}`; }
`;
}; };

View File

@@ -128,7 +128,7 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
.ant-card-body { .ant-card-body {
height: calc(100% - 2rem); height: 80%;
width: 100%; width: 100%;
// // background-color: red; // // background-color: red;
// height: 90%; // height: 90%;

View File

@@ -53,7 +53,6 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
insertAuditTrail({ insertAuditTrail({
jobid: event.job.id, jobid: event.job.id,
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason), operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
type: "appointmentcancel",
}) })
); );
} }

View File

@@ -28,8 +28,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function JobChecklistForm({ export function JobChecklistForm({
@@ -183,7 +183,6 @@ export function JobChecklistForm({
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) || (type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered) (type === "deliver" && bodyshop.md_ro_statuses.default_delivered)
), ),
type: "jobchecklist",
}); });
} else { } else {
notification["error"]({ notification["error"]({

View File

@@ -18,8 +18,10 @@ export default function JobDetailCardsTotalsComponent({ loading, data }) {
/> />
<Statistic <Statistic
className="imex-flex-row__margin-large" className="imex-flex-row__margin-large"
title={t("jobs.fields.customerowing")} title={t("jobs.fields.ded_amt")}
value={Dinero(data.job_totals.totals.custPayable.total).toFormat()} value={Dinero({
amount: Math.round((data.ded_amt || 0) * 100),
}).toFormat()}
/> />
<Statistic <Statistic
className="imex-flex-row__margin-large" className="imex-flex-row__margin-large"

View File

@@ -23,6 +23,7 @@ export function JobEmployeeAssignments({
jobRO, jobRO,
body, body,
refinish, refinish,
prep, prep,
csr, csr,
handleAdd, handleAdd,
@@ -77,7 +78,7 @@ export function JobEmployeeAssignments({
setVisibility(false); setVisibility(false);
}} }}
> >
{t("allocations.actions.assign")} Assign
</Button> </Button>
<Button onClick={() => setVisibility(false)}>Close</Button> <Button onClick={() => setVisibility(false)}>Close</Button>
</Space> </Space>

View File

@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
@@ -43,13 +43,12 @@ export function JobEmployeeAssignmentsContainer({
}); });
if (refetch) refetch(); if (refetch) refetch();
if (!!!result.errors) { insertAuditTrail({
insertAuditTrail({ jobid: job.id,
jobid: job.id, operation: AuditTrailMapping.jobassignmentchange(operation, name),
operation: AuditTrailMapping.jobassignmentchange(operation, name), });
type: "jobassignmentchange",
}); if (!!result.errors) {
} else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.assigning", { message: t("jobs.errors.assigning", {
message: JSON.stringify(result.errors), message: JSON.stringify(result.errors),
@@ -67,19 +66,17 @@ export function JobEmployeeAssignmentsContainer({
variables: { jobId: job.id, job: { [empAssignment]: null } }, variables: { jobId: job.id, job: { [empAssignment]: null } },
}); });
if (!!!result.errors) { if (!!result.errors) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentremoved(operation),
type: "jobassignmentremoved",
});
} else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.assigning", { message: t("jobs.errors.assigning", {
message: JSON.stringify(result.errors), message: JSON.stringify(result.errors),
}), }),
}); });
} }
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentremoved(operation),
});
setLoading(false); setLoading(false);
}; };

View File

@@ -28,8 +28,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
@@ -113,7 +113,6 @@ export function JobLineConvertToLabor({
hours: calculateAdjustment({ mod_lbr_ty, job, jobline }).toFixed(1), hours: calculateAdjustment({ mod_lbr_ty, job, jobline }).toFixed(1),
mod_lbr_ty, mod_lbr_ty,
}), }),
type: "jobmodifylbradj",
}); });
setLoading(false); setLoading(false);
setVisibility(false); setVisibility(false);

View File

@@ -1,16 +1,16 @@
import { useMutation, useLazyQuery } from "@apollo/client";
import { CheckCircleOutlined } from "@ant-design/icons"; import { CheckCircleOutlined } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import { import {
Button, Button,
Card, Card,
Form, Form,
InputNumber, InputNumber,
notification,
Popover, Popover,
Space, Space,
notification,
} from "antd"; } from "antd";
import moment from "moment"; import moment from "moment";
import React, { useEffect, useState } from "react"; import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { import {
@@ -50,7 +50,6 @@ export default function ScoreboardAddButton({
const handleFinish = async (values) => { const handleFinish = async (values) => {
logImEXEvent("job_close_add_to_scoreboard"); logImEXEvent("job_close_add_to_scoreboard");
values.date = moment(values.date).format("YYYY-MM-DD");
setLoading(true); setLoading(true);
let result; let result;
@@ -178,7 +177,7 @@ export default function ScoreboardAddButton({
return acc + job.lbr_adjustments[val]; return acc + job.lbr_adjustments[val];
}, 0); }, 0);
form.setFieldsValue({ form.setFieldsValue({
date: moment(), date: new moment(),
bodyhrs: Math.round(v.bodyhrs * 10) / 10, bodyhrs: Math.round(v.bodyhrs * 10) / 10,
painthrs: Math.round(v.painthrs * 10) / 10, painthrs: Math.round(v.painthrs * 10) / 10,
}); });

View File

@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus); export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
@@ -32,7 +32,6 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_jobstatuschange(status), operation: AuditTrailMapping.admin_jobstatuschange(status),
type: "admin_jobstatuschange",
}); });
// refetch(); // refetch();
}) })

View File

@@ -20,8 +20,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
@@ -57,7 +57,6 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
? DateTimeFormat(changedAuditFields[key]) ? DateTimeFormat(changedAuditFields[key])
: changedAuditFields[key] : changedAuditFields[key]
), ),
type: "admin_jobfieldchange",
}); });
}); });

View File

@@ -23,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
@@ -59,7 +59,6 @@ export function JobAdminMarkReexport({
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkforreexport(), operation: AuditTrailMapping.admin_jobmarkforreexport(),
type: "admin_jobmarkforreexport",
}); });
} else { } else {
notification["error"]({ notification["error"]({
@@ -100,7 +99,6 @@ export function JobAdminMarkReexport({
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkexported(), operation: AuditTrailMapping.admin_jobmarkexported(),
type: "admin_jobmarkexported",
}); });
} else { } else {
notification["error"]({ notification["error"]({
@@ -126,7 +124,6 @@ export function JobAdminMarkReexport({
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_jobuninvoice(), operation: AuditTrailMapping.admin_jobuninvoice(),
type: "admin_jobuninvoice",
}); });
} else { } else {
notification["error"]({ notification["error"]({

View File

@@ -10,8 +10,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({}); const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR); export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR);
@@ -34,7 +34,6 @@ export function JobsAdminRemoveAR({ insertAuditTrail, job }) {
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_job_remove_from_ar(value), operation: AuditTrailMapping.admin_job_remove_from_ar(value),
type: "admin_job_remove_from_ar",
}); });
setSwitchValue(value); setSwitchValue(value);
} else { } else {

View File

@@ -17,8 +17,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid); export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid);
@@ -49,7 +49,6 @@ export function JobsAdminUnvoid({
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_jobunvoid(), operation: AuditTrailMapping.admin_jobunvoid(),
type: "admin_jobunvoid",
}); });
} else { } else {
notification["error"]({ notification["error"]({

View File

@@ -6,10 +6,10 @@ import {
useQuery, useQuery,
} from "@apollo/client"; } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react"; import { useTreatments } from "@splitsoftware/splitio-react";
import { Col, Row, notification } from "antd"; import { Col, notification, Row } from "antd";
import Axios from "axios"; import Axios from "axios";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import moment from "moment-business-days"; import moment from "moment";
import queryString from "query-string"; import queryString from "query-string";
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -30,8 +30,8 @@ import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import confirmDialog from "../../utils/asyncConfirm"; import confirmDialog from "../../utils/asyncConfirm";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import CriticalPartsScan from "../../utils/criticalPartsScan"; import CriticalPartsScan from "../../utils/criticalPartsScan";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component"; import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
@@ -47,8 +47,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function JobsAvailableContainer({ export function JobsAvailableContainer({
bodyshop, bodyshop,
@@ -73,15 +73,7 @@ export function JobsAvailableContainer({
const [selectedJob, setSelectedJob] = useState(null); const [selectedJob, setSelectedJob] = useState(null);
const [selectedOwner, setSelectedOwner] = useState(null); const [selectedOwner, setSelectedOwner] = useState(null);
const [partsQueueToggle, setPartsQueueToggle] = useState( const [partsQueueToggle, setPartsQueueToggle] = useState(bodyshop.md_functionality_toggles.parts_queue_toggle);
bodyshop.md_functionality_toggles.parts_queue_toggle
);
const [updateSchComp, setSchComp] = useState({
actual_in: moment(),
checked: false,
scheduled_completion: moment(),
automatic: false,
});
const [insertLoading, setInsertLoading] = useState(false); const [insertLoading, setInsertLoading] = useState(false);
@@ -190,7 +182,6 @@ export function JobsAvailableContainer({
insertAuditTrail({ insertAuditTrail({
jobid: r.data.insert_jobs.returning[0].id, jobid: r.data.insert_jobs.returning[0].id,
operation: AuditTrailMapping.jobimported(), operation: AuditTrailMapping.jobimported(),
type: "jobimported",
}); });
deleteJob({ deleteJob({
@@ -206,16 +197,11 @@ export function JobsAvailableContainer({
notification["error"]({ notification["error"]({
message: t("jobs.errors.creating", { error: err.message }), message: t("jobs.errors.creating", { error: err.message }),
}); });
refetch().catch((e) => { refetch().catch(e => {console.error(`Something went wrong in jobs available table container - ${err.message || ''}`)});
console.error(
`Something went wrong in jobs available table container - ${
err.message || ""
}`
);
});
setInsertLoading(false); setInsertLoading(false);
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle); setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
} }
}; };
//Supplement scenario //Supplement scenario
@@ -239,22 +225,6 @@ export function JobsAvailableContainer({
//IO-539 Check for Parts Rate on PAL for SGI use case. //IO-539 Check for Parts Rate on PAL for SGI use case.
await CheckTaxRates(supp, bodyshop); await CheckTaxRates(supp, bodyshop);
if (updateSchComp.checked === true) {
if (updateSchComp.automatic === true) {
const job_hrs = supp.joblines.data.reduce(
(acc, val) => acc + val.mod_lb_hrs,
0
);
const num_days = job_hrs / bodyshop.target_touchtime;
supp.actual_in = updateSchComp.actual_in;
supp.scheduled_completion = moment(
updateSchComp.actual_in
).businessAdd(num_days, "days");
} else {
supp.scheduled_completion = updateSchComp.scheduled_completion;
}
}
delete supp.owner; delete supp.owner;
delete supp.vehicle; delete supp.vehicle;
delete supp.ins_co_nm; delete supp.ins_co_nm;
@@ -291,9 +261,9 @@ export function JobsAvailableContainer({
}, },
}); });
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle); setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
if (CriticalPartsScanning.treatment === "on") { if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(updateResult.data.update_jobs.returning[0].id); CriticalPartsScan(updateResult.data.update_jobs.returning[0].id);
} }
if (updateResult.errors) { if (updateResult.errors) {
@@ -351,7 +321,6 @@ export function JobsAvailableContainer({
insertAuditTrail({ insertAuditTrail({
jobid: selectedJob, jobid: selectedJob,
operation: AuditTrailMapping.jobsupplement(), operation: AuditTrailMapping.jobsupplement(),
type: "jobsupplement",
}); });
} }
}; };
@@ -398,6 +367,7 @@ export function JobsAvailableContainer({
if (error) return <AlertComponent type="error" message={error.message} />; if (error) return <AlertComponent type="error" message={error.message} />;
return ( return (
<LoadingSpinner <LoadingSpinner
loading={insertLoading} loading={insertLoading}
@@ -414,6 +384,7 @@ export function JobsAvailableContainer({
visible={ownerModalVisible} visible={ownerModalVisible}
onOk={onOwnerFindModalOk} onOk={onOwnerFindModalOk}
onCancel={onOwnerModalCancel} onCancel={onOwnerModalCancel}
/> />
<JobsFindModalContainer <JobsFindModalContainer
loading={estDataRaw.loading} loading={estDataRaw.loading}
@@ -427,8 +398,6 @@ export function JobsAvailableContainer({
modalSearchState={modalSearchState} modalSearchState={modalSearchState}
partsQueueToggle={partsQueueToggle} partsQueueToggle={partsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle} setPartsQueueToggle={setPartsQueueToggle}
updateSchComp={updateSchComp}
setSchComp={setSchComp}
/> />
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={24}> <Col span={24}>

View File

@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) { export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
@@ -35,7 +35,6 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.jobstatuschange(status), operation: AuditTrailMapping.jobstatuschange(status),
type: "jobstatuschange",
}); });
// refetch(); // refetch();
}) })

View File

@@ -9,12 +9,10 @@ import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils"; import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import client from "../../utils/GraphQLClient"; import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -22,11 +20,6 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
function updateJobCache(items) { function updateJobCache(items) {
client.cache.modify({ client.cache.modify({
id: "ROOT_QUERY", id: "ROOT_QUERY",
@@ -47,7 +40,6 @@ export function JobsCloseExportButton({
disabled, disabled,
setSelectedJobs, setSelectedJobs,
refetch, refetch,
insertAuditTrail,
}) { }) {
const history = useHistory(); const history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -189,11 +181,6 @@ export function JobsCloseExportButton({
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobexported(),
type: "jobexported",
});
updateJobCache( updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id) jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
); );
@@ -205,21 +192,12 @@ export function JobsCloseExportButton({
}); });
} }
} }
if ( if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
successfulTransactions.length > 0
) {
notification.open({ notification.open({
type: "success", type: "success",
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobexported(),
type: "jobexported",
});
updateJobCache([ updateJobCache([
...new Set( ...new Set(
successfulTransactions.map( successfulTransactions.map(
@@ -249,7 +227,4 @@ export function JobsCloseExportButton({
); );
} }
export default connect( export default connect(mapStateToProps, null)(JobsCloseExportButton);
mapStateToProps,
mapDispatchToProps
)(JobsCloseExportButton);

View File

@@ -25,8 +25,8 @@ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function JobsConvertButton({ export function JobsConvertButton({
@@ -78,7 +78,6 @@ export function JobsConvertButton({
operation: AuditTrailMapping.jobconverted( operation: AuditTrailMapping.jobconverted(
res.data.update_jobs.returning[0].ro_number res.data.update_jobs.returning[0].ro_number
), ),
type: "jobconverted",
}); });
setVisible(false); setVisible(false);

View File

@@ -29,7 +29,6 @@ export default function AddToProduction(
insertAuditTrail({ insertAuditTrail({
jobid: jobId, jobid: jobId,
operation: AuditTrailMapping.jobinproductionchange(!remove), operation: AuditTrailMapping.jobinproductionchange(!remove),
type: "jobinproductionchange",
}) })
); );
if (completionCallback) completionCallback(); if (completionCallback) completionCallback();

View File

@@ -52,8 +52,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "timeTicket" })), dispatch(setModalContext({ context: context, modal: "timeTicket" })),
setCardPaymentContext: (context) => setCardPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "cardPayment" })), dispatch(setModalContext({ context: context, modal: "cardPayment" })),
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function JobsDetailHeaderActions({ export function JobsDetailHeaderActions({
@@ -115,7 +115,6 @@ export function JobsDetailHeaderActions({
? !job.production_vars.alert ? !job.production_vars.alert
: true : true
), ),
type: "alertToggle",
}); });
}; };
@@ -130,13 +129,6 @@ export function JobsDetailHeaderActions({
}, },
}, },
}); });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobsuspend(
!!job.suspended ? !job.suspended : true
),
type: "jobsuspend",
});
}; };
const statusmenu = ( const statusmenu = (
@@ -192,7 +184,6 @@ export function JobsDetailHeaderActions({
jobid: job.id, jobid: job.id,
operation: operation:
AuditTrailMapping.appointmentcancel(lost_sale_reason), AuditTrailMapping.appointmentcancel(lost_sale_reason),
type: "appointmentcancel",
}); });
return; return;
} }
@@ -549,11 +540,6 @@ export function JobsDetailHeaderActions({
notification["success"]({ notification["success"]({
message: t("jobs.successes.voided"), message: t("jobs.successes.voided"),
}); });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobvoid(),
type: "jobvoid",
});
//go back to jobs list. //go back to jobs list.
history.push(`/manage/`); history.push(`/manage/`);
} else { } else {

View File

@@ -4,7 +4,7 @@ import {
PauseCircleOutlined, PauseCircleOutlined,
WarningFilled, WarningFilled,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Card, Col, Divider, Row, Space, Tag, Tooltip } from "antd"; import { Card, Col, Row, Space, Tag, Tooltip } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -26,7 +26,6 @@ import ProductionListColumnComment from "../production-list-columns/production-l
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component"; import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
import "./jobs-detail-header.styles.scss"; import "./jobs-detail-header.styles.scss";
import moment from "moment";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
@@ -63,13 +62,6 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
${job.v_make_desc || ""} ${job.v_make_desc || ""}
${job.v_model_desc || ""}`.trim(); ${job.v_model_desc || ""}`.trim();
const bodyHrs = job.joblines
.filter((j) => j.mod_lbr_ty !== "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
const refinishHrs = job.joblines
.filter((line) => line.mod_lbr_ty === "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
const ownerTitle = OwnerNameDisplayFunction(job).trim(); const ownerTitle = OwnerNameDisplayFunction(job).trim();
return ( return (
@@ -101,9 +93,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
{job.status === bodyshop.md_ro_statuses.default_scheduled && {job.status === bodyshop.md_ro_statuses.default_scheduled &&
job.scheduled_in ? ( job.scheduled_in ? (
<Tag> <Tag>
<Link to={`/manage/schedule?date=${moment(job.scheduled_in).format('YYYY-MM-DD')}`}> <DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
</Link>
</Tag> </Tag>
) : null} ) : null}
</Space> </Space>
@@ -133,23 +123,20 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
</DataLabel> </DataLabel>
{job.cccontracts.length > 0 && ( {job.cccontracts.length > 0 && (
<DataLabel label={t("jobs.labels.contracts")}> <DataLabel label={t("jobs.labels.contracts")}>
{job.cccontracts.map((c, index) => ( {job.cccontracts.map((c) => (
<Space wrap> <Link
<Link key={c.id}
key={c.id} to={`/manage/courtesycars/contracts/${c.id}`}
to={`/manage/courtesycars/contracts/${c.id}`} >{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}</Link>
>
{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
{index !== job.cccontracts.length - 1 ? "," : null}
</Link>
</Space>
))} ))}
</DataLabel> </DataLabel>
)} )}
<DataLabel label={t("jobs.fields.production_vars.note")}> <DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} /> <ProductionListColumnProductionNote record={job} />
</DataLabel> </DataLabel>
<Space wrap>
<Space>
{job.special_coverage_policy && ( {job.special_coverage_policy && (
<Tag color="tomato"> <Tag color="tomato">
<Space> <Space>
@@ -309,11 +296,6 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
> >
<div> <div>
<JobEmployeeAssignments job={job} /> <JobEmployeeAssignments job={job} />
<Divider style={{ margin: ".5rem" }} />
<DataLabel label={t("jobs.labels.labor_hrs")}>
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} /{" "}
{(bodyHrs + refinishHrs).toFixed(1)}
</DataLabel>
</div> </div>
</Card> </Card>
</Col> </Col>

View File

@@ -9,12 +9,10 @@ import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils"; import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_JOBS } from "../../graphql/jobs.queries"; import { UPDATE_JOBS } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import client from "../../utils/GraphQLClient"; import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -22,11 +20,6 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
function updateJobCache(items) { function updateJobCache(items) {
client.cache.modify({ client.cache.modify({
id: "ROOT_QUERY", id: "ROOT_QUERY",
@@ -48,7 +41,6 @@ export function JobsExportAllButton({
loadingCallback, loadingCallback,
completedCallback, completedCallback,
refetch, refetch,
insertAuditTrail,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOBS); const [updateJob] = useMutation(UPDATE_JOBS);
@@ -185,13 +177,6 @@ export function JobsExportAllButton({
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
jobUpdateResponse.data.update_jobs.returning.forEach((job) => {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobexported(),
type: "jobexported",
});
});
updateJobCache( updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map( jobUpdateResponse.data.update_jobs.returning.map(
(job) => job.id (job) => job.id
@@ -205,17 +190,13 @@ export function JobsExportAllButton({
}); });
} }
} }
if ( if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
successfulTransactions.length > 0
) {
notification.open({ notification.open({
type: "success", type: "success",
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
const successfulTransactionsSet = [ updateJobCache([
...new Set( ...new Set(
successfulTransactions.map( successfulTransactions.map(
(st) => (st) =>
@@ -226,15 +207,7 @@ export function JobsExportAllButton({
] ]
) )
), ),
]; ]);
if (successfulTransactionsSet.length > 0) {
insertAuditTrail({
jobid: successfulTransactionsSet[0],
operation: AuditTrailMapping.jobexported(),
type: "jobexported",
});
}
updateJobCache(successfulTransactionsSet);
} }
} }
}) })
@@ -252,7 +225,4 @@ export function JobsExportAllButton({
); );
} }
export default connect( export default connect(mapStateToProps, null)(JobsExportAllButton);
mapStateToProps,
mapDispatchToProps
)(JobsExportAllButton);

View File

@@ -1,11 +1,9 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { Button, Checkbox, Divider, Input, Space, Table } from "antd"; import { Checkbox, Divider, Input, Table, Button } from "antd";
import moment from "moment"; import React from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import PhoneFormatter from "../../utils/PhoneFormatter"; import PhoneFormatter from "../../utils/PhoneFormatter";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function JobsFindModalComponent({ export default function JobsFindModalComponent({
@@ -18,13 +16,11 @@ export default function JobsFindModalComponent({
jobsListRefetch, jobsListRefetch,
partsQueueToggle, partsQueueToggle,
setPartsQueueToggle, setPartsQueueToggle,
updateSchComp,
setSchComp,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [modalSearch, setModalSearch] = modalSearchState; const [modalSearch, setModalSearch] = modalSearchState;
const [importOptions, setImportOptions] = importOptionsState; const [importOptions, setImportOptions] = importOptionsState;
const [checkUTT, setCheckUTT] = useState(false);
const columns = [ const columns = [
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
@@ -146,35 +142,6 @@ export default function JobsFindModalComponent({
if (record) { if (record) {
if (record.id) { if (record.id) {
setSelectedJob(record.id); setSelectedJob(record.id);
if (record.actual_in && record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: record.actual_in,
scheduled_completion: record.scheduled_completion,
});
} else {
if (record.actual_in && !record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: record.actual_in,
scheduled_completion: moment(),
});
}
if (!record.actual_in && record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(record.scheduled_completion),
});
}
if (!record.actual_in && !record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(),
});
}
}
return; return;
} }
} }
@@ -210,35 +177,6 @@ export default function JobsFindModalComponent({
rowSelection={{ rowSelection={{
onSelect: (props) => { onSelect: (props) => {
setSelectedJob(props.id); setSelectedJob(props.id);
if (props.actual_in && props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: props.actual_in,
scheduled_completion: props.scheduled_completion,
});
} else {
if (props.actual_in && !props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: props.actual_in,
scheduled_completion: moment(),
});
}
if (!props.actual_in && props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(props.scheduled_completion),
});
}
if (!props.actual_in && !props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(),
});
}
}
}, },
type: "radio", type: "radio",
selectedRowKeys: [selectedJob], selectedRowKeys: [selectedJob],
@@ -252,58 +190,23 @@ export default function JobsFindModalComponent({
}} }}
/> />
<Divider /> <Divider />
<Space> <Checkbox
<Checkbox defaultChecked={importOptions.overrideHeader}
defaultChecked={importOptions.overrideHeader} onChange={(e) =>
onChange={(e) => setImportOptions({
setImportOptions({ ...importOptions,
...importOptions, overrideHeaders: e.target.checked,
overrideHeaders: e.target.checked, })
}) }
} >
> {t("jobs.labels.override_header")}
{t("jobs.labels.override_header")} </Checkbox>
</Checkbox> <Checkbox
<Checkbox
checked={partsQueueToggle} checked={partsQueueToggle}
onChange={(e) => setPartsQueueToggle(e.target.checked)} onChange={(e) => setPartsQueueToggle(e.target.checked)}
> >
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")} {t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
</Checkbox> </Checkbox>
<Checkbox
checked={updateSchComp.checked}
onChange={(e) =>
setSchComp({ ...updateSchComp, checked: e.target.checked })
}
>
{t("jobs.labels.update_scheduled_completion")}
</Checkbox>
{updateSchComp.checked === true ? (
<>
{checkUTT === false ? (
<FormDateTimePickerComponent
value={updateSchComp.scheduled_completion}
onChange={(e) => {
setSchComp({ ...updateSchComp, scheduled_completion: e });
}}
/>
) : null}
<Checkbox
checked={checkUTT}
onChange={(e) => {
setCheckUTT(e.target.checked);
setSchComp({
...updateSchComp,
scheduled_completion: null,
automatic: true,
});
}}
>
{t("jobs.labels.calc_scheuled_completion")}
</Checkbox>
</>
) : null}
</Space>
</div> </div>
); );
} }

View File

@@ -26,8 +26,6 @@ export default connect(
modalSearchState, modalSearchState,
partsQueueToggle, partsQueueToggle,
setPartsQueueToggle, setPartsQueueToggle,
updateSchComp,
setSchComp,
...modalProps ...modalProps
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -97,8 +95,6 @@ export default connect(
modalSearchState={modalSearchState} modalSearchState={modalSearchState}
partsQueueToggle={partsQueueToggle} partsQueueToggle={partsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle} setPartsQueueToggle={setPartsQueueToggle}
updateSchComp={updateSchComp}
setSchComp={setSchComp}
/> />
) : null} ) : null}
</Modal> </Modal>

View File

@@ -21,7 +21,6 @@ import useLocalStorage from "../../utils/useLocalStorage";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -146,8 +145,7 @@ export function JobsList({ bodyshop }) {
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
sorter: (a, b) => sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
@@ -190,8 +188,7 @@ export function JobsList({ bodyshop }) {
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
ellipsis: true, ellipsis: true,
sorter: (a, b) => sorter: (a, b) => alphaSort(a.status, b.status),
statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: filter?.status || null, filteredValue: filter?.status || null,
@@ -222,15 +219,6 @@ export function JobsList({ bodyshop }) {
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link <Link
@@ -278,9 +266,6 @@ export function JobsList({ bodyshop }) {
dataIndex: "ins_co_nm", dataIndex: "ins_co_nm",
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder:
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
filteredValue: filter?.ins_co_nm || null, filteredValue: filter?.ins_co_nm || null,
filters: filters:
(jobs && (jobs &&
@@ -317,13 +302,6 @@ export function JobsList({ bodyshop }) {
key: "estimator", key: "estimator",
ellipsis: true, ellipsis: true,
responsive: ["xl"], responsive: ["xl"],
sorter: (a, b) =>
alphaSort(
`${a.est_ct_fn || ""} ${a.est_ct_ln || ""}`.trim(),
`${b.est_ct_fn || ""} ${b.est_ct_ln || ""}`.trim()
),
sortOrder:
state.sortedInfo.columnKey === "estimator" && state.sortedInfo.order,
filterSearch: true, filterSearch: true,
filteredValue: filter?.estimator || null, filteredValue: filter?.estimator || null,
filters: filters:

View File

@@ -19,8 +19,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobNotesContainer); export default connect(mapStateToProps, mapDispatchToProps)(JobNotesContainer);
@@ -49,7 +49,6 @@ export function JobNotesContainer({ jobId, insertAuditTrail }) {
insertAuditTrail({ insertAuditTrail({
jobid: jobId, jobid: jobId,
operation: AuditTrailMapping.jobnotedeleted(), operation: AuditTrailMapping.jobnotedeleted(),
type: "jobnotedeleted",
}); });
}); });
setDeleteLoading(false); setDeleteLoading(false);

View File

@@ -20,8 +20,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
@@ -76,7 +76,6 @@ export function LaborAllocationsAdjustmentEdit({
values.hours - values.hours -
((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1), ((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1),
}), }),
type: "jobmodifylbradj",
}); });
} }
setLoading(false); setLoading(false);

View File

@@ -19,8 +19,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")), toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function NoteUpsertModalContainer({ export function NoteUpsertModalContainer({
@@ -70,7 +70,6 @@ export function NoteUpsertModalContainer({
insertAuditTrail({ insertAuditTrail({
jobid: context.jobId, jobid: context.jobId,
operation: AuditTrailMapping.jobnoteupdated(), operation: AuditTrailMapping.jobnoteupdated(),
type: "jobnoteupdated",
}); });
}); });
if (refetch) refetch(); if (refetch) refetch();
@@ -103,7 +102,6 @@ export function NoteUpsertModalContainer({
insertAuditTrail({ insertAuditTrail({
jobid: newJobId, jobid: newJobId,
operation: AuditTrailMapping.jobnoteadded(), operation: AuditTrailMapping.jobnoteadded(),
type: "jobnoteadded",
}); });
}); });
} }
@@ -117,7 +115,6 @@ export function NoteUpsertModalContainer({
insertAuditTrail({ insertAuditTrail({
jobid: context.jobId, jobid: context.jobId,
operation: AuditTrailMapping.jobnoteadded(), operation: AuditTrailMapping.jobnoteadded(),
type: "jobnoteadded",
}); });
} }
}; };

View File

@@ -24,7 +24,7 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
const columns = [ const columns = [
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
@@ -44,15 +44,6 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
title: t("jobs.fields.vehicle"), title: t("jobs.fields.vehicle"),
dataIndex: "vehicleid", dataIndex: "vehicleid",
key: "vehicleid", key: "vehicleid",
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicleid" && state.sortedInfo.order,
render: (text, record) => render: (text, record) =>
record.vehicleid ? ( record.vehicleid ? (
<Link to={`/manage/vehicles/${record.vehicleid}`}> <Link to={`/manage/vehicles/${record.vehicleid}`}>
@@ -76,15 +67,9 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters: bodyshop.md_ro_statuses.statuses.map((status) => ({
text: status,
value: status,
})),
onFilter: (value, record) => value.includes(record.status),
}, },
{ {

View File

@@ -1,16 +1,12 @@
import { useApolloClient, useMutation, useQuery } from "@apollo/client"; import { useMutation, useQuery, useApolloClient } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Form, Modal, notification } from "antd"; import { Form, Modal, notification } from "antd";
import axios from "axios";
import _ from "lodash";
import moment from "moment"; import moment from "moment";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent, auth } from "../../firebase/firebase.utils";
import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries"; import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { import {
INSERT_NEW_PARTS_ORDERS, INSERT_NEW_PARTS_ORDERS,
QUERY_PARTS_ORDER_OEC, QUERY_PARTS_ORDER_OEC,
@@ -33,6 +29,10 @@ import { TemplateList } from "../../utils/TemplateConstants";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import PartsOrderModalComponent from "./parts-order-modal.component"; import PartsOrderModalComponent from "./parts-order-modal.component";
import axios from "axios";
import { useTreatments } from "@splitsoftware/splitio-react";
import _ from "lodash";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
@@ -45,8 +45,8 @@ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("partsOrder")), toggleModalVisible: () => dispatch(toggleModalVisible("partsOrder")),
setBillEnterContext: (context) => setBillEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "billEnter" })), dispatch(setModalContext({ context: context, modal: "billEnter" })),
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function PartsOrderModalContainer({ export function PartsOrderModalContainer({
@@ -142,7 +142,6 @@ export function PartsOrderModalContainer({
: AuditTrailMapping.jobspartsorder( : AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number insertResult.data.insert_parts_orders.returning[0].order_number
), ),
type: isReturn ? "jobspartsreturn" : "jobspartsorder",
}); });
const jobLinesResult = await updateJobLines({ const jobLinesResult = await updateJobLines({

View File

@@ -1,77 +0,0 @@
import { useQuery } from "@apollo/client";
import { Card, Divider, Drawer, Grid } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom";
import { QUERY_PARTS_QUEUE_CARD_DETAILS } from "../../graphql/jobs.queries";
import AlertComponent from "../alert/alert.component";
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import PartsQueueJobLinesComponent from "./parts-queue-job-lines.component";
export default function PartsQueueDetailCard() {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = {
xs: "100%",
sm: "100%",
md: "100%",
lg: "75%",
xl: "75%",
xxl: "60%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]
: "100%";
const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams;
const history = useHistory();
const { loading, error, data } = useQuery(QUERY_PARTS_QUEUE_CARD_DETAILS, {
variables: { id: selected },
skip: !selected,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const { t } = useTranslation();
const handleDrawerClose = () => {
delete searchParams.selected;
history.push({
search: queryString.stringify({
...searchParams,
}),
});
};
return (
<Drawer
visible={!!selected}
destroyOnClose
width={drawerPercentage}
placement="right"
onClose={handleDrawerClose}
>
{loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null}
{data ? (
<Card
title={
<Link to={`/manage/jobs/${data.jobs_by_pk.id}`}>
{data.jobs_by_pk.ro_number || t("general.labels.na")}
</Link>
}
>
<JobsDetailHeader job={data ? data.jobs_by_pk : null} />
<Divider type="horizontal" />
<PartsQueueJobLinesComponent
jobLines={data.jobs_by_pk ? data.jobs_by_pk.joblines : null}
/>
</Card>
) : null}
</Drawer>
);
}

View File

@@ -1,208 +0,0 @@
import { Card, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort } from "../../utils/sorters";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({});
export function PartsQueueJobLinesComponent({ jobRO, loading, jobLines }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
});
const { t } = useTranslation();
const columns = [
{
title: "#",
dataIndex: "line_no",
key: "line_no",
sorter: (a, b) => a.line_no - b.line_no,
sortOrder:
state.sortedInfo.columnKey === "line_no" && state.sortedInfo.order,
},
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
onCell: (record) => ({
className: record.manual_line && "job-line-manual",
style: {
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}),
},
}),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
ellipsis: true,
},
{
title: t("joblines.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) =>
`${record.oem_partno || ""} ${
record.alt_partno ? `(${record.alt_partno})` : ""
}`.trim(),
},
{
title: t("joblines.fields.part_type"),
dataIndex: "part_type",
key: "part_type",
filteredValue: state.filteredInfo.part_type || null,
sorter: (a, b) => alphaSort(a.part_type, b.part_type),
sortOrder:
state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order,
filters: [
{
text: t("jobs.labels.partsfilter"),
value: [
"PAN",
"PAC",
"PAR",
"PAL",
"PAA",
"PAM",
"PAP",
"PAS",
"PASL",
"PAG",
],
},
{
text: t("joblines.fields.part_types.PAN"),
value: ["PAN"],
},
{
text: t("joblines.fields.part_types.PAP"),
value: ["PAP"],
},
{
text: t("joblines.fields.part_types.PAL"),
value: ["PAL"],
},
{
text: t("joblines.fields.part_types.PAA"),
value: ["PAA"],
},
{
text: t("joblines.fields.part_types.PAG"),
value: ["PAG"],
},
{
text: t("joblines.fields.part_types.PAS"),
value: ["PAS"],
},
{
text: t("joblines.fields.part_types.PASL"),
value: ["PASL"],
},
{
text: t("joblines.fields.part_types.PAC"),
value: ["PAC"],
},
{
text: t("joblines.fields.part_types.PAR"),
value: ["PAR"],
},
{
text: t("joblines.fields.part_types.PAM"),
value: ["PAM"],
},
],
onFilter: (value, record) => value.includes(record.part_type),
render: (text, record) =>
record.part_type
? t(`joblines.fields.part_types.${record.part_type}`)
: null,
},
{
title: t("joblines.fields.part_qty"),
dataIndex: "part_qty",
key: "part_qty",
},
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
sorter: (a, b) => a.act_price - b.act_price,
sortOrder:
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) => (
<CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511"
? record.prt_dsmk_m
: record.act_price}
</CurrencyFormatter>
),
},
{
title: t("joblines.fields.location"),
dataIndex: "location",
key: "location",
},
{
title: t("joblines.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: state.filteredInfo.status || null,
filters:
(jobLines &&
jobLines
.map((l) => l.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.status),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState((state) => ({
...state,
filteredInfo: filters,
sortedInfo: sorter,
}));
};
return (
<Card title={t("jobs.labels.parts_lines")}>
<Table
columns={columns}
rowKey="id"
loading={loading}
pagination={false}
dataSource={jobLines}
onChange={handleTableChange}
/>
</Card>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(PartsQueueJobLinesComponent);

View File

@@ -59,8 +59,8 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
await insertPayment({ await insertPayment({
variables: { variables: {
paymentInput: { paymentInput: {
amount: -refund_response?.data?.amount, amount: -refund_response.data.amount,
transactionid: payment_response?.response?.receiptelements?.transid, transactionid: payment_response.response.receiptelements.transid,
payer: record.payer, payer: record.payer,
type: "Refund", type: "Refund",
jobid: payment_response.jobid, jobid: payment_response.jobid,

View File

@@ -1,12 +1,6 @@
import { Button, Form, Input, PageHeader, Space } from "antd"; import { Button, Form, Input, PageHeader, Space } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectAuthLevel,
selectBodyshop,
} from "../../redux/user/user.selectors";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, { import PhoneFormItem, {
@@ -14,6 +8,12 @@ import PhoneFormItem, {
} from "../form-items-formatted/phone-form-item.component"; } from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component"; import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectAuthLevel,
selectBodyshop,
} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
authLevel: selectAuthLevel, authLevel: selectAuthLevel,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -46,19 +46,13 @@ export function PhonebookFormComponent({
return ( return (
<div> <div>
<PageHeader <PageHeader
title={ title={`${form.getFieldValue("firstname") || ""} ${
<Form.Item shouldUpdate> form.getFieldValue("lastname") || ""
{() => }${
`${form.getFieldValue("firstname") || ""} ${ form.getFieldValue("company")
form.getFieldValue("lastname") || "" ? ` - ${form.getFieldValue("company")}`
}${ : ""
form.getFieldValue("company") }`}
? ` - ${form.getFieldValue("company")}`
: ""
}`
}
</Form.Item>
}
extra={ extra={
<Space> <Space>
<Button <Button

View File

@@ -29,8 +29,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function ProductionBoardKanbanComponent({ export function ProductionBoardKanbanComponent({
@@ -133,7 +133,6 @@ export function ProductionBoardKanbanComponent({
insertAuditTrail({ insertAuditTrail({
jobid: card.id, jobid: card.id,
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId), operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
type: "jobstatuschange",
}); });
if (update.errors) { if (update.errors) {

View File

@@ -13,8 +13,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({}); const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function ProductionListColumnAlert({ record, insertAuditTrail }) { export function ProductionListColumnAlert({ record, insertAuditTrail }) {
@@ -46,7 +46,6 @@ export function ProductionListColumnAlert({ record, insertAuditTrail }) {
? !record.production_vars.alert ? !record.production_vars.alert
: true : true
), ),
type: "alertToggle",
}).then(() => { }).then(() => {
if (record.refetch) record.refetch(); if (record.refetch) record.refetch();
}); });

View File

@@ -1,6 +1,7 @@
import { BranchesOutlined, PauseCircleOutlined } from "@ant-design/icons"; import { BranchesOutlined, PauseCircleOutlined } from "@ant-design/icons";
import { Checkbox, Space, Tooltip } from "antd"; import { Space, Tooltip } from "antd";
import i18n from "i18next"; import i18n from "i18next";
import moment from "moment";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { TimeFormatter } from "../../utils/DateFormatter"; import { TimeFormatter } from "../../utils/DateFormatter";
@@ -9,9 +10,7 @@ import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort, dateSort, statusSort } from "../../utils/sorters"; import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
import JobAltTransportChange from "../job-at-change/job-at-change.component"; import JobAltTransportChange from "../job-at-change/job-at-change.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component"; import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import OwnerNameDisplay, { import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component"; import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
import ProductionListColumnAlert from "./production-list-columns.alert.component"; import ProductionListColumnAlert from "./production-list-columns.alert.component";
import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component"; import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component";
@@ -85,8 +84,7 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record} />
</Link> </Link>
), ),
sorter: (a, b) => sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order, state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
}, },
@@ -97,10 +95,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
ellipsis: true, ellipsis: true,
sorter: (a, b) => sorter: (a, b) =>
alphaSort( alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${ a.v_make_desc + a.v_model_desc,
a.v_model_desc || "" b.v_make_desc + b.v_model_desc
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
), ),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
@@ -189,12 +185,17 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
state.sortedInfo.columnKey === "date_next_contact" && state.sortedInfo.columnKey === "date_next_contact" &&
state.sortedInfo.order, state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<ProductionListDate <span
record={record} style={{
field="date_next_contact" color:
pastIndicator record.date_next_contact &&
time moment(record.date_next_contact).isBefore(moment())
/> ? "red"
: "",
}}
>
<ProductionListDate record={record} field="date_next_contact" time />
</span>
), ),
}, },
{ {
@@ -290,20 +291,6 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
dataIndex: "special_coverage_policy", dataIndex: "special_coverage_policy",
key: "special_coverage_policy", key: "special_coverage_policy",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
Number(a.special_coverage_policy) - Number(b.special_coverage_policy),
sortOrder:
state.sortedInfo.columnKey === "special_coverage_policy" &&
state.sortedInfo.order,
filters: [
{ text: "True", value: true },
{ text: "False", value: false },
],
onFilter: (value, record) =>
value.includes(record.special_coverage_policy),
render: (text, record) => (
<Checkbox disabled checked={record.special_coverage_policy} />
),
}, },
{ {
@@ -315,16 +302,6 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
sortOrder: sortOrder:
state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order, state.sortedInfo.order,
filters:
(bodyshop &&
bodyshop.appt_alt_transport.map((s) => {
return {
text: s,
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
render: (text, record) => ( render: (text, record) => (
<div> <div>
{record.alt_transport} {record.alt_transport}
@@ -405,11 +382,7 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
title: i18n.t("production.labels.alert"), title: i18n.t("production.labels.alert"),
dataIndex: "alert", dataIndex: "alert",
key: "alert", key: "alert",
sorter: (a, b) =>
Number(a.production_vars?.alert || false) -
Number(b.production_vars?.alert || false),
sortOrder:
state.sortedInfo.columnKey === "alert" && state.sortedInfo.order,
render: (text, record) => <ProductionListColumnAlert record={record} />, render: (text, record) => <ProductionListColumnAlert record={record} />,
}, },
{ {

View File

@@ -3,12 +3,12 @@ import { useMutation } from "@apollo/client";
import { import {
Button, Button,
Col, Col,
notification,
Popover, Popover,
Row, Row,
Select, Select,
Space, Space,
Spin, Spin,
notification,
} from "antd"; } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -25,8 +25,8 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function ProductionListEmpAssignment({ export function ProductionListEmpAssignment({
@@ -55,7 +55,6 @@ export function ProductionListEmpAssignment({
insertAuditTrail({ insertAuditTrail({
jobid: record.id, jobid: record.id,
operation: AuditTrailMapping.jobassignmentchange(empAssignment, name), operation: AuditTrailMapping.jobassignmentchange(empAssignment, name),
type: "jobassignmentchange",
}); });
if (!!result.errors) { if (!!result.errors) {
@@ -81,7 +80,6 @@ export function ProductionListEmpAssignment({
insertAuditTrail({ insertAuditTrail({
jobid: record.id, jobid: record.id,
operation: AuditTrailMapping.jobassignmentremoved(empAssignment), operation: AuditTrailMapping.jobassignmentremoved(empAssignment),
type: "jobassignmentremoved",
}); });
if (!!result.errors) { if (!!result.errors) {

View File

@@ -12,8 +12,8 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function ProductionListColumnCategory({ record, bodyshop }) { export function ProductionListColumnCategory({ record, bodyshop }) {
const [updateJob] = useMutation(UPDATE_JOB); const [updateJob] = useMutation(UPDATE_JOB);

View File

@@ -5,16 +5,16 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function ProductionListColumnStatus({ export function ProductionListColumnStatus({
record, record,
@@ -40,7 +40,6 @@ export function ProductionListColumnStatus({
insertAuditTrail({ insertAuditTrail({
jobid: record.id, jobid: record.id,
operation: AuditTrailMapping.jobstatuschange(key), operation: AuditTrailMapping.jobstatuschange(key),
type: "jobstatuschange",
}); });
setLoading(false); setLoading(false);

View File

@@ -1,432 +0,0 @@
import {Button, Card, Checkbox, Col, Form, Input, InputNumber, Row, Select} from "antd";
import React, {useCallback, useEffect, useMemo, useState} from "react";
import {fetchFilterData} from "../../utils/RenderTemplate";
import {DeleteFilled} from "@ant-design/icons";
import {useTranslation} from "react-i18next";
import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import {generateInternalReflections} from "./report-center-modal-utils";
import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx";
export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) {
return (
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
return <RenderFilters form={form} templateId={key} bodyshop={bodyshop}/>;
}}
</Form.Item>
);
}
/**
* Filters Section
* @param filters
* @param form
* @param bodyshop
* @returns {JSX.Element}
* @constructor
*/
function FiltersSection({filters, form, bodyshop}) {
const {t} = useTranslation();
return (
<Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}>
<Form.List name={["filters"]}>
{(fields, {add, remove}) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={10}>
<Form.Item
key={`${index}field`}
label={t('reportcenter.labels.advanced_filters_filter_field')}
name={[field.name, "field"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
getPopupContainer={trigger => trigger.parentNode}
onChange={() => {
// Clear related Fields
form.setFieldValue(['filters', field.name, 'value'], null);
form.setFieldValue(['filters', field.name, 'operator'], null);
}}
options={
filters.map((f) => {
return {
value: f.name,
label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
}
})
}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
dependencies={[['filters', field.name, "field"],['filters', field.name, "value"]]}
>
{
() => {
const name = form.getFieldValue(['filters', field.name, "field"]);
const type = filters.find(f => f.name === name)?.type;
return <Form.Item
key={`${index}operator`}
label={t('reportcenter.labels.advanced_filters_filter_operator')}
name={[field.name, "operator"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
getPopupContainer={trigger => trigger.parentNode}
options={ getWhereOperatorsByType(type)}
onChange={() => {
// Clear related Fields
form.setFieldValue(['filters', field.name, 'value'], undefined);
}}
/>
</Form.Item>
}
}
</Form.Item>
</Col>
<Col span={6}>
<Form.Item dependencies={[
['filters', field.name, "field"],
['filters', field.name, "operator"]
]}
>
{
() => {
// Because it looks cleaner than inlining.
const name = form.getFieldValue(['filters', field.name, "field"]);
const type = filters.find(f => f.name === name)?.type;
const reflector = filters.find(f => f.name === name)?.reflector;
const operator = form.getFieldValue(['filters', field.name, "operator"]);
const operatorType = operator ? getWhereOperatorsByType(type).find((o) => o.value === operator)?.type : null;
return <Form.Item
key={`${index}value`}
label={t('reportcenter.labels.advanced_filters_filter_value')}
name={[field.name, "value"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
{
(() => {
const generateReflections = (reflector) => {
if (!reflector) return [];
const {name} = reflector;
const path = name?.split('.');
const upperPath = path?.[0];
const finalPath = path?.slice(1).join('.');
return generateInternalReflections({
bodyshop,
upperPath,
finalPath,
t
});
};
const reflections = reflector ? generateReflections(reflector) : [];
const fieldPath = [[field.name, "value"]];
// We have reflections so we will use a select box
if (reflections.length > 0) {
// We have reflections and the operator type is array, so we will use a select box with multiple options
if (operatorType === "array") {
return (
<Select
disabled={!operator}
mode="multiple"
options={reflections}
getPopupContainer={trigger => trigger.parentNode}
onChange={(value) => {
form.setFieldValue(fieldPath, value);
}}
/>
);
}
return (
<Select
options={reflections}
getPopupContainer={trigger => trigger.parentNode}
onChange={(value) => {
form.setFieldValue(fieldPath, value);
}}
/>
);
}
// We have a type of number, so we will use a number input
if (type === "number") {
return (
<InputNumber
disabled={!operator}
onChange={(value) => form.setFieldValue(fieldPath, value)}/>
);
}
// We have a type of date, so we will use a date picker
if (type === "date") {
return (
<FormDatePicker
disabled={!operator}
onChange={(date) => form.setFieldValue(fieldPath, date)}
/>
);
}
// we have a type of boolean, so we will use a select box with a true or false option.
if (type === "boolean" || type === "bool") {
return (
<Select
disabled={!operator}
getPopupContainer={trigger => trigger.parentNode}
options={[
{
label: t('reportcenter.labels.advanced_filters_true'),
value: true
},
{
label: t('reportcenter.labels.advanced_filters_false'),
value: false
}
]}
onChange={(value) => form.setFieldValue(fieldPath, value)}
/>
);
}
return (
<Input
disabled={!operator}
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}
/>
);
})()
}
</Form.Item>
}
}
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{margin: "1rem", paddingTop: '23px'}}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{width: "100%"}}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</Card>
);
}
/**
* Sorters Section
* @param sorters
* @param form
* @returns {JSX.Element}
* @constructor
*/
function SortersSection({sorters}) {
const {t} = useTranslation();
return (
<Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}>
<Form.List name={["sorters"]}>
{(fields, {add, remove}) => {
return (
<div>
Sorters
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={11}>
<Form.Item
key={`${index}field`}
label={t('reportcenter.labels.advanced_filters_sorter_field')}
name={[field.name, "field"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={
sorters.map((f) => ({
value: f.name,
label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
}))
}
getPopupContainer={trigger => trigger.parentNode}
/>
</Form.Item>
</Col>
<Col span={11}>
<Form.Item
key={`${index}direction`}
label={t('reportcenter.labels.advanced_filters_sorter_direction')}
name={[field.name, "direction"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={getOrderOperatorsByType()}
getPopupContainer={trigger => trigger.parentNode}
/>
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{margin: "1rem", paddingTop: '23px'}}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{width: "100%"}}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</Card>
);
}
/**
* Render Filters
* @param templateId
* @param form
* @param bodyshop
* @returns {JSX.Element|null}
* @constructor
*/
function RenderFilters({templateId, form, bodyshop}) {
const [state, setState] = useState(null);
const [visible, setVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation();
const fetch = useCallback(async () => {
// Reset all the filters and Sorters.
form.resetFields(['filters']);
form.resetFields(['sorters']);
form.resetFields(['defaultSorters']);
setIsLoading(true);
const data = await fetchFilterData({name: templateId});
// We have Success
if (data?.success) {
if (data?.data?.sorters && data?.data?.sorters.length > 0) {
const defaultSorters = data?.data?.sorters.filter((sorter) => sorter.hasOwnProperty('default')).map((sorter) => {
return {
field: sorter.name,
direction: sorter.default.direction
};
}).sort((a, b) => a.default.order - b.default.order);
form.setFieldValue('defaultSorters', JSON.stringify(defaultSorters));
}
// Set the state
setState(data.data);
}
// Something went wrong fetching filter data
else {
setState(null);
}
setIsLoading(false);
}, [templateId, form]);
useEffect(() => {
if (templateId) {
fetch();
}
}, [templateId, fetch]);
const filters = useMemo(() => state?.filters || [], [state]);
const sorters = useMemo(() => state?.sorters || [], [state]);
if (!templateId) return null;
if (isLoading) return <LoadingSkeleton/>;
if (!state) return null;
return (
<div style={{marginTop: '10px'}}>
<Checkbox
checked={visible}
onChange={(e) => setVisible(e.target.checked)}
children={t('reportcenter.labels.advanced_filters')}
/>
{visible && (
<div>
{filters.length > 0 && (
<FiltersSection filters={filters} form={form} bodyshop={bodyshop}/>
)}
{sorters.length > 0 && (
<SortersSection sorters={sorters} form={form}/>
)}
</div>
)}
</div>
);
}

View File

@@ -1,161 +0,0 @@
import {uniqBy} from "lodash";
/**
* Get value from path
* @param obj
* @param path
* @returns {*}
*/
const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj);
/**
* Generate options from array
* @param bodyshop
* @param path
* @returns {unknown[]}
*/
const generateOptionsFromArray = (bodyshop, path) => {
const options = getValueFromPath(bodyshop, path);
return uniqBy(options.map((value) => ({
label: value,
value: value,
})), 'value');
}
/**
* Valid internal reflections
* Note: This is intended for future functionality
* @type {{special: string[], bodyshop: [{name: string, type: string}]}}
*/
const VALID_INTERNAL_REFLECTIONS = {
bodyshop: [
{
name: 'md_ro_statuses.statuses',
type: 'kv-to-v'
}
],
};
/**
* Generate options
* @param bodyshop
* @param path
* @param labelPath
* @param valuePath
* @returns {{label: *, value: *}[]}
*/
const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => {
const options = getValueFromPath(bodyshop, path);
return uniqBy(Object.values(options).map((value) => ({
label: value[labelPath],
value: value[valuePath],
})), 'value');
}
/**
* Generate special reflections
* @param bodyshop
* @param finalPath
* @param t - i18n
* @returns {{label: *, value: *}[]|{label: *, value: *}[]|{label: string, value: *}[]|*[]}
*/
const generateSpecialReflections = (bodyshop, finalPath, t) => {
switch (finalPath) {
case 'payment_payers':
return [
{
label: t("payments.labels.customer"),
value: t("payments.labels.customer"),
},
{
label: t("payments.labels.insurance"),
value: t("payments.labels.insurance"),
},
// This is a weird one supposedly only used by one shop and could potentially be
// placed behind a SplitSDK
{
label: t("payments.labels.external"),
value: t("payments.labels.external"),
}
];
case 'payment_types':
return generateOptionsFromArray(bodyshop, 'md_payment_types');
case 'alt_transports':
return generateOptionsFromArray(bodyshop, 'appt_alt_transport');
case 'lost_sale_reasons':
return generateOptionsFromArray(bodyshop, 'md_lost_sale_reasons');
// Special case because Referral Sources is an Array, not an Object.
case 'referral_source':
return generateOptionsFromArray(bodyshop, 'md_referral_sources');
case 'class':
return generateOptionsFromArray(bodyshop, 'md_classes');
case 'cost_centers':
return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name');
// Special case because Categories is an Array, not an Object.
case 'categories':
return generateOptionsFromArray(bodyshop, 'md_categories');
case 'insurance_companies':
return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name');
case 'employee_teams':
return generateOptionsFromObject(bodyshop, 'employee_teams', 'name', 'id');
// Special case because Employees uses a concatenation of first_name and last_name
case 'employees':
const employeesOptions = getValueFromPath(bodyshop, 'employees');
return uniqBy(Object.values(employeesOptions).map((value) => ({
label: `${value.first_name} ${value.last_name}`,
value: value.id,
})), 'value');
case 'last_names':
return generateOptionsFromObject(bodyshop, 'employees', 'last_name', 'last_name');
case 'first_names':
return generateOptionsFromObject(bodyshop, 'employees', 'first_name', 'first_name');
case 'job_statuses':
const statusOptions = getValueFromPath(bodyshop, 'md_ro_statuses.statuses');
return Object.values(statusOptions).map((value) => ({
label: value,
value
}));
default:
console.error('Invalid Special reflection provided by Report Filters');
return [];
}
}
/**
* Generate bodyshop reflections
* @param bodyshop
* @param finalPath
* @returns {{label: *, value: *}[]|*[]}
*/
const generateBodyshopReflections = (bodyshop, finalPath) => {
const options = getValueFromPath(bodyshop, finalPath);
const reflectionRenderer = VALID_INTERNAL_REFLECTIONS.bodyshop.find(reflection => reflection.name === finalPath);
if (reflectionRenderer?.type === 'kv-to-v') {
return Object.values(options).map((value) => ({
label: value,
value
}));
}
return [];
}
/**
* Generate internal reflections based on the path and bodyshop
* @param bodyshop
* @param upperPath
* @param finalPath
* @param t - i18n
* @returns {{label: *, value: *}[]|[]|{label: *, value: *}[]|{label: string, value: *}[]|{label: *, value: *}[]|*[]}
*/
const generateInternalReflections = ({bodyshop, upperPath, finalPath, t}) => {
switch (upperPath) {
case 'special':
return generateSpecialReflections(bodyshop, finalPath, t);
case 'bodyshop':
return generateBodyshopReflections(bodyshop, finalPath);
default:
return [];
}
};
export {generateInternalReflections}

View File

@@ -1,64 +1,70 @@
import {useLazyQuery} from "@apollo/client"; import { useLazyQuery } from "@apollo/client";
import {Button, Card, Col, DatePicker, Form, Input, Radio, Row, Typography,} from "antd"; import {
Button,
Card,
Col,
DatePicker,
Form,
Input,
Radio,
Row,
Typography,
} from "antd";
import _ from "lodash"; import _ from "lodash";
import moment from "moment"; import moment from "moment";
import React, {useState} from "react"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import {QUERY_ACTIVE_EMPLOYEES} from "../../graphql/employees.queries"; import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries";
import {QUERY_ALL_VENDORS} from "../../graphql/vendors.queries"; import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import {selectReportCenter} from "../../redux/modals/modals.selectors"; import { selectReportCenter } from "../../redux/modals/modals.selectors";
import DatePickerRanges from "../../utils/DatePickerRanges"; import DatePIckerRanges from "../../utils/DatePickerRanges";
import {GenerateDocument} from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import {TemplateList} from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component"; import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import "./report-center-modal.styles.scss"; import "./report-center-modal.styles.scss";
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
import {selectBodyshop} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
reportCenterModal: selectReportCenter, reportCenterModal: selectReportCenter,
bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(ReportCenterModalComponent); )(ReportCenterModalComponent);
export function ReportCenterModalComponent({reportCenterModal, bodyshop}) { export function ReportCenterModalComponent({ reportCenterModal }) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const {t} = useTranslation(); const { t } = useTranslation();
const Templates = TemplateList("report_center"); const Templates = TemplateList("report_center");
const ReportsList = Object.keys(Templates).map((key) => { const ReportsList = Object.keys(Templates).map((key) => {
return Templates[key]; return Templates[key];
}); });
const {open} = reportCenterModal; const { visible } = reportCenterModal;
const [callVendorQuery, {data: vendorData, called: vendorCalled}] = const [callVendorQuery, { data: vendorData, called: vendorCalled }] =
useLazyQuery(QUERY_ALL_VENDORS, { useLazyQuery(QUERY_ALL_VENDORS, {
skip: !( skip: !(
open && visible &&
Templates[form.getFieldValue("key")] && Templates[form.getFieldValue("key")] &&
Templates[form.getFieldValue("key")].idtype Templates[form.getFieldValue("key")].idtype
), ),
}); });
const [callEmployeeQuery, {data: employeeData, called: employeeCalled}] = const [callEmployeeQuery, { data: employeeData, called: employeeCalled }] =
useLazyQuery(QUERY_ACTIVE_EMPLOYEES, { useLazyQuery(QUERY_ACTIVE_EMPLOYEES, {
skip: !( skip: !(
open && visible &&
Templates[form.getFieldValue("key")] && Templates[form.getFieldValue("key")] &&
Templates[form.getFieldValue("key")].idtype Templates[form.getFieldValue("key")].idtype
), ),
}); });
const handleFinish = async (values) => { const handleFinish = async (values) => {
setLoading(true); setLoading(true);
@@ -66,256 +72,244 @@ export function ReportCenterModalComponent({reportCenterModal, bodyshop}) {
const end = values.dates ? values.dates[1] : null; const end = values.dates ? values.dates[1] : null;
const { id } = values; const { id } = values;
const templateConfig = { await GenerateDocument(
name: values.key, {
variables: { name: values.key,
...(start variables: {
? {start: moment(start).startOf("day").format("YYYY-MM-DD")} ...(start
: {}), ? { start: moment(start).startOf("day").format("YYYY-MM-DD") }
...(end ? {end: moment(end).endOf("day").format("YYYY-MM-DD")} : {}), : {}),
...(start ? {starttz: moment(start).startOf("day")} : {}), ...(end
...(end ? {endtz: moment(end).endOf("day")} : {}), ? { end: moment(end).endOf("day").format("YYYY-MM-DD") }
: {}),
...(start ? { starttz: moment(start).startOf("day") } : {}),
...(end ? { endtz: moment(end).endOf("day") } : {}),
...(id ? {id: id} : {}), ...(id ? { id: id } : {}),
},
filters: values.filters,
sorters: values.sorters,
};
if (_.isString(values.defaultSorters) && !_.isEmpty(values.defaultSorters)) {
templateConfig.defaultSorters = JSON.parse(values.defaultSorters);
}
await GenerateDocument(
templateConfig,
{
to: values.to,
subject: Templates[values.key]?.subject,
}, },
values.sendbyexcel === "excel" },
? "x" {
: values.sendby === "email" to: values.to,
? "e" subject: Templates[values.key]?.subject,
: "p", },
id values.sendbyexcel === "excel"
? "x"
: values.sendby === "email"
? "e"
: "p",
id
); );
setLoading(false); setLoading(false);
}; };
const FilteredReportsList = const FilteredReportsList =
search !== "" search !== ""
? ReportsList.filter((r) => ? ReportsList.filter((r) =>
r.title.toLowerCase().includes(search.toLowerCase()) r.title.toLowerCase().includes(search.toLowerCase())
) )
: ReportsList; : ReportsList;
//Group it, create cards, and then filter out. //Group it, create cards, and then filter out.
const grouped = _.groupBy(FilteredReportsList, "group"); const grouped = _.groupBy(FilteredReportsList, "group");
return ( return (
<div> <div>
<Form <Form
onFinish={handleFinish} onFinish={handleFinish}
autoComplete={"off"} autoComplete={"off"}
layout="vertical" layout="vertical"
form={form} form={form}
>
<Input.Search
onChange={(e) => setSearch(e.target.value)}
value={search}
/>
<Form.Item
name="key"
label={t("reportcenter.labels.key")}
// className="radio-group-columns"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
> >
<Input.Search <Radio.Group>
onChange={(e) => setSearch(e.target.value)} {/* {Object.keys(Templates).map((key) => (
value={search}
/>
<Form.Item name="defaultSorters" hidden/>
<Form.Item
name="key"
label={t("reportcenter.labels.key")}
// className="radio-group-columns"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Radio.Group>
{/* {Object.keys(Templates).map((key) => (
<Radio key={key} value={key}> <Radio key={key} value={key}>
{Templates[key].title} {Templates[key].title}
</Radio> </Radio>
))} */} ))} */}
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
{Object.keys(grouped).map((key) => ( {Object.keys(grouped).map((key) => (
<Col md={8} sm={12} key={key}> <Col md={8} sm={12} key={key}>
<Card.Grid <Card.Grid
style={{ style={{
width: "100%", width: "100%",
height: "100%", height: "100%",
maxHeight: "33vh", maxHeight: "33vh",
overflowY: "scroll", overflowY: "scroll",
}} }}
> >
<Typography.Title level={4}> <Typography.Title level={4}>
{t(`reportcenter.labels.groups.${key}`)} {t(`reportcenter.labels.groups.${key}`)}
</Typography.Title> </Typography.Title>
<ul style={{ listStyleType: 'none', columns: "2 auto"}}> <ul style={{ columns: "2 auto" }}>
{grouped[key].map((item) => ( {grouped[key].map((item) => (
<li key={item.key}> <li key={item.key}>
<Radio key={item.key} value={item.key}> <Radio key={item.key} value={item.key}>
{item.title} {item.title}
</Radio> </Radio>
</li> </li>
))} ))}
</ul> </ul>
</Card.Grid> </Card.Grid>
</Col> </Col>
))} ))}
</Row> </Row>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}> <Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
{() => { {() => {
const key = form.getFieldValue("key"); const key = form.getFieldValue("key");
if (!key) return null; if (!key) return null;
//Kind of Id //Kind of Id
const rangeFilter = Templates[key] && Templates[key].rangeFilter; const rangeFilter = Templates[key] && Templates[key].rangeFilter;
if (!rangeFilter) return null; if (!rangeFilter) return null;
return (
<div>
{t("reportcenter.labels.filterson", {
object: rangeFilter.object,
field: rangeFilter.field,
})}
</div>
);
}}
</Form.Item>
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
const currentId = form.getFieldValue("id");
if (!key) return null;
//Kind of Id
const idtype = Templates[key] && Templates[key].idtype;
if (!idtype && currentId) {
form.setFieldsValue({ id: null });
return null;
}
if (!vendorCalled && idtype === "vendor") callVendorQuery();
if (!employeeCalled && idtype === "employee") callEmployeeQuery();
if (idtype === "vendor")
return ( return (
<div> <Form.Item
{t("reportcenter.labels.filterson", { name="id"
object: rangeFilter.object, label={t("reportcenter.labels.vendor")}
field: rangeFilter.field, rules={[
})} {
</div> required: true,
//message: t("general.validation.required"),
},
]}
>
<VendorSearchSelect
options={vendorData ? vendorData.vendors : []}
/>
</Form.Item>
); );
}} if (idtype === "employee")
</Form.Item> return (
<ReportCenterModalFiltersSortersComponent form={form} bodyshop={bodyshop} /> <Form.Item
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}> name="id"
{() => { label={t("reportcenter.labels.employee")}
const key = form.getFieldValue("key"); rules={[
const currentId = form.getFieldValue("id"); {
if (!key) return null; required: true,
//Kind of Id //message: t("general.validation.required"),
const idtype = Templates[key] && Templates[key].idtype; },
if (!idtype && currentId) { ]}
form.setFieldsValue({id: null}); >
return null; <EmployeeSearchSelect
} options={employeeData ? employeeData.employees : []}
if (!vendorCalled && idtype === "vendor") callVendorQuery(); />
if (!employeeCalled && idtype === "employee") callEmployeeQuery(); </Form.Item>
if (idtype === "vendor") );
return ( else return null;
<Form.Item }}
name="id" </Form.Item>
label={t("reportcenter.labels.vendor")} <Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
rules={[ {() => {
{ const key = form.getFieldValue("key");
required: true, const datedisable = Templates[key] && Templates[key].datedisable;
//message: t("general.validation.required"), if (datedisable !== true) {
}, return (
]} <Form.Item
> name="dates"
<VendorSearchSelect label={t("reportcenter.labels.dates")}
options={vendorData ? vendorData.vendors : []} rules={[
/> {
</Form.Item> required: true,
); //message: t("general.validation.required"),
if (idtype === "employee") },
return ( ]}
<Form.Item >
name="id" <DatePicker.RangePicker
label={t("reportcenter.labels.employee")} format="MM/DD/YYYY"
rules={[ ranges={DatePIckerRanges}
{ />
required: true, </Form.Item>
//message: t("general.validation.required"), );
}, } else return null;
]} }}
> </Form.Item>
<EmployeeSearchSelect <Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
options={employeeData ? employeeData.employees : []} {() => {
/> const key = form.getFieldValue("key");
</Form.Item> //Kind of Id
); const reporttype = Templates[key] && Templates[key].reporttype;
else return null;
}}
</Form.Item>
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
const datedisable = Templates[key] && Templates[key].datedisable;
// TODO: MERGE NOTE, Ranges turns to presets in DatePicker.RangePicker if (reporttype === "excel")
return (
<Form.Item
label={t("general.labels.sendby")}
name="sendbyexcel"
initialValue="excel"
>
<Radio.Group>
<Radio value="excel">{t("general.labels.excel")}</Radio>
</Radio.Group>
</Form.Item>
);
if (reporttype !== "excel")
return (
<Form.Item
label={t("general.labels.sendby")}
name="sendby"
initialValue="print"
>
<Radio.Group>
<Radio value="email">{t("general.labels.email")}</Radio>
<Radio value="print">{t("general.labels.print")}</Radio>
</Radio.Group>
</Form.Item>
);
}}
</Form.Item>
if (datedisable !== true) { <div
return ( style={{
<Form.Item display: "flex",
name="dates" justifyContent: "center",
label={t("reportcenter.labels.dates")} marginTop: "1rem",
rules={[ }}
{ >
required: true, <Button onClick={() => form.submit()} style={{}} loading={loading}>
//message: t("general.validation.required"), {t("reportcenter.actions.generate")}
}, </Button>
]} </div>
> </Form>
<DatePicker.RangePicker </div>
format="MM/DD/YYYY"
ranges={DatePickerRanges}
/>
</Form.Item>
);
} else return null;
}}
</Form.Item>
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
//Kind of Id
const reporttype = Templates[key] && Templates[key].reporttype;
if (reporttype === "excel")
return (
<Form.Item
label={t("general.labels.sendby")}
name="sendbyexcel"
initialValue="excel"
>
<Radio.Group>
<Radio value="excel">{t("general.labels.excel")}</Radio>
</Radio.Group>
</Form.Item>
);
if (reporttype !== "excel")
return (
<Form.Item
label={t("general.labels.sendby")}
name="sendby"
initialValue="print"
>
<Radio.Group>
<Radio value="email">{t("general.labels.email")}</Radio>
<Radio value="print">{t("general.labels.print")}</Radio>
</Radio.Group>
</Form.Item>
);
}}
</Form.Item>
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: "1rem",
}}
>
<Button onClick={() => form.submit()} style={{}} loading={loading}>
{t("reportcenter.actions.generate")}
</Button>
</div>
</Form>
</div>
); );
} }

View File

@@ -34,8 +34,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")), toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
setEmailOptions: (e) => dispatch(setEmailOptions(e)), setEmailOptions: (e) => dispatch(setEmailOptions(e)),
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function ScheduleJobModalContainer({ export function ScheduleJobModalContainer({
@@ -146,7 +146,6 @@ export function ScheduleJobModalContainer({
operation: AuditTrailMapping.appointmentinsert( operation: AuditTrailMapping.appointmentinsert(
DateTimeFormat(values.start) DateTimeFormat(values.start)
), ),
type: "appointmentinsert",
}); });
} }

View File

@@ -25,8 +25,6 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) {
return acc + value.bodyhrs; return acc + value.bodyhrs;
}, 0); }, 0);
const numJobs = entries.length;
return ( return (
<Card <Card
title={moment(date).format("D - ddd")} title={moment(date).format("D - ddd")}
@@ -35,18 +33,17 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) {
> >
<Statistic <Statistic
valueStyle={{ color: dailyBodyTarget > bodyHrs ? "red" : "green" }} valueStyle={{ color: dailyBodyTarget > bodyHrs ? "red" : "green" }}
label="Body" label="B"
value={bodyHrs.toFixed(1)} value={bodyHrs.toFixed(1)}
/> />
<Statistic <Statistic
valueStyle={{ color: dailyPaintTarget > paintHrs ? "red" : "green" }} valueStyle={{ color: dailyPaintTarget > paintHrs ? "red" : "green" }}
label="Refinish" label="P"
value={paintHrs.toFixed(1)} value={paintHrs.toFixed(1)}
/> />
<Divider style={{ margin: 0 }} /> <Divider style={{ margin: 0 }} />
<Statistic label="Total" value={(bodyHrs + paintHrs).toFixed(1)} />
<Divider style={{ margin: 0 }} /> <Statistic value={(bodyHrs + paintHrs).toFixed(1)} />
<Statistic label="Jobs" value={numJobs} />
</Card> </Card>
); );
} }

View File

@@ -1,14 +1,5 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { import { Button, Card, Dropdown, Form, InputNumber, notification } from "antd";
Button,
Card,
Dropdown,
Form,
InputNumber,
notification,
Space,
} from "antd";
import moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UPDATE_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries"; import { UPDATE_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
@@ -22,7 +13,6 @@ export default function ScoreboardEntryEdit({ entry }) {
const handleFinish = async (values) => { const handleFinish = async (values) => {
setLoading(true); setLoading(true);
values.date = moment(values.date).format("YYYY-MM-DD");
const result = await updateScoreboardentry({ const result = await updateScoreboardentry({
variables: { sbId: entry.id, sbInput: values }, variables: { sbId: entry.id, sbInput: values },
}); });
@@ -87,14 +77,13 @@ export default function ScoreboardEntryEdit({ entry }) {
> >
<InputNumber precision={1} /> <InputNumber precision={1} />
</Form.Item> </Form.Item>
<Space wrap>
<Button type="primary" loading={loading} htmlType="submit"> <Button type="primary" loading={loading} htmlType="submit">
{t("general.actions.save")} {t("general.actions.save")}
</Button> </Button>
<Button onClick={() => setVisible(false)}> <Button onClick={() => setVisible(false)}>
{t("general.actions.cancel")} {t("general.actions.cancel")}
</Button> </Button>
</Space>
</Form> </Form>
</Card> </Card>
); );

View File

@@ -1,4 +1,3 @@
import { SyncOutlined } from "@ant-design/icons";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { Button, Card, Input, Modal, Space, Table, Typography } from "antd"; import { Button, Card, Input, Modal, Space, Table, Typography } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
@@ -6,14 +5,12 @@ import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { QUERY_SCOREBOARD_PAGINATED } from "../../graphql/scoreboard.queries"; import { QUERY_SCOREBOARD_PAGINATED } from "../../graphql/scoreboard.queries";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import OwnerNameDisplay, { import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component"; import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component";
import ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.component"; import ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.component";
import { SyncOutlined } from "@ant-design/icons";
import {pageLimit} from "../../utils/config";
export default function ScoreboardJobsList({ scoreBoardlist }) { export default function ScoreboardJobsList({ scoreBoardlist }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
@@ -47,7 +44,6 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
render: (text, record) => ( render: (text, record) => (
<Link to={"/manage/jobs/" + record.job.id}> <Link to={"/manage/jobs/" + record.job.id}>
{record.job.ro_number || t("general.labels.na")} {record.job.ro_number || t("general.labels.na")}
@@ -59,11 +55,7 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
OwnerNameDisplayFunction(a.job),
OwnerNameDisplayFunction(b.job)
),
render: (text, record) => <OwnerNameDisplay ownerObject={record.job} />, render: (text, record) => <OwnerNameDisplay ownerObject={record.job} />,
}, },
{ {
@@ -71,15 +63,6 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.job.v_model_yr || ""} ${a.job.v_make_desc || ""} ${
a.job.v_model_desc || ""
}`,
`${b.job.v_model_yr || ""} ${b.job.v_make_desc || ""} ${
b.job.v_model_desc || ""
}`
),
render: (text, record) => ( render: (text, record) => (
<span>{`${record.job.v_model_yr || ""} ${ <span>{`${record.job.v_model_yr || ""} ${
record.job.v_make_desc || "" record.job.v_make_desc || ""
@@ -90,20 +73,17 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
title: t("scoreboard.fields.date"), title: t("scoreboard.fields.date"),
dataIndex: "date", dataIndex: "date",
key: "date", key: "date",
sorter: (a, b) => dateSort(a.date, b.date),
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>, render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
}, },
{
title: t("scoreboard.fields.bodyhrs"),
dataIndex: "bodyhrs",
key: "bodyhrs",
sorter: (a, b) => Number(a.bodyhrs) - Number(b.bodyhrs),
},
{ {
title: t("scoreboard.fields.painthrs"), title: t("scoreboard.fields.painthrs"),
dataIndex: "painthrs", dataIndex: "painthrs",
key: "painthrs", key: "painthrs",
sorter: (a, b) => Number(a.painthrs) - Number(b.painthrs), },
{
title: t("scoreboard.fields.bodyhrs"),
dataIndex: "bodyhrs",
key: "bodyhrs",
}, },
{ {
title: t("general.labels.actions"), title: t("general.labels.actions"),
@@ -124,9 +104,8 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
visible={state.visible} visible={state.visible}
destroyOnClose destroyOnClose
width="80%" width="80%"
closable={false}
cancelButtonProps={{ style: { display: "none" } }} cancelButtonProps={{ style: { display: "none" } }}
onOk={() => onCancel={() =>
setState((state) => ({ setState((state) => ({
...state, ...state,
visible: false, visible: false,

View File

@@ -29,13 +29,10 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
let ret = { let ret = {
todayBody: 0, todayBody: 0,
todayPaint: 0, todayPaint: 0,
todayJobs: 0,
weeklyPaint: 0, weeklyPaint: 0,
weeklyJobs: 0,
weeklyBody: 0, weeklyBody: 0,
toDateBody: 0, toDateBody: 0,
toDatePaint: 0, toDatePaint: 0,
toDateJobs: 0,
}; };
const today = moment(); const today = moment();
@@ -43,7 +40,6 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
dateHash[today.format("YYYY-MM-DD")].forEach((d) => { dateHash[today.format("YYYY-MM-DD")].forEach((d) => {
ret.todayBody = ret.todayBody + d.bodyhrs; ret.todayBody = ret.todayBody + d.bodyhrs;
ret.todayPaint = ret.todayPaint + d.painthrs; ret.todayPaint = ret.todayPaint + d.painthrs;
ret.todayJobs++;
}); });
} }
@@ -53,7 +49,6 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => { dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => {
ret.weeklyBody = ret.weeklyBody + d.bodyhrs; ret.weeklyBody = ret.weeklyBody + d.bodyhrs;
ret.weeklyPaint = ret.weeklyPaint + d.painthrs; ret.weeklyPaint = ret.weeklyPaint + d.painthrs;
ret.weeklyJobs++;
}); });
} }
StartOfWeek = StartOfWeek.add(1, "day"); StartOfWeek = StartOfWeek.add(1, "day");
@@ -65,7 +60,6 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => { dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => {
ret.toDateBody = ret.toDateBody + d.bodyhrs; ret.toDateBody = ret.toDateBody + d.bodyhrs;
ret.toDatePaint = ret.toDatePaint + d.painthrs; ret.toDatePaint = ret.toDatePaint + d.painthrs;
ret.toDateJobs++;
}); });
} }
startOfMonth = startOfMonth.add(1, "day"); startOfMonth = startOfMonth.add(1, "day");
@@ -93,7 +87,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
<Statistic <Statistic
title={t("scoreboard.labels.dailytarget")} title={t("scoreboard.labels.dailytarget")}
value={bodyshop.scoreboard_target.dailyBodyTarget} value={bodyshop.scoreboard_target.dailyBodyTarget}
prefix={t("scoreboard.labels.bodyabbrev")} prefix="B"
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
@@ -146,7 +140,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
value={bodyshop.scoreboard_target.dailyPaintTarget} value={bodyshop.scoreboard_target.dailyPaintTarget}
prefix={t("scoreboard.labels.refinishabbrev")} prefix="P"
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
@@ -187,12 +181,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
<Divider style={{ margin: 5 }} /> <Divider style={{ margin: 5 }} />
</Row> </Row>
<Row> <Row>
<Col {...statSpans}> <Col {...statSpans}></Col>
<Statistic
value={"\u00A0"}
prefix={t("scoreboard.labels.total")}
/>
</Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
value={(values.todayPaint + values.todayBody).toFixed(1)} value={(values.todayPaint + values.todayBody).toFixed(1)}
@@ -251,29 +240,6 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
/> />
</Col> </Col>
</Row> </Row>
<Row>
<Divider style={{ margin: 5 }} />
</Row>
<Row>
<Col {...statSpans}>
<Statistic
value={"\u00A0"}
prefix={t("scoreboard.labels.jobs")}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.todayJobs} />
</Col>
<Col {...statSpans} />
<Col {...statSpans}>
<Statistic value={values.weeklyJobs} />
</Col>
<Col {...statSpans} />
<Col {...statSpans} />
<Col {...statSpans}>
<Statistic value={values.toDateJobs} />
</Col>
</Row>
</Col> </Col>
</Row> </Row>
</Card> </Card>

View File

@@ -1,5 +1,5 @@
import { Form } from "antd";
import React from "react"; import React from "react";
import { Form } from "antd";
import ConfigFormComponents from "../config-form-components/config-form-components.component"; import ConfigFormComponents from "../config-form-components/config-form-components.component";
export default function ShopCsiConfigForm({ selectedCsi }) { export default function ShopCsiConfigForm({ selectedCsi }) {
@@ -9,7 +9,7 @@ export default function ShopCsiConfigForm({ selectedCsi }) {
return ( return (
<div> <div>
{readOnly} The Config Form {readOnly}
{selectedCsi && ( {selectedCsi && (
<Form form={form} onFinish={handleFinish}> <Form form={form} onFinish={handleFinish}>
<ConfigFormComponents <ConfigFormComponents

View File

@@ -1,7 +1,7 @@
import { CheckCircleFilled } from "@ant-design/icons"; import { CheckCircleFilled } from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Col, List, Row } from "antd"; import { Button, Col, List, Row } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useQuery } from "@apollo/client";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { GET_ALL_QUESTION_SETS } from "../../graphql/csi.queries"; import { GET_ALL_QUESTION_SETS } from "../../graphql/csi.queries";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
@@ -21,6 +21,7 @@ export default function ShopCsiConfig() {
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error" />;
return ( return (
<div> <div>
The Config Form
<Row> <Row>
<Col span={3}> <Col span={3}>
<List <List
@@ -41,8 +42,7 @@ export default function ShopCsiConfig() {
)} )}
/> />
</Col> </Col>
<Col span={1} /> <Col span={21}>
<Col span={20}>
<ShopCsiConfigForm selectedCsi={selectedCsi} /> <ShopCsiConfigForm selectedCsi={selectedCsi} />
</Col> </Col>
</Row> </Row>

View File

@@ -5,7 +5,6 @@ import {
Input, Input,
InputNumber, InputNumber,
Select, Select,
Space,
Switch, Switch,
Typography, Typography,
} from "antd"; } from "antd";
@@ -18,7 +17,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { useTreatments } from "@splitsoftware/splitio-react"; import { useTreatments } from "@splitsoftware/splitio-react";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
const SelectorDiv = styled.div` const SelectorDiv = styled.div`
.ant-form-item .ant-select { .ant-form-item .ant-select {
@@ -193,7 +191,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.dms.cdk.payers")}> <LayoutFormRow header={t("bodyshop.labels.dms.cdk.payers")}>
<Form.List name={["cdk_configuration", "payers"]}> <Form.List name={["cdk_configuration", "payers"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove }) => {
return ( return (
<div> <div>
{fields.map((field, index) => ( {fields.map((field, index) => (
@@ -251,18 +249,11 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
</Select> </Select>
</Form.Item> </Form.Item>
<Space align="center"> <DeleteFilled
<DeleteFilled onClick={() => {
onClick={() => { remove(field.name);
remove(field.name); }}
}} />
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow> </LayoutFormRow>
</Form.Item> </Form.Item>
))} ))}
@@ -354,7 +345,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
id="costs" id="costs"
> >
<Form.List name={["md_responsibility_centers", "costs"]}> <Form.List name={["md_responsibility_centers", "costs"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove }) => {
return ( return (
<div> <div>
{fields.map((field, index) => ( {fields.map((field, index) => (
@@ -471,18 +462,12 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Input onBlur={handleBlur} /> <Input onBlur={handleBlur} />
</Form.Item> </Form.Item>
)} )}
<Space align="center">
<DeleteFilled <DeleteFilled
onClick={() => { onClick={() => {
remove(field.name); remove(field.name);
}} }}
/> />
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow> </LayoutFormRow>
</Form.Item> </Form.Item>
))} ))}
@@ -508,7 +493,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
id="profits" id="profits"
> >
<Form.List name={["md_responsibility_centers", "profits"]}> <Form.List name={["md_responsibility_centers", "profits"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove }) => {
return ( return (
<div> <div>
{fields.map((field, index) => ( {fields.map((field, index) => (
@@ -610,18 +595,11 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Input onBlur={handleBlur} /> <Input onBlur={handleBlur} />
</Form.Item> </Form.Item>
)} )}
<Space align="center"> <DeleteFilled
<DeleteFilled onClick={() => {
onClick={() => { remove(field.name);
remove(field.name); }}
}} />
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow> </LayoutFormRow>
</Form.Item> </Form.Item>
))} ))}

View File

@@ -7,9 +7,7 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort, statusSort } from "../../utils/sorters"; import { alphaSort, statusSort } from "../../utils/sorters";
import OwnerNameDisplay, { import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component"; import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -47,10 +45,6 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
title: t("jobs.fields.owner"), title: t("jobs.fields.owner"),
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link to={`/manage/owners/${record.owner.id}`}> <Link to={`/manage/owners/${record.owner.id}`}>
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record} />
@@ -69,15 +63,9 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters: bodyshop.md_ro_statuses.statuses.map((status) => ({
text: status,
value: status,
})),
onFilter: (value, record) => value.includes(record.status),
}, },
{ {

View File

@@ -4,9 +4,8 @@ import queryString from "query-string";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
import { pageLimit } from "../../utils/config";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
import { alphaSort } from './../../utils/sorters'; import {pageLimit} from "../../utils/config";
export default function VehiclesListComponent({ export default function VehiclesListComponent({
loading, loading,
vehicles, vehicles,
@@ -32,8 +31,6 @@ export default function VehiclesListComponent({
title: t("vehicles.fields.v_vin"), title: t("vehicles.fields.v_vin"),
dataIndex: "v_vin", dataIndex: "v_vin",
key: "v_vin", key: "v_vin",
sorter: (a, b) => alphaSort(a.v_vin, b.v_vin),
sortOrder: state.sortedInfo.columnKey === "v_vin" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link to={"/manage/vehicles/" + record.id}> <Link to={"/manage/vehicles/" + record.id}>
<VehicleVinDisplay>{record.v_vin || "N/A"}</VehicleVinDisplay> <VehicleVinDisplay>{record.v_vin || "N/A"}</VehicleVinDisplay>
@@ -54,10 +51,8 @@ export default function VehiclesListComponent({
}, },
{ {
title: t("vehicles.fields.plate_no"), title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no", dataIndex: "plate",
key: "plate_no", key: "plate",
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder: state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return ( return (
<span>{`${record.plate_st || ""} | ${record.plate_no || ""}`}</span> <span>{`${record.plate_st || ""} | ${record.plate_no || ""}`}</span>

View File

@@ -4,8 +4,6 @@ import { getAuth, updatePassword, updateProfile } from "firebase/auth";
import { getFirestore } from "firebase/firestore"; import { getFirestore } from "firebase/firestore";
import { getMessaging, getToken, onMessage } from "firebase/messaging"; import { getMessaging, getToken, onMessage } from "firebase/messaging";
import { store } from "../redux/store"; import { store } from "../redux/store";
import axios from "axios";
import { checkBeta } from "../utils/handleBeta";
const config = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG); const config = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
initializeApp(config); initializeApp(config);
@@ -88,18 +86,6 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
null, null,
...additionalParams, ...additionalParams,
}; };
axios.post("/ioevent", {
useremail:
(state.user && state.user.currentUser && state.user.currentUser.email) ||
null,
bodyshopid:
(state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
operationName: eventName,
variables: additionalParams,
dbevent: false,
env: checkBeta() ? "beta" : "master",
});
// console.log( // console.log(
// "%c[Analytics]", // "%c[Analytics]",
// "background-color: green ;font-weight:bold;", // "background-color: green ;font-weight:bold;",

View File

@@ -1,6 +1,5 @@
import { onError } from "@apollo/client/link/error"; import { onError } from "@apollo/client/link/error";
//https://stackoverflow.com/questions/57163454/refreshing-a-token-with-apollo-client-firebase-auth //https://stackoverflow.com/questions/57163454/refreshing-a-token-with-apollo-client-firebase-auth
import * as Sentry from "@sentry/react";
const errorLink = onError( const errorLink = onError(
({ graphQLErrors, networkError, operation, forward }) => { ({ graphQLErrors, networkError, operation, forward }) => {
@@ -9,7 +8,6 @@ const errorLink = onError(
console.log( console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}` `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
); );
Sentry.captureException({ message, locations, path });
}); });
} }
if (networkError) if (networkError)

View File

@@ -40,7 +40,6 @@ export const INSERT_AUDIT_TRAIL = gql`
bodyshopid bodyshopid
created created
operation operation
type
useremail useremail
} }
} }

View File

@@ -22,7 +22,6 @@ export const QUERY_AVAILABLE_CC = gql`
] ]
status: { _eq: "courtesycars.status.in" } status: { _eq: "courtesycars.status.in" }
} }
order_by: { fleetnumber: asc }
) { ) {
color color
dailycost dailycost
@@ -30,7 +29,6 @@ export const QUERY_AVAILABLE_CC = gql`
fleetnumber fleetnumber
fuel fuel
id id
insuranceexpires
make make
mileage mileage
model model
@@ -59,7 +57,7 @@ export const CHECK_CC_FLEET_NUMBER = gql`
`; `;
export const QUERY_ALL_CC = gql` export const QUERY_ALL_CC = gql`
query QUERY_ALL_CC { query QUERY_ALL_CC {
courtesycars(order_by: { fleetnumber: asc }) { courtesycars {
color color
created_at created_at
dailycost dailycost

View File

@@ -57,15 +57,19 @@ export const INSERT_CSI = gql`
`; `;
export const QUERY_CSI_RESPONSE_PAGINATED = gql` export const QUERY_CSI_RESPONSE_PAGINATED = gql`
query QUERY_CSI_RESPONSE_PAGINATED { query QUERY_CSI_RESPONSE_PAGINATED(
csi(order_by: { completedon: desc_nulls_last }) { $offset: Int
$limit: Int
$order: [csi_order_by!]!
) {
csi(offset: $offset, limit: $limit, order_by: $order) {
id id
completedon completedon
job { job {
ownr_fn ownr_fn
ownr_ln ownr_ln
ownerid
ro_number ro_number
id id
} }
} }
@@ -79,7 +83,6 @@ export const QUERY_CSI_RESPONSE_PAGINATED = gql`
export const QUERY_CSI_RESPONSE_BY_PK = gql` export const QUERY_CSI_RESPONSE_BY_PK = gql`
query QUERY_CSI_RESPONSE_BY_PK($id: uuid!) { query QUERY_CSI_RESPONSE_BY_PK($id: uuid!) {
csi_by_pk(id: $id) { csi_by_pk(id: $id) {
completedon
relateddata relateddata
valid valid
validuntil validuntil

View File

@@ -108,7 +108,12 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
`; `;
export const QUERY_PARTS_QUEUE = gql` export const QUERY_PARTS_QUEUE = gql`
query QUERY_PARTS_QUEUE($statuses: [String!]!, $offset: Int, $limit: Int) { query QUERY_PARTS_QUEUE(
$statuses: [String!]!
$offset: Int
$limit: Int
$order: [jobs_order_by!]
) {
jobs_aggregate(where: { _and: [{ status: { _in: $statuses } }] }) { jobs_aggregate(where: { _and: [{ status: { _in: $statuses } }] }) {
aggregate { aggregate {
count(distinct: true) count(distinct: true)
@@ -120,7 +125,7 @@ export const QUERY_PARTS_QUEUE = gql`
} }
offset: $offset offset: $offset
limit: $limit limit: $limit
order_by: { ro_number: desc } order_by: $order
) { ) {
ownr_fn ownr_fn
ownr_ln ownr_ln
@@ -137,9 +142,7 @@ export const QUERY_PARTS_QUEUE = gql`
v_color v_color
vehicleid vehicleid
scheduled_in scheduled_in
scheduled_completion
id id
ins_co_nm
clm_no clm_no
ro_number ro_number
status status
@@ -1862,7 +1865,6 @@ export const QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED = gql`
ownr_co_nm ownr_co_nm
ownr_ph1 ownr_ph1
ownr_ph2 ownr_ph2
ownerid
plate_no plate_no
plate_st plate_st
v_vin v_vin
@@ -2336,163 +2338,3 @@ export const MARK_JOB_AS_UNINVOICED = gql`
} }
} }
`; `;
export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
query QUERY_JOB_CARD_DETAILS($id: uuid!) {
jobs_by_pk(id: $id) {
actual_completion
actual_delivery
actual_in
alt_transport
available_jobs {
id
}
area_of_damage
ca_gst_registrant
cccontracts {
agreementnumber
courtesycar {
id
make
model
year
plate
fleetnumber
}
id
scheduledreturn
start
status
}
clm_no
clm_total
comment
date_estimated
date_exported
date_invoiced
date_last_contacted
date_next_contact
date_open
date_repairstarted
date_scheduled
ded_amt
employee_body
employee_body_rel {
id
first_name
last_name
}
employee_csr
employee_csr_rel {
id
first_name
last_name
}
employee_prep
employee_prep_rel {
id
first_name
last_name
}
employee_refinish
employee_refinish_rel {
id
first_name
last_name
}
est_co_nm
est_ct_fn
est_ct_ln
est_ea
est_ph1
id
ins_co_nm
ins_ct_fn
ins_ct_ln
ins_ea
ins_ph1
inproduction
job_totals
joblines(
order_by: { line_no: asc }
where: {
part_type: {
_in: [
"PAN"
"PAC"
"PAR"
"PAL"
"PAA"
"PAM"
"PAP"
"PAG"
]
}
removed: { _eq: false }
}
) {
act_price
alt_partno
db_ref
id
line_desc
line_no
location
mod_lbr_ty
mod_lb_hrs
oem_partno
part_qty
part_type
prt_dsmk_m
status
}
lbr_adjustments
ownr_co_nm
ownr_ea
ownr_fn
ownr_ln
ownr_ph1
ownr_ph2
owner {
id
allow_text_message
preferred_contact
tax_number
}
owner_owing
plate_no
plate_st
po_number
production_vars
ro_number
scheduled_completion
scheduled_delivery
scheduled_in
special_coverage_policy
status
suspended
updated_at
vehicle {
id
jobs {
id
clm_no
ro_number
}
notes
plate_no
v_color
v_make_desc
v_model_desc
v_model_yr
}
vehicleid
v_color
v_make_desc
v_model_desc
v_model_yr
v_vin
voided
}
}
`;

View File

@@ -31,7 +31,6 @@ export const QUERY_VEHICLE_BY_ID = gql`
jobs(order_by: { date_open: desc }) { jobs(order_by: { date_open: desc }) {
id id
ro_number ro_number
ownr_co_nm
ownr_fn ownr_fn
ownr_ln ownr_ln
owner { owner {

View File

@@ -1,67 +1,88 @@
// import { useMutation, useQuery } from "@apollo/client"; import { useQuery, useMutation } from "@apollo/client";
import { Button, Form, Layout, Result, Typography } from "antd"; import { Form, Layout, Typography, Button, Result } from "antd";
import axios from "axios"; import React, { useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import AlertComponent from "../../components/alert/alert.component"; import AlertComponent from "../../components/alert/alert.component";
import ConfigFormComponents from "../../components/config-form-components/config-form-components.component"; import ConfigFormComponents from "../../components/config-form-components/config-form-components.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { QUERY_SURVEY, COMPLETE_SURVEY } from "../../graphql/csi.queries";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selectors"; import { selectCurrentUser } from "../../redux/user/user.selectors";
import { DateTimeFormat } from "./../../utils/DateFormatter";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({}); const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(CsiContainerPage); export default connect(mapStateToProps, mapDispatchToProps)(CsiContainerPage);
export function CsiContainerPage({ currentUser }) { export function CsiContainerPage({ currentUser }) {
const { surveyId } = useParams(); const { surveyId } = useParams();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [axiosResponse, setAxiosResponse] = useState(null);
const [submitting, setSubmitting] = useState({ const [submitting, setSubmitting] = useState({
loading: false, loading: false,
submitted: false, submitted: false,
}); });
const { t } = useTranslation();
const getAxiosData = useCallback(async () => { const { loading, error, data } = useQuery(QUERY_SURVEY, {
try { variables: { surveyId },
try { fetchPolicy: "network-only",
window.$crisp.push(["do", "chat:hide"]); nextFetchPolicy: "network-only",
} catch { });
console.log("Unable to attach to crisp instance. ");
} const { t } = useTranslation();
setSubmitting((prevSubmitting) => ({ ...prevSubmitting, loading: true })); const [completeSurvey] = useMutation(COMPLETE_SURVEY);
const response = await axios.post("/csi/lookup", { surveyId }); if (loading) return <LoadingSpinner />;
setSubmitting((prevSubmitting) => ({
...prevSubmitting, if (error || !!!data.csi_by_pk)
return (
<div>
<Result
status="error"
title={t("csi.errors.notfoundtitle")}
subTitle={t("csi.errors.notfoundsubtitle")}
>
{error ? (
<div>ERROR: {error.graphQLErrors.map((e) => e.message)}</div>
) : null}
</Result>
</div>
);
const handleFinish = async (values) => {
setSubmitting({ ...submitting, loading: true });
const result = await completeSurvey({
variables: {
surveyId,
survey: {
response: values,
valid: false,
completedon: new Date(),
},
},
});
if (!!!result.errors) {
setSubmitting({ ...submitting, loading: false, submitted: true });
} else {
setSubmitting({
...submitting,
loading: false, loading: false,
})); error: JSON.stringify(result.errors),
setAxiosResponse(response.data);
} catch (error) {
console.error(`Something went wrong...: ${error.message}`);
console.dir({
stack: error?.stack,
message: error?.message,
}); });
} }
}, [setAxiosResponse, surveyId]); };
useEffect(() => { const {
getAxiosData().catch((err) => relateddata: { bodyshop, job },
console.error( csiquestion: { config: csiquestions },
`Something went wrong fetching axios data: ${err.message || ""}` } = data.csi_by_pk;
)
);
}, [getAxiosData]);
// Return if authorized if (currentUser && currentUser.authorized)
if (currentUser && currentUser.authorized) {
return ( return (
<Layout <Layout
style={{ height: "100vh", display: "flex", flexDirection: "column" }} style={{ height: "100vh", display: "flex", flexDirection: "column" }}
@@ -73,176 +94,85 @@ export function CsiContainerPage({ currentUser }) {
/> />
</Layout> </Layout>
); );
}
if (submitting.loading) return <LoadingSpinner />; return (
<Layout
style={{ height: "100vh", display: "flex", flexDirection: "column" }}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<div style={{ display: "flex", alignItems: "center", margin: "2em" }}>
{bodyshop.logo_img_path && bodyshop.logo_img_path.src ? (
<img src={bodyshop.logo_img_path.src} alt="Logo" />
) : null}
<div style={{ margin: "2em" }}>
<strong>{bodyshop.shopname || ""}</strong>
<div>{`${bodyshop.address1 || ""}`}</div>
<div>{`${bodyshop.address2 || ""}`}</div>
<div>{`${bodyshop.city || ""} ${bodyshop.state || ""} ${
bodyshop.zip_post || ""
}`}</div>
</div>
</div>
<Typography.Title>{t("csi.labels.title")}</Typography.Title>
<strong>{`Hi ${job.ownr_co_nm || job.ownr_fn || ""}!`}</strong>
<Typography.Paragraph>
{`At ${
bodyshop.shopname || ""
}, we value your feedback. We would love to
hear what you have to say. Please fill out the form below.`}
</Typography.Paragraph>
</div>
const handleFinish = async (values) => { {submitting.error ? (
try { <AlertComponent message={submitting.error} type="error" />
setSubmitting({ ...submitting, loading: true, submitting: true }); ) : null}
const result = await axios.post("/csi/submit", { surveyId, values });
console.log("result", result);
if (!!!result.errors && result.data.update_csi.affected_rows > 0) {
setSubmitting({ ...submitting, loading: false, submitted: true });
}
} catch (error) {
console.error(`Something went wrong...: ${error.message}`);
console.dir({
stack: error?.stack,
message: error?.message,
});
}
};
if (!axiosResponse || axiosResponse.csi_by_pk === null) { {submitting.submitted ? (
// Do something here , this is where you would return a loading box or something <Layout.Content
return (
<>
<Layout style={{ display: "flex", flexDirection: "column" }}>
<Layout.Content
style={{
backgroundColor: "#fff",
margin: "2em 4em",
padding: "2em",
overflowY: "auto",
textAlign: "center",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Form>
<Result
status="error"
title={t("csi.errors.notfoundtitle")}
subTitle={t("csi.errors.notfoundsubtitle")}
/>
</Form>
</Layout.Content>
<Layout.Footer>
{t("csi.labels.copyright")}{" "}
{t("csi.fields.surveyid", { surveyId: surveyId })}
</Layout.Footer>
</Layout>
</>
);
} else {
const {
relateddata: { bodyshop, job },
csiquestion: { config: csiquestions },
} = axiosResponse.csi_by_pk;
return (
<Layout style={{ display: "flex", flexDirection: "column" }}>
<div
style={{ style={{
display: "flex", backgroundColor: "#fff",
flexDirection: "column", margin: "2em 4em",
alignItems: "center", padding: "2em",
overflowY: "auto",
}} }}
> >
<div style={{ display: "flex", alignItems: "center", margin: "2em" }}> <Result
{bodyshop.logo_img_path && bodyshop.logo_img_path.src ? ( status="success"
<img title={t("csi.successes.submitted")}
src={bodyshop.logo_img_path.src} subTitle={t("csi.successes.submittedsub")}
alt={bodyshop.shopname.concat(" Logo")} />
height={bodyshop.logo_img_path.height} </Layout.Content>
width={bodyshop.logo_img_path.width} ) : (
/> <Layout.Content
) : null} style={{
<div style={{ margin: "2em", verticalAlign: "middle" }}> backgroundColor: "#fff",
<Typography.Title level={4} style={{ margin: 0 }}> margin: "2em 4em",
{bodyshop.shopname || ""} padding: "2em",
</Typography.Title> overflowY: "auto",
<Typography.Paragraph style={{ margin: 0 }}> }}
{`${bodyshop.address1 || ""}${bodyshop.address2 ? ", " : ""}${ >
bodyshop.address2 || "" <Form form={form} onFinish={handleFinish}>
}`.trim()} <ConfigFormComponents componentList={csiquestions} />
</Typography.Paragraph> <Button
<Typography.Paragraph style={{ margin: 0 }}> loading={submitting.loading}
{`${bodyshop.city || ""}${ type="primary"
bodyshop.city && bodyshop.state ? ", " : "" htmlType="submit"
}${bodyshop.state || ""} ${bodyshop.zip_post || ""}`.trim()} >
</Typography.Paragraph> {t("general.actions.submit")}
</div> </Button>
</div> </Form>
<Typography.Title>{t("csi.labels.title")}</Typography.Title> </Layout.Content>
<strong> )}
{t("csi.labels.greeting", {
name: job.ownr_co_nm || job.ownr_fn || "",
})}
</strong>
<Typography.Paragraph>
{t("csi.labels.intro", { shopname: bodyshop.shopname || "" })}
</Typography.Paragraph>
</div>
{submitting.error ? ( <Layout.Footer>
<AlertComponent message={submitting.error} type="error" /> {`Copyright ImEX.Online. Survey ID: ${surveyId}`}
) : null} </Layout.Footer>
</Layout>
{submitting.submitted ? ( );
<Layout.Content
style={{
backgroundColor: "#fff",
margin: "2em 4em",
padding: "2em",
overflowY: "auto",
}}
>
<Result
status="success"
title={t("csi.successes.submitted")}
subTitle={t("csi.successes.submittedsub")}
/>
</Layout.Content>
) : (
<Layout.Content
style={{
backgroundColor: "#fff",
margin: "2em 4em",
padding: "2em",
overflowY: "auto",
}}
>
<Form form={form} onFinish={handleFinish}>
{axiosResponse.csi_by_pk.valid ? (
<>
<ConfigFormComponents componentList={csiquestions} />
<Button
loading={submitting.loading}
type="primary"
htmlType="submit"
style={{ float: "right" }}
>
{t("general.actions.submit")}
</Button>
</>
) : (
<>
<Result
title={t("csi.errors.surveycompletetitle")}
status="warning"
subTitle={t("csi.errors.surveycompletesubtitle", {
date: DateTimeFormat(axiosResponse.csi_by_pk.completedon),
})}
/>
<Typography.Paragraph
type="secondary"
style={{ textAlign: "center" }}
>
{t("csi.successes.submittedsub")}
</Typography.Paragraph>
</>
)}
</Form>
</Layout.Content>
)}
<Layout.Footer>
{t("csi.labels.copyright")}{" "}
{t("csi.fields.surveyid", { surveyId: surveyId })}
</Layout.Footer>
</Layout>
);
}
} }

View File

@@ -26,12 +26,10 @@ import { OwnerNameDisplayFunction } from "../../components/owner-name-display/ow
import { auth } from "../../firebase/firebase.utils"; import { auth } from "../../firebase/firebase.utils";
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries"; import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
import { import {
insertAuditTrail,
setBreadcrumbs, setBreadcrumbs,
setSelectedHeader, setSelectedHeader,
} from "../../redux/application/application.actions"; } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -40,8 +38,6 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)), setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer); export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
@@ -50,7 +46,7 @@ export const socket = SocketIO(
process.env.NODE_ENV === "production" process.env.NODE_ENV === "production"
? process.env.REACT_APP_AXIOS_BASE_API_URL ? process.env.REACT_APP_AXIOS_BASE_API_URL
: window.location.origin, : window.location.origin,
// "http://localhost:4000", // for dev testing, // "http://localhost:4000", // for dev testing,
{ {
path: "/ws", path: "/ws",
withCredentials: true, withCredentials: true,
@@ -61,12 +57,7 @@ export const socket = SocketIO(
} }
); );
export function DmsContainer({ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
bodyshop,
setBreadcrumbs,
setSelectedHeader,
insertAuditTrail,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [logLevel, setLogLevel] = useState("DEBUG"); const [logLevel, setLogLevel] = useState("DEBUG");
const history = useHistory(); const history = useHistory();
@@ -124,11 +115,6 @@ export function DmsContainer({
notification.success({ notification.success({
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
insertAuditTrail({
jobid: payload,
operation: AuditTrailMapping.jobexported(),
type: "jobexported",
});
history.push("/manage/accounting/receivables"); history.push("/manage/accounting/receivables");
}); });

View File

@@ -12,8 +12,7 @@ import AlertComponent from "../../components/alert/alert.component";
import { QUERY_EXPORT_LOG_PAGINATED } from "../../graphql/accounting.queries"; import { QUERY_EXPORT_LOG_PAGINATED } from "../../graphql/accounting.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config"; import {pageLimit} from "../../utils/config";
import { alphaSort, dateSort } from "./../../utils/sorters";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -35,43 +34,11 @@ export function ExportLogsPageComponent({ bodyshop }) {
limit: pageLimit, limit: pageLimit,
order: [ order: [
{ {
...(sortcolumn === "ro_number" [sortcolumn || "created_at"]: sortorder
? { ? sortorder === "descend"
job: { ? "desc"
[sortcolumn || "created_at"]: sortorder : "asc"
? sortorder === "descend" : "desc",
? "desc"
: "asc"
: "desc",
},
}
: sortcolumn === "invoice_number"
? {
bill: {
[sortcolumn || "created_at"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
}
: sortcolumn === "paymentnum"
? {
payment: {
[sortcolumn || "created_at"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
}
: {
[sortcolumn || "created_at"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
}),
}, },
], ],
}, },
@@ -101,8 +68,6 @@ export function ExportLogsPageComponent({ bodyshop }) {
title: t("general.labels.created_at"), title: t("general.labels.created_at"),
dataIndex: "created_at", dataIndex: "created_at",
key: "created_at", key: "created_at",
sorter: (a, b) => dateSort(a.created_at, b.created_at),
sortOrder: sortcolumn === "created_at" && sortorder,
render: (text, record) => ( render: (text, record) => (
<DateTimeFormatter>{record.created_at}</DateTimeFormatter> <DateTimeFormatter>{record.created_at}</DateTimeFormatter>
), ),
@@ -116,8 +81,7 @@ export function ExportLogsPageComponent({ bodyshop }) {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder,
render: (text, record) => render: (text, record) =>
record.job && ( record.job && (
<Link to={`/manage/jobs/${record.job.id}`}> <Link to={`/manage/jobs/${record.job.id}`}>
@@ -129,8 +93,6 @@ export function ExportLogsPageComponent({ bodyshop }) {
title: t("bills.fields.invoice_number"), title: t("bills.fields.invoice_number"),
dataIndex: "invoice_number", dataIndex: "invoice_number",
key: "invoice_number", key: "invoice_number",
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
sortOrder: sortcolumn === "invoice_number" && sortorder,
render: (text, record) => render: (text, record) =>
record.bill && ( record.bill && (
<Link to={"/manage/bills?billid=" + (record.bill && record.bill.id)}> <Link to={"/manage/bills?billid=" + (record.bill && record.bill.id)}>
@@ -142,8 +104,6 @@ export function ExportLogsPageComponent({ bodyshop }) {
title: t("payments.fields.paymentnum"), title: t("payments.fields.paymentnum"),
dataIndex: "paymentnum", dataIndex: "paymentnum",
key: "paymentnum", key: "paymentnum",
sorter: (a, b) => alphaSort(a.paymentnum, b.paymentnum),
sortOrder: sortcolumn === "paymentnum" && sortorder,
render: (text, record) => render: (text, record) =>
record.payment && ( record.payment && (
<Link <Link
@@ -160,13 +120,6 @@ export function ExportLogsPageComponent({ bodyshop }) {
title: t("general.labels.successful"), title: t("general.labels.successful"),
dataIndex: "successful", dataIndex: "successful",
key: "successful", key: "successful",
sorter: (a, b) => Number(a.successful) - Number(b.successful),
sortOrder: sortcolumn === "successful" && sortorder,
filters: [
{ text: "True", value: true },
{ text: "False", value: false },
],
onFilter: (value, record) => record.successful === value,
render: (text, record) => ( render: (text, record) => (
<Checkbox disabled checked={record.successful} /> <Checkbox disabled checked={record.successful} />
), ),

View File

@@ -47,11 +47,11 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) { export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const client = useApolloClient(); const client = useApolloClient();
@@ -121,7 +121,6 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.jobinvoiced(), operation: AuditTrailMapping.jobinvoiced(),
type: "jobinvoiced",
}); });
// history.push(`/manage/jobs/${job.id}`); // history.push(`/manage/jobs/${job.id}`);
} else { } else {

View File

@@ -29,7 +29,6 @@ import { createStructuredSelector } from "reselect";
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component"; import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component"; import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container"; import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container"; import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container";
import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container"; import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container";
import JobSyncButton from "../../components/job-sync-button/job-sync-button.component"; import JobSyncButton from "../../components/job-sync-button/job-sync-button.component";
@@ -55,6 +54,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import UndefinedToNull from "../../utils/undefinedtonull"; import UndefinedToNull from "../../utils/undefinedtonull";
import { DateTimeFormat } from "./../../utils/DateFormatter"; import { DateTimeFormat } from "./../../utils/DateFormatter";
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -63,8 +63,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) => setPrintCenterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "printCenter" })), dispatch(setModalContext({ context: context, modal: "printCenter" })),
insertAuditTrail: ({ jobid, operation, type }) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation, type })), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export function JobsDetailPage({ export function JobsDetailPage({
bodyshop, bodyshop,
@@ -177,7 +177,6 @@ export function JobsDetailPage({
? DateTimeFormat(changedAuditFields[key]) ? DateTimeFormat(changedAuditFields[key])
: changedAuditFields[key] : changedAuditFields[key]
), ),
type: "jobfieldchange",
}); });
}); });
@@ -335,23 +334,15 @@ export function JobsDetailPage({
> >
<JobsDetailLaborContainer job={job} jobId={job.id} /> <JobsDetailLaborContainer job={job} jobId={job.id} />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane <Tabs.TabPane
forceRender forceRender
tab={ tab={<span><BarsOutlined />{t('menus.jobsdetail.lifecycle')}</span>}
<span> key="lifecycle"
<BarsOutlined /> >
{t("menus.jobsdetail.lifecycle")} <JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses}/>
</span> </Tabs.TabPane>
}
key="lifecycle"
>
<JobLifecycleComponent
job={job}
statuses={bodyshop.md_ro_statuses}
/>
</Tabs.TabPane>
<Tabs.TabPane <Tabs.TabPane
forceRender forceRender
tab={ tab={
<span> <span>

View File

@@ -8,28 +8,30 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import AlertComponent from "../../components/alert/alert.component";
import JobPartsQueueCount from "../../components/job-parts-queue-count/job-parts-queue-count.component";
import JobRemoveFromPartsQueue from "../../components/job-remove-from-parst-queue/job-remove-from-parts-queue.component";
import OwnerNameDisplay from "../../components/owner-name-display/owner-name-display.component";
import ProductionListColumnComment from "../../components/production-list-columns/production-list-columns.comment.component";
import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries"; import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage"; import useLocalStorage from "../../utils/useLocalStorage";
import AlertComponent from "../alert/alert.component"; import {pageLimit} from "../../utils/config";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import JobRemoveFromPartsQueue from "../job-remove-from-parst-queue/job-remove-from-parts-queue.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
export function PartsQueueListComponent({ bodyshop }) { export function PartsQueuePageComponent({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search); const searchParams = queryString.parse(useLocation().search);
const { selected, sortcolumn, sortorder, statusFilters } = searchParams; const {
//page,
sortcolumn,
sortorder,
statusFilters,
} = searchParams;
const history = useHistory(); const history = useHistory();
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null); const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
@@ -37,8 +39,19 @@ export function PartsQueueListComponent({ bodyshop }) {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
variables: { variables: {
// offset: page ? (page - 1) * 25 : 0,
// limit: 25,
statuses: (statusFilters && JSON.parse(statusFilters)) || statuses: (statusFilters && JSON.parse(statusFilters)) ||
bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
order: [
{
[sortcolumn || "ro_number"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
],
}, },
}); });
@@ -94,17 +107,6 @@ export function PartsQueueListComponent({ bodyshop }) {
history.push({ search: queryString.stringify(searchParams) }); history.push({ search: queryString.stringify(searchParams) });
}; };
const handleOnRowClick = (record) => {
if (record?.id) {
history.replace({
search: queryString.stringify({
...searchParams,
selected: record.id,
}),
});
}
};
const columns = [ const columns = [
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
@@ -123,8 +125,7 @@ export function PartsQueueListComponent({ bodyshop }) {
title: t("jobs.fields.owner"), title: t("jobs.fields.owner"),
dataIndex: "ownr_ln", dataIndex: "ownr_ln",
key: "ownr_ln", key: "ownr_ln",
sorter: (a, b) => sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortcolumn === "ownr_ln" && sortorder, sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => { render: (text, record) => {
return record.ownerid ? ( return record.ownerid ? (
@@ -138,56 +139,6 @@ export function PartsQueueListComponent({ bodyshop }) {
); );
}, },
}, },
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder: sortcolumn === "vehicle" && sortorder,
render: (text, record) => {
return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
},
{
title: t("jobs.fields.ins_co_nm_short"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder: sortcolumn === "ins_co_nm" && sortorder,
filteredValue: filter?.ins_co_nm || null,
filters:
(jobs &&
jobs
.map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Ins. Co.*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
},
{ {
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
@@ -219,16 +170,23 @@ export function PartsQueueListComponent({ bodyshop }) {
), ),
}, },
{ {
title: t("jobs.fields.scheduled_completion"), title: t("jobs.fields.vehicle"),
dataIndex: "scheduled_completion", dataIndex: "vehicle",
key: "scheduled_completion", key: "vehicle",
ellipsis: true, ellipsis: true,
sorter: (a, b) => render: (text, record) => {
dateSort(a.scheduled_completion, b.scheduled_completion), return record.vehicleid ? (
sortOrder: sortcolumn === "scheduled_completion" && sortorder, <Link to={"/manage/vehicles/" + record.vehicleid}>
render: (text, record) => ( {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
<DateTimeFormatter>{record.scheduled_completion}</DateTimeFormatter> record.v_model_desc || ""
), }`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
}, },
// { // {
// title: t("vehicles.fields.plate_no"), // title: t("vehicles.fields.plate_no"),
@@ -240,6 +198,14 @@ export function PartsQueueListComponent({ bodyshop }) {
// return record.plate_no ? record.plate_no : ""; // return record.plate_no ? record.plate_no : "";
// }, // },
// }, // },
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
ellipsis: true,
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder: sortcolumn === "clm_no" && sortorder,
},
// { // {
// title: t("jobs.fields.clm_total"), // title: t("jobs.fields.clm_total"),
// dataIndex: "clm_total", // dataIndex: "clm_total",
@@ -341,23 +307,9 @@ export function PartsQueueListComponent({ bodyshop }) {
style={{ height: "100%" }} style={{ height: "100%" }}
scroll={{ x: true }} scroll={{ x: true }}
onChange={handleTableChange} onChange={handleTableChange}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selected],
type: "radio",
}}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
},
};
}}
/> />
</Card> </Card>
); );
} }
export default connect(mapStateToProps, null)(PartsQueueListComponent); export default connect(mapStateToProps, null)(PartsQueuePageComponent);

View File

@@ -1,13 +1,12 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import PartsQueueDetailCard from "../../components/parts-queue-card/parts-queue-card.component";
import PartsQueueList from "../../components/parts-queue-list/parts-queue.list.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { import {
setBreadcrumbs, setBreadcrumbs,
setSelectedHeader, setSelectedHeader,
} from "../../redux/application/application.actions"; } from "../../redux/application/application.actions";
import PartsQueuePage from "./parts-queue.page.component";
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
@@ -27,8 +26,7 @@ export function PartsQueuePageContainer({ setBreadcrumbs, setSelectedHeader }) {
return ( return (
<RbacWrapper action="jobs:partsqueue"> <RbacWrapper action="jobs:partsqueue">
<PartsQueueList /> <PartsQueuePage />
<PartsQueueDetailCard />
</RbacWrapper> </RbacWrapper>
); );
} }

View File

@@ -1,20 +1,22 @@
import { Row, Col } from "antd";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { Col, Row } from "antd"; import queryString from "query-string";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import AlertComponent from "../../components/alert/alert.component"; import AlertComponent from "../../components/alert/alert.component";
import CsiResponseFormContainer from "../../components/csi-response-form/csi-response-form.container"; import CsiResponseFormContainer from "../../components/csi-response-form/csi-response-form.container";
import CsiResponseListPaginated from "../../components/csi-response-list-paginated/csi-response-list-paginated.component"; import CsiResponseListPaginated from "../../components/csi-response-list-paginated/csi-response-list-paginated.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { QUERY_CSI_RESPONSE_PAGINATED } from "../../graphql/csi.queries"; import { QUERY_CSI_RESPONSE_PAGINATED } from "../../graphql/csi.queries";
import { import {
setBreadcrumbs, setBreadcrumbs,
setSelectedHeader, setSelectedHeader,
} from "../../redux/application/application.actions"; } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
@@ -31,11 +33,28 @@ export function ShopCsiContainer({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const searchParams = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder } = searchParams;
const { loading, error, data, refetch } = useQuery( const { loading, error, data, refetch } = useQuery(
QUERY_CSI_RESPONSE_PAGINATED, QUERY_CSI_RESPONSE_PAGINATED,
{ {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
variables: {
//search: search || "",
offset: page ? (page - 1) * pageLimit : 0,
limit: pageLimit,
order: [
{
[sortcolumn || "completedon"]: sortorder
? sortorder === "descend"
? "desc_nulls_last"
: "asc"
: "desc_nulls_last",
},
],
},
} }
); );
@@ -54,7 +73,12 @@ export function ShopCsiContainer({
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error" />;
return ( return (
<RbacWrapper action="csi:page"> <RbacWrapper
action="csi:page"
// noauth={
// <AlertComponent message="You don't have acess to see this screen." />
// }
>
<Row gutter={16}> <Row gutter={16}>
<Col span={10}> <Col span={10}>
<CsiResponseListPaginated <CsiResponseListPaginated

View File

@@ -54,9 +54,9 @@ export const setOnline = (isOnline) => ({
payload: isOnline, payload: isOnline,
}); });
export const insertAuditTrail = ({ jobid, billid, operation, type }) => ({ export const insertAuditTrail = ({ jobid, billid, operation }) => ({
type: ApplicationActionTypes.INSERT_AUDIT_TRAIL, type: ApplicationActionTypes.INSERT_AUDIT_TRAIL,
payload: { jobid, billid, operation, type }, payload: { jobid, billid, operation },
}); });
export const setProblemJobs = (problemJobs) => ({ export const setProblemJobs = (problemJobs) => ({
type: ApplicationActionTypes.SET_PROBLEM_JOBS, type: ApplicationActionTypes.SET_PROBLEM_JOBS,

View File

@@ -266,7 +266,7 @@ export function* onInsertAuditTrail() {
} }
export function* insertAuditTrailSaga({ export function* insertAuditTrailSaga({
payload: { jobid, billid, operation, type }, payload: { jobid, billid, operation },
}) { }) {
const state = yield select(); const state = yield select();
const bodyshop = state.user.bodyshop; const bodyshop = state.user.bodyshop;
@@ -278,7 +278,6 @@ export function* insertAuditTrailSaga({
jobid, jobid,
billid, billid,
operation, operation,
type,
useremail: currentUser.email, useremail: currentUser.email,
}, },
}; };
@@ -290,7 +289,7 @@ export function* insertAuditTrailSaga({
fields: { fields: {
audit_trail(existingAuditTrail, { readField }) { audit_trail(existingAuditTrail, { readField }) {
const newAuditTrail = cache.writeQuery({ const newAuditTrail = cache.writeQuery({
data: data, data: data.insert_audit_trail_one,
query: INSERT_AUDIT_TRAIL, query: INSERT_AUDIT_TRAIL,
variables, variables,
}); });

View File

@@ -107,7 +107,6 @@
"alerttoggle": "Alert Toggle set to {{status}}", "alerttoggle": "Alert Toggle set to {{status}}",
"appointmentcancel": "Appointment canceled. Lost Reason: {{lost_sale_reason}}.", "appointmentcancel": "Appointment canceled. Lost Reason: {{lost_sale_reason}}.",
"appointmentinsert": "Appointment created. Appointment Date: {{start}}.", "appointmentinsert": "Appointment created. Appointment Date: {{start}}.",
"billdeleted": "Bill with invoice number {{invoice_number}} deleted.",
"billposted": "Bill with invoice number {{invoice_number}} posted.", "billposted": "Bill with invoice number {{invoice_number}} posted.",
"billupdated": "Bill with invoice number {{invoice_number}} updated.", "billupdated": "Bill with invoice number {{invoice_number}} updated.",
"failedpayment": "Failed payment", "failedpayment": "Failed payment",
@@ -115,7 +114,6 @@
"jobassignmentremoved": "Employee assignment removed for {{operation}}", "jobassignmentremoved": "Employee assignment removed for {{operation}}",
"jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.", "jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.",
"jobconverted": "Job converted and assigned number {{ro_number}}.", "jobconverted": "Job converted and assigned number {{ro_number}}.",
"jobexported": "Job has been exported.",
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.", "jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
"jobimported": "Job imported.", "jobimported": "Job imported.",
"jobinproductionchange": "Job production status set to {{inproduction}}", "jobinproductionchange": "Job production status set to {{inproduction}}",
@@ -128,9 +126,7 @@
"jobspartsorder": "Parts order {{order_number}} added to Job.", "jobspartsorder": "Parts order {{order_number}} added to Job.",
"jobspartsreturn": "Parts return {{order_number}} added to Job.", "jobspartsreturn": "Parts return {{order_number}} added to Job.",
"jobstatuschange": "Job status changed to {{status}}.", "jobstatuschange": "Job status changed to {{status}}.",
"jobsupplement": "Job supplement imported.", "jobsupplement": "Job supplement imported."
"jobsuspend": "Suspend Toggle set to {{status}}",
"jobvoid": "Job has been voided."
} }
}, },
"billlines": { "billlines": {
@@ -259,7 +255,6 @@
"saving": "Error encountered while saving. {{message}}" "saving": "Error encountered while saving. {{message}}"
}, },
"fields": { "fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
"address1": "Address 1", "address1": "Address 1",
"address2": "Address 2", "address2": "Address 2",
"appt_alt_transport": "Appointment Alternative Transportation Options", "appt_alt_transport": "Appointment Alternative Transportation Options",
@@ -478,6 +473,7 @@
"editaccess": "Users -> Edit access" "editaccess": "Users -> Edit access"
} }
}, },
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
"responsibilitycenter": "Responsibility Center", "responsibilitycenter": "Responsibility Center",
"responsibilitycenter_accountdesc": "Account Description", "responsibilitycenter_accountdesc": "Account Description",
"responsibilitycenter_accountitem": "Item", "responsibilitycenter_accountitem": "Item",
@@ -608,7 +604,7 @@
"dms": { "dms": {
"cdk": { "cdk": {
"controllist": "Control Number List", "controllist": "Control Number List",
"payers": "Payers" "payers": "CDK Payers"
}, },
"cdk_dealerid": "CDK Dealer ID", "cdk_dealerid": "CDK Dealer ID",
"pbs_serialnumber": "PBS Serial Number", "pbs_serialnumber": "PBS Serial Number",
@@ -751,7 +747,6 @@
"driverinformation": "Driver's Information", "driverinformation": "Driver's Information",
"findcontract": "Find Contract", "findcontract": "Find Contract",
"findermodal": "Contract Finder", "findermodal": "Contract Finder",
"insuranceexpired": "The courtesy car insurance expires before the car is expected to return.",
"noteconvertedfrom": "R.O. created from converted Courtesy Car Contract {{agreementnumber}}.", "noteconvertedfrom": "R.O. created from converted Courtesy Car Contract {{agreementnumber}}.",
"populatefromjob": "Populate from Job", "populatefromjob": "Populate from Job",
"rates": "Contract Rates", "rates": "Contract Rates",
@@ -843,27 +838,20 @@
"creating": "Error creating survey {{message}}", "creating": "Error creating survey {{message}}",
"notconfigured": "You do not have any current CSI Question Sets configured.", "notconfigured": "You do not have any current CSI Question Sets configured.",
"notfoundsubtitle": "We were unable to find a survey using the link you provided. Please ensure the URL is correct or reach out to your shop for more help.", "notfoundsubtitle": "We were unable to find a survey using the link you provided. Please ensure the URL is correct or reach out to your shop for more help.",
"notfoundtitle": "No survey found.", "notfoundtitle": "No survey found."
"surveycompletesubtitle": "This survey was already completed on {{date}}.",
"surveycompletetitle": "Survey previously completed"
}, },
"fields": { "fields": {
"completedon": "Completed On", "completedon": "Completed On",
"created_at": "Created At", "created_at": "Created At"
"surveyid": "Survey ID {{surveyId}}",
"validuntil": "Valid Until"
}, },
"labels": { "labels": {
"copyright": "Copyright © $t(titles.app). All Rights Reserved.", "nologgedinuser": "Please log out of ImEX Online",
"greeting": "Hi {{name}}!", "nologgedinuser_sub": "Users of ImEX Online cannot complete CSI surveys while logged in. Please log out and try again.",
"intro": "At {{shopname}}, we value your feedback. We would love to hear what you have to say. Please fill out the form below.",
"nologgedinuser": "Please log out of $t(titles.app)",
"nologgedinuser_sub": "Users of $t(titles.app) cannot complete CSI surveys while logged in. Please log out and try again.",
"noneselected": "No response selected.", "noneselected": "No response selected.",
"title": "Customer Satisfaction Survey" "title": "Customer Satisfaction Survey"
}, },
"successes": { "successes": {
"created": "CSI created successfully.", "created": "CSI created successfully. ",
"submitted": "Your responses have been submitted successfully.", "submitted": "Your responses have been submitted successfully.",
"submittedsub": "Your input is highly appreciated." "submittedsub": "Your input is highly appreciated."
} }
@@ -879,7 +867,6 @@
"labels": { "labels": {
"bodyhrs": "Body Hrs", "bodyhrs": "Body Hrs",
"dollarsinproduction": "Dollars in Production", "dollarsinproduction": "Dollars in Production",
"phone": "Phone",
"prodhrs": "Production Hrs", "prodhrs": "Production Hrs",
"refhrs": "Refinish Hrs" "refhrs": "Refinish Hrs"
}, },
@@ -895,11 +882,8 @@
"productiondollars": "Total Dollars in Production", "productiondollars": "Total Dollars in Production",
"productionhours": "Total Hours in Production", "productionhours": "Total Hours in Production",
"projectedmonthlysales": "Projected Monthly Sales", "projectedmonthlysales": "Projected Monthly Sales",
"scheduledindate": "Sheduled In Today: {{date}}", "scheduledintoday": "Sheduled In Today: {{date}}",
"scheduledintoday": "Sheduled In Today", "scheduledouttoday": "Sheduled Out Today: {{date}}"
"scheduledoutdate": "Sheduled Out Today: {{date}}",
"scheduledouttoday": "Sheduled Out Today",
"joblifecycle": "Job Lifecycle"
} }
}, },
"dms": { "dms": {
@@ -1237,15 +1221,7 @@
"relative_end": "Relative End", "relative_end": "Relative End",
"relative_start": "Relative Start", "relative_start": "Relative Start",
"start": "Start", "start": "Start",
"value": "Value", "value": "Value"
"status": "Status",
"percentage": "Percentage",
"human_readable": "Human Readable",
"status_count": "In Status"
},
"titles": {
"dashboard": "Job Lifecycle",
"top_durations": "Top Durations"
}, },
"content": { "content": {
"current_status_accumulated_time": "Current Status Accumulated Time", "current_status_accumulated_time": "Current Status Accumulated Time",
@@ -1257,9 +1233,7 @@
"title": "Job Lifecycle Component", "title": "Job Lifecycle Component",
"title_durations": "Historical Status Durations", "title_durations": "Historical Status Durations",
"title_loading": "Loading", "title_loading": "Loading",
"title_transitions": "Transitions", "title_transitions": "Transitions"
"calculated_based_on": "Calculated based on",
"jobs_in_since": "Jobs in since"
}, },
"errors": { "errors": {
"fetch": "Error getting Job Lifecycle Data" "fetch": "Error getting Job Lifecycle Data"
@@ -1751,7 +1725,6 @@
"ca_gst_all_if_null": "If the Job is marked as a \"GST Registrant\" and this value is set to $0, the customer will be responsible for paying all of the GST by default. ", "ca_gst_all_if_null": "If the Job is marked as a \"GST Registrant\" and this value is set to $0, the customer will be responsible for paying all of the GST by default. ",
"calc_repair_days": "Calculated Repair Days", "calc_repair_days": "Calculated Repair Days",
"calc_repair_days_tt": "This is the approximate number of days required to complete the repair according to the target touch time in your shop configuration (current set to {{target_touchtime}}).", "calc_repair_days_tt": "This is the approximate number of days required to complete the repair according to the target touch time in your shop configuration (current set to {{target_touchtime}}).",
"calc_scheuled_completion": "Calculate Scheduled Completion",
"cards": { "cards": {
"customer": "Customer Information", "customer": "Customer Information",
"damage": "Area of Damage", "damage": "Area of Damage",
@@ -1834,7 +1807,6 @@
"job": "Job Details", "job": "Job Details",
"jobcosting": "Job Costing", "jobcosting": "Job Costing",
"jobtotals": "Job Totals", "jobtotals": "Job Totals",
"labor_hrs": "B/P/T Hrs",
"labor_rates_subtotal": "Labor Rates Subtotal", "labor_rates_subtotal": "Labor Rates Subtotal",
"laborallocations": "Labor Allocations", "laborallocations": "Labor Allocations",
"labortotals": "Labor Totals", "labortotals": "Labor Totals",
@@ -1850,7 +1822,6 @@
"override_header": "Override estimate header on import?", "override_header": "Override estimate header on import?",
"ownerassociation": "Owner Association", "ownerassociation": "Owner Association",
"parts": "Parts", "parts": "Parts",
"parts_lines": "Parts Lines",
"parts_received": "Parts Rec.", "parts_received": "Parts Rec.",
"parts_tax_rates": "Parts Tax rates", "parts_tax_rates": "Parts Tax rates",
"partsfilter": "Parts Only", "partsfilter": "Parts Only",
@@ -1912,7 +1883,6 @@
"total_sales": "Total Sales", "total_sales": "Total Sales",
"totals": "Totals", "totals": "Totals",
"unvoidnote": "This Job was unvoided.", "unvoidnote": "This Job was unvoided.",
"update_scheduled_completion": "Update Scheduled Completion?",
"vehicle_info": "Vehicle", "vehicle_info": "Vehicle",
"vehicleassociation": "Vehicle Association", "vehicleassociation": "Vehicle Association",
"viewallocations": "View Allocations", "viewallocations": "View Allocations",
@@ -2429,7 +2399,6 @@
"invoice_total_payable": "Invoice (Total Payable)", "invoice_total_payable": "Invoice (Total Payable)",
"iou_form": "IOU Form", "iou_form": "IOU Form",
"job_costing_ro": "Job Costing", "job_costing_ro": "Job Costing",
"job_lifecycle_ro": "Job Lifecycle",
"job_notes": "Job Notes", "job_notes": "Job Notes",
"key_tag": "Key Tag", "key_tag": "Key Tag",
"labels": { "labels": {
@@ -2595,18 +2564,6 @@
"generate": "Generate" "generate": "Generate"
}, },
"labels": { "labels": {
"advanced_filters": "Advanced Filters and Sorters",
"advanced_filters_false": "False",
"advanced_filters_filter_field": "Field",
"advanced_filters_filter_operator": "Operator",
"advanced_filters_filter_value": "Value",
"advanced_filters_filters": "Filters",
"advanced_filters_hide": "Hide",
"advanced_filters_show": "Show",
"advanced_filters_sorter_direction": "Direction",
"advanced_filters_sorter_field": "Field",
"advanced_filters_sorters": "Sorters",
"advanced_filters_true": "True",
"dates": "Dates", "dates": "Dates",
"employee": "Employee", "employee": "Employee",
"filterson": "Filters on {{object}}: {{field}}", "filterson": "Filters on {{object}}: {{field}}",
@@ -2688,8 +2645,6 @@
"job_costing_ro_date_summary": "Job Costing by RO - Summary", "job_costing_ro_date_summary": "Job Costing by RO - Summary",
"job_costing_ro_estimator": "Job Costing by Estimator", "job_costing_ro_estimator": "Job Costing by Estimator",
"job_costing_ro_ins_co": "Job Costing by RO Source", "job_costing_ro_ins_co": "Job Costing by RO Source",
"job_lifecycle_date_detail": "Job Lifecycle by Date - Detail",
"job_lifecycle_date_summary": "Job Lifecycle by Date - Summary",
"jobs_completed_not_invoiced": "Jobs Completed not Invoiced", "jobs_completed_not_invoiced": "Jobs Completed not Invoiced",
"jobs_invoiced_not_exported": "Jobs Invoiced not Exported", "jobs_invoiced_not_exported": "Jobs Invoiced not Exported",
"jobs_reconcile": "Parts/Sublet/Labor Reconciliation", "jobs_reconcile": "Parts/Sublet/Labor Reconciliation",
@@ -2779,7 +2734,6 @@
"allemployeetimetickets": "All Employee Time Tickets", "allemployeetimetickets": "All Employee Time Tickets",
"asoftodaytarget": "As of Today", "asoftodaytarget": "As of Today",
"body": "Body", "body": "Body",
"bodyabbrev": "B",
"bodycharttitle": "Body Targets vs Actual", "bodycharttitle": "Body Targets vs Actual",
"calendarperiod": "Periods based on calendar weeks/months.", "calendarperiod": "Periods based on calendar weeks/months.",
"combinedcharttitle": "Combined Targets vs Actual", "combinedcharttitle": "Combined Targets vs Actual",
@@ -2796,7 +2750,6 @@
"productivestatistics": "Productive Hours Statistics", "productivestatistics": "Productive Hours Statistics",
"productivetimeticketsoverdate": "Productive Hours over Selected Dates", "productivetimeticketsoverdate": "Productive Hours over Selected Dates",
"refinish": "Refinish", "refinish": "Refinish",
"refinishabbrev": "R",
"refinishcharttitle": "Refinish Targets vs Actual", "refinishcharttitle": "Refinish Targets vs Actual",
"targets": "Targets", "targets": "Targets",
"thismonth": "This Month", "thismonth": "This Month",
@@ -2804,7 +2757,6 @@
"timetickets": "Time Tickets", "timetickets": "Time Tickets",
"timeticketsemployee": "Time Tickets by Employee", "timeticketsemployee": "Time Tickets by Employee",
"todateactual": "Actual (MTD)", "todateactual": "Actual (MTD)",
"total": "Total",
"totalhrs": "Total Hours", "totalhrs": "Total Hours",
"totaloverperiod": "Total over Selected Dates", "totaloverperiod": "Total over Selected Dates",
"weeklyactual": "Actual (W)", "weeklyactual": "Actual (W)",

View File

@@ -107,7 +107,6 @@
"alerttoggle": "", "alerttoggle": "",
"appointmentcancel": "", "appointmentcancel": "",
"appointmentinsert": "", "appointmentinsert": "",
"billdeleted": "",
"billposted": "", "billposted": "",
"billupdated": "", "billupdated": "",
"failedpayment": "", "failedpayment": "",
@@ -115,7 +114,6 @@
"jobassignmentremoved": "", "jobassignmentremoved": "",
"jobchecklist": "", "jobchecklist": "",
"jobconverted": "", "jobconverted": "",
"jobexported": "",
"jobfieldchanged": "", "jobfieldchanged": "",
"jobimported": "", "jobimported": "",
"jobinproductionchange": "", "jobinproductionchange": "",
@@ -128,9 +126,7 @@
"jobspartsorder": "", "jobspartsorder": "",
"jobspartsreturn": "", "jobspartsreturn": "",
"jobstatuschange": "", "jobstatuschange": "",
"jobsupplement": "", "jobsupplement": ""
"jobsuspend": "",
"jobvoid": ""
} }
}, },
"billlines": { "billlines": {
@@ -259,7 +255,6 @@
"saving": "" "saving": ""
}, },
"fields": { "fields": {
"ReceivableCustomField": "",
"address1": "", "address1": "",
"address2": "", "address2": "",
"appt_alt_transport": "", "appt_alt_transport": "",
@@ -478,6 +473,7 @@
"editaccess": "" "editaccess": ""
} }
}, },
"ReceivableCustomField": "",
"responsibilitycenter": "", "responsibilitycenter": "",
"responsibilitycenter_accountdesc": "", "responsibilitycenter_accountdesc": "",
"responsibilitycenter_accountitem": "", "responsibilitycenter_accountitem": "",
@@ -751,7 +747,6 @@
"driverinformation": "", "driverinformation": "",
"findcontract": "", "findcontract": "",
"findermodal": "", "findermodal": "",
"insuranceexpired": "",
"noteconvertedfrom": "", "noteconvertedfrom": "",
"populatefromjob": "", "populatefromjob": "",
"rates": "", "rates": "",
@@ -843,20 +838,13 @@
"creating": "", "creating": "",
"notconfigured": "", "notconfigured": "",
"notfoundsubtitle": "", "notfoundsubtitle": "",
"notfoundtitle": "", "notfoundtitle": ""
"surveycompletesubtitle": "",
"surveycompletetitle": ""
}, },
"fields": { "fields": {
"completedon": "", "completedon": "",
"created_at": "", "created_at": ""
"surveyid": "",
"validuntil": ""
}, },
"labels": { "labels": {
"copyright": "",
"greeting": "",
"intro": "",
"nologgedinuser": "", "nologgedinuser": "",
"nologgedinuser_sub": "", "nologgedinuser_sub": "",
"noneselected": "", "noneselected": "",
@@ -879,7 +867,6 @@
"labels": { "labels": {
"bodyhrs": "", "bodyhrs": "",
"dollarsinproduction": "", "dollarsinproduction": "",
"phone": "",
"prodhrs": "", "prodhrs": "",
"refhrs": "" "refhrs": ""
}, },
@@ -895,11 +882,8 @@
"productiondollars": "", "productiondollars": "",
"productionhours": "", "productionhours": "",
"projectedmonthlysales": "", "projectedmonthlysales": "",
"scheduledindate": "",
"scheduledintoday": "", "scheduledintoday": "",
"scheduledoutdate": "", "scheduledouttoday": ""
"scheduledouttoday": "",
"joblifecycle": ""
} }
}, },
"dms": { "dms": {
@@ -1237,15 +1221,7 @@
"relative_end": "", "relative_end": "",
"relative_start": "", "relative_start": "",
"start": "", "start": "",
"value": "", "value": ""
"status": "",
"percentage": "",
"human_readable": "",
"status_count": ""
},
"titles": {
"dashboard": "",
"top_durations": ""
}, },
"content": { "content": {
"current_status_accumulated_time": "", "current_status_accumulated_time": "",
@@ -1257,9 +1233,7 @@
"title": "", "title": "",
"title_durations": "", "title_durations": "",
"title_loading": "", "title_loading": "",
"title_transitions": "", "title_transitions": ""
"calculated_based_on": "",
"jobs_in_since": ""
}, },
"errors": { "errors": {
"fetch": "Error al obtener los datos del ciclo de vida del trabajo" "fetch": "Error al obtener los datos del ciclo de vida del trabajo"
@@ -1751,7 +1725,6 @@
"ca_gst_all_if_null": "", "ca_gst_all_if_null": "",
"calc_repair_days": "", "calc_repair_days": "",
"calc_repair_days_tt": "", "calc_repair_days_tt": "",
"calc_scheuled_completion": "",
"cards": { "cards": {
"customer": "Información al cliente", "customer": "Información al cliente",
"damage": "Área de Daño", "damage": "Área de Daño",
@@ -1834,7 +1807,6 @@
"job": "", "job": "",
"jobcosting": "", "jobcosting": "",
"jobtotals": "", "jobtotals": "",
"labor_hrs": "",
"labor_rates_subtotal": "", "labor_rates_subtotal": "",
"laborallocations": "", "laborallocations": "",
"labortotals": "", "labortotals": "",
@@ -1850,7 +1822,6 @@
"override_header": "¿Anular encabezado estimado al importar?", "override_header": "¿Anular encabezado estimado al importar?",
"ownerassociation": "", "ownerassociation": "",
"parts": "Partes", "parts": "Partes",
"parts_lines": "",
"parts_received": "", "parts_received": "",
"parts_tax_rates": "", "parts_tax_rates": "",
"partsfilter": "", "partsfilter": "",
@@ -1912,7 +1883,6 @@
"total_sales": "", "total_sales": "",
"totals": "", "totals": "",
"unvoidnote": "", "unvoidnote": "",
"update_scheduled_completion": "",
"vehicle_info": "Vehículo", "vehicle_info": "Vehículo",
"vehicleassociation": "", "vehicleassociation": "",
"viewallocations": "", "viewallocations": "",
@@ -2429,7 +2399,6 @@
"invoice_total_payable": "", "invoice_total_payable": "",
"iou_form": "", "iou_form": "",
"job_costing_ro": "", "job_costing_ro": "",
"job_lifecycle_ro": "",
"job_notes": "", "job_notes": "",
"key_tag": "", "key_tag": "",
"labels": { "labels": {
@@ -2595,18 +2564,6 @@
"generate": "" "generate": ""
}, },
"labels": { "labels": {
"advanced_filters": "",
"advanced_filters_false": "",
"advanced_filters_filter_field": "",
"advanced_filters_filter_operator": "",
"advanced_filters_filter_value": "",
"advanced_filters_filters": "",
"advanced_filters_hide": "",
"advanced_filters_show": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_sorter_field": "",
"advanced_filters_sorters": "",
"advanced_filters_true": "",
"dates": "", "dates": "",
"employee": "", "employee": "",
"filterson": "", "filterson": "",
@@ -2688,8 +2645,6 @@
"job_costing_ro_date_summary": "", "job_costing_ro_date_summary": "",
"job_costing_ro_estimator": "", "job_costing_ro_estimator": "",
"job_costing_ro_ins_co": "", "job_costing_ro_ins_co": "",
"job_lifecycle_date_detail": "",
"job_lifecycle_date_summary": "",
"jobs_completed_not_invoiced": "", "jobs_completed_not_invoiced": "",
"jobs_invoiced_not_exported": "", "jobs_invoiced_not_exported": "",
"jobs_reconcile": "", "jobs_reconcile": "",
@@ -2779,7 +2734,6 @@
"allemployeetimetickets": "", "allemployeetimetickets": "",
"asoftodaytarget": "", "asoftodaytarget": "",
"body": "", "body": "",
"bodyabbrev": "",
"bodycharttitle": "", "bodycharttitle": "",
"calendarperiod": "", "calendarperiod": "",
"combinedcharttitle": "", "combinedcharttitle": "",
@@ -2796,7 +2750,6 @@
"productivestatistics": "", "productivestatistics": "",
"productivetimeticketsoverdate": "", "productivetimeticketsoverdate": "",
"refinish": "", "refinish": "",
"refinishabbrev": "",
"refinishcharttitle": "", "refinishcharttitle": "",
"targets": "", "targets": "",
"thismonth": "", "thismonth": "",
@@ -2804,7 +2757,6 @@
"timetickets": "", "timetickets": "",
"timeticketsemployee": "", "timeticketsemployee": "",
"todateactual": "", "todateactual": "",
"total": "",
"totalhrs": "", "totalhrs": "",
"totaloverperiod": "", "totaloverperiod": "",
"weeklyactual": "", "weeklyactual": "",

View File

@@ -107,7 +107,6 @@
"alerttoggle": "", "alerttoggle": "",
"appointmentcancel": "", "appointmentcancel": "",
"appointmentinsert": "", "appointmentinsert": "",
"billdeleted": "",
"billposted": "", "billposted": "",
"billupdated": "", "billupdated": "",
"failedpayment": "", "failedpayment": "",
@@ -115,7 +114,6 @@
"jobassignmentremoved": "", "jobassignmentremoved": "",
"jobchecklist": "", "jobchecklist": "",
"jobconverted": "", "jobconverted": "",
"jobexported": "",
"jobfieldchanged": "", "jobfieldchanged": "",
"jobimported": "", "jobimported": "",
"jobinproductionchange": "", "jobinproductionchange": "",
@@ -128,9 +126,7 @@
"jobspartsorder": "", "jobspartsorder": "",
"jobspartsreturn": "", "jobspartsreturn": "",
"jobstatuschange": "", "jobstatuschange": "",
"jobsupplement": "", "jobsupplement": ""
"jobsuspend": "",
"jobvoid": ""
} }
}, },
"billlines": { "billlines": {
@@ -259,7 +255,6 @@
"saving": "" "saving": ""
}, },
"fields": { "fields": {
"ReceivableCustomField": "",
"address1": "", "address1": "",
"address2": "", "address2": "",
"appt_alt_transport": "", "appt_alt_transport": "",
@@ -478,6 +473,7 @@
"editaccess": "" "editaccess": ""
} }
}, },
"ReceivableCustomField": "",
"responsibilitycenter": "", "responsibilitycenter": "",
"responsibilitycenter_accountdesc": "", "responsibilitycenter_accountdesc": "",
"responsibilitycenter_accountitem": "", "responsibilitycenter_accountitem": "",
@@ -751,7 +747,6 @@
"driverinformation": "", "driverinformation": "",
"findcontract": "", "findcontract": "",
"findermodal": "", "findermodal": "",
"insuranceexpired": "",
"noteconvertedfrom": "", "noteconvertedfrom": "",
"populatefromjob": "", "populatefromjob": "",
"rates": "", "rates": "",
@@ -843,20 +838,13 @@
"creating": "", "creating": "",
"notconfigured": "", "notconfigured": "",
"notfoundsubtitle": "", "notfoundsubtitle": "",
"notfoundtitle": "", "notfoundtitle": ""
"surveycompletesubtitle": "",
"surveycompletetitle": ""
}, },
"fields": { "fields": {
"completedon": "", "completedon": "",
"created_at": "", "created_at": ""
"surveyid": "",
"validuntil": ""
}, },
"labels": { "labels": {
"copyright": "",
"greeting": "",
"intro": "",
"nologgedinuser": "", "nologgedinuser": "",
"nologgedinuser_sub": "", "nologgedinuser_sub": "",
"noneselected": "", "noneselected": "",
@@ -879,7 +867,6 @@
"labels": { "labels": {
"bodyhrs": "", "bodyhrs": "",
"dollarsinproduction": "", "dollarsinproduction": "",
"phone": "",
"prodhrs": "", "prodhrs": "",
"refhrs": "" "refhrs": ""
}, },
@@ -895,9 +882,7 @@
"productiondollars": "", "productiondollars": "",
"productionhours": "", "productionhours": "",
"projectedmonthlysales": "", "projectedmonthlysales": "",
"scheduledindate": "",
"scheduledintoday": "", "scheduledintoday": "",
"scheduledoutdate": "",
"scheduledouttoday": "" "scheduledouttoday": ""
} }
}, },
@@ -1236,15 +1221,7 @@
"relative_end": "", "relative_end": "",
"relative_start": "", "relative_start": "",
"start": "", "start": "",
"value": "", "value": ""
"status": "",
"percentage": "",
"human_readable": "",
"status_count": ""
},
"titles": {
"dashboard": "",
"top_durations": ""
}, },
"content": { "content": {
"current_status_accumulated_time": "", "current_status_accumulated_time": "",
@@ -1256,10 +1233,7 @@
"title": "", "title": "",
"title_durations": "", "title_durations": "",
"title_loading": "", "title_loading": "",
"title_transitions": "", "title_transitions": ""
"calculated_based_on": "",
"jobs_in_since": "",
"joblifecycle": ""
}, },
"errors": { "errors": {
"fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches" "fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches"
@@ -1751,7 +1725,6 @@
"ca_gst_all_if_null": "", "ca_gst_all_if_null": "",
"calc_repair_days": "", "calc_repair_days": "",
"calc_repair_days_tt": "", "calc_repair_days_tt": "",
"calc_scheuled_completion": "",
"cards": { "cards": {
"customer": "Informations client", "customer": "Informations client",
"damage": "Zone de dommages", "damage": "Zone de dommages",
@@ -1834,7 +1807,6 @@
"job": "", "job": "",
"jobcosting": "", "jobcosting": "",
"jobtotals": "", "jobtotals": "",
"labor_hrs": "",
"labor_rates_subtotal": "", "labor_rates_subtotal": "",
"laborallocations": "", "laborallocations": "",
"labortotals": "", "labortotals": "",
@@ -1850,7 +1822,6 @@
"override_header": "Remplacer l'en-tête d'estimation à l'importation?", "override_header": "Remplacer l'en-tête d'estimation à l'importation?",
"ownerassociation": "", "ownerassociation": "",
"parts": "les pièces", "parts": "les pièces",
"parts_lines": "",
"parts_received": "", "parts_received": "",
"parts_tax_rates": "", "parts_tax_rates": "",
"partsfilter": "", "partsfilter": "",
@@ -1912,7 +1883,6 @@
"total_sales": "", "total_sales": "",
"totals": "", "totals": "",
"unvoidnote": "", "unvoidnote": "",
"update_scheduled_completion": "",
"vehicle_info": "Véhicule", "vehicle_info": "Véhicule",
"vehicleassociation": "", "vehicleassociation": "",
"viewallocations": "", "viewallocations": "",
@@ -2429,7 +2399,6 @@
"invoice_total_payable": "", "invoice_total_payable": "",
"iou_form": "", "iou_form": "",
"job_costing_ro": "", "job_costing_ro": "",
"job_lifecycle_ro": "",
"job_notes": "", "job_notes": "",
"key_tag": "", "key_tag": "",
"labels": { "labels": {
@@ -2595,18 +2564,6 @@
"generate": "" "generate": ""
}, },
"labels": { "labels": {
"advanced_filters": "",
"advanced_filters_false": "",
"advanced_filters_filter_field": "",
"advanced_filters_filter_operator": "",
"advanced_filters_filter_value": "",
"advanced_filters_filters": "",
"advanced_filters_hide": "",
"advanced_filters_show": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_sorter_field": "",
"advanced_filters_sorters": "",
"advanced_filters_true": "",
"dates": "", "dates": "",
"employee": "", "employee": "",
"filterson": "", "filterson": "",
@@ -2688,8 +2645,6 @@
"job_costing_ro_date_summary": "", "job_costing_ro_date_summary": "",
"job_costing_ro_estimator": "", "job_costing_ro_estimator": "",
"job_costing_ro_ins_co": "", "job_costing_ro_ins_co": "",
"job_lifecycle_date_detail": "",
"job_lifecycle_date_summary": "",
"jobs_completed_not_invoiced": "", "jobs_completed_not_invoiced": "",
"jobs_invoiced_not_exported": "", "jobs_invoiced_not_exported": "",
"jobs_reconcile": "", "jobs_reconcile": "",
@@ -2779,7 +2734,6 @@
"allemployeetimetickets": "", "allemployeetimetickets": "",
"asoftodaytarget": "", "asoftodaytarget": "",
"body": "", "body": "",
"bodyabbrev": "",
"bodycharttitle": "", "bodycharttitle": "",
"calendarperiod": "", "calendarperiod": "",
"combinedcharttitle": "", "combinedcharttitle": "",
@@ -2796,7 +2750,6 @@
"productivestatistics": "", "productivestatistics": "",
"productivetimeticketsoverdate": "", "productivetimeticketsoverdate": "",
"refinish": "", "refinish": "",
"refinishabbrev": "",
"refinishcharttitle": "", "refinishcharttitle": "",
"targets": "", "targets": "",
"thismonth": "", "thismonth": "",
@@ -2804,7 +2757,6 @@
"timetickets": "", "timetickets": "",
"timeticketsemployee": "", "timeticketsemployee": "",
"todateactual": "", "todateactual": "",
"total": "",
"totalhrs": "", "totalhrs": "",
"totaloverperiod": "", "totaloverperiod": "",
"weeklyactual": "", "weeklyactual": "",

View File

@@ -20,8 +20,6 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }), i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }),
appointmentinsert: (start) => appointmentinsert: (start) =>
i18n.t("audit_trail.messages.appointmentinsert", { start }), i18n.t("audit_trail.messages.appointmentinsert", { start }),
billdeleted: (invoice_number) =>
i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
billposted: (invoice_number) => billposted: (invoice_number) =>
i18n.t("audit_trail.messages.billposted", { invoice_number }), i18n.t("audit_trail.messages.billposted", { invoice_number }),
billupdated: (invoice_number) => billupdated: (invoice_number) =>
@@ -35,7 +33,6 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }), i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
jobconverted: (ro_number) => jobconverted: (ro_number) =>
i18n.t("audit_trail.messages.jobconverted", { ro_number }), i18n.t("audit_trail.messages.jobconverted", { ro_number }),
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
jobfieldchange: (field, value) => jobfieldchange: (field, value) =>
i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), i18n.t("audit_trail.messages.jobfieldchanged", { field, value }),
jobimported: () => i18n.t("audit_trail.messages.jobimported"), jobimported: () => i18n.t("audit_trail.messages.jobimported"),
@@ -54,8 +51,6 @@ const AuditTrailMapping = {
jobstatuschange: (status) => jobstatuschange: (status) =>
i18n.t("audit_trail.messages.jobstatuschange", { status }), i18n.t("audit_trail.messages.jobstatuschange", { status }),
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"),
jobsuspend: (status) => i18n.t("audit_trail.messages.jobsuspend", { status }),
jobvoid: () => i18n.t("audit_trail.messages.jobvoid"),
}; };
export default AuditTrailMapping; export default AuditTrailMapping;

View File

@@ -24,13 +24,4 @@ const range = {
], ],
"Last 90 Days": [moment().add(-90, "days"), moment()], "Last 90 Days": [moment().add(-90, "days"), moment()],
}; };
// We are development, lets get crazy
if (process.env.NODE_ENV === "development") {
range["Last year"] = [
moment().subtract(1, "year"),
moment(),
];
}
export default range; export default range;

View File

@@ -1,16 +1,14 @@
import {gql} from "@apollo/client"; import { gql } from "@apollo/client";
import jsreport from "@jsreport/browser-client"; import jsreport from "@jsreport/browser-client";
import {notification} from "antd"; import { notification } from "antd";
import axios from "axios"; import axios from "axios";
import _ from "lodash"; import _ from "lodash";
import {auth} from "../firebase/firebase.utils"; import { auth } from "../firebase/firebase.utils";
import {setEmailOptions} from "../redux/email/email.actions"; import { setEmailOptions } from "../redux/email/email.actions";
import {store} from "../redux/store"; import { store } from "../redux/store";
import client from "../utils/GraphQLClient"; import client from "../utils/GraphQLClient";
import cleanAxios from "./CleanAxios"; import cleanAxios from "./CleanAxios";
import {TemplateList} from "./TemplateConstants"; import { TemplateList } from "./TemplateConstants";
import {generateTemplate} from "./graphQLmodifier";
const server = process.env.REACT_APP_REPORTS_SERVER_URL; const server = process.env.REACT_APP_REPORTS_SERVER_URL;
jsreport.serverUrl = server; jsreport.serverUrl = server;
@@ -18,11 +16,11 @@ jsreport.serverUrl = server;
const Templates = TemplateList(); const Templates = TemplateList();
export default async function RenderTemplate( export default async function RenderTemplate(
templateObject, templateObject,
bodyshop, bodyshop,
renderAsHtml = false, renderAsHtml = false,
renderAsExcel = false, renderAsExcel = false,
renderAsText = false renderAsText = false
) { ) {
if (window.jsr3) { if (window.jsr3) {
jsreport.serverUrl = "https://reports3.test.imex.online/"; jsreport.serverUrl = "https://reports3.test.imex.online/";
@@ -32,41 +30,41 @@ export default async function RenderTemplate(
jsreport.headers["Authorization"] = jsrAuth; jsreport.headers["Authorization"] = jsrAuth;
//Query assets that match the template name. Must be in format <<templateName>>.query //Query assets that match the template name. Must be in format <<templateName>>.query
let {contextData, useShopSpecificTemplate} = await fetchContextData( let { contextData, useShopSpecificTemplate } = await fetchContextData(
templateObject, templateObject,
jsrAuth jsrAuth
); );
const {ignoreCustomMargins} = Templates[templateObject.name]; const { ignoreCustomMargins } = Templates[templateObject.name];
let reportRequest = { let reportRequest = {
template: { template: {
name: useShopSpecificTemplate name: useShopSpecificTemplate
? `/${bodyshop.imexshopid}/${templateObject.name}` ? `/${bodyshop.imexshopid}/${templateObject.name}`
: `/${templateObject.name}`, : `/${templateObject.name}`,
...(renderAsHtml ...(renderAsHtml
? {} ? {}
: { : {
recipe: "chrome-pdf", recipe: "chrome-pdf",
...(!ignoreCustomMargins && { ...(!ignoreCustomMargins && {
chrome: { chrome: {
marginTop: marginTop:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin && bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36 bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin ? bodyshop.logo_img_path.headerMargin
: "36px", : "36px",
marginBottom: marginBottom:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin && bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50 bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin ? bodyshop.logo_img_path.footerMargin
: "50px", : "50px",
}, },
}), }),
}), }),
...(renderAsExcel ? {recipe: "html-to-xlsx"} : {}), ...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}),
...(renderAsText ? {recipe: "text"} : {}), ...(renderAsText ? { recipe: "text" } : {}),
}, },
data: { data: {
...contextData, ...contextData,
@@ -75,10 +73,7 @@ export default async function RenderTemplate(
headerpath: `/${bodyshop.imexshopid}/header.html`, headerpath: `/${bodyshop.imexshopid}/header.html`,
footerpath: `/${bodyshop.imexshopid}/footer.html`, footerpath: `/${bodyshop.imexshopid}/footer.html`,
bodyshop: bodyshop, bodyshop: bodyshop,
filters: templateObject?.filters, offset: bodyshop.timezone, //moment().utcOffset(),
sorters: templateObject?.sorters,
offset: bodyshop.timezone, //dayjs().utcOffset(),
defaultSorters: templateObject?.defaultSorters,
}, },
}; };
@@ -87,8 +82,8 @@ export default async function RenderTemplate(
if (!renderAsHtml) { if (!renderAsHtml) {
render.download( render.download(
(Templates[templateObject.name] && (Templates[templateObject.name] &&
Templates[templateObject.name].title) || Templates[templateObject.name].title) ||
"" ""
); );
} else { } else {
@@ -102,17 +97,17 @@ export default async function RenderTemplate(
...(!ignoreCustomMargins && { ...(!ignoreCustomMargins && {
chrome: { chrome: {
marginTop: marginTop:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin && bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36 bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin ? bodyshop.logo_img_path.headerMargin
: "36px", : "36px",
marginBottom: marginBottom:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin && bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50 bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin ? bodyshop.logo_img_path.footerMargin
: "50px", : "50px",
}, },
}), }),
}, },
@@ -126,21 +121,21 @@ export default async function RenderTemplate(
resolve({ resolve({
pdf, pdf,
filename: filename:
Templates[templateObject.name] && Templates[templateObject.name] &&
Templates[templateObject.name].title, Templates[templateObject.name].title,
html, html,
}); });
}); });
} }
} catch (error) { } catch (error) {
notification["error"]({message: JSON.stringify(error)}); notification["error"]({ message: JSON.stringify(error) });
} }
} }
export async function RenderTemplates( export async function RenderTemplates(
templateObjects, templateObjects,
bodyshop, bodyshop,
renderAsHtml = false renderAsHtml = false
) { ) {
//Query assets that match the template name. Must be in format <<templateName>>.query //Query assets that match the template name. Must be in format <<templateName>>.query
let unsortedTemplatesAndData = []; let unsortedTemplatesAndData = [];
@@ -150,17 +145,17 @@ export async function RenderTemplates(
templateObjects.forEach((template) => { templateObjects.forEach((template) => {
proms.push( proms.push(
(async () => { (async () => {
let {contextData, useShopSpecificTemplate} = await fetchContextData( let { contextData, useShopSpecificTemplate } = await fetchContextData(
template, template,
jsrAuth jsrAuth
); );
unsortedTemplatesAndData.push({ unsortedTemplatesAndData.push({
templateObject: template, templateObject: template,
contextData, contextData,
useShopSpecificTemplate, useShopSpecificTemplate,
}); });
})() })()
); );
}); });
await Promise.all(proms); await Promise.all(proms);
@@ -177,8 +172,8 @@ export async function RenderTemplates(
unsortedTemplatesAndData.sort(function (a, b) { unsortedTemplatesAndData.sort(function (a, b) {
return ( return (
templateObjects.findIndex((x) => x.name === a.templateObject.name) - templateObjects.findIndex((x) => x.name === a.templateObject.name) -
templateObjects.findIndex((x) => x.name === b.templateObject.name) templateObjects.findIndex((x) => x.name === b.templateObject.name)
); );
}); });
const templateAndData = unsortedTemplatesAndData; const templateAndData = unsortedTemplatesAndData;
@@ -188,25 +183,25 @@ export async function RenderTemplates(
let reportRequest = { let reportRequest = {
template: { template: {
name: rootTemplate.useShopSpecificTemplate name: rootTemplate.useShopSpecificTemplate
? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}` ? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}`
: `/${rootTemplate.templateObject.name}`, : `/${rootTemplate.templateObject.name}`,
...(renderAsHtml ...(renderAsHtml
? {} ? {}
: { : {
recipe: "chrome-pdf", recipe: "chrome-pdf",
chrome: { chrome: {
marginTop: marginTop:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin && bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36 bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin ? bodyshop.logo_img_path.headerMargin
: "36px", : "36px",
marginBottom: marginBottom:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin && bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50 bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin ? bodyshop.logo_img_path.footerMargin
: "50px", : "50px",
}, },
}), }),
pdfOperations: [ pdfOperations: [
@@ -223,22 +218,22 @@ export async function RenderTemplates(
template: { template: {
chrome: { chrome: {
marginTop: marginTop:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin && bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36 bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin ? bodyshop.logo_img_path.headerMargin
: "36px", : "36px",
marginBottom: marginBottom:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin && bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50 bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin ? bodyshop.logo_img_path.footerMargin
: "50px", : "50px",
}, },
name: template.useShopSpecificTemplate name: template.useShopSpecificTemplate
? `/${bodyshop.imexshopid}/${template.templateObject.name}` ? `/${bodyshop.imexshopid}/${template.templateObject.name}`
: `/${template.templateObject.name}`, : `/${template.templateObject.name}`,
...(renderAsHtml ? {} : {recipe: "chrome-pdf"}), ...(renderAsHtml ? {} : { recipe: "chrome-pdf" }),
}, },
type: "append", type: "append",
@@ -250,8 +245,8 @@ export async function RenderTemplates(
}, },
data: { data: {
...extend( ...extend(
rootTemplate.contextData, rootTemplate.contextData,
...templateAndData.map((temp) => temp.contextData) ...templateAndData.map((temp) => temp.contextData)
), ),
// ...rootTemplate.templateObject.variables, // ...rootTemplate.templateObject.variables,
@@ -271,33 +266,32 @@ export async function RenderTemplates(
return render.toString(); return render.toString();
} }
} catch (error) { } catch (error) {
notification["error"]({message: JSON.stringify(error)}); notification["error"]({ message: JSON.stringify(error) });
} }
} }
export const GenerateDocument = async ( export const GenerateDocument = async (
template, template,
messageOptions, messageOptions,
sendType, sendType,
jobid jobid
) => { ) => {
const bodyshop = store.getState().user.bodyshop; const bodyshop = store.getState().user.bodyshop;
if (sendType === "e") { if (sendType === "e") {
store.dispatch( store.dispatch(
setEmailOptions({ setEmailOptions({
jobid, jobid,
messageOptions: { messageOptions: {
...messageOptions, ...messageOptions,
to: Array.isArray(messageOptions.to) to: Array.isArray(messageOptions.to)
? messageOptions.to ? messageOptions.to
: [messageOptions.to], : [messageOptions.to],
}, },
template, template,
}) })
); );
} else if (sendType === "x") { } else if (sendType === "x") {
console.log("excel");
await RenderTemplate(template, bodyshop, false, true); await RenderTemplate(template, bodyshop, false, true);
} else if (sendType === "text") { } else if (sendType === "text") {
await RenderTemplate(template, bodyshop, false, false, true); await RenderTemplate(template, bodyshop, false, false, true);
@@ -311,74 +305,22 @@ export const GenerateDocuments = async (templates) => {
await RenderTemplates(templates, bodyshop); await RenderTemplates(templates, bodyshop);
}; };
export const fetchFilterData = async ({name}) => {
try {
const bodyshop = store.getState().user.bodyshop;
const jsrAuth = (await axios.post("/utils/jsr")).data;
jsreport.headers["FirebaseAuthorization"] =
"Bearer " + (await auth.currentUser.getIdToken());
const folders = await cleanAxios.get(`${server}/odata/folders`, {
headers: {Authorization: jsrAuth},
});
const shopSpecificFolder = folders.data.value.find(
(f) => f.name === bodyshop.imexshopid
);
const jsReportFilters = await cleanAxios.get(
`${server}/odata/assets?$filter=name eq '${name}.filters'`,
{headers: {Authorization: jsrAuth}}
);
console.log("🚀 ~ fetchFilterData ~ jsReportFilters:", jsReportFilters);
let parsedFilterData;
let useShopSpecificTemplate = false;
// let shopSpecificTemplate;
if (shopSpecificFolder) {
let shopSpecificTemplate = jsReportFilters.data.value.find(
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
);
if (shopSpecificTemplate) {
useShopSpecificTemplate = true;
parsedFilterData = atob(shopSpecificTemplate.content);
}
}
if (!parsedFilterData) {
const generalTemplate = jsReportFilters.data.value.find((f) => !f.folder);
useShopSpecificTemplate = false;
if (generalTemplate) parsedFilterData = atob(generalTemplate.content);
}
const data = JSON.parse(parsedFilterData);
return {
data,
useShopSpecificTemplate,
success: true,
}
} catch {
return {
success: false,
}
}
};
const fetchContextData = async (templateObject, jsrAuth) => { const fetchContextData = async (templateObject, jsrAuth) => {
const bodyshop = store.getState().user.bodyshop; const bodyshop = store.getState().user.bodyshop;
jsreport.headers["FirebaseAuthorization"] = jsreport.headers["FirebaseAuthorization"] =
"Bearer " + (await auth.currentUser.getIdToken()); "Bearer " + (await auth.currentUser.getIdToken());
const folders = await cleanAxios.get(`${server}/odata/folders`, { const folders = await cleanAxios.get(`${server}/odata/folders`, {
headers: {Authorization: jsrAuth}, headers: { Authorization: jsrAuth },
}); });
const shopSpecificFolder = folders.data.value.find( const shopSpecificFolder = folders.data.value.find(
(f) => f.name === bodyshop.imexshopid (f) => f.name === bodyshop.imexshopid
); );
const jsReportQueries = await cleanAxios.get( const jsReportQueries = await cleanAxios.get(
`${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`, `${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
{headers: {Authorization: jsrAuth}} { headers: { Authorization: jsrAuth } }
); );
let templateQueryToExecute; let templateQueryToExecute;
@@ -387,7 +329,7 @@ const fetchContextData = async (templateObject, jsrAuth) => {
if (shopSpecificFolder) { if (shopSpecificFolder) {
let shopSpecificTemplate = jsReportQueries.data.value.find( let shopSpecificTemplate = jsReportQueries.data.value.find(
(f) => f?.folder?.shortid === shopSpecificFolder.shortid (f) => f?.folder?.shortid === shopSpecificFolder.shortid
); );
if (shopSpecificTemplate) { if (shopSpecificTemplate) {
useShopSpecificTemplate = true; useShopSpecificTemplate = true;
@@ -401,35 +343,16 @@ const fetchContextData = async (templateObject, jsrAuth) => {
templateQueryToExecute = atob(generalTemplate.content); templateQueryToExecute = atob(generalTemplate.content);
} }
// Commented out for future revision debugging let contextData = {};
// console.log('Template Object'); if (templateQueryToExecute) {
// console.dir(templateObject); const { data } = await client.query({
// console.log('Unmodified Query'); query: gql(templateQueryToExecute),
// console.dir(templateQueryToExecute); variables: { ...templateObject.variables },
});
const hasFilters = templateObject?.filters?.length > 0; contextData = data;
const hasSorters = templateObject?.sorters?.length > 0;
const hasDefaultSorters = templateObject?.defaultSorters?.length > 0;
// We have no template filters or sorters, so we can just execute the query and return the data
if (!hasFilters && !hasSorters && !hasDefaultSorters) {
let contextData = {};
if (templateQueryToExecute) {
const {data} = await client.query({
query: gql(templateQueryToExecute),
variables: {...templateObject.variables},
});
contextData = data;
}
return {contextData, useShopSpecificTemplate};
} }
return await generateTemplate( return { contextData, useShopSpecificTemplate };
templateQueryToExecute,
templateObject,
useShopSpecificTemplate
);
}; };
//export const displayTemplateInWindow = (html) => { //export const displayTemplateInWindow = (html) => {
@@ -466,7 +389,7 @@ const fetchContextData = async (templateObject, jsrAuth) => {
function extend(o1, o2, o3) { function extend(o1, o2, o3) {
var result = {}, var result = {},
obj; obj;
for (var i = 0; i < arguments.length; i++) { for (var i = 0; i < arguments.length; i++) {
obj = arguments[i]; obj = arguments[i];
@@ -482,4 +405,4 @@ function extend(o1, o2, o3) {
} }
} }
return result; return result;
} }

View File

@@ -514,14 +514,6 @@ export const TemplateList = (type, context) => {
group: "financial", group: "financial",
dms: true, dms: true,
}, },
job_lifecycle_ro: {
title: i18n.t("printcenter.jobs.job_lifecycle_ro"),
description: "",
subject: i18n.t("printcenter.jobs.job_lifecycle_ro"),
key: "job_lifecycle_ro",
disabled: false,
group: "post",
},
} }
: {}), : {}),
...(!type || type === "job_special" ...(!type || type === "job_special"
@@ -2056,30 +2048,6 @@ export const TemplateList = (type, context) => {
datedisable: true, datedisable: true,
group: "customers", group: "customers",
}, },
job_lifecycle_date_detail: {
title: i18n.t("reportcenter.templates.job_lifecycle_date_detail"),
subject: i18n.t("reportcenter.templates.job_lifecycle_date_detail"),
key: "job_lifecycle_date_detail",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "jobs",
},
job_lifecycle_date_summary: {
title: i18n.t("reportcenter.templates.job_lifecycle_date_summary"),
subject: i18n.t("reportcenter.templates.job_lifecycle_date_summary"),
key: "job_lifecycle_date_summary",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "jobs",
},
} }
: {}), : {}),
...(!type || type === "courtesycarcontract" ...(!type || type === "courtesycarcontract"

View File

@@ -1,468 +0,0 @@
import {Kind, parse, print, visit} from "graphql";
import client from "./GraphQLClient";
import {gql} from "@apollo/client";
/* eslint-disable no-loop-func */
/**
* The available operators for filtering (string)
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
*/
const STRING_OPERATORS = [
{value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"},
{value: "_like", label: "contains"},
{value: "_nlike", label: "does not contain"},
{value: "_ilike", label: "contains case-insensitive"},
{value: "_nilike", label: "does not contain case-insensitive"},
{value: "_in", label: "in", type: "array"},
{value: "_nin", label: "not in", type: "array"}
];
/**
* The available operators for filtering (dates)
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
*/
const DATE_OPERATORS = [
{value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"},
{value: "_gt", label: "greater than"},
{value: "_lt", label: "less than"},
{value: "_gte", label: "greater than or equal"},
{value: "_lte", label: "less than or equal"},
{value: "_in", label: "in", type: "array"},
{value: "_nin", label: "not in", type: "array"}
];
/**
* The available operators for filtering (booleans)
* @type {[{label: string, value: string},{label: string, value: string}]}
*/
const BOOLEAN_OPERATORS = [
{value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"},
];
/**
* The available operators for filtering (numbers)
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
*/
const NUMBER_OPERATORS = [
{value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"},
{value: "_gt", label: "greater than"},
{value: "_lt", label: "less than"},
{value: "_gte", label: "greater than or equal"},
{value: "_lte", label: "less than or equal"},
{value: "_in", label: "in", type: "array"},
{value: "_nin", label: "not in", type: "array"}
];
/**
* The available operators for sorting
* @type {[{label: string, value: string},{label: string, value: string}]}
*/
const ORDER_BY_OPERATORS = [
{value: "asc", label: "ascending"},
{value: "desc", label: "descending"}
];
/**
* Get the available operators for filtering
* @returns {[{label: string, value: string},{label: string, value: string}]}
*/
export function getOrderOperatorsByType() {
return ORDER_BY_OPERATORS;
}
/**
* Get the available operators for filtering
* @param type
* @returns {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null]}
*/
export function getWhereOperatorsByType(type = 'string') {
const operators = {
string: STRING_OPERATORS,
number: NUMBER_OPERATORS,
boolean: BOOLEAN_OPERATORS,
bool: BOOLEAN_OPERATORS,
date: DATE_OPERATORS
};
return operators[type];
}
/**
* Parse a GraphQL query into an AST
* @param query
* @returns {DocumentNode}
*/
export function parseQuery(query) {
return parse(query);
}
/**
* Print an AST back into a GraphQL query
* @param query
* @returns {string}
*/
export function printQuery(query) {
return print(query);
}
/**
* Generate a template based on the query and object
* @param templateQueryToExecute
* @param templateObject
* @param useShopSpecificTemplate
* @returns {Promise<{contextData: {}, useShopSpecificTemplate}>}
*/
export async function generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate) {
// Advanced Filtering and Sorting modifications start here
// Parse the query and apply the filters and sorters
const ast = parseQuery(templateQueryToExecute);
if (templateObject?.filters && templateObject?.filters?.length) {
applyFilters(ast, templateObject.filters);
}
if (templateObject?.sorters && templateObject?.sorters?.length) {
applySorters(ast, templateObject.sorters);
} else if (templateObject?.defaultSorters && templateObject?.defaultSorters?.length) {
applySorters(ast, templateObject.defaultSorters);
}
const finalQuery = printQuery(ast);
// commented out for future revision debugging
// console.log('Modified Query');
// console.log(finalQuery);
let contextData = {};
if (templateQueryToExecute) {
const {data} = await client.query({
query: gql(finalQuery),
variables: {...templateObject.variables},
});
contextData = data;
}
return {contextData, useShopSpecificTemplate};
}
/**
* Apply sorters to the AST
* @param ast
* @param sorters
*/
export function applySorters(ast, sorters) {
sorters.forEach((sorter) => {
const fieldPath = sorter.field.split('.');
visit(ast, {
OperationDefinition: {
enter(node) {
// Loop through each sorter to apply it
// noinspection DuplicatedCode
let currentSelection = node; // Start with the root operation
// Navigate down the field path to the correct location
for (let i = 0; i < fieldPath.length - 1; i++) {
let found = false;
visit(currentSelection, {
Field: {
enter(node) {
if (node.name.value === fieldPath[i]) {
currentSelection = node; // Move down to the next level
found = true;
}
}
}
});
if (!found) break; // Stop if we can't find the next field in the path
}
// Apply the sorter at the correct level
if (currentSelection) {
const targetFieldName = fieldPath[fieldPath.length - 1];
let orderByArg = currentSelection.arguments.find(arg => arg.name.value === 'order_by');
if (!orderByArg) {
orderByArg = {
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'order_by'},
value: {kind: Kind.OBJECT, fields: []},
};
currentSelection.arguments.push(orderByArg);
}
const sorterField = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: targetFieldName},
value: {kind: Kind.ENUM, value: sorter.direction}, // Adjust if your schema uses a different type for sorting directions
};
// Add the new sorter condition
orderByArg.value.fields.push(sorterField);
}
}
}
});
});
}
/**
* Apply Top Level Sub to the AST
* @param node
* @param fieldPath
* @param filterField
*/
function applyTopLevelSub(node, fieldPath, filterField) {
// Find or create the where argument for the top-level subfield
let whereArg = node.selectionSet.selections
.find(selection => selection.name.value === fieldPath[0])
?.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
whereArg = {
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []},
};
const topLevelSubSelection = node.selectionSet.selections.find(selection =>
selection.name.value === fieldPath[0]
);
if (topLevelSubSelection) {
topLevelSubSelection.arguments = topLevelSubSelection.arguments || [];
topLevelSubSelection.arguments.push(whereArg);
}
}
// Correctly position the nested filter without an extra 'where'
if (fieldPath.length > 2) { // More than one level deep
let currentField = whereArg.value;
fieldPath.slice(1, -1).forEach((path, index) => {
let existingField = currentField.fields.find(f => f.name.value === path);
if (!existingField) {
existingField = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: path},
value: {kind: Kind.OBJECT, fields: []}
};
currentField.fields.push(existingField);
}
currentField = existingField.value;
});
currentField.fields.push(filterField);
} else { // Directly under the top level
whereArg.value.fields.push(filterField);
}
}
/**
* Apply filters to the AST
* @param ast
* @param filters
* @returns {ASTNode}
*/
export function applyFilters(ast, filters) {
return visit(ast, {
OperationDefinition: {
enter(node) {
filters.forEach(filter => {
const fieldPath = filter.field.split('.');
let topLevel = false;
let topLevelSub = false;
// Determine if the filter should be applied at the top level
if (fieldPath.length === 2) {
topLevel = true;
}
if (fieldPath.length > 2 && fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
topLevelSub = true;
}
// Construct the filter for a top-level application
const targetFieldName = fieldPath[fieldPath.length - 1];
let filterValue = createFilterValue(filter);
let filterField = createFilterField(targetFieldName, filter, filterValue);
if (topLevel) {
applyTopLevelFilter(node, fieldPath, filterField);
} else if (topLevelSub) {
applyTopLevelSub(node, fieldPath, filterField);
} else {
applyNestedFilter(node, fieldPath, filterField);
}
});
}
}
});
}
/**
* Create a filter value based on the filter
* @param filter
* @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}}
*/
function createFilterValue(filter) {
if (Array.isArray(filter.value)) {
// If it's an array, create a list value with the array items
return {
kind: Kind.LIST,
values: filter.value.map(item => ({
kind: getGraphQLKind(item),
value: item,
})),
};
} else {
// If it's not an array, use the existing logic
return {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
}
}
/**
* Create a filter field based on the target field and filter
* @param targetFieldName
* @param filter
* @param filterValue
* @returns {{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value: {kind: Kind.OBJECT, fields: [{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value}]}}}
*/
function createFilterField(targetFieldName, filter, filterValue) {
return {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: targetFieldName},
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: filter.operator},
value: filterValue,
}],
},
};
}
/**
* Apply a top-level filter to the AST
* @param node
* @param fieldPath
* @param filterField
*/
function applyTopLevelFilter(node, fieldPath, filterField) {
// Find or create the where argument for the top-level field
let whereArg = node.selectionSet.selections
.find(selection => selection.name.value === fieldPath[0])
?.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
whereArg = {
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []},
};
const topLevelSelection = node.selectionSet.selections.find(selection =>
selection.name.value === fieldPath[0]
);
if (topLevelSelection) {
topLevelSelection.arguments = topLevelSelection.arguments || [];
topLevelSelection.arguments.push(whereArg);
}
}
// Correctly position the nested filter without an extra 'where'
if (fieldPath.length > 2) { // More than one level deep
let currentField = whereArg.value;
fieldPath.slice(1, -1).forEach((path, index) => {
let existingField = currentField.fields.find(f => f.name.value === path);
if (!existingField) {
existingField = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: path},
value: {kind: Kind.OBJECT, fields: []}
};
currentField.fields.push(existingField);
}
currentField = existingField.value;
});
currentField.fields.push(filterField);
} else { // Directly under the top level
whereArg.value.fields.push(filterField);
}
}
/**
* Apply a nested filter to the AST
* @param node
* @param fieldPath
* @param filterField
*/
function applyNestedFilter(node, fieldPath, filterField) {
// Initialize a reference to the current selection to traverse down the AST
let currentSelection = node;
// Iterate over the fieldPath, except for the last entry, to navigate the structure
for (let i = 0; i < fieldPath.length - 1; i++) {
const fieldName = fieldPath[i];
let fieldFound = false;
// Check if the current selection has a selectionSet and selections
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
// Look for the field in the current selection's selections
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
if (selection) {
// Move down the AST to the found selection
currentSelection = selection;
fieldFound = true;
}
}
// If the field was not found in the current path, it's an issue
if (!fieldFound) {
console.error(`Field ${fieldName} not found in the current selection.`);
return; // Exit the loop and function due to error
}
}
// At this point, currentSelection should be the parent field where the filter needs to be applied
// Check if the 'where' argument already exists in the current selection
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
// If not found, create a new 'where' argument for the current selection
currentSelection.arguments.push({
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []} // Empty fields array to be populated with the filter
});
}
// Add the filter field to the 'where' clause of the current selection
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
}
/**
* Get the GraphQL kind for a value
* @param value
* @returns {Kind|Kind.INT}
*/
function getGraphQLKind(value) {
if (Array.isArray(value)) {
return Kind.LIST;
} else if (typeof value === 'number') {
return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
} else if (typeof value === 'boolean') {
return Kind.BOOLEAN;
} else if (typeof value === 'string') {
return Kind.STRING;
} else if (value instanceof Date) {
return Kind.STRING; // GraphQL does not have a Date type, so we return it as a string
}
}
/* eslint-enable no-loop-func */

View File

@@ -259,30 +259,28 @@
- active: - active:
_eq: true _eq: true
columns: columns:
- billid
- bodyshopid
- created
- id - id
- jobid - created
- operation
- new_val - new_val
- old_val - old_val
- operation
- type
- useremail - useremail
- bodyshopid
- jobid
- billid
select_permissions: select_permissions:
- role: user - role: user
permission: permission:
columns: columns:
- billid
- bodyshopid
- created
- id - id
- jobid
- new_val - new_val
- old_val - old_val
- operation - operation
- type
- useremail - useremail
- created
- billid
- bodyshopid
- jobid
filter: filter:
bodyshop: bodyshop:
associations: associations:
@@ -4200,7 +4198,7 @@
interval_sec: 10 interval_sec: 10
num_retries: 0 num_retries: 0
timeout_sec: 60 timeout_sec: 60
webhook: https://worktest.home.irony.online webhook_from_env: HASURA_API_URL
headers: headers:
- name: event-secret - name: event-secret
value_from_env: EVENT_SECRET value_from_env: EVENT_SECRET

View File

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

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