Files
LazyPM/pkg/web/routes/boardview.templ
Robin Olsen d900a5fcd3 Merge pull request #75 from LazyBachelor/LPM-140
LPM-140 The Sprinting Update
Adds sprint support across the web UI (board + issue detail), TUI kanban, CLI/REPL, and the Beads-backed storage layer.

Changes:

Introduces sprint entities in storage and exposes sprint operations via IssueService.
Updates web board to support backlog vs. sprint columns, sprint selection, and sprint creation routes.
Updates TUI kanban to show backlog + sprint columns and adds sprint selection/creation actions.
2026-03-23 19:26:04 +01:00

183 lines
5.9 KiB
Plaintext

package routes
import (
"fmt"
"github.com/LazyBachelor/LazyPM/internal/models"
"github.com/LazyBachelor/LazyPM/pkg/web/components"
)
type BoardViewProps struct {
Issues []*models.Issue
BacklogIssues []*models.Issue
SprintIssues []*models.Issue
CurrentSprint int
Sprints []int
BaseURL string
QueryParam string
EmptyText string
}
templ BoardView(props BoardViewProps) {
@BaseLayout() {
@BoardViewContent(props)
}
}
templ BoardViewContent(props BoardViewProps) {
<div class="flex flex-col h-full">
<div class="flex flex-col border-b border-base-200">
<div class="flex p-2 justify-between items-center gap-2">
<div class="flex gap-2">
<div class="join">
<button
class="btn btn-sm join-item"
hx-get="/"
hx-target="main"
hx-swap="innerHTML"
hx-push-url="true"
>List</button>
<button class="btn btn-sm join-item btn-active btn-primary">Board</button>
</div>
<button
class="btn btn-primary btn-sm"
hx-get="/issues/create?from=board"
hx-target="#modal-container"
hx-swap="innerHTML"
>
New Issue
</button>
</div>
<div class="flex items-center gap-2">
<span class="text-sm font-medium">Sprint:</span>
<select
class="select select-sm select-bordered"
hx-get="/board/sprint"
hx-target="#board-columns-container"
hx-swap="innerHTML"
name="sprint"
>
if len(props.Sprints) == 0 {
<option value="0">No Sprints</option>
}
for _, sprintNum := range props.Sprints {
if sprintNum == props.CurrentSprint {
<option value={ fmt.Sprintf("%d", sprintNum) } selected>{ fmt.Sprintf("Sprint %d", sprintNum) }</option>
} else {
<option value={ fmt.Sprintf("%d", sprintNum) }>{ fmt.Sprintf("Sprint %d", sprintNum) }</option>
}
}
</select>
<button
class="btn btn-sm btn-outline"
hx-post="/board/sprint/new"
hx-target="#board-columns-container"
hx-swap="innerHTML"
>
New Sprint
</button>
</div>
</div>
</div>
<div id="board-columns-container" class="flex-1 overflow-x-auto p-4">
@BoardColumns(props)
</div>
</div>
}
templ BoardColumns(props BoardViewProps) {
{{
backlogIssues := props.BacklogIssues
todoIssues := []*models.Issue{}
inProgressIssues := []*models.Issue{}
blockedIssues := []*models.Issue{}
doneIssues := []*models.Issue{}
for _, issue := range props.SprintIssues {
switch issue.Status {
case "open":
todoIssues = append(todoIssues, issue)
case "in_progress":
inProgressIssues = append(inProgressIssues, issue)
case "blocked":
blockedIssues = append(blockedIssues, issue)
case "closed":
doneIssues = append(doneIssues, issue)
}
}
sprintName := "No Sprint"
if props.CurrentSprint > 0 {
sprintName = fmt.Sprintf("Sprint %d", props.CurrentSprint)
}
}}
<div class="flex gap-4 h-full" id="board-columns">
@BoardColumn("Backlog", "backlog", backlogIssues, "badge-ghost", "")
@BoardColumn(fmt.Sprintf("%s - To Do", sprintName), "todo", todoIssues, "badge-info", sprintName)
@BoardColumn(fmt.Sprintf("%s - In Progress", sprintName), "in_progress", inProgressIssues, "badge-warning", sprintName)
@BoardColumn(fmt.Sprintf("%s - Blocked", sprintName), "blocked", blockedIssues, "badge-error", sprintName)
@BoardColumn(fmt.Sprintf("%s - Done", sprintName), "done", doneIssues, "badge-success", sprintName)
</div>
}
templ BoardColumn(title string, status string, issues []*models.Issue, badgeClass string, sprintName string) {
<div class="flex-1 min-w-80 flex flex-col board-column" data-status={ status } data-sprint={ sprintName }>
<div class="flex items-center gap-2 mb-4">
<h2 class="text-lg font-semibold">{ title }</h2>
<span class={ "badge", badgeClass }>{ fmt.Sprintf("%d", len(issues)) }</span>
</div>
<div class="flex-1 space-y-2 bg-base-200 rounded-lg p-2 min-h-30 column-drop-zone" data-status={ status } data-sprint={ sprintName }>
if len(issues) == 0 {
<div class="text-center text-base-content/50 py-4 empty-drop-hint">Drop issues here</div>
}
for _, issue := range issues {
@BoardCard(issue)
}
</div>
</div>
}
templ BoardCard(issue *models.Issue) {
<div
class="card bg-base-100 shadow-sm hover:shadow-md transition-shadow cursor-grab active:cursor-grabbing board-card"
draggable="true"
data-issue-id={ issue.ID }
>
<div class="card-body p-4">
<div class="flex items-start justify-between gap-2">
<h3 class="card-title text-sm font-medium line-clamp-2 flex-1">{ issue.Title }</h3>
<div class="flex gap-1 shrink-0">
<a
class="btn btn-ghost btn-xs"
href={ "/issues/" + issue.ID }
title="View details"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</a>
<button
class="btn btn-ghost btn-xs"
hx-get={ "/issues/" + issue.ID + "/edit?from=board" }
hx-target="#modal-container"
hx-swap="innerHTML"
title="Edit issue"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
</div>
</div>
<div class="flex items-center gap-2 mt-2">
<span class="badge badge-sm badge-ghost font-mono">{ issue.ID }</span>
@components.TypeBadge(issue.IssueType)
@components.PriorityBadge(issue.Priority)
</div>
if issue.Description != "" {
<p class="text-sm text-base-content/70 line-clamp-2 mt-2">{ issue.Description }</p>
}
</div>
</div>
}