feat: expand kanban assignment and filtering
Test and publish / verify (push) Successful in 2m22s

This commit is contained in:
2026-07-29 13:53:31 +03:30
parent e1475d0df0
commit 5a3ca4eb40
7 changed files with 432 additions and 91 deletions
+80 -12
View File
@@ -97,10 +97,18 @@ type PageData struct {
Board []BoardColumn
BoardTags []BoardTag
ArchivedTasks []BoardTask
BoardFilter BoardFilter
}
type Stats struct{ Present, Remote, Leave int }
type BoardFilter struct {
TagID int64
AssigneeID int64
Priority string
Active bool
}
type ReportTotals struct {
Present int
Remote int
@@ -217,6 +225,14 @@ func New(cfg Config) (*Server, error) {
}
return false
},
"hasUser": func(users []User, id int64) bool {
for _, user := range users {
if user.ID == id {
return true
}
}
return false
},
}
tmpl, err := template.New("root").Funcs(funcs).ParseFS(assets, "templates/*.html")
if err != nil {
@@ -275,6 +291,7 @@ func (s *Server) routes(mux *http.ServeMux) {
mux.HandleFunc("POST /board/tasks/{id}/restore", s.requireAuth(s.csrf(s.restoreBoardTask)))
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/tasks/{id}/details", s.requireAuth(s.csrf(s.setBoardTaskDetails)))
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))
@@ -763,6 +780,22 @@ func (s *Server) boardPage(w http.ResponseWriter, r *http.Request) {
http.Error(w, "could not load board tags", http.StatusInternalServerError)
return
}
filter := BoardFilter{Priority: r.URL.Query().Get("priority")}
filter.TagID, _ = strconv.ParseInt(r.URL.Query().Get("tag"), 10, 64)
filter.AssigneeID, _ = strconv.ParseInt(r.URL.Query().Get("assignee"), 10, 64)
if !validBoardPriority(filter.Priority) {
filter.Priority = ""
}
filter.Active = filter.TagID > 0 || filter.AssigneeID > 0 || filter.Priority != ""
if filter.Active {
filtered := make([]BoardTask, 0, len(tasks))
for _, task := range tasks {
if boardTaskMatchesFilter(task, filter) {
filtered = append(filtered, task)
}
}
tasks = filtered
}
columns := []BoardColumn{
{Status: "backlog", Title: "Backlog", Hint: "Ready to pick up"},
{Status: "in_progress", Title: "In progress", Hint: "Actively being worked"},
@@ -779,26 +812,52 @@ 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, BoardTags: tags,
ArchivedTasks: archivedTasks,
TodayJalali: jalali.FromTime(time.Now()).String(), Error: r.URL.Query().Get("error"),
ArchivedTasks: archivedTasks, BoardFilter: filter,
TodayJalali: jalali.FromTime(time.Now()).String(), Error: r.URL.Query().Get("error"),
Flash: r.URL.Query().Get("flash"), SelectedSection: "board",
})
}
func validBoardPriority(priority string) bool {
return priority == "" || priority == "low" || priority == "normal" || priority == "high" || priority == "urgent"
}
func boardTaskMatchesFilter(task BoardTask, filter BoardFilter) bool {
if filter.Priority != "" && task.Importance != filter.Priority {
return false
}
if filter.TagID > 0 {
found := false
for _, tag := range task.Tags {
if tag.ID == filter.TagID {
found = true
break
}
}
if !found {
return false
}
}
if filter.AssigneeID > 0 {
found := false
for _, assignee := range task.Assignees {
if assignee.ID == filter.AssigneeID {
found = true
break
}
}
if !found {
return false
}
}
return true
}
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)
if err != nil || id < 1 {
http.Redirect(w, r, "/board?error=Choose+a+valid+assignee", http.StatusSeeOther)
return
}
assigneeID = &id
}
dueDate := ""
if rawDue := strings.TrimSpace(r.FormValue("due_date")); rawDue != "" {
parsed, ok := parseUserDate(rawDue)
@@ -819,7 +878,7 @@ func (s *Server) createBoardTask(w http.ResponseWriter, r *http.Request) {
}
taskID, err := s.store.CreateBoardTask(
currentUser(r).ID, r.FormValue("title"), r.FormValue("description"),
r.FormValue("status"), assigneeID, dueDate,
r.FormValue("status"), r.FormValue("importance"), parseIDList(r.Form["assignee_ids"]), dueDate,
)
if err != nil {
http.Redirect(w, r, "/board?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
@@ -892,6 +951,15 @@ func (s *Server) setBoardTaskTags(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/board?flash=Task+labels+updated", http.StatusSeeOther)
}
func (s *Server) setBoardTaskDetails(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err := s.store.SetBoardTaskDetails(id, parseIDList(r.Form["assignee_ids"]), r.FormValue("importance")); err != nil {
http.Redirect(w, r, "/board?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
return
}
http.Redirect(w, r, "/board?flash=Card+details+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)