diff --git a/kell_creations_apps/apps/kell_web/lib/navigation/app_navigation.dart b/kell_creations_apps/apps/kell_web/lib/navigation/app_navigation.dart new file mode 100644 index 0000000..b5ba20f --- /dev/null +++ b/kell_creations_apps/apps/kell_web/lib/navigation/app_navigation.dart @@ -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; + } + } +} diff --git a/kell_creations_apps/apps/kell_web/lib/navigation/navigation_target.dart b/kell_creations_apps/apps/kell_web/lib/navigation/navigation_target.dart new file mode 100644 index 0000000..0d2c318 --- /dev/null +++ b/kell_creations_apps/apps/kell_web/lib/navigation/navigation_target.dart @@ -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 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 a, Map b) { + if (a.length != b.length) return false; + for (final key in a.keys) { + if (a[key] != b[key]) return false; + } + return true; + } +} diff --git a/kell_creations_apps/apps/kell_web/lib/pages/dashboard_page.dart b/kell_creations_apps/apps/kell_web/lib/pages/dashboard_page.dart index c0771b0..31c6962 100644 --- a/kell_creations_apps/apps/kell_web/lib/pages/dashboard_page.dart +++ b/kell_creations_apps/apps/kell_web/lib/pages/dashboard_page.dart @@ -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 { 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), ), ]; diff --git a/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart b/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart index 99a17b0..1cb2c19 100644 --- a/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart +++ b/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart @@ -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), + ), ), ); } diff --git a/kell_creations_apps/apps/kell_web/lib/shell/widgets/summary_card.dart b/kell_creations_apps/apps/kell_web/lib/shell/widgets/summary_card.dart index 06af4db..8b51dba 100644 --- a/kell_creations_apps/apps/kell_web/lib/shell/widgets/summary_card.dart +++ b/kell_creations_apps/apps/kell_web/lib/shell/widgets/summary_card.dart @@ -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), + ); } } diff --git a/kell_creations_apps/apps/kell_web/test/navigation/navigation_target_test.dart b/kell_creations_apps/apps/kell_web/test/navigation/navigation_target_test.dart new file mode 100644 index 0000000..7a404af --- /dev/null +++ b/kell_creations_apps/apps/kell_web/test/navigation/navigation_target_test.dart @@ -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')); + }); + }); +} diff --git a/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart index 8f80b11..fe8f79f 100644 --- a/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart +++ b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart @@ -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 createState() => _InventoryPageState(); @@ -64,7 +68,13 @@ class _InventoryPageState extends State { 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, + ); }, ); }, diff --git a/kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/inventory_item_card.dart b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/inventory_item_card.dart index 814d0e8..9f3ab5e 100644 --- a/kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/inventory_item_card.dart +++ b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/inventory_item_card.dart @@ -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, + ), + ), + ), + ], ], ), ); diff --git a/kell_creations_apps/packages/feature_orders/build/test_cache/build/b70f9274ac3b5ec9754be8774a3c9a1c.cache.dill.track.dill b/kell_creations_apps/packages/feature_orders/build/test_cache/build/b70f9274ac3b5ec9754be8774a3c9a1c.cache.dill.track.dill index f69ab61..e621673 100644 Binary files a/kell_creations_apps/packages/feature_orders/build/test_cache/build/b70f9274ac3b5ec9754be8774a3c9a1c.cache.dill.track.dill and b/kell_creations_apps/packages/feature_orders/build/test_cache/build/b70f9274ac3b5ec9754be8774a3c9a1c.cache.dill.track.dill differ diff --git a/kell_creations_apps/packages/feature_orders/lib/src/presentation/orders_page.dart b/kell_creations_apps/packages/feature_orders/lib/src/presentation/orders_page.dart index ae4995f..b19d735 100644 --- a/kell_creations_apps/packages/feature_orders/lib/src/presentation/orders_page.dart +++ b/kell_creations_apps/packages/feature_orders/lib/src/presentation/orders_page.dart @@ -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 createState() => _OrdersPageState(); @@ -93,6 +99,10 @@ class _OrdersPageState extends State { 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, + ); } } diff --git a/kell_creations_apps/packages/feature_orders/lib/src/presentation/widgets/order_detail_panel.dart b/kell_creations_apps/packages/feature_orders/lib/src/presentation/widgets/order_detail_panel.dart index 53b7756..45505d8 100644 --- a/kell_creations_apps/packages/feature_orders/lib/src/presentation/widgets/order_detail_panel.dart +++ b/kell_creations_apps/packages/feature_orders/lib/src/presentation/widgets/order_detail_panel.dart @@ -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, + ), + ), + ), + ], + ), + ), ], ), ), diff --git a/kell_creations_apps/packages/feature_policy/lib/src/presentation/policy_page.dart b/kell_creations_apps/packages/feature_policy/lib/src/presentation/policy_page.dart index 8b568f3..a957573 100644 --- a/kell_creations_apps/packages/feature_policy/lib/src/presentation/policy_page.dart +++ b/kell_creations_apps/packages/feature_policy/lib/src/presentation/policy_page.dart @@ -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 createState() => _PolicyPageState(); @@ -93,6 +97,6 @@ class _PolicyPageState extends State { 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); } } diff --git a/kell_creations_apps/packages/feature_policy/lib/src/presentation/widgets/policy_detail_panel.dart b/kell_creations_apps/packages/feature_policy/lib/src/presentation/widgets/policy_detail_panel.dart index 522b8bd..0a17f52 100644 --- a/kell_creations_apps/packages/feature_policy/lib/src/presentation/widgets/policy_detail_panel.dart +++ b/kell_creations_apps/packages/feature_policy/lib/src/presentation/widgets/policy_detail_panel.dart @@ -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, + ), + ), + ), + ], ], ), ), diff --git a/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/product_publishing_page.dart b/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/product_publishing_page.dart index 66721c4..f11e4fb 100644 --- a/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/product_publishing_page.dart +++ b/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/product_publishing_page.dart @@ -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 createState() => _ProductPublishingPageState(); @@ -95,6 +99,10 @@ class _ProductPublishingPageState extends State { 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, + ); } } diff --git a/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/widgets/product_preview_panel.dart b/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/widgets/product_preview_panel.dart index b99af44..40c2d30 100644 --- a/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/widgets/product_preview_panel.dart +++ b/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/widgets/product_preview_panel.dart @@ -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(