diff --git a/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx b/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx index 0da329d6f..ac97c8e81 100644 --- a/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx +++ b/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx @@ -1,5 +1,5 @@ import { Select } from "antd"; -import React, { forwardRef } from "react"; +import { forwardRef } from "react"; import { useTranslation } from "react-i18next"; import InstanceRenderMgr from "../../utils/instanceRenderMgr"; @@ -43,7 +43,7 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, item.oem_partno ? ` - ${item.oem_partno}` : "" }${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(), label: ( -
+
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${ item.oem_partno ? ` - ${item.oem_partno}` : "" diff --git a/client/src/components/contract-status-select/contract-status-select.component.jsx b/client/src/components/contract-status-select/contract-status-select.component.jsx index 8bd048195..3a4ee82ee 100644 --- a/client/src/components/contract-status-select/contract-status-select.component.jsx +++ b/client/src/components/contract-status-select/contract-status-select.component.jsx @@ -1,10 +1,10 @@ -import React, { forwardRef, useEffect, useState } from "react"; +import { forwardRef, useEffect, useState } from "react"; import { Select } from "antd"; import { useTranslation } from "react-i18next"; const { Option } = Select; -const ContractStatusComponent = ({ value, onChange }, ref) => { +const ContractStatusComponent = ({ value, onChange }) => { const [option, setOption] = useState(value); const { t } = useTranslation(); diff --git a/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx b/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx index ffe1c1f97..a189e0660 100644 --- a/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx +++ b/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx @@ -1,5 +1,5 @@ import { Slider } from "antd"; -import React, { forwardRef } from "react"; +import { forwardRef } from "react"; import { useTranslation } from "react-i18next"; const CourtesyCarFuelComponent = (props, ref) => { diff --git a/client/src/components/header/buildAccountingChildren.jsx b/client/src/components/header/buildAccountingChildren.jsx new file mode 100644 index 000000000..574dbf603 --- /dev/null +++ b/client/src/components/header/buildAccountingChildren.jsx @@ -0,0 +1,190 @@ +import { Link } from "react-router-dom"; +import { FaCreditCard, FaFileInvoiceDollar } from "react-icons/fa"; +import { GiPayMoney, GiPlayerTime } from "react-icons/gi"; +import { BankFilled, ExportOutlined, FieldTimeOutlined } from "@ant-design/icons"; +import LockWrapper from "../../components/lock-wrapper/lock-wrapper.component.jsx"; +import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component"; + +// --- Menu Item Builders --- +const buildAccountingChildren = ({ + t, + bodyshop, + currentUser, + setBillEnterContext, + setPaymentContext, + setCardPaymentContext, + setTimeTicketContext, + ImEXPay, + DmsAp, + Simple_Inventory +}) => [ + { + key: "bills", + id: "header-accounting-bills", + icon: , + label: ( + + + {t("menus.header.bills")} + + + ) + }, + { + key: "enterbills", + id: "header-accounting-enterbills", + icon: , + label: ( + + {t("menus.header.enterbills")} + + ), + onClick: () => + HasFeatureAccess({ featureName: "bills", bodyshop }) && setBillEnterContext({ actions: {}, context: {} }) + }, + ...(Simple_Inventory.treatment === "on" + ? [ + { type: "divider" }, + { + key: "inventory", + id: "header-accounting-inventory", + icon: , + label: {t("menus.header.inventory")} + } + ] + : []), + { type: "divider" }, + { + key: "allpayments", + id: "header-accounting-allpayments", + icon: , + label: {t("menus.header.allpayments")} + }, + { + key: "enterpayments", + id: "header-accounting-enterpayments", + icon: , + label: t("menus.header.enterpayment"), + onClick: () => setPaymentContext({ actions: {}, context: null }) + }, + ...(ImEXPay.treatment === "on" + ? [ + { + key: "entercardpayments", + id: "header-accounting-entercardpayments", + icon: , + label: t("menus.header.entercardpayment"), + onClick: () => setCardPaymentContext({ actions: {}, context: {} }) + } + ] + : []), + { type: "divider" }, + { + key: "timetickets", + id: "header-accounting-timetickets", + icon: , + label: ( + + + {t("menus.header.timetickets")} + + + ) + }, + ...(bodyshop?.md_tasks_presets?.use_approvals + ? [ + { + key: "ttapprovals", + id: "header-accounting-ttapprovals", + icon: , + label: {t("menus.header.ttapprovals")} + } + ] + : []), + { + key: "entertimetickets", + id: "header-accounting-entertimetickets", + icon: , + label: ( + + {t("menus.header.entertimeticket")} + + ), + onClick: () => + HasFeatureAccess({ featureName: "timetickets", bodyshop }) && + setTimeTicketContext({ + actions: {}, + context: { + created_by: currentUser.displayName ? `${currentUser.email} | ${currentUser.displayName}` : currentUser.email + } + }) + }, + { type: "divider" }, + { + key: "accountingexport", + id: "header-accounting-export", + icon: , + label: ( + + {t("menus.header.export")} + + ), + children: [ + { + key: "receivables", + id: "header-accounting-receivables", + label: ( + + + {t("menus.header.accounting-receivables")} + + + ) + }, + ...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || DmsAp.treatment === "on" + ? [ + { + key: "payables", + id: "header-accounting-payables", + label: ( + + + {t("menus.header.accounting-payables")} + + + ) + } + ] + : []), + ...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) + ? [ + { + key: "payments", + id: "header-accounting-payments", + label: ( + + + {t("menus.header.accounting-payments")} + + + ) + } + ] + : []), + { type: "divider" }, + { + key: "exportlogs", + id: "header-accounting-exportlogs", + label: ( + + + {t("menus.header.export-logs")} + + + ) + } + ] + } +]; + +export default buildAccountingChildren; diff --git a/client/src/components/header/buildLeftMenuItems.jsx b/client/src/components/header/buildLeftMenuItems.jsx new file mode 100644 index 000000000..166779572 --- /dev/null +++ b/client/src/components/header/buildLeftMenuItems.jsx @@ -0,0 +1,390 @@ +import { Link } from "react-router-dom"; +import { + BarChartOutlined, + CarFilled, + CheckCircleOutlined, + ClockCircleFilled, + DashboardFilled, + DollarCircleFilled, + FileAddFilled, + FileAddOutlined, + FileFilled, + HomeFilled, + ImportOutlined, + LineChartOutlined, + OneToOneOutlined, + PaperClipOutlined, + PhoneOutlined, + PlusCircleOutlined, + QuestionCircleFilled, + ScheduleOutlined, + SettingOutlined, + TeamOutlined, + ToolFilled, + UnorderedListOutlined, + UsergroupAddOutlined, + UserOutlined +} from "@ant-design/icons"; +import { FaCalendarAlt, FaCarCrash, FaTasks } from "react-icons/fa"; +import { BsKanban } from "react-icons/bs"; +import { FiLogOut } from "react-icons/fi"; +import { GiPlayerTime, GiSettingsKnobs } from "react-icons/gi"; +import { RiSurveyLine } from "react-icons/ri"; +import { IoBusinessOutline } from "react-icons/io5"; +import InstanceRenderManager from "../../utils/instanceRenderMgr"; +import LockWrapper from "../../components/lock-wrapper/lock-wrapper.component.jsx"; + +const buildLeftMenuItems = ({ + t, + bodyshop, + recentItems, + setTaskUpsertContext, + setReportCenterContext, + signOutStart, + accountingChildren +}) => { + return [ + { + key: "home", + id: "header-home", + icon: , + label: {t("menus.header.home")} + }, + { + key: "schedule", + id: "header-schedule", + icon: , + label: {t("menus.header.schedule")} + }, + { + key: "jobssubmenu", + id: "header-jobs", + icon: , + label: t("menus.header.jobs"), + children: [ + { + key: "activejobs", + id: "header-active-jobs", + icon: , + label: {t("menus.header.activejobs")} + }, + { + key: "readyjobs", + id: "header-ready-jobs", + icon: , + label: {t("menus.header.readyjobs")} + }, + { + key: "parts-queue", + id: "header-parts-queue", + icon: , + label: {t("menus.header.parts-queue")} + }, + { + key: "availablejobs", + id: "header-jobs-available", + icon: , + label: {t("menus.header.availablejobs")} + }, + { + key: "newjob", + id: "header-new-job", + icon: , + label: {t("menus.header.newjob")} + }, + { type: "divider" }, + { + key: "alljobs", + id: "header-all-jobs", + icon: , + label: {t("menus.header.alljobs")} + }, + { type: "divider" }, + { + key: "productionlist", + id: "header-production-list", + icon: , + label: {t("menus.header.productionlist")} + }, + { + key: "productionboard", + id: "header-production-board", + icon: , + label: ( + + + {t("menus.header.productionboard")} + + + ) + }, + { type: "divider" }, + { + key: "scoreboard", + id: "header-scoreboard", + icon: , + label: ( + + + {t("menus.header.scoreboard")} + + + ) + } + ] + }, + { + key: "customers", + id: "header-customers", + icon: , + label: t("menus.header.customers"), + children: [ + { + key: "owners", + id: "header-owners", + icon: , + label: {t("menus.header.owners")} + }, + { + key: "vehicles", + id: "header-vehicles", + icon: , + label: {t("menus.header.vehicles")} + } + ] + }, + { + key: "ccs", + id: "header-css", + icon: , + label: ( + + {t("menus.header.courtesycars")} + + ), + children: [ + { + key: "courtesycarsall", + id: "header-courtesycars-all", + icon: , + label: ( + + + {t("menus.header.courtesycars-all")} + + + ) + }, + { + key: "contracts", + id: "header-contracts", + icon: , + label: ( + + + {t("menus.header.courtesycars-contracts")} + + + ) + }, + { + key: "newcontract", + id: "header-newcontract", + icon: , + label: ( + + + {t("menus.header.courtesycars-newcontract")} + + + ) + } + ] + }, + ...(accountingChildren.length > 0 + ? [ + { + key: "accounting", + id: "header-accounting", + icon: , + label: t("menus.header.accounting"), + children: accountingChildren + } + ] + : []), + { + key: "phonebook", + id: "header-phonebook", + icon: , + label: {t("menus.header.phonebook")} + }, + { + key: "temporarydocs", + id: "header-temporarydocs", + icon: , + label: ( + + + {t("menus.header.temporarydocs")} + + + ) + }, + { + key: "tasks", + id: "tasks", + icon: , + label: t("menus.header.tasks"), + children: [ + { + key: "createTask", + id: "header-create-task", + icon: , + label: t("menus.header.create_task"), + onClick: () => setTaskUpsertContext({ actions: {}, context: {} }) + }, + { + key: "mytasks", + id: "header-my-tasks", + icon: , + label: {t("menus.header.my_tasks")} + }, + { + key: "all_tasks", + id: "header-all-tasks", + icon: , + label: {t("menus.header.all_tasks")} + } + ] + }, + { + key: "shopsubmenu", + id: "header-shopsubmenu", + icon: , + label: t("menus.header.shop"), + children: [ + { + key: "shop", + id: "header-shop", + icon: , + label: {t("menus.header.shop_config")} + }, + { + key: "dashboard", + id: "header-dashboard", + icon: , + label: ( + + {t("menus.header.dashboard")} + + ) + }, + { + key: "reportcenter", + id: "header-reportcenter", + icon: , + label: t("menus.header.reportcenter"), + onClick: () => setReportCenterContext({ actions: {}, context: {} }) + }, + { + key: "shop-vendors", + id: "header-shop-vendors", + icon: , + label: {t("menus.header.shop_vendors")} + }, + { + key: "shop-csi", + id: "header-shop-csi", + icon: , + label: ( + + + {t("menus.header.shop_csi")} + + + ) + } + ] + }, + { + key: "recent", + id: "header-recent", + icon: , + label: t("menus.header.recent"), + children: recentItems.map((i, idx) => ({ + key: idx, + id: `header-recent-${idx}`, + label: {i.label} + })) + }, + { + key: "user", + id: "header-user", + icon: , + label: t("menus.currentuser.profile"), + children: [ + { + key: "signout", + id: "header-signout", + icon: , + danger: true, + label: t("user.actions.signout"), + onClick: () => signOutStart() + }, + { + key: "help", + id: "header-help", + icon: , + label: t("menus.header.help"), + onClick: () => window.open("https://help.imex.online/", "_blank") + }, + { + key: "remoteassist", + id: "header-remote-assist", + icon: , + label: t("menus.header.remoteassist"), + children: [ + ...(InstanceRenderManager({ imex: true, rome: false }) + ? [ + { + key: "rescue", + id: "header-rescue", + icon: , + label: t("menus.header.rescueme"), + onClick: () => window.open("https://imexrescue.com/", "_blank") + } + ] + : []), + { + key: "rescue-zoho", + id: "header-rescue-zoho", + icon: , + label: t("menus.header.rescuemezoho"), + onClick: () => window.open("https://join.zoho.com/", "_blank") + } + ] + }, + { + key: "shiftclock", + id: "header-shiftclock", + icon: , + label: ( + + + {t("menus.header.shiftclock")} + + + ) + }, + { + key: "profile", + id: "header-profile", + icon: , + label: {t("menus.currentuser.profile")} + } + ] + } + ]; +}; + +export default buildLeftMenuItems; diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 826edea54..3312bf9f4 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -1,61 +1,29 @@ -import { - BankFilled, - BarChartOutlined, - BellFilled, - CarFilled, - CheckCircleOutlined, - ClockCircleFilled, - DashboardFilled, - DollarCircleFilled, - ExportOutlined, - FieldTimeOutlined, - FileAddFilled, - FileAddOutlined, - FileFilled, - HomeFilled, - ImportOutlined, - LineChartOutlined, - OneToOneOutlined, - PaperClipOutlined, - PhoneOutlined, - PlusCircleOutlined, - QuestionCircleFilled, - ScheduleOutlined, - SettingOutlined, - TeamOutlined, - ToolFilled, - UnorderedListOutlined, - UsergroupAddOutlined, - UserOutlined -} from "@ant-design/icons"; +// noinspection RegExpAnonymousGroup + +import { BellFilled } from "@ant-design/icons"; import { useQuery } from "@apollo/client"; import { useSplitTreatments } from "@splitsoftware/splitio-react"; -import { Badge, Layout, Menu, Spin } from "antd"; -import { useEffect, useRef, useState } from "react"; +import { Badge, Layout, Menu, Spin, Tooltip } from "antd"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { BsKanban } from "react-icons/bs"; -import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa"; -import { FiLogOut } from "react-icons/fi"; -import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi"; -import { IoBusinessOutline } from "react-icons/io5"; -import { RiSurveyLine } from "react-icons/ri"; +import { FaTasks } from "react-icons/fa"; import { connect } from "react-redux"; -import { Link } from "react-router-dom"; import { createStructuredSelector } from "reselect"; -import { useSocket } from "../../contexts/SocketIO/useSocket.js"; +import { TASKS_CENTER_POLL_INTERVAL, useSocket } from "../../contexts/SocketIO/useSocket.js"; import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js"; +import { QUERY_MY_TASKS_COUNT } from "../../graphql/tasks.queries.js"; import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors"; import { setModalContext } from "../../redux/modals/modals.actions"; import { signOutStart } from "../../redux/user/user.actions"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import day from "../../utils/day.js"; -import InstanceRenderManager from "../../utils/instanceRenderMgr"; import { useIsEmployee } from "../../utils/useIsEmployee.js"; -import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; -import LockWrapper from "../lock-wrapper/lock-wrapper.component"; import NotificationCenterContainer from "../notification-center/notification-center.container.jsx"; +import TaskCenterContainer from "../task-center/task-center.container.jsx"; +import buildAccountingChildren from "./buildAccountingChildren.jsx"; +import buildLeftMenuItems from "./buildLeftMenuItems.jsx"; -// Redux mappings +// --- Redux mappings --- const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, recentItems: selectRecentItems, @@ -73,36 +41,8 @@ const mapDispatchToProps = (dispatch) => ({ setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" })) }); -function Header({ - handleMenuClick, - currentUser, - bodyshop, - selectedHeader, - signOutStart, - setBillEnterContext, - setTimeTicketContext, - setPaymentContext, - setReportCenterContext, - recentItems, - setCardPaymentContext, - setTaskUpsertContext -}) { - const { - treatments: { ImEXPay, DmsAp, Simple_Inventory } - } = useSplitTreatments({ - attributes: {}, - names: ["ImEXPay", "DmsAp", "Simple_Inventory"], - splitKey: bodyshop && bodyshop.imexshopid - }); - - const { t } = useTranslation(); - const { isConnected, scenarioNotificationsOn } = useSocket(); - const [notificationVisible, setNotificationVisible] = useState(false); - const baseTitleRef = useRef(document.title || ""); - const lastSetTitleRef = useRef(""); - const userAssociationId = bodyshop?.associations?.[0]?.id; - const isEmployee = useIsEmployee(bodyshop, currentUser); - +// --- Utility Hooks --- +function useUnreadNotifications(userAssociationId, isConnected, scenarioNotificationsOn) { const { data: unreadData, refetch: refetchUnread, @@ -128,633 +68,286 @@ function Header({ } }, [isConnected, unreadLoading, refetchUnread, userAssociationId]); - // Keep The unread count in the title. + return { unreadCount, unreadLoading }; +} + +function useIncompleteTaskCount(assignedToId, bodyshopId, isEmployee, isConnected) { + const { data: taskCountData, loading: taskCountLoading } = useQuery(QUERY_MY_TASKS_COUNT, { + variables: { assigned_to: assignedToId, bodyshopid: bodyshopId }, + skip: !assignedToId || !bodyshopId || !isEmployee, + fetchPolicy: "network-only", + pollInterval: isConnected ? 0 : TASKS_CENTER_POLL_INTERVAL + }); + + const incompleteTaskCount = taskCountData?.tasks_aggregate?.aggregate?.count ?? 0; + return { incompleteTaskCount, taskCountLoading }; +} + +// --- Main Component --- +function Header(props) { + const { + handleMenuClick, + currentUser, + bodyshop, + selectedHeader, + signOutStart, + setBillEnterContext, + setTimeTicketContext, + setPaymentContext, + setReportCenterContext, + recentItems, + setCardPaymentContext, + setTaskUpsertContext + } = props; + + // Feature flags + const { + treatments: { ImEXPay, DmsAp, Simple_Inventory } + } = useSplitTreatments({ + attributes: {}, + names: ["ImEXPay", "DmsAp", "Simple_Inventory"], + splitKey: bodyshop && bodyshop.imexshopid + }); + + // Contexts and hooks + const { t } = useTranslation(); + const { isConnected, scenarioNotificationsOn } = useSocket(); + const [notificationVisible, setNotificationVisible] = useState(false); + const [taskCenterVisible, setTaskCenterVisible] = useState(false); + const baseTitleRef = useRef(document.title || ""); + const lastSetTitleRef = useRef(""); + const taskCenterRef = useRef(null); + const notificationRef = useRef(null); + const userAssociationId = bodyshop?.associations?.[0]?.id; + const isEmployee = useIsEmployee(bodyshop, currentUser); + + // Data hooks + const { unreadCount, unreadLoading } = useUnreadNotifications( + userAssociationId, + isConnected, + scenarioNotificationsOn + ); + const assignedToId = bodyshop?.employees?.find((e) => e.user_email === currentUser.email)?.id; + const { incompleteTaskCount, taskCountLoading } = useIncompleteTaskCount( + assignedToId, + bodyshop?.id, + isEmployee, + isConnected + ); + + // --- Effects --- + + // Update document title with unread count useEffect(() => { const updateTitle = () => { const currentTitle = document.title; - // Check if the current title differs from what we last set if (currentTitle !== lastSetTitleRef.current) { - // Extract base title by removing any unread count prefix const baseTitleMatch = currentTitle.match(/^\(\d+\)\s*(.*)$/); baseTitleRef.current = baseTitleMatch ? baseTitleMatch[1] : currentTitle; } - - // Apply unread count to the base title const newTitle = unreadCount > 0 ? `(${unreadCount}) ${baseTitleRef.current}` : baseTitleRef.current; - - // Only update if the title has changed to avoid unnecessary DOM writes if (document.title !== newTitle) { document.title = newTitle; - lastSetTitleRef.current = newTitle; // Store what we set + lastSetTitleRef.current = newTitle; + } + }; + updateTitle(); + const interval = setInterval(updateTitle, 100); + return () => { + clearInterval(interval); + document.title = baseTitleRef.current; + }; + }, [unreadCount]); + + // Handle outside clicks for popovers + useEffect(() => { + const handleClickOutside = (event) => { + const isNotificationClick = event.target.closest("#header-notifications"); + const isTaskCenterClick = event.target.closest("#header-taskcenter"); + + if (isNotificationClick && scenarioNotificationsOn) { + setTaskCenterVisible(false); // Close task center + return; + } + + if (isTaskCenterClick) { + setNotificationVisible(scenarioNotificationsOn ? false : notificationVisible); // Close notification center if enabled + return; + } + + if (taskCenterVisible && taskCenterRef.current && !taskCenterRef.current.contains(event.target)) { + setTaskCenterVisible(false); + } + + if ( + scenarioNotificationsOn && + notificationVisible && + notificationRef.current && + !notificationRef.current.contains(event.target) + ) { + setNotificationVisible(false); } }; - // Initial update - updateTitle(); + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [taskCenterVisible, notificationVisible, scenarioNotificationsOn]); - // Poll every 100ms to catch child component changes - const interval = setInterval(updateTitle, 100); + // --- Event Handlers --- + const handleTaskCenterClick = useCallback( + (e) => { + setTaskCenterVisible((prev) => { + if (prev) return false; + return true; + }); + if (handleMenuClick) handleMenuClick(e); + }, + [handleMenuClick] + ); - // Cleanup - return () => { - clearInterval(interval); - document.title = baseTitleRef.current; // Reset to base title on unmount - }; - }, [unreadCount]); // Re-run when unreadCount changes + const handleNotificationClick = useCallback( + (e) => { + setNotificationVisible((prev) => { + if (prev) return false; + return true; + }); + if (handleMenuClick) handleMenuClick(e); + }, + [handleMenuClick] + ); - const handleNotificationClick = (e) => { - setNotificationVisible(!notificationVisible); - if (handleMenuClick) handleMenuClick(e); - }; + // --- Menu Items --- - const accountingChildren = [ - { - key: "bills", - id: "header-accounting-bills", - icon: , - label: ( - - - {t("menus.header.bills")} - - - ) - }, - { - key: "enterbills", - id: "header-accounting-enterbills", - icon: , - label: ( - - {t("menus.header.enterbills")} - - ), - onClick: () => - HasFeatureAccess({ featureName: "bills", bodyshop }) && - setBillEnterContext({ - actions: {}, - context: {} - }) - }, - ...(Simple_Inventory.treatment === "on" - ? [ - { type: "divider" }, - { - key: "inventory", - id: "header-accounting-inventory", - icon: , - label: {t("menus.header.inventory")} - } - ] - : []), - { type: "divider" }, - { - key: "allpayments", - id: "header-accounting-allpayments", - icon: , - label: {t("menus.header.allpayments")} - }, - { - key: "enterpayments", - id: "header-accounting-enterpayments", - icon: , - label: t("menus.header.enterpayment"), - onClick: () => - setPaymentContext({ - actions: {}, - context: null - }) - }, - ...(ImEXPay.treatment === "on" - ? [ - { - key: "entercardpayments", - id: "header-accounting-entercardpayments", - icon: , - label: t("menus.header.entercardpayment"), - onClick: () => setCardPaymentContext({ actions: {}, context: {} }) - } - ] - : []), - { type: "divider" }, - { - key: "timetickets", - id: "header-accounting-timetickets", - icon: , - label: ( - - - {t("menus.header.timetickets")} - - - ) - }, - ...(bodyshop?.md_tasks_presets?.use_approvals - ? [ - { - key: "ttapprovals", - id: "header-accounting-ttapprovals", - icon: , - label: {t("menus.header.ttapprovals")} - } - ] - : []), - { - key: "entertimetickets", - id: "header-accounting-entertimetickets", - icon: , - label: ( - - {t("menus.header.entertimeticket")} - - ), - onClick: () => - HasFeatureAccess({ featureName: "timetickets", bodyshop }) && - setTimeTicketContext({ - actions: {}, - context: { - created_by: currentUser.displayName - ? `${currentUser.email} | ${currentUser.displayName}` - : currentUser.email - } - }) - }, - { type: "divider" }, - { - key: "accountingexport", - id: "header-accounting-export", - icon: , - label: ( - - {t("menus.header.export")} - - ), - children: [ - { - key: "receivables", - id: "header-accounting-receivables", - label: ( - - - {t("menus.header.accounting-receivables")} - - - ) - }, - ...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || - DmsAp.treatment === "on" - ? [ - { - key: "payables", - id: "header-accounting-payables", - label: ( - - - {t("menus.header.accounting-payables")} - - - ) - } - ] - : []), - ...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) - ? [ - { - key: "payments", - id: "header-accounting-payments", - label: ( - - - {t("menus.header.accounting-payments")} - - - ) - } - ] - : []), - { type: "divider" }, - { - key: "exportlogs", - id: "header-accounting-exportlogs", - label: ( - - - {t("menus.header.export-logs")} - - - ) - } - ] + // built externally to keep the component clean, but on this level to prevent unnecessary re-renders + const accountingChildren = useMemo( + () => + buildAccountingChildren({ + t, + bodyshop, + currentUser, + setBillEnterContext, + setPaymentContext, + setCardPaymentContext, + setTimeTicketContext, + ImEXPay, + DmsAp, + Simple_Inventory + }), + [ + t, + bodyshop, + currentUser, + setBillEnterContext, + setPaymentContext, + setCardPaymentContext, + setTimeTicketContext, + ImEXPay, + DmsAp, + Simple_Inventory + ] + ); + + // Built externally to keep the component clean + const leftMenuItems = useMemo( + () => + buildLeftMenuItems({ + t, + bodyshop, + recentItems, + setTaskUpsertContext, + setReportCenterContext, + signOutStart, + accountingChildren + }), + [t, bodyshop, recentItems, setTaskUpsertContext, setReportCenterContext, signOutStart, accountingChildren] + ); + + const rightMenuItems = useMemo(() => { + const items = []; + if (scenarioNotificationsOn) { + items.push({ + key: "notifications", + id: "header-notifications", + icon: unreadLoading ? ( + + ) : ( + + + + ), + onClick: handleNotificationClick + }); } - ]; - - // Left menu items (includes original navigation items) - const leftMenuItems = [ - { - key: "home", - id: "header-home", - icon: , - label: {t("menus.header.home")} - }, - { - key: "schedule", - id: "header-schedule", - icon: , - label: {t("menus.header.schedule")} - }, - { - key: "jobssubmenu", - id: "header-jobs", - icon: , - label: t("menus.header.jobs"), - children: [ - { - key: "activejobs", - id: "header-active-jobs", - icon: , - label: {t("menus.header.activejobs")} - }, - { - key: "readyjobs", - id: "header-ready-jobs", - icon: , - label: {t("menus.header.readyjobs")} - }, - { - key: "parts-queue", - id: "header-parts-queue", - icon: , - label: {t("menus.header.parts-queue")} - }, - { - key: "availablejobs", - id: "header-jobs-available", - icon: , - label: {t("menus.header.availablejobs")} - }, - { - key: "newjob", - id: "header-new-job", - icon: , - label: {t("menus.header.newjob")} - }, - { type: "divider" }, - { - key: "alljobs", - id: "header-all-jobs", - icon: , - label: {t("menus.header.alljobs")} - }, - { type: "divider" }, - { - key: "productionlist", - id: "header-production-list", - icon: , - label: {t("menus.header.productionlist")} - }, - { - key: "productionboard", - id: "header-production-board", - icon: , - label: ( - - - {t("menus.header.productionboard")} - - - ) - }, - { type: "divider" }, - { - key: "scoreboard", - id: "header-scoreboard", - icon: , - label: ( - - - {t("menus.header.scoreboard")} - - - ) - } - ] - }, - { - key: "customers", - id: "header-customers", - icon: , - label: t("menus.header.customers"), - children: [ - { - key: "owners", - id: "header-owners", - icon: , - label: {t("menus.header.owners")} - }, - { - key: "vehicles", - id: "header-vehicles", - icon: , - label: {t("menus.header.vehicles")} - } - ] - }, - { - key: "ccs", - id: "header-css", - icon: , - label: ( - - {t("menus.header.courtesycars")} - + items.push({ + key: "taskcenter", + id: "header-taskcenter", + icon: taskCountLoading ? ( + + ) : ( + 0 ? incompleteTaskCount : 0} showZero={false}> + + + + ), - children: [ - { - key: "courtesycarsall", - id: "header-courtesycars-all", - icon: , - label: ( - - - {t("menus.header.courtesycars-all")} - - - ) - }, - { - key: "contracts", - id: "header-contracts", - icon: , - label: ( - - - {t("menus.header.courtesycars-contracts")} - - - ) - }, - { - key: "newcontract", - id: "header-newcontract", - icon: , - label: ( - - - {t("menus.header.courtesycars-newcontract")} - - - ) - } - ] - }, - ...(accountingChildren.length > 0 - ? [ - { - key: "accounting", - id: "header-accounting", - icon: , - label: t("menus.header.accounting"), - children: accountingChildren - } - ] - : []), - { - key: "phonebook", - id: "header-phonebook", - icon: , - label: {t("menus.header.phonebook")} - }, - { - key: "temporarydocs", - id: "header-temporarydocs", - icon: , - label: ( - - - {t("menus.header.temporarydocs")} - - - ) - }, - { - key: "tasks", - id: "tasks", - icon: , - label: t("menus.header.tasks"), - children: [ - { - key: "createTask", - id: "header-create-task", - icon: , - label: t("menus.header.create_task"), - onClick: () => setTaskUpsertContext({ actions: {}, context: {} }) - }, - { - key: "mytasks", - id: "header-my-tasks", - icon: , - label: {t("menus.header.my_tasks")} - }, - { - key: "all_tasks", - id: "header-all-tasks", - icon: , - label: {t("menus.header.all_tasks")} - } - ] - }, - { - key: "shopsubmenu", - id: "header-shopsubmenu", - icon: , - label: t("menus.header.shop"), - children: [ - { - key: "shop", - id: "header-shop", - icon: , - label: {t("menus.header.shop_config")} - }, - { - key: "dashboard", - id: "header-dashboard", - icon: , - label: ( - - {t("menus.header.dashboard")} - - ) - }, - { - key: "reportcenter", - id: "header-reportcenter", - icon: , - label: t("menus.header.reportcenter"), - onClick: () => setReportCenterContext({ actions: {}, context: {} }) - }, - { - key: "shop-vendors", - id: "header-shop-vendors", - icon: , - label: {t("menus.header.shop_vendors")} - }, - { - key: "shop-csi", - id: "header-shop-csi", - icon: , - label: ( - - - {t("menus.header.shop_csi")} - - - ) - } - ] - }, - { - key: "recent", - id: "header-recent", - icon: , - label: t("menus.header.recent"), - children: recentItems.map((i, idx) => ({ - key: idx, - id: `header-recent-${idx}`, - label: {i.label} - })) - }, - { - key: "user", - id: "header-user", - icon: , - label: t("menus.currentuser.profile"), - children: [ - { - key: "signout", - id: "header-signout", - icon: , - danger: true, - label: t("user.actions.signout"), - onClick: () => signOutStart() - }, - { - key: "help", - id: "header-help", - icon: , - label: t("menus.header.help"), - onClick: () => window.open("https://help.imex.online/", "_blank") - }, - { - key: "remoteassist", - id: "header-remote-assist", - icon: , - label: t("menus.header.remoteassist"), - children: [ - ...(InstanceRenderManager({ imex: true, rome: false }) - ? [ - { - key: "rescue", - id: "header-rescue", - icon: , - label: t("menus.header.rescueme"), - onClick: () => window.open("https://imexrescue.com/", "_blank") - } - ] - : []), - { - key: "rescue-zoho", - id: "header-rescue-zoho", - icon: , - label: t("menus.header.rescuemezoho"), - onClick: () => window.open("https://join.zoho.com/", "_blank") - } - ] - }, - { - key: "shiftclock", - id: "header-shiftclock", - icon: , - label: ( - - - {t("menus.header.shiftclock")} - - - ) - }, - { - key: "profile", - id: "header-profile", - icon: , - label: {t("menus.currentuser.profile")} - } - ] - } - ]; - - // Notifications item (always on the right) - const notificationItem = scenarioNotificationsOn - ? [ - { - key: "notifications", - id: "header-notifications", - icon: unreadLoading ? ( - - ) : ( - - - - ), - onClick: handleNotificationClick - } - ] - : []; + onClick: handleTaskCenterClick + }); + return items; + }, [ + scenarioNotificationsOn, + unreadLoading, + unreadCount, + taskCountLoading, + incompleteTaskCount, + isEmployee, + handleNotificationClick, + handleTaskCenterClick, + t + ]); + // --- Render --- return ( -
- - {scenarioNotificationsOn && ( +
+
- )} +
+
+ +
{scenarioNotificationsOn && ( - setNotificationVisible(false)} - unreadCount={unreadCount} - /> +
+ setNotificationVisible(false)} + unreadCount={unreadCount} + /> +
)} +
+ setTaskCenterVisible(false)} + /> +
); } diff --git a/client/src/components/notification-center/notification-center.component.jsx b/client/src/components/notification-center/notification-center.component.jsx index 35dac2af6..a29e675fd 100644 --- a/client/src/components/notification-center/notification-center.component.jsx +++ b/client/src/components/notification-center/notification-center.component.jsx @@ -131,4 +131,6 @@ const NotificationCenterComponent = forwardRef( } ); +NotificationCenterComponent.displayName = "NotificationCenterComponent"; + export default NotificationCenterComponent; diff --git a/client/src/components/task-center/task-center.component.jsx b/client/src/components/task-center/task-center.component.jsx new file mode 100644 index 000000000..8b269f7b7 --- /dev/null +++ b/client/src/components/task-center/task-center.component.jsx @@ -0,0 +1,156 @@ +import { Virtuoso } from "react-virtuoso"; +import { Badge, Button, Spin } from "antd"; +import { useTranslation } from "react-i18next"; +import { forwardRef, useMemo, useRef } from "react"; +import day from "../../utils/day.js"; +import "./task-center.styles.scss"; +import { + ArrowRightOutlined, + CalendarOutlined, + ClockCircleOutlined, + PlusCircleOutlined, + QuestionCircleOutlined +} from "@ant-design/icons"; + +const TaskCenterComponent = forwardRef( + ({ visible, tasks, loading, error, onTaskClick, onLoadMore, hasMore, createNewTask, incompleteTaskCount }, ref) => { + const { t } = useTranslation(); + const virtuosoRef = useRef(null); + + const sectionIcons = { + [t("tasks.labels.overdue")]: , + [t("tasks.labels.due_today")]: , + [t("tasks.labels.upcoming")]: , + [t("tasks.labels.no_due_date")]: + }; + + const groups = useMemo(() => { + const now = day(); + const today = now.startOf("day"); + + const overdue = tasks.filter((t) => t.due_date && day(t.due_date).isBefore(today)); + const dueToday = tasks.filter((t) => t.due_date && day(t.due_date).isSame(today, "day")); + const upcoming = tasks.filter( + (t) => t.due_date && day(t.due_date).isAfter(today) && !day(t.due_date).isSame(today, "day") + ); + const noDueDate = tasks.filter((t) => !t.due_date); + + return [ + { label: t("tasks.labels.overdue"), tasks: overdue }, + { label: t("tasks.labels.due_today"), tasks: dueToday }, + { label: t("tasks.labels.upcoming"), tasks: upcoming }, + { label: t("tasks.labels.no_due_date"), tasks: noDueDate } + ].filter((group) => group.tasks.length > 0); + }, [tasks, t]); + + const groupCounts = useMemo(() => groups.map((group) => group.tasks.length), [groups]); + + const flatTasks = useMemo(() => groups.flatMap((group) => group.tasks), [groups]); + + const priorityColors = { + 1: "red", + 2: "orange", + 3: "green" + }; + + const getPriorityColor = (priority) => priorityColors[priority] || null; + + const groupContent = (groupIndex) => { + const { label, tasks } = groups[groupIndex]; + let displayCount = tasks.length; + if (label === t("tasks.labels.no_due_date")) { + displayCount = + incompleteTaskCount - + groups.reduce((sum, group, idx) => (idx !== groupIndex ? sum + group.tasks.length : sum), 0); + } + return ( +
+ {sectionIcons[label]} + {label} ({displayCount}) +
+ ); + }; + + const itemContent = (index) => { + const task = flatTasks[index]; + const priorityColor = getPriorityColor(task.priority); + return ( +
onTaskClick(task.id)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + onTaskClick(task.id); + } + }} + > +
+
+
{task.title}
+
+ {t("tasks.labels.ro-number", { + ro_number: task.job?.ro_number || t("general.labels.na") + })} +
+
+
+
+ {task.due_date && {day(task.due_date).fromNow()}} + {!!priorityColor && } +
+
+ ); + }; + + if (error) { + return ( +
+
+

{t("tasks.labels.my_tasks_center")}

+
+
{t("tasks.errors.load_failed")}
+
+ ); + } + + return ( +
+
+ +

{t("tasks.labels.my_tasks_center")}

+
+
+
+
+ + {tasks.length === 0 && !loading ? ( +
{t("tasks.labels.no_tasks")}
+ ) : ( + + loading ? ( +
+ +
+ ) : null + }} + /> + )} +
+ ); + } +); + +TaskCenterComponent.displayName = "TaskCenterComponent"; +export default TaskCenterComponent; diff --git a/client/src/components/task-center/task-center.container.jsx b/client/src/components/task-center/task-center.container.jsx new file mode 100644 index 000000000..9291b600d --- /dev/null +++ b/client/src/components/task-center/task-center.container.jsx @@ -0,0 +1,135 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useQuery } from "@apollo/client"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; +import { INITIAL_TASKS, TASKS_CENTER_POLL_INTERVAL, useSocket } from "../../contexts/SocketIO/useSocket"; +import { useIsEmployee } from "../../utils/useIsEmployee"; +import TaskCenterComponent from "./task-center.component"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { QUERY_TASKS_NO_DUE_DATE_PAGINATED, QUERY_TASKS_WITH_DUE_DATES } from "../../graphql/tasks.queries"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + currentUser: selectCurrentUser +}); + +const mapDispatchToProps = (dispatch) => ({ + setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" })) +}); + +const TaskCenterContainer = ({ + visible, + onClose, + bodyshop, + currentUser, + setTaskUpsertContext, + incompleteTaskCount +}) => { + const [tasks, setTasks] = useState([]); + const { isConnected } = useSocket(); + const isEmployee = useIsEmployee(bodyshop, currentUser); + + const assignedToId = useMemo(() => { + const employee = bodyshop?.employees?.find((e) => e.user_email === currentUser?.email); + return employee?.id || null; + }, [bodyshop, currentUser]); + + // Query 1: Tasks with due dates + const { + data: dueDateData, + loading: dueLoading, + error: dueError + } = useQuery(QUERY_TASKS_WITH_DUE_DATES, { + variables: { + bodyshop: bodyshop?.id, + assigned_to: assignedToId, + order: [{ due_date: "asc" }, { created_at: "desc" }] + }, + skip: !bodyshop?.id || !assignedToId || !isEmployee || !currentUser?.email, + fetchPolicy: "cache-and-network", + pollInterval: isConnected ? 0 : TASKS_CENTER_POLL_INTERVAL + }); + + // Query 2: Tasks with no due date (paginated) + const { + data: noDueDateData, + loading: noDueLoading, + error: noDueError, + fetchMore + } = useQuery(QUERY_TASKS_NO_DUE_DATE_PAGINATED, { + variables: { + bodyshop: bodyshop?.id, + assigned_to: assignedToId, + order: [{ priority: "asc" }, { created_at: "desc" }], + limit: INITIAL_TASKS, // Adjust this constant as needed + offset: 0 + }, + skip: !bodyshop?.id || !assignedToId || !isEmployee || !currentUser?.email, + fetchPolicy: "cache-and-network", + pollInterval: isConnected ? 0 : TASKS_CENTER_POLL_INTERVAL + }); + + // Combine tasks from both queries + useEffect(() => { + const dueDateTasks = dueDateData?.tasks || []; + const noDueDateTasks = noDueDateData?.tasks || []; + setTasks([...dueDateTasks, ...noDueDateTasks]); + }, [dueDateData, noDueDateData]); + + const noDueDateLength = noDueDateData?.tasks?.length || 0; + const totalNoDueDate = noDueDateData?.tasks_aggregate?.aggregate?.count || 0; + const hasMore = noDueDateLength < totalNoDueDate; + + // Handle pagination for no-due-date tasks + const handleLoadMore = () => { + fetchMore({ + variables: { + offset: noDueDateData?.tasks?.length || 0 + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + return { + ...prev, + tasks: [...prev.tasks, ...fetchMoreResult.tasks], + tasks_aggregate: fetchMoreResult.tasks_aggregate + }; + } + }); + }; + + const handleTaskClick = useCallback( + (id) => { + const task = tasks.find((t) => t.id === id); + if (task) { + setTaskUpsertContext({ + context: { + existingTask: task + } + }); + } + }, + [tasks, setTaskUpsertContext] + ); + + const createNewTask = () => { + setTaskUpsertContext({ actions: {}, context: {} }); + }; + + return ( + + ); +}; + +export default connect(mapStateToProps, mapDispatchToProps)(TaskCenterContainer); diff --git a/client/src/components/task-center/task-center.styles.scss b/client/src/components/task-center/task-center.styles.scss new file mode 100644 index 000000000..062aa133d --- /dev/null +++ b/client/src/components/task-center/task-center.styles.scss @@ -0,0 +1,147 @@ +.task-center { + position: absolute; + top: 64px; + right: 0; + width: 500px; + max-width: 500px; + background: #fff; + color: rgba(0, 0, 0, 0.85); + border: 1px solid #d9d9d9; + border-radius: 6px; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), 0 3px 6px rgba(0, 0, 0, 0.06); + z-index: 1000; + display: none; + overflow-x: hidden; + + &.visible { + display: block; + } + + .task-header { + padding: 4px 10px; + border-bottom: 1px solid #f0f0f0; + display: flex; + justify-content: space-between; + align-items: center; + background: #fafafa; + + h3 { + font-size: 14px; + margin: 0; + } + + .create-task-button { + border: none; + color: white; + padding: 4px 12px; + border-radius: 4px; + cursor: pointer; + font-weight: 500; + + &:hover { + background-color: #40a9ff; + } + } + } + + .task-section { + margin: 0; + padding: 0; + } + + .section-title { + padding: 0px 10px; + margin: 0px; + //font-size: 12px; + background: #f5f5f5; + font-weight: 650; + border-bottom: 1px solid #e8e8e8; + position: sticky; + top: 0; + z-index: 1; + } + + .task-row-container { + margin-top: 15px; + margin-bottom: 15px; + } + + .task-row { + cursor: pointer; + border-bottom: 1px solid #f0f0f0; + display: flex; + justify-content: space-between; + align-items: flex-start; + + &:hover { + background: #f5f5f5; + } + + .task-title-cell { + flex: 1; + padding: 6px 8px; + vertical-align: top; + //font-size: 12px; + line-height: 1.2; + max-width: 350px; // or whatever fits your layout + + .task-title { + font-size: 16px; + font-weight: 550; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; // Or a specific width if you want more control + display: inline-block; + vertical-align: middle; + } + + .task-ro-number { + margin-top: 20px; + color: #1677ff; + } + } + + .task-due-cell { + padding: 6px 8px; + vertical-align: top; + //font-size: 12px; + line-height: 1.2; + text-align: right; + white-space: nowrap; + color: rgba(0, 0, 0, 0.45); + } + } + + button { + margin: 8px auto; + padding: 4px 10px; + background-color: #1677ff; + color: white; + border: none; + border-radius: 4px; + //font-size: 12px; + cursor: pointer; + + &:hover { + background-color: #4096ff; + } + + &:disabled { + background-color: #d9d9d9; + cursor: not-allowed; + } + } + + .no-tasks-message, + .error-message { + padding: 16px; + text-align: center; + color: rgba(0, 0, 0, 0.45); + } + + .loading-footer { + padding: 16px; + text-align: center; + } +} diff --git a/client/src/components/task-list/task-list.component.jsx b/client/src/components/task-list/task-list.component.jsx index 3258f29a2..96a587908 100644 --- a/client/src/components/task-list/task-list.component.jsx +++ b/client/src/components/task-list/task-list.component.jsx @@ -4,13 +4,12 @@ import { DeleteFilled, DeleteOutlined, EditFilled, - ExclamationCircleFilled, PlusCircleFilled, SyncOutlined } from "@ant-design/icons"; import { Button, Card, Space, Switch, Table } from "antd"; import queryString from "query-string"; -import React, { useCallback, useEffect } from "react"; +import { useCallback, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link, useLocation, useNavigate } from "react-router-dom"; @@ -19,6 +18,7 @@ import { pageLimit } from "../../utils/config"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter.jsx"; import dayjs from "../../utils/day"; import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx"; +import PriorityLabel from "../../utils/tasksPriorityLabel.jsx"; /** * Task List Component @@ -54,47 +54,12 @@ const RemindAtRecord = ({ remindAt }) => { ); }; -/** - * Priority Label Component - * @param priority - * @returns {Element} - * @constructor - */ -const PriorityLabel = ({ priority }) => { - switch (priority) { - case 1: - return ( -
- High -
- ); - case 2: - return ( -
- Medium -
- ); - case 3: - return ( -
- Low -
- ); - default: - return ( -
- None -
- ); - } -}; - const mapDispatchToProps = (dispatch) => ({ // Existing dispatch props... setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" })) }); -const mapStateToProps = (state) => ({}); +const mapStateToProps = () => ({}); export default connect(mapStateToProps, mapDispatchToProps)(TaskListComponent); diff --git a/client/src/components/task-list/task-list.container.jsx b/client/src/components/task-list/task-list.container.jsx index bcf81b9ce..e34b9c199 100644 --- a/client/src/components/task-list/task-list.container.jsx +++ b/client/src/components/task-list/task-list.container.jsx @@ -4,7 +4,6 @@ import { useMutation, useQuery } from "@apollo/client"; import { MUTATION_TOGGLE_TASK_COMPLETED, MUTATION_TOGGLE_TASK_DELETED } from "../../graphql/tasks.queries.js"; import { pageLimit } from "../../utils/config.js"; import AlertComponent from "../alert/alert.component.jsx"; -import React from "react"; import TaskListComponent from "./task-list.component.jsx"; import { useTranslation } from "react-i18next"; import { connect, useDispatch } from "react-redux"; @@ -20,7 +19,7 @@ const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser }); -const mapDispatchToProps = (dispatch) => ({}); +const mapDispatchToProps = () => ({}); export default connect(mapStateToProps, mapDispatchToProps)(TaskListContainer); @@ -55,8 +54,8 @@ export function TaskListContainer({ bodyshop: bodyshop.id, [relationshipType]: relationshipId, deleted: deleted === "true", - completed: completed === "true", //TODO: Find where mine is set. - assigned_to: onlyMine ? bodyshop?.employees?.find((e) => e.user_email === currentUser.email)?.id : undefined, // replace currentUserID with the actual ID of the current user + completed: completed === "true", + assigned_to: onlyMine ? bodyshop?.employees?.find((e) => e.user_email === currentUser.email)?.id : undefined, offset: page ? (page - 1) * pageLimit : 0, limit: pageLimit, order: [ diff --git a/client/src/components/task-upsert-modal/task-upsert-modal.component.jsx b/client/src/components/task-upsert-modal/task-upsert-modal.component.jsx index 562141a40..0c6482c9d 100644 --- a/client/src/components/task-upsert-modal/task-upsert-modal.component.jsx +++ b/client/src/components/task-upsert-modal/task-upsert-modal.component.jsx @@ -1,5 +1,4 @@ import { Col, Form, Input, Row, Select, Switch } from "antd"; -import React from "react"; import { useTranslation } from "react-i18next"; import { createStructuredSelector } from "reselect"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js"; @@ -8,6 +7,7 @@ import { connect } from "react-redux"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx"; import JobSearchSelectComponent from "../job-search-select/job-search-select.component.jsx"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx"; +import { Link } from "react-router-dom"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -42,7 +42,7 @@ export function TaskUpsertModalComponent({ ]; const generatePresets = (job) => { - if (!job || !selectedJobDetails) return datePickerPresets; // return default presets if no job selected + if (!job || !selectedJobDetails) return datePickerPresets; const relativePresets = []; if (selectedJobDetails?.scheduled_completion) { @@ -97,13 +97,8 @@ export function TaskUpsertModalComponent({ }); }; - /** - * Change the selected job id - * @param jobId - */ const changeJobId = (jobId) => { setSelectedJobId(jobId || null); - // Reset the form fields when selectedJobId changes clearRelations(); }; @@ -163,6 +158,13 @@ export function TaskUpsertModalComponent({ required: true } ]} + extra={ + existingTask && selectedJobId ? ( +
+ {t("tasks.labels.go_to_job")} +
+ ) : null + } > - + + {t("tasks.links.go_to_bill")} ( + {selectedJobDetails?.bills?.find((bill) => bill.id === form.getFieldValue("billid"))?.invoice_number}) + + ) : null + } + >