brewlog/src/domain/gear.rs
Jon Seager e01b6d0a1c
feat: add optional created_at to entity creation and updates
Add `created_at: Option<DateTime<Utc>>` through all layers so CLI users
can backdate entities at creation/update time. When omitted, falls back
to `Utc::now()`.

- Domain: add field to all New*/Update* structs with serde(default)
- Domain: timeline events use entity created_at instead of Utc::now()
- Repos: unify INSERT to explicit Rust-side created_at with unwrap_or_else
- Repos: add created_at to UPDATE dynamic query builders
- Routes: add field to submission structs and has_changes guards
- Clients: pass created_at through manual JSON client methods
- CLI: add --created-at flag with parse_created_at helper (RFC 3339 or YYYY-MM-DD)
2026-02-07 09:48:20 +00:00

162 lines
4.3 KiB
Rust

use std::str::FromStr;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::ids::GearId;
use super::listing::{SortDirection, SortKey};
use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GearCategory {
Grinder,
Brewer,
#[serde(rename = "filter_paper")]
FilterPaper,
}
impl GearCategory {
pub fn as_str(&self) -> &'static str {
match self {
GearCategory::Grinder => "grinder",
GearCategory::Brewer => "brewer",
GearCategory::FilterPaper => "filter_paper",
}
}
pub fn display_label(&self) -> &'static str {
match self {
GearCategory::Grinder => "Grinder",
GearCategory::Brewer => "Brewer",
GearCategory::FilterPaper => "Filter Paper",
}
}
}
impl FromStr for GearCategory {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"grinder" => Ok(GearCategory::Grinder),
"brewer" => Ok(GearCategory::Brewer),
"filter_paper" => Ok(GearCategory::FilterPaper),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Gear {
pub id: GearId,
pub category: GearCategory,
pub make: String,
pub model: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Gear {
pub fn to_timeline_event(&self) -> NewTimelineEvent {
NewTimelineEvent {
entity_type: "gear".to_string(),
entity_id: self.id.into_inner(),
action: "added".to_string(),
occurred_at: self.created_at,
title: format!("{} {}", self.make, self.model),
details: vec![
TimelineEventDetail {
label: "Category".to_string(),
value: self.category.display_label().to_string(),
},
TimelineEventDetail {
label: "Make".to_string(),
value: self.make.clone(),
},
TimelineEventDetail {
label: "Model".to_string(),
value: self.model.clone(),
},
],
tasting_notes: vec![],
slug: None,
roaster_slug: None,
brew_data: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewGear {
pub category: GearCategory,
pub make: String,
pub model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateGear {
pub make: Option<String>,
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Default, Clone)]
pub struct GearFilter {
pub category: Option<GearCategory>,
}
impl GearFilter {
pub fn all() -> Self {
Self::default()
}
pub fn for_category(category: GearCategory) -> Self {
Self {
category: Some(category),
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum GearSortKey {
Make,
Model,
Category,
CreatedAt,
}
impl SortKey for GearSortKey {
fn default() -> Self {
GearSortKey::CreatedAt
}
fn from_query(value: &str) -> Option<Self> {
match value {
"make" => Some(GearSortKey::Make),
"model" => Some(GearSortKey::Model),
"category" => Some(GearSortKey::Category),
"created-at" => Some(GearSortKey::CreatedAt),
_ => None,
}
}
fn query_value(self) -> &'static str {
match self {
GearSortKey::Make => "make",
GearSortKey::Model => "model",
GearSortKey::Category => "category",
GearSortKey::CreatedAt => "created-at",
}
}
fn default_direction(self) -> SortDirection {
match self {
GearSortKey::Make | GearSortKey::Model | GearSortKey::Category => SortDirection::Asc,
GearSortKey::CreatedAt => SortDirection::Desc,
}
}
}