diff --git a/.circleci/config.yml b/.circleci/config.yml index 4f35873a2..23341d15d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -226,7 +226,9 @@ jobs: command: | curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash hasura migrate apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >> + sleep 5 hasura metadata apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >> + sleep 10 hasura metadata reload --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >> - jira/notify: environment: Test (Rome) - Hasura @@ -313,7 +315,9 @@ jobs: command: | curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >> + sleep 15 hasura metadata apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >> + sleep 30 hasura metadata reload --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >> - jira/notify: environment: Test (ImEX) - Hasura @@ -423,7 +427,7 @@ workflows: secret: ${HASURA_PROD_SECRET} filters: branches: - only: master + only: master-AIO - rome-api-deploy: filters: branches: @@ -433,7 +437,7 @@ workflows: branches: only: master-AIO - rome-hasura-migrate: - secret: ${HASURA_PROD_SECRET} + secret: ${HASURA_ROME_PROD_SECRET} filters: branches: only: master-AIO diff --git a/_reference/prHelper.html b/_reference/prHelper.html new file mode 100644 index 000000000..fd5ad7c71 --- /dev/null +++ b/_reference/prHelper.html @@ -0,0 +1,59 @@ + + + + + + IMEX IO Extractor + + + +

IMEX IO Extractor

+ +
+ + +
+ + + + + diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index fd7c9e586..3f0edb7b9 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -1,4 +1,4 @@ - + - + - - - - - - - - - + <% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %> ProManager diff --git a/client/package-lock.json b/client/package-lock.json index bc6897335..cad0b4b15 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "bodyshop", "version": "0.2.1", + "hasInstallScript": true, "dependencies": { "@ant-design/pro-layout": "^7.20.0", "@apollo/client": "^3.11.8", diff --git a/client/package.json b/client/package.json index e08d2f1d3..d2f2b92fa 100644 --- a/client/package.json +++ b/client/package.json @@ -84,6 +84,7 @@ "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", diff --git a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx index 937ff9ce1..e37985222 100644 --- a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx +++ b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx @@ -98,7 +98,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail }); billlines.forEach((billline) => { - const { deductedfromlbr, inventories, jobline, ...il } = billline; + const { deductedfromlbr, inventories, jobline, original_actual_price, create_ppc, ...il } = billline; delete il.__typename; if (il.id) { diff --git a/client/src/components/card-payment-modal/card-payment-modal.component..jsx b/client/src/components/card-payment-modal/card-payment-modal.component..jsx index b7606222e..fcdbd417c 100644 --- a/client/src/components/card-payment-modal/card-payment-modal.component..jsx +++ b/client/src/components/card-payment-modal/card-payment-modal.component..jsx @@ -1,6 +1,6 @@ -import { DeleteFilled } from "@ant-design/icons"; +import { DeleteFilled, CopyFilled } from "@ant-design/icons"; import { useLazyQuery, useMutation } from "@apollo/client"; -import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd"; +import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, message, notification } from "antd"; import axios from "axios"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -14,10 +14,12 @@ 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 + bodyshop: selectBodyshop, + currentUser: getCurrentUser }); const mapDispatchToProps = (dispatch) => ({ @@ -25,11 +27,17 @@ const mapDispatchToProps = (dispatch) => ({ toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")) }); -const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => { +const CardPaymentModalComponent = ({ + bodyshop, + currentUser, + 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); @@ -37,7 +45,7 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, { variables: { jobids: [context.jobid] }, - skip: true + skip: !context?.jobid }); //Initialize the intellipay window. @@ -51,8 +59,7 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi //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); @@ -86,7 +93,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi }); }; - const handleIntelliPayCharge = async () => { setLoading(true); //Validate @@ -101,7 +107,7 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi const response = await axios.post("/intellipay/lightbox_credentials", { bodyshop, refresh: !!window.intellipay, - paymentSplitMeta: form.getFieldsValue(), + paymentSplitMeta: form.getFieldsValue() }); if (window.intellipay) { @@ -126,6 +132,42 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi } }; + 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 ( @@ -202,16 +244,14 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi - prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).join() + prevValues.payments?.map((p) => p?.jobid + p?.amount).join() !== + curValues.payments?.map((p) => p?.jobid + p?.amount).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 ( @@ -246,7 +286,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi const totalAmountToCharge = payments?.reduce((acc, val) => { return acc + (val?.amount || 0); }, 0); - return ( @@ -273,11 +312,36 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi > {t("job_payments.buttons.proceedtopayment")} + + + ); }} + {paymentLink && ( + { + navigator.clipboard.writeText(paymentLink); + message.success(t("general.actions.copied")); + }} + > +
{paymentLink}
+ +
+ )}
); diff --git a/client/src/components/job-totals-table/job-totals.table.totals.component.jsx b/client/src/components/job-totals-table/job-totals.table.totals.component.jsx index 2d4372a09..a8b5e0604 100644 --- a/client/src/components/job-totals-table/job-totals.table.totals.component.jsx +++ b/client/src/components/job-totals-table/job-totals.table.totals.component.jsx @@ -141,10 +141,14 @@ export function JobTotalsTableTotals({ bodyshop, job }) { key: t("jobs.fields.ded_amt"), total: job.job_totals.totals.custPayable.deductible }, - // { - // key: t("jobs.fields.federal_tax_payable"), - // total: job.job_totals.totals.custPayable.federal_tax, - // }, + ...(InstanceRenderManager({ + imex: [{ + key: t("jobs.fields.federal_tax_payable"), + total: job.job_totals.totals.custPayable.federal_tax + }], + rome: [], + promanager: "USE_ROME" + })), { key: t("jobs.fields.other_amount_payable"), total: job.job_totals.totals.custPayable.other_customer_amount diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx index 004addbad..f7164f748 100644 --- a/client/src/components/jobs-list/jobs-list.component.jsx +++ b/client/src/components/jobs-list/jobs-list.component.jsx @@ -250,8 +250,8 @@ export function JobsList({ bodyshop }) { }, { title: t("jobs.labels.estimator"), - dataIndex: "jobs.labels.estimator", - key: "jobs.labels.estimator", + dataIndex: "estimator", + key: "estimator", ellipsis: true, responsive: ["xl"], sorter: (a, b) => diff --git a/client/src/components/parts-order-modal/parts-order-modal-price-change.component.jsx b/client/src/components/parts-order-modal/parts-order-modal-price-change.component.jsx index 4e89ab287..8e182f67f 100644 --- a/client/src/components/parts-order-modal/parts-order-modal-price-change.component.jsx +++ b/client/src/components/parts-order-modal/parts-order-modal-price-change.component.jsx @@ -27,6 +27,10 @@ export default function PartsOrderModalPriceChange({ form, field }) { key: "25", label: t("parts_orders.labels.discount", { percent: "25%" }) }, + { + key: "40", + label: t("parts_orders.labels.discount", { percent: "40%" }) + }, { key: "custom", label: ( diff --git a/client/src/components/payments-generate-link/payments-generate-link.component.jsx b/client/src/components/payments-generate-link/payments-generate-link.component.jsx index 092063d08..b0f4b26b9 100644 --- a/client/src/components/payments-generate-link/payments-generate-link.component.jsx +++ b/client/src/components/payments-generate-link/payments-generate-link.component.jsx @@ -8,11 +8,12 @@ import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions"; -import { selectBodyshop } from "../../redux/user/user.selectors"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop + bodyshop: selectBodyshop, + currentUser: selectCurrentUser }); const mapDispatchToProps = (dispatch) => ({ openChatByPhone: (phone) => dispatch(openChatByPhone(phone)), @@ -20,7 +21,7 @@ const mapDispatchToProps = (dispatch) => ({ }); export default connect(mapStateToProps, mapDispatchToProps)(PaymentsGenerateLink); -export function PaymentsGenerateLink({ bodyshop, callback, job, openChatByPhone, setMessage }) { +export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, openChatByPhone, setMessage }) { const { t } = useTranslation(); const [form] = Form.useForm(); @@ -30,29 +31,35 @@ export function PaymentsGenerateLink({ bodyshop, callback, job, openChatByPhone, const handleFinish = async ({ amount }) => { setLoading(true); - - const p = parsePhoneNumber(job.ownr_ph1, "CA"); + let p; + try { + p = parsePhoneNumber(job.ownr_ph1 || "", "CA"); + } catch (error) { + console.log("Unable to parse phone number"); + } setLoading(true); const response = await axios.post("/intellipay/generate_payment_url", { bodyshop, amount: amount, account: job.ro_number, - invoice: job.id + comment: btoa(JSON.stringify({ payments: [{ jobid: job.id, amount }], userEmail: currentUser.email })) }); setLoading(false); setPaymentLink(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 - }) - ); + 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 + }) + ); + } //Add in confirmation & errors. if (callback) callback(); diff --git a/client/src/components/production-board-kanban/production-board-kanban.container.jsx b/client/src/components/production-board-kanban/production-board-kanban.container.jsx index bf5940cb9..b8a0bef99 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.container.jsx +++ b/client/src/components/production-board-kanban/production-board-kanban.container.jsx @@ -1,8 +1,12 @@ -import React, { useEffect, useMemo } from "react"; +import React, { useEffect, useMemo, useRef } 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 } from "../../graphql/jobs.queries"; +import { + QUERY_JOBS_IN_PRODUCTION, + SUBSCRIPTION_JOBS_IN_PRODUCTION, + SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW +} 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"; @@ -12,7 +16,9 @@ const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser }); -function ProductionBoardKanbanContainer({ bodyshop, currentUser }) { +function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) { + const fired = useRef(false); // useRef to keep track of whether the subscription fired + const combinedStatuses = useMemo( () => [ ...bodyshop.md_ro_statuses.production_statuses, @@ -28,9 +34,12 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) { onError: (error) => console.error(`Error fetching 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 { 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 { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, { variables: { email: currentUser.email }, @@ -41,10 +50,15 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) { // const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {})); useEffect(() => { - if (updatedJobs && data) { - refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`)); + if (!updatedJobs) { + return; } - }, [updatedJobs, data, refetch]); + if (!fired.current) { + fired.current = true; + return; + } + refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`)); + }, [updatedJobs, refetch]); const filteredAssociationSettings = useMemo(() => { return associationSettings?.associations[0] || null; diff --git a/client/src/components/production-board-kanban/production-board-kanban.styles.scss b/client/src/components/production-board-kanban/production-board-kanban.styles.scss index 6e7ee63ce..8b04aaa5d 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.styles.scss +++ b/client/src/components/production-board-kanban/production-board-kanban.styles.scss @@ -17,7 +17,6 @@ border-radius: 5px 5px 0 0; } - .production-alert { background: transparent; border: none; @@ -70,3 +69,8 @@ } } } + +.clone.is-dragging .ant-card { + border: #1890ff 2px solid !important; + border-radius: 12px; +} diff --git a/client/src/components/production-board-kanban/trello-board/dnd/lib/state/get-droppable-over.js b/client/src/components/production-board-kanban/trello-board/dnd/lib/state/get-droppable-over.js index dcf0cf04c..cb7f7c75b 100644 --- a/client/src/components/production-board-kanban/trello-board/dnd/lib/state/get-droppable-over.js +++ b/client/src/components/production-board-kanban/trello-board/dnd/lib/state/get-droppable-over.js @@ -25,8 +25,8 @@ function getFurthestAway({ pageBorderBox, draggable, candidates }) { const axis = candidate.axis; const target = patch( candidate.axis.line, - // use the current center of the dragging item on the main axis - pageBorderBox.center[axis.line], + // use the center of the list on the main axis + candidate.page.borderBox.center[axis.line], // use the center of the list on the cross axis candidate.page.borderBox.center[axis.crossAxisLine] ); diff --git a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-droppable-publisher/get-closest-scrollable.js b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-droppable-publisher/get-closest-scrollable.js index 923622788..0b069cce5 100644 --- a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-droppable-publisher/get-closest-scrollable.js +++ b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-droppable-publisher/get-closest-scrollable.js @@ -5,6 +5,7 @@ import getBodyElement from "../get-body-element"; const isEqual = (base) => (value) => base === value; const isScroll = isEqual("scroll"); const isAuto = isEqual("auto"); +const isOverlay = isEqual("overlay"); const isVisible = isEqual("visible"); const isEither = (overflow, fn) => fn(overflow.overflowX) || fn(overflow.overflowY); const isBoth = (overflow, fn) => fn(overflow.overflowX) && fn(overflow.overflowY); @@ -14,7 +15,7 @@ const isElementScrollable = (el) => { overflowX: style.overflowX, overflowY: style.overflowY }; - return isEither(overflow, isScroll) || isEither(overflow, isAuto); + return isEither(overflow, isScroll) || isEither(overflow, isAuto) || isEither(overflow, isOverlay); }; // Special case for a body element diff --git a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/find-closest-draggable-id-from-event.js b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/find-closest-draggable-id-from-event.js index 60285d2a4..f17be1759 100644 --- a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/find-closest-draggable-id-from-event.js +++ b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/find-closest-draggable-id-from-event.js @@ -8,7 +8,7 @@ function getSelector(contextId) { return `[${attributes.dragHandle.contextId}="${contextId}"]`; } -function findClosestDragHandleFromEvent(contextId, event) { +export function findClosestDragHandleFromEvent(contextId, event) { const target = event.target; if (!isElement(target)) { warning("event.target must be a Element"); diff --git a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/sensors/use-touch-sensor.js b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/sensors/use-touch-sensor.js index 3210e2188..d679f7442 100644 --- a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/sensors/use-touch-sensor.js +++ b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/sensors/use-touch-sensor.js @@ -240,11 +240,14 @@ export default function useTouchSensor(api) { y: clientY }; + const handle = api.findClosestDragHandle(event); + invariant(handle, "Touch sensor unable to find drag handle"); + // unbind this event handler unbindEventsRef.current(); // eslint-disable-next-line no-use-before-define - startPendingDrag(actions, point); + startPendingDrag(actions, point, handle); } }), // not including stop or startPendingDrag as it is not defined initially @@ -288,7 +291,7 @@ export default function useTouchSensor(api) { } }, [stop]); const bindCapturingEvents = useCallback( - function bindCapturingEvents() { + function bindCapturingEvents(target) { const options = { capture: true, passive: false @@ -307,7 +310,7 @@ export default function useTouchSensor(api) { // Old behaviour: // https://gist.github.com/parris/dda613e3ae78f14eb2dc9fa0f4bfce3d // https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed - const unbindTarget = bindEvents(window, getHandleBindings(args), options); + const unbindTarget = bindEvents(target, getHandleBindings(args), options); const unbindWindow = bindEvents(window, getWindowBindings(args), options); unbindEventsRef.current = function unbindAll() { unbindTarget(); @@ -330,7 +333,7 @@ export default function useTouchSensor(api) { [getPhase, setPhase] ); const startPendingDrag = useCallback( - function startPendingDrag(actions, point) { + function startPendingDrag(actions, point, target) { invariant(getPhase().type === "IDLE", "Expected to move from IDLE to PENDING drag"); const longPressTimerId = setTimeout(startDragging, timeForLongPress); setPhase({ @@ -339,7 +342,7 @@ export default function useTouchSensor(api) { actions, longPressTimerId }); - bindCapturingEvents(); + bindCapturingEvents(target); }, [bindCapturingEvents, getPhase, setPhase, startDragging] ); diff --git a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/use-sensor-marshal.js b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/use-sensor-marshal.js index 17f84ee6b..c559a56ab 100644 --- a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/use-sensor-marshal.js +++ b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/use-sensor-marshal.js @@ -23,7 +23,9 @@ import getBorderBoxCenterPosition from "../get-border-box-center-position"; import { warning } from "../../dev-warning"; import useLayoutEffect from "../use-isomorphic-layout-effect"; import { noop } from "../../empty"; -import findClosestDraggableIdFromEvent from "./find-closest-draggable-id-from-event"; +import findClosestDraggableIdFromEvent, { + findClosestDragHandleFromEvent +} from "./find-closest-draggable-id-from-event"; import findDraggable from "../get-elements/find-draggable"; import bindEvents from "../event-bindings/bind-events"; @@ -339,6 +341,9 @@ export default function useSensorMarshal({ contextId, store, registry, customSen }), [contextId, lockAPI, registry, store] ); + + const findClosestDragHandle = useCallback((event) => findClosestDragHandleFromEvent(contextId, event), [contextId]); + const findClosestDraggableId = useCallback((event) => findClosestDraggableIdFromEvent(contextId, event), [contextId]); const findOptionsForDraggable = useCallback( (id) => { @@ -370,9 +375,18 @@ export default function useSensorMarshal({ contextId, store, registry, customSen findClosestDraggableId, findOptionsForDraggable, tryReleaseLock, - isLockClaimed + isLockClaimed, + findClosestDragHandle }), - [canGetLock, tryGetLock, findClosestDraggableId, findOptionsForDraggable, tryReleaseLock, isLockClaimed] + [ + canGetLock, + tryGetLock, + findClosestDraggableId, + findOptionsForDraggable, + tryReleaseLock, + isLockClaimed, + findClosestDragHandle + ] ); // Bad ass diff --git a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/get-styles.js b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/get-styles.js index 418acb50b..f62947525 100644 --- a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/get-styles.js +++ b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/get-styles.js @@ -83,7 +83,13 @@ const getFinalStyles = (contextId) => { return { selector: getSelector(attributes.draggable.contextId), styles: { - dragging: transition, + dragging: ` + ${transition} + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + `, dropAnimating: transition, userCancel: transition } diff --git a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/use-style-marshal.js b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/use-style-marshal.js index 19457b312..b27b4d534 100644 --- a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/use-style-marshal.js +++ b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/use-style-marshal.js @@ -67,7 +67,9 @@ export default function useStyleMarshal(contextId, nonce) { const remove = (ref) => { const current = ref.current; invariant(current, "Cannot unmount ref as it is not set"); - getHead().removeChild(current); + if (getHead().contains(current)) { + getHead().removeChild(current); + } ref.current = null; }; remove(alwaysRef); diff --git a/client/src/components/production-list-columns/production-list-columns.data.jsx b/client/src/components/production-list-columns/production-list-columns.data.jsx index 16a450dab..950d956af 100644 --- a/client/src/components/production-list-columns/production-list-columns.data.jsx +++ b/client/src/components/production-list-columns/production-list-columns.data.jsx @@ -298,6 +298,16 @@ 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) => }, { diff --git a/client/src/components/production-list-table/production-list-table.container.jsx b/client/src/components/production-list-table/production-list-table.container.jsx index 29ddf015a..9f564bde7 100644 --- a/client/src/components/production-list-table/production-list-table.container.jsx +++ b/client/src/components/production-list-table/production-list-table.container.jsx @@ -4,12 +4,13 @@ import { QUERY_EXACT_JOB_IN_PRODUCTION, QUERY_EXACT_JOBS_IN_PRODUCTION, QUERY_JOBS_IN_PRODUCTION, - SUBSCRIPTION_JOBS_IN_PRODUCTION + SUBSCRIPTION_JOBS_IN_PRODUCTION, + SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW } from "../../graphql/jobs.queries"; import ProductionListTable from "./production-list-table.component"; import _ from "lodash"; -export default function ProductionListTableContainer() { +export default function ProductionListTableContainer({ subscriptionType = "direct" }) { const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, { pollInterval: 3600000, fetchPolicy: "network-only", @@ -17,7 +18,9 @@ export default function ProductionListTableContainer() { }); const client = useApolloClient(); const [joblist, setJoblist] = useState([]); - const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION); + const { data: updatedJobs } = useSubscription( + subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION + ); useEffect(() => { if (!(data && data.jobs)) return; diff --git a/client/src/components/report-center-modal/report-center-modal.component.jsx b/client/src/components/report-center-modal/report-center-modal.component.jsx index 71439f287..974e62de7 100644 --- a/client/src/components/report-center-modal/report-center-modal.component.jsx +++ b/client/src/components/report-center-modal/report-center-modal.component.jsx @@ -34,28 +34,34 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) { const [form] = Form.useForm(); const [search, setSearch] = useState(""); const { - treatments: { Enhanced_Payroll } + treatments: { Enhanced_Payroll, ADPPayroll } } = useSplitTreatments({ attributes: {}, - names: ["Enhanced_Payroll"], + names: ["Enhanced_Payroll", "ADPPayroll"], splitKey: bodyshop.imexshopid }); const [loading, setLoading] = useState(false); const { t } = useTranslation(); const Templates = TemplateList("report_center"); - const ReportsList = - Enhanced_Payroll.treatment === "on" - ? Object.keys(Templates) - .map((key) => { - return Templates[key]; - }) - .filter((temp) => temp.enhanced_payroll === undefined || temp.enhanced_payroll === true) - : Object.keys(Templates) - .map((key) => { - return Templates[key]; - }) - .filter((temp) => temp.enhanced_payroll === undefined || temp.enhanced_payroll === false); + const ReportsList = Object.keys(Templates) + .map((key) => Templates[key]) + .filter((temp) => { + const enhancedPayrollOn = Enhanced_Payroll.treatment === "on"; + const adpPayrollOn = ADPPayroll.treatment === "on"; + + if (enhancedPayrollOn && adpPayrollOn) { + return temp.enhanced_payroll !== false || temp.adp_payroll !== false; + } + if (enhancedPayrollOn) { + return temp.enhanced_payroll !== false && temp.adp_payroll !== true; + } + if (adpPayrollOn) { + return temp.adp_payroll !== false && temp.enhanced_payroll !== true; + } + + return temp.enhanced_payroll !== true && temp.adp_payroll !== true; + }); const { open } = reportCenterModal; @@ -104,7 +110,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) { to: values.to, subject: Templates[values.key]?.subject }, - values.sendbyexcel === "excel" ? "x" : values.sendby === "email" ? "e" : "p", + values.sendbytext === "text" ? values.sendbytext : values.sendbyexcel === "excel" ? "x" : values.sendby === "email" ? "e" : "p", id ); setLoading(false); @@ -291,7 +297,15 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) { ); - if (reporttype !== "excel") + if (reporttype === "text") + return ( + + + {t("general.labels.text")} + + + ); + if (reporttype !== "excel" || reporttype !== "text") return ( diff --git a/client/src/components/schedule-calendar-wrapper/localizer.js b/client/src/components/schedule-calendar-wrapper/localizer.js new file mode 100644 index 000000000..e91016416 --- /dev/null +++ b/client/src/components/schedule-calendar-wrapper/localizer.js @@ -0,0 +1,505 @@ +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; diff --git a/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx b/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx index fc79cd4a9..fb866d89d 100644 --- a/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx +++ b/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx @@ -1,7 +1,7 @@ import dayjs from "../../utils/day"; import queryString from "query-string"; import React from "react"; -import { Calendar, dayjsLocalizer } from "react-big-calendar"; +import { Calendar } from "react-big-calendar"; import { connect } from "react-redux"; import { Link, useLocation, useNavigate } from "react-router-dom"; import { createStructuredSelector } from "reselect"; @@ -14,12 +14,13 @@ 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 = dayjsLocalizer(dayjs); +const localizer = local(dayjs); export function ScheduleCalendarWrapperComponent({ bodyshop, diff --git a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.util.js b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.util.js index 47394d966..32703881c 100644 --- a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.util.js +++ b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.util.js @@ -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().businessDaysInMonth().length; +export const CalculateWorkingDaysAsOfToday = () => dayjs().endOf("day").businessDiff(dayjs().startOf("month")); export const CalculateWorkingDaysLastMonth = () => dayjs().subtract(1, "month").endOf("month").businessDaysInMonth().length; diff --git a/client/src/components/shop-info/shop-info.component.jsx b/client/src/components/shop-info/shop-info.component.jsx index 6bdba17bd..ac18a673f 100644 --- a/client/src/components/shop-info/shop-info.component.jsx +++ b/client/src/components/shop-info/shop-info.component.jsx @@ -20,6 +20,7 @@ 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 @@ -135,6 +136,17 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) { ], rome: "USE_IMEX", promanager: [] + }), + ...InstanceRenderManager({ + imex: [], + rome: [ + { + key: "intellipay", + label: t("bodyshop.labels.intellipay"), + children: + } + ], + promanager: [] }) ]; return ( diff --git a/client/src/components/shop-info/shop-info.general.component.jsx b/client/src/components/shop-info/shop-info.general.component.jsx index 20cc88d36..2a62fdead 100644 --- a/client/src/components/shop-info/shop-info.general.component.jsx +++ b/client/src/components/shop-info/shop-info.general.component.jsx @@ -7,13 +7,13 @@ import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; import DatePickerRanges from "../../utils/DatePickerRanges"; +import InstanceRenderManager from "../../utils/instanceRenderMgr"; +import FeatureWrapper, { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component"; import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; -import InstanceRenderManager from "../../utils/instanceRenderMgr"; -import FeatureWrapper, { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; // TODO: Client Update, this might break const timeZonesList = Intl.supportedValuesOf("timeZone"); const mapStateToProps = createStructuredSelector({ @@ -28,10 +28,10 @@ export function ShopInfoGeneral({ form, bodyshop }) { const { t } = useTranslation(); const { - treatments: { ClosingPeriod } + treatments: { ClosingPeriod, ADPPayroll } } = useSplitTreatments({ attributes: {}, - names: ["ClosingPeriod"], + names: ["ClosingPeriod", "ADPPayroll"], splitKey: bodyshop && bodyshop.imexshopid }); @@ -98,7 +98,6 @@ export function ShopInfoGeneral({ form, bodyshop }) { - {ClosingPeriod.treatment === "on" && ( - <> - - - - + + + + )} + {ADPPayroll.treatment === "on" && ( + + + + )} + {ADPPayroll.treatment === "on" && ( + + + )} diff --git a/client/src/components/shop-info/shop-intellipay-config.component.jsx b/client/src/components/shop-info/shop-intellipay-config.component.jsx new file mode 100644 index 000000000..3dbdee3ed --- /dev/null +++ b/client/src/components/shop-info/shop-intellipay-config.component.jsx @@ -0,0 +1,54 @@ +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 ( + <> + + {() => { + const { intellipay_config } = form.getFieldsValue(); + + if (intellipay_config?.enable_cash_discount) + return ; + }} + + + + + + + ({ required: form.getFieldValue(["intellipay_config", "enable_cash_discount"]) }) + ]} + > + + + + + ); +} diff --git a/client/src/components/update-alert/update-alert.component.jsx b/client/src/components/update-alert/update-alert.component.jsx index b4a867052..32f80ea79 100644 --- a/client/src/components/update-alert/update-alert.component.jsx +++ b/client/src/components/update-alert/update-alert.component.jsx @@ -1,13 +1,14 @@ import { AlertOutlined } from "@ant-design/icons"; -import { Alert, Button, Col, Row, Space } from "antd"; +import { Alert, Button, Col, notification, Row, Space } from "antd"; import i18n from "i18next"; -import React, { useEffect } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectUpdateAvailable } from "../../redux/application/application.selectors"; import { useRegisterSW } from "virtual:pwa-register/react"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; +import useCountDown from "../../utils/countdownHook"; const mapStateToProps = createStructuredSelector({ updateAvailable: selectUpdateAvailable @@ -19,6 +20,15 @@ const mapDispatchToProps = (dispatch) => ({ export function UpdateAlert({ updateAvailable }) { const { t } = useTranslation(); + const [timerStarted, setTimerStarted] = useState(false); + const [loading, setLoading] = useState(false); + const [ + timeLeft, + { + start //pause, resume, reset + } + ] = useCountDown(180000, 1000); + const { offlineReady: [offlineReady], needRefresh: [needRefresh], @@ -40,11 +50,43 @@ export function UpdateAlert({ updateAvailable }) { } }); + const ReloadNewVersion = useCallback(() => { + setLoading(true); + updateServiceWorker(true); + setTimeout(() => { + window.location.reload(true); + }, 5000); + }, [updateServiceWorker]); + useEffect(() => { - if (import.meta.env.DEV) { - console.log(`SW Status => Refresh? ${needRefresh} - offlineReady? ${offlineReady}`); + if (needRefresh) { + start(); + setTimerStarted(true); } - }, [needRefresh, offlineReady]); + }, [start, needRefresh, offlineReady]); + + useEffect(() => { + if (needRefresh && timerStarted && timeLeft < 60000) { + notification.open({ + type: "warning", + closable: false, + duration: 65000, + key: "autoupdate", + message: t("general.actions.autoupdate", { + time: (timeLeft / 1000).toFixed(0), + app: InstanceRenderManager({ + imex: "$t(titles.imexonline)", + rome: "$t(titles.romeonline)", + promanager: "$t(titles.promanager)" + }) + }), + placement: "bottomRight" + }); + } + if (needRefresh && timerStarted && timeLeft <= 0) { + ReloadNewVersion(); + } + }, [timeLeft, t, needRefresh, ReloadNewVersion, timerStarted]); if (!needRefresh) return null; @@ -75,9 +117,10 @@ export function UpdateAlert({ updateAvailable }) { - + diff --git a/client/src/components/vendor-search-select/vendor-search-select.component.jsx b/client/src/components/vendor-search-select/vendor-search-select.component.jsx index 8e9d6b5c2..6236c8ad4 100644 --- a/client/src/components/vendor-search-select/vendor-search-select.component.jsx +++ b/client/src/components/vendor-search-select/vendor-search-select.component.jsx @@ -5,7 +5,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter"; const { Option } = Select; -//To be used as a form element only. +// To be used as a form element only. const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, preferredMake, showPhone }, ref) => { const [option, setOption] = useState(value); @@ -33,9 +33,25 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref if (!value || !options) return label; const discount = options?.find((o) => o.id === value)?.discount; return ( -
-
{label}
- +
+
+ {label} +
{discount && discount !== 0 ? {`${discount * 100}%`} : null}
); @@ -45,36 +61,67 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref optionFilterProp="name" onSelect={onSelect} disabled={disabled || false} - optionLabelProp={"name"} + optionLabelProp="name" > - {favorites - ? favorites.map((o) => ( -