aether-framework/resources/[aether]/aether-medical/server/module/kommunikation.lua
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark,
  Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI)
- aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource,
  an aether-core gekoppelt (Rechte über aether_users.admin_group), mit
  WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU)
- Prototypen admin/ vervollständigt
- SQL-Schema aether_admin.sql
- Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:55:32 +02:00

761 lines
31 KiB
Lua

-- =====================================================================
-- aether-medical / Modul: Nachrichten und Aushänge
--
-- Ein Postfach und ein Schwarzes Brett.
--
-- Was hier steht, hat jemand geschrieben und mit seinem Namen
-- unterschrieben. Deshalb gilt durchgehend:
--
-- * Eine abgeschickte Nachricht wird nicht mehr geändert und nicht
-- gelöscht. Sie ist raus.
-- * Ein Aushang wird archiviert, nie entfernt — auch ein
-- zurückgenommener bleibt nachlesbar.
-- * Eine Kenntnisnahme bestätigt nur, dass jemand gelesen hat.
-- Niemand kann sie für einen anderen setzen, und sie ändert
-- nichts ausser sich selbst.
-- * Eine Frist läuft ab, ohne dass etwas geschieht. Das System
-- erinnert daran und wartet.
-- =====================================================================
-- Lesen darf jeder im Dienst: was gelesen werden muss, soll nicht an
-- einem Recht scheitern. Geschrieben wird nur mit Recht.
Kern.RegistriereModul({
id = 'kommunikation', name = 'Nachrichten', icon = '✉',
reihenfolge = 13, recht = nil,
beschreibung = 'Postfach, Rundmeldungen und Schwarzes Brett',
})
-- ---------------------------------------------------------------------
-- Hilfsfunktionen
-- ---------------------------------------------------------------------
--- Begrenzt einen Text und liefert nil bei leerer Eingabe
--- @param wert any Eingabe
--- @param laenge number Maximale Zeichenzahl
--- @return string|nil
local function Text(wert, laenge)
if type(wert) ~= 'string' then return nil end
local sauber = wert:gsub('^%s+', ''):gsub('%s+$', '')
if sauber == '' then return nil end
return sauber:sub(1, laenge)
end
--- Name eines Charakters für die Anzeige
--- @param charakterId number|nil Charakter
--- @return string
local function NameVon(charakterId)
if not charakterId then return 'unbekannt' end
local zeilen = MySQL.query.await(
'SELECT firstname, lastname FROM aether_characters WHERE id = ?', { charakterId })
if not zeilen or not zeilen[1] then return 'unbekannt' end
return zeilen[1].firstname .. ' ' .. zeilen[1].lastname
end
--- Anzeigename einer Station aus der Konfiguration
--- @param kennung string|nil Stationskennung
--- @return string
local function StationName(kennung)
if not kennung then return 'gesamte Klinik' end
for _, station in ipairs(Medizin.Config.Stationen) do
if station.id == kennung then return station.name end
end
return tostring(kennung)
end
--- Prüft eine Kennung gegen eine Konfigurationsliste
--- @param liste table Liste aus Medizin.Config
--- @param wert any Eingabe
--- @return string|nil
local function AusListe(liste, wert)
local gesucht = tostring(wert or '')
for _, eintrag in ipairs(liste) do
if eintrag.id == gesucht then return eintrag.id end
end
return nil
end
--- Beschreibt, an wen eine Nachricht ging
--- @param eintrag table Nachrichtenzeile
--- @return string
local function EmpfaengerName(eintrag)
if eintrag.empf_typ == 'rundmeldung' then return 'Alle im Haus' end
if eintrag.empf_typ == 'station' then
return 'Station ' .. StationName(eintrag.empfaenger)
end
return NameVon(tonumber(eintrag.empfaenger))
end
--- Kürzt einen Text für die Listenansicht
--- @param wert string|nil Volltext
--- @param laenge number Zeichen
--- @return string
local function Vorschau(wert, laenge)
local roh = tostring(wert or ''):gsub('%s+', ' ')
if #roh <= laenge then return roh end
return roh:sub(1, laenge) .. '…'
end
--- Erreicht diese Nachricht den genannten Mitarbeiter?
--- @param eintrag table Nachrichtenzeile
--- @param mitarbeiter table Mitarbeiter
--- @return boolean
local function IstEmpfaenger(eintrag, mitarbeiter)
if eintrag.empf_typ == 'rundmeldung' then return true end
if eintrag.empf_typ == 'station' then
return eintrag.empfaenger == mitarbeiter.station
end
return tonumber(eintrag.empfaenger) == mitarbeiter.charakterId
end
-- =====================================================================
-- ÜBERSICHT
-- =====================================================================
Kern.RegistriereAktion('kommunikation', 'uebersicht', nil, function(quelle, mitarbeiter, daten)
local ordner = tostring(daten.ordner or 'posteingang')
local ich = mitarbeiter.charakterId
local liste = {}
if ordner == 'gesendet' then
liste = MySQL.query.await([[
SELECT n.id, n.empfaenger, n.empf_typ, n.betreff, n.text, n.gesendet,
(SELECT COUNT(*) FROM medical_nachricht_gelesen g
WHERE g.nachricht = n.id) AS gelesenVon
FROM medical_nachricht n
WHERE n.absender = ?
ORDER BY n.id DESC
LIMIT 100
]], { ich }) or {}
for _, eintrag in ipairs(liste) do
eintrag.empfaengerName = EmpfaengerName(eintrag)
eintrag.vorschau = Vorschau(eintrag.text, 140)
eintrag.gelesenVon = tonumber(eintrag.gelesenVon) or 0
eintrag.text = nil
end
elseif ordner == 'aushang' or ordner == 'archiv' then
local aktiv = ordner == 'aushang' and 1 or 0
liste = MySQL.query.await([[
SELECT a.id, a.titel, a.text, a.kategorie, a.station, a.angeheftet,
a.kenntnispflicht, a.frist, a.erstellt_von, a.erstellt_am,
a.archiviert_von, a.archiviert_am,
TIMESTAMPDIFF(MINUTE, NOW(), a.frist) AS restMinuten,
(SELECT COUNT(*) FROM medical_aushang_kenntnis k
WHERE k.aushang = a.id) AS kenntnisse,
(SELECT 1 FROM medical_aushang_kenntnis k
WHERE k.aushang = a.id AND k.`character` = ?) AS bestaetigt
FROM medical_aushang a
WHERE a.aktiv = ? AND (a.station IS NULL OR a.station = ?)
ORDER BY a.angeheftet DESC, a.erstellt_am DESC
LIMIT 100
]], { ich, aktiv, mitarbeiter.station }) or {}
for _, eintrag in ipairs(liste) do
eintrag.autor = NameVon(eintrag.erstellt_von)
eintrag.stationName = StationName(eintrag.station)
eintrag.angeheftet = tonumber(eintrag.angeheftet) == 1
eintrag.kenntnispflicht = tonumber(eintrag.kenntnispflicht) == 1
eintrag.kenntnisse = tonumber(eintrag.kenntnisse) or 0
eintrag.bestaetigt = eintrag.bestaetigt ~= nil
eintrag.restMinuten = eintrag.frist and (tonumber(eintrag.restMinuten) or 0) or nil
-- Eine abgelaufene Frist ist ein Zustand, keine Folge
eintrag.fristAbgelaufen = eintrag.restMinuten ~= nil and eintrag.restMinuten <= 0
eintrag.vorschau = Vorschau(eintrag.text, 200)
eintrag.archiviertVon = eintrag.archiviert_von
and NameVon(eintrag.archiviert_von) or nil
eintrag.erstellt_von, eintrag.archiviert_von = nil, nil
eintrag.text = nil
end
else
liste = MySQL.query.await([[
SELECT n.id, n.absender, n.empfaenger, n.empf_typ, n.betreff, n.text, n.gesendet,
c.firstname, c.lastname,
(SELECT 1 FROM medical_nachricht_gelesen g
WHERE g.nachricht = n.id AND g.`character` = ?) AS gelesen
FROM medical_nachricht n
JOIN aether_characters c ON c.id = n.absender
WHERE n.empf_typ = 'rundmeldung'
OR (n.empf_typ = 'person' AND n.empfaenger = ?)
OR (n.empf_typ = 'station' AND n.empfaenger = ?)
ORDER BY n.id DESC
LIMIT 100
]], { ich, tostring(ich), mitarbeiter.station or '' }) or {}
for _, eintrag in ipairs(liste) do
eintrag.absenderName = eintrag.firstname .. ' ' .. eintrag.lastname
eintrag.empfaengerName = EmpfaengerName(eintrag)
eintrag.vorschau = Vorschau(eintrag.text, 140)
eintrag.gelesen = eintrag.gelesen ~= nil
eintrag.anMich = eintrag.empf_typ == 'person'
eintrag.firstname, eintrag.lastname, eintrag.text = nil, nil, nil
end
end
-- Zählwerte für die Reiter
local ungelesen = tonumber(MySQL.scalar.await([[
SELECT COUNT(*) FROM medical_nachricht n
WHERE (n.empf_typ = 'rundmeldung'
OR (n.empf_typ = 'person' AND n.empfaenger = ?)
OR (n.empf_typ = 'station' AND n.empfaenger = ?))
AND NOT EXISTS (SELECT 1 FROM medical_nachricht_gelesen g
WHERE g.nachricht = n.id AND g.`character` = ?)
]], { tostring(ich), mitarbeiter.station or '', ich })) or 0
-- Offene Kenntnisnahmen: was mich betrifft und was ich noch nicht bestätigt habe
local offeneKenntnis = tonumber(MySQL.scalar.await([[
SELECT COUNT(*) FROM medical_aushang a
WHERE a.aktiv = 1 AND a.kenntnispflicht = 1
AND (a.station IS NULL OR a.station = ?)
AND NOT EXISTS (SELECT 1 FROM medical_aushang_kenntnis k
WHERE k.aushang = a.id AND k.`character` = ?)
]], { mitarbeiter.station, ich })) or 0
local zaehler = {
ungelesen = ungelesen,
gesendet = tonumber(MySQL.scalar.await(
'SELECT COUNT(*) FROM medical_nachricht WHERE absender = ?', { ich })) or 0,
aushaenge = tonumber(MySQL.scalar.await([[
SELECT COUNT(*) FROM medical_aushang
WHERE aktiv = 1 AND (station IS NULL OR station = ?)
]], { mitarbeiter.station })) or 0,
offeneKenntnis = offeneKenntnis,
}
return {
ordner = ordner,
liste = liste,
zaehler = zaehler,
kategorien = Medizin.Config.Aushangkategorien,
stationen = Medizin.Config.Stationen,
meineStation = mitarbeiter.station,
rechte = {
senden = Rechte.Hat(mitarbeiter, 'nachricht.senden'),
rundmeldung = Rechte.Hat(mitarbeiter, 'nachricht.rundmeldung'),
schreiben = Rechte.Hat(mitarbeiter, 'aushang.schreiben'),
archivieren = Rechte.Hat(mitarbeiter, 'aushang.archivieren'),
},
}
end)
-- =====================================================================
-- NACHRICHT LESEN
--
-- Als gelesen gilt sie, weil jemand sie geöffnet hat — nicht, weil
-- Zeit vergangen ist.
-- =====================================================================
Kern.RegistriereAktion('kommunikation', 'nachricht', nil, function(quelle, mitarbeiter, daten)
local nachrichtId = tonumber(daten.id)
if not nachrichtId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await([[
SELECT n.*, c.firstname, c.lastname
FROM medical_nachricht n
JOIN aether_characters c ON c.id = n.absender
WHERE n.id = ?
]], { nachrichtId })
if not zeilen or not zeilen[1] then return nil, 'Nachricht nicht gefunden.' end
local eintrag = zeilen[1]
-- Fremde Post bleibt fremd
local darfLesen = eintrag.absender == mitarbeiter.charakterId
or IstEmpfaenger(eintrag, mitarbeiter)
if not darfLesen then
return nil, 'Diese Nachricht ist nicht an dich gerichtet.'
end
-- Nur Empfänger quittieren, nicht der Absender selbst
if IstEmpfaenger(eintrag, mitarbeiter) then
MySQL.insert.await([[
INSERT IGNORE INTO medical_nachricht_gelesen (nachricht, `character`)
VALUES (?, ?)
]], { nachrichtId, mitarbeiter.charakterId })
end
-- Wer hat sie gelesen? Für den Absender die wichtigste Angabe.
local leser = {}
if eintrag.absender == mitarbeiter.charakterId then
leser = MySQL.query.await([[
SELECT g.gelesen_am, c.firstname, c.lastname
FROM medical_nachricht_gelesen g
JOIN aether_characters c ON c.id = g.`character`
WHERE g.nachricht = ?
ORDER BY g.gelesen_am
]], { nachrichtId }) or {}
for _, zeile in ipairs(leser) do
zeile.name = zeile.firstname .. ' ' .. zeile.lastname
zeile.firstname, zeile.lastname = nil, nil
end
end
return {
nachricht = {
id = eintrag.id,
absender = eintrag.absender,
absenderName = eintrag.firstname .. ' ' .. eintrag.lastname,
empfaengerName = EmpfaengerName(eintrag),
empfTyp = eintrag.empf_typ,
betreff = eintrag.betreff,
text = eintrag.text,
gesendet = eintrag.gesendet,
vonMir = eintrag.absender == mitarbeiter.charakterId,
},
leser = leser,
rechte = {
senden = Rechte.Hat(mitarbeiter, 'nachricht.senden'),
},
}
end)
Kern.RegistriereAktion('kommunikation', 'gelesen', nil, function(quelle, mitarbeiter, daten)
local nachrichtId = tonumber(daten.id)
if not nachrichtId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await(
'SELECT id, empfaenger, empf_typ FROM medical_nachricht WHERE id = ?', { nachrichtId })
if not zeilen or not zeilen[1] then return nil, 'Nachricht nicht gefunden.' end
if not IstEmpfaenger(zeilen[1], mitarbeiter) then
return nil, 'Diese Nachricht ist nicht an dich gerichtet.'
end
MySQL.insert.await([[
INSERT IGNORE INTO medical_nachricht_gelesen (nachricht, `character`)
VALUES (?, ?)
]], { nachrichtId, mitarbeiter.charakterId })
return { ok = true }
end)
-- =====================================================================
-- NACHRICHT SCHREIBEN
--
-- Abgeschickt ist abgeschickt: es gibt hier bewusst kein Bearbeiten
-- und kein Löschen.
-- =====================================================================
--- Mögliche Empfänger: das Personal und die Stationen
Kern.RegistriereAktion('kommunikation', 'empfaenger', 'nachricht.senden', function(quelle, mitarbeiter, daten)
local personal = MySQL.query.await([[
SELECT p.`character` AS charakterId, p.personalnummer, p.rolle, p.station,
c.firstname, c.lastname
FROM medical_personal p
JOIN aether_characters c ON c.id = p.`character`
WHERE p.aktiv = 1 AND c.deleted = 0
ORDER BY p.personalnummer
LIMIT 200
]]) or {}
-- Wer gerade im Dienst ist, wird gekennzeichnet — erreichbar sind alle
local imDienst = {}
for _, eintrag in pairs(Dienst.AlleImDienst()) do
imDienst[eintrag.charakterId] = true
end
for _, eintrag in ipairs(personal) do
eintrag.name = eintrag.firstname .. ' ' .. eintrag.lastname
eintrag.rolleName = Medizin.HoleRolle(eintrag.rolle)
and Medizin.HoleRolle(eintrag.rolle).name or eintrag.rolle
eintrag.stationName = StationName(eintrag.station)
eintrag.imDienst = imDienst[eintrag.charakterId] == true
eintrag.selbst = eintrag.charakterId == mitarbeiter.charakterId
eintrag.firstname, eintrag.lastname = nil, nil
end
return {
personal = personal,
stationen = Medizin.Config.Stationen,
rechte = {
rundmeldung = Rechte.Hat(mitarbeiter, 'nachricht.rundmeldung'),
},
}
end)
Kern.RegistriereAktion('kommunikation', 'senden', 'nachricht.senden', function(quelle, mitarbeiter, daten)
local typ = tostring(daten.typ or 'person')
if typ ~= 'person' and typ ~= 'station' and typ ~= 'rundmeldung' then
return nil, 'Unbekannte Empfängerart.'
end
-- Eine Rundmeldung erreicht das ganze Haus und braucht ein eigenes Recht
if typ == 'rundmeldung' and not Rechte.Hat(mitarbeiter, 'nachricht.rundmeldung') then
return nil, 'Rundmeldungen an alle darfst du nicht senden.'
end
local text = Text(daten.text, 4000)
if not text or #text < 2 then return nil, 'Bitte einen Text schreiben.' end
local betreff = Text(daten.betreff, 160)
-- Empfänger prüfen und in die gespeicherte Schreibweise bringen
local ziel, empfaengerName
if typ == 'rundmeldung' then
ziel = 'alle'
empfaengerName = 'Alle im Haus'
elseif typ == 'station' then
local station = AusListe(Medizin.Config.Stationen, daten.ziel)
if not station then return nil, 'Unbekannte Station.' end
ziel = station
empfaengerName = 'Station ' .. StationName(station)
else
local zielId = tonumber(daten.ziel)
if not zielId then return nil, 'Kein Empfänger gewählt.' end
local person = MySQL.scalar.await(
'SELECT `character` FROM medical_personal WHERE `character` = ? AND aktiv = 1',
{ zielId })
if not person then return nil, 'Diese Person gehört nicht zum Personal.' end
ziel = tostring(zielId)
empfaengerName = NameVon(zielId)
end
local nachrichtId = MySQL.insert.await([[
INSERT INTO medical_nachricht (absender, empfaenger, empf_typ, betreff, text)
VALUES (?, ?, ?, ?, ?)
]], { mitarbeiter.charakterId, ziel, typ, betreff, text })
Protokoll.Schreibe(mitarbeiter, 'kommunikation', 'Nachricht gesendet',
{ typ = 'nachricht', id = nachrichtId },
{ empfaengerTyp = typ, empfaenger = ziel, betreff = betreff })
-- Die Betroffenen erfahren es sofort — gelesen wird trotzdem von Hand
local kopfzeile = betreff or Vorschau(text, 60)
if typ == 'person' then
Hinweise.Erstelle({ charakterId = tonumber(ziel) }, {
kategorie = 'system', prioritaet = 'normal',
titel = ('Nachricht von %s'):format(mitarbeiter.name),
text = kopfzeile,
zielTyp = 'nachricht', zielId = nachrichtId,
})
else
-- Station und Rundmeldung: nur ansprechen, wen sie erreicht
for _, eintrag in pairs(Dienst.AlleImDienst()) do
local passt = typ == 'rundmeldung' or eintrag.station == ziel
if passt and eintrag.charakterId ~= mitarbeiter.charakterId then
Hinweise.Erstelle({ charakterId = eintrag.charakterId }, {
kategorie = 'system',
prioritaet = typ == 'rundmeldung' and 'hoch' or 'normal',
titel = typ == 'rundmeldung'
and ('Rundmeldung von %s'):format(mitarbeiter.name)
or ('Stationsnachricht von %s'):format(mitarbeiter.name),
text = kopfzeile,
zielTyp = 'nachricht', zielId = nachrichtId,
})
end
end
end
Kern.Loese('nachricht.gesendet', nachrichtId, typ, mitarbeiter)
return { id = nachrichtId, empfaenger = empfaengerName }
end)
-- =====================================================================
-- SCHWARZES BRETT
-- =====================================================================
Kern.RegistriereAktion('kommunikation', 'aushang', nil, function(quelle, mitarbeiter, daten)
local aushangId = tonumber(daten.id)
if not aushangId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await([[
SELECT a.*,
TIMESTAMPDIFF(MINUTE, NOW(), a.frist) AS restMinuten,
(SELECT 1 FROM medical_aushang_kenntnis k
WHERE k.aushang = a.id AND k.`character` = ?) AS bestaetigt
FROM medical_aushang a
WHERE a.id = ?
]], { mitarbeiter.charakterId, aushangId })
if not zeilen or not zeilen[1] then return nil, 'Aushang nicht gefunden.' end
local eintrag = zeilen[1]
-- Wer hat gelesen und wer noch nicht?
local kenntnis = MySQL.query.await([[
SELECT k.bestaetigt_am, c.firstname, c.lastname, p.personalnummer
FROM medical_aushang_kenntnis k
JOIN aether_characters c ON c.id = k.`character`
LEFT JOIN medical_personal p ON p.`character` = k.`character`
WHERE k.aushang = ?
ORDER BY k.bestaetigt_am
]], { aushangId }) or {}
for _, zeile in ipairs(kenntnis) do
zeile.name = zeile.firstname .. ' ' .. zeile.lastname
zeile.firstname, zeile.lastname = nil, nil
end
-- Wen der Aushang betrifft, der aber noch nicht bestätigt hat.
-- Reine Auskunft — niemand wird dadurch zu etwas gezwungen.
local offen = {}
if tonumber(eintrag.kenntnispflicht) == 1 then
offen = MySQL.query.await([[
SELECT p.personalnummer, p.station, c.firstname, c.lastname
FROM medical_personal p
JOIN aether_characters c ON c.id = p.`character`
WHERE p.aktiv = 1
AND (? IS NULL OR p.station = ?)
AND NOT EXISTS (SELECT 1 FROM medical_aushang_kenntnis k
WHERE k.aushang = ? AND k.`character` = p.`character`)
ORDER BY p.personalnummer
LIMIT 100
]], { eintrag.station, eintrag.station, aushangId }) or {}
for _, zeile in ipairs(offen) do
zeile.name = zeile.firstname .. ' ' .. zeile.lastname
zeile.firstname, zeile.lastname = nil, nil
end
end
local restMinuten = eintrag.frist and (tonumber(eintrag.restMinuten) or 0) or nil
return {
aushang = {
id = eintrag.id, titel = eintrag.titel, text = eintrag.text,
kategorie = eintrag.kategorie,
station = eintrag.station, stationName = StationName(eintrag.station),
angeheftet = tonumber(eintrag.angeheftet) == 1,
kenntnispflicht = tonumber(eintrag.kenntnispflicht) == 1,
frist = eintrag.frist, restMinuten = restMinuten,
fristAbgelaufen = restMinuten ~= nil and restMinuten <= 0,
autor = NameVon(eintrag.erstellt_von),
erstelltAm = eintrag.erstellt_am,
aktiv = tonumber(eintrag.aktiv) == 1,
archiviertAm = eintrag.archiviert_am,
archiviertVon = eintrag.archiviert_von
and NameVon(eintrag.archiviert_von) or nil,
bestaetigt = eintrag.bestaetigt ~= nil,
-- Ein Aushang, den schon jemand quittiert hat, wird im Wortlaut
-- nicht mehr angefasst — sonst hätte jemand etwas anderes gelesen
textGesperrt = #kenntnis > 0,
},
kenntnis = kenntnis,
offen = offen,
kategorien = Medizin.Config.Aushangkategorien,
stationen = Medizin.Config.Stationen,
rechte = {
schreiben = Rechte.Hat(mitarbeiter, 'aushang.schreiben'),
archivieren = Rechte.Hat(mitarbeiter, 'aushang.archivieren'),
},
}
end)
Kern.RegistriereAktion('kommunikation', 'aushangSchreiben', 'aushang.schreiben', function(quelle, mitarbeiter, daten)
local aushangId = tonumber(daten.id)
local titel = Text(daten.titel, 160)
local text = Text(daten.text, 8000)
local kategorie = AusListe(Medizin.Config.Aushangkategorien, daten.kategorie)
if not titel then return nil, 'Bitte einen Titel angeben.' end
if not text or #text < 5 then return nil, 'Bitte einen Text schreiben.' end
if not kategorie then return nil, 'Unbekannte Kategorie.' end
-- Ohne Station gilt der Aushang für das ganze Haus
local station = nil
if Text(daten.station, 32) then
station = AusListe(Medizin.Config.Stationen, daten.station)
if not station then return nil, 'Unbekannte Station.' end
end
local angeheftet = daten.angeheftet == true and 1 or 0
local kenntnispflicht = daten.kenntnispflicht == true and 1 or 0
-- Eine Frist erinnert nur. Sie sperrt nichts und verfällt folgenlos.
local frist = Text(daten.frist, 20)
if frist and not frist:match('^%d%d%d%d%-%d%d%-%d%d[ T]%d%d:%d%d') then
return nil, 'Die Frist braucht die Form JJJJ-MM-TT SS:MM.'
end
if frist then frist = frist:gsub('T', ' '):sub(1, 16) .. ':00' end
if aushangId then
local vorher = MySQL.query.await(
'SELECT id, titel, text, aktiv FROM medical_aushang WHERE id = ?', { aushangId })
if not vorher or not vorher[1] then return nil, 'Aushang nicht gefunden.' end
if tonumber(vorher[1].aktiv) ~= 1 then
return nil, 'Ein archivierter Aushang wird nicht mehr bearbeitet.'
end
-- Wer schon quittiert hat, hat einen bestimmten Wortlaut gelesen
local quittiert = tonumber(MySQL.scalar.await(
'SELECT COUNT(*) FROM medical_aushang_kenntnis WHERE aushang = ?',
{ aushangId })) or 0
if quittiert > 0 and (vorher[1].titel ~= titel or vorher[1].text ~= text) then
return nil, ('%d Person(en) haben diesen Wortlaut bereits zur Kenntnis genommen. Bitte archivieren und neu veröffentlichen.')
:format(quittiert)
end
MySQL.update.await([[
UPDATE medical_aushang
SET titel = ?, text = ?, kategorie = ?, station = ?,
angeheftet = ?, kenntnispflicht = ?, frist = ?
WHERE id = ?
]], { titel, text, kategorie, station, angeheftet, kenntnispflicht, frist, aushangId })
Protokoll.Schreibe(mitarbeiter, 'kommunikation', 'Aushang bearbeitet',
{ typ = 'aushang', id = aushangId },
{ titel = titel, kategorie = kategorie, station = station,
kenntnispflicht = kenntnispflicht == 1, frist = frist })
else
aushangId = MySQL.insert.await([[
INSERT INTO medical_aushang
(titel, text, kategorie, station, angeheftet, kenntnispflicht,
frist, erstellt_von)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
]], { titel, text, kategorie, station, angeheftet, kenntnispflicht,
frist, mitarbeiter.charakterId })
Protokoll.Schreibe(mitarbeiter, 'kommunikation', 'Aushang veröffentlicht',
{ typ = 'aushang', id = aushangId },
{ titel = titel, kategorie = kategorie, station = station,
kenntnispflicht = kenntnispflicht == 1, frist = frist })
-- Nur die ansprechen, die der Aushang betrifft
for _, eintrag in pairs(Dienst.AlleImDienst()) do
local passt = station == nil or eintrag.station == station
if passt then
Hinweise.Erstelle({ charakterId = eintrag.charakterId }, {
kategorie = 'system',
prioritaet = kenntnispflicht == 1 and 'hoch' or 'normal',
titel = ('Neuer Aushang · %s')
:format(kategorie == 'anweisung' and 'Dienstanweisung' or 'Schwarzes Brett'),
text = titel,
zielTyp = 'aushang', zielId = aushangId,
})
end
end
end
Kern.Loese('aushang.geaendert', aushangId, mitarbeiter)
return { id = aushangId }
end)
Kern.RegistriereAktion('kommunikation', 'anheften', 'aushang.schreiben', function(quelle, mitarbeiter, daten)
local aushangId = tonumber(daten.id)
if not aushangId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await(
'SELECT id, titel, angeheftet FROM medical_aushang WHERE id = ? AND aktiv = 1',
{ aushangId })
if not zeilen or not zeilen[1] then return nil, 'Aushang nicht gefunden.' end
local neu = daten.angeheftet == true and 1 or 0
MySQL.update.await('UPDATE medical_aushang SET angeheftet = ? WHERE id = ?',
{ neu, aushangId })
Protokoll.Schreibe(mitarbeiter, 'kommunikation',
neu == 1 and 'Aushang angeheftet' or 'Aushang losgelöst',
{ typ = 'aushang', id = aushangId }, { titel = zeilen[1].titel })
return { ok = true }
end)
Kern.RegistriereAktion('kommunikation', 'aushangArchivieren', 'aushang.archivieren', function(quelle, mitarbeiter, daten)
local aushangId = tonumber(daten.id)
if not aushangId then return nil, 'Ungültige Kennung.' end
local grund = Text(daten.grund, 255)
if not grund then return nil, 'Bitte angeben, warum der Aushang abgenommen wird.' end
local zeilen = MySQL.query.await(
'SELECT id, titel, aktiv, kenntnispflicht FROM medical_aushang WHERE id = ?',
{ aushangId })
if not zeilen or not zeilen[1] then return nil, 'Aushang nicht gefunden.' end
if tonumber(zeilen[1].aktiv) ~= 1 then return nil, 'Dieser Aushang hängt nicht mehr.' end
-- Offene Kenntnisnahmen halten nicht auf, werden aber festgehalten
local offen = 0
if tonumber(zeilen[1].kenntnispflicht) == 1 then
offen = tonumber(MySQL.scalar.await([[
SELECT COUNT(*) FROM medical_personal p
WHERE p.aktiv = 1
AND NOT EXISTS (SELECT 1 FROM medical_aushang_kenntnis k
WHERE k.aushang = ? AND k.`character` = p.`character`)
]], { aushangId })) or 0
end
MySQL.update.await([[
UPDATE medical_aushang
SET aktiv = 0, angeheftet = 0, archiviert_von = ?, archiviert_am = NOW()
WHERE id = ?
]], { mitarbeiter.charakterId, aushangId })
Protokoll.Schreibe(mitarbeiter, 'kommunikation', 'Aushang archiviert',
{ typ = 'aushang', id = aushangId },
{ titel = zeilen[1].titel, offeneKenntnisnahmen = offen }, grund)
Kern.Loese('aushang.archiviert', aushangId, mitarbeiter)
return { ok = true, offen = offen }
end)
-- =====================================================================
-- KENNTNISNAHME
--
-- Bestätigt ausschliesslich, dass jemand selbst gelesen hat. Sie lässt
-- sich nicht für andere setzen, nicht zurücknehmen und ändert nichts
-- ausser sich selbst.
-- =====================================================================
Kern.RegistriereAktion('kommunikation', 'kenntnis', nil, function(quelle, mitarbeiter, daten)
local aushangId = tonumber(daten.id)
if not aushangId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await(
'SELECT id, titel, station, aktiv FROM medical_aushang WHERE id = ?', { aushangId })
if not zeilen or not zeilen[1] then return nil, 'Aushang nicht gefunden.' end
local aushang = zeilen[1]
if tonumber(aushang.aktiv) ~= 1 then
return nil, 'Dieser Aushang hängt nicht mehr.'
end
if aushang.station and aushang.station ~= mitarbeiter.station then
return nil, 'Dieser Aushang betrifft eine andere Station.'
end
local schon = MySQL.scalar.await([[
SELECT 1 FROM medical_aushang_kenntnis WHERE aushang = ? AND `character` = ?
]], { aushangId, mitarbeiter.charakterId })
if schon then return { ok = true, schonBestaetigt = true } end
MySQL.insert.await([[
INSERT INTO medical_aushang_kenntnis (aushang, `character`) VALUES (?, ?)
]], { aushangId, mitarbeiter.charakterId })
Protokoll.Schreibe(mitarbeiter, 'kommunikation', 'Kenntnis genommen',
{ typ = 'aushang', id = aushangId }, { titel = aushang.titel })
return { ok = true }
end)
-- ---------------------------------------------------------------------
-- Schnittstelle für andere Ressourcen
-- ---------------------------------------------------------------------
--- Liefert die aktiven Aushänge einer Station.
--- Reine Auskunft — keine Ressource kann darüber etwas veröffentlichen
--- oder eine Kenntnisnahme setzen.
--- @param station string|nil Stationskennung
--- @return table Liste
exports('HoleAushaenge', function(station)
return MySQL.query.await([[
SELECT a.id, a.titel, a.text, a.kategorie, a.station,
a.angeheftet, a.kenntnispflicht, a.frist, a.erstellt_am
FROM medical_aushang a
WHERE a.aktiv = 1 AND (a.station IS NULL OR a.station = ?)
ORDER BY a.angeheftet DESC, a.erstellt_am DESC
]], { station }) or {}
end)