1use crate::common::ICONS;
2use crate::ids::ThingsId;
3use crate::store::{Task, ThingsStore};
4use crate::ui::components::empty_text::EmptyText;
5use crate::ui::components::task_group::{TaskGroup, TaskGroupHeader};
6use crate::ui::components::tasks::{TaskList, TaskOptions};
7use iocraft::prelude::*;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11const LIST_INDENT: u32 = 2;
12
13#[derive(Default)]
14struct AreaGroup<'a> {
15 tasks: Vec<&'a Task>,
16}
17
18#[derive(Default)]
19struct GroupedSection<'a> {
20 unscoped: Vec<&'a Task>,
21 by_project: Vec<(ThingsId, Vec<&'a Task>)>,
22 by_area: Vec<(ThingsId, AreaGroup<'a>)>,
23}
24
25fn header_text(items: &[Task]) -> String {
26 let project_count = items.iter().filter(|task| task.is_project()).count();
27 let task_count = items.iter().filter(|task| !task.is_project()).count();
28 if project_count > 0 {
29 let label = if project_count == 1 {
30 "project"
31 } else {
32 "projects"
33 };
34 format!(
35 "{} Today ({} tasks, {} {})",
36 ICONS.today, task_count, project_count, label
37 )
38 } else {
39 format!("{} Today ({} tasks)", ICONS.today, task_count)
40 }
41}
42
43fn has_regular(items: &[Task]) -> bool {
44 items.iter().any(|task| !task.evening)
45}
46
47fn has_evening(items: &[Task]) -> bool {
48 items.iter().any(|task| task.evening)
49}
50
51fn ensure_area_group<'a>(
52 grouped: &mut GroupedSection<'a>,
53 area_pos: &mut HashMap<ThingsId, usize>,
54 area_uuid: &ThingsId,
55) -> usize {
56 if let Some(i) = area_pos.get(area_uuid).copied() {
57 return i;
58 }
59 let i = grouped.by_area.len();
60 grouped
61 .by_area
62 .push((area_uuid.clone(), AreaGroup::default()));
63 area_pos.insert(area_uuid.clone(), i);
64 i
65}
66
67fn ensure_project_group<'a>(
68 grouped: &mut GroupedSection<'a>,
69 project_pos: &mut HashMap<ThingsId, usize>,
70 project_uuid: &ThingsId,
71) -> usize {
72 if let Some(i) = project_pos.get(project_uuid).copied() {
73 return i;
74 }
75 let i = grouped.by_project.len();
76 grouped.by_project.push((project_uuid.clone(), Vec::new()));
77 project_pos.insert(project_uuid.clone(), i);
78 i
79}
80
81fn group_regular_items<'a>(items: &'a [Task], store: &ThingsStore) -> GroupedSection<'a> {
82 let mut grouped = GroupedSection::default();
83 let mut project_pos: HashMap<ThingsId, usize> = HashMap::new();
84 let mut by_area_pos: HashMap<ThingsId, usize> = HashMap::new();
85
86 for task in items.iter().filter(|task| !task.evening) {
87 if task.is_project() {
88 if let Some(area_uuid) = store.effective_area_uuid(task) {
89 let area_idx = ensure_area_group(&mut grouped, &mut by_area_pos, &area_uuid);
90 grouped.by_area[area_idx].1.tasks.push(task);
91 } else {
92 grouped.unscoped.push(task);
93 }
94 continue;
95 }
96
97 let project_uuid = store.effective_project_uuid(task);
98 let area_uuid = store.effective_area_uuid(task);
99
100 match (project_uuid, area_uuid) {
101 (Some(project_uuid), _) => {
102 let project_idx =
103 ensure_project_group(&mut grouped, &mut project_pos, &project_uuid);
104 grouped.by_project[project_idx].1.push(task);
105 }
106 (None, Some(area_uuid)) => {
107 let area_idx = ensure_area_group(&mut grouped, &mut by_area_pos, &area_uuid);
108 grouped.by_area[area_idx].1.tasks.push(task);
109 }
110 (None, None) => grouped.unscoped.push(task),
111 }
112 }
113
114 grouped
115}
116
117fn evening_items(items: &[Task]) -> Vec<&Task> {
118 items.iter().filter(|task| task.evening).collect()
119}
120
121fn id_prefix_len(store: &ThingsStore, items: &[Task]) -> usize {
122 let mut ids = items
123 .iter()
124 .map(|task| task.uuid.clone())
125 .collect::<Vec<_>>();
126 for task in items {
127 if let Some(project_uuid) = store.effective_project_uuid(task) {
128 ids.push(project_uuid);
129 }
130 if let Some(area_uuid) = store.effective_area_uuid(task) {
131 ids.push(area_uuid);
132 }
133 }
134 store.unique_prefix_length(&ids)
135}
136
137#[derive(Default, Props)]
138pub struct TodayViewProps<'a> {
139 pub items: Option<&'a Vec<Task>>,
140 pub detailed: bool,
141}
142
143#[component]
144pub fn TodayView<'a>(hooks: Hooks, props: &TodayViewProps<'a>) -> impl Into<AnyElement<'a>> {
145 let store = hooks.use_context::<Arc<ThingsStore>>().clone();
146 let Some(items) = props.items else {
147 return element! { Text(content: "") }.into_any();
148 };
149
150 let content: AnyElement<'a> = {
151 if items.is_empty() {
152 element! { EmptyText(content: "No tasks for today.") }.into_any()
153 } else {
154 let prefix_len = id_prefix_len(store.as_ref(), items);
155 let regular = group_regular_items(items, store.as_ref());
156 let evening = evening_items(items);
157
158 let regular_options = TaskOptions {
159 detailed: props.detailed,
160 show_project: false,
161 show_area: false,
162 show_today_markers: false,
163 show_staged_today_marker: true,
164 };
165 let evening_options = TaskOptions {
166 detailed: props.detailed,
167 show_project: true,
168 show_area: true,
169 show_today_markers: false,
170 show_staged_today_marker: true,
171 };
172
173 let mut regular_blocks: Vec<AnyElement<'a>> = Vec::new();
174
175 if !regular.unscoped.is_empty() {
176 regular_blocks.push(
177 element! {
178 TaskGroup(
179 header: None,
180 items: regular.unscoped.clone(),
181 id_prefix_len: prefix_len,
182 options: regular_options,
183 indent_under_header: 0u16,
184 hidden_count: 0usize,
185 )
186 }
187 .into_any(),
188 );
189 }
190
191 for (project_uuid, tasks) in ®ular.by_project {
192 regular_blocks.push(
193 element! {
194 TaskGroup(
195 header: Some(TaskGroupHeader::Project {
196 project_uuid: project_uuid.clone(),
197 title: store.resolve_project_title(project_uuid),
198 id_prefix_len: prefix_len,
199 }),
200 items: tasks.clone(),
201 id_prefix_len: prefix_len,
202 options: regular_options,
203 indent_under_header: 2u16,
204 hidden_count: 0usize,
205 )
206 }
207 .into_any(),
208 );
209 }
210
211 for (area_uuid, area_group) in ®ular.by_area {
212 regular_blocks.push(
213 element! {
214 TaskGroup(
215 header: Some(TaskGroupHeader::Area {
216 area_uuid: area_uuid.clone(),
217 title: store.resolve_area_title(area_uuid),
218 id_prefix_len: prefix_len,
219 }),
220 items: area_group.tasks.clone(),
221 id_prefix_len: prefix_len,
222 options: regular_options,
223 indent_under_header: 2u16,
224 hidden_count: 0usize,
225 )
226 }
227 .into_any(),
228 );
229 }
230
231 element! {
232 View(flex_direction: FlexDirection::Column, gap: 1) {
233 Text(
234 content: header_text(items),
235 wrap: TextWrap::NoWrap,
236 color: Color::Yellow,
237 weight: Weight::Bold,
238 )
239
240 #(if has_regular(items) {
241 Some(element! {
242 View(flex_direction: FlexDirection::Column, padding_left: LIST_INDENT, gap: 1) {
243 #(regular_blocks)
244 }
245 })
246 } else { None })
247
248 #(if has_evening(items) {
249 Some(element! {
250 View(flex_direction: FlexDirection::Column, gap: 1) {
251 Text(
252 content: format!("{} This Evening", ICONS.evening),
253 wrap: TextWrap::NoWrap,
254 color: Color::Blue,
255 weight: Weight::Bold,
256 )
257 View(flex_direction: FlexDirection::Column, padding_left: LIST_INDENT) {
258 TaskList(
259 items: evening,
260 id_prefix_len: prefix_len,
261 options: evening_options,
262 )
263 }
264 }
265 })
266 } else { None })
267 }
268 }
269 .into_any()
270 }
271 };
272
273 content
274}