add homekeeper stack
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
screen_home_ui <- function(id) {
|
||||
ns <- NS(id)
|
||||
shiny::tagList(
|
||||
shiny::uiOutput(ns("filter_row")),
|
||||
shiny::div(class = "mb-3",
|
||||
shiny::checkboxInput(ns("show_closed"), "Erledigte anzeigen", value = FALSE)
|
||||
),
|
||||
shiny::uiOutput(ns("list_cards")),
|
||||
shiny::br(),
|
||||
shiny::actionButton(ns("new_btn"),
|
||||
shiny::tagList(shiny::icon("plus"), " Neue Liste"),
|
||||
class = "btn btn-primary w-100",
|
||||
style = "min-height:50px"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
screen_home_server <- function(id, db) {
|
||||
moduleServer(id, function(input, output, session) {
|
||||
ns <- session$ns
|
||||
reload <- reactiveVal(0)
|
||||
type_filter <- reactiveVal("")
|
||||
expanded <- reactiveVal(integer(0))
|
||||
|
||||
# ---- Filter chips --------------------------------------------------------
|
||||
FILTERS <- list(
|
||||
list(lbl = "Alle", val = ""),
|
||||
list(lbl = "Einkaufen", val = "shopping"),
|
||||
list(lbl = "Aufgaben", val = "todo"),
|
||||
list(lbl = "Projekte", val = "project")
|
||||
)
|
||||
|
||||
output$filter_row <- renderUI({
|
||||
cur <- type_filter()
|
||||
shiny::div(class = "d-flex gap-2 mb-3 flex-wrap",
|
||||
lapply(seq_along(FILTERS), function(i) {
|
||||
f <- FILTERS[[i]]
|
||||
active <- identical(cur, f$val)
|
||||
shiny::actionButton(ns(paste0("f_", i)), f$lbl,
|
||||
class = if (active) "btn btn-primary btn-sm" else "btn btn-outline-secondary btn-sm"
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
observe({
|
||||
lapply(seq_along(FILTERS), function(i) {
|
||||
local({
|
||||
li <- i; val <- FILTERS[[li]]$val
|
||||
observeEvent(input[[paste0("f_", li)]], type_filter(val), ignoreInit = TRUE)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
# ---- Data ----------------------------------------------------------------
|
||||
lists <- reactive({
|
||||
reload()
|
||||
db_lists_get_all(db, include_closed = isTRUE(input$show_closed))
|
||||
})
|
||||
|
||||
filtered <- reactive({
|
||||
ls <- lists()
|
||||
tf <- type_filter()
|
||||
if (!is.null(tf) && tf != "") ls[ls$type == tf, ] else ls
|
||||
})
|
||||
|
||||
# ---- Card interactions ---------------------------------------------------
|
||||
observeEvent(input$expand, {
|
||||
id <- as.integer(input$expand)
|
||||
cur <- expanded()
|
||||
expanded(if (id %in% cur) setdiff(cur, id) else c(cur, id))
|
||||
})
|
||||
|
||||
observeEvent(input$item_toggle, {
|
||||
req(input$item_toggle)
|
||||
db_items_set_checked(db,
|
||||
id = as.integer(input$item_toggle$id),
|
||||
checked = isTRUE(input$item_toggle$checked)
|
||||
)
|
||||
reload(reload() + 1L)
|
||||
})
|
||||
|
||||
observeEvent(input$add_item, {
|
||||
req(input$add_item)
|
||||
txt <- trimws(input$add_item$text %||% "")
|
||||
if (nchar(txt) == 0) return()
|
||||
db_items_insert(db,
|
||||
list_id = as.integer(input$add_item$list_id),
|
||||
text = txt
|
||||
)
|
||||
reload(reload() + 1L)
|
||||
})
|
||||
|
||||
observeEvent(input$close_list, {
|
||||
req(input$close_list)
|
||||
db_lists_close(db, id = as.integer(input$close_list))
|
||||
reload(reload() + 1L)
|
||||
})
|
||||
|
||||
observeEvent(input$reopen_list, {
|
||||
req(input$reopen_list)
|
||||
db_lists_reopen(db, id = as.integer(input$reopen_list))
|
||||
reload(reload() + 1L)
|
||||
})
|
||||
|
||||
observeEvent(input$delete_list, {
|
||||
req(input$delete_list)
|
||||
db_lists_delete(db, id = as.integer(input$delete_list))
|
||||
expanded(setdiff(expanded(), as.integer(input$delete_list)))
|
||||
reload(reload() + 1L)
|
||||
})
|
||||
|
||||
# ---- Render cards --------------------------------------------------------
|
||||
output$list_cards <- renderUI({
|
||||
ls <- filtered()
|
||||
exp <- expanded()
|
||||
|
||||
if (nrow(ls) == 0)
|
||||
return(shiny::p(class = "text-muted text-center py-4", "Keine Listen vorhanden."))
|
||||
|
||||
lapply(seq_len(nrow(ls)), function(i) {
|
||||
row <- ls[i, ]
|
||||
is_exp <- row$id %in% exp
|
||||
is_cls <- !is.na(row$closed_at)
|
||||
n_tot <- as.integer(row$item_count %||% 0L)
|
||||
n_chk <- as.integer(row$checked_count %||% 0L)
|
||||
items <- if (is_exp) db_items_by_list(db, row$id) else data.frame()
|
||||
|
||||
type_cls <- switch(row$type,
|
||||
shopping = "bg-warning text-dark",
|
||||
todo = "bg-primary",
|
||||
project = "bg-info text-dark",
|
||||
"bg-secondary"
|
||||
)
|
||||
type_lbl <- switch(row$type,
|
||||
shopping = "Einkaufen",
|
||||
todo = "Aufgaben",
|
||||
project = "Projekt",
|
||||
row$type
|
||||
)
|
||||
|
||||
# Header (always visible, tappable to expand)
|
||||
hdr <- shiny::div(
|
||||
class = paste("tap-card d-flex justify-content-between align-items-center py-2 px-3",
|
||||
if (is_cls) "bg-light" else "bg-white"),
|
||||
style = "border-bottom: 1px solid rgba(0,0,0,.125);",
|
||||
onclick = sprintf("Shiny.setInputValue('%s', %d, {priority:'event'})", ns("expand"), row$id),
|
||||
shiny::div(class = "d-flex align-items-center gap-2 overflow-hidden",
|
||||
shiny::HTML(sprintf('<span class="badge %s flex-shrink-0">%s</span>', type_cls, type_lbl)),
|
||||
shiny::tags$strong(
|
||||
class = paste("text-truncate", if (is_cls) "text-muted" else ""),
|
||||
row$name
|
||||
),
|
||||
if (!is.na(row$t_shirt_size))
|
||||
shiny::HTML(sprintf('<span class="badge bg-secondary flex-shrink-0">%s</span>', row$t_shirt_size))
|
||||
else NULL
|
||||
),
|
||||
shiny::div(class = "d-flex align-items-center gap-2 flex-shrink-0 ms-2",
|
||||
shiny::tags$small(class = "text-muted",
|
||||
if (n_tot > 0) sprintf("%d/%d", n_chk, n_tot) else "leer"
|
||||
),
|
||||
shiny::icon(if (is_exp) "chevron-up" else "chevron-down", class = "text-muted")
|
||||
)
|
||||
)
|
||||
|
||||
# Body (only when expanded)
|
||||
bdy <- if (is_exp) {
|
||||
item_rows <- if (nrow(items) > 0) {
|
||||
lapply(seq_len(n_tot), function(j) {
|
||||
it <- items[j, ]
|
||||
shiny::div(
|
||||
class = "item-row d-flex align-items-start gap-2 py-2 px-3",
|
||||
shiny::tags$input(
|
||||
type = "checkbox",
|
||||
class = "form-check-input mt-1 flex-shrink-0",
|
||||
style = "width:1.1rem;height:1.1rem;cursor:pointer",
|
||||
checked = if (isTRUE(it$checked)) NA else NULL,
|
||||
onclick = sprintf(
|
||||
"Shiny.setInputValue('%s',{id:%d,checked:this.checked},{priority:'event'})",
|
||||
ns("item_toggle"), it$id
|
||||
)
|
||||
),
|
||||
shiny::tags$span(
|
||||
class = if (isTRUE(it$checked))
|
||||
"flex-grow-1 text-muted text-decoration-line-through"
|
||||
else "flex-grow-1",
|
||||
it$text
|
||||
)
|
||||
)
|
||||
})
|
||||
} else {
|
||||
list(shiny::p(class = "text-muted small px-3 py-2 mb-0", "Noch keine Einträge."))
|
||||
}
|
||||
|
||||
inp_id <- sprintf("lk_inp_%d", row$id)
|
||||
add_row <- shiny::div(class = "d-flex gap-2 px-3 pt-2 pb-1",
|
||||
shiny::tags$input(
|
||||
type = "text",
|
||||
id = inp_id,
|
||||
class = "form-control form-control-sm",
|
||||
placeholder = "Neuer Eintrag...",
|
||||
onkeyup = sprintf(
|
||||
"if(event.key==='Enter'){var v=this.value.trim();if(v){Shiny.setInputValue('%s',{list_id:%d,text:v,ts:Date.now()},{priority:'event'});this.value='';}}",
|
||||
ns("add_item"), row$id
|
||||
)
|
||||
),
|
||||
shiny::tags$button(
|
||||
type = "button",
|
||||
class = "btn btn-outline-primary btn-sm flex-shrink-0",
|
||||
onclick = sprintf(
|
||||
"(function(i){var v=i.value.trim();if(v){Shiny.setInputValue('%s',{list_id:%d,text:v,ts:Date.now()},{priority:'event'});i.value='';}})(document.getElementById('%s'))",
|
||||
ns("add_item"), row$id, inp_id
|
||||
),
|
||||
shiny::icon("plus")
|
||||
)
|
||||
)
|
||||
|
||||
footer_btns <- shiny::div(
|
||||
class = "d-flex justify-content-between align-items-center px-3 py-2 bg-light border-top",
|
||||
if (!is_cls) {
|
||||
shiny::tags$button(
|
||||
type = "button",
|
||||
class = "btn btn-success btn-sm",
|
||||
onclick = sprintf("Shiny.setInputValue('%s',%d,{priority:'event'})", ns("close_list"), row$id),
|
||||
shiny::icon("check"), " Erledigt"
|
||||
)
|
||||
} else {
|
||||
shiny::tags$button(
|
||||
type = "button",
|
||||
class = "btn btn-outline-secondary btn-sm",
|
||||
onclick = sprintf("Shiny.setInputValue('%s',%d,{priority:'event'})", ns("reopen_list"), row$id),
|
||||
shiny::icon("undo"), " Wieder öffnen"
|
||||
)
|
||||
},
|
||||
shiny::tags$button(
|
||||
type = "button",
|
||||
class = "btn btn-outline-danger btn-sm",
|
||||
onclick = sprintf(
|
||||
"if(confirm('Liste und alle Einträge löschen?'))Shiny.setInputValue('%s',%d,{priority:'event'})",
|
||||
ns("delete_list"), row$id
|
||||
),
|
||||
shiny::icon("trash")
|
||||
)
|
||||
)
|
||||
|
||||
shiny::div(
|
||||
shiny::tagList(item_rows),
|
||||
add_row,
|
||||
footer_btns
|
||||
)
|
||||
} else NULL
|
||||
|
||||
shiny::div(class = "card mb-2 shadow-sm", hdr, bdy)
|
||||
})
|
||||
})
|
||||
|
||||
# ---- New list modal ------------------------------------------------------
|
||||
observeEvent(input$new_btn, {
|
||||
shiny::showModal(shiny::modalDialog(
|
||||
title = "Neue Liste",
|
||||
footer = shiny::tagList(
|
||||
shiny::modalButton("Abbrechen"),
|
||||
shiny::actionButton(ns("confirm_new"), "Erstellen", class = "btn-primary")
|
||||
),
|
||||
shiny::textInput(ns("new_name"), "Name *"),
|
||||
shiny::selectInput(ns("new_type"), "Typ",
|
||||
choices = c("Einkaufen" = "shopping", "Aufgaben" = "todo", "Projekt" = "project"),
|
||||
selected = "shopping"
|
||||
),
|
||||
shiny::uiOutput(ns("new_extra_ui"))
|
||||
))
|
||||
})
|
||||
|
||||
output$new_extra_ui <- renderUI({
|
||||
req(input$new_type)
|
||||
if (input$new_type == "project") {
|
||||
shiny::selectInput(ns("new_size"), "Aufwand (T-Shirt-Größe)",
|
||||
choices = c("XS", "S", "M", "L", "XL"),
|
||||
selected = "M"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
observeEvent(input$confirm_new, {
|
||||
name <- trimws(input$new_name %||% "")
|
||||
if (nchar(name) == 0) {
|
||||
shiny::showNotification("Bitte einen Namen eingeben.", type = "warning", duration = 3)
|
||||
return()
|
||||
}
|
||||
t_size <- if (!is.null(input$new_type) && input$new_type == "project")
|
||||
input$new_size else NA
|
||||
db_lists_insert(db,
|
||||
name = name,
|
||||
type = input$new_type,
|
||||
t_shirt_size = t_size
|
||||
)
|
||||
shiny::removeModal()
|
||||
reload(reload() + 1L)
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user