package main // Label designer — the Wails boundary for internal/labels. // // The designer edits two things: STOCKS (the physical roll in the printer, // geometry in mm) and TEMPLATES (one design per label kind: the QSO label glued // on a card, the address label for the envelope). Printing — a later module — // will ask for the default template of each kind and hand the rasterised pages // to a PDF; nothing here prints. import ( "fmt" "strings" "hamlog/internal/applog" "hamlog/internal/labels" "hamlog/internal/qso" ) // LabelTemplateInfo is one row of the designer's template list. type LabelTemplateInfo struct { ID int64 `json:"id"` Name string `json:"name"` Kind string `json:"kind"` StockID int64 `json:"stock_id"` ProfileID *int64 `json:"profile_id,omitempty"` IsDefault bool `json:"is_default"` UpdatedAt string `json:"updated_at"` } // LabelListStocks returns every label stock, seeding the builtin Brother rolls // on first use. func (a *App) LabelListStocks() ([]labels.Stock, error) { if a.labelRepo == nil { return nil, fmt.Errorf("db not initialized") } if err := a.labelRepo.SeedStocks(a.ctx); err != nil { applog.Printf("labels: seeding stocks failed: %v", err) } return a.labelRepo.Stocks(a.ctx) } // LabelSaveStock creates or updates one stock and returns its id. func (a *App) LabelSaveStock(s labels.Stock) (int64, error) { if a.labelRepo == nil { return 0, fmt.Errorf("db not initialized") } if err := a.labelRepo.SaveStock(a.ctx, &s); err != nil { return 0, err } return s.ID, nil } // LabelDeleteStock removes a stock; designs pointing at it keep their content. func (a *App) LabelDeleteStock(id int64) error { if a.labelRepo == nil { return fmt.Errorf("db not initialized") } return a.labelRepo.DeleteStock(a.ctx, id) } // LabelListTemplates lists the designs visible to the active profile. func (a *App) LabelListTemplates() ([]LabelTemplateInfo, error) { if a.labelRepo == nil { return nil, fmt.Errorf("db not initialized") } var recs []labels.Record var err error if p, e := a.profiles.Active(a.ctx); e == nil { recs, err = a.labelRepo.ListFor(a.ctx, p.ID) } else { recs, err = a.labelRepo.List(a.ctx) } if err != nil { return nil, err } out := make([]LabelTemplateInfo, 0, len(recs)) for _, r := range recs { info := LabelTemplateInfo{ ID: r.ID, Name: r.Name, Kind: r.Kind, ProfileID: r.ProfileID, IsDefault: r.IsDefault, UpdatedAt: r.UpdatedAt.Format("2006-01-02 15:04"), } if r.StockID != nil { info.StockID = *r.StockID } out = append(out, info) } return out, nil } // LabelGetTemplate returns one stored design document (JSON). func (a *App) LabelGetTemplate(id int64) (string, error) { if a.labelRepo == nil { return "", fmt.Errorf("db not initialized") } rec, err := a.labelRepo.Get(a.ctx, id) if err != nil { return "", err } return rec.JSON, nil } // LabelSaveTemplate validates and stores a design; id 0 creates. Returns the id. func (a *App) LabelSaveTemplate(id int64, name string, doc string, forActiveProfile bool) (int64, error) { if a.labelRepo == nil { return 0, fmt.Errorf("db not initialized") } name = strings.TrimSpace(name) if name == "" { return 0, fmt.Errorf("template name required") } t, err := labels.Parse([]byte(doc)) if err != nil { return 0, err } if err := labels.Validate(t); err != nil { return 0, err } rec := labels.Record{ID: id, Name: name, Kind: t.Kind, JSON: doc} if t.StockID != 0 { sid := t.StockID rec.StockID = &sid } if forActiveProfile { if p, err := a.profiles.Active(a.ctx); err == nil { rec.ProfileID = &p.ID } } if err := a.labelRepo.Save(a.ctx, &rec); err != nil { return 0, err } applog.Printf("labels: template %q (%s) saved (id %d)", name, t.Kind, rec.ID) return rec.ID, nil } // LabelDeleteTemplate removes a design. func (a *App) LabelDeleteTemplate(id int64) error { if a.labelRepo == nil { return fmt.Errorf("db not initialized") } return a.labelRepo.Delete(a.ctx, id) } // LabelSetDefaultTemplate marks a design as the default for its kind. func (a *App) LabelSetDefaultTemplate(id int64) error { if a.labelRepo == nil { return fmt.Errorf("db not initialized") } return a.labelRepo.SetDefault(a.ctx, id) } // LabelSampleQSO is one row of preview data for the designer's QSO table. type LabelSampleQSO struct { Callsign string `json:"callsign"` QSODate string `json:"qso_date"` // YYYY-MM-DD TimeOn string `json:"time_on"` // HH:MM Band string `json:"band"` FreqMHz string `json:"freq"` Mode string `json:"mode"` RSTSent string `json:"rst_sent"` RSTRcvd string `json:"rst_rcvd"` Name string `json:"name"` QTH string `json:"qth"` Country string `json:"country"` } // LabelSampleQSOs returns the last few real contacts for the designer's live // preview — real data shows a too-narrow column immediately ("14074.0" does not // fit where "7.1" did). Falls back to plausible fakes on an empty log; the // preview must never be blank. func (a *App) LabelSampleQSOs(limit int) []LabelSampleQSO { if limit <= 0 || limit > 20 { limit = 4 } fake := []LabelSampleQSO{ {Callsign: "DL1ABC", QSODate: "2026-08-01", TimeOn: "14:32", Band: "20m", FreqMHz: "14.074", Mode: "FT8", RSTSent: "-08", RSTRcvd: "-12", Name: "Hans", QTH: "Berlin", Country: "Germany"}, {Callsign: "VK3XYZ", QSODate: "2026-08-02", TimeOn: "09:15", Band: "15m", FreqMHz: "21.245", Mode: "SSB", RSTSent: "59", RSTRcvd: "57", Name: "Bruce", QTH: "Melbourne", Country: "Australia"}, {Callsign: "JA1TOK", QSODate: "2026-08-03", TimeOn: "21:47", Band: "40m", FreqMHz: "7.012", Mode: "CW", RSTSent: "599", RSTRcvd: "579", Name: "Ken", QTH: "Tokyo", Country: "Japan"}, {Callsign: "W1AW", QSODate: "2026-08-04", TimeOn: "18:03", Band: "10m", FreqMHz: "28.480", Mode: "SSB", RSTSent: "59", RSTRcvd: "59", Name: "Hiram", QTH: "Newington", Country: "United States"}, } if a.qso == nil { return fake[:min(limit, len(fake))] } rows, err := a.qso.List(a.ctx, qso.ListFilter{Limit: limit}) if err != nil || len(rows) == 0 { return fake[:min(limit, len(fake))] } out := make([]LabelSampleQSO, 0, len(rows)) for _, q := range rows { s := LabelSampleQSO{ Callsign: q.Callsign, QSODate: q.QSODate.UTC().Format("2006-01-02"), TimeOn: q.QSODate.UTC().Format("15:04"), Band: q.Band, Mode: q.Mode, RSTSent: q.RSTSent, RSTRcvd: q.RSTRcvd, Name: q.Name, QTH: q.QTH, Country: q.Country, } if q.FreqHz != nil && *q.FreqHz > 0 { s.FreqMHz = fmt.Sprintf("%.3f", float64(*q.FreqHz)/1e6) } out = append(out, s) } return out }