diff --git a/db/mongo.js b/db/mongo.js
index 3a0c7a2..8cbfcd8 100644
--- a/db/mongo.js
+++ b/db/mongo.js
@@ -40,7 +40,6 @@ export function getCollections() {
export const update_pflux = async(sn, doc) => {
try {
- let r = await prop_fluxCollection.findOne({_id: sn})
await prop_fluxCollection.updateOne({_id: sn},{ $set: { 'chip': doc}})
return {"error": null}
} catch (e) {
diff --git a/package.json b/package.json
index 369b895..28bf42d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "espid2sensor",
- "version": "1.0.0",
- "date": "2025-08-19 16:00 UTC",
+ "version": "1.1.0",
+ "date": "2025-09-01 17:00 UTC",
"type": "module",
"description": "Kleine Webapp ESP-ID <-> Sensornummer, speichern in MongoDB",
"main": "server.js",
diff --git a/public/global.js b/public/global.js
index 501853c..fa2974a 100644
--- a/public/global.js
+++ b/public/global.js
@@ -42,35 +42,66 @@ document.addEventListener('DOMContentLoaded', () => {
let editId = null;
// Modal für Fehleranzeige
- function showModal(message, callback) {
- // Vorherige Modals entfernen
- document.querySelectorAll('.custom-modal-popup').forEach(m => m.remove());
+function showModal(message, showCancelButton, callback) {
+ // Remove previous modals
+ document.querySelectorAll('.custom-modal-popup').forEach(m => m.remove());
- let modal = document.createElement('div');
- modal.className = 'custom-modal-popup';
+ let modal = document.createElement('div');
+ modal.className = 'custom-modal-popup';
- let box = document.createElement('div');
- box.className = 'custom-modal-box';
+ let box = document.createElement('div');
+ box.className = 'custom-modal-box';
- let msg = document.createElement('div');
- msg.className = 'custom-modal-msg';
- msg.textContent = message;
- box.appendChild(msg);
+ let msg = document.createElement('div');
+ msg.className = 'custom-modal-msg';
+ msg.textContent = message;
+ box.appendChild(msg);
- let btn = document.createElement('button');
- btn.className = 'custom-modal-btn';
- btn.textContent = 'OK';
- btn.onclick = () => {
+ let btndiv = document.createElement('div')
+ btndiv.className = 'twobuttons'
+
+ // Cancel Button (only if showCancelButton is true)
+ if (showCancelButton) {
+ let btnCancel = document.createElement('button');
+ btnCancel.className = 'custom-modal-btn';
+ btnCancel.textContent = 'Abbruch';
+ btnCancel.onclick = () => {
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
- if (callback) callback();
+ if (callback) callback(false); // Pass false for Cancel
};
- box.appendChild(btn);
- modal.appendChild(box);
- document.body.appendChild(modal);
+ btndiv.appendChild(btnCancel);
}
+ // OK Button
+ let btnOk = document.createElement('button');
+ btnOk.className = 'custom-modal-btn';
+ btnOk.textContent = 'OK';
+ btnOk.onclick = () => {
+ if (modal.parentNode) {
+ modal.parentNode.removeChild(modal);
+ }
+ if (callback) callback(true); // Pass true for OK
+ };
+
+ btndiv.appendChild(btnOk);
+ box.appendChild(btndiv)
+
+
+ modal.appendChild(box);
+ document.body.appendChild(modal);
+
+ // Optional: Close modal when clicking outside
+ modal.onclick = (e) => {
+ if (e.target === modal) {
+ if (modal.parentNode) {
+ modal.parentNode.removeChild(modal);
+ }
+ if (callback) callback(false); // Treat as cancel
+ }
+ };
+}
// Sensornummer nur Zahlen erlauben
sensorNumberInput.addEventListener('input', () => {
sensorNumberInput.value = sensorNumberInput.value.replace(/\D/g, '');
@@ -99,7 +130,7 @@ document.addEventListener('DOMContentLoaded', () => {
} else {
addressInput.value = '';
sensorNumberInput.disabled = true;
- showModal('Sensor unbekannt', () => {
+ showModal('Sensor unbekannt', false, () => {
sensorNumberInput.disabled = false;
sensorNumberInput.focus();
});
@@ -108,7 +139,7 @@ document.addEventListener('DOMContentLoaded', () => {
console.error('Fehler beim Abrufen der Adresse:', err);
addressInput.value = '';
sensorNumberInput.disabled = true;
- showModal('Sensor unbekannt', () => {
+ showModal('Sensor unbekannt', false, () => {
sensorNumberInput.disabled = false;
sensorNumberInput.focus();
});
@@ -141,8 +172,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
try {
- const url = editId ? `/api/update/${editId}` : '/api/save';
- const method = editId ? 'PUT' : 'POST';
+ const url = '/api/save';
+ const method = 'POST';
const res = await fetch(url, {
method,
@@ -154,8 +185,12 @@ document.addEventListener('DOMContentLoaded', () => {
if (data.error) {
resultDiv.textContent = data.error;
} else {
- resultDiv.textContent = editId ? 'Aktualisiert!' : 'Gespeichert!';
- clearForm();
+ resultDiv.textContent = 'OK!';
+ setTimeout(() => {
+ resultDiv.textContent = ''
+ saveBtn.textContent = 'Speichern';
+ }, 5000)
+ clearForm(false);
await loadEntries();
}
} catch (err) {
@@ -165,14 +200,16 @@ document.addEventListener('DOMContentLoaded', () => {
}
- function clearForm() {
+ function clearForm(mitButton) {
espIdInput.value = '';
sensorNumberInput.value = '';
nameInput.value = '';
descriptionInput.value = '';
addressInput.value = '';
editId = null;
- saveBtn.textContent = 'Speichern';
+ if (mitButton) {
+ saveBtn.textContent = 'Speichern';
+ }
}
// Globale Sortier-Variable
@@ -193,16 +230,19 @@ document.addEventListener('DOMContentLoaded', () => {
function renderTable(sortedItems) {
tableBody.innerHTML = '';
sortedItems.forEach(item => {
- const date = new Date(item.chip.createdAt).toISOString().split('T')[0];
+ const date = new Date(item.chip.lastUpdatedAt).toISOString().split('T')[0];
const tr = document.createElement('tr');
tr.innerHTML = `
-
${item._id} |
+ ${item._id} |
${item.chip.id} |
${item.chip.name || ''} |
${item.chip.description || ''} |
${date} |
-
+
+
+
+
|
`;
tableBody.appendChild(tr);
@@ -219,8 +259,8 @@ document.addEventListener('DOMContentLoaded', () => {
valA = a.chip.id;
valB = b.chip.id;
} else if (key === 'date') {
- valA = new Date(a.chip.createdAt);
- valB = new Date(b.chip.createdAt);
+ valA = new Date(a.chip.lastUpdatedAt);
+ valB = new Date(b.chip.lastUpdatedAt);
}
if (valA < valB) return asc ? -1 : 1;
if (valA > valB) return asc ? 1 : -1;
@@ -303,22 +343,29 @@ document.addEventListener('DOMContentLoaded', () => {
}
async function deleteEntry(id) {
- if (!confirm('Wirklich löschen?')) return;
- try {
- const res = await fetch(`/api/delete/${id}`, { method: 'DELETE' });
- const data = await res.json();
- if (data.success) {
- await loadEntries();
+ showModal('Wirklich löschen?', true, async (confirmed) => {
+ if (confirmed) {
+ try {
+ const res = await fetch(`/api/delete/${id}`, { method: 'DELETE' });
+ const data = await res.json();
+ if (data.success) {
+ await loadEntries();
+ resultDiv.textContent = 'Eintrag gelöscht.';
+ setTimeout(() => resultDiv.textContent = '', 3000);
+ } else {
+ resultDiv.textContent = 'Fehler beim Löschen.';
+ }
+ } catch (err) {
+ console.error(err);
+ resultDiv.textContent = 'Fehler beim Löschen.';
}
- } catch (err) {
- console.error(err);
- resultDiv.textContent = 'Fehler beim Löschen.';
}
- }
-
+ });
+}
saveBtn.addEventListener('click', saveEntry);
refreshBtn.addEventListener('click', loadEntries);
+ cancelBtn.addEventListener('click', () => clearForm(true));
tabInput.addEventListener('click', () => showTab('input'))
tabList.addEventListener('click', () => showTab('list'))
diff --git a/public/styles.css b/public/styles.css
index d010417..40953d1 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -99,21 +99,18 @@ table {
th, td {
text-align: left;
- padding: 8px;
- border-bottom: 1px solid #eee;
+ padding: 4px;
+ border-bottom: 1px solid #888;
}
-#tdDate {
- width: 8em;
-}
+/* Spaltenbreiten über colgroup steuern */
+col.col-sensornumber { width: 7em; }
+col.col-espid {width: 6em}
+col.col-bezeichnung { width: 8em; }
+col.col-beschreibung{ width: 12em; }
+col.col-date { width: 10em; }
+col.col-aktionen { width: 18em; }
-#tdBeschreibung {
- width: 10em;
-}
-
-#tdAktionen {
- width: 20em;
-}
.controls input#page,
.controls input#limit {
@@ -163,8 +160,10 @@ button:hover {
background: #0056b3;
}
-#saveBtn {
- margin-top: 20px;
+.twobuttons {
+ display:flex;
+ width: 100%;
+ justify-content: space-between;
}
p.error {
diff --git a/routes/api.js b/routes/api.js
index 59eb7e3..b7765e6 100644
--- a/routes/api.js
+++ b/routes/api.js
@@ -2,8 +2,6 @@ import { MongoAPIError, ObjectId } from 'mongodb';
import bcrypt from 'bcrypt';
import { getCollections, update_pflux } from '../db/mongo.js';
-
-
export function registerApiRoutes(app, requireLogin) {
const { usersCollection, prop_fluxCollection } = getCollections();
@@ -20,7 +18,7 @@ export function registerApiRoutes(app, requireLogin) {
});
app.post('/api/save', requireLogin, async (req, res) => {
- let { espId, sensorNumber, name, description, address } = req.body;
+ let { espId, sensorNumber, name, description} = req.body;
if (!espId || !sensorNumber) {
return res.json({ error: 'ESP-ID und Sensornummer sind Pflichtfelder' });
}
@@ -30,7 +28,7 @@ export function registerApiRoutes(app, requireLogin) {
id: espId,
name: name || '',
description: description || '',
- createdAt: new Date()
+ lastUpdatedAt: new Date()
};
await update_pflux(sensorNumber, doc)
res.json({ success: true });
@@ -40,24 +38,6 @@ export function registerApiRoutes(app, requireLogin) {
}
});
- app.put('/api/update/:id', requireLogin, async (req, res) => {
- const { id } = req.params;
- let { espId, sensorNumber, name, description, address } = req.body;
- if (!espId || !sensorNumber) {
- return res.json({ error: 'ESP-ID und Sensornummer sind Pflichtfelder' });
- }
- sensorNumber = parseInt(sensorNumber, 10);
- try {
- await prop_fluxCollection.updateOne(
- { _id: new ObjectId(id) },
- { $set: { espId, sensorNumber, name, description, address } }
- );
- res.json({ success: true });
- } catch (err) {
- console.error(err);
- res.status(500).json({ error: 'Fehler beim Aktualisieren' });
- }
- });
app.get('/api/list', requireLogin, async (req, res) => {
const { id } = req.query;
@@ -83,7 +63,7 @@ export function registerApiRoutes(app, requireLogin) {
const skip = (page - 1) * limit;
try {
const items = await prop_fluxCollection.find({chip: {$exists: true}})
- .sort({ "chip.createdAt": -1 })
+ .sort({ "chip.lastUpdatedAt": -1 })
.skip(skip)
.limit(limit)
.toArray();
@@ -96,7 +76,7 @@ export function registerApiRoutes(app, requireLogin) {
});
app.delete('/api/delete/:id', requireLogin, async (req, res) => {
- await prop_fluxCollection.deleteOne({ _id: new ObjectId(req.params.id) });
+ await prop_fluxCollection.deleteOne({ _id: parseInt(req.params.id) });
res.json({ success: true });
});
}
diff --git a/tests/server.test.js b/tests/server.test.js
index c47c757..e0ce57d 100644
--- a/tests/server.test.js
+++ b/tests/server.test.js
@@ -32,7 +32,7 @@ describe('Server.js API', () => {
return res.json({ error: 'ESP-ID und Sensornummer sind Pflichtfelder' });
}
sensorNumber = parseInt(sensorNumber, 10);
- const doc = { espId, sensorNumber, name, description, address, createdAt: new Date(), _id: String(entries.length + 1) };
+ const doc = { espId, sensorNumber, name, description, address, lastUpdatedAt: new Date(), _id: String(entries.length + 1) };
entries.push(doc);
res.json({ success: true });
});
@@ -115,7 +115,7 @@ describe('Server.js API', () => {
});
test('PUT /api/update/:id updates entry', async () => {
- entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', createdAt: new Date() });
+ entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', lastUpdatedAt: new Date() });
const res = await request(app).put('/api/update/1').send({ espId: 'esp2', sensorNumber: '1002', name: 'Neu', description: 'Neu', address: 'Neu' });
expect(res.body).toHaveProperty('success', true);
expect(entries[0].espId).toBe('esp2');
@@ -127,20 +127,20 @@ describe('Server.js API', () => {
});
test('GET /api/list returns all entries', async () => {
- entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', createdAt: new Date() });
+ entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', lastUpdatedAt: new Date() });
const res = await request(app).get('/api/list');
expect(res.body.length).toBe(1);
});
test('GET /api/list?id returns specific entry', async () => {
- entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', createdAt: new Date() });
+ entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', lastUpdatedAt: new Date() });
const res = await request(app).get('/api/list?id=1');
expect(res.body.length).toBe(1);
expect(res.body[0]._id).toBe('1');
});
test('DELETE /api/delete/:id deletes entry', async () => {
- entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', createdAt: new Date() });
+ entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', lastUpdatedAt: new Date() });
const res = await request(app).delete('/api/delete/1');
expect(res.body).toHaveProperty('success', true);
expect(entries.length).toBe(0);
diff --git a/views/index.pug b/views/index.pug
index 3a78db2..8182ec3 100644
--- a/views/index.pug
+++ b/views/index.pug
@@ -31,7 +31,9 @@ html(lang="de")
label(for="address") Anschrift:
input#address(type="text" placeholder="Wird automatisch ausgefüllt" readonly disabled)
- button#saveBtn(type="button") Speichern
+ .twobuttons
+ button#saveBtn(type="button") Speichern
+ button#cancelBtn(type="button") Abbrechen
div#result
// Listen-Tab
@@ -45,6 +47,13 @@ html(lang="de")
span#gzahl
table#entriesTable
+ colgroup
+ col.col-sensornumber
+ col.col-espid
+ col.col-bezeichnung
+ col.col-beschreibung
+ col.col-date
+ col.col-aktionen
thead
tr
th(id="thSensorNr" data-sort="sensorNr" style="cursor:pointer") SensorNr ↑