first commit

This commit is contained in:
Robert Nasarek 2022-08-25 13:30:44 +02:00
commit 5ada72873f
30 changed files with 1286 additions and 0 deletions

77
src/DocxInserter.js Normal file
View file

@ -0,0 +1,77 @@
// Config
import config from './config/config.json'
// Modules
import createReport from 'docx-templates';
import fs from 'fs';
import { mkdir } from 'node:fs/promises';
import path from 'path';
// Components
import ObjektkatalogApi from './ObjektkatalogApi';
////////////////////
// Main //
////////////////////
export async function fillTemplate(log, objectData) {
let buffer;
// Create docx document.
if (objectData.httpStatus === 200) {
try {
// Read template.
const template = fs.readFileSync('src/assets/templates/rp-template.docx');
// Create report.
buffer = await createReport({
template,
data: {
inventarnummer: objectData.inventarnummer,
titel: objectData.titel,
hersteller: objectData.hersteller,
herstellungsort: objectData.herstellungsort,
herstellungsdatum: objectData.herstellungsdatum,
materialTechnik: objectData.materialTechnik,
masse: objectData.masse
}
});
} catch (err) {
log = {
...log,
status: 'red',
message: 'Konnte Template nicht öffnen: ' + err,
tip: 'Ist das Template vorhanden?',
};
}
const folderPath = path.join(config.rootDir, objectData.inventarnummer);
// Create Folder if necessary.
try {
const createDir = await mkdir(folderPath, {recursive: true});
} catch (err) {
log = {
...log,
status: 'green',
message: 'Konnte den Pfad nicht erstellen' + err,
tip: 'Bestehen Schreibrechte auf dem Ordner?'
}
}
// Write document to disk.
if (buffer) {
fs.writeFileSync(path.join(folderPath, 'report.docx'), buffer)
log = {
...log,
status: 'green',
message: 'Dokument erstellt',
};
}
} else {
log = {
status: 'red',
message: 'Fehler bei der Kommunikation mit dem Objektkatalog',
code: objectData.httpStatus,
tip: 'Ist die Inventarnummer richtig?',
};
}
return log;
}

131
src/ObjektkatalogApi.js Normal file
View file

@ -0,0 +1,131 @@
const {parseString} = require("xml2js");
/*
* Sends Object ID to Objektkatalog-API and receives JSON
*/
class ObjektkatalogApi {
json;
short;
raw;
async getData(objectId) {
if (objectId) {
const response = await fetch('https://objektkatalog.gnm.de/rest_export/' + objectId);
if (response.status === 200) {
const responseJson = await response.json(); //extract JSON from the http response
if (responseJson.length !== 0) {
this.raw = {
eid: responseJson[0]['eid'],
allgemeineBezeichnung: responseJson[0]['fc6392714594e73ddb2fa363815a8fdf'],
beschreibung: responseJson[0]['f81f557caccf45074edfb65ff077011f'],
darstellung: responseJson[0]['f217e1053b1e29411d4b00f3b1e1d52b'],
erwerbsmethode: responseJson[0]['fa4aa035d411275cd36a2fbb5c159a2c'],
fruehstes: responseJson[0]['ff4a178095c895a12fce6320c04ba0b0'],
fundort: responseJson[0]['f1c2bf9f6f3d78302bbfa9cc8b3e439c'],
gehoertZuAggregation: responseJson[0]['fca2cff9e713e02b93be1f9f19dff88e'],
herstellerString: responseJson[0]['f9f9408fdacc1230497c591c89655777'],
herstellungsdatum: responseJson[0]['fd9f0912229c78d94dd6f807e683d06e'],
herstellungsort: responseJson[0]['fbbccf2979c1143b0fa25325a849b121'],
individuelleEinordnung: responseJson[0]['f8e7a568a6a0fb8907b41f5ba8a9cabe'],
inventarnummer: responseJson[0]['f9830c2c7747a792ebbe1ca2587d1f26'],
klassifikation: responseJson[0]['fdcd0101d5c77884d185560aa7521645'],
masse: responseJson[0]['ffdac65ffbb12438e2fef5b26533c909'],
materialTechnik: responseJson[0]['f23b67bd18913642fc3936c1f2af5682'],
muster: responseJson[0]['f88917aeadae224c8d2c1fec35720dc8'],
provisio: responseJson[0]['f930a6ca774cc5640694c5aa081c1e66'],
sammlung: responseJson[0]['ffd22ba4399f8d62e61c4e99463dddaa'],
spaetestens: responseJson[0]['f1c2bf9f6f3d78302bbfa9cc8b3e439c'],
standort: responseJson[0]['f95e9be48ed110cf4f92298712d364bd'],
titel: responseJson[0]['f2049b2456b20f8fd80f714e154f1d47'],
unterbringung: responseJson[0]['f15265e39237868a568ee63453492b17'],
vitrinentext: responseJson[0]['fe5e3ff18aedf1d25c639dc79fb24ad1'],
zustandsbeschreibung: responseJson[0]['f54b6da6006ea444c33a348b8c4370a8']
}
} else {
return {httpStatus: 404};
}
} else {
return {httpStatus: response.status};
}
} else {
return {httpStatus: 400}
}
// Inventarnummer
let inventarnummer;
if (this.raw.inventarnummer.length !== 0) {
inventarnummer = this.raw.inventarnummer[0].value;
} else {
inventarnummer = 'unbekannt';
}
// Titel
let titel;
if (this.raw.titel.length !== 0) {
titel = this.raw.titel[0].value;
} else {
titel = 'unbekannt';
}
// Hersteller
let herstellerArray = [];
let hersteller;
if (this.raw.herstellerString.length !== 0) {
this.raw.herstellerString.forEach((item) => {
parseString(item.value, (err, result) => {
herstellerArray.push(result['results']['lido:eventActor'][0]['lido:actorInRole'][0]['lido:actor'][0]['lido:nameActorSet'][0]['lido:appellationValue'][0]['_']);
return hersteller = herstellerArray.join(';')
})
});
} else {
hersteller = 'unbekannt';
}
// Herstellungsort
let herstellungsort;
if (this.raw.herstellungsort.length !== 0) {
herstellungsort = this.raw.herstellungsort[0].value
} else {
herstellungsort = 'unbekannt';
}
// Herstellungsdatum
let herstellungsdatum;
if (this.raw.herstellungsdatum.length !== 0) {
herstellungsdatum = this.raw.herstellungsdatum[0].value
} else {
herstellungsdatum = 'unbekannt';
}
// Material und Technik
let materialTechnik;
if (this.raw.materialTechnik.length !== 0) {
materialTechnik = this.raw.materialTechnik[0].value
} else {
materialTechnik = 'unbekannt';
}
// Maße
let masse;
if (this.raw.masse.length !== 0) {
parseString(this.raw.masse[0].value, (err, result) => {
return masse = result['results']['lido:objectMeasurementsSet'][0]['lido:displayObjectMeasurements'][0];
})
}
return this.short = {
inventarnummer: inventarnummer,
titel: titel,
hersteller: hersteller,
herstellungsort: herstellungsort,
herstellungsdatum: herstellungsdatum,
materialTechnik: materialTechnik,
masse: masse,
httpStatus: 200,
}
}
}
module.exports = ObjektkatalogApi;

7
src/actions/getData.js Normal file
View file

@ -0,0 +1,7 @@
import GnmObject from "../ObjektkatalogApi";
/*
*
*/

3
src/assets/css/App.css Normal file
View file

@ -0,0 +1,3 @@
/* Main CSS file */
@import '_example/_example.css';

View file

@ -0,0 +1,22 @@
/* Example stylesheet */
@media screen and (prefers-color-scheme: light), screen and (prefers-color-scheme: no-preference) {
/* Light theme */
body{
color: #000;
background-color: #fff;
}
}
@media screen and (prefers-color-scheme: dark) {
/* Dark theme */
body {
color: #fff;
background-color: #000;
}
}
h1 {
font-family: Helvetica, Arial, sans-serif;
font-size: 21px;
font-weight: 200;
}

Binary file not shown.

Binary file not shown.

121
src/components/App.js Normal file
View file

@ -0,0 +1,121 @@
// Components
import {DataForm} from "./DataForm";
import {Log} from "./Log"
// Modules
import {fillTemplate} from "../DocxInserter";
import SettingsIcon from '@mui/icons-material/Settings';
// React
import { Link } from "react-router-dom";
import React, {useState} from 'react'
// CSS
import '../styles.css'
import ObjektkatalogApi from "../ObjektkatalogApi";
////////////////////
// Main //
////////////////////
export const App = () => {
let log;
// States
/* Initialize state of the log.
* log: variables fpr logging message.
* logClass: for visibility of log <div>.
*/
const [logState, setLogState] = useState({
log: {
status: '',
message: '',
code: '',
tip:'',
}, // with message, code, tip
logClass: 'inactive'
});
const [objectData, setObjectData] = useState(
{
inventarnummer: '',
titel: '',
hersteller: '',
herstellungsort: '',
herstellungsdatum: '',
materialTechnik: '',
masse: '',
httpStatus: 500,
}
);
const [checkUpVisibility, setCheckUpVisibility] = useState(
false
);
// Handler
async function getDataAndAskForCheckUpClickHandler(e) {
// Prevent page reload.
e.preventDefault();
// Get objectId from user input.
let objectId = document.getElementById('object-id').value
// Get object data from objektkatalog.gnm.de
let objektkatalogApi = new ObjektkatalogApi();
let objectData = await objektkatalogApi.getData(objectId);
console.log(objectData)
log = await fillTemplate(log, objectData);
// Set new state of log div with message and visibility class
setLogState({
log: log,
logStatus: 'active'
});
}
return (
<div className="App">
<header className="App-header">
<nav className={"flex justify-content-end"}>
<Link to="/settings"><SettingsIcon/></Link>
</nav>
</header>
<main>
<div className={"container"}>
<form id={"object-id-form"} className={"flex-wrap"}>
<div className={"center column flex justify-content-space-between "}>
<label htmlFor="document-type" className={"center cut v-distance"}>Dokumenttyp:</label>
<select name="document-type" id="document-type" className={"input-field center cut"}>
<option value="restaurierungsprotokoll">Restaurierungsprotokoll</option>
<option value="leihgabenbegleitblatt">Leihgabenbegleitblatt</option>
<option value="analyse">Analyse</option>
</select>
</div>
<div className={"center column flex full justify-content-space-between"}>
<label htmlFor={"object-id"} className={"center cut v-distance"}>Inventarnummer:</label>
<input id={"object-id"} type={"text"} className={"center cut input-field"}/>
</div>
<button className={'send-button center top-distance'} onClick={getDataAndAskForCheckUpClickHandler}>
Dokument vorbereiten
</button>
</form>
</div>
<DataForm/>
</main>
<footer>
{/* We give state and state setter of the parent as params to the child components, so that child events can change parent states */}
<Log logState={logState} setLogState={setLogState}/>
</footer>
</div>
)
}
export default App

View file

@ -0,0 +1,43 @@
// React
import React from 'react'
////////////////////
// Main //
////////////////////
export const DataForm = () => {
return (<div>
<form className={'flex column'}>
<div >
<label htmlFor={'template-inventarnummer'} >Inventarnummer</label>
<input className={"full input-field"}/>
</div>
<div >
<label htmlFor={'template-titel'}>Titel</label>
<input type={"text"} className={"full input-field"}/>
</div>
<div>
<label htmlFor={'template-hersteller'}>Hersteller</label>
<input type={"text"} className={"full input-field"}/>
</div>
<div>
<label htmlFor={'template-herstellungsort'}>Herstellungsort</label>
<input type={"text"} className={"full input-field"}/>
</div>
<div >
<label htmlFor={'template-herstellungsdatum'}>Herstellungsdatum</label>
<input type={"text"} className={"full input-field"}/>
</div>
<div >
<label htmlFor={'template-materialTechnik'}>Material und Technik</label>
<input type={"text"} className={"full input-field"}/>
</div>
<div>
<label htmlFor={'template-masse'}>Maße</label>
<input type={"text"} className={"full input-field"}/>
</div>
<button type={"submit"} className={'send-button center top-distance'} >Dokument erstellen</button>
</form>
</div>
)
}

45
src/components/Log.js Normal file
View file

@ -0,0 +1,45 @@
// React
import React from 'react'
////////////////////
// Main //
////////////////////
export const Log = ({logState, setLogState}) => {
let code;
let tip;
const closeLog = (e, logClass) => {
e.preventDefault()
setLogState({log: logState.log,
logClass: 'inactive'});
}
const classes = 'round-box flex ' + logState.log.status + ' ' + logState.logClass
if (logState.log.code) {
code = ' Code: ' + logState.log.code
} else {
code = ''
}
if (logState.log.tip) {
tip = ' Tipp: ' + logState.log.tip
} else {
tip = ''
}
return (
<div className={'log '}>
<div id={'log-content'} className={classes}>
<div>
<button onClick={(e) => {closeLog(e)}}>x</button>
</div>
<div className={'scroll-y flex-full'} style={{maxHeight: 4 + 'em'}}>
<p>{logState.log.message}</p>
<p>{code}</p>
<p>{tip}</p>
</div>
</div>
</div>
)
}

3
src/config/config.json Normal file
View file

@ -0,0 +1,3 @@
{
"rootDir": "/home/rbrt/Dokumente/marvin"
}

18
src/index.html Normal file
View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<title>Marvin</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>

26
src/index.js Normal file
View file

@ -0,0 +1,26 @@
// React
import {createRoot} from 'react-dom/client'
import {
HashRouter,
Routes,
Route} from "react-router-dom";
import React from "react";
// Components
import App from './components/App'
// Routes
import Settings from "./routes/settings";
// entry index.html is managed by webpack via HtmlWebpackPlugin
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<HashRouter>
<Routes>
<Route path='/' element={<App />} />
<Route path='settings' element={<Settings />} />
</Routes>
</HashRouter>
);

79
src/routes/settings.js Normal file
View file

@ -0,0 +1,79 @@
import config from '../config/config.json'
// Icons
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import EditIcon from '@mui/icons-material/Edit';
// Modules
const {writeFile} = require('fs');
// React
import React, {useState} from "react";
import {Link} from "react-router-dom";
////////////////////
// Main //
////////////////////
export default function Settings() {
let rootDir;
const [disableButton, setDisableButton] = useState(true)
async function selectFolderHandler() {
return rootDir = await window.openFile();
}
// Handler
function saveInputClickHandler(rootDir) {
if (rootDir) {
config.rootDir = rootDir
writeFile('src/config/config.json', JSON.stringify(config, null, 2), (error) => {
if (error) {
console.log('An error has occurred ', error);
return;
}
console.log('Data written successfully to disk');
});
}
}
return (
<div className="App">
<header className="App-header">
<Link to="/"><ArrowBackIosIcon/></Link>
</header>
<main>
<form>
<label htmlFor={"output-root"}>
Wurzelverzeichnis
</label>
<div className="col-sm-10 d-flex align-items-center flex sticky-edit">
<input
id={"root-dir"}
className={"form-control select-folder-field edit-input"}
defaultValue={config.rootDir}
disabled={true}
/>
<button className="edit-button text-end" onClick={
(e) => {
console.log(e)
e.preventDefault()
selectFolderHandler().then((rootDir) => {saveInputClickHandler(rootDir)})
}
}>
<i className="fas fa-edit d-block">
<EditIcon sx={{fontSize: 16}} color={'#d5d5d5'}/>
</i>
</button>
</div>
</form>
</main>
<footer>
</footer>
</div>
);
}

7
src/shared/constants.js Normal file
View file

@ -0,0 +1,7 @@
module.exports = {
channels: {
GET_DATA: 'get_data',
},
log: {
}
};

195
src/styles.css Normal file
View file

@ -0,0 +1,195 @@
@import url('https://fonts.googleapis.com/css?family=Roboto');
a {
color: #d5d5d5;
}
.active {
display: block;
}
body {
background-color: #00152E ;
color: #d5d5d5;
font-family: 'Roboto', regular, serif;
font-size: larger;
}
.center {
margin: 0 auto;
}
.column {
flex-direction: column;
}
.container {
width: 300px;
margin: 0 auto;
}
.cut {
max-width: max-content;
}
div {
padding: 0.5em;
}
.edit-button {
background-color: #d5d5d5;
font-family: roboto, sans-serif;
font-size: 1rem;
border-radius: 0 0.4rem 0.4rem 0;
border: unset;
padding: 0 4px;
height: 1.1rem;
}
.edit-input {
border-radius: 0.4rem 0 0 0.4rem;
}
.flex {
display: flex;
}
.flex-full {
flex: 1
}
.flex-wrap {
display: flex;
flex-wrap: wrap;
}
.full {
width: 100%;
}
.green {
background-color: darkgreen;
}
.inactive {
display: none;
}
input {
border-top: none;
border-right: none;
border-left: none;
border-bottom: 2px solid darkred;
border-radius: 3px;
}
input:focus {
background-color: #d5d5d5;
color: #00152E;
}
input.edit-input:not(:disabled) {
background-color: #d5d5d5;
color: #00152E;
}
.input-field {
background-color: #00152E ;
color: #d5d5d5;
font-size: medium;
}
.justify-content-end {
justify-content: end;
}
.justify-content-space-between {
justify-content: space-between;
}
label {
padding-right: 5px;
}
.log {
font-size: 0.75em;
height: 5em;
}
p {
margin: 0;
}
.position-absolute {
position: absolute;
}
.position-relative {
position: relative;
}
.red {
background-color: crimson;
}
.round-box {
border-radius: 6px;
}
select {
border: none;
font-size: medium;
outline: none;
}
.send-button {
font-family: roboto, sans-serif;
font-size: 1rem;
border-radius: 0.4rem;
border: unset;
background-color: darkred;
color: #d5d5d5;
}
.scroll-y {
overflow-y: scroll;
}
.select-folder-field {
background-color: #00152E ;
color: #d5d5d5;
width: 100%;
font-size: .85rem;
}
.sticky-edit {
display: flex;
align-items: flex-start;
}
textarea, textarea:focus, input:focus{
outline: none;
font-size: medium;
;
}
textarea.select-folder-field, textarea.select-folder-field:focus, input.select-folder-field:focus {
font-size: .85rem;
}
.top-distance {
margin-top: 1em;
}
.v-distance {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
.yellow{
background-color: goldenrod;
}