From da524d4af57ae8ce4361b1d5d6ef983142d523fb Mon Sep 17 00:00:00 2001 From: Christian Oswald Date: Mon, 9 Mar 2026 13:52:59 +0100 Subject: [PATCH] buchungen in postings umbenannt --- R/{buchungen => postings}/buchungen.R | 3 +- R/todo.R | 378 ++++++++++++++++++ ...development.sqlite3 => development.sqlite} | Bin 1011712 -> 1011712 bytes 3 files changed, 380 insertions(+), 1 deletion(-) rename R/{buchungen => postings}/buchungen.R (85%) create mode 100644 R/todo.R rename db/{development.sqlite3 => development.sqlite} (96%) diff --git a/R/buchungen/buchungen.R b/R/postings/buchungen.R similarity index 85% rename from R/buchungen/buchungen.R rename to R/postings/buchungen.R index de8ef24..0abfcf7 100644 --- a/R/buchungen/buchungen.R +++ b/R/postings/buchungen.R @@ -9,7 +9,7 @@ read_buch_tabelle <- function(conn){ left_join(contacts, by = c("contact_id" = "id")) |> # contact_id jetzt verfügbar left_join(accounts, by = c("account_id" = "id")) |> left_join(projects, by = c("project_id" = "id")) |> - select(id, valuta, konto, projektname, display_name, amount, entry_id) |> + select(id, valuta, kontoname, projektname, display_name, amount, entry_id) |> collect() } @@ -19,3 +19,4 @@ read_posting <- function(conn, id){ read_postings_by_entry <- function(conn, id){ dbxSelect(conn, paste0("SELECT * FROM postings WHERE entry_id =", id)) } +tbl(conn, "accounts") %>% collect %>% str diff --git a/R/todo.R b/R/todo.R new file mode 100644 index 0000000..2f3ba01 --- /dev/null +++ b/R/todo.R @@ -0,0 +1,378 @@ +library(shiny) +library(shinyTree) + +# ---- UI ---- +ui <- fluidPage( + tags$head( + tags$style(HTML(" + @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Playfair+Display:wght@700&display=swap'); + + * { box-sizing: border-box; margin: 0; padding: 0; } + + body { + background: #0f0f0f; + color: #e8e4dc; + font-family: 'DM Mono', monospace; + min-height: 100vh; + padding: 40px 20px; + } + + .app-title { + font-family: 'Playfair Display', serif; + font-size: 2.8rem; + color: #f0e6c8; + letter-spacing: -1px; + margin-bottom: 6px; + } + + .app-subtitle { + font-size: 0.75rem; + color: #5a5550; + letter-spacing: 4px; + text-transform: uppercase; + margin-bottom: 40px; + } + + .main-container { + max-width: 900px; + margin: 0 auto; + } + + .panel { + background: #1a1a18; + border: 1px solid #2e2b26; + border-radius: 4px; + padding: 28px; + margin-bottom: 20px; + } + + .panel-title { + font-size: 0.65rem; + letter-spacing: 5px; + text-transform: uppercase; + color: #8a7e6e; + margin-bottom: 20px; + padding-bottom: 12px; + border-bottom: 1px solid #2e2b26; + } + + .form-row { + display: flex; + gap: 10px; + align-items: flex-end; + flex-wrap: wrap; + } + + .form-group { + display: flex; + flex-direction: column; + gap: 6px; + } + + .form-group label { + font-size: 0.65rem; + letter-spacing: 3px; + text-transform: uppercase; + color: #6b6158; + } + + input[type='text'], select { + background: #0f0f0f; + border: 1px solid #2e2b26; + color: #e8e4dc; + font-family: 'DM Mono', monospace; + font-size: 0.85rem; + padding: 10px 14px; + border-radius: 3px; + outline: none; + transition: border-color 0.2s; + min-width: 200px; + } + + input[type='text']:focus, select:focus { + border-color: #c8a96e; + } + + select option { + background: #1a1a18; + } + + .btn-add { + background: #c8a96e; + color: #0f0f0f; + border: none; + font-family: 'DM Mono', monospace; + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 3px; + text-transform: uppercase; + padding: 11px 22px; + border-radius: 3px; + cursor: pointer; + transition: background 0.2s, transform 0.1s; + } + + .btn-add:hover { background: #dbb97e; } + .btn-add:active { transform: scale(0.97); } + + .btn-del { + background: transparent; + color: #8a4040; + border: 1px solid #3d2020; + font-family: 'DM Mono', monospace; + font-size: 0.7rem; + letter-spacing: 2px; + text-transform: uppercase; + padding: 10px 18px; + border-radius: 3px; + cursor: pointer; + transition: all 0.2s; + } + + .btn-del:hover { + background: #2a1515; + border-color: #8a4040; + color: #c46060; + } + + /* shinyTree overrides */ + #todo_tree { + margin-top: 10px; + } + + .jstree-default .jstree-anchor { + font-family: 'DM Mono', monospace !important; + font-size: 0.88rem !important; + color: #c8c0b0 !important; + padding: 3px 6px !important; + } + + .jstree-default .jstree-hovered { + background: #252520 !important; + border-radius: 3px !important; + box-shadow: none !important; + } + + .jstree-default .jstree-clicked { + background: #2a2820 !important; + border-left: 2px solid #c8a96e !important; + border-radius: 3px !important; + box-shadow: none !important; + color: #f0e6c8 !important; + } + + .jstree-default .jstree-icon { + color: #5a5048 !important; + } + + .jstree-default .jstree-node { + margin: 2px 0 !important; + } + + /* category badges */ + .badge-arbeit { color: #7ab8d4; } + .badge-privat { color: #9dbb7a; } + .badge-einkauf { color: #d4a87a; } + .badge-projekt { color: #c47ab8; } + + .status-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + font-size: 0.7rem; + color: #5a5550; + letter-spacing: 2px; + text-transform: uppercase; + } + + #selected_info { + font-size: 0.78rem; + color: #8a7e6e; + padding: 8px 0; + min-height: 28px; + } + ")) + ), + + div(class = "main-container", + + tags$h1(class = "app-title", "TODO"), + tags$p(class = "app-subtitle", "hierarchische aufgabenliste"), + + # ADD FORM + div(class = "panel", + div(class = "panel-title", "Neue Aufgabe"), + div(class = "form-row", + div(class = "form-group", + tags$label(`for` = "task_name", "Aufgabe"), + textInput("task_name", label = NULL, placeholder = "Aufgabe eingeben…", width = "280px") + ), + div(class = "form-group", + tags$label(`for` = "task_cat", "Kategorie"), + selectInput("task_cat", label = NULL, + choices = c("Arbeit", "Privat", "Einkauf", "Projekt"), + width = "160px") + ), + div(class = "form-group", + tags$label(`for` = "parent_choice", "Unter-Aufgabe von"), + selectInput("parent_choice", label = NULL, + choices = c("— (Hauptaufgabe)" = ""), + width = "220px") + ), + div(style = "margin-bottom: 1px;", + actionButton("add_task", "Hinzufügen", class = "btn-add") + ) + ) + ), + + # TREE + div(class = "panel", + div(class = "panel-title", "Aufgaben"), + div(id = "selected_info", "Klicke auf eine Aufgabe zum Auswählen"), + shinyTree("todo_tree", + checkbox = TRUE, + theme = "default", + themeIcons = TRUE, + themeDots = TRUE, + dragAndDrop = TRUE + ), + tags$br(), + div(class = "status-bar", + div(id = "task_count", ""), + actionButton("delete_task", "Auswahl löschen", class = "btn-del") + ) + ) + ) +) + +# ---- SERVER ---- +server <- function(input, output, session) { + + # Reactive store: list of tasks + # Each task: list(id, label, category, parent_id) + tasks <- reactiveVal(list( + list(id = "t1", label = "Projektplanung", category = "Projekt", parent_id = ""), + list(id = "t2", label = "Meeting vorbereiten", category = "Arbeit", parent_id = "t1"), + list(id = "t3", label = "Folien erstellen", category = "Arbeit", parent_id = "t1"), + list(id = "t4", label = "Einkaufsliste", category = "Einkauf", parent_id = ""), + list(id = "t5", label = "Milch & Eier", category = "Einkauf", parent_id = "t4"), + list(id = "t6", label = "Sport", category = "Privat", parent_id = "") + )) + + id_counter <- reactiveVal(7) + + # Helper: build nested list for shinyTree from flat task list + build_tree <- function(tlist, parent_id = "") { + children <- Filter(function(t) t$parent_id == parent_id, tlist) + if (length(children) == 0) return(NULL) + + result <- lapply(children, function(t) { + subtree <- build_tree(tlist, t$id) + icon_class <- switch(tolower(t$category), + "arbeit" = "glyphicon glyphicon-briefcase", + "privat" = "glyphicon glyphicon-heart", + "einkauf" = "glyphicon glyphicon-shopping-cart", + "projekt" = "glyphicon glyphicon-star", + "glyphicon glyphicon-ok" + ) + node <- list( + text = paste0(t$label, " [", t$category, "]"), + icon = icon_class, + `data-id` = t$id + ) + attr(node, "stopSign") <- FALSE + if (!is.null(subtree)) node$children <- subtree + node + }) + names(result) <- sapply(children, function(t) t$label) + result + } + + # Render tree + output$todo_tree <- renderTree({ + build_tree(tasks()) + }) + + # Update parent selector + observe({ + tlist <- tasks() + choices <- c("— (Hauptaufgabe)" = "") + for (t in tlist) choices[t$label] <- t$id + updateSelectInput(session, "parent_choice", choices = choices) + }) + + # Task count + observe({ + n <- length(tasks()) + updateTextInput(session, "task_count_dummy") # just trigger + }) + + output$task_count <- renderText(NULL) + observe({ + n <- length(tasks()) + session$sendCustomMessage("updateCount", + list(id = "task_count", text = paste(n, "aufgaben gesamt"))) + }) + + tags$script(" + Shiny.addCustomMessageHandler('updateCount', function(msg) { + document.getElementById(msg.id).textContent = msg.text; + }); + ") + + # Add task + observeEvent(input$add_task, { + req(input$task_name != "") + new_id <- paste0("t", id_counter()) + id_counter(id_counter() + 1) + new_task <- list( + id = new_id, + label = trimws(input$task_name), + category = input$task_cat, + parent_id = input$parent_choice + ) + tasks(c(tasks(), list(new_task))) + updateTextInput(session, "task_name", value = "") + }) + + # Show selected + observeEvent(input$todo_tree, { + sel <- get_selected(input$todo_tree, format = "names") + if (length(sel) > 0) { + session$sendCustomMessage("updateCount", + list(id = "selected_info", + text = paste0("✦ Ausgewählt: ", paste(sel, collapse = ", ")))) + } else { + session$sendCustomMessage("updateCount", + list(id = "selected_info", text = "Klicke auf eine Aufgabe zum Auswählen")) + } + }) + + # Delete selected tasks (by label matching) + observeEvent(input$delete_task, { + sel <- get_selected(input$todo_tree, format = "names") + if (length(sel) == 0) return() + + tlist <- tasks() + # Get labels without category suffix + sel_labels <- trimws(gsub("\\s*\\[.*?\\]$", "", sel)) + + # IDs to delete (including children recursively) + delete_ids <- function(ids_to_delete, all_tasks) { + children <- Filter(function(t) t$parent_id %in% ids_to_delete, all_tasks) + if (length(children) == 0) return(ids_to_delete) + child_ids <- sapply(children, function(t) t$id) + c(ids_to_delete, delete_ids(child_ids, all_tasks)) + } + + matched_ids <- sapply( + Filter(function(t) t$label %in% sel_labels, tlist), + function(t) t$id + ) + all_del <- delete_ids(matched_ids, tlist) + tasks(Filter(function(t) !(t$id %in% all_del), tlist)) + }) +} + +shinyApp(ui, server) \ No newline at end of file diff --git a/db/development.sqlite3 b/db/development.sqlite similarity index 96% rename from db/development.sqlite3 rename to db/development.sqlite index f7eecda5c457ec202de46fc60523f79711bec1ba..64a6c2c40c0537d82753b0a3d7c164598a98dff1 100644 GIT binary patch delta 11293 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 SeQuS{9p-bZeQu4P0