blocked dates page and warning for special-cake orders

Add blocked_date table (migration 5), model, and store CRUD methods.
Add management page at /blocked with add/delete functionality.
Add nav link 'Terminy zablokowane' with event_busy icon.
Pass blocked dates to order form as JSON data attribute.
On form submit, if the order has special cakes and the date is
blocked, show a confirm() dialog warning the user.
This commit is contained in:
bronkuu 2026-06-15 18:54:48 +02:00
parent 5a5372b831
commit 5f156ab6a0
13 changed files with 175 additions and 16 deletions

View file

@ -103,3 +103,8 @@ type CakeCount struct {
Name string Name string
Amount int Amount int
} }
type BlockedDate struct {
ID int
Date string
}

View file

@ -141,6 +141,27 @@ func (h *Server) cake(r *http.Request) (templ.Component, int, error) {
return templates.CakePage(data, tags), http.StatusOK, nil return templates.CakePage(data, tags), http.StatusOK, nil
} }
func (h *Server) blocked(r *http.Request) (templ.Component, int, error) {
dates, err := h.s.GetBlockedDates()
if err != nil {
return nil, http.StatusInternalServerError, err
}
if r.Header.Get("HX-Request") == "true" {
return templates.MainContent("Terminy zablokowane", templ.NopComponent, templates.BlockedMain(dates)), http.StatusOK, nil
}
return templates.BlockedPage(dates), http.StatusOK, nil
}
func loadBlockedDates(h *Server) []models.BlockedDate {
dates, err := h.s.GetBlockedDates()
if err != nil {
return nil
}
return dates
}
func (h *Server) order(r *http.Request) (templ.Component, int, error) { func (h *Server) order(r *http.Request) (templ.Component, int, error) {
catalogue, err := h.s.GetCakes() catalogue, err := h.s.GetCakes()
if err != nil { if err != nil {
@ -150,13 +171,14 @@ func (h *Server) order(r *http.Request) (templ.Component, int, error) {
if err != nil { if err != nil {
return nil, http.StatusInternalServerError, err return nil, http.StatusInternalServerError, err
} }
blockedDates := loadBlockedDates(h)
url := strings.Split(r.URL.Path, "/") url := strings.Split(r.URL.Path, "/")
if len(url) < 3 || url[2] == "" { if len(url) < 3 || url[2] == "" {
if r.Header.Get("HX-Request") == "true" { if r.Header.Get("HX-Request") == "true" {
return templates.MainContent("Nowe zamówienie", templ.NopComponent, templates.OrderForm(models.Order{Date: time.Now()}, catalogue, tags)), http.StatusOK, nil return templates.MainContent("Nowe zamówienie", templ.NopComponent, templates.OrderForm(models.Order{Date: time.Now()}, catalogue, tags, blockedDates)), http.StatusOK, nil
} }
return templates.OrderPage(models.Order{Date: time.Now()}, catalogue, tags), http.StatusOK, nil return templates.OrderPage(models.Order{Date: time.Now()}, catalogue, tags, blockedDates), http.StatusOK, nil
} }
id, err := strconv.Atoi(url[2]) id, err := strconv.Atoi(url[2])
@ -170,8 +192,8 @@ func (h *Server) order(r *http.Request) (templ.Component, int, error) {
} }
if r.Header.Get("HX-Request") == "true" { if r.Header.Get("HX-Request") == "true" {
return templates.MainContent(templates.OrderTitle(order), templ.NopComponent, templates.OrderForm(order, catalogue, tags)), http.StatusOK, nil return templates.MainContent(templates.OrderTitle(order), templ.NopComponent, templates.OrderForm(order, catalogue, tags, blockedDates)), http.StatusOK, nil
} }
return templates.OrderPage(order, catalogue, tags), http.StatusOK, nil return templates.OrderPage(order, catalogue, tags, blockedDates), http.StatusOK, nil
} }

View file

@ -142,6 +142,29 @@ func safeStr(s []string, i int) string {
return "" return ""
} }
func (h *Server) postBlocked(r *http.Request) (templ.Component, int, error) {
if err := r.ParseForm(); err != nil {
return nil, http.StatusBadRequest, err
}
if err := h.s.AddBlockedDate(r.FormValue("date")); err != nil {
return nil, http.StatusInternalServerError, err
}
dates, _ := h.s.GetBlockedDates()
return templates.BlockedMain(dates), http.StatusOK, nil
}
func (h *Server) deleteBlocked(r *http.Request) (templ.Component, int, error) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
return nil, http.StatusBadRequest, err
}
if err := h.s.RemoveBlockedDate(id); err != nil {
return nil, http.StatusInternalServerError, err
}
dates, _ := h.s.GetBlockedDates()
return templates.BlockedMain(dates), http.StatusOK, nil
}
func (h *Server) updateStatus(r *http.Request) (templ.Component, int, error) { func (h *Server) updateStatus(r *http.Request) (templ.Component, int, error) {
id, err := strconv.Atoi(r.PathValue("id")) id, err := strconv.Atoi(r.PathValue("id"))
if err != nil { if err != nil {

View file

@ -38,6 +38,9 @@ func New(store *store.Store) *Server {
mux.HandleFunc("POST /order/", server.render(server.postOrder)) mux.HandleFunc("POST /order/", server.render(server.postOrder))
mux.HandleFunc("POST /order/{id}/status", server.render(server.updateStatus)) mux.HandleFunc("POST /order/{id}/status", server.render(server.updateStatus))
mux.HandleFunc("POST /cake/", server.render(server.postCake)) mux.HandleFunc("POST /cake/", server.render(server.postCake))
mux.HandleFunc("GET /blocked", server.render(server.blocked))
mux.HandleFunc("POST /blocked", server.render(server.postBlocked))
mux.HandleFunc("POST /blocked/{id}/delete", server.render(server.deleteBlocked))
fs := http.FileServerFS(static) fs := http.FileServerFS(static)
mux.Handle("GET /static/", fs) mux.Handle("GET /static/", fs)

View file

@ -125,6 +125,7 @@
/* ========== SPLIT PANE ========== */ /* ========== SPLIT PANE ========== */
.split-pane { .split-pane {
display: flex; display: flex;
flex-direction: column;
gap: 1rem; gap: 1rem;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@ -139,19 +140,19 @@
} }
.order-aside { .order-aside {
width: 20rem; width: 100%;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@media (max-width: 64em) { @media (min-width: 64em) {
.split-pane { .split-pane {
flex-direction: column; flex-direction: row;
} }
.order-aside { .order-aside {
width: 100%; width: 20rem;
} }
} }

View file

@ -219,7 +219,7 @@
/* ========== CAKE BREAKDOWN LIST ========== */ /* ========== CAKE BREAKDOWN LIST ========== */
.breakdown-list { list-style: none; padding: 0; margin: 0; } .breakdown-list { list-style: none; padding: 0; margin: 0; }
.breakdown-item { display: flex; justify-content: space-between; padding: 0.375rem 0; border-bottom: 1px solid var(--outline-variant); } .breakdown-item { display: flex; justify-content: space-between; padding: 0.375rem 1rem; margin: 0 -1rem; border-bottom: 1px solid var(--outline-variant); }
.breakdown-amount { font-weight: 600; } .breakdown-amount { font-weight: 600; }
.breakdown-empty { color: var(--on-surface-variant); font-size: 0.75rem; } .breakdown-empty { color: var(--on-surface-variant); font-size: 0.75rem; }

View file

@ -267,3 +267,13 @@ function applyCatalogueFilters() {
function filterCatalogue(input) { function filterCatalogue(input) {
applyCatalogueFilters(); applyCatalogueFilters();
} }
function confirmBlockedDate(form) {
var date = form.querySelector('[name="date"]').value;
var blocked = JSON.parse(form.getAttribute("data-blocked-dates") || "[]");
var hasSpecial = form.querySelector('[name="special_name[]"]');
if (hasSpecial && blocked.indexOf(date) >= 0) {
return confirm("Data " + date + " jest zablokowana dla zamówień z tortami specjalnymi. Czy na pewno chcesz kontynuować?");
}
return true;
}

View file

@ -0,0 +1,44 @@
package templates
import (
"strconv"
"git.bronku.xyz/bronku/cake-order-tracker/models"
)
templ BlockedPage(blocked []models.BlockedDate) {
@Layout("Terminy zablokowane", templ.NopComponent, BlockedMain(blocked))
}
templ BlockedMain(blocked []models.BlockedDate) {
<section class="card" style="max-width:30rem">
<header>
<h2>
<span class="material-symbols-outlined">block</span>
Terminy zablokowane
</h2>
</header>
<div>
<form method="POST" action="/blocked" class="horizontal-group" style="gap:0.5rem;margin-bottom:1rem">
<input type="date" name="date" required class="form-input" style="width:auto"/>
<button type="submit" class="btn primary">Zablokuj</button>
</form>
if len(blocked) == 0 {
<p class="text-muted" style="text-align:center;padding:1rem 0">Brak zablokowanych terminów</p>
} else {
<ul class="breakdown-list">
for _, b := range blocked {
<li class="breakdown-item">
<span>{ b.Date }</span>
<form method="POST" action={ templ.URL("/blocked/" + strconv.Itoa(b.ID) + "/delete") } style="display:inline">
<button type="submit" class="action-btn" title="Usuń">
<span class="material-symbols-outlined">close</span>
</button>
</form>
</li>
}
</ul>
}
</div>
</section>
}

View file

@ -1,6 +1,7 @@
package templates package templates
import ( import (
"encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -73,6 +74,15 @@ func orderItemsDesc(order models.Order) string {
return desc return desc
} }
func blockedDatesJSON(dates []models.BlockedDate) string {
names := make([]string, len(dates))
for i, d := range dates {
names[i] = d.Date
}
b, _ := json.Marshal(names)
return string(b)
}
func orderScript(order models.Order) string { func orderScript(order models.Order) string {
var b strings.Builder var b strings.Builder
b.WriteString("<script>") b.WriteString("<script>")

View file

@ -10,18 +10,22 @@ templ Nav() {
<p>Tracker</p> <p>Tracker</p>
</div> </div>
</div> </div>
<a href="/order" class="cta" hx-get="/order" hx-target="#main" hx-push-url="true"> <a href="/order" class="cta" hx-get="/order" hx-target="#main" hx-swap="outerHTML" hx-push-url="true">
<span class="material-symbols-outlined">add</span> <span class="material-symbols-outlined">add</span>
<span>Nowe zamówienie</span> <span>Nowe zamówienie</span>
</a> </a>
<nav> <nav>
<a href="/orders" class="link" hx-get="/orders" hx-target="#main" hx-push-url="true"> <a href="/orders" class="link" hx-get="/orders" hx-target="#main" hx-swap="outerHTML" hx-push-url="true">
<span class="material-symbols-outlined">dashboard</span> <span class="material-symbols-outlined">dashboard</span>
<span>Panel główny</span> <span>Panel główny</span>
</a> </a>
<a href="/cakes" class="link" hx-get="/cakes" hx-target="#main" hx-push-url="true"> <a href="/cakes" class="link" hx-get="/cakes" hx-target="#main" hx-swap="outerHTML" hx-push-url="true">
<span class="material-symbols-outlined">inventory_2</span> <span class="material-symbols-outlined">inventory_2</span>
<span>Katalog</span> <span>Katalog</span>
</a> </a>
<a href="/blocked" class="link" hx-get="/blocked" hx-target="#main" hx-swap="outerHTML" hx-push-url="true">
<span class="material-symbols-outlined">event_busy</span>
<span>Terminy zablokowane</span>
</a>
</nav> </nav>
} }

View file

@ -6,12 +6,12 @@ import (
"git.bronku.xyz/bronku/cake-order-tracker/models" "git.bronku.xyz/bronku/cake-order-tracker/models"
) )
templ OrderPage(order models.Order, catalogue []models.Cake, allTags []models.Tag) { templ OrderPage(order models.Order, catalogue []models.Cake, allTags []models.Tag, blockedDates []models.BlockedDate) {
@Layout(OrderTitle(order), templ.NopComponent, OrderForm(order, catalogue, allTags)) @Layout(OrderTitle(order), templ.NopComponent, OrderForm(order, catalogue, allTags, blockedDates))
} }
templ OrderForm(order models.Order, catalogue []models.Cake, allTags []models.Tag) { templ OrderForm(order models.Order, catalogue []models.Cake, allTags []models.Tag, blockedDates []models.BlockedDate) {
<form method="post" class="split-pane"> <form method="post" class="split-pane" onsubmit="return confirmBlockedDate(this)" data-blocked-dates={ blockedDatesJSON(blockedDates) }>
<input type="hidden" name="id" value={ strconv.Itoa(order.ID) }/> <input type="hidden" name="id" value={ strconv.Itoa(order.ID) }/>
<div class="order-left"> <div class="order-left">
<section class="card"> <section class="card">

33
store/blocked_date.go Normal file
View file

@ -0,0 +1,33 @@
package store
import (
"git.bronku.xyz/bronku/cake-order-tracker/models"
)
func (s *Store) GetBlockedDates() ([]models.BlockedDate, error) {
rows, err := s.db.Query("select id, date from blocked_date order by date;")
if err != nil {
return nil, err
}
defer rows.Close()
var out []models.BlockedDate
for rows.Next() {
var b models.BlockedDate
if err := rows.Scan(&b.ID, &b.Date); err != nil {
return nil, err
}
out = append(out, b)
}
return out, rows.Err()
}
func (s *Store) AddBlockedDate(date string) error {
_, err := s.db.Exec("insert or ignore into blocked_date(date) values (?);", date)
return err
}
func (s *Store) RemoveBlockedDate(id int) error {
_, err := s.db.Exec("delete from blocked_date where id = ?;", id)
return err
}

4
store/migrations/5.sql Normal file
View file

@ -0,0 +1,4 @@
create table blocked_date (
id integer primary key autoincrement,
date text not null unique
);