// Copyright 2026 Moko Consulting // SPDX-License-Identifier: GPL-3.0-or-later package issues import ( "context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil" ) func init() { db.RegisterModel(new(IssueStatusDef)) } // IssueStatusDef defines a custom issue status at the org level. type IssueStatusDef struct { ID int64 `xorm:"pk autoincr"` OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'org_id'"` Name string `xorm:"NOT NULL"` Color string `xorm:"VARCHAR(7)"` // hex color, e.g. "#e11d48" Description string `xorm:"TEXT"` ClosesIssue bool `xorm:"NOT NULL DEFAULT false 'closes_issue'"` SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"` IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"` UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"` } func (IssueStatusDef) TableName() string { return "issue_status_def" } // ────────────────────────────────────────────────────────────────────── // Queries // ────────────────────────────────────────────────────────────────────── // GetIssueStatusDefsByOrg returns active status definitions for an org. // If none exist, seeds the org with default statuses automatically. func GetIssueStatusDefsByOrg(ctx context.Context, orgID int64) ([]*IssueStatusDef, error) { defs := make([]*IssueStatusDef, 0, 10) if err := db.GetEngine(ctx). Where("org_id = ? AND is_active = ?", orgID, true). OrderBy("sort_order ASC, id ASC"). Find(&defs); err != nil { return nil, err } if len(defs) == 0 && orgID > 0 { if err := seedDefaultIssueStatuses(ctx, orgID); err != nil { return defs, nil // non-fatal } return GetIssueStatusDefsByOrg(ctx, orgID) } return defs, nil } // seedDefaultIssueStatuses creates the standard status presets for an org. func seedDefaultIssueStatuses(ctx context.Context, orgID int64) error { defaults := []*IssueStatusDef{ {OrgID: orgID, Name: "In Progress", Color: "#2563eb", Description: "Work is actively being done", SortOrder: 1, IsActive: true}, {OrgID: orgID, Name: "Needs Info", Color: "#f59e0b", Description: "Waiting for more information", SortOrder: 2, IsActive: true}, {OrgID: orgID, Name: "Blocked", Color: "#dc2626", Description: "Cannot proceed due to dependency", SortOrder: 3, IsActive: true}, {OrgID: orgID, Name: "Resolved", Color: "#16a34a", Description: "Fix implemented and verified", ClosesIssue: true, SortOrder: 4, IsActive: true}, {OrgID: orgID, Name: "Won't Fix", Color: "#6b7280", Description: "Decided not to address", ClosesIssue: true, SortOrder: 5, IsActive: true}, {OrgID: orgID, Name: "Duplicate", Color: "#8b5cf6", Description: "Already tracked elsewhere", ClosesIssue: true, SortOrder: 6, IsActive: true}, } for _, d := range defaults { if _, err := db.GetEngine(ctx).Insert(d); err != nil { return err } } return nil } // GetAllIssueStatusDefsByOrg returns all status definitions (including inactive). func GetAllIssueStatusDefsByOrg(ctx context.Context, orgID int64) ([]*IssueStatusDef, error) { defs := make([]*IssueStatusDef, 0, 10) return defs, db.GetEngine(ctx). Where("org_id = ?", orgID). OrderBy("sort_order ASC, id ASC"). Find(&defs) } // GetIssueStatusDefByID returns a single status definition. func GetIssueStatusDefByID(ctx context.Context, id int64) (*IssueStatusDef, error) { def := new(IssueStatusDef) has, err := db.GetEngine(ctx).ID(id).Get(def) if err != nil { return nil, err } if !has { return nil, db.ErrNotExist{Resource: "IssueStatusDef", ID: id} } return def, nil } // ────────────────────────────────────────────────────────────────────── // CRUD // ────────────────────────────────────────────────────────────────────── // CreateIssueStatusDef creates a new status definition. func CreateIssueStatusDef(ctx context.Context, def *IssueStatusDef) error { _, err := db.GetEngine(ctx).Insert(def) return err } // UpdateIssueStatusDef updates a status definition. func UpdateIssueStatusDef(ctx context.Context, def *IssueStatusDef) error { _, err := db.GetEngine(ctx).ID(def.ID).AllCols().Update(def) return err } // DeleteIssueStatusDef deletes a status definition and clears references on issues. func DeleteIssueStatusDef(ctx context.Context, id int64) error { // Clear status_id on all issues that reference this definition if _, err := db.GetEngine(ctx).Exec("UPDATE issue SET status_id = 0 WHERE status_id = ?", id); err != nil { return err } _, err := db.GetEngine(ctx).ID(id).Delete(new(IssueStatusDef)) return err } // ────────────────────────────────────────────────────────────────────── // Issue status helpers // ────────────────────────────────────────────────────────────────────── // SetIssueStatusID updates the status_id on an issue. func SetIssueStatusID(ctx context.Context, issueID, statusID int64) error { _, err := db.GetEngine(ctx).Exec("UPDATE issue SET status_id = ? WHERE id = ?", statusID, issueID) return err }