Initial commit

This commit is contained in:
Jonas Karneboge 2026-09-22 18:35:43 +02:00
commit 3cba772836
1341 changed files with 532924 additions and 0 deletions

640
VDS30-Stil/app.R Normal file
View file

@ -0,0 +1,640 @@
# Präambel ####
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_vds30_stil.R" # liefert: daten_vds30_stil
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R" # liefert: pseudo
AKZENT_FARBE = "#8B2635"
VDS30_STIL_DISCLAIMER = paste0(
"Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ",
"keine klinische Diagnose. Die Interpretation obliegt der behandelnden Person. ",
"Zu diesem Fragebogen liegt kein Auswertungsblatt mit Summenscores oder Cutoffs vor; ",
"die Darstellung ist rein deskriptiv."
)
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 ####
# Wert (0-3) eines Stil-Ratings. Die Antwortoptionen sind reine Ziffern ohne
# Wortanker ("0","1","2","3") - es ist NICHT auszuschliessen, dass formr diese
# als reiner numerischer Vektor OHNE labels-Attribut exportiert (siehe
# Build-Prompt). Deckt daher defensiv beide Faelle ab:
# (a) haven-labelled: Ziffer aus dem NAMEN des passenden labels-Eintrags lesen
# (b) kein labels-Attribut: Rohwert direkt als Ziffer 0-3 interpretieren
vds30stil_get_rating = function(spalte_voll, wert_roh) {
if (is.null(wert_roh) || length(wert_roh) == 0 || is.na(wert_roh[1])) return(NA_real_)
wert_roh = wert_roh[1]
labels_attr = attr(spalte_voll, "labels")
if (!is.null(labels_attr) && length(labels_attr) > 0) {
treffer = which(as.numeric(labels_attr) == as.numeric(unclass(wert_roh)))
if (length(treffer) > 0) {
ziffer = suppressWarnings(as.numeric(trimws(names(labels_attr)[treffer[1]])))
if (!is.na(ziffer)) return(ziffer)
}
}
wert_num = suppressWarnings(as.numeric(unclass(wert_roh)))
if (!is.na(wert_num) && wert_num >= 0 && wert_num <= 3) return(wert_num)
NA_real_
}
# Klartext eines Rangfolge-Feldes (formr-Typ select_one). Der Exportwert traegt
# ueblicherweise bereits den vollen Anzeigetext der gewaehlten Option; deckt
# defensiv aber auch den Fall ab, dass nur der interne Skalen-Code (z.B. "bo")
# exportiert wird - dann ueber VDS30STIL_SKALEN auf den Anzeigetext mappen
# (siehe Build-Prompt, Verifikationshinweis).
vds30stil_rang_text = function(spalte_voll, wert_roh) {
if (is.null(wert_roh) || length(wert_roh) == 0 || is.na(wert_roh[1])) return(NA_character_)
wert_roh = wert_roh[1]
lab = attr(spalte_voll, "labels")
klartext = NA_character_
if (!is.null(lab) && length(lab) > 0) {
wert_chr = trimws(as.character(unclass(wert_roh)))
if (is.character(lab) && !is.null(names(lab)) && wert_chr %in% names(lab)) {
klartext = unname(lab[[wert_chr]])
} else {
pos = which(as.character(unclass(as.vector(lab))) == wert_chr)
if (length(pos) > 0) klartext = names(lab)[pos[1]]
}
} else if (is.character(wert_roh)) {
klartext = wert_roh
} else {
klartext = as.character(wert_roh)
}
if (is.na(klartext) || trimws(klartext) == "") return(NA_character_)
klartext = trimws(klartext)
treffer_code = which(tolower(VDS30STIL_SKALEN$code_intern) == tolower(klartext))
if (length(treffer_code) > 0) {
return(paste0(VDS30STIL_SKALEN$code_anzeige[treffer_code[1]], " - ",
VDS30STIL_SKALEN$bezeichnung[treffer_code[1]]))
}
klartext
}
# Kleiner horizontaler Balken 0-3 fuer ein einzelnes Stil-Rating. Rein
# deskriptiv: keine Zonenfaerbung, keine Cutoff-Linie, kein Klassifikationstext,
# da es zu diesem Instrument keine Normwerte gibt.
vds30stil_mini_balken = function(wert) {
wert_plot = if (is.na(wert)) 0 else wert
ggplot() +
geom_rect(aes(xmin = 0, xmax = 3, ymin = 0, ymax = 1), fill = "#EAEAEA", color = NA) +
geom_rect(aes(xmin = 0, xmax = wert_plot, ymin = 0, ymax = 1), fill = AKZENT_FARBE, color = NA) +
scale_x_continuous(limits = c(0, 3), expand = c(0, 0)) +
scale_y_continuous(limits = c(0, 1), expand = c(0, 0)) +
theme_void() +
theme(plot.margin = margin(0, 0, 0, 0))
}
# Kombinierter Balkenchart aller 11 Ratings, verwendet fuer den Word-Export
# (dort ist ein einzelnes Bild praktikabler als 11 Einzelgrafiken).
vds30stil_kombi_balken = function(stil_df) {
stil_df$code_anzeige = factor(stil_df$code_anzeige, levels = rev(stil_df$code_anzeige))
stil_df$y_balken = ifelse(is.na(stil_df$wert), 0, stil_df$wert)
stil_df$beschriftung = ifelse(is.na(stil_df$wert), "k. A.", as.character(stil_df$wert))
ggplot(stil_df, aes(x = code_anzeige, y = y_balken)) +
geom_col(fill = AKZENT_FARBE, width = 0.6) +
geom_text(aes(label = beschriftung), hjust = -0.4, size = 4, color = "#333333") +
coord_flip(clip = "off") +
scale_y_continuous(limits = c(0, 3.6), breaks = 0:3) +
labs(x = NULL, y = "Rating (0-3), kein Cutoff / keine Normwerte") +
theme_minimal(base_size = 12) +
theme(
panel.grid.minor = element_blank(),
axis.text.y = element_text(face = "bold", color = "#333333"),
plot.margin = margin(t = 5, r = 40, b = 5, l = 5)
)
}
# Datenaufbereitung ####
# Reihenfolge entspricht der Originalreihenfolge des Bogens. Der formr-interne
# Feldname folgt den VDS30-11-Kuerzeln; bei "bo" und "pr" weicht der sichtbare
# Stil-Code im Original-Bogentext davon ab (dort "EI" bzw. "PN") - deshalb die
# Doppel-Anzeige, damit ein Kliniker das Item anhand des Original-PDF
# wiedererkennt (siehe Build-Prompt).
VDS30STIL_SKALEN = data.frame(
code_intern = c("su","de","zw","pa","hi","sc","na","bo","pr","ss","ko"),
code_anzeige = c("SU","DE","ZW","PA","HI","SC","NA","BO / EI","PR / PN","SS","KO"),
bezeichnung = c("Zurueckhaltend","Anhaenglich","Gewissenhaft","kritisch-wehrhaft","Gesellig",
"Einzelgaenger","Bester","Emotional","Misstrauisch","stark und selbstaendig",
"Vorausschauend"),
stringsAsFactors = FALSE
)
VDS30STIL_SKALEN$feld = paste0("vds30stil_", VDS30STIL_SKALEN$code_intern)
VDS30STIL_RANG1_FELDER = paste0("vds30stil_rang1_", 1:3)
VDS30STIL_RANG2_FELDER = paste0("vds30stil_rang2_", 1:3)
# 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; }
.item-zeile {
display: flex; align-items: flex-start; gap: 10px;
padding: 7px 0; border-bottom: 1px solid #F0F0F0;
}
.item-zeile:last-child { border-bottom: none; }
.item-nr { font-weight: 600; color: #8B2635; min-width: 30px; flex-shrink: 0; }
.item-text { flex: 1; color: #333; font-size: 0.92em; }
.stil-zeile {
display: flex; align-items: center; gap: 12px;
padding: 8px 0; border-bottom: 1px solid #F0F0F0;
}
.stil-zeile:last-child { border-bottom: none; }
.stil-code {
font-weight: 700; color: #8B2635; min-width: 80px; flex-shrink: 0; font-size: 0.9em;
}
.stil-bezeichnung { min-width: 190px; flex-shrink: 0; color: #333; font-size: 0.92em; }
.stil-balken { flex: 1; min-width: 140px; max-width: 220px; }
.stil-wert {
font-weight: 700; color: #333; min-width: 24px; text-align: right; flex-shrink: 0;
}
.rangfolge-hinweis {
font-size: 0.82em; color: #777; font-style: italic;
margin-bottom: 12px; border-bottom: 1px dashed #ddd; padding-bottom: 8px;
}
.rangfolge-liste { margin: 0; padding-left: 22px; }
.rangfolge-liste li { padding: 4px 0; color: #333; font-size: 0.95em; }
.disclaimer-zeile {
font-size: 0.82em; color: #777; font-style: italic;
margin-top: 10px; border-top: 1px solid rgba(0,0,0,.1); padding-top: 8px;
}
"
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("VDS30-STIL Persoenlichkeits-Stil"),
tags$p("11 Einzelratings, kein Summenscore, kein Cutoff rein deskriptiv")
),
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: 200px;",
textInput("chiffre", label = "Patientenchiffre",
placeholder = "z.B. P000123", width = "100%")
),
actionButton("btn_suchen", "Auswerten", class = "btn btn-primary btn-laden"),
div(style = "margin-left: auto;",
downloadButton("download_word", "Word-Export (.docx)")
)
),
uiOutput("fehler_ui"),
uiOutput("warnung_ui"),
uiOutput("ergebnis_ui")
)
)
# Word-Export ####
erstelle_vds30_stil_docx = function(erg) {
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_hinweis = fp_text(font.size = 10, italic = TRUE, color = "#555555")
fp_warnung = fp_text(font.size = 10, italic = TRUE, color = "#BF360C")
fp_disclaimer = fp_text(font.size = 9, italic = TRUE, color = "#777777")
doc = body_add_fpar(doc, fpar(ftext("VDS30-STIL Persoenlichkeits-Stil-Auswertung", fp_titel)))
doc = body_add_fpar(doc, fpar(
ftext("Chiffre: ", fp_label), ftext(erg$chiffre, fp_normal),
ftext(" Ausfuelldatum: ", fp_label), ftext(erg$ausfuelldatum, fp_normal)
))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext(paste0(
"Zu diesem Fragebogen liegt kein Auswertungsblatt mit Summenscore oder Cutoff vor. ",
"Die folgende Darstellung ist rein deskriptiv."
), fp_hinweis)))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Die 11 Einzelratings", fp_abschnitt)))
balken_img = tempfile(fileext = ".png")
ggsave(balken_img, plot = vds30stil_kombi_balken(erg$stil_df), width = 7, height = 4.2, dpi = 150, bg = "white")
doc = body_add_img(doc, src = balken_img, width = 6, height = 3.6)
if (file.exists(balken_img)) file.remove(balken_img)
doc = body_add_par(doc, "", style = "Normal")
for (r in seq_len(nrow(erg$stil_df))) {
zeile = erg$stil_df[r, ]
wert_txt = if (is.na(zeile$wert)) "k. A." else paste0(zeile$wert, " / 3")
doc = body_add_fpar(doc, fpar(
ftext(paste0(zeile$code_anzeige, " ", zeile$bezeichnung, ": "), fp_label),
ftext(wert_txt, fp_normal)
))
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Rangfolge 1 (SUPR) Selbstauskunft", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(ftext(
"Vom Patienten/von der Patientin selbst angegeben, unabhaengig von den Einzelratings erhoben.",
fp_hinweis
)))
for (i in seq_along(erg$rang1)) {
doc = body_add_fpar(doc, fpar(
ftext(paste0(i, ". am treffendsten: "), fp_label),
ftext(if (is.na(erg$rang1[i])) "keine Angabe" else erg$rang1[i], fp_normal)
))
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Rangfolge 2 (alle 11 Skalen) Selbstauskunft", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(ftext(
"Vom Patienten/von der Patientin selbst angegeben, nachdem geprueft wurde, ob SS/KO treffender sind.",
fp_hinweis
)))
for (i in seq_along(erg$rang2)) {
doc = body_add_fpar(doc, fpar(
ftext(paste0(i, ". am treffendsten: "), fp_label),
ftext(if (is.na(erg$rang2[i])) "keine Angabe" else erg$rang2[i], fp_normal)
))
}
doc = body_add_par(doc, "", style = "Normal")
if (!is.null(erg$mehrfach_warnung)) {
doc = body_add_fpar(doc, fpar(ftext(erg$mehrfach_warnung, fp_warnung)))
doc = body_add_par(doc, "", style = "Normal")
}
doc = body_add_fpar(doc, fpar(ftext(VDS30_STIL_DISCLAIMER, fp_disclaimer)))
doc
}
# Server ####
server = function(input, output, session) {
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 auf "Auswerten".
ergebnis = eventReactive(input$btn_suchen, {
chiffre = toupper(trimws(input$chiffre))
if (nchar(trimws(input$pseudonym)) == 0 && nchar(chiffre) == 0) {
return(list(typ = "leere_eingabe", meldung = "Bitte Chiffre oder Pseudonym eingeben."))
}
if (nchar(trimws(input$pseudonym)) == 0 && !grepl("^[A-Z][0-9]{6}$", chiffre)) {
return(list(typ = "format_fehler", chiffre = chiffre))
}
if (!file.exists(PFAD_DOWNLOAD_SKRIPT)) {
return(list(typ = "skript_fehler",
meldung = paste0("Download-Skript nicht gefunden:\n", PFAD_DOWNLOAD_SKRIPT)))
}
if (!file.exists(PFAD_PSEUDONYM_SKRIPT)) {
return(list(typ = "skript_fehler",
meldung = paste0("Pseudonym-Skript nicht gefunden:\n", PFAD_PSEUDONYM_SKRIPT)))
}
# Schritt 1: Download-Skript sourcen
ok_dl = tryCatch({
source(PFAD_DOWNLOAD_SKRIPT, local = FALSE)
list(ok = TRUE)
}, error = function(e) list(ok = FALSE, msg = e$message))
if (!ok_dl$ok) return(list(typ = "skript_fehler", meldung = ok_dl$msg))
# Schritt 2: pseudonyme.db suchen (bis zu 5 Ebenen ueber dem Pseudonym-Skript)
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
})
if (is.null(db_ordner)) return(list(typ = "db_nicht_gefunden"))
# Schritt 3: Pseudonym-Skript sourcen (relativer DB-Zugriff, daher setwd + on.exit)
alter_wd = getwd()
on.exit(setwd(alter_wd), add = TRUE)
setwd(db_ordner)
ok_ps = tryCatch({
source(PFAD_PSEUDONYM_SKRIPT, local = FALSE)
list(ok = TRUE)
}, error = function(e) list(ok = FALSE, msg = e$message))
if (!ok_ps$ok) return(list(typ = "skript_fehler", meldung = ok_ps$msg))
if (!exists("daten_vds30_stil", envir = .GlobalEnv) || !exists("pseudo", envir = .GlobalEnv)) {
return(list(typ = "daten_fehlen"))
}
daten = get("daten_vds30_stil", envir = .GlobalEnv)
pseudo_df = get("pseudo", envir = .GlobalEnv)
# Bei Bedarf zur Diagnose aktivieren (zeigt das tatsaechliche Exportformat
# der Rating- und Rangfolge-Spalten in der R-Konsole):
# print(str(daten[, c(VDS30STIL_SKALEN$feld, VDS30STIL_RANG1_FELDER, VDS30STIL_RANG2_FELDER)]))
# Spaltenname der Session-ID: hier als "session" angenommen, analog zu den
# uebrigen Instrumenten dieses Projekts (siehe Build-Prompt-Verifikationshinweis
# - vor Produktiveinsatz mit einem echten Testdatensatz pruefen).
if (!("session" %in% names(daten))) {
return(list(typ = "daten_fehlen",
meldung = "Erwartete Spalte 'session' nicht in 'daten_vds30_stil' gefunden."))
}
# Schritt 4: Chiffre-Rueckaufloesung, falls Pseudonym eingegeben wurde
if (nchar(trimws(input$pseudonym)) > 0) {
pw_treffer = pseudo_df[pseudo_df$pseudonym == trimws(input$pseudonym), ]
if (nrow(pw_treffer) > 0) chiffre = toupper(trimws(pw_treffer$chiffre[1]))
}
# Schritt 5: Chiffre -> moegliche Pseudonyme (Session-IDs)
treffer_ps = pseudo_df[toupper(trimws(pseudo_df$chiffre)) == chiffre, ]
if (nrow(treffer_ps) == 0) return(list(typ = "kein_datensatz", chiffre = chiffre))
alle_session_ids = unique(treffer_ps$pseudonym)
if (nchar(trimws(input$pseudonym)) > 0) alle_session_ids = trimws(input$pseudonym)
# Schritt 6: passende Datensaetze in daten_vds30_stil finden
treffer_daten = daten[daten$session %in% alle_session_ids, ]
if (nrow(treffer_daten) == 0) return(list(typ = "kein_datensatz", chiffre = chiffre))
mehrfach_warnung = NULL
if (nrow(treffer_daten) > 1) {
n = nrow(treffer_daten)
# Spaltenname des Erstellungszeitstempels: hier als "created" angenommen,
# analog zu den uebrigen Instrumenten (Verifikationshinweis, siehe oben).
if ("created" %in% names(treffer_daten)) {
treffer_daten = treffer_daten[order(treffer_daten$created, decreasing = TRUE), ]
}
treffer_daten = treffer_daten[1, , drop = FALSE]
mehrfach_warnung = paste0(
"Mehrere Ausfuellungen gefunden (", n, " Eintraege) es wird die neueste angezeigt."
)
}
zeile = treffer_daten[1, , drop = FALSE]
ausfuelldatum = tryCatch(
format(as.POSIXct(zeile[["created"]][1]), "%d.%m.%Y"),
error = function(e) format(Sys.Date(), "%d.%m.%Y")
)
# Schritt 7: die 11 Einzelratings, in Originalreihenfolge des Bogens
stil_df = VDS30STIL_SKALEN
stil_df$wert = sapply(seq_len(nrow(stil_df)), function(i) {
feld = stil_df$feld[i]
spalte_voll = daten[[feld]]
wert_roh = if (feld %in% names(zeile)) zeile[[feld]][1] else NA
if (is.null(spalte_voll)) NA_real_ else vds30stil_get_rating(spalte_voll, wert_roh)
})
# Schritt 8: die zwei Rangfolge-Bloecke, rein deskriptiv (Patientenangabe)
rang1 = sapply(VDS30STIL_RANG1_FELDER, function(feld) {
spalte_voll = daten[[feld]]
wert_roh = if (feld %in% names(zeile)) zeile[[feld]][1] else NA
if (is.null(spalte_voll)) NA_character_ else vds30stil_rang_text(spalte_voll, wert_roh)
})
rang2 = sapply(VDS30STIL_RANG2_FELDER, function(feld) {
spalte_voll = daten[[feld]]
wert_roh = if (feld %in% names(zeile)) zeile[[feld]][1] else NA
if (is.null(spalte_voll)) NA_character_ else vds30stil_rang_text(spalte_voll, wert_roh)
})
list(
typ = "erfolg",
chiffre = chiffre,
ausfuelldatum = ausfuelldatum,
mehrfach_warnung = mehrfach_warnung,
stil_df = stil_df,
rang1 = unname(rang1),
rang2 = unname(rang2)
)
})
vds30stil_fehlermeldung = function(d) {
switch(d$typ,
"leere_eingabe" = d$meldung,
"format_fehler" = paste0("Ungueltige Chiffre '", d$chiffre, "'. Erwartet: ein Grossbuchstabe + 6 Ziffern (z.B. P000123)."),
"skript_fehler" = paste0("Fehler beim Sourcen eines externen Skripts: ", d$meldung),
"db_nicht_gefunden" = "Die Datei 'pseudonyme.db' konnte in den uebergeordneten Verzeichnissen nicht gefunden werden.",
"daten_fehlen" = if (!is.null(d$meldung)) d$meldung else "Nach dem Sourcen der Skripte fehlen die erwarteten Objekte 'daten_vds30_stil' oder 'pseudo'.",
"kein_datensatz" = paste0("Kein VDS30-STIL-Datensatz fuer Chiffre '", d$chiffre, "' gefunden."),
"Unbekannter Fehler."
)
}
output$fehler_ui = renderUI({
req(input$btn_suchen)
d = ergebnis()
if (d$typ != "erfolg") div(class = "alert-fehler", vds30stil_fehlermeldung(d))
})
output$warnung_ui = renderUI({
req(input$btn_suchen)
d = ergebnis()
if (d$typ != "erfolg") return(NULL)
if (!is.null(d$mehrfach_warnung)) div(class = "alert-warnung", d$mehrfach_warnung)
})
output$ergebnis_ui = renderUI({
req(input$btn_suchen)
d = ergebnis()
if (d$typ != "erfolg") return(NULL)
stil_zeilen = lapply(seq_len(nrow(d$stil_df)), function(i) {
zeile = d$stil_df[i, ]
plot_id = paste0("balken_", zeile$code_intern)
div(class = "stil-zeile",
div(class = "stil-code", zeile$code_anzeige),
div(class = "stil-bezeichnung", zeile$bezeichnung),
div(class = "stil-balken", plotOutput(plot_id, height = "22px")),
div(class = "stil-wert", if (is.na(zeile$wert)) "k. A." else zeile$wert)
)
})
rang1_ui = tags$ol(class = "rangfolge-liste",
lapply(d$rang1, function(x) tags$li(if (is.na(x)) "keine Angabe" else x))
)
rang2_ui = tags$ol(class = "rangfolge-liste",
lapply(d$rang2, function(x) tags$li(if (is.na(x)) "keine Angabe" else x))
)
tagList(
div(class = "abschnitt-karte",
div(class = "meta-block",
tags$strong("Chiffre: "), d$chiffre,
tags$span(" | ", style = "color:#ccc;"),
tags$strong("Ausfuelldatum: "), d$ausfuelldatum
)
),
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Die 11 Einzelratings"),
div(stil_zeilen)
),
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Rangfolge 1 (SUPR)"),
div(class = "rangfolge-hinweis",
"Selbstauskunft des Patienten/der Patientin: am treffendsten / am zweittreffendsten / am dritttreffendsten beschreibt mich Skala..."),
rang1_ui
),
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Rangfolge 2 (alle 11 Skalen, nach Pruefung SS/KO)"),
div(class = "rangfolge-hinweis",
"Selbstauskunft des Patienten/der Patientin, unabhaengig von Rangfolge 1 erhoben."),
rang2_ui
),
div(class = "disclaimer-zeile", VDS30_STIL_DISCLAIMER)
)
})
observe({
req(input$btn_suchen)
d = ergebnis()
if (d$typ != "erfolg") return(NULL)
for (i in seq_len(nrow(d$stil_df))) {
local({
ii = i
wert = d$stil_df$wert[ii]
plot_id = paste0("balken_", d$stil_df$code_intern[ii])
output[[plot_id]] = renderPlot(vds30stil_mini_balken(wert), bg = "transparent")
})
}
})
output$download_word = downloadHandler(
filename = function() {
d = tryCatch(ergebnis(), error = function(e) NULL)
erfolgreich = is.list(d) && identical(d$typ, "erfolg")
chiffre_esc = if (erfolgreich && nchar(d$chiffre) > 0)
gsub("[^A-Za-z0-9_-]", "_", d$chiffre) else "export"
ausfuelldatum_fn = if (erfolgreich) {
tryCatch(format(as.Date(d$ausfuelldatum, "%d.%m.%Y"), "%Y%m%d"),
error = function(e) format(Sys.Date(), "%Y%m%d"))
} else {
format(Sys.Date(), "%Y%m%d")
}
paste0("VDS30-STIL_", chiffre_esc, "_", ausfuelldatum_fn, ".docx")
},
content = function(file) {
d = tryCatch(ergebnis(), error = function(e) NULL)
erfolgreich = is.list(d) && identical(d$typ, "erfolg")
if (!erfolgreich) {
doc = read_docx()
doc = body_add_par(doc,
"Kein Datensatz geladen. Bitte zuerst Chiffre oder Pseudonym eingeben und 'Auswerten' klicken.",
style = "Normal")
print(doc, target = file)
return()
}
doc = tryCatch(
erstelle_vds30_stil_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)