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", "confirm": "Bestätigen",
"create": "Erstellen" "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": { "sideMenu": {
"dashboard": "Dashboard", "dashboard": "Dashboard",
@ -335,8 +343,54 @@
}, },
"header": { "scanners": "Scanner" } "header": { "scanners": "Scanner" }
}, },
"equipmentDocumentation": {}, "equipmentDocumentationOverview": {
"equipmentViewModal": { "messageErrorInvalidStockItem": "Ungültiges Stock Item",
"equipmentDocumentationNotFound": "Gerätedokumentation nicht gefunden" "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", "confirm": "Confirm",
"create": "Create" "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": { "sideMenu": {
"dashboard": "Dashboard", "dashboard": "Dashboard",
@ -334,8 +342,54 @@
}, },
"header": { "scanners": "Scanners" } "header": { "scanners": "Scanners" }
}, },
"equipmentDocumentation": {}, "equipmentDocumentationOverview": {
"equipmentViewModal": { "messageErrorInvalidStockItem": "Invalid Stock Item",
"equipmentDocumentationNotFound": "Equipment documentation not found" "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 "antd/dist/reset.css";
import "./App.css"; import "./App.css";
import Login from "./Pages/Login"; import Login from "./Pages/Login";
import { Layout, notification } from "antd"; import { Layout, message, notification } from "antd";
import { UseUserSession, WebSocketProvider } from "./utils"; import { UseUserSession, WebSocketProvider } from "./utils";
import DashboardLayout from "./Components/DashboardLayout"; import DashboardLayout from "./Components/DashboardLayout";

View File

@ -16,8 +16,83 @@ import GroupTasks from "../../Pages/GroupTasks/Overview";
import GroupTasksHistory from "../../Pages/GroupTasks/History"; import GroupTasksHistory from "../../Pages/GroupTasks/History";
import PageNotFound from "../../Pages/PageNotFound"; import PageNotFound from "../../Pages/PageNotFound";
import EquipmentDocumentationOverview from "../../Pages/EquipmentDocumentation"; 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() { export default function AppRoutes() {
const webSocketContext = useContext(WebSocketContext); const webSocketContext = useContext(WebSocketContext);
@ -50,7 +125,7 @@ export default function AppRoutes() {
} }
element={<ViewEquipmentDocumentations />} element={<ViewEquipmentDocumentations />}
/> />
*/ */ /*
return ( return (
<Routes> <Routes>
@ -107,3 +182,4 @@ export default function AppRoutes() {
</Routes> </Routes>
); );
} }
*/

View File

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

View File

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

View File

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