253 lines
8.5 KiB
R
253 lines
8.5 KiB
R
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 |