library(shiny) library(shinyjs) library(shinyTree) library(reactable) library(dplyr) library(DBI) # ── UI ──────────────────────────────────────────────────────────────────────── accountsUI <- function(id) { ns <- NS(id) tagList( useShinyjs(), sidebarLayout( sidebarPanel( width = 3, h4("Konten"), shinyTree(ns("account_tree"), checkbox = FALSE, search = FALSE) ), mainPanel( width = 9, # Accounts-Felder wellPanel( fluidRow( column(12, actionButton(ns("new_account"), "Konto neu", class = "btn-success", icon = icon("plus")), hr() ) ), fluidRow( column(4, textInput(ns("account_name"), "Kontoname"), textInput(ns("bank_name"), "Bank") ), column(4, numericInput(ns("hibiscus_id"), "Hibiscus Account ID", value = NA), numericInput(ns("budget_id"), "Budget ID", value = NA) ), column(4, # ID read-only anzeigen disabled(numericInput(ns("account_id"), "ID", value = NA)), br(), actionButton(ns("save_account"), "Speichern", class = "btn-primary", icon = icon("save")), textOutput(ns("save_status")) ) ) ), # Buchungen h4("Buchungen"), reactableOutput(ns("postings_table")) ) ) ) } # ── Server ---- accountsServer <- function(id, conn, r_global) { moduleServer(id, function(input, output, session) { ns <- session$ns # ── Refresh-Trigger ---- refresh <- reactiveVal(0) # ── Kontendaten ---- accounts <- reactive({ refresh() dbReadTable(conn, "accounts") %>% arrange(account_name) }) # ── Baum aufbauen ---- tree_data <- reactive({ refresh() build_account_tree(conn) }) output$account_tree <- renderTree({ tree_data() }) # ── Gewähltes Konto ──────────────────────────────────────────────────────── selected_name <- reactive({ sel <- get_selected(input$account_tree, format = "names") cat("raw selected:", paste(sel, collapse=", "), "\n") if (length(sel) == 0) return(NULL) name <- sub(" \\(\\d+\\)$", "", sel[[1]]) cat("cleaned name:", name, "\n") name }) selected_account <- reactive({ req(selected_name()) df <- accounts() df[df$account_name == selected_name(), ] }) # ── Inputs befüllen ---- observeEvent(selected_account(), { acc <- selected_account() req(nrow(acc) > 0) updateNumericInput(session, "account_id", value = acc$id) updateTextInput(session, "account_name", value = acc$account_name) updateTextInput(session, "bank_name", value = acc$bank_name %||% "") updateNumericInput(session, "hibiscus_id", value = acc$hibiscus_account_id) updateNumericInput(session, "budget_id", value = acc$budget_id) }) # ── Neu-Button ---- observeEvent(input$new_account, { neue_id <- max_id(conn, "accounts") + 1L updateNumericInput(session, "account_id", value = neue_id) updateTextInput(session, "account_name", value = "") updateTextInput(session, "bank_name", value = "") updateNumericInput(session, "hibiscus_id", value = NA) updateNumericInput(session, "budget_id", value = 0) }) # ── Buchungsanzahl für gewähltes Konto ---- has_postings <- reactive({ req(input$account_id) count <- tbl(conn, "postings") %>% filter(account_id == !!input$account_id) %>% count() %>% pull(n) count > 0 }) # ── Lösch-Button sperren/freigeben ---- observe({ req(input$account_id) if (has_postings()) { shinyjs::disable("delete_account") shinyjs::runjs(sprintf( "$('#%s').attr('title', 'Konto hat Buchungen und kann nicht gelöscht werden')", ns("delete_account") )) } else { shinyjs::enable("delete_account") shinyjs::runjs(sprintf( "$('#%s').removeAttr('title')", ns("delete_account") )) } }) # ── Speichern (upsert ---- observeEvent(input$save_account, { record <- data.frame( id = input$account_id, account_name = input$account_name, bank_name = input$bank_name, hibiscus_account_id = input$hibiscus_id, budget_id = input$budget_id, stringsAsFactors = FALSE ) dbxUpsert(conn, "accounts", record, where_cols = c("id")) refresh(refresh() + 1) output$save_status <- renderText("✓ Gespeichert") shinyjs::delay(2000, output$save_status <- renderText("")) }) # ── Löschen ---- observeEvent(input$delete_account, { req(input$account_id) showModal(modalDialog( title = "Konto löschen", paste0("Konto '", input$account_name, "' wirklich löschen?"), footer = tagList( modalButton("Abbrechen"), actionButton(ns("delete_confirm"), "Ja, löschen", class = "btn-danger") ) )) }) observeEvent(input$delete_confirm, { dbxDelete(conn, "accounts", data.frame(id = input$account_id)) removeModal() refresh(refresh() + 1) output$save_status <- renderText("✓ Gelöscht") shinyjs::delay(2000, output$save_status <- renderText("")) updateNumericInput(session, "account_id", value = NA) updateTextInput(session, "account_name", value = "") updateTextInput(session, "bank_name", value = "") updateNumericInput(session, "hibiscus_id", value = NA) updateNumericInput(session, "budget_id", value = NA) }) # ── Buchungen laden (read-only ---- postings_data <- reactive({ req(selected_account()) acc <- selected_account() req(nrow(acc) > 0) refresh() read_buch_tabelle(conn) %>% filter(account_id == acc$id) }) # ── Reactable ---- output$postings_table <- renderReactable({ req(postings_data()) reactable( postings_data() %>% select(valuta, projektname, display_name, amount, n_attachments), striped = TRUE, highlight = TRUE, selection = "single", onClick = "select", defaultSorted = list(valuta = "desc"), columns = list( valuta = colDef(name = "Datum", maxWidth = 100), projektname = colDef(name = "Projekt", maxWidth = 120), display_name = colDef(name = "Kontakt"), amount = colDef( name = "Betrag", maxWidth = 110, align = "right", format = colFormat(currency = "EUR", separators = TRUE, locales = "de-DE") ), n_attachments = colDef(name = "Anhänge", maxWidth = 80, align = "center") ) ) }) # ── Sprung zu Buchungen via r_global ---- sel <- reactive(getReactableState("postings_table", "selected")) observeEvent(sel(), ignoreInit = TRUE, { req(sel(), postings_data()) entry_id <- postings_data()$entry_id[sel()] # In accountsServer vor dem Sprung: r_global$nav_history <- c(r_global$nav_history, list(list( tab = "konten", account_name = selected_name() ))) r_global$jump_to_entry_id <- entry_id r_global$active_tab <- "buchungen" r_global$jump_to_entry_id <- entry_id r_global$active_tab <- "buchungen" }) # ── Rückgabe ─────────────────────────────────────────────────────────────── return(list( selected_account = selected_account )) }) } # ── Hilfsfunktion ────────────────────────────────────────────────────────────── `%||%` <- function(a, b) if (is.na(a) || is.null(a) || a == "") b else a