Compare commits
46 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2239351f6 | ||
|
|
0e218abbf4 | ||
|
|
0cf7961d7d | ||
|
|
ca02937461 | ||
|
|
fa7e0a107b | ||
|
|
9ccffd73ba | ||
|
|
f68ad181e4 | ||
|
|
23bd6085a8 | ||
|
|
e4325e39bf | ||
|
|
584c2e5de2 | ||
|
|
eccc992cfa | ||
|
|
f293e80d0d | ||
|
|
eea2a758b0 | ||
|
|
8d7d4ab4ac | ||
|
|
f9105806ba | ||
|
|
acd278f5b4 | ||
|
|
a4a6fac224 | ||
|
|
b2b7064007 | ||
|
|
4a56fbb135 | ||
|
|
e1fcb0ecba | ||
|
|
ee5e091118 | ||
|
|
9f46f8ec31 | ||
|
|
8c737ff0ae | ||
|
|
a8ef681cf1 | ||
|
|
494c8b6867 | ||
|
|
262b5600d8 | ||
|
|
1861bd68d1 | ||
|
|
348561c812 | ||
|
|
bf4a52b3c1 | ||
|
|
1950023f37 | ||
|
|
a2798a02b3 | ||
|
|
5fcb5a3a3e | ||
|
|
ba41b29538 | ||
|
|
d187ed6f73 | ||
|
|
13a57406d9 | ||
|
|
68c1ac3e70 | ||
|
|
2f267a9f2c | ||
|
|
c7f293ceca | ||
|
|
66e60e96ad | ||
|
|
54a9beb37f | ||
|
|
779cc7d9e8 | ||
|
|
c1b3df9c3b | ||
|
|
80379cdd79 | ||
|
|
d6fbf02092 | ||
|
|
91c3ac56fa | ||
|
|
7d21cb7d70 |
@@ -6369,6 +6369,27 @@
|
||||
<folder_node>
|
||||
<name>md_parts_scan</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>caseInsensitive</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>expression</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -6390,6 +6411,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>field</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>flags</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -6411,6 +6453,48 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>operation</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>value</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>
|
||||
<concept_node>
|
||||
@@ -11987,6 +12071,158 @@
|
||||
</concept_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
<folder_node>
|
||||
<name>operations</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>contains</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>ends_with</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>equals</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>greater_than</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>less_than</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>not_equals</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>starts_with</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>
|
||||
<folder_node>
|
||||
<name>successes</name>
|
||||
<children>
|
||||
@@ -23131,6 +23367,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>alt_partno</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>amount</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -23236,6 +23493,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>include_in_part_cnt</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>
|
||||
<folder_node>
|
||||
<name>lbr_types</name>
|
||||
<children>
|
||||
|
||||
@@ -85,6 +85,17 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
|
||||
sortOrder: state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.amount}</CurrencyFormatter>
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
sorter: (a, b) => a.type.localeCompare(b.type),
|
||||
sortOrder: state.sortedInfo.columnKey === "type" && state.sortedInfo.order,
|
||||
filters: bodyshop.md_payment_types.map((s) => {
|
||||
return { text: s, value: [s] };
|
||||
}),
|
||||
onFilter: (value, record) => value.includes(record.type)
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.memo"),
|
||||
dataIndex: "memo",
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { AlertFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Divider, Dropdown, Form, Input, notification, Popover, Select, Space } from "antd";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import dayjs from "../../utils/day";
|
||||
import queryString from "query-string";
|
||||
import React, { useContext, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
|
||||
import ScheduleAtChange from "./job-at-change.component";
|
||||
import ScheduleEventColor from "./schedule-event.color.component";
|
||||
import ScheduleEventNote from "./schedule-event.note.component";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -127,6 +127,7 @@ export function ScheduleEventComponent({
|
||||
<DataLabel label={t("jobs.fields.ownr_ph2")}>
|
||||
<ChatOpenButton phone={event.job && event.job.ownr_ph2} jobid={event.job.id} />
|
||||
</DataLabel>
|
||||
<DataLabel hideIfNull label={t("jobs.fields.loss_of_use")}>{(event.job && event.job.loss_of_use) || ""}</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||
{(event.job && event.job.alt_transport) || ""}
|
||||
<ScheduleAtChange job={event && event.job} />
|
||||
|
||||
@@ -519,6 +519,7 @@ export function JobLinesComponent({
|
||||
{selectedLines.length > 0 && ` (${selectedLines.length})`}
|
||||
</Button>
|
||||
<Button
|
||||
id="job-lines-order-parts-button"
|
||||
disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician}
|
||||
onClick={() => {
|
||||
setPartsOrderContext({
|
||||
@@ -541,6 +542,7 @@ export function JobLinesComponent({
|
||||
{selectedLines.length > 0 && ` (${selectedLines.length})`}
|
||||
</Button>
|
||||
<Button
|
||||
id="job-lines-filter-parts-only-button"
|
||||
onClick={() => {
|
||||
setState((state) => ({
|
||||
...state,
|
||||
@@ -554,7 +556,7 @@ export function JobLinesComponent({
|
||||
<FilterFilled /> {t("jobs.actions.filterpartsonly")}
|
||||
</Button>
|
||||
<Dropdown menu={markMenu} trigger={["click"]}>
|
||||
<Button>{t("jobs.actions.mark")}</Button>
|
||||
<Button id="repair-data-mark-button">{t("jobs.actions.mark")}</Button>
|
||||
</Dropdown>
|
||||
<Button
|
||||
disabled={jobRO || technician}
|
||||
|
||||
@@ -192,6 +192,23 @@ export function JobLinesUpsertModalComponent({ bodyshop, open, jobLine, handleCa
|
||||
<Form.Item label={t("joblines.fields.tax_part")} name="tax_part" valuePropName="checked" initialValue={true}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item dependencies={[["act_price"]]} noStyle>
|
||||
{() => {
|
||||
if (form.getFieldValue("act_price") === 0) {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("joblines.fields.include_in_part_cnt")}
|
||||
name="include_in_part_cnt"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}}
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -173,7 +173,7 @@ export function JobsCloseExportButton({
|
||||
}
|
||||
});
|
||||
|
||||
if (!!!jobUpdateResponse.errors) {
|
||||
if (!jobUpdateResponse.errors) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "jobsuccessexport",
|
||||
@@ -222,7 +222,7 @@ export function JobsCloseExportButton({
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled} type="primary">
|
||||
{t("jobs.actions.export")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -982,7 +982,7 @@ export function JobsDetailRatesParts({ jobRO, expanded, required = true, form })
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<LayoutFormRow header={t("jobs.labels.cieca_pfo")}>
|
||||
<Form.Item label={t("jobs.fields.tax_tow_rt")} name="tax_tow_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -10,8 +10,8 @@ import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { UPDATE_JOBS } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -165,7 +165,7 @@ export function JobsExportAllButton({
|
||||
}
|
||||
});
|
||||
|
||||
if (!!!jobUpdateResponse.errors) {
|
||||
if (!jobUpdateResponse.errors) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "jobsuccessexport",
|
||||
@@ -213,13 +213,13 @@ export function JobsExportAllButton({
|
||||
})
|
||||
);
|
||||
|
||||
if (!!completedCallback) completedCallback([]);
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
if (completedCallback) completedCallback([]);
|
||||
if (loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled || jobIds?.length > 10}>
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled || jobIds?.length > 10} type="primary">
|
||||
{t("jobs.actions.exportselected")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { EditFilled } from "@ant-design/icons";
|
||||
import { Alert, Card, Col, Row, Space, Table, Typography } from "antd";
|
||||
import _ from "lodash";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -9,11 +8,11 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component";
|
||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||
import "./labor-allocations-table.styles.scss";
|
||||
import { CalculateAllocationsTotals } from "./labor-allocations-table.utility";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician
|
||||
@@ -65,6 +64,7 @@ export function LaborAllocationsTable({
|
||||
key: "total",
|
||||
sorter: (a, b) => a.total - b.total,
|
||||
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
align: "right",
|
||||
render: (text, record) => record.total.toFixed(1)
|
||||
},
|
||||
{
|
||||
@@ -73,6 +73,7 @@ export function LaborAllocationsTable({
|
||||
key: "hrs_claimed",
|
||||
sorter: (a, b) => a.claimed - b.claimed,
|
||||
sortOrder: state.sortedInfo.columnKey === "claimed" && state.sortedInfo.order,
|
||||
align: "right",
|
||||
render: (text, record) => record.claimed && record.claimed.toFixed(1)
|
||||
},
|
||||
{
|
||||
@@ -81,6 +82,7 @@ export function LaborAllocationsTable({
|
||||
key: "adjustments",
|
||||
sorter: (a, b) => a.adjustments - b.adjustments,
|
||||
sortOrder: state.sortedInfo.columnKey === "adjustments" && state.sortedInfo.order,
|
||||
align: "right",
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
{record.adjustments.toFixed(1)}
|
||||
@@ -100,17 +102,17 @@ export function LaborAllocationsTable({
|
||||
{
|
||||
title: t("jobs.labels.difference"),
|
||||
dataIndex: "difference",
|
||||
|
||||
key: "difference",
|
||||
sorter: (a, b) => a.difference - b.difference,
|
||||
sortOrder: state.sortedInfo.columnKey === "difference" && state.sortedInfo.order,
|
||||
align: "right",
|
||||
render: (text, record) => (
|
||||
<strong
|
||||
style={{
|
||||
color: record.difference >= 0 ? "green" : "red"
|
||||
color: record.difference.toFixed(1) >= 0 ? "green" : "red"
|
||||
}}
|
||||
>
|
||||
{_.round(record.difference, 1)}
|
||||
{(Math.abs(record.difference) < 0.05 ? 0 : record.difference).toFixed(1)}
|
||||
</strong>
|
||||
)
|
||||
}
|
||||
@@ -129,7 +131,6 @@ export function LaborAllocationsTable({
|
||||
ellipsis: true,
|
||||
render: (text, record) => `${record.op_code_desc || ""}${record.alt_partm ? ` ${record.alt_partm}` : ""}`
|
||||
},
|
||||
|
||||
{
|
||||
title: t("joblines.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
@@ -187,7 +188,7 @@ export function LaborAllocationsTable({
|
||||
{ hrs_total: 0, hrs_claimed: 0, adjustments: 0, difference: 0 }
|
||||
);
|
||||
|
||||
if (summary.difference !== 0 && typeof warningCallback === "function") {
|
||||
if (Math.abs(summary.difference.toFixed(1)) !== 0 && typeof warningCallback === "function") {
|
||||
warningCallback({ key: "labor", warning: t("jobs.labels.outstandinghours") });
|
||||
}
|
||||
|
||||
@@ -217,19 +218,21 @@ export function LaborAllocationsTable({
|
||||
summary={() => (
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell>
|
||||
<Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title>
|
||||
<Typography.Title level={4} style={{ margin: 0, lineHeight: 1 }}>
|
||||
{t("general.labels.totals")}
|
||||
</Typography.Title>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>{summary.hrs_total.toFixed(1)}</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>{summary.hrs_claimed.toFixed(1)}</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>{summary.adjustments.toFixed(1)}</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">{summary.hrs_total.toFixed(1)}</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">{summary.hrs_claimed.toFixed(1)}</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">{summary.adjustments.toFixed(1)}</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">
|
||||
<Typography.Text
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
color: summary.difference >= 0 ? "green" : "red"
|
||||
color: summary.difference.toFixed(1) >= 0 ? "green" : "red"
|
||||
}}
|
||||
>
|
||||
{summary.difference.toFixed(1)}
|
||||
{(Math.abs(summary.difference) < 0.05 ? 0 : summary.difference).toFixed(1)}
|
||||
</Typography.Text>
|
||||
</Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
@@ -261,11 +264,10 @@ export function LaborAllocationsTable({
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
{showWarning && summary.difference !== 0 && (
|
||||
{showWarning && Math.abs(summary.difference.toFixed(1)) !== 0 && (
|
||||
<Alert style={{ margin: "8px 0px" }} type="warning" message={t("jobs.labels.outstandinghours")} />
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(LaborAllocationsTable);
|
||||
|
||||
@@ -38,7 +38,11 @@ export default function OwnerFindModalContainer({
|
||||
}, [callSearchowners, modalProps.open, owner]);
|
||||
|
||||
return (
|
||||
<Modal title={t("owners.labels.existing_owners")} width={"80%"} {...modalProps}>
|
||||
<Modal
|
||||
title={<span id="owner-find-modal-title">{t("owners.labels.existing_owners")}</span>}
|
||||
width={"80%"}
|
||||
{...modalProps}
|
||||
>
|
||||
{loading ? <LoadingSpinner /> : null}
|
||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||
{owner ? (
|
||||
|
||||
@@ -48,7 +48,7 @@ export function PayableExportAll({
|
||||
let PartnerResponse;
|
||||
|
||||
setLoading(true);
|
||||
if (!!loadingCallback) loadingCallback(true);
|
||||
if (loadingCallback) loadingCallback(true);
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||
PartnerResponse = await axios.post(`/qbo/payables`, {
|
||||
bills: billids,
|
||||
@@ -85,7 +85,7 @@ export function PayableExportAll({
|
||||
notification["error"]({
|
||||
message: t("bills.errors.exporting-partner")
|
||||
});
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
if (loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -152,7 +152,7 @@ export function PayableExportAll({
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!!!billUpdateResponse.errors) {
|
||||
if (!billUpdateResponse.errors) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "billsuccessexport",
|
||||
@@ -187,8 +187,8 @@ export function PayableExportAll({
|
||||
});
|
||||
|
||||
await Promise.all(proms);
|
||||
if (!!completedCallback) completedCallback([]);
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
if (completedCallback) completedCallback([]);
|
||||
if (loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -200,7 +200,7 @@ export function PayableExportAll({
|
||||
);
|
||||
|
||||
return (
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled || billids?.length > 10}>
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled || billids?.length > 10} type="primary">
|
||||
{t("jobs.actions.exportselected")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -46,7 +46,7 @@ export function PayableExportButton({
|
||||
logImEXEvent("accounting_export_payable");
|
||||
|
||||
setLoading(true);
|
||||
if (!!loadingCallback) loadingCallback(true);
|
||||
if (loadingCallback) loadingCallback(true);
|
||||
|
||||
//Check if it's a QBO Setup.
|
||||
let PartnerResponse;
|
||||
@@ -88,7 +88,7 @@ export function PayableExportButton({
|
||||
notification["error"]({
|
||||
message: t("bills.errors.exporting-partner")
|
||||
});
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
if (loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -149,7 +149,7 @@ export function PayableExportButton({
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!!!billUpdateResponse.errors) {
|
||||
if (!billUpdateResponse.errors) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "billsuccessexport",
|
||||
@@ -186,7 +186,7 @@ export function PayableExportButton({
|
||||
}
|
||||
}
|
||||
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
if (loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -198,7 +198,7 @@ export function PayableExportButton({
|
||||
);
|
||||
|
||||
return (
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled} type="primary">
|
||||
{t("jobs.actions.export")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -55,7 +55,7 @@ export function PaymentExportButton({
|
||||
} else {
|
||||
//Default is QBD
|
||||
|
||||
if (!!loadingCallback) loadingCallback(true);
|
||||
if (loadingCallback) loadingCallback(true);
|
||||
|
||||
let QbXmlResponse;
|
||||
try {
|
||||
@@ -88,7 +88,7 @@ export function PaymentExportButton({
|
||||
notification["error"]({
|
||||
message: t("payments.errors.exporting-partner")
|
||||
});
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
if (loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -148,7 +148,7 @@ export function PaymentExportButton({
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!!!paymentUpdateResponse.errors) {
|
||||
if (!paymentUpdateResponse.errors) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "paymentsuccessexport",
|
||||
@@ -184,12 +184,12 @@ export function PaymentExportButton({
|
||||
)
|
||||
]);
|
||||
}
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
if (loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled} type="primary">
|
||||
{t("jobs.actions.export")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -44,7 +44,7 @@ export function PaymentsExportAllButton({
|
||||
|
||||
const handleQbxml = async () => {
|
||||
setLoading(true);
|
||||
if (!!loadingCallback) loadingCallback(true);
|
||||
if (loadingCallback) loadingCallback(true);
|
||||
let PartnerResponse;
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||
PartnerResponse = await axios.post(`/qbo/payments`, {
|
||||
@@ -76,7 +76,7 @@ export function PaymentsExportAllButton({
|
||||
notification["error"]({
|
||||
message: t("payments.errors.exporting-partner")
|
||||
});
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
if (loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -140,7 +140,7 @@ export function PaymentsExportAllButton({
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!!!paymentUpdateResponse.errors) {
|
||||
if (!paymentUpdateResponse.errors) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "paymentsuccessexport",
|
||||
@@ -174,13 +174,13 @@ export function PaymentsExportAllButton({
|
||||
);
|
||||
});
|
||||
await Promise.all(proms);
|
||||
if (!!completedCallback) completedCallback([]);
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
if (completedCallback) completedCallback([]);
|
||||
if (loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled || paymentIds?.length > 10}>
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled || paymentIds?.length > 10} type="primary">
|
||||
{t("jobs.actions.exportselected")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -35,6 +35,7 @@ const ret = {
|
||||
"bills:reexport": 3,
|
||||
|
||||
"employees:page": 5,
|
||||
"employee_teams:page": 5,
|
||||
|
||||
"owners:list": 2,
|
||||
"owners:detail": 3,
|
||||
@@ -67,6 +68,9 @@ const ret = {
|
||||
"timetickets:list": 3,
|
||||
"timetickets:edit": 4,
|
||||
"timetickets:shiftedit": 5,
|
||||
"timetickets:editcommitted": 5,
|
||||
"ttapprovals:view": 5,
|
||||
"ttapprovals:approve": 5,
|
||||
|
||||
"users:editaccess": 4,
|
||||
|
||||
|
||||
@@ -555,7 +555,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_email_cc", "parts_order"]}
|
||||
label={t("bodyshop.fields.md_email_cc", { template: "parts_order" })}
|
||||
label={t("bodyshop.fields.md_email_cc", { template: "parts_orders" })}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
@@ -567,9 +567,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_email_cc", "parts_return_slip"]}
|
||||
label={t("bodyshop.fields.md_email_cc", {
|
||||
template: "parts_return_slip"
|
||||
})}
|
||||
label={t("bodyshop.fields.md_email_cc", { template: "parts_returns" })}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
@@ -1261,7 +1259,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
key={`${index}prt_dsmk_p`}
|
||||
name={[field.name, "prt_dsmk_p"]}
|
||||
>
|
||||
<InputNumber precision={0} min={0} max={100} />
|
||||
<InputNumber precision={0} min={-100} max={100} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.ah_detail_line")}
|
||||
|
||||
@@ -73,6 +73,7 @@ export const QUERY_PAYMENTS_FOR_EXPORT = gql`
|
||||
transactionid
|
||||
paymentnum
|
||||
date
|
||||
type
|
||||
exportlogs {
|
||||
id
|
||||
successful
|
||||
|
||||
@@ -49,6 +49,7 @@ export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
|
||||
est_ct_fn
|
||||
est_ct_ln
|
||||
comment
|
||||
loss_of_use
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
|
||||
@@ -238,6 +238,7 @@ export const UPDATE_JOB_LINE = gql`
|
||||
convertedtolbr
|
||||
convertedtolbr_data
|
||||
assigned_team
|
||||
include_in_part_cnt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,6 +581,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
status
|
||||
tax_part
|
||||
unq_seq
|
||||
include_in_part_cnt
|
||||
}
|
||||
kmin
|
||||
kmout
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1444,7 +1444,8 @@ export const TemplateList = (type, context) => {
|
||||
object: i18n.t("reportcenter.labels.objects.jobs"),
|
||||
field: i18n.t("jobs.fields.date_exported")
|
||||
},
|
||||
group: "sales"
|
||||
group: "sales",
|
||||
featureNameRestricted: "export"
|
||||
},
|
||||
gsr_by_estimator: {
|
||||
title: i18n.t("reportcenter.templates.gsr_by_estimator"),
|
||||
@@ -1865,7 +1866,8 @@ export const TemplateList = (type, context) => {
|
||||
object: i18n.t("reportcenter.labels.objects.jobs"),
|
||||
field: i18n.t("jobs.fields.date_open")
|
||||
},
|
||||
group: "jobs"
|
||||
group: "jobs",
|
||||
featureNameRestricted: "bills"
|
||||
},
|
||||
psr_by_make: {
|
||||
title: i18n.t("reportcenter.templates.psr_by_make"),
|
||||
@@ -1901,7 +1903,8 @@ export const TemplateList = (type, context) => {
|
||||
object: i18n.t("reportcenter.labels.objects.parts_orders"),
|
||||
field: i18n.t("parts_orders.fields.order_date")
|
||||
},
|
||||
group: "jobs"
|
||||
group: "jobs",
|
||||
featureNameRestricted: "bills"
|
||||
},
|
||||
returns_grouped_by_vendor_detailed: {
|
||||
title: i18n.t("reportcenter.templates.returns_grouped_by_vendor_detailed"),
|
||||
@@ -1913,7 +1916,8 @@ export const TemplateList = (type, context) => {
|
||||
object: i18n.t("reportcenter.labels.objects.parts_orders"),
|
||||
field: i18n.t("parts_orders.fields.order_date")
|
||||
},
|
||||
group: "jobs"
|
||||
group: "jobs",
|
||||
featureNameRestricted: "bills"
|
||||
},
|
||||
scheduled_parts_list: {
|
||||
title: i18n.t("reportcenter.templates.scheduled_parts_list"),
|
||||
@@ -2225,7 +2229,19 @@ export const TemplateList = (type, context) => {
|
||||
field: i18n.t("jobs.fields.date_open")
|
||||
},
|
||||
group: "jobs"
|
||||
}
|
||||
},
|
||||
purchases_by_date_excel: {
|
||||
title: i18n.t("reportcenter.templates.purchases_by_date_excel"),
|
||||
subject: i18n.t("reportcenter.templates.purchases_by_date_excel"),
|
||||
key: "purchases_by_date_excel",
|
||||
reporttype: "excel",
|
||||
disabled: false,
|
||||
rangeFilter: {
|
||||
object: i18n.t("reportcenter.labels.objects.bills"),
|
||||
field: i18n.t("bills.fields.date")
|
||||
},
|
||||
group: "purchases"
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(!type || type === "courtesycarcontract"
|
||||
|
||||
@@ -2997,6 +2997,7 @@
|
||||
- est_seq
|
||||
- glass_flag
|
||||
- id
|
||||
- include_in_part_cnt
|
||||
- ioucreated
|
||||
- jobid
|
||||
- lbr_amt
|
||||
@@ -3066,6 +3067,7 @@
|
||||
- est_seq
|
||||
- glass_flag
|
||||
- id
|
||||
- include_in_part_cnt
|
||||
- ioucreated
|
||||
- jobid
|
||||
- lbr_amt
|
||||
@@ -3146,6 +3148,7 @@
|
||||
- est_seq
|
||||
- glass_flag
|
||||
- id
|
||||
- include_in_part_cnt
|
||||
- ioucreated
|
||||
- jobid
|
||||
- lbr_amt
|
||||
|
||||
@@ -39,50 +39,50 @@ END;
|
||||
$$;
|
||||
CREATE FUNCTION public.assign_ro_number() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
begin
|
||||
IF NEW.converted = true and (new.ro_number is null or new.ro_number = '') THEN
|
||||
UPDATE counters
|
||||
SET count = count + 1 where shopid=new.shopid AND countertype = 'ronum'
|
||||
RETURNING concat(prefix,count) into new.ro_number;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
AS $$
|
||||
begin
|
||||
IF NEW.converted = true and (new.ro_number is null or new.ro_number = '') THEN
|
||||
UPDATE counters
|
||||
SET count = count + 1 where shopid=new.shopid AND countertype = 'ronum'
|
||||
RETURNING concat(prefix,count) into new.ro_number;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
CREATE FUNCTION public.audit_trigger() RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
shopid uuid ;
|
||||
email text;
|
||||
BEGIN
|
||||
select b.id, u.email INTO shopid, email from users u join associations a on u.email = a.useremail join bodyshops b on b.id = a.shopid where u.authid = current_setting('hasura.user', 't')::jsonb->>'x-hasura-user-id' and a.active = true;
|
||||
IF TG_OP = 'INSERT'
|
||||
THEN
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, new_val, recordid, bodyshopid, useremail)
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(NEW), NEW.id, shopid, email);
|
||||
RETURN NEW;
|
||||
ELSIF TG_OP = 'UPDATE'
|
||||
THEN
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, new_val, recordid, bodyshopid, useremail)
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP,
|
||||
json_diff(to_jsonb(OLD), to_jsonb(NEW)) , json_diff(to_jsonb(NEW), to_jsonb(OLD)), OLD.id, shopid, email);
|
||||
RETURN NEW;
|
||||
ELSIF TG_OP = 'DELETE'
|
||||
THEN
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, recordid, bodyshopid, useremail)
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(OLD), OLD.ID, shopid, email);
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
END;
|
||||
AS $$
|
||||
DECLARE
|
||||
shopid uuid ;
|
||||
email text;
|
||||
BEGIN
|
||||
select b.id, u.email INTO shopid, email from users u join associations a on u.email = a.useremail join bodyshops b on b.id = a.shopid where u.authid = current_setting('hasura.user', 't')::jsonb->>'x-hasura-user-id' and a.active = true;
|
||||
IF TG_OP = 'INSERT'
|
||||
THEN
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, new_val, recordid, bodyshopid, useremail)
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(NEW), NEW.id, shopid, email);
|
||||
RETURN NEW;
|
||||
ELSIF TG_OP = 'UPDATE'
|
||||
THEN
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, new_val, recordid, bodyshopid, useremail)
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP,
|
||||
json_diff(to_jsonb(OLD), to_jsonb(NEW)) , json_diff(to_jsonb(NEW), to_jsonb(OLD)), OLD.id, shopid, email);
|
||||
RETURN NEW;
|
||||
ELSIF TG_OP = 'DELETE'
|
||||
THEN
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, recordid, bodyshopid, useremail)
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(OLD), OLD.ID, shopid, email);
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
CREATE FUNCTION public.json_diff(l jsonb, r jsonb) RETURNS jsonb
|
||||
LANGUAGE sql
|
||||
AS $$
|
||||
SELECT jsonb_object_agg(a.key, a.value) FROM
|
||||
( SELECT key, value FROM jsonb_each(l) ) a LEFT OUTER JOIN
|
||||
( SELECT key, value FROM jsonb_each(r) ) b ON a.key = b.key
|
||||
WHERE a.value != b.value OR b.key IS NULL;
|
||||
AS $$
|
||||
SELECT jsonb_object_agg(a.key, a.value) FROM
|
||||
( SELECT key, value FROM jsonb_each(l) ) a LEFT OUTER JOIN
|
||||
( SELECT key, value FROM jsonb_each(r) ) b ON a.key = b.key
|
||||
WHERE a.value != b.value OR b.key IS NULL;
|
||||
$$;
|
||||
CREATE TABLE public.bills (
|
||||
id uuid DEFAULT public.gen_random_uuid() NOT NULL,
|
||||
@@ -211,33 +211,33 @@ CREATE TABLE public.exportlog (
|
||||
);
|
||||
CREATE FUNCTION public.search_exportlog(search text) RETURNS SETOF public.exportlog
|
||||
LANGUAGE plpgsql STABLE
|
||||
AS $$ BEGIN IF search = '' THEN RETURN query
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
exportlog e;
|
||||
ELSE RETURN query
|
||||
SELECT
|
||||
e.*
|
||||
FROM
|
||||
exportlog e
|
||||
LEFT JOIN jobs j on j.id = e.jobid
|
||||
LEFT JOIN payments p
|
||||
ON p.id = e.paymentid
|
||||
LEFT JOIN bills b
|
||||
ON e.billid = b.id
|
||||
WHERE
|
||||
(
|
||||
j.ro_number ILIKE '%' || search || '%'
|
||||
OR b.invoice_number ILIKE '%' || search || '%'
|
||||
OR p.paymentnum ILIKE '%' || search || '%'
|
||||
OR e.useremail ILIKE '%' || search || '%'
|
||||
)
|
||||
AND (e.jobid = j.id
|
||||
or e.paymentid = p.id
|
||||
or e.billid = b.id)
|
||||
;
|
||||
END IF;
|
||||
AS $$ BEGIN IF search = '' THEN RETURN query
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
exportlog e;
|
||||
ELSE RETURN query
|
||||
SELECT
|
||||
e.*
|
||||
FROM
|
||||
exportlog e
|
||||
LEFT JOIN jobs j on j.id = e.jobid
|
||||
LEFT JOIN payments p
|
||||
ON p.id = e.paymentid
|
||||
LEFT JOIN bills b
|
||||
ON e.billid = b.id
|
||||
WHERE
|
||||
(
|
||||
j.ro_number ILIKE '%' || search || '%'
|
||||
OR b.invoice_number ILIKE '%' || search || '%'
|
||||
OR p.paymentnum ILIKE '%' || search || '%'
|
||||
OR e.useremail ILIKE '%' || search || '%'
|
||||
)
|
||||
AND (e.jobid = j.id
|
||||
or e.paymentid = p.id
|
||||
or e.billid = b.id)
|
||||
;
|
||||
END IF;
|
||||
END $$;
|
||||
CREATE TABLE public.jobs (
|
||||
id uuid DEFAULT public.gen_random_uuid() NOT NULL,
|
||||
|
||||
@@ -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 "include_in_part_cnt" boolean
|
||||
-- not null default 'false';
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."joblines" add column "include_in_part_cnt" boolean
|
||||
not null default 'false';
|
||||
10
hasura/migrations/1736890690896_run_sql_migration/down.sql
Normal file
10
hasura/migrations/1736890690896_run_sql_migration/down.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE OR REPLACE VIEW "public"."joblines_status" AS
|
||||
-- SELECT j.jobid,
|
||||
-- j.status,
|
||||
-- count(1) AS count,
|
||||
-- j.part_type
|
||||
-- FROM joblines j
|
||||
-- WHERE ((j.part_type IS NOT NULL) AND (j.part_type <> 'PAE'::text) AND (j.part_type <> 'PAS'::text) AND (j.part_type <> 'PASL'::text) AND ((j.part_qty)::numeric <> (0)::numeric) AND ((j.act_price <> (0)::numeric) OR (j.include_in_part_cnt is TRUE))AND (j.removed IS FALSE))
|
||||
-- GROUP BY j.jobid, j.status, j.part_type;
|
||||
8
hasura/migrations/1736890690896_run_sql_migration/up.sql
Normal file
8
hasura/migrations/1736890690896_run_sql_migration/up.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
CREATE OR REPLACE VIEW "public"."joblines_status" AS
|
||||
SELECT j.jobid,
|
||||
j.status,
|
||||
count(1) AS count,
|
||||
j.part_type
|
||||
FROM joblines j
|
||||
WHERE ((j.part_type IS NOT NULL) AND (j.part_type <> 'PAE'::text) AND (j.part_type <> 'PAS'::text) AND (j.part_type <> 'PASL'::text) AND ((j.part_qty)::numeric <> (0)::numeric) AND ((j.act_price <> (0)::numeric) OR (j.include_in_part_cnt is TRUE))AND (j.removed IS FALSE))
|
||||
GROUP BY j.jobid, j.status, j.part_type;
|
||||
10
hasura/migrations/1736890701752_run_sql_migration/down.sql
Normal file
10
hasura/migrations/1736890701752_run_sql_migration/down.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE OR REPLACE VIEW "public"."joblines_status" AS
|
||||
-- SELECT j.jobid,
|
||||
-- j.status,
|
||||
-- count(1) AS count,
|
||||
-- j.part_type
|
||||
-- FROM joblines j
|
||||
-- WHERE ((j.part_type IS NOT NULL) AND (j.part_type <> 'PAE'::text) AND (j.part_type <> 'PAS'::text) AND (j.part_type <> 'PASL'::text) AND ((j.part_qty)::numeric <> (0)::numeric) AND ((j.act_price <> (0)::numeric) OR (j.include_in_part_cnt is TRUE))AND (j.removed IS FALSE))
|
||||
-- GROUP BY j.jobid, j.status, j.part_type;
|
||||
8
hasura/migrations/1736890701752_run_sql_migration/up.sql
Normal file
8
hasura/migrations/1736890701752_run_sql_migration/up.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
CREATE OR REPLACE VIEW "public"."joblines_status" AS
|
||||
SELECT j.jobid,
|
||||
j.status,
|
||||
count(1) AS count,
|
||||
j.part_type
|
||||
FROM joblines j
|
||||
WHERE ((j.part_type IS NOT NULL) AND (j.part_type <> 'PAE'::text) AND (j.part_type <> 'PAS'::text) AND (j.part_type <> 'PASL'::text) AND ((j.part_qty)::numeric <> (0)::numeric) AND ((j.act_price <> (0)::numeric) OR (j.include_in_part_cnt is TRUE))AND (j.removed IS FALSE))
|
||||
GROUP BY j.jobid, j.status, j.part_type;
|
||||
@@ -39,50 +39,50 @@ END;
|
||||
$$;
|
||||
CREATE FUNCTION public.assign_ro_number() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
begin
|
||||
IF NEW.converted = true and (new.ro_number is null or new.ro_number = '') THEN
|
||||
UPDATE counters
|
||||
SET count = count + 1 where shopid=new.shopid AND countertype = 'ronum'
|
||||
RETURNING concat(prefix,count) into new.ro_number;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
AS $$
|
||||
begin
|
||||
IF NEW.converted = true and (new.ro_number is null or new.ro_number = '') THEN
|
||||
UPDATE counters
|
||||
SET count = count + 1 where shopid=new.shopid AND countertype = 'ronum'
|
||||
RETURNING concat(prefix,count) into new.ro_number;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
CREATE FUNCTION public.audit_trigger() RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
shopid uuid ;
|
||||
email text;
|
||||
BEGIN
|
||||
select b.id, u.email INTO shopid, email from users u join associations a on u.email = a.useremail join bodyshops b on b.id = a.shopid where u.authid = current_setting('hasura.user', 't')::jsonb->>'x-hasura-user-id' and a.active = true;
|
||||
IF TG_OP = 'INSERT'
|
||||
THEN
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, new_val, recordid, bodyshopid, useremail)
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(NEW), NEW.id, shopid, email);
|
||||
RETURN NEW;
|
||||
ELSIF TG_OP = 'UPDATE'
|
||||
THEN
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, new_val, recordid, bodyshopid, useremail)
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP,
|
||||
json_diff(to_jsonb(OLD), to_jsonb(NEW)) , json_diff(to_jsonb(NEW), to_jsonb(OLD)), OLD.id, shopid, email);
|
||||
RETURN NEW;
|
||||
ELSIF TG_OP = 'DELETE'
|
||||
THEN
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, recordid, bodyshopid, useremail)
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(OLD), OLD.ID, shopid, email);
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
END;
|
||||
AS $$
|
||||
DECLARE
|
||||
shopid uuid ;
|
||||
email text;
|
||||
BEGIN
|
||||
select b.id, u.email INTO shopid, email from users u join associations a on u.email = a.useremail join bodyshops b on b.id = a.shopid where u.authid = current_setting('hasura.user', 't')::jsonb->>'x-hasura-user-id' and a.active = true;
|
||||
IF TG_OP = 'INSERT'
|
||||
THEN
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, new_val, recordid, bodyshopid, useremail)
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(NEW), NEW.id, shopid, email);
|
||||
RETURN NEW;
|
||||
ELSIF TG_OP = 'UPDATE'
|
||||
THEN
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, new_val, recordid, bodyshopid, useremail)
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP,
|
||||
json_diff(to_jsonb(OLD), to_jsonb(NEW)) , json_diff(to_jsonb(NEW), to_jsonb(OLD)), OLD.id, shopid, email);
|
||||
RETURN NEW;
|
||||
ELSIF TG_OP = 'DELETE'
|
||||
THEN
|
||||
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, recordid, bodyshopid, useremail)
|
||||
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(OLD), OLD.ID, shopid, email);
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
CREATE FUNCTION public.json_diff(l jsonb, r jsonb) RETURNS jsonb
|
||||
LANGUAGE sql
|
||||
AS $$
|
||||
SELECT jsonb_object_agg(a.key, a.value) FROM
|
||||
( SELECT key, value FROM jsonb_each(l) ) a LEFT OUTER JOIN
|
||||
( SELECT key, value FROM jsonb_each(r) ) b ON a.key = b.key
|
||||
WHERE a.value != b.value OR b.key IS NULL;
|
||||
AS $$
|
||||
SELECT jsonb_object_agg(a.key, a.value) FROM
|
||||
( SELECT key, value FROM jsonb_each(l) ) a LEFT OUTER JOIN
|
||||
( SELECT key, value FROM jsonb_each(r) ) b ON a.key = b.key
|
||||
WHERE a.value != b.value OR b.key IS NULL;
|
||||
$$;
|
||||
CREATE TABLE public.bills (
|
||||
id uuid DEFAULT public.gen_random_uuid() NOT NULL,
|
||||
@@ -211,33 +211,33 @@ CREATE TABLE public.exportlog (
|
||||
);
|
||||
CREATE FUNCTION public.search_exportlog(search text) RETURNS SETOF public.exportlog
|
||||
LANGUAGE plpgsql STABLE
|
||||
AS $$ BEGIN IF search = '' THEN RETURN query
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
exportlog e;
|
||||
ELSE RETURN query
|
||||
SELECT
|
||||
e.*
|
||||
FROM
|
||||
exportlog e
|
||||
LEFT JOIN jobs j on j.id = e.jobid
|
||||
LEFT JOIN payments p
|
||||
ON p.id = e.paymentid
|
||||
LEFT JOIN bills b
|
||||
ON e.billid = b.id
|
||||
WHERE
|
||||
(
|
||||
j.ro_number ILIKE '%' || search || '%'
|
||||
OR b.invoice_number ILIKE '%' || search || '%'
|
||||
OR p.paymentnum ILIKE '%' || search || '%'
|
||||
OR e.useremail ILIKE '%' || search || '%'
|
||||
)
|
||||
AND (e.jobid = j.id
|
||||
or e.paymentid = p.id
|
||||
or e.billid = b.id)
|
||||
;
|
||||
END IF;
|
||||
AS $$ BEGIN IF search = '' THEN RETURN query
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
exportlog e;
|
||||
ELSE RETURN query
|
||||
SELECT
|
||||
e.*
|
||||
FROM
|
||||
exportlog e
|
||||
LEFT JOIN jobs j on j.id = e.jobid
|
||||
LEFT JOIN payments p
|
||||
ON p.id = e.paymentid
|
||||
LEFT JOIN bills b
|
||||
ON e.billid = b.id
|
||||
WHERE
|
||||
(
|
||||
j.ro_number ILIKE '%' || search || '%'
|
||||
OR b.invoice_number ILIKE '%' || search || '%'
|
||||
OR p.paymentnum ILIKE '%' || search || '%'
|
||||
OR e.useremail ILIKE '%' || search || '%'
|
||||
)
|
||||
AND (e.jobid = j.id
|
||||
or e.paymentid = p.id
|
||||
or e.billid = b.id)
|
||||
;
|
||||
END IF;
|
||||
END $$;
|
||||
CREATE TABLE public.jobs (
|
||||
id uuid DEFAULT public.gen_random_uuid() NOT NULL,
|
||||
|
||||
@@ -329,7 +329,8 @@ const main = async () => {
|
||||
main().catch((error) => {
|
||||
logger.log(`Main-API-Error: Something was not caught in the application.`, "error", "api", null, {
|
||||
error: error.message,
|
||||
errorjson: JSON.stringify(error)
|
||||
errorjson: JSON.stringify(error),
|
||||
stack: error.stack
|
||||
});
|
||||
// Note: If we want the app to crash on all uncaught async operations, we would
|
||||
// need to put a `process.exit(1);` here
|
||||
|
||||
@@ -3,3 +3,4 @@ exports.autohouse = require("./autohouse").default;
|
||||
exports.chatter = require("./chatter").default;
|
||||
exports.claimscorp = require("./claimscorp").default;
|
||||
exports.kaizen = require("./kaizen").default;
|
||||
exports.usageReport = require("./usageReport").default;
|
||||
90
server/data/usageReport.js
Normal file
90
server/data/usageReport.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const path = require("path");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||
});
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
const emailer = require("../email/sendemail");
|
||||
const moment = require("moment-timezone");
|
||||
const converter = require("json-2-csv");
|
||||
const logger = require("../utils/logger");
|
||||
const queries = require("../graphql-client/queries");
|
||||
const InstanceMgr = require("../utils/instanceMgr").default;
|
||||
|
||||
exports.default = async (req, res) => {
|
||||
try {
|
||||
logger.log("usage-report-email-start", "debug", req?.user?.email, null, {});
|
||||
|
||||
if (InstanceMgr({ rome: false, imex: true })) {
|
||||
//Disable for ImEX at the moment.
|
||||
res.sendStatus(403);
|
||||
logger.log("usage-report-email-forbidden", "warn", req?.user?.email, null, {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
res.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
// Validate using autohouse token header.
|
||||
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
|
||||
res.sendStatus(401);
|
||||
logger.log("usage-report-email-forbidden", "warn", req?.user?.email, null, {});
|
||||
return;
|
||||
}
|
||||
|
||||
//Query the usage data.
|
||||
const queryResults = await client.request(queries.STATUS_UPDATE, {
|
||||
today: moment().startOf("day").subtract(7, "days"),
|
||||
period: moment().subtract(90, "days").startOf("day")
|
||||
});
|
||||
|
||||
//Massage the data.
|
||||
const shopList = queryResults.bodyshops.map((shop) => ({
|
||||
"Shop Name": shop.shopname,
|
||||
"Days Since Creation": moment().diff(moment(shop.created_at), "days"),
|
||||
"Jobs Created": shop.jobs_created.aggregate.count,
|
||||
"Jobs Updated": shop.jobs_updated.aggregate.count,
|
||||
"Owners Created": shop.owners_created.aggregate.count,
|
||||
"Owners Updated": shop.owners_updated.aggregate.count,
|
||||
"Vehicles Created": shop.vehicles_created.aggregate.count,
|
||||
"Vehicles Updated": shop.vehicles_updated.aggregate.count,
|
||||
"Tasks Created": shop.tasks_created.aggregate.count,
|
||||
"Tasks Updated": shop.tasks_updated.aggregate.count
|
||||
}));
|
||||
|
||||
const csv = converter.json2csv(shopList, { emptyFieldValue: "" });
|
||||
emailer
|
||||
.sendTaskEmail({
|
||||
to: ["patrick.fic@convenient-brands.com", "bradley.rhoades@convenient-brands.com"],
|
||||
subject: `RO Usage Report - ${moment().format("MM/DD/YYYY")}`,
|
||||
text: `
|
||||
Usage Report for ${moment().format("MM/DD/YYYY")} for Rome Online Customers.
|
||||
|
||||
Notes:
|
||||
- Days Since Creation: The number of days since the shop was created. Only shops created in the last 90 days are included.
|
||||
- Updated values should be higher than created values.
|
||||
- Counts are inclusive of the last 7 days of data.
|
||||
`,
|
||||
attachments: [{ filename: `RO Usage Report ${moment().format("MM/DD/YYYY")}.csv`, content: csv }]
|
||||
})
|
||||
.then(() => {
|
||||
logger.log("usage-report-email-success", "debug", req?.user?.email, null, {
|
||||
csv
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.log("usage-report-email-send-error", "ERROR", req?.user?.email, null, {
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
});
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
} catch (error) {
|
||||
logger.log("usage-report-email-error", "ERROR", req?.user?.email, null, {
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
res.status(500).json({ error: error.message, stack: error.stack });
|
||||
}
|
||||
};
|
||||
@@ -2617,3 +2617,76 @@ exports.CREATE_CONVERSATION = `mutation CREATE_CONVERSATION($conversation: [conv
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
exports.STATUS_UPDATE = `query STATUS_UPDATE($period: timestamptz!, $today: timestamptz!) {
|
||||
bodyshops(where: { created_at: { _gte: $period } }) {
|
||||
shopname
|
||||
id
|
||||
created_at
|
||||
jobs_created: jobs_aggregate(where: { created_at: { _gte: $today } }) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
jobs_updated: jobs_aggregate(where: { updated_at: { _gte: $today } }) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
owners_created: owners_aggregate(where: { created_at: { _gte: $today } }) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
owners_updated: owners_aggregate(where: { updated_at: { _gte: $today } }) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
vehicles_created: vehicles_aggregate(
|
||||
where: { created_at: { _gte: $today } }
|
||||
) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
vehicles_updated: vehicles_aggregate(
|
||||
where: { updated_at: { _gte: $today } }
|
||||
) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
tasks_created: tasks_aggregate(where: { created_at: { _gte: $today } }) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
tasks_updated: tasks_aggregate(where: { updated_at: { _gte: $today } }) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
jobs {
|
||||
parts_orders_created: parts_orders_aggregate(
|
||||
where: { created_at: { _gte: $today } }
|
||||
) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
parts_orders_updated: parts_orders_aggregate(
|
||||
where: { updated_at: { _gte: $today } }
|
||||
) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -1,10 +1,11 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { autohouse, claimscorp, chatter, kaizen } = require("../data/data");
|
||||
const { autohouse, claimscorp, chatter, kaizen, usageReport } = require("../data/data");
|
||||
|
||||
router.post("/ah", autohouse);
|
||||
router.post("/cc", claimscorp);
|
||||
router.post("/chatter", chatter);
|
||||
router.post("/kaizen", kaizen);
|
||||
router.post("/usagereport", usageReport);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -41,7 +41,7 @@ exports.taskHandler = async (req, res) => {
|
||||
|
||||
return res.status(200).send(csv);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message, stack: error.stackTrace });
|
||||
res.status(500).json({ error: error.message, stack: error.stack });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user