DiagnostikApps/PG13R/app.R
2026-09-22 18:35:43 +02:00

777 lines
27 KiB
R
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Präambel ####
AKZENT_FARBE = "#8B2635"
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_pg13r.R"
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R"
PG13R_DISCLAIMER = paste0(
"Dies ist eine algorithmische Einordnung auf Basis des Selbstauskunfts-Screenings, ",
"kein automatisiertes klinisches Urteil und kein Ersatz fuer eine klinische ",
"Einschaetzung im Einzelfall."
)
# Verlauf gruen -> dunkelrot entspricht den 5 Antwortstufen 0-4.
PG13R_BADGE_FARBEN = c(
"0" = "#4CAF50",
"1" = "#F48FB1",
"2" = "#EF5350",
"3" = "#B71C1C",
"4" = "#4A0000"
)
PG13R_BADGE_TEXT_FARBEN = c(
"0" = "white",
"1" = "#333333",
"2" = "white",
"3" = "white",
"4" = "white"
)
PG13R_KAT_WORD_FARBEN = list(
"1" = list(bg = "#F5F5F5", text = "#424242"),
"2" = list(bg = "#E8F5E9", text = "#2E7D32"),
"3" = list(bg = "#FFF3E0", text = "#E65100"),
"4" = list(bg = "#FFF8E1", text = "#F57F17"),
"5" = list(bg = "#FFEBEE", text = "#B71C1C")
)
library(shiny)
library(dplyr)
library(ggplot2)
library(haven)
library(officer)
# Infrastruktur ####
APP_VERZEICHNIS = normalizePath(getwd())
absPath = function(pfad) {
if (grepl("^([A-Za-z]:[/\\\\]|/)", pfad)) return(pfad)
file.path(APP_VERZEICHNIS, pfad)
}
PFAD_DOWNLOAD_SKRIPT = normalizePath(absPath(PFAD_DOWNLOAD_SKRIPT), mustWork = FALSE)
PFAD_PSEUDONYM_SKRIPT = normalizePath(absPath(PFAD_PSEUDONYM_SKRIPT), mustWork = FALSE)
# Helper ####
# labels-Attribut der ORIGINAL-Spalte (vor Subsetting) lesen, damit
# die Zuordnung Wert -> Stufe immer aus den Daten selbst stammt.
pg13r_get_level = function(original_col, wert) {
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_integer_)
lbl_attr = attr(original_col, "labels")
if (!is.null(lbl_attr) && length(lbl_attr) > 0) {
lbl_sortiert = sort(as.vector(lbl_attr))
pos = which(lbl_sortiert == as.numeric(wert[1]))
if (length(pos) > 0) return(as.integer(pos[1]) - 1L)
}
# Fallback bei fehlenden labels: 1-basierte Kodierung angenommen
as.integer(as.numeric(wert[1])) - 1L
}
pg13r_get_anker = function(original_col, wert) {
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_character_)
lbl_attr = attr(original_col, "labels")
if (!is.null(lbl_attr) && length(lbl_attr) > 0) {
pos = which(as.vector(lbl_attr) == as.numeric(wert[1]))
if (length(pos) > 0) return(names(lbl_attr)[pos[1]])
}
stufe = max(0L, min(4L, as.integer(as.numeric(wert[1])) - 1L))
c("Ueberhaupt nicht", "Kaum", "Ein wenig", "Ziemlich", "Sehr")[stufe + 1L]
}
# Nie hartkodiert 1/2 - immer aus dem labels-Attribut der Original-Spalte.
pg13r_get_label_text = function(original_col, wert) {
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_character_)
lbl_attr = attr(original_col, "labels")
if (!is.null(lbl_attr) && length(lbl_attr) > 0) {
pos = which(as.vector(lbl_attr) == as.numeric(wert[1]))
if (length(pos) > 0) return(names(lbl_attr)[pos[1]])
}
NA_character_
}
# Entfernt formr-Nummerierungsartefakte am Anfang des Itemtexts
# (z.B. "1. " oder "01) "), die manchmal im label-Attribut erscheinen.
clean_item_label = function(text) {
if (is.null(text) || length(text) == 0 || is.na(text[1])) return(NA_character_)
sub("^\\d+[.)\\s]\\s*", "", trimws(as.character(text[1])))
}
pg13r_diagnose = function(verlust_text, summenscore, monate_num, beeintr_text) {
if (!is.na(verlust_text) && verlust_text == "Nein") {
return(list(
kategorie = 1L,
titel = "Kein Verlust angegeben",
hinweis = paste0(
"Es wurde kein Trauerfall angegeben. Die Fragen des PG-13-R ",
"waren fuer die aktuelle Situation vermutlich nicht relevant."
),
disclaimer = PG13R_DISCLAIMER
))
}
if (isTRUE(summenscore < 30)) {
return(list(
kategorie = 2L,
titel = "Score unter Schwelle",
hinweis = paste0(
"Summenscore (", summenscore, " / 50) liegt unter dem Cutoff von 30. ",
"Kriterien fuer Prolonged Grief Disorder nicht erfuellt."
),
disclaimer = PG13R_DISCLAIMER
))
}
monate_ok = !is.na(monate_num) && isTRUE(monate_num >= 12)
if (!monate_ok) {
monats_hinweis = if (is.na(monate_num)) {
"Die Angabe der Monate seit dem Verlust fehlt oder war nicht auswertbar. "
} else {
paste0("Vergangene Zeit seit dem Verlust: ", monate_num,
" Monate (Kriterium: mind. 12 Monate). ")
}
return(list(
kategorie = 3L,
titel = "Zeitkriterium nicht erfuellt",
hinweis = paste0(
"Deutlich belastet (Score ", summenscore, " >= 30), aber Diagnose ",
"noch nicht stellbar: ", monats_hinweis,
"Erneute Einschaetzung nach Ablauf von 12 Monaten seit dem Verlust empfohlen."
),
disclaimer = PG13R_DISCLAIMER
))
}
if (!is.na(beeintr_text) && beeintr_text == "Nein") {
return(list(
kategorie = 4L,
titel = "Keine relevante Beeintraechtigung angegeben",
hinweis = paste0(
"Score-Schwelle (", summenscore, " >= 30) und Zeitkriterium (>= 12 Monate) erfuellt, ",
"jedoch wurde keine relevante Funktionsbeeintraechtigung angegeben. ",
"Kriterien fuer die Diagnose damit nicht vollstaendig erfuellt."
),
disclaimer = PG13R_DISCLAIMER
))
}
if (!is.na(beeintr_text) && beeintr_text == "Ja") {
return(list(
kategorie = 5L,
titel = "Alle Kriterien erfuellt",
hinweis = paste0(
"Alle drei Kriterien erfuellt: Symptomschwere (Score ", summenscore, " >= 30), ",
"Zeitkriterium (>= 12 Monate seit dem Verlust) und relevante ",
"Funktionsbeeintraechtigung angegeben. ",
"Dies entspricht den diagnostischen Kriterien fuer Prolonged Grief Disorder ",
"nach diesem Screening-Algorithmus."
),
disclaimer = PG13R_DISCLAIMER
))
}
# Beeintraechtigung ist NA: konservativ wie Kategorie 4 behandeln.
list(
kategorie = 4L,
titel = "Beeintraechtigung nicht angegeben",
hinweis = paste0(
"Score-Schwelle (", summenscore, " >= 30) und Zeitkriterium (>= 12 Monate) erfuellt, ",
"jedoch liegt keine Angabe zur Funktionsbeeintraechtigung vor. ",
"Ohne Bestaetigung einer Beeintraechtigung gelten die vollstaendigen Kriterien ",
"als nicht erfuellt."
),
disclaimer = PG13R_DISCLAIMER
)
}
make_gauge_pg13r = function(score) {
ggplot() +
geom_rect(aes(xmin = 10, xmax = 30, ymin = 0, ymax = 1),
fill = "#E8F5E9", color = NA) +
geom_rect(aes(xmin = 30, xmax = 50, ymin = 0, ymax = 1),
fill = "#FFEBEE", color = NA) +
geom_rect(aes(xmin = 10, xmax = 50, ymin = 0, ymax = 1),
fill = NA, color = "#9E9E9E", linewidth = 0.6) +
geom_vline(xintercept = 30, color = "#E65100", linetype = "dashed", linewidth = 1) +
geom_segment(aes(x = score, xend = score, y = -0.25, yend = 1.25),
color = AKZENT_FARBE, linewidth = 2.5) +
geom_label(aes(x = score, y = 1.6, label = paste0("Score: ", score)),
fill = AKZENT_FARBE, color = "white", fontface = "bold",
linewidth = 0, size = 4) +
annotate("text", x = 30, y = -0.55, label = "Cutoff: 30",
color = "#E65100", size = 3.2, hjust = 0.5) +
annotate("text", x = 20, y = 0.5, label = "< 30",
color = "#2E7D32", size = 3.5, fontface = "italic") +
annotate("text", x = 40, y = 0.5, label = ">= 30",
color = "#B71C1C", size = 3.5, fontface = "italic") +
scale_x_continuous(limits = c(7, 53), breaks = c(10, 20, 30, 40, 50)) +
scale_y_continuous(limits = c(-0.8, 2.0)) +
theme_minimal(base_size = 12) +
theme(
axis.text.y = element_blank(),
axis.ticks.y = element_blank(),
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank(),
axis.title.y = element_blank(),
plot.margin = margin(t = 5, r = 10, b = 5, l = 10)
) +
labs(x = "PG-13-R Summenscore (10-50)", y = NULL)
}
# UI ####
app_css = "
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f5f5f5; }
.app-header {
background: #8B2635; color: white; padding: 18px 24px 14px;
margin-bottom: 20px; border-radius: 0 0 6px 6px;
}
.app-header h2 { margin: 0; font-size: 1.5rem; font-weight: 600; }
.app-header p { margin: 4px 0 0; opacity: 0.85; font-size: 0.9rem; }
.input-panel {
background: white; border-radius: 6px; padding: 16px 20px;
margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.12);
display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap;
}
.input-panel .form-group { margin-bottom: 0; }
.input-panel label { font-weight: 600; color: #333; }
.btn-laden {
background: #8B2635 !important; color: white !important;
border: none !important; border-radius: 4px !important;
padding: 8px 20px !important; font-weight: 600 !important; cursor: pointer;
}
.btn-laden:hover { background: #6d1e29 !important; }
.alert-fehler {
background: #FFEBEE; border-left: 5px solid #C62828;
padding: 12px 16px; border-radius: 4px; color: #B71C1C;
margin-bottom: 12px; font-weight: 500;
}
.alert-warnung {
background: #FFF3E0; border-left: 5px solid #E65100;
padding: 10px 16px; border-radius: 4px; color: #BF360C;
margin-bottom: 12px; font-size: 0.93em; font-weight: 500;
}
.abschnitt-karte {
background: white; border-radius: 6px; padding: 20px 24px;
margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.12);
}
.abschnitt-titel {
color: #8B2635; font-size: 1.15rem; font-weight: 700;
border-bottom: 2px solid #8B2635; padding-bottom: 8px; margin-bottom: 14px;
}
.meta-block { margin-bottom: 10px; color: #555; font-size: 0.95em; }
.meta-block strong { color: #222; }
.kontext-zeile {
display: flex; gap: 8px; align-items: baseline;
padding: 4px 0; color: #444; font-size: 0.93em;
}
.kontext-label { font-weight: 600; color: #333; min-width: 220px; }
.diagnose-box {
border-radius: 6px; padding: 14px 18px; margin: 12px 0;
border-left: 5px solid;
}
.diagnose-titel { font-weight: 700; font-size: 1.05rem; margin-bottom: 6px; }
.diagnose-hinweis { font-size: 0.93em; line-height: 1.55; }
.diagnose-disclaimer {
font-size: 0.82em; color: #777; font-style: italic;
margin-top: 10px; border-top: 1px solid rgba(0,0,0,.1); padding-top: 8px;
}
.pg-kat-1 { background: #F5F5F5; border-color: #9E9E9E; color: #424242; }
.pg-kat-2 { background: #E8F5E9; border-color: #A5D6A7; color: #2E7D32; }
.pg-kat-3 { background: #FFF3E0; border-color: #FFCC80; color: #E65100; }
.pg-kat-4 { background: #FFF8E1; border-color: #FFE082; color: #F57F17; }
.pg-kat-5 { background: #FFEBEE; border-color: #EF9A9A; color: #B71C1C; }
.item-zeile {
display: flex; align-items: flex-start; gap: 10px;
padding: 7px 0; border-bottom: 1px solid #F0F0F0;
}
.item-nr { font-weight: 600; color: #8B2635; min-width: 26px; flex-shrink: 0; }
.item-text { flex: 1; color: #333; font-size: 0.92em; }
.stufe-badge {
border-radius: 4px; padding: 2px 9px; font-weight: 700;
font-size: 0.82em; white-space: nowrap; display: inline-block; flex-shrink: 0;
}
.stufe-badge-0 { background: #4CAF50; color: white; }
.stufe-badge-1 { background: #F48FB1; color: #333333; }
.stufe-badge-2 { background: #EF5350; color: white; }
.stufe-badge-3 { background: #B71C1C; color: white; }
.stufe-badge-4 { background: #4A0000; color: white; }
.score-zahl { font-size: 2.2rem; font-weight: 800; color: #8B2635; }
.cutoff-info { font-size: 0.88em; color: #555; margin-top: 4px; }
"
app_css = gsub("#8B2635", AKZENT_FARBE, app_css, fixed = TRUE)
ui = fluidPage(
tags$head(
tags$meta(charset = "UTF-8"),
tags$style(HTML(app_css))
),
div(class = "app-header",
tags$h2("PG-13-Revised Prolonged Grief Disorder"),
tags$p("Prigerson et al. 2021 | dt. Uebersetzung AG Psychotraumatologie KU Eichstaett")
),
div(class = "container-fluid",
div(class = "input-panel",
div(style = "min-width: 360px; white-space: nowrap;",
textInput("pseudonym",
label = tagList(
"Pseudonym",
tags$span(style = "font-weight: normal; font-style: italic; font-size: 0.78em; color: #888; margin-left: 4px; white-space: nowrap;",
"optional, hat Vorrang vor Chiffre")
),
placeholder = "optional", width = "340px")
),
div(style = "min-width: 180px;",
textInput("chiffre", label = "Patientenchiffre",
placeholder = "z.B. P000123", width = "180px")
),
actionButton("btn_suchen", "Daten laden", class = "btn-laden"),
div(style = "margin-left: auto;",
downloadButton("download_word", "Word-Bericht herunterladen",
style = paste0(
"background:", AKZENT_FARBE, "; color:white; border:none;",
" font-weight:600; padding:8px 20px; border-radius:4px;"
)
)
)
),
uiOutput("fehler_ui"),
uiOutput("warnung_ui"),
uiOutput("ergebnis_ui")
)
)
# Word-Export ####
erstelle_pg13r_docx = function(d) {
doc = read_docx()
fp_titel = fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 18)
fp_abschnitt = fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 13)
fp_label = fp_text(bold = TRUE, font.size = 11)
fp_normal = fp_text(font.size = 11)
fp_disclaimer = fp_text(font.size = 9, italic = TRUE, color = "#777777")
kat_key = as.character(d$diagnose$kategorie)
kat_farbe = PG13R_KAT_WORD_FARBEN[[kat_key]]
fp_kat_titel = fp_text(bold = TRUE, font.size = 12,
color = kat_farbe$text, shading.color = kat_farbe$bg)
fp_kat_text = fp_text(font.size = 11,
color = kat_farbe$text, shading.color = kat_farbe$bg)
doc = body_add_fpar(doc, fpar(ftext("PG-13-Revised - Einzelauswertung", fp_titel)))
doc = body_add_fpar(doc, fpar(
ftext("Chiffre: ", fp_label),
ftext(d$chiffre, fp_normal),
ftext(" Datum: ", fp_label),
ftext(d$datum_str, fp_normal)
))
if (!is.null(d$info_mehrere)) {
doc = body_add_fpar(doc, fpar(
ftext(d$info_mehrere,
fp_text(font.size = 10, italic = TRUE, color = "#555555"))
))
}
doc = body_add_par(doc, "", style = "Normal")
fp_score = if (isTRUE(d$summenscore >= 30))
fp_text(bold = TRUE, font.size = 12, color = "#C62828")
else
fp_text(bold = TRUE, font.size = 12, color = "#2E7D32")
doc = body_add_fpar(doc, fpar(ftext("Auswertung", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(
ftext("Summenscore: ", fp_label),
ftext(paste0(d$summenscore, " / 50 (Cutoff: 30)"), fp_score)
))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Diagnostische Einordnung", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(
ftext(paste0("Kategorie ", d$diagnose$kategorie, ": ", d$diagnose$titel),
fp_kat_titel)
))
doc = body_add_fpar(doc, fpar(ftext(d$diagnose$hinweis, fp_kat_text)))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Kontextangaben", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(
ftext("Verlust erlebt: ", fp_label),
ftext(if (is.na(d$verlust_text)) "k. A." else d$verlust_text, fp_normal)
))
doc = body_add_fpar(doc, fpar(
ftext("Monate seit dem Verlust: ", fp_label),
ftext(if (d$monate_str == "k. A.") "k. A." else d$monate_str, fp_normal)
))
doc = body_add_fpar(doc, fpar(
ftext("Funktionsbeeintraechtigung: ", fp_label),
ftext(if (is.na(d$beeintr_text)) "k. A." else d$beeintr_text, fp_normal)
))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("PG-13-R Einzelitems", fp_abschnitt)))
for (i in seq_len(10)) {
stufe = d$stufen[i]
anker = d$anker_texte[i]
stufe_key = if (!is.na(stufe) && stufe >= 0L && stufe <= 4L)
as.character(stufe) else "0"
anker_txt = if (!is.na(anker)) anker else
c("Ueberhaupt nicht", "Kaum", "Ein wenig", "Ziemlich", "Sehr")[as.integer(stufe_key) + 1L]
item_txt = if (!is.na(d$item_texte[i])) d$item_texte[i] else paste0("Item ", i)
fp_badge = fp_text(
color = PG13R_BADGE_TEXT_FARBEN[[stufe_key]],
bold = TRUE,
shading.color = PG13R_BADGE_FARBEN[[stufe_key]],
font.size = 10
)
doc = body_add_fpar(doc, fpar(
ftext(paste0(i, ". ", item_txt, " "), fp_normal),
ftext(paste0(" ", anker_txt, " "), fp_badge)
))
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext(d$diagnose$disclaimer, fp_disclaimer)))
doc
}
# Server ####
server = function(input, output, session) {
# --- pseudonym-support-injection v1 ---
observe({
query = parseQueryString(session$clientData$url_search)
if (!is.null(query$pseudonym) && nchar(trimws(query$pseudonym)) > 0) {
updateTextInput(session, "pseudonym", value = trimws(query$pseudonym))
}
})
observe({
query = parseQueryString(session$clientData$url_search)
if (!is.null(query$chiffre) && nchar(trimws(query$chiffre)) > 0) {
updateTextInput(session, "chiffre", value = toupper(trimws(query$chiffre)))
}
})
# Skripte werden NICHT beim App-Start gesourct, nur beim Klick.
ergebnis_r = eventReactive(input$btn_suchen, {
chiffre = toupper(trimws(input$chiffre))
if ((nchar(trimws(input$pseudonym)) == 0 && nchar(chiffre) == 0))
return(list(error = "Bitte eine Patientenchiffre eingeben."))
if (!(nchar(trimws(input$pseudonym)) > 0 || grepl("^[A-Z][0-9]{6}$", chiffre)))
return(list(error = paste0(
"Ungueltige Chiffre. Erwartet: ein Grossbuchstabe + 6 Ziffern (z.B. P000123).")))
if (!file.exists(PFAD_DOWNLOAD_SKRIPT))
return(list(error = paste0(
"Download-Skript nicht gefunden:\n", PFAD_DOWNLOAD_SKRIPT)))
if (!file.exists(PFAD_PSEUDONYM_SKRIPT))
return(list(error = paste0(
"Pseudonym-Skript nicht gefunden:\n", PFAD_PSEUDONYM_SKRIPT)))
res_dl = tryCatch(
{ source(PFAD_DOWNLOAD_SKRIPT, local = FALSE); list(ok = TRUE) },
error = function(e) list(ok = FALSE, msg = e$message)
)
if (!res_dl$ok)
return(list(error = paste0("Fehler im Download-Skript: ", res_dl$msg)))
db_ordner = local({
ordner = dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT, mustWork = FALSE))
gefunden = NULL
for (i in 1:5) {
if (file.exists(file.path(ordner, "pseudonyme.db"))) {
gefunden = ordner
break
}
elternteil = dirname(ordner)
if (elternteil == ordner) break
ordner = elternteil
}
gefunden
})
alter_wd = getwd()
wd_ziel = if (!is.null(db_ordner)) db_ordner else
dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT, mustWork = FALSE))
setwd(wd_ziel)
on.exit(setwd(alter_wd), add = TRUE)
res_ps = tryCatch(
{ source(PFAD_PSEUDONYM_SKRIPT, local = FALSE)
if (nchar(trimws(input$pseudonym)) > 0) {
.pw_wert = trimws(input$pseudonym)
.pw_tab = get("pseudo", envir = .GlobalEnv)
.pw_treffer = .pw_tab[.pw_tab$pseudonym == .pw_wert, ]
if (nrow(.pw_treffer) > 0) chiffre = toupper(trimws(.pw_treffer$chiffre[1]))
}; list(ok = TRUE) },
error = function(e) list(ok = FALSE, msg = e$message)
)
if (!res_ps$ok)
return(list(error = paste0("Fehler im Pseudonym-Skript: ", res_ps$msg)))
if (!exists("daten_pg13r", envir = .GlobalEnv))
return(list(error = paste0(
"Objekt 'daten_pg13r' nach dem Sourcen nicht gefunden. ",
"Bitte Download-Skript pruefen.")))
if (!exists("pseudo", envir = .GlobalEnv))
return(list(error = paste0(
"Objekt 'pseudo' nach dem Sourcen nicht gefunden. ",
"Bitte Pseudonym-Skript pruefen.")))
daten = get("daten_pg13r", envir = .GlobalEnv)
pseudo_df = get("pseudo", envir = .GlobalEnv)
# Eine Chiffre kann mehrere Pseudonyme haben (eines pro Instrument/Run).
treffer_ps = pseudo_df[pseudo_df$chiffre == chiffre, ]
if (nrow(treffer_ps) == 0)
return(list(error = paste0(
"Chiffre '", chiffre, "' wurde in der Pseudonym-Datenbank nicht gefunden.")))
alle_session_ids = unique(treffer_ps$pseudonym)
if (nchar(trimws(input$pseudonym)) > 0) alle_session_ids = trimws(input$pseudonym)
treffer_dat = daten[daten$session %in% alle_session_ids, ]
if (nrow(treffer_dat) == 0)
return(list(error = paste0(
"Kein PG-13-Revised-Datensatz fuer Chiffre '", chiffre, "' gefunden. ",
"(", length(alle_session_ids), " Pseudonym(e) geprueft)")))
info_mehrere = NULL
if (nrow(treffer_dat) > 1) {
n = nrow(treffer_dat)
treffer_dat = treffer_dat[order(treffer_dat$created, decreasing = TRUE), ]
datum_neu = tryCatch(
format(as.POSIXct(treffer_dat$created[1]), "%d.%m.%Y %H:%M"),
error = function(e) "unbekanntes Datum"
)
info_mehrere = paste0(
"Mehrere Ausfuellungen gefunden (", n, " Eintraege). ",
"Angezeigt wird die neueste vom ", datum_neu, "."
)
treffer_dat = treffer_dat[1, , drop = FALSE]
}
zeile = treffer_dat[1, , drop = FALSE]
datum_str = tryCatch(
format(as.POSIXct(zeile[["created"]][1]), "%d.%m.%Y"),
error = function(e) format(Sys.Date(), "%d.%m.%Y")
)
verlust_text = pg13r_get_label_text(daten[["verlust_erlebt"]],
zeile[["verlust_erlebt"]])
beeintr_text = pg13r_get_label_text(daten[["beeintraechtigung"]],
zeile[["beeintraechtigung"]])
monate_raw = zeile[["monate_seit_verlust"]][1]
monate_str = if (is.null(monate_raw) || is.na(monate_raw) ||
trimws(as.character(monate_raw)) == "")
"k. A." else as.character(monate_raw)
monate_num = suppressWarnings(as.numeric(monate_raw))
item_texte = sapply(seq_len(10), function(i) {
var = paste0("item", sprintf("%02d", i))
clean_item_label(attr(daten[[var]], "label"))
})
stufen = sapply(seq_len(10), function(i) {
var = paste0("item", sprintf("%02d", i))
pg13r_get_level(daten[[var]], zeile[[var]])
})
anker_texte = sapply(seq_len(10), function(i) {
var = paste0("item", sprintf("%02d", i))
pg13r_get_anker(daten[[var]], zeile[[var]])
})
# Summenscore = Summe der ROHEN Werte 1-5 (Range 10-50), nicht Stufen 0-4.
# Entspricht der Originalformel im JS-Scoring-Tool (sum += parseInt(field.value)).
raw_werte = sapply(seq_len(10), function(i) {
as.numeric(zeile[[paste0("item", sprintf("%02d", i))]][1])
})
summenscore = sum(raw_werte, na.rm = TRUE)
diagnose = pg13r_diagnose(verlust_text, summenscore, monate_num, beeintr_text)
list(
chiffre = chiffre,
datum_str = datum_str,
info_mehrere = info_mehrere,
verlust_text = verlust_text,
monate_str = monate_str,
monate_num = monate_num,
beeintr_text = beeintr_text,
summenscore = summenscore,
stufen = stufen,
anker_texte = anker_texte,
item_texte = item_texte,
diagnose = diagnose,
error = NULL
)
})
output$fehler_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
if (!is.null(d$error)) div(class = "alert-fehler", d$error)
})
output$warnung_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
if (!is.null(d$error) || is.null(d$info_mehrere)) return(NULL)
div(class = "alert-warnung", d$info_mehrere)
})
output$ergebnis_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
if (!is.null(d$error)) return(NULL)
diag = d$diagnose
kat_key = as.character(diag$kategorie)
items_ui = lapply(seq_len(10), function(i) {
stufe = d$stufen[i]
anker = d$anker_texte[i]
sk = if (!is.na(stufe) && stufe >= 0L && stufe <= 4L)
as.character(stufe) else "0"
anker_txt = if (!is.na(anker)) anker else
c("Ueberhaupt nicht", "Kaum", "Ein wenig", "Ziemlich", "Sehr")[as.integer(sk) + 1L]
item_txt = if (!is.na(d$item_texte[i])) d$item_texte[i] else paste0("Item ", i)
div(class = "item-zeile",
div(class = "item-nr", paste0(i, ".")),
div(class = "item-text", item_txt),
span(class = paste0("stufe-badge stufe-badge-", sk), anker_txt)
)
})
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "PG-13-Revised"),
div(class = "meta-block",
tags$strong("Chiffre: "), d$chiffre,
tags$span(" | ", style = "color:#ccc;"),
tags$strong("Ausfuelldatum: "), d$datum_str
),
tags$hr(),
fluidRow(
column(3,
div(
div(class = "score-zahl", d$summenscore),
div("Summenscore (10-50)", style = "color:#555;"),
div(class = "cutoff-info",
if (isTRUE(d$summenscore >= 30))
tags$span(style = "color:#B71C1C; font-weight:600;",
paste0(d$summenscore, " >= 30: Schwelle erreicht"))
else
tags$span(style = "color:#2E7D32; font-weight:600;",
paste0(d$summenscore, " < 30: Unterhalb Cutoff"))
)
)
),
column(9, plotOutput("gauge_plot", height = "160px"))
),
tags$hr(),
tags$h5("Diagnostische Einordnung"),
div(class = paste0("diagnose-box pg-kat-", kat_key),
div(class = "diagnose-titel",
paste0("Kategorie ", diag$kategorie, ": ", diag$titel)),
div(class = "diagnose-hinweis", diag$hinweis),
div(class = "diagnose-disclaimer", diag$disclaimer)
),
tags$hr(),
tags$h5("Kontextangaben"),
div(class = "kontext-zeile",
div(class = "kontext-label", "Verlust erlebt:"),
div(if (is.na(d$verlust_text)) "k. A." else d$verlust_text)
),
div(class = "kontext-zeile",
div(class = "kontext-label", "Monate seit dem Verlust:"),
div(d$monate_str)
),
div(class = "kontext-zeile",
div(class = "kontext-label", "Funktionsbeeintraechtigung:"),
div(if (is.na(d$beeintr_text)) "k. A." else d$beeintr_text)
),
tags$hr(),
tags$h5("PG-13-R Einzelitems"),
div(items_ui)
)
})
output$gauge_plot = renderPlot({
req(input$btn_suchen)
d = ergebnis_r()
req(is.null(d$error))
make_gauge_pg13r(d$summenscore)
}, bg = "transparent")
output$download_word = downloadHandler(
filename = function() {
d = tryCatch(ergebnis_r(), error = function(e) NULL)
chiffre = if (is.list(d) && is.null(d$error) && nchar(d$chiffre) > 0)
d$chiffre else "export"
datum = if (is.list(d) && is.null(d$error) && !is.null(d$datum_str))
tryCatch(
format(as.Date(d$datum_str, "%d.%m.%Y"), "%Y%m%d"),
error = function(e) format(Sys.Date(), "%Y%m%d")
)
else
format(Sys.Date(), "%Y%m%d")
paste0("PG13R_", chiffre, "_", datum, ".docx")
},
content = function(file) {
d = tryCatch(ergebnis_r(), error = function(e) NULL)
daten_ok = is.list(d) && is.null(d$error)
if (!daten_ok) {
doc = read_docx()
doc = body_add_par(doc,
"Kein Datensatz geladen. Bitte zuerst Chiffre eingeben und 'Daten laden' klicken.",
style = "Normal")
print(doc, target = file)
return()
}
doc = tryCatch(
erstelle_pg13r_docx(d),
error = function(e) {
err_doc = read_docx()
body_add_par(err_doc,
paste0("Fehler beim Erstellen des Word-Dokuments: ", e$message),
style = "Normal")
}
)
print(doc, target = file)
}
)
}
# Start ####
shinyApp(ui, server)