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:
@@ -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