Files
2026-04-28 14:42:24 +02:00

252 lines
8.5 KiB
R
Executable File

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")
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