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