buchungen in postings umbenannt

This commit is contained in:
2026-03-09 13:52:59 +01:00
parent 788a355597
commit da524d4af5
3 changed files with 380 additions and 1 deletions
@@ -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
+378
View File
@@ -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)
Binary file not shown.