From 9aafbb94ec66c2c0b680c6008fb9a370c1dbec64 Mon Sep 17 00:00:00 2001 From: bronkuu Date: Mon, 15 Jun 2026 18:49:21 +0200 Subject: [PATCH] tags and special cakes: models, migrations, store layer Add Tag, SpecialCake, Cake.Tags, Order.SpecialCakes types. Add migrations for tag, cake_tag, special_cake, special_cake_tag tables. Add store methods: GetTags, GetCakeTags, SaveCakeTags, CreateTag, GetSpecialCakes, GetSpecialCakeTags, SaveSpecialCakes, saveSpecialCakeTags. Load tags in GetCake/GetCakes, cascade tag saves in SaveCake. Remove debug log calls from ParsePrice. --- models/data.go | 88 +++++++++++++++++++++++++++++++++----- models/price.go | 12 ++---- store/cake.go | 20 +++++++-- store/migrations/2.sql | 10 +++++ store/migrations/3.sql | 10 +++++ store/migrations/4.sql | 5 +++ store/special_cake.go | 97 ++++++++++++++++++++++++++++++++++++++++++ store/tag.go | 78 +++++++++++++++++++++++++++++++++ 8 files changed, 297 insertions(+), 23 deletions(-) create mode 100644 store/migrations/2.sql create mode 100644 store/migrations/3.sql create mode 100644 store/migrations/4.sql create mode 100644 store/special_cake.go create mode 100644 store/tag.go diff --git a/models/data.go b/models/data.go index 270d79c..a767190 100644 --- a/models/data.go +++ b/models/data.go @@ -1,25 +1,77 @@ package models -import "time" +import ( + "fmt" + "time" +) + +type Tag struct { + ID int + Name string +} type Cake struct { Name string ID int Price Price // increments of 0.01 Amount int + Tags []Tag +} + +type SpecialCake struct { + ID int + OrderID int + Name string + Price Price + Size string + Shape string + Flavour string + Notes string + Tags []Tag +} + +func (s SpecialCake) Detail() string { + parts := make([]string, 0, 4) + if s.Size != "" { + parts = append(parts, s.Size) + } + if s.Flavour != "" { + parts = append(parts, s.Flavour) + } + if s.Shape == "square" { + parts = append(parts, "kwadrat") + } else if s.Shape == "round" { + parts = append(parts, "okrągły") + } + if s.Notes != "" { + parts = append(parts, "notatka: "+s.Notes) + } + if len(parts) == 0 { + return "" + } + return fmt.Sprintf("(%s)", joinParts(parts)) +} + +func joinParts(parts []string) string { + out := parts[0] + for i := 1; i < len(parts); i++ { + out += ", " + parts[i] + } + return out } type Order struct { - ID int - Name string - Surname string - Phone string - Location string - Date time.Time - Accepted time.Time - Status string - Paid Price // increments of 0.01 - Cakes []Cake + ID int + Name string + Surname string + Phone string + Location string + Date time.Time + Accepted time.Time + Status string + Paid Price // increments of 0.01 + Cakes []Cake + SpecialCakes []SpecialCake } func (o *Order) Subtotal() Price { @@ -27,6 +79,9 @@ func (o *Order) Subtotal() Price { for _, e := range o.Cakes { out += e.Price * Price(e.Amount) } + for _, s := range o.SpecialCakes { + out += s.Price + } return out } @@ -37,3 +92,14 @@ func (o *Order) Total() Price { func (c *Cake) Total() Price { return Price(c.Amount) * c.Price } + +type OrderCounts struct { + Active int + DueToday int +} + +type CakeCount struct { + ID int + Name string + Amount int +} diff --git a/models/price.go b/models/price.go index d030034..92c3a08 100644 --- a/models/price.go +++ b/models/price.go @@ -2,7 +2,6 @@ package models import ( "fmt" - "log" "strconv" "strings" ) @@ -12,18 +11,14 @@ type Price int func (in Price) String() string { if in%100 == 0 { return strconv.Itoa(int(in) / 100) - } else { - price := strconv.Itoa(int(in)) - i := len(price) - 2 - price = price[:i] + "." + price[i:] - return price } + price := strconv.Itoa(int(in)) + i := len(price) - 2 + return price[:i] + "." + price[i:] } func ParsePrice(in string) (Price, error) { - log.Println(in) in = strings.ReplaceAll(in, ",", ".") - log.Println(in) if !strings.Contains(in, ".") { price, err := strconv.Atoi(in) return Price(price) * 100, err @@ -39,6 +34,5 @@ func ParsePrice(in string) (Price, error) { if len(after) < 1 { price *= 10 } - log.Println(price) return Price(price), err } diff --git a/store/cake.go b/store/cake.go index 5f125a5..cad80e3 100644 --- a/store/cake.go +++ b/store/cake.go @@ -18,7 +18,8 @@ func (s *Store) GetCake(id int) (models.Cake, error) { if err != nil { return models.Cake{}, errors.New("cake not found") } - return out, nil + out.Tags, err = s.GetCakeTags(id) + return out, err } func (s *Store) GetCakes() ([]models.Cake, error) { @@ -34,6 +35,11 @@ func (s *Store) GetCakes() ([]models.Cake, error) { if err := rows.Scan(&cake.ID, &cake.Name, &cake.Price); err != nil { return nil, err } + tags, err := s.GetCakeTags(cake.ID) + if err != nil { + return nil, err + } + cake.Tags = tags out = append(out, cake) } return out, rows.Err() @@ -47,7 +53,7 @@ func (s *Store) SaveCake(newCake models.Cake) (int, error) { return 0, err } log.Printf("UPDATE cake id=%d name=%q price=%d", newCake.ID, newCake.Name, newCake.Price) - return newCake.ID, nil + return newCake.ID, s.SaveCakeTags(newCake.ID, tagIDs(newCake.Tags)) } q := "insert into cake(name, price) values (?, ?);" @@ -62,5 +68,13 @@ func (s *Store) SaveCake(newCake models.Cake) (int, error) { } newCake.ID = int(id) log.Printf("INSERT cake id=%d name=%q price=%d", newCake.ID, newCake.Name, newCake.Price) - return newCake.ID, nil + return newCake.ID, s.SaveCakeTags(newCake.ID, tagIDs(newCake.Tags)) +} + +func tagIDs(tags []models.Tag) []int { + ids := make([]int, len(tags)) + for i, t := range tags { + ids[i] = t.ID + } + return ids } diff --git a/store/migrations/2.sql b/store/migrations/2.sql new file mode 100644 index 0000000..1241e8d --- /dev/null +++ b/store/migrations/2.sql @@ -0,0 +1,10 @@ +create table tag ( + id integer primary key autoincrement, + name text not null unique +); + +create table cake_tag ( + cake integer not null references cake(id) on delete cascade, + tag integer not null references tag(id) on delete cascade, + primary key (cake, tag) +); diff --git a/store/migrations/3.sql b/store/migrations/3.sql new file mode 100644 index 0000000..56ec04b --- /dev/null +++ b/store/migrations/3.sql @@ -0,0 +1,10 @@ +create table special_cake ( + id integer primary key autoincrement, + customer_order integer not null references customer_order(id) on delete cascade, + name text not null, + price integer not null, + size text not null default '', + shape text not null default '', + flavour text not null default '', + notes text not null default '' +); diff --git a/store/migrations/4.sql b/store/migrations/4.sql new file mode 100644 index 0000000..fdbb500 --- /dev/null +++ b/store/migrations/4.sql @@ -0,0 +1,5 @@ +create table special_cake_tag ( + special_cake integer not null references special_cake(id) on delete cascade, + tag integer not null references tag(id) on delete cascade, + primary key (special_cake, tag) +); diff --git a/store/special_cake.go b/store/special_cake.go new file mode 100644 index 0000000..9826587 --- /dev/null +++ b/store/special_cake.go @@ -0,0 +1,97 @@ +package store + +import ( + "database/sql" + "log" + + "git.bronku.xyz/bronku/cake-order-tracker/models" +) + +func (s *Store) GetSpecialCakeTags(specialCakeID int) ([]models.Tag, error) { + rows, err := s.db.Query(` + select t.id, t.name from tag t + join special_cake_tag sct on sct.tag = t.id + where sct.special_cake = ? + order by t.name + `, specialCakeID) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []models.Tag + for rows.Next() { + var t models.Tag + if err := rows.Scan(&t.ID, &t.Name); err != nil { + return nil, err + } + out = append(out, t) + } + return out, rows.Err() +} + +func (s *Store) GetSpecialCakes(orderID int) ([]models.SpecialCake, error) { + rows, err := s.db.Query("select id, customer_order, name, price, size, shape, flavour, notes from special_cake where customer_order = ?;", orderID) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []models.SpecialCake + for rows.Next() { + var sc models.SpecialCake + var price int + if err := rows.Scan(&sc.ID, &sc.OrderID, &sc.Name, &price, &sc.Size, &sc.Shape, &sc.Flavour, &sc.Notes); err != nil { + return nil, err + } + sc.Price = models.Price(price) + tags, err := s.GetSpecialCakeTags(sc.ID) + if err != nil { + return nil, err + } + sc.Tags = tags + out = append(out, sc) + } + return out, rows.Err() +} + +func (s *Store) saveSpecialCakeTags(tx *sql.Tx, specialCakeID int, tagIDs []int) error { + if _, err := tx.Exec("delete from special_cake_tag where special_cake = ?;", specialCakeID); err != nil { + return err + } + for _, tid := range tagIDs { + if _, err := tx.Exec("insert or ignore into special_cake_tag(special_cake, tag) values (?, ?);", specialCakeID, tid); err != nil { + return err + } + } + return nil +} + +func (s *Store) SaveSpecialCakes(tx *sql.Tx, orderID int, cakes []models.SpecialCake) error { + if _, err := tx.Exec("delete from special_cake where customer_order = ?;", orderID); err != nil { + return err + } + if len(cakes) == 0 { + return nil + } + q := "insert into special_cake(customer_order, name, price, size, shape, flavour, notes) values (?, ?, ?, ?, ?, ?, ?);" + for _, sc := range cakes { + log.Printf("INSERT special_cake order=%d name=%q price=%d", orderID, sc.Name, sc.Price) + res, err := tx.Exec(q, orderID, sc.Name, int(sc.Price), sc.Size, sc.Shape, sc.Flavour, sc.Notes) + if err != nil { + return err + } + id, err := res.LastInsertId() + if err != nil { + return err + } + tagIDs := make([]int, len(sc.Tags)) + for i, t := range sc.Tags { + tagIDs[i] = t.ID + } + if err := s.saveSpecialCakeTags(tx, int(id), tagIDs); err != nil { + return err + } + } + return nil +} diff --git a/store/tag.go b/store/tag.go new file mode 100644 index 0000000..0050336 --- /dev/null +++ b/store/tag.go @@ -0,0 +1,78 @@ +package store + +import ( + "git.bronku.xyz/bronku/cake-order-tracker/models" +) + +func (s *Store) GetTags() ([]models.Tag, error) { + rows, err := s.db.Query("select id, name from tag order by name;") + if err != nil { + return nil, err + } + defer rows.Close() + + var out []models.Tag + for rows.Next() { + var t models.Tag + if err := rows.Scan(&t.ID, &t.Name); err != nil { + return nil, err + } + out = append(out, t) + } + return out, rows.Err() +} + +func (s *Store) GetCakeTags(cakeID int) ([]models.Tag, error) { + rows, err := s.db.Query(` + select t.id, t.name from tag t + join cake_tag ct on ct.tag = t.id + where ct.cake = ? + order by t.name + `, cakeID) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []models.Tag + for rows.Next() { + var t models.Tag + if err := rows.Scan(&t.ID, &t.Name); err != nil { + return nil, err + } + out = append(out, t) + } + return out, rows.Err() +} + +func (s *Store) SaveCakeTags(cakeID int, tagIDs []int) error { + _, err := s.db.Exec("delete from cake_tag where cake = ?;", cakeID) + if err != nil { + return err + } + for _, tid := range tagIDs { + _, err := s.db.Exec("insert or ignore into cake_tag(cake, tag) values (?, ?);", cakeID, tid) + if err != nil { + return err + } + } + return nil +} + +func (s *Store) CreateTag(name string) (int, error) { + result, err := s.db.Exec("insert into tag(name) values (?) on conflict(name) do nothing;", name) + if err != nil { + return 0, err + } + id, err := result.LastInsertId() + if err != nil { + return 0, err + } + if id == 0 { + row := s.db.QueryRow("select id from tag where name = ?;", name) + if err := row.Scan(&id); err != nil { + return 0, err + } + } + return int(id), nil +}