From 23ea1bebe104e5c22c819aecc4e1e51cf80c4a2f Mon Sep 17 00:00:00 2001 From: Mike Kell Date: Sat, 4 Apr 2026 14:59:26 -0400 Subject: [PATCH] feat(workflows): add shared filtering search and selection persistence --- .../lib/navigation/app_navigation.dart | 15 ++- .../lib/navigation/navigation_target.dart | 3 + .../apps/kell_web/lib/routing/app_routes.dart | 25 ++++ .../navigation/navigation_target_test.dart | 8 ++ .../src/application/inventory_controller.dart | 99 ++++++++++++++- .../lib/src/presentation/inventory_page.dart | 34 ++++- .../test/feature_inventory_test.dart | 120 ++++++++++++++++++ ...5ec9754be8774a3c9a1c.cache.dill.track.dill | Bin 46510168 -> 46517560 bytes .../src/application/orders_controller.dart | 99 ++++++++++++++- .../lib/src/presentation/orders_page.dart | 35 ++++- .../test/orders_controller_test.dart | 72 +++++++++++ .../src/application/policy_controller.dart | 79 +++++++++++- .../lib/src/presentation/policy_page.dart | 35 ++++- .../test/feature_policy_test.dart | 72 +++++++++++ .../product_publishing_controller.dart | 96 +++++++++++++- .../presentation/product_publishing_page.dart | 34 ++++- .../product_publishing_controller_test.dart | 72 +++++++++++ 17 files changed, 867 insertions(+), 31 deletions(-) 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 index b5ba20f..c4f331b 100644 --- a/kell_creations_apps/apps/kell_web/lib/navigation/app_navigation.dart +++ b/kell_creations_apps/apps/kell_web/lib/navigation/app_navigation.dart @@ -20,24 +20,27 @@ abstract final class AppNavigation { // ── Dashboard → feature handoffs ────────────────────────────────────── /// Dashboard KPI "Total Products" / "In Stock" → Inventory page. - static void dashboardToInventory(BuildContext context, {String? filter}) { + static void dashboardToInventory(BuildContext context, {String? filter, String? query}) { navigateTo( context, - NavigationTarget(route: AppRoutes.inventory, arguments: {'filter': ?filter}), + NavigationTarget(route: AppRoutes.inventory, arguments: {'filter': ?filter, 'query': ?query}), ); } /// Dashboard KPI "Draft" → Products page. - static void dashboardToProducts(BuildContext context, {String? filter}) { + static void dashboardToProducts(BuildContext context, {String? filter, String? query}) { navigateTo( context, - NavigationTarget(route: AppRoutes.products, arguments: {'filter': ?filter}), + NavigationTarget(route: AppRoutes.products, arguments: {'filter': ?filter, 'query': ?query}), ); } /// Dashboard KPI "Total Orders" / "Pending" / "Active" → Orders page. - static void dashboardToOrders(BuildContext context, {String? filter}) { - navigateTo(context, NavigationTarget(route: AppRoutes.orders, arguments: {'filter': ?filter})); + static void dashboardToOrders(BuildContext context, {String? filter, String? query}) { + navigateTo( + context, + NavigationTarget(route: AppRoutes.orders, arguments: {'filter': ?filter, 'query': ?query}), + ); } /// Dashboard KPI "Revenue" → Finance page. 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 index 0d2c318..fe18a23 100644 --- a/kell_creations_apps/apps/kell_web/lib/navigation/navigation_target.dart +++ b/kell_creations_apps/apps/kell_web/lib/navigation/navigation_target.dart @@ -32,6 +32,9 @@ class NavigationTarget { /// Convenience: a category filter, if any. String? get category => arguments['category']; + /// Convenience: a search query, if any. + String? get query => arguments['query']; + @override bool operator ==(Object other) => identical(this, other) || 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 1cb2c19..c8c9c84 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 @@ -23,6 +23,9 @@ abstract final class AppRoutes { static const String integrations = '/integrations'; static Route onGenerateRoute(RouteSettings settings) { + // Extract navigation arguments passed via NavigationTarget. + final args = _extractArgs(settings); + switch (settings.name) { case dashboard: return _buildRoute(settings, (context) { @@ -49,6 +52,9 @@ abstract final class AppRoutes { child: InventoryPage( repository: AppScope.of(context).inventoryRepository, onViewProduct: (sku) => AppNavigation.inventoryToProduct(context, sku: sku), + initialFilter: args['filter'], + initialQuery: args['query'], + initialSelectedSku: args['selectedSku'], ), ), ); @@ -61,6 +67,9 @@ abstract final class AppRoutes { child: ProductPublishingPage( repository: AppScope.of(context).productPublishingRepository, onViewPolicy: () => AppNavigation.productToPolicy(context), + initialFilter: args['filter'], + initialQuery: args['query'], + initialSelectedSku: args['selectedSku'], ), ), ); @@ -74,6 +83,9 @@ abstract final class AppRoutes { repository: AppScope.of(context).ordersRepository, onViewProduct: (sku) => AppNavigation.orderToProduct(context, sku: sku), onViewInventory: (sku) => AppNavigation.orderToInventory(context, sku: sku), + initialFilter: args['filter'], + initialQuery: args['query'], + initialSelectedId: args['selectedId'], ), ), ); @@ -96,6 +108,9 @@ abstract final class AppRoutes { repository: AppScope.of(context).policyRepository, onViewRelatedPage: (category) => AppNavigation.policyToRelatedPage(context, category: category), + initialCategory: args['category'], + initialQuery: args['query'], + initialSelectedId: args['selectedId'], ), ), ); @@ -129,4 +144,14 @@ abstract final class AppRoutes { ) { return MaterialPageRoute(settings: settings, builder: pageBuilder); } + + /// Safely extracts the `Map` arguments from [RouteSettings]. + /// + /// Returns an empty map when no arguments are present or the type doesn't + /// match, so callers can always index safely. + static Map _extractArgs(RouteSettings settings) { + final raw = settings.arguments; + if (raw is Map) return raw; + return const {}; + } } 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 index 7a404af..a565d1e 100644 --- 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 @@ -23,12 +23,14 @@ void main() { 'selectedId': 'ID-001', 'filter': 'lowStock', 'category': 'Product Compliance', + 'query': 'coaster', }, ); expect(target.selectedSku, 'SKU-001'); expect(target.selectedId, 'ID-001'); expect(target.filter, 'lowStock'); expect(target.category, 'Product Compliance'); + expect(target.query, 'coaster'); }); test('convenience getters return null when key is absent', () { @@ -37,6 +39,7 @@ void main() { expect(target.selectedId, isNull); expect(target.filter, isNull); expect(target.category, isNull); + expect(target.query, isNull); }); test('equality works for same route and arguments', () { @@ -63,5 +66,10 @@ void main() { expect(target.toString(), contains('/inventory')); expect(target.toString(), contains('lowStock')); }); + + test('query argument round-trips through convenience getter', () { + const target = NavigationTarget(route: '/inventory', arguments: {'query': 'bowl cozy'}); + expect(target.query, 'bowl cozy'); + }); }); } diff --git a/kell_creations_apps/packages/feature_inventory/lib/src/application/inventory_controller.dart b/kell_creations_apps/packages/feature_inventory/lib/src/application/inventory_controller.dart index 7e2363c..65047b2 100644 --- a/kell_creations_apps/packages/feature_inventory/lib/src/application/inventory_controller.dart +++ b/kell_creations_apps/packages/feature_inventory/lib/src/application/inventory_controller.dart @@ -1,23 +1,44 @@ import 'package:flutter/foundation.dart'; import '../domain/inventory_item.dart'; +import '../domain/inventory_status.dart'; import 'get_inventory_items.dart'; +/// Controller that manages the inventory workspace state, including +/// filtering by status, free-text search, and item selection. class InventoryController extends ChangeNotifier { final GetInventoryItems _getInventoryItems; InventoryController(this._getInventoryItems); bool isLoading = false; - List items = []; Object? error; + /// All items returned by the repository (unfiltered). + List _allItems = []; + + /// The currently visible items after applying [activeFilter] and [searchQuery]. + List items = []; + + /// The currently selected item, if any. + InventoryItem? selectedItem; + + /// The active status filter label, or `null` for "all". + /// + /// Recognised values: `'inStock'`, `'lowStock'`, `'outOfStock'`, `'draft'`. + String? activeFilter; + + /// The current free-text search query applied to name / SKU. + String searchQuery = ''; + + /// Loads all inventory items and applies any current filter / search. Future load() async { isLoading = true; error = null; notifyListeners(); try { - items = await _getInventoryItems(); + _allItems = await _getInventoryItems(); + _applyFilters(); } catch (e) { error = e; } finally { @@ -25,4 +46,78 @@ class InventoryController extends ChangeNotifier { notifyListeners(); } } + + /// Sets the status filter and recomputes the visible list. + void setFilter(String? filter) { + activeFilter = filter; + _applyFilters(); + notifyListeners(); + } + + /// Sets the search query and recomputes the visible list. + void setSearchQuery(String query) { + searchQuery = query; + _applyFilters(); + notifyListeners(); + } + + /// Selects an item for detail view / highlight. + void selectItem(InventoryItem item) { + selectedItem = item; + notifyListeners(); + } + + /// Attempts to select an item by SKU. Returns `true` if found. + bool selectBySku(String sku) { + final match = _allItems.where((i) => i.sku == sku).firstOrNull; + if (match != null) { + selectedItem = match; + notifyListeners(); + return true; + } + return false; + } + + // ── Private helpers ──────────────────────────────────────────────────── + + void _applyFilters() { + var result = _allItems; + + // Status filter + final status = _parseStatus(activeFilter); + if (status != null) { + result = result.where((i) => i.status == status).toList(); + } + + // Free-text search on name and SKU + if (searchQuery.isNotEmpty) { + final q = searchQuery.toLowerCase(); + result = result.where((i) { + return i.name.toLowerCase().contains(q) || i.sku.toLowerCase().contains(q); + }).toList(); + } + + items = result; + + // Keep selection valid; clear if the selected item is no longer visible. + if (selectedItem != null && !items.contains(selectedItem)) { + selectedItem = null; + } + } + + static InventoryStatus? _parseStatus(String? filter) { + if (filter == null) return null; + switch (filter) { + case 'inStock': + return InventoryStatus.inStock; + case 'lowStock': + return InventoryStatus.lowStock; + case 'outOfStock': + return InventoryStatus.outOfStock; + case 'draft': + return InventoryStatus.draft; + default: + return null; + } + } } 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 fe8f79f..fab2842 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 @@ -13,7 +13,23 @@ class InventoryPage extends StatefulWidget { /// 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}); + /// Optional initial status filter to apply on first load (e.g. `'lowStock'`). + final String? initialFilter; + + /// Optional initial search query to apply on first load. + final String? initialQuery; + + /// Optional SKU to pre-select on first load (from a navigation handoff). + final String? initialSelectedSku; + + const InventoryPage({ + super.key, + required this.repository, + this.onViewProduct, + this.initialFilter, + this.initialQuery, + this.initialSelectedSku, + }); @override State createState() => _InventoryPageState(); @@ -26,7 +42,21 @@ class _InventoryPageState extends State { void initState() { super.initState(); controller = InventoryController(GetInventoryItems(widget.repository)); - controller.load(); + + // Apply any initial filter / query before loading. + if (widget.initialFilter != null) { + controller.activeFilter = widget.initialFilter; + } + if (widget.initialQuery != null && widget.initialQuery!.isNotEmpty) { + controller.searchQuery = widget.initialQuery!; + } + + controller.load().then((_) { + // After data is loaded, try to pre-select by SKU if requested. + if (widget.initialSelectedSku != null) { + controller.selectBySku(widget.initialSelectedSku!); + } + }); } @override diff --git a/kell_creations_apps/packages/feature_inventory/test/feature_inventory_test.dart b/kell_creations_apps/packages/feature_inventory/test/feature_inventory_test.dart index 6e8f167..fbd261a 100644 --- a/kell_creations_apps/packages/feature_inventory/test/feature_inventory_test.dart +++ b/kell_creations_apps/packages/feature_inventory/test/feature_inventory_test.dart @@ -1,6 +1,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:feature_inventory/feature_inventory.dart'; +import 'package:feature_inventory/src/application/get_inventory_items.dart'; +import 'package:feature_inventory/src/application/inventory_controller.dart'; void main() { group('InventoryStatus', () { @@ -36,4 +38,122 @@ void main() { expect(names, contains('Ocean Nightlight')); }); }); + + group('InventoryController', () { + late FakeInventoryRepository repository; + late InventoryController controller; + + setUp(() { + repository = FakeInventoryRepository(); + controller = InventoryController(GetInventoryItems(repository)); + }); + + tearDown(() { + controller.dispose(); + }); + + test('starts with empty state', () { + expect(controller.isLoading, false); + expect(controller.items, isEmpty); + expect(controller.selectedItem, isNull); + expect(controller.activeFilter, isNull); + expect(controller.searchQuery, ''); + expect(controller.error, isNull); + }); + + test('load populates items', () async { + await controller.load(); + + expect(controller.isLoading, false); + expect(controller.items.length, 6); + expect(controller.error, isNull); + }); + + test('setFilter filters by status', () async { + await controller.load(); + + controller.setFilter('lowStock'); + expect(controller.items.length, 2); + expect(controller.items.every((i) => i.status == InventoryStatus.lowStock), true); + }); + + test('setFilter with null shows all items', () async { + await controller.load(); + + controller.setFilter('lowStock'); + expect(controller.items.length, 2); + + controller.setFilter(null); + expect(controller.items.length, 6); + }); + + test('setSearchQuery filters by name', () async { + await controller.load(); + + controller.setSearchQuery('cozy'); + expect(controller.items.length, 1); + expect(controller.items.first.name, 'Floral Bowl Cozy'); + }); + + test('setSearchQuery filters by SKU', () async { + await controller.load(); + + controller.setSearchQuery('BC-FLR'); + expect(controller.items.length, 1); + expect(controller.items.first.sku, 'BC-FLR-001'); + }); + + test('search is case-insensitive', () async { + await controller.load(); + + controller.setSearchQuery('OCEAN'); + expect(controller.items.length, 1); + expect(controller.items.first.name, 'Ocean Nightlight'); + }); + + test('filter and search combine', () async { + await controller.load(); + + controller.setFilter('inStock'); + controller.setSearchQuery('floral'); + expect(controller.items.length, 1); + expect(controller.items.first.name, 'Floral Bowl Cozy'); + }); + + test('selectItem sets selectedItem', () async { + await controller.load(); + + final item = controller.items[2]; + controller.selectItem(item); + expect(controller.selectedItem, item); + }); + + test('selectBySku selects matching item', () async { + await controller.load(); + + final found = controller.selectBySku('CS-CIT-002'); + expect(found, true); + expect(controller.selectedItem!.sku, 'CS-CIT-002'); + }); + + test('selectBySku returns false for unknown SKU', () async { + await controller.load(); + + final found = controller.selectBySku('UNKNOWN'); + expect(found, false); + expect(controller.selectedItem, isNull); + }); + + test('selection is cleared when filtered out', () async { + await controller.load(); + + // Select an inStock item. + controller.selectBySku('BC-FLR-001'); + expect(controller.selectedItem, isNotNull); + + // Filter to lowStock — the selected item should be cleared. + controller.setFilter('lowStock'); + expect(controller.selectedItem, isNull); + }); + }); } 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 e621673dda99296edd3c2760de1ce8e9537d3067..d162925a404de0ae6bde30dccfa23d9e38e79c11 100644 GIT binary patch delta 40190 zcmdqKcU)9gw?4klnE{46fY_B8D=0cPP!kpG1r=LjZzw1rA_8JG>g;`{Gec8xz!Eg3 zs4+2JQ%p=Ty~p$h8l&me^zwVwnHd;O-uJ!td++D|@f&@fb9P&M?X~OKXV36<)2?NdaSm>~E3-Y*SD{KAC!R-vclkS;T6Dc!^F&TikZj1!rBgq1X(O1G9X?Wm2+y47Ue z8rH|^>cPmh(`4PLOR?_l$wr|@t+$%2x9UkM>TxmunoYAlHr4uAy48Hz=Fw36mrT}| z!lQ;*-=o-{mosa#$=Vzm1vNV{w4c8>fN;io`(*!Uvi=ww6%am{jp|`rsJAUFXSQi3 z+q6hn&A>ML9X)Kt@XrO7nrurWmYDrDlgQ$zGWmB5L7RXQldU8oP;ZGW9#zJ_XDK!z zL0&R$7SauqY*k42p=7Ir9ox_?W9hbf+g%b0WjkQ99ndw|4qKk?3EzioKg#6$q{;SV zSYO*KFw}Ku#64jIAL;}h-`8^W78!2vN~gOoViTuK-rnEu89x+E;*TvyJVIVNXLcvPVB zJF~75lTw`s;^&irgMuF>l8@6t=YN7cDI`Ebk^jw+U94=>L5um z`OnmnCDmsfJj@r6%{Me4@Lw65;@sWMd6V9GQ?v61TS>a}b{8XqJMT9+?~hDyK4mLW z74@0IdB)_t%oLf!sp%m!jLl6x6qQ-OcPRV}btokOT)R{Y{#DrLI0 zQg?SM(z=t0I+Zhb^f`B=|+`}AS`dg+?lDS27pQt(iQq*P5_P=gqG+*~7lY3J{lUuQGO=45r&E4I$Y4D`@mhq7LYZ(0C#|eSH@6L+nW&hSMFOa9uvUE>>&nz0tJwr{N zp^+(`iODP@bcpO(;#prc4E2^w6OYETl^j&H9vUJ1BQ$yo)vTwgYM4C5Q>XXrZ}K#_ zp3L+dmQp--W{mMXG|B9Fh$VQQa6PFmI6kLw;`30?8Oifbf#)4^U^?-f!OF<;bT);qLFhJw>MEkVA1aeb9Md~T%Cm?MYM^iC_Yv5c0O z^BJ2+3p2L1b-6iABl?H|HB(q0q=jxD5^t5XSC7KFW@GgxsqfIB<&zL1G!zlWg&Kr& z9yAE+oFsOq^hy#B_UoS{&YA`olz0)YVVYq|XS3t8pOkJtC_S06K*4~V7dbFVSv6>I zfzsd~Di*XiSbp}4u|^E(m1OOk+CRxUJ8gi$s`$iM-H>9fy=2`9Db{<)&;ygKXNC?g zu!RPs7!CF{ekt~?!+Isz_YLo#WPi+j)c}M2OP?6~j}T-3{YU#>5aYn`kQXvA$uaio z!3B=8fS6te=McXb=Y$cxlAO6C`zJYFqXrn9_xYqaAAuC-lRrA2h7{)~kRlIEa)qT2 zE^v(qNJ%kN?DI)6CsiEF=#^CQ^636a6_>^gFt~g8#JKx8KKs!jE0E;wA5D@d(>>u*r|hMNZ|}m3*;e%W@xH;^?@3+5Hy*B zYC(-*5Ykj1shJqOGW9DsuJz$UO0T{LaYhZPem}ALtcaAP#Srj^8KY&?5XLfVj8bAv zKM^BQ%faMUbCs#)s=h-5g3z!Ti^c*YdKW1|E(w}O-@;fV)@ac;`h3Yv%{p={6@?z0 z(4M=f?U^FTM;Lrm^gErk$7tkE8sA)XVQNX$Cx= z<8Mj*{T`EN!sB!PgERmh0*^>jW-+aZ6mgOf9(hcgIUDYNVvsaA&@&b=?bJE&94BT= z1AStoka=dUSRj@ucwj_V%2BA8)MLbE#wM%7B%{ZM$?ELG#ND{>S3uUWhz@~9#kylOxbEuwzg?;FS7E}gWV90``>oFGV@eCV#ALR~U2LP^T(HGvq^)z|-+-iTA&+tr5A4y&i(r>WX zZk&mGwF@wuvp*9ZFrcc?3s4yN*2Pse7`h@ zJoFenR(YH6iR)~hr6%WJpef8&Z!v?j$!YhG6Lm-`44Mr}93ZW4A?jg^uKQCt|?)m{BC}? z2%YEi=6AzCL|a%2|L^#BB6Pmr|2z2Wv}?;)sOW}&p$mNeq8t1pwM84?zYPCE7yA8| z!9PZ8#=}$S(4nD=0%HzxZn;gBLas;PP!vitbSM-@lcmdIwNB}7iCSeO5v9VgKU5MO zWhOVV9nsBfM|%wan?jkIbSeJ;@qJ=&gCw+2lFXS*>dE9Jwyc~sd6x4JC;;13jI|jF zl;Or_S=g?P71q24SMKvAw`$ofGy>9Q(pd6*1Jm8~dI0oJ9E2pdY ztq8hozA}5A_s4ae-nwn2ifjXA6HrZlWsqG0hRQZ~;o8IJOKw#e?A7To9zz>}c9Dil zqW9bTsX1^P_b{DeRQgJ^;NQx$ z+|DcEvrjo9tz_8}mF)Ay@OJPjrn4kk`b#;%-cDW(?;V!?QZ8GLCq6Txx_AxzzO(!( zty2BqRl#eS&YEj2msb0{V5vHtb*t54n}{-Ktd+Kj4BJomZ{5RK4a-|DiSB46^=a07 z&_6N0Y-LsqBD$3UoqU03;gu8QWk!NO;Fn9vs8MX~;9S+}G{L#L#feN=KJx0R#zkQ* zR8?U*>pc>;HCykgyIN;`R^s-v)@SR+>#S$7!FkSlwr-xz`iI1wA6x&ZE6~~6N!)eb z)~;@o&So}Ayy6p^sm>X0wMe434BO{5S@+cS&A-6vyizFNp|ef5720TUD3A(lS=4j} z!34IAsvjw|L>5V8c+w@)wpB9Nw(^@P0DCc^zRJBA=?Z@^%37y|OuhJp495so<< z9J6ql0Hm87=~qVaZ3JQiz_HA+msD|-B4(b+k>^(>dV^!98D7`~atM<{Tq&S0%b-0^ z$8n-N5$R@=<7R(anxkpI0E?uv(&Bz>og~#usg8#=j)$EBL3o06oWwR1D~9Ro$@-2{ zPA*Mx>bp7n>7D(szl1#jXB)H0*(Q*$+RD-K(w%9}O%i48oQgQ3P0rE&Tsxt1HaLsm zxdfPNa?ZUnEj`_%%034!3}hL;?F8DUTD(TCm)4`_n0z|di-DLd8qGRqm2Y^7_#^4oEMzG zd4588DMVikh;}j64;FI8&|0YsgWC{w#gs~Ra<%vTBxSl1C4;M%7_LE2+kh>q$tKri ze~#s;t{jak$MYEkWA1U~;_8#(DpDOYNIVWDKQCu~@ zsmL$0(ur-8D)>#!=a$sU$2FCYS5xPw)KB2*Yk8F~;Feo2OPXH!dUdp%Rrzt{XDo#F z#Ho@h|A3Z10biIZzksG@8WV$6wV|q;EC1Zv&0p0Rc*lwPEVD{48LHa(YKhyEtI%WU zlQVjop(>N#M~3Klwkm(`NbZQO+Fs78s?S!r_KsAa{id_Ds{K{_@#qwS*_@t=tM&tR zrm8x#-&(#6wH{Ub^;P@NRqfyVM2Z^aSOVQ^Fr&iVR6Siq zJ(4~&+hkU~Nad}18yUT5s(R6%PX`q5Y}LnmNBW94&b)X)ph>Ltw}f^6YiTGysH^T? z-KSZA&Mf*z}i^7CZ zi6pG25?5gLh#90Eo~sgqc=DD<$}0Evn_W@ni5iEuO=Y z35!S6Csrf(d8a%Y*Tvkr&=Yu{IGNftQ^FXF;tonJ6(|XG-KH3^ZsSf$tuED|*X0X0 zm_o|X>cnpnKin*SYwV>{!X$p=tP<8Z04rRH-*!$hHjWDyZ%JYijDTgCxY%fJoBuIu zoFj+wyTTQdGPZ)+9Htj#0CH8=Wy&Pg57UcsEyAvtU6iF2pW&%IS28GtVwnb3;h6pv zw@GpPltt0E8I=8EC2Ayuy9<;TV7(~Lxu?JaL*|HFE_kKEk}C44neeIt%TmNxgOz!8 zqD6>xq$;oKVY#`SS$3N&yM1#dTDa>kDj{8Pw6;)=z8- zBl)Re2HOZRiL~01Xj>pQ)A~T9$_87Zf6O-6YQ(k>dJ$y13%Jc>yNznqjJD$M^IDDp z#r7p|F46XuP@qtg?cK)HjPHOftH7wa6O(R*@GxHYgS`XaRbZclkSgx=w|%}RaI@E7 z_we^g9V1qOM}V75_M0d}An})P?K!Br@ z$z2%oZ!^vjg>s>)WkNIzWQSg%SyYH-{Tu$9^IFinu~$NcDc zSYUu7sF}>VTh+9d^>7yJoyE=0;v3hbIE#_H^ZEpGNTgo{ z--8iB;(H66G?H!SchIY{POKB(hvYjLzX6vuj92+CjWmKYUJ2Ju#`pU?58zVA_>H*e z89#_9X2iK^iKHHQp!hfY@5L@Wtooweii?-=LxGTbJRQJmJL89OLB@7pydw~If$`hm zYQO;&pXcqk>}GryE>(=*;Zy2ZK-3-#_;(^`FXMNq@u{5R28?BBXMwhKXMyuY_)q2c z7@U9jD#$fUgXNk5=xB0v^jAw3B4Jx4$<>G7-NQ8@)-@X8LxF)N*TBo;{NNhrpWLuA z!Wyd)sU`u__&qQ+|D5X3g->uDx?B)tPIQ&}>d;l*QioK9fyUOu<M)-pbNsJ%ssCIZ zkF`|CljcA#dnR!6TqwUMyyDx6zua!J&3&|I#Wd~+a?%5@I{{aIKUED@CFm^hO?P*4 zyYYn5P3q~kV;9^#u*scr>j-`vv1D-*OLC|26X*=?ETVCUFsjKt91$L%ClguR%=m+l zijh>6C~jr^A(_dS+;fxM3;4sWugkGcMutS<5Tq{kN!>tF3m|n>lRFzyPpU!>|0|(e zliWNY)C`H@GO3Ii4ltI~g|Xz0PzenJ1;lzR8GE^jM{uPL8H~i__AS@$$9b^-xLm_t z-|+}BP0Vh{H}Xdy0r_INHUaCk2lz=AAv}1+5?HT2h!_!yR*AcOx9=hTD7#A81zII`o}R zxhU}m&ng!W{jRe}5`XBNMLMhxR}Qo++Z6Moht6WzcDOs1ZI4A@Q*vTn2 zw;3$E#a;fp$HaAJ-{Q#f8xrEtceb^?Hp|*x9Ww&!s8nf?wLOAUV+_`kz!Jb()4Bq% z9t zLU*_Hwig3^?4<%5PNZN;d&OWI)v`{q;qk(Dt;u$+KZD?PQcwO`i8`BUA|&49+}N0#ae4(7D#+T|P&in!o}oVB8-J?!&5xp6(Cj zB=-v(`_S%p&biMVJ+Hnj%qCfBo&*o3Bj2jPOe<|q0ua&c(FV(ISq*p+^qz!|Jqfp; zN%17W#vTLG-+o3*gK=PeT~**24ksKWi zx5}^em0x2ct=UxB9IO-NT-EhFJLX2t$hfMj2=hPad(Xcd*X!~E(BiVH{9M)gmd4|I zXv0f1Y^)ve4JlRc@K>l!RrjgJ6yLhitnPzdCPQ;|$A4^6)qV8UeLkt~bLZHU>ON>v z)u{>OFw>bPYEv{nVS!n_No`XuJib*wh1>t7$G3mky3GGKk8l67b1 zD5owK58w?@&Dw4?LSKWipk}?PX8o1Ts^(a0<<=YvY~py|colUU99Nw{Y9r3ohWu0A z)~5UFwstLlmFl+k(b`9Y>$VnewrcNdu08gTbzA$WzV^{iY9GD(>Xh0?QMa|vC6MFF zy4|jCsoU)|t^WVLZp|%q`$wQ||GKPh&sUx}=Jp2(IdBtlSsc9ixP5vcNEf(?Fzh%S zct^3rx6jGOWAl5*_hOrtNj-P=l9P6tMN~=3qcQClU)g3Hnbq&pRJN`+-9(4!xbq7DL zJ9zKSDRl?Y-s+AekmIuHX>UQD`Q`tEM)q&U5A*-x{?ET0Kq^pQyR>S3#RaOVjqk?X z69=9Ge(eJ{9m)LauFe9aO2v|&4(f|k@en;%($PYnd$D*}ows)FR##gzdlc;2jhK&M zw_Qyy)Deni7;i#L9D_H(_i~M&F&B8L<{uS_TN1r%TGu?@ZN9-F0eZ?A37!BShd<&g=Nlx;*HTt|9!jhU(Q$mrn&vCwIP4(Uj9FAH~#D97HCF2TUu7{;MQ=~+Wzn_{y1il9EhC~ zxal9tpNOb`q5j>5RakOk*Mv^^XzI_Cla}T6U*me^u1Wo+hE-B#{qIuGhHUJbG(Cg;zEOyiDEp^9YcR$q+Rk-_V zgwc+LIp`e2?s0q9N}0PeCByDnElc0s^t7rzVeLK|c*5HKWa~h&`_;f?vzwX)K1(s# z?9r*iKsnpf)wHLpuY@ob_3J%z_snhW*Er3x2d}~ROiSD|+P68fXWWB1L9b~7Q(lif zbM<@XHt(7HU{lJTxv1_vD-y_|J~_6oP4`qnT|Dn#ZMx?X-2SKai}}C!ZsC8Wv;RY1 zH~$ykE&S)sJ|NJ22OIYM71;UNtM}K#-UQR$1Yhey164=#z3Js_uQ@ewZ?ezudsF_= z@By)@ft$3JQDs=b>ng^s9?jUuW2k;KL=binM`CrFydIrsJMJ-Y(e@+x`8FC~#kWeC zMjC(Ri{aH2fnEr|7RkTmKUt3%IW_?OeNNKyGw6bmVyGA?8PUD+=soZI-oM_zY|BuT zqASsklPc@7j`SL2(U(q3C4Qz^l%AX&E~d0>`-$Zz$D$9NT#Q}6wX?gARios5^L08DN(%2-&G$A#M>tEHkFBnpi!TSQzu``6{kLCkI4Q+ zwEOH!#YTN5o<7->E1nJtZV8n}eJ-9q`BkoX{+~kSO!0|i5MPQX)OW>CG%M1{*Z97( zO2Ww|e)ycy^W;~7N*@`@-;Gp;D6_0Mn}WA*@2M~IHOf44(>$T%;OeV}D+`A3bfwUG zR2rj{PEtyxCgu7^-b5wmg(x0L*;k;=nj}jP{y~9dn3{4@frU;(9bq(1%P%b(IfJrJH!jOAEL3kxjiZcwf|=T211u|!pIkx$=Q8*)oFE%{p(Y9WKf zclU0;5ZB3j&-u9a-c2<>MKvxj{z%fA%X9sP@SDh3n7tymuphKAE-%_pSh(D{3b);% zZ+A5d7?TP^Hl3Zja^32p{8ALZJiBm1Zt2y=s4IC_4%6gKzD znlr8^Q>Zq}hf-zhj4L;kmaSV0I~HZH%{312*Xoq+Ur}h&MhfT?&JOTmJ7+=(c^v#H~ZR4z)Js(%;%^bLp70K1D}wV z{DWE|Y^YYl2%ly_+GtzUX&mr%KRMi6b0MzW+jQiDuJNM-+vM<$sIXGC!@=PGszSd) zZFygPAheBn)w&HuIjT`stXo&;k2-eKl$?N0`8h2Lt;#RTE;O#qE?b!gg<89tX=5Im zT5|HbKE_e$#&sqbcho2&5}H=!mz0*xD9J*b_p6@2%9!kv&~22lXhY-Pi{DDo?R*;M z=BV8lveg(P8f#8{Dj6Ghe{)po+*mVifZU-IddS(G9RH0oOY+OJ%W{o*xrN236QlRB zVe!J-)F-}cTQZVb#X)u|X?*OQN$#+sRck2ZvnFKDQHL#m6Y}}`3hQrMC~a|eNolSx zUUKlT(I=>2YHaUaHD?nV2Nuqe!W+Lfe=3C;t}stP<|zLl(fI1TZBnO+P-Eg1qo#3P z(G|U|brhX4-lr!SG^nLC#<{o9kvs9U|8EF>cwSR;tfKTHk1RXfMQk^R-0^Yuvi)H7R^r z%eYvQTe_jJEZDSDb90OR%?d+44Qly0BaABx(fW_I=H>(KEJE>a@a0p(q*6 z6@iAKHW^G5y?YzG`LZ-&6uGkDU6!UBT4SG<7Hn+E3oWg2ecLCcE{(A<+oZ4&YRmIg zZ^q@7{Vzrb(#|(vG#-1SQf}uhYW^kET)HuzdWSzGDBqRYrMbqIf&6M;-O(uHo9&E2 z`J4R1y}w}@jsCQ)nIA{&!B%i|^uGRVE<&hhdH zqkL@_Ijy5SGHiUWj`EBy@)$$=HXY>1J%0tXY}mSu0x2Np45pheR^~O zk28+%l-MaDtWAiVC3i}c+YRhIHcTGYqhF8331MquAXw5nv?T4OdQ|ML9R*|dx^=3!J?w`!G7xj4TjbNoGbN{)ALSbWFEx}+?rm-pqc z_z3T~?(xGLa~`)yv2tNqBY(>(wGHv<@o@YMsiQVG2Z@@;pY7VX|Eo|*dpuDW-&VS& zWw0JY%QtoT5Gb_YiUa(`UXhFC6{dTb5UU(@a`TNQ8}oBk=a!jT&DU6)TP}5IoW9K> zMRW^V3u4^-TimkT#sfdsNo^X7HIY)-<@USl+s~wyYEg@_Yu8_$8nYl**F9(i{+nDi zZnwrqEElBCb4lUkJ|+x_Majv_f(8#v4P(@mYfg4qwlTj{En03)KVunIW|vo%I=9o% zS(3XRt49o-jm>kp+@|sARWqa*8P2=X&q!^(eDLR?HyAp~p;0DZHLG)e<70@GpjN=1 zFc<@H4llN4oPun#XnJv=bM!qCc8qvrrAo2T}X`wr^Y zBSr2UGVQ9A-tyET^0YL0mQfyMkZ1Rl+r=6>cJA1rYnU8;wMI@&k)zvn7%N}ZXIR+O zp<#0Oi33K2hkNtm;;o6#qOZLuUJ<>;YvN7uj(AUeD4NANu}v%z)3>$svYy_VMe*&A zyW-1FLa`3JvKt4JE{eEE{!8Ky{~ z+6D5DerJRtg=-heKamsP)cGq~k)pJ`S{6+f3u}i|1#%cLE zE&mvMk@)a|zMwXX6P9R~w#9di7Rd4KwEVo5f1()`@z%lOKZf{XbXeRGF*<4a1uehm zm)qYL)MatkB@s&#)x_q`1}*z{-cQoll z6qA&Y!H||0f;CQL3~j^uJ~+$IWF>s45;0WEztky_e$x-v@~@D5Hqcwkzt)MKOshm+ zt;CE_^dptnQ9A7B#3^mAQWDaXwrNVcG^Kr-(jiUhn5LLJr74}$l&)z?Vwz$|Q;ca! zw=|`Dn$j~(>75RN9kl$L%LI;AV#nyj2o|a&4^#T2DSgwFei>@?v0DBuQq0xz?^;t# zP*s?y6I)mt#WY?SkfsbwQwF6egVU5D$UikrNlR0PrYXbHl;LU0RcXrAY08K+Wn`K% zDosiEDKhCYdHIGO!1p#DwJ6XpauQVDqy;z&r)J%_>F2FI2ti>fcC9X zW-IzRh@ulyS%@-vmNEuK^eJ);BIauOrPjD}RdMs+zZ^)=@*i8n7AX3KO6(%|F92e- z{HNBiC5nEj61&WdfGhwV;OExBY(>99iCyW}(MT$;2l}+eSf%J!x2l+?jL8kCn1_hv zKs>&0+#0_?(XUZr3*leZ>RF`d%}`)tumkQP!{B3AP^fQzEjKp&fd!t0e7-k|9DZnbLEL4f>Lw=~f~TK-mWEH%PZ**uK3366CWV%>aML*xeoEs@`XBx%915BX$Hg#XZB|092s($bvyZHTPQ z*WvpQlCp@8y&cZ0g3}&T%RnMw?3Oa~yDsa)xj}u{>zI03J>H9YybtwwKh+Gxpi)}J zoM@3gEZF-&zi|+KqL%*^9Q3fSEc~QS9Kf%aEYYfMm_LH3fnf}lVvgabT8+_DD_F3M z$JN1sKY=vufEcZif`d;ZnxIA5g=(kZ&ua10puad2KxERHG<8ux;2#$SfSl`<_J8MZQd%3yyNEnc%ij!+|GrNS{~*{CEuK{5<`0l!aBxUdAUXnu1P7e` zTfVbels^|7ky#ea~1_I-tf*6crnfRVuh zzVHixKW6<_HNLu1h`Dvbpap|EY7SM#qw`J}s!ZeuAz(7lIatVd5b{0B2mdL-{+Imz ze-qQv$6*Pq1Nd9M#59STdj(7Q1%bb!R-Q#jc5uM&t*Mp;`~Mk~V5U~!3y&>j6--@m z2nn*fYDGv;5T;e3QDTKw9%g0}5S|zmC_)h$1|_K1jSCJ4S7*X`S`n%f5BkSn5g8N% zlQE?Xu6W<65j*G|!6 z1w^ZgjzQiu;E4`YIRAh>e6&{J7o}R#VTz@8Bv7S~Fr8UhCORq0)0FHqWks5@GEK?x zt#Cvaop_WAi8Z{cD|ZD65r&rWPr5NhdiRu8~rVa`i?o&#OUmMekD1V5+N_?p=V1&k?e!lLO zp&6-uzGzt@_*Wuglrl}EW4SSKcD!Lq%W^}ER;G$E2pX#{AhxKVFETVs)z4QMnp2O` z=j&i`Vm#7JK$?jVR}XRRTg6RA&=dq^k~nX1kGL-FL~H0Ygic3jmNI?UoOr|N){vP9 znT3$qsv7Op&(j&2j_T*x%W}WQH?fLqk^8ba@$G`vPFQh?d5|(+mGaKp^tqU!Nl-se zUM6KBqAo(zMHCg!LG*%93xol2KpUVPU`%uHzTn2@ z0Qf#VqqpyC0Q|%S+Xz^I3ZN3G0UCgtfkVJ;z`ejJ;A!AF;6>m~;4E+v_#F5O_zABp z(GMj(0OceoCMA1IuZjQ7ybnhT?f~uvo&X?9^E~hp@DTt}nhO9#X?{mhaSBE|6<7hR z1`2@d0em}83wvli0Ay-!0S*KA0g$DA3upp9#W${HAQDKxH~vGq;$|p-0}&w;fGi*f zz)_qKJmH6++#&VA9^h8s2mpmcpm4}jz}vw40F({+9za<`e#6fwp{$`DfG&U$fbl~! z0mu)9@}aAM&8URXYTWDupmpd007eZx0l*5O&^Qzthkge@Xc#mOgV3-P0JR!64wwW$ zaM&`S2q*#UKm~w`3xmusR9YCU5r)_NVUGb%04|0--X4Ig@X^3E09sc#swrFm zbpR9!H{-i#;g17oHsS9AU*YFIp-2Q2iRc6*0)qimQ3OgEu^50d5v9OJ0QDV#DvG!X zK(mZE37~`#C}9MuCIVFxfvSl>)kORRz>1Nv04y1qg!+%{iyH`!oC2VvkuYN<>L(JJ zL?V+&2jB*70B!`(pdudtUIN|%pmZdZj*1340#G;#oh=G7qcVW;z&rqoM->BQ0IV1V z#iOcG|54C9>Q>+(;1uvW0L`PIdDJ(+CHzWWIDm#4-5r47=n=pSU^Z|q0MkaV2Q~vc zfV}|fJo*mcY2X#$9pEeg3&dyvC>+xc^&c|;7!Hg9VB(mSKq0UJ*amn3)OQT(E9P3Iz)7c>=4xfz6ssKy6d`&Hod~TM|6+u9@QOw zaVaq=2`TMTI;3<;5p4#=4oZ#|@u?8sc4(hrLO)!zy{hjp5j#S(9f@mPx^|d|NAnZy z#!eb1^y52^?=`;v_yOaM;|Gr)I8MY)>ujFZYg+$l1Ev|L4W2e|+T>|j<3#*iBxpZ> z<^rK#IBVhTg)0;qF}SUd7$y8^^a7;1bXAoqA;r--X29LR?~b16>mM z9=gQweQWu?627mL??W~RHuD2!c*k?JG|`3M!YlZ#NO`CuKg7)rkQ44$ezYS$${heX z3#7cf52aZ_&a7X^|O8zi8;`pgjehLvE zN3!Ec_9RZ7@;CWYNb@v*g1&c$`&j%u=@S6%%}DZMDg5I3%g7;-zsld@uOas9MqFa~ z8+3`|Zt)BC_=)znG_pSllK|6A9NKV3=r+v>nj?;N1x#J1=KqIKz)jiK~SH+l*qQ z=q5%nkuG|O3}Hm%UJL=FMG|h487^_6?^;}9MZY2l90M)l#DI1Ds}j+-1Tn_~cap|z@WQd+3#z5TI>qNpPMEOxnK-fftWg>Jc z3OxuBC9z63c}ovf)|**KCm^<}m!$%qnrMSPf5%c4Dq55c8OrFADLSv}p0J60VTaxhzJnn{7Y>eD;dG+|BI8PA?B-tdy9Qi$BSI0d4k!lJ10_HyPzG!O z%7Kl*b@+LJII)lKF^dGzpNUOkKmQ9d?nT>)6$jCE4)Oz9z@7XAx=9{0~Nu~nQD zI@H}ZCd_7WN<@jr5c6?{n|Sdg6HlVuJq2U*g!38XBgAw3YwFEH^O&RD*L+m{)> z+YnEuAYrT5VU;s*y}@v66K~=6Z6-$G@-C@j$B#_Liw~%sImAb}Z9fxB(9=8ZUleVh=7qGI0QxpBaQz z0#*1vMZEZ(I+9-e$*tl~(NSUekxx;LV(JwQoLYQK#w==;P_bPJgP)GSrs%{D6mciY zu2-T&9k2`V0`;(fUO@xF?}0bqx*OO7>{a46;<698L5aimXIvo+q*uD|)4&ZtmC{8t z0{ejju$o@!k*D;)LrpJnqZy{rD@o!Ya1(Gd?4wswwxMb2mHy%u;8vwS-;V1cTwlla zFs`@bdIZ;9xZVaF#eE&F`vD5O12_iUiEgP^GPWui+%9r)J67BU+zs3V+$+ol;y##G zuS^#A1IK|AzyrX8u>M1^zFwIj<|;GA!@x<{T(8U#j{uLt@Oot~iZ@rB0v^NHH1x_s zej0_;D~tIHr~~mf_*F6bh^Lr2yiI%Kz^lhio;^Qz{T7T4)qF2z zsM|0N-Nzr|&+?Zr=zqvB^3V9!{0Dvs!?1)tqDL?3j8SfY7%b8;KrBGNE<%T>6E}$4 zVgA>}yW*lEE8UeoIPN+HKl)sZpL(uQ?p7X9PAeZMzgc8UtfjAIirJE7nPZt}S!h{m zS!T(ztha2kD3(e~jite|+j6tzsO5gkNy}4~mo0Bt-m^4WzO?*e`4fLXse`qbHPt%C zI^DX|nr(Ghw_9(t9<|tO3^8*Ce98)=(t z%d#!C71%b|Hruw?%!Z2!rjbwoJg9331z9RnPxjxF<9N~Wy5mj9yN-_> zXC0q9E;%($tuxx0=m2W#?96gr>s;nsif1ZbsyI_| zzTyY>X7@I?+2(e;tK4<&BknsrX`V5jHJ$^WL!LW5Cp@Ps<;sZ436<+BZ>)Ty^24fj zRasS)RcER@RU51ORu8OBt)5eDuijZ*Uww1+vFiJ(PgcKB{Z;ih)jw7LTBE6ntckDb zRMWerU(JA;)SAULn`#c$+*R{-&3m;`wQXxV)poBn)uz@iH`kWb*4I8=`(^EK+q-X1 z-9Bo2{`QjXV*B0OKiSb{N7o(GcP!kIy<^pm?K|ps?Ah_$jyHFFxHDpBo1I;D_S%`b zbN$Zpo!9NWY3J=b-`RP#E~c)1-GsVXb&Kkj*A>-?I!|3)-Hmk*)IC~vy6)AwpLg}% zwQN`3uG(EU?s{w2$KDR!KHm9We3@&9x88f7_i68o-nYEX-p{>1cz>>!>LcpY>eK5p z>aVR|P`{*pW&P^<4fVXB!btSDw>eK@yrI($*3O*_qbP%EtM;(gRmF2J6L??K2i*SN3_^@7S zereXavI%R8%QmUUVRBmzYx<6Qz&Qs8UCb-<)B_Ya=}@xCm#un^%BP2V0ELb<D7+m|HRqN40_v!mV6aRF<7zREnc!!KVNx zX0P!|x$zysrjF}BXyCv>UQ=#-#_`hJ_&Pao8b>{#gOj%EksnplzaNfyb$DDK@1VT+ z)b@1lV_A=lD+l(#@s+jNh3kqYka@kidGS4t@63yTFe1Wt7HtX+F*l0yRXEjQ_@qki z9yMyz%e=h#=Cb&i z((zX}#4BOTvAttq?4F+)dm@&xXPk_^a>x%X8on~pz>7~C0P83zAJ4>J<112Hv$KNn}C~vTYy`EL%?C+ z2yh#46u2F@12_iU3ETzT4cr6V3)~0X4;%+h01p5U0uKQX11Et;fJcE-z+=GUz!Sie zz-izq;A!9);91}~;CbK$;6>mi;AP+y;8ox?;C0{(@CNWE@D}hk@DA`U@E-6!@B#24 z@Db1iGy`XWbHK;IdEgV^0&o%d6!;AI9QXqG68H-E8u$kI7WfYM9{2&c1pEm61pEyA z0{ja62K)~E0sIO4<^FQ3R1FZxL>eM3Q3z2eQ5cbqD4ZyQD3U0OD4HmSNKX_?6h{PwVD z)Q_k?k%?#k(Lka>M1zTj5Tz2O5e+38Ml_u0Dx#~2Mi7l88by><DC_XbjO`8Cec))X++bBvWR97%_N#dG@ED+(KSTZ63r!=M>L;k0ntLDMMR5< zmJlr^T1K>-D4S>n(MqBmqFka?M5~GNi1LXFh}IAl60IdFB3egOOthY;gs7CLjA#Q< zInhR<>xecHZ6>;&XbX|r;I`47nTQh!B8A98WF@i@*@+xPP9hgk1(BP`LsUsrMN~~x zLsUz&ooENqPNF)ZT|{1@dZGrR-9&qc_7d$Qx`C*XXg|>bq8o`065T{}Gtn(Xw-Oy9 zI!ttg=r*FGM7I;&L3E7hPNKVr?k2j2=w71xi0&skPIQ9k0ip+q9wK^}=p@l2M2`}k zB6^JIaiS-Po+LU=^c2z4M9&aCOY|Jk^F%KYy-4&D(aS`y5WPzD8qw=SXNcY)dXwla zqPL0OA$phSJ)-xCJ|Oy#=p&*gqGqDAMCXV;COS{_3DE_ji$tFieMa;-(HBHt5`9JV zHPJUj-x7UC^gYoJM3;zuB>IWyXQE$-ekJ;i=y#$&i2fw{OTzFc<3=Kqi8Mr7q7b4` zqA(&IQ8-ZqQ6y0mQ8ZBuk)9})D2^zes0~p9QCp&RMD2+>5OpN#MAVt63sG01L?Q!` zk*FI{ccLCdJ&Ae|^(IOpN+#+<)R!oQs2@>(A`{U7qJcz%hz1i4Axb4mBN|FHjA%H~ zRYX@4jUXCHG>RylD1&G;(HNq!MB|9Y6HOqRNHmFPGSL*GOroho(}<=MWf9FFnn^T^ zXg1LtqHBn*C7Me#k7z#80-}XPi-;BzEg@P;w2WvuQ8v*EqLoBBM7cz(h*lHj5#L^l#0B)W;{W};h&ZY4TIbeQM}(QQOWiEbylgXkF1okVvL-A!~4(Y-|X z5#3L8oahA614IuJJw)^{(Mh65h#n<6Mf4ca<3vx$k7D?fzy94Ueo-u;^y8f!qyi-l z7p4sjW6EvH;}$)haqvD7N6%iNd-Y4s@8YV>rucYaw?ytFErNf#a^9lHH1;w=VC7NUemB*>C%$&zKm&w}MFft=)+qiwv{Iu@IbZ>@Fo*+w`Ph z{0I#GRB|Wlh9n&n>TC7*eiI~lvF}gmT5suOy&)-_e)^1l8?woITqH* zXJ)WR)1Q4Qu(w$v55*V8hs!L{-tTG>Y#&8}?N>G12m9mOuT}*k+7K$dePJj2>`}2Y zlh4|V#2x4C8^n7k8-DvEeYu>m7!RrVT^OnH?JcL}9%i?_#)Zv%DFKDj*mu&s`cVsK z_qw)fv#@1$4-08a4JFO~j{RBxuUd4FD6eNb*&mri#Y?h3*G{#dl)hZ*kjZ-3-_hHj zZL&YB+?!;7pC!-@gNhvpS@y5(KeCYa_$wP7q@IohIm!NWhbBk3a!VGr;YKiRN414X ziH=eD%S`z5Ez>lPY0mk6-OxnTMnuiPF~d1eJ1Ws}E#8-+?mIiyO*c5~vydp!aRdE9 zuq4M#ohZO*ytc1%c&2k4{^ChyKmvZtm@MYpnCM)+0Fj%W>y%$vXZ26UIJan=TU?tV z6_u|3y`C=1WxSNGr)-_ou1)G+U2*lK^n1@a>nw-5O41_y8RPvL=YCgre>%MXRU3=i zTby^hx=ESNAIWkqxxo3y@)&th2=$xZt~mCwE4VpTj0uT zj5eQ^9cC@M6?QPv7%NautgK-SzwFNN*4qD(LSrQUGykJ%e4s%HiSE^p+8~6sYCL}S zp$Cr2^j!2 z_-%(Bwqk?WUyeiuaTon1l_v3!4X=qw)*G8uSs!Y|hxE+`_2ZM(tVrsg;1S<9i;K46 zWD24|18?0&B`Oj4b#6IHiS3iB^wcOlt46CSsk736gFkJU5s6qP&edo``lc#b8YRm@ zxzb0?LYZaB2}P`hII@nv7sHgoW+mHJoU&X_<~G(>*+?1L1A+DbUu9Pw9z~JGdpdUr z2?^jJM`(^DlQ4uF10i6LBLTt@&J4(v1Tqk=$>bs+;#6j)&zXb-!I9OaK|mBiWfj)_ zR^<`llx4ch^wMRDyPS0lt~Jbp!-5@dt6CqE zG$KS4<8CdmxKG8nPkF9}EXS~Q3d(Vx_gsmG)%X%vl>3_Eet~sB`$V~9x}k2;+k1L% z|5Dr^1-0V-9{TabYK5LLu(;l#cm@SE1E=N!RRk>cz`0%kg^!7;s}-F5iOYb5uNxc#d9*K3eUw_+KF|EY2H)^-}ClV zi|0m+=LUN*LKUomeJ-}Fm`w&%u%i@qNEB66!P&%gNP_}Kc{~YOcsmeVp|CT8B4dg1 z+W0-YM0@!o>cQm5PbM+DSz*_(juN2gW9`CLvV21E2J{f{$->j0#QfOfZ7jS(o9$6p zE<$kl{cIn5O4zC?0!zqVRwZPwPPedM#IRp*FN1`TwW2y-b+p-Uxn3;dO<{Mm688!wncEJ#|Y^{UxbjtNI?NZ z5GH_-f<+N>2M|(Nt_T&ZV-7Y1p=K^&j0Qt^6gmXEqAioILQ;Go7HFOc3M<&WCsvJ-7(^QV|N(B~4_(~N?_-4LEG{%TV@%;#iL`HlM zBoZ+On>a%erx1z2{&lxcRx(^@Rm8=bNTO{)m`LJYO(d}goU5dW&T#$$!zSXHAfD2x zuK@Lna9vWwC&OwuDP1Ie%@VJ{K>^hLL={Q=e4#~(i;?1_|3-);rAj}5B}zu%S~4jT z%*e>K8JCqSQmJN%QdQV&b>iY(iUgjFjG?r@GE88}r->k)hSxM4QzUTDp#u^SSUL|( zOV8`n?*jE-;CfY&E{E69be@~V8bzO6yFeJzTwL1Wu@|MGrp`+8KaeGhdV%Ja#X5#R4s zMtm={{jZcxMYFli>tQyR*_EU<&^0Pw=>j*ZD|bg1yqk}BWLW0m zMaLjn+zjlY;ByxKTYiJ@rROA;ZYd}&wv|}qw)oGm)(NzupnefxKP_BERm+tg0|2#d#|K=y@Y4W=E#|VR5?J()JFp@?9C#sCpCskTj27dOB`{3qK|IdjTnG=GvxhjCm0sMg>23p}< zxxBI6Zj$Hxp&4LKmb+&dQk{WfI4o{+m~zyn=LHmJRo~dhVZPf56T2zFL9J8#f|0h5c96Z`f$IBZ%X@)`NxB(OHSI1Y0)L1n2hH2NI zo|>OiEbMuZqd6kTP3d6D!6Vuwtd1ArkwVPRcF zQ_$!L1*tGctqt3o>-|rsW(=o`Hp<;bgJIaBkPb|w{MLF6>su9%)B-`(@g+QKkXstk zQcHi}xM(~l1$dTwfU%(y1Gylk05D3#Sdl?o%M?Wvgy>bBJNY)iNejUc(tue2z>4U0 z6+wp5A$f8!~2S<>_9`N5UjQc=807COd&2% zl`%G05qBQF2F*-UIi5r~#+fr(;!Nl`=qWD**6ntf#?pth1#Qb-fjoa5V@lDH~wM4@eUflI$IV>`;#Y{F8wLgi zgW)L0%`(gDns;h41r{TmYo)D0Eh>Z?!0)K5cr@7tT8s!JoqzV*H-~K|pfuVlX^9kt zP^wch!tz6mE6UOoXftGDV5Bi3f(pq6eg?Z=217Nr*>~C@CK$k^ep!%7svy->i>sEc zsTxoa(xJ*x9=2}{Cle$75WqCRUxXw!3=xHaH{q|ww$^Bxog7#)bw7wP?nrY?9TULn zQimpAT}c4QP*;FyV81kwR*5SQ`oNHG)f_m2Qlz3-F@5N86xg?+c;Rf@1*o4kUDzOJ zI#OfM=3xaKXe|jFZav1(?Slkz3`0H+3{Kuh717h?R?=y?S#)aN%mg}p6o3)snI{z% zOv+87D;U~rUSpXKUuQ5hfEtyMFDA1!i7sR4JTpDPoJ7xtBJ+YIj7>J#JS~YXVrbJO z^E^UYGI}~8C>fJP-_6hyb1ihasW2%a#?Tyq6iOqp^Q}T^xGhL~5nj8vAtiQYa%ut` zh)~H%_$fOYJrRq}#wMb3@DLr|&Q3(|{1$);#m-1X4@2>VB=l^uEXr^j;2aIjjDss1 zu3We#!IgrZrB)eM)3l*FmNr=9W8zmO#>L0S52Hq=SO*WI)=+Dyd#H8Pde}nVNNu8O zjJ3wP;hU)~qv}Un$Jo-gj@`yk84csNO`tNiXH{qKFz(E<<|bzB8eeTr&P~eLJ;h!C zxJN~fVmm-58=I)+l9u9arLAdgGpp~~VccVEpJ|;vByI0_>%2kf`zosE-)r2z&{~;F z-M9F_lKUxV6++AG%PAx3SZQ6=3IHPjq+&&$HP*F=O+(xe#ILt*$XsXLG$suRBdhDi zr6F+>Q#WD5#I@tcS{p{eW7QPwH^YW9a(3=UUAuBAbkJdKLfv+I3)N_EqjsX6cKcqc z2@dYi1N+h8`>Y2{P3TCI0p0_|;Wrgpu5gkXe6&;_6 z4$eVxH99#D9WF-)m#$xp&a8!}t4_f6C_ESqN$scLIt|xjMsx-}y&63OMbFg0GgeQK zqmEj7Jvo-4&dNED_R`}FPs)|>nd`K}gCu()y}bh-lJIqOcO31I;YopKAVvQwJeBZ| zj&D0|x(u#V*GN~EYlW-YwcZ8K|ypLwvYXi z{f7O4i|0zX3T_d%oLk4)xLsTe*UmY)!`un(3il`Ob?$5KKim!ON1oVFNA#b zd-yuOjX%a;;Gg4Pfpqf^_)j3!{9FD^lq{7=3n7DirL<1kD*2>C(oyN8^tg0h`knOFt+gx`VSLIWHmp>d-c5eLfDJKCtBZUE3RET(eq z8!*faquWQ~M-l>th8*GHUwDnshV`INi{FA_ma|`+MdG4h6chA&1H>*Q$_P@a?OTFB zz{Udxtd1TpfXmOrK3^rrXohsZ0o`{?0v?fniqMV`6ZM~{;`p6aCw;>ZoQe|A%bDVc2k zkf!Ae3-;5)0 zTjOf}>B-9G+$5P)Cbdap@-z9H0!&&{pee}I$P{b}F*P=YnwsnnGdyDGz*wrpdNDRv zVxt&CA~p?&$0W7{htDLooH76S#L?3W^H{@g0-R$#r{rcDbt4s%7*hcd7Rx9(ev(Qp zaVswottnK>RPkdtX#4`0#Gm3XNPa4osmgQt?GB4=g$y!PY6K5dY550|-%0)fe?E}E zD)Hm4!MnrS1-&KBQSrASB+-?>JFK}OFMw$%lPncOKtpIgd7gowR2-2lh%zUI4LFW= z`zq(8cra5jrbZ0@4r2+5{uw9uGpQ5-FEynW?B`SmT3#zSYlujk)ip4CD zWlD)s7p9456{8ZX#X1%QAASs8l3#ljzs|3VmQYA7w8~MEB!g(onATsZvrJV8)I-II z#!{1$qOlws$gfNMV=9BnzpYAa6}ycwIB5i^#a?5K%3VOE;$};;nUp3DFiq+zCSFVs zF9viH$J;PE`zS$t6x2n06~j_jGbYX@h;zY^fl4>9+5A&BMpoS=tI}kD`3V)`%b^K! zs5ViKL}a<+N+w4q$kG16P_hL>+uqq&fs947)HvA?DjN`eS%SPQ5LFSyAWJ!klPlow z0E`K;QR}VAmdp@BHO;?Z2)Z9|CCDzH;Krz9{w3=o?~))-2Zw2Mx?_@j5H%J` zPR7#Y7vzs6QvbCC`L&>v@;k;kG4MTUY^4I8V}a?uKr$s%(waw1aPF75r;|>a6}cVH+91SU^O)sA<+bUL0%@ zX^0o6T1M&Qa9*co>fUlI>xFA_?5}b=)=DoA%~VZSwT_n;w29Ws)t*Kh9ilT{_lPkC z>6*uzI>$uDnHZZVdt5M%k{s^uocSfe2(s&%||NQY>>wa6%Oz z7{|UY&Ep;Kc8!d8T98u%Wg5nzeAf6=~{^fQcB`74T?G?!+J&QOICQr^uTn{ zQ_Vzoni`b>M|}fxQasvwpcIIC_c(b-{@vAOyzGJX=ppDb`Z7VCp(oOm?Os2^cXIGZ zPOP7N(TU5RLL5VCV%e9`WvBRQNtz7_e?r0;bo2D}F;evUuKcWfnp?@%_f1&ew`m8j zpl+1E|5V_8QiRSVXzG6oij2qcYqu|*E4{iG6@?ZS^yk(}f2IhkM1$rY&W(vN$kN@D zS%4-vm1%etuaP2X8vc%HQqth@7=Ko3?(yh75FW?*CsL%xqt76Ce9wQCqR8VLrs+GF zX+$FtCbfdcbf)Py1m|5vPbu0vGDb5^|DmjjMhq1brPgjalHW+z@dIJ43=vAkq8$1* zcRQbiGt%w+5Y8wi8*v%VR+NE^VLU3@+ki|)OO0x+L|0}FX`ogs?vavDihHcjYel(~ zQYXr-uWQAVQtwmZN$dMs@uAe`wD{2al~!Dq^nK5W%hq#RIYjFBtsG*#td--X{%7TQ zn-nPKd0JoPKTIR%%0==%RBIALtIPMu`*5TmvOb3@O_$e6G>IEAwXRE$*VSt{ge9w} z!q2(hc{WUENKxX;C@<@sNi$Y``Whwu{U_xb+pB#fCPC1Hb@DdbyL~CjWSTum&UrfH z-_xmzoSp+@(U}tX1M?rQkuS)XjXj`nKV+qr|1kC-ZD|%poJ)$vHz`M>A2Pa`Am6-0 z<4dYvj6q4p5VbMH*d6IqDwPpyD5D}=V#cO$))zq$Xl$(rj5%p+ZMV3KV2rObwzZe{ zrz8`hv!T8Wm-tWKGF)<(;gY)yIPgpS2T0fEFcyfM1AnI60oX!}1;MlkXoe$=1%S?# zo#Zpm*{QDwrHO!te9O(#?A~lARUB?Y?KS%t~lnned%1O=4s5IT!^Kb?%y*-ELIJM8*(v3{WUYQ znE%?f!hZ;qnl9e{N&ckpHw<tnVSRudHC}m;qjjb|1O$&3z&aOivLLOl!Kh>ok^14Vg#n3OzIRv z3KU02ro$-ce2I~ZhR%0Xjnh#QO#3Mk|5e3QCt306I#xUh(|@i%OQD$Q{B^Keal?Tm zlu$}%Qagr4UtcXcYD*hQnO274lBlY zFY-YB<^@d5;`_<;_H<*k%(4KMb^B_ShX}Wp4%g|l1wdzhov~ybZlJ8nAmk1tfyW`mN}|$pltl#LxQ!B{b_=!Q zOL59rXNbvgYyLZ`OtWGQ)5kc)+Z~QG@r0zr{zo|sXR25P^YP80A(uz9cywXIiu|QgR%2-gEsx_LVRi}-n((!@D zev&{Lm}E*0{xZ$VjX4BuQ`Fit-#YV2)55UsF6Ot2rt`(;wxM zZhTA98Q=e{t2zmoA z(=y{lhS|o~df}W3A{k)6&_sl)^5-_KX|!pUi4>eF=}qaRAn9kyR{SWIVsV=mn!Z-2 zwlNh=B`@Jmmo+9!g2^&PXWG~vQ;E*Bvyn2Rn4VtAOnVbdd#9mHLd&S;*B}kIC>FZu z16-k)jwP6m!GsIpCgoy^E&;H1za`CXi;-{Fn!Yt%GxtU!j93#)MvqQ^DSo7YqLW!N z_g2xhgt_TTX4WQ{wH}ceuQZpj*&>jowlQ~7&e0eBxkGDCGngmZ(F55GNpGeJSShc0 zn&O9R4Ra2{W@f4cuJXX>)KZ#xsXaqVH?NR%=6gjG)Tw$Fa;!}-*LrfCon$_wHXpKM z&ZBw6Ok>zB!+ex68=I3UktE0q!(t18$LxdDqs<>c$|>NJ1oI~zNdeI4L?lZy|6mz_ zEPs>q7I=xKLkEtwjE=R;4YkZ&$t=@>u?d#3oz;-WDyWYtBxx{G_%RezJFQt7)M*-mRwPH#p}aP0NN-TLbK!RimwT)Ub8?N@lG- zV=XD4fT@+A6EN%0+P$qijWIqJ$O&vwq*))a)=BBsgS6;aUrMmPl(ihPsXZV*m=_z^ zz;x?RlFoWf=%_01*4ug^BzDaFXxl{5R*kVBg4H@(j%ZK8*&S^wC_Ou}YETYD+C+j) z+$NGu)I&JGIbLsj45{O$=xkq#j#OkN#l-~M#Rc_B7)Xl}Z<6U=$83*`OwTdfpBHQG zQ|z;ylv2r1ZC|XMkT#?wbq)Jc=W;3Cj;>)hi>??}ntm&peLt`V*q&hD?rEd?t7=0_ zwh+o*5(#WklKmsK{UZmBQgj>p$L_vDeb4?GxpOI^l35%wOSgX`>FwWA33K&Hj+Sai zODFYKBy~jL=x#~}qO!%eBusO3c4n(G9Q_@GnV*3!q^WTnlUFiF1~4?iG4ysVTe2l+ z;5x^YjV(P53=1q#^kC_Z1(ME@=Wd+5jt)%x^vfQUqjNmKYpDjpPC1TjOkCQ;aUR0Y zo^qVrn5Y=^sVv!X&0)X-DElc}H3+^2Tug9W^yKf}wup0F3w2yO?YOq_Ko^SP^p7An zbq%j1Y(wzx+_vvAl=Xz@#qyJ!(Q0S3lLjIEXp%^GwxgmtyF*C31ZO*BW1v~52?}@0 z*?(iAyKsrKBT9Mt~izN zQz|l|c+RE`w9F@tTgi%H!488S;u)-t$Wybgdn%#M-z%4^@P^1X=}YDTgO=F zT`=}TG21uk%UJl`j72P9tQnc{SUFK$Sl*kB!@`ecyzv#d5uITek)L7!U{Mc*;|>d; z7v^^(fOPm_xhfA7lf_JDY0NU5qGX8}$r`?qyI5edC=nZ^Fw|Tk)9_8aiUo@8;xTC{ z4dD~;s^Hb|Ix3D!`B?7Jj9AcWUGt8GwKg=Y>H^loC}m4XhQ$)Rl+sFinflp@`{Wob z>iLvbS;C>xEekp?hf-M4#qSc|SSNn34Asg365n)64ybGiYeeD|r{$Q+u7TncNhH%W zGU22+S8;Qq;S8(nukz=c1LXvHh~om}Vil3QBU5D+kw+?iSViP12)k_+k>@%t;EFIu z(#cCiUo~nZ9~W891lg&mic;t3s>Za#5t4ZcGwGsbj3r z8)>p0&dn>MjY5nj9aqE|A6&_dH3`NVr8ZEpdgJ3tEW6%#RB@D$sNvah#*2tqmtd^( z#B_c2Mp%@ApIfCfbrNGSHaeQnr1a}Ch^^L}#(J)-#W-G(qceRhCX)Jh>&;Cdk7jOj zS1~Doga-%c%>9Lq6oT&zF_~-|5viNJ93wSd1eXC z8uUuyMdhvmYwdrSSy(IgS{hz*OH5_f^PEddJC?XMI3)fXKf~?t4UF73=oxI zjq)T`_9W7n3VK>603)NV{oQ(6(|q*2Pth~CK9@)I)@6$5$8^^Fy>(=L-n-@*)H>_C zN{;PpA*y)mIRmYrHz5BP;D-e351#Ckpy;__jC0h?6wQD2jM4}eV<}yaT#Ep8} zW}GhJd*`BNqgWI-A$=a>W*mmI6$*0;&CaX{CrcT(y8Uc8^7xadvyC-Ck$6*IesjCyUKq5kV)uZMQ;i-;2@==Li12 z=?p0PaMT#XFOE zEvng>K{ONwSL#V;A4J%uL@+S^07SSiJ}^59#<#1O>WXt}ymJQM!7vacWyACmNmsHE zGK9``3td4%^$@z?q%#Xbce;fd|4Hh)cqjiysf;zpux|4?W6_^LCCr{EBdn?D?_prM zaKxqw?ZMEw+t}KM`cwp1$qK#&+CCRXRh@CP9Reamfb?cfiwAW@9%Zm+eq zlRwOYWQ`nl%k~O?kUs)%v%Kln4Hmx7)%E>btS%Xvd#o;OZ)@ufhMrBG{- zc}hN4dr)hXB>v!Oqg4BLp!`6+92=Kx zo5<3Q>#*@+5>~Pna&*QT;qTgUi262jJ*XlkL!Ftt#o2CrEZEcGtEt(wlKR7pBWQI&*5haW6{<$?2RiX8!m~g=?T_!Pg?K9?#=OQy|qA5_oUAHy}Pv5pZGqibQ{b?TUec~ zk*BZ*ACuFYyiRZHuOvUMvn^7RBTXTYmtf2DWU1VL8md@{w#^7Cud_KD>aX7J@{0Uc zZ#$@n{7z?2c57ts#~)Yf-#!y2!1y|QdPDl#>;JsoPCX=!U&NM}WPj!q+jf9Ifx5sP zW`73B9|v|P*mrxh^SQCZpH!BoU@6IdTx~yIN&77rN%l8!?Crig0m&usOt*iCjgAjR z88vUA-u@^22l5|q=|9xl>m4}_z20%J=%{prr>;77AkwSe>joztW>ChGGewo*#1=Z1 z*@m$c?M!L7Zg5UAc=S;s>dtU>V*B~Tvg{>)=({8gUDN}Kk1y0DCyfd zzgERN-{RPsaej8%`QeUA<(|w0TI!10`A)8(i8tGACa=&J_rcjenp*#nrG-Vz*G&GOEb;%<3U9d8Oul1@ zzsyD#uWegg5nEgmT3mvfGb9un>X!zrI8to!!=c3wBcv*!xT=0*0*elH)#7)~upNK$ zr_oh)#qW5#R*9d|wSLLbl`I)2HYmO58GVTzjejm@{=|}J)fpwv-rjFYo^?MLB!cO| zpwT5?#g_aSTJqycR#KNxQs=7@Wn9|kTeho*JLA$eo-1N-LC#5S-moBUC zGM+20?`G%dN_V@5YUyYE0D4kcr?O7IJ*lkIN>-+;D{I-%LmN6$S*OslPG`$H?fSM$ zStoR)vZM$)@uw>R>PfT&t=wWsBh1Vj~+kdVOB(dJ(!F1AqOpg!RjA?@a6WHMHsaecm?h`vuz6H>$Ekln0$I z_xrmxEl+c|X?Y%h7HwL7xcsnho0cD5$;zLuE8q8bZCZXfwEXbd^1}~*)TR6|+O+(5 z1fAU0rW^3g6lMo%(+#w0{{Ns&4LA93-ZuT?mNxyi*f(Y^>e@m+w>u`1rZrDGZX_cm&7n=Ssm$pTlGm{k5(Hfri; z3$#K0nn?Q~VQJ)62zbwiF- zeIw{ZxjToY372}b7g#j|rCcIn{(eL07AuE#m;Qsz!2ha`{i`kpE9O7h46N^C-u~3K zzCqggc7ao-7TXSZ<8<&kI(Z#k{P~`ss<)~>uQuRLi^R9^ZJ00ARo_kLo7M{zNqy4b{ZE_%pNOyS>y|p6qz;GF!6&N+K&pG!i){@h4AK|Ma+CTjPf(>Fa9z9$y(){UJ6UsCQ31#U7U>fBtMxP1BnAEqM@w9fuma zMWO6C)O1z+(1bPgEZs~Lq*24|z?z|3@}%^dbV*k;!EMqS3iUN~#i?8_)ja0CT&j7s z;ZmpOgm+=7p(4ZJrvsd1~F}sZY%5vUw_+dh^{8bmHBibQ_(UOQ0$)Uogfu zzX<35+(1G8v;qJB*_8~gqHf>tzhjJd^A6h{y3IGe8#Y@)J@vmOB4JB}yH}xK((Jt@ z4M>XK($QVvTe|pGxTn6oA`QK~uEhG8B-!iL8#mfuV_nuHUK?nNK~9ar4x> z*r|KDk&s2D+!X>aZiLZ0Zp3EA593BWg0O~hV^E%_5Kqco{W8Q6#!P;m(VQV(SI!Po zmt+|N+Y$&nqMW6P=frcG3<2X$_Tw*QtJE~;DM$&%t0E`x@(W$TiBA*6r?mHl^0UiBtIpW9}wts~u5nXh-Q?b+w;`RNLa>VOC!S$h1>i6Q!{aHEU&A$s(rHiwYPW&LY z(tTRKyH1w&59g*+a>V{geEn&;-To|ZqxbXYuQrksE|R zaU_-Tc*U&+bB#Y27GZdLG}?8#pIj`-TlF%w$*0HReT!mj)8LupMe;N1c=-T-MK9w4 z2{Bxx>g6+vWAN?Ln0!St`s;gf;aJ_L7Ik;Vs zVDPPJ;{wUJN^e{R5uq&2xPV01A!0p5l=Gu{<90>FXuXl9li_0Q9ZDGAlZYwsyh8$>liNfE9- zKZa$y?*1t(%JtMwVJ%%XKfRL37R5fasHl%5I zSX5MipQ=+_>qtMfl)jc-N7V@Q$v9UBdevc%fzZ z8!FeHe&LNYh2ygp)-KzF^6Y`f%&COXgetCZOiupyi z_>Eq=GG{5ptd&{|Rb*`@Zd+;&JQ*F{Omh7a6W*-0-P|9g=-Pr44@g~IKUJTfQQQ5w zVkz!GP22F~0BNb~M5pi;jTfjw`l(e(T~r~>BZow~nsp90Mejf<9}>HTBpwrwi>JiX z;#qM>ydb8FWYPXUPw`6a;c>1e#o>VmlDmX=3GRYwL9)Z*Wjum%OdJ<)iMPeO;)K{C z+AE3OqCODG+~OnQnLgq3bgd{u-&h|)%17c8#5Fg>O+0XOT6m0~fg&s7(Vg)4U7ziE zD*ryl1~z4E{8PB7+{xI~%a}deVwS9AY|(DUR^X0g#S$?~b(Lv`Yo{}n_>74!FsmQa z@HbRfn0jr*-jQ{`bsD6_>lJsY$7sg-W%%P&4Gn)&^(&c4`0*a@P=j_%;MkyyMtHn% zv??H2Gt4c|PkhlBkFRU^+v?PyPj(dindpw*WK0-hhimvd8vd>)pKk7;h%wE^vW(`6 zpr$P}L*0Uc#1}0!{5=gnp}OYDn#H;kv>MZTY#T#HG^LPqs$ZC9h&xTJ_@b?bzpvpR zcyeg(PS9>ldz7R@eW`*n{FU6|J8DL_Q??ObbXJ0?RDSMKNnI5m%FQn$T5*%AL2gwI z>ZS?Z@k}yCd2e<^v+<;spJ9^trWYhn_lt`PRdw;Ijr&@n4wZd6MVv{|@DH`(TdI0! z+egDcLh?kQgNA>so3j^5EoLl;z1TBe&{PMC5s=E#ZSrN=VWm?S^SbL z48JCeE6L(ovbdfsZX}D}lEv@I;*VtUSF+5~ATUzHKemj~hzo*2h%p>!(GX>#I6nzdv?7lA$x##JRw$xdk;#ZSLc_moh&xpgHx2%S zfd~!%sv&HKI6qTdm<9iSKobrBx*=?iI6qfhm}fvhcOY29zi9}}66fz07v_6(j3E^# z0-YLSED+}xHmH~^x61LVxCjwRop8;vhUj{6ezCZ)1m05`JeNWR1BAeLJkU(T(fhnz zr$C%vCN3=3@Hzx#18p0^R*3T}#RYGh7Tw;a|5lT(a*H!iH-$XZ(;dBPwbG_*koN)& zf3P9@wMy;WD=9-p9Wux>WDHt?H^SsL6XfU#vTlMLGr?1TKMmCIQ(D>EEx1t&3(ilD z-WaH!q5M7@sGgww>IV8G;ufT_B8`pGxcaw)-P$0MI}qwb zXfcIq_-U=&(bFS&sX9#gJy9=iz1%KEZl5A|NKx`vCP&5wX~io{C3i`YyYdZMF%Ie` zq{!WQncS0?%f0wUIWY$_gcyeh1~vSQRu~ph7MM19rG|ftD(ehHdFqT;iF3SKoaZ&v zYVIZ`zlHUEwoBGDuiC7Qb(nI+U zZx`je5Ifm7_QP(OBjBIn>%aRBDfRuFKZ?lmbS)mklH}x}d=F0h`lj8dlz~JJ^!0w? zmcgClGq_zT+2J}veb?ao(T-1}9iO3=ff#f&WppUj`K(*Ij~8Y-rCv1rtdB3|2dW2t zNGsOjUANvrN~gxYTW{u_)f9OzdTM|wvYm#1=Ocn2QRWK%5`vloSQ*dx2ET%&uOf@1 z8h+l#f2J}_`0E;e0f}P0A#aFt1CZdjPgihzVOE@`#SR6n9UNX94ec`x`qmVG8xlME zNaXK&I*hL$)K)44al82XzYqUgyT*k2p78;dKfyQlM{X(g{T7;Gdb~|QL$CM*>3eAS z3%(*gbBn;*;AucA9lAJa$3I7cUJU_;bNnlBNP{n}q_<_+ptE=4Rw|8pEX4Y%OC96Z z@;0b@3c2_3$%h-x+$n&gE8ixeA@{S8Fu+&BIgbGNr~3L|xI;*T8sB^7&J@`Yjd7|= zkz@D{2p9#l@)dFkLViT?;6K{e|7VYXy&SKY6vUkB8}nDh{KzeVR@3&rVOPa@1HXnw zc^x5Hz9BanQqA-A{~i8H0=)U_8h+6y6r1qp1rrzWu2gRp?Gu27 z-yqaTNJDL?Pk``KmcwZp{)1L*@=UcN0I|G_EH%Cegj^)2+R67#-AGAI3$34qU-C#0 zA>s?s7=CMfqZ*nt$ndO@%4ihfZW%stVP3!#BJ-l9k1xjrCz?^=JQH*unxMH+!+-RN zB_dIA=#-+RR#f3-x4xo<+%H+~pKOo^B+IGEa+=%V5m8!EP346lttg8ZqZ$U0Xj4DO z%6Ow$nTfHdi0mA`v103E;TY8QP(NI9|o$w{)&D- z_(mmTOH6^(sY=w(KBeEef$9k5_v%1{kC7#&Bkl~uok4M77pVYjsWWViFlP?)M7t5rDVA{7_iGpMYP1 z>v)?w7-$Bx1lj;Sfsp{@`_BVV)BdaQnxTI&9OVGC_P-xMP53_zU}wu88v8?I{~rMe z4S>c05E{@C=ntd;69Fh3kOQm%)&U&Y1VHHk)NlY=EMPxy06^^qpmqaJ0A~!KtGI9u z1fqeq0BS;;4rBm#1FHb^1#KCy9e@xmnp^uK@BwfRxX4(b1VjLxfK*^S0Hp$R0VoxS zLI$Eq0(S#Q8~8F{_!#s(@E6`#R{{P2nj{Fy1oZ-j0i%G~0NOkVZ61VD20>sD1O`R@$HE93fy>MV7Dfl%2S_i)ioCZ*>!O%LS34k^af#8q?UdtMI<#(icE@1YLyh7)H;!08xS#| z`GCj)Q3F~Hh#t^-K+=GIiTuV0#Q$s5fYJPFdTM%FdjIr+qxrRQsfKZBk9>2ls=u`&|_+p zmW<8g*OzMY_>BUbs+MOI@~cIvqH#sz3;DIxs@3CG!*TuINrn8zeQfz;&iPd?rr>am z8*wo3-?^DiuXF2EZk^7pGq_c#xHF487r^@lFNIh&U%!l(BjtuLzF{ffkdKpVyfTbe zF6EVUa-G-Az{RT?cdobK6i?nGm4%+8_vH$ypxQ-g;b|RQ+P%2x8eR) zG=sZ2&;p3$=OE`oAIP}QFDApMB|i_R;m1Dw$3Zxa!lx{5@Lw1X*Z38PzdDBDF%rBg zE!yzkL^KmR{)dQRB9{Lp+M?BN2y~awCHhQMct@ZmDyHx-9#B48T-WV-O1j^K7c zdnP)dXy2e{H$*5Uif5uDH2;JNLno;FHB`NU$6lD|470NfRJ$gk=J6jh5$7vpe*@Vw ze3vIibi;W9e7_KJ@NLK7{8Ds)GaeDTi%$Hc=!~;2q6dE)Lb@%);kxL)1Ru1xE_#Zd z$h()oXb>3lB2lDp!+Fsgr+sjmB>Lf%@%}g)fU{(gCQ_K_Ee63H8w^iOJEE@`Ci*ea zUyKj~@VU28F-oK|L`)Y0#a)OqMhs$Nu*eWYpxAf#{@V>Pff{NkFpP=e==h(Bsqmc! zVI#y0G14GrLbF+76ceMx9Ffk%UHC%C7?BC>v+iMHEWVqPA?D&NXDt)s#6mG%fmtFO8Li?ntk#}P%og|Xvk+m}jQ(>?tmF3s z+wkq0>w?4N?#Z{~d?$lSCjPT9LuE{_c*5Ame-d__IT#W@j8a|{B{(gmmiS(*hqD|F z!JkHs^Y}%4HKjX$f{9GwLbfp?i;25M4gW=KM#L?QpW%iZ;(jLX$Hmq*F(2E z=K<<#dhr~b2N_1o4e>m~lMOe-iy{@RwpbjY{9j^Fc8Pce{;!fVPrL@_>u4?<-eBSl zH2a(Qc+Hz4A1{{P5brRt0*CkTof;h8$5-@30kBL|;q+q~Kv%`5i^ZoX)93h_idc?f zUKL;A^lRL)7DeKuSRv}*b&5YH45!3OlyDV_epQ^sXK~JoHNZW`%;M?_aTWKpuL>hzLX}+=e@+m8idb<|m{BcP zWl2~7D_|3b=iuR&gHCx>4iI+0AqT9%!6^sugE$uBxCzG+Iba1^s3xeR0B1@X1T3FY=L7d0`Et)UzIzF zzH(<#3v5GWUzNLw2Y~IU@vCx=6><-;1K5cNJ+8`0DDYYwQ}|P;`m1uEJh_i}5ZDDg z1UxMFS<&#ke*J^{w>a>jK=&hd^vy=)Sv$7_OZy|ZYJl-VzxG!^fsA#4oxxy1b)17^ zE5^FMj3XB6u4i%V2KWP#*v$f)wutG`Z|LZ$v*s;bz3x6dLR-qW^GEojc!20d{uXB8 zUoe~EI~xYfN4?P($DmJJ@XEOh-T7_tA-c-1a&x(j+({lR&yyG6`(35-9{EZ6fP6@P zQGQu|MSe^ENd8JbEB`G2D*q|}WehYnF-93-P9W&W*KRkVahTsFfBH%GOaU}n5s=X zO^=u!H9cWEWIAFxW;$W|%yiOp!Sti)3chI)WDYe)n%kN?n|qlDm{ZMT%u~#B%nQxS z%-rlSSDPO;A2J^`e_}pu{@(nnMP&)LG_`cFBwG4f23ba0CRyfM=3BBYc?QdB%e@wE z5f+Q3)KX!ov{YLjvFx?%x4djQZh6=8f#p-nSC;QA_`;u6W%aWLS{qx#tdZ7s)?U^` z>j3Lm>tt)DHQ&0-T4-HmU1v2}ORW{w`>hXI4_IHaeqjB|`h)eFE!5W1*2R`=OSet7 z&9;4DyJ)*?*V=>aVRl1Hd#t^ky{CPUeUyEneX)I+eU1H^{ZEJ55#fk)JmT2n*zY*# zc){_i<1@!M&TMCav&Q+J^AqPc&hMPR7WXXfTU=DUt@uQVrX-|fc*(kw=S$V4qf5t? z&M2K%I=@sdeY*6O($`BrEd9FlZ0V1s*Gm5?^Dhf8i!6&Si!Ezc*2z#dp=@bcRoSkx zx69sJFRc$+AG*HP`q=ej*UwvTUcZ0+=j+dvhnB~dcP^h=KCe8hyrBGn@{cyCHUw`N zuwnFu@f)UW5F4x;92=h7aCF1l8*Xe=ZEUnLVq@QpnHzI9F5I|r*M$$Q?eOYRRntB>%a`Xb zUy`4dy)?flcfp!5xrIeJOLLYN<`!n;7Y)xVC|a{1cRBnDbC%@HFH)Xr$frjeT++?3 z2-ny5g$Fdt&Aw}Bx1K$E^qjwz9;#SS?5w{?Jd0y6%140fz7^EFtl(RhR?j<=cOG$Xw@JU6>bC)j6 z&R+qar7QAsmlnAWm4ttPU~g&ocuAF-df>0Ja5=!$(=RN3Zpu|W%{GRy-7o_7z0TNy z=>V+U1IHQruqh@G`q6O#=8}cDTCp1~dm+0FgjTAPQ&&v{fd0S$AQeah1_FbC!N3q;C@>5d z4vYXs0;7P@Kss<2Fa{V4WB}uU@xTONA}|S<3`_y00@Hx$zzkp}FbkLs%mL;C^MFhs z3%DDY4`c&5zye?)un5Qn^uS_Z36KXY1@eIcU>UF+C5gJO>;E4gt>tF90tBhk+x&OTf#(E5NJ3QQ$S;b>JBA25=mB6L<@F8+ZqJ7kCdi z0lW`<0DK611bhs90(=U527C^D0elI31$+&B1DpivfK$L};0*9Ba2EIuI0u{uE&$&H z7l9vuOTdr7Pr%Q>W#AX!SKtb86}Sdm2W|kr0lx!(0Dl610XLl|T~Zl9q#{xiX^8xY z{D}gHv_yeKK}3y+f{8+i8WV*QH6aQk3MXnx6hYLCs5wy!qDZ2aL{UVoh*}f1A&Mr_ z5ycS26163YBWg#~o~Q#+JW)rYPDGuFx)60G>PD16)SajYQBR^?M2SR6M9D-cM7@dn z5cMVMN7SEa08uJY8qq+aK}3Uzh7b)U8b&mnXavznqESSniPDMgA{s+9mMDX09MO29 z2}Bc#CJ{|0nnE;{Xd2OUq8UUpiDnVaCYnPumuMbQCQ%mA-9+<=vWaqt77#5YT11pf zq$gTTw1g;+Xem)XQ3261qUA(|L`6g^h*lDJDkdr+DkUl-T2E9?w1H?N(I%n_qDmqcQ58`&Q4P^%qAf&Q ziS8$=CE7;x0MT}$9Yi~c9wgdD^bpa*M2`^dCVG_UF`_+0dx`cDJx=rl(UU|^5$z{> zn&=s#14PdfJx6qq=n&EKL@yA%NOYL!2+>PKFB82&^eWL&qSuICCpt#-2GMb%H;LXN zdYkASqIZejBRWC!KG6q69};~;^fA#VM4u9UM)Wz+7erqYeMR&&(KkdViRy?>5uGMF zL-Z}tS)%WV&Jmp_xJ_62%dnJJAlJokR~3?IL=J=wYHqh;|b_ zO7s}f9-_TO`-mPVdV=UlqNj-V6Fp7z4AB9iXNjI8I!JVg=y{?Sh+ZT*Omu|kC8C#! zUQr#!^rzDNX^>TNu|IuQGZG)6i^73v`~#T0Uw+dVi7Om6!<|j}Gjc0$6P(6TlST1c z(pP7rq*?G!lP?(~@xs7md=W((jW;Du8XNNs%s-GBTNyhVry+`;M1qVxjnh;rxG1eo z!oRzq51HcoyVIo9L}OaCaagSK347ysWPxwCG5;WDJZb#aQVtRRiiB(AR(`W7XX9^{ za(qdpi7ATt;fcDoQnaacI|{~YmH4=9l*Ku}dchJ5l(?jur67t8_ zQI4N99T6RaGF4r=DKBxtF+0H>ukNJj6VZ`AOPgdmr#79lltMV(t1$hHV=zjusA2le zQlic<-*4W@{P6jd?)b>-E_}{g>R^74-HVUQzEx*FDry?T=NI$aRM{-Ue9?RjAJW82 z+=)`0B{ZHh%)^K@OhP97%7}K;=dYBY#9f1!o6-lH43h|-Cu8(62?ay6;Z*116x znA*y^daTY`HUWvEtq)Bmy{vnpD8SZhN@rVox^0T2x6SrSP#cuCw%Ma?Yi1yFolVHU zu~vAoQcALws%@qAVn{`!+p2NICt1*=t0-IB19qqK^?Gud;w?!06vl3rn$vSKLmBW-g*We3p_<&1ov+AQNtAR!w zswx@ZaYa)rdlndzYzgiQ9aH`j2~?t=di2!Gz1=-|b@(`AsIfV+?4`U5ou8??2lGRF+W-|byBU<}4SJi>O~+NKiKaB2-N@5N}uIA`d<`&k;N=eL}JS8#rZ77KWt@iH~#XQbi6!R>-d1^yZ z%$dU#5&u*Y^OE|Km`mIxF;|aBGC!p@KV>ltbr;3F|8I-(vSkPrD#(Y;D{{ zIqfaVFAYWcw@YFP^p?cZ%%Hd6T{Ca1SfV=r%c5BDI+~|ZEMwh8vCJ5mWGPZximUw)u${QY-g2{7%V$IMX~JvZ;Eofz9{%8*9o7Je9`q^m*gjR zNid~GTf^K%v9=kNWbLQ6_OpKFE{e7P-xb9=+WIAmVx5ShSm&LxPPgwjORhWl^m7QjVultlxP`^2_KXTMMd(YA-`Wjv*~;ai^G+OR!)*X=TH74cSmZP68qY* zY_0**=s!&ycBi*C>^tgZ?9{nh%~uz%~W4g1gIk{p3*N1)>n zYQumP!9mtY{fgk7MjerkgXrdtSk#6i;glo3c)d~^j=rASaE$&hYQtgmmc+49?|#LeeJKW;bxCEDLW-6 zrC0XdwV|&cRYkT)PEPKf(xXTBC7zo;3E5f8i|iW?@7Z}&^`_*y+&et#uzeRS8GpC? zxyO#Ggd~;L_TPI{)m}PSn}y@>e@iM`tGop{O%W6^5BHObl-wN0?s%=VTWymOV`1=cox{HUsN?U>^zPVK$Pk4V83&+Q%VdcheU;<`CI zym4ip@UYq;h}vTEg50H9OJWMM@(PyZD7UX-Qr$1-b_@xM!7n$vUrao1VD-f>zN_2M z_eK^%Cqvtqp`M#t6vy=dZhl2jA@Gk06v+wm^YfID$2H-xemy%;q;3A;kwJxvatjJ_ zvgy2B72dpE&t8c!io8j96V@#_w+JuFPM#FggMK8WA~QV7py-M_VtE}4+%L}3T{W*V zo_A^EyT%kQUO}GYyW##DPM597T3Q5w{bG7`jajiYw`lzG-1$h>vqwUY9;6i&ImTT9 zeob_6WYnpDNDyT>EjN2%PEpL= zD{_})Q;FT@9o>zg+)V6L8@r85Eoz&zj#X)Oq#(4Q(kf~0nm?J&I(pQvo$zR~6cIwP z=y!L1_L_dRv2P10I$9AqET%j`7*Kg#}Bp))cy>#Ec#mle;u#QBD?af>NT|u*p{R#o~iURYA2u zf2~!855K*nKAo=3>~x_ek&zF<(8}{R=&E z^3c$KugIu>tMoB3cqnIxttzbNY;I^M#6uNJ#^kLeZD z**z`CbcyLnZkj={I3?2YpC^gg?UV>^JzYyi{~k1z<`jGvVk}GQ)Jv5zNfjC$(XL~M zw*4aJWNQ6Yomxh>7#pPZQ?(tO)<00yFD*GxHGo{HX=#C~foU0msu5}ZQt7;JUkdM- z92lUMd>3p(!-B2mJNYjDFiea&j72tXubB|k7+ZLKSWqC-&SEP5C?5RP;DZF3iQ1Jo z-RIAr2+*W!CefZGe(eTnS2KLyDUk0E;!peYXBuhl(o9x)KT*KCd%{Eb{>J?2Q2tC4 zNH?rj1&3*(@Tw1?s|K%}mO{w0P5E;X{9rSFh~Cf))!;R2AMZ$hs3p7!AsW05=M8Gb z54Prq+Q7R75Uj!bm);;9KN!Og;jPWBc>Mq$sZ#O7q5McFt9|XoqpD~F_M+%tXtEAK zN1!v12n+>abg_{@Ixq>C4`3sQc5YY!Pz0<5tbh|J0qzI31G@m!D0>cg0eA^`1^5v7 z3~0}fu<6nagGwdM)TpFc{%Zf}T0eh({|FWpJS!vuI~{YdSu~GjvMhEtvd(Ui(_%rh zg=|r)+}5+&=tCFlmc+2|ytYf?SoqTRGdtwBC}=;clQw)=+nHU1It7L=?_SsgD)w5D zSeV3GtYoWtuTEUjcTMO${bvryZ?U$;z5QnmY7%N#*LK#>Mq&32pE<(Ng7Z5fDTABG6^>^uxMkw3No&S%E917Q+&*pAbnXb{&L+Hg)~wmlGiS|h6Us}X zW@g2P^3u66S#h)5O>e84wWt;T*8~0QI9`tb=79g?u#|7gFD!t#isglc*#4?qL0{?T zRjYV4<27seX2!Ryopx{GI)rUC%))c+_cLD0<*D3Q$W8Fv#w}C0wUFCbUSIAg&X3=TZytwe?vu_#<QFg@fx0a{0xu0{vxa8U^!fll?Tfs zi}Jrmc8fG0b3YX^u5_G50p7nbXY$<`VM(^B3kb<_qS_ z=9`utmNd&y%V_1v#2m{KO938CEV0yBp0~VYIf@4mFIs-KT(SI)M-W?Bb=J<-Zq|v` znbxINmvy&wkM(8iTh@=PUs_LDzqekosqwJkDBE(|bGC177x1WIdwUOimVJ`mzR6x= zKWx8fzwT)3nB^#TR5)IAyyCdzxZ(73hB$jUQ=LPdi<~Q*Yn}HyA9U_xY>Ljp)l7yT5gI>Bfac)%F;lB89m+48Z59_Vy18 zbhU0C78IcLxx41Oo=6RAS=-~q0$l6-;Gd5SLk||<3s*6V+o=L;LvIwQ`qUm-RHlk{ zRaS(D*7krlQMC_%dqh(2@>r#;U= zw@|gL{bq=O0WdT_FE4-T2=p~PpGt>X!=z$W``T`M%TxiigPJeSmzkpFP<(QAgDaw0B8f(`~&_yOuQ2DAXAH-^Gowefbo orders = []; - Order? selectedOrder; Object? error; - /// Loads all orders. + /// All orders returned by the repository (unfiltered). + List _allOrders = []; + + /// The currently visible orders after applying [activeFilter] and [searchQuery]. + List orders = []; + + /// The currently selected order for detail view. + Order? selectedOrder; + + /// The active status filter label, or `null` for "all". + /// + /// Recognised values: `'pending'`, `'processing'`, `'shipped'`, + /// `'delivered'`, `'cancelled'`. + String? activeFilter; + + /// The current free-text search query applied to customer name / order ID. + String searchQuery = ''; + + /// Loads all orders and applies any current filter / search. Future load() async { isLoading = true; error = null; notifyListeners(); try { - orders = await _getOrders(); + _allOrders = await _getOrders(); + _applyFilters(); // Auto-select the first order if nothing is selected. selectedOrder ??= orders.isNotEmpty ? orders.first : null; } catch (e) { @@ -32,9 +51,79 @@ class OrdersController extends ChangeNotifier { } } + /// Sets the status filter and recomputes the visible list. + void setFilter(String? filter) { + activeFilter = filter; + _applyFilters(); + notifyListeners(); + } + + /// Sets the search query and recomputes the visible list. + void setSearchQuery(String query) { + searchQuery = query; + _applyFilters(); + notifyListeners(); + } + /// Selects an order for detail view. void selectOrder(Order order) { selectedOrder = order; notifyListeners(); } + + /// Attempts to select an order by ID. Returns `true` if found. + bool selectById(String id) { + final match = _allOrders.where((o) => o.id == id).firstOrNull; + if (match != null) { + selectedOrder = match; + notifyListeners(); + return true; + } + return false; + } + + // ── Private helpers ──────────────────────────────────────────────────── + + void _applyFilters() { + var result = _allOrders; + + // Status filter + final status = _parseStatus(activeFilter); + if (status != null) { + result = result.where((o) => o.status == status).toList(); + } + + // Free-text search on customer name and order ID + if (searchQuery.isNotEmpty) { + final q = searchQuery.toLowerCase(); + result = result.where((o) { + return o.customerName.toLowerCase().contains(q) || o.id.toLowerCase().contains(q); + }).toList(); + } + + orders = result; + + // Keep selection valid; clear if the selected order is no longer visible. + if (selectedOrder != null && !orders.any((o) => o.id == selectedOrder!.id)) { + selectedOrder = null; + } + } + + static OrderStatus? _parseStatus(String? filter) { + if (filter == null) return null; + switch (filter) { + case 'pending': + return OrderStatus.pending; + case 'processing': + return OrderStatus.processing; + case 'shipped': + return OrderStatus.shipped; + case 'delivered': + return OrderStatus.delivered; + case 'cancelled': + return OrderStatus.cancelled; + default: + return null; + } + } } 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 b19d735..98979db 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 @@ -20,7 +20,24 @@ class OrdersPage extends StatefulWidget { /// 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}); + /// Optional initial status filter to apply on first load (e.g. `'pending'`). + final String? initialFilter; + + /// Optional initial search query to apply on first load. + final String? initialQuery; + + /// Optional order ID to pre-select on first load (from a navigation handoff). + final String? initialSelectedId; + + const OrdersPage({ + super.key, + required this.repository, + this.onViewProduct, + this.onViewInventory, + this.initialFilter, + this.initialQuery, + this.initialSelectedId, + }); @override State createState() => _OrdersPageState(); @@ -33,7 +50,21 @@ class _OrdersPageState extends State { void initState() { super.initState(); controller = OrdersController(GetOrders(widget.repository)); - controller.load(); + + // Apply any initial filter / query before loading. + if (widget.initialFilter != null) { + controller.activeFilter = widget.initialFilter; + } + if (widget.initialQuery != null && widget.initialQuery!.isNotEmpty) { + controller.searchQuery = widget.initialQuery!; + } + + controller.load().then((_) { + // After data is loaded, try to pre-select by ID if requested. + if (widget.initialSelectedId != null) { + controller.selectById(widget.initialSelectedId!); + } + }); } @override diff --git a/kell_creations_apps/packages/feature_orders/test/orders_controller_test.dart b/kell_creations_apps/packages/feature_orders/test/orders_controller_test.dart index 8029a47..7aa7106 100644 --- a/kell_creations_apps/packages/feature_orders/test/orders_controller_test.dart +++ b/kell_creations_apps/packages/feature_orders/test/orders_controller_test.dart @@ -22,6 +22,8 @@ void main() { expect(controller.isLoading, false); expect(controller.orders, isEmpty); expect(controller.selectedOrder, isNull); + expect(controller.activeFilter, isNull); + expect(controller.searchQuery, ''); expect(controller.error, isNull); }); @@ -43,5 +45,75 @@ void main() { expect(controller.selectedOrder!.id, third.id); }); + + test('setFilter filters by order status', () async { + await controller.load(); + + controller.setFilter('pending'); + expect(controller.orders.length, 2); + expect(controller.orders.every((o) => o.status == OrderStatus.pending), true); + }); + + test('setFilter with null shows all orders', () async { + await controller.load(); + + controller.setFilter('cancelled'); + expect(controller.orders.length, 1); + + controller.setFilter(null); + expect(controller.orders.length, 6); + }); + + test('setSearchQuery filters by customer name', () async { + await controller.load(); + + controller.setSearchQuery('sarah'); + expect(controller.orders.length, 1); + expect(controller.orders.first.customerName, 'Sarah Mitchell'); + }); + + test('setSearchQuery filters by order ID', () async { + await controller.load(); + + controller.setSearchQuery('KC-1003'); + expect(controller.orders.length, 1); + expect(controller.orders.first.id, 'KC-1003'); + }); + + test('filter and search combine', () async { + await controller.load(); + + controller.setFilter('pending'); + controller.setSearchQuery('david'); + expect(controller.orders.length, 1); + expect(controller.orders.first.customerName, 'David Park'); + }); + + test('selectById selects matching order', () async { + await controller.load(); + + final found = controller.selectById('KC-1004'); + expect(found, true); + expect(controller.selectedOrder!.id, 'KC-1004'); + }); + + test('selectById returns false for unknown ID', () async { + await controller.load(); + + final found = controller.selectById('UNKNOWN'); + expect(found, false); + }); + + test('selection is cleared when filtered out', () async { + await controller.load(); + + // Select a delivered order. + controller.selectById('KC-1001'); + expect(controller.selectedOrder, isNotNull); + + // Filter to pending — the selected order should be cleared. + controller.setFilter('pending'); + expect(controller.selectedOrder, isNull); + }); }); } 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 index d6ecb3a..038d622 100644 --- 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 @@ -3,25 +3,43 @@ import 'package:flutter/foundation.dart'; import '../domain/policy_check_result.dart'; import 'get_policy_checks.dart'; -/// Controller that manages the policy-checks workspace state. +/// Controller that manages the policy-checks workspace state, including +/// filtering by category, free-text search, and check selection. class PolicyController extends ChangeNotifier { final GetPolicyChecks _getPolicyChecks; PolicyController(this._getPolicyChecks); bool isLoading = false; - List checks = []; - PolicyCheckResult? selectedCheck; Object? error; - /// Loads all policy checks. + /// All checks returned by the repository (unfiltered). + List _allChecks = []; + + /// The currently visible checks after applying [activeCategory] and [searchQuery]. + List checks = []; + + /// The currently selected check for detail view. + PolicyCheckResult? selectedCheck; + + /// The active category filter, or `null` for "all". + /// + /// Matches [PolicyCheckResult.category] values such as + /// `'Product Compliance'`, `'Inventory Governance'`, etc. + String? activeCategory; + + /// The current free-text search query applied to title / summary. + String searchQuery = ''; + + /// Loads all policy checks and applies any current filter / search. Future load() async { isLoading = true; error = null; notifyListeners(); try { - checks = await _getPolicyChecks(); + _allChecks = await _getPolicyChecks(); + _applyFilters(); // Auto-select the first check if nothing is selected. selectedCheck ??= checks.isNotEmpty ? checks.first : null; } catch (e) { @@ -32,9 +50,60 @@ class PolicyController extends ChangeNotifier { } } + /// Sets the category filter and recomputes the visible list. + void setCategory(String? category) { + activeCategory = category; + _applyFilters(); + notifyListeners(); + } + + /// Sets the search query and recomputes the visible list. + void setSearchQuery(String query) { + searchQuery = query; + _applyFilters(); + notifyListeners(); + } + /// Selects a policy check for detail view. void selectCheck(PolicyCheckResult check) { selectedCheck = check; notifyListeners(); } + + /// Attempts to select a check by ID. Returns `true` if found. + bool selectById(String id) { + final match = _allChecks.where((c) => c.id == id).firstOrNull; + if (match != null) { + selectedCheck = match; + notifyListeners(); + return true; + } + return false; + } + + // ── Private helpers ──────────────────────────────────────────────────── + + void _applyFilters() { + var result = _allChecks; + + // Category filter + if (activeCategory != null && activeCategory!.isNotEmpty) { + result = result.where((c) => c.category == activeCategory).toList(); + } + + // Free-text search on title and summary + if (searchQuery.isNotEmpty) { + final q = searchQuery.toLowerCase(); + result = result.where((c) { + return c.title.toLowerCase().contains(q) || c.summary.toLowerCase().contains(q); + }).toList(); + } + + checks = result; + + // Keep selection valid; clear if the selected check is no longer visible. + if (selectedCheck != null && !checks.any((c) => c.id == selectedCheck!.id)) { + selectedCheck = null; + } + } } 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 a957573..af01266 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 @@ -18,7 +18,24 @@ class PolicyPage extends StatefulWidget { /// 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}); + /// Optional initial category filter to apply on first load + /// (e.g. `'Product Compliance'`). + final String? initialCategory; + + /// Optional initial search query to apply on first load. + final String? initialQuery; + + /// Optional check ID to pre-select on first load (from a navigation handoff). + final String? initialSelectedId; + + const PolicyPage({ + super.key, + required this.repository, + this.onViewRelatedPage, + this.initialCategory, + this.initialQuery, + this.initialSelectedId, + }); @override State createState() => _PolicyPageState(); @@ -31,7 +48,21 @@ class _PolicyPageState extends State { void initState() { super.initState(); controller = PolicyController(GetPolicyChecks(widget.repository)); - controller.load(); + + // Apply any initial category / query before loading. + if (widget.initialCategory != null) { + controller.activeCategory = widget.initialCategory; + } + if (widget.initialQuery != null && widget.initialQuery!.isNotEmpty) { + controller.searchQuery = widget.initialQuery!; + } + + controller.load().then((_) { + // After data is loaded, try to pre-select by ID if requested. + if (widget.initialSelectedId != null) { + controller.selectById(widget.initialSelectedId!); + } + }); } @override 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 b47f6f2..4952dfa 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 @@ -80,6 +80,8 @@ void main() { expect(controller.isLoading, false); expect(controller.checks, isEmpty); expect(controller.selectedCheck, isNull); + expect(controller.activeCategory, isNull); + expect(controller.searchQuery, ''); expect(controller.error, isNull); }); @@ -101,5 +103,75 @@ void main() { expect(controller.selectedCheck!.id, third.id); }); + + test('setCategory filters by category', () async { + await controller.load(); + + controller.setCategory('Product Compliance'); + expect(controller.checks.length, 2); + expect(controller.checks.every((c) => c.category == 'Product Compliance'), true); + }); + + test('setCategory with null shows all checks', () async { + await controller.load(); + + controller.setCategory('Order Operations'); + expect(controller.checks.length, 2); + + controller.setCategory(null); + expect(controller.checks.length, 8); + }); + + test('setSearchQuery filters by title', () async { + await controller.load(); + + controller.setSearchQuery('pricing'); + expect(controller.checks.length, 1); + expect(controller.checks.first.title, 'Pricing Consistency'); + }); + + test('setSearchQuery filters by summary', () async { + await controller.load(); + + controller.setSearchQuery('reorder threshold'); + expect(controller.checks.length, 1); + expect(controller.checks.first.id, 'POL-006'); + }); + + test('category and search combine', () async { + await controller.load(); + + controller.setCategory('Inventory Governance'); + controller.setSearchQuery('accuracy'); + expect(controller.checks.length, 1); + expect(controller.checks.first.id, 'POL-002'); + }); + + test('selectById selects matching check', () async { + await controller.load(); + + final found = controller.selectById('POL-005'); + expect(found, true); + expect(controller.selectedCheck!.id, 'POL-005'); + }); + + test('selectById returns false for unknown ID', () async { + await controller.load(); + + final found = controller.selectById('UNKNOWN'); + expect(found, false); + }); + + test('selection is cleared when filtered out', () async { + await controller.load(); + + // Select a Product Compliance check. + controller.selectById('POL-001'); + expect(controller.selectedCheck, isNotNull); + + // Filter to Order Operations — the selected check should be cleared. + controller.setCategory('Order Operations'); + expect(controller.selectedCheck, isNull); + }); }); } diff --git a/kell_creations_apps/packages/feature_wordpress/lib/src/application/product_publishing_controller.dart b/kell_creations_apps/packages/feature_wordpress/lib/src/application/product_publishing_controller.dart index 6c3546d..69ff0a5 100644 --- a/kell_creations_apps/packages/feature_wordpress/lib/src/application/product_publishing_controller.dart +++ b/kell_creations_apps/packages/feature_wordpress/lib/src/application/product_publishing_controller.dart @@ -1,10 +1,12 @@ import 'package:flutter/foundation.dart'; import '../domain/product_draft.dart'; +import '../domain/publish_status.dart'; import 'get_product_drafts.dart'; import 'publish_product.dart'; -/// Controller that manages the product publishing workspace state. +/// Controller that manages the product publishing workspace state, including +/// filtering by publish status, free-text search, and draft selection. class ProductPublishingController extends ChangeNotifier { final GetProductDrafts _getProductDrafts; final PublishProduct _publishProduct; @@ -12,18 +14,34 @@ class ProductPublishingController extends ChangeNotifier { ProductPublishingController(this._getProductDrafts, this._publishProduct); bool isLoading = false; - List drafts = []; - ProductDraft? selectedDraft; Object? error; - /// Loads all product drafts. + /// All drafts returned by the repository (unfiltered). + List _allDrafts = []; + + /// The currently visible drafts after applying [activeFilter] and [searchQuery]. + List drafts = []; + + /// The currently selected draft for preview. + ProductDraft? selectedDraft; + + /// The active status filter label, or `null` for "all". + /// + /// Recognised values: `'draft'`, `'pendingReview'`, `'published'`, `'unpublished'`. + String? activeFilter; + + /// The current free-text search query applied to name / SKU. + String searchQuery = ''; + + /// Loads all product drafts and applies any current filter / search. Future load() async { isLoading = true; error = null; notifyListeners(); try { - drafts = await _getProductDrafts(); + _allDrafts = await _getProductDrafts(); + _applyFilters(); // Auto-select the first draft if nothing is selected. selectedDraft ??= drafts.isNotEmpty ? drafts.first : null; } catch (e) { @@ -34,12 +52,37 @@ class ProductPublishingController extends ChangeNotifier { } } + /// Sets the status filter and recomputes the visible list. + void setFilter(String? filter) { + activeFilter = filter; + _applyFilters(); + notifyListeners(); + } + + /// Sets the search query and recomputes the visible list. + void setSearchQuery(String query) { + searchQuery = query; + _applyFilters(); + notifyListeners(); + } + /// Selects a draft for preview. void selectDraft(ProductDraft draft) { selectedDraft = draft; notifyListeners(); } + /// Attempts to select a draft by SKU. Returns `true` if found. + bool selectBySku(String sku) { + final match = _allDrafts.where((d) => d.sku == sku).firstOrNull; + if (match != null) { + selectedDraft = match; + notifyListeners(); + return true; + } + return false; + } + /// Publishes the draft with the given [id] and reloads the list. Future publish(String id) async { try { @@ -50,4 +93,47 @@ class ProductPublishingController extends ChangeNotifier { notifyListeners(); } } + + // ── Private helpers ──────────────────────────────────────────────────── + + void _applyFilters() { + var result = _allDrafts; + + // Status filter + final status = _parseStatus(activeFilter); + if (status != null) { + result = result.where((d) => d.status == status).toList(); + } + + // Free-text search on name and SKU + if (searchQuery.isNotEmpty) { + final q = searchQuery.toLowerCase(); + result = result.where((d) { + return d.name.toLowerCase().contains(q) || d.sku.toLowerCase().contains(q); + }).toList(); + } + + drafts = result; + + // Keep selection valid; clear if the selected draft is no longer visible. + if (selectedDraft != null && !drafts.any((d) => d.id == selectedDraft!.id)) { + selectedDraft = null; + } + } + + static PublishStatus? _parseStatus(String? filter) { + if (filter == null) return null; + switch (filter) { + case 'draft': + return PublishStatus.draft; + case 'pendingReview': + return PublishStatus.pendingReview; + case 'published': + return PublishStatus.published; + case 'unpublished': + return PublishStatus.unpublished; + default: + return null; + } + } } 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 f11e4fb..f2694f6 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 @@ -19,7 +19,23 @@ class ProductPublishingPage extends StatefulWidget { /// Provided by the app layer to enable cross-feature handoffs. final VoidCallback? onViewPolicy; - const ProductPublishingPage({super.key, required this.repository, this.onViewPolicy}); + /// Optional initial status filter to apply on first load (e.g. `'draft'`). + final String? initialFilter; + + /// Optional initial search query to apply on first load. + final String? initialQuery; + + /// Optional SKU to pre-select on first load (from a navigation handoff). + final String? initialSelectedSku; + + const ProductPublishingPage({ + super.key, + required this.repository, + this.onViewPolicy, + this.initialFilter, + this.initialQuery, + this.initialSelectedSku, + }); @override State createState() => _ProductPublishingPageState(); @@ -33,7 +49,21 @@ class _ProductPublishingPageState extends State { super.initState(); final repo = widget.repository; controller = ProductPublishingController(GetProductDrafts(repo), PublishProduct(repo)); - controller.load(); + + // Apply any initial filter / query before loading. + if (widget.initialFilter != null) { + controller.activeFilter = widget.initialFilter; + } + if (widget.initialQuery != null && widget.initialQuery!.isNotEmpty) { + controller.searchQuery = widget.initialQuery!; + } + + controller.load().then((_) { + // After data is loaded, try to pre-select by SKU if requested. + if (widget.initialSelectedSku != null) { + controller.selectBySku(widget.initialSelectedSku!); + } + }); } @override diff --git a/kell_creations_apps/packages/feature_wordpress/test/product_publishing_controller_test.dart b/kell_creations_apps/packages/feature_wordpress/test/product_publishing_controller_test.dart index 8fa2620..f1708bf 100644 --- a/kell_creations_apps/packages/feature_wordpress/test/product_publishing_controller_test.dart +++ b/kell_creations_apps/packages/feature_wordpress/test/product_publishing_controller_test.dart @@ -26,6 +26,8 @@ void main() { expect(controller.isLoading, false); expect(controller.drafts, isEmpty); expect(controller.selectedDraft, isNull); + expect(controller.activeFilter, isNull); + expect(controller.searchQuery, ''); expect(controller.error, isNull); }); @@ -57,5 +59,75 @@ void main() { final updated = controller.drafts.firstWhere((d) => d.id == '4'); expect(updated.status, PublishStatus.published); }); + + test('setFilter filters by publish status', () async { + await controller.load(); + + controller.setFilter('draft'); + expect(controller.drafts.length, 2); + expect(controller.drafts.every((d) => d.status == PublishStatus.draft), true); + }); + + test('setFilter with null shows all drafts', () async { + await controller.load(); + + controller.setFilter('published'); + expect(controller.drafts.length, 2); + + controller.setFilter(null); + expect(controller.drafts.length, 6); + }); + + test('setSearchQuery filters by name', () async { + await controller.load(); + + controller.setSearchQuery('nightlight'); + expect(controller.drafts.length, 1); + expect(controller.drafts.first.name, 'Ocean Nightlight'); + }); + + test('setSearchQuery filters by SKU', () async { + await controller.load(); + + controller.setSearchQuery('JG-BLU'); + expect(controller.drafts.length, 1); + expect(controller.drafts.first.sku, 'JG-BLU-004'); + }); + + test('filter and search combine', () async { + await controller.load(); + + controller.setFilter('published'); + controller.setSearchQuery('floral'); + expect(controller.drafts.length, 1); + expect(controller.drafts.first.name, 'Floral Bowl Cozy'); + }); + + test('selectBySku selects matching draft', () async { + await controller.load(); + + final found = controller.selectBySku('CS-CIT-002'); + expect(found, true); + expect(controller.selectedDraft!.sku, 'CS-CIT-002'); + }); + + test('selectBySku returns false for unknown SKU', () async { + await controller.load(); + + final found = controller.selectBySku('UNKNOWN'); + expect(found, false); + }); + + test('selection is cleared when filtered out', () async { + await controller.load(); + + // Select a published draft. + controller.selectBySku('BC-FLR-001'); + expect(controller.selectedDraft, isNotNull); + + // Filter to draft status — the selected item should be cleared. + controller.setFilter('draft'); + expect(controller.selectedDraft, isNull); + }); }); }