Merge branch 'release/2023-08-11' into feature/IO-2278-parts-dispatching

This commit is contained in:
Patrick Fic
2023-08-04 12:36:30 -07:00
77 changed files with 1440 additions and 255 deletions

View File

@@ -4318,6 +4318,48 @@
<folder_node>
<name>dms</name>
<children>
<concept_node>
<name>apcontrol</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>appostingaccount</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>cashierid</name>
<definition_loaded>false</definition_loaded>
@@ -14025,6 +14067,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>scheduledintoday</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>scheduledouttoday</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>
</children>
</folder_node>
</children>
@@ -23886,6 +23970,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>dms_unsold</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>dms_wip_acctnumber</name>
<definition_loaded>false</definition_loaded>
@@ -43605,6 +43710,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>jobs_scheduled_completion</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>lag_time</name>
<definition_loaded>false</definition_loaded>

View File

@@ -148,6 +148,10 @@
background: #e7f3ff !important;
}
.ant-table-tbody > tr.ant-table-row-selected > td {
background: #e6f7ff !important;
}
.job-line-manual {
color: tomato;
font-style: italic;

View File

@@ -1,18 +1,18 @@
import { useApolloClient, useMutation } from "@apollo/client";
import { Button, Checkbox, Form, Modal, notification, Space } from "antd";
import { Button, Checkbox, Form, Modal, Space, notification } from "antd";
import _ from "lodash";
import React, { useEffect, useState, useMemo } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import {
QUERY_JOB_LBR_ADJUSTMENTS,
UPDATE_JOB,
} from "../../graphql/jobs.queries";
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
@@ -20,15 +20,15 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import confirmDialog from "../../utils/asyncConfirm";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import BillFormContainer from "../bill-form/bill-form.container";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
import useLocalStorage from "../../utils/useLocalStorage";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import confirmDialog from "../../utils/asyncConfirm";
import useLocalStorage from "../../utils/useLocalStorage";
import BillFormContainer from "../bill-form/bill-form.container";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility";
const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal,
@@ -126,6 +126,17 @@ function BillEnterModalContainer({
deductedfromlbr: deductedfromlbr,
lbr_adjustment,
joblineid: i.joblineid === "noline" ? null : i.joblineid,
applicable_taxes: {
federal:
(i.applicable_taxes && i.applicable_taxes.federal) ||
false,
state:
(i.applicable_taxes && i.applicable_taxes.state) ||
false,
local:
(i.applicable_taxes && i.applicable_taxes.local) ||
false,
},
};
}),
},

View File

@@ -1,6 +1,6 @@
import Icon, { UploadOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { MdOpenInNew } from "react-icons/md";
import { useTreatments } from "@splitsoftware/splitio-react";
import {
Alert,
Divider,
@@ -12,14 +12,17 @@ import {
Switch,
Upload,
} from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { MdOpenInNew } from "react-icons/md";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
@@ -28,8 +31,6 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility";
import { useTreatments } from "@splitsoftware/splitio-react";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -58,6 +59,11 @@ export function BillFormComponent({
{},
bodyshop.imexshopid
);
const { ClosingPeriod } = useTreatments(
["ClosingPeriod"],
{},
bodyshop.imexshopid
);
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
@@ -259,6 +265,37 @@ export function BillFormComponent({
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
validator(rule, value) {
if (
ClosingPeriod.treatment === "on" &&
bodyshop.accountingconfig.ClosingPeriod
) {
if (
moment(value)
.startOf("day")
.isSameOrAfter(
moment(
bodyshop.accountingconfig.ClosingPeriod[0]
).startOf("day")
) &&
moment(value)
.startOf("day")
.isSameOrBefore(
moment(
bodyshop.accountingconfig.ClosingPeriod[1]
).endOf("day")
)
) {
return Promise.resolve();
} else {
return Promise.reject(t("bills.validation.closingperiod"));
}
} else {
return Promise.resolve();
}
},
}),
]}
>
<FormDatePicker disabled={disabled} />

View File

@@ -1,13 +1,18 @@
import { Badge, List, Tag } from "antd";
import React, { useEffect } from "react";
import React from "react";
import { connect } from "react-redux";
import {
AutoSizer,
CellMeasurer,
CellMeasurerCache,
List as VirtualizedList,
} from "react-virtualized";
import { createStructuredSelector } from "reselect";
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
import { TimeAgoFormatter } from "../../utils/DateFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { List as VirtualizedList, AutoSizer } from "react-virtualized";
import "./chat-conversation-list.styles.scss";
@@ -24,57 +29,66 @@ function ChatConversationListComponent({
conversationList,
selectedConversation,
setSelectedConversation,
subscribeToMoreConversations,
loadMoreConversations,
}) {
useEffect(
() => subscribeToMoreConversations(),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 60,
});
const rowRenderer = ({ index, key, style }) => {
const rowRenderer = ({ index, key, style, parent }) => {
const item = conversationList[index];
return (
<List.Item
<CellMeasurer
key={key}
onClick={() => setSelectedConversation(item.id)}
className={`chat-list-item ${
item.id === selectedConversation
? "chat-list-selected-conversation"
: null
}`}
style={style}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
<div sryle={{ display: "inline-block" }}>
{item.label && <div className="chat-name">{item.label}</div>}
{item.job_conversations.length > 0 ? (
<div className="chat-name">
{item.job_conversations.map((j, idx) => (
<div key={idx}>
<OwnerNameDisplay ownerObject={j.job} />
</div>
))}
</div>
) : (
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
)}
</div>
<div sryle={{ display: "inline-block" }}>
<div>
{item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => (
<Tag key={idx} className="ro-number-tag">
{j.job.ro_number}
</Tag>
))
: null}
<List.Item
onClick={() => setSelectedConversation(item.id)}
className={`chat-list-item ${
item.id === selectedConversation
? "chat-list-selected-conversation"
: null
}`}
style={style}
>
<div
style={{
display: "inline-block",
}}
>
{item.label && <div className="chat-name">{item.label}</div>}
{item.job_conversations.length > 0 ? (
<div className="chat-name">
{item.job_conversations.map((j, idx) => (
<div key={idx}>
<OwnerNameDisplay ownerObject={j.job} />
</div>
))}
</div>
) : (
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
)}
</div>
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
</div>
<Badge count={item.messages_aggregate.aggregate.count || 0} />
</List.Item>
<div style={{ display: "inline-block" }}>
<div>
{item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => (
<Tag key={idx} className="ro-number-tag">
{j.job.ro_number}
</Tag>
))
: null}
</div>
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
</div>
<Badge count={item.messages_aggregate.aggregate.count || 0} />
</List.Item>
</CellMeasurer>
);
};
@@ -86,7 +100,7 @@ function ChatConversationListComponent({
height={height}
width={width}
rowCount={conversationList.length}
rowHeight={60}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
if (scrollTop + clientHeight === scrollHeight) {

View File

@@ -4,7 +4,7 @@ import {
ShrinkOutlined,
SyncOutlined,
} from "@ant-design/icons";
import { useLazyQuery, useSubscription } from "@apollo/client";
import { useLazyQuery, useQuery } from "@apollo/client";
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -12,8 +12,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
CONVERSATION_LIST_QUERY,
CONVERSATION_LIST_SUBSCRIPTION,
UNREAD_CONVERSATION_COUNT_SUBSCRIPTION,
UNREAD_CONVERSATION_COUNT,
} from "../../graphql/conversations.queries";
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
import {
@@ -42,19 +41,20 @@ export function ChatPopupComponent({
const { t } = useTranslation();
const [pollInterval, setpollInterval] = useState(0);
const { data: unreadData } = useSubscription(
UNREAD_CONVERSATION_COUNT_SUBSCRIPTION
);
const [
getConversations,
{ loading, data, called, refetch, fetchMore, subscribeToMore },
] = useLazyQuery(CONVERSATION_LIST_QUERY, {
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !chatVisible,
...(pollInterval > 0 ? { pollInterval } : {}),
});
const [getConversations, { loading, data, refetch, fetchMore }] =
useLazyQuery(CONVERSATION_LIST_QUERY, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !chatVisible,
...(pollInterval > 0 ? { pollInterval } : {}),
});
const fcmToken = sessionStorage.getItem("fcmtoken");
useEffect(() => {
@@ -66,13 +66,13 @@ export function ChatPopupComponent({
}, [fcmToken]);
useEffect(() => {
if (called && chatVisible)
if (chatVisible)
getConversations({
variables: {
offset: 0,
},
});
}, [chatVisible, called, getConversations]);
}, [chatVisible, getConversations]);
const loadMoreConversations = useCallback(() => {
if (data)
@@ -119,43 +119,6 @@ export function ChatPopupComponent({
<ChatConversationListComponent
conversationList={data ? data.conversations : []}
loadMoreConversations={loadMoreConversations}
subscribeToMoreConversations={() =>
subscribeToMore({
document: CONVERSATION_LIST_SUBSCRIPTION,
variables: { offset: 0 },
updateQuery: (prev, { subscriptionData }) => {
if (
!subscriptionData.data ||
subscriptionData.data.conversations.length === 0
)
return prev;
let conversations = [...prev.conversations];
const newConversations =
subscriptionData.data.conversations;
for (const conversation of newConversations) {
const index = conversations.findIndex(
(prevConversation) =>
prevConversation.id === conversation.id
);
if (index !== -1) {
conversations.splice(index, 1);
conversations.unshift(conversation);
continue;
}
conversations.unshift(conversation);
}
return Object.assign({}, prev, {
conversations: conversations,
});
},
})
}
/>
)}
</Col>

View File

@@ -59,6 +59,14 @@ export default function ContractsCarsComponent({
sortOrder:
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
},
{
title: t("courtesycars.fields.color"),
dataIndex: "color",
key: "color",
sorter: (a, b) => alphaSort(a.color, b.color),
sortOrder:
state.sortedInfo.columnKey === "color" && state.sortedInfo.order,
},
{
title: t("courtesycars.fields.plate"),
dataIndex: "plate",
@@ -93,6 +101,9 @@ export default function ContractsCarsComponent({
(cc.model || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.color || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
);

View File

@@ -0,0 +1,235 @@
import {
BranchesOutlined,
ExclamationCircleFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
import { Card, Space, Table, Tooltip } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardScheduledInToday({ data, ...cardProps }) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
});
if (!data) return null;
if (!data.scheduled_in_today)
return <DashboardRefreshRequired {...cardProps} />;
const appt = []; // Flatten Data
data.scheduled_in_today.forEach((item) => {
var i = {
canceled: item.canceled,
id: item.id,
alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no,
jobid: item.job.jobid,
ins_co_nm: item.job.ins_co_nm,
iouparent: item.job.iouparent,
ownerid: item.job.ownerid,
ownr_co_nm: item.job.ownr_co_nm,
ownr_ea: item.job.ownr_ea,
ownr_fn: item.job.ownr_fn,
ownr_ln: item.job.ownr_ln,
ownr_ph1: item.job.ownr_ph1,
ownr_ph2: item.job.ownr_ph2,
production_vars: item.job.production_vars,
ro_number: item.job.ro_number,
suspended: item.job.suspended,
v_make_desc: item.job.v_make_desc,
v_model_desc: item.job.v_model_desc,
v_model_yr: item.job.v_model_yr,
v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid,
note: item.note,
start: moment(item.start).format("hh:mm a"),
title: item.title,
};
appt.push(i);
});
appt.sort ( function (a, b) { return new Date(a.start) - new Date(b.start); });
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.jobid}
onClick={(e) => e.stopPropagation()}
>
<Space>
{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>
)}
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
),
},
{
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} />
),
},
{
title: t("jobs.fields.ownr_ea"),
dataIndex: "ownr_ea",
key: "ownr_ea",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
{`${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"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
responsive: ["md"],
},
{
title: t("appointments.fields.time"),
dataIndex: "start",
key: "start",
ellipsis: true,
responsive: ["md"],
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
responsive: ["md"],
},
];
const handleTableChange = (sorter) => {
setState({ ...state, sortedInfo: sorter });
};
return (
<Card title={t("dashboard.titles.scheduledintoday", {date: moment().startOf("day").format("MM/DD/YYYY")})} {...cardProps}>
<div style={{ height: "100%" }}>
<Table
onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns}
scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id"
style={{ height: "85%" }}
dataSource={appt}
/>
</div>
</Card>
);
}
export const DashboardScheduledInTodayGql = `
scheduled_in_today: appointments(where: {start: {_gte: "${moment()
.startOf("day")
.toISOString()}", _lte: "${moment()
.endOf("day")
.toISOString()}"}, canceled: {_eq: false}, block: {_neq: true}}) {
canceled
id
job {
alt_transport
clm_no
jobid: id
ins_co_nm
iouparent
ownerid
ownr_co_nm
ownr_ea
ownr_fn
ownr_ln
ownr_ph1
ownr_ph2
production_vars
ro_number
suspended
v_make_desc
v_model_desc
v_model_yr
v_vin
vehicleid
}
note
start
title
}
`;

View File

@@ -0,0 +1,210 @@
import {
BranchesOutlined,
ExclamationCircleFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
import { Card, Space, Table, Tooltip } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardScheduledOutToday({ data, ...cardProps }) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
});
if (!data) return null;
if (!data.scheduled_out_today)
return <DashboardRefreshRequired {...cardProps} />;
data.scheduled_out_today.forEach((item) => {
item.scheduled_completion= moment(item.scheduled_completion).format("hh:mm a")
});
data.scheduled_out_today.sort(function (a, b) {
return new Date(a.scheduled_completion) - new Date(b.scheduled_completion);
});
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.jobid}
onClick={(e) => e.stopPropagation()}
>
<Space>
{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>
)}
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
),
},
{
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} />
),
},
{
title: t("jobs.fields.ownr_ea"),
dataIndex: "ownr_ea",
key: "ownr_ea",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
{`${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"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
responsive: ["md"],
},
{
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
responsive: ["md"],
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
responsive: ["md"],
},
];
const handleTableChange = (sorter) => {
setState({ ...state, sortedInfo: sorter });
};
return (
<Card
title={t("dashboard.titles.scheduledouttoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
})}
{...cardProps}
>
<div style={{ height: "100%" }}>
<Table
onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns}
scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id"
style={{ height: "85%" }}
dataSource={data.scheduled_out_today}
/>
</div>
</Card>
);
}
export const DashboardScheduledOutTodayGql = `
scheduled_out_today: jobs(where: {
date_invoiced: {_is_null: true},
ro_number: {_is_null: false},
voided: {_eq: false},
scheduled_completion: {_gte: "${moment().startOf("day").toISOString()}",
_lte: "${moment().endOf("day").toISOString()}"}}) {
alt_transport
clm_no
jobid: id
ins_co_nm
iouparent
ownerid
ownr_co_nm
ownr_ea
ownr_fn
ownr_ln
ownr_ph1
ownr_ph2
production_vars
ro_number
scheduled_completion
suspended
v_make_desc
v_model_desc
v_model_yr
v_vin
vehicleid
}
`;

View File

@@ -1,6 +1,6 @@
import Icon, { SyncOutlined } from "@ant-design/icons";
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 _ from "lodash";
import moment from "moment";
@@ -37,6 +37,12 @@ import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
//Combination of the following:
// /node_modules/react-grid-layout/css/styles.css
// /node_modules/react-resizable/css/styles.css
import DashboardScheduledInToday, {
DashboardScheduledInTodayGql,
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component";
import DashboardScheduledOutToday, {
DashboardScheduledOutTodayGql,
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component";
import "./dashboard-grid.styles.scss";
import { GenerateDashboardData } from "./dashboard-grid.utils";
@@ -268,6 +274,28 @@ const componentList = {
w: 2,
h: 2,
},
ScheduleInToday: {
label: i18next.t("dashboard.titles.scheduledintoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
}),
component: DashboardScheduledInToday,
gqlFragment: DashboardScheduledInTodayGql,
minW: 10,
minH: 2,
w: 10,
h: 2,
},
ScheduleOutToday: {
label: i18next.t("dashboard.titles.scheduledouttoday", {
date: moment().startOf("day").format("MM/DD/YYYY"),
}),
component: DashboardScheduledOutToday,
gqlFragment: DashboardScheduledOutTodayGql,
minW: 10,
minH: 2,
w: 10,
h: 2,
},
};
const createDashboardQuery = (state) => {
@@ -283,8 +311,12 @@ const createDashboardQuery = (state) => {
monthly_sales: jobs(where: {_and: [
{ voided: {_eq: false}},
{date_invoiced: {_gte: "${moment()
.startOf("month").startOf('day').toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month").endOf('day').toISOString()}"}}]}) {
.startOf("month")
.startOf("day")
.toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month")
.endOf("day")
.toISOString()}"}}]}) {
id
ro_number
date_invoiced

View File

@@ -66,7 +66,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
key: "status",
},
{
title: t("jobs.fields.ro_number"),
title: t("bills.fields.invoice_number"),
dataIndex: ["Posting", "Reference"],
key: "reference",
},

View File

@@ -11,6 +11,7 @@ import {
Select,
Space,
Statistic,
Switch,
Typography,
} from "antd";
import Dinero from "dinero.js";
@@ -183,6 +184,13 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
<Space>
<DmsCdkMakes form={form} socket={socket} job={job} />
<DmsCdkMakesRefetch />
<Form.Item
name="dms_unsold"
label={t("jobs.fields.dms.dms_unsold")}
initialValue={false}
>
<Switch />
</Form.Item>
</Space>
</div>
)}

View File

@@ -311,7 +311,9 @@ function Header({
icon={<SettingOutlined />}
>
<Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}>
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
<Link to="/manage/shop?tab=info">
{t("menus.header.shop_config")}
</Link>
</Menu.Item>
<Menu.Item key="dashboard" icon={<DashboardFilled />}>
<Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>

View File

@@ -1,14 +1,14 @@
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import { useMutation } from "@apollo/client";
import { Button, Form, notification } from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import moment from "moment";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -38,8 +38,8 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
setLoading(true);
const result = await updateJob({
variables: { jobId: job.id, job: values },
refetchQueries: ['GET_JOB_BY_PK'],
awaitRefetchQueries:true
refetchQueries: ["GET_JOB_BY_PK"],
awaitRefetchQueries: true,
});
const changedAuditFields = form.getFieldsValue(
@@ -126,7 +126,10 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
<DateTimePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.date_repairstarted")} name="date_repairstarted">
<Form.Item
label={t("jobs.fields.date_repairstarted")}
name="date_repairstarted"
>
<DateTimePicker />
</Form.Item>
<Form.Item
@@ -173,6 +176,9 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
>
<DateTimePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
<DateTimePicker />
</Form.Item>
</LayoutFormRow>
</Form>

View File

@@ -1,19 +1,18 @@
import { useMutation } from "@apollo/client";
import { gql, useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import { gql } from "@apollo/client";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import moment from "moment";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import moment from "moment";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
@@ -150,6 +149,10 @@ export function JobAdminMarkReexport({
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobuninvoice(),
});
} else {
notification["error"]({
message: t("jobs.errors.saving", {

View File

@@ -33,8 +33,9 @@ export function JobsAdminUnvoid({
mutation UNVOID_JOB($jobId: uuid!) {
update_jobs_by_pk(pk_columns: {id: $jobId}, _set: {voided: false, status: "${
bodyshop.md_ro_statuses.default_imported
}"}) {
}", date_void: null}) {
id
date_void
voided
status
}

View File

@@ -141,6 +141,10 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
<DateTimePicker disabled={true || jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
<DateTimePicker disabled={true || jobRO} />
</Form.Item>
</FormRow>
</div>
);

View File

@@ -5,10 +5,10 @@ import {
Dropdown,
Form,
Menu,
notification,
Popconfirm,
Popover,
Select,
notification,
} from "antd";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
@@ -24,12 +24,12 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import JobsDetailHeaderActionsAddevent from "./jobs-detail-header-actions.addevent";
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import JobsDetaiLheaderCsi from "./jobs-detail-header-actions.csi.component";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
import JobsDetailHeaderActionsExportcustdataComponent from "./jobs-detail-header-actions.exportcustdata.component";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -480,6 +480,7 @@ export function JobsDetailHeaderActions({
scheduled_in: null,
scheduled_completion: null,
inproduction: false,
date_void: new Date(),
},
note: [
{

View File

@@ -47,9 +47,7 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
bodyhrs: dayAcc.bodyhrs + dayVal.bodyhrs,
painthrs: dayAcc.painthrs + dayVal.painthrs,
sales:
dayAcc.painthrs +
dayVal.job.job_totals.totals.subtotal.amount / 100 +
2500,
dayAcc.sales + dayVal.job.job_totals.totals.subtotal.amount / 100,
};
},
{ bodyhrs: 0, painthrs: 0, sales: 0 }

View File

@@ -52,7 +52,9 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
<Tabs
defaultActiveKey={search.subtab}
onChange={(key) =>
history.push({ search: `?tab=${search.tab}&subtab=${key}` })
history.push({
search: `?tab=${search.tab}&subtab=${key}`,
})
}
>
<Tabs.TabPane key="general" tab={t("bodyshop.labels.shopinfo")}>

View File

@@ -1,14 +1,14 @@
import React, { useEffect, useState } from "react";
import ShopInfoComponent from "./shop-info.component";
import { useMutation, useQuery } from "@apollo/client";
import { Form, notification } from "antd";
import { useQuery, useMutation } from "@apollo/client";
import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../graphql/bodyshop.queries";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import AlertComponent from "../alert/alert.component";
import moment from "moment";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../graphql/bodyshop.queries";
import AlertComponent from "../alert/alert.component";
import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import moment from "moment";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import ShopInfoComponent from "./shop-info.component";
export default function ShopInfoContainer() {
const [form] = Form.useForm();
const { t } = useTranslation();
@@ -52,13 +52,28 @@ export default function ShopInfoContainer() {
onFinish={handleFinish}
initialValues={
data
? {
...data.bodyshops[0],
schedule_start_time: moment(
data.bodyshops[0].schedule_start_time
),
schedule_end_time: moment(data.bodyshops[0].schedule_end_time),
}
? data.bodyshops[0].accountingconfig.ClosingPeriod
? {
...data.bodyshops[0],
accountingconfig: {
...data.bodyshops[0].accountingconfig,
ClosingPeriod: [
moment(data.bodyshops[0].accountingconfig.ClosingPeriod[0]),
moment(data.bodyshops[0].accountingconfig.ClosingPeriod[1]),
],
},
schedule_start_time: moment(
data.bodyshops[0].schedule_start_time
),
schedule_end_time: moment(data.bodyshops[0].schedule_end_time),
}
: {
...data.bodyshops[0],
schedule_start_time: moment(
data.bodyshops[0].schedule_start_time
),
schedule_end_time: moment(data.bodyshops[0].schedule_end_time),
}
: null
}
>

View File

@@ -1,6 +1,8 @@
import { DeleteFilled } from "@ant-design/icons";
import { useTreatments } from "@splitsoftware/splitio-react";
import {
Button,
DatePicker,
Form,
Input,
InputNumber,
@@ -9,8 +11,13 @@ import {
Space,
Switch,
} from "antd";
import momentTZ from "moment-timezone";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DatePickerRanges from "../../utils/DatePickerRanges";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, {
@@ -18,12 +25,22 @@ import PhoneFormItem, {
} from "../form-items-formatted/phone-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import momentTZ from "moment-timezone";
const timeZonesList = momentTZ.tz.names();
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral);
export default function ShopInfoGeneral({ form }) {
export function ShopInfoGeneral({ form, bodyshop }) {
const { t } = useTranslation();
const { ClosingPeriod } = useTreatments(
["ClosingPeriod"],
{},
bodyshop && bodyshop.imexshopid
);
return (
<div>
@@ -392,6 +409,20 @@ export default function ShopInfoGeneral({ form }) {
>
<Select mode="tags" />
</Form.Item>
{ClosingPeriod.treatment === "on" && (
<>
<Form.Item
allowClear
name={["accountingconfig", "ClosingPeriod"]}
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
>
<DatePicker.RangePicker
format="MM/DD/YYYY"
ranges={DatePickerRanges}
/>
</Form.Item>
</>
)}
</LayoutFormRow>
<LayoutFormRow
header={t("bodyshop.labels.scoreboardsetup")}
@@ -602,6 +633,20 @@ export default function ShopInfoGeneral({ form }) {
>
<Select mode="tags" />
</Form.Item>
<Form.Item
name={["md_email_cc", "parts_return_slip"]}
label={t("bodyshop.fields.md_email_cc", {
template: "parts_return_slip",
})}
rules={[
{
//message: t("general.validation.required"),
type: "array",
},
]}
>
<Select mode="tags" />
</Form.Item>
<Form.Item
name={["tt_allow_post_to_invoiced"]}
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}

View File

@@ -162,6 +162,32 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Switch />
</Form.Item>
)}
{bodyshop.pbs_serialnumber && (
<Form.Item
label={t("bodyshop.fields.dms.appostingaccount")}
name={["pbs_configuration", "appostingaccount"]}
>
<Select
options={[
{ value: "wip", label: "WIP" },
{ value: "cogs", label: "COGS" },
]}
/>
</Form.Item>
)}
{bodyshop.pbs_serialnumber && (
<Form.Item
label={t("bodyshop.fields.dms.apcontrol")}
name={["pbs_configuration", "apcontrol"]}
>
<Select
options={[
{ value: "ro", label: "RO Number" },
{ value: "vendordmsid", label: "Vendor DMS ID" },
]}
/>
</Form.Item>
)}
</LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.dms.cdk.payers")}>
<Form.List name={["cdk_configuration", "payers"]}>

View File

@@ -5,10 +5,10 @@ import {
Col,
Form,
InputNumber,
notification,
Popover,
Row,
Select,
notification,
} from "antd";
import axios from "axios";
import React, { useState } from "react";
@@ -16,13 +16,14 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries";
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
import { UPDATE_TIME_TICKET } from "../../graphql/timetickets.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
import TechJobClockoutDelete from "../tech-job-clock-out-delete/tech-job-clock-out-delete.component";
import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component";
import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries";
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -40,6 +41,7 @@ export function TechClockOffButton({
}) {
const [loading, setLoading] = useState(false);
const [updateTimeticket] = useMutation(UPDATE_TIME_TICKET);
const [updateJobStatus] = useMutation(UPDATE_JOB_STATUS);
const [form] = Form.useForm();
const { queryLoading, data: lineTicketData } = useQuery(
GET_LINE_TICKET_BY_PK,
@@ -59,7 +61,8 @@ export function TechClockOffButton({
const handleFinish = async (values) => {
logImEXEvent("tech_clock_out_job");
const status = values.status;
delete values.status;
setLoading(true);
const result = await updateTimeticket({
variables: {
@@ -98,6 +101,26 @@ export function TechClockOffButton({
message: t("timetickets.successes.clockedout"),
});
}
if (!isShiftTicket) {
const job_update_result = await updateJobStatus({
variables: {
jobId: jobId,
status: status,
},
});
if (!!job_update_result.errors) {
notification["error"]({
message: t("jobs.errors.updating", {
message: JSON.stringify(result.errors),
}),
});
} else {
notification["success"]({
message: t("jobs.successes.updated"),
});
}
}
setLoading(false);
if (completedCallback) completedCallback();
};
@@ -195,7 +218,6 @@ export function TechClockOffButton({
</Form.Item>
</div>
) : null}
<Form.Item
name="cost_center"
label={t("timetickets.fields.cost_center")}
@@ -228,6 +250,29 @@ export function TechClockOffButton({
</Select>
</Form.Item>
{isShiftTicket ? (
<div></div>
) : (
<Form.Item
name="status"
label={t("jobs.fields.status")}
initialValue={
lineTicketData && lineTicketData.jobs_by_pk.status
}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select>
{bodyshop.md_ro_statuses.production_statuses.map((item) => (
<Select.Option key={item}></Select.Option>
))}
</Select>
</Form.Item>
)}
<Button type="primary" htmlType="submit" loading={loading}>
{t("general.actions.save")}
</Button>

View File

@@ -0,0 +1,78 @@
import { Alert, Button, Col, Row, Space } from "antd";
import i18n from "i18next";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectUpdateAvailable } from "../../redux/application/application.selectors";
import { AlertOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { setUpdateAvailable } from "../../redux/application/application.actions";
import { store } from "../../redux/store";
import * as serviceWorkerRegistration from "../../serviceWorkerRegistration";
let globalRegistration;
const mapStateToProps = createStructuredSelector({
updateAvailable: selectUpdateAvailable,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(UpdateAlert);
export function UpdateAlert({ updateAvailable }) {
const { t } = useTranslation();
if (!updateAvailable) return null;
return (
<Alert
message={t("general.messages.newversiontitle")}
showIcon
icon={<AlertOutlined />}
description={
<Row gutter={[16, 16]}>
<Col sm={24} md={16} lg={18}>
{t("general.messages.newversionmessage")}
</Col>
<Col sm={24} md={8} lg={6}>
<Space wrap>
<Button
onClick={async () => {
window.open("https://imex-online.noticeable.news/", "_blank");
}}
>
{i18n.t("general.actions.viewreleasenotes")}
</Button>
<Button
type="primary"
onClick={async () => {
if (globalRegistration && globalRegistration.waiting) {
await globalRegistration.unregister();
// Makes Workbox call skipWaiting()
globalRegistration.waiting.postMessage({
type: "SKIP_WAITING",
});
// Once the service worker is unregistered, we can reload the page to let
// the browser download a fresh copy of our app (invalidating the cache)
window.location.reload();
}
}}
>
{i18n.t("general.actions.refresh")}
</Button>
</Space>
</Col>
</Row>
}
closable={false}
type="warning"
/>
);
}
const onServiceWorkerUpdate = (registration) => {
console.log("onServiceWorkerUpdate", registration);
globalRegistration = registration;
store.dispatch(setUpdateAvailable(true));
};
serviceWorkerRegistration.register({ onUpdate: onServiceWorkerUpdate });

View File

@@ -1,37 +1,5 @@
import { gql } from "@apollo/client";
export const CONVERSATION_LIST_SUBSCRIPTION = gql`
subscription CONVERSATION_LIST_SUBSCRIPTION($offset: Int!) {
conversations(
order_by: { updated_at: desc }
limit: 1
offset: $offset
where: { archived: { _eq: false } }
) {
phone_num
id
updated_at
unreadcnt
messages_aggregate(
where: { read: { _eq: false }, isoutbound: { _eq: false } }
) {
aggregate {
count
}
}
job_conversations {
job {
id
ro_number
ownr_fn
ownr_ln
ownr_co_nm
}
}
}
}
`;
export const UNREAD_CONVERSATION_COUNT = gql`
query UNREAD_CONVERSATION_COUNT {
messages_aggregate(
@@ -44,18 +12,6 @@ export const UNREAD_CONVERSATION_COUNT = gql`
}
`;
export const UNREAD_CONVERSATION_COUNT_SUBSCRIPTION = gql`
subscription UNREAD_CONVERSATION_COUNT_SUBSCRIPTION {
messages_aggregate(
where: { read: { _eq: false }, isoutbound: { _eq: false } }
) {
aggregate {
count
}
}
}
`;
export const CONVERSATION_LIST_QUERY = gql`
query CONVERSATION_LIST_QUERY($offset: Int!) {
conversations(

View File

@@ -34,6 +34,7 @@ export const GET_LINE_TICKET_BY_PK = gql`
id
lbr_adjustments
converted
status
}
joblines(where: { jobid: { _eq: $id }, removed: { _eq: false } }) {
id

View File

@@ -682,6 +682,7 @@ export const GET_JOB_BY_PK = gql`
date_rentalresp
date_exported
date_repairstarted
date_void
status
owner_owing
tax_registration_number
@@ -1086,6 +1087,7 @@ export const UPDATE_JOB = gql`
scheduled_completion
actual_in
date_repairstarted
date_void
}
}
}
@@ -1133,6 +1135,7 @@ export const VOID_JOB = gql`
update_jobs_by_pk(_set: $job, pk_columns: { id: $jobId }) {
id
date_exported
date_void
status
alt_transport
ro_number
@@ -1227,10 +1230,10 @@ export const ACTIVE_JOBS_FOR_AUTOCOMPLETE = gql`
query ACTIVE_JOBS_FOR_AUTOCOMPLETE($statuses: [String!]!) {
jobs(where: { status: { _in: $statuses } }) {
id
ownr_co_nm
ownr_fn
ownr_ln
ro_number
vehicleid
v_make_desc
v_model_desc
@@ -1258,6 +1261,7 @@ export const SEARCH_JOBS_FOR_AUTOCOMPLETE = gql`
}
) {
id
ownr_co_nm
ownr_fn
ownr_ln
ro_number
@@ -1274,6 +1278,7 @@ export const SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE = gql`
query SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE($id: uuid!) {
jobs_by_pk(id: $id) {
id
ownr_co_nm
ownr_fn
ownr_ln
ro_number
@@ -1292,6 +1297,7 @@ export const SEARCH_FOR_JOBS = gql`
search_jobs(args: { search: $search }, limit: 25) {
id
ro_number
ownr_co_nm
ownr_fn
ownr_ln
}

View File

@@ -13,7 +13,7 @@ import queryString from "query-string";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory, useLocation, Link } from "react-router-dom";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import SocketIO from "socket.io-client";
import AlertComponent from "../../components/alert/alert.component";
@@ -22,6 +22,7 @@ import DmsCustomerSelector from "../../components/dms-customer-selector/dms-cust
import DmsLogEvents from "../../components/dms-log-events/dms-log-events.component";
import DmsPostForm from "../../components/dms-post-form/dms-post-form.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { OwnerNameDisplayFunction } from "../../components/owner-name-display/owner-name-display.component";
import { auth } from "../../firebase/firebase.utils";
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
import {
@@ -29,7 +30,6 @@ import {
setSelectedHeader,
} from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { OwnerNameDisplayFunction } from "../../components/owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -46,6 +46,7 @@ export const socket = SocketIO(
process.env.NODE_ENV === "production"
? process.env.REACT_APP_AXIOS_BASE_API_URL
: window.location.origin,
// "http://localhost:4000", // for dev testing,
{
path: "/ws",
withCredentials: true,

View File

@@ -8,7 +8,6 @@ import {
Form,
Input,
InputNumber,
notification,
PageHeader,
Popconfirm,
Row,
@@ -17,12 +16,14 @@ import {
Statistic,
Switch,
Typography,
notification,
} from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
//import { useHistory } from "react-router-dom";
import { useTreatments } from "@splitsoftware/splitio-react";
import Dinero from "dinero.js";
import moment from "moment";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
@@ -37,7 +38,6 @@ import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.qu
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import Dinero from "dinero.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
@@ -55,6 +55,11 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
{},
bodyshop && bodyshop.imexshopid
);
const { ClosingPeriod } = useTreatments(
["ClosingPeriod"],
{},
bodyshop && bodyshop.imexshopid
);
const handleFinish = async ({ removefromproduction, ...values }) => {
setLoading(true);
@@ -254,12 +259,40 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
if (!value || moment(value).isSameOrAfter(moment(), "day")) {
return Promise.resolve();
}
return Promise.reject(
new Error(t("jobs.labels.dms.invoicedatefuture"))
);
},
}),
({ getFieldValue }) => ({
validator(_, value) {
if (
ClosingPeriod.treatment === "on" &&
bodyshop.accountingconfig.ClosingPeriod
) {
if (
moment(value).isSameOrAfter(
moment(
bodyshop.accountingconfig.ClosingPeriod[0]
).startOf("day")
) &&
moment(value).isSameOrBefore(
moment(
bodyshop.accountingconfig.ClosingPeriod[1]
).endOf("day")
)
) {
return Promise.resolve();
} else {
return Promise.reject(
new Error(t("jobs.labels.closingperiod"))
);
}
} else {
return Promise.resolve();
}
},
}),
]}
>
<DateTimePicker

View File

@@ -25,6 +25,7 @@ import {
import * as Sentry from "@sentry/react";
import "./manage.page.styles.scss";
import UpdateAlert from "../../components/update-alert/update-alert.component";
const ManageRootPage = lazy(() =>
import("../manage-root/manage-root.page.container")
@@ -394,6 +395,7 @@ export function Manage({ match, conflict, bodyshop }) {
<>
<ChatAffixContainer />
<Layout className="layout-container">
<UpdateAlert />
<HeaderContainer />
<Content className="content-container">

View File

@@ -6,7 +6,7 @@ import AlertComponent from "../../components/alert/alert.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries";
import { setBodyshop } from "../../redux/user/user.actions";
import "../../utils/RegisterSw";
//import "../../utils/RegisterSw";
import ManagePage from "./manage.page.component";
const mapDispatchToProps = (dispatch) => ({

View File

@@ -40,6 +40,10 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
]);
}, [t, setSelectedHeader, setBreadcrumbs, bodyshop.shopname]);
useEffect(() => {
if (!search.tab) history.push({ search: "?tab=info" });
}, [history, search]);
return (
<RbacWrapper action="shop:config">
<Tabs

View File

@@ -12,6 +12,7 @@ import TechSider from "../../components/tech-sider/tech-sider.component";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
import "./tech.page.styles.scss";
import UpdateAlert from "../../components/update-alert/update-alert.component";
const TimeTicketModalContainer = lazy(() =>
import("../../components/time-ticket-modal/time-ticket-modal.container")
);
@@ -59,7 +60,9 @@ export function TechPage({ technician, match }) {
<TechSider />
<Layout>
{technician ? null : <Redirect to={`${match.path}/login`} />}
<UpdateAlert />
<TechHeader />
<Content className="tech-content-container">
<ErrorBoundary>
<Suspense

View File

@@ -9,7 +9,7 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
import { useTranslation } from "react-i18next";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { createStructuredSelector } from "reselect";
import "../../utils/RegisterSw";
//import "../../utils/RegisterSw";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,

View File

@@ -62,3 +62,8 @@ export const setProblemJobs = (problemJobs) => ({
type: ApplicationActionTypes.SET_PROBLEM_JOBS,
payload: problemJobs,
});
export const setUpdateAvailable = (isUpdateAvailable) => ({
type: ApplicationActionTypes.SET_UPDATE_AVAILABLE,
payload: isUpdateAvailable,
});

View File

@@ -3,6 +3,7 @@ import ApplicationActionTypes from "./application.types";
const INITIAL_STATE = {
loading: false,
online: true,
updateAvailable: false,
breadcrumbs: [],
recentItems: [],
selectedHeader: "home",
@@ -18,6 +19,11 @@ const INITIAL_STATE = {
const applicationReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case ApplicationActionTypes.SET_UPDATE_AVAILABLE:
return {
...state,
updateAvailable: action.payload,
};
case ApplicationActionTypes.SET_SELECTED_HEADER:
return {
...state,

View File

@@ -48,3 +48,7 @@ export const selectProblemJobs = createSelector(
[selectApplication],
(application) => application.problemJobs
);
export const selectUpdateAvailable = createSelector(
[selectApplication],
(application) => application.updateAvailable
);

View File

@@ -12,5 +12,6 @@ const ApplicationActionTypes = {
SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE"
};
export default ApplicationActionTypes;

View File

@@ -101,6 +101,7 @@
"messages": {
"admin_jobmarkexported": "ADMIN: Job marked as exported.",
"admin_jobmarkforreexport": "ADMIN: Job marked for re-export.",
"admin_jobuninvoice": "ADMIN: Job has been uninvoiced.",
"admin_jobunvoid": "ADMIN: Job has been unvoided.",
"billposted": "Bill with invoice number {{invoice_number}} posted.",
"billupdated": "Bill with invoice number {{invoice_number}} updated.",
@@ -222,6 +223,7 @@
"reexport": "Bill marked for re-export."
},
"validation": {
"closingperiod": "This Bill Date is outside of the Closing Period.",
"inventoryquantity": "Quantity must be greater than or equal to what has been added to inventory ({{number}}).",
"manualinhouse": "Manual posting to the in house vendor is restricted. ",
"unique_invoice_number": "This invoice number has already been entered for this vendor."
@@ -261,6 +263,7 @@
"bill_local_tax_rate": "Bill - Provincial/State Tax Rate %",
"bill_state_tax_rate": "Bill - Provincial/State Tax Rate %",
"city": "City",
"closingperiod": "Closing Period",
"country": "Country",
"dailybodytarget": "Scoreboard - Daily Body Target",
"dailypainttarget": "Scoreboard - Daily Paint Target",
@@ -269,6 +272,8 @@
"templates": "Delivery Templates"
},
"dms": {
"apcontrol": "AP Control Number",
"appostingaccount": "AP Posting Account",
"cashierid": "Cashier ID",
"default_journal": "Default Journal",
"disablebillwip": "Disable bill WIP for A/P Posting",
@@ -858,7 +863,9 @@
"prodhrssummary": "Production Hours Summary",
"productiondollars": "Total dollars in Production",
"productionhours": "Total hours in Production",
"projectedmonthlysales": "Projected Monthly Sales"
"projectedmonthlysales": "Projected Monthly Sales",
"scheduledintoday": "Sheduled In Today: {{date}}",
"scheduledouttoday": "Sheduled Out Today: {{date}}"
}
},
"dms": {
@@ -1118,7 +1125,7 @@
},
"messages": {
"exception": "$t(titles.app) has encountered an error. Please try again. If the problem persists, please submit a support ticket or contact us.",
"newversionmessage": "Click refresh below to update to the latest available version of ImEX Online. Please make sure all other tabs and windows are closed.",
"newversionmessage": "Click refresh to update to the latest available version of ImEX Online. Please make sure all other tabs and windows are closed.",
"newversiontitle": "New version of ImEX Online Available",
"noacctfilepath": "There is no accounting file path set. You will not be able to export any items.",
"nofeatureaccess": "You do not have access to this feature of ImEX Online. Please contact support to request a license for this feature.",
@@ -1436,6 +1443,7 @@
"date_repairstarted": "Repairs Started",
"date_scheduled": "Scheduled",
"date_towin": "Towed In",
"date_void": "Void",
"ded_amt": "Deductible",
"ded_note": "Deductible Note",
"ded_status": "Deductible Status",
@@ -1451,6 +1459,7 @@
"cost_dms_acctnumber": "Cost DMS Acct #",
"dms_make": "DMS Make",
"dms_model": "DMS Model",
"dms_unsold": "New, Unsold Vehicle",
"dms_wip_acctnumber": "Cost WIP DMS Acct #",
"id": "DMS ID",
"inservicedate": "In Service Date",
@@ -1682,6 +1691,7 @@
"checklists": "Checklists",
"closeconfirm": "Are you sure you want to close this job? This cannot be easily undone.",
"closejob": "Close Job {{ro_number}}",
"closingperiod": "This Invoice Date is outside of the Closing Period.",
"contracts": "CC Contracts",
"convertedtolabor": "Lines Converted to Labor",
"cost": "Cost",
@@ -2423,6 +2433,7 @@
"jobs": {
"individual_job_note": "Job Note RO: {{ro_number}}",
"parts_order": "Parts Order PO: {{ro_number}} - {{name}}",
"parts_return_slip":"Parts Return PO: {{ro_number}} - {{name}}",
"sublet_order": "Sublet Order PO: {{ro_number}} - {{name}}"
}
},
@@ -2590,6 +2601,7 @@
"jobs_completed_not_invoiced": "Jobs Completed not Invoiced",
"jobs_invoiced_not_exported": "Jobs Invoiced not Exported",
"jobs_reconcile": "Parts/Sublet/Labor Reconciliation",
"jobs_scheduled_completion": "Jobs Scheduled Completion",
"lag_time": "Lag Time",
"open_orders": "Open Orders by Date",
"open_orders_csr": "Open Orders by CSR",
@@ -2638,6 +2650,7 @@
"timetickets_summary": "Time Tickets Summary",
"unclaimed_hrs": "Unclaimed Hours",
"void_ros": "Void ROs",
"work_in_progress_jobs": "Work in Progress - Jobs",
"work_in_progress_labour": "Work in Progress - Labor",
"work_in_progress_payables": "Work in Progress - Payables"
}

View File

@@ -101,6 +101,7 @@
"messages": {
"admin_jobmarkexported": "",
"admin_jobmarkforreexport": "",
"admin_jobuninvoice": "",
"admin_jobunvoid": "",
"billposted": "",
"billupdated": "",
@@ -222,6 +223,7 @@
"reexport": ""
},
"validation": {
"closingperiod": "",
"inventoryquantity": "",
"manualinhouse": "",
"unique_invoice_number": ""
@@ -261,6 +263,7 @@
"bill_local_tax_rate": "",
"bill_state_tax_rate": "",
"city": "",
"closingperiod": "",
"country": "",
"dailybodytarget": "",
"dailypainttarget": "",
@@ -269,6 +272,8 @@
"templates": ""
},
"dms": {
"apcontrol": "",
"appostingaccount": "",
"cashierid": "",
"default_journal": "",
"disablebillwip": "",
@@ -858,7 +863,9 @@
"prodhrssummary": "",
"productiondollars": "",
"productionhours": "",
"projectedmonthlysales": ""
"projectedmonthlysales": "",
"scheduledintoday": "",
"scheduledouttoday": ""
}
},
"dms": {
@@ -1436,6 +1443,7 @@
"date_repairstarted": "",
"date_scheduled": "Programado",
"date_towin": "",
"date_void": "",
"ded_amt": "Deducible",
"ded_note": "",
"ded_status": "Estado deducible",
@@ -1451,6 +1459,7 @@
"cost_dms_acctnumber": "",
"dms_make": "",
"dms_model": "",
"dms_unsold": "",
"dms_wip_acctnumber": "",
"id": "",
"inservicedate": "",
@@ -1682,6 +1691,7 @@
"checklists": "",
"closeconfirm": "",
"closejob": "",
"closingperiod": "",
"contracts": "",
"convertedtolabor": "",
"cost": "",
@@ -2423,6 +2433,7 @@
"jobs": {
"individual_job_note": "",
"parts_order": "",
"parts_return_slip": "",
"sublet_order": ""
}
},
@@ -2590,6 +2601,7 @@
"jobs_completed_not_invoiced": "",
"jobs_invoiced_not_exported": "",
"jobs_reconcile": "",
"jobs_scheduled_completion": "",
"lag_time": "",
"open_orders": "",
"open_orders_csr": "",
@@ -2638,6 +2650,7 @@
"timetickets_summary": "",
"unclaimed_hrs": "",
"void_ros": "",
"work_in_progress_jobs": "",
"work_in_progress_labour": "",
"work_in_progress_payables": ""
}

View File

@@ -101,6 +101,7 @@
"messages": {
"admin_jobmarkexported": "",
"admin_jobmarkforreexport": "",
"admin_jobuninvoice": "",
"admin_jobunvoid": "",
"billposted": "",
"billupdated": "",
@@ -222,6 +223,7 @@
"reexport": ""
},
"validation": {
"closingperiod": "",
"inventoryquantity": "",
"manualinhouse": "",
"unique_invoice_number": ""
@@ -261,6 +263,7 @@
"bill_local_tax_rate": "",
"bill_state_tax_rate": "",
"city": "",
"closingperiod": "",
"country": "",
"dailybodytarget": "",
"dailypainttarget": "",
@@ -269,6 +272,8 @@
"templates": ""
},
"dms": {
"apcontrol": "",
"appostingaccount": "",
"cashierid": "",
"default_journal": "",
"disablebillwip": "",
@@ -858,7 +863,9 @@
"prodhrssummary": "",
"productiondollars": "",
"productionhours": "",
"projectedmonthlysales": ""
"projectedmonthlysales": "",
"scheduledintoday": "",
"scheduledouttoday": ""
}
},
"dms": {
@@ -1436,6 +1443,7 @@
"date_repairstarted": "",
"date_scheduled": "Prévu",
"date_towin": "",
"date_void": "",
"ded_amt": "Déductible",
"ded_note": "",
"ded_status": "Statut de franchise",
@@ -1451,6 +1459,7 @@
"cost_dms_acctnumber": "",
"dms_make": "",
"dms_model": "",
"dms_unsold": "",
"dms_wip_acctnumber": "",
"id": "",
"inservicedate": "",
@@ -1682,6 +1691,7 @@
"checklists": "",
"closeconfirm": "",
"closejob": "",
"closingperiod": "",
"contracts": "",
"convertedtolabor": "",
"cost": "",
@@ -2423,6 +2433,7 @@
"jobs": {
"individual_job_note": "",
"parts_order": "",
"parts_return_slip": "",
"sublet_order": ""
}
},
@@ -2590,6 +2601,7 @@
"jobs_completed_not_invoiced": "",
"jobs_invoiced_not_exported": "",
"jobs_reconcile": "",
"jobs_scheduled_completion": "",
"lag_time": "",
"open_orders": "",
"open_orders_csr": "",
@@ -2638,6 +2650,7 @@
"timetickets_summary": "",
"unclaimed_hrs": "",
"void_ros": "",
"work_in_progress_jobs": "",
"work_in_progress_labour": "",
"work_in_progress_payables": ""
}

View File

@@ -36,6 +36,7 @@ const AuditTrailMapping = {
jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"),
jobnotedeleted: () => i18n.t("audit_trail.messages.jobnotedeleted"),
admin_jobunvoid: () => i18n.t("audit_trail.messages.admin_jobunvoid"),
admin_jobuninvoice: () => i18n.t("audit_trail.messages.admin_jobuninvoice"),
admin_jobmarkforreexport: () =>
i18n.t("audit_trail.messages.admin_jobmarkforreexport"),
admin_jobmarkexported: () =>

View File

@@ -3,6 +3,7 @@ import { Button, notification, Space } from "antd";
import i18n from "i18next";
import React from "react";
import * as serviceWorkerRegistration from "../serviceWorkerRegistration";
import { store } from "../redux/store";
const onServiceWorkerUpdate = (registration) => {
console.log("onServiceWorkerUpdate", registration);
@@ -33,6 +34,9 @@ const onServiceWorkerUpdate = (registration) => {
</Button>
</Space>
);
store.dispatch()
notification.open({
icon: <AlertOutlined />,
message: i18n.t("general.messages.newversiontitle"),

View File

@@ -615,7 +615,14 @@ export const TemplateList = (type, context) => {
},
parts_return_slip: {
title: i18n.t("printcenter.jobs.parts_return_slip"),
subject: i18n.t("printcenter.jobs.parts_return_slip"),
subject: i18n.t("printcenter.subjects.jobs.parts_return_slip", {
ro_number: context && context.job && context.job.ro_number,
name: (
(context && context.job && context.job.ownr_ln) ||
(context && context.job && context.job.ownr_co_nm) ||
""
).trim(),
}),
description: "",
key: "parts_return_slip",
disabled: false,
@@ -1246,7 +1253,7 @@ export const TemplateList = (type, context) => {
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_open"),
field: i18n.t("jobs.fields.date_void"),
},
group: "sales",
},
@@ -1549,6 +1556,19 @@ export const TemplateList = (type, context) => {
},
group: "payroll",
},
work_in_progress_jobs_excel: {
title: i18n.t("reportcenter.templates.work_in_progress_jobs"),
subject: i18n.t("reportcenter.templates.work_in_progress_jobs"),
key: "work_in_progress_jobs_excel",
//idtype: "vendor",
reporttype: "excel",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_open"),
},
group: "jobs",
},
work_in_progress_labour: {
title: i18n.t("reportcenter.templates.work_in_progress_labour"),
description: "",
@@ -1906,6 +1926,18 @@ export const TemplateList = (type, context) => {
},
group: "sales",
},
jobs_scheduled_completion: {
title: i18n.t("reportcenter.templates.jobs_scheduled_completion"),
subject: i18n.t("reportcenter.templates.jobs_scheduled_completion"),
key: "jobs_scheduled_completion",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.scheduled_completion"),
},
group: "jobs",
},
}
: {}),
...(!type || type === "courtesycarcontract"

View File

@@ -2092,6 +2092,13 @@
table:
name: employee_team_members
schema: public
- name: joblines
using:
foreign_key_constraint_on:
column: assigned_team
table:
name: joblines
schema: public
insert_permissions:
- role: user
permission:
@@ -2665,6 +2672,9 @@
name: joblines
schema: public
object_relationships:
- name: employee_team
using:
foreign_key_constraint_on: assigned_team
- name: job
using:
foreign_key_constraint_on: jobid
@@ -2726,6 +2736,7 @@
- alt_part_i
- alt_partm
- alt_partno
- assigned_team
- bett_amt
- bett_pctg
- bett_tax
@@ -2734,6 +2745,7 @@
- convertedtolbr
- convertedtolbr_data
- created_at
- critical
- db_hrs
- db_price
- db_ref
@@ -2793,6 +2805,7 @@
- alt_part_i
- alt_partm
- alt_partno
- assigned_team
- bett_amt
- bett_pctg
- bett_tax
@@ -2872,6 +2885,7 @@
- alt_part_i
- alt_partm
- alt_partno
- assigned_team
- bett_amt
- bett_pctg
- bett_tax
@@ -3286,6 +3300,7 @@
- clm_total
- clm_zip
- comment
- completed_tasks
- converted
- created_at
- cust_pr
@@ -3480,6 +3495,7 @@
- v_model_yr
- v_vin
- vehicleid
- date_void
- voided
select_permissions:
- role: user
@@ -3550,6 +3566,7 @@
- clm_total
- clm_zip
- comment
- completed_tasks
- converted
- created_at
- cust_pr
@@ -3745,6 +3762,7 @@
- v_model_yr
- v_vin
- vehicleid
- date_void
- voided
filter:
bodyshop:
@@ -3825,6 +3843,7 @@
- clm_total
- clm_zip
- comment
- completed_tasks
- converted
- created_at
- cust_pr
@@ -4020,6 +4039,7 @@
- v_model_yr
- v_vin
- vehicleid
- date_void
- voided
filter:
bodyshop:
@@ -4629,6 +4649,7 @@
_eq: X-Hasura-User-Id
- active:
_eq: true
allow_aggregations: true
update_permissions:
- role: user
permission:
@@ -4662,6 +4683,74 @@
- name: parts_dispatch
using:
foreign_key_constraint_on: partsdispatchid
insert_permissions:
- role: user
permission:
check:
parts_dispatch:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- id
- created_at
- updated_at
- partsdispatchid
- joblineid
- quantity
- accepted_at
select_permissions:
- role: user
permission:
columns:
- quantity
- accepted_at
- created_at
- updated_at
- id
- joblineid
- partsdispatchid
filter:
parts_dispatch:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
allow_aggregations: true
update_permissions:
- role: user
permission:
columns:
- id
- created_at
- updated_at
- partsdispatchid
- joblineid
- quantity
- accepted_at
filter:
parts_dispatch:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
check: null
- table:
name: parts_order_lines
schema: public
@@ -5475,6 +5564,7 @@
- memo
- productivehrs
- rate
- task_name
- ttapprovalqueueid
- updated_at
select_permissions:
@@ -5498,6 +5588,7 @@
- memo
- productivehrs
- rate
- task_name
- ttapprovalqueueid
- updated_at
filter:
@@ -5530,6 +5621,7 @@
- memo
- productivehrs
- rate
- task_name
- ttapprovalqueueid
- updated_at
filter:

View File

@@ -0,0 +1 @@
alter table "public"."parts_dispatch_lines" alter column "accepted_at" set not null;

View File

@@ -0,0 +1 @@
alter table "public"."parts_dispatch_lines" alter column "accepted_at" drop not null;

View File

@@ -0,0 +1,5 @@
alter table "public"."job_conversations" drop constraint "job_conversations_conversationid_fkey",
add constraint "job_conversations_conversationid_fkey"
foreign key ("conversationid")
references "public"."conversations"
("id") on update restrict on delete restrict;

View File

@@ -0,0 +1,5 @@
alter table "public"."job_conversations" drop constraint "job_conversations_conversationid_fkey",
add constraint "job_conversations_conversationid_fkey"
foreign key ("conversationid")
references "public"."conversations"
("id") on update cascade on delete cascade;

View File

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

View File

@@ -0,0 +1,2 @@
alter table "public"."joblines" add column "assigned_team" uuid
null;

View File

@@ -0,0 +1 @@
alter table "public"."joblines" drop constraint "joblines_assigned_team_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."joblines"
add constraint "joblines_assigned_team_fkey"
foreign key ("assigned_team")
references "public"."employee_teams"
("id") on update restrict on delete restrict;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
alter table "public"."jobs" add column "void_date" Timestamp
null;

View File

@@ -0,0 +1 @@
ALTER TABLE "public"."jobs" ALTER COLUMN "void_date" TYPE timestamp without time zone;

View File

@@ -0,0 +1 @@
ALTER TABLE "public"."jobs" ALTER COLUMN "void_date" TYPE timestamptz;

View File

@@ -0,0 +1 @@
alter table "public"."jobs" rename column "date_void" to "void_date";

View File

@@ -0,0 +1 @@
alter table "public"."jobs" rename column "void_date" to "date_void";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -123,8 +123,14 @@ async function PbsCalculateAllocationsAp(socket, billids) {
if (!billHash[cc.name]) {
billHash[cc.name] = {
Account: cc.dms_acctnumber,
ControlNumber: bill.vendor.dmsid,
Account:
bodyshop.pbs_configuration.appostingaccount === "wip"
? cc.dms_wip_acctnumber
: cc.dms_acctnumber,
ControlNumber:
bodyshop.pbs_configuration.apcontrol === "ro"
? bill.job.ro_number
: bill.vendor.dmsid,
Amount: Dinero(),
// Comment: "String",
AdditionalInfo: bill.vendor.name,

View File

@@ -133,7 +133,13 @@ const generateBillLine = (billLine, responsibilityCenters, jobClass) => {
const findTaxCode = (billLine, taxcode) => {
const {
applicable_taxes: { local, state, federal },
} = billLine;
} =
billLine.applicable_taxes === null
? {
...billLine,
applicable_taxes: { local: false, state: false, federal: false },
}
: billLine;
const t = taxcode.filter(
(t) =>
!!t.local === !!local &&

View File

@@ -262,10 +262,10 @@ const generateInvoiceQbxml = (
RefNumber: jobs_by_pk.ro_number,
BillAddress: {
Addr1: jobs_by_pk.ownr_co_nm
? jobs_by_pk.ownr_co_nm.substring(0, 30)
: `${`${jobs_by_pk.ownr_ln || ""} ${
jobs_by_pk.ownr_fn || ""
}`.substring(0, 30)}`,
? jobs_by_pk.ownr_co_nm.substring(0, 30).trim()
: `${`${jobs_by_pk.ownr_ln || ""} ${jobs_by_pk.ownr_fn || ""}`
.substring(0, 30)
.trim()}`,
Addr2: jobs_by_pk.ownr_addr1,
Addr3: jobs_by_pk.ownr_addr2,
City: jobs_by_pk.ownr_city,
@@ -274,10 +274,10 @@ const generateInvoiceQbxml = (
},
ShipAddress: {
Addr1: jobs_by_pk.ownr_co_nm
? jobs_by_pk.ownr_co_nm.substring(0, 30)
: `${`${jobs_by_pk.ownr_ln || ""} ${
jobs_by_pk.ownr_fn || ""
}`.substring(0, 30)}`,
? jobs_by_pk.ownr_co_nm.substring(0, 30).trim()
: `${`${jobs_by_pk.ownr_ln || ""} ${jobs_by_pk.ownr_fn || ""}`
.substring(0, 30)
.trim()}`,
Addr2: jobs_by_pk.ownr_addr1,
Addr3: jobs_by_pk.ownr_addr2,
City: jobs_by_pk.ownr_city,

View File

@@ -21,9 +21,9 @@ exports.generateOwnerTier = (jobs_by_pk, isThreeTier, twotierpref) => {
? `${jobs_by_pk.ownr_co_nm.substring(0, 30)} #${
jobs_by_pk.owner.accountingid || ""
}`
: `${`${jobs_by_pk.ownr_ln || ""} ${
jobs_by_pk.ownr_fn || ""
}`.substring(0, 30)} #${jobs_by_pk.owner.accountingid || ""}`
: `${`${jobs_by_pk.ownr_ln || ""} ${jobs_by_pk.ownr_fn || ""}`
.substring(0, 30)
.trim()} #${jobs_by_pk.owner.accountingid || ""}`
)
.trim()
.replace(":", " ");
@@ -39,9 +39,9 @@ exports.generateOwnerTier = (jobs_by_pk, isThreeTier, twotierpref) => {
? `${jobs_by_pk.ownr_co_nm.substring(0, 30)} #${
jobs_by_pk.owner.accountingid || ""
}`
: `${`${jobs_by_pk.ownr_ln || ""} ${
jobs_by_pk.ownr_fn || ""
}`.substring(0, 30)} #${jobs_by_pk.owner.accountingid || ""}`
: `${`${jobs_by_pk.ownr_ln || ""} ${jobs_by_pk.ownr_fn || ""}`
.substring(0, 30)
.trim()} #${jobs_by_pk.owner.accountingid || ""}`
)
.trim()
.replace(":", " ");

View File

@@ -717,18 +717,24 @@ async function InsertDmsVehicle(socket) {
dealer: {
dealerNumber: socket.JobData.bodyshop.cdk_dealerid,
...(socket.txEnvelope.inservicedate && {
inServiceDate: moment(socket.txEnvelope.inservicedate)
//.tz(socket.JobData.bodyshop.timezone)
.startOf("day")
.toISOString(),
inServiceDate:
socket.txEnvelope.dms_unsold === true
? ""
: moment(socket.txEnvelope.inservicedate)
//.tz(socket.JobData.bodyshop.timezone)
.startOf("day")
.toISOString(),
}),
vehicleId: socket.DMSVid.vehiclesVehId,
},
manufacturer: {},
vehicle: {
deliveryDate: moment()
// .tz(socket.JobData.bodyshop.timezone)
.format("YYYYMMDD"),
deliveryDate:
socket.txEnvelope.dms_unsold === true
? ""
: moment()
// .tz(socket.JobData.bodyshop.timezone)
.format("YYYYMMDD"),
licensePlateNo:
socket.JobData.plate_no === null
? null
@@ -860,19 +866,25 @@ async function UpdateDmsVehicle(socket) {
...socket.DMSVeh.dealer,
...((socket.txEnvelope.inservicedate ||
socket.DMSVeh.dealer.inServiceDate) && {
inServiceDate: moment(
socket.DMSVeh.dealer.inServiceDate ||
socket.txEnvelope.inservicedate
)
// .tz(socket.JobData.bodyshop.timezone)
.toISOString(),
inServiceDate:
socket.txEnvelope.dms_unsold === true
? ""
: moment(
socket.DMSVeh.dealer.inServiceDate ||
socket.txEnvelope.inservicedate
)
// .tz(socket.JobData.bodyshop.timezone)
.toISOString(),
}),
},
vehicle: {
...socket.DMSVeh.vehicle,
deliveryDate: moment(socket.DMSVeh.vehicle.deliveryDate)
//.tz(socket.JobData.bodyshop.timezone)
.toISOString(),
deliveryDate:
socket.txEnvelope.dms_unsold === true
? ""
: moment(socket.DMSVeh.vehicle.deliveryDate)
//.tz(socket.JobData.bodyshop.timezone)
.toISOString(),
},
owners: ids,
},

View File

@@ -1697,6 +1697,7 @@ query GET_PBS_AP_ALLOCATIONS($billids: [uuid!]) {
md_responsibility_centers
timezone
pbs_serialnumber
pbs_configuration
id
}
bills(where: {id: {_in: $billids}, exported:{_eq: false}}) {

View File

@@ -86,6 +86,7 @@ async function OpenSearchUpdateHandler(req, res) {
"v_model_yr",
"v_make_desc",
"v_model_desc",
"v_vin",
]);
document.bodyshopid = req.body.event.data.new.shopid;
break;
@@ -139,7 +140,7 @@ async function OpenSearchUpdateHandler(req, res) {
"exported_at",
"invoice_number",
"is_credit_memo",
"total"
"total",
]),
...bill.bills_by_pk,
bodyshopid: bill.bills_by_pk.job.shopid,
@@ -244,17 +245,54 @@ async function OpensearchSearchHandler(req, res) {
bool: {
must: [
{
multi_match: {
query: search,
type: "phrase_prefix",
//fields: ["*"],
// fuzziness: "5",
//prefix_length: 2,
match: {
bodyshopid: assocs.associations[0].shopid,
},
},
{
match: {
bodyshopid: assocs.associations[0].shopid,
bool: {
should: [
{
multi_match: {
query: search,
type: "cross_fields",
fields: ["*ownr_fn", "*ownr_ln"],
},
},
{
multi_match: {
query: search,
type: "most_fields",
fields: [
"*v_model_yr",
"*v_make_desc^2",
"*v_model_desc^3",
],
},
},
{
query_string: {
query: `*${search}*`,
fields: [
"*ro_number^20",
"*clm_no^14",
"*v_vin^12",
"*plate_no^12",
"*ownr_ln^10",
"transactionid^10",
"paymentnum^10",
"invoice_number^10",
"*ownr_fn^8",
"*ownr_co_nm^8",
"*ownr_ph1^8",
"*ownr_ph2^8",
"*",
],
},
},
],
minimum_should_match: 1,
},
},
],