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
|
||||
var commandNames = []string{
|
||||
"add", "list", "done", "modify", "delete",
|
||||
"add", "done", "modify", "delete",
|
||||
"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{
|
||||
@@ -39,9 +46,22 @@ var rootCmd = &cobra.Command{
|
||||
Long: `Opal is a powerful command-line task manager inspired by taskwarrior.
|
||||
It supports filtering, tags, priorities, projects, and recurring tasks.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Default behavior: list tasks
|
||||
// Default behavior: run configured default report (defaults to "list")
|
||||
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)
|
||||
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
|
||||
cmdName := ""
|
||||
|
||||
for i, arg := range args {
|
||||
// Check regular commands
|
||||
for _, name := range commandNames {
|
||||
if arg == name {
|
||||
cmdIdx = i
|
||||
@@ -114,6 +135,16 @@ func preprocessArgs(args []string) *ParsedArgs {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Check report names
|
||||
if cmdIdx < 0 {
|
||||
for _, name := range reportNames {
|
||||
if arg == name {
|
||||
cmdIdx = i
|
||||
cmdName = name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if cmdIdx >= 0 {
|
||||
break
|
||||
}
|
||||
@@ -159,9 +190,8 @@ func preprocessArgs(args []string) *ParsedArgs {
|
||||
func init() {
|
||||
cobra.OnInitialize(initializeApp)
|
||||
|
||||
// Add subcommands
|
||||
// Add regular subcommands
|
||||
rootCmd.AddCommand(addCmd)
|
||||
rootCmd.AddCommand(listCmd)
|
||||
rootCmd.AddCommand(doneCmd)
|
||||
rootCmd.AddCommand(modifyCmd)
|
||||
rootCmd.AddCommand(deleteCmd)
|
||||
@@ -172,6 +202,13 @@ func init() {
|
||||
rootCmd.AddCommand(tagsCmd)
|
||||
rootCmd.AddCommand(infoCmd)
|
||||
rootCmd.AddCommand(editCmd)
|
||||
rootCmd.AddCommand(reportsCmd)
|
||||
|
||||
// Add report commands dynamically
|
||||
reportCommands := CreateReportCommands()
|
||||
for _, cmd := range reportCommands {
|
||||
rootCmd.AddCommand(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func initializeApp() {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
type Config struct {
|
||||
DefaultFilter string `mapstructure:"default_filter"`
|
||||
DefaultSort string `mapstructure:"default_sort"`
|
||||
DefaultReport string `mapstructure:"default_report"`
|
||||
ColorOutput bool `mapstructure:"color_output"`
|
||||
WeekStartDay string `mapstructure:"week_start_day"`
|
||||
DefaultDueTime string `mapstructure:"default_due_time"`
|
||||
@@ -83,6 +84,7 @@ func LoadConfig() (*Config, error) {
|
||||
// Set defaults
|
||||
v.SetDefault("default_filter", "status:pending")
|
||||
v.SetDefault("default_sort", "due,priority")
|
||||
v.SetDefault("default_report", "list")
|
||||
v.SetDefault("color_output", true)
|
||||
v.SetDefault("week_start_day", "monday")
|
||||
v.SetDefault("default_due_time", "")
|
||||
@@ -131,6 +133,7 @@ func SaveConfig(cfg *Config) error {
|
||||
|
||||
v.Set("default_filter", cfg.DefaultFilter)
|
||||
v.Set("default_sort", cfg.DefaultSort)
|
||||
v.Set("default_report", cfg.DefaultReport)
|
||||
v.Set("color_output", cfg.ColorOutput)
|
||||
v.Set("week_start_day", cfg.WeekStartDay)
|
||||
v.Set("default_due_time", cfg.DefaultDueTime)
|
||||
|
||||
@@ -11,10 +11,35 @@ import (
|
||||
|
||||
// FormatTaskList formats a list of tasks for display
|
||||
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 {
|
||||
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()
|
||||
|
||||
// Configure style
|
||||
|
||||
@@ -70,11 +70,37 @@ func (f *Filter) ToSQL() (string, []interface{}) {
|
||||
conditions := []string{}
|
||||
args := []interface{}{}
|
||||
|
||||
// Track if we have an explicit status filter
|
||||
hasStatusFilter := false
|
||||
|
||||
// Status filter
|
||||
if status, ok := f.Attributes["status"]; ok {
|
||||
statusByte := statusStringToByte(status)
|
||||
conditions = append(conditions, "status = ?")
|
||||
args = append(args, statusByte)
|
||||
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)
|
||||
conditions = append(conditions, "status = ?")
|
||||
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
|
||||
|
||||
@@ -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