package api import ( "encoding/json" "errors" "net/http" "strconv" "time" "printer.backend/internal/layout" "printer.backend/internal/model" "printer.backend/internal/store" ) 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)) registerPDFRoutes(mux, configs, plates, items) } type configurationResponse struct { model.Configuration Preview *model.LayoutPreview `json:"preview,omitempty"` PreviewError string `json:"preview_error,omitempty"` } func listConfigurations(configs *store.ConfigurationStore, plates *store.PlateStore, items *store.ItemStore) http.HandlerFunc { return func(w http.ResponseWriter, _ *http.Request) { list, err := configs.List() if err != nil { writeError(w, http.StatusInternalServerError, err) return } if list == nil { list = []model.Configuration{} } out := make([]configurationResponse, 0, len(list)) for _, c := range list { out = append(out, configurationResponseFor(c, plates, items)) } writeJSON(w, http.StatusOK, out) } } type saveConfigurationRequest struct { Name string `json:"name"` PlateID string `json:"plate_id"` ItemID string `json:"item_id"` SpacingMM float64 `json:"spacing_mm"` } func saveConfiguration(configs *store.ConfigurationStore, plates *store.PlateStore, items *store.ItemStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { 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, CreatedAt: time.Now().UTC(), } saved, err := configs.Save(c) if err != nil { writeError(w, http.StatusInternalServerError, err) return } resp := configurationResponseFor(saved, plates, items) if resp.PreviewError != "" { writeError(w, http.StatusInternalServerError, errors.New(resp.PreviewError)) return } writeJSON(w, http.StatusCreated, resp) } } 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") if id == "" { writeError(w, http.StatusBadRequest, errors.New("id required")) return } if err := configs.Delete(id); err != nil { writeError(w, http.StatusNotFound, err) return } w.WriteHeader(http.StatusNoContent) } } func previewConfiguration(configs *store.ConfigurationStore, plates *store.PlateStore, items *store.ItemStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") c, err := configs.Get(id) if err != nil { writeError(w, http.StatusNotFound, err) return } preview, err := buildPreview(plates, items, c.PlateID, c.ItemID, c.SpacingMM) if err != nil { writeError(w, http.StatusBadRequest, err) return } preview.PlateID = c.PlateID preview.ItemID = c.ItemID writeJSON(w, http.StatusOK, preview) } } func layoutPreview(plates *store.PlateStore, items *store.ItemStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { plateID := r.URL.Query().Get("plate_id") itemID := r.URL.Query().Get("item_id") if plateID == "" || itemID == "" { writeError(w, http.StatusBadRequest, errors.New("plate_id and item_id query params are required")) return } spacing, err := parseSpacing(r.URL.Query().Get("spacing_mm")) if err != nil { writeError(w, http.StatusBadRequest, err) return } preview, err := buildPreview(plates, items, plateID, itemID, spacing) if err != nil { writeError(w, http.StatusBadRequest, err) return } preview.PlateID = plateID preview.ItemID = itemID writeJSON(w, http.StatusOK, preview) } } func configurationResponseFor(c model.Configuration, plates *store.PlateStore, items *store.ItemStore) configurationResponse { resp := configurationResponse{Configuration: c} preview, err := buildPreview(plates, items, c.PlateID, c.ItemID, c.SpacingMM) if err != nil { resp.PreviewError = err.Error() return resp } preview.PlateID = c.PlateID preview.ItemID = c.ItemID resp.Preview = &preview return resp } func buildPreview(plates *store.PlateStore, items *store.ItemStore, plateID, itemID string, spacingMM float64) (model.LayoutPreview, error) { plate, err := findPlate(plates, plateID) if err != nil { return model.LayoutPreview{}, err } item, err := items.Get(itemID) if err != nil { return model.LayoutPreview{}, err } return layout.Pack(plate, item.Spec, spacingMM), nil } func findPlate(plates *store.PlateStore, id string) (model.Plate, error) { list, err := plates.List() if err != nil { return model.Plate{}, err } plate, err := storeFindPlate(list, id) if err != nil { return model.Plate{}, errors.New("plate not found: " + id) } return plate, nil } func storeFindPlate(list []model.Plate, id string) (model.Plate, error) { for _, p := range list { if p.ID == id { return p, nil } } return model.Plate{}, errors.New("not found") } func parseSpacing(s string) (float64, error) { if s == "" { return 0, nil } v, err := strconv.ParseFloat(s, 64) if err != nil { return 0, errors.New("invalid spacing_mm") } if v < 0 { return 0, errors.New("spacing_mm must be non-negative") } return v, nil }