1use crate::user_input::{get_user_confirmation, get_user_selection};
2use chrono::{TimeZone, Utc};
3use colored::*;
4use home::home_dir;
5use rusqlite::{Connection, Result};
6use tabled::Tabled;
7
8pub mod dao;
9pub mod user_input;
10pub const TABLE_TASKS: &str = "tasks";
11pub const TABLE_BOARDS: &str = "boards";
12pub const TABLE_COMMENTS: &str = "comments";
13
14pub const MAIN_MENU_OPTIONS: [&str; 7] = [
15 "Create Task",
16 "View Tasks [Pending]",
17 "View Tasks [Done]",
18 "Create Board",
19 "View Boards",
20 "other",
21 "Exit",
22];
23
24const TASK_ACTIONS: [&str; 6] = [
25 "Delete",
26 "Change",
27 "Add comment",
28 "View comments",
29 "Set reminder",
30 "Cancel",
31];
32pub const BOARD_ACTIONS: [&str; 3] = ["Delete", "Change title", "Cancel"];
33pub const SAMPLE_TITLE: &str = "sample";
34pub const DATETIME_FORMAT: &str = "%a, %b %e %Y %T";
35pub const DATE_FORMAT: &str = "%Y%m%d";
36pub const TIME_FORMAT: &str = "%H:%M:%S";
37pub const ALTERNATIVE_DATETIME_FORMAT: &str = "%Y%m%d %H:%M:%S";
38
39#[derive(Debug, Tabled)]
40pub struct Task {
41 pub id: u16,
42 pub title: String,
43 pub done: u8,
44 pub board_id: u16,
45 pub created_at: String,
46 pub reminder: String,
47}
48
49#[derive(Debug)]
50pub struct Board {
51 pub id: u16,
52 pub title: String,
53}
54
55#[derive(Debug)]
56pub struct Comment {
57 pub id: u16,
58 pub title: String,
59 pub created_at: String,
60}
61
62#[derive(Debug)]
63pub enum Color {
64 Red,
65 Green,
66 Yellow,
67 Blue,
68 Magenta,
69 Cyan,
70 White,
71}
72
73#[derive(Debug)]
74pub struct Record {
75 pub qtd: u16,
76}
77
78pub fn get_connection() -> Connection {
79 let conn = Connection::open(&get_database_path()).unwrap();
80 conn
81}
82
83pub fn get_database_path() -> String {
84 format!(
85 "{}/{}.db3",
86 home_dir().unwrap().display(),
87 env!("CARGO_PKG_NAME")
88 )
89}
90
91fn select_comment(comments_raw: &Vec<Comment>, task_title: &str) -> Option<(String, u16)> {
93 let mut comments: Vec<String> = comments_raw
94 .iter()
95 .map(|x| format!("{} - {} [{}]", &x.id, &x.title, &x.created_at))
96 .collect();
97 comments.sort();
98
99 let selected_comment = user_input::get_user_selection(
100 &comments,
101 format!("Comments for task {}", task_title).as_str(),
102 );
103 let selected_comment_id: u16 = selected_comment
104 .0
105 .split_whitespace()
106 .next()
107 .unwrap()
108 .parse()
109 .unwrap();
110
111 let selected_comment: &Comment = comments_raw
112 .iter()
113 .find(|x| x.id == selected_comment_id)
114 .unwrap();
115
116 let selected_comment = (selected_comment.title.to_string(), selected_comment.id);
117 Some(selected_comment)
118}
119
120pub fn select_board() -> Option<(String, u16)> {
122 let records_qtd = dao::get_records_qtd(TABLE_BOARDS).unwrap();
123
124 if records_qtd == 0 {
125 display_message("info", "No Boards found in database", Color::Blue);
126 return None;
127 }
128
129 let boards_raw = dao::get_boards().unwrap();
130
131 let mut boards: Vec<String> = boards_raw
132 .iter()
133 .map(|x| format!("{} - {}", &x.id, &x.title))
134 .collect();
135 boards.sort();
136
137 let selected_board = user_input::get_user_selection(&boards, "Board");
138 let selected_board_id: u16 = selected_board
139 .0
140 .split_whitespace()
141 .next()
142 .unwrap()
143 .parse()
144 .unwrap();
145
146 let selected_board: &Board = boards_raw
147 .iter()
148 .find(|x| x.id == selected_board_id)
149 .unwrap();
150
151 let selected_board = (selected_board.title.to_string(), selected_board.id);
152 Some(selected_board)
153}
154
155pub fn list_boards() -> Result<()> {
156 let selected_board = select_board();
157 if selected_board.is_none() {
158 return Ok(());
159 }
160 let (board_title, board_id) = selected_board.unwrap();
161
162 let (_, action_index) = get_user_selection(
163 &BOARD_ACTIONS.to_vec(),
164 format!("Action on Board {}", board_title).as_str(),
165 );
166
167 match action_index {
168 0 => delete_board(&board_title, board_id)?,
169 1 => dao::edit_board(&board_title, board_id)?,
170 _ => return Ok(()),
171 };
172
173 Ok(())
174}
175
176fn delete_board(board_title: &str, board_id: u16) -> Result<()> {
177 let deletion_confirmation =
178 get_user_confirmation(format!("Are you sure you want to delete {}", &board_title).as_str());
179
180 if deletion_confirmation {
181 let deletion_successful = dao::delete_record_by_id(TABLE_BOARDS, board_id);
182 match deletion_successful {
183 Ok(_) => display_message(
184 "ok",
185 format!("Board {} has been deleted", &board_title).as_str(),
186 Color::Green,
187 ),
188 Err(_) => display_message(
189 "error",
190 format!("Could not delete {} ", &board_title).as_str(),
191 Color::Red,
192 ),
193 }
194 }
195 Ok(())
196}
197
198pub fn datetime_str_is_past(reminder: &str) -> bool {
199 let datetime = Utc.datetime_from_str(reminder, DATETIME_FORMAT);
200
201 if datetime.is_err() {
202 return false;
203 }
204 datetime.unwrap() < Utc::now()
205}
206
207fn delete_task(task_title: &str, task_id: u16) -> Result<()> {
208 let deletion_confirmation =
209 get_user_confirmation(format!("Are you sure you want to delete {}", &task_title).as_str());
210
211 if deletion_confirmation {
212 let deletion_successful = dao::delete_record_by_id(TABLE_TASKS, task_id);
213 match deletion_successful {
214 Ok(_) => display_message(
215 "ok",
216 format!("task {} has been deleted", &task_title).as_str(),
217 Color::Green,
218 ),
219 Err(_) => display_message(
220 "error",
221 format!("Could not delete {} ", &task_title).as_str(),
222 Color::Red,
223 ),
224 }
225 }
226 Ok(())
227}
228
229pub fn list_tasks(done: u8) -> Result<()> {
230 let selected_task = dao::select_task(done);
231 if selected_task.is_none() {
232 return Ok(());
233 }
234
235 let (task_title, task_id) = selected_task.unwrap();
236
237 let (_, action_index) = get_user_selection(
238 &TASK_ACTIONS.to_vec(),
239 format!("Action on Task {}", task_title).as_str(),
240 );
241
242 match action_index {
243 0 => delete_task(&task_title, task_id)?,
244 1 => dao::switch_task_status(task_id)?,
245 2 => dao::create_comment(task_id)?,
246 3 => list_comments(&task_title, task_id)?,
247 4 => dao::set_reminder(task_id)?,
248 _ => return Ok(()),
249 };
250
251 Ok(())
252}
253
254pub fn list_comments(task_title: &str, task_id: u16) -> Result<()> {
255 let comments = dao::get_comments_by_task_id(task_id)?;
256 if comments.len() == 0 {
257 display_message("info", "No comments for this Task", Color::Cyan);
258 return Ok(());
259 }
260
261 let selected_comment = select_comment(&comments, &task_title).unwrap();
262 println!("{:?}", selected_comment);
263
264 Ok(())
265}
266
267pub fn other() -> Result<()> {
268 Ok(())
269}
270
271pub fn display_message(message_type: &str, message: &str, color: Color) {
272 let color = match color {
273 Color::Red => "red",
274 Color::Green => "green",
275 Color::Yellow => "yellow",
276 Color::Blue => "blue",
277 Color::Magenta => "magenta",
278 Color::Cyan => "cyan",
279 Color::White => "white",
280 };
281
282 let msg = format!("[{}] {}", message_type.to_uppercase(), message).color(color);
283 println!("{msg}");
284}
285
286pub fn get_tasks(query: &str) -> Result<Vec<Task>> {
287 let conn = get_connection();
288
289 let mut records: Vec<Task> = Vec::new();
290
291 let mut stmt = conn.prepare(&query)?;
292
293 let result_iter = stmt.query_map([], |row| {
294 Ok(Task {
295 id: row.get(0)?,
296 title: row.get(1)?,
297 done: row.get(2)?,
298 board_id: row.get(3)?,
299 created_at: row.get(4)?,
300 reminder: row.get(5)?,
301 })
302 })?;
303
304 for i in result_iter {
305 records.push(i?);
306 }
307
308 Ok(records)
309}
310
311pub fn display_app_intro() {
312 let title = format!(
313 "\n{} - {} \nAuthors: {}\nVersion: {}\nLicense: {}\nCrafted with ❤️ using Rust language\nDatabase: {}\n",
314 env!("CARGO_PKG_NAME").to_uppercase(),
315 env!("CARGO_PKG_DESCRIPTION"),
316 env!("CARGO_PKG_AUTHORS"),
317 env!("CARGO_PKG_VERSION"),
318 env!("CARGO_PKG_LICENSE"),
319 get_database_path()
320 );
321
322 println!("{title}");
323}