# Präambel #### AKZENT_FARBE = "#8B2635" PFAD_DOWNLOAD_SKRIPT = "../API/get_data_vds90.R" PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R" VDS90_DISCLAIMER = paste0( "Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ", "keine klinische Diagnose. Die Interpretation der Ergebnisse obliegt der ", "behandelnden Person." ) library(shiny) library(dplyr) library(ggplot2) library(haven) library(officer) library(DBI) library(RSQLite) # 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 #### # Mapping ueber Position: kleinster Code = Stufe 0, naechster = Stufe 1 usw. # Unabhaengig vom Labeltext – funktioniert mit "nicht/leicht/mittel/stark" und # mit numerisch-kodierten Formaten gleichermassen. stufe_aus_labels_fuer_wert = function(spalte_orig, rohwert) { labels_attr = attr(spalte_orig, "labels") if (is.null(labels_attr) || is.na(rohwert)) return(NA_real_) codes_sortiert = sort(as.integer(labels_attr)) stufe = match(as.integer(rohwert), codes_sortiert) - 1L if (is.na(stufe)) return(NA_real_) as.numeric(stufe) } perzentil_nachschlagen = function(skalenwert_val, normtabelle, geschlecht) { tab = if (geschlecht == "weiblich") normtabelle$frauen else normtabelle$maenner if (is.na(skalenwert_val)) return(NA_real_) if (skalenwert_val <= min(tab$skalenwert)) return(tab$perzentil[1]) if (skalenwert_val >= max(tab$skalenwert)) return(100) approx(tab$skalenwert, tab$perzentil, xout = skalenwert_val)$y } perzentil_nachschlagen_ge = function(skalenwert_val, geschlecht) { tab = if (geschlecht == "weiblich") vds90_ge_normtabelle_frauen else vds90_ge_normtabelle_maenner if (is.na(skalenwert_val)) return(NA_real_) if (skalenwert_val <= min(tab$skalenwert)) return(tab$perzentil[1]) if (skalenwert_val >= max(tab$skalenwert)) return(100) approx(tab$skalenwert, tab$perzentil, xout = skalenwert_val)$y } berechne_prozentrang = function(summe_itemwerte, skalenwert_val, kuerzel, geschlecht) { # Summe = 0 -> Prozentrang = 0, unabhaengig vom Normtabellenwert. if (!is.na(summe_itemwerte) && summe_itemwerte == 0) return(0) if (kuerzel == "GE") { perzentil_nachschlagen_ge(skalenwert_val, geschlecht) } else { perzentil_nachschlagen(skalenwert_val, vds90_normtabellen[[kuerzel]], geschlecht) } } gauge_ggplot = function(skalenwert_val, prozentrang_val) { if (is.na(skalenwert_val)) skalenwert_val = 0 if (is.na(prozentrang_val)) prozentrang_val = 0 farb_grad = colorRampPalette(c("#28a745", "#ffc107", "#dc3545"))(101) idx = max(1, min(101, round(skalenwert_val / 3 * 100) + 1)) fill_col = farb_grad[idx] pr_label = sprintf("Mittelwert: %.3f | Prozentrang: %.0f%%", skalenwert_val, prozentrang_val) ggplot() + geom_rect(aes(xmin = 0, xmax = 3, ymin = 0.2, ymax = 0.8), fill = "#e9ecef") + geom_rect(aes(xmin = 0, xmax = skalenwert_val, ymin = 0.2, ymax = 0.8), fill = fill_col) + annotate("text", x = 3.08, y = 0.5, label = pr_label, hjust = 0, vjust = 0.5, size = 3.3, color = "#333") + scale_x_continuous(limits = c(0, 6.5), breaks = 0:3, labels = c("0\nnicht", "1\nleicht", "2\nmittel", "3\nstark")) + scale_y_continuous(limits = c(0, 1)) + theme_void() + theme(axis.text.x = element_text(size = 8, color = "#555", vjust = 1, margin = margin(t = 2)), axis.ticks.x = element_line(color = "#999"), axis.ticks.length = unit(3, "pt"), plot.margin = margin(4, 4, 8, 4)) } make_uebersicht_df = function(subsk_erg, geschlecht) { korder = vds90_kuerzel_reihenfolge nm_col = if (geschlecht == "weiblich") "mittelwert_frauen" else "mittelwert_maenner" norm_mw = setNames(vds90_norm_mittelwerte[[nm_col]], vds90_norm_mittelwerte$abkuerzung) data.frame( kuerzel = factor(korder, levels = korder), mittelwert = sapply(korder, function(k) subsk_erg[[k]]$mittelwert), prozentrang = sapply(korder, function(k) subsk_erg[[k]]$prozentrang), norm_mw = norm_mw[korder], stringsAsFactors = FALSE ) } plot_schweregrad = function(df) { ggplot(df, aes(x = kuerzel, y = mittelwert)) + geom_col(fill = AKZENT_FARBE, width = 0.65) + scale_y_continuous(limits = c(0, 3), breaks = 0:3, expand = expansion(mult = c(0, 0.05))) + labs(title = "Schweregrad (Skalenmittelwert)", x = NULL, y = "Mittelwert (0-3)") + theme_minimal(base_size = 11) + theme(axis.text.x = element_text(angle = 45, hjust = 1), panel.grid.major.x = element_blank(), plot.title = element_text(color = AKZENT_FARBE, face = "bold", size = 12)) } plot_prozentrang = function(df) { ggplot(df, aes(x = kuerzel, y = prozentrang)) + geom_col(fill = AKZENT_FARBE, width = 0.65) + scale_y_continuous(limits = c(0, 100), breaks = seq(0, 100, 25), expand = expansion(mult = c(0, 0.02))) + labs(title = "Prozentränge", x = NULL, y = "Prozentrang") + theme_minimal(base_size = 11) + theme(axis.text.x = element_text(angle = 45, hjust = 1), panel.grid.major.x = element_blank(), plot.title = element_text(color = AKZENT_FARBE, face = "bold", size = 12)) } plot_abweichung = function(df) { df$abweichung = df$mittelwert - df$norm_mw df$farbe = ifelse(df$abweichung >= 0, "#c0392b", "#27ae60") ggplot(df, aes(x = kuerzel, y = abweichung, fill = farbe)) + geom_col(width = 0.65) + geom_hline(yintercept = 0, color = "#333", linewidth = 0.6) + scale_fill_identity() + labs(title = "Abweichung Patient von Normstichprobe", x = NULL, y = "Patient - Norm (Mittelwert)") + theme_minimal(base_size = 11) + theme(axis.text.x = element_text(angle = 45, hjust = 1), panel.grid.major.x = element_blank(), plot.title = element_text(color = AKZENT_FARBE, face = "bold", size = 12)) } plot_vergleich = function(df) { df_long = data.frame( kuerzel = rep(df$kuerzel, 2), wert = c(df$mittelwert, df$norm_mw), gruppe = factor(rep(c("Patient", "Normstichprobe"), each = nrow(df)), levels = c("Patient", "Normstichprobe")) ) ggplot(df_long, aes(x = kuerzel, y = wert, color = gruppe, group = gruppe)) + geom_line(linewidth = 0.9) + geom_point(size = 2) + scale_color_manual(values = c("Patient" = AKZENT_FARBE, "Normstichprobe" = "#2980b9")) + labs(title = "Patient vs. Normstichprobe (Skalenmittelwert)", x = NULL, y = "Mittelwert", color = NULL) + theme_minimal(base_size = 11) + theme(axis.text.x = element_text(angle = 45, hjust = 1), panel.grid.major.x = element_blank(), legend.position = "bottom", plot.title = element_text(color = AKZENT_FARBE, face = "bold", size = 12)) } # Datenaufbereitung #### source("vds90_normtabellen.R") # UI #### app_css = " body { font-family: 'Segoe UI', Arial, sans-serif; background: #f5f5f5; } .container-fluid { max-width: 1100px; } .app-header { background: #8B2635; color: white; padding: 15px 22px 13px; margin-bottom: 18px; border-radius: 5px; } .app-header h2 { margin: 0; font-size: 1.4em; font-weight: 700; } .app-header p { margin: 4px 0 0; font-size: 0.87em; opacity: 0.88; } .input-panel { display: flex; align-items: flex-end; gap: 10px; background: white; border-radius: 6px; padding: 14px 18px; margin-bottom: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.09); flex-wrap: wrap; } .input-panel .form-group { margin-bottom: 0; } .btn-laden { background: #8B2635 !important; border-color: #8B2635 !important; color: white !important; font-weight: 600; padding: 6px 18px; border-radius: 4px; white-space: nowrap; } .btn-laden:hover, .btn-laden:focus { background: #6e1e2a !important; border-color: #6e1e2a !important; color: white !important; } .alert-warnung { background: #fff3cd; border: 1px solid #ffc107; padding: 10px 14px; border-radius: 6px; margin-bottom: 10px; color: #856404; } .alert-fehler { background: #f8d7da; border: 1px solid #f5c2c7; padding: 10px 14px; border-radius: 6px; color: #842029; } .abschnitt-karte { background: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 14px 18px; margin-bottom: 14px; } .abschnitt-titel { color: #8B2635; font-weight: 700; font-size: 1.05em; margin-bottom: 8px; } .abschnitt-titel .kuerzel { background: #8B2635; color: white; padding: 1px 7px; border-radius: 4px; font-size: 0.82em; margin-right: 6px; } .stufe-badge { display: inline-block; padding: 1px 9px; border-radius: 10px; font-weight: 600; font-size: 0.82em; margin-left: 6px; vertical-align: middle; white-space: nowrap; } .stufe-badge-0 { background: #28a745; color: white; } .stufe-badge-1 { background: #ffb3b3; color: #333; } .stufe-badge-2 { background: #dc3545; color: white; } .stufe-badge-3 { background: #7b0000; color: white; } .item-zeile { margin-bottom: 5px; padding: 6px 8px; background: #fafafa; border-radius: 4px; border: 1px solid #eee; font-size: 0.88em; display: flex; align-items: flex-start; gap: 8px; } .item-nr { font-weight: 700; color: #555; min-width: 62px; flex-shrink: 0; } .item-text { flex: 1; line-height: 1.4; } .items-container { margin-top: 8px; } summary { cursor: pointer; font-size: 0.9em; color: #666; padding: 4px 0; } summary:hover { color: #8B2635; } .uebersicht-box { background: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 14px 18px; margin-bottom: 22px; } .kopf-info { color: #555; font-size: 0.92em; padding-bottom: 10px; border-bottom: 1px solid #eee; margin-bottom: 8px; } .kopf-info b { color: #333; } .suizid-warn-box { background: #7b0000; color: white; padding: 12px 18px; border-radius: 8px; margin-bottom: 16px; font-weight: 600; font-size: 1.0em; border: 2px solid #ff4444; } .suizid-warn-box .suizid-detail { font-weight: normal; font-size: 0.9em; margin-top: 4px; opacity: 0.92; } .item-row-suizid { background: #fff0f0 !important; border-color: #dc3545 !important; } .suizid-badge { display: inline-block; background: #7b0000; color: white; padding: 1px 7px; border-radius: 10px; font-size: 0.78em; font-weight: 700; margin-left: 4px; vertical-align: middle; white-space: nowrap; } .start-hinweis { text-align: center; color: #bbb; padding: 40px 0; font-size: 0.95em; } " app_css = gsub("#8B2635", AKZENT_FARBE, app_css, fixed = TRUE) ui = fluidPage( tags$head( tags$style(HTML(app_css)), tags$script(HTML(" Shiny.addCustomMessageHandler('scroll_top', function(x) { window.scrollTo(0, 0); }); ")) ), div(class = "app-header", tags$h2("VDS-90 Auswertung"), tags$p("Veränderungsfragebogen des Erlebens und Verhaltens • Einzelfall-Auswertung") ), 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("ergebnis") ) # Word-Export #### erstelle_vds90_docx = function(daten) { ueb_df = make_uebersicht_df(daten$subsk_erg, daten$geschlecht) plots_list = list( Schweregrad = plot_schweregrad(ueb_df), Prozentrang = plot_prozentrang(ueb_df), Abweichung = plot_abweichung(ueb_df), Vergleich = plot_vergleich(ueb_df) ) plot_dateien = lapply(names(plots_list), function(nm) { tmp = tempfile(fileext = ".png") ggplot2::ggsave(tmp, plots_list[[nm]], width = 8, height = 3.8, dpi = 150, bg = "white") tmp }) badge_bg = c("0" = "#28a745", "1" = "#ffb3b3", "2" = "#dc3545", "3" = "#7b0000") badge_fg = c("0" = "white", "1" = "#333333", "2" = "white", "3" = "white") fp_titel = fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 18) fp_meta = fp_text(color = "#555555", bold = FALSE, font.size = 10) fp_warn = fp_text(color = "#856404", italic = TRUE, font.size = 9) fp_abschn = fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 12, underlined = TRUE) fp_mono = fp_text(font.size = 9, font.family = "Courier New") fp_normal = fp_text(font.size = 9) fp_disclaimer = fp_text(color = "#888888", italic = TRUE, font.size = 9) doc = read_docx() doc = body_add_fpar(doc, fpar(ftext("VDS-90 Auswertung", fp_titel))) doc = body_add_fpar(doc, fpar(ftext( sprintf("Chiffre: %s Datum: %s Geschlecht: %s", daten$chiffre, daten$datum, daten$geschlecht_label), fp_meta ))) if (!is.null(daten$warnung)) doc = body_add_fpar(doc, fpar(ftext(paste0("Hinweis: ", daten$warnung), fp_warn))) { s25 = daten$stufen[["vds90_25"]] s26 = daten$stufen[["vds90_26"]] aktiv_25 = !is.na(s25) && s25 > 0 aktiv_26 = !is.na(s26) && s26 > 0 if (aktiv_25 || aktiv_26) { detail_txt = paste0( if (aktiv_25) sprintf("Item 25 (Suizidgedanken): Antwort %d", as.integer(s25)), if (aktiv_25 && aktiv_26) " | ", if (aktiv_26) sprintf("Item 26 (Suizidabsicht): Antwort %d", as.integer(s26)) ) doc = body_add_fpar(doc, fpar(ftext(paste0("SUIZIDALITAET: Bitte Items 25/26 beachten -- ", detail_txt), fp_text(color = "white", bold = TRUE, font.size = 11, shading.color = "#7b0000")), fp_p = fp_par(padding = 8)) ) } } doc = body_add_par(doc, "") doc = body_add_fpar(doc, fpar(ftext("Uebersicht aller Subskalen", fp_abschn))) korder = vds90_kuerzel_reihenfolge for (sk in korder) { erg = daten$subsk_erg[[sk]] zeile_txt = sprintf("%-4s %-40s MW: %5.3f PR: %5.1f%%", sk, substr(vds90_bezeichnungen[[sk]], 1, 40), erg$mittelwert, erg$prozentrang) doc = body_add_fpar(doc, fpar(ftext(zeile_txt, fp_mono))) } doc = body_add_par(doc, "") doc = body_add_fpar(doc, fpar(ftext("Uebersichtsdiagramme", fp_abschn))) for (i in seq_along(plot_dateien)) { doc = body_add_img(doc, src = plot_dateien[[i]], width = 6.2, height = 2.8) } doc = body_add_par(doc, "") doc = body_add_fpar(doc, fpar(ftext("Itemlisten nach Subskala", fp_abschn))) for (sk in korder) { if (sk == "GE") next erg = daten$subsk_erg[[sk]] doc = body_add_fpar(doc, fpar(ftext(sprintf("%s – %s (MW: %.3f, PR: %.0f%%)", sk, vds90_bezeichnungen[[sk]], erg$mittelwert, erg$prozentrang), fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 11))) ) for (nm in erg$item_namen) { stufe_val = erg$stufen[[nm]] s_char = if (!is.na(stufe_val)) as.character(as.integer(stufe_val)) else "?" bg_col = if (!is.na(stufe_val)) badge_bg[s_char] else "#aaaaaa" fg_col = if (!is.na(stufe_val)) badge_fg[s_char] else "white" item_txt = vds90_itemtexte[[nm]] if (is.null(item_txt)) item_txt = nm ist_suizid = nm %in% c("vds90_25", "vds90_26") warn_aktiv = ist_suizid && !is.na(stufe_val) && stufe_val > 0 doc = body_add_fpar(doc, fpar( ftext(sprintf("%-10s", nm), fp_text(bold = TRUE, font.size = 9, font.family = "Courier New")), ftext(paste0(" ", s_char, " "), fp_text(color = fg_col, bold = TRUE, font.size = 9, shading.color = bg_col)), if (warn_aktiv) ftext(" SUIZID ", fp_text(color = "white", bold = TRUE, font.size = 9, shading.color = "#7b0000")), ftext(paste0(" ", substr(item_txt, 1, 120)), fp_normal) )) } doc = body_add_par(doc, "") } lapply(plot_dateien, function(f) if (file.exists(f)) unlink(f)) doc = body_add_par(doc, "") doc = body_add_fpar(doc, fpar(ftext(VDS90_DISCLAIMER, fp_disclaimer))) doc } # Server #### server = function(input, output, session) { # --- pseudonym-support-injection v1 --- observe({ query = parseQueryString(session$clientData$url_search) if (!is.null(query$pseudonym) && nchar(trimws(query$pseudonym)) > 0) { updateTextInput(session, "pseudonym", value = trimws(query$pseudonym)) } }) observe({ query = parseQueryString(session$clientData$url_search) if (!is.null(query$chiffre) && nchar(trimws(query$chiffre)) > 0) { updateTextInput(session, "chiffre", value = toupper(trimws(query$chiffre))) } }) daten_reaktiv = eventReactive(input$btn_suchen, { chiffre = toupper(trimws(input$chiffre)) if ((nchar(trimws(input$pseudonym)) == 0 && nchar(chiffre) == 0)) { return(list(fehler = "Bitte eine Chiffre eingeben.")) } if (!(nchar(trimws(input$pseudonym)) > 0 || grepl("^[A-Z][0-9]{6}$", chiffre))) { return(list(fehler = "Ungültige Chiffre. Erwartet: ein Großbuchstabe + 6 Ziffern, z.B. P000123.")) } if (!file.exists(PFAD_DOWNLOAD_SKRIPT)) { return(list(fehler = paste0("Download-Skript nicht gefunden:\n", PFAD_DOWNLOAD_SKRIPT))) } if (!file.exists(PFAD_PSEUDONYM_SKRIPT)) { return(list(fehler = paste0("Pseudonym-Skript nicht gefunden:\n", PFAD_PSEUDONYM_SKRIPT))) } 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(fehler = paste0("Fehler beim Laden der Daten:\n", ok_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 }) if (is.null(db_ordner)) { return(list(fehler = paste0( "pseudonyme.db nicht gefunden (bis 5 Ebenen oberhalb von ", dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT, mustWork = FALSE)), " gesucht)." ))) } ok_ps = tryCatch({ alter_wd = getwd() on.exit(setwd(alter_wd), add = TRUE) setwd(db_ordner) source(PFAD_PSEUDONYM_SKRIPT, local = FALSE) if (nchar(trimws(input$pseudonym)) > 0) { .pw_wert = trimws(input$pseudonym) .pw_tab = get("pseudo", envir = .GlobalEnv) .pw_treffer = .pw_tab[.pw_tab$pseudonym == .pw_wert, ] if (nrow(.pw_treffer) > 0) chiffre = toupper(trimws(.pw_treffer$chiffre[1])) } list(ok = TRUE) }, error = function(e) list(ok = FALSE, msg = e$message)) if (!ok_ps$ok) return(list(fehler = paste0("Fehler beim Laden der Pseudonymdaten:\n", ok_ps$msg))) if (!exists("daten_vds90", envir = .GlobalEnv)) return(list(fehler = "daten_vds90 fehlt nach dem Sourcen. Bitte Download-Skript pruefen.")) if (!exists("pseudo", envir = .GlobalEnv)) return(list(fehler = "pseudo fehlt nach dem Sourcen. Bitte Pseudonym-Skript pruefen.")) daten_vds90 = get("daten_vds90", envir = .GlobalEnv) pseudo = get("pseudo", envir = .GlobalEnv) treffer_pseudo = pseudo[tolower(trimws(as.character(pseudo$chiffre))) == tolower(chiffre), ] if (nrow(treffer_pseudo) == 0) return(list(fehler = "Chiffre nicht gefunden.")) alle_session_ids = unique(as.character(treffer_pseudo$pseudonym)) if (nchar(trimws(input$pseudonym)) > 0) alle_session_ids = trimws(input$pseudonym) kandidaten = daten_vds90[as.character(daten_vds90$session) %in% alle_session_ids, , drop = FALSE] if (nrow(kandidaten) == 0) { return(list(fehler = paste0( "Kein Fragebogen-Datensatz gefunden. (", nrow(treffer_pseudo), " Pseudonyme zu dieser Chiffre, keines in daten_vds90)" ))) } warnung = NULL if (nrow(kandidaten) > 1) { n_ausfuell = nrow(kandidaten) best_idx = which.max(as.POSIXct(kandidaten$created)) kandidaten = kandidaten[best_idx, , drop = FALSE] best_time = format(as.POSIXct(kandidaten$created[1]), "%d.%m.%Y %H:%M") warnung = paste0( "Mehrere Durchlaeufe gefunden (", n_ausfuell, " Datensaetze). ", "Es wird der neueste verwendet (", best_time, ")." ) } zeile = kandidaten geschlecht_raw = zeile[["vds90_geschlecht"]] geschlecht_labels_attr = attr(daten_vds90[["vds90_geschlecht"]], "labels") if (!is.null(geschlecht_labels_attr)) { g_text = names(geschlecht_labels_attr)[ as.integer(geschlecht_labels_attr) == as.integer(geschlecht_raw) ] if (length(g_text) == 0) g_text = as.character(geschlecht_raw) } else { g_text = tryCatch( as.character(haven::as_factor(geschlecht_raw)), error = function(e) as.character(geschlecht_raw) ) } geschlecht = if (grepl("weiblich", g_text[1], ignore.case = TRUE)) "weiblich" else "maennlich" geschlecht_label = g_text[1] # labels-Attribut aus Original-Spalten lesen, nicht aus dem subgesetteten Datensatz item_namen = sprintf("vds90_%02d", 1:90) stufen = sapply(item_namen, function(nm) { spalte_orig = daten_vds90[[nm]] rohwert = as.numeric(zeile[[nm]]) stufe_aus_labels_fuer_wert(spalte_orig, rohwert) }) names(stufen) = item_namen alle_kuerzel = c(names(vds90_subskalen_items)[names(vds90_subskalen_items) != "GE"], "GE") subsk_erg = lapply(setNames(alle_kuerzel, alle_kuerzel), function(sk) { item_nms = sprintf("vds90_%02d", vds90_subskalen_items[[sk]]) sk_stufen = stufen[item_nms] summe = sum(sk_stufen, na.rm = TRUE) mittelwert = mean(sk_stufen, na.rm = TRUE) pr = berechne_prozentrang(summe, mittelwert, sk, geschlecht) list( kuerzel = sk, bezeichnung = vds90_bezeichnungen[[sk]], item_namen = item_nms, stufen = sk_stufen, summe = summe, mittelwert = mittelwert, prozentrang = pr ) }) datum_str = tryCatch( format(as.Date(as.character(zeile$created[1])), "%d.%m.%Y"), error = function(e) as.character(zeile$created[1]) ) list( ok = TRUE, chiffre = chiffre, datum = datum_str, geschlecht = geschlecht, geschlecht_label = geschlecht_label, stufen = stufen, subsk_erg = subsk_erg, warnung = warnung ) }) output$ergebnis = renderUI({ if (input$btn_suchen == 0) { return(div(class = "start-hinweis", "Patientenchiffre eingeben und auf \"Auswerten\" klicken." )) } daten = daten_reaktiv() if (!is.null(daten$fehler)) { return(div(class = "alert-fehler", daten$fehler)) } if (!isTRUE(daten$ok)) return(NULL) korder_27 = vds90_kuerzel_reihenfolge[vds90_kuerzel_reihenfolge != "GE"] tagList( div(class = "abschnitt-karte", div(class = "kopf-info", tags$b("Chiffre: "), daten$chiffre, " ", tags$b("Datum: "), daten$datum, " ", tags$b("Geschlecht: "), daten$geschlecht_label ), if (!is.null(daten$warnung)) div(class = "alert-warnung", daten$warnung) ), { s25 = daten$stufen[["vds90_25"]] s26 = daten$stufen[["vds90_26"]] aktiv_25 = !is.na(s25) && s25 > 0 aktiv_26 = !is.na(s26) && s26 > 0 if (aktiv_25 || aktiv_26) { detail_txt = paste0( if (aktiv_25) sprintf("Item 25 (Suizidgedanken): Antwort %d", as.integer(s25)), if (aktiv_25 && aktiv_26) " | ", if (aktiv_26) sprintf("Item 26 (Suizidabsicht): Antwort %d", as.integer(s26)) ) div(class = "suizid-warn-box", tags$span("⚠️ Suizidalität: Bitte Items 25/26 beachten"), div(class = "suizid-detail", detail_txt) ) } }, div(class = "uebersicht-box", tags$details(open = NA, tags$summary(strong("Übersichtsdiagramme (4 Diagramme einblenden/ausblenden)")), br(), fluidRow( column(6, plotOutput("plot_schweregrad", height = "300px")), column(6, plotOutput("plot_prozentrang", height = "300px")) ), fluidRow( column(6, plotOutput("plot_abweichung", height = "300px")), column(6, plotOutput("plot_vergleich", height = "300px")) ) ) ), div(class = "abschnitt-karte", div(class = "abschnitt-titel", tags$span(class = "kuerzel", "GE"), vds90_bezeichnungen[["GE"]] ), plotOutput("gauge_GE", height = "70px") ), lapply(korder_27, function(sk) { n_items = length(vds90_subskalen_items[[sk]]) div(class = "abschnitt-karte", div(class = "abschnitt-titel", tags$span(class = "kuerzel", sk), vds90_bezeichnungen[[sk]] ), plotOutput(paste0("gauge_", sk), height = "70px"), tags$details( tags$summary(sprintf("Items einblenden (%d Item%s)", n_items, if (n_items > 1) "s" else "")), div(class = "items-container", uiOutput(paste0("items_", sk))) ) ) }) ) }) observe({ daten = daten_reaktiv() if (!isTRUE(daten$ok)) return() for (sk in vds90_kuerzel_reihenfolge) { local({ sk_ = sk erg = daten$subsk_erg[[sk_]] output[[paste0("gauge_", sk_)]] = renderPlot({ gauge_ggplot(erg$mittelwert, erg$prozentrang) }, height = 70, bg = "transparent") if (sk_ != "GE") output[[paste0("items_", sk_)]] = renderUI({ item_tags = lapply(erg$item_namen, function(nm) { stufe_val = erg$stufen[[nm]] badge_cls = if (!is.na(stufe_val)) { paste0("stufe-badge stufe-badge-", as.integer(stufe_val)) } else { "stufe-badge" } badge_txt = if (!is.na(stufe_val)) as.character(as.integer(stufe_val)) else "?" ist_suizid = nm %in% c("vds90_25", "vds90_26") warn_aktiv = ist_suizid && !is.na(stufe_val) && stufe_val > 0 row_cls = if (warn_aktiv) "item-zeile item-row-suizid" else "item-zeile" div(class = row_cls, span(class = "item-nr", nm), span(class = "item-text", vds90_itemtexte[[nm]]), span(class = badge_cls, badge_txt), if (warn_aktiv) span(class = "suizid-badge", "Suizid") ) }) do.call(tagList, item_tags) }) }) } ueb_df = make_uebersicht_df(daten$subsk_erg, daten$geschlecht) output$plot_schweregrad = renderPlot({ plot_schweregrad(ueb_df) }) output$plot_prozentrang = renderPlot({ plot_prozentrang(ueb_df) }) output$plot_abweichung = renderPlot({ plot_abweichung(ueb_df) }) output$plot_vergleich = renderPlot({ plot_vergleich(ueb_df) }) }) output$download_word = downloadHandler( filename = function() { daten = daten_reaktiv() chiffre_safe = if (!is.null(daten$chiffre)) gsub("[^A-Za-z0-9_-]", "_", daten$chiffre) else "unbekannt" datum_fn = if (!is.null(daten$datum)) { tryCatch(format(as.Date(daten$datum, "%d.%m.%Y"), "%Y%m%d"), error = function(e) format(Sys.Date(), "%Y%m%d")) } else format(Sys.Date(), "%Y%m%d") sprintf("VDS90_%s_%s.docx", chiffre_safe, datum_fn) }, content = function(file) { daten = daten_reaktiv() if (is.null(daten$ok) || !isTRUE(daten$ok)) { showNotification("Bitte zuerst eine Suche durchführen.", type = "error") return(NULL) } doc = erstelle_vds90_docx(daten) print(doc, target = file) } ) } # Start #### shinyApp(ui = ui, server = server)