Files
2026-04-28 15:40:44 +02:00

338 lines
12 KiB
R
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
umsatzUI <- function(id) {
ns <- NS(id)
tagList(
useShinyjs(),
# Sync-Button oben
div(
style = "display: flex; justify-content: flex-end; margin-bottom: 8px; gap: 8px;",
actionBttn(ns("deselect_all"), "Auswahl aufheben",
size = "xs", style = "minimal",
icon = icon("xmark"), color = "default"),
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({
# remote_names einmal laden
known_names <- dbGetQuery(conn, "SELECT contact_text FROM bank_connections")$contact_text
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,
pagination = F,
selection = "multiple",
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,
style = function(value) {
if (!is.na(value) && !(value %in% known_names)) {
list(color = "red", fontWeight = "bold")
}
}
),
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())
rows <- selected_umsatz()
alle_gebucht <- all(rows$gebucht)
manche_gebucht <- any(rows$gebucht) && !alle_gebucht
keine_gebucht <- !any(rows$gebucht)
if (alle_gebucht && nrow(rows) == 1) {
# Einzelauswahl, bereits gebucht → Sprung-Button
tagList(
h4("Bereits gebucht"),
actionBttn(ns("goto_buchung"),
paste0("→ Zur Buchung (Entry #", rows$entry_id, ")"),
size = "sm", style = "minimal", color = "primary")
)
} else if (manche_gebucht) {
div(style = "color: orange;",
icon("triangle-exclamation"),
strong(paste0(sum(rows$gebucht), " von ", nrow(rows),
" Umsätzen bereits gebucht bitte nur ungebuchte auswählen."))
)
} else {
# Alle ungebucht → Buchungsformular
info_text <- if (nrow(rows) > 1) {
tags$p(style = "color: steelblue; font-size: 0.9em;",
icon("info-circle"),
paste0(nrow(rows), " Umsätze ausgewählt ",
"Konto/Projekt/Kontakt werden für alle verwendet.")
)
}
# Kontakt: bei Mehrfachauswahl keinen Auto-Resolve
kontakt_selected <- if (nrow(rows) == 1) {
resolve_contact(conn, rows$empfaenger_name, rows$empfaenger_konto)
} else {
NULL
}
tagList(
h4(if (nrow(rows) == 1) "Buchen" else paste0(nrow(rows), " Umsätze buchen")),
info_text,
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 = kontakt_selected,
width = "100%")
),
column(1,
div(style = "margin-top: 25px;",
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, input$konto != 0)
rows <- selected_umsatz()
rows <- rows %>% filter(!gebucht)
req(nrow(rows) > 0)
bank_connections <- dbReadTable(conn, "bank_connections")
for (i in seq_len(nrow(rows))) {
u <- rows[i, ]
gegenkonto_id <- dbReadTable(conn, "accounts") %>%
filter(hibiscus_account_id == u$konto_id) %>%
pull(id)
if (length(gegenkonto_id) == 0) next
# contact_id: manuell gewählt oder aus bank_connections
contact_id <- if (!is.null(input$kontakt) && as.integer(input$kontakt) != 0) {
as.integer(input$kontakt)
} else {
match <- bank_connections %>%
filter(contact_text == u$empfaenger_name) %>%
pull(contact_id)
if (length(match) > 0) as.integer(match[[1]]) else NA_integer_
}
new_t_id <- max_id(conn, "entries") + 1L
dbxInsert(conn, "entries", data.frame(
id = new_t_id,
contact_id = contact_id,
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(
paste0("✓ ", nrow(rows), " Umsatz/Umsätze gebucht"),
type = "message"
)
})
# ── Neuer Kontakt / Bankverbindung ----
observeEvent(input$new_contact, {
req(selected_umsatz())
u <- selected_umsatz()
contact_choices <- get_contact_choices(conn)
showModal(modalDialog(
title = "Neuer Kontakt",
selectizeInput(ns("existing_contact_id"),
"Vorhandenen Kontakt verknüpfen (optional)",
choices = contact_choices,
selected = 0,
options = list(placeholder = "Kontakt suchen...")
),
hr(),
conditionalPanel(
condition = paste0("input['", ns("existing_contact_id"), "'] == '0'"),
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")
)
))
})
# ** Neuen Kontakt/Bvb speichern
observeEvent(input$save_contact, {
u <- selected_umsatz()
existing_id <- as.integer(input$existing_contact_id)
if (!is.na(existing_id) && existing_id != 0L) {
# ── Nur bank_connection anlegen ----
target_contact_id <- existing_id
} else {
# ── Neuen Kontakt + bank_connection anlegen ----
req(input$new_contact_name)
target_contact_id <- max_id(conn, "contacts") + 1L
dbxInsert(conn, "contacts", data.frame(
id = target_contact_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 = target_contact_id,
iban = input$new_contact_iban,
contact_text = u$empfaenger_name
))
removeModal()
updateSelectizeInput(session, "kontakt",
choices = get_contact_choices(conn),
selected = target_contact_id
)
msg <- if (existing_id != 0L) "✓ Bankverbindung verknüpft" else "✓ Kontakt angelegt"
showNotification(msg, type = "message")
})
## ** Auswahl aufheben ----
observeEvent(input$deselect_all, {
updateReactable("umsatz_table", selected = NA, session = session)
})
})
}