diff --git a/migrations/0005_add_action_to_timeline.sql b/migrations/0005_add_action_to_timeline.sql new file mode 100644 index 0000000..5151eb9 --- /dev/null +++ b/migrations/0005_add_action_to_timeline.sql @@ -0,0 +1 @@ +ALTER TABLE timeline_events ADD COLUMN action TEXT NOT NULL DEFAULT 'added'; diff --git a/src/application/routes/bags.rs b/src/application/routes/bags.rs index 8b42a18..0e90695 100644 --- a/src/application/routes/bags.rs +++ b/src/application/routes/bags.rs @@ -139,6 +139,7 @@ pub(crate) async fn create_bag( let event = NewTimelineEvent { entity_type: "bag".to_string(), entity_id: bag.id.into_inner(), + action: "added".to_string(), occurred_at: chrono::Utc::now(), title: roast.name.to_string(), details: vec![ @@ -235,6 +236,7 @@ pub(crate) async fn update_bag( let event = NewTimelineEvent { entity_type: "bag".to_string(), entity_id: bag.id.into_inner(), + action: "finished".to_string(), occurred_at: chrono::Utc::now(), title: format!("{}", roast.name), details: vec![TimelineEventDetail { diff --git a/src/domain/timeline.rs b/src/domain/timeline.rs index 3512ef9..72b25c3 100644 --- a/src/domain/timeline.rs +++ b/src/domain/timeline.rs @@ -15,6 +15,7 @@ pub struct TimelineEvent { pub id: TimelineEventId, pub entity_type: String, pub entity_id: i64, + pub action: String, pub occurred_at: DateTime, pub title: String, pub details: Vec, @@ -27,6 +28,7 @@ pub struct TimelineEvent { pub struct NewTimelineEvent { pub entity_type: String, pub entity_id: i64, + pub action: String, pub occurred_at: DateTime, pub title: String, pub details: Vec, diff --git a/src/infrastructure/repositories/roasters.rs b/src/infrastructure/repositories/roasters.rs index d217f5f..ec7c311 100644 --- a/src/infrastructure/repositories/roasters.rs +++ b/src/infrastructure/repositories/roasters.rs @@ -130,10 +130,11 @@ impl RoasterRepository for SqlRoasterRepository { let details_json = Self::details_for_roaster(&roaster)?; query( - "INSERT INTO timeline_events (entity_type, entity_id, occurred_at, title, details_json, tasting_notes_json) VALUES (?, ?, ?, ?, ?, ?)", + "INSERT INTO timeline_events (entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json) VALUES (?, ?, ?, ?, ?, ?, ?)", ) .bind("roaster") .bind(i64::from(roaster.id)) + .bind("added") .bind(roaster.created_at) .bind(&roaster.name) .bind(details_json) diff --git a/src/infrastructure/repositories/roasts.rs b/src/infrastructure/repositories/roasts.rs index 266fb1a..4a61d95 100644 --- a/src/infrastructure/repositories/roasts.rs +++ b/src/infrastructure/repositories/roasts.rs @@ -166,10 +166,11 @@ impl RoastRepository for SqlRoastRepository { }; query( - "INSERT INTO timeline_events (entity_type, entity_id, occurred_at, title, details_json, tasting_notes_json) VALUES (?, ?, ?, ?, ?, ?)", + "INSERT INTO timeline_events (entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json) VALUES (?, ?, ?, ?, ?, ?, ?)", ) .bind("roast") .bind(i64::from(roast.id)) + .bind("added") .bind(roast.created_at) .bind(&roast.name) .bind(details_json) diff --git a/src/infrastructure/repositories/timeline_events.rs b/src/infrastructure/repositories/timeline_events.rs index d639093..d9667e5 100644 --- a/src/infrastructure/repositories/timeline_events.rs +++ b/src/infrastructure/repositories/timeline_events.rs @@ -25,9 +25,9 @@ impl SqlTimelineEventRepository { impl TimelineEventRepository for SqlTimelineEventRepository { async fn insert(&self, event: NewTimelineEvent) -> Result { let query = r#" - INSERT INTO timeline_events (entity_type, entity_id, occurred_at, title, details_json, tasting_notes_json) - VALUES (?, ?, ?, ?, ?, ?) - RETURNING id, entity_type, entity_id, occurred_at, title, details_json, tasting_notes_json + INSERT INTO timeline_events (entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json) + VALUES (?, ?, ?, ?, ?, ?, ?) + RETURNING id, entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json "#; let details_json = serde_json::to_string(&event.details).map_err(|err| { @@ -43,6 +43,7 @@ impl TimelineEventRepository for SqlTimelineEventRepository { let record = sqlx::query_as::<_, TimelineEventRecord>(query) .bind(event.entity_type) .bind(event.entity_id) + .bind(event.action) .bind(event.occurred_at) .bind(event.title) .bind(details_json) @@ -65,7 +66,7 @@ impl TimelineEventRepository for SqlTimelineEventRepository { let order_clause = format!("t.occurred_at {direction_sql}, t.id DESC"); let base_query = "SELECT - t.id, t.entity_type, t.entity_id, t.occurred_at, t.title, t.details_json, t.tasting_notes_json, + t.id, t.entity_type, t.entity_id, t.action, t.occurred_at, t.title, t.details_json, t.tasting_notes_json, CASE WHEN t.entity_type = 'roaster' THEN r.slug WHEN t.entity_type = 'roast' THEN rst.slug @@ -103,6 +104,7 @@ struct TimelineEventRecord { id: i64, entity_type: String, entity_id: i64, + action: String, occurred_at: DateTime, title: String, details_json: Option, @@ -137,6 +139,7 @@ impl TimelineEventRecord { id: TimelineEventId::from(self.id), entity_type: self.entity_type, entity_id: self.entity_id, + action: self.action, occurred_at: self.occurred_at, title: self.title, details, diff --git a/src/presentation/web/views.rs b/src/presentation/web/views.rs index 01c9d84..2efc92a 100644 --- a/src/presentation/web/views.rs +++ b/src/presentation/web/views.rs @@ -440,6 +440,7 @@ impl TimelineEventView { id, entity_type, entity_id, + action, occurred_at, title, details, @@ -448,16 +449,11 @@ impl TimelineEventView { roaster_slug, } = event; - let kind_label = match entity_type.as_str() { - "roaster" => "Roaster Added", - "roast" => "Roast Added", - "bag" => { - if title.starts_with("Finished") { - "Bag Finished" - } else { - "Bag Added" - } - } + let kind_label = match (entity_type.as_str(), action.as_str()) { + ("roaster", "added") => "Roaster Added", + ("roast", "added") => "Roast Added", + ("bag", "added") => "Bag Added", + ("bag", "finished") => "Bag Finished", _ => "Event", }; diff --git a/tests/server/timeline.rs b/tests/server/timeline.rs index 46f362c..29df14e 100644 --- a/tests/server/timeline.rs +++ b/tests/server/timeline.rs @@ -410,8 +410,4 @@ async fn closing_a_bag_surfaces_on_the_timeline() { body.contains("Bag Finished"), "Expected 'Bag Finished' badge in timeline HTML, got: {body}" ); - assert!( - body.contains(&format!("Finished: {}", roast_name)), - "Expected bag finished title to appear in timeline HTML, got: {body}" - ); }