Add Kell Creations operations app foundation with feature slices and WooCommerce read-only integration #1
|
|
@ -0,0 +1,115 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../routing/app_routes.dart';
|
||||
import 'navigation_target.dart';
|
||||
|
||||
/// App-level navigation helpers for cross-feature handoffs.
|
||||
///
|
||||
/// All navigation logic lives here in the app layer so that feature packages
|
||||
/// never depend on each other's routes. Feature pages receive plain
|
||||
/// `void Function(String)` callbacks that they invoke with a SKU or ID;
|
||||
/// the wiring in [AppRoutes.onGenerateRoute] closes over these helpers.
|
||||
abstract final class AppNavigation {
|
||||
// ── Generic ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Navigate to a [NavigationTarget], replacing the current route.
|
||||
static void navigateTo(BuildContext context, NavigationTarget target) {
|
||||
Navigator.of(context).pushReplacementNamed(target.route, arguments: target.arguments);
|
||||
}
|
||||
|
||||
// ── Dashboard → feature handoffs ──────────────────────────────────────
|
||||
|
||||
/// Dashboard KPI "Total Products" / "In Stock" → Inventory page.
|
||||
static void dashboardToInventory(BuildContext context, {String? filter}) {
|
||||
navigateTo(
|
||||
context,
|
||||
NavigationTarget(route: AppRoutes.inventory, arguments: {'filter': ?filter}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Dashboard KPI "Draft" → Products page.
|
||||
static void dashboardToProducts(BuildContext context, {String? filter}) {
|
||||
navigateTo(
|
||||
context,
|
||||
NavigationTarget(route: AppRoutes.products, arguments: {'filter': ?filter}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Dashboard KPI "Total Orders" / "Pending" / "Active" → Orders page.
|
||||
static void dashboardToOrders(BuildContext context, {String? filter}) {
|
||||
navigateTo(context, NavigationTarget(route: AppRoutes.orders, arguments: {'filter': ?filter}));
|
||||
}
|
||||
|
||||
/// Dashboard KPI "Revenue" → Finance page.
|
||||
static void dashboardToFinance(BuildContext context) {
|
||||
navigateTo(context, NavigationTarget(route: AppRoutes.finance));
|
||||
}
|
||||
|
||||
// ── Policy detail → operational page handoffs ─────────────────────────
|
||||
|
||||
/// Policy detail action → related operational page based on category.
|
||||
static void policyToRelatedPage(BuildContext context, {required String category}) {
|
||||
final route = _categoryToRoute(category);
|
||||
navigateTo(
|
||||
context,
|
||||
NavigationTarget(route: route, arguments: {'fromPolicy': 'true', 'category': category}),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Orders → Products / Inventory ─────────────────────────────────────
|
||||
|
||||
/// Order line-item SKU → Products page, pre-selecting that SKU.
|
||||
static void orderToProduct(BuildContext context, {required String sku}) {
|
||||
navigateTo(
|
||||
context,
|
||||
NavigationTarget(route: AppRoutes.products, arguments: {'selectedSku': sku}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Order line-item SKU → Inventory page, pre-selecting that SKU.
|
||||
static void orderToInventory(BuildContext context, {required String sku}) {
|
||||
navigateTo(
|
||||
context,
|
||||
NavigationTarget(route: AppRoutes.inventory, arguments: {'selectedSku': sku}),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Products → Policy ─────────────────────────────────────────────────
|
||||
|
||||
/// Product draft → Policy page filtered to "Product Compliance".
|
||||
static void productToPolicy(BuildContext context) {
|
||||
navigateTo(
|
||||
context,
|
||||
NavigationTarget(route: AppRoutes.policy, arguments: {'category': 'Product Compliance'}),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Inventory → Products ──────────────────────────────────────────────
|
||||
|
||||
/// Inventory item → Products page, pre-selecting by SKU.
|
||||
static void inventoryToProduct(BuildContext context, {required String sku}) {
|
||||
navigateTo(
|
||||
context,
|
||||
NavigationTarget(route: AppRoutes.products, arguments: {'selectedSku': sku}),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
static String _categoryToRoute(String category) {
|
||||
switch (category) {
|
||||
case 'Product Compliance':
|
||||
return AppRoutes.products;
|
||||
case 'Inventory Governance':
|
||||
return AppRoutes.inventory;
|
||||
case 'Order Operations':
|
||||
return AppRoutes.orders;
|
||||
case 'Finance & Tax':
|
||||
return AppRoutes.finance;
|
||||
case 'Customer Policy':
|
||||
return AppRoutes.orders;
|
||||
default:
|
||||
return AppRoutes.dashboard;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/// A lightweight value object describing a cross-feature navigation handoff.
|
||||
///
|
||||
/// Carries the destination route and optional arguments (e.g. a selected-item
|
||||
/// ID or a preset filter) so the target page can open in the right state.
|
||||
///
|
||||
/// This lives in the app layer (`kell_web`) and is never imported by feature
|
||||
/// packages, keeping package boundaries strict.
|
||||
class NavigationTarget {
|
||||
/// The route path to navigate to (one of [AppRoutes] constants).
|
||||
final String route;
|
||||
|
||||
/// Optional key-value arguments for the destination page.
|
||||
///
|
||||
/// Common keys:
|
||||
/// - `selectedSku` – pre-select an item by SKU on the target page
|
||||
/// - `selectedId` – pre-select an item by ID on the target page
|
||||
/// - `filter` – apply a preset filter (e.g. `'lowStock'`)
|
||||
/// - `category` – filter by policy category
|
||||
final Map<String, String> arguments;
|
||||
|
||||
const NavigationTarget({required this.route, this.arguments = const {}});
|
||||
|
||||
/// Convenience: the selected SKU, if any.
|
||||
String? get selectedSku => arguments['selectedSku'];
|
||||
|
||||
/// Convenience: the selected ID, if any.
|
||||
String? get selectedId => arguments['selectedId'];
|
||||
|
||||
/// Convenience: a preset filter, if any.
|
||||
String? get filter => arguments['filter'];
|
||||
|
||||
/// Convenience: a category filter, if any.
|
||||
String? get category => arguments['category'];
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is NavigationTarget && route == other.route && _mapsEqual(arguments, other.arguments);
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
// Build a stable hash from sorted keys to ensure consistency.
|
||||
var argsHash = 0;
|
||||
final sortedKeys = arguments.keys.toList()..sort();
|
||||
for (final key in sortedKeys) {
|
||||
argsHash = argsHash ^ Object.hash(key, arguments[key]);
|
||||
}
|
||||
return Object.hash(route, argsHash);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'NavigationTarget(route: $route, arguments: $arguments)';
|
||||
|
||||
static bool _mapsEqual(Map<String, String> a, Map<String, String> b) {
|
||||
if (a.length != b.length) return false;
|
||||
for (final key in a.keys) {
|
||||
if (a[key] != b[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import '../dashboard/application/dashboard_controller.dart';
|
||||
import '../dashboard/domain/dashboard_summary.dart';
|
||||
import '../navigation/app_navigation.dart';
|
||||
import '../routing/app_routes.dart';
|
||||
import '../shell/widgets/empty_state_panel.dart';
|
||||
import '../shell/widgets/section_header.dart';
|
||||
|
|
@ -112,48 +113,56 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
iconColor: KcColors.denimBlue,
|
||||
label: 'Total Products',
|
||||
value: '${summary.totalProducts}',
|
||||
onTap: () => AppNavigation.dashboardToInventory(context),
|
||||
),
|
||||
SummaryCard(
|
||||
icon: Icons.check_circle_outline,
|
||||
iconColor: KcColors.success,
|
||||
label: 'In Stock',
|
||||
value: '${summary.inStock}',
|
||||
onTap: () => AppNavigation.dashboardToInventory(context, filter: 'inStock'),
|
||||
),
|
||||
SummaryCard(
|
||||
icon: Icons.warning_amber_rounded,
|
||||
iconColor: KcColors.warning,
|
||||
label: 'Low Stock',
|
||||
value: '${summary.lowStock}',
|
||||
onTap: () => AppNavigation.dashboardToInventory(context, filter: 'lowStock'),
|
||||
),
|
||||
SummaryCard(
|
||||
icon: Icons.edit_note,
|
||||
iconColor: KcColors.neutral,
|
||||
label: 'Draft',
|
||||
value: '${summary.draftProducts}',
|
||||
onTap: () => AppNavigation.dashboardToProducts(context, filter: 'draft'),
|
||||
),
|
||||
SummaryCard(
|
||||
icon: Icons.receipt_long,
|
||||
iconColor: KcColors.denimBlue,
|
||||
label: 'Total Orders',
|
||||
value: '${summary.totalOrders}',
|
||||
onTap: () => AppNavigation.dashboardToOrders(context),
|
||||
),
|
||||
SummaryCard(
|
||||
icon: Icons.hourglass_empty,
|
||||
iconColor: KcColors.warning,
|
||||
label: 'Pending Orders',
|
||||
value: '${summary.pendingOrders}',
|
||||
onTap: () => AppNavigation.dashboardToOrders(context, filter: 'pending'),
|
||||
),
|
||||
SummaryCard(
|
||||
icon: Icons.local_shipping_outlined,
|
||||
iconColor: KcColors.success,
|
||||
label: 'Active Orders',
|
||||
value: '${summary.activeOrders}',
|
||||
onTap: () => AppNavigation.dashboardToOrders(context, filter: 'active'),
|
||||
),
|
||||
SummaryCard(
|
||||
icon: Icons.attach_money,
|
||||
iconColor: KcColors.success,
|
||||
label: 'Revenue',
|
||||
value: '\$${summary.deliveredRevenue.toStringAsFixed(2)}',
|
||||
onTap: () => AppNavigation.dashboardToFinance(context),
|
||||
),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||
import '../composition/app_scope.dart';
|
||||
import '../dashboard/application/dashboard_controller.dart';
|
||||
import '../dashboard/application/get_dashboard_summary.dart';
|
||||
import '../navigation/app_navigation.dart';
|
||||
import '../pages/dashboard_page.dart';
|
||||
import '../pages/finance_placeholder_page.dart';
|
||||
import '../pages/integrations_placeholder_page.dart';
|
||||
|
|
@ -45,7 +46,10 @@ abstract final class AppRoutes {
|
|||
(context) => AppShell(
|
||||
selectedRoute: inventory,
|
||||
title: 'Inventory',
|
||||
child: InventoryPage(repository: AppScope.of(context).inventoryRepository),
|
||||
child: InventoryPage(
|
||||
repository: AppScope.of(context).inventoryRepository,
|
||||
onViewProduct: (sku) => AppNavigation.inventoryToProduct(context, sku: sku),
|
||||
),
|
||||
),
|
||||
);
|
||||
case products:
|
||||
|
|
@ -56,6 +60,7 @@ abstract final class AppRoutes {
|
|||
title: 'Products',
|
||||
child: ProductPublishingPage(
|
||||
repository: AppScope.of(context).productPublishingRepository,
|
||||
onViewPolicy: () => AppNavigation.productToPolicy(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -65,7 +70,11 @@ abstract final class AppRoutes {
|
|||
(context) => AppShell(
|
||||
selectedRoute: orders,
|
||||
title: 'Orders',
|
||||
child: OrdersPage(repository: AppScope.of(context).ordersRepository),
|
||||
child: OrdersPage(
|
||||
repository: AppScope.of(context).ordersRepository,
|
||||
onViewProduct: (sku) => AppNavigation.orderToProduct(context, sku: sku),
|
||||
onViewInventory: (sku) => AppNavigation.orderToInventory(context, sku: sku),
|
||||
),
|
||||
),
|
||||
);
|
||||
case finance:
|
||||
|
|
@ -83,7 +92,11 @@ abstract final class AppRoutes {
|
|||
(context) => AppShell(
|
||||
selectedRoute: policy,
|
||||
title: 'Policy',
|
||||
child: PolicyPage(repository: AppScope.of(context).policyRepository),
|
||||
child: PolicyPage(
|
||||
repository: AppScope.of(context).policyRepository,
|
||||
onViewRelatedPage: (category) =>
|
||||
AppNavigation.policyToRelatedPage(context, category: category),
|
||||
),
|
||||
),
|
||||
);
|
||||
case integrations:
|
||||
|
|
@ -101,7 +114,10 @@ abstract final class AppRoutes {
|
|||
(context) => AppShell(
|
||||
selectedRoute: inventory,
|
||||
title: 'Inventory',
|
||||
child: InventoryPage(repository: AppScope.of(context).inventoryRepository),
|
||||
child: InventoryPage(
|
||||
repository: AppScope.of(context).inventoryRepository,
|
||||
onViewProduct: (sku) => AppNavigation.inventoryToProduct(context, sku: sku),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,26 +4,31 @@ import 'package:flutter/material.dart';
|
|||
/// A stat / summary card that displays a [value] with a [label] and an [icon].
|
||||
///
|
||||
/// Used on the dashboard to show high-level KPIs such as total products,
|
||||
/// in-stock count, etc.
|
||||
/// in-stock count, etc. Optionally tappable via [onTap] to navigate to a
|
||||
/// related page.
|
||||
class SummaryCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color iconColor;
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
/// Optional tap handler for cross-feature navigation from dashboard KPIs.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const SummaryCard({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.iconColor,
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return KcCard(
|
||||
final card = KcCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -36,5 +41,12 @@ class SummaryCard extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (onTap == null) return card;
|
||||
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(onTap: onTap, child: card),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kell_web/navigation/navigation_target.dart';
|
||||
|
||||
void main() {
|
||||
group('NavigationTarget', () {
|
||||
test('creates with route and empty arguments by default', () {
|
||||
const target = NavigationTarget(route: '/inventory');
|
||||
expect(target.route, '/inventory');
|
||||
expect(target.arguments, isEmpty);
|
||||
});
|
||||
|
||||
test('creates with route and arguments', () {
|
||||
const target = NavigationTarget(route: '/products', arguments: {'selectedSku': 'ABC-123'});
|
||||
expect(target.route, '/products');
|
||||
expect(target.arguments, {'selectedSku': 'ABC-123'});
|
||||
});
|
||||
|
||||
test('convenience getters return correct values', () {
|
||||
const target = NavigationTarget(
|
||||
route: '/inventory',
|
||||
arguments: {
|
||||
'selectedSku': 'SKU-001',
|
||||
'selectedId': 'ID-001',
|
||||
'filter': 'lowStock',
|
||||
'category': 'Product Compliance',
|
||||
},
|
||||
);
|
||||
expect(target.selectedSku, 'SKU-001');
|
||||
expect(target.selectedId, 'ID-001');
|
||||
expect(target.filter, 'lowStock');
|
||||
expect(target.category, 'Product Compliance');
|
||||
});
|
||||
|
||||
test('convenience getters return null when key is absent', () {
|
||||
const target = NavigationTarget(route: '/orders');
|
||||
expect(target.selectedSku, isNull);
|
||||
expect(target.selectedId, isNull);
|
||||
expect(target.filter, isNull);
|
||||
expect(target.category, isNull);
|
||||
});
|
||||
|
||||
test('equality works for same route and arguments', () {
|
||||
const a = NavigationTarget(route: '/products', arguments: {'selectedSku': 'X'});
|
||||
const b = NavigationTarget(route: '/products', arguments: {'selectedSku': 'X'});
|
||||
expect(a, equals(b));
|
||||
expect(a.hashCode, equals(b.hashCode));
|
||||
});
|
||||
|
||||
test('inequality for different routes', () {
|
||||
const a = NavigationTarget(route: '/products');
|
||||
const b = NavigationTarget(route: '/orders');
|
||||
expect(a, isNot(equals(b)));
|
||||
});
|
||||
|
||||
test('inequality for different arguments', () {
|
||||
const a = NavigationTarget(route: '/products', arguments: {'selectedSku': 'X'});
|
||||
const b = NavigationTarget(route: '/products', arguments: {'selectedSku': 'Y'});
|
||||
expect(a, isNot(equals(b)));
|
||||
});
|
||||
|
||||
test('toString includes route and arguments', () {
|
||||
const target = NavigationTarget(route: '/inventory', arguments: {'filter': 'lowStock'});
|
||||
expect(target.toString(), contains('/inventory'));
|
||||
expect(target.toString(), contains('lowStock'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -9,7 +9,11 @@ import 'widgets/inventory_item_card.dart';
|
|||
class InventoryPage extends StatefulWidget {
|
||||
final InventoryRepository repository;
|
||||
|
||||
const InventoryPage({super.key, required this.repository});
|
||||
/// Optional callback to navigate to the Products page for a given SKU.
|
||||
/// Provided by the app layer to enable cross-feature handoffs.
|
||||
final void Function(String sku)? onViewProduct;
|
||||
|
||||
const InventoryPage({super.key, required this.repository, this.onViewProduct});
|
||||
|
||||
@override
|
||||
State<InventoryPage> createState() => _InventoryPageState();
|
||||
|
|
@ -64,7 +68,13 @@ class _InventoryPageState extends State<InventoryPage> {
|
|||
childAspectRatio: 1.5,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return InventoryItemCard(item: controller.items[index]);
|
||||
final item = controller.items[index];
|
||||
return InventoryItemCard(
|
||||
item: item,
|
||||
onViewProduct: widget.onViewProduct != null
|
||||
? () => widget.onViewProduct!(item.sku)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import '../../domain/inventory_status.dart';
|
|||
class InventoryItemCard extends StatelessWidget {
|
||||
final InventoryItem item;
|
||||
|
||||
const InventoryItemCard({super.key, required this.item});
|
||||
/// Optional callback to navigate to the Products page for this item's SKU.
|
||||
final VoidCallback? onViewProduct;
|
||||
|
||||
const InventoryItemCard({super.key, required this.item, this.onViewProduct});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -25,6 +28,19 @@ class InventoryItemCard extends StatelessWidget {
|
|||
Text('Quantity on hand: ${item.quantityOnHand}'),
|
||||
const SizedBox(height: KcSpacing.sm),
|
||||
Text('Unit price: \$${item.unitPrice.toStringAsFixed(2)}'),
|
||||
if (onViewProduct != null) ...[
|
||||
const SizedBox(height: KcSpacing.sm),
|
||||
GestureDetector(
|
||||
onTap: onViewProduct,
|
||||
child: Text(
|
||||
'View Product →',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: KcColors.denimBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -14,7 +14,13 @@ import 'widgets/order_detail_panel.dart';
|
|||
class OrdersPage extends StatefulWidget {
|
||||
final OrdersRepository repository;
|
||||
|
||||
const OrdersPage({super.key, required this.repository});
|
||||
/// Optional callback to navigate to the Products page for a given SKU.
|
||||
final void Function(String sku)? onViewProduct;
|
||||
|
||||
/// Optional callback to navigate to the Inventory page for a given SKU.
|
||||
final void Function(String sku)? onViewInventory;
|
||||
|
||||
const OrdersPage({super.key, required this.repository, this.onViewProduct, this.onViewInventory});
|
||||
|
||||
@override
|
||||
State<OrdersPage> createState() => _OrdersPageState();
|
||||
|
|
@ -93,6 +99,10 @@ class _OrdersPageState extends State<OrdersPage> {
|
|||
if (selected == null) {
|
||||
return const Center(child: Text('Select an order to view details'));
|
||||
}
|
||||
return OrderDetailPanel(order: selected);
|
||||
return OrderDetailPanel(
|
||||
order: selected,
|
||||
onViewProduct: widget.onViewProduct,
|
||||
onViewInventory: widget.onViewInventory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,18 @@ import 'order_status_chip.dart';
|
|||
class OrderDetailPanel extends StatelessWidget {
|
||||
final Order order;
|
||||
|
||||
const OrderDetailPanel({super.key, required this.order});
|
||||
/// Optional callback to navigate to the Products page for a given SKU.
|
||||
final void Function(String sku)? onViewProduct;
|
||||
|
||||
/// Optional callback to navigate to the Inventory page for a given SKU.
|
||||
final void Function(String sku)? onViewInventory;
|
||||
|
||||
const OrderDetailPanel({
|
||||
super.key,
|
||||
required this.order,
|
||||
this.onViewProduct,
|
||||
this.onViewInventory,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -63,6 +74,37 @@ class OrderDetailPanel extends StatelessWidget {
|
|||
'SKU: ${item.sku}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: KcColors.neutral),
|
||||
),
|
||||
if (onViewProduct != null || onViewInventory != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: KcSpacing.xs),
|
||||
child: Wrap(
|
||||
spacing: KcSpacing.sm,
|
||||
children: [
|
||||
if (onViewProduct != null)
|
||||
GestureDetector(
|
||||
onTap: () => onViewProduct!(item.sku),
|
||||
child: Text(
|
||||
'View Product',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: KcColors.denimBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (onViewInventory != null)
|
||||
GestureDetector(
|
||||
onTap: () => onViewInventory!(item.sku),
|
||||
child: Text(
|
||||
'View Inventory',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: KcColors.denimBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ import 'widgets/policy_detail_panel.dart';
|
|||
class PolicyPage extends StatefulWidget {
|
||||
final PolicyRepository repository;
|
||||
|
||||
const PolicyPage({super.key, required this.repository});
|
||||
/// Optional callback to navigate to a related operational page based on
|
||||
/// the policy check's category. Provided by the app layer.
|
||||
final void Function(String category)? onViewRelatedPage;
|
||||
|
||||
const PolicyPage({super.key, required this.repository, this.onViewRelatedPage});
|
||||
|
||||
@override
|
||||
State<PolicyPage> createState() => _PolicyPageState();
|
||||
|
|
@ -93,6 +97,6 @@ class _PolicyPageState extends State<PolicyPage> {
|
|||
if (selected == null) {
|
||||
return const Center(child: Text('Select a policy check to view details'));
|
||||
}
|
||||
return PolicyDetailPanel(check: selected);
|
||||
return PolicyDetailPanel(check: selected, onViewRelatedPage: widget.onViewRelatedPage);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ import 'governance_status_chip.dart';
|
|||
class PolicyDetailPanel extends StatelessWidget {
|
||||
final PolicyCheckResult check;
|
||||
|
||||
const PolicyDetailPanel({super.key, required this.check});
|
||||
/// Optional callback to navigate to the related operational page
|
||||
/// based on the check's category.
|
||||
final void Function(String category)? onViewRelatedPage;
|
||||
|
||||
const PolicyDetailPanel({super.key, required this.check, this.onViewRelatedPage});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -49,6 +53,21 @@ class PolicyDetailPanel extends StatelessWidget {
|
|||
Text('Detail', style: theme.textTheme.titleLarge),
|
||||
const SizedBox(height: KcSpacing.sm),
|
||||
Text(check.detail, style: theme.textTheme.bodyMedium),
|
||||
|
||||
// ── Related page link ──────────────────────────────────────
|
||||
if (onViewRelatedPage != null) ...[
|
||||
const SizedBox(height: KcSpacing.lg),
|
||||
GestureDetector(
|
||||
onTap: () => onViewRelatedPage!(check.category),
|
||||
child: Text(
|
||||
'Go to ${check.category} →',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: KcColors.denimBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,11 @@ import 'widgets/product_preview_panel.dart';
|
|||
class ProductPublishingPage extends StatefulWidget {
|
||||
final ProductPublishingRepository repository;
|
||||
|
||||
const ProductPublishingPage({super.key, required this.repository});
|
||||
/// Optional callback to navigate to the Policy page.
|
||||
/// Provided by the app layer to enable cross-feature handoffs.
|
||||
final VoidCallback? onViewPolicy;
|
||||
|
||||
const ProductPublishingPage({super.key, required this.repository, this.onViewPolicy});
|
||||
|
||||
@override
|
||||
State<ProductPublishingPage> createState() => _ProductPublishingPageState();
|
||||
|
|
@ -95,6 +99,10 @@ class _ProductPublishingPageState extends State<ProductPublishingPage> {
|
|||
if (selected == null) {
|
||||
return const Center(child: Text('Select a product draft to preview'));
|
||||
}
|
||||
return ProductPreviewPanel(draft: selected, onPublish: () => controller.publish(selected.id));
|
||||
return ProductPreviewPanel(
|
||||
draft: selected,
|
||||
onPublish: () => controller.publish(selected.id),
|
||||
onViewPolicy: widget.onViewPolicy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ class ProductPreviewPanel extends StatelessWidget {
|
|||
final ProductDraft draft;
|
||||
final VoidCallback? onPublish;
|
||||
|
||||
const ProductPreviewPanel({super.key, required this.draft, this.onPublish});
|
||||
/// Optional callback to navigate to the Policy page.
|
||||
final VoidCallback? onViewPolicy;
|
||||
|
||||
const ProductPreviewPanel({super.key, required this.draft, this.onPublish, this.onViewPolicy});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -66,6 +69,21 @@ class ProductPreviewPanel extends StatelessWidget {
|
|||
Text(draft.description, style: theme.textTheme.bodyLarge),
|
||||
const SizedBox(height: KcSpacing.xl),
|
||||
|
||||
// ── Policy link ────────────────────────────────────────────
|
||||
if (onViewPolicy != null) ...[
|
||||
GestureDetector(
|
||||
onTap: onViewPolicy,
|
||||
child: Text(
|
||||
'View Compliance Policy →',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: KcColors.denimBlue,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: KcSpacing.md),
|
||||
],
|
||||
|
||||
// ── Publish button ─────────────────────────────────────────
|
||||
if (draft.status != PublishStatus.published)
|
||||
SizedBox(
|
||||
|
|
|
|||
Loading…
Reference in New Issue