Implement report system and fix template task filtering
- Fix template task filtering bug: templates now hidden from all reports except 'template' and 'all' reports, even when using custom filters - Add support for status:template filter to explicitly show templates - Implement comprehensive report system with 12 predefined reports: * active - Started tasks * all - All tasks including templates * completed - Completed tasks * list - Pending tasks (default) * minimal - Pending tasks in minimal format * newest - Most recent pending tasks * oldest - Oldest pending tasks * overdue - Overdue tasks * ready - Tasks ready to work on * recurring - Pending recurring instances * template - Recurring template tasks * waiting - Hidden/waiting tasks - Replace list command with report-based architecture - Add configurable default_report option (defaults to 'list') - Add minimal display format (ID + description only) - Support flexible syntax: 'opal <report> [filters]' or 'opal [filters] <report>' - Add 'opal reports' command to list all available reports
This commit is contained in:
@@ -1,58 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.jnss.me/joakim/opal/internal/engine"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var listCmd = &cobra.Command{
|
|
||||||
Use: "list [filter...]",
|
|
||||||
Short: "List tasks",
|
|
||||||
Long: `List tasks matching the filter criteria.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
opal list # List all pending tasks
|
|
||||||
opal list +home # List tasks with +home tag
|
|
||||||
opal list project:backend # List backend project tasks
|
|
||||||
opal list priority:H # List high priority tasks
|
|
||||||
opal 2 list # List using filter 2 (flexible syntax)`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
parsed := getParsedArgs(cmd)
|
|
||||||
if err := listTasks(parsed.Filters); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func listTasks(args []string) error {
|
|
||||||
// Parse filter
|
|
||||||
var filter *engine.Filter
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if len(args) == 0 {
|
|
||||||
filter = engine.DefaultFilter()
|
|
||||||
} else {
|
|
||||||
filter, err = engine.ParseFilter(args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse filter: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build working set
|
|
||||||
ws, err := engine.BuildWorkingSet(filter)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to build working set: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get tasks
|
|
||||||
tasks := ws.GetTasks()
|
|
||||||
|
|
||||||
// Display
|
|
||||||
fmt.Println(engine.FormatTaskList(tasks, ws))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"git.jnss.me/joakim/opal/internal/engine"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateReportCommands generates commands for all reports dynamically
|
||||||
|
func CreateReportCommands() []*cobra.Command {
|
||||||
|
reports := engine.AllReports()
|
||||||
|
commands := make([]*cobra.Command, 0, len(reports))
|
||||||
|
|
||||||
|
// Create a command for each report
|
||||||
|
for name, report := range reports {
|
||||||
|
// Capture in closure
|
||||||
|
reportName := name
|
||||||
|
reportObj := report
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: reportName + " [filter...]",
|
||||||
|
Short: reportObj.Description,
|
||||||
|
Long: fmt.Sprintf("%s\n\nThis is a report that shows: %s", reportObj.Description, reportObj.Description),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
parsed := getParsedArgs(cmd)
|
||||||
|
if err := runReport(reportName, parsed.Filters); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
commands = append(commands, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands
|
||||||
|
}
|
||||||
|
|
||||||
|
// runReport executes a report by name with optional filters
|
||||||
|
func runReport(reportName string, filters []string) error {
|
||||||
|
// Get the report
|
||||||
|
report, err := engine.GetReport(reportName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the report
|
||||||
|
tasks, err := report.Execute(filters)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to execute report: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build working set for display IDs
|
||||||
|
ws, err := engine.BuildWorkingSet(report.BaseFilter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build working set: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display tasks based on format
|
||||||
|
var output string
|
||||||
|
if report.DisplayFormat == engine.DisplayFormatMinimal {
|
||||||
|
output = engine.FormatTaskListWithFormat(tasks, ws, "minimal")
|
||||||
|
} else {
|
||||||
|
output = engine.FormatTaskListWithFormat(tasks, ws, "table")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(output)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reportsCmd shows all available reports
|
||||||
|
var reportsCmd = &cobra.Command{
|
||||||
|
Use: "reports",
|
||||||
|
Short: "List all available reports",
|
||||||
|
Long: `Display a list of all available task reports with their descriptions.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
reports := engine.AllReports()
|
||||||
|
|
||||||
|
// Sort by name
|
||||||
|
names := make([]string, 0, len(reports))
|
||||||
|
for name := range reports {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
|
||||||
|
fmt.Println("Available reports:\n")
|
||||||
|
for _, name := range names {
|
||||||
|
report := reports[name]
|
||||||
|
fmt.Printf(" %-12s %s\n", name, report.Description)
|
||||||
|
}
|
||||||
|
fmt.Println("\nUsage: opal <report-name> [filters...]")
|
||||||
|
fmt.Println("Example: opal ready +home")
|
||||||
|
},
|
||||||
|
}
|
||||||
+44
-7
@@ -23,9 +23,16 @@ const parsedArgsKey contextKey = "parsedArgs"
|
|||||||
|
|
||||||
// Command classification
|
// Command classification
|
||||||
var commandNames = []string{
|
var commandNames = []string{
|
||||||
"add", "list", "done", "modify", "delete",
|
"add", "done", "modify", "delete",
|
||||||
"start", "stop", "count", "projects", "tags",
|
"start", "stop", "count", "projects", "tags",
|
||||||
"info", "edit", "server", "sync",
|
"info", "edit", "server", "sync", "reports",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report names (dynamically populated)
|
||||||
|
var reportNames = []string{
|
||||||
|
"active", "all", "completed", "list", "minimal",
|
||||||
|
"newest", "oldest", "overdue", "ready", "recurring",
|
||||||
|
"template", "waiting",
|
||||||
}
|
}
|
||||||
|
|
||||||
var commandsWithModifiers = map[string]bool{
|
var commandsWithModifiers = map[string]bool{
|
||||||
@@ -39,9 +46,22 @@ var rootCmd = &cobra.Command{
|
|||||||
Long: `Opal is a powerful command-line task manager inspired by taskwarrior.
|
Long: `Opal is a powerful command-line task manager inspired by taskwarrior.
|
||||||
It supports filtering, tags, priorities, projects, and recurring tasks.`,
|
It supports filtering, tags, priorities, projects, and recurring tasks.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
// Default behavior: list tasks
|
// Default behavior: run configured default report (defaults to "list")
|
||||||
parsed := getParsedArgs(cmd)
|
parsed := getParsedArgs(cmd)
|
||||||
if err := listTasks(parsed.Filters); err != nil {
|
|
||||||
|
// Get default report from config
|
||||||
|
cfg, err := engine.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultReport := cfg.DefaultReport
|
||||||
|
if defaultReport == "" {
|
||||||
|
defaultReport = "list"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runReport(defaultReport, parsed.Filters); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -102,11 +122,12 @@ func preprocessArgs(args []string) *ParsedArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find command position
|
// Find command position (check both regular commands and reports)
|
||||||
cmdIdx := -1
|
cmdIdx := -1
|
||||||
cmdName := ""
|
cmdName := ""
|
||||||
|
|
||||||
for i, arg := range args {
|
for i, arg := range args {
|
||||||
|
// Check regular commands
|
||||||
for _, name := range commandNames {
|
for _, name := range commandNames {
|
||||||
if arg == name {
|
if arg == name {
|
||||||
cmdIdx = i
|
cmdIdx = i
|
||||||
@@ -114,6 +135,16 @@ func preprocessArgs(args []string) *ParsedArgs {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Check report names
|
||||||
|
if cmdIdx < 0 {
|
||||||
|
for _, name := range reportNames {
|
||||||
|
if arg == name {
|
||||||
|
cmdIdx = i
|
||||||
|
cmdName = name
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if cmdIdx >= 0 {
|
if cmdIdx >= 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -159,9 +190,8 @@ func preprocessArgs(args []string) *ParsedArgs {
|
|||||||
func init() {
|
func init() {
|
||||||
cobra.OnInitialize(initializeApp)
|
cobra.OnInitialize(initializeApp)
|
||||||
|
|
||||||
// Add subcommands
|
// Add regular subcommands
|
||||||
rootCmd.AddCommand(addCmd)
|
rootCmd.AddCommand(addCmd)
|
||||||
rootCmd.AddCommand(listCmd)
|
|
||||||
rootCmd.AddCommand(doneCmd)
|
rootCmd.AddCommand(doneCmd)
|
||||||
rootCmd.AddCommand(modifyCmd)
|
rootCmd.AddCommand(modifyCmd)
|
||||||
rootCmd.AddCommand(deleteCmd)
|
rootCmd.AddCommand(deleteCmd)
|
||||||
@@ -172,6 +202,13 @@ func init() {
|
|||||||
rootCmd.AddCommand(tagsCmd)
|
rootCmd.AddCommand(tagsCmd)
|
||||||
rootCmd.AddCommand(infoCmd)
|
rootCmd.AddCommand(infoCmd)
|
||||||
rootCmd.AddCommand(editCmd)
|
rootCmd.AddCommand(editCmd)
|
||||||
|
rootCmd.AddCommand(reportsCmd)
|
||||||
|
|
||||||
|
// Add report commands dynamically
|
||||||
|
reportCommands := CreateReportCommands()
|
||||||
|
for _, cmd := range reportCommands {
|
||||||
|
rootCmd.AddCommand(cmd)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initializeApp() {
|
func initializeApp() {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
DefaultFilter string `mapstructure:"default_filter"`
|
DefaultFilter string `mapstructure:"default_filter"`
|
||||||
DefaultSort string `mapstructure:"default_sort"`
|
DefaultSort string `mapstructure:"default_sort"`
|
||||||
|
DefaultReport string `mapstructure:"default_report"`
|
||||||
ColorOutput bool `mapstructure:"color_output"`
|
ColorOutput bool `mapstructure:"color_output"`
|
||||||
WeekStartDay string `mapstructure:"week_start_day"`
|
WeekStartDay string `mapstructure:"week_start_day"`
|
||||||
DefaultDueTime string `mapstructure:"default_due_time"`
|
DefaultDueTime string `mapstructure:"default_due_time"`
|
||||||
@@ -83,6 +84,7 @@ func LoadConfig() (*Config, error) {
|
|||||||
// Set defaults
|
// Set defaults
|
||||||
v.SetDefault("default_filter", "status:pending")
|
v.SetDefault("default_filter", "status:pending")
|
||||||
v.SetDefault("default_sort", "due,priority")
|
v.SetDefault("default_sort", "due,priority")
|
||||||
|
v.SetDefault("default_report", "list")
|
||||||
v.SetDefault("color_output", true)
|
v.SetDefault("color_output", true)
|
||||||
v.SetDefault("week_start_day", "monday")
|
v.SetDefault("week_start_day", "monday")
|
||||||
v.SetDefault("default_due_time", "")
|
v.SetDefault("default_due_time", "")
|
||||||
@@ -131,6 +133,7 @@ func SaveConfig(cfg *Config) error {
|
|||||||
|
|
||||||
v.Set("default_filter", cfg.DefaultFilter)
|
v.Set("default_filter", cfg.DefaultFilter)
|
||||||
v.Set("default_sort", cfg.DefaultSort)
|
v.Set("default_sort", cfg.DefaultSort)
|
||||||
|
v.Set("default_report", cfg.DefaultReport)
|
||||||
v.Set("color_output", cfg.ColorOutput)
|
v.Set("color_output", cfg.ColorOutput)
|
||||||
v.Set("week_start_day", cfg.WeekStartDay)
|
v.Set("week_start_day", cfg.WeekStartDay)
|
||||||
v.Set("default_due_time", cfg.DefaultDueTime)
|
v.Set("default_due_time", cfg.DefaultDueTime)
|
||||||
|
|||||||
@@ -11,10 +11,35 @@ import (
|
|||||||
|
|
||||||
// FormatTaskList formats a list of tasks for display
|
// FormatTaskList formats a list of tasks for display
|
||||||
func FormatTaskList(tasks []*Task, ws *WorkingSet) string {
|
func FormatTaskList(tasks []*Task, ws *WorkingSet) string {
|
||||||
|
return FormatTaskListWithFormat(tasks, ws, "table")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatTaskListWithFormat formats a list of tasks with specified format
|
||||||
|
func FormatTaskListWithFormat(tasks []*Task, ws *WorkingSet, format string) string {
|
||||||
if len(tasks) == 0 {
|
if len(tasks) == 0 {
|
||||||
return "No tasks found."
|
return "No tasks found."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Minimal format: just ID and description
|
||||||
|
if format == "minimal" {
|
||||||
|
result := ""
|
||||||
|
for i, task := range tasks {
|
||||||
|
displayID := i + 1
|
||||||
|
if ws != nil {
|
||||||
|
// Use working set display ID if available
|
||||||
|
for id, uuid := range ws.byID {
|
||||||
|
if uuid == task.UUID {
|
||||||
|
displayID = id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result += fmt.Sprintf("%3d %s\n", displayID, task.Description)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table format (default)
|
||||||
t := table.NewWriter()
|
t := table.NewWriter()
|
||||||
|
|
||||||
// Configure style
|
// Configure style
|
||||||
|
|||||||
@@ -70,12 +70,38 @@ func (f *Filter) ToSQL() (string, []interface{}) {
|
|||||||
conditions := []string{}
|
conditions := []string{}
|
||||||
args := []interface{}{}
|
args := []interface{}{}
|
||||||
|
|
||||||
|
// Track if we have an explicit status filter
|
||||||
|
hasStatusFilter := false
|
||||||
|
|
||||||
// Status filter
|
// Status filter
|
||||||
if status, ok := f.Attributes["status"]; ok {
|
if status, ok := f.Attributes["status"]; ok {
|
||||||
|
hasStatusFilter = true
|
||||||
|
|
||||||
|
// Handle special "template" status
|
||||||
|
if status == "template" {
|
||||||
|
// Templates are: status=recurring AND parent_uuid IS NULL AND recurrence_duration IS NOT NULL
|
||||||
|
conditions = append(conditions, "status = ? AND parent_uuid IS NULL AND recurrence_duration IS NOT NULL")
|
||||||
|
args = append(args, byte(StatusRecurring))
|
||||||
|
} else {
|
||||||
statusByte := statusStringToByte(status)
|
statusByte := statusStringToByte(status)
|
||||||
conditions = append(conditions, "status = ?")
|
conditions = append(conditions, "status = ?")
|
||||||
args = append(args, statusByte)
|
args = append(args, statusByte)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implicit template exclusion (Option A):
|
||||||
|
// Exclude recurring templates UNLESS:
|
||||||
|
// - Filter explicitly includes status (any status, including "template")
|
||||||
|
// - This is an "all" report (marked with _all=true)
|
||||||
|
if !hasStatusFilter {
|
||||||
|
// Check if this is the "all" report
|
||||||
|
if _, isAllReport := f.Attributes["_all"]; !isAllReport {
|
||||||
|
// No explicit status filter and not "all" report - exclude templates
|
||||||
|
// Templates have status=recurring AND parent_uuid IS NULL
|
||||||
|
conditions = append(conditions, "(status != ? OR parent_uuid IS NOT NULL)")
|
||||||
|
args = append(args, byte(StatusRecurring))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Project filter
|
// Project filter
|
||||||
if project, ok := f.Attributes["project"]; ok {
|
if project, ok := f.Attributes["project"]; ok {
|
||||||
|
|||||||
@@ -0,0 +1,370 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DisplayFormat defines how tasks should be displayed
|
||||||
|
type DisplayFormat string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DisplayFormatTable DisplayFormat = "table"
|
||||||
|
DisplayFormatMinimal DisplayFormat = "minimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Report defines a named task report with filters and display options
|
||||||
|
type Report struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
BaseFilter *Filter // Base filter that cannot be overridden
|
||||||
|
DisplayFormat DisplayFormat // How to display results
|
||||||
|
SortFunc func([]*Task) []*Task
|
||||||
|
LimitFunc func([]*Task) []*Task
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllReports returns all predefined reports
|
||||||
|
func AllReports() map[string]*Report {
|
||||||
|
return map[string]*Report{
|
||||||
|
"active": ActiveReport(),
|
||||||
|
"all": AllReport(),
|
||||||
|
"completed": CompletedReport(),
|
||||||
|
"list": ListReport(),
|
||||||
|
"minimal": MinimalReport(),
|
||||||
|
"newest": NewestReport(),
|
||||||
|
"oldest": OldestReport(),
|
||||||
|
"overdue": OverdueReport(),
|
||||||
|
"ready": ReadyReport(),
|
||||||
|
"recurring": RecurringReport(),
|
||||||
|
"template": TemplateReport(),
|
||||||
|
"waiting": WaitingReport(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReport retrieves a report by name
|
||||||
|
func GetReport(name string) (*Report, error) {
|
||||||
|
reports := AllReports()
|
||||||
|
report, exists := reports[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("unknown report: %s", name)
|
||||||
|
}
|
||||||
|
return report, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveReport shows started tasks
|
||||||
|
func ActiveReport() *Report {
|
||||||
|
filter := NewFilter()
|
||||||
|
filter.Attributes["status"] = "pending"
|
||||||
|
filter.Attributes["_started"] = "true" // Special marker for tasks with start time
|
||||||
|
|
||||||
|
return &Report{
|
||||||
|
Name: "active",
|
||||||
|
Description: "Started tasks",
|
||||||
|
BaseFilter: filter,
|
||||||
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllReport shows all tasks (no status filter)
|
||||||
|
func AllReport() *Report {
|
||||||
|
filter := NewFilter()
|
||||||
|
filter.Attributes["_all"] = "true" // Special marker to include templates
|
||||||
|
|
||||||
|
return &Report{
|
||||||
|
Name: "all",
|
||||||
|
Description: "All tasks",
|
||||||
|
BaseFilter: filter,
|
||||||
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompletedReport shows completed tasks
|
||||||
|
func CompletedReport() *Report {
|
||||||
|
filter := NewFilter()
|
||||||
|
filter.Attributes["status"] = "completed"
|
||||||
|
|
||||||
|
return &Report{
|
||||||
|
Name: "completed",
|
||||||
|
Description: "Completed tasks",
|
||||||
|
BaseFilter: filter,
|
||||||
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListReport shows pending tasks (default report)
|
||||||
|
func ListReport() *Report {
|
||||||
|
filter := NewFilter()
|
||||||
|
filter.Attributes["status"] = "pending"
|
||||||
|
|
||||||
|
return &Report{
|
||||||
|
Name: "list",
|
||||||
|
Description: "Pending tasks",
|
||||||
|
BaseFilter: filter,
|
||||||
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinimalReport shows pending tasks in minimal format
|
||||||
|
func MinimalReport() *Report {
|
||||||
|
filter := NewFilter()
|
||||||
|
filter.Attributes["status"] = "pending"
|
||||||
|
|
||||||
|
return &Report{
|
||||||
|
Name: "minimal",
|
||||||
|
Description: "Pending tasks (minimal format)",
|
||||||
|
BaseFilter: filter,
|
||||||
|
DisplayFormat: DisplayFormatMinimal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewestReport shows most recent pending tasks
|
||||||
|
func NewestReport() *Report {
|
||||||
|
filter := NewFilter()
|
||||||
|
filter.Attributes["status"] = "pending"
|
||||||
|
|
||||||
|
return &Report{
|
||||||
|
Name: "newest",
|
||||||
|
Description: "Most recent pending tasks",
|
||||||
|
BaseFilter: filter,
|
||||||
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
SortFunc: func(tasks []*Task) []*Task {
|
||||||
|
// Sort by created descending
|
||||||
|
sorted := make([]*Task, len(tasks))
|
||||||
|
copy(sorted, tasks)
|
||||||
|
for i := 0; i < len(sorted)-1; i++ {
|
||||||
|
for j := i + 1; j < len(sorted); j++ {
|
||||||
|
if sorted[i].Created.Before(sorted[j].Created) {
|
||||||
|
sorted[i], sorted[j] = sorted[j], sorted[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sorted
|
||||||
|
},
|
||||||
|
LimitFunc: func(tasks []*Task) []*Task {
|
||||||
|
if len(tasks) > 10 {
|
||||||
|
return tasks[:10]
|
||||||
|
}
|
||||||
|
return tasks
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OldestReport shows oldest pending tasks
|
||||||
|
func OldestReport() *Report {
|
||||||
|
filter := NewFilter()
|
||||||
|
filter.Attributes["status"] = "pending"
|
||||||
|
|
||||||
|
return &Report{
|
||||||
|
Name: "oldest",
|
||||||
|
Description: "Oldest pending tasks",
|
||||||
|
BaseFilter: filter,
|
||||||
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
SortFunc: func(tasks []*Task) []*Task {
|
||||||
|
// Sort by created ascending (already default, but explicit)
|
||||||
|
sorted := make([]*Task, len(tasks))
|
||||||
|
copy(sorted, tasks)
|
||||||
|
for i := 0; i < len(sorted)-1; i++ {
|
||||||
|
for j := i + 1; j < len(sorted); j++ {
|
||||||
|
if sorted[i].Created.After(sorted[j].Created) {
|
||||||
|
sorted[i], sorted[j] = sorted[j], sorted[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sorted
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OverdueReport shows overdue tasks
|
||||||
|
func OverdueReport() *Report {
|
||||||
|
filter := NewFilter()
|
||||||
|
filter.Attributes["status"] = "pending"
|
||||||
|
filter.Attributes["_overdue"] = "true" // Special marker for overdue tasks
|
||||||
|
|
||||||
|
return &Report{
|
||||||
|
Name: "overdue",
|
||||||
|
Description: "Overdue tasks",
|
||||||
|
BaseFilter: filter,
|
||||||
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadyReport shows tasks ready to work on
|
||||||
|
func ReadyReport() *Report {
|
||||||
|
filter := NewFilter()
|
||||||
|
filter.Attributes["status"] = "pending"
|
||||||
|
filter.Attributes["_ready"] = "true" // Special marker for ready tasks
|
||||||
|
|
||||||
|
return &Report{
|
||||||
|
Name: "ready",
|
||||||
|
Description: "Tasks ready to work on",
|
||||||
|
BaseFilter: filter,
|
||||||
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecurringReport shows pending recurring instances
|
||||||
|
func RecurringReport() *Report {
|
||||||
|
filter := NewFilter()
|
||||||
|
filter.Attributes["status"] = "pending"
|
||||||
|
filter.Attributes["_recurring_instance"] = "true" // Special marker for recurring instances
|
||||||
|
|
||||||
|
return &Report{
|
||||||
|
Name: "recurring",
|
||||||
|
Description: "Pending recurring task instances",
|
||||||
|
BaseFilter: filter,
|
||||||
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateReport shows recurring template tasks
|
||||||
|
func TemplateReport() *Report {
|
||||||
|
filter := NewFilter()
|
||||||
|
filter.Attributes["status"] = "template" // Special status for templates
|
||||||
|
|
||||||
|
return &Report{
|
||||||
|
Name: "template",
|
||||||
|
Description: "Recurring template tasks",
|
||||||
|
BaseFilter: filter,
|
||||||
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitingReport shows waiting/hidden tasks
|
||||||
|
func WaitingReport() *Report {
|
||||||
|
filter := NewFilter()
|
||||||
|
filter.Attributes["status"] = "pending"
|
||||||
|
filter.Attributes["_waiting"] = "true" // Special marker for waiting tasks
|
||||||
|
|
||||||
|
return &Report{
|
||||||
|
Name: "waiting",
|
||||||
|
Description: "Hidden/waiting tasks",
|
||||||
|
BaseFilter: filter,
|
||||||
|
DisplayFormat: DisplayFormatTable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs the report with optional additional filters
|
||||||
|
func (r *Report) Execute(additionalFilters []string) ([]*Task, error) {
|
||||||
|
// Start with base filter
|
||||||
|
filter := r.BaseFilter
|
||||||
|
|
||||||
|
// Merge additional filters if provided
|
||||||
|
if len(additionalFilters) > 0 {
|
||||||
|
userFilter, err := ParseFilter(additionalFilters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse additional filters: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge filters (user filters add to base, but cannot override status)
|
||||||
|
filter = mergeFilters(r.BaseFilter, userFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get tasks
|
||||||
|
tasks, err := GetTasks(filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply post-filter for special markers
|
||||||
|
tasks = r.applyPostFilters(tasks)
|
||||||
|
|
||||||
|
// Apply sorting if defined
|
||||||
|
if r.SortFunc != nil {
|
||||||
|
tasks = r.SortFunc(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply limit if defined
|
||||||
|
if r.LimitFunc != nil {
|
||||||
|
tasks = r.LimitFunc(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyPostFilters applies special filters that can't be done in SQL
|
||||||
|
func (r *Report) applyPostFilters(tasks []*Task) []*Task {
|
||||||
|
now := timeNow()
|
||||||
|
filtered := []*Task{}
|
||||||
|
|
||||||
|
for _, task := range tasks {
|
||||||
|
include := true
|
||||||
|
|
||||||
|
// Check for _started marker
|
||||||
|
if r.BaseFilter.Attributes["_started"] == "true" {
|
||||||
|
if task.Start == nil {
|
||||||
|
include = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for _overdue marker
|
||||||
|
if r.BaseFilter.Attributes["_overdue"] == "true" {
|
||||||
|
if task.Due == nil || !task.Due.Before(now) {
|
||||||
|
include = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for _ready marker
|
||||||
|
if r.BaseFilter.Attributes["_ready"] == "true" {
|
||||||
|
// Task is ready if scheduled and wait are either null or in the past
|
||||||
|
if task.Scheduled != nil && task.Scheduled.After(now) {
|
||||||
|
include = false
|
||||||
|
}
|
||||||
|
if task.Wait != nil && task.Wait.After(now) {
|
||||||
|
include = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for _waiting marker
|
||||||
|
if r.BaseFilter.Attributes["_waiting"] == "true" {
|
||||||
|
if task.Wait == nil || !task.Wait.After(now) {
|
||||||
|
include = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for _recurring_instance marker
|
||||||
|
if r.BaseFilter.Attributes["_recurring_instance"] == "true" {
|
||||||
|
if task.ParentUUID == nil {
|
||||||
|
include = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if include {
|
||||||
|
filtered = append(filtered, task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeFilters combines base filter with user filter
|
||||||
|
func mergeFilters(base, user *Filter) *Filter {
|
||||||
|
merged := NewFilter()
|
||||||
|
|
||||||
|
// Copy base attributes (these take precedence)
|
||||||
|
for k, v := range base.Attributes {
|
||||||
|
merged.Attributes[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user attributes (but don't override status or special markers)
|
||||||
|
for k, v := range user.Attributes {
|
||||||
|
if k != "status" && k[0] != '_' {
|
||||||
|
merged.Attributes[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge tags
|
||||||
|
merged.IncludeTags = append(merged.IncludeTags, base.IncludeTags...)
|
||||||
|
merged.IncludeTags = append(merged.IncludeTags, user.IncludeTags...)
|
||||||
|
|
||||||
|
merged.ExcludeTags = append(merged.ExcludeTags, base.ExcludeTags...)
|
||||||
|
merged.ExcludeTags = append(merged.ExcludeTags, user.ExcludeTags...)
|
||||||
|
|
||||||
|
// Merge IDs and UUIDs
|
||||||
|
merged.IDs = append(merged.IDs, base.IDs...)
|
||||||
|
merged.IDs = append(merged.IDs, user.IDs...)
|
||||||
|
|
||||||
|
merged.UUIDs = append(merged.UUIDs, base.UUIDs...)
|
||||||
|
merged.UUIDs = append(merged.UUIDs, user.UUIDs...)
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user