more simpler + torty sectino

This commit is contained in:
bronkuu 2026-06-15 20:30:15 +02:00
parent a74c276cd3
commit 3b184c7c6f
10 changed files with 375 additions and 152 deletions

View file

@ -96,6 +96,21 @@ type CakeCount struct {
Amount int Amount int
} }
type SpecialCakeWithOrder struct {
ID int
OrderID int
Name string
Price Price
Size string
Shape string
Flavour string
Notes string
Tags []Tag
ClientName string
ClientSurname string
OrderDate time.Time
}
type BlockedDate struct { type BlockedDate struct {
ID int ID int
Date string Date string

View file

@ -65,6 +65,10 @@ func (h *Server) orders(r *http.Request) (templ.Component, int, error) {
if err != nil { if err != nil {
return nil, http.StatusInternalServerError, err return nil, http.StatusInternalServerError, err
} }
specialCakes, err := h.s.GetSpecialCakesInRange(first, last)
if err != nil {
return nil, http.StatusInternalServerError, err
}
firstStr := first.Format(config.DateFormat) firstStr := first.Format(config.DateFormat)
lastStr := last.Format(config.DateFormat) lastStr := last.Format(config.DateFormat)
@ -74,11 +78,11 @@ func (h *Server) orders(r *http.Request) (templ.Component, int, error) {
if err != nil { if err != nil {
return nil, http.StatusInternalServerError, err return nil, http.StatusInternalServerError, err
} }
return templates.MainContent("Panel główny", templates.OrdersMain(counts, cakeCounts, orders, firstStr, lastStr, specialFilter, today, tomorrow)), http.StatusOK, nil return templates.MainContent("Panel główny", templates.OrdersMain(counts, cakeCounts, orders, specialCakes, firstStr, lastStr, specialFilter, today, tomorrow)), http.StatusOK, nil
} }
if r.Header.Get("HX-Request") == "true" { if r.Header.Get("HX-Request") == "true" {
return templates.OrdersContent(orders, cakeCounts, firstStr, lastStr, specialFilter), http.StatusOK, nil return templates.OrdersContent(orders, cakeCounts, specialCakes, firstStr, lastStr, specialFilter), http.StatusOK, nil
} }
counts, err := h.s.GetOrderCounts() counts, err := h.s.GetOrderCounts()
@ -86,7 +90,7 @@ func (h *Server) orders(r *http.Request) (templ.Component, int, error) {
return nil, http.StatusInternalServerError, err return nil, http.StatusInternalServerError, err
} }
return templates.OrdersPage(firstStr, lastStr, today, tomorrow, specialFilter, counts, cakeCounts, orders), http.StatusOK, nil return templates.OrdersPage(firstStr, lastStr, today, tomorrow, specialFilter, counts, cakeCounts, specialCakes, orders), http.StatusOK, nil
} }
func (h *Server) cakes(r *http.Request) (templ.Component, int, error) { func (h *Server) cakes(r *http.Request) (templ.Component, int, error) {

View file

@ -25,7 +25,6 @@
--space-lg: 1.5rem; --space-lg: 1.5rem;
--space-xl: 2rem; --space-xl: 2rem;
--sidebar-w: 17.5rem;
--topnav-h: 3.5rem; --topnav-h: 3.5rem;
--radius-sm: 0.25rem; --radius-sm: 0.25rem;

View file

@ -749,3 +749,53 @@ textarea + label {
.hide { .hide {
display: none; display: none;
} }
.link-btn {
background: none;
border: none;
color: var(--primary);
font-weight: 600;
font-size: inherit;
cursor: pointer;
padding: 0;
text-align: left;
}
.link-btn:hover {
text-decoration: underline;
}
.overlay-panel--view {
width: 24rem;
}
.view-field {
display: flex;
flex-direction: column;
gap: 0.125rem;
padding: var(--space-sm) 0;
border-bottom: 1px solid var(--outline-dim);
}
.view-field:last-child {
border-bottom: none;
}
.view-field > label {
font-size: 0.75rem;
font-weight: 600;
color: var(--on-surface-dim);
letter-spacing: 0.02em;
}
.view-field > span,
.view-field > div {
font-size: 0.875rem;
color: var(--on-surface);
}
#sc-view-tags {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
}

View file

@ -1,32 +1,52 @@
body { body {
display: grid; display: grid;
grid-template-columns: var(--sidebar-w) 1fr;
grid-template-rows: var(--topnav-h) 1fr; grid-template-rows: var(--topnav-h) 1fr;
grid-template-areas:
"sidebar topnav"
"sidebar main";
height: 100dvh; height: 100dvh;
} }
aside { .topnav {
grid-area: sidebar; display: flex;
background: var(--surface-white); align-items: center;
border-right: 1px solid var(--outline-dim); gap: var(--space-md);
display: flex; padding: 0 var(--space-lg);
flex-direction: column; border-bottom: 1px solid var(--outline-dim);
padding: var(--space-md); background: var(--surface);
gap: var(--space-sm); z-index: 30;
overflow: hidden; }
z-index: 40;
} .topnav > h1 {
margin-right: auto;
aside > .cta { }
width: 100%;
height: 2.5rem; .topnav > nav {
display: flex;
gap: var(--space-xs);
}
.topnav > nav > a {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
gap: var(--space-sm); gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-md);
font-size: 0.875rem;
font-weight: 500;
color: var(--on-surface-dim);
text-decoration: none;
}
.topnav > nav > a.active {
background: var(--primary-container);
color: var(--primary);
font-weight: 700;
}
.topnav > .cta {
display: flex;
align-items: center;
gap: var(--space-sm);
height: 2.25rem;
padding: 0 var(--space-md);
background: var(--primary); background: var(--primary);
color: var(--on-primary); color: var(--on-primary);
border: none; border: none;
@ -35,74 +55,10 @@ aside > .cta {
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
}
aside > nav {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
aside > nav > a {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-md);
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
color: var(--on-surface-dim);
}
aside > nav > a.active {
background: var(--primary-container);
color: var(--primary);
font-weight: 700;
}
.topnav {
grid-area: topnav;
border-bottom: 1px solid var(--outline-dim);
background: var(--surface);
display: flex;
align-items: center;
gap: var(--space-lg);
padding: 0 var(--space-lg);
flex-shrink: 0; flex-shrink: 0;
position: sticky;
top: 0;
z-index: 30;
}
.topnav > h1 {
margin-right: auto;
}
.topnav > .search {
position: relative;
width: 15rem;
}
.topnav > .search input {
height: 2rem;
padding-left: 2rem;
font-size: 0.875rem;
}
.topnav > .search .material-symbols-outlined {
position: absolute;
left: var(--space-sm);
top: 50%;
transform: translateY(-50%);
font-size: 1.25rem;
color: var(--on-surface-dim);
pointer-events: none;
} }
main { main {
grid-area: main;
overflow-y: auto; overflow-y: auto;
} }
@ -120,52 +76,38 @@ main {
#orders-results { #orders-results {
grid-column: 1 / -1; grid-column: 1 / -1;
display: flex; display: grid;
flex-direction: column; grid-template-columns: 1fr 1fr;
gap: var(--space-md); gap: var(--space-md);
} }
@media (max-width: 64em) {
.topnav > .search {
width: 10rem;
}
}
@media (max-width: 48em) { @media (max-width: 48em) {
aside {
width: var(--sidebar-w);
padding: var(--space-xs);
}
aside > .cta {
width: 2.5rem;
height: 2.5rem;
padding: 0;
border-radius: 50%;
}
aside > .cta span:not(.material-symbols-outlined) {
display: none;
}
aside > nav > a {
justify-content: center;
padding: var(--space-sm);
}
aside > nav > a span:not(.material-symbols-outlined) {
display: none;
}
.topnav { .topnav {
padding: 0 var(--space-sm); padding: 0 var(--space-sm);
gap: var(--space-xs);
} }
.topnav > .search { .topnav > nav > a span:not(.material-symbols-outlined) {
display: none; display: none;
} }
.topnav > .cta span:not(.material-symbols-outlined) {
display: none;
}
.topnav > .cta {
width: 2.25rem;
height: 2.25rem;
padding: 0;
border-radius: 50%;
justify-content: center;
}
.main-content { .main-content {
padding: var(--space-sm); padding: var(--space-sm);
} }
.topnav > h1 {
font-size: 1rem;
}
} }

View file

@ -329,6 +329,47 @@ function setActiveLink() {
} }
} }
function viewSpecialCake(btn) {
var tr = btn.closest('tr');
document.getElementById('sc-view-title').textContent = tr.getAttribute('data-sc-name');
document.getElementById('sc-view-name').textContent = tr.getAttribute('data-sc-name');
document.getElementById('sc-view-price').textContent = tr.getAttribute('data-sc-price') + ' PLN';
document.getElementById('sc-view-size').textContent = tr.getAttribute('data-sc-size') || '—';
var shapeVal = tr.getAttribute('data-sc-shape');
var shapeText = '';
if (shapeVal === 'round') shapeText = 'Okrągły';
else if (shapeVal === 'square') shapeText = 'Kwadrat';
else shapeText = shapeVal || '—';
document.getElementById('sc-view-shape').textContent = shapeText;
document.getElementById('sc-view-flavour').textContent = tr.getAttribute('data-sc-flavour') || '—';
document.getElementById('sc-view-notes').textContent = tr.getAttribute('data-sc-notes') || '—';
document.getElementById('sc-view-client').textContent = tr.getAttribute('data-sc-client');
document.getElementById('sc-view-date').textContent = tr.getAttribute('data-sc-date');
var tagsStr = tr.getAttribute('data-sc-tags');
var tagsContainer = document.getElementById('sc-view-tags');
tagsContainer.innerHTML = '';
if (tagsStr) {
var tagNames = tagsStr.split(',');
for (var i = 0; i < tagNames.length; i++) {
if (tagNames[i].trim()) {
var tag = document.createElement('span');
tag.className = 'tag-chip sm';
tag.textContent = tagNames[i].trim();
tagsContainer.appendChild(tag);
}
}
document.getElementById('sc-view-tags-field').classList.remove('hide');
} else {
document.getElementById('sc-view-tags-field').classList.add('hide');
}
document.getElementById('sc-view-order-link').href = '/order/' + tr.getAttribute('data-sc-order-id');
document.getElementById('sc-view-overlay').classList.add('open');
}
function closeViewSpecialCake() {
document.getElementById('sc-view-overlay').classList.remove('open');
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
setActiveLink(); setActiveLink();
initOrderPage(); initOrderPage();

View file

@ -14,11 +14,26 @@ templ Layout(title string, main templ.Component) {
<script src="/static/order.js"></script> <script src="/static/order.js"></script>
</head> </head>
<body> <body>
<aside>
@Nav()
</aside>
<header class="topnav"> <header class="topnav">
<h1>{ title }</h1> <h1>{ title }</h1>
<nav>
<a href="/orders" 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" 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" 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>
<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>
</header> </header>
<main id="main"> <main id="main">
<div class="main-content"> <div class="main-content">
@ -32,6 +47,24 @@ templ Layout(title string, main templ.Component) {
templ MainContent(title string, main templ.Component) { templ MainContent(title string, main templ.Component) {
<header class="topnav" hx-swap-oob="true"> <header class="topnav" hx-swap-oob="true">
<h1>{ title }</h1> <h1>{ title }</h1>
<nav>
<a href="/orders" 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" 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" 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>
<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>
</header> </header>
<main id="main"> <main id="main">
<title hx-swap-oob="true">{ title } — Zamówienia ciast</title> <title hx-swap-oob="true">{ title } — Zamówienia ciast</title>

View file

@ -1,22 +0,0 @@
package templates
templ Nav() {
<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" 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" 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" 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>
}

View file

@ -6,8 +6,8 @@ import (
"git.bronku.xyz/bronku/cake-order-tracker/models" "git.bronku.xyz/bronku/cake-order-tracker/models"
) )
templ OrdersPage(first, last, today, tomorrow, special string, counts models.OrderCounts, cakeCounts []models.CakeCount, orders []models.Order) { templ OrdersPage(first, last, today, tomorrow, special string, counts models.OrderCounts, cakeCounts []models.CakeCount, specialCakes []models.SpecialCakeWithOrder, orders []models.Order) {
@Layout("Panel główny", OrdersMain(counts, cakeCounts, orders, first, last, special, today, tomorrow)) @Layout("Panel główny", OrdersMain(counts, cakeCounts, orders, specialCakes, first, last, special, today, tomorrow))
} }
templ DateForm(first, last, today, tomorrow, special string) { templ DateForm(first, last, today, tomorrow, special string) {
@ -27,7 +27,7 @@ templ DateForm(first, last, today, tomorrow, special string) {
</div> </div>
} }
templ OrdersMain(counts models.OrderCounts, cakeCounts []models.CakeCount, orders []models.Order, first, last, special, today, tomorrow string) { templ OrdersMain(counts models.OrderCounts, cakeCounts []models.CakeCount, orders []models.Order, specialCakes []models.SpecialCakeWithOrder, first, last, special, today, tomorrow string) {
@DateForm(first, last, today, tomorrow, special) @DateForm(first, last, today, tomorrow, special)
<div class="metric"> <div class="metric">
<span class="label"> <span class="label">
@ -46,11 +46,12 @@ templ OrdersMain(counts models.OrderCounts, cakeCounts []models.CakeCount, order
<span class="change">Do zrobienia</span> <span class="change">Do zrobienia</span>
</div> </div>
<div id="orders-results"> <div id="orders-results">
@OrdersContent(orders, cakeCounts, first, last, special) @OrdersContent(orders, cakeCounts, specialCakes, first, last, special)
</div> </div>
@SpecialCakeViewOverlay()
} }
templ OrdersContent(orders []models.Order, cakeCounts []models.CakeCount, first, last, special string) { templ OrdersContent(orders []models.Order, cakeCounts []models.CakeCount, specialCakes []models.SpecialCakeWithOrder, first, last, special string) {
<section class="card" id="orders-card"> <section class="card" id="orders-card">
<header> <header>
<h2> <h2>
@ -90,6 +91,7 @@ templ OrdersContent(orders []models.Order, cakeCounts []models.CakeCount, first,
</table> </table>
</section> </section>
@CakeBreakdown(cakeCounts) @CakeBreakdown(cakeCounts)
@SpecialCakesTable(specialCakes)
} }
templ OrderRow(order models.Order) { templ OrderRow(order models.Order) {
@ -156,6 +158,125 @@ templ OrderRow(order models.Order) {
</tr> </tr>
} }
templ SpecialCakesTable(specialCakes []models.SpecialCakeWithOrder) {
<section class="card" id="special-cakes-card">
<header>
<h2>
<span class="material-symbols-outlined">cake</span>
Torty specjalne
</h2>
</header>
<table class="data-table">
<thead>
<tr>
<th>Nazwa</th>
<th>Klient</th>
<th>Data odbioru</th>
<th class="text-right">Cena</th>
<th class="text-center"></th>
</tr>
</thead>
<tbody>
for _, sc := range specialCakes {
<tr data-sc-id={ strconv.Itoa(sc.ID) }
data-sc-name={ sc.Name }
data-sc-price={ sc.Price.String() }
data-sc-size={ sc.Size }
data-sc-shape={ sc.Shape }
data-sc-flavour={ sc.Flavour }
data-sc-notes={ sc.Notes }
data-sc-tags={ tagNames(sc.Tags) }
data-sc-order-id={ strconv.Itoa(sc.OrderID) }
data-sc-client={ sc.ClientName + " " + sc.ClientSurname }
data-sc-date={ sc.OrderDate.Format("2006-01-02") }>
<td>
<button type="button" class="link-btn" onclick="viewSpecialCake(this)">{ sc.Name }</button>
</td>
<td>{ sc.ClientName } { sc.ClientSurname }</td>
<td class="text-muted">{ sc.OrderDate.Format("2006-01-02") }</td>
<td class="text-right">{ sc.Price.String() } PLN</td>
<td class="text-center">
<a href={ templ.URL("/order/" + strconv.Itoa(sc.OrderID)) } class="btn-icon" title="Zobacz zamówienie">
<span class="material-symbols-outlined">receipt_long</span>
</a>
</td>
</tr>
}
if len(specialCakes) == 0 {
<tr>
<td colspan="5" class="empty-state">Brak tortów specjalnych w tym okresie</td>
</tr>
}
</tbody>
</table>
</section>
}
templ SpecialCakeViewOverlay() {
<div id="sc-view-overlay" class="overlay">
<div class="overlay-backdrop" onclick="closeViewSpecialCake()"></div>
<div class="overlay-panel overlay-panel--view">
<header>
<h2>
<span class="material-symbols-outlined">cake</span>
<span id="sc-view-title">Tort specjalny</span>
</h2>
<button type="button" class="close-btn" onclick="closeViewSpecialCake()">
<span class="material-symbols-outlined">close</span>
</button>
</header>
<div class="overlay-body">
<div class="view-field">
<label>Nazwa</label>
<span id="sc-view-name"></span>
</div>
<div class="view-field">
<label>Cena</label>
<span id="sc-view-price"></span>
</div>
<div class="view-field">
<label>Wielkość</label>
<span id="sc-view-size"></span>
</div>
<div class="view-field">
<label>Kształt</label>
<span id="sc-view-shape"></span>
</div>
<div class="view-field">
<label>Smak</label>
<span id="sc-view-flavour"></span>
</div>
<div class="view-field">
<label>Notatki</label>
<span id="sc-view-notes"></span>
</div>
<div class="view-field" id="sc-view-tags-field">
<label>Tagi</label>
<div id="sc-view-tags"></div>
</div>
<div class="view-field">
<label>Klient</label>
<span id="sc-view-client"></span>
</div>
<div class="view-field">
<label>Data odbioru</label>
<span id="sc-view-date"></span>
</div>
</div>
<footer>
<a href="" id="sc-view-order-link" class="btn btn-outline">
<span class="material-symbols-outlined">receipt_long</span>
Zobacz zamówienie
</a>
<button type="button" class="btn btn-primary" onclick="closeViewSpecialCake()">
<span class="material-symbols-outlined">close</span>
Zamknij
</button>
</footer>
</div>
</div>
}
templ CakeBreakdown(cakeCounts []models.CakeCount) { templ CakeBreakdown(cakeCounts []models.CakeCount) {
<section class="card" id="breakdown-card"> <section class="card" id="breakdown-card">
<header> <header>

View file

@ -3,7 +3,9 @@ package store
import ( import (
"database/sql" "database/sql"
"log" "log"
"time"
"git.bronku.xyz/bronku/cake-order-tracker/config"
"git.bronku.xyz/bronku/cake-order-tracker/models" "git.bronku.xyz/bronku/cake-order-tracker/models"
) )
@ -67,6 +69,44 @@ func (s *Store) saveSpecialCakeTags(tx *sql.Tx, specialCakeID int, tagIDs []int)
return nil return nil
} }
func (s *Store) GetSpecialCakesInRange(from, to time.Time) ([]models.SpecialCakeWithOrder, error) {
start, end := dateRangeParams(from, to)
rows, err := s.db.Query(`
SELECT sc.id, sc.customer_order, sc.name, sc.price, sc.size, sc.shape, sc.flavour, sc.notes,
co.name, co.surname, co.delivery_date
FROM special_cake sc
JOIN customer_order co ON sc.customer_order = co.id
WHERE co.status != 'picked_up' AND co.delivery_date >= ? AND co.delivery_date <= ?
ORDER BY co.delivery_date
`, start, end)
if err != nil {
return nil, err
}
defer rows.Close()
var out []models.SpecialCakeWithOrder
for rows.Next() {
var sw models.SpecialCakeWithOrder
var price int
var deliveryDate string
if err := rows.Scan(&sw.ID, &sw.OrderID, &sw.Name, &price, &sw.Size, &sw.Shape, &sw.Flavour, &sw.Notes,
&sw.ClientName, &sw.ClientSurname, &deliveryDate); err != nil {
return nil, err
}
sw.Price = models.Price(price)
sw.OrderDate, err = time.Parse(config.DateTimeFormat, deliveryDate)
if err != nil {
return nil, err
}
sw.Tags, err = s.GetSpecialCakeTags(sw.ID)
if err != nil {
return nil, err
}
out = append(out, sw)
}
return out, rows.Err()
}
func (s *Store) SaveSpecialCakes(tx *sql.Tx, orderID int, cakes []models.SpecialCake) error { func (s *Store) SaveSpecialCakes(tx *sql.Tx, orderID int, cakes []models.SpecialCake) error {
if _, err := tx.Exec("delete from special_cake where customer_order = ?;", orderID); err != nil { if _, err := tx.Exec("delete from special_cake where customer_order = ?;", orderID); err != nil {
return err return err