equipment documentation

main
alex 2023-08-26 15:36:27 +02:00
parent f056e645f4
commit c125751f17
10 changed files with 403 additions and 261 deletions

View File

@ -14,7 +14,15 @@
"confirm": "Bestätigen",
"create": "Erstellen"
},
"contactAdmin": "Bitte kontaktieren Sie einen Administrator"
"contactAdmin": "Bitte kontaktieren Sie einen Administrator",
"text": {
"id": "ID:",
"createdAt": "Erstellt am:",
"updatedAt": "Aktualisiert am:",
"createdBy": "Erstellt von:",
"endedAt": "Beendet am:",
"type": "Typ:"
}
},
"sideMenu": {
"dashboard": "Dashboard",
@ -335,8 +343,54 @@
},
"header": { "scanners": "Scanner" }
},
"equipmentDocumentation": {},
"equipmentViewModal": {
"equipmentDocumentationNotFound": "Gerätedokumentation nicht gefunden"
"equipmentDocumentationOverview": {
"messageErrorInvalidStockItem": "Ungültiges Stock Item",
"buttonCloseCamera": "Kamera schließen",
"scanEquipment": {
"title": "Ausrüstung scannen",
"inputPlaceholder": "Ausrüstungs ID",
"buttonSearch": "Suchen"
},
"noEquipmentScannedResult": {
"title": "Keine Ausrüstung gescannt",
"description": "Bitte scannen Sie eine Ausrüstung"
}
},
"equipmentDocumentationViewEditComponent": {
"selectDocumentationTypeOptions": [
{ "value": 1, "label": "Reparaturprotokoll" },
{ "value": 2, "label": "Dokumentation" }
],
"titleNewDocumentation": "Neue Dokumentation",
"textDocumentationType": "Dokumentationstyp",
"buttonAddNote": "Notiz hinzufügen",
"buttonTakePicture": "Foto aufnehmen",
"buttonMoveUp": "Nach oben verschieben",
"buttonMoveDown": "Nach unten verschieben",
"buttonDelete": "Löschen",
"textImageNoImage": "Kein Bild",
"textImageNoImageSelected": "Kein Bild ausgewählt",
"textareaPlaceholder": "Notiz",
"modalImageFullscreenTitle": "Vorschau"
},
"viewEquipmentDocumentations": {
"detailsPopover": {
"title": "Details"
},
"result403": {
"title": "Keine Berechtigung",
"description": "Der Backend-Server ist nicht berechtigt, auf Invex zuzugreifen. Bitte kontaktieren Sie einen Administrator."
},
"result500": {
"title": "Keine Ausrüstung gefunden",
"description": "Die gescannte Ausrüstung existiert nicht in Invex."
},
"result404": {
"title": "Keine Dokumentation gefunden",
"description": "Für die gescannte Ausrüstung existiert keine Dokumentation."
}
},
"createEquipmentDocumentationModal": {
"buttonCreateDocumentation": "Dokumentation erstellen"
}
}

View File

@ -14,7 +14,15 @@
"confirm": "Confirm",
"create": "Create"
},
"contactAdmin": "Please contact an administrator"
"contactAdmin": "Please contact an administrator",
"text": {
"id": "ID:",
"createdAt": "Created at:",
"updatedAt": "Updated at:",
"createdBy": "Created by:",
"endedAt": "Ended at:",
"type": "Type:"
}
},
"sideMenu": {
"dashboard": "Dashboard",
@ -334,8 +342,54 @@
},
"header": { "scanners": "Scanners" }
},
"equipmentDocumentation": {},
"equipmentViewModal": {
"equipmentDocumentationNotFound": "Equipment documentation not found"
"equipmentDocumentationOverview": {
"messageErrorInvalidStockItem": "Invalid Stock Item",
"buttonCloseCamera": "Close Camera",
"scanEquipment": {
"title": "Scan Equipment",
"inputPlaceholder": "Equipment ID",
"buttonSearch": "Search"
},
"noEquipmentScannedResult": {
"title": "No Equipment Scanned",
"description": "Please scan an equipment"
}
},
"equipmentDocumentationViewEditComponent": {
"selectDocumentationTypeOptions": [
{ "value": 1, "label": "Repair protocol" },
{ "value": 2, "label": "Documentation" }
],
"titleNewDocumentation": "New Documentation",
"textDocumentationType": "Documentation Type",
"buttonAddNote": "Add Note",
"buttonTakePicture": "Take Photo",
"buttonMoveUp": "Move Up",
"buttonMoveDown": "Move Down",
"buttonDelete": "Delete",
"textImageNoImage": "No Image",
"textImageNoImageSelected": "No Image Selected",
"textareaPlaceholder": "Note",
"modalImageFullscreenTitle": "Preview"
},
"viewEquipmentDocumentations": {
"detailsPopover": {
"title": "Details"
},
"result403": {
"title": "Unauthorized Access",
"description": "The backend server is not authorized to access Invex. Please contact an administrator."
},
"result500": {
"title": "Equipment Not Found",
"description": "The scanned equipment does not exist in Invex."
},
"result404": {
"title": "Documentation Not Found",
"description": "No documentation found for the scanned equipment."
}
},
"createEquipmentDocumentationModal": {
"buttonCreateDocumentation": "Create Documentation"
}
}

View File

@ -2,7 +2,7 @@ import { useState } from "react";
import "antd/dist/reset.css";
import "./App.css";
import Login from "./Pages/Login";
import { Layout, notification } from "antd";
import { Layout, message, notification } from "antd";
import { UseUserSession, WebSocketProvider } from "./utils";
import DashboardLayout from "./Components/DashboardLayout";

View File

@ -16,8 +16,83 @@ import GroupTasks from "../../Pages/GroupTasks/Overview";
import GroupTasksHistory from "../../Pages/GroupTasks/History";
import PageNotFound from "../../Pages/PageNotFound";
import EquipmentDocumentationOverview from "../../Pages/EquipmentDocumentation";
import ViewEquipmentDocumentations from "../../Pages/EquipmentDocumentation/ViewEquipmentDocumentation";
export default function AppRoutes() {
// const webSocketContext = useContext(WebSocketContext);
console.log("appRoutes");
/*
TODO: move down
{hasPermission(
webSocketContext.User.Permissions,
Constants.PERMISSIONS.EQUIPMENT_DOCUMENTATION.VIEW
) && (
<Route
path="/equipment-documentation"
element={<EquipmentDocumentation />}
/>
)}
<Route
path={
Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION_VIEW +
":paramEquipmentId"
}
element={<EquipmentDocumentation isEquipmentViewModalOpen={true} />}
/>
<Route
path={
Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION + "/:paramStockItemId"
}
element={<ViewEquipmentDocumentations />}
/>
*/
return (
<Routes>
<Route path="/" element={<Dashboard />} />
<Route
path={Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION}
element={<EquipmentDocumentationOverview />}
/>
<Route
path={Constants.ROUTE_PATHS.GROUP_TASKS}
element={<GroupTasks isGroupTasksViewModalOpen={false} />}
/>
<Route
path={Constants.ROUTE_PATHS.GROUP_TASKS_VIEW + ":paramGroupTaskId"}
element={<GroupTasks isGroupTasksViewModalOpen={true} />}
/>
<Route
path={Constants.ROUTE_PATHS.GROUP_TASKS + "-history"}
element={<GroupTasksHistory />}
/>
<Route path="/scanners" element={<Scanners />} />
<Route path="/users" element={<AllUsers />} />
<Route path="/user-profile" element={<UserProfile />} />
<Route path="/admin-area/roles" element={<AdminAreaRoles />} />
<Route path="/admin-area/logs" element={<AdminAreaLogs />} />
<Route path="*" element={<PageNotFound />} />
</Routes>
);
} /*
/*
export default function AppRoutes() {
const webSocketContext = useContext(WebSocketContext);
@ -50,7 +125,7 @@ export default function AppRoutes() {
}
element={<ViewEquipmentDocumentations />}
/>
*/
*/ /*
return (
<Routes>
@ -107,3 +182,4 @@ export default function AppRoutes() {
</Routes>
);
}
*/

View File

@ -10,6 +10,7 @@ export default function MyModal({
isOpen,
onCancel,
footer = <MyModalOnlyCloseButtonFooter onCancel={onCancel} />,
title,
}) {
const screenBreakpoint = useBreakpoint();
@ -21,6 +22,7 @@ export default function MyModal({
onCancel={onCancel}
footer={footer}
centered={screenBreakpoint.xs}
title={title}
>
{children}
</Modal>

View File

@ -0,0 +1,38 @@
import { EditOutlined } from "@ant-design/icons";
import { Input, Typography } from "antd";
import { useState } from "react";
import { Constants } from "../../utils";
export default function MyTypography({ value, setValue, maxLength }) {
const [editing, setEditing] = useState(false);
return (
<div
style={{
display: "flex",
flexDirection: "row",
gap: 10,
marginRight: 26,
alignItems: "center",
}}
>
{editing ? (
<Input
value={value}
onChange={(e) => setValue(e.target.value)}
maxLength={maxLength}
showCount
/>
) : (
<Typography.Title level={3} style={{ margin: 0 }}>
{value}
</Typography.Title>
)}
<EditOutlined
style={{ fontSize: 24 }}
onClick={() => setEditing(!editing)}
/>
</div>
);
}

View File

@ -2,12 +2,14 @@ import { Button } from "antd";
import { useState } from "react";
import { PlusOutlined } from "@ant-design/icons";
import { EquipmentDocumentationViewEditComponent } from ".";
import { useTranslation } from "react-i18next";
export default function CreateEquipmentDocumentationModal({
stockItemId,
fetchDocumentation,
buttonBlock,
}) {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
return (
@ -18,7 +20,7 @@ export default function CreateEquipmentDocumentationModal({
icon={<PlusOutlined />}
onClick={() => setIsOpen(true)}
>
Create documentation
{t("createEquipmentDocumentationModal.buttonCreateDocumentation")}
</Button>
<EquipmentDocumentationViewEditComponent
@ -31,161 +33,3 @@ export default function CreateEquipmentDocumentationModal({
</>
);
}
/*
export default function CreateEquipmentDocumentationModal({
scannerResult,
fetchDocumentation,
buttonBlock,
}) {
const [isOpen, setIsOpen] = useState(false);
const [title, setTitle] = useState("New documentation");
const [selectedDocumentationType, setSelectedDocumentationType] = useState(
selectDocumentationTypeOptions[0].value
);
const [notes, setNotes] = useState([emptyNote]);
const [isDocumentationUploading, setIsDocumentationUploading] =
useState(false);
const handleCancel = () => setIsOpen(false);
const handleCreate = () => {
setIsDocumentationUploading(true);
const updatedNotes = [...notes];
updatedNotes.forEach((note, index) => {
if (note.image === null && note.description === "") {
updatedNotes.splice(index, 1);
}
});
let body = {
stockItemId: scannerResult,
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();
handleCancel();
}
);
};
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;
};
return (
<>
<Button
block={buttonBlock}
type="primary"
icon={<PlusOutlined />}
onClick={() => setIsOpen(true)}
>
Create documentation
</Button>
<MyModal
isOpen={isOpen}
onCancel={handleCancel}
footer={
<MyModalCloseCreateButtonFooter
onCreate={handleCreate}
isCreateButtonDisabled={isCreateButtonDisabled()}
isCreateButtonLoading={isDocumentationUploading}
onCancel={handleCancel}
/>
}
>
<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
//defaultValue={selectedDocumentationType}
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)}
/>
))}
<div style={{ textAlign: "center" }}>
<Button
type="primary"
disabled={isAddNoteButtonDisabled()}
icon={<PlusOutlined />}
onClick={handleAddNote}
>
Add note
</Button>
</div>
</MyModal>
</>
);
}
*/

View File

@ -8,10 +8,16 @@ import {
Spin,
Typography,
} from "antd";
import { useEffect, useState } from "react";
import { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import CreateEquipmentDocumentationModal from "./CreateEquipmentDocumentationModal";
import { AppStyle, Constants, FormatDatetime, myFetch } from "../../utils";
import {
AppStyle,
Constants,
FormatDatetime,
WebSocketContext,
myFetch,
} from "../../utils";
import {
BookOutlined,
InfoCircleOutlined,
@ -19,8 +25,15 @@ import {
} from "@ant-design/icons";
import EditEquipmentDocumentationModal from "./EditEquipmentDocumentationModal";
import { NoteComponent } from ".";
import { useTranslation } from "react-i18next";
import { MyAvatar } from "../../Components/MyAvatar";
export default function ViewEquipmentDocumentations({ scannerResult }) {
const { t } = useTranslation();
const webSocketContext = useContext(WebSocketContext);
console.log("render ViewEquipmentDocumentations");
const [equipmentDocumentationResponse, setEquipmentDocumentationResponse] =
useState(null);
const [isEquipmentDocumentationLoading, setIsEquipmentDocumentationLoading] =
@ -31,8 +44,6 @@ export default function ViewEquipmentDocumentations({ scannerResult }) {
myFetch(`/equipment/documentations/${scannerResult}`, "GET").then(
(data) => {
console.log("data", data);
const updatedData = { ...data };
// sort by date
@ -47,11 +58,7 @@ export default function ViewEquipmentDocumentations({ scannerResult }) {
);
};
useEffect(() => {
console.log("scannerResult", scannerResult);
fetchDocumentation();
}, [scannerResult]);
useEffect(() => fetchDocumentation(), [scannerResult]);
const CreateDocumentationButton = () => {
const InvexStockItemThumbnail = ({ width }) => {
@ -65,7 +72,7 @@ export default function ViewEquipmentDocumentations({ scannerResult }) {
};
return (
<Row style={{ alignItems: "center" }}>
<Row gutter={AppStyle.grid.row.gutter} style={{ alignItems: "center" }}>
<Col
xs={24}
sm={{ span: 8 }}
@ -139,32 +146,45 @@ export default function ViewEquipmentDocumentations({ scannerResult }) {
alignItems: "center",
}}
>
<MyAvatar
allUsers={webSocketContext.AllUsers}
userId={documentation.CreatedByUserId}
tooltip
/>
<Popover
title={
<Typography.Title
level={4}
style={{ color: Constants.COLORS.SECONDARY }}
>
Details
{t("viewEquipmentDocumentations.detailsPopover.title")}
</Typography.Title>
}
trigger="click"
placement="left"
content={
<p style={{ color: "#000", margin: 0 }}>
<b>ID:</b> {documentation.Id}
<b>{t("common.text.id")}</b> {documentation.Id}
<br />
<b>Type:</b> {documentation.Type}
<b>{t("common.text.type")}</b>{" "}
{
t(
"equipmentDocumentationViewEditComponent.selectDocumentationTypeOptions",
{ returnObjects: true }
)[documentation.Type - 1]?.label
}
<br />
<b>Created at:</b> {FormatDatetime(documentation.CreatedAt)}
<b>{t("common.text.createdAt")}</b>{" "}
{FormatDatetime(documentation.CreatedAt)}
<br />
<b>Updated at:</b> {FormatDatetime(documentation.UpdatedAt)}
<b>{t("common.text.updatedAt")}</b>{" "}
{FormatDatetime(documentation.UpdatedAt)}
</p>
}
>
<InfoCircleOutlined
style={{ fontSize: 24, color: Constants.COLORS.ICON_INFO }}
onClick={() => console.log("info")}
/>
</Popover>
@ -177,10 +197,7 @@ export default function ViewEquipmentDocumentations({ scannerResult }) {
</div>
{documentation.Notes !== "" &&
JSON.parse(documentation.Notes).map((note, index) => {
console.log("map doc");
return (
JSON.parse(documentation.Notes).map((note, index) => (
<NoteComponent
key={index}
viewMode
@ -188,8 +205,7 @@ export default function ViewEquipmentDocumentations({ scannerResult }) {
documentationId={documentation.Id}
description={note.Description}
/>
);
})}
))}
</Card>
);
};
@ -216,8 +232,8 @@ export default function ViewEquipmentDocumentations({ scannerResult }) {
return (
<Result
status="403"
title="401"
subTitle="The backend server is not authorized to access invex."
title={t("viewEquipmentDocumentations.result403.title")}
subTitle={t("viewEquipmentDocumentations.result403.description")}
/>
);
}
@ -227,8 +243,8 @@ export default function ViewEquipmentDocumentations({ scannerResult }) {
return (
<Result
status="500"
title="500"
subTitle="The scanned item doesn't exists on invex"
title={t("viewEquipmentDocumentations.result500.title")}
subTitle={t("viewEquipmentDocumentations.result500.description")}
/>
);
}
@ -237,8 +253,8 @@ export default function ViewEquipmentDocumentations({ scannerResult }) {
return (
<Result
status="404"
title="404"
subTitle="Sorry, for the equipment does not exist an documentation."
title={t("viewEquipmentDocumentations.result404.title")}
subTitle={t("viewEquipmentDocumentations.result404.description")}
extra={[
<CreateEquipmentDocumentationModal
key="0"

View File

@ -3,6 +3,7 @@ import {
ArrowUpOutlined,
CameraOutlined,
DeleteOutlined,
EditOutlined,
EllipsisOutlined,
FullscreenOutlined,
PlusOutlined,
@ -12,7 +13,6 @@ import {
Button,
Card,
Col,
Divider,
Dropdown,
Input,
Result,
@ -33,18 +33,19 @@ import MyModal, {
} from "../../Components/MyModal";
import Webcam from "react-webcam";
import TextArea from "antd/es/input/TextArea";
message.config({
maxCount: 2,
});
import { useTranslation } from "react-i18next";
import MyTypography from "../../Components/MyTypography";
export default function EquipmentDocumentationOverview() {
const { t } = useTranslation();
const [messageApi, messageContextHolder] = message.useMessage({
maxCount: 2,
});
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
@ -72,6 +73,7 @@ export default function EquipmentDocumentationOverview() {
return (
<>
{messageContextHolder}
<Row style={{ marginBottom: AppStyle.app.margin }}>
<Col xs={0} md={7} />
<Col xs={24} md={10}>
@ -87,7 +89,11 @@ export default function EquipmentDocumentationOverview() {
console.log(result);
if (!isScannedQrCodeValid(result)) {
message.error("Invalid stock item QR code");
messageApi.error(
t(
"equipmentDocumentationOverview.messageErrorInvalidStockItem"
)
);
return;
}
@ -104,20 +110,24 @@ export default function EquipmentDocumentationOverview() {
marginBottom: AppStyle.app.margin,
}}
>
Close camera
{t("equipmentDocumentationOverview.buttonCloseCamera")}
</Button>
</>
) : (
<div onClick={() => setIsScannerActive(true)}>
<CameraOutlined style={{ fontSize: 64 }} />
<Typography.Title level={5}>Scan equipment</Typography.Title>
<Typography.Title level={5}>
{t("equipmentDocumentationOverview.scanEquipment.title")}
</Typography.Title>
</div>
)}
<Row gutter={AppStyle.grid.row.gutter}>
<Col xs={24} xl={16}>
<Input
placeholder="Equipment id"
placeholder={t(
"equipmentDocumentationOverview.scanEquipment.inputPlaceholder"
)}
value={inputValue}
onInput={(e) => setInputValue(e.target.value)}
/>
@ -128,14 +138,20 @@ export default function EquipmentDocumentationOverview() {
icon={<SearchOutlined />}
onClick={() => {
if (!isScannedQrCodeValid(inputValue)) {
message.error("Invalid stock item code");
messageApi.error(
t(
"equipmentDocumentationOverview.messageErrorInvalidStockItem"
)
);
return;
}
setScannerResult(inputValue);
}}
>
Search
{t(
"equipmentDocumentationOverview.scanEquipment.buttonSearch"
)}
</Button>
</Col>
</Row>
@ -147,8 +163,12 @@ export default function EquipmentDocumentationOverview() {
{scannerResult === "" ? (
<Result
status="404"
title="No equipment scanned"
subTitle="Scan a equipment to see the documentation."
title={t(
"equipmentDocumentationOverview.noEquipmentScannedResult.title"
)}
subTitle={t(
"equipmentDocumentationOverview.noEquipmentScannedResult.description"
)}
/>
) : (
<ViewEquipmentDocumentations scannerResult={scannerResult} />
@ -159,11 +179,6 @@ export default function EquipmentDocumentationOverview() {
export const EmptyNote = { Image: null, Description: "" };
const selectDocumentationTypeOptions = [
{ value: 1, label: "Repair protocol" },
{ value: 2, label: "Documentation" },
];
export function EquipmentDocumentationViewEditComponent({
createMode,
isOpen,
@ -172,9 +187,17 @@ export function EquipmentDocumentationViewEditComponent({
stockItemId,
documentationId,
}) {
const [title, setTitle] = useState(createMode ? "New documentation" : "");
const { t } = useTranslation();
const [title, setTitle] = useState(
createMode
? t("equipmentDocumentationViewEditComponent.titleNewDocumentation")
: ""
);
const [selectedDocumentationType, setSelectedDocumentationType] = useState(
selectDocumentationTypeOptions[0].value
t(
"equipmentDocumentationViewEditComponent.selectDocumentationTypeOptions",
{ returnObjects: true }
)[0].value
);
const [notes, setNotes] = useState([EmptyNote]);
const [isDocumentationUploading, setIsDocumentationUploading] =
@ -199,12 +222,8 @@ export function EquipmentDocumentationViewEditComponent({
notes: updatedNotes,
};
console.log("body", body);
myFetch(`/equipment/documentation/create`, "POST", body, {}).then(
(data) => {
console.log("data", data);
setIsDocumentationUploading(false);
fetchDocumentation();
onCancel();
@ -217,8 +236,6 @@ export function EquipmentDocumentationViewEditComponent({
const updatedNotes = [...notes];
console.log("documentationResponse.current", documentationResponse.current);
updatedNotes.forEach((note, index) => {
if (note.Image?.startsWith("http")) {
updatedNotes[index].Image = JSON.parse(
@ -234,11 +251,7 @@ export function EquipmentDocumentationViewEditComponent({
notes: updatedNotes,
};
console.log("body", body);
myFetch(`/equipment/documentation/edit`, "POST", body, {}).then((data) => {
console.log("data", data);
setIsDocumentationUploading(false);
fetchDocumentation();
onCancel();
@ -279,7 +292,9 @@ export function EquipmentDocumentationViewEditComponent({
if (notes.length === 0) return true;
if (notes.length === 1) {
return notes[0].Image === null && notes[0].Description === "";
return (
(notes[0].Image === null && notes[0].Description === "") || title === ""
);
}
return false;
@ -294,8 +309,6 @@ export function EquipmentDocumentationViewEditComponent({
`/equipment/documentation/${stockItemId}/${documentationId}`,
"GET"
).then((data) => {
console.log("data", data);
documentationResponse.current = data;
setTitle(data.Title);
@ -320,6 +333,13 @@ export function EquipmentDocumentationViewEditComponent({
<MyModal
isOpen={isOpen}
onCancel={onCancel}
title={
<MyTypography
value={title}
setValue={setTitle}
maxLength={Constants.GLOBALS.MAX_EQUIPMENT_DOCUMENTATION_TITLE_LENGTH}
/>
}
footer={
createMode ? (
<MyModalCloseCreateButtonFooter
@ -337,22 +357,20 @@ export function EquipmentDocumentationViewEditComponent({
)
}
>
<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>
<Typography.Text>
{t("equipmentDocumentationViewEditComponent.textDocumentationType")}
</Typography.Text>
</div>
<Select
value={selectedDocumentationType}
style={{ width: "100%", marginBottom: AppStyle.app.margin }}
onChange={(value) => setSelectedDocumentationType(value)}
options={selectDocumentationTypeOptions}
options={t(
"equipmentDocumentationViewEditComponent.selectDocumentationTypeOptions",
{ returnObjects: true }
)}
/>
{notes.map((note, index) => (
@ -440,19 +458,19 @@ export function EquipmentDocumentationViewEditComponent({
icon={<PlusOutlined />}
onClick={handleAddNote}
>
Add note
{t("equipmentDocumentationViewEditComponent.buttonAddNote")}
</Button>
</div>
</MyModal>
);
}
function UploadComponent({ setImagePreview }) {
function UploadComponent({ setImagePreview, messageApi }) {
const handleBeforeUpload = (file) => {
if (
!Constants.ACCEPTED_EQUIPMENT_DOCUMENTATION_FILE_TYPES.includes(file.type)
) {
message.error(`${file.name} is not valid file type`);
messageApi.error(`${file.name} is not valid file type`);
return false;
}
@ -476,6 +494,7 @@ function UploadComponent({ setImagePreview }) {
}
function CameraComponent({ setImagePreview }) {
const { t } = useTranslation();
const [modalVisible, setModalVisible] = useState(false);
const [cameraVisible, setCameraVisible] = useState(false);
const webcamRef = useRef(null);
@ -509,7 +528,7 @@ function CameraComponent({ setImagePreview }) {
onCancel={handleCancel}
footer={[
<Button key={0} block type="primary" onClick={handleCapture}>
Take picture
{t("equipmentDocumentationViewEditComponent.buttonTakePicture")}
</Button>,
]}
>
@ -532,6 +551,8 @@ function CameraComponent({ setImagePreview }) {
}
function MoreComponent({ onMoveUp, onMoveDown, onDelete }) {
const { t } = useTranslation();
return (
<Dropdown
trigger={["click"]}
@ -542,7 +563,9 @@ function MoreComponent({ onMoveUp, onMoveDown, onDelete }) {
<div onClick={onMoveUp}>
<Space>
<ArrowUpOutlined />
<span>Move up</span>
<span>
{t("equipmentDocumentationViewEditComponent.buttonMoveUp")}
</span>
</Space>
</div>
),
@ -553,7 +576,11 @@ function MoreComponent({ onMoveUp, onMoveDown, onDelete }) {
<div onClick={onMoveDown}>
<Space>
<ArrowDownOutlined />
<span>Move down</span>
<span>
{t(
"equipmentDocumentationViewEditComponent.buttonMoveDown"
)}
</span>
</Space>
</div>
),
@ -564,7 +591,9 @@ function MoreComponent({ onMoveUp, onMoveDown, onDelete }) {
<div onClick={onDelete}>
<Space>
<DeleteOutlined />
<span>Delete note</span>
<span>
{t("equipmentDocumentationViewEditComponent.buttonDelete")}
</span>
</Space>
</div>
),
@ -590,6 +619,8 @@ export function NoteComponent({
onMoveDown,
onDelete,
}) {
const { t } = useTranslation();
const [messageApi, messageContextHolder] = message.useMessage();
const [isImageFullScreenModalOpen, setIsImageFullScreenModalOpen] =
useState(false);
@ -610,6 +641,8 @@ export function NoteComponent({
return (
<div style={{ marginBottom: AppStyle.app.margin }}>
{messageContextHolder}
<Row gutter={[AppStyle.grid.row.gutter[0]]}>
<Col xs={24} md={8}>
<Card
@ -620,7 +653,10 @@ export function NoteComponent({
? [<FullscreenOutlinedIcon />]
: null
: [
<UploadComponent setImagePreview={onImageChange} />,
<UploadComponent
setImagePreview={onImageChange}
messageApi={messageApi}
/>,
<CameraComponent setImagePreview={onImageChange} />,
<DeleteOutlined onClick={onDeleteImage} />,
<FullscreenOutlinedIcon
@ -643,7 +679,13 @@ export function NoteComponent({
alignItems: "center",
}}
>
{viewMode ? "No image" : "No image selected"}
{viewMode
? t(
"equipmentDocumentationViewEditComponent.textImageNoImage"
)
: t(
"equipmentDocumentationViewEditComponent.textImageNoImageSelected"
)}
</div>
) : (
<img
@ -664,10 +706,16 @@ export function NoteComponent({
<Typography.Text>{description}</Typography.Text>
) : (
<TextArea
rows={8}
placeholder="Description"
autoSize={{ minRows: 8, maxRows: 13 }}
placeholder={t(
"equipmentDocumentationViewEditComponent.textareaPlaceholder"
)}
value={description}
onChange={onDescriptionChange}
showCount
maxLength={
Constants.GLOBALS.MAX_EQUIPMENT_DOCUMENTATION_NOTE_LENGTH
}
/>
)}
</Col>
@ -683,8 +731,16 @@ export function NoteComponent({
}
function ImageFullscreenModal({ isOpen, onCancel, image }) {
const { t } = useTranslation();
return (
<MyModal isOpen={isOpen} onCancel={onCancel}>
<MyModal
isOpen={isOpen}
onCancel={onCancel}
title={t(
"equipmentDocumentationViewEditComponent.modalImageFullscreenTitle"
)}
>
<img width="100%" src={image} alt="Fullscreen preview" />
</MyModal>
);

View File

@ -69,6 +69,8 @@ export const Constants = {
MIN_ROLE_DISPLAY_NAME: 3,
MAX_ROLE_DISPLAY_NAME: 30,
MAX_ROLE_DESCRIPTION: 80,
MAX_EQUIPMENT_DOCUMENTATION_TITLE_LENGTH: 60,
MAX_EQUIPMENT_DOCUMENTATION_NOTE_LENGTH: 2000,
},
MAX_AVATAR_SIZE: 5 * 1024 * 1024,
ACCEPTED_AVATAR_FILE_TYPES: [