From 929969d03a6e933df59c13cdfe93a63a964ed83f Mon Sep 17 00:00:00 2001 From: simon Date: Tue, 26 May 2026 17:53:30 +0200 Subject: [PATCH] Added Meta Data to Orders and made the frontend english --- internal/model/order.go | 3 + internal/server/admin/static/index.html | 123 ++++++++++++++++++------ internal/server/api/order.go | 18 +++- internal/store/order.go | 53 ++++++++-- 4 files changed, 156 insertions(+), 41 deletions(-) diff --git a/internal/model/order.go b/internal/model/order.go index 9fd35da..2df21e8 100644 --- a/internal/model/order.go +++ b/internal/model/order.go @@ -15,6 +15,9 @@ type OrderImage struct { type Order struct { ID string `json:"id"` Name string `json:"name,omitempty"` + Printed bool `json:"printed"` + Shipped bool `json:"shipped"` + Ref string `json:"ref,omitempty"` Images []OrderImage `json:"images"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` diff --git a/internal/server/admin/static/index.html b/internal/server/admin/static/index.html index 0ecd151..af3f8b9 100644 --- a/internal/server/admin/static/index.html +++ b/internal/server/admin/static/index.html @@ -331,6 +331,33 @@ padding: 0.5rem; font-size: 0.82rem; } + .form-check-row { + display: flex; + flex-wrap: wrap; + gap: 1rem 1.5rem; + margin-top: 0.75rem; + } + .form-check { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + color: var(--text); + cursor: pointer; + margin-top: 0; + } + .form-check input[type="checkbox"] { + width: auto; + margin: 0; + accent-color: var(--accent); + } + .ref-code { + margin-top: 0.35rem; + font-size: 0.82rem; + font-family: ui-monospace, monospace; + color: var(--muted); + word-break: break-all; + } @media (max-width: 720px) { .app { flex-direction: column; } .sidebar { @@ -430,7 +457,7 @@ @@ -765,7 +807,7 @@ orderSuccess: '', orderEditingId: null, orderImages: [], - orderForm: { name: '' }, + orderForm: { name: '', printed: false, shipped: false, ref: '' }, orderLoading: false, orderImageVersion: 0, orderLightbox: null, @@ -837,9 +879,23 @@ }, sidebarOrderLabel(o) { - const n = o.name || ('Order ' + o.id.slice(0, 8)); + const n = o.name || o.ref || ('Order ' + o.id.slice(0, 8)); + const parts = []; + if (o.name && o.ref) parts.push(o.ref); + if (o.printed) parts.push('printed'); + if (o.shipped) parts.push('shipped'); const count = (o.images || []).length; - return n + (count ? ' · ' + count + ' Bild' + (count === 1 ? '' : 'er') : ''); + if (count) parts.push(count + ' img' + (count === 1 ? '' : 's')); + return parts.length ? n + ' · ' + parts.join(' · ') : n; + }, + + orderFormFrom(o) { + return { + name: o.name || '', + printed: !!o.printed, + shipped: !!o.shipped, + ref: o.ref || o.ref_link || '', + }; }, apiUrl(path) { @@ -859,7 +915,7 @@ this.orderLightbox = { orderId: this.orderEditingId, imageId: img.id, - label: img.original_name || img.filename || 'Bild', + label: img.original_name || img.filename || 'Image', }; }, @@ -1326,14 +1382,14 @@ async editOrder(o) { this.orderEditingId = o.id; - this.orderForm = { name: o.name || '' }; + this.orderForm = this.orderFormFrom(o); this.orderImages = o.images ? [...o.images] : []; this.orderError = ''; this.orderSuccess = ''; this.orderLoading = true; try { const fresh = await this.apiJSON('GET', '/orders/' + o.id); - this.orderForm = { name: fresh.name || '' }; + this.orderForm = this.orderFormFrom(fresh); this.orderImages = fresh.images ? [...fresh.images] : []; this.orderImageVersion++; } catch (e) { @@ -1345,7 +1401,7 @@ cancelOrderEdit() { this.orderEditingId = null; - this.orderForm = { name: '' }; + this.orderForm = { name: '', printed: false, shipped: false, ref: '' }; this.orderImages = []; this.orderLightbox = null; this.orderError = ''; @@ -1356,15 +1412,20 @@ this.orderSaving = true; this.orderError = ''; this.orderSuccess = ''; - const body = { name: this.orderForm.name }; + const body = { + name: this.orderForm.name, + printed: !!this.orderForm.printed, + shipped: !!this.orderForm.shipped, + ref: (this.orderForm.ref || '').trim(), + }; try { let saved; if (this.orderEditingId) { saved = await this.apiJSON('PUT', '/orders/' + this.orderEditingId, body); - this.orderSuccess = 'Order aktualisiert.'; + this.orderSuccess = 'Order updated.'; } else { saved = await this.apiJSON('POST', '/orders', body); - this.orderSuccess = 'Order angelegt.'; + this.orderSuccess = 'Order created.'; } const keepId = saved?.id || this.orderEditingId; await this.loadOrders(); @@ -1419,7 +1480,7 @@ if (o) await this.editOrder(o); } const n = (result?.added || []).length; - this.orderSuccess = n === 1 ? '1 Bild hochgeladen.' : n + ' Bilder hochgeladen.'; + this.orderSuccess = n === 1 ? '1 image uploaded.' : n + ' images uploaded.'; } catch (e) { this.orderError = String(e.message || e); } finally { @@ -1430,7 +1491,7 @@ async deleteOrderImage(imageId) { if (!this.orderEditingId) return; - if (!confirm('Bild wirklich entfernen?')) return; + if (!confirm('Remove this image?')) return; this.orderError = ''; try { const res = await fetch( @@ -1445,14 +1506,14 @@ await this.loadOrders(); const o = this.orders.find(x => x.id === this.orderEditingId); if (o) await this.editOrder(o); - this.orderSuccess = 'Bild entfernt.'; + this.orderSuccess = 'Image removed.'; } catch (e) { this.orderError = String(e.message || e); } }, async deleteOrder(id) { - if (!confirm('Order inkl. aller Bilder wirklich löschen?')) return; + if (!confirm('Delete this order and all images?')) return; this.orderError = ''; try { const res = await fetch(this.apiUrl('/orders/' + id), { method: 'DELETE' }); diff --git a/internal/server/api/order.go b/internal/server/api/order.go index fffa075..2dd58fa 100644 --- a/internal/server/api/order.go +++ b/internal/server/api/order.go @@ -45,7 +45,19 @@ func listOrders(s *store.OrderStore) http.HandlerFunc { } type saveOrderRequest struct { - Name string `json:"name"` + Name string `json:"name"` + Printed bool `json:"printed"` + Shipped bool `json:"shipped"` + Ref string `json:"ref"` +} + +func orderFromRequest(req saveOrderRequest) model.Order { + return model.Order{ + Name: req.Name, + Printed: req.Printed, + Shipped: req.Shipped, + Ref: req.Ref, + } } func saveOrder(s *store.OrderStore) http.HandlerFunc { @@ -55,7 +67,7 @@ func saveOrder(s *store.OrderStore) http.HandlerFunc { writeError(w, http.StatusBadRequest, err) return } - saved, err := s.Save(model.Order{Name: req.Name}) + saved, err := s.Save(orderFromRequest(req)) if err != nil { writeError(w, http.StatusInternalServerError, err) return @@ -84,7 +96,7 @@ func updateOrder(s *store.OrderStore) http.HandlerFunc { writeError(w, http.StatusBadRequest, err) return } - saved, err := s.Update(id, model.Order{Name: req.Name}) + saved, err := s.Update(id, orderFromRequest(req)) if err != nil { writeError(w, http.StatusNotFound, err) return diff --git a/internal/store/order.go b/internal/store/order.go index 3dbc8b1..5393267 100644 --- a/internal/store/order.go +++ b/internal/store/order.go @@ -1,10 +1,12 @@ package store import ( + "encoding/json" "fmt" "mime" "os" "path/filepath" + "sort" "strings" "time" @@ -28,23 +30,60 @@ func NewOrderStore(dir string) *OrderStore { // List returns all orders sorted by creation time (newest first). func (s *OrderStore) List() ([]model.Order, error) { - return listFromDir(s.dir, - func(name string) bool { return filepath.Ext(name) == ".json" }, - "orders dir", - func(o model.Order) time.Time { return o.CreatedAt }, - ) + if err := ensureDir(s.dir); err != nil { + return nil, err + } + entries, err := os.ReadDir(s.dir) + if err != nil { + return nil, fmt.Errorf("read orders dir: %w", err) + } + + var out []model.Order + for _, e := range entries { + if e.IsDir() || filepath.Ext(e.Name()) != ".json" { + continue + } + o, err := readOrderFile(filepath.Join(s.dir, e.Name())) + if err != nil { + return nil, err + } + out = append(out, o) + } + + sort.Slice(out, func(i, j int) bool { + return out[i].CreatedAt.After(out[j].CreatedAt) + }) + return out, nil } // Get returns an order by ID. func (s *OrderStore) Get(id string) (model.Order, error) { - path := s.metaPath(id) - o, err := readJSON[model.Order](path) + o, err := readOrderFile(s.metaPath(id)) if err != nil { if os.IsNotExist(err) { return model.Order{}, fmt.Errorf("order not found: %s", id) } return model.Order{}, err } + return o, nil +} + +func readOrderFile(path string) (model.Order, error) { + var aux struct { + model.Order + RefLink string `json:"ref_link,omitempty"` + } + data, err := os.ReadFile(path) + if err != nil { + return model.Order{}, err + } + if err := json.Unmarshal(data, &aux); err != nil { + return model.Order{}, err + } + o := aux.Order + if o.Ref == "" && aux.RefLink != "" { + o.Ref = aux.RefLink + } if o.Images == nil { o.Images = []model.OrderImage{} }