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

1104 lines
48 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 ####
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_vds28.R" # liefert: daten_vds28
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R" # liefert: pseudo
AKZENT_FARBE = "#8B2635"
VDS28_DISCLAIMER = paste0(
"Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ",
"keine klinische Diagnose. Die Interpretation obliegt der behandelnden Person."
)
VDS28_DISCLAIMER = gsub("fuer", "für", VDS28_DISCLAIMER, fixed = TRUE)
# Fallback-Anker, nur falls das labels-Attribut an einer Rating-Spalte fehlen
# sollte. Der Regelfall liest die Stufenbeschriftung immer aus den echten
# Daten (labels-Attribut), nie aus dieser Konstante (siehe Abschnitt 3 der
# Spezifikation).
VDS28_STUFEN_TEXTE_STANDARD = c(
"0 = nicht", "1 = leicht", "2 = mittel", "3 = sehr"
)
VDS28_STUFEN_TEXTE_DYSFUNKTIONAL = c(
"0 = keine Dysfunktionalität", "1 = leichte Dysfunktionalität",
"2 = mittlere Dysfunktionalität", "3 = große Dysfunktionalität"
)
# Wortlaut 1:1 aus choices/inline-choices der vds28.xlsx uebernommen. Dient
# nur als Fallback, falls das labels-Attribut der Spalte fehlen sollte - der
# Regelfall liest den Anker immer aus den echten Daten.
VDS28_STUFEN_TEXTE_PRIORITAET = c(
"0 = Therapieziel geringer Priorität (kann als Ziel aufgenommen werden)",
"1 = Therapieziel mittlerer Priorität (sollte als Ziel aufgenommen werden)",
"2 = Therapieziel hoher Priorität (muss eines von 5 Hauptzielen sein)",
"3 = Therapieziel höchster Priorität (muss 1. oder 2. Hauptziel werden)"
)
# Badge-Farben fuer die 4 Antwortstufen, identisch zur Referenzimplementierung
# pg13r/app.R (dort PG13R_BADGE_FARBEN, gruen -> dunkelrot), hier nur auf die
# ersten 4 der dortigen 5 Stufen verkuerzt. Zeigt ausschliesslich die
# Antwortintensitaet des einzelnen Items, keine klinische Einordnung des
# Gesamtinstruments (Abschnitt 9 - keine Ampel-Logik fuer Gruppen/Summenwerte).
VDS28_BADGE_FARBEN = c(
"0" = "#4CAF50",
"1" = "#F48FB1",
"2" = "#EF5350",
"3" = "#B71C1C"
)
VDS28_BADGE_TEXT_FARBEN = c(
"0" = "white",
"1" = "#333333",
"2" = "white",
"3" = "white"
)
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 ####
# Stufe (0-3) wird ausschliesslich ueber die Position des Rohwerts im
# sortierten labels-Attribut der ORIGINAL-Spalte (vor Subsetting) bestimmt,
# niemals aus einem hartkodierten Zahlenwert (Abschnitt 3 der Spezifikation) -
# die genaue Kodierung kann je nach formr-Setup abweichen.
vds28_get_stufe = function(original_col, wert) {
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_integer_)
lab = attr(original_col, "labels")
if (!is.null(lab) && length(lab) > 0) {
lab_sortiert = sort(as.vector(lab))
pos = which(lab_sortiert == as.numeric(wert[1]))
if (length(pos) > 0) return(as.integer(pos[1]) - 1L)
}
# Fallback bei fehlendem labels-Attribut: Rohwert direkt als Stufe annehmen.
as.integer(round(as.numeric(wert[1])))
}
# Anker-Klartext (z.B. "2 = mittel") direkt aus dem labels-Attribut, mit
# generischem Fallback-Vektor nur falls das Attribut fehlt.
vds28_get_anker = function(original_col, wert, fallback_texte = VDS28_STUFEN_TEXTE_STANDARD) {
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_character_)
lab = attr(original_col, "labels")
if (!is.null(lab) && length(lab) > 0) {
pos = which(as.vector(lab) == as.numeric(wert[1]))
if (length(pos) > 0) return(names(lab)[pos[1]])
}
stufe = vds28_get_stufe(original_col, wert)
if (!is.na(stufe) && stufe >= 0L && stufe <= 3L) return(fallback_texte[stufe + 1L])
NA_character_
}
# Freitext lesen, leere/NA-Werte einheitlich als NA_character_. Feldname darf
# fehlen (z.B. abweichender Spaltenname im echten Export) - dann NA statt Fehler.
vds28_text_feld = function(zeile, feldname) {
if (!(feldname %in% names(zeile))) return(NA_character_)
roh = zeile[[feldname]][1]
if (is.null(roh) || is.na(roh)) return(NA_character_)
txt = trimws(as.character(roh))
if (nchar(txt) == 0) NA_character_ else txt
}
# Teil 4 verlaesst sich NICHT auf formr-seitiges Piping (Abschnitt 3), sondern
# rekonstruiert die Frageformulierung selbst aus der vds28.xlsx (Sheet
# 'survey', Items vds28_t4_1..6): dort wird die volle Angst-Aussage per
# Inline-R-Code auf eine Kurzform ohne Nummer und ohne "Ich habe " gekuerzt
# (z.B. "101. Ich habe Angst, nicht mehr zu sein." -> "Angst, nicht mehr zu
# sein."). Diese Transformation wird hier 1:1 nachgebildet statt eine zweite,
# potenziell abweichende Kurztext-Tabelle zu pflegen.
vds28_kurz_angst = function(voller_text) {
if (is.null(voller_text) || length(voller_text) == 0 || is.na(voller_text[1])) return(NA_character_)
sub("^\\d+\\.\\s*Ich habe\\s*", "", trimws(voller_text[1]))
}
# Zwei referenzierte Texte zu "A bzw. B" verbinden (Teil-4-Items 2/3/5/6
# referenzieren je zwei Umgangs- bzw. Reaktions-Items). Fehlende Angaben
# werden transparent als "(keine Angabe)" markiert statt die Zeile verzerrt
# wirken zu lassen.
vds28_bzw_verbinden = function(a, b) {
paste0(if (is.na(a)) "(keine Angabe)" else a, " bzw. ", if (is.na(b)) "(keine Angabe)" else b)
}
# Defensive Aufloesung eines select_one-Wahlfelds (Abschnitt 3): der Rohwert
# kann der interne Choice-Name (z.B. "ang3") oder bereits der volle
# Label-Text sein - beides ist laut Spezifikation nicht verifiziert. Reihen-
# folge: 1) Treffer gegen die internen Namen von 'mapping', 2) Treffer gegen
# die Volltexte von 'mapping', 3) Rohwert unveraendert anzeigen.
vds28_wahl_aufloesen = function(zeile, feldname, mapping) {
if (!(feldname %in% names(zeile))) return(NA_character_)
spalte = zeile[[feldname]]
wert = spalte[1]
if (is.null(wert) || (length(wert) == 1 && is.na(wert))) return(NA_character_)
lab = attr(spalte, "labels")
kandidat = if (!is.null(lab)) {
tryCatch(as.character(haven::as_factor(spalte))[1],
error = function(e) trimws(as.character(unclass(wert))))
} else {
trimws(as.character(unclass(wert)))
}
kandidat = trimws(as.character(kandidat))
if (is.na(kandidat) || kandidat == "" || kandidat == "NA") return(NA_character_)
# 1) Treffer gegen interne Namensliste
if (kandidat %in% names(mapping)) return(unname(mapping[[kandidat]]))
# 2) Treffer gegen vollen Label-Text (fallunabhaengig)
treffer = which(tolower(trimws(mapping)) == tolower(kandidat))
if (length(treffer) > 0) return(unname(mapping[treffer[1]]))
# 3) Rohwert unveraendert anzeigen statt leerer/falscher Anzeige
kandidat
}
# Interne Codeliste zu einem Wahlfeld ermitteln (fuer die Gruppenzuordnung
# einer gewaehlten Hauptangst) - analog zu vds28_wahl_aufloesen, liefert aber
# den internen Code (z.B. "ang7") statt des Volltexts.
vds28_wahl_code = function(zeile, feldname, mapping) {
if (!(feldname %in% names(zeile))) return(NA_character_)
spalte = zeile[[feldname]]
wert = spalte[1]
if (is.null(wert) || (length(wert) == 1 && is.na(wert))) return(NA_character_)
lab = attr(spalte, "labels")
kandidat = if (!is.null(lab)) {
tryCatch(as.character(haven::as_factor(spalte))[1],
error = function(e) trimws(as.character(unclass(wert))))
} else {
trimws(as.character(unclass(wert)))
}
kandidat = trimws(as.character(kandidat))
if (is.na(kandidat) || kandidat == "" || kandidat == "NA") return(NA_character_)
if (kandidat %in% names(mapping)) return(kandidat)
treffer = which(tolower(trimws(mapping)) == tolower(kandidat))
if (length(treffer) > 0) return(names(mapping)[treffer[1]])
NA_character_
}
# Badge-Style aus den Konstanten VDS28_BADGE_FARBEN/-_TEXT_FARBEN (Praeambel,
# an pg13r/app.R angelehnt). Fehlende Stufe (NA) bekommt bewusst ein
# neutrales Grau statt einer der vier Farben.
vds28_badge_style = function(stufe) {
if (is.na(stufe) || stufe < 0 || stufe > 3) {
return("background-color:#E0E0E0; color:#555555;")
}
k = as.character(as.integer(stufe))
paste0("background-color:", VDS28_BADGE_FARBEN[[k]], "; color:", VDS28_BADGE_TEXT_FARBEN[[k]], ";")
}
# Profildiagramm der 7 Gruppenmittelwerte: flache Akzentfarbe, keine
# Farbzonen/Cutoff-Linie/Ampel, da kein Normwert dokumentiert ist (Abschnitt 5).
make_vds28_profil_plot = function(gruppen_ergebnisse) {
df = data.frame(
label = sapply(gruppen_ergebnisse, function(g) g$bezeichnung_kurz),
mittelwert = sapply(gruppen_ergebnisse, function(g) g$mittelwert),
stringsAsFactors = FALSE
)
df$label = factor(df$label, levels = rev(df$label))
df$balken = ifelse(is.na(df$mittelwert), 0, df$mittelwert)
ggplot(df, aes(x = label, y = balken)) +
geom_col(fill = AKZENT_FARBE, width = 0.65) +
geom_text(
data = subset(df, !is.na(mittelwert)),
aes(label = sprintf("%.2f", mittelwert)),
hjust = -0.2, size = 3.5, color = "#333333"
) +
geom_text(
data = subset(df, is.na(mittelwert)),
aes(y = 0.08, label = "keine Angabe"),
hjust = 0, size = 3.0, color = "#888888", fontface = "italic"
) +
coord_flip(clip = "off") +
scale_y_continuous(limits = c(0, 3.5), breaks = 0:3) +
theme_minimal(base_size = 12) +
theme(
axis.title = element_blank(),
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank(),
plot.margin = margin(t = 5, r = 34, b = 5, l = 5)
)
}
# Datenaufbereitung ####
# 4.1 Item-zu-Gruppe-Zuordnung Teil 1 (7 Gruppen, 24 Items). Reihenfolge
# entspricht exakt der Nummerierung 101-703 und damit auch ang1-ang24 (4.2).
VDS28_GRUPPEN_ITEMS = list(
list(code = 1, grp = "grp1", bezeichnung_kurz = "Vernichtungsangst",
items = c("vds28_101", "vds28_102", "vds28_103")),
list(code = 2, grp = "grp2", bezeichnung_kurz = "Trennungsangst",
items = c("vds28_201", "vds28_202", "vds28_203", "vds28_204")),
list(code = 3, grp = "grp3", bezeichnung_kurz = "Kontrolle über andere verlieren",
items = c("vds28_301", "vds28_302", "vds28_303")),
list(code = 4, grp = "grp4", bezeichnung_kurz = "Kontrolle über sich verlieren",
items = c("vds28_401", "vds28_402", "vds28_403", "vds28_404")),
list(code = 5, grp = "grp5", bezeichnung_kurz = "Verlust von Zuneigung/Liebe",
items = c("vds28_501", "vds28_502", "vds28_503", "vds28_504")),
list(code = 6, grp = "grp6", bezeichnung_kurz = "Gegenaggression",
items = c("vds28_601", "vds28_602", "vds28_603")),
list(code = 7, grp = "grp7", bezeichnung_kurz = "Hingabe",
items = c("vds28_701", "vds28_702", "vds28_703"))
)
.vds28_kontrollsumme_t1 = sum(sapply(VDS28_GRUPPEN_ITEMS, function(g) length(g$items)))
if (.vds28_kontrollsumme_t1 != 24) {
stop(
"VDS28: Kontrollsumme der Gruppen-Item-Zuordnung ist ", .vds28_kontrollsumme_t1,
", erwartet 24. Bitte 'VDS28_GRUPPEN_ITEMS' pruefen (Copy-Paste-Fehler?)."
)
}
# 4.2 alle24-Mapping: interner Name (ang1..ang24) -> voller Itemtext + Var + Gruppe
VDS28_ALLE24 = data.frame(
ang_code = paste0("ang", 1:24),
var = c(
"vds28_101", "vds28_102", "vds28_103",
"vds28_201", "vds28_202", "vds28_203", "vds28_204",
"vds28_301", "vds28_302", "vds28_303",
"vds28_401", "vds28_402", "vds28_403", "vds28_404",
"vds28_501", "vds28_502", "vds28_503", "vds28_504",
"vds28_601", "vds28_602", "vds28_603",
"vds28_701", "vds28_702", "vds28_703"
),
gruppe = c(
"grp1", "grp1", "grp1",
"grp2", "grp2", "grp2", "grp2",
"grp3", "grp3", "grp3",
"grp4", "grp4", "grp4", "grp4",
"grp5", "grp5", "grp5", "grp5",
"grp6", "grp6", "grp6",
"grp7", "grp7", "grp7"
),
text = c(
"101. Ich habe Angst, nicht mehr zu sein.",
"102. Ich habe Angst, vernichtet zu werden",
"103. Ich habe Angst, meine Existenz zu verlieren",
"201. Ich habe Angst, allein gelassen zu werden",
"202. Ich habe Angst vor Trennung",
"203. Ich habe Angst, meine Bezugsperson zu verlieren",
"204. Ich habe Angst, allein zu sein",
"301. Ich habe Angst, dass der andere sich in seinen Entscheidungen nicht mehr durch mich beeinflussen lässt",
"302. Ich habe Angst, nicht mehr in der Lage zu sein, auf den anderen einzuwirken, so dass er in meinem Sinne handelt",
"303. Ich habe Angst, dass ich eine Situation nicht mehr im Griff habe und andere über mich verfügen können",
"401. Ich habe Angst, die Kontrolle über mich zu verlieren",
"402. Ich habe Angst, dass ich so in Wut geraten könnte, dass ich das Schlimmste anrichte",
"403. Ich habe Angst, dass ich mich aus Triebhaftigkeit in beschämende Exzesse ergehen lassen könnte",
"404. Ich habe Angst, meine vernunftmäßige gedankliche Kontrolle über mich zu verlieren und verrückt zu werden",
"501. Ich habe Angst, dass der andere mir böse ist, Ärger oder Unmut empfindet",
"502. Ich habe Angst, nicht mehr gemocht, nicht mehr geliebt zu werden",
"503. Ich habe Angst, abgelehnt zu werden, nicht angenommen zu werden",
"504. Ich habe Angst, nicht dazu zu gehören, ausgeschlossen zu werden",
"601. Ich habe Angst, dass die Regeln zwischenmenschlichen Umgangs außer Kraft gesetzt werden",
"602. Ich habe Angst vor Gegenaggression, wenn ich angreife",
"603. Ich habe Angst vor Anarchie und Chaos",
"701. Ich habe in einer nahen Beziehung Angst, mich hinzugeben",
"702. Ich habe in einer nahen Beziehung Angst, mich z. B. durch zu intensive Gefühle zu verlieren",
"703. Ich habe Angst, in eine Beziehung emotional hineingezogen zu werden, so dass ich nicht mehr über mich verfügen kann"
),
stringsAsFactors = FALSE
)
VDS28_ALLE24_MAP = setNames(VDS28_ALLE24$text, VDS28_ALLE24$ang_code)
# 4.3 gruppen-Mapping: interner Name (grp1..grp7) -> voller Gruppenname
VDS28_GRUPPEN_MAP = c(
grp1 = "Gruppe 1 Angst vor Vernichtung, Existenzverlust",
grp2 = "Gruppe 2 Angst vor Trennung, Alleinsein, Verlassenwerden",
grp3 = "Gruppe 3 Angst, die Kontrolle über die anderen zu verlieren",
grp4 = "Gruppe 4 Angst, die Kontrolle über mich zu verlieren",
grp5 = "Gruppe 5 Angst vor Liebesverlust und vor Ablehnung",
grp6 = "Gruppe 6 Angst vor Gegenaggression",
grp7 = "Gruppe 7 Angst vor Hingabe"
)
# 4.4 teil2items-Mapping: Umgang mit der Angst
VDS28_TEIL2_MAP = c(
t2i1 = "Ich kann nichts gegen meine Angst tun, spüre sie lähmend",
t2i2 = "Ich rufe meine Bezugsperson/Eltern zu Hilfe und sage, dass ich Angst habe",
t2i3 = "Ich flüchte, lauf schnell zu meiner Bezugsperson/Eltern",
t2i4 = "Ich passe vorsorglich gut auf, dass keine Situation kommt, in der ich diese Angst habe",
t2i5 = "Ich sorge dafür, dass ich immer mit Menschen zusammen bin, so dass die Angst nicht kommt",
t2i6 = "Ich halte mich an Regeln und achte darauf, dass andere dies auch tun, damit nichts passiert, was mir Angst macht",
t2i7 = "Ich lenke mich ab, sage mir, dass keine Gefahr besteht",
t2i8 = "Ich lasse mir nichts anmerken, reagiere eher ärgerlich oder wie einer, der keine Angst hat"
)
VDS28_TEIL2_VARS = paste0("vds28_t2_", 1:8)
# 4.5 teil3items-Mapping: Reaktion der Bezugspersonen
VDS28_TEIL3_MAP = c(
t3i1 = "Er/sie merkt gar nicht, dass ich Angst habe",
t3i2 = "Er/sie sagt, es gäbe doch keinen Grund zur Angst",
t3i3 = "Er/sie sagt, ich soll mich nicht so anstellen, soll mich zusammenreißen",
t3i4 = "Er/sie zeigt Verständnis für meine Angst",
t3i5 = "Er/sie gibt mir Schutz bzw. Sicherheit, beruhigt mich",
t3i6 = "Er/sie bekommt auch Angst",
t3i7 = "Er/sie sagt Dinge, die mir noch mehr Angst machen (was alles passieren könnte)",
t3i8 = "Er/sie wird ärgerlich oder wütend"
)
VDS28_TEIL3_VARS = paste0("vds28_t3_", 1:8)
VDS28_TEIL4_VARS = paste0("vds28_t4_", 1:6)
VDS28_TEIL4_VAR7 = "vds28_t4_7"
# Meta-Feldnamen Teil 1 (Hauptangst-Auswahl + Gruppen-Ranking, je mit Freitext)
VDS28_FELD_HAUPTANGST1_WAHL = "vds28_801_wahl"
VDS28_FELD_HAUPTANGST1_TEXT = "vds28_801_text"
VDS28_FELD_HAUPTANGST2_WAHL = "vds28_8011_wahl"
VDS28_FELD_HAUPTANGST2_TEXT = "vds28_8011_text"
VDS28_FELD_GRUPPE1_WAHL = "vds28_802_wahl"
VDS28_FELD_GRUPPE1_TEXT = "vds28_802_text"
VDS28_FELD_GRUPPE2_WAHL = "vds28_803_wahl"
VDS28_FELD_GRUPPE2_TEXT = "vds28_803_text"
# Ranking-Feldnamen Teil 2 / Teil 3 (laut Spezifikation ohne eigenes Freitextfeld)
VDS28_FELD_T2_RANG1_WAHL = "vds28_t2_09_wahl"
VDS28_FELD_T2_RANG2_WAHL = "vds28_t2_10_wahl"
VDS28_FELD_T3_RANG1_WAHL = "vds28_t3_09_wahl"
VDS28_FELD_T3_RANG2_WAHL = "vds28_t3_10_wahl"
# Feldnamen anhand der echten vds28.xlsx (Sheet 'survey') verifiziert:
# vds28_801_text / vds28_8011_text / vds28_802_text / vds28_803_text sowie
# vds28_t4_1_text .. vds28_t4_6_text existieren dort exakt so. Die Ranking-
# Felder aus Teil 2/3 (vds28_t2_09/10_wahl, vds28_t3_09/10_wahl) haben laut
# xlsx tatsaechlich kein eigenes Freitextfeld.
VDS28_FELD_T4_TEXT = paste0(VDS28_TEIL4_VARS, "_text")
# 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; }
.gesamt-zeile { display: flex; align-items: center; gap: 18px; margin-top: 10px; }
.score-zahl { font-size: 2.0rem; font-weight: 800; color: #8B2635; }
.score-label { color: #555; font-size: 0.95em; }
.meta-auswahl-zeile {
padding: 10px 0; border-bottom: 1px solid #F0F0F0;
}
.meta-auswahl-zeile:last-child { border-bottom: none; }
.meta-auswahl-label { font-weight: 600; color: #8B2635; font-size: 0.88em; margin-bottom: 3px; }
.meta-auswahl-text { color: #333; font-size: 0.95em; }
.meta-auswahl-gruppe { color: #777; font-size: 0.85em; font-style: italic; margin-top: 2px; }
.meta-auswahl-freitext {
margin-top: 5px; padding: 8px 10px; background: #FAFAFA; border-left: 3px solid #8B2635;
color: #444; font-size: 0.88em; border-radius: 0 4px 4px 0;
}
.item-zeile {
display: flex; align-items: flex-start; gap: 10px;
padding: 7px 0; border-bottom: 1px solid #F0F0F0;
flex-wrap: wrap;
}
.item-zeile:last-child { border-bottom: none; }
.item-nr { font-weight: 600; color: #8B2635; min-width: 26px; flex-shrink: 0; }
.item-text { flex: 1; color: #333; font-size: 0.92em; min-width: 200px; }
.stufe-badge {
border-radius: 4px; padding: 2px 9px; font-weight: 700;
font-size: 0.82em; white-space: nowrap; display: inline-block; flex-shrink: 0;
}
.ranking-badge {
border-radius: 4px; padding: 2px 9px; font-weight: 700;
font-size: 0.78em; white-space: nowrap; display: inline-block; flex-shrink: 0;
background: #333; color: white;
}
.item-freitext-block {
flex-basis: 100%; margin-top: 6px; padding: 8px 10px; background: #FAFAFA;
border-left: 3px solid #8B2635; color: #444; font-size: 0.88em; border-radius: 0 4px 4px 0;
}
.item-freitext-inline { color: #8B2635; font-weight: 600; }
.item-referenz-zeile {
flex-basis: 100%; margin-top: 4px; color: #777; font-size: 0.85em; font-style: italic;
}
.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("VDS28 Meine zentrale Angst / Grundformen der Angst"),
tags$p("Verhaltensdiagnostiksystem nach Serge Sulz 4 Teile, 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_vds28_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_freitext = fp_text(font.size = 10, italic = TRUE, color = "#555555")
fp_disclaimer = fp_text(font.size = 9, italic = TRUE, color = "#777777")
# 1. Titel + Metadaten
doc = body_add_fpar(doc, fpar(ftext("VDS28 Meine zentrale Angst", 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$mehrfach_warnung)) {
doc = body_add_fpar(doc, fpar(
ftext(erg$mehrfach_warnung, fp_text(font.size = 10, italic = TRUE, color = "#555555"))
))
}
doc = body_add_par(doc, "", style = "Normal")
# 2. Teil 1: Gruppen-Mittelwerte + Gesamtprozentwert
doc = body_add_fpar(doc, fpar(ftext("Teil 1 Profil der 7 Angstgruppen", fp_abschnitt)))
profil_df = data.frame(
Gruppe = sapply(erg$teil1$gruppen, function(g) paste0(g$code, ". ", g$bezeichnung_kurz)),
"Mittelwert (03)" = sapply(erg$teil1$gruppen, function(g)
if (is.na(g$mittelwert)) "keine Angabe" else sprintf("%.2f", g$mittelwert)),
check.names = FALSE, stringsAsFactors = FALSE
)
doc = body_add_table(doc, profil_df)
doc = body_add_fpar(doc, fpar(
ftext("Angst insgesamt: ", fp_label),
ftext(if (is.na(erg$teil1$gesamt_prozent)) "keine Angabe" else paste0(round(erg$teil1$gesamt_prozent), " %"),
fp_text(bold = TRUE, font.size = 12, color = AKZENT_FARBE))
))
doc = body_add_par(doc, "", style = "Normal")
# 3. Teil 1 Meta: Hauptangst-Auswahlen + Gruppen-Rankings
doc = body_add_fpar(doc, fpar(ftext("Hauptangst und Gruppen-Ranking", fp_abschnitt)))
meta_zeilen = list(
list(label = "1. Hauptangst:", text = erg$teil1$hauptangst1_text, gruppe = erg$teil1$hauptangst1_gruppe, freitext = erg$teil1$hauptangst1_freitext),
list(label = "2. Hauptangst:", text = erg$teil1$hauptangst2_text, gruppe = erg$teil1$hauptangst2_gruppe, freitext = erg$teil1$hauptangst2_freitext),
list(label = "1. Wichtigste Gruppe:", text = erg$teil1$gruppe1_text, gruppe = NA_character_, freitext = erg$teil1$gruppe1_freitext),
list(label = "2. Wichtigste Gruppe:", text = erg$teil1$gruppe2_text, gruppe = NA_character_, freitext = erg$teil1$gruppe2_freitext)
)
for (m in meta_zeilen) {
doc = body_add_fpar(doc, fpar(
ftext(m$label, fp_label), ftext(" ", fp_normal),
ftext(if (is.na(m$text)) "keine Angabe" else m$text, fp_normal)
))
if (!is.na(m$gruppe)) {
doc = body_add_fpar(doc, fpar(ftext(paste0(" gehört zu: ", m$gruppe), fp_freitext)))
}
if (!is.na(m$freitext)) {
doc = body_add_fpar(doc, fpar(ftext(paste0(" Erläuterung: ", m$freitext), fp_freitext)))
}
}
doc = body_add_par(doc, "", style = "Normal")
# 4. Teil 2
doc = body_add_fpar(doc, fpar(ftext("Teil 2 So ging ich bisher mit meiner Angst um", fp_abschnitt)))
for (r in seq_len(nrow(erg$teil2$item_tabelle))) {
z = erg$teil2$item_tabelle[r, ]
rang_txt = if (z$rang1) " [typischster Umgang]" else if (z$rang2) " [zweittypischster Umgang]" else ""
doc = body_add_fpar(doc, fpar(
ftext(paste0(r, ". ", z$text, rang_txt, " — "), fp_normal),
ftext(if (is.na(z$anker)) "keine Angabe" else z$anker, fp_text(bold = TRUE, font.size = 10, color = AKZENT_FARBE))
))
}
doc = body_add_par(doc, "", style = "Normal")
# 5. Teil 3
doc = body_add_fpar(doc, fpar(ftext("Teil 3 So reagierten wichtige Bezugspersonen", fp_abschnitt)))
for (r in seq_len(nrow(erg$teil3$item_tabelle))) {
z = erg$teil3$item_tabelle[r, ]
rang_txt = if (z$rang1) " [typischste Reaktion]" else if (z$rang2) " [zweittypischste Reaktion]" else ""
doc = body_add_fpar(doc, fpar(
ftext(paste0(r, ". ", z$text, rang_txt, " — "), fp_normal),
ftext(if (is.na(z$anker)) "keine Angabe" else z$anker, fp_text(bold = TRUE, font.size = 10, color = AKZENT_FARBE))
))
}
doc = body_add_par(doc, "", style = "Normal")
# 6. Teil 4 Kernfrage aus den echten Piping-Vorlagen der xlsx (Abschnitt 3),
# referenzierte Hauptangst fett hervorgehoben, Umgang/Reaktion darunter.
doc = body_add_fpar(doc, fpar(ftext("Teil 4 Angst als Therapieziel?", fp_abschnitt)))
fp_kern = fp_text(bold = TRUE, font.size = 11, color = AKZENT_FARBE)
for (r in seq_len(nrow(erg$teil4$item_tabelle))) {
z = erg$teil4$item_tabelle[r, ]
doc = body_add_fpar(doc, fpar(
ftext(paste0(r, ". ", z$praefix), fp_normal),
ftext(z$kern, fp_kern),
ftext(paste0(z$suffix, " — "), fp_normal),
ftext(if (is.na(z$anker)) "keine Angabe" else z$anker, fp_text(bold = TRUE, font.size = 10, color = AKZENT_FARBE))
))
if (!is.na(z$referenz)) {
doc = body_add_fpar(doc, fpar(ftext(paste0(" ", z$referenz), fp_freitext)))
}
if (!is.na(z$freitext)) {
doc = body_add_fpar(doc, fpar(ftext(paste0(" Wenn ja, inwiefern? ", z$freitext), fp_freitext)))
}
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(
ftext(erg$teil4$item7_praefix, fp_normal),
ftext(erg$teil4$item7_betont, fp_text(bold = TRUE, font.size = 11)),
ftext(erg$teil4$item7_suffix, fp_normal)
))
doc = body_add_fpar(doc, fpar(
ftext("Priorität: ", fp_label),
ftext(if (is.na(erg$teil4$item7_anker)) "keine Angabe" else erg$teil4$item7_anker,
fp_text(bold = TRUE, font.size = 10, color = AKZENT_FARBE))
))
doc = body_add_par(doc, "", style = "Normal")
# 7. Disclaimer als letzter Absatz
doc = body_add_fpar(doc, fpar(ftext(VDS28_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 = 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_vds28", envir = .GlobalEnv) || !exists("pseudo", envir = .GlobalEnv)) {
return(list(typ = "daten_fehlen"))
}
daten = get("daten_vds28", envir = .GlobalEnv)
pseudo_df = get("pseudo", envir = .GlobalEnv)
if (!("session" %in% names(daten))) {
return(list(typ = "daten_fehlen",
meldung = paste0(
"Erwartete Spalte 'session' nicht in 'daten_vds28' gefunden. ",
"Bitte Session-ID-Spaltenname vor Produktiveinsatz pruefen."
)))
}
# Schritt 4: Chiffre-Rueckaufloesung, falls nur 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 = "chiffre_nicht_gefunden", 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_vds28 finden
treffer_daten = daten[daten$session %in% alle_session_ids, ]
if (nrow(treffer_daten) == 0) return(list(typ = "keine_daten", chiffre = chiffre))
mehrfach_warnung = NULL
if (nrow(treffer_daten) > 1) {
n = nrow(treffer_daten)
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 Ausfüllungen gefunden (", n, " Einträge) — es wird die neueste angezeigt.")
}
zeile = treffer_daten[1, , drop = FALSE]
ausfuelldatum = tryCatch({
if ("ausfuelldatum" %in% names(zeile) && !is.na(zeile[["ausfuelldatum"]][1]) &&
trimws(as.character(zeile[["ausfuelldatum"]][1])) != "") {
as.character(zeile[["ausfuelldatum"]][1])
} else if ("created" %in% names(zeile)) {
format(as.POSIXct(zeile[["created"]][1]), "%d.%m.%Y")
} else {
format(Sys.Date(), "%d.%m.%Y")
}
}, error = function(e) format(Sys.Date(), "%d.%m.%Y"))
# ---- Teil 1: Gruppen-Mittelwerte + Gesamtprozentwert (Abschnitt 5) ----
gruppen_ergebnisse = lapply(VDS28_GRUPPEN_ITEMS, function(g) {
werte = sapply(g$items, function(var) vds28_get_stufe(daten[[var]], zeile[[var]]))
mittelwert = if (any(is.na(werte))) NA_real_ else sum(werte) / length(g$items)
c(g, list(werte = werte, mittelwert = mittelwert))
})
alle_rohwerte = unlist(lapply(gruppen_ergebnisse, function(g) g$werte))
gesamt_prozent = if (any(is.na(alle_rohwerte))) NA_real_ else (sum(alle_rohwerte) / 72) * 100
# ---- Teil 1 Meta: Hauptangst-Auswahlen + Gruppen-Rankings ----
hauptangst1_text = vds28_wahl_aufloesen(zeile, VDS28_FELD_HAUPTANGST1_WAHL, VDS28_ALLE24_MAP)
hauptangst1_code = vds28_wahl_code(zeile, VDS28_FELD_HAUPTANGST1_WAHL, VDS28_ALLE24_MAP)
hauptangst1_gruppe = if (!is.na(hauptangst1_code)) {
g = VDS28_ALLE24$gruppe[VDS28_ALLE24$ang_code == hauptangst1_code]
if (length(g) > 0) VDS28_GRUPPEN_MAP[[g[1]]] else NA_character_
} else NA_character_
hauptangst2_text = vds28_wahl_aufloesen(zeile, VDS28_FELD_HAUPTANGST2_WAHL, VDS28_ALLE24_MAP)
hauptangst2_code = vds28_wahl_code(zeile, VDS28_FELD_HAUPTANGST2_WAHL, VDS28_ALLE24_MAP)
hauptangst2_gruppe = if (!is.na(hauptangst2_code)) {
g = VDS28_ALLE24$gruppe[VDS28_ALLE24$ang_code == hauptangst2_code]
if (length(g) > 0) VDS28_GRUPPEN_MAP[[g[1]]] else NA_character_
} else NA_character_
gruppe1_text = vds28_wahl_aufloesen(zeile, VDS28_FELD_GRUPPE1_WAHL, VDS28_GRUPPEN_MAP)
gruppe2_text = vds28_wahl_aufloesen(zeile, VDS28_FELD_GRUPPE2_WAHL, VDS28_GRUPPEN_MAP)
teil1 = list(
gruppen = gruppen_ergebnisse,
gesamt_prozent = gesamt_prozent,
hauptangst1_text = hauptangst1_text,
hauptangst1_gruppe = hauptangst1_gruppe,
hauptangst1_freitext = vds28_text_feld(zeile, VDS28_FELD_HAUPTANGST1_TEXT),
hauptangst2_text = hauptangst2_text,
hauptangst2_gruppe = hauptangst2_gruppe,
hauptangst2_freitext = vds28_text_feld(zeile, VDS28_FELD_HAUPTANGST2_TEXT),
gruppe1_text = gruppe1_text,
gruppe1_freitext = vds28_text_feld(zeile, VDS28_FELD_GRUPPE1_TEXT),
gruppe2_text = gruppe2_text,
gruppe2_freitext = vds28_text_feld(zeile, VDS28_FELD_GRUPPE2_TEXT)
)
# ---- Teil 2: Umgang mit der Angst (rein deskriptiv, kein Score) ----
t2_rang1_code = vds28_wahl_code(zeile, VDS28_FELD_T2_RANG1_WAHL, VDS28_TEIL2_MAP)
t2_rang2_code = vds28_wahl_code(zeile, VDS28_FELD_T2_RANG2_WAHL, VDS28_TEIL2_MAP)
t2_item_tabelle = do.call(rbind, lapply(seq_along(VDS28_TEIL2_VARS), function(i) {
var = VDS28_TEIL2_VARS[i]
code = paste0("t2i", i)
data.frame(
code = code,
text = unname(VDS28_TEIL2_MAP[[code]]),
stufe = vds28_get_stufe(daten[[var]], zeile[[var]]),
anker = vds28_get_anker(daten[[var]], zeile[[var]], VDS28_STUFEN_TEXTE_STANDARD),
rang1 = !is.na(t2_rang1_code) && code == t2_rang1_code,
rang2 = !is.na(t2_rang2_code) && code == t2_rang2_code,
stringsAsFactors = FALSE
)
}))
teil2 = list(item_tabelle = t2_item_tabelle)
# ---- Teil 3: Reaktion der Bezugspersonen (rein deskriptiv, kein Score) ----
t3_rang1_code = vds28_wahl_code(zeile, VDS28_FELD_T3_RANG1_WAHL, VDS28_TEIL3_MAP)
t3_rang2_code = vds28_wahl_code(zeile, VDS28_FELD_T3_RANG2_WAHL, VDS28_TEIL3_MAP)
t3_item_tabelle = do.call(rbind, lapply(seq_along(VDS28_TEIL3_VARS), function(i) {
var = VDS28_TEIL3_VARS[i]
code = paste0("t3i", i)
data.frame(
code = code,
text = unname(VDS28_TEIL3_MAP[[code]]),
stufe = vds28_get_stufe(daten[[var]], zeile[[var]]),
anker = vds28_get_anker(daten[[var]], zeile[[var]], VDS28_STUFEN_TEXTE_STANDARD),
rang1 = !is.na(t3_rang1_code) && code == t3_rang1_code,
rang2 = !is.na(t3_rang2_code) && code == t3_rang2_code,
stringsAsFactors = FALSE
)
}))
teil3 = list(item_tabelle = t3_item_tabelle)
# ---- Teil 4: Dysfunktionalität und Therapieziel (rein deskriptiv) ----
# Die 6 Kernfragen werden exakt nach den Piping-Vorlagen aus vds28.xlsx
# (Sheet 'survey', vds28_t4_1..6) nachgebaut: Items 1-3 beziehen sich auf
# die 1. Hauptangst (vds28_801_wahl), Items 4-6 auf die 2. Hauptangst
# (vds28_8011_wahl); Items 2/5 zusaetzlich auf die beiden Umgangs-Rankings
# (t2_09/10), Items 3/6 auf die beiden Reaktions-Rankings (t3_09/10).
hauptangst1_kurz = vds28_kurz_angst(hauptangst1_text)
hauptangst2_kurz = vds28_kurz_angst(hauptangst2_text)
t2_referenz = vds28_bzw_verbinden(
if (!is.na(t2_rang1_code)) unname(VDS28_TEIL2_MAP[[t2_rang1_code]]) else NA_character_,
if (!is.na(t2_rang2_code)) unname(VDS28_TEIL2_MAP[[t2_rang2_code]]) else NA_character_
)
t3_referenz = vds28_bzw_verbinden(
if (!is.na(t3_rang1_code)) unname(VDS28_TEIL3_MAP[[t3_rang1_code]]) else NA_character_,
if (!is.na(t3_rang2_code)) unname(VDS28_TEIL3_MAP[[t3_rang2_code]]) else NA_character_
)
t4_vorlage = list(
list(praefix = "Ist Ihre 1. Angst (", kern = hauptangst1_kurz,
suffix = ") dysfunktional?", referenz = NA_character_),
list(praefix = "Ist der Umgang mit Ihrer 1. Angst (", kern = hauptangst1_kurz,
suffix = ") dysfunktional?", referenz = t2_referenz),
list(praefix = "Ist die Reaktion anderer auf die Art des Umgangs mit Ihrer 1. Angst (", kern = hauptangst1_kurz,
suffix = ") dysfunktional?", referenz = t3_referenz),
list(praefix = "Ist Ihre 2. Angst (", kern = hauptangst2_kurz,
suffix = ") dysfunktional?", referenz = NA_character_),
list(praefix = "Ist der Umgang mit Ihrer 2. Angst (", kern = hauptangst2_kurz,
suffix = ") dysfunktional?", referenz = t2_referenz),
list(praefix = "Ist die Reaktion anderer auf die Art des Umgangs mit Ihrer 2. Angst (", kern = hauptangst2_kurz,
suffix = ") dysfunktional?", referenz = t3_referenz)
)
t4_item_tabelle = do.call(rbind, lapply(seq_along(VDS28_TEIL4_VARS), function(i) {
var = VDS28_TEIL4_VARS[i]
v = t4_vorlage[[i]]
data.frame(
praefix = v$praefix,
kern = if (is.na(v$kern)) "(keine Angabe)" else v$kern,
suffix = v$suffix,
referenz = v$referenz,
stufe = vds28_get_stufe(daten[[var]], zeile[[var]]),
anker = vds28_get_anker(daten[[var]], zeile[[var]], VDS28_STUFEN_TEXTE_DYSFUNKTIONAL),
freitext = vds28_text_feld(zeile, VDS28_FELD_T4_TEXT[i]),
stringsAsFactors = FALSE
)
}))
# Item 7 hat keine Piping-Referenz, nur eingebettetes Markdown-Fett
# ("**wichtiges Therapieziel**") - hier direkt als eigenes Textsegment
# gefuehrt statt als Markdown-String, damit UI/Word sauber fett rendern
# statt die Sternchen roh anzuzeigen.
item7_stufe = vds28_get_stufe(daten[[VDS28_TEIL4_VAR7]], zeile[[VDS28_TEIL4_VAR7]])
item7_anker = vds28_get_anker(daten[[VDS28_TEIL4_VAR7]], zeile[[VDS28_TEIL4_VAR7]], VDS28_STUFEN_TEXTE_PRIORITAET)
teil4 = list(
item_tabelle = t4_item_tabelle,
item7_praefix = "Sind die zentralen Ängste, der Umgang mit ihnen oder die Reaktionen anderer ein ",
item7_betont = "wichtiges Therapieziel",
item7_suffix = "?",
item7_stufe = item7_stufe,
item7_anker = item7_anker
)
list(
typ = "erfolg",
chiffre = chiffre,
ausfuelldatum = ausfuelldatum,
mehrfach_warnung = mehrfach_warnung,
teil1 = teil1,
teil2 = teil2,
teil3 = teil3,
teil4 = teil4
)
})
vds28_fehlermeldung = function(d) {
switch(d$typ,
"leere_eingabe" = d$meldung,
"format_fehler" = paste0("Ungültige Chiffre '", d$chiffre, "'. Erwartet: ein Großbuchstabe + 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 übergeordneten Verzeichnissen nicht gefunden werden.",
"daten_fehlen" = if (!is.null(d$meldung)) d$meldung else "Nach dem Sourcen der Skripte fehlen die erwarteten Objekte 'daten_vds28' oder 'pseudo'.",
"chiffre_nicht_gefunden" = paste0("Chiffre '", d$chiffre, "' wurde in der Pseudonym-Datenbank nicht gefunden."),
"keine_daten" = paste0("Kein VDS28-Datensatz für Chiffre '", d$chiffre, "' gefunden."),
"Unbekannter Fehler."
)
}
output$fehler_ui = renderUI({
req(input$btn_suchen)
d = ergebnis()
if (d$typ != "erfolg") div(class = "alert-fehler", vds28_fehlermeldung(d))
})
output$warnung_ui = renderUI({
req(input$btn_suchen)
d = ergebnis()
if (d$typ != "erfolg" || is.null(d$mehrfach_warnung)) return(NULL)
div(class = "alert-warnung", d$mehrfach_warnung)
})
vds28_item_zeile = function(nr, text, stufe, anker, rang_label = NULL) {
div(class = "item-zeile",
div(class = "item-nr", paste0(nr, ".")),
div(class = "item-text", text),
if (!is.null(rang_label)) span(class = "ranking-badge", rang_label),
span(class = "stufe-badge", style = vds28_badge_style(stufe),
if (is.na(anker)) "keine Angabe" else anker)
)
}
output$ergebnis_ui = renderUI({
req(input$btn_suchen)
d = ergebnis()
if (d$typ != "erfolg") return(NULL)
# 1. Teil 1 Profil
karte_profil = div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Teil 1 Profil der 7 Angstgruppen"),
div(class = "meta-block",
tags$strong("Chiffre: "), d$chiffre,
tags$span(" | ", style = "color:#ccc;"),
tags$strong("Ausfülldatum: "), d$ausfuelldatum
),
plotOutput("profil_plot", height = "340px"),
div(class = "gesamt-zeile",
div(class = "score-zahl",
if (is.na(d$teil1$gesamt_prozent)) "" else paste0(round(d$teil1$gesamt_prozent), " %")),
div(class = "score-label", "Angst insgesamt")
)
)
# 2. Teil 1 Hauptangst und Gruppen-Ranking (getrennt, nicht zusammengefasst)
meta_zeile = function(label, text, gruppe = NA_character_, freitext = NA_character_) {
div(class = "meta-auswahl-zeile",
div(class = "meta-auswahl-label", label),
div(class = "meta-auswahl-text", if (is.na(text)) "keine Angabe" else text),
if (!is.na(gruppe)) div(class = "meta-auswahl-gruppe", paste0("gehört zu: ", gruppe)),
if (!is.na(freitext)) div(class = "meta-auswahl-freitext", freitext)
)
}
karte_meta = div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Teil 1 Hauptangst und Gruppen-Ranking"),
tags$h5("Hauptangst"),
meta_zeile("1. Hauptangst", d$teil1$hauptangst1_text, d$teil1$hauptangst1_gruppe, d$teil1$hauptangst1_freitext),
meta_zeile("2. Hauptangst", d$teil1$hauptangst2_text, d$teil1$hauptangst2_gruppe, d$teil1$hauptangst2_freitext),
tags$hr(),
tags$h5("Wichtigste Angstgruppen"),
meta_zeile("1. wichtigste Gruppe", d$teil1$gruppe1_text, NA_character_, d$teil1$gruppe1_freitext),
meta_zeile("2. wichtigste Gruppe", d$teil1$gruppe2_text, NA_character_, d$teil1$gruppe2_freitext)
)
# 3. Teil 2
t2_items_ui = lapply(seq_len(nrow(d$teil2$item_tabelle)), function(r) {
z = d$teil2$item_tabelle[r, ]
rang_label = if (isTRUE(z$rang1)) "typischster Umgang" else if (isTRUE(z$rang2)) "zweittypischster Umgang" else NULL
vds28_item_zeile(r, z$text, z$stufe, z$anker, rang_label)
})
karte_teil2 = div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Teil 2 So ging ich bisher mit meiner Angst um"),
div(t2_items_ui)
)
# 4. Teil 3
t3_items_ui = lapply(seq_len(nrow(d$teil3$item_tabelle)), function(r) {
z = d$teil3$item_tabelle[r, ]
rang_label = if (isTRUE(z$rang1)) "typischste Reaktion" else if (isTRUE(z$rang2)) "zweittypischste Reaktion" else NULL
vds28_item_zeile(r, z$text, z$stufe, z$anker, rang_label)
})
karte_teil3 = div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Teil 3 So reagierten wichtige Bezugspersonen"),
div(t3_items_ui)
)
# 5. Teil 4 jede Kernfrage traegt ihren rekonstruierten Referenztext
# (Hauptangst fett hervorgehoben, Umgang/Reaktion darunter) direkt bei
# sich, unabhaengig vom formr-Piping (Abschnitt 3).
t4_items_ui = lapply(seq_len(nrow(d$teil4$item_tabelle)), function(r) {
z = d$teil4$item_tabelle[r, ]
div(class = "item-zeile",
div(class = "item-nr", paste0(r, ".")),
div(class = "item-text",
z$praefix, tags$strong(class = "item-freitext-inline", z$kern), z$suffix,
if (!is.na(z$referenz)) div(class = "item-referenz-zeile", z$referenz)
),
span(class = "stufe-badge", style = vds28_badge_style(z$stufe),
if (is.na(z$anker)) "keine Angabe" else z$anker),
if (!is.na(z$freitext)) div(class = "item-freitext-block",
tags$strong("Wenn ja, inwiefern? "), z$freitext)
)
})
karte_teil4 = div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Teil 4 Angst als Therapieziel?"),
div(t4_items_ui),
tags$hr(),
div(class = "item-zeile",
div(class = "item-nr", "7."),
div(class = "item-text",
d$teil4$item7_praefix, tags$strong(d$teil4$item7_betont), d$teil4$item7_suffix),
span(class = "stufe-badge", style = vds28_badge_style(d$teil4$item7_stufe),
if (is.na(d$teil4$item7_anker)) "keine Angabe" else d$teil4$item7_anker)
)
)
tagList(
karte_profil,
karte_meta,
karte_teil2,
karte_teil3,
karte_teil4,
div(class = "disclaimer-zeile", VDS28_DISCLAIMER)
)
})
output$profil_plot = renderPlot({
req(input$btn_suchen)
d = ergebnis()
req(d$typ == "erfolg")
make_vds28_profil_plot(d$teil1$gruppen)
}, 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("VDS28_", 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_vds28_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)