diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index c18ffea89..2c77315d4 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -2863,6 +2863,27 @@ + + markexported + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + markforreexport false @@ -3120,6 +3141,27 @@ + + markexported + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + reexport false @@ -7794,6 +7836,27 @@ + + timezone + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + tt_allow_post_to_invoiced false @@ -20329,6 +20392,27 @@ + + comment + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + customerowing false @@ -36104,6 +36188,27 @@ + + comment + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + compact false @@ -38903,6 +39008,27 @@ + + dailyactual + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + dailytarget false @@ -38966,6 +39092,48 @@ + + todateactual + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + weeklyactual + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + weeklytarget false diff --git a/client/package.json b/client/package.json index 1153baef0..8aa8c9d56 100644 --- a/client/package.json +++ b/client/package.json @@ -33,6 +33,7 @@ "logrocket": "^2.1.2", "markerjs2": "^2.17.2", "moment-business-days": "^1.2.0", + "moment-timezone": "^0.5.34", "phone": "^3.1.10", "preval.macro": "^5.0.0", "prop-types": "^15.7.2", diff --git a/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx b/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx index 4a854497e..b8ac4e8cb 100644 --- a/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx +++ b/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx @@ -28,6 +28,7 @@ import { createStructuredSelector } from "reselect"; import { setModalContext } from "../../redux/modals/modals.actions"; import { insertAuditTrail } from "../../redux/application/application.actions"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; +import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser @@ -234,6 +235,7 @@ export function BillDetailEditcontainer({ + } /> diff --git a/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx b/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx new file mode 100644 index 000000000..d81244098 --- /dev/null +++ b/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx @@ -0,0 +1,82 @@ +import { useMutation } from "@apollo/client"; +import { Button, notification } from "antd"; +import { gql } from "@apollo/client"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { + selectAuthLevel, + selectBodyshop, +} from "../../redux/user/user.selectors"; +import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component"; +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + authLevel: selectAuthLevel, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(BillMarkExportedButton); + +export function BillMarkExportedButton({ bodyshop, authLevel, bill }) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + + const [updateBill] = useMutation(gql` + mutation UPDATE_BILL($billId: uuid!) { + update_bills(where: { id: { _eq: $billId } }, _set: { exported: true }) { + returning { + id + exported + exported_at + } + } + } + `); + + const handleUpdate = async () => { + setLoading(true); + const result = await updateBill({ + variables: { billId: bill.id }, + }); + + if (!result.errors) { + notification["success"]({ + message: t("bills.successes.markexported"), + }); + } else { + notification["error"]({ + message: t("bills.errors.saving", { + error: JSON.stringify(result.errors), + }), + }); + } + setLoading(false); + //Get the owner details, populate it all back into the job. + }; + + const hasAccess = HasRbacAccess({ + bodyshop, + authLevel, + action: "bills:reexport", + }); + + if (hasAccess) + return ( + + ); + + return <>; +} diff --git a/client/src/components/form-date-picker/form-date-picker.component.jsx b/client/src/components/form-date-picker/form-date-picker.component.jsx index ba97b051e..e8c3eb7d1 100644 --- a/client/src/components/form-date-picker/form-date-picker.component.jsx +++ b/client/src/components/form-date-picker/form-date-picker.component.jsx @@ -1,11 +1,24 @@ import { DatePicker } from "antd"; -import moment from "moment"; +import moment from "moment-timezone"; import React, { useRef } from "react"; //To be used as a form element only. +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect(mapStateToProps, mapDispatchToProps)(FormDatePicker); + const dateFormat = "MM/DD/YYYY"; -export default function FormDatePicker({ +export function FormDatePicker({ + bodyshop, value, onChange, onBlur, @@ -23,7 +36,7 @@ export default function FormDatePicker({ const handleKeyDown = (e) => { if (e.key.toLowerCase() === "t") { if (onChange) { - onChange(new moment()); + onChange(moment()); // if (ref.current && ref.current.blur) ref.current.blur(); } } else if (e.key.toLowerCase() === "enter") { diff --git a/client/src/components/job-bills-total/job-bills-total.component.jsx b/client/src/components/job-bills-total/job-bills-total.component.jsx index 8f8ba31e6..011a9b80f 100644 --- a/client/src/components/job-bills-total/job-bills-total.component.jsx +++ b/client/src/components/job-bills-total/job-bills-total.component.jsx @@ -72,7 +72,7 @@ export default function JobBillsTotalComponent({ const discrepWithLbrAdj = discrepancy.add(lbrAdjustments); - const discrepWithCms = discrepWithLbrAdj.add(billCms); + const discrepWithCms = discrepWithLbrAdj.add(totalReturns); const creditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number. return ( @@ -171,8 +171,8 @@ export default function JobBillsTotalComponent({ } > = diff --git a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx index 86fbde089..59ca3821c 100644 --- a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx +++ b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx @@ -21,6 +21,7 @@ import ProductionListColumnProductionNote from "../production-list-columns/produ import "./jobs-detail-header.styles.scss"; import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component"; import { DateTimeFormatter } from "../../utils/DateFormatter"; +import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component"; const mapStateToProps = createStructuredSelector({ jobRO: selectJobReadOnly, @@ -86,6 +87,12 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { ) : null} + + + {job.ins_co_nm} diff --git a/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx b/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx index a751be3e2..2a6ea300b 100644 --- a/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx +++ b/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx @@ -30,7 +30,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { title: t("jobs.fields.ro_number"), dataIndex: "ro_number", key: "ro_number", - width: "8%", + sorter: true, //(a, b) => alphaSort(a.ro_number, b.ro_number), sortOrder: sortcolumn === "ro_number" && sortorder, @@ -47,7 +47,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { key: "ownr_ln", ellipsis: true, //sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), - width: "25%", + //sortOrder: sortcolumn === "ownr_ln" && sortorder, render: (text, record) => { return record.ownerid ? ( @@ -67,7 +67,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { title: t("jobs.fields.ownr_ph1"), dataIndex: "ownr_ph1", key: "ownr_ph1", - width: "12%", + ellipsis: true, render: (text, record) => ( @@ -77,7 +77,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { title: t("jobs.fields.ownr_ph2"), dataIndex: "ownr_ph2", key: "ownr_ph2", - width: "12%", + ellipsis: true, render: (text, record) => ( @@ -87,7 +87,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { title: t("jobs.fields.status"), dataIndex: "status", key: "status", - width: "10%", + ellipsis: true, sorter: true, // (a, b) => alphaSort(a.status, b.status), sortOrder: sortcolumn === "status" && sortorder, @@ -104,7 +104,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { title: t("jobs.fields.vehicle"), dataIndex: "vehicle", key: "vehicle", - width: "15%", + ellipsis: true, render: (text, record) => { return record.vehicleid ? ( @@ -124,7 +124,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { title: t("vehicles.fields.plate_no"), dataIndex: "plate_no", key: "plate_no", - width: "8%", + ellipsis: true, sorter: true, //(a, b) => alphaSort(a.plate_no, b.plate_no), sortOrder: sortcolumn === "plate_no" && sortorder, @@ -136,7 +136,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { title: t("jobs.fields.clm_no"), dataIndex: "clm_no", key: "clm_no", - width: "12%", + ellipsis: true, sorter: true, //(a, b) => alphaSort(a.clm_no, b.clm_no), sortOrder: sortcolumn === "clm_no" && sortorder, @@ -155,7 +155,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { title: t("jobs.fields.clm_total"), dataIndex: "clm_total", key: "clm_total", - width: "10%", + sorter: true, //(a, b) => a.clm_total - b.clm_total, sortOrder: sortcolumn === "clm_total" && sortorder, render: (text, record) => { @@ -170,11 +170,17 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { title: t("jobs.fields.owner_owing"), dataIndex: "owner_owing", key: "owner_owing", - width: "8%", + render: (text, record) => ( {record.owner_owing} ), }, + { + title: t("jobs.fields.comment"), + dataIndex: "comment", + key: "comment", + ellipsis: true, + }, ]; const handleTableChange = (pagination, filters, sorter) => { @@ -224,7 +230,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { > {record.clm_total} ), }, + { + title: t("jobs.fields.comment"), + dataIndex: "comment", + key: "comment", + ellipsis: true, + responsive: ["md"], + }, // { // title: t("jobs.fields.owner_owing"), // dataIndex: "owner_owing", diff --git a/client/src/components/production-list-columns/production-list-columns.comment.component.jsx b/client/src/components/production-list-columns/production-list-columns.comment.component.jsx new file mode 100644 index 000000000..fafccff93 --- /dev/null +++ b/client/src/components/production-list-columns/production-list-columns.comment.component.jsx @@ -0,0 +1,79 @@ +import Icon from "@ant-design/icons"; +import { useMutation } from "@apollo/client"; +import { Button, Input, Popover } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { FaRegStickyNote } from "react-icons/fa"; +import { UPDATE_JOB } from "../../graphql/jobs.queries"; +export default function ProductionListColumnComment({ record }) { + const { t } = useTranslation(); + + const [note, setNote] = useState(record.comment || ""); + + const [visible, setVisible] = useState(false); + + const [updateAlert] = useMutation(UPDATE_JOB); + + const handleSaveNote = (e) => { + e.stopPropagation(); + setVisible(false); + updateAlert({ + variables: { + jobId: record.id, + job: { + comment: note, + }, + }, + }).then(() => { + if (record.refetch) record.refetch(); + }); + }; + + const handleChange = (e) => { + e.stopPropagation(); + setNote(e.target.value); + }; + + const handleVisibleChange = (flag) => { + setVisible(flag); + if (flag) setNote(record.comment || ""); + }; + + return ( + + +
+ +
+ + } + trigger={["click"]} + > +
+ + {record.comment || " "} +
+
+ ); +} diff --git a/client/src/components/production-list-columns/production-list-columns.data.js b/client/src/components/production-list-columns/production-list-columns.data.js index d8d547665..240d8074b 100644 --- a/client/src/components/production-list-columns/production-list-columns.data.js +++ b/client/src/components/production-list-columns/production-list-columns.data.js @@ -20,6 +20,7 @@ import ProductionListColumnNote from "./production-list-columns.productionnote.c import ProductionListColumnStatus from "./production-list-columns.status.component"; import ProductionListColumnCategory from "./production-list-columns.status.category"; import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.component"; +import ProductionListColumnComment from "./production-list-columns.comment.component"; const r = ({ technician, state, activeStatuses, bodyshop }) => { return [ @@ -109,7 +110,11 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => { state.sortedInfo.columnKey === "scheduled_completion" && state.sortedInfo.order, render: (text, record) => ( - + ), }, { @@ -156,7 +161,11 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => { state.sortedInfo.columnKey === "scheduled_delivery" && state.sortedInfo.order, render: (text, record) => ( - + ), }, { @@ -325,6 +334,13 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => { ellipsis: true, render: (text, record) => , }, + { + title: i18n.t("production.labels.comment"), + dataIndex: "comment", + key: "comment", + ellipsis: true, + render: (text, record) => , + }, { title: i18n.t("production.labels.touchtime"), dataIndex: "tt", diff --git a/client/src/components/schedule-calendar/schedule-calendar.container.jsx b/client/src/components/schedule-calendar/schedule-calendar.container.jsx index aff134b85..27e05258a 100644 --- a/client/src/components/schedule-calendar/schedule-calendar.container.jsx +++ b/client/src/components/schedule-calendar/schedule-calendar.container.jsx @@ -65,6 +65,7 @@ export function ScheduleCalendarContainer({ calculateScheduleLoad }) { color: "red", start: moment(e.start).startOf("day").toDate(), end: moment(e.end).startOf("day").toDate(), + allDay: true, vacation: true, }; }), diff --git a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx index 9b96febaf..bffc9cfcb 100644 --- a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx +++ b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx @@ -1,12 +1,14 @@ import { CalendarOutlined } from "@ant-design/icons"; import { Card, Col, Row, Statistic } from "antd"; -import React from "react"; +import React, { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; import ScoreboardJobsList from "../scoreboard-jobs-list/scoreboard-jobs-list.component"; import * as Util from "./scoreboard-targets-table.util"; +import _ from "lodash"; +import moment from "moment"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -16,25 +18,78 @@ const mapDispatchToProps = (dispatch) => ({ }); const rowGutter = [16, 16]; -const statSpans = { xs: 24, sm: 6 }; +const statSpans = { xs: 24, sm: 3 }; export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { const { t } = useTranslation(); + const values = useMemo(() => { + const dateHash = _.groupBy(scoreBoardlist, "date"); + console.log( + "🚀 ~ file: scoreboard-targets-table.component.jsx ~ line 31 ~ values ~ dateHash", + dateHash + ); + + let ret = { + todayBody: 0, + todayPaint: 0, + weeklyPaint: 0, + weeklyBody: 0, + toDateBody: 0, + toDatePaint: 0, + }; + + const today = moment().tz(bodyshop.timezone); + if (dateHash[today.format("YYYY-MM-DD")]) { + dateHash[today.format("YYYY-MM-DD")].forEach((d) => { + ret.todayBody = ret.todayBody + d.bodyhrs; + ret.todayPaint = ret.todayPaint + d.painthrs; + }); + } + + let StartOfWeek = moment().tz(bodyshop.timezone).startOf("week"); + while (StartOfWeek.isSameOrBefore(today)) { + if (dateHash[StartOfWeek.format("YYYY-MM-DD")]) { + dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => { + ret.weeklyBody = ret.weeklyBody + d.bodyhrs; + ret.weeklyPaint = ret.weeklyPaint + d.painthrs; + }); + } + StartOfWeek = StartOfWeek.add(1, "day"); + } + + let startOfMonth = moment().tz(bodyshop.timezone).startOf("month"); + while (startOfMonth.isSameOrBefore(today)) { + if (dateHash[startOfMonth.format("YYYY-MM-DD")]) { + dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => { + ret.toDateBody = ret.toDateBody + d.bodyhrs; + ret.toDatePaint = ret.toDatePaint + d.painthrs; + }); + } + startOfMonth = startOfMonth.add(1, "day"); + } + + return ret; + }, [scoreBoardlist, bodyshop.timezone]); + console.log( + "🚀 ~ file: scoreboard-targets-table.component.jsx ~ line 51 ~ values ~ values", + values + ); + return ( } > - + } /> - + + + + + + + + + + @@ -78,6 +151,9 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { prefix="P" /> + + + + + + + + + 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 2a167fdb2..603ef790f 100644 --- a/client/src/components/shop-info/shop-info.general.component.jsx +++ b/client/src/components/shop-info/shop-info.general.component.jsx @@ -19,6 +19,9 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component"; +import momentTZ from "moment-timezone"; +const timeZonesList = momentTZ.tz.names(); + export default function ShopInfoGeneral({ form }) { const { t } = useTranslation(); return ( @@ -84,6 +87,7 @@ export default function ShopInfoGeneral({ form }) { + + +