feat: enhance team planning and deployment
Test and publish / verify (push) Successful in 2m45s

This commit is contained in:
2026-07-29 12:09:21 +03:30
parent c3083b137f
commit eba6b3565c
10 changed files with 472 additions and 22 deletions
+74 -4
View File
@@ -95,6 +95,7 @@ type PageData struct {
ReportSummary []PersonReportSummary
ReportTotals ReportTotals
Board []BoardColumn
BoardTags []BoardTag
}
type Stats struct{ Present, Remote, Leave int }
@@ -207,6 +208,14 @@ func New(cfg Config) (*Server, error) {
r, _ := utf8.DecodeRuneInString(v)
return string(r)
},
"hasTag": func(tags []BoardTag, id int64) bool {
for _, tag := range tags {
if tag.ID == id {
return true
}
}
return false
},
}
tmpl, err := template.New("root").Funcs(funcs).ParseFS(assets, "templates/*.html")
if err != nil {
@@ -262,6 +271,9 @@ func (s *Server) routes(mux *http.ServeMux) {
mux.HandleFunc("POST /board/tasks", s.requireAuth(s.csrf(s.createBoardTask)))
mux.HandleFunc("POST /board/tasks/{id}/move", s.requireAuth(s.csrf(s.moveBoardTask)))
mux.HandleFunc("POST /board/tasks/{id}/delete", s.requireAuth(s.csrf(s.deleteBoardTask)))
mux.HandleFunc("POST /board/tasks/{id}/tags", s.requireAuth(s.csrf(s.setBoardTaskTags)))
mux.HandleFunc("POST /board/tags", s.requireAuth(s.csrf(s.createBoardTag)))
mux.HandleFunc("POST /board/tags/{id}/delete", s.requireAuth(s.csrf(s.deleteBoardTag)))
mux.HandleFunc("GET /admin/requests", s.requireAdmin(s.adminPage))
mux.HandleFunc("POST /admin/requests/{id}/review", s.requireAdmin(s.csrf(s.reviewRequest)))
mux.HandleFunc("GET /admin/users", s.requireAdmin(s.usersPage))
@@ -285,8 +297,8 @@ func (s *Server) dayPage(w http.ResponseWriter, r *http.Request) {
}
selected, _ := time.Parse("2006-01-02", day)
view := r.URL.Query().Get("view")
if view != "week" {
view = "day"
if view != "day" {
view = "week"
}
var detail DayDetail
var week WeekDetail
@@ -737,6 +749,11 @@ func (s *Server) boardPage(w http.ResponseWriter, r *http.Request) {
http.Error(w, "could not load teammates", http.StatusInternalServerError)
return
}
tags, err := s.store.BoardTags()
if err != nil {
http.Error(w, "could not load board tags", http.StatusInternalServerError)
return
}
columns := []BoardColumn{
{Status: "backlog", Title: "Backlog", Hint: "Ready to pick up"},
{Status: "in_progress", Title: "In progress", Hint: "Actively being worked"},
@@ -752,13 +769,17 @@ func (s *Server) boardPage(w http.ResponseWriter, r *http.Request) {
}
}
s.render(w, "board.html", PageData{
Title: "Team board", User: currentUser(r), CSRF: csrfToken(r), Users: users, Board: columns,
Title: "Team board", User: currentUser(r), CSRF: csrfToken(r), Users: users, Board: columns, BoardTags: tags,
TodayJalali: jalali.FromTime(time.Now()).String(), Error: r.URL.Query().Get("error"),
Flash: r.URL.Query().Get("flash"), SelectedSection: "board",
})
}
func (s *Server) createBoardTask(w http.ResponseWriter, r *http.Request) {
if strings.TrimSpace(r.FormValue("title")) == "" {
http.Redirect(w, r, "/board?error=Task+title+is+required", http.StatusSeeOther)
return
}
var assigneeID *int64
if raw := r.FormValue("assignee_id"); raw != "" {
id, err := strconv.ParseInt(raw, 10, 64)
@@ -777,7 +798,16 @@ func (s *Server) createBoardTask(w http.ResponseWriter, r *http.Request) {
}
dueDate = parsed
}
err := s.store.CreateBoardTask(
tagIDs := parseIDList(r.Form["tag_ids"])
if newTagName := strings.TrimSpace(r.FormValue("new_tag_name")); newTagName != "" {
tag, err := s.store.FindOrCreateBoardTag(newTagName, r.FormValue("new_tag_color"))
if err != nil {
http.Redirect(w, r, "/board?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
return
}
tagIDs = append(tagIDs, tag.ID)
}
taskID, err := s.store.CreateBoardTask(
currentUser(r).ID, r.FormValue("title"), r.FormValue("description"),
r.FormValue("status"), assigneeID, dueDate,
)
@@ -785,9 +815,23 @@ func (s *Server) createBoardTask(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/board?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
return
}
if err := s.store.SetBoardTaskTags(taskID, tagIDs); err != nil {
http.Redirect(w, r, "/board?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
return
}
http.Redirect(w, r, "/board?flash=Task+added+to+the+team+board", http.StatusSeeOther)
}
func parseIDList(values []string) []int64 {
ids := make([]int64, 0, len(values))
for _, raw := range values {
if id, err := strconv.ParseInt(raw, 10, 64); err == nil && id > 0 {
ids = append(ids, id)
}
}
return ids
}
func (s *Server) moveBoardTask(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err := s.store.MoveBoardTask(id, r.FormValue("status")); err != nil {
@@ -807,6 +851,32 @@ func (s *Server) deleteBoardTask(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/board?flash=Task+removed", http.StatusSeeOther)
}
func (s *Server) setBoardTaskTags(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err := s.store.SetBoardTaskTags(id, parseIDList(r.Form["tag_ids"])); err != nil {
http.Redirect(w, r, "/board?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
return
}
http.Redirect(w, r, "/board?flash=Task+labels+updated", http.StatusSeeOther)
}
func (s *Server) createBoardTag(w http.ResponseWriter, r *http.Request) {
if err := s.store.CreateBoardTag(r.FormValue("name"), r.FormValue("color")); err != nil {
http.Redirect(w, r, "/board?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
return
}
http.Redirect(w, r, "/board?flash=Tag+created", http.StatusSeeOther)
}
func (s *Server) deleteBoardTag(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err := s.store.DeleteBoardTag(id); err != nil {
http.Redirect(w, r, "/board?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
return
}
http.Redirect(w, r, "/board?flash=Tag+deleted", http.StatusSeeOther)
}
func (s *Server) adminPage(w http.ResponseWriter, r *http.Request) {
requests, err := s.store.Requests(currentUser(r).ID, true, r.URL.Query().Get("status"))
if err != nil {