ความคืบหน้างาน ไฟล์ต้นฉบับ

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)
	})
}