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