task_manager/
lib.rs

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
91///title, id
92fn 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
120///title, id
121pub 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}