equipment documentation

main
alex 2023-08-26 00:13:48 +02:00
parent 5116def685
commit f056e645f4
7 changed files with 796 additions and 585 deletions

View File

@ -41,6 +41,15 @@ export default function AppRoutes() {
} }
element={<EquipmentDocumentation isEquipmentViewModalOpen={true} />} element={<EquipmentDocumentation isEquipmentViewModalOpen={true} />}
/> />
<Route
path={
Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION + "/:paramStockItemId"
}
element={<ViewEquipmentDocumentations />}
/>
*/ */
return ( return (
@ -52,13 +61,6 @@ export default function AppRoutes() {
element={<EquipmentDocumentationOverview />} element={<EquipmentDocumentationOverview />}
/> />
<Route
path={
Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION + "/:paramStockItemId"
}
element={<ViewEquipmentDocumentations />}
/>
<Route <Route
path={Constants.ROUTE_PATHS.GROUP_TASKS} path={Constants.ROUTE_PATHS.GROUP_TASKS}
element={<GroupTasks isGroupTasksViewModalOpen={false} />} element={<GroupTasks isGroupTasksViewModalOpen={false} />}

View File

@ -104,13 +104,17 @@ export function MyModalOnlyCloseButtonFooter({ onCancel }) {
return <Button onClick={onCancel}>{t("common.button.close")}</Button>; return <Button onClick={onCancel}>{t("common.button.close")}</Button>;
} }
export function MyModalCloseSaveButtonFooter({ onCancel, onSave }) { export function MyModalCloseSaveButtonFooter({
onCancel,
onSave,
isSaveButtonLoading,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <>
<Button onClick={onCancel}>{t("common.button.close")}</Button> <Button onClick={onCancel}>{t("common.button.close")}</Button>
<Button onClick={onSave} type="primary"> <Button onClick={onSave} type="primary" loading={isSaveButtonLoading}>
{t("common.button.save")} {t("common.button.save")}
</Button> </Button>
</> </>

View File

@ -1,228 +1,43 @@
import { useParams } from "react-router-dom"; import { Button } from "antd";
import MyModal, { import { useState } from "react";
MyModalCloseCreateButtonFooter, import { PlusOutlined } from "@ant-design/icons";
} from "../../Components/MyModal"; import { EquipmentDocumentationViewEditComponent } from ".";
import { AppStyle, Constants, myFetch } from "../../utils";
import { Button, Card, Col, Row, Select, Typography } from "antd";
import { useRef, useState } from "react";
import TextArea from "antd/es/input/TextArea";
import Webcam from "react-webcam";
import {
CameraOutlined,
DeleteOutlined,
FullscreenOutlined,
PlusOutlined,
} from "@ant-design/icons";
import MyImage from "../../Components/MyImage";
function UploadComponent({ index, setImagePreview }) { export default function CreateEquipmentDocumentationModal({
const handleFileChange = (event) => { stockItemId,
const file = event.target.files[0]; fetchDocumentation,
buttonBlock,
if (file) { }) {
const reader = new FileReader(); const [isOpen, setIsOpen] = useState(false);
reader.onload = () => {
//setImagePreview(URL.createObjectURL(file));
setImagePreview(reader.result);
};
reader.readAsDataURL(file);
}
};
return (
<div>
<input
type="file"
accept="image/*"
id={`upload-input-${index}`}
style={{ display: "none" }}
onChange={handleFileChange}
/>
<label
htmlFor={`upload-input-${index}`}
style={{ cursor: "pointer", display: "inline-block" }}
>
<PlusOutlined />
</label>
</div>
);
}
function CameraComponent({ setImagePreview }) {
const [modalVisible, setModalVisible] = useState(false);
const [cameraVisible, setCameraVisible] = useState(false);
const webcamRef = useRef(null);
const handleCapture = () => {
const imageSrc = webcamRef.current.getScreenshot();
setImagePreview(imageSrc);
handleCancel();
};
const handleCancel = () => {
setCameraVisible(false);
// this is needed to close the camera correctly
setTimeout(() => setModalVisible(false), 100);
};
return ( return (
<> <>
<div <Button
onClick={() => { block={buttonBlock}
setModalVisible(true); type="primary"
setCameraVisible(true); icon={<PlusOutlined />}
}} onClick={() => setIsOpen(true)}
> >
<CameraOutlined /> Create documentation
</div> </Button>
<MyModal <EquipmentDocumentationViewEditComponent
isOpen={modalVisible} createMode
onCancel={handleCancel} isOpen={isOpen}
footer={[ onCancel={() => setIsOpen(false)}
<Button key={0} block type="primary" onClick={handleCapture}> stockItemId={stockItemId}
Take picture fetchDocumentation={fetchDocumentation}
</Button>, />
]}
>
{cameraVisible && (
<Webcam
audio={false}
ref={webcamRef}
screenshotFormat="image/jpeg"
width={"100%"}
videoConstraints={{
width: 1920,
height: 1080,
//facingMode: { exact: "environment" },
}}
/>
)}
</MyModal>
</> </>
); );
} }
export function NoteComponent({ /*
viewMode,
index,
image,
onImageChange,
description,
onDescriptionChange,
onDeleteImage,
imageDocumentationId,
}) {
const [isImageFullScreenModalOpen, setIsImageFullScreenModalOpen] =
useState(false);
const imageSrc = imageDocumentationId
? `${Constants.STATIC_CONTENT_ADDRESS}equipmentdocumentation/${imageDocumentationId}/${image}`
: image;
const FullscreenOutlinedIcon = ({ disabled }) => (
<FullscreenOutlined
disabled={true}
onClick={() => {
if (disabled) return;
setIsImageFullScreenModalOpen(true);
}}
/>
);
return (
<div style={{ marginBottom: AppStyle.app.margin }}>
<Row gutter={[AppStyle.grid.row.gutter[0]]}>
<Col xs={24} md={8}>
<Card
bodyStyle={{ padding: 0 }}
actions={
viewMode
? image
? [<FullscreenOutlinedIcon />]
: null
: [
<UploadComponent
index={index}
setImagePreview={onImageChange}
/>,
<CameraComponent setImagePreview={onImageChange} />,
<DeleteOutlined onClick={onDeleteImage} />,
<FullscreenOutlinedIcon disabled={image === null} />,
]
}
>
{!image ? (
<div
style={{
height: 250,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
{viewMode ? "No image" : "No image selected"}
</div>
) : (
<MyImage
src={imageSrc}
width="100%"
style={{
borderTopLeftRadius: 6,
borderTopRightRadius: 6,
}}
alt="Preview"
/>
)}
</Card>
</Col>
<Col xs={24} md={16}>
{viewMode ? (
<Typography.Text>{description}</Typography.Text>
) : (
<TextArea
rows={8}
placeholder="Description"
value={description}
onChange={onDescriptionChange}
/>
)}
</Col>
</Row>
<ImageFullscreenModal
isOpen={isImageFullScreenModalOpen}
onCancel={() => setIsImageFullScreenModalOpen(false)}
image={imageSrc}
/>
</div>
);
}
function ImageFullscreenModal({ isOpen, onCancel, image }) {
return (
<MyModal isOpen={isOpen} onCancel={onCancel}>
<img width="100%" src={image} alt="Fullscreen preview" />
</MyModal>
);
}
const emptyNote = { image: null, description: "" };
const selectDocumentationTypeOptions = [
{ value: 0, label: "Repair protocol" },
{ value: 1, label: "Documentation" },
];
export default function CreateEquipmentDocumentationModal({ export default function CreateEquipmentDocumentationModal({
scannerResult,
fetchDocumentation, fetchDocumentation,
buttonBlock, buttonBlock,
}) { }) {
let { paramStockItemId } = useParams();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [title, setTitle] = useState("New documentation"); const [title, setTitle] = useState("New documentation");
const [selectedDocumentationType, setSelectedDocumentationType] = useState( const [selectedDocumentationType, setSelectedDocumentationType] = useState(
@ -246,7 +61,7 @@ export default function CreateEquipmentDocumentationModal({
}); });
let body = { let body = {
stockItemId: paramStockItemId, stockItemId: scannerResult,
type: selectedDocumentationType, type: selectedDocumentationType,
title: title, title: title,
notes: updatedNotes, notes: updatedNotes,
@ -340,7 +155,8 @@ export default function CreateEquipmentDocumentationModal({
</div> </div>
<Select <Select
defaultValue={selectedDocumentationType} //defaultValue={selectedDocumentationType}
value={selectedDocumentationType}
style={{ width: "100%", marginBottom: AppStyle.app.margin }} style={{ width: "100%", marginBottom: AppStyle.app.margin }}
onChange={(value) => setSelectedDocumentationType(value)} onChange={(value) => setSelectedDocumentationType(value)}
options={selectDocumentationTypeOptions} options={selectDocumentationTypeOptions}
@ -355,9 +171,6 @@ export default function CreateEquipmentDocumentationModal({
description={note.description} description={note.description}
onDescriptionChange={handleDescriptionChange(index)} onDescriptionChange={handleDescriptionChange(index)}
onDeleteImage={() => handleImageChange(index)(null)} onDeleteImage={() => handleImageChange(index)(null)}
onImageFullscreen={() => {
console.log("onImageFullscreen");
}}
/> />
))} ))}
@ -375,3 +188,4 @@ export default function CreateEquipmentDocumentationModal({
</> </>
); );
} }
*/

View File

@ -0,0 +1,26 @@
import { EditOutlined } from "@ant-design/icons";
import { useState } from "react";
import { EquipmentDocumentationViewEditComponent } from ".";
export default function EditEquipmentDocumentationModal({
stockItemId,
documentationId,
fetchDocumentation,
}) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<EditOutlined style={{ fontSize: 24 }} onClick={() => setIsOpen(true)} />
<EquipmentDocumentationViewEditComponent
createMode={false}
isOpen={isOpen}
onCancel={() => setIsOpen(false)}
stockItemId={stockItemId}
documentationId={documentationId}
fetchDocumentation={fetchDocumentation}
/>
</>
);
}

View File

@ -1,5 +1,4 @@
import { import {
Breadcrumb,
Button, Button,
Card, Card,
Col, Col,
@ -10,23 +9,18 @@ import {
Typography, Typography,
} from "antd"; } from "antd";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import MyImage from "../../Components/MyImage"; import CreateEquipmentDocumentationModal from "./CreateEquipmentDocumentationModal";
import CreateEquipmentDocumentationModal, {
NoteComponent,
} from "./CreateEquipmentDocumentationModal";
import { AppStyle, Constants, FormatDatetime, myFetch } from "../../utils"; import { AppStyle, Constants, FormatDatetime, myFetch } from "../../utils";
import { import {
BookOutlined, BookOutlined,
EditOutlined,
InfoCircleOutlined, InfoCircleOutlined,
ToolOutlined, ToolOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import EditEquipmentDocumentationModal from "./EditEquipmentDocumentationModal";
import { NoteComponent } from ".";
export default function ViewEquipmentDocumentations() { export default function ViewEquipmentDocumentations({ scannerResult }) {
let { paramStockItemId } = useParams();
const navigate = useNavigate();
const [equipmentDocumentationResponse, setEquipmentDocumentationResponse] = const [equipmentDocumentationResponse, setEquipmentDocumentationResponse] =
useState(null); useState(null);
const [isEquipmentDocumentationLoading, setIsEquipmentDocumentationLoading] = const [isEquipmentDocumentationLoading, setIsEquipmentDocumentationLoading] =
@ -35,7 +29,7 @@ export default function ViewEquipmentDocumentations() {
const fetchDocumentation = () => { const fetchDocumentation = () => {
setIsEquipmentDocumentationLoading(true); setIsEquipmentDocumentationLoading(true);
myFetch(`/equipment/documentation/${paramStockItemId}`, "GET").then( myFetch(`/equipment/documentations/${scannerResult}`, "GET").then(
(data) => { (data) => {
console.log("data", data); console.log("data", data);
@ -54,25 +48,55 @@ export default function ViewEquipmentDocumentations() {
}; };
useEffect(() => { useEffect(() => {
console.log("paramStockItemId", paramStockItemId); console.log("scannerResult", scannerResult);
fetchDocumentation(); fetchDocumentation();
}, [paramStockItemId]); }, [scannerResult]);
const CreateDocumentationButton = () => { const CreateDocumentationButton = () => {
const InvexStockItemThumbnail = ({ width }) => {
return (
<img
src={`${Constants.API_ADDRESS}/equipment/thumbnail/${scannerResult}`}
width={width}
style={{ borderRadius: 6 }}
/>
);
};
return ( return (
<Row style={{ alignItems: "center" }}> <Row style={{ alignItems: "center" }}>
<Col xs={24} sm={{ span: 8 }} md={{ span: 6 }}> <Col
<MyImage xs={24}
src={ sm={{ span: 8 }}
"http://localhost:50050/v1/equipment/thumbnail/media/part_images/part_153_image.thumbnail.png" md={{ span: 6 }}
} style={{
width={64} display: "flex",
/> flexDirection: "row",
gap: 10,
alignItems: "center",
}}
>
{equipmentDocumentationResponse.Documentations.length !== 0 && (
<Popover
placement="right"
trigger="hover"
content={<InvexStockItemThumbnail width={256} />}
>
<>
<InvexStockItemThumbnail width={64} />
</>
</Popover>
)}
<Typography.Title level={4} style={{ margin: 0 }}>
{scannerResult}
</Typography.Title>
</Col> </Col>
<Col xs={24} sm={{ span: 8, offset: 8 }} md={{ span: 6, offset: 12 }}> <Col xs={24} sm={{ span: 8, offset: 8 }} md={{ span: 6, offset: 12 }}>
<CreateEquipmentDocumentationModal <CreateEquipmentDocumentationModal
stockItemId={scannerResult}
fetchDocumentation={fetchDocumentation} fetchDocumentation={fetchDocumentation}
buttonBlock={true} buttonBlock={true}
/> />
@ -144,9 +168,10 @@ export default function ViewEquipmentDocumentations() {
/> />
</Popover> </Popover>
<EditOutlined <EditEquipmentDocumentationModal
style={{ fontSize: 24 }} stockItemId={scannerResult}
onClick={() => console.log("edit")} documentationId={documentation.Id}
fetchDocumentation={fetchDocumentation}
/> />
</div> </div>
</div> </div>
@ -160,7 +185,7 @@ export default function ViewEquipmentDocumentations() {
key={index} key={index}
viewMode viewMode
image={note.Image} image={note.Image}
imageDocumentationId={documentation.Id} documentationId={documentation.Id}
description={note.Description} description={note.Description}
/> />
); );
@ -193,17 +218,6 @@ export default function ViewEquipmentDocumentations() {
status="403" status="403"
title="401" title="401"
subTitle="The backend server is not authorized to access invex." subTitle="The backend server is not authorized to access invex."
extra={
<>
<Button
onClick={() =>
navigate(Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION)
}
>
Back to Overview
</Button>
</>
}
/> />
); );
} }
@ -215,17 +229,6 @@ export default function ViewEquipmentDocumentations() {
status="500" status="500"
title="500" title="500"
subTitle="The scanned item doesn't exists on invex" subTitle="The scanned item doesn't exists on invex"
extra={
<>
<Button
onClick={() =>
navigate(Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION)
}
>
Back to Overview
</Button>
</>
}
/> />
); );
} }
@ -237,16 +240,9 @@ export default function ViewEquipmentDocumentations() {
title="404" title="404"
subTitle="Sorry, for the equipment does not exist an documentation." subTitle="Sorry, for the equipment does not exist an documentation."
extra={[ extra={[
<Button
key="0"
onClick={() =>
navigate(Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION)
}
>
Back to Overview
</Button>,
<CreateEquipmentDocumentationModal <CreateEquipmentDocumentationModal
key="1" key="0"
stockItemId={scannerResult}
fetchDocumentation={fetchDocumentation} fetchDocumentation={fetchDocumentation}
buttonBlock={false} buttonBlock={false}
/>, />,
@ -257,20 +253,6 @@ export default function ViewEquipmentDocumentations() {
return ( return (
<> <>
<Breadcrumb
style={{ marginBottom: AppStyle.app.margin }}
items={[
{
title: (
<Link to={Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION}>
Overview
</Link>
),
},
{ title: `Stock Item ${paramStockItemId}` },
]}
/>
<CreateDocumentationButton /> <CreateDocumentationButton />
{equipmentDocumentationResponse.Documentations.map( {equipmentDocumentationResponse.Documentations.map(
@ -284,9 +266,9 @@ export default function ViewEquipmentDocumentations() {
function DocumentationTypeIcon({ type }) { function DocumentationTypeIcon({ type }) {
switch (type) { switch (type) {
case 0:
return <ToolOutlined style={{ fontSize: 24 }} />;
case 1: case 1:
return <ToolOutlined style={{ fontSize: 24 }} />;
case 2:
return <BookOutlined style={{ fontSize: 24 }} />; return <BookOutlined style={{ fontSize: 24 }} />;
default: default:
console.log("DocumentationTypeIcon type not found:", type); console.log("DocumentationTypeIcon type not found:", type);

View File

@ -1,313 +1,691 @@
import { CameraOutlined, SearchOutlined } from "@ant-design/icons"; import {
import { Button, Card, Col, Input, Result, Row, Typography } from "antd"; ArrowDownOutlined,
import { AppStyle, Constants } from "../../utils"; ArrowUpOutlined,
import { useState } from "react"; CameraOutlined,
DeleteOutlined,
EllipsisOutlined,
FullscreenOutlined,
PlusOutlined,
SearchOutlined,
} from "@ant-design/icons";
import {
Button,
Card,
Col,
Divider,
Dropdown,
Input,
Result,
Row,
Select,
Space,
Typography,
Upload,
message,
} from "antd";
import { AppStyle, Constants, myFetch } from "../../utils";
import { useEffect, useRef, useState } from "react";
import { QrScanner } from "@yudiel/react-qr-scanner"; import { QrScanner } from "@yudiel/react-qr-scanner";
import { useNavigate } from "react-router-dom"; import ViewEquipmentDocumentations from "./ViewEquipmentDocumentation";
import MyModal, {
MyModalCloseCreateButtonFooter,
MyModalCloseSaveButtonFooter,
} from "../../Components/MyModal";
import Webcam from "react-webcam";
import TextArea from "antd/es/input/TextArea";
message.config({
maxCount: 2,
});
export default function EquipmentDocumentationOverview() { export default function EquipmentDocumentationOverview() {
const [scannerResult, setScannerResult] = useState("");
const [isScannerActive, setIsScannerActive] = useState(false);
const [inputValue, setInputValue] = useState("");
console.log("scan", scannerResult);
const isScannedQrCodeValid = (result) => {
// {"stockitem": 11} or 11 is valid
try {
// Try to parse the scanned content as JSON
const parsedResult = JSON.parse(result);
// Check if the parsed object has the expected format
if (parsedResult && typeof parsedResult === "object") {
if (
parsedResult.stockitem !== undefined &&
typeof parsedResult.stockitem === "number"
) {
return true; // The QR code has the expected format
}
} else if (!isNaN(parsedResult)) {
return true; // The QR code is a valid number
}
return false; // The QR code does not have the expected format or value
} catch (error) {
return false; // Error while parsing the QR code
}
};
return ( return (
<> <>
<ScanEquipmentComponent /> <Row style={{ marginBottom: AppStyle.app.margin }}>
<Col xs={0} md={7} />
<Result <Col xs={24} md={10}>
status="404" <Card
title="No equipment scanned"
subTitle="Scan a equipment to see the documentation."
/>
</>
);
}
/*
function DocumentationTypeIcon({ type }) {
switch (type) {
case 0:
return <ToolOutlined style={{ fontSize: 24 }} />;
case 1:
return <BookOutlined style={{ fontSize: 24 }} />;
default:
console.log("DocumentationTypeIcon type not found:", type);
return null;
}
} */
/*
export function EquipmentDocumentation() {
let { paramStockItemId } = useParams();
const navigate = useNavigate();
const [equipmentDocumentation, setEquipmentDocumentation] = useState([]);
const [isEquipmentDocumentationLoading, setIsEquipmentDocumentationLoading] =
useState(false);
const [isEquipmentCreateModalOpen, setIsEquipmentCreateModalOpen] =
useState(false);
const scannerResult = paramStockItemId;
const setScannerResult = (v) => {
console.log("set scanne result", v);
navigate(`${Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION}/${v}`);
};
const fetchDocumentation = () => {
if (!scannerResult) return;
setIsEquipmentDocumentationLoading(true);
myFetch(`/equipment/documentation/${scannerResult}`, "GET").then((data) => {
console.log("data", data);
// sort by date
// last created will be on top of the list
const sortedData = data.sort(
(a, b) => new Date(b.CreatedAt) - new Date(a.CreatedAt)
);
setIsEquipmentDocumentationLoading(false);
setEquipmentDocumentation(sortedData);
});
};
useEffect(() => {
console.log("scannerResult", scannerResult);
fetchDocumentation();
}, [scannerResult]);
const CreateDocumentationButton = () => {
return (
<Row style={{ alignItems: "center" }}>
<Col xs={24} sm={{ span: 8 }} md={{ span: 6 }}>
<MyImage
src={
"http://localhost:50050/v1/equipment/thumbnail/media/part_images/part_153_image.thumbnail.png"
}
width={64}
/>
</Col>
<Col xs={24} sm={{ span: 8, offset: 8 }} md={{ span: 6, offset: 12 }}>
<CreateEquipmentDocumentationModal />
</Col>
</Row>
);
};
const DocumentationContent = ({ documentation }) => {
return (
<Card style={{ marginTop: AppStyle.app.margin }}>
<div
style={{
display: "flex",
flexDirection: "row",
gap: 10,
justifyContent: "space-between",
marginBottom: AppStyle.typography.text.marginBottom,
}}
>
<div
style={{ style={{
display: "flex", textAlign: "center",
flexDirection: "row",
gap: 10,
alignItems: "center",
}} }}
> >
<DocumentationTypeIcon type={documentation.Type} /> {isScannerActive ? (
<Typography.Title level={4} style={{ margin: 0 }}> <>
{documentation.Title} <QrScanner
</Typography.Title> onDecode={(result) => {
</div> console.log(result);
<div if (!isScannedQrCodeValid(result)) {
style={{ message.error("Invalid stock item QR code");
display: "flex", return;
flexDirection: "row", }
gap: 10,
alignItems: "center", setScannerResult(JSON.parse(result).stockitem.toString());
}} setIsScannerActive(false);
> }}
<Popover onError={(error) => console.log(error?.message)}
title={ />
<Typography.Title <Button
level={4} block
style={{ color: Constants.COLORS.SECONDARY }} onClick={() => setIsScannerActive(false)}
style={{
marginTop: AppStyle.app.margin,
marginBottom: AppStyle.app.margin,
}}
> >
Details Close camera
</Typography.Title> </Button>
} </>
trigger="click" ) : (
placement="left" <div onClick={() => setIsScannerActive(true)}>
content={ <CameraOutlined style={{ fontSize: 64 }} />
<p style={{ color: "#000", margin: 0 }}> <Typography.Title level={5}>Scan equipment</Typography.Title>
<b>ID:</b> {documentation.Id} </div>
<br /> )}
<b>Type:</b> {documentation.Type}
<br />
<b>Created at:</b> {FormatDatetime(documentation.CreatedAt)}
<br />
<b>Updated at:</b> {FormatDatetime(documentation.UpdatedAt)}
</p>
}
>
<InfoCircleOutlined
style={{ fontSize: 24, color: Constants.COLORS.ICON_INFO }}
onClick={() => console.log("info")}
/>
</Popover>
<EditOutlined <Row gutter={AppStyle.grid.row.gutter}>
style={{ fontSize: 24 }} <Col xs={24} xl={16}>
onClick={() => console.log("edit")} <Input
/> placeholder="Equipment id"
</div> value={inputValue}
</div> onInput={(e) => setInputValue(e.target.value)}
/>
</Col>
<Col xs={24} xl={8}>
<Button
block
icon={<SearchOutlined />}
onClick={() => {
if (!isScannedQrCodeValid(inputValue)) {
message.error("Invalid stock item code");
return;
}
{documentation.Notes !== "" && setScannerResult(inputValue);
JSON.parse(documentation.Notes).map((note, index) => { }}
console.log("map doc"); >
Search
</Button>
</Col>
</Row>
</Card>
</Col>
<Col xs={0} md={7} />
</Row>
return ( {scannerResult === "" ? (
<NoteComponent
key={index}
viewMode
image={note.Image}
imageDocumentationId={documentation.Id}
description={note.Description}
/>
);
})}
</Card>
);
};
return (
<>
{isEquipmentDocumentationLoading ? (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
marginTop: 120,
}}
>
<Spin size="large" />
</div>
) : scannerResult === undefined ? (
<Result <Result
status="404" status="404"
title="No equipment scanned" title="No equipment scanned"
subTitle="Scan a equipment to see the documentation." subTitle="Scan a equipment to see the documentation."
/> />
) : scannerResult !== undefined && equipmentDocumentation.length === 0 ? (
<Result
status="404"
title="404"
subTitle="Sorry, for the equipment does not exist an documentation."
extra={
<>
<Button onClick={() => setScannerResult("")}>
Back to Overview
</Button>
<CreateDocumentationButton />
</>
}
/>
) : ( ) : (
<> <ViewEquipmentDocumentations scannerResult={scannerResult} />
<CreateDocumentationButton />
{equipmentDocumentation.map((documentation, index) => (
<DocumentationContent key={index} documentation={documentation} />
))}
</>
)} )}
<CreateEquipmentDocumentationModal
isOpen={isEquipmentCreateModalOpen}
onCancel={() => setIsEquipmentCreateModalOpen(false)}
fetchDocumentation={() => fetchDocumentation()}
/>
</> </>
); );
} */
export function ScanEquipmentComponent() {
const navigate = useNavigate();
const [isScannerActive, setIsScannerActive] = useState(false);
const setScannerResult = (v) => {
console.log("set scanne result", v);
navigate(`${Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION}/${v}`);
};
return (
<Row style={{ marginBottom: AppStyle.app.margin }}>
<Col xs={0} sm={7} />
<Col xs={24} sm={10}>
<Card
style={{
textAlign: "center",
}}
>
{isScannerActive ? (
<>
<QrScanner
onDecode={(result) => {
console.log(result);
setScannerResult(result);
setIsScannerActive(false);
}}
onError={(error) => console.log(error?.message)}
/>
<Button
block
onClick={() => setIsScannerActive(false)}
style={{
marginTop: AppStyle.app.margin,
marginBottom: AppStyle.app.margin,
}}
>
Close camera
</Button>
</>
) : (
<div onClick={() => setIsScannerActive(true)}>
<CameraOutlined style={{ fontSize: 64 }} />
<Typography.Title level={5}>Scan equipment</Typography.Title>
</div>
)}
<Row gutter={AppStyle.grid.row.gutter}>
<Col xs={24} md={16}>
<Input placeholder="Equipment id" />
</Col>
<Col xs={24} md={8}>
<Button
block
icon={<SearchOutlined />}
onClick={() => setScannerResult("169")}
>
Search
</Button>
</Col>
</Row>
</Card>
</Col>
<Col xs={0} sm={7} />
</Row>
);
} }
/* <EquipmentViewModal isOpen={isEquipmentViewModalOpen} /> export const EmptyNote = { Image: null, Description: "" };
<Table
loading={fetchingEquipment} const selectDocumentationTypeOptions = [
scroll={{ x: "max-content" }} { value: 1, label: "Repair protocol" },
columns={getTableColumns()} { value: 2, label: "Documentation" },
dataSource={getTableItems()} ];
export function EquipmentDocumentationViewEditComponent({
createMode,
isOpen,
onCancel,
fetchDocumentation,
stockItemId,
documentationId,
}) {
const [title, setTitle] = useState(createMode ? "New documentation" : "");
const [selectedDocumentationType, setSelectedDocumentationType] = useState(
selectDocumentationTypeOptions[0].value
);
const [notes, setNotes] = useState([EmptyNote]);
const [isDocumentationUploading, setIsDocumentationUploading] =
useState(false);
const documentationResponse = useRef();
const handleCreate = () => {
setIsDocumentationUploading(true);
const updatedNotes = [...notes];
updatedNotes.forEach((note, index) => {
if (note.Image === null && note.Description === "") {
updatedNotes.splice(index, 1);
}
});
let body = {
stockItemId: stockItemId,
type: selectedDocumentationType,
title: title,
notes: updatedNotes,
};
console.log("body", body);
myFetch(`/equipment/documentation/create`, "POST", body, {}).then(
(data) => {
console.log("data", data);
setIsDocumentationUploading(false);
fetchDocumentation();
onCancel();
}
);
};
const handleEdit = () => {
setIsDocumentationUploading(true);
const updatedNotes = [...notes];
console.log("documentationResponse.current", documentationResponse.current);
updatedNotes.forEach((note, index) => {
if (note.Image?.startsWith("http")) {
updatedNotes[index].Image = JSON.parse(
documentationResponse.current.Notes
)[index].Image;
}
});
let body = {
documentationId: documentationId,
type: selectedDocumentationType,
title: title,
notes: updatedNotes,
};
console.log("body", body);
myFetch(`/equipment/documentation/edit`, "POST", body, {}).then((data) => {
console.log("data", data);
setIsDocumentationUploading(false);
fetchDocumentation();
onCancel();
});
};
const handleDescriptionChange = (index) => (e) => {
const updatedNotes = [...notes];
updatedNotes[index] = {
...updatedNotes[index],
Description: e.target.value,
};
setNotes(updatedNotes);
};
const handleImageChange = (index) => (newImage) => {
const updatedNotes = [...notes];
updatedNotes[index] = {
...updatedNotes[index],
Image: newImage,
};
setNotes(updatedNotes);
};
const handleAddNote = () => setNotes([...notes, EmptyNote]);
const isAddNoteButtonDisabled = () => {
const lastNote = notes[notes.length - 1];
return lastNote.Image === null && lastNote.Description === "";
};
const isCreateButtonDisabled = () => {
if (notes.length === 0) return true;
if (notes.length === 1) {
return notes[0].Image === null && notes[0].Description === "";
}
return false;
};
// triggers on edit documentation
useEffect(() => {
if (!isOpen) return;
if (createMode) return;
myFetch(
`/equipment/documentation/${stockItemId}/${documentationId}`,
"GET"
).then((data) => {
console.log("data", data);
documentationResponse.current = data;
setTitle(data.Title);
// adding the static server url in front of the file name
const updatedDataNotes = JSON.parse(data.Notes);
updatedDataNotes.forEach((note, index) => {
if (note.Image !== "") {
updatedDataNotes[
index
].Image = `${Constants.STATIC_CONTENT_ADDRESS}equipmentdocumentation/${documentationId}/${note.Image}`;
}
});
setNotes(updatedDataNotes);
setSelectedDocumentationType(data.Type);
});
}, [isOpen]);
return (
<MyModal
isOpen={isOpen}
onCancel={onCancel}
footer={
createMode ? (
<MyModalCloseCreateButtonFooter
onCreate={handleCreate}
onCancel={onCancel}
isCreateButtonDisabled={isCreateButtonDisabled()}
isCreateButtonLoading={isDocumentationUploading}
/>
) : (
<MyModalCloseSaveButtonFooter
onSave={handleEdit}
onCancel={onCancel}
isSaveButtonLoading={isDocumentationUploading}
/>
)
}
>
<Typography.Title
editable={{ text: title, onChange: setTitle }}
level={1}
>
{title}
</Typography.Title>
<div style={{ marginBottom: AppStyle.typography.text.marginBottom }}>
<Typography.Text>Documentation type</Typography.Text>
</div>
<Select
value={selectedDocumentationType}
style={{ width: "100%", marginBottom: AppStyle.app.margin }}
onChange={(value) => setSelectedDocumentationType(value)}
options={selectDocumentationTypeOptions}
/> />
*/
{notes.map((note, index) => (
<NoteComponent
key={index}
index={index}
image={note.Image}
onImageChange={handleImageChange(index)}
description={note.Description}
onDescriptionChange={handleDescriptionChange(index)}
onDeleteImage={() => handleImageChange(index)(null)}
documentationId={documentationId}
onMoveUp={() => {
if (index === 0) return;
// updating the notes
const updatedNotes = [...notes];
const temp = updatedNotes[index - 1];
updatedNotes[index - 1] = updatedNotes[index];
updatedNotes[index] = temp;
setNotes(updatedNotes);
if (createMode) return;
// updating the documentationResponse.current
const updatedCurrentNotes = JSON.parse(
documentationResponse.current.Notes
);
const temp2 = updatedCurrentNotes[index - 1];
updatedCurrentNotes[index - 1] = updatedCurrentNotes[index];
updatedCurrentNotes[index] = temp2;
documentationResponse.current = {
...documentationResponse.current,
Notes: JSON.stringify(updatedCurrentNotes),
};
}}
onMoveDown={() => {
if (index === notes.length - 1) return;
// updating the notes
const updatedNotes = [...notes];
const temp = updatedNotes[index + 1];
updatedNotes[index + 1] = updatedNotes[index];
updatedNotes[index] = temp;
setNotes(updatedNotes);
if (createMode) return;
// updating the documentationResponse.current
const updatedCurrentNotes = JSON.parse(
documentationResponse.current.Notes
);
const temp2 = updatedCurrentNotes[index + 1];
updatedCurrentNotes[index + 1] = updatedCurrentNotes[index];
updatedCurrentNotes[index] = temp2;
documentationResponse.current = {
...documentationResponse.current,
Notes: JSON.stringify(updatedCurrentNotes),
};
}}
onDelete={() => {
if (notes.length === 1) {
setNotes([EmptyNote]);
return;
}
const updatedNotes = [...notes];
updatedNotes.splice(index, 1);
setNotes(updatedNotes);
}}
/>
))}
<div style={{ textAlign: "center" }}>
<Button
type="primary"
disabled={isAddNoteButtonDisabled()}
icon={<PlusOutlined />}
onClick={handleAddNote}
>
Add note
</Button>
</div>
</MyModal>
);
}
function UploadComponent({ setImagePreview }) {
const handleBeforeUpload = (file) => {
if (
!Constants.ACCEPTED_EQUIPMENT_DOCUMENTATION_FILE_TYPES.includes(file.type)
) {
message.error(`${file.name} is not valid file type`);
return false;
}
const reader = new FileReader();
reader.onload = () => setImagePreview(reader.result);
reader.readAsDataURL(file);
return false;
};
return (
<Upload
beforeUpload={handleBeforeUpload}
maxCount={1}
accept={Constants.ACCEPTED_EQUIPMENT_DOCUMENTATION_FILE_TYPES}
showUploadList={false}
>
<PlusOutlined />
</Upload>
);
}
function CameraComponent({ setImagePreview }) {
const [modalVisible, setModalVisible] = useState(false);
const [cameraVisible, setCameraVisible] = useState(false);
const webcamRef = useRef(null);
const handleCapture = () => {
const imageSrc = webcamRef.current.getScreenshot();
setImagePreview(imageSrc);
handleCancel();
};
const handleCancel = () => {
setCameraVisible(false);
// this is needed to close the camera correctly
setTimeout(() => setModalVisible(false), 100);
};
return (
<>
<div
onClick={() => {
setModalVisible(true);
setCameraVisible(true);
}}
>
<CameraOutlined />
</div>
<MyModal
isOpen={modalVisible}
onCancel={handleCancel}
footer={[
<Button key={0} block type="primary" onClick={handleCapture}>
Take picture
</Button>,
]}
>
{cameraVisible && (
<Webcam
audio={false}
ref={webcamRef}
screenshotFormat="image/jpeg"
width={"100%"}
videoConstraints={{
width: 1920,
height: 1080,
//facingMode: { exact: "environment" },
}}
/>
)}
</MyModal>
</>
);
}
function MoreComponent({ onMoveUp, onMoveDown, onDelete }) {
return (
<Dropdown
trigger={["click"]}
menu={{
items: [
{
label: (
<div onClick={onMoveUp}>
<Space>
<ArrowUpOutlined />
<span>Move up</span>
</Space>
</div>
),
key: "1",
},
{
label: (
<div onClick={onMoveDown}>
<Space>
<ArrowDownOutlined />
<span>Move down</span>
</Space>
</div>
),
key: "2",
},
{
label: (
<div onClick={onDelete}>
<Space>
<DeleteOutlined />
<span>Delete note</span>
</Space>
</div>
),
key: "0",
},
],
}}
>
<EllipsisOutlined />
</Dropdown>
);
}
export function NoteComponent({
viewMode,
image,
onImageChange,
description,
onDescriptionChange,
onDeleteImage,
documentationId,
onMoveUp,
onMoveDown,
onDelete,
}) {
const [isImageFullScreenModalOpen, setIsImageFullScreenModalOpen] =
useState(false);
const imageSrc = viewMode
? `${Constants.STATIC_CONTENT_ADDRESS}equipmentdocumentation/${documentationId}/${image}`
: image;
const FullscreenOutlinedIcon = ({ disabled }) => (
<FullscreenOutlined
disabled={true}
onClick={() => {
if (disabled) return;
setIsImageFullScreenModalOpen(true);
}}
/>
);
return (
<div style={{ marginBottom: AppStyle.app.margin }}>
<Row gutter={[AppStyle.grid.row.gutter[0]]}>
<Col xs={24} md={8}>
<Card
bodyStyle={{ padding: 0 }}
actions={
viewMode
? image
? [<FullscreenOutlinedIcon />]
: null
: [
<UploadComponent setImagePreview={onImageChange} />,
<CameraComponent setImagePreview={onImageChange} />,
<DeleteOutlined onClick={onDeleteImage} />,
<FullscreenOutlinedIcon
disabled={image === null || image === ""}
/>,
<MoreComponent
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onDelete={onDelete}
/>,
]
}
>
{!image ? (
<div
style={{
height: 250,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
{viewMode ? "No image" : "No image selected"}
</div>
) : (
<img
src={imageSrc}
width="100%"
style={{
borderTopLeftRadius: 6,
borderTopRightRadius: 6,
}}
alt="Preview"
/>
)}
</Card>
</Col>
<Col xs={24} md={16}>
{viewMode ? (
<Typography.Text>{description}</Typography.Text>
) : (
<TextArea
rows={8}
placeholder="Description"
value={description}
onChange={onDescriptionChange}
/>
)}
</Col>
</Row>
<ImageFullscreenModal
isOpen={isImageFullScreenModalOpen}
onCancel={() => setIsImageFullScreenModalOpen(false)}
image={imageSrc}
/>
</div>
);
}
function ImageFullscreenModal({ isOpen, onCancel, image }) {
return (
<MyModal isOpen={isOpen} onCancel={onCancel}>
<img width="100%" src={image} alt="Fullscreen preview" />
</MyModal>
);
}

View File

@ -77,6 +77,11 @@ export const Constants = {
"image/jpg", "image/jpg",
"image/gif", "image/gif",
], ],
ACCEPTED_EQUIPMENT_DOCUMENTATION_FILE_TYPES: [
"image/png",
"image/jpeg",
"image/jpg",
],
PERMISSIONS: { PERMISSIONS: {
EQUIPMENT_DOCUMENTATION: { EQUIPMENT_DOCUMENTATION: {
VIEW: "equipment_documentation.view", VIEW: "equipment_documentation.view",