use chrono::{DateTime, Duration, Local, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum Priority {
Low,
#[default]
Medium,
High,
Urgent,
}
impl PartialOrd for Priority {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Priority {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let self_val = match self {
Priority::Low => 0,
Priority::Medium => 1,
Priority::High => 2,
Priority::Urgent => 3,
};
let other_val = match other {
Priority::Low => 0,
Priority::Medium => 1,
Priority::High => 2,
Priority::Urgent => 3,
};
self_val.cmp(&other_val)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum TaskStatus {
#[default]
Pending,
InProgress,
Completed,
Archived,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Task {
pub id: String,
pub title: String,
pub description: Option<String>,
pub due_date: Option<DateTime<Utc>>,
pub priority: Priority,
pub status: TaskStatus,
pub project_id: Option<String>,
pub tags: Vec<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
}
impl Task {
pub fn new(title: &str) -> Self {
let now = Utc::now();
Self {
id: Uuid::now_v7().to_string(),
title: title.to_string(),
description: None,
due_date: None,
priority: Priority::default(),
status: TaskStatus::default(),
project_id: None,
tags: Vec::new(),
created_at: now,
updated_at: now,
completed_at: None,
}
}
pub fn is_overdue(&self) -> bool {
if self.status == TaskStatus::Completed || self.status == TaskStatus::Archived {
return false;
}
match self.due_date {
Some(due) => due < Utc::now(),
None => false,
}
}
pub fn is_due_today(&self) -> bool {
match self.due_date {
Some(due) => {
let today = Local::now().date_naive();
let due_local = due.with_timezone(&Local).date_naive();
today == due_local
}
None => false,
}
}
pub fn is_due_this_week(&self) -> bool {
match self.due_date {
Some(due) => {
let now = Utc::now();
let week_from_now = now + Duration::days(7);
due >= now && due <= week_from_now
}
None => false,
}
}
pub fn complete(&mut self) {
self.status = TaskStatus::Completed;
self.completed_at = Some(Utc::now());
self.updated_at = Utc::now();
}
pub fn reopen(&mut self) {
self.status = TaskStatus::Pending;
self.completed_at = None;
self.updated_at = Utc::now();
}
}
impl Default for Task {
fn default() -> Self {
Self::new("Untitled Task")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_task_new() {
let task = Task::new("Test Task");
assert_eq!(task.title, "Test Task");
assert_eq!(task.status, TaskStatus::Pending);
assert_eq!(task.priority, Priority::Medium);
assert!(task.description.is_none());
assert!(task.due_date.is_none());
}
#[test]
fn test_task_is_overdue() {
let mut task = Task::new("Test");
task.due_date = Some(Utc::now() - Duration::hours(1));
assert!(task.is_overdue());
}
#[test]
fn test_completed_task_not_overdue() {
let mut task = Task::new("Test");
task.due_date = Some(Utc::now() - Duration::hours(1));
task.complete();
assert!(!task.is_overdue());
}
#[test]
fn test_task_without_due_date_not_overdue() {
let task = Task::new("Test");
assert!(!task.is_overdue());
}
#[test]
fn test_priority_ordering() {
assert!(Priority::Urgent > Priority::High);
assert!(Priority::High > Priority::Medium);
assert!(Priority::Medium > Priority::Low);
}
#[test]
fn test_task_complete_and_reopen() {
let mut task = Task::new("Test");
assert_eq!(task.status, TaskStatus::Pending);
assert!(task.completed_at.is_none());
task.complete();
assert_eq!(task.status, TaskStatus::Completed);
assert!(task.completed_at.is_some());
task.reopen();
assert_eq!(task.status, TaskStatus::Pending);
assert!(task.completed_at.is_none());
}
#[test]
fn test_task_is_due_today() {
let mut task = Task::new("Test");
task.due_date = Some(Utc::now());
assert!(task.is_due_today());
task.due_date = Some(Utc::now() + Duration::days(1));
assert!(!task.is_due_today());
}
}