diff --git a/R/_funktionen/f_airdatepicker.R b/R/_funktionen/f_airdatepicker.R new file mode 100644 index 0000000..d1c2752 --- /dev/null +++ b/R/_funktionen/f_airdatepicker.R @@ -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) + diff --git a/R/_funktionen/f_table.R b/R/_funktionen/f_table.R new file mode 100644 index 0000000..2419b7e --- /dev/null +++ b/R/_funktionen/f_table.R @@ -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 + ) +} diff --git a/R/accounts/accounts.R b/R/accounts/accounts.R index a098629..0cb0dd2 100644 --- a/R/accounts/accounts.R +++ b/R/accounts/accounts.R @@ -6,8 +6,8 @@ read_accounts <- function(conn){ get_account_choices <- function(conn) { tbl(conn, "accounts") |> - select(id, konto) |> + select(id, account_name) |> collect() |> - arrange(konto) |> - (\(df) setNames(df$id, df$konto))() + arrange(account_name) |> + (\(df) setNames(df$id, df$account_name))() } diff --git a/R/buchungen_mod.R b/R/buchungen_mod.R index c335c3e..5e6578e 100644 --- a/R/buchungen_mod.R +++ b/R/buchungen_mod.R @@ -1,92 +1,48 @@ buchungenUI <- function(id) { ns <- NS(id) tagList( - reactableOutput(ns("buchungen_table")) + reactableOutput(ns("buchungen_table")), + reactableOutput(ns("details_table")), + postingModuleUI(ns("posting_modal")) ) } + buchungenServer <- function(id) { - moduleServer( id, function(input, output, session) { + moduleServer(id, function(input, output, session) { ns <- session$ns 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({ - reactable( - postings(), - 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) - ) - ) + # req(postings()) + f_reactable(postings(), coldefs = coldef_entries_tabelle) }) # Modal zum editieren 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, { - browser() - dbExecute(conn, "UPDATE postings SET - account_id = ?, project_id = ?, amount = ?, valuta = ?, - notiz = ?, rechnungsnummer = ?, betrag_muenzen = ?, verzicht = ? - 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 - ) + observeEvent(selected(), { + idwert <- postings()[selected(),"entry_id"] %>% pull + details <- read_buch_tabelle(conn, trans_id = idwert) + output$details_table <- renderReactable( + f_reactable(details, coldefs = coldef_entries_tabelle) ) - 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) }) - - - - } - ) + }) } \ No newline at end of file diff --git a/R/entry_edit_modal.R b/R/entry_edit_modal.R index 51bf23b..f681f89 100644 --- a/R/entry_edit_modal.R +++ b/R/entry_edit_modal.R @@ -1,93 +1,51 @@ -# entry_edit_mod.R -entryEditUI <- function(id) { + +# --- MODUL UI --- +postingModuleUI <- function(id) { ns <- NS(id) 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) { - 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)) - entry_data <- reactiveVal(read_entry(conn, entry_id)) - - 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) + showModal(modalDialog( + title = "Buchung-Eingabe", - session$onFlushed(function() { - # Contact - updateSelectizeInput(session, "contact_id", - choices = get_contact_choices(conn), - selected = e$contact_id) - - # Postings - lapply(seq_len(nrow(alle)), function(i) { - updateSelectizeInput(session, paste0("account_id_", i), - choices = get_account_choices(conn), - selected = alle$account_id[i]) - updateSelectizeInput(session, paste0("project_id_", i), - choices = get_project_choices(conn), - selected = alle$project_id[i]) - }) - }, once = TRUE) - }) + + f_airdatepicker_UI(ns("valuta"), "Wertstellung", record$valuta), + f_airdatepicker_UI(ns("buchungsdatum"), "Buchungsdatum", record$booking_date), + selectizeInput(ns("kontakt"), "Kontakt:",selected = trans$adress_id, choices = get_contact_choices(conn), width = "100%"), + 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%"), + splitLayout(cellWidths = c("70%", "30%"), + numericInput(ns("amount"), "Betrag:", value = record$amount, width = "100%"), + numericInput(ns("coin_share_amount"), "Münzne:", value = record$coin_share_amount, width = "100%") + ), + textAreaInput(ns("trans_notiz"), label= "Notiz (Transaktion)", value = trans$note, width = "100%"), + splitLayout(cellWidths = c("50%", "50%"), + numericInput(ns("receipt_id"), "Umsatz_id:", value = record$receipt_id, width = "100%"), + numericInput(ns("wiso_id"), "Wiso_id:", value = record$wiso_id, width = "100%") + ), + # + footer = tagList( + modalButton("Abbrechen"), + actionButton(ns("confirm"), "Bestätigen", class = "btn-success") + ), + easyClose = TRUE + )) - # Speichern-Logik - observeEvent(input$speichern, { - alle <- entry_postings() - lapply(seq_len(nrow(alle)), function(i) { - # update posting i in DB - }) + # Aktion beim Bestätigen + observeEvent(input$confirm, ignoreInit = T, { + message("Eingabe im Modul ", id, ": ", input$user_name) + removeModal() }) - + # }) } \ No newline at end of file diff --git a/R/postings/buchungen.R b/R/postings/buchungen.R index 0abfcf7..580529c 100644 --- a/R/postings/buchungen.R +++ b/R/postings/buchungen.R @@ -1,5 +1,10 @@ -read_buch_tabelle <- function(conn){ - postings <- tbl(conn, "postings") +read_buch_tabelle <- function(conn, trans_id = NULL){ + if(!is.null(trans_id)){ + postings <- tbl(conn, "postings") %>% + filter(entry_id == trans_id) + } else { + postings <- tbl(conn, "postings") + } entries <- tbl(conn, "entries") accounts <- tbl(conn, "accounts") 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(accounts, by = c("account_id" = "id")) |> left_join(projects, by = c("project_id" = "id")) |> - select(id, valuta, kontoname, projektname, display_name, amount, entry_id) |> - collect() + select(id, valuta, account_name, projektname, display_name, amount, entry_id) |> + collect() %>% + mutate(saldo = 0) } read_posting <- function(conn, 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)) -} -tbl(conn, "accounts") %>% collect %>% str + + +coldef_entries_tabelle <- + 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 ` + ${total.toLocaleString('de-DE', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + })} + ` + } + ") + ) + ) diff --git a/R/projects/projects.R b/R/projects/projects.R index be8d57a..a46e282 100644 --- a/R/projects/projects.R +++ b/R/projects/projects.R @@ -6,4 +6,4 @@ get_project_choices <- function(conn) { choices <- setNames(as.character(projekte$id), projekte$projektname) c("-" = "", choices) -} \ No newline at end of file +} diff --git a/db/development.sqlite3 b/db/development.sqlite3 new file mode 100644 index 0000000..e69de29 diff --git a/global.R b/global.R index b31571e..cba5b65 100644 --- a/global.R +++ b/global.R @@ -1,6 +1,7 @@ library("dbplyr") library("tidyverse") library("shiny") +library("shinyWidgets") library("DBI") library("RSQLite") library("dbx") @@ -15,5 +16,8 @@ conflicts_prefer(dplyr::filter) options(shiny.reactlog = TRUE) options(shiny.error = browser) -conn <- dbConnect(RSQLite::SQLite(), "db/development.sqlite3") +conn <- dbConnect(RSQLite::SQLite(), "db/development.sqlite") sourceDirectory("R/") + +dbReadTable(conn, "projects") %>% str +dbGetQuery(conn, "PRAGMA table_info(postings);")