app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 2add92927a857f4d4b46fa6bd895df653e7aa1ce
parent 34a5a7f29904667d701f4118f0f93679ec4c909e
Author: triesap <tyson@radroots.org>
Date:   Tue, 28 Apr 2026 18:06:03 +0000

app: wire pack day batch print action

- expose runtime prepare and finish paths for batch print
- run batch print from the pack day export card
- disable conflicting print and host-handoff actions while work is running
- cover runtime and window batch print state transitions

Diffstat:
Mcrates/launchers/desktop/src/runtime.rs | 341++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/launchers/desktop/src/source_guards.rs | 14++++++++++++++
Mcrates/launchers/desktop/src/window.rs | 406++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
3 files changed, 737 insertions(+), 24 deletions(-)

diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -16,11 +16,11 @@ use radroots_app_models::{ FarmId, FarmOrderMethod, FarmProfileRecord, FarmReadiness, FarmRulesProjection, FarmSetupDraft, FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId, LoggedOutStartupProjection, OrderDetailProjection, OrderId, OrderRecoveryProjection, - OrdersFilter, OrdersListProjection, OrdersScreenQueryState, PackDayExportBundle, - PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, - PackDayPrintKind, PackDayPrintStatus, PackDayProjection, PackDayScreenQueryState, - PersonalSection, PickupLocationRecord, ProductEditorDraft, ProductId, ProductsFilter, - ProductsListProjection, ProductsSort, RecoveryKind, RecoveryQueueProjection, + OrdersFilter, OrdersListProjection, OrdersScreenQueryState, PackDayBatchPrintStatus, + PackDayExportBundle, PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind, + PackDayHostHandoffStatus, PackDayPrintKind, PackDayPrintStatus, PackDayProjection, + PackDayScreenQueryState, PersonalSection, PickupLocationRecord, ProductEditorDraft, ProductId, + ProductsFilter, ProductsListProjection, ProductsSort, RecoveryKind, RecoveryQueueProjection, RecoveryRecordId, RecoveryState, ReminderDeadlineProjection, ReminderDeliveryState, ReminderFeedProjection, ReminderId, ReminderKind, ReminderLogEntryProjection, ReminderLogProjection, ReminderSurface, ReminderUrgency, SettingsAccountProjection, @@ -38,9 +38,9 @@ use radroots_app_state::{ AppStateStore, AppStateStoreError, BuyerBrowseScreenProjection, BuyerCartScreenProjection, BuyerOrdersScreenProjection, BuyerSearchScreenProjection, BuyerSearchScreenQueryState, FarmSetupFlowStage, FarmWorkspaceReadinessProjection, HomeRoute, OrdersScreenProjection, - PackDayExportRequest, PackDayHostHandoffRequest, PackDayPrintRequest, PackDayScreenProjection, - PersistedAppState, PersonalWorkspaceProjection, ProductsScreenProjection, - ProductsScreenQueryState, APP_STATE_FILE_NAME, + PackDayBatchPrintRequest, PackDayExportRequest, PackDayHostHandoffRequest, PackDayPrintRequest, + PackDayScreenProjection, PersistedAppState, PersonalWorkspaceProjection, + ProductsScreenProjection, ProductsScreenQueryState, APP_STATE_FILE_NAME, }; use radroots_app_sync::{ AppSyncProjection, AppSyncRequest, AppSyncResult, AppSyncTransport, AppSyncTransportError, @@ -64,7 +64,8 @@ use crate::pack_day_host_handoff::{ }; use crate::pack_day_print::{ cleanup_prepared_customer_label_asset_root, - cleanup_prepared_customer_label_assets_for_export_instance, plan_pack_day_print, + cleanup_prepared_customer_label_assets_for_export_instance, plan_pack_day_batch_print, + plan_pack_day_print, PackDayBatchPrintCommandPlan, PackDayBatchPrintError, PackDayPrintCommandPlan, PackDayPrintError, }; use crate::remote_signer::{ @@ -455,6 +456,24 @@ impl DesktopAppRuntime { self.lock_state_mut().finish_pack_day_print(request, result) } + pub fn prepare_pack_day_batch_print( + &self, + ) -> Result< + Option<(PackDayBatchPrintRequest, PackDayBatchPrintCommandPlan)>, + DesktopAppRuntimeCommandError, + > { + self.lock_state_mut().prepare_pack_day_batch_print() + } + + pub fn finish_pack_day_batch_print( + &self, + request: PackDayBatchPrintRequest, + result: Result<(), PackDayBatchPrintError>, + ) -> Result<bool, DesktopAppRuntimeCommandError> { + self.lock_state_mut() + .finish_pack_day_batch_print(request, result) + } + pub fn update_product_stock( &self, product_id: ProductId, @@ -1860,6 +1879,8 @@ impl DesktopAppRuntimeState { if self.state_store.pack_day_projection().host_handoff.status == PackDayHostHandoffStatus::Running || self.state_store.pack_day_projection().print.status == PackDayPrintStatus::Running + || self.state_store.pack_day_projection().batch_print.status + == PackDayBatchPrintStatus::Running { return Ok(None); } @@ -1896,6 +1917,8 @@ impl DesktopAppRuntimeState { if self.state_store.pack_day_projection().print.status == PackDayPrintStatus::Running || self.state_store.pack_day_projection().host_handoff.status == PackDayHostHandoffStatus::Running + || self.state_store.pack_day_projection().batch_print.status + == PackDayBatchPrintStatus::Running { return Ok(None); } @@ -1926,6 +1949,83 @@ impl DesktopAppRuntimeState { } } + fn prepare_pack_day_batch_print( + &mut self, + ) -> Result< + Option<(PackDayBatchPrintRequest, PackDayBatchPrintCommandPlan)>, + DesktopAppRuntimeCommandError, + > { + if self.state_store.pack_day_projection().batch_print.status + == PackDayBatchPrintStatus::Running + || self.state_store.pack_day_projection().print.status == PackDayPrintStatus::Running + || self.state_store.pack_day_projection().host_handoff.status + == PackDayHostHandoffStatus::Running + { + return Ok(None); + } + + let Some(bundle) = self.current_pack_day_export_bundle() else { + return Ok(None); + }; + let request = PackDayBatchPrintRequest::for_bundle(&bundle); + let _ = self + .state_store + .apply_in_memory(AppStateCommand::begin_pack_day_batch_print(request.clone())); + + match plan_pack_day_batch_print(&bundle) { + Ok(plan) => Ok(Some((request, plan))), + Err(error) => { + let _ = + self.state_store + .apply_in_memory(AppStateCommand::fail_pack_day_batch_print( + request, + error.failed_artifact(), + error.failure_kind(), + )); + Err(error.into()) + } + } + } + + fn finish_pack_day_batch_print( + &mut self, + request: PackDayBatchPrintRequest, + result: Result<(), PackDayBatchPrintError>, + ) -> Result<bool, DesktopAppRuntimeCommandError> { + if !self.current_pack_day_batch_print_request_matches(&request) { + return Ok(false); + } + + let cleanup_export_instance_id = request.export_instance_id; + + match result { + Ok(()) => { + let changed = self + .state_store + .apply_in_memory(AppStateCommand::succeed_pack_day_batch_print(request)); + self.cleanup_prepared_pack_day_print_assets_for_export_instance( + cleanup_export_instance_id, + "batch_print_completion", + ); + Ok(changed) + } + Err(error) => { + let _ = + self.state_store + .apply_in_memory(AppStateCommand::fail_pack_day_batch_print( + request, + error.failed_artifact(), + error.failure_kind(), + )); + self.cleanup_prepared_pack_day_print_assets_for_export_instance( + cleanup_export_instance_id, + "batch_print_completion", + ); + Err(error.into()) + } + } + } + fn finish_pack_day_print( &mut self, request: PackDayPrintRequest, @@ -3374,6 +3474,15 @@ impl DesktopAppRuntimeState { pack_day.print.status == PackDayPrintStatus::Running && pack_day.print.request.as_ref() == Some(request) } + + fn current_pack_day_batch_print_request_matches( + &self, + request: &PackDayBatchPrintRequest, + ) -> bool { + let pack_day = self.state_store.pack_day_projection(); + pack_day.batch_print.status == PackDayBatchPrintStatus::Running + && pack_day.batch_print.request.as_ref() == Some(request) + } } #[derive(Debug, Error)] @@ -3394,6 +3503,8 @@ pub enum DesktopAppRuntimeCommandError { PackDayHostHandoff(#[from] PackDayHostHandoffError), #[error(transparent)] PackDayPrint(#[from] PackDayPrintError), + #[error(transparent)] + PackDayBatchPrint(#[from] PackDayBatchPrintError), } #[derive(Debug, Error)] @@ -4723,7 +4834,8 @@ mod tests { FarmOperatingRulesRecord, FarmOrderMethod, FarmProfileRecord, FarmReadiness, FarmReadinessBlocker, FarmSetupDraft, FarmSetupProjection, FarmSummary, FarmerActivationProjection, FarmerSection, FulfillmentWindowId, FulfillmentWindowRecord, - LoggedOutStartupProjection, OrderId, OrderStatus, OrdersFilter, PackDayExportInstanceId, + LoggedOutStartupProjection, OrderId, OrderStatus, OrdersFilter, PackDayBatchPrintArtifact, + PackDayBatchPrintFailureKind, PackDayBatchPrintStatus, PackDayExportInstanceId, PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow, PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintStatus, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, PersonalSection, PickupLocationId, @@ -4762,7 +4874,9 @@ mod tests { SYNC_TRANSPORT_UNAVAILABLE_MESSAGE, }; use crate::pack_day_host_handoff::PackDayHostHandoffError; - use crate::pack_day_print::{prepared_customer_label_asset_root, PackDayPrintError}; + use crate::pack_day_print::{ + prepared_customer_label_asset_root, PackDayBatchPrintError, PackDayPrintError, + }; #[derive(Clone)] struct SharedRecordedSyncTransport(Arc<Mutex<RecordedAppSyncTransport>>); @@ -7525,6 +7639,211 @@ mod tests { } #[test] + fn runtime_prepare_pack_day_batch_print_uses_the_current_export_bundle_for_all_v1_documents() { + let (runtime, paths) = bootstrapped_runtime("pack_day_batch_print_prepare"); + let (_, farm_id) = provision_ready_farmer_account(&runtime); + + seed_order_workspace(&runtime, farm_id); + assert!(runtime.open_pack_day(None).expect("pack day should open")); + assert!(runtime + .export_pack_day() + .expect("pack day export should succeed")); + + let (request, plan) = runtime + .prepare_pack_day_batch_print() + .expect("batch print should prepare") + .expect("batch print should produce a plan"); + + let summary = runtime.summary(); + let bundle = summary + .pack_day_projection + .export + .bundle + .as_ref() + .expect("pack day export bundle"); + assert_eq!( + summary.pack_day_projection.batch_print.status, + PackDayBatchPrintStatus::Running + ); + assert_eq!( + summary.pack_day_projection.batch_print.request, + Some(request.clone()) + ); + assert_eq!(request.export_instance_id, bundle.export_instance_id); + assert_eq!(plan.export_instance_id, bundle.export_instance_id); + assert_eq!( + plan.plans + .iter() + .map(|plan| PackDayBatchPrintArtifact::from_print_kind(plan.kind)) + .collect::<Vec<_>>(), + Vec::from(PackDayBatchPrintArtifact::all_v1()) + ); + assert!( + plan.plans + .iter() + .all(|plan| plan.command_program == "lp") + ); + + assert!(runtime + .finish_pack_day_batch_print(request, Ok(())) + .expect("batch print success should apply")); + assert_eq!( + runtime.summary().pack_day_projection.batch_print.status, + PackDayBatchPrintStatus::Succeeded + ); + + cleanup_bootstrapped_runtime_paths(&paths); + } + + #[test] + fn runtime_pack_day_batch_print_blocks_conflicting_pack_day_actions() { + let (runtime, paths) = bootstrapped_runtime("pack_day_batch_print_conflicts"); + let (_, farm_id) = provision_ready_farmer_account(&runtime); + + seed_order_workspace(&runtime, farm_id); + assert!(runtime.open_pack_day(None).expect("pack day should open")); + assert!(runtime + .export_pack_day() + .expect("pack day export should succeed")); + + let (_, _) = runtime + .prepare_pack_day_batch_print() + .expect("batch print should prepare") + .expect("batch print should produce a plan"); + assert!(runtime + .prepare_pack_day_print(PackDayPrintKind::PrintPackSheet) + .expect("print prepare should not fail") + .is_none()); + assert!(runtime + .prepare_pack_day_host_handoff(PackDayHostHandoffKind::RevealBundle) + .expect("host handoff prepare should not fail") + .is_none()); + + let _ = runtime + .lock_state_mut() + .state_store + .apply_in_memory(AppStateCommand::reset_pack_day_batch_print()); + + let (print_request, _) = runtime + .prepare_pack_day_print(PackDayPrintKind::PrintPackSheet) + .expect("print should prepare") + .expect("print should produce a plan"); + assert!(runtime + .prepare_pack_day_batch_print() + .expect("batch print prepare should not fail") + .is_none()); + assert!(runtime + .finish_pack_day_print(print_request, Ok(())) + .expect("print success should apply")); + let _ = runtime + .lock_state_mut() + .state_store + .apply_in_memory(AppStateCommand::reset_pack_day_print()); + + let (_, _) = runtime + .prepare_pack_day_host_handoff(PackDayHostHandoffKind::RevealBundle) + .expect("host handoff should prepare") + .expect("host handoff should produce a plan"); + assert!(runtime + .prepare_pack_day_batch_print() + .expect("batch print prepare should not fail") + .is_none()); + + cleanup_bootstrapped_runtime_paths(&paths); + } + + #[test] + fn runtime_finish_pack_day_batch_print_records_failures_and_cleans_prepared_assets() { + let (runtime, paths) = bootstrapped_runtime("pack_day_batch_print_failure_cleanup"); + let (_, farm_id) = provision_ready_farmer_account(&runtime); + + seed_order_workspace(&runtime, farm_id); + assert!(runtime.open_pack_day(None).expect("pack day should open")); + assert!(runtime + .export_pack_day() + .expect("pack day export should succeed")); + + let (request, plan) = runtime + .prepare_pack_day_batch_print() + .expect("batch print should prepare") + .expect("batch print should produce a plan"); + let prepared_directory = plan + .plans + .iter() + .find(|plan| plan.kind == PackDayPrintKind::PrintCustomerLabels) + .and_then(|plan| plan.target_path.parent()) + .expect("prepared customer labels parent") + .to_path_buf(); + assert!(prepared_directory.is_dir()); + + let failed_artifact = + PackDayBatchPrintArtifact::from_print_kind(PackDayPrintKind::PrintPickupRoster); + let error = runtime + .finish_pack_day_batch_print( + request.clone(), + Err(PackDayBatchPrintError::QueueExit { + submitted_artifacts: vec![PackDayBatchPrintArtifact::from_print_kind( + PackDayPrintKind::PrintPackSheet, + )], + failed_artifact, + source: PackDayPrintError::UnsupportedPlatform, + }), + ) + .expect_err("batch print failure should surface"); + assert!(matches!( + error, + DesktopAppRuntimeCommandError::PackDayBatchPrint( + PackDayBatchPrintError::QueueExit { .. } + ) + )); + assert!(!prepared_directory.exists()); + + let summary = runtime.summary(); + let batch_print = &summary.pack_day_projection.batch_print; + assert_eq!(batch_print.status, PackDayBatchPrintStatus::Failed); + assert_eq!(batch_print.request, Some(request)); + assert_eq!(batch_print.failed_artifact, Some(failed_artifact)); + assert_eq!( + batch_print.failure, + Some(PackDayBatchPrintFailureKind::QueueExit) + ); + + cleanup_bootstrapped_runtime_paths(&paths); + } + + #[test] + fn runtime_finish_pack_day_batch_print_ignores_stale_background_completion() { + let (runtime, paths) = bootstrapped_runtime("pack_day_batch_print_stale"); + let (_, farm_id) = provision_ready_farmer_account(&runtime); + + seed_order_workspace(&runtime, farm_id); + assert!(runtime.open_pack_day(None).expect("pack day should open")); + assert!(runtime + .export_pack_day() + .expect("pack day export should succeed")); + + let (request, _) = runtime + .prepare_pack_day_batch_print() + .expect("batch print should prepare") + .expect("batch print should produce a plan"); + + let _ = runtime + .lock_state_mut() + .state_store + .apply_in_memory(AppStateCommand::reset_pack_day_batch_print()); + + assert!(!runtime + .finish_pack_day_batch_print(request, Ok(())) + .expect("stale completion should no-op")); + assert_eq!( + runtime.summary().pack_day_projection.batch_print.status, + PackDayBatchPrintStatus::Idle + ); + + cleanup_bootstrapped_runtime_paths(&paths); + } + + #[test] fn runtime_finish_pack_day_print_records_failures_in_state() { let (runtime, paths) = bootstrapped_runtime("pack_day_print_failure"); let (_, farm_id) = provision_ready_farmer_account(&runtime); diff --git a/crates/launchers/desktop/src/source_guards.rs b/crates/launchers/desktop/src/source_guards.rs @@ -83,8 +83,10 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "failed to open new product editor", "failed to acknowledge reminder", "failed to export pack day", + "failed to complete pack day batch print", "failed to complete pack day host handoff", "failed to complete pack day print", + "failed to prepare pack day batch print", "failed to prepare pack day host handoff", "failed to prepare pack day print", "failed to open order detail", @@ -155,10 +157,13 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "pack-day-open-customer-labels", "pack-day-open-pack-sheet", "pack-day-open-pickup-roster", + "pack-day-print-all", "pack-day-print-customer-labels", "pack-day-print-pack-sheet", "pack-day-print-pickup-roster", "pack_day", + "pack_day.batch_print_failed", + "pack_day.batch_print_prepare_failed", "pack_day.host_handoff_failed", "pack_day.host_handoff_prepare_failed", "pack_day.print_failed", @@ -486,6 +491,15 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::PackDayExportFolderLabel", "AppTextKey::PackDayExportFilesLabel", "AppTextKey::PackDayExportErrorLabel", + "AppTextKey::PackDayBatchPrintAction", + "AppTextKey::PackDayBatchPrintActionRunning", + "AppTextKey::PackDayBatchPrintQueuedTitle", + "AppTextKey::PackDayBatchPrintSucceededTitle", + "AppTextKey::PackDayBatchPrintFailedTitle", + "AppTextKey::PackDayBatchPrintFailedPreflightTitle", + "AppTextKey::PackDayBatchPrintFailedQueueLaunchTitle", + "AppTextKey::PackDayBatchPrintFailedQueueExitTitle", + "AppTextKey::PackDayBatchPrintCustomerLabelsAvery5160OverflowFailedTitle", "AppTextKey::PackDayPrintCustomerLabelsAvery5160OverflowFailedTitle", "AppTextKey::PackDayHostHandoffRevealAction", "AppTextKey::PackDayHostHandoffRevealActionRunning", diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -19,7 +19,8 @@ use radroots_app_models::{ FarmSetupBlocker, FarmSetupDraft, FarmSummary, FarmTimingConflictKind, FarmerSection, FulfillmentWindowId, FulfillmentWindowRecord, FulfillmentWindowSummary, LoggedOutStartupPhase, OrderDetailItemRow, OrderDetailProjection, OrderId, OrderListRow, OrderPrimaryAction, - OrderRecoveryProjection, OrderStatus, OrdersFilter, OrdersListRow, PackDayExportBundle, + OrderRecoveryProjection, OrderStatus, OrdersFilter, OrdersListRow, + PackDayBatchPrintFailureKind, PackDayBatchPrintStatus, PackDayExportBundle, PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow, PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintStatus, PackDayProductTotalRow, PackDayRosterRow, PersonalEntryState, PersonalSection, PickupLocationId, PickupLocationRecord, @@ -39,8 +40,9 @@ use radroots_app_remote_signer::{ }; use radroots_app_sqlite::derive_farm_rules_readiness; use radroots_app_state::{ - FarmSetupFlowStage, FarmWorkspaceStatus, HomeRoute, PackDayExportProjection, - PackDayHostHandoffRequest, PackDayPrintRequest, derive_product_publish_blockers, + FarmSetupFlowStage, FarmWorkspaceStatus, HomeRoute, PackDayBatchPrintRequest, + PackDayExportProjection, PackDayHostHandoffRequest, PackDayPrintRequest, + derive_product_publish_blockers, }; use radroots_app_sync::{ AppSyncRunStatus, SyncAggregateRef, SyncCheckpointState, SyncConflict, SyncConflictKind, @@ -78,7 +80,8 @@ use crate::pack_day_host_handoff::{ PackDayHostHandoffCommandPlan, PackDayHostHandoffError, execute_pack_day_host_handoff_plan, }; use crate::pack_day_print::{ - PackDayPrintCommandPlan, PackDayPrintError, execute_pack_day_print_plan, + PackDayBatchPrintCommandPlan, PackDayBatchPrintError, PackDayPrintCommandPlan, + PackDayPrintError, execute_pack_day_batch_print_plan, execute_pack_day_print_plan, }; use crate::runtime::{ DesktopAppRuntime, DesktopAppRuntimeSummary, DesktopAppSyncConflictSummary, @@ -1724,6 +1727,34 @@ impl HomeView { } } + fn start_pack_day_batch_print(&mut self, window: &mut Window, cx: &mut Context<Self>) { + match self.runtime.prepare_pack_day_batch_print() { + Ok(Some((request, plan))) => { + cx.notify(); + cx.spawn_in(window, async move |this, cx| { + let result = cx + .background_executor() + .spawn(run_pack_day_batch_print(plan)) + .await; + let _ = this.update(cx, |this, cx| { + this.finish_pack_day_batch_print(request, result, cx); + }); + }) + .detach(); + } + Ok(None) => {} + Err(runtime_error) => { + error!( + target: "pack_day", + event = "pack_day.batch_print_prepare_failed", + error = %runtime_error, + "failed to prepare pack day batch print" + ); + cx.notify(); + } + } + } + fn finish_pack_day_host_handoff( &mut self, request: PackDayHostHandoffRequest, @@ -1770,6 +1801,27 @@ impl HomeView { } } + fn finish_pack_day_batch_print( + &mut self, + request: PackDayBatchPrintRequest, + result: Result<(), PackDayBatchPrintError>, + cx: &mut Context<Self>, + ) { + match self.runtime.finish_pack_day_batch_print(request, result) { + Ok(true) => cx.notify(), + Ok(false) => {} + Err(runtime_error) => { + error!( + target: "pack_day", + event = "pack_day.batch_print_failed", + error = %runtime_error, + "failed to complete pack day batch print" + ); + cx.notify(); + } + } + } + fn open_today_next_window( &mut self, fulfillment_window_id: Option<FulfillmentWindowId>, @@ -3530,6 +3582,7 @@ impl HomeView { cx, ) }), + cx.listener(|this, _, window, cx| this.start_pack_day_batch_print(window, cx)), cx.listener(|this, _, window, cx| { this.start_pack_day_print(PackDayPrintKind::PrintPackSheet, window, cx) }), @@ -9179,6 +9232,12 @@ async fn run_pack_day_print(plan: PackDayPrintCommandPlan) -> Result<(), PackDay execute_pack_day_print_plan(&plan) } +async fn run_pack_day_batch_print( + plan: PackDayBatchPrintCommandPlan, +) -> Result<(), PackDayBatchPrintError> { + execute_pack_day_batch_print_plan(&plan) +} + async fn run_startup_signer_pending_poll( record: radroots_app_remote_signer::RadrootsAppRemoteSignerSessionRecord, client_secret_key_hex: String, @@ -10016,6 +10075,18 @@ struct PackDayPrintStatusPresentation { title_key: AppTextKey, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct PackDayBatchPrintActionPresentation { + label_key: AppTextKey, + enabled: bool, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct PackDayBatchPrintStatusPresentation { + indicator_color: u32, + title_key: AppTextKey, +} + fn pack_day_export_card( runtime: &DesktopAppRuntimeSummary, on_export: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, @@ -10023,6 +10094,7 @@ fn pack_day_export_card( on_open_pack_sheet: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, on_open_pickup_roster: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, on_open_customer_labels: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + on_print_all: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, on_print_pack_sheet: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, on_print_pickup_roster: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, on_print_customer_labels: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, @@ -10033,6 +10105,8 @@ fn pack_day_export_card( let detail_rows = pack_day_export_detail_rows(export); let host_handoff_actions = pack_day_host_handoff_action_presentations(runtime); let host_handoff_status = pack_day_host_handoff_status_presentation(runtime); + let batch_print_action = pack_day_batch_print_action_presentation(runtime); + let batch_print_status = pack_day_batch_print_status_presentation(runtime); let print_actions = pack_day_print_action_presentations(runtime); let print_status = pack_day_print_status_presentation(runtime); let host_handoff_error_message = runtime @@ -10205,6 +10279,32 @@ fn pack_day_export_card( }), ) }) + .when_some(batch_print_action, |this, action| { + let button = if action.enabled { + action_button( + "pack-day-print-all", + app_shared_text(action.label_key), + on_print_all, + cx, + ) + .into_any_element() + } else { + action_button_disabled( + "pack-day-print-all", + app_shared_text(action.label_key), + cx, + ) + .into_any_element() + }; + this.child( + app_stack_v(APP_UI_THEME.foundation.spacing.small_px) + .w_full() + .child(button) + .when_some(batch_print_status, |this, status| { + this.child(pack_day_batch_print_status_note(status)) + }), + ) + }) .when(!print_actions.is_empty(), |this| { let on_print_pack_sheet = Arc::new(on_print_pack_sheet); let on_print_pickup_roster = Arc::new(on_print_pickup_roster); @@ -10309,6 +10409,8 @@ fn pack_day_host_handoff_action_presentations( .then(|| host_handoff.request.as_ref().map(|request| request.kind)) .flatten(); let print_running = runtime.pack_day_projection.print.status == PackDayPrintStatus::Running; + let batch_print_running = + runtime.pack_day_projection.batch_print.status == PackDayBatchPrintStatus::Running; PackDayHostHandoffKind::all_v1() .into_iter() @@ -10317,6 +10419,7 @@ fn pack_day_host_handoff_action_presentations( label_key: pack_day_host_handoff_action_label_key(kind, running_kind), enabled: running_kind.is_none() && !print_running + && !batch_print_running && pack_day_host_handoff_action_is_available(bundle, kind), }) .collect() @@ -10356,6 +10459,94 @@ fn pack_day_export_artifact_path( Some(PathBuf::from(&bundle.bundle_directory).join(relative_path)) } +fn pack_day_batch_print_action_presentation( + runtime: &DesktopAppRuntimeSummary, +) -> Option<PackDayBatchPrintActionPresentation> { + let bundle = pack_day_export_succeeded_bundle(runtime)?; + let batch_print = &runtime.pack_day_projection.batch_print; + let batch_running = batch_print.status == PackDayBatchPrintStatus::Running; + let print_running = runtime.pack_day_projection.print.status == PackDayPrintStatus::Running; + let host_handoff_running = + runtime.pack_day_projection.host_handoff.status == PackDayHostHandoffStatus::Running; + let all_artifacts_available = PackDayPrintKind::all_v1() + .into_iter() + .all(|kind| pack_day_print_action_is_available(bundle, kind)); + + Some(PackDayBatchPrintActionPresentation { + label_key: if batch_running { + AppTextKey::PackDayBatchPrintActionRunning + } else { + AppTextKey::PackDayBatchPrintAction + }, + enabled: !batch_running + && !print_running + && !host_handoff_running + && all_artifacts_available, + }) +} + +fn pack_day_batch_print_status_presentation( + runtime: &DesktopAppRuntimeSummary, +) -> Option<PackDayBatchPrintStatusPresentation> { + let batch_print = &runtime.pack_day_projection.batch_print; + + let status = match ( + batch_print.status, + batch_print.failed_artifact, + batch_print.failure, + ) { + (PackDayBatchPrintStatus::Idle, _, _) => return None, + (PackDayBatchPrintStatus::Running, _, _) => PackDayBatchPrintStatusPresentation { + indicator_color: APP_UI_THEME.foundation.text.accent, + title_key: AppTextKey::PackDayBatchPrintQueuedTitle, + }, + (PackDayBatchPrintStatus::Succeeded, _, _) => PackDayBatchPrintStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.online, + title_key: AppTextKey::PackDayBatchPrintSucceededTitle, + }, + ( + PackDayBatchPrintStatus::Failed, + _, + Some(PackDayBatchPrintFailureKind::CustomerLabelsAvery5160Overflow), + ) => PackDayBatchPrintStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.attention, + title_key: AppTextKey::PackDayBatchPrintCustomerLabelsAvery5160OverflowFailedTitle, + }, + (PackDayBatchPrintStatus::Failed, Some(failed_artifact), _) => { + PackDayBatchPrintStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.attention, + title_key: pack_day_print_failed_title_key(failed_artifact.print_kind), + } + } + (PackDayBatchPrintStatus::Failed, None, Some(PackDayBatchPrintFailureKind::Preflight)) => { + PackDayBatchPrintStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.attention, + title_key: AppTextKey::PackDayBatchPrintFailedPreflightTitle, + } + } + ( + PackDayBatchPrintStatus::Failed, + None, + Some(PackDayBatchPrintFailureKind::QueueLaunch), + ) => PackDayBatchPrintStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.attention, + title_key: AppTextKey::PackDayBatchPrintFailedQueueLaunchTitle, + }, + (PackDayBatchPrintStatus::Failed, None, Some(PackDayBatchPrintFailureKind::QueueExit)) => { + PackDayBatchPrintStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.attention, + title_key: AppTextKey::PackDayBatchPrintFailedQueueExitTitle, + } + } + (PackDayBatchPrintStatus::Failed, None, _) => PackDayBatchPrintStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.attention, + title_key: AppTextKey::PackDayBatchPrintFailedTitle, + }, + }; + + Some(status) +} + fn pack_day_print_action_presentations( runtime: &DesktopAppRuntimeSummary, ) -> Vec<PackDayPrintActionPresentation> { @@ -10369,6 +10560,8 @@ fn pack_day_print_action_presentations( .flatten(); let host_handoff_running = runtime.pack_day_projection.host_handoff.status == PackDayHostHandoffStatus::Running; + let batch_print_running = + runtime.pack_day_projection.batch_print.status == PackDayBatchPrintStatus::Running; PackDayPrintKind::all_v1() .into_iter() @@ -10377,6 +10570,7 @@ fn pack_day_print_action_presentations( label_key: pack_day_print_action_label_key(kind, running_kind), enabled: running_kind.is_none() && !host_handoff_running + && !batch_print_running && pack_day_print_action_is_available(bundle, kind), }) .collect() @@ -10414,6 +10608,14 @@ fn pack_day_print_action_label_key( } } +fn pack_day_print_failed_title_key(kind: PackDayPrintKind) -> AppTextKey { + match kind { + PackDayPrintKind::PrintPackSheet => AppTextKey::PackDayPrintPackSheetFailedTitle, + PackDayPrintKind::PrintPickupRoster => AppTextKey::PackDayPrintPickupRosterFailedTitle, + PackDayPrintKind::PrintCustomerLabels => AppTextKey::PackDayPrintCustomerLabelsFailedTitle, + } +} + fn pack_day_print_status_presentation( runtime: &DesktopAppRuntimeSummary, ) -> Option<PackDayPrintStatusPresentation> { @@ -10649,6 +10851,26 @@ fn pack_day_print_status_note(status: PackDayPrintStatusPresentation) -> impl In ) } +fn pack_day_batch_print_status_note( + status: PackDayBatchPrintStatusPresentation, +) -> impl IntoElement { + app_stack_v(4.0).w_full().child( + div() + .w_full() + .flex() + .items_center() + .gap(px(APP_UI_THEME.shells.settings_account_status_gap_px)) + .child(status_indicator(status.indicator_color)) + .child( + div() + .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px)) + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .child(app_shared_text(status.title_key)), + ), + ) +} + fn pack_day_export_succeeded_bundle( runtime: &DesktopAppRuntimeSummary, ) -> Option<&PackDayExportBundle> { @@ -12592,7 +12814,8 @@ fn home_farm_order_method_label_key(method: FarmOrderMethod) -> AppTextKey { mod tests { use super::{ APP_UI_THEME, AppTextKey, FarmerHomeFarmState, HomeAutoFocusState, HomeAutoFocusTarget, - HomeStage, LabelValueRow, PackDayExportStatusPresentation, + HomeStage, LabelValueRow, PackDayBatchPrintActionPresentation, + PackDayBatchPrintStatusPresentation, PackDayExportStatusPresentation, PackDayHostHandoffActionPresentation, PackDayHostHandoffStatusPresentation, PackDayPrintActionPresentation, PackDayPrintStatusPresentation, ReminderActionTarget, SETTINGS_FARM_PANEL_SECTIONS, SETTINGS_NAVIGATION_ORDER, @@ -12603,11 +12826,13 @@ mod tests { about_status_rows, app_text, buyer_orders_status_key, farm_setup_onboarding_card_spec, farmer_home_farm_state, farmer_pack_day_available, home_auto_focus_target, home_content_scroll_id, home_saved_farm, home_sidebar_navigation_sections, home_stage, - home_window_launch_size_px, home_window_minimum_size_px, pack_day_export_action_enabled, - pack_day_export_action_label_key, pack_day_export_artifact_names, - pack_day_export_detail_rows, pack_day_export_status_presentation, - pack_day_host_handoff_action_presentations, pack_day_host_handoff_status_presentation, - pack_day_print_action_presentations, pack_day_print_status_presentation, + home_window_launch_size_px, home_window_minimum_size_px, + pack_day_batch_print_action_presentation, pack_day_batch_print_status_presentation, + pack_day_export_action_enabled, pack_day_export_action_label_key, + pack_day_export_artifact_names, pack_day_export_detail_rows, + pack_day_export_status_presentation, pack_day_host_handoff_action_presentations, + pack_day_host_handoff_status_presentation, pack_day_print_action_presentations, + pack_day_print_status_presentation, parse_optional_product_editor_stock_input, parse_product_editor_price_input, presented_farmer_reminder, product_display_title, reminder_action_target, reminder_deadline_text, reminder_delivery_state_key, reminder_urgency_color, @@ -12627,7 +12852,8 @@ mod tests { FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId, FulfillmentWindowSummary, LoggedOutStartupPhase, LoggedOutStartupProjection, OrderDetailProjection, OrderId, OrderPrimaryAction, OrderStatus, OrdersListRow, - PackDayExportArtifact, PackDayExportArtifactKind, PackDayExportBundle, + PackDayBatchPrintArtifact, PackDayBatchPrintFailureKind, PackDayExportArtifact, + PackDayExportArtifactKind, PackDayExportBundle, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintStatus, PackDayProductTotalRow, PackDayProjection, PersonalSection, ReminderDeadlineProjection, ReminderDeliveryState, ReminderId, @@ -12641,8 +12867,9 @@ mod tests { }; use radroots_app_state::{ AppShellProjection, FarmWorkspaceReadinessProjection, FarmWorkspaceStatus, HomeRoute, - PackDayExportProjection, PackDayHostHandoffProjection, PackDayHostHandoffRequest, - PackDayPrintProjection, PackDayPrintRequest, + PackDayBatchPrintProjection, PackDayBatchPrintRequest, PackDayExportProjection, + PackDayHostHandoffProjection, PackDayHostHandoffRequest, PackDayPrintProjection, + PackDayPrintRequest, }; use radroots_app_sync::{ AppSyncProjection, AppSyncRunStatus, SyncAggregateRef, SyncCheckpointStatus, SyncConflict, @@ -13614,6 +13841,159 @@ mod tests { } #[test] + fn pack_day_batch_print_action_only_surfaces_after_a_successful_export() { + let temp_dir = TestDirectory::new(); + write_artifact(temp_dir.path(), "pack_sheet.txt"); + write_artifact(temp_dir.path(), "pickup_roster.txt"); + write_artifact(temp_dir.path(), "customer_labels.txt"); + let bundle = sample_pack_day_bundle(temp_dir.path()); + let fulfillment_window_id = bundle.fulfillment_window_id; + let mut runtime = summary( + HomeRoute::Today, + TodayAgendaProjection::default(), + FarmSetupProjection::default(), + ); + + assert_eq!(pack_day_batch_print_action_presentation(&runtime), None); + + runtime.pack_day_projection.export = PackDayExportProjection::succeeded( + radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id), + bundle, + ); + + assert_eq!( + pack_day_batch_print_action_presentation(&runtime), + Some(PackDayBatchPrintActionPresentation { + label_key: AppTextKey::PackDayBatchPrintAction, + enabled: true, + }) + ); + } + + #[test] + fn pack_day_batch_print_running_disables_conflicting_pack_day_actions() { + let temp_dir = TestDirectory::new(); + write_artifact(temp_dir.path(), "pack_sheet.txt"); + write_artifact(temp_dir.path(), "pickup_roster.txt"); + write_artifact(temp_dir.path(), "customer_labels.txt"); + let bundle = sample_pack_day_bundle(temp_dir.path()); + let fulfillment_window_id = bundle.fulfillment_window_id; + let export_request = + radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id); + let batch_request = PackDayBatchPrintRequest::for_bundle(&bundle); + let mut runtime = summary( + HomeRoute::Today, + TodayAgendaProjection::default(), + FarmSetupProjection::default(), + ); + runtime.pack_day_projection.export = + PackDayExportProjection::succeeded(export_request, bundle); + runtime.pack_day_projection.batch_print = + PackDayBatchPrintProjection::running(batch_request); + + assert_eq!( + pack_day_batch_print_action_presentation(&runtime), + Some(PackDayBatchPrintActionPresentation { + label_key: AppTextKey::PackDayBatchPrintActionRunning, + enabled: false, + }) + ); + assert!( + pack_day_print_action_presentations(&runtime) + .into_iter() + .all(|action| !action.enabled) + ); + assert!( + pack_day_host_handoff_action_presentations(&runtime) + .into_iter() + .all(|action| !action.enabled) + ); + } + + #[test] + fn pack_day_batch_print_status_tracks_outcomes_and_failed_artifacts() { + let temp_dir = TestDirectory::new(); + write_artifact(temp_dir.path(), "pack_sheet.txt"); + write_artifact(temp_dir.path(), "pickup_roster.txt"); + write_artifact(temp_dir.path(), "customer_labels.txt"); + let bundle = sample_pack_day_bundle(temp_dir.path()); + let fulfillment_window_id = bundle.fulfillment_window_id; + let export_request = + radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id); + let batch_request = PackDayBatchPrintRequest::for_bundle(&bundle); + let mut runtime = summary( + HomeRoute::Today, + TodayAgendaProjection::default(), + FarmSetupProjection::default(), + ); + runtime.pack_day_projection.export = + PackDayExportProjection::succeeded(export_request, bundle); + + runtime.pack_day_projection.batch_print = + PackDayBatchPrintProjection::running(batch_request.clone()); + assert_eq!( + pack_day_batch_print_status_presentation(&runtime), + Some(PackDayBatchPrintStatusPresentation { + indicator_color: APP_UI_THEME.foundation.text.accent, + title_key: AppTextKey::PackDayBatchPrintQueuedTitle, + }) + ); + + runtime.pack_day_projection.batch_print = + PackDayBatchPrintProjection::succeeded(batch_request.clone()); + assert_eq!( + pack_day_batch_print_status_presentation(&runtime), + Some(PackDayBatchPrintStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.online, + title_key: AppTextKey::PackDayBatchPrintSucceededTitle, + }) + ); + + runtime.pack_day_projection.batch_print = PackDayBatchPrintProjection::failed( + batch_request.clone(), + Some(PackDayBatchPrintArtifact::from_print_kind( + PackDayPrintKind::PrintPickupRoster, + )), + PackDayBatchPrintFailureKind::QueueExit, + ); + assert_eq!( + pack_day_batch_print_status_presentation(&runtime), + Some(PackDayBatchPrintStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.attention, + title_key: AppTextKey::PackDayPrintPickupRosterFailedTitle, + }) + ); + + runtime.pack_day_projection.batch_print = PackDayBatchPrintProjection::failed( + batch_request.clone(), + None, + PackDayBatchPrintFailureKind::Preflight, + ); + assert_eq!( + pack_day_batch_print_status_presentation(&runtime), + Some(PackDayBatchPrintStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.attention, + title_key: AppTextKey::PackDayBatchPrintFailedPreflightTitle, + }) + ); + + runtime.pack_day_projection.batch_print = PackDayBatchPrintProjection::failed( + batch_request, + Some(PackDayBatchPrintArtifact::from_print_kind( + PackDayPrintKind::PrintCustomerLabels, + )), + PackDayBatchPrintFailureKind::CustomerLabelsAvery5160Overflow, + ); + assert_eq!( + pack_day_batch_print_status_presentation(&runtime), + Some(PackDayBatchPrintStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.attention, + title_key: AppTextKey::PackDayBatchPrintCustomerLabelsAvery5160OverflowFailedTitle, + }) + ); + } + + #[test] fn pack_day_print_running_and_failure_postures_track_the_active_request() { let temp_dir = TestDirectory::new(); write_artifact(temp_dir.path(), "pack_sheet.txt");