diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx index 9e77cae96..c9109dd24 100644 --- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx +++ b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx @@ -32,6 +32,7 @@ import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util"; import DuplicateJob from "./jobs-detail-header-actions.duplicate.util"; import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -133,10 +134,10 @@ export function JobsDetailHeaderActions({ const notification = useNotification(); const { - treatments: { ImEXPay } + treatments: { ImEXPay, Share_To_Teams } } = useSplitTreatments({ attributes: {}, - names: ["ImEXPay"], + names: ["ImEXPay", "Share_To_Teams"], splitKey: bodyshop && bodyshop.imexshopid }); @@ -971,6 +972,14 @@ export function JobsDetailHeaderActions({ } ); + if (Share_To_Teams?.treatment === "on") { + menuItems.push({ + key: "sharetoteams", + id: "job-actions-sharetoteams", + label: + }); + } + menuItems.push({ key: "exportcustdata", id: "job-actions-exportcustdata", diff --git a/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx b/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx index 6f47bad73..81d033e69 100644 --- a/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx +++ b/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx @@ -19,6 +19,7 @@ import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component"; import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container"; import PrintWrapper from "../print-wrapper/print-wrapper.component"; import PartsOrderDrawer from "./parts-order-list-table-drawer.component"; +import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx"; const mapStateToProps = createStructuredSelector({ jobRO: selectJobReadOnly, @@ -66,19 +67,21 @@ export function PartsOrderListTableComponent({ const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : []; const { refetch } = billsQuery; - + // label: const recordActions = (record, showView = false) => ( + {showView && ( } onClick={() => { handleOnRowClick(record); }} - > - - + /> )} - { @@ -106,6 +109,7 @@ export function PartsOrderListTableComponent({ } onClick={() => { setTaskUpsertContext({ context: { @@ -114,9 +118,7 @@ export function PartsOrderListTableComponent({ } }); }} - > - - + /> - - - + } /> { const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs)); @@ -417,9 +419,20 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe title={!isBodyEmpty ? headerContent : null} extra={ !isBodyEmpty && ( - - - + + + + + } + urlOverride={`${window.location.origin}/manage/jobs/${card.id}`} + /> + + + + ) } > diff --git a/client/src/components/production-board-kanban/production-board-kanban.styles.scss b/client/src/components/production-board-kanban/production-board-kanban.styles.scss index 8b04aaa5d..027b249a0 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.styles.scss +++ b/client/src/components/production-board-kanban/production-board-kanban.styles.scss @@ -10,6 +10,16 @@ .height-preserving-container { } +.share-to-teams-badge { + background-color: #cccccc; + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + .react-trello-column-header { font-weight: bold; cursor: pointer; diff --git a/client/src/components/production-list-columns/production-list-columns.data.jsx b/client/src/components/production-list-columns/production-list-columns.data.jsx index d120e6ce2..f26733c36 100644 --- a/client/src/components/production-list-columns/production-list-columns.data.jsx +++ b/client/src/components/production-list-columns/production-list-columns.data.jsx @@ -27,6 +27,7 @@ import ProductionListColumnNote from "./production-list-columns.productionnote.c import ProductionListColumnCategory from "./production-list-columns.status.category"; import ProductionListColumnStatus from "./production-list-columns.status.component"; import ProductionListColumnTouchTime from "./prodution-list-columns.touchtime.component"; +import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx"; const getEmployeeName = (employeeId, employees) => { const employee = employees.find((e) => e.id === employeeId); @@ -41,7 +42,17 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme dataIndex: "viewdetail", key: "viewdetail", ellipsis: true, - render: (text, record) => {i18n.t("general.labels.view")} + render: (text, record) => ( + + {i18n.t("general.labels.view")} + + + ) }, ...(Enhanced_Payroll.treatment === "on" ? [ diff --git a/client/src/components/share-to-teams/share-to-teams.component.jsx b/client/src/components/share-to-teams/share-to-teams.component.jsx new file mode 100644 index 000000000..fbc22648e --- /dev/null +++ b/client/src/components/share-to-teams/share-to-teams.component.jsx @@ -0,0 +1,124 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Button } from "antd"; +import { useLocation } from "react-router-dom"; +import { SiMicrosoftteams } from "react-icons/si"; +import { useTranslation } from "react-i18next"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors.js"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +/** + * ShareToTeamsButton component for sharing content to Microsoft Teams via an HTTP link. + * + * This component creates a button or link that opens the Microsoft Teams share dialog with + * the provided URL, title, and message text through query parameters. The popup window is centered on the screen. + * + * @param {Object} props - The component's props. + * @param {string} [props.messageTextOverride] - Custom message text for sharing. + * @param {string} [props.urlOverride] - Custom URL to share instead of the current page's URL. + * @param {string} [props.pageTitleOverride] - Custom title for the shared page. + * @param {boolean} [props.noIcon=false] - If true, renders as a simple text link instead of a button with an icon. + * @param {Object} [props.noIconStyle={}] - Style object for the text link when noIcon is true. + * @param {Object} [props.buttonStyle={}] - Style object for the Ant Design button. + * @param {Object} [props.buttonIconStyle={}] - Style object for the icon within the button. + * @param {string} [props.linkText] - Text to display on the button or link. + * @param {Object} [props.additionalProps] - Additional props to pass to the rendered component. + * @returns {React.ReactElement} A button or text link for sharing to Microsoft Teams. + */ +const ShareToTeamsComponent = ({ + bodyshop, + messageTextOverride, + urlOverride, + pageTitleOverride, + noIcon = false, + noIconStyle = {}, + buttonStyle = {}, + buttonIconStyle = {}, + linkText, + ...additionalProps +}) => { + const location = useLocation(); + const { t } = useTranslation(); + + const { + treatments: { Share_To_Teams } + } = useSplitTreatments({ + attributes: {}, + names: ["Share_To_Teams"], + splitKey: bodyshop && bodyshop.imexshopid + }); + + const currentUrl = + urlOverride || + encodeURIComponent(`${window.location.origin}${location.pathname}${location.search}${location.hash}`); + const pageTitle = + pageTitleOverride || + encodeURIComponent(typeof document !== "undefined" ? document.title : t("general.actions.sharetoteams")); + const messageText = messageTextOverride || encodeURIComponent(t("general.actions.sharetoteams")); + + // Construct the Teams share URL with parameters + const teamsShareUrl = `https://teams.microsoft.com/share?href=${currentUrl}&preText=${messageText}&title=${pageTitle}`; + + // Function to open the centered share link in a new window/tab + const handleShare = () => { + const screenWidth = window.screen.width; + const screenHeight = window.screen.height; + const windowWidth = 600; + const windowHeight = 400; + + const left = screenWidth / 2 - windowWidth / 2; + const top = screenHeight / 2 - windowHeight / 2; + + const windowFeatures = `width=${windowWidth},height=${windowHeight},left=${left},top=${top}`; + + // noinspection JSIgnoredPromiseFromCall + window.open(teamsShareUrl, "_blank", windowFeatures); + }; + + if (Share_To_Teams?.treatment !== "on") { + return null; + } + + if (noIcon) { + return ( + + {!linkText ? t("general.actions.sharetoteams") : linkText} + + ); + } + + return ( + } + onClick={handleShare} + title={linkText === null ? t("general.actions.sharetoteams") : linkText} + {...additionalProps} + /> + ); +}; + +ShareToTeamsComponent.propTypes = { + messageTextOverride: PropTypes.string, + urlOverride: PropTypes.string, + pageTitleOverride: PropTypes.string, + noIcon: PropTypes.bool, + noIconStyle: PropTypes.object, + buttonStyle: PropTypes.object, + buttonIconStyle: PropTypes.object, + linkText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + additionalProps: PropTypes.oneOfType([PropTypes.object, PropTypes.node]) +}; + +export default connect(mapStateToProps)(ShareToTeamsComponent); diff --git a/client/src/components/task-list/task-list.component.jsx b/client/src/components/task-list/task-list.component.jsx index ba7d71b17..3711981c7 100644 --- a/client/src/components/task-list/task-list.component.jsx +++ b/client/src/components/task-list/task-list.component.jsx @@ -18,6 +18,7 @@ import { setModalContext } from "../../redux/modals/modals.actions"; 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"; /** * Task List Component @@ -266,8 +267,13 @@ function TaskListComponent({ width: "8%", render: (text, record) => ( + } onClick={() => { setTaskUpsertContext({ context: { @@ -276,18 +282,18 @@ function TaskListComponent({ } }); }} - > - - + /> toggleCompletedStatus(record.id, record.completed)} - > - {record.completed ? : } - - toggleDeletedStatus(record.id, record.deleted)}> - {record.deleted ? : } - + icon={record.completed ? : } + /> + + toggleDeletedStatus(record.id, record.deleted)} + icon={record.deleted ? : } + /> ) } diff --git a/client/src/components/task-upsert-modal/task-upsert-modal.container.jsx b/client/src/components/task-upsert-modal/task-upsert-modal.container.jsx index 3d7acd06a..1d68dada2 100644 --- a/client/src/components/task-upsert-modal/task-upsert-modal.container.jsx +++ b/client/src/components/task-upsert-modal/task-upsert-modal.container.jsx @@ -35,7 +35,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to const [insertTask] = useMutation(MUTATION_INSERT_NEW_TASK); const [updateTask] = useMutation(MUTATION_UPDATE_TASK); const { open, context } = taskUpsert; - const { jobid, joblineid, billid, partsorderid, taskId, existingTask, query } = context; + const { jobid, joblineid, billid, partsorderid, taskId, existingTask, query, view } = context; const [form] = Form.useForm(); const [selectedJobId, setSelectedJobId] = useState(null); const [selectedJobDetails, setSelectedJobDetails] = useState(null); @@ -255,16 +255,14 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to } }; - const taskTitle = useMemo(() => { - return existingTask ? t("tasks.actions.edit") : t("tasks.actions.new"); - }, [existingTask, t]); - + return ( {taskTitle}} + title={{view ? t("tasks.actions.view") : existingTask ? t("tasks.actions.edit") : t("tasks.actions.new")}} open={open} okText={t("general.actions.save")} width="50%" + cancelText={!isTouched ? t("general.actions.ok") : t("general.actions.cancel")} onOk={() => { removeTaskIdFromUrl(); form.submit(); @@ -289,6 +287,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to loading={loading || (taskId && taskLoading)} error={error} data={data} + view={view} existingTask={existingTask || taskData?.tasks_by_pk} selectedJobId={selectedJobId} setSelectedJobId={setSelectedJobId} diff --git a/client/src/pages/tasks/allTasksPageContainer.jsx b/client/src/pages/tasks/allTasksPageContainer.jsx index 107143786..4b63347cb 100644 --- a/client/src/pages/tasks/allTasksPageContainer.jsx +++ b/client/src/pages/tasks/allTasksPageContainer.jsx @@ -46,7 +46,8 @@ export function AllTasksPageContainer({ setBreadcrumbs, setSelectedHeader, setTa if (taskId) { setTaskUpsertContext({ context: { - taskId + taskId, + view: true } }); urlParams.delete("taskid"); diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index ce321517d..c28c5da46 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1193,7 +1193,9 @@ "submit": "Submit", "tryagain": "Try Again", "view": "View", - "viewreleasenotes": "See What's Changed" + "viewreleasenotes": "See What's Changed", + "sharetoteams": "Share to Teams", + "ok": "Ok" }, "errors": { "fcm": "You must allow notification permissions to have real time messaging. Click to try again.", @@ -3183,7 +3185,8 @@ "tasks": { "actions": { "edit": "Edit Task", - "new": "New Task" + "new": "New Task", + "view": "View Task" }, "buttons": { "allTasks": "All", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 5b06b1d6f..0483c3bfe 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -1193,7 +1193,9 @@ "submit": "", "tryagain": "", "view": "", - "viewreleasenotes": "" + "viewreleasenotes": "", + "sharetoteams": "", + "ok": "" }, "errors": { "fcm": "", @@ -3183,7 +3185,8 @@ "tasks": { "actions": { "edit": "", - "new": "" + "new": "", + "view": "" }, "buttons": { "allTasks": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index faf2d46d7..a98f29c08 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -1193,7 +1193,9 @@ "submit": "", "tryagain": "", "view": "", - "viewreleasenotes": "" + "viewreleasenotes": "", + "sharetoteams": "", + "ok": "" }, "errors": { "fcm": "", @@ -3183,7 +3185,8 @@ "tasks": { "actions": { "edit": "", - "new": "" + "new": "", + "view": "" }, "buttons": { "allTasks": "",