diff --git a/cargo-tracker/Cargo.toml b/cargo-tracker/Cargo.toml index 83c2e01..2554206 100644 --- a/cargo-tracker/Cargo.toml +++ b/cargo-tracker/Cargo.toml @@ -6,3 +6,6 @@ edition = "2024" [dependencies] assert_cmd = "2" predicates = "2" +chrono = "0.4" +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 e7a11a9..df94377 100644 --- a/cargo-tracker/src/main.rs +++ b/cargo-tracker/src/main.rs @@ -1,3 +1,218 @@ +mod shipment; +mod shipment_manager; + +use crate::shipment::{Package, 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" => { + // 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_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_departure, 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" => { + // 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."); + continue; + } + 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" => { + 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!( + "Error: Invalid status. Valid options are: Pending, InTransit, Delivered, Lost." + ); + 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" => { + 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" => { + // List all shipments, optionally filter by status + let filter = read_input( + "Filter by status; Pending, InTransit, Delivered, Lost: (leave empty for all): ", + ); + + 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 shipments found for status '{}'.", 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 + 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!("'{}' is not a valid command.", command), + } + } } diff --git a/cargo-tracker/src/shipment.rs b/cargo-tracker/src/shipment.rs new file mode 100644 index 0000000..7a23519 --- /dev/null +++ b/cargo-tracker/src/shipment.rs @@ -0,0 +1,82 @@ +use chrono::{DateTime, Utc}; +use serde_json::json; +use uuid::Uuid; + +#[derive(Debug)] +pub struct Shipment { + pub tracking_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>, + tracking_id: String, + ) -> Self { + Self { + packages: Vec::new(), + tracking_id, + destination, + status, + time_of_departure, + time_of_arrival: None, + } + } + + pub fn add_package(&mut self, package: Package) { + self.packages.push(package); + } + + 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::>(); + + 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, + }); + + json_obj.to_string() + } +} + +#[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..d517b38 --- /dev/null +++ b/cargo-tracker/src/shipment_manager.rs @@ -0,0 +1,45 @@ +use crate::shipment::{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>, + tracking_id: Option, + ) -> &mut Shipment { + 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, tracking_id: &str) -> Option<&mut Shipment> { + self.shipments.get_mut(tracking_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() + } +}