app

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

commit 32baa849e6a0aab29b29719c3eb942ad288bc7c0
parent 58486f08ac15b19d1d9b2cee099650a92b5ec38b
Author: triesap <tyson@radroots.org>
Date:   Tue, 21 Apr 2026 18:33:41 +0000

pack_day: add export action and status ui

Diffstat:
Mcrates/launchers/desktop/src/runtime.rs | 63+++++++++++++++++++++++++++++----------------------------------
Mcrates/launchers/desktop/src/window.rs | 360+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mcrates/shared/i18n/src/keys.rs | 16++++++++++++++++
Mcrates/shared/i18n/src/lib.rs | 29+++++++++++++++++++++++++++++
Mi18n/locales/en/messages.json | 16++++++++++++++++
5 files changed, 435 insertions(+), 49 deletions(-)

diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -36,9 +36,8 @@ use radroots_app_state::{ AppStateStore, AppStateStoreError, BuyerBrowseScreenProjection, BuyerCartScreenProjection, BuyerOrdersScreenProjection, BuyerSearchScreenProjection, BuyerSearchScreenQueryState, FarmSetupFlowStage, FarmWorkspaceReadinessProjection, HomeRoute, OrdersScreenProjection, - PackDayExportRequest, PackDayScreenProjection, PersistedAppState, - PersonalWorkspaceProjection, ProductsScreenProjection, ProductsScreenQueryState, - derive_sync_projection, + PackDayExportRequest, PackDayScreenProjection, PersistedAppState, PersonalWorkspaceProjection, + ProductsScreenProjection, ProductsScreenQueryState, derive_sync_projection, }; use radroots_app_sync::{ AppSyncProjection, AppSyncRequest, AppSyncResult, AppSyncTransport, AppSyncTransportError, @@ -406,7 +405,6 @@ impl DesktopAppRuntime { self.lock_state_mut().open_pack_day(fulfillment_window_id) } - #[allow(dead_code)] pub fn export_pack_day(&self) -> Result<bool, DesktopAppRuntimeCommandError> { self.lock_state_mut().export_pack_day() } @@ -1733,7 +1731,6 @@ impl DesktopAppRuntimeState { Ok(query_changed || section_changed || editor_changed) } - #[allow(dead_code)] fn export_pack_day(&mut self) -> Result<bool, DesktopAppRuntimeCommandError> { let Some(farm_id) = self.selected_farm_id() else { return Ok(false); @@ -1763,8 +1760,9 @@ impl DesktopAppRuntimeState { return Ok(false); } - let request = - PackDayExportRequest::for_fulfillment_window(source.fulfillment_window.fulfillment_window_id); + let request = PackDayExportRequest::for_fulfillment_window( + source.fulfillment_window.fulfillment_window_id, + ); let _ = self .state_store .apply_in_memory(AppStateCommand::begin_pack_day_export(request.clone())); @@ -4440,11 +4438,11 @@ mod tests { FarmerActivationProjection, FarmerSection, FulfillmentWindowId, FulfillmentWindowRecord, LoggedOutStartupProjection, OrderId, OrderStatus, OrdersFilter, PackDayExportStatus, PackDayPackListRow, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, - PersonalSection, PickupLocationId, PickupLocationRecord, ProductEditorDraft, - ProductStatus, ProductsFilter, ProductsSort, RecoveryKind, RecoveryRecordId, - ReminderDeliveryState, ReminderFeedProjection, ReminderKind, SelectedSurfaceProjection, - SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, - TodaySetupTaskKind, TodaySummary, + PersonalSection, PickupLocationId, PickupLocationRecord, ProductEditorDraft, ProductStatus, + ProductsFilter, ProductsSort, RecoveryKind, RecoveryRecordId, ReminderDeliveryState, + ReminderFeedProjection, ReminderKind, SelectedSurfaceProjection, SettingsPreference, + SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, + TodaySummary, }; use radroots_app_remote_signer::{ RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSessionRecord, @@ -7073,28 +7071,25 @@ mod tests { .fulfillment_window .clone() .expect("pack day fulfillment window"); - let _ = runtime - .lock_state_mut() - .state_store - .apply_in_memory(AppStateCommand::replace_pack_day_projection( - PackDayProjection { - fulfillment_window: Some(fulfillment_window.clone()), - reminders: ReminderFeedProjection::default(), - totals_by_product: vec![PackDayProductTotalRow { - title: "Bogus totals".to_owned(), - quantity_display: "999 crates".to_owned(), - }], - pack_list: vec![PackDayPackListRow { - title: "Bogus pack list".to_owned(), - quantity_display: "Do not trust screen strings".to_owned(), - }], - pickup_roster: vec![PackDayRosterRow { - order_id: OrderId::new(), - order_number: "R-999".to_owned(), - customer_display_name: "Bogus".to_owned(), - }], - }, - )); + let _ = runtime.lock_state_mut().state_store.apply_in_memory( + AppStateCommand::replace_pack_day_projection(PackDayProjection { + fulfillment_window: Some(fulfillment_window.clone()), + reminders: ReminderFeedProjection::default(), + totals_by_product: vec![PackDayProductTotalRow { + title: "Bogus totals".to_owned(), + quantity_display: "999 crates".to_owned(), + }], + pack_list: vec![PackDayPackListRow { + title: "Bogus pack list".to_owned(), + quantity_display: "Do not trust screen strings".to_owned(), + }], + pickup_roster: vec![PackDayRosterRow { + order_id: OrderId::new(), + order_number: "R-999".to_owned(), + customer_display_name: "Bogus".to_owned(), + }], + }), + ); assert!( runtime diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -19,14 +19,15 @@ use radroots_app_models::{ FarmSetupBlocker, FarmSetupDraft, FarmSummary, FarmTimingConflictKind, FarmerSection, FulfillmentWindowId, FulfillmentWindowRecord, FulfillmentWindowSummary, LoggedOutStartupPhase, OrderDetailItemRow, OrderDetailProjection, OrderId, OrderListRow, OrderPrimaryAction, - OrderRecoveryProjection, OrderStatus, OrdersFilter, OrdersListRow, PackDayPackListRow, - PackDayProductTotalRow, PackDayRosterRow, PersonalEntryState, PersonalSection, - PickupLocationId, PickupLocationRecord, ProductAttentionState, ProductEditorDraft, ProductId, - ProductListRow, ProductPricePresentation, ProductPublishBlocker, ProductStatus, ProductsFilter, - ProductsListRow, ProductsSort, RecoveryKind, RecoveryState, ReminderDeadlineProjection, - ReminderDeliveryState, ReminderId, ReminderLogEntryProjection, ReminderLogProjection, - ReminderSurface, ReminderUrgency, RepeatDemandEligibility, RepeatDemandHandoffProjection, - ShellSection, TodayAgendaProjection, TodaySetupTaskKind, + OrderRecoveryProjection, OrderStatus, OrdersFilter, OrdersListRow, PackDayExportBundle, + PackDayExportStatus, PackDayPackListRow, PackDayProductTotalRow, PackDayRosterRow, + PersonalEntryState, PersonalSection, PickupLocationId, PickupLocationRecord, + ProductAttentionState, ProductEditorDraft, ProductId, ProductListRow, ProductPricePresentation, + ProductPublishBlocker, ProductStatus, ProductsFilter, ProductsListRow, ProductsSort, + RecoveryKind, RecoveryState, ReminderDeadlineProjection, ReminderDeliveryState, ReminderId, + ReminderLogEntryProjection, ReminderLogProjection, ReminderSurface, ReminderUrgency, + RepeatDemandEligibility, RepeatDemandHandoffProjection, ShellSection, TodayAgendaProjection, + TodaySetupTaskKind, }; use radroots_app_remote_signer::{ RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingPollOutcome, @@ -37,7 +38,8 @@ use radroots_app_remote_signer::{ }; use radroots_app_sqlite::derive_farm_rules_readiness; use radroots_app_state::{ - FarmSetupFlowStage, FarmWorkspaceStatus, HomeRoute, derive_product_publish_blockers, + FarmSetupFlowStage, FarmWorkspaceStatus, HomeRoute, PackDayExportProjection, + derive_product_publish_blockers, }; use radroots_app_sync::{ AppSyncRunStatus, SyncAggregateRef, SyncCheckpointState, SyncConflict, SyncConflictKind, @@ -1626,6 +1628,22 @@ impl HomeView { } } + fn export_pack_day(&mut self, cx: &mut Context<Self>) { + match self.runtime.export_pack_day() { + Ok(true) => cx.notify(), + Ok(false) => {} + Err(runtime_error) => { + error!( + target: "pack_day", + event = "pack_day.export_failed", + error = %runtime_error, + "failed to export pack day" + ); + cx.notify(); + } + } + } + fn open_today_next_window( &mut self, fulfillment_window_id: Option<FulfillmentWindowId>, @@ -3355,6 +3373,11 @@ impl HomeView { .max_w(px(APP_UI_THEME.shells.home_card_max_width_px)) .mx_auto() .child(pack_day_title_row(runtime)) + .child(pack_day_export_card( + runtime, + cx.listener(|this, _, _, cx| this.export_pack_day(cx)), + cx, + )) .when(!projection.reminders.is_empty(), |this| { this.child(self.render_reminder_feed_card( "pack-day-reminders", @@ -9787,6 +9810,163 @@ fn order_recovery_kind_index(kind: RecoveryKind) -> usize { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct PackDayExportStatusPresentation { + indicator_color: u32, + title_key: AppTextKey, + body_key: AppTextKey, +} + +fn pack_day_export_card( + runtime: &DesktopAppRuntimeSummary, + on_export: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + cx: &App, +) -> impl IntoElement { + let export = &runtime.pack_day_projection.export; + let status = pack_day_export_status_presentation(runtime); + let detail_rows = pack_day_export_detail_rows(export); + let action = if pack_day_export_action_enabled(runtime) { + action_button_primary( + "pack-day-export", + app_shared_text(AppTextKey::PackDayExportAction), + on_export, + cx, + ) + .into_any_element() + } else { + action_button_primary_disabled( + "pack-day-export", + app_shared_text(pack_day_export_action_label_key(export)), + cx, + ) + .into_any_element() + }; + + home_card( + app_shared_text(AppTextKey::PackDayExportTitle), + app_stack_v(APP_UI_THEME.foundation.spacing.small_px) + .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.body_text_px)) + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .child(app_shared_text(status.title_key)), + ), + ) + .child(home_body_text(app_shared_text(status.body_key))) + .when(!detail_rows.is_empty(), |this| { + this.child(label_value_list(detail_rows)) + }) + .child(div().child(action)), + ) +} + +fn pack_day_export_has_exportable_context(runtime: &DesktopAppRuntimeSummary) -> bool { + let projection = &runtime.pack_day_projection.projection; + projection.fulfillment_window.is_some() && !projection.is_empty() +} + +fn pack_day_export_action_enabled(runtime: &DesktopAppRuntimeSummary) -> bool { + pack_day_export_has_exportable_context(runtime) + && runtime.pack_day_projection.export.status != PackDayExportStatus::Running +} + +fn pack_day_export_action_label_key(export: &PackDayExportProjection) -> AppTextKey { + match export.status { + PackDayExportStatus::Running => AppTextKey::PackDayExportActionRunning, + PackDayExportStatus::Idle + | PackDayExportStatus::Succeeded + | PackDayExportStatus::Failed => AppTextKey::PackDayExportAction, + } +} + +fn pack_day_export_status_presentation( + runtime: &DesktopAppRuntimeSummary, +) -> PackDayExportStatusPresentation { + match runtime.pack_day_projection.export.status { + PackDayExportStatus::Running => PackDayExportStatusPresentation { + indicator_color: APP_UI_THEME.foundation.text.accent, + title_key: AppTextKey::PackDayExportRunningTitle, + body_key: AppTextKey::PackDayExportRunningBody, + }, + PackDayExportStatus::Succeeded => PackDayExportStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.online, + title_key: AppTextKey::PackDayExportSucceededTitle, + body_key: AppTextKey::PackDayExportSucceededBody, + }, + PackDayExportStatus::Failed => PackDayExportStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.attention, + title_key: AppTextKey::PackDayExportFailedTitle, + body_key: AppTextKey::PackDayExportFailedBody, + }, + PackDayExportStatus::Idle if pack_day_export_has_exportable_context(runtime) => { + PackDayExportStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.online, + title_key: AppTextKey::PackDayExportReadyTitle, + body_key: AppTextKey::PackDayExportReadyBody, + } + } + PackDayExportStatus::Idle => PackDayExportStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.offline, + title_key: AppTextKey::PackDayExportUnavailableTitle, + body_key: AppTextKey::PackDayExportUnavailableBody, + }, + } +} + +fn pack_day_export_detail_rows(export: &PackDayExportProjection) -> Vec<LabelValueRow> { + match export.status { + PackDayExportStatus::Succeeded => export + .bundle + .as_ref() + .map(pack_day_export_bundle_rows) + .unwrap_or_default(), + PackDayExportStatus::Failed => export + .error_message + .as_deref() + .map(str::trim) + .filter(|message| !message.is_empty()) + .map(|message| { + vec![LabelValueRow::new( + app_shared_text(AppTextKey::PackDayExportErrorLabel), + message.to_owned(), + )] + }) + .unwrap_or_default(), + PackDayExportStatus::Idle | PackDayExportStatus::Running => Vec::new(), + } +} + +fn pack_day_export_bundle_rows(bundle: &PackDayExportBundle) -> Vec<LabelValueRow> { + vec![ + LabelValueRow::new( + app_shared_text(AppTextKey::PackDayExportFolderLabel), + bundle.bundle_directory.clone(), + ), + LabelValueRow::new( + app_shared_text(AppTextKey::PackDayExportFilesLabel), + pack_day_export_artifact_names(bundle), + ), + ] +} + +fn pack_day_export_artifact_names(bundle: &PackDayExportBundle) -> String { + bundle + .artifacts + .iter() + .map(|artifact| artifact.kind.file_name()) + .collect::<Vec<_>>() + .join(", ") +} + fn pack_day_title_row(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement { app_stack_v(4.0) .child( @@ -11646,7 +11826,8 @@ fn home_farm_order_method_label_key(method: FarmOrderMethod) -> AppTextKey { mod tests { use super::{ APP_UI_THEME, AppTextKey, FarmerHomeFarmState, HomeAutoFocusState, HomeAutoFocusTarget, - HomeStage, ReminderActionTarget, SETTINGS_FARM_PANEL_SECTIONS, SETTINGS_NAVIGATION_ORDER, + HomeStage, LabelValueRow, PackDayExportStatusPresentation, ReminderActionTarget, + SETTINGS_FARM_PANEL_SECTIONS, SETTINGS_NAVIGATION_ORDER, SETTINGS_OPERATIONS_PANEL_SECTIONS, SettingsAutoFocusTarget, SettingsInventorySectionSpec, SettingsPanelViewKey, StartupHomeSurface, StartupSignerConnectState, about_conflict_action_specs, about_conflict_aggregate_text, about_conflict_detail_rows, @@ -11654,7 +11835,9 @@ 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, + 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, 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, @@ -11674,10 +11857,11 @@ mod tests { FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId, FulfillmentWindowSummary, LoggedOutStartupPhase, LoggedOutStartupProjection, OrderDetailProjection, OrderId, OrderPrimaryAction, OrderStatus, OrdersListRow, - PackDayProjection, PersonalSection, ReminderDeadlineProjection, ReminderDeliveryState, - ReminderId, ReminderKind, ReminderSurface, ReminderUrgency, RepeatDemandEligibility, - RepeatDemandHandoffProjection, ShellSection, TodayAgendaProjection, TodaySetupTask, - TodaySetupTaskKind, + PackDayExportArtifact, PackDayExportArtifactKind, PackDayExportBundle, + PackDayProductTotalRow, PackDayProjection, PersonalSection, ReminderDeadlineProjection, + ReminderDeliveryState, ReminderId, ReminderKind, ReminderSurface, ReminderUrgency, + RepeatDemandEligibility, RepeatDemandHandoffProjection, ShellSection, + TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, }; use radroots_app_remote_signer::{ RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingSession, @@ -11685,6 +11869,7 @@ mod tests { }; use radroots_app_state::{ AppShellProjection, FarmWorkspaceReadinessProjection, FarmWorkspaceStatus, HomeRoute, + PackDayExportProjection, }; use radroots_app_sync::{ AppSyncProjection, AppSyncRunStatus, SyncAggregateRef, SyncCheckpointStatus, SyncConflict, @@ -12250,6 +12435,151 @@ mod tests { } #[test] + fn pack_day_export_action_enabled_requires_a_window_and_exportable_rows() { + let farm_id = FarmId::new(); + let fulfillment_window_id = FulfillmentWindowId::new(); + let mut runtime = summary( + HomeRoute::Today, + TodayAgendaProjection::default(), + FarmSetupProjection::default(), + ); + + assert!(!pack_day_export_action_enabled(&runtime)); + assert_eq!( + pack_day_export_status_presentation(&runtime), + PackDayExportStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.offline, + title_key: AppTextKey::PackDayExportUnavailableTitle, + body_key: AppTextKey::PackDayExportUnavailableBody, + } + ); + + runtime.pack_day_projection.projection = PackDayProjection { + fulfillment_window: Some(FulfillmentWindowSummary { + fulfillment_window_id, + farm_id, + starts_at: String::new(), + ends_at: String::new(), + }), + reminders: Default::default(), + totals_by_product: Vec::new(), + pack_list: Vec::new(), + pickup_roster: Vec::new(), + }; + + assert!(!pack_day_export_action_enabled(&runtime)); + + runtime.pack_day_projection.projection.totals_by_product = vec![PackDayProductTotalRow { + title: "Salad mix".to_owned(), + quantity_display: "2 bags".to_owned(), + }]; + + assert!(pack_day_export_action_enabled(&runtime)); + assert_eq!( + pack_day_export_status_presentation(&runtime), + PackDayExportStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.online, + title_key: AppTextKey::PackDayExportReadyTitle, + body_key: AppTextKey::PackDayExportReadyBody, + } + ); + + runtime.pack_day_projection.export = PackDayExportProjection::running( + radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id), + ); + assert!(!pack_day_export_action_enabled(&runtime)); + assert_eq!( + pack_day_export_action_label_key(&runtime.pack_day_projection.export), + AppTextKey::PackDayExportActionRunning + ); + assert_eq!( + pack_day_export_status_presentation(&runtime), + PackDayExportStatusPresentation { + indicator_color: APP_UI_THEME.foundation.text.accent, + title_key: AppTextKey::PackDayExportRunningTitle, + body_key: AppTextKey::PackDayExportRunningBody, + } + ); + } + + #[test] + fn pack_day_export_detail_rows_surface_bundle_and_failure_details() { + let fulfillment_window_id = FulfillmentWindowId::new(); + let bundle = PackDayExportBundle { + fulfillment_window_id, + generated_at_utc: "2026-04-23T15:00:00Z".to_owned(), + bundle_directory: "exports/pack_day/window-1/20260423T150000Z".to_owned(), + artifacts: vec![ + PackDayExportArtifact { + kind: PackDayExportArtifactKind::PackSheet, + relative_path: "pack_sheet.txt".to_owned(), + }, + PackDayExportArtifact { + kind: PackDayExportArtifactKind::PickupRoster, + relative_path: "pickup_roster.txt".to_owned(), + }, + PackDayExportArtifact { + kind: PackDayExportArtifactKind::CustomerLabels, + relative_path: "customer_labels.txt".to_owned(), + }, + ], + }; + let request = + radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id); + + let rows = pack_day_export_detail_rows(&PackDayExportProjection::succeeded( + request.clone(), + bundle.clone(), + )); + assert_eq!(rows.len(), 2); + assert_eq!( + rows[0], + LabelValueRow::new( + app_text(AppTextKey::PackDayExportFolderLabel), + "exports/pack_day/window-1/20260423T150000Z" + ) + ); + assert_eq!( + rows[1], + LabelValueRow::new( + app_text(AppTextKey::PackDayExportFilesLabel), + "pack_sheet.txt, pickup_roster.txt, customer_labels.txt" + ) + ); + assert_eq!( + pack_day_export_artifact_names(&bundle), + "pack_sheet.txt, pickup_roster.txt, customer_labels.txt" + ); + + let failed = PackDayExportProjection::failed(request, "disk unavailable"); + assert_eq!( + pack_day_export_detail_rows(&failed), + vec![LabelValueRow::new( + app_text(AppTextKey::PackDayExportErrorLabel), + "disk unavailable" + )] + ); + assert_eq!( + pack_day_export_status_presentation(&DesktopAppRuntimeSummary { + pack_day_projection: radroots_app_state::PackDayScreenProjection { + export: failed, + ..Default::default() + }, + ..summary( + HomeRoute::Today, + TodayAgendaProjection::default(), + FarmSetupProjection::default(), + ) + }), + PackDayExportStatusPresentation { + indicator_color: APP_UI_THEME.components.app_status_indicator.attention, + title_key: AppTextKey::PackDayExportFailedTitle, + body_key: AppTextKey::PackDayExportFailedBody, + } + ); + } + + #[test] fn sidebar_navigation_keeps_the_active_destination_first() { assert_eq!( home_sidebar_navigation_sections(FarmerSection::Today, true, false), diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs @@ -225,6 +225,22 @@ define_app_text_keys! { PackDayPickupRosterTitle => "pack_day.pickup_roster.title", PackDayEmptyTitle => "pack_day.empty.title", PackDayEmptyBody => "pack_day.empty.body", + PackDayExportTitle => "pack_day.export.title", + PackDayExportReadyTitle => "pack_day.export.ready.title", + PackDayExportReadyBody => "pack_day.export.ready.body", + PackDayExportUnavailableTitle => "pack_day.export.unavailable.title", + PackDayExportUnavailableBody => "pack_day.export.unavailable.body", + PackDayExportRunningTitle => "pack_day.export.running.title", + PackDayExportRunningBody => "pack_day.export.running.body", + PackDayExportSucceededTitle => "pack_day.export.succeeded.title", + PackDayExportSucceededBody => "pack_day.export.succeeded.body", + PackDayExportFailedTitle => "pack_day.export.failed.title", + PackDayExportFailedBody => "pack_day.export.failed.body", + PackDayExportAction => "pack_day.export.action", + PackDayExportActionRunning => "pack_day.export.action.running", + PackDayExportFolderLabel => "pack_day.export.folder.label", + PackDayExportFilesLabel => "pack_day.export.files.label", + PackDayExportErrorLabel => "pack_day.export.error.label", HomeTodayRemindersTitle => "home.today.reminders.title", ReminderDeadlineLabel => "reminder.deadline.label", ReminderUrgencyUpcoming => "reminder.urgency.upcoming", diff --git a/crates/shared/i18n/src/lib.rs b/crates/shared/i18n/src/lib.rs @@ -446,6 +446,35 @@ mod tests { app_text(AppTextKey::PackDayEmptyTitle), "Nothing to pack yet" ); + assert_eq!(app_text(AppTextKey::PackDayExportTitle), "Export pack day"); + assert_eq!( + app_text(AppTextKey::PackDayExportReadyTitle), + "Ready to save locally" + ); + assert_eq!( + app_text(AppTextKey::PackDayExportUnavailableTitle), + "Not ready yet" + ); + assert_eq!( + app_text(AppTextKey::PackDayExportRunningTitle), + "Saving locally" + ); + assert_eq!( + app_text(AppTextKey::PackDayExportSucceededTitle), + "Saved locally" + ); + assert_eq!( + app_text(AppTextKey::PackDayExportFailedTitle), + "Couldn't save export" + ); + assert_eq!(app_text(AppTextKey::PackDayExportAction), "Export pack day"); + assert_eq!( + app_text(AppTextKey::PackDayExportActionRunning), + "Exporting..." + ); + assert_eq!(app_text(AppTextKey::PackDayExportFolderLabel), "Folder"); + assert_eq!(app_text(AppTextKey::PackDayExportFilesLabel), "Files"); + assert_eq!(app_text(AppTextKey::PackDayExportErrorLabel), "Error"); } #[test] diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -205,6 +205,22 @@ "pack_day.pickup_roster.title": "Pickup roster", "pack_day.empty.title": "Nothing to pack yet", "pack_day.empty.body": "Orders for this window will appear here when they are ready to pack.", + "pack_day.export.title": "Export pack day", + "pack_day.export.ready.title": "Ready to save locally", + "pack_day.export.ready.body": "Save a pack sheet, pickup roster, and customer labels for this window.", + "pack_day.export.unavailable.title": "Not ready yet", + "pack_day.export.unavailable.body": "Exports become available when this window has orders ready to pack.", + "pack_day.export.running.title": "Saving locally", + "pack_day.export.running.body": "Creating the pack sheet, pickup roster, and customer labels for this window.", + "pack_day.export.succeeded.title": "Saved locally", + "pack_day.export.succeeded.body": "Pack day files are ready in the folder below.", + "pack_day.export.failed.title": "Couldn't save export", + "pack_day.export.failed.body": "Pack day files couldn't be saved locally. Review the error below and try again.", + "pack_day.export.action": "Export pack day", + "pack_day.export.action.running": "Exporting...", + "pack_day.export.folder.label": "Folder", + "pack_day.export.files.label": "Files", + "pack_day.export.error.label": "Error", "reminder.deadline.label": "Due", "reminder.urgency.upcoming": "Upcoming", "reminder.urgency.due_soon": "Due soon",