diff --git a/client/.env.development.imex b/client/.env.development.imex index b5e6e8f53..2f2d96aaf 100644 --- a/client/.env.development.imex +++ b/client/.env.development.imex @@ -1,5 +1,5 @@ -VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql -VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql +VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql +VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql VITE_APP_GA_CODE=231099835 VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"} VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test diff --git a/client/.env.development.promanager b/client/.env.development.promanager index 15fa08bce..d870802cc 100644 --- a/client/.env.development.promanager +++ b/client/.env.development.promanager @@ -1,5 +1,5 @@ -VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql -VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql +VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql +VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql VITE_APP_GA_CODE=231099835 VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"} VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test diff --git a/client/.env.development.rome b/client/.env.development.rome index fce6ec37b..c68088a98 100644 --- a/client/.env.development.rome +++ b/client/.env.development.rome @@ -1,7 +1,8 @@ -VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql -VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql +VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql +VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql VITE_APP_GA_CODE=231099835 -VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"} +# VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"} +VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"} VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test VITE_APP_CLOUDINARY_API_KEY=957865933348715 diff --git a/client/package-lock.json b/client/package-lock.json index bf0a5afcd..f794877d6 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -109,7 +109,8 @@ "vite-plugin-legacy": "^2.1.0", "vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-pwa": "^0.20.1", - "vite-plugin-style-import": "^2.0.0" + "vite-plugin-style-import": "^2.0.0", + "workbox-window": "^7.1.0" }, "engines": { "node": ">=18.18.2" @@ -18429,6 +18430,7 @@ "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.1.0.tgz", "integrity": "sha512-ZHeROyqR+AS5UPzholQRDttLFqGMwP0Np8MKWAdyxsDETxq3qOAyXvqessc3GniohG6e0mAqSQyKOHmT8zPF7g==", "dev": true, + "license": "MIT", "dependencies": { "@types/trusted-types": "^2.0.2", "workbox-core": "7.1.0" diff --git a/client/package.json b/client/package.json index 42f26736d..e89aabfb0 100644 --- a/client/package.json +++ b/client/package.json @@ -153,6 +153,7 @@ "vite-plugin-legacy": "^2.1.0", "vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-pwa": "^0.20.1", - "vite-plugin-style-import": "^2.0.0" + "vite-plugin-style-import": "^2.0.0", + "workbox-window": "^7.1.0" } } diff --git a/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx b/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx index e8f52ee31..e34ae14f7 100644 --- a/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx +++ b/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import React, { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import dayjs from "../../utils/day"; -import { dateFormats, dateTimeFormats } from "./formats.js"; +import { fuzzyMatchDate } from "./formats.js"; const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, onlyToday, isDateOnly = false, ...restProps }) => { const [isManualInput, setIsManualInput] = useState(false); @@ -11,79 +11,34 @@ const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, onlyToday, is const handleChange = useCallback( (newDate) => { - if (newDate === null && onChange) { - onChange(null); - } else if (newDate && onChange) { - onChange(newDate); + if (onChange) { + onChange(newDate || null); } setIsManualInput(false); }, [onChange] ); - const normalizeDateTimeString = (input) => { - const upperV = input.toUpperCase().replaceAll(".", "/").replaceAll("-", "/"); - - const [datePart, ...timeParts] = upperV.split(" "); - - if (timeParts.length === 0) { - return datePart; // If there's no time part, just return the date part. - } - - const timePart = timeParts.join(" "); // In case there are multiple spaces, join them back - - // Normalize the time part by ensuring there's a space before AM/PM if not already present - const normalizedTime = timePart.replace(/(\d{1,2})(:\d{2})?\s?(AM|PM)/, "$1$2 $3"); - - // Combine the date part with the normalized time part - return `${datePart} ${normalizedTime}`.trim(); - }; - const handleBlur = useCallback( (e) => { + // Bail if this is not a manual input if (!isManualInput) { return; } - + // Reset manual input flag setIsManualInput(false); - const v = e.target.value; + const v = e?.target?.value; + if (!v) return; - const upperV = normalizeDateTimeString(v); - let parsedDate; + let parsedDate = isDateOnly ? fuzzyMatchDate(v)?.startOf("day") : fuzzyMatchDate(v); - for (const format of isDateOnly ? dateFormats : dateTimeFormats) { - parsedDate = dayjs(upperV, format); - if (parsedDate.isValid()) break; - } - - if (parsedDate && parsedDate.isValid()) { - if (isDateOnly) { - parsedDate = parsedDate.startOf("day"); - } - - if (value && value.isValid && value.isValid()) { - parsedDate = parsedDate.set({ - hours: value.hours(), - minutes: value.minutes(), - seconds: value.seconds(), - milliseconds: value.milliseconds() - }); - } - - if (onlyFuture) { - if (dayjs().subtract(1, "day").isBefore(parsedDate)) { - onChange(parsedDate); - } else { - onChange(dayjs().startOf("day")); - } - } else { - onChange(parsedDate); - } + if (parsedDate && onChange) { + onChange(parsedDate); } }, - [isManualInput, isDateOnly, onlyFuture, onChange, value] + [isManualInput, isDateOnly, onChange] ); const handleKeyDown = useCallback( diff --git a/client/src/components/form-date-time-picker/formats.js b/client/src/components/form-date-time-picker/formats.js index b68704a72..ffe9f68c2 100644 --- a/client/src/components/form-date-time-picker/formats.js +++ b/client/src/components/form-date-time-picker/formats.js @@ -1,96 +1,63 @@ -export const dateTimeFormats = [ - // Four-digit year with time - "M/D/YYYY h:mm A", // Example: 1/5/2023 9:00 AM - "M/D/YYYY h:mmA", // Example: 1/5/2023 9:00AM - "M/D/YYYY h A", // Example: 1/5/2023 9 AM - "M/D/YYYY hA", // Example: 1/5/2023 9AM - "M/D/YYYY hh:mm A", // Example: 1/5/2023 02:25 PM - "M/D/YYYY hh:mm:ss A", // Example: 1/5/2023 02:25:45 PM +import dayjs from "../../utils/day"; - "MM/D/YYYY h:mm A", // Example: 12/5/2023 9:00 AM - "MM/D/YYYY h:mmA", // Example: 12/5/2023 9:00AM - "MM/D/YYYY h A", // Example: 12/5/2023 9 AM - "MM/D/YYYY hA", // Example: 12/5/2023 9AM - "MM/D/YYYY hh:mm A", // Example: 12/5/2023 02:25 PM - "MM/D/YYYY hh:mm:ss A", // Example: 12/5/2023 02:25:45 PM - - "M/DD/YYYY h:mm A", // Example: 1/25/2023 9:00 AM - "M/DD/YYYY h:mmA", // Example: 1/25/2023 9:00AM - "M/DD/YYYY h A", // Example: 1/25/2023 9 AM - "M/DD/YYYY hA", // Example: 1/25/2023 9AM - "M/DD/YYYY hh:mm A", // Example: 1/25/2023 02:25 PM - "M/DD/YYYY hh:mm:ss A", // Example: 1/25/2023 02:25:45 PM - - "MM/DD/YYYY h:mm A", // Example: 12/25/2023 9:00 AM - "MM/DD/YYYY h:mmA", // Example: 12/25/2023 9:00AM - "MM/DD/YYYY h A", // Example: 12/25/2023 9 AM - "MM/DD/YYYY hA", // Example: 12/25/2023 9AM - "MM/DD/YYYY hh:mm A", // Example: 12/25/2023 02:25 PM - "MM/DD/YYYY hh:mm:ss A", // Example: 12/25/2023 02:25:45 PM - - // Two-digit year with time - "M/D/YY h:mm A", // Example: 1/5/23 9:00 AM - "M/D/YY h:mmA", // Example: 1/5/23 9:00AM - "M/D/YY h A", // Example: 1/5/23 9 AM - "M/D/YY hA", // Example: 1/5/23 9AM - "M/D/YY hh:mm A", // Example: 1/5/23 02:25 PM - "M/D/YY hh:mm:ss A", // Example: 1/5/23 02:25:45 PM - - "MM/D/YY h:mm A", // Example: 12/5/23 9:00 AM - "MM/D/YY h:mmA", // Example: 12/5/23 9:00AM - "MM/D/YY h A", // Example: 12/5/23 9 AM - "MM/D/YY hA", // Example: 12/5/23 9AM - "MM/D/YY hh:mm A", // Example: 12/5/23 02:25 PM - "MM/D/YY hh:mm:ss A", // Example: 12/5/23 02:25:45 PM - - "M/DD/YY h:mm A", // Example: 1/25/23 9:00 AM - "M/DD/YY h:mmA", // Example: 1/25/23 9:00AM - "M/DD/YY h A", // Example: 1/25/23 9 AM - "M/DD/YY hA", // Example: 1/25/23 9AM - "M/DD/YY hh:mm A", // Example: 1/25/23 02:25 PM - "M/DD/YY hh:mm:ss A", // Example: 1/25/23 02:25:45 PM - - "MM/DD/YY h:mm A", // Example: 12/25/23 9:00 AM - "MM/DD/YY h:mmA", // Example: 12/25/23 9:00AM - "MM/DD/YY h A", // Example: 12/25/23 9 AM - "MM/DD/YY hA", // Example: 12/25/23 9AM - "MM/DD/YY hh:mm A", // Example: 12/25/23 02:25 PM - "MM/DD/YY hh:mm:ss A", // Example: 12/25/23 02:25:45 PM - - // Four-digit year without time - "M/D/YYYY", // Example: 1/5/2023 - "MM/D/YYYY", // Example: 12/5/2023 - "M/DD/YYYY", // Example: 1/25/2023 - "MM/DD/YYYY", // Example: 12/25/2023 - - // Two-digit year without time - "M/D/YY", // Example: 1/5/23 - "MM/D/YY", // Example: 12/5/23 - "M/DD/YY", // Example: 1/25/23 - "MM/DD/YY" // Example: 12/25/23 -]; - -// CONFIRMED -export const dateFormats = [ +const dateFormats = [ "MMDDYYYY", "MMDDYY", - // Four-digit year - "M/D/YYYY", // Example: 1/5/2023 - "MM/D/YYYY", // Example: 12/5/2023 - "M/DD/YYYY", // Example: 1/25/2023 - "MM/DD/YYYY", // Example: 12/25/2023 - - // Two-digit year - "M/D/YY", // Example: 1/5/23 - "MM/D/YY", // Example: 12/5/23 - "M/DD/YY", // Example: 1/25/23 - "MM/DD/YY", // Example: 12/25/23 - - // Explicitly handle single-digit month and day - "M/D/YY", // Example: 1/5/23 - "M/D/YYYY", // Example: 1/5/2023 - "M/DD/YY", // Example: 1/25/23 - "M/DD/YYYY", // Example: 1/25/2023 - "MM/D/YY", // Example: 12/5/23 - "MM/D/YYYY" // Example: 12/5/2023 + "M/D/YYYY", + "MM/D/YYYY", + "M/DD/YYYY", + "MM/DD/YYYY", + "M/D/YY", + "MM/D/YY", + "M/DD/YY", + "MM/DD/YY" ]; + +const timeFormats = ["h:mm A", "h:mmA", "h A", "hA", "hh:mm A", "hh:mm:ss A"]; + +const dateTimeFormats = [ + ...["M/D/YYYY", "MM/D/YYYY", "M/DD/YYYY", "MM/DD/YYYY", "M/D/YY", "MM/D/YY", "M/DD/YY", "MM/DD/YY"].flatMap( + (dateFormat) => timeFormats.map((timeFormat) => `${dateFormat} ${timeFormat}`) + ), + + ...["MMDDYYYY", "MMDDYY"].flatMap((dateFormat) => timeFormats.map((timeFormat) => `${dateFormat} ${timeFormat}`)), + + "M/D/YYYY", + "MM/D/YYYY", + "M/DD/YYYY", + "MM/DD/YYYY", + "M/D/YY", + "MM/D/YY", + "M/DD/YY", + "MM/DD/YY", + "MMDDYYYY", + "MMDDYY" +]; + +const sanitizeInput = (input) => + input + .trim() + .toUpperCase() + .replace(/\s*(am|pm)\s*/i, " $1") + .replaceAll(".", "/") + .replaceAll("-", "/"); + +export const fuzzyMatchDate = (dateString) => { + const sanitizedInput = sanitizeInput(dateString); + + for (const format of dateFormats) { + const parsedDate = dayjs(sanitizedInput, format, true); + if (parsedDate.isValid()) { + return parsedDate; + } + } + + for (const format of dateTimeFormats) { + const parsedDateTime = dayjs(sanitizedInput, format, true); + if (parsedDateTime.isValid()) { + return parsedDateTime; // Return the dayjs object + } + } + + return null; // If no matching format is found +}; diff --git a/client/src/components/production-board-kanban/production-board-kanban.statistics.jsx b/client/src/components/production-board-kanban/production-board-kanban.statistics.jsx index 1af5ec59c..301ff17f0 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.statistics.jsx +++ b/client/src/components/production-board-kanban/production-board-kanban.statistics.jsx @@ -3,6 +3,7 @@ import { Card, Statistic } from "antd"; import { useTranslation } from "react-i18next"; import PropTypes from "prop-types"; import { defaultKanbanSettings, statisticsItems } from "./settings/defaultKanbanSettings.js"; +import Dinero from "dinero.js"; export const StatisticType = { HOURS: "hours", @@ -32,7 +33,21 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => { }; const calculateTotalAmount = (items, key) => { - return items.reduce((acc, item) => acc + (item[key]?.totals?.subtotal?.amount || 0), 0); + return items.reduce((acc, item) => acc.add(Dinero(item[key]?.totals?.subtotal ?? Dinero())), Dinero({ amount: 0 })); + }; + + const calculateReducerTotalAmount = (lanes, key) => { + return lanes.reduce( + (acc, lane) => { + return acc.add( + lane.cards.reduce( + (laneAcc, card) => laneAcc.add(Dinero(card.metadata[key]?.totals?.subtotal ?? Dinero())), + Dinero({ amount: 0 }) + ) + ); + }, + Dinero({ amount: 0 }) + ); }; const calculateReducerTotal = (lanes, key, subKey) => { @@ -43,14 +58,6 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => { }, 0); }; - const calculateReducerTotalAmount = (lanes, key) => { - return lanes.reduce((acc, lane) => { - return ( - acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata[key]?.totals?.subtotal?.amount || 0), 0) - ); - }, 0); - }; - const formatValue = (value, type) => { if (type === StatisticType.JOBS) { return value.toFixed(0); @@ -87,9 +94,15 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => { const totalAmountInProduction = useMemo(() => { if (!cardSettings.totalAmountInProduction) return null; const total = calculateTotalAmount(data, "job_totals"); - return parseFloat(total.toFixed(2)); + return total.toFormat("$0,0.00"); }, [data, cardSettings.totalAmountInProduction]); + const totalAmountOnBoard = useMemo(() => { + if (!reducerData || !cardSettings.totalAmountOnBoard) return null; + const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals"); + return total.toFormat("$0,0.00"); + }, [reducerData, cardSettings.totalAmountOnBoard]); + const totalHrsOnBoard = useMemo(() => { if (!reducerData || !cardSettings.totalHrsOnBoard) return null; const total = @@ -118,12 +131,6 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => { [reducerData, cardSettings.jobsOnBoard] ); - const totalAmountOnBoard = useMemo(() => { - if (!reducerData || !cardSettings.totalAmountOnBoard) return null; - const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals"); - return parseFloat(total.toFixed(2)); - }, [reducerData, cardSettings.totalAmountOnBoard]); - const tasksInProduction = useMemo(() => { if (!data || !cardSettings.tasksInProduction) return null; return data.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0); @@ -191,7 +198,6 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => { p.name === defaultView)?.columns.tableState) || - bodyshop.production_config[0]?.columns.tableState || { + (bodyshop.production_config && bodyshop.production_config[0]?.columns.tableState) || { sortedInfo: {}, filteredInfo: { text: "" } } diff --git a/client/src/components/tech-job-print-tickets/tech-job-print-tickets.component.jsx b/client/src/components/tech-job-print-tickets/tech-job-print-tickets.component.jsx index 3802c4fb5..3f060089f 100644 --- a/client/src/components/tech-job-print-tickets/tech-job-print-tickets.component.jsx +++ b/client/src/components/tech-job-print-tickets/tech-job-print-tickets.component.jsx @@ -1,5 +1,4 @@ import { Button, Card, DatePicker, Form, Popover, Radio, Space } from "antd"; -import dayjs from "../../utils/day"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -7,10 +6,12 @@ import { createStructuredSelector } from "reselect"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { selectTechnician } from "../../redux/tech/tech.selectors"; import DatePIckerRanges from "../../utils/DatePickerRanges"; +import dayjs from "../../utils/day"; import { GenerateDocument } from "../../utils/RenderTemplate"; import { TemplateList } from "../../utils/TemplateConstants"; const mapStateToProps = createStructuredSelector({ + bodyshop: selectTechnician, technician: selectTechnician }); const mapDispatchToProps = (dispatch) => ({ @@ -18,7 +19,7 @@ const mapDispatchToProps = (dispatch) => ({ }); export default connect(mapStateToProps, mapDispatchToProps)(TechJobPrintTickets); -export function TechJobPrintTickets({ technician, event, attendacePrint }) { +export function TechJobPrintTickets({ bodyshop, technician, event, attendacePrint }) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); @@ -57,7 +58,8 @@ export function TechJobPrintTickets({ technician, event, attendacePrint }) { subject: attendacePrint === true ? Templates.attendance_employee.subject : Templates.timetickets_employee.subject }, - values.sendby // === "email" ? "e" : "p" + values.sendby, + bodyshop ); } catch (error) { console.log(error); diff --git a/client/src/components/time-ticket-list/time-ticket-list.component.jsx b/client/src/components/time-ticket-list/time-ticket-list.component.jsx index 6385aa787..9985aaa3b 100644 --- a/client/src/components/time-ticket-list/time-ticket-list.component.jsx +++ b/client/src/components/time-ticket-list/time-ticket-list.component.jsx @@ -1,6 +1,6 @@ import { EditFilled, SyncOutlined } from "@ant-design/icons"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; import { Button, Card, Checkbox, Space, Table } from "antd"; -import dayjs from "../../utils/day"; import React, { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -10,10 +10,10 @@ import { setModalContext } from "../../redux/modals/modals.actions"; import { selectAuthLevel, selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; import { onlyUnique } from "../../utils/arrayHelper"; +import dayjs from "../../utils/day"; import { alphaSort, dateSort } from "../../utils/sorters"; import RbacWrapper, { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component"; import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component"; -import { useSplitTreatments } from "@splitsoftware/splitio-react"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -165,7 +165,7 @@ export function TimeTicketList({ key: "memo", sorter: (a, b) => alphaSort(a.memo, b.memo), sortOrder: state.sortedInfo.columnKey === "memo" && state.sortedInfo.order, - render: (text, record) => (record.clockon || record.clockoff ? t(record.memo) : record.memo) + render: (text, record) => (record.memo?.startsWith("timetickets.labels") ? t(record.memo) : record.memo) }, ...(Enhanced_Payroll.treatment === "on" ? [ @@ -206,76 +206,98 @@ export function TimeTicketList({ return null; } } - }, - + } ]), - { - title: t("timetickets.fields.created_by"), - dataIndex: "created_by", - key: "created_by", - sorter: (a, b) => alphaSort(a.created_by, b.created_by), - sortOrder: state.sortedInfo.columnKey === "created_by" && state.sortedInfo.order, - render: (text, record) => record.created_by - }, - // { - // title: "Pay", - // dataIndex: "pay", - // key: "pay", - // render: (text, record) => - // Dinero({ amount: Math.round(record.rate * 100) }) - // .multiply(record.flat_rate ? record.productivehrs : record.actualhrs) - // .toFormat("$0.00"), - // }, - { - title: t("general.labels.actions"), - dataIndex: "actions", - key: "actions", - render: (text, record) => ( - - {techConsole && ( - - - - )} - {!techConsole && ( - { - return
; - }} - > - alphaSort(a.created_by, b.created_by), + sortOrder: state.sortedInfo.columnKey === "created_by" && state.sortedInfo.order, + render: (text, record) => record.created_by + }, + // { + // title: "Pay", + // dataIndex: "pay", + // key: "pay", + // render: (text, record) => + // Dinero({ amount: Math.round(record.rate * 100) }) + // .multiply(record.flat_rate ? record.productivehrs : record.actualhrs) + // .toFormat("$0.00"), + // }, + { + title: t("general.labels.actions"), + dataIndex: "actions", + key: "actions", + render: (text, record) => ( + + {techConsole && ( + + + + )} + {!techConsole && ( + { + return
; + }} + > + - - - - )} - - ) - } + } + > + + + + )} + + ) + } ]; const handleTableChange = (pagination, filters, sorter) => { diff --git a/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx b/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx index 7a86d0d56..7f2a1df72 100644 --- a/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx +++ b/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx @@ -1,4 +1,5 @@ import { useLazyQuery } from "@apollo/client"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; import { Form, Input, InputNumber, Select, Switch } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; @@ -7,8 +8,10 @@ import { createStructuredSelector } from "reselect"; import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries"; import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors"; import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component"; -import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component"; -import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component"; +import { + default as DateTimePicker, + default as FormDateTimePicker +} from "../form-date-time-picker/form-date-time-picker.component"; import JobSearchSelect from "../job-search-select/job-search-select.component"; import LaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.component"; import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility"; @@ -16,7 +19,6 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component"; import TimeTicketList from "../time-ticket-list/time-ticket-list.component"; -import { useSplitTreatments } from "@splitsoftware/splitio-react"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -69,13 +71,7 @@ export function TimeTicketModalComponent({ }; const MemoInput = ({ value, ...props }) => { - return ( - - ); + return ; }; return ( @@ -333,7 +329,9 @@ export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideT timetickets={lineTicketData.timetickets} adjustments={lineTicketData.jobs_by_pk.lbr_adjustments} /> - {!hideTimeTickets && } + {!hideTimeTickets && ( + + )}
); } diff --git a/client/src/components/time-ticket-shift-active/time-ticket-shift-active.component.jsx b/client/src/components/time-ticket-shift-active/time-ticket-shift-active.component.jsx index dc67bae09..3401aa7a1 100644 --- a/client/src/components/time-ticket-shift-active/time-ticket-shift-active.component.jsx +++ b/client/src/components/time-ticket-shift-active/time-ticket-shift-active.component.jsx @@ -39,7 +39,7 @@ export default function TimeTicketShiftActive({ timetickets, refetch, isTechCons renderItem={(ticket) => ( { r.update(); }, - 10 * 60 * 1000 + 30 * 60 * 1000 ); } }, diff --git a/client/src/pages/tech-lookup/tech-lookup.container.jsx b/client/src/pages/tech-lookup/tech-lookup.container.jsx index b92a11d3f..a209777ba 100644 --- a/client/src/pages/tech-lookup/tech-lookup.container.jsx +++ b/client/src/pages/tech-lookup/tech-lookup.container.jsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import RbacWrapperComponent from "../../components/rbac-wrapper/rbac-wrapper.component"; import TechLookupJobsList from "../../components/tech-lookup-jobs-list/tech-lookup-jobs-list.component"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; +import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component"; export default function TechLookupContainer() { const { t } = useTranslation(); @@ -20,6 +21,7 @@ export default function TechLookupContainer() { return (
+
diff --git a/client/src/pages/tech/tech.page.component.jsx b/client/src/pages/tech/tech.page.component.jsx index 2733bae68..2be9f0a2a 100644 --- a/client/src/pages/tech/tech.page.component.jsx +++ b/client/src/pages/tech/tech.page.component.jsx @@ -9,7 +9,6 @@ import ErrorBoundary from "../../components/error-boundary/error-boundary.compon import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; import TechHeader from "../../components/tech-header/tech-header.component"; -import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component"; import TechSider from "../../components/tech-sider/tech-sider.component"; import UpdateAlert from "../../components/update-alert/update-alert.component"; import { selectTechnician } from "../../redux/tech/tech.selectors"; @@ -68,7 +67,7 @@ export function TechPage({ technician }) { - + diff --git a/client/src/redux/user/user.sagas.js b/client/src/redux/user/user.sagas.js index 99ba30225..da6b93d65 100644 --- a/client/src/redux/user/user.sagas.js +++ b/client/src/redux/user/user.sagas.js @@ -10,7 +10,7 @@ import { signInWithEmailAndPassword, signOut } from "firebase/auth"; -import { doc, getDoc, setDoc } from "firebase/firestore"; +import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "firebase/firestore"; import { getToken } from "firebase/messaging"; import i18next from "i18next"; import LogRocket from "logrocket"; @@ -48,6 +48,7 @@ import { validatePasswordResetSuccess } from "./user.actions"; import UserActionTypes from "./user.types"; +import cleanAxios from "../../utils/CleanAxios"; const fpPromise = FingerprintJS.load(); @@ -177,10 +178,24 @@ export function* setInstanceIdSaga({ payload: uid }) { // Get the visitor identifier when you need it. const fp = yield fpPromise; const result = yield fp.get(); - yield setDoc(userInstanceRef, { - timestamp: new Date(), - fingerprint: result.visitorId - }); + const res = yield cleanAxios.get("https://api.ipify.org/?format=json"); + const udoc = yield getDoc(userInstanceRef); + + if (!udoc.data()) { + yield setDoc(userInstanceRef, { + timestamp: new Date(), + fingerprint: result.visitorId, + //totalFingerprint: result, + ip: [res.data.ip] + }); + } else { + yield updateDoc(userInstanceRef, { + timestamp: new Date(), + fingerprint: result.visitorId, + //totalFingerprint: result, + ip: arrayUnion(res.data.ip) + }); + } yield put(setLocalFingerprint(result.visitorId)); yield delay(5 * 60 * 1000); diff --git a/hasura/config.yaml b/hasura/config.yaml index fe93f9198..0a62aa7a2 100644 --- a/hasura/config.yaml +++ b/hasura/config.yaml @@ -1,5 +1,5 @@ version: 2 -endpoint: https://db.dev.bodyshop.app +endpoint: https://db.dev.imex.online admin_secret: Dev-BodyShopApp! metadata_directory: metadata actions: diff --git a/hasura/metadata/cron_triggers.yaml b/hasura/metadata/cron_triggers.yaml index 31b24a000..ff7aef8d1 100644 --- a/hasura/metadata/cron_triggers.yaml +++ b/hasura/metadata/cron_triggers.yaml @@ -1,3 +1,27 @@ +- name: AutoHouse Data Pump + webhook: '{{HASURA_API_URL}}/data/ah' + schedule: 0 6 * * * + include_in_metadata: true + payload: {} + headers: + - name: x-imex-auth + value_from_env: DATAPUMP_AUTH +- name: Claimscorp Data Pump + webhook: '{{HASURA_API_URL}}/data/cc' + schedule: 30 6 * * * + include_in_metadata: true + payload: {} + headers: + - name: x-imex-auth + value_from_env: DATAPUMP_AUTH +- name: Kaizen Data Pump + webhook: '{{HASURA_API_URL}}/data/kaizen' + schedule: 30 5 * * * + include_in_metadata: true + payload: {} + headers: + - name: x-imex-auth + value_from_env: DATAPUMP_AUTH - name: Task Reminders webhook: '{{HASURA_API_URL}}/tasks-remind-handler' schedule: '*/15 * * * *' diff --git a/server/data/autohouse.js b/server/data/autohouse.js index 6b5f30af9..fa52679df 100644 --- a/server/data/autohouse.js +++ b/server/data/autohouse.js @@ -31,6 +31,12 @@ const ftpSetup = { }; exports.default = async (req, res) => { + // Only process if in production environment. + if (process.env.NODE_ENV !== "production") { + res.sendStatus(403); + return; + } + //Query for the List of Bodyshop Clients. logger.log("autohouse-start", "DEBUG", "api", null, null); const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS); diff --git a/server/data/claimscorp.js b/server/data/claimscorp.js index fb3012b7a..6ebc63f81 100644 --- a/server/data/claimscorp.js +++ b/server/data/claimscorp.js @@ -31,6 +31,12 @@ const ftpSetup = { }; exports.default = async (req, res) => { + // Only process if in production environment. + if (process.env.NODE_ENV !== "production") { + res.sendStatus(403); + return; + } + //Query for the List of Bodyshop Clients. logger.log("claimscorp-start", "DEBUG", "api", null, null); const { bodyshops } = await client.request(queries.GET_CLAIMSCORP_SHOPS); diff --git a/server/data/kaizen.js b/server/data/kaizen.js index 1dccef61c..c9794acff 100644 --- a/server/data/kaizen.js +++ b/server/data/kaizen.js @@ -31,6 +31,12 @@ const ftpSetup = { }; exports.default = async (req, res) => { + // Only process if in production environment. + if (process.env.NODE_ENV !== "production") { + res.sendStatus(403); + return; + } + //Query for the List of Bodyshop Clients. logger.log("kaizen-start", "DEBUG", "api", null, null); const kaizenShopsIDs = ["SUMMIT", "STRATHMORE", "SUNRIDGE", "SHAW"]; @@ -56,8 +62,8 @@ exports.default = async (req, res) => { try { const { jobs, bodyshops_by_pk } = await client.request(queries.KAIZEN_QUERY, { bodyshopid: bodyshop.id, - start: start ? moment(start).startOf("hours") : moment().subtract(2, "hours").startOf("hour"), - ...(end && { end: moment(end).endOf("hours") }) + start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"), + ...(end && { end: moment(end).endOf("day") }) }); const kaizenObject = { @@ -176,24 +182,19 @@ exports.default = async (req, res) => { } finally { sftp.end(); } - // sendServerEmail({ - // subject: `Kaizen Report ${moment().format("MM-DD-YY")}`, - // text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} - // Uploaded: ${JSON.stringify( - // allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), - // null, - // 2 - // )} - // `, - // }); + sendServerEmail({ + subject: `Kaizen Report ${moment().format("MM-DD-YY")}`, + text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} + Uploaded: ${JSON.stringify( + allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), + null, + 2 + )} + ` + }); res.sendStatus(200); } catch (error) { res.status(200).json(error); - sendServerEmail({ - subject: `Kaizen Report ${moment().format("MM-DD-YY @ HH:mm:ss")}`, - text: `Errors: JSON.stringify(error)} - All Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}` - }); } }; diff --git a/server/job/job-totals-USA.js b/server/job/job-totals-USA.js index 3dabed316..ca9a74bca 100644 --- a/server/job/job-totals-USA.js +++ b/server/job/job-totals-USA.js @@ -965,22 +965,17 @@ function CalculateTaxesTotals(job, otherTotals) { } }); - if (job.adjustment_bottom_line) { - const subtotal_before_adjustment = subtotal.add(Dinero({ amount: Math.round(job.adjustment_bottom_line * -100) })); - const percent_of_adjustment = - Math.round( - subtotal_before_adjustment.toUnit() / - (job.adjustment_bottom_line > 0 ? job.adjustment_bottom_line : job.adjustment_bottom_line * -1) - ) / 100; - - Object.keys(taxableAmountsByTier).forEach((taxTierKey) => { - taxable_adjustment = taxableAmountsByTier[taxTierKey].multiply(percent_of_adjustment); - if (job.adjustment_bottom_line > 0) { - taxableAmountsByTier[taxTierKey] = taxableAmountsByTier[taxTierKey].add(taxable_adjustment); - } else { - taxableAmountsByTier[taxTierKey] = taxableAmountsByTier[taxTierKey].subtract(taxable_adjustment); + if (job.adjustment_bottom_line && job.adjustment_bottom_line !== 0) { + for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { + if (IsTrueOrYes(pfp["PAN"][`prt_tx_in${tyCounter}`])) { + //This amount is taxable for this type. + taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[`ty${tyCounter}Tax`].add( + Dinero({ + amount: Math.round(job.adjustment_bottom_line * 100) + }) + ); } - }); + } } const remainingTaxableAmounts = taxableAmountsByTier;