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
|
Name string
|
||||||
Amount int
|
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
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
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
|
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>")
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
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