buchungen in postings umbenannt
This commit is contained in:
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user