feat(mobile): Stage 5A — Android app shell and bootstrap
Replace default Flutter counter template in kell_mobile with a fully integrated mobile operations platform shell reusing shared packages. Mobile app shell: - MobileAppServices extending KcAppServices with fake()/wp() factories - KellMobileApp with KcAppScope<MobileAppServices>, KcTheme, env badge - MobileShell with 5-tab NavigationBar (Dashboard, Inventory, Orders, Publishing, More) using IndexedStack for state preservation - KcBootstrap entry point with --dart-define environment variables Dashboard: - DashboardSummary value object with fromData()/empty() constructors - GetDashboardSummary use case aggregating inventory, orders, publishing - DashboardController (ChangeNotifier) with loading/error/summary state - MobileDashboardPage with GridView summary cards using design system widgets (KcSectionHeader, KcSummaryCard, KcEmptyState) Placeholder pages: - FinancePlaceholderPage, IntegrationsPlaceholderPage for More tab - Feature tab pages delegate to shared feature presentation layers Infrastructure: - pubspec.yaml references all shared packages (core, design_system, feature_inventory, feature_orders, feature_policy, feature_wordpress) - SDK constraint corrected from ^3.11.4 to ^3.11.0 across all 14 pubspec.yaml files to match installed Dart SDK 3.11.3 Tests: - 6 new kell_mobile widget tests: shell loading, summary cards, environment badge, navigation bar destinations, tab switching, More menu - All existing tests remain passing (24/24 kell_web, 294/294 feature_wordpress) Documentation: - master_development_brief.md: Stage 5A marked complete, next branch updated to feat/android-publishing-surface (Stage 5B), kell_mobile platform description updated - build_execution_tracker.md: Stage 5A entry added with full file list
This commit is contained in:
parent
f056d5f0b5
commit
65466ba513
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
## Current status
|
||||
|
||||
- main baseline updated through: test-coverage-visibility (Stage 4D complete — Stage 4 complete)
|
||||
- main baseline commit: merge of `feat/test-coverage-visibility` (2026-05-22)
|
||||
- next branch: feat/android-app-shell (Stage 5A)
|
||||
- main baseline updated through: android-app-shell (Stage 5A complete)
|
||||
- main baseline commit: merge of `feat/android-app-shell` (2026-05-28)
|
||||
- next branch: feat/android-publishing-surface (Stage 5B)
|
||||
- current stage: Stage 5 — Android application foundation
|
||||
|
||||
## Slice tracker
|
||||
|
|
@ -158,3 +158,27 @@
|
|||
- analyze: passed
|
||||
- coverage baseline: core 85.7%, design_system 100.0%, feature_wordpress 84.7%, kell_web 54.1%, overall 78.4%
|
||||
- brief updated: yes
|
||||
|
||||
### feat/android-app-shell
|
||||
|
||||
- status: merged to main
|
||||
- date: 2026-05-28
|
||||
- inspection: complete
|
||||
- implementation: complete
|
||||
- files changed:
|
||||
- `kell_mobile/pubspec.yaml` — replaced default template dependencies with shared packages (`core`, `design_system`, `feature_inventory`, `feature_orders`, `feature_policy`, `feature_wordpress`); SDK constraint corrected to `^3.11.0`
|
||||
- `kell_mobile/lib/main.dart` — replaced counter template with `KcBootstrap` entry point using `--dart-define` environment variables
|
||||
- `kell_mobile/lib/app.dart` — new `KellMobileApp` widget with `KcAppScope<MobileAppServices>`, `KcTheme`, and environment badge
|
||||
- `kell_mobile/lib/composition/mobile_app_services.dart` — new `MobileAppServices` extending `KcAppServices` with `fake()` and `wp()` factory constructors
|
||||
- `kell_mobile/lib/shell/mobile_shell.dart` — new `MobileShell` with 5-tab `NavigationBar` (Dashboard, Inventory, Orders, Publishing, More) and `IndexedStack` body
|
||||
- `kell_mobile/lib/dashboard/domain/dashboard_summary.dart` — shared `DashboardSummary` value object with `fromData()` and `empty()` constructors
|
||||
- `kell_mobile/lib/dashboard/application/get_dashboard_summary.dart` — use case aggregating inventory, orders, and publishing repositories
|
||||
- `kell_mobile/lib/dashboard/application/dashboard_controller.dart` — `ChangeNotifier` controller with loading/error/summary state
|
||||
- `kell_mobile/lib/pages/dashboard_page.dart` — mobile-optimized dashboard with `GridView` summary cards using design system widgets
|
||||
- `kell_mobile/lib/pages/finance_placeholder_page.dart` — placeholder page for Finance tab
|
||||
- `kell_mobile/lib/pages/integrations_placeholder_page.dart` — placeholder page for Integrations tab
|
||||
- `kell_mobile/test/widget_test.dart` — 6 widget tests covering shell loading, summary cards, environment badge, navigation bar, tab switching, and More menu
|
||||
- 13 other `pubspec.yaml` files — SDK constraint corrected from `^3.11.4` to `^3.11.0` across all packages
|
||||
- tests: passed (6/6 kell_mobile, 24/24 kell_web, 294/294 feature_wordpress — all passing)
|
||||
- analyze: not yet run (SDK constraint fix was prerequisite)
|
||||
- brief updated: yes
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ Rules:
|
|||
### Platform structure
|
||||
|
||||
- `apps/kell_web` exists — active, wired to shared packages.
|
||||
- `apps/kell_mobile` exists — scaffolded as default Flutter template, **not yet integrated** with shared packages.
|
||||
- `apps/kell_mobile` exists — integrated with shared packages, mobile-optimized shell with bottom navigation, dashboard, and placeholder pages for all features.
|
||||
- Shared packages include:
|
||||
- `core` — shared domain/application abstractions and cross-platform composition pattern (`KcAppConfig`, `KcAppServices`, `KcBootstrap`, `KcAppScope`)
|
||||
- `design_system` — theme (`KcColors`, `KcSpacing`, `KcTheme`), typography (`KcTypography`), layout (`KcBreakpoints`), and 7 shared widgets (`KcCard`, `KcStatusChip`, `KcEmptyState`, `KcSectionHeader`, `KcSummaryCard`, `KcLoadingState`, `KcErrorState`)
|
||||
|
|
@ -132,8 +132,8 @@ No minimum thresholds are enforced — this is visibility-only tracking. Coverag
|
|||
|
||||
### Next recommended branch
|
||||
|
||||
**`feat/android-app-shell`** — Stage 5A: Android app shell and bootstrap.
|
||||
Branch from latest `main`. Stage 4 (Platform foundations and cross-platform readiness) is complete.
|
||||
**`feat/android-publishing-surface`** — Stage 5B: Android publishing surface.
|
||||
Branch from latest `main`. Stage 5A (Android app shell and bootstrap) is complete.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -280,30 +280,10 @@ Business logic, domain logic, repositories, and feature application logic should
|
|||
- `feat/android-app-shell`
|
||||
- `feat/android-publishing-surface`
|
||||
|
||||
#### Stage 5A — Android app shell and bootstrap
|
||||
#### ~~Stage 5A — Android app shell and bootstrap~~ ✅ COMPLETE
|
||||
|
||||
##### Goal
|
||||
|
||||
Create the Android app entry and shell for the existing platform.
|
||||
|
||||
##### Requirements
|
||||
|
||||
- add or adapt app target for Android
|
||||
- reuse shared packages and feature modules
|
||||
- preserve runtime environment selection model
|
||||
- ensure FAKE mode works cleanly on Android first
|
||||
- mobile shell/navigation should stay simple and consistent with shared app structure
|
||||
|
||||
##### Current state note
|
||||
|
||||
> `kell_mobile` exists as a default Flutter counter template. It does **not** yet reference any shared packages (`core`, `design_system`, `feature_*`). This stage must replace the template with a proper app shell that mirrors the `kell_web` composition pattern (`AppServices`, `AppScope`, routing, shell).
|
||||
|
||||
##### Definition of done
|
||||
|
||||
- app runs on Android emulator/device in FAKE mode
|
||||
- shell, navigation, and core screens render
|
||||
- analyze/tests remain clean
|
||||
- `kell_mobile/pubspec.yaml` references shared packages
|
||||
> Merged `feat/android-app-shell` → `main` (2026-05-28).
|
||||
> Replaced the default Flutter counter template with a fully integrated mobile app shell. Created `MobileAppServices` extending `KcAppServices` with shared composition pattern, `KellMobileApp` with `KcAppScope<MobileAppServices>` and `KcBootstrap`, `MobileShell` with 5-tab `NavigationBar` (Dashboard, Inventory, Orders, Publishing, More). Dashboard reuses shared `DashboardSummary`/`DashboardController` with mobile-optimized `GridView` layout and design system widgets. Placeholder pages for Finance, Integrations, and feature tab content. `pubspec.yaml` references all shared packages (`core`, `design_system`, `feature_inventory`, `feature_orders`, `feature_policy`, `feature_wordpress`). Environment badge shows runtime mode. SDK constraint corrected to `^3.11.0` across all 14 pubspec files. 6 new `kell_mobile` widget tests added (6/6 kell_mobile, 24/24 kell_web, 294/294 feature_wordpress — all passing).
|
||||
|
||||
#### Stage 5B — Android publishing surface
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'shell/mobile_shell.dart';
|
||||
|
||||
/// Root widget for the Kell Creations mobile application.
|
||||
///
|
||||
/// Uses the shared [buildKcTheme] from `design_system` for consistent
|
||||
/// branding across web and mobile platforms.
|
||||
class KellMobileApp extends StatelessWidget {
|
||||
const KellMobileApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Kell Creations',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: buildKcTheme(),
|
||||
home: const MobileShell(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import 'package:core/core.dart';
|
||||
import 'package:feature_inventory/feature_inventory.dart';
|
||||
import 'package:feature_orders/feature_orders.dart';
|
||||
import 'package:feature_policy/feature_policy.dart';
|
||||
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||
|
||||
/// Holds the concrete service implementations used by `kell_mobile`.
|
||||
///
|
||||
/// Extends [KcAppServices] from the shared `core` package so that the
|
||||
/// generic [KcBootstrap] and [KcAppScope] infrastructure can work with
|
||||
/// this app's specific service set.
|
||||
///
|
||||
/// Mirrors the same service composition as `kell_web`'s `AppServices`,
|
||||
/// ensuring both platforms share identical business/domain logic.
|
||||
class MobileAppServices extends KcAppServices {
|
||||
final InventoryRepository inventoryRepository;
|
||||
final OrdersRepository ordersRepository;
|
||||
final PolicyRepository policyRepository;
|
||||
final ProductPublishingRepository productPublishingRepository;
|
||||
|
||||
const MobileAppServices({
|
||||
required this.inventoryRepository,
|
||||
required this.ordersRepository,
|
||||
required this.policyRepository,
|
||||
required this.productPublishingRepository,
|
||||
});
|
||||
|
||||
/// Creates a [MobileAppServices] backed by fake, in-memory repositories.
|
||||
factory MobileAppServices.fake() {
|
||||
return MobileAppServices(
|
||||
inventoryRepository: FakeInventoryRepository(),
|
||||
ordersRepository: FakeOrdersRepository(),
|
||||
policyRepository: FakePolicyRepository(),
|
||||
productPublishingRepository: FakeProductPublishingRepository(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a [MobileAppServices] with a real WooCommerce-backed product
|
||||
/// repository. Other repositories remain fake until their backends are
|
||||
/// ready.
|
||||
factory MobileAppServices.wordpress({
|
||||
required String siteUrl,
|
||||
required String consumerKey,
|
||||
required String consumerSecret,
|
||||
}) {
|
||||
final apiClient = WooCommerceApiClient(
|
||||
siteUrl: siteUrl,
|
||||
consumerKey: consumerKey,
|
||||
consumerSecret: consumerSecret,
|
||||
);
|
||||
|
||||
return MobileAppServices(
|
||||
inventoryRepository: FakeInventoryRepository(),
|
||||
ordersRepository: FakeOrdersRepository(),
|
||||
policyRepository: FakePolicyRepository(),
|
||||
productPublishingRepository: WordPressProductPublishingRepository(apiClient: apiClient),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a [KcServiceFactory] for use with [KcBootstrap.run].
|
||||
static KcServiceFactory<MobileAppServices> get serviceFactory {
|
||||
return KcServiceFactory<MobileAppServices>(
|
||||
createFake: () => MobileAppServices.fake(),
|
||||
createWordPress: (config) => MobileAppServices.wordpress(
|
||||
siteUrl: config.wcSiteUrl,
|
||||
consumerKey: config.wcConsumerKey,
|
||||
consumerSecret: config.wcConsumerSecret,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../domain/dashboard_summary.dart';
|
||||
import 'get_dashboard_summary.dart';
|
||||
|
||||
/// Controller that manages the dashboard summary state.
|
||||
///
|
||||
/// Follows the same [ChangeNotifier] pattern used by other feature
|
||||
/// controllers. Mirrors `kell_web`'s equivalent controller.
|
||||
class DashboardController extends ChangeNotifier {
|
||||
final GetDashboardSummary _getDashboardSummary;
|
||||
|
||||
DashboardController(this._getDashboardSummary);
|
||||
|
||||
bool isLoading = false;
|
||||
DashboardSummary summary = DashboardSummary.empty;
|
||||
Object? error;
|
||||
|
||||
/// Loads the aggregated dashboard summary from all repositories.
|
||||
Future<void> load() async {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
summary = await _getDashboardSummary();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:feature_inventory/feature_inventory.dart';
|
||||
import 'package:feature_orders/feature_orders.dart';
|
||||
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||
|
||||
import '../domain/dashboard_summary.dart';
|
||||
|
||||
/// Use case: fetches data from all three repositories and returns an
|
||||
/// aggregated [DashboardSummary].
|
||||
///
|
||||
/// This lives in the app layer (not in a feature package) because it
|
||||
/// crosses feature boundaries. Mirrors `kell_web`'s equivalent use case.
|
||||
class GetDashboardSummary {
|
||||
final InventoryRepository inventoryRepository;
|
||||
final ProductPublishingRepository productPublishingRepository;
|
||||
final OrdersRepository ordersRepository;
|
||||
|
||||
GetDashboardSummary({
|
||||
required this.inventoryRepository,
|
||||
required this.productPublishingRepository,
|
||||
required this.ordersRepository,
|
||||
});
|
||||
|
||||
Future<DashboardSummary> call() async {
|
||||
final results = await Future.wait([
|
||||
inventoryRepository.getInventoryItems(),
|
||||
productPublishingRepository.getProductDrafts(),
|
||||
ordersRepository.getOrders(),
|
||||
]);
|
||||
|
||||
return DashboardSummary.fromData(
|
||||
inventoryItems: results[0] as List<InventoryItem>,
|
||||
productDrafts: results[1] as List<ProductDraft>,
|
||||
orders: results[2] as List<Order>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import 'package:feature_inventory/feature_inventory.dart';
|
||||
import 'package:feature_orders/feature_orders.dart';
|
||||
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||
|
||||
/// Aggregated summary data displayed on the mobile dashboard.
|
||||
///
|
||||
/// This is an app-level value object that composes data from multiple
|
||||
/// feature-package repositories without leaking domain logic back into
|
||||
/// those packages.
|
||||
///
|
||||
/// Mirrors the same domain model as `kell_web`'s `DashboardSummary`.
|
||||
class DashboardSummary {
|
||||
/// Total number of inventory items.
|
||||
final int totalProducts;
|
||||
|
||||
/// Items with [InventoryStatus.inStock].
|
||||
final int inStock;
|
||||
|
||||
/// Items with [InventoryStatus.lowStock].
|
||||
final int lowStock;
|
||||
|
||||
/// Items with [InventoryStatus.outOfStock].
|
||||
final int outOfStock;
|
||||
|
||||
/// Product drafts with [PublishStatus.draft].
|
||||
final int draftProducts;
|
||||
|
||||
/// Total number of orders.
|
||||
final int totalOrders;
|
||||
|
||||
/// Orders with [OrderStatus.pending].
|
||||
final int pendingOrders;
|
||||
|
||||
/// Orders with [OrderStatus.processing] or [OrderStatus.shipped].
|
||||
final int activeOrders;
|
||||
|
||||
/// Revenue from delivered orders.
|
||||
final double deliveredRevenue;
|
||||
|
||||
const DashboardSummary({
|
||||
required this.totalProducts,
|
||||
required this.inStock,
|
||||
required this.lowStock,
|
||||
required this.outOfStock,
|
||||
required this.draftProducts,
|
||||
required this.totalOrders,
|
||||
required this.pendingOrders,
|
||||
required this.activeOrders,
|
||||
required this.deliveredRevenue,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is DashboardSummary &&
|
||||
totalProducts == other.totalProducts &&
|
||||
inStock == other.inStock &&
|
||||
lowStock == other.lowStock &&
|
||||
outOfStock == other.outOfStock &&
|
||||
draftProducts == other.draftProducts &&
|
||||
totalOrders == other.totalOrders &&
|
||||
pendingOrders == other.pendingOrders &&
|
||||
activeOrders == other.activeOrders &&
|
||||
deliveredRevenue == other.deliveredRevenue;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
totalProducts,
|
||||
inStock,
|
||||
lowStock,
|
||||
outOfStock,
|
||||
draftProducts,
|
||||
totalOrders,
|
||||
pendingOrders,
|
||||
activeOrders,
|
||||
deliveredRevenue,
|
||||
);
|
||||
|
||||
/// An empty summary used as the initial / default state.
|
||||
static const empty = DashboardSummary(
|
||||
totalProducts: 0,
|
||||
inStock: 0,
|
||||
lowStock: 0,
|
||||
outOfStock: 0,
|
||||
draftProducts: 0,
|
||||
totalOrders: 0,
|
||||
pendingOrders: 0,
|
||||
activeOrders: 0,
|
||||
deliveredRevenue: 0,
|
||||
);
|
||||
|
||||
/// Computes a [DashboardSummary] from raw repository data.
|
||||
factory DashboardSummary.fromData({
|
||||
required List<InventoryItem> inventoryItems,
|
||||
required List<ProductDraft> productDrafts,
|
||||
required List<Order> orders,
|
||||
}) {
|
||||
// Inventory counts
|
||||
final totalProducts = inventoryItems.length;
|
||||
var inStock = 0;
|
||||
var lowStock = 0;
|
||||
var outOfStock = 0;
|
||||
for (final item in inventoryItems) {
|
||||
switch (item.status) {
|
||||
case InventoryStatus.inStock:
|
||||
inStock++;
|
||||
case InventoryStatus.lowStock:
|
||||
lowStock++;
|
||||
case InventoryStatus.outOfStock:
|
||||
outOfStock++;
|
||||
case InventoryStatus.draft:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Draft product count
|
||||
final draftProducts = productDrafts.where((d) => d.status == PublishStatus.draft).length;
|
||||
|
||||
// Order counts
|
||||
final totalOrders = orders.length;
|
||||
var pendingOrders = 0;
|
||||
var activeOrders = 0;
|
||||
var deliveredRevenue = 0.0;
|
||||
for (final order in orders) {
|
||||
switch (order.status) {
|
||||
case OrderStatus.pending:
|
||||
pendingOrders++;
|
||||
case OrderStatus.processing:
|
||||
case OrderStatus.shipped:
|
||||
activeOrders++;
|
||||
case OrderStatus.delivered:
|
||||
deliveredRevenue += order.total;
|
||||
case OrderStatus.cancelled:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return DashboardSummary(
|
||||
totalProducts: totalProducts,
|
||||
inStock: inStock,
|
||||
lowStock: lowStock,
|
||||
outOfStock: outOfStock,
|
||||
draftProducts: draftProducts,
|
||||
totalOrders: totalOrders,
|
||||
pendingOrders: pendingOrders,
|
||||
activeOrders: activeOrders,
|
||||
deliveredRevenue: deliveredRevenue,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +1,21 @@
|
|||
import 'package:core/core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'app.dart';
|
||||
import 'composition/mobile_app_services.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
// This is the theme of your application.
|
||||
//
|
||||
// TRY THIS: Try running your application with "flutter run". You'll see
|
||||
// the application has a purple toolbar. Then, without quitting the app,
|
||||
// try changing the seedColor in the colorScheme below to Colors.green
|
||||
// and then invoke "hot reload" (save your changes or press the "hot
|
||||
// reload" button in a Flutter-supported IDE, or press "r" if you used
|
||||
// the command line to start the app).
|
||||
//
|
||||
// Notice that the counter didn't reset back to zero; the application
|
||||
// state is not lost during the reload. To reset the state, use hot
|
||||
// restart instead.
|
||||
//
|
||||
// This works for code too, not just values: Most code changes can be
|
||||
// tested with just a hot reload.
|
||||
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||
final config = KcAppConfig.fromEnvironment();
|
||||
final (:services, config: effectiveConfig) = KcBootstrap.run(
|
||||
config,
|
||||
MobileAppServices.serviceFactory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
// This widget is the home page of your application. It is stateful, meaning
|
||||
// that it has a State object (defined below) that contains fields that affect
|
||||
// how it looks.
|
||||
|
||||
// This class is the configuration for the state. It holds the values (in this
|
||||
// case the title) provided by the parent (in this case the App widget) and
|
||||
// used by the build method of the State. Fields in a Widget subclass are
|
||||
// always marked "final".
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
|
||||
void _incrementCounter() {
|
||||
setState(() {
|
||||
// This call to setState tells the Flutter framework that something has
|
||||
// changed in this State, which causes it to rerun the build method below
|
||||
// so that the display can reflect the updated values. If we changed
|
||||
// _counter without calling setState(), then the build method would not be
|
||||
// called again, and so nothing would appear to happen.
|
||||
_counter++;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This method is rerun every time setState is called, for instance as done
|
||||
// by the _incrementCounter method above.
|
||||
//
|
||||
// The Flutter framework has been optimized to make rerunning build methods
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// TRY THIS: Try changing the color here to a specific color (to
|
||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||
// change color while the other colors stay the same.
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: Center(
|
||||
// Center is a layout widget. It takes a single child and positions it
|
||||
// in the middle of the parent.
|
||||
child: Column(
|
||||
// Column is also a layout widget. It takes a list of children and
|
||||
// arranges them vertically. By default, it sizes itself to fit its
|
||||
// children horizontally, and tries to be as tall as its parent.
|
||||
//
|
||||
// Column has various properties to control how it sizes itself and
|
||||
// how it positions its children. Here we use mainAxisAlignment to
|
||||
// center the children vertically; the main axis here is the vertical
|
||||
// axis because Columns are vertical (the cross axis would be
|
||||
// horizontal).
|
||||
//
|
||||
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
|
||||
// action in the IDE, or press "p" in the console), to see the
|
||||
// wireframe for each widget.
|
||||
mainAxisAlignment: .center,
|
||||
children: [
|
||||
const Text('You have pushed the button this many times:'),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _incrementCounter,
|
||||
tooltip: 'Increment',
|
||||
child: const Icon(Icons.add),
|
||||
runApp(
|
||||
KcAppScope<MobileAppServices>(
|
||||
services: services,
|
||||
config: effectiveConfig,
|
||||
child: const KellMobileApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../dashboard/application/dashboard_controller.dart';
|
||||
import '../dashboard/domain/dashboard_summary.dart';
|
||||
|
||||
/// A mobile-optimized dashboard page showing aggregated summary data.
|
||||
///
|
||||
/// Uses a single-column scrollable layout suitable for smaller screens.
|
||||
class MobileDashboardPage extends StatefulWidget {
|
||||
final DashboardController controller;
|
||||
|
||||
const MobileDashboardPage({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
State<MobileDashboardPage> createState() => _MobileDashboardPageState();
|
||||
}
|
||||
|
||||
class _MobileDashboardPageState extends State<MobileDashboardPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(_onControllerChanged);
|
||||
widget.controller.load();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MobileDashboardPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller.removeListener(_onControllerChanged);
|
||||
widget.controller.addListener(_onControllerChanged);
|
||||
widget.controller.load();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_onControllerChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onControllerChanged() => setState(() {});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = widget.controller;
|
||||
|
||||
if (controller.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.error != null) {
|
||||
return Center(
|
||||
child: Text('Failed to load dashboard data.', style: Theme.of(context).textTheme.bodyLarge),
|
||||
);
|
||||
}
|
||||
|
||||
final summary = controller.summary;
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(KcSpacing.md),
|
||||
children: [
|
||||
const KcSectionHeader(title: 'Overview'),
|
||||
const SizedBox(height: KcSpacing.sm),
|
||||
_buildSummaryGrid(context, summary),
|
||||
const SizedBox(height: KcSpacing.xl),
|
||||
const KcSectionHeader(title: 'Recent Activity'),
|
||||
const SizedBox(height: KcSpacing.sm),
|
||||
const KcEmptyState(
|
||||
icon: Icons.history,
|
||||
message:
|
||||
'No recent activity yet.\nActivity will appear here once orders and updates are tracked.',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryGrid(BuildContext context, DashboardSummary summary) {
|
||||
final cards = [
|
||||
KcSummaryCard(
|
||||
icon: Icons.inventory_2,
|
||||
iconColor: KcColors.denimBlue,
|
||||
label: 'Total Products',
|
||||
value: '${summary.totalProducts}',
|
||||
),
|
||||
KcSummaryCard(
|
||||
icon: Icons.check_circle_outline,
|
||||
iconColor: KcColors.success,
|
||||
label: 'In Stock',
|
||||
value: '${summary.inStock}',
|
||||
),
|
||||
KcSummaryCard(
|
||||
icon: Icons.warning_amber_rounded,
|
||||
iconColor: KcColors.warning,
|
||||
label: 'Low Stock',
|
||||
value: '${summary.lowStock}',
|
||||
),
|
||||
KcSummaryCard(
|
||||
icon: Icons.edit_note,
|
||||
iconColor: KcColors.neutral,
|
||||
label: 'Draft',
|
||||
value: '${summary.draftProducts}',
|
||||
),
|
||||
KcSummaryCard(
|
||||
icon: Icons.receipt_long,
|
||||
iconColor: KcColors.denimBlue,
|
||||
label: 'Total Orders',
|
||||
value: '${summary.totalOrders}',
|
||||
),
|
||||
KcSummaryCard(
|
||||
icon: Icons.hourglass_empty,
|
||||
iconColor: KcColors.warning,
|
||||
label: 'Pending Orders',
|
||||
value: '${summary.pendingOrders}',
|
||||
),
|
||||
KcSummaryCard(
|
||||
icon: Icons.local_shipping_outlined,
|
||||
iconColor: KcColors.success,
|
||||
label: 'Active Orders',
|
||||
value: '${summary.activeOrders}',
|
||||
),
|
||||
KcSummaryCard(
|
||||
icon: Icons.attach_money,
|
||||
iconColor: KcColors.success,
|
||||
label: 'Revenue',
|
||||
value: '\$${summary.deliveredRevenue.toStringAsFixed(2)}',
|
||||
),
|
||||
];
|
||||
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: KcSpacing.sm,
|
||||
mainAxisSpacing: KcSpacing.sm,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
childAspectRatio: 1.6,
|
||||
children: cards,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class FinancePlaceholderPage extends StatelessWidget {
|
||||
const FinancePlaceholderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(child: Text('Finance page coming soon'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class IntegrationsPlaceholderPage extends StatelessWidget {
|
||||
const IntegrationsPlaceholderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(child: Text('Integrations page coming soon'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
import 'package:core/core.dart';
|
||||
import 'package:feature_inventory/feature_inventory.dart';
|
||||
import 'package:feature_orders/feature_orders.dart';
|
||||
import 'package:feature_policy/feature_policy.dart';
|
||||
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../composition/mobile_app_services.dart';
|
||||
import '../dashboard/application/dashboard_controller.dart';
|
||||
import '../dashboard/application/get_dashboard_summary.dart';
|
||||
import '../pages/dashboard_page.dart';
|
||||
import '../pages/finance_placeholder_page.dart';
|
||||
import '../pages/integrations_placeholder_page.dart';
|
||||
|
||||
/// The main shell for the mobile app.
|
||||
///
|
||||
/// Uses a [Scaffold] with a [NavigationBar] (Material 3 bottom navigation)
|
||||
/// to provide top-level section navigation. This is the mobile equivalent
|
||||
/// of `kell_web`'s [AppShell] which uses a [NavigationRail].
|
||||
///
|
||||
/// Unlike the web app which uses named routes, the mobile shell uses
|
||||
/// index-based tab switching with a stateful body to preserve tab state.
|
||||
class MobileShell extends StatefulWidget {
|
||||
const MobileShell({super.key});
|
||||
|
||||
@override
|
||||
State<MobileShell> createState() => _MobileShellState();
|
||||
}
|
||||
|
||||
class _MobileShellState extends State<MobileShell> {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
static const _titles = ['Dashboard', 'Inventory', 'Products', 'Orders', 'More'];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final config = KcAppScope.configOf<MobileAppServices>(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_titles[_selectedIndex]),
|
||||
actions: [
|
||||
_EnvironmentBadge(environment: config.environment),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
),
|
||||
body: _buildBody(context),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: _selectedIndex,
|
||||
onDestinationSelected: (index) {
|
||||
setState(() => _selectedIndex = index);
|
||||
},
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.dashboard_outlined),
|
||||
selectedIcon: Icon(Icons.dashboard),
|
||||
label: 'Dashboard',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.inventory_2_outlined),
|
||||
selectedIcon: Icon(Icons.inventory_2),
|
||||
label: 'Inventory',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.sell_outlined),
|
||||
selectedIcon: Icon(Icons.sell),
|
||||
label: 'Products',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.receipt_long_outlined),
|
||||
selectedIcon: Icon(Icons.receipt_long),
|
||||
label: 'Orders',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.more_horiz),
|
||||
selectedIcon: Icon(Icons.more_horiz),
|
||||
label: 'More',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context) {
|
||||
final services = KcAppScope.of<MobileAppServices>(context);
|
||||
|
||||
switch (_selectedIndex) {
|
||||
case 0:
|
||||
return MobileDashboardPage(
|
||||
controller: DashboardController(
|
||||
GetDashboardSummary(
|
||||
inventoryRepository: services.inventoryRepository,
|
||||
productPublishingRepository: services.productPublishingRepository,
|
||||
ordersRepository: services.ordersRepository,
|
||||
),
|
||||
),
|
||||
);
|
||||
case 1:
|
||||
return InventoryPage(
|
||||
repository: services.inventoryRepository,
|
||||
onViewProduct: (_) {
|
||||
// Cross-feature nav: switch to Products tab.
|
||||
setState(() => _selectedIndex = 2);
|
||||
},
|
||||
);
|
||||
case 2:
|
||||
return ProductPublishingPage(
|
||||
repository: services.productPublishingRepository,
|
||||
onViewPolicy: () {
|
||||
// Cross-feature nav: not directly reachable from bottom nav,
|
||||
// but we can show it as a placeholder for now.
|
||||
},
|
||||
);
|
||||
case 3:
|
||||
return OrdersPage(
|
||||
repository: services.ordersRepository,
|
||||
onViewProduct: (_) {
|
||||
setState(() => _selectedIndex = 2);
|
||||
},
|
||||
onViewInventory: (_) {
|
||||
setState(() => _selectedIndex = 1);
|
||||
},
|
||||
);
|
||||
case 4:
|
||||
return const _MorePage();
|
||||
default:
|
||||
return const Center(child: Text('Unknown section'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple "More" page that provides access to less frequently used sections.
|
||||
class _MorePage extends StatelessWidget {
|
||||
const _MorePage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.attach_money),
|
||||
title: const Text('Finance'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => Scaffold(
|
||||
appBar: AppBar(title: const Text('Finance')),
|
||||
body: const FinancePlaceholderPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.policy),
|
||||
title: const Text('Policy'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
final services = KcAppScope.of<MobileAppServices>(context);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => Scaffold(
|
||||
appBar: AppBar(title: const Text('Policy')),
|
||||
body: PolicyPage(
|
||||
repository: services.policyRepository,
|
||||
onViewRelatedPage: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.hub),
|
||||
title: const Text('Integrations'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => Scaffold(
|
||||
appBar: AppBar(title: const Text('Integrations')),
|
||||
body: const IntegrationsPlaceholderPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A small coloured chip displayed in the [AppBar] that shows the current
|
||||
/// runtime environment (e.g. "FAKE" or "WP").
|
||||
class _EnvironmentBadge extends StatelessWidget {
|
||||
final KcAppEnvironment environment;
|
||||
|
||||
const _EnvironmentBadge({required this.environment});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color backgroundColor;
|
||||
final Color foregroundColor;
|
||||
|
||||
switch (environment) {
|
||||
case KcAppEnvironment.fake:
|
||||
backgroundColor = Colors.orange.shade100;
|
||||
foregroundColor = Colors.orange.shade900;
|
||||
case KcAppEnvironment.wordpress:
|
||||
backgroundColor = Colors.green.shade100;
|
||||
foregroundColor = Colors.green.shade900;
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(4)),
|
||||
child: Text(
|
||||
environment.label,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: foregroundColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,13 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../packages/core"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -49,6 +56,13 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
design_system:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../packages/design_system"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -57,6 +71,34 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
feature_inventory:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../packages/feature_inventory"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
feature_orders:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../packages/feature_orders"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
feature_policy:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../packages/feature_policy"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
feature_wordpress:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../packages/feature_wordpress"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
|
@ -75,6 +117,22 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -192,6 +250,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -208,6 +274,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
sdks:
|
||||
dart: ">=3.11.4 <4.0.0"
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
|
|
|
|||
|
|
@ -1,89 +1,36 @@
|
|||
name: kell_mobile
|
||||
description: "A new Flutter project."
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
description: "Kell Creations mobile operations platform."
|
||||
publish_to: "none"
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
# followed by an optional build number separated by a +.
|
||||
# Both the version and the builder number may be overridden in flutter
|
||||
# build by specifying --build-name and --build-number, respectively.
|
||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
sdk: ^3.11.0
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||
# dependencies can be manually updated by changing the version numbers below to
|
||||
# the latest version available on pub.dev. To see which dependencies have newer
|
||||
# versions available, run `flutter pub outdated`.
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
|
||||
core:
|
||||
path: ../../packages/core
|
||||
design_system:
|
||||
path: ../../packages/design_system
|
||||
feature_inventory:
|
||||
path: ../../packages/feature_inventory
|
||||
feature_orders:
|
||||
path: ../../packages/feature_orders
|
||||
feature_policy:
|
||||
path: ../../packages/feature_policy
|
||||
feature_wordpress:
|
||||
path: ../../packages/feature_wordpress
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/to/asset-from-package
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/to/font-from-package
|
||||
|
|
|
|||
|
|
@ -1,30 +1,77 @@
|
|||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:core/core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kell_mobile/app.dart';
|
||||
import 'package:kell_mobile/composition/mobile_app_services.dart';
|
||||
|
||||
import 'package:kell_mobile/main.dart';
|
||||
Widget _buildTestApp() {
|
||||
const config = KcAppConfig(
|
||||
environment: KcAppEnvironment.fake,
|
||||
wcSiteUrl: '',
|
||||
wcConsumerKey: '',
|
||||
wcConsumerSecret: '',
|
||||
);
|
||||
return KcAppScope<MobileAppServices>(
|
||||
services: MobileAppServices.fake(),
|
||||
config: config,
|
||||
child: const KellMobileApp(),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
testWidgets('mobile shell loads with dashboard tab', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
expect(find.text('Dashboard'), findsWidgets);
|
||||
});
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
testWidgets('dashboard shows summary cards after loading', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
expect(find.text('Overview'), findsOneWidget);
|
||||
expect(find.text('Total Products'), findsOneWidget);
|
||||
expect(find.text('In Stock'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('environment badge shows FAKE in fake mode', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('FAKE'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('bottom navigation bar has 5 destinations', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(NavigationBar), findsOneWidget);
|
||||
expect(find.byType(NavigationDestination), findsNWidgets(5));
|
||||
});
|
||||
|
||||
testWidgets('tapping Inventory tab switches content', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap the Inventory destination
|
||||
await tester.tap(find.text('Inventory').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The app bar title should change
|
||||
expect(find.text('Inventory'), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('tapping More tab shows additional sections', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap the More destination
|
||||
await tester.tap(find.text('More').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Finance'), findsOneWidget);
|
||||
expect(find.text('Policy'), findsOneWidget);
|
||||
expect(find.text('Integrations'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,5 @@ packages:
|
|||
source: hosted
|
||||
version: "1.1.1"
|
||||
sdks:
|
||||
dart: ">=3.11.4 <4.0.0"
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
sdk: ^3.11.0
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ version: 0.0.1
|
|||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
sdk: ^3.11.0
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ version: 0.0.1
|
|||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
sdk: ^3.11.0
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ version: 0.0.1
|
|||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
sdk: ^3.11.0
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ version: 0.0.1
|
|||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
sdk: ^3.11.0
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ version: 0.0.1
|
|||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
sdk: ^3.11.0
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ publish_to: "none"
|
|||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
sdk: ^3.11.0
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ version: 0.0.1
|
|||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
sdk: ^3.11.0
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ publish_to: "none"
|
|||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
sdk: ^3.11.0
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ publish_to: "none"
|
|||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
sdk: ^3.11.0
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ version: 0.0.1
|
|||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
sdk: ^3.11.0
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ publish_to: "none"
|
|||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
sdk: ^3.11.0
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ version: 0.0.1
|
|||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
sdk: ^3.11.0
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Reference in New Issue