# Präambel #### library(shiny) library(dplyr) library(ggplot2) library(haven) library(officer) PFAD_DOWNLOAD_SKRIPT = "../API/get_data_oci12.R" PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R" AKZENT_FARBE = "#8B2635" OCI12_DISCLAIMER = paste0( "Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ", "keine klinische Diagnose. Der genannte Cutoff ist ein Screening-Hinweis aus der Testquelle, ", "kein Ausschluss- oder Einschlusskriterium. Die Interpretation obliegt der behandelnden Person." ) # Verlauf gruen -> dunkelrot entspricht den 5 Antwortstufen 0-4. OCI12_BADGE_FARBEN = c( "0" = "#4CAF50", "1" = "#AED581", "2" = "#FFB300", "3" = "#EF5350", "4" = "#B71C1C" ) OCI12_BADGE_TEXT_FARBEN = c( "0" = "white", "1" = "#333333", "2" = "#333333", "3" = "white", "4" = "white" ) OCI12_STUFEN_TEXTE = c("Überhaupt nicht", "Ein wenig", "Mäßig", "Stark", "Extrem") # 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 #### # Stufenableitung (0-4) IMMER ueber das labels-Attribut der ORIGINAL-Spalte (vor # Subsetting), nie hartkodiert - formr-Wertkodierung ist nicht garantiert 1-basiert. oci12_get_level = function(original_col, wert) { if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_integer_) lbl_attr = attr(original_col, "labels") if (!is.null(lbl_attr) && length(lbl_attr) > 0) { lbl_sortiert = sort(as.vector(lbl_attr)) pos = which(lbl_sortiert == as.numeric(wert[1])) if (length(pos) > 0) return(as.integer(pos[1]) - 1L) } # Fallback bei fehlenden labels: 1-basierte Kodierung angenommen (choice1 = Stufe 0) as.integer(as.numeric(wert[1])) - 1L } # Ankertext bevorzugt aus dem labels-Attribut (Original-Antworttext), sonst aus der # fest hinterlegten OCI-12-Antwortskala. oci12_get_anker = function(original_col, wert) { if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_character_) lbl_attr = attr(original_col, "labels") if (!is.null(lbl_attr) && length(lbl_attr) > 0) { pos = which(as.vector(lbl_attr) == as.numeric(wert[1])) if (length(pos) > 0) return(names(lbl_attr)[pos[1]]) } stufe = max(0L, min(4L, as.integer(as.numeric(wert[1])) - 1L)) OCI12_STUFEN_TEXTE[stufe + 1L] } oci12_klassifikation = function(gesamt) { if (isTRUE(gesamt >= 11)) { list( zone = "auffaellig", titel = "Klinisch auffaellig (Score >= 11)", text = paste0( "Der Gesamtscore von ", gesamt, " liegt bei oder ueber dem Cutoff von 11 Punkten. ", "Dies ist laut Testquelle ein Screening-Hinweis, der zwischen Personen mit ", "Zwangsstoerung (OCD) und nicht-klinischen Personen unterscheidet." ), farbe_text = "#B71C1C", farbe_bg = "#FFEBEE", css_klasse = "oci-zone-auffaellig" ) } else { list( zone = "unauffaellig", titel = "Unauffaellig (Score < 11)", text = paste0( "Der Gesamtscore von ", gesamt, " liegt unter dem Cutoff von 11 Punkten und damit ", "im unauffaelligen Bereich." ), farbe_text = "#2E7D32", farbe_bg = "#E8F5E9", css_klasse = "oci-zone-unauffaellig" ) } } make_gauge_oci12 = function(gesamt) { ggplot() + geom_rect(aes(xmin = 0, xmax = 10.5, ymin = 0, ymax = 1), fill = "#E8F5E9", color = NA) + geom_rect(aes(xmin = 10.5, xmax = 48, ymin = 0, ymax = 1), fill = "#FFEBEE", color = NA) + geom_rect(aes(xmin = 0, xmax = 48, ymin = 0, ymax = 1), fill = NA, color = "#9E9E9E", linewidth = 0.6) + geom_vline(xintercept = 10.5, color = "#B71C1C", linetype = "dashed", linewidth = 1) + geom_segment(aes(x = gesamt, xend = gesamt, y = -0.25, yend = 1.25), color = AKZENT_FARBE, linewidth = 2.5) + geom_label(aes(x = gesamt, y = 1.6, label = paste0("Score: ", gesamt)), fill = AKZENT_FARBE, color = "white", fontface = "bold", linewidth = 0, size = 4) + annotate("text", x = 10.5, y = -0.55, label = "Cutoff: 11", color = "#B71C1C", size = 3.2, hjust = 0.5) + annotate("text", x = 5, y = 0.5, label = "0-10", color = "#2E7D32", size = 3.5, fontface = "italic") + annotate("text", x = 29, y = 0.5, label = "11-48", color = "#B71C1C", size = 3.5, fontface = "italic") + scale_x_continuous(limits = c(-3, 51), breaks = c(0, 10, 20, 30, 40, 48)) + scale_y_continuous(limits = c(-0.8, 2.0)) + theme_minimal(base_size = 12) + theme( axis.text.y = element_blank(), axis.ticks.y = element_blank(), panel.grid.major.y = element_blank(), panel.grid.minor = element_blank(), axis.title.y = element_blank(), plot.margin = margin(t = 5, r = 10, b = 5, l = 10) ) + labs(x = "OCI-12 Gesamtscore (0-48)", y = NULL) } # Datenaufbereitung #### OCI12_SUBSKALEN = c( ko = "Kontrollieren", or = "Ordnen", wa = "Waschen", zg = "Zwangsgedanken" ) OCI12_ITEMS = data.frame( nr = 1:12, var = c("oci_01_ko", "oci_02_or", "oci_03_wa", "oci_04_zg", "oci_05_ko", "oci_06_or", "oci_07_wa", "oci_08_zg", "oci_09_ko", "oci_10_or", "oci_11_wa", "oci_12_zg"), subskala = c("ko", "or", "wa", "zg", "ko", "or", "wa", "zg", "ko", "or", "wa", "zg"), text = c( "Ich kontrolliere Dinge öfter als nötig.", "Es stört mich, wenn Gegenstände nicht richtig angeordnet sind.", "Es fällt mir schwer, einen Gegenstand zu berühren, wenn ich weiß, dass er von fremden oder bestimmten Personen berührt wurde.", "Es fällt mir schwer, meine eigenen Gedanken zu kontrollieren.", "Ich kontrolliere immer wieder Türen, Fenster, Schubladen usw.", "Es belastet mich, wenn andere etwas daran ändern, wie ich die Dinge angeordnet habe.", "Manchmal muss ich mich waschen oder reinigen, einfach weil ich mich kontaminiert/beschmutzt fühle.", "Mich belasten unangenehme Gedanken, die mir gegen meinen Willen in den Sinn kommen.", "Ich kontrolliere immer wieder Gas- und Wasserhähne und Lichtschalter, nachdem ich sie abgedreht/ausgeschaltet habe.", "Für mich müssen Dinge in einer bestimmten Art und Weise angeordnet sein.", "Ich wasche mir die Hände öfter und länger als nötig.", "Ich habe oft schlechte Gedanken und habe Schwierigkeiten, sie loszuwerden." ), stringsAsFactors = FALSE ) OCI12_ITEMS$subskala_label = unname(OCI12_SUBSKALEN[OCI12_ITEMS$subskala]) # 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; } #download_word { background: #8B2635; color: white; border: none; font-weight: 600; padding: 8px 20px; border-radius: 4px; } #download_word:hover { background: #6d1e29; color: white; } .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; } .diagnose-box { border-radius: 6px; padding: 14px 18px; margin: 12px 0; border-left: 5px solid; } .diagnose-titel { font-weight: 700; font-size: 1.05rem; margin-bottom: 6px; } .diagnose-hinweis { font-size: 0.93em; line-height: 1.55; } .diagnose-disclaimer { font-size: 0.82em; color: #777; font-style: italic; margin-top: 10px; border-top: 1px solid rgba(0,0,0,.1); padding-top: 8px; } .oci-zone-unauffaellig { background: #E8F5E9; border-color: #A5D6A7; color: #2E7D32; } .oci-zone-auffaellig { background: #FFEBEE; border-color: #EF9A9A; color: #B71C1C; } .subskala-grid { display: flex; gap: 14px; flex-wrap: wrap; margin: 10px 0 4px; } .subskala-box { flex: 1; min-width: 140px; background: #FAFAFA; border-radius: 6px; padding: 12px 16px; } .subskala-label { font-weight: 600; color: #333; font-size: 0.9em; margin-bottom: 4px; } .subskala-wert { font-size: 1.4rem; font-weight: 800; color: #8B2635; } .subskala-leiste { background: #E0E0E0; border-radius: 4px; height: 8px; margin-top: 8px; overflow: hidden; } .subskala-leiste-fill { background: #8B2635; height: 100%; } .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: 26px; flex-shrink: 0; } .item-text { flex: 1; color: #333; font-size: 0.92em; } .item-subskala-tag { display: inline-block; font-size: 0.72em; font-weight: 700; color: #8B2635; background: #F5E6E8; border-radius: 3px; padding: 1px 6px; margin-bottom: 3px; } .stufe-badge { border-radius: 4px; padding: 2px 9px; font-weight: 700; font-size: 0.82em; white-space: nowrap; display: inline-block; flex-shrink: 0; } .stufe-badge-0 { background: #4CAF50; color: white; } .stufe-badge-1 { background: #AED581; color: #333333; } .stufe-badge-2 { background: #FFB300; color: #333333; } .stufe-badge-3 { background: #EF5350; color: white; } .stufe-badge-4 { background: #B71C1C; color: white; } .score-zahl { font-size: 2.2rem; font-weight: 800; color: #8B2635; } .cutoff-info { font-size: 0.88em; color: #555; margin-top: 4px; } " 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("OCI-12 – Obsessive Compulsive Inventory (Kurzversion)"), tags$p("Müller, Fink-Lamotte, Jelinek, Lohse, Ehring, Noll-Hussong, Berberich, Wahl-Kordon, Borgelt, McKay, Abramowitz, Abramovitch & Cludius, 2024") ), 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_oci12_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_disclaimer = fp_text(font.size = 9, italic = TRUE, color = "#777777") klass = erg$klass fp_klass_titel = fp_text(bold = TRUE, font.size = 12, color = klass$farbe_text, shading.color = klass$farbe_bg) fp_klass_text = fp_text(font.size = 11, color = klass$farbe_text, shading.color = klass$farbe_bg) doc = body_add_fpar(doc, fpar(ftext("OCI-12 – Einzelauswertung", fp_titel))) doc = body_add_fpar(doc, fpar( ftext("Chiffre: ", fp_label), ftext(erg$chiffre, fp_normal), ftext(" Ausfülldatum: ", fp_label), ftext(erg$datum_str, fp_normal) )) if (!is.null(erg$warnung)) { doc = body_add_fpar(doc, fpar( ftext(erg$warnung, fp_text(font.size = 10, italic = TRUE, color = "#BF360C")) )) } doc = body_add_par(doc, "", style = "Normal") doc = body_add_fpar(doc, fpar(ftext("Gesamtscore", fp_abschnitt))) doc = body_add_fpar(doc, fpar( ftext(paste0(erg$gesamt, " / 48 (Cutoff: 11)"), fp_klass_text) )) doc = body_add_par(doc, "", style = "Normal") doc = body_add_fpar(doc, fpar(ftext("Klassifikation", fp_abschnitt))) doc = body_add_fpar(doc, fpar(ftext(klass$titel, fp_klass_titel))) doc = body_add_fpar(doc, fpar(ftext(klass$text, fp_normal))) doc = body_add_par(doc, "", style = "Normal") doc = body_add_fpar(doc, fpar(ftext("Subskalen (deskriptiv, ohne Cutoff)", fp_abschnitt))) for (sk in names(OCI12_SUBSKALEN)) { doc = body_add_fpar(doc, fpar( ftext(paste0(OCI12_SUBSKALEN[[sk]], ": "), fp_label), ftext(paste0(erg$subskala_summen[[sk]], " / 12"), fp_normal) )) } doc = body_add_par(doc, "", style = "Normal") doc = body_add_fpar(doc, fpar(ftext("OCI-12 Einzelitems", fp_abschnitt))) for (i in seq_len(nrow(erg$items))) { it = erg$items[i, ] sk = if (!is.na(it$stufe) && it$stufe >= 0 && it$stufe <= 4) as.character(it$stufe) else "0" anker_txt = if (!is.na(it$anker)) it$anker else OCI12_STUFEN_TEXTE[as.integer(sk) + 1L] fp_badge = fp_text( color = OCI12_BADGE_TEXT_FARBEN[[sk]], bold = TRUE, shading.color = OCI12_BADGE_FARBEN[[sk]], font.size = 10 ) doc = body_add_fpar(doc, fpar( ftext(paste0(it$nr, ". [", it$subskala_label, "] ", it$text, " "), fp_normal), ftext(paste0(" ", it$stufe, " – ", anker_txt, " "), fp_badge) )) } doc = body_add_par(doc, "", style = "Normal") doc = body_add_fpar(doc, fpar(ftext(OCI12_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))) } }) # Skripte werden NICHT beim App-Start gesourct, nur beim Klick auf "Auswerten". ergebnis_r = eventReactive(input$btn_suchen, { chiffre_roh = input$chiffre if (is.null(chiffre_roh) || (nchar(trimws(input$pseudonym)) == 0 && nchar(trimws(chiffre_roh)) == 0)) { return(list(typ = "leere_eingabe", meldung = "Bitte eine Patientenchiffre eingeben.")) } chiffre = toupper(trimws(input$chiffre)) if (!(nchar(trimws(input$pseudonym)) > 0 || grepl("^[A-Z][0-9]{6}$", chiffre))) { return(list(typ = "format_fehler", chiffre = chiffre, meldung = paste0( "Ungültige Chiffre '", chiffre, "'. Erwartetes Format: ein Großbuchstabe ", "gefolgt von 6 Ziffern (z.B. P000123)."))) } if (!file.exists(PFAD_DOWNLOAD_SKRIPT)) { return(list(typ = "pfad_fehler", meldung = paste0("Download-Skript nicht gefunden unter:\n", PFAD_DOWNLOAD_SKRIPT))) } if (!file.exists(PFAD_PSEUDONYM_SKRIPT)) { return(list(typ = "pfad_fehler", meldung = paste0("Pseudonym-Skript nicht gefunden unter:\n", PFAD_PSEUDONYM_SKRIPT))) } ok_download = tryCatch({ source(PFAD_DOWNLOAD_SKRIPT, local = FALSE) list(ok = TRUE) }, error = function(e) list(ok = FALSE, msg = e$message)) if (!ok_download$ok) { return(list(typ = "skript_fehler", meldung = paste0("Fehler im Download-Skript: ", ok_download$msg))) } db_ordner = local({ ordner = dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT, mustWork = FALSE)) gefunden = NULL for (i in 1:5) { if (file.exists(file.path(ordner, "pseudonyme.db"))) { gefunden = ordner; break } elternteil = dirname(ordner) if (elternteil == ordner) break ordner = elternteil } gefunden }) alter_wd = getwd() wd_ziel = if (!is.null(db_ordner)) db_ordner else dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT, mustWork = FALSE)) on.exit(setwd(alter_wd), add = TRUE) setwd(wd_ziel) ok_pseudo = tryCatch({ 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_pseudo$ok) { return(list(typ = "skript_fehler", meldung = paste0("Fehler im Pseudonym-Skript: ", ok_pseudo$msg))) } if (!exists("daten_oci12", envir = .GlobalEnv)) { return(list(typ = "objekt_fehler", meldung = "Objekt 'daten_oci12' wurde nach dem Sourcen des Download-Skripts nicht gefunden.")) } if (!exists("pseudo", envir = .GlobalEnv)) { return(list(typ = "objekt_fehler", meldung = "Objekt 'pseudo' wurde nach dem Sourcen des Pseudonym-Skripts nicht gefunden.")) } daten = get("daten_oci12", envir = .GlobalEnv) pseudo_df = get("pseudo", envir = .GlobalEnv) treffer_pseudo = pseudo_df[pseudo_df$chiffre == chiffre, ] if (nrow(treffer_pseudo) == 0) { return(list(typ = "chiffre_unbekannt", meldung = paste0("Chiffre '", chiffre, "' wurde in der Pseudonym-Datenbank nicht gefunden."))) } alle_pseudonyme = unique(treffer_pseudo$pseudonym) if (nchar(trimws(input$pseudonym)) > 0) alle_pseudonyme = trimws(input$pseudonym) treffer_daten = daten[daten$session %in% alle_pseudonyme, ] if (nrow(treffer_daten) == 0) { return(list(typ = "kein_datensatz", meldung = paste0("Kein OCI-12-Datensatz für Chiffre '", chiffre, "' gefunden."))) } warnung = NULL if (nrow(treffer_daten) > 1) { # Zeitstempelspalte 'created': formr-Session-Erstellungszeitpunkt (Annahme aus # Schwesterapps uebernommen; mit echten Daten per str(daten_oci12) zu pruefen). treffer_daten = treffer_daten[order(treffer_daten$created, decreasing = TRUE), ] warnung = paste0( "Der Fragebogen wurde mehrfach ausgefüllt (", nrow(treffer_daten), " Einträge gefunden). Angezeigt wird die neueste Ausfüllung." ) treffer_daten = treffer_daten[1, , drop = FALSE] } zeile = treffer_daten[1, , drop = FALSE] # Ausfuelldatum aus 'created' (Annahme, siehe Kommentar oben); Fallback auf # Sys.Date(), falls die Spalte fehlt oder nicht parsebar ist. datum_str = tryCatch( format(as.POSIXct(zeile[["created"]][1]), "%d.%m.%Y"), error = function(e) format(Sys.Date(), "%d.%m.%Y") ) items = OCI12_ITEMS items$stufe = NA_integer_ items$anker = NA_character_ for (i in seq_len(nrow(items))) { var = items$var[i] items$stufe[i] = oci12_get_level(daten[[var]], zeile[[var]]) items$anker[i] = oci12_get_anker(daten[[var]], zeile[[var]]) } subskala_summen = sapply(names(OCI12_SUBSKALEN), function(sk) { sum(items$stufe[items$subskala == sk], na.rm = TRUE) }) names(subskala_summen) = names(OCI12_SUBSKALEN) gesamt = sum(subskala_summen, na.rm = TRUE) klass = oci12_klassifikation(gesamt) list( typ = "ok", chiffre = chiffre, datum_str = datum_str, warnung = warnung, items = items, subskala_summen = subskala_summen, gesamt = gesamt, klass = klass ) }) output$fehler_ui = renderUI({ req(input$btn_suchen) d = ergebnis_r() if (d$typ != "ok") div(class = "alert-fehler", d$meldung) else NULL }) output$warnung_ui = renderUI({ req(input$btn_suchen) d = ergebnis_r() if (d$typ != "ok" || is.null(d$warnung)) return(NULL) div(class = "alert-warnung", d$warnung) }) output$ergebnis_ui = renderUI({ req(input$btn_suchen) d = ergebnis_r() if (d$typ != "ok") return(NULL) klass = d$klass subskala_boxen = lapply(names(OCI12_SUBSKALEN), function(sk) { wert = d$subskala_summen[[sk]] pct = round(wert / 12 * 100) div(class = "subskala-box", div(class = "subskala-label", OCI12_SUBSKALEN[[sk]]), div(class = "subskala-wert", wert, tags$span(style = "font-size:0.8rem; color:#999; font-weight:400;", " / 12")), div(class = "subskala-leiste", div(class = "subskala-leiste-fill", style = paste0("width:", pct, "%;")) ) ) }) items_ui = lapply(seq_len(nrow(d$items)), function(i) { it = d$items[i, ] sk = if (!is.na(it$stufe) && it$stufe >= 0 && it$stufe <= 4) as.character(it$stufe) else "0" anker_txt = if (!is.na(it$anker)) it$anker else OCI12_STUFEN_TEXTE[as.integer(sk) + 1L] div(class = "item-zeile", div(class = "item-nr", paste0(it$nr, ".")), div(class = "item-text", div(class = "item-subskala-tag", it$subskala_label), it$text ), span(class = paste0("stufe-badge stufe-badge-", sk), paste0(it$stufe, " – ", anker_txt)) ) }) div(class = "abschnitt-karte", div(class = "abschnitt-titel", "OCI-12 Auswertung"), div(class = "meta-block", tags$strong("Chiffre: "), d$chiffre, tags$span(" | ", style = "color:#ccc;"), tags$strong("Ausfülldatum: "), d$datum_str ), tags$hr(), fluidRow( column(3, div( div(class = "score-zahl", d$gesamt), div("Gesamtscore (0–48)", style = "color:#555;"), div(class = "cutoff-info", tags$span(style = paste0("color:", klass$farbe_text, "; font-weight:600;"), klass$titel) ) ) ), column(9, plotOutput("gauge_plot", height = "160px")) ), tags$hr(), div(class = paste0("diagnose-box ", klass$css_klasse), div(class = "diagnose-titel", klass$titel), div(class = "diagnose-hinweis", klass$text), div(class = "diagnose-disclaimer", OCI12_DISCLAIMER) ), tags$hr(), tags$h5("Subskalen (deskriptiv, ohne Cutoff)"), div(class = "subskala-grid", subskala_boxen), tags$hr(), tags$h5("OCI-12 Einzelitems"), div(items_ui) ) }) output$gauge_plot = renderPlot({ req(input$btn_suchen) d = ergebnis_r() req(identical(d$typ, "ok")) make_gauge_oci12(d$gesamt) }, bg = "transparent") output$download_word = downloadHandler( filename = function() { d = tryCatch(ergebnis_r(), error = function(e) NULL) chiffre = if (is.list(d) && identical(d$typ, "ok")) d$chiffre else "export" # Datum aus dem Ausfuelldatum der Daten, NICHT Sys.Date() - Fallback nur bei Parsing-Fehler. datum = if (is.list(d) && identical(d$typ, "ok") && !is.null(d$datum_str)) tryCatch( format(as.Date(d$datum_str, "%d.%m.%Y"), "%Y%m%d"), error = function(e) format(Sys.Date(), "%Y%m%d") ) else format(Sys.Date(), "%Y%m%d") paste0("OCI12_", chiffre, "_", datum, ".docx") }, content = function(file) { d = tryCatch(ergebnis_r(), error = function(e) NULL) daten_ok = is.list(d) && identical(d$typ, "ok") if (!daten_ok) { doc = read_docx() doc = body_add_par(doc, "Kein Datensatz geladen. Bitte zuerst Chiffre eingeben und 'Auswerten' klicken.", style = "Normal") print(doc, target = file) return() } doc = tryCatch( erstelle_oci12_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)