1use super::*;
2use crate::model::Priority;
3use crossterm::event::{KeyCode, KeyEvent};
4
5impl App {
6 pub fn close_popup(&mut self) {
7 self.popup = None;
8 self.input_mode = InputMode::Normal;
9 self.input_buffer.clear();
10 self.input_due_date.clear();
11 self.input_tags.clear();
12 }
13
14 fn preserved_task_notes(&self, id: u64) -> String {
15 self.data
16 .tasks
17 .iter()
18 .find(|t| t.id == id)
19 .map(|t| t.notes.clone())
20 .unwrap_or_default()
21 }
22
23 pub fn submit_popup(&mut self) {
24 match self.popup.clone() {
25 Some(Popup::AddTask) => {
26 let title = self.input_buffer.trim().to_string();
27 if title.is_empty() {
28 self.set_status("Title cannot be empty.", true);
29 return;
30 }
31 let due_date = match self.popup_due_date() {
32 Ok(d) => d,
33 Err(msg) => {
34 self.set_status(msg, true);
35 return;
36 }
37 };
38 let tags = self.popup_tags();
39 if let Err(e) = storage::add_task_full(
40 &self.db,
41 &mut self.data,
42 storage::TaskPayload {
43 title,
44 notes: String::new(),
45 estimated_minutes: self.input_number,
46 priority: self.input_priority,
47 tags,
48 due_date,
49 },
50 ) {
51 self.set_status(format!("Save error: {e}"), true);
52 return;
53 }
54 self.bump_data();
55 let indices = self.filtered_task_indices();
56 let sel = indices.len().saturating_sub(1);
57 self.task_state
58 .select(if indices.is_empty() { None } else { Some(sel) });
59 self.close_popup();
60 self.set_status("Task added.", false);
61 }
62 Some(Popup::EditTask(id)) => {
63 let title = self.input_buffer.trim().to_string();
64 if title.is_empty() {
65 self.set_status("Title cannot be empty.", true);
66 return;
67 }
68 let due_date = match self.popup_due_date() {
69 Ok(d) => d,
70 Err(msg) => {
71 self.set_status(msg, true);
72 return;
73 }
74 };
75 let tags = self.popup_tags();
76 let estimate = self.input_number.clamp(1, 480);
77 let priority = self.input_priority;
78 let notes = self.preserved_task_notes(id);
79 if let Err(e) = storage::update_task(
80 &self.db,
81 &mut self.data,
82 id,
83 storage::TaskPayload {
84 title,
85 notes,
86 estimated_minutes: estimate,
87 priority,
88 tags,
89 due_date,
90 },
91 ) {
92 self.set_status(format!("Save error: {e}"), true);
93 return;
94 }
95 self.bump_data();
96 self.close_popup();
97 self.set_status("Task updated.", false);
98 }
99 Some(Popup::ConfirmDelete(id)) => {
100 match storage::delete_task(&self.db, &mut self.data, id) {
101 Ok(true) => {
102 if self.active_task == Some(id) {
103 self.set_active_task(None);
104 }
105 self.bump_data();
106 self.clamp_task_selection_after_mutation();
107 self.set_status("Task deleted.", false);
108 self.check_queue_empty();
109 }
110 Ok(false) => {}
111 Err(e) => self.set_status(format!("Delete error: {e}"), true),
112 }
113 self.close_popup();
114 }
115 Some(Popup::EmptyQueueChoice) => {}
116 Some(Popup::AddSubtask(id)) => {
117 let title = self.input_buffer.trim().to_string();
118 if title.is_empty() {
119 self.set_status("Subtask title cannot be empty.", true);
120 return;
121 }
122 if let Err(e) = storage::add_subtask(&self.db, &mut self.data, id, title.clone()) {
123 self.set_status(format!("Save error: {e}"), true);
124 return;
125 }
126 self.bump_data();
127 if let Some(t) = self.data.tasks.iter().find(|t| t.id == id) {
128 self.subtask_selected = t.subtasks.len().saturating_sub(1);
129 }
130 self.input_buffer.clear();
131 self.subtask_focus = true;
132 self.sync_subtask_list();
133 self.set_status(
134 format!("Added \"{title}\" — type another or q to close"),
135 false,
136 );
137 }
138 Some(Popup::BulkConfirm(action)) => {
139 let ids: Vec<u64> = self.bulk_selected.iter().copied().collect();
140 let result = match action {
141 BulkAction::MarkDone => storage::bulk_mark_done(&self.db, &mut self.data, &ids),
142 BulkAction::Delete => storage::bulk_delete(&self.db, &mut self.data, &ids),
143 };
144 match result {
145 Ok(n) => {
146 self.bulk_selected.clear();
147 self.bulk_mode = false;
148 self.bump_data();
149 self.clamp_task_selection_after_mutation();
150 self.set_status(format!("Bulk action applied to {n} tasks."), false);
151 }
152 Err(e) => self.set_status(format!("Bulk error: {e}"), true),
153 }
154 self.close_popup();
155 }
156 None => {}
157 }
158 }
159
160 pub fn confirm_delete(&mut self) {
161 if let Some(Popup::ConfirmDelete(id)) = self.popup.clone() {
162 match storage::delete_task(&self.db, &mut self.data, id) {
163 Ok(true) => {
164 if self.active_task == Some(id) {
165 self.set_active_task(None);
166 }
167 self.bump_data();
168 self.clamp_task_selection_after_mutation();
169 self.set_status("Task deleted.", false);
170 self.check_queue_empty();
171 }
172 Ok(false) => {}
173 Err(e) => self.set_status(format!("Delete error: {e}"), true),
174 }
175 self.close_popup();
176 }
177 }
178
179 pub(crate) fn handle_popup_key(&mut self, key: KeyEvent) {
180 if matches!(self.popup, Some(Popup::EmptyQueueChoice)) {
181 match key.code {
182 KeyCode::Esc => self.close_popup(),
183 KeyCode::Enter | KeyCode::Char('y') | KeyCode::Char('Y') => {
184 self.data.empty_queue_behavior = EmptyQueueBehavior::FreeFocus;
185 self.close_popup();
186 self.set_status(
187 "All tasks done — free focus. Sessions log as general focus.",
188 false,
189 );
190 }
191 KeyCode::Char('p') | KeyCode::Char('P') => {
192 self.data.empty_queue_behavior = EmptyQueueBehavior::PauseTimer;
193 self.close_popup();
194 if self.timer.state == TimerState::Running {
195 self.pause_timer();
196 } else {
197 self.timer.reset();
198 }
199 self.set_status("All tasks done — timer paused.", false);
200 }
201 KeyCode::Char('a') | KeyCode::Char('A') => {
202 self.close_popup();
203 self.open_add_task();
204 }
205 _ => {}
206 }
207 return;
208 }
209 if matches!(self.popup, Some(Popup::ConfirmDelete(_))) {
210 match key.code {
211 KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => {
212 self.close_popup();
213 }
214 KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
215 self.confirm_delete();
216 }
217 _ => {}
218 }
219 return;
220 }
221 if matches!(self.popup, Some(Popup::BulkConfirm(_))) {
222 match key.code {
223 KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => self.close_popup(),
224 KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => self.submit_popup(),
225 _ => {}
226 }
227 return;
228 }
229 if matches!(self.popup, Some(Popup::AddSubtask(_))) {
230 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
231 match key.code {
232 KeyCode::Esc | KeyCode::Char('q') => self.close_popup(),
233 KeyCode::Enter => self.submit_popup(),
234 KeyCode::Backspace => {
235 self.input_buffer.pop();
236 }
237 KeyCode::Char(c) if !ctrl => {
238 self.input_buffer.push(c);
239 }
240 _ => {}
241 }
242 return;
243 }
244 let is_text_field = matches!(
245 self.input_field,
246 InputField::Title | InputField::DueDate | InputField::Tags
247 );
248 match key.code {
249 KeyCode::Esc => {
250 self.close_popup();
251 }
252 KeyCode::Tab | KeyCode::BackTab => {
253 let order = [
254 InputField::Title,
255 InputField::Estimate,
256 InputField::Priority,
257 InputField::DueDate,
258 InputField::Tags,
259 ];
260 let idx = order
261 .iter()
262 .position(|f| *f == self.input_field)
263 .unwrap_or(0);
264 let next = if key.code == KeyCode::Tab {
265 (idx + 1) % order.len()
266 } else {
267 (idx + order.len() - 1) % order.len()
268 };
269 self.input_field = order[next];
270 }
271 KeyCode::Enter => {
272 self.submit_popup();
273 }
274 _ => {
275 if is_text_field {
276 self.handle_text_input(key);
277 } else {
278 self.handle_field_input(key);
279 }
280 }
281 }
282 }
283
284 pub(crate) fn handle_text_input(&mut self, key: KeyEvent) {
285 if self.input_field == InputField::DueDate {
286 match key.code {
287 KeyCode::Left => {
288 self.calendar_date -= chrono::Duration::days(1);
289 self.input_due_date = self.calendar_date.format("%Y-%m-%d").to_string();
290 return;
291 }
292 KeyCode::Right => {
293 self.calendar_date += chrono::Duration::days(1);
294 self.input_due_date = self.calendar_date.format("%Y-%m-%d").to_string();
295 return;
296 }
297 KeyCode::Up => {
298 self.calendar_date -= chrono::Duration::days(7);
299 self.input_due_date = self.calendar_date.format("%Y-%m-%d").to_string();
300 return;
301 }
302 KeyCode::Down => {
303 self.calendar_date += chrono::Duration::days(7);
304 self.input_due_date = self.calendar_date.format("%Y-%m-%d").to_string();
305 return;
306 }
307 _ => {}
308 }
309 }
310 let buf = match self.input_field {
311 InputField::Title => &mut self.input_buffer,
312 InputField::DueDate => &mut self.input_due_date,
313 InputField::Tags => &mut self.input_tags,
314 _ => return,
315 };
316 match key.code {
317 KeyCode::Backspace => {
318 buf.pop();
319 }
320 KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
321 buf.push(c);
322 }
323 _ => {}
324 }
325 }
326
327 pub(crate) fn handle_field_input(&mut self, key: KeyEvent) {
328 match self.input_field {
329 InputField::Estimate => match key.code {
330 KeyCode::Char(c) if c.is_ascii_digit() => {
331 let d = c.to_digit(10).unwrap_or(0);
332 self.input_number = (self.input_number.saturating_mul(10) + d).min(480);
333 }
334 KeyCode::Backspace => {
335 self.input_number /= 10;
336 if self.input_number == 0 {
337 self.input_number = 1;
338 }
339 }
340 KeyCode::Up => self.input_number = (self.input_number + 5).min(480),
341 KeyCode::Down => self.input_number = self.input_number.saturating_sub(5).max(1),
342 _ => {}
343 },
344 InputField::Priority => {
345 let next = match key.code {
346 KeyCode::Right | KeyCode::Up | KeyCode::Char(' ') => {
347 match self.input_priority {
348 Priority::Low => Priority::Medium,
349 Priority::Medium => Priority::High,
350 Priority::High => Priority::Low,
351 }
352 }
353 KeyCode::Left | KeyCode::Down => match self.input_priority {
354 Priority::Low => Priority::High,
355 Priority::High => Priority::Medium,
356 Priority::Medium => Priority::Low,
357 },
358 KeyCode::Char('1') => Priority::Low,
359 KeyCode::Char('2') => Priority::Medium,
360 KeyCode::Char('3') => Priority::High,
361 _ => self.input_priority,
362 };
363 self.input_priority = next;
364 }
365 _ => {}
366 }
367 }
368}