Add Kell Creations operations app foundation with feature slices and WooCommerce read-only integration #1
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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<List<PolicyCheckResult>> call() => repository.getPolicyChecks();
|
||||
}
|
||||
|
|
@ -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<PolicyCheckResult> checks = [];
|
||||
PolicyCheckResult? selectedCheck;
|
||||
Object? error;
|
||||
|
||||
/// Loads all policy checks.
|
||||
Future<void> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<PolicyCheckResult>> getPolicyChecks() async {
|
||||
await Future<void>.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,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
/// The governance readiness status of a policy check.
|
||||
enum GovernanceStatus { compliant, nonCompliant, needsReview, notApplicable }
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import 'policy_check_result.dart';
|
||||
|
||||
/// Contract for fetching policy-check results.
|
||||
abstract class PolicyRepository {
|
||||
/// Returns all policy-check results.
|
||||
Future<List<PolicyCheckResult>> getPolicyChecks();
|
||||
}
|
||||
|
|
@ -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<PolicyPage> createState() => _PolicyPageState();
|
||||
}
|
||||
|
||||
class _PolicyPageState extends State<PolicyPage> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue