994 lines
37 KiB
R
994 lines
37 KiB
R
# Praeambel ####
|
||
|
||
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_vds23.R" # liefert: daten_vds23
|
||
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R" # liefert: pseudo
|
||
AKZENT_FARBE = "#8B2635"
|
||
|
||
VDS23_DISCLAIMER = paste0(
|
||
"Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ",
|
||
"keine klinische Diagnose. Es liegen keine Normwerte oder Cutoffs fuer dieses ",
|
||
"Instrument vor; die dargestellten Werte sind deskriptive Positionsangaben auf ",
|
||
"der 0 bis 5 Skala, keine Vergleichswerte gegen eine Referenzstichprobe. Die ",
|
||
"Interpretation obliegt der behandelnden Person."
|
||
)
|
||
VDS23_DISCLAIMER = gsub("fuer", "für", VDS23_DISCLAIMER, fixed = TRUE)
|
||
|
||
VDS23_STUFEN_TEXTE = c("nicht", "kaum", "etwas", "deutlich", "sehr", "extrem")
|
||
|
||
# 6 Stufen (0-5), gruen -> dunkelrot. Generische Klassennamen .stufe-badge-0 .. -5,
|
||
# siehe app_css weiter unten.
|
||
VDS23_BADGE_FARBEN = c(
|
||
"0" = "#4CAF50", # gruen
|
||
"1" = "#E6EE9C", # helles Gelbgruen
|
||
"2" = "#F48FB1", # helles Rosa
|
||
"3" = "#EF5350", # mittleres Rot
|
||
"4" = "#C62828", # kraeftiges Rot
|
||
"5" = "#4A0000" # dunkles Rot
|
||
)
|
||
VDS23_BADGE_TEXT_FARBEN = c(
|
||
"0" = "white", "1" = "#333333", "2" = "#333333",
|
||
"3" = "white", "4" = "white", "5" = "white"
|
||
)
|
||
|
||
# Item-Faktor-Zuordnung, fest aus dem Testmanual uebernommen (nicht aus dem
|
||
# Itemtext neu abgeleitet). 11 Items sind doppelt zugeordnet, 5 Items keinem
|
||
# Faktor (21, 43, 54, 55, 62).
|
||
vds23_faktoren = list(
|
||
"1" = list(
|
||
name = "Beziehung nimmt Schaden",
|
||
items = c(3, 6, 7, 11, 12, 13, 14, 15, 16, 17, 18, 19, 22, 29, 30, 32, 37, 38, 44, 47, 48, 50, 51, 58, 59)
|
||
),
|
||
"2" = list(
|
||
name = "Abgrenzung gelingt nicht",
|
||
items = c(17, 18, 23, 24, 25, 26, 40, 41, 42, 49, 52, 53, 60)
|
||
),
|
||
"3" = list(
|
||
name = "Bedürfnis-Frustration",
|
||
items = c(33, 35, 39, 45, 51, 53, 56, 57, 58, 61)
|
||
),
|
||
"4" = list(
|
||
name = "Abgelehnt werden",
|
||
items = c(2, 4, 5, 10, 15, 27, 28, 35, 38)
|
||
),
|
||
"5" = list(
|
||
name = "Einfluss-Verlust",
|
||
items = c(8, 9, 31, 46, 49, 63)
|
||
),
|
||
"6" = list(
|
||
name = "Unterlegen sein",
|
||
items = c(1, 20, 25, 33, 34, 36)
|
||
)
|
||
)
|
||
|
||
# Kontrollsumme: 25 + 13 + 10 + 9 + 6 + 6 = 69 Zuordnungen bei 63 Items.
|
||
# Bei Abweichung sofort abbrechen statt still weiterzurechnen, das wuerde
|
||
# einen Copy-Paste-Fehler in der Struktur oben sonst verschleiern.
|
||
.vds23_kontrollsumme = sum(sapply(vds23_faktoren, function(f) length(f$items)))
|
||
if (.vds23_kontrollsumme != 69) {
|
||
stop(
|
||
"VDS23: Kontrollsumme der Item-Faktor-Zuordnung ist ", .vds23_kontrollsumme,
|
||
", erwartet 69. Bitte 'vds23_faktoren' pruefen (Copy-Paste-Fehler?)."
|
||
)
|
||
}
|
||
|
||
VDS23_OHNE_FAKTOR = c(21, 43, 54, 55, 62)
|
||
|
||
vds23_itemtexte = c(
|
||
"01" = "Ich allein bin",
|
||
"02" = "Ich mit wenig vertrauten Personen zu tun habe",
|
||
"03" = "Ich mit einer wichtigen Bezugsperson zu tun habe",
|
||
"04" = "Ich nicht erkennen kann, worauf es ankommt",
|
||
"05" = "Andere darauf achten, was ich wie tue",
|
||
"06" = "Andere etwas von mir erwarten, das ich nicht kann",
|
||
"07" = "Andere etwas von mir erwarten, das ich nicht will",
|
||
"08" = "Ich Nein sagen müsste",
|
||
"09" = "Ich Forderungen stellen müsste",
|
||
"10" = "Ich Kontakt mit jemandem aufnehmen müsste, jemand ansprechen müsste",
|
||
"11" = "Andere mich links liegen lassen",
|
||
"12" = "Andere mich nicht akzeptieren, nicht aufnehmen",
|
||
"13" = "Andere gegen mich vorgehen",
|
||
"14" = "Andere mir etwas vorwerfen, mich kritisieren",
|
||
"15" = "Andere mit mir konkurrieren, rivalisieren",
|
||
"16" = "Andere kein Verständnis für mich haben",
|
||
"17" = "Andere mich ausnutzen wollen",
|
||
"18" = "Andere mich übervorteilen, betrügen wollen",
|
||
"19" = "Andere nicht einsehen wollen, dass ich Recht habe",
|
||
"20" = "Andere sich mir einfach entziehen",
|
||
"21" = "Sich andere mit Ausreden rauswinden",
|
||
"22" = "Andere mich beschämen (die mir peinlich sind)",
|
||
"23" = "Ich meine Pflicht nicht erfülle, erfüllen kann",
|
||
"24" = "Ich nicht erreiche, was ich will",
|
||
"25" = "Mich in meinen Kräften oder Fähigkeiten überfordern",
|
||
"26" = "Ich versage",
|
||
"27" = "Ich im Mittelpunkt stehe",
|
||
"28" = "Ich einen Fehler zugeben müsste",
|
||
"29" = "Ich bei etwas Unrechtem ertappt werden",
|
||
"30" = "Ich unangenehm auffalle",
|
||
"31" = "Die Harmonie gestört ist",
|
||
"32" = "Ich einen großen Verlust erleide",
|
||
"33" = "Ich etwas hergeben muss",
|
||
"34" = "Andere besser sind als ich",
|
||
"35" = "Ich eine schwierige Entscheidung treffen sollte",
|
||
"36" = "Ich tun muss, was andere mir anordnen",
|
||
"37" = "Ich es nicht schaffe, attraktiv genug zu sein",
|
||
"38" = "Mir meine Gefühle einen Strich durch die Rechnung machen",
|
||
"39" = "Sehr intim sind",
|
||
"40" = "Andere meine Grenzen überschreiten, übergriffig werden",
|
||
"41" = "Ich eingeengt und unfrei bin",
|
||
"42" = "Andere mir zu nahe kommen",
|
||
"43" = "Ich nicht bekomme, was ich brauche, so viel wie ich brauche",
|
||
"44" = "Ich im Stich gelassen werde",
|
||
"45" = "Meine Forderungen abgelehnt werden",
|
||
"46" = "Sich andere nicht durch mich beeinflussen lassen",
|
||
"47" = "Es mir nicht gelingt, die Kontrolle über mich zu bewahren",
|
||
"48" = "Meine Liebesbeziehung in Gefahr ist",
|
||
"49" = "Umgangsregeln nicht eingehalten werden",
|
||
"50" = "Meine Gefühle verletzt werden",
|
||
"51" = "Ich nicht gebraucht werde",
|
||
"52" = "Meine Meinungsfreiheit eingeschränkt wird",
|
||
"53" = "Meine Sicherheit und Existenz bedroht ist",
|
||
"54" = "Mein Glaube angefochten wird",
|
||
"55" = "Gefahr besteht, meine Familie zu verlieren",
|
||
"56" = "Ich gehindert werde, meinen Genüssen nachzugehen",
|
||
"57" = "Ich wichtige innere Gebote nicht eingehalten habe",
|
||
"58" = "Ich wichtige Verbote überschritten habe",
|
||
"59" = "Ich in einem großen Konflikt stehe",
|
||
"60" = "Die ewig wiederkehrenden Probleme mit einer wichtigen Bezugsperson auftreten",
|
||
"61" = "Ein sexuelles Problem auftritt",
|
||
"62" = "Meine Wünsche erfüllt, meine Bedürfnisse befriedigt werden",
|
||
"63" = "Jemand gut zu mir ist"
|
||
)
|
||
|
||
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 ####
|
||
|
||
# OFFENER PUNKT FUER ERSTEN TESTLAUF (siehe Projekt-Notizen, Abschnitt 16.1):
|
||
# Ob formr mc-Felder beim Export den Choice-Text oder den 1-basierten
|
||
# Choice-Index liefert, ist fuer diese Instanz nicht verifiziert. Diese
|
||
# Funktion deckt beide Faelle robust ueber das labels-Attribut der
|
||
# ORIGINAL-Spalte ab (nicht ueber hartkodierte Zahlenwerte) und liefert bei
|
||
# unbekanntem Format NA statt einer geratenen Zahl.
|
||
vds23_wert_inhaltlich = function(rohwert, labels_attr) {
|
||
mapping_text = c(
|
||
"nicht" = 0L, "kaum" = 1L, "etwas" = 2L,
|
||
"deutlich" = 3L, "sehr" = 4L, "extrem" = 5L
|
||
)
|
||
|
||
if (is.null(rohwert) || length(rohwert) == 0 || is.na(rohwert[1])) {
|
||
return(list(wert = NA_integer_, status = "na"))
|
||
}
|
||
|
||
roh_chr = trimws(as.character(rohwert[1]))
|
||
roh_lower = tolower(roh_chr)
|
||
roh_num = suppressWarnings(as.numeric(roh_chr))
|
||
|
||
# Fall A: Rohwert ist bereits direkt einer der 6 bekannten Choice-Texte.
|
||
if (roh_lower %in% names(mapping_text)) {
|
||
return(list(wert = unname(mapping_text[[roh_lower]]), status = "text_direkt"))
|
||
}
|
||
|
||
if (!is.null(labels_attr) && length(labels_attr) > 0) {
|
||
lbl_namen = tolower(trimws(names(labels_attr)))
|
||
lbl_werte = as.vector(labels_attr)
|
||
|
||
pos = if (!is.na(roh_num)) which(lbl_werte == roh_num) else which(lbl_namen == roh_lower)
|
||
|
||
if (length(pos) > 0) {
|
||
choice_text = lbl_namen[pos[1]]
|
||
# Fall B: Rohwert entspricht (ueber labels) einem bekannten Choice-Text.
|
||
if (choice_text %in% names(mapping_text)) {
|
||
return(list(wert = unname(mapping_text[[choice_text]]), status = "labels_text"))
|
||
}
|
||
# Fall C: Choice-Text nicht erkannt, aber die sortierte Position des
|
||
# Rohwerts unter allen labels-Werten ergibt einen 1-basierten Index 1-6.
|
||
lbl_sortiert = sort(lbl_werte)
|
||
idx_pos = which(lbl_sortiert == lbl_werte[pos[1]])
|
||
if (length(idx_pos) > 0 && idx_pos[1] >= 1 && idx_pos[1] <= 6) {
|
||
return(list(wert = as.integer(idx_pos[1] - 1L), status = "labels_index"))
|
||
}
|
||
}
|
||
}
|
||
|
||
# Fall D: kein (nutzbares) labels-Attribut, Rohwert ist eine Zahl 1-6 ->
|
||
# 1-basierter Index wird angenommen.
|
||
if (!is.na(roh_num) && roh_num >= 1 && roh_num <= 6 && roh_num == round(roh_num)) {
|
||
return(list(wert = as.integer(roh_num - 1L), status = "index_fallback"))
|
||
}
|
||
|
||
# Fall E: Rohwert ist bereits eine Zahl 0-5 (schon inhaltlich kodiert).
|
||
if (!is.na(roh_num) && roh_num >= 0 && roh_num <= 5 && roh_num == round(roh_num)) {
|
||
return(list(wert = as.integer(roh_num), status = "bereits_inhaltlich"))
|
||
}
|
||
|
||
list(wert = NA_integer_, status = "unbekannt")
|
||
}
|
||
|
||
# Faktor-Score = arithmetisches Mittel der zugeordneten Item-Rohwerte
|
||
# (0-5), kein Reverse-Coding. na.rm = TRUE fuer den defensiven Fall
|
||
# einzelner fehlender Werte trotz Pflichtfeld-Annahme.
|
||
vds23_faktor_score = function(werte_vektor, item_nummern) {
|
||
werte = werte_vektor[item_nummern]
|
||
n_vorhanden = sum(!is.na(werte))
|
||
n_gesamt = length(werte)
|
||
score = if (n_vorhanden > 0) mean(werte, na.rm = TRUE) else NA_real_
|
||
list(score = score, n_vorhanden = n_vorhanden, n_gesamt = n_gesamt)
|
||
}
|
||
|
||
# Choice-Format lt. vds23.xlsx fuer vds23_top5_auswahl: "N. Itemtext" (z.B.
|
||
# "5. Andere darauf achten, was ich wie tue"). Trennzeichen zwischen mehreren
|
||
# Ausgewaehlten ist in dieser formr-Instanz weiterhin nicht live verifiziert,
|
||
# es wird defensiv an ",", ";" und "|" gesplittet. Der Itemtext wird pro
|
||
# Fragment IMMER aus der festen vds23_itemtexte-Tabelle nachgeschlagen (ueber
|
||
# die per Regex gezogene fuehrende Nummer), nicht aus dem Fragment selbst
|
||
# uebernommen - das liefert auch dann den vollen Text, wenn formr ein
|
||
# Fragment nur als blosse Nummer ("5" statt "5. Andere darauf achten ...")
|
||
# exportiert. Nur wenn weder eine fuehrende Nummer noch ein exakter
|
||
# Text-Treffer gefunden wird, bleibt es beim unveraenderten Rohtext statt
|
||
# einer Ratewert-Zuordnung.
|
||
vds23_parse_top5 = function(rohtext, itemtexte) {
|
||
leer_ergebnis = data.frame(
|
||
nummer = integer(0), text = character(0), roh = character(0),
|
||
stringsAsFactors = FALSE
|
||
)
|
||
if (is.null(rohtext) || length(rohtext) == 0 || is.na(rohtext[1]) ||
|
||
trimws(as.character(rohtext[1])) == "") {
|
||
return(leer_ergebnis)
|
||
}
|
||
|
||
txt = trimws(as.character(rohtext[1]))
|
||
fragmente = trimws(strsplit(txt, "[,;|]")[[1]])
|
||
fragmente = fragmente[nchar(fragmente) > 0]
|
||
if (length(fragmente) == 0) return(leer_ergebnis)
|
||
|
||
itemtexte_lower = tolower(trimws(itemtexte))
|
||
|
||
zeilen = lapply(fragmente, function(frag) {
|
||
frag_trim = trimws(frag)
|
||
|
||
# Fall A: Fragment beginnt mit der Itemnummer (mit oder ohne Text
|
||
# dahinter) - Text immer aus der Itemtexte-Tabelle nachschlagen.
|
||
nr_match = regmatches(frag_trim, regexpr("^[0-9]{1,2}", frag_trim))
|
||
if (length(nr_match) > 0 && nchar(nr_match) > 0) {
|
||
nr = as.integer(nr_match)
|
||
key = sprintf("%02d", nr)
|
||
if (nr >= 1 && nr <= 63 && key %in% names(itemtexte)) {
|
||
return(data.frame(nummer = nr, text = unname(itemtexte[[key]]), roh = frag,
|
||
stringsAsFactors = FALSE))
|
||
}
|
||
}
|
||
|
||
# Fall B: Fragment ist der vollstaendige Itemtext ohne fuehrende Nummer.
|
||
frag_lower = tolower(frag_trim)
|
||
treffer = which(itemtexte_lower == frag_lower)
|
||
if (length(treffer) == 1) {
|
||
return(data.frame(nummer = as.integer(names(itemtexte)[treffer]),
|
||
text = unname(itemtexte[treffer]), roh = frag,
|
||
stringsAsFactors = FALSE))
|
||
}
|
||
|
||
data.frame(nummer = NA_integer_, text = NA_character_, roh = frag,
|
||
stringsAsFactors = FALSE)
|
||
})
|
||
do.call(rbind, zeilen)
|
||
}
|
||
|
||
vds23_badge_klasse = function(wert) {
|
||
if (is.na(wert)) return("stufe-badge-na")
|
||
paste0("stufe-badge-", max(0L, min(5L, as.integer(wert))))
|
||
}
|
||
|
||
vds23_badge_text = function(wert) {
|
||
if (is.na(wert)) return("k. A.")
|
||
VDS23_STUFEN_TEXTE[max(0L, min(5L, as.integer(wert))) + 1L]
|
||
}
|
||
|
||
# Neutrale 0-5-Achse ohne Farbzonen (kein Cutoff dokumentiert), nur eine
|
||
# Markierung an der Position des Faktor-Mittelwerts.
|
||
vds23_achse_plot = function(score) {
|
||
ggplot() +
|
||
geom_segment(aes(x = 0, xend = 5, y = 0, yend = 0), color = "#BDBDBD", linewidth = 1.2) +
|
||
geom_point(aes(x = 0:5, y = 0), color = "#9E9E9E", size = 2) +
|
||
geom_segment(aes(x = score, xend = score, y = -0.35, yend = 0.35),
|
||
color = AKZENT_FARBE, linewidth = 2.2) +
|
||
geom_label(aes(x = score, y = 0.8, label = format(round(score, 1), nsmall = 1)),
|
||
fill = AKZENT_FARBE, color = "white", fontface = "bold",
|
||
linewidth = 0, size = 4) +
|
||
scale_x_continuous(limits = c(-0.3, 5.3), breaks = 0:5) +
|
||
scale_y_continuous(limits = c(-0.6, 1.15)) +
|
||
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(),
|
||
axis.title.x = element_blank(),
|
||
plot.margin = margin(t = 5, r = 10, b = 5, l = 10)
|
||
)
|
||
}
|
||
|
||
|
||
# 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; }
|
||
.faktor-score-block { display: flex; align-items: baseline; gap: 14px; margin-bottom: 6px; }
|
||
.faktor-score-zahl { font-size: 2.0rem; font-weight: 800; color: #8B2635; }
|
||
.faktor-score-label { color: #555; font-size: 0.9em; }
|
||
.faktor-missing-hinweis { font-size: 0.85em; color: #E65100; margin-bottom: 8px; }
|
||
.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: 26px; flex-shrink: 0; }
|
||
.item-text-block { flex: 1; }
|
||
.item-text { color: #333; font-size: 0.92em; }
|
||
.item-beispiel {
|
||
color: #777; font-size: 0.86em; font-style: italic; margin-top: 2px;
|
||
}
|
||
.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-color: #4CAF50; color: white; }
|
||
.stufe-badge-1 { background-color: #E6EE9C; color: #333333; }
|
||
.stufe-badge-2 { background-color: #F48FB1; color: #333333; }
|
||
.stufe-badge-3 { background-color: #EF5350; color: white; }
|
||
.stufe-badge-4 { background-color: #C62828; color: white; }
|
||
.stufe-badge-5 { background-color: #4A0000; color: white; }
|
||
.stufe-badge-na { background-color: #E0E0E0; color: #555555; }
|
||
.ohne-faktor-hinweis { font-size: 0.85em; color: #777; font-style: italic; margin-bottom: 10px; }
|
||
.top5-liste { margin: 6px 0 0 0; padding-left: 20px; }
|
||
.top5-eintrag { padding: 3px 0; color: #333; font-size: 0.94em; }
|
||
.freitext-block { margin-bottom: 14px; }
|
||
.freitext-frage { font-weight: 600; color: #8B2635; font-size: 0.95em; margin-bottom: 3px; }
|
||
.freitext-antwort { color: #333; font-size: 0.93em; white-space: pre-wrap; line-height: 1.5; }
|
||
.qualitativ-hinweis {
|
||
font-size: 0.82em; color: #777; font-style: italic;
|
||
margin-bottom: 12px; border-bottom: 1px dashed #ddd; padding-bottom: 8px;
|
||
}
|
||
.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("VDS23 – Situationsanalyse"),
|
||
tags$p("63 Items zu unangenehmen Situationen, 6 Faktoren, keine Normwerte")
|
||
),
|
||
|
||
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_vds23_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_beispiel = fp_text(font.size = 9.5, italic = TRUE, color = "#777777")
|
||
fp_disclaimer = fp_text(font.size = 9, italic = TRUE, color = "#777777")
|
||
|
||
doc = body_add_fpar(doc, fpar(ftext("VDS23, Situationsanalyse", fp_titel)))
|
||
doc = body_add_fpar(doc, fpar(
|
||
ftext("Chiffre: ", fp_label),
|
||
ftext(erg$chiffre, fp_normal),
|
||
ftext(" Ausfülldatum: ", fp_label),
|
||
ftext(erg$ausfuelldatum, fp_normal)
|
||
))
|
||
if (!is.null(erg$info_mehrere)) {
|
||
doc = body_add_fpar(doc, fpar(
|
||
ftext(erg$info_mehrere, fp_text(font.size = 10, italic = TRUE, color = "#555555"))
|
||
))
|
||
}
|
||
if (!is.null(erg$kodierwarnung)) {
|
||
doc = body_add_fpar(doc, fpar(
|
||
ftext(erg$kodierwarnung, fp_text(font.size = 10, italic = TRUE, color = "#BF360C"))
|
||
))
|
||
}
|
||
doc = body_add_par(doc, "", style = "Normal")
|
||
|
||
for (fe in erg$faktor_ergebnisse) {
|
||
doc = body_add_fpar(doc, fpar(ftext(fe$name, fp_abschnitt)))
|
||
|
||
score_txt = if (is.na(fe$score)) "keine auswertbaren Items" else
|
||
paste0(format(round(fe$score, 1), nsmall = 1), " auf der Skala 0 bis 5")
|
||
doc = body_add_fpar(doc, fpar(
|
||
ftext("Faktor-Mittelwert: ", fp_label),
|
||
ftext(score_txt, fp_normal)
|
||
))
|
||
if (!is.na(fe$score) && fe$n_vorhanden < fe$n_gesamt) {
|
||
doc = body_add_fpar(doc, fpar(ftext(
|
||
paste0("Faktor-Score basiert auf ", fe$n_vorhanden, " von ", fe$n_gesamt, " Items."),
|
||
fp_text(font.size = 9.5, italic = TRUE, color = "#BF360C")
|
||
)))
|
||
}
|
||
|
||
for (r in seq_len(nrow(fe$items))) {
|
||
zeile = fe$items[r, ]
|
||
badge_key = if (is.na(zeile$wert)) NA_character_ else
|
||
as.character(max(0L, min(5L, as.integer(zeile$wert))))
|
||
if (is.na(badge_key)) {
|
||
fp_badge = fp_text(color = "#555555", bold = TRUE, shading.color = "#E0E0E0", font.size = 10)
|
||
badge_txt = "k. A."
|
||
} else {
|
||
fp_badge = fp_text(
|
||
color = VDS23_BADGE_TEXT_FARBEN[[badge_key]], bold = TRUE,
|
||
shading.color = VDS23_BADGE_FARBEN[[badge_key]], font.size = 10
|
||
)
|
||
badge_txt = VDS23_STUFEN_TEXTE[as.integer(badge_key) + 1L]
|
||
}
|
||
doc = body_add_fpar(doc, fpar(
|
||
ftext(paste0(zeile$nummer, ". ", zeile$text, " "), fp_normal),
|
||
ftext(paste0(" ", badge_txt, " "), fp_badge)
|
||
))
|
||
if (!is.na(zeile$beispiel)) {
|
||
doc = body_add_fpar(doc, fpar(ftext(paste0("Beispiel: ", zeile$beispiel), fp_beispiel)))
|
||
}
|
||
}
|
||
doc = body_add_par(doc, "", style = "Normal")
|
||
}
|
||
|
||
if (nrow(erg$ohne_faktor) > 0) {
|
||
doc = body_add_fpar(doc, fpar(ftext(
|
||
"Weitere erhobene Situationen (keinem Faktor zugeordnet)", fp_abschnitt)))
|
||
for (r in seq_len(nrow(erg$ohne_faktor))) {
|
||
zeile = erg$ohne_faktor[r, ]
|
||
badge_key = if (is.na(zeile$wert)) NA_character_ else
|
||
as.character(max(0L, min(5L, as.integer(zeile$wert))))
|
||
if (is.na(badge_key)) {
|
||
fp_badge = fp_text(color = "#555555", bold = TRUE, shading.color = "#E0E0E0", font.size = 10)
|
||
badge_txt = "k. A."
|
||
} else {
|
||
fp_badge = fp_text(
|
||
color = VDS23_BADGE_TEXT_FARBEN[[badge_key]], bold = TRUE,
|
||
shading.color = VDS23_BADGE_FARBEN[[badge_key]], font.size = 10
|
||
)
|
||
badge_txt = VDS23_STUFEN_TEXTE[as.integer(badge_key) + 1L]
|
||
}
|
||
doc = body_add_fpar(doc, fpar(
|
||
ftext(paste0(zeile$nummer, ". ", zeile$text, " "), fp_normal),
|
||
ftext(paste0(" ", badge_txt, " "), fp_badge)
|
||
))
|
||
if (!is.na(zeile$beispiel)) {
|
||
doc = body_add_fpar(doc, fpar(ftext(paste0("Beispiel: ", zeile$beispiel), fp_beispiel)))
|
||
}
|
||
}
|
||
doc = body_add_par(doc, "", style = "Normal")
|
||
}
|
||
|
||
doc = body_add_break(doc)
|
||
doc = body_add_fpar(doc, fpar(ftext("Top5-Auswahl und Reflexionsfragen", fp_abschnitt)))
|
||
doc = body_add_fpar(doc, fpar(ftext(
|
||
"Qualitative Zusatzinformation, kein Zahlenwert.",
|
||
fp_text(font.size = 9.5, italic = TRUE, color = "#777777")
|
||
)))
|
||
doc = body_add_par(doc, "", style = "Normal")
|
||
|
||
doc = body_add_fpar(doc, fpar(ftext("Ausgewählte unangenehmste Situationen", fp_label)))
|
||
if (nrow(erg$top5) == 0) {
|
||
doc = body_add_fpar(doc, fpar(ftext("Keine Angabe.", fp_normal)))
|
||
} else {
|
||
for (r in seq_len(nrow(erg$top5))) {
|
||
zeile = erg$top5[r, ]
|
||
txt = if (!is.na(zeile$nummer)) paste0(zeile$nummer, ". ", zeile$text) else zeile$roh
|
||
doc = body_add_fpar(doc, fpar(ftext(paste0("• ", txt), fp_normal)))
|
||
}
|
||
}
|
||
doc = body_add_par(doc, "", style = "Normal")
|
||
|
||
for (rf in erg$reflexionen) {
|
||
if (is.na(rf$antwort)) next
|
||
doc = body_add_fpar(doc, fpar(ftext(rf$frage, fp_label)))
|
||
doc = body_add_fpar(doc, fpar(ftext(rf$antwort, fp_normal)))
|
||
doc = body_add_par(doc, "", style = "Normal")
|
||
}
|
||
|
||
doc = body_add_fpar(doc, fpar(ftext(VDS23_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)))
|
||
}
|
||
})
|
||
|
||
ergebnis_r = eventReactive(input$btn_suchen, {
|
||
|
||
chiffre = toupper(trimws(input$chiffre))
|
||
|
||
if (nchar(trimws(input$pseudonym)) == 0 && nchar(chiffre) == 0) {
|
||
return(list(error = "Bitte Chiffre oder Pseudonym eingeben."))
|
||
}
|
||
if (nchar(trimws(input$pseudonym)) == 0 && !grepl("^[A-Z][0-9]{6}$", chiffre)) {
|
||
return(list(error = paste0(
|
||
"Ungültige Chiffre. Erwartet: ein Großbuchstabe + 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_treffer = pseudo[pseudo$pseudonym == trimws(input$pseudonym), ]
|
||
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_vds23", envir = .GlobalEnv)) {
|
||
return(list(error = paste0(
|
||
"Objekt 'daten_vds23' nach dem Sourcen nicht gefunden. Bitte Download-Skript prüfen.")))
|
||
}
|
||
if (!exists("pseudo", envir = .GlobalEnv)) {
|
||
return(list(error = paste0(
|
||
"Objekt 'pseudo' nach dem Sourcen nicht gefunden. Bitte Pseudonym-Skript prüfen.")))
|
||
}
|
||
|
||
daten = get("daten_vds23", envir = .GlobalEnv)
|
||
pseudo_df = get("pseudo", envir = .GlobalEnv)
|
||
|
||
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 VDS23-Datensatz für Chiffre '", chiffre, "' gefunden. ",
|
||
"(", length(alle_session_ids), " Pseudonym(e) geprüft)")))
|
||
}
|
||
|
||
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 Ausfüllungen gefunden (", n, " Einträge). ",
|
||
"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")
|
||
)
|
||
|
||
# --- 63 Item-Werte + Beispieltexte ---
|
||
werte_inhaltlich = integer(63)
|
||
werte_status = character(63)
|
||
beispiel_texte = character(63)
|
||
|
||
for (i in seq_len(63)) {
|
||
var = paste0("vds23_", sprintf("%02d", i))
|
||
var_beispiel = paste0(var, "_beispiel")
|
||
|
||
roh = if (var %in% names(zeile)) zeile[[var]][1] else NA
|
||
lbl_attr = if (var %in% names(daten)) attr(daten[[var]], "labels") else NULL
|
||
konv = vds23_wert_inhaltlich(roh, lbl_attr)
|
||
werte_inhaltlich[i] = konv$wert
|
||
werte_status[i] = konv$status
|
||
|
||
beispiel_roh = if (var_beispiel %in% names(zeile)) zeile[[var_beispiel]][1] else NA
|
||
beispiel_texte[i] = if (!is.na(beispiel_roh) &&
|
||
trimws(as.character(beispiel_roh)) != "") {
|
||
trimws(as.character(beispiel_roh))
|
||
} else {
|
||
NA_character_
|
||
}
|
||
}
|
||
|
||
# Debug-Ausgabe je Auswertung, damit die Umrechnung beim ersten echten
|
||
# Testlauf mit formr-Daten gegengeprueft werden kann (Item 1 als Beispiel).
|
||
message(
|
||
"VDS23-DEBUG: Rohwert vds23_01 = '",
|
||
if ("vds23_01" %in% names(zeile)) as.character(zeile[["vds23_01"]][1]) else "NA",
|
||
"' -> inhaltlicher Wert = ", werte_inhaltlich[1],
|
||
" (Status: ", werte_status[1], ")"
|
||
)
|
||
|
||
kodierwarnung = NULL
|
||
n_unbekannt = sum(werte_status == "unbekannt")
|
||
if (n_unbekannt > 0) {
|
||
kodierwarnung = paste0(
|
||
"Warnung: Die Kodierung konnte für ", n_unbekannt,
|
||
" Item(s) nicht eindeutig bestimmt werden. Die betroffenen Items ",
|
||
"werden als 'k. A.' angezeigt statt eines möglicherweise falschen Werts."
|
||
)
|
||
}
|
||
|
||
faktor_ergebnisse = lapply(names(vds23_faktoren), function(fid) {
|
||
fak = vds23_faktoren[[fid]]
|
||
fs = vds23_faktor_score(werte_inhaltlich, fak$items)
|
||
items_df = data.frame(
|
||
nummer = fak$items,
|
||
text = unname(vds23_itemtexte[sprintf("%02d", fak$items)]),
|
||
wert = werte_inhaltlich[fak$items],
|
||
beispiel = beispiel_texte[fak$items],
|
||
stringsAsFactors = FALSE
|
||
)
|
||
list(id = fid, name = fak$name, score = fs$score,
|
||
n_vorhanden = fs$n_vorhanden, n_gesamt = fs$n_gesamt, items = items_df)
|
||
})
|
||
|
||
ohne_faktor = data.frame(
|
||
nummer = VDS23_OHNE_FAKTOR,
|
||
text = unname(vds23_itemtexte[sprintf("%02d", VDS23_OHNE_FAKTOR)]),
|
||
wert = werte_inhaltlich[VDS23_OHNE_FAKTOR],
|
||
beispiel = beispiel_texte[VDS23_OHNE_FAKTOR],
|
||
stringsAsFactors = FALSE
|
||
)
|
||
|
||
top5_roh = if ("vds23_top5_auswahl" %in% names(zeile)) zeile[["vds23_top5_auswahl"]][1] else NA
|
||
top5 = vds23_parse_top5(top5_roh, vds23_itemtexte)
|
||
|
||
reflexion_felder = list(
|
||
list(spalte = "vds23_reflexion_bezugspersonen_heute",
|
||
frage = "Heute: häufigste Verhaltensweisen der Bezugspersonen"),
|
||
list(spalte = "vds23_reflexion_eigene_reaktion",
|
||
frage = "Eigene Reaktion darauf"),
|
||
list(spalte = "vds23_reflexion_situationsausgang",
|
||
frage = "Ausgang der Situationen"),
|
||
list(spalte = "vds23_reflexion_mutter_kindheit",
|
||
frage = "Kindheit, Mutter"),
|
||
list(spalte = "vds23_reflexion_vater_kindheit",
|
||
frage = "Kindheit, Vater")
|
||
)
|
||
reflexionen = lapply(reflexion_felder, function(f) {
|
||
roh = if (f$spalte %in% names(zeile)) zeile[[f$spalte]][1] else NA
|
||
antwort = if (!is.na(roh) && trimws(as.character(roh)) != "") {
|
||
trimws(as.character(roh))
|
||
} else {
|
||
NA_character_
|
||
}
|
||
list(frage = f$frage, antwort = antwort)
|
||
})
|
||
|
||
list(
|
||
chiffre = chiffre,
|
||
ausfuelldatum = datum_str,
|
||
info_mehrere = info_mehrere,
|
||
kodierwarnung = kodierwarnung,
|
||
faktor_ergebnisse = faktor_ergebnisse,
|
||
ohne_faktor = ohne_faktor,
|
||
top5 = top5,
|
||
reflexionen = reflexionen,
|
||
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)) return(NULL)
|
||
tagList(
|
||
if (!is.null(d$info_mehrere)) div(class = "alert-warnung", d$info_mehrere),
|
||
if (!is.null(d$kodierwarnung)) div(class = "alert-warnung", d$kodierwarnung)
|
||
)
|
||
})
|
||
|
||
output$ergebnis_ui = renderUI({
|
||
req(input$btn_suchen)
|
||
d = ergebnis_r()
|
||
if (!is.null(d$error)) return(NULL)
|
||
|
||
faktor_karten = lapply(seq_along(d$faktor_ergebnisse), function(idx) {
|
||
fe = d$faktor_ergebnisse[[idx]]
|
||
|
||
items_ui = lapply(seq_len(nrow(fe$items)), function(r) {
|
||
zeile = fe$items[r, ]
|
||
div(class = "item-zeile",
|
||
div(class = "item-nr", paste0(zeile$nummer, ".")),
|
||
div(class = "item-text-block",
|
||
div(class = "item-text", zeile$text),
|
||
if (!is.na(zeile$beispiel))
|
||
div(class = "item-beispiel", paste0("Beispiel: ", zeile$beispiel))
|
||
),
|
||
span(class = paste0("stufe-badge ", vds23_badge_klasse(zeile$wert)),
|
||
vds23_badge_text(zeile$wert))
|
||
)
|
||
})
|
||
|
||
div(class = "abschnitt-karte",
|
||
div(class = "abschnitt-titel", paste0("Faktor ", fe$id, ": ", fe$name)),
|
||
div(class = "faktor-score-block",
|
||
div(class = "faktor-score-zahl",
|
||
if (is.na(fe$score)) "–" else format(round(fe$score, 1), nsmall = 1)),
|
||
div(class = "faktor-score-label", "Mittelwert auf der Skala 0 bis 5")
|
||
),
|
||
if (!is.na(fe$score) && fe$n_vorhanden < fe$n_gesamt)
|
||
div(class = "faktor-missing-hinweis",
|
||
paste0("Faktor-Score basiert auf ", fe$n_vorhanden, " von ", fe$n_gesamt, " Items.")),
|
||
plotOutput(paste0("gauge_", idx), height = "110px"),
|
||
tags$hr(),
|
||
div(items_ui)
|
||
)
|
||
})
|
||
|
||
ohne_faktor_karte = div(class = "abschnitt-karte",
|
||
div(class = "abschnitt-titel", "Weitere erhobene Situationen (keinem Faktor zugeordnet)"),
|
||
div(class = "ohne-faktor-hinweis",
|
||
"Diese Items fließen in keinen Faktor-Score ein."),
|
||
div(lapply(seq_len(nrow(d$ohne_faktor)), function(r) {
|
||
zeile = d$ohne_faktor[r, ]
|
||
div(class = "item-zeile",
|
||
div(class = "item-nr", paste0(zeile$nummer, ".")),
|
||
div(class = "item-text-block",
|
||
div(class = "item-text", zeile$text),
|
||
if (!is.na(zeile$beispiel))
|
||
div(class = "item-beispiel", paste0("Beispiel: ", zeile$beispiel))
|
||
),
|
||
span(class = paste0("stufe-badge ", vds23_badge_klasse(zeile$wert)),
|
||
vds23_badge_text(zeile$wert))
|
||
)
|
||
}))
|
||
)
|
||
|
||
top5_ui = if (nrow(d$top5) == 0) {
|
||
div(style = "color:#777; font-style:italic;", "Keine Angabe.")
|
||
} else {
|
||
tags$ul(class = "top5-liste",
|
||
lapply(seq_len(nrow(d$top5)), function(r) {
|
||
zeile = d$top5[r, ]
|
||
txt = if (!is.na(zeile$nummer)) paste0(zeile$nummer, ". ", zeile$text) else zeile$roh
|
||
tags$li(class = "top5-eintrag", txt)
|
||
})
|
||
)
|
||
}
|
||
|
||
reflexionen_vorhanden = Filter(function(rf) !is.na(rf$antwort), d$reflexionen)
|
||
reflexionen_ui = if (length(reflexionen_vorhanden) == 0) {
|
||
div(style = "color:#777; font-style:italic;", "Keine Angaben zu den Reflexionsfragen vorhanden.")
|
||
} else {
|
||
lapply(reflexionen_vorhanden, function(rf) {
|
||
div(class = "freitext-block",
|
||
div(class = "freitext-frage", rf$frage),
|
||
div(class = "freitext-antwort", rf$antwort)
|
||
)
|
||
})
|
||
}
|
||
|
||
qualitativ_karte = div(class = "abschnitt-karte",
|
||
div(class = "abschnitt-titel", "Top5-Auswahl und Reflexionsfragen"),
|
||
div(class = "qualitativ-hinweis",
|
||
"Qualitative Zusatzinformation, kein Zahlenwert."),
|
||
tags$h5("Ausgewählte unangenehmste Situationen"),
|
||
top5_ui,
|
||
tags$hr(),
|
||
reflexionen_ui
|
||
)
|
||
|
||
tagList(
|
||
div(class = "abschnitt-karte",
|
||
div(class = "meta-block",
|
||
tags$strong("Chiffre: "), d$chiffre,
|
||
tags$span(" | ", style = "color:#ccc;"),
|
||
tags$strong("Ausfülldatum: "), d$ausfuelldatum
|
||
)
|
||
),
|
||
faktor_karten,
|
||
ohne_faktor_karte,
|
||
qualitativ_karte,
|
||
div(class = "disclaimer-zeile", VDS23_DISCLAIMER)
|
||
)
|
||
})
|
||
|
||
lapply(1:6, function(idx) {
|
||
output[[paste0("gauge_", idx)]] = renderPlot({
|
||
req(input$btn_suchen)
|
||
d = ergebnis_r()
|
||
req(is.null(d$error))
|
||
fe = d$faktor_ergebnisse[[idx]]
|
||
if (is.na(fe$score)) return(NULL)
|
||
vds23_achse_plot(fe$score)
|
||
}, bg = "transparent")
|
||
})
|
||
|
||
output$download_word = downloadHandler(
|
||
filename = function() {
|
||
d = tryCatch(ergebnis_r(), error = function(e) NULL)
|
||
chiffre_esc = if (is.list(d) && is.null(d$error) && nchar(d$chiffre) > 0)
|
||
gsub("[^A-Za-z0-9_-]", "_", d$chiffre) else "export"
|
||
ausfuelldatum_fn = if (is.list(d) && is.null(d$error) && !is.null(d$ausfuelldatum)) {
|
||
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("VDS23_", chiffre_esc, "_", ausfuelldatum_fn, ".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 oder Pseudonym eingeben und 'Auswerten' klicken.",
|
||
style = "Normal")
|
||
print(doc, target = file)
|
||
return()
|
||
}
|
||
doc = tryCatch(
|
||
erstelle_vds23_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)
|