app

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

window.rs (725673B)


      1 use gpui::{
      2     Animation, AnimationExt, AnyElement, App, AppContext, Bounds, ClickEvent, Context, ElementId,
      3     Entity, Image, ImageFormat, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render,
      4     SharedString, Styled, StyledImage, Subscription, Timer, Window, WindowBounds, WindowOptions,
      5     div, img, prelude::FluentBuilder, px, relative, rgb, size, transparent_black,
      6 };
      7 use gpui_component::{
      8     Icon, IconName, IndexPath, Root, Sizable, Size,
      9     input::InputEvent,
     10     input::InputState,
     11     menu::PopupMenuItem,
     12     select::{SearchableVec, Select, SelectDelegate, SelectEvent, SelectState},
     13 };
     14 use radroots_app_core::{
     15     AppSdkLifecycleState, AppSdkProjectionLifecycleState, AppSdkRelayUrlPolicy,
     16 };
     17 use radroots_app_i18n::{AppTextKey, app_text};
     18 use radroots_app_remote_signer::{
     19     RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingPollOutcome,
     20     RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSource,
     21     radroots_app_remote_signer_connect_pending,
     22     radroots_app_remote_signer_poll_pending_session_with_progress,
     23     radroots_app_remote_signer_preview, radroots_app_remote_signer_requested_permissions,
     24 };
     25 use radroots_app_sqlite::{AppSqliteError, derive_farm_rules_readiness};
     26 use radroots_app_state::{
     27     BuyerOrdersScreenProjection, FarmSetupFlowStage, FarmWorkspaceStatus, HomeRoute,
     28     PackDayBatchPrintRequest, PackDayExportProjection, PackDayHostHandoffRequest,
     29     PackDayPrintRequest, derive_product_publish_blockers,
     30 };
     31 use radroots_app_sync::{
     32     AppSyncRunStatus, SyncAggregateRef, SyncCheckpointState, SyncConflict, SyncConflictKind,
     33     SyncConflictResolutionStatus, SyncConflictSeverity,
     34 };
     35 use radroots_app_ui::{
     36     APP_UI_THEME, AppCheckboxFieldSpec, AppFormFieldSpec, AppPillTabSpec,
     37     AppSegmentButtonIconSpec as IconSegmentButtonSpec, AppUnderlineTabSpec, LabelValueRow,
     38     SettingsPreferencesGeneralRowState, app_button_account_selector_row as account_selector_row,
     39     app_button_card, app_button_choice as choice_button,
     40     app_button_compact as action_button_compact, app_button_ellipsis_menu as action_ellipsis_menu,
     41     app_button_list_row as list_row_button, app_button_primary as action_button_primary,
     42     app_button_primary_compact as action_button_primary_compact,
     43     app_button_primary_compact_disabled as action_button_primary_compact_disabled,
     44     app_button_primary_disabled as action_button_primary_disabled,
     45     app_button_primary_full_width as action_button_primary_full_width,
     46     app_button_secondary as action_button, app_button_secondary_disabled as action_button_disabled,
     47     app_button_secondary_full_width as action_button_full_width, app_button_sidebar_account_menu,
     48     app_button_square_dropdown_secondary as action_dropdown_button, app_button_text as text_button,
     49     app_checkbox_field, app_cluster, app_detail_row, app_divider as section_divider,
     50     app_focused_detail_view, app_focused_task_view, app_form_field, app_form_input_text,
     51     app_form_section, app_heading_section, app_heading_view, app_input_text as app_text_input,
     52     app_pill_tabs, app_scroll_panel, app_segment_button_icon as icon_segment_button,
     53     app_shared_label_text, app_shared_text, app_split_shell, app_stack_h, app_stack_v,
     54     app_status_indicator as status_indicator, app_surface_card,
     55     app_surface_card_section as home_card, app_surface_panel, app_surface_sidebar,
     56     app_surface_window as app_window_shell, app_text_badge as settings_badge_text,
     57     app_text_body_subtle as home_body_text, app_text_label,
     58     app_text_label as home_farm_setup_field_label, app_text_value, app_underline_tabs,
     59     label_value_list, runtime_metadata_rows, settings_preferences_general_rows, utility_title_row,
     60 };
     61 pub use radroots_app_view::SettingsSection as SettingsPanelViewKey;
     62 use radroots_app_view::{
     63     AccountCustody, AccountSummary, ActiveSurface, AppStartupGate, BlackoutPeriodId,
     64     BlackoutPeriodRecord, BuyerCartProjection, BuyerCartReplaceConfirmationProjection,
     65     BuyerListingRow, BuyerOrderDetailProjection, BuyerOrderReviewDraft,
     66     BuyerOrderReviewSummaryProjection, BuyerOrderStatus, BuyerOrdersListRow,
     67     BuyerProductDetailProjection, FarmId, FarmOperatingRulesRecord, FarmOrderMethod,
     68     FarmProfileRecord, FarmReadinessBlocker, FarmRulesProjection, FarmRulesReadiness,
     69     FarmSetupBlocker, FarmSetupDraft, FarmSummary, FarmTimingConflictKind, FarmerSection,
     70     FulfillmentWindowId, FulfillmentWindowRecord, FulfillmentWindowSummary, LoggedOutStartupPhase,
     71     OrderDetailItemRow, OrderDetailProjection, OrderId, OrderListRow, OrderPrimaryAction,
     72     OrderStatus, OrdersFilter, OrdersListRow, PackDayBatchPrintFailureKind,
     73     PackDayBatchPrintStatus, PackDayExportBundle, PackDayExportStatus, PackDayHostHandoffKind,
     74     PackDayHostHandoffStatus, PackDayPackListRow, PackDayPrintFailureKind, PackDayPrintKind,
     75     PackDayPrintStatus, PackDayProductTotalRow, PackDayRosterRow, PersonalEntryState,
     76     PersonalSection, PickupLocationId, PickupLocationRecord, ProductAttentionState,
     77     ProductEditorDraft, ProductId, ProductListRow, ProductPricePresentation, ProductPublishBlocker,
     78     ProductStatus, ProductsFilter, ProductsListRow, ProductsSort, ReminderDeadlineProjection,
     79     ReminderDeliveryState, ReminderId, ReminderLogEntryProjection, ReminderLogProjection,
     80     ReminderSurface, ReminderUrgency, RepeatDemandEligibility, RepeatDemandHandoffProjection,
     81     SettingsAccountProjection, ShellSection, TodayAgendaProjection, TodaySetupTaskKind,
     82     TradeAgreementStatus, TradeEconomicsProjection, TradeInventoryStatus, TradeRevisionStatus,
     83     TradeValidationReceiptProjection, TradeValidationReceiptResult, TradeValidationReceiptType,
     84     TradeWorkflowProjection, TradeWorkflowSource,
     85 };
     86 use radroots_nostr::prelude::RadrootsNostrClient;
     87 use std::{
     88     collections::BTreeSet,
     89     path::{Component, Path, PathBuf},
     90     sync::Arc,
     91     time::Duration,
     92 };
     93 use tracing::error;
     94 
     95 use crate::pack_day_host_handoff::{
     96     PackDayHostHandoffCommandPlan, PackDayHostHandoffError, execute_pack_day_host_handoff_plan,
     97 };
     98 use crate::pack_day_print::{
     99     PackDayBatchPrintCommandPlan, PackDayBatchPrintError, PackDayPrintCommandPlan,
    100     PackDayPrintError, execute_pack_day_batch_print_plan, execute_pack_day_print_plan,
    101 };
    102 use crate::runtime::{
    103     DesktopAppRuntime, DesktopAppRuntimeProductEditorSaveError,
    104     DesktopAppRuntimeProductStockUpdateError, DesktopAppRuntimeSummary,
    105     DesktopAppSdkDiagnosticsState, DesktopAppSdkDiagnosticsSummary, DesktopAppSdkIssueSummary,
    106     DesktopAppSdkReadyDiagnosticsSummary, DesktopAppSdkStatusSummary,
    107     DesktopAppSyncConflictSummary, DesktopAppSyncStatusSummary,
    108 };
    109 
    110 const HOME_WINDOW_MIN_WIDTH_PX: f32 = 1080.0;
    111 const HOME_WINDOW_MIN_HEIGHT_PX: f32 = 720.0;
    112 
    113 pub fn home_titlebar_options() -> gpui::TitlebarOptions {
    114     gpui::TitlebarOptions {
    115         title: None,
    116         appears_transparent: true,
    117         ..Default::default()
    118     }
    119 }
    120 
    121 pub fn settings_titlebar_options() -> gpui::TitlebarOptions {
    122     gpui::TitlebarOptions {
    123         title: None,
    124         appears_transparent: true,
    125         ..Default::default()
    126     }
    127 }
    128 
    129 #[cfg(test)]
    130 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    131 pub enum PrimaryWindowTarget {
    132     Home,
    133 }
    134 
    135 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    136 pub enum HomeStage {
    137     Setup,
    138     AccountWorkspace,
    139     BuyerWorkspace,
    140     FarmerWorkspace,
    141 }
    142 
    143 #[cfg(test)]
    144 pub fn primary_window_target(_: &DesktopAppRuntimeSummary) -> PrimaryWindowTarget {
    145     PrimaryWindowTarget::Home
    146 }
    147 
    148 pub fn home_stage(summary: &DesktopAppRuntimeSummary) -> HomeStage {
    149     if summary.startup_issue.is_some() || summary.startup_gate == AppStartupGate::Blocked {
    150         HomeStage::Setup
    151     } else if matches!(
    152         summary.shell_projection.selected_section,
    153         ShellSection::Account
    154     ) {
    155         HomeStage::AccountWorkspace
    156     } else if summary.startup_gate == AppStartupGate::Farmer {
    157         HomeStage::FarmerWorkspace
    158     } else if matches!(
    159         summary.shell_projection.selected_section,
    160         ShellSection::Personal(_)
    161     ) || summary.startup_gate == AppStartupGate::Personal
    162     {
    163         HomeStage::BuyerWorkspace
    164     } else {
    165         HomeStage::Setup
    166     }
    167 }
    168 
    169 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    170 enum HomeFocusedView {
    171     FarmSetup,
    172     ProductEditor,
    173     FarmerOrderDetail(OrderId),
    174     BuyerProductDetail(PersonalSection),
    175     BuyerOrderReview,
    176     BuyerOrderDetail(OrderId),
    177 }
    178 
    179 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
    180 enum AccountTab {
    181     #[default]
    182     Profile,
    183     FarmDetails,
    184     Preferences,
    185     Settings,
    186 }
    187 
    188 impl AccountTab {
    189     const ORDERED: [Self; 4] = [
    190         Self::Profile,
    191         Self::FarmDetails,
    192         Self::Preferences,
    193         Self::Settings,
    194     ];
    195 
    196     const fn text_key(self) -> AppTextKey {
    197         match self {
    198             Self::Profile => AppTextKey::AccountTabProfile,
    199             Self::FarmDetails => AppTextKey::AccountTabFarmDetails,
    200             Self::Preferences => AppTextKey::AccountTabPreferences,
    201             Self::Settings => AppTextKey::AccountTabSettings,
    202         }
    203     }
    204 
    205     const fn panel_text_key(self) -> AppTextKey {
    206         match self {
    207             Self::Profile | Self::FarmDetails => self.text_key(),
    208             Self::Preferences => AppTextKey::AccountNotImplemented,
    209             Self::Settings => AppTextKey::AccountSettingsTitle,
    210         }
    211     }
    212 
    213     fn selected_index(self) -> usize {
    214         Self::ORDERED
    215             .iter()
    216             .position(|tab| *tab == self)
    217             .unwrap_or(0)
    218     }
    219 
    220     fn from_index(index: usize) -> Self {
    221         Self::ORDERED.get(index).copied().unwrap_or_default()
    222     }
    223 }
    224 
    225 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
    226 enum AccountFarmDetailsTab {
    227     #[default]
    228     Profile,
    229     Location,
    230     Operations,
    231     Fulfilment,
    232 }
    233 
    234 impl AccountFarmDetailsTab {
    235     const ORDERED: [Self; 4] = [
    236         Self::Profile,
    237         Self::Location,
    238         Self::Operations,
    239         Self::Fulfilment,
    240     ];
    241 
    242     const fn text_key(self) -> AppTextKey {
    243         match self {
    244             Self::Profile => AppTextKey::AccountFarmDetailsTabProfile,
    245             Self::Location => AppTextKey::AccountFarmDetailsTabLocation,
    246             Self::Operations => AppTextKey::AccountFarmDetailsTabOperations,
    247             Self::Fulfilment => AppTextKey::AccountFarmDetailsTabFulfilment,
    248         }
    249     }
    250 
    251     fn selected_index(self) -> usize {
    252         Self::ORDERED
    253             .iter()
    254             .position(|tab| *tab == self)
    255             .unwrap_or(0)
    256     }
    257 
    258     fn from_index(index: usize) -> Self {
    259         Self::ORDERED.get(index).copied().unwrap_or_default()
    260     }
    261 }
    262 
    263 type AccountProfileSelectState = SelectState<SearchableVec<SharedString>>;
    264 type AccountFarmProfileSelectState = SelectState<SearchableVec<SharedString>>;
    265 
    266 #[derive(Clone)]
    267 struct AccountProfileFormState {
    268     full_name_input: Entity<InputState>,
    269     email_input: Entity<InputState>,
    270     phone_input: Entity<InputState>,
    271     role_select: Entity<AccountProfileSelectState>,
    272     time_zone_select: Entity<AccountProfileSelectState>,
    273     language_select: Entity<AccountProfileSelectState>,
    274 }
    275 
    276 impl AccountProfileFormState {
    277     fn new(window: &mut Window, cx: &mut Context<HomeView>) -> Self {
    278         Self {
    279             full_name_input: account_profile_input_state(
    280                 AppTextKey::AccountProfileFullNameValue,
    281                 window,
    282                 cx,
    283             ),
    284             email_input: account_profile_input_state(
    285                 AppTextKey::AccountProfileEmailValue,
    286                 window,
    287                 cx,
    288             ),
    289             phone_input: account_profile_input_state(
    290                 AppTextKey::AccountProfilePhoneValue,
    291                 window,
    292                 cx,
    293             ),
    294             role_select: account_profile_select_state(
    295                 &[
    296                     AppTextKey::AccountProfileRoleValue,
    297                     AppTextKey::AccountProfileRoleFarmManagerValue,
    298                     AppTextKey::AccountProfileRoleTeamMemberValue,
    299                 ],
    300                 window,
    301                 cx,
    302             ),
    303             time_zone_select: account_profile_select_state(
    304                 &[
    305                     AppTextKey::AccountProfileTimeZoneValue,
    306                     AppTextKey::AccountProfileTimeZoneMountainValue,
    307                     AppTextKey::AccountProfileTimeZoneEasternValue,
    308                 ],
    309                 window,
    310                 cx,
    311             ),
    312             language_select: account_profile_select_state(
    313                 &[
    314                     AppTextKey::AccountProfileLanguageValue,
    315                     AppTextKey::AccountProfileLanguageFrenchValue,
    316                     AppTextKey::AccountProfileLanguageSpanishValue,
    317                 ],
    318                 window,
    319                 cx,
    320             ),
    321         }
    322     }
    323 }
    324 
    325 fn account_profile_input_state(
    326     value_key: AppTextKey,
    327     window: &mut Window,
    328     cx: &mut Context<HomeView>,
    329 ) -> Entity<InputState> {
    330     let input = cx.new(|cx| InputState::new(window, cx).default_value(app_text(value_key)));
    331     account_subscribe_input_change(&input, window, cx);
    332     input
    333 }
    334 
    335 fn account_profile_autogrow_input_state(
    336     value_key: AppTextKey,
    337     window: &mut Window,
    338     cx: &mut Context<HomeView>,
    339 ) -> Entity<InputState> {
    340     let input = cx.new(|cx| {
    341         InputState::new(window, cx)
    342             .auto_grow(3, 6)
    343             .default_value(app_text(value_key))
    344     });
    345     account_subscribe_input_change(&input, window, cx);
    346     input
    347 }
    348 
    349 fn account_profile_select_state(
    350     value_keys: &[AppTextKey],
    351     window: &mut Window,
    352     cx: &mut Context<HomeView>,
    353 ) -> Entity<AccountProfileSelectState> {
    354     let values = value_keys
    355         .iter()
    356         .copied()
    357         .map(app_shared_text)
    358         .collect::<Vec<_>>();
    359     let select = cx.new(|cx| {
    360         SelectState::new(
    361             SearchableVec::new(values),
    362             Some(IndexPath::default().row(0)),
    363             window,
    364             cx,
    365         )
    366     });
    367     account_subscribe_profile_select_change(&select, window, cx);
    368     select
    369 }
    370 
    371 #[derive(Clone)]
    372 struct AccountFarmProfileFormState {
    373     farm_name_input: Entity<InputState>,
    374     public_farm_name_input: Entity<InputState>,
    375     short_description_input: Entity<InputState>,
    376     contact_email_input: Entity<InputState>,
    377     public_phone_input: Entity<InputState>,
    378     website_input: Entity<InputState>,
    379     established_year_input: Entity<InputState>,
    380     about_farm_input: Entity<InputState>,
    381     farm_type_select: Entity<AccountFarmProfileSelectState>,
    382     street_address_input: Entity<InputState>,
    383     city_input: Entity<InputState>,
    384     postal_code_input: Entity<InputState>,
    385     province_select: Entity<AccountFarmProfileSelectState>,
    386     country_select: Entity<AccountFarmProfileSelectState>,
    387     service_area_select: Entity<AccountFarmProfileSelectState>,
    388     growing_practices_select: Entity<AccountFarmProfileSelectState>,
    389     season_start_input: Entity<InputState>,
    390     season_end_input: Entity<InputState>,
    391     about_products_input: Entity<InputState>,
    392     customer_note_input: Entity<InputState>,
    393     primary_pickup_location_select: Entity<AccountFarmProfileSelectState>,
    394     pickup_instructions_input: Entity<InputState>,
    395     order_cutoff_select: Entity<AccountFarmProfileSelectState>,
    396     delivery_radius_input: Entity<InputState>,
    397 }
    398 
    399 impl AccountFarmProfileFormState {
    400     fn new(window: &mut Window, cx: &mut Context<HomeView>) -> Self {
    401         Self {
    402             farm_name_input: account_profile_input_state(
    403                 AppTextKey::AccountFarmDetailsFarmNameValue,
    404                 window,
    405                 cx,
    406             ),
    407             public_farm_name_input: account_profile_input_state(
    408                 AppTextKey::AccountFarmDetailsPublicFarmNameValue,
    409                 window,
    410                 cx,
    411             ),
    412             short_description_input: account_profile_input_state(
    413                 AppTextKey::AccountFarmDetailsShortDescriptionValue,
    414                 window,
    415                 cx,
    416             ),
    417             contact_email_input: account_profile_input_state(
    418                 AppTextKey::AccountFarmDetailsContactEmailValue,
    419                 window,
    420                 cx,
    421             ),
    422             public_phone_input: account_profile_input_state(
    423                 AppTextKey::AccountFarmDetailsPublicPhoneValue,
    424                 window,
    425                 cx,
    426             ),
    427             website_input: account_profile_input_state(
    428                 AppTextKey::AccountFarmDetailsWebsiteValue,
    429                 window,
    430                 cx,
    431             ),
    432             established_year_input: account_profile_input_state(
    433                 AppTextKey::AccountFarmDetailsEstablishedYearValue,
    434                 window,
    435                 cx,
    436             ),
    437             about_farm_input: account_profile_autogrow_input_state(
    438                 AppTextKey::AccountFarmDetailsAboutFarmValue,
    439                 window,
    440                 cx,
    441             ),
    442             farm_type_select: account_farm_profile_select_state(
    443                 &[
    444                     AppTextKey::AccountFarmDetailsFarmTypeVegetableFarm,
    445                     AppTextKey::AccountFarmDetailsFarmTypeFruitOrchard,
    446                     AppTextKey::AccountFarmDetailsFarmTypeBerryFarm,
    447                     AppTextKey::AccountFarmDetailsFarmTypeHerbFarm,
    448                     AppTextKey::AccountFarmDetailsFarmTypeFlowerFarm,
    449                     AppTextKey::AccountFarmDetailsFarmTypeMushroomFarm,
    450                     AppTextKey::AccountFarmDetailsFarmTypeGrainFieldCropFarm,
    451                     AppTextKey::AccountFarmDetailsFarmTypeDairyFarm,
    452                     AppTextKey::AccountFarmDetailsFarmTypeEggPoultryFarm,
    453                     AppTextKey::AccountFarmDetailsFarmTypeLivestockFarm,
    454                     AppTextKey::AccountFarmDetailsFarmTypeHoneyApiary,
    455                     AppTextKey::AccountFarmDetailsFarmTypeNurseryPlantFarm,
    456                     AppTextKey::AccountFarmDetailsFarmTypeMixedFarm,
    457                     AppTextKey::AccountFarmDetailsFarmTypeOther,
    458                 ],
    459                 window,
    460                 cx,
    461             ),
    462             street_address_input: account_profile_input_state(
    463                 AppTextKey::AccountFarmDetailsStreetAddressValue,
    464                 window,
    465                 cx,
    466             ),
    467             city_input: account_profile_input_state(
    468                 AppTextKey::AccountFarmDetailsCityValue,
    469                 window,
    470                 cx,
    471             ),
    472             postal_code_input: account_profile_input_state(
    473                 AppTextKey::AccountFarmDetailsPostalCodeValue,
    474                 window,
    475                 cx,
    476             ),
    477             province_select: account_farm_profile_select_state(
    478                 &[
    479                     AppTextKey::AccountFarmDetailsProvinceBritishColumbia,
    480                     AppTextKey::AccountFarmDetailsProvinceAlberta,
    481                 ],
    482                 window,
    483                 cx,
    484             ),
    485             country_select: account_farm_profile_select_state(
    486                 &[
    487                     AppTextKey::AccountFarmDetailsCountryCanada,
    488                     AppTextKey::AccountFarmDetailsCountryUnitedStates,
    489                 ],
    490                 window,
    491                 cx,
    492             ),
    493             service_area_select: account_farm_profile_select_state(
    494                 &[AppTextKey::AccountFarmDetailsServiceAreaValue],
    495                 window,
    496                 cx,
    497             ),
    498             growing_practices_select: account_farm_profile_select_state(
    499                 &[
    500                     AppTextKey::AccountFarmDetailsGrowingPracticeRegenerative,
    501                     AppTextKey::AccountFarmDetailsGrowingPracticeOrganic,
    502                 ],
    503                 window,
    504                 cx,
    505             ),
    506             season_start_input: account_profile_input_state(
    507                 AppTextKey::AccountFarmDetailsSeasonStartValue,
    508                 window,
    509                 cx,
    510             ),
    511             season_end_input: account_profile_input_state(
    512                 AppTextKey::AccountFarmDetailsSeasonEndValue,
    513                 window,
    514                 cx,
    515             ),
    516             about_products_input: account_profile_autogrow_input_state(
    517                 AppTextKey::AccountFarmDetailsAboutProductsValue,
    518                 window,
    519                 cx,
    520             ),
    521             customer_note_input: account_profile_autogrow_input_state(
    522                 AppTextKey::AccountFarmDetailsCustomerNoteValue,
    523                 window,
    524                 cx,
    525             ),
    526             primary_pickup_location_select: account_farm_profile_select_state(
    527                 &[AppTextKey::AccountFarmDetailsPrimaryPickupLocationTitleValue],
    528                 window,
    529                 cx,
    530             ),
    531             pickup_instructions_input: account_profile_autogrow_input_state(
    532                 AppTextKey::AccountFarmDetailsPickupInstructionsValue,
    533                 window,
    534                 cx,
    535             ),
    536             order_cutoff_select: account_farm_profile_select_state(
    537                 &[AppTextKey::AccountFarmDetailsOrderCutoffNoonValue],
    538                 window,
    539                 cx,
    540             ),
    541             delivery_radius_input: account_profile_input_state(
    542                 AppTextKey::AccountFarmDetailsDeliveryRadiusValue,
    543                 window,
    544                 cx,
    545             ),
    546         }
    547     }
    548 
    549     fn is_dirty(&self, cx: &App) -> bool {
    550         account_input_is_dirty(
    551             &self.farm_name_input,
    552             AppTextKey::AccountFarmDetailsFarmNameValue,
    553             cx,
    554         ) || account_input_is_dirty(
    555             &self.public_farm_name_input,
    556             AppTextKey::AccountFarmDetailsPublicFarmNameValue,
    557             cx,
    558         ) || account_input_is_dirty(
    559             &self.short_description_input,
    560             AppTextKey::AccountFarmDetailsShortDescriptionValue,
    561             cx,
    562         ) || account_input_is_dirty(
    563             &self.contact_email_input,
    564             AppTextKey::AccountFarmDetailsContactEmailValue,
    565             cx,
    566         ) || account_input_is_dirty(
    567             &self.public_phone_input,
    568             AppTextKey::AccountFarmDetailsPublicPhoneValue,
    569             cx,
    570         ) || account_input_is_dirty(
    571             &self.website_input,
    572             AppTextKey::AccountFarmDetailsWebsiteValue,
    573             cx,
    574         ) || account_input_is_dirty(
    575             &self.established_year_input,
    576             AppTextKey::AccountFarmDetailsEstablishedYearValue,
    577             cx,
    578         ) || account_input_is_dirty(
    579             &self.about_farm_input,
    580             AppTextKey::AccountFarmDetailsAboutFarmValue,
    581             cx,
    582         ) || account_select_is_dirty(&self.farm_type_select, cx)
    583             || account_input_is_dirty(
    584                 &self.street_address_input,
    585                 AppTextKey::AccountFarmDetailsStreetAddressValue,
    586                 cx,
    587             )
    588             || account_input_is_dirty(
    589                 &self.city_input,
    590                 AppTextKey::AccountFarmDetailsCityValue,
    591                 cx,
    592             )
    593             || account_input_is_dirty(
    594                 &self.postal_code_input,
    595                 AppTextKey::AccountFarmDetailsPostalCodeValue,
    596                 cx,
    597             )
    598             || account_select_is_dirty(&self.province_select, cx)
    599             || account_select_is_dirty(&self.country_select, cx)
    600             || account_select_is_dirty(&self.service_area_select, cx)
    601             || account_select_is_dirty(&self.growing_practices_select, cx)
    602             || account_input_is_dirty(
    603                 &self.season_start_input,
    604                 AppTextKey::AccountFarmDetailsSeasonStartValue,
    605                 cx,
    606             )
    607             || account_input_is_dirty(
    608                 &self.season_end_input,
    609                 AppTextKey::AccountFarmDetailsSeasonEndValue,
    610                 cx,
    611             )
    612             || account_input_is_dirty(
    613                 &self.about_products_input,
    614                 AppTextKey::AccountFarmDetailsAboutProductsValue,
    615                 cx,
    616             )
    617             || account_input_is_dirty(
    618                 &self.customer_note_input,
    619                 AppTextKey::AccountFarmDetailsCustomerNoteValue,
    620                 cx,
    621             )
    622             || account_select_is_dirty(&self.primary_pickup_location_select, cx)
    623             || account_input_is_dirty(
    624                 &self.pickup_instructions_input,
    625                 AppTextKey::AccountFarmDetailsPickupInstructionsValue,
    626                 cx,
    627             )
    628             || account_select_is_dirty(&self.order_cutoff_select, cx)
    629             || account_input_is_dirty(
    630                 &self.delivery_radius_input,
    631                 AppTextKey::AccountFarmDetailsDeliveryRadiusValue,
    632                 cx,
    633             )
    634     }
    635 }
    636 
    637 #[derive(Clone)]
    638 struct AccountSettingsFormState {
    639     add_relay_input: Entity<InputState>,
    640     blossom_server_input: Entity<InputState>,
    641 }
    642 
    643 impl AccountSettingsFormState {
    644     fn new(window: &mut Window, cx: &mut Context<HomeView>) -> Self {
    645         let add_relay_input = cx.new(|cx| {
    646             InputState::new(window, cx)
    647                 .placeholder(app_text(AppTextKey::AccountSettingsAddRelayPlaceholder))
    648         });
    649         let blossom_server_input = cx.new(|cx| {
    650             InputState::new(window, cx).default_value(ACCOUNT_SETTINGS_DEFAULT_BLOSSOM_SERVER)
    651         });
    652         account_subscribe_input_change(&add_relay_input, window, cx);
    653         account_subscribe_input_change(&blossom_server_input, window, cx);
    654 
    655         Self {
    656             add_relay_input,
    657             blossom_server_input,
    658         }
    659     }
    660 
    661     fn is_dirty(&self, cx: &App) -> bool {
    662         !self.add_relay_input.read(cx).value().trim().is_empty()
    663             || self.blossom_server_input.read(cx).value().as_ref()
    664                 != ACCOUNT_SETTINGS_DEFAULT_BLOSSOM_SERVER
    665     }
    666 }
    667 
    668 fn account_farm_profile_select_state(
    669     value_keys: &[AppTextKey],
    670     window: &mut Window,
    671     cx: &mut Context<HomeView>,
    672 ) -> Entity<AccountFarmProfileSelectState> {
    673     let values = value_keys
    674         .iter()
    675         .copied()
    676         .map(app_shared_text)
    677         .collect::<Vec<_>>();
    678     let select = cx.new(|cx| {
    679         SelectState::new(
    680             SearchableVec::new(values),
    681             Some(IndexPath::default().row(0)),
    682             window,
    683             cx,
    684         )
    685     });
    686     account_subscribe_farm_select_change(&select, window, cx);
    687     select
    688 }
    689 
    690 fn account_input_is_dirty(
    691     input: &Entity<InputState>,
    692     initial_value_key: AppTextKey,
    693     cx: &App,
    694 ) -> bool {
    695     input.read(cx).value().as_ref() != app_text(initial_value_key)
    696 }
    697 
    698 fn account_select_is_dirty(select: &Entity<AccountProfileSelectState>, cx: &App) -> bool {
    699     select
    700         .read(cx)
    701         .selected_index(cx)
    702         .is_some_and(|index| index.row != 0)
    703 }
    704 
    705 fn account_subscribe_input_change(
    706     input: &Entity<InputState>,
    707     window: &mut Window,
    708     cx: &mut Context<HomeView>,
    709 ) {
    710     cx.subscribe_in(
    711         input,
    712         window,
    713         |_: &mut HomeView, _: &Entity<InputState>, event: &InputEvent, _, cx| {
    714             if matches!(event, InputEvent::Change) {
    715                 cx.notify();
    716             }
    717         },
    718     )
    719     .detach();
    720 }
    721 
    722 fn account_subscribe_profile_select_change(
    723     select: &Entity<AccountProfileSelectState>,
    724     window: &mut Window,
    725     cx: &mut Context<HomeView>,
    726 ) {
    727     cx.subscribe_in(
    728         select,
    729         window,
    730         |_: &mut HomeView,
    731          _: &Entity<AccountProfileSelectState>,
    732          event: &SelectEvent<SearchableVec<SharedString>>,
    733          _,
    734          cx| {
    735             if matches!(event, SelectEvent::Confirm(_)) {
    736                 cx.notify();
    737             }
    738         },
    739     )
    740     .detach();
    741 }
    742 
    743 fn account_subscribe_farm_select_change(
    744     select: &Entity<AccountFarmProfileSelectState>,
    745     window: &mut Window,
    746     cx: &mut Context<HomeView>,
    747 ) {
    748     cx.subscribe_in(
    749         select,
    750         window,
    751         |_: &mut HomeView,
    752          _: &Entity<AccountFarmProfileSelectState>,
    753          event: &SelectEvent<SearchableVec<SharedString>>,
    754          _,
    755          cx| {
    756             if matches!(event, SelectEvent::Confirm(_)) {
    757                 cx.notify();
    758             }
    759         },
    760     )
    761     .detach();
    762 }
    763 
    764 fn buyer_order_detail_focus_after_open(
    765     runtime_changed: bool,
    766     runtime: &DesktopAppRuntimeSummary,
    767     order_id: OrderId,
    768 ) -> Option<HomeFocusedView> {
    769     if runtime_changed
    770         || runtime
    771             .personal_projection
    772             .orders
    773             .detail
    774             .as_ref()
    775             .is_some_and(|detail| detail.order_id == order_id)
    776     {
    777         Some(HomeFocusedView::BuyerOrderDetail(order_id))
    778     } else {
    779         None
    780     }
    781 }
    782 
    783 fn farmer_order_detail_focus_after_open(
    784     runtime_changed: bool,
    785     runtime: &DesktopAppRuntimeSummary,
    786     order_id: OrderId,
    787 ) -> Option<HomeFocusedView> {
    788     if runtime_changed
    789         || runtime
    790             .orders_projection
    791             .detail
    792             .as_ref()
    793             .is_some_and(|detail| detail.order_id == order_id)
    794     {
    795         Some(HomeFocusedView::FarmerOrderDetail(order_id))
    796     } else {
    797         None
    798     }
    799 }
    800 
    801 pub fn home_window_options(cx: &mut App) -> WindowOptions {
    802     let (launch_width_px, launch_height_px) = home_window_launch_size_px();
    803     let (minimum_width_px, minimum_height_px) = home_window_minimum_size_px();
    804     let bounds = Bounds::centered(None, size(px(launch_width_px), px(launch_height_px)), cx);
    805 
    806     WindowOptions {
    807         window_bounds: Some(WindowBounds::Windowed(bounds)),
    808         window_min_size: Some(size(px(minimum_width_px), px(minimum_height_px))),
    809         titlebar: Some(home_titlebar_options()),
    810         ..Default::default()
    811     }
    812 }
    813 
    814 fn home_window_launch_size_px() -> (f32, f32) {
    815     (
    816         APP_UI_THEME.shells.home_min_width_px,
    817         APP_UI_THEME.shells.home_min_height_px,
    818     )
    819 }
    820 
    821 fn home_window_minimum_size_px() -> (f32, f32) {
    822     (HOME_WINDOW_MIN_WIDTH_PX, HOME_WINDOW_MIN_HEIGHT_PX)
    823 }
    824 
    825 pub fn settings_window_options(cx: &mut App) -> WindowOptions {
    826     let bounds = Bounds::centered(
    827         None,
    828         size(
    829             px(APP_UI_THEME.shells.settings_width_px),
    830             px(APP_UI_THEME.shells.settings_height_px),
    831         ),
    832         cx,
    833     );
    834 
    835     WindowOptions {
    836         window_bounds: Some(WindowBounds::Windowed(bounds)),
    837         window_min_size: Some(size(
    838             px(APP_UI_THEME.shells.settings_width_px),
    839             px(APP_UI_THEME.shells.settings_height_px),
    840         )),
    841         titlebar: Some(settings_titlebar_options()),
    842         ..Default::default()
    843     }
    844 }
    845 
    846 pub fn open_home_window(
    847     window: &mut Window,
    848     cx: &mut App,
    849     runtime: DesktopAppRuntime,
    850 ) -> gpui::Entity<Root> {
    851     let _ = runtime.record_home_opened();
    852     let view = cx.new(|_| HomeView::new(runtime));
    853     cx.new(|cx| Root::new(view, window, cx))
    854 }
    855 
    856 pub fn open_settings_window(
    857     window: &mut Window,
    858     cx: &mut App,
    859     runtime: DesktopAppRuntime,
    860     initial_view: SettingsPanelViewKey,
    861 ) -> gpui::Entity<Root> {
    862     let _ = runtime.sync_settings_section(initial_view);
    863     let _ = runtime.record_settings_opened(initial_view);
    864     let view = cx.new(|_| SettingsWindowView::new(runtime, initial_view));
    865     cx.new(|cx| Root::new(view, window, cx))
    866 }
    867 
    868 pub struct HomeView {
    869     runtime: DesktopAppRuntime,
    870     startup_view: StartupHomeView,
    871     startup_signer_entry: Option<StartupSignerEntryState>,
    872     startup_signer_connect_state: StartupSignerConnectState,
    873     startup_signer_task_token: u64,
    874     startup_signer_recovery_attempted: bool,
    875     farm_setup_form: Option<FarmSetupFormState>,
    876     personal_search: Option<PersonalSearchState>,
    877     buyer_order_review_form: Option<BuyerOrderReviewFormState>,
    878     products_search: Option<ProductsSearchState>,
    879     products_stock_editor: Option<ProductsStockEditorState>,
    880     product_editor_form: Option<ProductEditorFormState>,
    881     focused_view: Option<HomeFocusedView>,
    882     selected_account_tab: AccountTab,
    883     selected_account_farm_details_tab: AccountFarmDetailsTab,
    884     account_profile_form: Option<AccountProfileFormState>,
    885     account_farm_profile_form: Option<AccountFarmProfileFormState>,
    886     account_settings_form: Option<AccountSettingsFormState>,
    887     account_farm_profile_textarea_wrap_ready: bool,
    888     account_farm_profile_textarea_wrap_requested: bool,
    889     relay_client: Option<RadrootsNostrClient>,
    890     buyer_workspace_notice: Option<String>,
    891 }
    892 
    893 #[derive(Clone, Debug)]
    894 enum StartupSignerConnectState {
    895     Idle,
    896     Connecting,
    897     PendingApproval {
    898         pending_session: RadrootsAppRemoteSignerPendingSession,
    899         auth_challenge_url: Option<String>,
    900     },
    901     Approved {
    902         pending_session: RadrootsAppRemoteSignerPendingSession,
    903         approved_session: RadrootsAppRemoteSignerApprovedSession,
    904         auth_challenge_url: Option<String>,
    905     },
    906 }
    907 
    908 #[derive(Clone, Debug, Eq, PartialEq)]
    909 struct StartupSignerPreviewSummary {
    910     source_label: String,
    911     signer_npub: String,
    912     relays_label: String,
    913     permissions_label: String,
    914 }
    915 
    916 #[derive(Clone, Debug)]
    917 struct StartupSignerPollCycleResult {
    918     auth_challenge_url: Option<String>,
    919     outcome: Result<RadrootsAppRemoteSignerPendingPollOutcome, String>,
    920 }
    921 
    922 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
    923 struct HomeAutoFocusState {
    924     has_startup_signer_input: bool,
    925     startup_signer_input_is_editable: bool,
    926     has_farm_setup_form: bool,
    927     has_personal_search_input: bool,
    928     has_buyer_order_review_form: bool,
    929     has_products_search_input: bool,
    930     has_products_stock_editor: bool,
    931     has_product_editor_form: bool,
    932 }
    933 
    934 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    935 enum HomeAutoFocusTarget {
    936     StartupContinue,
    937     StartupGenerateKey,
    938     StartupSignerInput,
    939     StartupSignerBack,
    940     BuyerSearchInput,
    941     BuyerListingOpenFirst,
    942     BuyerDetailBack,
    943     BuyerCartOpenOrderReview,
    944     BuyerOrderReviewNameInput,
    945     BuyerOrderOpenFirst,
    946     BuyerOrderConfirmReplace,
    947     BuyerOrderRepeatDemand,
    948     FarmerReminderPrimary,
    949     FarmerReminderDismiss,
    950     FarmerSetupStart,
    951     FarmerSetupContinue,
    952     FarmerSetupFarmNameInput,
    953     FarmerTodayReminderChipFirst,
    954     FarmerTodayOpenPackDay,
    955     FarmerTodayOpenOrders,
    956     FarmerTodayOpenProductsLowStock,
    957     FarmerTodayOpenProductsDrafts,
    958     ProductsSearchInput,
    959     ProductsRowOpenFirst,
    960     ProductsStockInput,
    961     ProductEditorTitleInput,
    962     OrdersRowOpenFirst,
    963 }
    964 
    965 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    966 enum BuyerWorkspaceNotice {
    967     MarketplaceRefreshFailed,
    968     DetailOpenFailed,
    969     OrderPlaceFailed,
    970     OrderCoordinationFailed,
    971 }
    972 
    973 impl BuyerWorkspaceNotice {
    974     fn text_key(self) -> AppTextKey {
    975         match self {
    976             Self::MarketplaceRefreshFailed => AppTextKey::PersonalMarketplaceRefreshFailedNotice,
    977             Self::DetailOpenFailed => AppTextKey::PersonalDetailOpenFailedNotice,
    978             Self::OrderPlaceFailed => AppTextKey::PersonalOrderPlaceFailedNotice,
    979             Self::OrderCoordinationFailed => AppTextKey::PersonalOrderCoordinationFailedNotice,
    980         }
    981     }
    982 
    983     fn text(self) -> String {
    984         app_text(self.text_key())
    985     }
    986 }
    987 
    988 impl HomeView {
    989     pub fn new(runtime: DesktopAppRuntime) -> Self {
    990         Self {
    991             runtime,
    992             startup_view: StartupHomeView::new(),
    993             startup_signer_entry: None,
    994             startup_signer_connect_state: StartupSignerConnectState::Idle,
    995             startup_signer_task_token: 0,
    996             startup_signer_recovery_attempted: false,
    997             farm_setup_form: None,
    998             personal_search: None,
    999             buyer_order_review_form: None,
   1000             products_search: None,
   1001             products_stock_editor: None,
   1002             product_editor_form: None,
   1003             focused_view: None,
   1004             selected_account_tab: AccountTab::default(),
   1005             selected_account_farm_details_tab: AccountFarmDetailsTab::default(),
   1006             account_profile_form: None,
   1007             account_farm_profile_form: None,
   1008             account_settings_form: None,
   1009             account_farm_profile_textarea_wrap_ready: false,
   1010             account_farm_profile_textarea_wrap_requested: false,
   1011             relay_client: None,
   1012             buyer_workspace_notice: None,
   1013         }
   1014     }
   1015 
   1016     fn auto_focus_state(&self) -> HomeAutoFocusState {
   1017         HomeAutoFocusState {
   1018             has_startup_signer_input: self.startup_signer_entry.is_some(),
   1019             startup_signer_input_is_editable: startup_signer_source_input_is_editable(
   1020                 &self.startup_signer_connect_state,
   1021             ),
   1022             has_farm_setup_form: self.farm_setup_form.is_some(),
   1023             has_personal_search_input: self.personal_search.is_some(),
   1024             has_buyer_order_review_form: self.buyer_order_review_form.is_some(),
   1025             has_products_search_input: self.products_search.is_some(),
   1026             has_products_stock_editor: self.products_stock_editor.is_some(),
   1027             has_product_editor_form: self.product_editor_form.is_some(),
   1028         }
   1029     }
   1030 
   1031     fn clear_focused_view(&mut self) -> bool {
   1032         self.focused_view.take().is_some()
   1033     }
   1034 
   1035     fn clear_focused_view_matching(&mut self, view: HomeFocusedView) -> bool {
   1036         if self.focused_view == Some(view) {
   1037             self.focused_view = None;
   1038             true
   1039         } else {
   1040             false
   1041         }
   1042     }
   1043 
   1044     fn apply_auto_focus(
   1045         &mut self,
   1046         runtime: &DesktopAppRuntimeSummary,
   1047         window: &mut Window,
   1048         cx: &mut Context<Self>,
   1049     ) {
   1050         let desired_target = home_auto_focus_target(runtime, self.auto_focus_state());
   1051         let focus_state = window.use_state(cx, |_, _| Option::<HomeAutoFocusTarget>::None);
   1052         let should_focus = {
   1053             let last_target = focus_state.read(cx);
   1054             last_target.as_ref().copied() != desired_target
   1055         };
   1056 
   1057         if !should_focus {
   1058             return;
   1059         }
   1060 
   1061         if let Some(target) = desired_target {
   1062             match target {
   1063                 HomeAutoFocusTarget::StartupContinue => {
   1064                     focus_button(window, "home-continue", cx);
   1065                 }
   1066                 HomeAutoFocusTarget::StartupGenerateKey => {
   1067                     focus_button(window, "home-generate-key", cx);
   1068                 }
   1069                 HomeAutoFocusTarget::StartupSignerInput => {
   1070                     if let Some(entry) = self.startup_signer_entry.as_ref() {
   1071                         entry.input.update(cx, |input, cx| input.focus(window, cx));
   1072                     }
   1073                 }
   1074                 HomeAutoFocusTarget::StartupSignerBack => {
   1075                     focus_button(window, "home-signer-back", cx);
   1076                 }
   1077                 HomeAutoFocusTarget::BuyerSearchInput => {
   1078                     if let Some(search) = self.personal_search.as_ref() {
   1079                         search.input.update(cx, |input, cx| input.focus(window, cx));
   1080                     }
   1081                 }
   1082                 HomeAutoFocusTarget::BuyerListingOpenFirst => {
   1083                     focus_button(window, ("buyer-listing-open", 0_usize), cx);
   1084                 }
   1085                 HomeAutoFocusTarget::BuyerDetailBack => {
   1086                     focus_button(window, "buyer-detail-back", cx);
   1087                 }
   1088                 HomeAutoFocusTarget::BuyerCartOpenOrderReview => {
   1089                     focus_button(window, "buyer-cart-open-order-review", cx);
   1090                 }
   1091                 HomeAutoFocusTarget::BuyerOrderReviewNameInput => {
   1092                     if let Some(form) = self.buyer_order_review_form.as_ref() {
   1093                         form.name_input
   1094                             .update(cx, |input, cx| input.focus(window, cx));
   1095                     }
   1096                 }
   1097                 HomeAutoFocusTarget::BuyerOrderOpenFirst => {
   1098                     focus_button(window, ("buyer-order-open", 0_usize), cx);
   1099                 }
   1100                 HomeAutoFocusTarget::BuyerOrderConfirmReplace => {
   1101                     focus_button(window, "buyer-order-confirm-replace", cx);
   1102                 }
   1103                 HomeAutoFocusTarget::BuyerOrderRepeatDemand => {
   1104                     focus_button(window, "buyer-order-repeat-demand", cx);
   1105                 }
   1106                 HomeAutoFocusTarget::FarmerReminderPrimary => {
   1107                     focus_button(window, "reminder-banner-action", cx);
   1108                 }
   1109                 HomeAutoFocusTarget::FarmerReminderDismiss => {
   1110                     focus_button(window, "reminder-banner-dismiss", cx);
   1111                 }
   1112                 HomeAutoFocusTarget::FarmerSetupStart => {
   1113                     focus_button(window, "home-farm-setup-start", cx);
   1114                 }
   1115                 HomeAutoFocusTarget::FarmerSetupContinue => {
   1116                     focus_button(window, "home-farm-setup-continue", cx);
   1117                 }
   1118                 HomeAutoFocusTarget::FarmerSetupFarmNameInput => {
   1119                     if let Some(form) = self.farm_setup_form.as_ref() {
   1120                         form.farm_name_input
   1121                             .update(cx, |input, cx| input.focus(window, cx));
   1122                     }
   1123                 }
   1124                 HomeAutoFocusTarget::FarmerTodayReminderChipFirst => {
   1125                     focus_button(window, ("today-reminder-chip", 0_usize), cx);
   1126                 }
   1127                 HomeAutoFocusTarget::FarmerTodayOpenPackDay => {
   1128                     focus_button(window, "home-today-open-pack-day", cx);
   1129                 }
   1130                 HomeAutoFocusTarget::FarmerTodayOpenOrders => {
   1131                     focus_button(window, "home-today-open-orders", cx);
   1132                 }
   1133                 HomeAutoFocusTarget::FarmerTodayOpenProductsLowStock => {
   1134                     focus_button(window, "home-today-open-products-low-stock", cx);
   1135                 }
   1136                 HomeAutoFocusTarget::FarmerTodayOpenProductsDrafts => {
   1137                     focus_button(window, "home-today-open-products-drafts", cx);
   1138                 }
   1139                 HomeAutoFocusTarget::ProductsSearchInput => {
   1140                     if let Some(search) = self.products_search.as_ref() {
   1141                         search.input.update(cx, |input, cx| input.focus(window, cx));
   1142                     }
   1143                 }
   1144                 HomeAutoFocusTarget::ProductsRowOpenFirst => {
   1145                     focus_button(window, ("products-row-open", 0_usize), cx);
   1146                 }
   1147                 HomeAutoFocusTarget::ProductsStockInput => {
   1148                     if let Some(editor) = self.products_stock_editor.as_ref() {
   1149                         editor.input.update(cx, |input, cx| input.focus(window, cx));
   1150                     }
   1151                 }
   1152                 HomeAutoFocusTarget::ProductEditorTitleInput => {
   1153                     if let Some(form) = self.product_editor_form.as_ref() {
   1154                         form.title_input
   1155                             .update(cx, |input, cx| input.focus(window, cx));
   1156                     }
   1157                 }
   1158                 HomeAutoFocusTarget::OrdersRowOpenFirst => {
   1159                     focus_button(window, ("orders-row-open", 0_usize), cx);
   1160                 }
   1161             }
   1162         }
   1163 
   1164         focus_state.update(cx, |last_target, _| *last_target = desired_target);
   1165     }
   1166 
   1167     fn generate_local_account(&mut self, cx: &mut Context<Self>) -> bool {
   1168         if self.runtime.generate_local_account(None).unwrap_or(false) {
   1169             cx.refresh_windows();
   1170             cx.notify();
   1171             return true;
   1172         }
   1173 
   1174         false
   1175     }
   1176 
   1177     fn reset_startup_signer_flow(&mut self) {
   1178         self.startup_signer_task_token = self.startup_signer_task_token.wrapping_add(1);
   1179         self.startup_signer_connect_state = StartupSignerConnectState::Idle;
   1180     }
   1181 
   1182     fn next_startup_signer_task_token(&mut self) -> u64 {
   1183         self.startup_signer_task_token = self.startup_signer_task_token.wrapping_add(1);
   1184         self.startup_signer_task_token
   1185     }
   1186 
   1187     fn startup_signer_task_is_current(&self, task_token: u64) -> bool {
   1188         self.startup_signer_task_token == task_token
   1189     }
   1190 
   1191     fn show_startup_identity_choice(&mut self, cx: &mut Context<Self>) {
   1192         self.startup_view.clear_notice();
   1193         self.reset_startup_signer_flow();
   1194         self.startup_signer_recovery_attempted = false;
   1195         if self.runtime.show_startup_identity_choice() {
   1196             cx.notify();
   1197         }
   1198     }
   1199 
   1200     fn cancel_startup_signer_flow(&mut self, cx: &mut Context<Self>) -> bool {
   1201         self.reset_startup_signer_flow();
   1202         if !self.clear_startup_pending_remote_signer_session(cx) {
   1203             return false;
   1204         }
   1205 
   1206         self.startup_signer_recovery_attempted = false;
   1207         true
   1208     }
   1209 
   1210     fn back_out_of_startup_signer_entry(&mut self, cx: &mut Context<Self>) {
   1211         if !self.cancel_startup_signer_flow(cx) {
   1212             return;
   1213         }
   1214 
   1215         self.startup_view.clear_notice();
   1216         if self.runtime.show_startup_identity_choice() {
   1217             cx.notify();
   1218         }
   1219     }
   1220 
   1221     fn show_startup_signer_entry(&mut self, cx: &mut Context<Self>) {
   1222         self.startup_view.clear_notice();
   1223         self.reset_startup_signer_flow();
   1224         self.startup_signer_recovery_attempted = false;
   1225         if self.runtime.show_startup_signer_entry() {
   1226             cx.notify();
   1227         }
   1228     }
   1229 
   1230     fn start_generate_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
   1231         if !self.cancel_startup_signer_flow(cx) {
   1232             return;
   1233         }
   1234         if !self.runtime.begin_generate_key_startup() {
   1235             return;
   1236         }
   1237 
   1238         self.startup_view.clear_notice();
   1239         let relay_urls = self.runtime.nostr_relay_urls();
   1240         cx.notify();
   1241         cx.spawn_in(window, async move |this, cx| {
   1242             let startup_task = cx
   1243                 .background_executor()
   1244                 .spawn(run_startup_app_init(relay_urls));
   1245             Timer::after(Duration::from_secs(1)).await;
   1246             let startup_result = startup_task.await;
   1247             let _ = this.update(cx, |this, cx| {
   1248                 this.finish_generate_key(startup_result, cx);
   1249             });
   1250         })
   1251         .detach();
   1252     }
   1253 
   1254     fn finish_generate_key(
   1255         &mut self,
   1256         startup_result: Result<StartupAppInitResult, String>,
   1257         cx: &mut Context<Self>,
   1258     ) {
   1259         match startup_result {
   1260             Ok(result) => {
   1261                 self.relay_client = Some(result.relay_client);
   1262                 self.startup_view.clear_notice();
   1263                 if !self.generate_local_account(cx) {
   1264                     self.show_startup_identity_choice(cx);
   1265                 }
   1266             }
   1267             Err(error) => {
   1268                 self.runtime.show_startup_identity_choice();
   1269                 self.startup_view.set_notice(error);
   1270                 cx.notify();
   1271             }
   1272         }
   1273     }
   1274 
   1275     fn sync_startup_signer_entry(
   1276         &mut self,
   1277         runtime_summary: &DesktopAppRuntimeSummary,
   1278         window: &mut Window,
   1279         cx: &mut Context<Self>,
   1280     ) {
   1281         if runtime_summary.startup_gate != AppStartupGate::SetupRequired
   1282             || runtime_summary.logged_out_startup.phase != LoggedOutStartupPhase::SignerEntry
   1283         {
   1284             if self.startup_signer_entry.is_some()
   1285                 || !matches!(
   1286                     self.startup_signer_connect_state,
   1287                     StartupSignerConnectState::Idle
   1288                 )
   1289             {
   1290                 self.reset_startup_signer_flow();
   1291             }
   1292             self.startup_signer_recovery_attempted = false;
   1293             self.startup_signer_entry = None;
   1294             return;
   1295         }
   1296 
   1297         let source_input = runtime_summary
   1298             .logged_out_startup
   1299             .signer_entry
   1300             .source_input
   1301             .as_str();
   1302 
   1303         match self.startup_signer_entry.as_mut() {
   1304             Some(entry) => entry.sync(source_input, window, cx),
   1305             None => {
   1306                 self.startup_signer_entry =
   1307                     Some(StartupSignerEntryState::new(source_input, window, cx));
   1308             }
   1309         }
   1310 
   1311         if !self.startup_signer_recovery_attempted {
   1312             self.startup_signer_recovery_attempted = true;
   1313             self.restore_startup_pending_remote_signer_session(window, cx);
   1314         }
   1315     }
   1316 
   1317     fn submit_startup_signer(&mut self, window: &mut Window, cx: &mut Context<Self>) {
   1318         let Some(entry) = self.startup_signer_entry.as_ref() else {
   1319             return;
   1320         };
   1321 
   1322         let source_input = entry.input.read(cx).value().to_string();
   1323         match startup_signer_preview_summary(source_input.as_str()) {
   1324             Ok(_) => {}
   1325             Err(error) => {
   1326                 self.startup_view.set_notice(error);
   1327                 cx.notify();
   1328                 return;
   1329             }
   1330         }
   1331 
   1332         self.startup_view.clear_notice();
   1333         let task_token = self.next_startup_signer_task_token();
   1334         self.startup_signer_connect_state = StartupSignerConnectState::Connecting;
   1335         cx.notify();
   1336 
   1337         cx.spawn_in(window, async move |this, cx| {
   1338             let connect_result = cx
   1339                 .background_executor()
   1340                 .spawn(run_startup_signer_connect(source_input))
   1341                 .await;
   1342             let Some(pending_session) = this
   1343                 .update(cx, |this, cx| {
   1344                     this.finish_startup_signer_connect(task_token, connect_result, cx)
   1345                 })
   1346                 .ok()
   1347                 .flatten()
   1348             else {
   1349                 return;
   1350             };
   1351             let _ = this.update_in(cx, |this, window, cx| {
   1352                 this.spawn_startup_signer_pending_poll(window, task_token, pending_session, cx);
   1353             });
   1354         })
   1355         .detach();
   1356     }
   1357 
   1358     fn finish_startup_signer_connect(
   1359         &mut self,
   1360         task_token: u64,
   1361         connect_result: Result<RadrootsAppRemoteSignerPendingSession, String>,
   1362         cx: &mut Context<Self>,
   1363     ) -> Option<RadrootsAppRemoteSignerPendingSession> {
   1364         if !self.startup_signer_task_is_current(task_token) {
   1365             return None;
   1366         }
   1367 
   1368         match connect_result {
   1369             Ok(pending_session) => {
   1370                 if let Err(error) = self
   1371                     .runtime
   1372                     .store_startup_pending_remote_signer_session(&pending_session)
   1373                 {
   1374                     self.startup_signer_connect_state = StartupSignerConnectState::Idle;
   1375                     self.startup_view.set_notice(error.to_string());
   1376                     cx.notify();
   1377                     return None;
   1378                 }
   1379                 self.startup_view.clear_notice();
   1380                 self.startup_signer_connect_state = StartupSignerConnectState::PendingApproval {
   1381                     pending_session: pending_session.clone(),
   1382                     auth_challenge_url: None,
   1383                 };
   1384                 cx.notify();
   1385                 Some(pending_session)
   1386             }
   1387             Err(error) => {
   1388                 self.startup_signer_connect_state = StartupSignerConnectState::Idle;
   1389                 self.startup_view.set_notice(error);
   1390                 cx.notify();
   1391                 None
   1392             }
   1393         }
   1394     }
   1395 
   1396     fn apply_startup_signer_poll_result(
   1397         &mut self,
   1398         task_token: u64,
   1399         pending_session: RadrootsAppRemoteSignerPendingSession,
   1400         poll_result: StartupSignerPollCycleResult,
   1401         cx: &mut Context<Self>,
   1402     ) -> bool {
   1403         if !self.startup_signer_task_is_current(task_token) {
   1404             return false;
   1405         }
   1406 
   1407         let auth_challenge_url = poll_result.auth_challenge_url;
   1408         match poll_result.outcome {
   1409             Ok(RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval) => {
   1410                 self.startup_view.clear_notice();
   1411                 self.startup_signer_connect_state = StartupSignerConnectState::PendingApproval {
   1412                     pending_session,
   1413                     auth_challenge_url,
   1414                 };
   1415                 cx.notify();
   1416                 true
   1417             }
   1418             Ok(RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { message }) => {
   1419                 if startup_signer_transport_failure_requires_notice(message.as_str()) {
   1420                     self.startup_view.set_notice(message);
   1421                 } else {
   1422                     self.startup_view.clear_notice();
   1423                 }
   1424                 self.startup_signer_connect_state = StartupSignerConnectState::PendingApproval {
   1425                     pending_session,
   1426                     auth_challenge_url,
   1427                 };
   1428                 cx.notify();
   1429                 true
   1430             }
   1431             Ok(RadrootsAppRemoteSignerPendingPollOutcome::Approved(approved_session)) => match self
   1432                 .runtime
   1433                 .activate_startup_approved_remote_signer_session(
   1434                     &pending_session,
   1435                     &approved_session,
   1436                 ) {
   1437                 Ok(_) => {
   1438                     self.startup_view.clear_notice();
   1439                     self.startup_signer_connect_state = StartupSignerConnectState::Approved {
   1440                         pending_session,
   1441                         approved_session,
   1442                         auth_challenge_url,
   1443                     };
   1444                     cx.notify();
   1445                     false
   1446                 }
   1447                 Err(error) => {
   1448                     self.startup_view.set_notice(error.to_string());
   1449                     self.startup_signer_connect_state =
   1450                         StartupSignerConnectState::PendingApproval {
   1451                             pending_session,
   1452                             auth_challenge_url,
   1453                         };
   1454                     cx.notify();
   1455                     false
   1456                 }
   1457             },
   1458             Ok(RadrootsAppRemoteSignerPendingPollOutcome::Rejected { message })
   1459             | Ok(RadrootsAppRemoteSignerPendingPollOutcome::FatalError { message })
   1460             | Err(message) => {
   1461                 let _ = self.runtime.clear_startup_pending_remote_signer_session();
   1462                 self.startup_signer_connect_state = StartupSignerConnectState::Idle;
   1463                 self.startup_view.set_notice(message);
   1464                 cx.notify();
   1465                 false
   1466             }
   1467         }
   1468     }
   1469 
   1470     fn spawn_startup_signer_pending_poll(
   1471         &mut self,
   1472         window: &mut Window,
   1473         task_token: u64,
   1474         pending_session: RadrootsAppRemoteSignerPendingSession,
   1475         cx: &mut Context<Self>,
   1476     ) {
   1477         cx.spawn_in(window, async move |this, cx| {
   1478             loop {
   1479                 let poll_result = cx
   1480                     .background_executor()
   1481                     .spawn(run_startup_signer_pending_poll(
   1482                         pending_session.record.clone(),
   1483                         pending_session.client_secret_key_hex.clone(),
   1484                     ))
   1485                     .await;
   1486                 let should_continue = this
   1487                     .update(cx, |this, cx| {
   1488                         this.apply_startup_signer_poll_result(
   1489                             task_token,
   1490                             pending_session.clone(),
   1491                             poll_result,
   1492                             cx,
   1493                         )
   1494                     })
   1495                     .ok()
   1496                     .unwrap_or(false);
   1497                 if !should_continue {
   1498                     return;
   1499                 }
   1500 
   1501                 Timer::after(Duration::from_secs(1)).await;
   1502             }
   1503         })
   1504         .detach();
   1505     }
   1506 
   1507     fn restore_startup_pending_remote_signer_session(
   1508         &mut self,
   1509         window: &mut Window,
   1510         cx: &mut Context<Self>,
   1511     ) {
   1512         let pending_session = match self.runtime.load_startup_pending_remote_signer_session() {
   1513             Ok(Some(pending_session)) => pending_session,
   1514             Ok(None) => return,
   1515             Err(error) => {
   1516                 self.startup_view.set_notice(error.to_string());
   1517                 cx.notify();
   1518                 return;
   1519             }
   1520         };
   1521 
   1522         let task_token = self.next_startup_signer_task_token();
   1523         self.startup_view.clear_notice();
   1524         self.startup_signer_connect_state = StartupSignerConnectState::PendingApproval {
   1525             pending_session: pending_session.clone(),
   1526             auth_challenge_url: None,
   1527         };
   1528         cx.notify();
   1529         self.spawn_startup_signer_pending_poll(window, task_token, pending_session, cx);
   1530     }
   1531 
   1532     fn clear_startup_pending_remote_signer_session(&mut self, cx: &mut Context<Self>) -> bool {
   1533         match self.runtime.clear_startup_pending_remote_signer_session() {
   1534             Ok(_) => true,
   1535             Err(error) => {
   1536                 self.startup_view.set_notice(error.to_string());
   1537                 cx.notify();
   1538                 false
   1539             }
   1540         }
   1541     }
   1542 
   1543     fn open_farm_setup(&mut self, window: &mut Window, cx: &mut Context<Self>) {
   1544         let runtime_summary = self.runtime.summary();
   1545         let Some(account_id) = runtime_summary
   1546             .settings_account_projection
   1547             .selected_account
   1548             .as_ref()
   1549             .map(|account| account.account.account_id.clone())
   1550         else {
   1551             return;
   1552         };
   1553 
   1554         if runtime_summary.farm_setup_projection.has_saved_farm() {
   1555             self.farm_setup_form = Some(FarmSetupFormState::new(
   1556                 account_id,
   1557                 runtime_summary.farm_setup_projection.draft,
   1558                 window,
   1559                 cx,
   1560             ));
   1561             self.focused_view = Some(HomeFocusedView::FarmSetup);
   1562             cx.notify();
   1563             return;
   1564         }
   1565 
   1566         let stage_changed = self
   1567             .runtime
   1568             .select_farm_setup_flow_stage(FarmSetupFlowStage::Editing);
   1569 
   1570         self.farm_setup_form = Some(FarmSetupFormState::new(
   1571             account_id,
   1572             runtime_summary.farm_setup_projection.draft,
   1573             window,
   1574             cx,
   1575         ));
   1576         self.focused_view = Some(HomeFocusedView::FarmSetup);
   1577         if stage_changed || self.farm_setup_form.is_some() {
   1578             cx.notify();
   1579         }
   1580     }
   1581 
   1582     fn sync_farm_setup_form(
   1583         &mut self,
   1584         runtime_summary: &DesktopAppRuntimeSummary,
   1585         window: &mut Window,
   1586         cx: &mut Context<Self>,
   1587     ) {
   1588         let Some(account_id) = runtime_summary
   1589             .settings_account_projection
   1590             .selected_account
   1591             .as_ref()
   1592             .map(|account| account.account.account_id.clone())
   1593         else {
   1594             self.farm_setup_form = None;
   1595             self.clear_focused_view_matching(HomeFocusedView::FarmSetup);
   1596             return;
   1597         };
   1598 
   1599         if runtime_summary.home_route != HomeRoute::FarmSetupForm && self.farm_setup_form.is_none()
   1600         {
   1601             self.farm_setup_form = None;
   1602             return;
   1603         }
   1604 
   1605         let draft = runtime_summary.farm_setup_projection.draft.clone();
   1606         let should_reset = self
   1607             .farm_setup_form
   1608             .as_ref()
   1609             .map(|form| form.account_id != account_id)
   1610             .unwrap_or(true);
   1611 
   1612         if should_reset {
   1613             self.farm_setup_form = Some(FarmSetupFormState::new(account_id, draft, window, cx));
   1614         }
   1615 
   1616         if runtime_summary.home_route == HomeRoute::FarmSetupForm {
   1617             self.focused_view = Some(HomeFocusedView::FarmSetup);
   1618         }
   1619     }
   1620 
   1621     fn sync_products_search(
   1622         &mut self,
   1623         runtime_summary: &DesktopAppRuntimeSummary,
   1624         window: &mut Window,
   1625         cx: &mut Context<Self>,
   1626     ) {
   1627         let Some(account_id) = runtime_summary
   1628             .settings_account_projection
   1629             .selected_account
   1630             .as_ref()
   1631             .map(|account| account.account.account_id.clone())
   1632         else {
   1633             self.products_search = None;
   1634             return;
   1635         };
   1636 
   1637         if !runtime_summary.farm_setup_projection.has_saved_farm() {
   1638             self.products_search = None;
   1639             return;
   1640         }
   1641 
   1642         let search_query = runtime_summary
   1643             .products_projection
   1644             .query
   1645             .search_query
   1646             .as_str();
   1647         let should_reset = self
   1648             .products_search
   1649             .as_ref()
   1650             .map(|state| state.account_id != account_id)
   1651             .unwrap_or(true);
   1652 
   1653         if should_reset {
   1654             self.products_search = Some(ProductsSearchState::new(
   1655                 account_id,
   1656                 search_query,
   1657                 window,
   1658                 cx,
   1659             ));
   1660             return;
   1661         }
   1662 
   1663         if let Some(products_search) = self.products_search.as_mut() {
   1664             products_search.sync(search_query, window, cx);
   1665         }
   1666     }
   1667 
   1668     fn sync_personal_search(
   1669         &mut self,
   1670         runtime_summary: &DesktopAppRuntimeSummary,
   1671         window: &mut Window,
   1672         cx: &mut Context<Self>,
   1673     ) {
   1674         if home_stage(runtime_summary) != HomeStage::BuyerWorkspace
   1675             || selected_personal_section(runtime_summary) != PersonalSection::Search
   1676         {
   1677             self.personal_search = None;
   1678             return;
   1679         }
   1680 
   1681         let workspace_id = runtime_summary
   1682             .settings_account_projection
   1683             .selected_account
   1684             .as_ref()
   1685             .map(|account| account.account.account_id.clone())
   1686             .unwrap_or_else(|| "guest".to_owned());
   1687         let search_query = runtime_summary
   1688             .personal_projection
   1689             .search
   1690             .query
   1691             .search_query
   1692             .as_str();
   1693         let should_reset = self
   1694             .personal_search
   1695             .as_ref()
   1696             .map(|state| state.workspace_id != workspace_id)
   1697             .unwrap_or(true);
   1698 
   1699         if should_reset {
   1700             self.personal_search = Some(PersonalSearchState::new(
   1701                 workspace_id,
   1702                 search_query,
   1703                 window,
   1704                 cx,
   1705             ));
   1706             return;
   1707         }
   1708 
   1709         if let Some(personal_search) = self.personal_search.as_mut() {
   1710             personal_search.sync(search_query, window, cx);
   1711         }
   1712     }
   1713 
   1714     fn sync_buyer_order_review_form(
   1715         &mut self,
   1716         runtime_summary: &DesktopAppRuntimeSummary,
   1717         window: &mut Window,
   1718         cx: &mut Context<Self>,
   1719     ) {
   1720         if home_stage(runtime_summary) != HomeStage::BuyerWorkspace
   1721             || selected_personal_section(runtime_summary) != PersonalSection::Cart
   1722             || runtime_summary
   1723                 .personal_projection
   1724                 .cart
   1725                 .cart
   1726                 .lines
   1727                 .is_empty()
   1728         {
   1729             self.buyer_order_review_form = None;
   1730             return;
   1731         }
   1732 
   1733         let workspace_id = personal_workspace_id(runtime_summary);
   1734         let draft = &runtime_summary.personal_projection.cart.order_review.draft;
   1735         let should_reset = self
   1736             .buyer_order_review_form
   1737             .as_ref()
   1738             .map(|form| form.workspace_id != workspace_id)
   1739             .unwrap_or(false);
   1740 
   1741         if should_reset {
   1742             self.buyer_order_review_form = Some(BuyerOrderReviewFormState::new(
   1743                 workspace_id,
   1744                 draft,
   1745                 window,
   1746                 cx,
   1747             ));
   1748             return;
   1749         }
   1750 
   1751         if let Some(form) = self.buyer_order_review_form.as_mut() {
   1752             form.sync(draft, window, cx);
   1753         }
   1754     }
   1755 
   1756     fn sync_products_stock_editor(&mut self, runtime_summary: &DesktopAppRuntimeSummary) {
   1757         let Some(editor) = self.products_stock_editor.as_ref() else {
   1758             return;
   1759         };
   1760         let Some(account_id) = runtime_summary
   1761             .settings_account_projection
   1762             .selected_account
   1763             .as_ref()
   1764             .map(|account| account.account.account_id.as_str())
   1765         else {
   1766             self.products_stock_editor = None;
   1767             return;
   1768         };
   1769 
   1770         let should_clear = editor.account_id != account_id
   1771             || selected_farmer_section(runtime_summary) != FarmerSection::Products
   1772             || !runtime_summary.farm_setup_projection.has_saved_farm()
   1773             || !runtime_summary
   1774                 .products_projection
   1775                 .list
   1776                 .rows
   1777                 .iter()
   1778                 .any(|row| row.product_id == editor.product_id);
   1779 
   1780         if should_clear {
   1781             self.products_stock_editor = None;
   1782         }
   1783     }
   1784 
   1785     fn sync_product_editor_form(
   1786         &mut self,
   1787         runtime_summary: &DesktopAppRuntimeSummary,
   1788         window: &mut Window,
   1789         cx: &mut Context<Self>,
   1790     ) {
   1791         let Some(account_id) = runtime_summary
   1792             .settings_account_projection
   1793             .selected_account
   1794             .as_ref()
   1795             .map(|account| account.account.account_id.clone())
   1796         else {
   1797             self.product_editor_form = None;
   1798             self.clear_focused_view_matching(HomeFocusedView::ProductEditor);
   1799             return;
   1800         };
   1801 
   1802         if selected_farmer_section(runtime_summary) != FarmerSection::Products
   1803             || !runtime_summary.farm_setup_projection.has_saved_farm()
   1804         {
   1805             self.product_editor_form = None;
   1806             self.clear_focused_view_matching(HomeFocusedView::ProductEditor);
   1807             return;
   1808         }
   1809 
   1810         let radroots_app_state::ProductEditorState::Open(session) =
   1811             &runtime_summary.products_projection.editor
   1812         else {
   1813             self.product_editor_form = None;
   1814             self.clear_focused_view_matching(HomeFocusedView::ProductEditor);
   1815             return;
   1816         };
   1817         let Some(product_id) = session.selected_product_id else {
   1818             self.product_editor_form = None;
   1819             self.clear_focused_view_matching(HomeFocusedView::ProductEditor);
   1820             return;
   1821         };
   1822         let should_reset = self
   1823             .product_editor_form
   1824             .as_ref()
   1825             .map(|form| form.account_id != account_id || form.product_id != product_id)
   1826             .unwrap_or(true);
   1827 
   1828         if should_reset {
   1829             self.product_editor_form = Some(ProductEditorFormState::new(
   1830                 account_id,
   1831                 product_id,
   1832                 session.draft.clone(),
   1833                 window,
   1834                 cx,
   1835             ));
   1836         }
   1837     }
   1838 
   1839     fn select_farmer_section(&mut self, section: FarmerSection, cx: &mut Context<Self>) {
   1840         if self.runtime.select_farmer_section(section) {
   1841             self.products_stock_editor = None;
   1842             self.clear_focused_view();
   1843             if section != FarmerSection::Products {
   1844                 self.product_editor_form = None;
   1845             }
   1846             cx.notify();
   1847         }
   1848     }
   1849 
   1850     fn select_personal_section(&mut self, section: PersonalSection, cx: &mut Context<Self>) {
   1851         if self.select_personal_section_update(section) {
   1852             cx.notify();
   1853         }
   1854     }
   1855 
   1856     fn select_personal_section_update(&mut self, section: PersonalSection) -> bool {
   1857         match self.runtime.select_personal_section(section) {
   1858             Ok(true) => {
   1859                 self.products_stock_editor = None;
   1860                 self.product_editor_form = None;
   1861                 self.clear_focused_view();
   1862                 self.clear_buyer_workspace_notice();
   1863                 true
   1864             }
   1865             Ok(false) => self.clear_buyer_workspace_notice(),
   1866             Err(runtime_error) => {
   1867                 error!(
   1868                     target: "shell",
   1869                     event = "buyer.section_select_failed",
   1870                     section = ?section,
   1871                     error = %runtime_error,
   1872                     "failed to select buyer section"
   1873                 );
   1874                 self.set_buyer_workspace_notice(BuyerWorkspaceNotice::MarketplaceRefreshFailed)
   1875             }
   1876         }
   1877     }
   1878 
   1879     fn switch_to_marketplace(&mut self, cx: &mut Context<Self>) {
   1880         match self
   1881             .runtime
   1882             .select_active_surface(radroots_app_view::ActiveSurface::Personal)
   1883         {
   1884             Ok(true) => {
   1885                 self.products_stock_editor = None;
   1886                 self.product_editor_form = None;
   1887                 self.clear_focused_view();
   1888                 cx.notify();
   1889             }
   1890             Ok(false) => {}
   1891             Err(runtime_error) => {
   1892                 error!(
   1893                     target: "shell",
   1894                     event = "shell.switch_marketplace_failed",
   1895                     error = %runtime_error,
   1896                     "failed to switch into marketplace mode"
   1897                 );
   1898             }
   1899         }
   1900     }
   1901 
   1902     fn switch_to_farmer_workspace(&mut self, cx: &mut Context<Self>) {
   1903         match self
   1904             .runtime
   1905             .select_active_surface(radroots_app_view::ActiveSurface::Farmer)
   1906         {
   1907             Ok(true) => {
   1908                 self.products_stock_editor = None;
   1909                 self.product_editor_form = None;
   1910                 self.clear_focused_view();
   1911                 cx.notify();
   1912             }
   1913             Ok(false) => {}
   1914             Err(runtime_error) => {
   1915                 error!(
   1916                     target: "shell",
   1917                     event = "shell.switch_farm_failed",
   1918                     error = %runtime_error,
   1919                     "failed to switch into farm mode"
   1920                 );
   1921             }
   1922         }
   1923     }
   1924 
   1925     fn open_account_entry(&mut self, cx: &mut Context<Self>) {
   1926         if self.runtime.select_account() {
   1927             self.products_stock_editor = None;
   1928             self.product_editor_form = None;
   1929             self.clear_focused_view();
   1930             cx.notify();
   1931         }
   1932     }
   1933 
   1934     fn open_account_tab(&mut self, tab: AccountTab, cx: &mut Context<Self>) {
   1935         let route_changed = self.runtime.select_account();
   1936         if route_changed {
   1937             self.products_stock_editor = None;
   1938             self.product_editor_form = None;
   1939             self.clear_focused_view();
   1940         }
   1941 
   1942         let tab_changed = self.selected_account_tab != tab;
   1943         self.selected_account_tab = tab;
   1944 
   1945         if route_changed || tab_changed {
   1946             cx.notify();
   1947         }
   1948     }
   1949 
   1950     fn select_account_tab(&mut self, tab: AccountTab, cx: &mut Context<Self>) {
   1951         if self.selected_account_tab != tab {
   1952             self.selected_account_tab = tab;
   1953             cx.notify();
   1954         }
   1955     }
   1956 
   1957     fn select_account_farm_details_tab(
   1958         &mut self,
   1959         tab: AccountFarmDetailsTab,
   1960         cx: &mut Context<Self>,
   1961     ) {
   1962         if self.selected_account_farm_details_tab != tab {
   1963             self.selected_account_farm_details_tab = tab;
   1964             cx.notify();
   1965         }
   1966     }
   1967 
   1968     fn handle_startup_signer_input_event(
   1969         &mut self,
   1970         state: &Entity<InputState>,
   1971         event: &InputEvent,
   1972         _: &mut Window,
   1973         cx: &mut Context<Self>,
   1974     ) {
   1975         if !matches!(event, InputEvent::Change) {
   1976             return;
   1977         }
   1978 
   1979         let Some(entry) = self.startup_signer_entry.as_ref() else {
   1980             return;
   1981         };
   1982         if entry.input != *state {
   1983             return;
   1984         }
   1985         if !startup_signer_source_input_is_editable(&self.startup_signer_connect_state) {
   1986             return;
   1987         }
   1988 
   1989         let value = state.read(cx).value().to_string();
   1990         if self.runtime.set_startup_signer_source_input(value.as_str()) {
   1991             self.startup_view.clear_notice();
   1992             self.reset_startup_signer_flow();
   1993             cx.notify();
   1994         }
   1995     }
   1996 
   1997     fn handle_products_search_input_event(
   1998         &mut self,
   1999         state: &Entity<InputState>,
   2000         event: &InputEvent,
   2001         _: &mut Window,
   2002         cx: &mut Context<Self>,
   2003     ) {
   2004         if !matches!(event, InputEvent::Change) {
   2005             return;
   2006         }
   2007 
   2008         let value = state.read(cx).value().to_string();
   2009         match self.runtime.set_products_search_query(value.as_str()) {
   2010             Ok(true) => cx.notify(),
   2011             Ok(false) => {}
   2012             Err(runtime_error) => {
   2013                 error!(
   2014                     target: "products",
   2015                     event = "products.search_query_update_failed",
   2016                     error = %runtime_error,
   2017                     "failed to update products search query"
   2018                 );
   2019             }
   2020         }
   2021     }
   2022 
   2023     fn handle_personal_search_input_event(
   2024         &mut self,
   2025         state: &Entity<InputState>,
   2026         event: &InputEvent,
   2027         _: &mut Window,
   2028         cx: &mut Context<Self>,
   2029     ) {
   2030         if !matches!(event, InputEvent::Change) {
   2031             return;
   2032         }
   2033 
   2034         let value = state.read(cx).value().to_string();
   2035         if self.set_personal_search_query_update(value.as_str()) {
   2036             cx.notify();
   2037         }
   2038     }
   2039 
   2040     fn set_personal_search_query_update(&mut self, value: &str) -> bool {
   2041         match self.runtime.set_personal_search_query(value) {
   2042             Ok(changed) => self.clear_buyer_workspace_notice() || changed,
   2043             Err(runtime_error) => {
   2044                 error!(
   2045                     target: "buyer",
   2046                     event = "buyer.search_query_update_failed",
   2047                     error = %runtime_error,
   2048                     "failed to update buyer search query"
   2049                 );
   2050                 self.set_buyer_workspace_notice(BuyerWorkspaceNotice::MarketplaceRefreshFailed)
   2051             }
   2052         }
   2053     }
   2054 
   2055     fn handle_buyer_order_review_input_event(
   2056         &mut self,
   2057         state: &Entity<InputState>,
   2058         event: &InputEvent,
   2059         _: &mut Window,
   2060         cx: &mut Context<Self>,
   2061     ) {
   2062         if !matches!(event, InputEvent::Change) {
   2063             return;
   2064         }
   2065 
   2066         let Some(form) = self.buyer_order_review_form.as_ref() else {
   2067             return;
   2068         };
   2069         let matches_input = form.name_input == *state
   2070             || form.email_input == *state
   2071             || form.phone_input == *state
   2072             || form.order_note_input == *state;
   2073         if !matches_input {
   2074             return;
   2075         }
   2076 
   2077         match self
   2078             .runtime
   2079             .save_personal_order_review_draft(form.current_draft(cx))
   2080         {
   2081             Ok(true) => cx.notify(),
   2082             Ok(false) => {}
   2083             Err(runtime_error) => {
   2084                 error!(
   2085                     target: "buyer",
   2086                     event = "buyer.order_review_save_failed",
   2087                     error = %runtime_error,
   2088                     "failed to save buyer order review draft"
   2089                 );
   2090             }
   2091         }
   2092     }
   2093 
   2094     fn toggle_personal_search_fulfillment_method(
   2095         &mut self,
   2096         method: FarmOrderMethod,
   2097         enabled: bool,
   2098         cx: &mut Context<Self>,
   2099     ) {
   2100         if self.set_personal_search_fulfillment_method_update(method, enabled) {
   2101             cx.notify();
   2102         }
   2103     }
   2104 
   2105     fn set_personal_search_fulfillment_method_update(
   2106         &mut self,
   2107         method: FarmOrderMethod,
   2108         enabled: bool,
   2109     ) -> bool {
   2110         match self
   2111             .runtime
   2112             .set_personal_search_fulfillment_method(method, enabled)
   2113         {
   2114             Ok(changed) => self.clear_buyer_workspace_notice() || changed,
   2115             Err(runtime_error) => {
   2116                 error!(
   2117                     target: "buyer",
   2118                     event = "buyer.fulfillment_filter_update_failed",
   2119                     error = %runtime_error,
   2120                     method = method.storage_key(),
   2121                     "failed to update buyer fulfillment filter"
   2122                 );
   2123                 self.set_buyer_workspace_notice(BuyerWorkspaceNotice::MarketplaceRefreshFailed)
   2124             }
   2125         }
   2126     }
   2127 
   2128     fn open_personal_product_detail(
   2129         &mut self,
   2130         section: PersonalSection,
   2131         product_id: ProductId,
   2132         cx: &mut Context<Self>,
   2133     ) {
   2134         if self.open_personal_product_detail_update(section, product_id) {
   2135             cx.notify();
   2136         }
   2137     }
   2138 
   2139     fn open_personal_product_detail_update(
   2140         &mut self,
   2141         section: PersonalSection,
   2142         product_id: ProductId,
   2143     ) -> bool {
   2144         match self
   2145             .runtime
   2146             .open_personal_product_detail(section, product_id)
   2147         {
   2148             Ok(true) => {
   2149                 self.clear_buyer_workspace_notice();
   2150                 self.focused_view = Some(HomeFocusedView::BuyerProductDetail(section));
   2151                 true
   2152             }
   2153             Ok(false) => self.clear_buyer_workspace_notice(),
   2154             Err(runtime_error) => {
   2155                 error!(
   2156                     target: "buyer",
   2157                     event = "buyer.detail_open_failed",
   2158                     error = %runtime_error,
   2159                     "failed to open buyer product detail"
   2160                 );
   2161                 self.set_buyer_workspace_notice(BuyerWorkspaceNotice::DetailOpenFailed)
   2162             }
   2163         }
   2164     }
   2165 
   2166     fn set_buyer_workspace_notice(&mut self, notice: BuyerWorkspaceNotice) -> bool {
   2167         let notice = notice.text();
   2168         let changed = self.buyer_workspace_notice.as_deref() != Some(notice.as_str());
   2169         self.buyer_workspace_notice = Some(notice);
   2170         changed
   2171     }
   2172 
   2173     fn clear_buyer_workspace_notice(&mut self) -> bool {
   2174         self.buyer_workspace_notice.take().is_some()
   2175     }
   2176 
   2177     fn close_personal_product_detail(&mut self, section: PersonalSection, cx: &mut Context<Self>) {
   2178         let runtime_changed = self.runtime.close_personal_product_detail(section);
   2179         let focus_changed =
   2180             self.clear_focused_view_matching(HomeFocusedView::BuyerProductDetail(section));
   2181         if runtime_changed || focus_changed {
   2182             cx.notify();
   2183         }
   2184     }
   2185 
   2186     fn increase_personal_product_quantity(
   2187         &mut self,
   2188         section: PersonalSection,
   2189         cx: &mut Context<Self>,
   2190     ) {
   2191         if self.runtime.increase_personal_product_quantity(section) {
   2192             cx.notify();
   2193         }
   2194     }
   2195 
   2196     fn decrease_personal_product_quantity(
   2197         &mut self,
   2198         section: PersonalSection,
   2199         cx: &mut Context<Self>,
   2200     ) {
   2201         if self.runtime.decrease_personal_product_quantity(section) {
   2202             cx.notify();
   2203         }
   2204     }
   2205 
   2206     fn add_personal_product_to_cart(
   2207         &mut self,
   2208         section: PersonalSection,
   2209         replace_existing: bool,
   2210         cx: &mut Context<Self>,
   2211     ) {
   2212         match self
   2213             .runtime
   2214             .add_personal_product_to_cart(section, replace_existing)
   2215         {
   2216             Ok(true) => cx.notify(),
   2217             Ok(false) => {}
   2218             Err(runtime_error) => {
   2219                 error!(
   2220                     target: "buyer",
   2221                     event = "buyer.add_to_cart_failed",
   2222                     error = %runtime_error,
   2223                     "failed to add buyer product to cart"
   2224                 );
   2225             }
   2226         }
   2227     }
   2228 
   2229     fn clear_personal_cart_replace_confirmation(&mut self, cx: &mut Context<Self>) {
   2230         if self.runtime.clear_personal_cart_replace_confirmation() {
   2231             cx.notify();
   2232         }
   2233     }
   2234 
   2235     fn open_personal_order_review(&mut self, window: &mut Window, cx: &mut Context<Self>) {
   2236         if self.buyer_order_review_form.is_some() {
   2237             return;
   2238         }
   2239 
   2240         let runtime_summary = self.runtime.summary();
   2241         if home_stage(&runtime_summary) != HomeStage::BuyerWorkspace
   2242             || selected_personal_section(&runtime_summary) != PersonalSection::Cart
   2243             || runtime_summary
   2244                 .personal_projection
   2245                 .cart
   2246                 .cart
   2247                 .lines
   2248                 .is_empty()
   2249         {
   2250             return;
   2251         }
   2252 
   2253         self.buyer_order_review_form = Some(BuyerOrderReviewFormState::new(
   2254             personal_workspace_id(&runtime_summary),
   2255             &runtime_summary.personal_projection.cart.order_review.draft,
   2256             window,
   2257             cx,
   2258         ));
   2259         self.focused_view = Some(HomeFocusedView::BuyerOrderReview);
   2260         cx.notify();
   2261     }
   2262 
   2263     fn close_personal_order_review(&mut self, cx: &mut Context<Self>) {
   2264         let cleared = self.buyer_order_review_form.take().is_some();
   2265         let focus_changed = self.clear_focused_view_matching(HomeFocusedView::BuyerOrderReview);
   2266         if cleared || focus_changed {
   2267             cx.notify();
   2268         }
   2269     }
   2270 
   2271     fn remove_personal_cart_line(&mut self, product_id: ProductId, cx: &mut Context<Self>) {
   2272         match self.runtime.remove_personal_cart_line(product_id) {
   2273             Ok(true) => cx.notify(),
   2274             Ok(false) => {}
   2275             Err(runtime_error) => {
   2276                 error!(
   2277                     target: "buyer",
   2278                     event = "buyer.cart_remove_failed",
   2279                     error = %runtime_error,
   2280                     product_id = %product_id,
   2281                     "failed to remove buyer cart line"
   2282                 );
   2283             }
   2284         }
   2285     }
   2286 
   2287     fn place_personal_order(&mut self, cx: &mut Context<Self>) {
   2288         if self.place_personal_order_update() {
   2289             cx.notify();
   2290         }
   2291     }
   2292 
   2293     fn place_personal_order_update(&mut self) -> bool {
   2294         match self.runtime.place_personal_order() {
   2295             Ok(true) => {
   2296                 self.buyer_order_review_form = None;
   2297                 let _ = self.clear_buyer_workspace_notice();
   2298                 true
   2299             }
   2300             Ok(false) => false,
   2301             Err(runtime_error) => {
   2302                 let notice = buyer_order_place_failure_notice(&runtime_error);
   2303                 if notice == BuyerWorkspaceNotice::OrderCoordinationFailed {
   2304                     self.buyer_order_review_form = None;
   2305                 }
   2306                 error!(
   2307                     target: "buyer",
   2308                     event = "buyer.order_review_place_failed",
   2309                     error = %runtime_error,
   2310                     "failed to place buyer order"
   2311                 );
   2312                 let notice_changed = self.set_buyer_workspace_notice(notice);
   2313                 buyer_order_coordination_notice_forces_redraw(notice) || notice_changed
   2314             }
   2315         }
   2316     }
   2317 
   2318     fn retry_pending_personal_order_coordination(&mut self, cx: &mut Context<Self>) {
   2319         if self.retry_pending_personal_order_coordination_update() {
   2320             cx.notify();
   2321         }
   2322     }
   2323 
   2324     fn retry_pending_personal_order_coordination_update(&mut self) -> bool {
   2325         match self.runtime.retry_pending_personal_order_coordination() {
   2326             Ok(true) => {
   2327                 let _ = self.clear_buyer_workspace_notice();
   2328                 true
   2329             }
   2330             Ok(false) => false,
   2331             Err(runtime_error) => {
   2332                 error!(
   2333                     target: "buyer",
   2334                     event = "buyer.order_coordination_retry_failed",
   2335                     error = %runtime_error,
   2336                     "failed to retry buyer order coordination"
   2337                 );
   2338                 let notice = BuyerWorkspaceNotice::OrderCoordinationFailed;
   2339                 let notice_changed = self.set_buyer_workspace_notice(notice);
   2340                 buyer_order_coordination_notice_forces_redraw(notice) || notice_changed
   2341             }
   2342         }
   2343     }
   2344 
   2345     fn open_personal_order_detail(&mut self, order_id: OrderId, cx: &mut Context<Self>) {
   2346         match self.runtime.open_personal_order_detail(order_id) {
   2347             Ok(runtime_changed) => {
   2348                 let Some(focused_view) = buyer_order_detail_focus_after_open(
   2349                     runtime_changed,
   2350                     &self.runtime.summary(),
   2351                     order_id,
   2352                 ) else {
   2353                     return;
   2354                 };
   2355                 self.focused_view = Some(focused_view);
   2356                 cx.notify();
   2357             }
   2358             Err(runtime_error) => {
   2359                 error!(
   2360                     target: "buyer",
   2361                     event = "buyer.order_open_failed",
   2362                     error = %runtime_error,
   2363                     order_id = %order_id,
   2364                     "failed to open buyer order detail"
   2365                 );
   2366             }
   2367         }
   2368     }
   2369 
   2370     fn close_personal_order_detail(&mut self, order_id: OrderId, cx: &mut Context<Self>) {
   2371         if self.clear_focused_view_matching(HomeFocusedView::BuyerOrderDetail(order_id)) {
   2372             cx.notify();
   2373         }
   2374     }
   2375 
   2376     fn repeat_personal_order(
   2377         &mut self,
   2378         order_id: OrderId,
   2379         replace_existing: bool,
   2380         cx: &mut Context<Self>,
   2381     ) {
   2382         match self
   2383             .runtime
   2384             .repeat_personal_order(order_id, replace_existing)
   2385         {
   2386             Ok(true) => cx.notify(),
   2387             Ok(false) => {}
   2388             Err(runtime_error) => {
   2389                 error!(
   2390                     target: "buyer",
   2391                     event = "buyer.repeat_demand_failed",
   2392                     error = %runtime_error,
   2393                     order_id = %order_id,
   2394                     "failed to reorder buyer order"
   2395                 );
   2396             }
   2397         }
   2398     }
   2399 
   2400     fn select_products_filter(&mut self, filter: ProductsFilter, cx: &mut Context<Self>) {
   2401         match self.runtime.select_products_filter(filter) {
   2402             Ok(true) => {
   2403                 self.products_stock_editor = None;
   2404                 cx.notify();
   2405             }
   2406             Ok(false) => {}
   2407             Err(runtime_error) => {
   2408                 error!(
   2409                     target: "products",
   2410                     event = "products.filter_update_failed",
   2411                     error = %runtime_error,
   2412                     filter = filter.storage_key(),
   2413                     "failed to update products filter"
   2414                 );
   2415             }
   2416         }
   2417     }
   2418 
   2419     fn select_products_sort(&mut self, sort: ProductsSort, cx: &mut Context<Self>) {
   2420         match self.runtime.select_products_sort(sort) {
   2421             Ok(true) => {
   2422                 self.products_stock_editor = None;
   2423                 cx.notify();
   2424             }
   2425             Ok(false) => {}
   2426             Err(runtime_error) => {
   2427                 error!(
   2428                     target: "products",
   2429                     event = "products.sort_update_failed",
   2430                     error = %runtime_error,
   2431                     sort = sort.storage_key(),
   2432                     "failed to update products sort"
   2433                 );
   2434             }
   2435         }
   2436     }
   2437 
   2438     fn open_products_filter(&mut self, filter: ProductsFilter, cx: &mut Context<Self>) {
   2439         match self.runtime.open_products_filter(filter) {
   2440             Ok(true) => {
   2441                 self.products_stock_editor = None;
   2442                 cx.notify();
   2443             }
   2444             Ok(false) => {}
   2445             Err(runtime_error) => {
   2446                 error!(
   2447                     target: "products",
   2448                     event = "products.route_failed",
   2449                     error = %runtime_error,
   2450                     filter = filter.storage_key(),
   2451                     "failed to route into products view"
   2452                 );
   2453             }
   2454         }
   2455     }
   2456 
   2457     fn open_orders(&mut self, cx: &mut Context<Self>) {
   2458         match self.runtime.open_orders() {
   2459             Ok(true) => {
   2460                 self.products_stock_editor = None;
   2461                 self.product_editor_form = None;
   2462                 self.clear_focused_view();
   2463                 cx.notify();
   2464             }
   2465             Ok(false) => {}
   2466             Err(runtime_error) => {
   2467                 error!(
   2468                     target: "orders",
   2469                     event = "orders.route_failed",
   2470                     error = %runtime_error,
   2471                     "failed to route into orders view"
   2472                 );
   2473             }
   2474         }
   2475     }
   2476 
   2477     fn open_orders_fulfillment_window(
   2478         &mut self,
   2479         fulfillment_window_id: FulfillmentWindowId,
   2480         cx: &mut Context<Self>,
   2481     ) {
   2482         match self
   2483             .runtime
   2484             .open_orders_fulfillment_window(fulfillment_window_id)
   2485         {
   2486             Ok(true) => {
   2487                 self.products_stock_editor = None;
   2488                 self.product_editor_form = None;
   2489                 self.clear_focused_view();
   2490                 cx.notify();
   2491             }
   2492             Ok(false) => {}
   2493             Err(runtime_error) => {
   2494                 error!(
   2495                     target: "orders",
   2496                     event = "orders.route_failed",
   2497                     error = %runtime_error,
   2498                     fulfillment_window_id = %fulfillment_window_id,
   2499                     "failed to route into orders view"
   2500                 );
   2501             }
   2502         }
   2503     }
   2504 
   2505     fn open_pack_day(
   2506         &mut self,
   2507         fulfillment_window_id: Option<FulfillmentWindowId>,
   2508         cx: &mut Context<Self>,
   2509     ) {
   2510         match self.runtime.open_pack_day(fulfillment_window_id) {
   2511             Ok(true) => {
   2512                 self.products_stock_editor = None;
   2513                 self.product_editor_form = None;
   2514                 self.clear_focused_view();
   2515                 cx.notify();
   2516             }
   2517             Ok(false) => {}
   2518             Err(runtime_error) => {
   2519                 error!(
   2520                     target: "pack_day",
   2521                     event = "pack_day.route_failed",
   2522                     error = %runtime_error,
   2523                     "failed to route into pack day view"
   2524                 );
   2525             }
   2526         }
   2527     }
   2528 
   2529     fn export_pack_day(&mut self, cx: &mut Context<Self>) {
   2530         match self.runtime.export_pack_day() {
   2531             Ok(true) => cx.notify(),
   2532             Ok(false) => {}
   2533             Err(runtime_error) => {
   2534                 error!(
   2535                     target: "pack_day",
   2536                     event = "pack_day.export_failed",
   2537                     error = %runtime_error,
   2538                     "failed to export pack day"
   2539                 );
   2540                 cx.notify();
   2541             }
   2542         }
   2543     }
   2544 
   2545     fn start_pack_day_host_handoff(
   2546         &mut self,
   2547         kind: PackDayHostHandoffKind,
   2548         window: &mut Window,
   2549         cx: &mut Context<Self>,
   2550     ) {
   2551         match self.runtime.prepare_pack_day_host_handoff(kind) {
   2552             Ok(Some((request, plan))) => {
   2553                 cx.notify();
   2554                 cx.spawn_in(window, async move |this, cx| {
   2555                     let result = cx
   2556                         .background_executor()
   2557                         .spawn(run_pack_day_host_handoff(plan))
   2558                         .await;
   2559                     let _ = this.update(cx, |this, cx| {
   2560                         this.finish_pack_day_host_handoff(request, result, cx);
   2561                     });
   2562                 })
   2563                 .detach();
   2564             }
   2565             Ok(None) => {}
   2566             Err(runtime_error) => {
   2567                 error!(
   2568                     target: "pack_day",
   2569                     event = "pack_day.host_handoff_prepare_failed",
   2570                     kind = %kind.storage_key(),
   2571                     error = %runtime_error,
   2572                     "failed to prepare pack day host handoff"
   2573                 );
   2574                 cx.notify();
   2575             }
   2576         }
   2577     }
   2578 
   2579     fn start_pack_day_print(
   2580         &mut self,
   2581         kind: PackDayPrintKind,
   2582         window: &mut Window,
   2583         cx: &mut Context<Self>,
   2584     ) {
   2585         match self.runtime.prepare_pack_day_print(kind) {
   2586             Ok(Some((request, plan))) => {
   2587                 cx.notify();
   2588                 cx.spawn_in(window, async move |this, cx| {
   2589                     let result = cx
   2590                         .background_executor()
   2591                         .spawn(run_pack_day_print(plan))
   2592                         .await;
   2593                     let _ = this.update(cx, |this, cx| {
   2594                         this.finish_pack_day_print(request, result, cx);
   2595                     });
   2596                 })
   2597                 .detach();
   2598             }
   2599             Ok(None) => {}
   2600             Err(runtime_error) => {
   2601                 error!(
   2602                     target: "pack_day",
   2603                     event = "pack_day.print_prepare_failed",
   2604                     kind = %kind.storage_key(),
   2605                     error = %runtime_error,
   2606                     "failed to prepare pack day print"
   2607                 );
   2608                 cx.notify();
   2609             }
   2610         }
   2611     }
   2612 
   2613     fn start_pack_day_batch_print(&mut self, window: &mut Window, cx: &mut Context<Self>) {
   2614         match self.runtime.prepare_pack_day_batch_print() {
   2615             Ok(Some((request, plan))) => {
   2616                 cx.notify();
   2617                 cx.spawn_in(window, async move |this, cx| {
   2618                     let result = cx
   2619                         .background_executor()
   2620                         .spawn(run_pack_day_batch_print(plan))
   2621                         .await;
   2622                     let _ = this.update(cx, |this, cx| {
   2623                         this.finish_pack_day_batch_print(request, result, cx);
   2624                     });
   2625                 })
   2626                 .detach();
   2627             }
   2628             Ok(None) => {}
   2629             Err(runtime_error) => {
   2630                 error!(
   2631                     target: "pack_day",
   2632                     event = "pack_day.batch_print_prepare_failed",
   2633                     error = %runtime_error,
   2634                     "failed to prepare pack day batch print"
   2635                 );
   2636                 cx.notify();
   2637             }
   2638         }
   2639     }
   2640 
   2641     fn finish_pack_day_host_handoff(
   2642         &mut self,
   2643         request: PackDayHostHandoffRequest,
   2644         result: Result<(), PackDayHostHandoffError>,
   2645         cx: &mut Context<Self>,
   2646     ) {
   2647         let kind = request.kind.storage_key();
   2648         match self.runtime.finish_pack_day_host_handoff(request, result) {
   2649             Ok(true) => cx.notify(),
   2650             Ok(false) => {}
   2651             Err(runtime_error) => {
   2652                 error!(
   2653                     target: "pack_day",
   2654                     event = "pack_day.host_handoff_failed",
   2655                     kind = %kind,
   2656                     error = %runtime_error,
   2657                     "failed to complete pack day host handoff"
   2658                 );
   2659                 cx.notify();
   2660             }
   2661         }
   2662     }
   2663 
   2664     fn finish_pack_day_print(
   2665         &mut self,
   2666         request: PackDayPrintRequest,
   2667         result: Result<(), PackDayPrintError>,
   2668         cx: &mut Context<Self>,
   2669     ) {
   2670         let kind = request.kind.storage_key();
   2671         match self.runtime.finish_pack_day_print(request, result) {
   2672             Ok(true) => cx.notify(),
   2673             Ok(false) => {}
   2674             Err(runtime_error) => {
   2675                 error!(
   2676                     target: "pack_day",
   2677                     event = "pack_day.print_failed",
   2678                     kind = %kind,
   2679                     error = %runtime_error,
   2680                     "failed to complete pack day print"
   2681                 );
   2682                 cx.notify();
   2683             }
   2684         }
   2685     }
   2686 
   2687     fn finish_pack_day_batch_print(
   2688         &mut self,
   2689         request: PackDayBatchPrintRequest,
   2690         result: Result<(), PackDayBatchPrintError>,
   2691         cx: &mut Context<Self>,
   2692     ) {
   2693         match self.runtime.finish_pack_day_batch_print(request, result) {
   2694             Ok(true) => cx.notify(),
   2695             Ok(false) => {}
   2696             Err(runtime_error) => {
   2697                 error!(
   2698                     target: "pack_day",
   2699                     event = "pack_day.batch_print_failed",
   2700                     error = %runtime_error,
   2701                     "failed to complete pack day batch print"
   2702                 );
   2703                 cx.notify();
   2704             }
   2705         }
   2706     }
   2707 
   2708     fn open_today_next_window(
   2709         &mut self,
   2710         fulfillment_window_id: Option<FulfillmentWindowId>,
   2711         cx: &mut Context<Self>,
   2712     ) {
   2713         let Some(fulfillment_window_id) = fulfillment_window_id else {
   2714             return;
   2715         };
   2716 
   2717         match self.runtime.open_pack_day(Some(fulfillment_window_id)) {
   2718             Ok(true) => {
   2719                 self.products_stock_editor = None;
   2720                 self.product_editor_form = None;
   2721                 self.clear_focused_view();
   2722                 cx.notify();
   2723             }
   2724             Ok(false) => self.open_orders_fulfillment_window(fulfillment_window_id, cx),
   2725             Err(runtime_error) => {
   2726                 error!(
   2727                     target: "pack_day",
   2728                     event = "pack_day.route_failed",
   2729                     error = %runtime_error,
   2730                     "failed to route into pack day view"
   2731                 );
   2732             }
   2733         }
   2734     }
   2735 
   2736     fn select_orders_filter(&mut self, filter: OrdersFilter, cx: &mut Context<Self>) {
   2737         match self.runtime.select_orders_filter(filter) {
   2738             Ok(true) => cx.notify(),
   2739             Ok(false) => {}
   2740             Err(runtime_error) => {
   2741                 error!(
   2742                     target: "orders",
   2743                     event = "orders.filter_update_failed",
   2744                     error = %runtime_error,
   2745                     filter = filter.storage_key(),
   2746                     "failed to update orders filter"
   2747                 );
   2748             }
   2749         }
   2750     }
   2751 
   2752     fn open_order_detail(&mut self, order_id: OrderId, cx: &mut Context<Self>) {
   2753         match self.runtime.open_order_detail(order_id) {
   2754             Ok(runtime_changed) => {
   2755                 let Some(focused_view) = farmer_order_detail_focus_after_open(
   2756                     runtime_changed,
   2757                     &self.runtime.summary(),
   2758                     order_id,
   2759                 ) else {
   2760                     return;
   2761                 };
   2762                 self.products_stock_editor = None;
   2763                 self.product_editor_form = None;
   2764                 self.focused_view = Some(focused_view);
   2765                 cx.notify();
   2766             }
   2767             Err(runtime_error) => {
   2768                 error!(
   2769                     target: "orders",
   2770                     event = "orders.detail_open_failed",
   2771                     error = %runtime_error,
   2772                     order_id = %order_id,
   2773                     "failed to open order detail"
   2774                 );
   2775             }
   2776         }
   2777     }
   2778 
   2779     fn close_order_detail(&mut self, order_id: OrderId, cx: &mut Context<Self>) {
   2780         if self.clear_focused_view_matching(HomeFocusedView::FarmerOrderDetail(order_id)) {
   2781             cx.notify();
   2782         }
   2783     }
   2784 
   2785     fn dismiss_presented_reminder(&mut self, reminder_id: ReminderId, cx: &mut Context<Self>) {
   2786         match self.runtime.acknowledge_reminder(reminder_id) {
   2787             Ok(true) => cx.notify(),
   2788             Ok(false) => {}
   2789             Err(runtime_error) => {
   2790                 error!(
   2791                     target: "reminders",
   2792                     event = "reminders.ack_failed",
   2793                     error = %runtime_error,
   2794                     reminder_id = %reminder_id,
   2795                     "failed to acknowledge reminder"
   2796                 );
   2797             }
   2798         }
   2799     }
   2800 
   2801     fn open_presented_order_reminder(
   2802         &mut self,
   2803         reminder_id: ReminderId,
   2804         order_id: OrderId,
   2805         cx: &mut Context<Self>,
   2806     ) {
   2807         match self.runtime.open_order_detail(order_id) {
   2808             Ok(true) | Ok(false) => {
   2809                 self.products_stock_editor = None;
   2810                 self.product_editor_form = None;
   2811                 self.focused_view = Some(HomeFocusedView::FarmerOrderDetail(order_id));
   2812                 self.dismiss_presented_reminder(reminder_id, cx);
   2813             }
   2814             Err(runtime_error) => {
   2815                 error!(
   2816                     target: "orders",
   2817                     event = "orders.detail_open_failed",
   2818                     error = %runtime_error,
   2819                     order_id = %order_id,
   2820                     "failed to open order detail"
   2821                 );
   2822             }
   2823         }
   2824     }
   2825 
   2826     fn open_presented_pack_day_reminder(
   2827         &mut self,
   2828         reminder_id: ReminderId,
   2829         fulfillment_window_id: FulfillmentWindowId,
   2830         cx: &mut Context<Self>,
   2831     ) {
   2832         match self.runtime.open_pack_day(Some(fulfillment_window_id)) {
   2833             Ok(true) | Ok(false) => {
   2834                 self.products_stock_editor = None;
   2835                 self.product_editor_form = None;
   2836                 self.dismiss_presented_reminder(reminder_id, cx);
   2837             }
   2838             Err(runtime_error) => {
   2839                 error!(
   2840                     target: "pack_day",
   2841                     event = "pack_day.route_failed",
   2842                     error = %runtime_error,
   2843                     "failed to route into pack day view"
   2844                 );
   2845             }
   2846         }
   2847     }
   2848 
   2849     fn open_presented_orders_reminder(&mut self, reminder_id: ReminderId, cx: &mut Context<Self>) {
   2850         match self.runtime.open_orders() {
   2851             Ok(true) | Ok(false) => {
   2852                 self.products_stock_editor = None;
   2853                 self.product_editor_form = None;
   2854                 self.dismiss_presented_reminder(reminder_id, cx);
   2855             }
   2856             Err(runtime_error) => {
   2857                 error!(
   2858                     target: "orders",
   2859                     event = "orders.route_failed",
   2860                     error = %runtime_error,
   2861                     "failed to route into orders view"
   2862                 );
   2863             }
   2864         }
   2865     }
   2866 
   2867     fn cancel_buyer_order(&mut self, order_id: OrderId, cx: &mut Context<Self>) {
   2868         match self.runtime.publish_buyer_order_cancel(order_id) {
   2869             Ok(true) => cx.notify(),
   2870             Ok(false) => {}
   2871             Err(runtime_error) => {
   2872                 error!(
   2873                     target: "personal_orders",
   2874                     event = "buyer.order_cancel_failed",
   2875                     error = %runtime_error,
   2876                     order_id = %order_id,
   2877                     "failed to cancel buyer order"
   2878                 );
   2879             }
   2880         }
   2881     }
   2882 
   2883     fn accept_buyer_order_revision(&mut self, order_id: OrderId, cx: &mut Context<Self>) {
   2884         match self.runtime.publish_buyer_order_revision_accept(order_id) {
   2885             Ok(true) => cx.notify(),
   2886             Ok(false) => {}
   2887             Err(runtime_error) => {
   2888                 error!(
   2889                     target: "personal_orders",
   2890                     event = "buyer.order_revision_accept_failed",
   2891                     error = %runtime_error,
   2892                     order_id = %order_id,
   2893                     "failed to accept buyer order change"
   2894                 );
   2895             }
   2896         }
   2897     }
   2898 
   2899     fn decline_buyer_order_revision(&mut self, order_id: OrderId, cx: &mut Context<Self>) {
   2900         match self.runtime.publish_buyer_order_revision_decline(order_id) {
   2901             Ok(true) => cx.notify(),
   2902             Ok(false) => {}
   2903             Err(runtime_error) => {
   2904                 error!(
   2905                     target: "personal_orders",
   2906                     event = "buyer.order_revision_decline_failed",
   2907                     error = %runtime_error,
   2908                     order_id = %order_id,
   2909                     "failed to keep buyer order"
   2910                 );
   2911             }
   2912         }
   2913     }
   2914 
   2915     fn open_products_stock_editor(
   2916         &mut self,
   2917         product_id: ProductId,
   2918         stock_quantity: Option<u32>,
   2919         window: &mut Window,
   2920         cx: &mut Context<Self>,
   2921     ) {
   2922         let _ = self.runtime.close_product_editor();
   2923         let Some(account_id) = self
   2924             .runtime
   2925             .summary()
   2926             .settings_account_projection
   2927             .selected_account
   2928             .as_ref()
   2929             .map(|account| account.account.account_id.clone())
   2930         else {
   2931             return;
   2932         };
   2933 
   2934         if self
   2935             .products_stock_editor
   2936             .as_ref()
   2937             .map(|editor| editor.product_id == product_id)
   2938             .unwrap_or(false)
   2939         {
   2940             self.products_stock_editor = None;
   2941             cx.notify();
   2942             return;
   2943         }
   2944 
   2945         self.products_stock_editor = Some(ProductsStockEditorState::new(
   2946             account_id,
   2947             product_id,
   2948             stock_quantity,
   2949             window,
   2950             cx,
   2951         ));
   2952         self.product_editor_form = None;
   2953         cx.notify();
   2954     }
   2955 
   2956     fn close_products_stock_editor(&mut self, cx: &mut Context<Self>) {
   2957         if self.products_stock_editor.take().is_some() {
   2958             cx.notify();
   2959         }
   2960     }
   2961 
   2962     fn handle_products_stock_input_event(
   2963         &mut self,
   2964         state: &Entity<InputState>,
   2965         event: &InputEvent,
   2966         _: &mut Window,
   2967         cx: &mut Context<Self>,
   2968     ) {
   2969         if !matches!(event, InputEvent::Change) {
   2970             return;
   2971         }
   2972 
   2973         let Some(editor) = self.products_stock_editor.as_mut() else {
   2974             return;
   2975         };
   2976 
   2977         if editor.input != *state || editor.save_issue.is_none() {
   2978             return;
   2979         }
   2980 
   2981         editor.save_issue = None;
   2982         cx.notify();
   2983     }
   2984 
   2985     fn save_products_stock_editor(&mut self, cx: &mut Context<Self>) {
   2986         let Some((product_id, stock_quantity)) =
   2987             self.products_stock_editor.as_ref().and_then(|editor| {
   2988                 editor
   2989                     .parsed_stock_quantity(cx)
   2990                     .map(|stock_quantity| (editor.product_id, stock_quantity))
   2991             })
   2992         else {
   2993             return;
   2994         };
   2995 
   2996         match self
   2997             .runtime
   2998             .update_product_stock(product_id, stock_quantity)
   2999         {
   3000             Ok(true) => {
   3001                 self.products_stock_editor = None;
   3002                 cx.notify();
   3003             }
   3004             Ok(false) => {}
   3005             Err(runtime_error) => {
   3006                 error!(
   3007                     target: "products",
   3008                     event = "products.stock_update_failed",
   3009                     error = %runtime_error,
   3010                     product_id = %product_id,
   3011                     stock_quantity,
   3012                     "failed to update product stock"
   3013                 );
   3014 
   3015                 if let Some(editor) = self.products_stock_editor.as_mut() {
   3016                     let save_issue =
   3017                         ProductsStockEditorSaveIssue::from_runtime_error(&runtime_error);
   3018                     if save_issue == ProductsStockEditorSaveIssue::PublishQueueFailed {
   3019                         editor.initial_stock_quantity = Some(stock_quantity);
   3020                     }
   3021                     editor.save_issue = Some(save_issue);
   3022                 }
   3023                 cx.notify();
   3024             }
   3025         }
   3026     }
   3027 
   3028     fn open_new_product_editor(&mut self, cx: &mut Context<Self>) {
   3029         match self.runtime.open_new_product_editor() {
   3030             Ok(true) => {
   3031                 self.products_stock_editor = None;
   3032                 self.focused_view = Some(HomeFocusedView::ProductEditor);
   3033                 cx.notify();
   3034             }
   3035             Ok(false) => {}
   3036             Err(runtime_error) => {
   3037                 error!(
   3038                     target: "products",
   3039                     event = "products.new_editor_open_failed",
   3040                     error = %runtime_error,
   3041                     "failed to open new product editor"
   3042                 );
   3043             }
   3044         }
   3045     }
   3046 
   3047     fn open_existing_product_editor(&mut self, product_id: ProductId, cx: &mut Context<Self>) {
   3048         match self.runtime.open_existing_product_editor(product_id) {
   3049             Ok(true) => {
   3050                 self.products_stock_editor = None;
   3051                 self.focused_view = Some(HomeFocusedView::ProductEditor);
   3052                 cx.notify();
   3053             }
   3054             Ok(false) => {}
   3055             Err(runtime_error) => {
   3056                 error!(
   3057                     target: "products",
   3058                     event = "products.editor_open_failed",
   3059                     error = %runtime_error,
   3060                     product_id = %product_id,
   3061                     "failed to open existing product editor"
   3062                 );
   3063             }
   3064         }
   3065     }
   3066 
   3067     fn close_product_editor(&mut self, cx: &mut Context<Self>) {
   3068         let changed = self.runtime.close_product_editor();
   3069         let cleared = self.product_editor_form.take().is_some();
   3070         let focus_changed = self.clear_focused_view_matching(HomeFocusedView::ProductEditor);
   3071 
   3072         if changed || cleared || focus_changed {
   3073             cx.notify();
   3074         }
   3075     }
   3076 
   3077     fn handle_product_editor_input_event(
   3078         &mut self,
   3079         state: &Entity<InputState>,
   3080         event: &InputEvent,
   3081         _: &mut Window,
   3082         cx: &mut Context<Self>,
   3083     ) {
   3084         if !matches!(event, InputEvent::Change) {
   3085             return;
   3086         }
   3087 
   3088         let Some(form) = self.product_editor_form.as_mut() else {
   3089             return;
   3090         };
   3091         let matches_input = form.title_input == *state
   3092             || form.subtitle_input == *state
   3093             || form.category_input == *state
   3094             || form.unit_input == *state
   3095             || form.price_input == *state
   3096             || form.stock_input == *state;
   3097 
   3098         if !matches_input {
   3099             return;
   3100         }
   3101 
   3102         if form.save_issue.is_some() {
   3103             form.save_issue = None;
   3104         }
   3105 
   3106         cx.notify();
   3107     }
   3108 
   3109     fn select_product_editor_availability_window(
   3110         &mut self,
   3111         availability_window_id: FulfillmentWindowId,
   3112         cx: &mut Context<Self>,
   3113     ) {
   3114         let Some(form) = self.product_editor_form.as_mut() else {
   3115             return;
   3116         };
   3117 
   3118         if form.selected_availability_window_id == Some(availability_window_id) {
   3119             return;
   3120         }
   3121 
   3122         form.selected_availability_window_id = Some(availability_window_id);
   3123         form.save_issue = None;
   3124         cx.notify();
   3125     }
   3126 
   3127     fn select_product_editor_status(&mut self, status: ProductStatus, cx: &mut Context<Self>) {
   3128         let Some(form) = self.product_editor_form.as_mut() else {
   3129             return;
   3130         };
   3131 
   3132         if form.status == status {
   3133             return;
   3134         }
   3135 
   3136         form.status = status;
   3137         form.save_issue = None;
   3138         cx.notify();
   3139     }
   3140 
   3141     fn save_product_editor(&mut self, cx: &mut Context<Self>) {
   3142         let Some(form) = self.product_editor_form.as_mut() else {
   3143             return;
   3144         };
   3145         let Some(draft) = form.current_draft(cx) else {
   3146             return;
   3147         };
   3148 
   3149         match self.runtime.save_product_editor_draft(draft.clone()) {
   3150             Ok(true) => {
   3151                 form.initial_draft = draft;
   3152                 form.save_issue = None;
   3153                 cx.notify();
   3154             }
   3155             Ok(false) => {}
   3156             Err(runtime_error) => {
   3157                 error!(
   3158                     target: "products",
   3159                     event = "products.editor_save_failed",
   3160                     error = %runtime_error,
   3161                     product_id = %form.product_id,
   3162                     "failed to save product editor draft"
   3163                 );
   3164                 if runtime_error.is_listing_publish_sdk_enqueue_failed() {
   3165                     form.initial_draft = draft;
   3166                 }
   3167                 form.save_issue = Some(ProductEditorSaveIssue::from_runtime_error(&runtime_error));
   3168                 cx.notify();
   3169             }
   3170         }
   3171     }
   3172 
   3173     fn handle_farm_name_input_event(
   3174         &mut self,
   3175         state: &Entity<InputState>,
   3176         event: &InputEvent,
   3177         _: &mut Window,
   3178         cx: &mut Context<Self>,
   3179     ) {
   3180         if matches!(event, InputEvent::Change) {
   3181             let value = state.read(cx).value().to_string();
   3182             self.update_farm_setup_draft(cx, |draft| {
   3183                 draft.farm_name = value;
   3184             });
   3185         }
   3186     }
   3187 
   3188     fn handle_location_input_event(
   3189         &mut self,
   3190         state: &Entity<InputState>,
   3191         event: &InputEvent,
   3192         _: &mut Window,
   3193         cx: &mut Context<Self>,
   3194     ) {
   3195         if matches!(event, InputEvent::Change) {
   3196             let value = state.read(cx).value().to_string();
   3197             self.update_farm_setup_draft(cx, |draft| {
   3198                 draft.location_or_service_area = value;
   3199             });
   3200         }
   3201     }
   3202 
   3203     fn toggle_farm_order_method(
   3204         &mut self,
   3205         method: FarmOrderMethod,
   3206         enabled: bool,
   3207         cx: &mut Context<Self>,
   3208     ) {
   3209         self.update_farm_setup_draft(cx, |draft| {
   3210             if enabled {
   3211                 draft.order_methods.insert(method);
   3212             } else {
   3213                 draft.order_methods.remove(&method);
   3214             }
   3215         });
   3216     }
   3217 
   3218     fn update_farm_setup_draft(
   3219         &mut self,
   3220         cx: &mut Context<Self>,
   3221         update: impl FnOnce(&mut FarmSetupDraft),
   3222     ) {
   3223         let Some(form) = self.farm_setup_form.as_mut() else {
   3224             return;
   3225         };
   3226 
   3227         update(&mut form.draft);
   3228 
   3229         match self.runtime.save_farm_setup_draft(form.draft.clone()) {
   3230             Ok(projection) => {
   3231                 form.draft = projection.draft;
   3232                 form.save_state = FarmSetupSaveState::SavedLocally;
   3233             }
   3234             Err(_) => {
   3235                 form.save_state = FarmSetupSaveState::SaveFailed;
   3236             }
   3237         }
   3238 
   3239         cx.notify();
   3240     }
   3241 
   3242     fn finish_farm_setup(&mut self, cx: &mut Context<Self>) {
   3243         let Some(form) = self.farm_setup_form.as_mut() else {
   3244             return;
   3245         };
   3246 
   3247         match self.runtime.finish_farm_setup() {
   3248             Ok(_) => {
   3249                 form.save_state = FarmSetupSaveState::SavedLocally;
   3250                 self.farm_setup_form = None;
   3251             }
   3252             Err(_) => {
   3253                 form.save_state = FarmSetupSaveState::SaveFailed;
   3254             }
   3255         }
   3256 
   3257         cx.notify();
   3258     }
   3259 
   3260     fn render_today_content(
   3261         &mut self,
   3262         runtime: &DesktopAppRuntimeSummary,
   3263         cx: &mut Context<Self>,
   3264     ) -> AnyElement {
   3265         let projection = &runtime.today_projection;
   3266         let home_status = home_status_presentation(runtime);
   3267         let setup_onboarding = farm_setup_onboarding_card_spec(runtime.home_route);
   3268         let farm_state = farmer_home_farm_state(runtime);
   3269         let next_fulfillment_window_id = projection
   3270             .next_fulfillment_window
   3271             .as_ref()
   3272             .map(|window| window.fulfillment_window_id);
   3273         let mut sections = Vec::<AnyElement>::new();
   3274 
   3275         if let Some(summary) = projection.summary.as_ref() {
   3276             sections.push(home_summary_card(summary).into_any_element());
   3277         }
   3278 
   3279         if let Some(issue) = runtime.startup_issue.as_ref() {
   3280             sections.push(
   3281                 home_card(
   3282                     app_shared_text(AppTextKey::MetadataStartupIssue),
   3283                     home_body_text(issue.clone()),
   3284                 )
   3285                 .into_any_element(),
   3286             );
   3287         }
   3288 
   3289         if let Some(spec) = setup_onboarding {
   3290             sections.push(
   3291                 home_farm_setup_onboarding_card(
   3292                     spec,
   3293                     cx.listener(|this, _, window, cx| this.open_farm_setup(window, cx)),
   3294                     cx,
   3295                 )
   3296                 .into_any_element(),
   3297             );
   3298         } else if projection.needs_setup() {
   3299             sections.push(
   3300                 home_setup_card(
   3301                     projection,
   3302                     matches!(farm_state, FarmerHomeFarmState::IncompleteFarm).then_some(
   3303                         action_button_primary(
   3304                             "home-farm-setup-continue",
   3305                             app_shared_text(AppTextKey::HomeFarmSetupContinueAction),
   3306                             cx.listener(|this, _, window, cx| this.open_farm_setup(window, cx)),
   3307                             cx,
   3308                         )
   3309                         .into_any_element(),
   3310                     ),
   3311                 )
   3312                 .into_any_element(),
   3313             );
   3314         }
   3315 
   3316         if let Some(saved_farm_summary_card) = home_saved_farm_summary_card(runtime) {
   3317             sections.push(saved_farm_summary_card);
   3318         }
   3319 
   3320         if !projection.reminders.is_empty() {
   3321             sections.push(self.render_today_reminder_strip(&projection.reminders.items, cx));
   3322         }
   3323 
   3324         if let Some(next_window) = projection.next_fulfillment_window.as_ref() {
   3325             sections.push(
   3326                 home_next_fulfillment_window_card(
   3327                     next_window,
   3328                     Some(
   3329                         action_button_compact(
   3330                             "home-today-open-pack-day",
   3331                             app_shared_text(AppTextKey::HomeTodayOpenInPackDayAction),
   3332                             cx.listener(move |this, _, _, cx| {
   3333                                 this.open_today_next_window(next_fulfillment_window_id, cx)
   3334                             }),
   3335                             cx,
   3336                         )
   3337                         .into_any_element(),
   3338                     ),
   3339                 )
   3340                 .into_any_element(),
   3341             );
   3342         }
   3343 
   3344         if !projection.orders_needing_action.is_empty() {
   3345             sections.push(
   3346                 home_list_card(
   3347                     AppTextKey::HomeTodayOrdersNeedingAction,
   3348                     projection
   3349                         .orders_needing_action
   3350                         .iter()
   3351                         .enumerate()
   3352                         .map(|(index, order)| {
   3353                             home_order_row(
   3354                                 index,
   3355                                 order,
   3356                                 cx.listener({
   3357                                     let order_id = order.order_id;
   3358                                     move |this, _, _, cx| this.open_order_detail(order_id, cx)
   3359                                 }),
   3360                                 cx,
   3361                             )
   3362                         })
   3363                         .collect::<Vec<_>>(),
   3364                     Some(
   3365                         action_button_compact(
   3366                             "home-today-open-orders",
   3367                             app_shared_text(AppTextKey::HomeTodayOpenInOrdersAction),
   3368                             cx.listener(|this, _, _, cx| this.open_orders(cx)),
   3369                             cx,
   3370                         )
   3371                         .into_any_element(),
   3372                     ),
   3373                 )
   3374                 .into_any_element(),
   3375             );
   3376         }
   3377 
   3378         if !projection.low_stock_products.is_empty() {
   3379             sections.push(
   3380                 home_list_card(
   3381                     AppTextKey::HomeTodayLowStock,
   3382                     projection
   3383                         .low_stock_products
   3384                         .iter()
   3385                         .map(home_low_stock_row)
   3386                         .collect::<Vec<_>>(),
   3387                     Some(
   3388                         action_button_compact(
   3389                             "home-today-open-products-low-stock",
   3390                             app_shared_text(AppTextKey::HomeTodayOpenInProductsAction),
   3391                             cx.listener(|this, _, _, cx| {
   3392                                 this.open_products_filter(ProductsFilter::NeedAttention, cx)
   3393                             }),
   3394                             cx,
   3395                         )
   3396                         .into_any_element(),
   3397                     ),
   3398                 )
   3399                 .into_any_element(),
   3400             );
   3401         }
   3402 
   3403         if !projection.draft_products.is_empty() {
   3404             sections.push(
   3405                 home_list_card(
   3406                     AppTextKey::HomeTodayDraftProducts,
   3407                     projection
   3408                         .draft_products
   3409                         .iter()
   3410                         .map(home_draft_row)
   3411                         .collect::<Vec<_>>(),
   3412                     Some(
   3413                         action_button_compact(
   3414                             "home-today-open-products-drafts",
   3415                             app_shared_text(AppTextKey::HomeTodayOpenInProductsAction),
   3416                             cx.listener(|this, _, _, cx| {
   3417                                 this.open_products_filter(ProductsFilter::Drafts, cx)
   3418                             }),
   3419                             cx,
   3420                         )
   3421                         .into_any_element(),
   3422                     ),
   3423                 )
   3424                 .into_any_element(),
   3425             );
   3426         }
   3427 
   3428         if runtime.startup_issue.is_none() && runtime.startup_gate == AppStartupGate::SetupRequired
   3429         {
   3430             sections.push(
   3431                 home_empty_state_card(
   3432                     AppTextKey::HomeTodayEmptySetupTitle,
   3433                     AppTextKey::HomeTodayEmptySetupBody,
   3434                 )
   3435                 .into_any_element(),
   3436             );
   3437         } else if runtime.startup_issue.is_none()
   3438             && farm_state == FarmerHomeFarmState::NoFarm
   3439             && setup_onboarding.is_none()
   3440         {
   3441             sections.push(
   3442                 home_empty_state_card(
   3443                     AppTextKey::HomeTodayEmptyNoFarmTitle,
   3444                     AppTextKey::HomeTodayEmptyNoFarmBody,
   3445                 )
   3446                 .into_any_element(),
   3447             );
   3448         } else if runtime.startup_issue.is_none()
   3449             && farm_state == FarmerHomeFarmState::ConfiguredFarm
   3450             && !projection.needs_setup()
   3451             && projection.next_fulfillment_window.is_none()
   3452             && !projection.has_attention_items()
   3453         {
   3454             sections.push(
   3455                 home_empty_state_card(
   3456                     AppTextKey::HomeTodayEmptyQuietTitle,
   3457                     AppTextKey::HomeTodayEmptyQuietBody,
   3458                 )
   3459                 .into_any_element(),
   3460             );
   3461         }
   3462 
   3463         div()
   3464             .w_full()
   3465             .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
   3466             .mx_auto()
   3467             .flex()
   3468             .flex_col()
   3469             .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
   3470             .child(
   3471                 div()
   3472                     .w_full()
   3473                     .flex()
   3474                     .flex_col()
   3475                     .gap(px(4.0))
   3476                     .child(
   3477                         div()
   3478                             .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 2.0))
   3479                             .font_weight(gpui::FontWeight::BOLD)
   3480                             .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   3481                             .child(app_shared_text(AppTextKey::HomeTodayTitle)),
   3482                     )
   3483                     .child(
   3484                         div()
   3485                             .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   3486                             .font_weight(gpui::FontWeight::MEDIUM)
   3487                             .line_height(relative(1.2))
   3488                             .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   3489                             .when_some(home_saved_farm(runtime), |this, farm| {
   3490                                 this.child(farm.display_name.clone())
   3491                             })
   3492                             .when(home_saved_farm(runtime).is_none(), |this| {
   3493                                 this.child(app_shared_text(home_status.label_key))
   3494                             }),
   3495                     )
   3496                     .child(home_status_row(&home_status)),
   3497             )
   3498             .children(sections)
   3499             .into_any_element()
   3500     }
   3501 
   3502     fn render_buyer_workspace(
   3503         &mut self,
   3504         runtime: &DesktopAppRuntimeSummary,
   3505         cx: &mut Context<Self>,
   3506     ) -> AnyElement {
   3507         let selected_personal_section = selected_personal_section(runtime);
   3508         let main_content = self
   3509             .render_buyer_focused_view(runtime, cx)
   3510             .unwrap_or_else(|| match selected_personal_section {
   3511                 PersonalSection::Browse => self
   3512                     .render_buyer_browse_content(runtime, cx)
   3513                     .into_any_element(),
   3514                 PersonalSection::Search => self
   3515                     .render_buyer_search_content(runtime, cx)
   3516                     .into_any_element(),
   3517                 PersonalSection::Cart => self
   3518                     .render_buyer_cart_content(runtime, cx)
   3519                     .into_any_element(),
   3520                 PersonalSection::Orders => self
   3521                     .render_buyer_orders_content(runtime, cx)
   3522                     .into_any_element(),
   3523             });
   3524 
   3525         app_split_shell(
   3526             buyer_sidebar(
   3527                 runtime,
   3528                 cx.listener(|this, _, _, cx| {
   3529                     this.select_personal_section(PersonalSection::Browse, cx)
   3530                 }),
   3531                 cx.listener(|this, _, _, cx| {
   3532                     this.select_personal_section(PersonalSection::Search, cx)
   3533                 }),
   3534                 cx.listener(|this, _, _, cx| {
   3535                     this.select_personal_section(PersonalSection::Cart, cx)
   3536                 }),
   3537                 cx.listener(|this, _, _, cx| {
   3538                     this.select_personal_section(PersonalSection::Orders, cx)
   3539                 }),
   3540                 cx,
   3541             )
   3542             .into_any_element(),
   3543             app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   3544                 .size_full()
   3545                 .child(shared_shell_header(
   3546                     runtime,
   3547                     cx.listener(|this, _, _, cx| this.switch_to_marketplace(cx)),
   3548                     cx.listener(|this, _, _, cx| this.switch_to_farmer_workspace(cx)),
   3549                     cx.listener(|this, _, _, cx| this.open_account_entry(cx)),
   3550                     cx,
   3551                 ))
   3552                 .when_some(self.buyer_workspace_notice.as_deref(), |this, notice| {
   3553                     this.child(buyer_workspace_notice_card(notice.to_owned()))
   3554                 })
   3555                 .child(
   3556                     app_scroll_panel(
   3557                         buyer_content_scroll_id(selected_personal_section),
   3558                         0.0,
   3559                         None,
   3560                         main_content,
   3561                     )
   3562                     .into_any_element(),
   3563                 )
   3564                 .into_any_element(),
   3565         )
   3566         .into_any_element()
   3567     }
   3568 
   3569     fn render_buyer_focused_view(
   3570         &mut self,
   3571         runtime: &DesktopAppRuntimeSummary,
   3572         cx: &mut Context<Self>,
   3573     ) -> Option<AnyElement> {
   3574         match self.focused_view? {
   3575             HomeFocusedView::BuyerProductDetail(section) => {
   3576                 let detail = match section {
   3577                     PersonalSection::Browse => runtime.personal_projection.browse.detail.as_ref(),
   3578                     PersonalSection::Search => runtime.personal_projection.search.detail.as_ref(),
   3579                     PersonalSection::Cart | PersonalSection::Orders => None,
   3580                 }?;
   3581                 Some(
   3582                     buyer_product_detail_card(
   3583                         detail,
   3584                         runtime
   3585                             .personal_projection
   3586                             .cart
   3587                             .cart
   3588                             .replace_confirmation
   3589                             .as_ref(),
   3590                         cx.listener(move |this, _, _, cx| {
   3591                             this.close_personal_product_detail(section, cx)
   3592                         }),
   3593                         cx.listener(move |this, _, _, cx| {
   3594                             this.decrease_personal_product_quantity(section, cx)
   3595                         }),
   3596                         cx.listener(move |this, _, _, cx| {
   3597                             this.increase_personal_product_quantity(section, cx)
   3598                         }),
   3599                         cx.listener(move |this, _, _, cx| {
   3600                             this.add_personal_product_to_cart(section, false, cx)
   3601                         }),
   3602                         cx.listener(move |this, _, _, cx| {
   3603                             this.add_personal_product_to_cart(section, true, cx)
   3604                         }),
   3605                         cx.listener(|this, _, _, cx| {
   3606                             this.clear_personal_cart_replace_confirmation(cx)
   3607                         }),
   3608                         cx,
   3609                     )
   3610                     .into_any_element(),
   3611                 )
   3612             }
   3613             HomeFocusedView::BuyerOrderReview => {
   3614                 let form = self.buyer_order_review_form.as_ref()?;
   3615                 Some(
   3616                     buyer_order_review_card(
   3617                         form,
   3618                         &runtime.personal_projection.cart.order_review,
   3619                         cx.listener(|this, _, _, cx| this.close_personal_order_review(cx)),
   3620                         cx.listener(|this, _, _, cx| this.place_personal_order(cx)),
   3621                         cx,
   3622                     )
   3623                     .into_any_element(),
   3624                 )
   3625             }
   3626             HomeFocusedView::BuyerOrderDetail(order_id) => {
   3627                 let detail = runtime
   3628                     .personal_projection
   3629                     .orders
   3630                     .detail
   3631                     .as_ref()
   3632                     .filter(|detail| detail.order_id == order_id)?;
   3633                 Some(
   3634                     buyer_order_detail_card(
   3635                         detail,
   3636                         runtime
   3637                             .personal_projection
   3638                             .cart
   3639                             .cart
   3640                             .replace_confirmation
   3641                             .as_ref(),
   3642                         cx.listener(move |this, _, _, cx| {
   3643                             this.close_personal_order_detail(order_id, cx)
   3644                         }),
   3645                         cx,
   3646                     )
   3647                     .into_any_element(),
   3648                 )
   3649             }
   3650             HomeFocusedView::FarmSetup
   3651             | HomeFocusedView::ProductEditor
   3652             | HomeFocusedView::FarmerOrderDetail(_) => None,
   3653         }
   3654     }
   3655 
   3656     fn render_buyer_browse_content(
   3657         &mut self,
   3658         runtime: &DesktopAppRuntimeSummary,
   3659         cx: &mut Context<Self>,
   3660     ) -> AnyElement {
   3661         let listings = &runtime.personal_projection.browse.listings.rows;
   3662         let selected_product_id = runtime
   3663             .personal_projection
   3664             .browse
   3665             .detail
   3666             .as_ref()
   3667             .map(|detail| detail.listing.product_id);
   3668 
   3669         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   3670             .w_full()
   3671             .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
   3672             .mx_auto()
   3673             .child(buyer_workspace_title_block(
   3674                 AppTextKey::HomeNavBrowse,
   3675                 AppTextKey::PersonalBrowsePlaceholderBody,
   3676             ))
   3677             .child(if listings.is_empty() {
   3678                 home_empty_state_card(
   3679                     AppTextKey::PersonalBrowseEmptyTitle,
   3680                     AppTextKey::PersonalBrowseEmptyBody,
   3681                 )
   3682                 .into_any_element()
   3683             } else {
   3684                 app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   3685                     .w_full()
   3686                     .child(buyer_listings_feed(
   3687                         PersonalSection::Browse,
   3688                         listings,
   3689                         selected_product_id,
   3690                         cx,
   3691                     ))
   3692                     .into_any_element()
   3693             })
   3694             .into_any_element()
   3695     }
   3696 
   3697     fn render_buyer_search_content(
   3698         &mut self,
   3699         runtime: &DesktopAppRuntimeSummary,
   3700         cx: &mut Context<Self>,
   3701     ) -> AnyElement {
   3702         let query = &runtime.personal_projection.search.query;
   3703         let listings = &runtime.personal_projection.search.listings.rows;
   3704         let selected_product_id = runtime
   3705             .personal_projection
   3706             .search
   3707             .detail
   3708             .as_ref()
   3709             .map(|detail| detail.listing.product_id);
   3710 
   3711         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   3712             .w_full()
   3713             .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
   3714             .mx_auto()
   3715             .child(buyer_workspace_title_block(
   3716                 AppTextKey::HomeNavSearch,
   3717                 AppTextKey::PersonalSearchPlaceholderBody,
   3718             ))
   3719             .child(
   3720                 home_card(
   3721                     app_shared_text(AppTextKey::PersonalSearchFiltersTitle),
   3722                     div()
   3723                         .w_full()
   3724                         .flex()
   3725                         .flex_col()
   3726                         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
   3727                         .when_some(self.personal_search.as_ref(), |this, personal_search| {
   3728                             this.child(
   3729                                 app_text_input(&personal_search.input, false)
   3730                                     .cleanable(true)
   3731                                     .w_full(),
   3732                             )
   3733                         })
   3734                         .child(
   3735                             app_cluster(8.0)
   3736                                 .child(choice_button(
   3737                                     "personal-search-pickup",
   3738                                     app_shared_text(AppTextKey::HomeFarmSetupOrderMethodPickup),
   3739                                     query.fulfillment_methods.contains(&FarmOrderMethod::Pickup),
   3740                                     cx.listener(|this, _, _, cx| {
   3741                                         let enabled = !this
   3742                                             .runtime
   3743                                             .summary()
   3744                                             .personal_projection
   3745                                             .search
   3746                                             .query
   3747                                             .fulfillment_methods
   3748                                             .contains(&FarmOrderMethod::Pickup);
   3749                                         this.toggle_personal_search_fulfillment_method(
   3750                                             FarmOrderMethod::Pickup,
   3751                                             enabled,
   3752                                             cx,
   3753                                         )
   3754                                     }),
   3755                                     cx,
   3756                                 ))
   3757                                 .child(choice_button(
   3758                                     "personal-search-delivery",
   3759                                     app_shared_text(AppTextKey::HomeFarmSetupOrderMethodDelivery),
   3760                                     query
   3761                                         .fulfillment_methods
   3762                                         .contains(&FarmOrderMethod::Delivery),
   3763                                     cx.listener(|this, _, _, cx| {
   3764                                         let enabled = !this
   3765                                             .runtime
   3766                                             .summary()
   3767                                             .personal_projection
   3768                                             .search
   3769                                             .query
   3770                                             .fulfillment_methods
   3771                                             .contains(&FarmOrderMethod::Delivery);
   3772                                         this.toggle_personal_search_fulfillment_method(
   3773                                             FarmOrderMethod::Delivery,
   3774                                             enabled,
   3775                                             cx,
   3776                                         )
   3777                                     }),
   3778                                     cx,
   3779                                 ))
   3780                                 .child(choice_button(
   3781                                     "personal-search-shipping",
   3782                                     app_shared_text(AppTextKey::HomeFarmSetupOrderMethodShipping),
   3783                                     query
   3784                                         .fulfillment_methods
   3785                                         .contains(&FarmOrderMethod::Shipping),
   3786                                     cx.listener(|this, _, _, cx| {
   3787                                         let enabled = !this
   3788                                             .runtime
   3789                                             .summary()
   3790                                             .personal_projection
   3791                                             .search
   3792                                             .query
   3793                                             .fulfillment_methods
   3794                                             .contains(&FarmOrderMethod::Shipping);
   3795                                         this.toggle_personal_search_fulfillment_method(
   3796                                             FarmOrderMethod::Shipping,
   3797                                             enabled,
   3798                                             cx,
   3799                                         )
   3800                                     }),
   3801                                     cx,
   3802                                 )),
   3803                         ),
   3804                 )
   3805                 .into_any_element(),
   3806             )
   3807             .child(if listings.is_empty() {
   3808                 home_empty_state_card(
   3809                     AppTextKey::PersonalSearchEmptyTitle,
   3810                     AppTextKey::PersonalSearchEmptyBody,
   3811                 )
   3812                 .into_any_element()
   3813             } else {
   3814                 app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   3815                     .w_full()
   3816                     .child(buyer_listings_feed(
   3817                         PersonalSection::Search,
   3818                         listings,
   3819                         selected_product_id,
   3820                         cx,
   3821                     ))
   3822                     .into_any_element()
   3823             })
   3824             .into_any_element()
   3825     }
   3826 
   3827     fn render_buyer_cart_content(
   3828         &mut self,
   3829         runtime: &DesktopAppRuntimeSummary,
   3830         cx: &mut Context<Self>,
   3831     ) -> AnyElement {
   3832         let cart = &runtime.personal_projection.cart.cart;
   3833         let order_review = &runtime.personal_projection.cart.order_review;
   3834 
   3835         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   3836             .w_full()
   3837             .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
   3838             .mx_auto()
   3839             .child(buyer_workspace_title_block(
   3840                 AppTextKey::HomeNavCart,
   3841                 AppTextKey::PersonalCartSurfaceBody,
   3842             ))
   3843             .child(if cart.lines.is_empty() {
   3844                 app_surface_card(home_body_text(app_shared_text(
   3845                     AppTextKey::PersonalCartPlaceholderBody,
   3846                 )))
   3847                 .into_any_element()
   3848             } else {
   3849                 app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   3850                     .w_full()
   3851                     .child(buyer_cart_card(
   3852                         cart,
   3853                         &order_review.summary,
   3854                         self.buyer_order_review_form.is_some(),
   3855                         cx,
   3856                     ))
   3857                     .into_any_element()
   3858             })
   3859             .into_any_element()
   3860     }
   3861 
   3862     fn render_buyer_orders_content(
   3863         &mut self,
   3864         runtime: &DesktopAppRuntimeSummary,
   3865         cx: &mut Context<Self>,
   3866     ) -> AnyElement {
   3867         let orders = &runtime.personal_projection.orders;
   3868         let selected_order_id = orders.detail.as_ref().map(|detail| detail.order_id);
   3869 
   3870         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   3871             .w_full()
   3872             .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
   3873             .mx_auto()
   3874             .child(buyer_workspace_title_block(
   3875                 AppTextKey::HomeNavOrders,
   3876                 AppTextKey::PersonalOrdersSurfaceBody,
   3877             ))
   3878             .when(buyer_orders_retry_action_visible(orders), |this| {
   3879                 this.child(buyer_orders_retry_card(cx))
   3880             })
   3881             .child(if orders.list.rows.is_empty() {
   3882                 home_empty_state_card(
   3883                     AppTextKey::PersonalOrdersEmptyTitle,
   3884                     AppTextKey::PersonalOrdersEmptyBody,
   3885                 )
   3886                 .into_any_element()
   3887             } else {
   3888                 app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   3889                     .w_full()
   3890                     .child(buyer_orders_list_card(
   3891                         &orders.list.rows,
   3892                         selected_order_id,
   3893                         cx,
   3894                     ))
   3895                     .into_any_element()
   3896             })
   3897             .into_any_element()
   3898     }
   3899 
   3900     fn render_farmer_workspace(
   3901         &mut self,
   3902         runtime: &DesktopAppRuntimeSummary,
   3903         cx: &mut Context<Self>,
   3904     ) -> AnyElement {
   3905         let selected_farmer_section = selected_farmer_section(runtime);
   3906         let main_content = self
   3907             .render_farmer_focused_view(runtime, cx)
   3908             .unwrap_or_else(|| match selected_farmer_section {
   3909                 FarmerSection::Products if farmer_products_available(runtime) => {
   3910                     self.render_products_content(runtime, cx)
   3911                 }
   3912                 FarmerSection::Orders if farmer_products_available(runtime) => {
   3913                     self.render_orders_content(runtime, cx)
   3914                 }
   3915                 FarmerSection::PackDay if farmer_pack_day_available(runtime) => {
   3916                     self.render_pack_day_content(runtime, cx)
   3917                 }
   3918                 FarmerSection::Today
   3919                 | FarmerSection::Products
   3920                 | FarmerSection::Orders
   3921                 | FarmerSection::PackDay
   3922                 | FarmerSection::Farm => self.render_today_content(runtime, cx),
   3923             });
   3924 
   3925         app_split_shell(
   3926             home_sidebar(
   3927                 runtime,
   3928                 cx.listener(|this, _, _, cx| this.select_farmer_section(FarmerSection::Today, cx)),
   3929                 cx.listener(|this, _, _, cx| {
   3930                     this.select_farmer_section(FarmerSection::Products, cx)
   3931                 }),
   3932                 cx.listener(|this, _, _, cx| this.open_orders(cx)),
   3933                 cx.listener(|this, _, _, cx| this.open_pack_day(None, cx)),
   3934                 cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::Profile, cx)),
   3935                 cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::FarmDetails, cx)),
   3936                 cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::Preferences, cx)),
   3937                 cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::Settings, cx)),
   3938                 cx,
   3939             )
   3940             .into_any_element(),
   3941             app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   3942                 .size_full()
   3943                 .child(shared_shell_header(
   3944                     runtime,
   3945                     cx.listener(|this, _, _, cx| this.switch_to_marketplace(cx)),
   3946                     cx.listener(|this, _, _, cx| this.switch_to_farmer_workspace(cx)),
   3947                     cx.listener(|this, _, _, cx| this.open_account_entry(cx)),
   3948                     cx,
   3949                 ))
   3950                 .when_some(presented_farmer_reminder(runtime), |this, reminder| {
   3951                     this.child(
   3952                         div()
   3953                             .w_full()
   3954                             .px(px(APP_UI_THEME.shells.home_window_padding_px))
   3955                             .child(
   3956                                 div()
   3957                                     .w_full()
   3958                                     .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
   3959                                     .mx_auto()
   3960                                     .child(self.render_presented_reminder_banner(reminder, cx)),
   3961                             ),
   3962                     )
   3963                 })
   3964                 .child(
   3965                     app_scroll_panel(
   3966                         home_content_scroll_id(selected_farmer_section),
   3967                         0.0,
   3968                         None,
   3969                         main_content,
   3970                     )
   3971                     .into_any_element(),
   3972                 )
   3973                 .into_any_element(),
   3974         )
   3975         .into_any_element()
   3976     }
   3977 
   3978     fn render_farmer_focused_view(
   3979         &mut self,
   3980         runtime: &DesktopAppRuntimeSummary,
   3981         cx: &mut Context<Self>,
   3982     ) -> Option<AnyElement> {
   3983         match self.focused_view? {
   3984             HomeFocusedView::FarmSetup => {
   3985                 let form = self.farm_setup_form.as_ref()?;
   3986                 Some(
   3987                     home_farm_setup_form_card(
   3988                         form,
   3989                         cx.listener(|this, checked: &bool, _, cx| {
   3990                             this.toggle_farm_order_method(FarmOrderMethod::Pickup, *checked, cx)
   3991                         }),
   3992                         cx.listener(|this, checked: &bool, _, cx| {
   3993                             this.toggle_farm_order_method(FarmOrderMethod::Delivery, *checked, cx)
   3994                         }),
   3995                         cx.listener(|this, checked: &bool, _, cx| {
   3996                             this.toggle_farm_order_method(FarmOrderMethod::Shipping, *checked, cx)
   3997                         }),
   3998                         cx.listener(|this, _, _, cx| this.finish_farm_setup(cx)),
   3999                         cx,
   4000                     )
   4001                     .into_any_element(),
   4002                 )
   4003             }
   4004             HomeFocusedView::ProductEditor => {
   4005                 let form = self.product_editor_form.as_ref()?;
   4006                 Some(products_editor_surface(form, runtime, cx).into_any_element())
   4007             }
   4008             HomeFocusedView::FarmerOrderDetail(order_id) => {
   4009                 let detail = runtime
   4010                     .orders_projection
   4011                     .detail
   4012                     .as_ref()
   4013                     .filter(|detail| detail.order_id == order_id)?;
   4014                 Some(self.render_order_detail_card(
   4015                     detail,
   4016                     cx.listener(move |this, _, _, cx| this.close_order_detail(order_id, cx)),
   4017                     cx,
   4018                 ))
   4019             }
   4020             HomeFocusedView::BuyerProductDetail(_)
   4021             | HomeFocusedView::BuyerOrderReview
   4022             | HomeFocusedView::BuyerOrderDetail(_) => None,
   4023         }
   4024     }
   4025 
   4026     fn render_products_content(
   4027         &mut self,
   4028         runtime: &DesktopAppRuntimeSummary,
   4029         cx: &mut Context<Self>,
   4030     ) -> AnyElement {
   4031         let projection = &runtime.products_projection;
   4032         let summary = &projection.list.summary;
   4033 
   4034         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   4035             .w_full()
   4036             .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
   4037             .mx_auto()
   4038             .child(products_title_row(
   4039                 runtime,
   4040                 action_button_primary(
   4041                     "products-add-product",
   4042                     app_shared_text(AppTextKey::ProductsAddAction),
   4043                     cx.listener(|this, _, _, cx| this.open_new_product_editor(cx)),
   4044                     cx,
   4045                 )
   4046                 .into_any_element(),
   4047             ))
   4048             .child(
   4049                 div()
   4050                     .w_full()
   4051                     .flex()
   4052                     .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
   4053                     .child(home_summary_metric(
   4054                         AppTextKey::ProductsSummaryTotal,
   4055                         summary.total_products,
   4056                     ))
   4057                     .child(home_summary_metric(
   4058                         AppTextKey::ProductsSummaryLive,
   4059                         summary.live_products,
   4060                     ))
   4061                     .child(home_summary_metric(
   4062                         AppTextKey::ProductsSummaryNeedAttention,
   4063                         summary.need_attention_products,
   4064                     ))
   4065                     .child(home_summary_metric(
   4066                         AppTextKey::ProductsSummaryDrafts,
   4067                         summary.draft_products,
   4068                     )),
   4069             )
   4070             .child(products_controls_card(
   4071                 runtime,
   4072                 self.products_search.as_ref(),
   4073                 cx.listener(|this, _, _, cx| this.select_products_filter(ProductsFilter::All, cx)),
   4074                 cx.listener(|this, _, _, cx| this.select_products_filter(ProductsFilter::Live, cx)),
   4075                 cx.listener(|this, _, _, cx| {
   4076                     this.select_products_filter(ProductsFilter::Drafts, cx)
   4077                 }),
   4078                 cx.listener(|this, _, _, cx| {
   4079                     this.select_products_filter(ProductsFilter::NeedAttention, cx)
   4080                 }),
   4081                 cx.listener(|this, _, _, cx| {
   4082                     this.select_products_filter(ProductsFilter::Paused, cx)
   4083                 }),
   4084                 cx.listener(|this, _, _, cx| {
   4085                     this.select_products_filter(ProductsFilter::Archived, cx)
   4086                 }),
   4087                 cx.listener(|this, _, _, cx| this.select_products_sort(ProductsSort::Updated, cx)),
   4088                 cx.listener(|this, _, _, cx| this.select_products_sort(ProductsSort::Name, cx)),
   4089                 cx.listener(|this, _, _, cx| {
   4090                     this.select_products_sort(ProductsSort::Availability, cx)
   4091                 }),
   4092                 cx.listener(|this, _, _, cx| this.select_products_sort(ProductsSort::Stock, cx)),
   4093                 cx.listener(|this, _, _, cx| this.select_products_sort(ProductsSort::Price, cx)),
   4094                 cx,
   4095             ))
   4096             .child(if projection.list.is_empty() {
   4097                 products_empty_state_card(projection.query.filter).into_any_element()
   4098             } else {
   4099                 self.render_products_table_card(&projection.list.rows, cx)
   4100             })
   4101             .into_any_element()
   4102     }
   4103 
   4104     fn render_orders_content(
   4105         &mut self,
   4106         runtime: &DesktopAppRuntimeSummary,
   4107         cx: &mut Context<Self>,
   4108     ) -> AnyElement {
   4109         let projection = &runtime.orders_projection;
   4110         let summary = &projection.list.summary;
   4111 
   4112         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   4113             .w_full()
   4114             .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
   4115             .mx_auto()
   4116             .child(app_text_value(app_shared_text(AppTextKey::OrdersTitle)))
   4117             .child(
   4118                 div()
   4119                     .w_full()
   4120                     .flex()
   4121                     .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
   4122                     .child(home_summary_metric(
   4123                         AppTextKey::OrdersSummaryTotal,
   4124                         summary.total_orders,
   4125                     ))
   4126                     .child(home_summary_metric(
   4127                         AppTextKey::OrdersStatusNeedsAction,
   4128                         summary.needs_action_orders,
   4129                     ))
   4130                     .child(home_summary_metric(
   4131                         AppTextKey::OrdersStatusScheduled,
   4132                         summary.scheduled_orders,
   4133                     ))
   4134                     .child(home_summary_metric(
   4135                         AppTextKey::OrdersStatusInHandoff,
   4136                         summary.packed_orders,
   4137                     )),
   4138             )
   4139             .child(home_card(
   4140                 app_shared_text(AppTextKey::OrdersFiltersTitle),
   4141                 app_cluster(APP_UI_THEME.foundation.spacing.tight_px)
   4142                     .child(choice_button(
   4143                         "orders-filter-all",
   4144                         app_shared_text(AppTextKey::OrdersFilterAll),
   4145                         projection.query.filter == OrdersFilter::All,
   4146                         cx.listener(|this, _, _, cx| {
   4147                             this.select_orders_filter(OrdersFilter::All, cx)
   4148                         }),
   4149                         cx,
   4150                     ))
   4151                     .child(choice_button(
   4152                         "orders-filter-needs-action",
   4153                         app_shared_text(AppTextKey::OrdersStatusNeedsAction),
   4154                         projection.query.filter == OrdersFilter::NeedsAction,
   4155                         cx.listener(|this, _, _, cx| {
   4156                             this.select_orders_filter(OrdersFilter::NeedsAction, cx)
   4157                         }),
   4158                         cx,
   4159                     ))
   4160                     .child(choice_button(
   4161                         "orders-filter-scheduled",
   4162                         app_shared_text(AppTextKey::OrdersStatusScheduled),
   4163                         projection.query.filter == OrdersFilter::Scheduled,
   4164                         cx.listener(|this, _, _, cx| {
   4165                             this.select_orders_filter(OrdersFilter::Scheduled, cx)
   4166                         }),
   4167                         cx,
   4168                     ))
   4169                     .child(choice_button(
   4170                         "orders-filter-packed",
   4171                         app_shared_text(AppTextKey::OrdersStatusInHandoff),
   4172                         projection.query.filter == OrdersFilter::Packed,
   4173                         cx.listener(|this, _, _, cx| {
   4174                             this.select_orders_filter(OrdersFilter::Packed, cx)
   4175                         }),
   4176                         cx,
   4177                     ))
   4178                     .child(choice_button(
   4179                         "orders-filter-completed",
   4180                         app_shared_text(AppTextKey::OrdersStatusCompleted),
   4181                         projection.query.filter == OrdersFilter::Completed,
   4182                         cx.listener(|this, _, _, cx| {
   4183                             this.select_orders_filter(OrdersFilter::Completed, cx)
   4184                         }),
   4185                         cx,
   4186                     )),
   4187             ))
   4188             .when(!projection.reminders.is_empty(), |this| {
   4189                 this.child(self.render_reminder_feed_card(
   4190                     "orders-reminders",
   4191                     AppTextKey::OrdersRemindersTitle,
   4192                     &projection.reminders.items,
   4193                     cx,
   4194                 ))
   4195             })
   4196             .child(self.render_orders_reminder_log_card(&runtime.reminder_log))
   4197             .child(if projection.list.is_empty() {
   4198                 orders_empty_state_card(projection.query.filter).into_any_element()
   4199             } else {
   4200                 self.render_orders_table_card(
   4201                     &projection.list.rows,
   4202                     projection.detail.as_ref().map(|detail| detail.order_id),
   4203                     cx,
   4204                 )
   4205             })
   4206             .into_any_element()
   4207     }
   4208 
   4209     fn render_presented_reminder_banner(
   4210         &mut self,
   4211         reminder: &ReminderDeadlineProjection,
   4212         cx: &mut Context<Self>,
   4213     ) -> AnyElement {
   4214         let primary_action = self.render_presented_reminder_primary_action(reminder, cx);
   4215 
   4216         home_card(
   4217             app_shared_text(AppTextKey::ReminderPresentationTitle),
   4218             app_stack_v(APP_UI_THEME.foundation.spacing.medium_px)
   4219                 .w_full()
   4220                 .child(
   4221                     div()
   4222                         .w_full()
   4223                         .min_w_0()
   4224                         .flex()
   4225                         .items_start()
   4226                         .justify_between()
   4227                         .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
   4228                         .child(
   4229                             div()
   4230                                 .flex_1()
   4231                                 .min_w_0()
   4232                                 .flex()
   4233                                 .items_center()
   4234                                 .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
   4235                                 .child(status_indicator(reminder_urgency_color(reminder.urgency)))
   4236                                 .child(
   4237                                     div()
   4238                                         .flex_1()
   4239                                         .min_w_0()
   4240                                         .text_size(px(APP_UI_THEME
   4241                                             .foundation
   4242                                             .typography
   4243                                             .body_text_px))
   4244                                         .font_weight(gpui::FontWeight::SEMIBOLD)
   4245                                         .line_height(relative(1.2))
   4246                                         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   4247                                         .child(reminder.title.clone()),
   4248                                 ),
   4249                         )
   4250                         .child(
   4251                             app_cluster(APP_UI_THEME.foundation.spacing.tight_px)
   4252                                 .child(reminder_urgency_badge(reminder.urgency))
   4253                                 .child(reminder_delivery_state_badge(reminder.delivery_state)),
   4254                         ),
   4255                 )
   4256                 .when(!reminder.detail.trim().is_empty(), |this| {
   4257                     this.child(home_body_text(reminder.detail.clone()))
   4258                 })
   4259                 .child(
   4260                     div()
   4261                         .w_full()
   4262                         .min_w_0()
   4263                         .flex()
   4264                         .items_center()
   4265                         .justify_between()
   4266                         .gap(px(APP_UI_THEME.foundation.spacing.medium_px))
   4267                         .child(
   4268                             div()
   4269                                 .min_w_0()
   4270                                 .text_size(px(APP_UI_THEME
   4271                                     .foundation
   4272                                     .typography
   4273                                     .utility_title_text_px))
   4274                                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   4275                                 .child(reminder_deadline_text(reminder)),
   4276                         )
   4277                         .child(
   4278                             div()
   4279                                 .flex()
   4280                                 .items_center()
   4281                                 .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
   4282                                 .when_some(primary_action, |this, action| this.child(action))
   4283                                 .child(text_button(
   4284                                     "reminder-banner-dismiss",
   4285                                     app_shared_text(AppTextKey::ReminderPresentationDismissAction),
   4286                                     cx.listener({
   4287                                         let reminder_id = reminder.reminder_id;
   4288                                         move |this, _, _, cx| {
   4289                                             this.dismiss_presented_reminder(reminder_id, cx)
   4290                                         }
   4291                                     }),
   4292                                     cx,
   4293                                 )),
   4294                         ),
   4295                 ),
   4296         )
   4297         .into_any_element()
   4298     }
   4299 
   4300     fn render_presented_reminder_primary_action(
   4301         &mut self,
   4302         reminder: &ReminderDeadlineProjection,
   4303         cx: &mut Context<Self>,
   4304     ) -> Option<AnyElement> {
   4305         let label = reminder.action_label.clone()?;
   4306 
   4307         match reminder_action_target(reminder) {
   4308             Some(ReminderActionTarget::OrderDetail(order_id)) => Some(
   4309                 action_button_primary(
   4310                     "reminder-banner-action",
   4311                     SharedString::from(label),
   4312                     cx.listener({
   4313                         let reminder_id = reminder.reminder_id;
   4314                         move |this, _, _, cx| {
   4315                             this.open_presented_order_reminder(reminder_id, order_id, cx)
   4316                         }
   4317                     }),
   4318                     cx,
   4319                 )
   4320                 .into_any_element(),
   4321             ),
   4322             Some(ReminderActionTarget::PackDay(fulfillment_window_id)) => Some(
   4323                 action_button_primary(
   4324                     "reminder-banner-action",
   4325                     SharedString::from(label),
   4326                     cx.listener({
   4327                         let reminder_id = reminder.reminder_id;
   4328                         move |this, _, _, cx| {
   4329                             this.open_presented_pack_day_reminder(
   4330                                 reminder_id,
   4331                                 fulfillment_window_id,
   4332                                 cx,
   4333                             )
   4334                         }
   4335                     }),
   4336                     cx,
   4337                 )
   4338                 .into_any_element(),
   4339             ),
   4340             None if reminder.surface == ReminderSurface::Orders => Some(
   4341                 action_button_primary(
   4342                     "reminder-banner-action",
   4343                     SharedString::from(label),
   4344                     cx.listener({
   4345                         let reminder_id = reminder.reminder_id;
   4346                         move |this, _, _, cx| this.open_presented_orders_reminder(reminder_id, cx)
   4347                     }),
   4348                     cx,
   4349                 )
   4350                 .into_any_element(),
   4351             ),
   4352             None => None,
   4353         }
   4354     }
   4355 
   4356     fn render_pack_day_content(
   4357         &mut self,
   4358         runtime: &DesktopAppRuntimeSummary,
   4359         cx: &mut Context<Self>,
   4360     ) -> AnyElement {
   4361         let projection = &runtime.pack_day_projection.projection;
   4362         let Some(fulfillment_window) = projection.fulfillment_window.as_ref() else {
   4363             return home_empty_state_card(
   4364                 AppTextKey::PackDayEmptyTitle,
   4365                 AppTextKey::PackDayEmptyBody,
   4366             )
   4367             .into_any_element();
   4368         };
   4369 
   4370         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   4371             .w_full()
   4372             .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
   4373             .mx_auto()
   4374             .child(pack_day_title_row(runtime))
   4375             .child(pack_day_export_card(
   4376                 runtime,
   4377                 cx.listener(|this, _, _, cx| this.export_pack_day(cx)),
   4378                 cx.listener(|this, _, window, cx| {
   4379                     this.start_pack_day_host_handoff(
   4380                         PackDayHostHandoffKind::RevealBundle,
   4381                         window,
   4382                         cx,
   4383                     )
   4384                 }),
   4385                 cx.listener(|this, _, window, cx| {
   4386                     this.start_pack_day_host_handoff(
   4387                         PackDayHostHandoffKind::OpenPackSheet,
   4388                         window,
   4389                         cx,
   4390                     )
   4391                 }),
   4392                 cx.listener(|this, _, window, cx| {
   4393                     this.start_pack_day_host_handoff(
   4394                         PackDayHostHandoffKind::OpenPickupRoster,
   4395                         window,
   4396                         cx,
   4397                     )
   4398                 }),
   4399                 cx.listener(|this, _, window, cx| {
   4400                     this.start_pack_day_host_handoff(
   4401                         PackDayHostHandoffKind::OpenCustomerLabels,
   4402                         window,
   4403                         cx,
   4404                     )
   4405                 }),
   4406                 cx.listener(|this, _, window, cx| this.start_pack_day_batch_print(window, cx)),
   4407                 cx.listener(|this, _, window, cx| {
   4408                     this.start_pack_day_print(PackDayPrintKind::PrintPackSheet, window, cx)
   4409                 }),
   4410                 cx.listener(|this, _, window, cx| {
   4411                     this.start_pack_day_print(PackDayPrintKind::PrintPickupRoster, window, cx)
   4412                 }),
   4413                 cx.listener(|this, _, window, cx| {
   4414                     this.start_pack_day_print(PackDayPrintKind::PrintCustomerLabels, window, cx)
   4415                 }),
   4416                 cx,
   4417             ))
   4418             .when(!projection.reminders.is_empty(), |this| {
   4419                 this.child(self.render_reminder_feed_card(
   4420                     "pack-day-reminders",
   4421                     AppTextKey::PackDayRemindersTitle,
   4422                     &projection.reminders.items,
   4423                     cx,
   4424                 ))
   4425             })
   4426             .child(pack_day_window_summary_card(fulfillment_window))
   4427             .when(!projection.totals_by_product.is_empty(), |this| {
   4428                 this.child(pack_day_totals_card(&projection.totals_by_product))
   4429             })
   4430             .when(!projection.pack_list.is_empty(), |this| {
   4431                 this.child(pack_day_pack_list_card(&projection.pack_list))
   4432             })
   4433             .when(!projection.pickup_roster.is_empty(), |this| {
   4434                 this.child(pack_day_pickup_roster_card(&projection.pickup_roster))
   4435             })
   4436             .when(projection.is_empty(), |this| {
   4437                 this.child(home_empty_state_card(
   4438                     AppTextKey::PackDayEmptyTitle,
   4439                     AppTextKey::PackDayEmptyBody,
   4440                 ))
   4441             })
   4442             .into_any_element()
   4443     }
   4444 
   4445     fn render_today_reminder_strip(
   4446         &mut self,
   4447         reminders: &[ReminderDeadlineProjection],
   4448         cx: &mut Context<Self>,
   4449     ) -> AnyElement {
   4450         app_surface_card(
   4451             app_stack_v(APP_UI_THEME.foundation.spacing.tight_px)
   4452                 .w_full()
   4453                 .child(app_text_label(app_shared_text(
   4454                     AppTextKey::HomeTodayRemindersTitle,
   4455                 )))
   4456                 .child(
   4457                     app_cluster(APP_UI_THEME.foundation.spacing.tight_px)
   4458                         .w_full()
   4459                         .items_start()
   4460                         .children(
   4461                             reminders
   4462                                 .iter()
   4463                                 .enumerate()
   4464                                 .map(|(index, reminder)| {
   4465                                     self.render_today_reminder_chip(index, reminder, cx)
   4466                                 })
   4467                                 .collect::<Vec<_>>(),
   4468                         ),
   4469                 ),
   4470         )
   4471         .into_any_element()
   4472     }
   4473 
   4474     fn render_today_reminder_chip(
   4475         &mut self,
   4476         index: usize,
   4477         reminder: &ReminderDeadlineProjection,
   4478         cx: &mut Context<Self>,
   4479     ) -> AnyElement {
   4480         let content = div()
   4481             .w_full()
   4482             .min_w_0()
   4483             .p(px(APP_UI_THEME.shells.home_card_padding_px))
   4484             .flex()
   4485             .flex_col()
   4486             .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
   4487             .child(
   4488                 div()
   4489                     .w_full()
   4490                     .min_w_0()
   4491                     .flex()
   4492                     .items_start()
   4493                     .justify_between()
   4494                     .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
   4495                     .child(
   4496                         div()
   4497                             .flex_1()
   4498                             .min_w_0()
   4499                             .flex()
   4500                             .items_center()
   4501                             .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
   4502                             .child(status_indicator(reminder_urgency_color(reminder.urgency)))
   4503                             .child(
   4504                                 div()
   4505                                     .flex_1()
   4506                                     .min_w_0()
   4507                                     .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   4508                                     .font_weight(gpui::FontWeight::SEMIBOLD)
   4509                                     .line_height(relative(1.2))
   4510                                     .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   4511                                     .child(reminder.title.clone()),
   4512                             ),
   4513                     )
   4514                     .child(reminder_urgency_badge(reminder.urgency)),
   4515             )
   4516             .child(
   4517                 div()
   4518                     .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
   4519                     .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   4520                     .child(reminder_deadline_text(reminder)),
   4521             );
   4522         let shell = div().min_w(px(244.0)).max_w(px(296.0)).flex_1();
   4523 
   4524         match reminder_action_target(reminder) {
   4525             Some(ReminderActionTarget::OrderDetail(order_id)) => shell
   4526                 .child(app_button_card(
   4527                     ("today-reminder-chip", index),
   4528                     false,
   4529                     cx.listener(move |this, _, _, cx| this.open_order_detail(order_id, cx)),
   4530                     cx,
   4531                     content,
   4532                 ))
   4533                 .into_any_element(),
   4534             Some(ReminderActionTarget::PackDay(fulfillment_window_id)) => shell
   4535                 .child(app_button_card(
   4536                     ("today-reminder-chip", index),
   4537                     false,
   4538                     cx.listener(move |this, _, _, cx| {
   4539                         this.open_pack_day(Some(fulfillment_window_id), cx)
   4540                     }),
   4541                     cx,
   4542                     content,
   4543                 ))
   4544                 .into_any_element(),
   4545             None => shell
   4546                 .child(
   4547                     div()
   4548                         .w_full()
   4549                         .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background))
   4550                         .rounded(px(APP_UI_THEME.foundation.radii.medium_px))
   4551                         .child(content),
   4552                 )
   4553                 .into_any_element(),
   4554         }
   4555     }
   4556 
   4557     fn render_reminder_feed_card(
   4558         &mut self,
   4559         scope: &'static str,
   4560         title_key: AppTextKey,
   4561         reminders: &[ReminderDeadlineProjection],
   4562         cx: &mut Context<Self>,
   4563     ) -> AnyElement {
   4564         let mut rows = Vec::with_capacity(reminders.len().saturating_mul(2));
   4565         for (index, reminder) in reminders.iter().enumerate() {
   4566             rows.push(self.render_reminder_feed_row(scope, index, reminder, cx));
   4567             if index + 1 < reminders.len() {
   4568                 rows.push(section_divider().into_any_element());
   4569             }
   4570         }
   4571 
   4572         home_card(
   4573             app_shared_text(title_key),
   4574             div()
   4575                 .w_full()
   4576                 .flex()
   4577                 .flex_col()
   4578                 .gap(px(APP_UI_THEME.foundation.spacing.medium_px))
   4579                 .children(rows),
   4580         )
   4581         .into_any_element()
   4582     }
   4583 
   4584     fn render_orders_reminder_log_card(&self, reminder_log: &ReminderLogProjection) -> AnyElement {
   4585         let body = if reminder_log.entries.is_empty() {
   4586             home_body_text(app_shared_text(AppTextKey::OrdersReminderLogEmptyBody))
   4587                 .into_any_element()
   4588         } else {
   4589             let mut rows = Vec::with_capacity(reminder_log.entries.len().saturating_mul(2));
   4590             for (index, entry) in reminder_log.entries.iter().enumerate() {
   4591                 rows.push(self.render_orders_reminder_log_row(entry));
   4592                 if index + 1 < reminder_log.entries.len() {
   4593                     rows.push(section_divider().into_any_element());
   4594                 }
   4595             }
   4596 
   4597             div()
   4598                 .w_full()
   4599                 .flex()
   4600                 .flex_col()
   4601                 .gap(px(APP_UI_THEME.foundation.spacing.medium_px))
   4602                 .children(rows)
   4603                 .into_any_element()
   4604         };
   4605 
   4606         home_card(app_shared_text(AppTextKey::OrdersReminderLogTitle), body).into_any_element()
   4607     }
   4608 
   4609     fn render_orders_reminder_log_row(&self, entry: &ReminderLogEntryProjection) -> AnyElement {
   4610         app_stack_v(APP_UI_THEME.foundation.spacing.tight_px)
   4611             .w_full()
   4612             .child(
   4613                 div()
   4614                     .w_full()
   4615                     .min_w_0()
   4616                     .flex()
   4617                     .items_start()
   4618                     .justify_between()
   4619                     .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
   4620                     .child(
   4621                         div()
   4622                             .flex_1()
   4623                             .min_w_0()
   4624                             .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   4625                             .font_weight(gpui::FontWeight::SEMIBOLD)
   4626                             .line_height(relative(1.2))
   4627                             .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   4628                             .child(entry.title.clone()),
   4629                     )
   4630                     .child(reminder_delivery_state_badge(entry.delivery_state)),
   4631             )
   4632             .when_some(
   4633                 entry
   4634                     .detail
   4635                     .as_ref()
   4636                     .map(|detail| detail.trim())
   4637                     .filter(|detail| !detail.is_empty()),
   4638                 |this, detail| this.child(home_body_text(detail.to_owned())),
   4639             )
   4640             .child(
   4641                 div()
   4642                     .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
   4643                     .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   4644                     .child(entry.recorded_at.clone()),
   4645             )
   4646             .into_any_element()
   4647     }
   4648 
   4649     fn render_reminder_feed_row(
   4650         &mut self,
   4651         scope: &'static str,
   4652         index: usize,
   4653         reminder: &ReminderDeadlineProjection,
   4654         cx: &mut Context<Self>,
   4655     ) -> AnyElement {
   4656         let action = self.render_reminder_action(scope, index, reminder, cx);
   4657 
   4658         app_stack_v(APP_UI_THEME.foundation.spacing.tight_px)
   4659             .w_full()
   4660             .child(
   4661                 div()
   4662                     .w_full()
   4663                     .min_w_0()
   4664                     .flex()
   4665                     .items_start()
   4666                     .justify_between()
   4667                     .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
   4668                     .child(
   4669                         div()
   4670                             .flex_1()
   4671                             .min_w_0()
   4672                             .flex()
   4673                             .items_center()
   4674                             .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
   4675                             .child(status_indicator(reminder_urgency_color(reminder.urgency)))
   4676                             .child(
   4677                                 div()
   4678                                     .flex_1()
   4679                                     .min_w_0()
   4680                                     .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   4681                                     .font_weight(gpui::FontWeight::SEMIBOLD)
   4682                                     .line_height(relative(1.2))
   4683                                     .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   4684                                     .child(reminder.title.clone()),
   4685                             ),
   4686                     )
   4687                     .child(reminder_urgency_badge(reminder.urgency)),
   4688             )
   4689             .child(home_body_text(reminder.detail.clone()))
   4690             .child(
   4691                 div()
   4692                     .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
   4693                     .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   4694                     .child(reminder_deadline_text(reminder)),
   4695             )
   4696             .when_some(action, |this, action| this.child(div().child(action)))
   4697             .into_any_element()
   4698     }
   4699 
   4700     fn render_reminder_action(
   4701         &mut self,
   4702         scope: &'static str,
   4703         index: usize,
   4704         reminder: &ReminderDeadlineProjection,
   4705         cx: &mut Context<Self>,
   4706     ) -> Option<AnyElement> {
   4707         let label = reminder.action_label.clone()?;
   4708 
   4709         match reminder_action_target(reminder) {
   4710             Some(ReminderActionTarget::OrderDetail(order_id)) => Some(
   4711                 action_button_compact(
   4712                     (scope, index),
   4713                     SharedString::from(label),
   4714                     cx.listener(move |this, _, _, cx| this.open_order_detail(order_id, cx)),
   4715                     cx,
   4716                 )
   4717                 .into_any_element(),
   4718             ),
   4719             Some(ReminderActionTarget::PackDay(fulfillment_window_id)) => Some(
   4720                 action_button_compact(
   4721                     (scope, index),
   4722                     SharedString::from(label),
   4723                     cx.listener(move |this, _, _, cx| {
   4724                         this.open_pack_day(Some(fulfillment_window_id), cx)
   4725                     }),
   4726                     cx,
   4727                 )
   4728                 .into_any_element(),
   4729             ),
   4730             None => None,
   4731         }
   4732     }
   4733 
   4734     fn render_products_table_card(
   4735         &mut self,
   4736         rows: &[ProductsListRow],
   4737         cx: &mut Context<Self>,
   4738     ) -> AnyElement {
   4739         let mut items = Vec::with_capacity(rows.len().saturating_mul(2));
   4740         for (index, row) in rows.iter().enumerate() {
   4741             items.push(self.render_products_table_entry(index, row, cx));
   4742             if index + 1 < rows.len() {
   4743                 items.push(section_divider().into_any_element());
   4744             }
   4745         }
   4746 
   4747         home_card(
   4748             app_shared_text(AppTextKey::ProductsTableTitle),
   4749             div()
   4750                 .w_full()
   4751                 .flex()
   4752                 .flex_col()
   4753                 .gap(px(12.0))
   4754                 .child(products_table_header())
   4755                 .child(section_divider())
   4756                 .children(items),
   4757         )
   4758         .into_any_element()
   4759     }
   4760 
   4761     fn render_orders_table_card(
   4762         &mut self,
   4763         rows: &[OrdersListRow],
   4764         selected_order_id: Option<OrderId>,
   4765         cx: &mut Context<Self>,
   4766     ) -> AnyElement {
   4767         let mut items = Vec::with_capacity(rows.len().saturating_mul(2));
   4768         for (index, row) in rows.iter().enumerate() {
   4769             items.push(self.render_orders_table_entry(index, row, selected_order_id, cx));
   4770             if index + 1 < rows.len() {
   4771                 items.push(section_divider().into_any_element());
   4772             }
   4773         }
   4774 
   4775         home_card(
   4776             app_shared_text(AppTextKey::OrdersTableTitle),
   4777             div()
   4778                 .w_full()
   4779                 .flex()
   4780                 .flex_col()
   4781                 .gap(px(12.0))
   4782                 .child(orders_table_header())
   4783                 .child(section_divider())
   4784                 .children(items),
   4785         )
   4786         .into_any_element()
   4787     }
   4788 
   4789     fn render_order_detail_card(
   4790         &mut self,
   4791         detail: &OrderDetailProjection,
   4792         on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   4793         cx: &mut Context<Self>,
   4794     ) -> AnyElement {
   4795         app_focused_detail_view(
   4796             app_shared_text(AppTextKey::OrdersDetailTitle),
   4797             app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   4798                 .w_full()
   4799                 .child(app_heading_section(detail.order_number.clone()))
   4800                 .child(home_body_text(detail.customer_display_name.clone()))
   4801                 .child(trade_workflow_detail_badge_strip(&detail.workflow))
   4802                 .child(label_value_list([
   4803                     LabelValueRow::new(
   4804                         app_shared_text(AppTextKey::OrdersDetailCustomerLabel),
   4805                         detail.customer_display_name.clone(),
   4806                     ),
   4807                     LabelValueRow::new(
   4808                         app_shared_text(AppTextKey::OrdersDetailWindowLabel),
   4809                         order_optional_text(detail.fulfillment_window_label.as_deref()),
   4810                     ),
   4811                     LabelValueRow::new(
   4812                         app_shared_text(AppTextKey::OrdersDetailPickupLabel),
   4813                         order_optional_text(detail.pickup_location_label.as_deref()),
   4814                     ),
   4815                     LabelValueRow::new(
   4816                         app_shared_text(AppTextKey::OrdersDetailTotalLabel),
   4817                         trade_economics_total_text(&detail.workflow.economics),
   4818                     ),
   4819                 ]))
   4820                 .when(!detail.validation_receipts.is_empty(), |this| {
   4821                     this.child(validation_receipts_summary_section(
   4822                         &detail.validation_receipts,
   4823                     ))
   4824                 })
   4825                 .child(app_form_section(
   4826                     app_shared_text(AppTextKey::OrdersDetailItemsTitle),
   4827                     div()
   4828                         .w_full()
   4829                         .flex()
   4830                         .flex_col()
   4831                         .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
   4832                         .children(
   4833                             detail
   4834                                 .items
   4835                                 .iter()
   4836                                 .map(order_detail_item_row)
   4837                                 .collect::<Vec<_>>(),
   4838                         )
   4839                         .when(detail.items.is_empty(), |this| {
   4840                             this.child(home_body_text(app_shared_text(AppTextKey::ValueNone)))
   4841                         }),
   4842                 )),
   4843             text_button(
   4844                 "orders-detail-back",
   4845                 app_shared_text(AppTextKey::PersonalDetailBackAction),
   4846                 on_close,
   4847                 cx,
   4848             ),
   4849         )
   4850     }
   4851 
   4852     fn render_products_table_entry(
   4853         &mut self,
   4854         index: usize,
   4855         row: &ProductsListRow,
   4856         cx: &mut Context<Self>,
   4857     ) -> AnyElement {
   4858         let is_open = self
   4859             .product_editor_form
   4860             .as_ref()
   4861             .map(|form| form.product_id == row.product_id)
   4862             .unwrap_or(false);
   4863         let is_editing = self
   4864             .products_stock_editor
   4865             .as_ref()
   4866             .map(|editor| editor.product_id == row.product_id)
   4867             .unwrap_or(false);
   4868         let product = list_row_button(
   4869             ("products-row-open", index),
   4870             product_display_title(row.title.as_str()),
   4871             row.subtitle.clone().map(SharedString::from),
   4872             is_open,
   4873             cx.listener({
   4874                 let product_id = row.product_id;
   4875                 move |this, _, _, cx| this.open_existing_product_editor(product_id, cx)
   4876             }),
   4877             cx,
   4878         )
   4879         .into_any_element();
   4880         let action = if is_editing {
   4881             action_button_compact(
   4882                 "products-stock-editor-cancel",
   4883                 app_shared_text(AppTextKey::ProductsStockEditorCancelAction),
   4884                 cx.listener(|this, _, _, cx| this.close_products_stock_editor(cx)),
   4885                 cx,
   4886             )
   4887             .into_any_element()
   4888         } else {
   4889             action_button_compact(
   4890                 ("products-row-stock-action", index),
   4891                 app_shared_text(AppTextKey::ProductsUpdateStockAction),
   4892                 cx.listener({
   4893                     let product_id = row.product_id;
   4894                     let stock_quantity = row.stock.quantity;
   4895                     move |this, _, window, cx| {
   4896                         this.open_products_stock_editor(product_id, stock_quantity, window, cx)
   4897                     }
   4898                 }),
   4899                 cx,
   4900             )
   4901             .into_any_element()
   4902         };
   4903 
   4904         div()
   4905             .w_full()
   4906             .flex()
   4907             .flex_col()
   4908             .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
   4909             .child(products_table_row(product, row, action))
   4910             .when(is_editing, |this| {
   4911                 this.when_some(self.products_stock_editor.as_ref(), |this, editor| {
   4912                     this.child(products_stock_editor_card(
   4913                         row,
   4914                         editor,
   4915                         cx.listener(|this, _, _, cx| this.save_products_stock_editor(cx)),
   4916                         cx.listener(|this, _, _, cx| this.close_products_stock_editor(cx)),
   4917                         cx,
   4918                     ))
   4919                 })
   4920             })
   4921             .into_any_element()
   4922     }
   4923 
   4924     fn render_orders_table_entry(
   4925         &mut self,
   4926         index: usize,
   4927         row: &OrdersListRow,
   4928         selected_order_id: Option<OrderId>,
   4929         cx: &mut Context<Self>,
   4930     ) -> AnyElement {
   4931         let is_selected = selected_order_id.is_some_and(|order_id| order_id == row.order_id);
   4932         let order = list_row_button(
   4933             ("orders-row-open", index),
   4934             row.order_number.clone(),
   4935             Some(SharedString::from(row.customer_display_name.clone())),
   4936             is_selected,
   4937             cx.listener({
   4938                 let order_id = row.order_id;
   4939                 move |this, _, _, cx| this.open_order_detail(order_id, cx)
   4940             }),
   4941             cx,
   4942         )
   4943         .into_any_element();
   4944         let action = orders_table_action(
   4945             index,
   4946             row,
   4947             cx.listener({
   4948                 let order_id = row.order_id;
   4949                 move |this, _, _, cx| this.open_order_detail(order_id, cx)
   4950             }),
   4951             cx,
   4952         );
   4953 
   4954         div()
   4955             .w_full()
   4956             .child(orders_table_row(order, row, action))
   4957             .into_any_element()
   4958     }
   4959 }
   4960 
   4961 impl Render for HomeView {
   4962     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
   4963         let runtime_summary = self.runtime.summary();
   4964         self.sync_startup_signer_entry(&runtime_summary, window, cx);
   4965         self.sync_farm_setup_form(&runtime_summary, window, cx);
   4966         self.sync_personal_search(&runtime_summary, window, cx);
   4967         self.sync_buyer_order_review_form(&runtime_summary, window, cx);
   4968         self.sync_products_search(&runtime_summary, window, cx);
   4969         self.sync_products_stock_editor(&runtime_summary);
   4970         self.sync_product_editor_form(&runtime_summary, window, cx);
   4971         self.apply_auto_focus(&runtime_summary, window, cx);
   4972         match home_stage(&runtime_summary) {
   4973             HomeStage::Setup => self
   4974                 .startup_view
   4975                 .render(
   4976                     &runtime_summary,
   4977                     self.startup_signer_entry.as_ref(),
   4978                     &self.startup_signer_connect_state,
   4979                     cx.listener(|this, _, _, cx| this.show_startup_identity_choice(cx)),
   4980                     cx.listener(|this, _, _, cx| {
   4981                         this.select_personal_section(PersonalSection::Browse, cx)
   4982                     }),
   4983                     cx.listener(|this, _, window, cx| this.start_generate_key(window, cx)),
   4984                     cx.listener(|this, _, _, cx| this.show_startup_signer_entry(cx)),
   4985                     cx.listener(|this, _, window, cx| this.submit_startup_signer(window, cx)),
   4986                     cx.listener(|this, _, _, cx| this.back_out_of_startup_signer_entry(cx)),
   4987                     cx,
   4988                 )
   4989                 .into_any_element(),
   4990             HomeStage::AccountWorkspace => {
   4991                 self.render_account_workspace(&runtime_summary, window, cx)
   4992             }
   4993             HomeStage::BuyerWorkspace => self.render_buyer_workspace(&runtime_summary, cx),
   4994             HomeStage::FarmerWorkspace => self.render_farmer_workspace(&runtime_summary, cx),
   4995         }
   4996     }
   4997 }
   4998 
   4999 impl HomeView {
   5000     fn render_account_workspace(
   5001         &mut self,
   5002         runtime: &DesktopAppRuntimeSummary,
   5003         window: &mut Window,
   5004         cx: &mut Context<Self>,
   5005     ) -> AnyElement {
   5006         let sidebar = if runtime.shell_projection.active_surface == ActiveSurface::Farmer {
   5007             home_sidebar(
   5008                 runtime,
   5009                 cx.listener(|this, _, _, cx| this.select_farmer_section(FarmerSection::Today, cx)),
   5010                 cx.listener(|this, _, _, cx| {
   5011                     this.select_farmer_section(FarmerSection::Products, cx)
   5012                 }),
   5013                 cx.listener(|this, _, _, cx| this.open_orders(cx)),
   5014                 cx.listener(|this, _, _, cx| this.open_pack_day(None, cx)),
   5015                 cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::Profile, cx)),
   5016                 cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::FarmDetails, cx)),
   5017                 cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::Preferences, cx)),
   5018                 cx.listener(|this, _, _, cx| this.open_account_tab(AccountTab::Settings, cx)),
   5019                 cx,
   5020             )
   5021             .into_any_element()
   5022         } else {
   5023             buyer_sidebar(
   5024                 runtime,
   5025                 cx.listener(|this, _, _, cx| {
   5026                     this.select_personal_section(PersonalSection::Browse, cx)
   5027                 }),
   5028                 cx.listener(|this, _, _, cx| {
   5029                     this.select_personal_section(PersonalSection::Search, cx)
   5030                 }),
   5031                 cx.listener(|this, _, _, cx| {
   5032                     this.select_personal_section(PersonalSection::Cart, cx)
   5033                 }),
   5034                 cx.listener(|this, _, _, cx| {
   5035                     this.select_personal_section(PersonalSection::Orders, cx)
   5036                 }),
   5037                 cx,
   5038             )
   5039             .into_any_element()
   5040         };
   5041 
   5042         app_split_shell(
   5043             sidebar,
   5044             app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   5045                 .size_full()
   5046                 .child(shared_shell_header(
   5047                     runtime,
   5048                     cx.listener(|this, _, _, cx| this.switch_to_marketplace(cx)),
   5049                     cx.listener(|this, _, _, cx| this.switch_to_farmer_workspace(cx)),
   5050                     cx.listener(|this, _, _, cx| this.open_account_entry(cx)),
   5051                     cx,
   5052                 ))
   5053                 .child(
   5054                     div()
   5055                         .flex_1()
   5056                         .w_full()
   5057                         .overflow_hidden()
   5058                         .child(self.render_account_content(window, cx)),
   5059                 )
   5060                 .into_any_element(),
   5061         )
   5062         .into_any_element()
   5063     }
   5064 
   5065     fn render_account_content(
   5066         &mut self,
   5067         window: &mut Window,
   5068         cx: &mut Context<Self>,
   5069     ) -> AnyElement {
   5070         let selected_tab = self.selected_account_tab;
   5071         let tabs = AccountTab::ORDERED
   5072             .into_iter()
   5073             .map(|tab| AppUnderlineTabSpec::new(app_shared_text(tab.text_key())));
   5074         let (heading_key, heading_actions, fixed_subheader, panel): (
   5075             AppTextKey,
   5076             Option<AnyElement>,
   5077             Option<AnyElement>,
   5078             AnyElement,
   5079         ) = match selected_tab {
   5080             AccountTab::Profile => {
   5081                 let form = self.account_profile_form(window, cx).clone();
   5082                 (
   5083                     AppTextKey::AccountProfilePersonalDetailsTitle,
   5084                     None,
   5085                     None,
   5086                     account_profile_panel(&form, cx).into_any_element(),
   5087                 )
   5088             }
   5089             AccountTab::FarmDetails => {
   5090                 self.prepare_account_farm_profile_textarea_wrap(window, cx);
   5091                 let form = self.account_farm_profile_form(window, cx).clone();
   5092                 let selected_farm_details_tab = self.selected_account_farm_details_tab;
   5093                 let farm_details_tabs = AccountFarmDetailsTab::ORDERED
   5094                     .into_iter()
   5095                     .map(|tab| AppPillTabSpec::new(app_shared_text(tab.text_key())));
   5096                 (
   5097                     AppTextKey::AccountFarmDetailsTitle,
   5098                     Some(
   5099                         account_form_heading_actions(
   5100                             "account-farm-save-draft",
   5101                             "account-farm-save",
   5102                             form.is_dirty(cx),
   5103                             cx,
   5104                         )
   5105                         .into_any_element(),
   5106                     ),
   5107                     Some(
   5108                         app_pill_tabs(
   5109                             "account-farm-details-tabs",
   5110                             farm_details_tabs,
   5111                             selected_farm_details_tab.selected_index(),
   5112                             cx.listener(|this, index: &usize, _, cx| {
   5113                                 this.select_account_farm_details_tab(
   5114                                     AccountFarmDetailsTab::from_index(*index),
   5115                                     cx,
   5116                                 )
   5117                             }),
   5118                             cx,
   5119                         )
   5120                         .into_any_element(),
   5121                     ),
   5122                     account_farm_profile_panel(
   5123                         &form,
   5124                         selected_farm_details_tab,
   5125                         self.account_farm_profile_textarea_wrap_ready,
   5126                         cx,
   5127                     )
   5128                     .into_any_element(),
   5129                 )
   5130             }
   5131             AccountTab::Preferences => (
   5132                 AppTextKey::AccountTabPreferences,
   5133                 None,
   5134                 None,
   5135                 account_placeholder_panel(selected_tab.panel_text_key()).into_any_element(),
   5136             ),
   5137             AccountTab::Settings => {
   5138                 let form = self.account_settings_form(window, cx).clone();
   5139                 (
   5140                     AppTextKey::AccountSettingsTitle,
   5141                     Some(
   5142                         account_form_heading_actions(
   5143                             "account-settings-save-draft",
   5144                             "account-settings-save",
   5145                             form.is_dirty(cx),
   5146                             cx,
   5147                         )
   5148                         .into_any_element(),
   5149                     ),
   5150                     None,
   5151                     account_settings_panel(&form, cx).into_any_element(),
   5152                 )
   5153             }
   5154         };
   5155         let panel_uses_inner_scroll = selected_tab == AccountTab::FarmDetails;
   5156 
   5157         account_tab_frame(
   5158             tabs,
   5159             selected_tab.selected_index(),
   5160             cx.listener(|this, index: &usize, _, cx| {
   5161                 this.select_account_tab(AccountTab::from_index(*index), cx)
   5162             }),
   5163             heading_key,
   5164             heading_actions,
   5165             fixed_subheader,
   5166             panel,
   5167             panel_uses_inner_scroll,
   5168         )
   5169         .into_any_element()
   5170     }
   5171 
   5172     fn account_profile_form(
   5173         &mut self,
   5174         window: &mut Window,
   5175         cx: &mut Context<Self>,
   5176     ) -> &AccountProfileFormState {
   5177         if self.account_profile_form.is_none() {
   5178             self.account_profile_form = Some(AccountProfileFormState::new(window, cx));
   5179         }
   5180 
   5181         let Some(form) = self.account_profile_form.as_ref() else {
   5182             unreachable!();
   5183         };
   5184         form
   5185     }
   5186     fn account_farm_profile_form(
   5187         &mut self,
   5188         window: &mut Window,
   5189         cx: &mut Context<Self>,
   5190     ) -> &AccountFarmProfileFormState {
   5191         if self.account_farm_profile_form.is_none() {
   5192             self.account_farm_profile_form = Some(AccountFarmProfileFormState::new(window, cx));
   5193         }
   5194 
   5195         let Some(form) = self.account_farm_profile_form.as_ref() else {
   5196             unreachable!();
   5197         };
   5198         form
   5199     }
   5200 
   5201     fn account_settings_form(
   5202         &mut self,
   5203         window: &mut Window,
   5204         cx: &mut Context<Self>,
   5205     ) -> &AccountSettingsFormState {
   5206         if self.account_settings_form.is_none() {
   5207             self.account_settings_form = Some(AccountSettingsFormState::new(window, cx));
   5208         }
   5209 
   5210         let Some(form) = self.account_settings_form.as_ref() else {
   5211             unreachable!();
   5212         };
   5213         form
   5214     }
   5215 
   5216     fn prepare_account_farm_profile_textarea_wrap(
   5217         &mut self,
   5218         window: &mut Window,
   5219         cx: &mut Context<Self>,
   5220     ) {
   5221         if self.account_farm_profile_textarea_wrap_ready
   5222             || self.account_farm_profile_textarea_wrap_requested
   5223         {
   5224             return;
   5225         }
   5226 
   5227         self.account_farm_profile_textarea_wrap_requested = true;
   5228         cx.spawn_in(window, async move |this, cx| {
   5229             Timer::after(Duration::from_millis(16)).await;
   5230             let _ = this.update(cx, |this, cx| {
   5231                 this.account_farm_profile_textarea_wrap_ready = true;
   5232                 cx.notify();
   5233             });
   5234         })
   5235         .detach();
   5236     }
   5237 }
   5238 
   5239 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
   5240 enum FarmSetupSaveState {
   5241     AutosavesLocally,
   5242     SavedLocally,
   5243     SaveFailed,
   5244 }
   5245 
   5246 struct FarmSetupFormState {
   5247     account_id: String,
   5248     draft: FarmSetupDraft,
   5249     farm_name_input: Entity<InputState>,
   5250     location_input: Entity<InputState>,
   5251     _farm_name_subscription: Subscription,
   5252     _location_subscription: Subscription,
   5253     save_state: FarmSetupSaveState,
   5254 }
   5255 
   5256 impl FarmSetupFormState {
   5257     fn new(
   5258         account_id: String,
   5259         draft: FarmSetupDraft,
   5260         window: &mut Window,
   5261         cx: &mut Context<HomeView>,
   5262     ) -> Self {
   5263         let farm_name_input =
   5264             cx.new(|cx| InputState::new(window, cx).default_value(draft.farm_name.clone()));
   5265         let location_input = cx.new(|cx| {
   5266             InputState::new(window, cx).default_value(draft.location_or_service_area.clone())
   5267         });
   5268         let farm_name_subscription = cx.subscribe_in(
   5269             &farm_name_input,
   5270             window,
   5271             HomeView::handle_farm_name_input_event,
   5272         );
   5273         let location_subscription = cx.subscribe_in(
   5274             &location_input,
   5275             window,
   5276             HomeView::handle_location_input_event,
   5277         );
   5278         let save_state = if draft.is_empty() {
   5279             FarmSetupSaveState::AutosavesLocally
   5280         } else {
   5281             FarmSetupSaveState::SavedLocally
   5282         };
   5283 
   5284         Self {
   5285             account_id,
   5286             draft,
   5287             farm_name_input,
   5288             location_input,
   5289             _farm_name_subscription: farm_name_subscription,
   5290             _location_subscription: location_subscription,
   5291             save_state,
   5292         }
   5293     }
   5294 }
   5295 
   5296 struct PersonalSearchState {
   5297     workspace_id: String,
   5298     input: Entity<InputState>,
   5299     _input_subscription: Subscription,
   5300 }
   5301 
   5302 impl PersonalSearchState {
   5303     fn new(
   5304         workspace_id: String,
   5305         search_query: &str,
   5306         window: &mut Window,
   5307         cx: &mut Context<HomeView>,
   5308     ) -> Self {
   5309         let input = cx.new(|cx| {
   5310             InputState::new(window, cx)
   5311                 .placeholder(app_shared_text(AppTextKey::PersonalSearchPlaceholder))
   5312                 .default_value(search_query.to_owned())
   5313         });
   5314         let input_subscription =
   5315             cx.subscribe_in(&input, window, HomeView::handle_personal_search_input_event);
   5316 
   5317         Self {
   5318             workspace_id,
   5319             input,
   5320             _input_subscription: input_subscription,
   5321         }
   5322     }
   5323 
   5324     fn sync(&mut self, search_query: &str, window: &mut Window, cx: &mut Context<HomeView>) {
   5325         if self.input.read(cx).value().as_ref() == search_query {
   5326             return;
   5327         }
   5328 
   5329         self.input.update(cx, |input, cx| {
   5330             input.set_value(search_query.to_owned(), window, cx);
   5331         });
   5332     }
   5333 }
   5334 
   5335 struct BuyerOrderReviewFormState {
   5336     workspace_id: String,
   5337     name_input: Entity<InputState>,
   5338     email_input: Entity<InputState>,
   5339     phone_input: Entity<InputState>,
   5340     order_note_input: Entity<InputState>,
   5341     _name_subscription: Subscription,
   5342     _email_subscription: Subscription,
   5343     _phone_subscription: Subscription,
   5344     _order_note_subscription: Subscription,
   5345 }
   5346 
   5347 impl BuyerOrderReviewFormState {
   5348     fn new(
   5349         workspace_id: String,
   5350         draft: &BuyerOrderReviewDraft,
   5351         window: &mut Window,
   5352         cx: &mut Context<HomeView>,
   5353     ) -> Self {
   5354         let name_input = cx.new(|cx| InputState::new(window, cx).default_value(draft.name.clone()));
   5355         let email_input =
   5356             cx.new(|cx| InputState::new(window, cx).default_value(draft.email.clone()));
   5357         let phone_input =
   5358             cx.new(|cx| InputState::new(window, cx).default_value(draft.phone.clone()));
   5359         let order_note_input =
   5360             cx.new(|cx| InputState::new(window, cx).default_value(draft.order_note.clone()));
   5361         let name_subscription = cx.subscribe_in(
   5362             &name_input,
   5363             window,
   5364             HomeView::handle_buyer_order_review_input_event,
   5365         );
   5366         let email_subscription = cx.subscribe_in(
   5367             &email_input,
   5368             window,
   5369             HomeView::handle_buyer_order_review_input_event,
   5370         );
   5371         let phone_subscription = cx.subscribe_in(
   5372             &phone_input,
   5373             window,
   5374             HomeView::handle_buyer_order_review_input_event,
   5375         );
   5376         let order_note_subscription = cx.subscribe_in(
   5377             &order_note_input,
   5378             window,
   5379             HomeView::handle_buyer_order_review_input_event,
   5380         );
   5381 
   5382         Self {
   5383             workspace_id,
   5384             name_input,
   5385             email_input,
   5386             phone_input,
   5387             order_note_input,
   5388             _name_subscription: name_subscription,
   5389             _email_subscription: email_subscription,
   5390             _phone_subscription: phone_subscription,
   5391             _order_note_subscription: order_note_subscription,
   5392         }
   5393     }
   5394 
   5395     fn sync(
   5396         &mut self,
   5397         draft: &BuyerOrderReviewDraft,
   5398         window: &mut Window,
   5399         cx: &mut Context<HomeView>,
   5400     ) {
   5401         sync_order_review_input(&self.name_input, draft.name.as_str(), window, cx);
   5402         sync_order_review_input(&self.email_input, draft.email.as_str(), window, cx);
   5403         sync_order_review_input(&self.phone_input, draft.phone.as_str(), window, cx);
   5404         sync_order_review_input(
   5405             &self.order_note_input,
   5406             draft.order_note.as_str(),
   5407             window,
   5408             cx,
   5409         );
   5410     }
   5411 
   5412     fn current_draft(&self, cx: &App) -> BuyerOrderReviewDraft {
   5413         BuyerOrderReviewDraft {
   5414             name: self.name_input.read(cx).value().to_string(),
   5415             email: self.email_input.read(cx).value().to_string(),
   5416             phone: self.phone_input.read(cx).value().to_string(),
   5417             order_note: self.order_note_input.read(cx).value().to_string(),
   5418         }
   5419     }
   5420 }
   5421 
   5422 fn sync_order_review_input(
   5423     input: &Entity<InputState>,
   5424     value: &str,
   5425     window: &mut Window,
   5426     cx: &mut Context<HomeView>,
   5427 ) {
   5428     if input.read(cx).value().as_ref() == value {
   5429         return;
   5430     }
   5431 
   5432     input.update(cx, |input, cx| {
   5433         input.set_value(value.to_owned(), window, cx);
   5434     });
   5435 }
   5436 
   5437 struct ProductsSearchState {
   5438     account_id: String,
   5439     input: Entity<InputState>,
   5440     _input_subscription: Subscription,
   5441 }
   5442 
   5443 impl ProductsSearchState {
   5444     fn new(
   5445         account_id: String,
   5446         search_query: &str,
   5447         window: &mut Window,
   5448         cx: &mut Context<HomeView>,
   5449     ) -> Self {
   5450         let input = cx.new(|cx| {
   5451             InputState::new(window, cx)
   5452                 .placeholder(app_shared_text(AppTextKey::ProductsSearchPlaceholder))
   5453                 .default_value(search_query.to_owned())
   5454         });
   5455         let input_subscription =
   5456             cx.subscribe_in(&input, window, HomeView::handle_products_search_input_event);
   5457 
   5458         Self {
   5459             account_id,
   5460             input,
   5461             _input_subscription: input_subscription,
   5462         }
   5463     }
   5464 
   5465     fn sync(&mut self, search_query: &str, window: &mut Window, cx: &mut Context<HomeView>) {
   5466         if self.input.read(cx).value().as_ref() == search_query {
   5467             return;
   5468         }
   5469 
   5470         self.input.update(cx, |input, cx| {
   5471             input.set_value(search_query.to_owned(), window, cx);
   5472         });
   5473     }
   5474 }
   5475 
   5476 struct StartupSignerEntryState {
   5477     input: Entity<InputState>,
   5478     _input_subscription: Subscription,
   5479 }
   5480 
   5481 impl StartupSignerEntryState {
   5482     fn new(source_input: &str, window: &mut Window, cx: &mut Context<HomeView>) -> Self {
   5483         let input = cx.new(|cx| {
   5484             InputState::new(window, cx)
   5485                 .placeholder(app_shared_text(
   5486                     AppTextKey::HomeSetupSignerSourcePlaceholder,
   5487                 ))
   5488                 .default_value(source_input.to_owned())
   5489         });
   5490         let input_subscription =
   5491             cx.subscribe_in(&input, window, HomeView::handle_startup_signer_input_event);
   5492 
   5493         Self {
   5494             input,
   5495             _input_subscription: input_subscription,
   5496         }
   5497     }
   5498 
   5499     fn sync(&mut self, source_input: &str, window: &mut Window, cx: &mut Context<HomeView>) {
   5500         if self.input.read(cx).value().as_ref() == source_input {
   5501             return;
   5502         }
   5503 
   5504         self.input.update(cx, |input, cx| {
   5505             input.set_value(source_input.to_owned(), window, cx);
   5506         });
   5507     }
   5508 }
   5509 
   5510 struct ProductsStockEditorState {
   5511     account_id: String,
   5512     product_id: ProductId,
   5513     initial_stock_quantity: Option<u32>,
   5514     input: Entity<InputState>,
   5515     _input_subscription: Subscription,
   5516     save_issue: Option<ProductsStockEditorSaveIssue>,
   5517 }
   5518 
   5519 impl ProductsStockEditorState {
   5520     fn new(
   5521         account_id: String,
   5522         product_id: ProductId,
   5523         stock_quantity: Option<u32>,
   5524         window: &mut Window,
   5525         cx: &mut Context<HomeView>,
   5526     ) -> Self {
   5527         let input = cx.new(|cx| {
   5528             InputState::new(window, cx)
   5529                 .placeholder(app_shared_text(AppTextKey::ProductsStockEditorFieldLabel))
   5530                 .default_value(
   5531                     stock_quantity
   5532                         .map(|quantity| quantity.to_string())
   5533                         .unwrap_or_else(|| "0".to_owned()),
   5534                 )
   5535         });
   5536         let input_subscription =
   5537             cx.subscribe_in(&input, window, HomeView::handle_products_stock_input_event);
   5538 
   5539         Self {
   5540             account_id,
   5541             product_id,
   5542             initial_stock_quantity: stock_quantity,
   5543             input,
   5544             _input_subscription: input_subscription,
   5545             save_issue: None,
   5546         }
   5547     }
   5548 
   5549     fn parsed_stock_quantity(&self, cx: &App) -> Option<u32> {
   5550         parse_products_stock_quantity(self.input.read(cx).value().as_ref())
   5551     }
   5552 
   5553     fn has_changes(&self, cx: &App) -> bool {
   5554         self.parsed_stock_quantity(cx)
   5555             .map(|stock_quantity| Some(stock_quantity) != self.initial_stock_quantity)
   5556             .unwrap_or(false)
   5557     }
   5558 }
   5559 
   5560 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
   5561 enum ProductsStockEditorSaveIssue {
   5562     SaveFailed,
   5563     PublishQueueFailed,
   5564 }
   5565 
   5566 impl ProductsStockEditorSaveIssue {
   5567     fn from_runtime_error(error: &DesktopAppRuntimeProductStockUpdateError) -> Self {
   5568         if error.is_listing_publish_sdk_enqueue_failed() {
   5569             Self::PublishQueueFailed
   5570         } else {
   5571             Self::SaveFailed
   5572         }
   5573     }
   5574 
   5575     fn text_key(self) -> AppTextKey {
   5576         match self {
   5577             Self::SaveFailed => AppTextKey::ProductsStockEditorSaveFailed,
   5578             Self::PublishQueueFailed => AppTextKey::ProductsStockEditorPublishQueueFailed,
   5579         }
   5580     }
   5581 }
   5582 
   5583 struct ProductEditorFormState {
   5584     account_id: String,
   5585     product_id: ProductId,
   5586     initial_draft: ProductEditorDraft,
   5587     status: ProductStatus,
   5588     selected_availability_window_id: Option<FulfillmentWindowId>,
   5589     title_input: Entity<InputState>,
   5590     subtitle_input: Entity<InputState>,
   5591     category_input: Entity<InputState>,
   5592     unit_input: Entity<InputState>,
   5593     price_input: Entity<InputState>,
   5594     stock_input: Entity<InputState>,
   5595     _title_subscription: Subscription,
   5596     _subtitle_subscription: Subscription,
   5597     _category_subscription: Subscription,
   5598     _unit_subscription: Subscription,
   5599     _price_subscription: Subscription,
   5600     _stock_subscription: Subscription,
   5601     save_issue: Option<ProductEditorSaveIssue>,
   5602 }
   5603 
   5604 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
   5605 enum ProductEditorSaveIssue {
   5606     SaveFailed,
   5607     PublishQueueFailed,
   5608 }
   5609 
   5610 impl ProductEditorSaveIssue {
   5611     fn from_runtime_error(error: &DesktopAppRuntimeProductEditorSaveError) -> Self {
   5612         if error.is_listing_publish_sdk_enqueue_failed() {
   5613             Self::PublishQueueFailed
   5614         } else {
   5615             Self::SaveFailed
   5616         }
   5617     }
   5618 
   5619     fn text_key(self) -> AppTextKey {
   5620         match self {
   5621             Self::SaveFailed => AppTextKey::ProductsEditorSaveFailed,
   5622             Self::PublishQueueFailed => AppTextKey::ProductsEditorPublishQueueFailed,
   5623         }
   5624     }
   5625 }
   5626 
   5627 impl ProductEditorFormState {
   5628     fn new(
   5629         account_id: String,
   5630         product_id: ProductId,
   5631         draft: ProductEditorDraft,
   5632         window: &mut Window,
   5633         cx: &mut Context<HomeView>,
   5634     ) -> Self {
   5635         let selected_availability_window_id = draft.availability_window_id;
   5636         let title_input =
   5637             cx.new(|cx| InputState::new(window, cx).default_value(draft.title.clone()));
   5638         let subtitle_input =
   5639             cx.new(|cx| InputState::new(window, cx).default_value(draft.subtitle.clone()));
   5640         let category_input =
   5641             cx.new(|cx| InputState::new(window, cx).default_value(draft.category.clone()));
   5642         let unit_input =
   5643             cx.new(|cx| InputState::new(window, cx).default_value(draft.unit_label.clone()));
   5644         let price_input = cx.new(|cx| {
   5645             InputState::new(window, cx)
   5646                 .default_value(product_editor_price_input_value(draft.price_minor_units))
   5647         });
   5648         let stock_input = cx.new(|cx| {
   5649             InputState::new(window, cx).default_value(
   5650                 draft
   5651                     .stock_quantity
   5652                     .map(|quantity| quantity.to_string())
   5653                     .unwrap_or_default(),
   5654             )
   5655         });
   5656         let title_subscription = cx.subscribe_in(
   5657             &title_input,
   5658             window,
   5659             HomeView::handle_product_editor_input_event,
   5660         );
   5661         let subtitle_subscription = cx.subscribe_in(
   5662             &subtitle_input,
   5663             window,
   5664             HomeView::handle_product_editor_input_event,
   5665         );
   5666         let category_subscription = cx.subscribe_in(
   5667             &category_input,
   5668             window,
   5669             HomeView::handle_product_editor_input_event,
   5670         );
   5671         let unit_subscription = cx.subscribe_in(
   5672             &unit_input,
   5673             window,
   5674             HomeView::handle_product_editor_input_event,
   5675         );
   5676         let price_subscription = cx.subscribe_in(
   5677             &price_input,
   5678             window,
   5679             HomeView::handle_product_editor_input_event,
   5680         );
   5681         let stock_subscription = cx.subscribe_in(
   5682             &stock_input,
   5683             window,
   5684             HomeView::handle_product_editor_input_event,
   5685         );
   5686 
   5687         Self {
   5688             account_id,
   5689             product_id,
   5690             status: draft.status,
   5691             selected_availability_window_id,
   5692             initial_draft: draft,
   5693             title_input,
   5694             subtitle_input,
   5695             category_input,
   5696             unit_input,
   5697             price_input,
   5698             stock_input,
   5699             _title_subscription: title_subscription,
   5700             _subtitle_subscription: subtitle_subscription,
   5701             _category_subscription: category_subscription,
   5702             _unit_subscription: unit_subscription,
   5703             _price_subscription: price_subscription,
   5704             _stock_subscription: stock_subscription,
   5705             save_issue: None,
   5706         }
   5707     }
   5708 
   5709     fn current_draft(&self, cx: &App) -> Option<ProductEditorDraft> {
   5710         Some(ProductEditorDraft {
   5711             title: self.title_input.read(cx).value().to_string(),
   5712             subtitle: self.subtitle_input.read(cx).value().to_string(),
   5713             category: self.category_input.read(cx).value().to_string(),
   5714             unit_label: self.unit_input.read(cx).value().to_string(),
   5715             price_minor_units: parse_product_editor_price_input(
   5716                 self.price_input.read(cx).value().as_ref(),
   5717             )?,
   5718             price_currency: "USD".to_owned(),
   5719             stock_quantity: parse_optional_product_editor_stock_input(
   5720                 self.stock_input.read(cx).value().as_ref(),
   5721             )?,
   5722             availability_window_id: self.selected_availability_window_id,
   5723             status: self.status,
   5724         })
   5725     }
   5726 
   5727     fn has_changes(&self, cx: &App) -> bool {
   5728         self.current_draft(cx)
   5729             .map(|draft| draft != self.initial_draft)
   5730             .unwrap_or(false)
   5731     }
   5732 }
   5733 
   5734 struct StartupHomeView {
   5735     startup_notice: Option<String>,
   5736 }
   5737 
   5738 impl StartupHomeView {
   5739     fn new() -> Self {
   5740         Self {
   5741             startup_notice: None,
   5742         }
   5743     }
   5744 
   5745     fn set_notice(&mut self, notice: String) {
   5746         self.startup_notice = Some(notice);
   5747     }
   5748 
   5749     fn clear_notice(&mut self) {
   5750         self.startup_notice = None;
   5751     }
   5752 
   5753     fn render(
   5754         &self,
   5755         runtime: &DesktopAppRuntimeSummary,
   5756         signer_entry: Option<&StartupSignerEntryState>,
   5757         connect_state: &StartupSignerConnectState,
   5758         on_continue: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   5759         on_browse_marketplace: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   5760         on_generate_key: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   5761         on_connect_signer: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   5762         on_submit_signer: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   5763         on_back: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   5764         cx: &App,
   5765     ) -> impl IntoElement {
   5766         startup_home_shell(
   5767             runtime,
   5768             self.startup_notice.as_deref(),
   5769             signer_entry,
   5770             connect_state,
   5771             on_continue,
   5772             on_browse_marketplace,
   5773             on_generate_key,
   5774             on_connect_signer,
   5775             on_submit_signer,
   5776             on_back,
   5777             cx,
   5778         )
   5779     }
   5780 }
   5781 
   5782 struct SettingsPickupLocationFormState {
   5783     pickup_location_id: PickupLocationId,
   5784     label_input: Entity<InputState>,
   5785     address_input: Entity<InputState>,
   5786     directions_input: Entity<InputState>,
   5787     is_default: bool,
   5788     can_remove: bool,
   5789     _label_subscription: Subscription,
   5790     _address_subscription: Subscription,
   5791     _directions_subscription: Subscription,
   5792 }
   5793 
   5794 impl SettingsPickupLocationFormState {
   5795     fn new(
   5796         record: &PickupLocationRecord,
   5797         can_remove: bool,
   5798         window: &mut Window,
   5799         cx: &mut Context<SettingsWindowView>,
   5800     ) -> Self {
   5801         let label_input =
   5802             cx.new(|cx| InputState::new(window, cx).default_value(record.label.clone()));
   5803         let address_input =
   5804             cx.new(|cx| InputState::new(window, cx).default_value(record.address_line.clone()));
   5805         let directions_input = cx.new(|cx| {
   5806             InputState::new(window, cx).default_value(record.directions.clone().unwrap_or_default())
   5807         });
   5808         let label_subscription = cx.subscribe_in(
   5809             &label_input,
   5810             window,
   5811             SettingsWindowView::handle_farm_rules_input_event,
   5812         );
   5813         let address_subscription = cx.subscribe_in(
   5814             &address_input,
   5815             window,
   5816             SettingsWindowView::handle_farm_rules_input_event,
   5817         );
   5818         let directions_subscription = cx.subscribe_in(
   5819             &directions_input,
   5820             window,
   5821             SettingsWindowView::handle_farm_rules_input_event,
   5822         );
   5823 
   5824         Self {
   5825             pickup_location_id: record.pickup_location_id,
   5826             label_input,
   5827             address_input,
   5828             directions_input,
   5829             is_default: record.is_default,
   5830             can_remove,
   5831             _label_subscription: label_subscription,
   5832             _address_subscription: address_subscription,
   5833             _directions_subscription: directions_subscription,
   5834         }
   5835     }
   5836 
   5837     fn current_draft(&self, cx: &App) -> SettingsPickupLocationDraft {
   5838         SettingsPickupLocationDraft {
   5839             pickup_location_id: self.pickup_location_id,
   5840             label: self.label_input.read(cx).value().to_string(),
   5841             address_line: self.address_input.read(cx).value().to_string(),
   5842             directions: self.directions_input.read(cx).value().to_string(),
   5843             is_default: self.is_default,
   5844         }
   5845     }
   5846 }
   5847 
   5848 #[derive(Clone, Debug, Eq, PartialEq)]
   5849 struct SettingsPickupLocationDraft {
   5850     pickup_location_id: PickupLocationId,
   5851     label: String,
   5852     address_line: String,
   5853     directions: String,
   5854     is_default: bool,
   5855 }
   5856 
   5857 impl SettingsPickupLocationDraft {
   5858     fn from_record(record: &PickupLocationRecord) -> Self {
   5859         Self {
   5860             pickup_location_id: record.pickup_location_id,
   5861             label: record.label.clone(),
   5862             address_line: record.address_line.clone(),
   5863             directions: record.directions.clone().unwrap_or_default(),
   5864             is_default: record.is_default,
   5865         }
   5866     }
   5867 
   5868     fn into_record(self, farm_id: FarmId) -> PickupLocationRecord {
   5869         let directions = self.directions.trim().to_owned();
   5870 
   5871         PickupLocationRecord {
   5872             pickup_location_id: self.pickup_location_id,
   5873             farm_id,
   5874             label: self.label.trim().to_owned(),
   5875             address_line: self.address_line.trim().to_owned(),
   5876             directions: (!directions.is_empty()).then_some(directions),
   5877             is_default: self.is_default,
   5878         }
   5879     }
   5880 }
   5881 
   5882 struct SettingsOperatingRulesFormState {
   5883     promise_lead_hours_input: Entity<InputState>,
   5884     substitution_policy_input: Entity<InputState>,
   5885     _promise_lead_hours_subscription: Subscription,
   5886     _substitution_policy_subscription: Subscription,
   5887 }
   5888 
   5889 impl SettingsOperatingRulesFormState {
   5890     fn new(
   5891         record: Option<&FarmOperatingRulesRecord>,
   5892         window: &mut Window,
   5893         cx: &mut Context<SettingsWindowView>,
   5894     ) -> Self {
   5895         let promise_lead_hours_input = cx.new(|cx| {
   5896             InputState::new(window, cx).default_value(
   5897                 record
   5898                     .map(|record| record.promise_lead_hours.to_string())
   5899                     .unwrap_or_default(),
   5900             )
   5901         });
   5902         let substitution_policy_input = cx.new(|cx| {
   5903             InputState::new(window, cx).default_value(
   5904                 record
   5905                     .map(|record| record.substitution_policy.clone())
   5906                     .unwrap_or_default(),
   5907             )
   5908         });
   5909         let promise_lead_hours_subscription = cx.subscribe_in(
   5910             &promise_lead_hours_input,
   5911             window,
   5912             SettingsWindowView::handle_farm_rules_input_event,
   5913         );
   5914         let substitution_policy_subscription = cx.subscribe_in(
   5915             &substitution_policy_input,
   5916             window,
   5917             SettingsWindowView::handle_farm_rules_input_event,
   5918         );
   5919 
   5920         Self {
   5921             promise_lead_hours_input,
   5922             substitution_policy_input,
   5923             _promise_lead_hours_subscription: promise_lead_hours_subscription,
   5924             _substitution_policy_subscription: substitution_policy_subscription,
   5925         }
   5926     }
   5927 
   5928     fn current_draft(&self, cx: &App) -> SettingsOperatingRulesDraft {
   5929         SettingsOperatingRulesDraft {
   5930             promise_lead_hours: self.promise_lead_hours_input.read(cx).value().to_string(),
   5931             substitution_policy: self.substitution_policy_input.read(cx).value().to_string(),
   5932         }
   5933     }
   5934 }
   5935 
   5936 #[derive(Clone, Debug, Eq, PartialEq)]
   5937 struct SettingsOperatingRulesDraft {
   5938     promise_lead_hours: String,
   5939     substitution_policy: String,
   5940 }
   5941 
   5942 impl SettingsOperatingRulesDraft {
   5943     fn from_record(record: Option<&FarmOperatingRulesRecord>) -> Self {
   5944         Self {
   5945             promise_lead_hours: record
   5946                 .map(|record| record.promise_lead_hours.to_string())
   5947                 .unwrap_or_default(),
   5948             substitution_policy: record
   5949                 .map(|record| record.substitution_policy.clone())
   5950                 .unwrap_or_default(),
   5951         }
   5952     }
   5953 
   5954     fn is_empty(&self) -> bool {
   5955         self.promise_lead_hours.trim().is_empty() && self.substitution_policy.trim().is_empty()
   5956     }
   5957 }
   5958 
   5959 struct SettingsFulfillmentWindowFormState {
   5960     fulfillment_window_id: FulfillmentWindowId,
   5961     selected_pickup_location_id: Option<PickupLocationId>,
   5962     label_input: Entity<InputState>,
   5963     starts_at_input: Entity<InputState>,
   5964     ends_at_input: Entity<InputState>,
   5965     order_cutoff_input: Entity<InputState>,
   5966     _label_subscription: Subscription,
   5967     _starts_at_subscription: Subscription,
   5968     _ends_at_subscription: Subscription,
   5969     _order_cutoff_subscription: Subscription,
   5970 }
   5971 
   5972 impl SettingsFulfillmentWindowFormState {
   5973     fn new(
   5974         draft: &SettingsFulfillmentWindowDraft,
   5975         window: &mut Window,
   5976         cx: &mut Context<SettingsWindowView>,
   5977     ) -> Self {
   5978         let label_input =
   5979             cx.new(|cx| InputState::new(window, cx).default_value(draft.label.clone()));
   5980         let starts_at_input =
   5981             cx.new(|cx| InputState::new(window, cx).default_value(draft.starts_at.clone()));
   5982         let ends_at_input =
   5983             cx.new(|cx| InputState::new(window, cx).default_value(draft.ends_at.clone()));
   5984         let order_cutoff_input =
   5985             cx.new(|cx| InputState::new(window, cx).default_value(draft.order_cutoff_at.clone()));
   5986         let label_subscription = cx.subscribe_in(
   5987             &label_input,
   5988             window,
   5989             SettingsWindowView::handle_farm_rules_input_event,
   5990         );
   5991         let starts_at_subscription = cx.subscribe_in(
   5992             &starts_at_input,
   5993             window,
   5994             SettingsWindowView::handle_farm_rules_input_event,
   5995         );
   5996         let ends_at_subscription = cx.subscribe_in(
   5997             &ends_at_input,
   5998             window,
   5999             SettingsWindowView::handle_farm_rules_input_event,
   6000         );
   6001         let order_cutoff_subscription = cx.subscribe_in(
   6002             &order_cutoff_input,
   6003             window,
   6004             SettingsWindowView::handle_farm_rules_input_event,
   6005         );
   6006 
   6007         Self {
   6008             fulfillment_window_id: draft.fulfillment_window_id,
   6009             selected_pickup_location_id: draft.selected_pickup_location_id,
   6010             label_input,
   6011             starts_at_input,
   6012             ends_at_input,
   6013             order_cutoff_input,
   6014             _label_subscription: label_subscription,
   6015             _starts_at_subscription: starts_at_subscription,
   6016             _ends_at_subscription: ends_at_subscription,
   6017             _order_cutoff_subscription: order_cutoff_subscription,
   6018         }
   6019     }
   6020 
   6021     fn current_draft(&self, cx: &App) -> SettingsFulfillmentWindowDraft {
   6022         SettingsFulfillmentWindowDraft {
   6023             fulfillment_window_id: self.fulfillment_window_id,
   6024             selected_pickup_location_id: self.selected_pickup_location_id,
   6025             label: self.label_input.read(cx).value().to_string(),
   6026             starts_at: self.starts_at_input.read(cx).value().to_string(),
   6027             ends_at: self.ends_at_input.read(cx).value().to_string(),
   6028             order_cutoff_at: self.order_cutoff_input.read(cx).value().to_string(),
   6029         }
   6030     }
   6031 }
   6032 
   6033 #[derive(Clone, Debug, Eq, PartialEq)]
   6034 struct SettingsFulfillmentWindowDraft {
   6035     fulfillment_window_id: FulfillmentWindowId,
   6036     selected_pickup_location_id: Option<PickupLocationId>,
   6037     label: String,
   6038     starts_at: String,
   6039     ends_at: String,
   6040     order_cutoff_at: String,
   6041 }
   6042 
   6043 impl SettingsFulfillmentWindowDraft {
   6044     fn from_record(record: &FulfillmentWindowRecord) -> Self {
   6045         Self {
   6046             fulfillment_window_id: record.fulfillment_window_id,
   6047             selected_pickup_location_id: Some(record.pickup_location_id),
   6048             label: record.label.clone(),
   6049             starts_at: record.starts_at.clone(),
   6050             ends_at: record.ends_at.clone(),
   6051             order_cutoff_at: record.order_cutoff_at.clone(),
   6052         }
   6053     }
   6054 }
   6055 
   6056 struct SettingsBlackoutPeriodFormState {
   6057     blackout_period_id: BlackoutPeriodId,
   6058     label_input: Entity<InputState>,
   6059     starts_at_input: Entity<InputState>,
   6060     ends_at_input: Entity<InputState>,
   6061     _label_subscription: Subscription,
   6062     _starts_at_subscription: Subscription,
   6063     _ends_at_subscription: Subscription,
   6064 }
   6065 
   6066 impl SettingsBlackoutPeriodFormState {
   6067     fn new(
   6068         draft: &SettingsBlackoutPeriodDraft,
   6069         window: &mut Window,
   6070         cx: &mut Context<SettingsWindowView>,
   6071     ) -> Self {
   6072         let label_input =
   6073             cx.new(|cx| InputState::new(window, cx).default_value(draft.label.clone()));
   6074         let starts_at_input =
   6075             cx.new(|cx| InputState::new(window, cx).default_value(draft.starts_at.clone()));
   6076         let ends_at_input =
   6077             cx.new(|cx| InputState::new(window, cx).default_value(draft.ends_at.clone()));
   6078         let label_subscription = cx.subscribe_in(
   6079             &label_input,
   6080             window,
   6081             SettingsWindowView::handle_farm_rules_input_event,
   6082         );
   6083         let starts_at_subscription = cx.subscribe_in(
   6084             &starts_at_input,
   6085             window,
   6086             SettingsWindowView::handle_farm_rules_input_event,
   6087         );
   6088         let ends_at_subscription = cx.subscribe_in(
   6089             &ends_at_input,
   6090             window,
   6091             SettingsWindowView::handle_farm_rules_input_event,
   6092         );
   6093 
   6094         Self {
   6095             blackout_period_id: draft.blackout_period_id,
   6096             label_input,
   6097             starts_at_input,
   6098             ends_at_input,
   6099             _label_subscription: label_subscription,
   6100             _starts_at_subscription: starts_at_subscription,
   6101             _ends_at_subscription: ends_at_subscription,
   6102         }
   6103     }
   6104 
   6105     fn current_draft(&self, cx: &App) -> SettingsBlackoutPeriodDraft {
   6106         SettingsBlackoutPeriodDraft {
   6107             blackout_period_id: self.blackout_period_id,
   6108             label: self.label_input.read(cx).value().to_string(),
   6109             starts_at: self.starts_at_input.read(cx).value().to_string(),
   6110             ends_at: self.ends_at_input.read(cx).value().to_string(),
   6111         }
   6112     }
   6113 }
   6114 
   6115 #[derive(Clone, Debug, Eq, PartialEq)]
   6116 struct SettingsBlackoutPeriodDraft {
   6117     blackout_period_id: BlackoutPeriodId,
   6118     label: String,
   6119     starts_at: String,
   6120     ends_at: String,
   6121 }
   6122 
   6123 impl SettingsBlackoutPeriodDraft {
   6124     fn from_record(record: &BlackoutPeriodRecord) -> Self {
   6125         Self {
   6126             blackout_period_id: record.blackout_period_id,
   6127             label: record.label.clone(),
   6128             starts_at: record.starts_at.clone(),
   6129             ends_at: record.ends_at.clone(),
   6130         }
   6131     }
   6132 }
   6133 
   6134 #[derive(Clone, Debug, Eq, PartialEq)]
   6135 struct SettingsFarmRulesDraft {
   6136     farm_profile: FarmProfileRecord,
   6137     pickup_locations: Vec<SettingsPickupLocationDraft>,
   6138     operating_rules: SettingsOperatingRulesDraft,
   6139     fulfillment_windows: Vec<SettingsFulfillmentWindowDraft>,
   6140     blackout_periods: Vec<SettingsBlackoutPeriodDraft>,
   6141 }
   6142 
   6143 impl SettingsFarmRulesDraft {
   6144     fn from_projection(farm_id: FarmId, projection: &FarmRulesProjection) -> Self {
   6145         let farm_profile = projection
   6146             .farm_profile
   6147             .as_ref()
   6148             .cloned()
   6149             .unwrap_or(FarmProfileRecord {
   6150                 farm_id,
   6151                 display_name: String::new(),
   6152                 timezone: String::new(),
   6153                 currency_code: String::new(),
   6154             });
   6155 
   6156         Self {
   6157             farm_profile,
   6158             pickup_locations: projection
   6159                 .pickup_locations
   6160                 .iter()
   6161                 .map(SettingsPickupLocationDraft::from_record)
   6162                 .collect(),
   6163             operating_rules: SettingsOperatingRulesDraft::from_record(
   6164                 projection.operating_rules.as_ref(),
   6165             ),
   6166             fulfillment_windows: projection
   6167                 .fulfillment_windows
   6168                 .iter()
   6169                 .map(SettingsFulfillmentWindowDraft::from_record)
   6170                 .collect(),
   6171             blackout_periods: projection
   6172                 .blackout_periods
   6173                 .iter()
   6174                 .map(SettingsBlackoutPeriodDraft::from_record)
   6175                 .collect(),
   6176         }
   6177     }
   6178 }
   6179 
   6180 struct SettingsFarmRulesEvaluation {
   6181     projection: FarmRulesProjection,
   6182     operating_rules_validation_keys: Vec<AppTextKey>,
   6183     fulfillment_window_validation_keys: Vec<Vec<AppTextKey>>,
   6184     blackout_period_validation_keys: Vec<Vec<AppTextKey>>,
   6185     blocking_keys: Vec<AppTextKey>,
   6186     readiness_keys: Vec<AppTextKey>,
   6187 }
   6188 
   6189 impl SettingsFarmRulesEvaluation {
   6190     fn has_blocking_errors(&self) -> bool {
   6191         !self.blocking_keys.is_empty()
   6192     }
   6193 }
   6194 
   6195 fn push_unique_text_key(keys: &mut Vec<AppTextKey>, key: AppTextKey) {
   6196     if !keys.contains(&key) {
   6197         keys.push(key);
   6198     }
   6199 }
   6200 
   6201 struct SettingsFarmPanelState {
   6202     account_id: String,
   6203     farm_id: FarmId,
   6204     initial_draft: SettingsFarmRulesDraft,
   6205     farm_name_input: Entity<InputState>,
   6206     timezone_input: Entity<InputState>,
   6207     currency_input: Entity<InputState>,
   6208     pickup_locations: Vec<SettingsPickupLocationFormState>,
   6209     operating_rules: SettingsOperatingRulesFormState,
   6210     fulfillment_windows: Vec<SettingsFulfillmentWindowFormState>,
   6211     blackout_periods: Vec<SettingsBlackoutPeriodFormState>,
   6212     _farm_name_subscription: Subscription,
   6213     _timezone_subscription: Subscription,
   6214     _currency_subscription: Subscription,
   6215     save_failed: bool,
   6216 }
   6217 
   6218 impl SettingsFarmPanelState {
   6219     fn new(
   6220         account_id: String,
   6221         projection: FarmRulesProjection,
   6222         window: &mut Window,
   6223         cx: &mut Context<SettingsWindowView>,
   6224     ) -> Self {
   6225         let farm_id = projection
   6226             .farm_profile
   6227             .as_ref()
   6228             .map(|farm_profile| farm_profile.farm_id)
   6229             .unwrap_or_else(FarmId::new);
   6230         let initial_draft = SettingsFarmRulesDraft::from_projection(farm_id, &projection);
   6231         let farm_name_input = cx.new(|cx| {
   6232             InputState::new(window, cx)
   6233                 .default_value(initial_draft.farm_profile.display_name.clone())
   6234         });
   6235         let timezone_input = cx.new(|cx| {
   6236             InputState::new(window, cx).default_value(initial_draft.farm_profile.timezone.clone())
   6237         });
   6238         let currency_input = cx.new(|cx| {
   6239             InputState::new(window, cx)
   6240                 .default_value(initial_draft.farm_profile.currency_code.clone())
   6241         });
   6242         let farm_name_subscription = cx.subscribe_in(
   6243             &farm_name_input,
   6244             window,
   6245             SettingsWindowView::handle_farm_rules_input_event,
   6246         );
   6247         let timezone_subscription = cx.subscribe_in(
   6248             &timezone_input,
   6249             window,
   6250             SettingsWindowView::handle_farm_rules_input_event,
   6251         );
   6252         let currency_subscription = cx.subscribe_in(
   6253             &currency_input,
   6254             window,
   6255             SettingsWindowView::handle_farm_rules_input_event,
   6256         );
   6257         let pickup_locations = projection
   6258             .pickup_locations
   6259             .iter()
   6260             .map(|record| {
   6261                 let can_remove = projection.fulfillment_windows.iter().all(|window_record| {
   6262                     window_record.pickup_location_id != record.pickup_location_id
   6263                 });
   6264                 SettingsPickupLocationFormState::new(record, can_remove, window, cx)
   6265             })
   6266             .collect();
   6267         let operating_rules =
   6268             SettingsOperatingRulesFormState::new(projection.operating_rules.as_ref(), window, cx);
   6269         let fulfillment_windows = projection
   6270             .fulfillment_windows
   6271             .iter()
   6272             .map(|record| {
   6273                 SettingsFulfillmentWindowFormState::new(
   6274                     &SettingsFulfillmentWindowDraft::from_record(record),
   6275                     window,
   6276                     cx,
   6277                 )
   6278             })
   6279             .collect();
   6280         let blackout_periods = projection
   6281             .blackout_periods
   6282             .iter()
   6283             .map(|record| {
   6284                 SettingsBlackoutPeriodFormState::new(
   6285                     &SettingsBlackoutPeriodDraft::from_record(record),
   6286                     window,
   6287                     cx,
   6288                 )
   6289             })
   6290             .collect();
   6291         let mut state = Self {
   6292             account_id,
   6293             farm_id,
   6294             initial_draft,
   6295             farm_name_input,
   6296             timezone_input,
   6297             currency_input,
   6298             pickup_locations,
   6299             operating_rules,
   6300             fulfillment_windows,
   6301             blackout_periods,
   6302             _farm_name_subscription: farm_name_subscription,
   6303             _timezone_subscription: timezone_subscription,
   6304             _currency_subscription: currency_subscription,
   6305             save_failed: false,
   6306         };
   6307         state.sync_pickup_location_removability();
   6308         state
   6309     }
   6310 
   6311     fn add_pickup_location(&mut self, window: &mut Window, cx: &mut Context<SettingsWindowView>) {
   6312         let record = PickupLocationRecord {
   6313             pickup_location_id: PickupLocationId::new(),
   6314             farm_id: self.farm_id,
   6315             label: String::new(),
   6316             address_line: String::new(),
   6317             directions: None,
   6318             is_default: self.pickup_locations.is_empty(),
   6319         };
   6320         let pickup_location = SettingsPickupLocationFormState::new(&record, true, window, cx);
   6321 
   6322         self.pickup_locations.push(pickup_location);
   6323         self.sync_pickup_location_removability();
   6324         self.save_failed = false;
   6325     }
   6326 
   6327     fn set_default_pickup_location(&mut self, pickup_location_id: PickupLocationId) {
   6328         for pickup_location in &mut self.pickup_locations {
   6329             pickup_location.is_default = pickup_location.pickup_location_id == pickup_location_id;
   6330         }
   6331         self.save_failed = false;
   6332     }
   6333 
   6334     fn remove_pickup_location(&mut self, pickup_location_id: PickupLocationId) {
   6335         self.pickup_locations
   6336             .retain(|pickup_location| pickup_location.pickup_location_id != pickup_location_id);
   6337         if !self
   6338             .pickup_locations
   6339             .iter()
   6340             .any(|pickup_location| pickup_location.is_default)
   6341         {
   6342             if let Some(first_pickup_location) = self.pickup_locations.first_mut() {
   6343                 first_pickup_location.is_default = true;
   6344             }
   6345         }
   6346         self.sync_pickup_location_removability();
   6347         self.save_failed = false;
   6348     }
   6349 
   6350     fn add_fulfillment_window(
   6351         &mut self,
   6352         window: &mut Window,
   6353         cx: &mut Context<SettingsWindowView>,
   6354     ) {
   6355         let selected_pickup_location_id = self
   6356             .pickup_locations
   6357             .iter()
   6358             .find(|pickup_location| pickup_location.is_default)
   6359             .or_else(|| self.pickup_locations.first())
   6360             .map(|pickup_location| pickup_location.pickup_location_id);
   6361         let fulfillment_window = SettingsFulfillmentWindowFormState::new(
   6362             &SettingsFulfillmentWindowDraft {
   6363                 fulfillment_window_id: FulfillmentWindowId::new(),
   6364                 selected_pickup_location_id,
   6365                 label: String::new(),
   6366                 starts_at: String::new(),
   6367                 ends_at: String::new(),
   6368                 order_cutoff_at: String::new(),
   6369             },
   6370             window,
   6371             cx,
   6372         );
   6373 
   6374         self.fulfillment_windows.push(fulfillment_window);
   6375         self.sync_pickup_location_removability();
   6376         self.save_failed = false;
   6377     }
   6378 
   6379     fn select_fulfillment_window_pickup_location(
   6380         &mut self,
   6381         fulfillment_window_id: FulfillmentWindowId,
   6382         pickup_location_id: PickupLocationId,
   6383     ) {
   6384         if let Some(fulfillment_window) =
   6385             self.fulfillment_windows
   6386                 .iter_mut()
   6387                 .find(|fulfillment_window| {
   6388                     fulfillment_window.fulfillment_window_id == fulfillment_window_id
   6389                 })
   6390         {
   6391             fulfillment_window.selected_pickup_location_id = Some(pickup_location_id);
   6392             self.sync_pickup_location_removability();
   6393             self.save_failed = false;
   6394         }
   6395     }
   6396 
   6397     fn remove_fulfillment_window(&mut self, fulfillment_window_id: FulfillmentWindowId) {
   6398         self.fulfillment_windows.retain(|fulfillment_window| {
   6399             fulfillment_window.fulfillment_window_id != fulfillment_window_id
   6400         });
   6401         self.sync_pickup_location_removability();
   6402         self.save_failed = false;
   6403     }
   6404 
   6405     fn add_blackout_period(&mut self, window: &mut Window, cx: &mut Context<SettingsWindowView>) {
   6406         let blackout_period = SettingsBlackoutPeriodFormState::new(
   6407             &SettingsBlackoutPeriodDraft {
   6408                 blackout_period_id: BlackoutPeriodId::new(),
   6409                 label: String::new(),
   6410                 starts_at: String::new(),
   6411                 ends_at: String::new(),
   6412             },
   6413             window,
   6414             cx,
   6415         );
   6416 
   6417         self.blackout_periods.push(blackout_period);
   6418         self.save_failed = false;
   6419     }
   6420 
   6421     fn remove_blackout_period(&mut self, blackout_period_id: BlackoutPeriodId) {
   6422         self.blackout_periods
   6423             .retain(|blackout_period| blackout_period.blackout_period_id != blackout_period_id);
   6424         self.save_failed = false;
   6425     }
   6426 
   6427     fn current_draft(&self, cx: &App) -> SettingsFarmRulesDraft {
   6428         SettingsFarmRulesDraft {
   6429             farm_profile: FarmProfileRecord {
   6430                 farm_id: self.farm_id,
   6431                 display_name: self.farm_name_input.read(cx).value().to_string(),
   6432                 timezone: self.timezone_input.read(cx).value().to_string(),
   6433                 currency_code: self.currency_input.read(cx).value().to_string(),
   6434             },
   6435             pickup_locations: self
   6436                 .pickup_locations
   6437                 .iter()
   6438                 .map(|pickup_location| pickup_location.current_draft(cx))
   6439                 .collect(),
   6440             operating_rules: self.operating_rules.current_draft(cx),
   6441             fulfillment_windows: self
   6442                 .fulfillment_windows
   6443                 .iter()
   6444                 .map(|fulfillment_window| fulfillment_window.current_draft(cx))
   6445                 .collect(),
   6446             blackout_periods: self
   6447                 .blackout_periods
   6448                 .iter()
   6449                 .map(|blackout_period| blackout_period.current_draft(cx))
   6450                 .collect(),
   6451         }
   6452     }
   6453 
   6454     fn evaluate(&self, cx: &App) -> SettingsFarmRulesEvaluation {
   6455         let draft = self.current_draft(cx);
   6456         let farm_profile = FarmProfileRecord {
   6457             farm_id: self.farm_id,
   6458             display_name: draft.farm_profile.display_name.trim().to_owned(),
   6459             timezone: draft.farm_profile.timezone.trim().to_owned(),
   6460             currency_code: draft.farm_profile.currency_code.trim().to_owned(),
   6461         };
   6462         let pickup_locations = draft
   6463             .pickup_locations
   6464             .clone()
   6465             .into_iter()
   6466             .map(|pickup_location| pickup_location.into_record(self.farm_id))
   6467             .collect();
   6468         let mut operating_rules_validation_keys = Vec::new();
   6469         let operating_rules = if draft.operating_rules.is_empty() {
   6470             None
   6471         } else {
   6472             let promise_lead_hours = match draft
   6473                 .operating_rules
   6474                 .promise_lead_hours
   6475                 .trim()
   6476                 .parse::<u16>()
   6477             {
   6478                 Ok(promise_lead_hours) => promise_lead_hours,
   6479                 Err(_) if draft.operating_rules.promise_lead_hours.trim().is_empty() => 0,
   6480                 Err(_) => {
   6481                     push_unique_text_key(
   6482                         &mut operating_rules_validation_keys,
   6483                         AppTextKey::SettingsOperatingRulesInvalidPromiseLeadTime,
   6484                     );
   6485                     0
   6486                 }
   6487             };
   6488 
   6489             Some(FarmOperatingRulesRecord {
   6490                 farm_id: self.farm_id,
   6491                 promise_lead_hours,
   6492                 substitution_policy: draft.operating_rules.substitution_policy.trim().to_owned(),
   6493             })
   6494         };
   6495         let mut fulfillment_windows = Vec::new();
   6496         let mut fulfillment_window_validation_keys =
   6497             Vec::with_capacity(draft.fulfillment_windows.len());
   6498         for fulfillment_window in &draft.fulfillment_windows {
   6499             let label = fulfillment_window.label.trim().to_owned();
   6500             let starts_at = fulfillment_window.starts_at.trim().to_owned();
   6501             let ends_at = fulfillment_window.ends_at.trim().to_owned();
   6502             let order_cutoff_at = fulfillment_window.order_cutoff_at.trim().to_owned();
   6503             let mut row_validation_keys = Vec::new();
   6504             let missing_required_fields = label.is_empty()
   6505                 || starts_at.is_empty()
   6506                 || ends_at.is_empty()
   6507                 || order_cutoff_at.is_empty();
   6508 
   6509             if missing_required_fields {
   6510                 push_unique_text_key(
   6511                     &mut row_validation_keys,
   6512                     AppTextKey::SettingsFulfillmentWindowsValidationCompleteBeforeSave,
   6513                 );
   6514             } else if fulfillment_window.selected_pickup_location_id.is_none() {
   6515                 push_unique_text_key(
   6516                     &mut row_validation_keys,
   6517                     AppTextKey::SettingsFulfillmentWindowsValidationChoosePickupLocation,
   6518                 );
   6519             }
   6520 
   6521             if let Some(pickup_location_id) = fulfillment_window.selected_pickup_location_id {
   6522                 if !missing_required_fields {
   6523                     if ends_at <= starts_at {
   6524                         push_unique_text_key(
   6525                             &mut row_validation_keys,
   6526                             AppTextKey::SettingsReadinessFieldFulfillmentWindowEndsBeforeStart,
   6527                         );
   6528                     }
   6529                     if order_cutoff_at >= starts_at {
   6530                         push_unique_text_key(
   6531                             &mut row_validation_keys,
   6532                             AppTextKey::SettingsReadinessFieldFulfillmentWindowCutoffAfterStart,
   6533                         );
   6534                     }
   6535                     fulfillment_windows.push(FulfillmentWindowRecord {
   6536                         fulfillment_window_id: fulfillment_window.fulfillment_window_id,
   6537                         farm_id: self.farm_id,
   6538                         pickup_location_id,
   6539                         label,
   6540                         starts_at,
   6541                         ends_at,
   6542                         order_cutoff_at,
   6543                     });
   6544                 }
   6545             }
   6546 
   6547             fulfillment_window_validation_keys.push(row_validation_keys);
   6548         }
   6549         let mut blackout_periods = Vec::new();
   6550         let mut blackout_period_validation_keys = Vec::with_capacity(draft.blackout_periods.len());
   6551         for blackout_period in &draft.blackout_periods {
   6552             let label = blackout_period.label.trim().to_owned();
   6553             let starts_at = blackout_period.starts_at.trim().to_owned();
   6554             let ends_at = blackout_period.ends_at.trim().to_owned();
   6555             let mut row_validation_keys = Vec::new();
   6556 
   6557             if label.is_empty() || starts_at.is_empty() || ends_at.is_empty() {
   6558                 push_unique_text_key(
   6559                     &mut row_validation_keys,
   6560                     AppTextKey::SettingsBlackoutPeriodsValidationCompleteBeforeSave,
   6561                 );
   6562             } else {
   6563                 if ends_at <= starts_at {
   6564                     push_unique_text_key(
   6565                         &mut row_validation_keys,
   6566                         AppTextKey::SettingsReadinessFieldBlackoutPeriodEndsBeforeStart,
   6567                     );
   6568                 }
   6569                 blackout_periods.push(BlackoutPeriodRecord {
   6570                     blackout_period_id: blackout_period.blackout_period_id,
   6571                     farm_id: self.farm_id,
   6572                     label,
   6573                     starts_at,
   6574                     ends_at,
   6575                 });
   6576             }
   6577 
   6578             blackout_period_validation_keys.push(row_validation_keys);
   6579         }
   6580 
   6581         let mut projection = FarmRulesProjection {
   6582             farm_profile: Some(farm_profile),
   6583             pickup_locations,
   6584             operating_rules,
   6585             fulfillment_windows,
   6586             blackout_periods,
   6587             readiness: FarmRulesReadiness::ready(),
   6588         };
   6589         projection.readiness = derive_farm_rules_readiness(&projection);
   6590 
   6591         let mut blocking_keys = operating_rules_validation_keys.clone();
   6592         for row_validation_keys in &fulfillment_window_validation_keys {
   6593             for validation_key in row_validation_keys {
   6594                 push_unique_text_key(&mut blocking_keys, *validation_key);
   6595             }
   6596         }
   6597         for row_validation_keys in &blackout_period_validation_keys {
   6598             for validation_key in row_validation_keys {
   6599                 push_unique_text_key(&mut blocking_keys, *validation_key);
   6600             }
   6601         }
   6602         for timing_conflict in &projection.readiness.timing_conflicts {
   6603             push_unique_text_key(
   6604                 &mut blocking_keys,
   6605                 settings_timing_conflict_key(timing_conflict.kind),
   6606             );
   6607         }
   6608 
   6609         let mut readiness_keys = projection
   6610             .readiness
   6611             .blockers
   6612             .iter()
   6613             .copied()
   6614             .map(settings_readiness_key)
   6615             .collect::<Vec<_>>();
   6616         for blocking_key in &blocking_keys {
   6617             push_unique_text_key(&mut readiness_keys, *blocking_key);
   6618         }
   6619 
   6620         SettingsFarmRulesEvaluation {
   6621             projection,
   6622             operating_rules_validation_keys,
   6623             fulfillment_window_validation_keys,
   6624             blackout_period_validation_keys,
   6625             blocking_keys,
   6626             readiness_keys,
   6627         }
   6628     }
   6629 
   6630     fn current_projection(&self, cx: &App) -> FarmRulesProjection {
   6631         self.evaluate(cx).projection
   6632     }
   6633 
   6634     fn has_changes(&self, cx: &App) -> bool {
   6635         self.current_draft(cx) != self.initial_draft
   6636     }
   6637 
   6638     fn save_ready(&self, cx: &App) -> bool {
   6639         let evaluation = self.evaluate(cx);
   6640         self.has_changes(cx) && !evaluation.has_blocking_errors()
   6641     }
   6642 
   6643     fn save_status_key(&self, cx: &App) -> AppTextKey {
   6644         if self.save_failed {
   6645             AppTextKey::SettingsFarmSaveFailed
   6646         } else if self.has_changes(cx) {
   6647             let evaluation = self.evaluate(cx);
   6648             if evaluation.has_blocking_errors() {
   6649                 AppTextKey::SettingsFarmSaveBlocked
   6650             } else {
   6651                 AppTextKey::SettingsFarmSavePending
   6652             }
   6653         } else {
   6654             AppTextKey::SettingsFarmSaveSaved
   6655         }
   6656     }
   6657 
   6658     fn sync_pickup_location_removability(&mut self) {
   6659         let selected_pickup_location_ids = self
   6660             .fulfillment_windows
   6661             .iter()
   6662             .filter_map(|fulfillment_window| fulfillment_window.selected_pickup_location_id)
   6663             .collect::<Vec<_>>();
   6664 
   6665         for pickup_location in &mut self.pickup_locations {
   6666             pickup_location.can_remove =
   6667                 !selected_pickup_location_ids.contains(&pickup_location.pickup_location_id);
   6668         }
   6669     }
   6670 }
   6671 
   6672 pub struct SettingsWindowView {
   6673     runtime: DesktopAppRuntime,
   6674     farm_panel_state: Option<SettingsFarmPanelState>,
   6675     farm_panel_error: Option<String>,
   6676     about_panel_notice: Option<String>,
   6677 }
   6678 
   6679 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
   6680 enum SettingsAutoFocusTarget {
   6681     Navigation(SettingsPanelViewKey),
   6682     AccountAdd,
   6683     FarmNameInput,
   6684     AboutRefresh,
   6685 }
   6686 
   6687 fn settings_preferences_general_row_state(
   6688     runtime: &DesktopAppRuntimeSummary,
   6689 ) -> SettingsPreferencesGeneralRowState {
   6690     let general = &runtime.shell_projection.settings.general;
   6691     SettingsPreferencesGeneralRowState {
   6692         allow_relay_connections: general.allow_relay_connections,
   6693         use_media_servers: general.use_media_servers,
   6694         use_nip05: general.use_nip05,
   6695         launch_at_login: general.launch_at_login,
   6696     }
   6697 }
   6698 
   6699 impl SettingsWindowView {
   6700     pub fn new(runtime: DesktopAppRuntime, initial_view: SettingsPanelViewKey) -> Self {
   6701         let _ = initial_view;
   6702         Self {
   6703             runtime,
   6704             farm_panel_state: None,
   6705             farm_panel_error: None,
   6706             about_panel_notice: None,
   6707         }
   6708     }
   6709 
   6710     fn select_view(&mut self, view: SettingsPanelViewKey, cx: &mut Context<Self>) {
   6711         self.about_panel_notice = None;
   6712         if self.runtime.select_settings_section(view) {
   6713             cx.notify();
   6714         }
   6715     }
   6716 
   6717     fn selected_view(&self) -> SettingsPanelViewKey {
   6718         self.runtime.selected_settings_section()
   6719     }
   6720 
   6721     fn select_account(&mut self, account_id: String, cx: &mut Context<Self>) {
   6722         match self.runtime.select_local_account(account_id.as_str()) {
   6723             Ok(changed) => {
   6724                 if changed {
   6725                     cx.refresh_windows();
   6726                 }
   6727                 cx.notify();
   6728             }
   6729             Err(runtime_error) => {
   6730                 error!(
   6731                     target: "settings",
   6732                     event = "settings.account.select_failed",
   6733                     error = %runtime_error,
   6734                     "failed to select account from settings panel"
   6735                 );
   6736             }
   6737         }
   6738     }
   6739 
   6740     fn handle_farm_rules_input_event(
   6741         &mut self,
   6742         _: &Entity<InputState>,
   6743         event: &InputEvent,
   6744         _: &mut Window,
   6745         cx: &mut Context<Self>,
   6746     ) {
   6747         if !matches!(event, InputEvent::Change) {
   6748             return;
   6749         }
   6750 
   6751         if let Some(form) = self.farm_panel_state.as_mut() {
   6752             form.save_failed = false;
   6753         }
   6754 
   6755         cx.notify();
   6756     }
   6757 
   6758     fn sync_farm_panel_state(&mut self, window: &mut Window, cx: &mut Context<Self>) {
   6759         let runtime = self.runtime.summary();
   6760         let Some((account_id, farm_id)) = settings_panel_farm_context(&runtime) else {
   6761             self.farm_panel_state = None;
   6762             self.farm_panel_error = None;
   6763             return;
   6764         };
   6765 
   6766         if self
   6767             .farm_panel_state
   6768             .as_ref()
   6769             .is_some_and(|form| form.account_id == account_id && form.farm_id == farm_id)
   6770         {
   6771             return;
   6772         }
   6773 
   6774         match self.runtime.load_farm_rules_projection() {
   6775             Ok(projection) => {
   6776                 self.farm_panel_state = Some(SettingsFarmPanelState::new(
   6777                     account_id, projection, window, cx,
   6778                 ));
   6779                 self.farm_panel_error = None;
   6780             }
   6781             Err(runtime_error) => {
   6782                 error!(
   6783                     target: "settings",
   6784                     event = "settings.farm.load_failed",
   6785                     error = %runtime_error,
   6786                     "failed to load farm settings projection"
   6787                 );
   6788                 self.farm_panel_state = None;
   6789                 self.farm_panel_error = Some(runtime_error.to_string());
   6790             }
   6791         }
   6792     }
   6793 
   6794     fn add_pickup_location(&mut self, window: &mut Window, cx: &mut Context<Self>) {
   6795         let Some(form) = self.farm_panel_state.as_mut() else {
   6796             return;
   6797         };
   6798 
   6799         form.add_pickup_location(window, cx);
   6800         cx.notify();
   6801     }
   6802 
   6803     fn select_default_pickup_location(
   6804         &mut self,
   6805         pickup_location_id: PickupLocationId,
   6806         cx: &mut Context<Self>,
   6807     ) {
   6808         let Some(form) = self.farm_panel_state.as_mut() else {
   6809             return;
   6810         };
   6811 
   6812         form.set_default_pickup_location(pickup_location_id);
   6813         cx.notify();
   6814     }
   6815 
   6816     fn remove_pickup_location(
   6817         &mut self,
   6818         pickup_location_id: PickupLocationId,
   6819         cx: &mut Context<Self>,
   6820     ) {
   6821         let Some(form) = self.farm_panel_state.as_mut() else {
   6822             return;
   6823         };
   6824 
   6825         form.remove_pickup_location(pickup_location_id);
   6826         cx.notify();
   6827     }
   6828 
   6829     fn add_fulfillment_window(&mut self, window: &mut Window, cx: &mut Context<Self>) {
   6830         let Some(form) = self.farm_panel_state.as_mut() else {
   6831             return;
   6832         };
   6833 
   6834         form.add_fulfillment_window(window, cx);
   6835         cx.notify();
   6836     }
   6837 
   6838     fn select_fulfillment_window_pickup_location(
   6839         &mut self,
   6840         fulfillment_window_id: FulfillmentWindowId,
   6841         pickup_location_id: PickupLocationId,
   6842         cx: &mut Context<Self>,
   6843     ) {
   6844         let Some(form) = self.farm_panel_state.as_mut() else {
   6845             return;
   6846         };
   6847 
   6848         form.select_fulfillment_window_pickup_location(fulfillment_window_id, pickup_location_id);
   6849         cx.notify();
   6850     }
   6851 
   6852     fn remove_fulfillment_window(
   6853         &mut self,
   6854         fulfillment_window_id: FulfillmentWindowId,
   6855         cx: &mut Context<Self>,
   6856     ) {
   6857         let Some(form) = self.farm_panel_state.as_mut() else {
   6858             return;
   6859         };
   6860 
   6861         form.remove_fulfillment_window(fulfillment_window_id);
   6862         cx.notify();
   6863     }
   6864 
   6865     fn add_blackout_period(&mut self, window: &mut Window, cx: &mut Context<Self>) {
   6866         let Some(form) = self.farm_panel_state.as_mut() else {
   6867             return;
   6868         };
   6869 
   6870         form.add_blackout_period(window, cx);
   6871         cx.notify();
   6872     }
   6873 
   6874     fn remove_blackout_period(
   6875         &mut self,
   6876         blackout_period_id: BlackoutPeriodId,
   6877         cx: &mut Context<Self>,
   6878     ) {
   6879         let Some(form) = self.farm_panel_state.as_mut() else {
   6880             return;
   6881         };
   6882 
   6883         form.remove_blackout_period(blackout_period_id);
   6884         cx.notify();
   6885     }
   6886 
   6887     fn save_farm_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
   6888         let Some((current_projection, save_ready)) = self
   6889             .farm_panel_state
   6890             .as_ref()
   6891             .map(|form| (form.current_projection(cx), form.save_ready(cx)))
   6892         else {
   6893             return;
   6894         };
   6895         if !save_ready {
   6896             return;
   6897         }
   6898 
   6899         match self.runtime.save_farm_rules_projection(current_projection) {
   6900             Ok(saved_projection) => {
   6901                 let account_id = self
   6902                     .farm_panel_state
   6903                     .as_ref()
   6904                     .map(|form| form.account_id.clone())
   6905                     .unwrap_or_default();
   6906                 self.farm_panel_state = Some(SettingsFarmPanelState::new(
   6907                     account_id,
   6908                     saved_projection,
   6909                     window,
   6910                     cx,
   6911                 ));
   6912                 self.farm_panel_error = None;
   6913                 cx.notify();
   6914             }
   6915             Err(runtime_error) => {
   6916                 error!(
   6917                     target: "settings",
   6918                     event = "settings.farm.save_failed",
   6919                     error = %runtime_error,
   6920                     "failed to save farm settings projection"
   6921                 );
   6922                 if let Some(form) = self.farm_panel_state.as_mut() {
   6923                     form.save_failed = true;
   6924                 }
   6925                 cx.notify();
   6926             }
   6927         }
   6928     }
   6929 
   6930     fn refresh_about_sync(&mut self, cx: &mut Context<Self>) {
   6931         match self.runtime.sync_on_manual_refresh() {
   6932             Ok(changed) => {
   6933                 if changed {
   6934                     self.about_panel_notice = None;
   6935                     cx.refresh_windows();
   6936                 } else {
   6937                     self.about_panel_notice = Some(app_text(about_conflict_review_body_key(
   6938                         &self.runtime.summary().sync_status,
   6939                     )));
   6940                 }
   6941                 cx.notify();
   6942             }
   6943             Err(runtime_error) => {
   6944                 error!(
   6945                     target: "settings",
   6946                     event = "settings.about.sync_refresh_failed",
   6947                     error = %runtime_error,
   6948                     "failed to refresh sync from the about panel"
   6949                 );
   6950                 self.about_panel_notice = Some(runtime_error.to_string());
   6951                 cx.notify();
   6952             }
   6953         }
   6954     }
   6955 
   6956     fn resolve_about_conflict(
   6957         &mut self,
   6958         conflict_id: String,
   6959         resolution: SyncConflictResolutionStatus,
   6960         cx: &mut Context<Self>,
   6961     ) {
   6962         match self
   6963             .runtime
   6964             .resolve_sync_conflict(conflict_id.as_str(), resolution)
   6965         {
   6966             Ok(changed) => {
   6967                 if changed {
   6968                     self.about_panel_notice = None;
   6969                     cx.refresh_windows();
   6970                 } else {
   6971                     self.about_panel_notice = Some(app_text(about_conflict_review_body_key(
   6972                         &self.runtime.summary().sync_status,
   6973                     )));
   6974                 }
   6975                 cx.notify();
   6976             }
   6977             Err(runtime_error) => {
   6978                 error!(
   6979                     target: "settings",
   6980                     event = "settings.about.conflict_resolution_failed",
   6981                     conflict_id = %conflict_id,
   6982                     error = %runtime_error,
   6983                     "failed to resolve sync conflict from the about panel"
   6984                 );
   6985                 self.about_panel_notice = Some(runtime_error.to_string());
   6986                 cx.notify();
   6987             }
   6988         }
   6989     }
   6990 
   6991     fn about_conflict_card(
   6992         &mut self,
   6993         conflict_index: usize,
   6994         conflict: &DesktopAppSyncConflictSummary,
   6995         cx: &mut Context<Self>,
   6996     ) -> impl IntoElement {
   6997         let action_specs = about_conflict_action_specs(&conflict.conflict);
   6998 
   6999         app_surface_panel(
   7000             app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
   7001                 .w_full()
   7002                 .p(px(APP_UI_THEME.shells.home_card_padding_px))
   7003                 .child(app_text_value(about_conflict_aggregate_text(
   7004                     &conflict.conflict,
   7005                 )))
   7006                 .child(label_value_list(about_conflict_detail_rows(conflict)))
   7007                 .when(!action_specs.is_empty(), |this| {
   7008                     this.child(
   7009                         app_cluster(APP_UI_THEME.foundation.spacing.small_px)
   7010                             .w_full()
   7011                             .children(
   7012                                 action_specs
   7013                                     .into_iter()
   7014                                     .enumerate()
   7015                                     .map(|(action_index, (key, resolution))| {
   7016                                         action_button_compact(
   7017                                             (
   7018                                                 gpui::ElementId::from((
   7019                                                     "settings-about-conflict-action",
   7020                                                     conflict_index,
   7021                                                 )),
   7022                                                 action_index.to_string(),
   7023                                             ),
   7024                                             app_shared_text(key),
   7025                                             cx.listener({
   7026                                                 let conflict_id = conflict.conflict_id.clone();
   7027                                                 move |this, _, _, cx| {
   7028                                                     this.resolve_about_conflict(
   7029                                                         conflict_id.clone(),
   7030                                                         resolution,
   7031                                                         cx,
   7032                                                     )
   7033                                                 }
   7034                                             }),
   7035                                             cx,
   7036                                         )
   7037                                         .into_any_element()
   7038                                     })
   7039                                     .collect::<Vec<_>>(),
   7040                             ),
   7041                     )
   7042                 }),
   7043         )
   7044     }
   7045 
   7046     fn navigation_button(
   7047         &mut self,
   7048         view: SettingsPanelViewKey,
   7049         cx: &mut Context<Self>,
   7050     ) -> impl IntoElement {
   7051         let (navigation_id, navigation_icon) = settings_panel_spec(view);
   7052         icon_segment_button(
   7053             IconSegmentButtonSpec::new(
   7054                 navigation_id,
   7055                 app_shared_text(settings_panel_label_key(view)),
   7056                 navigation_icon,
   7057             ),
   7058             self.selected_view() == view,
   7059             cx.listener(move |this, _, _, cx| this.select_view(view, cx)),
   7060             cx,
   7061         )
   7062     }
   7063 
   7064     fn account_panel(&self, cx: &mut Context<Self>) -> impl IntoElement {
   7065         let runtime = self.runtime.summary();
   7066         let projection = &runtime.settings_account_projection;
   7067         let detail_text_px = APP_UI_THEME
   7068             .foundation
   7069             .typography
   7070             .settings_account_detail_text_px;
   7071         let detail_account = settings_account_detail_account(projection);
   7072         let selected_account_id = projection
   7073             .selected_account
   7074             .as_ref()
   7075             .map(|account| account.account.account_id.as_str());
   7076         let account_rows = projection
   7077             .roster
   7078             .iter()
   7079             .enumerate()
   7080             .map(|(index, account)| {
   7081                 let account_id = account.account_id.clone();
   7082                 let is_selected = selected_account_id
   7083                     .is_some_and(|selected_account_id| selected_account_id == account.account_id);
   7084 
   7085                 account_selector_row(
   7086                     ("settings-account-row", index),
   7087                     account_display_name(account),
   7088                     SharedString::from(abbreviated_npub(account.npub.as_str())),
   7089                     is_selected,
   7090                     cx.listener(move |this, _, _, cx| this.select_account(account_id.clone(), cx)),
   7091                     cx,
   7092                 )
   7093                 .into_any_element()
   7094             })
   7095             .collect::<Vec<_>>();
   7096 
   7097         div()
   7098             .size_full()
   7099             .flex()
   7100             .child(
   7101                 div()
   7102                     .h_full()
   7103                     .w(px(APP_UI_THEME.shells.settings_account_sidebar_width_px))
   7104                     .p(px(APP_UI_THEME.shells.settings_account_sidebar_padding_px))
   7105                     .flex()
   7106                     .flex_col()
   7107                     .justify_between()
   7108                     .child(
   7109                         app_stack_v(APP_UI_THEME.foundation.spacing.tight_px)
   7110                             .w_full()
   7111                             .rounded(px(APP_UI_THEME
   7112                                 .shells
   7113                                 .settings_account_sidebar_button_corner_radius_px))
   7114                             .children(account_rows)
   7115                             .when(projection.roster.is_empty(), |this| {
   7116                                 this.child(
   7117                                     div()
   7118                                         .flex()
   7119                                         .flex_col()
   7120                                         .gap(px(2.0))
   7121                                         .child(
   7122                                             div()
   7123                                                 .text_size(px(APP_UI_THEME
   7124                                                     .foundation
   7125                                                     .typography
   7126                                                     .settings_account_identity_text_px))
   7127                                                 .font_weight(gpui::FontWeight::MEDIUM)
   7128                                                 .text_color(rgb(
   7129                                                     APP_UI_THEME.foundation.text.primary,
   7130                                                 ))
   7131                                                 .child(app_shared_text(
   7132                                                     AppTextKey::SettingsAccountNoSelectionTitle,
   7133                                                 )),
   7134                                         )
   7135                                         .child(
   7136                                             div()
   7137                                                 .text_size(px(APP_UI_THEME
   7138                                                     .foundation
   7139                                                     .typography
   7140                                                     .settings_account_identity_text_px))
   7141                                                 .text_color(rgb(
   7142                                                     APP_UI_THEME.foundation.text.secondary,
   7143                                                 ))
   7144                                                 .line_height(relative(1.2))
   7145                                                 .child(app_shared_text(
   7146                                                     AppTextKey::SettingsAccountNoSelectionBody,
   7147                                                 )),
   7148                                         ),
   7149                                 )
   7150                             }),
   7151                     )
   7152                     .child(
   7153                         div()
   7154                             .w_full()
   7155                             .pt(px(APP_UI_THEME
   7156                                 .shells
   7157                                 .settings_account_sidebar_footer_padding_top_px))
   7158                             .flex()
   7159                             .flex_col()
   7160                             .gap(px(APP_UI_THEME
   7161                                 .shells
   7162                                 .settings_account_sidebar_footer_row_gap_px))
   7163                             .child(section_divider())
   7164                             .child(
   7165                                 div()
   7166                                     .w_full()
   7167                                     .flex()
   7168                                     .items_center()
   7169                                     .justify_between()
   7170                                     .gap(px(APP_UI_THEME
   7171                                         .shells
   7172                                         .settings_account_sidebar_footer_button_gap_px))
   7173                                     .child(action_button(
   7174                                         "account-add",
   7175                                         app_shared_text(AppTextKey::SettingsAccountAddAction),
   7176                                         cx.listener(|_, _, _, _| {}),
   7177                                         cx,
   7178                                     ))
   7179                                     .child(settings_account_more_actions_button(cx)),
   7180                             ),
   7181                     ),
   7182             )
   7183             .child(
   7184                 div()
   7185                     .h_full()
   7186                     .w(px(APP_UI_THEME.foundation.borders.divider_thickness_px))
   7187                     .bg(rgb(APP_UI_THEME.foundation.surfaces.divider)),
   7188             )
   7189             .child(
   7190                 div()
   7191                     .flex_1()
   7192                     .h_full()
   7193                     .p(px(APP_UI_THEME.shells.settings_account_main_padding_px))
   7194                     .flex()
   7195                     .flex_col()
   7196                     .items_center()
   7197                     .justify_start()
   7198                     .child(
   7199                         div()
   7200                             .w_full()
   7201                             .max_w(px(APP_UI_THEME
   7202                                 .shells
   7203                                 .settings_account_content_max_width_px))
   7204                             .flex()
   7205                             .flex_col()
   7206                             .items_start()
   7207                             .gap(px(APP_UI_THEME.shells.settings_account_main_stack_gap_px))
   7208                             .child(
   7209                                 div()
   7210                                     .w_full()
   7211                                     .flex()
   7212                                     .flex_col()
   7213                                     .items_center()
   7214                                     .gap(px(APP_UI_THEME.shells.settings_account_main_stack_gap_px))
   7215                                     .child(
   7216                                         div()
   7217                                             .size(px(APP_UI_THEME
   7218                                                 .shells
   7219                                                 .settings_account_profile_avatar_size_px))
   7220                                             .bg(rgb(APP_UI_THEME
   7221                                                 .foundation
   7222                                                 .surfaces
   7223                                                 .card_background))
   7224                                             .rounded(px(APP_UI_THEME
   7225                                                 .shells
   7226                                                 .settings_account_profile_avatar_size_px
   7227                                                 / 2.0)),
   7228                                     )
   7229                                     .child(
   7230                                         div()
   7231                                             .text_size(px(detail_text_px))
   7232                                             .font_weight(gpui::FontWeight::MEDIUM)
   7233                                             .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   7234                                             .child(
   7235                                                 detail_account
   7236                                                     .map(account_display_name)
   7237                                                     .unwrap_or_else(|| {
   7238                                                         app_shared_text(
   7239                                                             AppTextKey::SettingsAccountNoSelectionTitle,
   7240                                                         )
   7241                                                         .to_string()
   7242                                                     }),
   7243                                             ),
   7244                                     ),
   7245                             )
   7246                             .child(
   7247                                 div()
   7248                                     .w_full()
   7249                                     .flex()
   7250                                     .flex_col()
   7251                                     .gap(px(APP_UI_THEME.shells.settings_account_detail_row_gap_px))
   7252                                     .child(app_detail_row(
   7253                                         app_shared_label_text(
   7254                                             AppTextKey::SettingsAccountProfileLabel,
   7255                                         ),
   7256                                         div()
   7257                                             .text_size(px(detail_text_px))
   7258                                             .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   7259                                             .child(
   7260                                                 detail_account
   7261                                                     .map(account_display_name)
   7262                                                     .unwrap_or_else(|| {
   7263                                                         app_shared_text(AppTextKey::ValueNone)
   7264                                                             .to_string()
   7265                                                     }),
   7266                                             ),
   7267                                     ))
   7268                                     .child(app_detail_row(
   7269                                         app_shared_label_text(
   7270                                             AppTextKey::SettingsAccountStatusLabel,
   7271                                         ),
   7272                                         div()
   7273                                             .flex()
   7274                                             .items_center()
   7275                                             .gap(px(APP_UI_THEME
   7276                                                 .shells
   7277                                                 .settings_account_status_gap_px))
   7278                                             .child(status_indicator(settings_account_status_color(
   7279                                                 detail_account,
   7280                                                 selected_account_id,
   7281                                             )))
   7282                                             .child(
   7283                                                 div()
   7284                                                     .text_size(px(detail_text_px))
   7285                                                     .text_color(rgb(APP_UI_THEME
   7286                                                         .foundation
   7287                                                         .text
   7288                                                         .primary))
   7289                                                     .child(app_shared_text(
   7290                                                         settings_account_status_key(
   7291                                                             detail_account,
   7292                                                             selected_account_id,
   7293                                                         ),
   7294                                                     )),
   7295                                             ),
   7296                                     ))
   7297                                     .child(
   7298                                         div()
   7299                                             .w_full()
   7300                                             .flex()
   7301                                             .min_w_0()
   7302                                             .items_center()
   7303                                             .gap(px(APP_UI_THEME
   7304                                                 .shells
   7305                                                 .settings_account_action_row_gap_px))
   7306                                             .child(div().child(action_button(
   7307                                                 "account-log-out",
   7308                                                 app_shared_text(
   7309                                                     AppTextKey::SettingsAccountLogOutAction,
   7310                                                 ),
   7311                                                 cx.listener(|_, _, _, _| {}),
   7312                                                 cx,
   7313                                             )))
   7314                                             .child(div().child(action_button(
   7315                                                 "account-open-workspace",
   7316                                                 app_shared_text(
   7317                                                     AppTextKey::SettingsAccountOpenWorkspaceAction,
   7318                                                 ),
   7319                                                 cx.listener(|_, _, _, _| {}),
   7320                                                 cx,
   7321                                             ))),
   7322                                     )
   7323                                     .child(app_detail_row(
   7324                                         app_shared_label_text(
   7325                                             AppTextKey::SettingsAccountCustodyLabel,
   7326                                         ),
   7327                                         div()
   7328                                             .text_size(px(detail_text_px))
   7329                                             .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   7330                                             .child(app_shared_text(
   7331                                                 detail_account
   7332                                                     .map(|account| {
   7333                                                         account_custody_key(account.custody)
   7334                                                     })
   7335                                                     .unwrap_or(AppTextKey::ValueNone),
   7336                                             )),
   7337                                     ))
   7338                                     .child(app_detail_row(
   7339                                         app_shared_label_text(
   7340                                             AppTextKey::SettingsAccountSurfaceLabel,
   7341                                         ),
   7342                                         div()
   7343                                             .text_size(px(detail_text_px))
   7344                                             .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   7345                                             .child(app_shared_text(settings_account_surface_key(
   7346                                                 projection,
   7347                                                 detail_account,
   7348                                             ))),
   7349                                     ))
   7350                                     .child(app_detail_row(
   7351                                         app_shared_label_text(
   7352                                             AppTextKey::SettingsAccountActivationLabel,
   7353                                         ),
   7354                                         div()
   7355                                             .text_size(px(detail_text_px))
   7356                                             .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   7357                                             .child(app_shared_text(
   7358                                                 settings_account_activation_key(
   7359                                                     projection,
   7360                                                     detail_account,
   7361                                                 ),
   7362                                             )),
   7363                                     ))
   7364                                     .when(detail_account.is_none(), |this| {
   7365                                         this.child(home_body_text(app_shared_text(
   7366                                             AppTextKey::SettingsAccountNoSelectionBody,
   7367                                         )))
   7368                                     }),
   7369                             ),
   7370                     ),
   7371             )
   7372     }
   7373 
   7374     fn settings_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
   7375         self.sync_farm_panel_state(window, cx);
   7376         let runtime = self.runtime.summary();
   7377 
   7378         let mut cards = Vec::new();
   7379 
   7380         if let Some(error) = self.farm_panel_error.as_ref() {
   7381             cards.push(
   7382                 home_card(
   7383                     app_shared_text(AppTextKey::SettingsNavSettings),
   7384                     home_body_text(error.clone()),
   7385                 )
   7386                 .into_any_element(),
   7387             );
   7388         } else if let Some(form) = self.farm_panel_state.as_ref() {
   7389             let evaluation = form.evaluate(cx);
   7390             let save_ready = form.has_changes(cx) && !evaluation.has_blocking_errors();
   7391             let save_action = if save_ready {
   7392                 action_button_primary(
   7393                     "settings-farm-save",
   7394                     app_shared_text(AppTextKey::SettingsFarmSaveAction),
   7395                     cx.listener(|this, _, window, cx| this.save_farm_panel(window, cx)),
   7396                     cx,
   7397                 )
   7398                 .into_any_element()
   7399             } else {
   7400                 action_button_primary_disabled(
   7401                     "settings-farm-save",
   7402                     app_shared_text(AppTextKey::SettingsFarmSaveAction),
   7403                     cx,
   7404                 )
   7405                 .into_any_element()
   7406             };
   7407 
   7408             cards.push(
   7409                 home_card(
   7410                     app_shared_text(AppTextKey::SettingsOperatingRulesSectionLabel),
   7411                     app_stack_v(12.0)
   7412                         .w_full()
   7413                         .child(app_form_input_text(
   7414                             AppFormFieldSpec::new(
   7415                                 app_shared_text(
   7416                                     AppTextKey::SettingsOperatingRulesFieldPromiseLeadTime,
   7417                                 ),
   7418                                 Option::<SharedString>::None,
   7419                             ),
   7420                             &form.operating_rules.promise_lead_hours_input,
   7421                             false,
   7422                         ))
   7423                         .child(app_form_input_text(
   7424                             AppFormFieldSpec::new(
   7425                                 app_shared_text(
   7426                                     AppTextKey::SettingsOperatingRulesFieldSubstitutionPolicy,
   7427                                 ),
   7428                                 Option::<SharedString>::None,
   7429                             ),
   7430                             &form.operating_rules.substitution_policy_input,
   7431                             false,
   7432                         ))
   7433                         .children(
   7434                             evaluation
   7435                                 .operating_rules_validation_keys
   7436                                 .iter()
   7437                                 .copied()
   7438                                 .map(|key| home_body_text(app_shared_text(key)).into_any_element())
   7439                                 .collect::<Vec<_>>(),
   7440                         ),
   7441                 )
   7442                 .into_any_element(),
   7443             );
   7444             cards.push(
   7445                 home_card(
   7446                     app_shared_text(AppTextKey::SettingsFulfillmentWindowsSectionLabel),
   7447                     div()
   7448                         .w_full()
   7449                         .flex()
   7450                         .flex_col()
   7451                         .gap(px(12.0))
   7452                         .when(form.fulfillment_windows.is_empty(), |this| {
   7453                             this.child(home_body_text(app_shared_text(
   7454                                 AppTextKey::SettingsFulfillmentWindowsEmptyBody,
   7455                             )))
   7456                         })
   7457                         .when(form.pickup_locations.is_empty(), |this| {
   7458                             this.child(home_body_text(app_shared_text(
   7459                                 AppTextKey::SettingsFulfillmentWindowsPickupLocationsBody,
   7460                             )))
   7461                         })
   7462                         .children(
   7463                             form.fulfillment_windows
   7464                                 .iter()
   7465                                 .enumerate()
   7466                                 .map(|(index, fulfillment_window)| {
   7467                                     let fulfillment_window_id =
   7468                                         fulfillment_window.fulfillment_window_id;
   7469                                     let pickup_location_options = form
   7470                                         .pickup_locations
   7471                                         .iter()
   7472                                         .enumerate()
   7473                                         .map(|(pickup_index, pickup_location)| {
   7474                                             let pickup_location_id =
   7475                                                 pickup_location.pickup_location_id;
   7476                                             let is_selected = fulfillment_window
   7477                                                 .selected_pickup_location_id
   7478                                                 .is_some_and(|selected_pickup_location_id| {
   7479                                                     selected_pickup_location_id
   7480                                                         == pickup_location_id
   7481                                                 });
   7482                                             choice_button(
   7483                                                 (
   7484                                                     "settings-fulfillment-window-pickup-location",
   7485                                                     index * 100 + pickup_index,
   7486                                                 ),
   7487                                                 settings_pickup_location_title(
   7488                                                     pickup_index,
   7489                                                     pickup_location,
   7490                                                     cx,
   7491                                                 ),
   7492                                                 is_selected,
   7493                                                 cx.listener(move |this, _, _, cx| {
   7494                                                     this.select_fulfillment_window_pickup_location(
   7495                                                         fulfillment_window_id,
   7496                                                         pickup_location_id,
   7497                                                         cx,
   7498                                                     )
   7499                                                 }),
   7500                                                 cx,
   7501                                             )
   7502                                             .into_any_element()
   7503                                         })
   7504                                         .collect::<Vec<_>>();
   7505                                     let validation_keys = evaluation
   7506                                         .fulfillment_window_validation_keys
   7507                                         .get(index)
   7508                                         .cloned()
   7509                                         .unwrap_or_default();
   7510 
   7511                                     settings_fulfillment_window_card(
   7512                                         index,
   7513                                         fulfillment_window,
   7514                                         pickup_location_options,
   7515                                         &validation_keys,
   7516                                         cx.listener(move |this, _, _, cx| {
   7517                                             this.remove_fulfillment_window(
   7518                                                 fulfillment_window_id,
   7519                                                 cx,
   7520                                             )
   7521                                         }),
   7522                                         cx,
   7523                                     )
   7524                                     .into_any_element()
   7525                                 })
   7526                                 .collect::<Vec<_>>(),
   7527                         )
   7528                         .child(
   7529                             action_button_compact(
   7530                                 "settings-add-fulfillment-window",
   7531                                 app_shared_text(AppTextKey::SettingsFulfillmentWindowsAddAction),
   7532                                 cx.listener(|this, _, window, cx| {
   7533                                     this.add_fulfillment_window(window, cx)
   7534                                 }),
   7535                                 cx,
   7536                             )
   7537                             .into_any_element(),
   7538                         ),
   7539                 )
   7540                 .into_any_element(),
   7541             );
   7542             cards.push(
   7543                 home_card(
   7544                     app_shared_text(AppTextKey::SettingsBlackoutPeriodsSectionLabel),
   7545                     div()
   7546                         .w_full()
   7547                         .flex()
   7548                         .flex_col()
   7549                         .gap(px(12.0))
   7550                         .when(form.blackout_periods.is_empty(), |this| {
   7551                             this.child(home_body_text(app_shared_text(
   7552                                 AppTextKey::SettingsBlackoutPeriodsEmptyBody,
   7553                             )))
   7554                         })
   7555                         .children(
   7556                             form.blackout_periods
   7557                                 .iter()
   7558                                 .enumerate()
   7559                                 .map(|(index, blackout_period)| {
   7560                                     let blackout_period_id = blackout_period.blackout_period_id;
   7561                                     let validation_keys = evaluation
   7562                                         .blackout_period_validation_keys
   7563                                         .get(index)
   7564                                         .cloned()
   7565                                         .unwrap_or_default();
   7566 
   7567                                     settings_blackout_period_card(
   7568                                         index,
   7569                                         blackout_period,
   7570                                         &validation_keys,
   7571                                         cx.listener(move |this, _, _, cx| {
   7572                                             this.remove_blackout_period(blackout_period_id, cx)
   7573                                         }),
   7574                                         cx,
   7575                                     )
   7576                                     .into_any_element()
   7577                                 })
   7578                                 .collect::<Vec<_>>(),
   7579                         )
   7580                         .child(
   7581                             action_button_compact(
   7582                                 "settings-add-blackout-period",
   7583                                 app_shared_text(AppTextKey::SettingsBlackoutPeriodsAddAction),
   7584                                 cx.listener(|this, _, window, cx| {
   7585                                     this.add_blackout_period(window, cx)
   7586                                 }),
   7587                                 cx,
   7588                             )
   7589                             .into_any_element(),
   7590                         ),
   7591                 )
   7592                 .into_any_element(),
   7593             );
   7594             cards.push(
   7595                 home_card(
   7596                     app_shared_text(AppTextKey::SettingsReadinessSectionLabel),
   7597                     div()
   7598                         .w_full()
   7599                         .flex()
   7600                         .flex_col()
   7601                         .gap(px(12.0))
   7602                         .children(settings_farm_readiness_rows(&evaluation))
   7603                         .child(section_divider())
   7604                         .child(home_body_text(app_shared_text(form.save_status_key(cx))))
   7605                         .child(div().child(save_action)),
   7606                 )
   7607                 .into_any_element(),
   7608             );
   7609         } else {
   7610             cards.push(
   7611                 home_card(
   7612                     app_shared_text(AppTextKey::SettingsNavSettings),
   7613                     home_body_text(app_shared_text(AppTextKey::SettingsFarmUnavailableBody)),
   7614                 )
   7615                 .into_any_element(),
   7616             );
   7617         }
   7618 
   7619         cards.push(
   7620             home_card(
   7621                 app_shared_text(AppTextKey::SettingsGeneralSectionLabel),
   7622                 app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
   7623                     .w_full()
   7624                     .child(label_value_list(settings_preferences_general_rows(
   7625                         settings_preferences_general_row_state(&runtime),
   7626                     ))),
   7627             )
   7628             .into_any_element(),
   7629         );
   7630 
   7631         app_scroll_panel(
   7632             "settings-panel-scroll",
   7633             APP_UI_THEME.shells.settings_content_padding_px,
   7634             Some(APP_UI_THEME.shells.settings_panel_content_max_width_px),
   7635             app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   7636                 .w_full()
   7637                 .child(home_body_text(app_shared_text(
   7638                     AppTextKey::SettingsSettingsPanelBody,
   7639                 )))
   7640                 .children(cards),
   7641         )
   7642     }
   7643 
   7644     fn farm_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
   7645         self.sync_farm_panel_state(window, cx);
   7646 
   7647         let mut cards = Vec::new();
   7648 
   7649         if let Some(error) = self.farm_panel_error.as_ref() {
   7650             cards.push(
   7651                 home_card(
   7652                     app_shared_text(AppTextKey::SettingsNavFarm),
   7653                     home_body_text(error.clone()),
   7654                 )
   7655                 .into_any_element(),
   7656             );
   7657             return app_scroll_panel(
   7658                 "settings-panel-scroll",
   7659                 APP_UI_THEME.shells.settings_content_padding_px,
   7660                 Some(APP_UI_THEME.shells.settings_panel_content_max_width_px),
   7661                 app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   7662                     .w_full()
   7663                     .child(home_body_text(app_shared_text(
   7664                         AppTextKey::SettingsFarmPanelBody,
   7665                     )))
   7666                     .children(cards),
   7667             );
   7668         }
   7669 
   7670         let Some(form) = self.farm_panel_state.as_ref() else {
   7671             cards.push(
   7672                 home_card(
   7673                     app_shared_text(AppTextKey::SettingsNavFarm),
   7674                     home_body_text(app_shared_text(AppTextKey::SettingsFarmUnavailableBody)),
   7675                 )
   7676                 .into_any_element(),
   7677             );
   7678             return app_scroll_panel(
   7679                 "settings-panel-scroll",
   7680                 APP_UI_THEME.shells.settings_content_padding_px,
   7681                 Some(APP_UI_THEME.shells.settings_panel_content_max_width_px),
   7682                 app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   7683                     .w_full()
   7684                     .child(home_body_text(app_shared_text(
   7685                         AppTextKey::SettingsFarmPanelBody,
   7686                     )))
   7687                     .children(cards),
   7688             );
   7689         };
   7690 
   7691         let evaluation = form.evaluate(cx);
   7692         let save_action = if form.has_changes(cx) && !evaluation.has_blocking_errors() {
   7693             action_button_primary(
   7694                 "settings-farm-save",
   7695                 app_shared_text(AppTextKey::SettingsFarmSaveAction),
   7696                 cx.listener(|this, _, window, cx| this.save_farm_panel(window, cx)),
   7697                 cx,
   7698             )
   7699             .into_any_element()
   7700         } else {
   7701             action_button_primary_disabled(
   7702                 "settings-farm-save",
   7703                 app_shared_text(AppTextKey::SettingsFarmSaveAction),
   7704                 cx,
   7705             )
   7706             .into_any_element()
   7707         };
   7708 
   7709         cards.push(
   7710             home_card(
   7711                 app_shared_text(AppTextKey::HomeFarmSetupSectionFarm),
   7712                 app_stack_v(12.0)
   7713                     .w_full()
   7714                     .child(app_form_input_text(
   7715                         AppFormFieldSpec::new(
   7716                             app_shared_text(AppTextKey::HomeFarmSetupFieldFarmName),
   7717                             Option::<SharedString>::None,
   7718                         ),
   7719                         &form.farm_name_input,
   7720                         false,
   7721                     ))
   7722                     .child(app_form_input_text(
   7723                         AppFormFieldSpec::new(
   7724                             app_shared_text(AppTextKey::SettingsFarmFieldTimezone),
   7725                             Option::<SharedString>::None,
   7726                         ),
   7727                         &form.timezone_input,
   7728                         false,
   7729                     ))
   7730                     .child(app_form_input_text(
   7731                         AppFormFieldSpec::new(
   7732                             app_shared_text(AppTextKey::SettingsFarmFieldCurrency),
   7733                             Option::<SharedString>::None,
   7734                         ),
   7735                         &form.currency_input,
   7736                         false,
   7737                     )),
   7738             )
   7739             .into_any_element(),
   7740         );
   7741         cards.push(
   7742             home_card(
   7743                 app_shared_text(AppTextKey::SettingsPickupLocationsSectionLabel),
   7744                 div()
   7745                     .w_full()
   7746                     .flex()
   7747                     .flex_col()
   7748                     .gap(px(12.0))
   7749                     .when(form.pickup_locations.is_empty(), |this| {
   7750                         this.child(home_body_text(app_shared_text(
   7751                             AppTextKey::SettingsPickupLocationsEmptyBody,
   7752                         )))
   7753                     })
   7754                     .children(
   7755                         form.pickup_locations
   7756                             .iter()
   7757                             .enumerate()
   7758                             .map(|(index, pickup_location)| {
   7759                                 let pickup_location_id = pickup_location.pickup_location_id;
   7760                                 settings_pickup_location_card(
   7761                                     index,
   7762                                     pickup_location,
   7763                                     cx.listener(move |this, _, _, cx| {
   7764                                         this.select_default_pickup_location(pickup_location_id, cx)
   7765                                     }),
   7766                                     cx.listener(move |this, _, _, cx| {
   7767                                         this.remove_pickup_location(pickup_location_id, cx)
   7768                                     }),
   7769                                     cx,
   7770                                 )
   7771                                 .into_any_element()
   7772                             })
   7773                             .collect::<Vec<_>>(),
   7774                     )
   7775                     .child(
   7776                         action_button_compact(
   7777                             "settings-farm-add-pickup",
   7778                             app_shared_text(AppTextKey::SettingsPickupLocationsAddAction),
   7779                             cx.listener(|this, _, window, cx| this.add_pickup_location(window, cx)),
   7780                             cx,
   7781                         )
   7782                         .into_any_element(),
   7783                     )
   7784                     .child(section_divider())
   7785                     .child(home_body_text(app_shared_text(form.save_status_key(cx))))
   7786                     .child(div().child(save_action)),
   7787             )
   7788             .into_any_element(),
   7789         );
   7790 
   7791         app_scroll_panel(
   7792             "settings-panel-scroll",
   7793             APP_UI_THEME.shells.settings_content_padding_px,
   7794             Some(APP_UI_THEME.shells.settings_panel_content_max_width_px),
   7795             app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   7796                 .w_full()
   7797                 .child(home_body_text(app_shared_text(
   7798                     AppTextKey::SettingsFarmPanelBody,
   7799                 )))
   7800                 .children(cards),
   7801         )
   7802     }
   7803 
   7804     fn about_panel(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
   7805         let runtime = self.runtime.summary();
   7806         let sdk_diagnostics = self.runtime.sdk_diagnostics_summary();
   7807         let status_rows = about_status_rows(&runtime, sdk_diagnostics.as_ref());
   7808         let runtime_rows = about_runtime_rows(&runtime);
   7809         let manual_refresh_enabled = about_manual_refresh_enabled(&runtime.sync_status);
   7810         let conflict_cards = runtime
   7811             .sync_status
   7812             .conflicts
   7813             .iter()
   7814             .enumerate()
   7815             .map(|(conflict_index, conflict)| {
   7816                 self.about_conflict_card(conflict_index, conflict, cx)
   7817                     .into_any_element()
   7818             })
   7819             .collect::<Vec<_>>();
   7820 
   7821         app_scroll_panel(
   7822             "settings-panel-scroll",
   7823             APP_UI_THEME.shells.settings_content_padding_px,
   7824             None,
   7825             app_stack_v(APP_UI_THEME.shells.settings_account_main_stack_gap_px)
   7826                 .size_full()
   7827                 .py_12()
   7828                 .child(settings_about_product_section(cx))
   7829                 .child(app_surface_card(
   7830                     app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   7831                         .w_full()
   7832                         .child(app_heading_section(app_shared_text(
   7833                             AppTextKey::SettingsAboutStatusSectionLabel,
   7834                         )))
   7835                         .child(label_value_list(status_rows))
   7836                         .child(if manual_refresh_enabled {
   7837                             action_button_primary(
   7838                                 "settings-about-refresh-sync",
   7839                                 app_shared_text(AppTextKey::SettingsAboutRefreshAction),
   7840                                 cx.listener(|this, _, _, cx| this.refresh_about_sync(cx)),
   7841                                 cx,
   7842                             )
   7843                             .into_any_element()
   7844                         } else {
   7845                             action_button_primary_disabled(
   7846                                 "settings-about-refresh-sync-disabled",
   7847                                 app_shared_text(AppTextKey::SettingsAboutRefreshAction),
   7848                                 cx,
   7849                             )
   7850                             .into_any_element()
   7851                         }),
   7852                 ))
   7853                 .child(app_surface_card(
   7854                     app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   7855                         .w_full()
   7856                         .child(app_heading_section(app_shared_text(
   7857                             AppTextKey::SettingsAboutConflictReviewSectionLabel,
   7858                         )))
   7859                         .child(home_body_text(app_text(about_conflict_review_body_key(
   7860                             &runtime.sync_status,
   7861                         ))))
   7862                         .when_some(self.about_panel_notice.as_deref(), |this, notice| {
   7863                             this.child(home_body_text(notice.to_owned()))
   7864                         })
   7865                         .children(conflict_cards),
   7866                 ))
   7867                 .child(app_surface_card(
   7868                     app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   7869                         .w_full()
   7870                         .child(app_heading_section(app_shared_text(
   7871                             AppTextKey::SettingsAboutRuntimeSectionLabel,
   7872                         )))
   7873                         .child(label_value_list(runtime_rows)),
   7874                 )),
   7875         )
   7876     }
   7877 
   7878     fn settings_panel_content(
   7879         &mut self,
   7880         window: &mut Window,
   7881         cx: &mut Context<Self>,
   7882     ) -> AnyElement {
   7883         match self.selected_view() {
   7884             SettingsPanelViewKey::Account => self.account_panel(cx).into_any_element(),
   7885             SettingsPanelViewKey::Farm => self.farm_panel(window, cx).into_any_element(),
   7886             SettingsPanelViewKey::Settings => self.settings_panel(window, cx).into_any_element(),
   7887             SettingsPanelViewKey::About => self.about_panel(cx).into_any_element(),
   7888         }
   7889     }
   7890 
   7891     fn apply_auto_focus(&mut self, window: &mut Window, cx: &mut Context<Self>) {
   7892         let runtime = self.runtime.summary();
   7893         let desired_target = settings_auto_focus_target(
   7894             self.selected_view(),
   7895             self.farm_panel_state.as_ref(),
   7896             &runtime,
   7897         );
   7898         let focus_state = window.use_state(cx, |_, _| Option::<SettingsAutoFocusTarget>::None);
   7899         let should_focus = {
   7900             let last_target = focus_state.read(cx);
   7901             last_target.as_ref().copied() != desired_target
   7902         };
   7903 
   7904         if !should_focus {
   7905             return;
   7906         }
   7907 
   7908         if let Some(target) = desired_target {
   7909             match target {
   7910                 SettingsAutoFocusTarget::Navigation(view) => {
   7911                     let (navigation_id, _) = settings_panel_spec(view);
   7912                     focus_button(window, navigation_id, cx);
   7913                 }
   7914                 SettingsAutoFocusTarget::AccountAdd => {
   7915                     focus_button(window, "account-add", cx);
   7916                 }
   7917                 SettingsAutoFocusTarget::FarmNameInput => {
   7918                     if let Some(form) = self.farm_panel_state.as_ref() {
   7919                         form.farm_name_input
   7920                             .update(cx, |input, cx| input.focus(window, cx));
   7921                     }
   7922                 }
   7923                 SettingsAutoFocusTarget::AboutRefresh => {
   7924                     focus_button(window, "settings-about-refresh-sync", cx);
   7925                 }
   7926             }
   7927         }
   7928 
   7929         focus_state.update(cx, |last_target, _| *last_target = desired_target);
   7930     }
   7931 }
   7932 
   7933 fn settings_account_more_actions_button(cx: &App) -> impl IntoElement {
   7934     action_dropdown_button(
   7935         "account-more",
   7936         |menu, _, _| {
   7937             menu.item(
   7938                 PopupMenuItem::new(app_text(AppTextKey::SettingsAccountImportFileAction))
   7939                     .on_click(|_, _, _| {}),
   7940             )
   7941             .item(
   7942                 PopupMenuItem::new(app_text(AppTextKey::SettingsAccountImportDatabaseAction))
   7943                     .on_click(|_, _, _| {}),
   7944             )
   7945             .item(
   7946                 PopupMenuItem::new(app_text(
   7947                     AppTextKey::SettingsAccountConnectRemoteBunkerAction,
   7948                 ))
   7949                 .on_click(|_, _, _| {}),
   7950             )
   7951         },
   7952         cx,
   7953     )
   7954 }
   7955 
   7956 fn settings_about_product_section(cx: &mut Context<SettingsWindowView>) -> impl IntoElement {
   7957     let app_icon = Arc::new(Image::from_bytes(
   7958         ImageFormat::Png,
   7959         include_bytes!("../../../platforms/macos/App/Resources/AppIconSource.png").to_vec(),
   7960     ));
   7961     let version = format!(
   7962         "{} {}",
   7963         app_text(AppTextKey::SettingsAboutVersionLabel),
   7964         env!("CARGO_PKG_VERSION")
   7965     );
   7966 
   7967     div()
   7968         .w_full()
   7969         .flex()
   7970         .flex_col()
   7971         .items_center()
   7972         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
   7973         .child(
   7974             div()
   7975                 .w_full()
   7976                 .flex()
   7977                 .items_start()
   7978                 .justify_center()
   7979                 .gap(px(APP_UI_THEME.shells.settings_account_main_padding_px))
   7980                 .child(
   7981                     img(app_icon)
   7982                         .w(px(128.0))
   7983                         .h(px(128.0))
   7984                         .object_fit(ObjectFit::Contain)
   7985                         .flex_shrink_0(),
   7986                 )
   7987                 .child(
   7988                     app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
   7989                         .min_w_0()
   7990                         .child(
   7991                             div()
   7992                                 .text_size(
   7993                                     px(APP_UI_THEME.foundation.typography.body_text_px * 1.7),
   7994                                 )
   7995                                 .font_weight(gpui::FontWeight::SEMIBOLD)
   7996                                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   7997                                 .child(app_shared_text(AppTextKey::AppName)),
   7998                         )
   7999                         .child(
   8000                             div()
   8001                                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   8002                                 .font_weight(gpui::FontWeight::MEDIUM)
   8003                                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   8004                                 .child(version),
   8005                         )
   8006                         .child(
   8007                             div()
   8008                                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   8009                                 .font_weight(gpui::FontWeight::MEDIUM)
   8010                                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   8011                                 .child(app_shared_text(AppTextKey::SettingsAboutVariantLabel)),
   8012                         )
   8013                         .child(
   8014                             div()
   8015                                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   8016                                 .font_weight(gpui::FontWeight::MEDIUM)
   8017                                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   8018                                 .child(app_shared_text(AppTextKey::SettingsAboutCompanyName)),
   8019                         )
   8020                         .child(text_button(
   8021                             "settings-about-acknowledgements",
   8022                             app_shared_text(AppTextKey::SettingsAboutAcknowledgementsAction),
   8023                             cx.listener(|_, _, _, _| {}),
   8024                             cx,
   8025                         ))
   8026                         .child(text_button(
   8027                             "settings-about-privacy-policy",
   8028                             app_shared_text(AppTextKey::SettingsAboutPrivacyPolicyAction),
   8029                             cx.listener(|_, _, _, _| {}),
   8030                             cx,
   8031                         ))
   8032                         .child(text_button(
   8033                             "settings-about-terms",
   8034                             app_shared_text(AppTextKey::SettingsAboutTermsAction),
   8035                             cx.listener(|_, _, _, _| {}),
   8036                             cx,
   8037                         ))
   8038                         .child(action_button(
   8039                             "settings-about-report-issue",
   8040                             app_shared_text(AppTextKey::SettingsAboutReportIssueAction),
   8041                             cx.listener(|_, _, _, _| {}),
   8042                             cx,
   8043                         )),
   8044                 ),
   8045         )
   8046         .child(
   8047             app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
   8048                 .items_center()
   8049                 .child(
   8050                     div()
   8051                         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   8052                         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   8053                         .child(app_shared_text(AppTextKey::SettingsAboutCopyrightNotice)),
   8054                 )
   8055                 .child(
   8056                     div()
   8057                         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   8058                         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   8059                         .child(app_shared_text(AppTextKey::SettingsAboutTrademarkNotice)),
   8060                 ),
   8061         )
   8062 }
   8063 
   8064 fn settings_account_detail_account(
   8065     projection: &SettingsAccountProjection,
   8066 ) -> Option<&AccountSummary> {
   8067     projection
   8068         .selected_account
   8069         .as_ref()
   8070         .map(|selected_account| &selected_account.account)
   8071         .or_else(|| projection.roster.first())
   8072 }
   8073 
   8074 fn account_display_name(account: &AccountSummary) -> String {
   8075     account
   8076         .label
   8077         .as_deref()
   8078         .map(str::trim)
   8079         .filter(|label| !label.is_empty())
   8080         .map(ToOwned::to_owned)
   8081         .unwrap_or_else(|| abbreviated_npub(account.npub.as_str()))
   8082 }
   8083 
   8084 fn abbreviated_npub(npub: &str) -> String {
   8085     let trimmed = npub.trim();
   8086     if trimmed.chars().count() <= 20 {
   8087         return trimmed.to_owned();
   8088     }
   8089 
   8090     let prefix = trimmed.chars().take(10).collect::<String>();
   8091     let suffix = trimmed
   8092         .chars()
   8093         .rev()
   8094         .take(6)
   8095         .collect::<Vec<_>>()
   8096         .into_iter()
   8097         .rev()
   8098         .collect::<String>();
   8099     format!("{prefix}...{suffix}")
   8100 }
   8101 
   8102 fn settings_account_status_color(
   8103     account: Option<&AccountSummary>,
   8104     selected_account_id: Option<&str>,
   8105 ) -> u32 {
   8106     if settings_account_is_selected(account, selected_account_id) {
   8107         APP_UI_THEME.components.app_status_indicator.online
   8108     } else {
   8109         APP_UI_THEME.components.app_status_indicator.offline
   8110     }
   8111 }
   8112 
   8113 fn settings_account_status_key(
   8114     account: Option<&AccountSummary>,
   8115     selected_account_id: Option<&str>,
   8116 ) -> AppTextKey {
   8117     if settings_account_is_selected(account, selected_account_id) {
   8118         AppTextKey::SettingsAccountStatusLoggedIn
   8119     } else {
   8120         AppTextKey::SettingsAccountStatusLoggedOut
   8121     }
   8122 }
   8123 
   8124 fn settings_account_is_selected(
   8125     account: Option<&AccountSummary>,
   8126     selected_account_id: Option<&str>,
   8127 ) -> bool {
   8128     account
   8129         .zip(selected_account_id)
   8130         .is_some_and(|(account, selected_account_id)| account.account_id == selected_account_id)
   8131 }
   8132 
   8133 fn account_custody_key(custody: AccountCustody) -> AppTextKey {
   8134     match custody {
   8135         AccountCustody::LocalManaged => AppTextKey::SettingsAccountCustodyLocalManaged,
   8136         AccountCustody::BrowserSigner => AppTextKey::SettingsAccountCustodyBrowserSigner,
   8137         AccountCustody::RemoteSigner => AppTextKey::SettingsAccountCustodyRemoteSigner,
   8138     }
   8139 }
   8140 
   8141 fn settings_account_surface_key(
   8142     projection: &SettingsAccountProjection,
   8143     account: Option<&AccountSummary>,
   8144 ) -> AppTextKey {
   8145     projection
   8146         .selected_account
   8147         .as_ref()
   8148         .filter(|selected_account| {
   8149             account.is_some_and(|account| account.account_id == selected_account.account.account_id)
   8150         })
   8151         .map(|selected_account| active_surface_settings_key(selected_account.active_surface()))
   8152         .unwrap_or(AppTextKey::ValueNone)
   8153 }
   8154 
   8155 fn active_surface_settings_key(surface: ActiveSurface) -> AppTextKey {
   8156     match surface {
   8157         ActiveSurface::Personal => AppTextKey::SettingsAccountSurfacePersonal,
   8158         ActiveSurface::Farmer => AppTextKey::SettingsAccountSurfaceFarmer,
   8159     }
   8160 }
   8161 
   8162 fn settings_account_activation_key(
   8163     projection: &SettingsAccountProjection,
   8164     account: Option<&AccountSummary>,
   8165 ) -> AppTextKey {
   8166     if projection
   8167         .selected_account
   8168         .as_ref()
   8169         .filter(|selected_account| {
   8170             account.is_some_and(|account| account.account_id == selected_account.account.account_id)
   8171         })
   8172         .is_some_and(|selected_account| selected_account.farmer_activation.is_active())
   8173     {
   8174         AppTextKey::SettingsAccountActivationActive
   8175     } else {
   8176         AppTextKey::SettingsAccountActivationInactive
   8177     }
   8178 }
   8179 
   8180 fn about_status_rows(
   8181     runtime: &DesktopAppRuntimeSummary,
   8182     sdk_diagnostics: Option<&DesktopAppSdkDiagnosticsSummary>,
   8183 ) -> Vec<LabelValueRow> {
   8184     let mut rows = vec![
   8185         LabelValueRow::new(
   8186             app_shared_text(AppTextKey::MetadataSelectedAccount),
   8187             selected_account_label(runtime.sync_status.account_id.as_deref()),
   8188         ),
   8189         LabelValueRow::new(
   8190             app_shared_text(AppTextKey::MetadataSyncRunStatus),
   8191             about_sync_run_status_text(&runtime.sync_status),
   8192         ),
   8193         LabelValueRow::new(
   8194             app_shared_text(AppTextKey::MetadataSyncCheckpointState),
   8195             about_sync_checkpoint_state_text(&runtime.sync_status),
   8196         ),
   8197         LabelValueRow::new(
   8198             app_shared_text(AppTextKey::MetadataSyncPendingWriteCount),
   8199             runtime.sync_status.pending_write_count.to_string(),
   8200         ),
   8201         LabelValueRow::new(
   8202             app_shared_text(AppTextKey::MetadataSyncConflictCount),
   8203             runtime
   8204                 .sync_status
   8205                 .projection
   8206                 .conflict_status
   8207                 .unresolved_count
   8208                 .to_string(),
   8209         ),
   8210     ];
   8211 
   8212     if runtime
   8213         .sync_status
   8214         .projection
   8215         .conflict_status
   8216         .blocking_count
   8217         > 0
   8218     {
   8219         rows.push(LabelValueRow::new(
   8220             app_shared_text(AppTextKey::MetadataSyncBlockingConflictCount),
   8221             runtime
   8222                 .sync_status
   8223                 .projection
   8224                 .conflict_status
   8225                 .blocking_count
   8226                 .to_string(),
   8227         ));
   8228     }
   8229 
   8230     rows.push(LabelValueRow::new(
   8231         app_shared_text(AppTextKey::MetadataStartupIssue),
   8232         runtime
   8233             .startup_issue
   8234             .as_deref()
   8235             .map(startup_issue_summary_text)
   8236             .unwrap_or_else(|| app_text(AppTextKey::ValueNone)),
   8237     ));
   8238 
   8239     append_sdk_status_rows(&mut rows, runtime.sdk_status.as_ref(), sdk_diagnostics);
   8240 
   8241     rows
   8242 }
   8243 
   8244 fn append_sdk_status_rows(
   8245     rows: &mut Vec<LabelValueRow>,
   8246     sdk_status: Option<&DesktopAppSdkStatusSummary>,
   8247     sdk_diagnostics: Option<&DesktopAppSdkDiagnosticsSummary>,
   8248 ) {
   8249     let status = sdk_diagnostics
   8250         .map(|diagnostics| &diagnostics.status)
   8251         .or(sdk_status);
   8252     let Some(status) = status else {
   8253         rows.push(LabelValueRow::new(
   8254             app_shared_text(AppTextKey::MetadataSdkLifecycleState),
   8255             app_text(AppTextKey::ValueDisabled),
   8256         ));
   8257         rows.push(LabelValueRow::new(
   8258             app_shared_text(AppTextKey::MetadataSdkDiagnosticState),
   8259             app_text(AppTextKey::ValueSdkUnavailable),
   8260         ));
   8261         return;
   8262     };
   8263 
   8264     rows.push(LabelValueRow::new(
   8265         app_shared_text(AppTextKey::MetadataSdkLifecycleState),
   8266         sdk_lifecycle_state_text(status.lifecycle_state),
   8267     ));
   8268     rows.push(LabelValueRow::new(
   8269         app_shared_text(AppTextKey::MetadataSdkProjectionLifecycleState),
   8270         sdk_projection_lifecycle_state_text(status.projection_lifecycle_state),
   8271     ));
   8272     rows.push(LabelValueRow::new(
   8273         app_shared_text(AppTextKey::MetadataSdkRelayTargetCount),
   8274         status.relay_target_count.to_string(),
   8275     ));
   8276 
   8277     match sdk_diagnostics.map(|diagnostics| &diagnostics.state) {
   8278         Some(DesktopAppSdkDiagnosticsState::Ready(ready)) => {
   8279             append_ready_sdk_rows(rows, ready);
   8280             append_sdk_issue_rows(rows, status.last_issue.as_ref());
   8281         }
   8282         Some(DesktopAppSdkDiagnosticsState::Blocked(issue)) => {
   8283             rows.push(LabelValueRow::new(
   8284                 app_shared_text(AppTextKey::MetadataSdkDiagnosticState),
   8285                 app_text(AppTextKey::ValueSdkDiagnosticsBlocked),
   8286             ));
   8287             append_sdk_issue_rows(rows, Some(issue));
   8288         }
   8289         None => {
   8290             rows.push(LabelValueRow::new(
   8291                 app_shared_text(AppTextKey::MetadataSdkDiagnosticState),
   8292                 app_text(AppTextKey::ValueNone),
   8293             ));
   8294             append_sdk_issue_rows(rows, status.last_issue.as_ref());
   8295         }
   8296     }
   8297 }
   8298 
   8299 fn append_ready_sdk_rows(
   8300     rows: &mut Vec<LabelValueRow>,
   8301     ready: &DesktopAppSdkReadyDiagnosticsSummary,
   8302 ) {
   8303     rows.push(LabelValueRow::new(
   8304         app_shared_text(AppTextKey::MetadataSdkDiagnosticState),
   8305         app_text(AppTextKey::ValueSdkDiagnosticsReady),
   8306     ));
   8307     rows.push(LabelValueRow::new(
   8308         app_shared_text(AppTextKey::MetadataSdkStorageKind),
   8309         sdk_storage_kind_text(ready.storage_kind.as_str()),
   8310     ));
   8311     rows.push(LabelValueRow::new(
   8312         app_shared_text(AppTextKey::MetadataSdkEventCount),
   8313         ready.event_store_total_events.to_string(),
   8314     ));
   8315     rows.push(LabelValueRow::new(
   8316         app_shared_text(AppTextKey::MetadataSdkOutboxCount),
   8317         ready.outbox_total_events.to_string(),
   8318     ));
   8319     rows.push(LabelValueRow::new(
   8320         app_shared_text(AppTextKey::MetadataSdkOutboxPendingCount),
   8321         ready.outbox_pending_events.to_string(),
   8322     ));
   8323     rows.push(LabelValueRow::new(
   8324         app_shared_text(AppTextKey::MetadataSdkOutboxFailedCount),
   8325         ready.outbox_failed_terminal_events.to_string(),
   8326     ));
   8327     rows.push(LabelValueRow::new(
   8328         app_shared_text(AppTextKey::MetadataSdkIntegrityStatus),
   8329         sdk_integrity_status_text(ready.integrity_ok()),
   8330     ));
   8331     rows.push(LabelValueRow::new(
   8332         app_shared_text(AppTextKey::MetadataSdkSyncStatus),
   8333         app_text(AppTextKey::ValueSdkDiagnosticsReady),
   8334     ));
   8335 }
   8336 
   8337 fn append_sdk_issue_rows(rows: &mut Vec<LabelValueRow>, issue: Option<&DesktopAppSdkIssueSummary>) {
   8338     rows.push(LabelValueRow::new(
   8339         app_shared_text(AppTextKey::MetadataSdkLastIssueCode),
   8340         issue
   8341             .map(|issue| issue.code.clone())
   8342             .unwrap_or_else(|| app_text(AppTextKey::ValueNone)),
   8343     ));
   8344     if let Some(issue) = issue {
   8345         rows.push(LabelValueRow::new(
   8346             app_shared_text(AppTextKey::MetadataSdkLastIssueClass),
   8347             issue.class.clone(),
   8348         ));
   8349         rows.push(LabelValueRow::new(
   8350             app_shared_text(AppTextKey::MetadataSdkIssueRetryable),
   8351             yes_no_text(issue.retryable),
   8352         ));
   8353         rows.push(LabelValueRow::new(
   8354             app_shared_text(AppTextKey::MetadataSdkRecoveryAction),
   8355             sdk_recovery_actions_text(&issue.recovery_actions),
   8356         ));
   8357     }
   8358 }
   8359 
   8360 fn about_conflict_review_body_key(sync_status: &DesktopAppSyncStatusSummary) -> AppTextKey {
   8361     if !sync_status.is_enabled() {
   8362         AppTextKey::SettingsAboutConflictReviewUnavailable
   8363     } else if sync_status
   8364         .projection
   8365         .conflict_status
   8366         .has_blocking_conflicts()
   8367     {
   8368         AppTextKey::SettingsAboutConflictReviewBlocking
   8369     } else if sync_status.projection.conflict_status.requires_attention() {
   8370         AppTextKey::SettingsAboutConflictReviewNeedsAttention
   8371     } else {
   8372         AppTextKey::SettingsAboutConflictReviewClear
   8373     }
   8374 }
   8375 
   8376 fn about_manual_refresh_enabled(sync_status: &DesktopAppSyncStatusSummary) -> bool {
   8377     sync_status.is_enabled()
   8378         && !sync_status
   8379             .projection
   8380             .conflict_status
   8381             .has_blocking_conflicts()
   8382 }
   8383 
   8384 fn about_conflict_action_specs(
   8385     conflict: &SyncConflict,
   8386 ) -> Vec<(AppTextKey, SyncConflictResolutionStatus)> {
   8387     if !conflict.is_unresolved() {
   8388         return Vec::new();
   8389     }
   8390 
   8391     let mut actions = vec![
   8392         (
   8393             AppTextKey::SettingsAboutConflictAcceptLocalAction,
   8394             SyncConflictResolutionStatus::AcceptedLocal,
   8395         ),
   8396         (
   8397             AppTextKey::SettingsAboutConflictAcceptRemoteAction,
   8398             SyncConflictResolutionStatus::AcceptedRemote,
   8399         ),
   8400     ];
   8401     if !matches!(
   8402         conflict.severity,
   8403         radroots_app_sync::SyncConflictSeverity::Blocking
   8404     ) {
   8405         actions.push((
   8406             AppTextKey::SettingsAboutConflictDismissAction,
   8407             SyncConflictResolutionStatus::Dismissed,
   8408         ));
   8409     }
   8410 
   8411     actions
   8412 }
   8413 
   8414 fn about_conflict_detail_rows(conflict: &DesktopAppSyncConflictSummary) -> Vec<LabelValueRow> {
   8415     vec![
   8416         LabelValueRow::new(
   8417             app_shared_text(AppTextKey::MetadataSyncConflictAggregate),
   8418             about_conflict_aggregate_text(&conflict.conflict),
   8419         ),
   8420         LabelValueRow::new(
   8421             app_shared_text(AppTextKey::MetadataSyncConflictKind),
   8422             about_conflict_kind_text(&conflict.conflict),
   8423         ),
   8424         LabelValueRow::new(
   8425             app_shared_text(AppTextKey::MetadataSyncConflictSeverity),
   8426             about_conflict_severity_text(&conflict.conflict),
   8427         ),
   8428         LabelValueRow::new(
   8429             app_shared_text(AppTextKey::MetadataSyncConflictDetectedAt),
   8430             conflict.conflict.detected_at.clone(),
   8431         ),
   8432         LabelValueRow::new(
   8433             app_shared_text(AppTextKey::MetadataSyncConflictResolution),
   8434             about_conflict_resolution_text(&conflict.conflict),
   8435         ),
   8436     ]
   8437 }
   8438 
   8439 fn about_conflict_aggregate_text(conflict: &SyncConflict) -> String {
   8440     let (aggregate_kind_key, aggregate_id) = match &conflict.aggregate {
   8441         SyncAggregateRef::Farm(farm_id) => (
   8442             AppTextKey::ValueSyncConflictAggregateFarm,
   8443             farm_id.to_string(),
   8444         ),
   8445         SyncAggregateRef::FulfillmentWindow(fulfillment_window_id) => (
   8446             AppTextKey::ValueSyncConflictAggregateFulfillmentWindow,
   8447             fulfillment_window_id.to_string(),
   8448         ),
   8449         SyncAggregateRef::Product(product_id) => (
   8450             AppTextKey::ValueSyncConflictAggregateProduct,
   8451             product_id.to_string(),
   8452         ),
   8453         SyncAggregateRef::Order(order_id) => (
   8454             AppTextKey::ValueSyncConflictAggregateOrder,
   8455             order_id.to_string(),
   8456         ),
   8457     };
   8458 
   8459     format!("{}: {}", app_text(aggregate_kind_key), aggregate_id)
   8460 }
   8461 
   8462 fn about_conflict_kind_text(conflict: &SyncConflict) -> String {
   8463     app_text(match conflict.kind {
   8464         SyncConflictKind::RevisionMismatch => AppTextKey::ValueSyncConflictKindRevisionMismatch,
   8465         SyncConflictKind::RemoteDelete => AppTextKey::ValueSyncConflictKindRemoteDelete,
   8466         SyncConflictKind::RemoteValidationReject => {
   8467             AppTextKey::ValueSyncConflictKindRemoteValidationReject
   8468         }
   8469     })
   8470 }
   8471 
   8472 fn about_conflict_severity_text(conflict: &SyncConflict) -> String {
   8473     match conflict.severity {
   8474         SyncConflictSeverity::ReviewRequired => {
   8475             app_text(AppTextKey::ValueSyncConflictSeverityReviewRequired)
   8476         }
   8477         SyncConflictSeverity::Blocking => app_text(AppTextKey::ValueSyncConflictSeverityBlocking),
   8478     }
   8479 }
   8480 
   8481 fn about_conflict_resolution_text(conflict: &SyncConflict) -> String {
   8482     match conflict.resolution {
   8483         SyncConflictResolutionStatus::Unresolved => {
   8484             app_text(AppTextKey::ValueSyncConflictResolutionUnresolved)
   8485         }
   8486         SyncConflictResolutionStatus::AcceptedLocal => {
   8487             app_text(AppTextKey::ValueSyncConflictResolutionAcceptedLocal)
   8488         }
   8489         SyncConflictResolutionStatus::AcceptedRemote => {
   8490             app_text(AppTextKey::ValueSyncConflictResolutionAcceptedRemote)
   8491         }
   8492         SyncConflictResolutionStatus::Dismissed => {
   8493             app_text(AppTextKey::ValueSyncConflictResolutionDismissed)
   8494         }
   8495     }
   8496 }
   8497 
   8498 fn about_runtime_rows(runtime: &DesktopAppRuntimeSummary) -> Vec<LabelValueRow> {
   8499     let mut rows = runtime_metadata_rows(&runtime.runtime_metadata.snapshot);
   8500     rows.push(LabelValueRow::new(
   8501         app_shared_text(AppTextKey::MetadataDataRoot),
   8502         path_or_none(runtime.runtime_metadata.data_root.as_ref()),
   8503     ));
   8504     rows.push(LabelValueRow::new(
   8505         app_shared_text(AppTextKey::MetadataLogsRoot),
   8506         path_or_none(runtime.runtime_metadata.logs_root.as_ref()),
   8507     ));
   8508     rows.push(LabelValueRow::new(
   8509         app_shared_text(AppTextKey::MetadataDatabasePath),
   8510         path_or_none(runtime.runtime_metadata.database_path.as_ref()),
   8511     ));
   8512     rows.push(LabelValueRow::new(
   8513         app_shared_text(AppTextKey::MetadataDatabaseSchemaVersion),
   8514         runtime
   8515             .runtime_metadata
   8516             .database_schema_version
   8517             .map(|version| version.to_string())
   8518             .unwrap_or_else(|| app_text(AppTextKey::ValueNone)),
   8519     ));
   8520     rows.push(LabelValueRow::new(
   8521         app_shared_text(AppTextKey::MetadataShellSection),
   8522         runtime
   8523             .shell_projection
   8524             .selected_section
   8525             .storage_key()
   8526             .to_owned(),
   8527     ));
   8528     if let Some(sdk_status) = runtime.sdk_status.as_ref() {
   8529         rows.push(LabelValueRow::new(
   8530             app_shared_text(AppTextKey::MetadataSdkStorageRoot),
   8531             sdk_status.storage_root.display().to_string(),
   8532         ));
   8533         rows.push(LabelValueRow::new(
   8534             app_shared_text(AppTextKey::MetadataSdkEventStorePath),
   8535             path_or_none(sdk_status.event_store_path.as_ref()),
   8536         ));
   8537         rows.push(LabelValueRow::new(
   8538             app_shared_text(AppTextKey::MetadataSdkOutboxPath),
   8539             path_or_none(sdk_status.outbox_path.as_ref()),
   8540         ));
   8541         rows.push(LabelValueRow::new(
   8542             app_shared_text(AppTextKey::MetadataSdkRelayUrlPolicy),
   8543             sdk_relay_url_policy_text(sdk_status.relay_url_policy),
   8544         ));
   8545     }
   8546     rows
   8547 }
   8548 
   8549 fn selected_account_label(account_id: Option<&str>) -> String {
   8550     account_id
   8551         .map(ToOwned::to_owned)
   8552         .unwrap_or_else(|| app_text(AppTextKey::ValueNone))
   8553 }
   8554 
   8555 fn about_sync_run_status_text(sync_status: &DesktopAppSyncStatusSummary) -> String {
   8556     if !sync_status.is_enabled() {
   8557         return app_text(AppTextKey::ValueDisabled);
   8558     }
   8559 
   8560     match sync_status.projection.run_status {
   8561         AppSyncRunStatus::Idle => app_text(AppTextKey::ValueSyncRunStatusIdle),
   8562         AppSyncRunStatus::Syncing => app_text(AppTextKey::ValueSyncRunStatusSyncing),
   8563         AppSyncRunStatus::Succeeded => app_text(AppTextKey::ValueSyncRunStatusSucceeded),
   8564         AppSyncRunStatus::Conflicted => app_text(AppTextKey::ValueSyncRunStatusConflicted),
   8565         AppSyncRunStatus::Failed => app_text(AppTextKey::ValueSyncRunStatusFailed),
   8566     }
   8567 }
   8568 
   8569 fn about_sync_checkpoint_state_text(sync_status: &DesktopAppSyncStatusSummary) -> String {
   8570     if !sync_status.is_enabled() {
   8571         return app_text(AppTextKey::ValueNone);
   8572     }
   8573 
   8574     match sync_status.projection.checkpoint.state {
   8575         SyncCheckpointState::NeverSynced => app_text(AppTextKey::ValueSyncCheckpointNeverSynced),
   8576         SyncCheckpointState::Syncing => app_text(AppTextKey::ValueSyncCheckpointSyncing),
   8577         SyncCheckpointState::Current => app_text(AppTextKey::ValueSyncCheckpointCurrent),
   8578         SyncCheckpointState::Failed => app_text(AppTextKey::ValueSyncCheckpointFailed),
   8579     }
   8580 }
   8581 
   8582 fn path_or_none(path: Option<&PathBuf>) -> String {
   8583     path.map(|value| value.display().to_string())
   8584         .unwrap_or_else(|| app_text(AppTextKey::ValueNone))
   8585 }
   8586 
   8587 fn sdk_lifecycle_state_text(state: AppSdkLifecycleState) -> String {
   8588     app_text(match state {
   8589         AppSdkLifecycleState::Starting => AppTextKey::ValueSdkLifecycleStarting,
   8590         AppSdkLifecycleState::Ready => AppTextKey::ValueSdkLifecycleReady,
   8591         AppSdkLifecycleState::Degraded => AppTextKey::ValueSdkLifecycleDegraded,
   8592         AppSdkLifecycleState::Pausing => AppTextKey::ValueSdkLifecyclePausing,
   8593         AppSdkLifecycleState::Paused => AppTextKey::ValueSdkLifecyclePaused,
   8594         AppSdkLifecycleState::Restoring => AppTextKey::ValueSdkLifecycleRestoring,
   8595         AppSdkLifecycleState::RebuildingProjections => {
   8596             AppTextKey::ValueSdkLifecycleRebuildingProjections
   8597         }
   8598         AppSdkLifecycleState::ShuttingDown => AppTextKey::ValueSdkLifecycleShuttingDown,
   8599         AppSdkLifecycleState::Stopped => AppTextKey::ValueSdkLifecycleStopped,
   8600     })
   8601 }
   8602 
   8603 fn sdk_projection_lifecycle_state_text(state: AppSdkProjectionLifecycleState) -> String {
   8604     app_text(match state {
   8605         AppSdkProjectionLifecycleState::Current => AppTextKey::ValueSdkProjectionCurrent,
   8606         AppSdkProjectionLifecycleState::Stale => AppTextKey::ValueSdkProjectionStale,
   8607         AppSdkProjectionLifecycleState::Rebuilding => AppTextKey::ValueSdkProjectionRebuilding,
   8608     })
   8609 }
   8610 
   8611 fn sdk_relay_url_policy_text(policy: AppSdkRelayUrlPolicy) -> String {
   8612     app_text(match policy {
   8613         AppSdkRelayUrlPolicy::Public => AppTextKey::ValueSdkRelayPolicyPublic,
   8614         AppSdkRelayUrlPolicy::Localhost => AppTextKey::ValueSdkRelayPolicyLocalhost,
   8615     })
   8616 }
   8617 
   8618 fn sdk_storage_kind_text(kind: &str) -> String {
   8619     match kind {
   8620         "directory" => app_text(AppTextKey::ValueSdkStorageKindDirectory),
   8621         _ => app_text(AppTextKey::ValueSdkStorageKindUnknown),
   8622     }
   8623 }
   8624 
   8625 fn sdk_integrity_status_text(ok: bool) -> String {
   8626     if ok {
   8627         app_text(AppTextKey::ValueSdkIntegrityOk)
   8628     } else {
   8629         app_text(AppTextKey::ValueSdkIntegrityFailed)
   8630     }
   8631 }
   8632 
   8633 fn yes_no_text(value: bool) -> String {
   8634     if value {
   8635         app_text(AppTextKey::ValueYes)
   8636     } else {
   8637         app_text(AppTextKey::ValueNo)
   8638     }
   8639 }
   8640 
   8641 fn sdk_recovery_actions_text(actions: &[String]) -> String {
   8642     if actions.is_empty() {
   8643         return app_text(AppTextKey::ValueNone);
   8644     }
   8645 
   8646     actions
   8647         .iter()
   8648         .map(|action| app_text(sdk_recovery_action_key(action)))
   8649         .collect::<Vec<_>>()
   8650         .join(", ")
   8651 }
   8652 
   8653 fn sdk_recovery_action_key(action: &str) -> AppTextKey {
   8654     match action {
   8655         "configure_relay_targets" => AppTextKey::ValueSdkRecoveryConfigureRelayTargets,
   8656         "retry_startup" => AppTextKey::ValueSdkRecoveryRetryStartup,
   8657         "wait_for_sdk_lifecycle" => AppTextKey::ValueSdkRecoveryWaitForLifecycle,
   8658         "retry_status_refresh" => AppTextKey::ValueSdkRecoveryRetryStatusRefresh,
   8659         "review_runtime_configuration" => AppTextKey::ValueSdkRecoveryReviewRuntimeConfiguration,
   8660         _ => AppTextKey::ValueSdkRecoveryReviewStatus,
   8661     }
   8662 }
   8663 
   8664 fn focus_button<V>(window: &mut Window, id: impl Into<ElementId>, cx: &mut Context<V>) {
   8665     let focus_handle = window
   8666         .use_keyed_state(id, cx, |_, cx| cx.focus_handle())
   8667         .read(cx)
   8668         .clone();
   8669     focus_handle.focus(window);
   8670 }
   8671 
   8672 fn home_auto_focus_target(
   8673     runtime: &DesktopAppRuntimeSummary,
   8674     state: HomeAutoFocusState,
   8675 ) -> Option<HomeAutoFocusTarget> {
   8676     match home_stage(runtime) {
   8677         HomeStage::Setup => startup_auto_focus_target(runtime, state),
   8678         HomeStage::AccountWorkspace => None,
   8679         HomeStage::BuyerWorkspace => buyer_auto_focus_target(runtime, state),
   8680         HomeStage::FarmerWorkspace => farmer_auto_focus_target(runtime, state),
   8681     }
   8682 }
   8683 
   8684 fn startup_auto_focus_target(
   8685     runtime: &DesktopAppRuntimeSummary,
   8686     state: HomeAutoFocusState,
   8687 ) -> Option<HomeAutoFocusTarget> {
   8688     match startup_home_surface(runtime) {
   8689         StartupHomeSurface::ContinuePrompt => Some(HomeAutoFocusTarget::StartupContinue),
   8690         StartupHomeSurface::IdentityChoice => Some(HomeAutoFocusTarget::StartupGenerateKey),
   8691         StartupHomeSurface::GenerateKeyStarting | StartupHomeSurface::IssueCard => None,
   8692         StartupHomeSurface::SignerEntry => {
   8693             if state.has_startup_signer_input && state.startup_signer_input_is_editable {
   8694                 Some(HomeAutoFocusTarget::StartupSignerInput)
   8695             } else {
   8696                 Some(HomeAutoFocusTarget::StartupSignerBack)
   8697             }
   8698         }
   8699     }
   8700 }
   8701 
   8702 fn buyer_auto_focus_target(
   8703     runtime: &DesktopAppRuntimeSummary,
   8704     state: HomeAutoFocusState,
   8705 ) -> Option<HomeAutoFocusTarget> {
   8706     match selected_personal_section(runtime) {
   8707         PersonalSection::Browse => {
   8708             if runtime.personal_projection.browse.detail.is_some() {
   8709                 Some(HomeAutoFocusTarget::BuyerDetailBack)
   8710             } else if !runtime.personal_projection.browse.listings.rows.is_empty() {
   8711                 Some(HomeAutoFocusTarget::BuyerListingOpenFirst)
   8712             } else {
   8713                 None
   8714             }
   8715         }
   8716         PersonalSection::Search => {
   8717             if runtime.personal_projection.search.detail.is_some() {
   8718                 Some(HomeAutoFocusTarget::BuyerDetailBack)
   8719             } else if state.has_personal_search_input {
   8720                 Some(HomeAutoFocusTarget::BuyerSearchInput)
   8721             } else if !runtime.personal_projection.search.listings.rows.is_empty() {
   8722                 Some(HomeAutoFocusTarget::BuyerListingOpenFirst)
   8723             } else {
   8724                 None
   8725             }
   8726         }
   8727         PersonalSection::Cart => {
   8728             if state.has_buyer_order_review_form {
   8729                 Some(HomeAutoFocusTarget::BuyerOrderReviewNameInput)
   8730             } else if !runtime.personal_projection.cart.cart.lines.is_empty() {
   8731                 Some(HomeAutoFocusTarget::BuyerCartOpenOrderReview)
   8732             } else {
   8733                 None
   8734             }
   8735         }
   8736         PersonalSection::Orders => {
   8737             if let Some(detail) = runtime.personal_projection.orders.detail.as_ref() {
   8738                 let replace_confirmation = runtime
   8739                     .personal_projection
   8740                     .cart
   8741                     .cart
   8742                     .replace_confirmation
   8743                     .as_ref()
   8744                     .is_some_and(|confirmation| {
   8745                         confirmation.incoming_farm_display_name == detail.farm_display_name
   8746                     });
   8747                 if replace_confirmation {
   8748                     Some(HomeAutoFocusTarget::BuyerOrderConfirmReplace)
   8749                 } else if detail.repeat_demand.as_ref().is_some_and(|repeat_demand| {
   8750                     repeat_demand.eligibility != RepeatDemandEligibility::Unavailable
   8751                 }) {
   8752                     Some(HomeAutoFocusTarget::BuyerOrderRepeatDemand)
   8753                 } else if !runtime.personal_projection.orders.list.rows.is_empty() {
   8754                     Some(HomeAutoFocusTarget::BuyerOrderOpenFirst)
   8755                 } else {
   8756                     None
   8757                 }
   8758             } else if !runtime.personal_projection.orders.list.rows.is_empty() {
   8759                 Some(HomeAutoFocusTarget::BuyerOrderOpenFirst)
   8760             } else {
   8761                 None
   8762             }
   8763         }
   8764     }
   8765 }
   8766 
   8767 fn farmer_auto_focus_target(
   8768     runtime: &DesktopAppRuntimeSummary,
   8769     state: HomeAutoFocusState,
   8770 ) -> Option<HomeAutoFocusTarget> {
   8771     if let Some(reminder) = presented_farmer_reminder(runtime) {
   8772         if reminder.action_label.is_some() {
   8773             return Some(HomeAutoFocusTarget::FarmerReminderPrimary);
   8774         }
   8775         return Some(HomeAutoFocusTarget::FarmerReminderDismiss);
   8776     }
   8777 
   8778     match selected_farmer_section(runtime) {
   8779         FarmerSection::Today | FarmerSection::Farm => today_auto_focus_target(runtime, state),
   8780         FarmerSection::Products if farmer_products_available(runtime) => {
   8781             if state.has_product_editor_form {
   8782                 Some(HomeAutoFocusTarget::ProductEditorTitleInput)
   8783             } else if state.has_products_stock_editor {
   8784                 Some(HomeAutoFocusTarget::ProductsStockInput)
   8785             } else if state.has_products_search_input {
   8786                 Some(HomeAutoFocusTarget::ProductsSearchInput)
   8787             } else if !runtime.products_projection.list.rows.is_empty() {
   8788                 Some(HomeAutoFocusTarget::ProductsRowOpenFirst)
   8789             } else {
   8790                 None
   8791             }
   8792         }
   8793         FarmerSection::Orders if farmer_products_available(runtime) => {
   8794             if !runtime.orders_projection.list.rows.is_empty() {
   8795                 Some(HomeAutoFocusTarget::OrdersRowOpenFirst)
   8796             } else {
   8797                 None
   8798             }
   8799         }
   8800         FarmerSection::PackDay if farmer_pack_day_available(runtime) => None,
   8801         FarmerSection::Products | FarmerSection::Orders | FarmerSection::PackDay => {
   8802             today_auto_focus_target(runtime, state)
   8803         }
   8804     }
   8805 }
   8806 
   8807 fn today_auto_focus_target(
   8808     runtime: &DesktopAppRuntimeSummary,
   8809     state: HomeAutoFocusState,
   8810 ) -> Option<HomeAutoFocusTarget> {
   8811     let projection = &runtime.today_projection;
   8812 
   8813     if state.has_farm_setup_form {
   8814         return Some(HomeAutoFocusTarget::FarmerSetupFarmNameInput);
   8815     }
   8816 
   8817     if let Some(spec) = farm_setup_onboarding_card_spec(runtime.home_route) {
   8818         if spec.action_key.is_some() {
   8819             return Some(HomeAutoFocusTarget::FarmerSetupStart);
   8820         }
   8821     } else if projection.needs_setup()
   8822         && farmer_home_farm_state(runtime) == FarmerHomeFarmState::IncompleteFarm
   8823     {
   8824         return Some(HomeAutoFocusTarget::FarmerSetupContinue);
   8825     }
   8826 
   8827     if projection
   8828         .reminders
   8829         .items
   8830         .iter()
   8831         .any(|reminder| reminder_action_target(reminder).is_some())
   8832     {
   8833         return Some(HomeAutoFocusTarget::FarmerTodayReminderChipFirst);
   8834     }
   8835     if projection.next_fulfillment_window.is_some() {
   8836         return Some(HomeAutoFocusTarget::FarmerTodayOpenPackDay);
   8837     }
   8838     if !projection.orders_needing_action.is_empty() {
   8839         return Some(HomeAutoFocusTarget::FarmerTodayOpenOrders);
   8840     }
   8841     if !projection.low_stock_products.is_empty() {
   8842         return Some(HomeAutoFocusTarget::FarmerTodayOpenProductsLowStock);
   8843     }
   8844     if !projection.draft_products.is_empty() {
   8845         return Some(HomeAutoFocusTarget::FarmerTodayOpenProductsDrafts);
   8846     }
   8847 
   8848     None
   8849 }
   8850 
   8851 fn settings_auto_focus_target(
   8852     selected_view: SettingsPanelViewKey,
   8853     farm_panel_state: Option<&SettingsFarmPanelState>,
   8854     runtime: &DesktopAppRuntimeSummary,
   8855 ) -> Option<SettingsAutoFocusTarget> {
   8856     match selected_view {
   8857         SettingsPanelViewKey::Account => Some(SettingsAutoFocusTarget::AccountAdd),
   8858         SettingsPanelViewKey::Farm => farm_panel_state
   8859             .map(|_| SettingsAutoFocusTarget::FarmNameInput)
   8860             .or(Some(SettingsAutoFocusTarget::Navigation(
   8861                 SettingsPanelViewKey::Farm,
   8862             ))),
   8863         SettingsPanelViewKey::Settings => Some(SettingsAutoFocusTarget::Navigation(
   8864             SettingsPanelViewKey::Settings,
   8865         )),
   8866         SettingsPanelViewKey::About => {
   8867             if about_manual_refresh_enabled(&runtime.sync_status) {
   8868                 Some(SettingsAutoFocusTarget::AboutRefresh)
   8869             } else {
   8870                 Some(SettingsAutoFocusTarget::Navigation(
   8871                     SettingsPanelViewKey::About,
   8872                 ))
   8873             }
   8874         }
   8875     }
   8876 }
   8877 
   8878 impl Render for SettingsWindowView {
   8879     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
   8880         let navigation_buttons = SETTINGS_NAVIGATION_ORDER
   8881             .iter()
   8882             .copied()
   8883             .map(|view| self.navigation_button(view, cx).into_any_element())
   8884             .collect::<Vec<_>>();
   8885         let panel_content = self.settings_panel_content(window, cx);
   8886         self.apply_auto_focus(window, cx);
   8887 
   8888         app_window_shell(
   8889             APP_UI_THEME.foundation.surfaces.panel_background,
   8890             app_stack_v(0.0)
   8891                 .size_full()
   8892                 .bg(rgb(APP_UI_THEME.foundation.surfaces.panel_background))
   8893                 .overflow_hidden()
   8894                 .child(
   8895                     app_stack_v(0.0)
   8896                         .w_full()
   8897                         .h(px(APP_UI_THEME.shells.settings_chrome_height_px))
   8898                         .bg(rgb(APP_UI_THEME.foundation.surfaces.chrome_background))
   8899                         .child(utility_title_row(app_shared_text(
   8900                             AppTextKey::SettingsTitle,
   8901                         )))
   8902                         .child(
   8903                             app_cluster(APP_UI_THEME.shells.settings_navigation_row_gap_px)
   8904                                 .w_full()
   8905                                 .justify_center()
   8906                                 .pt(px(APP_UI_THEME.shells.settings_navigation_row_padding_px))
   8907                                 .pb(px(APP_UI_THEME.shells.settings_navigation_row_padding_px))
   8908                                 .children(navigation_buttons),
   8909                         ),
   8910                 )
   8911                 .child(section_divider())
   8912                 .child(div().flex_1().overflow_hidden().child(panel_content)),
   8913         )
   8914     }
   8915 }
   8916 
   8917 fn settings_panel_label_key(view: SettingsPanelViewKey) -> AppTextKey {
   8918     match view {
   8919         SettingsPanelViewKey::Account => AppTextKey::SettingsNavAccounts,
   8920         SettingsPanelViewKey::Farm => AppTextKey::SettingsNavFarm,
   8921         SettingsPanelViewKey::Settings => AppTextKey::SettingsNavSettings,
   8922         SettingsPanelViewKey::About => AppTextKey::SettingsNavAbout,
   8923     }
   8924 }
   8925 
   8926 fn settings_panel_spec(view: SettingsPanelViewKey) -> (&'static str, IconName) {
   8927     match view {
   8928         SettingsPanelViewKey::Account => ("settings-nav-accounts", IconName::CircleUser),
   8929         SettingsPanelViewKey::Farm => ("settings-nav-farm", IconName::Map),
   8930         SettingsPanelViewKey::Settings => ("settings-nav-settings", IconName::Settings2),
   8931         SettingsPanelViewKey::About => ("settings-nav-about", IconName::Info),
   8932     }
   8933 }
   8934 
   8935 #[derive(Clone, Copy)]
   8936 struct HomeStatusPresentation {
   8937     indicator_color: u32,
   8938     label_key: AppTextKey,
   8939 }
   8940 
   8941 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
   8942 struct FarmSetupOnboardingCardSpec {
   8943     title_key: AppTextKey,
   8944     body_key: AppTextKey,
   8945     action_key: Option<AppTextKey>,
   8946 }
   8947 
   8948 #[cfg(test)]
   8949 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
   8950 struct SettingsInventorySectionSpec {
   8951     title_key: AppTextKey,
   8952     field_keys: &'static [AppTextKey],
   8953 }
   8954 
   8955 const SETTINGS_NAVIGATION_ORDER: &[SettingsPanelViewKey] = &[
   8956     SettingsPanelViewKey::Account,
   8957     SettingsPanelViewKey::Farm,
   8958     SettingsPanelViewKey::Settings,
   8959     SettingsPanelViewKey::About,
   8960 ];
   8961 
   8962 #[cfg(test)]
   8963 const SETTINGS_FARM_SECTION_FIELDS: &[AppTextKey] = &[
   8964     AppTextKey::HomeFarmSetupFieldFarmName,
   8965     AppTextKey::SettingsFarmFieldTimezone,
   8966     AppTextKey::SettingsFarmFieldCurrency,
   8967 ];
   8968 
   8969 #[cfg(test)]
   8970 const SETTINGS_PICKUP_LOCATIONS_SECTION_FIELDS: &[AppTextKey] = &[
   8971     AppTextKey::SettingsPickupLocationsFieldLabel,
   8972     AppTextKey::SettingsPickupLocationsFieldAddress,
   8973     AppTextKey::SettingsPickupLocationsFieldDirections,
   8974     AppTextKey::SettingsPickupLocationsFieldDefault,
   8975 ];
   8976 
   8977 #[cfg(test)]
   8978 const SETTINGS_OPERATING_RULES_SECTION_FIELDS: &[AppTextKey] = &[
   8979     AppTextKey::SettingsOperatingRulesFieldPromiseLeadTime,
   8980     AppTextKey::SettingsOperatingRulesFieldSubstitutionPolicy,
   8981 ];
   8982 
   8983 #[cfg(test)]
   8984 const SETTINGS_FULFILLMENT_WINDOWS_SECTION_FIELDS: &[AppTextKey] = &[
   8985     AppTextKey::SettingsFulfillmentWindowsFieldLabel,
   8986     AppTextKey::SettingsFulfillmentWindowsFieldPickupLocation,
   8987     AppTextKey::SettingsFulfillmentWindowsFieldStartsAt,
   8988     AppTextKey::SettingsFulfillmentWindowsFieldEndsAt,
   8989     AppTextKey::SettingsFulfillmentWindowsFieldOrderCutoff,
   8990 ];
   8991 
   8992 #[cfg(test)]
   8993 const SETTINGS_BLACKOUT_PERIODS_SECTION_FIELDS: &[AppTextKey] = &[
   8994     AppTextKey::SettingsBlackoutPeriodsFieldLabel,
   8995     AppTextKey::SettingsBlackoutPeriodsFieldStartsAt,
   8996     AppTextKey::SettingsBlackoutPeriodsFieldEndsAt,
   8997 ];
   8998 
   8999 #[cfg(test)]
   9000 const SETTINGS_READINESS_SECTION_FIELDS: &[AppTextKey] = &[
   9001     AppTextKey::SettingsReadinessFieldMissingProfileBasics,
   9002     AppTextKey::SettingsReadinessFieldMissingPickupLocation,
   9003     AppTextKey::SettingsReadinessFieldMissingFulfillmentWindow,
   9004     AppTextKey::SettingsReadinessFieldMissingOperatingRules,
   9005     AppTextKey::SettingsReadinessFieldInvalidTimingConflicts,
   9006 ];
   9007 
   9008 #[cfg(test)]
   9009 const SETTINGS_FARM_PANEL_SECTIONS: &[SettingsInventorySectionSpec] = &[
   9010     SettingsInventorySectionSpec {
   9011         title_key: AppTextKey::HomeFarmSetupSectionFarm,
   9012         field_keys: SETTINGS_FARM_SECTION_FIELDS,
   9013     },
   9014     SettingsInventorySectionSpec {
   9015         title_key: AppTextKey::SettingsPickupLocationsSectionLabel,
   9016         field_keys: SETTINGS_PICKUP_LOCATIONS_SECTION_FIELDS,
   9017     },
   9018 ];
   9019 
   9020 #[cfg(test)]
   9021 const SETTINGS_OPERATIONS_PANEL_SECTIONS: &[SettingsInventorySectionSpec] = &[
   9022     SettingsInventorySectionSpec {
   9023         title_key: AppTextKey::SettingsOperatingRulesSectionLabel,
   9024         field_keys: SETTINGS_OPERATING_RULES_SECTION_FIELDS,
   9025     },
   9026     SettingsInventorySectionSpec {
   9027         title_key: AppTextKey::SettingsFulfillmentWindowsSectionLabel,
   9028         field_keys: SETTINGS_FULFILLMENT_WINDOWS_SECTION_FIELDS,
   9029     },
   9030     SettingsInventorySectionSpec {
   9031         title_key: AppTextKey::SettingsBlackoutPeriodsSectionLabel,
   9032         field_keys: SETTINGS_BLACKOUT_PERIODS_SECTION_FIELDS,
   9033     },
   9034     SettingsInventorySectionSpec {
   9035         title_key: AppTextKey::SettingsReadinessSectionLabel,
   9036         field_keys: SETTINGS_READINESS_SECTION_FIELDS,
   9037     },
   9038 ];
   9039 
   9040 fn shared_shell_header(
   9041     runtime: &DesktopAppRuntimeSummary,
   9042     on_select_marketplace: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   9043     on_select_farm: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   9044     on_open_account: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   9045     cx: &App,
   9046 ) -> impl IntoElement {
   9047     let can_enter_farmer_workspace = runtime.personal_projection.entry.can_enter_farmer_workspace;
   9048     let active_mode = shell_header_active_mode(runtime);
   9049     let is_account_active = active_mode == ShellHeaderActiveMode::Account;
   9050     let is_farm_active = active_mode == ShellHeaderActiveMode::Farm;
   9051     let is_marketplace_active = active_mode == ShellHeaderActiveMode::Marketplace;
   9052     let farm_name = home_saved_farm(runtime).map(|farm| farm.display_name.clone());
   9053     let account_label = shell_account_label(runtime);
   9054 
   9055     app_surface_panel(
   9056         div()
   9057             .w_full()
   9058             .px(px(APP_UI_THEME.shells.home_card_padding_px))
   9059             .py(px(APP_UI_THEME.foundation.spacing.small_px))
   9060             .flex()
   9061             .justify_between()
   9062             .items_center()
   9063             .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
   9064             .child(
   9065                 div()
   9066                     .flex()
   9067                     .flex_col()
   9068                     .gap(px(2.0))
   9069                     .child(app_text_label(app_shared_text(AppTextKey::AppName)))
   9070                     .when_some(farm_name, |this, farm_name| {
   9071                         this.child(home_body_text(farm_name))
   9072                     }),
   9073             )
   9074             .child(
   9075                 app_cluster(APP_UI_THEME.foundation.spacing.small_px)
   9076                     .items_center()
   9077                     .when(can_enter_farmer_workspace, |this| {
   9078                         this.child(
   9079                             shared_shell_mode_button(
   9080                                 "shell-mode-marketplace",
   9081                                 AppTextKey::HomeHeaderMarketplaceMode,
   9082                                 is_marketplace_active,
   9083                                 on_select_marketplace,
   9084                                 cx,
   9085                             )
   9086                             .into_any_element(),
   9087                         )
   9088                         .child(
   9089                             shared_shell_mode_button(
   9090                                 "shell-mode-farm",
   9091                                 AppTextKey::HomeHeaderFarmMode,
   9092                                 is_farm_active,
   9093                                 on_select_farm,
   9094                                 cx,
   9095                             )
   9096                             .into_any_element(),
   9097                         )
   9098                     })
   9099                     .child(shell_account_entry(
   9100                         runtime,
   9101                         account_label,
   9102                         is_account_active,
   9103                         on_open_account,
   9104                         cx,
   9105                     )),
   9106             ),
   9107     )
   9108 }
   9109 
   9110 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
   9111 enum ShellHeaderActiveMode {
   9112     Marketplace,
   9113     Farm,
   9114     Account,
   9115 }
   9116 
   9117 fn shell_header_active_mode(runtime: &DesktopAppRuntimeSummary) -> ShellHeaderActiveMode {
   9118     if matches!(
   9119         runtime.shell_projection.selected_section,
   9120         ShellSection::Account
   9121     ) {
   9122         ShellHeaderActiveMode::Account
   9123     } else if runtime.shell_projection.active_surface == ActiveSurface::Farmer {
   9124         ShellHeaderActiveMode::Farm
   9125     } else {
   9126         ShellHeaderActiveMode::Marketplace
   9127     }
   9128 }
   9129 
   9130 fn shared_shell_mode_button(
   9131     id: &'static str,
   9132     key: AppTextKey,
   9133     is_active: bool,
   9134     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   9135     cx: &App,
   9136 ) -> AnyElement {
   9137     choice_button(id, app_shared_text(key), is_active, on_click, cx).into_any_element()
   9138 }
   9139 
   9140 fn shell_account_entry(
   9141     runtime: &DesktopAppRuntimeSummary,
   9142     account_label: String,
   9143     is_active: bool,
   9144     on_open_account: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
   9145     cx: &App,
   9146 ) -> AnyElement {
   9147     if runtime.personal_projection.entry.state == PersonalEntryState::Guest {
   9148         choice_button(
   9149             "shell-account-entry",
   9150             app_shared_text(AppTextKey::HomeHeaderAccountSetupAction),
   9151             is_active,
   9152             on_open_account,
   9153             cx,
   9154         )
   9155         .into_any_element()
   9156     } else {
   9157         choice_button(
   9158             "shell-account-entry",
   9159             account_label,
   9160             is_active,
   9161             on_open_account,
   9162             cx,
   9163         )
   9164         .into_any_element()
   9165     }
   9166 }
   9167 
   9168 fn shell_account_label(runtime: &DesktopAppRuntimeSummary) -> String {
   9169     runtime
   9170         .settings_account_projection
   9171         .selected_account
   9172         .as_ref()
   9173         .and_then(|account| {
   9174             account
   9175                 .account
   9176                 .label
   9177                 .as_ref()
   9178                 .map(|label| label.trim().to_owned())
   9179                 .filter(|label| !label.is_empty())
   9180                 .or_else(|| Some(app_shared_text(AppTextKey::HomeHeaderAccountLabel).to_string()))
   9181         })
   9182         .unwrap_or_else(|| app_shared_text(AppTextKey::HomeHeaderGuestLabel).to_string())
   9183 }
   9184 
   9185 fn buyer_workspace_title_block(title_key: AppTextKey, body_key: AppTextKey) -> impl IntoElement {
   9186     div()
   9187         .w_full()
   9188         .flex()
   9189         .flex_col()
   9190         .gap(px(4.0))
   9191         .child(
   9192             div()
   9193                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 2.0))
   9194                 .font_weight(gpui::FontWeight::BOLD)
   9195                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   9196                 .child(app_shared_text(title_key)),
   9197         )
   9198         .child(
   9199             div()
   9200                 .w_full()
   9201                 .min_w_0()
   9202                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   9203                 .font_weight(gpui::FontWeight::MEDIUM)
   9204                 .line_height(relative(1.2))
   9205                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   9206                 .child(app_shared_text(body_key)),
   9207         )
   9208 }
   9209 
   9210 fn account_tab_frame(
   9211     tabs: impl IntoIterator<Item = AppUnderlineTabSpec>,
   9212     selected_index: usize,
   9213     on_select_tab: impl Fn(&usize, &mut Window, &mut App) + 'static,
   9214     heading_key: AppTextKey,
   9215     heading_actions: Option<AnyElement>,
   9216     fixed_subheader: Option<AnyElement>,
   9217     panel: AnyElement,
   9218     panel_uses_inner_scroll: bool,
   9219 ) -> AnyElement {
   9220     div()
   9221         .size_full()
   9222         .overflow_hidden()
   9223         .flex()
   9224         .flex_col()
   9225         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
   9226         .child(
   9227             app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   9228                 .w_full()
   9229                 .flex_none()
   9230                 .child(app_text_value(app_shared_text(AppTextKey::AccountTitle)))
   9231                 .child(app_underline_tabs(
   9232                     "account-tabs",
   9233                     tabs,
   9234                     selected_index,
   9235                     on_select_tab,
   9236                 ))
   9237                 .child(account_section_heading_row(heading_key, heading_actions)),
   9238         )
   9239         .when_some(fixed_subheader, |this, fixed_subheader| {
   9240             this.child(div().w_full().flex_none().child(fixed_subheader))
   9241         })
   9242         .child(if panel_uses_inner_scroll {
   9243             div()
   9244                 .flex_1()
   9245                 .w_full()
   9246                 .overflow_hidden()
   9247                 .child(panel)
   9248                 .into_any_element()
   9249         } else {
   9250             div()
   9251                 .flex_1()
   9252                 .w_full()
   9253                 .overflow_hidden()
   9254                 .child(app_scroll_panel("account-scroll", 0.0, None, panel))
   9255                 .into_any_element()
   9256         })
   9257         .into_any_element()
   9258 }
   9259 
   9260 fn account_placeholder_panel(text_key: AppTextKey) -> impl IntoElement {
   9261     div()
   9262         .w_full()
   9263         .min_h(px(320.0))
   9264         .flex()
   9265         .items_center()
   9266         .justify_center()
   9267         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   9268         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   9269         .child(app_shared_text(text_key))
   9270 }
   9271 
   9272 fn account_profile_panel(
   9273     form: &AccountProfileFormState,
   9274     cx: &mut Context<HomeView>,
   9275 ) -> impl IntoElement {
   9276     app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   9277         .w_full()
   9278         .child(account_profile_details_card(form, cx))
   9279 }
   9280 
   9281 fn account_section_heading_row(
   9282     label_key: AppTextKey,
   9283     actions: Option<impl IntoElement>,
   9284 ) -> impl IntoElement {
   9285     div()
   9286         .w_full()
   9287         .flex()
   9288         .items_center()
   9289         .justify_between()
   9290         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
   9291         .child(
   9292             div()
   9293                 .min_w_0()
   9294                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 1.5))
   9295                 .font_weight(gpui::FontWeight::BOLD)
   9296                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   9297                 .child(app_shared_text(label_key)),
   9298         )
   9299         .when_some(actions, |this, actions| {
   9300             this.child(div().flex_none().child(actions))
   9301         })
   9302 }
   9303 
   9304 fn account_form_heading_actions(
   9305     draft_id: &'static str,
   9306     save_id: &'static str,
   9307     save_is_active: bool,
   9308     cx: &mut Context<HomeView>,
   9309 ) -> impl IntoElement {
   9310     let save_button = if save_is_active {
   9311         action_button_primary_compact(
   9312             save_id,
   9313             app_shared_text(AppTextKey::AccountFormSaveAction),
   9314             |_, _, _| {},
   9315             cx,
   9316         )
   9317         .into_any_element()
   9318     } else {
   9319         action_button_primary_compact_disabled(
   9320             save_id,
   9321             app_shared_text(AppTextKey::AccountFormSaveAction),
   9322             cx,
   9323         )
   9324         .into_any_element()
   9325     };
   9326 
   9327     div()
   9328         .flex()
   9329         .items_center()
   9330         .gap(px(APP_UI_THEME.foundation.spacing.small_px))
   9331         .child(action_button_compact(
   9332             draft_id,
   9333             app_shared_text(AppTextKey::AccountFormSaveDraftAction),
   9334             |_, _, _| {},
   9335             cx,
   9336         ))
   9337         .child(save_button)
   9338 }
   9339 
   9340 fn account_profile_details_card(
   9341     form: &AccountProfileFormState,
   9342     cx: &mut Context<HomeView>,
   9343 ) -> impl IntoElement {
   9344     div()
   9345         .w_full()
   9346         .border_1()
   9347         .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider))
   9348         .rounded(px(APP_UI_THEME.foundation.radii.large_px))
   9349         .bg(transparent_black())
   9350         .child(
   9351             div()
   9352                 .w_full()
   9353                 .p(px(APP_UI_THEME.shells.home_card_padding_px))
   9354                 .flex()
   9355                 .items_start()
   9356                 .gap(px(APP_UI_THEME.shells.home_card_padding_px))
   9357                 .child(account_profile_photo_actions(cx))
   9358                 .child(
   9359                     account_profile_field_column()
   9360                         .child(account_profile_input_field(
   9361                             AppTextKey::AccountProfileFullNameLabel,
   9362                             &form.full_name_input,
   9363                         ))
   9364                         .child(account_profile_input_field(
   9365                             AppTextKey::AccountProfilePhoneLabel,
   9366                             &form.phone_input,
   9367                         ))
   9368                         .child(account_profile_select_field(
   9369                             AppTextKey::AccountProfileTimeZoneLabel,
   9370                             &form.time_zone_select,
   9371                         )),
   9372                 )
   9373                 .child(
   9374                     account_profile_field_column()
   9375                         .child(account_profile_input_field(
   9376                             AppTextKey::AccountProfileEmailLabel,
   9377                             &form.email_input,
   9378                         ))
   9379                         .child(account_profile_select_field(
   9380                             AppTextKey::AccountProfileRoleLabel,
   9381                             &form.role_select,
   9382                         ))
   9383                         .child(account_profile_select_field(
   9384                             AppTextKey::AccountProfileLanguageLabel,
   9385                             &form.language_select,
   9386                         )),
   9387                 ),
   9388         )
   9389 }
   9390 
   9391 fn account_profile_photo_actions(cx: &mut Context<HomeView>) -> impl IntoElement {
   9392     app_stack_v(10.0)
   9393         .flex_none()
   9394         .flex_basis(relative(0.24))
   9395         .min_w(px(136.0))
   9396         .child(
   9397             div()
   9398                 .w_full()
   9399                 .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
   9400                 .font_weight(gpui::FontWeight::SEMIBOLD)
   9401                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   9402                 .child(app_shared_text(AppTextKey::AccountProfilePictureLabel)),
   9403         )
   9404         .child(
   9405             div().w_full().flex().justify_center().child(
   9406                 div()
   9407                     .size(px(72.0))
   9408                     .rounded(px(36.0))
   9409                     .border_1()
   9410                     .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider))
   9411                     .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background))
   9412                     .flex()
   9413                     .items_center()
   9414                     .justify_center()
   9415                     .child(
   9416                         Icon::new(IconName::CircleUser)
   9417                             .with_size(gpui_component::Size::Size(px(34.0)))
   9418                             .text_color(rgb(APP_UI_THEME.foundation.text.secondary)),
   9419                     ),
   9420             ),
   9421         )
   9422         .child(
   9423             app_stack_v(8.0)
   9424                 .w_full()
   9425                 .child(action_button_primary_full_width(
   9426                     "account-profile-change-photo",
   9427                     app_shared_text(AppTextKey::AccountProfileChangePhotoAction),
   9428                     |_, _, _| {},
   9429                     cx,
   9430                 ))
   9431                 .child(action_button_full_width(
   9432                     "account-profile-remove-photo",
   9433                     app_shared_text(AppTextKey::AccountProfileRemovePhotoAction),
   9434                     |_, _, _| {},
   9435                     cx,
   9436                 )),
   9437         )
   9438 }
   9439 
   9440 fn account_profile_field_column() -> gpui::Div {
   9441     app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   9442         .w_full()
   9443         .flex_1()
   9444         .min_w_0()
   9445 }
   9446 
   9447 fn account_profile_input_field(
   9448     label_key: AppTextKey,
   9449     input: &Entity<InputState>,
   9450 ) -> impl IntoElement {
   9451     account_profile_labeled_control(label_key, account_form_text_input(input))
   9452 }
   9453 
   9454 fn account_profile_select_field(
   9455     label_key: AppTextKey,
   9456     select: &Entity<AccountProfileSelectState>,
   9457 ) -> impl IntoElement {
   9458     account_profile_labeled_control(label_key, account_profile_select_input(select))
   9459 }
   9460 
   9461 fn account_profile_labeled_control(
   9462     label_key: AppTextKey,
   9463     control: impl IntoElement,
   9464 ) -> impl IntoElement {
   9465     app_stack_v(6.0)
   9466         .w_full()
   9467         .min_w_0()
   9468         .child(
   9469             div()
   9470                 .w_full()
   9471                 .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
   9472                 .font_weight(gpui::FontWeight::SEMIBOLD)
   9473                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   9474                 .child(app_shared_text(label_key)),
   9475         )
   9476         .child(control)
   9477 }
   9478 
   9479 const ACCOUNT_FORM_CONTROL_HEIGHT_PX: f32 = 28.0;
   9480 const ACCOUNT_FORM_CONTROL_RADIUS_PX: f32 = 8.0;
   9481 const ACCOUNT_FARM_DETAILS_FIELD_MIN_WIDTH_PX: f32 = 220.0;
   9482 const ACCOUNT_FARM_DETAILS_FULL_FIELD_MIN_WIDTH_PX: f32 = 464.0;
   9483 const ACCOUNT_SETTINGS_DEFAULT_BLOSSOM_SERVER: &str = "http://localhost:8082";
   9484 const ACCOUNT_SETTINGS_RELAY_LOCALHOST_8080: &str = "ws://localhost:8080";
   9485 const ACCOUNT_SETTINGS_RELAY_LOCALHOST_8081: &str = "ws://localhost:8081";
   9486 
   9487 fn account_form_text_input(input: &Entity<InputState>) -> impl IntoElement {
   9488     gpui::Styled::h(
   9489         app_text_input(input, false)
   9490             .with_size(Size::Small)
   9491             .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
   9492             .font_weight(gpui::FontWeight::NORMAL)
   9493             .rounded(px(ACCOUNT_FORM_CONTROL_RADIUS_PX))
   9494             .w_full(),
   9495         px(ACCOUNT_FORM_CONTROL_HEIGHT_PX),
   9496     )
   9497 }
   9498 
   9499 fn account_form_select_input<D>(select: &Entity<SelectState<D>>) -> impl IntoElement
   9500 where
   9501     D: SelectDelegate + 'static,
   9502 {
   9503     Select::new(select)
   9504         .with_size(Size::Small)
   9505         .h(px(ACCOUNT_FORM_CONTROL_HEIGHT_PX))
   9506         .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
   9507         .font_weight(gpui::FontWeight::NORMAL)
   9508         .rounded(px(ACCOUNT_FORM_CONTROL_RADIUS_PX))
   9509         .w_full()
   9510 }
   9511 
   9512 fn account_form_text_area_input_with_wrapped_preview(
   9513     input: &Entity<InputState>,
   9514     is_wrap_ready: bool,
   9515     cx: &mut Context<HomeView>,
   9516 ) -> impl IntoElement {
   9517     let preview = input.read(cx).value();
   9518 
   9519     div()
   9520         .relative()
   9521         .w_full()
   9522         .min_w(px(ACCOUNT_FARM_DETAILS_FULL_FIELD_MIN_WIDTH_PX))
   9523         .child(
   9524             app_text_input(input, false)
   9525                 .with_size(Size::Small)
   9526                 .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
   9527                 .font_weight(gpui::FontWeight::NORMAL)
   9528                 .rounded(px(ACCOUNT_FORM_CONTROL_RADIUS_PX))
   9529                 .w_full()
   9530                 .opacity(if is_wrap_ready { 1.0 } else { 0.0 }),
   9531         )
   9532         .when(!is_wrap_ready, |this| {
   9533             this.child(
   9534                 div()
   9535                     .absolute()
   9536                     .top_0()
   9537                     .left_0()
   9538                     .right_0()
   9539                     .px(px(8.0))
   9540                     .py(px(2.0))
   9541                     .line_height(relative(1.25))
   9542                     .whitespace_normal()
   9543                     .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
   9544                     .font_weight(gpui::FontWeight::NORMAL)
   9545                     .text_color(rgb(APP_UI_THEME.foundation.text.primary))
   9546                     .child(preview),
   9547             )
   9548         })
   9549 }
   9550 
   9551 fn account_profile_select_input(select: &Entity<AccountProfileSelectState>) -> impl IntoElement {
   9552     account_form_select_input(select)
   9553 }
   9554 
   9555 fn account_farm_profile_select_input(
   9556     select: &Entity<AccountFarmProfileSelectState>,
   9557 ) -> impl IntoElement {
   9558     account_form_select_input(select)
   9559 }
   9560 
   9561 fn account_farm_profile_panel(
   9562     form: &AccountFarmProfileFormState,
   9563     selected_tab: AccountFarmDetailsTab,
   9564     is_textarea_wrap_ready: bool,
   9565     cx: &mut Context<HomeView>,
   9566 ) -> impl IntoElement {
   9567     div()
   9568         .size_full()
   9569         .overflow_hidden()
   9570         .child(account_farm_details_tab_panel(
   9571             form,
   9572             selected_tab,
   9573             is_textarea_wrap_ready,
   9574             cx,
   9575         ))
   9576 }
   9577 
   9578 fn account_farm_details_tab_panel(
   9579     form: &AccountFarmProfileFormState,
   9580     selected_tab: AccountFarmDetailsTab,
   9581     is_textarea_wrap_ready: bool,
   9582     cx: &mut Context<HomeView>,
   9583 ) -> AnyElement {
   9584     match selected_tab {
   9585         AccountFarmDetailsTab::Profile => account_farm_profile_section_row(
   9586             account_farm_profile_main_card(form, is_textarea_wrap_ready, cx),
   9587             account_farm_profile_summary_card(cx),
   9588         )
   9589         .into_any_element(),
   9590         AccountFarmDetailsTab::Location => account_farm_profile_section_row(
   9591             account_farm_location_card(form),
   9592             account_farm_location_preview_card(),
   9593         )
   9594         .into_any_element(),
   9595         AccountFarmDetailsTab::Operations => account_farm_profile_section_row(
   9596             account_farm_operating_card(form, is_textarea_wrap_ready, cx),
   9597             account_farm_profile_preview_card(cx),
   9598         )
   9599         .into_any_element(),
   9600         AccountFarmDetailsTab::Fulfilment => account_farm_profile_section_row(
   9601             account_farm_fulfillment_card(form, is_textarea_wrap_ready, cx),
   9602             account_farm_customer_experience_card(),
   9603         )
   9604         .into_any_element(),
   9605     }
   9606 }
   9607 
   9608 fn account_farm_profile_section_row(
   9609     main: impl IntoElement,
   9610     rail: impl IntoElement,
   9611 ) -> impl IntoElement {
   9612     div()
   9613         .size_full()
   9614         .overflow_hidden()
   9615         .flex()
   9616         .items_start()
   9617         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
   9618         .child(div().flex_1().h_full().min_w_0().child(main))
   9619         .child(div().w(px(336.0)).min_w(px(300.0)).flex_none().child(rail))
   9620 }
   9621 
   9622 fn account_farm_profile_main_card(
   9623     form: &AccountFarmProfileFormState,
   9624     is_textarea_wrap_ready: bool,
   9625     cx: &mut Context<HomeView>,
   9626 ) -> impl IntoElement + use<> {
   9627     account_farm_profile_card(
   9628         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   9629             .w_full()
   9630             .child(account_farm_profile_title_block(
   9631                 AppTextKey::AccountFarmDetailsFarmProfileTitle,
   9632                 AppTextKey::AccountFarmDetailsFarmProfileIntro,
   9633             ))
   9634             .child(account_farm_profile_field_row(
   9635                 account_profile_input_field(
   9636                     AppTextKey::AccountFarmDetailsFarmNameLabel,
   9637                     &form.farm_name_input,
   9638                 ),
   9639                 account_profile_input_field(
   9640                     AppTextKey::AccountFarmDetailsPublicFarmNameLabel,
   9641                     &form.public_farm_name_input,
   9642                 ),
   9643             ))
   9644             .child(account_farm_profile_field_row(
   9645                 account_profile_input_field(
   9646                     AppTextKey::AccountFarmDetailsShortDescriptionLabel,
   9647                     &form.short_description_input,
   9648                 ),
   9649                 account_farm_profile_select_field(
   9650                     AppTextKey::AccountFarmDetailsFarmTypeLabel,
   9651                     &form.farm_type_select,
   9652                 ),
   9653             ))
   9654             .child(account_farm_profile_field_row(
   9655                 account_profile_input_field(
   9656                     AppTextKey::AccountFarmDetailsContactEmailLabel,
   9657                     &form.contact_email_input,
   9658                 ),
   9659                 account_profile_input_field(
   9660                     AppTextKey::AccountFarmDetailsPublicPhoneLabel,
   9661                     &form.public_phone_input,
   9662                 ),
   9663             ))
   9664             .child(account_farm_profile_field_row(
   9665                 account_profile_input_field(
   9666                     AppTextKey::AccountFarmDetailsWebsiteLabel,
   9667                     &form.website_input,
   9668                 ),
   9669                 account_profile_input_field(
   9670                     AppTextKey::AccountFarmDetailsEstablishedYearLabel,
   9671                     &form.established_year_input,
   9672                 ),
   9673             ))
   9674             .child(account_farm_profile_text_area_field(
   9675                 AppTextKey::AccountFarmDetailsAboutFarmLabel,
   9676                 &form.about_farm_input,
   9677                 is_textarea_wrap_ready,
   9678                 cx,
   9679             )),
   9680     )
   9681 }
   9682 
   9683 fn account_farm_location_card(form: &AccountFarmProfileFormState) -> impl IntoElement {
   9684     account_farm_profile_card(
   9685         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   9686             .w_full()
   9687             .child(account_farm_profile_title_block(
   9688                 AppTextKey::AccountFarmDetailsLocationTitle,
   9689                 AppTextKey::AccountFarmDetailsLocationIntro,
   9690             ))
   9691             .child(account_farm_map_placeholder())
   9692             .child(account_farm_profile_field_row(
   9693                 account_profile_input_field(
   9694                     AppTextKey::AccountFarmDetailsStreetAddressLabel,
   9695                     &form.street_address_input,
   9696                 ),
   9697                 account_profile_input_field(
   9698                     AppTextKey::AccountFarmDetailsCityLabel,
   9699                     &form.city_input,
   9700                 ),
   9701             ))
   9702             .child(account_farm_profile_field_row(
   9703                 account_farm_profile_select_field(
   9704                     AppTextKey::AccountFarmDetailsProvinceLabel,
   9705                     &form.province_select,
   9706                 ),
   9707                 account_profile_input_field(
   9708                     AppTextKey::AccountFarmDetailsPostalCodeLabel,
   9709                     &form.postal_code_input,
   9710                 ),
   9711             ))
   9712             .child(account_farm_profile_field_row(
   9713                 account_farm_profile_select_field(
   9714                     AppTextKey::AccountFarmDetailsCountryLabel,
   9715                     &form.country_select,
   9716                 ),
   9717                 account_farm_profile_select_field(
   9718                     AppTextKey::AccountFarmDetailsServiceAreaLabel,
   9719                     &form.service_area_select,
   9720                 ),
   9721             ))
   9722             .child(account_farm_profile_helper_text(
   9723                 AppTextKey::AccountFarmDetailsServiceAreaHelper,
   9724             ))
   9725             .child(account_farm_toggle_preview_row(
   9726                 AppTextKey::AccountFarmDetailsExactAddressPublicLabel,
   9727                 AppTextKey::AccountFarmDetailsExactAddressPublicHelper,
   9728             )),
   9729     )
   9730 }
   9731 
   9732 fn account_farm_map_placeholder() -> impl IntoElement {
   9733     div()
   9734         .w_full()
   9735         .h(px(220.0))
   9736         .rounded(px(APP_UI_THEME.foundation.radii.large_px))
   9737         .border_1()
   9738         .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider))
   9739         .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background))
   9740         .flex()
   9741         .items_center()
   9742         .justify_center()
   9743         .child(
   9744             div()
   9745                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   9746                 .font_weight(gpui::FontWeight::MEDIUM)
   9747                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   9748                 .child(app_shared_text(
   9749                     AppTextKey::AccountFarmDetailsMapNotImplemented,
   9750                 )),
   9751         )
   9752 }
   9753 
   9754 fn account_farm_location_preview_card() -> impl IntoElement {
   9755     account_farm_profile_rail_card(
   9756         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   9757             .w_full()
   9758             .child(account_farm_preview_title(
   9759                 AppTextKey::AccountFarmDetailsLocationPreviewTitle,
   9760             ))
   9761             .child(account_farm_icon_value_row(
   9762                 IconName::Map,
   9763                 AppTextKey::AccountFarmDetailsFarmLocationValue,
   9764                 AppTextKey::AccountFarmDetailsServiceAreaValue,
   9765             ))
   9766             .child(account_farm_service_area_preview())
   9767             .child(account_farm_profile_helper_text(
   9768                 AppTextKey::AccountFarmDetailsLocationPreviewHelper,
   9769             )),
   9770     )
   9771 }
   9772 
   9773 fn account_farm_service_area_preview() -> impl IntoElement {
   9774     div()
   9775         .w_full()
   9776         .h(px(132.0))
   9777         .rounded(px(APP_UI_THEME.foundation.radii.medium_px))
   9778         .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background))
   9779         .border_1()
   9780         .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider))
   9781         .flex()
   9782         .items_center()
   9783         .justify_center()
   9784         .child(
   9785             div()
   9786                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
   9787                 .font_weight(gpui::FontWeight::MEDIUM)
   9788                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
   9789                 .child(app_shared_text(
   9790                     AppTextKey::AccountFarmDetailsMapNotImplemented,
   9791                 )),
   9792         )
   9793 }
   9794 
   9795 fn account_farm_operating_card(
   9796     form: &AccountFarmProfileFormState,
   9797     is_textarea_wrap_ready: bool,
   9798     cx: &mut Context<HomeView>,
   9799 ) -> impl IntoElement + use<> {
   9800     account_farm_profile_card(
   9801         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   9802             .w_full()
   9803             .child(account_farm_profile_title_block(
   9804                 AppTextKey::AccountFarmDetailsOperatingTitle,
   9805                 AppTextKey::AccountFarmDetailsOperatingIntro,
   9806             ))
   9807             .child(account_farm_profile_field_row(
   9808                 account_farm_profile_select_field(
   9809                     AppTextKey::AccountFarmDetailsGrowingPracticesLabel,
   9810                     &form.growing_practices_select,
   9811                 ),
   9812                 account_farm_static_chip_group(
   9813                     AppTextKey::AccountFarmDetailsProductionMethodsLabel,
   9814                     &[
   9815                         AppTextKey::AccountFarmDetailsProductionMethodOrganicPractices,
   9816                         AppTextKey::AccountFarmDetailsProductionMethodNoSpray,
   9817                         AppTextKey::AccountFarmDetailsGrowingPracticeRegenerative,
   9818                     ],
   9819                     3,
   9820                 ),
   9821             ))
   9822             .child(account_farm_profile_field_row(
   9823                 account_farm_date_range_fields(
   9824                     AppTextKey::AccountFarmDetailsSeasonDatesLabel,
   9825                     &form.season_start_input,
   9826                     &form.season_end_input,
   9827                 ),
   9828                 account_farm_day_chip_group(),
   9829             ))
   9830             .child(account_farm_profile_text_area_field(
   9831                 AppTextKey::AccountFarmDetailsAboutProductsLabel,
   9832                 &form.about_products_input,
   9833                 is_textarea_wrap_ready,
   9834                 cx,
   9835             ))
   9836             .child(account_farm_static_chip_group_with_helper(
   9837                 AppTextKey::AccountFarmDetailsCertificationsTitle,
   9838                 AppTextKey::AccountFarmDetailsCertificationsHelper,
   9839                 &[
   9840                     AppTextKey::AccountFarmDetailsCertificationCertifiedOrganic,
   9841                     AppTextKey::AccountFarmDetailsCertificationNaturallyGrown,
   9842                     AppTextKey::AccountFarmDetailsCertificationSmallFamilyFarm,
   9843                     AppTextKey::AccountFarmDetailsCertificationDeliveryAvailable,
   9844                 ],
   9845                 3,
   9846             ))
   9847             .child(account_farm_profile_text_area_field_with_helper(
   9848                 AppTextKey::AccountFarmDetailsCustomerNoteTitle,
   9849                 AppTextKey::AccountFarmDetailsCustomerNoteHelper,
   9850                 &form.customer_note_input,
   9851                 is_textarea_wrap_ready,
   9852                 cx,
   9853             )),
   9854     )
   9855 }
   9856 
   9857 fn account_farm_profile_preview_card(cx: &mut Context<HomeView>) -> impl IntoElement + use<> {
   9858     account_farm_profile_rail_card(
   9859         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   9860             .w_full()
   9861             .child(account_farm_preview_title(
   9862                 AppTextKey::AccountFarmDetailsProfilePreviewTitle,
   9863             ))
   9864             .child(account_farm_icon_value_row(
   9865                 IconName::Building2,
   9866                 AppTextKey::AccountFarmDetailsFarmNameValue,
   9867                 AppTextKey::AccountFarmDetailsFarmLocationValue,
   9868             ))
   9869             .child(account_farm_profile_summary_row(
   9870                 AppTextKey::AccountFarmDetailsGrowingPracticesSummaryLabel,
   9871                 AppTextKey::AccountFarmDetailsGrowingPracticeRegenerative,
   9872             ))
   9873             .child(account_farm_profile_summary_row(
   9874                 AppTextKey::AccountFarmDetailsSeasonSummaryLabel,
   9875                 AppTextKey::AccountFarmDetailsSeasonStartValue,
   9876             ))
   9877             .child(account_farm_profile_summary_row(
   9878                 AppTextKey::AccountFarmDetailsOrderDaysSummaryLabel,
   9879                 AppTextKey::AccountFarmDetailsOrderDaysSummaryValue,
   9880             ))
   9881             .child(account_farm_profile_summary_row(
   9882                 AppTextKey::AccountFarmDetailsFarmTypeSummaryLabel,
   9883                 AppTextKey::AccountFarmDetailsFarmTypeVegetableFarm,
   9884             ))
   9885             .child(action_button_full_width(
   9886                 "account-farm-profile-preview",
   9887                 app_shared_text(AppTextKey::AccountFarmDetailsViewFarmProfileAction),
   9888                 |_, _, _| {},
   9889                 cx,
   9890             )),
   9891     )
   9892 }
   9893 
   9894 fn account_farm_fulfillment_card(
   9895     form: &AccountFarmProfileFormState,
   9896     is_textarea_wrap_ready: bool,
   9897     cx: &mut Context<HomeView>,
   9898 ) -> impl IntoElement + use<> {
   9899     account_farm_profile_card(
   9900         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
   9901             .w_full()
   9902             .child(account_farm_profile_title_block(
   9903                 AppTextKey::AccountFarmDetailsPickupFulfillmentTitle,
   9904                 AppTextKey::AccountFarmDetailsPickupFulfillmentIntro,
   9905             ))
   9906             .child(account_farm_static_chip_group(
   9907                 AppTextKey::AccountFarmDetailsFulfillmentModeLabel,
   9908                 &[
   9909                     AppTextKey::AccountFarmDetailsFulfillmentPickupOnly,
   9910                     AppTextKey::AccountFarmDetailsFulfillmentDelivery,
   9911                     AppTextKey::AccountFarmDetailsFulfillmentBoth,
   9912                 ],
   9913                 2,
   9914             ))
   9915             .child(account_farm_profile_labeled_control_with_helper(
   9916                 AppTextKey::AccountFarmDetailsPrimaryPickupLocationLabel,
   9917                 account_farm_profile_select_input(&form.primary_pickup_location_select),
   9918                 Some(AppTextKey::AccountFarmDetailsPrimaryPickupLocationAddressValue),
   9919             ))
   9920             .child(account_farm_profile_text_area_field_with_helper(
   9921                 AppTextKey::AccountFarmDetailsPickupInstructionsLabel,
   9922                 AppTextKey::AccountFarmDetailsPickupInstructionsHelper,
   9923                 &form.pickup_instructions_input,
   9924                 is_textarea_wrap_ready,
   9925                 cx,
   9926             ))
   9927             .child(account_farm_pickup_schedule_row(form, cx))
   9928             .child(account_farm_delivery_radius_row(
   9929                 &form.delivery_radius_input,
   9930             )),
   9931     )
   9932 }
   9933 
   9934 fn account_farm_pickup_schedule_row(
   9935     form: &AccountFarmProfileFormState,
   9936     cx: &mut Context<HomeView>,
   9937 ) -> impl IntoElement {
   9938     div()
   9939         .w_full()
   9940         .flex()
   9941         .items_start()
   9942         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
   9943         .child(
   9944             app_stack_v(8.0)
   9945                 .flex_1()
   9946                 .min_w_0()
   9947                 .child(account_farm_field_label(
   9948                     AppTextKey::AccountFarmDetailsPickupWindowsLabel,
   9949                 ))
   9950                 .child(account_farm_pickup_windows_table())
   9951                 .child(div().w(px(180.0)).child(action_button_compact(
   9952                     "account-farm-add-pickup-window",
   9953                     app_shared_text(AppTextKey::AccountFarmDetailsAddPickupWindowAction),
   9954                     |_, _, _| {},
   9955                     cx,
   9956                 ))),
   9957         )
   9958         .child(
   9959             div()
   9960                 .w(px(176.0))
   9961                 .child(account_farm_profile_labeled_control_with_helper(
   9962                     AppTextKey::AccountFarmDetailsOrderCutoffLabel,
   9963                     account_farm_profile_select_input(&form.order_cutoff_select),
   9964                     Some(AppTextKey::AccountFarmDetailsOrderCutoffHelper),
   9965                 )),
   9966         )
   9967 }
   9968 
   9969 fn account_farm_pickup_windows_table() -> impl IntoElement {
   9970     app_stack_v(0.0)
   9971         .w_full()
   9972         .border_1()
   9973         .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider))
   9974         .rounded(px(APP_UI_THEME.foundation.radii.medium_px))
   9975         .overflow_hidden()
   9976         .child(account_farm_pickup_window_table_row(
   9977             AppTextKey::AccountFarmDetailsPickupWindowDayHeader,
   9978             AppTextKey::AccountFarmDetailsPickupWindowStartHeader,
   9979             AppTextKey::AccountFarmDetailsPickupWindowEndHeader,
   9980             false,
   9981         ))
   9982         .child(account_farm_pickup_window_table_row(
   9983             AppTextKey::AccountFarmDetailsPickupWindowWednesday,
   9984             AppTextKey::AccountFarmDetailsPickupWindowWednesdayStart,
   9985             AppTextKey::AccountFarmDetailsPickupWindowWednesdayEnd,
   9986             true,
   9987         ))
   9988         .child(account_farm_pickup_window_table_row(
   9989             AppTextKey::AccountFarmDetailsPickupWindowSaturday,
   9990             AppTextKey::AccountFarmDetailsPickupWindowSaturdayStart,
   9991             AppTextKey::AccountFarmDetailsPickupWindowSaturdayEnd,
   9992             true,
   9993         ))
   9994 }
   9995 
   9996 fn account_farm_pickup_window_table_row(
   9997     day_key: AppTextKey,
   9998     start_key: AppTextKey,
   9999     end_key: AppTextKey,
  10000     action: bool,
  10001 ) -> impl IntoElement {
  10002     let bg = if action {
  10003         APP_UI_THEME.foundation.surfaces.window_background
  10004     } else {
  10005         APP_UI_THEME.foundation.surfaces.card_background
  10006     };
  10007 
  10008     div()
  10009         .w_full()
  10010         .bg(rgb(bg))
  10011         .px(px(10.0))
  10012         .py(px(7.0))
  10013         .flex()
  10014         .items_center()
  10015         .gap(px(10.0))
  10016         .child(account_farm_table_cell(day_key, 1.0))
  10017         .child(account_farm_table_cell(start_key, 1.0))
  10018         .child(account_farm_table_cell(end_key, 1.0))
  10019         .when(action, |this| {
  10020             this.child(
  10021                 div()
  10022                     .flex_none()
  10023                     .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  10024                     .child(Icon::new(IconName::Ellipsis).with_size(gpui_component::Size::Small)),
  10025             )
  10026         })
  10027 }
  10028 
  10029 fn account_farm_table_cell(label_key: AppTextKey, basis: f32) -> impl IntoElement {
  10030     div()
  10031         .flex_basis(relative(basis))
  10032         .min_w_0()
  10033         .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
  10034         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  10035         .child(app_shared_text(label_key))
  10036 }
  10037 
  10038 fn account_farm_delivery_radius_row(input: &Entity<InputState>) -> impl IntoElement {
  10039     app_stack_v(8.0)
  10040         .w_full()
  10041         .pt(px(APP_UI_THEME.shells.home_stack_gap_px))
  10042         .child(account_farm_profile_title_block(
  10043             AppTextKey::AccountFarmDetailsDeliveryRadiusTitle,
  10044             AppTextKey::AccountFarmDetailsDeliveryRadiusHelper,
  10045         ))
  10046         .child(
  10047             div()
  10048                 .w_full()
  10049                 .flex()
  10050                 .items_center()
  10051                 .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  10052                 .child(account_farm_slider_preview())
  10053                 .child(div().w(px(74.0)).child(account_form_text_input(input)))
  10054                 .child(
  10055                     div()
  10056                         .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
  10057                         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  10058                         .child(app_shared_text(
  10059                             AppTextKey::AccountFarmDetailsDeliveryRadiusUnit,
  10060                         )),
  10061                 ),
  10062         )
  10063         .child(account_farm_profile_helper_text(
  10064             AppTextKey::AccountFarmDetailsDeliveryRadiusNote,
  10065         ))
  10066 }
  10067 
  10068 fn account_farm_slider_preview() -> impl IntoElement {
  10069     div()
  10070         .flex_1()
  10071         .min_w_0()
  10072         .h(px(4.0))
  10073         .rounded(px(2.0))
  10074         .bg(rgb(APP_UI_THEME.foundation.surfaces.divider))
  10075         .child(div().w(relative(0.38)).h(px(4.0)).rounded(px(2.0)).bg(rgb(
  10076             APP_UI_THEME.components.app_button.primary_colors.background,
  10077         )))
  10078 }
  10079 
  10080 fn account_farm_customer_experience_card() -> impl IntoElement {
  10081     account_farm_profile_rail_card(
  10082         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  10083             .w_full()
  10084             .child(account_farm_preview_title(
  10085                 AppTextKey::AccountFarmDetailsCustomerExperienceTitle,
  10086             ))
  10087             .child(account_farm_profile_helper_text(
  10088                 AppTextKey::AccountFarmDetailsCustomerExperienceIntro,
  10089             ))
  10090             .child(account_farm_customer_experience_panel(
  10091                 AppTextKey::AccountFarmDetailsCustomerExperiencePickupTitle,
  10092                 AppTextKey::AccountFarmDetailsPrimaryPickupLocationTitleValue,
  10093                 AppTextKey::AccountFarmDetailsPrimaryPickupLocationAddressValue,
  10094             ))
  10095             .child(account_farm_customer_experience_panel(
  10096                 AppTextKey::AccountFarmDetailsCustomerExperienceDeliveryTitle,
  10097                 AppTextKey::AccountFarmDetailsCustomerExperienceDeliveryBody,
  10098                 AppTextKey::AccountFarmDetailsServiceAreaValue,
  10099             )),
  10100     )
  10101 }
  10102 
  10103 fn account_farm_customer_experience_panel(
  10104     title_key: AppTextKey,
  10105     primary_key: AppTextKey,
  10106     secondary_key: AppTextKey,
  10107 ) -> impl IntoElement {
  10108     div()
  10109         .w_full()
  10110         .rounded(px(APP_UI_THEME.foundation.radii.medium_px))
  10111         .bg(rgb(APP_UI_THEME.foundation.surfaces.card_background))
  10112         .p(px(10.0))
  10113         .child(
  10114             app_stack_v(4.0)
  10115                 .w_full()
  10116                 .child(
  10117                     div()
  10118                         .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
  10119                         .font_weight(gpui::FontWeight::BOLD)
  10120                         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  10121                         .child(app_shared_text(title_key)),
  10122                 )
  10123                 .child(
  10124                     div()
  10125                         .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
  10126                         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  10127                         .child(app_shared_text(primary_key)),
  10128                 )
  10129                 .child(
  10130                     div()
  10131                         .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
  10132                         .line_height(relative(1.25))
  10133                         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  10134                         .child(app_shared_text(secondary_key)),
  10135                 ),
  10136         )
  10137 }
  10138 
  10139 fn account_farm_profile_card(content: impl IntoElement) -> impl IntoElement {
  10140     div()
  10141         .size_full()
  10142         .overflow_hidden()
  10143         .border_1()
  10144         .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider))
  10145         .rounded(px(APP_UI_THEME.foundation.radii.large_px))
  10146         .bg(transparent_black())
  10147         .child(app_scroll_panel(
  10148             "account-farm-card-scroll",
  10149             APP_UI_THEME.shells.home_card_padding_px,
  10150             None,
  10151             content,
  10152         ))
  10153 }
  10154 
  10155 fn account_farm_profile_rail_card(content: impl IntoElement) -> impl IntoElement {
  10156     div()
  10157         .w_full()
  10158         .border_1()
  10159         .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider))
  10160         .rounded(px(APP_UI_THEME.foundation.radii.large_px))
  10161         .bg(transparent_black())
  10162         .child(
  10163             div()
  10164                 .w_full()
  10165                 .p(px(APP_UI_THEME.shells.home_card_padding_px))
  10166                 .child(content),
  10167         )
  10168 }
  10169 
  10170 fn account_farm_profile_title_block(
  10171     title_key: AppTextKey,
  10172     body_key: AppTextKey,
  10173 ) -> impl IntoElement {
  10174     app_stack_v(8.0)
  10175         .w_full()
  10176         .child(
  10177             div()
  10178                 .w_full()
  10179                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 1.1))
  10180                 .font_weight(gpui::FontWeight::BOLD)
  10181                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  10182                 .child(app_shared_text(title_key)),
  10183         )
  10184         .child(
  10185             div()
  10186                 .w_full()
  10187                 .line_height(relative(1.35))
  10188                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  10189                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  10190                 .child(app_shared_text(body_key)),
  10191         )
  10192 }
  10193 
  10194 fn account_farm_profile_field_row(
  10195     first: impl IntoElement,
  10196     second: impl IntoElement,
  10197 ) -> impl IntoElement {
  10198     div()
  10199         .w_full()
  10200         .flex()
  10201         .items_start()
  10202         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  10203         .child(
  10204             div()
  10205                 .flex_1()
  10206                 .min_w(px(ACCOUNT_FARM_DETAILS_FIELD_MIN_WIDTH_PX))
  10207                 .child(first),
  10208         )
  10209         .child(
  10210             div()
  10211                 .flex_1()
  10212                 .min_w(px(ACCOUNT_FARM_DETAILS_FIELD_MIN_WIDTH_PX))
  10213                 .child(second),
  10214         )
  10215 }
  10216 
  10217 fn account_farm_field_label(label_key: AppTextKey) -> impl IntoElement {
  10218     div()
  10219         .w_full()
  10220         .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  10221         .font_weight(gpui::FontWeight::SEMIBOLD)
  10222         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  10223         .child(app_shared_text(label_key))
  10224 }
  10225 
  10226 fn account_farm_profile_helper_text(text_key: AppTextKey) -> impl IntoElement {
  10227     div()
  10228         .w_full()
  10229         .line_height(relative(1.3))
  10230         .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
  10231         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  10232         .child(app_shared_text(text_key))
  10233 }
  10234 
  10235 fn account_farm_profile_labeled_control_with_helper(
  10236     label_key: AppTextKey,
  10237     control: impl IntoElement,
  10238     helper_key: Option<AppTextKey>,
  10239 ) -> impl IntoElement {
  10240     app_stack_v(6.0)
  10241         .w_full()
  10242         .child(account_farm_field_label(label_key))
  10243         .child(control)
  10244         .when_some(helper_key, |this, helper_key| {
  10245             this.child(account_farm_profile_helper_text(helper_key))
  10246         })
  10247 }
  10248 
  10249 fn account_farm_profile_select_field(
  10250     label_key: AppTextKey,
  10251     select: &Entity<AccountFarmProfileSelectState>,
  10252 ) -> impl IntoElement {
  10253     account_profile_labeled_control(label_key, account_farm_profile_select_input(select))
  10254 }
  10255 
  10256 fn account_farm_profile_text_area_field(
  10257     label_key: AppTextKey,
  10258     input: &Entity<InputState>,
  10259     is_wrap_ready: bool,
  10260     cx: &mut Context<HomeView>,
  10261 ) -> impl IntoElement {
  10262     account_profile_labeled_control(
  10263         label_key,
  10264         account_form_text_area_input_with_wrapped_preview(input, is_wrap_ready, cx),
  10265     )
  10266 }
  10267 
  10268 fn account_farm_profile_text_area_field_with_helper(
  10269     label_key: AppTextKey,
  10270     helper_key: AppTextKey,
  10271     input: &Entity<InputState>,
  10272     is_wrap_ready: bool,
  10273     cx: &mut Context<HomeView>,
  10274 ) -> impl IntoElement {
  10275     account_farm_profile_labeled_control_with_helper(
  10276         label_key,
  10277         account_form_text_area_input_with_wrapped_preview(input, is_wrap_ready, cx),
  10278         Some(helper_key),
  10279     )
  10280 }
  10281 
  10282 fn account_farm_toggle_preview_row(
  10283     label_key: AppTextKey,
  10284     helper_key: AppTextKey,
  10285 ) -> impl IntoElement {
  10286     div()
  10287         .w_full()
  10288         .flex()
  10289         .items_start()
  10290         .gap(px(10.0))
  10291         .child(
  10292             div()
  10293                 .flex_none()
  10294                 .w(px(34.0))
  10295                 .h(px(20.0))
  10296                 .rounded(px(10.0))
  10297                 .bg(rgb(APP_UI_THEME.foundation.surfaces.divider))
  10298                 .p(px(2.0))
  10299                 .child(
  10300                     div()
  10301                         .size(px(16.0))
  10302                         .rounded(px(8.0))
  10303                         .bg(rgb(APP_UI_THEME.foundation.surfaces.window_background)),
  10304                 ),
  10305         )
  10306         .child(
  10307             app_stack_v(3.0)
  10308                 .flex_1()
  10309                 .min_w_0()
  10310                 .child(
  10311                     div()
  10312                         .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
  10313                         .font_weight(gpui::FontWeight::MEDIUM)
  10314                         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  10315                         .child(app_shared_text(label_key)),
  10316                 )
  10317                 .child(account_farm_profile_helper_text(helper_key)),
  10318         )
  10319 }
  10320 
  10321 fn account_farm_preview_title(title_key: AppTextKey) -> impl IntoElement {
  10322     div()
  10323         .w_full()
  10324         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 1.1))
  10325         .font_weight(gpui::FontWeight::BOLD)
  10326         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  10327         .child(app_shared_text(title_key))
  10328 }
  10329 
  10330 fn account_farm_icon_value_row(
  10331     icon_name: IconName,
  10332     title_key: AppTextKey,
  10333     subtitle_key: AppTextKey,
  10334 ) -> impl IntoElement {
  10335     div()
  10336         .w_full()
  10337         .flex()
  10338         .items_center()
  10339         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  10340         .child(
  10341             div()
  10342                 .size(px(46.0))
  10343                 .rounded(px(23.0))
  10344                 .bg(rgb(0xA7F3B8))
  10345                 .flex()
  10346                 .items_center()
  10347                 .justify_center()
  10348                 .child(
  10349                     Icon::new(icon_name)
  10350                         .with_size(gpui_component::Size::Size(px(24.0)))
  10351                         .text_color(rgb(APP_UI_THEME.foundation.text.primary)),
  10352                 ),
  10353         )
  10354         .child(
  10355             app_stack_v(3.0)
  10356                 .flex_1()
  10357                 .min_w_0()
  10358                 .child(
  10359                     div()
  10360                         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  10361                         .font_weight(gpui::FontWeight::BOLD)
  10362                         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  10363                         .child(app_shared_text(title_key)),
  10364                 )
  10365                 .child(
  10366                     div()
  10367                         .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
  10368                         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  10369                         .child(app_shared_text(subtitle_key)),
  10370                 ),
  10371         )
  10372 }
  10373 
  10374 fn account_farm_static_chip_group(
  10375     label_key: AppTextKey,
  10376     chips: &[AppTextKey],
  10377     selected_count: usize,
  10378 ) -> impl IntoElement {
  10379     account_farm_profile_labeled_control_with_helper(
  10380         label_key,
  10381         account_farm_chip_wrap(chips, selected_count),
  10382         None,
  10383     )
  10384 }
  10385 
  10386 fn account_farm_static_chip_group_with_helper(
  10387     label_key: AppTextKey,
  10388     helper_key: AppTextKey,
  10389     chips: &[AppTextKey],
  10390     selected_count: usize,
  10391 ) -> impl IntoElement {
  10392     account_farm_profile_labeled_control_with_helper(
  10393         label_key,
  10394         account_farm_chip_wrap(chips, selected_count),
  10395         Some(helper_key),
  10396     )
  10397 }
  10398 
  10399 fn account_farm_chip_wrap(chips: &[AppTextKey], selected_count: usize) -> impl IntoElement {
  10400     div().w_full().flex().flex_wrap().gap(px(8.0)).children(
  10401         chips
  10402             .iter()
  10403             .enumerate()
  10404             .map(|(index, key)| account_farm_chip(*key, index < selected_count).into_any_element()),
  10405     )
  10406 }
  10407 
  10408 fn account_farm_chip(label_key: AppTextKey, selected: bool) -> impl IntoElement {
  10409     let border = if selected {
  10410         APP_UI_THEME.components.app_button.primary_colors.background
  10411     } else {
  10412         APP_UI_THEME.foundation.surfaces.divider
  10413     };
  10414     let text = if selected {
  10415         APP_UI_THEME.components.app_button.primary_colors.background
  10416     } else {
  10417         APP_UI_THEME.foundation.text.primary
  10418     };
  10419 
  10420     div()
  10421         .rounded(px(APP_UI_THEME.foundation.radii.medium_px))
  10422         .border_1()
  10423         .border_color(rgb(border))
  10424         .bg(rgb(APP_UI_THEME.foundation.surfaces.window_background))
  10425         .px(px(10.0))
  10426         .py(px(6.0))
  10427         .flex()
  10428         .items_center()
  10429         .gap(px(6.0))
  10430         .when(selected, |this| {
  10431             this.child(
  10432                 Icon::new(IconName::Check)
  10433                     .with_size(gpui_component::Size::Small)
  10434                     .text_color(rgb(text)),
  10435             )
  10436         })
  10437         .child(
  10438             div()
  10439                 .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
  10440                 .font_weight(gpui::FontWeight::MEDIUM)
  10441                 .text_color(rgb(text))
  10442                 .child(app_shared_text(label_key)),
  10443         )
  10444 }
  10445 
  10446 fn account_farm_day_chip_group() -> impl IntoElement {
  10447     account_farm_static_chip_group(
  10448         AppTextKey::AccountFarmDetailsOrderDaysLabel,
  10449         &[
  10450             AppTextKey::AccountFarmDetailsDayMon,
  10451             AppTextKey::AccountFarmDetailsDayTue,
  10452             AppTextKey::AccountFarmDetailsDayWed,
  10453             AppTextKey::AccountFarmDetailsDayThu,
  10454             AppTextKey::AccountFarmDetailsDayFri,
  10455             AppTextKey::AccountFarmDetailsDaySat,
  10456             AppTextKey::AccountFarmDetailsDaySun,
  10457         ],
  10458         5,
  10459     )
  10460 }
  10461 
  10462 fn account_farm_date_range_fields(
  10463     label_key: AppTextKey,
  10464     start_input: &Entity<InputState>,
  10465     end_input: &Entity<InputState>,
  10466 ) -> impl IntoElement {
  10467     app_stack_v(6.0)
  10468         .w_full()
  10469         .child(account_farm_field_label(label_key))
  10470         .child(
  10471             div()
  10472                 .w_full()
  10473                 .flex()
  10474                 .items_center()
  10475                 .gap(px(8.0))
  10476                 .child(
  10477                     div()
  10478                         .flex_1()
  10479                         .min_w_0()
  10480                         .child(account_form_text_input(start_input)),
  10481                 )
  10482                 .child(
  10483                     Icon::new(IconName::ArrowRight)
  10484                         .with_size(gpui_component::Size::Small)
  10485                         .text_color(rgb(APP_UI_THEME.foundation.text.secondary)),
  10486                 )
  10487                 .child(
  10488                     div()
  10489                         .flex_1()
  10490                         .min_w_0()
  10491                         .child(account_form_text_input(end_input)),
  10492                 ),
  10493         )
  10494 }
  10495 
  10496 fn account_farm_profile_summary_card(cx: &mut Context<HomeView>) -> impl IntoElement {
  10497     account_farm_profile_rail_card(
  10498         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  10499             .w_full()
  10500             .child(
  10501                 div()
  10502                     .w_full()
  10503                     .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 1.1))
  10504                     .font_weight(gpui::FontWeight::BOLD)
  10505                     .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  10506                     .child(app_shared_text(AppTextKey::AccountFarmDetailsSummaryTitle)),
  10507             )
  10508             .child(
  10509                 div()
  10510                     .w_full()
  10511                     .flex()
  10512                     .items_center()
  10513                     .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  10514                     .child(
  10515                         div()
  10516                             .size(px(56.0))
  10517                             .rounded(px(28.0))
  10518                             .bg(rgb(0xA7F3B8))
  10519                             .flex()
  10520                             .items_center()
  10521                             .justify_center()
  10522                             .child(
  10523                                 Icon::new(IconName::Building2)
  10524                                     .with_size(gpui_component::Size::Size(px(28.0)))
  10525                                     .text_color(rgb(APP_UI_THEME.foundation.text.primary)),
  10526                             ),
  10527                     )
  10528                     .child(
  10529                         app_stack_v(4.0)
  10530                             .flex_1()
  10531                             .min_w_0()
  10532                             .child(
  10533                                 div()
  10534                                     .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  10535                                     .font_weight(gpui::FontWeight::BOLD)
  10536                                     .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  10537                                     .child(app_shared_text(
  10538                                         AppTextKey::AccountFarmDetailsFarmNameValue,
  10539                                     )),
  10540                             )
  10541                             .child(
  10542                                 div()
  10543                                     .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  10544                                     .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  10545                                     .child(app_shared_text(
  10546                                         AppTextKey::AccountFarmDetailsFarmLocationValue,
  10547                                     )),
  10548                             ),
  10549                     ),
  10550             )
  10551             .child(account_farm_profile_summary_row(
  10552                 AppTextKey::AccountFarmDetailsFarmTypeSummaryLabel,
  10553                 AppTextKey::AccountFarmDetailsFarmTypeVegetableFarm,
  10554             ))
  10555             .child(account_farm_profile_summary_row(
  10556                 AppTextKey::AccountFarmDetailsEstablishedSummaryLabel,
  10557                 AppTextKey::AccountFarmDetailsEstablishedYearValue,
  10558             ))
  10559             .child(action_button_full_width(
  10560                 "account-farm-view-profile",
  10561                 app_shared_text(AppTextKey::AccountFarmDetailsViewFarmProfileAction),
  10562                 |_, _, _| {},
  10563                 cx,
  10564             )),
  10565     )
  10566 }
  10567 
  10568 fn account_farm_profile_summary_row(
  10569     label_key: AppTextKey,
  10570     value_key: AppTextKey,
  10571 ) -> impl IntoElement {
  10572     div()
  10573         .w_full()
  10574         .flex()
  10575         .items_center()
  10576         .justify_between()
  10577         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  10578         .child(
  10579             div()
  10580                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  10581                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  10582                 .child(app_shared_text(label_key)),
  10583         )
  10584         .child(
  10585             div()
  10586                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  10587                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  10588                 .child(app_shared_text(value_key)),
  10589         )
  10590 }
  10591 
  10592 fn account_settings_panel(
  10593     form: &AccountSettingsFormState,
  10594     cx: &mut Context<HomeView>,
  10595 ) -> impl IntoElement {
  10596     app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  10597         .w_full()
  10598         .child(
  10599             div()
  10600                 .w_full()
  10601                 .flex()
  10602                 .items_start()
  10603                 .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  10604                 .child(
  10605                     div()
  10606                         .flex_1()
  10607                         .min_w_0()
  10608                         .child(account_settings_nostr_relays_card(form, cx)),
  10609                 )
  10610                 .child(
  10611                     div()
  10612                         .flex_basis(relative(0.4))
  10613                         .min_w(px(320.0))
  10614                         .child(account_settings_blossom_server_card(form, cx)),
  10615                 ),
  10616         )
  10617 }
  10618 
  10619 fn account_settings_nostr_relays_card(
  10620     form: &AccountSettingsFormState,
  10621     cx: &mut Context<HomeView>,
  10622 ) -> impl IntoElement {
  10623     account_settings_card(
  10624         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  10625             .w_full()
  10626             .child(account_farm_profile_title_block(
  10627                 AppTextKey::AccountSettingsNostrRelaysTitle,
  10628                 AppTextKey::AccountSettingsNostrRelaysHelper,
  10629             ))
  10630             .child(account_settings_relay_list(cx))
  10631             .child(account_settings_add_relay_controls(form, cx))
  10632             .child(div().w(px(160.0)).child(action_button_full_width(
  10633                 "account-settings-reset-relays",
  10634                 app_shared_text(AppTextKey::AccountSettingsResetRelaysAction),
  10635                 |_, _, _| {},
  10636                 cx,
  10637             )))
  10638             .child(account_settings_helper_note(
  10639                 AppTextKey::AccountSettingsDefaultRelaysNote,
  10640             )),
  10641     )
  10642 }
  10643 
  10644 fn account_settings_relay_list(cx: &mut Context<HomeView>) -> impl IntoElement {
  10645     app_stack_v(0.0)
  10646         .w_full()
  10647         .border_1()
  10648         .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider))
  10649         .rounded(px(APP_UI_THEME.foundation.radii.medium_px))
  10650         .overflow_hidden()
  10651         .child(account_settings_relay_row(
  10652             "account-settings-relay-localhost-8080",
  10653             "account-settings-relay-menu-localhost-8080",
  10654             ACCOUNT_SETTINGS_RELAY_LOCALHOST_8080,
  10655             AppTextKey::AccountSettingsRelayAccessReadWrite,
  10656             APP_UI_THEME.components.app_status_indicator.online,
  10657             cx,
  10658         ))
  10659         .child(account_settings_relay_row(
  10660             "account-settings-relay-localhost-8081",
  10661             "account-settings-relay-menu-localhost-8081",
  10662             ACCOUNT_SETTINGS_RELAY_LOCALHOST_8081,
  10663             AppTextKey::AccountSettingsRelayAccessReadOnly,
  10664             APP_UI_THEME.components.app_status_indicator.offline,
  10665             cx,
  10666         ))
  10667 }
  10668 
  10669 fn account_settings_relay_row(
  10670     row_id: &'static str,
  10671     menu_id: &'static str,
  10672     relay_url: &'static str,
  10673     access_key: AppTextKey,
  10674     status_color: u32,
  10675     cx: &mut Context<HomeView>,
  10676 ) -> impl IntoElement {
  10677     div()
  10678         .id(row_id)
  10679         .w_full()
  10680         .min_h(px(52.0))
  10681         .px(px(APP_UI_THEME.foundation.spacing.medium_px))
  10682         .py(px(APP_UI_THEME.foundation.spacing.small_px))
  10683         .flex()
  10684         .items_center()
  10685         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  10686         .child(
  10687             div()
  10688                 .flex_1()
  10689                 .min_w_0()
  10690                 .flex()
  10691                 .items_center()
  10692                 .gap(px(APP_UI_THEME.foundation.spacing.medium_px))
  10693                 .child(account_settings_relay_status_indicator(status_color))
  10694                 .child(
  10695                     div()
  10696                         .flex_1()
  10697                         .min_w_0()
  10698                         .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
  10699                         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  10700                         .child(relay_url),
  10701                 ),
  10702         )
  10703         .child(
  10704             div()
  10705                 .w(px(104.0))
  10706                 .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
  10707                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  10708                 .child(app_shared_text(access_key)),
  10709         )
  10710         .child(account_settings_relay_menu_button(menu_id, cx))
  10711 }
  10712 
  10713 fn account_settings_relay_status_indicator(status_color: u32) -> impl IntoElement {
  10714     div().flex_none().child(status_indicator(status_color))
  10715 }
  10716 
  10717 fn account_settings_relay_menu_button(
  10718     id: &'static str,
  10719     cx: &mut Context<HomeView>,
  10720 ) -> impl IntoElement {
  10721     action_ellipsis_menu(
  10722         id,
  10723         |menu, _, _| {
  10724             menu.item(PopupMenuItem::new(app_text(
  10725                 AppTextKey::AccountSettingsRelayMenuAbout,
  10726             )))
  10727             .item(PopupMenuItem::new(app_text(
  10728                 AppTextKey::AccountSettingsRelayMenuView,
  10729             )))
  10730             .item(
  10731                 PopupMenuItem::new(app_text(
  10732                     AppTextKey::AccountSettingsRelayMenuCheckConnection,
  10733                 ))
  10734                 .on_click(|_, _, _| {}),
  10735             )
  10736             .separator()
  10737             .item(account_settings_copy_menu_item())
  10738             .item(account_settings_remove_menu_item())
  10739         },
  10740         cx,
  10741     )
  10742 }
  10743 
  10744 fn account_settings_copy_menu_item() -> PopupMenuItem {
  10745     PopupMenuItem::element(|_, _| {
  10746         div()
  10747             .w(px(180.0))
  10748             .flex()
  10749             .items_center()
  10750             .justify_between()
  10751             .gap(px(APP_UI_THEME.foundation.spacing.large_px))
  10752             .child(
  10753                 div()
  10754                     .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
  10755                     .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  10756                     .child(app_shared_text(AppTextKey::AccountSettingsRelayMenuCopy)),
  10757             )
  10758             .child(
  10759                 div()
  10760                     .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  10761                     .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  10762                     .child(app_shared_text(
  10763                         AppTextKey::AccountSettingsRelayMenuCopyShortcut,
  10764                     )),
  10765             )
  10766     })
  10767     .on_click(|_, _, _| {})
  10768 }
  10769 
  10770 fn account_settings_remove_menu_item() -> PopupMenuItem {
  10771     PopupMenuItem::element(|_, _| {
  10772         div()
  10773             .w(px(180.0))
  10774             .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px))
  10775             .text_color(rgb(APP_UI_THEME.components.app_status_indicator.attention))
  10776             .child(app_shared_text(
  10777                 AppTextKey::AccountSettingsRemoveRelayAction,
  10778             ))
  10779     })
  10780     .on_click(|_, _, _| {})
  10781 }
  10782 
  10783 fn account_settings_add_relay_controls(
  10784     form: &AccountSettingsFormState,
  10785     cx: &mut Context<HomeView>,
  10786 ) -> impl IntoElement {
  10787     app_stack_v(APP_UI_THEME.foundation.spacing.tight_px)
  10788         .w_full()
  10789         .child(
  10790             div()
  10791                 .w_full()
  10792                 .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  10793                 .font_weight(gpui::FontWeight::SEMIBOLD)
  10794                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  10795                 .child(app_shared_text(AppTextKey::AccountSettingsAddRelayLabel)),
  10796         )
  10797         .child(
  10798             div()
  10799                 .w_full()
  10800                 .flex()
  10801                 .items_center()
  10802                 .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  10803                 .child(
  10804                     div()
  10805                         .flex_1()
  10806                         .min_w_0()
  10807                         .child(account_form_text_input(&form.add_relay_input)),
  10808                 )
  10809                 .child(action_button(
  10810                     "account-settings-add-relay",
  10811                     app_shared_text(AppTextKey::AccountSettingsAddRelayAction),
  10812                     |_, _, _| {},
  10813                     cx,
  10814                 )),
  10815         )
  10816 }
  10817 
  10818 fn account_settings_blossom_server_card(
  10819     form: &AccountSettingsFormState,
  10820     cx: &mut Context<HomeView>,
  10821 ) -> impl IntoElement {
  10822     account_settings_card(
  10823         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  10824             .w_full()
  10825             .child(account_farm_profile_title_block(
  10826                 AppTextKey::AccountSettingsBlossomServerTitle,
  10827                 AppTextKey::AccountSettingsBlossomServerHelper,
  10828             ))
  10829             .child(account_profile_labeled_control(
  10830                 AppTextKey::AccountSettingsBlossomServerUrlLabel,
  10831                 account_form_text_input(&form.blossom_server_input),
  10832             ))
  10833             .child(app_checkbox_field(
  10834                 AppCheckboxFieldSpec::new(
  10835                     "account-settings-blossom-product-photos",
  10836                     app_shared_text(AppTextKey::AccountSettingsBlossomProductPhotosLabel),
  10837                     Option::<SharedString>::None,
  10838                 ),
  10839                 true,
  10840                 cx,
  10841                 |_, _, _| {},
  10842             ))
  10843             .child(app_checkbox_field(
  10844                 AppCheckboxFieldSpec::new(
  10845                     "account-settings-blossom-profile-farm-media",
  10846                     app_shared_text(AppTextKey::AccountSettingsBlossomProfileFarmMediaLabel),
  10847                     Option::<SharedString>::None,
  10848                 ),
  10849                 true,
  10850                 cx,
  10851                 |_, _, _| {},
  10852             ))
  10853             .child(div().w(px(172.0)).child(action_button_full_width(
  10854                 "account-settings-reset-blossom",
  10855                 app_shared_text(AppTextKey::AccountSettingsResetBlossomServerAction),
  10856                 |_, _, _| {},
  10857                 cx,
  10858             )))
  10859             .child(account_settings_blossom_health_card(form, cx)),
  10860     )
  10861 }
  10862 
  10863 fn account_settings_blossom_health_card(
  10864     form: &AccountSettingsFormState,
  10865     cx: &mut Context<HomeView>,
  10866 ) -> impl IntoElement {
  10867     let status =
  10868         account_settings_blossom_status(form.blossom_server_input.read(cx).value().as_ref());
  10869 
  10870     div()
  10871         .w_full()
  10872         .border_1()
  10873         .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider))
  10874         .rounded(px(APP_UI_THEME.foundation.radii.medium_px))
  10875         .p(px(APP_UI_THEME.foundation.spacing.large_px))
  10876         .flex()
  10877         .items_center()
  10878         .justify_between()
  10879         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  10880         .child(
  10881             div()
  10882                 .flex()
  10883                 .items_center()
  10884                 .gap(px(APP_UI_THEME.foundation.spacing.medium_px))
  10885                 .child(status_indicator(status.indicator_color))
  10886                 .child(
  10887                     app_stack_v(APP_UI_THEME.foundation.spacing.micro_px)
  10888                         .min_w_0()
  10889                         .child(
  10890                             div()
  10891                                 .text_size(px(APP_UI_THEME
  10892                                     .foundation
  10893                                     .typography
  10894                                     .settings_row_text_px))
  10895                                 .font_weight(gpui::FontWeight::MEDIUM)
  10896                                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  10897                                 .child(app_shared_text(status.title_key)),
  10898                         )
  10899                         .child(
  10900                             div()
  10901                                 .text_size(px(APP_UI_THEME
  10902                                     .foundation
  10903                                     .typography
  10904                                     .settings_row_text_px))
  10905                                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  10906                                 .child(app_shared_text(status.body_key)),
  10907                         ),
  10908                 ),
  10909         )
  10910         .child(
  10911             Icon::new(status.icon_name)
  10912                 .with_size(gpui_component::Size::Size(px(18.0)))
  10913                 .text_color(rgb(status.indicator_color)),
  10914         )
  10915 }
  10916 
  10917 #[derive(Clone)]
  10918 struct AccountSettingsBlossomStatusPresentation {
  10919     indicator_color: u32,
  10920     icon_name: IconName,
  10921     title_key: AppTextKey,
  10922     body_key: AppTextKey,
  10923 }
  10924 
  10925 fn account_settings_blossom_status(server_url: &str) -> AccountSettingsBlossomStatusPresentation {
  10926     let trimmed = server_url.trim();
  10927     let status = APP_UI_THEME.components.app_status_indicator;
  10928 
  10929     if trimmed.is_empty() || !(trimmed.starts_with("http://") || trimmed.starts_with("https://")) {
  10930         return AccountSettingsBlossomStatusPresentation {
  10931             indicator_color: status.attention,
  10932             icon_name: IconName::CircleX,
  10933             title_key: AppTextKey::AccountSettingsBlossomConnectionInvalid,
  10934             body_key: AppTextKey::AccountSettingsBlossomUploadsUnavailable,
  10935         };
  10936     }
  10937 
  10938     if trimmed.contains("localhost") || trimmed.contains("127.0.0.1") || trimmed.contains("[::1]") {
  10939         return AccountSettingsBlossomStatusPresentation {
  10940             indicator_color: status.offline,
  10941             icon_name: IconName::TriangleAlert,
  10942             title_key: AppTextKey::AccountSettingsBlossomConnectionLocal,
  10943             body_key: AppTextKey::AccountSettingsBlossomUploadsPending,
  10944         };
  10945     }
  10946 
  10947     AccountSettingsBlossomStatusPresentation {
  10948         indicator_color: status.online,
  10949         icon_name: IconName::CircleCheck,
  10950         title_key: AppTextKey::AccountSettingsBlossomConnectionHealthy,
  10951         body_key: AppTextKey::AccountSettingsBlossomUploadsAvailable,
  10952     }
  10953 }
  10954 
  10955 fn account_settings_card(content: impl IntoElement) -> impl IntoElement {
  10956     div()
  10957         .w_full()
  10958         .border_1()
  10959         .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider))
  10960         .rounded(px(APP_UI_THEME.foundation.radii.large_px))
  10961         .bg(transparent_black())
  10962         .child(
  10963             div()
  10964                 .w_full()
  10965                 .p(px(APP_UI_THEME.shells.home_card_padding_px))
  10966                 .child(content),
  10967         )
  10968 }
  10969 
  10970 fn account_settings_helper_note(text_key: AppTextKey) -> impl IntoElement {
  10971     div()
  10972         .w_full()
  10973         .flex()
  10974         .items_center()
  10975         .gap(px(APP_UI_THEME.foundation.spacing.small_px))
  10976         .child(
  10977             Icon::new(IconName::Info)
  10978                 .with_size(gpui_component::Size::Size(px(14.0)))
  10979                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary)),
  10980         )
  10981         .child(
  10982             div()
  10983                 .flex_1()
  10984                 .min_w_0()
  10985                 .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  10986                 .line_height(relative(1.25))
  10987                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  10988                 .child(app_shared_text(text_key)),
  10989         )
  10990 }
  10991 
  10992 fn buyer_listings_feed(
  10993     section: PersonalSection,
  10994     rows: &[BuyerListingRow],
  10995     selected_product_id: Option<ProductId>,
  10996     cx: &mut Context<HomeView>,
  10997 ) -> impl IntoElement {
  10998     app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  10999         .w_full()
  11000         .children(
  11001             rows.iter()
  11002                 .enumerate()
  11003                 .map(|(index, row)| {
  11004                     buyer_listing_card(
  11005                         index,
  11006                         section,
  11007                         row,
  11008                         selected_product_id == Some(row.product_id),
  11009                         cx,
  11010                     )
  11011                 })
  11012                 .collect::<Vec<_>>(),
  11013         )
  11014 }
  11015 
  11016 fn buyer_listing_card(
  11017     index: usize,
  11018     section: PersonalSection,
  11019     row: &BuyerListingRow,
  11020     is_selected: bool,
  11021     cx: &mut Context<HomeView>,
  11022 ) -> AnyElement {
  11023     let subtitle = row
  11024         .subtitle
  11025         .as_deref()
  11026         .map(str::trim)
  11027         .filter(|subtitle| !subtitle.is_empty())
  11028         .map(str::to_owned);
  11029     app_button_card(
  11030         ("buyer-listing-open", index),
  11031         is_selected,
  11032         cx.listener({
  11033             let product_id = row.product_id;
  11034             move |this, _, _, cx| this.open_personal_product_detail(section, product_id, cx)
  11035         }),
  11036         cx,
  11037         div()
  11038             .w_full()
  11039             .min_w_0()
  11040             .p(px(APP_UI_THEME.shells.home_card_padding_px))
  11041             .flex()
  11042             .flex_col()
  11043             .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  11044             .child(
  11045                 div()
  11046                     .w_full()
  11047                     .min_w_0()
  11048                     .flex()
  11049                     .items_start()
  11050                     .justify_between()
  11051                     .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  11052                     .child(
  11053                         div()
  11054                             .flex_1()
  11055                             .min_w_0()
  11056                             .flex()
  11057                             .flex_col()
  11058                             .gap(px(4.0))
  11059                             .child(
  11060                                 div()
  11061                                     .w_full()
  11062                                     .min_w_0()
  11063                                     .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  11064                                     .font_weight(gpui::FontWeight::BOLD)
  11065                                     .line_height(relative(1.2))
  11066                                     .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  11067                                     .child(product_display_title(row.title.as_str())),
  11068                             )
  11069                             .child(
  11070                                 div()
  11071                                     .w_full()
  11072                                     .min_w_0()
  11073                                     .text_size(px(APP_UI_THEME
  11074                                         .foundation
  11075                                         .typography
  11076                                         .utility_title_text_px))
  11077                                     .font_weight(gpui::FontWeight::SEMIBOLD)
  11078                                     .text_color(rgb(APP_UI_THEME.foundation.text.accent))
  11079                                     .child(row.farm_display_name.clone()),
  11080                             )
  11081                             .when_some(subtitle, |this, subtitle| {
  11082                                 this.child(
  11083                                     div()
  11084                                         .w_full()
  11085                                         .min_w_0()
  11086                                         .text_size(px(APP_UI_THEME
  11087                                             .foundation
  11088                                             .typography
  11089                                             .utility_title_text_px))
  11090                                         .line_height(relative(1.2))
  11091                                         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  11092                                         .child(subtitle),
  11093                                 )
  11094                             }),
  11095                     )
  11096                     .child(
  11097                         div()
  11098                             .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  11099                             .font_weight(gpui::FontWeight::SEMIBOLD)
  11100                             .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  11101                             .child(buyer_listing_price_text(&row.price)),
  11102                     ),
  11103             )
  11104             .child(
  11105                 app_cluster(APP_UI_THEME.foundation.spacing.small_px)
  11106                     .w_full()
  11107                     .child(buyer_listing_chip(buyer_listing_next_window_text(row)))
  11108                     .child(buyer_listing_chip(buyer_listing_fulfillment_methods_text(
  11109                         &row.fulfillment_methods,
  11110                     )))
  11111                     .child(buyer_listing_chip(
  11112                         buyer_listing_stock_or_availability_text(row),
  11113                     )),
  11114             ),
  11115     )
  11116     .into_any_element()
  11117 }
  11118 
  11119 fn buyer_listing_chip(content: impl Into<SharedString>) -> impl IntoElement {
  11120     div()
  11121         .flex()
  11122         .items_center()
  11123         .min_w_0()
  11124         .bg(rgb(APP_UI_THEME.foundation.surfaces.window_background))
  11125         .rounded(px(APP_UI_THEME.foundation.radii.small_px))
  11126         .px(px(8.0))
  11127         .py(px(6.0))
  11128         .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  11129         .font_weight(gpui::FontWeight::MEDIUM)
  11130         .line_height(relative(1.1))
  11131         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  11132         .child(content.into())
  11133 }
  11134 
  11135 fn buyer_listing_next_window_text(row: &BuyerListingRow) -> String {
  11136     row.next_fulfillment_window_label
  11137         .clone()
  11138         .unwrap_or_else(|| row.availability.label.clone())
  11139 }
  11140 
  11141 fn buyer_listing_fulfillment_methods_text(methods: &BTreeSet<FarmOrderMethod>) -> String {
  11142     if methods.is_empty() {
  11143         return app_shared_text(AppTextKey::ValueNone).to_string();
  11144     }
  11145 
  11146     methods
  11147         .iter()
  11148         .map(|method| app_shared_text(home_farm_order_method_label_key(*method)).to_string())
  11149         .collect::<Vec<_>>()
  11150         .join(", ")
  11151 }
  11152 
  11153 fn buyer_listing_stock_or_availability_text(row: &BuyerListingRow) -> String {
  11154     match row.stock.quantity {
  11155         Some(quantity) => match row.stock.unit_label.as_deref() {
  11156             Some(unit_label) if !unit_label.trim().is_empty() => format!("{quantity} {unit_label}"),
  11157             Some(_) | None => quantity.to_string(),
  11158         },
  11159         None => row.availability.label.clone(),
  11160     }
  11161 }
  11162 
  11163 fn buyer_listing_price_text(price: &ProductPricePresentation) -> String {
  11164     let dollars = price.amount_minor_units / 100;
  11165     let cents = price.amount_minor_units % 100;
  11166 
  11167     format!("${dollars}.{cents:02} / {}", price.unit_label)
  11168 }
  11169 
  11170 fn buyer_product_detail_card(
  11171     detail: &BuyerProductDetailProjection,
  11172     replace_confirmation: Option<&BuyerCartReplaceConfirmationProjection>,
  11173     on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  11174     on_decrease_quantity: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  11175     on_increase_quantity: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  11176     on_add_to_cart: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  11177     on_confirm_replace: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  11178     on_keep_current_cart: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  11179     cx: &App,
  11180 ) -> impl IntoElement {
  11181     app_focused_detail_view(
  11182         product_display_title(detail.listing.title.as_str()),
  11183         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  11184             .w_full()
  11185             .child(settings_badge_text(
  11186                 detail.listing.farm_display_name.clone(),
  11187             ))
  11188             .when_some(
  11189                 detail
  11190                     .detail_text
  11191                     .as_deref()
  11192                     .map(str::trim)
  11193                     .filter(|value| !value.is_empty())
  11194                     .map(str::to_owned),
  11195                 |this, detail_text| this.child(home_body_text(detail_text)),
  11196             )
  11197             .child(
  11198                 app_cluster(APP_UI_THEME.foundation.spacing.small_px)
  11199                     .w_full()
  11200                     .child(buyer_listing_chip(buyer_listing_price_text(
  11201                         &detail.listing.price,
  11202                     )))
  11203                     .child(buyer_listing_chip(buyer_listing_next_window_text(
  11204                         &detail.listing,
  11205                     )))
  11206                     .child(buyer_listing_chip(buyer_listing_fulfillment_methods_text(
  11207                         &detail.listing.fulfillment_methods,
  11208                     )))
  11209                     .child(buyer_listing_chip(
  11210                         buyer_listing_stock_or_availability_text(&detail.listing),
  11211                     )),
  11212             )
  11213             .child(
  11214                 div()
  11215                     .w_full()
  11216                     .flex()
  11217                     .items_center()
  11218                     .justify_between()
  11219                     .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  11220                     .child(app_text_label(app_shared_text(
  11221                         AppTextKey::PersonalDetailQuantityLabel,
  11222                     )))
  11223                     .child(
  11224                         app_stack_h(APP_UI_THEME.foundation.spacing.small_px)
  11225                             .child(action_button_compact(
  11226                                 "buyer-detail-quantity-decrease",
  11227                                 SharedString::from("-"),
  11228                                 on_decrease_quantity,
  11229                                 cx,
  11230                             ))
  11231                             .child(
  11232                                 div()
  11233                                     .min_w(px(36.0))
  11234                                     .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  11235                                     .font_weight(gpui::FontWeight::SEMIBOLD)
  11236                                     .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  11237                                     .child(detail.selected_quantity.to_string()),
  11238                             )
  11239                             .child(action_button_compact(
  11240                                 "buyer-detail-quantity-increase",
  11241                                 SharedString::from("+"),
  11242                                 on_increase_quantity,
  11243                                 cx,
  11244                             )),
  11245                     ),
  11246             )
  11247             .when_some(replace_confirmation, |this, replace_confirmation| {
  11248                 this.child(app_surface_panel(
  11249                     app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
  11250                         .w_full()
  11251                         .p(px(APP_UI_THEME.shells.home_card_padding_px))
  11252                         .child(app_text_label(app_shared_text(
  11253                             AppTextKey::PersonalDetailReplaceCartTitle,
  11254                         )))
  11255                         .child(home_body_text(format!(
  11256                             "{} {} {}.",
  11257                             replace_confirmation.current_farm_display_name,
  11258                             app_shared_text(AppTextKey::PersonalDetailReplaceCartBody),
  11259                             replace_confirmation.incoming_farm_display_name,
  11260                         )))
  11261                         .child(
  11262                             app_cluster(APP_UI_THEME.foundation.spacing.small_px)
  11263                                 .w_full()
  11264                                 .child(action_button_primary(
  11265                                     "buyer-detail-confirm-replace",
  11266                                     app_shared_text(AppTextKey::PersonalDetailReplaceCartAction),
  11267                                     on_confirm_replace,
  11268                                     cx,
  11269                                 ))
  11270                                 .child(action_button_compact(
  11271                                     "buyer-detail-keep-current",
  11272                                     app_shared_text(
  11273                                         AppTextKey::PersonalDetailKeepCurrentCartAction,
  11274                                     ),
  11275                                     on_keep_current_cart,
  11276                                     cx,
  11277                                 )),
  11278                         ),
  11279                 ))
  11280             })
  11281             .child(action_button_primary(
  11282                 "buyer-detail-add-to-cart",
  11283                 app_shared_text(AppTextKey::PersonalDetailAddToCartAction),
  11284                 on_add_to_cart,
  11285                 cx,
  11286             )),
  11287         text_button(
  11288             "buyer-detail-back",
  11289             app_shared_text(AppTextKey::PersonalDetailBackAction),
  11290             on_close,
  11291             cx,
  11292         ),
  11293     )
  11294 }
  11295 
  11296 fn buyer_cart_card(
  11297     cart: &BuyerCartProjection,
  11298     summary: &BuyerOrderReviewSummaryProjection,
  11299     order_review_open: bool,
  11300     cx: &mut Context<HomeView>,
  11301 ) -> impl IntoElement {
  11302     app_surface_card(
  11303         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  11304             .w_full()
  11305             .children(
  11306                 cart.lines
  11307                     .iter()
  11308                     .enumerate()
  11309                     .map(|(index, line)| buyer_cart_line_card(index, line, cx).into_any_element())
  11310                     .collect::<Vec<_>>(),
  11311             )
  11312             .child(app_surface_panel(
  11313                 app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
  11314                     .w_full()
  11315                     .p(px(APP_UI_THEME.shells.home_card_padding_px))
  11316                     .child(app_text_label(app_shared_text(
  11317                         AppTextKey::PersonalOrderSummaryTitle,
  11318                     )))
  11319                     .child(label_value_list(buyer_order_summary_rows(summary))),
  11320             ))
  11321             .when(!order_review_open, |this| {
  11322                 this.child(action_button_primary(
  11323                     "buyer-cart-open-order-review",
  11324                     app_shared_text(AppTextKey::PersonalCartReviewOrderAction),
  11325                     cx.listener(|this, _, window, cx| this.open_personal_order_review(window, cx)),
  11326                     cx,
  11327                 ))
  11328             }),
  11329     )
  11330 }
  11331 
  11332 fn buyer_cart_line_card(
  11333     index: usize,
  11334     line: &radroots_app_view::BuyerCartLineProjection,
  11335     cx: &mut Context<HomeView>,
  11336 ) -> impl IntoElement {
  11337     app_surface_panel(
  11338         app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
  11339             .w_full()
  11340             .p(px(APP_UI_THEME.shells.home_card_padding_px))
  11341             .child(
  11342                 div()
  11343                     .w_full()
  11344                     .flex()
  11345                     .items_start()
  11346                     .justify_between()
  11347                     .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  11348                     .child(
  11349                         app_stack_v(4.0)
  11350                             .flex_1()
  11351                             .min_w_0()
  11352                             .child(app_text_label(product_display_title(line.title.as_str())))
  11353                             .child(settings_badge_text(line.farm_display_name.clone())),
  11354                     )
  11355                     .child(action_button_compact(
  11356                         ("buyer-cart-remove-line", index),
  11357                         app_shared_text(AppTextKey::PersonalCartRemoveLineAction),
  11358                         cx.listener({
  11359                             let product_id = line.product_id;
  11360                             move |this, _, _, cx| this.remove_personal_cart_line(product_id, cx)
  11361                         }),
  11362                         cx,
  11363                     )),
  11364             )
  11365             .child(label_value_list(vec![
  11366                 LabelValueRow::new(
  11367                     app_shared_text(AppTextKey::PersonalCartLineQuantityLabel),
  11368                     line.quantity.to_string(),
  11369                 ),
  11370                 LabelValueRow::new(
  11371                     app_shared_text(AppTextKey::PersonalCartLineUnitPriceLabel),
  11372                     buyer_listing_price_text(&line.unit_price),
  11373                 ),
  11374                 LabelValueRow::new(
  11375                     app_shared_text(AppTextKey::PersonalCartLineTotalLabel),
  11376                     buyer_money_text(
  11377                         line.line_total_minor_units,
  11378                         line.unit_price.currency_code.as_str(),
  11379                     ),
  11380                 ),
  11381             ]))
  11382             .child(buyer_listing_chip(line.fulfillment_summary.clone())),
  11383     )
  11384 }
  11385 
  11386 fn buyer_order_review_card(
  11387     form: &BuyerOrderReviewFormState,
  11388     order_review: &radroots_app_view::BuyerOrderReviewProjection,
  11389     on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  11390     on_place_order: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  11391     cx: &App,
  11392 ) -> impl IntoElement {
  11393     app_surface_card(
  11394         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  11395             .w_full()
  11396             .child(
  11397                 div()
  11398                     .w_full()
  11399                     .flex()
  11400                     .items_start()
  11401                     .justify_between()
  11402                     .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  11403                     .child(app_text_value(app_shared_text(
  11404                         AppTextKey::PersonalOrderReviewTitle,
  11405                     )))
  11406                     .child(text_button(
  11407                         "buyer-order-review-back",
  11408                         app_shared_text(AppTextKey::PersonalOrderReviewBackAction),
  11409                         on_close,
  11410                         cx,
  11411                     )),
  11412             )
  11413             .child(home_body_text(app_shared_text(
  11414                 AppTextKey::PersonalOrderReviewLocalOnlyBody,
  11415             )))
  11416             .child(app_surface_panel(
  11417                 app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
  11418                     .w_full()
  11419                     .p(px(APP_UI_THEME.shells.home_card_padding_px))
  11420                     .child(app_text_label(app_shared_text(
  11421                         AppTextKey::PersonalOrderSummaryTitle,
  11422                     )))
  11423                     .child(label_value_list(buyer_order_summary_rows(
  11424                         &order_review.summary,
  11425                     ))),
  11426             ))
  11427             .child(app_surface_panel(
  11428                 app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
  11429                     .w_full()
  11430                     .p(px(APP_UI_THEME.shells.home_card_padding_px))
  11431                     .child(app_text_label(app_shared_text(
  11432                         AppTextKey::PersonalFulfillmentTitle,
  11433                     )))
  11434                     .child(home_body_text(
  11435                         order_review
  11436                             .summary
  11437                             .fulfillment_summary
  11438                             .clone()
  11439                             .unwrap_or_else(|| app_shared_text(AppTextKey::ValueNone).to_string()),
  11440                     )),
  11441             ))
  11442             .child(app_form_section(
  11443                 app_shared_text(AppTextKey::PersonalOrderReviewContactTitle),
  11444                 div()
  11445                     .w_full()
  11446                     .flex()
  11447                     .flex_col()
  11448                     .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  11449                     .child(app_form_input_text(
  11450                         AppFormFieldSpec::new(
  11451                             app_shared_text(AppTextKey::PersonalOrderReviewFieldName),
  11452                             Option::<SharedString>::None,
  11453                         ),
  11454                         &form.name_input,
  11455                         false,
  11456                     ))
  11457                     .child(app_form_input_text(
  11458                         AppFormFieldSpec::new(
  11459                             app_shared_text(AppTextKey::PersonalOrderReviewFieldEmail),
  11460                             Option::<SharedString>::None,
  11461                         ),
  11462                         &form.email_input,
  11463                         false,
  11464                     ))
  11465                     .child(app_form_input_text(
  11466                         AppFormFieldSpec::new(
  11467                             app_shared_text(AppTextKey::PersonalOrderReviewFieldPhone),
  11468                             Option::<SharedString>::None,
  11469                         ),
  11470                         &form.phone_input,
  11471                         false,
  11472                     ))
  11473                     .child(app_form_input_text(
  11474                         AppFormFieldSpec::new(
  11475                             app_shared_text(AppTextKey::PersonalOrderReviewFieldOrderNote),
  11476                             Option::<SharedString>::None,
  11477                         ),
  11478                         &form.order_note_input,
  11479                         false,
  11480                     )),
  11481             ))
  11482             .child(if order_review.can_place_order {
  11483                 action_button_primary(
  11484                     "buyer-order-review-place-order",
  11485                     app_shared_text(AppTextKey::PersonalOrderReviewPlaceOrderAction),
  11486                     on_place_order,
  11487                     cx,
  11488                 )
  11489                 .into_any_element()
  11490             } else {
  11491                 action_button_primary_disabled(
  11492                     "buyer-order-review-place-order",
  11493                     app_shared_text(AppTextKey::PersonalOrderReviewPlaceOrderAction),
  11494                     cx,
  11495                 )
  11496                 .into_any_element()
  11497             }),
  11498     )
  11499 }
  11500 
  11501 fn buyer_order_summary_rows(summary: &BuyerOrderReviewSummaryProjection) -> Vec<LabelValueRow> {
  11502     vec![
  11503         LabelValueRow::new(
  11504             app_shared_text(AppTextKey::PersonalSummaryFarmLabel),
  11505             summary
  11506                 .farm_display_name
  11507                 .clone()
  11508                 .unwrap_or_else(|| app_shared_text(AppTextKey::ValueNone).to_string()),
  11509         ),
  11510         LabelValueRow::new(
  11511             app_shared_text(AppTextKey::PersonalSummaryItemsLabel),
  11512             summary.line_count.to_string(),
  11513         ),
  11514         LabelValueRow::new(
  11515             app_shared_text(AppTextKey::PersonalSummarySubtotalLabel),
  11516             summary
  11517                 .subtotal_minor_units
  11518                 .zip(summary.currency_code.as_deref())
  11519                 .map(|(amount, currency_code)| buyer_money_text(amount, currency_code))
  11520                 .unwrap_or_else(|| app_shared_text(AppTextKey::ValueNone).to_string()),
  11521         ),
  11522     ]
  11523 }
  11524 
  11525 fn buyer_money_text(amount_minor_units: u32, currency_code: &str) -> String {
  11526     let dollars = amount_minor_units / 100;
  11527     let cents = amount_minor_units % 100;
  11528 
  11529     if currency_code == "USD" {
  11530         format!("${dollars}.{cents:02}")
  11531     } else {
  11532         format!("{currency_code} {dollars}.{cents:02}")
  11533     }
  11534 }
  11535 
  11536 fn trade_economics_total_text(economics: &TradeEconomicsProjection) -> String {
  11537     economics
  11538         .total_minor_units
  11539         .zip(economics.currency_code.as_deref())
  11540         .map(|(amount, currency_code)| buyer_money_text(amount, currency_code))
  11541         .unwrap_or_else(|| app_shared_text(AppTextKey::ValueNone).to_string())
  11542 }
  11543 
  11544 fn trade_workflow_detail_badge_strip(workflow: &TradeWorkflowProjection) -> AnyElement {
  11545     let mut badges = vec![
  11546         trade_workflow_labeled_key_badge(
  11547             AppTextKey::TradeWorkflowAxisAgreement,
  11548             trade_agreement_status_key(workflow.agreement),
  11549         ),
  11550         trade_workflow_labeled_key_badge(
  11551             AppTextKey::TradeWorkflowAxisRevision,
  11552             trade_revision_status_key(workflow.revision),
  11553         ),
  11554     ];
  11555 
  11556     badges.push(trade_workflow_labeled_key_badge(
  11557         AppTextKey::TradeWorkflowAxisInventory,
  11558         trade_inventory_status_key(workflow.inventory),
  11559     ));
  11560     if workflow.provenance.primary_source != TradeWorkflowSource::Unknown {
  11561         badges.push(trade_workflow_labeled_key_badge(
  11562             AppTextKey::TradeWorkflowAxisSource,
  11563             trade_workflow_source_key(workflow.provenance.primary_source),
  11564         ));
  11565     }
  11566 
  11567     app_cluster(APP_UI_THEME.foundation.spacing.small_px)
  11568         .w_full()
  11569         .children(badges)
  11570         .into_any_element()
  11571 }
  11572 
  11573 fn trade_workflow_list_badge_strip(workflow: &TradeWorkflowProjection) -> AnyElement {
  11574     let mut badges = vec![trade_workflow_value_badge(trade_agreement_status_key(
  11575         workflow.agreement,
  11576     ))];
  11577 
  11578     if workflow.revision != TradeRevisionStatus::None {
  11579         badges.push(trade_workflow_value_badge(trade_revision_status_key(
  11580             workflow.revision,
  11581         )));
  11582     }
  11583 
  11584     badges.push(trade_workflow_value_badge(trade_inventory_status_key(
  11585         workflow.inventory,
  11586     )));
  11587 
  11588     app_cluster(APP_UI_THEME.foundation.spacing.tight_px)
  11589         .w_full()
  11590         .children(badges)
  11591         .into_any_element()
  11592 }
  11593 
  11594 fn trade_workflow_status_stack(workflow: &TradeWorkflowProjection) -> AnyElement {
  11595     app_stack_v(2.0)
  11596         .min_w_0()
  11597         .child(trade_workflow_value_badge(trade_agreement_status_key(
  11598             workflow.agreement,
  11599         )))
  11600         .child(trade_workflow_value_badge(trade_inventory_status_key(
  11601             workflow.inventory,
  11602         )))
  11603         .into_any_element()
  11604 }
  11605 
  11606 fn trade_workflow_labeled_key_badge(label_key: AppTextKey, value_key: AppTextKey) -> AnyElement {
  11607     settings_badge_text(format!("{}: {}", app_text(label_key), app_text(value_key)))
  11608         .into_any_element()
  11609 }
  11610 
  11611 fn trade_workflow_value_badge(value_key: AppTextKey) -> AnyElement {
  11612     settings_badge_text(app_shared_text(value_key)).into_any_element()
  11613 }
  11614 
  11615 fn trade_agreement_status_key(status: TradeAgreementStatus) -> AppTextKey {
  11616     match status {
  11617         TradeAgreementStatus::Ordered => AppTextKey::TradeWorkflowAgreementOrdered,
  11618         TradeAgreementStatus::Confirmed => AppTextKey::TradeWorkflowAgreementConfirmed,
  11619         TradeAgreementStatus::Declined => AppTextKey::TradeWorkflowAgreementDeclined,
  11620         TradeAgreementStatus::Cancelled => AppTextKey::TradeWorkflowAgreementCancelled,
  11621         TradeAgreementStatus::NeedsReview => AppTextKey::TradeWorkflowAgreementNeedsReview,
  11622     }
  11623 }
  11624 
  11625 fn trade_revision_status_key(status: TradeRevisionStatus) -> AppTextKey {
  11626     match status {
  11627         TradeRevisionStatus::None => AppTextKey::TradeWorkflowRevisionNone,
  11628         TradeRevisionStatus::ChangeProposed => AppTextKey::TradeWorkflowRevisionChangeProposed,
  11629         TradeRevisionStatus::Updated => AppTextKey::TradeWorkflowRevisionUpdated,
  11630         TradeRevisionStatus::KeptAsPlaced => AppTextKey::TradeWorkflowRevisionKeptAsPlaced,
  11631     }
  11632 }
  11633 
  11634 fn trade_inventory_status_key(status: TradeInventoryStatus) -> AppTextKey {
  11635     match status {
  11636         TradeInventoryStatus::Available => AppTextKey::TradeWorkflowInventoryAvailable,
  11637         TradeInventoryStatus::Reserved => AppTextKey::TradeWorkflowInventoryReserved,
  11638         TradeInventoryStatus::SoldOut => AppTextKey::TradeWorkflowInventorySoldOut,
  11639         TradeInventoryStatus::NeedsReview => AppTextKey::TradeWorkflowInventoryNeedsReview,
  11640     }
  11641 }
  11642 
  11643 fn trade_workflow_source_key(source: TradeWorkflowSource) -> AppTextKey {
  11644     match source {
  11645         TradeWorkflowSource::App => AppTextKey::TradeWorkflowProvenanceApp,
  11646         TradeWorkflowSource::Cli => AppTextKey::TradeWorkflowProvenanceCli,
  11647         TradeWorkflowSource::Relay => AppTextKey::TradeWorkflowProvenanceRelay,
  11648         TradeWorkflowSource::LocalEvents => AppTextKey::TradeWorkflowProvenanceLocalEvents,
  11649         TradeWorkflowSource::Unknown => AppTextKey::TradeWorkflowProvenanceUnknown,
  11650     }
  11651 }
  11652 
  11653 fn buyer_orders_list_card(
  11654     rows: &[BuyerOrdersListRow],
  11655     selected_order_id: Option<OrderId>,
  11656     cx: &mut Context<HomeView>,
  11657 ) -> AnyElement {
  11658     home_card(
  11659         app_shared_text(AppTextKey::PersonalOrdersListTitle),
  11660         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  11661             .w_full()
  11662             .children(
  11663                 rows.iter()
  11664                     .enumerate()
  11665                     .map(|(index, row)| {
  11666                         buyer_orders_list_entry(
  11667                             index,
  11668                             row,
  11669                             selected_order_id == Some(row.order_id),
  11670                             cx,
  11671                         )
  11672                     })
  11673                     .collect::<Vec<_>>(),
  11674             ),
  11675     )
  11676     .into_any_element()
  11677 }
  11678 
  11679 fn buyer_orders_retry_action_visible(orders: &BuyerOrdersScreenProjection) -> bool {
  11680     orders.has_recoverable_coordination
  11681 }
  11682 
  11683 fn buyer_orders_retry_card(cx: &mut Context<HomeView>) -> AnyElement {
  11684     home_card(
  11685         app_shared_text(AppTextKey::PersonalOrdersCoordinationRetryTitle),
  11686         app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
  11687             .w_full()
  11688             .child(home_body_text(app_shared_text(
  11689                 AppTextKey::PersonalOrdersCoordinationRetryBody,
  11690             )))
  11691             .child(action_button_primary(
  11692                 "buyer-orders-retry-coordination",
  11693                 app_shared_text(AppTextKey::PersonalOrdersCoordinationRetryAction),
  11694                 cx.listener(|this, _, _, cx| this.retry_pending_personal_order_coordination(cx)),
  11695                 cx,
  11696             )),
  11697     )
  11698     .into_any_element()
  11699 }
  11700 
  11701 fn buyer_orders_list_entry(
  11702     index: usize,
  11703     row: &BuyerOrdersListRow,
  11704     is_selected: bool,
  11705     cx: &mut Context<HomeView>,
  11706 ) -> AnyElement {
  11707     app_button_card(
  11708         ("buyer-order-open", index),
  11709         is_selected,
  11710         cx.listener({
  11711             let order_id = row.order_id;
  11712             move |this, _, _, cx| this.open_personal_order_detail(order_id, cx)
  11713         }),
  11714         cx,
  11715         div()
  11716             .w_full()
  11717             .min_w_0()
  11718             .p(px(APP_UI_THEME.shells.home_card_padding_px))
  11719             .flex()
  11720             .flex_col()
  11721             .gap(px(APP_UI_THEME.foundation.spacing.small_px))
  11722             .child(
  11723                 div()
  11724                     .w_full()
  11725                     .min_w_0()
  11726                     .flex()
  11727                     .items_start()
  11728                     .justify_between()
  11729                     .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  11730                     .child(
  11731                         app_stack_v(4.0)
  11732                             .flex_1()
  11733                             .min_w_0()
  11734                             .child(app_text_label(row.order_number.clone()))
  11735                             .child(settings_badge_text(row.farm_display_name.clone()))
  11736                             .child(settings_badge_text(trade_economics_total_text(
  11737                                 &row.workflow.economics,
  11738                             ))),
  11739                     )
  11740                     .child(
  11741                         div()
  11742                             .flex()
  11743                             .items_center()
  11744                             .gap(px(6.0))
  11745                             .child(status_indicator(buyer_orders_status_color(row.status)))
  11746                             .child(
  11747                                 div()
  11748                                     .text_size(px(APP_UI_THEME
  11749                                         .foundation
  11750                                         .typography
  11751                                         .utility_title_text_px))
  11752                                     .font_weight(gpui::FontWeight::MEDIUM)
  11753                                     .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  11754                                     .child(app_shared_text(trade_agreement_status_key(
  11755                                         row.workflow.agreement,
  11756                                     ))),
  11757                             ),
  11758                     ),
  11759             )
  11760             .child(trade_workflow_list_badge_strip(&row.workflow))
  11761             .child(buyer_listing_chip(row.fulfillment_summary.clone())),
  11762     )
  11763     .into_any_element()
  11764 }
  11765 
  11766 fn buyer_order_detail_card(
  11767     detail: &BuyerOrderDetailProjection,
  11768     replace_confirmation: Option<&BuyerCartReplaceConfirmationProjection>,
  11769     on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  11770     cx: &mut Context<HomeView>,
  11771 ) -> AnyElement {
  11772     let repeat_confirmation = replace_confirmation
  11773         .filter(|confirmation| confirmation.incoming_farm_display_name == detail.farm_display_name);
  11774 
  11775     app_focused_detail_view(
  11776         app_shared_text(AppTextKey::PersonalOrdersDetailTitle),
  11777         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  11778             .w_full()
  11779             .child(app_heading_section(detail.order_number.clone()))
  11780             .child(settings_badge_text(detail.farm_display_name.clone()))
  11781             .child(trade_workflow_detail_badge_strip(&detail.workflow))
  11782             .child(label_value_list([
  11783                 LabelValueRow::new(
  11784                     app_shared_text(AppTextKey::PersonalOrdersDetailFarmLabel),
  11785                     detail.farm_display_name.clone(),
  11786                 ),
  11787                 LabelValueRow::new(
  11788                     app_shared_text(AppTextKey::PersonalOrdersDetailFulfillmentLabel),
  11789                     detail.fulfillment_summary.clone(),
  11790                 ),
  11791                 LabelValueRow::new(
  11792                     app_shared_text(AppTextKey::PersonalOrdersDetailTotalLabel),
  11793                     trade_economics_total_text(&detail.workflow.economics),
  11794                 ),
  11795                 LabelValueRow::new(
  11796                     app_shared_text(AppTextKey::PersonalOrdersDetailNoteLabel),
  11797                     order_optional_text(detail.order_note.as_deref()),
  11798                 ),
  11799             ]))
  11800             .when(!detail.validation_receipts.is_empty(), |this| {
  11801                 this.child(validation_receipts_summary_section(
  11802                     &detail.validation_receipts,
  11803                 ))
  11804             })
  11805             .child(app_form_section(
  11806                 app_shared_text(AppTextKey::PersonalOrdersDetailItemsTitle),
  11807                 div()
  11808                     .w_full()
  11809                     .flex()
  11810                     .flex_col()
  11811                     .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
  11812                     .children(
  11813                         detail
  11814                             .items
  11815                             .iter()
  11816                             .map(order_detail_item_row)
  11817                             .collect::<Vec<_>>(),
  11818                     )
  11819                     .when(detail.items.is_empty(), |this| {
  11820                         this.child(home_body_text(app_shared_text(AppTextKey::ValueNone)))
  11821                     }),
  11822             ))
  11823             .when(
  11824                 detail.status == BuyerOrderStatus::Scheduled
  11825                     && detail.workflow.revision == TradeRevisionStatus::ChangeProposed,
  11826                 |this| {
  11827                     this.child(
  11828                         app_stack_h(APP_UI_THEME.foundation.spacing.small_px)
  11829                             .w_full()
  11830                             .child(action_button_primary(
  11831                                 "buyer-order-accept-change",
  11832                                 app_shared_text(AppTextKey::PersonalOrdersActionAcceptChange),
  11833                                 cx.listener({
  11834                                     let order_id = detail.order_id;
  11835                                     move |this, _, _, cx| {
  11836                                         this.accept_buyer_order_revision(order_id, cx)
  11837                                     }
  11838                                 }),
  11839                                 cx,
  11840                             ))
  11841                             .child(action_button_compact(
  11842                                 "buyer-order-keep-order",
  11843                                 app_shared_text(AppTextKey::PersonalOrdersActionKeepOrder),
  11844                                 cx.listener({
  11845                                     let order_id = detail.order_id;
  11846                                     move |this, _, _, cx| {
  11847                                         this.decline_buyer_order_revision(order_id, cx)
  11848                                     }
  11849                                 }),
  11850                                 cx,
  11851                             )),
  11852                     )
  11853                 },
  11854             )
  11855             .when(detail.status == BuyerOrderStatus::Placed, |this| {
  11856                 this.child(action_button_compact(
  11857                     "buyer-order-cancel",
  11858                     app_shared_text(AppTextKey::PersonalOrdersActionCancel),
  11859                     cx.listener({
  11860                         let order_id = detail.order_id;
  11861                         move |this, _, _, cx| this.cancel_buyer_order(order_id, cx)
  11862                     }),
  11863                     cx,
  11864                 ))
  11865             })
  11866             .when_some(detail.repeat_demand.as_ref(), |this, repeat_demand| {
  11867                 this.child(app_form_section(
  11868                     app_shared_text(AppTextKey::PersonalOrdersRepeatDemandTitle),
  11869                     app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
  11870                         .w_full()
  11871                         .when_some(buyer_repeat_demand_note(repeat_demand), |this, note| {
  11872                             this.child(home_body_text(note))
  11873                         })
  11874                         .when_some(repeat_confirmation, |this, replace_confirmation| {
  11875                             this.child(app_surface_panel(
  11876                                 app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
  11877                                     .w_full()
  11878                                     .p(px(APP_UI_THEME.shells.home_card_padding_px))
  11879                                     .child(app_text_label(app_shared_text(
  11880                                         AppTextKey::PersonalDetailReplaceCartTitle,
  11881                                     )))
  11882                                     .child(home_body_text(format!(
  11883                                         "{} {} {}.",
  11884                                         replace_confirmation.current_farm_display_name,
  11885                                         app_shared_text(AppTextKey::PersonalDetailReplaceCartBody,),
  11886                                         replace_confirmation.incoming_farm_display_name,
  11887                                     )))
  11888                                     .child(
  11889                                         app_cluster(APP_UI_THEME.foundation.spacing.small_px)
  11890                                             .w_full()
  11891                                             .child(action_button_primary(
  11892                                                 "buyer-order-confirm-replace",
  11893                                                 app_shared_text(
  11894                                                     AppTextKey::PersonalDetailReplaceCartAction,
  11895                                                 ),
  11896                                                 cx.listener({
  11897                                                     let order_id = detail.order_id;
  11898                                                     move |this, _, _, cx| {
  11899                                                         this.repeat_personal_order(
  11900                                                             order_id, true, cx,
  11901                                                         )
  11902                                                     }
  11903                                                 }),
  11904                                                 cx,
  11905                                             ))
  11906                                             .child(action_button_compact(
  11907                                                 "buyer-order-keep-current",
  11908                                                 app_shared_text(
  11909                                                     AppTextKey::PersonalDetailKeepCurrentCartAction,
  11910                                                 ),
  11911                                                 cx.listener(|this, _, _, cx| {
  11912                                                     this.clear_personal_cart_replace_confirmation(
  11913                                                         cx,
  11914                                                     )
  11915                                                 }),
  11916                                                 cx,
  11917                                             )),
  11918                                     ),
  11919                             ))
  11920                         })
  11921                         .when(
  11922                             repeat_confirmation.is_none()
  11923                                 && repeat_demand.eligibility
  11924                                     != RepeatDemandEligibility::Unavailable,
  11925                             |this| {
  11926                                 this.child(action_button_primary(
  11927                                     "buyer-order-repeat-demand",
  11928                                     buyer_repeat_demand_action_label(repeat_demand),
  11929                                     cx.listener({
  11930                                         let order_id = detail.order_id;
  11931                                         move |this, _, _, cx| {
  11932                                             this.repeat_personal_order(order_id, false, cx)
  11933                                         }
  11934                                     }),
  11935                                     cx,
  11936                                 ))
  11937                             },
  11938                         ),
  11939                 ))
  11940             }),
  11941         text_button(
  11942             "buyer-order-detail-back",
  11943             app_shared_text(AppTextKey::PersonalDetailBackAction),
  11944             on_close,
  11945             cx,
  11946         ),
  11947     )
  11948 }
  11949 
  11950 fn validation_receipts_summary_section(
  11951     receipts: &[TradeValidationReceiptProjection],
  11952 ) -> AnyElement {
  11953     app_form_section(
  11954         app_shared_text(AppTextKey::TradeValidationReceiptSectionLabel),
  11955         app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
  11956             .w_full()
  11957             .children(
  11958                 receipts
  11959                     .iter()
  11960                     .map(validation_receipt_summary_panel)
  11961                     .collect::<Vec<_>>(),
  11962             ),
  11963     )
  11964     .into_any_element()
  11965 }
  11966 
  11967 fn validation_receipt_summary_panel(receipt: &TradeValidationReceiptProjection) -> AnyElement {
  11968     app_surface_panel(
  11969         app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
  11970             .w_full()
  11971             .p(px(APP_UI_THEME.shells.home_card_padding_px))
  11972             .child(
  11973                 app_cluster(APP_UI_THEME.foundation.spacing.small_px)
  11974                     .w_full()
  11975                     .child(trade_workflow_value_badge(validation_receipt_result_key(
  11976                         receipt.result,
  11977                     )))
  11978                     .child(trade_workflow_value_badge(validation_receipt_type_key(
  11979                         receipt.receipt_type,
  11980                     ))),
  11981             )
  11982             .child(home_body_text(format!(
  11983                 "{} {}",
  11984                 app_shared_text(AppTextKey::TradeValidationReceiptRecordedAtLabel),
  11985                 receipt.recorded_at
  11986             ))),
  11987     )
  11988     .into_any_element()
  11989 }
  11990 
  11991 fn validation_receipt_result_key(result: TradeValidationReceiptResult) -> AppTextKey {
  11992     match result {
  11993         TradeValidationReceiptResult::Valid => AppTextKey::TradeValidationReceiptResultValid,
  11994         TradeValidationReceiptResult::NeedsReview => {
  11995             AppTextKey::TradeValidationReceiptResultNeedsReview
  11996         }
  11997     }
  11998 }
  11999 
  12000 fn validation_receipt_type_key(receipt_type: TradeValidationReceiptType) -> AppTextKey {
  12001     match receipt_type {
  12002         TradeValidationReceiptType::ListingValidation => {
  12003             AppTextKey::TradeValidationReceiptTypeListingValidation
  12004         }
  12005         TradeValidationReceiptType::TradeTransition => {
  12006             AppTextKey::TradeValidationReceiptTypeTradeTransition
  12007         }
  12008         TradeValidationReceiptType::InventoryState => {
  12009             AppTextKey::TradeValidationReceiptTypeInventoryState
  12010         }
  12011         TradeValidationReceiptType::StateCheckpoint => {
  12012             AppTextKey::TradeValidationReceiptTypeStateCheckpoint
  12013         }
  12014     }
  12015 }
  12016 
  12017 fn buyer_repeat_demand_action_label(repeat_demand: &RepeatDemandHandoffProjection) -> SharedString {
  12018     match repeat_demand.eligibility {
  12019         RepeatDemandEligibility::Eligible => {
  12020             app_shared_text(AppTextKey::PersonalOrdersRepeatDemandActionEligible)
  12021         }
  12022         RepeatDemandEligibility::Partial => {
  12023             app_shared_text(AppTextKey::PersonalOrdersRepeatDemandActionPartial)
  12024         }
  12025         RepeatDemandEligibility::Unavailable => {
  12026             app_shared_text(AppTextKey::PersonalOrdersRepeatDemandActionEligible)
  12027         }
  12028     }
  12029 }
  12030 
  12031 fn buyer_repeat_demand_note(repeat_demand: &RepeatDemandHandoffProjection) -> Option<SharedString> {
  12032     match repeat_demand.eligibility {
  12033         RepeatDemandEligibility::Eligible => None,
  12034         RepeatDemandEligibility::Partial if repeat_demand.unavailable_item_count == 1 => Some(
  12035             app_shared_text(AppTextKey::PersonalOrdersRepeatDemandNotePartialSingle),
  12036         ),
  12037         RepeatDemandEligibility::Partial => Some(app_shared_text(
  12038             AppTextKey::PersonalOrdersRepeatDemandNotePartialMultiple,
  12039         )),
  12040         RepeatDemandEligibility::Unavailable => Some(app_shared_text(
  12041             AppTextKey::PersonalOrdersRepeatDemandNoteUnavailable,
  12042         )),
  12043     }
  12044 }
  12045 
  12046 fn buyer_orders_status_color(status: BuyerOrderStatus) -> u32 {
  12047     match status {
  12048         BuyerOrderStatus::Placed => APP_UI_THEME.components.app_status_indicator.attention,
  12049         BuyerOrderStatus::Scheduled | BuyerOrderStatus::Ready => {
  12050             APP_UI_THEME.components.app_status_indicator.online
  12051         }
  12052         BuyerOrderStatus::Completed
  12053         | BuyerOrderStatus::Declined
  12054         | BuyerOrderStatus::NeedsReview => APP_UI_THEME.components.app_status_indicator.offline,
  12055     }
  12056 }
  12057 
  12058 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
  12059 enum StartupHomeSurface {
  12060     IssueCard,
  12061     ContinuePrompt,
  12062     IdentityChoice,
  12063     GenerateKeyStarting,
  12064     SignerEntry,
  12065 }
  12066 
  12067 fn startup_home_surface(runtime: &DesktopAppRuntimeSummary) -> StartupHomeSurface {
  12068     if runtime.startup_issue.is_some() || runtime.startup_gate != AppStartupGate::SetupRequired {
  12069         return StartupHomeSurface::IssueCard;
  12070     }
  12071 
  12072     match runtime.logged_out_startup.phase {
  12073         LoggedOutStartupPhase::ContinuePrompt => StartupHomeSurface::ContinuePrompt,
  12074         LoggedOutStartupPhase::IdentityChoice => StartupHomeSurface::IdentityChoice,
  12075         LoggedOutStartupPhase::GenerateKeyStarting => StartupHomeSurface::GenerateKeyStarting,
  12076         LoggedOutStartupPhase::SignerEntry => StartupHomeSurface::SignerEntry,
  12077     }
  12078 }
  12079 
  12080 fn startup_home_shell(
  12081     runtime: &DesktopAppRuntimeSummary,
  12082     startup_notice: Option<&str>,
  12083     signer_entry: Option<&StartupSignerEntryState>,
  12084     connect_state: &StartupSignerConnectState,
  12085     on_continue: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12086     on_browse_marketplace: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12087     on_generate_key: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12088     on_connect_signer: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12089     on_submit_signer: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12090     on_back: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12091     cx: &App,
  12092 ) -> impl IntoElement {
  12093     let surface = startup_home_surface(runtime);
  12094     let startup_notice = startup_notice.map(startup_notice_text);
  12095 
  12096     app_window_shell(
  12097         APP_UI_THEME.foundation.surfaces.window_background,
  12098         div()
  12099             .size_full()
  12100             .bg(rgb(APP_UI_THEME.foundation.surfaces.window_background))
  12101             .child(
  12102                 div()
  12103                     .size_full()
  12104                     .p(px(APP_UI_THEME.shells.home_window_padding_px))
  12105                     .child(
  12106                         div()
  12107                             .size_full()
  12108                             .flex()
  12109                             .items_center()
  12110                             .justify_center()
  12111                             .child(
  12112                                 app_stack_v(APP_UI_THEME.shells.startup_stack_gap_px)
  12113                                     .w_full()
  12114                                     .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
  12115                                     .mx_auto()
  12116                                     .items_center()
  12117                                     .child(startup_home_title(surface))
  12118                                     .child(startup_home_tagline())
  12119                                     .child(match surface {
  12120                                         StartupHomeSurface::ContinuePrompt => app_stack_v(
  12121                                             APP_UI_THEME.shells.startup_stack_gap_px,
  12122                                         )
  12123                                         .items_center()
  12124                                         .child(action_button_primary(
  12125                                             "home-continue",
  12126                                             app_shared_text(AppTextKey::HomeSetupContinueAction),
  12127                                             on_continue,
  12128                                             cx,
  12129                                         ))
  12130                                         .child(action_button(
  12131                                             "home-browse-marketplace",
  12132                                             app_shared_text(
  12133                                                 AppTextKey::HomeSetupBrowseMarketplaceAction,
  12134                                             ),
  12135                                             on_browse_marketplace,
  12136                                             cx,
  12137                                         ))
  12138                                         .when_some(startup_notice, |this, error: String| {
  12139                                             this.child(
  12140                                                 div()
  12141                                                     .w_full()
  12142                                                     .text_center()
  12143                                                     .child(home_body_text(error.to_owned())),
  12144                                             )
  12145                                         })
  12146                                         .into_any_element(),
  12147                                         StartupHomeSurface::IdentityChoice => {
  12148                                             app_stack_v(APP_UI_THEME.shells.startup_stack_gap_px)
  12149                                                 .items_center()
  12150                                                 .child(action_button_primary(
  12151                                                     "home-generate-key",
  12152                                                     app_shared_text(
  12153                                                         AppTextKey::HomeSetupGenerateKeyAction,
  12154                                                     ),
  12155                                                     on_generate_key,
  12156                                                     cx,
  12157                                                 ))
  12158                                                 .child(action_button(
  12159                                                     "home-connect-signer",
  12160                                                     app_shared_text(
  12161                                                         AppTextKey::HomeSetupConnectSignerAction,
  12162                                                     ),
  12163                                                     on_connect_signer,
  12164                                                     cx,
  12165                                                 ))
  12166                                                 .when_some(startup_notice, |this, error: String| {
  12167                                                     this.child(
  12168                                                         div().w_full().text_center().child(
  12169                                                             home_body_text(error.to_owned()),
  12170                                                         ),
  12171                                                     )
  12172                                                 })
  12173                                                 .into_any_element()
  12174                                         }
  12175                                         StartupHomeSurface::GenerateKeyStarting => {
  12176                                             app_stack_v(APP_UI_THEME.shells.startup_stack_gap_px)
  12177                                                 .items_center()
  12178                                                 .child(action_button_primary_disabled(
  12179                                                     "home-generate-key",
  12180                                                     app_shared_text(
  12181                                                         AppTextKey::HomeSetupGenerateKeyAction,
  12182                                                     ),
  12183                                                     cx,
  12184                                                 ))
  12185                                                 .into_any_element()
  12186                                         }
  12187                                         StartupHomeSurface::SignerEntry => {
  12188                                             startup_signer_entry_surface(
  12189                                                 signer_entry,
  12190                                                 connect_state,
  12191                                                 startup_notice,
  12192                                                 on_submit_signer,
  12193                                                 on_back,
  12194                                                 cx,
  12195                                             )
  12196                                             .into_any_element()
  12197                                         }
  12198                                         StartupHomeSurface::IssueCard => app_surface_card(
  12199                                             app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  12200                                                 .w_full()
  12201                                                 .items_center()
  12202                                                 .child(app_heading_section(app_shared_text(
  12203                                                     AppTextKey::MetadataStartupIssue,
  12204                                                 )))
  12205                                                 .child(startup_home_body(runtime)),
  12206                                         )
  12207                                         .into_any_element(),
  12208                                     }),
  12209                             ),
  12210                     ),
  12211             ),
  12212     )
  12213 }
  12214 
  12215 fn startup_home_title(surface: StartupHomeSurface) -> impl IntoElement {
  12216     let (animation_id, title_key) = if surface == StartupHomeSurface::GenerateKeyStarting {
  12217         ("startup-title-starting", AppTextKey::HomeSetupStarting)
  12218     } else {
  12219         ("startup-title-radroots", AppTextKey::HomeSetupTitle)
  12220     };
  12221 
  12222     div()
  12223         .text_center()
  12224         .child(app_heading_view(app_shared_text(title_key)))
  12225         .with_animation(
  12226             animation_id,
  12227             Animation::new(Duration::from_millis(180)),
  12228             |this, delta| this.opacity(delta),
  12229         )
  12230 }
  12231 
  12232 fn startup_home_tagline() -> impl IntoElement {
  12233     div()
  12234         .text_size(px(APP_UI_THEME
  12235             .foundation
  12236             .typography
  12237             .startup_tagline_text_px))
  12238         .font_weight(gpui::FontWeight::SEMIBOLD)
  12239         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  12240         .text_center()
  12241         .child(app_shared_text(AppTextKey::HomeSetupTagline))
  12242 }
  12243 
  12244 fn startup_signer_entry_surface(
  12245     signer_entry: Option<&StartupSignerEntryState>,
  12246     connect_state: &StartupSignerConnectState,
  12247     startup_notice: Option<String>,
  12248     on_submit_signer: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12249     on_back: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12250     cx: &App,
  12251 ) -> impl IntoElement {
  12252     let source_input = signer_entry
  12253         .map(|signer_entry| signer_entry.input.read(cx).value().to_string())
  12254         .unwrap_or_default();
  12255     let preview =
  12256         startup_signer_preview_summary_for_connect_state(source_input.as_str(), connect_state);
  12257     let parse_error = if source_input.trim().is_empty()
  12258         || !matches!(connect_state, StartupSignerConnectState::Idle)
  12259     {
  12260         None
  12261     } else {
  12262         preview
  12263             .as_ref()
  12264             .err()
  12265             .map(|error| startup_notice_text(error))
  12266     };
  12267     let submit_enabled =
  12268         preview.is_ok() && matches!(connect_state, StartupSignerConnectState::Idle);
  12269     let source_input_is_editable = startup_signer_source_input_is_editable(connect_state);
  12270 
  12271     app_stack_v(APP_UI_THEME.shells.startup_stack_gap_px)
  12272         .w_full()
  12273         .items_center()
  12274         .when_some(signer_entry, |this, signer_entry| {
  12275             this.child(
  12276                 div()
  12277                     .w_full()
  12278                     .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
  12279                     .id("home-signer-source-input")
  12280                     .child(
  12281                         app_text_input(&signer_entry.input, !source_input_is_editable)
  12282                             .disabled(!source_input_is_editable)
  12283                             .w_full(),
  12284                     ),
  12285             )
  12286         })
  12287         .when_some(preview.as_ref().ok(), |this, preview| {
  12288             this.child(app_surface_card(
  12289                 app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  12290                     .w_full()
  12291                     .items_center()
  12292                     .child(app_heading_section(app_shared_text(
  12293                         AppTextKey::HomeSetupSignerReviewTitle,
  12294                     )))
  12295                     .child(label_value_list([
  12296                         LabelValueRow::new(
  12297                             app_shared_text(AppTextKey::HomeSetupSignerSourceLabel),
  12298                             preview.source_label.clone(),
  12299                         ),
  12300                         LabelValueRow::new(
  12301                             app_shared_text(AppTextKey::HomeSetupSignerSignerLabel),
  12302                             preview.signer_npub.clone(),
  12303                         ),
  12304                         LabelValueRow::new(
  12305                             app_shared_text(AppTextKey::HomeSetupSignerRelaysLabel),
  12306                             preview.relays_label.clone(),
  12307                         ),
  12308                         LabelValueRow::new(
  12309                             app_shared_text(AppTextKey::HomeSetupSignerPermissionsLabel),
  12310                             preview.permissions_label.clone(),
  12311                         ),
  12312                     ])),
  12313             ))
  12314         })
  12315         .when_some(startup_signer_status_spec(connect_state), |this, status| {
  12316             this.child(app_surface_card(
  12317                 app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  12318                     .w_full()
  12319                     .items_center()
  12320                     .child(app_heading_section(app_shared_text(status.0)))
  12321                     .child(
  12322                         status
  12323                             .1
  12324                             .map(|body| {
  12325                                 div()
  12326                                     .w_full()
  12327                                     .text_center()
  12328                                     .child(home_body_text(body))
  12329                                     .into_any_element()
  12330                             })
  12331                             .unwrap_or_else(|| div().into_any_element()),
  12332                     ),
  12333             ))
  12334         })
  12335         .when_some(parse_error, |this, error| {
  12336             this.child(div().w_full().text_center().child(home_body_text(error)))
  12337         })
  12338         .child(if submit_enabled {
  12339             action_button_primary(
  12340                 "home-connect-signer-submit",
  12341                 app_shared_text(AppTextKey::HomeSetupSignerConnectAction),
  12342                 on_submit_signer,
  12343                 cx,
  12344             )
  12345             .into_any_element()
  12346         } else {
  12347             action_button_primary_disabled(
  12348                 "home-connect-signer-submit",
  12349                 app_shared_text(AppTextKey::HomeSetupSignerConnectAction),
  12350                 cx,
  12351             )
  12352             .into_any_element()
  12353         })
  12354         .child(text_button(
  12355             "home-signer-back",
  12356             app_shared_text(AppTextKey::HomeSetupBackAction),
  12357             on_back,
  12358             cx,
  12359         ))
  12360         .when_some(startup_notice, |this, notice: String| {
  12361             this.child(div().w_full().text_center().child(home_body_text(notice)))
  12362         })
  12363 }
  12364 
  12365 fn startup_signer_preview_summary(input: &str) -> Result<StartupSignerPreviewSummary, String> {
  12366     let target = radroots_app_remote_signer_preview(input).map_err(|error| error.to_string())?;
  12367 
  12368     Ok(StartupSignerPreviewSummary {
  12369         source_label: startup_signer_source_text(target.source),
  12370         signer_npub: target.signer_identity.public_key_npub.clone(),
  12371         relays_label: startup_signer_csv_or_none(target.relays.as_slice()),
  12372         permissions_label: startup_signer_permissions_label(target.requested_permission_labels()),
  12373     })
  12374 }
  12375 
  12376 fn startup_signer_preview_summary_for_connect_state(
  12377     input: &str,
  12378     connect_state: &StartupSignerConnectState,
  12379 ) -> Result<StartupSignerPreviewSummary, String> {
  12380     let mut preview = startup_signer_preview_summary(input)?;
  12381 
  12382     match connect_state {
  12383         StartupSignerConnectState::Idle | StartupSignerConnectState::Connecting => {}
  12384         StartupSignerConnectState::PendingApproval {
  12385             pending_session, ..
  12386         } => {
  12387             preview.signer_npub = pending_session
  12388                 .record
  12389                 .signer_identity
  12390                 .public_key_npub
  12391                 .clone();
  12392             preview.relays_label =
  12393                 startup_signer_csv_or_none(pending_session.record.relays.as_slice());
  12394             preview.permissions_label = startup_signer_requested_permissions_label();
  12395         }
  12396         StartupSignerConnectState::Approved {
  12397             pending_session,
  12398             approved_session,
  12399             ..
  12400         } => {
  12401             preview.signer_npub = pending_session
  12402                 .record
  12403                 .signer_identity
  12404                 .public_key_npub
  12405                 .clone();
  12406             preview.relays_label = startup_signer_csv_or_none(approved_session.relays.as_slice());
  12407             preview.permissions_label = startup_signer_permissions_label(
  12408                 approved_session
  12409                     .approved_permissions
  12410                     .as_slice()
  12411                     .iter()
  12412                     .map(ToString::to_string)
  12413                     .collect(),
  12414             );
  12415         }
  12416     }
  12417 
  12418     Ok(preview)
  12419 }
  12420 
  12421 fn startup_signer_source_input_is_editable(connect_state: &StartupSignerConnectState) -> bool {
  12422     matches!(connect_state, StartupSignerConnectState::Idle)
  12423 }
  12424 
  12425 fn startup_signer_csv_or_none(values: &[String]) -> String {
  12426     if values.is_empty() {
  12427         return app_text(AppTextKey::ValueNone);
  12428     }
  12429 
  12430     values.join(", ")
  12431 }
  12432 
  12433 fn startup_signer_requested_permissions_label() -> String {
  12434     startup_signer_permissions_label(
  12435         radroots_app_remote_signer_requested_permissions()
  12436             .as_slice()
  12437             .iter()
  12438             .map(ToString::to_string)
  12439             .collect(),
  12440     )
  12441 }
  12442 
  12443 fn startup_signer_permissions_label(permissions: Vec<String>) -> String {
  12444     if permissions.is_empty() {
  12445         return app_text(AppTextKey::ValueNone);
  12446     }
  12447 
  12448     permissions
  12449         .into_iter()
  12450         .map(|permission| startup_signer_permission_text(permission.as_str()))
  12451         .collect::<Vec<_>>()
  12452         .join(", ")
  12453 }
  12454 
  12455 fn startup_signer_status_spec(
  12456     connect_state: &StartupSignerConnectState,
  12457 ) -> Option<(AppTextKey, Option<String>)> {
  12458     match connect_state {
  12459         StartupSignerConnectState::Idle => None,
  12460         StartupSignerConnectState::Connecting => {
  12461             Some((AppTextKey::HomeSetupSignerConnectingTitle, None))
  12462         }
  12463         StartupSignerConnectState::PendingApproval {
  12464             auth_challenge_url, ..
  12465         } => Some(match auth_challenge_url {
  12466             Some(url) => (
  12467                 AppTextKey::HomeSetupSignerAuthChallengeTitle,
  12468                 Some(url.clone()),
  12469             ),
  12470             None => (AppTextKey::HomeSetupSignerPendingTitle, None),
  12471         }),
  12472         StartupSignerConnectState::Approved {
  12473             auth_challenge_url, ..
  12474         } => Some((
  12475             AppTextKey::HomeSetupSignerApprovedTitle,
  12476             auth_challenge_url.clone(),
  12477         )),
  12478     }
  12479 }
  12480 
  12481 fn startup_signer_transport_failure_requires_notice(message: &str) -> bool {
  12482     message != "remote signer did not respond yet"
  12483 }
  12484 
  12485 fn startup_issue_summary_text(_startup_issue: &str) -> String {
  12486     app_text(AppTextKey::HomeSetupIssueUnavailableBody)
  12487 }
  12488 
  12489 fn startup_signer_source_text(source: RadrootsAppRemoteSignerSource) -> String {
  12490     app_text(match source {
  12491         RadrootsAppRemoteSignerSource::BunkerUri => AppTextKey::HomeSetupSignerSourceValueBunkerUri,
  12492         RadrootsAppRemoteSignerSource::DiscoveryUrl => {
  12493             AppTextKey::HomeSetupSignerSourceValueDiscoveryUrl
  12494         }
  12495     })
  12496 }
  12497 
  12498 fn startup_signer_permission_text(permission: &str) -> String {
  12499     app_text(match permission {
  12500         "sign_event:kind:1" => AppTextKey::HomeSetupSignerPermissionSignEventKind1,
  12501         "switch_relays" => AppTextKey::HomeSetupSignerPermissionSwitchRelays,
  12502         _ => AppTextKey::HomeSetupSignerPermissionAdditional,
  12503     })
  12504 }
  12505 
  12506 fn startup_notice_text(message: &str) -> String {
  12507     app_text(match message {
  12508         "enter a bunker or discovery url to continue" => {
  12509             AppTextKey::HomeSetupSignerErrorEnterSource
  12510         }
  12511         "discovery url does not contain a remote signer uri" => {
  12512             AppTextKey::HomeSetupSignerErrorMissingDiscoveryUri
  12513         }
  12514         "a remote signer connection is already pending approval" => {
  12515             AppTextKey::HomeSetupSignerErrorPendingApprovalExists
  12516         }
  12517         _ if message.contains("raw nostrconnect client uris are signer-side only") => {
  12518             AppTextKey::HomeSetupSignerErrorUseSignerUri
  12519         }
  12520         _ if message.starts_with("invalid discovery url:") => {
  12521             AppTextKey::HomeSetupSignerErrorInvalidDiscoveryUrl
  12522         }
  12523         _ if message.starts_with("invalid remote signer uri:") => {
  12524             AppTextKey::HomeSetupSignerErrorInvalidRemoteSignerUri
  12525         }
  12526         _ if message.contains("remote signer") => AppTextKey::HomeSetupSignerErrorConnectionFailed,
  12527         _ => AppTextKey::HomeSetupErrorStartupFailed,
  12528     })
  12529 }
  12530 
  12531 fn startup_home_body(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement {
  12532     let body = runtime.startup_issue.as_deref().map_or_else(
  12533         || app_shared_text(AppTextKey::HomeTodayEmptySetupBody).to_string(),
  12534         startup_issue_summary_text,
  12535     );
  12536 
  12537     div().w_full().text_center().child(home_body_text(body))
  12538 }
  12539 
  12540 async fn connect_configured_relays(relay_urls: Vec<String>) -> Result<RadrootsNostrClient, String> {
  12541     let client = RadrootsNostrClient::new_signerless();
  12542     for relay_url in relay_urls {
  12543         client
  12544             .add_relay(relay_url.as_str())
  12545             .await
  12546             .map_err(|error| format!("failed to add relay `{relay_url}`: {error}"))?;
  12547     }
  12548     client.connect().await;
  12549     Ok(client)
  12550 }
  12551 
  12552 struct StartupAppInitResult {
  12553     relay_client: RadrootsNostrClient,
  12554 }
  12555 
  12556 async fn run_startup_app_init(relay_urls: Vec<String>) -> Result<StartupAppInitResult, String> {
  12557     let relay_client = connect_configured_relays(relay_urls).await?;
  12558     Ok(StartupAppInitResult { relay_client })
  12559 }
  12560 
  12561 async fn run_startup_signer_connect(
  12562     source_input: String,
  12563 ) -> Result<RadrootsAppRemoteSignerPendingSession, String> {
  12564     radroots_app_remote_signer_connect_pending(source_input.as_str())
  12565         .await
  12566         .map_err(|error| error.to_string())
  12567 }
  12568 
  12569 async fn run_pack_day_host_handoff(
  12570     plan: PackDayHostHandoffCommandPlan,
  12571 ) -> Result<(), PackDayHostHandoffError> {
  12572     execute_pack_day_host_handoff_plan(&plan)
  12573 }
  12574 
  12575 async fn run_pack_day_print(plan: PackDayPrintCommandPlan) -> Result<(), PackDayPrintError> {
  12576     execute_pack_day_print_plan(&plan)
  12577 }
  12578 
  12579 async fn run_pack_day_batch_print(
  12580     plan: PackDayBatchPrintCommandPlan,
  12581 ) -> Result<(), PackDayBatchPrintError> {
  12582     execute_pack_day_batch_print_plan(&plan)
  12583 }
  12584 
  12585 async fn run_startup_signer_pending_poll(
  12586     record: radroots_app_remote_signer::RadrootsAppRemoteSignerSessionRecord,
  12587     client_secret_key_hex: String,
  12588 ) -> StartupSignerPollCycleResult {
  12589     let mut auth_challenge_url = None;
  12590     let outcome = radroots_app_remote_signer_poll_pending_session_with_progress(
  12591         &record,
  12592         client_secret_key_hex.as_str(),
  12593         |progress| match progress {
  12594             radroots_app_remote_signer::RadrootsAppRemoteSignerProgressUpdate::AuthChallenge {
  12595                 url,
  12596             } => auth_challenge_url = Some(url),
  12597         },
  12598     )
  12599     .await
  12600     .map_err(|error| error.to_string());
  12601 
  12602     StartupSignerPollCycleResult {
  12603         auth_challenge_url,
  12604         outcome,
  12605     }
  12606 }
  12607 
  12608 fn home_sidebar(
  12609     runtime: &DesktopAppRuntimeSummary,
  12610     on_select_today: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12611     on_select_products: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12612     on_select_orders: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12613     on_select_pack_day: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12614     on_select_account_profile: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12615     on_select_account_farm_details: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12616     on_select_account_preferences: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12617     on_select_account_settings: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12618     cx: &App,
  12619 ) -> impl IntoElement {
  12620     let selected_section = selected_farmer_section(runtime);
  12621     let workspace_available = farmer_products_available(runtime);
  12622     let pack_day_available = farmer_pack_day_available(runtime);
  12623     let navigation_sections =
  12624         home_sidebar_navigation_sections(selected_section, workspace_available, pack_day_available);
  12625     let on_select_today = Arc::new(on_select_today);
  12626     let on_select_products = Arc::new(on_select_products);
  12627     let on_select_orders = Arc::new(on_select_orders);
  12628     let on_select_pack_day = Arc::new(on_select_pack_day);
  12629     let on_select_account_profile = Arc::new(on_select_account_profile);
  12630     let on_select_account_farm_details = Arc::new(on_select_account_farm_details);
  12631     let on_select_account_preferences = Arc::new(on_select_account_preferences);
  12632     let on_select_account_settings = Arc::new(on_select_account_settings);
  12633     let mut navigation_elements = Vec::with_capacity(navigation_sections.len());
  12634     for section in navigation_sections {
  12635         let element = match section {
  12636             FarmerSection::Today => {
  12637                 let on_click = Arc::clone(&on_select_today);
  12638                 home_sidebar_nav_button(
  12639                     "home-nav-today",
  12640                     AppTextKey::HomeNavToday,
  12641                     true,
  12642                     selected_section == FarmerSection::Today,
  12643                     move |event, window, app| on_click(event, window, app),
  12644                     cx,
  12645                 )
  12646                 .into_any_element()
  12647             }
  12648             FarmerSection::Products => {
  12649                 let on_click = Arc::clone(&on_select_products);
  12650                 home_sidebar_nav_button(
  12651                     "home-nav-products",
  12652                     AppTextKey::HomeNavProducts,
  12653                     true,
  12654                     selected_section == FarmerSection::Products,
  12655                     move |event, window, app| on_click(event, window, app),
  12656                     cx,
  12657                 )
  12658                 .into_any_element()
  12659             }
  12660             FarmerSection::Orders => {
  12661                 let on_click = Arc::clone(&on_select_orders);
  12662                 home_sidebar_nav_button(
  12663                     "home-nav-orders",
  12664                     AppTextKey::HomeNavOrders,
  12665                     true,
  12666                     selected_section == FarmerSection::Orders,
  12667                     move |event, window, app| on_click(event, window, app),
  12668                     cx,
  12669                 )
  12670                 .into_any_element()
  12671             }
  12672             FarmerSection::PackDay => {
  12673                 let on_click = Arc::clone(&on_select_pack_day);
  12674                 home_sidebar_nav_button(
  12675                     "home-nav-pack-day",
  12676                     AppTextKey::PackDayTitle,
  12677                     true,
  12678                     selected_section == FarmerSection::PackDay,
  12679                     move |event, window, app| on_click(event, window, app),
  12680                     cx,
  12681                 )
  12682                 .into_any_element()
  12683             }
  12684             FarmerSection::Farm => unreachable!(),
  12685         };
  12686         navigation_elements.push(element);
  12687     }
  12688 
  12689     app_surface_sidebar(
  12690         div()
  12691             .h_full()
  12692             .w(px(APP_UI_THEME.shells.home_sidebar_width_px))
  12693             .p(px(APP_UI_THEME.shells.home_window_padding_px))
  12694             .flex()
  12695             .flex_col()
  12696             .justify_between()
  12697             .child(
  12698                 div()
  12699                     .flex_1()
  12700                     .flex()
  12701                     .flex_col()
  12702                     .justify_start()
  12703                     .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  12704                     .children(navigation_elements),
  12705             )
  12706             .when_some(home_saved_farm(runtime), |this, farm| {
  12707                 this.child(home_sidebar_account_menu(
  12708                     farm.display_name.clone(),
  12709                     Arc::clone(&on_select_account_profile),
  12710                     Arc::clone(&on_select_account_farm_details),
  12711                     Arc::clone(&on_select_account_preferences),
  12712                     Arc::clone(&on_select_account_settings),
  12713                     cx,
  12714                 ))
  12715             }),
  12716     )
  12717 }
  12718 
  12719 fn home_sidebar_account_menu<ProfileAction, FarmDetailsAction, PreferencesAction, SettingsAction>(
  12720     label: impl Into<SharedString>,
  12721     on_select_profile: Arc<ProfileAction>,
  12722     on_select_farm_details: Arc<FarmDetailsAction>,
  12723     on_select_preferences: Arc<PreferencesAction>,
  12724     on_select_settings: Arc<SettingsAction>,
  12725     cx: &App,
  12726 ) -> impl IntoElement
  12727 where
  12728     ProfileAction: Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12729     FarmDetailsAction: Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12730     PreferencesAction: Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12731     SettingsAction: Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12732 {
  12733     app_button_sidebar_account_menu(
  12734         "home-sidebar-account-menu",
  12735         label,
  12736         move |menu, _, _| {
  12737             let on_select_profile = Arc::clone(&on_select_profile);
  12738             let on_select_farm_details = Arc::clone(&on_select_farm_details);
  12739             let on_select_preferences = Arc::clone(&on_select_preferences);
  12740             let on_select_settings = Arc::clone(&on_select_settings);
  12741 
  12742             menu.item(
  12743                 PopupMenuItem::new(app_text(AccountTab::Profile.text_key()))
  12744                     .on_click(move |event, window, app| on_select_profile(event, window, app)),
  12745             )
  12746             .item(
  12747                 PopupMenuItem::new(app_text(AccountTab::FarmDetails.text_key()))
  12748                     .on_click(move |event, window, app| on_select_farm_details(event, window, app)),
  12749             )
  12750             .item(
  12751                 PopupMenuItem::new(app_text(AccountTab::Preferences.text_key()))
  12752                     .on_click(move |event, window, app| on_select_preferences(event, window, app)),
  12753             )
  12754             .item(
  12755                 PopupMenuItem::new(app_text(AccountTab::Settings.text_key()))
  12756                     .on_click(move |event, window, app| on_select_settings(event, window, app)),
  12757             )
  12758         },
  12759         cx,
  12760     )
  12761 }
  12762 
  12763 fn buyer_sidebar(
  12764     runtime: &DesktopAppRuntimeSummary,
  12765     on_select_browse: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12766     on_select_search: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12767     on_select_cart: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12768     on_select_orders: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12769     cx: &App,
  12770 ) -> impl IntoElement {
  12771     let selected_section = selected_personal_section(runtime);
  12772 
  12773     app_surface_sidebar(
  12774         div()
  12775             .h_full()
  12776             .w(px(APP_UI_THEME.shells.home_sidebar_width_px))
  12777             .p(px(APP_UI_THEME.shells.home_window_padding_px))
  12778             .flex()
  12779             .flex_col()
  12780             .justify_between()
  12781             .child(
  12782                 div()
  12783                     .flex()
  12784                     .flex_col()
  12785                     .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  12786                     .child(
  12787                         buyer_sidebar_nav_button(
  12788                             "buyer-nav-browse",
  12789                             AppTextKey::HomeNavBrowse,
  12790                             selected_section == PersonalSection::Browse,
  12791                             on_select_browse,
  12792                             cx,
  12793                         )
  12794                         .into_any_element(),
  12795                     )
  12796                     .child(
  12797                         buyer_sidebar_nav_button(
  12798                             "buyer-nav-search",
  12799                             AppTextKey::HomeNavSearch,
  12800                             selected_section == PersonalSection::Search,
  12801                             on_select_search,
  12802                             cx,
  12803                         )
  12804                         .into_any_element(),
  12805                     )
  12806                     .child(
  12807                         buyer_sidebar_nav_button(
  12808                             "buyer-nav-cart",
  12809                             AppTextKey::HomeNavCart,
  12810                             selected_section == PersonalSection::Cart,
  12811                             on_select_cart,
  12812                             cx,
  12813                         )
  12814                         .into_any_element(),
  12815                     )
  12816                     .child(
  12817                         buyer_sidebar_nav_button(
  12818                             "buyer-nav-orders",
  12819                             AppTextKey::HomeNavOrders,
  12820                             selected_section == PersonalSection::Orders,
  12821                             on_select_orders,
  12822                             cx,
  12823                         )
  12824                         .into_any_element(),
  12825                     ),
  12826             )
  12827             .child(
  12828                 div().child(div().when_some(home_saved_farm(runtime), |this, farm| {
  12829                     this.child(home_body_text(farm.display_name.clone()))
  12830                 })),
  12831             ),
  12832     )
  12833 }
  12834 
  12835 fn buyer_sidebar_nav_button(
  12836     id: &'static str,
  12837     key: AppTextKey,
  12838     is_active: bool,
  12839     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12840     cx: &App,
  12841 ) -> AnyElement {
  12842     choice_button(id, app_shared_text(key), is_active, on_click, cx).into_any_element()
  12843 }
  12844 
  12845 fn home_sidebar_navigation_sections(
  12846     _selected_section: FarmerSection,
  12847     workspace_available: bool,
  12848     pack_day_available: bool,
  12849 ) -> Vec<FarmerSection> {
  12850     let mut sections = vec![FarmerSection::Today];
  12851     if workspace_available {
  12852         sections.push(FarmerSection::Products);
  12853         sections.push(FarmerSection::Orders);
  12854     }
  12855     if pack_day_available {
  12856         sections.push(FarmerSection::PackDay);
  12857     }
  12858 
  12859     sections
  12860 }
  12861 
  12862 fn selected_farmer_section(runtime: &DesktopAppRuntimeSummary) -> FarmerSection {
  12863     match runtime.shell_projection.selected_section {
  12864         ShellSection::Farmer(section) => section,
  12865         ShellSection::Home
  12866         | ShellSection::Account
  12867         | ShellSection::Personal(_)
  12868         | ShellSection::Settings(_) => FarmerSection::Today,
  12869     }
  12870 }
  12871 
  12872 fn selected_personal_section(runtime: &DesktopAppRuntimeSummary) -> PersonalSection {
  12873     match runtime.shell_projection.selected_section {
  12874         ShellSection::Personal(section) => section,
  12875         ShellSection::Home
  12876         | ShellSection::Account
  12877         | ShellSection::Farmer(_)
  12878         | ShellSection::Settings(_) => PersonalSection::Browse,
  12879     }
  12880 }
  12881 
  12882 fn personal_workspace_id(runtime: &DesktopAppRuntimeSummary) -> String {
  12883     runtime
  12884         .settings_account_projection
  12885         .selected_account
  12886         .as_ref()
  12887         .map(|account| account.account.account_id.clone())
  12888         .unwrap_or_else(|| "guest".to_owned())
  12889 }
  12890 
  12891 fn farmer_products_available(runtime: &DesktopAppRuntimeSummary) -> bool {
  12892     runtime.farm_setup_projection.has_saved_farm()
  12893 }
  12894 
  12895 fn farmer_pack_day_available(runtime: &DesktopAppRuntimeSummary) -> bool {
  12896     runtime
  12897         .pack_day_projection
  12898         .projection
  12899         .fulfillment_window
  12900         .is_some()
  12901 }
  12902 
  12903 fn home_content_scroll_id(section: FarmerSection) -> &'static str {
  12904     match section {
  12905         FarmerSection::Products => "home-products-scroll",
  12906         FarmerSection::Orders => "home-orders-scroll",
  12907         FarmerSection::PackDay => "home-pack-day-scroll",
  12908         FarmerSection::Today | FarmerSection::Farm => "home-today-scroll",
  12909     }
  12910 }
  12911 
  12912 fn buyer_content_scroll_id(section: PersonalSection) -> &'static str {
  12913     match section {
  12914         PersonalSection::Browse => "buyer-browse-scroll",
  12915         PersonalSection::Search => "buyer-search-scroll",
  12916         PersonalSection::Cart => "buyer-cart-scroll",
  12917         PersonalSection::Orders => "buyer-orders-scroll",
  12918     }
  12919 }
  12920 
  12921 fn home_sidebar_nav_button(
  12922     id: &'static str,
  12923     key: AppTextKey,
  12924     is_available: bool,
  12925     is_active: bool,
  12926     on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12927     cx: &App,
  12928 ) -> impl IntoElement {
  12929     if !is_available {
  12930         return div().id(id).into_any_element();
  12931     }
  12932 
  12933     choice_button(id, app_shared_text(key), is_active, on_click, cx).into_any_element()
  12934 }
  12935 
  12936 fn products_title_row(
  12937     runtime: &DesktopAppRuntimeSummary,
  12938     add_product_action: AnyElement,
  12939 ) -> impl IntoElement {
  12940     app_stack_h(APP_UI_THEME.shells.home_stack_gap_px)
  12941         .w_full()
  12942         .items_end()
  12943         .justify_between()
  12944         .child(
  12945             app_stack_v(4.0)
  12946                 .child(
  12947                     div()
  12948                         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 2.0))
  12949                         .font_weight(gpui::FontWeight::BOLD)
  12950                         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  12951                         .child(app_shared_text(AppTextKey::ProductsTitle)),
  12952                 )
  12953                 .child(
  12954                     div()
  12955                         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  12956                         .font_weight(gpui::FontWeight::MEDIUM)
  12957                         .line_height(relative(1.2))
  12958                         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  12959                         .when_some(home_saved_farm(runtime), |this, farm| {
  12960                             this.child(farm.display_name.clone())
  12961                         }),
  12962                 ),
  12963         )
  12964         .child(add_product_action)
  12965 }
  12966 
  12967 fn products_controls_card(
  12968     runtime: &DesktopAppRuntimeSummary,
  12969     products_search: Option<&ProductsSearchState>,
  12970     on_select_all_products: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12971     on_select_live_products: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12972     on_select_draft_products: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12973     on_select_products_needing_attention: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12974     on_select_paused_products: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12975     on_select_archived_products: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12976     on_sort_products_by_updated: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12977     on_sort_products_by_name: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12978     on_sort_products_by_availability: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12979     on_sort_products_by_stock: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12980     on_sort_products_by_price: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  12981     cx: &App,
  12982 ) -> impl IntoElement {
  12983     let selected_filter = runtime.products_projection.query.filter;
  12984     let selected_sort = runtime.products_projection.query.sort;
  12985 
  12986     home_card(
  12987         app_shared_text(AppTextKey::ProductsFiltersTitle),
  12988         div()
  12989             .w_full()
  12990             .flex()
  12991             .flex_col()
  12992             .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  12993             .when_some(products_search, |this, products_search| {
  12994                 this.child(
  12995                     app_text_input(&products_search.input, false)
  12996                         .cleanable(true)
  12997                         .w_full(),
  12998                 )
  12999             })
  13000             .child(
  13001                 div()
  13002                     .w_full()
  13003                     .flex()
  13004                     .items_center()
  13005                     .gap(px(8.0))
  13006                     .child(choice_button(
  13007                         "products-filter-all",
  13008                         app_shared_text(AppTextKey::ProductsFilterAll),
  13009                         selected_filter == ProductsFilter::All,
  13010                         on_select_all_products,
  13011                         cx,
  13012                     ))
  13013                     .child(choice_button(
  13014                         "products-filter-live",
  13015                         app_shared_text(AppTextKey::ProductsFilterLive),
  13016                         selected_filter == ProductsFilter::Live,
  13017                         on_select_live_products,
  13018                         cx,
  13019                     ))
  13020                     .child(choice_button(
  13021                         "products-filter-drafts",
  13022                         app_shared_text(AppTextKey::ProductsFilterDrafts),
  13023                         selected_filter == ProductsFilter::Drafts,
  13024                         on_select_draft_products,
  13025                         cx,
  13026                     ))
  13027                     .child(choice_button(
  13028                         "products-filter-need-attention",
  13029                         app_shared_text(AppTextKey::ProductsFilterNeedAttention),
  13030                         selected_filter == ProductsFilter::NeedAttention,
  13031                         on_select_products_needing_attention,
  13032                         cx,
  13033                     ))
  13034                     .child(choice_button(
  13035                         "products-filter-paused",
  13036                         app_shared_text(AppTextKey::ProductsFilterPaused),
  13037                         selected_filter == ProductsFilter::Paused,
  13038                         on_select_paused_products,
  13039                         cx,
  13040                     ))
  13041                     .child(choice_button(
  13042                         "products-filter-archived",
  13043                         app_shared_text(AppTextKey::ProductsFilterArchived),
  13044                         selected_filter == ProductsFilter::Archived,
  13045                         on_select_archived_products,
  13046                         cx,
  13047                     )),
  13048             )
  13049             .child(
  13050                 div()
  13051                     .w_full()
  13052                     .flex()
  13053                     .items_center()
  13054                     .justify_between()
  13055                     .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  13056                     .child(
  13057                         div()
  13058                             .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  13059                             .font_weight(gpui::FontWeight::SEMIBOLD)
  13060                             .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  13061                             .child(app_shared_text(AppTextKey::ProductsSortTitle)),
  13062                     )
  13063                     .child(
  13064                         div()
  13065                             .flex()
  13066                             .items_center()
  13067                             .gap(px(8.0))
  13068                             .child(choice_button(
  13069                                 "products-sort-updated",
  13070                                 app_shared_text(AppTextKey::ProductsSortUpdated),
  13071                                 selected_sort == ProductsSort::Updated,
  13072                                 on_sort_products_by_updated,
  13073                                 cx,
  13074                             ))
  13075                             .child(choice_button(
  13076                                 "products-sort-name",
  13077                                 app_shared_text(AppTextKey::ProductsSortName),
  13078                                 selected_sort == ProductsSort::Name,
  13079                                 on_sort_products_by_name,
  13080                                 cx,
  13081                             ))
  13082                             .child(choice_button(
  13083                                 "products-sort-availability",
  13084                                 app_shared_text(AppTextKey::ProductsSortAvailability),
  13085                                 selected_sort == ProductsSort::Availability,
  13086                                 on_sort_products_by_availability,
  13087                                 cx,
  13088                             ))
  13089                             .child(choice_button(
  13090                                 "products-sort-stock",
  13091                                 app_shared_text(AppTextKey::ProductsSortStock),
  13092                                 selected_sort == ProductsSort::Stock,
  13093                                 on_sort_products_by_stock,
  13094                                 cx,
  13095                             ))
  13096                             .child(choice_button(
  13097                                 "products-sort-price",
  13098                                 app_shared_text(AppTextKey::ProductsSortPrice),
  13099                                 selected_sort == ProductsSort::Price,
  13100                                 on_sort_products_by_price,
  13101                                 cx,
  13102                             )),
  13103                     ),
  13104             ),
  13105     )
  13106 }
  13107 
  13108 fn products_table_header() -> impl IntoElement {
  13109     div()
  13110         .w_full()
  13111         .flex()
  13112         .items_center()
  13113         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  13114         .child(products_table_header_column(
  13115             AppTextKey::ProductsColumnProduct,
  13116             None,
  13117             true,
  13118         ))
  13119         .child(products_table_header_column(
  13120             AppTextKey::ProductsColumnStatus,
  13121             Some(112.0),
  13122             false,
  13123         ))
  13124         .child(products_table_header_column(
  13125             AppTextKey::ProductsColumnAvailability,
  13126             Some(192.0),
  13127             false,
  13128         ))
  13129         .child(products_table_header_column(
  13130             AppTextKey::ProductsColumnStock,
  13131             Some(128.0),
  13132             false,
  13133         ))
  13134         .child(products_table_header_column(
  13135             AppTextKey::ProductsColumnPrice,
  13136             Some(128.0),
  13137             false,
  13138         ))
  13139         .child(products_table_header_column(
  13140             AppTextKey::ProductsColumnUpdated,
  13141             Some(164.0),
  13142             false,
  13143         ))
  13144         .child(products_table_header_column(
  13145             AppTextKey::ProductsColumnAction,
  13146             Some(120.0),
  13147             false,
  13148         ))
  13149 }
  13150 
  13151 fn products_table_header_column(
  13152     key: AppTextKey,
  13153     width_px: Option<f32>,
  13154     grows: bool,
  13155 ) -> impl IntoElement {
  13156     div()
  13157         .when_some(width_px, |this, width_px| this.w(px(width_px)))
  13158         .when(grows, |this| this.flex_1().min_w_0())
  13159         .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  13160         .font_weight(gpui::FontWeight::SEMIBOLD)
  13161         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  13162         .child(app_shared_text(key))
  13163 }
  13164 
  13165 fn products_table_row(
  13166     product: AnyElement,
  13167     row: &ProductsListRow,
  13168     action: AnyElement,
  13169 ) -> impl IntoElement {
  13170     div()
  13171         .w_full()
  13172         .flex()
  13173         .items_center()
  13174         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  13175         .child(product)
  13176         .child(
  13177             div()
  13178                 .w(px(112.0))
  13179                 .flex()
  13180                 .items_center()
  13181                 .gap(px(6.0))
  13182                 .child(status_indicator(products_row_status_color(row)))
  13183                 .child(
  13184                     div()
  13185                         .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  13186                         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  13187                         .child(app_shared_text(products_status_key(row.status))),
  13188                 ),
  13189         )
  13190         .child(
  13191             div()
  13192                 .w(px(192.0))
  13193                 .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  13194                 .line_height(relative(1.2))
  13195                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  13196                 .child(row.availability.label.clone()),
  13197         )
  13198         .child(
  13199             div()
  13200                 .w(px(128.0))
  13201                 .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  13202                 .line_height(relative(1.2))
  13203                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  13204                 .child(products_stock_text(row)),
  13205         )
  13206         .child(
  13207             div()
  13208                 .w(px(128.0))
  13209                 .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  13210                 .line_height(relative(1.2))
  13211                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  13212                 .child(products_price_text(row)),
  13213         )
  13214         .child(
  13215             div()
  13216                 .w(px(164.0))
  13217                 .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  13218                 .line_height(relative(1.2))
  13219                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  13220                 .child(row.updated_at.clone()),
  13221         )
  13222         .child(div().w(px(120.0)).flex().justify_end().child(action))
  13223 }
  13224 
  13225 fn orders_table_header() -> impl IntoElement {
  13226     div()
  13227         .w_full()
  13228         .flex()
  13229         .items_center()
  13230         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  13231         .child(products_table_header_column(
  13232             AppTextKey::OrdersColumnOrder,
  13233             None,
  13234             true,
  13235         ))
  13236         .child(products_table_header_column(
  13237             AppTextKey::OrdersColumnStatus,
  13238             Some(144.0),
  13239             false,
  13240         ))
  13241         .child(products_table_header_column(
  13242             AppTextKey::OrdersDetailTotalLabel,
  13243             Some(112.0),
  13244             false,
  13245         ))
  13246         .child(products_table_header_column(
  13247             AppTextKey::OrdersColumnWindow,
  13248             Some(160.0),
  13249             false,
  13250         ))
  13251         .child(products_table_header_column(
  13252             AppTextKey::OrdersColumnPickup,
  13253             Some(160.0),
  13254             false,
  13255         ))
  13256         .child(products_table_header_column(
  13257             AppTextKey::OrdersColumnAction,
  13258             Some(120.0),
  13259             false,
  13260         ))
  13261 }
  13262 
  13263 fn orders_table_row(
  13264     order: AnyElement,
  13265     row: &OrdersListRow,
  13266     action: AnyElement,
  13267 ) -> impl IntoElement {
  13268     div()
  13269         .w_full()
  13270         .flex()
  13271         .items_center()
  13272         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  13273         .child(order)
  13274         .child(
  13275             div()
  13276                 .w(px(144.0))
  13277                 .flex()
  13278                 .items_start()
  13279                 .gap(px(6.0))
  13280                 .child(status_indicator(orders_status_color(row.status)))
  13281                 .child(trade_workflow_status_stack(&row.workflow)),
  13282         )
  13283         .child(
  13284             div()
  13285                 .w(px(112.0))
  13286                 .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  13287                 .line_height(relative(1.2))
  13288                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  13289                 .child(trade_economics_total_text(&row.workflow.economics)),
  13290         )
  13291         .child(
  13292             div()
  13293                 .w(px(160.0))
  13294                 .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  13295                 .line_height(relative(1.2))
  13296                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  13297                 .child(order_optional_text(row.fulfillment_window_label.as_deref())),
  13298         )
  13299         .child(
  13300             div()
  13301                 .w(px(160.0))
  13302                 .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  13303                 .line_height(relative(1.2))
  13304                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  13305                 .child(order_optional_text(row.pickup_location_label.as_deref())),
  13306         )
  13307         .child(div().w(px(120.0)).flex().justify_end().child(action))
  13308 }
  13309 
  13310 fn orders_table_action(
  13311     index: usize,
  13312     row: &OrdersListRow,
  13313     on_review: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  13314     cx: &App,
  13315 ) -> AnyElement {
  13316     match row.primary_action {
  13317         Some(OrderPrimaryAction::Review) => action_button_compact(
  13318             ("orders-row-action-review", index),
  13319             app_shared_text(AppTextKey::OrdersActionReview),
  13320             on_review,
  13321             cx,
  13322         )
  13323         .into_any_element(),
  13324         None => div()
  13325             .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  13326             .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  13327             .child(app_shared_text(AppTextKey::ValueNone))
  13328             .into_any_element(),
  13329     }
  13330 }
  13331 
  13332 fn orders_empty_state_card(filter: OrdersFilter) -> impl IntoElement {
  13333     let (title_key, body_key) = if filter == OrdersFilter::NeedsAction {
  13334         (
  13335             AppTextKey::OrdersEmptyNeedsActionTitle,
  13336             AppTextKey::OrdersEmptyNeedsActionBody,
  13337         )
  13338     } else {
  13339         (AppTextKey::OrdersEmptyTitle, AppTextKey::OrdersEmptyBody)
  13340     };
  13341 
  13342     home_empty_state_card(title_key, body_key)
  13343 }
  13344 
  13345 fn orders_status_color(status: OrderStatus) -> u32 {
  13346     match status {
  13347         OrderStatus::NeedsAction => APP_UI_THEME.components.app_status_indicator.attention,
  13348         OrderStatus::Scheduled | OrderStatus::Packed => {
  13349             APP_UI_THEME.components.app_status_indicator.online
  13350         }
  13351         OrderStatus::Completed | OrderStatus::Declined | OrderStatus::NeedsReview => {
  13352             APP_UI_THEME.components.app_status_indicator.offline
  13353         }
  13354     }
  13355 }
  13356 
  13357 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
  13358 struct PackDayExportStatusPresentation {
  13359     indicator_color: u32,
  13360     title_key: AppTextKey,
  13361     body_key: AppTextKey,
  13362 }
  13363 
  13364 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
  13365 struct PackDayHostHandoffActionPresentation {
  13366     kind: PackDayHostHandoffKind,
  13367     label_key: AppTextKey,
  13368     enabled: bool,
  13369 }
  13370 
  13371 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
  13372 struct PackDayHostHandoffStatusPresentation {
  13373     indicator_color: u32,
  13374     title_key: AppTextKey,
  13375 }
  13376 
  13377 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
  13378 struct PackDayPrintActionPresentation {
  13379     kind: PackDayPrintKind,
  13380     label_key: AppTextKey,
  13381     enabled: bool,
  13382 }
  13383 
  13384 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
  13385 struct PackDayPrintStatusPresentation {
  13386     indicator_color: u32,
  13387     title_key: AppTextKey,
  13388 }
  13389 
  13390 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
  13391 struct PackDayBatchPrintActionPresentation {
  13392     label_key: AppTextKey,
  13393     enabled: bool,
  13394 }
  13395 
  13396 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
  13397 struct PackDayBatchPrintStatusPresentation {
  13398     indicator_color: u32,
  13399     title_key: AppTextKey,
  13400 }
  13401 
  13402 fn pack_day_export_card(
  13403     runtime: &DesktopAppRuntimeSummary,
  13404     on_export: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  13405     on_reveal_bundle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  13406     on_open_pack_sheet: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  13407     on_open_pickup_roster: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  13408     on_open_customer_labels: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  13409     on_print_all: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  13410     on_print_pack_sheet: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  13411     on_print_pickup_roster: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  13412     on_print_customer_labels: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  13413     cx: &App,
  13414 ) -> impl IntoElement {
  13415     let export = &runtime.pack_day_projection.export;
  13416     let status = pack_day_export_status_presentation(runtime);
  13417     let detail_rows = pack_day_export_detail_rows(export);
  13418     let host_handoff_actions = pack_day_host_handoff_action_presentations(runtime);
  13419     let host_handoff_status = pack_day_host_handoff_status_presentation(runtime);
  13420     let batch_print_action = pack_day_batch_print_action_presentation(runtime);
  13421     let batch_print_status = pack_day_batch_print_status_presentation(runtime);
  13422     let print_actions = pack_day_print_action_presentations(runtime);
  13423     let print_status = pack_day_print_status_presentation(runtime);
  13424     let host_handoff_error_message = runtime
  13425         .pack_day_projection
  13426         .host_handoff
  13427         .error_message
  13428         .as_deref()
  13429         .map(str::trim)
  13430         .filter(|message| !message.is_empty())
  13431         .map(str::to_owned);
  13432     let action = if pack_day_export_action_enabled(runtime) {
  13433         action_button_primary(
  13434             "pack-day-export",
  13435             app_shared_text(AppTextKey::PackDayExportAction),
  13436             on_export,
  13437             cx,
  13438         )
  13439         .into_any_element()
  13440     } else {
  13441         action_button_primary_disabled(
  13442             "pack-day-export",
  13443             app_shared_text(pack_day_export_action_label_key(export)),
  13444             cx,
  13445         )
  13446         .into_any_element()
  13447     };
  13448 
  13449     home_card(
  13450         app_shared_text(AppTextKey::PackDayExportTitle),
  13451         app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
  13452             .w_full()
  13453             .child(
  13454                 div()
  13455                     .w_full()
  13456                     .flex()
  13457                     .items_center()
  13458                     .gap(px(APP_UI_THEME.shells.settings_account_status_gap_px))
  13459                     .child(status_indicator(status.indicator_color))
  13460                     .child(
  13461                         div()
  13462                             .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  13463                             .font_weight(gpui::FontWeight::SEMIBOLD)
  13464                             .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  13465                             .child(app_shared_text(status.title_key)),
  13466                     ),
  13467             )
  13468             .child(home_body_text(app_shared_text(status.body_key)))
  13469             .when(!detail_rows.is_empty(), |this| {
  13470                 this.child(label_value_list(detail_rows))
  13471             })
  13472             .child(div().child(action))
  13473             .when(!host_handoff_actions.is_empty(), |this| {
  13474                 let on_reveal_bundle = Arc::new(on_reveal_bundle);
  13475                 let on_open_pack_sheet = Arc::new(on_open_pack_sheet);
  13476                 let on_open_pickup_roster = Arc::new(on_open_pickup_roster);
  13477                 let on_open_customer_labels = Arc::new(on_open_customer_labels);
  13478                 this.child(
  13479                     app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
  13480                         .w_full()
  13481                         .child(
  13482                             app_cluster(APP_UI_THEME.foundation.spacing.small_px)
  13483                                 .items_center()
  13484                                 .children(host_handoff_actions.into_iter().map(move |action| {
  13485                                     let button = match action.kind {
  13486                                         PackDayHostHandoffKind::RevealBundle if action.enabled => {
  13487                                             action_button(
  13488                                                 "pack-day-reveal-bundle",
  13489                                                 app_shared_text(action.label_key),
  13490                                                 {
  13491                                                     let on_reveal_bundle =
  13492                                                         Arc::clone(&on_reveal_bundle);
  13493                                                     move |event, window, cx| {
  13494                                                         (on_reveal_bundle)(event, window, cx)
  13495                                                     }
  13496                                                 },
  13497                                                 cx,
  13498                                             )
  13499                                             .into_any_element()
  13500                                         }
  13501                                         PackDayHostHandoffKind::OpenPackSheet if action.enabled => {
  13502                                             action_button(
  13503                                                 "pack-day-open-pack-sheet",
  13504                                                 app_shared_text(action.label_key),
  13505                                                 {
  13506                                                     let on_open_pack_sheet =
  13507                                                         Arc::clone(&on_open_pack_sheet);
  13508                                                     move |event, window, cx| {
  13509                                                         (on_open_pack_sheet)(event, window, cx)
  13510                                                     }
  13511                                                 },
  13512                                                 cx,
  13513                                             )
  13514                                             .into_any_element()
  13515                                         }
  13516                                         PackDayHostHandoffKind::OpenPickupRoster
  13517                                             if action.enabled =>
  13518                                         {
  13519                                             action_button(
  13520                                                 "pack-day-open-pickup-roster",
  13521                                                 app_shared_text(action.label_key),
  13522                                                 {
  13523                                                     let on_open_pickup_roster =
  13524                                                         Arc::clone(&on_open_pickup_roster);
  13525                                                     move |event, window, cx| {
  13526                                                         (on_open_pickup_roster)(event, window, cx)
  13527                                                     }
  13528                                                 },
  13529                                                 cx,
  13530                                             )
  13531                                             .into_any_element()
  13532                                         }
  13533                                         PackDayHostHandoffKind::OpenCustomerLabels
  13534                                             if action.enabled =>
  13535                                         {
  13536                                             action_button(
  13537                                                 "pack-day-open-customer-labels",
  13538                                                 app_shared_text(action.label_key),
  13539                                                 {
  13540                                                     let on_open_customer_labels =
  13541                                                         Arc::clone(&on_open_customer_labels);
  13542                                                     move |event, window, cx| {
  13543                                                         (on_open_customer_labels)(event, window, cx)
  13544                                                     }
  13545                                                 },
  13546                                                 cx,
  13547                                             )
  13548                                             .into_any_element()
  13549                                         }
  13550                                         PackDayHostHandoffKind::RevealBundle => {
  13551                                             action_button_disabled(
  13552                                                 "pack-day-reveal-bundle",
  13553                                                 app_shared_text(action.label_key),
  13554                                                 cx,
  13555                                             )
  13556                                             .into_any_element()
  13557                                         }
  13558                                         PackDayHostHandoffKind::OpenPackSheet => {
  13559                                             action_button_disabled(
  13560                                                 "pack-day-open-pack-sheet",
  13561                                                 app_shared_text(action.label_key),
  13562                                                 cx,
  13563                                             )
  13564                                             .into_any_element()
  13565                                         }
  13566                                         PackDayHostHandoffKind::OpenPickupRoster => {
  13567                                             action_button_disabled(
  13568                                                 "pack-day-open-pickup-roster",
  13569                                                 app_shared_text(action.label_key),
  13570                                                 cx,
  13571                                             )
  13572                                             .into_any_element()
  13573                                         }
  13574                                         PackDayHostHandoffKind::OpenCustomerLabels => {
  13575                                             action_button_disabled(
  13576                                                 "pack-day-open-customer-labels",
  13577                                                 app_shared_text(action.label_key),
  13578                                                 cx,
  13579                                             )
  13580                                             .into_any_element()
  13581                                         }
  13582                                     };
  13583                                     button
  13584                                 })),
  13585                         )
  13586                         .when_some(host_handoff_status, |this, status| {
  13587                             this.child(pack_day_host_handoff_status_note(
  13588                                 status,
  13589                                 host_handoff_error_message.clone(),
  13590                             ))
  13591                         }),
  13592                 )
  13593             })
  13594             .when_some(batch_print_action, |this, action| {
  13595                 let button = if action.enabled {
  13596                     action_button(
  13597                         "pack-day-print-all",
  13598                         app_shared_text(action.label_key),
  13599                         on_print_all,
  13600                         cx,
  13601                     )
  13602                     .into_any_element()
  13603                 } else {
  13604                     action_button_disabled(
  13605                         "pack-day-print-all",
  13606                         app_shared_text(action.label_key),
  13607                         cx,
  13608                     )
  13609                     .into_any_element()
  13610                 };
  13611                 this.child(
  13612                     app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
  13613                         .w_full()
  13614                         .child(button)
  13615                         .when_some(batch_print_status, |this, status| {
  13616                             this.child(pack_day_batch_print_status_note(status))
  13617                         }),
  13618                 )
  13619             })
  13620             .when(!print_actions.is_empty(), |this| {
  13621                 let on_print_pack_sheet = Arc::new(on_print_pack_sheet);
  13622                 let on_print_pickup_roster = Arc::new(on_print_pickup_roster);
  13623                 let on_print_customer_labels = Arc::new(on_print_customer_labels);
  13624                 this.child(
  13625                     app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
  13626                         .w_full()
  13627                         .child(
  13628                             app_cluster(APP_UI_THEME.foundation.spacing.small_px)
  13629                                 .items_center()
  13630                                 .children(print_actions.into_iter().map(move |action| {
  13631                                     match action.kind {
  13632                                         PackDayPrintKind::PrintPackSheet if action.enabled => {
  13633                                             action_button(
  13634                                                 "pack-day-print-pack-sheet",
  13635                                                 app_shared_text(action.label_key),
  13636                                                 {
  13637                                                     let on_print_pack_sheet =
  13638                                                         Arc::clone(&on_print_pack_sheet);
  13639                                                     move |event, window, cx| {
  13640                                                         (on_print_pack_sheet)(event, window, cx)
  13641                                                     }
  13642                                                 },
  13643                                                 cx,
  13644                                             )
  13645                                             .into_any_element()
  13646                                         }
  13647                                         PackDayPrintKind::PrintPickupRoster if action.enabled => {
  13648                                             action_button(
  13649                                                 "pack-day-print-pickup-roster",
  13650                                                 app_shared_text(action.label_key),
  13651                                                 {
  13652                                                     let on_print_pickup_roster =
  13653                                                         Arc::clone(&on_print_pickup_roster);
  13654                                                     move |event, window, cx| {
  13655                                                         (on_print_pickup_roster)(event, window, cx)
  13656                                                     }
  13657                                                 },
  13658                                                 cx,
  13659                                             )
  13660                                             .into_any_element()
  13661                                         }
  13662                                         PackDayPrintKind::PrintCustomerLabels if action.enabled => {
  13663                                             action_button(
  13664                                                 "pack-day-print-customer-labels",
  13665                                                 app_shared_text(action.label_key),
  13666                                                 {
  13667                                                     let on_print_customer_labels =
  13668                                                         Arc::clone(&on_print_customer_labels);
  13669                                                     move |event, window, cx| {
  13670                                                         (on_print_customer_labels)(
  13671                                                             event, window, cx,
  13672                                                         )
  13673                                                     }
  13674                                                 },
  13675                                                 cx,
  13676                                             )
  13677                                             .into_any_element()
  13678                                         }
  13679                                         PackDayPrintKind::PrintPackSheet => action_button_disabled(
  13680                                             "pack-day-print-pack-sheet",
  13681                                             app_shared_text(action.label_key),
  13682                                             cx,
  13683                                         )
  13684                                         .into_any_element(),
  13685                                         PackDayPrintKind::PrintPickupRoster => {
  13686                                             action_button_disabled(
  13687                                                 "pack-day-print-pickup-roster",
  13688                                                 app_shared_text(action.label_key),
  13689                                                 cx,
  13690                                             )
  13691                                             .into_any_element()
  13692                                         }
  13693                                         PackDayPrintKind::PrintCustomerLabels => {
  13694                                             action_button_disabled(
  13695                                                 "pack-day-print-customer-labels",
  13696                                                 app_shared_text(action.label_key),
  13697                                                 cx,
  13698                                             )
  13699                                             .into_any_element()
  13700                                         }
  13701                                     }
  13702                                 })),
  13703                         )
  13704                         .when_some(print_status, |this, status| {
  13705                             this.child(pack_day_print_status_note(status))
  13706                         }),
  13707                 )
  13708             }),
  13709     )
  13710 }
  13711 
  13712 fn pack_day_host_handoff_action_presentations(
  13713     runtime: &DesktopAppRuntimeSummary,
  13714 ) -> Vec<PackDayHostHandoffActionPresentation> {
  13715     let Some(bundle) = pack_day_export_succeeded_bundle(runtime) else {
  13716         return Vec::new();
  13717     };
  13718 
  13719     let host_handoff = &runtime.pack_day_projection.host_handoff;
  13720     let running_kind = (host_handoff.status == PackDayHostHandoffStatus::Running)
  13721         .then(|| host_handoff.request.as_ref().map(|request| request.kind))
  13722         .flatten();
  13723     let print_running = runtime.pack_day_projection.print.status == PackDayPrintStatus::Running;
  13724     let batch_print_running =
  13725         runtime.pack_day_projection.batch_print.status == PackDayBatchPrintStatus::Running;
  13726 
  13727     PackDayHostHandoffKind::all_v1()
  13728         .into_iter()
  13729         .map(|kind| PackDayHostHandoffActionPresentation {
  13730             kind,
  13731             label_key: pack_day_host_handoff_action_label_key(kind, running_kind),
  13732             enabled: running_kind.is_none()
  13733                 && !print_running
  13734                 && !batch_print_running
  13735                 && pack_day_host_handoff_action_is_available(bundle, kind),
  13736         })
  13737         .collect()
  13738 }
  13739 
  13740 fn pack_day_host_handoff_action_is_available(
  13741     bundle: &PackDayExportBundle,
  13742     kind: PackDayHostHandoffKind,
  13743 ) -> bool {
  13744     match kind.artifact_kind() {
  13745         None => Path::new(&bundle.bundle_directory).is_dir(),
  13746         Some(artifact_kind) => bundle
  13747             .artifacts
  13748             .iter()
  13749             .find(|artifact| artifact.kind == artifact_kind)
  13750             .and_then(|artifact| pack_day_export_artifact_path(bundle, &artifact.relative_path))
  13751             .is_some_and(|path| path.is_file()),
  13752     }
  13753 }
  13754 
  13755 fn pack_day_export_artifact_path(
  13756     bundle: &PackDayExportBundle,
  13757     relative_path: &str,
  13758 ) -> Option<PathBuf> {
  13759     let relative_path = Path::new(relative_path);
  13760     if relative_path.is_absolute()
  13761         || relative_path.components().any(|component| {
  13762             matches!(
  13763                 component,
  13764                 Component::ParentDir | Component::RootDir | Component::Prefix(_)
  13765             )
  13766         })
  13767     {
  13768         return None;
  13769     }
  13770 
  13771     Some(PathBuf::from(&bundle.bundle_directory).join(relative_path))
  13772 }
  13773 
  13774 fn pack_day_batch_print_action_presentation(
  13775     runtime: &DesktopAppRuntimeSummary,
  13776 ) -> Option<PackDayBatchPrintActionPresentation> {
  13777     let bundle = pack_day_export_succeeded_bundle(runtime)?;
  13778     let batch_print = &runtime.pack_day_projection.batch_print;
  13779     let batch_running = batch_print.status == PackDayBatchPrintStatus::Running;
  13780     let print_running = runtime.pack_day_projection.print.status == PackDayPrintStatus::Running;
  13781     let host_handoff_running =
  13782         runtime.pack_day_projection.host_handoff.status == PackDayHostHandoffStatus::Running;
  13783     let all_artifacts_available = PackDayPrintKind::all_v1()
  13784         .into_iter()
  13785         .all(|kind| pack_day_print_action_is_available(bundle, kind));
  13786 
  13787     Some(PackDayBatchPrintActionPresentation {
  13788         label_key: if batch_running {
  13789             AppTextKey::PackDayBatchPrintActionRunning
  13790         } else {
  13791             AppTextKey::PackDayBatchPrintAction
  13792         },
  13793         enabled: !batch_running
  13794             && !print_running
  13795             && !host_handoff_running
  13796             && all_artifacts_available,
  13797     })
  13798 }
  13799 
  13800 fn pack_day_batch_print_status_presentation(
  13801     runtime: &DesktopAppRuntimeSummary,
  13802 ) -> Option<PackDayBatchPrintStatusPresentation> {
  13803     let batch_print = &runtime.pack_day_projection.batch_print;
  13804 
  13805     let status = match (
  13806         batch_print.status,
  13807         batch_print.failed_artifact,
  13808         batch_print.failure,
  13809     ) {
  13810         (PackDayBatchPrintStatus::Idle, _, _) => return None,
  13811         (PackDayBatchPrintStatus::Running, _, _) => PackDayBatchPrintStatusPresentation {
  13812             indicator_color: APP_UI_THEME.foundation.text.accent,
  13813             title_key: AppTextKey::PackDayBatchPrintQueuedTitle,
  13814         },
  13815         (PackDayBatchPrintStatus::Succeeded, _, _) => PackDayBatchPrintStatusPresentation {
  13816             indicator_color: APP_UI_THEME.components.app_status_indicator.online,
  13817             title_key: AppTextKey::PackDayBatchPrintSucceededTitle,
  13818         },
  13819         (
  13820             PackDayBatchPrintStatus::Failed,
  13821             _,
  13822             Some(PackDayBatchPrintFailureKind::CustomerLabelsAvery5160Overflow),
  13823         ) => PackDayBatchPrintStatusPresentation {
  13824             indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  13825             title_key: AppTextKey::PackDayBatchPrintCustomerLabelsAvery5160OverflowFailedTitle,
  13826         },
  13827         (PackDayBatchPrintStatus::Failed, Some(failed_artifact), _) => {
  13828             PackDayBatchPrintStatusPresentation {
  13829                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  13830                 title_key: pack_day_print_failed_title_key(failed_artifact.print_kind),
  13831             }
  13832         }
  13833         (PackDayBatchPrintStatus::Failed, None, Some(PackDayBatchPrintFailureKind::Preflight)) => {
  13834             PackDayBatchPrintStatusPresentation {
  13835                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  13836                 title_key: AppTextKey::PackDayBatchPrintFailedPreflightTitle,
  13837             }
  13838         }
  13839         (
  13840             PackDayBatchPrintStatus::Failed,
  13841             None,
  13842             Some(PackDayBatchPrintFailureKind::QueueLaunch),
  13843         ) => PackDayBatchPrintStatusPresentation {
  13844             indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  13845             title_key: AppTextKey::PackDayBatchPrintFailedQueueLaunchTitle,
  13846         },
  13847         (PackDayBatchPrintStatus::Failed, None, Some(PackDayBatchPrintFailureKind::QueueExit)) => {
  13848             PackDayBatchPrintStatusPresentation {
  13849                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  13850                 title_key: AppTextKey::PackDayBatchPrintFailedQueueExitTitle,
  13851             }
  13852         }
  13853         (PackDayBatchPrintStatus::Failed, None, _) => PackDayBatchPrintStatusPresentation {
  13854             indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  13855             title_key: AppTextKey::PackDayBatchPrintFailedTitle,
  13856         },
  13857     };
  13858 
  13859     Some(status)
  13860 }
  13861 
  13862 fn pack_day_print_action_presentations(
  13863     runtime: &DesktopAppRuntimeSummary,
  13864 ) -> Vec<PackDayPrintActionPresentation> {
  13865     let Some(bundle) = pack_day_export_succeeded_bundle(runtime) else {
  13866         return Vec::new();
  13867     };
  13868 
  13869     let print = &runtime.pack_day_projection.print;
  13870     let running_kind = (print.status == PackDayPrintStatus::Running)
  13871         .then(|| print.request.as_ref().map(|request| request.kind))
  13872         .flatten();
  13873     let host_handoff_running =
  13874         runtime.pack_day_projection.host_handoff.status == PackDayHostHandoffStatus::Running;
  13875     let batch_print_running =
  13876         runtime.pack_day_projection.batch_print.status == PackDayBatchPrintStatus::Running;
  13877 
  13878     PackDayPrintKind::all_v1()
  13879         .into_iter()
  13880         .map(|kind| PackDayPrintActionPresentation {
  13881             kind,
  13882             label_key: pack_day_print_action_label_key(kind, running_kind),
  13883             enabled: running_kind.is_none()
  13884                 && !host_handoff_running
  13885                 && !batch_print_running
  13886                 && pack_day_print_action_is_available(bundle, kind),
  13887         })
  13888         .collect()
  13889 }
  13890 
  13891 fn pack_day_print_action_is_available(
  13892     bundle: &PackDayExportBundle,
  13893     kind: PackDayPrintKind,
  13894 ) -> bool {
  13895     bundle
  13896         .artifacts
  13897         .iter()
  13898         .find(|artifact| artifact.kind == kind.artifact_kind())
  13899         .and_then(|artifact| pack_day_export_artifact_path(bundle, &artifact.relative_path))
  13900         .is_some_and(|path| path.is_file())
  13901 }
  13902 
  13903 fn pack_day_print_action_label_key(
  13904     kind: PackDayPrintKind,
  13905     running_kind: Option<PackDayPrintKind>,
  13906 ) -> AppTextKey {
  13907     match (kind, running_kind == Some(kind)) {
  13908         (PackDayPrintKind::PrintPackSheet, true) => AppTextKey::PackDayPrintPackSheetActionRunning,
  13909         (PackDayPrintKind::PrintPackSheet, false) => AppTextKey::PackDayPrintPackSheetAction,
  13910         (PackDayPrintKind::PrintPickupRoster, true) => {
  13911             AppTextKey::PackDayPrintPickupRosterActionRunning
  13912         }
  13913         (PackDayPrintKind::PrintPickupRoster, false) => AppTextKey::PackDayPrintPickupRosterAction,
  13914         (PackDayPrintKind::PrintCustomerLabels, true) => {
  13915             AppTextKey::PackDayPrintCustomerLabelsActionRunning
  13916         }
  13917         (PackDayPrintKind::PrintCustomerLabels, false) => {
  13918             AppTextKey::PackDayPrintCustomerLabelsAction
  13919         }
  13920     }
  13921 }
  13922 
  13923 fn pack_day_print_failed_title_key(kind: PackDayPrintKind) -> AppTextKey {
  13924     match kind {
  13925         PackDayPrintKind::PrintPackSheet => AppTextKey::PackDayPrintPackSheetFailedTitle,
  13926         PackDayPrintKind::PrintPickupRoster => AppTextKey::PackDayPrintPickupRosterFailedTitle,
  13927         PackDayPrintKind::PrintCustomerLabels => AppTextKey::PackDayPrintCustomerLabelsFailedTitle,
  13928     }
  13929 }
  13930 
  13931 fn pack_day_print_status_presentation(
  13932     runtime: &DesktopAppRuntimeSummary,
  13933 ) -> Option<PackDayPrintStatusPresentation> {
  13934     let print = &runtime.pack_day_projection.print;
  13935     let kind = print.request.as_ref()?.kind;
  13936     let failure = print.failure;
  13937 
  13938     let status = match (print.status, kind, failure) {
  13939         (PackDayPrintStatus::Idle, _, _) => return None,
  13940         (PackDayPrintStatus::Running, PackDayPrintKind::PrintPackSheet, _) => {
  13941             PackDayPrintStatusPresentation {
  13942                 indicator_color: APP_UI_THEME.foundation.text.accent,
  13943                 title_key: AppTextKey::PackDayPrintPackSheetQueuedTitle,
  13944             }
  13945         }
  13946         (PackDayPrintStatus::Running, PackDayPrintKind::PrintPickupRoster, _) => {
  13947             PackDayPrintStatusPresentation {
  13948                 indicator_color: APP_UI_THEME.foundation.text.accent,
  13949                 title_key: AppTextKey::PackDayPrintPickupRosterQueuedTitle,
  13950             }
  13951         }
  13952         (PackDayPrintStatus::Running, PackDayPrintKind::PrintCustomerLabels, _) => {
  13953             PackDayPrintStatusPresentation {
  13954                 indicator_color: APP_UI_THEME.foundation.text.accent,
  13955                 title_key: AppTextKey::PackDayPrintCustomerLabelsQueuedTitle,
  13956             }
  13957         }
  13958         (PackDayPrintStatus::Succeeded, PackDayPrintKind::PrintPackSheet, _) => {
  13959             PackDayPrintStatusPresentation {
  13960                 indicator_color: APP_UI_THEME.components.app_status_indicator.online,
  13961                 title_key: AppTextKey::PackDayPrintPackSheetSubmittedTitle,
  13962             }
  13963         }
  13964         (PackDayPrintStatus::Succeeded, PackDayPrintKind::PrintPickupRoster, _) => {
  13965             PackDayPrintStatusPresentation {
  13966                 indicator_color: APP_UI_THEME.components.app_status_indicator.online,
  13967                 title_key: AppTextKey::PackDayPrintPickupRosterSubmittedTitle,
  13968             }
  13969         }
  13970         (PackDayPrintStatus::Succeeded, PackDayPrintKind::PrintCustomerLabels, _) => {
  13971             PackDayPrintStatusPresentation {
  13972                 indicator_color: APP_UI_THEME.components.app_status_indicator.online,
  13973                 title_key: AppTextKey::PackDayPrintCustomerLabelsSubmittedTitle,
  13974             }
  13975         }
  13976         (PackDayPrintStatus::Failed, PackDayPrintKind::PrintPackSheet, _) => {
  13977             PackDayPrintStatusPresentation {
  13978                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  13979                 title_key: AppTextKey::PackDayPrintPackSheetFailedTitle,
  13980             }
  13981         }
  13982         (PackDayPrintStatus::Failed, PackDayPrintKind::PrintPickupRoster, _) => {
  13983             PackDayPrintStatusPresentation {
  13984                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  13985                 title_key: AppTextKey::PackDayPrintPickupRosterFailedTitle,
  13986             }
  13987         }
  13988         (
  13989             PackDayPrintStatus::Failed,
  13990             PackDayPrintKind::PrintCustomerLabels,
  13991             Some(PackDayPrintFailureKind::CustomerLabelsAvery5160Overflow),
  13992         ) => PackDayPrintStatusPresentation {
  13993             indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  13994             title_key: AppTextKey::PackDayPrintCustomerLabelsAvery5160OverflowFailedTitle,
  13995         },
  13996         (PackDayPrintStatus::Failed, PackDayPrintKind::PrintCustomerLabels, _) => {
  13997             PackDayPrintStatusPresentation {
  13998                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  13999                 title_key: AppTextKey::PackDayPrintCustomerLabelsFailedTitle,
  14000             }
  14001         }
  14002     };
  14003 
  14004     Some(status)
  14005 }
  14006 
  14007 fn pack_day_host_handoff_action_label_key(
  14008     kind: PackDayHostHandoffKind,
  14009     running_kind: Option<PackDayHostHandoffKind>,
  14010 ) -> AppTextKey {
  14011     match (kind, running_kind == Some(kind)) {
  14012         (PackDayHostHandoffKind::RevealBundle, true) => {
  14013             AppTextKey::PackDayHostHandoffRevealActionRunning
  14014         }
  14015         (PackDayHostHandoffKind::RevealBundle, false) => AppTextKey::PackDayHostHandoffRevealAction,
  14016         (PackDayHostHandoffKind::OpenPackSheet, true) => {
  14017             AppTextKey::PackDayHostHandoffOpenPackSheetActionRunning
  14018         }
  14019         (PackDayHostHandoffKind::OpenPackSheet, false) => {
  14020             AppTextKey::PackDayHostHandoffOpenPackSheetAction
  14021         }
  14022         (PackDayHostHandoffKind::OpenPickupRoster, true) => {
  14023             AppTextKey::PackDayHostHandoffOpenPickupRosterActionRunning
  14024         }
  14025         (PackDayHostHandoffKind::OpenPickupRoster, false) => {
  14026             AppTextKey::PackDayHostHandoffOpenPickupRosterAction
  14027         }
  14028         (PackDayHostHandoffKind::OpenCustomerLabels, true) => {
  14029             AppTextKey::PackDayHostHandoffOpenCustomerLabelsActionRunning
  14030         }
  14031         (PackDayHostHandoffKind::OpenCustomerLabels, false) => {
  14032             AppTextKey::PackDayHostHandoffOpenCustomerLabelsAction
  14033         }
  14034     }
  14035 }
  14036 
  14037 fn pack_day_host_handoff_status_presentation(
  14038     runtime: &DesktopAppRuntimeSummary,
  14039 ) -> Option<PackDayHostHandoffStatusPresentation> {
  14040     let host_handoff = &runtime.pack_day_projection.host_handoff;
  14041     let kind = host_handoff.request.as_ref()?.kind;
  14042 
  14043     let status = match (host_handoff.status, kind) {
  14044         (PackDayHostHandoffStatus::Idle, _) => return None,
  14045         (PackDayHostHandoffStatus::Running, PackDayHostHandoffKind::RevealBundle) => {
  14046             PackDayHostHandoffStatusPresentation {
  14047                 indicator_color: APP_UI_THEME.foundation.text.accent,
  14048                 title_key: AppTextKey::PackDayHostHandoffRevealRunningTitle,
  14049             }
  14050         }
  14051         (PackDayHostHandoffStatus::Running, PackDayHostHandoffKind::OpenPackSheet) => {
  14052             PackDayHostHandoffStatusPresentation {
  14053                 indicator_color: APP_UI_THEME.foundation.text.accent,
  14054                 title_key: AppTextKey::PackDayHostHandoffOpenPackSheetRunningTitle,
  14055             }
  14056         }
  14057         (PackDayHostHandoffStatus::Running, PackDayHostHandoffKind::OpenPickupRoster) => {
  14058             PackDayHostHandoffStatusPresentation {
  14059                 indicator_color: APP_UI_THEME.foundation.text.accent,
  14060                 title_key: AppTextKey::PackDayHostHandoffOpenPickupRosterRunningTitle,
  14061             }
  14062         }
  14063         (PackDayHostHandoffStatus::Running, PackDayHostHandoffKind::OpenCustomerLabels) => {
  14064             PackDayHostHandoffStatusPresentation {
  14065                 indicator_color: APP_UI_THEME.foundation.text.accent,
  14066                 title_key: AppTextKey::PackDayHostHandoffOpenCustomerLabelsRunningTitle,
  14067             }
  14068         }
  14069         (PackDayHostHandoffStatus::Succeeded, PackDayHostHandoffKind::RevealBundle) => {
  14070             PackDayHostHandoffStatusPresentation {
  14071                 indicator_color: APP_UI_THEME.components.app_status_indicator.online,
  14072                 title_key: AppTextKey::PackDayHostHandoffRevealSucceededTitle,
  14073             }
  14074         }
  14075         (PackDayHostHandoffStatus::Succeeded, PackDayHostHandoffKind::OpenPackSheet) => {
  14076             PackDayHostHandoffStatusPresentation {
  14077                 indicator_color: APP_UI_THEME.components.app_status_indicator.online,
  14078                 title_key: AppTextKey::PackDayHostHandoffOpenPackSheetSucceededTitle,
  14079             }
  14080         }
  14081         (PackDayHostHandoffStatus::Succeeded, PackDayHostHandoffKind::OpenPickupRoster) => {
  14082             PackDayHostHandoffStatusPresentation {
  14083                 indicator_color: APP_UI_THEME.components.app_status_indicator.online,
  14084                 title_key: AppTextKey::PackDayHostHandoffOpenPickupRosterSucceededTitle,
  14085             }
  14086         }
  14087         (PackDayHostHandoffStatus::Succeeded, PackDayHostHandoffKind::OpenCustomerLabels) => {
  14088             PackDayHostHandoffStatusPresentation {
  14089                 indicator_color: APP_UI_THEME.components.app_status_indicator.online,
  14090                 title_key: AppTextKey::PackDayHostHandoffOpenCustomerLabelsSucceededTitle,
  14091             }
  14092         }
  14093         (PackDayHostHandoffStatus::Failed, PackDayHostHandoffKind::RevealBundle) => {
  14094             PackDayHostHandoffStatusPresentation {
  14095                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  14096                 title_key: AppTextKey::PackDayHostHandoffRevealFailedTitle,
  14097             }
  14098         }
  14099         (PackDayHostHandoffStatus::Failed, PackDayHostHandoffKind::OpenPackSheet) => {
  14100             PackDayHostHandoffStatusPresentation {
  14101                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  14102                 title_key: AppTextKey::PackDayHostHandoffOpenPackSheetFailedTitle,
  14103             }
  14104         }
  14105         (PackDayHostHandoffStatus::Failed, PackDayHostHandoffKind::OpenPickupRoster) => {
  14106             PackDayHostHandoffStatusPresentation {
  14107                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  14108                 title_key: AppTextKey::PackDayHostHandoffOpenPickupRosterFailedTitle,
  14109             }
  14110         }
  14111         (PackDayHostHandoffStatus::Failed, PackDayHostHandoffKind::OpenCustomerLabels) => {
  14112             PackDayHostHandoffStatusPresentation {
  14113                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  14114                 title_key: AppTextKey::PackDayHostHandoffOpenCustomerLabelsFailedTitle,
  14115             }
  14116         }
  14117     };
  14118 
  14119     Some(status)
  14120 }
  14121 
  14122 fn pack_day_host_handoff_status_note(
  14123     status: PackDayHostHandoffStatusPresentation,
  14124     error_message: Option<String>,
  14125 ) -> impl IntoElement {
  14126     app_stack_v(4.0)
  14127         .w_full()
  14128         .child(
  14129             div()
  14130                 .w_full()
  14131                 .flex()
  14132                 .items_center()
  14133                 .gap(px(APP_UI_THEME.shells.settings_account_status_gap_px))
  14134                 .child(status_indicator(status.indicator_color))
  14135                 .child(
  14136                     div()
  14137                         .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  14138                         .font_weight(gpui::FontWeight::SEMIBOLD)
  14139                         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  14140                         .child(app_shared_text(status.title_key)),
  14141                 ),
  14142         )
  14143         .when_some(error_message, |this, error_message| {
  14144             this.child(home_body_text(error_message))
  14145         })
  14146 }
  14147 
  14148 fn pack_day_print_status_note(status: PackDayPrintStatusPresentation) -> impl IntoElement {
  14149     app_stack_v(4.0).w_full().child(
  14150         div()
  14151             .w_full()
  14152             .flex()
  14153             .items_center()
  14154             .gap(px(APP_UI_THEME.shells.settings_account_status_gap_px))
  14155             .child(status_indicator(status.indicator_color))
  14156             .child(
  14157                 div()
  14158                     .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  14159                     .font_weight(gpui::FontWeight::SEMIBOLD)
  14160                     .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  14161                     .child(app_shared_text(status.title_key)),
  14162             ),
  14163     )
  14164 }
  14165 
  14166 fn pack_day_batch_print_status_note(
  14167     status: PackDayBatchPrintStatusPresentation,
  14168 ) -> impl IntoElement {
  14169     app_stack_v(4.0).w_full().child(
  14170         div()
  14171             .w_full()
  14172             .flex()
  14173             .items_center()
  14174             .gap(px(APP_UI_THEME.shells.settings_account_status_gap_px))
  14175             .child(status_indicator(status.indicator_color))
  14176             .child(
  14177                 div()
  14178                     .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  14179                     .font_weight(gpui::FontWeight::SEMIBOLD)
  14180                     .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  14181                     .child(app_shared_text(status.title_key)),
  14182             ),
  14183     )
  14184 }
  14185 
  14186 fn pack_day_export_succeeded_bundle(
  14187     runtime: &DesktopAppRuntimeSummary,
  14188 ) -> Option<&PackDayExportBundle> {
  14189     (runtime.pack_day_projection.export.status == PackDayExportStatus::Succeeded)
  14190         .then_some(runtime.pack_day_projection.export.bundle.as_ref())
  14191         .flatten()
  14192 }
  14193 
  14194 fn pack_day_export_has_exportable_context(runtime: &DesktopAppRuntimeSummary) -> bool {
  14195     let projection = &runtime.pack_day_projection.projection;
  14196     projection.fulfillment_window.is_some() && !projection.is_empty()
  14197 }
  14198 
  14199 fn pack_day_export_action_enabled(runtime: &DesktopAppRuntimeSummary) -> bool {
  14200     pack_day_export_has_exportable_context(runtime)
  14201         && runtime.pack_day_projection.export.status != PackDayExportStatus::Running
  14202 }
  14203 
  14204 fn pack_day_export_action_label_key(export: &PackDayExportProjection) -> AppTextKey {
  14205     match export.status {
  14206         PackDayExportStatus::Running => AppTextKey::PackDayExportActionRunning,
  14207         PackDayExportStatus::Idle
  14208         | PackDayExportStatus::Succeeded
  14209         | PackDayExportStatus::Failed => AppTextKey::PackDayExportAction,
  14210     }
  14211 }
  14212 
  14213 fn pack_day_export_status_presentation(
  14214     runtime: &DesktopAppRuntimeSummary,
  14215 ) -> PackDayExportStatusPresentation {
  14216     match runtime.pack_day_projection.export.status {
  14217         PackDayExportStatus::Running => PackDayExportStatusPresentation {
  14218             indicator_color: APP_UI_THEME.foundation.text.accent,
  14219             title_key: AppTextKey::PackDayExportRunningTitle,
  14220             body_key: AppTextKey::PackDayExportRunningBody,
  14221         },
  14222         PackDayExportStatus::Succeeded => PackDayExportStatusPresentation {
  14223             indicator_color: APP_UI_THEME.components.app_status_indicator.online,
  14224             title_key: AppTextKey::PackDayExportSucceededTitle,
  14225             body_key: AppTextKey::PackDayExportSucceededBody,
  14226         },
  14227         PackDayExportStatus::Failed => PackDayExportStatusPresentation {
  14228             indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  14229             title_key: AppTextKey::PackDayExportFailedTitle,
  14230             body_key: AppTextKey::PackDayExportFailedBody,
  14231         },
  14232         PackDayExportStatus::Idle if pack_day_export_has_exportable_context(runtime) => {
  14233             PackDayExportStatusPresentation {
  14234                 indicator_color: APP_UI_THEME.components.app_status_indicator.online,
  14235                 title_key: AppTextKey::PackDayExportReadyTitle,
  14236                 body_key: AppTextKey::PackDayExportReadyBody,
  14237             }
  14238         }
  14239         PackDayExportStatus::Idle => PackDayExportStatusPresentation {
  14240             indicator_color: APP_UI_THEME.components.app_status_indicator.offline,
  14241             title_key: AppTextKey::PackDayExportUnavailableTitle,
  14242             body_key: AppTextKey::PackDayExportUnavailableBody,
  14243         },
  14244     }
  14245 }
  14246 
  14247 fn pack_day_export_detail_rows(export: &PackDayExportProjection) -> Vec<LabelValueRow> {
  14248     match export.status {
  14249         PackDayExportStatus::Succeeded => export
  14250             .bundle
  14251             .as_ref()
  14252             .map(pack_day_export_bundle_rows)
  14253             .unwrap_or_default(),
  14254         PackDayExportStatus::Failed => export
  14255             .error_message
  14256             .as_deref()
  14257             .map(str::trim)
  14258             .filter(|message| !message.is_empty())
  14259             .map(|message| {
  14260                 vec![LabelValueRow::new(
  14261                     app_shared_text(AppTextKey::PackDayExportErrorLabel),
  14262                     message.to_owned(),
  14263                 )]
  14264             })
  14265             .unwrap_or_default(),
  14266         PackDayExportStatus::Idle | PackDayExportStatus::Running => Vec::new(),
  14267     }
  14268 }
  14269 
  14270 fn pack_day_export_bundle_rows(bundle: &PackDayExportBundle) -> Vec<LabelValueRow> {
  14271     vec![
  14272         LabelValueRow::new(
  14273             app_shared_text(AppTextKey::PackDayExportFolderLabel),
  14274             bundle.bundle_directory.clone(),
  14275         ),
  14276         LabelValueRow::new(
  14277             app_shared_text(AppTextKey::PackDayExportFilesLabel),
  14278             pack_day_export_artifact_names(bundle),
  14279         ),
  14280     ]
  14281 }
  14282 
  14283 fn pack_day_export_artifact_names(bundle: &PackDayExportBundle) -> String {
  14284     bundle
  14285         .artifacts
  14286         .iter()
  14287         .map(|artifact| artifact.kind.file_name())
  14288         .collect::<Vec<_>>()
  14289         .join(", ")
  14290 }
  14291 
  14292 fn pack_day_title_row(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement {
  14293     app_stack_v(4.0)
  14294         .child(
  14295             div()
  14296                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 2.0))
  14297                 .font_weight(gpui::FontWeight::BOLD)
  14298                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  14299                 .child(app_shared_text(AppTextKey::PackDayTitle)),
  14300         )
  14301         .child(
  14302             div()
  14303                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  14304                 .font_weight(gpui::FontWeight::MEDIUM)
  14305                 .line_height(relative(1.2))
  14306                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  14307                 .when_some(home_saved_farm(runtime), |this, farm| {
  14308                     this.child(farm.display_name.clone())
  14309                 }),
  14310         )
  14311 }
  14312 
  14313 fn pack_day_window_summary_card(fulfillment_window: &FulfillmentWindowSummary) -> impl IntoElement {
  14314     home_card(
  14315         app_shared_text(AppTextKey::PackDayWindowSummaryTitle),
  14316         label_value_list([
  14317             LabelValueRow::new(
  14318                 app_shared_text(AppTextKey::HomeTodayWindowStartsLabel),
  14319                 fulfillment_window.starts_at.clone(),
  14320             ),
  14321             LabelValueRow::new(
  14322                 app_shared_text(AppTextKey::HomeTodayWindowEndsLabel),
  14323                 fulfillment_window.ends_at.clone(),
  14324             ),
  14325         ]),
  14326     )
  14327 }
  14328 
  14329 fn pack_day_totals_card(rows: &[PackDayProductTotalRow]) -> impl IntoElement {
  14330     home_card(
  14331         app_shared_text(AppTextKey::PackDayTotalsTitle),
  14332         div()
  14333             .w_full()
  14334             .flex()
  14335             .flex_col()
  14336             .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
  14337             .children(
  14338                 rows.iter()
  14339                     .map(pack_day_product_total_row)
  14340                     .collect::<Vec<_>>(),
  14341             ),
  14342     )
  14343 }
  14344 
  14345 fn pack_day_pack_list_card(rows: &[PackDayPackListRow]) -> impl IntoElement {
  14346     home_card(
  14347         app_shared_text(AppTextKey::PackDayPackListTitle),
  14348         div()
  14349             .w_full()
  14350             .flex()
  14351             .flex_col()
  14352             .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
  14353             .children(rows.iter().map(pack_day_pack_list_row).collect::<Vec<_>>()),
  14354     )
  14355 }
  14356 
  14357 fn pack_day_pickup_roster_card(rows: &[PackDayRosterRow]) -> impl IntoElement {
  14358     home_card(
  14359         app_shared_text(AppTextKey::PackDayPickupRosterTitle),
  14360         div()
  14361             .w_full()
  14362             .flex()
  14363             .flex_col()
  14364             .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
  14365             .children(rows.iter().map(pack_day_roster_row).collect::<Vec<_>>()),
  14366     )
  14367 }
  14368 
  14369 fn pack_day_product_total_row(row: &PackDayProductTotalRow) -> AnyElement {
  14370     pack_day_label_value_row(row.title.as_str(), row.quantity_display.as_str())
  14371 }
  14372 
  14373 fn pack_day_pack_list_row(row: &PackDayPackListRow) -> AnyElement {
  14374     pack_day_label_value_row(row.title.as_str(), row.quantity_display.as_str())
  14375 }
  14376 
  14377 fn pack_day_roster_row(row: &PackDayRosterRow) -> AnyElement {
  14378     div()
  14379         .w_full()
  14380         .min_w_0()
  14381         .flex()
  14382         .items_center()
  14383         .justify_between()
  14384         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  14385         .child(
  14386             div()
  14387                 .min_w_0()
  14388                 .flex()
  14389                 .flex_col()
  14390                 .gap(px(2.0))
  14391                 .child(
  14392                     div()
  14393                         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  14394                         .font_weight(gpui::FontWeight::SEMIBOLD)
  14395                         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  14396                         .child(row.order_number.clone()),
  14397                 )
  14398                 .child(
  14399                     div()
  14400                         .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  14401                         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  14402                         .child(row.customer_display_name.clone()),
  14403                 ),
  14404         )
  14405         .into_any_element()
  14406 }
  14407 
  14408 fn pack_day_label_value_row(label: &str, value: &str) -> AnyElement {
  14409     div()
  14410         .w_full()
  14411         .min_w_0()
  14412         .flex()
  14413         .items_center()
  14414         .justify_between()
  14415         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  14416         .child(
  14417             div()
  14418                 .flex_1()
  14419                 .min_w_0()
  14420                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  14421                 .font_weight(gpui::FontWeight::MEDIUM)
  14422                 .line_height(relative(1.2))
  14423                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  14424                 .child(label.to_owned()),
  14425         )
  14426         .child(
  14427             div()
  14428                 .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  14429                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  14430                 .child(value.to_owned()),
  14431         )
  14432         .into_any_element()
  14433 }
  14434 
  14435 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
  14436 enum ReminderActionTarget {
  14437     OrderDetail(OrderId),
  14438     PackDay(FulfillmentWindowId),
  14439 }
  14440 
  14441 fn reminder_action_target(reminder: &ReminderDeadlineProjection) -> Option<ReminderActionTarget> {
  14442     reminder
  14443         .order_id
  14444         .map(ReminderActionTarget::OrderDetail)
  14445         .or_else(|| {
  14446             reminder
  14447                 .fulfillment_window_id
  14448                 .map(ReminderActionTarget::PackDay)
  14449         })
  14450 }
  14451 
  14452 fn reminder_urgency_key(urgency: ReminderUrgency) -> AppTextKey {
  14453     match urgency {
  14454         ReminderUrgency::Upcoming => AppTextKey::ReminderUrgencyUpcoming,
  14455         ReminderUrgency::DueSoon => AppTextKey::ReminderUrgencyDueSoon,
  14456         ReminderUrgency::Overdue => AppTextKey::ReminderUrgencyOverdue,
  14457         ReminderUrgency::Blocking => AppTextKey::ReminderUrgencyBlocking,
  14458     }
  14459 }
  14460 
  14461 fn reminder_urgency_color(urgency: ReminderUrgency) -> u32 {
  14462     match urgency {
  14463         ReminderUrgency::Upcoming => APP_UI_THEME.components.app_status_indicator.offline,
  14464         ReminderUrgency::DueSoon => APP_UI_THEME.foundation.text.accent,
  14465         ReminderUrgency::Overdue | ReminderUrgency::Blocking => {
  14466             APP_UI_THEME.components.app_status_indicator.attention
  14467         }
  14468     }
  14469 }
  14470 
  14471 fn reminder_urgency_badge(urgency: ReminderUrgency) -> AnyElement {
  14472     div()
  14473         .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  14474         .font_weight(gpui::FontWeight::SEMIBOLD)
  14475         .text_color(rgb(reminder_urgency_color(urgency)))
  14476         .child(app_shared_text(reminder_urgency_key(urgency)))
  14477         .into_any_element()
  14478 }
  14479 
  14480 fn reminder_delivery_state_key(delivery_state: ReminderDeliveryState) -> AppTextKey {
  14481     match delivery_state {
  14482         ReminderDeliveryState::Scheduled => AppTextKey::ReminderDeliveryStateScheduled,
  14483         ReminderDeliveryState::Presented => AppTextKey::ReminderDeliveryStatePresented,
  14484         ReminderDeliveryState::Acknowledged => AppTextKey::ReminderDeliveryStateAcknowledged,
  14485         ReminderDeliveryState::Resolved => AppTextKey::ReminderDeliveryStateResolved,
  14486     }
  14487 }
  14488 
  14489 fn reminder_delivery_state_color(delivery_state: ReminderDeliveryState) -> u32 {
  14490     match delivery_state {
  14491         ReminderDeliveryState::Scheduled => APP_UI_THEME.components.app_status_indicator.offline,
  14492         ReminderDeliveryState::Presented => APP_UI_THEME.foundation.text.accent,
  14493         ReminderDeliveryState::Acknowledged => APP_UI_THEME.foundation.text.secondary,
  14494         ReminderDeliveryState::Resolved => APP_UI_THEME.components.app_status_indicator.online,
  14495     }
  14496 }
  14497 
  14498 fn reminder_delivery_state_badge(delivery_state: ReminderDeliveryState) -> AnyElement {
  14499     div()
  14500         .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  14501         .font_weight(gpui::FontWeight::SEMIBOLD)
  14502         .text_color(rgb(reminder_delivery_state_color(delivery_state)))
  14503         .child(app_shared_text(reminder_delivery_state_key(delivery_state)))
  14504         .into_any_element()
  14505 }
  14506 
  14507 fn presented_farmer_reminder(
  14508     runtime: &DesktopAppRuntimeSummary,
  14509 ) -> Option<&ReminderDeadlineProjection> {
  14510     runtime
  14511         .today_projection
  14512         .reminders
  14513         .items
  14514         .iter()
  14515         .chain(runtime.orders_projection.reminders.items.iter())
  14516         .chain(
  14517             runtime
  14518                 .pack_day_projection
  14519                 .projection
  14520                 .reminders
  14521                 .items
  14522                 .iter(),
  14523         )
  14524         .filter(|reminder| reminder.delivery_state == ReminderDeliveryState::Presented)
  14525         .min_by(|left, right| {
  14526             reminder_urgency_priority(left.urgency)
  14527                 .cmp(&reminder_urgency_priority(right.urgency))
  14528                 .then_with(|| left.deadline_at.cmp(&right.deadline_at))
  14529                 .then_with(|| left.reminder_id.cmp(&right.reminder_id))
  14530         })
  14531 }
  14532 
  14533 fn reminder_urgency_priority(urgency: ReminderUrgency) -> u8 {
  14534     match urgency {
  14535         ReminderUrgency::Blocking => 0,
  14536         ReminderUrgency::Overdue => 1,
  14537         ReminderUrgency::DueSoon => 2,
  14538         ReminderUrgency::Upcoming => 3,
  14539     }
  14540 }
  14541 
  14542 fn reminder_deadline_text(reminder: &ReminderDeadlineProjection) -> String {
  14543     format!(
  14544         "{}: {}",
  14545         app_text(AppTextKey::ReminderDeadlineLabel),
  14546         reminder.deadline_at
  14547     )
  14548 }
  14549 
  14550 fn products_empty_state_card(filter: ProductsFilter) -> impl IntoElement {
  14551     let (title_key, body_key) = if filter == ProductsFilter::NeedAttention {
  14552         (
  14553             AppTextKey::ProductsEmptyNeedAttentionTitle,
  14554             AppTextKey::ProductsEmptyNeedAttentionBody,
  14555         )
  14556     } else {
  14557         (
  14558             AppTextKey::ProductsEmptyTitle,
  14559             AppTextKey::ProductsEmptyBody,
  14560         )
  14561     };
  14562 
  14563     home_empty_state_card(title_key, body_key)
  14564 }
  14565 
  14566 fn products_status_key(status: ProductStatus) -> AppTextKey {
  14567     match status {
  14568         ProductStatus::Draft => AppTextKey::ProductsStatusDraft,
  14569         ProductStatus::Published => AppTextKey::ProductsStatusLive,
  14570         ProductStatus::Paused => AppTextKey::ProductsStatusPaused,
  14571         ProductStatus::Archived => AppTextKey::ProductsStatusArchived,
  14572     }
  14573 }
  14574 
  14575 fn products_row_status_color(row: &ProductsListRow) -> u32 {
  14576     if row.attention_state != ProductAttentionState::Healthy {
  14577         APP_UI_THEME.components.app_status_indicator.attention
  14578     } else {
  14579         match row.status {
  14580             ProductStatus::Published => APP_UI_THEME.components.app_status_indicator.online,
  14581             ProductStatus::Draft | ProductStatus::Paused | ProductStatus::Archived => {
  14582                 APP_UI_THEME.components.app_status_indicator.offline
  14583             }
  14584         }
  14585     }
  14586 }
  14587 
  14588 fn products_stock_text(row: &ProductsListRow) -> String {
  14589     match row.stock.quantity {
  14590         Some(quantity) => match row.stock.unit_label.as_ref() {
  14591             Some(unit_label) => format!("{quantity} {unit_label}"),
  14592             None => quantity.to_string(),
  14593         },
  14594         None => app_shared_text(AppTextKey::ValueNone).to_string(),
  14595     }
  14596 }
  14597 
  14598 fn products_price_text(row: &ProductsListRow) -> String {
  14599     let Some(price) = row.price.as_ref() else {
  14600         return app_shared_text(AppTextKey::ValueNone).to_string();
  14601     };
  14602     let dollars = price.amount_minor_units / 100;
  14603     let cents = price.amount_minor_units % 100;
  14604 
  14605     format!("${dollars}.{cents:02} / {}", price.unit_label)
  14606 }
  14607 
  14608 fn products_stock_editor_card(
  14609     row: &ProductsListRow,
  14610     editor: &ProductsStockEditorState,
  14611     on_save: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  14612     on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  14613     cx: &App,
  14614 ) -> impl IntoElement {
  14615     let validation_key = products_stock_editor_validation_key(editor, cx);
  14616     let save_ready = (editor.has_changes(cx)
  14617         || matches!(
  14618             editor.save_issue,
  14619             Some(ProductsStockEditorSaveIssue::PublishQueueFailed)
  14620         ))
  14621         && editor.parsed_stock_quantity(cx).is_some();
  14622 
  14623     div()
  14624         .w_full()
  14625         .bg(rgb(APP_UI_THEME.foundation.surfaces.window_background))
  14626         .rounded(px(APP_UI_THEME.foundation.radii.medium_px))
  14627         .p(px(16.0))
  14628         .flex()
  14629         .flex_col()
  14630         .gap(px(8.0))
  14631         .child(
  14632             div()
  14633                 .w_full()
  14634                 .flex()
  14635                 .flex_col()
  14636                 .gap(px(2.0))
  14637                 .child(
  14638                     div()
  14639                         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  14640                         .font_weight(gpui::FontWeight::SEMIBOLD)
  14641                         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  14642                         .child(app_shared_text(AppTextKey::ProductsStockEditorTitle)),
  14643                 )
  14644                 .child(
  14645                     div()
  14646                         .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  14647                         .line_height(relative(1.2))
  14648                         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  14649                         .child(product_display_title(row.title.as_str())),
  14650                 ),
  14651         )
  14652         .child(
  14653             div()
  14654                 .w_full()
  14655                 .flex()
  14656                 .items_end()
  14657                 .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  14658                 .child(
  14659                     div()
  14660                         .flex_1()
  14661                         .min_w_0()
  14662                         .flex()
  14663                         .flex_col()
  14664                         .gap(px(6.0))
  14665                         .child(
  14666                             div()
  14667                                 .text_size(px(APP_UI_THEME
  14668                                     .foundation
  14669                                     .typography
  14670                                     .utility_title_text_px))
  14671                                 .font_weight(gpui::FontWeight::SEMIBOLD)
  14672                                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  14673                                 .child(app_shared_text(AppTextKey::ProductsStockEditorFieldLabel)),
  14674                         )
  14675                         .child(app_text_input(&editor.input, false).w_full())
  14676                         .when_some(validation_key, |this, key| {
  14677                             this.child(
  14678                                 div()
  14679                                     .text_size(px(APP_UI_THEME
  14680                                         .foundation
  14681                                         .typography
  14682                                         .utility_title_text_px))
  14683                                     .line_height(relative(1.2))
  14684                                     .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  14685                                     .child(app_shared_text(key)),
  14686                             )
  14687                         })
  14688                         .when_some(editor.save_issue, |this, issue| {
  14689                             this.child(
  14690                                 div()
  14691                                     .text_size(px(APP_UI_THEME
  14692                                         .foundation
  14693                                         .typography
  14694                                         .utility_title_text_px))
  14695                                     .line_height(relative(1.2))
  14696                                     .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  14697                                     .child(app_shared_text(issue.text_key())),
  14698                             )
  14699                         }),
  14700                 )
  14701                 .child(
  14702                     div()
  14703                         .flex()
  14704                         .items_center()
  14705                         .gap(px(8.0))
  14706                         .child(action_button_compact(
  14707                             "products-stock-editor-close",
  14708                             app_shared_text(AppTextKey::ProductsStockEditorCancelAction),
  14709                             on_cancel,
  14710                             cx,
  14711                         ))
  14712                         .child(if save_ready {
  14713                             action_button_primary(
  14714                                 "products-stock-editor-save",
  14715                                 app_shared_text(AppTextKey::ProductsStockEditorSaveAction),
  14716                                 on_save,
  14717                                 cx,
  14718                             )
  14719                             .into_any_element()
  14720                         } else {
  14721                             action_button_primary_disabled(
  14722                                 "products-stock-editor-save",
  14723                                 app_shared_text(AppTextKey::ProductsStockEditorSaveAction),
  14724                                 cx,
  14725                             )
  14726                             .into_any_element()
  14727                         }),
  14728                 ),
  14729         )
  14730 }
  14731 
  14732 fn products_stock_editor_validation_key(
  14733     editor: &ProductsStockEditorState,
  14734     cx: &App,
  14735 ) -> Option<AppTextKey> {
  14736     if editor.parsed_stock_quantity(cx).is_some() {
  14737         return None;
  14738     }
  14739 
  14740     Some(AppTextKey::ProductsStockEditorInvalidQuantity)
  14741 }
  14742 
  14743 fn products_editor_surface(
  14744     form: &ProductEditorFormState,
  14745     runtime: &DesktopAppRuntimeSummary,
  14746     cx: &mut Context<HomeView>,
  14747 ) -> AnyElement {
  14748     let validation_keys = products_editor_validation_keys(form, cx);
  14749     let save_ready = (form.has_changes(cx)
  14750         || matches!(
  14751             form.save_issue,
  14752             Some(ProductEditorSaveIssue::PublishQueueFailed)
  14753         ))
  14754         && validation_keys.is_empty();
  14755 
  14756     let save_action = if save_ready {
  14757         action_button_primary(
  14758             "products-editor-save",
  14759             app_shared_text(AppTextKey::ProductsEditorSaveAction),
  14760             cx.listener(|this, _, _, cx| this.save_product_editor(cx)),
  14761             cx,
  14762         )
  14763         .into_any_element()
  14764     } else {
  14765         action_button_primary_disabled(
  14766             "products-editor-save",
  14767             app_shared_text(AppTextKey::ProductsEditorSaveAction),
  14768             cx,
  14769         )
  14770         .into_any_element()
  14771     };
  14772 
  14773     app_focused_task_view(
  14774         app_shared_text(AppTextKey::ProductsEditorTitle),
  14775         app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
  14776             .w_full()
  14777             .child(home_body_text(app_shared_text(
  14778                 AppTextKey::ProductsEditorBody,
  14779             )))
  14780             .child(app_form_input_text(
  14781                 AppFormFieldSpec::new(
  14782                     app_shared_text(AppTextKey::ProductsEditorFieldTitle),
  14783                     Option::<SharedString>::None,
  14784                 ),
  14785                 &form.title_input,
  14786                 false,
  14787             ))
  14788             .child(app_form_input_text(
  14789                 AppFormFieldSpec::new(
  14790                     app_shared_text(AppTextKey::ProductsEditorFieldSubtitle),
  14791                     Option::<SharedString>::None,
  14792                 ),
  14793                 &form.subtitle_input,
  14794                 false,
  14795             ))
  14796             .child(app_form_input_text(
  14797                 AppFormFieldSpec::new(
  14798                     app_shared_text(AppTextKey::ProductsEditorFieldCategory),
  14799                     Option::<SharedString>::None,
  14800                 ),
  14801                 &form.category_input,
  14802                 false,
  14803             ))
  14804             .child(app_form_input_text(
  14805                 AppFormFieldSpec::new(
  14806                     app_shared_text(AppTextKey::ProductsEditorFieldUnit),
  14807                     Option::<SharedString>::None,
  14808                 ),
  14809                 &form.unit_input,
  14810                 false,
  14811             ))
  14812             .child(app_form_input_text(
  14813                 AppFormFieldSpec::new(
  14814                     app_shared_text(AppTextKey::ProductsEditorFieldPrice),
  14815                     products_editor_invalid_price_key(form, cx).map(app_shared_text),
  14816                 ),
  14817                 &form.price_input,
  14818                 false,
  14819             ))
  14820             .child(app_form_input_text(
  14821                 AppFormFieldSpec::new(
  14822                     app_shared_text(AppTextKey::ProductsEditorFieldStock),
  14823                     products_editor_invalid_stock_key(form, cx).map(app_shared_text),
  14824                 ),
  14825                 &form.stock_input,
  14826                 false,
  14827             ))
  14828             .child(products_editor_availability_section(
  14829                 form,
  14830                 &runtime.farm_rules_projection.fulfillment_windows,
  14831                 cx,
  14832             ))
  14833             .child(products_editor_status_section(
  14834                 form.status,
  14835                 cx.listener(|this, _, _, cx| {
  14836                     this.select_product_editor_status(ProductStatus::Draft, cx)
  14837                 }),
  14838                 cx.listener(|this, _, _, cx| {
  14839                     this.select_product_editor_status(ProductStatus::Published, cx)
  14840                 }),
  14841                 cx.listener(|this, _, _, cx| {
  14842                     this.select_product_editor_status(ProductStatus::Paused, cx)
  14843                 }),
  14844                 cx.listener(|this, _, _, cx| {
  14845                     this.select_product_editor_status(ProductStatus::Archived, cx)
  14846                 }),
  14847                 cx,
  14848             ))
  14849             .child(products_editor_publish_readiness_section(form, runtime, cx))
  14850             .when_some(form.save_issue, |this, issue| {
  14851                 this.child(home_body_text(app_shared_text(issue.text_key())))
  14852             })
  14853             .child(
  14854                 div()
  14855                     .w_full()
  14856                     .flex()
  14857                     .items_center()
  14858                     .justify_between()
  14859                     .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  14860                     .child(
  14861                         div()
  14862                             .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  14863                             .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  14864                             .child(product_display_title(
  14865                                 form.title_input.read(cx).value().as_ref(),
  14866                             )),
  14867                     )
  14868                     .child(save_action),
  14869             ),
  14870         text_button(
  14871             "products-editor-close",
  14872             app_shared_text(AppTextKey::ProductsEditorCloseAction),
  14873             cx.listener(|this, _, _, cx| this.close_product_editor(cx)),
  14874             cx,
  14875         ),
  14876     )
  14877 }
  14878 
  14879 fn products_editor_status_section(
  14880     selected_status: ProductStatus,
  14881     on_select_draft: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  14882     on_select_live: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  14883     on_select_paused: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  14884     on_select_archived: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  14885     cx: &App,
  14886 ) -> impl IntoElement {
  14887     div()
  14888         .w_full()
  14889         .flex()
  14890         .flex_col()
  14891         .items_start()
  14892         .gap(px(8.0))
  14893         .child(home_farm_setup_field_label(app_shared_text(
  14894             AppTextKey::ProductsEditorFieldStatus,
  14895         )))
  14896         .child(
  14897             div()
  14898                 .w_full()
  14899                 .flex()
  14900                 .items_center()
  14901                 .gap(px(8.0))
  14902                 .child(choice_button(
  14903                     "products-editor-status-draft",
  14904                     app_shared_text(AppTextKey::ProductsStatusDraft),
  14905                     selected_status == ProductStatus::Draft,
  14906                     on_select_draft,
  14907                     cx,
  14908                 ))
  14909                 .child(choice_button(
  14910                     "products-editor-status-live",
  14911                     app_shared_text(AppTextKey::ProductsStatusLive),
  14912                     selected_status == ProductStatus::Published,
  14913                     on_select_live,
  14914                     cx,
  14915                 ))
  14916                 .child(choice_button(
  14917                     "products-editor-status-paused",
  14918                     app_shared_text(AppTextKey::ProductsStatusPaused),
  14919                     selected_status == ProductStatus::Paused,
  14920                     on_select_paused,
  14921                     cx,
  14922                 ))
  14923                 .child(choice_button(
  14924                     "products-editor-status-archived",
  14925                     app_shared_text(AppTextKey::ProductsStatusArchived),
  14926                     selected_status == ProductStatus::Archived,
  14927                     on_select_archived,
  14928                     cx,
  14929                 )),
  14930         )
  14931 }
  14932 
  14933 fn products_editor_availability_section(
  14934     form: &ProductEditorFormState,
  14935     fulfillment_windows: &[FulfillmentWindowRecord],
  14936     cx: &mut Context<HomeView>,
  14937 ) -> impl IntoElement {
  14938     let choices = fulfillment_windows
  14939         .iter()
  14940         .enumerate()
  14941         .map(|(index, fulfillment_window)| {
  14942             let fulfillment_window_id = fulfillment_window.fulfillment_window_id;
  14943             choice_button(
  14944                 ("products-editor-availability", index),
  14945                 SharedString::from(fulfillment_window.label.clone()),
  14946                 form.selected_availability_window_id == Some(fulfillment_window_id),
  14947                 cx.listener(move |this, _, _, cx| {
  14948                     this.select_product_editor_availability_window(fulfillment_window_id, cx)
  14949                 }),
  14950                 cx,
  14951             )
  14952             .into_any_element()
  14953         })
  14954         .collect::<Vec<_>>();
  14955 
  14956     div()
  14957         .w_full()
  14958         .flex()
  14959         .flex_col()
  14960         .items_start()
  14961         .gap(px(APP_UI_THEME.foundation.spacing.small_px))
  14962         .child(home_farm_setup_field_label(app_shared_text(
  14963             AppTextKey::ProductsEditorFieldAvailability,
  14964         )))
  14965         .child(if choices.is_empty() {
  14966             home_body_text(app_shared_text(AppTextKey::ProductsEditorAvailabilityEmpty))
  14967                 .into_any_element()
  14968         } else {
  14969             app_cluster(APP_UI_THEME.foundation.spacing.tight_px)
  14970                 .w_full()
  14971                 .children(choices)
  14972                 .into_any_element()
  14973         })
  14974 }
  14975 
  14976 fn products_editor_publish_readiness_section(
  14977     form: &ProductEditorFormState,
  14978     runtime: &DesktopAppRuntimeSummary,
  14979     cx: &App,
  14980 ) -> impl IntoElement {
  14981     let blockers = form
  14982         .current_draft(cx)
  14983         .map(|draft| {
  14984             derive_product_publish_blockers(
  14985                 &draft,
  14986                 &runtime.farm_readiness_projection,
  14987                 &runtime.farm_rules_projection,
  14988             )
  14989         })
  14990         .unwrap_or_default();
  14991 
  14992     div()
  14993         .w_full()
  14994         .flex()
  14995         .flex_col()
  14996         .items_start()
  14997         .gap(px(8.0))
  14998         .child(home_farm_setup_field_label(app_shared_text(
  14999             AppTextKey::ProductsEditorPublishReadinessTitle,
  15000         )))
  15001         .child(if blockers.is_empty() {
  15002             home_body_text(app_shared_text(AppTextKey::ProductsEditorReady)).into_any_element()
  15003         } else {
  15004             div()
  15005                 .w_full()
  15006                 .flex()
  15007                 .flex_col()
  15008                 .items_start()
  15009                 .gap(px(8.0))
  15010                 .children(
  15011                     blockers
  15012                         .into_iter()
  15013                         .map(products_editor_publish_blocker_row)
  15014                         .collect::<Vec<_>>(),
  15015                 )
  15016                 .into_any_element()
  15017         })
  15018 }
  15019 
  15020 fn products_editor_publish_blocker_row(blocker: ProductPublishBlocker) -> AnyElement {
  15021     div()
  15022         .w_full()
  15023         .flex()
  15024         .items_start()
  15025         .gap(px(APP_UI_THEME.shells.settings_account_status_gap_px))
  15026         .child(status_indicator(
  15027             APP_UI_THEME.components.app_status_indicator.attention,
  15028         ))
  15029         .child(home_body_text(app_shared_text(
  15030             products_editor_publish_blocker_key(blocker),
  15031         )))
  15032         .into_any_element()
  15033 }
  15034 
  15035 fn products_editor_publish_blocker_key(blocker: ProductPublishBlocker) -> AppTextKey {
  15036     match blocker {
  15037         ProductPublishBlocker::AddProductName => AppTextKey::ProductsEditorBlockerAddProductName,
  15038         ProductPublishBlocker::ChooseCategory => AppTextKey::ProductsEditorBlockerChooseCategory,
  15039         ProductPublishBlocker::ChooseUnit => AppTextKey::ProductsEditorBlockerChooseUnit,
  15040         ProductPublishBlocker::SetPrice => AppTextKey::ProductsEditorBlockerSetPrice,
  15041         ProductPublishBlocker::SetStock => AppTextKey::ProductsEditorBlockerSetStock,
  15042         ProductPublishBlocker::AttachAvailability => {
  15043             AppTextKey::ProductsEditorBlockerAttachAvailability
  15044         }
  15045         ProductPublishBlocker::CompleteFarmProfile => {
  15046             AppTextKey::ProductsEditorBlockerCompleteFarmProfile
  15047         }
  15048         ProductPublishBlocker::AddPickupLocation => {
  15049             AppTextKey::ProductsEditorBlockerAddPickupLocation
  15050         }
  15051         ProductPublishBlocker::AddOperatingRules => {
  15052             AppTextKey::ProductsEditorBlockerAddOperatingRules
  15053         }
  15054         ProductPublishBlocker::AddFulfillmentWindow => {
  15055             AppTextKey::ProductsEditorBlockerAddFulfillmentWindow
  15056         }
  15057         ProductPublishBlocker::ResolveAvailabilityConflicts => {
  15058             AppTextKey::ProductsEditorBlockerResolveAvailabilityConflicts
  15059         }
  15060     }
  15061 }
  15062 
  15063 fn products_editor_validation_keys(form: &ProductEditorFormState, cx: &App) -> Vec<AppTextKey> {
  15064     let mut keys = Vec::new();
  15065 
  15066     if let Some(key) = products_editor_invalid_price_key(form, cx) {
  15067         keys.push(key);
  15068     }
  15069 
  15070     if let Some(key) = products_editor_invalid_stock_key(form, cx) {
  15071         keys.push(key);
  15072     }
  15073 
  15074     keys
  15075 }
  15076 
  15077 fn products_editor_invalid_price_key(
  15078     form: &ProductEditorFormState,
  15079     cx: &App,
  15080 ) -> Option<AppTextKey> {
  15081     parse_product_editor_price_input(form.price_input.read(cx).value().as_ref())
  15082         .is_none()
  15083         .then_some(AppTextKey::ProductsEditorInvalidPrice)
  15084 }
  15085 
  15086 fn products_editor_invalid_stock_key(
  15087     form: &ProductEditorFormState,
  15088     cx: &App,
  15089 ) -> Option<AppTextKey> {
  15090     parse_optional_product_editor_stock_input(form.stock_input.read(cx).value().as_ref())
  15091         .is_none()
  15092         .then_some(AppTextKey::ProductsEditorInvalidStock)
  15093 }
  15094 
  15095 fn parse_products_stock_quantity(input: &str) -> Option<u32> {
  15096     input.trim().parse().ok()
  15097 }
  15098 
  15099 fn parse_product_editor_price_input(input: &str) -> Option<Option<u32>> {
  15100     let trimmed = input.trim();
  15101     if trimmed.is_empty() {
  15102         return Some(None);
  15103     }
  15104 
  15105     let parse_whole_dollars = |value: &str| -> Option<u32> { value.parse::<u32>().ok() };
  15106 
  15107     if let Some((dollars, cents)) = trimmed.split_once('.') {
  15108         if trimmed.matches('.').count() != 1 || cents.is_empty() || cents.len() > 2 {
  15109             return None;
  15110         }
  15111 
  15112         let dollars = if dollars.is_empty() {
  15113             0
  15114         } else {
  15115             parse_whole_dollars(dollars)?
  15116         };
  15117         let cents = match cents.len() {
  15118             1 => cents.parse::<u32>().ok()?.checked_mul(10)?,
  15119             2 => cents.parse::<u32>().ok()?,
  15120             _ => return None,
  15121         };
  15122 
  15123         return dollars
  15124             .checked_mul(100)
  15125             .and_then(|amount| amount.checked_add(cents))
  15126             .map(Some);
  15127     }
  15128 
  15129     parse_whole_dollars(trimmed)
  15130         .and_then(|dollars| dollars.checked_mul(100))
  15131         .map(Some)
  15132 }
  15133 
  15134 fn parse_optional_product_editor_stock_input(input: &str) -> Option<Option<u32>> {
  15135     let trimmed = input.trim();
  15136     if trimmed.is_empty() {
  15137         return Some(None);
  15138     }
  15139 
  15140     trimmed.parse::<u32>().ok().map(Some)
  15141 }
  15142 
  15143 fn product_editor_price_input_value(price_minor_units: Option<u32>) -> String {
  15144     price_minor_units
  15145         .map(|amount_minor_units| {
  15146             format!(
  15147                 "{}.{:02}",
  15148                 amount_minor_units / 100,
  15149                 amount_minor_units % 100
  15150             )
  15151         })
  15152         .unwrap_or_default()
  15153 }
  15154 
  15155 fn product_display_title(title: &str) -> String {
  15156     let trimmed = title.trim();
  15157     if trimmed.is_empty() {
  15158         app_shared_text(AppTextKey::ProductsUntitledDraft).to_string()
  15159     } else {
  15160         trimmed.to_owned()
  15161     }
  15162 }
  15163 
  15164 fn home_farm_setup_onboarding_card(
  15165     spec: FarmSetupOnboardingCardSpec,
  15166     on_open_farm_setup: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  15167     cx: &App,
  15168 ) -> impl IntoElement {
  15169     home_card(
  15170         app_shared_text(spec.title_key),
  15171         div()
  15172             .w_full()
  15173             .flex()
  15174             .flex_col()
  15175             .items_start()
  15176             .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  15177             .child(home_body_text(app_shared_text(spec.body_key)))
  15178             .when_some(spec.action_key, |this, action_key| {
  15179                 this.child(div().child(action_button_primary(
  15180                     "home-farm-setup-start",
  15181                     app_shared_text(action_key),
  15182                     on_open_farm_setup,
  15183                     cx,
  15184                 )))
  15185             }),
  15186     )
  15187 }
  15188 
  15189 fn home_farm_setup_form_card(
  15190     form: &FarmSetupFormState,
  15191     on_pickup_change: impl Fn(&bool, &mut Window, &mut App) + 'static,
  15192     on_delivery_change: impl Fn(&bool, &mut Window, &mut App) + 'static,
  15193     on_shipping_change: impl Fn(&bool, &mut Window, &mut App) + 'static,
  15194     on_finish_setup: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  15195     cx: &App,
  15196 ) -> impl IntoElement {
  15197     let blockers = form.draft.blockers();
  15198     let finish_ready = blockers.is_empty();
  15199 
  15200     home_card(
  15201         app_shared_text(AppTextKey::HomeFarmSetupOnboardingTitle),
  15202         div()
  15203             .w_full()
  15204             .flex()
  15205             .flex_col()
  15206             .items_start()
  15207             .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  15208             .child(home_body_text(app_shared_text(
  15209                 AppTextKey::HomeFarmSetupOnboardingBody,
  15210             )))
  15211             .child(app_form_section(
  15212                 app_shared_text(AppTextKey::HomeFarmSetupSectionFarm),
  15213                 app_form_input_text(
  15214                     AppFormFieldSpec::new(
  15215                         app_shared_text(AppTextKey::HomeFarmSetupFieldFarmName),
  15216                         blockers
  15217                             .contains(&FarmSetupBlocker::AddFarmName)
  15218                             .then_some(AppTextKey::HomeFarmSetupBlockerAddFarmName)
  15219                             .map(app_shared_text),
  15220                     ),
  15221                     &form.farm_name_input,
  15222                     false,
  15223                 ),
  15224             ))
  15225             .child(app_form_section(
  15226                 app_shared_text(AppTextKey::HomeFarmSetupSectionLocation),
  15227                 app_form_input_text(
  15228                     AppFormFieldSpec::new(
  15229                         app_shared_text(AppTextKey::HomeFarmSetupFieldLocationOrServiceArea),
  15230                         blockers
  15231                             .contains(&FarmSetupBlocker::AddLocationOrServiceArea)
  15232                             .then_some(AppTextKey::HomeFarmSetupBlockerAddLocationOrServiceArea)
  15233                             .map(app_shared_text),
  15234                     ),
  15235                     &form.location_input,
  15236                     false,
  15237                 ),
  15238             ))
  15239             .child(home_farm_setup_order_method_section(
  15240                 form,
  15241                 blockers
  15242                     .contains(&FarmSetupBlocker::ChooseOrderMethod)
  15243                     .then_some(AppTextKey::HomeFarmSetupBlockerChooseOrderMethod),
  15244                 on_pickup_change,
  15245                 on_delivery_change,
  15246                 on_shipping_change,
  15247                 cx,
  15248             ))
  15249             .child(
  15250                 div()
  15251                     .w_full()
  15252                     .flex()
  15253                     .flex_col()
  15254                     .items_start()
  15255                     .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  15256                     .child(home_body_text(app_shared_text(farm_setup_save_state_key(
  15257                         form.save_state,
  15258                     ))))
  15259                     .child(div().child(if finish_ready {
  15260                         action_button_primary(
  15261                             "home-farm-setup-finish",
  15262                             app_shared_text(AppTextKey::HomeFarmSetupFinishAction),
  15263                             on_finish_setup,
  15264                             cx,
  15265                         )
  15266                         .into_any_element()
  15267                     } else {
  15268                         action_button_primary_disabled(
  15269                             "home-farm-setup-finish",
  15270                             app_shared_text(AppTextKey::HomeFarmSetupFinishAction),
  15271                             cx,
  15272                         )
  15273                         .into_any_element()
  15274                     })),
  15275             ),
  15276     )
  15277 }
  15278 
  15279 fn home_farm_setup_order_method_section(
  15280     form: &FarmSetupFormState,
  15281     blocker_key: Option<AppTextKey>,
  15282     on_pickup_change: impl Fn(&bool, &mut Window, &mut App) + 'static,
  15283     on_delivery_change: impl Fn(&bool, &mut Window, &mut App) + 'static,
  15284     on_shipping_change: impl Fn(&bool, &mut Window, &mut App) + 'static,
  15285     cx: &App,
  15286 ) -> impl IntoElement {
  15287     app_form_section(
  15288         app_shared_text(AppTextKey::HomeFarmSetupSectionOrderMethods),
  15289         div()
  15290             .w_full()
  15291             .flex()
  15292             .flex_col()
  15293             .items_start()
  15294             .gap(px(8.0))
  15295             .child(app_checkbox_field(
  15296                 AppCheckboxFieldSpec::new(
  15297                     "home-farm-setup-pickup",
  15298                     app_shared_text(AppTextKey::HomeFarmSetupOrderMethodPickup),
  15299                     Option::<SharedString>::None,
  15300                 ),
  15301                 form.draft.order_methods.contains(&FarmOrderMethod::Pickup),
  15302                 cx,
  15303                 move |checked, window, cx| on_pickup_change(&checked, window, cx),
  15304             ))
  15305             .child(app_checkbox_field(
  15306                 AppCheckboxFieldSpec::new(
  15307                     "home-farm-setup-delivery",
  15308                     app_shared_text(AppTextKey::HomeFarmSetupOrderMethodDelivery),
  15309                     Option::<SharedString>::None,
  15310                 ),
  15311                 form.draft
  15312                     .order_methods
  15313                     .contains(&FarmOrderMethod::Delivery),
  15314                 cx,
  15315                 move |checked, window, cx| on_delivery_change(&checked, window, cx),
  15316             ))
  15317             .child(app_checkbox_field(
  15318                 AppCheckboxFieldSpec::new(
  15319                     "home-farm-setup-shipping",
  15320                     app_shared_text(AppTextKey::HomeFarmSetupOrderMethodShipping),
  15321                     Option::<SharedString>::None,
  15322                 ),
  15323                 form.draft
  15324                     .order_methods
  15325                     .contains(&FarmOrderMethod::Shipping),
  15326                 cx,
  15327                 move |checked, window, cx| on_shipping_change(&checked, window, cx),
  15328             ))
  15329             .when_some(blocker_key, |this, blocker_key| {
  15330                 this.child(home_body_text(app_shared_text(blocker_key)))
  15331             }),
  15332     )
  15333 }
  15334 
  15335 fn settings_panel_farm_context(runtime: &DesktopAppRuntimeSummary) -> Option<(String, FarmId)> {
  15336     let account_id = runtime
  15337         .settings_account_projection
  15338         .selected_account
  15339         .as_ref()?
  15340         .account
  15341         .account_id
  15342         .clone();
  15343     let farm_id = runtime
  15344         .settings_account_projection
  15345         .selected_account
  15346         .as_ref()
  15347         .and_then(|account| account.farmer_activation.farm_id)
  15348         .or(runtime
  15349             .farm_setup_projection
  15350             .saved_farm
  15351             .as_ref()
  15352             .map(|farm| farm.farm_id))?;
  15353 
  15354     Some((account_id, farm_id))
  15355 }
  15356 
  15357 fn settings_pickup_location_title(
  15358     index: usize,
  15359     pickup_location: &SettingsPickupLocationFormState,
  15360     cx: &App,
  15361 ) -> String {
  15362     let label = pickup_location
  15363         .label_input
  15364         .read(cx)
  15365         .value()
  15366         .trim()
  15367         .to_owned();
  15368     if label.is_empty() {
  15369         format!(
  15370             "{} {}",
  15371             app_shared_text(AppTextKey::SettingsPickupLocationsSectionLabel),
  15372             index + 1
  15373         )
  15374     } else {
  15375         label
  15376     }
  15377 }
  15378 
  15379 fn settings_pickup_location_card(
  15380     index: usize,
  15381     pickup_location: &SettingsPickupLocationFormState,
  15382     on_make_default: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  15383     on_remove: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  15384     cx: &App,
  15385 ) -> impl IntoElement {
  15386     let title = settings_pickup_location_title(index, pickup_location, cx);
  15387     let action_row = div()
  15388         .flex()
  15389         .items_center()
  15390         .gap(px(8.0))
  15391         .child(if pickup_location.is_default {
  15392             settings_badge_text(app_shared_text(
  15393                 AppTextKey::SettingsPickupLocationsDefaultBadge,
  15394             ))
  15395             .into_any_element()
  15396         } else {
  15397             action_button_compact(
  15398                 ("settings-farm-default-pickup", index),
  15399                 app_shared_text(AppTextKey::SettingsPickupLocationsMakeDefaultAction),
  15400                 on_make_default,
  15401                 cx,
  15402             )
  15403             .into_any_element()
  15404         })
  15405         .when(pickup_location.can_remove, |this| {
  15406             this.child(
  15407                 action_button_compact(
  15408                     ("settings-farm-remove-pickup", index),
  15409                     app_shared_text(AppTextKey::SettingsPickupLocationsRemoveAction),
  15410                     on_remove,
  15411                     cx,
  15412                 )
  15413                 .into_any_element(),
  15414             )
  15415         });
  15416 
  15417     app_surface_panel(
  15418         app_stack_v(10.0)
  15419             .w_full()
  15420             .p(px(12.0))
  15421             .child(
  15422                 div()
  15423                     .w_full()
  15424                     .flex()
  15425                     .items_start()
  15426                     .justify_between()
  15427                     .gap(px(8.0))
  15428                     .child(
  15429                         div()
  15430                             .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  15431                             .font_weight(gpui::FontWeight::SEMIBOLD)
  15432                             .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  15433                             .child(title),
  15434                     )
  15435                     .child(action_row),
  15436             )
  15437             .child(app_form_input_text(
  15438                 AppFormFieldSpec::new(
  15439                     app_shared_text(AppTextKey::SettingsPickupLocationsFieldLabel),
  15440                     Option::<SharedString>::None,
  15441                 ),
  15442                 &pickup_location.label_input,
  15443                 false,
  15444             ))
  15445             .child(app_form_input_text(
  15446                 AppFormFieldSpec::new(
  15447                     app_shared_text(AppTextKey::SettingsPickupLocationsFieldAddress),
  15448                     Option::<SharedString>::None,
  15449                 ),
  15450                 &pickup_location.address_input,
  15451                 false,
  15452             ))
  15453             .child(app_form_input_text(
  15454                 AppFormFieldSpec::new(
  15455                     app_shared_text(AppTextKey::SettingsPickupLocationsFieldDirections),
  15456                     Option::<SharedString>::None,
  15457                 ),
  15458                 &pickup_location.directions_input,
  15459                 false,
  15460             )),
  15461     )
  15462 }
  15463 
  15464 fn settings_fulfillment_window_title(
  15465     index: usize,
  15466     fulfillment_window: &SettingsFulfillmentWindowFormState,
  15467     cx: &App,
  15468 ) -> String {
  15469     let label = fulfillment_window
  15470         .label_input
  15471         .read(cx)
  15472         .value()
  15473         .trim()
  15474         .to_owned();
  15475     if label.is_empty() {
  15476         format!(
  15477             "{} {}",
  15478             app_shared_text(AppTextKey::SettingsFulfillmentWindowsItemLabel),
  15479             index + 1
  15480         )
  15481     } else {
  15482         label
  15483     }
  15484 }
  15485 
  15486 fn settings_blackout_period_title(
  15487     index: usize,
  15488     blackout_period: &SettingsBlackoutPeriodFormState,
  15489     cx: &App,
  15490 ) -> String {
  15491     let label = blackout_period
  15492         .label_input
  15493         .read(cx)
  15494         .value()
  15495         .trim()
  15496         .to_owned();
  15497     if label.is_empty() {
  15498         format!(
  15499             "{} {}",
  15500             app_shared_text(AppTextKey::SettingsBlackoutPeriodsItemLabel),
  15501             index + 1
  15502         )
  15503     } else {
  15504         label
  15505     }
  15506 }
  15507 
  15508 fn settings_fulfillment_window_card(
  15509     index: usize,
  15510     fulfillment_window: &SettingsFulfillmentWindowFormState,
  15511     pickup_location_options: Vec<AnyElement>,
  15512     validation_keys: &[AppTextKey],
  15513     on_remove: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  15514     cx: &App,
  15515 ) -> impl IntoElement {
  15516     app_surface_panel(
  15517         app_stack_v(10.0)
  15518             .w_full()
  15519             .p(px(12.0))
  15520             .child(
  15521                 div()
  15522                     .w_full()
  15523                     .flex()
  15524                     .items_start()
  15525                     .justify_between()
  15526                     .gap(px(8.0))
  15527                     .child(
  15528                         div()
  15529                             .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  15530                             .font_weight(gpui::FontWeight::SEMIBOLD)
  15531                             .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  15532                             .child(settings_fulfillment_window_title(
  15533                                 index,
  15534                                 fulfillment_window,
  15535                                 cx,
  15536                             )),
  15537                     )
  15538                     .child(
  15539                         action_button_compact(
  15540                             ("settings-remove-fulfillment-window", index),
  15541                             app_shared_text(AppTextKey::SettingsFulfillmentWindowsRemoveAction),
  15542                             on_remove,
  15543                             cx,
  15544                         )
  15545                         .into_any_element(),
  15546                     ),
  15547             )
  15548             .child(app_form_input_text(
  15549                 AppFormFieldSpec::new(
  15550                     app_shared_text(AppTextKey::SettingsFulfillmentWindowsFieldLabel),
  15551                     Option::<SharedString>::None,
  15552                 ),
  15553                 &fulfillment_window.label_input,
  15554                 false,
  15555             ))
  15556             .child(app_form_field(
  15557                 AppFormFieldSpec::new(
  15558                     app_shared_text(AppTextKey::SettingsFulfillmentWindowsFieldPickupLocation),
  15559                     Option::<SharedString>::None,
  15560                 ),
  15561                 div()
  15562                     .w_full()
  15563                     .flex()
  15564                     .flex_wrap()
  15565                     .gap(px(8.0))
  15566                     .children(pickup_location_options),
  15567             ))
  15568             .child(app_form_input_text(
  15569                 AppFormFieldSpec::new(
  15570                     app_shared_text(AppTextKey::SettingsFulfillmentWindowsFieldStartsAt),
  15571                     Option::<SharedString>::None,
  15572                 ),
  15573                 &fulfillment_window.starts_at_input,
  15574                 false,
  15575             ))
  15576             .child(app_form_input_text(
  15577                 AppFormFieldSpec::new(
  15578                     app_shared_text(AppTextKey::SettingsFulfillmentWindowsFieldEndsAt),
  15579                     Option::<SharedString>::None,
  15580                 ),
  15581                 &fulfillment_window.ends_at_input,
  15582                 false,
  15583             ))
  15584             .child(app_form_input_text(
  15585                 AppFormFieldSpec::new(
  15586                     app_shared_text(AppTextKey::SettingsFulfillmentWindowsFieldOrderCutoff),
  15587                     Option::<SharedString>::None,
  15588                 ),
  15589                 &fulfillment_window.order_cutoff_input,
  15590                 false,
  15591             ))
  15592             .children(
  15593                 validation_keys
  15594                     .iter()
  15595                     .copied()
  15596                     .map(|key| home_body_text(app_shared_text(key)).into_any_element())
  15597                     .collect::<Vec<_>>(),
  15598             ),
  15599     )
  15600 }
  15601 
  15602 fn settings_blackout_period_card(
  15603     index: usize,
  15604     blackout_period: &SettingsBlackoutPeriodFormState,
  15605     validation_keys: &[AppTextKey],
  15606     on_remove: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  15607     cx: &App,
  15608 ) -> impl IntoElement {
  15609     app_surface_panel(
  15610         app_stack_v(10.0)
  15611             .w_full()
  15612             .p(px(12.0))
  15613             .child(
  15614                 div()
  15615                     .w_full()
  15616                     .flex()
  15617                     .items_start()
  15618                     .justify_between()
  15619                     .gap(px(8.0))
  15620                     .child(
  15621                         div()
  15622                             .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  15623                             .font_weight(gpui::FontWeight::SEMIBOLD)
  15624                             .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  15625                             .child(settings_blackout_period_title(index, blackout_period, cx)),
  15626                     )
  15627                     .child(
  15628                         action_button_compact(
  15629                             ("settings-remove-blackout-period", index),
  15630                             app_shared_text(AppTextKey::SettingsBlackoutPeriodsRemoveAction),
  15631                             on_remove,
  15632                             cx,
  15633                         )
  15634                         .into_any_element(),
  15635                     ),
  15636             )
  15637             .child(app_form_input_text(
  15638                 AppFormFieldSpec::new(
  15639                     app_shared_text(AppTextKey::SettingsBlackoutPeriodsFieldLabel),
  15640                     Option::<SharedString>::None,
  15641                 ),
  15642                 &blackout_period.label_input,
  15643                 false,
  15644             ))
  15645             .child(app_form_input_text(
  15646                 AppFormFieldSpec::new(
  15647                     app_shared_text(AppTextKey::SettingsBlackoutPeriodsFieldStartsAt),
  15648                     Option::<SharedString>::None,
  15649                 ),
  15650                 &blackout_period.starts_at_input,
  15651                 false,
  15652             ))
  15653             .child(app_form_input_text(
  15654                 AppFormFieldSpec::new(
  15655                     app_shared_text(AppTextKey::SettingsBlackoutPeriodsFieldEndsAt),
  15656                     Option::<SharedString>::None,
  15657                 ),
  15658                 &blackout_period.ends_at_input,
  15659                 false,
  15660             ))
  15661             .children(
  15662                 validation_keys
  15663                     .iter()
  15664                     .copied()
  15665                     .map(|key| home_body_text(app_shared_text(key)).into_any_element())
  15666                     .collect::<Vec<_>>(),
  15667             ),
  15668     )
  15669 }
  15670 
  15671 fn settings_farm_readiness_rows(evaluation: &SettingsFarmRulesEvaluation) -> Vec<AnyElement> {
  15672     let readiness_keys = if evaluation.readiness_keys.is_empty() {
  15673         vec![AppTextKey::SettingsReadinessReady]
  15674     } else {
  15675         evaluation.readiness_keys.clone()
  15676     };
  15677 
  15678     readiness_keys
  15679         .into_iter()
  15680         .map(|key| {
  15681             app_surface_panel(
  15682                 div()
  15683                     .px(px(12.0))
  15684                     .py(px(10.0))
  15685                     .child(home_farm_setup_field_label(app_shared_text(key))),
  15686             )
  15687             .into_any_element()
  15688         })
  15689         .collect()
  15690 }
  15691 
  15692 fn settings_readiness_key(blocker: FarmReadinessBlocker) -> AppTextKey {
  15693     match blocker {
  15694         FarmReadinessBlocker::MissingProfileBasics => {
  15695             AppTextKey::SettingsReadinessFieldMissingProfileBasics
  15696         }
  15697         FarmReadinessBlocker::MissingPickupLocation => {
  15698             AppTextKey::SettingsReadinessFieldMissingPickupLocation
  15699         }
  15700         FarmReadinessBlocker::MissingFulfillmentWindow => {
  15701             AppTextKey::SettingsReadinessFieldMissingFulfillmentWindow
  15702         }
  15703         FarmReadinessBlocker::MissingOperatingRules => {
  15704             AppTextKey::SettingsReadinessFieldMissingOperatingRules
  15705         }
  15706     }
  15707 }
  15708 
  15709 fn settings_timing_conflict_key(kind: FarmTimingConflictKind) -> AppTextKey {
  15710     match kind {
  15711         FarmTimingConflictKind::FulfillmentWindowEndsBeforeStart => {
  15712             AppTextKey::SettingsReadinessFieldFulfillmentWindowEndsBeforeStart
  15713         }
  15714         FarmTimingConflictKind::FulfillmentWindowCutoffAfterStart => {
  15715             AppTextKey::SettingsReadinessFieldFulfillmentWindowCutoffAfterStart
  15716         }
  15717         FarmTimingConflictKind::BlackoutPeriodEndsBeforeStart => {
  15718             AppTextKey::SettingsReadinessFieldBlackoutPeriodEndsBeforeStart
  15719         }
  15720         FarmTimingConflictKind::BlackoutOverlapsFulfillmentWindow => {
  15721             AppTextKey::SettingsReadinessFieldBlackoutOverlapsFulfillmentWindow
  15722         }
  15723     }
  15724 }
  15725 
  15726 fn home_saved_farm_summary_card(runtime: &DesktopAppRuntimeSummary) -> Option<AnyElement> {
  15727     let saved_farm = home_saved_farm(runtime)?;
  15728     let location_or_service_area = if runtime
  15729         .farm_setup_projection
  15730         .draft
  15731         .location_or_service_area
  15732         .trim()
  15733         .is_empty()
  15734     {
  15735         app_shared_text(AppTextKey::ValueNone).to_string()
  15736     } else {
  15737         runtime
  15738             .farm_setup_projection
  15739             .draft
  15740             .location_or_service_area
  15741             .clone()
  15742     };
  15743 
  15744     Some(
  15745         home_card(
  15746             saved_farm.display_name.clone(),
  15747             label_value_list(vec![
  15748                 LabelValueRow::new(
  15749                     app_shared_text(AppTextKey::HomeFarmSetupFieldLocationOrServiceArea),
  15750                     location_or_service_area,
  15751                 ),
  15752                 LabelValueRow::new(
  15753                     app_shared_text(AppTextKey::HomeFarmSetupSectionOrderMethods),
  15754                     home_farm_order_methods_summary(&runtime.farm_setup_projection.draft),
  15755                 ),
  15756             ]),
  15757         )
  15758         .into_any_element(),
  15759     )
  15760 }
  15761 
  15762 fn home_status_row(status: &HomeStatusPresentation) -> impl IntoElement {
  15763     div()
  15764         .flex()
  15765         .items_center()
  15766         .gap(px(APP_UI_THEME.shells.settings_account_status_gap_px))
  15767         .child(status_indicator(status.indicator_color))
  15768         .child(
  15769             div()
  15770                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  15771                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  15772                 .child(app_shared_text(status.label_key)),
  15773         )
  15774 }
  15775 
  15776 fn home_summary_card(summary: &radroots_app_view::TodaySummary) -> impl IntoElement {
  15777     home_card(
  15778         app_shared_text(AppTextKey::HomeTodayTitle),
  15779         div()
  15780             .w_full()
  15781             .flex()
  15782             .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  15783             .child(home_summary_metric(
  15784                 AppTextKey::HomeTodayOrdersNeedingAction,
  15785                 summary.orders_needing_action,
  15786             ))
  15787             .child(home_summary_metric(
  15788                 AppTextKey::HomeTodayLowStock,
  15789                 summary.low_stock_products,
  15790             ))
  15791             .child(home_summary_metric(
  15792                 AppTextKey::HomeTodayDraftProducts,
  15793                 summary.draft_products,
  15794             )),
  15795     )
  15796 }
  15797 
  15798 fn home_summary_metric(label_key: AppTextKey, value: u32) -> impl IntoElement {
  15799     div()
  15800         .flex_1()
  15801         .min_w_0()
  15802         .bg(rgb(APP_UI_THEME.foundation.surfaces.window_background))
  15803         .rounded(px(APP_UI_THEME.foundation.radii.medium_px))
  15804         .p(px(16.0))
  15805         .flex()
  15806         .flex_col()
  15807         .gap(px(4.0))
  15808         .child(
  15809             div()
  15810                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px * 2.0))
  15811                 .font_weight(gpui::FontWeight::BOLD)
  15812                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  15813                 .child(value.to_string()),
  15814         )
  15815         .child(
  15816             div()
  15817                 .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  15818                 .line_height(relative(1.2))
  15819                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  15820                 .child(app_shared_text(label_key)),
  15821         )
  15822 }
  15823 
  15824 fn home_setup_card(
  15825     projection: &TodayAgendaProjection,
  15826     continue_action: Option<AnyElement>,
  15827 ) -> impl IntoElement {
  15828     home_list_card(
  15829         AppTextKey::HomeTodaySetupChecklist,
  15830         projection
  15831             .setup_checklist
  15832             .iter()
  15833             .map(home_setup_task_row)
  15834             .collect::<Vec<_>>(),
  15835         continue_action,
  15836     )
  15837 }
  15838 
  15839 fn home_next_fulfillment_window_card(
  15840     next_window: &FulfillmentWindowSummary,
  15841     action: Option<AnyElement>,
  15842 ) -> impl IntoElement {
  15843     home_card(
  15844         app_shared_text(AppTextKey::HomeTodayNextFulfillmentWindow),
  15845         div()
  15846             .w_full()
  15847             .flex()
  15848             .flex_col()
  15849             .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  15850             .child(label_value_list(vec![
  15851                 LabelValueRow::new(
  15852                     app_shared_text(AppTextKey::HomeTodayWindowStartsLabel),
  15853                     next_window.starts_at.clone(),
  15854                 ),
  15855                 LabelValueRow::new(
  15856                     app_shared_text(AppTextKey::HomeTodayWindowEndsLabel),
  15857                     next_window.ends_at.clone(),
  15858                 ),
  15859             ]))
  15860             .when_some(action, |this, action| this.child(div().child(action))),
  15861     )
  15862 }
  15863 
  15864 fn home_list_card(
  15865     title_key: AppTextKey,
  15866     rows: Vec<AnyElement>,
  15867     action: Option<AnyElement>,
  15868 ) -> impl IntoElement {
  15869     home_card(
  15870         app_shared_text(title_key),
  15871         div()
  15872             .w_full()
  15873             .flex()
  15874             .flex_col()
  15875             .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  15876             .children(rows)
  15877             .when_some(action, |this, action| this.child(div().child(action))),
  15878     )
  15879 }
  15880 
  15881 fn order_detail_item_row(item: &OrderDetailItemRow) -> AnyElement {
  15882     let unit_price = item.unit_price.as_ref().map(buyer_listing_price_text);
  15883     let line_total = item.unit_price.as_ref().and_then(|unit_price| {
  15884         item.line_total_minor_units
  15885             .map(|amount| buyer_money_text(amount, unit_price.currency_code.as_str()))
  15886     });
  15887 
  15888     div()
  15889         .w_full()
  15890         .min_w_0()
  15891         .flex()
  15892         .items_center()
  15893         .justify_between()
  15894         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  15895         .child(
  15896             div()
  15897                 .flex_1()
  15898                 .min_w_0()
  15899                 .flex()
  15900                 .flex_col()
  15901                 .gap(px(2.0))
  15902                 .child(
  15903                     div()
  15904                         .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  15905                         .font_weight(gpui::FontWeight::MEDIUM)
  15906                         .line_height(relative(1.2))
  15907                         .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  15908                         .child(item.title.clone()),
  15909                 )
  15910                 .when_some(unit_price, |this, unit_price| {
  15911                     this.child(
  15912                         div()
  15913                             .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  15914                             .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  15915                             .child(unit_price),
  15916                     )
  15917                 }),
  15918         )
  15919         .child(
  15920             div()
  15921                 .flex()
  15922                 .flex_col()
  15923                 .items_end()
  15924                 .gap(px(2.0))
  15925                 .child(
  15926                     div()
  15927                         .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
  15928                         .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  15929                         .child(item.quantity_display.clone()),
  15930                 )
  15931                 .when_some(line_total, |this, line_total| {
  15932                     this.child(
  15933                         div()
  15934                             .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  15935                             .font_weight(gpui::FontWeight::MEDIUM)
  15936                             .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  15937                             .child(line_total),
  15938                     )
  15939                 }),
  15940         )
  15941         .into_any_element()
  15942 }
  15943 
  15944 fn order_optional_text(value: Option<&str>) -> SharedString {
  15945     value
  15946         .filter(|value| !value.trim().is_empty())
  15947         .map(|value| SharedString::from(value.to_owned()))
  15948         .unwrap_or_else(|| app_shared_text(AppTextKey::ValueNone))
  15949 }
  15950 
  15951 fn home_order_row(
  15952     index: usize,
  15953     order: &OrderListRow,
  15954     on_open: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
  15955     cx: &App,
  15956 ) -> AnyElement {
  15957     div()
  15958         .w_full()
  15959         .min_w_0()
  15960         .flex()
  15961         .items_center()
  15962         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  15963         .child(list_row_button(
  15964             ("home-today-order-open", index),
  15965             order.order_number.clone(),
  15966             Some(SharedString::from(order.customer_display_name.clone())),
  15967             false,
  15968             on_open,
  15969             cx,
  15970         ))
  15971         .child(status_indicator(
  15972             APP_UI_THEME.components.app_status_indicator.attention,
  15973         ))
  15974         .into_any_element()
  15975 }
  15976 
  15977 fn home_low_stock_row(product: &ProductListRow) -> AnyElement {
  15978     div()
  15979         .w_full()
  15980         .min_w_0()
  15981         .flex()
  15982         .items_center()
  15983         .justify_between()
  15984         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  15985         .child(
  15986             div()
  15987                 .min_w_0()
  15988                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  15989                 .font_weight(gpui::FontWeight::SEMIBOLD)
  15990                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  15991                 .child(product_display_title(product.title.as_str())),
  15992         )
  15993         .child(
  15994             div()
  15995                 .flex()
  15996                 .items_center()
  15997                 .gap(px(APP_UI_THEME.shells.settings_account_status_gap_px))
  15998                 .child(status_indicator(
  15999                     APP_UI_THEME.components.app_status_indicator.attention,
  16000                 ))
  16001                 .child(
  16002                     div()
  16003                         .flex()
  16004                         .items_center()
  16005                         .gap(px(4.0))
  16006                         .child(
  16007                             div()
  16008                                 .text_size(px(APP_UI_THEME
  16009                                     .foundation
  16010                                     .typography
  16011                                     .utility_title_text_px))
  16012                                 .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
  16013                                 .child(app_shared_label_text(AppTextKey::HomeTodayStockCountLabel)),
  16014                         )
  16015                         .child(
  16016                             div()
  16017                                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  16018                                 .font_weight(gpui::FontWeight::SEMIBOLD)
  16019                                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  16020                                 .child(product.stock_count.to_string()),
  16021                         ),
  16022                 ),
  16023         )
  16024         .into_any_element()
  16025 }
  16026 
  16027 fn home_draft_row(product: &ProductListRow) -> AnyElement {
  16028     div()
  16029         .w_full()
  16030         .min_w_0()
  16031         .flex()
  16032         .items_center()
  16033         .justify_between()
  16034         .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
  16035         .child(
  16036             div()
  16037                 .min_w_0()
  16038                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  16039                 .font_weight(gpui::FontWeight::SEMIBOLD)
  16040                 .text_color(rgb(APP_UI_THEME.foundation.text.primary))
  16041                 .child(product_display_title(product.title.as_str())),
  16042         )
  16043         .child(status_indicator(
  16044             APP_UI_THEME.components.app_status_indicator.offline,
  16045         ))
  16046         .into_any_element()
  16047 }
  16048 
  16049 fn home_setup_task_row(task: &radroots_app_view::TodaySetupTask) -> AnyElement {
  16050     let is_complete = task.is_complete;
  16051 
  16052     div()
  16053         .w_full()
  16054         .min_w_0()
  16055         .flex()
  16056         .items_center()
  16057         .gap(px(APP_UI_THEME.shells.settings_account_status_gap_px))
  16058         .child(status_indicator(if is_complete {
  16059             APP_UI_THEME.components.app_status_indicator.online
  16060         } else {
  16061             APP_UI_THEME.components.app_status_indicator.offline
  16062         }))
  16063         .child(
  16064             div()
  16065                 .flex_1()
  16066                 .min_w_0()
  16067                 .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
  16068                 .font_weight(gpui::FontWeight::MEDIUM)
  16069                 .line_height(relative(1.2))
  16070                 .text_color(rgb(if is_complete {
  16071                     APP_UI_THEME.foundation.text.secondary
  16072                 } else {
  16073                     APP_UI_THEME.foundation.text.primary
  16074                 }))
  16075                 .child(app_shared_text(home_setup_task_label_key(task.kind))),
  16076         )
  16077         .into_any_element()
  16078 }
  16079 
  16080 fn home_empty_state_card(title_key: AppTextKey, body_key: AppTextKey) -> impl IntoElement {
  16081     home_card(
  16082         app_shared_text(title_key),
  16083         home_body_text(app_shared_text(body_key)),
  16084     )
  16085 }
  16086 
  16087 fn buyer_order_place_failure_notice(error: &AppSqliteError) -> BuyerWorkspaceNotice {
  16088     match error {
  16089         AppSqliteError::LocalEventsSql { .. } | AppSqliteError::LocalEvents { .. } => {
  16090             BuyerWorkspaceNotice::OrderCoordinationFailed
  16091         }
  16092         _ => BuyerWorkspaceNotice::OrderPlaceFailed,
  16093     }
  16094 }
  16095 
  16096 fn buyer_order_coordination_notice_forces_redraw(notice: BuyerWorkspaceNotice) -> bool {
  16097     notice == BuyerWorkspaceNotice::OrderCoordinationFailed
  16098 }
  16099 
  16100 fn buyer_workspace_notice_card(notice: String) -> impl IntoElement {
  16101     app_surface_card(home_body_text(notice))
  16102 }
  16103 
  16104 fn farm_setup_onboarding_card_spec(home_route: HomeRoute) -> Option<FarmSetupOnboardingCardSpec> {
  16105     match home_route {
  16106         HomeRoute::FarmSetupOnboarding => Some(FarmSetupOnboardingCardSpec {
  16107             title_key: AppTextKey::HomeFarmSetupOnboardingTitle,
  16108             body_key: AppTextKey::HomeFarmSetupOnboardingBody,
  16109             action_key: Some(AppTextKey::HomeFarmSetupOnboardingAction),
  16110         }),
  16111         HomeRoute::FarmSetupForm => Some(FarmSetupOnboardingCardSpec {
  16112             title_key: AppTextKey::HomeFarmSetupOnboardingTitle,
  16113             body_key: AppTextKey::HomeFarmSetupOnboardingBody,
  16114             action_key: None,
  16115         }),
  16116         _ => None,
  16117     }
  16118 }
  16119 
  16120 fn farm_setup_save_state_key(state: FarmSetupSaveState) -> AppTextKey {
  16121     match state {
  16122         FarmSetupSaveState::AutosavesLocally => AppTextKey::HomeFarmSetupSaveAutosavesLocally,
  16123         FarmSetupSaveState::SavedLocally => AppTextKey::HomeFarmSetupSaveSavedLocally,
  16124         FarmSetupSaveState::SaveFailed => AppTextKey::HomeFarmSetupSaveFailedLocally,
  16125     }
  16126 }
  16127 
  16128 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
  16129 enum FarmerHomeFarmState {
  16130     NoFarm,
  16131     IncompleteFarm,
  16132     ConfiguredFarm,
  16133 }
  16134 
  16135 fn home_saved_farm(runtime: &DesktopAppRuntimeSummary) -> Option<&FarmSummary> {
  16136     runtime
  16137         .today_projection
  16138         .farm
  16139         .as_ref()
  16140         .or(runtime.farm_setup_projection.saved_farm.as_ref())
  16141 }
  16142 
  16143 fn farmer_home_farm_state(runtime: &DesktopAppRuntimeSummary) -> FarmerHomeFarmState {
  16144     match runtime.farm_readiness_projection.status {
  16145         FarmWorkspaceStatus::NoFarm => FarmerHomeFarmState::NoFarm,
  16146         FarmWorkspaceStatus::SetupRequired => {
  16147             if home_saved_farm(runtime).is_some() {
  16148                 FarmerHomeFarmState::IncompleteFarm
  16149             } else {
  16150                 FarmerHomeFarmState::NoFarm
  16151             }
  16152         }
  16153         FarmWorkspaceStatus::Ready => FarmerHomeFarmState::ConfiguredFarm,
  16154     }
  16155 }
  16156 
  16157 fn home_farm_order_methods_summary(draft: &FarmSetupDraft) -> String {
  16158     if draft.order_methods.is_empty() {
  16159         return app_shared_text(AppTextKey::ValueNone).to_string();
  16160     }
  16161 
  16162     draft
  16163         .order_methods
  16164         .iter()
  16165         .copied()
  16166         .map(home_farm_order_method_label_key)
  16167         .map(app_shared_text)
  16168         .map(|label| label.to_string())
  16169         .collect::<Vec<_>>()
  16170         .join(", ")
  16171 }
  16172 
  16173 fn home_status_presentation(runtime: &DesktopAppRuntimeSummary) -> HomeStatusPresentation {
  16174     if runtime.startup_issue.is_some() || runtime.startup_gate == AppStartupGate::Blocked {
  16175         return HomeStatusPresentation {
  16176             indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  16177             label_key: AppTextKey::HomeTodayStatusStartupIssue,
  16178         };
  16179     }
  16180 
  16181     if runtime.startup_gate == AppStartupGate::SetupRequired {
  16182         return HomeStatusPresentation {
  16183             indicator_color: APP_UI_THEME.components.app_status_indicator.offline,
  16184             label_key: AppTextKey::HomeTodayStatusSetup,
  16185         };
  16186     }
  16187 
  16188     match farmer_home_farm_state(runtime) {
  16189         FarmerHomeFarmState::NoFarm => {
  16190             return HomeStatusPresentation {
  16191                 indicator_color: APP_UI_THEME.components.app_status_indicator.offline,
  16192                 label_key: AppTextKey::HomeTodayStatusNoFarm,
  16193             };
  16194         }
  16195         FarmerHomeFarmState::IncompleteFarm => {
  16196             return HomeStatusPresentation {
  16197                 indicator_color: APP_UI_THEME.components.app_status_indicator.offline,
  16198                 label_key: AppTextKey::HomeTodayStatusSetup,
  16199             };
  16200         }
  16201         FarmerHomeFarmState::ConfiguredFarm => {}
  16202     }
  16203 
  16204     if runtime.today_projection.has_attention_items() {
  16205         return HomeStatusPresentation {
  16206             indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  16207             label_key: AppTextKey::HomeTodayStatusAttention,
  16208         };
  16209     }
  16210 
  16211     HomeStatusPresentation {
  16212         indicator_color: APP_UI_THEME.components.app_status_indicator.online,
  16213         label_key: AppTextKey::HomeTodayStatusReady,
  16214     }
  16215 }
  16216 
  16217 fn home_setup_task_label_key(kind: TodaySetupTaskKind) -> AppTextKey {
  16218     match kind {
  16219         TodaySetupTaskKind::CompleteFarmProfile => AppTextKey::HomeTodaySetupCompleteFarmProfile,
  16220         TodaySetupTaskKind::AddPickupLocation => AppTextKey::HomeTodaySetupAddPickupLocation,
  16221         TodaySetupTaskKind::AddOperatingRules => AppTextKey::HomeTodaySetupAddOperatingRules,
  16222         TodaySetupTaskKind::AddFulfillmentWindow => AppTextKey::HomeTodaySetupAddFulfillmentWindow,
  16223         TodaySetupTaskKind::ResolveAvailabilityConflicts => {
  16224             AppTextKey::HomeTodaySetupResolveAvailabilityConflicts
  16225         }
  16226         TodaySetupTaskKind::PublishProduct => AppTextKey::HomeTodaySetupPublishProduct,
  16227     }
  16228 }
  16229 
  16230 fn home_farm_order_method_label_key(method: FarmOrderMethod) -> AppTextKey {
  16231     match method {
  16232         FarmOrderMethod::Pickup => AppTextKey::HomeFarmSetupOrderMethodPickup,
  16233         FarmOrderMethod::Delivery => AppTextKey::HomeFarmSetupOrderMethodDelivery,
  16234         FarmOrderMethod::Shipping => AppTextKey::HomeFarmSetupOrderMethodShipping,
  16235     }
  16236 }
  16237 
  16238 #[cfg(test)]
  16239 mod tests {
  16240     use super::{
  16241         APP_UI_THEME, AppTextKey, BuyerWorkspaceNotice, FarmerHomeFarmState, HomeAutoFocusState,
  16242         HomeAutoFocusTarget, HomeFocusedView, HomeStage, HomeView, LabelValueRow,
  16243         PackDayBatchPrintActionPresentation, PackDayBatchPrintStatusPresentation,
  16244         PackDayExportStatusPresentation, PackDayHostHandoffActionPresentation,
  16245         PackDayHostHandoffStatusPresentation, PackDayPrintActionPresentation,
  16246         PackDayPrintStatusPresentation, ReminderActionTarget, SETTINGS_FARM_PANEL_SECTIONS,
  16247         SETTINGS_NAVIGATION_ORDER, SETTINGS_OPERATIONS_PANEL_SECTIONS, SettingsAutoFocusTarget,
  16248         SettingsInventorySectionSpec, SettingsPanelViewKey, ShellHeaderActiveMode,
  16249         StartupHomeSurface, StartupSignerConnectState, abbreviated_npub,
  16250         about_conflict_action_specs, about_conflict_aggregate_text, about_conflict_detail_rows,
  16251         about_conflict_review_body_key, about_manual_refresh_enabled, about_runtime_rows,
  16252         about_status_rows, account_display_name, app_text,
  16253         buyer_order_coordination_notice_forces_redraw, buyer_order_detail_focus_after_open,
  16254         buyer_orders_retry_action_visible, farm_setup_onboarding_card_spec, farmer_home_farm_state,
  16255         farmer_order_detail_focus_after_open, farmer_pack_day_available, home_auto_focus_target,
  16256         home_content_scroll_id, home_saved_farm, home_sidebar_navigation_sections, home_stage,
  16257         home_window_launch_size_px, home_window_minimum_size_px,
  16258         pack_day_batch_print_action_presentation, pack_day_batch_print_status_presentation,
  16259         pack_day_export_action_enabled, pack_day_export_action_label_key,
  16260         pack_day_export_artifact_names, pack_day_export_detail_rows,
  16261         pack_day_export_status_presentation, pack_day_host_handoff_action_presentations,
  16262         pack_day_host_handoff_status_presentation, pack_day_print_action_presentations,
  16263         pack_day_print_status_presentation, parse_optional_product_editor_stock_input,
  16264         parse_product_editor_price_input, presented_farmer_reminder, product_display_title,
  16265         reminder_action_target, reminder_deadline_text, reminder_delivery_state_key,
  16266         reminder_urgency_color, reminder_urgency_key, settings_auto_focus_target,
  16267         settings_preferences_general_row_state, shell_header_active_mode, startup_home_surface,
  16268         startup_issue_summary_text, startup_notice_text, startup_signer_preview_summary,
  16269         startup_signer_preview_summary_for_connect_state, startup_signer_source_input_is_editable,
  16270         startup_signer_status_spec, startup_signer_transport_failure_requires_notice,
  16271         trade_agreement_status_key, trade_inventory_status_key, trade_revision_status_key,
  16272         trade_workflow_source_key,
  16273     };
  16274     use crate::runtime::{
  16275         DesktopAppRuntimeMetadataSummary, DesktopAppRuntimeSummary, DesktopAppSdkDiagnosticsState,
  16276         DesktopAppSdkDiagnosticsSummary, DesktopAppSdkIssueSummary,
  16277         DesktopAppSdkReadyDiagnosticsSummary, DesktopAppSdkStatusSummary,
  16278         DesktopAppSyncConflictSummary, DesktopAppSyncStatusSummary,
  16279     };
  16280     use radroots_app_core::{
  16281         AppDesktopRuntimePaths, AppRuntimeHostEnvironment, AppRuntimePlatform,
  16282         AppSdkLifecycleState, AppSdkProjectionLifecycleState, AppSdkRelayUrlPolicy,
  16283     };
  16284     use radroots_app_remote_signer::{
  16285         RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingSession,
  16286         RadrootsAppRemoteSignerSessionRecord,
  16287     };
  16288     use radroots_app_state::{
  16289         AppShellProjection, BuyerOrdersScreenProjection, FarmWorkspaceReadinessProjection,
  16290         FarmWorkspaceStatus, HomeRoute, PackDayBatchPrintProjection, PackDayBatchPrintRequest,
  16291         PackDayExportProjection, PackDayHostHandoffProjection, PackDayHostHandoffRequest,
  16292         PackDayPrintProjection, PackDayPrintRequest,
  16293     };
  16294     use radroots_app_sync::{
  16295         AppSyncProjection, AppSyncRunStatus, SyncAggregateRef, SyncCheckpointStatus, SyncConflict,
  16296         SyncConflictKind, SyncConflictResolutionStatus, SyncConflictSeverity, SyncConflictStatus,
  16297     };
  16298     use radroots_app_view::SettingsAccountProjection;
  16299     use radroots_app_view::{
  16300         AccountCustody, AccountSummary, ActiveSurface, AppStartupGate, BuyerOrderDetailProjection,
  16301         BuyerOrderStatus, BuyerOrdersListRow, FarmId, FarmOrderMethod, FarmReadiness,
  16302         FarmSetupDraft, FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId,
  16303         FulfillmentWindowSummary, LoggedOutStartupPhase, LoggedOutStartupProjection,
  16304         OrderDetailProjection, OrderId, OrderStatus, OrdersListRow, PackDayBatchPrintArtifact,
  16305         PackDayBatchPrintFailureKind, PackDayExportArtifact, PackDayExportArtifactKind,
  16306         PackDayExportBundle, PackDayHostHandoffKind, PackDayHostHandoffStatus,
  16307         PackDayPrintFailureKind, PackDayPrintKind, PackDayPrintStatus, PackDayProductTotalRow,
  16308         PackDayProjection, PersonalSection, ProductId, ReminderDeadlineProjection,
  16309         ReminderDeliveryState, ReminderId, ReminderKind, ReminderSurface, ReminderUrgency,
  16310         RepeatDemandEligibility, RepeatDemandHandoffProjection, ShellSection,
  16311         TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TradeAgreementStatus,
  16312         TradeEconomicsProjection, TradeInventoryStatus, TradeRevisionStatus,
  16313         TradeWorkflowProjection, TradeWorkflowSource,
  16314     };
  16315     use radroots_identity::RadrootsIdentity;
  16316     use std::{
  16317         fs,
  16318         path::PathBuf,
  16319         time::{SystemTime, UNIX_EPOCH},
  16320     };
  16321 
  16322     struct TestDirectory {
  16323         path: PathBuf,
  16324     }
  16325 
  16326     impl TestDirectory {
  16327         fn new() -> Self {
  16328             let path = std::env::temp_dir().join(FulfillmentWindowId::new().to_string());
  16329             fs::create_dir_all(&path).unwrap();
  16330             Self { path }
  16331         }
  16332 
  16333         fn path(&self) -> &PathBuf {
  16334             &self.path
  16335         }
  16336     }
  16337 
  16338     impl Drop for TestDirectory {
  16339         fn drop(&mut self) {
  16340             let _ = fs::remove_dir_all(&self.path);
  16341         }
  16342     }
  16343 
  16344     fn write_artifact(bundle_directory: &PathBuf, file_name: &str) -> PathBuf {
  16345         let path = bundle_directory.join(file_name);
  16346         fs::write(&path, file_name).unwrap();
  16347         path
  16348     }
  16349 
  16350     fn test_home_view(label: &str) -> (HomeView, AppDesktopRuntimePaths, PathBuf) {
  16351         let suffix = SystemTime::now()
  16352             .duration_since(UNIX_EPOCH)
  16353             .expect("clock")
  16354             .as_nanos();
  16355         let home_dir = std::env::temp_dir().join(format!("radroots_home_view_{label}_{suffix}"));
  16356         let paths = AppDesktopRuntimePaths::for_desktop(
  16357             AppRuntimePlatform::Macos,
  16358             AppRuntimeHostEnvironment {
  16359                 home_dir: Some(home_dir.clone()),
  16360                 ..AppRuntimeHostEnvironment::default()
  16361             },
  16362         )
  16363         .expect("desktop runtime paths should resolve");
  16364         let runtime = crate::runtime::DesktopAppRuntime::bootstrap_with_paths(
  16365             paths.clone(),
  16366             vec!["wss://relay.example".to_owned()],
  16367         );
  16368 
  16369         (HomeView::new(runtime), paths, home_dir)
  16370     }
  16371 
  16372     fn block_shared_local_events_database(paths: &AppDesktopRuntimePaths) {
  16373         let database_path = paths.shared_local_events_database_path().unwrap();
  16374         if let Some(parent) = database_path.parent() {
  16375             fs::create_dir_all(parent).unwrap();
  16376         }
  16377         if database_path.is_file() {
  16378             fs::remove_file(&database_path).unwrap();
  16379         } else if database_path.is_dir() {
  16380             fs::remove_dir_all(&database_path).unwrap();
  16381         }
  16382         fs::create_dir(&database_path).unwrap();
  16383     }
  16384 
  16385     #[test]
  16386     fn buyer_workspace_notice_tracks_visible_buyer_runtime_errors() {
  16387         let (mut view, _, home_dir) = test_home_view("buyer_notice");
  16388 
  16389         assert!(view.set_buyer_workspace_notice(BuyerWorkspaceNotice::MarketplaceRefreshFailed));
  16390         assert_eq!(
  16391             view.buyer_workspace_notice.as_deref(),
  16392             Some(app_text(AppTextKey::PersonalMarketplaceRefreshFailedNotice).as_str())
  16393         );
  16394         assert!(!view.set_buyer_workspace_notice(BuyerWorkspaceNotice::MarketplaceRefreshFailed));
  16395         assert!(view.set_buyer_workspace_notice(BuyerWorkspaceNotice::OrderPlaceFailed));
  16396         assert_eq!(
  16397             view.buyer_workspace_notice.as_deref(),
  16398             Some(app_text(AppTextKey::PersonalOrderPlaceFailedNotice).as_str())
  16399         );
  16400         assert!(view.set_buyer_workspace_notice(BuyerWorkspaceNotice::OrderCoordinationFailed));
  16401         assert_eq!(
  16402             view.buyer_workspace_notice.as_deref(),
  16403             Some(app_text(AppTextKey::PersonalOrderCoordinationFailedNotice).as_str())
  16404         );
  16405         assert!(view.clear_buyer_workspace_notice());
  16406         assert_eq!(view.buyer_workspace_notice, None);
  16407 
  16408         let _ = fs::remove_dir_all(home_dir);
  16409     }
  16410 
  16411     #[test]
  16412     fn buyer_order_place_failure_uses_typed_visible_notice() {
  16413         let (mut view, _, home_dir) = test_home_view("buyer_notice");
  16414 
  16415         assert!(view.place_personal_order_update());
  16416         assert_eq!(
  16417             view.buyer_workspace_notice.as_deref(),
  16418             Some(app_text(AppTextKey::PersonalOrderPlaceFailedNotice).as_str())
  16419         );
  16420 
  16421         let _ = fs::remove_dir_all(home_dir);
  16422     }
  16423 
  16424     #[test]
  16425     fn buyer_order_coordination_failure_forces_redraw_when_notice_is_unchanged() {
  16426         assert!(buyer_order_coordination_notice_forces_redraw(
  16427             BuyerWorkspaceNotice::OrderCoordinationFailed
  16428         ));
  16429         assert!(!buyer_order_coordination_notice_forces_redraw(
  16430             BuyerWorkspaceNotice::OrderPlaceFailed
  16431         ));
  16432     }
  16433 
  16434     #[test]
  16435     fn buyer_orders_retry_action_tracks_recoverable_coordination() {
  16436         let mut orders = BuyerOrdersScreenProjection::default();
  16437         assert!(!buyer_orders_retry_action_visible(&orders));
  16438 
  16439         orders.has_recoverable_coordination = true;
  16440         assert!(buyer_orders_retry_action_visible(&orders));
  16441     }
  16442 
  16443     #[test]
  16444     fn buyer_order_detail_focus_reopens_same_selected_detail() {
  16445         let order_id = OrderId::new();
  16446         let farm_id = FarmId::new();
  16447         let mut runtime = summary(
  16448             HomeRoute::Personal,
  16449             TodayAgendaProjection::default(),
  16450             FarmSetupProjection::default(),
  16451         );
  16452 
  16453         assert_eq!(
  16454             buyer_order_detail_focus_after_open(false, &runtime, order_id),
  16455             None
  16456         );
  16457 
  16458         runtime.personal_projection.orders.detail = Some(BuyerOrderDetailProjection {
  16459             order_id,
  16460             farm_id,
  16461             order_number: String::new(),
  16462             farm_display_name: String::new(),
  16463             fulfillment_summary: String::new(),
  16464             status: BuyerOrderStatus::Placed,
  16465             items: Vec::new(),
  16466             economics: TradeEconomicsProjection::default(),
  16467             workflow: TradeWorkflowProjection::from_buyer_order_status(
  16468                 order_id,
  16469                 BuyerOrderStatus::Placed,
  16470             ),
  16471             validation_receipts: Vec::new(),
  16472             order_note: None,
  16473             repeat_demand: None,
  16474         });
  16475 
  16476         assert_eq!(
  16477             buyer_order_detail_focus_after_open(false, &runtime, order_id),
  16478             Some(HomeFocusedView::BuyerOrderDetail(order_id))
  16479         );
  16480         assert_eq!(
  16481             buyer_order_detail_focus_after_open(false, &runtime, OrderId::new()),
  16482             None
  16483         );
  16484     }
  16485 
  16486     #[test]
  16487     fn farmer_order_detail_focus_reopens_same_selected_detail() {
  16488         let order_id = OrderId::new();
  16489         let farm_id = FarmId::new();
  16490         let mut runtime = summary(
  16491             HomeRoute::Today,
  16492             TodayAgendaProjection::default(),
  16493             FarmSetupProjection::default(),
  16494         );
  16495 
  16496         assert_eq!(
  16497             farmer_order_detail_focus_after_open(false, &runtime, order_id),
  16498             None
  16499         );
  16500 
  16501         runtime.orders_projection.detail = Some(OrderDetailProjection {
  16502             order_id,
  16503             farm_id,
  16504             order_number: String::new(),
  16505             customer_display_name: String::new(),
  16506             status: OrderStatus::Scheduled,
  16507             fulfillment_window_id: None,
  16508             fulfillment_window_label: None,
  16509             pickup_location_label: None,
  16510             items: Vec::new(),
  16511             economics: TradeEconomicsProjection::default(),
  16512             workflow: TradeWorkflowProjection::from_order_status(order_id, OrderStatus::Scheduled),
  16513             validation_receipts: Vec::new(),
  16514             primary_action: None,
  16515         });
  16516 
  16517         assert_eq!(
  16518             farmer_order_detail_focus_after_open(false, &runtime, order_id),
  16519             Some(HomeFocusedView::FarmerOrderDetail(order_id))
  16520         );
  16521         assert_eq!(
  16522             farmer_order_detail_focus_after_open(false, &runtime, OrderId::new()),
  16523             None
  16524         );
  16525     }
  16526 
  16527     #[test]
  16528     fn buyer_browse_refresh_failure_uses_typed_visible_notice() {
  16529         let (mut view, paths, home_dir) = test_home_view("buyer_notice");
  16530         block_shared_local_events_database(&paths);
  16531 
  16532         assert!(view.select_personal_section_update(PersonalSection::Browse));
  16533         assert_eq!(
  16534             view.buyer_workspace_notice.as_deref(),
  16535             Some(app_text(AppTextKey::PersonalMarketplaceRefreshFailedNotice).as_str())
  16536         );
  16537 
  16538         let _ = fs::remove_dir_all(home_dir);
  16539     }
  16540 
  16541     #[test]
  16542     fn buyer_search_refresh_failure_uses_typed_visible_notice() {
  16543         let (mut view, paths, home_dir) = test_home_view("buyer_notice");
  16544         block_shared_local_events_database(&paths);
  16545 
  16546         assert!(view.set_personal_search_query_update("eggs"));
  16547         assert_eq!(
  16548             view.buyer_workspace_notice.as_deref(),
  16549             Some(app_text(AppTextKey::PersonalMarketplaceRefreshFailedNotice).as_str())
  16550         );
  16551 
  16552         let _ = fs::remove_dir_all(home_dir);
  16553     }
  16554 
  16555     #[test]
  16556     fn buyer_detail_open_failure_uses_typed_visible_notice() {
  16557         let (mut view, paths, home_dir) = test_home_view("buyer_notice");
  16558         block_shared_local_events_database(&paths);
  16559 
  16560         assert!(
  16561             view.open_personal_product_detail_update(PersonalSection::Browse, ProductId::new())
  16562         );
  16563         assert_eq!(
  16564             view.buyer_workspace_notice.as_deref(),
  16565             Some(app_text(AppTextKey::PersonalDetailOpenFailedNotice).as_str())
  16566         );
  16567 
  16568         let _ = fs::remove_dir_all(home_dir);
  16569     }
  16570 
  16571     fn sample_pack_day_bundle(bundle_directory: &PathBuf) -> PackDayExportBundle {
  16572         PackDayExportBundle {
  16573             fulfillment_window_id: FulfillmentWindowId::new(),
  16574             export_instance_id: radroots_app_view::PackDayExportInstanceId::new(),
  16575             generated_at_utc: "2026-04-23T15:00:00Z".to_owned(),
  16576             bundle_directory: bundle_directory.to_string_lossy().into_owned(),
  16577             artifacts: vec![
  16578                 PackDayExportArtifact {
  16579                     kind: PackDayExportArtifactKind::PackSheet,
  16580                     relative_path: "pack_sheet.txt".to_owned(),
  16581                 },
  16582                 PackDayExportArtifact {
  16583                     kind: PackDayExportArtifactKind::PickupRoster,
  16584                     relative_path: "pickup_roster.txt".to_owned(),
  16585                 },
  16586                 PackDayExportArtifact {
  16587                     kind: PackDayExportArtifactKind::CustomerLabels,
  16588                     relative_path: "customer_labels.txt".to_owned(),
  16589                 },
  16590             ],
  16591         }
  16592     }
  16593 
  16594     #[test]
  16595     fn farm_setup_onboarding_uses_frozen_copy_and_primary_action() {
  16596         let spec = farm_setup_onboarding_card_spec(HomeRoute::FarmSetupOnboarding).unwrap();
  16597 
  16598         assert_eq!(spec.title_key, AppTextKey::HomeFarmSetupOnboardingTitle);
  16599         assert_eq!(spec.body_key, AppTextKey::HomeFarmSetupOnboardingBody);
  16600         assert_eq!(
  16601             spec.action_key,
  16602             Some(AppTextKey::HomeFarmSetupOnboardingAction)
  16603         );
  16604     }
  16605 
  16606     #[test]
  16607     fn farm_setup_form_route_keeps_onboarding_copy_without_no_farm_empty_state() {
  16608         let spec = farm_setup_onboarding_card_spec(HomeRoute::FarmSetupForm).unwrap();
  16609 
  16610         assert_eq!(spec.title_key, AppTextKey::HomeFarmSetupOnboardingTitle);
  16611         assert_eq!(spec.body_key, AppTextKey::HomeFarmSetupOnboardingBody);
  16612         assert_eq!(spec.action_key, None);
  16613     }
  16614 
  16615     #[test]
  16616     fn settings_navigation_order_keeps_farm_between_account_and_settings() {
  16617         assert_eq!(
  16618             SETTINGS_NAVIGATION_ORDER,
  16619             &[
  16620                 SettingsPanelViewKey::Account,
  16621                 SettingsPanelViewKey::Farm,
  16622                 SettingsPanelViewKey::Settings,
  16623                 SettingsPanelViewKey::About,
  16624             ]
  16625         );
  16626     }
  16627 
  16628     #[test]
  16629     fn settings_account_display_uses_label_before_npub_fallback() {
  16630         let labeled = AccountSummary {
  16631             account_id: "account_1".to_owned(),
  16632             npub: "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq".to_owned(),
  16633             label: Some("  Farm Profile  ".to_owned()),
  16634             custody: AccountCustody::LocalManaged,
  16635         };
  16636         let unlabeled = AccountSummary {
  16637             label: None,
  16638             ..labeled.clone()
  16639         };
  16640 
  16641         assert_eq!(account_display_name(&labeled), "Farm Profile");
  16642         assert_eq!(account_display_name(&unlabeled), "npub1qqqqq...qqqqqq");
  16643     }
  16644 
  16645     #[test]
  16646     fn settings_account_npub_fallback_stays_compact() {
  16647         assert_eq!(
  16648             abbreviated_npub("npub1sxczrq2dp4jtehcm8mtemj975u5ytf2d7mc6dpuuq3rzkjzr76ls5lkheq"),
  16649             "npub1sxczr...5lkheq"
  16650         );
  16651     }
  16652 
  16653     #[test]
  16654     fn settings_inventory_sections_follow_the_frozen_farm_rules_order() {
  16655         assert_eq!(
  16656             SETTINGS_FARM_PANEL_SECTIONS,
  16657             &[
  16658                 SettingsInventorySectionSpec {
  16659                     title_key: AppTextKey::HomeFarmSetupSectionFarm,
  16660                     field_keys: &[
  16661                         AppTextKey::HomeFarmSetupFieldFarmName,
  16662                         AppTextKey::SettingsFarmFieldTimezone,
  16663                         AppTextKey::SettingsFarmFieldCurrency,
  16664                     ],
  16665                 },
  16666                 SettingsInventorySectionSpec {
  16667                     title_key: AppTextKey::SettingsPickupLocationsSectionLabel,
  16668                     field_keys: &[
  16669                         AppTextKey::SettingsPickupLocationsFieldLabel,
  16670                         AppTextKey::SettingsPickupLocationsFieldAddress,
  16671                         AppTextKey::SettingsPickupLocationsFieldDirections,
  16672                         AppTextKey::SettingsPickupLocationsFieldDefault,
  16673                     ],
  16674                 },
  16675             ]
  16676         );
  16677         assert_eq!(
  16678             SETTINGS_OPERATIONS_PANEL_SECTIONS,
  16679             &[
  16680                 SettingsInventorySectionSpec {
  16681                     title_key: AppTextKey::SettingsOperatingRulesSectionLabel,
  16682                     field_keys: &[
  16683                         AppTextKey::SettingsOperatingRulesFieldPromiseLeadTime,
  16684                         AppTextKey::SettingsOperatingRulesFieldSubstitutionPolicy,
  16685                     ],
  16686                 },
  16687                 SettingsInventorySectionSpec {
  16688                     title_key: AppTextKey::SettingsFulfillmentWindowsSectionLabel,
  16689                     field_keys: &[
  16690                         AppTextKey::SettingsFulfillmentWindowsFieldLabel,
  16691                         AppTextKey::SettingsFulfillmentWindowsFieldPickupLocation,
  16692                         AppTextKey::SettingsFulfillmentWindowsFieldStartsAt,
  16693                         AppTextKey::SettingsFulfillmentWindowsFieldEndsAt,
  16694                         AppTextKey::SettingsFulfillmentWindowsFieldOrderCutoff,
  16695                     ],
  16696                 },
  16697                 SettingsInventorySectionSpec {
  16698                     title_key: AppTextKey::SettingsBlackoutPeriodsSectionLabel,
  16699                     field_keys: &[
  16700                         AppTextKey::SettingsBlackoutPeriodsFieldLabel,
  16701                         AppTextKey::SettingsBlackoutPeriodsFieldStartsAt,
  16702                         AppTextKey::SettingsBlackoutPeriodsFieldEndsAt,
  16703                     ],
  16704                 },
  16705                 SettingsInventorySectionSpec {
  16706                     title_key: AppTextKey::SettingsReadinessSectionLabel,
  16707                     field_keys: &[
  16708                         AppTextKey::SettingsReadinessFieldMissingProfileBasics,
  16709                         AppTextKey::SettingsReadinessFieldMissingPickupLocation,
  16710                         AppTextKey::SettingsReadinessFieldMissingFulfillmentWindow,
  16711                         AppTextKey::SettingsReadinessFieldMissingOperatingRules,
  16712                         AppTextKey::SettingsReadinessFieldInvalidTimingConflicts,
  16713                     ],
  16714                 },
  16715             ]
  16716         );
  16717     }
  16718 
  16719     #[test]
  16720     fn trade_workflow_badge_keys_cover_refactored_status_axes() {
  16721         for (status, key) in [
  16722             (
  16723                 TradeAgreementStatus::Ordered,
  16724                 AppTextKey::TradeWorkflowAgreementOrdered,
  16725             ),
  16726             (
  16727                 TradeAgreementStatus::Confirmed,
  16728                 AppTextKey::TradeWorkflowAgreementConfirmed,
  16729             ),
  16730             (
  16731                 TradeAgreementStatus::Declined,
  16732                 AppTextKey::TradeWorkflowAgreementDeclined,
  16733             ),
  16734             (
  16735                 TradeAgreementStatus::Cancelled,
  16736                 AppTextKey::TradeWorkflowAgreementCancelled,
  16737             ),
  16738             (
  16739                 TradeAgreementStatus::NeedsReview,
  16740                 AppTextKey::TradeWorkflowAgreementNeedsReview,
  16741             ),
  16742         ] {
  16743             assert_eq!(trade_agreement_status_key(status), key);
  16744             assert!(!app_text(key).is_empty());
  16745         }
  16746 
  16747         for (status, key) in [
  16748             (
  16749                 TradeRevisionStatus::None,
  16750                 AppTextKey::TradeWorkflowRevisionNone,
  16751             ),
  16752             (
  16753                 TradeRevisionStatus::ChangeProposed,
  16754                 AppTextKey::TradeWorkflowRevisionChangeProposed,
  16755             ),
  16756             (
  16757                 TradeRevisionStatus::Updated,
  16758                 AppTextKey::TradeWorkflowRevisionUpdated,
  16759             ),
  16760             (
  16761                 TradeRevisionStatus::KeptAsPlaced,
  16762                 AppTextKey::TradeWorkflowRevisionKeptAsPlaced,
  16763             ),
  16764         ] {
  16765             assert_eq!(trade_revision_status_key(status), key);
  16766             assert!(!app_text(key).is_empty());
  16767         }
  16768 
  16769         for (status, key) in [
  16770             (
  16771                 TradeInventoryStatus::Available,
  16772                 AppTextKey::TradeWorkflowInventoryAvailable,
  16773             ),
  16774             (
  16775                 TradeInventoryStatus::Reserved,
  16776                 AppTextKey::TradeWorkflowInventoryReserved,
  16777             ),
  16778             (
  16779                 TradeInventoryStatus::SoldOut,
  16780                 AppTextKey::TradeWorkflowInventorySoldOut,
  16781             ),
  16782             (
  16783                 TradeInventoryStatus::NeedsReview,
  16784                 AppTextKey::TradeWorkflowInventoryNeedsReview,
  16785             ),
  16786         ] {
  16787             assert_eq!(trade_inventory_status_key(status), key);
  16788             assert!(!app_text(key).is_empty());
  16789         }
  16790 
  16791         for (source, key) in [
  16792             (
  16793                 TradeWorkflowSource::App,
  16794                 AppTextKey::TradeWorkflowProvenanceApp,
  16795             ),
  16796             (
  16797                 TradeWorkflowSource::Cli,
  16798                 AppTextKey::TradeWorkflowProvenanceCli,
  16799             ),
  16800             (
  16801                 TradeWorkflowSource::Relay,
  16802                 AppTextKey::TradeWorkflowProvenanceRelay,
  16803             ),
  16804             (
  16805                 TradeWorkflowSource::LocalEvents,
  16806                 AppTextKey::TradeWorkflowProvenanceLocalEvents,
  16807             ),
  16808             (
  16809                 TradeWorkflowSource::Unknown,
  16810                 AppTextKey::TradeWorkflowProvenanceUnknown,
  16811             ),
  16812         ] {
  16813             assert_eq!(trade_workflow_source_key(source), key);
  16814             assert!(!app_text(key).is_empty());
  16815         }
  16816     }
  16817 
  16818     #[test]
  16819     fn today_route_has_no_setup_onboarding_card() {
  16820         assert!(farm_setup_onboarding_card_spec(HomeRoute::Today).is_none());
  16821     }
  16822 
  16823     #[test]
  16824     fn home_window_launch_frame_and_minimum_size_are_split() {
  16825         assert_eq!(home_window_launch_size_px(), (1284.0, 795.0));
  16826         assert_eq!(home_window_minimum_size_px(), (1080.0, 720.0));
  16827     }
  16828 
  16829     #[test]
  16830     fn startup_home_surface_tracks_the_shared_logged_out_phase_contract() {
  16831         let continue_prompt = summary_with_logged_out_phase(LoggedOutStartupPhase::ContinuePrompt);
  16832         let identity_choice = summary_with_logged_out_phase(LoggedOutStartupPhase::IdentityChoice);
  16833         let generate_key_starting =
  16834             summary_with_logged_out_phase(LoggedOutStartupPhase::GenerateKeyStarting);
  16835         let signer_entry = summary_with_logged_out_phase(LoggedOutStartupPhase::SignerEntry);
  16836 
  16837         assert_eq!(
  16838             startup_home_surface(&continue_prompt),
  16839             StartupHomeSurface::ContinuePrompt
  16840         );
  16841         assert_eq!(
  16842             startup_home_surface(&identity_choice),
  16843             StartupHomeSurface::IdentityChoice
  16844         );
  16845         assert_eq!(
  16846             startup_home_surface(&generate_key_starting),
  16847             StartupHomeSurface::GenerateKeyStarting
  16848         );
  16849         assert_eq!(
  16850             startup_home_surface(&signer_entry),
  16851             StartupHomeSurface::SignerEntry
  16852         );
  16853     }
  16854 
  16855     #[test]
  16856     fn startup_home_surface_uses_issue_card_when_setup_is_unavailable() {
  16857         let blocked = DesktopAppRuntimeSummary {
  16858             startup_gate: AppStartupGate::Blocked,
  16859             startup_issue: Some("runtime unavailable".to_owned()),
  16860             ..summary_with_logged_out_phase(LoggedOutStartupPhase::IdentityChoice)
  16861         };
  16862 
  16863         assert_eq!(
  16864             startup_home_surface(&blocked),
  16865             StartupHomeSurface::IssueCard
  16866         );
  16867         assert_eq!(
  16868             startup_home_surface(&summary(
  16869                 HomeRoute::Personal,
  16870                 TodayAgendaProjection::default(),
  16871                 FarmSetupProjection::default(),
  16872             )),
  16873             StartupHomeSurface::IssueCard
  16874         );
  16875     }
  16876 
  16877     #[test]
  16878     fn home_stage_uses_buyer_workspace_when_guest_enters_marketplace() {
  16879         let mut guest_marketplace = summary(
  16880             HomeRoute::SetupRequired,
  16881             TodayAgendaProjection::default(),
  16882             FarmSetupProjection::default(),
  16883         );
  16884         guest_marketplace.startup_gate = AppStartupGate::SetupRequired;
  16885         guest_marketplace.shell_projection = AppShellProjection::new(
  16886             ActiveSurface::Personal,
  16887             ShellSection::Personal(PersonalSection::Browse),
  16888         );
  16889 
  16890         assert_eq!(home_stage(&guest_marketplace), HomeStage::BuyerWorkspace);
  16891     }
  16892 
  16893     #[test]
  16894     fn shell_header_active_mode_tracks_account_as_a_peer_selector() {
  16895         let mut runtime = summary(
  16896             HomeRoute::Personal,
  16897             TodayAgendaProjection::default(),
  16898             FarmSetupProjection::default(),
  16899         );
  16900         runtime.shell_projection = AppShellProjection::new(
  16901             ActiveSurface::Personal,
  16902             ShellSection::Personal(PersonalSection::Browse),
  16903         );
  16904         assert_eq!(
  16905             shell_header_active_mode(&runtime),
  16906             ShellHeaderActiveMode::Marketplace
  16907         );
  16908 
  16909         runtime.shell_projection = AppShellProjection::new(
  16910             ActiveSurface::Farmer,
  16911             ShellSection::Farmer(FarmerSection::Today),
  16912         );
  16913         assert_eq!(
  16914             shell_header_active_mode(&runtime),
  16915             ShellHeaderActiveMode::Farm
  16916         );
  16917 
  16918         runtime.shell_projection =
  16919             AppShellProjection::new(ActiveSurface::Personal, ShellSection::Account);
  16920         assert_eq!(
  16921             shell_header_active_mode(&runtime),
  16922             ShellHeaderActiveMode::Account
  16923         );
  16924 
  16925         runtime.shell_projection =
  16926             AppShellProjection::new(ActiveSurface::Farmer, ShellSection::Account);
  16927         assert_eq!(
  16928             shell_header_active_mode(&runtime),
  16929             ShellHeaderActiveMode::Account
  16930         );
  16931     }
  16932 
  16933     #[test]
  16934     fn home_auto_focus_target_tracks_startup_surface_contract() {
  16935         assert_eq!(
  16936             home_auto_focus_target(
  16937                 &summary_with_logged_out_phase(LoggedOutStartupPhase::ContinuePrompt),
  16938                 HomeAutoFocusState::default(),
  16939             ),
  16940             Some(HomeAutoFocusTarget::StartupContinue)
  16941         );
  16942         assert_eq!(
  16943             home_auto_focus_target(
  16944                 &summary_with_logged_out_phase(LoggedOutStartupPhase::IdentityChoice),
  16945                 HomeAutoFocusState::default(),
  16946             ),
  16947             Some(HomeAutoFocusTarget::StartupGenerateKey)
  16948         );
  16949         assert_eq!(
  16950             home_auto_focus_target(
  16951                 &summary_with_logged_out_phase(LoggedOutStartupPhase::SignerEntry),
  16952                 HomeAutoFocusState {
  16953                     has_startup_signer_input: true,
  16954                     startup_signer_input_is_editable: true,
  16955                     ..HomeAutoFocusState::default()
  16956                 },
  16957             ),
  16958             Some(HomeAutoFocusTarget::StartupSignerInput)
  16959         );
  16960         assert_eq!(
  16961             home_auto_focus_target(
  16962                 &summary_with_logged_out_phase(LoggedOutStartupPhase::SignerEntry),
  16963                 HomeAutoFocusState {
  16964                     has_startup_signer_input: true,
  16965                     startup_signer_input_is_editable: false,
  16966                     ..HomeAutoFocusState::default()
  16967                 },
  16968             ),
  16969             Some(HomeAutoFocusTarget::StartupSignerBack)
  16970         );
  16971     }
  16972 
  16973     #[test]
  16974     fn home_auto_focus_target_tracks_buyer_surface_contract() {
  16975         let mut buyer_search = summary(
  16976             HomeRoute::Personal,
  16977             TodayAgendaProjection::default(),
  16978             FarmSetupProjection::default(),
  16979         );
  16980         buyer_search.startup_gate = AppStartupGate::Personal;
  16981         buyer_search.shell_projection = AppShellProjection::new(
  16982             ActiveSurface::Personal,
  16983             ShellSection::Personal(PersonalSection::Search),
  16984         );
  16985         assert_eq!(
  16986             home_auto_focus_target(
  16987                 &buyer_search,
  16988                 HomeAutoFocusState {
  16989                     has_personal_search_input: true,
  16990                     ..HomeAutoFocusState::default()
  16991                 },
  16992             ),
  16993             Some(HomeAutoFocusTarget::BuyerSearchInput)
  16994         );
  16995 
  16996         let mut buyer_cart_order_review = buyer_search.clone();
  16997         buyer_cart_order_review.shell_projection = AppShellProjection::new(
  16998             ActiveSurface::Personal,
  16999             ShellSection::Personal(PersonalSection::Cart),
  17000         );
  17001         assert_eq!(
  17002             home_auto_focus_target(
  17003                 &buyer_cart_order_review,
  17004                 HomeAutoFocusState {
  17005                     has_buyer_order_review_form: true,
  17006                     ..HomeAutoFocusState::default()
  17007                 },
  17008             ),
  17009             Some(HomeAutoFocusTarget::BuyerOrderReviewNameInput)
  17010         );
  17011 
  17012         let order_id = OrderId::new();
  17013         let farm_id = FarmId::new();
  17014         let mut buyer_orders = buyer_search.clone();
  17015         buyer_orders.shell_projection = AppShellProjection::new(
  17016             ActiveSurface::Personal,
  17017             ShellSection::Personal(PersonalSection::Orders),
  17018         );
  17019         buyer_orders.personal_projection.orders.list.rows = vec![BuyerOrdersListRow {
  17020             order_id,
  17021             farm_id,
  17022             order_number: String::new(),
  17023             farm_display_name: String::new(),
  17024             fulfillment_summary: String::new(),
  17025             status: BuyerOrderStatus::Placed,
  17026             workflow: TradeWorkflowProjection::from_buyer_order_status(
  17027                 order_id,
  17028                 BuyerOrderStatus::Placed,
  17029             ),
  17030             repeat_demand: None,
  17031         }];
  17032         buyer_orders.personal_projection.orders.detail = Some(BuyerOrderDetailProjection {
  17033             order_id,
  17034             farm_id,
  17035             order_number: String::new(),
  17036             farm_display_name: String::new(),
  17037             fulfillment_summary: String::new(),
  17038             status: BuyerOrderStatus::Placed,
  17039             items: Vec::new(),
  17040             economics: TradeEconomicsProjection::default(),
  17041             workflow: TradeWorkflowProjection::from_buyer_order_status(
  17042                 order_id,
  17043                 BuyerOrderStatus::Placed,
  17044             ),
  17045             validation_receipts: Vec::new(),
  17046             order_note: None,
  17047             repeat_demand: Some(RepeatDemandHandoffProjection {
  17048                 order_id,
  17049                 farm_id,
  17050                 eligibility: RepeatDemandEligibility::Eligible,
  17051                 available_item_count: 1,
  17052                 unavailable_item_count: 0,
  17053             }),
  17054         });
  17055         assert_eq!(
  17056             home_auto_focus_target(&buyer_orders, HomeAutoFocusState::default()),
  17057             Some(HomeAutoFocusTarget::BuyerOrderRepeatDemand)
  17058         );
  17059     }
  17060 
  17061     #[test]
  17062     fn home_auto_focus_target_tracks_farmer_surface_contract() {
  17063         let mut onboarding = summary(
  17064             HomeRoute::FarmSetupOnboarding,
  17065             TodayAgendaProjection::default(),
  17066             FarmSetupProjection::default(),
  17067         );
  17068         onboarding.startup_gate = AppStartupGate::Farmer;
  17069         onboarding.shell_projection = AppShellProjection::new(
  17070             ActiveSurface::Farmer,
  17071             ShellSection::Farmer(FarmerSection::Today),
  17072         );
  17073         assert_eq!(
  17074             home_auto_focus_target(&onboarding, HomeAutoFocusState::default()),
  17075             Some(HomeAutoFocusTarget::FarmerSetupStart)
  17076         );
  17077 
  17078         let farm_id = FarmId::new();
  17079         let incomplete_farm = FarmSummary {
  17080             farm_id,
  17081             display_name: String::new(),
  17082             readiness: FarmReadiness::Incomplete,
  17083         };
  17084         let incomplete_today = summary(
  17085             HomeRoute::Today,
  17086             TodayAgendaProjection {
  17087                 farm: Some(incomplete_farm.clone()),
  17088                 setup_checklist: vec![TodaySetupTask {
  17089                     kind: TodaySetupTaskKind::AddFulfillmentWindow,
  17090                     is_complete: false,
  17091                 }],
  17092                 ..TodayAgendaProjection::default()
  17093             },
  17094             FarmSetupProjection::new(
  17095                 FarmSetupDraft::new(String::new(), String::new(), [FarmOrderMethod::Pickup]),
  17096                 Some(incomplete_farm),
  17097             ),
  17098         );
  17099         assert_eq!(
  17100             home_auto_focus_target(&incomplete_today, HomeAutoFocusState::default()),
  17101             Some(HomeAutoFocusTarget::FarmerSetupContinue)
  17102         );
  17103 
  17104         let saved_farm = FarmSummary {
  17105             farm_id: FarmId::new(),
  17106             display_name: String::new(),
  17107             readiness: FarmReadiness::Ready,
  17108         };
  17109         let mut products = summary(
  17110             HomeRoute::Today,
  17111             TodayAgendaProjection::default(),
  17112             FarmSetupProjection::from_saved_farm(saved_farm.clone()),
  17113         );
  17114         products.startup_gate = AppStartupGate::Farmer;
  17115         products.shell_projection = AppShellProjection::new(
  17116             ActiveSurface::Farmer,
  17117             ShellSection::Farmer(FarmerSection::Products),
  17118         );
  17119         assert_eq!(
  17120             home_auto_focus_target(
  17121                 &products,
  17122                 HomeAutoFocusState {
  17123                     has_products_search_input: true,
  17124                     ..HomeAutoFocusState::default()
  17125                 },
  17126             ),
  17127             Some(HomeAutoFocusTarget::ProductsSearchInput)
  17128         );
  17129         assert_eq!(
  17130             home_auto_focus_target(
  17131                 &products,
  17132                 HomeAutoFocusState {
  17133                     has_product_editor_form: true,
  17134                     ..HomeAutoFocusState::default()
  17135                 },
  17136             ),
  17137             Some(HomeAutoFocusTarget::ProductEditorTitleInput)
  17138         );
  17139 
  17140         let mut orders = summary(
  17141             HomeRoute::Today,
  17142             TodayAgendaProjection::default(),
  17143             FarmSetupProjection::from_saved_farm(saved_farm),
  17144         );
  17145         orders.startup_gate = AppStartupGate::Farmer;
  17146         orders.shell_projection = AppShellProjection::new(
  17147             ActiveSurface::Farmer,
  17148             ShellSection::Farmer(FarmerSection::Orders),
  17149         );
  17150         let farmer_order_id = OrderId::new();
  17151         let farmer_order_farm_id = FarmId::new();
  17152         orders.orders_projection.list.rows = vec![OrdersListRow {
  17153             order_id: farmer_order_id,
  17154             farm_id: farmer_order_farm_id,
  17155             fulfillment_window_id: None,
  17156             order_number: String::new(),
  17157             customer_display_name: String::new(),
  17158             fulfillment_window_label: None,
  17159             pickup_location_label: None,
  17160             status: OrderStatus::Scheduled,
  17161             workflow: TradeWorkflowProjection::from_order_status(
  17162                 farmer_order_id,
  17163                 OrderStatus::Scheduled,
  17164             ),
  17165             primary_action: None,
  17166         }];
  17167         orders.orders_projection.detail = Some(OrderDetailProjection {
  17168             order_id: farmer_order_id,
  17169             farm_id: farmer_order_farm_id,
  17170             order_number: String::new(),
  17171             customer_display_name: String::new(),
  17172             status: OrderStatus::Scheduled,
  17173             fulfillment_window_id: None,
  17174             fulfillment_window_label: None,
  17175             pickup_location_label: None,
  17176             items: Vec::new(),
  17177             economics: TradeEconomicsProjection::default(),
  17178             workflow: TradeWorkflowProjection::from_order_status(
  17179                 farmer_order_id,
  17180                 OrderStatus::Scheduled,
  17181             ),
  17182             validation_receipts: Vec::new(),
  17183             primary_action: None,
  17184         });
  17185         assert_eq!(
  17186             home_auto_focus_target(&orders, HomeAutoFocusState::default()),
  17187             Some(HomeAutoFocusTarget::OrdersRowOpenFirst)
  17188         );
  17189     }
  17190 
  17191     #[test]
  17192     fn settings_auto_focus_target_tracks_panel_contract() {
  17193         let runtime = summary(
  17194             HomeRoute::Today,
  17195             TodayAgendaProjection::default(),
  17196             FarmSetupProjection::default(),
  17197         );
  17198         assert_eq!(
  17199             settings_auto_focus_target(SettingsPanelViewKey::Account, None, &runtime),
  17200             Some(SettingsAutoFocusTarget::AccountAdd)
  17201         );
  17202         assert_eq!(
  17203             settings_auto_focus_target(SettingsPanelViewKey::Farm, None, &runtime),
  17204             Some(SettingsAutoFocusTarget::Navigation(
  17205                 SettingsPanelViewKey::Farm
  17206             ))
  17207         );
  17208         assert_eq!(
  17209             settings_auto_focus_target(SettingsPanelViewKey::Settings, None, &runtime),
  17210             Some(SettingsAutoFocusTarget::Navigation(
  17211                 SettingsPanelViewKey::Settings
  17212             ))
  17213         );
  17214 
  17215         let mut about_enabled = runtime.clone();
  17216         about_enabled.sync_status.account_id = Some("guest".to_owned());
  17217         assert_eq!(
  17218             settings_auto_focus_target(SettingsPanelViewKey::About, None, &about_enabled),
  17219             Some(SettingsAutoFocusTarget::AboutRefresh)
  17220         );
  17221         assert_eq!(
  17222             settings_auto_focus_target(SettingsPanelViewKey::About, None, &runtime),
  17223             Some(SettingsAutoFocusTarget::Navigation(
  17224                 SettingsPanelViewKey::About
  17225             ))
  17226         );
  17227     }
  17228 
  17229     #[test]
  17230     fn settings_general_rows_read_runtime_projection_values() {
  17231         let mut runtime = summary(
  17232             HomeRoute::Today,
  17233             TodayAgendaProjection::default(),
  17234             FarmSetupProjection::default(),
  17235         );
  17236         runtime
  17237             .shell_projection
  17238             .settings
  17239             .general
  17240             .allow_relay_connections = false;
  17241         runtime.shell_projection.settings.general.use_media_servers = true;
  17242         runtime.shell_projection.settings.general.use_nip05 = false;
  17243         runtime.shell_projection.settings.general.launch_at_login = true;
  17244 
  17245         let state = settings_preferences_general_row_state(&runtime);
  17246 
  17247         assert!(!state.allow_relay_connections);
  17248         assert!(state.use_media_servers);
  17249         assert!(!state.use_nip05);
  17250         assert!(state.launch_at_login);
  17251     }
  17252 
  17253     #[test]
  17254     fn farmer_home_farm_state_distinguishes_no_farm_incomplete_and_configured() {
  17255         let farm_id = FarmId::new();
  17256         let incomplete_farm = FarmSummary {
  17257             farm_id,
  17258             display_name: String::new(),
  17259             readiness: FarmReadiness::Incomplete,
  17260         };
  17261         let configured_farm = FarmSummary {
  17262             farm_id: FarmId::new(),
  17263             display_name: String::new(),
  17264             readiness: FarmReadiness::Ready,
  17265         };
  17266 
  17267         assert_eq!(
  17268             farmer_home_farm_state(&summary(
  17269                 HomeRoute::FarmSetupOnboarding,
  17270                 TodayAgendaProjection::default(),
  17271                 FarmSetupProjection::default(),
  17272             )),
  17273             FarmerHomeFarmState::NoFarm
  17274         );
  17275         assert_eq!(
  17276             farmer_home_farm_state(&summary(
  17277                 HomeRoute::Today,
  17278                 TodayAgendaProjection {
  17279                     farm: Some(incomplete_farm.clone()),
  17280                     setup_checklist: vec![TodaySetupTask {
  17281                         kind: TodaySetupTaskKind::AddFulfillmentWindow,
  17282                         is_complete: false,
  17283                     }],
  17284                     ..TodayAgendaProjection::default()
  17285                 },
  17286                 FarmSetupProjection::new(
  17287                     FarmSetupDraft::new(String::new(), String::new(), [FarmOrderMethod::Pickup]),
  17288                     Some(incomplete_farm),
  17289                 ),
  17290             )),
  17291             FarmerHomeFarmState::IncompleteFarm
  17292         );
  17293         assert_eq!(
  17294             farmer_home_farm_state(&summary(
  17295                 HomeRoute::Today,
  17296                 TodayAgendaProjection {
  17297                     farm: Some(configured_farm.clone()),
  17298                     ..TodayAgendaProjection::default()
  17299                 },
  17300                 FarmSetupProjection::new(
  17301                     FarmSetupDraft::new(
  17302                         String::new(),
  17303                         String::new(),
  17304                         [FarmOrderMethod::Pickup, FarmOrderMethod::Delivery],
  17305                     ),
  17306                     Some(configured_farm),
  17307                 ),
  17308             )),
  17309             FarmerHomeFarmState::ConfiguredFarm
  17310         );
  17311     }
  17312 
  17313     #[test]
  17314     fn pack_day_availability_tracks_the_contextual_window_projection() {
  17315         let farm_id = FarmId::new();
  17316         let mut runtime = summary(
  17317             HomeRoute::Today,
  17318             TodayAgendaProjection::default(),
  17319             FarmSetupProjection::from_saved_farm(FarmSummary {
  17320                 farm_id,
  17321                 display_name: String::new(),
  17322                 readiness: FarmReadiness::Ready,
  17323             }),
  17324         );
  17325 
  17326         assert!(!farmer_pack_day_available(&runtime));
  17327         assert_eq!(
  17328             home_content_scroll_id(FarmerSection::PackDay),
  17329             "home-pack-day-scroll"
  17330         );
  17331 
  17332         runtime.pack_day_projection.projection = PackDayProjection {
  17333             fulfillment_window: Some(FulfillmentWindowSummary {
  17334                 fulfillment_window_id: FulfillmentWindowId::new(),
  17335                 farm_id,
  17336                 starts_at: String::new(),
  17337                 ends_at: String::new(),
  17338             }),
  17339             reminders: Default::default(),
  17340             totals_by_product: Vec::new(),
  17341             pack_list: Vec::new(),
  17342             pickup_roster: Vec::new(),
  17343         };
  17344 
  17345         assert!(farmer_pack_day_available(&runtime));
  17346     }
  17347 
  17348     #[test]
  17349     fn pack_day_export_action_enabled_requires_a_window_and_exportable_rows() {
  17350         let farm_id = FarmId::new();
  17351         let fulfillment_window_id = FulfillmentWindowId::new();
  17352         let mut runtime = summary(
  17353             HomeRoute::Today,
  17354             TodayAgendaProjection::default(),
  17355             FarmSetupProjection::default(),
  17356         );
  17357 
  17358         assert!(!pack_day_export_action_enabled(&runtime));
  17359         assert_eq!(
  17360             pack_day_export_status_presentation(&runtime),
  17361             PackDayExportStatusPresentation {
  17362                 indicator_color: APP_UI_THEME.components.app_status_indicator.offline,
  17363                 title_key: AppTextKey::PackDayExportUnavailableTitle,
  17364                 body_key: AppTextKey::PackDayExportUnavailableBody,
  17365             }
  17366         );
  17367 
  17368         runtime.pack_day_projection.projection = PackDayProjection {
  17369             fulfillment_window: Some(FulfillmentWindowSummary {
  17370                 fulfillment_window_id,
  17371                 farm_id,
  17372                 starts_at: String::new(),
  17373                 ends_at: String::new(),
  17374             }),
  17375             reminders: Default::default(),
  17376             totals_by_product: Vec::new(),
  17377             pack_list: Vec::new(),
  17378             pickup_roster: Vec::new(),
  17379         };
  17380 
  17381         assert!(!pack_day_export_action_enabled(&runtime));
  17382 
  17383         runtime.pack_day_projection.projection.totals_by_product = vec![PackDayProductTotalRow {
  17384             title: "Salad mix".to_owned(),
  17385             quantity_display: "2 bags".to_owned(),
  17386         }];
  17387 
  17388         assert!(pack_day_export_action_enabled(&runtime));
  17389         assert_eq!(
  17390             pack_day_export_status_presentation(&runtime),
  17391             PackDayExportStatusPresentation {
  17392                 indicator_color: APP_UI_THEME.components.app_status_indicator.online,
  17393                 title_key: AppTextKey::PackDayExportReadyTitle,
  17394                 body_key: AppTextKey::PackDayExportReadyBody,
  17395             }
  17396         );
  17397 
  17398         runtime.pack_day_projection.export = PackDayExportProjection::running(
  17399             radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id),
  17400         );
  17401         assert!(!pack_day_export_action_enabled(&runtime));
  17402         assert_eq!(
  17403             pack_day_export_action_label_key(&runtime.pack_day_projection.export),
  17404             AppTextKey::PackDayExportActionRunning
  17405         );
  17406         assert_eq!(
  17407             pack_day_export_status_presentation(&runtime),
  17408             PackDayExportStatusPresentation {
  17409                 indicator_color: APP_UI_THEME.foundation.text.accent,
  17410                 title_key: AppTextKey::PackDayExportRunningTitle,
  17411                 body_key: AppTextKey::PackDayExportRunningBody,
  17412             }
  17413         );
  17414     }
  17415 
  17416     #[test]
  17417     fn pack_day_export_detail_rows_surface_bundle_and_failure_details() {
  17418         let fulfillment_window_id = FulfillmentWindowId::new();
  17419         let bundle = PackDayExportBundle {
  17420             fulfillment_window_id,
  17421             export_instance_id: radroots_app_view::PackDayExportInstanceId::new(),
  17422             generated_at_utc: "2026-04-23T15:00:00Z".to_owned(),
  17423             bundle_directory: "exports/pack_day/window-1/20260423T150000Z".to_owned(),
  17424             artifacts: vec![
  17425                 PackDayExportArtifact {
  17426                     kind: PackDayExportArtifactKind::PackSheet,
  17427                     relative_path: "pack_sheet.txt".to_owned(),
  17428                 },
  17429                 PackDayExportArtifact {
  17430                     kind: PackDayExportArtifactKind::PickupRoster,
  17431                     relative_path: "pickup_roster.txt".to_owned(),
  17432                 },
  17433                 PackDayExportArtifact {
  17434                     kind: PackDayExportArtifactKind::CustomerLabels,
  17435                     relative_path: "customer_labels.txt".to_owned(),
  17436                 },
  17437             ],
  17438         };
  17439         let request =
  17440             radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id);
  17441 
  17442         let rows = pack_day_export_detail_rows(&PackDayExportProjection::succeeded(
  17443             request.clone(),
  17444             bundle.clone(),
  17445         ));
  17446         assert_eq!(rows.len(), 2);
  17447         assert_eq!(
  17448             rows[0],
  17449             LabelValueRow::new(
  17450                 app_text(AppTextKey::PackDayExportFolderLabel),
  17451                 "exports/pack_day/window-1/20260423T150000Z"
  17452             )
  17453         );
  17454         assert_eq!(
  17455             rows[1],
  17456             LabelValueRow::new(
  17457                 app_text(AppTextKey::PackDayExportFilesLabel),
  17458                 "pack_sheet.txt, pickup_roster.txt, customer_labels.txt"
  17459             )
  17460         );
  17461         assert_eq!(
  17462             pack_day_export_artifact_names(&bundle),
  17463             "pack_sheet.txt, pickup_roster.txt, customer_labels.txt"
  17464         );
  17465 
  17466         let failed = PackDayExportProjection::failed(request, "disk unavailable");
  17467         assert_eq!(
  17468             pack_day_export_detail_rows(&failed),
  17469             vec![LabelValueRow::new(
  17470                 app_text(AppTextKey::PackDayExportErrorLabel),
  17471                 "disk unavailable"
  17472             )]
  17473         );
  17474         assert_eq!(
  17475             pack_day_export_status_presentation(&DesktopAppRuntimeSummary {
  17476                 pack_day_projection: radroots_app_state::PackDayScreenProjection {
  17477                     export: failed,
  17478                     ..Default::default()
  17479                 },
  17480                 ..summary(
  17481                     HomeRoute::Today,
  17482                     TodayAgendaProjection::default(),
  17483                     FarmSetupProjection::default(),
  17484                 )
  17485             }),
  17486             PackDayExportStatusPresentation {
  17487                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  17488                 title_key: AppTextKey::PackDayExportFailedTitle,
  17489                 body_key: AppTextKey::PackDayExportFailedBody,
  17490             }
  17491         );
  17492     }
  17493 
  17494     #[test]
  17495     fn pack_day_host_handoff_actions_only_surface_after_a_successful_export() {
  17496         let temp_dir = TestDirectory::new();
  17497         write_artifact(temp_dir.path(), "pack_sheet.txt");
  17498         write_artifact(temp_dir.path(), "pickup_roster.txt");
  17499         write_artifact(temp_dir.path(), "customer_labels.txt");
  17500         let bundle = sample_pack_day_bundle(temp_dir.path());
  17501         let fulfillment_window_id = bundle.fulfillment_window_id;
  17502         let mut runtime = summary(
  17503             HomeRoute::Today,
  17504             TodayAgendaProjection::default(),
  17505             FarmSetupProjection::default(),
  17506         );
  17507 
  17508         assert!(pack_day_host_handoff_action_presentations(&runtime).is_empty());
  17509 
  17510         runtime.pack_day_projection.export = PackDayExportProjection::succeeded(
  17511             radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id),
  17512             bundle,
  17513         );
  17514 
  17515         assert_eq!(
  17516             pack_day_host_handoff_action_presentations(&runtime),
  17517             vec![
  17518                 PackDayHostHandoffActionPresentation {
  17519                     kind: PackDayHostHandoffKind::RevealBundle,
  17520                     label_key: AppTextKey::PackDayHostHandoffRevealAction,
  17521                     enabled: true,
  17522                 },
  17523                 PackDayHostHandoffActionPresentation {
  17524                     kind: PackDayHostHandoffKind::OpenPackSheet,
  17525                     label_key: AppTextKey::PackDayHostHandoffOpenPackSheetAction,
  17526                     enabled: true,
  17527                 },
  17528                 PackDayHostHandoffActionPresentation {
  17529                     kind: PackDayHostHandoffKind::OpenPickupRoster,
  17530                     label_key: AppTextKey::PackDayHostHandoffOpenPickupRosterAction,
  17531                     enabled: true,
  17532                 },
  17533                 PackDayHostHandoffActionPresentation {
  17534                     kind: PackDayHostHandoffKind::OpenCustomerLabels,
  17535                     label_key: AppTextKey::PackDayHostHandoffOpenCustomerLabelsAction,
  17536                     enabled: true,
  17537                 },
  17538             ]
  17539         );
  17540     }
  17541 
  17542     #[test]
  17543     fn pack_day_host_handoff_running_and_failure_postures_track_the_active_request() {
  17544         let temp_dir = TestDirectory::new();
  17545         write_artifact(temp_dir.path(), "pack_sheet.txt");
  17546         write_artifact(temp_dir.path(), "pickup_roster.txt");
  17547         write_artifact(temp_dir.path(), "customer_labels.txt");
  17548         let bundle = sample_pack_day_bundle(temp_dir.path());
  17549         let fulfillment_window_id = bundle.fulfillment_window_id;
  17550         let export_request =
  17551             radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id);
  17552         let reveal_request =
  17553             PackDayHostHandoffRequest::for_bundle(PackDayHostHandoffKind::RevealBundle, &bundle);
  17554         let open_request = PackDayHostHandoffRequest::for_bundle(
  17555             PackDayHostHandoffKind::OpenCustomerLabels,
  17556             &bundle,
  17557         );
  17558         let mut runtime = summary(
  17559             HomeRoute::Today,
  17560             TodayAgendaProjection::default(),
  17561             FarmSetupProjection::default(),
  17562         );
  17563         runtime.pack_day_projection.export =
  17564             PackDayExportProjection::succeeded(export_request, bundle);
  17565 
  17566         runtime.pack_day_projection.host_handoff =
  17567             PackDayHostHandoffProjection::running(reveal_request);
  17568         assert_eq!(
  17569             pack_day_host_handoff_action_presentations(&runtime),
  17570             vec![
  17571                 PackDayHostHandoffActionPresentation {
  17572                     kind: PackDayHostHandoffKind::RevealBundle,
  17573                     label_key: AppTextKey::PackDayHostHandoffRevealActionRunning,
  17574                     enabled: false,
  17575                 },
  17576                 PackDayHostHandoffActionPresentation {
  17577                     kind: PackDayHostHandoffKind::OpenPackSheet,
  17578                     label_key: AppTextKey::PackDayHostHandoffOpenPackSheetAction,
  17579                     enabled: false,
  17580                 },
  17581                 PackDayHostHandoffActionPresentation {
  17582                     kind: PackDayHostHandoffKind::OpenPickupRoster,
  17583                     label_key: AppTextKey::PackDayHostHandoffOpenPickupRosterAction,
  17584                     enabled: false,
  17585                 },
  17586                 PackDayHostHandoffActionPresentation {
  17587                     kind: PackDayHostHandoffKind::OpenCustomerLabels,
  17588                     label_key: AppTextKey::PackDayHostHandoffOpenCustomerLabelsAction,
  17589                     enabled: false,
  17590                 },
  17591             ]
  17592         );
  17593         assert_eq!(
  17594             pack_day_host_handoff_status_presentation(&runtime),
  17595             Some(PackDayHostHandoffStatusPresentation {
  17596                 indicator_color: APP_UI_THEME.foundation.text.accent,
  17597                 title_key: AppTextKey::PackDayHostHandoffRevealRunningTitle,
  17598             })
  17599         );
  17600 
  17601         runtime.pack_day_projection.host_handoff =
  17602             PackDayHostHandoffProjection::failed(open_request, "finder unavailable");
  17603         assert_eq!(
  17604             runtime.pack_day_projection.host_handoff.status,
  17605             PackDayHostHandoffStatus::Failed
  17606         );
  17607         assert_eq!(
  17608             pack_day_host_handoff_status_presentation(&runtime),
  17609             Some(PackDayHostHandoffStatusPresentation {
  17610                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  17611                 title_key: AppTextKey::PackDayHostHandoffOpenCustomerLabelsFailedTitle,
  17612             })
  17613         );
  17614     }
  17615 
  17616     #[test]
  17617     fn pack_day_host_handoff_actions_disable_missing_artifacts_even_after_export_success() {
  17618         let temp_dir = TestDirectory::new();
  17619         write_artifact(temp_dir.path(), "pack_sheet.txt");
  17620         let bundle = sample_pack_day_bundle(temp_dir.path());
  17621         let fulfillment_window_id = bundle.fulfillment_window_id;
  17622         let mut runtime = summary(
  17623             HomeRoute::Today,
  17624             TodayAgendaProjection::default(),
  17625             FarmSetupProjection::default(),
  17626         );
  17627 
  17628         runtime.pack_day_projection.export = PackDayExportProjection::succeeded(
  17629             radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id),
  17630             bundle,
  17631         );
  17632 
  17633         assert_eq!(
  17634             pack_day_host_handoff_action_presentations(&runtime),
  17635             vec![
  17636                 PackDayHostHandoffActionPresentation {
  17637                     kind: PackDayHostHandoffKind::RevealBundle,
  17638                     label_key: AppTextKey::PackDayHostHandoffRevealAction,
  17639                     enabled: true,
  17640                 },
  17641                 PackDayHostHandoffActionPresentation {
  17642                     kind: PackDayHostHandoffKind::OpenPackSheet,
  17643                     label_key: AppTextKey::PackDayHostHandoffOpenPackSheetAction,
  17644                     enabled: true,
  17645                 },
  17646                 PackDayHostHandoffActionPresentation {
  17647                     kind: PackDayHostHandoffKind::OpenPickupRoster,
  17648                     label_key: AppTextKey::PackDayHostHandoffOpenPickupRosterAction,
  17649                     enabled: false,
  17650                 },
  17651                 PackDayHostHandoffActionPresentation {
  17652                     kind: PackDayHostHandoffKind::OpenCustomerLabels,
  17653                     label_key: AppTextKey::PackDayHostHandoffOpenCustomerLabelsAction,
  17654                     enabled: false,
  17655                 },
  17656             ]
  17657         );
  17658     }
  17659 
  17660     #[test]
  17661     fn pack_day_print_actions_only_surface_after_a_successful_export() {
  17662         let temp_dir = TestDirectory::new();
  17663         write_artifact(temp_dir.path(), "pack_sheet.txt");
  17664         write_artifact(temp_dir.path(), "pickup_roster.txt");
  17665         write_artifact(temp_dir.path(), "customer_labels.txt");
  17666         let bundle = sample_pack_day_bundle(temp_dir.path());
  17667         let fulfillment_window_id = bundle.fulfillment_window_id;
  17668         let mut runtime = summary(
  17669             HomeRoute::Today,
  17670             TodayAgendaProjection::default(),
  17671             FarmSetupProjection::default(),
  17672         );
  17673 
  17674         assert!(pack_day_print_action_presentations(&runtime).is_empty());
  17675 
  17676         runtime.pack_day_projection.export = PackDayExportProjection::succeeded(
  17677             radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id),
  17678             bundle,
  17679         );
  17680 
  17681         assert_eq!(
  17682             pack_day_print_action_presentations(&runtime),
  17683             vec![
  17684                 PackDayPrintActionPresentation {
  17685                     kind: PackDayPrintKind::PrintPackSheet,
  17686                     label_key: AppTextKey::PackDayPrintPackSheetAction,
  17687                     enabled: true,
  17688                 },
  17689                 PackDayPrintActionPresentation {
  17690                     kind: PackDayPrintKind::PrintPickupRoster,
  17691                     label_key: AppTextKey::PackDayPrintPickupRosterAction,
  17692                     enabled: true,
  17693                 },
  17694                 PackDayPrintActionPresentation {
  17695                     kind: PackDayPrintKind::PrintCustomerLabels,
  17696                     label_key: AppTextKey::PackDayPrintCustomerLabelsAction,
  17697                     enabled: true,
  17698                 },
  17699             ]
  17700         );
  17701     }
  17702 
  17703     #[test]
  17704     fn pack_day_batch_workflow_action_only_surfaces_after_a_successful_export() {
  17705         let temp_dir = TestDirectory::new();
  17706         write_artifact(temp_dir.path(), "pack_sheet.txt");
  17707         write_artifact(temp_dir.path(), "pickup_roster.txt");
  17708         write_artifact(temp_dir.path(), "customer_labels.txt");
  17709         let bundle = sample_pack_day_bundle(temp_dir.path());
  17710         let fulfillment_window_id = bundle.fulfillment_window_id;
  17711         let mut runtime = summary(
  17712             HomeRoute::Today,
  17713             TodayAgendaProjection::default(),
  17714             FarmSetupProjection::default(),
  17715         );
  17716 
  17717         assert_eq!(pack_day_batch_print_action_presentation(&runtime), None);
  17718 
  17719         runtime.pack_day_projection.export = PackDayExportProjection::succeeded(
  17720             radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id),
  17721             bundle,
  17722         );
  17723 
  17724         assert_eq!(
  17725             pack_day_batch_print_action_presentation(&runtime),
  17726             Some(PackDayBatchPrintActionPresentation {
  17727                 label_key: AppTextKey::PackDayBatchPrintAction,
  17728                 enabled: true,
  17729             })
  17730         );
  17731     }
  17732 
  17733     #[test]
  17734     fn pack_day_batch_print_running_disables_conflicting_pack_day_actions() {
  17735         let temp_dir = TestDirectory::new();
  17736         write_artifact(temp_dir.path(), "pack_sheet.txt");
  17737         write_artifact(temp_dir.path(), "pickup_roster.txt");
  17738         write_artifact(temp_dir.path(), "customer_labels.txt");
  17739         let bundle = sample_pack_day_bundle(temp_dir.path());
  17740         let fulfillment_window_id = bundle.fulfillment_window_id;
  17741         let export_request =
  17742             radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id);
  17743         let batch_request = PackDayBatchPrintRequest::for_bundle(&bundle);
  17744         let mut runtime = summary(
  17745             HomeRoute::Today,
  17746             TodayAgendaProjection::default(),
  17747             FarmSetupProjection::default(),
  17748         );
  17749         runtime.pack_day_projection.export =
  17750             PackDayExportProjection::succeeded(export_request, bundle);
  17751         runtime.pack_day_projection.batch_print =
  17752             PackDayBatchPrintProjection::running(batch_request);
  17753 
  17754         assert_eq!(
  17755             pack_day_batch_print_action_presentation(&runtime),
  17756             Some(PackDayBatchPrintActionPresentation {
  17757                 label_key: AppTextKey::PackDayBatchPrintActionRunning,
  17758                 enabled: false,
  17759             })
  17760         );
  17761         assert!(
  17762             pack_day_print_action_presentations(&runtime)
  17763                 .into_iter()
  17764                 .all(|action| !action.enabled)
  17765         );
  17766         assert!(
  17767             pack_day_host_handoff_action_presentations(&runtime)
  17768                 .into_iter()
  17769                 .all(|action| !action.enabled)
  17770         );
  17771     }
  17772 
  17773     #[test]
  17774     fn pack_day_batch_print_status_tracks_outcomes_and_failed_artifacts() {
  17775         let temp_dir = TestDirectory::new();
  17776         write_artifact(temp_dir.path(), "pack_sheet.txt");
  17777         write_artifact(temp_dir.path(), "pickup_roster.txt");
  17778         write_artifact(temp_dir.path(), "customer_labels.txt");
  17779         let bundle = sample_pack_day_bundle(temp_dir.path());
  17780         let fulfillment_window_id = bundle.fulfillment_window_id;
  17781         let export_request =
  17782             radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id);
  17783         let batch_request = PackDayBatchPrintRequest::for_bundle(&bundle);
  17784         let mut runtime = summary(
  17785             HomeRoute::Today,
  17786             TodayAgendaProjection::default(),
  17787             FarmSetupProjection::default(),
  17788         );
  17789         runtime.pack_day_projection.export =
  17790             PackDayExportProjection::succeeded(export_request, bundle);
  17791 
  17792         runtime.pack_day_projection.batch_print =
  17793             PackDayBatchPrintProjection::running(batch_request.clone());
  17794         assert_eq!(
  17795             pack_day_batch_print_status_presentation(&runtime),
  17796             Some(PackDayBatchPrintStatusPresentation {
  17797                 indicator_color: APP_UI_THEME.foundation.text.accent,
  17798                 title_key: AppTextKey::PackDayBatchPrintQueuedTitle,
  17799             })
  17800         );
  17801 
  17802         runtime.pack_day_projection.batch_print =
  17803             PackDayBatchPrintProjection::succeeded(batch_request.clone());
  17804         assert_eq!(
  17805             pack_day_batch_print_status_presentation(&runtime),
  17806             Some(PackDayBatchPrintStatusPresentation {
  17807                 indicator_color: APP_UI_THEME.components.app_status_indicator.online,
  17808                 title_key: AppTextKey::PackDayBatchPrintSucceededTitle,
  17809             })
  17810         );
  17811 
  17812         runtime.pack_day_projection.batch_print = PackDayBatchPrintProjection::failed(
  17813             batch_request.clone(),
  17814             Some(PackDayBatchPrintArtifact::from_print_kind(
  17815                 PackDayPrintKind::PrintPickupRoster,
  17816             )),
  17817             PackDayBatchPrintFailureKind::QueueExit,
  17818         );
  17819         assert_eq!(
  17820             pack_day_batch_print_status_presentation(&runtime),
  17821             Some(PackDayBatchPrintStatusPresentation {
  17822                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  17823                 title_key: AppTextKey::PackDayPrintPickupRosterFailedTitle,
  17824             })
  17825         );
  17826 
  17827         runtime.pack_day_projection.batch_print = PackDayBatchPrintProjection::failed(
  17828             batch_request.clone(),
  17829             None,
  17830             PackDayBatchPrintFailureKind::Preflight,
  17831         );
  17832         assert_eq!(
  17833             pack_day_batch_print_status_presentation(&runtime),
  17834             Some(PackDayBatchPrintStatusPresentation {
  17835                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  17836                 title_key: AppTextKey::PackDayBatchPrintFailedPreflightTitle,
  17837             })
  17838         );
  17839 
  17840         runtime.pack_day_projection.batch_print = PackDayBatchPrintProjection::failed(
  17841             batch_request,
  17842             Some(PackDayBatchPrintArtifact::from_print_kind(
  17843                 PackDayPrintKind::PrintCustomerLabels,
  17844             )),
  17845             PackDayBatchPrintFailureKind::CustomerLabelsAvery5160Overflow,
  17846         );
  17847         assert_eq!(
  17848             pack_day_batch_print_status_presentation(&runtime),
  17849             Some(PackDayBatchPrintStatusPresentation {
  17850                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  17851                 title_key: AppTextKey::PackDayBatchPrintCustomerLabelsAvery5160OverflowFailedTitle,
  17852             })
  17853         );
  17854     }
  17855 
  17856     #[test]
  17857     fn pack_day_print_running_and_failure_postures_track_the_active_request() {
  17858         let temp_dir = TestDirectory::new();
  17859         write_artifact(temp_dir.path(), "pack_sheet.txt");
  17860         write_artifact(temp_dir.path(), "pickup_roster.txt");
  17861         write_artifact(temp_dir.path(), "customer_labels.txt");
  17862         let bundle = sample_pack_day_bundle(temp_dir.path());
  17863         let fulfillment_window_id = bundle.fulfillment_window_id;
  17864         let export_request =
  17865             radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id);
  17866         let print_request =
  17867             PackDayPrintRequest::for_bundle(PackDayPrintKind::PrintPackSheet, &bundle);
  17868         let failed_request =
  17869             PackDayPrintRequest::for_bundle(PackDayPrintKind::PrintCustomerLabels, &bundle);
  17870         let mut runtime = summary(
  17871             HomeRoute::Today,
  17872             TodayAgendaProjection::default(),
  17873             FarmSetupProjection::default(),
  17874         );
  17875         runtime.pack_day_projection.export =
  17876             PackDayExportProjection::succeeded(export_request, bundle.clone());
  17877 
  17878         runtime.pack_day_projection.print = PackDayPrintProjection::running(print_request);
  17879         assert_eq!(
  17880             pack_day_print_action_presentations(&runtime),
  17881             vec![
  17882                 PackDayPrintActionPresentation {
  17883                     kind: PackDayPrintKind::PrintPackSheet,
  17884                     label_key: AppTextKey::PackDayPrintPackSheetActionRunning,
  17885                     enabled: false,
  17886                 },
  17887                 PackDayPrintActionPresentation {
  17888                     kind: PackDayPrintKind::PrintPickupRoster,
  17889                     label_key: AppTextKey::PackDayPrintPickupRosterAction,
  17890                     enabled: false,
  17891                 },
  17892                 PackDayPrintActionPresentation {
  17893                     kind: PackDayPrintKind::PrintCustomerLabels,
  17894                     label_key: AppTextKey::PackDayPrintCustomerLabelsAction,
  17895                     enabled: false,
  17896                 },
  17897             ]
  17898         );
  17899         assert_eq!(
  17900             pack_day_print_status_presentation(&runtime),
  17901             Some(PackDayPrintStatusPresentation {
  17902                 indicator_color: APP_UI_THEME.foundation.text.accent,
  17903                 title_key: AppTextKey::PackDayPrintPackSheetQueuedTitle,
  17904             })
  17905         );
  17906         assert_eq!(
  17907             pack_day_host_handoff_action_presentations(&runtime),
  17908             vec![
  17909                 PackDayHostHandoffActionPresentation {
  17910                     kind: PackDayHostHandoffKind::RevealBundle,
  17911                     label_key: AppTextKey::PackDayHostHandoffRevealAction,
  17912                     enabled: false,
  17913                 },
  17914                 PackDayHostHandoffActionPresentation {
  17915                     kind: PackDayHostHandoffKind::OpenPackSheet,
  17916                     label_key: AppTextKey::PackDayHostHandoffOpenPackSheetAction,
  17917                     enabled: false,
  17918                 },
  17919                 PackDayHostHandoffActionPresentation {
  17920                     kind: PackDayHostHandoffKind::OpenPickupRoster,
  17921                     label_key: AppTextKey::PackDayHostHandoffOpenPickupRosterAction,
  17922                     enabled: false,
  17923                 },
  17924                 PackDayHostHandoffActionPresentation {
  17925                     kind: PackDayHostHandoffKind::OpenCustomerLabels,
  17926                     label_key: AppTextKey::PackDayHostHandoffOpenCustomerLabelsAction,
  17927                     enabled: false,
  17928                 },
  17929             ]
  17930         );
  17931 
  17932         runtime.pack_day_projection.print = PackDayPrintProjection::failed(failed_request);
  17933         assert_eq!(
  17934             runtime.pack_day_projection.print.status,
  17935             PackDayPrintStatus::Failed
  17936         );
  17937         assert_eq!(
  17938             pack_day_print_status_presentation(&runtime),
  17939             Some(PackDayPrintStatusPresentation {
  17940                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  17941                 title_key: AppTextKey::PackDayPrintCustomerLabelsFailedTitle,
  17942             })
  17943         );
  17944 
  17945         let overflow_request =
  17946             PackDayPrintRequest::for_bundle(PackDayPrintKind::PrintCustomerLabels, &bundle);
  17947         runtime.pack_day_projection.print = PackDayPrintProjection::failed_with_kind(
  17948             overflow_request,
  17949             PackDayPrintFailureKind::CustomerLabelsAvery5160Overflow,
  17950         );
  17951         assert_eq!(
  17952             pack_day_print_status_presentation(&runtime),
  17953             Some(PackDayPrintStatusPresentation {
  17954                 indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
  17955                 title_key: AppTextKey::PackDayPrintCustomerLabelsAvery5160OverflowFailedTitle,
  17956             })
  17957         );
  17958     }
  17959 
  17960     #[test]
  17961     fn pack_day_print_actions_disable_missing_artifacts_and_host_handoff_runs() {
  17962         let temp_dir = TestDirectory::new();
  17963         write_artifact(temp_dir.path(), "pack_sheet.txt");
  17964         let bundle = sample_pack_day_bundle(temp_dir.path());
  17965         let fulfillment_window_id = bundle.fulfillment_window_id;
  17966         let export_request =
  17967             radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id);
  17968         let host_handoff_request =
  17969             PackDayHostHandoffRequest::for_bundle(PackDayHostHandoffKind::RevealBundle, &bundle);
  17970         let mut runtime = summary(
  17971             HomeRoute::Today,
  17972             TodayAgendaProjection::default(),
  17973             FarmSetupProjection::default(),
  17974         );
  17975 
  17976         runtime.pack_day_projection.export =
  17977             PackDayExportProjection::succeeded(export_request, bundle.clone());
  17978         assert_eq!(
  17979             pack_day_print_action_presentations(&runtime),
  17980             vec![
  17981                 PackDayPrintActionPresentation {
  17982                     kind: PackDayPrintKind::PrintPackSheet,
  17983                     label_key: AppTextKey::PackDayPrintPackSheetAction,
  17984                     enabled: true,
  17985                 },
  17986                 PackDayPrintActionPresentation {
  17987                     kind: PackDayPrintKind::PrintPickupRoster,
  17988                     label_key: AppTextKey::PackDayPrintPickupRosterAction,
  17989                     enabled: false,
  17990                 },
  17991                 PackDayPrintActionPresentation {
  17992                     kind: PackDayPrintKind::PrintCustomerLabels,
  17993                     label_key: AppTextKey::PackDayPrintCustomerLabelsAction,
  17994                     enabled: false,
  17995                 },
  17996             ]
  17997         );
  17998 
  17999         runtime.pack_day_projection.host_handoff =
  18000             PackDayHostHandoffProjection::running(host_handoff_request);
  18001         assert_eq!(
  18002             pack_day_print_action_presentations(&runtime),
  18003             vec![
  18004                 PackDayPrintActionPresentation {
  18005                     kind: PackDayPrintKind::PrintPackSheet,
  18006                     label_key: AppTextKey::PackDayPrintPackSheetAction,
  18007                     enabled: false,
  18008                 },
  18009                 PackDayPrintActionPresentation {
  18010                     kind: PackDayPrintKind::PrintPickupRoster,
  18011                     label_key: AppTextKey::PackDayPrintPickupRosterAction,
  18012                     enabled: false,
  18013                 },
  18014                 PackDayPrintActionPresentation {
  18015                     kind: PackDayPrintKind::PrintCustomerLabels,
  18016                     label_key: AppTextKey::PackDayPrintCustomerLabelsAction,
  18017                     enabled: false,
  18018                 },
  18019             ]
  18020         );
  18021     }
  18022 
  18023     #[test]
  18024     fn sidebar_navigation_keeps_destinations_stable() {
  18025         assert_eq!(
  18026             home_sidebar_navigation_sections(FarmerSection::Today, true, false),
  18027             vec![
  18028                 FarmerSection::Today,
  18029                 FarmerSection::Products,
  18030                 FarmerSection::Orders,
  18031             ]
  18032         );
  18033         assert_eq!(
  18034             home_sidebar_navigation_sections(FarmerSection::Products, true, false),
  18035             vec![
  18036                 FarmerSection::Today,
  18037                 FarmerSection::Products,
  18038                 FarmerSection::Orders,
  18039             ]
  18040         );
  18041         assert_eq!(
  18042             home_sidebar_navigation_sections(FarmerSection::Orders, true, false),
  18043             vec![
  18044                 FarmerSection::Today,
  18045                 FarmerSection::Products,
  18046                 FarmerSection::Orders,
  18047             ]
  18048         );
  18049         assert_eq!(
  18050             home_sidebar_navigation_sections(FarmerSection::PackDay, true, true),
  18051             vec![
  18052                 FarmerSection::Today,
  18053                 FarmerSection::Products,
  18054                 FarmerSection::Orders,
  18055                 FarmerSection::PackDay,
  18056             ]
  18057         );
  18058     }
  18059 
  18060     #[test]
  18061     fn saved_farm_falls_back_to_local_projection_when_today_is_empty() {
  18062         let saved_farm = FarmSummary {
  18063             farm_id: FarmId::new(),
  18064             display_name: String::new(),
  18065             readiness: FarmReadiness::Ready,
  18066         };
  18067         let runtime = summary(
  18068             HomeRoute::Today,
  18069             TodayAgendaProjection::default(),
  18070             FarmSetupProjection::new(
  18071                 FarmSetupDraft::new(String::new(), String::new(), [FarmOrderMethod::Shipping]),
  18072                 Some(saved_farm.clone()),
  18073             ),
  18074         );
  18075 
  18076         assert_eq!(home_saved_farm(&runtime), Some(&saved_farm));
  18077     }
  18078 
  18079     #[test]
  18080     fn product_editor_price_parser_handles_blank_whole_and_decimal_inputs() {
  18081         assert_eq!(parse_product_editor_price_input(""), Some(None));
  18082         assert_eq!(parse_product_editor_price_input("6"), Some(Some(600)));
  18083         assert_eq!(parse_product_editor_price_input("6.5"), Some(Some(650)));
  18084         assert_eq!(parse_product_editor_price_input("6.50"), Some(Some(650)));
  18085         assert_eq!(parse_product_editor_price_input("6."), None);
  18086         assert_eq!(parse_product_editor_price_input("6.500"), None);
  18087         assert_eq!(parse_product_editor_price_input("abc"), None);
  18088     }
  18089 
  18090     #[test]
  18091     fn product_editor_stock_parser_accepts_blank_or_whole_numbers_only() {
  18092         assert_eq!(parse_optional_product_editor_stock_input(""), Some(None));
  18093         assert_eq!(
  18094             parse_optional_product_editor_stock_input("14"),
  18095             Some(Some(14))
  18096         );
  18097         assert_eq!(parse_optional_product_editor_stock_input("14.5"), None);
  18098         assert_eq!(parse_optional_product_editor_stock_input("abc"), None);
  18099     }
  18100 
  18101     #[test]
  18102     fn blank_product_titles_fall_back_to_the_untitled_copy() {
  18103         assert_eq!(
  18104             product_display_title(""),
  18105             app_text(AppTextKey::ProductsUntitledDraft)
  18106         );
  18107         assert_eq!(
  18108             product_display_title("  "),
  18109             app_text(AppTextKey::ProductsUntitledDraft)
  18110         );
  18111         assert_eq!(product_display_title("Salad mix"), "Salad mix");
  18112     }
  18113 
  18114     #[test]
  18115     fn startup_signer_preview_summary_surfaces_parsed_signer_details() {
  18116         let preview = startup_signer_preview_summary(
  18117             "bunker://466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27?relay=wss%3A%2F%2Frelay.radroots.example",
  18118         )
  18119         .expect("preview");
  18120 
  18121         assert_eq!(
  18122             preview.source_label,
  18123             app_text(AppTextKey::HomeSetupSignerSourceValueBunkerUri)
  18124         );
  18125         assert!(preview.signer_npub.starts_with("npub1"));
  18126         assert_eq!(preview.relays_label, "wss://relay.radroots.example");
  18127         assert_eq!(
  18128             preview.permissions_label,
  18129             format!(
  18130                 "{}, {}",
  18131                 app_text(AppTextKey::HomeSetupSignerPermissionSignEventKind1),
  18132                 app_text(AppTextKey::HomeSetupSignerPermissionSwitchRelays)
  18133             )
  18134         );
  18135     }
  18136 
  18137     #[test]
  18138     fn startup_signer_status_prefers_auth_challenge_until_approval_is_complete() {
  18139         let pending_session = fixture_pending_session();
  18140 
  18141         assert_eq!(
  18142             startup_signer_status_spec(&StartupSignerConnectState::Connecting),
  18143             Some((AppTextKey::HomeSetupSignerConnectingTitle, None))
  18144         );
  18145         assert_eq!(
  18146             startup_signer_status_spec(&StartupSignerConnectState::PendingApproval {
  18147                 pending_session: pending_session.clone(),
  18148                 auth_challenge_url: None,
  18149             }),
  18150             Some((AppTextKey::HomeSetupSignerPendingTitle, None))
  18151         );
  18152         assert_eq!(
  18153             startup_signer_status_spec(&StartupSignerConnectState::PendingApproval {
  18154                 pending_session: pending_session.clone(),
  18155                 auth_challenge_url: Some("https://auth.example/challenge".to_owned()),
  18156             }),
  18157             Some((
  18158                 AppTextKey::HomeSetupSignerAuthChallengeTitle,
  18159                 Some("https://auth.example/challenge".to_owned()),
  18160             ))
  18161         );
  18162         assert_eq!(
  18163             startup_signer_status_spec(&StartupSignerConnectState::Approved {
  18164                 pending_session,
  18165                 approved_session: RadrootsAppRemoteSignerApprovedSession {
  18166                     user_identity: fixture_identity(
  18167                         "2222222222222222222222222222222222222222222222222222222222222222",
  18168                     )
  18169                     .to_public(),
  18170                     relays: vec!["wss://relay.radroots.example".to_owned()],
  18171                     approved_permissions: Default::default(),
  18172                 },
  18173                 auth_challenge_url: None,
  18174             }),
  18175             Some((AppTextKey::HomeSetupSignerApprovedTitle, None))
  18176         );
  18177     }
  18178 
  18179     #[test]
  18180     fn startup_signer_source_input_is_editable_only_while_idle() {
  18181         let pending_session = fixture_pending_session();
  18182 
  18183         assert!(startup_signer_source_input_is_editable(
  18184             &StartupSignerConnectState::Idle
  18185         ));
  18186         assert!(!startup_signer_source_input_is_editable(
  18187             &StartupSignerConnectState::Connecting
  18188         ));
  18189         assert!(!startup_signer_source_input_is_editable(
  18190             &StartupSignerConnectState::PendingApproval {
  18191                 pending_session: pending_session.clone(),
  18192                 auth_challenge_url: None,
  18193             }
  18194         ));
  18195         assert!(!startup_signer_source_input_is_editable(
  18196             &StartupSignerConnectState::Approved {
  18197                 pending_session,
  18198                 approved_session: RadrootsAppRemoteSignerApprovedSession {
  18199                     user_identity: fixture_identity(
  18200                         "2222222222222222222222222222222222222222222222222222222222222222",
  18201                     )
  18202                     .to_public(),
  18203                     relays: vec!["wss://relay.radroots.example".to_owned()],
  18204                     approved_permissions: Default::default(),
  18205                 },
  18206                 auth_challenge_url: None,
  18207             }
  18208         ));
  18209     }
  18210 
  18211     #[test]
  18212     fn startup_signer_preview_summary_prefers_pending_session_details_once_connect_starts() {
  18213         let pending_session = fixture_pending_session();
  18214         let preview = startup_signer_preview_summary_for_connect_state(
  18215             "bunker://466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27?relay=wss%3A%2F%2Frelay.radroots.example",
  18216             &StartupSignerConnectState::PendingApproval {
  18217                 pending_session: pending_session.clone(),
  18218                 auth_challenge_url: None,
  18219             },
  18220         )
  18221         .expect("preview");
  18222 
  18223         assert_eq!(
  18224             preview.signer_npub,
  18225             pending_session.record.signer_identity.public_key_npub
  18226         );
  18227         assert_eq!(preview.relays_label, "wss://relay.radroots.example");
  18228         assert_eq!(
  18229             preview.permissions_label,
  18230             format!(
  18231                 "{}, {}",
  18232                 app_text(AppTextKey::HomeSetupSignerPermissionSignEventKind1),
  18233                 app_text(AppTextKey::HomeSetupSignerPermissionSwitchRelays)
  18234             )
  18235         );
  18236     }
  18237 
  18238     #[test]
  18239     fn startup_signer_transport_failure_notice_ignores_the_waiting_timeout_copy() {
  18240         assert!(!startup_signer_transport_failure_requires_notice(
  18241             "remote signer did not respond yet"
  18242         ));
  18243         assert!(startup_signer_transport_failure_requires_notice(
  18244             "remote signer connection failed: relay refused the request"
  18245         ));
  18246     }
  18247 
  18248     #[test]
  18249     fn startup_signer_notice_copy_maps_known_signer_failures() {
  18250         assert_eq!(
  18251             startup_notice_text("enter a bunker or discovery url to continue"),
  18252             app_text(AppTextKey::HomeSetupSignerErrorEnterSource)
  18253         );
  18254         assert_eq!(
  18255             startup_notice_text(
  18256                 "enter a bunker or discovery url from the signer; raw nostrconnect client uris are signer-side only"
  18257             ),
  18258             app_text(AppTextKey::HomeSetupSignerErrorUseSignerUri)
  18259         );
  18260         assert_eq!(
  18261             startup_notice_text("discovery url does not contain a remote signer uri"),
  18262             app_text(AppTextKey::HomeSetupSignerErrorMissingDiscoveryUri)
  18263         );
  18264         assert_eq!(
  18265             startup_notice_text("invalid discovery url: relative URL without a base"),
  18266             app_text(AppTextKey::HomeSetupSignerErrorInvalidDiscoveryUrl)
  18267         );
  18268         assert_eq!(
  18269             startup_notice_text("invalid remote signer uri: invalid public key"),
  18270             app_text(AppTextKey::HomeSetupSignerErrorInvalidRemoteSignerUri)
  18271         );
  18272         assert_eq!(
  18273             startup_notice_text("a remote signer connection is already pending approval"),
  18274             app_text(AppTextKey::HomeSetupSignerErrorPendingApprovalExists)
  18275         );
  18276         assert_eq!(
  18277             startup_notice_text("remote signer connection failed: relay refused the request"),
  18278             app_text(AppTextKey::HomeSetupSignerErrorConnectionFailed)
  18279         );
  18280         assert_eq!(
  18281             startup_notice_text("failed to add relay `{relay_url}`: {error}"),
  18282             app_text(AppTextKey::HomeSetupErrorStartupFailed)
  18283         );
  18284     }
  18285 
  18286     #[test]
  18287     fn startup_issue_copy_fails_closed_to_a_localized_summary() {
  18288         assert_eq!(
  18289             startup_issue_summary_text("runtime unavailable"),
  18290             app_text(AppTextKey::HomeSetupIssueUnavailableBody)
  18291         );
  18292         assert_eq!(
  18293             startup_issue_summary_text("desktop runtime roots require HOME for macos"),
  18294             app_text(AppTextKey::HomeSetupIssueUnavailableBody)
  18295         );
  18296     }
  18297 
  18298     #[test]
  18299     fn reminder_action_target_prefers_order_detail_before_pack_day() {
  18300         let order_id = radroots_app_view::OrderId::new();
  18301         let fulfillment_window_id = FulfillmentWindowId::new();
  18302 
  18303         assert_eq!(
  18304             reminder_action_target(&fixture_reminder(
  18305                 Some(order_id),
  18306                 Some(fulfillment_window_id),
  18307                 ReminderKind::OrderAction,
  18308                 ReminderUrgency::DueSoon,
  18309             )),
  18310             Some(ReminderActionTarget::OrderDetail(order_id))
  18311         );
  18312         assert_eq!(
  18313             reminder_action_target(&fixture_reminder(
  18314                 None,
  18315                 Some(fulfillment_window_id),
  18316                 ReminderKind::FulfillmentWindow,
  18317                 ReminderUrgency::Upcoming,
  18318             )),
  18319             Some(ReminderActionTarget::PackDay(fulfillment_window_id))
  18320         );
  18321         assert_eq!(
  18322             reminder_action_target(&fixture_reminder(
  18323                 None,
  18324                 None,
  18325                 ReminderKind::SyncImpact,
  18326                 ReminderUrgency::Blocking,
  18327             )),
  18328             None
  18329         );
  18330     }
  18331 
  18332     #[test]
  18333     fn reminder_urgency_helpers_follow_the_surface_contract() {
  18334         assert_eq!(
  18335             reminder_urgency_key(ReminderUrgency::Upcoming),
  18336             AppTextKey::ReminderUrgencyUpcoming
  18337         );
  18338         assert_eq!(
  18339             reminder_urgency_key(ReminderUrgency::DueSoon),
  18340             AppTextKey::ReminderUrgencyDueSoon
  18341         );
  18342         assert_eq!(
  18343             reminder_urgency_color(ReminderUrgency::Upcoming),
  18344             APP_UI_THEME.components.app_status_indicator.offline
  18345         );
  18346         assert_eq!(
  18347             reminder_urgency_color(ReminderUrgency::DueSoon),
  18348             APP_UI_THEME.foundation.text.accent
  18349         );
  18350         assert_eq!(
  18351             reminder_urgency_color(ReminderUrgency::Blocking),
  18352             APP_UI_THEME.components.app_status_indicator.attention
  18353         );
  18354     }
  18355 
  18356     #[test]
  18357     fn reminder_deadline_text_uses_the_typed_due_label() {
  18358         let reminder = fixture_reminder(
  18359             None,
  18360             Some(FulfillmentWindowId::new()),
  18361             ReminderKind::FulfillmentWindow,
  18362             ReminderUrgency::Upcoming,
  18363         );
  18364 
  18365         assert_eq!(
  18366             reminder_deadline_text(&reminder),
  18367             format!("{}: {}", app_text(AppTextKey::ReminderDeadlineLabel), "0")
  18368         );
  18369     }
  18370 
  18371     #[test]
  18372     fn reminder_delivery_state_key_matches_the_local_presentation_contract() {
  18373         assert_eq!(
  18374             reminder_delivery_state_key(ReminderDeliveryState::Scheduled),
  18375             AppTextKey::ReminderDeliveryStateScheduled
  18376         );
  18377         assert_eq!(
  18378             reminder_delivery_state_key(ReminderDeliveryState::Presented),
  18379             AppTextKey::ReminderDeliveryStatePresented
  18380         );
  18381         assert_eq!(
  18382             reminder_delivery_state_key(ReminderDeliveryState::Acknowledged),
  18383             AppTextKey::ReminderDeliveryStateAcknowledged
  18384         );
  18385         assert_eq!(
  18386             reminder_delivery_state_key(ReminderDeliveryState::Resolved),
  18387             AppTextKey::ReminderDeliveryStateResolved
  18388         );
  18389     }
  18390 
  18391     #[test]
  18392     fn presented_farmer_reminder_prefers_the_highest_priority_presented_item() {
  18393         let mut runtime = summary(
  18394             HomeRoute::Today,
  18395             TodayAgendaProjection::default(),
  18396             FarmSetupProjection::default(),
  18397         );
  18398         let due_soon = fixture_reminder(
  18399             None,
  18400             Some(FulfillmentWindowId::new()),
  18401             ReminderKind::FulfillmentWindow,
  18402             ReminderUrgency::DueSoon,
  18403         );
  18404         let blocking = fixture_reminder(
  18405             None,
  18406             None,
  18407             ReminderKind::SyncImpact,
  18408             ReminderUrgency::Blocking,
  18409         );
  18410 
  18411         runtime
  18412             .today_projection
  18413             .reminders
  18414             .items
  18415             .push(ReminderDeadlineProjection {
  18416                 delivery_state: ReminderDeliveryState::Presented,
  18417                 ..due_soon
  18418             });
  18419         runtime
  18420             .orders_projection
  18421             .reminders
  18422             .items
  18423             .push(ReminderDeadlineProjection {
  18424                 delivery_state: ReminderDeliveryState::Presented,
  18425                 ..blocking.clone()
  18426             });
  18427 
  18428         assert_eq!(
  18429             presented_farmer_reminder(&runtime)
  18430                 .expect("presented reminder")
  18431                 .reminder_id,
  18432             blocking.reminder_id
  18433         );
  18434     }
  18435 
  18436     #[test]
  18437     fn about_status_rows_disable_sync_without_a_selected_account() {
  18438         let rows = about_status_rows(
  18439             &summary(
  18440                 HomeRoute::SetupRequired,
  18441                 TodayAgendaProjection::default(),
  18442                 FarmSetupProjection::default(),
  18443             ),
  18444             None,
  18445         );
  18446 
  18447         assert!(rows.iter().any(|row| {
  18448             row.label == app_text(AppTextKey::MetadataSelectedAccount)
  18449                 && row.value == app_text(AppTextKey::ValueNone)
  18450         }));
  18451         assert!(rows.iter().any(|row| {
  18452             row.label == app_text(AppTextKey::MetadataSyncRunStatus)
  18453                 && row.value == app_text(AppTextKey::ValueDisabled)
  18454         }));
  18455         assert!(rows.iter().any(|row| {
  18456             row.label == app_text(AppTextKey::MetadataSyncCheckpointState)
  18457                 && row.value == app_text(AppTextKey::ValueNone)
  18458         }));
  18459         assert!(rows.iter().any(|row| {
  18460             row.label == app_text(AppTextKey::MetadataStartupIssue)
  18461                 && row.value == app_text(AppTextKey::ValueNone)
  18462         }));
  18463     }
  18464 
  18465     #[test]
  18466     fn about_conflict_review_helpers_surface_actions_and_details_truthfully() {
  18467         let blocking_conflict = DesktopAppSyncConflictSummary {
  18468             conflict_id: String::new(),
  18469             conflict: SyncConflict {
  18470                 aggregate: SyncAggregateRef::Farm(FarmId::new()),
  18471                 kind: SyncConflictKind::RevisionMismatch,
  18472                 severity: SyncConflictSeverity::Blocking,
  18473                 resolution: SyncConflictResolutionStatus::Unresolved,
  18474                 local_payload_json: String::new(),
  18475                 remote_payload_json: Some(String::new()),
  18476                 detected_at: "0".to_owned(),
  18477                 resolved_at: None,
  18478             },
  18479         };
  18480         let review_conflict = DesktopAppSyncConflictSummary {
  18481             conflict_id: String::new(),
  18482             conflict: SyncConflict {
  18483                 aggregate: SyncAggregateRef::Order(radroots_app_view::OrderId::new()),
  18484                 kind: SyncConflictKind::RemoteValidationReject,
  18485                 severity: SyncConflictSeverity::ReviewRequired,
  18486                 resolution: SyncConflictResolutionStatus::Unresolved,
  18487                 local_payload_json: String::new(),
  18488                 remote_payload_json: Some(String::new()),
  18489                 detected_at: "0".to_owned(),
  18490                 resolved_at: None,
  18491             },
  18492         };
  18493         let mut runtime = summary(
  18494             HomeRoute::Today,
  18495             TodayAgendaProjection::default(),
  18496             FarmSetupProjection::default(),
  18497         );
  18498         runtime.sync_status = DesktopAppSyncStatusSummary {
  18499             account_id: Some(app_text(AppTextKey::AppName)),
  18500             projection: AppSyncProjection {
  18501                 run_status: AppSyncRunStatus::Conflicted,
  18502                 checkpoint: SyncCheckpointStatus::never_synced(),
  18503                 conflict_status: SyncConflictStatus {
  18504                     unresolved_count: 2,
  18505                     blocking_count: 1,
  18506                 },
  18507             },
  18508             pending_write_count: 3,
  18509             conflicts: vec![blocking_conflict.clone(), review_conflict.clone()],
  18510         };
  18511 
  18512         assert_eq!(
  18513             about_conflict_review_body_key(&runtime.sync_status),
  18514             AppTextKey::SettingsAboutConflictReviewBlocking
  18515         );
  18516         assert!(!about_manual_refresh_enabled(&runtime.sync_status));
  18517 
  18518         let blocking_actions = about_conflict_action_specs(&blocking_conflict.conflict);
  18519         assert_eq!(
  18520             blocking_actions,
  18521             vec![
  18522                 (
  18523                     AppTextKey::SettingsAboutConflictAcceptLocalAction,
  18524                     SyncConflictResolutionStatus::AcceptedLocal,
  18525                 ),
  18526                 (
  18527                     AppTextKey::SettingsAboutConflictAcceptRemoteAction,
  18528                     SyncConflictResolutionStatus::AcceptedRemote,
  18529                 ),
  18530             ]
  18531         );
  18532 
  18533         let review_actions = about_conflict_action_specs(&review_conflict.conflict);
  18534         assert_eq!(
  18535             review_actions,
  18536             vec![
  18537                 (
  18538                     AppTextKey::SettingsAboutConflictAcceptLocalAction,
  18539                     SyncConflictResolutionStatus::AcceptedLocal,
  18540                 ),
  18541                 (
  18542                     AppTextKey::SettingsAboutConflictAcceptRemoteAction,
  18543                     SyncConflictResolutionStatus::AcceptedRemote,
  18544                 ),
  18545                 (
  18546                     AppTextKey::SettingsAboutConflictDismissAction,
  18547                     SyncConflictResolutionStatus::Dismissed,
  18548                 ),
  18549             ]
  18550         );
  18551 
  18552         let rows = about_conflict_detail_rows(&blocking_conflict);
  18553         assert_eq!(rows.len(), 5);
  18554         assert!(rows.iter().any(|row| {
  18555             row.label == app_text(AppTextKey::MetadataSyncConflictAggregate)
  18556                 && row.value == about_conflict_aggregate_text(&blocking_conflict.conflict)
  18557         }));
  18558         assert!(rows.iter().any(|row| {
  18559             row.label == app_text(AppTextKey::MetadataSyncConflictResolution)
  18560                 && row.value == app_text(AppTextKey::ValueSyncConflictResolutionUnresolved)
  18561         }));
  18562     }
  18563 
  18564     #[test]
  18565     fn about_status_rows_surface_ready_sdk_diagnostics() {
  18566         let sdk_status = fixture_sdk_status(AppSdkLifecycleState::Ready);
  18567         let sdk_diagnostics = DesktopAppSdkDiagnosticsSummary {
  18568             status: sdk_status.clone(),
  18569             state: DesktopAppSdkDiagnosticsState::Ready(DesktopAppSdkReadyDiagnosticsSummary {
  18570                 storage_kind: "directory".to_owned(),
  18571                 event_store_total_events: 7,
  18572                 outbox_total_events: 3,
  18573                 outbox_pending_events: 2,
  18574                 outbox_failed_terminal_events: 0,
  18575                 integrity_event_store_ok: true,
  18576                 integrity_outbox_ok: true,
  18577                 sync_source: "sdk_canonical_stores".to_owned(),
  18578                 sync_observed_at_ms: 42,
  18579                 sync_relay_target_count: 2,
  18580             }),
  18581         };
  18582         let mut runtime = summary(
  18583             HomeRoute::Today,
  18584             TodayAgendaProjection::default(),
  18585             FarmSetupProjection::default(),
  18586         );
  18587         runtime.sdk_status = Some(sdk_status);
  18588 
  18589         let rows = about_status_rows(&runtime, Some(&sdk_diagnostics));
  18590 
  18591         assert!(rows.iter().any(|row| {
  18592             row.label == app_text(AppTextKey::MetadataSdkLifecycleState)
  18593                 && row.value == app_text(AppTextKey::ValueSdkLifecycleReady)
  18594         }));
  18595         assert!(rows.iter().any(|row| {
  18596             row.label == app_text(AppTextKey::MetadataSdkDiagnosticState)
  18597                 && row.value == app_text(AppTextKey::ValueSdkDiagnosticsReady)
  18598         }));
  18599         assert!(rows.iter().any(|row| {
  18600             row.label == app_text(AppTextKey::MetadataSdkStorageKind)
  18601                 && row.value == app_text(AppTextKey::ValueSdkStorageKindDirectory)
  18602         }));
  18603         assert!(rows.iter().any(|row| {
  18604             row.label == app_text(AppTextKey::MetadataSdkOutboxPendingCount) && row.value == "2"
  18605         }));
  18606         assert!(rows.iter().any(|row| {
  18607             row.label == app_text(AppTextKey::MetadataSdkIntegrityStatus)
  18608                 && row.value == app_text(AppTextKey::ValueSdkIntegrityOk)
  18609         }));
  18610         assert!(rows.iter().any(|row| {
  18611             row.label == app_text(AppTextKey::MetadataSdkLastIssueCode)
  18612                 && row.value == app_text(AppTextKey::ValueNone)
  18613         }));
  18614     }
  18615 
  18616     #[test]
  18617     fn about_status_rows_surface_blocked_sdk_issue_metadata() {
  18618         let issue = DesktopAppSdkIssueSummary {
  18619             code: "invalid_relay_url".to_owned(),
  18620             class: "configuration".to_owned(),
  18621             retryable: false,
  18622             recovery_actions: vec!["configure_relay_targets".to_owned()],
  18623         };
  18624         let mut sdk_status = fixture_sdk_status(AppSdkLifecycleState::Degraded);
  18625         sdk_status.last_issue = Some(issue.clone());
  18626         let sdk_diagnostics = DesktopAppSdkDiagnosticsSummary {
  18627             status: sdk_status.clone(),
  18628             state: DesktopAppSdkDiagnosticsState::Blocked(issue),
  18629         };
  18630         let mut runtime = summary(
  18631             HomeRoute::Today,
  18632             TodayAgendaProjection::default(),
  18633             FarmSetupProjection::default(),
  18634         );
  18635         runtime.sdk_status = Some(sdk_status);
  18636 
  18637         let rows = about_status_rows(&runtime, Some(&sdk_diagnostics));
  18638 
  18639         assert!(rows.iter().any(|row| {
  18640             row.label == app_text(AppTextKey::MetadataSdkLifecycleState)
  18641                 && row.value == app_text(AppTextKey::ValueSdkLifecycleDegraded)
  18642         }));
  18643         assert!(rows.iter().any(|row| {
  18644             row.label == app_text(AppTextKey::MetadataSdkDiagnosticState)
  18645                 && row.value == app_text(AppTextKey::ValueSdkDiagnosticsBlocked)
  18646         }));
  18647         assert!(rows.iter().any(|row| {
  18648             row.label == app_text(AppTextKey::MetadataSdkLastIssueCode)
  18649                 && row.value == "invalid_relay_url"
  18650         }));
  18651         assert!(rows.iter().any(|row| {
  18652             row.label == app_text(AppTextKey::MetadataSdkIssueRetryable)
  18653                 && row.value == app_text(AppTextKey::ValueNo)
  18654         }));
  18655         assert!(rows.iter().any(|row| {
  18656             row.label == app_text(AppTextKey::MetadataSdkRecoveryAction)
  18657                 && row.value == app_text(AppTextKey::ValueSdkRecoveryConfigureRelayTargets)
  18658         }));
  18659     }
  18660 
  18661     #[test]
  18662     fn about_runtime_rows_append_paths_schema_and_shell_section() {
  18663         let mut runtime = summary(
  18664             HomeRoute::Today,
  18665             TodayAgendaProjection::default(),
  18666             FarmSetupProjection::default(),
  18667         );
  18668         let data_root = PathBuf::from("/tmp/radroots/data/apps/app");
  18669         let logs_root = PathBuf::from("/tmp/radroots/logs/apps/app");
  18670         let database_path = data_root.join("app.sqlite3");
  18671         runtime.shell_projection.selected_section =
  18672             ShellSection::Settings(SettingsPanelViewKey::About);
  18673         runtime.runtime_metadata = DesktopAppRuntimeMetadataSummary {
  18674             data_root: Some(data_root.clone()),
  18675             logs_root: Some(logs_root),
  18676             database_path: Some(database_path),
  18677             database_schema_version: Some(7),
  18678             ..DesktopAppRuntimeMetadataSummary::default()
  18679         };
  18680         runtime.sdk_status = Some(fixture_sdk_status(AppSdkLifecycleState::Ready));
  18681 
  18682         let rows = about_runtime_rows(&runtime);
  18683 
  18684         assert!(rows.iter().any(|row| {
  18685             row.label == app_text(AppTextKey::MetadataDataRoot)
  18686                 && row.value == data_root.display().to_string()
  18687         }));
  18688         assert!(rows.iter().any(|row| {
  18689             row.label == app_text(AppTextKey::MetadataDatabaseSchemaVersion)
  18690                 && row.value == 7.to_string()
  18691         }));
  18692         assert!(rows.iter().any(|row| {
  18693             row.label == app_text(AppTextKey::MetadataShellSection)
  18694                 && row.value == ShellSection::Settings(SettingsPanelViewKey::About).storage_key()
  18695         }));
  18696         assert!(rows.iter().any(|row| {
  18697             row.label == app_text(AppTextKey::MetadataSdkStorageRoot)
  18698                 && row.value == "/tmp/radroots/data/apps/app/sdk"
  18699         }));
  18700         assert!(rows.iter().any(|row| {
  18701             row.label == app_text(AppTextKey::MetadataSdkRelayUrlPolicy)
  18702                 && row.value == app_text(AppTextKey::ValueSdkRelayPolicyLocalhost)
  18703         }));
  18704     }
  18705 
  18706     fn summary(
  18707         home_route: HomeRoute,
  18708         today_projection: TodayAgendaProjection,
  18709         farm_setup_projection: FarmSetupProjection,
  18710     ) -> DesktopAppRuntimeSummary {
  18711         let farm_readiness_projection = match farm_setup_projection.saved_farm.as_ref() {
  18712             Some(saved_farm)
  18713                 if saved_farm.readiness == FarmReadiness::Ready
  18714                     && !today_projection.needs_setup() =>
  18715             {
  18716                 FarmWorkspaceReadinessProjection {
  18717                     has_saved_farm: true,
  18718                     status: FarmWorkspaceStatus::Ready,
  18719                     ..FarmWorkspaceReadinessProjection::default()
  18720                 }
  18721             }
  18722             Some(_) => FarmWorkspaceReadinessProjection {
  18723                 has_saved_farm: true,
  18724                 status: FarmWorkspaceStatus::SetupRequired,
  18725                 ..FarmWorkspaceReadinessProjection::default()
  18726             },
  18727             None => FarmWorkspaceReadinessProjection::default(),
  18728         };
  18729 
  18730         DesktopAppRuntimeSummary {
  18731             shell_projection: AppShellProjection::default(),
  18732             settings_account_projection: SettingsAccountProjection::default(),
  18733             startup_gate: AppStartupGate::Farmer,
  18734             logged_out_startup: LoggedOutStartupProjection::default(),
  18735             home_route,
  18736             personal_projection: Default::default(),
  18737             farm_rules_projection: Default::default(),
  18738             farm_readiness_projection,
  18739             farm_setup_projection,
  18740             today_projection,
  18741             products_projection: Default::default(),
  18742             orders_projection: Default::default(),
  18743             pack_day_projection: Default::default(),
  18744             reminder_log: Default::default(),
  18745             runtime_metadata: DesktopAppRuntimeMetadataSummary::default(),
  18746             sync_status: crate::runtime::DesktopAppSyncStatusSummary::default(),
  18747             startup_issue: None,
  18748             sdk_status: None,
  18749         }
  18750     }
  18751 
  18752     fn fixture_sdk_status(lifecycle_state: AppSdkLifecycleState) -> DesktopAppSdkStatusSummary {
  18753         let storage_root = PathBuf::from("/tmp/radroots/data/apps/app/sdk");
  18754         DesktopAppSdkStatusSummary {
  18755             lifecycle_state,
  18756             projection_lifecycle_state: AppSdkProjectionLifecycleState::Current,
  18757             projection_lifecycle_reason: None,
  18758             storage_root: storage_root.clone(),
  18759             event_store_path: Some(storage_root.join("event_store.sqlite")),
  18760             outbox_path: Some(storage_root.join("outbox.sqlite")),
  18761             relay_target_count: 2,
  18762             relay_url_policy: AppSdkRelayUrlPolicy::Localhost,
  18763             last_issue: None,
  18764         }
  18765     }
  18766 
  18767     fn summary_with_logged_out_phase(phase: LoggedOutStartupPhase) -> DesktopAppRuntimeSummary {
  18768         DesktopAppRuntimeSummary {
  18769             startup_gate: AppStartupGate::SetupRequired,
  18770             home_route: HomeRoute::SetupRequired,
  18771             logged_out_startup: LoggedOutStartupProjection {
  18772                 phase,
  18773                 ..LoggedOutStartupProjection::default()
  18774             },
  18775             ..summary(
  18776                 HomeRoute::SetupRequired,
  18777                 TodayAgendaProjection::default(),
  18778                 FarmSetupProjection::default(),
  18779             )
  18780         }
  18781     }
  18782 
  18783     fn fixture_identity(secret_key_hex: &str) -> RadrootsIdentity {
  18784         RadrootsIdentity::from_secret_key_str(secret_key_hex).expect("identity")
  18785     }
  18786 
  18787     fn fixture_pending_session() -> RadrootsAppRemoteSignerPendingSession {
  18788         let signer_identity =
  18789             fixture_identity("1111111111111111111111111111111111111111111111111111111111111111");
  18790         let client_identity =
  18791             fixture_identity("3333333333333333333333333333333333333333333333333333333333333333");
  18792 
  18793         RadrootsAppRemoteSignerPendingSession {
  18794             record: RadrootsAppRemoteSignerSessionRecord::pending(
  18795                 client_identity.to_public(),
  18796                 signer_identity.to_public(),
  18797                 vec!["wss://relay.radroots.example".to_owned()],
  18798             ),
  18799             client_secret_key_hex: client_identity.secret_key_hex(),
  18800         }
  18801     }
  18802 
  18803     fn fixture_reminder(
  18804         order_id: Option<radroots_app_view::OrderId>,
  18805         fulfillment_window_id: Option<FulfillmentWindowId>,
  18806         kind: ReminderKind,
  18807         urgency: ReminderUrgency,
  18808     ) -> ReminderDeadlineProjection {
  18809         ReminderDeadlineProjection {
  18810             reminder_id: ReminderId::new(),
  18811             farm_id: FarmId::new(),
  18812             order_id,
  18813             fulfillment_window_id,
  18814             kind,
  18815             surface: ReminderSurface::Orders,
  18816             urgency,
  18817             title: String::new(),
  18818             detail: String::new(),
  18819             deadline_at: "0".to_owned(),
  18820             action_label: None,
  18821             delivery_state: ReminderDeliveryState::Scheduled,
  18822         }
  18823     }
  18824 }