Buchungen funktioniern mitsamt modal und laufendem saldo

This commit is contained in:
2026-03-09 15:41:08 +01:00
parent da524d4af5
commit 5cb4eb96e8
9 changed files with 182 additions and 163 deletions
+35
View File
@@ -0,0 +1,35 @@
##
## Datum: 2024-10-17_11-38
## Name: Christian Oswald
## Projekt: etatverwaltung
## Datei: f_airdatepicker.R
## Kommentar: Funktionen für den Airdatepicker
##
## * Update unter Berücksichtigung nicht vorhandern Werte
f_updateAirdate <- function(session, id, wert){
if(is.na(wert)){
updateAirDateInput(session, id, clear = TRUE)
}else{
updateAirDateInput(session, id, value = wert)
}
}
## * Format für den Picker
f_airdatepicker_UI <- function(id, label, value) {
ns <- NS(id)
airDatepickerInput(
id,
label = label,
value = value,
width = "100%",
update_on = "close",
readonly = FALSE, # Manuelle Eingabe erlauben
onkeydown = "Shiny.setInputValue(this.id, this.value, {priority: 'event'});", # Übergabe an Shiny
todayButton = T)
}
# f_airdatepicker_UI(id, label, value)
+20
View File
@@ -0,0 +1,20 @@
f_reactable <- function(daten, coldefs = NULL){
reactable(daten,
selection = "single",
bordered = TRUE,
defaultPageSize = 17,
filterable = TRUE,
highlight = TRUE,
striped = F,
theme = reactableTheme(
highlightColor = "#00f200",
borderColor = "#dfe2e5",
rowSelectedStyle = list(backgroundColor = "#98F5FF"),
),
compact = TRUE,
rowStyle = list(cursor = "pointer"),
onClick = "select",
columns = coldefs
)
}
+3 -3
View File
@@ -6,8 +6,8 @@ read_accounts <- function(conn){
get_account_choices <- function(conn) { get_account_choices <- function(conn) {
tbl(conn, "accounts") |> tbl(conn, "accounts") |>
select(id, konto) |> select(id, account_name) |>
collect() |> collect() |>
arrange(konto) |> arrange(account_name) |>
(\(df) setNames(df$id, df$konto))() (\(df) setNames(df$id, df$account_name))()
} }
+25 -69
View File
@@ -1,92 +1,48 @@
buchungenUI <- function(id) { buchungenUI <- function(id) {
ns <- NS(id) ns <- NS(id)
tagList( tagList(
reactableOutput(ns("buchungen_table")) reactableOutput(ns("buchungen_table")),
reactableOutput(ns("details_table")),
postingModuleUI(ns("posting_modal"))
) )
} }
buchungenServer <- function(id) { buchungenServer <- function(id) {
moduleServer( id, function(input, output, session) { moduleServer(id, function(input, output, session) {
ns <- session$ns ns <- session$ns
postings <- reactiveVal(read_buch_tabelle(conn)) postings <- reactiveVal(read_buch_tabelle(conn))
# NEU: Eine reactive variable für die ID, die wir editieren wollen
# current_entry_id <- reactiveVal(NULL)
# NEU: Den Modul-Server EINMALIG hier oben starten (nicht im Observer!)
# entryEditServer("entry_edit", entry_id = current_entry_id, conn = conn)
output$buchungen_table <- renderReactable({ output$buchungen_table <- renderReactable({
reactable( # req(postings())
postings(), f_reactable(postings(), coldefs = coldef_entries_tabelle)
onClick = "expand",
selection = "single",
details = function(index) {
entry_id <- postings()$entry_id[index]
detail_rows <- postings()[postings()$entry_id == entry_id, ]
div(
style = "padding: 10px; background: #f4f4f4; border-left: 4px solid #3c8dbc; max-width: 800px; margin-left: auto",
reactable(
dplyr::select(detail_rows, konto, projektname, amount),
fullWidth = TRUE,
columns = list(
konto = colDef(name = "Konto", minWidth = 150),
projektname = colDef(name = "Projekt", minWidth = 150),
amount = colDef(name = "Betrag", minWidth = 80)
)
)
)
},
columns = list(
id = colDef(name = "ID", minWidth = 80),
valuta = colDef(name = "Wertstellung", minWidth = 80),
konto = colDef(name = "Kontoname", minWidth = 200),
entry_id = colDef(show = FALSE)
)
)
}) })
# Modal zum editieren # Modal zum editieren
selected <- reactive(getReactableState("buchungen_table", "selected")) selected <- reactive(getReactableState("buchungen_table", "selected"))
observeEvent(selected(),{
idwert <- postings()[selected(), "id"]
selected_row <- read_posting(conn, idwert) %>%
mutate(verzicht = ifelse(is.na(verzicht), F, verzicht))
showModal(modalDialog(
title = "Buchung bearbeiten",
size = "l",
tags$style(HTML(".modal-dialog { max-width: 90% !important; width: 90% !important; }")),
entryEditUI(ns("entry_edit")),
footer = tagList(
modalButton("Abbrechen"),
actionButton(ns("speichern"), "Speichern")
)
))
entryEditServer("entry_edit", entry_id = selected_row$entry_id, conn = conn)
}, ignoreInit = TRUE)
observeEvent(input$speichern, { observeEvent(selected(), {
browser() idwert <- postings()[selected(),"entry_id"] %>% pull
dbExecute(conn, "UPDATE postings SET details <- read_buch_tabelle(conn, trans_id = idwert)
account_id = ?, project_id = ?, amount = ?, valuta = ?, output$details_table <- renderReactable(
notiz = ?, rechnungsnummer = ?, betrag_muenzen = ?, verzicht = ? f_reactable(details, coldefs = coldef_entries_tabelle)
WHERE id = ?",
params = list(
input$account_id, input$project_id, input$amount, input$valuta,
input$notiz, input$rechnungsnummer, input$betrag_muenzen, input$verzicht,
selected_row$id
)
) )
removeModal() })
buchungen(read_buch_tabelle(conn)) # Tabelle neu laden
selected_det <- reactive(getReactableState("details_table", "selected"))
observeEvent(selected_det(),{
idwert <- postings()[selected(),"id"] %>% pull
postingModuleServer("posting_modal",conn, idwert)
}) })
})
}
)
} }
+39 -81
View File
@@ -1,93 +1,51 @@
# entry_edit_mod.R
entryEditUI <- function(id) { # --- MODUL UI ---
postingModuleUI <- function(id) {
ns <- NS(id) ns <- NS(id)
tagList( tagList(
uiOutput(ns("entry_ui")),
uiOutput(ns("postings_ui"))
) )
} }
entryEditServer <- function(id, entry_id, conn) { # --- MODUL SERVER ---
postingModuleServer <- function(id, conn, idwert) {
moduleServer(id, function(input, output, session) { moduleServer(id, function(input, output, session) {
ns <- session$ns ns <- session$ns # Wichtig für das Namespacing innerhalb des Modals
record <- read_posting(conn, idwert)
trans <- dbxSelect(conn, paste0("SELECT * FROM entries WHERE id=", record$entry_id))
# Modal öffnen
entry_postings <- reactiveVal(read_postings_by_entry(conn, entry_id)) showModal(modalDialog(
entry_data <- reactiveVal(read_entry(conn, entry_id)) title = "Buchung-Eingabe",
output$entry_ui <- renderUI({
e <- entry_data()
tagList(
fluidRow(
column(6, selectizeInput(ns("contact_id"), "Kontakt",
choices = get_contact_choices(conn),
selected = e$contact_id)),
column(6, textInput(ns("purpose"), "Verwendungszweck", value = e$purpose))
),
hr()
)
})
# Choices für contact nach dem Flush setzen
output$postings_ui <- renderUI({
alle <- entry_postings()
lapply(seq_len(nrow(alle)), function(i) {
print(paste("project selected:", alle$project_id[i]))
print(head(get_project_choices(conn)))
print(class(get_project_choices(conn)))
div(
style = "border-left: 3px solid #3c8dbc; padding: 5px; margin-bottom: 5px",
fluidRow(
column(3,
selectInput(ns(paste0("account_id_", i)), "Konto",
choices = get_account_choices(conn),
selected = as.character(alle$account_id[i]))
),
column(3, selectizeInput(ns(paste0("project_id_", i)), "Projekt",
choices = get_project_choices(conn),
selected = ifelse(is.na(alle$project_id[i]), "", as.character(alle$project_id[i])))
),
column(2, numericInput(ns(paste0("amount_", i)), "Betrag", value = alle$amount[i])),
column(3, textInput(ns(paste0("notiz_", i)), "Notiz", value = alle$notiz[i])),
column(1, actionButton(ns(paste0("delete_", i)), "", icon = icon("trash"), class = "btn-danger btn-sm"))
)
)
})
})
# Choices befüllen nachdem renderUI fertig ist
observe({
alle <- entry_postings()
e <- entry_data()
req(nrow(alle) > 0, nrow(e) > 0)
session$onFlushed(function() {
# Contact f_airdatepicker_UI(ns("valuta"), "Wertstellung", record$valuta),
updateSelectizeInput(session, "contact_id", f_airdatepicker_UI(ns("buchungsdatum"), "Buchungsdatum", record$booking_date),
choices = get_contact_choices(conn), selectizeInput(ns("kontakt"), "Kontakt:",selected = trans$adress_id, choices = get_contact_choices(conn), width = "100%"),
selected = e$contact_id) selectizeInput(ns("konto"), "Kontoname:",selected = record$account_id, choices = get_account_choices(conn), width = "100%"),
selectizeInput(ns("projekt"), "Projektname", selected = NULL, choices = get_project_choices(conn), width = "100%"),
# Postings splitLayout(cellWidths = c("70%", "30%"),
lapply(seq_len(nrow(alle)), function(i) { numericInput(ns("amount"), "Betrag:", value = record$amount, width = "100%"),
updateSelectizeInput(session, paste0("account_id_", i), numericInput(ns("coin_share_amount"), "Münzne:", value = record$coin_share_amount, width = "100%")
choices = get_account_choices(conn), ),
selected = alle$account_id[i]) textAreaInput(ns("trans_notiz"), label= "Notiz (Transaktion)", value = trans$note, width = "100%"),
updateSelectizeInput(session, paste0("project_id_", i), splitLayout(cellWidths = c("50%", "50%"),
choices = get_project_choices(conn), numericInput(ns("receipt_id"), "Umsatz_id:", value = record$receipt_id, width = "100%"),
selected = alle$project_id[i]) numericInput(ns("wiso_id"), "Wiso_id:", value = record$wiso_id, width = "100%")
}) ),
}, once = TRUE) #
}) footer = tagList(
modalButton("Abbrechen"),
actionButton(ns("confirm"), "Bestätigen", class = "btn-success")
),
easyClose = TRUE
))
# Speichern-Logik # Aktion beim Bestätigen
observeEvent(input$speichern, { observeEvent(input$confirm, ignoreInit = T, {
alle <- entry_postings() message("Eingabe im Modul ", id, ": ", input$user_name)
lapply(seq_len(nrow(alle)), function(i) { removeModal()
# update posting i in DB
})
}) })
#
}) })
} }
+54 -8
View File
@@ -1,5 +1,10 @@
read_buch_tabelle <- function(conn){ read_buch_tabelle <- function(conn, trans_id = NULL){
postings <- tbl(conn, "postings") if(!is.null(trans_id)){
postings <- tbl(conn, "postings") %>%
filter(entry_id == trans_id)
} else {
postings <- tbl(conn, "postings")
}
entries <- tbl(conn, "entries") entries <- tbl(conn, "entries")
accounts <- tbl(conn, "accounts") accounts <- tbl(conn, "accounts")
projects <- tbl(conn, "projects") projects <- tbl(conn, "projects")
@@ -9,14 +14,55 @@ read_buch_tabelle <- function(conn){
left_join(contacts, by = c("contact_id" = "id")) |> # contact_id jetzt verfügbar left_join(contacts, by = c("contact_id" = "id")) |> # contact_id jetzt verfügbar
left_join(accounts, by = c("account_id" = "id")) |> left_join(accounts, by = c("account_id" = "id")) |>
left_join(projects, by = c("project_id" = "id")) |> left_join(projects, by = c("project_id" = "id")) |>
select(id, valuta, kontoname, projektname, display_name, amount, entry_id) |> select(id, valuta, account_name, projektname, display_name, amount, entry_id) |>
collect() collect() %>%
mutate(saldo = 0)
} }
read_posting <- function(conn, id){ read_posting <- function(conn, id){
dbxSelect(conn, paste0("SELECT * FROM postings WHERE id =", id)) dbxSelect(conn, paste0("SELECT * FROM postings WHERE id =", id))
} }
read_postings_by_entry <- function(conn, id){
dbxSelect(conn, paste0("SELECT * FROM postings WHERE entry_id =", id))
} coldef_entries_tabelle <-
tbl(conn, "accounts") %>% collect %>% str list(
id = colDef(name = "ID", width = 80),
valuta = colDef(name = "Wertstellung", width = 120),
account_name = colDef(name = "Kontoname", width = 200),
projektname = colDef(name = "Projektname", width = 150),
display_name = colDef(name = "Kontakt", width = 250),
amount = colDef(name = "Betrag", width = 120,
format = colFormat(prefix = "", separators = TRUE, digits = 2),
style = function(value) {
color <- if (value < 0) "#e06666" else "#2b2b2b" # Rot bei negativ, sonst dunkelgrau
list(color = color, fontWeight = "bold") # Gibt CSS-Styles zurück
}
),
entry_id = colDef(name ="trans_id", width = 120),
saldo = colDef(
name = "Saldo",
width = 140,
html = TRUE,
cell = JS("
function(cellInfo, state) {
const page = state.page || 0
const pageSize = state.pageSize || state.sortedData.length
const globalIndex = page * pageSize + cellInfo.viewIndex
let total = 0
for (let i = 0; i <= globalIndex && i < state.sortedData.length; i++) {
total += Number(state.sortedData[i].amount || 0)
}
const color = total < 0 ? '#e06666' : '#2b2b2b'
return `<span style='color:${color}; font-weight:bold'>
${total.toLocaleString('de-DE', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})}
</span>`
}
")
)
)
+1 -1
View File
@@ -6,4 +6,4 @@ get_project_choices <- function(conn) {
choices <- setNames(as.character(projekte$id), projekte$projektname) choices <- setNames(as.character(projekte$id), projekte$projektname)
c("-" = "", choices) c("-" = "", choices)
} }
View File
+5 -1
View File
@@ -1,6 +1,7 @@
library("dbplyr") library("dbplyr")
library("tidyverse") library("tidyverse")
library("shiny") library("shiny")
library("shinyWidgets")
library("DBI") library("DBI")
library("RSQLite") library("RSQLite")
library("dbx") library("dbx")
@@ -15,5 +16,8 @@ conflicts_prefer(dplyr::filter)
options(shiny.reactlog = TRUE) options(shiny.reactlog = TRUE)
options(shiny.error = browser) options(shiny.error = browser)
conn <- dbConnect(RSQLite::SQLite(), "db/development.sqlite3") conn <- dbConnect(RSQLite::SQLite(), "db/development.sqlite")
sourceDirectory("R/") sourceDirectory("R/")
dbReadTable(conn, "projects") %>% str
dbGetQuery(conn, "PRAGMA table_info(postings);")