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:
parent
5a5372b831
commit
5f156ab6a0
13 changed files with 175 additions and 16 deletions
|
|
@ -103,3 +103,8 @@ type CakeCount struct {
|
|||
Name string
|
||||
Amount int
|
||||
}
|
||||
|
||||
type BlockedDate struct {
|
||||
ID int
|
||||
Date string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,6 +141,27 @@ func (h *Server) cake(r *http.Request) (templ.Component, int, error) {
|
|||
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) {
|
||||
catalogue, err := h.s.GetCakes()
|
||||
if err != nil {
|
||||
|
|
@ -150,13 +171,14 @@ func (h *Server) order(r *http.Request) (templ.Component, int, error) {
|
|||
if err != nil {
|
||||
return nil, http.StatusInternalServerError, err
|
||||
}
|
||||
blockedDates := loadBlockedDates(h)
|
||||
|
||||
url := strings.Split(r.URL.Path, "/")
|
||||
if len(url) < 3 || url[2] == "" {
|
||||
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])
|
||||
|
|
@ -170,8 +192,8 @@ func (h *Server) order(r *http.Request) (templ.Component, int, error) {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,6 +142,29 @@ func safeStr(s []string, i int) string {
|
|||
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) {
|
||||
id, err := strconv.Atoi(r.PathValue("id"))
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ func New(store *store.Store) *Server {
|
|||
mux.HandleFunc("POST /order/", server.render(server.postOrder))
|
||||
mux.HandleFunc("POST /order/{id}/status", server.render(server.updateStatus))
|
||||
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)
|
||||
mux.Handle("GET /static/", fs)
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@
|
|||
/* ========== SPLIT PANE ========== */
|
||||
.split-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
|
@ -139,19 +140,19 @@
|
|||
}
|
||||
|
||||
.order-aside {
|
||||
width: 20rem;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media (max-width: 64em) {
|
||||
@media (min-width: 64em) {
|
||||
.split-pane {
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.order-aside {
|
||||
width: 100%;
|
||||
width: 20rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@
|
|||
|
||||
/* ========== CAKE BREAKDOWN LIST ========== */
|
||||
.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-empty { color: var(--on-surface-variant); font-size: 0.75rem; }
|
||||
|
||||
|
|
|
|||
|
|
@ -267,3 +267,13 @@ function applyCatalogueFilters() {
|
|||
function filterCatalogue(input) {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
44
server/templates/blocked.templ
Normal file
44
server/templates/blocked.templ
Normal 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>
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -73,6 +74,15 @@ func orderItemsDesc(order models.Order) string {
|
|||
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 {
|
||||
var b strings.Builder
|
||||
b.WriteString("<script>")
|
||||
|
|
|
|||
|
|
@ -10,18 +10,22 @@ templ Nav() {
|
|||
<p>Tracker</p>
|
||||
</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>Nowe zamówienie</span>
|
||||
</a>
|
||||
<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>Panel główny</span>
|
||||
</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>Katalog</span>
|
||||
</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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import (
|
|||
"git.bronku.xyz/bronku/cake-order-tracker/models"
|
||||
)
|
||||
|
||||
templ OrderPage(order models.Order, catalogue []models.Cake, allTags []models.Tag) {
|
||||
@Layout(OrderTitle(order), templ.NopComponent, OrderForm(order, catalogue, allTags))
|
||||
templ OrderPage(order models.Order, catalogue []models.Cake, allTags []models.Tag, blockedDates []models.BlockedDate) {
|
||||
@Layout(OrderTitle(order), templ.NopComponent, OrderForm(order, catalogue, allTags, blockedDates))
|
||||
}
|
||||
|
||||
templ OrderForm(order models.Order, catalogue []models.Cake, allTags []models.Tag) {
|
||||
<form method="post" class="split-pane">
|
||||
templ OrderForm(order models.Order, catalogue []models.Cake, allTags []models.Tag, blockedDates []models.BlockedDate) {
|
||||
<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) }/>
|
||||
<div class="order-left">
|
||||
<section class="card">
|
||||
|
|
|
|||
33
store/blocked_date.go
Normal file
33
store/blocked_date.go
Normal 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
4
store/migrations/5.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
create table blocked_date (
|
||||
id integer primary key autoincrement,
|
||||
date text not null unique
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue