# Präambel #### PFAD_DOWNLOAD_SKRIPT = "../API/get_data_stai_state.R" # liefert beim Sourcen: daten_stai_state PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R" # liefert beim Sourcen: pseudo AKZENT_FARBE = "#8B2635" # Pflicht-Disclaimer, erscheint als Fusstext in UI und Word-Export. Fuer die # State-Angstskala existieren laut Testmanual keine Normtabellen (Skala wurde zur # Veraenderungsmessung konstruiert) - deshalb wird ausschliesslich der Rohwert berichtet. STAI_STATE_DISCLAIMER = paste0( "Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ", "keine klinische Diagnose. Die Interpretation obliegt der behandelnden Person. ", "Fuer die State-Angstskala existieren laut Testmanual keine Normtabellen, es wird ", "ausschliesslich der Rohwert ausgewiesen." ) # Die 10 Umkehr-Items der State-Skala (Wert = 5 - Rohwert); alle uebrigen 10 Items # unrecodiert. Als benannte Konstante zentral abgelegt statt inline im Scoring-Code. STAI_STATE_UMPOL_ITEMS = c( "stai_state_01", "stai_state_02", "stai_state_05", "stai_state_08", "stai_state_10", "stai_state_11", "stai_state_15", "stai_state_16", "stai_state_19", "stai_state_20" ) # Verlauf gruen -> dunkelrot entspricht den 4 Antwortstufen 1-4 NACH Umpolung # (Stufe = Ausmass der angezeigten Angst durch diese Antwort, nicht der Rohwert - # bei Umkehr-Items zeigt sonst z.B. "sehr ruhig" faelschlich als dunkelrot an). STAI_STATE_BADGE_FARBEN = c( "1" = "#4CAF50", "2" = "#F48FB1", "3" = "#EF5350", "4" = "#B71C1C" ) STAI_STATE_BADGE_TEXT_FARBEN = c( "1" = "white", "2" = "#333333", "3" = "white", "4" = "white" ) 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 #### # Entfernt formr-Markdown-Reste aus Item- und Introtexten: escapete Nummerierung # am Zeilenanfang (z.B. "16\. " oder "16. ") und doppelte Sternchen. Ueberall # anwenden, wo Item- oder Introtext angezeigt wird - nie Rohtext direkt ausgeben. bereinige_markdown = function(text) { if (is.null(text) || length(text) == 0 || is.na(text[1])) return(NA_character_) txt = trimws(as.character(text[1])) txt = sub("^(\\d+)\\\\([.)])", "\\1\\2", txt) txt = sub("^\\d+[.)]\\s*", "", txt) txt = gsub("\\*\\*", "", txt) trimws(txt) } # Loest einen labelled-Wert (haven dbl+lbl) ueber das labels-Attribut der # Originalspalte in seinen Antworttext auf, mit as_factor()-Fallback. Liefert # NA_character_, wenn keiner der beiden Wege einen Text liefert (kein Rateergebnis). stai_state_labeltext = function(spalte_orig, wert) { if (length(wert) == 0 || is.na(wert[1])) return(NA_character_) lbl_attr = attr(spalte_orig, "labels") if (!is.null(lbl_attr) && length(lbl_attr) > 0) { pos = which(as.vector(lbl_attr) == suppressWarnings(as.numeric(wert[1]))) if (length(pos) > 0) { txt = trimws(names(lbl_attr)[pos[1]]) if (nchar(txt) > 0) return(txt) } } txt_af = tryCatch(as.character(haven::as_factor(wert[1])), error = function(e) NA_character_) if (!is.na(txt_af) && nchar(trimws(txt_af)) > 0) return(trimws(txt_af)) NA_character_ } # Liest EIN Item (EINE Person) aus einer "mc"-Itemspalte: Rohcode 1-4 fuer das # Scoring (immer der numerische Wert hinter dem labelled-Objekt) und Anzeigetext # (Label ueber labels-Attribut/as_factor, sonst Fallback auf den blossen Zahlenwert). # Nicht zuordenbare Werte (Code ausserhalb 1-4) werden NA, aber sichtbar vermerkt - # kein stiller Default. stai_state_lese_item = function(spalte_orig, wert) { if (length(wert) == 0 || is.na(wert[1])) { return(list(code = NA_real_, text = NA_character_, zuordenbar = TRUE, hinweis = "keine Angabe")) } code_roh = suppressWarnings(as.numeric(wert[1])) if (is.na(code_roh) || !(code_roh %in% 1:4)) { return(list(code = NA_real_, text = NA_character_, zuordenbar = FALSE, hinweis = paste0("nicht zuordenbarer Wert (", wert[1], ")"))) } text_label = stai_state_labeltext(spalte_orig, wert) text_anzeige = if (is.na(text_label)) { paste0("Wert ", code_roh, " (kein Antworttext verfuegbar)") } else { bereinige_markdown(text_label) } list(code = code_roh, text = text_anzeige, zuordenbar = TRUE, hinweis = NULL) } # Titel eines Items aus dem formr-Label der Spalte, mit Fallback auf den Spaltennamen. stai_state_item_titel = function(spalte_orig, fallback) { lbl = attr(spalte_orig, "label") if (is.null(lbl) || length(lbl) == 0 || is.na(lbl[1]) || nchar(trimws(as.character(lbl[1]))) == 0) { return(fallback) } bereinige_markdown(lbl[1]) } # Wendet die Umpolung (5 - Rohwert) auf die 10 dafuer vorgesehenen Items an, # alle uebrigen Items bleiben unveraendert. NA bleibt NA. stai_state_umpolen = function(item_name, code) { if (is.na(code)) return(NA_real_) if (item_name %in% STAI_STATE_UMPOL_ITEMS) return(5 - code) code } # Summenscore nach der Missing-Value-Regel des Manuals: # > 2 fehlende Items: nicht auswertbar (rohwert = NA) # 1-2 fehlende Items: Schaetzwert = ceiling(mean(vorhandene) * 20) # 0 fehlende Items: normale Summe stai_state_score = function(codes_umgepolt) { n_fehlt = sum(is.na(codes_umgepolt)) if (n_fehlt > 2) { return(list(rohwert = NA_real_, missing_n = n_fehlt, auswertbar = FALSE)) } rohwert = if (n_fehlt == 0) { sum(codes_umgepolt) } else { ceiling(mean(codes_umgepolt, na.rm = TRUE) * 20) } list(rohwert = rohwert, missing_n = n_fehlt, auswertbar = TRUE) } # Neutraler Balken 20-80 ohne Farbzonen und ohne Cutoff-Markierung - rein zur # Veranschaulichung der Position des Rohwerts im theoretischen Wertebereich. Es # gibt fuer die State-Skala keinen Normvergleich, daher keine Zonenfaerbung. erstelle_balken_stai_state = function(rohwert) { p = ggplot() + geom_rect(aes(xmin = 20, xmax = 80, ymin = 0, ymax = 1), fill = "#EDEDED", colour = "#CCCCCC", linewidth = 0.5) + scale_x_continuous(limits = c(20, 80), expand = c(0, 0), breaks = c(20, 35, 50, 65, 80)) + scale_y_continuous(limits = c(-0.25, 1.35), expand = c(0, 0)) + labs(x = "Rohwert (Range 20-80)", y = NULL) + theme_minimal(base_size = 11) + theme( axis.text.y = element_blank(), axis.ticks.y = element_blank(), panel.grid = element_blank(), plot.background = element_rect(fill = "white", colour = NA), panel.background = element_rect(fill = "white", colour = NA), axis.line.x = element_line(colour = "#cccccc", linewidth = 0.5), axis.text.x = element_text(colour = "#666666", size = 9) ) if (!is.na(rohwert)) { p = p + geom_segment(aes(x = rohwert, xend = rohwert, y = -0.1, yend = 1.1), colour = AKZENT_FARBE, linewidth = 2.5, lineend = "round") + annotate("text", x = rohwert, y = 1.26, label = as.character(rohwert), colour = AKZENT_FARBE, fontface = "bold", size = 4.2) } p } # 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: 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-fehler h4 { color: #C62828; margin-top: 0; margin-bottom: 8px; } .alert-fehler p, .alert-fehler li { color: #444; font-weight: 400; font-size: 0.92em; } .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; } .meta-block .hinweis-inline { color: #999; font-size: 0.88em; font-style: italic; } .rohwert-anzeige { font-size: 1.6rem; font-weight: 700; color: #222; } .rohwert-hinweis { color: #777; font-size: 0.88em; margin-top: 4px; font-style: italic; } .disclaimer-block { font-size: 0.82em; color: #777; font-style: italic; margin-top: 10px; border-top: 1px solid rgba(0,0,0,.1); padding-top: 8px; } .item-zeile { display: flex; align-items: flex-start; gap: 10px; padding: 7px 0; border-bottom: 1px solid #F0F0F0; } .item-nr { font-weight: 600; color: #8B2635; min-width: 22px; flex-shrink: 0; } .item-text { flex: 2; color: #333; font-size: 0.92em; } .item-antwort { flex: 1; display: flex; justify-content: flex-end; min-width: 140px; } .antwort-badge { display: inline-block; border-radius: 3px; padding: 2px 10px; font-size: 0.85em; font-weight: 700; min-width: 90px; text-align: center; flex-shrink: 0; background-color: #EDEDED; color: #444; } .antwort-badge-1 { background-color: #4CAF50; color: white; } .antwort-badge-2 { background-color: #F48FB1; color: #333333; } .antwort-badge-3 { background-color: #EF5350; color: white; } .antwort-badge-4 { background-color: #B71C1C; color: white; } .antwort-badge-fehlend { background-color: #FEECEB; color: #B71C1C; } .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$meta(charset = "UTF-8"), tags$style(HTML(app_css)) ), div(class = "app-header", tags$h2("STAI-G (State-Angstskala, Einzelbogen)"), tags$p("Einzelfall-Auswertung - Rohwert ohne Normvergleich") ), 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("ergebnis_ui") ) ) # Word-Export #### erstelle_stai_state_docx = function(erg) { fmt_titel = fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 18) fmt_meta = fp_text(color = "#555555", bold = FALSE, font.size = 10) fmt_warn = fp_text(color = "#B8860B", italic = TRUE, font.size = 9) fmt_score_l = fp_text(color = "#888888", bold = FALSE, font.size = 10) fmt_score = fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 22) fmt_norm_hinw = fp_text(color = "#888888", italic = TRUE, font.size = 9) fmt_abschn = fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 12, underlined = TRUE) fmt_item_nr = fp_text(color = "#888888", bold = TRUE, font.size = 10) fmt_item_tit = fp_text(color = "#333333", bold = FALSE, font.size = 10) fmt_antwort = fp_text(color = "#555555", italic = TRUE, font.size = 9) fmt_disclaimer = fp_text(color = "#888888", italic = TRUE, font.size = 9) doc = read_docx() doc = body_add_fpar(doc, fpar(ftext("STAI-G, State-Angstskala", fmt_titel))) doc = body_add_fpar(doc, fpar(ftext( paste0( "Chiffre: ", erg$chiffre, " Ausfuelldatum: ", erg$ausfuelldatum, " Alter: ", if (is.na(erg$alter)) "k. A." else erg$alter, " Geschlecht: ", if (is.na(erg$geschlecht)) "k. A." else erg$geschlecht, " (nur Metadatum, keine Normauswertung fuer diese Skala verfuegbar)" ), fmt_meta ))) if (!is.null(erg$warnung_mehrfach)) { doc = body_add_fpar(doc, fpar(ftext(paste0("Hinweis: ", erg$warnung_mehrfach), fmt_warn))) } if (erg$missing_n > 0 && erg$auswertbar) { doc = body_add_fpar(doc, fpar(ftext(paste0( "Hinweis: ", erg$missing_n, " Item(s) ohne Angabe, Rohwert nach Schaetzverfahren ", "(ceiling(Mittelwert der vorhandenen Items * 20)) ermittelt." ), fmt_warn))) } doc = body_add_par(doc, "") if (erg$auswertbar) { doc = body_add_fpar(doc, fpar( ftext("Rohwert: ", fmt_score_l), ftext(paste0(erg$rohwert, " / 80"), fmt_score) )) } else { doc = body_add_fpar(doc, fpar( ftext("Rohwert: ", fmt_score_l), ftext(paste0("nicht auswertbar (", erg$missing_n, " fehlende Items)"), fmt_score) )) } doc = body_add_fpar(doc, fpar(ftext( "Fuer die State-Angstskala existieren laut Testmanual keine Normtabellen (Skala zur ", "Veraenderungsmessung konstruiert); es wird ausschliesslich der Rohwert (Range 20-80) berichtet.", fmt_norm_hinw ))) doc = body_add_par(doc, "") doc = body_add_fpar(doc, fpar(ftext("Einzelitems (20 Items)", fmt_abschn))) for (it in erg$items) { antwort_txt = if (it$zuordenbar && !is.na(it$code)) it$text else paste0("(", it$hinweis, ")") doc = body_add_fpar(doc, fpar( ftext(sprintf("%2d. ", it$nr), fmt_item_nr), ftext(paste0(it$titel, " - "), fmt_item_tit), ftext(antwort_txt, fmt_antwort) )) } doc = body_add_par(doc, "") doc = body_add_fpar(doc, fpar(ftext(STAI_STATE_DISCLAIMER, fmt_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))) } }) # Die beiden externen Skripte werden bewusst NICHT beim App-Start gesourct, sondern # erst hier, beim Klick auf "Auswerten" (Source-bei-Klick-Muster). ergebnis_r = eventReactive(input$btn_suchen, { chiffre = toupper(trimws(input$chiffre)) if ((nchar(trimws(input$pseudonym)) == 0 && nchar(chiffre) == 0) || !(nchar(trimws(input$pseudonym)) > 0 || grepl("^[A-Z][0-9]{6}$", chiffre))) { return(list(typ = "format_fehler", chiffre = chiffre)) } pfadfehler = character(0) if (!file.exists(PFAD_DOWNLOAD_SKRIPT)) { pfadfehler = c(pfadfehler, paste0("Download-Skript nicht gefunden: >>", PFAD_DOWNLOAD_SKRIPT, "<<")) } if (!file.exists(PFAD_PSEUDONYM_SKRIPT)) { pfadfehler = c(pfadfehler, paste0("Pseudonym-Skript nicht gefunden: >>", PFAD_PSEUDONYM_SKRIPT, "<<")) } if (length(pfadfehler) > 0) { return(list(typ = "pfad_fehler", meldung = paste("Bitte Pfade am Kopf der app.R anpassen:", paste(pfadfehler, collapse = "\n"), sep = "\n"))) } # Download-Skript sourcen. Achtung: Rueckgabewert-Pattern exakt so, kein # `<=` statt `=` - das waere syntaktisch gueltig, aber semantisch ein stiller Bug. ok_download = tryCatch({ source(PFAD_DOWNLOAD_SKRIPT, local = FALSE) list(ok = TRUE) }, error = function(e) list(ok = FALSE, msg = conditionMessage(e))) if (!ok_download$ok) { return(list(typ = "skript_fehler", meldung = paste0("Fehler im Download-Skript (", basename(PFAD_DOWNLOAD_SKRIPT), "):\n", ok_download$msg))) } if (!exists("daten_stai_state", envir = .GlobalEnv) || !is.data.frame(get("daten_stai_state", envir = .GlobalEnv))) { return(list(typ = "daten_fehler", meldung = paste0("Objekt 'daten_stai_state' fehlt nach dem Sourcen von:\n", PFAD_DOWNLOAD_SKRIPT))) } daten = get("daten_stai_state", envir = .GlobalEnv) # Ordner mit pseudonyme.db suchen, ausgehend vom Pseudonym-Skript-Ordner, bis # zu 5 Ebenen nach oben. 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_fehler", meldung = paste0( "pseudonyme.db nicht gefunden.\n", "Gesucht ausgehend vom Pseudonym-Skript-Ordner bis zu 5 Ebenen nach oben.\n", "Bitte sicherstellen, dass pseudonyme.db im selben oder einem ", "uebergeordneten Ordner liegt." ))) } # Arbeitsverzeichnis fuer die Dauer des Sourcens auf den DB-Ordner setzen (das # Pseudonym-Skript oeffnet die DB relativ) und danach zuverlaessig zuruecksetzen. # add = TRUE ist Pflicht, sonst wuerden ggf. bereits registrierte on.exit()-Handler # ueberschrieben statt ergaenzt. ok_pseudonym = 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 = conditionMessage(e))) if (!ok_pseudonym$ok) { return(list(typ = "skript_fehler", meldung = paste0("Fehler im Pseudonym-Skript (", basename(PFAD_PSEUDONYM_SKRIPT), "):\n", ok_pseudonym$msg))) } if (!exists("pseudo", envir = .GlobalEnv)) { return(list(typ = "skript_fehler", meldung = paste0("Objekt 'pseudo' fehlt nach dem Sourcen von:\n", PFAD_PSEUDONYM_SKRIPT))) } pseudo = get("pseudo", envir = .GlobalEnv) # Chiffre-Lookup. Kein Filter auf 'instrument' noetig, da diese App nur Daten # dieses einen STAI-State-Runs erhaelt. ps_treffer = pseudo[toupper(trimws(as.character(pseudo$chiffre))) == chiffre, , drop = FALSE] if (nrow(ps_treffer) == 0) { return(list(typ = "chiffre_nicht_gefunden", chiffre = chiffre)) } alle_session_ids = unique(as.character(ps_treffer$pseudonym)) if (nchar(trimws(input$pseudonym)) > 0) alle_session_ids = trimws(input$pseudonym) treffer = daten[as.character(daten$session) %in% alle_session_ids, , drop = FALSE] if (nrow(treffer) == 0) { return(list(typ = "session_nicht_gefunden", chiffre = chiffre)) } # Mehrfachtreffer (Bogen mehrfach ausgefuellt): nicht stillschweigend den ersten # nehmen, sondern den neuesten (nach 'created') waehlen und sichtbar warnen. warnung_mehrfach = NULL if (nrow(treffer) > 1) { n_ausfuell = nrow(treffer) reihenfolge = order(as.POSIXct(treffer$created), decreasing = TRUE) treffer = treffer[reihenfolge, , drop = FALSE] datum_neuestes = tryCatch( format(as.POSIXct(treffer$created[1]), "%d.%m.%Y %H:%M"), error = function(e) "unbekanntes Datum" ) warnung_mehrfach = paste0( "Es wurden ", n_ausfuell, " Ausfuellungen gefunden, es wird die neueste vom ", datum_neuestes, " angezeigt." ) treffer = treffer[1, , drop = FALSE] } zeile = treffer[1, , drop = FALSE] ausfuelldatum_geparst = tryCatch(as.POSIXct(zeile$created[1]), error = function(e) NA) ausfuelldatum_fallback = is.null(ausfuelldatum_geparst) || is.na(ausfuelldatum_geparst) ausfuelldatum = if (!ausfuelldatum_fallback) format(ausfuelldatum_geparst, "%d.%m.%Y") else format(Sys.Date(), "%d.%m.%Y") ausfuelldatum_dateikennung = if (!ausfuelldatum_fallback) format(ausfuelldatum_geparst, "%Y%m%d") else format(Sys.Date(), "%Y%m%d") # Alter und Geschlecht: reine Metadaten, fliessen in keine Berechnung ein (siehe # STAI_STATE_DISCLAIMER / Spezifikation - keine Normtabellen fuer diese Skala). alter = suppressWarnings(as.numeric(zeile[["alter"]][1])) geschlecht = tryCatch( stai_state_labeltext(daten[["geschlecht"]], zeile[["geschlecht"]][1]), error = function(e) NA_character_ ) geschlecht = if (is.null(geschlecht)) NA_character_ else geschlecht # 20 Items einlesen (Rohcode 1-4 + Anzeigetext), fehlende Item-Spalten sind ein # Konfigurationsfehler (daten_fehler), nicht zuordenbare Einzelwerte werden pro # Item als NA mit sichtbarem Hinweis behandelt (siehe stai_state_lese_item()). item_cols = sprintf("stai_state_%02d", 1:20) fehlende_item_spalten = item_cols[!(item_cols %in% names(daten))] if (length(fehlende_item_spalten) > 0) { return(list(typ = "daten_fehler", meldung = paste0( "Folgende erwartete Item-Spalten fehlen in 'daten_stai_state': ", paste(fehlende_item_spalten, collapse = ", "), "." ))) } items = lapply(seq_along(item_cols), function(i) { col = item_cols[i] spalte_gesamt = daten[[col]] roh_wert = zeile[[col]][1] gelesen = stai_state_lese_item(spalte_gesamt, roh_wert) titel = stai_state_item_titel(spalte_gesamt, paste0("Item ", i)) code_umgepolt = stai_state_umpolen(col, gelesen$code) list(nr = i, spalte = col, titel = titel, code = gelesen$code, code_umgepolt = code_umgepolt, text = gelesen$text, zuordenbar = gelesen$zuordenbar, hinweis = gelesen$hinweis) }) codes_umgepolt = vapply(items, `[[`, numeric(1), "code_umgepolt") score = stai_state_score(codes_umgepolt) list( typ = "ok", chiffre = chiffre, ausfuelldatum = ausfuelldatum, ausfuelldatum_fallback = ausfuelldatum_fallback, ausfuelldatum_dateikennung = ausfuelldatum_dateikennung, warnung_mehrfach = warnung_mehrfach, alter = alter, geschlecht = geschlecht, items = items, missing_n = score$missing_n, auswertbar = score$auswertbar, rohwert = score$rohwert ) }) output$ergebnis_ui = renderUI({ if (input$btn_suchen == 0) { return(div(class = "start-hinweis", "Patientenchiffre eingeben und auf \"Auswerten\" klicken." )) } erg = ergebnis_r() if (erg$typ == "format_fehler") { return(div(class = "alert-warnung", "Ungueltige Chiffre. Erwartet wird ein Grossbuchstabe gefolgt von 6 Ziffern, ", "z.B. P000123." )) } if (erg$typ == "pfad_fehler") { return(div(class = "alert-fehler", tags$h4("Konfigurationsfehler: Pfad nicht gefunden"), tags$pre(style = "font-size:0.88em; white-space:pre-wrap;", erg$meldung) )) } if (erg$typ == "skript_fehler") { return(div(class = "alert-fehler", tags$h4("Fehler beim Sourcen eines externen Skripts"), tags$pre(style = "font-size:0.88em; white-space:pre-wrap;", erg$meldung) )) } if (erg$typ == "daten_fehler") { return(div(class = "alert-fehler", tags$h4("Datenfehler"), tags$pre(style = "font-size:0.88em; white-space:pre-wrap;", erg$meldung) )) } if (erg$typ == "db_fehler") { return(div(class = "alert-fehler", tags$h4("pseudonyme.db nicht gefunden"), tags$pre(style = "font-size:0.88em; white-space:pre-wrap;", erg$meldung) )) } if (erg$typ == "chiffre_nicht_gefunden") { return(div(class = "alert-fehler", tags$h4("Chiffre nicht gefunden"), tags$p("Die Chiffre ", tags$b(paste0("«", erg$chiffre, "»")), " ist in der Pseudonymtabelle nicht vorhanden."), tags$p("Bitte Schreibweise pruefen oder Pseudonymtabelle aktualisieren.") )) } if (erg$typ == "session_nicht_gefunden") { return(div(class = "alert-fehler", tags$h4("Kein STAI-State-Datensatz gefunden"), tags$p("Zur Chiffre ", tags$b(paste0("«", erg$chiffre, "»")), " existiert ein Pseudonymeintrag, aber kein Datensatz in ", tags$code("daten_stai_state"), "."), tags$p("Moegliche Ursachen: Bogen noch nicht ausgefuellt, ", "oder Daten noch nicht heruntergeladen.") )) } # typ == "ok" kopf_block = div(class = "abschnitt-karte", div(class = "meta-block", tags$strong("Chiffre: "), erg$chiffre, tags$span(" | ", style = "color:#ccc;"), tags$strong("Ausfuelldatum: "), erg$ausfuelldatum, tags$span(" | ", style = "color:#ccc;"), tags$strong("Alter: "), if (is.na(erg$alter)) "k. A." else erg$alter, tags$span(" | ", style = "color:#ccc;"), tags$strong("Geschlecht: "), if (is.na(erg$geschlecht)) "k. A." else erg$geschlecht, " ", tags$span(class = "hinweis-inline", "(nur Metadatum, keine Normauswertung fuer diese Skala verfuegbar)") ), if (!is.null(erg$warnung_mehrfach)) div(class = "alert-warnung", "⚠ Hinweis: ", erg$warnung_mehrfach), if (isTRUE(erg$ausfuelldatum_fallback)) div(class = "alert-warnung", "⚠ Ausfuelldatum nicht in den Daten gefunden, Erstellungsdatum verwendet."), if (erg$missing_n > 0 && erg$auswertbar) div(class = "alert-warnung", "⚠ ", erg$missing_n, " Item(s) ohne Angabe, Rohwert nach Schaetzverfahren ermittelt.") ) score_block = div(class = "abschnitt-karte", div(class = "abschnitt-titel", "Rohwert STAI-State"), fluidRow( column(4, if (erg$auswertbar) { div(class = "rohwert-anzeige", paste0(erg$rohwert, " / 80")) } else { div(class = "rohwert-anzeige", paste0("nicht auswertbar (", erg$missing_n, " fehlende Items)")) }, div(class = "rohwert-hinweis", "Fuer die State-Angstskala existieren laut Testmanual keine Normtabellen; ", "es wird ausschliesslich der Rohwert (Range 20-80) berichtet." ) ), column(8, style = "padding-top: 6px;", if (erg$auswertbar) plotOutput("balken_plot", height = "110px") ) ) ) item_zeilen = lapply(erg$items, function(it) { unresolved = !it$zuordenbar || is.na(it$code) antwort_anzeige = if (unresolved) paste0("(", it$hinweis, ")") else it$text stufe_key = if (!unresolved && !is.na(it$code_umgepolt) && as.character(it$code_umgepolt) %in% names(STAI_STATE_BADGE_FARBEN)) { as.character(it$code_umgepolt) } else { NA_character_ } badge_klasse = if (unresolved || is.na(stufe_key)) { "antwort-badge antwort-badge-fehlend" } else { paste0("antwort-badge antwort-badge-", stufe_key) } div(class = "item-zeile", span(class = "item-nr", paste0(it$nr, ".")), span(class = "item-text", it$titel), span(class = "item-antwort", tags$span(class = badge_klasse, antwort_anzeige) ) ) }) item_block = div(class = "abschnitt-karte", div(class = "abschnitt-titel", "Einzelitems (20 Items)"), div(item_zeilen) ) tagList( kopf_block, score_block, item_block, div(class = "disclaimer-block", STAI_STATE_DISCLAIMER) ) }) output$balken_plot = renderPlot({ req(input$btn_suchen > 0) erg = ergebnis_r() req(erg$typ == "ok", erg$auswertbar) erstelle_balken_stai_state(erg$rohwert) }, bg = "white") output$download_word = downloadHandler( filename = function() { erg = ergebnis_r() if (is.null(erg) || erg$typ != "ok") return("STAIstate_Auswertung.docx") chiffre_esc = gsub("[^A-Za-z0-9_-]", "_", erg$chiffre) paste0("STAIstate_", chiffre_esc, "_", erg$ausfuelldatum_dateikennung, ".docx") }, content = function(file) { req(ergebnis_r()$typ == "ok") doc = erstelle_stai_state_docx(ergebnis_r()) print(doc, target = file) } ) } # Start #### shinyApp(ui = ui, server = server)