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/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..cd42f6667
--- /dev/null
+++ b/client/src/components/documents-local-upload/documents-local-upload.component.jsx
@@ -0,0 +1,67 @@
+import { UploadOutlined } from "@ant-design/icons";
+import { Result, Upload } from "antd";
+import React, { useEffect, useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
+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,
+ callbackAfterUpload,
+}) {
+ const { t } = useTranslation();
+ 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,
+ 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..020425515
--- /dev/null
+++ b/client/src/components/documents-local-upload/documents-local-upload.utility.js
@@ -0,0 +1,56 @@
+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, 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);
+ formData.append("file", file);
+ const bodyshop = store.getState().user.bodyshop;
+
+ const imexMediaServerResponse = await cleanAxios.post(
+ normalizeUrl(`${bodyshop.localmediaserverhttp}/jobs/upload`),
+ formData,
+ {
+ ...options,
+ }
+ );
+
+ if (imexMediaServerResponse.status !== 200) {
+ if (!!onError) {
+ onError(imexMediaServerResponse.statusText);
+ }
+ } else {
+ onSuccess(file);
+ store.dispatch(
+ addMediaForJob({
+ jobid,
+ media: imexMediaServerResponse.data.map((d) => {
+ return {
+ ...d,
+ src: normalizeUrl(`${bodyshop.localmediaserverhttp}/${d.src}`),
+ thumbnail: normalizeUrl(
+ `${bodyshop.localmediaserverhttp}/${d.thumbnail}`
+ ),
+ };
+ }),
+ })
+ );
+ }
+
+ if (callbackAfterUpload) {
+ callbackAfterUpload();
+ }
+};
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..e67b53e77
--- /dev/null
+++ b/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx
@@ -0,0 +1,73 @@
+import React, { useEffect } from "react";
+import { SyncOutlined } from "@ant-design/icons";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import { selectAllMedia } from "../../redux/media/media.selectors";
+import { getJobMedia } from "../../redux/media/media.actions";
+import { Button, Card, Space } from "antd";
+import { useTranslation } from "react-i18next";
+import Gallery from "react-grid-gallery";
+import DocumentsLocalUploadComponent from "../documents-local-upload/documents-local-upload.component";
+import { Link } from "react-router-dom";
+const mapStateToProps = createStructuredSelector({
+ bodyshop: selectBodyshop,
+ allMedia: selectAllMedia,
+});
+const mapDispatchToProps = (dispatch) => ({
+ getJobMedia: (id) => dispatch(getJobMedia(id)),
+});
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(JobsDocumentsLocalGallery);
+
+export function JobsDocumentsLocalGallery({
+ bodyshop,
+ getJobMedia,
+ allMedia,
+ job,
+}) {
+ const { t } = useTranslation();
+ useEffect(() => {
+ if (job) {
+ getJobMedia(job.id);
+ }
+ }, [job, getJobMedia]);
+
+ return (
+
+ );
+}
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 ? (
+
+ ) : (
+
+ )}
({
+ type: MediaActionTypes.GET_MEDIA_FOR_JOB,
+ payload: jobid,
+});
+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 },
+});
diff --git a/client/src/redux/media/media.reducer.js b/client/src/redux/media/media.reducer.js
new file mode 100644
index 000000000..8e7761f88
--- /dev/null
+++ b/client/src/redux/media/media.reducer.js
@@ -0,0 +1,24 @@
+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],
+ ...(action.payload.media || []),
+ ],
+ };
+ 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..dcef73625
--- /dev/null
+++ b/client/src/redux/media/media.sagas.js
@@ -0,0 +1,66 @@
+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}`
+ ),
+
+ key: idx,
+ };
+ }),
+ ...documentsFetch.data.map((d, idx) => {
+ return {
+ ...d,
+ src: normalizeUrl(`${localmediaserverhttp}/${d.src}`),
+ thumbnail: normalizeUrl(
+ `${localmediaserverhttp}/${d.thumbnail}`
+ ),
+
+ key: idx,
+ };
+ }),
+ ],
+ })
+ );
+ }
+ } catch (error) {
+ yield put(getJobMediaError(error));
+ }
+}
+
+export function* mediaSagas() {
+ yield all([call(onSetJobMedia)]);
+}
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..aa3d7ba11
--- /dev/null
+++ b/client/src/redux/media/media.types.js
@@ -0,0 +1,10 @@
+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",
+ 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",
+};
+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/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/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": {