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 let mut first = true;
175
176 if !regular.unscoped.is_empty() {
177 regular_blocks.push(
178 element! {
179 TaskGroup(
180 header: None,
181 items: regular.unscoped.clone(),
182 id_prefix_len: prefix_len,
183 options: regular_options,
184 indent_under_header: 0u16,
185 hidden_count: 0usize,
186 )
187 }
188 .into_any(),
189 );
190 first = false;
191 }
192
193 for (project_uuid, tasks) in ®ular.by_project {
194 if !first {
195 regular_blocks
196 .push(element! { Text(content: "", wrap: TextWrap::NoWrap) }.into_any());
197 }
198 regular_blocks.push(
199 element! {
200 TaskGroup(
201 header: Some(TaskGroupHeader::Project {
202 project_uuid: project_uuid.clone(),
203 title: store.resolve_project_title(project_uuid),
204 id_prefix_len: prefix_len,
205 }),
206 items: tasks.clone(),
207 id_prefix_len: prefix_len,
208 options: regular_options,
209 indent_under_header: 2u16,
210 hidden_count: 0usize,
211 )
212 }
213 .into_any(),
214 );
215 first = false;
216 }
217
218 for (area_uuid, area_group) in ®ular.by_area {
219 if !first {
220 regular_blocks
221 .push(element! { Text(content: "", wrap: TextWrap::NoWrap) }.into_any());
222 }
223 regular_blocks.push(
224 element! {
225 TaskGroup(
226 header: Some(TaskGroupHeader::Area {
227 area_uuid: area_uuid.clone(),
228 title: store.resolve_area_title(area_uuid),
229 id_prefix_len: prefix_len,
230 }),
231 items: area_group.tasks.clone(),
232 id_prefix_len: prefix_len,
233 options: regular_options,
234 indent_under_header: 2u16,
235 hidden_count: 0usize,
236 )
237 }
238 .into_any(),
239 );
240 first = false;
241 }
242
243 element! {
244 View(flex_direction: FlexDirection::Column) {
245 Text(
246 content: header_text(items),
247 wrap: TextWrap::NoWrap,
248 color: Color::Yellow,
249 weight: Weight::Bold,
250 )
251
252 #(if has_regular(items) {
253 Some(element! {
254 View(flex_direction: FlexDirection::Column) {
255 Text(content: "", wrap: TextWrap::NoWrap)
256 View(flex_direction: FlexDirection::Column, padding_left: LIST_INDENT) {
257 #(regular_blocks)
258 }
259 }
260 })
261 } else { None })
262
263 #(if has_evening(items) {
264 Some(element! {
265 View(flex_direction: FlexDirection::Column) {
266 Text(content: "", wrap: TextWrap::NoWrap)
267 Text(
268 content: format!("{} This Evening", ICONS.evening),
269 wrap: TextWrap::NoWrap,
270 color: Color::Blue,
271 weight: Weight::Bold,
272 )
273 Text(content: "", wrap: TextWrap::NoWrap)
274 View(flex_direction: FlexDirection::Column, padding_left: LIST_INDENT) {
275 TaskList(
276 items: evening,
277 id_prefix_len: prefix_len,
278 options: evening_options,
279 )
280 }
281 }
282 })
283 } else { None })
284 }
285 }
286 .into_any()
287 }
288 };
289
290 content
291}