1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
package handlers
import (
"time"
"task-dashboard/internal/models"
)
// ComputeBudgetPeriod is a pure function: given the weekly availability
// template, calendar events, and candidate tasks, it returns the scheduled
// (tracked, incomplete, due-in-window) load versus the available minutes
// in [start, end) -- availability minus any overlapping calendar events.
// Never mutates its inputs and never drives scheduling decisions; it only
// answers "does this fit."
func ComputeBudgetPeriod(
blocks []models.AvailabilityBlock,
events []models.CalendarEvent,
tasks []models.Task,
trackedProjects map[string]bool,
trackedLabels map[string]bool,
start, end time.Time,
) models.BudgetPeriod {
available := 0
for day := start; day.Before(end); day = day.AddDate(0, 0, 1) {
weekday := int(day.Weekday())
for _, block := range blocks {
if block.Weekday != weekday {
continue
}
blockStart, blockEnd, ok := blockTimesOnDay(block, day)
if !ok {
continue
}
minutes := int(blockEnd.Sub(blockStart).Minutes())
for _, event := range events {
minutes -= overlapMinutes(blockStart, blockEnd, event.Start, event.End)
}
if minutes > 0 {
available += minutes
}
}
}
scheduled := 0
for _, task := range tasks {
if task.Completed || task.DueDate == nil {
continue
}
if !task.DueDate.Before(end) {
continue
}
if !isBudgetTracked(task, trackedProjects, trackedLabels) {
continue
}
scheduled += task.EstimatedMinutes
}
return models.BudgetPeriod{ScheduledMinutes: scheduled, AvailableMinutes: available}
}
// blockTimesOnDay resolves an availability block's "HH:MM" start/end
// strings to concrete times on the given day. ok is false if either time
// fails to parse (a malformed block is skipped rather than panicking).
func blockTimesOnDay(block models.AvailabilityBlock, day time.Time) (time.Time, time.Time, bool) {
start, err := time.ParseInLocation("15:04", block.StartTime, day.Location())
if err != nil {
return time.Time{}, time.Time{}, false
}
end, err := time.ParseInLocation("15:04", block.EndTime, day.Location())
if err != nil {
return time.Time{}, time.Time{}, false
}
y, m, d := day.Date()
return time.Date(y, m, d, start.Hour(), start.Minute(), 0, 0, day.Location()),
time.Date(y, m, d, end.Hour(), end.Minute(), 0, 0, day.Location()), true
}
// overlapMinutes returns how many minutes [bStart, bEnd) and [eStart, eEnd) overlap.
func overlapMinutes(bStart, bEnd, eStart, eEnd time.Time) int {
lo := bStart
if eStart.After(lo) {
lo = eStart
}
hi := bEnd
if eEnd.Before(hi) {
hi = eEnd
}
if hi.Before(lo) || hi.Equal(lo) {
return 0
}
return int(hi.Sub(lo).Minutes())
}
// isBudgetTracked reports whether task counts against any budget
// calculation -- true if its project or any of its labels is opted in.
func isBudgetTracked(task models.Task, trackedProjects, trackedLabels map[string]bool) bool {
if trackedProjects[task.ProjectID] {
return true
}
for _, label := range task.Labels {
if trackedLabels[label] {
return true
}
}
return false
}
|