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 f7eecda..64a6c2c 100644 Binary files a/db/development.sqlite3 and b/db/development.sqlite differ