Compare commits
10 commits
49f754465e
...
4bcd25cc09
Author | SHA1 | Date | |
---|---|---|---|
4bcd25cc09 | |||
05569817aa | |||
34ff456eeb | |||
20c41d05de | |||
0f4d21b974 | |||
3a7c37f7a5 | |||
5ac0fa668a | |||
7e7fd1fd35 | |||
1ed172937a | |||
c75e518658 |
9 changed files with 313 additions and 177 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/target
|
/target
|
||||||
|
*.json
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -938,7 +938,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "worthit"
|
name = "worthit"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "worthit"
|
name = "worthit"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
@ -15,4 +15,6 @@ tabled = "0.20.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
46
README.md
Normal file
46
README.md
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
記錄自己買過咩東西
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> worthit
|
||||||
|
記錄買過咩東西
|
||||||
|
|
||||||
|
Usage: worthit <COMMAND>
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
add
|
||||||
|
set
|
||||||
|
show
|
||||||
|
delete
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Print help (see more with '--help')
|
||||||
|
-V, --version Print version
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> worthit add --help
|
||||||
|
Usage: worthit add --name <NAME> --price <PRICE> --pd <PURCHASE_DATE>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-n, --name <NAME> 佢個名
|
||||||
|
-p, --price <PRICE> 幾钱
|
||||||
|
--pd <PURCHASE_DATE> 幾時買,例如2024-1-6
|
||||||
|
-h, --help Print help
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> worthit set --help
|
||||||
|
Usage: worthit set [OPTIONS]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-n, --current-name <NAME> 佢個名
|
||||||
|
--new-name <NEW_NAME> 起個新名
|
||||||
|
-p, --price <PRICE> 幾钱
|
||||||
|
--purchase-date <PURCHASE_DATE> 幾時買,例如2024-1-6
|
||||||
|
-s, --status <STATUS> 0: 用緊, 1: 食塵, 2: 壞咗, 3: 賣咗 [default: 0]
|
||||||
|
--repair-count <REPAIR_COUNT> 整咗幾次
|
||||||
|
--repair-cost <REPAIR_COST> 維修費
|
||||||
|
--sold-price <SOLD_PRICE> 轉手價
|
||||||
|
--sold-date <SOLD_DATE> 幾時賣,例如2025-1-6
|
||||||
|
-h, --help Print help
|
||||||
|
```
|
64
src/cli.rs
64
src/cli.rs
|
@ -1,7 +1,18 @@
|
||||||
|
use anyhow::Result;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
model::Records,
|
||||||
|
utils::{add_handler, delete_handler, set_handler, show_handler},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about="敗家記錄", long_about = None, disable_help_subcommand = true)]
|
#[command(
|
||||||
|
version,
|
||||||
|
about = "記錄買過咩東西",
|
||||||
|
long_about = "大額商品總是希望可以耐用不易壞,記錄買過啲貴嘢睇下可以用幾耐",
|
||||||
|
disable_help_subcommand = true
|
||||||
|
)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Commands,
|
pub command: Commands,
|
||||||
|
@ -20,9 +31,12 @@ pub enum Commands {
|
||||||
purchase_date: String,
|
purchase_date: String,
|
||||||
},
|
},
|
||||||
Set {
|
Set {
|
||||||
#[arg(short = 'n', long, help = "佢個名")]
|
#[arg(short = 'n', long = "current-name", help = "佢個名")]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "new-name", help = "起個新名")]
|
||||||
|
new_name: Option<String>,
|
||||||
|
|
||||||
#[arg(short = 'p', long, help = "幾钱")]
|
#[arg(short = 'p', long, help = "幾钱")]
|
||||||
price: Option<f64>,
|
price: Option<f64>,
|
||||||
|
|
||||||
|
@ -37,8 +51,6 @@ pub enum Commands {
|
||||||
)]
|
)]
|
||||||
status: Option<u32>,
|
status: Option<u32>,
|
||||||
|
|
||||||
// #[arg(long, help = "用咗幾次")]
|
|
||||||
// usage_count: Option<u32>,
|
|
||||||
#[arg(long, help = "整咗幾次")]
|
#[arg(long, help = "整咗幾次")]
|
||||||
repair_count: Option<u32>,
|
repair_count: Option<u32>,
|
||||||
|
|
||||||
|
@ -57,3 +69,47 @@ pub enum Commands {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait CommandHandler {
|
||||||
|
fn execute(self, records: Records, record_path: String) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandHandler for Commands {
|
||||||
|
fn execute(self, records: Records, record_path: String) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Commands::Add {
|
||||||
|
name,
|
||||||
|
price,
|
||||||
|
purchase_date,
|
||||||
|
} => add_handler(name, price, purchase_date, records, record_path),
|
||||||
|
Commands::Set {
|
||||||
|
name,
|
||||||
|
new_name,
|
||||||
|
price,
|
||||||
|
purchase_date,
|
||||||
|
status,
|
||||||
|
repair_count,
|
||||||
|
repair_cost,
|
||||||
|
sold_price,
|
||||||
|
sold_date,
|
||||||
|
} => set_handler(
|
||||||
|
name,
|
||||||
|
new_name,
|
||||||
|
price,
|
||||||
|
purchase_date,
|
||||||
|
status,
|
||||||
|
repair_count,
|
||||||
|
repair_cost,
|
||||||
|
sold_price,
|
||||||
|
sold_date,
|
||||||
|
records,
|
||||||
|
record_path,
|
||||||
|
),
|
||||||
|
Commands::Show => {
|
||||||
|
show_handler(records);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Commands::Delete { name } => delete_handler(name, records, record_path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,8 +16,6 @@ pub struct ProductTable {
|
||||||
#[tabled(rename = "狀態")]
|
#[tabled(rename = "狀態")]
|
||||||
status: Status,
|
status: Status,
|
||||||
|
|
||||||
// #[tabled(rename = "使用次數")]
|
|
||||||
// usage_count: u32,
|
|
||||||
#[tabled(rename = "維修次數")]
|
#[tabled(rename = "維修次數")]
|
||||||
repair_count: String,
|
repair_count: String,
|
||||||
|
|
||||||
|
|
166
src/main.rs
166
src/main.rs
|
@ -1,14 +1,8 @@
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::Result;
|
||||||
use chrono::NaiveDate;
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{Cli, Commands};
|
use cli::Cli;
|
||||||
use tabled::Table;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{cli::CommandHandler, model::Records, utils::setup_records_file};
|
||||||
display::ProductTable,
|
|
||||||
model::{Product, Records, Status},
|
|
||||||
utils::setup_records_file,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod display;
|
mod display;
|
||||||
|
@ -16,158 +10,12 @@ mod model;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
// let record_path = "records.json";
|
// let record_path = "records.json".to_string();
|
||||||
let binding = setup_records_file()?;
|
let record_path = setup_records_file()?;
|
||||||
let record_path = binding
|
|
||||||
.to_str()
|
|
||||||
.ok_or_else(|| anyhow!("Invalid UTF-8 in record file path"))?;
|
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let mut records = Records::load(record_path)?;
|
let records = Records::load(&record_path)?;
|
||||||
|
cli.command.execute(records, record_path)?;
|
||||||
match cli.command {
|
|
||||||
Commands::Add {
|
|
||||||
name,
|
|
||||||
price,
|
|
||||||
purchase_date,
|
|
||||||
} => {
|
|
||||||
if name.is_empty() {
|
|
||||||
anyhow::bail!("product name can't not be empty.")
|
|
||||||
}
|
|
||||||
if price <= 0.0 {
|
|
||||||
anyhow::bail!("price must be greater than 0.")
|
|
||||||
}
|
|
||||||
let purchase_date = NaiveDate::parse_from_str(&purchase_date, "%Y-%m-%d")
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"Invalid date format. Please use YYYY-M-D, e.g. 2023-1-9. You entered: {}",
|
|
||||||
purchase_date
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let product = Product::new(name, price, purchase_date);
|
|
||||||
|
|
||||||
records.add_product(product)?;
|
|
||||||
records.save(record_path)?;
|
|
||||||
}
|
|
||||||
Commands::Set {
|
|
||||||
name,
|
|
||||||
price,
|
|
||||||
purchase_date,
|
|
||||||
// usage_count,
|
|
||||||
status,
|
|
||||||
repair_count,
|
|
||||||
repair_cost,
|
|
||||||
sold_price,
|
|
||||||
sold_date,
|
|
||||||
} => {
|
|
||||||
let product_name = name.as_ref().ok_or_else(|| anyhow!("Name is required"))?;
|
|
||||||
let product = records.get_product_mut(product_name)?;
|
|
||||||
|
|
||||||
let validated_price = price
|
|
||||||
.map(|price| {
|
|
||||||
(price > 0.0)
|
|
||||||
.then_some(price)
|
|
||||||
.ok_or_else(|| anyhow!("price must be greater than 0"))
|
|
||||||
})
|
|
||||||
.transpose()?;
|
|
||||||
|
|
||||||
let parsed_purchase_date = purchase_date
|
|
||||||
.map(|date_str| {
|
|
||||||
NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").with_context(|| anyhow!(
|
|
||||||
"Invalid date format. Please use YYYY-M-D, e.g. 2023-1-9. You entered: {}",
|
|
||||||
date_str
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.transpose()?;
|
|
||||||
|
|
||||||
let parsed_status = status.map(Status::from_u32).transpose()?;
|
|
||||||
|
|
||||||
let validated_repair_cost = repair_cost
|
|
||||||
.map(|cost| {
|
|
||||||
(cost > 0.0)
|
|
||||||
.then_some(cost)
|
|
||||||
.ok_or_else(|| anyhow!("cost price must be greater than 0"))
|
|
||||||
})
|
|
||||||
.transpose()?;
|
|
||||||
|
|
||||||
// Begin ===============================================
|
|
||||||
let sold_price_result = sold_price.map(|price| {
|
|
||||||
(price > 0.0)
|
|
||||||
.then_some(price)
|
|
||||||
.ok_or_else(|| anyhow!("sold price can't be negative"))
|
|
||||||
});
|
|
||||||
let sold_date_result = sold_date.map(|date_str| {
|
|
||||||
NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").with_context(|| {
|
|
||||||
anyhow!(
|
|
||||||
"Invalid date format. Please use YYYY-M-D, e.g. 2023-1-9. You entered: {}",
|
|
||||||
date_str
|
|
||||||
)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
match (&sold_date_result, &sold_price_result) {
|
|
||||||
// sold_date_result 有值,但 sold_price_result 是 None
|
|
||||||
// 或是 sold_price_result 有值,但 sold_date_result 是 None
|
|
||||||
(Some(_), None) | (None, Some(_)) => {
|
|
||||||
if product.sold_price.is_none() && product.sold_date.is_none() {
|
|
||||||
anyhow::bail!(
|
|
||||||
"Both `sold-price` and `sold-date` must be provided together when setting for the first time"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
let validated_sold_price = sold_price_result.transpose()?;
|
|
||||||
let parsed_sold_date = sold_date_result.transpose()?;
|
|
||||||
// =============================================== End
|
|
||||||
|
|
||||||
product.update(
|
|
||||||
validated_price,
|
|
||||||
parsed_purchase_date,
|
|
||||||
parsed_status,
|
|
||||||
// usage_count,
|
|
||||||
repair_count,
|
|
||||||
validated_repair_cost,
|
|
||||||
validated_sold_price,
|
|
||||||
parsed_sold_date,
|
|
||||||
);
|
|
||||||
records.save(record_path)?;
|
|
||||||
}
|
|
||||||
Commands::Show => {
|
|
||||||
let products: Vec<ProductTable> = records
|
|
||||||
.list_products()
|
|
||||||
.iter()
|
|
||||||
.map(|p| ProductTable::from_product(p))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if products.is_empty() {
|
|
||||||
println!("No products found");
|
|
||||||
} else {
|
|
||||||
println!("{}", Table::new(products))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Commands::Delete { name } => {
|
|
||||||
if let Some(name) = name {
|
|
||||||
records.remove_product(&name)?;
|
|
||||||
records.save(record_path)?;
|
|
||||||
} else {
|
|
||||||
let product_names: Vec<String> = records.products.keys().cloned().collect();
|
|
||||||
if product_names.is_empty() {
|
|
||||||
println!("No product to delete.");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let target = dialoguer::Select::new()
|
|
||||||
.with_prompt("Choose product to delete")
|
|
||||||
.items(&product_names)
|
|
||||||
.interact()?;
|
|
||||||
|
|
||||||
let selected_name = &product_names[target];
|
|
||||||
records.remove_product(selected_name)?;
|
|
||||||
let _ = records.save(record_path);
|
|
||||||
println!("Deleted product: {}", selected_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
18
src/model.rs
18
src/model.rs
|
@ -45,7 +45,6 @@ pub struct Product {
|
||||||
pub price: f64,
|
pub price: f64,
|
||||||
pub purchase_date: NaiveDate,
|
pub purchase_date: NaiveDate,
|
||||||
pub status: Option<Status>,
|
pub status: Option<Status>,
|
||||||
// pub usage_count: Option<u32>,
|
|
||||||
pub repair_count: Option<u32>,
|
pub repair_count: Option<u32>,
|
||||||
pub repair_cost: Option<f64>,
|
pub repair_cost: Option<f64>,
|
||||||
pub sold_price: Option<f64>,
|
pub sold_price: Option<f64>,
|
||||||
|
@ -59,7 +58,6 @@ impl Product {
|
||||||
price,
|
price,
|
||||||
purchase_date,
|
purchase_date,
|
||||||
status: Some(Status::Active),
|
status: Some(Status::Active),
|
||||||
// usage_count: None,
|
|
||||||
repair_count: None,
|
repair_count: None,
|
||||||
repair_cost: None,
|
repair_cost: None,
|
||||||
sold_price: None,
|
sold_price: None,
|
||||||
|
@ -73,7 +71,6 @@ impl Product {
|
||||||
price: Option<f64>,
|
price: Option<f64>,
|
||||||
purchase_date: Option<NaiveDate>,
|
purchase_date: Option<NaiveDate>,
|
||||||
status: Option<Status>,
|
status: Option<Status>,
|
||||||
// usage_count: Option<u32>,
|
|
||||||
repair_count: Option<u32>,
|
repair_count: Option<u32>,
|
||||||
repair_cost: Option<f64>,
|
repair_cost: Option<f64>,
|
||||||
sold_price: Option<f64>,
|
sold_price: Option<f64>,
|
||||||
|
@ -89,7 +86,6 @@ impl Product {
|
||||||
self.status = Some(s)
|
self.status = Some(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// self.usage_count = usage_count.or(self.usage_count);
|
|
||||||
self.repair_count = repair_count.or(self.repair_count);
|
self.repair_count = repair_count.or(self.repair_count);
|
||||||
self.repair_cost = repair_cost.or(self.repair_cost);
|
self.repair_cost = repair_cost.or(self.repair_cost);
|
||||||
self.sold_price = sold_price.or(self.sold_price);
|
self.sold_price = sold_price.or(self.sold_price);
|
||||||
|
@ -108,7 +104,7 @@ impl Records {
|
||||||
return Ok(Self::new());
|
return Ok(Self::new());
|
||||||
}
|
}
|
||||||
let content = fs::read_to_string(file_path)
|
let content = fs::read_to_string(file_path)
|
||||||
.with_context(|| format!("Failed to read `record.json` file: {}", file_path))?;
|
.with_context(|| format!("Failed to read `records.json` file: {}", file_path))?;
|
||||||
let records: Records = serde_json::from_str(&content)?;
|
let records: Records = serde_json::from_str(&content)?;
|
||||||
|
|
||||||
Ok(records)
|
Ok(records)
|
||||||
|
@ -123,7 +119,7 @@ impl Records {
|
||||||
pub fn add_product(&mut self, product: Product) -> Result<()> {
|
pub fn add_product(&mut self, product: Product) -> Result<()> {
|
||||||
if self.products.contains_key(&product.name) {
|
if self.products.contains_key(&product.name) {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"product name `{}` already exists. choose another one",
|
"Product name `{}` already exists. choose another one",
|
||||||
product.name
|
product.name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -153,4 +149,14 @@ impl Records {
|
||||||
pub fn list_products(&self) -> Vec<&Product> {
|
pub fn list_products(&self) -> Vec<&Product> {
|
||||||
self.products.values().collect()
|
self.products.values().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn rename_product(&mut self, current_name: &str, new_name: String) -> Result<()> {
|
||||||
|
if self.products.contains_key(&new_name) {
|
||||||
|
anyhow::bail!("Product name `{}` already exists", new_name);
|
||||||
|
}
|
||||||
|
let mut product = self.remove_product(current_name)?;
|
||||||
|
product.name = new_name.clone();
|
||||||
|
self.products.insert(new_name, product);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
185
src/utils.rs
185
src/utils.rs
|
@ -1,10 +1,189 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use chrono::NaiveDate;
|
||||||
use dirs::data_local_dir;
|
use dirs::data_local_dir;
|
||||||
use std::path::PathBuf;
|
use tabled::Table;
|
||||||
|
|
||||||
pub fn setup_records_file() -> Result<PathBuf> {
|
use crate::{
|
||||||
|
display::ProductTable,
|
||||||
|
model::{Product, Records, Status},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn setup_records_file() -> Result<String> {
|
||||||
let local_path = data_local_dir().context("Unable to determine the local directory")?;
|
let local_path = data_local_dir().context("Unable to determine the local directory")?;
|
||||||
let worthit_dir = local_path.join("worthit");
|
let worthit_dir = local_path.join("worthit");
|
||||||
let records_file_path = worthit_dir.join("records.json");
|
let records_file_path = worthit_dir.join("records.json");
|
||||||
Ok(records_file_path)
|
let record_path = records_file_path
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Invalid UTF-8 in record file path"))?;
|
||||||
|
Ok(record_path.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_handler(
|
||||||
|
name: String,
|
||||||
|
price: f64,
|
||||||
|
purchase_date: String,
|
||||||
|
mut records: Records,
|
||||||
|
record_path: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
if name.is_empty() {
|
||||||
|
anyhow::bail!("Product name can't not be empty.")
|
||||||
|
}
|
||||||
|
if price <= 0.0 {
|
||||||
|
anyhow::bail!("Price must be greater than 0.")
|
||||||
|
}
|
||||||
|
let purchase_date =
|
||||||
|
NaiveDate::parse_from_str(&purchase_date, "%Y-%m-%d").with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Invalid date format. Please use YYYY-M-D, e.g. 2023-1-9. You entered: {}",
|
||||||
|
purchase_date
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let product = Product::new(name, price, purchase_date);
|
||||||
|
|
||||||
|
records.add_product(product)?;
|
||||||
|
records.save(&record_path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn set_handler(
|
||||||
|
current_name: Option<String>,
|
||||||
|
new_name: Option<String>,
|
||||||
|
price: Option<f64>,
|
||||||
|
purchase_date: Option<String>,
|
||||||
|
status: Option<u32>,
|
||||||
|
repair_count: Option<u32>,
|
||||||
|
repair_cost: Option<f64>,
|
||||||
|
sold_price: Option<f64>,
|
||||||
|
sold_date: Option<String>,
|
||||||
|
mut records: Records,
|
||||||
|
record_path: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
let current_product_name = current_name
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Name is required"))?;
|
||||||
|
|
||||||
|
let lookup_product = match &new_name {
|
||||||
|
Some(new_name) => {
|
||||||
|
records.rename_product(current_product_name, new_name.into())?;
|
||||||
|
new_name
|
||||||
|
}
|
||||||
|
None => current_product_name,
|
||||||
|
};
|
||||||
|
let product = records.get_product_mut(lookup_product)?;
|
||||||
|
|
||||||
|
let validated_price = price
|
||||||
|
.map(|price| {
|
||||||
|
(price > 0.0)
|
||||||
|
.then_some(price)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Price must be greater than 0"))
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let parsed_purchase_date = purchase_date
|
||||||
|
.map(|date_str| {
|
||||||
|
NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").with_context(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Invalid date format. Please use YYYY-M-D, e.g. 2023-1-9. You entered: {}",
|
||||||
|
date_str
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let parsed_status = status.map(Status::from_u32).transpose()?;
|
||||||
|
|
||||||
|
let validated_repair_cost = repair_cost
|
||||||
|
.map(|cost| {
|
||||||
|
(cost > 0.0)
|
||||||
|
.then_some(cost)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Cost price must be greater than 0"))
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
// Begin ===============================================
|
||||||
|
let sold_price_result = sold_price.map(|price| {
|
||||||
|
(price > 0.0)
|
||||||
|
.then_some(price)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Sold price can't be negative"))
|
||||||
|
});
|
||||||
|
let sold_date_result = sold_date.map(|date_str| {
|
||||||
|
NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").with_context(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Invalid date format. Please use YYYY-M-D, e.g. 2023-1-9. You entered: {}",
|
||||||
|
date_str
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
match (&sold_date_result, &sold_price_result) {
|
||||||
|
// sold_date_result 有值,但 sold_price_result 是 None
|
||||||
|
// 或是 sold_price_result 有值,但 sold_date_result 是 None
|
||||||
|
(Some(_), None) | (None, Some(_)) => {
|
||||||
|
if product.sold_price.is_none() && product.sold_date.is_none() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Both `sold-price` and `sold-date` must be provided together when setting for the first time"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
let validated_sold_price = sold_price_result.transpose()?;
|
||||||
|
let parsed_sold_date = sold_date_result.transpose()?;
|
||||||
|
// =============================================== End
|
||||||
|
|
||||||
|
product.update(
|
||||||
|
validated_price,
|
||||||
|
parsed_purchase_date,
|
||||||
|
parsed_status,
|
||||||
|
repair_count,
|
||||||
|
validated_repair_cost,
|
||||||
|
validated_sold_price,
|
||||||
|
parsed_sold_date,
|
||||||
|
);
|
||||||
|
records.save(&record_path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_handler(records: Records) {
|
||||||
|
let products: Vec<ProductTable> = records
|
||||||
|
.list_products()
|
||||||
|
.iter()
|
||||||
|
.map(|p| ProductTable::from_product(p))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if products.is_empty() {
|
||||||
|
println!("No products found");
|
||||||
|
} else {
|
||||||
|
println!("{}", Table::new(products))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_handler(
|
||||||
|
name: Option<String>,
|
||||||
|
mut records: Records,
|
||||||
|
record_path: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Some(name) = name {
|
||||||
|
records.remove_product(&name)?;
|
||||||
|
records.save(&record_path)?;
|
||||||
|
} else {
|
||||||
|
let product_names: Vec<String> = records.products.keys().cloned().collect();
|
||||||
|
if product_names.is_empty() {
|
||||||
|
println!("No product to delete.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = dialoguer::Select::new()
|
||||||
|
.with_prompt("Choose product to delete")
|
||||||
|
.items(&product_names)
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
let selected_name = &product_names[target];
|
||||||
|
records.remove_product(selected_name)?;
|
||||||
|
let _ = records.save(&record_path);
|
||||||
|
println!("Deleted product: {}", selected_name);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue