app

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

commit 9c19d6dce13dc9cf6c5f8b1e418483fbd5de5540
parent cc3f3a6922fa5c1899e7af7aa346be9925f546da
Author: triesap <tyson@radroots.org>
Date:   Fri, 17 Apr 2026 21:11:43 +0000

home: replace metadata shell with today agenda

Diffstat:
Mcrates/launchers/desktop/src/app.rs | 2+-
Mcrates/launchers/desktop/src/runtime.rs | 43+------------------------------------------
Mcrates/launchers/desktop/src/window.rs | 573++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mcrates/shared/i18n/src/keys.rs | 20++++++++++++++++++++
Mi18n/locales/en/messages.json | 20++++++++++++++++++++
5 files changed, 523 insertions(+), 135 deletions(-)

diff --git a/crates/launchers/desktop/src/app.rs b/crates/launchers/desktop/src/app.rs @@ -36,7 +36,7 @@ pub fn launch() { titlebar: Some(home_titlebar_options()), ..Default::default() }, - |_, cx| cx.new(|_| HomeView::new(snapshot.clone(), runtime.clone())), + |_, cx| cx.new(|_| HomeView::new(runtime.clone())), ) .expect("main radroots app window should open"); diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -1,7 +1,4 @@ -use std::{ - path::PathBuf, - sync::{Arc, Mutex, MutexGuard, PoisonError}, -}; +use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; use radroots_app_core::{AppRuntimePathsError, AppRuntimeRoots}; use radroots_app_models::{AppMode, SettingsSection, TodayAgendaProjection}; @@ -10,7 +7,6 @@ use radroots_app_state::{ AppShellProjection, AppStateCommand, AppStateStore, AppStateStoreError, InMemoryAppStateRepository, SettingsPreference, }; -use radroots_app_sync::{AppSyncProjection, SyncCheckpointStatus, SyncConflictStatus}; use thiserror::Error; const APP_DATABASE_FILE_NAME: &str = "app.sqlite3"; @@ -34,13 +30,8 @@ impl DesktopAppRuntime { let state = self.lock_state(); DesktopAppRuntimeSummary { - data_dir: state.data_dir.clone(), - logs_dir: state.logs_dir.clone(), - database_path: state.database_path.clone(), - sqlite_schema_version: state.sqlite_schema_version, shell_projection: state.state_store.shell_projection().clone(), today_projection: state.state_store.today_projection().clone(), - sync_projection: state.sync_projection.clone(), startup_issue: state.startup_issue.clone(), } } @@ -92,25 +83,14 @@ impl DesktopAppRuntime { #[derive(Clone, Debug)] pub struct DesktopAppRuntimeSummary { - pub data_dir: Option<PathBuf>, - pub logs_dir: Option<PathBuf>, - pub database_path: Option<PathBuf>, - pub sqlite_schema_version: Option<u32>, pub shell_projection: AppShellProjection, - #[allow(dead_code)] pub today_projection: TodayAgendaProjection, - pub sync_projection: AppSyncProjection, pub startup_issue: Option<String>, } #[derive(Debug)] struct DesktopAppRuntimeState { - data_dir: Option<PathBuf>, - logs_dir: Option<PathBuf>, - database_path: Option<PathBuf>, - sqlite_schema_version: Option<u32>, state_store: AppStateStore<InMemoryAppStateRepository>, - sync_projection: AppSyncProjection, startup_issue: Option<String>, } @@ -121,36 +101,21 @@ impl DesktopAppRuntimeState { let sqlite_store = AppSqliteStore::open(DatabaseTarget::Path(database_path.clone()))?; let mut state_store = AppStateStore::load(InMemoryAppStateRepository::default())?; let today_projection = sqlite_store.load_today_agenda(None)?; - let sync_projection = AppSyncProjection { - checkpoint: SyncCheckpointStatus::never_synced(), - conflict_status: SyncConflictStatus::clear(), - ..AppSyncProjection::default() - }; let _ = state_store.apply_in_memory(AppStateCommand::replace_today_agenda(today_projection)); Ok(Self { - data_dir: Some(roots.data), - logs_dir: Some(roots.logs), - database_path: Some(database_path), - sqlite_schema_version: Some(sqlite_store.schema_version()?), state_store, - sync_projection, startup_issue: None, }) } fn degraded(error: DesktopAppRuntimeBootstrapError) -> Self { Self { - data_dir: None, - logs_dir: None, - database_path: None, - sqlite_schema_version: None, state_store: AppStateStore::in_memory(AppShellProjection { app_mode: AppMode::Farmer, ..AppShellProjection::default() }), - sync_projection: AppSyncProjection::default(), startup_issue: Some(error.to_string()), } } @@ -173,7 +138,6 @@ mod tests { use radroots_app_core::{AppRuntimeHostEnvironment, AppRuntimePlatform, AppRuntimeRoots}; use radroots_app_models::TodayAgendaProjection; use radroots_app_state::{AppStateStore, InMemoryAppStateRepository, SettingsPreference}; - use radroots_app_sync::AppSyncProjection; use super::{ APP_DATABASE_FILE_NAME, DesktopAppRuntime, DesktopAppRuntimeState, SettingsSection, @@ -207,13 +171,8 @@ mod tests { #[test] fn cloned_runtime_handles_share_shell_state() { let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState { - data_dir: None, - logs_dir: None, - database_path: None, - sqlite_schema_version: None, state_store: AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory state store should load"), - sync_projection: AppSyncProjection::default(), startup_issue: None, }); let cloned_runtime = runtime.clone(); diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -1,19 +1,21 @@ use gpui::{ AnyElement, App, AppContext, Bounds, Context, InteractiveElement, IntoElement, ParentElement, - Render, StatefulInteractiveElement, Styled, Window, WindowBounds, WindowOptions, div, - prelude::FluentBuilder, px, rgb, size, + Render, SharedString, StatefulInteractiveElement, Styled, Window, WindowBounds, WindowOptions, + div, prelude::FluentBuilder, px, relative, rgb, size, }; use gpui_component::IconName; -use radroots_app_core::AppRuntimeSnapshot; -use radroots_app_i18n::{AppTextKey, app_text}; +use radroots_app_i18n::AppTextKey; pub use radroots_app_models::SettingsSection as SettingsPanelViewKey; +use radroots_app_models::{ + FulfillmentWindowSummary, OrderListRow, ProductListRow, TodayAgendaProjection, + TodaySetupTaskKind, +}; use radroots_app_state::SettingsPreference; -use radroots_app_sync::{AppSyncRunStatus, SyncCheckpointState}; use radroots_app_ui::{ APP_UI_THEME, AppCheckboxFieldSpec, IconSegmentButtonSpec, LabelValueRow, action_button, action_button_compact, action_icon_button, app_checkbox_field, app_shared_label_text, - app_shared_text, app_window_shell, icon_segment_button, label_value_list, - runtime_metadata_rows, section_divider, status_indicator, utility_title_row, + app_shared_text, app_window_shell, icon_segment_button, label_value_list, section_divider, + status_indicator, utility_title_row, }; use crate::runtime::{DesktopAppRuntime, DesktopAppRuntimeSummary}; @@ -65,20 +67,19 @@ pub fn open_settings_window( } pub struct HomeView { - snapshot: AppRuntimeSnapshot, runtime: DesktopAppRuntime, } impl HomeView { - pub fn new(snapshot: AppRuntimeSnapshot, runtime: DesktopAppRuntime) -> Self { - Self { snapshot, runtime } + pub fn new(runtime: DesktopAppRuntime) -> Self { + Self { runtime } } } impl Render for HomeView { fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement { let runtime_summary = self.runtime.summary(); - let metadata_rows = home_metadata_rows(&self.snapshot, &runtime_summary); + let home_status = home_status_presentation(&runtime_summary); app_window_shell( APP_UI_THEME.surfaces.window_background, @@ -94,12 +95,37 @@ impl Render for HomeView { .p(px(APP_UI_THEME.layout.home_window_padding_px)) .flex() .flex_col() + .justify_between() + .child( + div() + .flex() + .flex_col() + .gap(px(APP_UI_THEME.layout.home_stack_gap_px)) + .child( + div() + .text_size(px(APP_UI_THEME.typography.brand_text_px)) + .text_color(rgb(APP_UI_THEME.text.primary)) + .child(app_shared_text(AppTextKey::HomeBrand)), + ) + .child( + div() + .text_size(px(APP_UI_THEME.typography.body_text_px * 2.0)) + .font_weight(gpui::FontWeight::BOLD) + .text_color(rgb(APP_UI_THEME.text.primary)) + .child(app_shared_text(AppTextKey::HomeTodayTitle)), + ) + .child(home_status_row(&home_status)), + ) .child( div().child( div() .text_size(px(APP_UI_THEME.typography.body_text_px)) - .text_color(rgb(APP_UI_THEME.text.primary)) - .child(app_shared_text(AppTextKey::HomeBrand)), + .line_height(relative(1.2)) + .text_color(rgb(APP_UI_THEME.text.secondary)) + .when_some( + runtime_summary.today_projection.farm.as_ref(), + |this, farm| this.child(farm.display_name.clone()), + ), ), ), ) @@ -124,7 +150,7 @@ impl Render for HomeView { .id("home-metadata-scroll") .size_full() .overflow_y_scroll() - .child(label_value_list(metadata_rows)), + .child(home_view_content(&runtime_summary)), ), ), ), @@ -730,98 +756,461 @@ fn settings_panel_spec(view: SettingsPanelViewKey) -> (&'static str, IconName) { } } -fn home_metadata_rows( - snapshot: &AppRuntimeSnapshot, - runtime: &DesktopAppRuntimeSummary, -) -> Vec<LabelValueRow> { - let mut rows = runtime_metadata_rows(snapshot); - - rows.push(metadata_row( - AppTextKey::MetadataDataRoot, - path_or_none(runtime.data_dir.as_ref()), - )); - rows.push(metadata_row( - AppTextKey::MetadataLogsRoot, - path_or_none(runtime.logs_dir.as_ref()), - )); - rows.push(metadata_row( - AppTextKey::MetadataDatabasePath, - path_or_none(runtime.database_path.as_ref()), - )); - rows.push(metadata_row( - AppTextKey::MetadataDatabaseSchemaVersion, - optional_text( - runtime - .sqlite_schema_version - .map(|version| version.to_string()), - ), - )); - rows.push(metadata_row( - AppTextKey::MetadataShellSection, - runtime - .shell_projection - .selected_section - .storage_key() - .to_owned(), - )); - rows.push(metadata_row( - AppTextKey::MetadataSyncRunStatus, - sync_run_status_text(runtime.sync_projection.run_status), - )); - rows.push(metadata_row( - AppTextKey::MetadataSyncCheckpointState, - sync_checkpoint_state_text(runtime.sync_projection.checkpoint.state), - )); - rows.push(metadata_row( - AppTextKey::MetadataSyncConflictCount, - runtime - .sync_projection - .conflict_status - .unresolved_count - .to_string(), - )); +#[derive(Clone, Copy)] +struct HomeStatusPresentation { + indicator_color: u32, + label_key: AppTextKey, +} + +fn home_view_content(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement { + let projection = &runtime.today_projection; + let home_status = home_status_presentation(runtime); + let mut sections = Vec::<AnyElement>::new(); + + if let Some(summary) = projection.summary.as_ref() { + sections.push(home_summary_card(summary).into_any_element()); + } if let Some(issue) = runtime.startup_issue.as_ref() { - rows.push(metadata_row( - AppTextKey::MetadataStartupIssue, - issue.clone(), - )); + sections.push( + home_card( + app_shared_text(AppTextKey::MetadataStartupIssue), + home_body_text(issue.clone()), + ) + .into_any_element(), + ); } - rows + if projection.needs_setup() { + sections.push(home_setup_card(projection).into_any_element()); + } + + if let Some(next_window) = projection.next_fulfillment_window.as_ref() { + sections.push(home_next_fulfillment_window_card(next_window).into_any_element()); + } + + if !projection.orders_needing_action.is_empty() { + sections.push( + home_list_card( + AppTextKey::HomeTodayOrdersNeedingAction, + projection + .orders_needing_action + .iter() + .map(home_order_row) + .collect::<Vec<_>>(), + ) + .into_any_element(), + ); + } + + if !projection.low_stock_products.is_empty() { + sections.push( + home_list_card( + AppTextKey::HomeTodayLowStock, + projection + .low_stock_products + .iter() + .map(home_low_stock_row) + .collect::<Vec<_>>(), + ) + .into_any_element(), + ); + } + + if !projection.draft_products.is_empty() { + sections.push( + home_list_card( + AppTextKey::HomeTodayDraftProducts, + projection + .draft_products + .iter() + .map(home_draft_row) + .collect::<Vec<_>>(), + ) + .into_any_element(), + ); + } + + if runtime.startup_issue.is_none() && projection.farm.is_none() { + sections.push( + home_empty_state_card( + AppTextKey::HomeTodayEmptyNoFarmTitle, + AppTextKey::HomeTodayEmptyNoFarmBody, + ) + .into_any_element(), + ); + } else if runtime.startup_issue.is_none() + && projection.farm.is_some() + && !projection.needs_setup() + && projection.next_fulfillment_window.is_none() + && !projection.has_attention_items() + { + sections.push( + home_empty_state_card( + AppTextKey::HomeTodayEmptyQuietTitle, + AppTextKey::HomeTodayEmptyQuietBody, + ) + .into_any_element(), + ); + } + + div() + .w_full() + .max_w(px(APP_UI_THEME.layout.home_card_max_width_px)) + .mx_auto() + .flex() + .flex_col() + .gap(px(APP_UI_THEME.layout.home_stack_gap_px)) + .child( + div() + .w_full() + .flex() + .flex_col() + .gap(px(4.0)) + .child( + div() + .text_size(px(APP_UI_THEME.typography.body_text_px * 2.0)) + .font_weight(gpui::FontWeight::BOLD) + .text_color(rgb(APP_UI_THEME.text.primary)) + .child(app_shared_text(AppTextKey::HomeTodayTitle)), + ) + .child( + div() + .text_size(px(APP_UI_THEME.typography.body_text_px)) + .font_weight(gpui::FontWeight::MEDIUM) + .line_height(relative(1.2)) + .text_color(rgb(APP_UI_THEME.text.primary)) + .when_some(projection.farm.as_ref(), |this, farm| { + this.child(farm.display_name.clone()) + }) + .when(projection.farm.is_none(), |this| { + this.child(app_shared_text(home_status.label_key)) + }), + ) + .child(home_status_row(&home_status)), + ) + .children(sections) +} + +fn home_card(title: impl Into<SharedString>, body: impl IntoElement) -> impl IntoElement { + div() + .w_full() + .bg(rgb(APP_UI_THEME.surfaces.card_background)) + .rounded(px(APP_UI_THEME + .controls + .action_button + .sizing + .corner_radius_px)) + .child( + div() + .w_full() + .p(px(APP_UI_THEME.layout.home_card_padding_px)) + .flex() + .flex_col() + .gap(px(APP_UI_THEME.layout.home_stack_gap_px)) + .child( + div() + .text_size(px(APP_UI_THEME.typography.body_text_px)) + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(APP_UI_THEME.text.primary)) + .child(title.into()), + ) + .child(body), + ) +} + +fn home_body_text(body: impl Into<SharedString>) -> impl IntoElement { + div() + .w_full() + .text_size(px(APP_UI_THEME.typography.body_text_px)) + .line_height(relative(1.2)) + .text_color(rgb(APP_UI_THEME.text.secondary)) + .child(body.into()) +} + +fn home_status_row(status: &HomeStatusPresentation) -> impl IntoElement { + div() + .flex() + .items_center() + .gap(px(APP_UI_THEME.layout.settings_account_status_gap_px)) + .child(status_indicator(status.indicator_color)) + .child( + div() + .text_size(px(APP_UI_THEME.typography.body_text_px)) + .text_color(rgb(APP_UI_THEME.text.secondary)) + .child(app_shared_text(status.label_key)), + ) +} + +fn home_summary_card(summary: &radroots_app_models::TodaySummary) -> impl IntoElement { + home_card( + app_shared_text(AppTextKey::HomeTodayTitle), + div() + .w_full() + .flex() + .gap(px(APP_UI_THEME.layout.home_stack_gap_px)) + .child(home_summary_metric( + AppTextKey::HomeTodayOrdersNeedingAction, + summary.orders_needing_action, + )) + .child(home_summary_metric( + AppTextKey::HomeTodayLowStock, + summary.low_stock_products, + )) + .child(home_summary_metric( + AppTextKey::HomeTodayDraftProducts, + summary.draft_products, + )), + ) +} + +fn home_summary_metric(label_key: AppTextKey, value: u32) -> impl IntoElement { + div() + .flex_1() + .min_w_0() + .bg(rgb(APP_UI_THEME.surfaces.window_background)) + .rounded(px(APP_UI_THEME + .controls + .action_button + .sizing + .corner_radius_px)) + .p(px(16.0)) + .flex() + .flex_col() + .gap(px(4.0)) + .child( + div() + .text_size(px(APP_UI_THEME.typography.body_text_px * 2.0)) + .font_weight(gpui::FontWeight::BOLD) + .text_color(rgb(APP_UI_THEME.text.primary)) + .child(value.to_string()), + ) + .child( + div() + .text_size(px(APP_UI_THEME.typography.utility_title_text_px)) + .line_height(relative(1.2)) + .text_color(rgb(APP_UI_THEME.text.secondary)) + .child(app_shared_text(label_key)), + ) } -fn metadata_row(label: AppTextKey, value: impl Into<String>) -> LabelValueRow { - LabelValueRow::new(app_shared_text(label), value.into()) +fn home_setup_card(projection: &TodayAgendaProjection) -> impl IntoElement { + home_list_card( + AppTextKey::HomeTodaySetupChecklist, + projection + .setup_checklist + .iter() + .map(home_setup_task_row) + .collect::<Vec<_>>(), + ) } -fn path_or_none(path: Option<&std::path::PathBuf>) -> String { - optional_text(path.map(|path| path.display().to_string())) +fn home_next_fulfillment_window_card(next_window: &FulfillmentWindowSummary) -> impl IntoElement { + home_card( + app_shared_text(AppTextKey::HomeTodayNextFulfillmentWindow), + label_value_list(vec![ + LabelValueRow::new( + app_shared_text(AppTextKey::HomeTodayWindowStartsLabel), + next_window.starts_at.clone(), + ), + LabelValueRow::new( + app_shared_text(AppTextKey::HomeTodayWindowEndsLabel), + next_window.ends_at.clone(), + ), + ]), + ) } -fn optional_text(value: Option<String>) -> String { - value.unwrap_or_else(|| app_text(AppTextKey::ValueNone)) +fn home_list_card(title_key: AppTextKey, rows: Vec<AnyElement>) -> impl IntoElement { + home_card( + app_shared_text(title_key), + div() + .w_full() + .flex() + .flex_col() + .gap(px(APP_UI_THEME.layout.home_stack_gap_px)) + .children(rows), + ) } -fn sync_run_status_text(status: AppSyncRunStatus) -> String { - let key = match status { - AppSyncRunStatus::Idle => AppTextKey::ValueSyncRunStatusIdle, - AppSyncRunStatus::Syncing => AppTextKey::ValueSyncRunStatusSyncing, - AppSyncRunStatus::Succeeded => AppTextKey::ValueSyncRunStatusSucceeded, - AppSyncRunStatus::Conflicted => AppTextKey::ValueSyncRunStatusConflicted, - AppSyncRunStatus::Failed => AppTextKey::ValueSyncRunStatusFailed, - }; +fn home_order_row(order: &OrderListRow) -> AnyElement { + div() + .w_full() + .min_w_0() + .flex() + .items_center() + .justify_between() + .gap(px(APP_UI_THEME.layout.home_stack_gap_px)) + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap(px(2.0)) + .child( + div() + .text_size(px(APP_UI_THEME.typography.body_text_px)) + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(APP_UI_THEME.text.primary)) + .child(order.order_number.clone()), + ) + .child( + div() + .text_size(px(APP_UI_THEME.typography.utility_title_text_px)) + .text_color(rgb(APP_UI_THEME.text.secondary)) + .child(order.customer_display_name.clone()), + ), + ) + .child(status_indicator( + APP_UI_THEME.controls.status_indicator.attention, + )) + .into_any_element() +} - app_text(key) +fn home_low_stock_row(product: &ProductListRow) -> AnyElement { + div() + .w_full() + .min_w_0() + .flex() + .items_center() + .justify_between() + .gap(px(APP_UI_THEME.layout.home_stack_gap_px)) + .child( + div() + .min_w_0() + .text_size(px(APP_UI_THEME.typography.body_text_px)) + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(APP_UI_THEME.text.primary)) + .child(product.title.clone()), + ) + .child( + div() + .flex() + .items_center() + .gap(px(APP_UI_THEME.layout.settings_account_status_gap_px)) + .child(status_indicator( + APP_UI_THEME.controls.status_indicator.attention, + )) + .child( + div() + .flex() + .items_center() + .gap(px(4.0)) + .child( + div() + .text_size(px(APP_UI_THEME.typography.utility_title_text_px)) + .text_color(rgb(APP_UI_THEME.text.secondary)) + .child(app_shared_label_text(AppTextKey::HomeTodayStockCountLabel)), + ) + .child( + div() + .text_size(px(APP_UI_THEME.typography.body_text_px)) + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(APP_UI_THEME.text.primary)) + .child(product.stock_count.to_string()), + ), + ), + ) + .into_any_element() +} + +fn home_draft_row(product: &ProductListRow) -> AnyElement { + div() + .w_full() + .min_w_0() + .flex() + .items_center() + .justify_between() + .gap(px(APP_UI_THEME.layout.home_stack_gap_px)) + .child( + div() + .min_w_0() + .text_size(px(APP_UI_THEME.typography.body_text_px)) + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(APP_UI_THEME.text.primary)) + .child(product.title.clone()), + ) + .child(status_indicator( + APP_UI_THEME.controls.status_indicator.offline, + )) + .into_any_element() +} + +fn home_setup_task_row(task: &radroots_app_models::TodaySetupTask) -> AnyElement { + let is_complete = task.is_complete; + + div() + .w_full() + .min_w_0() + .flex() + .items_center() + .gap(px(APP_UI_THEME.layout.settings_account_status_gap_px)) + .child(status_indicator(if is_complete { + APP_UI_THEME.controls.status_indicator.online + } else { + APP_UI_THEME.controls.status_indicator.offline + })) + .child( + div() + .min_w_0() + .text_size(px(APP_UI_THEME.typography.body_text_px)) + .font_weight(gpui::FontWeight::MEDIUM) + .line_height(relative(1.2)) + .text_color(rgb(if is_complete { + APP_UI_THEME.text.secondary + } else { + APP_UI_THEME.text.primary + })) + .child(app_shared_text(home_setup_task_label_key(task.kind))), + ) + .into_any_element() +} + +fn home_empty_state_card(title_key: AppTextKey, body_key: AppTextKey) -> impl IntoElement { + home_card( + app_shared_text(title_key), + home_body_text(app_shared_text(body_key)), + ) } -fn sync_checkpoint_state_text(state: SyncCheckpointState) -> String { - let key = match state { - SyncCheckpointState::NeverSynced => AppTextKey::ValueSyncCheckpointNeverSynced, - SyncCheckpointState::Syncing => AppTextKey::ValueSyncCheckpointSyncing, - SyncCheckpointState::Current => AppTextKey::ValueSyncCheckpointCurrent, - SyncCheckpointState::Failed => AppTextKey::ValueSyncCheckpointFailed, - }; +fn home_status_presentation(runtime: &DesktopAppRuntimeSummary) -> HomeStatusPresentation { + if runtime.startup_issue.is_some() { + return HomeStatusPresentation { + indicator_color: APP_UI_THEME.controls.status_indicator.attention, + label_key: AppTextKey::HomeTodayStatusStartupIssue, + }; + } + + if runtime.today_projection.farm.is_none() { + return HomeStatusPresentation { + indicator_color: APP_UI_THEME.controls.status_indicator.offline, + label_key: AppTextKey::HomeTodayStatusNoFarm, + }; + } + + if runtime.today_projection.needs_setup() { + return HomeStatusPresentation { + indicator_color: APP_UI_THEME.controls.status_indicator.offline, + label_key: AppTextKey::HomeTodayStatusSetup, + }; + } + + if runtime.today_projection.has_attention_items() { + return HomeStatusPresentation { + indicator_color: APP_UI_THEME.controls.status_indicator.attention, + label_key: AppTextKey::HomeTodayStatusAttention, + }; + } + + HomeStatusPresentation { + indicator_color: APP_UI_THEME.controls.status_indicator.online, + label_key: AppTextKey::HomeTodayStatusReady, + } +} - app_text(key) +fn home_setup_task_label_key(kind: TodaySetupTaskKind) -> AppTextKey { + match kind { + TodaySetupTaskKind::AddFulfillmentWindow => AppTextKey::HomeTodaySetupAddFulfillmentWindow, + TodaySetupTaskKind::PublishProduct => AppTextKey::HomeTodaySetupPublishProduct, + } } diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs @@ -22,6 +22,26 @@ macro_rules! define_app_text_keys { define_app_text_keys! { AppName => "app.name", HomeBrand => "home.brand", + HomeTodayTitle => "home.today.title", + HomeTodayStatusNoFarm => "home.today.status.no_farm", + HomeTodayStatusSetup => "home.today.status.setup", + HomeTodayStatusAttention => "home.today.status.attention", + HomeTodayStatusReady => "home.today.status.ready", + HomeTodayStatusStartupIssue => "home.today.status.startup_issue", + HomeTodayOrdersNeedingAction => "home.today.orders_needing_action", + HomeTodayLowStock => "home.today.low_stock", + HomeTodayDraftProducts => "home.today.draft_products", + HomeTodaySetupChecklist => "home.today.setup_checklist", + HomeTodayNextFulfillmentWindow => "home.today.next_fulfillment_window", + HomeTodayWindowStartsLabel => "home.today.window.starts", + HomeTodayWindowEndsLabel => "home.today.window.ends", + HomeTodayStockCountLabel => "home.today.stock_count.label", + HomeTodaySetupAddFulfillmentWindow => "home.today.setup.add_fulfillment_window", + HomeTodaySetupPublishProduct => "home.today.setup.publish_product", + HomeTodayEmptyNoFarmTitle => "home.today.empty.no_farm.title", + HomeTodayEmptyNoFarmBody => "home.today.empty.no_farm.body", + HomeTodayEmptyQuietTitle => "home.today.empty.quiet.title", + HomeTodayEmptyQuietBody => "home.today.empty.quiet.body", MenuQuit => "menu.quit", MenuAbout => "menu.about", MenuServices => "menu.services", diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -1,6 +1,26 @@ { "app.name": "Radroots", "home.brand": "radroots", + "home.today.title": "Today", + "home.today.status.no_farm": "No farm configured", + "home.today.status.setup": "Setup required", + "home.today.status.attention": "Needs attention", + "home.today.status.ready": "Ready", + "home.today.status.startup_issue": "Startup issue", + "home.today.orders_needing_action": "Orders needing action", + "home.today.low_stock": "Low stock", + "home.today.draft_products": "Draft products", + "home.today.setup_checklist": "Setup checklist", + "home.today.next_fulfillment_window": "Next fulfillment window", + "home.today.window.starts": "Starts", + "home.today.window.ends": "Ends", + "home.today.stock_count.label": "Stock", + "home.today.setup.add_fulfillment_window": "Add a fulfillment window", + "home.today.setup.publish_product": "Publish a product", + "home.today.empty.no_farm.title": "No farm yet", + "home.today.empty.no_farm.body": "Create a farm to start using the farmer workspace.", + "home.today.empty.quiet.title": "Nothing urgent right now", + "home.today.empty.quiet.body": "Orders, stock, and drafts will appear here when they need attention.", "menu.about": "About Radroots", "menu.services": "Services", "menu.quit": "Quit Radroots",