Merge branch 'rome/test-beta' into master-AIO. Initia

This commit is contained in:
Patrick Fic
2024-02-12 13:12:45 -08:00
25 changed files with 975 additions and 209 deletions

View File

@@ -182,7 +182,7 @@ jobs:
imex-test-app-build:
docker:
- image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client
steps:

View File

@@ -32669,6 +32669,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>parts_lines</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>parts_received</name>
<definition_loaded>false</definition_loaded>

View File

@@ -101,6 +101,7 @@ function BillEnterModalContainer({
location,
outstanding_returns,
inventory,
federal_tax_exempt,
...remainingValues
} = values;

View File

@@ -280,24 +280,22 @@ const CardPaymentModalComponent = ({
<Input
className="ipayfield"
data-ipayname="account"
//type="hidden"
type="hidden"
value={
payments && data && data.jobs.length > 0
? data.jobs.map((j) => j.ro_number).join(", ")
: null
}
hidden
/>
<Input
className="ipayfield"
data-ipayname="email"
// type="hidden"
type="hidden"
value={
payments && data && data.jobs.length > 0
? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea
: null
}
hidden
/>
</>
);
@@ -325,9 +323,8 @@ const CardPaymentModalComponent = ({
<Input
className="ipayfield"
data-ipayname="amount"
//type="hidden"
type="hidden"
value={totalAmountToCharge?.toFixed(2)}
hidden
/>
<Button
type="primary"

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
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, useNavigate, 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 = useNavigate();
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({
search: queryString.stringify({
...searchParams,
}),
});
};
return (
<Drawer
open={!!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

@@ -0,0 +1,209 @@
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

@@ -8,31 +8,30 @@ import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {Link, useLocation, useNavigate} from "react-router-dom";
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 {selectBodyshop} from "../../redux/user/user.selectors";
import {DateTimeFormatter, TimeAgoFormatter} from "../../utils/DateFormatter";
import {onlyUnique} from "../../utils/arrayHelper";
import {pageLimit} from "../../utils/config";
import {alphaSort, dateSort} from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import {pageLimit} from "../../utils/config";
import AlertComponent from "../alert/alert.component";
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({
bodyshop: selectBodyshop,
});
export function PartsQueuePageComponent({bodyshop}) {
export function PartsQueueListComponent({bodyshop}) {
const searchParams = queryString.parse(useLocation().search);
const {
//page,
selected,
sortcolumn,
sortorder,
statusFilters,
statusFilters
} = searchParams;
const history = useNavigate();
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
@@ -41,19 +40,10 @@ export function PartsQueuePageComponent({bodyshop}) {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
// offset: page ? (page - 1) * 25 : 0,
// limit: 25,
statuses: (statusFilters && JSON.parse(statusFilters)) ||
bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
order: [
{
[sortcolumn || "ro_number"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
],
},
});
@@ -109,6 +99,18 @@ export function PartsQueuePageComponent({bodyshop}) {
history({search: queryString.stringify(searchParams)});
};
const handleOnRowClick = (record) => {
if (record) {
if (record.id) {
history({
search: queryString.stringify({
...searchParams,
selected: record.id,
}),
});
}
}
};
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -127,7 +129,8 @@ export function PartsQueuePageComponent({bodyshop}) {
title: t("jobs.fields.owner"),
dataIndex: "ownr_ln",
key: "ownr_ln",
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => {
return record.ownerid ? (
@@ -141,6 +144,56 @@ export function PartsQueuePageComponent({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"),
dataIndex: "status",
@@ -172,23 +225,16 @@ export function PartsQueuePageComponent({bodyshop}) {
),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
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>
);
},
sorter: (a, b) =>
dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder: sortcolumn === "scheduled_completion" && sortorder,
render: (text, record) => (
<DateTimeFormatter>{record.scheduled_completion}</DateTimeFormatter>
),
},
// {
// title: t("vehicles.fields.plate_no"),
@@ -199,15 +245,8 @@ export function PartsQueuePageComponent({bodyshop}) {
// render: (text, record) => {
// 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"),
// dataIndex: "clm_total",
@@ -309,9 +348,15 @@ export function PartsQueuePageComponent({bodyshop}) {
style={{height: "100%"}}
scroll={{x: true}}
onChange={handleTableChange}
/>
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selected],
type: "radio",
}}/>
</Card>
);
}
export default connect(mapStateToProps, null)(PartsQueuePageComponent);
export default connect(mapStateToProps, null)(PartsQueueListComponent);

View File

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

View File

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

View File

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

View File

@@ -110,10 +110,9 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
export const QUERY_PARTS_QUEUE = gql`
query QUERY_PARTS_QUEUE(
$statuses: [String!]!
$offset: Int
$limit: Int
$order: [jobs_order_by!]
) {
, $offset: Int
, $limit: Int) {
jobs_aggregate(where: { _and: [{ status: { _in: $statuses } }] }) {
aggregate {
count(distinct: true)
@@ -125,7 +124,7 @@ export const QUERY_PARTS_QUEUE = gql`
}
offset: $offset
limit: $limit
order_by: $order
order_by: { ro_number: desc }
) {
ownr_fn
ownr_ln
@@ -142,7 +141,9 @@ export const QUERY_PARTS_QUEUE = gql`
v_color
vehicleid
scheduled_in
scheduled_completion
id
ins_co_nm
clm_no
ro_number
status
@@ -2056,7 +2057,7 @@ export const generate_UPDATE_JOB_KANBAN = (
// console.log("new", newChildId, newChildParent);
const oldChildQuery = `
updateOldChild: update_jobs(where: { id: { _eq: "${oldChildId}" } },
updateOldChild: update_jobs(where: { id: { _eq: "${oldChildId}" } },
_set: {kanbanparent: ${
oldChildNewParent ? `"${oldChildNewParent}"` : null
}}) {
@@ -2067,7 +2068,7 @@ export const generate_UPDATE_JOB_KANBAN = (
}`;
const movedQuery = `
updateMovedChild: update_jobs(where: { id: { _eq: "${movedId}" } },
updateMovedChild: update_jobs(where: { id: { _eq: "${movedId}" } },
_set: {kanbanparent: ${
movedNewParent ? `"${movedNewParent}"` : null
} , status: "${movedNewStatus}"}) {
@@ -2079,7 +2080,7 @@ export const generate_UPDATE_JOB_KANBAN = (
}`;
const newChildQuery = `
updateNewChild: update_jobs(where: { id: { _eq: "${newChildId}" } },
updateNewChild: update_jobs(where: { id: { _eq: "${newChildId}" } },
_set: {kanbanparent: ${newChildParent ? `"${newChildParent}"` : null}}) {
returning {
id
@@ -2440,3 +2441,163 @@ export const QUERY_JOBS_TECH_ASIGNED_TO_BY_TEAM = 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

@@ -1,88 +1,75 @@
import {useMutation, useQuery} from "@apollo/client";
//import {useMutation, useQuery } from "@apollo/client";
import {Button, Form, Layout, Result, Typography} from "antd";
import React, {useState} from "react";
import axios from "axios";
import React, {useCallback, useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {useParams} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import AlertComponent from "../../components/alert/alert.component";
import ConfigFormComponents from "../../components/config-form-components/config-form-components.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import {COMPLETE_SURVEY, QUERY_SURVEY} from "../../graphql/csi.queries";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectCurrentUser} from "../../redux/user/user.selectors";
import {DateTimeFormat} from "./../../utils/DateFormatter";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(CsiContainerPage);
export function CsiContainerPage({currentUser}) {
const {surveyId} = useParams();
const [form] = Form.useForm();
const [axiosResponse, setAxiosResponse] = useState(null);
const [submitting, setSubmitting] = useState({
loading: false,
submitted: false,
});
const {loading, error, data} = useQuery(QUERY_SURVEY, {
variables: {surveyId},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const {t} = useTranslation();
const [completeSurvey] = useMutation(COMPLETE_SURVEY);
if (loading) return <LoadingSpinner/>;
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 getAxiosData = useCallback(async () => {
try {
try {
window.$crisp.push(["do", "chat:hide"]);
} catch {
console.log("Unable to attach to crisp instance. ");
}
setSubmitting((prevSubmitting) => ({...prevSubmitting, 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,
const response = await axios.post("/csi/lookup", {
surveyId
});
setSubmitting((prevSubmitting) => ({
...prevSubmitting,
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]);
const {
relateddata: {bodyshop, job},
csiquestion: {config: csiquestions},
} = data.csi_by_pk;
useEffect(() => {
getAxiosData().catch((err) =>
console.error(
`Something went wrong fetching axios data: ${err.message || ""}`
)
);
}, [getAxiosData]);
if (currentUser && currentUser.authorized)
// Return if authorized
if (currentUser && currentUser.authorized) {
return (
<Layout
style={{height: "100vh", display: "flex", flexDirection: "column"}}
@@ -94,11 +81,68 @@ export function CsiContainerPage({currentUser}) {
/>
</Layout>
);
}
if (submitting.loading) return <LoadingSpinner/>;
const handleFinish = async (values) => {
try {
setSubmitting({...submitting, loading: true, submitting: true});
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) {
// Do something here , this is where you would return a loading box or something
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={{height: "100vh", display: "flex", flexDirection: "column"}}
>
<Layout style={{display: "flex", flexDirection: "column"}}>
<div
style={{
display: "flex",
@@ -108,24 +152,35 @@ export function CsiContainerPage({currentUser}) {
>
<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>
<img src={bodyshop.logo_img_path.src} alt={bodyshop.shopname.concat("Logo")}
height={bodyshop.logo_img_path.height}
width={bodyshop.logo_img_path.width}
/>) : null}
<div style={{margin: "2em", verticalAlign: "middle"}}>
<Typography.Title level={4} style={{margin: 0}}>
{bodyshop.shopname || ""}
</Typography.Title>
<Typography.Paragraph style={{margin: 0}}>
{`${bodyshop.address1 || ""}${bodyshop.address2 ? ", " : ""}${
bodyshop.address2 || ""
}`.trim()}
</Typography.Paragraph>
<Typography.Paragraph style={{margin: 0}}>
{`${bodyshop.city || ""}${
bodyshop.city && bodyshop.state ? ", " : ""
}${bodyshop.state || ""} ${bodyshop.zip_post || ""}`.trim()}
</Typography.Paragraph>
</div>
</div>
<Typography.Title>{t("csi.labels.title")}</Typography.Title>
<strong>{`Hi ${job.ownr_co_nm || job.ownr_fn || ""}!`}</strong>
<strong>{t("csi.labels.greeting", {
name: job.ownr_co_nm || job.ownr_fn || "",
})}</strong>
<Typography.Paragraph>
{`At ${
{t("csi.labels.intro", {
shopname:
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>
@@ -158,21 +213,42 @@ export function CsiContainerPage({currentUser}) {
}}
>
<Form form={form} onFinish={handleFinish}>
<ConfigFormComponents componentList={csiquestions}/>
{axiosResponse.csi_by_pk.valid ? (
<><ConfigFormComponents componentList={csiquestions}/>
<Button
loading={submitting.loading}
type="primary"
htmlType="submit"
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>
{`Survey ID: ${surveyId}`}
{t("csi.labels.copyright")}{" "}
{t("csi.fields.surveyid", {surveyId: surveyId})}
</Layout.Footer>
</Layout>
);
}
}

View File

@@ -1,9 +1,10 @@
import React, {useEffect} from "react";
import {useTranslation} from "react-i18next";
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 {setBreadcrumbs, setSelectedHeader,} from "../../redux/application/application.actions";
import PartsQueuePage from "./parts-queue.page.component";
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
@@ -23,7 +24,8 @@ export function PartsQueuePageContainer({setBreadcrumbs, setSelectedHeader}) {
return (
<RbacWrapper action="jobs:partsqueue">
<PartsQueuePage/>
<PartsQueueList/>
<PartsQueueDetailCard/>
</RbacWrapper>
);
}

View File

@@ -1,20 +1,17 @@
import {Col, Row} from "antd";
import {useQuery} from "@apollo/client";
import {Col, Row} from "antd";
import React, {useEffect} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {useLocation} from "react-router-dom";
import {createStructuredSelector} from "reselect";
import AlertComponent from "../../components/alert/alert.component";
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 {QUERY_CSI_RESPONSE_PAGINATED} from "../../graphql/csi.queries";
import {setBreadcrumbs, setSelectedHeader} from "../../redux/application/application.actions";
import {selectBodyshop} from "../../redux/user/user.selectors";
from "../../components/csi-response-list-paginated/csi-response-list-paginated.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import {pageLimit} from "../../utils/config";
import queryString from "query-string";
import {QUERY_CSI_RESPONSE_PAGINATED} from "../../graphql/csi.queries";
import {setBreadcrumbs, setSelectedHeader,} from "../../redux/application/application.actions";
import {selectBodyshop} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -25,29 +22,18 @@ const mapDispatchToProps = (dispatch) => ({
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
});
export function ShopCsiContainer({bodyshop, setBreadcrumbs, setSelectedHeader}) {
export function ShopCsiContainer({
bodyshop,
setBreadcrumbs,
setSelectedHeader,
}) {
const {t} = useTranslation();
const searchParams = queryString.parse(useLocation().search);
const {page, sortcolumn, sortorder} = searchParams;
const {loading, error, data, refetch} = useQuery(
QUERY_CSI_RESPONSE_PAGINATED,
{
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
offset: page ? (page - 1) * pageLimit : 0,
limit: pageLimit,
order: [
{
[sortcolumn || "completedon"]: sortorder
? sortorder === "descend"
? "desc_nulls_last"
: "asc"
: "desc_nulls_last",
},
],
},
}
);

View File

@@ -1923,6 +1923,7 @@
"override_header": "Override estimate header on import?",
"ownerassociation": "Owner Association",
"parts": "Parts",
"parts_lines": "Parts Lines",
"parts_received": "Parts Rec.",
"parts_tax_rates": "Parts Tax rates",
"partsfilter": "Parts Only",

View File

@@ -1923,6 +1923,7 @@
"override_header": "¿Anular encabezado estimado al importar?",
"ownerassociation": "",
"parts": "Partes",
"parts_lines": "",
"parts_received": "",
"parts_tax_rates": "",
"partsfilter": "",

View File

@@ -1923,6 +1923,7 @@
"override_header": "Remplacer l'en-tête d'estimation à l'importation?",
"ownerassociation": "",
"parts": "les pièces",
"parts_lines": "",
"parts_received": "",
"parts_tax_rates": "",
"partsfilter": "",

View File

@@ -84,6 +84,7 @@ app.use('/adm', require("./server/routes/adminRoutes"));
app.use('/tech', require("./server/routes/techRoutes"));
app.use('/intellipay', require("./server/routes/intellipayRoutes"));
app.use('/cdk', require("./server/routes/cdkRoutes"));
app.use('/csi', require("./server/routes/csiRoutes"));
app.use('/payroll', require("./server/routes/payrollRoutes"));
// Default route for forbidden access

2
server/csi/csi.js Normal file
View File

@@ -0,0 +1,2 @@
exports.lookup = require("./lookup").default;
exports.submit = require("./submit").default;

24
server/csi/lookup.js Normal file
View File

@@ -0,0 +1,24 @@
const path = require("path");
const queries = require("../graphql-client/queries");
const logger = require("../utils/logger");
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
const client = require("../graphql-client/graphql-client").client;
exports.default = async (req, res) => {
try {
logger.log("csi-surveyID-lookup", "DEBUG", "csi", req.body.surveyId, null);
const gql_response = await client.request(queries.QUERY_SURVEY, {
surveyId: req.body.surveyId,
});
res.status(200).json(gql_response);
} catch (error) {
logger.log("csi-surveyID-lookup", "ERROR", "csi", req.body.surveyId, error);
res.status(400).json(error);
}
};

29
server/csi/submit.js Normal file
View File

@@ -0,0 +1,29 @@
const path = require("path");
const queries = require("../graphql-client/queries");
const logger = require("../utils/logger");
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
const client = require("../graphql-client/graphql-client").client;
exports.default = async (req, res) => {
try {
logger.log("csi-surveyID-submit", "DEBUG", "csi", req.body.surveyId, null);
const gql_response = await client.request(queries.COMPLETE_SURVEY, {
surveyId: req.body.surveyId,
survey: {
response: req.body.values,
valid: false,
completedon: new Date(),
},
});
res.status(200).json(gql_response);
} catch (error) {
logger.log("csi-surveyID-submit", "ERROR", "csi", req.body.surveyId, error);
res.status(400).json(error);
}
};

View File

@@ -2272,3 +2272,124 @@ exports.INSERT_TIME_TICKETS = `mutation INSERT_TIMETICKETS($timetickets: [timeti
}
}
`;
exports.QUERY_JOB_PAYROLL_DATA = `query QUERY_JOB_PAYROLL_DATA($id: uuid!) {
jobs_by_pk(id: $id) {
bodyshop{
id
md_responsibility_centers
md_tasks_presets
employee_teams{
id
name
employee_team_members{
id
employee{
id
first_name
last_name
}
percentage
labor_rates
}
}
}
timetickets{
id
employeeid
rate
productivehrs
actualhrs
ciecacode
}
lbr_adjustments
ro_number
id
job_totals
rate_la1
rate_la2
rate_la3
rate_la4
rate_laa
rate_lab
rate_lad
rate_lae
rate_laf
rate_lag
rate_lam
rate_lar
rate_las
rate_lau
rate_ma2s
rate_ma2t
rate_ma3s
rate_mabl
rate_macs
rate_mahw
rate_mapa
rate_mash
rate_matd
status
materials
completed_tasks
joblines(where: { removed: { _eq: false } }){
id
line_no
unq_seq
line_ind
line_desc
part_type
line_ref
oem_partno
db_price
act_price
part_qty
mod_lbr_ty
db_hrs
mod_lb_hrs
lbr_op
lbr_amt
op_code_desc
status
notes
location
tax_part
db_ref
manual_line
prt_dsmk_p
prt_dsmk_m
misc_amt
misc_tax
assigned_team
convertedtolbr
convertedtolbr_data
}
}
}`;
exports.INSERT_TIME_TICKETS = `mutation INSERT_TIMETICKETS($timetickets: [timetickets_insert_input!]!) {
insert_timetickets(objects: $timetickets) {
affected_rows
}
}
`;
exports.QUERY_SURVEY = `query QUERY_SURVEY($surveyId: uuid!) {
csi_by_pk(id: $surveyId) {
completedon
csiquestion {
id
config
}
id
relateddata
valid
validuntil
}
}`;
exports.COMPLETE_SURVEY = `mutation COMPLETE_SURVEY($surveyId: uuid!, $survey: csi_set_input) {
update_csi(where: { id: { _eq: $surveyId } }, _set: $survey) {
affected_rows
}
}`;

View File

@@ -0,0 +1,8 @@
const express = require("express");
const router = express.Router();
const {lookup, submit} = require("../csi/csi");
router.post("/lookup", lookup);
router.post("/submit", submit);
module.exports = router;