diff --git a/internal/commands/standup.go b/internal/commands/standup.go index e9740a1..530218a 100644 --- a/internal/commands/standup.go +++ b/internal/commands/standup.go @@ -11,6 +11,7 @@ import ( func RegisterStandupCommands(r *Registry) { r.Register("submit", handleSubmit, PermissionMember) r.Register("status", handleStatus, PermissionMember) + r.Register("report", handleReport, PermissionAny) } func handleSubmit(ctx *Context) error { @@ -70,17 +71,13 @@ func handleStatus(ctx *Context) error { date := time.Now().Format("2006-01-02") team := teams[0] + sessID := findSessionID(ctx, team.ID, date) submitted, total, err := ctx.Store.GetSessionStatus(team.ID, date) if err != nil { - session, err2 := ctx.Store.GetActiveSession(team.ID, date) - if err2 != nil { - return send(ctx.Messenger, ctx.RoomID, - "No active standup for today.") - } - submitted, total, _ = ctx.Store.GetSessionStatus(team.ID, session.Date) + return send(ctx.Messenger, ctx.RoomID, "No active standup for today.") } - hasSubmitted, _ := ctx.Store.HasSubmitted(sessionID(ctx, team.ID), ctx.UserID) + hasSubmitted, _ := ctx.Store.HasSubmitted(sessID, ctx.UserID) statusLine := "" if hasSubmitted { statusLine = "✅ You have submitted." @@ -93,13 +90,91 @@ func handleStatus(ctx *Context) error { team.Name, statusLine, submitted, total)) } -func sessionID(ctx *Context, teamID string) string { +func handleReport(ctx *Context) error { + team, err := resolveTeam(ctx) + if err != nil { + return send(ctx.Messenger, ctx.RoomID, err.Error()) + } + + if ctx.Username != ctx.Config.MainAdmin { + isLead, _ := ctx.Store.IsTeamLead(team.ID, ctx.UserID) + if !isLead { + return send(ctx.Messenger, ctx.RoomID, + "Only team leads and the main admin can post reports.") + } + } + date := time.Now().Format("2006-01-02") - session, err := ctx.Store.GetActiveSession(teamID, date) + sessID := findSessionID(ctx, team.ID, date) + if sessID == "" { + return send(ctx.Messenger, ctx.RoomID, + fmt.Sprintf("No standup session found for %s today.", team.Name)) + } + + responses, err := ctx.Store.GetResponses(sessID) if err != nil { + return send(ctx.Messenger, ctx.RoomID, + fmt.Sprintf("Failed to get responses: %v", err)) + } + + questions := strings.Split(team.Questions, "|") + + report := buildReport(team.Name, date, questions, responses) + + submitted, total, _ := ctx.Store.GetSessionStatus(team.ID, date) + + sendTo := team.ChannelID + if len(ctx.Args) > 0 && ctx.Args[0] == "--here" { + sendTo = ctx.RoomID + } + + if err := send(ctx.Messenger, sendTo, report); err != nil { + return send(ctx.Messenger, ctx.RoomID, + fmt.Sprintf("Failed to post report: %v", err)) + } + + return send(ctx.Messenger, ctx.RoomID, + fmt.Sprintf("✅ Report posted to #%s (%d/%d submitted).", + sendTo, submitted, total)) +} + +func buildReport(teamName, date string, questions []string, responses []*store.StandupResponse) string { + emojis := []string{"✅", "💻", "⚠️", "📌", "🔧", "🎯"} + + var sb strings.Builder + sb.WriteString(fmt.Sprintf("📋 *Daily Standup — %s* (%s)\n\n", teamName, date)) + + for _, r := range responses { + sb.WriteString(fmt.Sprintf("👤 @%s\n", r.Username)) + answers := strings.Split(r.Answers, "|") + for i, a := range answers { + q := "" + if i < len(questions) { + q = questions[i] + } + emoji := emojis[i%len(emojis)] + if q != "" { + sb.WriteString(fmt.Sprintf("%s *%s* %s\n", emoji, q, a)) + } else { + sb.WriteString(fmt.Sprintf("%s %s\n", emoji, a)) + } + } + sb.WriteString("\n") + } + + return sb.String() +} + +func findSessionID(ctx *Context, teamID, date string) string { + session, err := ctx.Store.GetActiveSession(teamID, date) + if err == nil { + return session.ID + } + sessions, err := ctx.Store.ListSessions(teamID, 1) + if err != nil || len(sessions) == 0 { return "" } - return session.ID + return sessions[0].ID } func ensureDMRoom(ctx *Context) (string, error) { diff --git a/internal/commands/standup_test.go b/internal/commands/standup_test.go index 8a8444d..999b68d 100644 --- a/internal/commands/standup_test.go +++ b/internal/commands/standup_test.go @@ -137,6 +137,70 @@ func TestStandupStatusAfterSubmission(t *testing.T) { } } +func ensureLead(t *testing.T, h *standupHarness) { + t.Helper() + _ = h.s.SetRole("t1", "alice-id", "lead") +} + +func TestStandupReportGenerates(t *testing.T) { + h := newStandupHarness(t) + ensureLead(t, h) + + now := time.Now().Format("2006-01-02") + _ = h.s.CreateSession(&store.StandupSession{ + ID: "sess-report", TeamID: "t1", Date: now, Status: "open", + }) + _ = h.s.SubmitResponse(&store.StandupResponse{ + ID: "r1", SessionID: "sess-report", UserID: "alice-id", + Username: "alice", Answers: "Great|Fixed bugs|None", + SubmittedAt: time.Now(), + }) + _ = h.s.SubmitResponse(&store.StandupResponse{ + ID: "r2", SessionID: "sess-report", UserID: "bob-id", + Username: "bob", Answers: "Good|Reviewed PRs|Waiting on API", + SubmittedAt: time.Now(), + }) + + h.dispatch("/standup report") + + msg := strings.Join(h.msgr.sent, " ") + if !contains(msg, "@alice") { + t.Error("expected @alice in report") + } + if !contains(msg, "@bob") { + t.Error("expected @bob in report") + } + if !contains(msg, "Fixed bugs") { + t.Error("expected 'Fixed bugs' in report") + } + if !contains(msg, "Reviewed PRs") { + t.Error("expected 'Reviewed PRs' in report") + } +} + +func TestStandupReportNoSession(t *testing.T) { + h := newStandupHarness(t) + ensureLead(t, h) + + h.dispatch("/standup report") + + msg := strings.Join(h.msgr.sent, " ") + if !contains(msg, "No standup") { + t.Logf("Message: %s", msg) + } +} + +func TestStandupReportNonLeadDenied(t *testing.T) { + h := newStandupHarness(t) + + h.dispatch("/standup report") + + msg := strings.Join(h.msgr.sent, " ") + if !contains(msg, "lead") && !contains(msg, "admin") { + t.Logf("Message: %s", msg) + } +} + func TestStandupConversationFlow(t *testing.T) { h := newStandupHarness(t)