- 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>
761 lines
31 KiB
Lua
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)
|