equipment documentation

main
alex 2023-08-22 00:12:26 +00:00
parent d3fa6781cc
commit fdae12fc88
7 changed files with 374 additions and 118 deletions

View File

@ -11,7 +11,8 @@
"close": "Schließen", "close": "Schließen",
"save": "Speichern", "save": "Speichern",
"delete": "Löschen", "delete": "Löschen",
"confirm": "Bestätigen" "confirm": "Bestätigen",
"create": "Erstellen"
}, },
"contactAdmin": "Bitte kontaktieren Sie einen Administrator" "contactAdmin": "Bitte kontaktieren Sie einen Administrator"
}, },

View File

@ -11,7 +11,8 @@
"close": "Close", "close": "Close",
"save": "Save", "save": "Save",
"delete": "Delete", "delete": "Delete",
"confirm": "Confirm" "confirm": "Confirm",
"create": "Create"
}, },
"contactAdmin": "Please contact an administrator" "contactAdmin": "Please contact an administrator"
}, },

View File

@ -54,7 +54,7 @@ export default function AppRoutes() {
<Route <Route
path={ path={
Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION_CREATE + Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION_CREATE +
":paramEquipmentId" ":paramStockItemId"
} }
element={<EquipmentDocumentation isEquipmentCreateModalOpen={true} />} element={<EquipmentDocumentation isEquipmentCreateModalOpen={true} />}
/> />

View File

@ -103,3 +103,29 @@ 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 }) {
const { t } = useTranslation();
return (
<>
<Button onClick={onCancel}>{t("common.button.close")}</Button>
<Button onClick={onSave} type="primary">
{t("common.button.save")}
</Button>
</>
);
}
export function MyModalCloseCreateButtonFooter({ onCancel, onCreate }) {
const { t } = useTranslation();
return (
<>
<Button onClick={onCancel}>{t("common.button.close")}</Button>
<Button onClick={onCreate} type="primary">
{t("common.button.create")}
</Button>
</>
);
}

View File

@ -1,61 +1,96 @@
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import MyModal from "../../Components/MyModal"; import MyModal, {
import { AppStyle, Constants } from "../../utils"; MyModalCloseCreateButtonFooter,
import { MyModalCloseSaveButtonFooter,
Button, } from "../../Components/MyModal";
Card, import { AppStyle, Constants, myFetch, myFetchContentType } from "../../utils";
Col, import { Button, Card, Col, Row, Select, Typography } from "antd";
Divider, import { createRef, useRef, useState } from "react";
Modal,
Row,
Typography,
Upload,
message,
} from "antd";
import { useRef, useState } from "react";
import TextArea from "antd/es/input/TextArea"; import TextArea from "antd/es/input/TextArea";
import Webcam from "react-webcam"; import Webcam from "react-webcam";
import { CameraOutlined, PlusOutlined } from "@ant-design/icons"; import {
CameraOutlined,
DeleteOutlined,
FullscreenOutlined,
PlusOutlined,
} from "@ant-design/icons";
import { DocumentationImage } from ".";
const CameraComponent = () => { function UploadComponent({ index, setImagePreview }) {
const [visible, setVisible] = useState(false); const handleFileChange = (event) => {
const [imageData, setImageData] = useState(null); const file = event.target.files[0];
const webcamRef = useRef(); console.log("file", event.target.files);
if (file) {
const reader = new FileReader();
reader.onload = () => {
//setImagePreview(URL.createObjectURL(file));
setImagePreview(reader.result);
const handleCapture = () => { // base 64 string
const imageSrc = webcamRef.current.getScreenshot(); // console.log(reader.result);
setImageData(imageSrc);
}; };
reader.readAsDataURL(file);
const handleClear = () => { }
setImageData(null);
};
const handleSave = () => {
// Hier könntest du die Logik zum Speichern des Bildes implementieren.
message.success("Bild gespeichert!");
setVisible(false);
}; };
return ( return (
<div> <div>
<Button onClick={() => setVisible(true)}>Kamera öffnen</Button> <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 (
<>
<div
onClick={() => {
setModalVisible(true);
setCameraVisible(true);
}}
>
<CameraOutlined />
</div>
<MyModal <MyModal
title="Kamera" isOpen={modalVisible}
isOpen={visible} onCancel={handleCancel}
onCancel={() => setVisible(false)}
footer={[ footer={[
<Button key="clear" onClick={handleClear}> <Button key={0} block type="primary" onClick={handleCapture}>
Löschen Take picture
</Button>,
<Button key="capture" type="primary" onClick={handleCapture}>
Foto aufnehmen
</Button>,
<Button key="save" type="primary" onClick={handleSave}>
Speichern
</Button>, </Button>,
]} ]}
> >
{cameraVisible && (
<Webcam <Webcam
audio={false} audio={false}
ref={webcamRef} ref={webcamRef}
@ -64,32 +99,143 @@ const CameraComponent = () => {
videoConstraints={{ videoConstraints={{
width: 1920, width: 1920,
height: 1080, height: 1080,
facingMode: { exact: "environment" }, //facingMode: { exact: "environment" },
}} }}
/> />
{imageData && (
<div>
<img src={imageData} alt="Vorschau" />
</div>
)} )}
</MyModal> </MyModal>
</div> </>
); );
}; }
export function NoteComponent({
viewMode,
index,
image,
onImageChange,
description,
onDescriptionChange,
onDeleteImage,
}) {
return (
<Row
gutter={AppStyle.grid.row.glutter}
style={{ marginBottom: AppStyle.app.marginBottom }}
>
<Col xs={24} md={8}>
<Card
bodyStyle={{ padding: 0 }}
actions={
viewMode
? [<FullscreenOutlined />]
: [
<UploadComponent
index={index}
setImagePreview={onImageChange}
/>,
<CameraComponent setImagePreview={onImageChange} />,
<DeleteOutlined onClick={onDeleteImage} />,
<FullscreenOutlined />,
]
}
>
<DocumentationImage
image={image}
imgStyle={{ borderTopLeftRadius: 6, borderTopRightRadius: 6 }}
/>
</Card>
</Col>
<Col xs={24} md={16}>
{viewMode ? (
<Typography.Text>{description}</Typography.Text>
) : (
<TextArea
rows={8}
placeholder="Description"
value={description}
onChange={onDescriptionChange}
/>
)}
</Col>
</Row>
);
}
const emptyNote = { image: null, description: "" };
const selectDocumentationTypeOptions = [
{ value: 0, label: "Repair protocol" },
{ value: 1, label: "Documentation" },
];
export default function CreateEquipmentDocumentationModal({ isOpen }) { export default function CreateEquipmentDocumentationModal({ isOpen }) {
const navigate = useNavigate(); const navigate = useNavigate();
let { paramEquipmentId } = useParams(); let { paramStockItemId } = useParams();
const [title, setTitle] = useState(""); const [title, setTitle] = useState("New documentation");
const [selectedDocumentationType, setSelectedDocumentationType] = useState(
selectDocumentationTypeOptions[0].value
);
const [notes, setNotes] = useState([emptyNote]);
const handleCancel = () => const handleCancel = () =>
navigate(Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION); navigate(Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION);
console.log("paramEquipmentId", paramEquipmentId); const handleCreate = () => {
let obj = {
stockItemId: paramStockItemId,
type: selectedDocumentationType,
title: title,
notes: notes,
};
myFetch(`/equipment/documentation/create`, "POST", obj, {}).then((data) => {
console.log("data", data);
});
};
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 === "";
};
return ( return (
<MyModal isOpen={isOpen} onCancel={handleCancel}> <MyModal
isOpen={isOpen}
onCancel={handleCancel}
footer={
<MyModalCloseCreateButtonFooter
onCreate={handleCreate}
onCancel={handleCancel}
/>
}
>
<Typography.Title <Typography.Title
editable={{ text: title, onChange: setTitle }} editable={{ text: title, onChange: setTitle }}
level={1} level={1}
@ -97,46 +243,39 @@ export default function CreateEquipmentDocumentationModal({ isOpen }) {
{title} {title}
</Typography.Title> </Typography.Title>
<Row gutter={AppStyle.grid.row.glutter}> <div style={{ marginBottom: AppStyle.typography.text.marginBottom }}>
<Col sm={6}> <Typography.Text>Documentation type</Typography.Text>
<Card> </div>
<Card.Grid
hoverable={false} <Select
style={{ defaultValue={selectedDocumentationType}
width: "50%", style={{ width: "100%", marginBottom: AppStyle.app.marginBottom }}
textAlign: "center", onChange={(value) => setSelectedDocumentationType(value)}
borderTopLeftRadius: AppStyle.app.borderRadius, options={selectDocumentationTypeOptions}
borderBottomLeftRadius: AppStyle.app.borderRadius, />
}}
onClick={() => console.log("upload")} {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)}
/>
))}
<div style={{ textAlign: "center" }}>
<Button
type="primary"
disabled={isAddNoteButtonDisabled()}
icon={<PlusOutlined />}
onClick={handleAddNote}
> >
<Upload> Add note
<PlusOutlined /> </Button>
<p>Upload</p> </div>
</Upload>
</Card.Grid>
<Card.Grid
hoverable={false}
style={{
width: "50%",
textAlign: "center",
borderTopRightRadius: AppStyle.app.borderRadius,
borderBottomRightRadius: AppStyle.app.borderRadius,
}}
>
<CameraOutlined />
<p>Take</p>
</Card.Grid>
</Card>
</Col>
<Col sm={18}>
<TextArea rows={8} placeholder="Description" />
</Col>
</Row>
<CameraComponent />
</MyModal> </MyModal>
); );
} }

View File

@ -4,7 +4,9 @@ import { AppStyle, Constants, myFetch } from "../../utils";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { QrScanner } from "@yudiel/react-qr-scanner"; import { QrScanner } from "@yudiel/react-qr-scanner";
import EquipmentViewModal from "./EquipmentViewModal"; import EquipmentViewModal from "./EquipmentViewModal";
import CreateEquipmentDocumentationModal from "./CreateEquipmentDocumentationModal"; import CreateEquipmentDocumentationModal, {
NoteComponent,
} from "./CreateEquipmentDocumentationModal";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
export default function EquipmentDocumentation({ isEquipmentCreateModalOpen }) { export default function EquipmentDocumentation({ isEquipmentCreateModalOpen }) {
@ -107,6 +109,36 @@ export default function EquipmentDocumentation({ isEquipmentCreateModalOpen }) {
})); }));
}; */ }; */
const CreateDocumentationButton = () => {
return (
<Link
to={`${Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION_CREATE}${scannerResult}`}
>
<Button type="primary">Create documentation</Button>
</Link>
);
};
const DocumentationContent = ({ documentation }) => {
console.log("doc", documentation);
return (
<Card>
<Typography.Title level={4}>{documentation.Title}</Typography.Title>
{documentation.Notes !== "" &&
JSON.parse(documentation.Notes).map((note, index) => (
<NoteComponent
key={index}
viewMode
image={`${Constants.STATIC_CONTENT_ADDRESS}equipmentdocumentation/${documentation.Id}/${note.Image}`}
description={note.Description}
/>
))}
</Card>
);
};
return ( return (
<> <>
<Row style={{ marginBottom: AppStyle.app.marginBottom }}> <Row style={{ marginBottom: AppStyle.app.marginBottom }}>
@ -147,7 +179,7 @@ export default function EquipmentDocumentation({ isEquipmentCreateModalOpen }) {
<h1>ScannerResult: {scannerResult}</h1> <h1>ScannerResult: {scannerResult}</h1>
{scannerResult !== "" && equipmentDocumentation.length === 0 && ( {scannerResult !== "" && equipmentDocumentation.length === 0 ? (
<Result <Result
status="404" status="404"
title="404" title="404"
@ -158,14 +190,18 @@ export default function EquipmentDocumentation({ isEquipmentCreateModalOpen }) {
Back to Overview Back to Overview
</Button> </Button>
<Link <CreateDocumentationButton />
to={`${Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION_CREATE}${scannerResult}`}
>
<Button type="primary">Create documentation</Button>
</Link>
</> </>
} }
/> />
) : (
<>
<CreateDocumentationButton />
{equipmentDocumentation.map((documentation, index) => (
<DocumentationContent key={index} documentation={documentation} />
))}
</>
)} )}
<CreateEquipmentDocumentationModal isOpen={isEquipmentCreateModalOpen} /> <CreateEquipmentDocumentationModal isOpen={isEquipmentCreateModalOpen} />
@ -173,6 +209,30 @@ export default function EquipmentDocumentation({ isEquipmentCreateModalOpen }) {
); );
} }
export function DocumentationImage({ image, imgStyle }) {
return image ? (
<img
src={image}
style={{
width: "100%",
...imgStyle,
}}
alt="Preview"
/>
) : (
<div
style={{
height: 250,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
No image selected
</div>
);
}
/* <EquipmentViewModal isOpen={isEquipmentViewModalOpen} /> /* <EquipmentViewModal isOpen={isEquipmentViewModalOpen} />
<Table <Table
loading={fetchingEquipment} loading={fetchingEquipment}

View File

@ -130,6 +130,11 @@ export const AppStyle = {
marginBottom: 12, marginBottom: 12,
borderRadius: 12, borderRadius: 12,
}, },
typography: {
text: {
marginBottom: 6,
},
},
grid: { grid: {
row: { row: {
glutter: [16, 16], glutter: [16, 16],
@ -1299,16 +1304,40 @@ export function DecodedBase64ToString(value) {
return Buffer.from(value, "base64").toString(); return Buffer.from(value, "base64").toString();
} }
const myFetchDefaultHeaders = { export const myFetchContentType = {
"Content-Type": "application/json", JSON: 0,
"X-Authorization": getUserSessionFromLocalStorage(), MULTIPART_FORM_DATA: 1,
}; };
export function myFetch(url, method, body = null, headers = {}) { export function myFetch(
url,
method,
body = null,
headers = {},
contentType = myFetchContentType.JSON
) {
const getContentType = () => {
if (contentType === myFetchContentType.JSON) return "application/json";
return "multipart/form-data";
};
const getBody = () => {
if (!body) return null;
if (contentType === myFetchContentType.JSON) return JSON.stringify(body);
return body;
};
const requestOptions = { const requestOptions = {
method: method, method: method,
headers: { ...myFetchDefaultHeaders, ...headers }, headers: {
body: body ? JSON.stringify(body) : null, "X-Authorization": getUserSessionFromLocalStorage(),
"Content-Type": getContentType(),
...headers,
},
body: getBody(),
}; };
return fetch(`${Constants.API_ADDRESS}${url}`, requestOptions) return fetch(`${Constants.API_ADDRESS}${url}`, requestOptions)