- Merge client update into test-beta

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-01-18 19:20:08 -05:00
696 changed files with 92291 additions and 107075 deletions

View File

@@ -1,26 +1,26 @@
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
import {DetermineFileType} from "../documents-upload/documents-upload.utility";
export const GenerateSrcUrl = (value) => {
let extension = value.extension;
if (extension && extension.toLowerCase().includes("heic")) extension = "jpg";
let extension = value.extension;
if (extension && extension.toLowerCase().includes("heic")) extension = "jpg";
return `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${DetermineFileType(
value.type
)}/upload/${value.key}${extension ? `.${extension}` : ""}`;
return `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${DetermineFileType(
value.type
)}/upload/${value.key}${extension ? `.${extension}` : ""}`;
};
export const GenerateThumbUrl = (value) => {
let extension = value.extension;
if (extension && extension.toLowerCase().includes("heic")) extension = "jpg";
else if (
DetermineFileType(value.type) !== "image" ||
(value.type && value.type.includes("application"))
)
extension = "jpg";
let extension = value.extension;
if (extension && extension.toLowerCase().includes("heic")) extension = "jpg";
else if (
DetermineFileType(value.type) !== "image" ||
(value.type && value.type.includes("application"))
)
extension = "jpg";
return `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${DetermineFileType(
value.type
)}/upload/${process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS}/${
value.key
}${extension ? `.${extension}` : ""}`;
return `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${DetermineFileType(
value.type
)}/upload/${process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS}/${
value.key
}${extension ? `.${extension}` : ""}`;
};

View File

@@ -1,145 +1,145 @@
import { Button, Space } from "antd";
import {Button, Space} from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {logImEXEvent} from "../../firebase/firebase.utils";
import cleanAxios from "../../utils/CleanAxios";
import formatBytes from "../../utils/formatbytes";
//import yauzl from "yauzl";
import { useTreatments } from "@splitsoftware/splitio-react";
import {useSplitTreatments} from "@splitsoftware/splitio-react";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(JobsDocumentsDownloadButton);
export function JobsDocumentsDownloadButton({
bodyshop,
galleryImages,
identifier,
}) {
const { t } = useTranslation();
const [download, setDownload] = useState(null);
const { Direct_Media_Download } = useTreatments(
["Direct_Media_Download"],
{},
bodyshop.imexshopid
);
const imagesToDownload = [
...galleryImages.images.filter((image) => image.isSelected),
...galleryImages.other.filter((image) => image.isSelected),
];
export function JobsDocumentsDownloadButton({bodyshop, galleryImages, identifier}) {
function downloadProgress(progressEvent) {
setDownload((currentDownloadState) => {
return {
downloaded: progressEvent.loaded || 0,
speed:
(progressEvent.loaded || 0) -
((currentDownloadState && currentDownloadState.downloaded) || 0),
};
});
}
const {t} = useTranslation();
const [download, setDownload] = useState(null);
const handleDownload = async () => {
logImEXEvent("jobs_documents_download");
const zipUrl = await axios({
url: "/media/download",
method: "POST",
//responseType: "arraybuffer", // Important
data: { ids: imagesToDownload.map((_) => _.key) },
const {treatments: {Direct_Media_Download}} = useSplitTreatments({
attributes: {},
names: ["Direct_Media_Download"],
splitKey: bodyshop.imexshopid,
});
const theDownloadedZip = await cleanAxios({
url: zipUrl.data,
method: "GET",
responseType: "arraybuffer",
onDownloadProgress: downloadProgress,
});
setDownload(null);
if (Direct_Media_Download.treatment === "on") {
try {
// const parentDir = await window.showDirectoryPicker({
// id: "media",
// startIn: "downloads",
// });
const imagesToDownload = [
...galleryImages.images.filter((image) => image.isSelected),
...galleryImages.other.filter((image) => image.isSelected),
];
// const directory = await parentDir.getDirectoryHandle(identifier, {
// create: true,
// });
// yauzl.fromBuffer(
// Buffer.from(theDownloadedZip.data),
// {},
// (err, zipFile) => {
// if (err) throw err;
// zipFile.on("entry", (entry) => {
// zipFile.openReadStream(entry, async (readErr, readStream) => {
// if (readErr) {
// zipFile.close();
// throw readErr;
// }
// if (err) throw err;
// let fileSystemHandle = await directory.getFileHandle(
// entry.fileName,
// {
// create: true,
// }
// );
// const writable = await fileSystemHandle.createWritable();
// readStream.on("data", async function (chunk) {
// await writable.write(chunk);
// });
// readStream.on("end", async function () {
// await writable.close();
// });
// });
// });
// }
// );
} catch (e) {
console.log(e);
standardMediaDownload(theDownloadedZip.data);
}
} else {
standardMediaDownload(theDownloadedZip.data);
function downloadProgress(progressEvent) {
setDownload((currentDownloadState) => {
return {
downloaded: progressEvent.loaded || 0,
speed:
(progressEvent.loaded || 0) -
((currentDownloadState && currentDownloadState.downloaded) || 0),
};
});
}
function standardMediaDownload(bufferData) {
const a = document.createElement("a");
const url = window.URL.createObjectURL(new Blob([bufferData]));
a.href = url;
a.download = `${identifier || "documents"}.zip`;
a.click();
}
};
return (
<>
<Button
loading={!!download}
disabled={imagesToDownload.length < 1}
onClick={handleDownload}
>
<Space>
<span>{t("documents.actions.download")}</span>
{download && (
<span>{`(${formatBytes(download.downloaded)} @ ${formatBytes(
download.speed
)} / second)`}</span>
)}
</Space>
</Button>
</>
);
const handleDownload = async () => {
logImEXEvent("jobs_documents_download");
const zipUrl = await axios({
url: "/media/download",
method: "POST",
//responseType: "arraybuffer", // Important
data: {ids: imagesToDownload.map((_) => _.key)},
});
const theDownloadedZip = await cleanAxios({
url: zipUrl.data,
method: "GET",
responseType: "arraybuffer",
onDownloadProgress: downloadProgress,
});
setDownload(null);
if (Direct_Media_Download.treatment === "on") {
try {
// const parentDir = await window.showDirectoryPicker({
// id: "media",
// startIn: "downloads",
// });
// const directory = await parentDir.getDirectoryHandle(identifier, {
// create: true,
// });
// yauzl.fromBuffer(
// Buffer.from(theDownloadedZip.data),
// {},
// (err, zipFile) => {
// if (err) throw err;
// zipFile.on("entry", (entry) => {
// zipFile.openReadStream(entry, async (readErr, readStream) => {
// if (readErr) {
// zipFile.close();
// throw readErr;
// }
// if (err) throw err;
// let fileSystemHandle = await directory.getFileHandle(
// entry.fileName,
// {
// create: true,
// }
// );
// const writable = await fileSystemHandle.createWritable();
// readStream.on("data", async function (chunk) {
// await writable.write(chunk);
// });
// readStream.on("end", async function () {
// await writable.close();
// });
// });
// });
// }
// );
} catch (e) {
console.log(e);
standardMediaDownload(theDownloadedZip.data);
}
} else {
standardMediaDownload(theDownloadedZip.data);
}
function standardMediaDownload(bufferData) {
const a = document.createElement("a");
const url = window.URL.createObjectURL(new Blob([bufferData]));
a.href = url;
a.download = `${identifier || "documents"}.zip`;
a.click();
}
};
return (
<>
<Button
loading={!!download}
disabled={imagesToDownload.length < 1}
onClick={handleDownload}
>
<Space>
<span>{t("documents.actions.download")}</span>
{download && (
<span>{`(${formatBytes(download.downloaded)} @ ${formatBytes(
download.speed
)} / second)`}</span>
)}
</Space>
</Button>
</>
);
}

View File

@@ -1,172 +1,172 @@
import { useApolloClient } from "@apollo/client";
import { Button, Form, notification, Popover, Space } from "antd";
import {useApolloClient} from "@apollo/client";
import {Button, Form, notification, Popover, Space} from "antd";
import axios from "axios";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { GET_DOC_SIZE_BY_JOB } from "../../graphql/documents.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import React, {useMemo, useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {GET_DOC_SIZE_BY_JOB} from "../../graphql/documents.queries";
import {selectBodyshop} from "../../redux/user/user.selectors";
import JobSearchSelect from "../job-search-select/job-search-select.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(JobsDocumentsGalleryReassign);
export function JobsDocumentsGalleryReassign({
bodyshop,
galleryImages,
callback,
}) {
const { t } = useTranslation();
const [form] = Form.useForm();
bodyshop,
galleryImages,
callback,
}) {
const {t} = useTranslation();
const [form] = Form.useForm();
const selectedImages = useMemo(() => {
return [
...galleryImages.images.filter((image) => image.isSelected),
...galleryImages.other.filter((image) => image.isSelected),
];
}, [galleryImages]);
const client = useApolloClient();
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const selectedImages = useMemo(() => {
return [
...galleryImages.images.filter((image) => image.isSelected),
...galleryImages.other.filter((image) => image.isSelected),
];
}, [galleryImages]);
const client = useApolloClient();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
// const updateImage = async (i, jobid) => {
// //Move the cloudinary image
// const updateImage = async (i, jobid) => {
// //Move the cloudinary image
// //Update it in the database.
// const result = await updateDocument({
// variables: {
// id: i.id,
// document: {
// key: i.public_id,
// jobid: jobid,
// },
// },
// });
// //Update it in the database.
// const result = await updateDocument({
// variables: {
// id: i.id,
// document: {
// key: i.public_id,
// jobid: jobid,
// },
// },
// });
// if (!!result.errors) {
// notification["error"]({
// message: t("documents.errors.updating", {
// message: JSON.stringify(result.errors),
// }),
// });
// } else {
// notification["success"]({
// message: t("documents.successes.updated"),
// });
// }
// };
// if (!!result.errors) {
// notification["error"]({
// message: t("documents.errors.updating", {
// message: JSON.stringify(result.errors),
// }),
// });
// } else {
// notification["success"]({
// message: t("documents.successes.updated"),
// });
// }
// };
const handleFinish = async ({ jobid }) => {
setLoading(true);
const handleFinish = async ({jobid}) => {
setLoading(true);
//Check to see if the space remaining on the new job is sufficient. If it isn't cancel this.
const newJobData = await client.query({
query: GET_DOC_SIZE_BY_JOB,
variables: { jobId: jobid },
});
//Check to see if the space remaining on the new job is sufficient. If it isn't cancel this.
const newJobData = await client.query({
query: GET_DOC_SIZE_BY_JOB,
variables: {jobId: jobid},
});
const transferedDocSizeTotal = selectedImages.reduce(
(acc, val) => acc + val.size,
0
const transferedDocSizeTotal = selectedImages.reduce(
(acc, val) => acc + val.size,
0
);
const shouldPreventTransfer =
bodyshop.jobsizelimit -
newJobData.data.documents_aggregate.aggregate.sum.size <
transferedDocSizeTotal;
if (shouldPreventTransfer) {
notification.open({
key: "cannotuploaddocuments",
type: "error",
message: t("documents.labels.reassign_limitexceeded_title"),
description: t("documents.labels.reassign_limitexceeded"),
});
setLoading(false);
return;
}
const res = await axios.post("/media/rename", {
tojobid: jobid,
documents: selectedImages.map((i) => {
//Need to check if the current key folder is null, or another job.
const currentKeys = i.key.split("/");
currentKeys[1] = jobid;
currentKeys.join("/");
return {
id: i.id,
from: i.key,
to: currentKeys.join("/"),
extension: i.extension,
type: i.type,
};
}),
});
//Add in confirmation & errors.
if (callback) callback();
if (res.errors) {
notification["error"]({
message: t("documents.errors.updating", {
message: JSON.stringify(res.errors),
}),
});
}
if (!res.mutationResult?.errors) {
notification["success"]({
message: t("documents.successes.updated"),
});
}
setOpen(false);
setLoading(false);
};
const popContent = (
<div>
<Form onFinish={handleFinish} layout="vertical" form={form}>
<Form.Item
label={t("documents.labels.newjobid")}
style={{width: "20rem"}}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={"jobid"}
>
<JobSearchSelect/>
</Form.Item>
</Form>
<Space>
<Button type="primary" onClick={() => form.submit()}>
{t("general.actions.submit")}
</Button>
<Button onClick={() => setOpen(false)}>
{t("general.actions.cancel")}
</Button>
</Space>
</div>
);
const shouldPreventTransfer =
bodyshop.jobsizelimit -
newJobData.data.documents_aggregate.aggregate.sum.size <
transferedDocSizeTotal;
if (shouldPreventTransfer) {
notification.open({
key: "cannotuploaddocuments",
type: "error",
message: t("documents.labels.reassign_limitexceeded_title"),
description: t("documents.labels.reassign_limitexceeded"),
});
setLoading(false);
return;
}
const res = await axios.post("/media/rename", {
tojobid: jobid,
documents: selectedImages.map((i) => {
//Need to check if the current key folder is null, or another job.
const currentKeys = i.key.split("/");
currentKeys[1] = jobid;
currentKeys.join("/");
return {
id: i.id,
from: i.key,
to: currentKeys.join("/"),
extension: i.extension,
type: i.type,
};
}),
});
//Add in confirmation & errors.
if (callback) callback();
if (res.errors) {
notification["error"]({
message: t("documents.errors.updating", {
message: JSON.stringify(res.errors),
}),
});
}
if (!res.mutationResult?.errors) {
notification["success"]({
message: t("documents.successes.updated"),
});
}
setVisible(false);
setLoading(false);
};
const popContent = (
<div>
<Form onFinish={handleFinish} layout="vertical" form={form}>
<Form.Item
label={t("documents.labels.newjobid")}
style={{ width: "20rem" }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={"jobid"}
>
<JobSearchSelect />
</Form.Item>
</Form>
<Space>
<Button type="primary" onClick={() => form.submit()}>
{t("general.actions.submit")}
</Button>
<Button onClick={() => setVisible(false)}>
{t("general.actions.cancel")}
</Button>
</Space>
</div>
);
return (
<Popover content={popContent} visible={visible}>
<Button
disabled={selectedImages.length < 1}
onClick={() => setVisible(true)}
loading={loading}
>
{t("documents.actions.reassign")}
</Button>
</Popover>
);
return (
<Popover content={popContent} open={open}>
<Button
disabled={selectedImages.length < 1}
onClick={() => setOpen(true)}
loading={loading}
>
{t("documents.actions.reassign")}
</Button>
</Popover>
);
}

View File

@@ -1,11 +1,11 @@
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";
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";
import DocumentsUploadComponent from "../documents-upload/documents-upload.component";
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
import {DetermineFileType} from "../documents-upload/documents-upload.utility";
import {GenerateSrcUrl, GenerateThumbUrl} from "./job-documents.utility";
import JobsDocumentsDownloadButton from "./jobs-document-gallery.download.component";
import JobsDocumentsGalleryReassign from "./jobs-document-gallery.reassign.component";
import JobsDocumentsDeleteButton from "./jobs-documents-gallery.delete.component";
@@ -15,237 +15,238 @@ import Lightbox from "react-image-lightbox";
import "react-image-lightbox/style.css";
function JobsDocumentsComponent({
data,
jobId,
refetch,
billId,
billsCallback,
totalSize,
downloadIdentifier,
ignoreSizeLimit,
}) {
const [galleryImages, setgalleryImages] = useState({ images: [], other: [] });
const { t } = useTranslation();
const [modalState, setModalState] = useState({ open: false, index: 0 });
data,
jobId,
refetch,
billId,
billsCallback,
totalSize,
downloadIdentifier,
ignoreSizeLimit,
}) {
const [galleryImages, setgalleryImages] = useState({images: [], other: []});
const {t} = useTranslation();
const [modalState, setModalState] = useState({open: false, index: 0});
useEffect(() => {
let documents = data.reduce(
(acc, value) => {
const fileType = DetermineFileType(value.type);
if (value.type.startsWith("image")) {
acc.images.push({
// src: GenerateSrcUrl(value),
src: GenerateThumbUrl(value),
// src: GenerateSrcUrl(value),
// thumbnail: GenerateThumbUrl(value),
fullsize: GenerateSrcUrl(value),
height: 225,
width: 225,
isSelected: false,
key: value.key,
extension: value.extension,
id: value.id,
type: value.type,
size: value.size,
tags: [{ value: value.type, title: value.type }],
});
} else {
let thumb;
switch (fileType) {
case "raw":
thumb = `${window.location.origin}/file.png`;
break;
default:
thumb = GenerateThumbUrl(value);
break;
}
useEffect(() => {
let documents = data.reduce(
(acc, value) => {
const fileType = DetermineFileType(value.type);
if (value.type.startsWith("image")) {
acc.images.push({
// src: GenerateSrcUrl(value),
src: GenerateThumbUrl(value),
// src: GenerateSrcUrl(value),
// thumbnail: GenerateThumbUrl(value),
fullsize: GenerateSrcUrl(value),
height: 225,
width: 225,
isSelected: false,
key: value.key,
extension: value.extension,
id: value.id,
type: value.type,
size: value.size,
tags: [{value: value.type, title: value.type}],
});
} else {
let thumb;
switch (fileType) {
case "raw":
thumb = `${window.location.origin}/file.png`;
break;
default:
thumb = GenerateThumbUrl(value);
break;
}
const fileName = value.key.split("/").pop();
acc.other.push({
source: GenerateSrcUrl(value),
src: thumb,
thumbnail: thumb,
tags: [
{
value: fileName,
title: fileName,
},
const fileName = value.key.split("/").pop();
acc.other.push({
source: GenerateSrcUrl(value),
src: thumb,
thumbnail: thumb,
tags: [
{
value: fileName,
title: fileName,
},
{ value: value.type, title: value.type },
...(value.bill
? [
{
value: value.bill.vendor.name,
title: t("vendors.fields.name"),
},
{ value: value.bill.date, title: t("bills.fields.date") },
{
value: value.bill.invoice_number,
title: t("bills.fields.invoice_number"),
},
]
: []),
],
height: 225,
width: 225,
isSelected: false,
extension: value.extension,
key: value.key,
id: value.id,
type: value.type,
size: value.size,
});
}
{value: value.type, title: value.type},
...(value.bill
? [
{
value: value.bill.vendor.name,
title: t("vendors.fields.name"),
},
{value: value.bill.date, title: t("bills.fields.date")},
{
value: value.bill.invoice_number,
title: t("bills.fields.invoice_number"),
},
]
: []),
],
height: 225,
width: 225,
isSelected: false,
extension: value.extension,
key: value.key,
id: value.id,
type: value.type,
size: value.size,
});
}
return acc;
},
{ images: [], other: [] }
return acc;
},
{images: [], other: []}
);
setgalleryImages(documents);
}, [data, setgalleryImages, t]);
return (
<div>
<Row gutter={[16, 16]}>
<Col span={24}>
<Space wrap>
<Button onClick={() => refetch && refetch()}>
<SyncOutlined/>
</Button>
<JobsDocumentsGallerySelectAllComponent
galleryImages={galleryImages}
setGalleryImages={setgalleryImages}
/>
<JobsDocumentsDownloadButton
galleryImages={galleryImages}
identifier={downloadIdentifier}
/>
<JobsDocumentsDeleteButton
galleryImages={galleryImages}
deletionCallback={billsCallback || refetch}
/>
{!billId && (
<JobsDocumentsGalleryReassign
galleryImages={galleryImages}
callback={refetch}
/>
)}
</Space>
</Col>
<Col span={24}>
<Card>
<DocumentsUploadComponent
jobId={jobId}
totalSize={totalSize}
billId={billId}
callbackAfterUpload={billsCallback || refetch}
ignoreSizeLimit={ignoreSizeLimit}
/>
</Card>
</Col>
<Col span={24}>
<Card title={t("jobs.labels.documents-images")}>
<Gallery
images={galleryImages.images}
onClick={(index, item) => {
setModalState({open: true, index: index});
// window.open(
// item.fullsize,
// "_blank",
// "toolbar=0,location=0,menubar=0"
// );
}}
onSelect={(index, image) => {
setgalleryImages({
...galleryImages,
images: galleryImages.images.map((g, idx) =>
index === idx ? {...g, isSelected: !g.isSelected} : g
),
});
}}
/>
</Card>
</Col>
<Col span={24}>
<Card title={t("jobs.labels.documents-other")}>
<Gallery
images={galleryImages.other}
thumbnailStyle={() => {
return {
backgroundImage: <FileExcelFilled/>,
height: "100%",
width: "100%",
cursor: "pointer",
};
}}
onClick={(index) => {
window.open(
galleryImages.other[index].source,
"_blank",
"toolbar=0,location=0,menubar=0"
);
}}
onSelect={(index) => {
setgalleryImages({
...galleryImages,
other: galleryImages.other.map((g, idx) =>
index === idx ? {...g, isSelected: !g.isSelected} : g
),
});
}}
/>
</Card>
</Col>
{modalState.open && (
<Lightbox
toolbarButtons={[
<EditFilled
onClick={() => {
const newWindow = window.open(
`${window.location.protocol}//${
window.location.host
}/edit?documentId=${
galleryImages.images[modalState.index].id
}`,
"_blank",
"noopener,noreferrer"
);
if (newWindow) newWindow.opener = null;
}}
/>,
]}
mainSrc={galleryImages.images[modalState.index].fullsize}
nextSrc={
galleryImages.images[
(modalState.index + 1) % galleryImages.images.length
].fullsize
}
prevSrc={
galleryImages.images[
(modalState.index + galleryImages.images.length - 1) %
galleryImages.images.length
].fullsize
}
onCloseRequest={() => setModalState({open: false, index: 0})}
onMovePrevRequest={() =>
setModalState({
...modalState,
index:
(modalState.index + galleryImages.images.length - 1) %
galleryImages.images.length,
})
}
onMoveNextRequest={() =>
setModalState({
...modalState,
index: (modalState.index + 1) % galleryImages.images.length,
})
}
/>
)}
</Row>
</div>
);
setgalleryImages(documents);
}, [data, setgalleryImages, t]);
return (
<div>
<Row gutter={[16, 16]}>
<Col span={24}>
<Space wrap>
<Button onClick={() => refetch && refetch()}>
<SyncOutlined />
</Button>
<JobsDocumentsGallerySelectAllComponent
galleryImages={galleryImages}
setGalleryImages={setgalleryImages}
/>
<JobsDocumentsDownloadButton
galleryImages={galleryImages}
identifier={downloadIdentifier}
/>
<JobsDocumentsDeleteButton
galleryImages={galleryImages}
deletionCallback={billsCallback || refetch}
/>
{!billId && (
<JobsDocumentsGalleryReassign
galleryImages={galleryImages}
callback={refetch}
/>
)}
</Space>
</Col>
<Col span={24}>
<Card>
<DocumentsUploadComponent
jobId={jobId}
totalSize={totalSize}
billId={billId}
callbackAfterUpload={billsCallback || refetch}
ignoreSizeLimit={ignoreSizeLimit}
/>
</Card>
</Col>
<Col span={24}>
<Card title={t("jobs.labels.documents-images")}>
<Gallery
images={galleryImages.images}
onClick={(index, item) => {
setModalState({ open: true, index: index });
// window.open(
// item.fullsize,
// "_blank",
// "toolbar=0,location=0,menubar=0"
// );
}}
onSelect={(index, image) => {
setgalleryImages({
...galleryImages,
images: galleryImages.images.map((g, idx) =>
index === idx ? { ...g, isSelected: !g.isSelected } : g
),
});
}}
/>
</Card>
</Col>
<Col span={24}>
<Card title={t("jobs.labels.documents-other")}>
<Gallery
images={galleryImages.other}
thumbnailStyle={() => {
return {
backgroundImage: <FileExcelFilled />,
height: "100%",
width: "100%",
cursor: "pointer",
};
}}
onClick={(index) => {
window.open(
galleryImages.other[index].source,
"_blank",
"toolbar=0,location=0,menubar=0"
);
}}
onSelect={(index) => {
setgalleryImages({
...galleryImages,
other: galleryImages.other.map((g, idx) =>
index === idx ? { ...g, isSelected: !g.isSelected } : g
),
});
}}
/>
</Card>
</Col>
{modalState.open && (
<Lightbox
toolbarButtons={[
<EditFilled
onClick={() => {
const newWindow = window.open(
`${window.location.protocol}//${
window.location.host
}/edit?documentId=${
galleryImages.images[modalState.index].id
}`,
"_blank",
"noopener,noreferrer"
);
if (newWindow) newWindow.opener = null;
}}
/>,
]}
mainSrc={galleryImages.images[modalState.index].fullsize}
nextSrc={
galleryImages.images[
(modalState.index + 1) % galleryImages.images.length
].fullsize
}
prevSrc={
galleryImages.images[
(modalState.index + galleryImages.images.length - 1) %
galleryImages.images.length
].fullsize
}
onCloseRequest={() => setModalState({ open: false, index: 0 })}
onMovePrevRequest={() =>
setModalState({
...modalState,
index:
(modalState.index + galleryImages.images.length - 1) %
galleryImages.images.length,
})
}
onMoveNextRequest={() =>
setModalState({
...modalState,
index: (modalState.index + 1) % galleryImages.images.length,
})
}
/>
)}
</Row>
</div>
);
}
export default JobsDocumentsComponent;

View File

@@ -1,35 +1,35 @@
import { useQuery } from "@apollo/client";
import {useQuery} from "@apollo/client";
import React from "react";
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
import {GET_DOCUMENTS_BY_JOB} from "../../graphql/documents.queries";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobDocuments from "./jobs-documents-gallery.component";
export default function JobsDocumentsContainer({
jobId,
billId,
documentsList,
billsCallback,
}) {
const { loading, error, data, refetch } = useQuery(GET_DOCUMENTS_BY_JOB, {
variables: { jobId: jobId },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !!billId,
});
jobId,
billId,
documentsList,
billsCallback,
}) {
const {loading, error, data, refetch} = useQuery(GET_DOCUMENTS_BY_JOB, {
variables: {jobId: jobId},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !!billId,
});
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent type="error" message={error.message} />;
if (loading) return <LoadingSpinner/>;
if (error) return <AlertComponent type="error" message={error.message}/>;
return (
<JobDocuments
data={(data && data.documents) || documentsList || []}
downloadIdentifier={data && data.jobs_by_pk.ro_number}
totalSize={data && data.documents_aggregate.aggregate.sum.size}
billId={billId}
jobId={jobId}
refetch={refetch}
billsCallback={billsCallback}
/>
);
return (
<JobDocuments
data={(data && data.documents) || documentsList || []}
downloadIdentifier={data && data.jobs_by_pk.ro_number}
totalSize={data && data.documents_aggregate.aggregate.sum.size}
billId={billId}
jobId={jobId}
refetch={refetch}
billsCallback={billsCallback}
/>
);
}

View File

@@ -1,62 +1,62 @@
import { QuestionCircleOutlined } from "@ant-design/icons";
import { Button, notification, Popconfirm } from "antd";
import {QuestionCircleOutlined} from "@ant-design/icons";
import {Button, notification, Popconfirm} from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {logImEXEvent} from "../../firebase/firebase.utils";
//Context: currentUserEmail, bodyshop, jobid, invoiceid
export default function JobsDocumentsDeleteButton({
galleryImages,
deletionCallback,
}) {
const { t } = useTranslation();
galleryImages,
deletionCallback,
}) {
const {t} = useTranslation();
const imagesToDelete = [
...galleryImages.images.filter((image) => image.isSelected),
...galleryImages.other.filter((image) => image.isSelected),
];
const [loading, setLoading] = useState(false);
const imagesToDelete = [
...galleryImages.images.filter((image) => image.isSelected),
...galleryImages.other.filter((image) => image.isSelected),
];
const [loading, setLoading] = useState(false);
const handleDelete = async () => {
logImEXEvent("job_documents_delete", { count: imagesToDelete.length });
setLoading(true);
const res = await axios.post("/media/delete", {
ids: imagesToDelete,
});
const handleDelete = async () => {
logImEXEvent("job_documents_delete", {count: imagesToDelete.length});
setLoading(true);
const res = await axios.post("/media/delete", {
ids: imagesToDelete,
});
if (res.data.error) {
notification["error"]({
message: t("documents.errors.deleting", {
error: JSON.stringify(res.data.error.response.errors),
}),
});
} else {
notification.open({
key: "docdeletedsuccesfully",
type: "success",
message: t("documents.successes.delete"),
});
if (res.data.error) {
notification["error"]({
message: t("documents.errors.deleting", {
error: JSON.stringify(res.data.error.response.errors),
}),
});
} else {
notification.open({
key: "docdeletedsuccesfully",
type: "success",
message: t("documents.successes.delete"),
});
if (deletionCallback) deletionCallback();
}
if (deletionCallback) deletionCallback();
}
setLoading(false);
};
setLoading(false);
};
return (
<Popconfirm
disabled={imagesToDelete.length < 1}
icon={<QuestionCircleOutlined style={{ color: "red" }} />}
onConfirm={handleDelete}
title={t("documents.labels.confirmdelete")}
okText={t("general.actions.delete")}
okButtonProps={{ type: "danger" }}
cancelText={t("general.actions.cancel")}
>
<Button disabled={imagesToDelete.length < 1} loading={loading}>
{t("documents.actions.delete")}
</Button>
</Popconfirm>
);
return (
<Popconfirm
disabled={imagesToDelete.length < 1}
icon={<QuestionCircleOutlined style={{color: "red"}}/>}
onConfirm={handleDelete}
title={t("documents.labels.confirmdelete")}
okText={t("general.actions.delete")}
okButtonProps={{type: "danger"}}
cancelText={t("general.actions.cancel")}
>
<Button disabled={imagesToDelete.length < 1} loading={loading}>
{t("documents.actions.delete")}
</Button>
</Popconfirm>
);
}

View File

@@ -1,53 +1,54 @@
import React, { useEffect } from "react";
import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
import React, {useEffect} from "react";
import {Gallery} from "react-grid-gallery";
import {useTranslation} from "react-i18next";
import {GenerateSrcUrl, GenerateThumbUrl} from "./job-documents.utility";
function JobsDocumentGalleryExternal({
data,
data,
externalMediaState,
}) {
const [galleryImages, setgalleryImages] = externalMediaState;
const { t } = useTranslation();
externalMediaState,
}) {
const [galleryImages, setgalleryImages] = externalMediaState;
const {t} = useTranslation();
useEffect(() => {
let documents = data.reduce((acc, value) => {
if (value.type.startsWith("image")) {
acc.push({
fullsize: GenerateSrcUrl(value),
src: GenerateThumbUrl(value),
thumbnailHeight: 225,
thumbnailWidth: 225,
isSelected: false,
key: value.key,
extension: value.extension,
id: value.id,
type: value.type,
tags: [{ value: value.type, title: value.type }],
size: value.size,
});
}
useEffect(() => {
let documents = data.reduce((acc, value) => {
if (value.type.startsWith("image")) {
acc.push({
fullsize: GenerateSrcUrl(value),
src: GenerateThumbUrl(value),
thumbnailHeight: 225,
thumbnailWidth: 225,
isSelected: false,
key: value.key,
extension: value.extension,
id: value.id,
type: value.type,
tags: [{value: value.type, title: value.type}],
size: value.size,
});
}
return acc;
}, []);
setgalleryImages(documents);
}, [data, setgalleryImages, t]);
return acc;
}, []);
setgalleryImages(documents);
}, [data, setgalleryImages, t]);
return (
<div className="clearfix">
<Gallery
images={galleryImages}
backdropClosesModal={true}
onSelect={(index, image) => {
setgalleryImages(
galleryImages.map((g, idx) =>
index === idx ? { ...g, isSelected: !g.isSelected } : g
)
);
}}
/>
</div>
);
return (
<div className="clearfix">
<Gallery
images={galleryImages}
backdropClosesModal={true}
onSelect={(index, image) => {
setgalleryImages(
galleryImages.map((g, idx) =>
index === idx ? {...g, isSelected: !g.isSelected} : g
)
);
}}
/>
</div>
);
}
export default JobsDocumentGalleryExternal;

View File

@@ -1,67 +1,67 @@
import { Button, Space } from "antd";
import {Button, Space} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import {useTranslation} from "react-i18next";
export default function JobsDocumentsGallerySelectAllComponent({
galleryImages,
setGalleryImages,
}) {
const { t } = useTranslation();
galleryImages,
setGalleryImages,
}) {
const {t} = useTranslation();
const handleSelectAll = () => {
setGalleryImages({
...galleryImages,
other: galleryImages.other.map((i) => {
return { ...i, isSelected: true };
}),
images: galleryImages.images.map((i) => {
return { ...i, isSelected: true };
}),
});
};
const handleSelectAllImages = () => {
setGalleryImages({
...galleryImages,
const handleSelectAll = () => {
setGalleryImages({
...galleryImages,
other: galleryImages.other.map((i) => {
return {...i, isSelected: true};
}),
images: galleryImages.images.map((i) => {
return {...i, isSelected: true};
}),
});
};
const handleSelectAllImages = () => {
setGalleryImages({
...galleryImages,
images: galleryImages.images.map((i) => {
return { ...i, isSelected: true };
}),
});
};
const handleSelectAllDocuments = () => {
setGalleryImages({
...galleryImages,
other: galleryImages.other.map((i) => {
return { ...i, isSelected: true };
}),
});
};
const handleDeselectAll = () => {
setGalleryImages({
...galleryImages,
other: galleryImages.other.map((i) => {
return { ...i, isSelected: false };
}),
images: galleryImages.images.map((i) => {
return { ...i, isSelected: false };
}),
});
};
images: galleryImages.images.map((i) => {
return {...i, isSelected: true};
}),
});
};
const handleSelectAllDocuments = () => {
setGalleryImages({
...galleryImages,
other: galleryImages.other.map((i) => {
return {...i, isSelected: true};
}),
});
};
const handleDeselectAll = () => {
setGalleryImages({
...galleryImages,
other: galleryImages.other.map((i) => {
return {...i, isSelected: false};
}),
images: galleryImages.images.map((i) => {
return {...i, isSelected: false};
}),
});
};
return (
<Space wrap>
<Button onClick={handleSelectAll}>
{t("general.actions.selectall")}
</Button>
<Button onClick={handleSelectAllImages}>
{t("documents.actions.selectallimages")}
</Button>
<Button onClick={handleSelectAllDocuments}>
{t("documents.actions.selectallotherdocuments")}
</Button>
<Button onClick={handleDeselectAll}>
{t("general.actions.deselectall")}
</Button>
</Space>
);
return (
<Space wrap>
<Button onClick={handleSelectAll}>
{t("general.actions.selectall")}
</Button>
<Button onClick={handleSelectAllImages}>
{t("documents.actions.selectallimages")}
</Button>
<Button onClick={handleSelectAllDocuments}>
{t("documents.actions.selectallotherdocuments")}
</Button>
<Button onClick={handleDeselectAll}>
{t("general.actions.deselectall")}
</Button>
</Space>
);
}