Compare commits
7 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbd52091d8 | ||
|
|
873eb65e75 | ||
|
|
4b6e140e3e | ||
|
|
8f118937f3 | ||
|
|
cd0a08a7be | ||
|
|
b0ea516fd6 | ||
|
|
10ba19f0d2 |
@@ -1,59 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>IMEX IO Extractor</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
.output-box {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f9f9f9;
|
||||
min-height: 40px;
|
||||
}
|
||||
.copy-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>IMEX IO Extractor</h1>
|
||||
<textarea id="inputText" placeholder="Paste your text here..."></textarea>
|
||||
<br>
|
||||
<button onclick="extractIO()">Extract</button>
|
||||
|
||||
<div class="output-box" id="outputBox" contenteditable="true"></div>
|
||||
<button class="copy-button" onclick="copyToClipboard()">Copy to Clipboard</button>
|
||||
|
||||
<script>
|
||||
function extractIO() {
|
||||
const inputText = document.getElementById('inputText').value;
|
||||
const ioNumbers = [...new Set(inputText.match(/IO-\d{4}/g))] // Extract unique IO-#### matches
|
||||
.map(io => ({ io, num: parseInt(io.split('-')[1]) })) // Extract number part for sorting
|
||||
.sort((a, b) => a.num - b.num) // Sort by the number
|
||||
.map(item => item.io); // Extract sorted IO-####
|
||||
|
||||
document.getElementById('outputBox').innerText = ioNumbers.join(', '); // Display horizontally
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
const outputBox = document.getElementById('outputBox');
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(outputBox);
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand('copy');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4730,27 +4730,6 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>batchid</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>bill_allow_post_to_closed</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -4877,27 +4856,6 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>companycode</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>country</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -5606,53 +5564,6 @@
|
||||
</concept_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
<folder_node>
|
||||
<name>intellipay_config</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>cash_discount_percentage</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>enable_cash_discount</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>
|
||||
<name>invoice_federal_tax_rate</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -11198,48 +11109,6 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>intellipay</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>intellipay_cash_discount</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>jobstatuses</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -22361,27 +22230,6 @@
|
||||
<folder_node>
|
||||
<name>buttons</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>create_short_link</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>goback</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -49787,48 +49635,6 @@
|
||||
<folder_node>
|
||||
<name>templates</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>adp_payroll_flat</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>adp_payroll_straight</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>anticipated_revenue</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
|
||||
@@ -49,23 +49,77 @@
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
<meta name="description" content="Rome Online"/>
|
||||
<title>Rome Online</title>
|
||||
<script type="text/javascript" id="zsiqchat">
|
||||
var $zoho = $zoho || {};
|
||||
$zoho.salesiq = $zoho.salesiq || {
|
||||
widgetcode: "siq01bb8ac617280bdacddfeb528f07734dadc64ef3f05efef9f769c1ec171af666",
|
||||
values: {},
|
||||
ready: function () {
|
||||
}
|
||||
};
|
||||
var d = document;
|
||||
s = d.createElement("script");
|
||||
s.type = "text/javascript";
|
||||
s.id = "zsiqscript";
|
||||
s.defer = true;
|
||||
s.src = "https://salesiq.zohopublic.com/widget";
|
||||
t = d.getElementsByTagName("script")[0];
|
||||
t.parentNode.insertBefore(s, t);
|
||||
</script>
|
||||
|
||||
<!--Use the below code snippet to provide real time updates to the live chat plugin without the need of copying and paste each time to your website when changes are made via PBX-->
|
||||
|
||||
<call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001
|
||||
party="LiveChat528346"></call-us-selector>
|
||||
|
||||
<!--Incase you don't want real time updates to the live chat plugin when options are changed, use the below code snippet. Please note that each time you change the settings you will need to copy and paste the snippet code to your website-->
|
||||
|
||||
<!--<call-us
|
||||
|
||||
phonesystem-url=https://rometech.east.3cx.us:5001
|
||||
|
||||
style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
|
||||
|
||||
id="wp-live-chat-by-3CX"
|
||||
|
||||
minimized="true"
|
||||
|
||||
animation-style="noanimation"
|
||||
|
||||
party="LiveChat528346"
|
||||
|
||||
minimized-style="bubbleright"
|
||||
|
||||
allow-call="true"
|
||||
|
||||
allow-video="false"
|
||||
|
||||
allow-soundnotifications="true"
|
||||
|
||||
enable-mute="true"
|
||||
|
||||
enable-onmobile="true"
|
||||
|
||||
offline-enabled="true"
|
||||
|
||||
enable="true"
|
||||
|
||||
ignore-queueownership="false"
|
||||
|
||||
authentication="both"
|
||||
|
||||
show-operator-actual-name="true"
|
||||
|
||||
aknowledge-received="true"
|
||||
|
||||
gdpr-enabled="false"
|
||||
|
||||
message-userinfo-format="name"
|
||||
|
||||
message-dateformat="both"
|
||||
|
||||
lang="browser"
|
||||
|
||||
button-icon-type="default"
|
||||
|
||||
greeting-visibility="none"
|
||||
|
||||
greeting-offline-visibility="none"
|
||||
|
||||
chat-delay="2000"
|
||||
|
||||
enable-direct-call="true"
|
||||
|
||||
enable-ga="false"
|
||||
|
||||
></call-us>-->
|
||||
|
||||
<script defer src=https://downloads-global.3cx.com/downloads/livechatandtalk/v1/callus.js
|
||||
id="tcx-callus-js" charset="utf-8"></script>
|
||||
|
||||
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
|
||||
<title>ProManager</title>
|
||||
|
||||
9
client/package-lock.json
generated
9
client/package-lock.json
generated
@@ -7,7 +7,6 @@
|
||||
"": {
|
||||
"name": "bodyshop",
|
||||
"version": "0.2.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@ant-design/pro-layout": "^7.19.12",
|
||||
"@apollo/client": "^3.11.4",
|
||||
@@ -48,7 +47,7 @@
|
||||
"query-string": "^9.1.0",
|
||||
"raf-schd": "^4.0.3",
|
||||
"react": "^18.3.1",
|
||||
"react-big-calendar": "^1.13.2",
|
||||
"react-big-calendar": "^1.14.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^7.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -14672,9 +14671,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-big-calendar": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.13.2.tgz",
|
||||
"integrity": "sha512-yzeVRM1I+JloeJXytrZx2lJWKUfLAi5bsgGuBjh3aFSHZrdFcGnfA7LE6pBacdyOG+NGP+332m2MziszkmQWcw==",
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.14.1.tgz",
|
||||
"integrity": "sha512-6Le0kV/4yiV/mlqv5YYBBS+FaBeYBPNGjcYitLoVdPCiXsc0xzSHyX8+2FRqX9AM16XZYIjjomouK3wcnq6+XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.7",
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"query-string": "^9.1.0",
|
||||
"raf-schd": "^4.0.3",
|
||||
"react": "^18.3.1",
|
||||
"react-big-calendar": "^1.13.2",
|
||||
"react-big-calendar": "^1.14.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^7.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -84,7 +84,6 @@
|
||||
"web-vitals": "^3.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "echo 'when updating react-big-calendar, remember to check to localizer in the calendar wrapper'",
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"start": "vite",
|
||||
"build": "dotenvx run --env-file=.env.development.imex -- vite build",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DeleteFilled, CopyFilled } from "@ant-design/icons";
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, message, notification } from "antd";
|
||||
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -14,12 +14,10 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
||||
import { getCurrentUser } from "../../firebase/firebase.utils";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
cardPaymentModal: selectCardPayment,
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: getCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
@@ -27,17 +25,11 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
|
||||
});
|
||||
|
||||
const CardPaymentModalComponent = ({
|
||||
bodyshop,
|
||||
currentUser,
|
||||
cardPaymentModal,
|
||||
toggleModalVisible,
|
||||
insertAuditTrail
|
||||
}) => {
|
||||
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
|
||||
const { context, actions } = cardPaymentModal;
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const [paymentLink, setPaymentLink] = useState();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||
@@ -45,7 +37,7 @@ const CardPaymentModalComponent = ({
|
||||
|
||||
const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
|
||||
variables: { jobids: [context.jobid] },
|
||||
skip: !context?.jobid
|
||||
skip: true
|
||||
});
|
||||
|
||||
//Initialize the intellipay window.
|
||||
@@ -59,7 +51,8 @@ const CardPaymentModalComponent = ({
|
||||
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
|
||||
//Add a slight delay to allow the refetch to properly get the data.
|
||||
setTimeout(() => {
|
||||
if (actions && actions.refetch && typeof actions.refetch === "function") actions.refetch();
|
||||
if (actions && actions.refetch && typeof actions.refetch === "function")
|
||||
actions.refetch();
|
||||
setLoading(false);
|
||||
toggleModalVisible();
|
||||
}, 750);
|
||||
@@ -93,6 +86,7 @@ const CardPaymentModalComponent = ({
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleIntelliPayCharge = async () => {
|
||||
setLoading(true);
|
||||
//Validate
|
||||
@@ -107,7 +101,7 @@ const CardPaymentModalComponent = ({
|
||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||
bodyshop,
|
||||
refresh: !!window.intellipay,
|
||||
paymentSplitMeta: form.getFieldsValue()
|
||||
paymentSplitMeta: form.getFieldsValue(),
|
||||
});
|
||||
|
||||
if (window.intellipay) {
|
||||
@@ -132,42 +126,6 @@ const CardPaymentModalComponent = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleIntelliPayChargeShortLink = async () => {
|
||||
setLoading(true);
|
||||
//Validate
|
||||
try {
|
||||
await form.validateFields();
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { payments } = form.getFieldsValue();
|
||||
const response = await axios.post("/intellipay/generate_payment_url", {
|
||||
bodyshop,
|
||||
amount: payments?.reduce((acc, val) => {
|
||||
return acc + (val?.amount || 0);
|
||||
}, 0),
|
||||
account: payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null,
|
||||
comment: btoa(JSON.stringify({ payments, userEmail: currentUser.email })),
|
||||
paymentSplitMeta: form.getFieldsValue()
|
||||
});
|
||||
if (response.data) {
|
||||
setPaymentLink(response.data?.shorUrl);
|
||||
navigator.clipboard.writeText(response.data?.shorUrl);
|
||||
message.success(t("general.actions.copied"));
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("job_payments.notifications.error.openingip")
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="Card Payment">
|
||||
<Spin spinning={loading}>
|
||||
@@ -244,14 +202,16 @@ const CardPaymentModalComponent = ({
|
||||
|
||||
<Form.Item
|
||||
shouldUpdate={(prevValues, curValues) =>
|
||||
prevValues.payments?.map((p) => p?.jobid + p?.amount).join() !==
|
||||
curValues.payments?.map((p) => p?.jobid + p?.amount).join()
|
||||
prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).join()
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
//If all of the job ids have been fileld in, then query and update the IP field.
|
||||
const { payments } = form.getFieldsValue();
|
||||
if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
|
||||
if (
|
||||
payments?.length > 0 &&
|
||||
payments?.filter((p) => p?.jobid).length === payments?.length
|
||||
) {
|
||||
refetch({ jobids: payments.map((p) => p.jobid) });
|
||||
}
|
||||
return (
|
||||
@@ -286,6 +246,7 @@ const CardPaymentModalComponent = ({
|
||||
const totalAmountToCharge = payments?.reduce((acc, val) => {
|
||||
return acc + (val?.amount || 0);
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<Space style={{ float: "right" }}>
|
||||
<Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} />
|
||||
@@ -312,36 +273,11 @@ const CardPaymentModalComponent = ({
|
||||
>
|
||||
{t("job_payments.buttons.proceedtopayment")}
|
||||
</Button>
|
||||
<Space direction="vertical" align="center">
|
||||
<Button
|
||||
type="primary"
|
||||
// data-ipayname="submit"
|
||||
className="ipayfield"
|
||||
loading={queryLoading || loading}
|
||||
disabled={!(totalAmountToCharge > 0)}
|
||||
onClick={handleIntelliPayChargeShortLink}
|
||||
>
|
||||
{t("job_payments.buttons.create_short_link")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{paymentLink && (
|
||||
<Space
|
||||
style={{ cursor: "pointer", float: "right" }}
|
||||
align="end"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(paymentLink);
|
||||
message.success(t("general.actions.copied"));
|
||||
}}
|
||||
>
|
||||
<div>{paymentLink}</div>
|
||||
<CopyFilled />
|
||||
</Space>
|
||||
)}
|
||||
</Spin>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -11,55 +11,61 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function ScheduleEventColor({ bodyshop, event }) {
|
||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClick = async ({ key }) => {
|
||||
const result = await updateAppointment({
|
||||
variables: {
|
||||
appid: event.id,
|
||||
app: { color: key === "null" ? null : key }
|
||||
}
|
||||
});
|
||||
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({ message: t("appointments.successes.saved") });
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.saving", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
const onClick = useCallback(
|
||||
async ({ key }) => {
|
||||
const result = await updateAppointment({
|
||||
variables: {
|
||||
appid: event.id,
|
||||
app: { color: key === "null" ? null : key }
|
||||
}
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification.success({ message: t("appointments.successes.saved") });
|
||||
} else {
|
||||
notification.error({
|
||||
message: t("appointments.errors.saving", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
},
|
||||
[event.id, t, updateAppointment]
|
||||
);
|
||||
|
||||
const selectedColor = useMemo(() => {
|
||||
if (event.color && bodyshop.appt_colors) {
|
||||
const colorObj = bodyshop.appt_colors.find((color) => color.color.hex === event.color);
|
||||
return colorObj?.label;
|
||||
}
|
||||
};
|
||||
return null;
|
||||
}, [event.color, bodyshop.appt_colors]);
|
||||
|
||||
const selectedColor =
|
||||
event.color &&
|
||||
bodyshop.appt_colors &&
|
||||
bodyshop.appt_colors.filter((color) => color.color.hex === event.color)[0]?.label;
|
||||
|
||||
const menu = {
|
||||
defaultSelectedKeys: [event.color],
|
||||
onClick: onClick,
|
||||
items: [
|
||||
...(bodyshop.appt_colors || []).map((color) => ({
|
||||
key: color.color.hex,
|
||||
label: color.label,
|
||||
style: { color: color.color.hex }
|
||||
})),
|
||||
{ type: "divider" },
|
||||
{ key: "null", label: t("general.actions.clear") }
|
||||
]
|
||||
};
|
||||
const menu = useMemo(
|
||||
() => ({
|
||||
defaultSelectedKeys: [event.color],
|
||||
onClick: onClick,
|
||||
items: [
|
||||
...(bodyshop.appt_colors || []).map((color) => ({
|
||||
key: color.color.hex,
|
||||
label: color.label,
|
||||
style: { color: color.color.hex }
|
||||
})),
|
||||
{ type: "divider" },
|
||||
{ key: "null", label: t("general.actions.clear") }
|
||||
]
|
||||
}),
|
||||
[bodyshop.appt_colors, event.color, onClick, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown menu={menu}>
|
||||
<a href=" #" onClick={(e) => e.preventDefault()}>
|
||||
<a href="#" onClick={(e) => e.preventDefault()}>
|
||||
{selectedColor}
|
||||
<DownOutlined />
|
||||
</a>
|
||||
@@ -67,4 +73,4 @@ export function ScheduleEventColor({ bodyshop, event }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventColor);
|
||||
export default connect(mapStateToProps)(React.memo(ScheduleEventColor));
|
||||
|
||||
@@ -2,11 +2,10 @@ import { AlertFilled } from "@ant-design/icons";
|
||||
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, { useState } from "react";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
@@ -27,6 +26,7 @@ import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
@@ -44,301 +44,319 @@ export function ScheduleEventComponent({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const history = useNavigate();
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||
const [title, setTitle] = useState(event.title);
|
||||
const blockContent = (
|
||||
<Space direction="vertical" wrap>
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.currentTarget.value)}
|
||||
onBlur={async () => {
|
||||
await updateAppointment({
|
||||
variables: {
|
||||
appid: event.id,
|
||||
app: {
|
||||
title: title
|
||||
}
|
||||
},
|
||||
optimisticResponse: {
|
||||
update_appointments: {
|
||||
__typename: "appointments_mutation_response",
|
||||
returning: [
|
||||
{
|
||||
...event,
|
||||
title: title,
|
||||
__typename: "appointments"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button onClick={() => handleCancel({ id: event.id })} disabled={event.arrived}>
|
||||
{t("appointments.actions.unblock")}
|
||||
</Button>
|
||||
</Space>
|
||||
const handleTitleBlur = useCallback(async () => {
|
||||
await updateAppointment({
|
||||
variables: {
|
||||
appid: event.id,
|
||||
app: {
|
||||
title: title
|
||||
}
|
||||
},
|
||||
optimisticResponse: {
|
||||
update_appointments: {
|
||||
__typename: "appointments_mutation_response",
|
||||
returning: [
|
||||
{
|
||||
...event,
|
||||
title: title,
|
||||
__typename: "appointments"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [updateAppointment, event, title]);
|
||||
|
||||
const handleUnblock = useCallback(() => {
|
||||
handleCancel({ id: event.id });
|
||||
}, [handleCancel, event.id]);
|
||||
|
||||
const handlePreviewClick = useCallback(() => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("selected", event.job?.id);
|
||||
navigate({ search: `?${params.toString()}` });
|
||||
}, [navigate, searchParams, event.job?.id]);
|
||||
|
||||
const handleSendEmailReminder = useCallback(() => {
|
||||
const Template = TemplateList("job").appointment_reminder;
|
||||
GenerateDocument(
|
||||
{
|
||||
name: Template.key,
|
||||
variables: { id: event.job.id }
|
||||
},
|
||||
{
|
||||
to: event.job && event.job.ownr_ea,
|
||||
subject: Template.subject
|
||||
},
|
||||
"e",
|
||||
event.job && event.job.id
|
||||
);
|
||||
}, [event.job]);
|
||||
|
||||
const handleSendSMSReminder = useCallback(() => {
|
||||
const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
|
||||
if (p && p.isValid()) {
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: event.job.id
|
||||
});
|
||||
setMessage(
|
||||
t("appointments.labels.reminder", {
|
||||
shopname: bodyshop.shopname,
|
||||
date: dayjs(event.start).format("MM/DD/YYYY"),
|
||||
time: dayjs(event.start).format("HH:mm a")
|
||||
})
|
||||
);
|
||||
setOpen(false);
|
||||
} else {
|
||||
notification.error({
|
||||
message: t("messaging.error.invalidphone")
|
||||
});
|
||||
}
|
||||
}, [event.job, openChatByPhone, setMessage, t, bodyshop.shopname, event.start, setOpen]);
|
||||
|
||||
const reminderMenuItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "email",
|
||||
label: t("general.labels.email"),
|
||||
disabled: event.arrived,
|
||||
onClick: handleSendEmailReminder
|
||||
},
|
||||
{
|
||||
key: "sms",
|
||||
label: t("general.labels.sms"),
|
||||
disabled: event.arrived || !bodyshop.messagingservicesid,
|
||||
onClick: handleSendSMSReminder
|
||||
}
|
||||
],
|
||||
[t, event.arrived, handleSendEmailReminder, handleSendSMSReminder, bodyshop.messagingservicesid]
|
||||
);
|
||||
|
||||
const popoverContent = (
|
||||
<div style={{ maxWidth: "40vw" }}>
|
||||
{!event.isintake ? (
|
||||
<Space>
|
||||
<strong>{event.title}</strong>
|
||||
<ScheduleEventColor event={event} />
|
||||
</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<strong>
|
||||
<OwnerNameDisplay ownerObject={event.job} />
|
||||
</strong>
|
||||
<span style={{ margin: 4 }}>
|
||||
{`${(event.job && event.job.v_model_yr) || ""} ${
|
||||
(event.job && event.job.v_make_desc) || ""
|
||||
} ${(event.job && event.job.v_model_desc) || ""}`}
|
||||
</span>
|
||||
<ScheduleEventColor event={event} />
|
||||
</Space>
|
||||
)}
|
||||
const reminderMenu = useMemo(() => ({ items: reminderMenuItems }), [reminderMenuItems]);
|
||||
|
||||
{event.job ? (
|
||||
<div>
|
||||
<DataLabel label={t("jobs.fields.ro_number")}>{(event.job && event.job.ro_number) || ""}</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.clm_total")}>
|
||||
<CurrencyFormatter>{(event.job && event.job.clm_total) || ""}</CurrencyFormatter>
|
||||
</DataLabel>
|
||||
<DataLabel hideIfNull label={t("jobs.fields.ins_co_nm")}>
|
||||
{(event.job && event.job.ins_co_nm) || ""}
|
||||
</DataLabel>
|
||||
<DataLabel hideIfNull label={t("jobs.fields.clm_no")}>
|
||||
{(event.job && event.job.clm_no) || ""}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ownr_ea")}>{(event.job && event.job.ownr_ea) || ""}</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ownr_ph1")}>
|
||||
<ChatOpenButton phone={event.job && event.job.ownr_ph1} jobid={event.job.id} />
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ownr_ph2")}>
|
||||
<ChatOpenButton phone={event.job && event.job.ownr_ph2} jobid={event.job.id} />
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||
{(event.job && event.job.alt_transport) || ""}
|
||||
<ScheduleAtChange job={event && event.job} />
|
||||
</DataLabel>
|
||||
<ScheduleEventNote event={event} />
|
||||
</div>
|
||||
) : (
|
||||
<div>{event.note || ""}</div>
|
||||
)}
|
||||
<Divider />
|
||||
<Space wrap>
|
||||
{event.job ? (
|
||||
<Link to={`/manage/jobs/${event.job && event.job.id}`}>
|
||||
<Button>{t("appointments.actions.viewjob")}</Button>
|
||||
</Link>
|
||||
) : null}
|
||||
{event.job ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
history({
|
||||
search: queryString.stringify({
|
||||
...searchParams,
|
||||
selected: event.job.id
|
||||
})
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("appointments.actions.preview")}
|
||||
</Button>
|
||||
) : null}
|
||||
{event.job ? (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "email",
|
||||
label: t("general.labels.email"),
|
||||
disabled: event.arrived,
|
||||
onClick: () => {
|
||||
const Template = TemplateList("job").appointment_reminder;
|
||||
GenerateDocument(
|
||||
{
|
||||
name: Template.key,
|
||||
variables: { id: event.job.id }
|
||||
},
|
||||
{
|
||||
to: event.job && event.job.ownr_ea,
|
||||
subject: Template.subject
|
||||
},
|
||||
"e",
|
||||
event.job && event.job.id
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "sms",
|
||||
label: t("general.labels.sms"),
|
||||
disabled: event.arrived || !bodyshop.messagingservicesid,
|
||||
onClick: () => {
|
||||
const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
|
||||
if (p && p.isValid()) {
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: event.job.id
|
||||
});
|
||||
setMessage(
|
||||
t("appointments.labels.reminder", {
|
||||
shopname: bodyshop.shopname,
|
||||
date: dayjs(event.start).format("MM/DD/YYYY"),
|
||||
time: dayjs(event.start).format("HH:mm a")
|
||||
})
|
||||
);
|
||||
setOpen(false);
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("messaging.error.invalidphone")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
>
|
||||
<Button>{t("appointments.actions.sendreminder")}</Button>
|
||||
</Dropdown>
|
||||
) : null}
|
||||
{event.arrived ? (
|
||||
<Button
|
||||
// onClick={() => handleCancel(event.id)}
|
||||
disabled={event.arrived}
|
||||
>
|
||||
{t("appointments.actions.cancel")}
|
||||
</Button>
|
||||
) : (
|
||||
<Popover
|
||||
trigger="click"
|
||||
disabled={event.arrived}
|
||||
content={
|
||||
<Form
|
||||
layout="vertical"
|
||||
onFinish={({ lost_sale_reason }) => {
|
||||
handleCancel({ id: event.id, lost_sale_reason });
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="lost_sale_reason"
|
||||
label={t("jobs.fields.lost_sale_reason")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
|
||||
label: lsr,
|
||||
value: lsr
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button htmlType="submit">{t("appointments.actions.cancel")}</Button>
|
||||
</Form>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
// onClick={() => handleCancel(event.id)}
|
||||
disabled={event.arrived}
|
||||
>
|
||||
{t("appointments.actions.cancel")}
|
||||
</Button>
|
||||
</Popover>
|
||||
)}
|
||||
const handleCancelFormFinish = useCallback(
|
||||
({ lost_sale_reason }) => {
|
||||
handleCancel({ id: event.id, lost_sale_reason });
|
||||
},
|
||||
[handleCancel, event.id]
|
||||
);
|
||||
|
||||
{event.isintake ? (
|
||||
<Button
|
||||
disabled={event.arrived}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
setScheduleContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
jobId: event.job.id,
|
||||
job: event.job,
|
||||
previousEvent: event.id,
|
||||
color: event.color,
|
||||
alt_transport: event.job && event.job.alt_transport,
|
||||
note: event.note
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("appointments.actions.reschedule")}
|
||||
</Button>
|
||||
) : (
|
||||
<ScheduleManualEvent event={event} />
|
||||
)}
|
||||
{event.isintake ? (
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
||||
search: `?appointmentId=${event.id}`
|
||||
}}
|
||||
>
|
||||
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
||||
</Link>
|
||||
) : null}
|
||||
const handleRescheduleClick = useCallback(() => {
|
||||
setOpen(false);
|
||||
setScheduleContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
jobId: event.job.id,
|
||||
job: event.job,
|
||||
previousEvent: event.id,
|
||||
color: event.color,
|
||||
alt_transport: event.job && event.job.alt_transport,
|
||||
note: event.note
|
||||
}
|
||||
});
|
||||
}, [setOpen, setScheduleContext, refetch, event]);
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(vis) => {
|
||||
if (!event.vacation) setOpen(vis);
|
||||
},
|
||||
[event.vacation]
|
||||
);
|
||||
|
||||
const blockContent = useMemo(
|
||||
() => (
|
||||
<Space direction="vertical" wrap>
|
||||
<Input value={title} onChange={(e) => setTitle(e.currentTarget.value)} onBlur={handleTitleBlur} />
|
||||
|
||||
<Button onClick={handleUnblock} disabled={event.arrived}>
|
||||
{t("appointments.actions.unblock")}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
[title, handleTitleBlur, handleUnblock, event.arrived, t]
|
||||
);
|
||||
|
||||
const RegularEvent = event.isintake ? (
|
||||
<Space
|
||||
wrap
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
||||
}}
|
||||
>
|
||||
{event.note && <AlertFilled className="production-alert" />}
|
||||
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
|
||||
const popoverContent = useMemo(() => {
|
||||
console.log("hit");
|
||||
return (
|
||||
<div style={{ maxWidth: "40vw" }}>
|
||||
{!event.isintake ? (
|
||||
<Space>
|
||||
<strong>{event.title}</strong>
|
||||
<ScheduleEventColor event={event} />
|
||||
</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<strong>
|
||||
<OwnerNameDisplay ownerObject={event.job} />
|
||||
</strong>
|
||||
<span style={{ margin: 4 }}>
|
||||
{`${(event.job && event.job.v_model_yr) || ""} ${
|
||||
(event.job && event.job.v_make_desc) || ""
|
||||
} ${(event.job && event.job.v_model_desc) || ""}`}
|
||||
</span>
|
||||
<ScheduleEventColor event={event} />
|
||||
</Space>
|
||||
)}
|
||||
|
||||
<OwnerNameDisplay ownerObject={event.job} />
|
||||
{event.job ? (
|
||||
<div>
|
||||
<DataLabel label={t("jobs.fields.ro_number")}>{(event.job && event.job.ro_number) || ""}</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.clm_total")}>
|
||||
<CurrencyFormatter>{(event.job && event.job.clm_total) || ""}</CurrencyFormatter>
|
||||
</DataLabel>
|
||||
<DataLabel hideIfNull label={t("jobs.fields.ins_co_nm")}>
|
||||
{(event.job && event.job.ins_co_nm) || ""}
|
||||
</DataLabel>
|
||||
<DataLabel hideIfNull label={t("jobs.fields.clm_no")}>
|
||||
{(event.job && event.job.clm_no) || ""}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ownr_ea")}>{(event.job && event.job.ownr_ea) || ""}</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ownr_ph1")}>
|
||||
<ChatOpenButton phone={event.job && event.job.ownr_ph1} jobid={event.job.id} />
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ownr_ph2")}>
|
||||
<ChatOpenButton phone={event.job && event.job.ownr_ph2} jobid={event.job.id} />
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||
{(event.job && event.job.alt_transport) || ""}
|
||||
<ScheduleAtChange job={event && event.job} />
|
||||
</DataLabel>
|
||||
<ScheduleEventNote event={event} />
|
||||
</div>
|
||||
) : (
|
||||
<div>{event.note || ""}</div>
|
||||
)}
|
||||
<Divider />
|
||||
<Space wrap>
|
||||
{event.job ? (
|
||||
<Link to={`/manage/jobs/${event.job && event.job.id}`}>
|
||||
<Button>{t("appointments.actions.viewjob")}</Button>
|
||||
</Link>
|
||||
) : null}
|
||||
{event.job ? <Button onClick={handlePreviewClick}>{t("appointments.actions.preview")}</Button> : null}
|
||||
{event.job ? (
|
||||
<Dropdown menu={reminderMenu}>
|
||||
<Button>{t("appointments.actions.sendreminder")}</Button>
|
||||
</Dropdown>
|
||||
) : null}
|
||||
{event.arrived ? (
|
||||
<Button disabled={event.arrived}>{t("appointments.actions.cancel")}</Button>
|
||||
) : (
|
||||
<Popover
|
||||
trigger="click"
|
||||
disabled={event.arrived}
|
||||
content={
|
||||
<Form layout="vertical" onFinish={handleCancelFormFinish}>
|
||||
<Form.Item
|
||||
name="lost_sale_reason"
|
||||
label={t("jobs.fields.lost_sale_reason")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
|
||||
label: lsr,
|
||||
value: lsr
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button htmlType="submit">{t("appointments.actions.cancel")}</Button>
|
||||
</Form>
|
||||
}
|
||||
>
|
||||
<Button disabled={event.arrived}>{t("appointments.actions.cancel")}</Button>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
{`${(event.job && event.job.v_model_yr) || ""} ${
|
||||
(event.job && event.job.v_make_desc) || ""
|
||||
} ${(event.job && event.job.v_model_desc) || ""}`}
|
||||
{event.isintake ? (
|
||||
<Button disabled={event.arrived} onClick={handleRescheduleClick}>
|
||||
{t("appointments.actions.reschedule")}
|
||||
</Button>
|
||||
) : (
|
||||
<ScheduleManualEvent event={event} />
|
||||
)}
|
||||
{event.isintake ? (
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
||||
search: `?appointmentId=${event.id}`
|
||||
}}
|
||||
>
|
||||
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
||||
</Link>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
event,
|
||||
t,
|
||||
handlePreviewClick,
|
||||
reminderMenu,
|
||||
bodyshop.md_lost_sale_reasons,
|
||||
handleCancelFormFinish,
|
||||
handleRescheduleClick
|
||||
]);
|
||||
|
||||
{`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
|
||||
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
|
||||
})`}
|
||||
const RegularEvent = useMemo(
|
||||
() =>
|
||||
event.isintake ? (
|
||||
<Space
|
||||
wrap
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
||||
}}
|
||||
>
|
||||
{event.note && <AlertFilled className="production-alert" />}
|
||||
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
|
||||
|
||||
{event.job && event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
|
||||
</Space>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
||||
}}
|
||||
>
|
||||
<strong>{`${event.title || ""}`}</strong>
|
||||
</div>
|
||||
<OwnerNameDisplay ownerObject={event.job} />
|
||||
|
||||
{`${(event.job && event.job.v_model_yr) || ""} ${
|
||||
(event.job && event.job.v_make_desc) || ""
|
||||
} ${(event.job && event.job.v_model_desc) || ""}`}
|
||||
|
||||
{`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
|
||||
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
|
||||
})`}
|
||||
|
||||
{event.job && event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
|
||||
</Space>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
||||
}}
|
||||
>
|
||||
<strong>{`${event.title || ""}`}</strong>
|
||||
</div>
|
||||
),
|
||||
[event, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(vis) => !event.vacation && setOpen(vis)}
|
||||
onOpenChange={handleOpenChange}
|
||||
trigger="click"
|
||||
content={event.block ? blockContent : popoverContent}
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
|
||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
||||
}}
|
||||
>
|
||||
@@ -347,4 +365,4 @@ export function ScheduleEventComponent({
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventComponent);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(React.memo(ScheduleEventComponent));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { notification } from "antd";
|
||||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
@@ -10,64 +10,70 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import ScheduleEventComponent from "./schedule-event.component";
|
||||
|
||||
export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
|
||||
function ScheduleEventContainer({ bodyshop, event, refetch }) {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
|
||||
const [updateJob] = useMutation(UPDATE_JOB);
|
||||
const handleCancel = async ({ id, lost_sale_reason }) => {
|
||||
logImEXEvent("schedule_cancel_appt");
|
||||
|
||||
const cancelAppt = await cancelAppointment({
|
||||
variables: { appid: event.id }
|
||||
});
|
||||
notification["success"]({
|
||||
message: t("appointments.successes.canceled")
|
||||
});
|
||||
const handleCancel = useCallback(
|
||||
async ({ id, lost_sale_reason }) => {
|
||||
logImEXEvent("schedule_cancel_appt");
|
||||
|
||||
if (!!cancelAppt.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.canceling", {
|
||||
message: JSON.stringify(cancelAppt.errors)
|
||||
})
|
||||
const cancelAppt = await cancelAppointment({
|
||||
variables: { appid: event.id }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.job) {
|
||||
const jobUpdate = await updateJob({
|
||||
variables: {
|
||||
jobId: event.job.id,
|
||||
job: {
|
||||
date_scheduled: null,
|
||||
scheduled_in: null,
|
||||
scheduled_completion: null,
|
||||
lost_sale_reason,
|
||||
date_lost_sale: new Date(),
|
||||
status: bodyshop.md_ro_statuses.default_imported
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!jobUpdate.errors) {
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
jobid: event.job.id,
|
||||
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
|
||||
type: "appointmentcancel"
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!!jobUpdate.errors) {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.updating", {
|
||||
message: JSON.stringify(jobUpdate.errors)
|
||||
if (!cancelAppt.errors) {
|
||||
notification.success({
|
||||
message: t("appointments.successes.canceled")
|
||||
});
|
||||
} else {
|
||||
notification.error({
|
||||
message: t("appointments.errors.canceling", {
|
||||
message: JSON.stringify(cancelAppt.errors)
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (refetch) refetch();
|
||||
};
|
||||
|
||||
if (event.job) {
|
||||
const jobUpdate = await updateJob({
|
||||
variables: {
|
||||
jobId: event.job.id,
|
||||
job: {
|
||||
date_scheduled: null,
|
||||
scheduled_in: null,
|
||||
scheduled_completion: null,
|
||||
lost_sale_reason,
|
||||
date_lost_sale: new Date(),
|
||||
status: bodyshop.md_ro_statuses.default_imported
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!jobUpdate.errors) {
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
jobid: event.job.id,
|
||||
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
|
||||
type: "appointmentcancel"
|
||||
})
|
||||
);
|
||||
} else {
|
||||
notification.error({
|
||||
message: t("jobs.errors.updating", {
|
||||
message: JSON.stringify(jobUpdate.errors)
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (refetch) refetch();
|
||||
},
|
||||
[cancelAppointment, event.id, event.job, updateJob, bodyshop.md_ro_statuses.default_imported, dispatch, t, refetch]
|
||||
);
|
||||
|
||||
return <ScheduleEventComponent event={event} refetch={refetch} handleCancel={handleCancel} />;
|
||||
}
|
||||
|
||||
export default React.memo(ScheduleEventContainer);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EditFilled, SaveFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Input, notification, Space } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -12,9 +12,6 @@ import DataLabel from "../data-label/data-label.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function ScheduleEventNote({ event }) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
@@ -23,9 +20,9 @@ export function ScheduleEventNote({ event }) {
|
||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggleEdit = async () => {
|
||||
const toggleEdit = useCallback(async () => {
|
||||
if (editing) {
|
||||
//Await the update
|
||||
// Await the update
|
||||
setLoading(true);
|
||||
const result = await updateAppointment({
|
||||
variables: {
|
||||
@@ -34,10 +31,10 @@ export function ScheduleEventNote({ event }) {
|
||||
}
|
||||
});
|
||||
|
||||
if (!!!result.errors) {
|
||||
// notification["success"]({ message: t("appointments.successes.saved") });
|
||||
if (!result.errors) {
|
||||
// notification.success({ message: t("appointments.successes.saved") });
|
||||
} else {
|
||||
notification["error"]({
|
||||
notification.error({
|
||||
message: t("jobs.errors.saving", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
@@ -45,11 +42,15 @@ export function ScheduleEventNote({ event }) {
|
||||
}
|
||||
|
||||
setEditing(false);
|
||||
setLoading(false);
|
||||
} else {
|
||||
setEditing(true);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
}, [editing, note, updateAppointment, event.id, t]);
|
||||
|
||||
const handleNoteChange = useCallback((e) => {
|
||||
setNote(e.target.value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DataLabel label={t("appointments.fields.note")}>
|
||||
@@ -57,7 +58,7 @@ export function ScheduleEventNote({ event }) {
|
||||
{!editing ? (
|
||||
event.note || ""
|
||||
) : (
|
||||
<Input.TextArea rows={3} value={note} onChange={(e) => setNote(e.target.value)} style={{ maxWidth: "8vw" }} />
|
||||
<Input.TextArea rows={3} value={note} onChange={handleNoteChange} style={{ maxWidth: "8vw" }} />
|
||||
)}
|
||||
<Button onClick={toggleEdit} loading={loading}>
|
||||
{editing ? <SaveFilled /> : <EditFilled />}
|
||||
@@ -67,4 +68,4 @@ export function ScheduleEventNote({ event }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventNote);
|
||||
export default connect(mapStateToProps)(React.memo(ScheduleEventNote));
|
||||
|
||||
@@ -250,8 +250,8 @@ export function JobsList({ bodyshop }) {
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.estimator"),
|
||||
dataIndex: "estimator",
|
||||
key: "estimator",
|
||||
dataIndex: "jobs.labels.estimator",
|
||||
key: "jobs.labels.estimator",
|
||||
ellipsis: true,
|
||||
responsive: ["xl"],
|
||||
sorter: (a, b) =>
|
||||
|
||||
@@ -8,12 +8,11 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
@@ -21,7 +20,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PaymentsGenerateLink);
|
||||
|
||||
export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, openChatByPhone, setMessage }) {
|
||||
export function PaymentsGenerateLink({ bodyshop, callback, job, openChatByPhone, setMessage }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
@@ -31,35 +30,29 @@ export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, ope
|
||||
|
||||
const handleFinish = async ({ amount }) => {
|
||||
setLoading(true);
|
||||
let p;
|
||||
try {
|
||||
p = parsePhoneNumber(job.ownr_ph1 || "", "CA");
|
||||
} catch (error) {
|
||||
console.log("Unable to parse phone number");
|
||||
}
|
||||
|
||||
const p = parsePhoneNumber(job.ownr_ph1, "CA");
|
||||
setLoading(true);
|
||||
const response = await axios.post("/intellipay/generate_payment_url", {
|
||||
bodyshop,
|
||||
amount: amount,
|
||||
account: job.ro_number,
|
||||
comment: btoa(JSON.stringify({ payments: [{ jobid: job.id, amount }], userEmail: currentUser.email }))
|
||||
invoice: job.id
|
||||
});
|
||||
setLoading(false);
|
||||
setPaymentLink(response.data.shorUrl);
|
||||
|
||||
if (p) {
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: job.id
|
||||
});
|
||||
setMessage(
|
||||
t("payments.labels.smspaymentreminder", {
|
||||
shopname: bodyshop.shopname,
|
||||
amount: amount,
|
||||
payment_link: response.data.shorUrl
|
||||
})
|
||||
);
|
||||
}
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: job.id
|
||||
});
|
||||
setMessage(
|
||||
t("payments.labels.smspaymentreminder", {
|
||||
shopname: bodyshop.shopname,
|
||||
amount: amount,
|
||||
payment_link: response.data.shorUrl
|
||||
})
|
||||
);
|
||||
|
||||
//Add in confirmation & errors.
|
||||
if (callback) callback();
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import React, { useEffect, useMemo, useRef } from "react";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { useQuery, useSubscription } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
QUERY_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW
|
||||
} from "../../graphql/jobs.queries";
|
||||
import { QUERY_JOBS_IN_PRODUCTION, SUBSCRIPTION_JOBS_IN_PRODUCTION } from "../../graphql/jobs.queries";
|
||||
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||
@@ -16,9 +12,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) {
|
||||
const fired = useRef(false); // useRef to keep track of whether the subscription fired
|
||||
|
||||
function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
||||
const combinedStatuses = useMemo(
|
||||
() => [
|
||||
...bodyshop.md_ro_statuses.production_statuses,
|
||||
@@ -34,12 +28,9 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionTyp
|
||||
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
|
||||
});
|
||||
|
||||
const { data: updatedJobs } = useSubscription(
|
||||
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||
{
|
||||
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
||||
}
|
||||
);
|
||||
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
|
||||
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
||||
});
|
||||
|
||||
const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, {
|
||||
variables: { email: currentUser.email },
|
||||
@@ -49,15 +40,10 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionTyp
|
||||
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
||||
|
||||
useEffect(() => {
|
||||
if (!updatedJobs) {
|
||||
return;
|
||||
if (updatedJobs && data) {
|
||||
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
|
||||
}
|
||||
if (!fired.current) {
|
||||
fired.current = true;
|
||||
return;
|
||||
}
|
||||
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
|
||||
}, [updatedJobs, refetch]);
|
||||
}, [updatedJobs, data, refetch]);
|
||||
|
||||
const filteredAssociationSettings = useMemo(() => {
|
||||
return associationSettings?.associations[0] || null;
|
||||
|
||||
@@ -298,16 +298,6 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => statusSort(a.status, b.status, activeStatuses),
|
||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
filters:
|
||||
activeStatuses
|
||||
?.map((s) => {
|
||||
return {
|
||||
text: s || "No Status*",
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
.sort((a, b) => statusSort(a.text, b.text, activeStatuses)) || [],
|
||||
onFilter: (value, record) => value.includes(record.status),
|
||||
render: (text, record) => <ProductionListColumnStatus record={record} />
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,13 +4,12 @@ import {
|
||||
QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||
QUERY_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION
|
||||
} from "../../graphql/jobs.queries";
|
||||
import ProductionListTable from "./production-list-table.component";
|
||||
import _ from "lodash";
|
||||
|
||||
export default function ProductionListTableContainer({ subscriptionType = "direct" }) {
|
||||
export default function ProductionListTableContainer() {
|
||||
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
|
||||
pollInterval: 3600000,
|
||||
fetchPolicy: "network-only",
|
||||
@@ -18,9 +17,7 @@ export default function ProductionListTableContainer({ subscriptionType = "direc
|
||||
});
|
||||
const client = useApolloClient();
|
||||
const [joblist, setJoblist] = useState([]);
|
||||
const { data: updatedJobs } = useSubscription(
|
||||
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION
|
||||
);
|
||||
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION);
|
||||
|
||||
useEffect(() => {
|
||||
if (!(data && data.jobs)) return;
|
||||
|
||||
@@ -1,50 +1,36 @@
|
||||
import { Space } from "antd";
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectScheduleLoad } from "../../redux/application/application.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
scheduleLoad: selectScheduleLoad
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function ScheduleAtsSummary({ scheduleLoad, appointments }) {
|
||||
const ScheduleAtsSummary = React.memo(function ScheduleAtsSummary({ appointments }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const atsSummary = useMemo(() => {
|
||||
let atsSummary = {};
|
||||
if (!appointments || appointments.length === 0) {
|
||||
return {};
|
||||
}
|
||||
const summary = {};
|
||||
appointments
|
||||
.filter((a) => a.isintake)
|
||||
.filter((a) => a.isintake && a.job?.alt_transport)
|
||||
.forEach((a) => {
|
||||
if (!a.job.alt_transport) return;
|
||||
if (!atsSummary[a.job.alt_transport]) {
|
||||
atsSummary[a.job.alt_transport] = 1;
|
||||
} else {
|
||||
atsSummary[a.job.alt_transport] = atsSummary[a.job.alt_transport] + 1;
|
||||
}
|
||||
const key = a.job.alt_transport;
|
||||
summary[key] = (summary[key] || 0) + 1;
|
||||
});
|
||||
return atsSummary;
|
||||
return summary;
|
||||
}, [appointments]);
|
||||
|
||||
if (Object.keys(atsSummary).length > 0)
|
||||
if (Object.keys(atsSummary).length > 0) {
|
||||
return (
|
||||
<Space wrap>
|
||||
{t("schedule.labels.atssummary")}
|
||||
{Object.keys(atsSummary).map((key) => (
|
||||
<span key={key}>{`${key}: ${atsSummary[key]}`}</span>
|
||||
{Object.entries(atsSummary).map(([key, value]) => (
|
||||
<span key={key}>{`${key}: ${value}`}</span>
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleAtsSummary);
|
||||
export default ScheduleAtsSummary;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Dropdown, notification } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -13,57 +13,61 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function ScheduleBlockDay({ date, children, refetch, bodyshop, alreadyBlocked }) {
|
||||
const ScheduleBlockDay = React.memo(function ScheduleBlockDay({ date, children, refetch, bodyshop, alreadyBlocked }) {
|
||||
const { t } = useTranslation();
|
||||
const [insertBlock] = useMutation(INSERT_APPOINTMENT_BLOCK);
|
||||
|
||||
const handleMenu = async (e) => {
|
||||
e.domEvent.stopPropagation();
|
||||
const handleMenu = useCallback(
|
||||
async (e) => {
|
||||
e.domEvent.stopPropagation();
|
||||
|
||||
if (e.key === "block") {
|
||||
const blockAppt = {
|
||||
title: t("appointments.labels.blocked"),
|
||||
block: true,
|
||||
isintake: false,
|
||||
bodyshopid: bodyshop.id,
|
||||
start: dayjs(date).startOf("day"),
|
||||
end: dayjs(date).endOf("day")
|
||||
};
|
||||
logImEXEvent("dashboard_change_layout");
|
||||
if (e.key === "block") {
|
||||
const blockAppt = {
|
||||
title: t("appointments.labels.blocked"),
|
||||
block: true,
|
||||
isintake: false,
|
||||
bodyshopid: bodyshop.id,
|
||||
start: dayjs(date).startOf("day"),
|
||||
end: dayjs(date).endOf("day")
|
||||
};
|
||||
logImEXEvent("dashboard_change_layout");
|
||||
|
||||
const result = await insertBlock({
|
||||
variables: { app: [blockAppt] }
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.blocking", {
|
||||
message: JSON.stringify(result.errors)
|
||||
})
|
||||
const result = await insertBlock({
|
||||
variables: { app: [blockAppt] }
|
||||
});
|
||||
}
|
||||
|
||||
if (!!refetch) refetch();
|
||||
}
|
||||
};
|
||||
if (result.errors) {
|
||||
notification.error({
|
||||
message: t("appointments.errors.blocking", {
|
||||
message: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const menu = {
|
||||
items: [
|
||||
{
|
||||
key: "block",
|
||||
label: t("appointments.actions.block")
|
||||
if (refetch) refetch();
|
||||
}
|
||||
],
|
||||
onClick: handleMenu
|
||||
};
|
||||
},
|
||||
[t, bodyshop.id, date, insertBlock, refetch]
|
||||
);
|
||||
|
||||
const menu = useMemo(
|
||||
() => ({
|
||||
items: [
|
||||
{
|
||||
key: "block",
|
||||
label: t("appointments.actions.block")
|
||||
}
|
||||
],
|
||||
onClick: handleMenu
|
||||
}),
|
||||
[t, handleMenu]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown menu={menu} disabled={alreadyBlocked} trigger={["contextMenu"]}>
|
||||
{children}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleBlockDay);
|
||||
export default connect(mapStateToProps)(ScheduleBlockDay);
|
||||
|
||||
@@ -1,505 +0,0 @@
|
||||
import isBetween from "dayjs/plugin/isBetween";
|
||||
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
|
||||
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
|
||||
import localeData from "dayjs/plugin/localeData";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import minMax from "dayjs/plugin/minMax";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { DateLocalizer } from "react-big-calendar";
|
||||
|
||||
function arrayWithHoles(arr) {
|
||||
if (Array.isArray(arr)) return arr;
|
||||
}
|
||||
|
||||
function iterableToArrayLimit(arr, i) {
|
||||
if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return;
|
||||
var _arr = [];
|
||||
var _n = true;
|
||||
var _d = false;
|
||||
var _e = undefined;
|
||||
try {
|
||||
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
|
||||
_arr.push(_s.value);
|
||||
if (i && _arr.length === i) break;
|
||||
}
|
||||
} catch (err) {
|
||||
_d = true;
|
||||
_e = err;
|
||||
} finally {
|
||||
try {
|
||||
if (!_n && _i["return"] != null) _i["return"]();
|
||||
} finally {
|
||||
if (_d) throw _e;
|
||||
}
|
||||
}
|
||||
return _arr;
|
||||
}
|
||||
|
||||
function unsupportedIterableToArray(o, minLen) {
|
||||
if (!o) return;
|
||||
if (typeof o === "string") return arrayLikeToArray(o, minLen);
|
||||
var n = Object.prototype.toString.call(o).slice(8, -1);
|
||||
if (n === "Object" && o.constructor) n = o.constructor.name;
|
||||
if (n === "Map" || n === "Set") return Array.from(o);
|
||||
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return arrayLikeToArray(o, minLen);
|
||||
}
|
||||
|
||||
function arrayLikeToArray(arr, len) {
|
||||
if (len == null || len > arr.length) len = arr.length;
|
||||
for (var i = 0, arr2 = new Array(len); i < len; i++) {
|
||||
arr2[i] = arr[i];
|
||||
}
|
||||
return arr2;
|
||||
}
|
||||
|
||||
function nonIterableRest() {
|
||||
throw new TypeError(
|
||||
"Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
|
||||
);
|
||||
}
|
||||
|
||||
function _slicedToArray(arr, i) {
|
||||
return arrayWithHoles(arr) || iterableToArrayLimit(arr, i) || unsupportedIterableToArray(arr, i) || nonIterableRest();
|
||||
}
|
||||
|
||||
function fixUnit(unit) {
|
||||
var datePart = unit ? unit.toLowerCase() : unit;
|
||||
if (datePart === "FullYear") {
|
||||
datePart = "year";
|
||||
} else if (!datePart) {
|
||||
datePart = undefined;
|
||||
}
|
||||
return datePart;
|
||||
}
|
||||
|
||||
var timeRangeFormat = function timeRangeFormat(_ref3, culture, local) {
|
||||
var start = _ref3.start,
|
||||
end = _ref3.end;
|
||||
return local.format(start, "LT", culture) + " – " + local.format(end, "LT", culture);
|
||||
};
|
||||
var timeRangeStartFormat = function timeRangeStartFormat(_ref4, culture, local) {
|
||||
var start = _ref4.start;
|
||||
return local.format(start, "LT", culture) + " – ";
|
||||
};
|
||||
var timeRangeEndFormat = function timeRangeEndFormat(_ref5, culture, local) {
|
||||
var end = _ref5.end;
|
||||
return " – " + local.format(end, "LT", culture);
|
||||
};
|
||||
var weekRangeFormat = function weekRangeFormat(_ref, culture, local) {
|
||||
var start = _ref.start,
|
||||
end = _ref.end;
|
||||
return (
|
||||
local.format(start, "MMMM DD", culture) +
|
||||
" – " +
|
||||
// updated to use this localizer 'eq()' method
|
||||
local.format(end, local.eq(start, end, "month") ? "DD" : "MMMM DD", culture)
|
||||
);
|
||||
};
|
||||
var dateRangeFormat = function dateRangeFormat(_ref2, culture, local) {
|
||||
var start = _ref2.start,
|
||||
end = _ref2.end;
|
||||
return local.format(start, "L", culture) + " – " + local.format(end, "L", culture);
|
||||
};
|
||||
|
||||
var formats = {
|
||||
dateFormat: "DD",
|
||||
dayFormat: "DD ddd",
|
||||
weekdayFormat: "ddd",
|
||||
selectRangeFormat: timeRangeFormat,
|
||||
eventTimeRangeFormat: timeRangeFormat,
|
||||
eventTimeRangeStartFormat: timeRangeStartFormat,
|
||||
eventTimeRangeEndFormat: timeRangeEndFormat,
|
||||
timeGutterFormat: "LT",
|
||||
monthHeaderFormat: "MMMM YYYY",
|
||||
dayHeaderFormat: "dddd MMM DD",
|
||||
dayRangeHeaderFormat: weekRangeFormat,
|
||||
agendaHeaderFormat: dateRangeFormat,
|
||||
agendaDateFormat: "ddd MMM DD",
|
||||
agendaTimeFormat: "LT",
|
||||
agendaTimeRangeFormat: timeRangeFormat
|
||||
};
|
||||
|
||||
const localizer = (dayjsLib) => {
|
||||
// load dayjs plugins
|
||||
dayjsLib.extend(isBetween);
|
||||
dayjsLib.extend(isSameOrAfter);
|
||||
dayjsLib.extend(isSameOrBefore);
|
||||
dayjsLib.extend(localeData);
|
||||
dayjsLib.extend(localizedFormat);
|
||||
dayjsLib.extend(minMax);
|
||||
dayjsLib.extend(utc);
|
||||
var locale = function locale(dj, c) {
|
||||
return c ? dj.locale(c) : dj;
|
||||
};
|
||||
|
||||
// if the timezone plugin is loaded,
|
||||
// then use the timezone aware version
|
||||
|
||||
//TODO This was the issue entirely...
|
||||
// var dayjs = dayjsLib.tz ? dayjsLib.tz : dayjsLib;
|
||||
var dayjs = dayjsLib;
|
||||
|
||||
function getTimezoneOffset(date) {
|
||||
// ensures this gets cast to timezone
|
||||
return dayjs(date).toDate().getTimezoneOffset();
|
||||
}
|
||||
|
||||
function getDstOffset(start, end) {
|
||||
var _st$tz$$x$$timezone;
|
||||
// convert to dayjs, in case
|
||||
var st = dayjs(start);
|
||||
var ed = dayjs(end);
|
||||
// if not using the dayjs timezone plugin
|
||||
if (!dayjs.tz) {
|
||||
return st.toDate().getTimezoneOffset() - ed.toDate().getTimezoneOffset();
|
||||
}
|
||||
/**
|
||||
* If a default timezone has been applied, then
|
||||
* use this to get the proper timezone offset, otherwise default
|
||||
* the timezone to the browser local
|
||||
*/
|
||||
var tzName =
|
||||
(_st$tz$$x$$timezone = st.tz().$x.$timezone) !== null && _st$tz$$x$$timezone !== void 0
|
||||
? _st$tz$$x$$timezone
|
||||
: dayjsLib.tz.guess();
|
||||
// invert offsets to be inline with moment.js
|
||||
var startOffset = -dayjs.tz(+st, tzName).utcOffset();
|
||||
var endOffset = -dayjs.tz(+ed, tzName).utcOffset();
|
||||
return startOffset - endOffset;
|
||||
}
|
||||
|
||||
function getDayStartDstOffset(start) {
|
||||
var dayStart = dayjs(start).startOf("day");
|
||||
return getDstOffset(dayStart, start);
|
||||
}
|
||||
|
||||
/*** BEGIN localized date arithmetic methods with dayjs ***/
|
||||
function defineComparators(a, b, unit) {
|
||||
var datePart = fixUnit(unit);
|
||||
var dtA = datePart ? dayjs(a).startOf(datePart) : dayjs(a);
|
||||
var dtB = datePart ? dayjs(b).startOf(datePart) : dayjs(b);
|
||||
return [dtA, dtB, datePart];
|
||||
}
|
||||
|
||||
function startOf() {
|
||||
var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
|
||||
var unit = arguments.length > 1 ? arguments[1] : undefined;
|
||||
var datePart = fixUnit(unit);
|
||||
if (datePart) {
|
||||
return dayjs(date).startOf(datePart).toDate();
|
||||
}
|
||||
return dayjs(date).toDate();
|
||||
}
|
||||
|
||||
function endOf() {
|
||||
var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
|
||||
var unit = arguments.length > 1 ? arguments[1] : undefined;
|
||||
var datePart = fixUnit(unit);
|
||||
if (datePart) {
|
||||
return dayjs(date).endOf(datePart).toDate();
|
||||
}
|
||||
return dayjs(date).toDate();
|
||||
}
|
||||
|
||||
// dayjs comparison operations *always* convert both sides to dayjs objects
|
||||
// prior to running the comparisons
|
||||
function eq(a, b, unit) {
|
||||
var _defineComparators = defineComparators(a, b, unit),
|
||||
_defineComparators2 = _slicedToArray(_defineComparators, 3),
|
||||
dtA = _defineComparators2[0],
|
||||
dtB = _defineComparators2[1],
|
||||
datePart = _defineComparators2[2];
|
||||
return dtA.isSame(dtB, datePart);
|
||||
}
|
||||
|
||||
function neq(a, b, unit) {
|
||||
return !eq(a, b, unit);
|
||||
}
|
||||
|
||||
function gt(a, b, unit) {
|
||||
var _defineComparators3 = defineComparators(a, b, unit),
|
||||
_defineComparators4 = _slicedToArray(_defineComparators3, 3),
|
||||
dtA = _defineComparators4[0],
|
||||
dtB = _defineComparators4[1],
|
||||
datePart = _defineComparators4[2];
|
||||
return dtA.isAfter(dtB, datePart);
|
||||
}
|
||||
|
||||
function lt(a, b, unit) {
|
||||
var _defineComparators5 = defineComparators(a, b, unit),
|
||||
_defineComparators6 = _slicedToArray(_defineComparators5, 3),
|
||||
dtA = _defineComparators6[0],
|
||||
dtB = _defineComparators6[1],
|
||||
datePart = _defineComparators6[2];
|
||||
return dtA.isBefore(dtB, datePart);
|
||||
}
|
||||
|
||||
function gte(a, b, unit) {
|
||||
var _defineComparators7 = defineComparators(a, b, unit),
|
||||
_defineComparators8 = _slicedToArray(_defineComparators7, 3),
|
||||
dtA = _defineComparators8[0],
|
||||
dtB = _defineComparators8[1],
|
||||
datePart = _defineComparators8[2];
|
||||
return dtA.isSameOrBefore(dtB, datePart);
|
||||
}
|
||||
|
||||
function lte(a, b, unit) {
|
||||
var _defineComparators9 = defineComparators(a, b, unit),
|
||||
_defineComparators10 = _slicedToArray(_defineComparators9, 3),
|
||||
dtA = _defineComparators10[0],
|
||||
dtB = _defineComparators10[1],
|
||||
datePart = _defineComparators10[2];
|
||||
return dtA.isSameOrBefore(dtB, datePart);
|
||||
}
|
||||
|
||||
function inRange(day, min, max) {
|
||||
var unit = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : "day";
|
||||
var datePart = fixUnit(unit);
|
||||
var djDay = dayjs(day);
|
||||
var djMin = dayjs(min);
|
||||
var djMax = dayjs(max);
|
||||
return djDay.isBetween(djMin, djMax, datePart, "[]");
|
||||
}
|
||||
|
||||
function min(dateA, dateB) {
|
||||
var dtA = dayjs(dateA);
|
||||
var dtB = dayjs(dateB);
|
||||
var minDt = dayjsLib.min(dtA, dtB);
|
||||
return minDt.toDate();
|
||||
}
|
||||
|
||||
function max(dateA, dateB) {
|
||||
var dtA = dayjs(dateA);
|
||||
var dtB = dayjs(dateB);
|
||||
var maxDt = dayjsLib.max(dtA, dtB);
|
||||
return maxDt.toDate();
|
||||
}
|
||||
|
||||
function merge(date, time) {
|
||||
if (!date && !time) return null;
|
||||
var tm = dayjs(time).format("HH:mm:ss");
|
||||
var dt = dayjs(date).startOf("day").format("MM/DD/YYYY");
|
||||
// We do it this way to avoid issues when timezone switching
|
||||
return dayjsLib("".concat(dt, " ").concat(tm), "MM/DD/YYYY HH:mm:ss").toDate();
|
||||
}
|
||||
|
||||
function add(date, adder, unit) {
|
||||
var datePart = fixUnit(unit);
|
||||
return dayjs(date).add(adder, datePart).toDate();
|
||||
}
|
||||
|
||||
function range(start, end) {
|
||||
var unit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "day";
|
||||
var datePart = fixUnit(unit);
|
||||
// because the add method will put these in tz, we have to start that way
|
||||
var current = dayjs(start).toDate();
|
||||
var days = [];
|
||||
while (lte(current, end)) {
|
||||
days.push(current);
|
||||
current = add(current, 1, datePart);
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
function ceil(date, unit) {
|
||||
var datePart = fixUnit(unit);
|
||||
var floor = startOf(date, datePart);
|
||||
return eq(floor, date) ? floor : add(floor, 1, datePart);
|
||||
}
|
||||
|
||||
function diff(a, b) {
|
||||
var unit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "day";
|
||||
var datePart = fixUnit(unit);
|
||||
// don't use 'defineComparators' here, as we don't want to mutate the values
|
||||
var dtA = dayjs(a);
|
||||
var dtB = dayjs(b);
|
||||
return dtB.diff(dtA, datePart);
|
||||
}
|
||||
|
||||
function minutes(date) {
|
||||
var dt = dayjs(date);
|
||||
return dt.minutes();
|
||||
}
|
||||
|
||||
function firstOfWeek(culture) {
|
||||
var data = culture ? dayjsLib.localeData(culture) : dayjsLib.localeData();
|
||||
return data ? data.firstDayOfWeek() : 0;
|
||||
}
|
||||
|
||||
function firstVisibleDay(date) {
|
||||
return dayjs(date).startOf("month").startOf("week").toDate();
|
||||
}
|
||||
|
||||
function lastVisibleDay(date) {
|
||||
return dayjs(date).endOf("month").endOf("week").toDate();
|
||||
}
|
||||
|
||||
function visibleDays(date) {
|
||||
var current = firstVisibleDay(date);
|
||||
var last = lastVisibleDay(date);
|
||||
var days = [];
|
||||
while (lte(current, last)) {
|
||||
days.push(current);
|
||||
current = add(current, 1, "d");
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
/*** END localized date arithmetic methods with dayjs ***/
|
||||
|
||||
/**
|
||||
* Moved from TimeSlots.js, this method overrides the method of the same name
|
||||
* in the localizer.js, using dayjs to construct the js Date
|
||||
* @param {Date} dt - date to start with
|
||||
* @param {Number} minutesFromMidnight
|
||||
* @param {Number} offset
|
||||
* @returns {Date}
|
||||
*/
|
||||
function getSlotDate(dt, minutesFromMidnight, offset) {
|
||||
return dayjs(dt)
|
||||
.startOf("day")
|
||||
.minute(minutesFromMidnight + offset)
|
||||
.toDate();
|
||||
}
|
||||
|
||||
// dayjs will automatically handle DST differences in it's calculations
|
||||
function getTotalMin(start, end) {
|
||||
return diff(start, end, "minutes");
|
||||
}
|
||||
|
||||
function getMinutesFromMidnight(start) {
|
||||
var dayStart = dayjs(start).startOf("day");
|
||||
var day = dayjs(start);
|
||||
return day.diff(dayStart, "minutes") + getDayStartDstOffset(start);
|
||||
}
|
||||
|
||||
// These two are used by DateSlotMetrics
|
||||
function continuesPrior(start, first) {
|
||||
var djStart = dayjs(start);
|
||||
var djFirst = dayjs(first);
|
||||
return djStart.isBefore(djFirst, "day");
|
||||
}
|
||||
|
||||
function continuesAfter(start, end, last) {
|
||||
var djEnd = dayjs(end);
|
||||
var djLast = dayjs(last);
|
||||
return djEnd.isSameOrAfter(djLast, "minutes");
|
||||
}
|
||||
|
||||
function daySpan(start, end) {
|
||||
var startDay = dayjs(start);
|
||||
var endDay = dayjs(end);
|
||||
return endDay.diff(startDay, "day");
|
||||
}
|
||||
|
||||
// These two are used by eventLevels
|
||||
function sortEvents(_ref6) {
|
||||
var _ref6$evtA = _ref6.evtA,
|
||||
aStart = _ref6$evtA.start,
|
||||
aEnd = _ref6$evtA.end,
|
||||
aAllDay = _ref6$evtA.allDay,
|
||||
_ref6$evtB = _ref6.evtB,
|
||||
bStart = _ref6$evtB.start,
|
||||
bEnd = _ref6$evtB.end,
|
||||
bAllDay = _ref6$evtB.allDay;
|
||||
var startSort = +startOf(aStart, "day") - +startOf(bStart, "day");
|
||||
var durA = daySpan(aStart, aEnd);
|
||||
var durB = daySpan(bStart, bEnd);
|
||||
return (
|
||||
startSort ||
|
||||
// sort by start Day first
|
||||
durB - durA ||
|
||||
// events spanning multiple days go first
|
||||
!!bAllDay - !!aAllDay ||
|
||||
// then allDay single day events
|
||||
+aStart - +bStart ||
|
||||
// then sort by start time *don't need dayjs conversion here
|
||||
+aEnd - +bEnd // then sort by end time *don't need dayjs conversion here either
|
||||
);
|
||||
}
|
||||
|
||||
function inEventRange(_ref7) {
|
||||
var _ref7$event = _ref7.event,
|
||||
start = _ref7$event.start,
|
||||
end = _ref7$event.end,
|
||||
_ref7$range = _ref7.range,
|
||||
rangeStart = _ref7$range.start,
|
||||
rangeEnd = _ref7$range.end;
|
||||
var startOfDay = dayjs(start).startOf("day");
|
||||
var eEnd = dayjs(end);
|
||||
var rStart = dayjs(rangeStart);
|
||||
var rEnd = dayjs(rangeEnd);
|
||||
var startsBeforeEnd = startOfDay.isSameOrBefore(rEnd, "day");
|
||||
// when the event is zero duration we need to handle a bit differently
|
||||
var sameMin = !startOfDay.isSame(eEnd, "minutes");
|
||||
var endsAfterStart = sameMin ? eEnd.isAfter(rStart, "minutes") : eEnd.isSameOrAfter(rStart, "minutes");
|
||||
return startsBeforeEnd && endsAfterStart;
|
||||
}
|
||||
|
||||
function isSameDate(date1, date2) {
|
||||
var dt = dayjs(date1);
|
||||
var dt2 = dayjs(date2);
|
||||
return dt.isSame(dt2, "day");
|
||||
}
|
||||
|
||||
/**
|
||||
* This method, called once in the localizer constructor, is used by eventLevels
|
||||
* 'eventSegments()' to assist in determining the 'span' of the event in the display,
|
||||
* specifically when using a timezone that is greater than the browser native timezone.
|
||||
* @returns number
|
||||
*/
|
||||
function browserTZOffset() {
|
||||
/**
|
||||
* Date.prototype.getTimezoneOffset horrifically flips the positive/negative from
|
||||
* what you see in it's string, so we have to jump through some hoops to get a value
|
||||
* we can actually compare.
|
||||
*/
|
||||
var dt = new Date();
|
||||
var neg = /-/.test(dt.toString()) ? "-" : "";
|
||||
var dtOffset = dt.getTimezoneOffset();
|
||||
var comparator = Number("".concat(neg).concat(Math.abs(dtOffset)));
|
||||
// dayjs correctly provides positive/negative offset, as expected
|
||||
var mtOffset = dayjs().utcOffset();
|
||||
return mtOffset > comparator ? 1 : 0;
|
||||
}
|
||||
|
||||
return new DateLocalizer({
|
||||
formats: formats,
|
||||
firstOfWeek: firstOfWeek,
|
||||
firstVisibleDay: firstVisibleDay,
|
||||
lastVisibleDay: lastVisibleDay,
|
||||
visibleDays: visibleDays,
|
||||
format: function format(value, _format, culture) {
|
||||
return locale(dayjs(value), culture).format(_format);
|
||||
},
|
||||
lt: lt,
|
||||
lte: lte,
|
||||
gt: gt,
|
||||
gte: gte,
|
||||
eq: eq,
|
||||
neq: neq,
|
||||
merge: merge,
|
||||
inRange: inRange,
|
||||
startOf: startOf,
|
||||
endOf: endOf,
|
||||
range: range,
|
||||
add: add,
|
||||
diff: diff,
|
||||
ceil: ceil,
|
||||
min: min,
|
||||
max: max,
|
||||
minutes: minutes,
|
||||
getSlotDate: getSlotDate,
|
||||
getTimezoneOffset: getTimezoneOffset,
|
||||
getDstOffset: getDstOffset,
|
||||
getTotalMin: getTotalMin,
|
||||
getMinutesFromMidnight: getMinutesFromMidnight,
|
||||
continuesPrior: continuesPrior,
|
||||
continuesAfter: continuesAfter,
|
||||
sortEvents: sortEvents,
|
||||
inEventRange: inEventRange,
|
||||
isSameDate: isSameDate,
|
||||
browserTZOffset: browserTZOffset
|
||||
});
|
||||
};
|
||||
export default localizer;
|
||||
@@ -10,55 +10,48 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
const ScheduleCalendarHeaderGraph = React.memo(function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
|
||||
const { ssbuckets } = bodyshop;
|
||||
const { t } = useTranslation();
|
||||
const data = useMemo(() => {
|
||||
return (
|
||||
(loadData &&
|
||||
loadData.expectedLoad &&
|
||||
Object.keys(loadData.expectedLoad).map((key) => {
|
||||
const metadataBucket = ssbuckets.filter((b) => b.id === key)[0];
|
||||
|
||||
return {
|
||||
bucket: loadData.expectedLoad[key].label,
|
||||
current: loadData.expectedLoad[key].count,
|
||||
target: metadataBucket && metadataBucket.target
|
||||
};
|
||||
})) ||
|
||||
[]
|
||||
);
|
||||
const data = useMemo(() => {
|
||||
if (!loadData || !loadData.expectedLoad || !ssbuckets) return [];
|
||||
|
||||
return Object.keys(loadData.expectedLoad).map((key) => {
|
||||
const metadataBucket = ssbuckets.find((b) => b.id === key);
|
||||
|
||||
return {
|
||||
bucket: loadData.expectedLoad[key].label,
|
||||
current: loadData.expectedLoad[key].count,
|
||||
target: metadataBucket?.target || 0
|
||||
};
|
||||
});
|
||||
}, [loadData, ssbuckets]);
|
||||
|
||||
const popContent = (
|
||||
<div>
|
||||
<Space>
|
||||
{t("appointments.labels.expectedprodhrs")}
|
||||
<strong>{loadData?.expectedHours?.toFixed(1)}</strong>
|
||||
{t("appointments.labels.expectedjobs")}
|
||||
<strong>{loadData?.expectedJobCount}</strong>
|
||||
</Space>
|
||||
<RadarChart
|
||||
// cx={300}
|
||||
// cy={250}
|
||||
// outerRadius={150}
|
||||
width={800}
|
||||
height={600}
|
||||
data={data}
|
||||
>
|
||||
<PolarGrid />
|
||||
<PolarAngleAxis dataKey="bucket" />
|
||||
<PolarRadiusAxis angle={90} />
|
||||
<Radar name="Ideal Load" dataKey="target" stroke="darkgreen" fill="white" fillOpacity={0} />
|
||||
<Radar name="EOD Load" dataKey="current" stroke="dodgerblue" fill="dodgerblue" fillOpacity={0.6} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
</RadarChart>
|
||||
</div>
|
||||
const popContent = useMemo(
|
||||
() => (
|
||||
<div>
|
||||
<Space>
|
||||
{t("appointments.labels.expectedprodhrs")}
|
||||
<strong>{loadData?.expectedHours?.toFixed(1) || 0}</strong>
|
||||
{t("appointments.labels.expectedjobs")}
|
||||
<strong>{loadData?.expectedJobCount || 0}</strong>
|
||||
</Space>
|
||||
<RadarChart width={300} height={250} data={data}>
|
||||
<PolarGrid />
|
||||
<PolarAngleAxis dataKey="bucket" />
|
||||
<PolarRadiusAxis angle={90} />
|
||||
<Radar name="Ideal Load" dataKey="target" stroke="darkgreen" fill="white" fillOpacity={0} />
|
||||
<Radar name="EOD Load" dataKey="current" stroke="dodgerblue" fill="dodgerblue" fillOpacity={0.6} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
</RadarChart>
|
||||
</div>
|
||||
),
|
||||
[t, loadData, data]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -66,6 +59,6 @@ export function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
|
||||
<RadarChartOutlined />
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarHeaderGraph);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import Icon from "@ant-design/icons";
|
||||
import { Popover, Space } from "antd";
|
||||
import _ from "lodash";
|
||||
import dayjs from "../../utils/day";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MdFileDownload, MdFileUpload } from "react-icons/md";
|
||||
import { connect } from "react-redux";
|
||||
@@ -24,115 +23,114 @@ const mapStateToProps = createStructuredSelector({
|
||||
calculating: selectScheduleLoadCalculating
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export function ScheduleCalendarHeaderComponent({
|
||||
export const ScheduleCalendarHeaderComponent = React.memo(function ScheduleCalendarHeaderComponent({
|
||||
bodyshop,
|
||||
label,
|
||||
refetch,
|
||||
date,
|
||||
load,
|
||||
calculating,
|
||||
events,
|
||||
...otherProps
|
||||
events
|
||||
}) {
|
||||
const dayjsDate = useMemo(() => dayjs(date), [date]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ATSToday = useMemo(() => {
|
||||
if (!events) return [];
|
||||
return _.groupBy(
|
||||
events.filter((e) => !e.vacation && e.isintake && dayjs(date).isSame(dayjs(e.start), "day")),
|
||||
"job.alt_transport"
|
||||
);
|
||||
}, [events, date]);
|
||||
const filteredEvents = events.filter((e) => !e.vacation && e.isintake && dayjsDate.isSame(dayjs(e.start), "day"));
|
||||
return _.groupBy(filteredEvents, "job.alt_transport");
|
||||
}, [events, dayjsDate]);
|
||||
|
||||
const isDayBlocked = useMemo(() => {
|
||||
if (!events) return [];
|
||||
return events && events.filter((e) => dayjs(date).isSame(dayjs(e.start), "day") && e.block);
|
||||
}, [events, date]);
|
||||
return events.filter((e) => dayjsDate.isSame(dayjs(e.start), "day") && e.block);
|
||||
}, [events, dayjsDate]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const loadData = load[date.toISOString().substr(0, 10)];
|
||||
const dateString = dayjsDate.format("YYYY-MM-DD");
|
||||
const loadData = load[dateString];
|
||||
|
||||
const jobsOutPopup = () => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<table>
|
||||
<tbody>
|
||||
{loadData && loadData.allJobsOut ? (
|
||||
loadData.allJobsOut.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link> (
|
||||
{j.status})
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{`(${j.labhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0}/${
|
||||
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
|
||||
}/${(
|
||||
j.labhrs.aggregate?.sum?.mod_lb_hrs +
|
||||
j.larhrs.aggregate?.sum?.mod_lb_hrs
|
||||
).toFixed(1)} ${t("general.labels.hours")})`}
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<DateTimeFormatter>
|
||||
{j.scheduled_completion}
|
||||
</DateTimeFormatter>
|
||||
</td>
|
||||
const jobsOutPopup = useCallback(
|
||||
() => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<table>
|
||||
<tbody>
|
||||
{loadData && loadData.allJobsOut && loadData.allJobsOut.length > 0 ? (
|
||||
loadData.allJobsOut.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link> ({j.status})
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{`(${j.labhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0}/${
|
||||
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
|
||||
}/${(
|
||||
(j.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) + (j.larhrs?.aggregate?.sum?.mod_lb_hrs || 0)
|
||||
).toFixed(1)} ${t("general.labels.hours")})`}
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<DateTimeFormatter>{j.scheduled_completion}</DateTimeFormatter>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td style={{ padding: "2.5px" }}>{t("appointments.labels.nocompletingjobs")}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{t("appointments.labels.nocompletingjobs")}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
[loadData, t]
|
||||
);
|
||||
|
||||
const jobsInPopup = () => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<table>
|
||||
<tbody>
|
||||
{loadData && loadData.allJobsIn ? (
|
||||
loadData.allJobsIn.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{`(${j.labhrs?.aggregate?.sum.mod_lb_hrs?.toFixed(1) || 0}/${
|
||||
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
|
||||
}/${(
|
||||
j.labhrs?.aggregate?.sum?.mod_lb_hrs +
|
||||
j.larhrs?.aggregate?.sum?.mod_lb_hrs
|
||||
).toFixed(1)} ${t("general.labels.hours")})`}
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<DateTimeFormatter>{j.scheduled_in}</DateTimeFormatter>
|
||||
</td>
|
||||
const jobsInPopup = useCallback(
|
||||
() => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<table>
|
||||
<tbody>
|
||||
{loadData && loadData.allJobsIn && loadData.allJobsIn.length > 0 ? (
|
||||
loadData.allJobsIn.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{`(${j.labhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0}/${
|
||||
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
|
||||
}/${(
|
||||
(j.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) + (j.larhrs?.aggregate?.sum?.mod_lb_hrs || 0)
|
||||
).toFixed(1)} ${t("general.labels.hours")})`}
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<DateTimeFormatter>{j.scheduled_in}</DateTimeFormatter>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td style={{ padding: "2.5px" }}>{t("appointments.labels.noarrivingjobs")}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{t("appointments.labels.noarrivingjobs")}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
[loadData, t]
|
||||
);
|
||||
|
||||
const LoadComponent = loadData ? (
|
||||
<div>
|
||||
const LoadComponent = useMemo(() => {
|
||||
if (!loadData) return null;
|
||||
return (
|
||||
<div>
|
||||
<Space align="center">
|
||||
<Popover
|
||||
placement={"bottom"}
|
||||
@@ -141,12 +139,8 @@ export function ScheduleCalendarHeaderComponent({
|
||||
title={t("appointments.labels.arrivingjobs")}
|
||||
>
|
||||
<Icon component={MdFileDownload} style={{ color: "green" }} />
|
||||
{(loadData.allHoursInBody || 0) &&
|
||||
loadData.allHoursInBody.toFixed(1)}
|
||||
/
|
||||
{(loadData.allHoursInRefinish || 0) &&
|
||||
loadData.allHoursInRefinish.toFixed(1)}
|
||||
/{(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(1)}
|
||||
{(loadData.allHoursInBody || 0).toFixed(1)}/{(loadData.allHoursInRefinish || 0).toFixed(1)}/
|
||||
{(loadData.allHoursIn || 0).toFixed(1)}
|
||||
</Popover>
|
||||
<Popover
|
||||
placement={"bottom"}
|
||||
@@ -155,57 +149,31 @@ export function ScheduleCalendarHeaderComponent({
|
||||
title={t("appointments.labels.completingjobs")}
|
||||
>
|
||||
<Icon component={MdFileUpload} style={{ color: "red" }} />
|
||||
{(loadData.allHoursOut || 0) && loadData.allHoursOut.toFixed(1)}
|
||||
{(loadData.allHoursOut || 0).toFixed(1)}
|
||||
</Popover>
|
||||
<ScheduleCalendarHeaderGraph loadData={loadData} />
|
||||
</Space>
|
||||
|
||||
<div>
|
||||
<ul style={{ listStyleType: "none", columns: "2 auto", padding: 0 }}>
|
||||
{Object.keys(ATSToday).map((key, idx) => (
|
||||
<li key={idx}>{`${key === "null" || key === "undefined" ? "N/A" : key}: ${ATSToday[key].length}`}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
<ul style={{ listStyleType: "none", columns: "2 auto", padding: 0 }}>
|
||||
{Object.keys(ATSToday).map((key, idx) => (
|
||||
<li key={idx}>{`${key === "null" || key === "undefined" ? "N/A" : key}: ${ATSToday[key].length}`}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const isShopOpen = (date) => {
|
||||
let day;
|
||||
switch (dayjs(date).day()) {
|
||||
case 0:
|
||||
day = "sunday";
|
||||
break;
|
||||
case 1:
|
||||
day = "monday";
|
||||
break;
|
||||
case 2:
|
||||
day = "tuesday";
|
||||
break;
|
||||
case 3:
|
||||
day = "wednesday";
|
||||
break;
|
||||
case 4:
|
||||
day = "thursday";
|
||||
break;
|
||||
case 5:
|
||||
day = "friday";
|
||||
break;
|
||||
case 6:
|
||||
day = "saturday";
|
||||
break;
|
||||
default:
|
||||
day = "sunday";
|
||||
break;
|
||||
}
|
||||
);
|
||||
}, [loadData, jobsInPopup, jobsOutPopup, t, ATSToday]);
|
||||
|
||||
const isShopOpen = useCallback(() => {
|
||||
const days = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
||||
const day = days[dayjsDate.day()];
|
||||
return bodyshop.workingdays[day];
|
||||
};
|
||||
}, [bodyshop, dayjsDate]);
|
||||
|
||||
return (
|
||||
<div className="imex-calendar-load">
|
||||
<ScheduleBlockDay alreadyBlocked={isDayBlocked.length > 0} date={date} refetch={refetch}>
|
||||
<div style={{ color: isShopOpen(date) ? "" : "tomato" }}>
|
||||
<div style={{ color: isShopOpen() ? "" : "tomato" }}>
|
||||
{label}
|
||||
{InstanceRenderMgr({
|
||||
imex: calculating ? <LoadingSkeleton /> : LoadComponent,
|
||||
@@ -216,6 +184,6 @@ export function ScheduleCalendarHeaderComponent({
|
||||
</ScheduleBlockDay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarHeaderComponent);
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import dayjs from "../../utils/day";
|
||||
|
||||
export function getRange(dateParam, viewParam) {
|
||||
let start, end;
|
||||
let date = dateParam || new Date();
|
||||
let view = viewParam || "week";
|
||||
// if view is day: from dayjs(date).startOf('day') to dayjs(date).endOf('day');
|
||||
if (view === "day") {
|
||||
start = dayjs(date).startOf("day");
|
||||
end = dayjs(date).endOf("day");
|
||||
}
|
||||
// if view is week: from dayjs(date).startOf('isoWeek') to dayjs(date).endOf('isoWeek');
|
||||
else if (view === "week") {
|
||||
start = dayjs(date).startOf("week");
|
||||
end = dayjs(date).endOf("week");
|
||||
}
|
||||
//if view is month: from dayjs(date).startOf('month').subtract(7, 'day') to dayjs(date).endOf('month').add(7, 'day'); i do additional 7 days math because you can see adjacent weeks on month view (that is the way how i generate my recurrent events for the Big Calendar, but if you need only start-end of month - just remove that math);
|
||||
else if (view === "month") {
|
||||
start = dayjs(date).startOf("month").subtract(7, "day");
|
||||
end = dayjs(date).endOf("month").add(7, "day");
|
||||
}
|
||||
// if view is agenda: from dayjs(date).startOf('day') to dayjs(date).endOf('day').add(1, 'month');
|
||||
else if (view === "agenda") {
|
||||
start = dayjs(date).startOf("day");
|
||||
end = dayjs(date).endOf("day").add(1, "month");
|
||||
}
|
||||
// Predefine range calculation functions for each view
|
||||
const viewRanges = {
|
||||
day: (date) => ({
|
||||
start: date.startOf("day"),
|
||||
end: date.endOf("day")
|
||||
}),
|
||||
week: (date) => ({
|
||||
start: date.startOf("week"),
|
||||
end: date.endOf("week")
|
||||
}),
|
||||
month: (date) => ({
|
||||
// Adjusting for adjacent weeks
|
||||
start: date.startOf("month").subtract(7, "day"),
|
||||
end: date.endOf("month").add(7, "day")
|
||||
}),
|
||||
agenda: (date) => ({
|
||||
start: date.startOf("day"),
|
||||
end: date.endOf("day").add(1, "month")
|
||||
})
|
||||
};
|
||||
|
||||
return { start, end };
|
||||
export function getRange(dateParam = new Date(), viewParam = "week") {
|
||||
const date = dayjs(dateParam);
|
||||
const view = viewRanges[viewParam] ? viewParam : "week";
|
||||
return viewRanges[view](date);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import dayjs from "../../utils/day";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
import { Calendar } from "react-big-calendar";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { Calendar, dayjsLocalizer } from "react-big-calendar";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -14,15 +14,15 @@ import { selectProblemJobs } from "../../redux/application/application.selectors
|
||||
import { Alert, Collapse, Space } from "antd";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import local from "./localizer";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
problemJobs: selectProblemJobs
|
||||
});
|
||||
const localizer = local(dayjs);
|
||||
|
||||
export function ScheduleCalendarWrapperComponent({
|
||||
const localizer = dayjsLocalizer(dayjs);
|
||||
|
||||
export const ScheduleCalendarWrapperComponent = React.memo(function ScheduleCalendarWrapperComponent({
|
||||
bodyshop,
|
||||
problemJobs,
|
||||
data,
|
||||
@@ -32,23 +32,79 @@ export function ScheduleCalendarWrapperComponent({
|
||||
date,
|
||||
...otherProps
|
||||
}) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useNavigate();
|
||||
const location = useLocation();
|
||||
const search = useMemo(() => queryString.parse(location.search), [location.search]);
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const handleEventPropStyles = (event, start, end, isSelected) => {
|
||||
return {
|
||||
...(event.color && !((search.view || defaultView) === "agenda")
|
||||
? {
|
||||
style: {
|
||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
className: `${event.arrived ? "imex-event-arrived" : ""} ${event.block ? "imex-event-block" : ""}`
|
||||
};
|
||||
};
|
||||
|
||||
const selectedDate = new Date(date || dayjs(search.date) || Date.now());
|
||||
const selectedDate = useMemo(() => {
|
||||
return new Date(date || dayjs(search.date).toDate() || Date.now());
|
||||
}, [date, search.date]);
|
||||
|
||||
const minTime = useMemo(() => {
|
||||
return bodyshop.schedule_start_time ? new Date(bodyshop.schedule_start_time) : new Date("2020-01-01T06:00:00");
|
||||
}, [bodyshop.schedule_start_time]);
|
||||
|
||||
const maxTime = useMemo(() => {
|
||||
return bodyshop.schedule_end_time ? new Date(bodyshop.schedule_end_time) : new Date("2020-01-01T20:00:00");
|
||||
}, [bodyshop.schedule_end_time]);
|
||||
|
||||
const handleEventPropStyles = useCallback(
|
||||
(event, start, end, isSelected) => {
|
||||
return {
|
||||
...(event.color && !((search.view || defaultView) === "agenda")
|
||||
? {
|
||||
style: {
|
||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
className: `${event.arrived ? "imex-event-arrived" : ""} ${event.block ? "imex-event-block" : ""}`
|
||||
};
|
||||
},
|
||||
[search.view, defaultView]
|
||||
);
|
||||
|
||||
const eventComponent = useCallback(
|
||||
(e) => <Event bodyshop={bodyshop} event={e.event} refetch={refetch} />,
|
||||
[bodyshop, refetch]
|
||||
);
|
||||
|
||||
const headerComponent = useCallback(
|
||||
(p) => <HeaderComponent {...p} events={data} refetch={refetch} />,
|
||||
[data, refetch]
|
||||
);
|
||||
|
||||
const calendarComponents = useMemo(
|
||||
() => ({
|
||||
event: eventComponent,
|
||||
header: headerComponent
|
||||
}),
|
||||
[eventComponent, headerComponent]
|
||||
);
|
||||
|
||||
const onNavigate = useCallback(
|
||||
(date, view, action) => {
|
||||
const newSearch = { ...search, date: date.toISOString().substr(0, 10) };
|
||||
navigate({ search: queryString.stringify(newSearch) });
|
||||
},
|
||||
[search, navigate]
|
||||
);
|
||||
|
||||
const onView = useCallback(
|
||||
(view) => {
|
||||
const newSearch = { ...search, view };
|
||||
navigate({ search: queryString.stringify(newSearch) });
|
||||
},
|
||||
[search, navigate]
|
||||
);
|
||||
|
||||
const onRangeChange = useCallback(
|
||||
(range) => {
|
||||
if (setDateRangeCallback) setDateRangeCallback(range);
|
||||
},
|
||||
[setDateRangeCallback]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -110,32 +166,20 @@ export function ScheduleCalendarWrapperComponent({
|
||||
events={data}
|
||||
defaultView={search.view || defaultView || "week"}
|
||||
date={selectedDate}
|
||||
onNavigate={(date, view, action) => {
|
||||
search.date = date.toISOString().substr(0, 10);
|
||||
history({ search: queryString.stringify(search) });
|
||||
}}
|
||||
onRangeChange={(start, end) => {
|
||||
if (setDateRangeCallback) setDateRangeCallback({ start, end });
|
||||
}}
|
||||
onView={(view) => {
|
||||
search.view = view;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}}
|
||||
onNavigate={onNavigate}
|
||||
onRangeChange={onRangeChange}
|
||||
onView={onView}
|
||||
step={15}
|
||||
// timeslots={1}
|
||||
showMultiDayTimes
|
||||
localizer={localizer}
|
||||
min={bodyshop.schedule_start_time ? new Date(bodyshop.schedule_start_time) : new Date("2020-01-01T06:00:00")}
|
||||
max={bodyshop.schedule_end_time ? new Date(bodyshop.schedule_end_time) : new Date("2020-01-01T20:00:00")}
|
||||
min={minTime}
|
||||
max={maxTime}
|
||||
eventPropGetter={handleEventPropStyles}
|
||||
components={{
|
||||
event: (e) => Event({ bodyshop: bodyshop, event: e.event, refetch: refetch }),
|
||||
header: (p) => <HeaderComponent {...p} events={data} refetch={refetch} />
|
||||
}}
|
||||
components={calendarComponents}
|
||||
{...otherProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(ScheduleCalendarWrapperComponent);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Checkbox, Col, Row, Select, Space } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { t } from "i18next";
|
||||
import React, { useMemo } from "react";
|
||||
import React, { Profiler, useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import ScheduleAtsSummary from "../schedule-ats-summary/schedule-ats-summary.component";
|
||||
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
|
||||
@@ -18,19 +18,17 @@ import _ from "lodash";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarComponent);
|
||||
|
||||
export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
||||
const ScheduleCalendarComponent = React.memo(function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [filter, setFilter] = useLocalStorage("filter_events", {
|
||||
intake: true,
|
||||
manual: true,
|
||||
employeevacation: true,
|
||||
ins_co_nm: null
|
||||
});
|
||||
const [estimatorsFilter, setEstimatiorsFilter] = useLocalStorage("estimators", []);
|
||||
const [estimatorsFilter, setEstimatorsFilter] = useLocalStorage("estimators", []);
|
||||
|
||||
const estimators = useMemo(() => {
|
||||
return _.uniq([
|
||||
@@ -48,7 +46,7 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
||||
d.__typename === "appointments"
|
||||
? estimatorsFilter.length === 0
|
||||
? true
|
||||
: !!estimatorsFilter.find((e) => e === `${d.job?.est_ct_fn || ""} ${d.job?.est_ct_ln || ""}`.trim())
|
||||
: estimatorsFilter.includes(`${d.job?.est_ct_fn || ""} ${d.job?.est_ct_ln || ""}`.trim())
|
||||
: true;
|
||||
|
||||
return (
|
||||
@@ -62,7 +60,85 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
||||
});
|
||||
}, [data, filter, estimatorsFilter]);
|
||||
|
||||
const estimatorsOptions = useMemo(() => {
|
||||
return estimators.map((e) => ({
|
||||
label: e,
|
||||
value: e
|
||||
}));
|
||||
}, [estimators]);
|
||||
|
||||
const insCoNmOptions = useMemo(() => {
|
||||
return bodyshop.md_ins_cos.map((i) => ({
|
||||
label: i.name,
|
||||
value: i.name
|
||||
}));
|
||||
}, [bodyshop.md_ins_cos]);
|
||||
|
||||
const handleEstimatorsFilterChange = useCallback(
|
||||
(e) => {
|
||||
setEstimatorsFilter(e);
|
||||
},
|
||||
[setEstimatorsFilter]
|
||||
);
|
||||
|
||||
const handleEstimatorsFilterClear = useCallback(() => {
|
||||
setEstimatorsFilter([]);
|
||||
}, [setEstimatorsFilter]);
|
||||
|
||||
const handleInsCoNmFilterChange = useCallback(
|
||||
(e) => {
|
||||
setFilter((prevFilter) => ({ ...prevFilter, ins_co_nm: e }));
|
||||
},
|
||||
[setFilter]
|
||||
);
|
||||
|
||||
const handleInsCoNmFilterClear = useCallback(() => {
|
||||
setFilter((prevFilter) => ({ ...prevFilter, ins_co_nm: [] }));
|
||||
}, [setFilter]);
|
||||
|
||||
const handleIntakeFilterChange = useCallback(
|
||||
(e) => {
|
||||
const checked = e.target.checked;
|
||||
setFilter((prevFilter) => ({ ...prevFilter, intake: checked }));
|
||||
},
|
||||
[setFilter]
|
||||
);
|
||||
|
||||
const handleManualFilterChange = useCallback(
|
||||
(e) => {
|
||||
const checked = e.target.checked;
|
||||
setFilter((prevFilter) => ({ ...prevFilter, manual: checked }));
|
||||
},
|
||||
[setFilter]
|
||||
);
|
||||
|
||||
const handleEmployeeVacationFilterChange = useCallback(
|
||||
(e) => {
|
||||
const checked = e.target.checked;
|
||||
setFilter((prevFilter) => ({ ...prevFilter, employeevacation: checked }));
|
||||
},
|
||||
[setFilter]
|
||||
);
|
||||
|
||||
const handleRefetch = useCallback(() => {
|
||||
refetch();
|
||||
}, [refetch]);
|
||||
|
||||
return (
|
||||
// TODO Remove when done
|
||||
// <Profiler
|
||||
// id="cal"
|
||||
// onRender={(id, phase, actualDuration, baseDuration, startTime, commitTime) => {
|
||||
// console.dir({
|
||||
// id,
|
||||
// phase,
|
||||
// actualDuration,
|
||||
// baseDuration,
|
||||
// startTime,
|
||||
// commitTime
|
||||
// });
|
||||
// }}
|
||||
// >
|
||||
<Row gutter={[16, 16]}>
|
||||
<ScheduleModal />
|
||||
|
||||
@@ -76,65 +152,35 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
||||
mode="multiple"
|
||||
placeholder={t("schedule.labels.estimators")}
|
||||
allowClear
|
||||
onClear={() => setEstimatiorsFilter([])}
|
||||
value={[...estimatorsFilter]}
|
||||
onChange={(e) => {
|
||||
setEstimatiorsFilter(e);
|
||||
}}
|
||||
options={estimators.map((e) => ({
|
||||
label: e,
|
||||
value: e
|
||||
}))}
|
||||
onClear={handleEstimatorsFilterClear}
|
||||
value={estimatorsFilter}
|
||||
onChange={handleEstimatorsFilterChange}
|
||||
options={estimatorsOptions}
|
||||
/>
|
||||
<Select
|
||||
style={{ minWidth: "15rem" }}
|
||||
mode="multiple"
|
||||
placeholder={t("schedule.labels.ins_co_nm_filter")}
|
||||
allowClear
|
||||
onClear={() => setFilter({ ...filter, ins_co_nm: [] })}
|
||||
value={filter?.ins_co_nm ? filter.ins_co_nm : []}
|
||||
onChange={(e) => {
|
||||
setFilter({ ...filter, ins_co_nm: e });
|
||||
}}
|
||||
options={bodyshop.md_ins_cos.map((i) => ({
|
||||
label: i.name,
|
||||
value: i.name
|
||||
}))}
|
||||
onClear={handleInsCoNmFilterClear}
|
||||
value={filter.ins_co_nm || []}
|
||||
onChange={handleInsCoNmFilterChange}
|
||||
options={insCoNmOptions}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={filter?.intake}
|
||||
onChange={(e) => {
|
||||
setFilter({ ...filter, intake: e.target.checked });
|
||||
}}
|
||||
>
|
||||
<Checkbox checked={filter.intake} onChange={handleIntakeFilterChange}>
|
||||
{t("schedule.labels.intake")}
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={filter?.manual}
|
||||
onChange={(e) => {
|
||||
setFilter({ ...filter, manual: e.target.checked });
|
||||
}}
|
||||
>
|
||||
<Checkbox checked={filter.manual} onChange={handleManualFilterChange}>
|
||||
{t("schedule.labels.manual")}
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={filter?.employeevacation}
|
||||
onChange={(e) => {
|
||||
setFilter({ ...filter, employeevacation: e.target.checked });
|
||||
}}
|
||||
>
|
||||
<Checkbox checked={filter.employeevacation} onChange={handleEmployeeVacationFilterChange}>
|
||||
{t("schedule.labels.employeevacation")}
|
||||
</Checkbox>
|
||||
<ScheduleVerifyIntegrity />
|
||||
<Button
|
||||
onClick={() => {
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button onClick={handleRefetch}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<ScheduleProductionList />
|
||||
|
||||
<ScheduleManualEvent />
|
||||
</Space>
|
||||
}
|
||||
@@ -147,5 +193,9 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
// TODO Remove when done
|
||||
// </Profiler>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(ScheduleCalendarComponent);
|
||||
|
||||
@@ -15,56 +15,65 @@ import dayjs from "../../utils/day";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
calculateScheduleLoad: (endDate) => dispatch(calculateScheduleLoad(endDate))
|
||||
});
|
||||
|
||||
export function ScheduleCalendarContainer({ calculateScheduleLoad }) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const ScheduleCalendarContainer = React.memo(function ScheduleCalendarContainer({ calculateScheduleLoad }) {
|
||||
const location = useLocation();
|
||||
const search = useMemo(() => queryString.parse(location.search), [location.search]);
|
||||
|
||||
const { date, view } = search;
|
||||
const range = useMemo(() => getRange(date, view), [date, view]);
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_APPOINTMENTS, {
|
||||
variables: {
|
||||
const queryVariables = useMemo(
|
||||
() => ({
|
||||
start: range.start.toDate(),
|
||||
end: range.end.toDate(),
|
||||
startd: range.start,
|
||||
endd: range.end
|
||||
},
|
||||
skip: !!!range.start || !!!range.end,
|
||||
}),
|
||||
[range]
|
||||
);
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_APPOINTMENTS, {
|
||||
variables: queryVariables,
|
||||
skip: !range.start || !range.end,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data && range.end) calculateScheduleLoad(range.end);
|
||||
}, [data, range, calculateScheduleLoad]);
|
||||
if (data && range.end) {
|
||||
calculateScheduleLoad(range.end);
|
||||
}
|
||||
}, [data, range.end, calculateScheduleLoad]);
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
let normalizedData = [
|
||||
...data.appointments.map((e) => {
|
||||
//Required because Hasura returns a string instead of a date object.
|
||||
return Object.assign({}, e, { start: new Date(e.start) }, { end: new Date(e.end) });
|
||||
}),
|
||||
...data.employee_vacation.map((e) => {
|
||||
//Required because Hasura returns a string instead of a date object.
|
||||
return {
|
||||
const normalizedData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return [
|
||||
...data.appointments.map((e) => ({
|
||||
...e,
|
||||
title: `${
|
||||
(e.employee.first_name && e.employee.first_name.substr(0, 1)) || ""
|
||||
} ${e.employee.last_name || ""} OUT`,
|
||||
start: new Date(e.start),
|
||||
end: new Date(e.end)
|
||||
})),
|
||||
...data.employee_vacation.map((e) => ({
|
||||
...e,
|
||||
title: `${e.employee.first_name?.[0] || ""} ${e.employee.last_name || ""} OUT`,
|
||||
color: "red",
|
||||
start: dayjs(e.start).startOf("day").toDate(),
|
||||
end: dayjs(e.end).startOf("day").toDate(),
|
||||
allDay: true,
|
||||
vacation: true
|
||||
};
|
||||
})
|
||||
];
|
||||
}))
|
||||
];
|
||||
}, [data]);
|
||||
|
||||
return <ScheduleCalendarComponent refetch={refetch} data={data ? normalizedData : []} />;
|
||||
}
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return <ScheduleCalendarComponent refetch={refetch} data={normalizedData} />;
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarContainer);
|
||||
|
||||
@@ -2,9 +2,14 @@ import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
|
||||
|
||||
export default function ScheduleDayViewComponent({ data, day }) {
|
||||
const ScheduleDayViewComponent = React.memo(function ScheduleDayViewComponent({ data, day }) {
|
||||
const { t } = useTranslation();
|
||||
if (data)
|
||||
return <ScheduleCalendarWrapperComponent events={data} defaultView="day" view={"day"} views={["day"]} date={day} />;
|
||||
else return <div>{t("appointments.labels.nodateselected")}</div>;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
return <ScheduleCalendarWrapperComponent events={data} defaultView="day" view="day" views={["day"]} date={day} />;
|
||||
} else {
|
||||
return <div>{t("appointments.labels.nodateselected")}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
export default ScheduleDayViewComponent;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import ScheduleDayViewComponent from "./schedule-day-view.component";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { QUERY_APPOINTMENT_BY_DATE } from "../../graphql/appointments.queries";
|
||||
@@ -6,45 +6,59 @@ import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import dayjs from "../../utils/day";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ScheduleDayViewContainer({ day }) {
|
||||
const ScheduleDayViewContainer = React.memo(function ScheduleDayViewContainer({ day }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Memoize dayjs computations
|
||||
const dayjsDay = useMemo(() => dayjs(day), [day]);
|
||||
|
||||
// Memoize query variables
|
||||
const queryVariables = useMemo(
|
||||
() => ({
|
||||
start: dayjsDay.startOf("day").toISOString(),
|
||||
end: dayjsDay.endOf("day").toISOString(),
|
||||
startd: dayjsDay.startOf("day").format("YYYY-MM-DD"),
|
||||
endd: dayjsDay.add(1, "day").format("YYYY-MM-DD")
|
||||
}),
|
||||
[dayjsDay]
|
||||
);
|
||||
|
||||
// Use the useQuery hook
|
||||
const { loading, error, data } = useQuery(QUERY_APPOINTMENT_BY_DATE, {
|
||||
variables: {
|
||||
start: dayjs(day).startOf("day"),
|
||||
end: dayjs(day).endOf("day"),
|
||||
startd: dayjs(day).startOf("day").format("YYYY-MM-DD"),
|
||||
endd: dayjs(day).add(1, "day").format("YYYY-MM-DD")
|
||||
},
|
||||
skip: !dayjs(day).isValid(),
|
||||
variables: queryVariables,
|
||||
skip: !dayjsDay.isValid(),
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Memoize normalizedData
|
||||
const normalizedData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
|
||||
const appointments = data.appointments.map((e) => ({
|
||||
...e,
|
||||
start: new Date(e.start),
|
||||
end: new Date(e.end)
|
||||
}));
|
||||
|
||||
const vacations = data.employee_vacation.map((e) => ({
|
||||
...e,
|
||||
title: `${e.employee.first_name?.[0] || ""} ${e.employee.last_name || ""} OUT`,
|
||||
color: "red",
|
||||
start: dayjs(e.start).startOf("day").toDate(),
|
||||
end: dayjs(e.end).startOf("day").toDate(),
|
||||
vacation: true
|
||||
}));
|
||||
|
||||
return [...appointments, ...vacations];
|
||||
}, [data]);
|
||||
|
||||
// Handle conditional rendering
|
||||
if (!day) return <div>{t("appointments.labels.nodateselected")}</div>;
|
||||
if (loading) return <LoadingSkeleton paragraph={{ rows: 4 }} />;
|
||||
if (error) return <div>{error.message}</div>;
|
||||
let normalizedData;
|
||||
|
||||
if (data) {
|
||||
normalizedData = [
|
||||
...data.appointments.map((e) => {
|
||||
//Required becuase Hasura returns a string instead of a date object.
|
||||
return Object.assign({}, e, { start: new Date(e.start) }, { end: new Date(e.end) });
|
||||
}),
|
||||
...data.employee_vacation.map((e) => {
|
||||
//Required becuase Hasura returns a string instead of a date object.
|
||||
return {
|
||||
...e,
|
||||
title: `${
|
||||
(e.employee.first_name && e.employee.first_name.substr(0, 1)) || ""
|
||||
} ${e.employee.last_name || ""} OUT`,
|
||||
color: "red",
|
||||
start: dayjs(e.start).startOf("day").toDate(),
|
||||
end: dayjs(e.end).startOf("day").toDate(),
|
||||
vacation: true
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
return <ScheduleDayViewComponent data={normalizedData} day={day} />;
|
||||
});
|
||||
|
||||
return <ScheduleDayViewComponent data={data ? normalizedData : []} day={day} />;
|
||||
}
|
||||
export default ScheduleDayViewContainer;
|
||||
|
||||
@@ -1,38 +1,43 @@
|
||||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import { Timeline } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
|
||||
export default function ScheduleExistingAppointmentsList({ existingAppointments }) {
|
||||
const ScheduleExistingAppointmentsList = React.memo(function ScheduleExistingAppointmentsList({
|
||||
existingAppointments
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (existingAppointments.loading) return <LoadingSpinner />;
|
||||
if (existingAppointments.error) return <AlertComponent message={existingAppointments.error.message} type="error" />;
|
||||
const { loading, error, data } = existingAppointments;
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return data.appointments.map((item) => ({
|
||||
key: item.id,
|
||||
color: item.canceled ? "red" : item.arrived ? "green" : "blue",
|
||||
children: (
|
||||
<>
|
||||
{item.canceled
|
||||
? t("appointments.labels.cancelledappointment")
|
||||
: item.arrived
|
||||
? t("appointments.labels.arrivedon")
|
||||
: t("appointments.labels.scheduledfor")}
|
||||
<DateTimeFormatter>{item.start}</DateTimeFormatter>
|
||||
</>
|
||||
)
|
||||
}));
|
||||
}, [data, t]);
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{t("appointments.labels.priorappointments")}
|
||||
<Timeline
|
||||
items={
|
||||
existingAppointments.data
|
||||
? existingAppointments.data.appointments.map((item) => ({
|
||||
key: item.id,
|
||||
color: item.canceled ? "red" : item.arrived ? "green" : "blue",
|
||||
children: (
|
||||
<>
|
||||
{item.canceled
|
||||
? t("appointments.labels.cancelledappointment")
|
||||
: item.arrived
|
||||
? t("appointments.labels.arrivedon")
|
||||
: t("appointments.labels.scheduledfor")}
|
||||
<DateTimeFormatter>{item.start}</DateTimeFormatter>
|
||||
</>
|
||||
)
|
||||
}))
|
||||
: []
|
||||
}
|
||||
/>
|
||||
<Timeline items={items} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ScheduleExistingAppointmentsList;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } from "antd";
|
||||
import axios from "axios";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -19,12 +19,12 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
calculateScheduleLoad: (endDate) => dispatch(calculateScheduleLoad(endDate))
|
||||
});
|
||||
|
||||
export function ScheduleJobModalComponent({
|
||||
const ScheduleJobModalComponent = React.memo(function ScheduleJobModalComponent({
|
||||
bodyshop,
|
||||
form,
|
||||
existingAppointments,
|
||||
@@ -36,7 +36,7 @@ export function ScheduleJobModalComponent({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [smartOptions, setSmartOptions] = useState([]);
|
||||
|
||||
const handleSmartScheduling = async () => {
|
||||
const handleSmartScheduling = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.post("/scheduling/job", {
|
||||
@@ -48,21 +48,66 @@ export function ScheduleJobModalComponent({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [jobId]);
|
||||
|
||||
const handleDateBlur = () => {
|
||||
const handleDateBlur = useCallback(() => {
|
||||
const values = form.getFieldsValue();
|
||||
|
||||
if (lbrHrsData) {
|
||||
const totalHours =
|
||||
lbrHrsData.jobs_by_pk.labhrs.aggregate.sum.mod_lb_hrs + lbrHrsData.jobs_by_pk.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
(lbrHrsData.jobs_by_pk.labhrs.aggregate.sum.mod_lb_hrs || 0) +
|
||||
(lbrHrsData.jobs_by_pk.larhrs.aggregate.sum.mod_lb_hrs || 0);
|
||||
|
||||
if (values.start && !values.scheduled_completion)
|
||||
form.setFieldsValue({
|
||||
scheduled_completion: dayjs(values.start).businessDaysAdd(totalHours / bodyshop.target_touchtime, "day")
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [form, lbrHrsData, bodyshop.target_touchtime]);
|
||||
|
||||
const colorOptions = useMemo(() => {
|
||||
return (
|
||||
bodyshop.appt_colors &&
|
||||
bodyshop.appt_colors.map((color) => (
|
||||
<Select.Option style={{ color: color.color.hex }} key={color.color.hex} value={color.color.hex}>
|
||||
{color.label}
|
||||
</Select.Option>
|
||||
))
|
||||
);
|
||||
}, [bodyshop.appt_colors]);
|
||||
|
||||
const altTransportOptions = useMemo(() => {
|
||||
return (
|
||||
bodyshop.appt_alt_transport &&
|
||||
bodyshop.appt_alt_transport.map((alt) => (
|
||||
<Select.Option key={alt} value={alt}>
|
||||
{alt}
|
||||
</Select.Option>
|
||||
))
|
||||
);
|
||||
}, [bodyshop.appt_alt_transport]);
|
||||
|
||||
const smartOptionsButtons = useMemo(() => {
|
||||
return smartOptions.map((d, idx) => (
|
||||
<Button
|
||||
className="imex-flex-row__margin"
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
const ssDate = dayjs(d);
|
||||
if (ssDate.isBefore(dayjs())) {
|
||||
form.setFieldsValue({ start: dayjs() });
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
start: dayjs(d).add(8, "hour")
|
||||
});
|
||||
}
|
||||
handleDateBlur();
|
||||
}}
|
||||
>
|
||||
<DateFormatter includeDay>{d}</DateFormatter>
|
||||
</Button>
|
||||
));
|
||||
}, [smartOptions, form, handleDateBlur]);
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
@@ -80,7 +125,6 @@ export function ScheduleJobModalComponent({
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
@@ -92,7 +136,6 @@ export function ScheduleJobModalComponent({
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
@@ -107,25 +150,7 @@ export function ScheduleJobModalComponent({
|
||||
<Button onClick={handleSmartScheduling} loading={loading}>
|
||||
{t("appointments.actions.calculate")}
|
||||
</Button>
|
||||
{smartOptions.map((d, idx) => (
|
||||
<Button
|
||||
className="imex-flex-row__margin"
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
const ssDate = dayjs(d);
|
||||
if (ssDate.isBefore(dayjs())) {
|
||||
form.setFieldsValue({ start: dayjs() });
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
start: dayjs(d).add(8, "hour")
|
||||
});
|
||||
}
|
||||
handleDateBlur();
|
||||
}}
|
||||
>
|
||||
<DateFormatter includeDay>{d}</DateFormatter>
|
||||
</Button>
|
||||
))}
|
||||
{smartOptionsButtons}
|
||||
</Space>
|
||||
</>
|
||||
),
|
||||
@@ -144,20 +169,10 @@ export function ScheduleJobModalComponent({
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item name="color" label={t("appointments.fields.color")}>
|
||||
<Select allowClear>
|
||||
{bodyshop.appt_colors &&
|
||||
bodyshop.appt_colors.map((color) => (
|
||||
<Select.Option style={{ color: color.color.hex }} key={color.color.hex} value={color.color.hex}>
|
||||
{color.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select allowClear>{colorOptions}</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name={"alt_transport"} label={t("jobs.fields.alt_transport")}>
|
||||
<Select allowClear>
|
||||
{bodyshop.appt_alt_transport &&
|
||||
bodyshop.appt_alt_transport.map((alt) => <Select.Option key={alt}>{alt}</Select.Option>)}
|
||||
</Select>
|
||||
<Select allowClear>{altTransportOptions}</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name={"note"} label={t("appointments.fields.note")}>
|
||||
<Input />
|
||||
@@ -183,6 +198,6 @@ export function ScheduleJobModalComponent({
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleJobModalComponent);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Form, Modal, notification } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -27,13 +27,21 @@ const mapStateToProps = createStructuredSelector({
|
||||
scheduleModal: selectSchedule,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
jobid,
|
||||
operation,
|
||||
type
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
export function ScheduleJobModalContainer({
|
||||
const ScheduleJobModalContainer = React.memo(function ScheduleJobModalContainer({
|
||||
scheduleModal,
|
||||
bodyshop,
|
||||
toggleModalVisible,
|
||||
@@ -43,168 +51,186 @@ export function ScheduleJobModalContainer({
|
||||
}) {
|
||||
const { open, context, actions } = scheduleModal;
|
||||
const { jobId, job, previousEvent } = context;
|
||||
|
||||
const { refetch } = actions;
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { data: lbrHrsData } = useQuery(QUERY_LBR_HRS_BY_PK, {
|
||||
variables: { id: job && job.id },
|
||||
skip: !job || !job.id,
|
||||
variables: { id: job?.id },
|
||||
skip: !job?.id,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
|
||||
const [insertAppointment] = useMutation(INSERT_APPOINTMENT);
|
||||
const [updateJobStatus] = useMutation(UPDATE_JOBS);
|
||||
|
||||
useEffect(() => {
|
||||
if (job) form.resetFields();
|
||||
}, [job, form]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const existingAppointments = useQuery(QUERY_APPOINTMENTS_BY_JOBID, {
|
||||
variables: { jobid: jobId },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
skip: !open || !!!jobId
|
||||
skip: !open || !jobId
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
existingAppointments.data &&
|
||||
existingAppointments.data.appointments.length > 0 &&
|
||||
!existingAppointments.data.appointments[0].canceled
|
||||
) {
|
||||
form.setFieldsValue({
|
||||
color: existingAppointments.data.appointments[0].color,
|
||||
if (job) form.resetFields();
|
||||
}, [job, form]);
|
||||
|
||||
note: existingAppointments.data.appointments[0].note
|
||||
useEffect(() => {
|
||||
const appointments = existingAppointments.data?.appointments;
|
||||
if (appointments?.length && !appointments[0].canceled) {
|
||||
form.setFieldsValue({
|
||||
color: appointments[0].color,
|
||||
note: appointments[0].note
|
||||
});
|
||||
}
|
||||
}, [existingAppointments.data, form]);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("schedule_new_appointment");
|
||||
const handleFinish = useCallback(
|
||||
async (values) => {
|
||||
logImEXEvent("schedule_new_appointment");
|
||||
setLoading(true);
|
||||
|
||||
setLoading(true);
|
||||
if (!!previousEvent) {
|
||||
const cancelAppt = await cancelAppointment({
|
||||
variables: { appid: previousEvent }
|
||||
});
|
||||
|
||||
if (!!cancelAppt.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.canceling", {
|
||||
message: JSON.stringify(cancelAppt.errors)
|
||||
})
|
||||
if (previousEvent) {
|
||||
const cancelAppt = await cancelAppointment({
|
||||
variables: { appid: previousEvent }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
notification["success"]({
|
||||
message: t("appointments.successes.canceled")
|
||||
});
|
||||
}
|
||||
|
||||
if (existingAppointments.data.appointments.length > 0) {
|
||||
await Promise.all(
|
||||
existingAppointments.data.appointments.map((app) => {
|
||||
return cancelAppointment({
|
||||
variables: { appid: app.id }
|
||||
if (cancelAppt.errors) {
|
||||
notification.error({
|
||||
message: t("appointments.errors.canceling", {
|
||||
message: JSON.stringify(cancelAppt.errors)
|
||||
})
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const appt = await insertAppointment({
|
||||
variables: {
|
||||
app: {
|
||||
jobid: jobId,
|
||||
bodyshopid: bodyshop.id,
|
||||
start: dayjs(values.start),
|
||||
end: dayjs(values.start).add(bodyshop.appt_length || 60, "minute"),
|
||||
color: values.color,
|
||||
note: values.note,
|
||||
created_by: currentUser.email
|
||||
},
|
||||
jobId: jobId,
|
||||
altTransport: values.alt_transport
|
||||
notification.success({
|
||||
message: t("appointments.successes.canceled")
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!appt.errors) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.appointmentinsert(DateTimeFormat(values.start)),
|
||||
type: "appointmentinsert"
|
||||
});
|
||||
}
|
||||
const existingApps = existingAppointments.data?.appointments || [];
|
||||
if (existingApps.length > 0) {
|
||||
await Promise.all(
|
||||
existingApps.map((app) =>
|
||||
cancelAppointment({
|
||||
variables: { appid: app.id }
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!!appt.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.saving", {
|
||||
message: JSON.stringify(appt.errors)
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
notification["success"]({
|
||||
message: t("appointments.successes.created")
|
||||
});
|
||||
if (jobId) {
|
||||
const jobUpdate = await updateJobStatus({
|
||||
const appt = await insertAppointment({
|
||||
variables: {
|
||||
jobIds: [jobId],
|
||||
fields: {
|
||||
status: bodyshop.md_ro_statuses.default_scheduled,
|
||||
date_scheduled: new Date(),
|
||||
scheduled_in: values.start,
|
||||
scheduled_completion: values.scheduled_completion,
|
||||
lost_sale_reason: null,
|
||||
date_lost_sale: null
|
||||
}
|
||||
app: {
|
||||
jobid: jobId,
|
||||
bodyshopid: bodyshop.id,
|
||||
start: dayjs(values.start),
|
||||
end: dayjs(values.start).add(bodyshop.appt_length || 60, "minute"),
|
||||
color: values.color,
|
||||
note: values.note,
|
||||
created_by: currentUser.email
|
||||
},
|
||||
jobId: jobId,
|
||||
altTransport: values.alt_transport
|
||||
}
|
||||
});
|
||||
|
||||
if (!!jobUpdate.errors) {
|
||||
notification["error"]({
|
||||
if (!appt.errors) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.appointmentinsert(DateTimeFormat(values.start)),
|
||||
type: "appointmentinsert"
|
||||
});
|
||||
} else {
|
||||
notification.error({
|
||||
message: t("appointments.errors.saving", {
|
||||
message: JSON.stringify(jobUpdate.errors)
|
||||
message: JSON.stringify(appt.errors)
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
toggleModalVisible();
|
||||
if (values.notifyCustomer) {
|
||||
setEmailOptions({
|
||||
jobid: jobId,
|
||||
messageOptions: {
|
||||
to: [values.email],
|
||||
replyTo: bodyshop.email,
|
||||
subject: TemplateList("appointment").appointment_confirmation.subject
|
||||
},
|
||||
template: {
|
||||
name: TemplateList("appointment").appointment_confirmation.key,
|
||||
variables: {
|
||||
id: appt.data.insert_appointments.returning[0].id
|
||||
}
|
||||
}
|
||||
|
||||
notification.success({
|
||||
message: t("appointments.successes.created")
|
||||
});
|
||||
}
|
||||
if (refetch) refetch();
|
||||
};
|
||||
|
||||
if (jobId) {
|
||||
const jobUpdate = await updateJobStatus({
|
||||
variables: {
|
||||
jobIds: [jobId],
|
||||
fields: {
|
||||
status: bodyshop.md_ro_statuses.default_scheduled,
|
||||
date_scheduled: new Date(),
|
||||
scheduled_in: values.start,
|
||||
scheduled_completion: values.scheduled_completion,
|
||||
lost_sale_reason: null,
|
||||
date_lost_sale: null
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (jobUpdate.errors) {
|
||||
notification.error({
|
||||
message: t("appointments.errors.saving", {
|
||||
message: JSON.stringify(jobUpdate.errors)
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (values.notifyCustomer) {
|
||||
setEmailOptions({
|
||||
jobid: jobId,
|
||||
messageOptions: {
|
||||
to: [values.email],
|
||||
replyTo: bodyshop.email,
|
||||
subject: TemplateList("appointment").appointment_confirmation.subject
|
||||
},
|
||||
template: {
|
||||
name: TemplateList("appointment").appointment_confirmation.key,
|
||||
variables: {
|
||||
id: appt.data.insert_appointments.returning[0].id
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (refetch) refetch();
|
||||
toggleModalVisible();
|
||||
setLoading(false);
|
||||
},
|
||||
[
|
||||
t,
|
||||
previousEvent,
|
||||
cancelAppointment,
|
||||
existingAppointments.data,
|
||||
insertAppointment,
|
||||
jobId,
|
||||
bodyshop.id,
|
||||
bodyshop.appt_length,
|
||||
currentUser.email,
|
||||
insertAuditTrail,
|
||||
job,
|
||||
updateJobStatus,
|
||||
bodyshop.md_ro_statuses.default_scheduled,
|
||||
setEmailOptions,
|
||||
refetch,
|
||||
toggleModalVisible,
|
||||
bodyshop.email
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
onCancel={toggleModalVisible}
|
||||
onOk={() => form.submit()}
|
||||
width={"90%"}
|
||||
width="90%"
|
||||
maskClosable={false}
|
||||
destroyOnClose
|
||||
okButtonProps={{
|
||||
@@ -217,10 +243,9 @@ export function ScheduleJobModalContainer({
|
||||
layout="vertical"
|
||||
onFinish={handleFinish}
|
||||
initialValues={{
|
||||
notifyCustomer: !!(job && job.ownr_ea),
|
||||
email: (job && job.ownr_ea) || "",
|
||||
notifyCustomer: !!job?.ownr_ea,
|
||||
email: job?.ownr_ea || "",
|
||||
start: null,
|
||||
// smartDates: [],
|
||||
scheduled_completion: null,
|
||||
color: context.color,
|
||||
alt_transport: context.alt_transport,
|
||||
@@ -236,6 +261,6 @@ export function ScheduleJobModalContainer({
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleJobModalContainer);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Form, Input, Popover, Select, Space } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -13,142 +13,143 @@ import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleManualEvent);
|
||||
|
||||
export function ScheduleManualEvent({ bodyshop, event }) {
|
||||
const ScheduleManualEvent = React.memo(function ScheduleManualEvent({ bodyshop, event }) {
|
||||
const { t } = useTranslation();
|
||||
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
|
||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT, {
|
||||
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
|
||||
});
|
||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT, {
|
||||
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [visibility, setVisibility] = useState(false);
|
||||
// const [callQuery, { loading: entryLoading, data: entryData }] = useLazyQuery(
|
||||
// QUERY_SCOREBOARD_ENTRY
|
||||
// );
|
||||
|
||||
const handleFinish = useCallback(
|
||||
async (values) => {
|
||||
logImEXEvent("schedule_manual_event");
|
||||
setLoading(true);
|
||||
try {
|
||||
if (event && event.id) {
|
||||
await updateAppointment({
|
||||
variables: { appid: event.id, app: values }
|
||||
});
|
||||
} else {
|
||||
await insertAppointment({
|
||||
variables: {
|
||||
apt: {
|
||||
...values,
|
||||
isintake: false,
|
||||
bodyshopid: bodyshop.id
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
form.resetFields();
|
||||
setVisibility(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[event, updateAppointment, insertAppointment, bodyshop.id, form]
|
||||
);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setVisibility(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (visibility && event) {
|
||||
form.setFieldsValue(event);
|
||||
} else if (!visibility) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [visibility, form, event]);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("schedule_manual_event");
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
if (event && event.id) {
|
||||
updateAppointment({
|
||||
variables: { appid: event.id, app: values },
|
||||
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
|
||||
});
|
||||
} else {
|
||||
insertAppointment({
|
||||
variables: {
|
||||
apt: { ...values, isintake: false, bodyshopid: bodyshop.id }
|
||||
},
|
||||
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
|
||||
});
|
||||
}
|
||||
form.resetFields();
|
||||
setVisibility(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const colorOptions = useMemo(() => {
|
||||
return bodyshop.appt_colors.map((col, idx) => (
|
||||
<Select.Option key={idx} value={col.color.hex}>
|
||||
{col.label}
|
||||
</Select.Option>
|
||||
));
|
||||
}, [bodyshop.appt_colors]);
|
||||
|
||||
const overlay = (
|
||||
<Card>
|
||||
<div>
|
||||
<Form form={form} layout="vertical" onFinish={handleFinish}>
|
||||
<Form.Item
|
||||
label={t("appointments.fields.title")}
|
||||
name="title"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("appointments.fields.note")} name="note">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("appointments.fields.start")}
|
||||
name="start"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<FormDateTimePickerComponent />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("appointments.fields.end")}
|
||||
name="end"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
async validator(rule, value) {
|
||||
if (value) {
|
||||
const { start } = form.getFieldsValue();
|
||||
if (dayjs(start).isAfter(dayjs(value))) {
|
||||
return Promise.reject(t("employees.labels.endmustbeafterstart"));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
<Form form={form} layout="vertical" onFinish={handleFinish}>
|
||||
<Form.Item
|
||||
label={t("appointments.fields.title")}
|
||||
name="title"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("appointments.fields.note")} name="note">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("appointments.fields.start")}
|
||||
name="start"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<FormDateTimePickerComponent />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("appointments.fields.end")}
|
||||
name="end"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (value) {
|
||||
const start = form.getFieldValue("start");
|
||||
if (dayjs(start).isAfter(dayjs(value))) {
|
||||
return Promise.reject(t("employees.labels.endmustbeafterstart"));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<FormDateTimePickerComponent />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("appointments.fields.color")} name="color">
|
||||
<Select>
|
||||
{bodyshop.appt_colors.map((col, idx) => (
|
||||
<Select.Option key={idx} value={col.color.hex}>
|
||||
{col.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>{t("general.actions.cancel")}</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<FormDateTimePickerComponent />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("appointments.fields.color")} name="color">
|
||||
<Select>{colorOptions}</Select>
|
||||
</Form.Item>
|
||||
<Space wrap>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>{t("general.actions.cancel")}</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const handleClick = (e) => {
|
||||
setVisibility(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover content={overlay} open={visibility}>
|
||||
<Button loading={loading} onClick={handleClick}>
|
||||
<Button onClick={handleClick}>
|
||||
{event ? t("appointments.actions.reschedule") : t("appointments.labels.manualevent")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(ScheduleManualEvent);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Popover } from "antd";
|
||||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -11,52 +11,52 @@ import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import "./schedule-production-list.styles.scss";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
export default function ScheduleProductionList() {
|
||||
const ScheduleProductionList = React.memo(function ScheduleProductionList() {
|
||||
const { t } = useTranslation();
|
||||
const [callQuery, { loading, error, data }] = useLazyQuery(QUERY_JOBS_IN_PRODUCTION);
|
||||
|
||||
const content = () => {
|
||||
const content = useCallback(() => {
|
||||
return (
|
||||
<Card>
|
||||
<div onClick={(e) => e.stopPropagation()} className="jobs-in-production-table">
|
||||
{loading ? <LoadingSkeleton /> : null}
|
||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||
{data ? (
|
||||
{loading && <LoadingSkeleton />}
|
||||
{error && <AlertComponent message={error.message} type="error" />}
|
||||
{data && data.jobs && (
|
||||
<table>
|
||||
<tbody>
|
||||
{data && data.jobs
|
||||
? data.jobs.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td>{`${j.v_model_yr || ""} ${j.v_make_desc || ""} ${j.v_model_desc || ""}`}</td>
|
||||
<td>{`${j.labhrs.aggregate.sum.mod_lb_hrs || "0"} / ${
|
||||
j.larhrs.aggregate.sum.mod_lb_hrs || "0"
|
||||
}`}</td>
|
||||
<td>
|
||||
<DateTimeFormatter>{j.scheduled_completion}</DateTimeFormatter>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
: null}
|
||||
{data.jobs.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td>{`${j.v_model_yr || ""} ${j.v_make_desc || ""} ${j.v_model_desc || ""}`}</td>
|
||||
<td>{`${j.labhrs.aggregate.sum.mod_lb_hrs || "0"} / ${
|
||||
j.larhrs.aggregate.sum.mod_lb_hrs || "0"
|
||||
}`}</td>
|
||||
<td>
|
||||
<DateTimeFormatter>{j.scheduled_completion}</DateTimeFormatter>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
}, [loading, error, data]);
|
||||
|
||||
return (
|
||||
<Popover content={content} trigger="click" placement="bottomRight">
|
||||
<Button onClick={() => callQuery()}>
|
||||
<Button onClick={callQuery}>
|
||||
{t("appointments.labels.inproduction")}
|
||||
<DownOutlined />
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ScheduleProductionList;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { Button } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_SCHEDULE_LOAD_DATA } from "../../graphql/appointments.queries";
|
||||
@@ -10,49 +10,46 @@ import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleVerifyIntegrity);
|
||||
|
||||
export function ScheduleVerifyIntegrity({ currentUser }) {
|
||||
const ScheduleVerifyIntegrity = React.memo(function ScheduleVerifyIntegrity({ currentUser }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const client = useApolloClient();
|
||||
const handleVerify = async () => {
|
||||
|
||||
const handleVerify = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const {
|
||||
data: { arrJobs, compJobs, prodJobs }
|
||||
} = await client.query({
|
||||
query: QUERY_SCHEDULE_LOAD_DATA,
|
||||
variables: { start: dayjs(), end: dayjs().add(180, "day") }
|
||||
});
|
||||
try {
|
||||
const {
|
||||
data: { arrJobs, compJobs, prodJobs }
|
||||
} = await client.query({
|
||||
query: QUERY_SCHEDULE_LOAD_DATA,
|
||||
variables: { start: dayjs(), end: dayjs().add(180, "day") }
|
||||
});
|
||||
|
||||
//check that the leaving jobs are either in the arriving list, or in production.
|
||||
const issues = [];
|
||||
// Check that the completing jobs are either in production or arriving within the next 180 days.
|
||||
const issues = compJobs.filter((j) => {
|
||||
const inProdJobs = prodJobs.some((p) => p.id === j.id);
|
||||
const inArrJobs = arrJobs.some((p) => p.id === j.id);
|
||||
return !(inProdJobs || inArrJobs);
|
||||
});
|
||||
|
||||
compJobs.forEach((j) => {
|
||||
const inProdJobs = prodJobs.find((p) => p.id === j.id);
|
||||
const inArrJobs = arrJobs.find((p) => p.id === j.id);
|
||||
console.log("The following completing jobs are not in production or arriving within the next 180 days:", issues);
|
||||
} catch (error) {
|
||||
console.error("Error verifying schedule integrity:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [client]);
|
||||
|
||||
if (!(inProdJobs || inArrJobs)) {
|
||||
// NOT FOUND!
|
||||
issues.push(j);
|
||||
}
|
||||
});
|
||||
console.log(
|
||||
"The following completing jobs are not in production, or are arriving within the next 180 days. ",
|
||||
issues
|
||||
);
|
||||
// TODO: A Global helper with developer emails
|
||||
if (currentUser.email !== "patrick@imex.prod") {
|
||||
return null;
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
return (
|
||||
<Button loading={loading} onClick={handleVerify}>
|
||||
Developer Use Only - Verify Schedule Integrity
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
if (currentUser.email === "patrick@imex.prod")
|
||||
return (
|
||||
<Button loading={loading} onClick={handleVerify}>
|
||||
Developer Use Only - Verify Schedule Integrity
|
||||
</Button>
|
||||
);
|
||||
else return null;
|
||||
}
|
||||
export default connect(mapStateToProps)(ScheduleVerifyIntegrity);
|
||||
|
||||
@@ -4,7 +4,7 @@ export const CalculateWorkingDaysThisMonth = () => dayjs().endOf("month").busine
|
||||
|
||||
export const CalculateWorkingDaysInPeriod = (start, end) => dayjs(end).businessDiff(dayjs(start));
|
||||
|
||||
export const CalculateWorkingDaysAsOfToday = () => dayjs().endOf("day").businessDiff(dayjs().startOf("month"));
|
||||
export const CalculateWorkingDaysAsOfToday = () => dayjs().businessDaysInMonth().length;
|
||||
|
||||
export const CalculateWorkingDaysLastMonth = () =>
|
||||
dayjs().subtract(1, "month").endOf("month").businessDaysInMonth().length;
|
||||
|
||||
@@ -20,7 +20,6 @@ import ShopInfoTaskPresets from "./shop-info.task-presets.component";
|
||||
import queryString from "query-string";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import ShopInfoRoGuard from "./shop-info.roguard.component";
|
||||
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -136,17 +135,6 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
||||
],
|
||||
rome: "USE_IMEX",
|
||||
promanager: []
|
||||
}),
|
||||
...InstanceRenderManager({
|
||||
imex: [],
|
||||
rome: [
|
||||
{
|
||||
key: "intellipay",
|
||||
label: t("bodyshop.labels.intellipay"),
|
||||
children: <ShopInfoIntellipay form={form} />
|
||||
}
|
||||
],
|
||||
promanager: []
|
||||
})
|
||||
];
|
||||
return (
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Alert, Form, InputNumber, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoIntellipay);
|
||||
|
||||
export function ShopInfoIntellipay({ bodyshop, form }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item dependencies={[["intellipay_config", "enable_cash_discount"]]}>
|
||||
{() => {
|
||||
const { intellipay_config } = form.getFieldsValue();
|
||||
|
||||
if (intellipay_config?.enable_cash_discount)
|
||||
return <Alert message={t("bodyshop.labels.intellipay_cash_discount")} />;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<LayoutFormRow noDivider>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.intellipay_config.enable_cash_discount")}
|
||||
valuePropName="checked"
|
||||
name={["intellipay_config", "enable_cash_discount"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.intellipay_config.cash_discount_percentage")}
|
||||
valuePropName="checked"
|
||||
dependencies={[["intellipay_config", "enable_cash_discount"]]}
|
||||
name={["intellipay_config", "cash_discount_percentage"]}
|
||||
rules={[
|
||||
({ getFieldsValue }) => ({ required: form.getFieldValue(["intellipay_config", "enable_cash_discount"]) })
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={1} suffix='%'/>
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
||||
operationName: eventName,
|
||||
variables: additionalParams,
|
||||
dbevent: false,
|
||||
env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
|
||||
env: "master"
|
||||
});
|
||||
// console.log(
|
||||
// "%c[Analytics]",
|
||||
|
||||
@@ -138,8 +138,7 @@ export const QUERY_BODYSHOP = gql`
|
||||
tt_enforce_hours_for_tech_console
|
||||
md_tasks_presets
|
||||
use_paint_scale_data
|
||||
intellipay_config
|
||||
md_ro_guard
|
||||
md_ro_guard
|
||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||
id
|
||||
name
|
||||
@@ -267,8 +266,7 @@ export const UPDATE_SHOP = gql`
|
||||
enforce_conversion_category
|
||||
tt_enforce_hours_for_tech_console
|
||||
md_tasks_presets
|
||||
intellipay_config
|
||||
md_ro_guard
|
||||
md_ro_guard
|
||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -2461,14 +2461,6 @@ export const SUBSCRIPTION_JOBS_IN_PRODUCTION = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW = gql`
|
||||
subscription SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW {
|
||||
jobs: jobs_inproduction {
|
||||
id
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_JOBS_IN_PRODUCTION = gql`
|
||||
query QUERY_JOBS_IN_PRODUCTION {
|
||||
|
||||
@@ -1,26 +1,6 @@
|
||||
import React from "react";
|
||||
import ProductionBoardKanbanContainer from "../../components/production-board-kanban/production-board-kanban.container";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardComponent);
|
||||
|
||||
export function ProductionBoardComponent({ bodyshop }) {
|
||||
const {
|
||||
treatments: { Production_Use_View }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Production_Use_View"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
return <ProductionBoardKanbanContainer subscriptionType={Production_Use_View.treatment} />;
|
||||
export default function ProductionBoardComponent() {
|
||||
return <ProductionBoardKanbanContainer />;
|
||||
}
|
||||
|
||||
@@ -2,31 +2,11 @@ import React from "react";
|
||||
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
|
||||
import ProductionListTable from "../../components/production-list-table/production-list-table.container";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionListComponent);
|
||||
|
||||
export function ProductionListComponent({ bodyshop }) {
|
||||
const {
|
||||
treatments: { Production_Use_View }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Production_Use_View"],
|
||||
splitKey: bodyshop && bodyshop.imexshopid
|
||||
});
|
||||
|
||||
export default function ProductionListComponent() {
|
||||
return (
|
||||
<>
|
||||
<NoteUpsertModal />
|
||||
<ProductionListTable subscriptionType={Production_Use_View.treatment} />
|
||||
<ProductionListTable />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { Tabs } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import ShopCsiConfig from "../../components/shop-csi-config/shop-csi-config.component";
|
||||
import queryString from "query-string";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ShopEmployeesContainer from "../../components/shop-employees/shop-employees.container";
|
||||
import ShopInfoContainer from "../../components/shop-info/shop-info.container";
|
||||
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
|
||||
import ShopCsiConfig from "../../components/shop-csi-config/shop-csi-config.component";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
|
||||
|
||||
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
||||
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
|
||||
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -36,8 +36,7 @@ export function* openChatByPhone({ payload }) {
|
||||
data: { conversations }
|
||||
} = yield client.query({
|
||||
query: CONVERSATION_ID_BY_PHONE,
|
||||
variables: { phone: p.number },
|
||||
fetchPolicy: 'no-cache'
|
||||
variables: { phone: p.number }
|
||||
});
|
||||
|
||||
if (conversations.length === 0) {
|
||||
|
||||
@@ -332,10 +332,6 @@
|
||||
"next_contact_hours": "Automatic Next Contact Date - Hours from Intake",
|
||||
"templates": "Intake Templates"
|
||||
},
|
||||
"intellipay_config": {
|
||||
"cash_discount_percentage": "Cash Discount %",
|
||||
"enable_cash_discount": "Enable Cash Discounting"
|
||||
},
|
||||
"invoice_federal_tax_rate": "Invoices - Federal Tax Rate",
|
||||
"invoice_local_tax_rate": "Invoices - Local Tax Rate",
|
||||
"invoice_state_tax_rate": "Invoices - State Tax Rate",
|
||||
@@ -667,8 +663,6 @@
|
||||
"filehandlers": "Adjusters",
|
||||
"insurancecos": "Insurance Companies",
|
||||
"intakechecklist": "Intake Checklist",
|
||||
"intellipay": "IntelliPay",
|
||||
"intellipay_cash_discount": "Please ensure that cash discounting has been enabled on your merchant account. Reach out to IntelliPay Support if you need assistance. ",
|
||||
"jobstatuses": "Job Statuses",
|
||||
"laborrates": "Labor Rates",
|
||||
"licensing": "Licensing",
|
||||
@@ -1373,7 +1367,6 @@
|
||||
},
|
||||
"job_payments": {
|
||||
"buttons": {
|
||||
"create_short_link": "Generate Short Link",
|
||||
"goback": "Go Back",
|
||||
"proceedtopayment": "Proceed to Payment",
|
||||
"refundpayment": "Refund Payment"
|
||||
|
||||
@@ -332,10 +332,6 @@
|
||||
"next_contact_hours": "",
|
||||
"templates": ""
|
||||
},
|
||||
"intellipay_config": {
|
||||
"cash_discount_percentage": "",
|
||||
"enable_cash_discount": ""
|
||||
},
|
||||
"invoice_federal_tax_rate": "",
|
||||
"invoice_local_tax_rate": "",
|
||||
"invoice_state_tax_rate": "",
|
||||
@@ -667,8 +663,6 @@
|
||||
"filehandlers": "",
|
||||
"insurancecos": "",
|
||||
"intakechecklist": "",
|
||||
"intellipay": "",
|
||||
"intellipay_cash_discount": "",
|
||||
"jobstatuses": "",
|
||||
"laborrates": "",
|
||||
"licensing": "",
|
||||
@@ -1373,7 +1367,6 @@
|
||||
},
|
||||
"job_payments": {
|
||||
"buttons": {
|
||||
"create_short_link": "",
|
||||
"goback": "",
|
||||
"proceedtopayment": "",
|
||||
"refundpayment": ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -918,7 +918,6 @@
|
||||
- bill_tax_rates
|
||||
- cdk_configuration
|
||||
- cdk_dealerid
|
||||
- chatterid
|
||||
- city
|
||||
- claimscorpid
|
||||
- convenient_company
|
||||
@@ -940,7 +939,6 @@
|
||||
- inhousevendorid
|
||||
- insurance_vendor_id
|
||||
- intakechecklist
|
||||
- intellipay_config
|
||||
- jc_hourly_rates
|
||||
- jobsizelimit
|
||||
- last_name_first
|
||||
@@ -1042,7 +1040,6 @@
|
||||
- inhousevendorid
|
||||
- insurance_vendor_id
|
||||
- intakechecklist
|
||||
- intellipay_config
|
||||
- jc_hourly_rates
|
||||
- last_name_first
|
||||
- localmediaserverhttp
|
||||
@@ -4243,63 +4240,6 @@
|
||||
- active:
|
||||
_eq: true
|
||||
event_triggers:
|
||||
- name: job_modified
|
||||
definition:
|
||||
enable_manual: false
|
||||
update:
|
||||
columns:
|
||||
- clm_no
|
||||
- v_make_desc
|
||||
- date_next_contact
|
||||
- status
|
||||
- employee_csr
|
||||
- employee_prep
|
||||
- clm_total
|
||||
- suspended
|
||||
- employee_body
|
||||
- ro_number
|
||||
- actual_in
|
||||
- ownr_co_nm
|
||||
- v_model_yr
|
||||
- comment
|
||||
- job_totals
|
||||
- v_vin
|
||||
- ownr_fn
|
||||
- scheduled_completion
|
||||
- special_coverage_policy
|
||||
- v_color
|
||||
- ca_gst_registrant
|
||||
- scheduled_delivery
|
||||
- actual_delivery
|
||||
- actual_completion
|
||||
- kanbanparent
|
||||
- est_ct_fn
|
||||
- employee_refinish
|
||||
- ownr_ph1
|
||||
- date_last_contacted
|
||||
- alt_transport
|
||||
- inproduction
|
||||
- est_ct_ln
|
||||
- production_vars
|
||||
- category
|
||||
- v_model_desc
|
||||
- date_invoiced
|
||||
- est_co_nm
|
||||
- ownr_ln
|
||||
retry_conf:
|
||||
interval_sec: 10
|
||||
num_retries: 0
|
||||
timeout_sec: 60
|
||||
webhook_from_env: HASURA_API_URL
|
||||
headers:
|
||||
- name: event-secret
|
||||
value_from_env: EVENT_SECRET
|
||||
request_transform:
|
||||
method: POST
|
||||
query_params: {}
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/job/job-updated'
|
||||
version: 2
|
||||
- name: job_status_transition
|
||||
definition:
|
||||
enable_manual: true
|
||||
@@ -4359,35 +4299,6 @@
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/opensearch'
|
||||
version: 2
|
||||
- table:
|
||||
name: jobs_inproduction
|
||||
schema: public
|
||||
object_relationships:
|
||||
- name: bodyshop
|
||||
using:
|
||||
manual_configuration:
|
||||
column_mapping:
|
||||
shopid: id
|
||||
insertion_order: null
|
||||
remote_table:
|
||||
name: bodyshops
|
||||
schema: public
|
||||
select_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
columns:
|
||||
- id
|
||||
- shopid
|
||||
- updated_at
|
||||
filter:
|
||||
bodyshop:
|
||||
associations:
|
||||
_and:
|
||||
- user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
- active:
|
||||
_eq: true
|
||||
- table:
|
||||
name: masterdata
|
||||
schema: public
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."bodyshops" add column "intellipay_config" jsonb
|
||||
-- not null default jsonb_build_object();
|
||||
@@ -1,2 +0,0 @@
|
||||
alter table "public"."bodyshops" add column "intellipay_config" jsonb
|
||||
not null default jsonb_build_object();
|
||||
@@ -1,4 +0,0 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."bodyshops" add column "chatterid" text
|
||||
-- null;
|
||||
@@ -1,2 +0,0 @@
|
||||
alter table "public"."bodyshops" add column "chatterid" text
|
||||
null;
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "courtesycars_idx_fleet" on
|
||||
"public"."courtesycars" using btree ("fleetnumber");
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."courtesycars_idx_fleet";
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "idx_jobs_ownrfn" on
|
||||
"public"."jobs" using gin ("ownr_fn");
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_ownrfn";
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "idx_jobs_ownrln" on
|
||||
"public"."jobs" using gin ("ownr_ln");
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_ownrln";
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "jobs_idx_iouparent" on
|
||||
"public"."jobs" using btree ("iouparent");
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."jobs_idx_iouparent";
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "idx_jobs_ronumber" on
|
||||
"public"."jobs" using gin ("ro_number");
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_ronumber";
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "idx_jobs_clmno" on
|
||||
"public"."jobs" using gin ("clm_no");
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_clmno";
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "idx_jobs_vmodeldesc" on
|
||||
"public"."jobs" using gin ("v_model_desc");
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_vmodeldesc";
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "idx_jobs_vmakedesc" on
|
||||
"public"."jobs" using gin ("v_make_desc");
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_vmakedesc";
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "idx_jobs_plateno" on
|
||||
"public"."jobs" using gin ("plate_no");
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_plateno";
|
||||
@@ -1,11 +0,0 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE
|
||||
-- OR REPLACE VIEW "public"."jobs_inproduction" AS
|
||||
-- SELECT
|
||||
-- j.id,
|
||||
-- j.updated_at
|
||||
-- FROM
|
||||
-- jobs j
|
||||
-- WHERE
|
||||
-- j.inproduction=true;
|
||||
@@ -1,9 +0,0 @@
|
||||
CREATE
|
||||
OR REPLACE VIEW "public"."jobs_inproduction" AS
|
||||
SELECT
|
||||
j.id,
|
||||
j.updated_at
|
||||
FROM
|
||||
jobs j
|
||||
WHERE
|
||||
j.inproduction=true;
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE OR REPLACE VIEW "public"."jobs_inproduction" AS
|
||||
-- SELECT j.id,
|
||||
-- j.updated_at,
|
||||
-- j.shopid
|
||||
-- FROM jobs j
|
||||
-- WHERE (j.inproduction = true);
|
||||
@@ -1,6 +0,0 @@
|
||||
CREATE OR REPLACE VIEW "public"."jobs_inproduction" AS
|
||||
SELECT j.id,
|
||||
j.updated_at,
|
||||
j.shopid
|
||||
FROM jobs j
|
||||
WHERE (j.inproduction = true);
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE OR REPLACE VIEW "public"."jobs_inproduction" AS
|
||||
-- SELECT j.id,
|
||||
-- j.updated_at,
|
||||
-- j.shopid
|
||||
-- FROM jobs j
|
||||
-- WHERE (j.inproduction = true);
|
||||
@@ -1,6 +0,0 @@
|
||||
CREATE OR REPLACE VIEW "public"."jobs_inproduction" AS
|
||||
SELECT j.id,
|
||||
j.updated_at,
|
||||
j.shopid
|
||||
FROM jobs j
|
||||
WHERE (j.inproduction = true);
|
||||
@@ -1,3 +0,0 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE INDEX idx_jobs_inproduction_true_cast ON jobs(inproduction) WHERE inproduction = ('true') :: boolean;
|
||||
@@ -1 +0,0 @@
|
||||
CREATE INDEX idx_jobs_inproduction_true_cast ON jobs(inproduction) WHERE inproduction = ('true') :: boolean;
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "idx_jobs_inproduction_true_cast" on
|
||||
"public"."jobs" using btree ("inproduction");
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_inproduction_true_cast";
|
||||
@@ -1,3 +0,0 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE INDEX idx_jobs_created_at_desc ON jobs (created_at DESC);
|
||||
@@ -1 +0,0 @@
|
||||
CREATE INDEX idx_jobs_created_at_desc ON jobs (created_at DESC);
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_jobs_vehicleid";
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "idx_jobs_vehicleid" on
|
||||
"public"."jobs" using btree ("vehicleid");
|
||||
@@ -1,34 +0,0 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE OR REPLACE FUNCTION public.search_exportlog(search text)
|
||||
-- RETURNS SETOF exportlog
|
||||
-- LANGUAGE plpgsql
|
||||
-- STABLE
|
||||
-- AS $function$ 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 $function$;
|
||||
@@ -1,32 +0,0 @@
|
||||
CREATE OR REPLACE FUNCTION public.search_exportlog(search text)
|
||||
RETURNS SETOF exportlog
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $function$ 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 $function$;
|
||||
@@ -1,34 +0,0 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE OR REPLACE FUNCTION public.search_exportlog(search text)
|
||||
-- RETURNS SETOF exportlog
|
||||
-- LANGUAGE plpgsql
|
||||
-- STABLE
|
||||
-- AS $function$ 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 $function$;
|
||||
@@ -1,32 +0,0 @@
|
||||
CREATE OR REPLACE FUNCTION public.search_exportlog(search text)
|
||||
RETURNS SETOF exportlog
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $function$ 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 $function$;
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_bill_invoice_number";
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "idx_bill_invoice_number" on
|
||||
"public"."bills" using btree ("invoice_number");
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_payments_paymentnum";
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "idx_payments_paymentnum" on
|
||||
"public"."payments" using btree ("paymentnum");
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."exportlog_useremail";
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "exportlog_useremail" on
|
||||
"public"."exportlog" using btree ("useremail");
|
||||
@@ -1 +0,0 @@
|
||||
DROP INDEX IF EXISTS "public"."available_jobs_jobid";
|
||||
@@ -1,2 +0,0 @@
|
||||
CREATE INDEX "available_jobs_jobid" on
|
||||
"public"."available_jobs" using btree ("jobid");
|
||||
@@ -1,3 +0,0 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE INDEX idx_jobslines_ordering ON joblines (jobid, removed, line_no asc) where removed=false;
|
||||
@@ -1 +0,0 @@
|
||||
CREATE INDEX idx_jobslines_ordering ON joblines (jobid, removed, line_no asc) where removed=false;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE INDEX idx_joblines_types ON joblines (jobid, mod_lbr_ty, removed) where removed=false;
|
||||
@@ -1 +0,0 @@
|
||||
CREATE INDEX idx_joblines_types ON joblines (jobid, mod_lbr_ty, removed) where removed=false;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE INDEX idx_exportlog_created_at_desc ON exportlog (created_at desc);
|
||||
@@ -1 +0,0 @@
|
||||
CREATE INDEX idx_exportlog_created_at_desc ON exportlog (created_at desc);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user