diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 9322e859a..265958b38 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -4535,6 +4535,48 @@ + + localmediaserverhttp + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + localmediaservernetwork + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + logo_img_footer_margin false @@ -8261,6 +8303,27 @@ + + uselocalmediaserver + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + website false @@ -13253,6 +13316,27 @@ + + openinexplorer + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + reassign_limitexceeded false diff --git a/client/package.json b/client/package.json index 5c05e9556..216bf52b7 100644 --- a/client/package.json +++ b/client/package.json @@ -34,6 +34,7 @@ "markerjs2": "^2.21.1", "moment-business-days": "^1.2.0", "moment-timezone": "^0.5.34", + "normalize-url": "^7.0.3", "phone": "^3.1.15", "preval.macro": "^5.0.0", "prop-types": "^15.8.1", 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 c8eb78281..9bbca0a60 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 @@ -12,27 +12,29 @@ import moment from "moment"; import queryString from "query-string"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useLocation, useHistory } from "react-router-dom"; +import { connect } from "react-redux"; +import { useHistory, useLocation } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; import { DELETE_BILL_LINE, INSERT_NEW_BILL_LINES, UPDATE_BILL_LINE, } from "../../graphql/bill-lines.queries"; import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AlertComponent from "../alert/alert.component"; import BillFormContainer from "../bill-form/bill-form.container"; -import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container"; -import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; -import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component"; -import { connect } from "react-redux"; -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"; +import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component"; +import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container"; +import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container"; +import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser + bodyshop: selectBodyshop, }); const mapDispatchToProps = (dispatch) => ({ setPartsOrderContext: (context) => @@ -49,6 +51,7 @@ export default connect( export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail, + bodyshop, }) { const search = queryString.parse(useLocation().search); const history = useHistory(); @@ -265,12 +268,21 @@ export function BillDetailEditcontainer({ layout="vertical" > - + + {bodyshop.uselocalmediaserver ? ( + + ) : ( + + )} )} diff --git a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx index 5cd926785..bfadbcd37 100644 --- a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx +++ b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx @@ -24,6 +24,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings"; import BillFormContainer from "../bill-form/bill-form.container"; import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility"; import { handleUpload } from "../documents-upload/documents-upload.utility"; +import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility"; const mapStateToProps = createStructuredSelector({ billEnterModal: selectBillEnterModal, @@ -210,19 +211,33 @@ function BillEnterModalContainer({ ///////////////////////// if (upload && upload.length > 0) { //insert Each of the documents? - upload.forEach((u) => { - handleUpload( - { file: u.originFileObj }, - { - bodyshop: bodyshop, - uploaded_by: currentUser.email, - jobId: values.jobid, - billId: billId, - tagsArray: null, - callback: null, - } - ); - }); + + if (bodyshop.uselocalmediaserver) { + upload.forEach((u) => { + handleLocalUpload({ + ev: { file: u.originFileObj }, + context: { + jobid: values.jobid, + invoice_number: remainingValues.invoice_number, + vendorid: remainingValues.vendorid, + }, + }); + }); + } else { + upload.forEach((u) => { + handleUpload( + { file: u.originFileObj }, + { + bodyshop: bodyshop, + uploaded_by: currentUser.email, + jobId: values.jobid, + billId: billId, + tagsArray: null, + callback: null, + } + ); + }); + } } /////////////////////////// setLoading(false); diff --git a/client/src/components/chat-media-selector/chat-media-selector.component.jsx b/client/src/components/chat-media-selector/chat-media-selector.component.jsx index 60156294a..35ea6fe1d 100644 --- a/client/src/components/chat-media-selector/chat-media-selector.component.jsx +++ b/client/src/components/chat-media-selector/chat-media-selector.component.jsx @@ -6,12 +6,13 @@ import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import AlertComponent from "../alert/alert.component"; import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser + bodyshop: selectBodyshop, }); const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) @@ -19,6 +20,7 @@ const mapDispatchToProps = (dispatch) => ({ export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector); export function ChatMediaSelector({ + bodyshop, selectedMedia, setSelectedMedia, conversation, @@ -27,7 +29,6 @@ export function ChatMediaSelector({ const [visible, setVisible] = useState(false); const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, { - fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { @@ -66,6 +67,8 @@ export function ChatMediaSelector({ ); + if (bodyshop.uselocalmediaserver) return null; + return ( {bodyshop.md_messaging_presets.map((i, idx) => ( - setMessage(i.text)} onItemHover key={idx}> + setMessage(i.text)} key={idx}> {i.label} ))} diff --git a/client/src/components/documents-local-upload/documents-local-upload.component.jsx b/client/src/components/documents-local-upload/documents-local-upload.component.jsx new file mode 100644 index 000000000..630b1a368 --- /dev/null +++ b/client/src/components/documents-local-upload/documents-local-upload.component.jsx @@ -0,0 +1,70 @@ +import { UploadOutlined } from "@ant-design/icons"; +import { Upload } from "antd"; +import React, { useState } from "react"; + +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { + selectBodyshop, + selectCurrentUser, +} from "../../redux/user/user.selectors"; +import { handleUpload } from "./documents-local-upload.utility"; + +const mapStateToProps = createStructuredSelector({ + currentUser: selectCurrentUser, + bodyshop: selectBodyshop, +}); + +export function DocumentsLocalUploadComponent({ + children, + currentUser, + bodyshop, + job, + vendorid, + invoice_number, + callbackAfterUpload, +}) { + const [fileList, setFileList] = useState([]); + + const handleDone = (uid) => { + setTimeout(() => { + setFileList((fileList) => fileList.filter((x) => x.uid !== uid)); + }, 2000); + }; + + return ( + { + if (f.event && f.event.percent === 100) handleDone(f.file.uid); + + setFileList(f.fileList); + }} + customRequest={(ev) => + handleUpload({ + ev, + context: { + jobid: job.id, + vendorid, + invoice_number, + callback: callbackAfterUpload, + }, + }) + } + accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx" + > + {children || ( + <> +

+ +

+

+ Click or drag files to this area to upload. +

+ + )} +
+ ); +} +export default connect(mapStateToProps, null)(DocumentsLocalUploadComponent); diff --git a/client/src/components/documents-local-upload/documents-local-upload.utility.js b/client/src/components/documents-local-upload/documents-local-upload.utility.js new file mode 100644 index 000000000..d7d0b289d --- /dev/null +++ b/client/src/components/documents-local-upload/documents-local-upload.utility.js @@ -0,0 +1,65 @@ +import cleanAxios from "../../utils/CleanAxios"; +import { store } from "../../redux/store"; +import { addMediaForJob } from "../../redux/media/media.actions"; +import normalizeUrl from "normalize-url"; + +export const handleUpload = async ({ ev, context }) => { + const { onError, onSuccess, onProgress, file } = ev; + const { jobid, invoice_number, vendorid, callbackAfterUpload } = context; + + var options = { + headers: { "X-Requested-With": "XMLHttpRequest" }, + onUploadProgress: (e) => { + if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 }); + }, + }; + + const formData = new FormData(); + + formData.append("jobid", jobid); + if (invoice_number) { + formData.append("invoice_number", invoice_number); + formData.append("vendorid", vendorid); + } + formData.append("file", file); + const bodyshop = store.getState().user.bodyshop; + + const imexMediaServerResponse = await cleanAxios.post( + normalizeUrl( + `${bodyshop.localmediaserverhttp}/${ + invoice_number ? "bills" : "jobs" + }/upload` + ), + formData, + { + ...options, + } + ); + + if (imexMediaServerResponse.status !== 200) { + if (!!onError) { + onError(imexMediaServerResponse.statusText); + } + } else { + onSuccess && onSuccess(file); + store.dispatch( + addMediaForJob({ + jobid, + media: imexMediaServerResponse.data.map((d) => { + return { + ...d, + selected: false, + src: normalizeUrl(`${bodyshop.localmediaserverhttp}/${d.src}`), + thumbnail: normalizeUrl( + `${bodyshop.localmediaserverhttp}/${d.thumbnail}` + ), + }; + }), + }) + ); + } + + if (callbackAfterUpload) { + callbackAfterUpload(); + } +}; diff --git a/client/src/components/email-overlay/email-overlay.component.jsx b/client/src/components/email-overlay/email-overlay.component.jsx index 0c7fefc78..ef9ab2772 100644 --- a/client/src/components/email-overlay/email-overlay.component.jsx +++ b/client/src/components/email-overlay/email-overlay.component.jsx @@ -9,6 +9,7 @@ import { Space, Menu, Dropdown, + Button, } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; @@ -20,10 +21,13 @@ import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; +import { CreateExplorerLinkForJob } from "../../utils/localmedia"; +import { selectEmailConfig } from "../../redux/email/email.selectors"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, currentUser: selectCurrentUser, + emailConfig: selectEmailConfig, }); const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) @@ -34,6 +38,7 @@ export default connect( )(EmailOverlayComponent); export function EmailOverlayComponent({ + emailConfig, form, selectedMediaState, bodyshop, @@ -153,10 +158,17 @@ export function EmailOverlayComponent({ - - - + {!bodyshop.uselocalmediaserver && ( + + + + )} + {bodyshop.uselocalmediaserver && emailConfig.jobid && ( + + + + )} ({ setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" })), @@ -31,7 +37,7 @@ const span = { lg: { span: 8 }, }; -export function JobDetailCards({ setPrintCenterContext }) { +export function JobDetailCards({ bodyshop, setPrintCenterContext }) { const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) .filter((screen) => !!screen[1]) .slice(-1)[0]; @@ -143,12 +149,14 @@ export function JobDetailCards({ setPrintCenterContext }) { data={data ? data.jobs_by_pk : null} /> - - - + {!bodyshop.uselocalmediaserver && ( + + + + )} ); } -export default connect(null, mapDispatchToProps)(JobDetailCards); +export default connect(mapStateToProps, mapDispatchToProps)(JobDetailCards); diff --git a/client/src/components/job-lines-preset-button/job-lines-preset-button.component.jsx b/client/src/components/job-lines-preset-button/job-lines-preset-button.component.jsx index 8e0aa2cb9..71b7ac920 100644 --- a/client/src/components/job-lines-preset-button/job-lines-preset-button.component.jsx +++ b/client/src/components/job-lines-preset-button/job-lines-preset-button.component.jsx @@ -24,7 +24,7 @@ export function JoblinePresetButton({ bodyshop, form }) { const menu = ( {bodyshop.md_jobline_presets.map((i, idx) => ( - handleSelect(i)} onItemHover key={idx}> + handleSelect(i)} key={idx}> {i.label} ))} diff --git a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx index ce25fe6d8..dbc603660 100644 --- a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx +++ b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx @@ -1,5 +1,5 @@ -import { FileExcelFilled, EditFilled, SyncOutlined } from "@ant-design/icons"; -import { Card, Col, Row, Space, Button } from "antd"; +import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons"; +import { Button, Card, Col, Row, Space } from "antd"; import React, { useEffect, useState } from "react"; import Gallery from "react-grid-gallery"; import { useTranslation } from "react-i18next"; diff --git a/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx b/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx new file mode 100644 index 000000000..cc7cbe31f --- /dev/null +++ b/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx @@ -0,0 +1,105 @@ +import { SyncOutlined } from "@ant-design/icons"; +import { Button, Card, Space } from "antd"; +import React, { useEffect } from "react"; +import Gallery from "react-grid-gallery"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { + getBillMedia, + getJobMedia, + toggleMediaSelected, +} from "../../redux/media/media.actions"; +import { selectAllMedia } from "../../redux/media/media.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { CreateExplorerLinkForJob } from "../../utils/localmedia"; +import DocumentsLocalUploadComponent from "../documents-local-upload/documents-local-upload.component"; +import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + allMedia: selectAllMedia, +}); + +const mapDispatchToProps = (dispatch) => ({ + getJobMedia: (id) => dispatch(getJobMedia(id)), + getBillMedia: ({ jobid, invoice_number }) => { + dispatch(getBillMedia({ jobid, invoice_number })); + }, + toggleMediaSelected: ({ jobid, filename }) => + dispatch(toggleMediaSelected({ jobid, filename })), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(JobsDocumentsLocalGallery); + +export function JobsDocumentsLocalGallery({ + bodyshop, + toggleMediaSelected, + getJobMedia, + getBillMedia, + allMedia, + job, + invoice_number, + vendorid, +}) { + const { t } = useTranslation(); + useEffect(() => { + if (job) { + if (invoice_number) { + getBillMedia({ jobid: job.id, invoice_number }); + } else { + getJobMedia(job.id); + } + } + }, [job, invoice_number, getJobMedia, getBillMedia]); + + return ( +
+ + + + + + + + + + + + { + toggleMediaSelected({ jobid: job.id, filename: image.filename }); + }} + onClickImage={(props) => { + window.open( + props.target.src, + "_blank", + "toolbar=0,location=0,menubar=0" + ); + }} + /> + +
+ ); +} diff --git a/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.reassign.component.jsx b/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.reassign.component.jsx new file mode 100644 index 000000000..c8f336d7c --- /dev/null +++ b/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.reassign.component.jsx @@ -0,0 +1,92 @@ +import { Button, Form, Popover, Space } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { getJobMedia } from "../../redux/media/media.actions"; +import { selectAllMedia } from "../../redux/media/media.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import cleanAxios from "../../utils/CleanAxios"; +import JobSearchSelect from "../job-search-select/job-search-select.component"; + +const mapStateToProps = createStructuredSelector({ + allMedia: selectAllMedia, + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + getJobMedia: (id) => dispatch(getJobMedia(id)), + + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(JobsDocumentsLocalGalleryReassign); + +export function JobsDocumentsLocalGalleryReassign({ + bodyshop, + jobid, + allMedia, + getJobMedia, +}) { + const { t } = useTranslation(); + const [form] = Form.useForm(); + + const [visible, setVisible] = useState(false); + const [loading, setLoading] = useState(false); + + const handleFinish = async ({ jobid: newJobid }) => { + setLoading(true); + const selectedDocuments = allMedia[jobid].filter((m) => m.isSelected); + + await cleanAxios.post(`${bodyshop.localmediaserverhttp}/jobs/move`, { + from_jobid: jobid, + jobid: newJobid, + files: selectedDocuments.map((f) => f.filename), + }); + + getJobMedia(jobid); + setVisible(false); + setLoading(false); + }; + + const popContent = ( +
+
+ + + +
+ + + + +
+ ); + + return ( + + + + ); +} diff --git a/client/src/components/notes-preset-button/notes-preset-button.component.jsx b/client/src/components/notes-preset-button/notes-preset-button.component.jsx index 87412ec8c..e330b15f6 100644 --- a/client/src/components/notes-preset-button/notes-preset-button.component.jsx +++ b/client/src/components/notes-preset-button/notes-preset-button.component.jsx @@ -24,7 +24,7 @@ export function NotesPresetButton({ bodyshop, form }) { const menu = ( {bodyshop.md_notes_presets.map((i, idx) => ( - handleSelect(i)} onItemHover key={idx}> + handleSelect(i)} key={idx}> {i.label} ))} diff --git a/client/src/components/production-list-detail/production-list-detail.component.jsx b/client/src/components/production-list-detail/production-list-detail.component.jsx index e5495b181..ac5b4146c 100644 --- a/client/src/components/production-list-detail/production-list-detail.component.jsx +++ b/client/src/components/production-list-detail/production-list-detail.component.jsx @@ -22,9 +22,10 @@ import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { setModalContext } from "../../redux/modals/modals.actions"; import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser + bodyshop: selectBodyshop, }); const mapDispatchToProps = (dispatch) => ({ setPrintCenterContext: (context) => @@ -35,7 +36,11 @@ export default connect( mapDispatchToProps )(ProductionListDetail); -export function ProductionListDetail({ jobs, setPrintCenterContext }) { +export function ProductionListDetail({ + bodyshop, + jobs, + setPrintCenterContext, +}) { const search = queryString.parse(useLocation().search); const history = useHistory(); const { selected } = search; @@ -144,11 +149,12 @@ export function ProductionListDetail({ jobs, setPrintCenterContext }) { loading={loading} data={data ? data.jobs_by_pk : null} /> - - + {!bodyshop.uselocalmediaserver && ( + + )} )} 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 92e99791b..5d2531705 100644 --- a/client/src/components/shop-info/shop-info.general.component.jsx +++ b/client/src/components/shop-info/shop-info.general.component.jsx @@ -584,6 +584,25 @@ export default function ShopInfoGeneral({ form }) { > + + + + + + + + + diff --git a/client/src/graphql/bodyshop.queries.js b/client/src/graphql/bodyshop.queries.js index 06d86ff3c..ef52c9c32 100644 --- a/client/src/graphql/bodyshop.queries.js +++ b/client/src/graphql/bodyshop.queries.js @@ -107,6 +107,9 @@ export const QUERY_BODYSHOP = gql` md_parts_order_comment bill_allow_post_to_closed md_to_emails + uselocalmediaserver + localmediaserverhttp + localmediaservernetwork employees { user_email id @@ -212,6 +215,9 @@ export const UPDATE_SHOP = gql` md_parts_order_comment bill_allow_post_to_closed md_to_emails + uselocalmediaserver + localmediaserverhttp + localmediaservernetwork employees { id first_name diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx index 63a34b554..f2614e9c9 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx @@ -50,6 +50,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors"; import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import { insertAuditTrail } from "../../redux/application/application.actions"; +import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -62,6 +63,7 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(insertAuditTrail({ jobid, operation })), }); export function JobsDetailPage({ + bodyshop, setPrintCenterContext, jobRO, job, @@ -344,7 +346,11 @@ export function JobsDetailPage({ } key="documents" > - + {bodyshop.uselocalmediaserver ? ( + + ) : ( + + )} ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(TemporaryDocsComponent); + +export function TemporaryDocsComponent({ bodyshop }) { const { loading, error, data, refetch } = useQuery(QUERY_TEMPORARY_DOCS, { fetchPolicy: "network-only", nextFetchPolicy: "network-only", + skip: bodyshop.uselocalmediaserver, }); if (loading) return ; if (error) return ; + if (bodyshop.uselocalmediaserver) { + return ; + } return ( ({ + type: MediaActionTypes.GET_MEDIA_FOR_JOB, + payload: jobid, +}); + +export const getBillMedia = ({ jobid, invoice_number }) => { + console.log("in the action"); + return { + type: MediaActionTypes.GET_MEDIA_FOR_BILL, + payload: { jobid, invoice_number }, + }; +}; + +export const setJobMedia = ({ jobid, media }) => ({ + type: MediaActionTypes.SET_MEDIA_FOR_JOB, + payload: { jobid, media }, +}); + +export const addMediaForJob = ({ jobid, media }) => ({ + type: MediaActionTypes.ADD_MEDIA_FOR_JOB, + payload: { jobid, media }, +}); + +export const getJobMediaError = ({ error, message }) => ({ + type: MediaActionTypes.GET_MEDIA_FOR_JOB_ERROR, + payload: { error, message }, +}); + +export const toggleMediaSelected = ({ jobid, filename }) => ({ + type: MediaActionTypes.TOGGLE_MEDIA_SELECTED, + payload: { jobid, filename }, +}); diff --git a/client/src/redux/media/media.reducer.js b/client/src/redux/media/media.reducer.js new file mode 100644 index 000000000..47c52a0f9 --- /dev/null +++ b/client/src/redux/media/media.reducer.js @@ -0,0 +1,34 @@ +import MediaActionTypes from "./media.types"; + +const INITIAL_STATE = { error: null }; + +const mediaReducer = (state = INITIAL_STATE, action) => { + switch (action.type) { + case MediaActionTypes.SET_MEDIA_FOR_JOB: + return { ...state, [action.payload.jobid]: action.payload.media }; + case MediaActionTypes.GET_MEDIA_FOR_JOB_ERROR: + return { ...state, error: action.payload }; + case MediaActionTypes.ADD_MEDIA_FOR_JOB: + return { + ...state, + [action.payload.jobid]: [ + ...(state[action.payload.jobid] ? state[action.payload.jobid] : []), + ...(action.payload.media || []), + ], + }; + case MediaActionTypes.TOGGLE_MEDIA_SELECTED: + return { + ...state, + [action.payload.jobid]: state[action.payload.jobid].map((p) => { + if (p.filename === action.payload.filename) { + p.isSelected = !p.isSelected; + } + return p; + }), + }; + default: + return state; + } +}; + +export default mediaReducer; diff --git a/client/src/redux/media/media.sagas.js b/client/src/redux/media/media.sagas.js new file mode 100644 index 000000000..6ec037109 --- /dev/null +++ b/client/src/redux/media/media.sagas.js @@ -0,0 +1,108 @@ +import { all, call, takeLatest, put, select } from "redux-saga/effects"; +import { getJobMediaError, setJobMedia } from "./media.actions"; +import MediaActionTypes from "./media.types"; +import cleanAxios from "../../utils/CleanAxios"; +import normalizeUrl from "normalize-url"; + +export function* onSetJobMedia() { + yield takeLatest(MediaActionTypes.GET_MEDIA_FOR_JOB, getJobMedia); +} +export function* getJobMedia({ payload: jobid }) { + try { + const localmediaserverhttp = (yield select( + (state) => state.user.bodyshop.localmediaserverhttp + )).trim(); + + if (localmediaserverhttp && localmediaserverhttp !== "") { + const imagesFetch = yield cleanAxios.post( + `${localmediaserverhttp}/jobs/list`, + { + jobid, + } + ); + const documentsFetch = yield cleanAxios.post( + `${localmediaserverhttp}/bills/list`, + { + jobid, + } + ); + + yield put( + setJobMedia({ + jobid, + media: [ + ...imagesFetch.data.map((d, idx) => { + return { + ...d, + src: normalizeUrl(`${localmediaserverhttp}/${d.src}`), + thumbnail: normalizeUrl( + `${localmediaserverhttp}/${d.thumbnail}` + ), + isSelected: false, + key: idx, + }; + }), + ...documentsFetch.data.map((d, idx) => { + return { + ...d, + src: normalizeUrl(`${localmediaserverhttp}/${d.src}`), + thumbnail: normalizeUrl( + `${localmediaserverhttp}/${d.thumbnail}` + ), + isSelected: false, + key: idx, + }; + }), + ], + }) + ); + } + } catch (error) { + yield put(getJobMediaError(error)); + } +} +export function* onSetBillMedia() { + yield takeLatest(MediaActionTypes.GET_MEDIA_FOR_BILL, getBillMedia); +} +export function* getBillMedia({ payload: { jobid, invoice_number } }) { + try { + const localmediaserverhttp = (yield select( + (state) => state.user.bodyshop.localmediaserverhttp + )).trim(); + + if (localmediaserverhttp && localmediaserverhttp !== "") { + const documentsFetch = yield cleanAxios.post( + `${localmediaserverhttp}/bills/list`, + { + jobid, + invoice_number, + } + ); + + yield put( + setJobMedia({ + jobid, + media: [ + ...documentsFetch.data.map((d, idx) => { + return { + ...d, + src: normalizeUrl(`${localmediaserverhttp}/${d.src}`), + thumbnail: normalizeUrl( + `${localmediaserverhttp}/${d.thumbnail}` + ), + isSelected: false, + key: idx, + }; + }), + ], + }) + ); + } + } catch (error) { + yield put(getJobMediaError(error)); + } +} + +export function* mediaSagas() { + yield all([call(onSetJobMedia), call(onSetBillMedia)]); +} diff --git a/client/src/redux/media/media.selectors.js b/client/src/redux/media/media.selectors.js new file mode 100644 index 000000000..e5c930e5c --- /dev/null +++ b/client/src/redux/media/media.selectors.js @@ -0,0 +1,5 @@ +import { createSelector } from "reselect"; + +const selectMedia = (state) => state.media; + +export const selectAllMedia = createSelector([selectMedia], (media) => media); diff --git a/client/src/redux/media/media.types.js b/client/src/redux/media/media.types.js new file mode 100644 index 000000000..91eaf3d1c --- /dev/null +++ b/client/src/redux/media/media.types.js @@ -0,0 +1,12 @@ +const MediaActionTypes = { + SET_MEDIA_FOR_JOB: "SET_MEDIA_FOR_JOB", + GET_MEDIA_FOR_JOB: "GET_MEDIA_FOR_JOB", + GET_MEDIA_FOR_JOB_ERROR: "GET_MEDIA_FOR_JOB_ERROR", + ADD_MEDIA_FOR_JOB: "ADD_MEDIA_FOR_JOB", + TOGGLE_MEDIA_SELECTED: "TOGGLE_MEDIA_SELECTED", + POST_MEDIA_FOR_JOB: "POST_MEDIA_FOR_JOB", + POST_MEDIA_FOR_JOB_SUCCESS: "POST_MEDIA_FOR_JOB_SUCCESS", + POST_MEDIA_FOR_JOB_ERROR: "POST_MEDIA_FOR_JOB_ERROR", + GET_MEDIA_FOR_BILL: "GET_MEDIA_FOR_BILL", +}; +export default MediaActionTypes; diff --git a/client/src/redux/root.reducer.js b/client/src/redux/root.reducer.js index f29081378..a61ac43eb 100644 --- a/client/src/redux/root.reducer.js +++ b/client/src/redux/root.reducer.js @@ -4,6 +4,7 @@ import storage from "redux-persist/lib/storage"; import { withReduxStateSync } from "redux-state-sync"; import applicationReducer from "./application/application.reducer"; import emailReducer from "./email/email.reducer"; +import mediaReducer from "./media/media.reducer"; import messagingReducer from "./messaging/messaging.reducer"; import modalsReducer from "./modals/modals.reducer"; import techReducer from "./tech/tech.reducer"; @@ -29,6 +30,7 @@ const rootReducer = combineReducers({ modals: modalsReducer, application: persistReducer(applicationPersistConfig, applicationReducer), tech: techReducer, + media: mediaReducer, }); export default withReduxStateSync( diff --git a/client/src/redux/root.saga.js b/client/src/redux/root.saga.js index d18dd9d9c..d2fe2965c 100644 --- a/client/src/redux/root.saga.js +++ b/client/src/redux/root.saga.js @@ -6,6 +6,7 @@ import { emailSagas } from "./email/email.sagas"; import { modalsSagas } from "./modals/modals.sagas"; import { applicationSagas } from "./application/application.sagas"; import { techSagas } from "./tech/tech.sagas"; +import { mediaSagas } from "./media/media.sagas"; export default function* rootSaga() { yield all([ @@ -15,5 +16,6 @@ export default function* rootSaga() { call(modalsSagas), call(applicationSagas), call(techSagas), + call(mediaSagas), ]); } diff --git a/client/src/redux/user/user.reducer.js b/client/src/redux/user/user.reducer.js index 5f316d9e8..f9e391e61 100644 --- a/client/src/redux/user/user.reducer.js +++ b/client/src/redux/user/user.reducer.js @@ -97,7 +97,10 @@ const userReducer = (state = INITIAL_STATE, action) => { }; case UserActionTypes.SET_SHOP_DETAILS: - return { ...state, bodyshop: action.payload }; + return { + ...state, + bodyshop: action.payload, + }; case UserActionTypes.SIGN_IN_FAILURE: case UserActionTypes.SIGN_OUT_FAILURE: case UserActionTypes.EMAIL_SIGN_UP_FAILURE: diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 0dc3c96d2..1f5646da7 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -282,6 +282,8 @@ }, "last_name_first": "Display Owner Info as , ", "lastnumberworkingdays": "Scoreboard - Last Number of Working Days", + "localmediaserverhttp": "Local Media Server - HTTP Path", + "localmediaservernetwork": "Local Media Server - Network Path", "logo_img_footer_margin": "Footer Margin (px)", "logo_img_header_margin": "Header Margin (px)", "logo_img_path": "Shop Logo", @@ -507,6 +509,7 @@ "timezone": "Timezone", "tt_allow_post_to_invoiced": "Allow Time Tickets to be posted to Invoiced & Exported Jobs", "use_fippa": "Use FIPPA for Names on Generated Documents?", + "uselocalmediaserver": "Use Local Media Server?", "website": "Website", "zip_post": "Zip/Postal Code" }, @@ -824,6 +827,7 @@ "confirmdelete": "Are you sure you want to delete these documents. This CANNOT be undone.", "doctype": "Document Type", "newjobid": "Assign to Job", + "openinexplorer": "Open in Explorer", "reassign_limitexceeded": "Reassigning all selected documents will exceed the job storage limit for your shop. ", "reassign_limitexceeded_title": "Unable to reassign document(s)", "storageexceeded": "You've exceeded your storage limit for this job. Please remove documents, or increase your storage plan.", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 8670f0f47..72ae91870 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -282,6 +282,8 @@ }, "last_name_first": "", "lastnumberworkingdays": "", + "localmediaserverhttp": "", + "localmediaservernetwork": "", "logo_img_footer_margin": "", "logo_img_header_margin": "", "logo_img_path": "", @@ -507,6 +509,7 @@ "timezone": "", "tt_allow_post_to_invoiced": "", "use_fippa": "", + "uselocalmediaserver": "", "website": "", "zip_post": "" }, @@ -824,6 +827,7 @@ "confirmdelete": "", "doctype": "", "newjobid": "", + "openinexplorer": "", "reassign_limitexceeded": "", "reassign_limitexceeded_title": "", "storageexceeded": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index fc457d9ff..796d7db37 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -282,6 +282,8 @@ }, "last_name_first": "", "lastnumberworkingdays": "", + "localmediaserverhttp": "", + "localmediaservernetwork": "", "logo_img_footer_margin": "", "logo_img_header_margin": "", "logo_img_path": "", @@ -507,6 +509,7 @@ "timezone": "", "tt_allow_post_to_invoiced": "", "use_fippa": "", + "uselocalmediaserver": "", "website": "", "zip_post": "" }, @@ -824,6 +827,7 @@ "confirmdelete": "", "doctype": "", "newjobid": "", + "openinexplorer": "", "reassign_limitexceeded": "", "reassign_limitexceeded_title": "", "storageexceeded": "", diff --git a/client/src/utils/RenderTemplate.js b/client/src/utils/RenderTemplate.js index 5584b3aa4..2caa4556e 100644 --- a/client/src/utils/RenderTemplate.js +++ b/client/src/utils/RenderTemplate.js @@ -25,7 +25,7 @@ export default async function RenderTemplate( let { contextData, useShopSpecificTemplate } = await fetchContextData( templateObject ); - console.log(templateObject.name); + const { ignoreCustomMargins } = Templates[templateObject.name]; let reportRequest = { diff --git a/client/src/utils/localmedia.js b/client/src/utils/localmedia.js new file mode 100644 index 000000000..e12950981 --- /dev/null +++ b/client/src/utils/localmedia.js @@ -0,0 +1,6 @@ +import { store } from "../redux/store"; + +export function CreateExplorerLinkForJob({ jobid }) { + const bodyshop = store.getState().user.bodyshop; + return `imexmedia://${bodyshop.localmediaservernetwork}/Jobs/${jobid}`; +} diff --git a/client/yarn.lock b/client/yarn.lock index 7e6e0248d..94e47d3b9 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -9702,6 +9702,11 @@ normalize-url@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== +normalize-url@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-7.0.3.tgz#12e56889f7a54b2d5b09616f36c442a9063f61af" + integrity sha512-RiCOdwdPnzvwcBFJE4iI1ss3dMVRIrEzFpn8ftje6iBfzBInqlnRrNhxcLwBEKjPPXQKzm1Ptlxtaiv9wdcj5w== + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 9d71bdd90..4ad12293a 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -835,6 +835,8 @@ - jc_hourly_rates - jobsizelimit - last_name_first + - localmediaserverhttp + - localmediaservernetwork - logo_img_path - md_categories - md_ccc_rates @@ -885,6 +887,7 @@ - tt_allow_post_to_invoiced - updated_at - use_fippa + - uselocalmediaserver - website - workingdays - zip_post @@ -922,6 +925,8 @@ - intakechecklist - jc_hourly_rates - last_name_first + - localmediaserverhttp + - localmediaservernetwork - logo_img_path - md_categories - md_ccc_rates @@ -965,6 +970,7 @@ - tt_allow_post_to_invoiced - updated_at - use_fippa + - uselocalmediaserver - website - workingdays - zip_post diff --git a/hasura/migrations/1652211838128_alter_table_public_bodyshops_add_column_uselocalmediaserver/down.sql b/hasura/migrations/1652211838128_alter_table_public_bodyshops_add_column_uselocalmediaserver/down.sql new file mode 100644 index 000000000..4395f22f6 --- /dev/null +++ b/hasura/migrations/1652211838128_alter_table_public_bodyshops_add_column_uselocalmediaserver/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."bodyshops" add column "uselocalmediaserver" boolean +-- not null default 'false'; diff --git a/hasura/migrations/1652211838128_alter_table_public_bodyshops_add_column_uselocalmediaserver/up.sql b/hasura/migrations/1652211838128_alter_table_public_bodyshops_add_column_uselocalmediaserver/up.sql new file mode 100644 index 000000000..58f6d87b8 --- /dev/null +++ b/hasura/migrations/1652211838128_alter_table_public_bodyshops_add_column_uselocalmediaserver/up.sql @@ -0,0 +1,2 @@ +alter table "public"."bodyshops" add column "uselocalmediaserver" boolean + not null default 'false'; diff --git a/hasura/migrations/1652211916491_alter_table_public_bodyshops_add_column_localmediaserverhttp/down.sql b/hasura/migrations/1652211916491_alter_table_public_bodyshops_add_column_localmediaserverhttp/down.sql new file mode 100644 index 000000000..406b46f36 --- /dev/null +++ b/hasura/migrations/1652211916491_alter_table_public_bodyshops_add_column_localmediaserverhttp/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."bodyshops" add column "localmediaserverhttp" text +-- null; diff --git a/hasura/migrations/1652211916491_alter_table_public_bodyshops_add_column_localmediaserverhttp/up.sql b/hasura/migrations/1652211916491_alter_table_public_bodyshops_add_column_localmediaserverhttp/up.sql new file mode 100644 index 000000000..f5d87f2a1 --- /dev/null +++ b/hasura/migrations/1652211916491_alter_table_public_bodyshops_add_column_localmediaserverhttp/up.sql @@ -0,0 +1,2 @@ +alter table "public"."bodyshops" add column "localmediaserverhttp" text + null; diff --git a/hasura/migrations/1652211955289_alter_table_public_bodyshops_add_column_localmediaservernetwork/down.sql b/hasura/migrations/1652211955289_alter_table_public_bodyshops_add_column_localmediaservernetwork/down.sql new file mode 100644 index 000000000..abce900d5 --- /dev/null +++ b/hasura/migrations/1652211955289_alter_table_public_bodyshops_add_column_localmediaservernetwork/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."bodyshops" add column "localmediaservernetwork" text +-- null; diff --git a/hasura/migrations/1652211955289_alter_table_public_bodyshops_add_column_localmediaservernetwork/up.sql b/hasura/migrations/1652211955289_alter_table_public_bodyshops_add_column_localmediaservernetwork/up.sql new file mode 100644 index 000000000..e5d80e90f --- /dev/null +++ b/hasura/migrations/1652211955289_alter_table_public_bodyshops_add_column_localmediaservernetwork/up.sql @@ -0,0 +1,2 @@ +alter table "public"."bodyshops" add column "localmediaservernetwork" text + null; diff --git a/package.json b/package.json index 36054c692..9fbecaa1f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "license": "UNLICENSED", "engines": { - "node": "12.22.6", + "node": "16.15.0", "npm": "7.17.0" }, "scripts": {