# Präambel #### PFAD_DOWNLOAD_SKRIPT = "../API/get_data_bodyimage.R" PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R" AKZENT_FARBE = "#8B2635" BODYIMAGE_DISCLAIMER = paste0( "Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ", "keine klinische Diagnose. Die Interpretation obliegt der behandelnden Person. ", "Die Normierung gilt ausschliesslich fuer weibliche Patientinnen." ) FRAUEN_HINWEIS_TEXT = paste0( "Die hinterlegte Normtabelle gilt ausschließlich für weibliche Patientinnen. ", "Für männliche Patienten liegt keine Normierung vor." ) 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 #### # formr-Markdown-Sternchen stehen woertlich in Choice-Texten und Itemlabels. strip_stars = function(x) { if (is.null(x) || length(x) == 0) return(x) gsub("\\*\\*", "", as.character(x)) } # Liest fuer ein mc-Item (dbl+lbl) den Choice-Text ueber das labels-Attribut der # ORIGINAL-Spalte aus und ordnet ihm dann per fest vorgegebener Tabelle (skala_map, # Text -> Wert) den inhaltlichen Wert zu. Niemals wird der rohe formr-Code direkt # verwertet, da formr Choice-Reihenfolgen intern beliebig durchnumeriert. # Gibt sowohl den zugeordneten Wert als auch den Original-Choice-Text zurueck, damit # die UI/der Word-Export den Klartext anzeigen kann, ohne die Zuordnung zu wiederholen. melde_problem = function(warn_sammler, text) { assign("liste", c(get("liste", envir = warn_sammler), text), envir = warn_sammler) } hole_item_info = function(original_spalte, wert, skala_map, item_id, warn_sammler) { if (is.null(original_spalte)) { melde_problem(warn_sammler, paste0(item_id, ": Spalte nicht im Datensatz gefunden.")) return(list(wert = NA_real_, text = NA_character_)) } if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) { return(list(wert = NA_real_, text = NA_character_)) } lbl = attr(original_spalte, "labels") if (is.null(lbl) || length(lbl) == 0) { melde_problem(warn_sammler, paste0( item_id, ": keine labels im Export gefunden, Wert nicht bestimmbar.")) return(list(wert = NA_real_, text = NA_character_)) } pos = which(as.numeric(lbl) == as.numeric(wert[1])) if (length(pos) == 0) { melde_problem(warn_sammler, paste0( item_id, ": Rohwert ", wert[1], " nicht in labels-Attribut gefunden.")) return(list(wert = NA_real_, text = NA_character_)) } choice_text = trimws(strip_stars(names(lbl)[pos[1]])) if (!(choice_text %in% names(skala_map))) { melde_problem(warn_sammler, paste0( item_id, ": Choice-Text \"", choice_text, "\" nicht in erwarteter Recoding-Tabelle.")) return(list(wert = NA_real_, text = choice_text)) } list(wert = as.numeric(skala_map[[choice_text]]), text = choice_text) } hole_freitext = function(original_spalte, wert) { if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return("") trimws(strip_stars(as.character(wert[1]))) } leer = function(text) is.null(text) || length(text) == 0 || is.na(text) || trimws(text) == "" # Ordnet einem Antwortwert seine Position (0-basiert) innerhalb der zugehoerigen # Skala zu (skala_map-Werte sind immer eine luecklose Ganzzahlfolge ab min()). # Position bestimmt ausschliesslich die Badge-Farbe, keine inhaltliche Wertung. item_badge_position = function(wert, skala_map) { if (is.null(wert) || length(wert) == 0 || is.na(wert)) return(NA_integer_) as.integer(round(wert - min(skala_map))) } sortiere_nach_wert = function(items, feld = "wert") { werte = sapply(items, `[[`, feld) items[order(is.na(werte), -werte)] } # Ordnet einen Summenscore anhand der 5 fest vorgegebenen Baendern (min, max, stufe) # einer Stufe zu. baender ist ein data.frame mit Spalten min, max, stufe (in dieser # Reihenfolge sehr_gering < gering < normal < hoch < sehr_hoch). klassifiziere_stufe = function(score, baender) { if (is.null(score) || length(score) == 0 || is.na(score)) { return(list(stufe = NA_character_, label = "nicht berechenbar", bereich = "")) } treffer = baender[score >= baender$min & score <= baender$max, ] if (nrow(treffer) == 0) { if (score < min(baender$min)) treffer = baender[1, ] else treffer = baender[nrow(baender), ] } list( stufe = treffer$stufe[1], label = STUFEN_LABEL[[treffer$stufe[1]]], bereich = paste0(treffer$min[1], "-", treffer$max[1]) ) } stufe_css_klasse = function(stufe) { if (is.na(stufe) || is.null(stufe)) return("stufe-badge-normal") paste0("stufe-badge-", gsub("_", "-", stufe)) } summe_score = function(werte) { if (any(is.na(werte))) return(NA_real_) sum(werte) } sichere_spalte = function(df, col) { if (is.null(df) || !(col %in% names(df))) return(NULL) df[[col]] } # Berechnet FB1 (Koerperzonen-Zufriedenheit): Items 01-08 gehen in den Summenscore ein, # Items 09/10 sind reine Freitext+Rating-Zusatzangaben (nicht im Score). berechne_fb1 = function(zeile, daten, warn_env) { items = lapply(sprintf("%02d", 1:8), function(nr) { col = paste0("bi_fb1_", nr) info = hole_item_info(sichere_spalte(daten, col), sichere_spalte(zeile, col), FB1_SKALA, paste0("FB1 Item ", nr), warn_env) list(nr = nr, text = FB1_ITEMS[[nr]], wert = info$wert, antwort = info$text, badge = item_badge_position(info$wert, FB1_SKALA)) }) score = summe_score(sapply(items, `[[`, "wert")) hole_zusatz = function(n) { col_text = paste0("bi_fb1_", n, "_text") col_rating = paste0("bi_fb1_", n, "_rating") text = hole_freitext(sichere_spalte(daten, col_text), sichere_spalte(zeile, col_text)) rating = hole_item_info(sichere_spalte(daten, col_rating), sichere_spalte(zeile, col_rating), FB1_SKALA, paste0("FB1 Item ", n, " Rating"), warn_env) list(text = text, rating_wert = rating$wert, rating_text = rating$text) } list(items = items, score = score, zusatz_09 = hole_zusatz("09"), zusatz_10 = hole_zusatz("10")) } # Berechnet FB2 (Wunschtraum): pro Attribut Produkt aus Teilfrage A (Ist-Ideal- # Abweichung) und B (Wichtigkeit), Summe der 10 Produkte ergibt den Score. berechne_fb2 = function(zeile, daten, warn_env) { items = lapply(sprintf("%02d", 1:10), function(nr) { col_a = paste0("bi_fb2_", nr, "_a") col_b = paste0("bi_fb2_", nr, "_b") info_a = hole_item_info(sichere_spalte(daten, col_a), sichere_spalte(zeile, col_a), FB2A_SKALA, paste0("FB2 Item ", nr, "a"), warn_env) info_b = hole_item_info(sichere_spalte(daten, col_b), sichere_spalte(zeile, col_b), FB2B_SKALA, paste0("FB2 Item ", nr, "b"), warn_env) produkt = if (is.na(info_a$wert) || is.na(info_b$wert)) NA_real_ else info_a$wert * info_b$wert list(nr = nr, attribut = FB2_ATTRIBUTE[[nr]], a_wert = info_a$wert, a_text = info_a$text, a_badge = item_badge_position(info_a$wert, FB2A_SKALA), b_wert = info_b$wert, b_text = info_b$text, b_badge = item_badge_position(info_b$wert, FB2B_SKALA), produkt = produkt) }) score = summe_score(sapply(items, `[[`, "produkt")) list(items = items, score = score) } # Berechnet FB3 (belastende Situationen): Items 01-48 gehen in den Summenscore ein, # Items 43-48 haben zusaetzliche Klaerfelder (Text), die den Score nicht beeinflussen. # Items 49/50 (inkl. Ratings) sind komplett ausgeschlossen, rein qualitativ. berechne_fb3 = function(zeile, daten, warn_env) { items = lapply(sprintf("%02d", 1:48), function(nr) { col = paste0("bi_fb3_", nr) info = hole_item_info(sichere_spalte(daten, col), sichere_spalte(zeile, col), FB3_SKALA, paste0("FB3 Item ", nr), warn_env) klaerfeld = NULL if (nr %in% names(FB3_KLAERFELD_LABEL)) { col_t = paste0("bi_fb3_", nr, "_text") txt = hole_freitext(sichere_spalte(daten, col_t), sichere_spalte(zeile, col_t)) if (!leer(txt)) klaerfeld = paste0(FB3_KLAERFELD_LABEL[[nr]], " ", txt) } list(nr = nr, text = FB3_ITEMS[[nr]], wert = info$wert, antwort = info$text, badge = item_badge_position(info$wert, FB3_SKALA), klaerfeld = klaerfeld) }) score = summe_score(sapply(items, `[[`, "wert")) hole_zusatz = function(n) { col_text = paste0("bi_fb3_", n, "_text") col_rate = paste0("bi_fb3_", n) text = hole_freitext(sichere_spalte(daten, col_text), sichere_spalte(zeile, col_text)) rating = hole_item_info(sichere_spalte(daten, col_rate), sichere_spalte(zeile, col_rate), FB3_SKALA, paste0("FB3 Item ", n, " (Zusatz)"), warn_env) list(text = text, rating_wert = rating$wert, rating_text = rating$text) } list(items = items, score = score, zusatz_49 = hole_zusatz("49"), zusatz_50 = hole_zusatz("50")) } # FB4 negativ: Items 01-30 im Score, 31/32 Freitext+Rating komplett ausgeschlossen. berechne_fb4_neg = function(zeile, daten, warn_env) { items = lapply(sprintf("%02d", 1:30), function(nr) { col = paste0("bi_fb4_neg_", nr) info = hole_item_info(sichere_spalte(daten, col), sichere_spalte(zeile, col), FB4_SKALA, paste0("FB4 negativ Item ", nr), warn_env) list(nr = nr, text = FB4_NEG_ITEMS[[nr]], wert = info$wert, antwort = info$text, badge = item_badge_position(info$wert, FB4_SKALA)) }) score = summe_score(sapply(items, `[[`, "wert")) hole_zusatz = function(n) { col_text = paste0("bi_fb4_neg_", n, "_text") col_rate = paste0("bi_fb4_neg_", n) text = hole_freitext(sichere_spalte(daten, col_text), sichere_spalte(zeile, col_text)) rating = hole_item_info(sichere_spalte(daten, col_rate), sichere_spalte(zeile, col_rate), FB4_SKALA, paste0("FB4 negativ Item ", n, " (Zusatz)"), warn_env) list(text = text, rating_wert = rating$wert, rating_text = rating$text) } list(items = items, score = score, zusatz_31 = hole_zusatz("31"), zusatz_32 = hole_zusatz("32")) } # FB4 positiv: Items 01-15 im Score, 16/17 Freitext+Rating komplett ausgeschlossen. berechne_fb4_pos = function(zeile, daten, warn_env) { items = lapply(sprintf("%02d", 1:15), function(nr) { col = paste0("bi_fb4_pos_", nr) info = hole_item_info(sichere_spalte(daten, col), sichere_spalte(zeile, col), FB4_SKALA, paste0("FB4 positiv Item ", nr), warn_env) list(nr = nr, text = FB4_POS_ITEMS[[nr]], wert = info$wert, antwort = info$text, badge = item_badge_position(info$wert, FB4_SKALA)) }) score = summe_score(sapply(items, `[[`, "wert")) hole_zusatz = function(n) { col_text = paste0("bi_fb4_pos_", n, "_text") col_rate = paste0("bi_fb4_pos_", n) text = hole_freitext(sichere_spalte(daten, col_text), sichere_spalte(zeile, col_text)) rating = hole_item_info(sichere_spalte(daten, col_rate), sichere_spalte(zeile, col_rate), FB4_SKALA, paste0("FB4 positiv Item ", n, " (Zusatz)"), warn_env) list(text = text, rating_wert = rating$wert, rating_text = rating$text) } list(items = items, score = score, zusatz_16 = hole_zusatz("16"), zusatz_17 = hole_zusatz("17")) } # FB5: 44 Items, 4 Subskalen ueber wortwoertliche Additions-/Subtraktionsformeln # aus dem Manual (keine Reverse-Scoring-Umformung pro Einzelitem). berechne_fb5 = function(zeile, daten, warn_env) { items = lapply(sprintf("%02d", 1:44), function(nr) { col = paste0("bi_fb5_", nr) info = hole_item_info(sichere_spalte(daten, col), sichere_spalte(zeile, col), FB5_SKALA, paste0("FB5 Item ", nr), warn_env) list(nr = nr, text = FB5_ITEMS[[nr]], wert = info$wert, antwort = info$text, badge = item_badge_position(info$wert, FB5_SKALA)) }) werte = setNames(sapply(items, `[[`, "wert"), sapply(items, `[[`, "nr")) formel = function(pos_range, neg_range, konstante) { pos_summe = summe_score(werte[sprintf("%02d", pos_range)]) neg_summe = summe_score(werte[sprintf("%02d", neg_range)]) if (is.na(pos_summe) || is.na(neg_summe)) return(NA_real_) pos_summe - neg_summe + konstante } list( items = items, A = formel(1:5, 6:7, 12), B = formel(8:15, 16:19, 24), C = formel(20:27, 28:30, 18), D = formel(31:39, 40:44, 30) ) } make_gauge = function(score, baender, akzent, x_label) { baender$stufe = factor(baender$stufe, levels = names(STUFEN_LABEL)) bereich_min = min(baender$min) bereich_max = max(baender$max) # Zonenbreiten sind ungleich (z.B. FB1 "gering" nur 3 Punkte breit) - Beschriftungen # innerhalb der Rechtecke wuerden dort ueberlappen. Stattdessen Farbzuordnung ueber # eine gemeinsame Legende unter dem Plot, unabhaengig von der Zonenbreite lesbar. p = ggplot() + geom_rect(data = baender, aes(xmin = min, xmax = max, ymin = 0, ymax = 1, fill = stufe), color = "white", linewidth = 0.6) + scale_fill_manual(values = STUFEN_FARBEN, breaks = names(STUFEN_LABEL), labels = STUFEN_LABEL, name = NULL, drop = FALSE) + scale_x_continuous(limits = c(bereich_min, bereich_max), breaks = c(bereich_min, bereich_max)) + scale_y_continuous(limits = c(-0.05, 1.05)) + guides(fill = guide_legend(nrow = 1, byrow = TRUE, override.aes = list(linewidth = 0))) + theme_minimal(base_size = 8) + theme( axis.text.y = element_blank(), axis.ticks.y = element_blank(), panel.grid = element_blank(), axis.title.y = element_blank(), axis.title.x = element_text(size = 8, color = "#555555"), axis.text.x = element_text(size = 6.5, color = "#666666"), legend.position = "bottom", legend.text = element_text(size = 6.5), legend.key.size = unit(7, "pt"), legend.margin = margin(t = -6), plot.margin = margin(t = 5, r = 10, b = 2, l = 10) ) + labs(x = x_label, y = NULL) # Kein zusaetzliches Textlabel auf dem Plot: der Zahlenwert steht bereits gross # links neben dem Gauge (score-zahl). Ein floating geom_label ueber der Zone # braucht Vertikal-Headroom, der bei knapper Panelhoehe abgeschnitten wird - # daher hier nur eine schlanke Markierungslinie im Zonenbalken selbst. if (!is.null(score) && !is.na(score)) { p = p + geom_segment(aes(x = score, xend = score, y = 0, yend = 1), color = akzent, linewidth = 2, lineend = "round") } p } baue_score_karte = function(score_key, score_wert, gauge_id, item_rows, zusatz_uis = list()) { meta = SCORE_META[[score_key]] klass = klassifiziere_stufe(score_wert, meta$baender) div(class = "abschnitt-karte", div(class = "abschnitt-titel", meta$titel), fluidRow( column(3, div(class = "score-zahl", if (!is.na(score_wert)) score_wert else "-"), div(style = "color:#555; font-size:0.85em;", paste0("Range: ", meta$range)), span(class = paste0("stufe-badge ", stufe_css_klasse(klass$stufe)), klass$label) ), column(9, plotOutput(gauge_id, height = "135px")) ), tags$hr(), div(item_rows), if (length(zusatz_uis) > 0) tagList(tags$hr(), zusatz_uis) ) } item_badge_ui = function(wert, position) { bk = if (is.null(position) || is.na(position)) "na" else as.character(position) span(class = paste0("item-badge item-badge-", bk), if (is.na(wert)) "?" else wert) } item_zeile_ui = function(nr, text, antwort_text, wert, badge = NA_integer_, zusatz = NULL) { antwort_anzeige = if (is.na(wert)) { tags$span(style = "color:#999; font-style:italic;", "keine Angabe") } else { tagList( tags$span(style = "color:#555; margin-right:8px;", if (!is.na(antwort_text)) antwort_text else ""), item_badge_ui(wert, badge) ) } div(class = "item-zeile", div(class = "item-nr", paste0(nr, ".")), div(class = "item-text", text, zusatz), div(style = "min-width: 260px; text-align:right;", antwort_anzeige) ) } fb2_item_zeile_ui = function(it) { antwort_anzeige = if (is.na(it$a_wert) || is.na(it$b_wert)) { tags$span(style = "color:#999; font-style:italic;", "keine Angabe") } else { tags$span(style = "font-size:0.88em;", tags$span(style = "color:#555;", paste0("A: ", it$a_text, " ")), item_badge_ui(it$a_wert, it$a_badge), tags$span(style = "color:#555; margin: 0 6px;", paste0(" | B: ", it$b_text, " ")), item_badge_ui(it$b_wert, it$b_badge), tags$span(style = "color:#555; margin-left:6px;", paste0(" = ", it$produkt)) ) } div(class = "item-zeile", div(class = "item-nr", paste0(it$nr, ".")), div(class = "item-text", it$attribut), div(style = "min-width: 400px; text-align:right;", antwort_anzeige) ) } freitext_block_ui = function(titel, freitext, rating_text, rating_wert) { div(class = "abschnitt-karte", style = "background:#FAFAFA;", tags$div(style = paste0("color:", AKZENT_FARBE, "; font-weight:700; margin-bottom:4px;"), titel), tags$div(style = "color:#333; margin-bottom:4px;", tags$em(freitext)), if (!is.na(rating_wert)) tags$div(style = "color:#555;", paste0("Einordnung: ", rating_text, " (", rating_wert, ")")), tags$div(style = "color:#888; font-size:0.82em; font-style:italic; margin-top:4px;", "Zusätzliche qualitative Angabe, nicht im Summenscore enthalten.") ) } # Datenaufbereitung #### STUFEN_LABEL = c( sehr_gering = "sehr gering", gering = "gering", normal = "normal", hoch = "hoch", sehr_hoch = "sehr hoch" ) # Badge-Farben fuer Einzelitem-Antworten (Position 0-4 innerhalb der jeweiligen # Skala), analog zum bestehenden Farbverlauf gruen->dunkelrot in Schwester-Apps. # 4-stufige Skalen (FB2 A/B, Werte 0-3) nutzen nur die Positionen 0-3. ITEM_BADGE_FARBEN = c( "0" = "#4CAF50", "1" = "#F48FB1", "2" = "#EF5350", "3" = "#B71C1C", "4" = "#4A0000" ) ITEM_BADGE_TEXT_FARBEN = c( "0" = "white", "1" = "#333333", "2" = "white", "3" = "white", "4" = "white" ) # Gauge-Zonen und Klassifikations-Badges nutzen denselben Farbverlauf wie die # Einzelitem-Badges (ITEM_BADGE_FARBEN), damit beide Darstellungen konsistent sind. STUFEN_FARBEN = setNames(ITEM_BADGE_FARBEN[c("0","1","2","3","4")], names(STUFEN_LABEL)) STUFEN_TEXT_FARBEN = setNames(ITEM_BADGE_TEXT_FARBEN[c("0","1","2","3","4")], names(STUFEN_LABEL)) STUFEN_WORD_FARBEN = list( sehr_gering = list(bg = "#E3F2FD", text = "#0D47A1"), gering = list(bg = "#E1F5FE", text = "#01579B"), normal = list(bg = "#E8F5E9", text = "#2E7D32"), hoch = list(bg = "#FFF3E0", text = "#E65100"), sehr_hoch = list(bg = "#FBE9E7", text = "#BF360C") ) # FB1 ---- FB1_ITEMS = c( "01" = "Gesicht (Gesichtszüge, Hautbeschaffenheit)", "02" = "Haar (Farbe, Dicke, Struktur)", "03" = "Unterkörper (Gesäß, Hüften, Oberschenkel, Beine)", "04" = "Körperstamm (Taille, Bauch)", "05" = "Oberkörper (Brüste bzw. Brust, Schultern, Arme)", "06" = "Muskulatur", "07" = "Gewicht", "08" = "Größe" ) FB1_SKALA = c( "sehr unzufrieden" = 1, "meistens unzufrieden" = 2, "weder unzufrieden noch zufrieden" = 3, "meistens zufrieden" = 4, "sehr zufrieden" = 5 ) # Tippfehler im Original-Arbeitsblatt "33 - 402" fuer FB1/sehr hoch stillschweigend # zu "33-40" korrigiert (Range von FB1 ist 8-40, 402 ist ausserhalb jeder moeglichen # Itemsumme und damit ein offensichtlicher Tippfehler). FB1_BAENDER = data.frame( min = c(8, 23, 26, 28, 33), max = c(22, 25, 27, 32, 40), stufe = c("sehr_gering", "gering", "normal", "hoch", "sehr_hoch"), stringsAsFactors = FALSE ) # FB2 ---- FB2_ATTRIBUTE = c( "01" = "Größe", "02" = "Haut", "03" = "Haarfarbe", "04" = "Haarstruktur und -länge", "05" = "Gesichtszüge", "06" = "Muskulatur", "07" = "Körperproportionen", "08" = "Gewicht", "09" = "Körperkraft", "10" = "Geschicklichkeit" ) FB2A_SKALA = c( "genau so wie ich bin" = 0, "annähernd so wie ich bin" = 1, "ziemlich anders als bei mir" = 2, "völlig anders als bei mir" = 3 ) FB2B_SKALA = c( "nicht wichtig" = 0, "etwas wichtig" = 1, "ziemlich wichtig" = 2, "sehr wichtig" = 3 ) FB2_BAENDER = data.frame( min = c(0, 9, 18, 27, 51), max = c(8, 17, 26, 50, 90), stufe = c("sehr_gering", "gering", "normal", "hoch", "sehr_hoch"), stringsAsFactors = FALSE ) # FB3 ---- FB3_ITEMS = c( "01" = "In sozialen Situationen, in denen ich nur wenige Menschen kenne?", "02" = "Wenn ich im Mittelpunkt der Aufmerksamkeit stehe?", "03" = "Wenn Menschen mich sehen, bevor ich mich zurecht gemacht habe?", "04" = "Wenn ich mit attraktiven Menschen meines Geschlechts zusammen bin?", "05" = "Wenn ich mit attraktiven Menschen des anderen Geschlechts zusammen bin?", "06" = "Wenn jemand auf Teile meines Körpers schaut, die ich nicht mag?", "07" = "Wenn Menschen mich aus einem bestimmten Winkel betrachten?", "08" = "Wenn mir jemand Komplimente macht?", "09" = "Wenn ich das Gefühl habe, abgelehnt oder ignoriert zu werden?", "10" = "Wenn das Gesprächsthema sich um das Aussehen dreht?", "11" = "Wenn sich jemand ablehnend über mein Aussehen äußert?", "12" = "Wenn jemand anderes Komplimente bekommt und zu mir nichts gesagt wird?", "13" = "Wenn ich höre, dass das Aussehen Anderer kritisiert wird?", "14" = "Wenn ich mich daran erinnere, dass Andere scherzhafte oder unfreundliche Dinge über meine Erscheinung gesagt haben?", "15" = "Wenn ich mit Anderen zusammen bin, die über Diäten oder Gewicht reden?", "16" = "Wenn ich attraktive Menschen im Fernsehen oder in Zeitschriften sehe?", "17" = "Wenn ich in einem Geschäft Kleidung anprobiere?", "18" = "Wenn ich \"gewagte\" Kleidung trage?", "19" = "Wenn ich an einem sozialen Ereignis anders als die Anderen gekleidet bin?", "20" = "Wenn meine Kleidung nicht richtig sitzt?", "21" = "Wenn ich eine neue Frisur habe?", "22" = "Wenn ich nicht geschminkt bin (für Frauen)?", "23" = "Wenn meine Frisur nicht richtig sitzt?", "24" = "Wenn mein Partner nicht bemerkt, dass ich mich schön gemacht habe?", "25" = "Wenn ich mich im Spiegel anschaue?", "26" = "Wenn ich im Spiegel meinen nackten Körper anschaue?", "27" = "Wenn ich mich auf einem Foto oder Video sehe?", "28" = "Wenn ich fotografiert werde?", "29" = "Wenn ich nicht so viel Sport gemacht habe wie gewöhnlich?", "30" = "Wenn ich Sport mache?", "31" = "Wenn ich eine ganze Mahlzeit gegessen habe?", "32" = "Wenn ich auf der Waage stehe?", "33" = "Wenn ich das Gefühl habe, zugenommen zu haben?", "34" = "Wenn ich das Gefühl habe, abgenommen zu haben?", "35" = "Wenn ich wegen etwas anderem sowieso schon schlecht gelaunt bin?", "36" = "Wenn ich daran denke, wie ich früher ausgesehen habe?", "37" = "Wenn ich daran denke, wie ich gern aussehen würde?", "38" = "Wenn ich daran denke, wie ich in der Zukunft aussehen werde?", "39" = "Wenn ich mir vorstelle, eine sexuelle Beziehung zu haben?", "40" = "Wenn mein Partner mich unbekleidet sieht?", "41" = "Wenn mein Partner mich an Stellen berührt, die ich an mir nicht mag?", "42" = "Wenn mein Partner kein sexuelles Interesse an mir zeigt?", "43" = "Wenn ich mit einer bestimmten Person zusammen bin?", "44" = "Zu einer bestimmten Tageszeit?", "45" = "Während einer bestimmten Zeit im Monat?", "46" = "Während einer bestimmten Zeit im Jahr?", "47" = "Während bestimmter erholsamer Aktivitäten?", "48" = "Wenn ich bestimmte Lebensmittel esse?", "49" = "Andere schwierige Situation", "50" = "Andere schwierige Situation" ) FB3_KLAERFELD_LABEL = c( "43" = "Mit wem?", "44" = "Wann?", "45" = "Wann?", "46" = "Wann?", "47" = "Welche?", "48" = "Welche?" ) FB3_SKALA = c( "niemals" = 0, "manchmal" = 1, "ziemlich häufig" = 2, "häufig" = 3, "fast immer" = 4 ) FB3_BAENDER = data.frame( min = c(0, 51, 73, 81, 111), max = c(50, 72, 80, 110, 192), stufe = c("sehr_gering", "gering", "normal", "hoch", "sehr_hoch"), stringsAsFactors = FALSE ) # FB4 ---- FB4_NEG_ITEMS = c( "01" = "Mein Leben ist elend aufgrund meines Aussehens.", "02" = "Mein Äußeres macht mich zu einem Niemand.", "03" = "Ich sehe nicht gut genug aus, um in dieser (einer bestimmten) Situation zu sein.", "04" = "Warum kann ich nicht besser aussehen?", "05" = "Es ist nicht fair, dass ich so aussehe.", "06" = "So, wie ich aussehe, wird mich nie jemand lieben.", "07" = "Ich wünschte, ich würde besser aussehen.", "08" = "Ich muss unbedingt abnehmen.", "09" = "Die Anderen denken, ich bin fett.", "10" = "Die Anderen lachen über mein Aussehen.", "11" = "Ich bin nicht attraktiv.", "12" = "Ich wünschte, ich sähe wie jemand anderes aus.", "13" = "Andere werden mich aufgrund meines Äußeren nicht mögen.", "14" = "Ich werde nie attraktiv sein.", "15" = "Ich hasse meinen Körper.", "16" = "Irgend etwas muss mit meinem Aussehen passieren.", "17" = "Wie ich aussehe, ruiniert mir alles.", "18" = "Ich sehe nie so aus, wie ich gern möchte.", "19" = "Ich bin so enttäuscht über meine Erscheinung.", "20" = "Ich fühle mich unattraktiv, daher muss etwas mit meinem Aussehen nicht in Ordnung sein.", "21" = "Ich wünschte, ich würde mir nicht so viele Sorgen um mein Aussehen machen.", "22" = "Andere Menschen bemerken sofort, was mit meinem Körper nicht in Ordnung ist.", "23" = "Die Menschen denken, ich bin unattraktiv.", "24" = "Die Anderen sehen besser aus als ich.", "25" = "Besonders wenn ich mit attraktiven Menschen zusammen bin, denke ich, dass ich hässlich bin.", "26" = "Ich kann keine modischen Kleider tragen.", "27" = "Mein Körper braucht mehr Konturen.", "28" = "Meine Kleider passen nicht richtig.", "29" = "Ich wünschte, andere würden mich nicht anschauen.", "30" = "Ich kann mein Äußeres nicht mehr ertragen.", "31" = "Andere häufige negative Gedanken", "32" = "Andere häufige negative Gedanken" ) FB4_POS_ITEMS = c( "01" = "Andere denken, ich sehe gut aus.", "02" = "Mein Aussehen hilft mir, selbstbewusster zu sein.", "03" = "Ich bin stolz auf meinen Körper.", "04" = "Mein Körper hat gute Proportionen.", "05" = "Mein Äußeres scheint mir, gesellschaftlich hilfreich zu sein.", "06" = "Ich mag die Art, wie ich aussehe.", "07" = "Ich empfinde mich auch dann noch attraktiv, wenn ich mit schöneren Menschen zusammen bin.", "08" = "Eigentlich sehe ich genauso gut aus, wie die meisten Anderen.", "09" = "Es kümmert mich nicht, wenn Andere mich anschauen.", "10" = "Ich bin mit meinem Aussehen zufrieden.", "11" = "Ich sehe gesund aus.", "12" = "Ich mag mich im Badeanzug.", "13" = "Diese Kleider stehen mir gut.", "14" = "Mein Körper ist nicht perfekt, aber ich denke, er ist attraktiv.", "15" = "Ich muss mein Aussehen nicht verändern.", "16" = "Andere häufige positive Gedanken", "17" = "Andere häufige positive Gedanken" ) FB4_SKALA = FB3_SKALA FB4_NEG_BAENDER = data.frame( min = c(0, 9, 18, 22, 40), max = c(8, 17, 21, 39, 120), stufe = c("sehr_gering", "gering", "normal", "hoch", "sehr_hoch"), stringsAsFactors = FALSE ) FB4_POS_BAENDER = data.frame( min = c(0, 17, 27, 33, 40), max = c(16, 26, 32, 39, 60), stufe = c("sehr_gering", "gering", "normal", "hoch", "sehr_hoch"), stringsAsFactors = FALSE ) # FB5 ---- FB5_ITEMS = c( "01" = "Mein Körper ist sexuell ansprechend.", "02" = "Ich mag mein Aussehen, wie es ist.", "03" = "Die meisten Menschen würden mich als gut aussehend bezeichnen.", "04" = "Ich mag mich ohne Kleider.", "05" = "Ich mag die Art, wie meine Kleidungsstücke sitzen.", "06" = "Ich mag meinen Körper nicht.", "07" = "Ich bin körperlich unattraktiv.", "08" = "Bevor ich mich in der Öffentlichkeit zeige, prüfe ich immer, wie ich aussehe.", "09" = "Ich achte bei neuer Kleidung immer darauf, dass sie möglichst vorteilhaft wirkt.", "10" = "Ich prüfe mein Aussehen im Spiegel, wann immer es möglich ist.", "11" = "Wenn ich ausgehe, brauche ich viel Zeit, um mich zurecht zu machen.", "12" = "Es ist wichtig, dass ich immer gut aussehe.", "13" = "Ich bin gehemmt, wenn meine Aufmachung nicht stimmt.", "14" = "Ich gebe mir besondere Mühe mit meiner Frisur.", "15" = "Ich versuche immer, meine körperliche Erscheinung zu verbessern.", "16" = "Was ich gewöhnlich trage, ist praktisch, egal wie es aussieht.", "17" = "Ich mache mir keine Gedanken darüber, was Andere über mein Aussehen denken.", "18" = "Ich denke nie über mein Aussehen nach.", "19" = "Ich brauche nur wenige Pflegeprodukte.", "20" = "Ich kann leicht körperliche Fertigkeiten erlernen.", "21" = "Ich bin geschickt.", "22" = "Ich kümmere mich um meine Gesundheit.", "23" = "Ich bin selten krank.", "24" = "Ich habe das Vertrauen in meinen Körper verloren.", "25" = "Ich bin körperlich gesund.", "26" = "Ich würde die meisten Fitness-Tests bestehen.", "27" = "Meine körperliche Ausdauer ist gut.", "28" = "Mit meiner Gesundheit geht es ständig auf und ab.", "29" = "Ich bin unsportlich.", "30" = "Ich fühle mich oft anfällig für Krankheiten.", "31" = "Ich kenne eine Menge Dinge, die meine Gesundheit beeinträchtigen.", "32" = "Ich habe einen gesunden Lebensstil entwickelt.", "33" = "Gute Gesundheit ist eines der wichtigsten Dinge in meinem Leben.", "34" = "Ich tue nichts, was meine Gesundheit beeinträchtigen könnte.", "35" = "Ich arbeite daran, meine körperliche Kraft zu verbessern.", "36" = "Ich lese oft Bücher oder Zeitschriften, die sich mit Gesundheit beschäftigen.", "37" = "Ich arbeite daran, meine körperliche Ausdauer zu verbessern.", "38" = "Ich versuche, körperlich aktiv zu sein.", "39" = "Ich weiß eine Menge über körperliche Fitness.", "40" = "Körperlich fit zu sein, ist hat die erste Priorität in meinem Leben.", "41" = "Ich führe kein regelmäßiges Fitness-Programm durch.", "42" = "Ich halte meine Gesundheit für selbstverständlich.", "43" = "Ich bemühe mich, eine ausgewogene und nahrhafte Diät einzuhalten.", "44" = "Ich kümmere mich nicht darum, meine körperlichen Fähigkeiten zu verbessern." ) FB5_SKALA = c( "trifft nicht zu" = 1, "trifft kaum zu" = 2, "weder noch" = 3, "trifft größtenteils zu" = 4, "trifft voll zu" = 5 ) # Gegenlaeufige Items je Subskala (fliessen mit Minuszeichen in die Formel ein). # Item 24 ist inhaltlich negativ formuliert, zaehlt aber laut Manual-Formel zum # Plus-Block 20-27 der Subskala C - hier bewusst NICHT als gegenlaeufig markiert. FB5_GEGENLAEUFIG = sprintf("%02d", c(6, 7, 16, 17, 18, 19, 28, 29, 30, 40, 41, 42, 43, 44)) FB5_A_BAENDER = data.frame( min = c(7, 18, 24, 26, 30), max = c(17, 23, 25, 29, 35), stufe = c("sehr_gering", "gering", "normal", "hoch", "sehr_hoch"), stringsAsFactors = FALSE) FB5_B_BAENDER = data.frame( min = c(12, 41, 47, 49, 54), max = c(40, 46, 48, 53, 60), stufe = c("sehr_gering", "gering", "normal", "hoch", "sehr_hoch"), stringsAsFactors = FALSE) FB5_C_BAENDER = data.frame( min = c(11, 34, 41, 43, 48), max = c(33, 40, 42, 47, 55), stufe = c("sehr_gering", "gering", "normal", "hoch", "sehr_hoch"), stringsAsFactors = FALSE) FB5_D_BAENDER = data.frame( min = c(14, 42, 50, 53, 60), max = c(41, 49, 52, 59, 70), stufe = c("sehr_gering", "gering", "normal", "hoch", "sehr_hoch"), stringsAsFactors = FALSE) SCORE_META = list( FB1 = list(titel = "FB1 - Körperzonen-Zufriedenheit", baender = FB1_BAENDER, range = "8-40"), FB2 = list(titel = "FB2 - Wunschtraum-Diskrepanz", baender = FB2_BAENDER, range = "0-90"), FB3 = list(titel = "FB3 - Belastende Situationen", baender = FB3_BAENDER, range = "0-192"), FB4_neg = list(titel = "FB4 - Negative Gedanken", baender = FB4_NEG_BAENDER, range = "0-120"), FB4_pos = list(titel = "FB4 - Positive Gedanken", baender = FB4_POS_BAENDER, range = "0-60"), FB5_A = list(titel = "FB5A - Beurteilung des Aussehens", baender = FB5_A_BAENDER, range = "7-35"), FB5_B = list(titel = "FB5B - Investitionen in das Aussehen", baender = FB5_B_BAENDER, range = "12-60"), FB5_C = list(titel = "FB5C - Beurteilung der Fitness und Gesundheit", baender = FB5_C_BAENDER, range = "11-55"), FB5_D = list(titel = "FB5D - Investition in Fitness und Gesundheit", baender = FB5_D_BAENDER, range = "14-70") ) SCORE_REIHENFOLGE = c("FB1", "FB2", "FB3", "FB4_neg", "FB4_pos", "FB5_A", "FB5_B", "FB5_C", "FB5_D") # UI #### app_css = " body { font-family: 'Segoe UI', Helvetica, Arial, sans-serif; background: #f5f5f5; color: #222; font-size: 14px; } .app-header { background-color: #8B2635; color: white; padding: 15px 22px 13px; margin-bottom: 12px; 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; } .frauen-hinweis { background: #FFF3E0; border-left: 5px solid #E65100; color: #6D4C00; padding: 10px 16px; border-radius: 4px; margin-bottom: 16px; font-size: 0.9em; font-weight: 500; } .input-panel { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; background: white; border-radius: 6px; padding: 16px 20px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.12); } .input-panel .form-group { margin-bottom: 0; } .input-panel label { font-weight: 600; color: #333; } .btn-laden { background-color: #8B2635 !important; border-color: #7A2030 !important; color: white !important; font-weight: 600; padding: 6px 18px; border-radius: 4px; letter-spacing: 0.02em; white-space: nowrap; } .btn-laden:hover, .btn-laden:focus { background-color: #6E1E29 !important; border-color: #6E1E29 !important; outline: none; box-shadow: 0 0 0 2px rgba(139,38,53,0.3) !important; } .alert-fehler { background: #FFEBEE; border-left: 5px solid #C62828; color: #B71C1C; padding: 12px 16px; border-radius: 4px; margin-bottom: 12px; font-weight: 500; } .alert-warnung { background: #FFF8E1; border-left: 5px solid #E65100; color: #6D4C00; padding: 10px 16px; border-radius: 4px; 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.1rem; 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; } .score-zahl { font-size: 2.4rem; font-weight: 800; color: #8B2635; line-height: 1.1; } .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; } .stufe-badge { border-radius: 4px; padding: 3px 11px; font-weight: 700; font-size: 0.85em; white-space: nowrap; display: inline-block; margin-top: 6px; } .stufe-badge-sehr-gering { background: #4CAF50; color: white; } .stufe-badge-gering { background: #F48FB1; color: #333333; } .stufe-badge-normal { background: #EF5350; color: white; } .stufe-badge-hoch { background: #B71C1C; color: white; } .stufe-badge-sehr-hoch { background: #4A0000; color: white; } .item-badge { border-radius: 4px; padding: 2px 9px; font-weight: 700; font-size: 0.85em; white-space: nowrap; display: inline-block; } .item-badge-0 { background: #4CAF50; color: white; } .item-badge-1 { background: #F48FB1; color: #333333; } .item-badge-2 { background: #EF5350; color: white; } .item-badge-3 { background: #B71C1C; color: white; } .item-badge-4 { background: #4A0000; color: white; } .item-badge-na { background: #bbb; color: white; } .na-hinweis { color: #999; font-style: italic; } " 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("Body Image Fragebögen (FB1-FB5)"), tags$p("Persönliches Körperbild-Profil - Einzelfall-Auswertung") ), div(class = "frauen-hinweis", FRAUEN_HINWEIS_TEXT), 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_bodyimage_docx = function(erg) { fp_titel = fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 18) fp_meta = fp_text(color = "#555555", font.size = 10) fp_hinweis = fp_text(color = "#6D4C00", italic = TRUE, font.size = 10) 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_zusatz_txt = fp_text(font.size = 10, italic = TRUE) fp_zusatz_inf = fp_text(font.size = 9, italic = TRUE, color = "#888888") fp_disclaimer = fp_text(font.size = 9, italic = TRUE, color = "#777777") doc = read_docx() doc = body_add_fpar(doc, fpar( ftext("Body Image Fragebögen (FB1-FB5) - Auswertung", fp_titel) )) doc = body_add_fpar(doc, fpar( ftext(paste0("Chiffre: ", erg$chiffre, " Ausfuelldatum: ", erg$datum_str, " Erstellt: ", format(Sys.Date(), "%d.%m.%Y")), fp_meta) )) doc = body_add_fpar(doc, fpar(ftext(FRAUEN_HINWEIS_TEXT, fp_hinweis))) if (!is.null(erg$warnung)) { doc = body_add_fpar(doc, fpar(ftext(paste0("Hinweis: ", erg$warnung), fp_hinweis))) } doc = body_add_par(doc, "", style = "Normal") doc = body_add_fpar(doc, fpar(ftext("Summenscores", fp_abschnitt))) for (key in SCORE_REIHENFOLGE) { meta = SCORE_META[[key]] wert = erg$scores[[key]] klass = klassifiziere_stufe(wert, meta$baender) farb_key = if (is.na(klass$stufe)) "normal" else klass$stufe farben = STUFEN_WORD_FARBEN[[farb_key]] wert_txt = if (!is.na(wert)) as.character(wert) else "nicht berechenbar" doc = body_add_fpar(doc, fpar( ftext(paste0(meta$titel, ": "), fp_label), ftext(paste0(wert_txt, " / ", meta$range, " "), fp_normal), ftext(paste0(" ", klass$label, " "), fp_text(bold = TRUE, font.size = 10, color = farben$text, shading.color = farben$bg)) )) } doc = body_add_par(doc, "", style = "Normal") klaerfeld_liste = Filter(Negate(is.null), lapply(erg$fb3$items[43:48], function(it) { if (!is.null(it$klaerfeld)) paste0("FB3 Item ", it$nr, ": ", it$klaerfeld) else NULL })) freitext_paare = list( list(titel = "FB1 - Andere Körperteile (Item 9)", info = erg$fb1$zusatz_09), list(titel = "FB1 - Andere Körperteile (Item 10)", info = erg$fb1$zusatz_10), list(titel = "FB3 - Andere schwierige Situation (Item 49)", info = erg$fb3$zusatz_49), list(titel = "FB3 - Andere schwierige Situation (Item 50)", info = erg$fb3$zusatz_50), list(titel = "FB4 negativ - Andere häufige negative Gedanken (Item 31)", info = erg$fb4_neg$zusatz_31), list(titel = "FB4 negativ - Andere häufige negative Gedanken (Item 32)", info = erg$fb4_neg$zusatz_32), list(titel = "FB4 positiv - Andere häufige positive Gedanken (Item 16)", info = erg$fb4_pos$zusatz_16), list(titel = "FB4 positiv - Andere häufige positive Gedanken (Item 17)", info = erg$fb4_pos$zusatz_17) ) hat_freitext = any(sapply(freitext_paare, function(x) !leer(x$info$text))) if (length(klaerfeld_liste) > 0 || hat_freitext) { doc = body_add_fpar(doc, fpar(ftext("Qualitative Zusatzangaben", fp_abschnitt))) if (length(klaerfeld_liste) > 0) { doc = body_add_fpar(doc, fpar(ftext("FB3 - Klärfelder (Items 43-48)", fp_label))) for (kl in klaerfeld_liste) { doc = body_add_fpar(doc, fpar(ftext(kl, fp_normal))) } doc = body_add_par(doc, "", style = "Normal") } for (fp in freitext_paare) { if (leer(fp$info$text)) next doc = body_add_fpar(doc, fpar(ftext(fp$titel, fp_label))) doc = body_add_fpar(doc, fpar(ftext(fp$info$text, fp_zusatz_txt))) if (!is.na(fp$info$rating_wert)) { doc = body_add_fpar(doc, fpar(ftext( paste0("Einordnung: ", fp$info$rating_text, " (", fp$info$rating_wert, ")"), fp_text(font.size = 10, color = "#555555")))) } doc = body_add_fpar(doc, fpar(ftext( "Zusätzliche qualitative Angabe, nicht im Summenscore enthalten.", fp_zusatz_inf))) doc = body_add_par(doc, "", style = "Normal") } } doc = body_add_par(doc, "", style = "Normal") doc = body_add_fpar(doc, fpar(ftext(BODYIMAGE_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))) } }) ergebnis_r = eventReactive(input$btn_suchen, { chiffre = toupper(trimws(input$chiffre)) if ((nchar(trimws(input$pseudonym)) == 0 && nchar(chiffre) == 0)) { return(list(typ = "leere_eingabe")) } if (!(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 = "skript_fehler", meldung = paste("Bitte Pfade am Kopf der app.R anpassen:", paste(pfadfehler, collapse = "\n"), sep = "\n"))) } ok = tryCatch({ source(PFAD_DOWNLOAD_SKRIPT, local = FALSE) list(ok = TRUE) }, error = function(e) list(ok = FALSE, msg = e$message)) if (!ok$ok) { return(list(typ = "skript_fehler", meldung = paste0("Fehler im Download-Skript: ", ok$msg))) } if (!exists("daten_bodyimage", envir = .GlobalEnv)) { return(list(typ = "skript_fehler", meldung = paste0("Objekt 'daten_bodyimage' fehlt nach dem Sourcen von:\n", PFAD_DOWNLOAD_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 = "skript_fehler", meldung = paste0( "pseudonyme.db nicht gefunden.\n", "Gesucht ausgehend vom Pseudonym-Skript-Ordner bis zu 5 Ebenen nach oben."))) } 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(typ = "skript_fehler", meldung = paste0("Fehler im Pseudonym-Skript: ", ok_ps$msg))) } if (!exists("pseudo", envir = .GlobalEnv)) { return(list(typ = "skript_fehler", meldung = paste0("Objekt 'pseudo' fehlt nach dem Sourcen von:\n", PFAD_PSEUDONYM_SKRIPT))) } daten = get("daten_bodyimage", envir = .GlobalEnv) dat_ps = get("pseudo", envir = .GlobalEnv) ps_treffer = dat_ps[tolower(trimws(dat_ps$chiffre)) == tolower(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) if (!("session" %in% names(daten))) { return(list(typ = "skript_fehler", meldung = paste0( "Erwartete Spalte 'session' fehlt in 'daten_bodyimage'. ", "Bitte Struktur des Download-Skripts pruefen - ", "vorhandene Spalten: ", paste(names(daten), collapse = ", ")))) } treffer = daten[daten$session %in% alle_session_ids, , drop = FALSE] if (nrow(treffer) == 0) { return(list(typ = "session_nicht_gefunden", chiffre = chiffre, session_id = paste(alle_session_ids, collapse = ", "))) } warnung = NULL if (nrow(treffer) > 1) { if ("created" %in% names(treffer)) { created_vals = as.POSIXct(treffer$created, tz = "UTC") neueste_idx = which.max(created_vals) datum_neu = format(created_vals[neueste_idx], "%d.%m.%Y %H:%M") warnung = paste0("Mehrere Ausfuellungen gefunden (", nrow(treffer), "). ", "Angezeigt wird die neueste vom ", datum_neu, ".") treffer = treffer[neueste_idx, , drop = FALSE] } else { warnung = paste0("Mehrere Ausfuellungen gefunden (", nrow(treffer), "). ", "Angezeigt wird der erste Eintrag (keine 'created'-Spalte vorhanden).") treffer = treffer[1, , drop = FALSE] } } zeile = treffer[1, , drop = FALSE] datum_str = if ("created" %in% names(zeile) && !is.na(zeile$created[1])) { tryCatch(format(as.POSIXct(zeile$created[1], tz = "UTC"), "%d.%m.%Y"), error = function(e) format(Sys.Date(), "%d.%m.%Y")) } else format(Sys.Date(), "%d.%m.%Y") warn_env = new.env() assign("liste", character(0), envir = warn_env) fb1 = berechne_fb1(zeile, daten, warn_env) fb2 = berechne_fb2(zeile, daten, warn_env) fb3 = berechne_fb3(zeile, daten, warn_env) fb4_neg = berechne_fb4_neg(zeile, daten, warn_env) fb4_pos = berechne_fb4_pos(zeile, daten, warn_env) fb5 = berechne_fb5(zeile, daten, warn_env) scores = list( FB1 = fb1$score, FB2 = fb2$score, FB3 = fb3$score, FB4_neg = fb4_neg$score, FB4_pos = fb4_pos$score, FB5_A = fb5$A, FB5_B = fb5$B, FB5_C = fb5$C, FB5_D = fb5$D ) list( typ = "ergebnis", chiffre = chiffre, datum_str = datum_str, warnung = warnung, item_warnungen = get("liste", envir = warn_env), fb1 = fb1, fb2 = fb2, fb3 = fb3, fb4_neg = fb4_neg, fb4_pos = fb4_pos, fb5 = fb5, scores = scores ) }) output$fehler_ui = renderUI({ req(input$btn_suchen) erg = ergebnis_r() if (erg$typ == "leere_eingabe") { return(div(class = "alert-fehler", "Bitte eine Patientenchiffre eingeben.")) } if (erg$typ == "format_fehler") { return(div(class = "alert-fehler", "Ungültige Chiffre \"", erg$chiffre, "\". Erwartet wird ein Großbuchstabe ", "gefolgt von 6 Ziffern, z.B. P000123.")) } if (erg$typ == "skript_fehler") { return(div(class = "alert-fehler", tags$strong("Konfigurationsfehler: "), tags$pre(style = "white-space:pre-wrap; font-size:0.88em; margin:6px 0 0;", erg$meldung))) } if (erg$typ == "chiffre_nicht_gefunden") { return(div(class = "alert-fehler", "Chiffre \"", erg$chiffre, "\" wurde in der Pseudonym-Datenbank nicht gefunden.")) } if (erg$typ == "session_nicht_gefunden") { return(div(class = "alert-fehler", "Kein Body-Image-Datensatz fuer Chiffre \"", erg$chiffre, "\" gefunden ", "(", erg$session_id, ").")) } NULL }) output$warnung_ui = renderUI({ req(input$btn_suchen) erg = ergebnis_r() if (erg$typ != "ergebnis") return(NULL) tagList( if (!is.null(erg$warnung)) div(class = "alert-warnung", erg$warnung), if (length(erg$item_warnungen) > 0) div(class = "alert-warnung", tags$strong("Datenqualitaets-Hinweis: "), tags$ul(lapply(erg$item_warnungen, tags$li)) ) ) }) output$ergebnis_ui = renderUI({ req(input$btn_suchen) erg = ergebnis_r() if (erg$typ != "ergebnis") return(NULL) kopf_block = div(class = "abschnitt-karte", div(class = "meta-block", tags$strong("Chiffre: "), erg$chiffre, tags$span(" | ", style = "color:#ccc;"), tags$strong("Ausfülldatum: "), erg$datum_str ) ) fb1_zusatz = list() if (!leer(erg$fb1$zusatz_09$text)) fb1_zusatz[[length(fb1_zusatz) + 1]] = freitext_block_ui( "Andere Körperteile, die Sie nicht mögen (Item 9)", erg$fb1$zusatz_09$text, erg$fb1$zusatz_09$rating_text, erg$fb1$zusatz_09$rating_wert) if (!leer(erg$fb1$zusatz_10$text)) fb1_zusatz[[length(fb1_zusatz) + 1]] = freitext_block_ui( "Andere Körperteile, die Sie nicht mögen (Item 10)", erg$fb1$zusatz_10$text, erg$fb1$zusatz_10$rating_text, erg$fb1$zusatz_10$rating_wert) fb1_karte = baue_score_karte("FB1", erg$scores$FB1, "gauge_FB1", lapply(sortiere_nach_wert(erg$fb1$items, "wert"), function(it) item_zeile_ui(it$nr, it$text, it$antwort, it$wert, it$badge)), fb1_zusatz) fb2_karte = baue_score_karte("FB2", erg$scores$FB2, "gauge_FB2", lapply(sortiere_nach_wert(erg$fb2$items, "produkt"), fb2_item_zeile_ui)) fb3_items_ui = lapply(sortiere_nach_wert(erg$fb3$items, "wert"), function(it) { zusatz = if (!is.null(it$klaerfeld)) tags$div(style = "color:#888; font-size:0.85em; margin-top:2px;", paste0("(", FB3_KLAERFELD_LABEL[[it$nr]], " ", it$klaerfeld, ")")) else NULL item_zeile_ui(it$nr, it$text, it$antwort, it$wert, it$badge, zusatz) }) fb3_zusatz = list() if (!leer(erg$fb3$zusatz_49$text)) fb3_zusatz[[length(fb3_zusatz) + 1]] = freitext_block_ui( "Andere schwierige Situation (Item 49)", erg$fb3$zusatz_49$text, erg$fb3$zusatz_49$rating_text, erg$fb3$zusatz_49$rating_wert) if (!leer(erg$fb3$zusatz_50$text)) fb3_zusatz[[length(fb3_zusatz) + 1]] = freitext_block_ui( "Andere schwierige Situation (Item 50)", erg$fb3$zusatz_50$text, erg$fb3$zusatz_50$rating_text, erg$fb3$zusatz_50$rating_wert) fb3_karte = baue_score_karte("FB3", erg$scores$FB3, "gauge_FB3", fb3_items_ui, fb3_zusatz) fb4_neg_zusatz = list() if (!leer(erg$fb4_neg$zusatz_31$text)) fb4_neg_zusatz[[length(fb4_neg_zusatz) + 1]] = freitext_block_ui( "Andere häufige negative Gedanken (Item 31)", erg$fb4_neg$zusatz_31$text, erg$fb4_neg$zusatz_31$rating_text, erg$fb4_neg$zusatz_31$rating_wert) if (!leer(erg$fb4_neg$zusatz_32$text)) fb4_neg_zusatz[[length(fb4_neg_zusatz) + 1]] = freitext_block_ui( "Andere häufige negative Gedanken (Item 32)", erg$fb4_neg$zusatz_32$text, erg$fb4_neg$zusatz_32$rating_text, erg$fb4_neg$zusatz_32$rating_wert) fb4_neg_karte = baue_score_karte("FB4_neg", erg$scores$FB4_neg, "gauge_FB4_neg", lapply(sortiere_nach_wert(erg$fb4_neg$items, "wert"), function(it) item_zeile_ui(it$nr, it$text, it$antwort, it$wert, it$badge)), fb4_neg_zusatz) fb4_pos_zusatz = list() if (!leer(erg$fb4_pos$zusatz_16$text)) fb4_pos_zusatz[[length(fb4_pos_zusatz) + 1]] = freitext_block_ui( "Andere häufige positive Gedanken (Item 16)", erg$fb4_pos$zusatz_16$text, erg$fb4_pos$zusatz_16$rating_text, erg$fb4_pos$zusatz_16$rating_wert) if (!leer(erg$fb4_pos$zusatz_17$text)) fb4_pos_zusatz[[length(fb4_pos_zusatz) + 1]] = freitext_block_ui( "Andere häufige positive Gedanken (Item 17)", erg$fb4_pos$zusatz_17$text, erg$fb4_pos$zusatz_17$rating_text, erg$fb4_pos$zusatz_17$rating_wert) fb4_pos_karte = baue_score_karte("FB4_pos", erg$scores$FB4_pos, "gauge_FB4_pos", lapply(sortiere_nach_wert(erg$fb4_pos$items, "wert"), function(it) item_zeile_ui(it$nr, it$text, it$antwort, it$wert, it$badge)), fb4_pos_zusatz) fb5_item_row = function(it) { zusatz = if (it$nr %in% FB5_GEGENLAEUFIG) tags$span(style = "color:#888; font-size:0.82em; font-style:italic; margin-left:6px;", "(gegenläufig)") else NULL item_zeile_ui(it$nr, it$text, it$antwort, it$wert, it$badge, zusatz) } fb5_items_bereich = function(von, bis) { idx = sprintf("%02d", von:bis) auswahl = erg$fb5$items[sapply(erg$fb5$items, `[[`, "nr") %in% idx] lapply(sortiere_nach_wert(auswahl, "wert"), fb5_item_row) } fb5_a_karte = baue_score_karte("FB5_A", erg$scores$FB5_A, "gauge_FB5_A", fb5_items_bereich(1, 7)) fb5_b_karte = baue_score_karte("FB5_B", erg$scores$FB5_B, "gauge_FB5_B", fb5_items_bereich(8, 19)) fb5_c_karte = baue_score_karte("FB5_C", erg$scores$FB5_C, "gauge_FB5_C", fb5_items_bereich(20, 30)) fb5_d_karte = baue_score_karte("FB5_D", erg$scores$FB5_D, "gauge_FB5_D", fb5_items_bereich(31, 44)) tagList( kopf_block, fb1_karte, fb2_karte, fb3_karte, fb4_neg_karte, fb4_pos_karte, fb5_a_karte, fb5_b_karte, fb5_c_karte, fb5_d_karte ) }) observe({ erg = tryCatch(ergebnis_r(), error = function(e) NULL) if (is.null(erg) || erg$typ != "ergebnis") return() for (key in SCORE_REIHENFOLGE) { local({ key_ = key meta_ = SCORE_META[[key_]] wert_ = erg$scores[[key_]] output[[paste0("gauge_", key_)]] = renderPlot({ make_gauge(wert_, meta_$baender, AKZENT_FARBE, paste0(meta_$titel, " (", meta_$range, ")")) }, bg = "white", res = 144) }) } }) output$download_word = downloadHandler( filename = function() { erg = tryCatch(ergebnis_r(), error = function(e) NULL) if (is.null(erg) || erg$typ != "ergebnis") return("Bodyimage_Export.docx") chiffre_esc = gsub("[^A-Za-z0-9_-]", "_", erg$chiffre) datum_fn = tryCatch(format(as.Date(erg$datum_str, "%d.%m.%Y"), "%Y%m%d"), error = function(e) format(Sys.Date(), "%Y%m%d")) paste0("Bodyimage_", chiffre_esc, "_", datum_fn, ".docx") }, content = function(file) { erg = tryCatch(ergebnis_r(), error = function(e) NULL) if (is.null(erg) || erg$typ != "ergebnis") { 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 = erstelle_bodyimage_docx(erg) print(doc, target = file) } ) } # Start #### shinyApp(ui = ui, server = server)