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:
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");