Compare commits

..

19 Commits

Author SHA1 Message Date
cosw 12ef7693d5 Dokumente hinzugefügt und data.transfer mit attachments-transfer erweitert 2026-04-28 20:37:19 +02:00
cosw 0bbb656d22 Gegenbuchungen unter Haupttabelle, speichern angefangen. 2026-04-28 17:40:05 +02:00
cosw 1f0b9eb214 Buchungs-Seite aufgeräumt 2026-04-28 17:37:29 +02:00
cosw 23e3c3d4fb Buchungen als tr mit inputs 2026-04-28 17:06:08 +02:00
cosw a0e2fb986d verschiedene änderungen 2026-04-28 15:40:44 +02:00
cosw db63fecded cat entfernt 2026-04-28 14:42:24 +02:00
cosw 089a930488 Adress-Modul erstellt 2026-04-28 14:39:37 +02:00
cosw f16386810f Die Umsatztabelle zeigt jetzt unbekannte bvbs an und man kann auch eine bvb den contacts zuordnern oder einen neuen contacct erstellen 2026-04-28 11:36:11 +02:00
cosw 85ad80add7 Umsatz-Suche funktioniert mit kommazahlen und negativ 2026-04-28 11:10:42 +02:00
cosw d0a5cebdcd umsatzliste verbessert 2026-04-28 10:23:20 +02:00
cosw 2d2eb2fa1c Umsatz-modul eingebaut, buchen noch nicht getestet 2026-03-24 17:28:21 +01:00
cosw 126c2fc7d7 Accounts Tree erstellt 2026-03-24 16:00:52 +01:00
cosw 71621ad8ac Attachments Liste wird jetzt angezeigt und attachment in iframe 2026-03-20 17:32:13 +01:00
cosw 581c6d9ecc Anzeige der Attachments 2026-03-19 17:11:10 +01:00
cosw 84026498d1 sqlite aktualisiert attachments hinzu 2026-03-19 17:07:27 +01:00
cosw 70a2fe2526 Filter hinzugefügt 2026-03-19 17:06:40 +01:00
cosw daba64c83a Message entfernt 2026-03-19 14:35:44 +01:00
cosw d757a61cdb anzeige der neuen DS hinzugefügt 2026-03-19 14:28:46 +01:00
cosw 8e2b4932ab Versuch mit datatable 2026-03-19 14:10:31 +01:00
760 changed files with 32483 additions and 176 deletions
Regular → Executable
+2
View File
@@ -3,4 +3,6 @@
.Rhistory .Rhistory
.RData .RData
.Ruserdata .Ruserdata
www/attachments/
.positai
Regular → Executable
View File
View File
Regular → Executable
View File
Regular → Executable
+41 -4
View File
@@ -1,4 +1,5 @@
f_reactable <- function(daten, coldefs = NULL, selection = "single", defaultSelected = NULL, hoehe = NULL) { f_reactable <- function(daten, coldefs = NULL, selection = "single",
defaultSelected = NULL, hoehe = NULL, highlight_valuta = NULL, filterable = T) {
reactable( reactable(
daten, daten,
selection = selection, selection = selection,
@@ -6,7 +7,8 @@ f_reactable <- function(daten, coldefs = NULL, selection = "single", defaultSele
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,
@@ -16,9 +18,44 @@ f_reactable <- function(daten, coldefs = NULL, selection = "single", defaultSele
theme = reactableTheme( theme = reactableTheme(
highlightColor = "#e6f7ff", # Etwas dezenter als knallgrün, optional highlightColor = "#e6f7ff", # Etwas dezenter als knallgrün, optional
# borderColor = "#dfe2e5", # borderColor = "#dfe2e5",
rowSelectedStyle = list(backgroundColor = "#98F5FF") rowSelectedStyle = list(backgroundColor = "#98F5FF")#
), ),
rowStyle = list(cursor = "pointer"), rowStyle = function(index) {
style <- list(cursor = "pointer") # immer aktiv
if (!is.null(highlight_valuta) &&
daten$valuta[index] == highlight_valuta) {
style$background <- "#fff3cd"
}
style
},
onClick = "select",
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", onClick = "select",
columns = coldefs 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)
}
+19
View File
@@ -0,0 +1,19 @@
##
## Datum : 2026-03-19_14-26
## Name : Christian Oswald
## Datei : scroll_to_Row.R
## Projekt : gemfin-shiny
## Kommentar: Springt zur gewünschten Reactable Zeile
##
scroll_to_row <- function(table_id, row_idx, delay_ms = 200) {
shinyjs::delay(delay_ms, {
runjs(sprintf("
const rows = document.querySelectorAll('#%s .rt-tr-group');
const idx = %d - 1;
if (rows[idx]) {
rows[idx].scrollIntoView({behavior: 'smooth', block: 'center'});
}
", table_id, row_idx))
})
}
+69
View File
@@ -0,0 +1,69 @@
# R/sync_hibiscus.R
sync_hibiscus <- function(conn) {
ok <- TRUE
error_f <- NULL
# ── Einstellungen ────────────────────────────────────────────────────────────
SYNC_AB <- as.Date("2026-01-01")
if (as.character(Sys.info())[1] == "Darwin") {
h2jar <- "/Users/cosw/bin/jameica.app/lib/h2/migration-h2/disabled/h2-1.4.199.jar"
url <- "jdbc:h2:/Users/cosw/insync/Projekte/Gemeindefinanzen/jameica/hibiscus/h2db/hibiscus;IFEXISTS=TRUE;CIPHER=AES"
}
if (as.character(Sys.info())[1] == "Linux") {
h2jar <- "..."
url <- "jdbc:h2:/home/cosw/.jameica/hibiscus/h2db/hibiscus;IFEXISTS=TRUE;CIPHER=AES"
}
user <- "hibiscus"
pw <- "FbvGoL+yJlH1GtUojnC8ZajYuTA= FbvGoL+yJlH1GtUojnC8ZajYuTA="
# ── Hibiscus H2 lesen ────────────────────────────────────────────────────────
if (ok) {
tryCatch({
drv <- JDBC("org.h2.Driver", h2jar, identifier.quote = "`")
con_j <- dbConnect(drv, url, user, pw)
originaldaten <- dbReadTable(con_j, "UMSATZ")
dbDisconnect(con_j)
ok <- nrow(originaldaten) > 0
error_f <- fehler_add(paste("Hibiscus Daten gelesen:", nrow(originaldaten)), ok, error_f)
}, error = function(e) {
ok <<- FALSE
error_f <<- fehler_add(paste("H2 Fehler:", e$message), FALSE, error_f)
})
}
# ── Bereits vorhandene IDs ────────────────────────────────────────────────────
if (ok) {
vh <- dbxSelect(conn, "SELECT id FROM hibiscus_transactions") %>% pull()
error_f <- fehler_add(paste("Vorhandene IDs:", length(vh)), TRUE, error_f)
}
# ── Neue Datensätze bestimmen ─────────────────────────────────────────────────
if (ok) {
neue_daten <- originaldaten %>%
filter(!ID %in% vh) %>%
filter(DATUM >= SYNC_AB) %>%
rename_with(tolower) %>%
mutate(
id = as.integer(id),
datum = as.character(datum),
valuta = as.character(valuta)
)
ok <- nrow(neue_daten) > 0
error_f <- fehler_add(paste("Neue Datensätze:", nrow(neue_daten)), ok, error_f)
}
# ── In SQLite schreiben ───────────────────────────────────────────────────────
if (ok) {
dbWriteTable(conn, "hibiscus_transactions", neue_daten, append = TRUE)
error_f <- fehler_add(
paste(nrow(neue_daten), "Umsätze gespeichert"), TRUE, error_f
)
}
error_f
}
+72
View File
@@ -0,0 +1,72 @@
# R/tree_helpers.R
# Buchungsanzahl rekursiv für einen Knoten und alle Nachkommen
count_recursive <- function(num, df, counts) {
# Alle Nachkommen: alles im Bereich (num, num+1000) je nach Ebene
divisor <- ifelse(num %% 1000 == 0, 1000,
ifelse(num %% 100 == 0, 100,
ifelse(num %% 10 == 0, 10, 1)))
descendants <- df$id[
df$num >= num &
df$num < num + divisor
]
sum(counts$n[counts$account_id %in% descendants], na.rm = TRUE)
}
# Label mit Buchungsanzahl
make_label <- function(num, df, counts) {
name <- df$account_name[df$num == num]
total <- count_recursive(num, df, counts)
ifelse(total > 0,
paste0(name, " (", total, ")"),
name
)
}
# Baum rekursiv aufbauen
build_level <- function(df, counts, parent_num, divisor) {
if (divisor < 1) return(list())
children <- df[
df$num > parent_num &
df$num < parent_num + divisor * 10 &
df$num %% divisor == 0 &
df$num %% (divisor * 10) != 0,
]
if (nrow(children) == 0) return(list())
result <- lapply(seq_len(nrow(children)), function(i) {
child_num <- children$num[i]
subtree <- build_level(df, counts, child_num, divisor / 10)
if (length(subtree) == 0)
structure(list(), sttype = "default")
else
subtree
})
setNames(result, sapply(children$num, make_label, df = df, counts = counts))
}
# Hauptfunktion: kompletten Baum aus accounts-Tabelle bauen
build_account_tree <- function(conn) {
df <- dbReadTable(conn, "accounts")
df$num <- as.integer(substr(df$account_name, 1, 4))
counts <- tbl(conn, "postings") %>%
count(account_id) %>%
collect()
tops <- df[df$num %% 1000 == 0, ]
tree <- lapply(seq_len(nrow(tops)), function(i) {
build_level(df, counts, tops$num[i], 100)
})
setNames(
tree,
sapply(tops$num, make_label, df = df, counts = counts)
)
}
Regular → Executable
View File
+252
View File
@@ -0,0 +1,252 @@
library(shiny)
library(shinyjs)
library(shinyTree)
library(reactable)
library(dplyr)
library(DBI)
# ── UI ────────────────────────────────────────────────────────────────────────
accountsUI <- function(id) {
ns <- NS(id)
tagList(
useShinyjs(),
sidebarLayout(
sidebarPanel(
width = 3,
h4("Konten"),
shinyTree(ns("account_tree"), checkbox = FALSE, search = FALSE)
),
mainPanel(
width = 9,
# Accounts-Felder
wellPanel(
fluidRow(
column(12,
actionButton(ns("new_account"), "Konto neu",
class = "btn-success", icon = icon("plus")),
hr()
)
),
fluidRow(
column(4,
textInput(ns("account_name"), "Kontoname"),
textInput(ns("bank_name"), "Bank")
),
column(4,
numericInput(ns("hibiscus_id"), "Hibiscus Account ID", value = NA),
numericInput(ns("budget_id"), "Budget ID", value = NA)
),
column(4,
# ID read-only anzeigen
disabled(numericInput(ns("account_id"), "ID", value = NA)),
br(),
actionButton(ns("save_account"), "Speichern",
class = "btn-primary", icon = icon("save")),
textOutput(ns("save_status"))
)
)
),
# Buchungen
h4("Buchungen"),
reactableOutput(ns("postings_table"))
)
)
)
}
# ── Server ----
accountsServer <- function(id, conn, r_global) {
moduleServer(id, function(input, output, session) {
ns <- session$ns
# ── Refresh-Trigger ----
refresh <- reactiveVal(0)
# ── Kontendaten ----
accounts <- reactive({
refresh()
dbReadTable(conn, "accounts") %>% arrange(account_name)
})
# ── Baum aufbauen ----
tree_data <- reactive({
refresh()
build_account_tree(conn)
})
output$account_tree <- renderTree({
tree_data()
})
# ── Gewähltes Konto ────────────────────────────────────────────────────────
selected_name <- reactive({
sel <- get_selected(input$account_tree, format = "names")
if (length(sel) == 0) return(NULL)
name <- sub(" \\(\\d+\\)$", "", sel[[1]])
cat("cleaned name:", name, "\n")
name
})
selected_account <- reactive({
req(selected_name())
df <- accounts()
df[df$account_name == selected_name(), ]
})
# ── Inputs befüllen ----
observeEvent(selected_account(), {
acc <- selected_account()
req(nrow(acc) > 0)
updateNumericInput(session, "account_id", value = acc$id)
updateTextInput(session, "account_name", value = acc$account_name)
updateTextInput(session, "bank_name", value = acc$bank_name %||% "")
updateNumericInput(session, "hibiscus_id", value = acc$hibiscus_account_id)
updateNumericInput(session, "budget_id", value = acc$budget_id)
})
# ── Neu-Button ----
observeEvent(input$new_account, {
neue_id <- max_id(conn, "accounts") + 1L
updateNumericInput(session, "account_id", value = neue_id)
updateTextInput(session, "account_name", value = "")
updateTextInput(session, "bank_name", value = "")
updateNumericInput(session, "hibiscus_id", value = NA)
updateNumericInput(session, "budget_id", value = 0)
})
# ── Buchungsanzahl für gewähltes Konto ----
has_postings <- reactive({
req(input$account_id)
count <- tbl(conn, "postings") %>%
filter(account_id == !!input$account_id) %>%
count() %>%
pull(n)
count > 0
})
# ── Lösch-Button sperren/freigeben ----
observe({
req(input$account_id)
if (has_postings()) {
shinyjs::disable("delete_account")
shinyjs::runjs(sprintf(
"$('#%s').attr('title', 'Konto hat Buchungen und kann nicht gelöscht werden')",
ns("delete_account")
))
} else {
shinyjs::enable("delete_account")
shinyjs::runjs(sprintf(
"$('#%s').removeAttr('title')",
ns("delete_account")
))
}
})
# ── Speichern (upsert ----
observeEvent(input$save_account, {
record <- data.frame(
id = input$account_id,
account_name = input$account_name,
bank_name = input$bank_name,
hibiscus_account_id = input$hibiscus_id,
budget_id = input$budget_id,
stringsAsFactors = FALSE
)
dbxUpsert(conn, "accounts", record, where_cols = c("id"))
refresh(refresh() + 1)
output$save_status <- renderText("✓ Gespeichert")
shinyjs::delay(2000, output$save_status <- renderText(""))
})
# ── Löschen ----
observeEvent(input$delete_account, {
req(input$account_id)
showModal(modalDialog(
title = "Konto löschen",
paste0("Konto '", input$account_name, "' wirklich löschen?"),
footer = tagList(
modalButton("Abbrechen"),
actionButton(ns("delete_confirm"), "Ja, löschen", class = "btn-danger")
)
))
})
observeEvent(input$delete_confirm, {
dbxDelete(conn, "accounts", data.frame(id = input$account_id))
removeModal()
refresh(refresh() + 1)
output$save_status <- renderText("✓ Gelöscht")
shinyjs::delay(2000, output$save_status <- renderText(""))
updateNumericInput(session, "account_id", value = NA)
updateTextInput(session, "account_name", value = "")
updateTextInput(session, "bank_name", value = "")
updateNumericInput(session, "hibiscus_id", value = NA)
updateNumericInput(session, "budget_id", value = NA)
})
# ── Buchungen laden (read-only ----
postings_data <- reactive({
req(selected_account())
acc <- selected_account()
req(nrow(acc) > 0)
refresh()
read_buch_tabelle(conn) %>%
filter(account_id == acc$id)
})
# ── Reactable ----
output$postings_table <- renderReactable({
req(postings_data())
reactable(
postings_data() %>%
select(valuta, projektname, display_name, amount, n_attachments),
striped = TRUE,
highlight = TRUE,
selection = "single",
onClick = "select",
defaultSorted = list(valuta = "desc"),
columns = list(
valuta = colDef(name = "Datum", maxWidth = 100),
projektname = colDef(name = "Projekt", maxWidth = 120),
display_name = colDef(name = "Kontakt"),
amount = colDef(
name = "Betrag",
maxWidth = 110,
align = "right",
format = colFormat(currency = "EUR", separators = TRUE, locales = "de-DE")
),
n_attachments = colDef(name = "Anhänge", maxWidth = 80, align = "center")
)
)
})
# ── Sprung zu Buchungen via r_global ----
sel <- reactive(getReactableState("postings_table", "selected"))
observeEvent(sel(), ignoreInit = TRUE, {
req(sel(), postings_data())
entry_id <- postings_data()$entry_id[sel()]
# In accountsServer vor dem Sprung:
r_global$nav_history <- c(r_global$nav_history, list(list(
tab = "konten",
account_name = selected_name()
)))
r_global$jump_to_entry_id <- entry_id
r_global$active_tab <- "buchungen"
r_global$jump_to_entry_id <- entry_id
r_global$active_tab <- "buchungen"
})
# ── Rückgabe ───────────────────────────────────────────────────────────────
return(list(
selected_account = selected_account
))
})
}
# ── Hilfsfunktion ──────────────────────────────────────────────────────────────
`%||%` <- function(a, b) if (is.na(a) || is.null(a) || a == "") b else a
-130
View File
@@ -1,130 +0,0 @@
# module_buchungen.R
buchungenUI <- function(id) {
ns <- NS(id)
tagList(
h3("Hauptbuchungen"),
reactableOutput(ns("buchungen_table")),
hr(),
h3("Details / Gegenbuchungen"),
reactableOutput(ns("details_table")),
br(),
actionBttn(ns("add_trans"), "Transaktion hinzu", size = "sm", style = "material-flat", color = "warning"),
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"),
# Das UI-Element des Moduls (auch wenn es leer ist)
postingModuleUI(ns("posting_modal"))
)
}
buchungenServer <- function(id, conn) {
moduleServer(id, function(input, output, session) {
ns <- session$ns
# Reactive States ----
postings_data <- reactiveVal(read_buch_tabelle(conn))
details_data <- reactiveVal(NULL)
selected_trans_id <- reactiveVal(NULL)
current_main_idx <- reactiveVal(NULL)
current_page <- reactiveVal(1)
# Trigger-Objekt für das Modal: enthält post_id und einen Counter
# Der Counter erzwingt eine Reaktion, auch wenn die gleiche ID zweimal geklickt wird
modal_trigger <- reactiveVal(list(post_id = NULL, counter = 0))
# --- MODUL-SERVER STARTEN (EINMALIG) ----
update_db_trigger <- postingModuleServer("posting_modal", conn, selected_trans_id, modal_trigger)
# Haupttabelle rendern ----
output$buchungen_table <- renderReactable({
req(postings_data())
f_reactable(daten = postings_data(), coldefs = coldef_entries_tabelle,
selection = "single",
hoehe = "60vh",
defaultSelected = current_main_idx())
})
# Details laden wenn Zeile gewählt wird ----
sel_details <- reactive(getReactableState("buchungen_table", "selected"))
observeEvent(sel_details(), ignoreInit = T, {
current_main_idx(getReactableState("buchungen_table", "selected"))
t_id <- postings_data()[sel_details(), "entry_id"] %>% pull()
selected_trans_id(t_id)
details_data(read_buch_tabelle(conn, trans_id = t_id))
})
output$details_table <- renderReactable({
req(details_data())
f_reactable(details_data(), coldefs = coldef_entries_tabelle, selection = "single", hoehe = NULL)
})
# Event: Detail hinzufügen (Neu) ----
observeEvent(input$add_detail, {
req(selected_trans_id())
modal_trigger(list(post_id = NULL, counter = modal_trigger()$counter + 1))
})
# Event: Detail editieren (Klick auf Detail-Tabelle) ----
sel_detail <- reactive(getReactableState("details_table", "selected"))
observeEvent(sel_detail(), ignoreInit = T, {
p_id <- details_data()$id[sel_detail()]
modal_trigger(list(post_id = p_id, counter = modal_trigger()$counter + 1))
})
# --- DAS REAKTIVE UPDATE ----
# Wenn das Modal sagt "Fertig", laden wir die Details neu
observeEvent(update_db_trigger(), ignoreInit = TRUE, {
req(selected_trans_id())
# Tabellen-Daten neu aus DB ziehen
details_data(read_buch_tabelle(conn, trans_id = selected_trans_id()))
# Optional: Auch Haupttabelle erneuern, falls sich Summen geändert haben
postings_data(read_buch_tabelle(conn))
updateReactable("buchungen_table", data = postings_data(read_buch_tabelle(conn)))
})
# Neue Transaktion Observer ----
observeEvent(input$add_trans, {
new_t_id <- max_id(conn, "entries") + 1
# 1. Entry anlegen
dbxInsert(conn, "entries", data.frame(id = new_t_id))
# 2. Zwei Postings direkt vor-anlegen (z.B. mit Betrag 0)
p_id1 <- max_id(conn, "postings") + 1
p_id2 <- p_id1 + 1
new_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())
)
dbxInsert(conn, "postings", new_postings)
# 3. UI refreshen
selected_trans_id(new_t_id)
postings_data(read_buch_tabelle(conn))
details_data(read_buch_tabelle(conn, trans_id = new_t_id))
daten_postings <- postings_data()
daten_details <- details_data()
updateReactable("buchungen_table", data = daten_postings)
})
observeEvent(input$del_trans, ignoreInit = T, {
req(selected_trans_id())
print( 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))
details_data(leer_df_from_table(conn, "postings") )
daten_postings <- postings_data()
daten_details <- details_data()
updateReactable("buchungen_table", data = daten_postings)
updateReactable("details_table", data = daten_details )
})
})
}
+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)
}
}
Regular → Executable
+24 -3
View File
@@ -1,8 +1,29 @@
##
## 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) {
tbl(conn, "contacts") |> contacts <- tbl(conn, "contacts") |>
select(id, display_name) |> select(id, display_name) |>
collect() |> collect() |>
arrange(display_name) |> arrange(display_name)
(\(df) setNames(df$id, df$display_name))()
choices <- setNames(as.character(contacts$id), contacts$display_name)
return(c("Kein Kontakt" = 0, choices))
} }
+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"
# })
})
}
Regular → Executable
View File
View File
Regular → Executable
View File
Regular → Executable
+30 -3
View File
@@ -9,20 +9,37 @@ read_buch_tabelle <- function(conn, trans_id = NULL){
accounts <- tbl(conn, "accounts") accounts <- tbl(conn, "accounts")
projects <- tbl(conn, "projects") projects <- tbl(conn, "projects")
contacts <- tbl(conn, "contacts") contacts <- tbl(conn, "contacts")
attachments <- tbl(conn, "attachments")
# Anzahl Attachments pro entry_id vorberechnen
attachment_counts <- attachments %>%
group_by(entry_id) %>%
summarise(n_attachments = n(), .groups = "drop")
result <- postings |> result <- postings |>
left_join(entries, by = c("entry_id" = "id")) |> left_join(entries, by = c("entry_id" = "id")) |>
left_join(contacts, by = c("contact_id" = "id")) |> # contact_id jetzt verfügbar left_join(contacts, by = c("contact_id" = "id")) |>
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, account_name, projektname, display_name, amount, entry_id) |> left_join(attachment_counts, by = "entry_id") |>
select(id, valuta, account_name, projektname, display_name, amount, entry_id, n_attachments, account_id, contact_id) |>
collect() %>% collect() %>%
mutate(saldo = 0) mutate(
saldo = 0,
n_attachments = tidyr::replace_na(n_attachments, 0L)
) %>%
arrange(valuta, entry_id)
} }
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_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(
@@ -31,6 +48,8 @@ coldef_entries_tabelle <-
account_name = colDef(name = "Kontoname", width = 200), account_name = colDef(name = "Kontoname", width = 200),
projektname = colDef(name = "Projektname", width = 150), projektname = colDef(name = "Projektname", width = 150),
display_name = colDef(name = "Kontakt", width = 250), display_name = colDef(name = "Kontakt", width = 250),
account_id = colDef(show = FALSE),
entry_id = colDef(show = FALSE),
amount = colDef(name = "Betrag", width = 120, amount = colDef(name = "Betrag", width = 120,
format = colFormat(prefix = "", separators = TRUE, digits = 2), format = colFormat(prefix = "", separators = TRUE, digits = 2),
style = function(value) { style = function(value) {
@@ -38,6 +57,14 @@ coldef_entries_tabelle <-
list(color = color, fontWeight = "bold") # Gibt CSS-Styles zurück list(color = color, fontWeight = "bold") # Gibt CSS-Styles zurück
} }
), ),
n_attachments = colDef(
name = "Anhänge",
filterMethod = JS("function(rows, columnId, filterValue) {
const val = parseFloat(filterValue);
if (isNaN(val)) return rows; // kein gültiger Wert → alles zeigen
return rows.filter(row => row.values[columnId] > val);
}")
),
entry_id = colDef(name ="trans_id", width = 120), entry_id = colDef(name ="trans_id", width = 120),
saldo = colDef( saldo = colDef(
name = "Saldo", name = "Saldo",
+195
View File
@@ -0,0 +1,195 @@
# module_buchungen.R
buchungenUI <- function(id) {
ns <- NS(id)
tagList(
useShinyjs(),
h3("Hauptbuchungen"),
fluidRow(
column(8, reactableOutput(ns("buchungen_table")),
hr(),
h3("Details / Gegenbuchungen"),
uiOutput(ns("details_table")) ,
),
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")),
fileInput(ns("anhang"), NULL, multiple = TRUE,
accept = c(".pdf", ".jpg", ".png", ".xlsx", ".docx"),
width = "100%"),
div(
style = "display: flex; flex-direction: column; gap: 10px; align-items: flex-start;",
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")),
)
)
),
)
}
buchungenServer <- function(id, conn, r_global) {
moduleServer(id, function(input, output, session) {
ns <- session$ns
# ── Reactive Variablen ----
postings_data <- reactiveVal(read_buch_tabelle(conn))
sel_details <- reactiveVal(NULL)
selected_trans_id <- reactiveVal(NULL)
reset_trigger <- reactiveVal(NULL)
current_main_idx <- reactiveVal(NULL)
# ── Filter ----
aktiver_filter <- reactive(r_global$buchungen_filter)
gefilterte_daten <- reactive({
d <- postings_data()
f <- aktiver_filter()
if (f == "alle") d
else if (f == "giro") d |> filter(grepl("0130", account_name))
else if (f == "monat") d |> filter(
floor_date(as.Date(valuta), "month") == floor_date(Sys.Date(), "month")
)
else if (startsWith(f, "contact:")) d |> filter(contact_id == as.integer(sub("contact:", "", f)))
else if (startsWith(f, "project:")) d |> filter(project_id == as.integer(sub("project:", "", f)))
else if (startsWith(f, "account:")) d |> filter(account_id == as.integer(sub("account:", "", f)))
else d
})
# ── Haupttabelle rendern ----
output$buchungen_table <- renderReactable({
reset_trigger()
f_reactable(
daten = gefilterte_daten(),
coldefs = coldef_entries_tabelle,
selection = "single",
hoehe = "60vh",
filterable = TRUE,
)
})
sel <- 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)
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())
n_rows <- nrow(sel_details())
# Alle Zeilen in einer Liste sammeln
all_records <- lapply(1:n_rows, function(ind) {
data.frame(
id = as.integer(input[[paste0("id_", ind)]]),
valuta = as.Date(input[[paste0("valuta_", ind)]]),
amount = as.numeric(input[[paste0("betrag_", ind)]]),
account_id = input[[paste0("account_", ind)]],
projekt_id = input[[paste0("projekt_", ind)]],
stringsAsFactors = FALSE
)
})
# Zu einem großen Dataframe zusammenfügen
final_df <- do.call(rbind, all_records)
browser()
# 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")
})
})
# ── Anhänge ----
output$attachments_ui <- renderUI({
req(sel_details())
entry_id <-
dbxSelect(conn, paste0("SELECT entry_id FROM postings WHERE id=",
sel_details()$id[1])) %>% pull
att <- dbxSelect(conn, paste0(
"SELECT * FROM attachments WHERE entry_id = ", entry_id
))
if (nrow(att) == 0) return(p("Keine Anhänge vorhanden."))
tagList(lapply(seq_len(nrow(att)), function(i) {
ext <- tools::file_ext(att$original_name[i])
filename <- paste0("attachments/", att$id[i], ".", ext)
observeEvent(input[[paste0("open_att_", att$id[i])]], {
showModal(modalDialog(
tags$iframe(src = filename, width = "100%", height = "600px",
style = "border:none;"),
title = att$original_name[i],
size = "l",
easyClose = TRUE,
footer = modalButton("Schließen")
))
}, ignoreInit = TRUE, once = FALSE)
div(
actionLink(ns(paste0("open_att_", att$id[i])), att$original_name[i]),
actionLink(ns(paste0("del_att_", att$id[i])), "✕",
style = "color:red; margin-left:8px;")
)
}))
})
})
}
Regular → Executable
View File
+29
View File
@@ -0,0 +1,29 @@
# R/read_functions.R — Hilfsfunktion
# R/read_functions.R — read_hibiscus bereinigen
read_hibiscus <- function(conn) {
tbl(conn, "hibiscus_transactions") %>%
select(id, konto_id, empfaenger_name, empfaenger_konto,
betrag, zweck, datum, valuta, saldo) %>%
left_join(
tbl(conn, "postings") %>%
select(id, bank_transaction_id, entry_id) %>%
rename(posting_id = id),
by = c("id" = "bank_transaction_id")
) %>%
collect() %>%
mutate(
datum = as.Date(datum), # einfach as.Date reicht da Text "2023-12-25"
valuta = as.Date(valuta),
gebucht = !is.na(posting_id)
) %>%
filter(datum >= "2026-01-01")
}
# R/read_functions.R — Kontakt aus bank_connections auflösen
resolve_contact <- function(conn, empfaenger_name, empfaenger_konto) {
bc <- tbl(conn, "bank_connections") %>%
filter(iban == empfaenger_konto | remote_name == empfaenger_name) %>%
select(contact_id) %>%
collect()
if (nrow(bc) > 0) bc$contact_id[1] else NA_integer_
}
+337
View File
@@ -0,0 +1,337 @@
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)
})
})
}
Regular → Executable
+89 -10
View File
@@ -218,6 +218,22 @@ if (ok) {
creditorid TEXT creditorid TEXT
);") );")
# Attachments ----
exec_sql("CREATE TABLE attachments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entry_id INTEGER,
quittung_id INTEGER,
adress_id INTEGER,
wiso_id INTEGER,
btisch_id TEXT,
original_name TEXT,
kategorie TEXT,
path TEXT NOT NULL,
note TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);")
error_f <- fehler_add("Schema created", TRUE, error_f) error_f <- fehler_add("Schema created", TRUE, error_f)
} }
@@ -226,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(
@@ -237,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
@@ -400,16 +416,79 @@ if (ok) {
dbWriteTable(con_s, "postings", postings, append = TRUE) dbWriteTable(con_s, "postings", postings, append = TRUE)
} }
# Transfer Hibiscus transactions ---- # Daten aus Attachments Tabelle übertragen ----
if(ok){
att <- dbxSelect(con_f, "SELECT id, trans_id, quittung_id, wiso_id, btisch_id,
adress_id, ft_dateiname, beschreibung, created_at, updated_at, ft_extension FROM Attachments") %>%
mutate(
id = as.integer(id),
trans_id = as.integer(trans_id),
quittung_id = as.integer(quittung_id),
wiso_id = as.integer(wiso_id),
btisch_id = as.integer(btisch_id),
adress_id = as.integer(adress_id),
created_at = as.character(created_at),
updated_at = as.character(updated_at),
path = paste0(id,".", ft_extension)
) %>%
rename(
entry_id = trans_id,
note = beschreibung,
original_name = ft_dateiname
) %>%
select(-ft_extension)
dbWriteTable(con_s, "Attachments", att, append = T)
} ## ------------------------------------------------------- 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 ----
if (ok) { if (ok) {
spalten <- dbxSelect(con_f, "SELECT * FROM Umsatz WHERE id=1") %>% bank_connections <- dbxSelect(con_f, "
names %>% SELECT id, adress_id, kontakt, iban, bic, kreditinstitut,
paste(collapse = ",") remote_name, created_at, updated_at
query <- paste("SELECT", spalten, "FROM Umsatz") FROM Bankverbindungen
hib <- dbxSelect(con_f, query) %>% ") %>%
select(-fz_jahr) transmute(
dbWriteTable(con_s, "hibiscus_transactions", hib, append = TRUE) id = as.integer(id),
contact_id = as.integer(adress_id),
contact_text = kontakt,
iban = iban,
bic = bic,
bank_name = kreditinstitut,
remote_name = remote_name,
created_at = as.character(created_at),
updated_at = as.character(updated_at)
)
sum(duplicated(bank_connections$id))
dbWriteTable(con_s, "bank_connections", bank_connections, append = TRUE)
error_f <- fehler_add(
paste(nrow(bank_connections), "Bankverbindungen übertragen"), TRUE, error_f
)
} }
# Close ---- # Close ----
Regular → Executable
BIN
View File
Binary file not shown.
Regular → Executable
View File
Regular → Executable
+3 -2
View File
@@ -5,18 +5,19 @@ library("shinyWidgets")
library("DBI") library("DBI")
library("RSQLite") library("RSQLite")
library("dbx") library("dbx")
library("RJDBC")
library("shinydashboard") library("shinydashboard")
library("reactable") library("reactable")
library("conflicted") library("conflicted")
library("R.utils") library("R.utils")
library("shinyjs")
conflicts_prefer(dplyr::select) conflicts_prefer(dplyr::select)
conflicts_prefer(dplyr::filter) conflicts_prefer(dplyr::filter)
options(shiny.reactlog = TRUE)
options(shiny.error = browser)
conn <- dbConnect(RSQLite::SQLite(), "db/development.sqlite") conn <- dbConnect(RSQLite::SQLite(), "db/development.sqlite")
sourceDirectory("R/") sourceDirectory("R/")
source("~/R/rfunc/fehler_add.R")
Regular → Executable
+55 -2
View File
@@ -1,5 +1,58 @@
server <- function(input, output) { # server.R
server <- function(input, output, session) {
buchungenServer("buchungen_tab", conn) # ── Globaler Navigations-Bus ───────────────────────────────────────────────
r_global <- reactiveValues(
active_tab = NULL,
nav_history = list(),
buchungen_filter = "alle",
umsatz_zeige_alle = FALSE,
umsatz_refresh = 0,
jump_to_entry_id = NULL,
jump_to_account_id = NULL,
jump_to_contact_id = NULL,
jump_to_project_id = NULL
)
# ── Tab-Wechsel ────────────────────────────────────────────────────────────
observeEvent(r_global$active_tab, ignoreInit = TRUE, {
req(r_global$active_tab)
updateTabItems(session, "tabs", selected = r_global$active_tab)
r_global$active_tab <- NULL
})
# ── Back-Button ────────────────────────────────────────────────────────────
observeEvent(input$back_btn, {
req(length(r_global$nav_history) > 0)
last <- tail(r_global$nav_history, 1)[[1]]
r_global$nav_history <- head(r_global$nav_history, -1)
updateTabItems(session, "tabs", selected = last$tab)
})
# ── Filter Buchungen ───────────────────────────────────────────────────────
observeEvent(input$filter_alle, { r_global$buchungen_filter <- "alle" })
observeEvent(input$filter_giro, { r_global$buchungen_filter <- "giro" })
observeEvent(input$filter_monat, { r_global$buchungen_filter <- "monat" })
# ── Filter Umsätze ─────────────────────────────────────────────────────────
observeEvent(input$umsatz_filter_ungebucht, { r_global$umsatz_zeige_alle <- FALSE })
observeEvent(input$umsatz_filter_alle, { r_global$umsatz_zeige_alle <- TRUE })
# ── Sync Hibiscus ──────────────────────────────────────────────────────────
observeEvent(input$umsatz_sync, {
showNotification("Sync läuft...", type = "message", duration = 3)
tryCatch({
source("sync_hibiscus.R", local = TRUE)
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")
})
})
# ── Module ─────────────────────────────────────────────────────────────────
accountsServer("accounts_tab", conn, r_global)
buchungenServer("buchungen_tab", conn, r_global)
umsatzServer("umsatz_tab", conn, r_global)
contactsServer("contacts_tab", conn, r_global)
} }
Regular → Executable
+64 -12
View File
@@ -1,23 +1,75 @@
## ui.R ## # ui.R
dashboardPage( dashboardPage(
dashboardHeader(), dashboardHeader(
## Sidebar content title = "GemFin",
dashboardSidebar(
sidebarMenu( # Back-Button
menuItem("buchungen", tabName = "buchungen", icon = icon("dashboard")), tags$li(
menuItem("konten", tabName = "Konten", icon = icon("th")) class = "dropdown",
style = "padding: 8px 10px;",
actionBttn("back_btn", "Zurück",
size = "xs", style = "minimal",
icon = icon("arrow-left"))
),
# Filter Buchungen
tags$li(
class = "dropdown",
conditionalPanel(
condition = "input.tabs == 'buchungen'",
div(
style = "display: flex; align-items: center; gap: 4px; padding: 8px 4px;",
actionBttn("filter_alle", "Alle", size = "xs", style = "minimal"),
actionBttn("filter_giro", "Girokonto", size = "xs", style = "minimal"),
actionBttn("filter_monat", "Dieser Monat", size = "xs", style = "minimal")
)
) )
), ),
# Filter Umsätze
tags$li(
class = "dropdown",
conditionalPanel(
condition = "input.tabs == 'umsatz'",
div(
style = "display: flex; align-items: center; gap: 4px; padding: 8px 4px;",
actionBttn("umsatz_filter_ungebucht", "Ungebucht",
size = "xs", style = "minimal", color = "warning"),
actionBttn("umsatz_filter_alle", "Alle",
size = "xs", style = "minimal"),
actionBttn("umsatz_sync", "Sync",
size = "xs", style = "minimal",
icon = icon("rotate"), color = "primary")
)
)
)
),
dashboardSidebar(
sidebarMenu(id = "tabs",
menuItem("Buchungen", tabName = "buchungen", icon = icon("list")),
menuItem("Umsätze", tabName = "umsatz", icon = icon("bank")),
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(
# First tab content
tabItem(tabName = "buchungen", tabItem(tabName = "buchungen",
buchungenUI("buchungen_tab") buchungenUI("buchungen_tab")
), ),
tabItem(tabName = "umsatz",
# Second tab content umsatzUI("umsatz_tab")
tabItem(tabName = "widgets", ),
h2("Widgets tab content") tabItem(tabName = "konten",
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.

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