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