From c2049e2c467b1eda7cbb3cb1d150cbdb6ec4efe7 Mon Sep 17 00:00:00 2001 From: Mike Kell Date: Sat, 4 Apr 2026 14:21:49 -0400 Subject: [PATCH] feat(policy): add policy governance workspace vertical slice --- .../lib/composition/app_services.dart | 4 + .../apps/kell_web/lib/routing/app_routes.dart | 6 +- .../apps/kell_web/pubspec.lock | 7 ++ .../apps/kell_web/pubspec.yaml | 2 + .../feature_policy/lib/feature_policy.dart | 12 +- .../src/application/get_policy_checks.dart | 11 ++ .../src/application/policy_controller.dart | 40 ++++++ .../lib/src/data/fake_policy_repository.dart | 118 ++++++++++++++++++ .../lib/src/domain/governance_status.dart | 2 + .../lib/src/domain/policy_check_result.dart | 22 ++++ .../lib/src/domain/policy_repository.dart | 7 ++ .../lib/src/presentation/policy_page.dart | 98 +++++++++++++++ .../widgets/governance_status_chip.dart | 31 +++++ .../widgets/policy_check_card.dart | 76 +++++++++++ .../widgets/policy_detail_panel.dart | 90 +++++++++++++ .../packages/feature_policy/pubspec.yaml | 40 +----- .../test/feature_policy_test.dart | 103 ++++++++++++++- 17 files changed, 620 insertions(+), 49 deletions(-) create mode 100644 kell_creations_apps/packages/feature_policy/lib/src/application/get_policy_checks.dart create mode 100644 kell_creations_apps/packages/feature_policy/lib/src/application/policy_controller.dart create mode 100644 kell_creations_apps/packages/feature_policy/lib/src/data/fake_policy_repository.dart create mode 100644 kell_creations_apps/packages/feature_policy/lib/src/domain/governance_status.dart create mode 100644 kell_creations_apps/packages/feature_policy/lib/src/domain/policy_check_result.dart create mode 100644 kell_creations_apps/packages/feature_policy/lib/src/domain/policy_repository.dart create mode 100644 kell_creations_apps/packages/feature_policy/lib/src/presentation/policy_page.dart create mode 100644 kell_creations_apps/packages/feature_policy/lib/src/presentation/widgets/governance_status_chip.dart create mode 100644 kell_creations_apps/packages/feature_policy/lib/src/presentation/widgets/policy_check_card.dart create mode 100644 kell_creations_apps/packages/feature_policy/lib/src/presentation/widgets/policy_detail_panel.dart diff --git a/kell_creations_apps/apps/kell_web/lib/composition/app_services.dart b/kell_creations_apps/apps/kell_web/lib/composition/app_services.dart index 9ceb74e..9e45b87 100644 --- a/kell_creations_apps/apps/kell_web/lib/composition/app_services.dart +++ b/kell_creations_apps/apps/kell_web/lib/composition/app_services.dart @@ -1,5 +1,6 @@ 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 the app. @@ -10,11 +11,13 @@ import 'package:feature_wordpress/feature_wordpress.dart'; class AppServices { final InventoryRepository inventoryRepository; final OrdersRepository ordersRepository; + final PolicyRepository policyRepository; final ProductPublishingRepository productPublishingRepository; const AppServices({ required this.inventoryRepository, required this.ordersRepository, + required this.policyRepository, required this.productPublishingRepository, }); @@ -23,6 +26,7 @@ class AppServices { return AppServices( inventoryRepository: FakeInventoryRepository(), ordersRepository: FakeOrdersRepository(), + policyRepository: FakePolicyRepository(), productPublishingRepository: FakeProductPublishingRepository(), ); } 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 a42de1b..99a17b0 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 @@ -1,5 +1,6 @@ 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'; @@ -9,7 +10,6 @@ import '../dashboard/application/get_dashboard_summary.dart'; import '../pages/dashboard_page.dart'; import '../pages/finance_placeholder_page.dart'; import '../pages/integrations_placeholder_page.dart'; -import '../pages/policy_placeholder_page.dart'; import '../shell/app_shell.dart'; abstract final class AppRoutes { @@ -80,10 +80,10 @@ abstract final class AppRoutes { case policy: return _buildRoute( settings, - (context) => const AppShell( + (context) => AppShell( selectedRoute: policy, title: 'Policy', - child: PolicyPlaceholderPage(), + child: PolicyPage(repository: AppScope.of(context).policyRepository), ), ); case integrations: diff --git a/kell_creations_apps/apps/kell_web/pubspec.lock b/kell_creations_apps/apps/kell_web/pubspec.lock index 6b004a7..6b87a10 100644 --- a/kell_creations_apps/apps/kell_web/pubspec.lock +++ b/kell_creations_apps/apps/kell_web/pubspec.lock @@ -85,6 +85,13 @@ packages: 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: diff --git a/kell_creations_apps/apps/kell_web/pubspec.yaml b/kell_creations_apps/apps/kell_web/pubspec.yaml index f73d2ae..1d9050e 100644 --- a/kell_creations_apps/apps/kell_web/pubspec.yaml +++ b/kell_creations_apps/apps/kell_web/pubspec.yaml @@ -43,6 +43,8 @@ dependencies: path: ../../packages/feature_inventory feature_orders: path: ../../packages/feature_orders + feature_policy: + path: ../../packages/feature_policy feature_wordpress: path: ../../packages/feature_wordpress diff --git a/kell_creations_apps/packages/feature_policy/lib/feature_policy.dart b/kell_creations_apps/packages/feature_policy/lib/feature_policy.dart index 298576d..695847e 100644 --- a/kell_creations_apps/packages/feature_policy/lib/feature_policy.dart +++ b/kell_creations_apps/packages/feature_policy/lib/feature_policy.dart @@ -1,5 +1,7 @@ -/// A Calculator. -class Calculator { - /// Returns [value] plus 1. - int addOne(int value) => value + 1; -} +library; + +export 'src/data/fake_policy_repository.dart'; +export 'src/domain/governance_status.dart'; +export 'src/domain/policy_check_result.dart'; +export 'src/domain/policy_repository.dart'; +export 'src/presentation/policy_page.dart'; diff --git a/kell_creations_apps/packages/feature_policy/lib/src/application/get_policy_checks.dart b/kell_creations_apps/packages/feature_policy/lib/src/application/get_policy_checks.dart new file mode 100644 index 0000000..417128a --- /dev/null +++ b/kell_creations_apps/packages/feature_policy/lib/src/application/get_policy_checks.dart @@ -0,0 +1,11 @@ +import '../domain/policy_check_result.dart'; +import '../domain/policy_repository.dart'; + +/// Use case: retrieve all policy-check results from the repository. +class GetPolicyChecks { + final PolicyRepository repository; + + GetPolicyChecks(this.repository); + + Future> call() => repository.getPolicyChecks(); +} diff --git a/kell_creations_apps/packages/feature_policy/lib/src/application/policy_controller.dart b/kell_creations_apps/packages/feature_policy/lib/src/application/policy_controller.dart new file mode 100644 index 0000000..d6ecb3a --- /dev/null +++ b/kell_creations_apps/packages/feature_policy/lib/src/application/policy_controller.dart @@ -0,0 +1,40 @@ +import 'package:flutter/foundation.dart'; + +import '../domain/policy_check_result.dart'; +import 'get_policy_checks.dart'; + +/// Controller that manages the policy-checks workspace state. +class PolicyController extends ChangeNotifier { + final GetPolicyChecks _getPolicyChecks; + + PolicyController(this._getPolicyChecks); + + bool isLoading = false; + List checks = []; + PolicyCheckResult? selectedCheck; + Object? error; + + /// Loads all policy checks. + Future load() async { + isLoading = true; + error = null; + notifyListeners(); + + try { + checks = await _getPolicyChecks(); + // Auto-select the first check if nothing is selected. + selectedCheck ??= checks.isNotEmpty ? checks.first : null; + } catch (e) { + error = e; + } finally { + isLoading = false; + notifyListeners(); + } + } + + /// Selects a policy check for detail view. + void selectCheck(PolicyCheckResult check) { + selectedCheck = check; + notifyListeners(); + } +} diff --git a/kell_creations_apps/packages/feature_policy/lib/src/data/fake_policy_repository.dart b/kell_creations_apps/packages/feature_policy/lib/src/data/fake_policy_repository.dart new file mode 100644 index 0000000..0718b7a --- /dev/null +++ b/kell_creations_apps/packages/feature_policy/lib/src/data/fake_policy_repository.dart @@ -0,0 +1,118 @@ +import '../domain/governance_status.dart'; +import '../domain/policy_check_result.dart'; +import '../domain/policy_repository.dart'; + +/// In-memory fake that returns sample readiness and governance checks +/// derived from Kell Creations products, inventory, and orders. +class FakePolicyRepository implements PolicyRepository { + @override + Future> getPolicyChecks() async { + await Future.delayed(const Duration(milliseconds: 300)); + + final now = DateTime(2026, 4, 4); + + return [ + PolicyCheckResult( + id: 'POL-001', + title: 'Product Safety Labeling', + category: 'Product Compliance', + status: GovernanceStatus.compliant, + summary: 'All active products carry required safety labels.', + detail: + 'Floral Bowl Cozy, Citrus Coaster Set, Ocean Nightlight, Fabric ' + 'Jar Gripper, and Skillet Handle Sleeve have been reviewed and ' + 'carry the appropriate consumer-safety labels per CPSC guidelines. ' + 'Sublimated Slate Coaster is still in draft and will be evaluated ' + 'before listing.', + lastEvaluated: now.subtract(const Duration(days: 2)), + ), + PolicyCheckResult( + id: 'POL-002', + title: 'Inventory Accuracy Audit', + category: 'Inventory Governance', + status: GovernanceStatus.needsReview, + summary: 'Two SKUs show zero on-hand quantity and need recount.', + detail: + 'Ocean Nightlight (NL-OCN-003) and Sublimated Slate Coaster ' + '(SC-SUB-006) report zero quantity on hand. A physical recount ' + 'should be scheduled to confirm whether stock-outs are accurate ' + 'or the result of a data-entry discrepancy.', + lastEvaluated: now.subtract(const Duration(days: 1)), + ), + PolicyCheckResult( + id: 'POL-003', + title: 'Order Fulfillment SLA', + category: 'Order Operations', + status: GovernanceStatus.compliant, + summary: 'All orders shipped within the 3-business-day SLA.', + detail: + 'Review of recent orders (KC-1001 through KC-1006) confirms that ' + 'processing-to-ship times remain within the published 3-business-day ' + 'service-level agreement. No SLA breaches detected.', + lastEvaluated: now, + ), + PolicyCheckResult( + id: 'POL-004', + title: 'Pricing Consistency', + category: 'Product Compliance', + status: GovernanceStatus.nonCompliant, + summary: 'Citrus Coaster Set price differs between store and inventory.', + detail: + 'The Citrus Coaster Set (CS-CIT-002) is listed at \$16.50 in ' + 'inventory but the WordPress storefront shows \$15.99. Prices must ' + 'be reconciled before the next sales cycle to avoid consumer ' + 'protection issues.', + lastEvaluated: now, + ), + PolicyCheckResult( + id: 'POL-005', + title: 'Return & Refund Policy Published', + category: 'Customer Policy', + status: GovernanceStatus.compliant, + summary: 'Return and refund policy is published and up to date.', + detail: + 'The 30-day return and refund policy is published on the Kell ' + 'Creations storefront and was last reviewed on 2026-03-15. No ' + 'changes are required at this time.', + lastEvaluated: now.subtract(const Duration(days: 20)), + ), + PolicyCheckResult( + id: 'POL-006', + title: 'Low-Stock Reorder Trigger', + category: 'Inventory Governance', + status: GovernanceStatus.needsReview, + summary: 'Two products are below the reorder threshold.', + detail: + 'Citrus Coaster Set (7 units) and Skillet Handle Sleeve (5 units) ' + 'are below the 10-unit reorder point. Purchase orders should be ' + 'raised to avoid stock-outs that could delay order fulfillment.', + lastEvaluated: now.subtract(const Duration(days: 1)), + ), + PolicyCheckResult( + id: 'POL-007', + title: 'Sales Tax Configuration', + category: 'Finance & Tax', + status: GovernanceStatus.notApplicable, + summary: 'Sales tax collection is not yet enabled.', + detail: + 'Kell Creations has not yet reached the sales-tax nexus threshold ' + 'in any state. This check will become applicable once annual ' + 'revenue exceeds the relevant economic-nexus limits.', + lastEvaluated: now.subtract(const Duration(days: 30)), + ), + PolicyCheckResult( + id: 'POL-008', + title: 'Cancelled Order Review', + category: 'Order Operations', + status: GovernanceStatus.nonCompliant, + summary: 'A cancelled order lacks a documented reason.', + detail: + 'Order KC-1006 was cancelled but no cancellation reason was ' + 'recorded in the system. Store policy requires a documented reason ' + 'for every cancellation to support dispute resolution and trend ' + 'analysis.', + lastEvaluated: now, + ), + ]; + } +} diff --git a/kell_creations_apps/packages/feature_policy/lib/src/domain/governance_status.dart b/kell_creations_apps/packages/feature_policy/lib/src/domain/governance_status.dart new file mode 100644 index 0000000..7f61cf4 --- /dev/null +++ b/kell_creations_apps/packages/feature_policy/lib/src/domain/governance_status.dart @@ -0,0 +1,2 @@ +/// The governance readiness status of a policy check. +enum GovernanceStatus { compliant, nonCompliant, needsReview, notApplicable } diff --git a/kell_creations_apps/packages/feature_policy/lib/src/domain/policy_check_result.dart b/kell_creations_apps/packages/feature_policy/lib/src/domain/policy_check_result.dart new file mode 100644 index 0000000..ac328a0 --- /dev/null +++ b/kell_creations_apps/packages/feature_policy/lib/src/domain/policy_check_result.dart @@ -0,0 +1,22 @@ +import 'governance_status.dart'; + +/// A single policy-check result describing whether a governance rule is met. +class PolicyCheckResult { + final String id; + final String title; + final String category; + final GovernanceStatus status; + final String summary; + final String detail; + final DateTime lastEvaluated; + + const PolicyCheckResult({ + required this.id, + required this.title, + required this.category, + required this.status, + required this.summary, + required this.detail, + required this.lastEvaluated, + }); +} diff --git a/kell_creations_apps/packages/feature_policy/lib/src/domain/policy_repository.dart b/kell_creations_apps/packages/feature_policy/lib/src/domain/policy_repository.dart new file mode 100644 index 0000000..8bacaf2 --- /dev/null +++ b/kell_creations_apps/packages/feature_policy/lib/src/domain/policy_repository.dart @@ -0,0 +1,7 @@ +import 'policy_check_result.dart'; + +/// Contract for fetching policy-check results. +abstract class PolicyRepository { + /// Returns all policy-check results. + Future> getPolicyChecks(); +} 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 new file mode 100644 index 0000000..8b568f3 --- /dev/null +++ b/kell_creations_apps/packages/feature_policy/lib/src/presentation/policy_page.dart @@ -0,0 +1,98 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import '../application/get_policy_checks.dart'; +import '../application/policy_controller.dart'; +import '../domain/policy_repository.dart'; +import 'widgets/policy_check_card.dart'; +import 'widgets/policy_detail_panel.dart'; + +/// The main Policy page. +/// +/// Displays a list of policy checks on the left and a detail panel on the +/// right. Users can select a check to view its full details. +class PolicyPage extends StatefulWidget { + final PolicyRepository repository; + + const PolicyPage({super.key, required this.repository}); + + @override + State createState() => _PolicyPageState(); +} + +class _PolicyPageState extends State { + late final PolicyController controller; + + @override + void initState() { + super.initState(); + controller = PolicyController(GetPolicyChecks(widget.repository)); + controller.load(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller, + builder: (context, _) { + if (controller.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (controller.error != null) { + return const Center(child: Text('Failed to load policy checks.')); + } + + return LayoutBuilder( + builder: (context, constraints) { + // On narrow screens show only the list; on wide screens show + // a master-detail layout. + if (constraints.maxWidth < 800) { + return _buildCheckList(); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 400, child: _buildCheckList()), + const SizedBox(width: KcSpacing.md), + Expanded(child: _buildDetail()), + ], + ); + }, + ); + }, + ); + } + + Widget _buildCheckList() { + return ListView.separated( + itemCount: controller.checks.length, + separatorBuilder: (_, _) => const SizedBox(height: KcSpacing.sm), + itemBuilder: (context, index) { + final check = controller.checks[index]; + return SizedBox( + height: 160, + child: PolicyCheckCard( + check: check, + isSelected: check.id == controller.selectedCheck?.id, + onTap: () => controller.selectCheck(check), + ), + ); + }, + ); + } + + Widget _buildDetail() { + final selected = controller.selectedCheck; + if (selected == null) { + return const Center(child: Text('Select a policy check to view details')); + } + return PolicyDetailPanel(check: selected); + } +} diff --git a/kell_creations_apps/packages/feature_policy/lib/src/presentation/widgets/governance_status_chip.dart b/kell_creations_apps/packages/feature_policy/lib/src/presentation/widgets/governance_status_chip.dart new file mode 100644 index 0000000..820f579 --- /dev/null +++ b/kell_creations_apps/packages/feature_policy/lib/src/presentation/widgets/governance_status_chip.dart @@ -0,0 +1,31 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import '../../domain/governance_status.dart'; + +/// A chip that displays the [GovernanceStatus] of a policy check using the +/// design-system [KcStatusChip]. +class GovernanceStatusChip extends StatelessWidget { + final GovernanceStatus status; + + const GovernanceStatusChip({super.key, required this.status}); + + @override + Widget build(BuildContext context) { + final (label, bg, fg) = _style(status); + return KcStatusChip(label: label, background: bg, foreground: fg); + } + + static (String, Color, Color) _style(GovernanceStatus status) { + switch (status) { + case GovernanceStatus.compliant: + return ('Compliant', const Color(0xFFE8F5E9), KcColors.success); + case GovernanceStatus.nonCompliant: + return ('Non-compliant', const Color(0xFFFFEBEE), KcColors.danger); + case GovernanceStatus.needsReview: + return ('Needs review', const Color(0xFFFFF8E1), KcColors.warning); + case GovernanceStatus.notApplicable: + return ('N/A', const Color(0xFFECEFF1), KcColors.neutral); + } + } +} diff --git a/kell_creations_apps/packages/feature_policy/lib/src/presentation/widgets/policy_check_card.dart b/kell_creations_apps/packages/feature_policy/lib/src/presentation/widgets/policy_check_card.dart new file mode 100644 index 0000000..edbbb9f --- /dev/null +++ b/kell_creations_apps/packages/feature_policy/lib/src/presentation/widgets/policy_check_card.dart @@ -0,0 +1,76 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import '../../domain/policy_check_result.dart'; +import 'governance_status_chip.dart'; + +/// A card displaying a summary of a [PolicyCheckResult]. +/// +/// Shows the check ID, title, category, status chip, and summary. +/// Highlights when [isSelected] is true. +class PolicyCheckCard extends StatelessWidget { + final PolicyCheckResult check; + final bool isSelected; + final VoidCallback? onTap; + + const PolicyCheckCard({super.key, required this.check, this.isSelected = false, this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(KcSpacing.md), + decoration: BoxDecoration( + color: KcColors.surface, + border: Border.all( + color: isSelected ? KcColors.denimBlue : KcColors.border, + width: isSelected ? 2 : 1, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow(blurRadius: 8, offset: Offset(0, 2), color: Color(0x11000000)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + check.title, + style: Theme.of(context).textTheme.titleMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + GovernanceStatusChip(status: check.status), + ], + ), + const SizedBox(height: KcSpacing.xs), + Text( + check.category, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: KcColors.neutral, fontWeight: FontWeight.w600), + ), + const SizedBox(height: KcSpacing.sm), + Text( + check.summary, + style: Theme.of(context).textTheme.bodyMedium, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + Text( + check.id, + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: KcColors.neutral), + ), + ], + ), + ), + ); + } +} 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 new file mode 100644 index 0000000..522b8bd --- /dev/null +++ b/kell_creations_apps/packages/feature_policy/lib/src/presentation/widgets/policy_detail_panel.dart @@ -0,0 +1,90 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import '../../domain/policy_check_result.dart'; +import 'governance_status_chip.dart'; + +/// A detail panel that shows the full information for the selected +/// [PolicyCheckResult]. +/// +/// Includes the title, status, category, summary, full detail text, +/// and last-evaluated date. +class PolicyDetailPanel extends StatelessWidget { + final PolicyCheckResult check; + + const PolicyDetailPanel({super.key, required this.check}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return KcCard( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Header ───────────────────────────────────────────────── + Row( + children: [ + Expanded(child: Text(check.title, style: theme.textTheme.headlineMedium)), + const SizedBox(width: KcSpacing.sm), + GovernanceStatusChip(status: check.status), + ], + ), + const SizedBox(height: KcSpacing.md), + + // ── Metadata ─────────────────────────────────────────────── + _MetadataRow(label: 'Check ID', value: check.id), + _MetadataRow(label: 'Category', value: check.category), + _MetadataRow(label: 'Last Evaluated', value: _formatDate(check.lastEvaluated)), + const SizedBox(height: KcSpacing.md), + + // ── Summary ──────────────────────────────────────────────── + Text('Summary', style: theme.textTheme.titleLarge), + const SizedBox(height: KcSpacing.sm), + Text(check.summary, style: theme.textTheme.bodyLarge), + const SizedBox(height: KcSpacing.md), + + // ── Detail ───────────────────────────────────────────────── + Text('Detail', style: theme.textTheme.titleLarge), + const SizedBox(height: KcSpacing.sm), + Text(check.detail, style: theme.textTheme.bodyMedium), + ], + ), + ), + ); + } + + static String _formatDate(DateTime date) { + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + } +} + +class _MetadataRow extends StatelessWidget { + final String label; + final String value; + + const _MetadataRow({required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: KcSpacing.xs), + child: Row( + children: [ + SizedBox( + width: 130, + child: Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: KcColors.neutral, + fontWeight: FontWeight.w600, + ), + ), + ), + Expanded(child: Text(value, style: Theme.of(context).textTheme.bodyMedium)), + ], + ), + ); + } +} diff --git a/kell_creations_apps/packages/feature_policy/pubspec.yaml b/kell_creations_apps/packages/feature_policy/pubspec.yaml index 0ea69b8..b51b6de 100644 --- a/kell_creations_apps/packages/feature_policy/pubspec.yaml +++ b/kell_creations_apps/packages/feature_policy/pubspec.yaml @@ -1,6 +1,7 @@ name: feature_policy -description: "A new Flutter package project." +description: "Policy governance checks for Kell Creations." version: 0.0.1 +publish_to: "none" homepage: environment: @@ -10,45 +11,12 @@ environment: dependencies: flutter: sdk: flutter + design_system: + path: ../design_system dev_dependencies: flutter_test: sdk: flutter 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: - - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/to/asset-from-package - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # To add custom fonts to your package, 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 in packages, see - # https://flutter.dev/to/font-from-package diff --git a/kell_creations_apps/packages/feature_policy/test/feature_policy_test.dart b/kell_creations_apps/packages/feature_policy/test/feature_policy_test.dart index f99d883..b47f6f2 100644 --- a/kell_creations_apps/packages/feature_policy/test/feature_policy_test.dart +++ b/kell_creations_apps/packages/feature_policy/test/feature_policy_test.dart @@ -1,12 +1,105 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:feature_policy/feature_policy.dart'; +import 'package:feature_policy/src/application/get_policy_checks.dart'; +import 'package:feature_policy/src/application/policy_controller.dart'; void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); + group('GovernanceStatus', () { + test('has four values', () { + expect(GovernanceStatus.values.length, 4); + }); + + test('contains expected statuses', () { + expect(GovernanceStatus.values, contains(GovernanceStatus.compliant)); + expect(GovernanceStatus.values, contains(GovernanceStatus.nonCompliant)); + expect(GovernanceStatus.values, contains(GovernanceStatus.needsReview)); + expect(GovernanceStatus.values, contains(GovernanceStatus.notApplicable)); + }); + }); + + group('FakePolicyRepository', () { + late FakePolicyRepository repository; + + setUp(() { + repository = FakePolicyRepository(); + }); + + test('getPolicyChecks returns eight sample checks', () async { + final checks = await repository.getPolicyChecks(); + expect(checks.length, 8); + }); + + test('getPolicyChecks returns checks with expected IDs', () async { + final checks = await repository.getPolicyChecks(); + final ids = checks.map((c) => c.id).toList(); + expect(ids, contains('POL-001')); + expect(ids, contains('POL-002')); + expect(ids, contains('POL-003')); + expect(ids, contains('POL-004')); + expect(ids, contains('POL-005')); + expect(ids, contains('POL-006')); + expect(ids, contains('POL-007')); + expect(ids, contains('POL-008')); + }); + + test('getPolicyChecks returns checks with various statuses', () async { + final checks = await repository.getPolicyChecks(); + final statuses = checks.map((c) => c.status).toSet(); + expect(statuses, contains(GovernanceStatus.compliant)); + expect(statuses, contains(GovernanceStatus.nonCompliant)); + expect(statuses, contains(GovernanceStatus.needsReview)); + expect(statuses, contains(GovernanceStatus.notApplicable)); + }); + + test('getPolicyChecks returns checks with various categories', () async { + final checks = await repository.getPolicyChecks(); + final categories = checks.map((c) => c.category).toSet(); + expect(categories, contains('Product Compliance')); + expect(categories, contains('Inventory Governance')); + expect(categories, contains('Order Operations')); + expect(categories, contains('Customer Policy')); + expect(categories, contains('Finance & Tax')); + }); + }); + + group('PolicyController', () { + late FakePolicyRepository repository; + late PolicyController controller; + + setUp(() { + repository = FakePolicyRepository(); + controller = PolicyController(GetPolicyChecks(repository)); + }); + + tearDown(() { + controller.dispose(); + }); + + test('starts with empty state', () { + expect(controller.isLoading, false); + expect(controller.checks, isEmpty); + expect(controller.selectedCheck, isNull); + expect(controller.error, isNull); + }); + + test('load populates checks and auto-selects first', () async { + await controller.load(); + + expect(controller.isLoading, false); + expect(controller.checks.length, 8); + expect(controller.selectedCheck, isNotNull); + expect(controller.selectedCheck!.id, 'POL-001'); + expect(controller.error, isNull); + }); + + test('selectCheck updates selectedCheck', () async { + await controller.load(); + + final third = controller.checks[2]; + controller.selectCheck(third); + + expect(controller.selectedCheck!.id, third.id); + }); }); }