umsatzUI <- function(id) { ns <- NS(id) tagList( useShinyjs(), # Sync-Button oben div( style = "display: flex; justify-content: flex-end; margin-bottom: 8px;", actionBttn(ns("sync"), "Sync Hibiscus", size = "xs", style = "minimal", icon = icon("rotate"), color = "primary") ), # Buchungspanel immer sichtbar unten uiOutput(ns("buchungs_panel")), # Tabelle mit begrenzter Höhe und Scroll div( style = "max-height: 70vh; overflow-y: auto;", reactableOutput(ns("umsatz_table")) ) ) } umsatzServer <- function(id, conn, r_global) { moduleServer(id, function(input, output, session) { ns <- session$ns # ── Daten ---- refresh <- reactiveVal(0) zeige_alle <- reactiveVal(FALSE) umsatz_data <- reactive({ r_global$umsatz_refresh d <- read_hibiscus(conn) if (!r_global$umsatz_zeige_alle) d <- d %>% filter(!gebucht) d %>% arrange(desc(valuta)) }) # ── Filter-Buttons ---- observeEvent(input$filter_ungebucht, { zeige_alle(FALSE) }) observeEvent(input$filter_alle, { zeige_alle(TRUE) }) # ── Sync ─────────────────────────────────────────────────────────────────── observeEvent(input$sync, { showNotification("Sync läuft...", type = "message", duration = 3) tryCatch({ result <- sync_hibiscus(conn) r_global$umsatz_refresh <- isolate(r_global$umsatz_refresh) + 1 showNotification("✓ Sync abgeschlossen", type = "message") }, error = function(e) { showNotification(paste("Fehler:", e$message), type = "error") }) }) # ── Tabelle ──────────────────────────────────────────────────────────────── output$umsatz_table <- renderReactable({ reactable( umsatz_data() %>% select(id, valuta, empfaenger_name, empfaenger_konto, betrag, zweck, gebucht, posting_id, entry_id), striped = TRUE, highlight = TRUE, searchable = T, searchMethod = JS("function(rows, columnIds, filterValue) { var filter = filterValue.toLowerCase().replace(',', '.').replace(/\\s/g, ''); return rows.filter(function(row) { return columnIds.some(function(col) { var val = row.values[col]; var str = val === null || val === undefined ? '' : String(val); var strNorm = str.toLowerCase().replace(',', '.').replace(/\\s/g, ''); return strNorm.includes(filter) || (typeof val === 'number' && String(Math.abs(val)).includes(filter)); }); }); }"), filterable = F, # ← das reicht pagination = F, selection = "single", onClick = "select", defaultSorted = list(valuta = "desc"), columns = list( id = colDef(show = FALSE), posting_id = colDef(show = FALSE), entry_id = colDef(show = FALSE), valuta = colDef(name = "Datum", maxWidth = 120), empfaenger_name = colDef(name = "Empfänger", minWidth = 150), empfaenger_konto = colDef(name = "IBAN", minWidth = 150), betrag = colDef( name = "Betrag", maxWidth = 110, align = "right", format = colFormat(currency = "EUR", separators = TRUE, locales = "de-DE") ), zweck = colDef(name = "Zweck", minWidth = 200), gebucht = colDef( name = "Gebucht", maxWidth = 80, align = "center", cell = function(value) if (value) "✓" else "—" ) ), rowStyle = function(index) { if (umsatz_data()$gebucht[index]) list(color = "gray", opacity = "0.5") } ) }) # ── Selektierter Umsatz ──────────────────────────────────────────────────── sel <- reactive(getReactableState("umsatz_table", "selected")) selected_umsatz <- reactive({ req(sel()) umsatz_data()[sel(), ] }) # ── Buchungs-Panel ───────────────────────────────────────────────────────── output$buchungs_panel <- renderUI({ req(selected_umsatz()) u <- selected_umsatz() if (u$gebucht) { tagList( h4("Bereits gebucht"), actionBttn(ns("goto_buchung"), paste0("→ Zur Buchung (Entry #", u$entry_id, ")"), size = "sm", style = "minimal", color = "primary") ) } else { tagList( h4("Buchen"), fluidRow( column(4, selectizeInput(ns("konto"), "Konto:", choices = get_account_choices(conn), selected = 0, width = "100%") ), column(3, selectizeInput(ns("projekt"), "Projekt:", choices = get_project_choices(conn), width = "100%") ), column(3, selectizeInput(ns("kontakt"), "Kontakt:", choices = get_contact_choices(conn), selected = resolve_contact(conn, u$empfaenger_name, u$empfaenger_konto), width = "100%") ), column(1, div(style = "margin-top: 25px;", # Label-Höhe ausgleichen actionBttn(ns("new_contact"), "Neuer Kontakt", size = "xs", style = "minimal", icon = icon("plus"), color = "warning") ) ), column(1, div(style = "margin-top: 25px;", actionBttn(ns("buchen"), "Buchen", size = "sm", style = "minimal", color = "success", icon = icon("check")) ) ) ) ) } }) # ── Zur Buchung springen ─────────────────────────────────────────────────── observeEvent(input$goto_buchung, { req(selected_umsatz()) r_global$nav_history <- c(r_global$nav_history, list(list(tab = "umsatz"))) r_global$jump_to_entry_id <- selected_umsatz()$entry_id r_global$active_tab <- "buchungen" }) # ── Buchen ──────────────────────────────────────────────────────────────── observeEvent(input$buchen, { req(selected_umsatz(), input$konto) u <- selected_umsatz() gegenkonto_id <- dbReadTable(conn, "accounts") %>% filter(hibiscus_account_id == u$konto_id) %>% pull(id) req(length(gegenkonto_id) > 0, input$konto != 0) new_t_id <- max_id(conn, "entries") + 1L dbxInsert(conn, "entries", data.frame( id = new_t_id, contact_id = as.integer(input$kontakt), note = u$zweck )) p_id1 <- max_id(conn, "postings") + 1L dbxInsert(conn, "postings", data.frame( id = c(p_id1, p_id1 + 1L), entry_id = c(new_t_id, new_t_id), account_id = c(as.integer(input$konto), gegenkonto_id), project_id = c(as.integer(input$projekt), NA_integer_), amount = c(u$betrag, -u$betrag), valuta = c(as.character(u$valuta), as.character(u$valuta)), booking_date = c(as.character(u$datum), as.character(u$datum)), bank_transaction_id = c(u$id, NA_integer_) )) refresh(refresh() + 1) showNotification("✓ Gebucht", type = "message") }) # ── Neuer Kontakt ────────────────────────────────────────────────────────── observeEvent(input$new_contact, { req(selected_umsatz()) u <- selected_umsatz() showModal(modalDialog( title = "Neuer Kontakt", textInput(ns("new_contact_name"), "Name", value = u$empfaenger_name), textInput(ns("new_contact_iban"), "IBAN", value = u$empfaenger_konto), footer = tagList( modalButton("Abbrechen"), actionBttn(ns("save_contact"), "Speichern", style = "minimal", color = "success") ) )) }) observeEvent(input$save_contact, { req(input$new_contact_name) u <- selected_umsatz() new_c_id <- max_id(conn, "contacts") + 1L dbxInsert(conn, "contacts", data.frame( id = new_c_id, display_name = input$new_contact_name )) new_bc_id <- max_id(conn, "bank_connections") + 1L dbxInsert(conn, "bank_connections", data.frame( id = new_bc_id, contact_id = new_c_id, iban = input$new_contact_iban, remote_name = u$empfaenger_name )) removeModal() updateSelectizeInput(session, "kontakt", choices = get_contact_choices(conn), selected = new_c_id ) showNotification("✓ Kontakt angelegt", type = "message") }) }) }