Add Kell Creations operations app foundation with feature slices and WooCommerce read-only integration #1

Merged
mtkell merged 12 commits from feat/inventory-first-slice into main 2026-04-04 19:46:31 +00:00
15 changed files with 425 additions and 18 deletions
Showing only changes of commit 0f61badba6 - Show all commits

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import '../dashboard/application/dashboard_controller.dart'; import '../dashboard/application/dashboard_controller.dart';
import '../dashboard/domain/dashboard_summary.dart'; import '../dashboard/domain/dashboard_summary.dart';
import '../navigation/app_navigation.dart';
import '../routing/app_routes.dart'; import '../routing/app_routes.dart';
import '../shell/widgets/empty_state_panel.dart'; import '../shell/widgets/empty_state_panel.dart';
import '../shell/widgets/section_header.dart'; import '../shell/widgets/section_header.dart';
@ -112,48 +113,56 @@ class _DashboardPageState extends State<DashboardPage> {
iconColor: KcColors.denimBlue, iconColor: KcColors.denimBlue,
label: 'Total Products', label: 'Total Products',
value: '${summary.totalProducts}', value: '${summary.totalProducts}',
onTap: () => AppNavigation.dashboardToInventory(context),
), ),
SummaryCard( SummaryCard(
icon: Icons.check_circle_outline, icon: Icons.check_circle_outline,
iconColor: KcColors.success, iconColor: KcColors.success,
label: 'In Stock', label: 'In Stock',
value: '${summary.inStock}', value: '${summary.inStock}',
onTap: () => AppNavigation.dashboardToInventory(context, filter: 'inStock'),
), ),
SummaryCard( SummaryCard(
icon: Icons.warning_amber_rounded, icon: Icons.warning_amber_rounded,
iconColor: KcColors.warning, iconColor: KcColors.warning,
label: 'Low Stock', label: 'Low Stock',
value: '${summary.lowStock}', value: '${summary.lowStock}',
onTap: () => AppNavigation.dashboardToInventory(context, filter: 'lowStock'),
), ),
SummaryCard( SummaryCard(
icon: Icons.edit_note, icon: Icons.edit_note,
iconColor: KcColors.neutral, iconColor: KcColors.neutral,
label: 'Draft', label: 'Draft',
value: '${summary.draftProducts}', value: '${summary.draftProducts}',
onTap: () => AppNavigation.dashboardToProducts(context, filter: 'draft'),
), ),
SummaryCard( SummaryCard(
icon: Icons.receipt_long, icon: Icons.receipt_long,
iconColor: KcColors.denimBlue, iconColor: KcColors.denimBlue,
label: 'Total Orders', label: 'Total Orders',
value: '${summary.totalOrders}', value: '${summary.totalOrders}',
onTap: () => AppNavigation.dashboardToOrders(context),
), ),
SummaryCard( SummaryCard(
icon: Icons.hourglass_empty, icon: Icons.hourglass_empty,
iconColor: KcColors.warning, iconColor: KcColors.warning,
label: 'Pending Orders', label: 'Pending Orders',
value: '${summary.pendingOrders}', value: '${summary.pendingOrders}',
onTap: () => AppNavigation.dashboardToOrders(context, filter: 'pending'),
), ),
SummaryCard( SummaryCard(
icon: Icons.local_shipping_outlined, icon: Icons.local_shipping_outlined,
iconColor: KcColors.success, iconColor: KcColors.success,
label: 'Active Orders', label: 'Active Orders',
value: '${summary.activeOrders}', value: '${summary.activeOrders}',
onTap: () => AppNavigation.dashboardToOrders(context, filter: 'active'),
), ),
SummaryCard( SummaryCard(
icon: Icons.attach_money, icon: Icons.attach_money,
iconColor: KcColors.success, iconColor: KcColors.success,
label: 'Revenue', label: 'Revenue',
value: '\$${summary.deliveredRevenue.toStringAsFixed(2)}', value: '\$${summary.deliveredRevenue.toStringAsFixed(2)}',
onTap: () => AppNavigation.dashboardToFinance(context),
), ),
]; ];

View File

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import '../composition/app_scope.dart'; import '../composition/app_scope.dart';
import '../dashboard/application/dashboard_controller.dart'; import '../dashboard/application/dashboard_controller.dart';
import '../dashboard/application/get_dashboard_summary.dart'; import '../dashboard/application/get_dashboard_summary.dart';
import '../navigation/app_navigation.dart';
import '../pages/dashboard_page.dart'; import '../pages/dashboard_page.dart';
import '../pages/finance_placeholder_page.dart'; import '../pages/finance_placeholder_page.dart';
import '../pages/integrations_placeholder_page.dart'; import '../pages/integrations_placeholder_page.dart';
@ -45,7 +46,10 @@ abstract final class AppRoutes {
(context) => AppShell( (context) => AppShell(
selectedRoute: inventory, selectedRoute: inventory,
title: '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: case products:
@ -56,6 +60,7 @@ abstract final class AppRoutes {
title: 'Products', title: 'Products',
child: ProductPublishingPage( child: ProductPublishingPage(
repository: AppScope.of(context).productPublishingRepository, repository: AppScope.of(context).productPublishingRepository,
onViewPolicy: () => AppNavigation.productToPolicy(context),
), ),
), ),
); );
@ -65,7 +70,11 @@ abstract final class AppRoutes {
(context) => AppShell( (context) => AppShell(
selectedRoute: orders, selectedRoute: orders,
title: '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: case finance:
@ -83,7 +92,11 @@ abstract final class AppRoutes {
(context) => AppShell( (context) => AppShell(
selectedRoute: policy, selectedRoute: policy,
title: '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: case integrations:
@ -101,7 +114,10 @@ abstract final class AppRoutes {
(context) => AppShell( (context) => AppShell(
selectedRoute: inventory, selectedRoute: inventory,
title: 'Inventory', title: 'Inventory',
child: InventoryPage(repository: AppScope.of(context).inventoryRepository), child: InventoryPage(
repository: AppScope.of(context).inventoryRepository,
onViewProduct: (sku) => AppNavigation.inventoryToProduct(context, sku: sku),
),
), ),
); );
} }

View File

@ -4,26 +4,31 @@ import 'package:flutter/material.dart';
/// A stat / summary card that displays a [value] with a [label] and an [icon]. /// 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, /// 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 { class SummaryCard extends StatelessWidget {
final IconData icon; final IconData icon;
final Color iconColor; final Color iconColor;
final String label; final String label;
final String value; final String value;
/// Optional tap handler for cross-feature navigation from dashboard KPIs.
final VoidCallback? onTap;
const SummaryCard({ const SummaryCard({
super.key, super.key,
required this.icon, required this.icon,
required this.iconColor, required this.iconColor,
required this.label, required this.label,
required this.value, required this.value,
this.onTap,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return KcCard( final card = KcCard(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, 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),
);
} }
} }

View File

@ -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'));
});
});
}

View File

@ -9,7 +9,11 @@ import 'widgets/inventory_item_card.dart';
class InventoryPage extends StatefulWidget { class InventoryPage extends StatefulWidget {
final InventoryRepository repository; 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 @override
State<InventoryPage> createState() => _InventoryPageState(); State<InventoryPage> createState() => _InventoryPageState();
@ -64,7 +68,13 @@ class _InventoryPageState extends State<InventoryPage> {
childAspectRatio: 1.5, childAspectRatio: 1.5,
), ),
itemBuilder: (context, index) { 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,
);
}, },
); );
}, },

View File

@ -6,7 +6,10 @@ import '../../domain/inventory_status.dart';
class InventoryItemCard extends StatelessWidget { class InventoryItemCard extends StatelessWidget {
final InventoryItem item; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -25,6 +28,19 @@ class InventoryItemCard extends StatelessWidget {
Text('Quantity on hand: ${item.quantityOnHand}'), Text('Quantity on hand: ${item.quantityOnHand}'),
const SizedBox(height: KcSpacing.sm), const SizedBox(height: KcSpacing.sm),
Text('Unit price: \$${item.unitPrice.toStringAsFixed(2)}'), 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,
),
),
),
],
], ],
), ),
); );

View File

@ -14,7 +14,13 @@ import 'widgets/order_detail_panel.dart';
class OrdersPage extends StatefulWidget { class OrdersPage extends StatefulWidget {
final OrdersRepository repository; 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 @override
State<OrdersPage> createState() => _OrdersPageState(); State<OrdersPage> createState() => _OrdersPageState();
@ -93,6 +99,10 @@ class _OrdersPageState extends State<OrdersPage> {
if (selected == null) { if (selected == null) {
return const Center(child: Text('Select an order to view details')); 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,
);
} }
} }

View File

@ -10,7 +10,18 @@ import 'order_status_chip.dart';
class OrderDetailPanel extends StatelessWidget { class OrderDetailPanel extends StatelessWidget {
final Order order; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -63,6 +74,37 @@ class OrderDetailPanel extends StatelessWidget {
'SKU: ${item.sku}', 'SKU: ${item.sku}',
style: theme.textTheme.bodySmall?.copyWith(color: KcColors.neutral), 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,
),
),
),
],
),
),
], ],
), ),
), ),

View File

@ -14,7 +14,11 @@ import 'widgets/policy_detail_panel.dart';
class PolicyPage extends StatefulWidget { class PolicyPage extends StatefulWidget {
final PolicyRepository repository; 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 @override
State<PolicyPage> createState() => _PolicyPageState(); State<PolicyPage> createState() => _PolicyPageState();
@ -93,6 +97,6 @@ class _PolicyPageState extends State<PolicyPage> {
if (selected == null) { if (selected == null) {
return const Center(child: Text('Select a policy check to view details')); return const Center(child: Text('Select a policy check to view details'));
} }
return PolicyDetailPanel(check: selected); return PolicyDetailPanel(check: selected, onViewRelatedPage: widget.onViewRelatedPage);
} }
} }

View File

@ -12,7 +12,11 @@ import 'governance_status_chip.dart';
class PolicyDetailPanel extends StatelessWidget { class PolicyDetailPanel extends StatelessWidget {
final PolicyCheckResult check; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -49,6 +53,21 @@ class PolicyDetailPanel extends StatelessWidget {
Text('Detail', style: theme.textTheme.titleLarge), Text('Detail', style: theme.textTheme.titleLarge),
const SizedBox(height: KcSpacing.sm), const SizedBox(height: KcSpacing.sm),
Text(check.detail, style: theme.textTheme.bodyMedium), 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,
),
),
),
],
], ],
), ),
), ),

View File

@ -15,7 +15,11 @@ import 'widgets/product_preview_panel.dart';
class ProductPublishingPage extends StatefulWidget { class ProductPublishingPage extends StatefulWidget {
final ProductPublishingRepository repository; 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 @override
State<ProductPublishingPage> createState() => _ProductPublishingPageState(); State<ProductPublishingPage> createState() => _ProductPublishingPageState();
@ -95,6 +99,10 @@ class _ProductPublishingPageState extends State<ProductPublishingPage> {
if (selected == null) { if (selected == null) {
return const Center(child: Text('Select a product draft to preview')); 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,
);
} }
} }

View File

@ -13,7 +13,10 @@ class ProductPreviewPanel extends StatelessWidget {
final ProductDraft draft; final ProductDraft draft;
final VoidCallback? onPublish; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -66,6 +69,21 @@ class ProductPreviewPanel extends StatelessWidget {
Text(draft.description, style: theme.textTheme.bodyLarge), Text(draft.description, style: theme.textTheme.bodyLarge),
const SizedBox(height: KcSpacing.xl), 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 // Publish button
if (draft.status != PublishStatus.published) if (draft.status != PublishStatus.published)
SizedBox( SizedBox(