package main import ( "database/sql" "fmt" ) func ensureSchema(db *sql.DB) error { stmts := []string{ `CREATE TABLE IF NOT EXISTS settings( id TINYINT PRIMARY KEY, days_per_month INT NOT NULL DEFAULT 2, current_month TINYINT NOT NULL DEFAULT 1, current_day TINYINT NOT NULL DEFAULT 1 )`, `ALTER TABLE settings ADD COLUMN IF NOT EXISTS current_month TINYINT NOT NULL DEFAULT 1`, `ALTER TABLE settings ADD COLUMN IF NOT EXISTS current_day TINYINT NOT NULL DEFAULT 1`, `INSERT INTO settings(id,days_per_month,current_month,current_day) VALUES (1,2,1,1) ON DUPLICATE KEY UPDATE id=id`, `UPDATE settings SET current_month=1 WHERE current_month < 1 OR current_month > 12`, `UPDATE settings SET current_day=1 WHERE current_day < 1`, `CREATE TABLE IF NOT EXISTS fields( id BIGINT AUTO_INCREMENT PRIMARY KEY, number INT NOT NULL UNIQUE, name VARCHAR(120) NOT NULL DEFAULT '', owned TINYINT(1) NOT NULL DEFAULT 0, group_key VARCHAR(64) NOT NULL DEFAULT '', group_name VARCHAR(120) NOT NULL DEFAULT '', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP )`, `ALTER TABLE fields ADD COLUMN IF NOT EXISTS owned TINYINT(1) NOT NULL DEFAULT 0`, `ALTER TABLE fields ADD COLUMN IF NOT EXISTS group_key VARCHAR(64) NOT NULL DEFAULT ''`, `ALTER TABLE fields ADD COLUMN IF NOT EXISTS group_name VARCHAR(120) NOT NULL DEFAULT ''`, `CREATE TABLE IF NOT EXISTS crops( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(80) NOT NULL UNIQUE, sow_start_month TINYINT NOT NULL, sow_end_month TINYINT NOT NULL, grow_months TINYINT NOT NULL, regrow_enabled TINYINT(1) NOT NULL DEFAULT 0, regrow_cycles INT NOT NULL DEFAULT 0 )`, `ALTER TABLE crops ADD COLUMN IF NOT EXISTS regrow_enabled TINYINT(1) NOT NULL DEFAULT 0`, `ALTER TABLE crops ADD COLUMN IF NOT EXISTS regrow_cycles INT NOT NULL DEFAULT 0`, `UPDATE crops SET regrow_cycles=0 WHERE regrow_cycles < 0`, `CREATE TABLE IF NOT EXISTS crop_steps( id BIGINT AUTO_INCREMENT PRIMARY KEY, crop_id BIGINT NOT NULL, phase ENUM('pre','post') NOT NULL, month_offset INT NOT NULL DEFAULT 0, title VARCHAR(140) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_crop_steps_crop FOREIGN KEY(crop_id) REFERENCES crops(id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS custom_task_templates( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(140) NOT NULL UNIQUE, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP )`, `CREATE TABLE IF NOT EXISTS custom_tasks( id BIGINT AUTO_INCREMENT PRIMARY KEY, template_id BIGINT NULL, title VARCHAR(140) NOT NULL, month TINYINT NOT NULL, day TINYINT NOT NULL, year_offset SMALLINT NOT NULL DEFAULT 0, target_name VARCHAR(120) NOT NULL DEFAULT '', notes VARCHAR(255) NOT NULL DEFAULT '', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_custom_tasks_template FOREIGN KEY(template_id) REFERENCES custom_task_templates(id) ON DELETE SET NULL )`, `CREATE TABLE IF NOT EXISTS task_completions( task_uid VARCHAR(190) NOT NULL, month TINYINT NOT NULL, day TINYINT NOT NULL, year_offset SMALLINT NOT NULL DEFAULT 0, done TINYINT(1) NOT NULL DEFAULT 1, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY(task_uid,month,day,year_offset) )`, `CREATE TABLE IF NOT EXISTS plans( id BIGINT AUTO_INCREMENT PRIMARY KEY, field_id BIGINT NULL, target_ref VARCHAR(80) NOT NULL DEFAULT '', target_name VARCHAR(140) NOT NULL DEFAULT '', crop_id BIGINT NOT NULL, start_month TINYINT NOT NULL, start_day TINYINT NOT NULL, harvest_month TINYINT NOT NULL, harvest_day TINYINT NOT NULL, notes VARCHAR(255) NOT NULL DEFAULT '', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_plans_fields FOREIGN KEY(field_id) REFERENCES fields(id) ON DELETE CASCADE, CONSTRAINT fk_plans_crops FOREIGN KEY(crop_id) REFERENCES crops(id) ON DELETE RESTRICT )`, `ALTER TABLE plans MODIFY COLUMN field_id BIGINT NULL`, `ALTER TABLE plans ADD COLUMN IF NOT EXISTS target_ref VARCHAR(80) NOT NULL DEFAULT '' AFTER field_id`, `ALTER TABLE plans ADD COLUMN IF NOT EXISTS target_name VARCHAR(140) NOT NULL DEFAULT '' AFTER target_ref`, } for _, stmt := range stmts { if _, err := db.Exec(stmt); err != nil { return err } } return seedCrops(db) } func (a *App) getSettings() (Settings, error) { var s Settings err := a.db.QueryRow(`SELECT days_per_month,current_month,current_day FROM settings WHERE id=1`). Scan(&s.DaysPerMonth, &s.CurrentMonth, &s.CurrentDay) return s, err } func (a *App) listFields() ([]Field, error) { rows, err := a.db.Query(`SELECT id,number,name,owned,COALESCE(group_key,''),COALESCE(group_name,'') FROM fields ORDER BY number`) if err != nil { return nil, err } defer rows.Close() var out []Field for rows.Next() { var f Field if err := rows.Scan(&f.ID, &f.Number, &f.Name, &f.Owned, &f.GroupKey, &f.GroupName); err != nil { return nil, err } out = append(out, f) } return out, rows.Err() } func (a *App) getFieldsByIDs(ids []int64) ([]Field, error) { args := make([]any, 0, len(ids)) for _, id := range ids { args = append(args, id) } q := fmt.Sprintf(`SELECT id,number,name,owned,COALESCE(group_key,''),COALESCE(group_name,'') FROM fields WHERE id IN (%s)`, placeholders(len(ids))) rows, err := a.db.Query(q, args...) if err != nil { return nil, err } defer rows.Close() var out []Field for rows.Next() { var f Field if err := rows.Scan(&f.ID, &f.Number, &f.Name, &f.Owned, &f.GroupKey, &f.GroupName); err != nil { return nil, err } out = append(out, f) } return out, rows.Err() } func (a *App) listCrops() ([]Crop, error) { rows, err := a.db.Query(`SELECT id,name,sow_start_month,sow_end_month,grow_months,regrow_enabled,regrow_cycles FROM crops ORDER BY name`) if err != nil { return nil, err } defer rows.Close() var out []Crop for rows.Next() { var c Crop if err := rows.Scan(&c.ID, &c.Name, &c.SowStartMonth, &c.SowEndMonth, &c.GrowMonths, &c.RegrowEnabled, &c.RegrowCycles); err != nil { return nil, err } out = append(out, c) } return out, rows.Err() } func (a *App) listPlans() ([]Plan, error) { rows, err := a.db.Query(` SELECT p.id,p.field_id,COALESCE(p.target_ref,''),COALESCE(p.target_name,''),p.crop_id,COALESCE(c.name,''),COALESCE(c.grow_months,1),p.start_month,p.start_day,p.harvest_month,p.harvest_day,COALESCE(p.notes,''),COALESCE(c.regrow_enabled,0),COALESCE(c.regrow_cycles,0) FROM plans p JOIN crops c ON c.id=p.crop_id ORDER BY p.start_month,p.start_day,p.id DESC`) if err != nil { return nil, err } defer rows.Close() var out []Plan for rows.Next() { var p Plan if err := rows.Scan(&p.ID, &p.FieldID, &p.TargetRef, &p.TargetName, &p.CropID, &p.CropName, &p.GrowMonths, &p.StartMonth, &p.StartDay, &p.HarvestMonth, &p.HarvestDay, &p.Notes, &p.RegrowEnabled, &p.RegrowCycles); err != nil { return nil, err } out = append(out, p) } return out, rows.Err() } func (a *App) listCropSteps() ([]CropStep, error) { rows, err := a.db.Query(` SELECT s.id,s.crop_id,COALESCE(c.name,''),s.phase,s.month_offset,s.title FROM crop_steps s JOIN crops c ON c.id=s.crop_id ORDER BY c.name, s.phase, s.month_offset, s.id`) if err != nil { return nil, err } defer rows.Close() var out []CropStep for rows.Next() { var s CropStep if err := rows.Scan(&s.ID, &s.CropID, &s.CropName, &s.Phase, &s.MonthOffset, &s.Title); err != nil { return nil, err } out = append(out, s) } return out, rows.Err() } func (a *App) listCropStepsMap() (map[int64][]CropStep, error) { all, err := a.listCropSteps() if err != nil { return nil, err } m := make(map[int64][]CropStep) for _, s := range all { m[s.CropID] = append(m[s.CropID], s) } return m, nil } func (a *App) listCustomTaskTemplates() ([]CustomTaskTemplate, error) { rows, err := a.db.Query(`SELECT id,title FROM custom_task_templates ORDER BY title`) if err != nil { return nil, err } defer rows.Close() var out []CustomTaskTemplate for rows.Next() { var t CustomTaskTemplate if err := rows.Scan(&t.ID, &t.Title); err != nil { return nil, err } out = append(out, t) } return out, rows.Err() } func (a *App) listCustomTasks() ([]CustomTask, error) { rows, err := a.db.Query(` SELECT id,template_id,title,month,day,year_offset,COALESCE(target_name,''),COALESCE(notes,'') FROM custom_tasks ORDER BY year_offset, month, day, title`) if err != nil { return nil, err } defer rows.Close() var out []CustomTask for rows.Next() { var t CustomTask if err := rows.Scan(&t.ID, &t.TemplateID, &t.Title, &t.Month, &t.Day, &t.YearOffset, &t.TargetName, &t.Notes); err != nil { return nil, err } out = append(out, t) } return out, rows.Err() } func (a *App) listTaskCompletionsMap() (map[string]bool, error) { rows, err := a.db.Query(`SELECT task_uid,month,day,year_offset,done FROM task_completions WHERE done=1`) if err != nil { return nil, err } defer rows.Close() out := make(map[string]bool) for rows.Next() { var uid string var month, day, yearOffset int var done bool if err := rows.Scan(&uid, &month, &day, &yearOffset, &done); err != nil { return nil, err } if done { out[completionKey(uid, month, day, yearOffset)] = true } } return out, rows.Err() } func seedCrops(db *sql.DB) error { items := []Crop{ {Name: "Weizen", SowStartMonth: 9, SowEndMonth: 11, GrowMonths: 10}, {Name: "Gerste", SowStartMonth: 9, SowEndMonth: 10, GrowMonths: 9}, {Name: "Hafer", SowStartMonth: 3, SowEndMonth: 4, GrowMonths: 4}, {Name: "Raps", SowStartMonth: 8, SowEndMonth: 9, GrowMonths: 11}, {Name: "Mais", SowStartMonth: 4, SowEndMonth: 5, GrowMonths: 6}, {Name: "Sorghum", SowStartMonth: 4, SowEndMonth: 5, GrowMonths: 5}, {Name: "Sojabohnen", SowStartMonth: 4, SowEndMonth: 5, GrowMonths: 5}, {Name: "Sonnenblumen", SowStartMonth: 4, SowEndMonth: 5, GrowMonths: 5}, {Name: "Kartoffeln", SowStartMonth: 3, SowEndMonth: 4, GrowMonths: 7}, {Name: "Zuckerrueben", SowStartMonth: 3, SowEndMonth: 4, GrowMonths: 8}, {Name: "Baumwolle", SowStartMonth: 2, SowEndMonth: 3, GrowMonths: 8}, } for _, c := range items { if _, err := db.Exec(`INSERT INTO crops(name,sow_start_month,sow_end_month,grow_months,regrow_enabled,regrow_cycles) VALUES (?,?,?,?,0,0) ON DUPLICATE KEY UPDATE sow_start_month=VALUES(sow_start_month),sow_end_month=VALUES(sow_end_month),grow_months=VALUES(grow_months)`, c.Name, c.SowStartMonth, c.SowEndMonth, c.GrowMonths); err != nil { return err } } return nil }