diff --git a/server/get.go b/server/get.go index 34fed4b..e930e6d 100644 --- a/server/get.go +++ b/server/get.go @@ -19,11 +19,11 @@ func weekInterval(t time.Time) (firstDay, lastDay time.Time) { return monday, sunday } -func (h *Server) orders(r *http.Request) (templ.Component, int, error) { +func parseDateRange(r *http.Request) (first, last time.Time) { now := time.Now() weekFirst, weekLast := weekInterval(now) - first, last := weekFirst, weekLast + first, last = weekFirst, weekLast if from := r.URL.Query().Get("from"); from != "" { if t, err := time.Parse(config.DateFormat, from); err == nil { first = t @@ -34,23 +34,89 @@ func (h *Server) orders(r *http.Request) (templ.Component, int, error) { last = t } } - - today := now.Format(config.DateFormat) - tomorrow := now.AddDate(0, 0, 1).Format(config.DateFormat) - - orders, err := h.s.GetOrders(first, last) - return templates.OrdersPage(first.Format(config.DateFormat), last.Format(config.DateFormat), today, tomorrow, orders), http.StatusOK, err + return } -func (h *Server) cakes(_ *http.Request) (templ.Component, int, error) { - data, err := h.s.GetCakes() - return templates.CakesPage(data), http.StatusOK, err +func filterOrdersWithSpecialCakes(orders []models.Order) []models.Order { + var out []models.Order + for _, o := range orders { + if len(o.SpecialCakes) > 0 { + out = append(out, o) + } + } + return out +} + +func (h *Server) orders(r *http.Request) (templ.Component, int, error) { + first, last := parseDateRange(r) + now := time.Now() + today := now.Format(config.DateFormat) + tomorrow := now.AddDate(0, 0, 1).Format(config.DateFormat) + specialFilter := r.URL.Query().Get("special") + + orders, err := h.s.GetOrders(first, last) + if err != nil { + return nil, http.StatusInternalServerError, err + } + if specialFilter == "true" { + orders = filterOrdersWithSpecialCakes(orders) + } + cakeCounts, err := h.s.GetCakeCounts(first, last) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + firstStr := first.Format(config.DateFormat) + lastStr := last.Format(config.DateFormat) + + if r.Header.Get("HX-Target") == "main" { + counts, err := h.s.GetOrderCounts() + if err != nil { + return nil, http.StatusInternalServerError, err + } + return templates.MainContent("Panel główny", templates.DateForm(firstStr, lastStr, today, tomorrow, specialFilter), templates.OrdersMain(counts, cakeCounts, orders, firstStr, lastStr, specialFilter)), http.StatusOK, nil + } + + if r.Header.Get("HX-Request") == "true" { + return templates.OrdersContent(orders, cakeCounts, firstStr, lastStr, specialFilter), http.StatusOK, nil + } + + counts, err := h.s.GetOrderCounts() + if err != nil { + return nil, http.StatusInternalServerError, err + } + + return templates.OrdersPage(firstStr, lastStr, today, tomorrow, specialFilter, counts, cakeCounts, orders), http.StatusOK, nil +} + +func (h *Server) cakes(r *http.Request) (templ.Component, int, error) { + cakes, err := h.s.GetCakes() + if err != nil { + return nil, http.StatusInternalServerError, err + } + tags, err := h.s.GetTags() + if err != nil { + return nil, http.StatusInternalServerError, err + } + + if r.Header.Get("HX-Request") == "true" { + return templates.MainContent("Katalog", templ.NopComponent, templates.CakesMain(cakes, tags)), http.StatusOK, nil + } + + return templates.CakesPage(cakes, tags), http.StatusOK, nil } func (h *Server) cake(r *http.Request) (templ.Component, int, error) { url := strings.Split(r.URL.Path, "/") if len(url) < 3 || url[2] == "" { - return templates.CakePage(models.Cake{}), http.StatusOK, nil + tags, err := h.s.GetTags() + if err != nil { + return nil, http.StatusInternalServerError, err + } + if r.Header.Get("HX-Request") == "true" { + return templates.MainContent("Ciasto nowe", templ.NopComponent, templates.CakeForm(models.Cake{}, tags)), http.StatusOK, nil + } + return templates.CakePage(models.Cake{}, tags), http.StatusOK, nil } id, err := strconv.Atoi(url[2]) @@ -63,19 +129,34 @@ func (h *Server) cake(r *http.Request) (templ.Component, int, error) { return nil, http.StatusNotFound, err } - return templates.CakePage(data), http.StatusOK, nil + tags, err := h.s.GetTags() + if err != nil { + return nil, http.StatusInternalServerError, err + } + + if r.Header.Get("HX-Request") == "true" { + return templates.MainContent(templates.CakeTitle(data), templ.NopComponent, templates.CakeForm(data, tags)), http.StatusOK, nil + } + + return templates.CakePage(data, tags), http.StatusOK, nil } func (h *Server) order(r *http.Request) (templ.Component, int, error) { - var err error catalogue, err := h.s.GetCakes() if err != nil { return nil, http.StatusInternalServerError, err } + tags, err := h.s.GetTags() + if err != nil { + return nil, http.StatusInternalServerError, err + } url := strings.Split(r.URL.Path, "/") if len(url) < 3 || url[2] == "" { - return templates.OrderPage(models.Order{Date: time.Now()}, catalogue), http.StatusOK, nil + 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.OrderPage(models.Order{Date: time.Now()}, catalogue, tags), http.StatusOK, nil } id, err := strconv.Atoi(url[2]) @@ -88,5 +169,9 @@ func (h *Server) order(r *http.Request) (templ.Component, int, error) { return nil, http.StatusNotFound, err } - return templates.OrderPage(order, catalogue), http.StatusOK, nil + 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.OrderPage(order, catalogue, tags), http.StatusOK, nil } diff --git a/server/post.go b/server/post.go index dde7f04..fca74c3 100644 --- a/server/post.go +++ b/server/post.go @@ -29,6 +29,22 @@ func (h *Server) postCake(r *http.Request) (templ.Component, int, error) { if err != nil { return nil, http.StatusBadRequest, err } + + for _, idStr := range r.Form["tag"] { + tagID, err := strconv.Atoi(idStr) + if err != nil { + continue + } + n.Tags = append(n.Tags, models.Tag{ID: tagID}) + } + if newTag := strings.TrimSpace(r.FormValue("new_tag")); newTag != "" { + tagID, err := h.s.CreateTag(newTag) + if err != nil { + return nil, http.StatusInternalServerError, err + } + n.Tags = append(n.Tags, models.Tag{ID: tagID, Name: newTag}) + } + n.ID, err = h.s.SaveCake(n) return templates.Confirmation("Potwierdzone", n.ID), http.StatusAccepted, err } @@ -67,6 +83,47 @@ func (h *Server) postOrder(r *http.Request) (templ.Component, int, error) { n.Cakes = append(n.Cakes, e) } + specialNames := r.Form["special_name[]"] + specialPrices := r.Form["special_price[]"] + specialSizes := r.Form["special_size[]"] + specialShapes := r.Form["special_shape[]"] + specialFlavours := r.Form["special_flavour[]"] + specialNotes := r.Form["special_notes[]"] + specialTags := r.Form["special_tags[]"] + specialNewTags := r.Form["special_new_tag[]"] + for i := range specialNames { + price, err := models.ParsePrice(specialPrices[i]) + if err != nil { + continue + } + + var tags []models.Tag + if i < len(specialTags) && specialTags[i] != "" { + for _, idStr := range strings.Split(specialTags[i], ",") { + id, err := strconv.Atoi(strings.TrimSpace(idStr)) + if err == nil { + tags = append(tags, models.Tag{ID: id}) + } + } + } + if newTag := strings.TrimSpace(safeStr(specialNewTags, i)); newTag != "" { + tagID, err := h.s.CreateTag(newTag) + if err == nil { + tags = append(tags, models.Tag{ID: tagID, Name: newTag}) + } + } + + n.SpecialCakes = append(n.SpecialCakes, models.SpecialCake{ + Name: specialNames[i], + Price: price, + Size: safeStr(specialSizes, i), + Shape: safeStr(specialShapes, i), + Flavour: safeStr(specialFlavours, i), + Notes: safeStr(specialNotes, i), + Tags: tags, + }) + } + n.Accepted = time.Now() n.Name = strings.TrimSpace(r.FormValue("name")) n.Surname = strings.TrimSpace(r.FormValue("surname")) @@ -77,3 +134,26 @@ func (h *Server) postOrder(r *http.Request) (templ.Component, int, error) { n.ID, err = h.s.SaveOrder(n) return templates.Confirmation("Potwierdzone", n.ID), http.StatusAccepted, err } + +func safeStr(s []string, i int) string { + if i < len(s) { + return s[i] + } + return "" +} + +func (h *Server) updateStatus(r *http.Request) (templ.Component, int, error) { + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + return nil, http.StatusBadRequest, err + } + order, err := h.s.GetOrder(id) + if err != nil { + return nil, http.StatusNotFound, err + } + order.Status = r.FormValue("status") + if _, err := h.s.SaveOrder(order); err != nil { + return nil, http.StatusInternalServerError, err + } + return templates.OrderRow(order), http.StatusOK, nil +} diff --git a/server/server.go b/server/server.go index babbe4a..92ddd77 100644 --- a/server/server.go +++ b/server/server.go @@ -36,6 +36,7 @@ func New(store *store.Store) *Server { mux.HandleFunc("GET /cake/", server.render(server.cake)) mux.HandleFunc("GET /cakes", server.render(server.cakes)) 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)) fs := http.FileServerFS(static) diff --git a/server/static/basket.css b/server/static/basket.css index 381d178..65d4d36 100644 --- a/server/static/basket.css +++ b/server/static/basket.css @@ -42,6 +42,10 @@ white-space: nowrap; } + .basket-actions .action-btn .material-symbols-outlined { + font-size: 1rem; + } + .remove { color: var(--error); cursor: pointer; diff --git a/server/static/card.css b/server/static/card.css index 06b27c1..3ab5d76 100644 --- a/server/static/card.css +++ b/server/static/card.css @@ -55,7 +55,7 @@ /* ========== METRICS GRID ========== */ .metrics-grid { display: grid; - grid-template-columns: repeat(4, 1fr); + grid-template-columns: repeat(2, 1fr); gap: 0.75rem; > div { @@ -128,21 +128,21 @@ gap: 1rem; flex: 1; min-height: 0; +} - > div:first-child { - flex: 1; - display: flex; - flex-direction: column; - gap: 1rem; - min-width: 0; - } +.order-left { + flex: 1; + display: flex; + flex-direction: column; + gap: 1rem; + min-width: 0; +} - > aside { - width: 20rem; - flex-shrink: 0; - display: flex; - flex-direction: column; - } +.order-aside { + width: 20rem; + flex-shrink: 0; + display: flex; + flex-direction: column; } @media (max-width: 64em) { @@ -150,7 +150,7 @@ flex-direction: column; } - .split-pane > aside { + .order-aside { width: 100%; } } @@ -237,5 +237,6 @@ .text-muted { color: var(--on-surface-variant); + font-weight: 400; } } diff --git a/server/static/component.css b/server/static/component.css index 9255f4b..9bced30 100644 --- a/server/static/component.css +++ b/server/static/component.css @@ -60,61 +60,31 @@ } } -/* ========== STATUS CHIPS ========== */ -.status-chip { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.5rem; - border-radius: var(--border-radius-full); - font-size: 0.75rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.02em; - - &.ready, - &.done { - background: var(--success-bg); - color: var(--success-text); - } - - &.decorating { - background: var(--warning-bg); - color: var(--warning-text); - } - - &.baking { - background: var(--danger-bg); - color: var(--danger-text); - } - - &.queued { - background: var(--info-bg); - color: var(--info-text); - } - - &.accepted { - background: #fff3e0; - color: #e65100; - } -} - -/* ========== BADGE PILLS ========== */ -.badge-pill { +/* ========== TAG CHIPS ========== */ +.tag-chip { display: inline-flex; align-items: center; padding: 0.25rem 0.5rem; border-radius: var(--border-radius-full); font-size: 0.75rem; font-weight: 600; + color: var(--on-surface-variant); + background: var(--surface-container-high); + border: 1px solid var(--outline-variant); + cursor: pointer; + user-select: none; + transition: all var(--transition); - &.signature { - background: var(--primary-container); - color: var(--primary); + &.sm { + font-size: 0.625rem; + padding: 0.125rem 0.375rem; } - &.seasonal { - background: var(--surface-container-high); - color: var(--on-surface); + &:hover, + &.active { + color: var(--primary); + border-color: var(--primary); + background: var(--primary-container); } } @@ -138,46 +108,154 @@ } } -/* ========== PAGINATION ========== */ -.pagination { - display: flex; - gap: 0.25rem; - align-items: center; - - .btn { - width: 1.5rem; - height: 1.5rem; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid var(--outline-variant); - border-radius: var(--border-radius-sm); - background: transparent; - cursor: pointer; - color: var(--on-surface-variant); - transition: all var(--transition); - font-size: 0.75rem; - - &:hover { - background: var(--surface-container-low); - } - - &:disabled { - opacity: 0.5; - cursor: default; - } - - &.active { - background: var(--primary); - color: var(--on-primary); - border-color: var(--primary); - } - - .material-symbols-outlined { - font-size: 1rem; - } - } +/* ========== OVERLAY / DRAWER ========== */ +.overlay { + position: fixed; + inset: 0; + z-index: 1000; + display: none; } +.overlay.open { + display: flex; + justify-content: flex-end; +} +.overlay-backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.3); +} +.overlay-panel { + position: relative; + width: 22rem; + max-width: 100vw; + height: 100%; + background: var(--surface); + display: flex; + flex-direction: column; + box-shadow: -0.25rem 0 1.5rem rgba(0, 0, 0, 0.15); + animation: slideIn 0.2s ease; +} +@keyframes slideIn { + from { transform: translateX(100%); } + to { transform: translateX(0); } +} +.overlay-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--outline-variant); +} +.overlay-header h2 { + font-size: 1rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; +} +.overlay-header .close-btn { + background: none; + border: none; + cursor: pointer; + color: var(--on-surface-variant); + padding: 0.25rem; + border-radius: var(--border-radius-sm); + display: flex; + align-items: center; + justify-content: center; +} +.overlay-header .close-btn:hover { + background: var(--surface-container-high); + color: var(--on-surface); +} +.overlay-body { + flex: 1; + overflow-y: auto; + padding: 1.25rem; +} +.overlay-footer { + display: flex; + gap: 0.5rem; + justify-content: flex-end; + padding: 1rem 1.25rem; + border-top: 1px solid var(--outline-variant); +} + +/* ========== CARD HEADER ACTIONS ========== */ +.header-actions { + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* ========== TORT BADGE ========== */ +.tort-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.25rem; + height: 1.25rem; + border-radius: var(--border-radius-full); + background: var(--primary-container); + color: var(--primary); + flex-shrink: 0; + vertical-align: middle; + margin-right: 0.25rem; +} +.tort-badge .material-symbols-outlined { + font-size: 0.75rem; +} + +/* ========== SEARCH INPUT UTILITY ========== */ +.search-wrapper { position: relative; width: 11.25rem; } +.search-icon { position: absolute; left: 0.5rem; top: 50%; transform: translateY(-50%); font-size: 1rem; color: var(--on-surface-variant); pointer-events: none; } +.search-input { height: 1.75rem; padding-left: 1.75rem; padding-right: 0.5rem; font-size: 0.75rem; border: 1px solid var(--outline-variant); border-radius: var(--border-radius-sm); background: var(--surface-container-lowest); color: var(--on-surface); outline: none; width: 100%; } + +/* ========== STATUS SELECT ========== */ +.status-select { font-size: 0.75rem; font-weight: 600; padding: 0.25rem 0.375rem; border: 1px solid var(--outline-variant); border-radius: var(--border-radius-sm); background: var(--surface); color: var(--on-surface); cursor: pointer; } + +/* ========== FONT UTILITIES ========== */ +.font-bold { font-weight: 700; } + +/* ========== 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-amount { font-weight: 600; } +.breakdown-empty { color: var(--on-surface-variant); font-size: 0.75rem; } + +/* ========== FILTER BAR (catalogue/cakes page) ========== */ +.filter-bar { padding: 0.5rem 1rem; border-bottom: 1px solid var(--outline-variant); display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: center; } +.filter-bar .filter-label { font-size: 0.75rem; font-weight: 600; color: var(--on-surface-variant); align-self: center; margin-right: 0.25rem; } + +/* ========== CATALOGUE TAG ROW ========== */ +.tag-row { padding: 0.5rem 1rem 0; display: flex; flex-wrap: wrap; gap: 0.25rem; } + +/* ========== CATALOGUE SCROLL ========== */ +.catalogue-scroll { overflow-y: auto; padding: 1rem; } + +/* ========== BASKET ACTIONS ========== */ +.basket-actions { display: flex; flex-direction: column; gap: 0.125rem; } + +/* ========== TAG GROUP ========== */ +.tag-group { margin-top: 0.25rem; display: flex; flex-wrap: wrap; gap: 0.125rem; } + +/* ========== OVERLAY FORM ========== */ +.overlay-form { display: flex; flex-direction: column; gap: 0.75rem; } + +/* ========== TAG CONTAINER ========== */ +.tag-grid { display: flex; flex-wrap: wrap; gap: 0.25rem; } + +/* ========== FONT UTILITIES ========== */ +.font-medium { font-weight: 500; } + +/* ========== FLEX UTILITIES ========== */ +.grow { flex: 1; } + +.icon.sm { width: 1.75rem; height: 1.75rem; } +.icon.sm .material-symbols-outlined { font-size: 1rem; } + +/* ========== RESULTS PANE ========== */ +#results-pane { display: flex; flex-direction: column; gap: 1rem; } /* ========== CONFIRMATION ========== */ section.confirmation { @@ -189,109 +267,14 @@ section.confirmation { gap: 0.5rem; text-align: center; + .success-icon { + font-size: 3rem; + color: var(--success-text); + } + h2 { font-size: 1.25rem; font-weight: 600; color: var(--on-surface); } } - -/* ========== LOGIN PAGE ========== */ -.login-page { - display: flex; - justify-content: center; - align-items: center; - min-height: 100dvh; - background-color: var(--surface-container-low); -} - -.login-card { - background-color: var(--surface); - border: 1px solid var(--outline-variant); - border-radius: var(--border-radius-md); - padding: 2.5rem; - box-shadow: 0 0.25rem 1.5rem rgba(0, 0, 0, 0.1); - width: 100%; - max-width: 25rem; - position: relative; - overflow: hidden; - - &::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 0.25rem; - background: linear-gradient( - 90deg, - var(--primary), - var(--primary-container) - ); - } - - .icon { - display: flex; - justify-content: center; - margin-bottom: 1.5rem; - color: var(--primary); - } - - h1 { - font-size: 1.5rem; - font-weight: 700; - margin-bottom: 1.5rem; - color: var(--on-background); - text-align: center; - letter-spacing: -0.02em; - } - - form { - display: flex; - flex-direction: column; - gap: 1rem; - } - - label { - display: flex; - flex-direction: column; - gap: 0.25rem; - font-size: 0.75rem; - font-weight: 600; - color: var(--on-surface-variant); - } - - input { - padding: 0.5rem 0.75rem; - border: 1px solid var(--outline-variant); - border-radius: var(--border-radius-sm); - font-size: 1rem; - font-family: inherit; - background-color: var(--surface); - color: var(--on-surface); - transition: border-color var(--transition); - - &:focus { - outline: none; - border-color: var(--primary); - } - } - - button { - padding: 0.5rem 2rem; - background-color: var(--primary); - color: #fff; - border: none; - border-radius: var(--border-radius-md); - font-size: 1rem; - font-weight: 600; - font-family: inherit; - cursor: pointer; - transition: opacity var(--transition); - margin-top: 0.5rem; - - &:hover { - opacity: 0.9; - } - } -} diff --git a/server/static/form.css b/server/static/form.css index 007a65a..69581d7 100644 --- a/server/static/form.css +++ b/server/static/form.css @@ -81,6 +81,40 @@ form { } } +/* ========== TOGGLE GROUP ========== */ +.toggle-group { + display: flex; + height: 2rem; + border-radius: var(--border-radius-sm); + border: 1px solid var(--outline-variant); + overflow: hidden; +} + +.toggle-btn { + flex: 1; + border: none; + background: var(--surface-container-lowest); + color: var(--on-surface-variant); + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + transition: all var(--transition); + padding: 0 0.5rem; +} + +.toggle-btn:not(:last-child) { + border-right: 1px solid var(--outline-variant); +} + +.toggle-btn.active { + background: var(--primary); + color: var(--on-primary); +} + +.toggle-btn:not(.active):hover { + background: var(--surface-container-high); +} + .form-actions { display: flex; justify-content: flex-end; diff --git a/server/static/layout.css b/server/static/layout.css index b91203d..027d0fc 100644 --- a/server/static/layout.css +++ b/server/static/layout.css @@ -11,23 +11,54 @@ aside.sidebar { flex-direction: column; padding: 1rem; z-index: 40; + transition: width 0.2s ease, padding 0.2s ease; .brand { + display: flex; + align-items: center; + gap: 0.5rem; margin-bottom: 1.5rem; padding: 0 0.5rem; - h1 { - font-size: 1.25rem; - font-weight: 700; - color: var(--primary); - letter-spacing: -0.01em; - line-height: 1.3; + .collapse-btn { + background: none; + border: none; + cursor: pointer; + color: var(--on-surface-variant); + padding: 0.25rem; + border-radius: var(--border-radius-sm); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + &:hover { + background: var(--surface-container-high); + color: var(--on-surface); + } + + .material-symbols-outlined { + font-size: 1.25rem; + } } - p { - font-size: 0.75rem; - color: var(--on-surface-variant); - margin-top: 0.25rem; + .brand-text { + overflow: hidden; + white-space: nowrap; + + h1 { + font-size: 1.25rem; + font-weight: 700; + color: var(--primary); + letter-spacing: -0.01em; + line-height: 1.3; + } + + p { + font-size: 0.75rem; + color: var(--on-surface-variant); + margin-top: 0.25rem; + } } } @@ -140,6 +171,47 @@ aside.sidebar { } } +/* ---------- COLLAPSED ---------- */ +body.sidebar-collapsed aside.sidebar { + width: 3.5rem; + padding: 0.5rem; + + .brand .brand-text { + display: none; + } + + .cta { + padding: 0; + width: 2.5rem; + height: 2.5rem; + margin-left: auto; + margin-right: auto; + border-radius: 50%; + + span:not(.material-symbols-outlined) { + display: none; + } + } + + nav a.link { + justify-content: center; + padding: 0.75rem 0.5rem; + + span:not(.material-symbols-outlined) { + display: none; + } + } + + footer .user .info { + display: none; + } +} + +body.sidebar-collapsed main { + margin-left: 3.5rem; + width: calc(100% - 3.5rem); +} + /* ========== MAIN LAYOUT ========== */ main { margin-left: var(--sidebar-width); @@ -147,6 +219,7 @@ main { display: flex; flex-direction: column; height: 100dvh; + transition: margin-left 0.2s ease, width 0.2s ease; } /* ========== TOP NAVBAR ========== */ @@ -228,14 +301,13 @@ header.topnav { } @media (max-width: 48em) { - aside.sidebar { + body:not(.sidebar-collapsed) aside.sidebar { width: 3.5rem; padding: 0.5rem; - .brand h1, - .brand p, - nav a.link span:not(.material-symbols-outlined), + .brand .brand-text, .cta span:not(.material-symbols-outlined), + nav a.link span:not(.material-symbols-outlined), .user .info { display: none; } @@ -258,6 +330,18 @@ header.topnav { } } + body.sidebar-collapsed aside.sidebar { + width: 0; + padding: 0; + overflow: hidden; + border-right: none; + } + + body.sidebar-collapsed main { + margin-left: 0; + width: 100%; + } + main { margin-left: 3.5rem; width: calc(100% - 3.5rem); diff --git a/server/static/order.js b/server/static/order.js index 48df069..dd2c912 100644 --- a/server/static/order.js +++ b/server/static/order.js @@ -1,3 +1,144 @@ +var editingSpecialId = null; + +function openSpecialCakeOverlay() { + editingSpecialId = null; + document.getElementById("sc-overlay-title").textContent = "Dodaj tort specjalny"; + document.getElementById("sc-name").value = ""; + document.getElementById("sc-price").value = ""; + document.getElementById("sc-size").value = ""; + document.getElementById("sc-shape").value = ""; + document.getElementById("sc-flavour").value = ""; + document.getElementById("sc-notes").value = ""; + document.getElementById("sc-new-tag").value = ""; + var cbs = document.querySelectorAll(".sc-tag-cb"); + for (var i = 0; i < cbs.length; i++) cbs[i].checked = false; + document.getElementById("sc-overlay").classList.add("open"); + document.getElementById("sc-name").focus(); +} + +function closeSpecialCakeOverlay() { + document.getElementById("sc-overlay").classList.remove("open"); + editingSpecialId = null; +} + +function getOverlayFormData() { + return { + name: document.getElementById("sc-name").value.trim(), + price: document.getElementById("sc-price").value, + size: document.getElementById("sc-size").value.trim(), + shape: document.getElementById("sc-shape").value, + flavour: document.getElementById("sc-flavour").value.trim(), + notes: document.getElementById("sc-notes").value.trim(), + tagIds: (function() { + var ids = []; + var cbs = document.querySelectorAll(".sc-tag-cb:checked"); + for (var i = 0; i < cbs.length; i++) ids.push(cbs[i].value); + return ids; + })(), + tagNames: (function() { + var names = []; + var cbs = document.querySelectorAll(".sc-tag-cb:checked"); + for (var i = 0; i < cbs.length; i++) + names.push(cbs[i].closest(".tag-chip").textContent.trim()); + return names; + })(), + newTag: document.getElementById("sc-new-tag").value.trim() + }; +} + +function buildSpecialCakeHTML(d) { + var parts = []; + if (d.size) parts.push(d.size); + if (d.flavour) parts.push(d.flavour); + if (d.shape === "round") parts.push("okragly"); + else if (d.shape === "square") parts.push("kwadrat"); + if (d.notes) parts.push("notatka: " + d.notes); + var detail = parts.length ? parts.join(", ") : ""; + var tagHtml = d.tagNames.length > 0 || d.newTag ? '