diff --git a/backend/src/index.js b/backend/src/index.js index c2a314f..ef5d57a 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -289,6 +289,9 @@ setTimeout(() => { async function runScheduler() { mqttClient.publishPresence(); + mqttClient.publishMediaAnfragen(); + mqttClient.publishBestellungen(); + mqttClient.publishDruckStatistik(); try { const now = db.prepare("SELECT datetime('now','localtime') as t").get().t; const due = db.prepare(` diff --git a/backend/src/mqtt.js b/backend/src/mqtt.js index 9bd13d5..f507ee1 100644 --- a/backend/src/mqtt.js +++ b/backend/src/mqtt.js @@ -114,6 +114,40 @@ function publishDiscovery() { availability_topic: AVAILABILITY_TOPIC, device: DEVICE, }], + ['sensor', 'dickendock_media_anfragen', { + name: 'Media Anfragen offen', + unique_id: 'dickendock_media_anfragen', + state_topic: `${BASE_TOPIC}/media/anfragen`, + json_attributes_topic: `${BASE_TOPIC}/media/anfragen_attributes`, + icon: 'mdi:movie-open-star', + unit_of_measurement: 'Anfragen', + state_class: 'measurement', + availability_topic: AVAILABILITY_TOPIC, + device: DEVICE, + }], + ['sensor', 'dickendock_bestellungen_offen', { + name: 'Bestellungen offen', + unique_id: 'dickendock_bestellungen_offen', + state_topic: `${BASE_TOPIC}/bestellungen/offen`, + json_attributes_topic: `${BASE_TOPIC}/bestellungen/attributes`, + icon: 'mdi:cube-send', + unit_of_measurement: 'Bestellungen', + state_class: 'measurement', + availability_topic: AVAILABILITY_TOPIC, + device: DEVICE, + }], + ['sensor', 'dickendock_3ddruck_umsatz_monat', { + name: '3D-Druck Umsatz (Monat)', + unique_id: 'dickendock_3ddruck_umsatz_monat', + state_topic: `${BASE_TOPIC}/druck/umsatz_monat`, + json_attributes_topic: `${BASE_TOPIC}/druck/attributes`, + icon: 'mdi:printer-3d', + device_class: 'monetary', + unit_of_measurement: '€', + state_class: 'measurement', + availability_topic: AVAILABILITY_TOPIC, + device: DEVICE, + }], ]; for (const [component, objectId, payload] of entities) { client.publish(`homeassistant/${component}/${objectId}/config`, JSON.stringify(payload), { retain: true }); @@ -207,7 +241,89 @@ function publishPresence() { }), { retain: true }); } +// Wird jede Minute vom Scheduler aufgerufen — offene (unquittierte) Media-Favoriten, +// systemweit über alle Nutzer (spiegelt die Admin-Ansicht in "Media/Favoriten") +function publishMediaAnfragen() { + if (!client?.connected) return; + const offen = db.prepare('SELECT COUNT(*) AS n FROM movie_favorites WHERE acknowledged=0').get().n; + const liste = db.prepare(` + SELECT f.title, u.username, f.added_at + FROM movie_favorites f JOIN users u ON u.id = f.user_id + WHERE f.acknowledged = 0 + ORDER BY f.added_at DESC LIMIT 20 + `).all(); + client.publish(`${BASE_TOPIC}/media/anfragen`, String(offen), { retain: true }); + client.publish(`${BASE_TOPIC}/media/anfragen_attributes`, JSON.stringify({ anfragen: liste }), { retain: true }); +} + +// Wird jede Minute vom Scheduler aufgerufen — offene 3D-Druck-Bestellungen, +// systemweit über alle Nutzer, Status-Verteilung als Attribut +function publishBestellungen() { + if (!client?.connected) return; + const orders = db.prepare('SELECT status, bezahlt, abgeholt FROM orders').all(); + const byStatus = { warteliste: 0, in_arbeit: 0, fertig: 0, bezahlt: 0, abgeschlossen: 0 }; + for (const o of orders) { + if (o.bezahlt && o.abgeholt) byStatus.abgeschlossen++; + else if (o.bezahlt) byStatus.bezahlt++; + else if (o.status in byStatus) byStatus[o.status]++; + } + const offen = orders.length - byStatus.abgeschlossen; + client.publish(`${BASE_TOPIC}/bestellungen/offen`, String(offen), { retain: true }); + client.publish(`${BASE_TOPIC}/bestellungen/attributes`, JSON.stringify({ + ...byStatus, + gesamt: orders.length, + }), { retain: true }); +} + +function round2(n) { return Math.round((n || 0) * 100) / 100; } + +// Umsatz eines einzelnen Auftrags — Festpreis falls gesetzt, sonst Summe der +// Positions-Festpreise (spiegelt die Logik aus statistik/routes.js) +function getOrderRevenue(order) { + if (order.custom_price != null && order.custom_price > 0) return parseFloat(order.custom_price); + const r = db.prepare(` + SELECT COALESCE(SUM(custom_price * stueckzahl), 0) AS s + FROM order_items WHERE order_id=? AND custom_price IS NOT NULL + `).get(order.id); + return r?.s || 0; +} + +// Wird jede Minute vom Scheduler aufgerufen — 3D-Druck-Umsatz im laufenden Kalendermonat, +// systemweit über alle Nutzer (bezahlte Aufträge nach bezahlt_am, sonst created_at) +function publishDruckStatistik() { + if (!client?.connected) return; + const now = new Date(); + const pad = n => String(n).padStart(2, '0'); + const monatsAnfang = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-01`; + const heute = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; + + const allOrders = db.prepare('SELECT * FROM orders').all(); + const bezahltDiesenMonat = allOrders.filter(o => { + if (!o.bezahlt) return false; + const d = (o.bezahlt_am || o.created_at || '').slice(0, 10); + return d >= monatsAnfang && d <= heute; + }); + const umsatzMonat = bezahltDiesenMonat.reduce((s, o) => s + getOrderRevenue(o), 0); + + const ausgabenMonat = db.prepare(` + SELECT COALESCE(SUM(amount), 0) AS s FROM expenses WHERE date>=? AND date<=? + `).get(monatsAnfang, heute).s; + + const offeneAuftraege = allOrders.filter(o => !(o.bezahlt && o.abgeholt)).length; + + client.publish(`${BASE_TOPIC}/druck/umsatz_monat`, String(round2(umsatzMonat)), { retain: true }); + client.publish(`${BASE_TOPIC}/druck/attributes`, JSON.stringify({ + ausgaben_monat: round2(ausgabenMonat), + netto_monat: round2(umsatzMonat - ausgabenMonat), + bezahlte_auftraege_monat: bezahltDiesenMonat.length, + offene_auftraege_gesamt: offeneAuftraege, + }), { retain: true }); +} + // Wird von index.js gesetzt, damit der HA-Button ohne Require-Zirkel den Check auslösen kann function setKoepiCheckHandler(fn) { koepiCheckHandler = fn; } -module.exports = { connectMqtt, publishKoepiState, publishPresence, setKoepiCheckHandler }; +module.exports = { + connectMqtt, publishKoepiState, publishPresence, setKoepiCheckHandler, + publishMediaAnfragen, publishBestellungen, publishDruckStatistik, +};