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",
"save": "Speichern",
"delete": "Löschen",
"confirm": "Bestätigen"
"confirm": "Bestätigen",
"create": "Erstellen"
},
"contactAdmin": "Bitte kontaktieren Sie einen Administrator"
},

View File

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

View File

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

View File

@ -103,3 +103,29 @@ export function MyModalOnlyCloseButtonFooter({ onCancel }) {
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,95 +1,241 @@
import { useNavigate, useParams } from "react-router-dom";
import MyModal from "../../Components/MyModal";
import { AppStyle, Constants } from "../../utils";
import {
Button,
Card,
Col,
Divider,
Modal,
Row,
Typography,
Upload,
message,
} from "antd";
import { useRef, useState } from "react";
import MyModal, {
MyModalCloseCreateButtonFooter,
MyModalCloseSaveButtonFooter,
} from "../../Components/MyModal";
import { AppStyle, Constants, myFetch, myFetchContentType } from "../../utils";
import { Button, Card, Col, Row, Select, Typography } from "antd";
import { createRef, useRef, useState } from "react";
import TextArea from "antd/es/input/TextArea";
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 = () => {
const [visible, setVisible] = useState(false);
const [imageData, setImageData] = useState(null);
const webcamRef = useRef();
function UploadComponent({ index, setImagePreview }) {
const handleFileChange = (event) => {
const file = event.target.files[0];
console.log("file", event.target.files);
if (file) {
const reader = new FileReader();
reader.onload = () => {
//setImagePreview(URL.createObjectURL(file));
setImagePreview(reader.result);
const handleCapture = () => {
const imageSrc = webcamRef.current.getScreenshot();
setImageData(imageSrc);
};
const handleClear = () => {
setImageData(null);
};
const handleSave = () => {
// Hier könntest du die Logik zum Speichern des Bildes implementieren.
message.success("Bild gespeichert!");
setVisible(false);
// base 64 string
// console.log(reader.result);
};
reader.readAsDataURL(file);
}
};
return (
<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
title="Kamera"
isOpen={visible}
onCancel={() => setVisible(false)}
isOpen={modalVisible}
onCancel={handleCancel}
footer={[
<Button key="clear" onClick={handleClear}>
Löschen
</Button>,
<Button key="capture" type="primary" onClick={handleCapture}>
Foto aufnehmen
</Button>,
<Button key="save" type="primary" onClick={handleSave}>
Speichern
<Button key={0} block type="primary" onClick={handleCapture}>
Take picture
</Button>,
]}
>
<Webcam
audio={false}
ref={webcamRef}
screenshotFormat="image/jpeg"
width={"100%"}
videoConstraints={{
width: 1920,
height: 1080,
facingMode: { exact: "environment" },
}}
/>
{imageData && (
<div>
<img src={imageData} alt="Vorschau" />
</div>
{cameraVisible && (
<Webcam
audio={false}
ref={webcamRef}
screenshotFormat="image/jpeg"
width={"100%"}
videoConstraints={{
width: 1920,
height: 1080,
//facingMode: { exact: "environment" },
}}
/>
)}
</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 }) {
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 = () =>
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 (
<MyModal isOpen={isOpen} onCancel={handleCancel}>
<MyModal
isOpen={isOpen}
onCancel={handleCancel}
footer={
<MyModalCloseCreateButtonFooter
onCreate={handleCreate}
onCancel={handleCancel}
/>
}
>
<Typography.Title
editable={{ text: title, onChange: setTitle }}
level={1}
@ -97,46 +243,39 @@ export default function CreateEquipmentDocumentationModal({ isOpen }) {
{title}
</Typography.Title>
<Row gutter={AppStyle.grid.row.glutter}>
<Col sm={6}>
<Card>
<Card.Grid
hoverable={false}
style={{
width: "50%",
textAlign: "center",
borderTopLeftRadius: AppStyle.app.borderRadius,
borderBottomLeftRadius: AppStyle.app.borderRadius,
}}
onClick={() => console.log("upload")}
>
<Upload>
<PlusOutlined />
<p>Upload</p>
</Upload>
</Card.Grid>
<div style={{ marginBottom: AppStyle.typography.text.marginBottom }}>
<Typography.Text>Documentation type</Typography.Text>
</div>
<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>
<Select
defaultValue={selectedDocumentationType}
style={{ width: "100%", marginBottom: AppStyle.app.marginBottom }}
onChange={(value) => setSelectedDocumentationType(value)}
options={selectDocumentationTypeOptions}
/>
<Col sm={18}>
<TextArea rows={8} placeholder="Description" />
</Col>
</Row>
{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)}
/>
))}
<CameraComponent />
<div style={{ textAlign: "center" }}>
<Button
type="primary"
disabled={isAddNoteButtonDisabled()}
icon={<PlusOutlined />}
onClick={handleAddNote}
>
Add note
</Button>
</div>
</MyModal>
);
}

View File

@ -4,7 +4,9 @@ import { AppStyle, Constants, myFetch } from "../../utils";
import { useEffect, useState } from "react";
import { QrScanner } from "@yudiel/react-qr-scanner";
import EquipmentViewModal from "./EquipmentViewModal";
import CreateEquipmentDocumentationModal from "./CreateEquipmentDocumentationModal";
import CreateEquipmentDocumentationModal, {
NoteComponent,
} from "./CreateEquipmentDocumentationModal";
import { Link } from "react-router-dom";
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 (
<>
<Row style={{ marginBottom: AppStyle.app.marginBottom }}>
@ -147,7 +179,7 @@ export default function EquipmentDocumentation({ isEquipmentCreateModalOpen }) {
<h1>ScannerResult: {scannerResult}</h1>
{scannerResult !== "" && equipmentDocumentation.length === 0 && (
{scannerResult !== "" && equipmentDocumentation.length === 0 ? (
<Result
status="404"
title="404"
@ -158,14 +190,18 @@ export default function EquipmentDocumentation({ isEquipmentCreateModalOpen }) {
Back to Overview
</Button>
<Link
to={`${Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION_CREATE}${scannerResult}`}
>
<Button type="primary">Create documentation</Button>
</Link>
<CreateDocumentationButton />
</>
}
/>
) : (
<>
<CreateDocumentationButton />
{equipmentDocumentation.map((documentation, index) => (
<DocumentationContent key={index} documentation={documentation} />
))}
</>
)}
<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} />
<Table
loading={fetchingEquipment}

View File

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