Compare commits

...

9 Commits

745 changed files with 31543 additions and 205 deletions
+1
View File
@@ -5,3 +5,4 @@
.Ruserdata .Ruserdata
www/attachments/ www/attachments/
.positai
+29 -2
View File
@@ -1,5 +1,5 @@
f_reactable <- function(daten, coldefs = NULL, selection = "single", f_reactable <- function(daten, coldefs = NULL, selection = "single",
defaultSelected = NULL, hoehe = NULL, highlight_valuta = NULL) { defaultSelected = NULL, hoehe = NULL, highlight_valuta = NULL, filterable = T) {
reactable( reactable(
daten, daten,
selection = selection, selection = selection,
@@ -7,7 +7,8 @@ f_reactable <- function(daten, coldefs = NULL, selection = "single",
pagination = F, pagination = F,
defaultPageSize = 17, defaultPageSize = 17,
showPageSizeOptions = TRUE, showPageSizeOptions = TRUE,
filterable = TRUE, defaultSorted = list(valuta = "desc"),
filterable = filterable,
highlight = TRUE, highlight = TRUE,
height = hoehe, # <--- FIXIERT DEN HEADER und macht den Body scrollbar height = hoehe, # <--- FIXIERT DEN HEADER und macht den Body scrollbar
bordered = TRUE, bordered = TRUE,
@@ -33,3 +34,29 @@ f_reactable <- function(daten, coldefs = NULL, selection = "single",
columns = coldefs columns = coldefs
) )
} }
f_reactable_sub <- function(daten, coldefs = NULL) {
reactable(
daten,
selection = "single",
pagination = T,
defaultPageSize = 17,
showPageSizeOptions = TRUE,
filterable = TRUE,
highlight = TRUE,
bordered = TRUE,
striped = FALSE,
compact = TRUE,
# Styling
theme = reactableTheme(
highlightColor = "#e6f7ff", # Etwas dezenter als knallgrün, optional
# borderColor = "#dfe2e5",
rowSelectedStyle = list(backgroundColor = "#98F5FF")#
),
rowStyle = function(index) {
style <- list(cursor = "pointer") # immer aktiv
},
onClick = "select",
columns = coldefs
)
}
+18
View File
@@ -0,0 +1,18 @@
##
## Datum: 2024-10-17_14-31
## Name: Christian Oswald
## Projekt: etatverwaltung
## Datei: f_zeitstempel.R
## Kommentar: Erstellt einen Zeitstempel für die DB-Felder updatea und created
##
# Funkton die prüft ob das Format stimmt
is.ymd_hms <- function(x) !is.na(lubridate::ymd_hms(x, quiet = TRUE))
# Zeitstempel
f_zeitstempel <- function(ts){
# Erstellt einen wenn leer
if(is.na(ts)) ts <- format(Sys.time(), tz="")
# Erstellt einen wenn das Format nicht stimmt
if(!is.ymd_hms(ts)) ts <- format(Sys.time(), tz="")
return(ts)
}
+1 -1
View File
@@ -6,7 +6,7 @@ sync_hibiscus <- function(conn) {
error_f <- NULL error_f <- NULL
# ── Einstellungen ──────────────────────────────────────────────────────────── # ── Einstellungen ────────────────────────────────────────────────────────────
SYNC_AB <- as.Date("2024-01-01") SYNC_AB <- as.Date("2026-01-01")
if (as.character(Sys.info())[1] == "Darwin") { if (as.character(Sys.info())[1] == "Darwin") {
h2jar <- "/Users/cosw/bin/jameica.app/lib/h2/migration-h2/disabled/h2-1.4.199.jar" h2jar <- "/Users/cosw/bin/jameica.app/lib/h2/migration-h2/disabled/h2-1.4.199.jar"
-1
View File
@@ -80,7 +80,6 @@ accountsServer <- function(id, conn, r_global) {
# ── Gewähltes Konto ──────────────────────────────────────────────────────── # ── Gewähltes Konto ────────────────────────────────────────────────────────
selected_name <- reactive({ selected_name <- reactive({
sel <- get_selected(input$account_tree, format = "names") sel <- get_selected(input$account_tree, format = "names")
cat("raw selected:", paste(sel, collapse=", "), "\n")
if (length(sel) == 0) return(NULL) if (length(sel) == 0) return(NULL)
name <- sub(" \\(\\d+\\)$", "", sel[[1]]) name <- sub(" \\(\\d+\\)$", "", sel[[1]])
cat("cleaned name:", name, "\n") cat("cleaned name:", name, "\n")
+44
View File
@@ -0,0 +1,44 @@
##
## Datum : 2026-04-28_13-43
## Name : Christian Oswald
## Datei : contact_io.R
## Projekt : gemfin-shiny
## Kommentar: Befüllt die Felder und liest sie aus
##
contact_io <- function(session, input, output, record, flag){
if(flag == "lesen"){
record[1,"id"] <- input$cid
record$first_name <- input$first_name
record$last_name <- input$last_name
record$postal_code <- input$postal_code
record$city <- input$city
record$street <- input$street
record$phone <- input$phone
record$mobile <- input$mobile
record$email <- input$email
record$member <- input$member
record$display_name <- input$display_name
record$is_company <- input$is_company
record$notes <- input$note
record$created_at <- f_zeitstempel( record$created_at)
record$updated_at <- format(Sys.time(), tz="")
return(record)
}
if(flag == "schreiben"){
updateNumericInput(session,"cid", value = record$id)
updateTextInput(session,"display_name", value = record$display_name)
updateTextInput(session,"first_name", value = record$first_name)
updateTextInput(session,"last_name", value = record$last_name)
updateTextInput(session,"postal_code", value = record$postal_code)
updateTextInput(session,"city", value = record$city)
updateTextInput(session,"email", value = record$email)
updateTextInput(session,"street", value = record$street)
updateTextInput(session,"mobile", value = record$mobile)
updateTextInput(session,"phone", value = record$phone)
updateTextAreaInput(session,"note", value = record$notes)
updateCheckboxInput(session, "is_company", value = record$is_company)
updateCheckboxInput(session, "member", value = record$member)
}
}
+19
View File
@@ -1,3 +1,22 @@
##
## Datum : 2026-04-28_13-11
## Name : Christian Oswald
## Datei : contacts.R
## Projekt : gemfin-shiny
## Kommentar: model für contacts
##
f_contacts_tabelle <- function(conn){
tbl(conn, "contacts") %>%
select(id, display_name, street, city, member) %>%
arrange(display_name) %>%
collect
}
f_contact <- function(conn, idwert){
dbxSelect(conn, paste0("SELECT * FROM contacts WHERE id=", idwert))
}
get_contact_choices <- function(conn) { get_contact_choices <- function(conn) {
contacts <- tbl(conn, "contacts") |> contacts <- tbl(conn, "contacts") |>
+168
View File
@@ -0,0 +1,168 @@
contactsUI <- 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")
),
sidebarLayout(
sidebarPanel(width = 6,
reactableOutput(ns("contacts_table"), height = 800)
),
mainPanel(width = 6,
fluidRow(
column(8, textInput(ns("display_name"), label = "Bezeichnung", value = NA, width = "100%")),
column(4, numericInput(ns("cid"), label = "ID", value = NA, width = "100%"))
),
fluidRow(
column(6, textInput(ns("first_name"), label = "Vorname", value = NA, width = "100%")),
column(6, textInput(ns("last_name"), label = "Nachname", value = NA, width = "100%"))
),
fluidRow(
column(12, textInput(ns("street"), label = "Strasse", value = NA, width = "100%"))
),
fluidRow(
column(4, textInput(ns("postal_code"), label = "PLZ", value = NA, width = "100%")),
column(8, textInput(ns("city"), label = "Ort", value = NA, width = "100%"))
),
fluidRow(
column(4, textInput(ns("phone"), label = "Telefon", value = NA, width = "100%")),
column(4, textInput(ns("mobile"), label = "Mobil", value = NA, width = "100%")),
column(4, textInput(ns("email"), label = "E-Mail", value = NA, width = "100%"))
),
fluidRow(
column(4, checkboxInput(ns("is_company"), label = "Firma", value = F, width = "100%")),
column(4, checkboxInput(ns("member"), label = "Mitglied", value = F, width = "100%"))
),
fluidRow(
column(12, textAreaInput(ns("note"), label = "Notiz", value = NA, width = "100%"))
),
fluidRow(
column(4, actionBttn(ns("add"), "Neu", size = "sm", style = "material-flat", color = "warning", icon = icon("plus"), block = T)),
column(4, actionBttn(ns("del"), "Löschen", size = "sm", style = "material-flat", color = "danger", icon = icon("trash"), block = T)),
column(4, actionBttn(ns("save"), "Speichern", size = "sm", style = "material-flat", color = "success", icon = icon("floppy-disk"), block = T)),
),
br(),
reactableOutput(ns("entries"))
)
)
)
}
contactsServer <- function(id, conn, r_global) {
moduleServer(id, function(input, output, session) {
ns <- session$ns
# ── Daten ----
refresh <- reactiveVal(0)
zeige_alle <- reactiveVal(FALSE)
contact_data <- reactive({
f_contacts_tabelle(conn)
})
# ── Filter-Buttons ----
observeEvent(input$mitglieder, { zeige_alle(FALSE) })
observeEvent(input$filter_alle, { zeige_alle(TRUE) })
# ── Tabelle ----
output$contacts_table <- renderReactable({
reactable(
contact_data(),
striped = TRUE,
highlight = TRUE,
filterable = T,
pagination = F,
selection = "single",
onClick = "select",
defaultSorted = list(display_name = "asc"),
columns = list(
id = colDef(show = FALSE),
display_name = colDef( name = "Bezeichnung", minWidth = 150 ),
street = colDef(name = "Strasse", minWidth = 150),
city = colDef(name = "Ort", minWidth = 150),
member = colDef(name = "Mitglied", maxWidth = 80,
align = "center")
)
)
})
## * Selektierter Umsatz ----
sel <- reactive(getReactableState("contacts_table", "selected"))
observeEvent(sel(), ignoreInit = T, {
idwert <- contact_data()$id[sel()]
record <- f_contact(conn, idwert)
contact_io(session, input, output, record, "schreiben")
# ── Buchungs-Panel ----
entries <- read_buch_tabelle(conn) %>%
filter(contact_id == idwert) %>%
select(id, valuta, account_name, projektname, amount)
output$entries <- renderReactable(
reactable(entries,
striped = TRUE,
highlight = TRUE,
searchable = T,
filterable = F,
pagination = T,
defaultPageSize = 6,
selection = "single",
onClick = "select",
columns = list(
id = colDef(show = FALSE),
valuta = colDef(name = "Wertstellung", minWidth = 80),
account_name = colDef( name = "Konto", minWidth = 150 ),
projektname = colDef(name = "Projekt", minWidth = 150),
amount = colDef(name = "Betrag", minWidth = 150)
)
)
)
})
## * Speichern ----
observeEvent(input$save, ignoreInit = T, {
record <- leer_df_from_table(conn, "contacts")
record <- contact_io(session, input, output, record, "lesen")
print(record)
dbxUpsert(conn, "contacts", records = record, where_cols = c("id"))
})
## * Hinzufügen ----
observeEvent(input$add, ignoreInit = T, {
record <- leer_df_from_table(conn, "contacts")
record$id <- max_id(conn, "contacts")
record <- contact_io(session, input, output, record, "schreiben")
})
## * Löschen ----
observeEvent(input$del, ignoreInit = T, {
dbxDelete(conn, "contacts", where = data.frame(id = input$cid))
})
# ── 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"
# })
})
}
+6 -1
View File
@@ -22,7 +22,7 @@ read_buch_tabelle <- function(conn, trans_id = NULL){
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")) |>
left_join(attachment_counts, by = "entry_id") |> left_join(attachment_counts, by = "entry_id") |>
select(id, valuta, account_name, projektname, display_name, amount, entry_id, n_attachments, account_id) |> select(id, valuta, account_name, projektname, display_name, amount, entry_id, n_attachments, account_id, contact_id) |>
collect() %>% collect() %>%
mutate( mutate(
saldo = 0, saldo = 0,
@@ -35,6 +35,11 @@ read_posting <- function(conn, id){
dbxSelect(conn, paste0("SELECT * FROM postings WHERE id =", id)) dbxSelect(conn, paste0("SELECT * FROM postings WHERE id =", id))
} }
read_buch_entries <- function(conn, idwert){
dbxSelect(conn, paste0("SELECT id, entry_id, valuta, account_id, project_id, amount FROM postings WHERE entry_id=", idwert))
}
coldef_entries_tabelle <- coldef_entries_tabelle <-
list( list(
+104 -127
View File
@@ -5,22 +5,46 @@ buchungenUI <- function(id) {
tagList( tagList(
useShinyjs(), useShinyjs(),
h3("Hauptbuchungen"), h3("Hauptbuchungen"),
reactableOutput(ns("buchungen_table")), fluidRow(
column(8, reactableOutput(ns("buchungen_table")),
hr(), hr(),
h3("Details / Gegenbuchungen"), h3("Details / Gegenbuchungen"),
reactableOutput(ns("details_table")), uiOutput(ns("details_table")) ,
h3("Anhänge"),
),
column(4,
selectizeInput(ns("contact"), label = "Kontakt", choices = get_contact_choices(conn),
selected = NA, width = "100%"),
textAreaInput(ns("note"), label = "Verwendungszweck", value = NA, width = "100%"),
h3("Dateien"),
uiOutput(ns("attachments_ui")), uiOutput(ns("attachments_ui")),
fileInput(ns("anhang"), NULL, multiple = TRUE, fileInput(ns("anhang"), NULL, multiple = TRUE,
accept = c(".pdf", ".jpg", ".png", ".xlsx", ".docx"), accept = c(".pdf", ".jpg", ".png", ".xlsx", ".docx"),
width = "100%"), width = "100%"),
br(), div(
actionBttn(ns("add_trans"), "Transaktion hinzu", size = "sm", style = "material-flat", color = "warning"), style = "display: flex; flex-direction: column; gap: 10px; align-items: flex-start;",
actionBttn(ns("del_trans"), "Transaktion Löschen", size = "sm", style = "material-flat", color = "danger"),
actionBttn(ns("add_detail"), "Detail hinzu", size = "sm", style = "material-flat", color = "success"), actionBttn(ns("add_detail"), "Detail hinzu",
size = "sm", style = "material-flat", color = "default", block = TRUE),
actionBttn(ns("del_detail"), "Detail Löschen",
size = "sm", style = "material-flat", color = "danger", block = TRUE),
actionBttn(ns("add_trans"), "Transaktion hinzu",
size = "sm", style = "material-flat", color = "default", block = TRUE),
actionBttn(ns("del_trans"), "Transaktion Löschen",
size = "sm", style = "material-flat", color = "danger", block = TRUE),
actionBttn(ns("save_trans"), "Transaktion speichern",
size = "sm", style = "material-flat", color = "success", block = TRUE, icon = icon("floppy-disk")),
)
)
),
# Das UI-Element des Moduls (auch wenn es leer ist)
postingModuleUI(ns("posting_modal"))
) )
} }
@@ -28,20 +52,17 @@ buchungenServer <- function(id, conn, r_global) {
moduleServer(id, function(input, output, session) { moduleServer(id, function(input, output, session) {
ns <- session$ns ns <- session$ns
# ── Reactive Variablen ───────────────────────────────────────────────────── # ── Reactive Variablen ----
postings_data <- reactiveVal(read_buch_tabelle(conn)) postings_data <- reactiveVal(read_buch_tabelle(conn))
details_data <- reactiveVal(NULL) sel_details <- reactiveVal(NULL)
selected_trans_id <- reactiveVal(NULL) selected_trans_id <- reactiveVal(NULL)
reset_trigger <- reactiveVal(NULL)
current_main_idx <- reactiveVal(NULL) current_main_idx <- reactiveVal(NULL)
reset_trigger <- reactiveVal(0)
modal_trigger <- reactiveVal(list(post_id = NULL, counter = 0))
highlighted_valuta <- reactiveVal(NULL)
update_db_trigger <- postingModuleServer(
"posting_modal", conn, selected_trans_id, modal_trigger
)
# ── Filter ─────────────────────────────────────────────────────────────────
# ── Filter ----
aktiver_filter <- reactive(r_global$buchungen_filter) aktiver_filter <- reactive(r_global$buchungen_filter)
gefilterte_daten <- reactive({ gefilterte_daten <- reactive({
@@ -58,37 +79,8 @@ buchungenServer <- function(id, conn, r_global) {
else d else d
}) })
observeEvent(aktiver_filter(), {
current_main_idx(NULL)
selected_trans_id(NULL)
details_data(NULL)
})
# ── Sprung von anderem Modul ─────────────────────────────────────────────── # ── Haupttabelle rendern ----
observeEvent(r_global$jump_to_entry_id, ignoreInit = TRUE, {
req(r_global$jump_to_entry_id)
t_id <- r_global$jump_to_entry_id
# Daten neu laden
postings_data(read_buch_tabelle(conn))
# Zeile suchen
idx <- which(gefilterte_daten()$entry_id == t_id)[1]
req(!is.na(idx))
current_main_idx(idx)
selected_trans_id(t_id)
details_data(read_buch_tabelle(conn, trans_id = t_id))
highlighted_valuta(gefilterte_daten()[idx, "valuta"])
reset_trigger(isolate(reset_trigger()) + 1)
scroll_to_row(ns("buchungen_table"), idx)
# Sprungziel zurücksetzen
r_global$jump_to_entry_id <- NULL
})
# ── Haupttabelle rendern ───────────────────────────────────────────────────
output$buchungen_table <- renderReactable({ output$buchungen_table <- renderReactable({
reset_trigger() reset_trigger()
f_reactable( f_reactable(
@@ -96,99 +88,84 @@ buchungenServer <- function(id, conn, r_global) {
coldefs = coldef_entries_tabelle, coldefs = coldef_entries_tabelle,
selection = "single", selection = "single",
hoehe = "60vh", hoehe = "60vh",
defaultSelected = current_main_idx(), filterable = TRUE,
highlight_valuta = highlighted_valuta()
) )
}) })
# ── Details laden ────────────────────────────────────────────────────────── sel <- reactive(getReactableState("buchungen_table", "selected"))
sel_details <- reactive(getReactableState("buchungen_table", "selected")) observeEvent(sel(), ignoreInit = T, {
output$details_table <- renderUI({
req(sel())
# Daten laden
df <- read_buch_entries(conn, gefilterte_daten()$entry_id[sel()])
sel_details(df)
observeEvent(sel_details(), ignoreInit = TRUE, { rows <- lapply(1:nrow(df), function(ind) {
fluidRow(class = "details-row",
# HIER: Index an die ID hängen!
column(1, numericInput(ns(paste0("id_", ind)), label = NULL, width = "100%", value = df$id[ind])),
column(2, dateInput(ns(paste0("valuta_", ind)), label = NULL, width = "100%", value = df$valuta[ind])),
column(3, selectizeInput(ns(paste0("account_", ind)), label = NULL,
choices = get_account_choices(conn),
selected = df$account_id[ind], width = "100%")),
column(3, selectizeInput(ns(paste0("projekt_", ind)), label = NULL,
choices = get_project_choices(conn),
selected = df$projekt_id[ind], width = "100%")),
column(2, numericInput(ns(paste0("betrag_", ind)), label = NULL, width = "100%", value = df$amount[ind]))
)
})
tagList(rows)
})
})
observeEvent(input$save_trans, {
req(sel_details()) req(sel_details())
highlighted_valuta(gefilterte_daten()[sel_details(), "valuta"]) n_rows <- nrow(sel_details())
current_main_idx(sel_details())
t_id <- gefilterte_daten()[sel_details(), "entry_id"] |> pull()
selected_trans_id(t_id)
details_data(read_buch_tabelle(conn, trans_id = t_id))
})
output$details_table <- renderReactable({ # Alle Zeilen in einer Liste sammeln
req(details_data()) all_records <- lapply(1:n_rows, function(ind) {
f_reactable( data.frame(
details_data(), id = as.integer(input[[paste0("id_", ind)]]),
coldefs = coldef_entries_tabelle, valuta = as.Date(input[[paste0("valuta_", ind)]]),
selection = "single", amount = as.numeric(input[[paste0("betrag_", ind)]]),
hoehe = NULL account_id = input[[paste0("account_", ind)]],
projekt_id = input[[paste0("projekt_", ind)]],
stringsAsFactors = FALSE
) )
}) })
# ── Detail hinzufügen ────────────────────────────────────────────────────── # Zu einem großen Dataframe zusammenfügen
observeEvent(input$add_detail, { final_df <- do.call(rbind, all_records)
req(selected_trans_id()) browser()
modal_trigger(list(post_id = NULL, counter = modal_trigger()$counter + 1))
# Datenbank-Update-Logik
tryCatch({
# Beispiel: Für jede Zeile ein Update ausführen
for(i in 1:nrow(final_df)) {
update_buchung_detail(conn, final_df[i, ]) # Deine DB-Funktion
}
showNotification("Daten erfolgreich gespeichert", type = "message")
# Optional: Haupttabelle aktualisieren, falls sich Summen geändert haben
# trigger_refresh(trigger_refresh() + 1)
}, error = function(e) {
showNotification(paste("Fehler beim Speichern:", e$message), type = "error")
})
}) })
# ── Detail editieren ───────────────────────────────────────────────────────
sel_detail <- reactive(getReactableState("details_table", "selected"))
observeEvent(sel_detail(), ignoreInit = TRUE, { # ── Anhänge ----
p_id <- details_data()$id[sel_detail()]
modal_trigger(list(post_id = p_id, counter = modal_trigger()$counter + 1))
})
# ── DB-Update durch Modal ──────────────────────────────────────────────────
observeEvent(update_db_trigger(), ignoreInit = TRUE, {
req(selected_trans_id())
details_data(read_buch_tabelle(conn, trans_id = selected_trans_id()))
postings_data(read_buch_tabelle(conn))
updateReactable("buchungen_table", data = gefilterte_daten())
})
# ── Neue Transaktion ───────────────────────────────────────────────────────
observeEvent(input$add_trans, {
new_t_id <- max_id(conn, "entries") + 1
dbxInsert(conn, "entries", data.frame(id = new_t_id))
p_id1 <- max_id(conn, "postings") + 1
p_id2 <- p_id1 + 1
dbxInsert(conn, "postings", data.frame(
id = c(p_id1, p_id2),
entry_id = c(new_t_id, new_t_id),
amount = c(0, 0),
account_id = c(0, 0),
valuta = c(Sys.Date(), Sys.Date())
))
# Filter zurücksetzen damit neue Transaktion sichtbar ist
r_global$buchungen_filter <- "alle"
postings_data(read_buch_tabelle(conn))
selected_trans_id(new_t_id)
details_data(read_buch_tabelle(conn, trans_id = new_t_id))
neue_zeile <- which(postings_data()$id == p_id1)
current_main_idx(neue_zeile)
reset_trigger(isolate(reset_trigger()) + 1)
scroll_to_row(ns("buchungen_table"), neue_zeile)
})
# ── Transaktion löschen ────────────────────────────────────────────────────
observeEvent(input$del_trans, ignoreInit = TRUE, {
req(selected_trans_id())
dbxDelete(conn, "postings", where = data.frame(entry_id = selected_trans_id()))
dbxDelete(conn, "entries", where = data.frame(id = selected_trans_id()))
postings_data(read_buch_tabelle(conn))
current_main_idx(NULL)
selected_trans_id(NULL)
details_data(NULL)
updateReactable("buchungen_table", data = gefilterte_daten())
})
# ── Anhänge ────────────────────────────────────────────────────────────────
output$attachments_ui <- renderUI({ output$attachments_ui <- renderUI({
req(selected_trans_id()) req(sel_details())
entry_id <-
dbxSelect(conn, paste0("SELECT entry_id FROM postings WHERE id=",
sel_details()$id[1])) %>% pull
att <- dbxSelect(conn, paste0( att <- dbxSelect(conn, paste0(
"SELECT * FROM attachments WHERE entry_id = ", selected_trans_id() "SELECT * FROM attachments WHERE entry_id = ", entry_id
)) ))
if (nrow(att) == 0) return(p("Keine Anhänge vorhanden.")) if (nrow(att) == 0) return(p("Keine Anhänge vorhanden."))
+131 -35
View File
@@ -4,11 +4,15 @@ umsatzUI <- function(id) {
useShinyjs(), useShinyjs(),
# Sync-Button oben # Sync-Button oben
div( div(
style = "display: flex; justify-content: flex-end; margin-bottom: 8px;", 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", actionBttn(ns("sync"), "Sync Hibiscus",
size = "xs", style = "minimal", size = "xs", style = "minimal",
icon = icon("rotate"), color = "primary") icon = icon("rotate"), color = "primary")
), ),
# Buchungspanel immer sichtbar unten # Buchungspanel immer sichtbar unten
uiOutput(ns("buchungs_panel")), uiOutput(ns("buchungs_panel")),
# Tabelle mit begrenzter Höhe und Scroll # Tabelle mit begrenzter Höhe und Scroll
@@ -51,17 +55,34 @@ umsatzServer <- function(id, conn, r_global) {
}) })
}) })
# ── Tabelle ──────────────────────────────────────────────────────────────── # ── Tabelle ----
output$umsatz_table <- renderReactable({ output$umsatz_table <- renderReactable({
# remote_names einmal laden
known_names <- dbGetQuery(conn, "SELECT contact_text FROM bank_connections")$contact_text
reactable( reactable(
umsatz_data() %>% umsatz_data() %>%
select(id, valuta, empfaenger_name, empfaenger_konto, select(id, valuta, empfaenger_name, empfaenger_konto,
betrag, zweck, gebucht, posting_id, entry_id), betrag, zweck, gebucht, posting_id, entry_id),
striped = TRUE, striped = TRUE,
highlight = TRUE, highlight = TRUE,
filterable = TRUE, # ← das reicht 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, pagination = F,
selection = "single", selection = "multiple",
onClick = "select", onClick = "select",
defaultSorted = list(valuta = "desc"), defaultSorted = list(valuta = "desc"),
columns = list( columns = list(
@@ -69,7 +90,15 @@ umsatzServer <- function(id, conn, r_global) {
posting_id = colDef(show = FALSE), posting_id = colDef(show = FALSE),
entry_id = colDef(show = FALSE), entry_id = colDef(show = FALSE),
valuta = colDef(name = "Datum", maxWidth = 120), valuta = colDef(name = "Datum", maxWidth = 120),
empfaenger_name = colDef(name = "Empfänger", minWidth = 150), 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), empfaenger_konto = colDef(name = "IBAN", minWidth = 150),
betrag = colDef( betrag = colDef(
name = "Betrag", name = "Betrag",
@@ -92,7 +121,7 @@ umsatzServer <- function(id, conn, r_global) {
) )
}) })
# ── Selektierter Umsatz ──────────────────────────────────────────────────── # ── Selektierter Umsatz ----
sel <- reactive(getReactableState("umsatz_table", "selected")) sel <- reactive(getReactableState("umsatz_table", "selected"))
selected_umsatz <- reactive({ selected_umsatz <- reactive({
@@ -100,21 +129,49 @@ umsatzServer <- function(id, conn, r_global) {
umsatz_data()[sel(), ] umsatz_data()[sel(), ]
}) })
# ── Buchungs-Panel ───────────────────────────────────────────────────────── # ── Buchungs-Panel ----
output$buchungs_panel <- renderUI({ output$buchungs_panel <- renderUI({
req(selected_umsatz()) req(selected_umsatz())
u <- selected_umsatz() rows <- selected_umsatz()
if (u$gebucht) { 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( tagList(
h4("Bereits gebucht"), h4("Bereits gebucht"),
actionBttn(ns("goto_buchung"), actionBttn(ns("goto_buchung"),
paste0("→ Zur Buchung (Entry #", u$entry_id, ")"), paste0("→ Zur Buchung (Entry #", rows$entry_id, ")"),
size = "sm", style = "minimal", color = "primary") 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 { } 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( tagList(
h4("Buchen"), h4(if (nrow(rows) == 1) "Buchen" else paste0(nrow(rows), " Umsätze buchen")),
info_text,
fluidRow( fluidRow(
column(4, column(4,
selectizeInput(ns("konto"), "Konto:", selectizeInput(ns("konto"), "Konto:",
@@ -130,11 +187,11 @@ umsatzServer <- function(id, conn, r_global) {
column(3, column(3,
selectizeInput(ns("kontakt"), "Kontakt:", selectizeInput(ns("kontakt"), "Kontakt:",
choices = get_contact_choices(conn), choices = get_contact_choices(conn),
selected = resolve_contact(conn, u$empfaenger_name, u$empfaenger_konto), selected = kontakt_selected,
width = "100%") width = "100%")
), ),
column(1, column(1,
div(style = "margin-top: 25px;", # Label-Höhe ausgleichen div(style = "margin-top: 25px;",
actionBttn(ns("new_contact"), "Neuer Kontakt", actionBttn(ns("new_contact"), "Neuer Kontakt",
size = "xs", style = "minimal", icon = icon("plus"), color = "warning") size = "xs", style = "minimal", icon = icon("plus"), color = "warning")
) )
@@ -151,7 +208,7 @@ umsatzServer <- function(id, conn, r_global) {
} }
}) })
# ── Zur Buchung springen ─────────────────────────────────────────────────── # ── Zur Buchung springen ----
observeEvent(input$goto_buchung, { observeEvent(input$goto_buchung, {
req(selected_umsatz()) req(selected_umsatz())
r_global$nav_history <- c(r_global$nav_history, list(list(tab = "umsatz"))) r_global$nav_history <- c(r_global$nav_history, list(list(tab = "umsatz")))
@@ -159,21 +216,36 @@ umsatzServer <- function(id, conn, r_global) {
r_global$active_tab <- "buchungen" r_global$active_tab <- "buchungen"
}) })
# ── Buchen ──────────────────────────────────────────────────────────────── # ── Buchen ----
observeEvent(input$buchen, { observeEvent(input$buchen, {
req(selected_umsatz(), input$konto) req(selected_umsatz(), input$konto, input$konto != 0)
u <- selected_umsatz() 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") %>% gegenkonto_id <- dbReadTable(conn, "accounts") %>%
filter(hibiscus_account_id == u$konto_id) %>% filter(hibiscus_account_id == u$konto_id) %>%
pull(id) pull(id)
if (length(gegenkonto_id) == 0) next
req(length(gegenkonto_id) > 0, input$konto != 0) # 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 new_t_id <- max_id(conn, "entries") + 1L
dbxInsert(conn, "entries", data.frame( dbxInsert(conn, "entries", data.frame(
id = new_t_id, id = new_t_id,
contact_id = as.integer(input$kontakt), contact_id = contact_id,
note = u$zweck note = u$zweck
)) ))
@@ -188,19 +260,33 @@ umsatzServer <- function(id, conn, r_global) {
booking_date = c(as.character(u$datum), as.character(u$datum)), booking_date = c(as.character(u$datum), as.character(u$datum)),
bank_transaction_id = c(u$id, NA_integer_) bank_transaction_id = c(u$id, NA_integer_)
)) ))
}
refresh(refresh() + 1) refresh(refresh() + 1)
showNotification("✓ Gebucht", type = "message") showNotification(
paste0("✓ ", nrow(rows), " Umsatz/Umsätze gebucht"),
type = "message"
)
}) })
# ── Neuer Kontakt ────────────────────────────────────────────────────────── # ── Neuer Kontakt / Bankverbindung ----
observeEvent(input$new_contact, { observeEvent(input$new_contact, {
req(selected_umsatz()) req(selected_umsatz())
u <- selected_umsatz() u <- selected_umsatz()
contact_choices <- get_contact_choices(conn)
showModal(modalDialog( showModal(modalDialog(
title = "Neuer Kontakt", title = "Neuer Kontakt",
textInput(ns("new_contact_name"), "Name", value = u$empfaenger_name), 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), textInput(ns("new_contact_iban"), "IBAN", value = u$empfaenger_konto),
footer = tagList( footer = tagList(
modalButton("Abbrechen"), modalButton("Abbrechen"),
@@ -209,33 +295,43 @@ umsatzServer <- function(id, conn, r_global) {
) )
)) ))
}) })
# ** Neuen Kontakt/Bvb speichern
observeEvent(input$save_contact, { observeEvent(input$save_contact, {
req(input$new_contact_name)
u <- selected_umsatz() 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
new_c_id <- max_id(conn, "contacts") + 1L } else {
# ── Neuen Kontakt + bank_connection anlegen ----
req(input$new_contact_name)
target_contact_id <- max_id(conn, "contacts") + 1L
dbxInsert(conn, "contacts", data.frame( dbxInsert(conn, "contacts", data.frame(
id = new_c_id, id = target_contact_id,
display_name = input$new_contact_name display_name = input$new_contact_name
)) ))
}
new_bc_id <- max_id(conn, "bank_connections") + 1L new_bc_id <- max_id(conn, "bank_connections") + 1L
dbxInsert(conn, "bank_connections", data.frame( dbxInsert(conn, "bank_connections", data.frame(
id = new_bc_id, id = new_bc_id,
contact_id = new_c_id, contact_id = target_contact_id,
iban = input$new_contact_iban, iban = input$new_contact_iban,
remote_name = u$empfaenger_name contact_text = u$empfaenger_name
)) ))
removeModal() removeModal()
updateSelectizeInput(session, "kontakt", updateSelectizeInput(session, "kontakt",
choices = get_contact_choices(conn), choices = get_contact_choices(conn),
selected = new_c_id selected = target_contact_id
) )
msg <- if (existing_id != 0L) "✓ Bankverbindung verknüpft" else "✓ Kontakt angelegt"
showNotification("✓ Kontakt angelegt", type = "message") showNotification(msg, type = "message")
}) })
## ** Auswahl aufheben ----
observeEvent(input$deselect_all, {
updateReactable("umsatz_table", selected = NA, session = session)
})
}) })
} }
+27 -2
View File
@@ -242,7 +242,7 @@ if (ok) {
if (ok) { if (ok) {
accounts <- dbxSelect(con_f, " accounts <- dbxSelect(con_f, "
SELECT id, konto, budget_id, bankname, updated_at, created_at, SELECT id, konto, budget_id, bankname, updated_at, created_at,
konto_hibiscus, konto_wiso, kontonummer_bank, notizen, b_spenden konto_hibiscus, konto_wiso, kontonummer_bank, notizen, ist_spende
FROM Konten FROM Konten
") %>% ") %>%
transmute( transmute(
@@ -253,7 +253,7 @@ if (ok) {
budget_id = as.integer(budget_id), budget_id = as.integer(budget_id),
wiso_account = konto_wiso, wiso_account = konto_wiso,
bank_account_no = kontonummer_bank, bank_account_no = kontonummer_bank,
is_donations = as.integer(b_spenden), is_donations = as.integer(ist_spende),
notes = notizen, notes = notizen,
created_at = created_at, created_at = created_at,
updated_at = updated_at updated_at = updated_at
@@ -441,6 +441,30 @@ if(ok){
} ## ------------------------------------------------------- 2026-03-19 16:38 } ## ------------------------------------------------------- 2026-03-19 16:38
## * Daeien übertragen ----
if(ok){
pfad <- "~/Insync/Projekte/Gemeindefinanzen/gemfin-fm/gemfin04/Dokumente/datei/"
zielpfad <- "~/Documents/workspace/gemfin-shiny/www/documents/"
vorhanden <- list.files(pfad, pattern = "pdf")
eintraege <- att$original_name
anz <- length(eintraege)
for(ind in 1:length(eintraege)){
if( exists(paste0(zielpfad, eintraege[ind])) ){
cat(ind, " von ", anz, "\n")
file.copy(
from = paste0(pfad, vorhanden[ind]),
to = "~/Documents/workspace/gemfin-shiny/www/documents/")
}
}
neue_dateien <- list.files("~/Documents/workspace/gemfin-shiny/www/documents/")
length(neue_dateien)
eintraege <- att$original_name
length(eintraege)
nv <- att[-which(eintraege %in% neue_dateien),]
error_f <- fehler_add("Alle Dateien übertragen", nrow(nv) == 0, error_f)
} ## ------------------------------------------------------- 2026-04-28 18:40
# Transfer Bankverbindungen ---- # Transfer Bankverbindungen ----
if (ok) { if (ok) {
bank_connections <- dbxSelect(con_f, " bank_connections <- dbxSelect(con_f, "
@@ -460,6 +484,7 @@ if (ok) {
updated_at = as.character(updated_at) updated_at = as.character(updated_at)
) )
sum(duplicated(bank_connections$id))
dbWriteTable(con_s, "bank_connections", bank_connections, append = TRUE) dbWriteTable(con_s, "bank_connections", bank_connections, append = TRUE)
error_f <- fehler_add( error_f <- fehler_add(
paste(nrow(bank_connections), "Bankverbindungen übertragen"), TRUE, error_f paste(nrow(bank_connections), "Bankverbindungen übertragen"), TRUE, error_f
Binary file not shown.
+1
View File
@@ -54,4 +54,5 @@ server <- function(input, output, session) {
accountsServer("accounts_tab", conn, r_global) accountsServer("accounts_tab", conn, r_global)
buchungenServer("buchungen_tab", conn, r_global) buchungenServer("buchungen_tab", conn, r_global)
umsatzServer("umsatz_tab", conn, r_global) umsatzServer("umsatz_tab", conn, r_global)
contactsServer("contacts_tab", conn, r_global)
} }
+8 -1
View File
@@ -49,11 +49,15 @@ dashboardPage(
sidebarMenu(id = "tabs", sidebarMenu(id = "tabs",
menuItem("Buchungen", tabName = "buchungen", icon = icon("list")), menuItem("Buchungen", tabName = "buchungen", icon = icon("list")),
menuItem("Umsätze", tabName = "umsatz", icon = icon("bank")), menuItem("Umsätze", tabName = "umsatz", icon = icon("bank")),
menuItem("Konten", tabName = "konten", icon = icon("building-columns")) menuItem("Konten", tabName = "konten", icon = icon("building-columns")),
menuItem("Adressen", tabName = "contacts", icon = icon("people-group"))
) )
), ),
dashboardBody( dashboardBody(
tags$head(
tags$link(rel = "stylesheet", type = "text/css", href = "main.css")
),
tabItems( tabItems(
tabItem(tabName = "buchungen", tabItem(tabName = "buchungen",
buchungenUI("buchungen_tab") buchungenUI("buchungen_tab")
@@ -63,6 +67,9 @@ dashboardPage(
), ),
tabItem(tabName = "konten", tabItem(tabName = "konten",
accountsUI("accounts_tab") accountsUI("accounts_tab")
),
tabItem(tabName = "contacts",
contactsUI("contacts_tab")
) )
) )
) )
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+9619
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+9619
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More