Initial commit
This commit is contained in:
commit
3cba772836
1341 changed files with 532924 additions and 0 deletions
665
YSQ-S3/app.R
Normal file
665
YSQ-S3/app.R
Normal file
|
|
@ -0,0 +1,665 @@
|
|||
# Präambel ####
|
||||
|
||||
AKZENT_FARBE = "#8B2635"
|
||||
|
||||
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_ysq3.R"
|
||||
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R"
|
||||
|
||||
YSQ3_DISCLAIMER = paste0(
|
||||
"Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ",
|
||||
"keine klinische Diagnose. Die Interpretation obliegt der behandelnden Person. ",
|
||||
"Es liegen keine publizierten Normwerte oder klinischen Cutoffs fuer dieses Instrument vor; ",
|
||||
"die Werte sind ausschliesslich im intraindividuellen und therapeutischen Kontext zu interpretieren."
|
||||
)
|
||||
|
||||
# 6-stufige Antwortskala, identisch fuer alle 90 Items, keine invertierten Items.
|
||||
YSQ3_ANTWORTSKALA = c(
|
||||
"Trifft auf mich überhaupt nicht zu" = 1,
|
||||
"Trifft auf mich kaum zu" = 2,
|
||||
"Eher zutreffend als unzutreffend" = 3,
|
||||
"Trifft auf mich mäßig zu" = 4,
|
||||
"Trifft auf mich meistens zu" = 5,
|
||||
"Beschreibt mich perfekt" = 6
|
||||
)
|
||||
|
||||
# Schema-Item-Zuordnung, explizit hinterlegt (nicht aus dem Feldnamen-Suffix re-parsen).
|
||||
YSQ3_SCHEMATA = list(
|
||||
ee = list(name = "Emotionale Entbehrung", items = c(1, 19, 37, 55, 73)),
|
||||
vl = list(name = "Verlassenheit", items = c(2, 20, 38, 56, 74)),
|
||||
mt = list(name = "Misstrauen", items = c(3, 21, 39, 57, 75)),
|
||||
si = list(name = "Soziale Isolation / Entfremdung", items = c(4, 22, 40, 58, 76)),
|
||||
mu = list(name = "Mangelhaftigkeit / Unliebenswürdigkeit", items = c(5, 23, 41, 59, 77)),
|
||||
ve = list(name = "Versagen bei der Leistungserbringung", items = c(6, 24, 42, 60, 78)),
|
||||
pi = list(name = "Praktische Inkompetenz / Abhängigkeit", items = c(7, 25, 43, 61, 79)),
|
||||
vw = list(name = "Verwundbarkeit durch Leid oder Krankheit", items = c(8, 26, 44, 62, 80)),
|
||||
vs = list(name = "Verstrickung", items = c(9, 27, 45, 63, 81)),
|
||||
uw = list(name = "Unterwerfung", items = c(10, 28, 46, 64, 82)),
|
||||
so = list(name = "Selbstaufopferung", items = c(11, 29, 47, 65, 83)),
|
||||
eg = list(name = "Emotionale Gehemmtheit", items = c(12, 30, 48, 66, 84)),
|
||||
us = list(name = "Unerbittliche Standards", items = c(13, 31, 49, 67, 85)),
|
||||
ag = list(name = "Ansprüchlichkeit / Großartigkeit", items = c(14, 32, 50, 68, 86)),
|
||||
sk = list(name = "Unzureichende Selbstkontrolle / Selbstdisziplin", items = c(15, 33, 51, 69, 87)),
|
||||
ba = list(name = "Suche nach Bewunderung / Anerkennung", items = c(16, 34, 52, 70, 88)),
|
||||
ps = list(name = "Pessimismus / Sorgenmachen", items = c(17, 35, 53, 71, 89)),
|
||||
sb = list(name = "Selbstbestrafung", items = c(18, 36, 54, 72, 90))
|
||||
)
|
||||
|
||||
# Itemnummer (1-90) -> Spaltenname im formr-Export, aus der Schema-Tabelle abgeleitet.
|
||||
YSQ3_ITEM_SPALTEN = character(90)
|
||||
for (.kuerzel in names(YSQ3_SCHEMATA)) {
|
||||
for (.item_nr in YSQ3_SCHEMATA[[.kuerzel]]$items) {
|
||||
YSQ3_ITEM_SPALTEN[.item_nr] = sprintf("ysq_%03d_%s", .item_nr, .kuerzel)
|
||||
}
|
||||
}
|
||||
rm(.kuerzel, .item_nr)
|
||||
|
||||
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 ####
|
||||
|
||||
# Entfernt "**"-Markdown-Fettung und die fuehrende Itemnummer ("**1. Text**" -> "Text").
|
||||
extrahiere_itemtext = function(label_roh) {
|
||||
if (is.null(label_roh) || length(label_roh) == 0 || is.na(label_roh)) return(NA_character_)
|
||||
text = as.character(label_roh)
|
||||
text = gsub("\\*\\*", "", text)
|
||||
text = sub("^\\s*[0-9]+\\.\\s*", "", text)
|
||||
trimws(text)
|
||||
}
|
||||
|
||||
# Erkennt das Exportformat pro Item zur Laufzeit: numerisch 1-6, dbl+lbl ueber
|
||||
# das labels-Attribut der Original-Spalte, oder Freitext ueber die Antworttabelle.
|
||||
# Nicht eindeutig zuordenbare Werte werden NA (kein stillschweigendes Raten).
|
||||
recodiere_einzelwert = function(spalte_gesamt, wert) {
|
||||
if (is.null(wert) || length(wert) == 0 || is.na(wert)) return(NA_real_)
|
||||
|
||||
roh = suppressWarnings(as.numeric(wert))
|
||||
if (!is.na(roh) && roh >= 1 && roh <= 6) return(roh)
|
||||
|
||||
lbl_attr = attr(spalte_gesamt, "labels")
|
||||
if (!is.null(lbl_attr) && length(lbl_attr) > 0 && !is.na(roh)) {
|
||||
pos = which(as.vector(lbl_attr) == roh)
|
||||
if (length(pos) > 0) {
|
||||
treffer = unname(YSQ3_ANTWORTSKALA[trimws(names(lbl_attr)[pos[1]])])
|
||||
if (!is.na(treffer)) return(treffer)
|
||||
}
|
||||
}
|
||||
|
||||
text = trimws(as.character(wert))
|
||||
treffer = unname(YSQ3_ANTWORTSKALA[text])
|
||||
if (!is.na(treffer)) return(treffer)
|
||||
|
||||
NA_real_
|
||||
}
|
||||
|
||||
# Liest alle 90 Items einer einzelnen Zeile aus und recodiert sie auf 1-6.
|
||||
# Gibt zusaetzlich die Itemnummern zurueck, die fehlen oder nicht auswertbar waren.
|
||||
berechne_ysq3_werte = function(daten, zeile) {
|
||||
werte = rep(NA_real_, 90)
|
||||
fehlende = character(0)
|
||||
|
||||
for (nr in 1:90) {
|
||||
spalte_name = YSQ3_ITEM_SPALTEN[nr]
|
||||
if (!(spalte_name %in% names(daten))) {
|
||||
fehlende = c(fehlende, as.character(nr))
|
||||
next
|
||||
}
|
||||
wert = recodiere_einzelwert(daten[[spalte_name]], zeile[[spalte_name]][1])
|
||||
werte[nr] = wert
|
||||
if (is.na(wert)) fehlende = c(fehlende, as.character(nr))
|
||||
}
|
||||
|
||||
list(werte = werte, fehlende = fehlende)
|
||||
}
|
||||
|
||||
# Schema-Total = Summe der 5 Itemwerte, NA wenn mindestens ein Item NA ist
|
||||
# (kein stillschweigendes Weiterrechnen mit den restlichen 4 Items).
|
||||
berechne_schema_ergebnisse = function(werte_90) {
|
||||
ergebnisse = lapply(names(YSQ3_SCHEMATA), function(kuerzel) {
|
||||
schema = YSQ3_SCHEMATA[[kuerzel]]
|
||||
werte_schema = werte_90[schema$items]
|
||||
total = if (any(is.na(werte_schema))) NA_real_ else sum(werte_schema)
|
||||
list(kuerzel = kuerzel, name = schema$name, total = total)
|
||||
})
|
||||
names(ergebnisse) = names(YSQ3_SCHEMATA)
|
||||
ergebnisse
|
||||
}
|
||||
|
||||
# Bricht lange Schemanamen auf zwei Zeilen um, damit die Achsenbeschriftung lesbar bleibt.
|
||||
umbreche_schemaname = function(name, breite = 22) {
|
||||
paste(strwrap(name, width = breite), collapse = "\n")
|
||||
}
|
||||
|
||||
# Horizontales Balkendiagramm der 18 Schema-Totals, absteigend sortiert,
|
||||
# fixer Range 5-30, keine Farbzonen (keine klinischen Schwellen vorhanden).
|
||||
erstelle_schema_profil_plot = function(schema_ergebnisse) {
|
||||
df = data.frame(
|
||||
kuerzel = names(schema_ergebnisse),
|
||||
name = sapply(schema_ergebnisse, function(x) x$name),
|
||||
total = sapply(schema_ergebnisse, function(x) x$total),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
reihenfolge = order(df$total, decreasing = FALSE, na.last = FALSE)
|
||||
df$name_umbruch = sapply(df$name, umbreche_schemaname)
|
||||
df$name_f = factor(df$name_umbruch, levels = df$name_umbruch[reihenfolge])
|
||||
df$balken = ifelse(is.na(df$total), 0, df$total)
|
||||
df$label = ifelse(is.na(df$total), "unvollständig", as.character(df$total))
|
||||
df$label_y = ifelse(is.na(df$total), 5.3, pmin(df$total + 0.8, 29.3))
|
||||
df$label_hjust = 0
|
||||
df$label_farbe = ifelse(is.na(df$total), "#999999", "#333333")
|
||||
|
||||
ggplot(df, aes(x = name_f, y = balken)) +
|
||||
geom_col(fill = AKZENT_FARBE, width = 0.65) +
|
||||
geom_text(aes(y = label_y, label = label, hjust = label_hjust, color = label_farbe),
|
||||
size = 3.4, fontface = "bold") +
|
||||
scale_color_identity() +
|
||||
scale_y_continuous(breaks = seq(5, 30, 5)) +
|
||||
coord_flip(ylim = c(5, 30)) +
|
||||
guides(color = "none") +
|
||||
labs(x = NULL, y = "Schema-Total (5-30)") +
|
||||
theme_minimal(base_size = 12) +
|
||||
theme(
|
||||
axis.text.y = element_text(hjust = 1, lineheight = 0.85),
|
||||
panel.grid.minor = element_blank(),
|
||||
plot.margin = margin(t = 5, r = 20, b = 5, l = 5)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# 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; }
|
||||
.score-zahl { font-size: 2.4rem; font-weight: 800; color: #8B2635; }
|
||||
.score-info { font-size: 0.9em; color: #555; margin-top: 2px; }
|
||||
.gruppen-block {
|
||||
margin-bottom: 18px; padding-top: 10px; border-top: 1px solid #F0F0F0;
|
||||
}
|
||||
.gruppen-titel {
|
||||
font-weight: 700; color: #333; margin-bottom: 4px;
|
||||
display: flex; justify-content: space-between; align-items: baseline;
|
||||
}
|
||||
.gruppen-total { font-weight: 700; color: #8B2635; }
|
||||
.item-zeile {
|
||||
display: flex; align-items: flex-start; gap: 10px;
|
||||
padding: 6px 0; border-bottom: 1px solid #F5F5F5;
|
||||
}
|
||||
.item-nr { font-weight: 600; color: #8B2635; min-width: 26px; flex-shrink: 0; }
|
||||
.item-text { flex: 1; color: #333; font-size: 0.92em; }
|
||||
.item-wert {
|
||||
background: #F0F0F0; color: #333; border-radius: 4px; padding: 2px 9px;
|
||||
font-weight: 700; font-size: 0.82em; flex-shrink: 0;
|
||||
}
|
||||
"
|
||||
|
||||
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("YSQ-S3 – Young Schema Questionnaire (Kurzform 3)"),
|
||||
tags$p("18 Schemata, 90 Items | deskriptive Summenwertauswertung, keine klinischen Cutoffs")
|
||||
),
|
||||
|
||||
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_ysq3_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_score = fp_text(bold = TRUE, font.size = 14, color = AKZENT_FARBE)
|
||||
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("YSQ-S3 - Einzelauswertung", fp_titel)))
|
||||
doc = body_add_fpar(doc, fpar(
|
||||
ftext("Chiffre: ", fp_label),
|
||||
ftext(erg$chiffre, fp_normal),
|
||||
ftext(" Ausfuelldatum: ", fp_label),
|
||||
ftext(erg$ausfuelldatum_str, fp_normal)
|
||||
))
|
||||
if (length(erg$warnungen) > 0) {
|
||||
for (w in erg$warnungen) {
|
||||
doc = body_add_fpar(doc, fpar(ftext(w, fp_warnung)))
|
||||
}
|
||||
}
|
||||
doc = body_add_par(doc, "", style = "Normal")
|
||||
|
||||
gesamt_text = if (is.na(erg$gesamtscore)) "unvollstaendig" else paste0(erg$gesamtscore, " / 540")
|
||||
doc = body_add_fpar(doc, fpar(ftext("Gesamtscore", fp_abschnitt)))
|
||||
doc = body_add_fpar(doc, fpar(ftext(gesamt_text, fp_score)))
|
||||
doc = body_add_par(doc, "", style = "Normal")
|
||||
|
||||
doc = body_add_fpar(doc, fpar(ftext("Schema-Profil", fp_abschnitt)))
|
||||
bild_pfad = tempfile(fileext = ".png")
|
||||
ggsave(bild_pfad, plot = erg$profil_plot, width = 7, height = 7.8, dpi = 150)
|
||||
doc = doc %>% body_add_img(src = bild_pfad, width = 6, height = 6.7)
|
||||
file.remove(bild_pfad)
|
||||
doc = body_add_par(doc, "", style = "Normal")
|
||||
|
||||
doc = body_add_fpar(doc, fpar(ftext("Schema-Totalwerte", fp_abschnitt)))
|
||||
for (kuerzel in names(erg$schema_ergebnisse)) {
|
||||
s = erg$schema_ergebnisse[[kuerzel]]
|
||||
wert_text = if (is.na(s$total)) "unvollstaendig" else paste0(s$total, " / 30")
|
||||
doc = body_add_fpar(doc, fpar(
|
||||
ftext(paste0(s$name, " (", toupper(kuerzel), "): "), fp_label),
|
||||
ftext(wert_text, fp_normal)
|
||||
))
|
||||
}
|
||||
doc = body_add_par(doc, "", style = "Normal")
|
||||
|
||||
fp_item_wert = fp_text(bold = TRUE, font.size = 10, color = AKZENT_FARBE)
|
||||
doc = body_add_fpar(doc, fpar(ftext("Einzelitems nach Schema", fp_abschnitt)))
|
||||
for (kuerzel in names(erg$schema_ergebnisse)) {
|
||||
s = erg$schema_ergebnisse[[kuerzel]]
|
||||
doc = body_add_fpar(doc, fpar(
|
||||
ftext(paste0(s$name, " (", toupper(kuerzel), ")"), fp_label)
|
||||
))
|
||||
for (nr in YSQ3_SCHEMATA[[kuerzel]]$items) {
|
||||
spalte_name = YSQ3_ITEM_SPALTEN[nr]
|
||||
label_roh = if (spalte_name %in% names(erg$daten)) attr(erg$daten[[spalte_name]], "label") else NA
|
||||
item_text = extrahiere_itemtext(label_roh)
|
||||
if (is.na(item_text)) item_text = paste0("Item ", nr)
|
||||
wert = erg$werte_90[nr]
|
||||
wert_text = if (is.na(wert)) "k. A." else as.character(wert)
|
||||
|
||||
doc = body_add_fpar(doc, fpar(
|
||||
ftext(paste0(nr, ". ", item_text, " "), fp_normal),
|
||||
ftext(paste0("(", wert_text, ")"), fp_item_wert)
|
||||
))
|
||||
}
|
||||
doc = body_add_par(doc, "", style = "Normal")
|
||||
}
|
||||
|
||||
doc = body_add_fpar(doc, fpar(ftext(YSQ3_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_r = 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,
|
||||
meldung = paste0(
|
||||
"Ungueltige Chiffre '", chiffre, "'. Erwartet: ein Grossbuchstabe + 6 Ziffern (z. B. P000123)."
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
|
||||
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()
|
||||
on.exit(setwd(alter_wd), add = TRUE)
|
||||
wd_ziel = if (!is.null(db_ordner)) db_ordner else
|
||||
dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT, mustWork = FALSE))
|
||||
setwd(wd_ziel)
|
||||
|
||||
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 = paste0("Fehler im Pseudonym-Skript: ", ok_ps$msg)))
|
||||
}
|
||||
|
||||
if (!exists("daten_ysq3", envir = .GlobalEnv)) {
|
||||
return(list(typ = "skript_fehler",
|
||||
meldung = "Objekt 'daten_ysq3' nach dem Sourcen nicht gefunden. Bitte Download-Skript pruefen."))
|
||||
}
|
||||
if (!exists("pseudo", envir = .GlobalEnv)) {
|
||||
return(list(typ = "skript_fehler",
|
||||
meldung = "Objekt 'pseudo' nach dem Sourcen nicht gefunden. Bitte Pseudonym-Skript pruefen."))
|
||||
}
|
||||
|
||||
daten = get("daten_ysq3", envir = .GlobalEnv)
|
||||
pseudo = get("pseudo", envir = .GlobalEnv)
|
||||
|
||||
# Chiffre-Rueckauflösung aus Pseudonym: explizit eingegebenes Pseudonym hat Vorrang.
|
||||
if (nchar(trimws(input$pseudonym)) > 0) {
|
||||
pw_treffer = pseudo[pseudo$pseudonym == trimws(input$pseudonym), ]
|
||||
if (nrow(pw_treffer) > 0) chiffre = toupper(trimws(pw_treffer$chiffre[1]))
|
||||
}
|
||||
|
||||
treffer_ps = pseudo[pseudo$chiffre == chiffre, ]
|
||||
alle_session_ids = unique(treffer_ps$pseudonym)
|
||||
if (nchar(trimws(input$pseudonym)) > 0) alle_session_ids = trimws(input$pseudonym)
|
||||
|
||||
treffer_dat = daten[daten$session %in% alle_session_ids, ]
|
||||
if (nrow(treffer_dat) == 0) {
|
||||
return(list(typ = "keine_daten",
|
||||
meldung = paste0("Kein YSQ-S3-Datensatz fuer Chiffre '", chiffre, "' gefunden.")))
|
||||
}
|
||||
|
||||
warnungen = character(0)
|
||||
|
||||
if (nrow(treffer_dat) > 1) {
|
||||
datum_spalte_sort = intersect(c("ended", "created", "modified"), names(treffer_dat))[1]
|
||||
if (!is.na(datum_spalte_sort)) {
|
||||
treffer_dat = treffer_dat[order(treffer_dat[[datum_spalte_sort]], decreasing = TRUE), ]
|
||||
}
|
||||
n = nrow(treffer_dat)
|
||||
treffer_dat = treffer_dat[1, , drop = FALSE]
|
||||
warnungen = c(warnungen, paste0(
|
||||
"Mehrere Ausfuellungen gefunden (", n, " Eintraege). Angezeigt wird die neueste."
|
||||
))
|
||||
}
|
||||
|
||||
zeile = treffer_dat[1, , drop = FALSE]
|
||||
|
||||
# Ausfuelldatum defensiv aus der ersten passenden Spalte, nicht aus Sys.Date().
|
||||
datum_spalte = intersect(c("ended", "created", "modified"), names(daten))[1]
|
||||
ausfuelldatum_str = if (!is.na(datum_spalte)) {
|
||||
tryCatch(format(as.POSIXct(zeile[[datum_spalte]][1]), "%d.%m.%Y"),
|
||||
error = function(e) "unbekannt")
|
||||
} else "unbekannt"
|
||||
|
||||
werte_ergebnis = berechne_ysq3_werte(daten, zeile)
|
||||
werte_90 = werte_ergebnis$werte
|
||||
fehlende_items = werte_ergebnis$fehlende
|
||||
|
||||
if (length(fehlende_items) > 0) {
|
||||
warnungen = c(warnungen, paste0(
|
||||
"Achtung: ", length(fehlende_items),
|
||||
" Item(s) konnten nicht eindeutig ausgewertet werden (Item(s) ",
|
||||
paste(fehlende_items, collapse = ", "), ")."
|
||||
))
|
||||
}
|
||||
|
||||
schema_ergebnisse = berechne_schema_ergebnisse(werte_90)
|
||||
totals = sapply(schema_ergebnisse, function(x) x$total)
|
||||
gesamtscore = if (any(is.na(totals))) NA_real_ else sum(totals)
|
||||
|
||||
list(
|
||||
typ = "ok",
|
||||
chiffre = chiffre,
|
||||
ausfuelldatum_str = ausfuelldatum_str,
|
||||
warnungen = warnungen,
|
||||
werte_90 = werte_90,
|
||||
schema_ergebnisse = schema_ergebnisse,
|
||||
gesamtscore = gesamtscore,
|
||||
daten = daten,
|
||||
zeile = zeile
|
||||
)
|
||||
})
|
||||
|
||||
output$fehler_ui = renderUI({
|
||||
req(input$btn_suchen)
|
||||
d = ergebnis_r()
|
||||
if (d$typ != "ok") div(class = "alert-fehler", d$meldung)
|
||||
})
|
||||
|
||||
output$warnung_ui = renderUI({
|
||||
req(input$btn_suchen)
|
||||
d = ergebnis_r()
|
||||
if (d$typ != "ok" || length(d$warnungen) == 0) return(NULL)
|
||||
tagList(lapply(d$warnungen, function(w) div(class = "alert-warnung", w)))
|
||||
})
|
||||
|
||||
output$ergebnis_ui = renderUI({
|
||||
req(input$btn_suchen)
|
||||
d = ergebnis_r()
|
||||
if (d$typ != "ok") return(NULL)
|
||||
|
||||
gesamt_text = if (is.na(d$gesamtscore)) "unvollständig" else paste0(d$gesamtscore, " / 540")
|
||||
|
||||
gruppen_ui = lapply(names(d$schema_ergebnisse), function(kuerzel) {
|
||||
schema = d$schema_ergebnisse[[kuerzel]]
|
||||
total_text = if (is.na(schema$total)) "unvollständig" else paste0(schema$total, " / 30")
|
||||
item_nrn = YSQ3_SCHEMATA[[kuerzel]]$items
|
||||
|
||||
item_zeilen = lapply(item_nrn, function(nr) {
|
||||
spalte_name = YSQ3_ITEM_SPALTEN[nr]
|
||||
label_roh = if (spalte_name %in% names(d$daten)) attr(d$daten[[spalte_name]], "label") else NA
|
||||
item_text = extrahiere_itemtext(label_roh)
|
||||
if (is.na(item_text)) item_text = paste0("Item ", nr)
|
||||
wert = d$werte_90[nr]
|
||||
wert_text = if (is.na(wert)) "–" else as.character(wert)
|
||||
|
||||
div(class = "item-zeile",
|
||||
div(class = "item-nr", paste0(nr, ".")),
|
||||
div(class = "item-text", item_text),
|
||||
div(class = "item-wert", wert_text)
|
||||
)
|
||||
})
|
||||
|
||||
div(class = "gruppen-block",
|
||||
div(class = "gruppen-titel",
|
||||
span(paste0(schema$name, " (", toupper(kuerzel), ")")),
|
||||
span(class = "gruppen-total", total_text)
|
||||
),
|
||||
item_zeilen
|
||||
)
|
||||
})
|
||||
|
||||
div(class = "abschnitt-karte",
|
||||
div(class = "abschnitt-titel", "YSQ-S3 Auswertung"),
|
||||
|
||||
div(class = "meta-block",
|
||||
tags$strong("Chiffre: "), d$chiffre,
|
||||
tags$span(" | ", style = "color:#ccc;"),
|
||||
tags$strong("Ausfuelldatum: "), d$ausfuelldatum_str
|
||||
),
|
||||
|
||||
tags$hr(),
|
||||
|
||||
div(
|
||||
div(class = "score-zahl", gesamt_text),
|
||||
div(class = "score-info", "Gesamtscore")
|
||||
),
|
||||
|
||||
plotOutput("profil_plot", height = "750px"),
|
||||
|
||||
tags$hr(),
|
||||
|
||||
tags$h5("Einzelitems nach Schema"),
|
||||
gruppen_ui
|
||||
)
|
||||
})
|
||||
|
||||
output$profil_plot = renderPlot({
|
||||
req(input$btn_suchen)
|
||||
d = ergebnis_r()
|
||||
req(d$typ == "ok")
|
||||
erstelle_schema_profil_plot(d$schema_ergebnisse)
|
||||
}, bg = "transparent")
|
||||
|
||||
output$download_word = downloadHandler(
|
||||
filename = function() {
|
||||
d = tryCatch(ergebnis_r(), error = function(e) NULL)
|
||||
chiffre_datei = if (is.list(d) && identical(d$typ, "ok"))
|
||||
gsub("[^A-Za-z0-9]", "", d$chiffre) else "export"
|
||||
datum_datei = if (is.list(d) && identical(d$typ, "ok")) {
|
||||
tryCatch(format(as.Date(d$ausfuelldatum_str, "%d.%m.%Y"), "%Y%m%d"),
|
||||
error = function(e) format(Sys.Date(), "%Y%m%d"))
|
||||
} else format(Sys.Date(), "%Y%m%d")
|
||||
paste0("YSQ3_", chiffre_datei, "_", datum_datei, ".docx")
|
||||
},
|
||||
content = function(file) {
|
||||
d = tryCatch(ergebnis_r(), error = function(e) NULL)
|
||||
if (!is.list(d) || !identical(d$typ, "ok")) {
|
||||
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()
|
||||
}
|
||||
|
||||
erg = list(
|
||||
chiffre = d$chiffre,
|
||||
ausfuelldatum_str = d$ausfuelldatum_str,
|
||||
warnungen = d$warnungen,
|
||||
gesamtscore = d$gesamtscore,
|
||||
schema_ergebnisse = d$schema_ergebnisse,
|
||||
werte_90 = d$werte_90,
|
||||
daten = d$daten,
|
||||
profil_plot = erstelle_schema_profil_plot(d$schema_ergebnisse)
|
||||
)
|
||||
|
||||
doc = tryCatch(
|
||||
erstelle_ysq3_docx(erg),
|
||||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue