From f4af88ffa681c5b1b1d3e916f9d6ddcc4f77fc49 Mon Sep 17 00:00:00 2001 From: rafaeljohn9 Date: Tue, 10 Jun 2025 15:33:25 +0300 Subject: [PATCH 1/5] feat: added shipment manager, shipment modules\n\nCreated the Logic and APIs to connect to the CLI\n Signed-off-by: rafaeljohn9 --- cargo-tracker/Cargo.toml | 2 + cargo-tracker/src/main.rs | 76 ++++++++++++++++++++++- cargo-tracker/src/shipment.rs | 86 +++++++++++++++++++++++++++ cargo-tracker/src/shipment_manager.rs | 52 ++++++++++++++++ 4 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 cargo-tracker/src/shipment.rs create mode 100644 cargo-tracker/src/shipment_manager.rs diff --git a/cargo-tracker/Cargo.toml b/cargo-tracker/Cargo.toml index 83c2e01..80e94d5 100644 --- a/cargo-tracker/Cargo.toml +++ b/cargo-tracker/Cargo.toml @@ -6,3 +6,5 @@ edition = "2024" [dependencies] assert_cmd = "2" predicates = "2" +chrono = "0.4" +uuid = { version = "1", features = ["v4"] } \ No newline at end of file diff --git a/cargo-tracker/src/main.rs b/cargo-tracker/src/main.rs index e7a11a9..561cca2 100644 --- a/cargo-tracker/src/main.rs +++ b/cargo-tracker/src/main.rs @@ -1,3 +1,77 @@ +mod shipment; +mod shipment_manager; + +use crate::shipment::{Package, Shipment, ShipmentStatus}; +use crate::shipment_manager::ShipmentManager; +use std::io::{self, Write}; +use std::process::exit; + +fn print_help() { + println!("Available commands:"); + println!(" add-shipment - Add a new shipment with tracking ID and destination"); + println!(" add-package - Add a package to an existing shipment"); + println!(" update-status - Update the status of a shipment"); + println!(" view-shipment - View details of a specific shipment"); + println!(" list-shipments - List all shipments (optionally filter by status)"); + println!(" generate-report - Generate shipment/package reports"); + println!(" clear - Clear the screen"); + println!(" help - Show available commands"); + println!(" exit - Exit the Cargo Tracker CLI"); +} + +fn read_input(prompt: &str) -> String { + print!("{}", prompt); + io::stdout().flush().unwrap(); + let mut input = String::new(); + io::stdin().read_line(&mut input).unwrap(); + input.trim().to_string() +} + fn main() { - println!("Hello, world!"); + println!("Welcome to Cargo Tracker 1.0!"); + println!("Type 'help' to see a list of available commands."); + + let mut manager = ShipmentManager::new(); + + loop { + let command = read_input("> ").to_lowercase(); + + match command.as_str() { + "help" => print_help(), + "add-shipment" => { + // TODO: Implement add-shipment logic + println!("(add-shipment not yet implemented)"); + } + "add-package" => { + // TODO: Implement add-package logic + println!("(add-package not yet implemented)"); + } + "update-status" => { + // TODO: Implement update-status logic + println!("(update-status not yet implemented)"); + } + "view-shipment" => { + // TODO: Implement view-shipment logic + println!("(view-shipment not yet implemented)"); + } + "list-shipments" => { + // TODO: Implement list-shipments logic + println!("(list-shipments not yet implemented)"); + } + "generate-report" => { + // TODO: Implement generate-report logic + println!("(generate-report not yet implemented)"); + } + "clear" => { + // Clear screen (works on most Unix terminals) + print!("\x1B[2J\x1B[1;1H"); + } + "exit" => { + println!("Goodbye!"); + exit(0); + } + "" => continue, + _ => println!("Unknown command. Type 'help' to see available commands."), + } + } } diff --git a/cargo-tracker/src/shipment.rs b/cargo-tracker/src/shipment.rs new file mode 100644 index 0000000..513b74c --- /dev/null +++ b/cargo-tracker/src/shipment.rs @@ -0,0 +1,86 @@ +use chrono::{DateTime, Utc}; +use uuid::Uuid; + +#[derive(Debug)] +pub struct Shipment { + pub shipment_id: String, + pub destination: String, + pub packages: Vec, + pub status: ShipmentStatus, + pub time_of_departure: Option>, + pub time_of_arrival: Option>, +} + +impl Shipment { + pub fn new( + status: ShipmentStatus, + destination: String, + time_of_departure: Option>, + time_of_arrival: Option>, + shipment_id: String, + ) -> Self { + Self { + packages: Vec::new(), + shipment_id, + destination, + status, + time_of_departure, + time_of_arrival, + } + } + + pub fn add_package(&mut self, package: Package) { + self.packages.push(package); + } + + pub fn remove_package(&mut self, package_id: Uuid) -> Option { + if let Some(pos) = self.packages.iter().position(|p| p.id == package_id) { + Some(self.packages.remove(pos)) + } else { + None + } + } + + pub fn update_package(&mut self, package_id: Uuid, new_description: String) -> bool { + if let Some(pkg) = self.packages.iter_mut().find(|p| p.id == package_id) { + pkg.description = new_description; + true + } else { + false + } + } + + pub fn update_status(&mut self, status_str: &str) -> bool { + match status_str { + "Pending" => self.status = ShipmentStatus::Pending, + "InTransit" => self.status = ShipmentStatus::InTransit, + "Delivered" => self.status = ShipmentStatus::Delivered, + "Lost" => self.status = ShipmentStatus::Lost, + _ => return false, + } + true + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ShipmentStatus { + Pending, + InTransit, + Delivered, + Lost, +} + +#[derive(Debug, Clone)] +pub struct Package { + pub id: Uuid, + pub description: String, +} + +impl Package { + pub fn new(description: String) -> Self { + Self { + id: Uuid::new_v4(), + description, + } + } +} diff --git a/cargo-tracker/src/shipment_manager.rs b/cargo-tracker/src/shipment_manager.rs new file mode 100644 index 0000000..17d22a0 --- /dev/null +++ b/cargo-tracker/src/shipment_manager.rs @@ -0,0 +1,52 @@ +use crate::shipment::{Package, Shipment, ShipmentStatus}; +use std::collections::HashMap; + +pub struct ShipmentManager { + shipments: HashMap, +} + +impl ShipmentManager { + pub fn new() -> Self { + ShipmentManager { + shipments: HashMap::new(), + } + } + + pub fn create_shipment( + &mut self, + status: ShipmentStatus, + destination: String, + time_of_departure: Option>, + time_of_arrival: Option>, + shipment_id: Option, + ) -> &mut Shipment { + let id = shipment_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); + let shipment = Shipment::new( + status, + destination, + time_of_departure, + time_of_arrival, + id.clone(), + ); + self.shipments.insert(id.clone(), shipment); + self.shipments.get_mut(&id).unwrap() + } + + pub fn get_shipment(&mut self, shipment_id: &str) -> Option<&mut Shipment> { + self.shipments.get_mut(shipment_id) + } + + /// List all shipments, with optional status filter. + pub fn list_shipments(&self, status_filter: Option) -> Vec<&Shipment> { + self.shipments + .values() + .filter(|s| { + if let Some(ref status) = status_filter { + &s.status == status + } else { + true + } + }) + .collect() + } +} From 46e6de1f5cd949460e7591d0d2ac2d47b69eb780 Mon Sep 17 00:00:00 2001 From: rafaeljohn9 Date: Tue, 10 Jun 2025 16:43:51 +0300 Subject: [PATCH 2/5] Feat: Finished implementing the MVP for this Signed-off-by: rafaeljohn9 --- cargo-tracker/Cargo.toml | 3 +- cargo-tracker/src/main.rs | 154 ++++++++++++++++++++++++-- cargo-tracker/src/shipment.rs | 54 +++++---- cargo-tracker/src/shipment_manager.rs | 19 +--- 4 files changed, 176 insertions(+), 54 deletions(-) diff --git a/cargo-tracker/Cargo.toml b/cargo-tracker/Cargo.toml index 80e94d5..2554206 100644 --- a/cargo-tracker/Cargo.toml +++ b/cargo-tracker/Cargo.toml @@ -7,4 +7,5 @@ edition = "2024" assert_cmd = "2" predicates = "2" chrono = "0.4" -uuid = { version = "1", features = ["v4"] } \ No newline at end of file +uuid = { version = "1", features = ["v4"] } +serde_json = "1" \ No newline at end of file diff --git a/cargo-tracker/src/main.rs b/cargo-tracker/src/main.rs index 561cca2..4f39baf 100644 --- a/cargo-tracker/src/main.rs +++ b/cargo-tracker/src/main.rs @@ -1,7 +1,7 @@ mod shipment; mod shipment_manager; -use crate::shipment::{Package, Shipment, ShipmentStatus}; +use crate::shipment::{Package, ShipmentStatus}; use crate::shipment_manager::ShipmentManager; use std::io::{self, Write}; use std::process::exit; @@ -39,24 +39,156 @@ fn main() { match command.as_str() { "help" => print_help(), "add-shipment" => { - // TODO: Implement add-shipment logic - println!("(add-shipment not yet implemented)"); + // Create a new shipment + let tracking_id = read_input("Please enter the Tracking ID: "); + let destination = read_input("Please enter the destination: "); + let status = ShipmentStatus::Pending; + let time_of_arrival = Some(chrono::Utc::now()); + let shipment_id = if tracking_id.is_empty() { + None + } else { + Some(tracking_id) + }; + let shipment = + manager.create_shipment(status, destination, time_of_arrival, shipment_id); + println!("Shipment Created\n\n"); + + // Let's add packages to the shipment + println!("Let's add packages. Type 'q' to quit"); + let mut count = 0; + let mut package_num = 1; + loop { + let prompt = format!("Enter package #{} description: ", package_num); + let description = read_input(&prompt); + if description.trim().eq_ignore_ascii_case("q") { + break; + } + let package = Package::new(description); + shipment.add_package(package); + count += 1; + package_num += 1; + } + let tracking = &shipment.tracking_id; + println!( + "{} package{} added to shipment '{}'.", + count, + if count == 1 { "" } else { "s" }, + tracking + ); } "add-package" => { - // TODO: Implement add-package logic - println!("(add-package not yet implemented)"); + // Add a package to an existing shipment + let tracking_id = read_input("Enter the Tracking ID of the shipment: "); + if tracking_id.is_empty() { + println!("Tracking ID cannot be empty."); + return; + } + match manager.get_shipment(&tracking_id) { + Some(shipment) => { + let description = read_input("Enter package description: "); + if description.trim().is_empty() { + println!("Package description cannot be empty."); + return; + } + let package = Package::new(description); + shipment.add_package(package); + println!("Package added to shipment '{}'.", shipment.tracking_id); + } + None => { + println!("Shipment with Tracking ID '{}' not found.", tracking_id); + } + } } "update-status" => { - // TODO: Implement update-status logic - println!("(update-status not yet implemented)"); + let tracking_id = read_input("Please enter the Tracking ID: "); + match manager.get_shipment(&tracking_id) { + Some(shipment) => { + let status_input = + read_input("Enter new status (Pending, InTransit, Delivered, Lost): "); + + let new_status = match status_input.trim().to_lowercase().as_str() { + "pending" => ShipmentStatus::Pending, + "intransit" => ShipmentStatus::InTransit, + "delivered" => { + shipment.time_of_arrival = Some(chrono::Utc::now()); + ShipmentStatus::Delivered + } + "lost" => ShipmentStatus::Lost, + _ => { + println!("Invalid status entered."); + return; + } + }; + shipment.status = new_status.clone(); + println!( + "Shipment '{}' status updated to {:?}.", + shipment.tracking_id, new_status + ); + } + None => { + println!("Shipment with Tracking ID '{}' not found.", tracking_id); + } + } } "view-shipment" => { - // TODO: Implement view-shipment logic - println!("(view-shipment not yet implemented)"); + let tracking_id = read_input("Please enter the Tracking ID: "); + match manager.get_shipment(&tracking_id) { + Some(shipment) => { + println!("Tracking ID: {}", shipment.tracking_id); + println!("Destination: {}", shipment.destination); + println!("Status: {:?}", shipment.status); + println!("Packages:"); + for package in &shipment.packages { + println!("({}) - {}", package.id, package.description); + } + println!(); + } + None => { + println!("Shipment with Tracking ID '{}' not found.", tracking_id); + } + } } "list-shipments" => { - // TODO: Implement list-shipments logic - println!("(list-shipments not yet implemented)"); + // List all shipments, optionally filter by status + let filter = read_input( + "Filter by status; Pending, InTransit, Delivered, Lost: (leave empty for all): ", + ); + + let filter = filter.trim().to_lowercase(); + let status_filter = match filter.as_str() { + "" => None, + "pending" => Some(ShipmentStatus::Pending), + "intransit" => Some(ShipmentStatus::InTransit), + "delivered" => Some(ShipmentStatus::Delivered), + "lost" => Some(ShipmentStatus::Lost), + _ => { + println!("No such status '{}'. Showing all shipments.", filter); + None + } + }; + let filtered_shipments = manager.list_shipments(status_filter); + + if filtered_shipments.is_empty() { + println!("No shipments found."); + } else { + for shipment in filtered_shipments { + println!( + "Tracking ID: {} | Destination: {} | Status: {:?} | Packages: {} | Departure: {} | Arrival: {}", + shipment.tracking_id, + shipment.destination, + shipment.status, + shipment.packages.len(), + shipment + .time_of_departure + .map(|t| t.to_rfc3339()) + .unwrap_or_else(|| "N/A".to_string()), + shipment + .time_of_arrival + .map(|t| t.to_rfc3339()) + .unwrap_or_else(|| "N/A".to_string()), + ); + } + } } "generate-report" => { // TODO: Implement generate-report logic diff --git a/cargo-tracker/src/shipment.rs b/cargo-tracker/src/shipment.rs index 513b74c..7a23519 100644 --- a/cargo-tracker/src/shipment.rs +++ b/cargo-tracker/src/shipment.rs @@ -1,9 +1,10 @@ use chrono::{DateTime, Utc}; +use serde_json::json; use uuid::Uuid; #[derive(Debug)] pub struct Shipment { - pub shipment_id: String, + pub tracking_id: String, pub destination: String, pub packages: Vec, pub status: ShipmentStatus, @@ -16,16 +17,15 @@ impl Shipment { status: ShipmentStatus, destination: String, time_of_departure: Option>, - time_of_arrival: Option>, - shipment_id: String, + tracking_id: String, ) -> Self { Self { packages: Vec::new(), - shipment_id, + tracking_id, destination, status, time_of_departure, - time_of_arrival, + time_of_arrival: None, } } @@ -33,32 +33,28 @@ impl Shipment { self.packages.push(package); } - pub fn remove_package(&mut self, package_id: Uuid) -> Option { - if let Some(pos) = self.packages.iter().position(|p| p.id == package_id) { - Some(self.packages.remove(pos)) - } else { - None - } - } + pub fn to_json_str(&self) -> String { + let packages_json = self + .packages + .iter() + .map(|p| { + json!({ + "id": p.id.to_string(), + "description": p.description, + }) + }) + .collect::>(); - pub fn update_package(&mut self, package_id: Uuid, new_description: String) -> bool { - if let Some(pkg) = self.packages.iter_mut().find(|p| p.id == package_id) { - pkg.description = new_description; - true - } else { - false - } - } + let json_obj = json!({ + "tracking_id": self.tracking_id, + "destination": self.destination, + "status": format!("{:?}", self.status), + "time_of_departure": self.time_of_departure.map(|t| t.to_rfc3339()), + "time_of_arrival": self.time_of_arrival.map(|t| t.to_rfc3339()), + "packages": packages_json, + }); - pub fn update_status(&mut self, status_str: &str) -> bool { - match status_str { - "Pending" => self.status = ShipmentStatus::Pending, - "InTransit" => self.status = ShipmentStatus::InTransit, - "Delivered" => self.status = ShipmentStatus::Delivered, - "Lost" => self.status = ShipmentStatus::Lost, - _ => return false, - } - true + json_obj.to_string() } } diff --git a/cargo-tracker/src/shipment_manager.rs b/cargo-tracker/src/shipment_manager.rs index 17d22a0..d517b38 100644 --- a/cargo-tracker/src/shipment_manager.rs +++ b/cargo-tracker/src/shipment_manager.rs @@ -1,4 +1,4 @@ -use crate::shipment::{Package, Shipment, ShipmentStatus}; +use crate::shipment::{Shipment, ShipmentStatus}; use std::collections::HashMap; pub struct ShipmentManager { @@ -17,23 +17,16 @@ impl ShipmentManager { status: ShipmentStatus, destination: String, time_of_departure: Option>, - time_of_arrival: Option>, - shipment_id: Option, + tracking_id: Option, ) -> &mut Shipment { - let id = shipment_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); - let shipment = Shipment::new( - status, - destination, - time_of_departure, - time_of_arrival, - id.clone(), - ); + let id = tracking_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); + let shipment = Shipment::new(status, destination, time_of_departure, id.clone()); self.shipments.insert(id.clone(), shipment); self.shipments.get_mut(&id).unwrap() } - pub fn get_shipment(&mut self, shipment_id: &str) -> Option<&mut Shipment> { - self.shipments.get_mut(shipment_id) + pub fn get_shipment(&mut self, tracking_id: &str) -> Option<&mut Shipment> { + self.shipments.get_mut(tracking_id) } /// List all shipments, with optional status filter. From b7a37cf529941e3e0c045c84ace83dd32de8058f Mon Sep 17 00:00:00 2001 From: rafaeljohn9 Date: Wed, 11 Jun 2025 00:45:51 +0300 Subject: [PATCH 3/5] Update: to match the messages in test --- cargo-tracker/src/main.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/cargo-tracker/src/main.rs b/cargo-tracker/src/main.rs index 4f39baf..aef09f3 100644 --- a/cargo-tracker/src/main.rs +++ b/cargo-tracker/src/main.rs @@ -41,13 +41,20 @@ fn main() { "add-shipment" => { // Create a new shipment let tracking_id = read_input("Please enter the Tracking ID: "); + if manager.get_shipment(&tracking_id).is_some() { + println!( + "Error: Shipment with tracking ID '{}' already exists.", + tracking_id + ); + continue; + } let destination = read_input("Please enter the destination: "); let status = ShipmentStatus::Pending; let time_of_arrival = Some(chrono::Utc::now()); let shipment_id = if tracking_id.is_empty() { None } else { - Some(tracking_id) + Some(tracking_id.clone()) }; let shipment = manager.create_shipment(status, destination, time_of_arrival, shipment_id); @@ -115,7 +122,9 @@ fn main() { } "lost" => ShipmentStatus::Lost, _ => { - println!("Invalid status entered."); + println!( + "Error: Invalid status. Valid options are: Pending, InTransit, Delivered, Lost." + ); return; } }; @@ -154,15 +163,15 @@ fn main() { "Filter by status; Pending, InTransit, Delivered, Lost: (leave empty for all): ", ); - let filter = filter.trim().to_lowercase(); - let status_filter = match filter.as_str() { + let filter_trimmed = filter.trim().to_lowercase(); + let status_filter = match filter_trimmed.as_str() { "" => None, "pending" => Some(ShipmentStatus::Pending), "intransit" => Some(ShipmentStatus::InTransit), "delivered" => Some(ShipmentStatus::Delivered), "lost" => Some(ShipmentStatus::Lost), _ => { - println!("No such status '{}'. Showing all shipments.", filter); + println!("No shipments found for status '{}'.", filter); None } }; @@ -203,7 +212,7 @@ fn main() { exit(0); } "" => continue, - _ => println!("Unknown command. Type 'help' to see available commands."), + _ => println!("'{}' is not a valid command.", command), } } } From 7545d84f64fc1f7374d387f9b4f06cc730899ff7 Mon Sep 17 00:00:00 2001 From: JohnKagunda <125447154+RafaelJohn9@users.noreply.github.com> Date: Wed, 11 Jun 2025 23:46:26 +0300 Subject: [PATCH 4/5] Update cargo-tracker/src/main.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cargo-tracker/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cargo-tracker/src/main.rs b/cargo-tracker/src/main.rs index aef09f3..8e332d9 100644 --- a/cargo-tracker/src/main.rs +++ b/cargo-tracker/src/main.rs @@ -50,14 +50,14 @@ fn main() { } let destination = read_input("Please enter the destination: "); let status = ShipmentStatus::Pending; - let time_of_arrival = Some(chrono::Utc::now()); + let time_of_departure = Some(chrono::Utc::now()); let shipment_id = if tracking_id.is_empty() { None } else { Some(tracking_id.clone()) }; let shipment = - manager.create_shipment(status, destination, time_of_arrival, shipment_id); + manager.create_shipment(status, destination, time_of_departure, shipment_id); println!("Shipment Created\n\n"); // Let's add packages to the shipment From 3714969cd60f6d01d606e6042619bcb74329b826 Mon Sep 17 00:00:00 2001 From: JohnKagunda <125447154+RafaelJohn9@users.noreply.github.com> Date: Wed, 11 Jun 2025 23:46:53 +0300 Subject: [PATCH 5/5] Update cargo-tracker/src/main.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cargo-tracker/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cargo-tracker/src/main.rs b/cargo-tracker/src/main.rs index 8e332d9..df94377 100644 --- a/cargo-tracker/src/main.rs +++ b/cargo-tracker/src/main.rs @@ -88,7 +88,7 @@ fn main() { let tracking_id = read_input("Enter the Tracking ID of the shipment: "); if tracking_id.is_empty() { println!("Tracking ID cannot be empty."); - return; + continue; } match manager.get_shipment(&tracking_id) { Some(shipment) => {