794 lines
28 KiB
R
794 lines
28 KiB
R
# Präambel ####
|
||
|
||
AKZENT_FARBE = "#8B2635"
|
||
|
||
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_pssi.R"
|
||
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R"
|
||
PFAD_NORMTABELLEN = "normtabellen"
|
||
|
||
PSSI_DISCLAIMER = paste0(
|
||
"Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ",
|
||
"keine klinische Diagnose. Die Interpretation obliegt der behandelnden Person. ",
|
||
"Es wird kein klinischer Cutoff-Wert angewendet; Prozentrang und T-Wert werden ",
|
||
"neutral berichtet und muessen fachlich eingeordnet werden."
|
||
)
|
||
|
||
library(shiny)
|
||
library(dplyr)
|
||
library(ggplot2)
|
||
library(haven)
|
||
library(officer)
|
||
library(DBI)
|
||
library(RSQLite)
|
||
|
||
|
||
# Infrastruktur ####
|
||
|
||
APP_VERZEICHNIS = normalizePath(getwd())
|
||
|
||
absPath = function(pfad) {
|
||
if (grepl("^([A-Za-z]:[/\\\\]|/)", pfad)) return(pfad)
|
||
file.path(APP_VERZEICHNIS, pfad)
|
||
}
|
||
|
||
PFAD_DOWNLOAD_SKRIPT = normalizePath(absPath(PFAD_DOWNLOAD_SKRIPT), mustWork = FALSE)
|
||
PFAD_PSEUDONYM_SKRIPT = normalizePath(absPath(PFAD_PSEUDONYM_SKRIPT), mustWork = FALSE)
|
||
PFAD_NORMTABELLEN = normalizePath(absPath(PFAD_NORMTABELLEN), mustWork = FALSE)
|
||
|
||
|
||
# Helper ####
|
||
|
||
pssi_antwortkategorien = c(
|
||
"trifft gar nicht zu", "trifft etwas zu",
|
||
"trifft ueberwiegend zu", "trifft ausgesprochen zu"
|
||
)
|
||
|
||
pssi_normalisiere_kat = function(text) {
|
||
x = tolower(trimws(as.character(text)))
|
||
x = gsub("ü", "ue", x, fixed = TRUE)
|
||
x
|
||
}
|
||
pssi_antwortkategorien_norm = pssi_normalisiere_kat(pssi_antwortkategorien)
|
||
|
||
# Extrahiert die Stufe 0-3 aus einer PSSI-Item-Spalte. Nutzt bei
|
||
# haven-labelled Spalten IMMER das labels-Attribut (Antworttext -> Code),
|
||
# nie den rohen numerischen Code direkt, da dessen Kodierung variieren kann.
|
||
stufe_aus_item = function(spalte, item_name = "") {
|
||
if (length(spalte) == 0 || is.na(spalte[1])) return(NA_integer_)
|
||
|
||
if (haven::is.labelled(spalte)) {
|
||
lbl_attr = attr(spalte, "labels")
|
||
wert = as.numeric(spalte[1])
|
||
if (is.null(lbl_attr) || length(lbl_attr) == 0 || is.na(wert)) {
|
||
stop(paste0("Unerwartetes Antwortformat bei PSSI-Item ", item_name,
|
||
": labelled-Spalte ohne labels-Attribut."))
|
||
}
|
||
pos = which(as.vector(lbl_attr) == wert)
|
||
if (length(pos) == 0) {
|
||
stop(paste0("Unerwartetes Antwortformat bei PSSI-Item ", item_name,
|
||
": Code ", wert, " nicht in labels-Attribut gefunden."))
|
||
}
|
||
txt_norm = pssi_normalisiere_kat(names(lbl_attr)[pos[1]])
|
||
stufe = match(txt_norm, pssi_antwortkategorien_norm) - 1L
|
||
if (is.na(stufe)) {
|
||
stop(paste0("Unerwartetes Antwortformat bei PSSI-Item ", item_name,
|
||
": Antworttext '", names(lbl_attr)[pos[1]],
|
||
"' passt zu keiner der vier bekannten Kategorien."))
|
||
}
|
||
return(as.integer(stufe))
|
||
}
|
||
|
||
txt_norm = pssi_normalisiere_kat(spalte[1])
|
||
stufe = match(txt_norm, pssi_antwortkategorien_norm) - 1L
|
||
if (is.na(stufe)) {
|
||
stop(paste0("Unerwartetes Antwortformat bei PSSI-Item ", item_name,
|
||
": Wert '", spalte[1], "' passt zu keiner der vier bekannten Kategorien."))
|
||
}
|
||
as.integer(stufe)
|
||
}
|
||
|
||
pssi_pr_t_lookup = function(normtabelle, skala, rohwert) {
|
||
zeile = normtabelle[normtabelle$rohwert == rohwert, , drop = FALSE]
|
||
if (nrow(zeile) == 0) return(list(pr = NA_real_, t = NA_real_))
|
||
pr_col = paste0(skala, "_PR")
|
||
t_col = paste0(skala, "_T")
|
||
list(
|
||
pr = suppressWarnings(as.numeric(zeile[[pr_col]][1])),
|
||
t = suppressWarnings(as.numeric(zeile[[t_col]][1]))
|
||
)
|
||
}
|
||
|
||
pssi_vorschlag_normtabelle = function(alter, geschlecht) {
|
||
if (is.na(alter) || alter < 14 || alter > 82) return(NULL)
|
||
if (is.na(geschlecht) || !(geschlecht %in% c("weiblich", "maennlich"))) return(NULL)
|
||
|
||
if (alter >= 14 && alter <= 17) return("B4")
|
||
|
||
altersgruppe = if (alter <= 25) "18_25"
|
||
else if (alter <= 45) "26_45"
|
||
else if (alter <= 55) "46_55"
|
||
else "56_82"
|
||
|
||
schluessel = list(
|
||
"18_25" = c(weiblich = "B10", maennlich = "B9"),
|
||
"26_45" = c(weiblich = "B12", maennlich = "B11"),
|
||
"46_55" = c(weiblich = "B14", maennlich = "B13"),
|
||
"56_82" = c(weiblich = "B16", maennlich = "B15")
|
||
)
|
||
unname(schluessel[[altersgruppe]][geschlecht])
|
||
}
|
||
|
||
pssi_profil_plot = function(profil_df) {
|
||
df = profil_df
|
||
df$fehlend = is.na(df$t)
|
||
df_plot = df
|
||
df_plot$t_plot = ifelse(df_plot$fehlend, NA, df_plot$t)
|
||
|
||
ggplot(df_plot, aes(x = skala, y = t_plot, group = 1)) +
|
||
geom_hline(yintercept = 50, color = "#777777", linetype = "dashed", linewidth = 0.6) +
|
||
annotate("text", x = levels(df_plot$skala)[1], y = 52,
|
||
label = "Populationsmittelwert (T=50) - statistische Konvention, kein Cutoff",
|
||
hjust = 0, size = 3, color = "#555555") +
|
||
geom_line(color = AKZENT_FARBE, linewidth = 0.9, na.rm = TRUE) +
|
||
geom_point(data = df_plot[!df_plot$fehlend, ], aes(x = skala, y = t_plot),
|
||
color = AKZENT_FARBE, size = 2.6) +
|
||
geom_point(data = df_plot[df_plot$fehlend, ], aes(x = skala, y = 50),
|
||
shape = 21, size = 3, color = AKZENT_FARBE, fill = "white", stroke = 1.1) +
|
||
scale_y_continuous(limits = c(10, 90), breaks = seq(10, 90, 10)) +
|
||
labs(x = NULL, y = "T-Wert",
|
||
caption = "Offener Kreis = kein T-Wert ausgewiesen (Boden-/Deckeneffekt der Normstichprobe)") +
|
||
theme_minimal(base_size = 11) +
|
||
theme(
|
||
panel.grid.minor = element_blank(),
|
||
axis.text.x = element_text(angle = 0),
|
||
plot.caption = element_text(size = 8, color = "#777777", hjust = 0)
|
||
)
|
||
}
|
||
|
||
|
||
# Datenaufbereitung ####
|
||
|
||
pssi_skala_reihenfolge_items = c("PN","SZ","ST","BL","HI","NA","SU","AB","ZW","NT","DP","SL","RH","AS")
|
||
pssi_item_map = data.frame(
|
||
item = 1:140,
|
||
skala = rep(pssi_skala_reihenfolge_items, times = 10),
|
||
umgepolt = (1:140) %in% c(15, 39, 43, 44, 49, 67, 71, 72, 86, 91, 99, 104, 105, 109, 137),
|
||
stringsAsFactors = FALSE
|
||
)
|
||
|
||
{
|
||
n_je_skala = table(pssi_item_map$skala)
|
||
if (!all(n_je_skala == 10) || length(n_je_skala) != 14) {
|
||
stop("Datenintegritaetsfehler: pssi_item_map weist nicht jeder der 14 Skalen genau 10 Items zu.")
|
||
}
|
||
if (length(unique(pssi_item_map$item)) != 140 || anyNA(pssi_item_map$skala)) {
|
||
stop("Datenintegritaetsfehler: nicht alle 140 PSSI-Items sind genau einer Skala zugeordnet.")
|
||
}
|
||
}
|
||
|
||
pssi_skalennamen = c(
|
||
AS = "Selbstbestimmt (antisoziale PS)",
|
||
PN = "Eigenwillig (paranoide PS)",
|
||
SZ = "Zurueckhaltend (schizoide PS)",
|
||
SU = "Selbstkritisch (selbstunsichere PS)",
|
||
ZW = "Sorgfaeltig (zwanghafte PS)",
|
||
ST = "Ahnungsvoll (schizotypische PS)",
|
||
RH = "Optimistisch (rhapsodisch, kein DSM-Bezug)",
|
||
"NA" = "Ehrgeizig (narzisstische PS)",
|
||
NT = "Kritisch (negativistische/passiv-aggressive PS)",
|
||
AB = "Loyal (abhaengige PS)",
|
||
BL = "Spontan (Borderline-PS)",
|
||
HI = "Liebenswuerdig (histrionische PS)",
|
||
DP = "Still/passiv (depressive PS, kein DSM-Achse-II-Bezug)",
|
||
SL = "Hilfsbereit/altruistisch (selbstlose PS, kein DSM-Bezug)"
|
||
)
|
||
|
||
pssi_skala_anzeige_reihenfolge = c("AS","PN","SZ","SU","ZW","ST","RH","NA","NT","AB","BL","HI","DP","SL")
|
||
|
||
pssi_normtabellen_meta = data.frame(
|
||
key = paste0("B", 1:16),
|
||
datei = c(
|
||
"B1_gesamtstichprobe.csv", "B2_maenner_gesamt.csv", "B3_frauen_gesamt.csv",
|
||
"B4_14_17_jahre.csv", "B5_18_25_jahre.csv", "B6_26_45_jahre.csv",
|
||
"B7_46_55_jahre.csv", "B8_56_82_jahre.csv", "B9_18_25_maenner.csv",
|
||
"B10_18_25_frauen.csv", "B11_26_45_maenner.csv", "B12_26_45_frauen.csv",
|
||
"B13_46_55_maenner.csv", "B14_46_55_frauen.csv", "B15_56_82_maenner.csv",
|
||
"B16_56_82_frauen.csv"
|
||
),
|
||
beschreibung = c(
|
||
"Gesamtstichprobe, alle Erwachsenen", "alle erwachsenen Maenner", "alle erwachsenen Frauen",
|
||
"14-17 Jahre (beide Geschlechter)", "18-25 Jahre (beide Geschlechter)", "26-45 Jahre (beide Geschlechter)",
|
||
"46-55 Jahre (beide Geschlechter)", "56-82 Jahre (beide Geschlechter)", "18-25 Jahre, maennlich",
|
||
"18-25 Jahre, weiblich", "26-45 Jahre, maennlich", "26-45 Jahre, weiblich",
|
||
"46-55 Jahre, maennlich", "46-55 Jahre, weiblich", "56-82 Jahre, maennlich",
|
||
"56-82 Jahre, weiblich"
|
||
),
|
||
n = c(1903, 1037, 866, 40, 658, 852, 256, 137, 327, 331, 456, 396, 183, 73, 71, 66),
|
||
stringsAsFactors = FALSE
|
||
)
|
||
pssi_normtabellen_meta$label = paste0(
|
||
pssi_normtabellen_meta$key, " – ", pssi_normtabellen_meta$beschreibung,
|
||
" (N=", pssi_normtabellen_meta$n, ")"
|
||
)
|
||
|
||
pssi_normtabellen = list()
|
||
for (i in seq_len(nrow(pssi_normtabellen_meta))) {
|
||
key = pssi_normtabellen_meta$key[i]
|
||
datei = pssi_normtabellen_meta$datei[i]
|
||
pfad = file.path(PFAD_NORMTABELLEN, datei)
|
||
|
||
if (!file.exists(pfad)) {
|
||
stop(paste0("Normtabelle nicht gefunden: ", pfad))
|
||
}
|
||
|
||
tab = tryCatch(
|
||
read.csv(pfad, na.strings = character(0), stringsAsFactors = FALSE),
|
||
error = function(e) stop(paste0("Fehler beim Einlesen von '", datei, "': ", e$message))
|
||
)
|
||
|
||
if (!("rohwert" %in% names(tab))) {
|
||
stop(paste0("Normtabelle '", datei, "' hat keine Spalte 'rohwert'."))
|
||
}
|
||
if (!setequal(sort(tab$rohwert), 0:30)) {
|
||
stop(paste0("Normtabelle '", datei, "' enthaelt nicht lueckenlos die Rohwerte 0-30."))
|
||
}
|
||
for (sk in pssi_skala_reihenfolge_items) {
|
||
pr_col = paste0(sk, "_PR")
|
||
t_col = paste0(sk, "_T")
|
||
if (!(pr_col %in% names(tab))) {
|
||
stop(paste0("Normtabelle '", datei, "' hat keine Spalte '", pr_col, "'."))
|
||
}
|
||
if (!(t_col %in% names(tab))) {
|
||
stop(paste0("Normtabelle '", datei, "' hat keine Spalte '", t_col, "'."))
|
||
}
|
||
}
|
||
|
||
pssi_normtabellen[[key]] = tab
|
||
}
|
||
|
||
|
||
# UI ####
|
||
|
||
app_css = "
|
||
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f5f5f5; }
|
||
.container-fluid { max-width: 1100px; }
|
||
.app-header {
|
||
background: #8B2635; color: white; padding: 18px 24px 14px;
|
||
margin-bottom: 20px; border-radius: 0 0 6px 6px;
|
||
}
|
||
.app-header h2 { margin: 0; font-size: 1.5rem; font-weight: 600; }
|
||
.app-header p { margin: 4px 0 0; opacity: 0.85; font-size: 0.9rem; }
|
||
.input-panel {
|
||
background: white; border-radius: 6px; padding: 16px 20px;
|
||
margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.12);
|
||
display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap;
|
||
}
|
||
.input-panel .form-group { margin-bottom: 0; }
|
||
.input-panel label { font-weight: 600; color: #333; }
|
||
.btn-laden {
|
||
background: #8B2635 !important; color: white !important;
|
||
border: none !important; border-radius: 4px !important;
|
||
padding: 8px 20px !important; font-weight: 600 !important; cursor: pointer;
|
||
}
|
||
.btn-laden:hover { background: #6d1e29 !important; }
|
||
.alert-fehler {
|
||
background: #FFEBEE; border-left: 5px solid #C62828;
|
||
padding: 12px 16px; border-radius: 4px; color: #B71C1C;
|
||
margin-bottom: 12px; font-weight: 500;
|
||
}
|
||
.alert-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; }
|
||
.normtabelle-info { margin-bottom: 14px; color: #444; font-size: 0.92em; }
|
||
.disclaimer-block {
|
||
font-size: 0.82em; color: #777; font-style: italic;
|
||
margin-top: 10px; border-top: 1px solid rgba(0,0,0,.1); padding-top: 8px;
|
||
}
|
||
.item-zeile {
|
||
display: flex; align-items: flex-start; gap: 10px;
|
||
padding: 7px 0; border-bottom: 1px solid #F0F0F0;
|
||
}
|
||
.item-nr { font-weight: 600; color: #8B2635; min-width: 26px; flex-shrink: 0; }
|
||
.item-text { flex: 1; color: #333; font-size: 0.92em; }
|
||
table.pssi-tabelle { width: 100%; border-collapse: collapse; font-size: 0.92em; }
|
||
table.pssi-tabelle th {
|
||
text-align: left; border-bottom: 2px solid #8B2635; padding: 6px 8px; color: #8B2635;
|
||
}
|
||
table.pssi-tabelle td { padding: 6px 8px; border-bottom: 1px solid #eee; }
|
||
table.pssi-tabelle td.zahl { text-align: right; }
|
||
"
|
||
|
||
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("PSSI – Persoenlichkeits-Stil- und Stoerungs-Inventar"),
|
||
tags$p("Einzelfall-Auswertung mit Normtabellen (Prozentrang, T-Wert)")
|
||
),
|
||
|
||
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_normtabelle_ui"),
|
||
uiOutput("warnung_mehrfach_ui"),
|
||
uiOutput("normtabelle_auswahl_ui"),
|
||
uiOutput("ergebnis_ui")
|
||
)
|
||
)
|
||
|
||
|
||
# Word-Export ####
|
||
|
||
erstelle_pssi_docx = function(erg, normtabelle_info) {
|
||
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")
|
||
|
||
doc = body_add_fpar(doc, fpar(ftext("PSSI - 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_anzeige, fp_normal)
|
||
))
|
||
doc = body_add_fpar(doc, fpar(
|
||
ftext("Normtabelle: ", fp_label),
|
||
ftext(normtabelle_info, fp_normal)
|
||
))
|
||
if (!is.null(erg$info_mehrere)) {
|
||
doc = body_add_fpar(doc, fpar(
|
||
ftext(erg$info_mehrere, fp_text(font.size = 10, italic = TRUE, color = "#555555"))
|
||
))
|
||
}
|
||
doc = body_add_par(doc, "", style = "Normal")
|
||
|
||
doc = body_add_fpar(doc, fpar(ftext("Skalenwerte", fp_abschnitt)))
|
||
|
||
for (i in seq_len(nrow(erg$profil))) {
|
||
zeile = erg$profil[i, ]
|
||
pr_txt = if (is.na(zeile$pr)) "k. A." else sprintf("%.1f", zeile$pr)
|
||
t_txt = if (is.na(zeile$t)) "kein T-Wert ausgewiesen (Boden-/Deckeneffekt)" else sprintf("%.0f", zeile$t)
|
||
doc = body_add_fpar(doc, fpar(
|
||
ftext(sprintf("%-4s ", zeile$skala), fp_text(bold = TRUE, font.size = 10, font.family = "Courier New")),
|
||
ftext(sprintf("%-55s ", substr(zeile$skala_name, 1, 55)), fp_normal),
|
||
ftext(sprintf("Rohwert: %2d PR: %-6s T: %s", zeile$rohwert, pr_txt, t_txt), fp_normal)
|
||
))
|
||
}
|
||
|
||
doc = body_add_par(doc, "", style = "Normal")
|
||
doc = body_add_fpar(doc, fpar(ftext("Profildiagramm", fp_abschnitt)))
|
||
|
||
tmp_png = tempfile(fileext = ".png")
|
||
ggplot2::ggsave(tmp_png, pssi_profil_plot(erg$profil), width = 8, height = 4, dpi = 150, bg = "white")
|
||
doc = body_add_img(doc, src = tmp_png, width = 6.2, height = 3.1)
|
||
if (file.exists(tmp_png)) unlink(tmp_png)
|
||
|
||
doc = body_add_par(doc, "", style = "Normal")
|
||
doc = body_add_fpar(doc, fpar(ftext(PSSI_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(error = "Bitte eine Patientenchiffre eingeben."))
|
||
}
|
||
if (!(nchar(trimws(input$pseudonym)) > 0 || grepl("^[A-Z][0-9]{6}$", chiffre))) {
|
||
return(list(error = "Ungueltige Chiffre. Erwartet: ein Grossbuchstabe + 6 Ziffern (z.B. P000123)."))
|
||
}
|
||
|
||
if (!file.exists(PFAD_DOWNLOAD_SKRIPT)) {
|
||
return(list(error = paste0("Download-Skript nicht gefunden:\n", PFAD_DOWNLOAD_SKRIPT)))
|
||
}
|
||
if (!file.exists(PFAD_PSEUDONYM_SKRIPT)) {
|
||
return(list(error = paste0("Pseudonym-Skript nicht gefunden:\n", PFAD_PSEUDONYM_SKRIPT)))
|
||
}
|
||
|
||
ok_dl = tryCatch(
|
||
{ source(PFAD_DOWNLOAD_SKRIPT, local = FALSE); list(ok = TRUE) },
|
||
error = function(e) list(ok = FALSE, msg = e$message)
|
||
)
|
||
if (!ok_dl$ok) {
|
||
return(list(error = paste0("Fehler im Download-Skript: ", ok_dl$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
|
||
})
|
||
if (is.null(db_ordner)) {
|
||
return(list(error = paste0(
|
||
"pseudonyme.db nicht gefunden (bis 5 Ebenen oberhalb von ",
|
||
dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT, mustWork = FALSE)), " gesucht)."
|
||
)))
|
||
}
|
||
|
||
alter_wd = getwd()
|
||
setwd(db_ordner)
|
||
on.exit(setwd(alter_wd), add = TRUE)
|
||
|
||
ok_ps = 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_ps$ok) {
|
||
return(list(error = paste0("Fehler im Pseudonym-Skript: ", ok_ps$msg)))
|
||
}
|
||
|
||
if (!exists("daten_pssi", envir = .GlobalEnv)) {
|
||
return(list(error = "Objekt 'daten_pssi' nach dem Sourcen nicht gefunden. Bitte Download-Skript pruefen."))
|
||
}
|
||
if (!exists("pseudo", envir = .GlobalEnv)) {
|
||
return(list(error = "Objekt 'pseudo' nach dem Sourcen nicht gefunden. Bitte Pseudonym-Skript pruefen."))
|
||
}
|
||
|
||
daten_pssi = get("daten_pssi", envir = .GlobalEnv)
|
||
pseudo = get("pseudo", envir = .GlobalEnv)
|
||
|
||
treffer_ps = pseudo[toupper(trimws(as.character(pseudo$chiffre))) == chiffre, ]
|
||
if (nrow(treffer_ps) == 0) {
|
||
return(list(error = paste0("Chiffre '", chiffre, "' wurde in der Pseudonym-Datenbank nicht gefunden.")))
|
||
}
|
||
|
||
alle_session_ids = unique(as.character(treffer_ps$pseudonym))
|
||
if (nchar(trimws(input$pseudonym)) > 0) alle_session_ids = trimws(input$pseudonym)
|
||
treffer_dat = daten_pssi[as.character(daten_pssi$session) %in% alle_session_ids, , drop = FALSE]
|
||
if (nrow(treffer_dat) == 0) {
|
||
return(list(error = paste0(
|
||
"Kein PSSI-Datensatz fuer Chiffre '", chiffre, "' gefunden. (",
|
||
length(alle_session_ids), " Pseudonym(e) geprueft)"
|
||
)))
|
||
}
|
||
|
||
info_mehrere = NULL
|
||
if (nrow(treffer_dat) > 1) {
|
||
n = nrow(treffer_dat)
|
||
idx_neu = which.max(as.POSIXct(treffer_dat$created))
|
||
datum_neu = tryCatch(
|
||
format(as.POSIXct(treffer_dat$created[idx_neu]), "%d.%m.%Y %H:%M"),
|
||
error = function(e) "unbekanntes Datum"
|
||
)
|
||
info_mehrere = paste0(
|
||
"Mehrere Ausfuellungen gefunden (", n, " Eintraege). Angezeigt wird die neueste vom ", datum_neu, "."
|
||
)
|
||
treffer_dat = treffer_dat[idx_neu, , drop = FALSE]
|
||
}
|
||
|
||
zeile = treffer_dat[1, , drop = FALSE]
|
||
|
||
ausfuelldatum_raw = zeile[["created"]][1]
|
||
ausfuelldatum_anzeige = tryCatch(
|
||
format(as.POSIXct(ausfuelldatum_raw), "%d.%m.%Y"),
|
||
error = function(e) "unbekannt"
|
||
)
|
||
|
||
alter_roh = zeile[["pssi_alter"]][1]
|
||
alter_num = suppressWarnings(as.numeric(trimws(as.character(alter_roh))))
|
||
alter_gueltig = !is.na(alter_num) && alter_num >= 10 && alter_num <= 110
|
||
if (!alter_gueltig) alter_num = NA_real_
|
||
|
||
geschlecht_roh = zeile[["pssi_geschlecht"]][1]
|
||
geschlecht_txt = tryCatch({
|
||
spalte_orig = daten_pssi[["pssi_geschlecht"]]
|
||
if (haven::is.labelled(spalte_orig)) {
|
||
lbl_attr = attr(spalte_orig, "labels")
|
||
pos = which(as.vector(lbl_attr) == as.numeric(geschlecht_roh))
|
||
if (length(pos) > 0) trimws(names(lbl_attr)[pos[1]]) else NA_character_
|
||
} else {
|
||
trimws(as.character(geschlecht_roh))
|
||
}
|
||
}, error = function(e) NA_character_)
|
||
|
||
geschlecht = if (!is.na(geschlecht_txt) && grepl("^weiblich$", geschlecht_txt, ignore.case = TRUE)) {
|
||
"weiblich"
|
||
} else if (!is.na(geschlecht_txt) && grepl("^m(a|ä)nnlich$", geschlecht_txt, ignore.case = TRUE)) {
|
||
"maennlich"
|
||
} else {
|
||
NA_character_
|
||
}
|
||
|
||
stufen = sapply(1:140, function(i) {
|
||
var = sprintf("pssi_%03d", i)
|
||
if (!(var %in% names(daten_pssi))) {
|
||
stop(paste0("Item-Spalte '", var, "' fehlt in daten_pssi."))
|
||
}
|
||
stufe_aus_item(zeile[[var]], var)
|
||
})
|
||
|
||
rohwerte = sapply(pssi_skala_anzeige_reihenfolge, function(sk) {
|
||
idx = pssi_item_map$item[pssi_item_map$skala == sk]
|
||
umgepolt = pssi_item_map$umgepolt[pssi_item_map$skala == sk]
|
||
werte = stufen[idx]
|
||
werte_final = ifelse(umgepolt, 3 - werte, werte)
|
||
sum(werte_final)
|
||
})
|
||
names(rohwerte) = pssi_skala_anzeige_reihenfolge
|
||
|
||
normtabelle_vorschlag = pssi_vorschlag_normtabelle(alter_num, geschlecht)
|
||
kein_vorschlag = is.null(normtabelle_vorschlag)
|
||
|
||
list(
|
||
chiffre = chiffre,
|
||
ausfuelldatum_anzeige = ausfuelldatum_anzeige,
|
||
info_mehrere = info_mehrere,
|
||
alter_num = alter_num,
|
||
geschlecht = geschlecht,
|
||
rohwerte = rohwerte,
|
||
normtabelle_vorschlag = normtabelle_vorschlag,
|
||
kein_vorschlag = kein_vorschlag,
|
||
error = NULL
|
||
)
|
||
})
|
||
|
||
output$fehler_ui = renderUI({
|
||
req(input$btn_suchen)
|
||
d = ergebnis_r()
|
||
if (!is.null(d$error)) div(class = "alert-fehler", d$error)
|
||
})
|
||
|
||
output$warnung_mehrfach_ui = renderUI({
|
||
req(input$btn_suchen)
|
||
d = ergebnis_r()
|
||
if (!is.null(d$error) || is.null(d$info_mehrere)) return(NULL)
|
||
div(class = "alert-warnung", d$info_mehrere)
|
||
})
|
||
|
||
output$warnung_normtabelle_ui = renderUI({
|
||
req(input$btn_suchen)
|
||
d = ergebnis_r()
|
||
if (!is.null(d$error) || !isTRUE(d$kein_vorschlag)) return(NULL)
|
||
div(class = "alert-warnung",
|
||
"Kein automatischer Normtabellen-Vorschlag moeglich (Alter und/oder Geschlecht ",
|
||
"nicht eindeutig auswertbar). Bitte Normtabelle manuell pruefen und auswaehlen."
|
||
)
|
||
})
|
||
|
||
output$normtabelle_auswahl_ui = renderUI({
|
||
req(input$btn_suchen)
|
||
d = ergebnis_r()
|
||
if (!is.null(d$error)) return(NULL)
|
||
|
||
default_key = if (isTRUE(d$kein_vorschlag)) "B1" else d$normtabelle_vorschlag
|
||
|
||
div(class = "abschnitt-karte",
|
||
div(class = "abschnitt-titel", "Normtabelle"),
|
||
selectInput("normtabelle_wahl", label = "Verwendete Normtabelle",
|
||
choices = setNames(pssi_normtabellen_meta$key, pssi_normtabellen_meta$label),
|
||
selected = default_key, width = "100%"),
|
||
uiOutput("normtabelle_info_ui")
|
||
)
|
||
})
|
||
|
||
output$normtabelle_info_ui = renderUI({
|
||
req(input$normtabelle_wahl)
|
||
meta = pssi_normtabellen_meta[pssi_normtabellen_meta$key == input$normtabelle_wahl, ]
|
||
if (nrow(meta) == 0) return(NULL)
|
||
div(class = "normtabelle-info",
|
||
tags$strong("Aktuell verwendet: "), meta$label[1]
|
||
)
|
||
})
|
||
|
||
profil_r = reactive({
|
||
req(input$btn_suchen, input$normtabelle_wahl)
|
||
d = ergebnis_r()
|
||
if (!is.null(d$error)) return(NULL)
|
||
|
||
tab = pssi_normtabellen[[input$normtabelle_wahl]]
|
||
zeilen = lapply(pssi_skala_anzeige_reihenfolge, function(sk) {
|
||
rw = d$rohwerte[[sk]]
|
||
lk = pssi_pr_t_lookup(tab, sk, rw)
|
||
data.frame(
|
||
skala = sk,
|
||
skala_name = unname(pssi_skalennamen[sk]),
|
||
rohwert = rw,
|
||
pr = lk$pr,
|
||
t = lk$t,
|
||
stringsAsFactors = FALSE
|
||
)
|
||
})
|
||
df = do.call(rbind, zeilen)
|
||
df$skala = factor(df$skala, levels = pssi_skala_anzeige_reihenfolge)
|
||
df
|
||
})
|
||
|
||
output$ergebnis_ui = renderUI({
|
||
req(input$btn_suchen)
|
||
d = ergebnis_r()
|
||
if (!is.null(d$error)) return(NULL)
|
||
profil = profil_r()
|
||
if (is.null(profil)) return(NULL)
|
||
|
||
tabellen_zeilen = lapply(seq_len(nrow(profil)), function(i) {
|
||
z = profil[i, ]
|
||
t_anzeige = if (is.na(z$t)) {
|
||
tags$span(style = "color:#999; font-style:italic;",
|
||
"kein T-Wert ausgewiesen (Boden-/Deckeneffekt)")
|
||
} else {
|
||
sprintf("%.0f", z$t)
|
||
}
|
||
pr_anzeige = if (is.na(z$pr)) "k. A." else sprintf("%.1f", z$pr)
|
||
tags$tr(
|
||
tags$td(paste0(z$skala, " – ", z$skala_name)),
|
||
tags$td(class = "zahl", z$rohwert),
|
||
tags$td(class = "zahl", pr_anzeige),
|
||
tags$td(class = "zahl", t_anzeige)
|
||
)
|
||
})
|
||
|
||
div(class = "abschnitt-karte",
|
||
div(class = "abschnitt-titel", "PSSI-Auswertung"),
|
||
|
||
div(class = "meta-block",
|
||
tags$strong("Chiffre: "), d$chiffre,
|
||
tags$span(" | ", style = "color:#ccc;"),
|
||
tags$strong("Ausfuelldatum: "), d$ausfuelldatum_anzeige,
|
||
tags$span(" | ", style = "color:#ccc;"),
|
||
tags$strong("Alter: "), if (is.na(d$alter_num)) "nicht auswertbar" else d$alter_num,
|
||
tags$span(" | ", style = "color:#ccc;"),
|
||
tags$strong("Geschlecht: "), if (is.na(d$geschlecht)) "nicht zuordenbar" else d$geschlecht
|
||
),
|
||
|
||
tags$hr(),
|
||
|
||
tags$h5("Skalenwerte"),
|
||
tags$table(class = "pssi-tabelle",
|
||
tags$thead(
|
||
tags$tr(
|
||
tags$th("Skala"), tags$th("Rohwert (0-30)"),
|
||
tags$th("Prozentrang"), tags$th("T-Wert")
|
||
)
|
||
),
|
||
tags$tbody(tabellen_zeilen)
|
||
),
|
||
|
||
tags$hr(),
|
||
|
||
tags$h5("Profildiagramm"),
|
||
plotOutput("profil_plot", height = "380px"),
|
||
|
||
div(class = "disclaimer-block", PSSI_DISCLAIMER)
|
||
)
|
||
})
|
||
|
||
output$profil_plot = renderPlot({
|
||
profil = profil_r()
|
||
req(profil)
|
||
pssi_profil_plot(profil)
|
||
})
|
||
|
||
output$download_word = downloadHandler(
|
||
filename = function() {
|
||
d = tryCatch(ergebnis_r(), error = function(e) NULL)
|
||
chiffre_esc = if (is.list(d) && is.null(d$error) && nchar(d$chiffre) > 0) {
|
||
gsub("[^A-Za-z0-9_-]", "_", d$chiffre)
|
||
} else {
|
||
"export"
|
||
}
|
||
ausfuelldatum_fn = tryCatch(
|
||
format(as.Date(d$ausfuelldatum_anzeige, "%d.%m.%Y"), "%Y%m%d"),
|
||
error = function(e) format(Sys.Date(), "%Y%m%d")
|
||
)
|
||
if (is.na(ausfuelldatum_fn) || length(ausfuelldatum_fn) == 0) {
|
||
ausfuelldatum_fn = format(Sys.Date(), "%Y%m%d")
|
||
}
|
||
paste0("PSSI_", chiffre_esc, "_", ausfuelldatum_fn, ".docx")
|
||
},
|
||
content = function(file) {
|
||
d = tryCatch(ergebnis_r(), error = function(e) NULL)
|
||
daten_ok = is.list(d) && is.null(d$error)
|
||
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()
|
||
}
|
||
profil = profil_r()
|
||
meta = pssi_normtabellen_meta[pssi_normtabellen_meta$key == input$normtabelle_wahl, ]
|
||
normtabelle_info = if (nrow(meta) > 0) meta$label[1] else input$normtabelle_wahl
|
||
|
||
erg = list(
|
||
chiffre = d$chiffre,
|
||
ausfuelldatum_anzeige = d$ausfuelldatum_anzeige,
|
||
info_mehrere = d$info_mehrere,
|
||
profil = profil
|
||
)
|
||
|
||
doc = tryCatch(
|
||
erstelle_pssi_docx(erg, normtabelle_info),
|
||
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)
|