diff --git a/packages/core/types/project.ts b/packages/core/types/project.ts
index d81d72440..868be238c 100644
--- a/packages/core/types/project.ts
+++ b/packages/core/types/project.ts
@@ -14,6 +14,8 @@ export interface Project {
lead_id: string | null;
created_at: string;
updated_at: string;
+ issue_count: number;
+ done_count: number;
}
export interface CreateProjectRequest {
diff --git a/packages/views/projects/components/projects-page.tsx b/packages/views/projects/components/projects-page.tsx
index 20531ced5..7c76d057d 100644
--- a/packages/views/projects/components/projects-page.tsx
+++ b/packages/views/projects/components/projects-page.tsx
@@ -75,6 +75,25 @@ function ProjectRow({ project }: { project: Project }) {
{statusCfg.label}
+ {/* Progress */}
+
+ {project.issue_count > 0 ? (
+ <>
+
+
+
+
+ {project.done_count}/{project.issue_count}
+
+ >
+ ) : (
+ --
+ )}
+
+
{/* Lead */}
{project.lead_type && project.lead_id ? (
@@ -443,6 +462,7 @@ export function ProjectsPage() {
Name
Priority
Status
+ Progress
Lead
Created
diff --git a/server/internal/handler/project.go b/server/internal/handler/project.go
index c87db0a1e..92b96dca6 100644
--- a/server/internal/handler/project.go
+++ b/server/internal/handler/project.go
@@ -23,6 +23,8 @@ type ProjectResponse struct {
LeadID *string `json:"lead_id"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
+ IssueCount int64 `json:"issue_count"`
+ DoneCount int64 `json:"done_count"`
}
func projectToResponse(p db.Project) ProjectResponse {
@@ -80,9 +82,29 @@ func (h *Handler) ListProjects(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusInternalServerError, "failed to list projects")
return
}
+
+ // Batch-fetch issue stats for all projects
+ statsMap := make(map[string]db.GetProjectIssueStatsRow)
+ if len(projects) > 0 {
+ projectIDs := make([]pgtype.UUID, len(projects))
+ for i, p := range projects {
+ projectIDs[i] = p.ID
+ }
+ stats, err := h.Queries.GetProjectIssueStats(r.Context(), projectIDs)
+ if err == nil {
+ for _, s := range stats {
+ statsMap[uuidToString(s.ProjectID)] = s
+ }
+ }
+ }
+
resp := make([]ProjectResponse, len(projects))
for i, p := range projects {
resp[i] = projectToResponse(p)
+ if s, ok := statsMap[resp[i].ID]; ok {
+ resp[i].IssueCount = s.TotalCount
+ resp[i].DoneCount = s.DoneCount
+ }
}
writeJSON(w, http.StatusOK, map[string]any{"projects": resp, "total": len(resp)})
}
diff --git a/server/pkg/db/generated/project.sql.go b/server/pkg/db/generated/project.sql.go
index 3cd46272d..2147e1479 100644
--- a/server/pkg/db/generated/project.sql.go
+++ b/server/pkg/db/generated/project.sql.go
@@ -133,6 +133,41 @@ func (q *Queries) GetProjectInWorkspace(ctx context.Context, arg GetProjectInWor
return i, err
}
+const getProjectIssueStats = `-- name: GetProjectIssueStats :many
+SELECT project_id,
+ count(*)::bigint AS total_count,
+ count(*) FILTER (WHERE status IN ('done', 'cancelled'))::bigint AS done_count
+FROM issue
+WHERE project_id = ANY($1::uuid[])
+GROUP BY project_id
+`
+
+type GetProjectIssueStatsRow struct {
+ ProjectID pgtype.UUID `json:"project_id"`
+ TotalCount int64 `json:"total_count"`
+ DoneCount int64 `json:"done_count"`
+}
+
+func (q *Queries) GetProjectIssueStats(ctx context.Context, projectIds []pgtype.UUID) ([]GetProjectIssueStatsRow, error) {
+ rows, err := q.db.Query(ctx, getProjectIssueStats, projectIds)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []GetProjectIssueStatsRow{}
+ for rows.Next() {
+ var i GetProjectIssueStatsRow
+ if err := rows.Scan(&i.ProjectID, &i.TotalCount, &i.DoneCount); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
const listProjects = `-- name: ListProjects :many
SELECT id, workspace_id, title, description, icon, status, lead_type, lead_id, created_at, updated_at, priority FROM project
WHERE workspace_id = $1
diff --git a/server/pkg/db/queries/project.sql b/server/pkg/db/queries/project.sql
index 7d882fcb3..c126d56ae 100644
--- a/server/pkg/db/queries/project.sql
+++ b/server/pkg/db/queries/project.sql
@@ -40,3 +40,11 @@ DELETE FROM project WHERE id = $1;
-- name: CountIssuesByProject :one
SELECT count(*) FROM issue
WHERE project_id = $1;
+
+-- name: GetProjectIssueStats :many
+SELECT project_id,
+ count(*)::bigint AS total_count,
+ count(*) FILTER (WHERE status IN ('done', 'cancelled'))::bigint AS done_count
+FROM issue
+WHERE project_id = ANY(sqlc.arg('project_ids')::uuid[])
+GROUP BY project_id;