tracker/usecases.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
)
type UseCase struct {
SourceHash string `json:"-"`
Approved bool `json:"-"`
ApprovalState string `json:"-"`
Channels []string `json:"-"`
ChannelReasons []string `json:"-"`
ID string `json:"-"`
Name string `json:"n"`
Phase string `json:"p"`
Modules []string `json:"m"`
Start string `json:"s"`
Outcome string `json:"o"`
Actors []string `json:"a"`
Flow [][2]string `json:"f"`
Relations [][2]string `json:"r"`
Screens []string `json:"sc"`
Workflow *WorkflowSpec `json:"wf,omitempty"`
Review *WorkflowReview `json:"review,omitempty"`
}
type WorkflowReview struct {
Date string `json:"date"`
State string `json:"state"`
Note string `json:"note"`
}
type WorkflowSpec struct {
Decisions []WorkflowDecision `json:"decisions"`
Next map[string]int `json:"next,omitempty"`
Stop map[string]string `json:"stop,omitempty"`
}
type WorkflowDecision struct {
Before int `json:"before"`
Title string `json:"title"`
Description string `json:"description"`
Branches []WorkflowBranch `json:"branches"`
}
type WorkflowBranch struct {
Label string `json:"label"`
Target int `json:"target,omitempty"`
Outcome string `json:"outcome,omitempty"`
}
// encoding/json escapes HTML metacharacters before embedding canonical draft data.
func (u UseCase) DraftJSON() template.JS {
b, _ := json.Marshal(draftDiagram(u))
return template.JS(b)
}
type UCModule struct {
Name string `json:"n"`
Group string `json:"g"`
IDs []string `json:"u"`
}
type UCCatalog struct {
UC map[string]UseCase `json:"uc"`
Modules map[string]UCModule `json:"mods"`
}
type UCModuleGroup struct {
Code, Name, Group string
Items []UseCase
}
type UCView struct {
ModuleNames map[string]string
Groups []UCModuleGroup
Items []UseCase
Modules map[string]UCModule
Actors []string
Selected *UseCase
Query, Module, Actor string
Total int
Channel string
Tabs []UCTab
}
func loadUseCases(root string) (UCCatalog, error) {
var c UCCatalog
b, err := os.ReadFile(filepath.Join(root, "use_cases", "UC1-Explorer.dc.html"))
if err != nil {
return c, err
}
marker := []byte("const D = ")
i := bytes.Index(b, marker)
if i < 0 {
return c, fmt.Errorf("use case source has no data")
}
err = json.NewDecoder(bytes.NewReader(b[i+len(marker):])).Decode(&c)
if err == nil && (len(c.UC) == 0 || len(c.Modules) == 0) {
err = fmt.Errorf("empty use case catalog")
}
for id, u := range c.UC {
u.ID = id
u.SourceHash = sourceDigest(u)
c.UC[id] = classifyUseCase(u)
}
return c, err
}
func ucView(c UCCatalog, r *http.Request) UCView {
v := UCView{Modules: c.Modules, Total: len(c.UC), Query: strings.TrimSpace(r.URL.Query().Get("q")), Module: r.URL.Query().Get("module"), Actor: r.URL.Query().Get("actor")}
v.ModuleNames = map[string]string{}
for code, m := range c.Modules {
v.ModuleNames[code] = m.Name
}
v.Channel = r.URL.Query().Get("channel")
if v.Channel != "frontend" && v.Channel != "backoffice" && v.Channel != "integration" {
v.Channel = ""
}
v.Tabs = channelTabs(v)
actors := map[string]bool{}
for _, u := range c.UC {
for _, a := range u.Actors {
actors[a] = true
}
data, _ := json.Marshal(u)
if v.Query != "" && !strings.Contains(strings.ToLower(u.ID+" "+string(data)), strings.ToLower(v.Query)) {
continue
}
if v.Module != "" && !containsUC(u.Modules, v.Module) {
continue
}
if v.Actor != "" && !containsUC(u.Actors, v.Actor) {
continue
}
for i := range v.Tabs {
if v.Tabs[i].Key == "" || containsUC(u.Channels, v.Tabs[i].Key) {
v.Tabs[i].Count++
}
}
if v.Channel != "" && !containsUC(u.Channels, v.Channel) {
continue
}
v.Items = append(v.Items, u)
}
for a := range actors {
v.Actors = append(v.Actors, a)
}
sort.Strings(v.Actors)
sort.Slice(v.Items, func(i, j int) bool {
a, _ := strconv.Atoi(strings.TrimPrefix(v.Items[i].ID, "W-"))
b, _ := strconv.Atoi(strings.TrimPrefix(v.Items[j].ID, "W-"))
return a < b
})
grouped := map[string][]UseCase{}
for _, u := range v.Items {
code := "Other"
if len(u.Modules) > 0 {
code = u.Modules[0]
}
grouped[code] = append(grouped[code], u)
}
codes := []string{}
for code := range grouped {
codes = append(codes, code)
}
sort.Strings(codes)
for _, code := range codes {
m := c.Modules[code]
v.Groups = append(v.Groups, UCModuleGroup{Code: code, Name: m.Name, Group: m.Group, Items: grouped[code]})
}
return v
}
func containsUC(a []string, s string) bool {
for _, v := range a {
if v == s {
return true
}
}
return false
}
func registerUseCases(mux *http.ServeMux, store *Store) {
tpl := template.Must(template.New("").Funcs(templateFuncs()).ParseFS(templateFS, "templates/layout.html", "templates/partials.html", "templates/usecases.html"))
ds := &diagramStore{dir: store.dir}
registerDiagramHistory(mux, ds)
registerUseCaseApproval(mux, ds)
page := func(w http.ResponseWriter, r *http.Request) {
c, err := loadUseCases(repoRoot)
if err != nil {
http.Error(w, "อ่านข้อมูล Use cases ไม่สำเร็จ", 500)
return
}
for id, u := range c.UC {
status := ds.approvalStatus(u)
u.Approved = status.Approved
u.ApprovalState = status.State
c.UC[id] = u
}
v := ucView(c, r)
if id := r.PathValue("uid"); id != "" {
u, ok := c.UC[id]
if !ok {
http.NotFound(w, r)
return
}
v.Selected = &u
}
p := buildPage(store, "usecases", "Use cases")
p.UseCases = &v
var b bytes.Buffer
if err := tpl.ExecuteTemplate(&b, "layout", p); err != nil {
http.Error(w, "render error", 500)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
b.WriteTo(w)
}
mux.HandleFunc("GET /use-cases", page)
mux.HandleFunc("GET /use-cases/{uid}", page)
mux.HandleFunc("/api/use-cases/{uid}/diagram", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "PUT" {
w.Header().Set("Allow", "GET, PUT")
http.Error(w, "method not allowed", 405)
return
}
c, err := loadUseCases(repoRoot)
if err != nil {
http.Error(w, "อ่านต้นฉบับไม่สำเร็จ", 500)
return
}
id := r.PathValue("uid")
u, ok := c.UC[id]
if !ok {
http.NotFound(w, r)
return
}
var d Diagram
if r.Method == "GET" {
d, err = ds.load(id, u)
} else {
if !sameOrigin(r) {
http.Error(w, "ต้องบันทึกจาก tracker นี้", 403)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 2<<20)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err = dec.Decode(&d); err != nil {
http.Error(w, "ข้อมูล diagram ไม่ถูกต้องหรือใหญ่เกินไป", 400)
return
}
var extra any
if err = dec.Decode(&extra); err != io.EOF {
http.Error(w, "ข้อมูล JSON ต้องมีชุดเดียว", 400)
return
}
if err = validateDiagram(d); err != nil {
http.Error(w, err.Error(), 400)
return
}
d, err = ds.save(id, d)
}
if err == errDiagramConflict {
http.Error(w, "มีคนบันทึกเวอร์ชันใหม่แล้ว กรุณาส่งออกงานของคุณก่อนโหลดใหม่", 409)
return
}
if err != nil {
http.Error(w, "อ่านหรือบันทึก diagram ไม่สำเร็จ", 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
json.NewEncoder(w).Encode(d)
})
}