Added Meta Data to Orders and made the frontend english
This commit is contained in:
parent
8bc4691992
commit
929969d03a
@ -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"`
|
||||
|
||||
@ -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 @@
|
||||
</button>
|
||||
<div class="nav-children" x-show="expanded.orders">
|
||||
<template x-if="orders.length === 0 && !ordersLoading">
|
||||
<p class="nav-empty">Keine Orders</p>
|
||||
<p class="nav-empty">No orders</p>
|
||||
</template>
|
||||
<template x-for="o in orders" :key="o.id">
|
||||
<button type="button" class="nav-item"
|
||||
@ -440,7 +467,7 @@
|
||||
</template>
|
||||
<button type="button" class="nav-item add"
|
||||
:class="{ selected: section === 'orders' && !orderEditingId }"
|
||||
@click="newOrder()">+ Neu</button>
|
||||
@click="newOrder()">+ New</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@ -630,57 +657,72 @@
|
||||
<!-- Orders -->
|
||||
<div x-show="section === 'orders'">
|
||||
<div class="main-header">
|
||||
<h2 x-text="orderEditingId ? 'Order bearbeiten' : 'Neue Order'"></h2>
|
||||
<p class="muted" x-show="!orderEditingId">Order anlegen, danach Bilder hochladen.</p>
|
||||
<p class="muted" x-show="orderEditingId">Beliebig viele Bilder pro Order (PNG, JPEG, WebP, GIF).</p>
|
||||
<h2 x-text="orderEditingId ? 'Edit order' : 'New order'"></h2>
|
||||
<p class="muted" x-show="!orderEditingId">Create an order, then upload images.</p>
|
||||
<p class="muted" x-show="orderEditingId">Any number of images per order (PNG, JPEG, WebP, GIF).</p>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<form @submit.prevent="saveOrder()">
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label>Name (optional)</label>
|
||||
<input type="text" x-model="orderForm.name" placeholder="z.B. Kunde Müller">
|
||||
<input type="text" x-model="orderForm.name" placeholder="e.g. Customer Smith">
|
||||
</div>
|
||||
<div>
|
||||
<label>Reference code</label>
|
||||
<input type="text" x-model="orderForm.ref" placeholder="e.g. SHOP-10482" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check-row">
|
||||
<label class="form-check">
|
||||
<input type="checkbox" x-model="orderForm.printed">
|
||||
Printed
|
||||
</label>
|
||||
<label class="form-check">
|
||||
<input type="checkbox" x-model="orderForm.shipped">
|
||||
Shipped
|
||||
</label>
|
||||
</div>
|
||||
<p class="ref-code" x-show="orderForm.ref" x-text="'Ref: ' + orderForm.ref"></p>
|
||||
<div class="btn-row">
|
||||
<button type="submit" :disabled="orderSaving">
|
||||
<span x-text="orderSaving ? 'Speichere…' : (orderEditingId ? 'Speichern' : 'Anlegen')"></span>
|
||||
<span x-text="orderSaving ? 'Saving…' : (orderEditingId ? 'Save' : 'Create')"></span>
|
||||
</button>
|
||||
<button type="button" class="danger" x-show="orderEditingId" @click="deleteOrder(orderEditingId)">Löschen</button>
|
||||
<button type="button" class="danger" x-show="orderEditingId" @click="deleteOrder(orderEditingId)">Delete</button>
|
||||
</div>
|
||||
<p class="msg-err" x-show="orderError" x-text="orderError"></p>
|
||||
<p class="msg-ok" x-show="orderSuccess" x-text="orderSuccess"></p>
|
||||
</form>
|
||||
|
||||
<div x-show="orderEditingId" style="margin-top: 1.5rem;">
|
||||
<label>Bilder hochladen</label>
|
||||
<label>Upload images</label>
|
||||
<input type="file" class="file-input" accept="image/*" multiple
|
||||
:disabled="orderImageUploading || orderLoading"
|
||||
@change="uploadOrderImages($event)">
|
||||
<p class="muted" x-show="orderImageUploading">Lade hoch…</p>
|
||||
<p class="muted" x-show="orderLoading">Lade Bilder…</p>
|
||||
<p class="muted" x-show="orderImageUploading">Uploading…</p>
|
||||
<p class="muted" x-show="orderLoading">Loading images…</p>
|
||||
|
||||
<div class="order-images" x-show="orderImages.length > 0">
|
||||
<template x-for="img in orderImages" :key="img.id">
|
||||
<div class="order-image-card">
|
||||
<button type="button" class="order-image-thumb"
|
||||
@click="openOrderImage(img)"
|
||||
:title="(img.original_name || img.filename) + ' — Klicken für Vollbild'">
|
||||
:title="(img.original_name || img.filename) + ' — click for full size'">
|
||||
<img :src="orderImageUrl(orderEditingId, img.id)"
|
||||
:alt="img.original_name || 'Bild'"
|
||||
:alt="img.original_name || 'Image'"
|
||||
loading="lazy"
|
||||
@error="onOrderImageError($event, img)">
|
||||
</button>
|
||||
<p class="order-image-meta" x-text="img.original_name || img.filename"></p>
|
||||
<div class="order-image-actions">
|
||||
<button type="button" class="secondary" @click="openOrderImage(img)">Vergrößern</button>
|
||||
<button type="button" class="danger" @click="deleteOrderImage(img.id)">Entfernen</button>
|
||||
<button type="button" class="secondary" @click="openOrderImage(img)">Enlarge</button>
|
||||
<button type="button" class="danger" @click="deleteOrderImage(img.id)">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<p class="muted" x-show="orderImages.length === 0 && !orderImageUploading && !orderLoading" style="margin-top: 1rem;">
|
||||
Noch keine Bilder — Dateien oben auswählen.
|
||||
No images yet — choose files above.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -694,7 +736,7 @@
|
||||
<div @click.stop style="display: contents;">
|
||||
<img :src="orderImageUrl(orderLightbox.orderId, orderLightbox.imageId)" :alt="orderLightbox.label">
|
||||
<p class="order-lightbox-caption" x-text="orderLightbox.label"></p>
|
||||
<p class="order-lightbox-hint">Klicken oder Esc zum Schließen</p>
|
||||
<p class="order-lightbox-hint">Click or press Esc to close</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@ -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' });
|
||||
|
||||
@ -46,6 +46,18 @@ func listOrders(s *store.OrderStore) http.HandlerFunc {
|
||||
|
||||
type saveOrderRequest struct {
|
||||
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
|
||||
|
||||
@ -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{}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user