+
+
@@ -470,6 +481,7 @@
plateSaving: false,
plateError: '',
plateSuccess: '',
+ plateEditingId: null,
plateForm: {
name: '',
width_mm: 300,
@@ -485,6 +497,7 @@
itemSaving: false,
itemError: '',
itemSuccess: '',
+ itemEditingId: null,
itemForm: {
name: '',
size_mm: 80,
@@ -494,6 +507,7 @@
},
configurations: [],
+ configEditingId: null,
configSaving: false,
configError: '',
configSuccess: '',
@@ -576,21 +590,62 @@
}
},
+ defaultPlateForm() {
+ return {
+ name: '',
+ width_mm: 300,
+ height_mm: 400,
+ margin_top_mm: 10,
+ margin_right_mm: 10,
+ margin_bottom_mm: 10,
+ margin_left_mm: 10,
+ };
+ },
+
+ editPlate(p) {
+ this.plateEditingId = p.id;
+ this.plateForm = {
+ name: p.name || '',
+ width_mm: p.width_mm,
+ height_mm: p.height_mm,
+ margin_top_mm: p.margin_top_mm,
+ margin_right_mm: p.margin_right_mm,
+ margin_bottom_mm: p.margin_bottom_mm,
+ margin_left_mm: p.margin_left_mm,
+ };
+ this.plateError = '';
+ this.plateSuccess = '';
+ },
+
+ cancelPlateEdit() {
+ this.plateEditingId = null;
+ this.plateForm = this.defaultPlateForm();
+ this.plateError = '';
+ this.plateSuccess = '';
+ },
+
async savePlate() {
this.plateSaving = true;
this.plateError = '';
this.plateSuccess = '';
+ const body = {
+ name: this.plateForm.name,
+ width_mm: Number(this.plateForm.width_mm),
+ height_mm: Number(this.plateForm.height_mm),
+ margin_top_mm: Number(this.plateForm.margin_top_mm) || 0,
+ margin_right_mm: Number(this.plateForm.margin_right_mm) || 0,
+ margin_bottom_mm: Number(this.plateForm.margin_bottom_mm) || 0,
+ margin_left_mm: Number(this.plateForm.margin_left_mm) || 0,
+ };
try {
- await this.apiJSON('POST', '/plates', {
- name: this.plateForm.name,
- width_mm: Number(this.plateForm.width_mm),
- height_mm: Number(this.plateForm.height_mm),
- margin_top_mm: Number(this.plateForm.margin_top_mm) || 0,
- margin_right_mm: Number(this.plateForm.margin_right_mm) || 0,
- margin_bottom_mm: Number(this.plateForm.margin_bottom_mm) || 0,
- margin_left_mm: Number(this.plateForm.margin_left_mm) || 0,
- });
- this.plateSuccess = 'Platte gespeichert.';
+ if (this.plateEditingId) {
+ await this.apiJSON('PUT', '/plates/' + this.plateEditingId, body);
+ this.plateSuccess = 'Platte aktualisiert.';
+ } else {
+ await this.apiJSON('POST', '/plates', body);
+ this.plateSuccess = 'Platte gespeichert.';
+ }
+ this.cancelPlateEdit();
await this.loadPlates();
this.refreshLayoutPreview();
} catch (e) {
@@ -644,6 +699,7 @@
throw new Error(text || res.statusText);
}
await Promise.all([this.loadPlates(), this.loadConfigurations()]);
+ if (this.plateEditingId === id) this.cancelPlateEdit();
if (this.configForm.plate_id === id) {
this.configForm.plate_id = '';
this.layoutPreview = null;
@@ -664,20 +720,59 @@
}
},
+ defaultItemForm() {
+ return {
+ name: '',
+ size_mm: 80,
+ bleed_mm: 2,
+ margin_mm: 5,
+ padding_mm: 3,
+ };
+ },
+
+ editItem(it) {
+ this.itemEditingId = it.id;
+ this.itemForm = {
+ name: it.name || '',
+ size_mm: it.spec.size_mm,
+ bleed_mm: it.spec.bleed_mm,
+ margin_mm: it.spec.margin_mm,
+ padding_mm: it.spec.padding_mm,
+ };
+ this.itemError = '';
+ this.itemSuccess = '';
+ },
+
+ cancelItemEdit() {
+ this.itemEditingId = null;
+ this.itemForm = this.defaultItemForm();
+ this.itemError = '';
+ this.itemSuccess = '';
+ },
+
async saveItem() {
this.itemSaving = true;
this.itemError = '';
this.itemSuccess = '';
+ const body = {
+ name: this.itemForm.name,
+ size_mm: Number(this.itemForm.size_mm),
+ bleed_mm: Number(this.itemForm.bleed_mm) || 0,
+ margin_mm: Number(this.itemForm.margin_mm) || 0,
+ padding_mm: Number(this.itemForm.padding_mm) || 0,
+ };
try {
- await this.apiJSON('POST', '/items', {
- name: this.itemForm.name,
- size_mm: Number(this.itemForm.size_mm),
- bleed_mm: Number(this.itemForm.bleed_mm) || 0,
- margin_mm: Number(this.itemForm.margin_mm) || 0,
- padding_mm: Number(this.itemForm.padding_mm) || 0,
- });
- this.itemSuccess = 'Item erzeugt.';
- this.itemForm.name = '';
+ if (this.itemEditingId) {
+ await this.apiJSON('PUT', '/items/' + this.itemEditingId, body);
+ this.itemSuccess = 'Item aktualisiert.';
+ } else {
+ if (!body.name) {
+ throw new Error('Name ist erforderlich');
+ }
+ await this.apiJSON('POST', '/items', body);
+ this.itemSuccess = 'Item erzeugt.';
+ }
+ this.cancelItemEdit();
await this.loadItems();
this.refreshLayoutPreview();
} catch (e) {
@@ -697,6 +792,7 @@
throw new Error(text || res.statusText);
}
await Promise.all([this.loadItems(), this.loadConfigurations()]);
+ if (this.itemEditingId === id) this.cancelItemEdit();
if (this.configForm.item_id === id) {
this.configForm.item_id = '';
this.layoutPreview = null;
@@ -714,18 +810,55 @@
}
},
+ defaultConfigForm() {
+ return {
+ name: '',
+ plate_id: '',
+ item_id: '',
+ spacing_mm: 2,
+ };
+ },
+
+ editConfiguration(c) {
+ this.configEditingId = c.id;
+ this.configForm = {
+ name: c.name || '',
+ plate_id: c.plate_id,
+ item_id: c.item_id,
+ spacing_mm: c.spacing_mm,
+ };
+ this.configError = '';
+ this.configSuccess = '';
+ this.refreshLayoutPreview();
+ },
+
+ cancelConfigEdit() {
+ this.configEditingId = null;
+ this.configForm = this.defaultConfigForm();
+ this.layoutPreview = null;
+ this.configError = '';
+ this.configSuccess = '';
+ },
+
async saveConfiguration() {
this.configSaving = true;
this.configError = '';
this.configSuccess = '';
+ const body = {
+ name: this.configForm.name,
+ plate_id: this.configForm.plate_id,
+ item_id: this.configForm.item_id,
+ spacing_mm: Number(this.configForm.spacing_mm) || 0,
+ };
try {
- await this.apiJSON('POST', '/configurations', {
- name: this.configForm.name,
- plate_id: this.configForm.plate_id,
- item_id: this.configForm.item_id,
- spacing_mm: Number(this.configForm.spacing_mm) || 0,
- });
- this.configSuccess = 'Konfiguration gespeichert.';
+ if (this.configEditingId) {
+ await this.apiJSON('PUT', '/configurations/' + this.configEditingId, body);
+ this.configSuccess = 'Konfiguration aktualisiert.';
+ } else {
+ await this.apiJSON('POST', '/configurations', body);
+ this.configSuccess = 'Konfiguration gespeichert.';
+ }
+ this.cancelConfigEdit();
await this.loadConfigurations();
} catch (e) {
this.configError = String(e.message || e);
@@ -784,6 +917,7 @@
throw new Error(text || res.statusText);
}
await this.loadConfigurations();
+ if (this.configEditingId === id) this.cancelConfigEdit();
} catch (e) {
this.configError = String(e.message || e);
}
diff --git a/internal/server/api/api.go b/internal/server/api/api.go
index 9882e27..d2c8bf5 100644
--- a/internal/server/api/api.go
+++ b/internal/server/api/api.go
@@ -22,9 +22,11 @@ func NewHandler() http.Handler {
mux.HandleFunc("GET /", root)
mux.HandleFunc("GET /plates", listPlates(plates))
mux.HandleFunc("POST /plates", savePlate(plates))
+ mux.HandleFunc("PUT /plates/{id}", updatePlate(plates))
mux.HandleFunc("DELETE /plates/{id}", deletePlate(plates))
mux.HandleFunc("GET /items", listItems(items))
mux.HandleFunc("POST /items", saveItem(items))
+ mux.HandleFunc("PUT /items/{id}", updateItem(items))
mux.HandleFunc("DELETE /items/{id}", deleteItem(items))
mux.HandleFunc("GET /items/{id}/svg", serveItemSVG(items))
registerConfigurationRoutes(mux, configs, plates, items)
@@ -34,7 +36,7 @@ func NewHandler() http.Handler {
func withCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
@@ -111,6 +113,40 @@ func savePlate(s *store.PlateStore) http.HandlerFunc {
}
}
+func updatePlate(s *store.PlateStore) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ id := r.PathValue("id")
+ if id == "" {
+ writeError(w, http.StatusBadRequest, errors.New("id required"))
+ return
+ }
+ var req savePlateRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ writeError(w, http.StatusBadRequest, err)
+ return
+ }
+ if req.WidthMM <= 0 || req.HeightMM <= 0 {
+ writeError(w, http.StatusBadRequest, errors.New("width_mm and height_mm must be positive"))
+ return
+ }
+ p := model.Plate{
+ Name: req.Name,
+ WidthMM: req.WidthMM,
+ HeightMM: req.HeightMM,
+ MarginTop: req.MarginTop,
+ MarginRight: req.MarginRight,
+ MarginBottom: req.MarginBottom,
+ MarginLeft: req.MarginLeft,
+ }
+ saved, err := s.Update(id, p)
+ if err != nil {
+ writeError(w, http.StatusNotFound, err)
+ return
+ }
+ writeJSON(w, http.StatusOK, saved)
+ }
+}
+
func deletePlate(s *store.PlateStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
@@ -170,6 +206,32 @@ func saveItem(s *store.ItemStore) http.HandlerFunc {
}
}
+func updateItem(s *store.ItemStore) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ id := r.PathValue("id")
+ if id == "" {
+ writeError(w, http.StatusBadRequest, errors.New("id required"))
+ return
+ }
+ var req saveItemRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ writeError(w, http.StatusBadRequest, err)
+ return
+ }
+ saved, err := s.Update(id, req.Name, model.ItemSpec{
+ SizeMM: req.SizeMM,
+ BleedMM: req.BleedMM,
+ MarginMM: req.MarginMM,
+ PaddingMM: req.PaddingMM,
+ })
+ if err != nil {
+ writeError(w, http.StatusBadRequest, err)
+ return
+ }
+ writeJSON(w, http.StatusOK, saved)
+ }
+}
+
func deleteItem(s *store.ItemStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
diff --git a/internal/server/api/configuration.go b/internal/server/api/configuration.go
index 5262501..11f4fcf 100644
--- a/internal/server/api/configuration.go
+++ b/internal/server/api/configuration.go
@@ -15,6 +15,7 @@ import (
func registerConfigurationRoutes(mux *http.ServeMux, configs *store.ConfigurationStore, plates *store.PlateStore, items *store.ItemStore) {
mux.HandleFunc("GET /configurations", listConfigurations(configs, plates, items))
mux.HandleFunc("POST /configurations", saveConfiguration(configs, plates, items))
+ mux.HandleFunc("PUT /configurations/{id}", updateConfiguration(configs, plates, items))
mux.HandleFunc("DELETE /configurations/{id}", deleteConfiguration(configs))
mux.HandleFunc("GET /configurations/{id}/preview", previewConfiguration(configs, plates, items))
mux.HandleFunc("GET /layout/preview", layoutPreview(plates, items))
@@ -99,6 +100,56 @@ func saveConfiguration(configs *store.ConfigurationStore, plates *store.PlateSto
}
}
+func updateConfiguration(configs *store.ConfigurationStore, plates *store.PlateStore, items *store.ItemStore) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ id := r.PathValue("id")
+ if id == "" {
+ writeError(w, http.StatusBadRequest, errors.New("id required"))
+ return
+ }
+ var req saveConfigurationRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ writeError(w, http.StatusBadRequest, err)
+ return
+ }
+ if req.PlateID == "" || req.ItemID == "" {
+ writeError(w, http.StatusBadRequest, errors.New("plate_id and item_id are required"))
+ return
+ }
+ if _, err := findPlate(plates, req.PlateID); err != nil {
+ writeError(w, http.StatusBadRequest, err)
+ return
+ }
+ if _, err := items.Get(req.ItemID); err != nil {
+ writeError(w, http.StatusBadRequest, err)
+ return
+ }
+ if req.SpacingMM < 0 {
+ writeError(w, http.StatusBadRequest, errors.New("spacing_mm must be non-negative"))
+ return
+ }
+
+ c := model.Configuration{
+ Name: req.Name,
+ PlateID: req.PlateID,
+ ItemID: req.ItemID,
+ SpacingMM: req.SpacingMM,
+ }
+ saved, err := configs.Update(id, c)
+ if err != nil {
+ writeError(w, http.StatusNotFound, err)
+ return
+ }
+
+ resp := configurationResponseFor(saved, plates, items)
+ if resp.PreviewError != "" {
+ writeError(w, http.StatusInternalServerError, errors.New(resp.PreviewError))
+ return
+ }
+ writeJSON(w, http.StatusOK, resp)
+ }
+}
+
func deleteConfiguration(configs *store.ConfigurationStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
diff --git a/internal/store/configuration.go b/internal/store/configuration.go
index f809c89..9a4fa70 100644
--- a/internal/store/configuration.go
+++ b/internal/store/configuration.go
@@ -58,6 +58,21 @@ func (s *ConfigurationStore) Save(c model.Configuration) (model.Configuration, e
return c, nil
}
+// Update replaces an existing configuration by ID (preserves created_at).
+func (s *ConfigurationStore) Update(id string, c model.Configuration) (model.Configuration, error) {
+ path := filepath.Join(s.dir, id+".json")
+ existing, err := readJSON[model.Configuration](path)
+ if err != nil {
+ return model.Configuration{}, fmt.Errorf("configuration not found: %s", id)
+ }
+ c.ID = existing.ID
+ c.CreatedAt = existing.CreatedAt
+ if err := writeJSON(path, c); err != nil {
+ return model.Configuration{}, fmt.Errorf("write configuration: %w", err)
+ }
+ return c, nil
+}
+
// Delete removes a configuration by ID.
func (s *ConfigurationStore) Delete(id string) error {
path := filepath.Join(s.dir, id+".json")
diff --git a/internal/store/item.go b/internal/store/item.go
index 9ee6508..f9d4e5f 100644
--- a/internal/store/item.go
+++ b/internal/store/item.go
@@ -65,6 +65,30 @@ func (s *ItemStore) Create(name string, spec model.ItemSpec) (model.Item, error)
return svgtemplate.WriteMeta(basename, spec, displayName)
}
+// Update regenerates the SVG and metadata for an existing item (preserves id, svg filename, created_at).
+func (s *ItemStore) Update(id string, name string, spec model.ItemSpec) (model.Item, error) {
+ if spec.SizeMM <= 0 {
+ return model.Item{}, fmt.Errorf("size_mm must be positive")
+ }
+ item, err := s.Get(id)
+ if err != nil {
+ return model.Item{}, err
+ }
+ data := svgtemplate.Build(spec.SizeMM, spec.BleedMM, spec.MarginMM, spec.PaddingMM)
+ if err := svgtemplate.WriteFile(item.SVGTemplate, data); err != nil {
+ return model.Item{}, fmt.Errorf("write svg: %w", err)
+ }
+ if name != "" {
+ item.Name = name
+ }
+ item.Spec = spec
+ metaPath := filepath.Join(s.dir, model.MetaFilename(item.SVGTemplate))
+ if err := writeJSON(metaPath, item); err != nil {
+ return model.Item{}, fmt.Errorf("write meta: %w", err)
+ }
+ return item, nil
+}
+
// Delete removes an item's SVG and metadata by ID.
func (s *ItemStore) Delete(id string) error {
item, err := s.Get(id)
diff --git a/internal/store/plate.go b/internal/store/plate.go
index 21e46d3..eb61fc5 100644
--- a/internal/store/plate.go
+++ b/internal/store/plate.go
@@ -49,6 +49,21 @@ func (s *PlateStore) Save(p model.Plate) (model.Plate, error) {
return p, nil
}
+// Update replaces an existing plate by ID (preserves created_at).
+func (s *PlateStore) Update(id string, p model.Plate) (model.Plate, error) {
+ path := filepath.Join(s.dir, id+".json")
+ existing, err := readJSON[model.Plate](path)
+ if err != nil {
+ return model.Plate{}, fmt.Errorf("plate not found: %s", id)
+ }
+ p.ID = existing.ID
+ p.CreatedAt = existing.CreatedAt
+ if err := writeJSON(path, p); err != nil {
+ return model.Plate{}, fmt.Errorf("write plate: %w", err)
+ }
+ return p, nil
+}
+
// Delete removes a plate by ID.
func (s *PlateStore) Delete(id string) error {
path := filepath.Join(s.dir, id+".json")