Accounts Tree erstellt

This commit is contained in:
2026-03-24 16:00:52 +01:00
parent 71621ad8ac
commit 126c2fc7d7
7 changed files with 460 additions and 56 deletions
+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)
)
}
+253
View File
@@ -0,0 +1,253 @@
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")
cat("raw selected:", paste(sel, collapse=", "), "\n")
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
+61 -29
View File
@@ -24,32 +24,38 @@ buchungenUI <- function(id) {
) )
} }
buchungenServer <- function(id, conn, aktiver_filter = reactive("alle")) { 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) details_data <- reactiveVal(NULL)
selected_trans_id <- reactiveVal(NULL) selected_trans_id <- reactiveVal(NULL)
current_main_idx <- reactiveVal(NULL) current_main_idx <- reactiveVal(NULL)
reset_trigger <- reactiveVal(0) # ← neu: erzwingt Re-render mit Filter-Reset reset_trigger <- reactiveVal(0)
modal_trigger <- reactiveVal(list(post_id = NULL, counter = 0)) modal_trigger <- reactiveVal(list(post_id = NULL, counter = 0))
highlighted_valuta <- reactiveVal(NULL) highlighted_valuta <- reactiveVal(NULL)
update_db_trigger <- postingModuleServer(
"posting_modal", conn, selected_trans_id, modal_trigger
)
update_db_trigger <- postingModuleServer("posting_modal", conn, selected_trans_id, modal_trigger) # ── Filter ─────────────────────────────────────────────────────────────────
aktiver_filter <- reactive(r_global$buchungen_filter)
gefilterte_daten <- reactive({ gefilterte_daten <- reactive({
d <- postings_data() d <- postings_data()
switch(aktiver_filter(), f <- aktiver_filter()
"giro" = d |> filter(grepl("0130", account_name)), if (f == "alle") d
"monat" = d |> filter( 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") floor_date(as.Date(valuta), "month") == floor_date(Sys.Date(), "month")
),
d
) )
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
}) })
observeEvent(aktiver_filter(), { observeEvent(aktiver_filter(), {
@@ -58,11 +64,35 @@ buchungenServer <- function(id, conn, aktiver_filter = reactive("alle")) {
details_data(NULL) details_data(NULL)
}) })
# * Haupttabelle rendern ---- # ── Sprung von anderem Modul ───────────────────────────────────────────────
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(
daten = gefilterte_daten(), # ← geändert daten = gefilterte_daten(),
coldefs = coldef_entries_tabelle, coldefs = coldef_entries_tabelle,
selection = "single", selection = "single",
hoehe = "60vh", hoehe = "60vh",
@@ -71,16 +101,18 @@ buchungenServer <- function(id, conn, aktiver_filter = reactive("alle")) {
) )
}) })
# * Details-Tabelle laden ---- # ── Details laden ──────────────────────────────────────────────────────────
sel_details <- reactive(getReactableState("buchungen_table", "selected")) sel_details <- reactive(getReactableState("buchungen_table", "selected"))
observeEvent(sel_details(), ignoreInit = TRUE, { observeEvent(sel_details(), ignoreInit = TRUE, {
req(sel_details()) req(sel_details())
highlighted_valuta(gefilterte_daten()[sel_details(), "valuta"]) highlighted_valuta(gefilterte_daten()[sel_details(), "valuta"])
current_main_idx(sel_details()) current_main_idx(sel_details())
t_id <- gefilterte_daten()[sel_details(), "entry_id"] |> pull() # ← geändert t_id <- gefilterte_daten()[sel_details(), "entry_id"] |> pull()
selected_trans_id(t_id) selected_trans_id(t_id)
details_data(read_buch_tabelle(conn, trans_id = t_id)) details_data(read_buch_tabelle(conn, trans_id = t_id))
}) })
output$details_table <- renderReactable({ output$details_table <- renderReactable({
req(details_data()) req(details_data())
f_reactable( f_reactable(
@@ -91,13 +123,13 @@ buchungenServer <- function(id, conn, aktiver_filter = reactive("alle")) {
) )
}) })
# * Detail hinzufügen ---- # ── Detail hinzufügen ──────────────────────────────────────────────────────
observeEvent(input$add_detail, { observeEvent(input$add_detail, {
req(selected_trans_id()) req(selected_trans_id())
modal_trigger(list(post_id = NULL, counter = modal_trigger()$counter + 1)) modal_trigger(list(post_id = NULL, counter = modal_trigger()$counter + 1))
}) })
# * Detail editieren (Klick auf Detail-Tabelle) ---- # ── Detail editieren ───────────────────────────────────────────────────────
sel_detail <- reactive(getReactableState("details_table", "selected")) sel_detail <- reactive(getReactableState("details_table", "selected"))
observeEvent(sel_detail(), ignoreInit = TRUE, { observeEvent(sel_detail(), ignoreInit = TRUE, {
@@ -105,16 +137,15 @@ buchungenServer <- function(id, conn, aktiver_filter = reactive("alle")) {
modal_trigger(list(post_id = p_id, counter = modal_trigger()$counter + 1)) modal_trigger(list(post_id = p_id, counter = modal_trigger()$counter + 1))
}) })
# * DB-Update durch Modal ---- # ── DB-Update durch Modal ──────────────────────────────────────────────────
observeEvent(update_db_trigger(), ignoreInit = TRUE, { observeEvent(update_db_trigger(), ignoreInit = TRUE, {
req(selected_trans_id()) req(selected_trans_id())
details_data(read_buch_tabelle(conn, trans_id = selected_trans_id())) details_data(read_buch_tabelle(conn, trans_id = selected_trans_id()))
postings_data(read_buch_tabelle(conn)) postings_data(read_buch_tabelle(conn))
# Kein Re-render nötig Filter/Sortierung bleibt erhalten updateReactable("buchungen_table", data = gefilterte_daten())
updateReactable("buchungen_table", data = postings_data())
}) })
# * Neue Transaktion ---- # ── Neue Transaktion ───────────────────────────────────────────────────────
observeEvent(input$add_trans, { observeEvent(input$add_trans, {
new_t_id <- max_id(conn, "entries") + 1 new_t_id <- max_id(conn, "entries") + 1
dbxInsert(conn, "entries", data.frame(id = new_t_id)) dbxInsert(conn, "entries", data.frame(id = new_t_id))
@@ -127,19 +158,21 @@ buchungenServer <- function(id, conn, aktiver_filter = reactive("alle")) {
account_id = c(0, 0), account_id = c(0, 0),
valuta = c(Sys.Date(), Sys.Date()) valuta = c(Sys.Date(), Sys.Date())
)) ))
## ** Daten aktualisieren ----
# Filter zurücksetzen damit neue Transaktion sichtbar ist
r_global$buchungen_filter <- "alle"
postings_data(read_buch_tabelle(conn)) postings_data(read_buch_tabelle(conn))
selected_trans_id(new_t_id) selected_trans_id(new_t_id)
details_data(read_buch_tabelle(conn, trans_id = new_t_id)) details_data(read_buch_tabelle(conn, trans_id = new_t_id))
## ** Index in neuen Daten suchen ----
neue_zeile <- which(postings_data()$id == p_id1) neue_zeile <- which(postings_data()$id == p_id1)
current_main_idx(neue_zeile) current_main_idx(neue_zeile)
## ** Re-render erzwingen → Filter wird zurückgesetzt, neue Zeile selektiert ----
reset_trigger(isolate(reset_trigger()) + 1) reset_trigger(isolate(reset_trigger()) + 1)
scroll_to_row(ns("buchungen_table"), neue_zeile) scroll_to_row(ns("buchungen_table"), neue_zeile)
}) })
# * Transaktion löschen ---- # ── Transaktion löschen ────────────────────────────────────────────────────
observeEvent(input$del_trans, ignoreInit = TRUE, { observeEvent(input$del_trans, ignoreInit = TRUE, {
req(selected_trans_id()) req(selected_trans_id())
dbxDelete(conn, "postings", where = data.frame(entry_id = selected_trans_id())) dbxDelete(conn, "postings", where = data.frame(entry_id = selected_trans_id()))
@@ -148,11 +181,10 @@ buchungenServer <- function(id, conn, aktiver_filter = reactive("alle")) {
current_main_idx(NULL) current_main_idx(NULL)
selected_trans_id(NULL) selected_trans_id(NULL)
details_data(NULL) details_data(NULL)
updateReactable("buchungen_table", data = postings_data()) updateReactable("buchungen_table", data = gefilterte_daten())
# details_table re-rendert automatisch weil details_data(NULL) → req() schlägt fehl
}) })
# ── Anhänge ────────────────────────────────────────────────────────────────
output$attachments_ui <- renderUI({ output$attachments_ui <- renderUI({
req(selected_trans_id()) req(selected_trans_id())
att <- dbxSelect(conn, paste0( att <- dbxSelect(conn, paste0(
@@ -164,10 +196,10 @@ buchungenServer <- function(id, conn, aktiver_filter = reactive("alle")) {
ext <- tools::file_ext(att$original_name[i]) ext <- tools::file_ext(att$original_name[i])
filename <- paste0("attachments/", att$id[i], ".", ext) filename <- paste0("attachments/", att$id[i], ".", ext)
# Observer direkt hier registrieren
observeEvent(input[[paste0("open_att_", att$id[i])]], { observeEvent(input[[paste0("open_att_", att$id[i])]], {
showModal(modalDialog( showModal(modalDialog(
tags$iframe(src = filename, width = "100%", height = "600px", style = "border:none;"), tags$iframe(src = filename, width = "100%", height = "600px",
style = "border:none;"),
title = att$original_name[i], title = att$original_name[i],
size = "l", size = "l",
easyClose = TRUE, easyClose = TRUE,
+3 -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) |> select(id, valuta, account_name, projektname, display_name, amount, entry_id, n_attachments, account_id) |>
collect() %>% collect() %>%
mutate( mutate(
saldo = 0, saldo = 0,
@@ -43,6 +43,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) {
Binary file not shown.
+47 -12
View File
@@ -1,14 +1,49 @@
server <- function(input, output) { server <- function(input, output, session) {
aktiver_filter <- reactive({
if (input$filter_giro > 0 &&
input$filter_giro == max(input$filter_giro, input$filter_monat, input$filter_alle))
"giro"
else if (input$filter_monat > 0 &&
input$filter_monat == max(input$filter_giro, input$filter_monat, input$filter_alle))
"monat"
else
"alle"
})
buchungenServer("buchungen_tab", conn, aktiver_filter = aktiver_filter)
# ── Globaler Navigations-Bus ───────────────────────────────────────────────
r_global <- reactiveValues(
# Navigation
active_tab = NULL,
nav_history = list(), # ← Stack der letzten Positionen
# Filter + Sprungziele
buchungen_filter = "alle",
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)
cat("Tab-Wechsel zu:", r_global$active_tab, "\n")
updateTabItems(session, "tabs", selected = r_global$active_tab)
r_global$active_tab <- NULL
})
observeEvent(input$back_btn, {
req(length(r_global$nav_history) > 0)
# Letzten Zustand holen und Stack kürzen
last <- tail(r_global$nav_history, 1)[[1]]
r_global$nav_history <- head(r_global$nav_history, -1)
# Zurückspringen
updateTabItems(session, "tabs", selected = last$tab)
# Zustand wiederherstellen — je nach Tab
if (last$tab == "konten") {
r_global$jump_to_account_name <- last$account_name
}
})
# ── Filter-Buttons (falls in der Navbar oder UI definiert) ─────────────────
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" })
# ── Module ─────────────────────────────────────────────────────────────────
accountsServer("accounts_tab", conn, r_global)
buchungenServer("buchungen_tab", conn, r_global)
} }
+13 -3
View File
@@ -1,6 +1,16 @@
## ui.R ## ## ui.R ##
dashboardPage( dashboardPage(
dashboardHeader( ), dashboardHeader(
title = "GemFin",
tags$li(
class = "dropdown",
style = "padding: 8px 10px;",
actionBttn("back_btn", "Zurück",
size = "xs",
style = "minimal",
icon = icon("arrow-left"))
)
),
## Sidebar content ## Sidebar content
dashboardSidebar( dashboardSidebar(
sidebarMenu(id = "tabs", sidebarMenu(id = "tabs",
@@ -38,8 +48,8 @@ dashboardPage(
buchungenUI("buchungen_tab") buchungenUI("buchungen_tab")
), ),
# Second tab content # Second tab content
tabItem(tabName = "widgets", tabItem(tabName = "konten",
h2("Widgets tab content") accountsUI("accounts_tab")
) )
) )
) )