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