From a4db2bb2cf137656cae03e0ad7ac83ddb1ad6478 Mon Sep 17 00:00:00 2001 From: Khesir Date: Wed, 14 Jan 2026 14:22:28 +0800 Subject: [PATCH 01/13] feat: added buckets with migration changes: - added the whole working module - added controller, half implemented --- lib/core/migrations/migration_manager.dart | 4 + .../migrations/043_create_buckets_table.dart | 109 ++++++ ...044_add_bucket_id_to_task_and_project.dart | 68 ++++ .../presentation/state/auth_controller.dart | 41 +- .../bucket_initialization_service.dart | 85 +++++ .../data/datasources/bucket_datasource.dart | 24 ++ .../supabase/bucket_datasource_supabase.dart | 94 +++++ .../buckets/data/models/bucket_model.dart | 60 +++ .../repositories/bucket_repository_impl.dart | 75 ++++ .../buckets/domain/entities/bucket.dart | 51 +++ .../repositories/bucket_repository.dart | 25 ++ .../data/datasources/project_datasource.dart | 3 + .../supabase/project_datasource_supabase.dart | 14 + .../projects/data/models/project_model.dart | 4 + .../repositories/project_repository_impl.dart | 7 + .../projects/domain/entities/project.dart | 7 +- .../repositories/project_repository.dart | 2 + .../task_datasource_supabase.dart | 20 +- .../data/datasources/task_datasource.dart | 2 + .../modules/tasks/data/models/task_model.dart | 22 +- .../repositories/task_repository_impl.dart | 15 +- .../modules/tasks/domain/entities/task.dart | 4 + .../domain/repositories/task_repository.dart | 3 + .../tabs/task/components/task_form_page.dart | 360 ++++++++++-------- .../components/task_management_dialog.dart | 7 +- .../tabs/task/create_task_route_page.dart | 63 +-- .../presentation/state/bucket_controller.dart | 67 ++++ .../state/project_controller.dart | 9 + .../presentation/state/task_controller.dart | 8 + lib/features/tasks/tasks.dart | 2 +- lib/features/tasks/tasks_di.dart | 27 +- 31 files changed, 1072 insertions(+), 210 deletions(-) create mode 100644 lib/core/migrations/migrations/043_create_buckets_table.dart create mode 100644 lib/core/migrations/migrations/044_add_bucket_id_to_task_and_project.dart create mode 100644 lib/features/tasks/data/sevices/bucket_initialization_service.dart create mode 100644 lib/features/tasks/modules/buckets/data/datasources/bucket_datasource.dart create mode 100644 lib/features/tasks/modules/buckets/data/datasources/supabase/bucket_datasource_supabase.dart create mode 100644 lib/features/tasks/modules/buckets/data/models/bucket_model.dart create mode 100644 lib/features/tasks/modules/buckets/data/repositories/bucket_repository_impl.dart create mode 100644 lib/features/tasks/modules/buckets/domain/entities/bucket.dart create mode 100644 lib/features/tasks/modules/buckets/domain/repositories/bucket_repository.dart rename lib/features/tasks/modules/tasks/data/datasources/{mongodb => supabase}/task_datasource_supabase.dart (91%) create mode 100644 lib/features/tasks/presentation/state/bucket_controller.dart diff --git a/lib/core/migrations/migration_manager.dart b/lib/core/migrations/migration_manager.dart index 1da742d..dd8b9e1 100644 --- a/lib/core/migrations/migration_manager.dart +++ b/lib/core/migrations/migration_manager.dart @@ -43,6 +43,8 @@ import 'migrations/039_create_pomodoro_sessions_table.dart'; import 'migrations/040_add_pomodoro_session_pause_and_project.dart'; import 'migrations/041_cleanup_stale_pomodoro_sessions.dart'; import 'migrations/042_add_pomodoro_session_title.dart'; +import 'migrations/043_create_buckets_table.dart'; +import 'migrations/044_add_bucket_id_to_task_and_project.dart'; /// Manages database migrations /// @@ -297,6 +299,8 @@ class MigrationManager { Migration040AddPomodoroSessionPauseAndProject(), Migration041CleanupStalePomodoroSessions(), Migration042AddPomodoroSessionTitle(), + Migration043CreateBucketsTable(), + Migration044AddBucketIdToTaskAndProject(), // Add new migrations here: ]; diff --git a/lib/core/migrations/migrations/043_create_buckets_table.dart b/lib/core/migrations/migrations/043_create_buckets_table.dart new file mode 100644 index 0000000..239f0d2 --- /dev/null +++ b/lib/core/migrations/migrations/043_create_buckets_table.dart @@ -0,0 +1,109 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:keep_track/core/logging/app_logger.dart'; +import '../migration.dart'; + +/// Creates buckets table for task categorization +class Migration043CreateBucketsTable extends Migration { + @override + String get version => '043_create_buckets_table'; + + @override + String get description => 'Create buckets table for task categorization'; + + @override + Future up(SupabaseClient client) async { + AppLogger.info(' 🪣 Creating buckets table...'); + + final sql = ''' + -- Create buckets table + CREATE TABLE IF NOT EXISTS buckets ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + name TEXT NOT NULL, + is_archive BOOLEAN DEFAULT FALSE NOT NULL, + user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + -- Constraints + CONSTRAINT buckets_name_user_id_unique UNIQUE(name, user_id), + CONSTRAINT buckets_name_not_empty CHECK (length(trim(name)) > 0), + CONSTRAINT buckets_name_length CHECK (length(name) <= 100) + ); + + -- Add indexes for performance + CREATE INDEX IF NOT EXISTS idx_buckets_user_id ON buckets(user_id); + CREATE INDEX IF NOT EXISTS idx_buckets_is_archive ON buckets(is_archive); + CREATE INDEX IF NOT EXISTS idx_buckets_user_id_archive ON buckets(user_id, is_archive); + + -- Add RLS policies + ALTER TABLE buckets ENABLE ROW LEVEL SECURITY; + + -- Users can view their own buckets + CREATE POLICY "Users can view own buckets" ON buckets + FOR SELECT USING (auth.uid() = user_id); + + -- Users can insert their own buckets + CREATE POLICY "Users can insert own buckets" ON buckets + FOR INSERT WITH CHECK (auth.uid() = user_id); + + -- Users can update their own buckets + CREATE POLICY "Users can update own buckets" ON buckets + FOR UPDATE USING (auth.uid() = user_id); + + -- Users can delete their own buckets + CREATE POLICY "Users can delete own buckets" ON buckets + FOR DELETE USING (auth.uid() = user_id); + + -- Trigger to update updated_at timestamp + CREATE OR REPLACE FUNCTION update_buckets_updated_at() + RETURNS TRIGGER AS \$\$ + BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; + END; + \$\$ LANGUAGE plpgsql; + + CREATE TRIGGER buckets_updated_at + BEFORE UPDATE ON buckets + FOR EACH ROW + EXECUTE FUNCTION update_buckets_updated_at(); + '''; + + try { + await client.rpc('exec_sql', params: {'sql': sql}); + AppLogger.info(' ✅ Successfully created buckets table'); + } catch (e, stackTrace) { + AppLogger.error(' ❌ Failed to create buckets table', e, stackTrace); + rethrow; + } + } + + @override + Future down(SupabaseClient client) async { + AppLogger.info(' 🗑️ Dropping buckets table...'); + + final sql = ''' + -- Drop trigger + DROP TRIGGER IF EXISTS buckets_updated_at ON buckets; + + -- Drop function + DROP FUNCTION IF EXISTS update_buckets_updated_at(); + + -- Drop indexes + DROP INDEX IF EXISTS idx_buckets_user_id; + DROP INDEX IF EXISTS idx_buckets_is_archive; + DROP INDEX IF EXISTS idx_buckets_user_id_archive; + + -- Drop table + DROP TABLE IF EXISTS buckets; + '''; + + try { + await client.rpc('exec_sql', params: {'sql': sql}); + AppLogger.info(' ✅ Successfully dropped buckets table'); + } catch (e, stackTrace) { + AppLogger.error(' ❌ Failed to drop buckets table', e, stackTrace); + rethrow; + } + } +} \ No newline at end of file diff --git a/lib/core/migrations/migrations/044_add_bucket_id_to_task_and_project.dart b/lib/core/migrations/migrations/044_add_bucket_id_to_task_and_project.dart new file mode 100644 index 0000000..bf86fad --- /dev/null +++ b/lib/core/migrations/migrations/044_add_bucket_id_to_task_and_project.dart @@ -0,0 +1,68 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:keep_track/core/logging/app_logger.dart'; +import '../migration.dart'; + +/// Adds bucket_id foreign key to tasks and projects tables +class Migration044AddBucketIdToTaskAndProject extends Migration { + @override + String get version => '044_add_bucket_id_to_task_and_project'; + + @override + String get description => 'Add bucket_id foreign key to tasks and projects tables'; + + @override + Future up(SupabaseClient client) async { + AppLogger.info(' 🔗 Adding bucket_id to tasks and projects tables...'); + + final sql = ''' + -- Add bucket_id column to tasks table + ALTER TABLE tasks + ADD COLUMN IF NOT EXISTS bucket_id UUID REFERENCES buckets(id) ON DELETE SET NULL; + + -- Add bucket_id column to projects table + ALTER TABLE projects + ADD COLUMN IF NOT EXISTS bucket_id UUID REFERENCES buckets(id) ON DELETE SET NULL; + + -- Add indexes for bucket_id columns for better query performance + CREATE INDEX IF NOT EXISTS idx_tasks_bucket_id ON tasks(bucket_id); + CREATE INDEX IF NOT EXISTS idx_projects_bucket_id ON projects(bucket_id); + + -- Add composite indexes for common queries + CREATE INDEX IF NOT EXISTS idx_tasks_bucket_id_status ON tasks(bucket_id, status); + CREATE INDEX IF NOT EXISTS idx_projects_bucket_id_archived ON projects(bucket_id, is_archived); + '''; + + try { + await client.rpc('exec_sql', params: {'sql': sql}); + AppLogger.info(' ✅ Successfully added bucket_id to tasks and projects tables'); + } catch (e, stackTrace) { + AppLogger.error(' ❌ Failed to add bucket_id to tables', e, stackTrace); + rethrow; + } + } + + @override + Future down(SupabaseClient client) async { + AppLogger.info(' 🗑️ Removing bucket_id from tasks and projects tables...'); + + final sql = ''' + -- Drop indexes + DROP INDEX IF EXISTS idx_tasks_bucket_id; + DROP INDEX IF EXISTS idx_projects_bucket_id; + DROP INDEX IF EXISTS idx_tasks_bucket_id_status; + DROP INDEX IF EXISTS idx_projects_bucket_id_archived; + + -- Remove bucket_id columns + ALTER TABLE tasks DROP COLUMN IF EXISTS bucket_id; + ALTER TABLE projects DROP COLUMN IF EXISTS bucket_id; + '''; + + try { + await client.rpc('exec_sql', params: {'sql': sql}); + AppLogger.info(' ✅ Successfully removed bucket_id from tasks and projects tables'); + } catch (e, stackTrace) { + AppLogger.error(' ❌ Failed to remove bucket_id from tables', e, stackTrace); + rethrow; + } + } +} \ No newline at end of file diff --git a/lib/features/auth/presentation/state/auth_controller.dart b/lib/features/auth/presentation/state/auth_controller.dart index 55619b2..6097ec3 100644 --- a/lib/features/auth/presentation/state/auth_controller.dart +++ b/lib/features/auth/presentation/state/auth_controller.dart @@ -5,6 +5,7 @@ import 'package:keep_track/core/di/service_locator.dart'; import 'package:keep_track/features/auth/data/services/auth_service.dart'; import 'package:keep_track/features/auth/domain/entities/user.dart'; import 'package:keep_track/features/finance/data/services/finance_initialization_service.dart'; +import 'package:keep_track/features/tasks/data/sevices/bucket_initialization_service.dart'; class AuthController extends StreamState> { final AuthService _authService; @@ -182,14 +183,46 @@ class AuthController extends StreamState> { // Initialize default finance categories final financeService = locator.get(); - final result = await financeService.initializeDefaultCategories(userId); - result.fold( + final financeResult = await financeService.initializeDefaultCategories( + userId, + ); + + financeResult.fold( + onSuccess: (wasInitialized) { + if (wasInitialized) { + AppLogger.info( + '✅ Finance data initialization completed successfully', + ); + } else { + AppLogger.info( + 'Finance data already exists, skipping initialization', + ); + } + }, + onError: (failure) { + // Log error but don't block user from using the app + AppLogger.warning( + 'Failed to initialize user data (non-blocking): ${failure.message}', + ); + }, + ); + final bucketService = locator.get(); + + final bucketResult = await bucketService.initializeDefaultCategories( + userId, + ); + + bucketResult.fold( onSuccess: (wasInitialized) { if (wasInitialized) { - AppLogger.info('✅ User data initialization completed successfully'); + AppLogger.info( + '✅ Bucket data initialization completed successfully', + ); } else { - AppLogger.info('User data already exists, skipping initialization'); + AppLogger.info( + 'Bucket data already exists, skipping initialization', + ); } }, onError: (failure) { diff --git a/lib/features/tasks/data/sevices/bucket_initialization_service.dart b/lib/features/tasks/data/sevices/bucket_initialization_service.dart new file mode 100644 index 0000000..6badb01 --- /dev/null +++ b/lib/features/tasks/data/sevices/bucket_initialization_service.dart @@ -0,0 +1,85 @@ +import 'package:keep_track/core/error/result.dart'; +import 'package:keep_track/core/error/failure.dart'; +import 'package:keep_track/core/logging/app_logger.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/entities/bucket.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/repositories/bucket_repository.dart'; + +/// Service to initialize default bucket data for new users +class BucketInitializationService { + final BucketRepository _bucketRepository; + + BucketInitializationService(this._bucketRepository); + + /// Initialize default buckets for a new user + /// Returns true if initialization was successful, false if user already has buckets + Future> initializeDefaultCategories(String userId) async { + try { + AppLogger.info('Initializing default buckets for user: $userId'); + + // Check if user already has buckets + final existingResult = await _bucketRepository.getBuckets(); + if (existingResult.isSuccess) { + final existing = existingResult.dataOrNull ?? []; + if (existing.isNotEmpty) { + AppLogger.info( + 'User already has ${existing.length} buckets, skipping initialization', + ); + return Result.success(false); + } + } + + // Define default buckets + final defaultBuckets = _getDefaultCategories(userId); + + // Create all buckets + int successCount = 0; + for (final bucket in defaultBuckets) { + final result = await _bucketRepository.createBucket(bucket); + if (result.isSuccess) { + successCount++; + AppLogger.info('Created bucket: ${bucket.name}'); + } else { + AppLogger.warning( + 'Failed to create bucket: ${bucket.name}', + result.failureOrNull, + ); + } + } + + AppLogger.info( + 'Default buckets initialized: $successCount/${defaultBuckets.length} created', + ); + return Result.success(true); + } catch (e, stackTrace) { + AppLogger.error('Failed to initialize default buckets', e, stackTrace); + return Result.error( + UnknownFailure( + message: 'Failed to initialize default buckets', + stackTrace: stackTrace, + originalError: e, + ), + ); + } + } + + /// Get list of default buckets + List _getDefaultCategories(String userId) { + return [ + Bucket(name: 'Work', userId: userId), + Bucket(name: 'Personal', userId: userId), + Bucket(name: 'Shopping', userId: userId), + Bucket(name: 'Home', userId: userId), + Bucket(name: 'Health & Fitness', userId: userId), + Bucket(name: 'Finance', userId: userId), + Bucket(name: 'Learning', userId: userId), + Bucket(name: 'Projects', userId: userId), + Bucket(name: 'Family', userId: userId), + Bucket(name: 'Social', userId: userId), + Bucket(name: 'Travel', userId: userId), + Bucket(name: 'Goals', userId: userId), + Bucket(name: 'Ideas', userId: userId), + Bucket(name: 'Errands', userId: userId), + Bucket(name: 'Other', userId: userId), + ]; + } +} diff --git a/lib/features/tasks/modules/buckets/data/datasources/bucket_datasource.dart b/lib/features/tasks/modules/buckets/data/datasources/bucket_datasource.dart new file mode 100644 index 0000000..051deb2 --- /dev/null +++ b/lib/features/tasks/modules/buckets/data/datasources/bucket_datasource.dart @@ -0,0 +1,24 @@ +import 'package:keep_track/features/tasks/modules/buckets/data/models/bucket_model.dart'; + +import '../../domain/entities/bucket.dart'; + +/// Data source interface for bucket operations +abstract class BucketDataSource { + /// Get all buckets for a user + Future> getBuckets(); + + /// Get a specific bucket by ID + Future getBucketById(String id); + + /// Get multiple buckets by IDs + Future> getByIds(List ids); + + /// Create a new bucket + Future createBucket(BucketModel bucket); + + /// Update an existing bucket + Future updateBucket(BucketModel bucket); + + /// Delete a bucket + Future deleteBucket(String id); +} diff --git a/lib/features/tasks/modules/buckets/data/datasources/supabase/bucket_datasource_supabase.dart b/lib/features/tasks/modules/buckets/data/datasources/supabase/bucket_datasource_supabase.dart new file mode 100644 index 0000000..71fe9bc --- /dev/null +++ b/lib/features/tasks/modules/buckets/data/datasources/supabase/bucket_datasource_supabase.dart @@ -0,0 +1,94 @@ +import 'package:keep_track/shared/infrastructure/supabase/supabase_service.dart'; + +import '../../../domain/entities/bucket.dart'; +import '../bucket_datasource.dart'; +import '../../models/bucket_model.dart'; + +/// Supabase implementation of bucket data source +class BucketDataSourceSupabase implements BucketDataSource { + final SupabaseService supabaseService; + static const String tableName = 'buckets'; + + BucketDataSourceSupabase(this.supabaseService); + + @override + Future> getBuckets() async { + final response = await supabaseService.client + .from(tableName) + .select() + .eq('user_id', supabaseService.userId!) + .eq('is_archive', false) + .order('created_at', ascending: false); + + return (response as List) + .map((doc) => BucketModel.fromJson(doc as Map)) + .toList(); + } + + @override + Future getBucketById(String id) async { + final response = await supabaseService.client + .from(tableName) + .select() + .eq('id', id) + .eq('user_id', supabaseService.userId!) + .maybeSingle(); + + return response != null + ? BucketModel.fromJson(response as Map) + : null; + } + + @override + Future> getByIds(List ids) async { + final response = await supabaseService.client + .from(tableName) + .select() + .filter('id', 'in', ids) + .eq('user_id', supabaseService.userId!) + .order('created_at', ascending: false); + + return (response as List) + .map((doc) => BucketModel.fromJson(doc as Map)) + .toList(); + } + + @override + Future createBucket(BucketModel bucket) async { + final doc = bucket.toJson(); + final response = await supabaseService.client + .from(tableName) + .insert(doc) + .select() + .single(); + + return BucketModel.fromJson(response as Map); + } + + @override + Future updateBucket(BucketModel bucket) async { + if (bucket.id == null) { + throw Exception('Cannot update bucket without an ID'); + } + + final doc = bucket.toJson(); + final response = await supabaseService.client + .from(tableName) + .update(doc) + .eq('id', bucket.id!) + .eq('user_id', supabaseService.userId!) + .select() + .single(); + + return BucketModel.fromJson(response as Map); + } + + @override + Future deleteBucket(String id) async { + await supabaseService.client + .from(tableName) + .delete() + .eq('id', id) + .eq('user_id', supabaseService.userId!); + } +} \ No newline at end of file diff --git a/lib/features/tasks/modules/buckets/data/models/bucket_model.dart b/lib/features/tasks/modules/buckets/data/models/bucket_model.dart new file mode 100644 index 0000000..47a6fbc --- /dev/null +++ b/lib/features/tasks/modules/buckets/data/models/bucket_model.dart @@ -0,0 +1,60 @@ +import '../../domain/entities/bucket.dart'; + +class BucketModel extends Bucket { + BucketModel({ + required super.id, + required super.name, + super.isArchive = false, + required super.userId, + super.createdAt, + super.updatedAt, + }); + + /// Convert from JSON (Supabase response) + factory BucketModel.fromJson(Map json) { + return BucketModel( + id: json['id'] as String?, + name: json['name'] as String, + isArchive: json['is_archive'] as bool? ?? false, + userId: json['user_id'] as String, + createdAt: json['created_at'] != null + ? DateTime.parse(json['created_at'] as String) + : null, + updatedAt: json['updated_at'] != null + ? DateTime.parse(json['updated_at'] as String) + : null, + ); + } + + /// Convert to JSON (for Supabase insert/update) + Map toJson() { + return { + if (id != null) 'id': id, + 'name': name, + 'is_archive': isArchive, + 'user_id': userId, + }; + } + + /// Convert entity to model + factory BucketModel.fromEntity(Bucket bucket) { + return BucketModel( + id: bucket.id, + name: bucket.name, + isArchive: bucket.isArchive, + userId: bucket.userId, + createdAt: bucket.createdAt, + updatedAt: bucket.updatedAt, + ); + } + + /// Convert model back to entity + Bucket toEntity() { + return Bucket( + id: id, + name: name, + isArchive: isArchive, + userId: userId, + ); + } +} \ No newline at end of file diff --git a/lib/features/tasks/modules/buckets/data/repositories/bucket_repository_impl.dart b/lib/features/tasks/modules/buckets/data/repositories/bucket_repository_impl.dart new file mode 100644 index 0000000..71f0c71 --- /dev/null +++ b/lib/features/tasks/modules/buckets/data/repositories/bucket_repository_impl.dart @@ -0,0 +1,75 @@ +import 'package:keep_track/core/error/failure.dart'; +import 'package:keep_track/core/error/result.dart'; + +import '../../domain/entities/bucket.dart'; +import '../../domain/repositories/bucket_repository.dart'; +import '../datasources/bucket_datasource.dart'; +import '../datasources/supabase/bucket_datasource_supabase.dart'; +import '../models/bucket_model.dart'; +import 'package:keep_track/shared/infrastructure/supabase/supabase_service.dart'; + +/// Repository implementation for buckets +class BucketRepositoryImpl implements BucketRepository { + final BucketDataSource dataSource; + + BucketRepositoryImpl(this.dataSource); + + /// Create repository with Supabase data source + factory BucketRepositoryImpl.withSupabase(SupabaseService supabase) { + return BucketRepositoryImpl(BucketDataSourceSupabase(supabase)); + } + + @override + Future>> getBuckets() async { + final bucketModels = await dataSource.getBuckets(); + final buckets = bucketModels.cast(); + return Result.success(buckets); + } + + @override + Future> getBucketById(String id) async { + final bucket = await dataSource.getBucketById(id); + if (bucket == null) { + return Result.error( + NotFoundFailure( + resourceType: 'Bucket', + resourceId: id, + ), + ); + } + return Result.success(bucket); + } + + @override + Future>> getByIds(List ids) async { + final bucketModels = await dataSource.getByIds(ids); + final buckets = bucketModels.cast(); + return Result.success(buckets); + } + + @override + Future> createBucket(Bucket bucket) async { + final model = BucketModel.fromEntity(bucket); + final created = await dataSource.createBucket(model); + return Result.success(created); + } + + @override + Future> updateBucket(Bucket bucket) async { + if (bucket.id == null) { + return Result.error( + ValidationFailure('Bucket ID is required for updates'), + ); + } + + final model = BucketModel.fromEntity(bucket); + final updated = await dataSource.updateBucket(model); + return Result.success(updated); + } + + @override + Future> deleteBucket(String id) async { + await dataSource.deleteBucket(id); + return Result.success(null); + } +} \ No newline at end of file diff --git a/lib/features/tasks/modules/buckets/domain/entities/bucket.dart b/lib/features/tasks/modules/buckets/domain/entities/bucket.dart new file mode 100644 index 0000000..0d9a761 --- /dev/null +++ b/lib/features/tasks/modules/buckets/domain/entities/bucket.dart @@ -0,0 +1,51 @@ +/// Bucket entity - Task categorization similar to FinanceCategory +class Bucket { + final String? id; + final String name; + final bool isArchive; + final String? userId; + final DateTime? createdAt; // Optional - Supabase auto-generates + final DateTime? updatedAt; // Optional - Supabase auto-generates + + Bucket({ + this.id, + required this.name, + this.isArchive = false, + this.userId, + this.createdAt, + this.updatedAt, + }); + + Bucket copyWith({ + String? id, + String? name, + bool? isArchive, + String? userId, + DateTime? createdAt, + DateTime? updatedAt, + }) { + return Bucket( + id: id ?? this.id, + name: name ?? this.name, + isArchive: isArchive ?? this.isArchive, + userId: userId ?? this.userId, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Bucket && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; + + @override + String toString() { + return 'Bucket(id: $id, name: $name, isArchive: $isArchive, userId: $userId, createdAt: $createdAt, updatedAt: $updatedAt)'; + } +} \ No newline at end of file diff --git a/lib/features/tasks/modules/buckets/domain/repositories/bucket_repository.dart b/lib/features/tasks/modules/buckets/domain/repositories/bucket_repository.dart new file mode 100644 index 0000000..986f070 --- /dev/null +++ b/lib/features/tasks/modules/buckets/domain/repositories/bucket_repository.dart @@ -0,0 +1,25 @@ +import 'package:keep_track/core/error/result.dart'; + +import '../entities/bucket.dart'; + +/// Repository contract for task buckets +abstract class BucketRepository { + /// Get all buckets available to the current user + /// (system + user-created) + Future>> getBuckets(); + + /// Get a specific bucket by ID + Future> getBucketById(String id); + + /// Get multiple buckets by IDs (used for hydration) + Future>> getByIds(List ids); + + /// Create a new user-defined bucket + Future> createBucket(Bucket bucket); + + /// Update an existing bucket + Future> updateBucket(Bucket bucket); + + /// Delete a bucket + Future> deleteBucket(String id); +} \ No newline at end of file diff --git a/lib/features/tasks/modules/projects/data/datasources/project_datasource.dart b/lib/features/tasks/modules/projects/data/datasources/project_datasource.dart index 75bce90..243a1a6 100644 --- a/lib/features/tasks/modules/projects/data/datasources/project_datasource.dart +++ b/lib/features/tasks/modules/projects/data/datasources/project_datasource.dart @@ -11,6 +11,9 @@ abstract class ProjectDataSource { /// Get project by ID Future getProjectById(String id); + // Get Projects by bucketIDs + Future> getProjectsByBucketId(String bucketId); + /// Create a new project Future createProject(ProjectModel project); diff --git a/lib/features/tasks/modules/projects/data/datasources/supabase/project_datasource_supabase.dart b/lib/features/tasks/modules/projects/data/datasources/supabase/project_datasource_supabase.dart index 5aa224c..2f742a7 100644 --- a/lib/features/tasks/modules/projects/data/datasources/supabase/project_datasource_supabase.dart +++ b/lib/features/tasks/modules/projects/data/datasources/supabase/project_datasource_supabase.dart @@ -50,6 +50,20 @@ class ProjectDataSourceSupabase implements ProjectDataSource { : null; } + @override + Future> getProjectsByBucketId(String bucketId) async { + final response = await supabaseService.client + .from(tableName) + .select() + .eq('user_id', supabaseService.userId!) + .eq('bucket_id', bucketId) + .order('created_at', ascending: false); + + return (response as List) + .map((doc) => ProjectModel.fromJson(doc as Map)) + .toList(); + } + @override Future createProject(ProjectModel project) async { final doc = project.toJson(); diff --git a/lib/features/tasks/modules/projects/data/models/project_model.dart b/lib/features/tasks/modules/projects/data/models/project_model.dart index a8cf59f..aaa2d36 100644 --- a/lib/features/tasks/modules/projects/data/models/project_model.dart +++ b/lib/features/tasks/modules/projects/data/models/project_model.dart @@ -14,6 +14,7 @@ class ProjectModel extends Project { super.userId, super.status, super.metadata, + super.bucketId, }); /// Convert from domain entity to model @@ -29,6 +30,7 @@ class ProjectModel extends Project { userId: project.userId, status: project.status, metadata: project.metadata, + bucketId: project.bucketId, ); } @@ -70,6 +72,7 @@ class ProjectModel extends Project { userId: json['user_id'] as String?, status: status, metadata: metadata, + bucketId: json['bucket_id'] as String?, ); } @@ -84,6 +87,7 @@ class ProjectModel extends Project { if (userId != null) 'user_id': userId, 'status': status.name, 'metadata': metadata.isNotEmpty ? jsonEncode(metadata) : null, + if (bucketId != null) 'bucket_id': bucketId, }; } } diff --git a/lib/features/tasks/modules/projects/data/repositories/project_repository_impl.dart b/lib/features/tasks/modules/projects/data/repositories/project_repository_impl.dart index d5c3b87..ad00396 100644 --- a/lib/features/tasks/modules/projects/data/repositories/project_repository_impl.dart +++ b/lib/features/tasks/modules/projects/data/repositories/project_repository_impl.dart @@ -35,6 +35,13 @@ class ProjectRepositoryImpl implements ProjectRepository { return Result.success(project); } + @override + Future>> getProjectsByBucketID(String bucketId) async { + final taskModels = await dataSource.getProjectsByBucketId(bucketId); + final tasks = taskModels.cast(); + return Result.success(tasks); + } + @override Future> createProject(Project project) async { final model = ProjectModel.fromEntity(project); diff --git a/lib/features/tasks/modules/projects/domain/entities/project.dart b/lib/features/tasks/modules/projects/domain/entities/project.dart index 6ac6a2e..e5ff3b0 100644 --- a/lib/features/tasks/modules/projects/domain/entities/project.dart +++ b/lib/features/tasks/modules/projects/domain/entities/project.dart @@ -9,7 +9,9 @@ class Project { final bool isArchived; final String? userId; final ProjectStatus status; // Project status: active, postponed, closed - final Map metadata; // Dynamic metadata (e.g., links, ERD, etc.) + final Map + metadata; // Dynamic metadata (e.g., links, ERD, etc.) + final String? bucketId; Project({ this.id, @@ -22,6 +24,7 @@ class Project { this.userId, this.status = ProjectStatus.active, this.metadata = const {}, + this.bucketId, }); /// Copy with method for immutability @@ -36,6 +39,7 @@ class Project { String? userId, ProjectStatus? status, Map? metadata, + String? bucketId, }) { return Project( id: id ?? this.id, @@ -48,6 +52,7 @@ class Project { userId: userId ?? this.userId, status: status ?? this.status, metadata: metadata ?? this.metadata, + bucketId: bucketId ?? this.bucketId, ); } diff --git a/lib/features/tasks/modules/projects/domain/repositories/project_repository.dart b/lib/features/tasks/modules/projects/domain/repositories/project_repository.dart index 4bc524f..c8ba1d8 100644 --- a/lib/features/tasks/modules/projects/domain/repositories/project_repository.dart +++ b/lib/features/tasks/modules/projects/domain/repositories/project_repository.dart @@ -13,6 +13,8 @@ abstract class ProjectRepository { /// Get project by ID Future> getProjectById(String id); + Future>> getProjectsByBucketID(String bucketId); + /// Create a new project Future> createProject(Project project); diff --git a/lib/features/tasks/modules/tasks/data/datasources/mongodb/task_datasource_supabase.dart b/lib/features/tasks/modules/tasks/data/datasources/supabase/task_datasource_supabase.dart similarity index 91% rename from lib/features/tasks/modules/tasks/data/datasources/mongodb/task_datasource_supabase.dart rename to lib/features/tasks/modules/tasks/data/datasources/supabase/task_datasource_supabase.dart index 551f855..43dc90c 100644 --- a/lib/features/tasks/modules/tasks/data/datasources/mongodb/task_datasource_supabase.dart +++ b/lib/features/tasks/modules/tasks/data/datasources/supabase/task_datasource_supabase.dart @@ -78,6 +78,20 @@ class TaskDataSourceSupabase implements TaskDataSource { : null; } + @override + Future> getTasksByBucketId(String bucketId) async { + final response = await supabaseService.client + .from(tableName) + .select() + .eq('user_id', supabaseService.userId!) + .eq('bucket_id', bucketId) + .order('created_at', ascending: false); + + return (response as List) + .map((doc) => TaskModel.fromJson(doc as Map)) + .toList(); + } + @override Future createTask(TaskModel task) async { final doc = task.toJson(); @@ -186,7 +200,11 @@ class TaskDataSourceSupabase implements TaskDataSource { if (completedAtStr != null) { final completedAt = DateTime.parse(completedAtStr); // Normalize to day (remove time component) - final date = DateTime(completedAt.year, completedAt.month, completedAt.day); + final date = DateTime( + completedAt.year, + completedAt.month, + completedAt.day, + ); activity[date] = (activity[date] ?? 0) + 1; } } diff --git a/lib/features/tasks/modules/tasks/data/datasources/task_datasource.dart b/lib/features/tasks/modules/tasks/data/datasources/task_datasource.dart index 480835d..e38b168 100644 --- a/lib/features/tasks/modules/tasks/data/datasources/task_datasource.dart +++ b/lib/features/tasks/modules/tasks/data/datasources/task_datasource.dart @@ -17,6 +17,8 @@ abstract class TaskDataSource { /// Get task by ID Future getTaskById(String id); + Future> getTasksByBucketId(String bucketId); + /// Create a new task Future createTask(TaskModel task); diff --git a/lib/features/tasks/modules/tasks/data/models/task_model.dart b/lib/features/tasks/modules/tasks/data/models/task_model.dart index dcc74c6..df5f5fc 100644 --- a/lib/features/tasks/modules/tasks/data/models/task_model.dart +++ b/lib/features/tasks/modules/tasks/data/models/task_model.dart @@ -22,11 +22,12 @@ class TaskModel extends Task { super.financeCategoryId, super.actualTransactionId, super.userId, + super.bucketId, }) : super( - status: status, - priority: priority, - transactionType: transactionType, - ); + status: status, + priority: priority, + transactionType: transactionType, + ); /// Convert from domain entity to model factory TaskModel.fromEntity(Task task) { @@ -50,6 +51,7 @@ class TaskModel extends Task { financeCategoryId: task.financeCategoryId, actualTransactionId: task.actualTransactionId, userId: task.userId, + bucketId: task.bucketId, ); } @@ -60,7 +62,9 @@ class TaskModel extends Task { title: json['title'] as String, description: json['description'] as String?, status: TaskStatus.values.firstWhere((e) => e.name == json['status']), - priority: TaskPriority.values.firstWhere((e) => e.name == json['priority']), + priority: TaskPriority.values.firstWhere( + (e) => e.name == json['priority'], + ), projectId: json['project_id'] as String?, parentTaskId: json['parent_task_id'] as String?, tags: (json['tags'] as List?)?.cast() ?? [], @@ -80,11 +84,14 @@ class TaskModel extends Task { isMoneyRelated: json['is_money_related'] as bool? ?? false, expectedAmount: (json['expected_amount'] as num?)?.toDouble(), transactionType: json['transaction_type'] != null - ? TaskTransactionType.values.firstWhere((e) => e.name == json['transaction_type']) + ? TaskTransactionType.values.firstWhere( + (e) => e.name == json['transaction_type'], + ) : null, financeCategoryId: json['finance_category_id'] as String?, actualTransactionId: json['actual_transaction_id'] as String?, userId: json['user_id'] as String?, + bucketId: json['bucket_id'] as String?, ); } @@ -106,7 +113,8 @@ class TaskModel extends Task { if (expectedAmount != null) 'expected_amount': expectedAmount, if (transactionType != null) 'transaction_type': transactionType!.name, if (financeCategoryId != null) 'finance_category_id': financeCategoryId, - if (actualTransactionId != null) 'actual_transaction_id': actualTransactionId, + if (actualTransactionId != null) + 'actual_transaction_id': actualTransactionId, if (userId != null) 'user_id': userId, }; } diff --git a/lib/features/tasks/modules/tasks/data/repositories/task_repository_impl.dart b/lib/features/tasks/modules/tasks/data/repositories/task_repository_impl.dart index 291d647..22c4257 100644 --- a/lib/features/tasks/modules/tasks/data/repositories/task_repository_impl.dart +++ b/lib/features/tasks/modules/tasks/data/repositories/task_repository_impl.dart @@ -42,6 +42,13 @@ class TaskRepositoryImpl implements TaskRepository { return Result.success(task); } + @override + Future>> getTasksByBucketID(String bucketId) async { + final taskModels = await dataSource.getTasksByBucketId(bucketId); + final tasks = taskModels.cast(); + return Result.success(tasks); + } + @override Future> createTask(Task task) async { final model = TaskModel.fromEntity(task); @@ -89,12 +96,16 @@ class TaskRepositoryImpl implements TaskRepository { } @override - Future>> getTaskActivityForLastMonths(int months) async { + Future>> getTaskActivityForLastMonths( + int months, + ) async { try { final activity = await dataSource.getTaskActivityForLastMonths(months); return Result.success(activity); } catch (e) { - return Result.error(ServerFailure(message: 'Failed to fetch task activity: $e')); + return Result.error( + ServerFailure(message: 'Failed to fetch task activity: $e'), + ); } } } diff --git a/lib/features/tasks/modules/tasks/domain/entities/task.dart b/lib/features/tasks/modules/tasks/domain/entities/task.dart index d5b0917..877984e 100644 --- a/lib/features/tasks/modules/tasks/domain/entities/task.dart +++ b/lib/features/tasks/modules/tasks/domain/entities/task.dart @@ -14,6 +14,7 @@ class Task { final DateTime? completedAt; final bool archived; // Soft delete flag final String? userId; + final String? bucketId; // Financial integration fields final bool @@ -43,6 +44,7 @@ class Task { this.transactionType, this.financeCategoryId, this.actualTransactionId, + this.bucketId, this.userId, }); @@ -67,6 +69,7 @@ class Task { String? financeCategoryId, String? actualTransactionId, String? userId, + String? bucketId, }) { return Task( id: id ?? this.id, @@ -88,6 +91,7 @@ class Task { financeCategoryId: financeCategoryId ?? this.financeCategoryId, actualTransactionId: actualTransactionId ?? this.actualTransactionId, userId: userId ?? this.userId, + bucketId: bucketId ?? this.bucketId, ); } diff --git a/lib/features/tasks/modules/tasks/domain/repositories/task_repository.dart b/lib/features/tasks/modules/tasks/domain/repositories/task_repository.dart index 94fad88..e139a37 100644 --- a/lib/features/tasks/modules/tasks/domain/repositories/task_repository.dart +++ b/lib/features/tasks/modules/tasks/domain/repositories/task_repository.dart @@ -13,6 +13,9 @@ abstract class TaskRepository { /// Get tasks by status Future>> getTasksByStatus(TaskStatus status); + // Get tasks by bucket + Future>> getTasksByBucketID(String bucketId); + /// Get task by ID Future> getTaskById(String id); diff --git a/lib/features/tasks/presentation/screens/tabs/task/components/task_form_page.dart b/lib/features/tasks/presentation/screens/tabs/task/components/task_form_page.dart index deb3c27..41b491f 100644 --- a/lib/features/tasks/presentation/screens/tabs/task/components/task_form_page.dart +++ b/lib/features/tasks/presentation/screens/tabs/task/components/task_form_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/entities/bucket.dart'; import 'package:keep_track/features/tasks/modules/projects/domain/entities/project.dart'; import 'package:keep_track/features/tasks/modules/tasks/domain/entities/task.dart'; @@ -8,9 +9,11 @@ class TaskFormPage extends StatefulWidget { final Future Function(Task) onSave; final Future Function()? onDelete; final List? projects; + final List? buckets; final String? parentTaskId; final bool isDialog; // Whether this is shown in a dialog or as a full page - final bool isDialogContent; // Whether to return just content for custom dialog wrapper + final bool + isDialogContent; // Whether to return just content for custom dialog wrapper const TaskFormPage({ super.key, @@ -22,6 +25,7 @@ class TaskFormPage extends StatefulWidget { this.parentTaskId, this.isDialog = false, this.isDialogContent = false, + this.buckets, }); @override @@ -38,6 +42,8 @@ class _TaskFormPageState extends State { DateTime? _dueDate; List _tags = []; String? _selectedProjectId; + String? _selectedBucketId; + final TextEditingController _tagController = TextEditingController(); @override @@ -52,6 +58,7 @@ class _TaskFormPageState extends State { _dueDate = widget.task?.dueDate; _tags = widget.task?.tags.toList() ?? []; _selectedProjectId = widget.task?.projectId; + _selectedBucketId = widget.task?.bucketId; // Add bucket selection } @override @@ -145,6 +152,7 @@ class _TaskFormPageState extends State { userId: widget.userId, createdAt: widget.task?.createdAt, updatedAt: widget.task?.updatedAt, + bucketId: _selectedBucketId, ); await widget.onSave(task); @@ -180,178 +188,200 @@ class _TaskFormPageState extends State { // Use Column when wrapped in a parent ScrollView (isDialogContent or isDialog) // Use ListView otherwise for built-in scrolling final formFields = [ - // Title - TextFormField( - controller: _titleController, - decoration: const InputDecoration( - labelText: 'Title', - border: OutlineInputBorder(), - ), - validator: (v) => - v == null || v.isEmpty ? 'Enter task title' : null, - autofocus: !widget.isDialog, + // Title + TextFormField( + controller: _titleController, + decoration: const InputDecoration( + labelText: 'Title', + border: OutlineInputBorder(), + ), + validator: (v) => v == null || v.isEmpty ? 'Enter task title' : null, + autofocus: !widget.isDialog, + ), + const SizedBox(height: 16), + + // Description + TextFormField( + controller: _descriptionController, + decoration: const InputDecoration( + labelText: 'Description', + border: OutlineInputBorder(), + ), + maxLines: 5, + minLines: 3, + ), + const SizedBox(height: 16), + + // Status + DropdownButtonFormField( + value: _selectedStatus, + decoration: const InputDecoration( + labelText: 'Status', + border: OutlineInputBorder(), + ), + items: TaskStatus.values + .map( + (status) => DropdownMenuItem( + value: status, + child: Row( + children: [ + Icon( + Icons.circle, + size: 16, + color: _getStatusColor(status), + ), + const SizedBox(width: 8), + Text(status.displayName), + ], + ), + ), + ) + .toList(), + onChanged: (v) { + if (v != null) setState(() => _selectedStatus = v); + }, + ), + const SizedBox(height: 16), + + // Priority + DropdownButtonFormField( + value: _selectedPriority, + decoration: const InputDecoration( + labelText: 'Priority', + border: OutlineInputBorder(), + ), + items: TaskPriority.values + .map( + (priority) => DropdownMenuItem( + value: priority, + child: Row( + children: [ + Icon( + Icons.flag, + size: 16, + color: _getPriorityColor(priority), + ), + const SizedBox(width: 8), + Text(priority.displayName), + ], + ), + ), + ) + .toList(), + onChanged: (v) { + if (v != null) setState(() => _selectedPriority = v); + }, + ), + const SizedBox(height: 16), + + // Project + if (widget.projects != null && widget.projects!.isNotEmpty) ...[ + DropdownButtonFormField( + value: _selectedProjectId, + decoration: const InputDecoration( + labelText: 'Project', + border: OutlineInputBorder(), ), - const SizedBox(height: 16), - - // Description - TextFormField( - controller: _descriptionController, - decoration: const InputDecoration( - labelText: 'Description', - border: OutlineInputBorder(), + items: [ + const DropdownMenuItem( + value: null, + child: Text('No Project'), ), - maxLines: 5, - minLines: 3, - ), - const SizedBox(height: 16), - - // Status - DropdownButtonFormField( - value: _selectedStatus, - decoration: const InputDecoration( - labelText: 'Status', - border: OutlineInputBorder(), + ...widget.projects!.map( + (project) => DropdownMenuItem( + value: project.id, + child: Text(project.name), + ), ), - items: TaskStatus.values - .map( - (status) => DropdownMenuItem( - value: status, - child: Row( - children: [ - Icon( - Icons.circle, - size: 16, - color: _getStatusColor(status), - ), - const SizedBox(width: 8), - Text(status.displayName), - ], - ), - ), - ) - .toList(), - onChanged: (v) { - if (v != null) setState(() => _selectedStatus = v); - }, + ], + onChanged: (v) => setState(() => _selectedProjectId = v), + ), + const SizedBox(height: 16), + ], + // Buckets + if (widget.buckets != null && widget.buckets!.isNotEmpty) ...[ + DropdownButtonFormField( + value: _selectedBucketId, + decoration: const InputDecoration( + labelText: 'Buckets', + border: OutlineInputBorder(), ), - const SizedBox(height: 16), - - // Priority - DropdownButtonFormField( - value: _selectedPriority, - decoration: const InputDecoration( - labelText: 'Priority', - border: OutlineInputBorder(), + items: [ + const DropdownMenuItem( + value: null, + child: Text('No Buckets'), ), - items: TaskPriority.values - .map( - (priority) => DropdownMenuItem( - value: priority, - child: Row( - children: [ - Icon( - Icons.flag, - size: 16, - color: _getPriorityColor(priority), - ), - const SizedBox(width: 8), - Text(priority.displayName), - ], - ), - ), - ) - .toList(), - onChanged: (v) { - if (v != null) setState(() => _selectedPriority = v); - }, - ), - const SizedBox(height: 16), - - // Project - if (widget.projects != null && widget.projects!.isNotEmpty) ...[ - DropdownButtonFormField( - value: _selectedProjectId, - decoration: const InputDecoration( - labelText: 'Project', - border: OutlineInputBorder(), + ...widget.buckets!.map( + (bucket) => DropdownMenuItem( + value: bucket.id, + child: Text(bucket.name), ), - items: [ - const DropdownMenuItem( - value: null, - child: Text('No Project'), - ), - ...widget.projects!.map( - (project) => DropdownMenuItem( - value: project.id, - child: Text(project.name), - ), - ), - ], - onChanged: (v) => setState(() => _selectedProjectId = v), ), - const SizedBox(height: 16), ], - - // Due Date - Card( - child: ListTile( - leading: const Icon(Icons.calendar_today), - title: const Text('Due Date'), - subtitle: _dueDate != null - ? Text( - '${_dueDate!.toString().split('.')[0]}', - style: const TextStyle(fontWeight: FontWeight.w500), - ) - : const Text('Not set'), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (_dueDate != null) - IconButton( - icon: const Icon(Icons.clear), - onPressed: () => setState(() => _dueDate = null), - tooltip: 'Clear due date', - ), - IconButton( - icon: const Icon(Icons.edit), - onPressed: _pickDueDate, - tooltip: 'Set due date', - ), - ], + onChanged: (v) => setState(() => _selectedBucketId = v), + ), + const SizedBox(height: 16), + ], + // Due Date + Card( + child: ListTile( + leading: const Icon(Icons.calendar_today), + title: const Text('Due Date'), + subtitle: _dueDate != null + ? Text( + '${_dueDate!.toString().split('.')[0]}', + style: const TextStyle(fontWeight: FontWeight.w500), + ) + : const Text('Not set'), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_dueDate != null) + IconButton( + icon: const Icon(Icons.clear), + onPressed: () => setState(() => _dueDate = null), + tooltip: 'Clear due date', + ), + IconButton( + icon: const Icon(Icons.edit), + onPressed: _pickDueDate, + tooltip: 'Set due date', ), - ), + ], ), - const SizedBox(height: 16), + ), + ), + const SizedBox(height: 16), - // Tags - const Text( - 'Tags', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), - ), - const SizedBox(height: 8), - Card( - child: Padding( - padding: const EdgeInsets.all(12), - child: Wrap( - spacing: 8, - runSpacing: 8, - children: [ - ..._tags.map( - (tag) => Chip( - label: Text(tag), - onDeleted: () => setState(() => _tags.remove(tag)), - backgroundColor: Colors.blue.withOpacity(0.1), - ), - ), - ActionChip( - avatar: const Icon(Icons.add, size: 16), - label: const Text('Add tag'), - onPressed: _addTag, - ), - ], + // Tags + const Text( + 'Tags', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 8), + Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ..._tags.map( + (tag) => Chip( + label: Text(tag), + onDeleted: () => setState(() => _tags.remove(tag)), + backgroundColor: Colors.blue.withOpacity(0.1), + ), ), - ), + ActionChip( + avatar: const Icon(Icons.add, size: 16), + label: const Text('Add tag'), + onPressed: _addTag, + ), + ], ), + ), + ), ]; return Form( @@ -361,10 +391,7 @@ class _TaskFormPageState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: formFields, ) - : ListView( - padding: const EdgeInsets.all(16), - children: formFields, - ), + : ListView(padding: const EdgeInsets.all(16), children: formFields), ); } @@ -427,7 +454,10 @@ class _TaskFormPageState extends State { if (isEdit && widget.onDelete != null) TextButton( onPressed: _handleDelete, - child: const Text('Delete', style: TextStyle(color: Colors.red)), + child: const Text( + 'Delete', + style: TextStyle(color: Colors.red), + ), ), const SizedBox(width: 8), TextButton( diff --git a/lib/features/tasks/presentation/screens/tabs/task/components/task_management_dialog.dart b/lib/features/tasks/presentation/screens/tabs/task/components/task_management_dialog.dart index 43682f6..fda0fc1 100644 --- a/lib/features/tasks/presentation/screens/tabs/task/components/task_management_dialog.dart +++ b/lib/features/tasks/presentation/screens/tabs/task/components/task_management_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/entities/bucket.dart'; import 'package:keep_track/features/tasks/modules/projects/domain/entities/project.dart'; import 'package:keep_track/features/tasks/modules/tasks/domain/entities/task.dart'; @@ -12,8 +13,10 @@ class TaskManagementDialog extends StatelessWidget { final Future Function(Task) onSave; final Future Function()? onDelete; final List? projects; + final List? buckets; final String? parentTaskId; - final bool useDialogContent; // Use dialog content mode (for custom dialog wrappers) + final bool + useDialogContent; // Use dialog content mode (for custom dialog wrappers) const TaskManagementDialog({ super.key, @@ -24,6 +27,7 @@ class TaskManagementDialog extends StatelessWidget { this.projects, this.parentTaskId, this.useDialogContent = false, + this.buckets, }); @override @@ -34,6 +38,7 @@ class TaskManagementDialog extends StatelessWidget { onSave: onSave, onDelete: onDelete, projects: projects, + buckets: buckets, parentTaskId: parentTaskId, isDialog: !useDialogContent, isDialogContent: useDialogContent, diff --git a/lib/features/tasks/presentation/screens/tabs/task/create_task_route_page.dart b/lib/features/tasks/presentation/screens/tabs/task/create_task_route_page.dart index 474e0e5..2c7c9e8 100644 --- a/lib/features/tasks/presentation/screens/tabs/task/create_task_route_page.dart +++ b/lib/features/tasks/presentation/screens/tabs/task/create_task_route_page.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:keep_track/core/di/service_locator.dart'; import 'package:keep_track/core/state/stream_builder_widget.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/entities/bucket.dart'; import 'package:keep_track/features/tasks/modules/tasks/domain/entities/task.dart'; +import 'package:keep_track/features/tasks/presentation/state/bucket_controller.dart'; import 'package:keep_track/features/tasks/presentation/state/task_controller.dart'; import 'package:keep_track/features/tasks/presentation/screens/tabs/task/components/task_form_page.dart'; import 'package:keep_track/shared/infrastructure/supabase/supabase_service.dart'; @@ -20,6 +22,7 @@ class _CreateTaskRoutePageState extends State { late final TaskController _controller; late final SupabaseService _supabase; late final ProjectController _projectController; + late final BucketController _bucketController; @override void initState() { @@ -27,9 +30,10 @@ class _CreateTaskRoutePageState extends State { _controller = locator.get(); _supabase = locator.get(); _projectController = locator.get(); - + _bucketController = locator.get(); // Load active projects _projectController.loadActiveProjects(); + _bucketController.loadBuckets(); } @override @@ -40,36 +44,41 @@ class _CreateTaskRoutePageState extends State { final activeProjects = projects .where((p) => p.status == ProjectStatus.active && !p.isArchived) .toList(); + return AsyncStreamBuilder>( + state: _bucketController, + builder: (context, buckets) { + return TaskFormPage( + userId: _supabase.userId!, + projects: activeProjects, + buckets: buckets, + isDialog: false, + isDialogContent: false, + onSave: (Task task) async { + try { + await _controller.createTask(task); - return TaskFormPage( - userId: _supabase.userId!, - projects: activeProjects, - isDialog: false, - isDialogContent: false, - onSave: (Task task) async { - try { - await _controller.createTask(task); - - if (!mounted) return; + if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Task created successfully'), - backgroundColor: Colors.green, - ), - ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Task created successfully'), + backgroundColor: Colors.green, + ), + ); - Navigator.pop(context); - } catch (e) { - if (!mounted) return; + Navigator.pop(context); + } catch (e) { + if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to create task: $e'), - backgroundColor: Colors.red, - ), - ); - } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to create task: $e'), + backgroundColor: Colors.red, + ), + ); + } + }, + ); }, ); }, diff --git a/lib/features/tasks/presentation/state/bucket_controller.dart b/lib/features/tasks/presentation/state/bucket_controller.dart new file mode 100644 index 0000000..aa4ee52 --- /dev/null +++ b/lib/features/tasks/presentation/state/bucket_controller.dart @@ -0,0 +1,67 @@ +import 'package:keep_track/core/error/result.dart'; +import 'package:keep_track/core/state/stream_state.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/entities/bucket.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/repositories/bucket_repository.dart'; + +/// Controller for managing bucket state and operations +class BucketController extends StreamState>> { + final BucketRepository _repository; + + BucketController(this._repository) : super(const AsyncLoading()) { + loadBuckets(); + } + + /// Load all buckets + Future loadBuckets() async { + await execute(() async { + return await _repository.getBuckets().then((r) => r.unwrap()); + }); + } + + /// Create a new bucket + Future createBucket({required String name}) async { + // TODO: Implement Create bucket + } + + /// Update an existing bucket + Future updateBucket(Bucket bucket) async { + // TODO: Implement UpdateBucket + } + + /// Delete a bucket + Future deleteBucket(String id) async { + // TODO: Implement DeleteBucket + } + + /// Archive a bucket (soft delete) + Future archiveBucket(String id) async { + // TODO: Implement ArchiveBucket + } + + /// Refresh current data + Future refresh() async { + await loadBuckets(); + } + + /// Clear all buckets (for testing or reset) + void clearBuckets() { + emit(const AsyncData([])); + } + + /// Get buckets currently in state (if loaded) + List? get currentBuckets => state is AsyncData> + ? (state as AsyncData>).data + : null; + + /// Get bucket by ID from current state + Bucket? getBucketFromCurrentState(String id) { + final buckets = currentBuckets; + if (buckets == null) return null; + + try { + return buckets.where((b) => b.id == id).first; + } catch (e) { + return null; + } + } +} diff --git a/lib/features/tasks/presentation/state/project_controller.dart b/lib/features/tasks/presentation/state/project_controller.dart index 8db1f41..7d43825 100644 --- a/lib/features/tasks/presentation/state/project_controller.dart +++ b/lib/features/tasks/presentation/state/project_controller.dart @@ -25,6 +25,15 @@ class ProjectController extends StreamState>> { }); } + // Load all projects related to selected bucket + Future loadProjectsByBucketId(String bucketId) async { + await execute(() async { + return await _repository + .getProjectsByBucketID(bucketId) + .then((r) => r.unwrap()); + }); + } + /// Create a new project Future createProject(Project project) async { await execute(() async { diff --git a/lib/features/tasks/presentation/state/task_controller.dart b/lib/features/tasks/presentation/state/task_controller.dart index ca28cd5..90c37b6 100644 --- a/lib/features/tasks/presentation/state/task_controller.dart +++ b/lib/features/tasks/presentation/state/task_controller.dart @@ -58,6 +58,14 @@ class TaskController extends StreamState>> { }); } + Future loadProjectsByBucketId(String bucketId) async { + await execute(() async { + return await _repository + .getTasksByBucketID(bucketId) + .then((r) => r.unwrap()); + }); + } + /// Search tasks Future searchTasks(String query) async { await execute(() async { diff --git a/lib/features/tasks/tasks.dart b/lib/features/tasks/tasks.dart index 51098a7..edc8fad 100644 --- a/lib/features/tasks/tasks.dart +++ b/lib/features/tasks/tasks.dart @@ -12,7 +12,7 @@ export 'modules/projects/domain/repositories/project_repository.dart'; // Data export 'modules/tasks/data/models/task_model.dart'; export 'modules/tasks/data/datasources/task_datasource.dart'; -export 'modules/tasks/data/datasources/mongodb/task_datasource_supabase.dart'; +export 'modules/tasks/data/datasources/supabase/task_datasource_supabase.dart'; export 'modules/tasks/data/repositories/task_repository_impl.dart'; export 'modules/projects/data/datasources/supabase/project_datasource_supabase.dart'; export 'modules/projects/data/datasources/project_datasource.dart'; diff --git a/lib/features/tasks/tasks_di.dart b/lib/features/tasks/tasks_di.dart index 792afb7..eae21ec 100644 --- a/lib/features/tasks/tasks_di.dart +++ b/lib/features/tasks/tasks_di.dart @@ -4,11 +4,17 @@ library; import 'package:keep_track/features/profile/presentation/state/task_activity_controller.dart'; +import 'package:keep_track/features/tasks/data/sevices/bucket_initialization_service.dart'; +import 'package:keep_track/features/tasks/modules/buckets/data/datasources/bucket_datasource.dart'; +import 'package:keep_track/features/tasks/modules/buckets/data/datasources/supabase/bucket_datasource_supabase.dart'; +import 'package:keep_track/features/tasks/modules/buckets/data/repositories/bucket_repository_impl.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/repositories/bucket_repository.dart'; +import 'package:keep_track/features/tasks/presentation/state/bucket_controller.dart'; import 'package:keep_track/features/tasks/presentation/state/task_controller.dart'; import 'package:keep_track/shared/infrastructure/supabase/supabase_service.dart'; import '../../core/di/service_locator.dart'; -import 'modules/tasks/data/datasources/mongodb/task_datasource_supabase.dart'; +import 'modules/tasks/data/datasources/supabase/task_datasource_supabase.dart'; import 'modules/tasks/data/datasources/task_datasource.dart'; import 'modules/tasks/data/repositories/task_repository_impl.dart'; import 'modules/tasks/domain/repositories/task_repository.dart'; @@ -80,4 +86,23 @@ void setupTasksDependencies() { final supabaseService = locator.get(); return PomodoroSessionController(repo, supabaseService.userId!); }); + + locator.registerFactory(() { + final supabaseService = locator.get(); + return BucketDataSourceSupabase(supabaseService); + }); + + locator.registerFactory(() { + final dataSource = locator.get(); + return BucketRepositoryImpl(dataSource); + }); + + locator.registerFactory(() { + final bucketRepository = locator.get(); + return BucketInitializationService(bucketRepository); + }); + locator.registerFactory(() { + final bucketRepository = locator.get(); + return BucketController(bucketRepository); + }); } From 363c9e89af4fbf170bdaa484a4ab88f52e7ddff4 Mon Sep 17 00:00:00 2001 From: Khesir Date: Sat, 17 Jan 2026 23:07:24 +0800 Subject: [PATCH 02/13] feat: improve task and finance visuals Changes: - added bar graph on finance home - fixed tasks buckets - added management screen for buckets - added account details page --- lib/core/routing/app_router.dart | 21 + .../screens/account_details_screen.dart | 717 +++++++++++++ .../tabs/accounts/accounts_tab_new.dart | 8 +- lib/features/home/home_screen.dart | 401 +++++++ lib/features/home/task_home_screen.dart | 717 +++++++------ .../subpages/app_configuration_task_page.dart | 27 + .../modules/tasks/data/models/task_model.dart | 1 + .../modules/tasks/domain/entities/task.dart | 76 +- .../bucket_management_screen.dart | 544 ++++++++++ .../screens/project_details_screen.dart | 324 +++++- .../screens/tabs/task/task_details_page.dart | 274 +++-- .../screens/tabs/task/tasks_tab_new.dart | 511 +++++---- .../screens/tasks_home_screen_new.dart | 976 ------------------ .../presentation/state/bucket_controller.dart | 33 +- .../state/project_controller.dart | 17 + pubspec.yaml | 1 + 16 files changed, 3041 insertions(+), 1607 deletions(-) create mode 100644 lib/features/finance/presentation/screens/account_details_screen.dart create mode 100644 lib/features/tasks/presentation/screens/configuration/bucket_management_screen.dart delete mode 100644 lib/features/tasks/presentation/screens/tasks_home_screen_new.dart diff --git a/lib/core/routing/app_router.dart b/lib/core/routing/app_router.dart index c19c766..2d7c3a7 100644 --- a/lib/core/routing/app_router.dart +++ b/lib/core/routing/app_router.dart @@ -9,6 +9,7 @@ import 'package:keep_track/features/module_selection/task_module_screen.dart'; import 'package:keep_track/features/settings/setting_page.dart'; import 'package:keep_track/features/settings/subpages/app_configuration_finance_page.dart'; import 'package:keep_track/features/settings/subpages/app_configuration_task_page.dart'; +import 'package:keep_track/features/tasks/presentation/screens/configuration/bucket_management_screen.dart'; import 'package:keep_track/features/tasks/presentation/screens/configuration/project_management_screen.dart'; import 'package:keep_track/features/tasks/presentation/screens/configuration/task_management_screen.dart'; import 'package:keep_track/features/tasks/presentation/screens/tabs/task/create_task_page.dart'; @@ -16,6 +17,8 @@ import 'package:keep_track/features/tasks/presentation/screens/create_project_pa import '../../features/finance/modules/budget/domain/entities/budget.dart'; import '../../features/finance/modules/transaction/domain/entities/transaction.dart'; import '../../features/finance/presentation/screens/configuration/accounts/account_management.dart'; +import '../../features/finance/presentation/screens/account_details_screen.dart'; +import '../../features/finance/modules/account/domain/entities/account.dart'; import '../../features/finance/presentation/screens/configuration/budgets/budget_management_screen.dart'; import '../../features/finance/presentation/screens/configuration/budgets/create_budget_screen.dart'; import '../../features/finance/presentation/screens/configuration/budgets/budget_detail_screen.dart'; @@ -66,9 +69,11 @@ class AppRoutes { // Task Management Settings static const String taskManagement = '/task-management'; static const String projectManagement = '/project-management'; + static const String bucketManagement = '/bucket-management'; // Finance Management static const String accountManagement = '/account-management'; + static const String accountDetail = '/account-detail'; static const String categoryManagement = '/category-management'; static const String budgetManagement = '/budget-management'; static const String goalsManagement = '/goals-management'; @@ -125,6 +130,11 @@ class AppRouter { builder: (_) => const ProjectManagementScreen(), settings: settings, ); + case AppRoutes.bucketManagement: + return MaterialPageRoute( + builder: (_) => const BucketManagementScreen(), + settings: settings, + ); case AppRoutes.projectDetail: final project = settings.arguments as Project?; @@ -207,6 +217,17 @@ class AppRouter { builder: (_) => const AccountManagementScreen(), settings: settings, ); + case AppRoutes.accountDetail: + final account = settings.arguments as Account?; + if (account == null) { + return MaterialPageRoute( + builder: (_) => UnknownRouteScreen(routeName: settings.name ?? ''), + ); + } + return MaterialPageRoute( + builder: (_) => AccountDetailsScreen(account: account), + settings: settings, + ); case AppRoutes.categoryManagement: return MaterialPageRoute( builder: (_) => const CategoryManagementScreen(), diff --git a/lib/features/finance/presentation/screens/account_details_screen.dart b/lib/features/finance/presentation/screens/account_details_screen.dart new file mode 100644 index 0000000..cbe5105 --- /dev/null +++ b/lib/features/finance/presentation/screens/account_details_screen.dart @@ -0,0 +1,717 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:keep_track/core/di/service_locator.dart'; +import 'package:keep_track/core/settings/utils/currency_formatter.dart'; +import 'package:keep_track/core/state/stream_builder_widget.dart'; +import 'package:keep_track/core/theme/app_theme.dart'; +import 'package:keep_track/core/ui/app_layout_controller.dart'; +import 'package:keep_track/core/ui/responsive/desktop_aware_screen.dart'; +import 'package:keep_track/core/ui/ui.dart'; +import 'package:keep_track/core/utils/icon_helper.dart'; +import 'package:keep_track/features/finance/modules/account/domain/entities/account.dart'; +import 'package:keep_track/features/finance/modules/transaction/domain/entities/transaction.dart'; +import 'package:keep_track/features/finance/presentation/state/transaction_controller.dart'; + +/// Daily finance data for bar graph +class DailyAccountData { + final DateTime date; + final double income; + final double expense; + final double transfer; + + DailyAccountData({ + required this.date, + required this.income, + required this.expense, + required this.transfer, + }); +} + +class AccountDetailsScreen extends ScopedScreen { + final Account account; + + const AccountDetailsScreen({super.key, required this.account}); + + @override + State createState() => _AccountDetailsScreenState(); +} + +class _AccountDetailsScreenState extends ScopedScreenState + with AppLayoutControlled { + late final TransactionController _transactionController; + int _daysToShow = 15; + + @override + void registerServices() { + _transactionController = locator.get(); + } + + @override + void onReady() { + configureLayout(title: widget.account.name, showBottomNav: false); + _transactionController.loadTransactionsByAccount(widget.account.id!); + } + + List _processTransactionsToDaily(List transactions) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final startDate = today.subtract(Duration(days: _daysToShow - 1)); + + // Create a map with all dates initialized to zero + final Map dailyMap = {}; + for (int i = 0; i < _daysToShow; i++) { + final date = startDate.add(Duration(days: i)); + final dateKey = DateFormat('yyyy-MM-dd').format(date); + dailyMap[dateKey] = DailyAccountData( + date: date, + income: 0, + expense: 0, + transfer: 0, + ); + } + + // Aggregate transactions by date + for (final transaction in transactions) { + final dateKey = DateFormat('yyyy-MM-dd').format(transaction.date); + if (dailyMap.containsKey(dateKey)) { + final existing = dailyMap[dateKey]!; + switch (transaction.type) { + case TransactionType.income: + dailyMap[dateKey] = DailyAccountData( + date: existing.date, + income: existing.income + transaction.amount, + expense: existing.expense, + transfer: existing.transfer, + ); + break; + case TransactionType.expense: + dailyMap[dateKey] = DailyAccountData( + date: existing.date, + income: existing.income, + expense: existing.expense + transaction.amount, + transfer: existing.transfer, + ); + break; + case TransactionType.transfer: + dailyMap[dateKey] = DailyAccountData( + date: existing.date, + income: existing.income, + expense: existing.expense, + transfer: existing.transfer + transaction.amount, + ); + break; + } + } + } + + // Convert to list and sort by date + final result = dailyMap.values.toList(); + result.sort((a, b) => a.date.compareTo(b.date)); + return result; + } + + @override + Widget build(BuildContext context) { + final accountColor = widget.account.colorHex != null + ? Color(int.parse(widget.account.colorHex!.replaceFirst('#', '0xff'))) + : Colors.blue[700]!; + final accountIcon = IconHelper.fromString(widget.account.iconCodePoint); + + return DesktopAwareScreen( + builder: (context, isDesktop) { + return Scaffold( + backgroundColor: isDesktop + ? (Theme.of(context).brightness == Brightness.dark + ? const Color(0xFF09090B) + : AppColors.backgroundSecondary) + : null, + appBar: AppBar( + title: Text(widget.account.name), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), + ), + body: AsyncStreamBuilder>( + state: _transactionController, + loadingBuilder: (_) => const Center(child: CircularProgressIndicator()), + errorBuilder: (context, message) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 64, color: Colors.red[300]), + const SizedBox(height: 16), + Text('Error: $message'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => _transactionController + .loadTransactionsByAccount(widget.account.id!), + child: const Text('Retry'), + ), + ], + ), + ), + builder: (context, transactions) { + // Filter transactions for this account + final accountTransactions = transactions.where((t) => + t.accountId == widget.account.id || + t.toAccountId == widget.account.id).toList(); + + // Sort by date descending + accountTransactions.sort((a, b) => b.date.compareTo(a.date)); + + // Calculate totals + double totalIncome = 0; + double totalExpense = 0; + double totalTransfer = 0; + + for (final t in accountTransactions) { + switch (t.type) { + case TransactionType.income: + totalIncome += t.amount; + break; + case TransactionType.expense: + totalExpense += t.amount; + break; + case TransactionType.transfer: + totalTransfer += t.amount; + break; + } + } + + return SingleChildScrollView( + padding: EdgeInsets.all(isDesktop ? AppSpacing.xl : 16), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: isDesktop ? 1200 : double.infinity, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Account Info Card + _buildAccountInfoCard(accountColor, accountIcon, isDesktop), + SizedBox(height: isDesktop ? AppSpacing.xl : 24), + + // Summary Cards + _buildSummaryCards( + totalIncome, + totalExpense, + totalTransfer, + isDesktop, + ), + SizedBox(height: isDesktop ? AppSpacing.xl : 24), + + // Bar Graph + _buildBarGraph(accountTransactions, isDesktop), + SizedBox(height: isDesktop ? AppSpacing.xl : 24), + + // Transactions List + _buildTransactionsList(accountTransactions, isDesktop), + ], + ), + ), + ), + ); + }, + ), + ); + }, + ); + } + + Widget _buildAccountInfoCard(Color accountColor, IconData accountIcon, bool isDesktop) { + return Card( + elevation: 0, + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [accountColor, accountColor.withOpacity(0.7)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon(accountIcon, color: Colors.white, size: 28), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.account.name, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + widget.account.accountType.toString().split('.').last, + style: const TextStyle(color: Colors.white70, fontSize: 14), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 24), + Text( + 'Current Balance', + style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 14), + ), + const SizedBox(height: 4), + Text( + NumberFormat.currency( + symbol: currencyFormatter.currencySymbol, + decimalDigits: 2, + ).format(widget.account.balance), + style: const TextStyle( + color: Colors.white, + fontSize: 36, + fontWeight: FontWeight.bold, + ), + ), + if (widget.account.bankAccountNumber != null) ...[ + const SizedBox(height: 16), + Row( + children: [ + const Icon(Icons.account_balance, color: Colors.white70, size: 16), + const SizedBox(width: 8), + Text( + widget.account.bankAccountNumber!, + style: const TextStyle(color: Colors.white70, fontSize: 14), + ), + ], + ), + ], + ], + ), + ), + ); + } + + Widget _buildSummaryCards( + double totalIncome, + double totalExpense, + double totalTransfer, + bool isDesktop, + ) { + return Row( + children: [ + Expanded( + child: _buildSummaryCard( + 'Income', + totalIncome, + Colors.green, + Icons.arrow_downward, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildSummaryCard( + 'Expense', + totalExpense, + Colors.red, + Icons.arrow_upward, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildSummaryCard( + 'Transfer', + totalTransfer, + Colors.blue, + Icons.swap_horiz, + ), + ), + ], + ); + } + + Widget _buildSummaryCard( + String label, + double amount, + Color color, + IconData icon, + ) { + return Card( + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, color: color, size: 20), + const SizedBox(width: 8), + Text( + label, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + fontSize: 12, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + NumberFormat.currency( + symbol: currencyFormatter.currencySymbol, + decimalDigits: 0, + ).format(amount), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), + ), + ); + } + + Widget _buildBarGraph(List transactions, bool isDesktop) { + final data = _processTransactionsToDaily(transactions); + final maxAmount = data.fold( + 0, + (max, d) { + final dayMax = [d.income, d.expense, d.transfer].reduce((a, b) => a > b ? a : b); + return dayMax > max ? dayMax : max; + }, + ); + + return Card( + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Transaction History', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: DropdownButton( + value: _daysToShow, + isDense: true, + underline: const SizedBox(), + items: [7, 15, 30].map((days) { + return DropdownMenuItem( + value: days, + child: Text('$days days', style: const TextStyle(fontSize: 12)), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + setState(() { + _daysToShow = value; + }); + } + }, + ), + ), + ], + ), + const SizedBox(height: 12), + + // Legend + Row( + children: [ + _buildLegendIndicator(Colors.green, 'Income'), + const SizedBox(width: 16), + _buildLegendIndicator(Colors.red, 'Expense'), + const SizedBox(width: 16), + _buildLegendIndicator(Colors.blue, 'Transfer'), + ], + ), + const SizedBox(height: 16), + + // Bar Graph + SizedBox( + height: isDesktop ? 200 : 160, + child: data.isEmpty || maxAmount == 0 + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.bar_chart, + size: 48, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.3), + ), + const SizedBox(height: 8), + Text( + 'No transactions in this period', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + ) + : LayoutBuilder( + builder: (context, constraints) { + final barWidth = (constraints.maxWidth - (data.length - 1) * 6) / (data.length * 3); + final clampedBarWidth = barWidth.clamp(3.0, 16.0); + + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: data.map((dayData) { + final incomeHeight = maxAmount > 0 + ? (dayData.income / maxAmount) * (isDesktop ? 160 : 120) + : 0.0; + final expenseHeight = maxAmount > 0 + ? (dayData.expense / maxAmount) * (isDesktop ? 160 : 120) + : 0.0; + final transferHeight = maxAmount > 0 + ? (dayData.transfer / maxAmount) * (isDesktop ? 160 : 120) + : 0.0; + + return Tooltip( + message: '${DateFormat('MMM d').format(dayData.date)}\n' + 'Income: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.income)}\n' + 'Expense: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.expense)}\n' + 'Transfer: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.transfer)}', + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Income bar + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: clampedBarWidth, + height: incomeHeight, + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.8), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(3), + ), + ), + ), + const SizedBox(width: 1), + // Expense bar + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: clampedBarWidth, + height: expenseHeight, + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.8), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(3), + ), + ), + ), + const SizedBox(width: 1), + // Transfer bar + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: clampedBarWidth, + height: transferHeight, + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.8), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(3), + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + DateFormat('d').format(dayData.date), + style: TextStyle( + fontSize: 9, + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.5), + ), + ), + ], + ), + ); + }).toList(), + ); + }, + ), + ), + ], + ), + ), + ); + } + + Widget _buildLegendIndicator(Color color, String label) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: color.withOpacity(0.8), + borderRadius: BorderRadius.circular(3), + ), + ), + const SizedBox(width: 6), + Text( + label, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + ], + ); + } + + Widget _buildTransactionsList(List transactions, bool isDesktop) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'All Transactions', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Text( + '${transactions.length} transaction${transactions.length != 1 ? 's' : ''}', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + const SizedBox(height: 16), + if (transactions.isEmpty) + Card( + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(32), + child: Center( + child: Column( + children: [ + Icon( + Icons.receipt_long, + size: 48, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.3), + ), + const SizedBox(height: 8), + Text( + 'No transactions yet', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + ), + ), + ) + else + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: transactions.length, + itemBuilder: (context, index) { + final transaction = transactions[index]; + return _buildTransactionItem(transaction); + }, + ), + ], + ); + } + + Widget _buildTransactionItem(Transaction transaction) { + Color typeColor; + IconData typeIcon; + String prefix; + + switch (transaction.type) { + case TransactionType.income: + typeColor = Colors.green; + typeIcon = Icons.arrow_downward; + prefix = '+'; + break; + case TransactionType.expense: + typeColor = Colors.red; + typeIcon = Icons.arrow_upward; + prefix = '-'; + break; + case TransactionType.transfer: + typeColor = Colors.blue; + typeIcon = Icons.swap_horiz; + prefix = transaction.accountId == widget.account.id ? '-' : '+'; + break; + } + + return Card( + elevation: 0, + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: typeColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(typeIcon, color: typeColor, size: 20), + ), + title: Text( + transaction.description ?? transaction.type.displayName, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + subtitle: Text( + DateFormat('MMM d, yyyy - h:mm a').format(transaction.date), + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + trailing: Text( + '$prefix${NumberFormat.currency( + symbol: currencyFormatter.currencySymbol, + decimalDigits: 2, + ).format(transaction.amount)}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: typeColor, + ), + ), + ), + ); + } +} diff --git a/lib/features/finance/presentation/screens/tabs/accounts/accounts_tab_new.dart b/lib/features/finance/presentation/screens/tabs/accounts/accounts_tab_new.dart index 360c225..021825c 100644 --- a/lib/features/finance/presentation/screens/tabs/accounts/accounts_tab_new.dart +++ b/lib/features/finance/presentation/screens/tabs/accounts/accounts_tab_new.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:keep_track/core/settings/utils/currency_formatter.dart'; import 'package:keep_track/core/theme/app_theme.dart'; import 'package:keep_track/core/ui/responsive/desktop_aware_screen.dart'; +import 'package:keep_track/core/routing/app_router.dart'; import 'package:intl/intl.dart'; import 'package:keep_track/core/di/service_locator.dart'; import 'package:keep_track/core/state/stream_builder_widget.dart'; @@ -285,8 +286,11 @@ class _AccountsTabNewState extends State { clipBehavior: Clip.antiAlias, child: InkWell( onTap: () { - // Navigate to account detail or show edit dialog - // You can implement navigation here + Navigator.pushNamed( + context, + AppRoutes.accountDetail, + arguments: account, + ); }, child: Padding( padding: const EdgeInsets.all(16), diff --git a/lib/features/home/home_screen.dart b/lib/features/home/home_screen.dart index 5069f64..96d52ce 100644 --- a/lib/features/home/home_screen.dart +++ b/lib/features/home/home_screen.dart @@ -10,8 +10,10 @@ import 'package:keep_track/core/ui/ui.dart'; import 'package:keep_track/core/routing/app_router.dart'; import 'package:keep_track/features/finance/modules/account/domain/entities/account.dart'; import 'package:keep_track/features/finance/modules/budget/domain/entities/budget.dart'; +import 'package:keep_track/features/finance/modules/transaction/domain/entities/transaction.dart'; import 'package:keep_track/features/finance/presentation/state/account_controller.dart'; import 'package:keep_track/features/finance/presentation/state/budget_controller.dart'; +import 'package:keep_track/features/finance/presentation/state/transaction_controller.dart'; import 'package:keep_track/features/home/widgets/admin_panel_widget.dart'; class HomeScreen extends ScopedScreen { @@ -25,11 +27,14 @@ class _HomeScreenState extends ScopedScreenState with AppLayoutControlled { late final AccountController _accountController; late final BudgetController _budgetController; + late final TransactionController _transactionController; + int _daysToShow = 15; // Adjustable days for bar graph @override void registerServices() { _accountController = locator.get(); _budgetController = locator.get(); + _transactionController = locator.get(); } @override @@ -37,6 +42,13 @@ class _HomeScreenState extends ScopedScreenState configureLayout(title: 'Home', showBottomNav: true); _accountController.loadAccounts(); _budgetController.loadBudgets(); + _loadTransactionsForGraph(); + } + + void _loadTransactionsForGraph() { + final endDate = DateTime.now(); + final startDate = endDate.subtract(Duration(days: _daysToShow)); + _transactionController.loadTransactionsByDateRange(startDate, endDate); } String _getGreeting() { @@ -74,6 +86,10 @@ class _HomeScreenState extends ScopedScreenState _buildWelcomeSection(isDesktop), SizedBox(height: isDesktop ? AppSpacing.xl : AppSpacing.lg), + // Income/Expense Bar Graph + _buildIncomeExpenseGraph(isDesktop), + SizedBox(height: isDesktop ? AppSpacing.xl : AppSpacing.lg), + // Desktop: Two-column layout, Mobile: Single column if (isDesktop) Row( @@ -130,6 +146,376 @@ class _HomeScreenState extends ScopedScreenState return Icons.nightlight_round; } + Widget _buildIncomeExpenseGraph(bool isDesktop) { + return AsyncStreamBuilder>( + state: _transactionController, + loadingBuilder: (_) => Card( + elevation: 0, + margin: EdgeInsets.zero, + child: Container( + height: isDesktop ? 350 : 300, + alignment: Alignment.center, + child: const CircularProgressIndicator(), + ), + ), + errorBuilder: (context, message) => Card( + elevation: 0, + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(20), + child: Text('Error loading transactions: $message'), + ), + ), + builder: (context, transactions) { + // Process real transaction data into daily aggregates + final List data = _processTransactionsToDaily(transactions); + final maxAmount = data.fold( + 0, + (max, d) { + final dayMax = [d.income, d.expense, d.transfer].reduce((a, b) => a > b ? a : b); + return dayMax > max ? dayMax : max; + }, + ); + + return Card( + elevation: 0, + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header with title and days selector + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Daily Income & Expense', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + // Days selector + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: DropdownButton( + value: _daysToShow, + isDense: true, + underline: const SizedBox(), + items: [7, 15, 30].map((days) { + return DropdownMenuItem( + value: days, + child: Text( + '$days days', + style: const TextStyle(fontSize: 12), + ), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + setState(() { + _daysToShow = value; + _loadTransactionsForGraph(); + }); + } + }, + ), + ), + ], + ), + const SizedBox(height: 8), + // Legend + Row( + children: [ + _buildLegendIndicator(Colors.green, 'Income'), + const SizedBox(width: 16), + _buildLegendIndicator(Colors.red, 'Expense'), + const SizedBox(width: 16), + _buildLegendIndicator(Colors.blue, 'Transfer'), + ], + ), + const SizedBox(height: 16), + // Bar Graph + SizedBox( + height: isDesktop ? 200 : 160, + child: data.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.bar_chart, + size: 48, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.3), + ), + const SizedBox(height: 8), + Text( + 'No transactions in this period', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + ) + : LayoutBuilder( + builder: (context, constraints) { + final barWidth = (constraints.maxWidth - (data.length - 1) * 6) / (data.length * 3); + final clampedBarWidth = barWidth.clamp(3.0, 16.0); + + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: data.map((dayData) { + final incomeHeight = maxAmount > 0 + ? (dayData.income / maxAmount) * (isDesktop ? 160 : 120) + : 0.0; + final expenseHeight = maxAmount > 0 + ? (dayData.expense / maxAmount) * (isDesktop ? 160 : 120) + : 0.0; + final transferHeight = maxAmount > 0 + ? (dayData.transfer / maxAmount) * (isDesktop ? 160 : 120) + : 0.0; + + return Tooltip( + message: '${DateFormat('MMM d').format(dayData.date)}\n' + 'Income: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.income)}\n' + 'Expense: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.expense)}\n' + 'Transfer: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.transfer)}', + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Income bar + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: clampedBarWidth, + height: incomeHeight, + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.8), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(3), + ), + ), + ), + const SizedBox(width: 1), + // Expense bar + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: clampedBarWidth, + height: expenseHeight, + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.8), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(3), + ), + ), + ), + const SizedBox(width: 1), + // Transfer bar + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: clampedBarWidth, + height: transferHeight, + decoration: BoxDecoration( + color: Colors.blue.withValues(alpha: 0.8), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(3), + ), + ), + ), + ], + ), + const SizedBox(height: 4), + // Date label + Text( + DateFormat('d').format(dayData.date), + style: TextStyle( + fontSize: 9, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + ); + }).toList(), + ); + }, + ), + ), + const SizedBox(height: 12), + // Summary row + Row( + children: [ + Expanded( + child: _buildSummaryCard( + 'Income', + data.fold(0, (sum, d) => sum + d.income), + Colors.green, + ), + ), + const SizedBox(width: 8), + Expanded( + child: _buildSummaryCard( + 'Expense', + data.fold(0, (sum, d) => sum + d.expense), + Colors.red, + ), + ), + const SizedBox(width: 8), + Expanded( + child: _buildSummaryCard( + 'Transfer', + data.fold(0, (sum, d) => sum + d.transfer), + Colors.blue, + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + } + + /// Process transactions into daily income/expense/transfer aggregates + List _processTransactionsToDaily(List transactions) { + final Map dailyMap = {}; + + // Initialize all days in the range with zero values + for (int i = _daysToShow - 1; i >= 0; i--) { + final date = DateTime.now().subtract(Duration(days: i)); + final dateKey = DateFormat('yyyy-MM-dd').format(date); + dailyMap[dateKey] = DailyFinanceData( + date: DateTime(date.year, date.month, date.day), + income: 0, + expense: 0, + transfer: 0, + ); + } + + // Aggregate transactions by day + for (final transaction in transactions) { + final dateKey = DateFormat('yyyy-MM-dd').format(transaction.date); + if (dailyMap.containsKey(dateKey)) { + final existing = dailyMap[dateKey]!; + switch (transaction.type) { + case TransactionType.income: + dailyMap[dateKey] = DailyFinanceData( + date: existing.date, + income: existing.income + transaction.amount, + expense: existing.expense, + transfer: existing.transfer, + ); + break; + case TransactionType.expense: + dailyMap[dateKey] = DailyFinanceData( + date: existing.date, + income: existing.income, + expense: existing.expense + transaction.totalCost, + transfer: existing.transfer, + ); + break; + case TransactionType.transfer: + dailyMap[dateKey] = DailyFinanceData( + date: existing.date, + income: existing.income, + expense: existing.expense, + transfer: existing.transfer + transaction.amount, + ); + break; + } + } + } + + // Convert to sorted list + final result = dailyMap.values.toList(); + result.sort((a, b) => a.date.compareTo(b.date)); + return result; + } + + Widget _buildLegendIndicator(Color color, String label) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.8), + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + ], + ); + } + + Widget _buildSummaryCard(String title, double amount, Color color) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: color.withValues(alpha: 0.2)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 11, + color: color.withValues(alpha: 0.8), + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + '${currencyFormatter.currencySymbol}${NumberFormat('#,##0.00').format(amount)}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), + ); + } + + List _generateDummyFinanceData() { + final random = DateTime.now().millisecondsSinceEpoch; + final List data = []; + + for (int i = _daysToShow - 1; i >= 0; i--) { + final date = DateTime.now().subtract(Duration(days: i)); + // Generate somewhat realistic dummy data + final baseIncome = (random + i * 1000) % 500 + 100; + final baseExpense = (random + i * 500) % 400 + 50; + data.add(DailyFinanceData( + date: date, + income: baseIncome.toDouble() * (1 + (i % 3) * 0.3), + expense: baseExpense.toDouble() * (1 + (i % 4) * 0.25), + )); + } + + return data; + } + Widget _buildFinanceSnapshot(bool isDesktop) { return AsyncStreamBuilder>( state: _accountController, @@ -705,3 +1091,18 @@ class _HomeScreenState extends ScopedScreenState ); } } + +// Data class for daily finance data +class DailyFinanceData { + final DateTime date; + final double income; + final double expense; + final double transfer; + + DailyFinanceData({ + required this.date, + required this.income, + required this.expense, + this.transfer = 0, + }); +} diff --git a/lib/features/home/task_home_screen.dart b/lib/features/home/task_home_screen.dart index 7881159..db69966 100644 --- a/lib/features/home/task_home_screen.dart +++ b/lib/features/home/task_home_screen.dart @@ -18,9 +18,11 @@ import '../module_selection/task_module_screen.dart'; import 'package:keep_track/core/theme/app_theme.dart'; import 'package:keep_track/core/ui/responsive/desktop_aware_screen.dart'; +import '../tasks/modules/buckets/domain/entities/bucket.dart'; import '../tasks/presentation/screens/tabs/task/create_task_page.dart'; +import '../tasks/presentation/state/bucket_controller.dart'; -enum TaskTimeFilter { current, week, month, noDate } +enum TaskTimeFilter { today, sevenDays, thirtyDays, all } enum TaskSortOption { priority, dueDate, status } @@ -36,16 +38,19 @@ class _TaskHomeScreenState extends ScopedScreenState with AppLayoutControlled { late final TaskController _taskController; late final ProjectController _projectController; + late final BucketController _bucketController; late final SupabaseService _supabaseService; - TaskTimeFilter _timeFilter = TaskTimeFilter.current; + TaskTimeFilter _timeFilter = TaskTimeFilter.today; TaskSortOption _sortOption = TaskSortOption.priority; - @override void registerServices() { _taskController = locator.get(); _projectController = locator.get(); _supabaseService = locator.get(); + _bucketController = locator.get(); + + _bucketController.loadBuckets(); } @override @@ -80,98 +85,106 @@ class _TaskHomeScreenState extends ScopedScreenState .where((p) => p.status == ProjectStatus.active && !p.isArchived) .toList(); - return Dialog( - child: Container( - width: 600, - constraints: const BoxConstraints(maxHeight: 700), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.grey[300]!), - ), - ), - child: Row( - children: [ - const Expanded( - child: Text( - 'Create Task', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), + return AsyncStreamBuilder>( + state: _bucketController, + builder: (context, buckets) { + return Dialog( + child: Container( + width: 600, + constraints: const BoxConstraints(maxHeight: 700), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey[300]!), ), ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), + child: Row( + children: [ + const Expanded( + child: Text( + 'Create Task', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], ), - ], - ), - ), + ), - // Content - Expanded( - child: TaskManagementDialog( - userId: _supabaseService.userId!, - projects: activeProjects, - useDialogContent: true, - onSave: (newTask) async { - try { - await _taskController.createTask(newTask); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Task created successfully'), - backgroundColor: Colors.green, - ), - ); - Navigator.pop(context); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: ${e.toString()}'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - ), - ), + // Content + Expanded( + child: TaskManagementDialog( + userId: _supabaseService.userId!, + projects: activeProjects, + buckets: buckets, + useDialogContent: true, + onSave: (newTask) async { + try { + await _taskController.createTask(newTask); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Task created successfully'), + backgroundColor: Colors.green, + ), + ); + Navigator.pop(context); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + ), + ), - // Footer with "View Full Page" button - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border(top: BorderSide(color: Colors.grey[300]!)), - ), - child: SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - icon: const Icon(Icons.open_in_full), - label: const Text('View Full Page'), - onPressed: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const CreateTaskPage(), - ), - ); - }, + // Footer with "View Full Page" button + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: Colors.grey[300]!), + ), + ), + child: SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + icon: const Icon(Icons.open_in_full), + label: const Text('View Full Page'), + onPressed: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CreateTaskPage(), + ), + ); + }, + ), + ), ), - ), + ], ), - ], - ), - ), + ), + ); + }, ); }, ), @@ -198,7 +211,11 @@ class _TaskHomeScreenState extends ScopedScreenState return DesktopAwareScreen( builder: (context, isDesktop) { return Scaffold( - backgroundColor: isDesktop ? (Theme.of(context).brightness == Brightness.dark ? const Color(0xFF09090B) : AppColors.backgroundSecondary) : null, + backgroundColor: isDesktop + ? (Theme.of(context).brightness == Brightness.dark + ? const Color(0xFF09090B) + : AppColors.backgroundSecondary) + : null, body: SingleChildScrollView( padding: EdgeInsets.all(isDesktop ? AppSpacing.xl : 16), child: Center( @@ -627,30 +644,37 @@ class _TaskHomeScreenState extends ScopedScreenState List _filterTasks(List tasks) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); + final todayEnd = today.add(const Duration(days: 1)); switch (_timeFilter) { - case TaskTimeFilter.current: - return tasks - .where((t) => t.status == TaskStatus.inProgress && !t.isArchived) - .toList(); - case TaskTimeFilter.week: - final weekEnd = today.add(const Duration(days: 7)); + case TaskTimeFilter.today: return tasks.where((t) { - if (t.isArchived || t.isCompleted) return false; + if (t.isArchived) return false; if (t.dueDate == null) return false; - return t.dueDate!.isAfter(today) && t.dueDate!.isBefore(weekEnd); + final taskDate = DateTime(t.dueDate!.year, t.dueDate!.month, t.dueDate!.day); + return taskDate.isAtSameMomentAs(today) || + (t.dueDate!.isAfter(today) && t.dueDate!.isBefore(todayEnd)); }).toList(); - case TaskTimeFilter.month: - final monthEnd = today.add(const Duration(days: 30)); + case TaskTimeFilter.sevenDays: + final endDate = today.add(const Duration(days: 7)); return tasks.where((t) { - if (t.isArchived || t.isCompleted) return false; + if (t.isArchived) return false; if (t.dueDate == null) return false; - return t.dueDate!.isAfter(today) && t.dueDate!.isBefore(monthEnd); + final taskDate = DateTime(t.dueDate!.year, t.dueDate!.month, t.dueDate!.day); + return (taskDate.isAtSameMomentAs(today) || taskDate.isAfter(today)) && + taskDate.isBefore(endDate); }).toList(); - case TaskTimeFilter.noDate: - return tasks - .where((t) => t.dueDate == null && !t.isArchived && !t.isCompleted) - .toList(); + case TaskTimeFilter.thirtyDays: + final endDate = today.add(const Duration(days: 30)); + return tasks.where((t) { + if (t.isArchived) return false; + if (t.dueDate == null) return false; + final taskDate = DateTime(t.dueDate!.year, t.dueDate!.month, t.dueDate!.day); + return (taskDate.isAtSameMomentAs(today) || taskDate.isAfter(today)) && + taskDate.isBefore(endDate); + }).toList(); + case TaskTimeFilter.all: + return tasks.where((t) => !t.isArchived).toList(); } } @@ -728,20 +752,20 @@ class _TaskHomeScreenState extends ScopedScreenState underline: const SizedBox(), items: const [ DropdownMenuItem( - value: TaskTimeFilter.current, - child: Text('Current'), + value: TaskTimeFilter.today, + child: Text('Today'), ), DropdownMenuItem( - value: TaskTimeFilter.week, - child: Text('This Week'), + value: TaskTimeFilter.sevenDays, + child: Text('7 Days'), ), DropdownMenuItem( - value: TaskTimeFilter.month, - child: Text('This Month'), + value: TaskTimeFilter.thirtyDays, + child: Text('30 Days'), ), DropdownMenuItem( - value: TaskTimeFilter.noDate, - child: Text('No Date'), + value: TaskTimeFilter.all, + child: Text('All Tasks'), ), ], onChanged: (value) { @@ -853,40 +877,23 @@ class _TaskHomeScreenState extends ScopedScreenState } Widget _buildTaskItem(Task task, List allTasks) { - Color priorityColor; - switch (task.priority) { - case TaskPriority.urgent: - priorityColor = Colors.red[700]!; - break; - case TaskPriority.high: - priorityColor = Colors.orange[700]!; - break; - case TaskPriority.medium: - priorityColor = Colors.blue[700]!; - break; - case TaskPriority.low: - priorityColor = Colors.grey[700]!; - break; - } - final isOverdue = task.dueDate != null && task.dueDate!.isBefore(DateTime.now()) && !task.isCompleted; + final subtasks = allTasks.where((t) => t.parentTaskId == task.id).toList(); + final subtaskCount = subtasks.length; return Container( - margin: const EdgeInsets.only(bottom: 12), + margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( color: isOverdue ? Colors.red.withOpacity(0.05) : Theme.of(context).colorScheme.surface.withOpacity(0.3), borderRadius: BorderRadius.circular(8), - border: Border.all( - color: isOverdue - ? Colors.red.withOpacity(0.5) - : Theme.of(context).colorScheme.outline.withOpacity(0.2), - width: isOverdue ? 1.5 : 1, - ), + border: isOverdue + ? Border.all(color: Colors.red.withOpacity(0.5), width: 1.5) + : null, ), child: InkWell( onTap: () { @@ -907,7 +914,18 @@ class _TaskHomeScreenState extends ScopedScreenState padding: const EdgeInsets.all(12.0), child: Row( children: [ - // Checkbox to mark task as complete + // Priority indicator + Container( + width: 4, + height: 50, + decoration: BoxDecoration( + color: _getPriorityColor(task.priority), + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 12), + + // Checkbox Checkbox( value: task.isCompleted, onChanged: (value) async { @@ -915,71 +933,29 @@ class _TaskHomeScreenState extends ScopedScreenState final updatedTask = task.copyWith( status: value ? TaskStatus.completed - : TaskStatus.inProgress, + : TaskStatus.todo, completedAt: value ? DateTime.now() : null, ); await _taskController.updateTask(updatedTask); } }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), ), const SizedBox(width: 8), - Container( - width: 4, - height: 50, - decoration: BoxDecoration( - color: priorityColor, - borderRadius: BorderRadius.circular(2), - ), - ), - const SizedBox(width: 12), + + // Task info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: Text( - task.title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - decoration: task.isCompleted - ? TextDecoration.lineThrough - : null, - color: task.isCompleted - ? Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.5) - : null, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - if (isOverdue) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(4), - ), - child: const Text( - 'OVERDUE', - style: TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ), - ], + Text( + task.title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + decoration: task.isCompleted + ? TextDecoration.lineThrough + : null, + ), ), if (task.description != null && task.description!.isNotEmpty) ...[ @@ -987,47 +963,87 @@ class _TaskHomeScreenState extends ScopedScreenState Text( task.description!, style: TextStyle( - fontSize: 14, - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.6), - decoration: task.isCompleted - ? TextDecoration.lineThrough - : null, + fontSize: 12, + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.6), ), - maxLines: 1, + maxLines: 2, overflow: TextOverflow.ellipsis, ), ], - if (task.dueDate != null) ...[ - const SizedBox(height: 4), - Row( - children: [ - Icon( - Icons.calendar_today, - size: 12, - color: isOverdue - ? Colors.red - : Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.6), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 4, + children: [ + if (isOverdue) + _buildTaskBadge( + 'OVERDUE', + Colors.red, + Icons.warning_amber_rounded, ), - const SizedBox(width: 4), - Text( - 'Due: ${_formatDate(task.dueDate!)}', - style: TextStyle( - fontSize: 12, - color: isOverdue - ? Colors.red - : Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.6), - fontWeight: isOverdue ? FontWeight.w600 : null, - ), + _buildTaskBadge( + task.priority.displayName, + _getPriorityColor(task.priority), + Icons.flag, + ), + _buildTaskBadge( + task.status.displayName, + _getStatusColor(task.status), + Icons.circle, + ), + _buildTaskBadge( + task.dueDate != null + ? DateFormat('MMM d, h:mm a').format(task.dueDate!) + : 'No date', + task.dueDate != null + ? (isOverdue ? Colors.red : Colors.grey[700]!) + : Colors.grey[400]!, + Icons.calendar_today, + ), + if (subtaskCount > 0) + _buildTaskBadge( + '$subtaskCount subtask${subtaskCount > 1 ? 's' : ''}', + Colors.blue[700]!, + Icons.list, ), - ], - ), - ], + // Project badge + if (task.projectId != null) + Builder( + builder: (context) { + final project = _projectController.currentProjects + ?.where((p) => p.id == task.projectId) + .firstOrNull; + if (project == null) return const SizedBox.shrink(); + final projectColor = project.color != null + ? Color(int.parse( + project.color!.replaceFirst('#', '0xff'))) + : Colors.blue[700]!; + return _buildTaskBadge( + project.name, + projectColor, + Icons.folder, + ); + }, + ), + // Bucket badge + if (task.bucketId != null) + Builder( + builder: (context) { + final bucket = _bucketController + .getBucketFromCurrentState(task.bucketId!); + if (bucket == null) return const SizedBox.shrink(); + return _buildTaskBadge( + bucket.name, + Colors.purple[700]!, + Icons.inbox, + ); + }, + ), + ], + ), ], ), ), @@ -1038,6 +1054,58 @@ class _TaskHomeScreenState extends ScopedScreenState ); } + Widget _buildTaskBadge(String label, Color color, IconData icon) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 12, color: color), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: color, + ), + ), + ], + ), + ); + } + + Color _getPriorityColor(TaskPriority priority) { + switch (priority) { + case TaskPriority.urgent: + return Colors.red[700]!; + case TaskPriority.high: + return Colors.orange[700]!; + case TaskPriority.medium: + return Colors.blue[700]!; + case TaskPriority.low: + return Colors.grey[600]!; + } + } + + Color _getStatusColor(TaskStatus status) { + switch (status) { + case TaskStatus.todo: + return Colors.orange[700]!; + case TaskStatus.inProgress: + return Colors.purple[700]!; + case TaskStatus.completed: + return Colors.green[700]!; + case TaskStatus.cancelled: + return Colors.red[700]!; + } + } + void _showTaskDrawer(Task task, List allTasks) { showGeneralDialog( context: context, @@ -1385,12 +1453,13 @@ class _TaskHomeScreenState extends ScopedScreenState 'Active Projects', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), - Text( - '${activeProjects.length} project${activeProjects.length != 1 ? 's' : ''}', - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.6), + TextButton.icon( + onPressed: () { + Navigator.pushNamed(context, AppRoutes.projectManagement); + }, + icon: const Icon(Icons.arrow_forward, size: 16), + label: Text( + '${activeProjects.length} project${activeProjects.length != 1 ? 's' : ''}', ), ), ], @@ -1427,16 +1496,23 @@ class _TaskHomeScreenState extends ScopedScreenState ), ) else - SizedBox( - height: 140, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: activeProjects.length, - itemBuilder: (context, index) { - final project = activeProjects[index]; - return _buildProjectCard(project); - }, + // 3-Grid Layout for Projects + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.85, ), + itemCount: activeProjects.length > 6 + ? 6 + : activeProjects.length, + itemBuilder: (context, index) { + final project = activeProjects[index]; + return _buildProjectCard(project); + }, ), ], ); @@ -1444,70 +1520,131 @@ class _TaskHomeScreenState extends ScopedScreenState ); } - Widget _buildProjectCard(project) { + Widget _buildProjectCard(Project project) { final projectColor = project.color != null ? Color(int.parse(project.color!.replaceFirst('#', '0xff'))) : Colors.blue[700]!; - return Container( - width: 160, - margin: const EdgeInsets.only(right: 12), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [projectColor, projectColor.withOpacity(0.7)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), + // Get bucket from project metadata or default + final bucket = project.metadata['bucket'] ?? 'Work'; + + return Card( + elevation: 0, + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), + side: BorderSide(color: projectColor.withOpacity(0.3)), ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () { - Navigator.pushNamed( - context, - AppRoutes.projectDetail, - arguments: project, - ); - }, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.folder, color: Colors.white, size: 32), - const SizedBox(height: 12), - Text( - project.name, + child: InkWell( + onTap: () { + Navigator.pushNamed( + context, + AppRoutes.projectDetail, + arguments: project, + ); + }, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Project Icon & Color + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: projectColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.folder, color: projectColor, size: 20), + ), + const SizedBox(height: 8), + // Project Title + Text( + project.name, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 13, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + // Bucket Tab + _buildBucketChip(bucket), + const SizedBox(height: 8), + // Status indicator + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + project.status.displayName, style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, + fontSize: 9, + color: Colors.green, + fontWeight: FontWeight.w600, ), - maxLines: 2, - overflow: TextOverflow.ellipsis, ), - const Spacer(), - if (project.description != null && - project.description!.isNotEmpty) - Text( - project.description!, - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 12, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), + ), + ], ), ), ), ); } + Widget _buildBucketChip(String bucket) { + Color bucketColor; + IconData bucketIcon; + + switch (bucket.toLowerCase()) { + case 'work': + bucketColor = Colors.blue; + bucketIcon = Icons.work; + break; + case 'personal': + bucketColor = Colors.green; + bucketIcon = Icons.person; + break; + case 'urgent': + bucketColor = Colors.red; + bucketIcon = Icons.priority_high; + break; + case 'learning': + bucketColor = Colors.purple; + bucketIcon = Icons.school; + break; + default: + bucketColor = Colors.grey; + bucketIcon = Icons.folder; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: bucketColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(bucketIcon, size: 10, color: bucketColor), + const SizedBox(width: 4), + Text( + bucket, + style: TextStyle( + fontSize: 9, + color: bucketColor, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + String _formatDate(DateTime date) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); diff --git a/lib/features/settings/subpages/app_configuration_task_page.dart b/lib/features/settings/subpages/app_configuration_task_page.dart index e206ed3..f9a4832 100644 --- a/lib/features/settings/subpages/app_configuration_task_page.dart +++ b/lib/features/settings/subpages/app_configuration_task_page.dart @@ -32,6 +32,33 @@ class AppConfigurationTaskPage extends StatelessWidget { onTap: () => Navigator.pushNamed(context, '/project-create'), ), ]), + const SizedBox(height: 24), + _buildSection(context, 'Management', [ + _buildTile( + context, + icon: Icons.folder, + title: 'Manage Projects', + subtitle: 'View, edit, and organize projects', + color: Colors.purple, + onTap: () => Navigator.pushNamed(context, '/project-management'), + ), + _buildTile( + context, + icon: Icons.inbox, + title: 'Manage Buckets', + subtitle: 'Create and organize task buckets', + color: Colors.deepPurple, + onTap: () => Navigator.pushNamed(context, '/bucket-management'), + ), + _buildTile( + context, + icon: Icons.task_alt, + title: 'Manage Tasks', + subtitle: 'View and manage all tasks', + color: Colors.blue, + onTap: () => Navigator.pushNamed(context, '/task-management'), + ), + ]), ], ), ); diff --git a/lib/features/tasks/modules/tasks/data/models/task_model.dart b/lib/features/tasks/modules/tasks/data/models/task_model.dart index df5f5fc..99e9e5e 100644 --- a/lib/features/tasks/modules/tasks/data/models/task_model.dart +++ b/lib/features/tasks/modules/tasks/data/models/task_model.dart @@ -116,6 +116,7 @@ class TaskModel extends Task { if (actualTransactionId != null) 'actual_transaction_id': actualTransactionId, if (userId != null) 'user_id': userId, + if (bucketId != null) 'bucket_id': bucketId, }; } } diff --git a/lib/features/tasks/modules/tasks/domain/entities/task.dart b/lib/features/tasks/modules/tasks/domain/entities/task.dart index 877984e..b1824aa 100644 --- a/lib/features/tasks/modules/tasks/domain/entities/task.dart +++ b/lib/features/tasks/modules/tasks/domain/entities/task.dart @@ -1,3 +1,6 @@ +// Sentinel value for clearing nullable fields in copyWith +const _sentinel = Object(); + /// Task entity - Pure domain model class Task { final String? id; // Optional - Supabase auto-generates @@ -49,49 +52,74 @@ class Task { }); /// Copy with method for immutability + /// Use Object? for nullable fields to allow explicitly setting them to null Task copyWith({ String? id, String? title, - String? description, + Object? description = _sentinel, TaskStatus? status, TaskPriority? priority, - String? projectId, - String? parentTaskId, + Object? projectId = _sentinel, + Object? parentTaskId = _sentinel, List? tags, - DateTime? createdAt, - DateTime? updatedAt, - DateTime? dueDate, - DateTime? completedAt, + Object? createdAt = _sentinel, + Object? updatedAt = _sentinel, + Object? dueDate = _sentinel, + Object? completedAt = _sentinel, bool? archived, bool? isMoneyRelated, - double? expectedAmount, - TaskTransactionType? transactionType, - String? financeCategoryId, - String? actualTransactionId, + Object? expectedAmount = _sentinel, + Object? transactionType = _sentinel, + Object? financeCategoryId = _sentinel, + Object? actualTransactionId = _sentinel, String? userId, - String? bucketId, + Object? bucketId = _sentinel, }) { return Task( id: id ?? this.id, title: title ?? this.title, - description: description ?? this.description, + description: description == _sentinel + ? this.description + : description as String?, status: status ?? this.status, priority: priority ?? this.priority, - projectId: projectId ?? this.projectId, - parentTaskId: parentTaskId ?? this.parentTaskId, + projectId: projectId == _sentinel + ? this.projectId + : projectId as String?, + parentTaskId: parentTaskId == _sentinel + ? this.parentTaskId + : parentTaskId as String?, tags: tags ?? this.tags, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - dueDate: dueDate ?? this.dueDate, - completedAt: completedAt ?? this.completedAt, + createdAt: createdAt == _sentinel + ? this.createdAt + : createdAt as DateTime?, + updatedAt: updatedAt == _sentinel + ? this.updatedAt + : updatedAt as DateTime?, + dueDate: dueDate == _sentinel + ? this.dueDate + : dueDate as DateTime?, + completedAt: completedAt == _sentinel + ? this.completedAt + : completedAt as DateTime?, archived: archived ?? this.archived, isMoneyRelated: isMoneyRelated ?? this.isMoneyRelated, - expectedAmount: expectedAmount ?? this.expectedAmount, - transactionType: transactionType ?? this.transactionType, - financeCategoryId: financeCategoryId ?? this.financeCategoryId, - actualTransactionId: actualTransactionId ?? this.actualTransactionId, + expectedAmount: expectedAmount == _sentinel + ? this.expectedAmount + : expectedAmount as double?, + transactionType: transactionType == _sentinel + ? this.transactionType + : transactionType as TaskTransactionType?, + financeCategoryId: financeCategoryId == _sentinel + ? this.financeCategoryId + : financeCategoryId as String?, + actualTransactionId: actualTransactionId == _sentinel + ? this.actualTransactionId + : actualTransactionId as String?, userId: userId ?? this.userId, - bucketId: bucketId ?? this.bucketId, + bucketId: bucketId == _sentinel + ? this.bucketId + : bucketId as String?, ); } diff --git a/lib/features/tasks/presentation/screens/configuration/bucket_management_screen.dart b/lib/features/tasks/presentation/screens/configuration/bucket_management_screen.dart new file mode 100644 index 0000000..befa57e --- /dev/null +++ b/lib/features/tasks/presentation/screens/configuration/bucket_management_screen.dart @@ -0,0 +1,544 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:keep_track/core/di/service_locator.dart'; +import 'package:keep_track/core/state/stream_builder_widget.dart'; +import 'package:keep_track/core/ui/app_layout_controller.dart'; +import 'package:keep_track/core/ui/ui.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/entities/bucket.dart'; +import 'package:keep_track/features/tasks/modules/tasks/domain/entities/task.dart'; +import 'package:keep_track/shared/infrastructure/supabase/supabase_service.dart'; + +import '../../state/bucket_controller.dart'; +import '../../state/task_controller.dart'; + +class BucketManagementScreen extends ScopedScreen { + const BucketManagementScreen({super.key}); + + @override + State createState() => _BucketManagementScreenState(); +} + +class _BucketManagementScreenState extends ScopedScreenState + with AppLayoutControlled { + late final BucketController _controller; + late final TaskController _taskController; + late final SupabaseService _supabaseService; + + @override + void registerServices() { + _controller = locator.get(); + _taskController = locator.get(); + _supabaseService = locator.get(); + } + + @override + void onReady() { + configureLayout(title: 'Manage Buckets', showBottomNav: false); + } + + void _showBucketDialog({Bucket? bucket, List? allTasks}) { + showDialog( + context: context, + builder: (context) => _BucketManagementDialog( + bucket: bucket, + userId: _supabaseService.userId!, + onSave: (updatedBucket) async { + try { + if (bucket != null) { + await _controller.updateBucket(updatedBucket); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Bucket updated successfully'), + backgroundColor: Colors.green, + ), + ); + } + } else { + await _controller.createBucket(updatedBucket); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Bucket created successfully'), + backgroundColor: Colors.green, + ), + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + onDelete: bucket != null + ? () async { + try { + // Check if bucket has tasks + if (allTasks != null) { + final tasksInBucket = + allTasks.where((t) => t.bucketId == bucket.id).toList(); + if (tasksInBucket.isNotEmpty) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Cannot delete bucket: ${tasksInBucket.length} task(s) associated. Remove tasks from bucket first.', + ), + backgroundColor: Colors.orange, + duration: const Duration(seconds: 4), + ), + ); + } + return; + } + } + + await _controller.deleteBucket(bucket.id!); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Bucket deleted successfully'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error deleting bucket: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + } + : null, + onArchive: bucket != null && !bucket.isArchive + ? () async { + try { + await _controller.archiveBucket(bucket.id!); + if (mounted) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Bucket archived successfully'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error archiving bucket: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + } + : null, + onUnarchive: bucket != null && bucket.isArchive + ? () async { + try { + await _controller.unarchiveBucket(bucket.id!); + if (mounted) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Bucket unarchived successfully'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error unarchiving bucket: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + } + : null, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Buckets'), + actions: [ + AsyncStreamBuilder>( + state: _taskController, + builder: (context, tasks) { + return IconButton( + onPressed: () => _showBucketDialog(allTasks: tasks), + icon: const Icon(Icons.add), + ); + }, + loadingBuilder: (_) => IconButton( + onPressed: () => _showBucketDialog(), + icon: const Icon(Icons.add), + ), + errorBuilder: (_, __) => IconButton( + onPressed: () => _showBucketDialog(), + icon: const Icon(Icons.add), + ), + ), + ], + ), + body: AsyncStreamBuilder>( + state: _taskController, + builder: (context, tasks) { + return AsyncStreamBuilder>( + state: _controller, + builder: (context, buckets) { + final activeBuckets = buckets.where((b) => !b.isArchive).length; + + return Column( + children: [ + // Stats card + Card( + margin: const EdgeInsets.all(16), + child: ListTile( + title: const Text('Total Buckets'), + subtitle: Text('$activeBuckets active'), + trailing: Text( + '${buckets.length}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ), + ), + + // Buckets list or empty state + Expanded( + child: buckets.isEmpty + ? const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inbox_outlined, + size: 64, + color: Colors.grey, + ), + SizedBox(height: 16), + Text('No buckets found.'), + SizedBox(height: 8), + Text( + 'Tap + to create your first bucket', + style: TextStyle(color: Colors.grey), + ), + ], + ), + ) + : ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: buckets.length, + itemBuilder: (context, index) { + final bucket = buckets[index]; + final taskCount = tasks + .where((t) => t.bucketId == bucket.id) + .length; + + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.purple.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.inbox, + color: bucket.isArchive + ? Colors.grey + : Colors.purple[700], + ), + ), + title: Text( + bucket.name, + style: TextStyle( + decoration: bucket.isArchive + ? TextDecoration.lineThrough + : null, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('$taskCount task(s)'), + if (bucket.createdAt != null) + Text( + 'Created: ${DateFormat('MMM d, yyyy').format(bucket.createdAt!)}', + style: TextStyle( + fontSize: 12, + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.6), + ), + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (bucket.isArchive) + const Icon( + Icons.archive, + color: Colors.orange, + ), + ], + ), + onTap: () => _showBucketDialog( + bucket: bucket, + allTasks: tasks, + ), + ), + ); + }, + ), + ), + ], + ); + }, + loadingBuilder: (_) => + const Center(child: CircularProgressIndicator()), + errorBuilder: (context, message) => Center(child: Text(message)), + ); + }, + loadingBuilder: (_) => const Center(child: CircularProgressIndicator()), + errorBuilder: (context, message) => Center(child: Text(message)), + ), + ); + } +} + +/// Bucket Management Dialog +class _BucketManagementDialog extends StatefulWidget { + final Bucket? bucket; + final String userId; + final Future Function(Bucket) onSave; + final Future Function()? onDelete; + final Future Function()? onArchive; + final Future Function()? onUnarchive; + + const _BucketManagementDialog({ + this.bucket, + required this.userId, + required this.onSave, + this.onDelete, + this.onArchive, + this.onUnarchive, + }); + + @override + State<_BucketManagementDialog> createState() => + _BucketManagementDialogState(); +} + +class _BucketManagementDialogState extends State<_BucketManagementDialog> { + final _formKey = GlobalKey(); + late final TextEditingController _nameController; + bool _isSaving = false; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.bucket?.name ?? ''); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + + setState(() { + _isSaving = true; + }); + + try { + final bucket = Bucket( + id: widget.bucket?.id, + name: _nameController.text.trim(), + isArchive: widget.bucket?.isArchive ?? false, + userId: widget.userId, + createdAt: widget.bucket?.createdAt, + updatedAt: widget.bucket?.updatedAt, + ); + + await widget.onSave(bucket); + if (mounted) Navigator.pop(context); + } catch (e) { + if (mounted) { + setState(() { + _isSaving = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + Future _handleDelete() async { + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Bucket'), + content: const Text( + 'Are you sure you want to delete this bucket? This action cannot be undone.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Delete', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + + if (confirm == true && mounted) { + await widget.onDelete?.call(); + if (mounted) Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + final isEdit = widget.bucket != null; + + return Dialog( + child: Container( + width: 500, + constraints: const BoxConstraints(maxHeight: 400), + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isEdit ? 'Edit Bucket' : 'Create Bucket', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], + ), + const SizedBox(height: 24), + + // Form + Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Bucket Name', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.inbox), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter a bucket name'; + } + return null; + }, + autofocus: true, + ), + ], + ), + ), + const SizedBox(height: 24), + + // Action buttons + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (isEdit && widget.onDelete != null) ...[ + TextButton( + onPressed: _isSaving ? null : _handleDelete, + child: const Text( + 'Delete', + style: TextStyle(color: Colors.red), + ), + ), + const Spacer(), + ], + if (widget.onArchive != null) + TextButton( + onPressed: _isSaving ? null : widget.onArchive, + child: const Text('Archive'), + ), + if (widget.onUnarchive != null) + TextButton( + onPressed: _isSaving ? null : widget.onUnarchive, + child: const Text('Unarchive'), + ), + const SizedBox(width: 8), + TextButton( + onPressed: _isSaving ? null : () => Navigator.pop(context), + child: const Text('Cancel'), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _isSaving ? null : _save, + child: _isSaving + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text(isEdit ? 'Save' : 'Create'), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/tasks/presentation/screens/project_details_screen.dart b/lib/features/tasks/presentation/screens/project_details_screen.dart index 3124fcb..3e8b880 100644 --- a/lib/features/tasks/presentation/screens/project_details_screen.dart +++ b/lib/features/tasks/presentation/screens/project_details_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:keep_track/core/di/service_locator.dart'; import 'package:keep_track/core/routing/app_router.dart'; import 'package:keep_track/core/state/stream_builder_widget.dart'; @@ -11,6 +12,7 @@ import 'package:keep_track/features/tasks/modules/pomodoro/domain/entities/pomod import 'package:keep_track/features/tasks/presentation/state/project_controller.dart'; import 'package:keep_track/features/tasks/presentation/state/task_controller.dart'; import 'package:keep_track/features/tasks/presentation/state/pomodoro_session_controller.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:keep_track/core/theme/app_theme.dart'; import 'package:keep_track/core/ui/responsive/desktop_aware_screen.dart'; @@ -224,6 +226,13 @@ class _ProjectDetailsScreenState extends ScopedScreenState icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.pop(context), ), + actions: [ + IconButton( + icon: const Icon(Icons.edit), + tooltip: 'Edit Project', + onPressed: () => _showEditProjectDialog(currentProject), + ), + ], ), body: SingleChildScrollView( padding: EdgeInsets.all(isDesktop ? AppSpacing.xl : 16), @@ -589,9 +598,7 @@ class _ProjectDetailsScreenState extends ScopedScreenState if (_isUrl(entry.value)) IconButton( icon: const Icon(Icons.open_in_new, size: 18), - onPressed: () { - // TODO: Open URL in browser - }, + onPressed: () => _launchUrl(entry.value), ), ], ), @@ -873,6 +880,46 @@ class _ProjectDetailsScreenState extends ScopedScreenState return text.startsWith('http://') || text.startsWith('https://'); } + Future _launchUrl(String url) async { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Could not open $url'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + void _showEditProjectDialog(Project project) async { + final result = await showDialog( + context: context, + builder: (context) => _ProjectEditorDialog( + project: project, + onSave: (updatedProject) async { + await _projectController.updateProject(updatedProject); + return true; + }, + ), + ); + + if (result == true && mounted) { + await _projectController.loadProjects(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Project updated successfully'), + backgroundColor: Colors.green, + duration: Duration(seconds: 2), + ), + ); + } + } + void _showMetadataEditor(Project project) async { final result = await showDialog( context: context, @@ -1464,3 +1511,274 @@ class _MetadataEditorDialogState extends State<_MetadataEditorDialog> { } } } + +/// Project Editor Dialog - Allows editing project name, description, status, and color +class _ProjectEditorDialog extends StatefulWidget { + final Project project; + final Future Function(Project) onSave; + + const _ProjectEditorDialog({required this.project, required this.onSave}); + + @override + State<_ProjectEditorDialog> createState() => _ProjectEditorDialogState(); +} + +class _ProjectEditorDialogState extends State<_ProjectEditorDialog> { + late TextEditingController _nameController; + late TextEditingController _descriptionController; + late ProjectStatus _status; + late Color _selectedColor; + bool _isSaving = false; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.project.name); + _descriptionController = TextEditingController( + text: widget.project.description ?? '', + ); + _status = widget.project.status; + _selectedColor = widget.project.color != null + ? Color(int.parse(widget.project.color!.replaceFirst('#', '0xff'))) + : Colors.blue[700]!; + } + + @override + void dispose() { + _nameController.dispose(); + _descriptionController.dispose(); + super.dispose(); + } + + Future _saveProject() async { + if (_nameController.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Project name cannot be empty'), + backgroundColor: Colors.red, + ), + ); + return; + } + + setState(() { + _isSaving = true; + }); + + try { + final colorHex = + '#${_selectedColor.value.toRadixString(16).substring(2).toUpperCase()}'; + final updatedProject = widget.project.copyWith( + name: _nameController.text.trim(), + description: _descriptionController.text.trim().isEmpty + ? null + : _descriptionController.text.trim(), + status: _status, + color: colorHex, + ); + + final success = await widget.onSave(updatedProject); + if (mounted) { + Navigator.pop(context, success); + } + } catch (e) { + if (mounted) { + setState(() { + _isSaving = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error saving project: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + void _showColorPicker() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Pick a Color'), + content: SingleChildScrollView( + child: BlockPicker( + pickerColor: _selectedColor, + onColorChanged: (color) { + setState(() { + _selectedColor = color; + }); + Navigator.pop(context); + }, + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Dialog( + child: Container( + width: 500, + constraints: const BoxConstraints(maxHeight: 600), + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Edit Project', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], + ), + const SizedBox(height: 24), + + // Project Name + TextField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Project Name', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.folder), + ), + ), + const SizedBox(height: 16), + + // Description + TextField( + controller: _descriptionController, + maxLines: 3, + decoration: const InputDecoration( + labelText: 'Description (optional)', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.description), + alignLabelWithHint: true, + ), + ), + const SizedBox(height: 16), + + // Status + DropdownButtonFormField( + value: _status, + decoration: const InputDecoration( + labelText: 'Status', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.flag), + ), + items: ProjectStatus.values.map((status) { + return DropdownMenuItem( + value: status, + child: Row( + children: [ + Icon( + status == ProjectStatus.active + ? Icons.play_circle + : status == ProjectStatus.postponed + ? Icons.pause_circle + : Icons.check_circle, + color: status == ProjectStatus.active + ? Colors.green + : status == ProjectStatus.postponed + ? Colors.orange + : Colors.grey, + size: 20, + ), + const SizedBox(width: 8), + Text(status.displayName), + ], + ), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + setState(() { + _status = value; + }); + } + }, + ), + const SizedBox(height: 16), + + // Color Picker + InkWell( + onTap: _showColorPicker, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outline, + ), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + children: [ + Icon( + Icons.color_lens, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Project Color', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ), + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: _selectedColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 24), + + // Action buttons + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: _isSaving ? null : () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + const SizedBox(width: 12), + ElevatedButton( + onPressed: _isSaving ? null : _saveProject, + child: _isSaving + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), + ), + ) + : const Text('Save Changes'), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/tasks/presentation/screens/tabs/task/task_details_page.dart b/lib/features/tasks/presentation/screens/tabs/task/task_details_page.dart index 09174c9..659de7a 100644 --- a/lib/features/tasks/presentation/screens/tabs/task/task_details_page.dart +++ b/lib/features/tasks/presentation/screens/tabs/task/task_details_page.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:keep_track/core/di/service_locator.dart'; import 'package:keep_track/core/state/stream_builder_widget.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/entities/bucket.dart'; import 'package:keep_track/features/tasks/modules/projects/domain/entities/project.dart'; import 'package:keep_track/features/tasks/modules/tasks/domain/entities/task.dart'; +import 'package:keep_track/features/tasks/presentation/state/bucket_controller.dart'; import 'package:keep_track/features/tasks/presentation/state/project_controller.dart'; import 'package:keep_track/features/tasks/presentation/state/task_controller.dart'; import 'package:keep_track/features/tasks/presentation/screens/tabs/task/components/task_management_dialog.dart'; @@ -26,6 +28,7 @@ class TaskDetailsPage extends StatefulWidget { class _TaskDetailsPageState extends State { late final TaskController _controller; late final ProjectController _projectController; + late final BucketController _bucketController; late final SupabaseService _supabaseService; @override @@ -33,8 +36,10 @@ class _TaskDetailsPageState extends State { super.initState(); _controller = locator.get(); _projectController = locator.get(); + _bucketController = locator.get(); _supabaseService = locator.get(); _projectController.loadActiveProjects(); + _bucketController.loadBuckets(); } void _showCreateSubtaskDialog(Task parentTask) { @@ -95,99 +100,107 @@ class _TaskDetailsPageState extends State { .where((p) => p.status == ProjectStatus.active && !p.isArchived) .toList(); - return Dialog( - child: Container( - width: 600, - constraints: const BoxConstraints(maxHeight: 700), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.grey[300]!), - ), - ), - child: Row( - children: [ - const Expanded( - child: Text( - 'Edit Task', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), + return AsyncStreamBuilder>( + state: _bucketController, + builder: (context, buckets) { + final activeBuckets = buckets.where((b) => !b.isArchive).toList(); + + return Dialog( + child: Container( + width: 600, + constraints: const BoxConstraints(maxHeight: 700), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey[300]!), ), ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), + child: Row( + children: [ + const Expanded( + child: Text( + 'Edit Task', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], ), - ], - ), - ), + ), - // Content - Expanded( - child: TaskManagementDialog( - task: task, - userId: _supabaseService.userId!, - projects: activeProjects, - useDialogContent: true, - onSave: (updatedTask) async { - try { - await _controller.updateTask(updatedTask); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Task updated successfully'), - backgroundColor: Colors.green, - ), - ); - Navigator.pop(context); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: ${e.toString()}'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - onDelete: () async { - try { - await _controller.deleteTask(task.id!); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Task deleted successfully'), - backgroundColor: Colors.green, - ), - ); - Navigator.pop(context); // Close dialog - Navigator.pop(context); // Close drawer - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: ${e.toString()}'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - ), + // Content + Expanded( + child: TaskManagementDialog( + task: task, + userId: _supabaseService.userId!, + projects: activeProjects, + buckets: activeBuckets, + useDialogContent: true, + onSave: (updatedTask) async { + try { + await _controller.updateTask(updatedTask); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Task updated successfully'), + backgroundColor: Colors.green, + ), + ); + Navigator.pop(context); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + onDelete: () async { + try { + await _controller.deleteTask(task.id!); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Task deleted successfully'), + backgroundColor: Colors.green, + ), + ); + Navigator.pop(context); // Close dialog + Navigator.pop(context); // Close drawer + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + ), + ), + ], ), - ], - ), - ), + ), + ); + }, ); }, ), @@ -577,8 +590,14 @@ class _TaskDetailsPageState extends State { ), ...activeProjects.map((project) { final projectColor = project.color != null - ? Color(int.parse( - project.color!.replaceFirst('#', '0xff'))) + ? Color( + int.parse( + project.color!.replaceFirst( + '#', + '0xff', + ), + ), + ) : Colors.blue[700]!; return DropdownMenuItem( @@ -618,6 +637,81 @@ class _TaskDetailsPageState extends State { ), const SizedBox(height: 16), + // Bucket - Editable + AsyncStreamBuilder>( + state: _bucketController, + builder: (context, buckets) { + final activeBuckets = buckets + .where((b) => !b.isArchive) + .toList(); + + return _buildEditableDetailSection( + 'Bucket', + Icons.inbox, + DropdownButton( + value: task.bucketId, + isExpanded: true, + underline: const SizedBox(), + items: [ + const DropdownMenuItem( + value: null, + child: Row( + children: [ + Icon( + Icons.remove_circle_outline, + size: 16, + color: Colors.grey, + ), + SizedBox(width: 8), + Text( + 'No Bucket', + style: TextStyle( + color: Colors.grey, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ...activeBuckets.map((bucket) { + return DropdownMenuItem( + value: bucket.id, + child: Row( + children: [ + Icon( + Icons.inbox, + color: Colors.purple[700], + size: 16, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + bucket.name, + style: TextStyle( + color: Colors.purple[700], + fontWeight: FontWeight.w600, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + }), + ], + onChanged: (newBucketId) async { + final updatedTask = task.copyWith( + bucketId: newBucketId, + ); + print(task.bucketId); + await _controller.updateTask(updatedTask); + }, + ), + ); + }, + ), + const SizedBox(height: 16), + // Due Date - Editable _buildEditableDetailSection( 'Due Date', diff --git a/lib/features/tasks/presentation/screens/tabs/task/tasks_tab_new.dart b/lib/features/tasks/presentation/screens/tabs/task/tasks_tab_new.dart index a03f567..258a2f8 100644 --- a/lib/features/tasks/presentation/screens/tabs/task/tasks_tab_new.dart +++ b/lib/features/tasks/presentation/screens/tabs/task/tasks_tab_new.dart @@ -8,8 +8,10 @@ import 'package:keep_track/core/theme/app_theme.dart'; import 'package:keep_track/core/ui/app_layout_controller.dart'; import 'package:keep_track/core/ui/responsive/desktop_aware_screen.dart'; import 'package:keep_track/core/ui/scoped_screen.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/entities/bucket.dart'; import 'package:keep_track/features/tasks/modules/tasks/domain/entities/task.dart'; import 'package:keep_track/features/tasks/modules/projects/domain/entities/project.dart'; +import 'package:keep_track/features/tasks/presentation/state/bucket_controller.dart'; import 'package:keep_track/features/tasks/presentation/state/task_controller.dart'; import 'package:keep_track/features/tasks/presentation/state/project_controller.dart'; import 'package:keep_track/features/tasks/presentation/screens/tabs/task/components/task_management_dialog.dart'; @@ -35,6 +37,7 @@ class _TasksTabNewState extends ScopedScreenState with AppLayoutControlled { late final TaskController _controller; late final ProjectController _projectController; + late final BucketController _bucketController; late final SupabaseService _supabaseService; late ScrollController _dateScrollController; @@ -49,11 +52,13 @@ class _TasksTabNewState extends ScopedScreenState void registerServices() { _controller = locator.get(); _projectController = locator.get(); + _bucketController = locator.get(); _supabaseService = locator.get(); _dateScrollController = ScrollController(); - // Load active projects + // Load active projects and buckets _projectController.loadActiveProjects(); + _bucketController.loadBuckets(); // Scroll to center on init WidgetsBinding.instance.addPostFrameCallback((_) { @@ -111,7 +116,11 @@ class _TasksTabNewState extends ScopedScreenState final sortedTasks = _sortTasksByPriority(filteredTasks); return Scaffold( - backgroundColor: isDesktop ? (Theme.of(context).brightness == Brightness.dark ? const Color(0xFF09090B) : AppColors.backgroundSecondary) : null, + backgroundColor: isDesktop + ? (Theme.of(context).brightness == Brightness.dark + ? const Color(0xFF09090B) + : AppColors.backgroundSecondary) + : null, body: isDesktop ? _buildDesktopLayout(tasks, sortedTasks) : _buildMobileLayout(tasks, sortedTasks), @@ -1203,6 +1212,50 @@ class _TasksTabNewState extends ScopedScreenState Colors.blue[700]!, Icons.list, ), + // Project badge + if (task.projectId != null) + Builder( + builder: (context) { + final project = _projectController + .currentProjects + ?.where((p) => p.id == task.projectId) + .firstOrNull; + if (project == null) + return const SizedBox.shrink(); + final projectColor = project.color != null + ? Color( + int.parse( + project.color!.replaceFirst( + '#', + '0xff', + ), + ), + ) + : Colors.blue[700]!; + return _buildTaskBadge( + project.name, + projectColor, + Icons.folder, + ); + }, + ), + // Bucket badge + if (task.bucketId != null) + Builder( + builder: (context) { + final bucket = _bucketController + .getBucketFromCurrentState( + task.bucketId!, + ); + if (bucket == null) + return const SizedBox.shrink(); + return _buildTaskBadge( + bucket.name, + Colors.purple[700]!, + Icons.inbox, + ); + }, + ), ], ), ], @@ -1283,147 +1336,163 @@ class _TasksTabNewState extends ScopedScreenState // Desktop: Use Dialog wrapper with content mode showDialog( context: context, - builder: (context) => Dialog( - child: Container( - width: 600, - constraints: const BoxConstraints(maxHeight: 700), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.grey[300]!), - ), - ), - child: Row( - children: [ - const Expanded( - child: Text( - 'Edit Task', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), + builder: (context) => AsyncStreamBuilder>( + state: _bucketController, + builder: (context, buckets) { + final activeBuckets = buckets.where((b) => !b.isArchive).toList(); + + return Dialog( + child: Container( + width: 600, + constraints: const BoxConstraints(maxHeight: 700), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey[300]!), ), ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), + child: Row( + children: [ + const Expanded( + child: Text( + 'Edit Task', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], ), - ], - ), - ), + ), - // Content - Expanded( - child: TaskManagementDialog( - task: task, - userId: _supabaseService.userId!, - useDialogContent: true, - onSave: (updatedTask) async { - try { - await _controller.updateTask(updatedTask); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Task updated successfully'), - backgroundColor: Colors.green, - ), - ); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: ${e.toString()}'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - onDelete: () async { - try { - await _controller.deleteTask(task.id!); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Task deleted successfully'), - backgroundColor: Colors.green, - ), - ); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: ${e.toString()}'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - ), + // Content + Expanded( + child: TaskManagementDialog( + task: task, + userId: _supabaseService.userId!, + buckets: activeBuckets, + useDialogContent: true, + onSave: (updatedTask) async { + try { + await _controller.updateTask(updatedTask); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Task updated successfully'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + onDelete: () async { + try { + await _controller.deleteTask(task.id!); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Task deleted successfully'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + ), + ), + ], ), - ], - ), - ), + ), + ); + }, ), ); } else { // Mobile: Use AlertDialog mode showDialog( context: context, - builder: (context) => TaskManagementDialog( - task: task, - userId: _supabaseService.userId!, - onSave: (updatedTask) async { - try { - await _controller.updateTask(updatedTask); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Task updated successfully'), - backgroundColor: Colors.green, - ), - ); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: ${e.toString()}'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - onDelete: () async { - try { - await _controller.deleteTask(task.id!); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Task deleted successfully'), - backgroundColor: Colors.green, - ), - ); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: ${e.toString()}'), - backgroundColor: Colors.red, - ), - ); - } - } + builder: (context) => AsyncStreamBuilder>( + state: _bucketController, + builder: (context, buckets) { + final activeBuckets = buckets.where((b) => !b.isArchive).toList(); + + return TaskManagementDialog( + task: task, + userId: _supabaseService.userId!, + buckets: activeBuckets, + onSave: (updatedTask) async { + try { + await _controller.updateTask(updatedTask); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Task updated successfully'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + onDelete: () async { + try { + await _controller.deleteTask(task.id!); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Task deleted successfully'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + ); }, ), ); @@ -1567,98 +1636,106 @@ class _TasksTabNewState extends ScopedScreenState .where((p) => p.status == ProjectStatus.active && !p.isArchived) .toList(); - return Dialog( - child: Container( - width: 600, - constraints: const BoxConstraints(maxHeight: 700), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.grey[300]!), - ), - ), - child: Row( - children: [ - const Expanded( - child: Text( - 'Create Task', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), + return AsyncStreamBuilder>( + state: _bucketController, + builder: (context, buckets) { + return Dialog( + child: Container( + width: 600, + constraints: const BoxConstraints(maxHeight: 700), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey[300]!), ), ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), + child: Row( + children: [ + const Expanded( + child: Text( + 'Create Task', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], ), - ], - ), - ), + ), - // Content - Expanded( - child: TaskManagementDialog( - userId: _supabaseService.userId!, - projects: activeProjects, - useDialogContent: true, - onSave: (newTask) async { - try { - await _controller.createTask(newTask); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Task created successfully'), - backgroundColor: Colors.green, - ), - ); - Navigator.pop(context); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: ${e.toString()}'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - ), - ), + // Content + Expanded( + child: TaskManagementDialog( + userId: _supabaseService.userId!, + projects: activeProjects, + buckets: buckets, + useDialogContent: true, + onSave: (newTask) async { + try { + await _controller.createTask(newTask); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Task created successfully'), + backgroundColor: Colors.green, + ), + ); + Navigator.pop(context); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + ), + ), - // Footer with "View Full Page" button - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border(top: BorderSide(color: Colors.grey[300]!)), - ), - child: SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - icon: const Icon(Icons.open_in_full), - label: const Text('View Full Page'), - onPressed: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const CreateTaskPage(), - ), - ); - }, + // Footer with "View Full Page" button + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: Colors.grey[300]!), + ), + ), + child: SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + icon: const Icon(Icons.open_in_full), + label: const Text('View Full Page'), + onPressed: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CreateTaskPage(), + ), + ); + }, + ), + ), ), - ), + ], ), - ], - ), - ), + ), + ); + }, ); }, ), diff --git a/lib/features/tasks/presentation/screens/tasks_home_screen_new.dart b/lib/features/tasks/presentation/screens/tasks_home_screen_new.dart deleted file mode 100644 index 9afcbbb..0000000 --- a/lib/features/tasks/presentation/screens/tasks_home_screen_new.dart +++ /dev/null @@ -1,976 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:keep_track/core/ui/app_layout_controller.dart'; -import 'package:keep_track/core/ui/ui.dart'; - -/// New Task Management Screen with Calendar View and Subtasks -class TasksHomeScreenNew extends ScopedScreen { - const TasksHomeScreenNew({super.key}); - - @override - State createState() => _TasksHomeScreenNewState(); -} - -class _TasksHomeScreenNewState extends ScopedScreenState - with AppLayoutControlled { - CalendarViewMode _viewMode = CalendarViewMode.month; - DateTime _selectedDate = DateTime.now(); - TaskItem? _selectedTask; - bool _isPomodoroActive = false; - int _pomodoroMinutesRemaining = 25; - - @override - void registerServices() { - // Services will be wired later - } - - @override - void onReady() { - configureLayout(title: 'Tasks', showBottomNav: true); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - // View Mode Selector - _buildViewModeSelector(), - - // Pomodoro Session Widget (shows when active or for today's tasks) - if (_isPomodoroActive || _isToday(_selectedDate)) - _buildPomodoroWidget(), - - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - // Calendar Grid View - _buildCalendarGrid(), - const SizedBox(height: 24), - - // Task List with Subtasks - _buildTaskList(), - ], - ), - ), - ), - ], - ); - } - - bool _isToday(DateTime date) { - final now = DateTime.now(); - return date.year == now.year && - date.month == now.month && - date.day == now.day; - } - - Widget _buildViewModeSelector() { - return Card( - margin: const EdgeInsets.all(16), - elevation: 0, - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - Expanded( - child: _buildViewModeButton( - 'Month', - CalendarViewMode.month, - Icons.calendar_month, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildViewModeButton( - 'Week', - CalendarViewMode.week, - Icons.calendar_view_week, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildViewModeButton( - 'Day', - CalendarViewMode.day, - Icons.calendar_today, - ), - ), - ], - ), - ), - ); - } - - Widget _buildViewModeButton(String label, CalendarViewMode mode, IconData icon) { - final isSelected = _viewMode == mode; - return Material( - color: isSelected - ? Theme.of(context).colorScheme.primary.withOpacity(0.1) - : Colors.transparent, - borderRadius: BorderRadius.circular(8), - child: InkWell( - onTap: () => setState(() => _viewMode = mode), - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Column( - children: [ - Icon( - icon, - color: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - const SizedBox(height: 4), - Text( - label, - style: TextStyle( - fontSize: 12, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - color: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ), - ], - ), - ), - ), - ); - } - - Widget _buildPomodoroWidget() { - return Card( - margin: const EdgeInsets.symmetric(horizontal: 16), - elevation: 0, - color: Colors.red[50], - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.red[100], - shape: BoxShape.circle, - ), - child: Icon(Icons.timer, color: Colors.red[700]), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _isPomodoroActive ? 'Pomodoro Session' : 'Ready for Focus', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.red[900], - ), - ), - Text( - _isPomodoroActive - ? '$_pomodoroMinutesRemaining minutes remaining' - : 'Start a pomodoro session', - style: TextStyle(fontSize: 12, color: Colors.red[700]), - ), - ], - ), - ), - ElevatedButton( - onPressed: () { - setState(() => _isPomodoroActive = !_isPomodoroActive); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red[700], - foregroundColor: Colors.white, - ), - child: Text(_isPomodoroActive ? 'Stop' : 'Start'), - ), - ], - ), - if (_isPomodoroActive) ...[ - const SizedBox(height: 12), - Text( - 'Today\'s Tasks Available', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.red[900], - ), - ), - const SizedBox(height: 8), - ...dummyTasks - .where((task) => task.parentId == null && _isToday(task.dueDate)) - .take(3) - .map((task) => Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Row( - children: [ - Icon(Icons.circle, size: 8, color: Colors.red[700]), - const SizedBox(width: 8), - Expanded( - child: Text( - task.title, - style: TextStyle( - fontSize: 12, - color: Colors.red[900], - ), - ), - ), - ], - ), - )), - ], - ], - ), - ), - ); - } - - Widget _buildCalendarGrid() { - return Card( - elevation: 0, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Calendar Header - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: const Icon(Icons.chevron_left), - onPressed: () { - setState(() { - _selectedDate = _getPreviousPeriod(_selectedDate, _viewMode); - }); - }, - ), - Text( - _getCalendarTitle(), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - IconButton( - icon: const Icon(Icons.chevron_right), - onPressed: () { - setState(() { - _selectedDate = _getNextPeriod(_selectedDate, _viewMode); - }); - }, - ), - ], - ), - const SizedBox(height: 16), - - // Calendar Grid - if (_viewMode == CalendarViewMode.month) - _buildMonthGrid() - else if (_viewMode == CalendarViewMode.week) - _buildWeekGrid() - else - _buildDayView(), - ], - ), - ), - ); - } - - String _getCalendarTitle() { - switch (_viewMode) { - case CalendarViewMode.month: - return DateFormat('MMMM yyyy').format(_selectedDate); - case CalendarViewMode.week: - final weekStart = _getWeekStart(_selectedDate); - final weekEnd = weekStart.add(const Duration(days: 6)); - return '${DateFormat('MMM d').format(weekStart)} - ${DateFormat('MMM d, yyyy').format(weekEnd)}'; - case CalendarViewMode.day: - return DateFormat('EEEE, MMMM d, yyyy').format(_selectedDate); - } - } - - DateTime _getPreviousPeriod(DateTime date, CalendarViewMode mode) { - switch (mode) { - case CalendarViewMode.month: - return DateTime(date.year, date.month - 1, 1); - case CalendarViewMode.week: - return date.subtract(const Duration(days: 7)); - case CalendarViewMode.day: - return date.subtract(const Duration(days: 1)); - } - } - - DateTime _getNextPeriod(DateTime date, CalendarViewMode mode) { - switch (mode) { - case CalendarViewMode.month: - return DateTime(date.year, date.month + 1, 1); - case CalendarViewMode.week: - return date.add(const Duration(days: 7)); - case CalendarViewMode.day: - return date.add(const Duration(days: 1)); - } - } - - DateTime _getWeekStart(DateTime date) { - return date.subtract(Duration(days: date.weekday - 1)); - } - - Widget _buildMonthGrid() { - final firstDay = DateTime(_selectedDate.year, _selectedDate.month, 1); - final lastDay = DateTime(_selectedDate.year, _selectedDate.month + 1, 0); - final startOffset = firstDay.weekday - 1; - final totalDays = lastDay.day; - - return Column( - children: [ - // Day names header - Row( - children: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] - .map((day) => Expanded( - child: Center( - child: Text( - day, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ), - ), - )) - .toList(), - ), - const SizedBox(height: 8), - - // Calendar grid - GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 7, - childAspectRatio: 1, - crossAxisSpacing: 4, - mainAxisSpacing: 4, - ), - itemCount: 42, // 6 weeks - itemBuilder: (context, index) { - final dayNumber = index - startOffset + 1; - - if (dayNumber < 1 || dayNumber > totalDays) { - return Container(); // Empty cell - } - - final date = DateTime(_selectedDate.year, _selectedDate.month, dayNumber); - final tasksOnDay = dummyTasks - .where((task) => _isSameDay(task.dueDate, date)) - .length; - - return _buildDayCell(date, tasksOnDay); - }, - ), - ], - ); - } - - Widget _buildWeekGrid() { - final weekStart = _getWeekStart(_selectedDate); - - return Row( - children: List.generate(7, (index) { - final date = weekStart.add(Duration(days: index)); - final tasksOnDay = dummyTasks - .where((task) => _isSameDay(task.dueDate, date)) - .length; - - return Expanded( - child: Column( - children: [ - Text( - DateFormat('E').format(date), - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ), - const SizedBox(height: 8), - _buildDayCell(date, tasksOnDay, height: 80), - ], - ), - ); - }), - ); - } - - Widget _buildDayView() { - final tasksToday = dummyTasks - .where((task) => _isSameDay(task.dueDate, _selectedDate)) - .toList(); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${tasksToday.length} task${tasksToday.length == 1 ? '' : 's'} today', - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ), - const SizedBox(height: 12), - Container( - height: 120, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.05), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Theme.of(context).colorScheme.primary.withOpacity(0.2), - ), - ), - child: tasksToday.isEmpty - ? Center( - child: Text( - 'No tasks for this day', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.4), - ), - ), - ) - : ListView.builder( - padding: const EdgeInsets.all(12), - itemCount: tasksToday.length, - itemBuilder: (context, index) { - final task = tasksToday[index]; - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Row( - children: [ - Icon( - Icons.circle, - size: 8, - color: _getPriorityColor(task.priority), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - task.title, - style: const TextStyle(fontSize: 13), - ), - ), - ], - ), - ); - }, - ), - ), - ], - ); - } - - Widget _buildDayCell(DateTime date, int taskCount, {double? height}) { - final isToday = _isToday(date); - final isSelected = _isSameDay(date, _selectedDate); - - return InkWell( - onTap: () => setState(() => _selectedDate = date), - child: Container( - height: height, - decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).colorScheme.primary.withOpacity(0.1) - : isToday - ? Theme.of(context).colorScheme.primary.withOpacity(0.05) - : null, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: isToday - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline.withOpacity(0.2), - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '${date.day}', - style: TextStyle( - fontWeight: isToday ? FontWeight.bold : FontWeight.normal, - color: isToday - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface, - ), - ), - if (taskCount > 0) ...[ - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(10), - ), - child: Text( - '$taskCount', - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ], - ), - ), - ); - } - - bool _isSameDay(DateTime date1, DateTime date2) { - return date1.year == date2.year && - date1.month == date2.month && - date1.day == date2.day; - } - - Widget _buildTaskList() { - final tasksOnSelectedDate = dummyTasks - .where((task) => - task.parentId == null && - _isSameDay(task.dueDate, _selectedDate)) - .toList(); - - if (tasksOnSelectedDate.isEmpty) { - return Card( - elevation: 0, - child: Padding( - padding: const EdgeInsets.all(32), - child: Center( - child: Column( - children: [ - Icon( - Icons.task_alt, - size: 48, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.3), - ), - const SizedBox(height: 8), - Text( - 'No tasks for ${DateFormat('MMM d').format(_selectedDate)}', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ), - ], - ), - ), - ), - ); - } - - return Card( - elevation: 0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Icon( - Icons.list, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Text( - 'Tasks for ${DateFormat('MMM d').format(_selectedDate)}', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - const Divider(height: 1), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: tasksOnSelectedDate.length, - itemBuilder: (context, index) { - final task = tasksOnSelectedDate[index]; - return _buildTaskItem(task, 0); - }, - ), - ], - ), - ); - } - - Widget _buildTaskItem(TaskItem task, int indentLevel) { - final subtasks = dummyTasks.where((t) => t.parentId == task.id).toList(); - final isExpanded = _selectedTask?.id == task.id; - - return Column( - children: [ - InkWell( - onTap: () { - setState(() { - _selectedTask = isExpanded ? null : task; - }); - }, - child: Padding( - padding: EdgeInsets.only( - left: 16.0 + (indentLevel * 24.0), - right: 16.0, - top: 12.0, - bottom: 12.0, - ), - child: Row( - children: [ - // Checkbox - Checkbox( - value: task.isCompleted, - onChanged: (value) { - // Will be wired later - }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - ), - const SizedBox(width: 8), - - // Priority indicator - Container( - width: 4, - height: 40, - decoration: BoxDecoration( - color: _getPriorityColor(task.priority), - borderRadius: BorderRadius.circular(2), - ), - ), - const SizedBox(width: 12), - - // Task info - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - task.title, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - decoration: task.isCompleted - ? TextDecoration.lineThrough - : null, - ), - ), - if (task.description != null) ...[ - const SizedBox(height: 4), - Text( - task.description!, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - const SizedBox(height: 4), - Row( - children: [ - Icon( - Icons.flag, - size: 12, - color: _getPriorityColor(task.priority), - ), - const SizedBox(width: 4), - Text( - task.priority, - style: TextStyle( - fontSize: 11, - color: _getPriorityColor(task.priority), - ), - ), - if (task.pomodoroSessions > 0) ...[ - const SizedBox(width: 12), - Icon( - Icons.timer, - size: 12, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - const SizedBox(width: 4), - Text( - '${task.pomodoroSessions} sessions', - style: TextStyle( - fontSize: 11, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ), - ], - if (subtasks.isNotEmpty) ...[ - const SizedBox(width: 12), - Icon( - Icons.subdirectory_arrow_right, - size: 12, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - const SizedBox(width: 4), - Text( - '${subtasks.length} subtask${subtasks.length > 1 ? 's' : ''}', - style: TextStyle( - fontSize: 11, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ), - ], - ], - ), - ], - ), - ), - - // Expand/collapse icon - if (subtasks.isNotEmpty || isExpanded) - Icon( - isExpanded ? Icons.expand_less : Icons.expand_more, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ], - ), - ), - ), - - // Metadata when expanded - if (isExpanded) ...[ - Container( - margin: EdgeInsets.only( - left: 16.0 + (indentLevel * 24.0), - right: 16.0, - bottom: 12.0, - ), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3), - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildMetadataRow('Created', DateFormat('MMM d, yyyy').format(task.createdAt)), - _buildMetadataRow('Due', DateFormat('MMM d, yyyy HH:mm').format(task.dueDate)), - _buildMetadataRow('Status', task.status), - _buildMetadataRow('Priority', task.priority), - if (task.tags.isNotEmpty) - _buildMetadataRow('Tags', task.tags.join(', ')), - if (task.estimatedMinutes != null) - _buildMetadataRow('Estimated', '${task.estimatedMinutes} min'), - if (task.pomodoroSessions > 0) - _buildMetadataRow('Pomodoro Sessions', '${task.pomodoroSessions}'), - ], - ), - ), - ], - - // Subtasks (indented) - ...subtasks.map((subtask) => _buildTaskItem(subtask, indentLevel + 1)), - - if (indentLevel == 0) - const Divider(height: 1), - ], - ); - } - - Widget _buildMetadataRow(String label, String value) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 100, - child: Text( - label, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), - ), - ), - ), - Expanded( - child: Text( - value, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ), - ), - ], - ), - ); - } - - Color _getPriorityColor(String priority) { - switch (priority.toLowerCase()) { - case 'urgent': - return Colors.red[700]!; - case 'high': - return Colors.orange[700]!; - case 'medium': - return Colors.blue[700]!; - case 'low': - return Colors.grey[600]!; - default: - return Colors.grey[600]!; - } - } -} - -enum CalendarViewMode { month, week, day } - -// Dummy Data Classes -class TaskItem { - final String id; - final String? parentId; - final String title; - final String? description; - final String status; - final String priority; - final DateTime createdAt; - final DateTime dueDate; - final bool isCompleted; - final List tags; - final int? estimatedMinutes; - final int pomodoroSessions; - - TaskItem({ - required this.id, - this.parentId, - required this.title, - this.description, - required this.status, - required this.priority, - required this.createdAt, - required this.dueDate, - this.isCompleted = false, - this.tags = const [], - this.estimatedMinutes, - this.pomodoroSessions = 0, - }); -} - -// Dummy Data -final dummyTasks = [ - TaskItem( - id: '1', - title: 'Complete project proposal', - description: 'Write and submit Q1 project proposal', - status: 'In Progress', - priority: 'Urgent', - createdAt: DateTime.now().subtract(const Duration(days: 2)), - dueDate: DateTime.now(), - tags: ['work', 'important'], - estimatedMinutes: 120, - pomodoroSessions: 3, - ), - TaskItem( - id: '1.1', - parentId: '1', - title: 'Research market trends', - status: 'Completed', - priority: 'High', - createdAt: DateTime.now().subtract(const Duration(days: 2)), - dueDate: DateTime.now(), - isCompleted: true, - pomodoroSessions: 2, - ), - TaskItem( - id: '1.2', - parentId: '1', - title: 'Draft executive summary', - status: 'In Progress', - priority: 'High', - createdAt: DateTime.now().subtract(const Duration(days: 1)), - dueDate: DateTime.now(), - pomodoroSessions: 1, - ), - TaskItem( - id: '1.3', - parentId: '1', - title: 'Get approval from stakeholders', - status: 'To Do', - priority: 'Urgent', - createdAt: DateTime.now(), - dueDate: DateTime.now(), - ), - TaskItem( - id: '2', - title: 'Team meeting at 2 PM', - description: 'Discuss sprint retrospective', - status: 'To Do', - priority: 'Medium', - createdAt: DateTime.now().subtract(const Duration(days: 1)), - dueDate: DateTime.now(), - tags: ['meeting'], - estimatedMinutes: 60, - ), - TaskItem( - id: '3', - title: 'Review code changes', - description: 'Review PR #234 and PR #235', - status: 'To Do', - priority: 'High', - createdAt: DateTime.now(), - dueDate: DateTime.now().add(const Duration(days: 1)), - tags: ['code-review', 'urgent'], - estimatedMinutes: 45, - ), - TaskItem( - id: '3.1', - parentId: '3', - title: 'Review PR #234', - status: 'To Do', - priority: 'High', - createdAt: DateTime.now(), - dueDate: DateTime.now().add(const Duration(days: 1)), - ), - TaskItem( - id: '3.2', - parentId: '3', - title: 'Review PR #235', - status: 'To Do', - priority: 'High', - createdAt: DateTime.now(), - dueDate: DateTime.now().add(const Duration(days: 1)), - ), - TaskItem( - id: '4', - title: 'Update documentation', - status: 'To Do', - priority: 'Low', - createdAt: DateTime.now(), - dueDate: DateTime.now().add(const Duration(days: 3)), - tags: ['docs'], - estimatedMinutes: 90, - ), - TaskItem( - id: '5', - title: 'Design system updates', - description: 'Update color palette and typography', - status: 'To Do', - priority: 'Medium', - createdAt: DateTime.now(), - dueDate: DateTime.now().add(const Duration(days: 5)), - tags: ['design', 'ui'], - estimatedMinutes: 180, - pomodoroSessions: 1, - ), -]; diff --git a/lib/features/tasks/presentation/state/bucket_controller.dart b/lib/features/tasks/presentation/state/bucket_controller.dart index aa4ee52..cc21536 100644 --- a/lib/features/tasks/presentation/state/bucket_controller.dart +++ b/lib/features/tasks/presentation/state/bucket_controller.dart @@ -19,23 +19,46 @@ class BucketController extends StreamState>> { } /// Create a new bucket - Future createBucket({required String name}) async { - // TODO: Implement Create bucket + Future createBucket(Bucket bucket) async { + await execute(() async { + final created = await _repository.createBucket(bucket).then((r) => r.unwrap()); + final current = data ?? []; + return [...current, created]; + }); } /// Update an existing bucket Future updateBucket(Bucket bucket) async { - // TODO: Implement UpdateBucket + await execute(() async { + await _repository.updateBucket(bucket).then((r) => r.unwrap()); + await loadBuckets(); + return data ?? []; + }); } /// Delete a bucket Future deleteBucket(String id) async { - // TODO: Implement DeleteBucket + await execute(() async { + await _repository.deleteBucket(id).then((r) => r.unwrap()); + await loadBuckets(); + return data ?? []; + }); } /// Archive a bucket (soft delete) Future archiveBucket(String id) async { - // TODO: Implement ArchiveBucket + final bucket = getBucketFromCurrentState(id); + if (bucket != null) { + await updateBucket(bucket.copyWith(isArchive: true)); + } + } + + /// Unarchive a bucket + Future unarchiveBucket(String id) async { + final bucket = getBucketFromCurrentState(id); + if (bucket != null) { + await updateBucket(bucket.copyWith(isArchive: false)); + } } /// Refresh current data diff --git a/lib/features/tasks/presentation/state/project_controller.dart b/lib/features/tasks/presentation/state/project_controller.dart index 7d43825..e579681 100644 --- a/lib/features/tasks/presentation/state/project_controller.dart +++ b/lib/features/tasks/presentation/state/project_controller.dart @@ -80,4 +80,21 @@ class ProjectController extends StreamState>> { return data ?? []; }); } + + /// Get projects currently in state (if loaded) + List? get currentProjects => state is AsyncData> + ? (state as AsyncData>).data + : null; + + /// Get project by ID from current state + Project? getProjectFromCurrentState(String id) { + final projects = currentProjects; + if (projects == null) return null; + + try { + return projects.where((p) => p.id == id).first; + } catch (e) { + return null; + } + } } diff --git a/pubspec.yaml b/pubspec.yaml index 2176888..a07a466 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: flutter_colorpicker: ^1.1.0 shared_preferences: ^2.3.3 app_links: ^6.3.2 + url_launcher: ^6.3.1 dev_dependencies: flutter_test: From 845becc5dabe4f5d4ecedc4fa53a1c43d5574f3f Mon Sep 17 00:00:00 2001 From: Khesir Date: Tue, 20 Jan 2026 21:14:31 +0800 Subject: [PATCH 03/13] feat: fixed account details on transaction filter and added buckets on projectts --- .../screens/account_details_screen.dart | 466 +++++++++++------- lib/features/home/home_screen.dart | 187 ++++--- .../project_management_screen.dart | 28 +- .../widgets/project_management_dialog.dart | 31 ++ .../screens/create_project_page.dart | 72 +-- .../screens/tabs/projects_tab.dart | 138 ++++-- 6 files changed, 599 insertions(+), 323 deletions(-) diff --git a/lib/features/finance/presentation/screens/account_details_screen.dart b/lib/features/finance/presentation/screens/account_details_screen.dart index cbe5105..f13e7dd 100644 --- a/lib/features/finance/presentation/screens/account_details_screen.dart +++ b/lib/features/finance/presentation/screens/account_details_screen.dart @@ -4,9 +4,7 @@ import 'package:keep_track/core/di/service_locator.dart'; import 'package:keep_track/core/settings/utils/currency_formatter.dart'; import 'package:keep_track/core/state/stream_builder_widget.dart'; import 'package:keep_track/core/theme/app_theme.dart'; -import 'package:keep_track/core/ui/app_layout_controller.dart'; import 'package:keep_track/core/ui/responsive/desktop_aware_screen.dart'; -import 'package:keep_track/core/ui/ui.dart'; import 'package:keep_track/core/utils/icon_helper.dart'; import 'package:keep_track/features/finance/modules/account/domain/entities/account.dart'; import 'package:keep_track/features/finance/modules/transaction/domain/entities/transaction.dart'; @@ -27,7 +25,7 @@ class DailyAccountData { }); } -class AccountDetailsScreen extends ScopedScreen { +class AccountDetailsScreen extends StatefulWidget { final Account account; const AccountDetailsScreen({super.key, required this.account}); @@ -36,70 +34,112 @@ class AccountDetailsScreen extends ScopedScreen { State createState() => _AccountDetailsScreenState(); } -class _AccountDetailsScreenState extends ScopedScreenState - with AppLayoutControlled { +class _AccountDetailsScreenState extends State { late final TransactionController _transactionController; int _daysToShow = 15; + Set _selectedTypes = {'income', 'expense', 'transfer'}; @override - void registerServices() { + void initState() { + super.initState(); _transactionController = locator.get(); - } - - @override - void onReady() { - configureLayout(title: widget.account.name, showBottomNav: false); _transactionController.loadTransactionsByAccount(widget.account.id!); } List _processTransactionsToDaily(List transactions) { - final now = DateTime.now(); - final today = DateTime(now.year, now.month, now.day); - final startDate = today.subtract(Duration(days: _daysToShow - 1)); - - // Create a map with all dates initialized to zero final Map dailyMap = {}; - for (int i = 0; i < _daysToShow; i++) { - final date = startDate.add(Duration(days: i)); - final dateKey = DateFormat('yyyy-MM-dd').format(date); - dailyMap[dateKey] = DailyAccountData( - date: date, - income: 0, - expense: 0, - transfer: 0, - ); - } - // Aggregate transactions by date - for (final transaction in transactions) { - final dateKey = DateFormat('yyyy-MM-dd').format(transaction.date); - if (dailyMap.containsKey(dateKey)) { - final existing = dailyMap[dateKey]!; - switch (transaction.type) { - case TransactionType.income: - dailyMap[dateKey] = DailyAccountData( - date: existing.date, - income: existing.income + transaction.amount, - expense: existing.expense, - transfer: existing.transfer, - ); - break; - case TransactionType.expense: - dailyMap[dateKey] = DailyAccountData( - date: existing.date, - income: existing.income, - expense: existing.expense + transaction.amount, - transfer: existing.transfer, - ); - break; - case TransactionType.transfer: - dailyMap[dateKey] = DailyAccountData( - date: existing.date, - income: existing.income, - expense: existing.expense, - transfer: existing.transfer + transaction.amount, - ); - break; + if (_daysToShow == 0) { + // "All" mode: process all transactions + for (final transaction in transactions) { + final dateKey = DateFormat('yyyy-MM-dd').format(transaction.date); + final existing = dailyMap[dateKey]; + + if (existing == null) { + dailyMap[dateKey] = DailyAccountData( + date: DateTime(transaction.date.year, transaction.date.month, transaction.date.day), + income: transaction.type == TransactionType.income ? transaction.amount : 0, + expense: transaction.type == TransactionType.expense ? transaction.amount : 0, + transfer: transaction.type == TransactionType.transfer ? transaction.amount : 0, + ); + } else { + switch (transaction.type) { + case TransactionType.income: + dailyMap[dateKey] = DailyAccountData( + date: existing.date, + income: existing.income + transaction.amount, + expense: existing.expense, + transfer: existing.transfer, + ); + break; + case TransactionType.expense: + dailyMap[dateKey] = DailyAccountData( + date: existing.date, + income: existing.income, + expense: existing.expense + transaction.amount, + transfer: existing.transfer, + ); + break; + case TransactionType.transfer: + dailyMap[dateKey] = DailyAccountData( + date: existing.date, + income: existing.income, + expense: existing.expense, + transfer: existing.transfer + transaction.amount, + ); + break; + } + } + } + } else { + // Fixed days mode: only process transactions within the range + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final startDate = today.subtract(Duration(days: _daysToShow - 1)); + + // Create a map with all dates initialized to zero + for (int i = 0; i < _daysToShow; i++) { + final date = startDate.add(Duration(days: i)); + final dateKey = DateFormat('yyyy-MM-dd').format(date); + dailyMap[dateKey] = DailyAccountData( + date: date, + income: 0, + expense: 0, + transfer: 0, + ); + } + + // Aggregate transactions by date + for (final transaction in transactions) { + final dateKey = DateFormat('yyyy-MM-dd').format(transaction.date); + if (dailyMap.containsKey(dateKey)) { + final existing = dailyMap[dateKey]!; + switch (transaction.type) { + case TransactionType.income: + dailyMap[dateKey] = DailyAccountData( + date: existing.date, + income: existing.income + transaction.amount, + expense: existing.expense, + transfer: existing.transfer, + ); + break; + case TransactionType.expense: + dailyMap[dateKey] = DailyAccountData( + date: existing.date, + income: existing.income, + expense: existing.expense + transaction.amount, + transfer: existing.transfer, + ); + break; + case TransactionType.transfer: + dailyMap[dateKey] = DailyAccountData( + date: existing.date, + income: existing.income, + expense: existing.expense, + transfer: existing.transfer + transaction.amount, + ); + break; + } } } } @@ -110,6 +150,91 @@ class _AccountDetailsScreenState extends ScopedScreenState return result; } + Widget _buildBody(BuildContext context, bool isDesktop, Color accountColor, IconData accountIcon) { + return AsyncStreamBuilder>( + state: _transactionController, + loadingBuilder: (_) => const Center(child: CircularProgressIndicator()), + errorBuilder: (context, message) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 64, color: Colors.red[300]), + const SizedBox(height: 16), + Text('Error: $message'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => _transactionController.loadTransactionsByAccount(widget.account.id!), + child: const Text('Retry'), + ), + ], + ), + ), + builder: (context, transactions) { + // Filter transactions for this account + final accountTransactions = transactions.where((t) => + t.accountId == widget.account.id || + t.toAccountId == widget.account.id).toList(); + + // Sort by date descending + accountTransactions.sort((a, b) => b.date.compareTo(a.date)); + + // Calculate totals + double totalIncome = 0; + double totalExpense = 0; + double totalTransfer = 0; + + for (final t in accountTransactions) { + switch (t.type) { + case TransactionType.income: + totalIncome += t.amount; + break; + case TransactionType.expense: + totalExpense += t.amount; + break; + case TransactionType.transfer: + totalTransfer += t.amount; + break; + } + } + + return SingleChildScrollView( + padding: EdgeInsets.all(isDesktop ? AppSpacing.xl : 16), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: isDesktop ? 1200 : double.infinity, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Account Info Card + _buildAccountInfoCard(accountColor, accountIcon, isDesktop), + SizedBox(height: isDesktop ? AppSpacing.xl : 24), + + // Summary Cards + _buildSummaryCards( + totalIncome, + totalExpense, + totalTransfer, + isDesktop, + ), + SizedBox(height: isDesktop ? AppSpacing.xl : 24), + + // Bar Graph + _buildBarGraph(accountTransactions, isDesktop), + SizedBox(height: isDesktop ? AppSpacing.xl : 24), + + // Transactions List + _buildTransactionsList(accountTransactions, isDesktop), + ], + ), + ), + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { final accountColor = widget.account.colorHex != null @@ -132,89 +257,7 @@ class _AccountDetailsScreenState extends ScopedScreenState onPressed: () => Navigator.pop(context), ), ), - body: AsyncStreamBuilder>( - state: _transactionController, - loadingBuilder: (_) => const Center(child: CircularProgressIndicator()), - errorBuilder: (context, message) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.error_outline, size: 64, color: Colors.red[300]), - const SizedBox(height: 16), - Text('Error: $message'), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => _transactionController - .loadTransactionsByAccount(widget.account.id!), - child: const Text('Retry'), - ), - ], - ), - ), - builder: (context, transactions) { - // Filter transactions for this account - final accountTransactions = transactions.where((t) => - t.accountId == widget.account.id || - t.toAccountId == widget.account.id).toList(); - - // Sort by date descending - accountTransactions.sort((a, b) => b.date.compareTo(a.date)); - - // Calculate totals - double totalIncome = 0; - double totalExpense = 0; - double totalTransfer = 0; - - for (final t in accountTransactions) { - switch (t.type) { - case TransactionType.income: - totalIncome += t.amount; - break; - case TransactionType.expense: - totalExpense += t.amount; - break; - case TransactionType.transfer: - totalTransfer += t.amount; - break; - } - } - - return SingleChildScrollView( - padding: EdgeInsets.all(isDesktop ? AppSpacing.xl : 16), - child: Center( - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: isDesktop ? 1200 : double.infinity, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Account Info Card - _buildAccountInfoCard(accountColor, accountIcon, isDesktop), - SizedBox(height: isDesktop ? AppSpacing.xl : 24), - - // Summary Cards - _buildSummaryCards( - totalIncome, - totalExpense, - totalTransfer, - isDesktop, - ), - SizedBox(height: isDesktop ? AppSpacing.xl : 24), - - // Bar Graph - _buildBarGraph(accountTransactions, isDesktop), - SizedBox(height: isDesktop ? AppSpacing.xl : 24), - - // Transactions List - _buildTransactionsList(accountTransactions, isDesktop), - ], - ), - ), - ), - ); - }, - ), + body: _buildBody(context, isDesktop, accountColor, accountIcon), ); }, ); @@ -422,10 +465,13 @@ class _AccountDetailsScreenState extends ScopedScreenState value: _daysToShow, isDense: true, underline: const SizedBox(), - items: [7, 15, 30].map((days) { + items: [7, 15, 30, 0].map((days) { return DropdownMenuItem( value: days, - child: Text('$days days', style: const TextStyle(fontSize: 12)), + child: Text( + days == 0 ? 'All' : '$days days', + style: const TextStyle(fontSize: 12), + ), ); }).toList(), onChanged: (value) { @@ -441,14 +487,14 @@ class _AccountDetailsScreenState extends ScopedScreenState ), const SizedBox(height: 12), - // Legend - Row( + // Filter chips + Wrap( + spacing: 8, + runSpacing: 8, children: [ - _buildLegendIndicator(Colors.green, 'Income'), - const SizedBox(width: 16), - _buildLegendIndicator(Colors.red, 'Expense'), - const SizedBox(width: 16), - _buildLegendIndicator(Colors.blue, 'Transfer'), + _buildFilterChip('income', 'Income', Colors.green), + _buildFilterChip('expense', 'Expense', Colors.red), + _buildFilterChip('transfer', 'Transfer', Colors.blue), ], ), const SizedBox(height: 16), @@ -478,28 +524,39 @@ class _AccountDetailsScreenState extends ScopedScreenState ) : LayoutBuilder( builder: (context, constraints) { - final barWidth = (constraints.maxWidth - (data.length - 1) * 6) / (data.length * 3); - final clampedBarWidth = barWidth.clamp(3.0, 16.0); + final selectedCount = _selectedTypes.length; + final barWidth = selectedCount > 0 + ? (constraints.maxWidth - (data.length - 1) * 6) / (data.length * selectedCount) + : (constraints.maxWidth - (data.length - 1) * 6) / (data.length * 3); + final clampedBarWidth = barWidth.clamp(3.0, 20.0); return Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: data.map((dayData) { - final incomeHeight = maxAmount > 0 + final incomeHeight = maxAmount > 0 && _selectedTypes.contains('income') ? (dayData.income / maxAmount) * (isDesktop ? 160 : 120) : 0.0; - final expenseHeight = maxAmount > 0 + final expenseHeight = maxAmount > 0 && _selectedTypes.contains('expense') ? (dayData.expense / maxAmount) * (isDesktop ? 160 : 120) : 0.0; - final transferHeight = maxAmount > 0 + final transferHeight = maxAmount > 0 && _selectedTypes.contains('transfer') ? (dayData.transfer / maxAmount) * (isDesktop ? 160 : 120) : 0.0; + final tooltipLines = [DateFormat('MMM d').format(dayData.date)]; + if (_selectedTypes.contains('income')) { + tooltipLines.add('Income: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.income)}'); + } + if (_selectedTypes.contains('expense')) { + tooltipLines.add('Expense: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.expense)}'); + } + if (_selectedTypes.contains('transfer')) { + tooltipLines.add('Transfer: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.transfer)}'); + } + return Tooltip( - message: '${DateFormat('MMM d').format(dayData.date)}\n' - 'Income: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.income)}\n' - 'Expense: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.expense)}\n' - 'Transfer: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.transfer)}', + message: tooltipLines.join('\n'), child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -507,43 +564,48 @@ class _AccountDetailsScreenState extends ScopedScreenState crossAxisAlignment: CrossAxisAlignment.end, children: [ // Income bar - AnimatedContainer( - duration: const Duration(milliseconds: 300), - width: clampedBarWidth, - height: incomeHeight, - decoration: BoxDecoration( - color: Colors.green.withOpacity(0.8), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(3), + if (_selectedTypes.contains('income')) + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: clampedBarWidth, + height: incomeHeight, + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.8), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(3), + ), ), ), - ), - const SizedBox(width: 1), + if (_selectedTypes.contains('income') && _selectedTypes.length > 1) + const SizedBox(width: 1), // Expense bar - AnimatedContainer( - duration: const Duration(milliseconds: 300), - width: clampedBarWidth, - height: expenseHeight, - decoration: BoxDecoration( - color: Colors.red.withOpacity(0.8), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(3), + if (_selectedTypes.contains('expense')) + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: clampedBarWidth, + height: expenseHeight, + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.8), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(3), + ), ), ), - ), - const SizedBox(width: 1), + if (_selectedTypes.contains('expense') && _selectedTypes.contains('transfer')) + const SizedBox(width: 1), // Transfer bar - AnimatedContainer( - duration: const Duration(milliseconds: 300), - width: clampedBarWidth, - height: transferHeight, - decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.8), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(3), + if (_selectedTypes.contains('transfer')) + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: clampedBarWidth, + height: transferHeight, + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.8), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(3), + ), ), ), - ), ], ), const SizedBox(height: 4), @@ -595,6 +657,38 @@ class _AccountDetailsScreenState extends ScopedScreenState ); } + Widget _buildFilterChip(String type, String label, Color color) { + final isSelected = _selectedTypes.contains(type); + return FilterChip( + label: Text( + label, + style: TextStyle( + fontSize: 12, + color: isSelected ? Colors.white : color, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + selected: isSelected, + onSelected: (selected) { + setState(() { + if (selected) { + _selectedTypes.add(type); + } else { + if (_selectedTypes.length > 1) { + _selectedTypes.remove(type); + } + } + }); + }, + selectedColor: color, + backgroundColor: color.withOpacity(0.1), + checkmarkColor: Colors.white, + side: BorderSide(color: color.withOpacity(0.3)), + padding: const EdgeInsets.symmetric(horizontal: 4), + visualDensity: VisualDensity.compact, + ); + } + Widget _buildTransactionsList(List transactions, bool isDesktop) { return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/features/home/home_screen.dart b/lib/features/home/home_screen.dart index 96d52ce..4a90b88 100644 --- a/lib/features/home/home_screen.dart +++ b/lib/features/home/home_screen.dart @@ -29,6 +29,7 @@ class _HomeScreenState extends ScopedScreenState late final BudgetController _budgetController; late final TransactionController _transactionController; int _daysToShow = 15; // Adjustable days for bar graph + Set _selectedTypes = {'income', 'expense', 'transfer'}; @override void registerServices() { @@ -225,15 +226,15 @@ class _HomeScreenState extends ScopedScreenState ), ], ), - const SizedBox(height: 8), - // Legend - Row( + const SizedBox(height: 12), + // Transaction type filter chips + Wrap( + spacing: 8, + runSpacing: 8, children: [ - _buildLegendIndicator(Colors.green, 'Income'), - const SizedBox(width: 16), - _buildLegendIndicator(Colors.red, 'Expense'), - const SizedBox(width: 16), - _buildLegendIndicator(Colors.blue, 'Transfer'), + _buildFilterChip('income', 'Income', Colors.green), + _buildFilterChip('expense', 'Expense', Colors.red), + _buildFilterChip('transfer', 'Transfer', Colors.blue), ], ), const SizedBox(height: 16), @@ -265,25 +266,39 @@ class _HomeScreenState extends ScopedScreenState final barWidth = (constraints.maxWidth - (data.length - 1) * 6) / (data.length * 3); final clampedBarWidth = barWidth.clamp(3.0, 16.0); + final selectedCount = _selectedTypes.length; + final adjustedBarWidth = selectedCount > 0 + ? (constraints.maxWidth - (data.length - 1) * 6) / (data.length * selectedCount) + : clampedBarWidth; + final adjustedClampedBarWidth = adjustedBarWidth.clamp(3.0, 20.0); + return Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: data.map((dayData) { - final incomeHeight = maxAmount > 0 + final incomeHeight = maxAmount > 0 && _selectedTypes.contains('income') ? (dayData.income / maxAmount) * (isDesktop ? 160 : 120) : 0.0; - final expenseHeight = maxAmount > 0 + final expenseHeight = maxAmount > 0 && _selectedTypes.contains('expense') ? (dayData.expense / maxAmount) * (isDesktop ? 160 : 120) : 0.0; - final transferHeight = maxAmount > 0 + final transferHeight = maxAmount > 0 && _selectedTypes.contains('transfer') ? (dayData.transfer / maxAmount) * (isDesktop ? 160 : 120) : 0.0; + final tooltipLines = [DateFormat('MMM d').format(dayData.date)]; + if (_selectedTypes.contains('income')) { + tooltipLines.add('Income: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.income)}'); + } + if (_selectedTypes.contains('expense')) { + tooltipLines.add('Expense: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.expense)}'); + } + if (_selectedTypes.contains('transfer')) { + tooltipLines.add('Transfer: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.transfer)}'); + } + return Tooltip( - message: '${DateFormat('MMM d').format(dayData.date)}\n' - 'Income: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.income)}\n' - 'Expense: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.expense)}\n' - 'Transfer: ${currencyFormatter.currencySymbol}${NumberFormat('#,##0').format(dayData.transfer)}', + message: tooltipLines.join('\n'), child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -291,43 +306,48 @@ class _HomeScreenState extends ScopedScreenState crossAxisAlignment: CrossAxisAlignment.end, children: [ // Income bar - AnimatedContainer( - duration: const Duration(milliseconds: 300), - width: clampedBarWidth, - height: incomeHeight, - decoration: BoxDecoration( - color: Colors.green.withValues(alpha: 0.8), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(3), + if (_selectedTypes.contains('income')) + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: adjustedClampedBarWidth, + height: incomeHeight, + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.8), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(3), + ), ), ), - ), - const SizedBox(width: 1), + if (_selectedTypes.contains('income') && _selectedTypes.length > 1) + const SizedBox(width: 1), // Expense bar - AnimatedContainer( - duration: const Duration(milliseconds: 300), - width: clampedBarWidth, - height: expenseHeight, - decoration: BoxDecoration( - color: Colors.red.withValues(alpha: 0.8), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(3), + if (_selectedTypes.contains('expense')) + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: adjustedClampedBarWidth, + height: expenseHeight, + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.8), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(3), + ), ), ), - ), - const SizedBox(width: 1), + if (_selectedTypes.contains('expense') && _selectedTypes.contains('transfer')) + const SizedBox(width: 1), // Transfer bar - AnimatedContainer( - duration: const Duration(milliseconds: 300), - width: clampedBarWidth, - height: transferHeight, - decoration: BoxDecoration( - color: Colors.blue.withValues(alpha: 0.8), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(3), + if (_selectedTypes.contains('transfer')) + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: adjustedClampedBarWidth, + height: transferHeight, + decoration: BoxDecoration( + color: Colors.blue.withValues(alpha: 0.8), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(3), + ), ), ), - ), ], ), const SizedBox(height: 4), @@ -348,32 +368,37 @@ class _HomeScreenState extends ScopedScreenState ), ), const SizedBox(height: 12), - // Summary row + // Summary row - only show selected types Row( children: [ - Expanded( - child: _buildSummaryCard( - 'Income', - data.fold(0, (sum, d) => sum + d.income), - Colors.green, + if (_selectedTypes.contains('income')) + Expanded( + child: _buildSummaryCard( + 'Income', + data.fold(0, (sum, d) => sum + d.income), + Colors.green, + ), ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildSummaryCard( - 'Expense', - data.fold(0, (sum, d) => sum + d.expense), - Colors.red, + if (_selectedTypes.contains('income') && _selectedTypes.length > 1) + const SizedBox(width: 8), + if (_selectedTypes.contains('expense')) + Expanded( + child: _buildSummaryCard( + 'Expense', + data.fold(0, (sum, d) => sum + d.expense), + Colors.red, + ), ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildSummaryCard( - 'Transfer', - data.fold(0, (sum, d) => sum + d.transfer), - Colors.blue, + if (_selectedTypes.contains('expense') && _selectedTypes.contains('transfer')) + const SizedBox(width: 8), + if (_selectedTypes.contains('transfer')) + Expanded( + child: _buildSummaryCard( + 'Transfer', + data.fold(0, (sum, d) => sum + d.transfer), + Colors.blue, + ), ), - ), ], ), ], @@ -464,6 +489,38 @@ class _HomeScreenState extends ScopedScreenState ); } + Widget _buildFilterChip(String type, String label, Color color) { + final isSelected = _selectedTypes.contains(type); + return FilterChip( + label: Text( + label, + style: TextStyle( + fontSize: 12, + color: isSelected ? Colors.white : color, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + selected: isSelected, + onSelected: (selected) { + setState(() { + if (selected) { + _selectedTypes.add(type); + } else { + if (_selectedTypes.length > 1) { + _selectedTypes.remove(type); + } + } + }); + }, + selectedColor: color, + backgroundColor: color.withValues(alpha: 0.1), + checkmarkColor: Colors.white, + side: BorderSide(color: color.withValues(alpha: 0.3)), + padding: const EdgeInsets.symmetric(horizontal: 4), + visualDensity: VisualDensity.compact, + ); + } + Widget _buildSummaryCard(String title, double amount, Color color) { return Container( padding: const EdgeInsets.all(12), diff --git a/lib/features/tasks/presentation/screens/configuration/project_management_screen.dart b/lib/features/tasks/presentation/screens/configuration/project_management_screen.dart index 57d25e8..49238f9 100644 --- a/lib/features/tasks/presentation/screens/configuration/project_management_screen.dart +++ b/lib/features/tasks/presentation/screens/configuration/project_management_screen.dart @@ -4,10 +4,12 @@ import 'package:keep_track/core/di/service_locator.dart'; import 'package:keep_track/core/state/stream_builder_widget.dart'; import 'package:keep_track/core/ui/app_layout_controller.dart'; import 'package:keep_track/core/ui/ui.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/entities/bucket.dart'; import 'package:keep_track/features/tasks/modules/projects/domain/entities/project.dart'; import 'package:keep_track/features/tasks/modules/tasks/domain/entities/task.dart'; import 'package:keep_track/shared/infrastructure/supabase/supabase_service.dart'; +import '../../state/bucket_controller.dart'; import '../../state/project_controller.dart'; import '../../state/task_controller.dart'; import 'widgets/project_management_dialog.dart'; @@ -24,18 +26,21 @@ class _ProjectManagementScreenState extends ScopedScreenState(); _taskController = locator.get(); + _bucketController = locator.get(); supabaseService = locator.get(); } @override void onReady() { configureLayout(title: 'Manage Projects', showBottomNav: false); + _bucketController.loadBuckets(); } void _showMetadataEditor(Project project) async { @@ -66,12 +71,13 @@ class _ProjectManagementScreenState extends ScopedScreenState? allTasks}) { + void _showProjectDialog({Project? project, List? allTasks, List? buckets}) { showDialog( context: context, builder: (context) => ProjectManagementDialog( project: project, userId: supabaseService.userId!, + buckets: buckets, onSave: (updatedProject) async { try { if (project != null) { @@ -155,6 +161,19 @@ class _ProjectManagementScreenState extends ScopedScreenState>( + state: _bucketController, + loadingBuilder: (_) => const Scaffold( + body: Center(child: CircularProgressIndicator()), + ), + errorBuilder: (_, __) => _buildScaffold(null, null, null), + builder: (context, buckets) { + return _buildScaffold(buckets, null, null); + }, + ); + } + + Widget _buildScaffold(List? buckets, List? tasks, List? projects) { return Scaffold( appBar: AppBar( title: const Text('Projects'), @@ -163,16 +182,16 @@ class _ProjectManagementScreenState extends ScopedScreenState _showProjectDialog(allTasks: tasks), + onPressed: () => _showProjectDialog(allTasks: tasks, buckets: buckets), icon: const Icon(Icons.add), ); }, loadingBuilder: (_) => IconButton( - onPressed: () => _showProjectDialog(), + onPressed: () => _showProjectDialog(buckets: buckets), icon: const Icon(Icons.add), ), errorBuilder: (_, __) => IconButton( - onPressed: () => _showProjectDialog(), + onPressed: () => _showProjectDialog(buckets: buckets), icon: const Icon(Icons.add), ), ), @@ -291,6 +310,7 @@ class _ProjectManagementScreenState extends ScopedScreenState _showProjectDialog( project: project, allTasks: tasks, + buckets: buckets, ), ), ); diff --git a/lib/features/tasks/presentation/screens/configuration/widgets/project_management_dialog.dart b/lib/features/tasks/presentation/screens/configuration/widgets/project_management_dialog.dart index e833b08..29db7bd 100644 --- a/lib/features/tasks/presentation/screens/configuration/widgets/project_management_dialog.dart +++ b/lib/features/tasks/presentation/screens/configuration/widgets/project_management_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/entities/bucket.dart'; import 'package:keep_track/features/tasks/modules/projects/domain/entities/project.dart'; class ProjectManagementDialog extends StatefulWidget { @@ -7,6 +8,7 @@ class ProjectManagementDialog extends StatefulWidget { final String userId; final Future Function(Project) onSave; final Future Function()? onDelete; + final List? buckets; const ProjectManagementDialog({ super.key, @@ -14,6 +16,7 @@ class ProjectManagementDialog extends StatefulWidget { required this.userId, required this.onSave, this.onDelete, + this.buckets, }); @override @@ -28,6 +31,7 @@ class _ProjectManagementDialogState extends State { bool _isArchived = false; Color _selectedColor = Colors.blue; + String? _selectedBucketId; @override void initState() { @@ -40,6 +44,7 @@ class _ProjectManagementDialogState extends State { ? Color( int.parse(widget.project!.color!.replaceFirst('#', '0xff'))) : Colors.blue; + _selectedBucketId = widget.project?.bucketId; } @override @@ -102,6 +107,31 @@ class _ProjectManagementDialogState extends State { ), const SizedBox(height: 16), + // Bucket Selection + if (widget.buckets != null && widget.buckets!.isNotEmpty) ...[ + DropdownButtonFormField( + value: _selectedBucketId, + decoration: const InputDecoration( + labelText: 'Bucket', + border: OutlineInputBorder(), + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('No Bucket'), + ), + ...widget.buckets!.where((b) => !b.isArchive).map( + (bucket) => DropdownMenuItem( + value: bucket.id, + child: Text(bucket.name), + ), + ), + ], + onChanged: (v) => setState(() => _selectedBucketId = v), + ), + const SizedBox(height: 16), + ], + // Archived SwitchListTile( contentPadding: EdgeInsets.zero, @@ -198,6 +228,7 @@ class _ProjectManagementDialogState extends State { userId: widget.userId, createdAt: widget.project?.createdAt, updatedAt: widget.project?.updatedAt, + bucketId: _selectedBucketId, ); await widget.onSave(project); diff --git a/lib/features/tasks/presentation/screens/create_project_page.dart b/lib/features/tasks/presentation/screens/create_project_page.dart index 72ef4d9..af370f1 100644 --- a/lib/features/tasks/presentation/screens/create_project_page.dart +++ b/lib/features/tasks/presentation/screens/create_project_page.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:keep_track/core/di/service_locator.dart'; +import 'package:keep_track/core/state/stream_builder_widget.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/entities/bucket.dart'; +import 'package:keep_track/features/tasks/presentation/state/bucket_controller.dart'; import 'package:keep_track/features/tasks/presentation/state/project_controller.dart'; import 'package:keep_track/features/tasks/presentation/screens/configuration/widgets/project_management_dialog.dart'; import 'package:keep_track/shared/infrastructure/supabase/supabase_service.dart'; @@ -13,48 +16,61 @@ class CreateProjectPage extends StatefulWidget { class _CreateProjectPageState extends State { late final ProjectController _controller; + late final BucketController _bucketController; late final SupabaseService _supabaseService; @override void initState() { super.initState(); _controller = locator.get(); + _bucketController = locator.get(); _supabaseService = locator.get(); + _bucketController.loadBuckets(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Create Project')), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: ProjectManagementDialog( - userId: _supabaseService.userId!, - onSave: (newProject) async { - try { - await _controller.createProject(newProject); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Project created successfully'), - backgroundColor: Colors.green, - ), - ); - Navigator.pop(context); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: ${e.toString()}'), - backgroundColor: Colors.red, - ), - ); - } + body: AsyncStreamBuilder>( + state: _bucketController, + loadingBuilder: (_) => const Center(child: CircularProgressIndicator()), + errorBuilder: (_, __) => _buildForm(null), + builder: (context, buckets) => _buildForm(buckets), + ), + ); + } + + Widget _buildForm(List? buckets) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: ProjectManagementDialog( + userId: _supabaseService.userId!, + buckets: buckets, + onSave: (newProject) async { + try { + await _controller.createProject(newProject); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Project created successfully'), + backgroundColor: Colors.green, + ), + ); + Navigator.pop(context); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); } - }, - ), + } + }, ), ), ); diff --git a/lib/features/tasks/presentation/screens/tabs/projects_tab.dart b/lib/features/tasks/presentation/screens/tabs/projects_tab.dart index 7482146..961f272 100644 --- a/lib/features/tasks/presentation/screens/tabs/projects_tab.dart +++ b/lib/features/tasks/presentation/screens/tabs/projects_tab.dart @@ -7,7 +7,9 @@ import 'package:keep_track/core/theme/app_theme.dart'; import 'package:keep_track/core/ui/app_layout_controller.dart'; import 'package:keep_track/core/ui/responsive/desktop_aware_screen.dart'; import 'package:keep_track/core/ui/ui.dart'; +import 'package:keep_track/features/tasks/modules/buckets/domain/entities/bucket.dart'; import 'package:keep_track/features/tasks/modules/projects/domain/entities/project.dart'; +import 'package:keep_track/features/tasks/presentation/state/bucket_controller.dart'; import 'package:keep_track/features/tasks/presentation/state/project_controller.dart'; enum ProjectStatusFilter { all, active, postponed, closed } @@ -23,16 +25,19 @@ class ProjectsTab extends ScopedScreen { class _ProjectsTabState extends ScopedScreenState with AppLayoutControlled { late final ProjectController _controller; + late final BucketController _bucketController; ProjectStatusFilter _statusFilter = ProjectStatusFilter.all; @override void registerServices() { _controller = locator.get(); + _bucketController = locator.get(); } @override void onReady() { configureLayout(title: 'Projects', showBottomNav: true); + _bucketController.loadBuckets(); } List _filterProjects(List projects) { @@ -60,10 +65,21 @@ class _ProjectsTabState extends ScopedScreenState Widget build(BuildContext context) { return DesktopAwareScreen( builder: (context, isDesktop) { - return AsyncStreamBuilder>( - state: _controller, - builder: (context, projects) { - final filteredProjects = _filterProjects(projects); + return AsyncStreamBuilder>( + state: _bucketController, + loadingBuilder: (_) => const Center(child: CircularProgressIndicator()), + errorBuilder: (_, __) => _buildBody(context, isDesktop, []), + builder: (context, buckets) => _buildBody(context, isDesktop, buckets), + ); + }, + ); + } + + Widget _buildBody(BuildContext context, bool isDesktop, List buckets) { + return AsyncStreamBuilder>( + state: _controller, + builder: (context, projects) { + final filteredProjects = _filterProjects(projects); return Scaffold( backgroundColor: isDesktop ? (Theme.of(context).brightness == Brightness.dark ? const Color(0xFF09090B) : AppColors.backgroundSecondary) : null, @@ -164,7 +180,7 @@ class _ProjectsTabState extends ScopedScreenState spacing: AppSpacing.lg, desktopChildAspectRatio: 0.85, children: filteredProjects - .map((project) => _buildProjectCard(project)) + .map((project) => _buildProjectCard(project, buckets)) .toList(), ) else @@ -181,7 +197,7 @@ class _ProjectsTabState extends ScopedScreenState itemCount: filteredProjects.length, itemBuilder: (context, index) { final project = filteredProjects[index]; - return _buildProjectCard(project); + return _buildProjectCard(project, buckets); }, ), ], @@ -242,8 +258,6 @@ class _ProjectsTabState extends ScopedScreenState ), ), ); - }, - ); } Widget _buildFilterChip(String label, ProjectStatusFilter filter) { @@ -294,7 +308,7 @@ class _ProjectsTabState extends ScopedScreenState ); } - Widget _buildProjectCard(Project project) { + Widget _buildProjectCard(Project project, List buckets) { // Parse color from project final projectColor = project.color != null ? Color(int.parse(project.color!.replaceFirst('#', '0xff'))) @@ -306,6 +320,11 @@ class _ProjectsTabState extends ScopedScreenState ? Colors.orange : Colors.grey; + // Find bucket name if project has a bucket + final bucket = project.bucketId != null + ? buckets.where((b) => b.id == project.bucketId).firstOrNull + : null; + return Card( elevation: 0, clipBehavior: Clip.antiAlias, @@ -397,41 +416,80 @@ class _ProjectsTabState extends ScopedScreenState ], ), - // Status Badge + // Status and Bucket Badges const SizedBox(height: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - border: Border.all(color: statusColor), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - project.status == ProjectStatus.active - ? Icons.check_circle - : project.status == ProjectStatus.postponed - ? Icons.pause_circle - : Icons.cancel, - size: 12, - color: statusColor, + Wrap( + spacing: 8, + runSpacing: 4, + children: [ + // Status Badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, ), - const SizedBox(width: 4), - Text( - project.status.displayName, - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w600, - color: statusColor, + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: statusColor), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + project.status == ProjectStatus.active + ? Icons.check_circle + : project.status == ProjectStatus.postponed + ? Icons.pause_circle + : Icons.cancel, + size: 12, + color: statusColor, + ), + const SizedBox(width: 4), + Text( + project.status.displayName, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: statusColor, + ), + ), + ], + ), + ), + // Bucket Badge + if (bucket != null) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.purple.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.purple), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.inventory_2_outlined, + size: 12, + color: Colors.purple, + ), + const SizedBox(width: 4), + Text( + bucket.name, + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: Colors.purple, + ), + ), + ], ), ), - ], - ), + ], ), ], ), From f7cf0f6a26f29a562cb8f7516ee963eba3c10cbc Mon Sep 17 00:00:00 2001 From: Khesir Date: Fri, 23 Jan 2026 12:27:10 +0800 Subject: [PATCH 04/13] feat: added stopwatch session (pomodoro) & fix ui inconsistencies --- lib/core/migrations/migration_manager.dart | 2 + .../045_add_pomodoro_stopwatch_type.dart | 60 ++ lib/features/home/task_home_screen.dart | 571 +++++------------- .../finance_module_screen.dart | 16 + .../module_selection/task_module_screen.dart | 18 +- .../domain/entities/pomodoro_session.dart | 12 +- .../domain/entities/pomodoro_settings.dart | 3 + .../screens/tabs/pomodoro_tab.dart | 74 ++- .../screens/tabs/task/task_details_page.dart | 10 +- .../screens/tabs/task/tasks_tab_new.dart | 100 +-- .../state/pomodoro_session_controller.dart | 9 +- .../widgets/pomodoro_nav_indicator.dart | 200 ++++++ .../transaction_completion_dialog.dart | 2 +- 13 files changed, 579 insertions(+), 498 deletions(-) create mode 100644 lib/core/migrations/migrations/045_add_pomodoro_stopwatch_type.dart create mode 100644 lib/features/tasks/presentation/widgets/pomodoro_nav_indicator.dart diff --git a/lib/core/migrations/migration_manager.dart b/lib/core/migrations/migration_manager.dart index dd8b9e1..d7c82ec 100644 --- a/lib/core/migrations/migration_manager.dart +++ b/lib/core/migrations/migration_manager.dart @@ -45,6 +45,7 @@ import 'migrations/041_cleanup_stale_pomodoro_sessions.dart'; import 'migrations/042_add_pomodoro_session_title.dart'; import 'migrations/043_create_buckets_table.dart'; import 'migrations/044_add_bucket_id_to_task_and_project.dart'; +import 'migrations/045_add_pomodoro_stopwatch_type.dart'; /// Manages database migrations /// @@ -301,6 +302,7 @@ class MigrationManager { Migration042AddPomodoroSessionTitle(), Migration043CreateBucketsTable(), Migration044AddBucketIdToTaskAndProject(), + Migration045AddPomodoroStopwatchType(), // Add new migrations here: ]; diff --git a/lib/core/migrations/migrations/045_add_pomodoro_stopwatch_type.dart b/lib/core/migrations/migrations/045_add_pomodoro_stopwatch_type.dart new file mode 100644 index 0000000..50123c4 --- /dev/null +++ b/lib/core/migrations/migrations/045_add_pomodoro_stopwatch_type.dart @@ -0,0 +1,60 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:keep_track/core/logging/app_logger.dart'; +import '../migration.dart'; + +/// Adds 'stopwatch' to the allowed pomodoro session types +class Migration045AddPomodoroStopwatchType extends Migration { + @override + String get version => '045_add_pomodoro_stopwatch_type'; + + @override + String get description => 'Add stopwatch type to pomodoro_sessions table'; + + @override + Future up(SupabaseClient client) async { + AppLogger.info(' Updating pomodoro_sessions type constraint...'); + + final sql = ''' + -- Drop the existing check constraint on type column + ALTER TABLE pomodoro_sessions + DROP CONSTRAINT IF EXISTS pomodoro_sessions_type_check; + + -- Add new check constraint that includes 'stopwatch' + ALTER TABLE pomodoro_sessions + ADD CONSTRAINT pomodoro_sessions_type_check + CHECK (type IN ('pomodoro', 'short', 'long', 'stopwatch')); + '''; + + try { + await client.rpc('exec_sql', params: {'sql': sql}); + AppLogger.info(' Added stopwatch type to pomodoro_sessions'); + } catch (e, stackTrace) { + AppLogger.error(' Failed to update type constraint', e, stackTrace); + rethrow; + } + } + + @override + Future down(SupabaseClient client) async { + AppLogger.info(' Reverting pomodoro_sessions type constraint...'); + + final sql = ''' + -- Drop the constraint with stopwatch + ALTER TABLE pomodoro_sessions + DROP CONSTRAINT IF EXISTS pomodoro_sessions_type_check; + + -- Restore original constraint (without stopwatch) + ALTER TABLE pomodoro_sessions + ADD CONSTRAINT pomodoro_sessions_type_check + CHECK (type IN ('pomodoro', 'short', 'long')); + '''; + + try { + await client.rpc('exec_sql', params: {'sql': sql}); + AppLogger.info(' Reverted type constraint'); + } catch (e, stackTrace) { + AppLogger.error(' Failed to revert type constraint', e, stackTrace); + rethrow; + } + } +} diff --git a/lib/features/home/task_home_screen.dart b/lib/features/home/task_home_screen.dart index db69966..f20568d 100644 --- a/lib/features/home/task_home_screen.dart +++ b/lib/features/home/task_home_screen.dart @@ -237,17 +237,8 @@ class _TaskHomeScreenState extends ScopedScreenState Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Left Column - Task Snapshot & Current Tasks - Expanded( - flex: 2, - child: Column( - children: [ - _buildTaskSnapshot(isDesktop), - const SizedBox(height: AppSpacing.xl), - _buildCurrentTasks(), - ], - ), - ), + // Left Column - Tasks + Expanded(flex: 2, child: _buildCurrentTasks()), const SizedBox(width: AppSpacing.xl), // Right Column - Projects & Quick Actions Expanded( @@ -264,8 +255,6 @@ class _TaskHomeScreenState extends ScopedScreenState ) else ...[ // Mobile: Stack vertically - _buildTaskSnapshot(isDesktop), - const SizedBox(height: 24), _buildCurrentTasks(), const SizedBox(height: 24), _buildProjectOverview(), @@ -481,166 +470,6 @@ class _TaskHomeScreenState extends ScopedScreenState ); } - Widget _buildTaskSnapshot(bool isDesktop) { - return AsyncStreamBuilder( - state: _taskController, - builder: (context, tasks) { - final totalTasks = tasks.where((t) => !t.isArchived).length; - final completedTasks = tasks - .where((t) => t.isCompleted && !t.isArchived) - .length; - final inProgressTasks = tasks - .where((t) => t.status == TaskStatus.inProgress && !t.isArchived) - .length; - final todayTasks = tasks.where((t) { - if (t.dueDate == null || t.isArchived) return false; - final today = DateTime.now(); - return t.dueDate!.year == today.year && - t.dueDate!.month == today.month && - t.dueDate!.day == today.day; - }).length; - final overdueTasks = tasks.where((t) { - if (t.dueDate == null || t.isArchived || t.isCompleted) return false; - return t.dueDate!.isBefore(DateTime.now()); - }).length; - final noDateTasks = tasks - .where((t) => t.dueDate == null && !t.isArchived && !t.isCompleted) - .length; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isDesktop) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Task Overview', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - ElevatedButton.icon( - onPressed: _handleCreateTask, - icon: const Icon(Icons.add, size: 20), - label: const Text('New Task'), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 12, - ), - ), - ), - ], - ) - else - const Text( - 'Task Overview', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: _buildStatCard( - 'Total Tasks', - totalTasks.toString(), - Icons.task_alt, - Colors.blue, - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildStatCard( - 'In Progress', - inProgressTasks.toString(), - Icons.play_circle_outline, - Colors.blue[700]!, - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: _buildStatCard( - 'Completed', - completedTasks.toString(), - Icons.check_circle, - Colors.green, - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildStatCard( - 'Due Today', - todayTasks.toString(), - Icons.today, - Colors.purple, - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: _buildStatCard( - 'Overdue', - overdueTasks.toString(), - Icons.warning_amber_rounded, - Colors.red, - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildStatCard( - 'No Date', - noDateTasks.toString(), - Icons.event_busy, - Colors.grey, - ), - ), - ], - ), - ], - ); - }, - ); - } - - Widget _buildStatCard( - String title, - String value, - IconData icon, - Color color, - ) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: color.withOpacity(0.3)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(icon, color: color, size: 24), - const SizedBox(height: 8), - Text( - value, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: color, - ), - ), - const SizedBox(height: 4), - Text(title, style: TextStyle(fontSize: 12, color: Colors.grey[700])), - ], - ), - ); - } - List _filterTasks(List tasks) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); @@ -651,27 +480,41 @@ class _TaskHomeScreenState extends ScopedScreenState return tasks.where((t) { if (t.isArchived) return false; if (t.dueDate == null) return false; - final taskDate = DateTime(t.dueDate!.year, t.dueDate!.month, t.dueDate!.day); + final taskDate = DateTime( + t.dueDate!.year, + t.dueDate!.month, + t.dueDate!.day, + ); return taskDate.isAtSameMomentAs(today) || - (t.dueDate!.isAfter(today) && t.dueDate!.isBefore(todayEnd)); + (t.dueDate!.isAfter(today) && t.dueDate!.isBefore(todayEnd)); }).toList(); case TaskTimeFilter.sevenDays: final endDate = today.add(const Duration(days: 7)); return tasks.where((t) { if (t.isArchived) return false; if (t.dueDate == null) return false; - final taskDate = DateTime(t.dueDate!.year, t.dueDate!.month, t.dueDate!.day); - return (taskDate.isAtSameMomentAs(today) || taskDate.isAfter(today)) && - taskDate.isBefore(endDate); + final taskDate = DateTime( + t.dueDate!.year, + t.dueDate!.month, + t.dueDate!.day, + ); + return (taskDate.isAtSameMomentAs(today) || + taskDate.isAfter(today)) && + taskDate.isBefore(endDate); }).toList(); case TaskTimeFilter.thirtyDays: final endDate = today.add(const Duration(days: 30)); return tasks.where((t) { if (t.isArchived) return false; if (t.dueDate == null) return false; - final taskDate = DateTime(t.dueDate!.year, t.dueDate!.month, t.dueDate!.day); - return (taskDate.isAtSameMomentAs(today) || taskDate.isAfter(today)) && - taskDate.isBefore(endDate); + final taskDate = DateTime( + t.dueDate!.year, + t.dueDate!.month, + t.dueDate!.day, + ); + return (taskDate.isAtSameMomentAs(today) || + taskDate.isAfter(today)) && + taskDate.isBefore(endDate); }).toList(); case TaskTimeFilter.all: return tasks.where((t) => !t.isArchived).toList(); @@ -722,12 +565,12 @@ class _TaskHomeScreenState extends ScopedScreenState crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Tasks', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), + const SizedBox(width: 12), Text( '${sortedTasks.length} task${sortedTasks.length != 1 ? 's' : ''}', style: TextStyle( @@ -736,6 +579,18 @@ class _TaskHomeScreenState extends ScopedScreenState ).colorScheme.onSurface.withOpacity(0.6), ), ), + const Spacer(), + ElevatedButton.icon( + onPressed: _handleCreateTask, + icon: const Icon(Icons.add, size: 18), + label: const Text('New Task'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + ), + ), ], ), const SizedBox(height: 12), @@ -931,9 +786,7 @@ class _TaskHomeScreenState extends ScopedScreenState onChanged: (value) async { if (value != null) { final updatedTask = task.copyWith( - status: value - ? TaskStatus.completed - : TaskStatus.todo, + status: value ? TaskStatus.completed : TaskStatus.todo, completedAt: value ? DateTime.now() : null, ); await _taskController.updateTask(updatedTask); @@ -964,10 +817,9 @@ class _TaskHomeScreenState extends ScopedScreenState task.description!, style: TextStyle( fontSize: 12, - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(0.6), + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.6), ), maxLines: 2, overflow: TextOverflow.ellipsis, @@ -996,7 +848,9 @@ class _TaskHomeScreenState extends ScopedScreenState ), _buildTaskBadge( task.dueDate != null - ? DateFormat('MMM d, h:mm a').format(task.dueDate!) + ? DateFormat( + 'MMM d, h:mm a', + ).format(task.dueDate!) : 'No date', task.dueDate != null ? (isOverdue ? Colors.red : Colors.grey[700]!) @@ -1016,10 +870,17 @@ class _TaskHomeScreenState extends ScopedScreenState final project = _projectController.currentProjects ?.where((p) => p.id == task.projectId) .firstOrNull; - if (project == null) return const SizedBox.shrink(); + if (project == null) + return const SizedBox.shrink(); final projectColor = project.color != null - ? Color(int.parse( - project.color!.replaceFirst('#', '0xff'))) + ? Color( + int.parse( + project.color!.replaceFirst( + '#', + '0xff', + ), + ), + ) : Colors.blue[700]!; return _buildTaskBadge( project.name, @@ -1034,7 +895,8 @@ class _TaskHomeScreenState extends ScopedScreenState builder: (context) { final bucket = _bucketController .getBucketFromCurrentState(task.bucketId!); - if (bucket == null) return const SizedBox.shrink(); + if (bucket == null) + return const SizedBox.shrink(); return _buildTaskBadge( bucket.name, Colors.purple[700]!, @@ -1126,14 +988,6 @@ class _TaskHomeScreenState extends ScopedScreenState return SlideTransition(position: animation.drive(tween), child: child); }, pageBuilder: (context, animation, secondaryAnimation) { - final subtasks = allTasks - .where((t) => t.parentTaskId == task.id) - .toList(); - final isOverdue = - task.dueDate != null && - task.dueDate!.isBefore(DateTime.now()) && - !task.isCompleted; - return Align( alignment: Alignment.centerRight, child: Container( @@ -1149,130 +1003,7 @@ class _TaskHomeScreenState extends ScopedScreenState width: MediaQuery.of(context).size.width > 600 ? 500 : MediaQuery.of(context).size.width * 0.85, - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - ), - child: Column( - children: [ - // Header - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border( - bottom: BorderSide( - color: Theme.of( - context, - ).colorScheme.outline.withOpacity(0.2), - ), - ), - ), - child: Row( - children: [ - Expanded( - child: Text( - 'Task Details', - style: Theme.of(context).textTheme.titleLarge - ?.copyWith(fontWeight: FontWeight.bold), - ), - ), - IconButton( - icon: const Icon(Icons.edit), - onPressed: () { - Navigator.pop(context); - _showTaskEditDialog(task); - }, - tooltip: 'Edit Task', - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - ), - ], - ), - ), - - // Quick task details preview - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - task.title, - style: Theme.of(context).textTheme.headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 16), - if (isOverdue) - Chip( - avatar: const Icon( - Icons.warning_amber_rounded, - color: Colors.red, - size: 18, - ), - label: const Text('OVERDUE'), - backgroundColor: Colors.red.withOpacity(0.1), - side: BorderSide( - color: Colors.red.withOpacity(0.5), - ), - ), - if (task.description != null) - Padding( - padding: const EdgeInsets.only(top: 16), - child: Text(task.description!), - ), - if (subtasks.isNotEmpty) ...[ - const SizedBox(height: 16), - Text( - '${subtasks.length} Subtask${subtasks.length > 1 ? 's' : ''}', - style: const TextStyle( - fontWeight: FontWeight.w600, - ), - ), - ], - ], - ), - ), - ), - - // Action Buttons - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border( - top: BorderSide( - color: Theme.of( - context, - ).colorScheme.outline.withOpacity(0.2), - ), - ), - ), - child: SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - icon: const Icon(Icons.open_in_full), - label: const Text('View Full Details'), - onPressed: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - TaskDetailsPage(task: task), - ), - ); - }, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - ), - ), - ), - ), - ], - ), + child: TaskDetailsPage(task: task, isDrawerMode: true), ), ), ), @@ -1496,19 +1227,14 @@ class _TaskHomeScreenState extends ScopedScreenState ), ) else - // 3-Grid Layout for Projects - GridView.builder( + // Simplified list layout for projects + ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - childAspectRatio: 0.85, - ), - itemCount: activeProjects.length > 6 - ? 6 + itemCount: activeProjects.length > 5 + ? 5 : activeProjects.length, + separatorBuilder: (context, index) => const SizedBox(height: 8), itemBuilder: (context, index) { final project = activeProjects[index]; return _buildProjectCard(project); @@ -1525,8 +1251,10 @@ class _TaskHomeScreenState extends ScopedScreenState ? Color(int.parse(project.color!.replaceFirst('#', '0xff'))) : Colors.blue[700]!; - // Get bucket from project metadata or default - final bucket = project.metadata['bucket'] ?? 'Work'; + // Get bucket info + final bucket = _bucketController.getBucketFromCurrentState( + project.bucketId ?? '', + ); return Card( elevation: 0, @@ -1544,50 +1272,95 @@ class _TaskHomeScreenState extends ScopedScreenState }, borderRadius: BorderRadius.circular(12), child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + padding: const EdgeInsets.all(16), + child: Row( children: [ - // Project Icon & Color + // Project Icon Container( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: projectColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon(Icons.folder, color: projectColor, size: 20), - ), - const SizedBox(height: 8), - // Project Title - Text( - project.name, - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 13, + borderRadius: BorderRadius.circular(10), ), - maxLines: 2, - overflow: TextOverflow.ellipsis, + child: Icon(Icons.folder, color: projectColor, size: 24), ), - const Spacer(), - // Bucket Tab - _buildBucketChip(bucket), - const SizedBox(height: 8), - // Status indicator - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.green.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - project.status.displayName, - style: const TextStyle( - fontSize: 9, - color: Colors.green, - fontWeight: FontWeight.w600, - ), + const SizedBox(width: 16), + // Project Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + project.name, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + children: [ + // Status chip + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + project.status.displayName, + style: const TextStyle( + fontSize: 12, + color: Colors.green, + fontWeight: FontWeight.w500, + ), + ), + ), + if (bucket != null) ...[ + const SizedBox(width: 8), + // Bucket chip + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + decoration: BoxDecoration( + color: Colors.purple.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.inbox, + size: 12, + color: Colors.purple[700], + ), + const SizedBox(width: 4), + Text( + bucket.name, + style: TextStyle( + fontSize: 12, + color: Colors.purple[700], + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ], + ), + ], ), ), + // Arrow icon + Icon(Icons.chevron_right, color: Colors.grey[400]), ], ), ), @@ -1595,56 +1368,6 @@ class _TaskHomeScreenState extends ScopedScreenState ); } - Widget _buildBucketChip(String bucket) { - Color bucketColor; - IconData bucketIcon; - - switch (bucket.toLowerCase()) { - case 'work': - bucketColor = Colors.blue; - bucketIcon = Icons.work; - break; - case 'personal': - bucketColor = Colors.green; - bucketIcon = Icons.person; - break; - case 'urgent': - bucketColor = Colors.red; - bucketIcon = Icons.priority_high; - break; - case 'learning': - bucketColor = Colors.purple; - bucketIcon = Icons.school; - break; - default: - bucketColor = Colors.grey; - bucketIcon = Icons.folder; - } - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: bucketColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(bucketIcon, size: 10, color: bucketColor), - const SizedBox(width: 4), - Text( - bucket, - style: TextStyle( - fontSize: 9, - color: bucketColor, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ); - } - String _formatDate(DateTime date) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); diff --git a/lib/features/module_selection/finance_module_screen.dart b/lib/features/module_selection/finance_module_screen.dart index 105a824..832ec01 100644 --- a/lib/features/module_selection/finance_module_screen.dart +++ b/lib/features/module_selection/finance_module_screen.dart @@ -16,7 +16,9 @@ import 'package:keep_track/features/finance/presentation/screens/tabs/planned_pa import 'package:keep_track/features/home/home_screen.dart'; import 'package:keep_track/features/logs/logs_screen.dart'; import 'package:keep_track/features/module_selection/module_selection_screen.dart'; +import 'package:keep_track/features/module_selection/task_module_screen.dart'; import 'package:keep_track/features/profile/presentation/profile_screen.dart'; +import 'package:keep_track/features/tasks/presentation/widgets/pomodoro_nav_indicator.dart'; import '../auth/presentation/screens/auth_settings_screen.dart'; @@ -59,8 +61,22 @@ class _FinanceModuleScreenState extends State { ); } + void _navigateToTaskModulePomodoro() { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const TaskModuleScreen(initialTabIndex: 3), + ), + ); + } + List _buildActions() { return [ + // Pomodoro timer indicator (shows when session is active) + PomodoroNavIndicator( + onTap: _navigateToTaskModulePomodoro, + ), + const SizedBox(width: 8), // Theme toggle button IconButton( icon: Icon( diff --git a/lib/features/module_selection/task_module_screen.dart b/lib/features/module_selection/task_module_screen.dart index 0ae7dc0..989b6ab 100644 --- a/lib/features/module_selection/task_module_screen.dart +++ b/lib/features/module_selection/task_module_screen.dart @@ -13,22 +13,31 @@ import 'package:keep_track/features/profile/presentation/profile_screen.dart'; import 'package:keep_track/features/tasks/presentation/screens/tabs/task/tasks_tab_new.dart'; import 'package:keep_track/features/tasks/presentation/screens/tabs/projects_tab.dart'; import 'package:keep_track/features/tasks/presentation/screens/tabs/pomodoro_tab.dart'; +import 'package:keep_track/features/tasks/presentation/widgets/pomodoro_nav_indicator.dart'; import '../auth/presentation/screens/auth_settings_screen.dart'; /// Task Module Screen - Wraps the task management functionality /// This is what users see when they select "Task Management" from module selection class TaskModuleScreen extends StatefulWidget { - const TaskModuleScreen({super.key}); + final int initialTabIndex; + + const TaskModuleScreen({super.key, this.initialTabIndex = 0}); @override State createState() => _TaskModuleScreenState(); } class _TaskModuleScreenState extends State { - int _currentIndex = 0; + late int _currentIndex; final _layoutController = AppLayoutController(); + @override + void initState() { + super.initState(); + _currentIndex = widget.initialTabIndex; + } + void _changeTab(int index) { setState(() => _currentIndex = index); } @@ -56,6 +65,11 @@ class _TaskModuleScreenState extends State { List _buildActions() { return [ + // Pomodoro timer indicator (shows when session is active) + PomodoroNavIndicator( + onTap: () => _changeTab(3), // Navigate to Pomodoro tab + ), + const SizedBox(width: 8), // Theme toggle button IconButton( icon: Icon( diff --git a/lib/features/tasks/modules/pomodoro/domain/entities/pomodoro_session.dart b/lib/features/tasks/modules/pomodoro/domain/entities/pomodoro_session.dart index 19c39e5..4741cbd 100644 --- a/lib/features/tasks/modules/pomodoro/domain/entities/pomodoro_session.dart +++ b/lib/features/tasks/modules/pomodoro/domain/entities/pomodoro_session.dart @@ -41,6 +41,8 @@ class PomodoroSession { return 'Short Break'; case PomodoroSessionType.longBreak: return 'Long Break'; + case PomodoroSessionType.stopwatch: + return 'Stopwatch Session'; } } @@ -116,6 +118,8 @@ class PomodoroSession { bool get isCanceled => status == PomodoroSessionStatus.canceled; + bool get isStopwatch => type == PomodoroSessionType.stopwatch; + double get progress { if (durationSeconds == 0) return 1.0; final elapsed = elapsedSeconds; @@ -124,7 +128,7 @@ class PomodoroSession { } } -enum PomodoroSessionType { pomodoro, shortBreak, longBreak } +enum PomodoroSessionType { pomodoro, shortBreak, longBreak, stopwatch } enum PomodoroSessionStatus { running, paused, completed, canceled } @@ -137,6 +141,8 @@ extension PomodoroSessionTypeExtension on PomodoroSessionType { return 'short'; case PomodoroSessionType.longBreak: return 'long'; + case PomodoroSessionType.stopwatch: + return 'stopwatch'; } } @@ -148,6 +154,8 @@ extension PomodoroSessionTypeExtension on PomodoroSessionType { return 'Short Break'; case PomodoroSessionType.longBreak: return 'Long Break'; + case PomodoroSessionType.stopwatch: + return 'Stopwatch'; } } @@ -159,6 +167,8 @@ extension PomodoroSessionTypeExtension on PomodoroSessionType { return PomodoroSessionType.shortBreak; case 'long': return PomodoroSessionType.longBreak; + case 'stopwatch': + return PomodoroSessionType.stopwatch; default: return PomodoroSessionType.pomodoro; } diff --git a/lib/features/tasks/modules/pomodoro/domain/entities/pomodoro_settings.dart b/lib/features/tasks/modules/pomodoro/domain/entities/pomodoro_settings.dart index 11b07fe..69d62e7 100644 --- a/lib/features/tasks/modules/pomodoro/domain/entities/pomodoro_settings.dart +++ b/lib/features/tasks/modules/pomodoro/domain/entities/pomodoro_settings.dart @@ -30,6 +30,7 @@ class PomodoroSettings { } /// Get duration in seconds for a specific session type + /// Returns 0 for stopwatch type (indicates no limit/endless) int getDurationSeconds(PomodoroSessionType type) { switch (type) { case PomodoroSessionType.pomodoro: @@ -38,6 +39,8 @@ class PomodoroSettings { return shortBreakDuration * 60; case PomodoroSessionType.longBreak: return longBreakDuration * 60; + case PomodoroSessionType.stopwatch: + return 0; // No limit for stopwatch } } diff --git a/lib/features/tasks/presentation/screens/tabs/pomodoro_tab.dart b/lib/features/tasks/presentation/screens/tabs/pomodoro_tab.dart index 0089888..7498347 100644 --- a/lib/features/tasks/presentation/screens/tabs/pomodoro_tab.dart +++ b/lib/features/tasks/presentation/screens/tabs/pomodoro_tab.dart @@ -246,6 +246,9 @@ class _PomodoroTabState extends ScopedScreenState case PomodoroSessionType.longBreak: sessionColor = Colors.blue; break; + case PomodoroSessionType.stopwatch: + sessionColor = Colors.amber; + break; } } @@ -353,16 +356,26 @@ class _PomodoroTabState extends ScopedScreenState bool isRunning, Color sessionColor, ) { - int remainingSeconds = 0; + int displaySeconds = 0; double progress = 0.0; + bool isStopwatch = session?.isStopwatch ?? false; if (session != null) { - remainingSeconds = session.remainingSeconds; - progress = session.progress; + if (isStopwatch) { + // Stopwatch: show elapsed time (counting up) + displaySeconds = session.elapsedSeconds; + progress = 1.0; // Full circle for stopwatch + } else { + // Regular timer: show remaining time (counting down) + displaySeconds = session.remainingSeconds; + progress = session.progress; + } } - final minutes = remainingSeconds ~/ 60; - final seconds = remainingSeconds % 60; + // Handle hour display for long stopwatch sessions + final hours = displaySeconds ~/ 3600; + final minutes = (displaySeconds % 3600) ~/ 60; + final seconds = displaySeconds % 60; return SizedBox( width: 300, @@ -384,10 +397,12 @@ class _PomodoroTabState extends ScopedScreenState mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}', + hours > 0 + ? '${hours.toString()}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}' + : '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}', style: Theme.of(context).textTheme.displayLarge?.copyWith( fontWeight: FontWeight.bold, - fontSize: 72, + fontSize: hours > 0 ? 56 : 72, color: session != null ? sessionColor : null, ), ), @@ -453,6 +468,13 @@ class _PomodoroTabState extends ScopedScreenState Colors.blue, '${_settings.longBreakDuration} min', ), + _buildSessionTypeChip( + 'Stopwatch', + PomodoroSessionType.stopwatch, + Colors.amber, + 'No limit', + icon: Icons.timer_outlined, + ), ], ); } @@ -461,12 +483,13 @@ class _PomodoroTabState extends ScopedScreenState String label, PomodoroSessionType type, Color color, - String duration, - ) { + String duration, { + IconData icon = Icons.timer, + }) { return ActionChip( avatar: CircleAvatar( backgroundColor: color, - child: const Icon(Icons.timer, color: Colors.white, size: 18), + child: Icon(icon, color: Colors.white, size: 18), ), label: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -483,7 +506,8 @@ class _PomodoroTabState extends ScopedScreenState ], ), onPressed: () { - if (type == PomodoroSessionType.pomodoro) { + if (type == PomodoroSessionType.pomodoro || + type == PomodoroSessionType.stopwatch) { _showProjectSelectorDialog(type); } else { _controller.startSession(type); @@ -562,6 +586,7 @@ class _PomodoroTabState extends ScopedScreenState horizontal: 16, vertical: 16, ), + foregroundColor: Colors.green, ), ), ), @@ -615,6 +640,7 @@ class _PomodoroTabState extends ScopedScreenState label: const Text('Complete'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + foregroundColor: Colors.green, ), ), const SizedBox(width: 12), @@ -986,6 +1012,9 @@ class _PomodoroTabState extends ScopedScreenState case PomodoroSessionType.longBreak: typeColor = Colors.blue; break; + case PomodoroSessionType.stopwatch: + typeColor = Colors.amber; + break; } IconData statusIcon; @@ -1030,8 +1059,13 @@ class _PomodoroTabState extends ScopedScreenState subtitle: Padding( padding: const EdgeInsets.only(top: 6), child: Text( - '${_formatDateTime(session.startedAt)} • ${session.durationSeconds ~/ 60} min', - style: TextStyle(fontSize: 13, color: Colors.grey[600]), + session.isStopwatch + ? '${_formatDateTime(session.startedAt)} • ${_formatElapsedTime(session.elapsedSeconds)}' + : '${_formatDateTime(session.startedAt)} • ${session.durationSeconds ~/ 60} min', + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), trailing: Container( @@ -1532,6 +1566,20 @@ class _PomodoroTabState extends ScopedScreenState return '${dateTime.month}/${dateTime.day} at $timeStr'; } } + + String _formatElapsedTime(int totalSeconds) { + final hours = totalSeconds ~/ 3600; + final minutes = (totalSeconds % 3600) ~/ 60; + final seconds = totalSeconds % 60; + + if (hours > 0) { + return '${hours}h ${minutes}m ${seconds}s'; + } else if (minutes > 0) { + return '${minutes}m ${seconds}s'; + } else { + return '${seconds}s'; + } + } } /// Mobile bottom sheet for session start diff --git a/lib/features/tasks/presentation/screens/tabs/task/task_details_page.dart b/lib/features/tasks/presentation/screens/tabs/task/task_details_page.dart index 659de7a..b632f1b 100644 --- a/lib/features/tasks/presentation/screens/tabs/task/task_details_page.dart +++ b/lib/features/tasks/presentation/screens/tabs/task/task_details_page.dart @@ -234,19 +234,20 @@ class _TaskDetailsPageState extends State { } Widget _buildDetailSection(String title, IconData icon, Widget content) { + final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Icon(icon, size: 20, color: Colors.grey[600]), + Icon(icon, size: 20, color: theme.colorScheme.onSurfaceVariant), const SizedBox(width: 8), Text( title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: Colors.grey[700], + color: theme.colorScheme.onSurfaceVariant, ), ), ], @@ -262,19 +263,20 @@ class _TaskDetailsPageState extends State { IconData icon, Widget content, ) { + final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Icon(icon, size: 20, color: Colors.grey[600]), + Icon(icon, size: 20, color: theme.colorScheme.onSurfaceVariant), const SizedBox(width: 8), Text( title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: Colors.grey[700], + color: theme.colorScheme.onSurfaceVariant, ), ), ], diff --git a/lib/features/tasks/presentation/screens/tabs/task/tasks_tab_new.dart b/lib/features/tasks/presentation/screens/tabs/task/tasks_tab_new.dart index 258a2f8..ef804ec 100644 --- a/lib/features/tasks/presentation/screens/tabs/task/tasks_tab_new.dart +++ b/lib/features/tasks/presentation/screens/tabs/task/tasks_tab_new.dart @@ -853,51 +853,51 @@ class _TasksTabNewState extends ScopedScreenState const SizedBox(height: 12), // Due Date Filter - Text( - 'Due Date', - style: Theme.of( - context, - ).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - _buildFilterChip( - 'All Tasks', - _dueDateFilter == DueDateFilter.all, - () { - setState(() => _dueDateFilter = DueDateFilter.all); - }, - ), - const SizedBox(width: 8), - _buildFilterChip( - 'Due Now', - _dueDateFilter == DueDateFilter.dueNow, - () { - setState(() => _dueDateFilter = DueDateFilter.dueNow); - }, - ), - const SizedBox(width: 8), - _buildFilterChip( - 'Due This Week', - _dueDateFilter == DueDateFilter.dueThisWeek, - () { - setState(() => _dueDateFilter = DueDateFilter.dueThisWeek); - }, - ), - const SizedBox(width: 8), - _buildFilterChip( - 'Due This Month', - _dueDateFilter == DueDateFilter.dueThisMonth, - () { - setState(() => _dueDateFilter = DueDateFilter.dueThisMonth); - }, - ), - ], - ), - ), + // Text( + // 'Due Date', + // style: Theme.of( + // context, + // ).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold), + // ), + // const SizedBox(height: 8), + // SingleChildScrollView( + // scrollDirection: Axis.horizontal, + // child: Row( + // children: [ + // _buildFilterChip( + // 'All Tasks', + // _dueDateFilter == DueDateFilter.all, + // () { + // setState(() => _dueDateFilter = DueDateFilter.all); + // }, + // ), + // const SizedBox(width: 8), + // _buildFilterChip( + // 'Due Now', + // _dueDateFilter == DueDateFilter.dueNow, + // () { + // setState(() => _dueDateFilter = DueDateFilter.dueNow); + // }, + // ), + // const SizedBox(width: 8), + // _buildFilterChip( + // 'Due This Week', + // _dueDateFilter == DueDateFilter.dueThisWeek, + // () { + // setState(() => _dueDateFilter = DueDateFilter.dueThisWeek); + // }, + // ), + // const SizedBox(width: 8), + // _buildFilterChip( + // 'Due This Month', + // _dueDateFilter == DueDateFilter.dueThisMonth, + // () { + // setState(() => _dueDateFilter = DueDateFilter.dueThisMonth); + // }, + // ), + // ], + // ), + // ), ], ); } @@ -1787,19 +1787,20 @@ class _TasksTabNewState extends ScopedScreenState } Widget _buildDetailSection(String title, IconData icon, Widget content) { + final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Icon(icon, size: 20, color: Colors.grey[600]), + Icon(icon, size: 20, color: theme.colorScheme.onSurfaceVariant), const SizedBox(width: 8), Text( title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: Colors.grey[700], + color: theme.colorScheme.onSurfaceVariant, ), ), ], @@ -1815,19 +1816,20 @@ class _TasksTabNewState extends ScopedScreenState IconData icon, Widget content, ) { + final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Icon(icon, size: 20, color: Colors.grey[600]), + Icon(icon, size: 20, color: theme.colorScheme.onSurfaceVariant), const SizedBox(width: 8), Text( title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: Colors.grey[700], + color: theme.colorScheme.onSurfaceVariant, ), ), ], diff --git a/lib/features/tasks/presentation/state/pomodoro_session_controller.dart b/lib/features/tasks/presentation/state/pomodoro_session_controller.dart index 484c2be..da850fa 100644 --- a/lib/features/tasks/presentation/state/pomodoro_session_controller.dart +++ b/lib/features/tasks/presentation/state/pomodoro_session_controller.dart @@ -37,8 +37,9 @@ class PomodoroSessionController final activeSession = result.unwrap(); if (activeSession != null) { - // Check if session time has expired - if (activeSession.remainingSeconds <= 0 && + // Check if session time has expired (skip for stopwatch - no expiration) + if (!activeSession.isStopwatch && + activeSession.remainingSeconds <= 0 && (activeSession.isRunning || activeSession.status == PomodoroSessionStatus.paused)) { // Session expired while user was away - auto-complete it @@ -327,8 +328,8 @@ class PomodoroSessionController // The session entity calculates elapsed/remaining from database startedAt final remaining = currentSession.remainingSeconds; - // Check if timer is complete - if (remaining <= 0) { + // Check if timer is complete (skip for stopwatch - runs forever until manually stopped) + if (!currentSession.isStopwatch && remaining <= 0) { timer.cancel(); _autoCompleteSession(currentSession); return; diff --git a/lib/features/tasks/presentation/widgets/pomodoro_nav_indicator.dart b/lib/features/tasks/presentation/widgets/pomodoro_nav_indicator.dart new file mode 100644 index 0000000..00ff34c --- /dev/null +++ b/lib/features/tasks/presentation/widgets/pomodoro_nav_indicator.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; +import 'package:keep_track/core/di/service_locator.dart'; +import 'package:keep_track/core/state/stream_builder_widget.dart'; +import 'package:keep_track/core/state/stream_state.dart'; +import 'package:keep_track/features/tasks/modules/pomodoro/domain/entities/pomodoro_session.dart'; +import 'package:keep_track/features/tasks/presentation/state/pomodoro_session_controller.dart'; + +/// Compact pomodoro timer indicator for navigation bars +/// Shows elapsed/remaining time when a session is active +class PomodoroNavIndicator extends StatelessWidget { + final VoidCallback? onTap; + + const PomodoroNavIndicator({super.key, this.onTap}); + + @override + Widget build(BuildContext context) { + final controller = locator.get(); + + return AsyncStreamBuilder( + state: controller, + builder: (context, state) { + final session = state.currentSession; + + // Don't show if no active session or session is completed/cancelled + if (session == null || + session.status == PomodoroSessionStatus.completed || + session.status == PomodoroSessionStatus.canceled) { + return const SizedBox.shrink(); + } + + // Get session color + Color sessionColor; + switch (session.type) { + case PomodoroSessionType.pomodoro: + sessionColor = Colors.red; + break; + case PomodoroSessionType.shortBreak: + sessionColor = Colors.green; + break; + case PomodoroSessionType.longBreak: + sessionColor = Colors.blue; + break; + case PomodoroSessionType.stopwatch: + sessionColor = Colors.amber; + break; + } + + // Calculate display time + final int displaySeconds; + if (session.isStopwatch) { + displaySeconds = session.elapsedSeconds; + } else { + displaySeconds = session.remainingSeconds; + } + + final hours = displaySeconds ~/ 3600; + final minutes = (displaySeconds % 3600) ~/ 60; + final seconds = displaySeconds % 60; + + final timeText = hours > 0 + ? '${hours}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}' + : '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + + final isRunning = state.isRunning; + + return Tooltip( + message: '${session.type.displayName} - Tap to view', + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: sessionColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: sessionColor.withOpacity(0.4), + width: 1.5, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Pulsing indicator for running sessions + _PulsingDot( + color: sessionColor, + isAnimating: isRunning, + ), + const SizedBox(width: 8), + // Time display + Text( + timeText, + style: TextStyle( + color: sessionColor, + fontWeight: FontWeight.bold, + fontSize: 14, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + // Pause indicator + if (!isRunning) ...[ + const SizedBox(width: 4), + Icon( + Icons.pause, + size: 14, + color: sessionColor, + ), + ], + ], + ), + ), + ), + ); + }, + loadingBuilder: (_) => const SizedBox.shrink(), + errorBuilder: (_, __) => const SizedBox.shrink(), + ); + } +} + +/// Pulsing dot indicator for active sessions +class _PulsingDot extends StatefulWidget { + final Color color; + final bool isAnimating; + + const _PulsingDot({ + required this.color, + required this.isAnimating, + }); + + @override + State<_PulsingDot> createState() => _PulsingDotState(); +} + +class _PulsingDotState extends State<_PulsingDot> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1000), + vsync: this, + ); + _animation = Tween(begin: 0.5, end: 1.0).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + + if (widget.isAnimating) { + _controller.repeat(reverse: true); + } + } + + @override + void didUpdateWidget(_PulsingDot oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isAnimating && !_controller.isAnimating) { + _controller.repeat(reverse: true); + } else if (!widget.isAnimating && _controller.isAnimating) { + _controller.stop(); + _controller.value = 1.0; + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: widget.color.withOpacity( + widget.isAnimating ? _animation.value : 1.0, + ), + shape: BoxShape.circle, + boxShadow: widget.isAnimating + ? [ + BoxShadow( + color: widget.color.withOpacity(0.4 * _animation.value), + blurRadius: 4, + spreadRadius: 1, + ), + ] + : null, + ), + ); + }, + ); + } +} diff --git a/lib/features/tasks/presentation/widgets/transaction_completion_dialog.dart b/lib/features/tasks/presentation/widgets/transaction_completion_dialog.dart index fad9cd7..f4319ff 100644 --- a/lib/features/tasks/presentation/widgets/transaction_completion_dialog.dart +++ b/lib/features/tasks/presentation/widgets/transaction_completion_dialog.dart @@ -121,7 +121,7 @@ class _TransactionCompletionDialogState prefixText: '₱ ', suffixIcon: Icon( Icons.info_outline, - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), controller: TextEditingController( From b88f556c19eb3149ec21adb3c29deb01d13b9735 Mon Sep 17 00:00:00 2001 From: Khesir Date: Sat, 24 Jan 2026 17:27:55 +0800 Subject: [PATCH 05/13] feat: added version checker for latest release --- lib/core/config/app_info.dart | 24 +++ .../services/version_checker_service.dart | 192 ++++++++++++++++++ .../module_selection_screen.dart | 116 ++++++++++- pubspec.yaml | 2 + 4 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 lib/core/config/app_info.dart create mode 100644 lib/core/services/version_checker_service.dart diff --git a/lib/core/config/app_info.dart b/lib/core/config/app_info.dart new file mode 100644 index 0000000..a4b107c --- /dev/null +++ b/lib/core/config/app_info.dart @@ -0,0 +1,24 @@ +/// Application information and configuration +class AppInfo { + AppInfo._(); + + /// GitHub repository owner + static const String gitHubOwner = 'khesir'; + + /// GitHub repository name + static const String gitHubRepo = 'KeepTrack'; + + /// Full GitHub repository path + static String get gitHubRepoPath => '$gitHubOwner/$gitHubRepo'; + + /// Primary download URL for app updates + static const String downloadUrl = 'https://keep-track.khesir.com/download'; + + /// GitHub releases URL (fallback) + static String get releasesUrl => + 'https://github.com/$gitHubOwner/$gitHubRepo/releases'; + + /// GitHub API URL for latest release + static String get latestReleaseApiUrl => + 'https://api.github.com/repos/$gitHubOwner/$gitHubRepo/releases/latest'; +} diff --git a/lib/core/services/version_checker_service.dart b/lib/core/services/version_checker_service.dart new file mode 100644 index 0000000..52aab01 --- /dev/null +++ b/lib/core/services/version_checker_service.dart @@ -0,0 +1,192 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:keep_track/core/config/app_info.dart'; +import 'package:keep_track/core/logging/app_logger.dart'; + +/// Result of a version check +class VersionCheckResult { + final bool updateAvailable; + final String currentVersion; + final String? latestVersion; + final String? releaseUrl; + final String? releaseNotes; + final String? error; + + const VersionCheckResult({ + required this.updateAvailable, + required this.currentVersion, + this.latestVersion, + this.releaseUrl, + this.releaseNotes, + this.error, + }); + + factory VersionCheckResult.noUpdate(String currentVersion) { + return VersionCheckResult( + updateAvailable: false, + currentVersion: currentVersion, + ); + } + + factory VersionCheckResult.error(String currentVersion, String error) { + return VersionCheckResult( + updateAvailable: false, + currentVersion: currentVersion, + error: error, + ); + } +} + +/// Service for checking app version against GitHub releases +class VersionCheckerService { + VersionCheckerService._(); + + static final VersionCheckerService _instance = VersionCheckerService._(); + static VersionCheckerService get instance => _instance; + + /// Check if a newer version is available on GitHub + Future checkForUpdates() async { + try { + final packageInfo = await PackageInfo.fromPlatform(); + final currentVersion = packageInfo.version; + + AppLogger.info('Version Check: Current version is $currentVersion'); + + final response = await http + .get( + Uri.parse(AppInfo.latestReleaseApiUrl), + headers: {'Accept': 'application/vnd.github.v3+json'}, + ) + .timeout(const Duration(seconds: 10)); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + final tagName = data['tag_name'] as String?; + final htmlUrl = data['html_url'] as String?; + final body = data['body'] as String?; + + if (tagName == null) { + AppLogger.warning('Version Check: No tag_name in response'); + return VersionCheckResult.noUpdate(currentVersion); + } + + // Remove 'v' prefix if present (e.g., v0.7.4 -> 0.7.4) + final latestVersion = tagName.startsWith('v') + ? tagName.substring(1) + : tagName; + + AppLogger.info('Version Check: Latest version is $latestVersion'); + + final isNewer = _isNewerVersion(currentVersion, latestVersion); + + if (isNewer) { + AppLogger.info('Version Check: Update available!'); + return VersionCheckResult( + updateAvailable: true, + currentVersion: currentVersion, + latestVersion: latestVersion, + releaseUrl: htmlUrl ?? AppInfo.releasesUrl, + releaseNotes: body, + ); + } + + return VersionCheckResult.noUpdate(currentVersion); + } else if (response.statusCode == 404) { + AppLogger.info('Version Check: No releases found on GitHub'); + return VersionCheckResult.noUpdate(currentVersion); + } else { + AppLogger.warning( + 'Version Check: Failed with status ${response.statusCode}', + ); + return VersionCheckResult.error( + currentVersion, + 'GitHub API returned status ${response.statusCode}', + ); + } + } catch (e) { + AppLogger.error('Version Check: Error - $e'); + final packageInfo = await PackageInfo.fromPlatform(); + return VersionCheckResult.error( + packageInfo.version, + e.toString(), + ); + } + } + + /// Compare two semantic versions + /// Returns true if latestVersion is newer than currentVersion + bool _isNewerVersion(String current, String latest) { + try { + // Strip any pre-release or build metadata for comparison + // e.g., "0.7.3-alpha.4+38" -> "0.7.3" + final currentBase = _extractBaseVersion(current); + final latestBase = _extractBaseVersion(latest); + + // Validate that both versions contain only numeric parts + final currentParts = _parseVersionParts(currentBase); + final latestParts = _parseVersionParts(latestBase); + + if (currentParts == null || latestParts == null) { + AppLogger.warning( + 'Version Check: Cannot compare non-semantic versions: $current vs $latest', + ); + return false; + } + + // Pad shorter version with zeros + while (currentParts.length < 3) { + currentParts.add(0); + } + while (latestParts.length < 3) { + latestParts.add(0); + } + + // Compare major.minor.patch + for (var i = 0; i < 3; i++) { + if (latestParts[i] > currentParts[i]) return true; + if (latestParts[i] < currentParts[i]) return false; + } + + // Base versions are equal, check pre-release + // A release version is newer than a pre-release of the same base + final currentHasPreRelease = current.contains('-'); + final latestHasPreRelease = latest.contains('-'); + + if (currentHasPreRelease && !latestHasPreRelease) { + // Current is pre-release, latest is stable -> update available + return true; + } + + return false; + } catch (e) { + AppLogger.error('Version comparison error: $e'); + return false; + } + } + + /// Extract base version (major.minor.patch) from full version string + String _extractBaseVersion(String version) { + // Remove pre-release suffix (e.g., -alpha.4) + var base = version.split('-').first; + // Remove build metadata (e.g., +38) + base = base.split('+').first; + return base; + } + + /// Parse version string into numeric parts, returns null if invalid + List? _parseVersionParts(String version) { + final parts = version.split('.'); + final result = []; + + for (final part in parts) { + final number = int.tryParse(part); + if (number == null) { + return null; // Non-numeric part found + } + result.add(number); + } + + return result.isEmpty ? null : result; + } +} diff --git a/lib/features/module_selection/module_selection_screen.dart b/lib/features/module_selection/module_selection_screen.dart index 29dce05..267187b 100644 --- a/lib/features/module_selection/module_selection_screen.dart +++ b/lib/features/module_selection/module_selection_screen.dart @@ -1,13 +1,127 @@ import 'package:flutter/material.dart'; import 'package:keep_track/core/di/service_locator.dart'; +import 'package:keep_track/core/config/app_info.dart'; +import 'package:keep_track/core/services/version_checker_service.dart'; import 'package:keep_track/features/auth/presentation/state/auth_controller.dart'; import 'package:keep_track/features/auth/presentation/screens/auth_settings_screen.dart'; +import 'package:url_launcher/url_launcher.dart'; /// Netflix-style module selection screen after login /// Users can choose between Task Management and Finance Management -class ModuleSelectionScreen extends StatelessWidget { +class ModuleSelectionScreen extends StatefulWidget { const ModuleSelectionScreen({super.key}); + @override + State createState() => _ModuleSelectionScreenState(); +} + +class _ModuleSelectionScreenState extends State { + @override + void initState() { + super.initState(); + _checkForUpdates(); + } + + Future _checkForUpdates() async { + final result = await VersionCheckerService.instance.checkForUpdates(); + + if (!mounted) return; + + if (result.updateAvailable) { + _showUpdateDialog(result); + } + } + + void _showUpdateDialog(VersionCheckResult result) { + showDialog( + context: context, + builder: (context) => AlertDialog( + icon: const Icon(Icons.system_update, size: 48, color: Colors.blue), + title: const Text('Update Available'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'A new version of KeepTrack is available!', + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 16), + _buildVersionRow('Current:', result.currentVersion), + _buildVersionRow('Latest:', result.latestVersion ?? 'Unknown'), + if (result.releaseNotes != null && result.releaseNotes!.isNotEmpty) ...[ + const SizedBox(height: 16), + Text( + 'What\'s new:', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 150), + child: SingleChildScrollView( + child: Text( + result.releaseNotes!, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ), + ], + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Later'), + ), + FilledButton.icon( + onPressed: () async { + Navigator.pop(context); + // Try custom download URL first, fallback to GitHub releases + final primaryUrl = Uri.parse(AppInfo.downloadUrl); + final fallbackUrl = Uri.parse(result.releaseUrl ?? AppInfo.releasesUrl); + + if (await canLaunchUrl(primaryUrl)) { + await launchUrl(primaryUrl, mode: LaunchMode.externalApplication); + } else if (await canLaunchUrl(fallbackUrl)) { + await launchUrl(fallbackUrl, mode: LaunchMode.externalApplication); + } + }, + icon: const Icon(Icons.download, size: 18), + label: const Text('Download'), + ), + ], + ), + ); + } + + Widget _buildVersionRow(String label, String version) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + SizedBox( + width: 70, + child: Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ), + Text( + version, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + fontFamily: 'monospace', + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/pubspec.yaml b/pubspec.yaml index a07a466..8425eba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,8 @@ dependencies: shared_preferences: ^2.3.3 app_links: ^6.3.2 url_launcher: ^6.3.1 + http: ^1.2.0 + package_info_plus: ^8.0.0 dev_dependencies: flutter_test: From 3f92c89bf160ca0542408e5240d8f65e7c831031 Mon Sep 17 00:00:00 2001 From: Khesir Date: Sat, 24 Jan 2026 19:54:18 +0800 Subject: [PATCH 06/13] feat: added notification service for mobile and ios support --- android/app/build.gradle.kts | 5 + android/app/src/main/AndroidManifest.xml | 19 + ios/Runner/Info.plist | 5 + .../notification/notification_ids.dart | 21 + .../notification/notification_scheduler.dart | 207 ++++++++ .../notification/notification_service.dart | 294 +++++++++++ .../platform_notification_helper.dart | 77 +++ .../screens/auth_settings_screen.dart | 58 +++ .../module_selection_screen.dart | 45 ++ .../notification_settings_repository.dart | 61 +++ .../entities/notification_settings.dart | 167 ++++++ .../notifications/notifications_di.dart | 63 +++ .../pomodoro_notification_helper.dart | 79 +++ .../screens/notification_settings_screen.dart | 481 ++++++++++++++++++ .../notification_settings_controller.dart | 130 +++++ .../task_notification_helper.dart | 82 +++ .../state/pomodoro_session_controller.dart | 4 + .../presentation/state/task_controller.dart | 10 +- lib/main.dart | 5 + pubspec.yaml | 5 + 20 files changed, 1817 insertions(+), 1 deletion(-) create mode 100644 lib/core/services/notification/notification_ids.dart create mode 100644 lib/core/services/notification/notification_scheduler.dart create mode 100644 lib/core/services/notification/notification_service.dart create mode 100644 lib/core/services/notification/platform_notification_helper.dart create mode 100644 lib/features/notifications/data/repositories/notification_settings_repository.dart create mode 100644 lib/features/notifications/domain/entities/notification_settings.dart create mode 100644 lib/features/notifications/notifications_di.dart create mode 100644 lib/features/notifications/pomodoro_notification_helper.dart create mode 100644 lib/features/notifications/presentation/screens/notification_settings_screen.dart create mode 100644 lib/features/notifications/presentation/state/notification_settings_controller.dart create mode 100644 lib/features/notifications/task_notification_helper.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 4848f9f..c878bb7 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -11,6 +11,7 @@ android { ndkVersion = flutter.ndkVersion compileOptions { + isCoreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } @@ -42,3 +43,7 @@ android { flutter { source = "../.." } + +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index afd79b7..f5b0b31 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,12 @@ + + + + + + + + + + + + + + + + + + + 0`=)KS#*<9VdImADB=L;JTW!N4#K1q zGgZFQW%VDKM82YI<9Khn<96-Xw^vKOo>qE&fu?Ss61ffVyn|fpq2{Z%lb0^)6x;WW zsR=d5$8`40IaPx~lWhOnupo%BT0FiDS463B7Af-W_CB5J#}%86ro{>UqO?(oYxa^koyF(xjm^L-s6|OIe+NV)i3M0Qy2V%Hud|zr5{O*`vyql4e->#B=mNa`dVXaf2CC_HCk%f ztQy4@^);n1zeRU^{7yaa(Ob2BuqgfLDaBV#saoyHub4%-a`_yqz@^uXkABJpdp}lV zv+RvfnVHPi;l~=jLj|s5Zv^iUxwnQkY2WYu@1J?_tu?;(R^i&KzO|akSO00fttSo@ zj}D$Ei_+FNCR;Ul`6krwF?-s8L|TJ+3AJztzSZr2g16(&Yodd}Em5ij&^iPnU{h~Y ziG{8bL4a!SX)<0@zzvx{CPf4kyvs1SDv}x&)#L1b$DZA~_3pbg3Z`YE z<_Zi6qi%*%M2Qn^9H+0yJnyT|CF-5JctL&IOl;jI|MS8N=PqkJYt?@ssK8+ut5xB& z5`FVtS)Eq!JJfK{Q^k?P9IM4~jvxZGs!B3=bJ`_=O)|rgS9Lw%=p$0>u=T%nW>OQK zwsQIwVJv|clTbTKFw}-4g3DK~5&pVk9W^>F1=GXzTQc8fF0 z5krt_+4wlRd?MRO0jTJAX;R3ej6taSeGL|u8OKU3=$6AZ6{Ehg5R}1dgfS~k7BU&T zDkbIhA46d!-^_9~@?3q!cC8v}VscD3Y}ujxv8&?PIsNFNhxMf|{H6ZtD__&oFTSYD z&?*Q%3IdH%Q3)M3h5E;3W5@xQJ zKJg*lf7>nEvNBNd;$aQXo>s}CrbMp_0@RV2N{>rpwEVx_D_p(`@}u%+BlV=?h9rBTZXJ zjS9!$PJlN4YlN&?vi2^`lft;4z$8 zL&5t;!34)E76gp}@7HN1`Itm0Wi?oev43zvH!u8$jd5LiqVu6r5bBP1p!w z4TsCbQaxlFL?xN!j8#w|a3GqibuGYqLVBWaiLq{4rlvCGjAJuvYIb&u4$N&;`|_$@ z{_dmt)|bDkzxd)`=^H=zkxsEn>&2!<1dqd%$pS5b+MYopRU>3vDn!ZW&j{mMp`%F@ z&==7cORC4l(2A6^J3M8rpbgqLwHI$c9 zu7Qf`GIQjz%B)X7P^p)in%Z--?*FyFt6M*O4|P`6Kl-eS%a^s>tJKfC8W6CKG(vMq z>}bUtG*?-bRniDuEeR^5S2?M3N%d9d>zuBtK|cl1SEFz4_{~gaMyal(dfnx39h`Wo zb-a+NR78P;l}ZH9DXl4`9(vuQH9$U%dK#`RYXFoS{rUW>*ySCt8|b^Le*8dXSqPFv zfK>0X>s{Cdct?AUI{psh#K4F_Q+=V&LS<1@)v#Vze@bRr|4Hj@5=9%}l}+oHr;Z2T zrC}&#%}ir#uRByYyrp4JJ&fTf2CcB{Kg;U<*rm%lzPzj-9X+Dw{Tr|-atN6cPQeDH z#@LSc-+i|nMbF_a6MR*)L?7@M zO3j8AS-qb+cV0PL=ezgqSD)?vvz+}mF~$uHcq)@BuOB=GaQZp8a>@p)tM%%?1)ech z<`%ryLeZ@TF$shCfv*vYU6n&7Yf>WLCa9Pk@2cHuP&eVQNAmX3Jp*kJvYbz2GG@@r zN~zUqszty+OrbJ}9R=|b=s)!6TgWrDSRn_Tt#1t)JVT9oyc8GCMGoLM2pB~U5(7y7 zdT(#*02wfdxvl>-@G?0VQSIdJI*H(1u{8=46d0fxG^rDU&KCI=-3mAdE@?@GF zo6wG#Ic>-J&$lNu#uzjS9Lt3zU1VqA{P7d@3MkNrgyVFsD?@g`2q=ImR1)M-D_pi| zYHVElcJI{w-8-~<#};*3IYEFMpc1*z79;_gBS8h{9eEpgTqZ6%21kxER)(Ad{R;8` zt<&ykKLN^gmh0qmFX=D;jpEBj?U(4LvNo9jylX&9Os4{~B3Kjez1Z zo&~{4<#I=7ECxnHHMlIDJk{%CCdlfYzYv1+@|NIDxO0?m^6IS=yG*XD-CLsXjT|=S z%N2rNhDNL>lp5f-q4QRcn@O)pRbSrV_29U@c7|SM*ez4^!_d%E75n6R>R`7l>!>$0 z%^f(PkN@Vc>K8wLzs5Ocy!70YQty(AYEUnn61!2!up#J_5v*crWfp|#0$*Z8oxZMI zPMN|h&zqULoQEsvW`?qBxqZsV5R{?I5&CYoDt68iHa?3clj^^1u&{8cPN}Bc%b9Z{ zc3^$?w%=P-#m+;<+{qicVwOpcSjv^##sjXG_Gp5hGn;^K;W%LApgQd=fwH8nXSp=d=ORM56G%&1|^ zEm0g4m;@b+?47sVroH?3>ms`Vy&#oYLlp{D^3MN~+lr}fBfYF6}hYDRN7yMUti*#_$?Nyx;^6lr9DJ5$xPjkif20E~}~)%`Ssi5h6< z_eQ-|tEI>UDxoK?;LOsR?CWkC)g_y0{qUAP9K$BK!x}nm==vBJIO@yD_y{Zg=bn33 zFTebfmU-@=2h-!@n!rg;b~-r!G0l%pXsX##hKvT6m$Y)?tVSo9hT0|#Q4*gwBUP&MsI7bV>ZA96LLa^P9T zYjENPS&tfNLPHxv?;8?9Km=`OCX2j_*v(qAucq_nd3*C$Kk-dsK9tOd5{K_|qhc4J z#7;U$fwsoH37EGg(9nROesOV$zLv^Z9JC0g8`#?{%auE@vyrnb0bGLbNVii8^*8?w><$4z*Rb(S@|wGTSJz1xD-*s~#`8Mx%rF~GrY;z(DvzBb>X2f0(t~HBA}@Q6zXIk3G6(GV8)M8DzFCtjX6kY!ZCE<+tW(A1C>)kMd%eo?>9Y^L?YHGP0? zy&g@u*Ygx60JiIXm&nzh{ZAZUWXtmC@nbr2gfmQnv58Jw4jqEACd&ZdwV6VOEejeh zt!R;BpvAN2v~uAhXQD~l2>SMK+o?NlxLNnydWUW~c!PYG#{)27WDF%tK!qL@X{1mU zYQa@RCKWP|?98;LCsYw!`EN zIA;*HOik&~j@_DKhw9`@hxMgD|Dyi*Pya+;{QB4Q!ii&AW`eB3%Zj8y%#;{^Q-D_y zu$B^rQ_PW)fToB9B(B#%Tn0HKegD3qYu6wO*S%|~_8zG6YOgj&IkJoAUwB3+h3l zUGcGWrPu^<@k&Tve)-nU74QOHn4Cob$19#y&%pHoG@Os&5HuQUgn?f@dqHdGFRNS| zD0qE5U^%GNZj7lrF{2eljd1eaUAwjIrkmB-zEef3tJED=HZdjTP1Z=Z4mdq?M*S&D ziJ!cZ8m@1{QhV3|*nh|Ex>yW!oXK()`ll{0>g?*8&fR5_GGw)#qC zOpGR#{J+voce-^jk#I~yoWlU{Jm{4}El|%LxYI5PUMxsWqGY3>syXpbI!nGDGd?^3 zXf$%=z&bzv9(I^aqMqt2@wo|P-@@%8hg{v#DO>>x{dbkc1TF|<&fv(89Y3y>l@(>Y zb4Nc#A6oDdF$_6+5ioEA#$cJP!DRxqVz8!(PD^`sZr6cB2X(`3x9GslH)-pR9cmFM zbO<0?tPm3V6bS$_9B~GY21cgMwt5R_0D*xhVYDBUY^LEIR{2Kwa4C%@B zrc4RBS-BPfy{;GkmHG(#N*+X4=XreVcxk+ei)@X@Q&hCJyQ(`VE|cK|-S!MI1_k-5 zk*R0~%^upVkALVxx}!U(mIu@?KckWV-+-!8FEljFnku4HX(+e3s6O~~g_hIbBva?> zx03UqjqB?CM$u1A67)yRrJ*0|_E)vaQQs;ya+Nu@w$>vkz#v=1oP`=%%`r_)&ExPJ zvXH?q`rQz_000mGNklBGl$_UK8ion25`8A=0bYI07E);Oeaf7VcuqT=|JW)9t;DICD$ zjW=lWzc!eh@A$1Xyg1M!A8ZDNz4xJnkTOp`~w_K9Bh-o#LQ@lT&VHtO&^fctIuyk z&M+_=;MmNJ37j;R-C7-la=g^YefI-Tu))V;l+W z-m_CTv%y}%p+&`U7R^f2m-27{vClXTZ(fi^Nzf& zUEK+7otV`0#JEB;({j|8GFIId^rn}~*~}CeU(Y+l z7-?V0INQ%-P=&)DMs%*#(9~F0^UX1}R|}nZ{CWN5AAU}M{6~MLZ$0`$y>#J%&J(co z7{in`Ss=w`5dMg4j8eK2YL~qjOE%D&^UI&}D@iqAT z0X@T{sq&P0I>4^fJDuoDuPODqLGvt%e(4u~Q@4#xDja!M!=q2Azj98iD9Ks_yBQ68 zShC=V%CMv6MhN-|@P5^)=U)fL3y0SgU(=%nQ^w9kquH$gfKZnm(RQb!HanbNmSwpN zyOtCr%M@di`c3&xfQ3wn{yLcXdy`)#ekeC1CGTbLX-L|qT%XoEa3PhqAL}5bPXOyc z6uk|{wADwM zv}>E1Hy+Svenvf%tcbZRMXmn?(E_Ja!DmwKIH4CE7yX#uwNv+e^ds7R(~a6gAaRgm zf&I7Nu3b0Yq&|vetjcD0Y|{d}0cTE~Q%<`UTeL0@-~NwTgbt{27>NnFf>ZOA<2ZbE zSdkj?a{Agx-9dQUb;0q5)q6xX8uitVrY+M`nxbv)PNb2mH9n>h=m&|BAP6xFG&La6 zM_xd4$O<=rzs-4h$oxI!juS9?Z=TYmg2x6c(H22K!EQs%3*)%HOOm|NkAVj*74q`B zo2i)*JiKxJA45Qaw-qu&@Q>=*F99;yq`&Q0e(8}ra8u+*UdqZ_&rmZ;R zL;DZtCdTHbTW{9hg9kLdWlo`!Yt#r@Vrzc|Cp~ONg1=nNv5q>(Awn?F$2R@jf}uHt z{@i=upgwTZP5RK?cj;DejkERNCyW~aS+l9Jv92b^#R)Uaylk?iyn$ptIGy3wM{<(ht zU;TUi`CoiVk3aXkF0e8x2x1a2!f*~~TcN8=7mfr(a3SRgGXOph12pxXzY-ieko=UJ zZs`pnp!N<%x&akwwSmvEUk`Y@>#6@!>u&M}KVG*|V&=Wd>l^k7{$F2C5g1;3``Fju zV@(6ID8um=&=^2vltry%rrzwV4*v45>eqh#S9IeZ^_j*2+ZN%m(QK&IYSqra-EOPHZf-`fD;z?=`RAUO zX~04YQ&ICLo{jzrJU$PY4stP=8Rs*I8(RRTUC!$qkrq)|Id7cUw0J2 z>AC#8K4mmv)>ZIQs47QtN0s_Lb#czqQxl4LP}XkX{5x{m19ls3)-k7_W{r&)=&^QIOE_AZ_}tHI?ZjfGr$=C|vvn{U%S zciyF&2*h?VK9e&uqLb>QGi&IQ|E+)`2KhE<#aNUe^)DA1`Y=Y&lq18r*(n{^zgu_S zakK8a;}+o7`g!-R?b@+(o0>S-sW#1y7p)cJm*WRt$b<@s$8e@{Y_`S#V zxqtm1^alh2fA!UG=;@IlB3>~k6sS3y+9?Y!f&+OHd zH$V3qc4{6FPc5$c!9UDD-UJfmZva7t^k{hXyA3a1+aJk?>6+oS3<>N~*koAOcX_+8 z$=7Dn&FAY(*Q@hsj);^bIFQAx!lx<0CrbisZcs%Y^+Fe#T4^=3INj9VU%X$x{u{rf zd*?bzFaJ=zmmkw;bx{f}m0?_o1r%y4#{)uv8VYZ6`^&4q-lW=_6~i7Rtfo;yv_${Q zt5QcTYLt{0rQ022s^_v8;nAm|Mre{RKZl`+Nw%EW*G)~FkI8} z$^uShpnxHdRx06;eW2r5OIvTfLEQs8HGjtq`tWc5svi6Ye^+<>(kIk9xLeCpZS~r@ zWKj(WWOzhZm=OlDhqJtd6ppY;f8y{loj89o(P#7(+@@Re}ZD zW+u`9$~-ki+HlLv)L?tBNzh_AAPQvsX#J(kOhKj$-eL^$oryfpRl>8d%sz=!U(kV5 z1XVxzhre?O;C1l4gTH$ts4*CCfI80yHwEi=3{i>m_zZshIOQ_OEf+az=`XKpifzjq z_8-!%H{Yt=yZ3NrKCAB7xDpEFA}Hxk8G_0PkdNSMGah+m@-gs0EYR0>x24WRM~7~^ zK{wrfi+0_Flifb0K~`uKOO5>7^p+H(sUn#0)p>!lu4vopwl%{J)NX>ou?8~Yyi%i~ zTu&>ERtQ0nd0?detgEjLSytdmv>oP|2DHf()Ep(XfJS9%ekC?2~$BX+f(omqJ_3s-D8?Lb;=vez7fb333&ZGs;hqY`WHXpxqjuiUI&f`)u^Yy8>SF@qjGP@E3{faGk9m1 z?%0jAiB|OmubZh+!O*J&wTe_#CaW4MSgc%VNf+kFwdZ3W)-QbQe%*$;G@tpQ%HywS zWo@99(7~?c3h0cuD&Wm-OY3iHd97!!`_oY*_>}YMfbQz*XUf*!n^cz@{a%OlXP1b- z+e|m}dd;<7`kJEm-b2u*-)^SD+llaZfA4>OusPO!usbs)KYJda0e%!PLLC(`yeN&2 zp9|Z;X>X=NDc1*5>%-`Ey&bFEa@c1jHB>e1X~asoV5OY%u7&e#Sf&OJcpOJxPBgV_ zs3a@?gCD$GTkpI{#g<7eqr`piRduLas=|0ePGVIoR+ClU)KF1p7nb$h;iEc65YXqj z32MBCjOp&R;rs9`JI9x`=1efYT$9^X}PCC|I-EtIl4^pB;`a0S!Iki@}(~= z^6(u5 zw*rWjk=Hc=By=| zmv82;IfchmLtuV5n36~#3<>h^0%EG@HNg971FqDA=z5+8^;uvE&M3O+$9`k@4_iU_!3!X)uVUA9<# zMchv#Ee%$6>GDM_5kPb(&l)XVTv${Aov~Jj`jMc)WI}0EQiJ|f>=gL7{ypEtx#q0? zgfM(7-Y0URF171VW&kpx1bpRiB%`klu+5H-YjSc-p_wU;w`BBv7?MhNfYfy-5>iky zV=N4(C?$VKd*4=X;80rZ0Dze~kb*&~D)2!T3Y5XGulhUfj$(}TX+)vPosi61@_5x< zx1+rmRnYR(%vzvQLjV8}07*naR7*W~{opN~=Lk0yeJ|Gfnj4?g?Kj@68*aQ&v#h$~ zSVslp>&~r$#|lT60MQ0U>2y*cANf=zg(^@MC^4CtoKFvgM(pGaS?QNi>Q_U8!;#7+ zRa-%$NJDfdb4N@7Ai7Rdp|;3Y%38U$ZQY^+yLW0lW-5CF)wE^u%Y!JLuAOu9+R`1@>ZyzR{+Axo7yjT2 z`r;SAu1BALS;sNrYu&DfosRl6@UDkiHu_tCF@)DFgX^h#%}C$Iiq79AI9fwNje?L& zSA+3vwN9GqM3oxsu60Wz7;bQMG@<6MgSzcQpV005Zcyj^dG%j5_beiN=#t}|X*Xa2f; zn11K&WYs0~w&4rGKBX+E>)Ya!te!B&RfI&08nR*xyxSb2ny5e28Pz@{^6ZddV?A|S z+pn)&N!((GL5q>+9i<4XjjFCPU5s5d-)YTckH;%EiS3RN!MFm!IbEO z=$AhS96-dm`LC(;m=xzOU(kt@$2c!r)y%{c+wYb9chy6TYUeEl0u!VmzbXP7cW6C! z2!xZK#EJVhJhq>ySgFF`yE8~kGPdtx<0B zotMgKfIG0tjTRA)se}Q<3z{T%L?w0Hku(}j-b2aY4&WxUar0n-hCKS0N=<^NM7ur#W&nq|BC4cC ziRJ(xO2JTppv3qa3Iw#ct7Pt*45e5Fh7Z!!+c5lVEq>{8sJj&M2H&)AQeY zT%Y~JFX;dMhkvX;dGz~wV&$SPHcg{Ol=e--*wwfk$2^`r{yZTchyx_hy^d zm%pc4g>%ac1y5Xi9G5S;Lvi<3QeB~f3z#Io^ z&@#}Da1uq78ts+m6Enb zTmKG5K;QaZ*9e|ai9G#-2Mbe2N zNe<9^a1A3{Rb{ObiB(Q-u%?x@Wi9oVF?fZF zW=RCPqCxCwe)o*-{K!4p^^tq^k$>=a^(+7QAL-6td_XgI-JlzP@jgv#o5c`SN+{im zjkL&;zt4&&p?p)U>RYUKEF`wI0O~}e+X%G4@(x0Y0WCEoU*Rw;WHQ>s*wv`ab?nT7 zzWdB`dgAC2EiNvr>J2qoSldR?~F%BhIg504erxQ#S zIy8)>%2w3;{vF!BYn!a!*KlQ7uID3YW{g|3OpVeQN2vvoOpZQjlnW>r|5CpDP!IrA zoV(^BhyevhQbd~qJY+7I{zx5bC?|N$(CZkaN<($qrrmSnIygJ2?&_*efAqi28CmbMeJ#8>7Go9|<84q}hpO)LdOp?pH*ft;zDXdz zKB$bbK%SMAl2t%@+kPA1t&E0VfBi4N_nA+z1-dU_nsT<#OB_+esf9+4a!3&cix}3D zLBU6$Ye6L|*ceP`i2PTuMmQLEGQO%F;9%2eAh&t#@wWEea!50`9h83|E1RFtzK?uB z_x*$4(7pfk?`iud?~`V`LJcvbL2V3Sn-x-Eh;s{SMpsA#NH9QbB4PotNJEa)fKQ>o z6+$g>E=C=KT4fkJI9C(?d_{HX@@4s7oqOr|7xc=r&+EkDBRYHLv@Tt^P}iNC7}u_C zTjj?UpwiU*oKhZjd0|OTuwhxPw;?*0Nv@a`^!n@|#AdEWr&)`Xai*TtDl0S9KTIYs z53bB6mGD?Gp*rwA-yM_xQJQAEt*qISLZmGyYH4`ggv65~bgib4M3-b{N-o?aMJ8`2 zZ%{@B$}1qr_(48Sn_?c6I1^8dQG+oZBiJb2A;VLJQzvnvUb6;o?|1|*Gg~jKD^YDD ztqWiOkthHHP#f%x-lWYGnt=}FjZ6V2U4gm;e=>NL(Kq8$$X5z_-DNxjbP1Wj2aisJ zj!{1WlGANunL3@e2E(BS1X&gJ6nxxhgRBe+4o*`%dqd~1WK1gfQ) zyENCu(Jox3aG(~vmnb0S5k3-NO5O*jQ{#q?k`jSd$a%+jm++BM$N6)}BI67_&o|)B z2wX(qr^-};gM_rIY0aaXhqi9jT+DU(m7{v(YY*v*fBr>%<)Mf4fZdJ`*im`cWVay?Z5J}h8NDMz$OgQL4{md&M_i7P`MFQh2u}~L7&m3 zit=@RaqgVrI}cEXNZJ6BzqWV9aG??n z(8xENxvUYjmPYEKR3q{Om2zVjQ3kAy+=%y4rXdQqW%o|qbEKPWUZviJC3Qkm zd-fgB4Ij8$t(~*#H!7_XnGU*{CJ*k?u3xxU;m{7PHG>ig+^m8+y`*kmY8R$X%;jIM zbEPkFU`SChMAa+ILNcOO0;^h4BStpjkn>?rjbc@e;fh*76KL@+tq!zu{<0P?UXp(S zZ;Gw49jx?s9o(<+9b427(s_)0qTkIev-02LUL#I#M-)U)^8ZILJ3FnZ=_$249Yts;zyuY!CD9T( z6|37|l#AZO0|~Ms&!G9383w-DY*3pre_4Z5t}}IA&pFPDv1)U~FflP9cl-|W7gSGx z*K>ff9O^vp&3M|NZFW^fuW&W^E1<^Z%D$Q3sN)XXo9DFZnVBl&Z5Cuk9^7pLv;BK^ z>)`HP$~dp@EiN#w1*WmkD5B$e(i+gqOeK8@1h0)ED3*>RD9ds=Z~nap7l&)P&#Q@v zF||4J@jo)upsoK(Y({%2NQ&?gHL6U7Map*9Q(yj*Kh)>` z?2q)V#~#s%g^Q}c7#P`gO1X#$ltxf1z*WgvkkAu%{2tgi@1vOLgF{8S4h6dfC5*bE zH#jkPh=6m&Rqx~~1a_;%?eey5ej{Hl3Pd=q$VL=0e z@j=McBUml6QdKO_bHk1{WmQ{4f`b%BG8;evh2r!5``RVI5GYXH{Q|>4PGV>0({<)& z{BXQKBMRYM-_y7P907*naR8P5*m|-M|2pbsX4%>>Mk>UJHt)LXWrm3Gv6}?ET zUUCeU+ufBB-g>g5=>xlU%SS(~zw_JwNT2-GU)BCS2efd2%2qBa0+ZI)&aqs3rYCjd_T9Sk=38~w?RV;?8*kLU9lNw`c3xq;t$riu_{H-C z01FzMo7J$>(D}thtu3u6#{g#*B>D_Xs&EVi7Iv+-sw|J1z$jNtuma~HR_b(x$)gQO zJg$S0^I$^55j(U=ljCEWo}E#5d;&ZT^upxw6hsUFrK*C-2)#gXP&+EGz@%oBkNul;(c95+oHc~ge|IJd@ld$)c9wx9aSWLN5B$g0!AXbpz7JYop-n7vBF6etVE zm}>Jfd6|JHnCiUC*yS)iIiXu`I;j0Sw`#DoptXfd@?(^g8-}y00{}m+8W}j4)y*;z zkb>KSVxB96sA4cs#k``5GIV?YfjtDvcWCSUjEn%gD*E!{8CvlS9&|p$Tnd@Y@*3KC zTU(}PwQuVdO=UqRpMP3k{^LK^pZ?q5*Vn%O4Lxz>6`d>wT5Pp6Ah@jvzVj+qJ`A#9 zg)EW^cpkALtZ?+c${WQB=U)Pz0)aX)F~?P3fzQDFGJx#+_j_0Z25-iMb0Ev@b5;bB zl|r+LWn_G@QCkn}*L|P(xDIZeSM}WUS^!p7RyDwu`SI&88;LC>?3XE2E%c_P(lVh$ z=;09XS*!Q2>yUs^muaPtB+rwuV9-9j4VTZ+p9#(X@_YY}Pc@pkB8I78MH{hF8LO!X zr-o7t8m7gp)B+LU3ajvu3CEEz<)$G!1!^@lxo4M}dw1wk3wE~7=#&4+f2@D1x+M4 zWJ{~UU^a1z?(~~D(Fl9cgsQDck3bj!UyiveRtnO zP_kR==ig$LmcVP!EVOEsj-NlLmDM#(Z<$j;FmicmN&ZiX+Ia(`EZC`lQVAZ1!vO}p zQg^JQjMaa^#43X+nd$-dNw_)K@{0nawOB=Z0MMn)9L|4gZc2?#Q=)B2Ul2RBtp-yd zFsq2UC)I`L6B(D~|A zr8{oFNpn+O+F)F9;3e-}PrOedWC|fyP$om55#A;Uns?63YS;9P^5veM{l<6or~mFh z=-&|xeC6@S^uo%5E<>}QwWO@0%$jvumq8Vrt4e-^W$2@W%ie>5(khG+`i5XDz{^E{ zOn$9@dhS}mDZLxu{5Ty?|Mr4QU=xsnXFXxql35)Dq#Rvpx76S$;HEoo*MlFsU;8_4 zt#BlI;e{78BnYkA8G%Ac!0e4P6$GCpV9C(I0&OVt{&oElaQQj-^fSf=aWebTzW>Yr z@tL3by8z+(*?Db?<0nr1PRW_MaAF1oEMLI6L<`C~Erp4WhMio?IN#M!%KtbJyYm|0 zJXTo=Pp~qYyns)2jb^ALp{on8{xlW#Xz*(tx9eMxLq5)_sW_ESLn8@Cr)$#3KXkgH01JQbM+A zG*DzEIKf1mo1fO$#F%0encxH~WZ*bxM%|-zF300!d{@T51JLtdwL`aq1w{Z-)RJ6o z#se7S7t%(~NnY-sXlZB;`To3f{-9__8^ zA{M8YM-?H`ErVaFQJEU0>U@rlMPwh8;5NyfzV~JQ$+VdY%6gAp&f|NSy?0G&wH!96 zp}HF)A%_kYjAVuWRM@N)>}5VRt~)PVT9W@c(*(WDg4$WG26PJokdalqt5k+k zk+bU#&UliQR2Qe;=r5{M_B2n#dT8go?!0lo?z#0Q-Fm}8?I7Uj;1JD>g;ZGwD$1e~ zt2F;j0bjNGZ!r`&z7fu$M|$q!C0#myLETnYeht2xPpzD8BhE=j3 zX(afrfjS~k@jiGv$K-g;=)1q8(48#Fo7^=Pu<@F)t?_s}$LW2+;ebkJj@j$d8}A2? zW_-|W^L{frbvZb>YdmJO^ZwBgF6HmX>6`a=IOtPH3e+*N$_a( z3(3q>{{*YYr*DPYlN}v8xL-S_rz8&!mzLGwm?1}pB0A*l%aBx}XC-~|U;wrh(PuH+ zOZ3;tm(=y&P#EIihZtiP`P#d8C&9%b-F#q&cFv7yoH^8hX2x!p5$vLRstArzJ^GTP z&TXlRYBt*1I=4l0?Q!YiieCEm5B2Ro|BAl<@DKFFu_JnMWkIK6sb%J2fqpe;)8dGt zQ3esKD~^)#(g@2V`YItj|4vIBR)d9$suWaNQ-$T&-=t9-q5}ms zIAFWN-zITyNbl`P3?vRDNHEn&Vuqy$|A*iE%+K^?z~H#vD*Q*E`p2I>f8m18oj)t= zp|BAQe`1S}iJ!ol<;uGqZRgx*e*a#z<~bj4WE$dh!gyEn`}b*T_byfKmPRxlGSde* zqswsh%*C^M`s`_)>-SNhOje;-{X#0G5TlAHjW}K?8UbZX8nW6M`DzETl#~rIg8og` zfY+RWt2uTJ<}h+|!+~~9Hg(Is?fTGNH{tki)XuH5YGdRgPN&4t7C4&HaLysefWxmz z?)-}k^10RYU`NV$2fd}si(0$9qOq|FnMEeTnuY`b4aNFL7(oT8vQh(9@#NH`pOJDi zWA#{pMKWX{cnDZ}qr{z1Ql-i&Y-=S@C{NPtd}}= z32-b%KOg}H$D^8tnNj;1Zu;vuYH-I&KR^erqyP@?D@e@1$xA-3lzGPcxej2V1M7L` zQS!dRu*^(GBl;EdchTen&`*Ps^58Wav$^H&B~qL6t_-8F8-ex}88tx0QuV4T6CB z7=V?0Mfnn|^HtyK&w@(YG+LRq%ueVQj;-#x^?>&6oYy#WBiBex^gl9bU_+64W0uj! z0JTgb@QUNAtuwPaG%>Ham86T0KBsU0>0jt8U-^o@@%W>9>ij94Q=uVaUJ?KhQ*cZX zFzB&{KwusS$RquQl&i05d70$AmK!OBZ|P=eYn<;?%FwjTkl`jBLU1q zt)>ZkrFuM6Kg+KW9`NL?0z>&$n@al+9r#qvS@6+g$2dk`g*6N`O5zwTL$6 zVUWuh)Wet!FsQ=@MwOLx$@Ws!j4C?~`IiJ2^GZu$q=o*HmKQFmWV^L>X;ItP3f((3 zuaDn+v+iTd^yV9OYkp=-d90MNHX_=(%hvh> zT|9kWS{-SUbJY>XaCm8@4gfORQdMTA0>|RKj*6i&`jKP2BZ8`(JdPq!CsEl4o8sE& zdESkv^A)J?Uk2-BxwdbaufIPqv;rPeZD=bRdHq+xa7YeGw()~+qRcBMB~_|$d@Uw) zgddUT%^TcCS&-q4ERiK`a%?a)IVHpbj*>#^aQkZ>)^CVyI!!ZPu9w2eM&?bDZT{d+)kS+X&(k zy8s2dM}Zx*7UL0AXi)W45GXP_O32U@)NOS&8jjRM*BsZ{XsCrAyk#bQvPyn@2|hN~ z*1-dNb?-g5>5khEX~))Sbs8{NtTBFlWz=&y8cJ1GP7S@xqVhb~Tz5hTr{}b#3|e^R zMSbtj{zBjU+`eY-y8r+X07*naRG;hNhrX?6UOudo@VL;(G-$UqVoq0E(6_|y zLW8-IAr&7pAG^ed>tuzjD${%Rs#!xs?^y=l1;)JlHPpfoqo^yF7x}x*u`C%+w}*o~ zsu&;B-h1!R{U5nk+n9&F7hcu?eJ;pXjDN67MJUwIN*P8hCorJ+3hwta*De8{Lk@20 z90E-p(rENEeGkBKfa?11|M6dZ=2Nrt^ZR!1*{ii)PbW{FR52W?VkKT+G%e`u~P@`x>MV4xkw*%Z~(B&o8K4?y>sH6qrt+Vs$st5a&NC2WrrdMwTh^PB<4g^q%#B zb%2~didmz?BuZG-N~MZHUtJv29064%AQ%!xqWbNjOuGDDV!1 z@_H4YwPPdjXcNijS+0tD-qdKTH@KX~;N`SwWSOSn!Rz}jK!TX#-E3-JZxeVhGnv_D z$u*^3y9N-`ePqt~u*1`Yb_1sslPQ9;K;Pv3!CC66%gkOw)6A}VE4}tfuU^p0z(3X< z)2jakKjf0Y?=fUggo$oj{#yb!ax9RY zy{seOepKK5<1gsTpZ}u1^P?y9^3tL%Qh$wLyhmU9^f{3a%;6F}Mn%D`8qDAEkx1Xl zP1Aae-s#kNa~huhtpyhVaAmB>*ao0lSHwaQkA{948fqHkWHFm-YpzX>iuAD_UBII8yjPyhS>`j(FY z!nH8)Ms-Tr1Hypp+qYl-t?9$8)-PYaBr|h6pb{=K9E(*!<)Bgtv!U72%${AE!}*Wx z-l3x1)G7v69xQ|$qm(HOO>vMk7Tap&Ev<4+`r@gRdidF=^~}j*TEy8)rbfs#VkNYS zQCnVH*7?(Cbo80$bojeJ)TxIblf8UK2ePg{dh6}F=e|2Ne#2HRO;%dW3$038MHdSU zeQ32+HFAOw!L!MQEV-dw2UCebOMuwM0xE#EDVPS}Ie-2<0mpfbu^LL9mKHIjOP3cE zsh5W+y1fq6?Zc}-H!+$Sa!PFJA{eHK(IXRRuK?A_CpkZ$Qi-$i8h-ZQWfkUMfU9!J zA~a~7YS3#h8oHyZu^aU;`s>fap(G_7jDG>p9bUbXeQj%=tOoz2yTl{wZH$7+i3zok z-6{c)^MxuUO*GRdi1kSAsXc-;^pZaw4>R zWQv@fm(X5g$0|&;wg2`T_3=-9RJZzEpnP!QqK20)DaU~~LQnwAg$|AkJ!&MN@T zYFeo{8p?V@WtpjJntHKN5lgkAbo=&Q+Byol{KQN8!C!t;U;O-+^o6f~OW%6#IUVN+ zVvS&7nfbH8&cpzvE*KwixluBB+ypd@()aK8vIH0|<|Us#XyjbSUk`Fc2NJqkLquoL zo7LRZ#Fl^bmws8dY}uya3(slg*o*4-E@L^R%vx#)-Q?_YkQMU9g5JNbO9Ghjbw6Ot zCcqm9J_7UK7ek>)Z~4oBjt5lNe<#_eQj*4T1Y5Um(O@vp30CX==WT`YN-3y;QOYq$ zRhd)(CC+LUriYD8!#qZKR-@=LGi z@~Lwgp1Z8e$4~3*%SUwjnHO~8hfnIn(TPuG#%N)o+_F6f0V? zz6N>FAj=2W%z{r*uvML5u0S9LlN(ylmGE~?hvXZV>OjVM9^Esm(H&Jk@{UOgAgux zpD@msJNyu%<~U|(vu*EK6@yA9Ej6{?js~<{|CHQeRq!liYPMOrUY_Vv1rP4TBYb+E zrdQq6uaS~c8Z62K249hS-LbK*CMPHAacB6b;djl6XEuOmUYZ#!*hbl#CtkyWAwecd zCICJ~A3Ru^nw-@BeS0;>nRRl!pz`k}1aH7IHzlv$W+t86l6wl@;}I^a=V^i0lUB z=wDmItVKRkVeo=3x0*qR_U_Qlhj!_Py<4??rmL}B@*p55sLU1kF(rr;>`oPBPdWTC zCA4*RM!VPz7%r^nF?IvK_tl5>@b`bHhcWU;Ru;94{`ILiiY7y+%^a5q%m5}xa29~~ z@ArlT61w|Fz~D=!*IuY|Juj|G<@UhAD?2XqYltmcW9+xwbd&DC=Wgv_mviC8r*--K zNv)u(v1+JOHdMw+{Wy~EY`vDy`%l**fs*lYsKrkM9~<12nilbs)w{a`@Vo!wfBTf7 z1QElNFi`Vbw`pp2S|`q)(up&tC5%aqG01V;8BVLtR+uzeO-}`}%Jo&87y!c8T3`|>7 zH}Bk|58QsI?!5ONP4C{SG}X~kD77pi&qh#)QDrifin7>{Ww|Q)QI<)*(o0&`_CUH0 zC7d}V8FgIXf%XPe^kD!`=gyo}e|4>Ph9#3?VR>0={l3I|3m8@?xC0AGD%s7*;lTp~ zcPM39@CpvhTfkUU=*7VerD*7N+weohfu=N_O z(8+)lQAx-_&=6?q7sfm4IxnvWGgF{Zqz@VGGx*P-5jd*ZxpiLq_UzWw)PySZ0+KQd z3h=g$M{jzW)kJ^ny+PyZLZ_W6s6*gtjPIyFttasRBA?*XcP=u^L|4!~Q-uzUAY4Xi2dk%h?z&6&+4&bUTw^=3Yiq}Q3`HC((K%{Zo2WHZn^n@ zcJG+gcsr^|RFXOK%(D#K(IkN{WkCa@9dmPhVp8)HQ_5FII`!Cddi3kx);Avd4o3oy z>G0)?S|f1jqeC9dS4b*Wfn5w!hPCoJx$n) zkMC{nev+V2yISU(X+0n`NWj(iW)D4m6*g*-82aKkO2)ikXE&Rj)U9{luKRAhQQ_P< zEgwCqwdF-6jz+@}`%3+S1_@*)|3RH0=sMxLQJ^0_XIw8L83%zoQCBtSCtQhluLp|W z(*1foo6pP#pG!X#HtUmeyL{<3x^6WX0!Vd#Od>TPI8bhh5+Go2jb}OV&X7Y!S>)Mx z?|#Z{c~rtLmA)w5_q+eY|M@Ki0YPu!%BuW!0*-fW&{Lz)pc@9bSunW~tYVNmv6!S5#r zYT_d2q>DXit*6MUb#7)xhYlRn9YmhDedu28zT+0PcWzP1>Z*@$+)xLEma`2T(Xa@= zIm*?*ndbx**{Eb2v{J-KXVi;8V4RKAK?q&WamZBQFvVbqccmo%o!E1iF6#1`3o>x} z3N7Qjw|8k#ek`GOMn4k$E0v@C)oM>IjAJ)zD1xV;%#F9pAQ|%TGAwWjWIQ*(84`r3 zXU-G6w%}uYd_sm{HiDN-8Mp#1;W9`xlay3rGbWw?h5$0PKnbfoe;>ftNm9<%ZQis5}>J4zP(%R)E4f+H1k;0k{r93IuP%({Qs8VUB z0v++A0Y9%#&6dXIX0`7I=F6S8>d?X6njUMa9Y$(f#Y#fT7_VfRhGkS08e(?H1D(lM zR|jV2G{JWNrKeudb6@|S9{%dL^!4w3Ur%2=rxWPe60}lis=;cnGmP+<gA>(vPj4$BvV#7sFt~*e^iF=#B-t#fD^7q20Yi4()?Y$4Qkg5G#0qcel2 z4KO3WSphLJ^){dC)ZYejULH2yuRrtfCNek59hlByXw_mqh+WlHj+bU{yFnlP_{Vkk z_Wf!cKd1iTmt~_BG;~DWhPv4#TAwTMW6aqK{s!2)wT7uj&{?7zSsCOzW;r^j6jh>+ z35$?uvz|J2v<2o<4N0%275O9?Ait`wRjANK02>ulf}#2`@bXPC>N&VU z@LgNDl3&h2pk;u`2&}Va@M$vUTLd!tnWc<55dl(VvMLjk5Xh91?+_)mnc7(eR_04$ z(UpO$!NO>G@uHr6{P9oo=DP22-7UO93ZIG$r88gPNCSp$Zu>UPZr`p;D@!_c{Fv6* z&SY%W9q+PYDzF8ZKvpb1*{PQ()^TXSEYCV5_|qUA%l&7tWv7>c#Ww zvg+ACHlcfV?TUQ$(f|Mu07*naRM9Ve@Pqn|U->0{`2PEJ>+QE||NeuT*|J5}X=#w5 zNFiYqUHn0iP^e59$`-3g9!NsCN$Uemo@ZTwAknV`R6tVT6@a1&;}vj#3F9uDU_s4u z=P&5=u@mxb^sL#G289ZC0|uPqiux%gG1MwJ9vQBzs!97c0YJi|IENMF&~w^T4iE;XJeAep#$cAf!hD52a<MHZtnWK+S*s;{)^R{>AH%I-tZ$9FIosm*w}{4+DbuIQXx zih)mu7;ac#+%XGhqH zd*~5;>#x49FMsvxdibd)b#iT03ykY3{T=3+3NzF;G0cu(1Cn^&fO@~Ju5BT- zFqs5^s(Gs@8{I#YK0R^A0p|bwWi;X^eza7N0&{a?U*}?Oh zjz0-mxO^gSK!UkdN&nr~Ikvz!8I)YV6LO$7j3x!a;qV zW0lZqwRDNC!KKAT`89z7FL3T7Ru6&B8b&5zsIc_BPNJypy8CvTz>hgs5(hydH*Gvgh==VtY*(i#)bm0}=a z>;f~mL=m?Tcuh=BaOBaG`wQ<=0*}iS|Btdtu`(i9lTlzePnx>=$Ytq$Uay~2D9OLi zP|>e6WS55(@Az0-yZ7wU?!9|7wPi*Vvr{tU=VQQ#(>#C+O?172wd=iYqd_7An{p`0 z_u-*rB8u(pRrnZ^A2JbB9%ZDfYb)vx*EBsnu8c`Kg5Q8lGxRvRTnTg|V;0a6;ovKJ zR|Pos@I4oOFomWmW%L;s*ib7)kHB>xu{FPS&ko&l?|u64hwj%0?!H(1cJ5S1(vYA! z5eQ`co&teMj{vkWOlm1p2J@~m4v4ZI=sHikfWG=zi@v!o7x3BYcC;6{-g*12I<#+} z<`_panjpr0+lUZT;25-_}zL z=X9h|=yW$|nO!)!kt#BE2c|B3HnD_RJy!JFB>%NqKfV}6@z2 z^Zt#1S8v;$Vo=$Ls-h{ksKzLD3@@yhpM0)-KT1`sel_ znW=^itlCJu4n{}o7qn8~(Sba5>NXZ|_(>yz@sW|nN~7FVGQn8P^R7@LXeyXf^@4y! z?=XzitNL0g*R%@s!$3u|RM{#uYy*?&`#HM^qMbQcM>7B$#kO{&nX(4e;a6v*KYLR2v&|;fioM%X1lcy}zk`rz;mMAs3Sw zbSN#I3zLj~R47Vff^=}!k=0d29f3!Ff!7nNItgkg@lm&PzCACV`D|mv@%m9V9a>lGC!j&+vk+y6o)wC5j7o3IZu+H<#=KU zJY3&RH{`I{$}v`SpB+e*j-LQ7v>RH+xjW51j=h3DGgrm1uVx;ZbR9)_XtG-nY2!ga zpq~+0Wuy*rUPG z!F%L?M)=UKt(tCTa$Ew}>V?Zny+TrTIPZB$m_&XvH~@z)&Zh!E!Kfo66IHczejm|PCMv&FBCh0 zF)UuOWR*d6F7Q zhG8_y8u+v}`EDpRp;D6s36spl&R~@}zo=^IvUK^9mW~|O%isULp84*#b^4_jl&~iq z76j?=iR=DtfS~KY>L|m^6fnHCGhKk$!&H5-0>7{U?_JZ-N+kSSz40eu%jfG zjYBX5MJI=>j#uk%J{KA%h!_K$v4T}p!F&IG|LN^p)R^eXR}3Z5)BxoZ2txD$lS&M9 ziNUJ8+%*8)+^-p~#JhwOb4OEyD!6e->*+>29$<{mnt#zx^9O3N%y_AuW2{cko*v{e`w8&M;K>JCHjZ6PSpNds4ej;`~fnVdCG zE81Sk(^=n+v=|j*CjT23jy}&b$OaX?RV`n-s0rjY!ERT{*7@4B0pqQtq%o{xYyc`(=XQWpaG^L z`0Ois>Cq?j@I&9xHy{3<9)0lzoglbbV_pohCWeo(f*aS9DJj z_uQo&iW)xmvQ{sgQ4vS#Gna?xQ-!hy+T>)|G*U)zXfW%GEB^y)B?|!6*edjcj(8_{ zzuqxAvQgy4uL1N-04gNC=Ea*eSTC{kkp41#9Z|F|w*2cK*D4Mlzrs$e)@4+=!{vZFv|M4lh zwhxebOG~s(3LyYdt#(H<)3fShoR(Ks5I^tCG|?TyKrUk22g-PMh_n>K8DT*CzB*!X zo2|BTj7D%psw8Y1#y_ghyG!g4oMrp-605H@ggl~70sYDq1!M`qCKSd~Udh->5|B_h z0ZAkHA)+*gQbw88`2tQHAV}0z4;XmD1{h_D!7MR`6;3e4paJRS#U-6PbB?}Es6+n- z)LB?qkPmvBKEN6>2`bpSFLB^aoJBxR2`)+;AO4RVq~!_&AWB~y1^Px7>XP@x=${d7 zxAMsD${4F*95%8qX-cb&)*^i^h^s&f+IpTQ(5g`TO3vFZwyWDXsUD8eos7U^^fSZ=-Xzh$aR!2)CQh?~BMsCE$S@)Y zhm6!$hdEBKOF#GQ+@@P@I;0&00XfeFd=vx)WoC?GgZMn7hI6-^0qhgBo;`g^=g*$iMR)uh zd##|$eReP^`YrmLH(P2F7&p4(Y5|?`NsUe5I41#i%NnfoBYJPmmMX?#1l={-t^jL! zP>+6(GO0Py(v}@_y7{&nv=3+9=`?iy!g-Eg`gEVck%MVd9-h`9F2*l0t`${WZ$`+w zxGZ6{qb95vFQmoX=Dh&BZ9dvs8 z__XG#hKgesbo9GV>4#tWj=uiIuj}DQAJfYhFX%#)mI>Z^7(M^zFhh9o02;2?J&_tP z)WGWJK=8pZspgZ^$Ha@S3qV|52RIEMcbfdoxa$tpA6NS0PpH6^=eHvh(5DFaoUgy{ z5t-HKRWq4}=vA8Q>h_O+P#-#UgBmX!)6&z=svuAvn(L3(iK+q>(OV;`4?gJRD{z(= z8|N7nP=Il_JL{l&sJs)A1ls@r5CBO;K~(%zFv?yFsxQY-XRhfZF^gcmwkrvYR{(yz zT%_xWUZs^NaDFS9{1%t<={#rHw+1*{*h1e`Ytz2vW%z@1h zrIjU>b)BS6yQPeJzRC$Gi99mGm{g-7I|FQO@SHW9Sc4`5fKpg43k{XZ)WblWU0l^; zFTSkrJ@dSN^ujB8>g8j4;rJIg`lrsERc~okbCa|3 zz-NGyUtmy)E7Zb}W49&h4~IjNiom8Ji$N}@3NDiTQ!?<%FB}S};fDu^`P)Q2fg>QR z7S3d1VnPT~Bij0V$>@iF>Wwd>7y^l8ovxI47VSMEZ8sarqN}XekP`Uh?;U94CGv6Q zP3C1ua+J=KzpIeTmMwFdoEX<=)Yp*U3{_BwQA3;tBcKriDUo{xM4WkdtfL$`7=1D( zLrYSNK4!>i1|7Kbrkivp=h9oY%qq)~7qr&{Zc~DIg^;{2G=rB=%gX~4f8K-y&zs<7 zX0+WX1BDmKgRla+Le^%RrN$~ZD8m@VQKd1kZJAe`n^l^bQaL#(o1Fz_ zWW00G{Jg}m$7o_qedf%dovYuB>NSMZEtNP5*}89sZo2C>ZQZ*~=UFLSTwPLuJlzn{ zMfv5use$qG@v8_RT^_O^?{D%kr{85}8e~bmP-?(989NKJTc&jrM*?>c2prtKL$lqs z8jN#38YpAFCg#N`uheHwRkUpq(CwU>)4s7;wJ-H__R;6`)YreShrjk6edVEV>xmP` zw9pLl?*y!Xf56c|i7r=_DM!xDLTZhqdKJhV@ILq)!zYL;-ks|Mqx$s%P5!#wW+pRx z4cqk?E7j%x%0X1|)ZsOie})H8Vp5^QF;`hu@?(Gs9TXEZW3ra)F0C{xP43^Ok9_c6 z-8qF`KlL&}zzg#KY0-lj4|4kiyG6`ZDZ)*J)l7+eWwCzF(2ooyaC#Na)9o)W>f8Hj zW-?m`$>%s$2?g?ZdJ9*H{&}-NUmR597K!6d^2_)rnNQA3h$h&>*OV%m7yd5KqT0-@ z7S;3NNF|HH#p5S*`uXQ|{K=ekAFm;eBgfl)_?SC`kmkYhx#YK^hw=0w^cis7o;$@L%)9DHa)QIfbJgO zqPd~8#*X5NV@Do<#cRI9HP`ftCZY>^#xO}$Ef_OXh0sl)V(^>ow%j20F%~17Vu7KL z2xV$=Tnm>jX~5Z~pPRb#H#(lE=Ry;!tsyvykg3G+S0Ptod%etCS_=)GXBGAmXYNlQ zIjJXJI;I~z`;s0(pg(wm^T#J2*JIB16=Iap)_F7D)7)<= zy!Q6~Zcf(CHx_~d5OG`q{_1Xl!^{}^Qp44r663gZ?t%uFmeoJEpbIY_S8;J!TRIcE z?ayb>g^QJaZB_ts@uD>ohB;Q!9%)v$~`e z0c{$k_1KGe9Y62%kfY^#~|fp$~ED z1(3I*cJAJ$TR3*O@xWefnH*OW8aY8-gLWBm8xky!GWyD#ah;rOc67tsPK~WqI{)-5 zdf|JI>AMeoS6~0m!+P|k7j=5j(`qv(kck=?jD{#13CC}0)TtEN1%M^yzf|kEN!-^5 z-o@*H*9c30^DdX)JJGAa7&dU!F$6?t%vGrHjOA3x+#Cd{nrLeO9XHkv?ZzTk_0$oq zp1mlab3RXc=vj}tBj$55-k0FPz@`eW$p;6oj8de8ek;kSA7UU4D)BArP(j}<8$6&b zX67|{NY@NB5naLDR^YAV^eXTt4+=`2aryylIrF*M%oUvn8YSkPFD`vfWPxCsUBHuv z(f`LE(-YtMj(+gfujxnMd`KtR0g212+A}_-kKcToe)B^g)j#=_-_)mm>$mmK|Fi#0 z|K+Ftnf~+t^gq#W|Ks1%um9$+=o1fqOb`CXC-tFE+^M%M(tX?Z z>W=Yw?F$`sc)oP{jEJiW|1FOAn*hczd6&v8oVi+9#Z!$ay0(7j)tgP>@+b zFuA&8V=8bmt2p~W9d61qWn|8Cw$VV+hCmC>jhhHEI=*14VpK&PH%?V$Diu+tOl8b9 z%Gw&nw)&}|6%5AWTB(bRYdU**Q72HUBPY-3rK897+~Fg7_T|HR^7$9^6j9gHY@auxKqMr}SNy13TY3C{jsId)plzi?PTdg2*9^62Aw z*Y9?^Gx z_(Of?vB&gX0^Vnj9DycXW&AFqza?60@RBHUIcx%~QS$0-uV&?SD&*qe2D=E7Lm3mh zHs}Na!SwvJZoKKBZoT=Cc5U0DP9D`jR$b2KvZ7+Xlv;}pC-#L<`agYPnLzx0>-E@$+| z33@YZ|Lunk=^y>ZZ|cAOXa7w9%|H7c{TKh~KhdxMgTJHuKlusW{J}f4o#VXjmTASY zrV?|m(QefCxdMNQ{dn@>@9WS0)xX#A$DYy7Mpw6Q+oNsm301>F$1a}LQ`bnS zU%W?m{PTaL`~Oe>#lF##Ut^&T^Ve{PYp7FK**O2N^ZvnDlO&i;GIP#4ap zOj6S2r3Lw*`0Fe)x$$50FFGwQD5r3^x+?aL)oJ9)Fd&K5M}^?m!Ep;EGL2A*!XmgD zQpTpStwR}F8mXav6}2*ymN+k1?2WXrI@Gy^Rh_=Lq+@3;>hOs(dWoHb7hXB8r(Zlm za6o|IM+q3^XLx@4#aHwkTf47d^pBoA#nHlff{P`s^!pmPGMJQH5m2y%B3@it(1lBv zHP#tZx7FpHX$__D?f#tp2lx$1C{hVj%Co2jkz<7eO4N2hSTZx2xG4A3+tA_l8bJb$ zKtO<*G}Y~@g@H|!uWy4_v^9nLQ+!H-CIdod{SNT!FTB`OLC-6>1B|@uf-|kpmh}dF zdfimllbk05={opRpt;qP(vmSj#fT?uW9xd$?2JYPMvRhjWaL3WZ)r)zT2Bp2YSVur z1i9ni$u7X`%mn(-)`0_ibldGWYb#}WGblx=pnVyFDuW(940Jj|DOT$=O{#fIh9r4N zfZpITGX?Ot)i4jw$Dd+)tR zhe)?=-KNg?gi89chK?@N-v6J(AVz}ITz!x(5=5R|SkjSGX9+%E)WeVbP!Hqae*pXt zXaD%aKhP83{h^+I6dikxx+fmj@E@Er*!$mSuLEppo^!^>FmkVI)y$S zK733sym(kov*Y#Tb1&*Kjw2ra!4KK#dPU339}lu4eKIq-JP;8n!=!Z}sc-MHXHYfy z7<=B3tGU2Q6X0!&AdONxq+LLYVJ+xQ1+h6*PbZ&vPES4jsJ{2m_X&EQ(5bVR#3q5(ENGDB1ocq`W8}0q z$I*FZXpyMiaS)f&BYFS69tqS!OUz0sR#IR9eH;fdD2G5ax-oPjFNz2?CUx7h)ge`55KTUc>dK z1{pPt`ZT-FJ29s#cC*+F!(JQo6HqaUD%Fl2KVVgo&o$?7fUP^nF5TmgeqRrN^Q-#a zx4)@V$6nEvnMwWXFMdLw`ltVy{_B7KDShgn{pb4afB5(H3%~GD-EhNxO>s16vtt@g zHnr9XT4_{T4W)W`%mItck^sAjKhsiQ&wuSWWSj zYbp+G)82pZ%ewc!`cI!UD@_0Z5CBO;K~!|l@BAa}|BVOKym7ydtX+CQ*LX4Aud$}m zr(dmEiydE8DJ9PvqUSAHv%m^zN*P1j2xOs9?>(;Cj991PSV zTFi|>^X(RlNJ0=+4`Yd=sQ}4@$UOjXz)-2;xr|EIRu#K7l%cJH=TRakNKNwVX}`#{ zIy5c!la_l)m;GDI%YB_)SYvgzsKY1E=|xs(&mKOmCkY6ic>ZNQ_S_44;`tZl{|5DW z46vW~FS4~W91b*K8}uAO%w^7Y#@YVQLbkr!G3av!K0+KbC-@K4G7 z2gB;A{PKDx&^!G~-fo;-g_;-Mn^}z}U6oP{mPF#}F`>b8MEdR$d9VV@B`)`?>(XF@Mq+5W4Hy+UZ_Bn-Cz!8)xqEy1S z8EV_SU>o|V|xCnXL0h-c;%Ru{C`+nTu@qEl`Z!afDB#CiC%L8 z*T!0*oGm?v&?BM+DxOC`pLc7(($bpFVMH&SyQpedXt2B@528gME0Z=Mk#~u>PF(>C zHi{WXDKeKb=23>h2{cFwb^HH6d;cBn*>U7|g5Suz_h+7eue?PS3Wb9A011L5=v}j$ z?3UWMS}nC&W;EIv>71Pz&F-AtIkU5W?5=jt7>#x`nvrj{H}s%~2MEFgRWJpV_wLnm zKX>oV{lxv%D^vjm62&Gd?ohXG-1LzVk&%&+neV;wYXJXEo354WTA?`F&}yy(cHjhD zf6Xx4?p<0 zUc<4O#h}h-LGyXg63_!tiHv1X!T1vBKRiFV5=hLt<*~{tDKWoF#=Tuu$7;F`GK4D|YQj-m;XZ!B|?CDc__2pf9@cwV< zoA-TL2VZ|xQ=?6N^aJnH-~FB6)<5`9{;q!exBiMg`q2;QhMTU{n)R#UFH?^zvp)Rw z@+AMunDbewMNZGUSyDp(qua);sRnXeJa|ISeBoRA&Yyf$pt~<-SpePtZRPhXSC|h8`a<%c8PiO?DISI`@j2#Kl~E_gt-Mc_d+y6N-XSH zVVSv@Bp-}KjSPWJu9(t7XHlnFEPXKyGBq)rQxoG_U@<+ocb|IfU>aGJI|3C)E4Ypd zD&&d~!5;)R21bNN+Ixv8P#$tMVuSv2P6t}FXsNVT9TqJ91Fi}BWm5zH>Es%kD%Dbn zgW%93-m`{!h`WoxyD3u#>&2~td>428?7a3JJE2$hACzAwJo~~6+PU+2z541adVSws z?LT~2^LSyCW!CtPt-Fn5DU!t01B^c^sk!NHqPFb%^S65<+SE!Ppi-UwjYtTL#8&2 z3})xlIdw`I{N$FDgTk*3u3+~+Ix)h|v8%o>Axw_i0fOWdEW1SMqXjZdkQeJGO1tmMvSflGBXP$fQP8 zSEV|D|G-;6H+369eVnjacD{$Wm*074zaD%3d3}?+zOR1!0X_KW<9hb_UD~_npiUe+ zttA%t0Umt?-wE&;Y{*_72Zx9!^dzwu`my4m*VUlYQPt@xcKb3MfW+co0)xeY`rLi> z7D^U=sp=NOhNwTcAa#2jxele^qVZ945YKW*w0@C>YM^2)O${7?211WG)h<`EM3(#% zV1RMzTB(Jy!%n@@{Vq+1-e%Fyg`WrYe&E~8lmV*3bp>!X4D|&GG3#;LrR5w zJ_h>AV!+;&^0|Bb=n*~k@WcAz=Rb!N`=AUO?tSk)`ak^bzoq~3AN<M}On5>BBey zTeob|*mzqk1K6;n`bsTS>Tyic&n1?bT4ot*MjVvNxJq+~{gU})n&%Yo$!|QM$M5@= z21ib7$4cz#$Q0}7th(HnZhZHxy5%=Mt8G8~A?4dPs$jku^Gl1;{wH77eZTu>`tG+L z`@m;!xn|2dVL!Z+N`Cy0JT9)v0ILR_Gbn<)3P{+sQqwat4DLWDPn}fBy|9!tV1e{S za|LCmPM*@zVux`J44ahsV#u+oR4Iar@dj_AEXm`66eGdll&Z>s{BAvwS1Br4uB3*N z0m@XdpvSB!g{CUcqr5~&MU`RB^E$6k6Z4a4K;3~d_1U^~N>dl>H&Cu#oepBt#dW}v z4jw(tiNId%;$+~^frIKp`^dl~M+}}aB_$hz&OIcTW1<@+>dWEW zczHh#{)3Ie!GIVoKbAz&0M+eCs%~cI(sii%F$zek5i_k@vs%})a~AYf{jU7?#8*#E z>iX^5wR!bwApk}CYU3P6?Y zBGqU){EC?KRtyH}E-va6yYzi~_iFE+*L3vAVU3J5wf(AXy5Yv_+4XPJa)oA(USr4DLHCHktLNfE0sm5kw>+-UTAZwFhYN$v>jvc+Jr5jfvz-U z3P}oxt?R#@2)@D{3=D};X5yioZ~fL$GCg#tGx@me$ksBhI`n)%GBgjMLIwXmK8EA& z;UJVusiU)JTIgjQR5&UFWu-x7sGa^EN14&vfFmRKij`}1#l$L=rxx`3i!bZG zFMm~k_SO6J!l472Z)REmIvDv99u1vxdLp6V$%7&7$x}6`9;u{ZNKn6YR);^>$6LQ+ zpliDX)MS0n?hX3&BL%(9<8t&O$o4?X={n&bG7^*}=3j~0GMwnaP{Ph@z|J@rA=ajD zpM1Q4WKso~k{YG3rXHgf1DR2-N?Mv|YHHgC-E-IN+QiA@QRdLmUAwf4d-KP>exIJW z|C@UJ+g}5|uJ1hbEq(W4;L-c_^kWa`Me6K+;yc>+?6W$$`(>R)Z;`(@(w(r=k-}xKAt^evD{ula(Ec_q8_x;+wWivb@J=VE48w4q-UY~+g0sceK z1@!bNFTtN9qt?LW(4c~cV?Ej*oz-`~aG$>R=bu;LbZy(ZjhbqYse|oUaoslE{wu$r z8-C@p8ohCw7RGb++L`k9h+0*yrB@H?<*z-Y6FYZnlnrNV<=79;0SHy9?-X2UETi}gZeGMLU0Zosg&ARy0j`JV@#J#$8j98-FEhLH;)XmWZ=>o%-MB;;DA!~&gI za6`z!6ETaShDw1lgjgkPc_pPJSykuN){8xVOf0evEa!n^7NezsxkA{27Pnyroo3MJ zAXAM1oJx!iyd=U3*Q`o8EgLF0MX1yU6W0h;OI4WBLYmZEb3${SO1&d<8pCmDwOi_r zL@iO%4RXZzTS@{IjzwRId#;ftJf%uaU?$e=@SL~-ar1#Jo@S_$O|2qroB0`JOhj86sficf~DqoPW1wwf*a zG{t6<#i_`pJ)+RDN_b5qghkeUU{W%z5ZHt z=T4!7m2SWJTD|*Sx9a`xzgzEp?_Ju=9rD=3s2U>;B@?4(u$knBzl5&>zF+}-CU<1!GzP@&}#*gnMa^`{zN`SK;H%qH4&z&k5AoQzvx#_%R(hxKAgK9bz3b zZM*UcZP~F^qw8l>wj+ytsR4XBk0o@8vFcS4L#-Y%TZGp`?6h~jzE6*Em;cB^kL%^< zU()HrM^$jT(d7DI6k5jUv&GS&xCW`Z9f>O=dAh39K(H;K4U7Pzz*vwbqG`-bBeZV= z{&&7dkbQHIGy-pJcx|#PX@vne2Ix_t5DT&5s!M;1;O@tOzDb5qgG8wMA?f0jhc%Gv z?A73U3eQC>(V0}HwIsN)xYoElG?N9uu~ z<~s}c`Pr_|cDN1GOV+6)0(Msi&}Gpd`{OzM`kQ@VNWgl-{yD>n2xoXRcpXSC|r zVYT<{R(knGbzgc`-B+L2(w?1~Kd?(@kG!JA*}dv^PiTN%`;L2nGtk8;7-XgdKW+zI zzoQxu8PvfRE+LMdge@x7LT5*j`|SS1`tAb{>AtUgNiXbqMc2Lm4*k9V`ajoy{9pe4 z`X}{o;v{T>v1uZgi2lvw_DKK$5CBO;K~%PoLr%Kv_2E4!uz?VGFmCexn&@?llj&#; zb;wV#DT9V?3NnsQ@>5;yzwbdE;_7fD6k5GvTobKascBk$`?cEki=R~3u}MqJtq$YR zX9LsW>R~Y^6{@swL$modrJr?U2>m5=H&m~~=y+uXcA)YA7!SRA0q3R<15=o_y zYA_goD>$a4dNS+MkCAO4atI2!Rk3oOo{px>B<~Q1s{aG0;r`divUVQMq+X@EOIBri065x5aHXTL_wl89V#RV zokA*}GCpuYKW->03X^A%E9wUl4yj1WL#9j z4cc+d4z1s~UJV=$x}Z6Tc$4E!MCdUQSF)N0XUTJQ!Bg`|oe-iz2!aGvEKVLqef-!NV}sO8v4%#< zaF!ZsgqB+9(`aKvBdu|bk4|c8at2tT@v&*OT4QRC09-GHMpGe1H9}Ae-E2ai@3=GX zk3QHKs0Xh-1X@{eG1R(Yy7-~^q6{y0NJ=V0r4pGX;+|*882_M9nv6a?je@8KBS1Nf z6PpGGsKPm^(4`1%qHdzCf~l4(_|lLfnpQxeNu0k;t5$2IU+M7kujr-6pVbo&J)xIb zAWkFm?nq0E@Y}~I2RssYEHlRm9?&xflYE>!E*>vR9(s8l^0~|xmT0ZZ13I+}L8{w} z$MF%o<)wO^=jHDdyumxIyIjxPd{0;=G#@ZJ?|GBO+TmkOpUi`aHKA~@Z?W_9Y=l)#Z`r6m^($mlD@R1|3 zsZnje`8s{%10T?D{LE+cpZ?}w)8GH?zoEbXcmB5i!QcCj^!NVOZ|iUW)^F->{^qai zcmB$+>aYLiujn^_>zDN#zx6Bnm0$f!`pl<4rVrn9m)?EDHM;G}ExLN;j8@Tqo9n%B z?5KLL?a}t*Tftq=+mG995z$ z5h0O~s!&pQ03vq4^_l5LFA~)?07n}e<4(k>mv`$Gt`HZwn%purq0O9rz{Objh9k!oPPQ5<;VsYXhj#)X>H8x~I{&y3A|BAl z=%}(R*YV>g)Z<7aAskGIye!XiMU>4#Ko}w^q4!nJrP-c?ztq6-#}J|19?ds|GO3?54+P1kpSw9Al#VVnz&YbypwN+MgX0FKYhQy$YWkF;S# zT`mdZiaIH8G}L6_49H67o|+!7TQ~R55bAHD5|KR5Sa5Lwpwa6m?;o6$Drb`CS;R0W z`lT{-Lz)*|5?q#4hI}ZZuBVBSl_Z)IVXQN)U%y_r-g>L9X2GB23PAAU<2^Ppt|kj` z1uZm}^B8Ct%<9SG6DTUk%%xakhMeV4sz{YA5?pl>Db*8Q89Uib?QBF7?HR3_T(2z~uF_4{ z-==rH>-~D)y&u#2Kk_sB*w6jEKK$vQ*S(+ktls~LpR3{CPku&seE4J9dixz(d(AbP z<`{5vVn)rZtvHApv!=#lQ;jN$je-W5`ux}Q@Hg+*3;SNvNj#gyv8KAg z8%b@PfCgab#ehC~lO5{D^&P zqtk8XN1JiRZ&lF@>a+MP#u6v2V*W%mV^G0W-w77oT`#_%haY}WU;XNR`uyiVuWx+w zo7%Z^r#ii!)~?&2ThZkYfAnMe_-B4jAO6hG>Qle`OZx1u|EhlG7ypv({rE?9(_MFH z%hlVpX7gsPz&W2wdylI7-RE_T9rcmNpU?@e=}x}(vQ8b`tA*1? zIh{DG)H^G6&MJ22RnDE!kv*^L0r>d**T1BL{WH4z=Rc5ABUJDZai*d>oab7ZsamswHjd^Vw z#(Sn#CvEw}C+_$F9e>X^e9uw}|J#51k1y*F3(y1^B;_nH4R$d8Lsd(ij`r-?qZUWz zZZNz~U;+mwvpEHZ+yKx2NraLC^##u-kQ>_`yTS&Gzu~|LJ_-h-&wWwGz`7cgOr!x0 zPZn7S7#y+K!9FCP;#lgml;Jzz{0 z*#*qa&T46XQSOkrqt?YSo0^=`mMgYsdU{4?f$D~%W-Qny`N{#&22Ba0)@64y$5=l1 z(k^}T!3XsCcc0LK0|&IUxF})IDqR~88i38BXAw@j;i$8B* zFf*AM84jxBCiGKM{QB3%9@!Hy0Eg@)-}^> zw2sVEFTAQpzx+*o`Ahfd=~rITNp@%R9BuZ{+gL>$soGvy;k>r@Pqp3CqJo=eezR!-v>UV8*jQ* z8#ZrcQD37tIbOq<|6PgIE7fmgwZrF5Uyq?4Fb4~_8;pN}qh2&}6>NZd%ujdz3i5py z++;(s5@%uS)mQ5l9E|tA_kH@%2S2QjeCQ+k;72~BcYo;JdiQPD>z0|7y7E*KkAEGd=vs*Y(VI9@JA`zfWKL!k_3*?)#j+^V)OTe9I2~-T&i% zqW_pviTB=en^xg)PjYoN!F(T;@vl;aEaF%TJv1d`mJsSP5M@G$dg4iSn27ls`82VC zspvC@Gue@odW|c^-OoRdB}v+}dY#70T#HLxwXWK%D?W6OMy}o}8y}IuyRcumMaC&| z3z)Dwrc%<>6`Qqb>y?^pk84F3!GTCPXvMu0ea{z>f6sD^W%|BI-ipB#LI6yw@o2`z z#`XH^dsHy_6Uvq_7D*5vyq?KSJPG*8d!5b_n*I3Mgs|D|7eIStM80;EEL;V5p+&FH zQEf&__0dkjqMKo$Ekx&3RZ8apG<9AwX&F4PmIL(?ATlbvaF=7d-ByeY6(Smz(e7Lv z6exjsJVsEz6e=lW(&W(Ki=|lD7)*H|1}gunf==HMpULWWZ~rkfnOy=HL&sr^cVyy* z7$uV`oTdsI1l-Xftx$)#u!Nyp1dfHElfY@lXby*IfpMPWq~~lSS8r^z{?oxL zwr$h)TW;4~7}5`a>Sy)d4}4TNU{E)0+@kUENp%*Mw6w73+&DEqbU<8h;560!7~|7w zwUj}(c>H?EVW^0;KvY*`yu{<pA$qJsS)_T0^6__XX)&oLwfYf-_SR|{B^ys zd$*3+KnpEoKtBZpRE~`LvLF%q82a3E5Pe{R>#jW+W#F%^ZHIC;%Rt_F)vr{B?@1ABGr@(g}c~pL4 zaQp4I>DPblSM}@c=s*7PkLhll*Bv{q(fSRWG(I`4*l5bH(ul~=ZU5ETMf&T5yASR~ zPSQH)McTxPJ;JPUFeD0ZYKSo!M3PdG~>S8|z*c_V}QNC(gW1H4% z{q;Mv{av@}T|e^y{md_aTEG1_|Em78zxQ|bpZ(IW>0_I((w0u8_QB&?dS$l`Kl_Y! zKK_WFd-8GZJHMs>(|`M4>C+$ksMfVd)m-eV$(3)z=RGHXRaK}D^l)ms zKncCBpUH*jKspc_JV>ej*2+nK0tAnVD`NqX_3+p;FX+HyPbr^U(zZ1lv~prfi}On= z$4AxJu|?UI4eGU;VpFdHHm3rj1vQvZ9LUI+4?S#qYyBEs^Pao4`HCww9@`q_ZeYC9 zU;G3B4nE+7e_t?X7dRJ|5Gn?#!Jw~Ow@w3?J$UGlV$784I!R_GXTXhl4Wj*J09D`i zt?{8CmlIwRnGICNaTzDoNng&Jih<{5CBO;K~!HrfxrMan@pl}`IO4+f_l`Y$g}0S4s`;o z8Iw9^lDX0*a3yG};K7?TSR}KIu^?e))OIRrOh3@4ZVjy=25^C*#wq!3-N(Bm*F%S* zrXkhzy1bUbpe1=d2Mj`8fjHQBsSi%i^B^GCMEx$~v2b>-{*_XpbKY#g(PM{2(NAku ztz4zasVTXhxol#Vvu0a8nF4gQo#1Y^_Ed0LfKg4}yLS~p78AtuuKx#AT z@=AE#ax+9Kun0CZxniZRy6#5Ze$TzS{rw-%wRgQs>#n{=bS(cH_-Dr&I^7OB(UgvZ z;6#>mx?$>!x3oBxYo7eGvC`R4X%4OG#%oqIgqG)+3kJ+KD?jhW^`GSoaB5b!-*k)q>R6)R@s_9QS) z1*Zok3wN;$FkL7x-=q!>wEqBHKL&Nsg91WAvmtBd3fQCAXv(sNLJj0)O*Qy8UPO)e zBa2$5MiFGp#Q{23W>K9y$mhcwLnk0HyGvD8s1Zzqlc%-!J5S4w zp3)V~ajmvoB@X?mVo9Af6Y8&Dp^}ZJDN~*|kxM4fsp=IHYfvL*;G(~FL)Hjdb?sHU z>mwi0t#{m|Yu8_)?KZOI91(9DgtslB;J^7N|AR07-xmp0YrV~?!CxpOwS1ORj09ao16SRX-707+uojhz$a z(;Dai??4d0B)E>JzRH<#ZMbfSZhH5-w1Rum&SFOe4yftkr%uNv!1>jSY?Vr8Bz^>WQ!2 zuLr*MZ9TtluMWfW*;eETIx29r(nR*nLbW5{4ncAn>A<^0+zN~p%c2}v}I`ugKP1t|}IuxQ@b!z(nJ?^xZ&{N?+2tfsl3L#TcU>+w`PCjiy z-cT7#?nD%3s$x)CJBt$9rZSprq^0>->3FfIVtPc^eE27Mt#S0BIk zL;Cek|GfUrul%Nd=QF>gd#}DhE2^kA4u1ojOM|W!t*^7{Y9W>?n-PtyR5I5xY(b+k z=+c|OB-+X!=g&i^P7YS73<)|VKLK0XcSI+-UJPuArbk9KRtB}u?~K#6Vx*x#GgE0n z2{|m`kYL6XuweoIU1k;XS{n2;Xhij`P&Luk>UZ6)yZ!3$BOlexS8m(#|M~PMK2VeJ zHlOe|`E_=R;TihsVzYIM6+muT)07;;7#cd=s83_!}@PON&d2GNEf?_1A!- zE1H-V6Jzs1rg^j6ohZ$^&(MlEo~N8ou7f^BtE^4g;G} zWFFF=91`-S%@~eK1P6r4Lw8|f$$ zCbQ){uOUQgb)MN9VIb}>YzZ|+UAJH!J*Q&{2Ls{Ae-7i!sZ&~*pHrPjoyrXmO^TvW zw4l|iSIbXo7%qvi5;QVrRX|q}Og#+7EIXbTUfZqvA9+MCakp7wcq*UViW;$~#<@kO zO~V{;nU7CQY4g^rbm!gg)x9761iSwCYkK2Gb|X=9O!7`E(*hoOrx`S8nyN5B&lhy^ z_(8qGLjT?Szo`em@Hsv3r+=bH|NPJO-OqnfPk#POdW!IwFMdf+fBCC=l3mv$U%5}; z{@Pdd(4!COxz}IP-m@ok+6Lg3X1TL%PE9E1+Mt6|YT_UAGc%okh?`xIK3{OD$i;cC zV;AQPQ=VV>kt4^F?%27Wh7gb_`g3lK>Y9v4B26GP0|@f^eXG>R2=+rM|A$Crv>)ec zWSZUB(Os|Wfj{_y?)$=*^(e=(r&!hdd6cM_S5Y|zGB2fi!3VjkN1($%UmiE&mwKI+ zp~AmWYGyLCH!|K7^AazKU}zNy;fC7~H%>H7j$=~Cw5+W^^bdF%RoVu*5GJyV4>w28GyCXtcFJ+wQt) zjox#UuKJZv>dwFZOSiiM@@7s|7SBA#qA286*~TI#s0 zl*2*APN&49tND+yrZ2&N;%Lm3AkgL_+G)?o`e>p$;H{%$l6Lp3ll*xGu9)iq}W3V7U;$Ofx`E^WPj!eZE1$3yQPQmVdYHC{3Gc)pj zoE{if6`~S+yW{3ROy&meyU#qM2OfS*d-orbDk%q-6-f;Rv1ym}D`g;PSf(s*YUR3( zx`89Mdp`70-F5E=v~l~@63$=;+)I)=LGm9~>V<^C63$>pvxoO<_mkhzcfax_7W>cX zm9Kw8r=NIIi`@U|<<~TN;E1My&?$V)Wp4Ia>Cl7q%>-xqw zzQ!rVb2@bLxaRvECFWQ@($*42q>pYU`ga{87`{HXo;ghYP`-#qF6Wpe*Lb z(OiF^t|eI`r|&4o85omDXMuzpWCDSyE~9GQ=bOX2PSPdI-dxwyUSIU_R?uZXPMhPb zLt1~|5ITqbQ~6SHvTJ;lT^PEpsW@tnj7d#5^V$1^c?O;33hn8q zpVgPX@>M;x^F>W>Sg-eg@{{`bXMbLIa@Dj3owv}CfPtbq@^Oqz7sJ*ga>oZ=0m%WH zSmW!m3i%D=XasT5R!qv8X6BnaZs&oDf%zLe9}svFb$!8foneE_4qCz|m;X$bgLoX5 z>rr3AeEoI0^2)2Vb>kIUJ2fLM zbadq9-8%5>P95juWA^YdQ)2gF_&4aod@qfuu%5;U7;GPd}k+bt)H+bsl>xF1YTf>hCgK1RL*@>iO{>`Ei5dk zR=^Zg;2>1+)xbf>7-*jb38kzJb8s&P=w z@qk9dkcPZu*KdudPWvaxX&xURSBu?#eN65ZfEWVCJFDrCKgmDe8@vilYvbv0ukGRY z1FvU%mVxNozRY!WL3JO?eIb!LT^kAU;7E0r7PW*E>CRRp%O`<1bh$Z1WHvUz?%3rP z=-DR@gbsiz28@Vlo@;|=x!-)~;YaFU6>OrfY)a*qmAMmw6OrhP*7P9dOw4s#w(52k z{oCJvuQqbWUvX?V$9T=- zrQCGZN_8^Tj)c!QchKDEi_ZOTRQT@7VC07i2=Tlw5~nM8p_9vey{R|3EZS)Fw)Nkp zELc!5lV3{~=w)E8I}Sfqk0Hozba2i901yC4L_t*Til-gC&Z{_B-}uHi_2^?ys5v^W zyWjT#-SwXLY1Ia7b+fG{nL067x6#ln<9m{g#2k5j^s^#N0NM4}R^p*=Py6@}7k{if zx*)*wykC#I9DNKO3>`AchafP@i|f$qd!5=TNR$D?QX&kD&$Bm!*UxZNB8z`SDf}cs z#0!Pmk(>*aPIF{_nhC$e&RbJsYOk8ny6sza=iTqp9XH&paTeiIJ73e@?>wiIukO<+ z=HIE)oDy(d*kwH^Vvx{(zwRuc-=TyDD3r_JY7U@L{&;u}2r4!h3Jr~pPO4;`=r=RX zroK9jq`B_AW*SYcS}L{h)Gqbe6!@<;R|qv>TdF*&BrK7$uQePADDw&B>0s){pe5>b zoVG?(*=~H#Q~coMT?+iq|9ZX;fQOd_FliPs61jvzx}v!P&afj}w|<>YBghjcj;VoC zx#0TT5n43;L_UyJVblV`9dKIF?{?%1YTjt5RHcP3ixG=S2A}S|R!k7*BWJf_A)xES z3E;}_U}g&B6KMJIe2-Ts(lDN6A*#gqM+Q3>M0kbdpGyOC;e5ez+*^%|v}=01(Cx-L z$P{B>Y%*e~sVl8llw@Wyz4h@tpH#~wo_3vEPJge^+c`ZK!uk3jay-l6`DA%egU9Q3 zr0O`-x%@kei>h!K-NB0Fc^xs~DjcuKF{J;@$;7$8L69gfgJ}St75r7mvx`&u{0lqv z?32%Eac)UtjS=M~J#+^e3Av03YL%&xHqifec1jt}0e?RVUvII}_vjA1X! zl^U&jLM96s-Jbj!!teJMxt@3i=i-U4e_f}zTA1kcHOaBx%1)tcn`63@JJpX}b&Wo` z{W^W>+8gz$8*b6h+;ppc=9b&^GdJF>PhWGbKDd69ZfcEa3-oO0C9NNr@{_YVzUx)J z@c4K2;B z@sB~~t5C<~RcoiY!hy~u}T;-7z6|MZ{yFZyTy>|f~j{^ZZ}l}8`ZcMtB-p23pNa4%hA!;A58b?BpH zG!x^R2<78yK_V2-a2(Pbsib<$m-W)HoymdmHs-F6yIDo0N{Nu_su*K;3L2&4e=H}1 z2Zw;Zp(t`lO#X`c_1!u;P_np}F=3l2hAxAG0F5~~+NhWR1=k77)xtqAY{60{oz8Ix zn$p=kX(1K${|EBHyWXph-12U1&L(u?*;lpa@#l5)6~7vo*HT$%L51eMmpqrtIw23p zlk*oW$ibWh!ATuS#1i>Zz$r=CjTKj4DH|P8k2$cgw5T(KCC$cxLX_5LBg*%k(3$66 zR&nx-dW+rRX+cOV^6(c;8T2>6)#OMZ2SQ@au%JE&2Jl_>Az%=<%pZ9AZBGGw#HjVd zeJwDgDXGEEb7XW>CC1wS7J!*?FS|zW2pm0nOc4fJC~41si0=HL{%~peVPInj6=G1y z!r>RNm1esyoskdg7Q^>D`6$4FLU~+?EO0|w@J&9 zON3Mh=NkoyjPC^$*Y!Poqt(SR@9#qJQmTax{j4YB2|z%#|5cx_)v>_invX zAHhj@|K@GFX`H&9N>dAcwHLaYbXG4#Qm1+==-wPK}NPxtT_;qdi5qEJOz0b`{ z!TAbriugir6UCYMv4<0zEKmOxC)<6H>{<*&SO?Ll}KcPSW@>lf5 zFW;vxe*Vk)@}GT4pZ}B3>tFx7-_t+;!{67x{_2>x(#IERy}0u>=NH zjCFEgoIPaV>wX|XPKS?(hpY%Xmm$w{o;*YusLw#E;xIJ50i;Xjwa;Bj>U?ig)%r&L zI&>{a=+?#HHrVTyj)TrCdW1wZAYinRM*9v)`Jj+H9aRo>EUej}Qj^mT-`#hzq|tRN zb<3T%>s>e9rVY&r&A)s=d%pX;_P@GE$7W|WKj_J?-UROveHML{%;5w_ba}z+bPkgd zU2lYjs%VvF%KEu}Rd|fABc(wiugKH633eHE;kTn;0YQV^W1oA!|z!grRk>-sTB!laH5@>PAlbrQq$_`Jl!Sd|cIF zp&Y{j@I|eQ@$R!gwns;_e$yt!Eb91)6Y}Hj!02@S9^H{9?+q*gFftrGARA}TWlZYA zmjYc3J^&TFA}|Ql=PEDPIhVYg0GceAO6V_e0-ScSP?zL)*;yM$wV4H#C~O6hc?^ow z_byx!Jv19KVrI}}WH}I=dp%`lv;&V-@*j?AB-04T*uHQv#v)nE)$8i~<9rPh9vcER zJc2SdSh@#at6V7VrR@NdE3H!9DghO(Djb|%TkzzAKQ zXtJUPEYbk<v91e7kGs5QDqvnjFX*#X3Hfhrdm!bs$Bb?e_3C>@9TPy z>y&+Svs#Qnz5u0=c~3PMU!f}y)-yie*aJZR9}DiLA`YFfKH*S z9UQlM4A^xBh+(+Egym&&8{-=2-wf(&kPowh6ARU~EQvcW9`|wcI84aFb>j_40=+64 zd$^o09s>eAJNUfG5R5~pQax^Ni%aM!18LeDeP@*UX~GDI>L2ojYN3~rc{<93&dDFB z)Y+`mTvO_g=Gt=Am3sHBcj)%3uGM4}b!7J*y}|}!@7}$dJ#|LiP6ykcME9aOR1Mz* z?h5`ZLXR7TsZ6jN)D4vyI2bqGeU~;N?-rYgS=PY)i)VFQJ#`yNV~tFcXBTziq3>uv zCw`}2->ZtN&xox;>>AJ`>powXgjlnsyfQT^$>&so#Cr~AKfnoqscr;5l|knRz|-)d zI+O%EeC6YI4otBWkc8aTWC6?A<#$oco|PJKd^b8hshL%)!QcZWM*}qnbmPK zJC_!?3YAu=0C%1@R0MIXT&lOwQL~U590&CAiThS)jAP?SOK%-~u|Vl6+DRre8p`C4 znaRt|tj@vdlq>I`5fDS6iGeig6S!gO!h8wsuIRCnT(%~-VFD!bD{!E{DhLyUZkS%- z4K}l;Re_Q^^8R0m-5iq81kx^wO@EwC}Y&YGP4F4eyw-bDCIJRYU~Nd1G1iH8DA%n{U5U zx8CyswKr_lsccm9$g;{J^axp1iqH`QaWu6kyE=OOfS&&DBYKtHQaF2B)5v=r#`m`A zmHP0OYjnrVdTmZEP4-PW{SKgUiTSXYGrr(T2qCLXA>``Q*T7idDyhMuxn^Wkw{O{^ zcVjHB&Rbfw*w+Zgp_*UR0{zVOF+vF5W#trsPChoGj}#=>6{u_4#p7i`NnVdCGU7uJ z@i##tjgY*Jct}EHQbvC@sH*$&@@U8hgW6D`i9Bh#S!u;6#w_b=1o^g*RW|6?yD^%p zhYoU2scti>$0{<8^OhDCRdyEDAaB#elxF5j?YsXeJ^ZC_>xGwJ*Kv&X99M8%Y|9eV z8b)=rFf|7d$6LYBc?hQSaq?k<8tEKVgboCkc&RQkdke^<1hdOPiq^|G6S8;%Y8fjj z_~asdbKSR33p}R4z?oZn2C<^!nwOdiP@#F~|AaTnegZL5uJr`R~G zADh&c6>GF68`0U9_UWNN{j$FKm9ObVoZKVTKua8B8@tzhU~>Cs*to(9RaVKLKpzIp z5Gh}@B6Bn_-XTfnfpSx*zoNbaqJ{Ti9gn{uBMqN(^-^V`GbLp721HfhD`}XhZA$b- ztg32x`BhP$Y1Kv729y~+x3CdSY{n36rCOl0FX5wXNDE=0FxArLYp&F-x8JN=*tJjf zlMX-gqIN&`f)4E6r&DLnsEcD%1;g%y8WP5#j8O&SQ9_e3jt25xsMM$b)XFryeT%NS z_ijz^*rsxHTu0}Yw4dvM!-GZj=yxsLtUq#6-ETao6JPkMjz9RYy8HL5>UHWhVZI%7 z7Ff)@Ut?ZLBg_N;!$smL(O+N-(M!JJlDT9evSA}5!An|00ZDb-^UjqKt@AwetN_Cv zXJiyuT%zyB5aFvUVB(01yC4L_t(1w0-x#Z_hrtVQ#S?^%s{^4hE`H zDX&{WU4;N>iulAp4nh8yf8HL@~v%oOFL13a!2fCMoyvT)kt|K$gkPJKtTB``1;A>&n znpvilhx)j*LJ)395R3ddkAmiIjOzKoAbGxdC$x_WTDNMACPv59MX{WZm}iQv@Trp1 z#YYa_j^IXa&3b0*9i@6;1$8$rzRBS_)Eo zVpKQYa;t8><1US@T%&nB;3W)Qfl(|a?^eMHryr-zXgAA(zw^cCwEy*2RC0`+b22nZ z-#cbj>9&nqw0&}gW>Th65VR0*$?>BfANC~qkz>M8$by0oqA}(eSJ9soRG@cm+^1V3 zx^Ck}-MoIYu53?eq*JKDv8C^}I~at5c|&zY_=^yjTx#%@=z}ih^U32R8P5brrtkN7 zjb%VMb&Eu!q%1-&ieV1q_*-&{(Z=E5x@D7Yz403P{|I^KE!XMRn|EmIrZpOG=gO^8 z1G z0dk|6K*^k2rq3ynoM)%BCYg`N9lX?($T7fE$v=N#FfhCYKneqC|n zjfx}VIy!q+drltJ(S_4WT;y+HOs`xRsP*itI{t-k>9s%pvX0#U9rgAfQ^~18fy~QR zRM`yhR!a0A;NAHbJSx~0y1V4-#cwHC{sF+cAgQJod|C#*>7j~TM=KNJ0)mk?;z?$v z28+83pyFsVcZH$y*w~mhZr-d#7SIDn4$ERt;-CMtdWCV6&uSJ#lmGJ)7AuozT0`|F z8ZJzlnS3#DW7NaQ`>}msa+sNTFRc2G;voh)PU;}+0G<)9F=7bnK>HF4i!Yu~hdnD5 z;XTVT`Z2N-i&mnKBp-M)?;jvX=ASnK6Qt$~9M!qSxxr~e1Yc6RsB+!hKj0KX8I~Go z6Hv*OH-qfRWL!Blsm<&0CAqU2uuzPRjVN#A_4p`h80shEfgIT~DbCw+{9IVMjL;32 zcZgA99K20HMy-)HYi7=IT!q{#g%}i&rT_ZVv17;d{Ikz#X>nfb*RG<@K;6L-G41v(TQ$zfnH%Y>SE^X-Vl0x(f&|Z!$sadtD56J)P-mxMy#8MP z$G~l7avqGn7!oDYLPmWIR(pI@S8l&j*Ia+Cw(r=XD>*{F=9(S4{)X$cW$Px5vdb*l zE#RUf^!y=!BT5hkTbXz9Ul;+PZqZ%EPDhD{!S}pZ zSEHNVL&vq7)0Vw^Ue}Su(>j^DT8bqjmN>Zvun7s-fP&K*Dbk1k+{#kZWNT-%@%9^a z(>-_V${p7zZ;oh|Q?UIfk8AJTX`L;48ds((s;1j?dfhg(3iC60reWDk=;iA;J1e55BbJ5|1s^oO*!pBG? zzz232YK5V30o%hP+@UJC@yj`8Nlfqvri3BQS=gp$R%p$}^*VX>tPUPIq81Z5pqM2) z_dG@gEQ7dzC0PbOi-LXg2{j$v2uHViqQbVZrLcko8ke!JH~pcbWs^$aBCMwXnn zyw1il3=6%7VXXebD^#gIdQ++GS6-pfW>nE#(A313n(d~{Xybj+Ah;DKbpC4a_>Uc+ z$NO|eI?tSW4q$4JjHt==im@2FBjD9ShDPO4S*roS6KQV>YYOldW57{c)Sg#g)tS?$ zwU!;w#Q22H&dtdhQr-wcIhbBS(VDvA%4>A);CPs<9-0!p ztUQlJY^k%LgZuWXb7q$7prm}M(2Cg5j@28qX=0k)u2i81oB*oGqyg5}4SR?Cyx!S) z4Vdgjr>6qPcfeS=1JIdYP>&bVSu`m?sv0F!g zTDf{g+pgNGHEU*&;Xrw2LYm6J)rgrA@~yZ^%d;kUqk0&=E<3LZBiYI`WVu3}J%{z3 z`@W@DUU^j~%C1h~AS~nox#m)cH7mYA)<6of=tGzbsN^LEoi>ycB`&WzTR(YERqIKm zDmayb2UtY!GR8Nd2NAo1C#wM81-kn1V~^?|{rCT!{^X0F*BsYEE7q>n)XJ4=jgEpN zs872-dN{!T7TBr^8&U6o5dd8%I6m<6qJ@$tLySmC*o~e>=w~Wx==8ordhO+xb*eL` zCGIT?=3mKJ8@k{&&T%m;HNBvggX={l)p3HK_}6J(2A4xZM-3fJ@Yq9!9pqQS#fnuk zy5Z&CE()K%V)5wOkI1z>R%$?ER`7=6`dg`RU#`11m2%ND(g%eq2B4zj3PL=D45?O4z3tO%Hq~mi6j9>p9G+nld)qc|E*SPBnVAMm zI`@W!5;(r1Kal^Vt%5OOP;dHZpd>NNB)G5X_7ITh6C45@=bJ%1W+rbl{0O>(8hJwr zyk( z9p>o&Rc+B=ovYWDOQb+|J(H(pFoqjD>2Y0b*@y8X`gav!=;gJxSD^rmDyeU~4Y z@P4;yJ{HiHf|NCL#VqRNiQ_uFcR%;~mD+=(HYZ(IaBRJGdX?N@T5q6=MbKENgu^k{ zU($)wC)De3gALx1m}{)nRiF&+qn$xd@G-&>7;m+-nX8>u zSzE28Qn6R5gf?EnN{Ko?v@w_r28}clL!zmc9g+I|!9IazK~3hDg(%U7V=cj)Gd2^` zQ_5QG&Q;V7Kou(`?mP!oUt{BKZP>U@6O*GFFb08{BkVm;_v ze@J(c#jl@BMYLKLh2um{EOY7;_OLV6%(!N5yhb;E>chI}lONH>+ip@ZKB~R5r}e_o z{n|ZyOh>zO>d>MIo^@G6JF14RIJ}_j;pcVikM7gU|K?Bi%1 zOs974Qt!}_cYXo@Jr`9*56=%MbYCQ^dLYU4fkjP~P!<=I8_2p015B~95EVh31{KE5 zgdR)ewa3P_di{DC%Jcf3*VJ3;Xp&>jgwc#xGoQQ?f`^ED@h5Zw%sgPAfWQiT`Th1P@t2c?{6tgaeN5DV}}!^-J~kVGij znfFgwEf>n2{0o6BmEd*UUMFK=Xkbu>lzQo<0OI5@EOYVH6WS{I@qfU$y0VP2SlxE> z4SL@_@7AVG8x&pPXyUZ_Lg{Useh0_43{ zF?R4H9A`R=dYs*6-fqb!XTd}jat<*lBJ0J4dF|oue~N36Yj#{qUexj9CsiPq0T3`; zLeD0~rgY0KcWJ{F+ck(S^|Gc4WM9Uh!kq-1sVFHxlYw7^0)Lk)mHiyMp2Z1hq3ant z`IUJ~*RI>7bt4np(I;hz@(&8d&kQABtPH$j0w{d!y*%|eilLb1HpsNP>tI6bJz|f9}T!UPb6f~3wL_&Kq>74)wX#3-Q zUD^qn6E--pNIOV5^e@cK>w$0Ium9A2NfSI7IyYib8K+IBPoC28`B^P+-`r<| z;eW)5>Ijie>OQ>n5Xez&Lyckw5xxVU%8&JRI_nP4N9vS>oGN%KfHFwVt3%%^7UP0z zC{qU~wK2lH-@aY%x%qbOShHRYtQ3J+j zAxiyK6PmdBYF+b@_v`M@d|G#X_(R%w)m2(lrG4|K^}?xxdTHUf4%wn+^S;!SrkHD+ ztfB24)0TttTKV)o+`fs=B5>L29p5XtD$|RQS*adOV zGh;M)eR1@nU_k*!%wf3+-><5*PKE=>8v?tXa2KXE_o*boh{3C~xNN zF$~_P1%v&hk^%63yvT9Zp#|9`fU5FA^H8ETbvzt`F6(ic-4IuJ+%W{jMqV$}3x15u zWHI;kUfSIf_>kz`TJX3}&%xO2t%!Y6Y6qKk2uOks3 zLc*b7`>Dhtwr=$*?bxhs-!(h6T zYE_{oJO<=YFn<1PYz1?TrkWTTtxwrzxn}9oc4TZ!>p5K+967B=Klc@V{!c!qU3>Rx zp8k5^=$Y6bNwuN&#fx+;Gh{;aoE%g?iEjlY`XzWrZ+b_yemIq!-U!_h`do+H7ABlb zPakKo|F{44-|2t)|Nc`w@#K@p*fhh5P-}D?*$4HgS35*#ZJ7)`5LgUwDVzK;QpqEH zJU39c?uR~6TP6Ml=8^YVaxLpl6o)zTjhX*7&V}xhx{O_EKCS?G44gwC!3zq}z3QlW zXMl(A0K6Twf9S9Cb|F$TCuT@L)0!e1a#0gt6!=TXn*yI0UX4s%EqW^um(i5Z zjA`xFTXeuUFxw5%H0Tf9rMp+Lc)+#3{b+rM(9;A_2IL_@oa=J z=gVaiLr@+gq%n1%+uM<-jSfvD=mC}9_JJ>#msyZ6E-?nh5JVSm+r$gpFgRa0djep z%L^?bbUk$a6I#~i9`w|ylN#mJV9lyk^6Q#Yr%&U&Rr1HHcR8+aPfTd*wb!UMHO&H? zsSnOR{gr^#@^=~%`e=?;K88+fg=(9XI^T-(u_v) zIE-M?*yN;)9shtlV_pzF6~JUFkX^~xG;zj88ZGq)efggY z9&n%99vjh2b5z||59_%veM?V0^tkq(IjQ+ZP%j1*jG2&c30)P~f~TQ_?rRwgsilB7 z0iM1bpvjNrHHfRYX7st6TSRXQHE|^OA2_Ig^KXAwzyAk+q~oVfY1O(7n&hruj7Og_ z4_PKdU#etq9Tj6#Krf6m8N#>>fpKK>j!X>Pm8vQcCPFDu=1eB?NXf$(OhHl>18q|6 za8~r^bzRp=#xKE9o-ziZ2$M!q`({t*`D6R^-06dQY2la- z_RiKPG9g4wSk$V1r49Y0%>(I5jvTI1Ym46Wgg2!XF8B|Cye#pl^@dTbvBQTI z5NF0WAYeZhC#i^c6wDET1rOIKlNVejIEr@);_(3=;JBdi3J{QhdZz;Kj>nb0g0~1A z89)=kw%bVg#PRlZ9v`oNNkF)DzBiw(YqI*A7?;YUjD6)}LzqBv z;>Z#8IHLFC{{}j;f@AyjI1;TGK$6&*p&c0HPPeBX*IFYS?Kg1_D)0@UbpgFOFndbR zzP?+}z4n@R?%S)q$B$|*_0VLjkR0TrVya~D!Iu~` zLq}Ql)x^Qswq-NN{0-zZpl%7JiE$aQz+|)u8nC!jINcMC5lvV_T@2?uZ3p8mP0UPb zWooK=@t~gh{5SRZ(@*K>U`c)YE*cH>Ak-R^Ls#;+RO2_KX9%aJ4KV~j{&@IK!R!AR z08wE_qvVbq`W!9l#howdU;gWVqd)uNm)Yq{Yc_6DY>lY0OeK(jsxo*rMgG+`Bt?Z3 zWGv=ZNx>nR3dAD2fNSoDEDPym%9NvdURmE?cmXYJ~> z+Om1GR*p|eoxT>1o>Z6Xn}RVNG)(=BA;u1f@oAK%W(k)m-x<+W}5+En~QsC`= z0B~m~^-DF_kp%cFFvk9?TRy2AU5VCbA=Z>Gg40hPp?1a0Ooms?{ub_~QhF zMu%aaHVnucM&OIcP#3M{YbYI4LZ3p2ipVhGVV^y7MvX?U4I4HnA&3(=0G)0}bsa@n zk!fsVirxQpc{f>%_tmj#JPAjzfVQ5EM?BYkq>F3jlfoGBJlF$}SJ|V|XI+YNDr2 z>}ZAUdGyi8^xyrD|D(SCzyr#W^SZ59D&qVt;vf{%E0A*{bX#vIY@od#*|VvWl`!c) zaaM!JlMK6wY6=l3#H@yhvl@eso{^svEGWi^?pN6DO8zr38CN$eIB{4vy-JguKs6Mw z`_!XuLayRTKrK{4R84^l55ZbzUrq>$AIqDd%t3?>hWr9xq&;--xP!*%&`u57l1hiYJqwMrwS7{?$C};d{FQE)xWHd{*_4TWZvhSfVwo0Oj-ty z>Jks6YZ)$4{KL)ijvS+hfVuK{`aIZ>7U-u~75NDU5F0TghU_tFSfb zZB>JWjW!*Fo}Gse>S%vS6+7n)I$PtD&|rGy$RX`MdQh|6vzGKFjErApxE}f`K!}0q zQ7Vw1H*(buGV_oOnMLT7Opsm+CwYVv0bf#*i2MMrx2@1kZyJ$vjPylEo}a__7<@zv z82n1X#{yaLsv=LpWFgWt$t>Z}G_-B&66DK1xnedN=_g1m$@HvyrPBRLYVe zzY*F6E#bsf7}YW4wSw!E7V>=kv8VKvKl!{Kefk-l>GkBlz0p&};-C0B-%0*82Q+LSz1&b5?`XMgUtHa*hITvrXV0`=p+y#ex=OR zNdx)MVvHjD$@ZusS2cb@VJuz=8%R~=k4t(8;#ua!Ly|wCuT%FXsF^loa0#HyD$jRz zOg={)0{N}N)aWGz9M=k2x@}oP?MrNVi}~Se+{Eaps$O56)3YjZTH7I0%c3&m@8gd6 zRRw`pGUh^4O;sGf%$szE6M@C`(@Ixw)bzV=*43Z-u-^U4e_0>!zRmq-O59L;W3B=-^aKFNUt3=$_u9H$8N5aSj7`Q6U6jo4K2tCmit5Z( zC{KwZXU$wISFgq}v~}XdaSbrAN=as>7$7xCzUaE)HI&Wt<9U)2wDeUD3i*O;F=*%j z3?Kla__|Sy7`GUL5`##VXr=`H9yha^50Aec3^7?PKSxk0)_rgauho~HYR4OD*mNt@J_^{&VC-E_1xk{3>c*++hmz)xq&k4DKik zU7KA}1b5NSR8F)t#qO$owy#|eKB3Qj>C5`gbI<84GV&j+>NAE4Ch1dBKW?pzMg*OV$`WZ+Bk%}tdg zKgl|azH)G;RjXIY^*zJ+$|}iEd_^VSvLMqJ^Ua_RwxnQwpkfl^laQ#gDC53=VroLG#wIn!_8L04-8{( z`X9uE`)v>*MHH*QTW$`|~f4}y!@IUw6C-m6; z_v@i=eqE0}@NK=eYp0GL*r$cr(=4cc@G+Z1MF&ZAqxNW9Gb>lfcWXU%{9*#T5pJ^^ zXrc6e_ulDsRgmvWDirmt;F$D7c?>L0*#O6-HXO)7I@0N6lg}>#_R`c>oIyX$)547rbPc@rhEYN?r7`q>bxn z2ENf|Q?8>Gx)L%|0#}bCz5$D&pGvs%SV4>HLYGs4GYj+bT|;cOI9`=X#y#N-n}Tw# z#N2V1<>X?(yu-FCSkOqTtpSd)|2#l~7K=eyBUhr1pAMAt<<4jY{RvXd49cWHI#4H| zSEfo1iMg!HeN2K31v4Cy2n0cbVl<76v@|x>R-{LRV3R-o4cNUXF$YS7U(?0ONQo0; z49JFc>onFLk<&JG0!kIgH6bepGpKiYDyS85O=K-qy@7m36{p7(;eR^iiYMpv%mWYU zi}!s+yAK^ym--cBTS8Hip)-8X@t^3o3P=GyQVpn(T>v6pL#n^asbNUgWxh3*bNIcf z)nk#M3SDh$^AEk9-W?l!Caq z(x6*tiG`%M*i(P0SEnl$Xrnx1>BuZ9Aj^0T0yqo`EYWLeEHb5f>`Hq$00nw$$f^OI zkume}@W0x67H45(d_vo<*rL@qye-O6bMT7xg(_$g4=CmQNX{b}yxtcX0my*d z;g^CR`RhHuxs!w>hTt?WgF%XWyS zNBRRsfOgc<(2MpsQ~u2|AyXAytL z4cFHij3`1Am5A%47J__ zjHOqT(A+u>-*qVP(Lt}X00T+Q=9mT;n|bc!B9pNVzQ&-d+JqyUC5E^P4Zem!FIBR0 zF*5}xfKW=R@iL-rw3neGvxN9w(bWj?oEcq(0wY(ctOg2_D%$%`IS)wCTmjyUx{xdf zs+@ZvUI`ADp(4HvZuxo0u z8!%>anF@tQsDCGTiCq^8q6VYuJE?IDfroAMt5r3Ws-QXe=rhIzrv?FIK7wuq;stZj z@t5dHi+*ziQ*w+Op&8QAnF@W8lIuS0BMOrd=BgA88e%CoXcgrZ`AO8h1bCT)mwQ-v zePj~Qa9S7Gx|1F(3T(w5uvYiK)PZ+^^s#hMh(*A%<1*) z;z!7Dfm`T%S@k(t?n#-z*;E#r5{pzyQ9=e;KBBT)N(+6hY>#P#4Z=8!SM%5jJ^k1t zdia@VbeM&3k-OM3G&LwH<;?#Eu2x_iQZv(1mZ=jWXbq`2h)`%9V9<(8Q3eg6A_UIY zs4GZ%XQ9gg@<9wsRgy0j5m^d+g-Ru^pSne#t}3A8J-HFL#A z709VXUI86WAPXpG(J9nFi#E=7RLpjzr9!SJk(DTdCjg?Z&@od6{vm`aQ!F!Ogn5;# zQRK?l+85~90^`@iLCw+CapX2ZUg#H^U+QYUGtj0D+q7-vI<1D@5$si^0soRRN2q;12Ha>wC2Gi6?Ym z*Dh^cyGHlE>n>e$8H6#Y3swz3ivo8g8O+Iffqe1BgHzjaKl?c4g0GHs* z={sYhM8cs$NR=U0ylybj(!Upi$u3M@5O+oMWX8ho#v!0M$tMO_Tp3(BN^n;&Xh}65 zqfFo9@$$EV&b56b)Z097N9)^E{|jd)I$VaD$Twl6xXvDiM%)m1rGQ*~{QH~^EzZv? zp*PLEC4sC&Px>qlA`cy}%f?sCXl&IA`J`hg$&=i8#VTMVX-2=5qXs}hN=!6H035NB z8X;3NYbf&1$6~bIP@ZMfGsT#pepCXnFT93h!qx~H6Xakn*}fe8F+m0?CMCjo0W$L+y*WaYgGi%uO7IG(nCh{>&XqnY^&o$GbX1D$%c^ZCseavexqyYGYbG@qblR73{+c=ULrK|8kxG$@FZ0Kj$`NSyiUHbTZbNbM(OZrU3KL) zt@Z!Pkb8)-nMrP#Ft{z4S+in=uG_Igciefq?s?yP^wE!hRKN2(e?$N9AO7e12mk4R zqQCT+pVO8N8?^8B*YxnC59|4zFY5TQV`^bOrrpEGgRhvV1#~zQ1DFwlYKBZ>fgKWM zZZr$R$_*M&A#`8l6Cuglz(6~0v!Fyj1w0n?D;a4d4~>yB0`!?Q z{Z|;tMCf(oPaXFN;1vnp{2DmCje#PI(WjKWs;51y>r+;T|KTHq4@fas?c6P4s1_C$ z6hf$fmL$X=A2(k#5_JT)_|~jjs}-wOsV+y3KA8zX=cOKdEBF{>mDFVQ8o_9emQfAj z)u599B)0#sU&kFqNx|!36*IsXcVBRfkqkh^V%=cJnrAtNwx=SMvSy}&U0{zX)C(n^ ze)8!qr`gL3oWcPR;48v|#|H~qq|sutrN#EBN=}7hDdjkY4dfN*(`vA~|2po(IcWLuT;k^fVwd#>aKVzi zIV!=o(pbLS}`*gT4?&*smIe{0{UX}-00ujM-eaEv5%Xt+4@BHCn%uHp{qd%{jHv%$ zL{?Pt|FmEY$9ij5t<*~H^w)1#r!CvIXv4WMD|c&aMZu-{Tl=t1O*6l$UIJEcQSmDG&2cE^jF`>!lTO# zMV3Dv8hNN_oxGe6UDnMK-w=ax7Jk?7cDt?7(Gex)M1ub0^FsqAXqnkij(%{JY6>&?35 zn(Or5Yp>NcW8>QU@FRNUn_ty)dtcQ7?ALLNS_lpG0Si%W@V3}_N92Tw!?p;pAxLy> zIP<#TG2+WW^fF#=6|h?_1$;*p3|twi8beM`qajybNS(6_`rPNfpl{s&fN$!N$G8R@ z0i8Z|Ry}t5eHMNP#4I<6KE`4!9FY+sN{?O=62g89cpK**hsYfO;Q*L5WK}~Y*8)pL z(4o0`E#%VLo37Da?|+Z3+_YX1`$|)#6ow!|_(*SoA$19gY8enQ=%@h%R;P_tcCLnV z0WKIxHBROnCI8{Qdm%S`6IO=%}X0$ApcL@6z4Sh8QIiPf{b#)MA0#vSy8b<>x=6 zzxtbhMYrB^t7ea#(vuH9qWKf23|IArWXq9 zc1{pw0=ypCH3fA@W!5b@h`19KLy*(z@r?dF+riVKk;l!$3!pChDfpO~%-#yYBwzRz z=NB+qg?tyEBzTA+2ohvfVzyZg zC=NxWsV534qNdg0X}O}gKEjMI;396i+=rGls%RGABhNEMb;voNc<($YF1>o=~`8l3cJzV>xJ^w9l!eQs7KW2SC4s*V_U zXlSs4=FIH~GZ6w=lgXtYe@Po4L3U+6J#m}5W)+%n0JXUjT+%zmk6VG1npPd@rx z-S?HRYY7`uhD-~KeJw8Z7~06#Mim_O2Sr6gC%}n_<+@d&#}zVjNc54w{Z7EylRvaI z^dZWz=L*(D&vTs}lsfG{_{RqB*7w|{8*kX58Jx5Rb~#etAmDpLMHd%9-A3dA(7!Af z4|R^9g9hdY#6aCjNQb8M}v`sKf7#?x&{I7oJ|XHyFTi7RQ%kEHqd-7F1-7%!M$`OlJ1BGV5DXH#c=NfNB9e8?M=*n?CS9jb43) zdh4c@UAkyqHYcy6*}4VT`aoe_wbPuf)yX{*u9B$a9~rvrH;AjLVS0SRwAjEVk@GLyxqf<-vs zNK8zQYizVFr#MJzG@6QxWq{7a2}mEiu{eK5gYLZ6ubI&uH(#ggcU-AeE5@+}r3~4O z$P^i)b?YeKyh)=c&giKxe_ang{HR_(d0I>Cw7azffU1DEVmx&2F%KcqLkEFtIfPjK zMIOgPcQqQfB@UR3x(7dgueyEh*|l3={@j;!^ympS#wO66hRj^1QCVope{>|F7n$WU zSLRAG@nF~Z*0>?xqwg5Jeud{#x);RQW#~@>+@_!&$WL|`RgE-s=ezIJz3;k1n4{rHHXokny>T! zR%U&8zZbl<#V{w)=u&Xvz|-eX`hA|&wNnfic?O>8<5C;eg+&(rSuGtorRnyFR;^y4 z`J$_Njw`z;f*4pgP+7l_8>@ESkRJ&eliL@|iotee$NP{IU>3vH#`v}=pF)|h zUb{}8`Gud;Xa3qRD_cFGJ%{#b?)V7>?x0*T9giDSVZ?I_5(?Zdw}(ej0{;-n{rP>++3yYC=PCLzxd zqdXmA)acl#+M^s-N965lgv?#HT%u+s(_0@etI)3k@FU!kDiyeg|JW10hB zx1FmTYpM%g-{mFhH`*;#G1NcMR(fB)n`ejT4#16@x9Zw8>$MW2-t-ICGjkf}$oJY+ z>viqgjat*f;L=$$X3F8s9Z$#QFo;qwRO(=K2dX&r8YrQw6={PDvmi5ggjUJ4{CG~F z{u?>VCEq_xjt_8oK@KtvOh?m5yTQWmaVfwvFUXQ$cNp>=zUcx=m;=bA3X~wbHap8v zWR+Eg0%OTKu6C=b`edQs)zaKqMR?n^Znd^=-K@21Ruez&Uubtf5* z&725aw`#4*R}Sd@fA^<)`tc`p`0QDApua$#{->Y&Ig&8?uV0TWL!AMEc8*>nPF@0j zz?W|8mS8%EQwrFsh6OEh#Q!)>%8M`TRMr|%55tjSt}+E=XtWE+IfLI!eeV8C^e2GO zz+`3$A=F@mNL^0mo1wNx49J_qg8Qs~UPv0KftS(1E+bVfS|5jLpp92;(Z}w7kM7#K zUF)-4W9@v zlaOTQIaK-g@FwK-@1fftTbDld>dsg;(?FC{48$ zW=0iTdHoZQBSq3^GWp^^%Fe&RoqolnPBAEDs04zHS(GbnZK&xdXJ8FQZeYO2afwGu zX~TxK`s6SFy#CT({|&9@KJ}%YFKW-uof5mdh+10|lcG>!;t0B2*()57B-IljGO=Tp zk&?_zAx0UK!p!Qlnd$8wGm{Uv!-aL6-g?o8haBAApO+{4tuVMAk0Ho0CwQrOgg$TM zWoAJYX_LABlVvh{1IVb~5KMh`G0O{;^CVNY9}NbiIl3a43;& zx7SrdRv|<&h-x$%${2f*x14TpYUN`xHZ~@YCuA5HPhXVHhGTC=1H{REz+fRy;=DC( z16ggDxSP%x(?HnbnqmIzyyiKw?{qpk!yGw&{FnwTL^%!pr@O~S$JK1LG|K`u;9ANN zHyF>1d+tdE-L-v(-hcf~y65Wab?^1J=tDQ%uG_Y3*Q%_oCdb3|uN1*+!NS;M0gR(9 z`40;&U{Fu9_#I!IS7*@Yj<^Hw6@D=jTwP9T000mGNklV?Qf)p`){~G>u^S9(8o5ksOTx-2vh@{PINB~dJI)h9ts@7kuYEQ(a``HkQ|f*5xgcS zR`RiC97&L@NTa(dVLQnP<@FO^!0W&w36K3~Ak`g6W>$lKM331Jzu7Ph^*Fz^vmreR zTJ*M{bC1~&m)RSk7dcA~9RDz^`2~qxRIO znyV%h#v4>CHBw0H zg`^CHn#!B{@Q3czFa5P&)7D$A(M$VZ)y`+1Rio2avscJaK;QWc4299f`jANb>ml>Q zqcwQE1`2rVyzliH3Vv9lLVEtQ1R*-rPz!>Rh2PBMBzT84;g2}qI_cqgxFEkSyx?PI z46e!hu^2HxNu^H=cbG`0K$)q|E1{(}b>KtnNX`*a=)q4ZAPNN($js#95|CSi#U4?i za|zi7HS=820*gjayVX`k+KqjZ&dE?R={)Gd#|udNTxhZIwMwZm07_Fi*VQu*e@FL! z;Vb&eAO4xX^zZ&qyPtesTIg_0YHHF+BW9YJo~EB9|1;O#r

Esf$(MD#ynem7bCdrR}G`W^Ys|C;SvwQ71w4f?P9VLU>O z&3P2~l3=#Zar&kvCba`6;O-4ubojwX_0XUGnO=JBWgX)Lvy-Js%2ZNs z*b_X#lsULklWAmOE}=>_h=(k`!`CT&r{F(aTA@#D(;IK{}IjsE$_G!=l{d(PDA7Ksq^!mPi+JESv4jeh8Lr0J5*r}838_zNw z7S+K?@Af*VbYCTMsPzFx42u^S>p(m@hzW+XU^sWe&X$0jcjOlsr}GRCS^c4XU3V`9 zKeP^h#E6{schUvpeTgf8pe=gK!w}(#4;JRtn?0kM@lmZ=vr<_rD&)pEREoU;4nWcb zlV^;(&L)$$4PT>@ax#yUZ2JYzhh~s46P?5tcm9B;OTv%AShU*8~P@jH`r6 zj=yfb>o$GrH~z9V-Lyk5?s{JPIUY+Kkv36+Dwq6c3UtAfE{H?iFa>_PspA)w*E#R( zQ(=t!=ru(6#_;&As!l_zV{pL-S58376SOW69GjNQ%&aEM4VQyY;AJrJgdjIUE{~Ev zGvCRWA)itmc9c$s(Z9&UKFGX&@RmO)XY~2MhZhlqEXu*JoLnvqbkRc}n~03s94+TQ ze(>vy)ljyW71|1TvjoYa=EZNU5({%s3x&!sU^!#mpy7dCuj;FR@MrqQAN;wV{nkV3 z96hCpkjoeFFeteeODtL|G&MP;(`RROW^PsrVTc?jSm=Ae@r~^M+yUl{mpM|(>8hw! zq9lD5(2kWFG=gSXynONBbMmO>*k!HUyix18?>3VJUBMedmn^6TPuHM}AFqFL&i70? zN{0%*UAFFUWLZ>uWJFDuH~B(ulc+`f4fLdI%&Degm~TpEf=iW<>H6%rCODlLYqi9> zuO=slksAHZf+ojCv}4;At)7`7KL-!(S(JiB1?5u8q&k-ZlUItw=E2gwbEVa-09iUBlZo-}ky0_f;N!^gm_B%CMxK#%{fcmc`~ zBO91X89V7_VOlypt0x}&j&{BJN==8;(sRCIQx?3;4`kG$c{hFS8=@sBrz3}oYdf}Db+V%SDdU?-2?Z#O+a{RcC zpE#jYXHMzt!kjud1cSmIf&t_g%Fy#@m7+@cD99@nn7?MG<;ToqX3NQ+>d@_*!?F-O z?y#KxejzG3tb($<#Q~@nDJDkQ?P`GcHa#_thu)OoC1&j0TkKHAk~G0?YR$?St-?7N zT{)p_azslZ>Dc_N_8va0JqHfyFn9TWOj@Gc(K04D6Sk^U03nZ3n;~c519(R;giUrn zc_wYxwn?A*rO#;l&DZJ4XTGaL`}U}WmdcNqF3&8*HfUHUOf6SJ@aN@jILrWN7IQ>SzY zkNe26qdIW-kdAP?yx8TaH<-le{YCh?G$sH4muqIqkXP1d$YQ3LWpz4+fK*Mvj9u!8 z8dxqxv!(=K1S$}C{Yc% z%*TxOj-J2~L}buzHhKsaR4GB8BC46;&kCoLiS5(lipDJab3wq<&{ zrR&$P(D<3tdV$^cQ%^mkeW&Jh8YjS7E%`NNA^PkFee2kPo2=**)!@aEvTSxVcUZS%B+ytH$k z=CCgO0X`K<=YL4XGA1Q_CnjLQLJ&L-?VdKKq6HI@=Uwi3xigd55QaHkK6DT~ZDt+} z+tpE1fjkZ$J)&oxdsa_9{j?r_^by_v;DdVT;fJ}$#CRRtuX(O!D*Bh|M*69J7>C%% zH99(?iScobwA;$!xv`QzR_A$sjscD#A~CD&rLRRk8eVG|+^{akmqV|EDso($)MKUT zV4RkKE{pc+^=maTIU!-VdrKXi;@ESE1-{Is7=wbJcyUrTIWeiV>(|S7yn7BE(3!feCxMNr%DM8~cMfLW+Uhy$0(7Ui&9S}nnd;@-aVELZ z_HxyIdmFvwNuJ~QC@%cCmvCgHsSy_ZK)OOU441CFVw1LRTF-(Y7O4d8N(tK#NSPu$ zMi`BXDI#mieT~Ec7{pwK>jAc8Y^tg0>87q;w^HT)!}|6geNm4*w^Ii@9rd6)HW@4o zPzAk)Q$r6bF{)tG#g9?r;+*f7N>uO@j>0MlTT)*Epm-&Y;qDi9=`f39hZCRy0;{rI ziT)Gi27e;Y!7d0r?jRlm-sx**GJ7jz$gPR8CKU55&g&B5T+hyb)5eWL4b^2KUt&Yn z4MB?`YSA)vxH?(F!RpX&>9 zTrT9)_3py-1@ZH1eelfx>p%G?Tf8zlry9s)+?W9(SM9h;pY@*!xbsH6eqgWmbIfGivo@2!t|8qf z$rT(X3~OyN2{6)udU~giH}ks7A(OVkxEczWLhup{5M0%GK})xX={ANlK|E6 zFt5S;&S#$0i9?5Q z(E7BZ@lW|1(aYAzs3xYS)W#_dA*dD%vh%RjY^u#7*=*#>G4#=k885Ho<4_Uzac~fi zkH<2Q9KI|h*hvjCscb|Qxf{MxGi%rAsvXy8VtPsoIEJrtmwWik87(jmdN_*>7NXoT z^)Y~B(74i13~-wL?`oXpI$*KiRSC}22)Vdr#>3{ru)7SS`JE*=k{_S`(G zQ^Sufml|n+R|X2#(TapSUTv-bvMj6f!La<&KxnhoAb)75WqcYwfpIc}Hpa=!)MQi8 zU=a*ZJ25e;tJ#Ha+OV!3GeeftRHR-87z0N$8~_lL4l&kcp#(R?4T4mFr=n7)>}rIQ zN&g|jE4gQ%ef}kV;rD(|4?X_44xgQ8fE*0g000mGNklUEc-%C8DabPYgbtp+${jGZH561tGI zTw+SlOr}JhF8L%0-Ac?O0UoA0jV^%KN8qi^Xai;%UR1Q4dr@ZHuS?zG;*#%`a$Mi5 zye_Mt;DU-3^pb^CRD&0Juc(r%6LPCt>By0%YbU^UQBlPfSgjjWap z$aMxUao?GFJ-2%gcSd`3`XnmF0-xnARZJ3ZD_?BcHDLqQhMP7=Od}jQ-Lmy6{iV-- zR@c1iHa&e{w_e-zlEyd*aL1tvPOmATSdA(w17elIbWlt|hCvXxhGT(Pm{__9q+yv9 zz-2)^NkkRBE%a1bChw>8NnF!dN*>3Mh8XIdL;=o#aSe1)qD%shE0Zefl4oX+ku+fE z2wAEOS;Mr^d34(Gsv|P0Xqb#=%7MS%~w_%gkuU@AuYlx3csKM?b%q=RzU_hwE zz}MkV>Bktbhyjl*wh=>G%2Xy~jl4y?$wE0g(pH|C8bD5d&Hxy4u0p_=nFc}f@iPhi z8A42=wFD~0N~)Adn?iEEipo;Xj&(v;U44VrZoX0_hVn4;Whc(rk;NtXe>PLF3oWT% zvI`15X}U3{P19?%YJ5`S2>Q(IS)DvPt1}q5B`f7e{--!W*nRSdUSh{TpCzrk?rPom z{(E#YuZ#KP6p!or+RoH)rY`vkknMGZKjo@O(%DVFLz{*dm+33%+~{W{V$b>XRM zpaa#Cved)&RLYo>d{wfjRSHN(m;D$mVinXXGG%aDGUpThpY&_J{Rh>nqEa?4r4i+2 zLpeMa*3lsLR9R9oBLu+1uqhEHkouTW>ZuxRSaR+w(d9YtlDYxC2Y@&_!56;7f?nw? z^qd%APe$9i@rGNpdCg`u${c%|GmjeTvv7A<@cW?w&L(-8D#qI(G4=`0YQTQUnlfuC zrRESS4_mZpVUtyZkW`P#y=Ho_c! zdV?NDrP4Ty?-&eUyXDII{}j05hO715OD|~up4XHQ3MNiPJ%e6By`dPP2217{5`0DC zmWb+74+XUJ%4SA&$CX4(p^{wK3FBBnQ$ik=C_@3v%RFvE=t(6V@Ya}6A%RM!N?t#l ztLIAPyEdQPmDhAUl6;^XA9pmekf}8~3XCft$5fhf3mV6fnqe$vS|gfaH`&S>^7=_X zeUF*R%wXK19=Tfi6M0oKBx{rh#TeB*cLIYvchs)XE!(xZud0@H|TYUq<{ z2;u=J2&pT%J~(hR!?+rSQLEQ)&~|oBYqwshJ`3`R z4owANwf;zI=9wDc?)Q35CdPKilbOjV_fM^F=Mn*Cp$J|o1UIaksOwO{y=Y*`_|gga zW=57NB*`6zAewm=d}V<173%uKe5#4Y$S2ip0to{=bGb}B!+9Y^@Vo3I^Sp_pmV(KE z96ke~QYkaZqXvf-ym7^NyRl{gz;Q~k2D$>bd9j@V6h;%bePrOauAmCn}*IQsquN-6#ba|5StI^H-PoL~K{a(YPU_ zl1W~cJ)QQuZ%#pF4OJ|f70zU^44q3#8jx2K(w}OMl@P#rIN!fZ^9}HtC~;j{=NeJK z+lAnK_;?s{H1*lq*LH~0tg}^@MY+;;PL{S`wOu3m2#Y`USfkUeA%mqV*Zd&pOsCSx`6V4W zc~bkBORv7RTQ74>u>18_wSWI!#`c6dOE?0^GFqi*rBn@A+b>6WPDV0>r~KVDTlMkFKjm)ai4Q2~_jdIj;6 zRfUZ!6){dD+^gn_$`v$*$3MYcYhdEnJB^Hij{%Rn5aS@EFBg2|)5aLCTD3ycQR_ynaxHM^$T2O>&a;pwHBdIOyY})jGxZ&?woGyG4>XSRaMQM{^m8Bk zm`2vm=%JlYYw^S>jrEd73Mp^pTBuNfAtCYs8pI?weu04pl?0ElcNy<;gHX41Lj}gV zy_Xo0tHD&q-Fl!oeXWeODJ3}i-Kar)r2M+J2j z7v;Z!Fg3G6lPgvzFgEA$)>wWzKQ1N*=YBc#xbZPFHS%134G=?+aNr^`ZQwXIIKgS4 zZyCDoN8*9TMjHY~&1wh?N`?WCmE`&pNgH)2R07?A5fgfM1-tb1*KSi-F|OG#(5@2) zwDZ`0o#-#9YGpuF4KF@=v|{aAZMt#` z;TknsO(~juNpSs0P6K>74F)Y4rwlPf`Jc|Mcgy4nMX#%*qy$5Du5u#bb2E>zuI2TD ziM7+b26+Z7zcd8Ta>{i&d73H#-b0KEF(^Wtnb)NzWqgP4h4Ep|MPD35hX1==|uVjNATE_eGKt}k=OeZ`8E+PG<>*5agX z+O$dQkn6_v>vaWJDC7M?yTAQ_cE0qS4iyXPvcTmwijHI|kg5N%C1T2O1_FE|cJKPK zVMtzC^Wrwi|G$cbxp|dbbNU?5;Kl77>mG}u6i}=TFxh4If=atWo1VRFy@xw9Jr&vlEPFndNg=Jql(0m6Oem@`PHn$3JeL5 z$j)F6%McICvvhV8|w#zzY!bPRzU#3266$ASOdD z4jw0)iI9d+5dw%pW`Te-%`12|jDeUIXr(e5wUE;&hOFmD$|(9c&eW!DTQxF0$s}P_ zqMhUasRj5?$T^v*0(1_{NF%?J^R5^oNA)c=vmDV@ia4Q7@>@U?y=|hm8ROw2;c`r( zEg+_dVIiYK3bo-wh2@Z_i1gXPBz4JXub$S`cipNR?tPEKni(A&boJcfy?SOJJC^xV zn$tl2Mo@uHyF*|j4K1SAN9IoJmBRGQ}ERabGfsdhUQbecnMn;yAmqb%Sf0F{d zNaXXXuAMXthhH>RP795d#R!0h`c{MZQqKUL1L8z4`8pnlx(8?#gp6_YADQb;cf~}j zutNfe!FxrWioDY4vY?E45#Y;+3oypnZB~!Xg_&t$VnQqYj|*|N;IuYdEse9#t;Gr0 zcI7r*vv#%Y;66R^z&G^bYp-e+$FLh(TB@R|ELX+9!0AH})|9v1CsUasdX7c#F;|eFRXi>Of(zb? zxI-d@Gb8j$7l3a-E7Gn7)Sw=9^v+-?p*Pj_KD<4lg(O(0VHr166HRqL73G6ytfN6C z{0d$&;QzK~7Bzd0L-y_yqdmnwZgt*WmB~pvZs_Lqqn(1ky86RlIhoOz}W!`D? zFnr`AKf-(kl1BD4n_WFqA}g~}ClZNFAfd0;;%K?mS`qwt>~k(IkU$lXh0GR(b(gQu z9v&X<9v%_roJ--s*Hr#_eR6~GK}Zr_D#Ejg000mGNkl`Z;f?p|S^*=u*q_zm z#Y1X(Jv=4TqzdY2kfETmsEQ6qg3oaz8Oqx4RXTR`kWQg&fx=iAe@PE18;zL?f^8@9c}Mudt+ToOG|qEsVB9-70iUm zccG&CKkL`|nVrSVuj{JEVW$LR1CWuQ8W~=kU(h_)S52r?!)reQ=({7p;##vas^GmZ zem*a*G|Ah__!p34he$kvH%LfSCY4M|A*coRn5Ts^r^-G37yjhinm>0&%W16FZeG!k zUi+0^y?RkMx7T!ccT?BbmUZFIO}%#cg6`~W>d+%+_3SskqR)Q&YkKm9&*|(F?EKH1 z)a;=JRUBC>n!GPMzWES>c&JKi|7ODM5lRG1<7J_M4xF?`2-x0G1$FSKgN7j+ zs-59LCr+K#v(G)JJ{vuYLDA*H+d0EL1e}&YIuQSdUNe)~hky?9gUqIJSx~YFmxEgL zs#7G4xXXYwYU8X1Ec^rY$~D1&Yl0E5*Gj8fgL0L8;qo=Tb>V`}U%aR*To2q^zN4L; zb+ud%1c)|E%4w_)r@R~OsU7a9+uM=YFgD0o%{*J1MKzoRi6(L%JvM!fQfVe_z}bpWSvx-f&X|5tzZFZ7i^`?gjmyL$7TH&uWF>=6VTwe z_=HZPa0e;i=Pd+K@GVLFk-oLK}9zGg5&1|KtRVI*+_RPx`gS#U5e52v}b@fHSM z@G!xAS3sXgu}c_kZE1IXL&v!a3FP2A z$QXm>Sy<*~=hRe{+_9=~Zfgv$gU2JXV#tgi(Yn%f2u)Y!!?Bn$hN`nj4S-NUmdArS zGmV+kA#xmM=Jffme?#B;vp>_h&pxYgWL`ISxAfNSt9tF)1-*Wq9Sg_&JLtgi#~;yW zzVT%}{pBy{)RX7bJ9bztrvxL)CiFChCfAh~5>N;ExXL8cUkUd=?xY9CIKiM#9aLnA z%7#Hz;Q|NgY7g{fI#4eASF?iYm478wi2DJYmoA;&LOuSwVbxlR?y_WHD zAkefwk9`2_S2B295P{6;@+0UNBBGClr&;vfAY~btTUgLMPH=@Ikf7CRrizSzQ$w4G znP(}+C?8)34;?H7ZhtxnJRLTr#ra!0Jg=wEozkg6t1I9Ax&HpgKi50E+uCFy>~g?S z1ebN{LSl{?qey2!AMeS`r8&UAMDx%zI68}(>(8jc&IYc7GIQ?zu=ca~m||z%n?BxS zWj^$tmQ0{>`Iq2@oDc9|m&I=n`!ct*sHdKJN{5dgQHW73r(%V#H1kCTj6xL(UdU$= z0Xt>-9UjN!IJ{|whA9MSGj%Y=dERE?Vy-MI0G2>$zu}A}t^$&xIu`vd^ps*>ZRo3u zGxDD$*y|#v0ZR^sldpdvZw%)jKS?9STxPIrZRyY;d8SIt6K$uuJCZhpY&THDd zcU#-bw=~*VQQFy(;TXhr$efSW<7BP@+%`lu3o&PSkezI34f_z`vOT~LFQ3A}_x)i( z@G(F3Kz)zHo(FPN^v-RZpLDjYf0N-z1E1SEYgh}p6|_qg{0%hDP(3j#I70a{I1gP9 z!R8n}Sq4^7*^i%_Dk@T8c5YUUD+ld61StX8Cz5(_MOXq*AZ!R)084P(7}*AoJ5lQpfA#hqUA}rlyW4x}rJ&gyHK$6xh*w==AmDaI!n2u8 z(!)oO>uZ1dZ9Vtx7j$FuE=NHZIUx{WiYQi;Xd}5{WBhW+8C(YYAKJmD*^0)<)k*=sT84k6( zwW)sH=&2{3&{^&-nFw{%$V20Hg`NnL12LtB8_U z$ft*axM75|CFCTj#Zq-Zg2x%aj}$G`S)~l5j3Hn`EgLcxRC8n*L#;GPTsJJ}GoSgq zzVV&!=vj93OJ|NK&DNUqEA@^p>Gac&m4*Kq7XA~DKdOeEe(pD#)Ik&GwnKURn#^es zx))*;lgw*VkmO@?AgUNE)fMg8`TAH>cEW;OzG$E%^J;*Vct&0-ocAgQN~o($XfiVp zQw{y)E63%b%QGEPUQ+%cnFOiCGi_3d=h7}s^XQ2J>SfZr`vI}?deXs>I*&!yh8_jR z>$JJf$%15FU@~~no0-w<{Ji=&a*^F^hZA7bF^kiFNXw)Gd=m5|c;Wr{9A@HPOl66> zu`wM#xunk@JE7*DX~>v(*y(3cAWl?QYs{6{~u&jTj1;2B_W^a zRRXfmFVANI4&=uWB#Uq{s`W#7U^l#QVQEGI<>>%_RkX%9F zmT)9wEWUp1KZ6I3rBO?9&LBzxNd$tUAYlqIN{&mKewn}s17~}cP98t1qlXWvLV5kK z0Ok-ectLv%V$`d(#SZG~&D*+t{f@@lLp5Wm&-P~~M0Q zPrjkEpLs%WUwKD&u3ytU)_MjA^ec>NLa{PqFn#dj!gx^Paff%)^aouL!<5N9Fp5MO zb7qXDz&J$A!5}h=r+frUv&^gj33)lC>R6OQaK2?v7F?5dEax65)Dri2Vf` zRS!w&}XUc&Gx#4r@oQ<(k!q`{L-~7V7LJU;yWT9nFS_PyzVi+SQ z0ucuwpzzs}5=XKbaAojxC*%M;G?w=hbH;&?D&(wUM;XVRDo(2ALZiCP8gK1sdwW|u z96N@&Mn@kyrE||et*5{Ac|Gxk&+72Qrxh1xH9-Q@|?=0!JtBLtym9p|q!-)8u(xFH@GyW1#+^7xL&FT=3h=LE`NjWZoBU z&+1qlBXd`C?HHjqLbp2tJx(QM=G4)PWP+#+Lb^-nmVwK0IE@yAlG7$AV-Be$*QE(_ z-pp2doa=zcnmKK}eqKNQyYK1h@@;K%CqJTHuu7(=7RI*Tn>W_G57{MQ_P&CWyZ@oc z`QUXkG>*wWbo!)DJ#t2~IDR#{9>+tu9A^4R*}qi!*e@MHtqO<-X26{cgeveQhArG> z7d%4dkDocM$JkiRakA^vWM-K6y0p&l&|A|8JfFl{uz`q>m&kkGgDejV@G<5LmzphL zRwVA}40SnF>VP)(H0D%bWPOd}T!B3`+D(mC2U)k)_jL8{n*8SlF5kYZ+be6@-s3cY z(;xqT33j(PxmqP}?VfhlZfmf9Tchn|b$jcw@s8>=QpMGQ+lBx?0`r0(DWUTnn?i#Y zqz)4SRg{^_7NN_q2o2^LqdWkjqKUsccwqT{Iv-@cC-k=O$v8+*z-$U}QbbPOzP0H8 zHYezttE<}L%6L2($*+q2XOxCfayyfPNm&m#0OE0C3y8r;!3V;k7eLsTRHICDOAG2B zUQ~-9wd4u1BbJKOvC@X9T9cBGh%y6$#KdInA36kovO4>kt)z4qQYu*JIK*wsd%AR?j~Bw7&DN{!IO2 zi~8|Tf218IdzLoBfQ>3>YATM@pe4!81eEC`Lts2^OvK}c1;UB(nuummtU#Qc@|vuK z{_&U@VvtEW5E$hibp<0ieaUbK#jjivM{t2QzIYf5!{d*i)00m=t{#SBg5bJvnMDP7 z5ki=@|7ZR+#_vE-Wbu9+KAIW43Tpa&&CbuOZfX|Uq!e43O_0wCN{*+GL2Lluk%e*^ zK)E_u>=dz%F2DUMC?HdLG(?`pnpM&q@_2{^>QFHC$75~XyrnmO{-S>Nx8K!Uue?f) zPR~C7oWAm{uj?~k{-Vx4c}}y(mde6EW_~BMo9d1BAEV9vXo(nK2Lz56wMTDU4jdBN zESc&U)uAg1dIC79qVi<_n%qznAlahdF$VdaFG&~VX@HT7v^6tidLJa2dGFp63gl71 zahaJc1bG*35Vij#@Ixi}SwY-uM ztN-qQ)hoYvQ7hd*yZyjIQ&XqMoCHmndt{TsSFQr02t;1i#FPABQR zkDaZycQgxrx3wLMbV4>{AGL$`*Lz<>CcqsK1yhuwjQ{`;07*naRLObmN8-KtIX(ON z&*{t~kEn`~u4n_l4SxC}=NM_`fnbv|#h&oEhaVsI5zy0n+d4*vgIteK!Pkw5Q(mEm zkr{B>yd){Avx@b`p<$7G5N0$sKz;2_qESGM%Q_y0p^~KMBR)6vr|3cMl zqgP+~l{PsFiFMQv@7g5IvNOY$6Z?3tF$Qnp#`XZ@N&@N0Cq9_O7+k@%|77U&sF}$; zavw})CU5U201hVpnF^2l7`m24q=V*&0XTW;xE^}+Aw^C{-1xv!RaGrBL*^1_AmTpm zf1VLoH#PXM!WGcd_c;NdoM*l$R&}KX?mK5W#mX*1>SRDY_7$CZ=&YLAzFOvWkA8*$83tN(1JY$l`;Z0M^2f6Deh4!108Jk=Q;b0s zJL3-Lu|;p?`X_-|ZX$?@I1=h|Oa0Cj3OqA7&jH=`Y=nt7%gjpsXhDIv$rM8XqRdQ= z$-$!`7^XN21;#@ouuF&RqC)GT~q1I*%SKa z*@t!L-kN^?mw%~O&%dd=jAH{L$08GF4iMcr)!j{apNiK8l0Cez@;S~PloIg@Ht*_szONNs3hXD-zqyc;K@Z*o^6i!bc z8Hgc38SS9KHzHo&q{1OR@GykJT@N3~{uoimx?er-hmMiB!C51I0KwDHW6*8rAuNC_ zO6fcUg5r$yr4j~^1zJx**m}jjteR2cbYRH*ZH}VR z<@1GL$#r7tAY_;ei3AWygk<(aiT)ElrhuMiCO)&_ontOfaGG^&eo5Wk4Q*b&sa3Ak zw>b6K!;tK`y$O~7Qzx~%sg}uWEMp)GX9apG#T}iGM?@np31{G0B6peQq2PGi)f&a)R~-*q3Nq@ z8ZC22_13NHdi%z8?P8o&MTvS9li$#1hY{_w6Y&3$us`Z_B=q#B-}vvw)?%u9?3^s5)TRSa`4?ckBqrb3Xzp|AO6lH>)Uk+wLW|VmWfmiYg@zBglnb$)s zJgcN7Q^H{8)QKvROhVWz5>8)@fjD#elpcEIVKwl6`nMCZEYcUsN>yceZ{ALyKFBrl zr2@`B2j`0MNieI0*NONnyPm~Eiwf-IIa;EAhXdQGNBInjJPXvEvbZDABdn3J=*}2N zX$^~iZ@jIrwW{&*4Xs^zOILpVqR#*F$GZCJFSK^`9nGXYedh5~`qq~}qi?+MyqC?t|~vTXbrs*Qlvb=uF3e*AN149=B}9ho^u3wBLWE~HWgU>9F~nF- z1?D-vj*LHJ03}AS-Tkbj0amReZf>vwp(tv53>x@{%;Sz27uDr!_$x`WsC+SNd3_c|<7Y8807c-;1lo4A0Db8M9 zYi4Ffr*M##4lk)ipatErj*yY%*8qa1DwslPseks5!GQ;XWO?DX1b;p3P`sK2&leL5 z;>pNlQic39oG$f|b$0@K$XUZ8TCM|d00N4x;Xv)yn%1vg)UCH()r~iPt-I%6*J$;o z4pp6=JbOf6|NK+>qpyBeU;NDDI!AhGKB~qF8O}upUkpJ3xyB~B@+mo8fYTRb7!XtK zIt92P{w;uF&vP9hT3#QQgPEzqX>K_Im@p3W0eV5HB~dKN=u;92=f#L?jDSPg&<^QC zq>_w(?i^HAB?mJjKfepe>$Gl`8MV#P@j)IVxLj_V3bPm_k?nQ$Ax|dX!52pl9XjU0 zs-ULnA?sA2k2!Y74K$W|R7%Vj6j7cPV`OtOtA2l`#Hn6rE&X}|9g&9xz$eo*N=PyX zXzkA`){QC{dw90i7r2{0!x7P|KYdZ(<0|0oolR}Y)S;hl!;G=g^kcpzF(-(iq@WJE z_E_jN-`6voTz>1zU)EE{PpTe_^hPK*VvXxUa8RL>dY!=BdmTU1Y&?t697H+GC@*rAFIfMq6=S+tq@0 zdrR5_Hq_VMLE?m9S67!g5xBdeWfsm2PLnrq0JhieX%84}tZ2Nwp%xwL&~3x{gjDF4 z%9>F67HK+1XFRU}0#HI^IN?ZU!UEU`78L5I4+RiQO78>klV=!)009|ZI4>7x=XCD$ zSsm-mN$Y!@1l-cfl^fbx+t41H@SV_Ur!R$|e9looFNnPuf}u6Wq$kq?9^(wh zr=6D>+U=4_wP}ltSgC-hYepsBgB^g~XtdOh_&zIv%#0k9!8Zrzp}{{R7u{jkkP6f&WrLUOyZ=5r{~pk=Q;Xw0 zh2gIH?NCSh6(06+J@e?@g9djWxIe~b~iNzgBxfIw?X%UJnr52G43I-8U-`G>sXX3)uh*!;QT4H+meW2>)YSdQ z>2Wy7n2sL`7)Qn+dVYgVsH#dX=QXq#j+0-NkinRYAjqE%y@)Jg{n;5soP-QLV3LE; zt9d2B)1}Nas6&xN*wLrYk1SmstqZr3c1mzS{7NniTn=XHz| z&BU&=#Ykt$m=%HWT>wkTdmuF8_kr=a-3iD@AYSUyVFp_A65)wcC$W*o6hlx2_{PI! zN<3z!02t#45?__a+k3*p0wwo%_)M8Z!}}59)7J>5VBS>$@l6U!7L*S! zpC6;l$n&r)?4E{D)B7LK*$3w3pfpq0<25?sV{_+Jj4?BxE#q%P;@WIROE|Yns%7iD z+PZm9tM^v4&Z+nYeA0>&1d_f_O z$!*nPY%>O|_Q_&b-?6Aqn6%}sUA^|s1zo*%OQ6NbM(kXx*sCiEw+ z`Ix@(XW!PSYW2>=Hx-$j8gH$Ft{f(y7_0NV7>XhUr!Sd;|1-uID~Jr#%g{y|Mt3ka zF=;}uf}}`WpN9(LWELoeG|DE_sY6Ad1`jiW;bq{WPG)D}6{Gwge_t;l8cr zw8kRyhIA)u)P}xB7?v?%TlLgB{-7dDbd7`=vd!BX2><{P07*naR3TMKB@~a-+Xbn` z8|j!=r`hv92r^?ZGZ}b82(n<389U(_)fO}v3>C#gUJ6p8JRvYu3W}X6Gv;Bew2lLC zb7fUq)VEN{ada3dm&F#xCDS&rn77F0w9gT2)j1{~d-y4lIh8~~miQ}Y?1iqR&e%e!s5rAt4HfV81 zkDflRXPUn5{NSg0!fZ3&svH89d8HkRrnGpH|EyB3SK2m zU{<0px>STmJ&#YMv1+x83tD*gF@5bXzN2qE_l!;sCK}yc)rfoBNjs9!)?!xMjpb=# zk|m&%nFDe7-y*N=cpzaz4ccn*D)^S*N6UEIAkF%w>*0rW;@F9DirR2>TEn9NFEjLK z!5q*zdN(qb=|((+yh;>zfQRzB9}{S*g42aT{*)Z}nvZJaL=c|g2XqaT5Ji?lUXr!M!^g*n!h z1?^P}TGO0vjcc9XZ1tO!9lf%=qc=7Ny12cg<>9h6hIh2ScUx=Qx3#sqtZ}=AUJT1T z*VyzLU24#^9=5*ceRrZP6fk%%=rHCfGoFsQ%A`rkpj$FQwkC;?F)%giSiA*yVxijv zdd6R=i&cY(3!FU zWcr(hQp4=NvJT^2H}kV>*Rx#Ey|GtGo-Y?tFANG6Wr;ZbIS%i~4S!<% z-bY1wpbwGoyaFK2`*}p=<1OHcf96p506P!De~29h4-JKPkOFgSfX{IjlfvTJaTMRG zhx|MA(Xc*>+*6XvZH0^lpHD&|tEsezlvc_Z^aQl3)LN~M@-8@jQ}#CAx5W2d5KlJm zU(_cI0BO(rhk$19zSKA9v5dZPYG*EVmXr`e$3Za;k7SHHE2yRJ_Gql-jZF=PBX!7O zu%H;Gu*c%#KRrC=)J)LtvTkP39l;X{9#6?1^A8eC-x3_Cqx`ofq(t3m*}*pxIRK&x z-Y*1VKEUZY-YUytl=s`BuRey>oHs$85mBHbVHUjs9*w5ht5whR6|VI5EOvj z-CgZ6rXl0Wqz#9@Mekd7+7VuP-3}Z>Kr^+DaP{^SJM&ZHkzV?@f2r^P^5NYbDK2FnYzmfT3!e+?( z3yc+OuE^;^e|}!G3-g-e^ud2FWhPYWg$TvG@R6@TsP{hyMeoo5r==b1yO8Plz2MyQ z^7rQYc&BjCyQc!Yk3cmABhBCsS_<5-3BHhnUAKJ*9wN7%rlE2I7dq1Bs0n&JvYsZ@ ztVYeec4J?wU9FqDtuCx>>-?R2x^VNBF5kMPYs}N_)m8cb>bbGAs~t|Q2gpvxf-d9> zQlKCUMN>jQvjcOaoMgy2nUp6aJBdp269j|c7=o&*MlY*&*A>8iu1o}3J7leyv zj2CrhWku(&Ue)EBH?;?Y87GF?in5h<3_df4df4jmqbKz*zw^g>_{qoh^7%J)_4W;A z5QjD^cC-Pv8@4P4L!0bYC`y6;19UN3G&(862k{u=CyiY(NHo>4gLCGd+hfyd9I(-@p_R4F$DP!6%KF$CfE(}SOB9jE_U#k17&czVfOr4 zHaurfoYM0r&S>f0P{00v{|Eiz`#;dN$%gLDnzpbrlUOUWSQtCIve;ukGP|#2cnOv; z>={s@!oO3{^s#A>Jq%)Gi~@QlEDCc=3;NR6zpAf&>l=FJ*=Kd^=rKhLn&7aFSXg#( zW_%nYPHu*K1C5#Qmc>3}+dB9)!l_ZkoCVX6vHBcCjm|#wupWN&oK9eC7Y;3`X`ml6 z3eVwjeh}abMelhFsacF?3i>bU@xd|v>4Y)JgE(VM%t;4t#d;A!lq{-c4cQ%J-Cmin z_>Xk;)-G$(mM-1g(1jZ-x_o<;jpw><;$Sc1ysdLGv(1KLC|oh*1XS{q9U&`;4Mw6Y zal+GiSWa?=YMHyv=aM-!WK$F53dg}6$B?pGa1E(Hf+YXp`!RHeF3bOTUT-4#DSe-7 zu_gxjhO!Bjgsg6J#dQ1TZCzp`jojvQ;wge?VsiP2$BhS_8lnHse?7cjq+PCbUc>8W zs(sMwNUd+_7(@rdab2GC#cJhQ~5XugrWegJK0Y)2~Vq6+=$KP2{ij~&5 z!+7iRC9SNk$(ho3?*@kWA3PXGL1xn2L|SNro_hG4zVl~)f(Ix4>h0ILs~IZ5gunzE z9hr=&Hg$}%W1;U*MS}+Szz767Q9=73YD;FcVjRwkOuyvFA2ad+ubaT-d1bst4E!RT zDw`BQp5d<>VM?Xa4qEm@3-F0}mQkknJk%l=m8K7)e`>s^{L{og&H+$$PYYS*5EEeXi?9ZZCqDk@s#*ec(eC^1YT7`$9quIAREDar#~V+zvT82lROz zkE23Ua_Fn7Yt78`)#v)a^(Rs2?E;mT<`>n+hGh(S%k_YVF+_M)D>1$nUF!D7n6L|p z_>qN0J$>YqzIx=OhQGO@AOGcF>6M%3bvf^8v#B)@8y2@CNySbij`lli)nTEtB`C^l zOz7N=B`IR>c|F_5o$xv}PT%_DKh|IT#ec1T_22v}{a1hS7y9Er z{u6!u>)+6qzx)+F`|NXi{D~*@@FS1t7}rpfD>oV=*wUI zs-FM+=k&a`&BUTVVDaD7g_|onfAcPIN9Q?m zzqEW$H`Z5lcYA{?w>|BR1{$)s`+OxO;YSM8;I;y0^o~5Fu0u8`w;+G;FWJ0`>jR^D zP>WLd^>AC8T4)88npUcD7L0YSph>jH98ITZwi!(aQ4LxsZb7z$Cfx5sw*V`^`<$GBcCo@H%cV;fl&*qOGS)7Uz|3U!F+m@*KKKQgeQ>*vMesjTnJ!eb0Lmoa>AEb-GXXOr>L5Y~;sW;N zSUmS-abKRz4AIVV`ApaXswg0jX6Df;l(?sZ41E&mA%-PjaAx|mih+D;28Yb&f!AAk z(riEG0g#Y*&u>^@V+=R|c=ApY`6iPu%#O-Iq^A&w8Omb{F{l|hRaGlMH(e5sdg$;`edh2mf$1nd(H-=l< zq@PLElVZc-U$M|<4E2P?zf}sYvawsk9vKs1^e^m@KH%}bWhOdR?A7wC))Dq?Vv@mw z*`-CDJaa~mKlPNJdhR(r_u0?s3t#$@zW(iR>zm*C4)DkN<{$kLrv=~AS8xu#_?55d z>CZf`$DV$wgfFo0fBEZQ*Jr-)1sy$c0_SK}0ekEHCTxcVDaHW3+9uD6z_4mGZ;-Y zV&U$DoGbY3AX8bBlrM-Gzw<{r`p9Xn4=!tcWmOdea7J}7 zN#)6+E4k;Mm6h;}ovwB=QidKgE4BRNJTW6p6S|@Ya!Go`%p*=l_N07_lulnSVho;e z94>%vU_I?%+1q(cGWa6FZ>Q5knaV>QgT7E89psjxPd~q34*1lXnaqA0KCEZ^u!2uG z-^^4NFXU#bHzo@vh}F9fX*rNX;0xq|{F#wKw!dbk#3JNz2ba?rqr9ot&&VEPSkfwe z5XCl_1^IEahcoQLBWa^<&=j}Jx5pgBbC6ciiD|C)m8l;&!VYA0=;8azLFsP+Gb#bz zrepUES~b|K8XJ*u<{1daPB;>+Ds0FsbTv2wZm#yr?*IT007*naRNowL^uAEy7>F^( zOgsUup9y4s^?8~j_|KhvNVdAEU;W?*dh_kKb$e?^1ME_$xo%LPW|psv$&yk?&_){E za>(HJP`~|nt>m`OTQKI{+^qa(fy&=;$LPB=um=xz*)0w^DsCfvan->Qi2{yJe{n%a zPMy@*#~;&^&p)TnzVL#+^o_4^h46KK`5P?!FMLraPM^`3`D}p%ubevXGhx>ceOL@G^FhqxtSniBYIlzCfY?#c3IapmREK2!WCV5?JZq+{ViR- zcv&kr5_F|52Kr9SoN3Iuy0y8fy`4SD*YZxPQU=84mpHfsV*#PHv;9zZ#dEtp86AyJ*F>y z^Q(%zN;j`xlm8BM#$aT;BPY*C=1Q2{P?b7aLFB{tK$#KGp8uc@xU^=MON-cqz!DPB(OeeW zUfn2$D7PULs(>WddGS0*RwnJZJtQhe>GZf0kTH4*%mqPNpd{3zfKhSU2E8+3<8rAP z>q@09on_iMLJz4TfloZ1ZD#7nMm5Hkkgq}X$0f|pDiI$jYaAmRA^K+ChV(LFa4mTm=ceKsq@J6Rsv3Kmoy0-PH!T_oCD>cVkZ3b~ssBzj04@E?m=EH0WO3ERTfqLv8jx{V+Mkg&zMjQDRrc>6v)URa$rOf zl3J1`rdCaWT3HzZ; zycy%s5rPIKvS2bZ`D3LcDR@YpE12VVA(|jaUME3E6NusM8T=8D;m?$RMu8W4=paqM z`tXdRrCV>{k>@a#EMEc)SpX2t0`FT+)*Dp|e zGx@`sCo{Vbo>#`;w0V9o1@vSFCp@tjkB37QJA^(2@Qa7mlBqli!ZYnM8JRqT#u@q^ zV*t0dw>20HC{ckH+hJykb(GT|uyb`QVJ=8#aZI2J=OvRr!cI+QqVmyMEu1-`$4(yC z!sbBl{LPQ`^56ehx9{F%m)B}sMU5r+9i#+w1kl<+S4T*Ji26&d_q`BojFkbibIh8X zpHqds*&Xe1w4Bv3mJ$5egC74?NRM~eqA}wfF^)+L@;hKR`eW+(uL`&H1PKLSM;*mV zGUiQzU13}7XQEE)rwW5Tb%X|;2LIg&2$W^w@`rvrfJ_hmlL9l7*~fzi2VnMYTM+U7 zcA8AyP(T)>!U$6cQ8n{!8Y!B?817Dhma?qKdSo)D-uYe;>(REs$?UOK%_vnkbJd)> zdR~***Fd$_hn;S5w157_9ld_>s$OH4?@qxh7cT3aJ9l(zXGVn6J1XvYdrZ4_82>g-7MV&r=RJ|B9 z;tsyU^X@qEqMEQP1!ciOTLDg26QhnDJ*ukTQ$m0}Z76u8kJge>$U-Mnp*&5J9y)zW zU;4@mI`Pm6UAuh)?|3XXFkZ*o7&vGmgD&sG8J1w0(NAE)jeZk65sv}xK@&*yC!Q&N z%ro(W7nBIWK?%7;GxP`qyrptN*^ChyAp{{s@R=j8_?r`0UvYk8nY>2WHNnr(A`Pc9Frf#k3!OJoiL=0&YeB2XXY1` zuH4Zp-~X{*|IKf-F&=2Il6(Q~vT?K;u{UHuO9EdCDvy<~;Q#w0p3X8eHEcL~I8GJx zC$imn=xEqc)?%a_O8mh*$~@1ZlvB^+rEki>yx%7?0DY!dh=|OWJ86N^!S0T7J5jf+492>2 z{gy7?xTRajvO59gG$_>S+CX2GChk0NQtL6{cI8xNe|+3{E})G z*aa5xqYLwzts_d*p$tKS!epJYp|D14lXyM5KvmT`boj7pLL?PDm69*6F(^?#M^mT+ z!VE5U5euOzM|qDw_oTK)JGy!ImZ~bM3e;z}5V|b7VzN*ckq2a_7lxTx!QHVd2-J@l z5*I!}k(v3Ib`lXnlqTx~A4(8{T&g)9`9xtb9an^AwPR@LR0k7pfdrpmg0iHJmN^@7 zz(auSPwaI%J_tUs4n9Tnd*eWeK@RSiCgjw-Cl?wIKT&X`F3E_x%cIaQ56Nzkg>pcIchZ<1RA}(D2IDFA?eJJJVONAR|7^ zV~s+qlgAJ1x#K5wvdvn4{cXMYlOO5w-5c79Src|!*=ki?rP_Z9J43r+Lul{E{NcgD zmqDxaM|W_~rg1hIe)Sj$Gj=TDq&OW@0E013aAv?aCLX>Yl9v^L?FaSgK>#Qfz#T18 zDCQ;tl{)_cwCx?0(^4u;#yCo}jltvfhE8)YT_B@9F!bz$}*4& z1a;(f#5=2$V-41*oy|QhbL@X}c~!TO{uc z?(FTVo1ax52cTkYO{`fRV}uA^gKm`{0#tdde8osqp3E2I<{1XjB%xgQ8El>^ndM{D zFF?ve0&nTTeV~av)6Aw!=~&nSOIdSGqhkvTTAH0z6HUP+r?7*;Mfe!3_vpA|Dn)Wd z4ZT{m0$#UdX7XPJY-yiR@D?UbI7M)IujpgGla4Me>iI8yMn@kyrgtvAqv3du)>($Z z#NQxpbmP;<~tq`<%_=`(^oLEs%tvrrNE5(8AIYUl9oTDJ@_y%@P-i7F@C=S zaE13pzZ(x_7_8Yo>zfEf67sHR~L{Qa2DGR+=kI|(EA=E zO_7^9h)<-XKpP`g<`0`?&}4AXV1QxhOPV~V)#C@D#|wzZAlRf#8wXKe#4+9q@Yg{; z_AL3&NzFK)nXkPqmK3fbYwTX~@?bJ-lz6CG(3EgLP4HjPYcLFePZ>}F@nG9Mo0z}Z zKb|f;CW519%)2o+a$w%2pj-fy#IhlmmXrY&8j&n>9zb>gp6BzEk-04XZMD%w6d)2oRnw!?(xY_ zwGy~Xe;^Mo8_+@D-XS@>r7%gQqHHPzW|U0dQhyqS)M>;^55RK@4zK8meek3&X49nC zmVCf;q#1Vc$GHYLd*ZnKS9Zpuft+C(MA=n32uh>~@i^Rtfnyf;UA)yW>F~OlC}p)# z9H~qNyFw{PDDGID0?ITy&YUnbdgQ6c^o6f}NyEudSFc~;%3*-hQOOw{vY0wLqqhWf zK5mr2fDDd~2@1rkP(gf_kH^fYIgQI7XiOe{2pIW}!=TR%kvjkhykhs#j)o{@)G>B0 zeVn4~`P9!AZHmrpu)l! zHbMn`8{CFu1x_EDaVP&%stxrM*8z)17Ie;k>V9rst2eLfZ-4ZC{rb||+T{4$*o9RX z>KZT>hQwSX+A**){sUVpo(K65j1KqpJQBy`sfb`jN&o;507*naRAV8|@GsCPB!f1? z_L)jLGtVg~0^tFO)b%yyJ}>T1JiSpSzyR@(Be`=EgBIr&G~4TGYhy!uJG+X(DK<(aeLUkO>{+1J`uid_Bh4og)q~BiB;uO(8eP zPepl{f)`s>=aa%>odGH;B5hu)x*ulpvJgd8@O}zXpslY7q0O2>C~cIrxwEGmTkE>P z3HEZ8HJEKQ!igQa&U0O0oyoeO;i%R5TQ_h5xDvQ?TbrE94Uw_r<3YC=2z_e54^lp% zKC!gdIAl#iA~#XNByT_fy$mqx;3+ZBCnL)kG?D$cEZhtrlYS6YOKh^}mmAb^LC%D40f8-g1ZV46;_0V?28#|eW` z)6B>tRX~^f`!F78C-B#mu22FHry{uL6*g$gOd*-7)UKoh5PtZH&q-+^9tw#AN#G` z-_Ch|l~SYh^Zt?t)23ii0bT_Q!~p^OaVr)%k@GGb}2z%f;fI zu=NhCtMV|D&X(kMxI;$USh4X0LXgjWN3Xt)Ixr;?vXu%*9{2f=PF=%AcF6+Y>`3YQ?Rji*z-%PPY-Hwxq+iM%Ta&K8H zyF1#QOw@rVqYqirCxkL8Fv-+KlmEcW#$48J(|Qi*sEC)T>~*B6SH<8VOhA_ZGrpos zf_7!gIv8*QOQu7CDS)SfunMmD;DZzHgLEKX!A$>)R)>!rQ=jXA3`0fzBJ^-Gg2VIt zCt>$!V|YBHwJnWdLZWWR;vQj)8;mgvsLgr}LYW!Na_7eWg;^50~TnF4fAIEm;mOmTOhMGDWHJg&JThctw?Hpar&$QVPK zfT5M>Q<`FQ{=5q|&8CP~QzwcT{dixZ7zDI8<_GW$gJ#0QW1kn8bYN_Zv6RjUea4-y z4oTB)2o9*2X3$m1I0(JM#{h5kF{9{iU;$h$uAft{)XAcTF0rFTf^h(7OO6bfyE@1f2*9 z>St4n-1`qbO+qbGrN$aXI?DlxD%$UEs59#V@w_6Xmcf|58BaZ#%^X*j|#mxrK%@0 zV<k#vu*Evpv2JDZ6VSrH7NOj$I!)>fuBeK-ePncox;64u{$B}8N+FAb z0!>0tM#h@1kvNsug_qkoYfQe;myynrI%KLo=FYL0ti(&*(80@E!*2%$YOgCwRx>iCj8d z5Mnk+4-hQ|D7CDviN$%qk;s6BIh)Blga`8aW>y5=;CNsRB$TKUN#on7A1Xcj%+q@G z@pHPjx~z33-ISDB^lK(9Qnvy}U=jq)kx5LHC6qXUgyx`3lt`i;a{_%tVRXWv_~_xc z0%ac0GROlw4Rnszf?|wH+;Q$9fO)v5qq8$Qb>f8TS~a*Bsq<6Au}+? zYXz&oZudbv=0HEZ#4>D51v2wWj8?hiGYFBO?KDZ3a&WxlP?o9UK$MJWE53A)0kXWg zrU>-zb5KU;^*#!CV=#O0vBBGYls^6`b(|<~@8SD_VHOtrM+uo^?>8WOOVXEEoB{Yl+^OYt|_1!J@ANZ z|JZr_=pj9KXboe+Th#&XT#7Unn}D2(sNL6>J{C+ zd`WlDU(mJR{6=s7^rw3D2S3nne)L2A<|jYV>py={Z@lzN{pQ7&^a`gvZ~o$!I{&L* z>#bkCtn;tGs<&SIwcdI2Rb9RKj_%#QsZE@x!C+S-u7$?TyZ>tDBuZ^GjVjZy7LYX} zJz?A}<3?O1CYfOh)qo#S!F48>XDhkvz}x_zV=FankUOeP7VAP*X6_O|OogvFN)??( zYt-Rn-dMRKzl*PE7ir%*!$Xi?BZw!m6UZ#u2_qNCFJpn>a9(Ccjp^&L3{Nw43HCEh z=Hn)#l#y#@iqH|j1?k$`-IFW)kt0WRrR4K(yOD63Y9GGhP zoZbgAJ&)ki_YcGYV?zD3DzWHJFr+@0@0kOi)88X430X6jY4p%{*kcTN2Q3LXL8YW3 zZ_+GI?qTG7W^Q($noN-u$SgKkJ9Hzc#_5hBs4ZhjGPw%8XGG+vgop@dMQHVA1;F92`Nx5X_vYZq0_3TZdUK7A9bM8RM~|r5 z+S6OUD7^KiR(7_u$2eQ+88)CsA>0{or-rB)0bLmoWQ8eY`D;OfcLr@vlP`kVc?S;X zU!j32WlrIz>!C`fH@+Y7#K#1a;8Au^P}TG`zjR1Ve}*-5pdGFRS{Bua4T#uVwIc=M zJq$_>5z#u*340GHDUiny@jTh(}z#eH3AcSV!!yBclY(O~_Cw(ebKF~6XD*WWI1=h_9`ymVE!uH4eS z8+WvP>z>x`tY~9-LwlQhYKN`7%bK%|nR-?WbBkI$bVR*{MNQ!M>hhXyUAU^-Z(h{u z>zB3st9P{W@;h21T>j-7y7}U(y7H4>>e3H?uB(6lb6x(sAL;zx{6KI1_2282zy3S@ z;(z;F{p7FyT0i~X_w>u3|4eVa`MPf2xuMNnWW!GoShWW;LG4_nb|z}VI9bo-b6_wp zSS+#;6%CesQ~gU$;@Qpp_9@l(Fb2I^L*x6F$v)i^}Quxz~pxKLFEa2y{>_; z1~=L+d|*BM)RX%9mtWA+k8^i-{D|gf8radw@cv*OHlo#llZwr5q^*rjCUvAbMD;ru zh4w|dbf%7?*Qd-OY8a;3YOOD7Rzqck+KGDTbiB zsg*ErEygHP$H;?Z*yLDM9#4D37siCZd)&eNQ-EC#`35h8!zn8hmUHO z`#8@x8H=@ZNaeBeoqjy-cwmS!40I5olxdZzlvFYBii))ZGwnPPrz{Kvyb`QoY*Gy^ zO;f3^gA$`oD5K*M6Ui{C6Tk^dM+=?A4vOi`GfR$K$ledrl25;X2o9K8`N26~)omOLv$}QlmE*uBi^@%^xU~rp*xrZRZXK~Cr;|&N73t(rxapM4R2)%-kLUUydv=s z_#%MrNVq-(;$Bl9@ECcC1+&9}h_rJjzrST8||o%5$!Wwk!w)l2>32Ma*`6*jf*Rt zL_GN^DLnSYK4C|E15xOY%r4*#f{~wD_~jv|LEfgM1!u`?vA=`it~Rzdw6=1WUG+8H zxpq-Eu3XTK%NKOZ7kJV)f$I*}FY3;nYuZ@9tG&S{SHdGsiIJU|IrR=5)6(gObn5Y^ z^zd_^(ZkO^uZNy_4tQQ?o_R9gS=s4&kN9qsP^Ew@7bf%uwvvWuE^r+JFqlU27qq|AR?!l+4cclwAwDaoQy7t4L z>6O3y8~yx$`S<$yzx(%k`LDmLcYgUR-MV~PJ2;DeRWRuVjpr)4Q!wa74Wns{zGSYr z8ytYyRI&IY7c6M*02soO5L{pQyC|{b$sa?)0-hwp7w(5yQ$i4B1Hj(_z6di8YDr$#3HPI> zip8*|-a@a@!>3Q`h0i^wZ+_*A`tlb)r^g;Ti{eIg2p>AGQJuA86RobVX={Cx`<$jnUs1eveJ9wnBE1K@F71&kti=G6<3M1~YeWE66=x2v6v4Q;NjXywjr zT|WPg-gxyDz4@D0_4b=@=)ybPUtTz`8`rPl_3voF@zs#)h{W^~W>n6CB_O@jkj_LTVtfPC~$1u`}& zTc?@%zD^%MrpJyP)%@N_Z~p8>z5J_R>dy8$yE<$G<2Rwp2`z=TvJf61ls`TYE8V+I zN|w}XYIdYE%D#xQ5GLle)O=4n&-vuQ4CJ9wwy2ClmT_o8Z$^g>9nw6fG^3qO-F3%i z?VfzGkLsi}k%twwp<+{4StiYRSipVS0`pFa((JX>(MK}jV00x<#4SWcY+lt=^eqjr zH(M-BIKkK-+s`Z4SoT8MPX7Zk?*0;tVo$?1YXz^oY(r`LrH={&_w2 z+0W_RXFjV(fG5BF6`lLy3p)GUb2|LkW12mCRy`cgcpFY4UcM|5a*UOoB`T(8-1q-HqQ+1S&$ zYDSOu=Ja?oucu}f^_lr2dT#cRo}XFL=jM*;S@KWsbc$E*XzOL{?O**+zxnrnt6%+Z zf2UV}_A_0=_OAH<5{P3pY)omcr-U4&5MWb8A50ZO8Iy|qk*Q?VBa#5^Kc=jpL{1{d zf#4r`L(7@BFUT#}{f8Ia0l=xzxRyNZ@JI!7M29}11s_7`i<`%pcX`LmrzhV70fLOW z4kDg`M1u}^@ZJjfQDlGtuP5LU285^^4wT13^`hy}!mOTn^dWr}@9gVe{gNI(_XsBpxeS9`WUid@W^LznM?f?MBxV=URJwiIs zzkA7nwlPErE_UqBGi5mx&iO{2{*ug0CLstS^j@Y=>M}?-t`3Q|BJKTn<1swk-P*!~ zU)37BpRKhu?QL&M;|ZrMhEPcn@82+lbfn2>q*eb9Dem6W%DrW6P=9A*Oa5~f9lU}8 z-h=${F*+3`&d1{gjG5jCh^6Gy_YcJZLvAYM)MeCV>HcK>AqZg3AM7TvNG8_>G7(=1 z9daG?Khonrg+IW6M)a)LpHuL~$90yvl?Ay@pmwS$k0GezD#39X6hH!zRB^t^QBy`fzxHVqTeXTN`w6uGqfLjH+Zmy$6dcBTq|_g zpCvX9rw$*{DHbGK+tN?}?t6NRyWkb>rw5F=Wjt9?L6t;bg;GAKDJih=dY>JWmEf1# znT6R|?zYL{v@v1d5@j?|W_)_!p(!|2Ks;WD;A6iAjSg+c$O0#wK$lHb)6(7{Yk>mlhLYtL$A^LuDsvyVNZQ!jj0&;R)!=^OvMf2FVgH~&&E{K>cV!neMz z)2u;{^&LmAHDd=+(wzK>eesZi^YW=_v89MKmRkL$D5yiRYn zO7C3P&7ZuiSGhWP@vpwCH-7Q5ZeF{l9X4Pi7O-J$+C^;kpl5s^q)9L(fQ}SUs_4Mv z2ca33DMMli$r4%r;2SI^Z2w4!qx5z(P|FT@Z+(M%l@+x+dkP4GmCnFZB#sYEF*5Gidz?J@Nr3Qzt(!<3 zGtF}T3KMy#A1#%8_SYgnWvxzdGjt^C!9XlNi<%Y+dEp}+Abky4YFv+WTw3o6ufPwEf8a+ z3PatofRLjEe#b`((v(`d7c!>#x72yZ7#CksaTuGiS6!7z6zP z72rBpx^@H(;PE%pk%Pw_oUTB`$ggPNkSi&$TbrAo(Ne56KgzoC>YIA?l~;9VcT<~| zG=~0^f}&vo4FtRlQ69DZZzjxSb}T+kuhu+^QOrqc-|^{Cq)a=QLkv!M!ol+%fNYcj z4`h9$sE@M%baWW*mR(P_N(q|$MCHh#MV&Z)L^U>fYinJ1@7~oan}cmO6I(lslb!n3 zK5US;&Zc8)cTYp7ADj_e(qM0DclH1SLT^?X`{K|Mre;PqGp}ZTNi$1_)nB5&g~JN- zhh+1|6cwCwohJEcJC_eW?1v zZLR$H*Sh+Hmvrq{uj%gf>)IaeY0zZt^rRhd4;pC*>AseEFLWeG#90(za%UjRaI~_z zkVRDg*b}x_manYbxT;J3(~lfsz4G@z(l7t!@ASrxey**nw-nnXLs60Y`?!o)KzTeY z;Dz)jDDr4aW{2=3+KGXy?c@tfP?AHb=XJp&>UrRK9*rSTCrfyj21IoV+!sZZ!%qh) z+AYq{>JgOrNse~vUZas^t#Jylxw=j%ZL2^}FeRJdpHeUwVxpShBlsmh^{~oFXU$U2 z@tu46ajg$`I0d++9&~WSAU8gVLG|dzLst|!k)Hr`ES8eV^URE>$;?3|kBssLjFNZ~ zj!gzssLL9&;3sf2D2N-n2@b&~%Coz-M;DAk-~vGAUy%tGf4wXVvPpQHz`U3|lG$k> z%kP5yD43|~SSgSnNVsf7jB^Fn$k-Sp0y|d1&^Ap^enh&su&Bj_1PalLH{}g`z9PqiQS#Vq)M;IRueGWWd<@3qXU}lKGyC9j~eN1Ln zl4hoayd`9vOl|M(>JE#@#^$zSU8|~UvO}rsGL^FPPdxF2PM$g`U;JH{ylrxuM8qk~ga<&BDxUJ#6wFNKQO|aLp>1F+ zEjDQqCF@ni5YJ7dZf!@u{P9n9@%Bw^V7rFw1XDIy0!#vx?16vesOVKNwb+IRJ2Ths zW0#T=rwj=`BngX=6}&NVD_~$D;xrLV6Z)N|*=TR2CHBLjQmE9*ZK!F2O{nUid7PX> zI0B1oy0FN)ySA>YI9WGt-_hOW)$%{l?XlRkDXW>8(+u`we({jzfd1TqV$+vZJ!K%p z9`?6U$09wkN)9cHa)H=r;t(oMcmYL?8C-{h)sgREOIae`(#C&1v?cBNorgdIXH1m> zcfhd?@G=6GqNh8;HY#|2caTqdVv4QF7g1p=Crz#0CTf$p-RM_Zt)+EfKzxFe)-28` z*HM0lKW3fkn4yF`Kq_yNF>jS5Uqk97uBPD&YheIdW^%_ z*(=Ms_w!fu_K#oG+gw}Sx_eVQ-B6=m))0rIoY04)mi9hn0lBKcAAx|J(Ix!M1+qzI zrVo8gSHj=?KmMP3>2Lm4SMgeR@7z;w-0Jx3f*w0@MrV(nWC38}f+^X)qX#Q-ipHL4m?(u-yOv5CBO;K~#PSz@n+o zm!%Hk+mPrZbFX7MhUfXEuY3vbb53t_gy#k#!;8Q~hG420WA}RP8Jn0v;l~WBZeTXGBkk!f$z`EX@MiSLx+!`v`cEZ zEA_gWdJaZAqn()B!aI7POS*WMGLJn7Uj8Zk;W&UuC@^1jU2Dtnfzy3<~_Qh$ZGt9+^?VwqS6xNY~f#oHaC@8j^`aZUx!#lDX2jFs7Z-}vb-_0He@ zNY`1Lw>fbcjCPffCq+{VrVJRg2gc!`OrDG}n;=6UHn(# z>3{sc=>PQJ|9ATH|N6huXJ7cDj+{K9&}-xjN-mJ{2k!BDtboTIN@Ds+c)i;!ZkxN? znsD@7$A~9HC5%$QdyW_;Cv{p|qC|-V^edZ}n5P(u#MOr?@*D!S3`KRuMe-Kl6C4OPb_qP*ro)hl`d&AI zchXK2qN#2gRType>h7)JO|P$eH3bWWZmc2>kxJa#PAS*^lgSTc0mL%7xrE^D^;Gp{ zB=|qUusO8eFFc&!Om!wz>^A0>7PWNzsFvVkZ*gAr+?>>)j4^0phLMwyXXWS(qO6Jv z5vVIwRYi}}*RrN`J`n2D=MTaGW(KL2BJxCgFXtP{z z&Cbrq@1l1(ad13lMjuq<4d{sLm7jVUeJFStD5+y+s0?yWC~Q?mXC1_2W(pxfTMrfC z*qNj5>v!*H4_&U&nZvz#O*Z!Qs~2C=4Xz%xZ~`XaKCo}%aXksp05NhhGUc82$KO;s z%uYM9S(pq5?DQysKChli1JT6f&-ClDDR`Y}?BjhMPryb^DaiB^u(VOhqAKiSHfe(^ zn|ti~C#tk`;;c?S`lL>=;2%Bvh~|#4PR!1;Q;*smcN(RD-4~8qE@8()DB77-5N6OH z2px>jfq+~Axj=vh<^x{4K{2dPl=nQrb`!GjR}SRWYFS$*9OzHOhO_|_(EI6(!vSzpq?BKuXvCiVe*wlmm8mMw${fP?1Gx0?4En(+;>Cb9mVTrZH#0`L| z)}SFMP{!0LNpjpy$3ee?D0$q+l&R6OneA{|GT^`dSg8}1S7*Wf9M>ODsNrhhhOYkY zkM;WZeyHnjzO5l^gRtESVw64(Q$ilI1sM>aF;eIN3IFID{?Gs2|51Pb=ikv&&p)B1 zQ!J#tph3?xS?p;zS1TcqiiT27abB$)XHg{#>eBO77x{HLq^u9<4GEK#+98#f0}>(l2C#(~Vl`vTja>1%-Oy4-jkQ}YL-;r$&U zhNx~8VnL#T)0-Geg`?>wZ>~EDUGlNWtLcNE z4%u{>9ARhtZa<@oAq4esgcI@7-jUPX053Dj4~$!gLFPc7SM&M@Ky?K(1~&eG#Re?ms0zkn0AEMd9$$ zl6s^^g8>dqD>{~$(R6BGd|V{%x4$2;`>~G|F}UxWszB%sv&gZ4RSOz%EjeM{a*PVt zs0{cK{Wvrl$DSrt!y3_7Ze|r&xFYu0dVQ%TFV^6QkQxLEW#&E7L7<=a0Jg>*kU|6V zUP9^^ATZE`uN4rq9e`o$$W(b~XIXL<@>LtN%5k}e9G&}#w69Bk>f2IrLdDhrLsjxT zB<*qsm#1q4>qZ0nYvwtOCA=|YSvWeT-0!J5w4_O0X&6*sTt|&LjUHA(dxU%bLwtn0 z4o%Q*V;WZ`Gk~#m^f$0pyA^h~o@iSmbur=4BrQxjJzmf1h1nx|5-(=-`gz^tMBu{D zf2rkbH<*_R{5Op%fiut!&az$v4`c|V|A3i{>44F$6!#hbKK#sqhA86N-VXOftGc(cq8;wFBkkr94D*GVO7L3- zJ^`6X0y2pLCC?FiB+0#W(cUPB*9=Tzc#5=%EY4?+9M>UsuY0w0ef6GJSJz}HX?B26 zcRn0aG?gEYfcfwlY%1xNbcW`cY>ahYo9rqy0lik7@}x#G=0dzZ#B4 z#loMPsIIA$h_}5;Blxt-z1CJcMh+(0#ej{CUZEosCcyg$80ZFvg3jrC`Z|3Sm#l;M zwB*z8@0$ZIV*xqpVWfwg0Qm9o@4L@`6hDIEG8{vYhf~=w z2cKKj^cOYmVhKGhs}zzcrd*)vvK(qcUk4iU8{!T%o3olV1SZRW@_rB#V@uGVrA}g5 zRO}p=N${YRC^xk;jjE*uh8C5;g%~MQZr(5Bt_noeDFB4f*8@B)9;zxw6(|mfB_$wM zQiao!xysIayPAbB56{f&>1IiDt22eW0)1lit4mCaN1 z^bfybP}!9s9?J&1(|a2m+SuKZD_k&BKxhq*MZ_7M{K?)!rWZmZ!()t@m=GWUVsbQi zm*v0ibM3|rz4gv{UAumZo$sKaVVu*tG!7Ezv%H0Is9{Q!Wd-^$Dw>&U7Md6W!jUBx z7$5*W%DUm6{7+XNJ#|98!;4zO*|<%g!Cg)kE>|dH!UIh+^^*)5Oi==#DUsJfErV7s zo93!YU*`Jbv1gvp?UlRQWTDRBDRc%?=f=uuqh5w@$OxDmIf3IQaE0~VzaQ~Wa3BP) zBD^F}2VIHXP{NorO{2zlCromp&@djdN~>!dT3KFG$8j3ODh87~U=9iX(gi(95qb?l z6QRg4K;@ynvYd`g7-Cc(12u~?(6IZ@G2r+F<+@gimG+=%WouhkZ{N{t=g;e<-~2{D zeDOtn_lG~w-+cdj1-|>e@9X=2{{#KxCqLtU^A%mZbV)1j+;QCM{dl)*M?=a8Umb5}Nmt716z#d3CwV$UW2V1;7EtME}sQ>kHtrTxg>I z;Q90dCeVOz)4=x(xF3IWM{X?@oul~D~3wm#`P#+ zCt4P!4yKMV`YZm!Z>t-sdOZd7OEWzj>?1N3mC4>n*WbRNn>TJ@q$NK|%h0IuWH2yV zCz!M)DHuAL#lpkx4_Xf6094oszXrf)$Zc5m_V7byGTPmLv*f4j|h z3O�$z^FY=@c6H!6|8iCmre$93Q%(bYM8IU;6*HOT`s8behecSF;cvbY zTAS0}UeL|!>$-hwUG2y;A9|{p@Whd?xdQ-GqH7EjmEcH#_(0kfX&e$0kz_ERYNg8v z(*>gkqgE?>L!H07qF=moK|gu*b-nWD1zosw4dU-wywT5Iy!WvcuP66got|yP5(w}h^ z5c*Mb z_W$?q^~+aY(FWHVZm=f^Yl7xhiQNES;-2nM{tyDpl8Pb7CHh+*s{dBr?<2$K{r7?Q zL7X2l#0RTa3@vLLX0|;f(E-=8r8@Y!*N3<@D;goYYRVm5W?=3YN2EmV4BVNUadU1d&WE3Ofm zPLxND(4ia~$(1deoEL73E9euC^Mv4IzE<#Mo8se(E$*Bv+<>FW|fHy(5o4gSgN%+e?i1t{S@7(PaxiTh@xC zFk~iHmUndb_NIn=nbwsmW&=4_#b^x%K{4crCkwhoHV8lZKoQm?)K8{N2mMVo8O%>A~awGuS;P`o3Dj%a>v5hJjp8yxu! zw$~|2$}v$Vsz$*~6;wwh1P@XbpgR%?VgxMMHM@=DDD!6@e^QS>{kZPzZeURMG>c)) znepQ!xVM*>6k}J>8I5V{?E@U3Cng~a`IVyNrvPIdsf0J*O(qifqXk7KGdPZ5C_;jV zSs5ddaRNH{x4E{d?e$HRNoAt4IwiobFtUj$0SA&@xsfM>Eb^?3#~71JD{U7LOKYvLVFJ9IPj@tUlng%-qb)!}zb}?fhq>1`i-|go3?_pRD$LDNN~{T_Zntp=GuUoxo+wnMj*43V4%pF z;4m=)|0e2YIqmubJ+Q0vt&dn=_N+AR_ERctA zB5GuP?n_FtZM@9jO}+w@&z}*1B0hfuCgFPB+HE$syD)3d0@0MxDZ&h0eI+rCFWCZK(k}b;BOUK zXlray)g@K%CSgzKtDvRHK(}9eSwH!!f2UV|_@ny+5Ju8qwNCe~yOu4U2-F%aZw zfh18P(t6aB}<=h_|J-q_Xk)h%7PxvV#-|CYNE(ht3Nd;W z1STqgG=dA%w9hiL0s)0FIY9xfkg5A1nLw|y*m|L|1Y{`je5WTF zIfg<}cLcqOP$9~XMg55XM=!mkU%l~~uHCw>Z5*%hXiwwet}=Xb2PdLnh__}ez(2;x z@m0$S3fa)?-I*eEHyG8;jgj7AKK}AA|5|_dgCA&XXHS`hsHJihE7d@c>kY5(KhNPj z(x=bwg##{IeH_>hM|CtBBLu&fZhr4b4S6;6M97%`V4sgCBjyf>`Umu%RkP3^J8@i( zoI9uE=&$4O-K(JtmQ)O4Q}<=a;s6V|gJaQ7OwFiKZ_-1~dn^{F0jGQ;8nm@I{p+ZY zzF|{*M^(30{_ObW>o>H{b?H0{b+12zoe1csNi0tDI2CgDY`%pPc;<^NjvG=fjD?rDZ=ju0y<`&$|j@sMEu^_0^B%asDO83h0%lQA|9b>y4N zi1DhZnvc9k&l|OMP>d0tK`elqL9+;8P|p9)q?Td)#lc=CMuvt${Y)x};Zs z_+vJWFKTD`juwVvj{lQP6SL1@wh)RE#=`BUg|nW%~>$TCpTL$@CJ*JdAbZT1T+(K zu)-djj*Y-bXOT21IS}v2OIFc$g;y8V#zqOn>*^VG^{jG#UYcE0*IUrQ8f~$-tn9Wd z@;f?zb4?d-uj$^_NV{#NvCYtaj`oX6&4Tt)qq|!ZUB0uf-(0+=H!fV!)hjo2=hi(< zIBA$!Tv9^$)&>Jzy?#@B+q>%FVT&DTi&vOYL@^e~oCZ$3r=dMCshPO{+7W@yJ^H8~ zK6_U8{MUSTb~VTSmvO8j9Hc%*q>7P|f(F5LG)5{Tln#z7z_&3w7yp4Q?=Mn6hNz0R zH4s98*R(9J$@vyIY3R+*X?Hll0l2A+4Nd`=ikl+kB{Y>G0 zxhX-@1Zd5)w!Nd*-hM~Fe&Y>YxqVAp^pP2}1;L()y75p3y@?}Bjtk^Flg1snE~=_o z$T6^jW`mzG){3E$a?lJ%qdh#~HJr4c{^Ujd;DL9*)o9 z<43i4cu65f?7>(a&ToY+vSf;(RzhBfEE=85E(>>?Dh)T2G^}K)$3h*jSy^3`FrckE z3~t9Uc8AV4Rx7h{xpQk-_qI0`=Vvv81K{@2gf4@3g45;yna={J0tQhlY#!WxwAh~x zT}#Nn{Bb2E^dJY+2n8|9GJ0are(2aSE%@oe&QQbg0P+&UPt!%qZ4hnu?YJ0P0i&(2 zmlS>MH{TbO%t(?`QgFBfFiA8XMqV+_164(3lL^v*;HsLi#G(heVWc#!hU7> z#v($nL!h#UHR+l$!E(yMRd8f=u~Rwmbi9H|C8Us-T8deUv}>^YOW=5@nbSj|uZ7Kl z?!5dfU4QF!t!yr9WSvGzn&6y`A$)|bYLTq~zCF3}>t9g8Ezw^i?bJFEg^v>`J(|vVO%G6JjNi=@3*Y(19#S z5g<&`{!mP2nu0tciHsyu>)=@y6kHRmjuM}H`Wbe{W8J=SQ%wK| z6CDtw+M*hG=4~>#|s!cc=m(APB%RHC$0;^VK8Hp)~WLOT|bIIZZv1E7LohXTr8Y6YWsFuCeMOEsD(Z)~gH-qUf` zlqX_eC)aj$_pLYd+WFUXb9-51R;LJ~%%VEl_*`Vh9Sa_)w#+C+KT}#v?>=Vqxu4-} zk{6K|v5^s*=Si2nj7)j_AoTcykoR#snD~1Z>lr)#6Q`L-4wox@3 z;_Zu9v~um5HU^s-A}EsvIj*A2BFYgIS`%z2@r0bUj3I384zGxzgHMSGFa+HwoGw&R zAyg6-rxR+#C1#*}%sg!>WyCum!Q8=cqzRBQ_*9gUmo1rlQHm;kM}qfYM4PaTQ9iPl#(aRhGa-ui~z!%OT2c3E(* zU%RQT)eSY!0RJE-35tPCJj)nDnRN!`0!5~RXEK>?JRW=GQ7tVj==!y5a_>H7DI|c< zf88k4t}|E?`0&C+5?LOtFb*@bI4m3~`cKSHFk(i%fMJM%2>}Gpz0cAmktr_tjsvrU zGjpAz#_g>g7XOH+8Wgh?+}Y~}xp}5S4<4G3OQu{8~Jg;z9^v3z~^3$H- zV65Tp5XWYu7DqrBpa{ak9~=~y10ggui~ivgr}fB_&*&@P`HsH!$A6^9o_$91hn8TO z$qnl~26mCToKwY7YF58B2`At;FaAm|{{2t1es@I`bVNYlHs~>U3*{mT#U-XspMN3; z3@L!GiJfMzX+*LtEKKV^5%GVV=59odu6yh{vurvR77?Q4dI`b%UyPkF%AFE&-6~_$ zqFiT$3|6E~kwvV_rfV$9=WpKBIyR!6ThbmUly^8SytT5TUGCiz3t#fnuy%y3cj|Lm zFk~^lcH<7aO7wAVR`vXXQizIx%XVc^b;Mqx(^Uxac^GYP>lU_S6}%ls#vStsI(l65oGP>A8W^Pe|QvgV#OiIPJXNk_7i~}G;nFQt>-cSlbjxU1CL#Me5 zQX2sey)Gb7QB^Sse)C}tX_llC^@I^5P!+dsX$3ywgN%Xp6Se8Nud@zH3B4W{_)AfG%k9I*HSSq@XeN#<`NLmW5j5NvCZV+mQveRfDJ3=+4TT?%uv5 zvG@w5HVkB#VuX69JnP^I!Q@6GQ^ys79(CmKVVyaBTID~#*xI6Jlb9tb0b|TgqxZ)Oj0kifaA2;OVi&LsR)=J!t=(O{_U4;<=i(&{+fXA6UIGHgW*W89 zs1qDi6-|NuV=t<|IImOOqkidI-`1b}xBp6i@~{6}z3@lhQvb-3uHU(>TO4a9##|vE zVKC7wCo!jI7Ip5>2|c`USV!@|Co9`}>*d!t33y4Ht84I0g1z)%67(tS)8`ZBKw!RI z7Jb(&cSIy|qnUxwEVx zcB+5qup+u_*qw;2k&K1QBy3j4Nm_$Vn`!#!*F<+NU(vmL_cVc~Nuk4OF>g_ZlK2M7 zj4(5RPC;R&uVeoA08nkirlmYl#@32Dz7QlM7Igoi8Tjree}{-8lmdpxu-74HCG6m6 zGLhAds@^QtCCZ@RZKI$C)+z^^_T>o?=k@W(7>#8)&4Id=17-fho#QHNV4eIA^^7ZN z%s2`j+C=)TfCxUb1W&V!!TKBkwm?b0g#AI1R7c%0SGtqjYQmBDXflE!o&0|$oR3lS z$jjLrwXm|Kd#}Es3$MJY<>k8?k4J(G#%W6QJBd;YogIAjfq0pj$;?6G>mQk!{BaTr zkO(n?ClI476F3g)W$5?RCkLj0%q8T?Wh;Tdg+W~t)ZsDBgrJk5rz3l<($za!f9s;w zZrorWwyQzt3ZKhwUer;ku4oe&J2WdOz@w`fp*6EI$ksa{9U?PhQS@A(qf=wR1Wc-U z7%_=N-NfW3+T&4Zncem^7Msm`EAn6CtEuTnJGq-E!f0q!5i*lv48)l?@EG+3XPp+8 zsfw})P8YTUlzSbM|MLWcP*kXA)#d<*c#V*n60}bQ{Rr(A3)Fe;M*Ik#99mfE6VwQn zWM&MJHVA!(LlWpbhM`T}&0+p&|b4jexD zJ_a+BKX6I2b90&lJnaraRaHVYWM+!Ndyz_QA3xCIKtv2cO`A2Gs0){`YLneb3=#Sn zcbf#)3XTpXCBd9@I*2FunZdos@&4;?zNI&C0L+5MI28WV`-vm7YLe6gO+$WzaVtNQ zfWcZkc2b}H>euznfBEP7(x3i`&OY~?B6q6q+`O(|y!vZhy>VSFcnlhviS*=&v--;8 z&*;nNp3;{eeM(Jgn=JfaBi-+t{aUB>{r0d~iu<=P=3FEXe4Q~0Df;Br^w z3K{kv0`&Bl#mr^#lj`I5iO1=E5WKIch?qh^zgdV9i}zG+T&wh3kB>F)%a)4ZC)4 zxTl5_u*m6{&>LA)i!&Vw=|--1?qt+Wgw%wM$y(zS&ws1Q|2+D{tkkh=lm}(L6XvW8 zjl2%MH0)OwG3uvJ$^RaJc6Jn@$FMI63)iZXAHQeu_y>I#;8AjZhZmX>%|PH(8~vDd znzR#*Sr2-%3(B!(p-uD~B(Dd3kSn^SAuo|!Ksakj7Ne%+oyvlaLnY*~!;wiC;*nZ) zy`XW-8e+p*@Co~7HswozF9Ea;zJ7>xCKvn}4rJ#57^Ga8MmPZusWxy;pw+P7L5H{w zc*f@S(CSE=FaAasU;3qP-oB=d(H5JmF+E6@q={v@BjEI!(fG{ySuibn>TOCShxgqfO1wq=zP(f8i%Mr zdvhWvQZf+vS6U8d3VRx?>5lAVSWESL)pLR&z(g7Y4PtcnQ zK`9Y8;>zOjm=Oj=@(f_4@)MB;2cQj6*KXa`HH^rFMc0L(E7?f$dQ_i#_z69>bWD%TAJIc|hjeb~sGd4`M$epj zNN3o=jJayK#Wlcmqy;VV@J;uwmC5lL0@P%%?J%uoI;bCiDsk|Adv$dCP6?z&way~ zvFmO_19Xo!HaVKUs;!k}wOnb$Cdhxl&ljiEi%KzgQwcf@Yymu0fGlNlR6qxh5eb4) zX@sMaNGEuisvzIdSI}2s_vRt*$uOs922qn&-_hllUefiOm$f$9##tD{zog2d0>B4+ znT=EeI!tAfJCLw~&P3h!LUy|7*O+4+JXEG@8UGHl{;46Gd^LWx3ZhYz2DPb;CfLIo zB`2FTAPW}i2v=Fh_u$u+yX6lCZQQwo?u?a@`GK_>g`|$Qg1(4@MYc zEbj-?JR{$j!1sjY2>}%$uL8I&2X)Zs$7^i>4b*v_p}48)DbLJm9cSdyPq!9q|EC5Ln~=4rrWB>eTzfw(j70RTnenfDn&R0blnt7&wblZOjeu4s?Dtqg6>7onVmHk8fz zO1B^l%HZ_&BX;?>Zr|4W+B&_PcG*R=7@!`?Trutjo<-)wr(|YfN#~yaj6U;~Z|cmG z&#GEFjAvd0QR85`!tVd#JMSp(4%8nf%}lbMIC@H-d+3~wsMf;7bSOt1vPws)zD{yq zc$y>9lfYcRr!m(b{%2PsV`?y*31WR5dhmf?20WPcY5e1HAj3a1Q;1P@RpF?P5yvD1 zK>xWuk+DR^;-?uCHW+0bR=&UW@$7*7yBs;d4wZ$Ugd6gNd6&Ttc8tT_Z6$U`a~MXO z4AgiYo0rH=b&-?9mizve-FSz6Szh1Ng&Q}ugWT5K&DXQDim}H2M9^6QQ7^-LU%XSG zXY|HT4`-k~f$JXEkbb?8LM(J!Hf1IT6DQH`AhZzFl1Mlx^GBCPuAJfk01yC4L_t(^ z{Mcbt+k48(>#AcYyWKJPGi}`di6>-J$P$4tiTb!tmeIuc8es-kB1~lw^o3lERdr8E z0ke~fOl6#@3@`mIo}Oh{A}J4pQ4d~WjKqc$-ckeGwSo=4`s#1AaqYS?HrXAp4qKEN zSAt&&{MmtlWby2WS^NWeh5`Xb+SdiQmDDP~YMl^%s!NWGOb%7laWbRfM2Aw)qX8Sf zHB$S|CEb4QHLiuPX>U9bMT#&jvY~+#FiVaL6f$Ts3mz@wGGY$$TFjA;PjqT#GBZkf z2?BFyMugB~62>R`$ZcF}jQ-B5><60KB}5Skc+j0roKbkB3)Qcm=c70fijL6|?h zq(jG#X?A{=`}_f3Rja1oQ^gg5D}51U2+at-!IvP>UMe4On3>F`p!YpS+94ze_@Mi1I#SmLg+e7%~?`1%+7APt?Z7mTup=qcM)agyTbZ z0EE*}gSU!CJ;{}Kp2hy@&wYVo|8M9tNBMbXNqb$?06J7PoP@Tze)YPxxzbsJ!E-nO zXL|E`mZSe;)@TXEISvmF$40ZzU!i$D3!0yqDbUB^XsSw8hzg9ulx3PenE zm>7$@uz#JOAi;xB^`uxsd!(LuH*$Jmw;a4P<5S=u13MVOS1GGUi$3`ioS2)xeoa?i zeno3nt|@Uk*nmG#x5aKaZyd7c(I(M8SymSk<4VfJrOdc9{MQs%04Ly~3h4lkf*@4U zYW3i~^JdbHXfx4qWa*JnR(RvGuKe)lx^eM>HU@j@n4ik$Gen6x2FQ$_#5fb5Bx8Jm zZy>b92ffPlov^9dYkCD33HzvJGMl!i=KZ&je?RqcBH)52h8#pDxb939wm8C5?=P9u zA`fj&a{A^utvON8=+M?slgrn&j^j1h+)#$_9IbTheCf5i;`OhH2{150vx4VP(I=&2 z;q$>|M{A{k#s=gvKsW<~JT47U(6!BVZQG9qmbS-FJfD8&^RwyL|Ty33pA-}|}DjQ!mFy;oiB zz;3X+(SnebHzP7K;>Nu5J7+_>#nWJjfFdZp|DiffV45z z(&1Z=>$ZFD)8<|KH3&@&tf`c z#xxZgYE{C}6*BOKV3I2N%0HErCZT^ENYGng#3KgO>N-^W(fy`Kz!+zrtHFXojLNf2 z0|J1AA-6ZBhaU`*2rnK0`0C$({^om8;qxb86x}A&Jo9jwxD!zncBfJZ(x&MN&2X%@ z%8LK|6Hn;U0}rbE>>16V!?Vq{25W274f5Y2dgl4FdKUd3(y&RJkmrzK3Z;NlgC*5h zB{HtpF`)ekyOOC*5Rl_2u5%nW&(7$O@klAjEXV+vL73zzC3W8DtCXqCGwnLCPy1)L zYVpiDT{wS^;C#p&O)9}dOKPc}+abw&r(b;^m(hmap~b+m0VQ+U=W3QWBxuDP$QX|R zecr-lE5GR5WnjqOUqMmRm06U*d&b*1fz?YLxzbB{iumH7qaM7ehedxxz2em5fKkPWrDyW|gVz_7&+Tr){pNP*@FyA5sh|Ld61g zpoW#2rR{zeI8*26^}?6FrPB{SrfaJ!S_Y~2Wky@t2p)Z${O375br6@qqstn{Ct=TA zFWm-1ykx2`my1LF@+Lf-u0a;)BB)&ijSYiZqyc_mrQQ|rz7dZ~?5pdT|DBrIfz-UZ zsKwJ~b%`K$g`=bjpI*-sg0GfGLl?1%lrmjO> z9}MFg$hiHxzZfk^lN6Xh?&w0RrNG2Kcj2;LICnvdoJR?stV*hgJe~hhYZ}qCqBjzW z4<=&;f?CC(4hFz?0vxA1vj&sF@(3#MpbQl_9ZXW7Elp)+;zcsOc+ri&I=oouIa3Ke zu2>;J5p5$uMvEXIAj@Z2p2z?i=ri0rf`fjU&1fV?f@lBV9lI8&&+XgKo^0(VKsMr$Itjg^q;~ z6_^K!ov#WVb2<|bw+-fL4PK(Uh1RdcTOyyltpyFx(HL{4OQi-*dEd?*I-3Ig?$xmESMsY$S1}tBghEM&nh!*+O|dS zI(A&s%-%CkKB=YU1wjDKk^;^zB$ET(EAdF7lH<|$ylTl%BSCwr)7q=jzH$RHD!~ku za@rh9WKufMV5f8BP*IB;K4d1ys6qa&RF7Hoa^xv@&W(mV2-p%b&9IBueg3?j`1&{W zcZC^&_mz(md?&y)N5d zGDkup$)Lsg7jmi?t72Fpr-~0IuV&(cz8lqe{nOsC#o`PsAZOLCwmWvFHYM0>seqQC zK(N_BOM(ViVjs^Li`q;uB!aH%&z;uX#q;W|t_4QLXv8Q zM_3Rn2L^=@vHorl9Vs&8lTd9Xz29Ct6I1|FF|yKlz?IzP7;RX5vE5}iHTRmAfL>1#~BvlEu0Uj zXysZKywiTd8p_+vTA&$9o~Vr21X@`%nZW^N0$gFKSo0Qf{SAXxUdNMCTckCW~P4b}HoxDU;UN?U*ClE2!gt*3?r` zH(;pxNC!)FbbT>@qQz2SN%-a*$sJ zAAo1ya`;p9!?3&|@?h}|)%VKZPa7GNguyp6o|W1-kW>uVnJSn;LOVRRa%tAyf$pM+Ep$I)x{VZx*jn2LB_udB`TTBehfIw zqKcS}ctuAmMW$Z{r!CT^`rV}N!YXIbJLPjHi_hULnkzro5aBT8Oap3}9( zIl0uGhd%TcfhLsXI99hqiM*LWS_%`aULI9Wj_dCC+^6^4daH&PUeMaPGZNd@Sqhle zrsA+6m6;Sw!rrAcR3e{1^dj)dKLfHP-fA$UQs=S5DZ+;e_z*nh*nmU;DMbtLVv0CB zsF6{(g3cI%a%^sdnPkG&C%N;k?J>45V0#R{QVJTdqd9DK)E*nx6zB3=y8|7@c5Yo+ z)BF>U>zN0?sS8(5YtdG;#yR3DUcE;@2I3a@+l9$6tYt#rsUZVrz-D)FzAbR|*MU3mlhr& z=4ue_rBG3)1S&9<3Q%Zt991c-gP~MjX7ziboR@8Sx_kRH%cP_01yC4L_t&s zLer6#G!DPmGZ{in$giMJVm_@|sY~4@J$mtZefP{MEu$;D4js^e<44qF=cJ@>?}KLY z3$6s)@Sz&?HeWuz-w(WDG7XWwLGV5p2~XZ21Y~LTRB^{+=!F2sm26|yL61beRDoq*X#nW&0p8*{57qvE@-v8%$lIDlBSnftv`P9l&&l+t2I8M@ySidDwpvwt4!uf z>;@R{Ia~6<=dCG0j})pPx1ljfkR!-ff6ygxUy;v)(u7k?Drg_P4RaFDA=sflSioPw zOCKAu3QX_WsSkbR1G;DT9+fYg*V6eH)a$KD1TdL$cx$k;ASM1q>eDSbe;!2)BnI)QPC@ct{96dmeeWajh%)7jR8MZ1H2jhNTr(Cy6psvyViQLQ>S(2kG`eT z4?m1f%z3BsUr{Uw;JiP>&Sl;f{wsn5AM+t;1rC#BW`N}5Xe4Cnr{_1o3tU#7z7a6` ztYv^iN|$j+A}*@Ch{!N1)cJ~fo(Xi~eOAHHL(_D~G~FGlI)7O!=Pzn;X-TU_C5WJ< zAl%L%BZiIU#ybM4c=NwUKew=;`MEhc@|fi+QO+cjBToptz^ScvyPQ!k5QUX$_&}qW zF$9?-WuR%yAnH{hC@^p#nKrIO<`n>A-v5nq-s?Qgk~3d}A^UaUz5Wg^$b%D8h?z2u z0R}{I7yMkCAi#Nv(3(ay;L3mmh--v7`oMY#eddi^O|}aa16zs%lAva*DKq*-E&@-Y z9)!@|052gSn@DON{5$zqzrEJ_7uuLc|3Iq@I>#|a^BAr(XP=j^@+-a<9sq#TdGIMAL6MH0OU~iGg8FHbipH4Dl)Rtgl9=yv5s zpS&|xC-NK*77NTb07J*4m2mU!uI*{%Dk$kpdG^=X^%}Z%cOOC>P}z- zypN4MXkT*Ey%3$UDk$PiM)sI0^#{qI!NKb|STY%t*{}h@^Wxg5O9k^+f=-i>Lmz2h zIfF5(F*Tubyr}`ZEkhO+W7OH5b+mN}3-Z_*UHj5^bm1%C*4pWF@?TR{F7=Gny_Fh- zN(C#AB1$FWn7~oxrapLv>;@zy=t?vRir{s9s#GEBo51Tp3&SBpk3gd~^o@n6ZGF?0 z)7Q23m521gSHG?6?6mfZo`x>t7X27B!gz+9Gwh&YBYT4$ffx8CnVCOoNaT|ct3g!_ zQpM*4Q2c$}H#c?M-wTP?H8UBs7SR;gwVpsXnsY1aBing+TkI`-NnEvB8<}X4C&I<3uRBjNONt>V($*C!XSLkvbQD6A|7AZxz}Ad#({ z7y?yIfqKQD&x&TH&Igl>b|i9#EiR{ddh-@-+P*_-HZ`Qo;IHsN&Fdy9QWlsXLw0|9 z7%gK$*8u?2%YVYuzk^UAhB^QkU|g>(&g;VUD+ci;DJ?K`kvz8h3JZYWI9 zS0WXW)<8c$y5Bqrn3?2bNSZeqHDcGnI`a;F_`Gs(@-|Y>J6Mon7SupbO+ob!0Xl!O z`kyEhIr)X&2DCAvi;*Q+Wfq7BN=SQQ}Qfax)Ubw1nKm3@! z_246V4(Fe-Wj!^sNo}^RLyYqI?cD@qE2n|}{N4QVGBES`%1}@qd~O0_4h{MPCFX)b zGbljgw!o-0x*es1m-u%l&{%Y=(ALIUy667;^wIm?qphX1dj1@{0E=45hFXny>c zP@R_RixT%Epu-kdf+V#G0J7bXDoy3RiM zu+}&-i8y&hv>4WW^*h8753pK8f`W>GV0G9B7yKtdm(;MRECpynNOiB?bHzKrYCTm2 zG?p$Yc%cm0=I}9#)jKxKweNagg9o3|$uE9Mr`Qp=eD*B#4+%g@bwjB>{rWDB7{@-l z7ZtoFNoH1q{4ySZmpix&lE1G5U*GcE=MuxGk^sPerlExnJCChP^IG$t2RQ#cXGuZ^ zkYZ+E0t5#rdL|-gvn4ifJmsvcmLgg-z})3G{kXK zW)ezcHcAtz3qo9;*1)GxxhaVW31}Bl9$|o1m2DE1ZNfXlH>3i8!33S4#_{l zUV@I2d_plSX3EN=Qk8NZ7#Pr1(S#@SD-DA~2#@Zj1tx zqSt5JC;0G`exY1~;!(*QxlJ01967uIrnG*hC^TQ;1OkTHGLAy*{VM zo_v~Jmt}<*l~HcsOem$VFb0fMmQ&95mK!V+lB)zBDa&(B&CaqdKgq;NjAu{mNPdJP9Pmm%#vGh>=w@W-T%9=mkaZ*xTpA2E3d|FE(WBS8yd`I7X z?4+(PuBkmSt(nbR)ft~u#pLdD{4ivvATdW$?Lfdo@2Ims5wf5_H?HJ4tBYkZC@^ol zC@{}z;Kl03{-idyBk+Vz^-XgXKA&lmf@!f!05dzO`|iJ2?>l-_cHxq)pLv#J%}Ohc zQfu&<*sIPk!fv;idXDlHP$PfY!}|&eI^FhRD_K_%oFsUM2m1Ey{j^T6%(w$!H5Tv3 zZHA}Ljh80m>UPfQcb*^%n_H^cX=`kJtd_fEavP#mB`!-yuRhPd50zAhmiZoD#WPG1htzv25Vfvmp^;DrXl#V zVp2G`a#}PTylSZ6rvkyCOnRQPgBLDb){xOG5wic#g>RW%Vgk8CO4KW;=aZ^JLiCMe zF(vS!TS_PrvZYKwQdQ$7U#G^qDOq|2R2uzz>9-29okOorw!T4>)`9+&oV5{D42IKF zkQ%Fr04%Vwb)Ni(lU+hYb^GgH8rIQ{Lh2?dTa601a)Nx_5(A*#N-GWuXC)%8tQ(;RsBiRH2h z;4&&Z|L)|6j8Tc;w(r`ZBPVXv%+^_Ui6mbE$bvvY3{lp2WCoee4NN%_{pfl#B*5_8 zK;vSK=xi-z#`eaCf5$RbNyfh%y?1>a`Vjzn%`B>Krl0ByRTA}TU@q3Fzi;Hxxe#Nm zk8Ay*=9X9W{Kcz!t}NnUCnnXI-mLMN&5F#i(xQCMyBv-7C4IY{ z1f4c6)ZTmAc&$@2gES{9@y#UO19NW-oH=-W{)|BNm3TUkyyW&Vt#B|AIx>QmYpJK^ zmI=M@p1X7h0ZRAOvszlXqGff}#W@L^*A5MAR}#IFYLrTKs@J#@)?)+QR(fl6oT4?dXC-mg+e_5x#{eZ5WdQNG6ULEY)IJn!;5WqQL$Fs!2SumNI zBA|8qCBD~SN$x*9@83=221L>&f_g9w$1|^kB-~qeL0%hEeoWyUJh=V9|Rdm=W_w=mL;KA7-A`JU<@=mx?p$1e?`dP(`2j4NeU=*$TIaYM&etJLymb=LH&yQb-#&;8Z=d5ZCx-;TiHem>G3MGlKCK3_?Xjb>$^xawHvXDb8nVI{ zxEnaE89LggT-yqh;d9P@^V>QNTzcqntv+{Fc70JzR_i&t&%&9)9U$FyyEsPN1Sdg1rKt|x!z z5B2P~zpEFJ`~1Q@8wdjz4-|{8y;r%3)XkUozVV($RW@GVx)eDCS-o=`jrl;o1LWj8 zAJe5Yz0i|i!5^72#XxqQ(h9)@N052Ls6izhzUji%>w1O(c%Ger=g(i()2B}B!o|xf zD^mgj(v3ig11K4g5mYMJb}3Py4X7B1mjHDWU=YkF$qm^$$Z7HN4S+_2XonC@A($ex zM#v{&FsMLV$78e+kN^veZU`EHZ?p^0(&Eah&T$NI?dqHo0ZqVIc)MtU7T!_{k|N06 z00laTA;^EXeT}WFGF8f3O|{3y@U?|t0r1|W5R8_RnaM&>457{^6i-}clml)oYN#96 z&0#r_5xkI)H8W*Qo)Q{kY$(JgTbSdzHn*rpAA3@B{%eiYOJ$`Fj(yM0UD~m2tJ=_( zdB5QHKES9f%au1;asxM@ek34p`sj)NK!WU?*T^Kz8x6HMGAWqxtMuJ1QG{spA2eVB z3;s=#_t7G#*yO)Q-OLF9hEfol5rt}nM}DE!B`F~ndZE>i?l)Tk)lItPJ3SR+S=D7$ z>mJgV>w(X;H@ole7oQoly-H)J(dSCSDZ~(z=NUTJujw~&p+ufKpoXg873Jd2Yp5(& zZ;<*$GHQ9b*@Wwy;DPJcpUtol2X)5USL)~kf z2?eGr`mGeuUC5QwCGQV1Hg2cGWc91dSXA`XZA8J&L1$uIQ}s{5TRqPb?Jb0Qek5;Y zJUo;9A^9s`73ov(zJ~E``s$v1HQCVd_q>=~8IOGv6vkG(fcF?bVl z0E^QBwo1J7s+V0}l&CWVioi#L9iFtHK6cvxr=hliS-^@{^2J+0Z&X61@FqS0ycpm- zJJ6j1IGs;emBmboQDak+%Ge1Rnp8#+)TBBsBmIR645dOb&=@k?Y*9P0cUuanICEZS zzV^p@^xyu09{C;M55BCYzx9C5Jo%K?mR6i(ZRmW#LMYWGbOJ`*1Wt>;2XkhDO7gMv znw3==aLzw$nu=(io2nod{|+GL0v__%i^?iZfnaG||l$ zjTJIfDqz%2tDGO4J%34$JoXguq|R|HFdz!cs1p?EEXWX;JA^z2Wi^;$Fa?BbR;mWn z$WS0t5}){{8tU$Nc>Nf_w+H{H9L|@5baY(mOQ3?%EfUQvfXbHBGj9q{h+xW5B(&;m2 zb@Jq?IsnPDs2p0iZP}uo+qP*8x&j!2jIoF)v`a52W|^EeiV+X^ke5-8gX`-CtQXaZ&sRaW z+th?T@^Kkwer%rERL@U$6dN4`3xbtVJNmeJT#~=_l$oYvavg4W_t1w@r#^x(tOWKzZZ2oo7@isV366$F$)8bWhP+dw`tkJaz1 zx45L#>xqT25J8@uQvL-8JoP_GR+PtW3e{(_%nL;kG`$%ay)SM@uyYw`f9SU zF*Bz|E^nhh(eDO~Xz=fV`PUc0sONN+u46{;00fDg0yG$loi+}B79H8$7}L(w;E1Bs z_{u;N1dXjmQ+db~z~fLihmSf13qOlvDrl~$LkDVN#j`?K!wR7>IWnT-VPRTf%fH(jN{VV^Z5c=t&NE}6A{?e+ zp@d^LCaXj6AJPCk4LIu>P^N%T{kzx-@^kBoHubOi(9V?&FRBq#j0XUL!9haUk_Q^- z6m?KTaQR?0Fcz&mlNdN}Yd!&~ZwzitCk;xRWMtqX{J7D$eEEtN<`t82y22F-ww-;8OowZCP{c5R;Brq1|`D$5B}(SPLR zbGX5dm(Q;PJCM0UpXc#%??1sawY%Z zmAx^Lyp6waKq3S9xK&okZE9Wy3ZqJF?jQuM;59ez-KNie@+bAaJqI*+`jqC*ol(E) zX-KUCyzbBoRjL9nKq5(C*|>Qf@nNa1Q{e;>HY`Ga1JG($SI?Z$g_9@M<@`PJtr4OU zwx|NH(}&3yEcu%%+5~7bI;aC1$4;k-a*9;MpfokE$Sy!pNvU5c$Bi`MEf&;3E)_JC zV5{IEp}X!MvVKn!_=L^yzCCMcZ+k+Ar)TAVj(q#rq_$2@tCKgBK$_?)!zLws7HGsP zbkP@#gWiki!A5}GD`-qGP?6Tb67>SUtOod1f+8XlKf;z(YSCs(XlQ>N)4|rXcF%6r z?552c!`8Y#LX^+}COY$c4X{Rs^dR}W-iFKdX2@C2hM`U5*&_sXlsi)33 z8Lm~H3dx)?mlj}@1jscrS-|o6paT{8lJXLPB?hA?626-*h|CE#lv5{Tz#A~{3X~(j z9DEFonn>UVnAAcc+aahEqsEwQ{yXL^CSa4*V@`My-HFiF6;h-d5X`gzF_`=h{rF!Q zG#q|~f$+aS-|Ma^NTRBIsKp>w1RN#cKg?432l{BR3t-T^Fux@KoA+yLYl2KA>buYq z{l*aFhBOA0mv@7%7S7cT#en`|3^>CE(J*rGAcT72svHri;{l<}V4Az6j^pJM#b_HD zGd_@lp64rXFZY^GtLT4#qF2bd)fv;+#Dp$izO1L8dYS-WO?hoBgC;OIV+08i8P)wc z0#H{~QpLCk9yh>c$LMz;sl&O*WbD8bMpTiPd%f>Ys-qTJUi9W?j2K_K$lzSXha_6u+*SGWUdG0S(8*;CNooTU=CRDyeLaG ztXI;XbeZ`~N>oc&fT~_3RLr>sW0A#N8}`iYhL|6f&qZp|;1#zUafhsuGPZCFiyC5s z6GygYL2K;B?LK-ypZfTZ>%^9=D$ZX}_v#fE30R+KNX(|b zOq*IAZEBCR67Q&ujNK0nq1(LQ6mYmfQcwa$DNc}lwGM*TQQpgAT|a}KCJ|H)Ax7)O zj?18tv}T36$UL#*&}KJ$d?4kOpgl>*#E=^pl!d5hMh}7tfX^l_Ql530r*9=dmMUZ9 z5v+F`6Eu~XVwEG)M(zB)|KN`iq`I&sbW~6i>VxYJv*K+qtW+{eh7yL5!(>KjKo4>? zGLR>Rj1^#m(x#-1Aiz_PoZ1MxQ6}XGE8<`RQM?V9h-|S`v@3C_o+i$4JQ#`KiL?o% z0o)EQWCtPaA(3HG4iq3dXO-T>>3EvQ0Br({Zzc)4QPAPAkIoI{&aFfxVxB8+G+0Hi z=>n7I8VWuf^yL#X(28a=&Zr&*MH7o0PTTq%~n%^K0VrxA*gctHM$_1 zo;>e#*m_dDbE||Vm-eKc@VI)w4`UBdsdG;^st_K>M5N*{X9YNf>LGL zF*~bw9Y3n~zxPgk;Qo7b&mFhx$bkdeJT)Pq^98o1kwtZU=Z?A2BJ}{jWQQ`PN{wbi zO&~Eh5^^(x2uN`{NH7T-IcfAouYS?aYYwq%1-2(J7GkbD-WWn{mlAV0c|Tt96*N!UYkBTE zvMl7bq{iuu9UMRHM}jfAeb7ype{!2LF)^h^Xvzjj#$qPWAW^T3*fpR65FbYX!eLb^ ztDzFMBvPjxf;z~->1&iyMtfn0tA1aGJR9h}Lr{+ccRwX)5$_BgA4EMsqNd)K3$lgJ zSF$9tpei`AnKF)%Iw`Aty%Q4raWiZ%A{j&@C=?+{D88vcS$)8Hm3J^wY8N4;D0zv4 z&FL_a29#)wiMwrPMu+z7*1L`#)jhZ0t~-w()4Pt`qFWCh&@Fp+Yv0ao+O>`CoXyjk z<$P!pTSU_n9Zj?w8f)enYh)VBBU?O?NYK;(f5vJ*q7(*{1Ocl{3tE`FrUn1gp394@ zgjY2nQZ4wF2r2@DQBNHhF_eXUKN)-l6h3FNTMSxSUdD)AmhS>sGDXUQl6*qA!H9V# zS$!*yQ5D8yz$9zn_}aeG##y?PHH2O8&154TpiftPqc1a4Vj?CA{Cka!?{(_m^ja6S z`CcJ^vjAUsZCns{PU4VpV0XiE{IN)g{XIcfI8hOFE4Eh7; zF9Z^%2F1_-I=Gb#I-R~i`^aSK677EN8{g0u{^38=ulj$4TxPQQ&p&M5%%)rj z>aDEG2o5LNV!eLhl0-g$6E;C8AD1gW$=^C6q#dDLDittpf}LPix>4YTi(wBOyvbpLzq(f#kePw&3-4&8S2h>jlIkMrMGr$-O$*Hoves^7(~70Pfj!m1^= zB{8UhGtFXDVtd_eH^mOap0cY^r;^s2w>HE z?$L*}eC48s@QP)oSr`F)fKm?PGQj(ACuYb3pC<2ytO5psMX$qjr15m9LzC~U?Y1Y<-7AW z`uf&B=?Z?mXCHeT?Y=;3M0Xl}du0HSV)iAWpuwVOsfrN^AxCLV0fv*6s$xYDFwmgr zQ3d9!zN$pt21Ha{U`<(GXkt9m&h0ZgaqOVpjbh$)$8p_y%YGd{xJP#$IjFmj9n!rg zZqd8%II4T^I;#8bIi~mBdt4v9_f~!QzT5PX`)=1q?gj2Sp^w~sLLa{KR(vkvui}R1%ZX75``$#Ab_9>{NglIp&`c$ zzJuYb-5!%7Lpl?aQ)o|yY?9377lKSthSGQ{$`Z0gSCLmjzNjAFJCJopU7@@QJmPW% zF*Dlpa^nu#!D>?ti>TOeS{wRvV*6BAiB3n?39NqiH-A@;eCrX-Ev)J~ zI|4n7Cc9)cezHmg6+TI->nm&*o|K_zW>Iupa~7dJKr`guxFJUll}U6ZPj7sD0~wmv zwLN{^;2qbkUjL&W8>+wVKK>MSY5E3llgrl~T7bX|S#uKr6tWXSCUu&)kK8dV``5E>f9j{317nNnUw9A(U$!O^sd|Q)*ZLpstI;ZE}nZ%Cm(-67tTDb!ODUH4!Oa6kMvb~KTPNX zLW>~z`FVxIOyn!e)ES>o>ty(%#|fm$ygQOstkFqgTNX#fPo(U99fc=`8Nj-z-S>*fYtjK{W+K_ zY;{FDhm9}B7zO%;Sab;te5gcSAh|986;S8(y{}}nUx$jc2H3-Ws0Q#DvM|Q#^+IH& zpOr1essZvS=g(zQAd|lI$^f7o6ko1z2=JvwyQ!?zz@bSM5qH+@EA!Q^DmB>t2n<`>OLNlY3o3BhJ)+CO+0#lF&MD)3du(w*v(T}ZNq2dC!c*zi*xgeM3V`g6I+9VU!hnY_^d)pEGzlXE4Z#66X z=Oqo9Q2m^No|us}I`Thmbn@wwtd#oNvT3t6Pfe-GPE247q8l!hd1-k`B@?fKJdG&Y zz~l54(Bf$Y?;h#qXlAF`)Q%n7HQt_JoEuuq-nk6#-U%VMP~C zpXEs6JXpaDa~1L`F^Vv+Ks`Q;ZvVUv6{#1(vL|mCy!q>eH;nvc4PI|Q-*3H_b@%#S zkuyppp^x5gV7$%HIp&=)R^p=@fm*(lr&@o!ro-#&;;pK+LBWVjrwnQ&lhOT3uOIo@E;Av?S(*XzR{G%o$_;8#06xRm_W` zN;Q3f)R3jm>54iP{bnIbIRD;im&MXhRU@j&PFy343Nfe(K?P^o#cH2ddjicTc-8y58SKIfAVA6++WpIb^`jB zFR8b(q>3>QS+0V)?Q(S~57?b3aNJR)vNXWvA@r0HP~_^d2=HKGV)s_)`pM*MDu) zg$*cj%2i9GegUVw5kPJcr3%Q-`7@*>L;kT;+0fYys+{UN-)t3U31E@^IgPCXQ& z(P^lK@t$Do&7pm=_nX>G7u@*B}1IZ|PtDo&Qn)^#AkM^&9`y-_`H__5WGF1N_#1 z^Y`>`{(t|y{?&i=cl7K3<$tGt{$Knp{rZ3XclFQz>%XIa{Qv#y`e*;;-_*bUZ~m^n z`H%h&J^$5jsycICW6-jV33eOD3Lky{z53i|KCVxH;>Y#A`|r~6Zr{zK*2eY526oLi6Qd5cyP;%Qos#G!l1}M7|+p1iM;&scTa(h zqK%ZISKI+@$rROSG}WG%kZ;fXHvM^4NX=$T`*-ivj?J?Y^z?@VLYYd7ON;8lOU7gz z4odv8)r#g5CP9cB2tg~hjU;IM&RyENbGyWG$r?f8d5&AIVJ!W9fl0r{Zi%nbx&##- z1Z|q#taInjbF8wY$e85lOU*YEPh2HB<&#wW$%S%$;T6sQSz z+-|r*v5wL-WWJ6u$H&Ge)aMv*i8-*evZ9sM6&!gf|5XrsEV6YVb> z;5}{RN2!-=fVVL-G5uAz?8pb^S##?a-TIRs*T+8g32iNFc{R%WojVfo+ z){@1R2EkN@LK!;{F2jJKGZ30ZB?ana+Oc<^ZoBI)9U;(a%}i>IpueF0u3dZdV|Tq* zcN{pb)E(;4hac7h4}J&Vaa{v*J2OzlE=xHe$YE!tiJbiBF1l8#-!xSnf(*_W`gC08 zQAMO|ow+)Jr#hA7seXtrW9Xf|4L<#S1A>3!C0V7)iq{Q^xHM#3_7Mbk!!3GHQ6sTv9P9$^W6rG#v7TMC~hQT?4TU8IJ|>fw(71!hjrhrx9Rr7 zN3>`A4oyx?DvYt+)F`zY2U?0lEmU1yyn0d3J@&Z1^KX7fU;Ur{vA*$le@$op#qXydx*uXOGYzNVGOpVSW2_k|Y7A0`mEPff4_)yR5lu#=lJC0#dGRLsh6APx~p2{cw~{Z$&tgc z+fJxMdftP8#U)k9Dqs}FWo8N(9{uQg(Ak(V2ovaT zrqg(rJ5y*6t}`+%=IPi#pH-#B0|$D_TehK zh|8qiL7~`gYsapgYBL7|ZG87_FzBgb-h~*T)Ug4y$JWyy8NT_Gmsh>bsLz)=K}H4h zbchUAOKCVZsrP>Lqx#IJeo_-fSMyIjs^T&`a;wXV$R}Epd7srXsT!ss;Xz=<7+A$x zWislBnguqci@onLw|ysO08Kq$U{aT{sR$zW9=t^-j-OB)eS6}Mzol>e_HXOCZ+=rt zXP#5uTh%zhLWURXvtzubVmcety-x;O!AdIUCS@fLxTbS_U{Lb#$7ef zCcPp}R_P4`-b^*%B+xcFAn(8N)I(>qeuec^|Ic6fYiCyai)R+EUDWlHr_?`nMw{09 zn#7s6w~lM)z8yM#rum8Hf_0RsbF8|&))p+!b_FXS^OWDzGwX}(Cr!f?;jpDUny@len z(Ee5>wMoZAMy;T6&UrR>#x*-Op=l=C?#7t*^JQ-lwS8%*#;FUs^u@2~!C(Dndhpl& zxxVth{HnhDFaDKQpM6$)$6C7U(0=*PARIfiSNk{*+{vnT$IhMFGP@mPuuW5&wyS8g zbZ&83kF(M^ed)4FoNFIB%yIVEU+HSw%x3M_x=rJqF?E^X1(Pl(vLDBIxZy1c2tpPG z+1M|CYIK=o!SMGvrE51Gtz1-2Rw z9XqbEt+VRoL2J=;^72JJd-alRa!e)Xn+t=kmVhx<3C&JRdoa+AR#R&P1!Y-+$7<@` zsD==3yw`<4db}wTs4&Vggj(++I+y_k4pOx?zA0V2Wk_5vHejO@-_G~BlEG7+WjOmv zTXER?S(Ucg-9n4hVwbL&a8&Y8I?{=YTg9Lp+pu@v zK25bdEW`>8SrHG}jT833ogQYcra#nliz@-#25@`gK=#J%eN~LNnW+vUz(sQ<=584p zikqhO?jL`jKJ&?sY7;xJSHJT>{XYm6y>6`^VrwD`naU{j2x5|u4G2;#uZY-!O5|Q4 z_ky&KJcfcUbgIHO^x%5Mg1Yp#ZTl|WO%SjLo4Cr6z?m<7MNj?q7xl#N{l3n!Gq8M} z9aDmcDo8y7A7MWlV;z+oC$nVI(31Q(pb{blycs&?fm$AwEHLDr@f#0fDZoDhjsWf zAJSglcSFP0``@LBI}Rxv+NS>Y33bOZ4ci!oMkY59lLQFc^0xNntc0&DNzY!;g|B^E zU;l^yOket6e@!oZ^^Y`hZ9zvm6FR}Mau_hsPqGVn5oWfnD{a+&99PtT~&1b_00r*-Mv zMQz)>MaOSBD*sspWAc@3<6Sy;PAdxwa^s5S6f7t((alW81b5srQ;1O|op0KtxukB!NB6qm22MZ28f*W0*_3(SMj#0k`hN(z9fC@WR;l`%&=Xr~LxO8Gf>LGaLH zKCiB=;iLxCrzw3?z0Pl>lydu<6l0LvF{h=AV_jfAUAlakU6)cVoP85I15U9uHLY#i zwrjlAQes6tUSTNnVK)dng#{Ne6t7JCh@(CNUn?=#FUAwe1Z)yvRqSm!JUB`Al{`-HZZ-4QNdhn}X*E0`2qy>&p zi{)jt=mRr?>0wM5lnM|u7=4OIMY;j7!`l!44cpGT0t`F2E|0(zD#L$=HvlHp*gVzx z<@s=9``-(LKQ`cS8Nc+IcipSM{2%<{`MJ}2_+NcdPyh2TYVoTNsr~F#&CU-r(~~-i>dek)+nvXC z{HH&v5B%jntB?L){(|1~7k@#w|ME}k*8lKlbcFQ4pZ%2f{oKd3_p=|>zR!M4hd=id zI{x#Y&>g?@Y2EwzpV5!~hkr&t{+E7HAN)^#Sx0~2liK{Dd(>ikKkc5?Qqy#KX@QBE zYl4_Mxi5 z4B;`EYGwBi{_ZJ|C?^4vZotO7r>_QJlQ4eFV}%e@QfUYejqwRhLF@d&iXM6RF)hq3 z>iE%PI(+D$@O8x4E^8_^)WW`X4Ridx9slOI#L{Jl5-CQ8NSb=I@I{eW+ye{^j`A9-%idvS$haNTLKGw1WJ zLXRpOd9KRN(<(=1%dFV_ z*Lt%?u12dN3x@tPZzXv=A>JJvePUjZAZdu6FD)&}|1i<~{Jg|GX|v*w=zgK3ltpdc zzFk|kZdC>^V&z|OF76J#$vg>JE}2O(XcnDGPC2>yc79$C{=tk|R#zuLPjWdW`m>N^ zY^j$Rg*D_9Ca3k@4}V1e!Ds)Bb~l?^KJ~P&JokiFSFfsH^^pVhEmw*`g{2XI+TSZY zbhl3(@MPG!44FraOe8W$1L_Vc8ExrMo$)d4=9pt@tgQ%tGcxUIb+G%T2B%)og>OEn zCx7p&`s07|+xqVBe@V|j@szrA3yO{#UK}^1E7Zak!HHA`(vY0pA9`RYYaY}3JaD_~ z;O`hsbiUfIQxlD ze@cgMJF3REDXoqN4O*riN)W50hK~gfqrhAG4{|IwOZ8fnDh6qh$oHwQ|FItW^?$40 zZ+=S$9>1;=^HH0Y3Taqs;=pbl_~Zw5-+%IXz4x#FvX1@Yr!{^59g4T?)^Pib=4aZv zGMVf8M6UTx&;qbRer>X$E-U@PctdHrqp)R4tzBC+erS&--+e+eAA7G({D(iM_x-1T zQTP7JFY7MyM}GQa8f=@Wu3g!OK%mJ)?-RJaPP(wr`nLgMPc5r=C4^QiIhs!LAG^Z_t+9z`~!&P&UcSVhCas zwQ2iK-E!M)n%=QZzQX86(^aoxXBW6WcZ^XO)(aLsBI-a^ecKHv=Q!d5V5? zzsVACEs7z^SF;w-17z)GuB#OR7(-jHw$L>6qkhWF-efA?I65<{>2N(B(dkr6o_9#h z9V)2Bd~4vqoF7^!26PgKE~*C$N;ndO$*_*kj9P%VH8Y9&SFck25H-e1eUe>0|HD}sax@EaTnU^B+lOzg-$jT1O){;qTX6mQoUUHGs+HAM`O!kF zg=QK z#LBQ;Lly!aKqwlfyQ~8YtAw)_L5wNKGq)pV8i4RO{NZ7Ehj4@B9_DF)CvmR|NXc7{?6f?{X2+aGz4fN|o_J|9)Jx zuEA<$KKx0DV+52H$fzoi1><0R2xv-FlskUU2lCM}e=1r)f+ng0o*LtuRMfhxFN5yJ z9d6{vjPKokX04NSJu9{1CLau~p=o?z>I|eNd%GDEsR&7{VW={ff+bx!^{k%!{jcc! zSN>RQkDk&r^>YT7U{RY7?bAJ<`3b%M&;A*`>!*KG)5nfzfOB1jm^C8pIab=&bJGG7 z=Gwxl&Rx8sXP4vgA*BUE`5KL_x-xz_!kO<#Q7+D4o9)R0GLv8Rvn~_mBN){Y z$bi?plPYGXJ6nbVh46}zrcqgIOjVZY>C>n6*yB%VD@OrG_aD-j)6XQoeC~O60nVwD zM_Dyg#)S9r474^?BRZhTF&6M|;M#lmu#Vqxr#c*;tioH_3c9koT>l`?6z8)I40W&Q z3mui2dD9=k+bIEL?ou~&He+jcc4@VL=W)pWPw&XQ%6C(E3Fa)lDU@CU+ba{Fm&|8a5`R zAj1I{d0Q*dwEwmv`uI=&xb8W;U%ltg>OAN3y~PFkLtKmNi*uTu%qLo55K_{&qIoAtIwbuVry&c=m8!3)JOI1U;M0& ze){8@IDSxTom@*HX|>tVbyk_DE?m+dJ@lyl`EUNV{=xtDkM;Ne$1mvb{jL8&f9t>f z+xlDo4e;0hw*KaS``_tr*7?7yzx_A=uKxSK{XgjM|IfdwfASCiiT?2a_$@vE?Z?%- zxS(=nsIm&`BOu2=amzs+_~83=|1WzW@HvegJ**4WKre8%Un1OnTX$*)tN)7x7mt1I z>$-OKwDMt3lSyiJhibCL)d)=$Ap!xRTMcPlsIif&0UxUb0RF3QKA|Tk$5j*q8G<%5 zRS3%?bXVqDqa~Snd4+uGNUGmAzSk+m22epa-X$jIE5YlP%$PFxG_2}6Ss4_VHD*yc z0ZBGKuItMydgO^G)LrT7*#5&hvUi^*p#9pp^Lp;-C)LM+6NfSP2a>P2yz_Oix^JGz zlo;EonN2!!;x?VQ>n?3Sut!GV)1H~s7FId4JGaS|uaA)p=t%Nl=QRMdKf2#U2^hE% z5L`czxxf3itvkyK;}Dn^#y2yQ8Q_(v|AW3=N6d+UCeK&ZI$;!|a)Ma7GmSWy9G&&u zvw~eVuVqFhZWNPxl9&)FFt>a@kXC@>a6qxCRw+`y1)d24fL5Lh8!MMn0GB&`D$%9Y zFrufb^DzXOR0k0Ai%YsTx1erspb#U@yrU-0&&Y=u>Y3w}3!MG<NH!53e=GzNF(?e zHik0QpttKvh?#lgWByiy>)1>BP!d|J3fo(fE0L2seFrvKWDqPIa{0 z&UCGl>5;YT`d3eWSAXxf{-yrAU;S16{r}}3=wJTYzt9(d>kssmZ+u%1KJkoBpTDFF zoLgU7Sk?vL{NkdX=X~wyix>6Hr=QYqeBfAOc(dDktvg21lyR^*d&+xVFJ7cc0- zgAZxxmSOb!H@2@O9D3PC>Udp9=V=n z=%jC%7Z`99LS}~TNu!G7N0}e&)8_$%aj!GLT<|i351(&NZQ_%~pq!Pv{|8(H_^Gfp zs(IjJ-KOHnT*7$z>c1lOcAh$v%*CmxNsTk#dz|+#%w1P^d6`|o9>)NE`ObjLs`9zH z4i(ONM2jgfSKSf%5BgnW6VOtsxRZ|e3R5={ti3-`kL@!{iw{EN(4-0hV{YLiQPTtf7O3> zhk)dk+wRb3Kk;dO>fP_wWOs?3fN$&klaFaIe_a#s*@X6lT?@#;|Kx7L%91!mzWAvu zs4#PPqADc0?HQ`V86^!UADiB+*=@TRtB(3bsUbLqR`IP+MQQ{WhFpa#YG!ETDPqC) zm_R8H06ZA*ylpV1xA-z4YkS+ldrio|!Ch^MHWYx%tbq9{NF~DPtFN8`>Z$Ah5-G4& zt!XWFN2R2z<*F9@OIqqJXr;HPrIk6&FI@w!0@w8So_grDe+xidtty^r6{cnp<<5UV z*-Bi52(>f{AYEcUc*d{W2WeY@`d?8kKE(;w2<9k*z0 zb6ay`L089vo`9e)KK`Ko_CNjK^|!w81^tWP{I`1KktcNNg)7R2QB&46_eO-XquVS@)+COOPBQ9fBY5w>OcKQ`ob6fzP|GN zzpw7%io}-n5Jm3czgM84zh_EYKYE|;{ru;2>nA^={_MEUt<5QGM(t%AYLYgK4?m%$ zXV0ibJ3}K=Q7KQcR*D4Q79&b*wNeMR(L1#3;rv%Q?+P(!f-S`uqbhbcT$L(j4QQ|f zL1vTyWFW0neXYMU-u3GnexM*Fv!wDF!{mx|mQ_*&BvTF`E z&P}tr^Te$>cKCqSmKOEIV~^JE>ydspK5WzfMj~>j1|A4tozC z)DD7xoSh#-4jD2SA_F&|W+t;&f_`+p1rlInTu+fVLRZF^7Z_Z3Tos1comqvBc*kap ziD%vc;FLHxDNduOW=4X}sj77^pjVAn)8FcI>BQ%{N?nTwcVa#y=0U2{q>U5=!f$E! z?p-=@>v8SazD*TQZDkoFiT+UN^}?P=WrpIO5z#puJ_wEZ>;(RjHhB|4z-eSKXR3i=hZDJ69AN0JzrCZk z7+`b66aCbA(wZ?+x1AOvW0vqSrTWa>9?<7V$DMzf7qT&9LD$mw)-g?PpU~`{S?xHm zUAqqN(%z$cwEy@%9XxSRhfW;O!DIWh|L|_@-nU&_cW&0Et>a&)`?~4jGQR0m41V>c zfRS%mrc6P8hP;*sE$33N8PsF@?J`dC%-6r8>eM++u=PHJLfvxL9lC=v{@HtPS9kNc zuHl?7wN2-mRqgzL<2!$(|KWfCNBYW_zoz-~S2SMb+SHuTWP4nVyeT8p$$*?*V-@9k z!u*plVG=N81{Vk?v8t(vp#AsJ=O;2PY#-Bb&nB&OqW<{tNAyqs#s8r%|IY6y9~5e^ zom8Ps{>LVkTc#!I$KBg>{G%V#iBEr2!|gM=)Lm91Nqab(YRoO^g~uOPcWFU%iSsuC zA(IuTctlJw@Gw?UQr>82Y=XWy2Jt@(6fCIK8B-+k9?*A!_(~;pnfOeAfO3nWH#1dr zU_hso^C~LIcr|?=7wr;wyrt1+aNynhFUgl6jm?&3ShcT0>$A_Eu8r9KeS37rZ6`F* zX%U2-)$^xLs_d=FCutJ14;+vSU8aB>gU|(>h?yxvI2Dt>;7Di)xM7``oRk~A5M$lC zqHcx}sriD&*OQ71a(RBgHyt_Me*$du@p@>wxsFq{(bn^CHM~AyV+Z z^*CXqD-3W#?<#=mbq!B{(BOKyJ~du4Y9qRsD=_9dEap|K*;I~xC2Cd<2JZmbQFWeF z=jnxf^ADo}nn&eX&{mxBk;8|ych^o$;lSH@t{9?XmMPC0HCV`eN&=(C!(pMhxjD^U zpGR()#>XesXfz4r6Tt{ZIT-D7xeX|p7a;}>Fx)wFYwOPK+B`X-jMcD!7$ZjpZGlLF zzCfndG2Y0^W#Qnj&Zp-$fT<_1Bd!r;WL|Lxl_`h^bGK9(hAJX!bSsrB(q*n?5U#dz zZQj3AANj!j`g5QDjNZes-qPjEdgPD3q30faP|Fw2OE~_j+tYx+XfW)u1|4dcNd*hS zkfhGg)G88KO%?K6rF?~*-fZtS^*4=cneKXC(WFKN&@as-#v$ZU6>_U>lL7+jyd9jCP_ zqvVS?hdzN<*X7JHOl6g_R;IDZwx(yNw0r+9?Z(L;Jhop)Zabvo?>edzcO28LcO2Ex z+Yal{aUA}U{W^4XzYdc>!jb#YV+VDDdUvxhf6sgG)_wQg>!iN~!b{$GDO7#ACdLXC z5aEuvm35Fy(14Zh+PMq5_~mbE{F(FGvCx-@5cmApJ=*h=A7Hzkt)F~KODSlXXnmf^ z@cf0V`h(y4ef{P?`j@)?^z+);nAA>I`;#G;Z;3@lIJu)#K_4XwP$+~-wUYu^000mG zNklOqba!u+zD z!%A)F7!M6;ASSGorlz(1{`+F!1y_JLE0tjo49+5QF>pApglvro zli)885m`vKZk*jHn$ZFfm%o#|;x+^l9Kb2?m(=$yIUhfTq>T4Sxf`a?8COI;C+8OQ z(3xkoilQCZyGtMBEWf?h)AJ8KtV_>6t#Y`kNT4^2rlJap6}*&9#nO~fmN*m42`s9W z6)khDuxn^*Hh^=>(o8J=GT{gNJ!EXpX>Ba2p$`wevLhco{D|W6T95RKK2Bkb84IyKeNimDmlsph%7^(e(xt%`;Gv*D1 zB!dndf*VGM`n8mNZZN&2@my2vu#Lf++nfsf(BqslSX8u3N&qe{rCdhdQdB^H&E=q^ zfNZ>v5+mDbWZF74sTrJjgW$-|rpE{d8m*R+>up3v)DI!5-|K1a%4K%SR@7mkGuD|@ z#CFwllKC2-IR!WY4^x`0aMLhrs7rZdfw6^E*;a6F>X*#e?dR@IgV{NjcM_L@95-j|DLX}L)&Cgv~u>WR@bhno2_cCIgs0iXbr}u z1*%*bvd$}EC!JoHnQo}HW14!Ux~yLNQEFSRe2|nmSu74vK=@0@gaV@-^tj#f9RVMI zAycEAuiw`VI`C@*dgv1R$?FwhdaGi&)!{YD|1jGEXwu3ZPVvU=a8 zBe(9=vD^0R*4qy1$SntTc;9~Q+0JTyW?D02ZA~_Fwducw=Co117SO<-WCWiPeauzi ziISSZ)P|;sPX3bL0B{*Vx+gC}q3leRn+gc^vxh8Xs=7l3GesQbnQuR+^4as6S?a0L z8|c=1@6w?Uy+`@po$AE~P9v9^W13^)oI3M@{_Su4Tm9a@`5kGgtF6s(O)^a%ZOZv)h ze^HNr{f}fTYxPg{N1S;FC3JrJ^dG0Db>QCnbmYSyg7$q{$fFVw`*^#pd7|IvpL{|A z9x?74f)^O#drdJ^8|1?c zyimwIPeQaZsRrh_UVf!lD_pN+OukUj8Lcanp^TMEIAE& z?X#QJ#HrLgVAPVN#7lB|kO{Bk^twRb_m$RLdir|6tEbmRlTmd8$i6LiTJI&%@#_ZK zcwIMAt}m{u(Z_)?@NI3OyU97O$H-LYY4ZcW&1r!OQiC>f^=;4_W%a+>DPWUYja)4p zWkMIdtbhyA&h3C;KFQ}nL;<4$Ho?o85;eXGJCXp@Upe~P2qqh1*Vx*g9PbdYXZ5@s zGT%%35Z_l;S2TYO$H&5_!Cd$Go|tb5yJL>kkq5d1Z^AwV+C_`%f^&_snS;BvW$zBn zz}MKIf>cu#OocawKc=U$Dk)Zy7ZGFE-#nFHNzRrqS2qAQRae$)T@{~8K025TdJP9A zqi?H}LLp;;Rhg*=txJuTy3LMeckIxoKK%*(m0$Ub`isB#^SbNSBWgy|cqOeocUEV< z{VnycUxuKb5|QQ562}Q8^g3Pury1cT2QmVv>CH20HXB+S_B9ZUAcKT_Bm;*e>YIbx zC%z}}Id~9IAy>Mk676`t(VHH;m%5qf>Ly1MC2qq@+MaP|?oIEtCOX#EWSO)c7L7fKoR`TUYU0wgZfm*-9Qjstg{LCMaz=qt(-4lG7f zNk^xjcv2T0eM*yPush;SY&jhN$OkmBeTS%qprY0gO4-PC=FD0B?iatvmj73@ytJ&b zc1L;4l~4jVP(Co8dOvjY#&2(EU|~YRr6~7oI%@PY90&PPY)Qg`f(>ErGG| zakYV!wKe(ncbfoUYT9fglo zM3J4g76Hz}{DK~R@KIg6bXA)srgX=#TeW}3PR(7osFRO9rWMXa#Y*3-B#fxlT6sun z5dbt$He*sZ(1}f)9ga?$Sx(IdG?X0*N$>4NXkV=)j5Ndf(4}Qa|&b{005_|NCFnU;ZoqXZ_rVKc>lx z3tIfv!x}z+j(}yLzV$V1B^B+UVM7|ie2JYk0*UwZC2&7D6dW0l`P(G~hgwWqO$6uYY` z78ewdLz69~7H3p}K%iju_=?QO1I}ScqR3g(+jY52Yd)d&=Jf6A{|DVqxJh};td@Po zcspIg!LZgbGpkcCGk?4Tz-mfIgaqc5gBf&s5ozUig1qZhgB4|#XBslsT%WzJH}|op z%KH7%xAj7%Y>)-R&m_%ro`31mCH4DVbhXst^=q2DdPV)URRtRKD4(OO$Gpzzv)OLS zGLwSI1Aqz}VosMy?)>W@fILm>v+g0buDy9m2M!(3=FXS`^FLuX5^cPWS2YMXUS6GA zKD>>0G8}V=@|oU`LAH5PQ%Ck`&)p|<&yT%Fpa0zF^b7aBU$gV8y87LR)xC6Hk#mb) z)>jYvFsRTkr#I#*9oT&tYffl$b6R;B)$R9nnWM#SE2wa$>CYTw*a?8P=HT){RxVo< zRYGR+GQPc$JAtfn2$HA%eslXIr0#gvfj6f_!7H*%PCz}@jCpAl#~1_k*vA-zp^B_h zKHkvuwi)f*vrWg2?$fbj`*o`WXYVIiy`LcU@>>q?(cwe8bqMEwaQ}AgqyD~KTeW+3 zM%$+*w5ik51p3^DxF*iM27FQuWTnuhpq3&Kl;f{5jX_<0J3lBu2-E5w3*Q}_(4=! z(pp2hhC26fREsZMRK+TMSPmIrG0>7wtem>-*idI;LKTBC+`38A>;_ag$K| z1*yU~y1tm1LJh%t*V}c`rOZaWn!T95UH!ik`bms$Eqm9wO7Z}}45N#~6iOG!CkS;t zCXxB$2MlH-Dx5AeQ*heF1-(3P+!eev8ck(+g8-nA^J+$0Z+N4YzihxpA$8!?%w%Q~ zc-_HEl{Rml(d?#a1y*{!LN^8~9qwo&HUd8Bc(xe$ZEf&w@Z_i2d>(wjMGD zWiylSr1sg`oxwhS=EEP>Prv{D>ad%8{=47RfF0Hf8*8CZ#1=4c*np@$25Ok18u^$e z*a2u{O%;+Zt}SS;>S=)e#Kb6>+e9Sk@~Gv2B$C8aBE121MsN6f2?|<67SGXbW-@yz zc>Mw!>2}$WN648sR%w;3{C?^ybTUoNPHOw^t=h{f{?IM^b&$pHj_sSZZFWkVrr~!i zSG#FyaD3j(CEpovXtJC~yrXx@n=z@u-fE8R%#m*^m^vIwHDz+gZmjqn0%_vd-NF4; zCb5_`x#Vb~z{BZAinJ8vVh6(j$!a0%hAgd2XLx(X9bQpF0dwukE--hnJp|BQS=8J! zCzW4b)Z{=KC#u~_bUV)0#tKw*X@iKg#$a7zYweknCw1xh^J;{s9AQQ?(U46d000mG zNkl`DZZ!0j|DnRuO|ZwrLzIjk{C9%70~X zQA*8Q&cuIS+7uqO>0ebRJDS}c5Kr7KKNdJ{Qmni zfsE!Jd{}nvs#?gTkw^HiG(ctr0x6jPZezmum^3-AHcqm@CeHWP)T7RT{t5=9#7TKd zg6vFki9mzigbNZ>y$q$7IqLOd&)$HFoR6a)ArEojt7TsUwzcz4u1(OtXa6qU!q)xa zTMlaX&TaK?PX+k!qtVPtq*ef>f=Xpp$*O^>VHfBr75zG`3i9BM)QK9(IA*QzO+(}| zV0SeU5CoiWL>lg^a(31mK#snp+Mv3QC8^FQqyqT-p#10$G*R>-KPn4LMQCaRYvPAN~%{%#ZX20fC@M239k36mo?ckgFP* zmR6Rvw!Es6i665J2c2ukL@XJgkOf7!Z}AvsE5E^tKTS-kxqGLI%~P6Z&@zlqSX$8% zJ0F}4$O9R(AVbhGWV8oot0e>X!2hN=Wo-h9K)6>yLzXubV=f~Q60IcK8g#;Nh4MJ> zM!BaH)x7gcOZd|I<@MLIZ#Y3V72xe6W8ts}el zXbhzqT)Ug~SVX*hVl zo;rjOYT5c3aKYK0uO>Aj)~o$cfx(R-edpWIH|O1hKr`MZ*O826jR z6f64!tn`l_JEVPkcWQcKObw_E@Q_1)D`3yDm8g&$96#JE4-v?eM*w+xnL|KXjCK`{ zKasjh=6O(#ugcLGgBAtJ^#~yfZwT6o9h6G0S7k_Y@P#=;RDTy2#kJUsr_o0s<)hz{ znfwZ|@roY+tjqoJa@eust}}GNF+kGt%91XfKBI|+u4cqIgqPd%$k=PzoiGcIS^aK3JU#&NQPUSCd-*@!4;tqpTZ3=>6W#^*NxE-vkg9=OhkJVw;$0;JD30W zpxYZ`T4FcDx&wJ26R1-ngJ=QTO=FXjYT{7+2OW(*XQrprBpB-BXeF$^3?yp%f*Wq_1tsM>GI{v@<2ew#~-5G*@+RIB=QfzKG@DkN*8_!V?gwCOqE~D21y~UUJ>s^>Z6|j*H&>P^x%v54$oR0>(h#CC2o(djl zAz!JhzL-PsBIZ&3dgEc_z=5v1JW8BTB6uAR`|4q@rY6UY%O57;HZ zQ?JWDDR$ENbutqUAdxXMnT>!9w>ieQ6-=gx!52Ix@`m<=A}Jdrg-RMAl(kOKYR5E< z?f>*AKCX}5akugd*L3bX4{71t3yOX$2v0e3&ar9zENM8?)_lv9HcjZx`|i{66Sr%~ zV)1%!RZGafq+SsM&dpRIgCs#hQZmUb$gIxjjb4t&+iaws-vBVycq?eFkiXEeQb_|~ zg%$r=RwPT;?cTFp(^F$=Wkz6Ba4uf}z(7C0sfC=HrPP4Xpa_akgDJ?t z(K?9lqQutA13WocFbS{ij=WUGzfvVf0g0SDe_X0Y-;(GCr*9g1fC3az^?Op1!0q@v zg`K)2A+v;!vn-?76k<@tm(TuI!@b<@R7EAo!B=8Ase=7sM5*#|pu>UfRu;TH|erzDpEEbeO20oJey5 zN8kfkD~Xq{XZYf8HF*B@KwbF_Je-a}QK^c#^6?2p4By3t1wFnTXbaa zUfq7nA>Dd#zjn=TQiq8fAjnWB)!LTe%4!e-u5-x=Nt7i9H<9}LTMuR?H>MSi!h17= z8k3fnmvxHZ=JCfL*98I`uhYPZeKp}PklmLLe!N99@gSxPb*(tQAz{#GGx}Mlux!5nz zzrxNOe)nBEeAl~_O;74NTi(}zo|1-E-B{&T^a~lP_~+f~PjNB4KGZdu_2H(sb%b?4 z^o>pL;{$r|Rkfogc5c$1!+W%AA1nT8d_j~l7CreoWZ0MtMR0IGV^xscW4}&F$g|d2 zquDxCo`S~HmjFapKw;3ycmG)vRI28|lF6uS^lKrL{~U)O;jBRCB6Gu`51pmZvS43W z=lT^1XkSq_DhT0AgW*tXa|;?=UC?wH6bGe3yQvndWVLd+gM&UP2DVKqEvzo<>cvYk zj~-EsfMWToS)>k-LXsdUjZ$WnPs)mtQKxV0X&^M3kklAFb$XhGX@V`Rj3}r%Goh@J zBdkioK;ZjC4uA$im&CctSRS=yYEsQS*DAuZnQ4vf+@V313)`s(KUYq_KtMxFXpSMs z`;H-iyCH+uK1!Z2vi`fu<7|@$$|@#Rs@2aVQpSwBL4H*zQUxxGYuZ&m&0lNWj{o(5 z<9eeIVmZZdpv#Fteu>U%Koo`o2Khg} zJ99F=`b`b8JpJWufKg9oCYP(Pc&}c)stw2A;+W>hks~^O{J6Gm-3mQMEa$N`*&mjB zB!bZhpm$he8RM0qhb4~8oxihcwVsi4-E$!OPQBeWY|vJdYNm)zinL_@ILl@z8nuHJ&ALudv^ZEJ0FX)sBY``4 ziX1BBQbJE6|3+_@@CJBWH(fRO%R0`)SS0u>ydNNIH|n$g?Yp*W^OhOLx}gj&5jdl7 zU=tgRuRH(9xI3;Il|Q^A`QvqhGX$h$Oi>4sN(_N0x&b~88<2ujLT|-bh`ubD0)+v+ zG(t2Z02qd-6;|AHgi@CY!mbRvnqOPhD#uzV?<@WQkk&6yujgKt6jk+Qk9PZWOA1R} zO~xihlVM>+olQ-spk)Oy$b+W|o`{YI>MpOaos^V8q)`}Ta?vEktf}?>Y3{9Rl)Sa8x0E1xO37vWBP=T}4X{$CM{&N7}m?XlDIL^{X)l71r zU1HG8Oy){nC0K@d33AAj*7vBW&b}27dPlLNvNEa~R%&C=#wVu<-c|?z&T5`jbBSz* zY?GFN2Tx7pXRMOSAXU)r<8CrRxI%>*I>29qc1mlqM;uX5zrD4O3RxBy<6x$U{7;`g ztp^_Xt_GaF9zA-rcKn;z=Jh%iE>&OAkHacnz zDh>NRt#Q;4A~h|@=*RR9yJ~_GL?yV9N=tPR(1YeOX3DLhYGt5mwNS_!8Z2Z)$b^vc}s{lk66oueh)cL*%)r zQr%42dT5_+x$|~Sa8$C`>*1(Y1gb-hB$+wD0Sm6-OOomZ5^1%rQ zHY%F&Z3A5R>v@rG)OlSg!t%j%kVzZz43xLfq!z~&o2I9=dGAj7Kt~Jp@+*U$deB=?7hf+~kl?`*DRKz#02j(g(732osxBgR+<15QPQF%9tLGWYi%E^uvfHNz~SPca1aka5I0nMRj8 ze$OW#oHVLG`tnYELMda5sxftv%nW{Vk&(}zN|nsV5~ai3=+!T9$Yh4DQo)QzeJ6>$ zMU&gHl3l(EN1n%q#>OTzJvGA=Xwt9CQ}O`c=ey7Iz;<}vZmW?ul!vH{cET1^;F{jQ zQ~uv%Pg<_P@qyb=-$^Z*M+U9lznMwix!xHU8>Z@KNE4$L&`|v62pAyJRaTq(Za=DD_%ol?$M3vH)v2?3=8J!*vkyO_rPW0Z2?V+{OW2!M zV@!U$kXjAxJ9?@vaME(dSzX)SR_=b5^Y zKFIr-yD^?kXlk+0X~S0&z8Wlka>g{U3!dTEHdg*#9}&oTFR7Q18JNK6$p2(8xQh6M zsGI)<-U6jI(hAVjG}me3n-nW4Lxz#gGmNkY171hy-++2c=+{~^sc6`j7s`KYrEoe~6|}f4&MF*m2jHaya}_>HocMw}c2|>??&%Q`XFtVvx}} zI}Qp=09TqO+dQqYmascG*`xs8U?ww(yMDJs9?(tK%U*B#a#Pk3tiO)xry8dhC4V4T z!wNJ?DJSY2i@Ek6Jg6=E_S6cMT;3AecxnZZ;x3p->foX_n}i zctI8PdR{lKbR+jxWnBJQGBp*|v?$*m&TxtyjxCmPpf6mwsNS$t$a5JWc&`BTj`4?c z3sL|q*2d8vCe@S8;7jXQ@{W+*`s-Von3?b@b>l-F^4nnw{OEX0ru} zNoEGwQ9~wl;SP~^{g7VnqzvB7Ou>Q*cpkDt;`-)#hcu9fxJ>?!%u8>(`8C~mc`&u_ z19!XAQQmC2KUYQHE;D?RLd?}*u1D8<`d|u)tTONhr_%+CjX8GL9h#ioq`+=q==Zg} zFwcA%sELh05vgso9sw`k^S1B#^-ucvC$)z0breNN>SGe!p+$Jku%Yg7Jiaf3iS-ns zGG!rGNZ5?Z8=4~gmaSZ?S)uL6_Ufm9>Qnms2R^DpWv0syJ*tz>J*{dySCeDdi1TXD zHgO!_KM3f*7P#Zke!cts_iNYw{Ti~0UGA=`JM7DY01rx%OA0}|DcRsANj_??34E?^ zSgLIjmP$2vLt2^Rq=vux_!jh=C=d9vESjR{DT_G&jDZiNMk+41X_Xmv(B!HApq^1~ zw5b6?Z5`^)Jvac;O(3sJ=TsY2@qCR#^}EyUL0F*P1{h-!spsI|{oUKcip=O4A_~%B zBUd@z(O?1~-F4ugjvYPz1$~dpW%HV!Q@8w*Ev#vBNDsZfjM~!i=9SXCV^Bh&Dhe1r(_=vC@<*P-yV5!F*k+lqlJ)nvP4l9hVC+Sis{J9Xl&yEHyAg_C25lN3WBl1j>D{G$N6 z7<2jgR!a>{jm;>;mU<|i|F45t78ThBa)%k21=*k!)T+oU>Lmi7LT->M&X0y!&~UY@ zoH`AHpfMb3pudv#KC&SSgHj08vKUptcoO(6gBB_Hh#Ke}NZjz<3>3Zg?LvCJKySe@ z5i!kVCzC#gGWzYkEix{R_3;P?L6*Rg7IF)D~2v0%+p~ zhlDq)L2t`t$jcPXwV!FYM1P_VF2x?sw7q2Pt z-JMZl?uDp4k7_icnmDtFQ*+1Q+u+GC!9Z1GvYPi&!a*giVc%*wRehyufRpd5!>+-t zM{m=l2c7C_ZTYHJ7w0t0aupmW3S3Hzlmpx)GoAu(lJ~ix&u$6=bb3011W&IQL|ybd zdHQlld|L;4HhLaGTA$aVR9w76V^&E`Xm+^{Q^8tqq#qfK2g(a-7tyenB>_azkvmJ>FPC2VUJtvjP{E)t;9lWAtTK+5ATaHXw&|^*y%g8jh%pjlCG{T zX|C*pFJRe&0xOROLJusqD#p4n36nGe@_bSWE{6&(9IP_o9%@@3XcLnZ@kxPsU=HLB z5d4cit5m9lKeyEt_S)dxD5secc6|^9Jr$7X*Gns9)X7W3KVgR<9jiE}MQep?j z@OzP6X`HRPIOPR+T!hRr>uArOTVC`0Ut9rr?R#zTtEoHC7=spJxGW(SVWiyZNx+!k zOoLjSC%5s^n zD=De?f)>{Z3d#~Qq(T5KlxfVQoOAkVw(Na-Y5%dK%8A?)%Ia;+j6NzE15y(KK39&6 z?Y|}g%v5Jmjb;bIMpe{F2)#iaH#oi;i3rwdDKjUylq*yT4QjC~LZ5@>HAQwGS_A+S zGgHuLC>A|iDbZ1~3T#Pc0Xq<210ZjbMv&xJLe^B^hJVj{;hPGTDgpr1UKJDAqkQF= z@7@WYqX|L*vQO(!1*rxj1#06)`lgb76S7)H>+dTo zD_U4s(AKTnw4c>PiE-|BdopAj&^=Ef8=hUhBcl9$z4=?3c#nxL-zdCMqR$~J#W;39!rwvOd{3#MWHi(b@my+O|V`x9-v=Y=~jlS6AlL@2#n1UiGn| zj*l+tc`d(qNsfv%`tw%wgQENEjsyM!`#p4hzLoFlF( zA_c1$k4oa3STH0>3EA~SQom6t-!iVd-}i2P`Xe9FEnBu~Y^kfkxr;h`>M32gblx#3 z#Y{uwy9&U+8t!WgJQuu!Hx3fNJ?MCWbCX8S$VgnkkTX zkY5nQEu&pRpWPNz*aI&M=nGAeKYZ{_W(w=|3LK|TkxGUiO|S>od>c~7!MFT}K`Jd# zLzwfVtTG`mAb``sXa^u0pGKo^QsCW`gop$R!2&pvgf!g8IDKVNsKnnS8YXC~0Gd!o z)D6mzbrgLVt)QV~91=2$RBDzxh?bXE5R&t8OM6Ur9Xg_!?OU|kNE+buN(3v90%PtC0hJ4mO3{L%jK0?)Ng3~n757Zu)C}A8VKCGRP9UMMx8HH6 z-uK}TYI1fn17@m->Rp-qOlz1)KA2@NFi4R;vP!J~O%t8AI_)OqbO4?bWfXiB$e={% zkRn4Vf|(NSVhHlV8*6vuu)4g$RX91t&O@LcbxLrVpqX}o0Y)q`+M_K9fe&x< zZ3Z>aRd-I-%oIaZldaD_a=dWiLj41sW_(nVj|HsgoqG8sA7hJk$BrEuCv|!&WbUb% z^YBik{LQ5ct!1=O<_)y49x=2of+4zG8#}%+wRm##z19I*xh|F%p<*ylL0#8xM&7@u z0)gWuq0!rPduL7`#Cqa%nhZS+BEeWz=p|Vc)6B@|`?_eUJdbq{u%RyoCeM?+#+|T7 zP=vwuoz2yvuQCtXxp%jAZJyPH2sg+Puu2 zS#*P#0zM$9pP5#%t!tS1G||#sA9$ZW^0AL;+sw4e>kBH*U)KE7r&L|Nt_Hiw8M13& zYrC{tYi8QaPQc+?PiXtr9qO})yS}ifm1?LpaIWGU1{mpt(+kY&9EaXyYrc)+tAVom zKgv-h3#ELg*jL*_bi)I+BASXAG=%U1VI?>!aHd?r z8jAFpW$ii@o~QV6x#AEQWQqmpFv`HmZoSHdo~mmrYF_Uv^hzy5 zMEkD$U-SGQrWE|~y`akDmj(=y@?3_1N(QH5b>$5`04QS85W%}+n+dsY-L_jF{?Lci z+&rV}Yec^x$U-CrC^cZ9kcFUEo-(%;Dycqma*0C7C^4A=|2!4_nuH*YAR!b<283ay znXIX}+?AGBRjjVc9q)&J{KNWl|M6ec)b?$vFn&E8ZfPbK461^u@Mtv(bV-bpV#vYI zr~m*E07*naR5GDkKYM$U;Z}`CML!eJ53cqDwI^htjXv{B9LZfogktT+9iPyGN`4u=kosO2?}JS z;yu7i)3+`y=)&d8T3B9^mmBXU{!vHo&})*Nr#*Z2$b$g?fvdQo`347vo9R4)qLYxjyk5_3K;cY%fQQV zUjCEfIraFeWmd7xO%pnN^pLhsOscUq)Y7#@%`L2G&1pmzb9Mn38nFLx+?X{*{6PSc zqJ|-=M-a5yF!>LoX{@35e(3%BmH+rZ*8k(L{-^qne(H1DiqT(q@+oyMU6B8fUBb?m z_?Bgyz+9AO4M+Nt}&WjMb2_D18ttpg}D0z(|_|CCR~(L~!De*zu`sh@CHJGgJXR6|}mJQcFI+g3%*EMVWUrq>p}V zY7Hm9OhB-RgIWL<@on?Kb&j~ML(etny2vj4sihS?ho3t``uyCo9L~-y>Nys@XO@=r z0>_`vU%#&N=P&8T(W0cX6FGWieC?cnJ12m!zr76RGoKucGbwT$r|U_U~mrRY1LNk2SS3boEei|}Ns zqTvUS1v|sboTm>62CUW4;X{XX|9$uA(?9k>{iV0`}9ql9tc+Y$8*WTUxw89R+mBETuLZLyZR5720%-w!f!LaQnU)j5(PZ=}-!(19R z0(Lj3k3O&B8ytFg^4j(@#+Wtvj+grtP-DN7{0F#KmwQ?xc&idT!57VC{0np#fx!4V zi1GK`2-lN52IW+b&=dS4;ETSL=W@R}B<)dQB?c{1=Q=npvLk+pNaQM>a1OoLfXn!m z3;2YKe7j7&D}28iqOLS@UE4ISt25)8pPta-^tcu$$233L(KU8k<~McJ+di#=AaqfM z&Rl#!4?q5>zV*;|zvubCxB~D!@6d=}^7EL4QD^{#|K~dMOKL!*`8jeZRpe0XLSXeU zfuQzI&gf@8_6dFR6Q9tgom;gGQOiWhCA|d((6>eM3ei%xKymuH)w=?9gv~0$00D+T zLP|J%BTkCSEs_rmSf#|k_7)d4*=p)N_uQ?||Jh&CCqMIP&f&Hag;i|IR4Ri>&`gWE zU4-TUUdT{f?>{lX?xcOoh}{@4ep#y+^QzfaY&2zohHsEMdVynjr`AG-SU;Kx_izxL81o~y4g(K@+oZ!3Ob*p~zBOlbZX0Gc`Kcm%)msGKk z>|*G<>>Mmxsa_+~?EXDEeCO@j#4hd{dbzN+CU@WoTU_EWyNuTYeq#*Ii z-_>)2o?akuy$s&PF&y`17OUH~s@lF?+PzDGW2CrmkFtGxl^@ur{NMp)hxREQ+^y`$ zK6Q>A*34~3HG9W#&E9#drtdta*}IQx*L}BZ-@PZa^XPs}%uFf@3YU7{^Zeh(F!KBv$B@}JZDKKNsr**dEp0IXGVlrbub#8N^kA$pnh}-7{XL2Jk0`(U&*vS>uKVk6vvZYd{VL z2$|)rwn{TyWg_$lvYZA(yEp2tH+e@TW`^jDNyuP9S!7H}c&|M9_X5nhL|?GV+^Lp_ zWQrIXGnY+8xwz6gNQP4~*G-IKiJ@JZpV#u@k}hAmtQF3$E83TqRH_=u5zf0!hOX!Xa)Z^}$;=c(kn>bAr~QaRa^9%qyz7l!bw9L=!IXhl$RUB(!0G*q zZzGJ*VU>K&BxDhhNkA&TLpFe>*5s7-9o(<2o2O-KU9DWXuH|K%R*I@{{t;UcsHgAe zGAQ}e0{tQ(`4oS%K_HHZCNE768B?$g&mMpWoOziCt+4=1olI3zy5qff>Z9*{k2Vh~ zU3u~uEwIq&mIDoub(KY}veI8_nx+r$)h)N)s`2)OR@eIKa;9BiaQh+CLJT_3c$}>I zI&Fnc)BXkEB0J3&aIhC}w3i7y<`|>Jn5dsL2Mt%LGmpPoj_lNCCja{cy^xfkQ{e2Q z^Vd24oM$V&kHap}2ZzLX`|z2l>tIq9r6F^t3-8PLyLsrl%8t(!w(GC-3td4j*JDEq z1b{2!lj?4s)qsE|?cAvhh}*YITV^$!o>q_D;x%@c2UAljW@hR#&-W)MHJF-I+BBoh zhxTjttw(g|&f9eKt~+(&-g|W0eec%0-t%r``flBQ|GV|x`|s0_z2{zi0Dt>|yYJS2 z@z?%~GYtLrykX&cey=|ktZe-c!gO#BX|=CK0)Xo3ygD$SGjMogl~&54m?M<}(Nwdn zw0G02K6uZ&^@~6IS$*c?AJ^VJyR@>lf^)g1EAv;iu)3(_-YQ!)tTYjum+ zWCW=Im*x)_O*EGlsCg$&z(GN1@Fmw_jbQO&WImWdwMIFD&D=_eT2jIG70s$_gOXsW22%w*6t zBr3IF67S+NvpTp4q)9JcX4HA{^-rCv`-c`Y@WMclDTFAqppfN?t(NBDVTt1m7?qEU z)1-IMWo9ya5fXezqj#5a2=D^v-rSvanz^=MlY&F{tz{P|8Pj{8N&dG3z`H z^%yT+U(kRfwM!Q+>nigtfr781qI{(ts7+0QMr6M?==EJ@bV5~T6L@)oykw4*%o*>~ zI|(t?-z1YUrWvciAwhPkO2v(D1}_+F{Rx*DH~_EaI^%r=WR-!(C@g(W!km}!%AJ6h z-SlICuOintGMV1CL;Lsa)s#Cv=FP&jtLj2u>2p7CDl(^F;AKc&d^IzfnRsy94Ujqa zdc#)$ml=)MTOw6BdKEs_zu!o_%IgH*eJQ|N0fs$ou%K!cQj6_W4+4Us+^PHV$gml- z){uIwpfK6eyYIR~AG-Z6jU&5D&poGAc1@#7%uKl+07(6@rgrV!uYJ4rDvJ%a=KEUf z6gKyY&+X-#C^SHL$bzGb7IHt~#{YtxL zal&I-n#^@AuXGI}=R%=*7JX~YsJ?$5(2+8P z+VQWUpYwyEu0qchi<-+CT4|5tyk`}*?ohUUw;DTkDc=rk+pT=-PJsND?aDc>4rA0s zH@X(o1N!W041nP}^h2!62jC8kmUe8}sw4Xj=-B>4I=Fj}4(!+s?9`#{z}D@$Wp=xc zgZsp`?YeW;1~2dE}zeTVzq2$=KnQMWM!O1_-gw4sk2&q=Cm@-OFB5n)a$Dx z`m#)_n3Q-#1q@D3Ah0#>=)OZo_2+-~bNbKz%3smv|NQ55|3^Nc9S8R)?=)4iJ<^?< z*UI%dEicSzk(K8Xu*Ny`pu4JQm3D30ruV(~KK(N1`~TU0`hV45{VTtsKll01%l`~z zo0WMV$KUlWIWzenrC_QMs(3dP+At~+MGdOtKR}WvQwt-KNId``u$0r=hL^>sPwD!@ zPiT8=Y6ijkADGEHZH>*&D72dz!e_!jS(Xt~CV^Hi#4OL$;9O<}Ijylv(TrJrEJ=_(P?CArV$QN$LZuteH+gk03W=*X*X;miJElT#y^Ov_)r2_v#AvV znR&**X?7k{m8kiqk?_yD3TfBQxGB3%>%;f*XGvUNYOl07*naR4z_3zB!P*q1((993Muuk>|?DJF?*A3S=F3(u>!69{S8oYs`rOJCpkl z9M=Dzy*~l7?6~s#!2gN3_q~_MKnb_*xv`%(apJ@|Cn7Sl9z1wh9n!)P zh#`Pxt39!sy*G}*)2IvPN4Et`*=^Oo)`!HHZw4vzWZZ}mzK2j{81$~8^bW<3P2i`5#4HPPfqE+ zhab`IefzL4Nh=#0sxZRShY#uSN54sjzU!mf{jDF-gCF~Dec=1PPapXB_v)j+{}1Tf zxC;KrAN)Z*@$dYAKKKW}U+@3@->b)e=;M0q2ftg7{l1Us(I5CO+I&>^fBTa<^es=Q zbN_y6yruE7RAhs=%1PL(r%vdlldtLY;uS696t2P4b>@7Pajw)^tLVwqvZGCQ_B|ZH z`rttshS?4fVu)>am(uvGa(hY%8(3RgRW6mPC`*Nm(;?${IHagRJQ5ntlYmeKMUKoP zq30PV3dYGoyGm*yLS-@)Rc0e=8n2|ue$o^t5R-#iQ=CRjGmBFe-}3%nJOOyixAVa6 z6N`b#iPF^cl-4g^)``!3S-qD}sWX%&SwPOX0H#<5kyJpc{B)oLLuVM^q3Jn2@#v#m z3H&krg`fHh`YZp{U(sLrEC0FvbDVb!+|HGfqPyNNep#S_o z{om`q`Y->A{`z10Yx?W|&0o_`|L^{?{`8;vllnME{SQ8HpUPIy5T({*0rpW|$xMdP z%?v(eiFX9h;YT=WD~!>UX*Xn2jrt*$icDt5kEEV?LC1dOH*^r@ADNm}!Fbj=N}4{n zN1fT(#tW1J$iZ$>jo>GokHlgR2<;emFPl|giyR>p)LWRH)2`Wh(Ex>&m1vWKDMN$D zD+tZt03!kcPU1#zBhVNj(M-K3#@E5=jWH@apU5*HF;=g;6MUg0=<&Y$AStJ3_1L5D*WQU)8Ar%V zmoI6tyQU2#)kUci2cV535LjflP9=zWz5crqAWf)D0AW!r!wF!ctbqJDP{8Pe5_44n zHGFn`M@41yVt=fq$%hW=`@a9<`q6LyZcSfZ(}kD5s=Dqex?bo09_6mO-O|kc2leoW zKcJbt^BQEV9dp0y-~k%!KmnAw zz@|j(jTDnqY{9iSjzz(#h+k2+Ym;B6m)IqaBD8$*iWaY2-Ey zo}KcMgU(1-K~KK60_u^;~Z`gecy zNA>Uikw2_IO8BFH=nv?Jf8P)2d%x#n`sg=*lO8&9zjn>d!pNlIpsT^4PdHGPDZq%b zY%4NppjU#4+IrmRr;wQl+QH>Ptkp+C{RjT=TZA}4Ur@o_&FU*Bb@Z1$tMbGJJvqBi z`#KZa=)>%paqW4SBcxWLE@Ox#rX3?$xq^tsWMtKJTtiJ~WdMtC8gBM9KRK(t zyBAbIV+Bv}E0hu78inKaYj++{CLs$4UQj(qT8P0dYceSKLgOYnGY4LK;( zZg=GRHKV_3Nqy#R6XT6qRl=B7?4F0iS~YRd$(_i=PTE22c*csB4zOcC%#r;BJN439 zN&(}IP3d!gzY+^Ag+l9{aaFUsWV;VYyAG(DUeKU3twHE$U?u$)>ZhO~pdOinu|WF?Q#bgPb7J9vblg{n3HC9u_j}lMtOst1GLzfIhyox}r7gMxPDm5V=b9 zmFu9YH{_b6hvU*0Cl#NZJM!dP-v85;yiX6`5_Ua)|5NK2(4i#^+q5R}P|k2v{?ey^ zUA?mxHHiUE{ecP#>Y#KA1_6Dl*;)(%_@WjWos`w$>Y%mW(*(yAlOf_ONiA02SgTYA zXkjFyWhE4W8~h3fBBcsuR?wLgLV`7fd;_i}BneBb%*4V+l?YGvnkF@OI7L>!NDcTO zl&H#}+=jjMQ$h2wXJ627{jWc#}rL4LUQE{|ksf1D=KsCm|EISXj%;%UWJuQf8q9FZUK6-T+?@Detwt zLmXhe$)%CG*@-bAuVyCyp)4;}^T`V4wQ`f%&Xa3R@I=<+=H{jb7&(^*m(hfLZ#D6T zcMROvHpoD3W-ehU>q@05)haqyaEqecN{ zbddKLA}pw(!AI>NnVDeWC^Si|Uo*j*3L3H=`M?u8a_9j?HW6#*FRIVArT;*2-9f+L zRJ70mMWg|`gVz7XyoTf$8qL9VVWZC?e1VsJpo&2W#)TYIrl7GZs35E@D#K@mptZWE zx&8C{!SDMXecRz9s*au3l~-OM^SX<`sGNxu_A`X}A)WwrZ~hvjA^EpKY`{1t7Uy`sF* zh0cPd5Vh&X6y=^Krj%#q6n5{$=Q}VN zG8VEhxQteYlK|`FauN}w28tlFfFlJ~7%Z6Uki4Cw=!yZMGSUg`i=-z4SxUO zpMYm5p6K}&<$abMBhKyKt?r_xHJtIE1xP4WvJ{+d z^BB|;`tTYciyUf8O?qe9R-7ug|ALg}8UYE7z=A1}>pQbY}P}c_9_rO8@ zf$#WkeQ;uzdMD58?71^q#yQCBax3JuA5D>y*uD23(ER+g2AmQs9Y3M<(`UJYt~Cx$ z*ZVFDiEFDieU;c2zt-*alFBs<)zuYMXRfGs{H#`vzNV`$zQQT?D?0O~=XLsv&+8&5 z!2X*XG*chis-mNbJ^Qrl{s&d;TF`I|gU_9RuV}N8>~NCYQpG^y(A2I>9V~Z80hb`C z#Z1Ev`FhGBE5!^?l7@s1&ZCMk4FL}uK%Qz(2dNKNhcK==rKWvG7^6jX*SorK{<4mr zIIWW>&+5{pWi2hP>B^N=UAVZYQ>QQLNf&?dDV_XRpH{eZncaVuAN^+(O3nR6s3Z;!RKnB}2GLZJ0fi9g zILjDlr);T(aV=CR@-Hx05f}Aqgk^|Vg@cE(@+MI+utR4ehV0}wTheBcIEF!4M58Ql z7nPZWLhW1vZM9QdWz~^YEuqsCS(M$StNOyv|D2xrU;eqq@H*c|ABVvkP(Cl5KdF_; zmLB@pcc`;}KNDi?MiPorvQ}FI+7AZ^UIzCds#OQItWXPDr{`w4f9hy;aaqAnI;JPo zIk=!+D=33s@Ij2r^{IoQU^5COGwC$vD|liu=T9&aQYOeEya|wX`!5Jxr@JA@VqCZ; z9@aQciL!QUQnI#s-AXHqtMWPXx{Wb#dRdg7*46`gGAD2=y&ZTK=tlHY$>Uz;`5s;e zWZn$Nb#=Z06Ab&_Q61=NYi@!WbCOjet3`;)HS2@96MW8md@cj|k$SXhPN)V5MOT%I zPD_E)wBhDJ!<1!pV5bvVV^W_<%HH56U`C%?aFgO|S=;SBbU89JggeXVtCVR1MO*j# zsOl07*naR57&}cjBbc;8P&qu`zO1h$+HH&oAhwmxo$S>soklpPqR9h$eaitzTW$ z#m!omtkgO>!cbU~g*4fY3aqQhy7yku=kjnTXzR($OJe@Ek_4uJ9@q&+noMQJkfesT z1jcO`>9ahK3H~O7_gN82R7(f)+pbw3fd7C0%*` z8BQBs)Y{2YN|&z6dPA+SMy@jVd3;iPx!O5!-$R<5+pUCsYECtZAa_bEN`X-qA*x)N^>6x?VbZl*17g@8*I3qv(6MyxVj{iXpb^g6YIPkG=eQNUC zKCFw=EuE>lDkeJG%Mr}n#!$yU{gi(5zx)5_#4mIDz+#w6L9+~YvXZ8Ap)m%O*hvmy z+$M{xt6GicBa{D?GQX?I3?Lye6=B6>3#h@MZ@CkvwMvM4Oi-*@SoYW1w>&fCe!q*+a4d z^OWI5aG^s`S60^KyTRZ*h8SLNbj#rNx4|1B-`-OLp8mH1U@P;jy9&DDF|($>pwO(3 zgklrSH$yoQCgrA_Kn_f`DvWzYA{0@<*B*u~Q7cg2#dAQs^7c3MbxTR37LWq%Ys$Tb)$nt=CRdG*#$;j0_gYXGuCjP?ZL%tyYcvs=-7Dpu3&7rm{o93BdUH zm;(J7yb9ziIX>rKfl7fu)@f8yzv`);C^`OrR1fUit%}pW#mkG@P*9hh90JeD@#{6NvPFF5~ju{l&iYgZ4Zhh|2qWj zu@+8cp|G~Dbmg*aX;E5TmNwR8TrUSs4SJkhUmo_<$EGbjcti&td`J`1Gcxp`L8F{@ zgJNVLV82QSB`G9iGdJL}Is!3I&JL4hhoFE15U@Ydp^j{d0gpR)8DMEt&pf^jjNg!X z^EDX@Qt+dadYd=`E9*MDvZAx=>$-rgV?^qQ@{`{4twOlfie7L2(7w+;^0AL<{#)L! z%kwS0q75yN7nd)!$cYIj0hxX`wk3FLK=}DEA6#*dtRXtRV0OVlu39sn0227?7j=^5;{VQl$ zSzDz|(7uHQ?OxbLUC9OC(Xbxmi`v^EBhV-y>)t(fLswa5*6_H(k&3RwLe%2YvV0uI zSVa>vKDz$NBz+77@4fFP4*0}CPhjq9lp-sG6fXj3h9iK?x(s^0>t#e+BEKk0Da`9vgquJNaKSj6nIyny4RVIU1-mkXkF*BJYm&1$^@p1>UWoC-VrDRI(P>WU+ zX41q+-R4V275s30GRw}dqoLQ%8Z^bO3eLx=RuPkcz@q0oh^ zm$is7aXk>B&z%>GffI$cTCGxnJSovU1Go2PZwoXbbm%&eP#O|^vmHRjJ$rpb4mWma zWH=C9djqlPY#^*i^kP=8a7gNgTGRV?>3e?obKB=|+GdkB! zTH$zmYGzIc$9HM}psm%f9Mu>9#lO(!KJ|}u^p}1~i!Z$-TU^3x>#N1FUJJ$*be!?> zGn1BCa1ljQu}Y2kII1jJT?yQUkU8^uT{2xAvKUjs)UL1FCE)byu}` z;+UTQ)z9cR|LH%~=l{PytE->=obF#q`q0>J%@2b5D_vd0lRDAe(9{!;>pTARAJe{% zeh5#nK;VYjyL(n2c=Tb-&rU1TXHd;VBu)YbN=QR0MQC#G7G0%QY-wS3L7j z-JWJT<2v%d0}5lMszwn=K_*wOL+JLlq&%kqSaX+O@9p(2uBC`%GbQFC;%LRP&S`HQR23~36yT=3&uCq?#PWog+lc;0JT6$zscrI?f?iqR8TQk&;~otsxTdSHF;CJr}Z7*{ZW1R{f{ayUe)ILv&yL_i&>l4 zfDNvS$M+r7eGfj;IA5LqrWTJK)ym1&q|J3Lldi^FO3anevUN5Ic^+dEHhK7f;_M9i zx=_qfCI2$z{wIhx$D+m$?bWV_?o%;0r7k+Katc7Qg}>J%!*3dL4XEXl}~x~ofCJbg;9 zKK-=5^b4QXZ?WTl=70GY+I;#ex_@n;_Z5@cgZnu)Od7JVU*mr0BEof;@qFi>{G;0Y z;SXwM&;?&qJ4!8RIJ9p;?|bw?9p1l3h0%_3_evA-MoB~=aE7v!ph(m3z#V`^9Dsxp zohn;8xNo1@I0XaRh$jH9T!-*2F_A{PS-T#-Bar0=!HvI}$(Wa-)mD$ArK>n^6$T`- zs3?(x%o=j@O=vapwt?t-?>o$az+6`BmVJJm6q6A!e@vbQYf@w&ze$<(R&oO1CvScN zkVt!l7?4=j;p?@%#hn2}N#X-~IgL&)q-lu4_izNrr>T*-A{I^k0h+o93xX9lKQRjrn4CcVb~blE{~;Jl?> z(~Sc#v2TwCWmK2-0#QxB0!K_H|5-LnMdJ`aZbP@vf$s8?y(iX(0Owso2r8fkD#ls^ zo=?5oI{tUEk!lZa9omENsRHIr&CcnOkAAbh^~e68#-DslM?+7~<#k<|?KDR&_m9o# zp<-4K3|g8wv8c;G_gi}U@Bd@{-rxPCKKI}M6Fu{@|3WYS+EY6B?6X=tevE~5PQ8_@ zsy3FT{+a?tHV!ewgH2g)T>?^XO?7uo)#fVoRz4*_5QTZ#!_1K`Jk9HRHP`{;liz>!T9bdbm-#!0|miA8Ri9h~_ z^sztlr?i`+rOblcH9oDhwxQhZs>4hd2*W{~fRB9eeR}ZzLz-kKA28&GG*lcBCb*b_ z38zp2&vLvR)Aa0|dR1SW+|hJ$(5~58PEhv9ihxitRk9|ma~550pXBM@Jw{g!1j_ya zpao&FqEHQehPREMq)S&9)$R7>JD}kC)V%TC#&Bc)d+B$U1LzF7LG=>=r^M5Zylqdt zhfQWy!cZmTu?SHk2cX~VJQ62wAx60~kdP0@b2q`v8uT^&B5raRHPz2{>drWg&17uG6s%ju#lrfqNCn!o9Vlnzb+W+7KdfB%>fEuHv~vEU>h&JFRrF!1Vf-dtt(Q{0dqxxYAJo{wZe=z%l}!FS z_1%epP|NIyAxqt9K>N9`)|D}KNH~(Ax(@*>@ z{geOW|3yFhfBwJgpa0+fuKv~kSzDf z-_}2(?*I5-{|)`@U;jVpU;T~$tA6#r{d;=)zx&5J^S}PG(&xXb-6xmzkvOMspSw>V zo;#$yHlcRc)IxYJa98&`r(e>mwx-8^^au2(|Hfa@H~sM+k?on+5aYbbj=jRz&5Vs} zoSn;kERz5L5CBO;K~zvZ>}#xDD58w!rY7{wAAUmL1ni#YC@Bq!hkIng_)waZId^XoB= z`Ku@lhvWk(EU3k)U#mcGm;?%$ncnQVHtrpjMtP%l+v~~1KL*Q}m;jUbj_QiX(**Cz z*A4#&j9K$z^iCyB08_bOqc>DwGS}DEIeF~s(MKN9Bkc5h=PqjL#3}VRFy$#|z+Ckw zI-20f{>Y<8G|qg5)irHU@60WM{~0!S96e9>+j_%o)u!^?kta{En|LZgfFH>?Q)`Y-9{kw1>EnOqPw0t1`Ug~f z;D}zGY3VuZ>6i_4AxfKNU{_IUKTg3Db9?n2v-|ZOtvP*5uhf&P(qmVLI&^MbyI#Aj z$rsP4^~`a_FCA0({L2cTJF4=F$29(xQ`+^)MIAW3rX!bndSWT*+q#9m*QWIcClBZk z&fKT(A3vaPE_drdztnioH0)(94wH^;tm|`^F5sYSX!d(Pq96S){=9zhKmPaCx__Tm z^FX~Q4d8|$6gA3kfWfc0QZVww>LW`5K^rTh9=`9ezU3ny)`vduK21+g%75b5cPj;o zz?G##ha;zQvZHkzj{e4mI!x37t^p>er{p_^P!{s1!t)iY*8&8*g_7|4cN2WUI3yO2 zLQs2bTxDlWODxEXm#!!cDrW@?L<-RAJHngM1G}2+?XXF__qpdBK&*stGL@k*=00CO zZ!T}HNkTBW+%{>6AX7cCP76&=OsbF5*Y6Kx)XNfO<{z4RO4mJRl#yd*O+B;ML$crS zjWNnk9%#gC5Rnx^X!>wjBm!OJ%bI^)zwosKO}-=dI+4DbdM>wb*wf7m0&N|0Vr`FO z{3pl8#7Y!I2`Xp=J>DPdx5r7~?A)9ld;EP0DXWW4X|-A!lAi)iqG8rD^k&UWO{zm# zrYzEjI7KiEGNZQ}AiJTWr=73onP*UDGL`f%95FB`I3A|J!-(Sd-;_$!G3q3*@8vi2 zVFs2Pz|r4JaL&|n9a5{6qzNqPWM%5GIZNvU4OTXF?$jB*dg7#>J9<>d&!5ww!}sa2 z1BbMH{H)f`UXpQIkkP>#A#1Z0wfB*SwExf{#a^ZMT35r<7j*P@Kd1GpSCtcep@b@9 zn?)Pn;MMH#cX5KV@Yti;Xcy{rTAD?lPc1BHtn8?jqe`5SLV*d*Qo~P^f(dfvMt>Nk zVKh}-9Z=)mu+Zt)?umI6;C*jHO6il^#kUhO%G;?vs@p5{i2=s0hJkgffEhulRcijg zem(xtZ_zjZ=|83;|K5+N{JwA2#Ygw*=$@9Io2>Q1*igq>1D&z1R&1!qq|9Mt_H`!p zU}r{;b>{R~drptGX7xDn_jh*b1LM2&!SUS?-Lb_?x}=3=1(u6&zPxx*pI^PK3;Sku;75KyAOETUKtJ#w|F|Cb_HS073G5FBGA6fD zt$s{6D$+)owOOcf2z=5j7I8*F6ihm)e{GlV7o1IofAbc`}67ttgX;#TW@WAqdRpuEwEMpMwQFu(p_{nzdiC z9Llk_#yf4C{wQBBjz3Z6W4IZ-A;Hk*>rAXQ|AA=_19S$T2N*LL3~}r#DF)T#vCnBj z*&AYfM+-8>IJ2oL7|(bujSV4mwXd~HOFDnztd6~MTt{C%p;ulzrIYBJ3+(u-k`tE) z9@Yc13tB#YN^473Re{5suCYl=)6+Wmfe&hKZnq*1!~}YF^TK%@fAKl4Ul*l%$UBhh zWWks!n9^iNnxE46eFt>leUE7tyVdTDtAzlRDR9~=`AGzRgZzQj%(Aq-`>#zn0$I&? zh=rMAJh{+{$slTPyDdr_kKiIpE#hS``UVTy3&`5O+Bkky{JvSk~Vc_xT(wit`=Ej zD;$|`vS9iM_Ao^CVRFyY^yOifiC@PcU(!&g!$2q0(@VW&J+pR6&#qn8vzu4+^!g>e z)~;22=wUtar~k0N`#9ZB+%zWV|Tl zJBWmHFvNoui`E7;9;XTRH3F7(w;!Ve`qL;RA<-#s6ZNrr1acdvB;WuHs-$!0&S~KP zNe6S_W32JS9Rr>x&GQJh%Dmj=VjHL9dk@|@4g_S^=VUZj(BTtDz@#-&;L+rBhhjFf zCNIV)H~#+9$3_KjZ$^xIB=5=V^5fuiI#ErWo}IQ3qAJ!~qu3#fY?*CEJaMZ4QeTS6 zJ3$9b%uK57JAT%97!GCdL`BqQ9D3J9-CC^_SI*H*+*x%th*!OYuMLyG1k&# zXH0E2Chh?E0&5Jp_n}*Vd^C_Ghk*Vwz@31EbJFLkvez9zQ&2>YC4(OpB=qk+oT6JF z3^fgC1ydX45;q6xUtZRw)93Z-$kLciiM;_5c=qPa&Pz<`da{N`DKlZY!ja5yci{qeDfzI-y z{T>Wj!hVH?dF^HcH#@(e$<~D0zVpYD2Dk8uS)>0fbR~r27r<-sh`ZsZciy}O1b6v0 z=x`7;iTI81H~-R4-@6llBm18`5u$x^41-o;^vfy>yQ#tgi>IZ4QERiMogSaizC#D} z@V956tKby9<4OveNIgd-|+a z^vi=Q`t-(m{o?vLeP-iA1D{?yFNe>p60V-pFRY!@FK%4aX9idGR9@AWCzH-UydZtY z`}O`G|0DV?`uc&t^k?n(w1#**LnrSQqjo~A@?_*XK!&gz^NR`Q$}42e{4bk+PnCuLA(9SdFJ{PAPqV!We?c3UON z+E_#83^Y2r&DUUdqczGOaZDRUN6WF)N(`D@*sc4~ zvlS-*!{w{W%!mJlzV)K5HH`PJ2OrVF2OmN`4V5|hExVh#cHLS_sH!+QCFWTxOZ_VaR3a44Rh8;Jy4rPG0CEAxd+`FsFh69 zV^f;ny;p}Gd{iI&&X4JP{@9<;ANZ!zdk8zGRj`uk}l-gBbvVl-E%CSaxGlEnw-VO{K!|N^>^Pc%#I&|QGW@o1) z48QN1G7E8x`=%*Q8?G*1)iQTd1&et%f;^4qnWC{yO%a1CJX6>wp-t!J~z@*4LxpDM) zHOMjMhM;-PObCM#GU(+#=ME(fK#jvN91f^wl-Dw&E~z(qy^ov08`XcSe3q7zHUbik z+ldV^D#U+IPsufuB87H0w&K|BrTN7jQKWAJdDfh_FUWaP$!Z$KUEp%@3|19m?MO`_2 zL6=sRbz#`mCG_T1Je*bN-$+q?6{^cI4LW10xMKAyaH#-ZftL<=1Y-N=HRi{DA)UA6cjGbSv7)OfBe+oc$PMh_O&nlnN|DU({$G6h~5#r4nJ;yRb_KP!4VG|0=w7m!npzqge#g z*g;>ubV);w?WV@YG(SJDq75$2_5y9iqL4r4pL+oi-E{MMhZ%wmaC$2ECFqJVGJ__+ z0$5wy(3x{*)$Mj6w?=SGLMe%$LB$6)QJ=`$d3WG>JI&u~GvYuV1=qetMhtv~`BbqkFX+0f(Z5p9{Y$2`aD zH&vi41CjMqAe(*#;0|e~k_D3k(c5~B+Sne+h~bo%twOu!XEaV9HCJAVU1=Yv%czIB zFWx;pt9rAqjPdaePi;X(EEEYd4pZ4FH8wG>u~sSE5~*3+PN8o92`2NogBQL@Fy^CT zL$$(Dxv$3|I&FoMh*vlXa9Ca6R6s{%dcOV6Zm)&CMfFU11I@%`MI|;nm8#V>EnT>z z%jYj?d3{4`@Mkp!t*|@WfcFFVRx^g$3NCn~`XOk8∽52pLJbln=k@3}eOP<?RTq$}oWY-UPow=}@< zZHE#)&TK(TcDl`!p(&f>8z2wKZnzqlgXloT0Ql5+*>ey$W@ zPz0ZD^4tlfyAv(kkh#0-nJBKrm@`Y%6!-_97*l}yzgN=r%Ozrjp0rgWKgmpg~At*0NGGHS+*- zBl+&2`a&EsSBZ+X&l?LfGPnLZ)F{H5c@&OtMs^MPF42CmSF2`T2JN6s9#NV37sR*n zdR2oE#|cCLOi0@(Pn0F7u8l8{NgMRl$XxT=GK|Sj7^C;axGLn!kgs+u61(AHZH7c@%)O` zH&Sy_-LDfb!Pm>wS%zPOa$E`>g;dZs%FL8ajG&+3_{ zKd&>FFG$Rh$w@A;tCj?8zHekEe%yLGcCaHv81W%bHy79S$ zDw9xNPG`l4YbLp7p$E8Hs!-(qM?j5`Rg7lG1iQn1JXt6RwpiPXxV}TI>@Ix%PNQn_LL{Xq@L3I&yZfQ~HSU`0(gl9>{ zb%2lEe?Nc<&HDVr^;!MR%3$FxZlB}_2O|Y|0WJT6Ni*^AeTd`IKg~=NPNoIB(i04Kp z)oi0%F3LmmH1(hgdb1$aG0Ab&jKtS#kZ}|*MFcBTcPPgrwQ&H730Ksj&8T=X9tI-*P2OX1UIxS7K3we5otoQmo9en7B_8&T+$kBhGZVjITGVRB} zwZU^n0LwJRY3K}V8-?#UTD-EV9w(K3bXJcIPCt}d!U?#Hq3fbM>D5(aRvp#gF^+~{cx{-T{EboL+w_1hiI+;^Yuf9OGt zho}~H<`{Qi4v(F9S;sG()du>%VuFmmOX}3nos?w|pD7b*r@;DX>` zHbL%<;6riPYCv$aw`otojiF$^=Ly^tYda)c+mDy;1kZO`w(EL&ySc}A)*Z!1UukpSbfEFQ`8lXmV;wb91xG zwD;XuP3SZ@F30>%gPC1pkFfwHQ`u^1ldF?6XU@xi&C*ynKB1A&7^g7?){lJsFfhpr z8zB1Ld-pkjnA%n^37JGX#WV_?p%?tG_-ttzyjT zrlQH$TZRC4RMt2>UD;TdZ%(|+?4J>;=4;LO`j5+p`M9sozO7WIwgZ70J;1mF;JA@I z8ID!W^=EfuQ-18fF&L@`Un@ht92$kJEovRp4q)D62nsQ3NE>6`i%=-^Yx(J~pAvcP zQNQ%YTt%*64m|RZ7Pyj4T>IAOEm_byo5)p8bGkTnhK#kDvI0n!RKaYQ0Nvu_ecoc# zyI0Nh$)EY@pT-8i{Vw3x+i$$p@_G-%XML;SwwdG-daJP2$Gr)BBuEMiMF@*fL?Uht zs1fjpKymV~*|Blmzki<&>|Icm291!;%+6@n!Y*B2x~#SCy2d8QwXkom3KT`J-&Y7G zXIlt&VmkD7KW2=Kh387w4BAX8TP>7i)|qqXo1fgRIB6-%w)`iO#R6`;WE4^31VDke z1qHG9-h0CVmr;6x=}ea*^A>T=$ieW}8gflFHfe1`$RN zqbpt7B#dlR9o$W$+x?8X`r1~IoROKPn7GqnhU`H{!e|6v7o?kVT93NVGV)t4kJM|% zgkexqsrgXzc3xAA0f=%(ybrEcyRF^3cCk*|+N=kfJFrh5dHnsF;zX~9@f|el-!(v% z40_QCZafN%)70doI;`s;$^4&}L%6R8z1icaC^x{{M$#qc`0Zg(&2IPH#mhQ*<&u^# zU_C0-;0-{@+AHbDdAG^2`g(VRdCV+Hr^{&sA+O{0I!rAoHS@l`R`oU?x)x-<-n!{T zGBZvr1Xy#o9vDaEf1hQ2Q|lPxUNQ|8IH?NC7T{(EwDmYfhnlqO+|R(=pn#Zy~_v(Q;j&Y$fGN*YM|ZIQ+nuu!XU3c$yF4d+*)m0Cr0V7&B$px^jc=JJvyOfCEsW9BZW3i@grOVmZ-1yoJR5r0`p zt5;Wb^85u|V)M{#2l?M9sZ8>5$7CvVV9jOq(IuOMo@)N(X!1Uj94U2~Ylhgoklc_G zNHuY)5dHdJ@IaOvr+4dfiCyJbU#gM8P447-@TJ0$s$;2|d8|W#+HLwWbd5mHUzlOg z?G9*mgq+Ugb_*U)s>RNJd1*=ORiE~>VXy&ttr3bUMvnQd`J+wuJ^GkRPIESy@4exG(Ty8R4>N|`e`R&Rb<+mNu8nbb8D(D0 zT$;07)_kf&12-f!hCVTGk#e6)SK@&(H^ib+fwCq^wE5~zJl8NGJ57Itf&L**2}2?J z&B#_JUp!vl%oHL}}XCX?Q@QF`5*%=%ZTi z>!Sb4+{%j9SySs&>UsY~fLv3=QSj3@pTjb4^XqktTW{xaq&wQ#j<_(ykg7;;t{41=v->y!J=*j`)X<#(3led&Afv4{6gE_oq9WrB z6l8eDg>C=<5CBO;K~(0aMz3b(r7jJfI;m+K2>-)mZvOphz@0{;DcwqCsewfqr!o5< zeOUV*ewb67{o1{6Kbx{ywTn_YQJDrzm5k6G4Z5P=Z;+Sc|2sixxD!a2=ubEvBQl!A zW^GDv)Wz?Xps71*=TrZdKz;dxpzo|J(w@CD@A^2@_@a0d@b))?GsP6g(7?^e6jR!TK8TOj^f zf_^g^3tm7Yfkw)DM7O8!8tmxTjG3f=Gm`~e99-m3wDGp1E-kJ!e}v@Xl|>b?WbA>X zKC#%L*_9q6F;*@V>%HgBa=>gfFD?_;VK!2TPmn24r^cXnxfAZlv1EPRX@Pem_@2Jy6vqnw5Txw`X zAL9UE*+|Pvx9b=w*D-{_g0LObOG>OeDM(&dZ}8a7@-r=f9pg;FG=R{xL0{ceY5(Cv zy6^CP8h1JGSMt6R^+)7pW$GvDhaxsV_v6R^ZomTu#to}w@0ZSLBYG2$^M#hQBGJ4y zGRDLsbqp)!1}P7d)|S?Eg>}|tK8NUxO_<|9q@3901{mOd6yAp6SLl%er;!OfO?QON zYw#avI&s1Qr@_lSduQnJTL$oWdxz~d&3K)DGJFipDG0pCoG?|xT3p?$kMgXTuNugJ zIsHw2p-a?|scVl>I!bWBsJ_)~no6!-gd7N6iLL@?!A;PP!AlwU-J{wYT+DY7*xYE2d7hP;AjlSZ;&_Z`m6W1Nj5Ban=N-Vs|uZf|Dzv9 zAfhdB*}omoZ9bWPMhoeN?^gac9oylk)@^n|b!QSNM`%1ko)Dlcl<`(N5D(=0ol*lR zr8(~U4`cLaCdY&g)y&MaECyZS=qeP2rbx$DITBn~2V1zzz{}$DMd|?BYwwNeYXx^9 z;(eK!T-oLvFe8PE);Nne>%^IJdhOI{c0^L(ABBp9zWa1%>x0|q*u7sNwm zBRO%!<%*Y-e6E5_{ccbGLsvC-_#*A%HN=Qyl(FLow3R=;(*{|M(;7p-p^Ko)$ZJr< zz&Ku?PcL^tHOHmvAJ22RKKJ$1_PX=%_1sa+LKsD$!C25vztcDzR7!~z=5(55fzh$1 zMmRiQ%@5nv(DVXuVo zsyI<`2O!(1|KPL-A?I1GPN@mjdxv#x&P$LN;BvZTfj6jma`PR1z~ghNZOSQggBfKq z=4FEuhs#%&w8s5;4_*w-RMBVUA@k^M9Rj>5a0oKIs68LpMk(qB!03_UCYx?ZjozK{ zQO>P@9cP1Zs|nL51(SH4HwF*9+;xKgtWaGgRcMJ}3Q88GP!RgOj;JuHG5#Lv+LLAA zEZ}Q1K0Kb-ifS`n-)Omf50xa!&4L0liys8rP5ltG*^U}#BX*dRfQ9)5jay6pgMVhu zOKHfK2J|ZQjrDvzzx%&)q+w^`?Z)?W4M5vZ0C)V&NKi(QZU%4FnYVeXI&W3(!sy_P zG8xqoSVF{786nyR3h-XZg;qZaXj8SVtOEz2}Z{Ah~h}V{olS=;?%UAgUr1GRjw$m$kaOA~)KGOgRK9 z)b0QfBBjV25r0i6OXQET;PXg*rrAs6bMLa?iX2VH5TUxUecWl@Pq%76uH=DC7r z-WT>!(ZaPJK<_Q|`h<_7nHmjH0W>>{DS3NugO8>@)+Wx}W-$Bs67*G!A%!3Z5r$ws zokR^@sfnA^&}$7*K8`>-0?e=Dic)A7nwp+c+URSDlT_2sz|`<0D9Ug!5F-tPG-$!A ziMCokKVuUbvl0h@0`U7uj)RDMe?~9*26#;R8Qq}=PTUzxbyV|n0L{cpdN+rVcU!M9 z7(+-#ja(1|)g%Ug@4_ApP;N`hD{=>* z^!uv8uwj@PQ>%CQxd{y+Fka6OrHniO@tHYUyQ9}mpVqU_J*Vam9HF#Wf#@ItZ z?cl|>f!GydVqE5O4$J^k6&_*AX>$ zzRCObuCw=S3?)+|D+E)4yb;Kqnr58x$Lo1`8$jP6mG_xgmkvwp{xA18wD-P4dfy|D zX&RYd=Q^dAS*J=0&=sJ`Xy^_=Q5Kr!G;expN(DqklcJ9+I~|!8QZkR<1gPUhPKM(Q zq%+2H@zQ0TI(JrAxH9a~PsX{E1&kZDlVrpdSzq+$f8a@Q$Mwqu>oQX2c!Jb$)ys^y z1L&zq4he=Zic@wwFHL12YJi3w5;U*lWc7pB$L&C0lvHfXYh)>#&m%y4rdm63In9>I zaZsdHP@+Z+Hh*aP%0_dIMF<>#Att4nQsX0Z^!kG}9OlhW@W|5m@4k)k^3Ag@0vlHWb-(!!;f3?Ve|9@h9To*20kSmTB zrvepsjia$R7QHJTSM*MZnK2&3#0n{t8lc!}l<(C1ZWdn9GdKXRyz+{=y}kl-eU_Q<-G z)_Xk-SprT#u7*l3E8Fv_ul)lI6zC!N)Zk!>FH|ea%9HFNUCYS!!*0 zRhQ0R(AAX{t)fTz%u8m?MRvA3f9S7*MuJrWIlMzR4}^G^zh0zf-A z>`q)^Q2}1%XbN1pv>Cu)X;ppnXGLCye=e#S40V&H9z+-j5vf5+$H z4j}9HUL@EM7!gX&t`TJ7*@Y2E2vBZ7h?wN#Ky+0SYko6;9v9|!>EOQonwg%_EQ@t% zX<5Fj7{@s%*g+&#Mt13??z`zVGr3{N@E`%hB=y*#v~gmlXXmuMx~k`2d{JjkpK6qt zpJ>DwVU;mgi58NXeGTZn=WTJo%$jy1c~QexHw4|yt~NH-HNfEv7SzmvsGF%1Ofkgf zk5FY_G~ywjuh&pU_J*7en3>GXvt|4%na6~Y%PhcAJCkVdO>YKG!?*Z&yPZ`WuLFqb zk#!6$HOJq6ub;RF&)HZ-v}kA`B$ZiZC2OYDDv`0QiW37bdApFn2g~45P@k) zF{X6t#m?UbNPwd0*~12!)Z6=P_u;MSN)0~xaGGjDPb-)V2PQIC1yP6U^%qyP*4@+qV%z~plzV@E{GY*D(72UD zGq<@FRTZJZT|i(P@GzOl=a#Cpc$dF-=!gHdN9lauj(^>up8^E`%Ybm6`Os|vz1vMd z2*{Q=xJX7S0($grPa*xFNJ=Ic8+V!Q1G)=LQaB(hdaL zIX?km@N}Q!{xux2AxGFPSF-Rlv2zSwf%!8tg@EG2pa=yie`qUai0P2$RmiBBd5&ij zN)ZPjQ{Vd#55-OYd~{Qw&Ktcx&orVK<4LF2WTBC@1iCCghOfDb7;;kK4vF`hOmgt? zyHO=2Z(EcFyXBS)S##Pe){~>k4dpbVr#F8y#aaF@HIBC_A=FDNt6E~MF5Gum4;*-jvF~$L-6XA>h{(nu{|7zeYNB`sPwxah95l*)pvy%b%`UyfaVqr$TQc8YP z4_+8`W1t*|XLET?S1w)ADvpR7{;Gxkz_G|M-@spB%o;;TJLf|R3?rHf^hi6F&>;EG zC3LLwd7!C zaQ4Re-}Wed{M+$66Q<%*LP(Ln z>+qSG%tmls0g0xr&yY{}4PZ>+8-d>H0jCShHe9QI107}~y39u5wR#>g1fzjQRY(Y; z0nq`GLDUo_gp7~VE<>4x#zt1Kr7JSXvuB&+a2{V(KDWO=M4u=>6BW3wmCBt3!9cNa-G>>Uw zcE9@8(F?Df))$}sYV&JLQ?aEY)5CBO;K~yrua?rm)4>KThS=t(ynKh$J@TiFr7RX3JV#w}ib9qI5!a5Bl z48Q;UNuWh$*8Eqb5Gr}y$j-Ig!r8ZkLCeZm*NPo<<-A8p8^(sF5`C1Rkdf~OnXZ&A zD**@yuiX>^bU9dfy*$uIU|v}YO3FwC03N5!ia?GO>A;AYjIs1o`)>hY5dCgg`t=$slGm!QvorUuDW4GxkG7CHaGCBQ7m@!)asQDISnhVQhmk=`>0`!jtR=p zP9Ge5XP7!j1J(R%K~prz*HOencV0J+LXv_^F$6Ua{TmRjK>)8&gHk8t79r6hH8`VU z0_rHo>oS^hQnajy!{a_6wc3P&X5-d z*N=?bqyjmfL#gaSkDkAHN*B+cR4;Y4R&_Pm>ZsVL)q=?xoV_S@5&8l1CFa_=K+l#m z09T(f|M`KKtoiSKL-yX^#c+pr|31>-pF0V4T|R}3`mY5x6UlttQb?Oys&}jS-m0D( z&RZKdQh_id-D*GyI!6$QZ-X%a?~_UrORmZ$Um(~)(LiM>jTO>VDM%_224iW=$2x>kHNB%nUWlPAf5}T$}0)&8GQlrabmUzwkvj09%BNy(A~uOmX}|A~!N*^^@W zD5FSHjloDL+YqURQa3YdOGOL{w5?q}spoS=@P;qVu9b~2jFOQcX8MI9W?VIzwgRsM zVngYuEPx=GxW?!;e`30Z)*&)nV z<}tu@?IG2SA(8_+Rg8NWqB4BJcB>7q=cgt#!Lk10>62Q%a!FO0h^U(kHzdHd#ct;VAlb~Mj>gP`U<-Z#T7;en?dei%lyzd0_`N^*r&o_E| zXRNWPBt8hQA&l*!)Ocr1Ep}S-yXLiPVWIh}fEyg)3Z{8gy~`dmgEVUvV-O+f>;{7c zm>E-GQI3sIYGP(qS2&LQ+!vqGS6+OXd$|Ehu28_mo(N-_j2)F|+E@jZ>=**z?Gu=5 z6v?nzs3xiiw6BBS{4t6h2>uOvG2YoB!NVjzg6$0ik0{H3*m7fILmO*r@;h_0n{XkI zSYjBui1n&C=C3&+0CQ7d!4xQST%O-_pwUD3iXqArxSkr)Bdt7BxAwT>xLtFLjP2yL zu%Vbjuj{A_XsmH?Ha9ob@As65dmV$$jIqoZju4@Vv04aiDK45Xfb z>LyLt>vkpTl&rxbL}ldHuN)%67LmoX7cc4M^JjFix1s$H+^2^gd_-PvbA3ZyXyI%H z$0f*j_W`4^Yi>#pK5$TbaRS=V6rjzZvFUod`-X590H-ne;K*-PQh#|} z=b_=u`m&ZZbW$;%CLf_2DBbXw(U{H*KDhBOTZIbvLGBGT?`CqxK%qtQbXG8^hOV#! z3U)y4P8$axC{oYhmz-bM$V0w}k1%ApLzgTef9^ie2F3>*4TZ=&0933udYaE|bzeKL zrQ>HbK(;T3zBZ={??I6^1yDNR@s>&G9+UIa0pw!(=}QghUHmYwKXW(83wHx4JH$Ko zYYKORzI%bw7eQeNE>hMo8G(-|it#ZFA@_A-$&WY$JTNauePQ+TO4al#x&@ zW{4Y=vZo1sESbd`fEWS`T-cqdC*|lSdKH^*Vuhb4MX<8Px#aJzvP(U@(y9N95JaTX4i517Z8fkw$^mg)mWHw{`9fE?=AnV-8^LpvE6FPe7 zoT{m|9)ADh+C4kV?mubJ9jIRug2=B;LaWfs^rQ~$*{%J%<~YqOF*KD5%KfxtT9hPl|7$D5R&BdJO4e*8a+&vC|2U1 zSBN3V=h)-YcCAs}yZeQ}8#>7J@4CUM@TrC6GU)T4muSZbudLxgpq}%KKweKg9&Upe zi~$9D!XXl73#vm@<P<>KjOab5ZvIxvfd-v|e3Aj(Kv9>BWLNmEsI?z-*!SikeFZVXOlaDJ&XftC)fY%kP zE0&C*M{9AkXU#Y~?q#*t=cux_sEPUA8k^m%6BiftTTef$ue|WGmY3F8goR4}MHZhg zWM6~@dLu%-_u`9>A{Mn+++y*LV0*k?PE|SR--4$ZCm&;iM<6->GsdqP44Ym5DyJ>J za}P19nV$g6Kr_D`+;Mp`_~ZZ@Vv@^T!uV&jv4Pj;&Zo``hd2P9Uu+pvFB9`qIBx2? zgV2Zr4b04&yd^X^wrV*I+1mjbd~(R(X+RSBzC*@GZvH^lCcE{T#E=sUKecdNK1NzJ zj!Kh`fibz+6&uNqyacv6CKv=E*;!6uc=nVk?mtpDn*zc>4AW6rhkI1 zw;3k8S%m}!j1wmoUes(5xaRPq|Erf4b+x;x6_9L#*Ifl;Q8>O4_~>cnL6aeHfsb*A zAQjFJBIe~(%Xb+-PycyqoY|8Nc=s7y;gFCQ#0}k+BxI@8LJI^#6J&Un8;7=W*udnC zM|@&j<|z*|A5o7s%tgcjNGO9L>-~8r4ZR)#%&al^ zo);wr{$<}mK`%*}MH$C?mFE{NP0a092Pfe4)fJq8FYC8I{{@}Bcu|ErK8(A@*ce)& zfIkJw%kKjn%q@;;P>#6Zw6Jckam3>kUQ2uY-*_-#+yR*~JgYH?Yux#FIr{f_f`2)s zE#oI@0nRw>csvaRGknmbOdGFAmb|T)hsMa#B%-86q337vLyWRu%@u%nJkQLCz4`0) zwrhC2>5B{x3Ftu$xDuut1q;ePKE~4Lw88&Mrr+!LxEk{@HeFJ&X}{Hd2mzy4$jo`e zxM)vIJ0GuVTaAXpiXu}8rTYD$E?m5uG7YB3H2>fs9k~CnIw4B^ zO1;g#dhU3|pwMY)ZedRQ_U_XB%Xhr#srxA7tr@C$xsQz`mzEB=v2$&;X53WEBz7uBTF|Z(; zo!3^{oe6InLloU9kuUm>v~j%K<>K*jf;W73%2I;{Jd$J4>yOjvK#pOswo4Q7;czHF z*~rBGiX&ru4LTgJLuhC;8F?|XqD=C3&M!G}q(S@oOPPFV#{Im`|{*VqGKByA<2J0Kzg zupLZ34$+5r47m1gC7j>JJhF*Z9fYh075%Jgc~dK^Yg!rfwF#b@QiQo+_cMgLBB;Z;E`9rVe!C9L%xUfT z8J&IRc_s8t6|(w;%-UjF}`qI1edFoF8>fg>`!kV4QI~+HC z!VFde+n9Y3Z^ySoFLu~Y-|g^58a(Z=-LJhJ{ z&|4aPCzOcCw}I~1ZAGQJQk$c~+I8qYX>wdI zoIIg_{!2fvU-|9d)Y;3IG=L{wJA*G`yxak35if?d3_fI6`o1VZ>~y*@aK6#aH{5G> z{XD(d0GhGXD?X3_01yC4L_t*gxLK8fostn3;BvExQd_#Zh#U>&yhs>+F;4}CA~0v( z)^VGS`fK7cd3#w<3nMY$`k?Q|%#|Iy4Hz7wV!zkto0;2I^8Dzc1%eV!hJP-=-XCl0y%tLZQ&fv*-1NFF&i7 zaRAo(L-mrh!X1AqJE~f3P3~RLBac6(`ROT@@Ya9(AsIyCv`$P;=z+tBw14kzjbVHu zH2DdZF%KCT4w4{GCLWroi8U$t6DV}&zXC7as$@s2&4DhTzM!*f+hrQYs4lCVJn|9 za*a73Z~`+nKCX`I4|rFa$@wP6=`!p!vwx`XxEws){Om*_bW4o`P%F~T9iUY%#h38X z)+Y+>f8+r@@qzd0;Mjz!SI_E|-}#&_FJ4wJ47G_y>H<9^VF;ZSeGHg`x?wCmxUK<4 zP5_1P!0YSgM%Y2dCw9c|c+}{QXvGdehute?DdG}ZNuxicP_R) zE~m@mj(IyYnFJ$zSgyQ$G80QO17?)5+`TNz7k(hu!DOwr#&_+~#KLY}TwB*~KK-Q39*U2>ors<|BF?f#-iCz&1Z` zM(fV9=GXGzM}!aV{PcQV^|=>M7_va)!1#l61Vx&+;2I~8D`$+aH4mxI3p4qma^?8E z%*+%l$g3w5qB%bCcs@ab_uhxg<^TihKQS-rBz9ohda^g(ioaQq!Px-qwFTavWhlbw z3koV+4p78h`f|K2j(FY3yOZkas;ZTs(?{ZIP|BT?7^3{i&0tMpOlFaw5JOOJP+?~) z9X)YUFTHkLr!HL5;zn2N^xLKX)E?6SIUV3&9K8QN?c2Ru6Y#x+{~6v6IPHq9LJM5g z?U|X;cnk_Ss}baEGUedo5V))%4jRO`bZtgRkx<5YNqku)(3kz!{ubA?y0)QpayLp- zKaAS=QMZ8xloAmSlf=3LntX7PBC0WHpTe-Vp*h080FON1X}M(pchlxJL#oT25f9~K zZzBG2Y$9um4v>ChLz=|43By8(PeLV4QPUzA)nn+3bnFT3JfTSdNiuw zRlgOqUS!o1E#3Fv1Nznvenj(31LNC<%%mbiYlv%A|G-`MNxyUjiM_!zS|Z)nj)4Gvi>#TZJKhBZkOxp ze0B<=$8UztSR}&gurZM@_{;*#9NYW2nng?_4;$t|n?@sGnH92fLKCwKnwVSA1_)k! z?WBJFcfX*|e(q@vo$IRi730-p+JvDe*HcVmNl>%I&H9}&_(Md$a3A1Fy=ZNz&T^EPqn zWakX^J?;2}k{cOvjb?~G61zxuNCR_hQj_n^gdqtQR3e*x7w=)sYDpKbEb7IVkLjgjujxGTO?c3^s7)+V=H#t4ze`<=e=#+oM<0DuNA~Sg$7Ky? zYcS|3LD%g3oDS{Xr-jK$jX^}=ypb^eCUMWHObF*PedZ<{rK!T38}M&UEKt7<)EfgW zUR~0ajdiVK=AiCsF25GjA45Q=Cl9O-*Txc`6uNq=BCt zIi6dFrj1x$YK0Wf_LpsD=a@iZGK(>czpz+yB9Xxzcd*)^}E^#$bsf5Rd5tsL;Al(4j zd=-#=?afLiCFn)E)#_z+m)CV=WmQY{Kwak5{~%4GKByb4jr^1LH}hdR=~@Ubks8C8 z&T%R+%^2G7rl6h!!994P#-P{;p=H#e$49a4?=`wTgSqN-T3kmlN2Gn;jnOtJNZStZ zla&B70viowuDv+MPv&8bKB!gTqy-#g-?|?3&WUPduh?|LC`9jBBk|zworq zzjRdWx>Vyxi_@Hv(;O><5;PAv$*BORgRbw{3mX)IT>~?Z2bnnswEf)m-M@Df{BzeK ze@YYx=VPDdDwEm9%b!sDcnrLxNAy zOeV?kXWj64!3|mOwxsiSnIwG^j-TuilXZ6*iui#i7=LFy?R{%p@T3qrUF;#ndMkU5SOiXAPOD(ZO9AFe4 zeE<9Op#NQtqNO(DZPQg}qNCjh7PPQ;K@;t^O7dIuT}brA3^YzapzCcQ&&NZl*U+^m z%a5#UcAv49>ZJ{>TwKy3^Sg$n9~7oUo9qB%I4EN+Wx2zfui=1Ql(|0fypp5;U0h2n zaAnbHcQA@3PQz&(3zWF&@_OR;Ol6?2R2~-4mM`ZtVAN zq_G1}p}C%7$V}_k*MswH8@@hFcei!4AZ(+2cj7(u27O&!Sq7Gr5#T699D%eDWPx}i z6<+i1gDd{+UOc`N5;Sd>ZHH#ihm879@h9SP+L=jj*CdGt=%-dwVjlNNsw54ALT;hG zN@c>#sX#F{ty!FaJqM0xY}1PQxe%R>7Ci82sz$LBFTf)m05p zS{VfwP>kkH*d=2rSW2`dZD!(?k#xu|aKyJ?$0bSw$}UEnOyWMjnF609I#yi`RU?mq z-D#j+u%IBx@g|gIB%LT9uq$pvgEXbO)yL_Q{JRCveO+3j)D-EvMj7|$$6|p4Fr|!= z?{#||>DThd%veuZ6@>U0O4d$^A@MQ!Uk|)+@seIXc1%av^`AL^LF-&E4dB_3@v2p7 zVs={NGt-LgF@>THD@~jIfu`qY^??t6h-3Z(82OA*NfNZo?wZxYzC9dUk8uiJsSTxV z#&67t^Ug?TK!AtWfvI_XO$fxfq#_ix` z=ARvKW7=`Jzf^Ly)8WLT({3q-3yma%geMmnix&T0wX6v)PS zGL_7)_myy>Ht?=i{pSV73O(`YWBT}$-=aOt^@*>%pl5#TcXaXON%{Ysk=%*k6d;#T z6&*IyiEKHZhKRfP$xKd0wi1wiOpGOOfdnH)aN=(K+25NDE8HyePUr34|Ky2{s`shf z%1Ty=8C8JW#$rke8WL8deUW6{{PFxOqrItSvK$iWo7aAOxn?FayVjVwayh(~zG)Tl{_TaS@o9U+ilFYW8O7$g zpaD;eAh5eJyz-EHOAe(BeL zTfge8<49M~pw+>Xl_TJc5(C4y{z$=s72qui~20O#v#-^}za4E== z5vMhR6yRM1`0gLoxjLYLbGy|y{7WWH0?(^!qY>_V>ujkI_=CH4f z0}$B}yU~qwC6rSLj2lEP`s=N8MPsFtM9VjR&FE<-P}MnV=Et{{GV|fX8LeLmNgMrS zxIXZ|g4btY`-P_f01yC4L_t)p*`2q`md22;VYf@4iFV9fk`lOw%y|WPyHZ&RBYyev z6}@ueq>i1vpylpBJ-X{xQf*P8Gp=0+4@zSl^-;VDhD5rK@UX@)U$}3t9(wqQCOc!C zK1CUAhRSMTNXFX2A?T@vqvSj6oJ*`NEv;cz zhPLJ96|FF?zNx__`0J3VU(vn>PXV1J;}00Pkf=!6P__C#FRh@t-P4+#o6=aj)X0$5 zph$pJN0alnrkOV#Iqpdb+(}um5ikIrLYriAS#S{LnK^0xzXs-I7!pP@DVeX1#3@5k z3%W`W7j>ZkD3_92G%e)(v1|%Wxd`DB_31)&2+HGQ8s|Et-D+vVqQ?DHg)1oMtw};{ z%IU3DVdn>A48c*$%kPay4BsBxZoCo@j(kI>m=^ZN$cMQ{8A*y>%WlK*no3fB) zCZPF<`BL_OJ_`=S1sIC4;d@r|jOv*ZjdKG{_8t4Uf^^tMNEgruB~FLa;{eIzPz8#~ z_xcT;GA7gH^g0yDFd6_tUa^C^%F+LY)nyG>oC2{SY^5s^b}-@yQas)c3=|uw(d+o> zNo0;d+r7UYdhOc*P&BWZfp!F*;eC1N;Bhnas61YTC}SZPGLBL$R4Ii_M_R#Th`OfDSKpf#c67q04ezVw`)`mN9D zOV7NZQ>V_caEFRmO#gWR@86B3Ko)~3gwW9G2F?42963MLMEH`h2BfBdc&2Ug&?Ik@ z%?P)Ec0MMGsZNZ!rbN_KZoVFSbD&!Dj_T9N7S8Np(--aGDN)an@BG)-SJlMd}v7r*nXHykLl>o(eoeDoect%$4^R4NtJUBw|W@Db0=s9yh=fqN_IGI1S@ zI-~zeVr@IWecCfkvVqA#C_xl?9|xd|Gg4wCT*f9tso`cO5;g&O+W zq>XW}U{tRxuWF6E{&k?|#ujI=9Gg^Ya!M6rOJ=ed6yQ&0%zdupDirU14?Um*?DX3f zRR9@kRYc{Gq%=_Ap1xo%gv|RcJ{z(o$>|?KB3_dNFvi?K9?4@So58k`+c8+sU0c`X z)fKJNZw(%!zkt=VWb&$sZ)Tzl@LmiOAeR)S%tdTPjZKbeVb3m2O^z$TJK+>SjbvHb zK}P66uBSoLp-^X0lb-RN%5+AtoeTJ$MYQ^0^Vo`n3=q<8ky>Du4{-R zV2m$w%8?n5a(&{z`OE*=Cv}e=D7-g49XaxSC*m-Ds$jt;GZti4&Ez?=OBlLM2+@XL zJE?M3I*39K9lV@*_u!sDZl>RR-U%XCPL}-1{viih6+=W$YXv5^%Wm!hN0QymJ~fI4 z%mp=pi;4lIB%+zR6gDr(CVZpMwoG|D=fmy58{us7op>T!x}x9x@-zDR zU->ot?iatv5$|gpk8eUlOYP86fmSgTesm>*;&2mX)j%T6Vq$XWo$gXLB@{&6K!ma& zvPLBNU>tmm2M z1|pyoOZsckBuWY~s89LDr4@bUl~?tJ=U>tnUU)^%96h1uUOBC2UOK5Sy>Lt~9Y3SR z)vl5iN{XtPA46#dCNKh~TA5u&N||z#;Jg2DUsI1hoL)17Pg!on=~{l;LzcST%?4kH zPLY?~Iv7=rksF~1Uk9mXT#ny@X^2s-F$A%UDvCm#R-yjpriT4KtxOOl5epbiBP@`a zPHRlj3RT0T9>?pwVFjI%%Ti|?<3Bb|q9iZLuaa6hDlpIr9<0NU@%b5j;QjB@uIU+d zH~|qy%P>GO2AP@YRQ@$|mB&RY%b(HLV`Mdff8d;6PiMh)`V*@?%AoP;(y|ue118UZ_n%w3VX)Qq4SyS)1r|v}b-^3$t^YY`1AcA9bQjLW$7G(+B0j!I&F zax-qC*WOHQ2N=m6uTeyo(4FM*F0{4}?$%@9_9WLlA5=S=UVG*_z4V1I>k2yA<<0PN z@xeq9m`s)c5hG^-^Rb9DsRcR=Mt85^sMV+XC5-!Rh z7xsiuWqKp=rmy3F(+XclVM0K?xtT#<79R+mf@u212G{(PR3=ZbT1)R#pIKaz%C6rrMM8^j)I+PzAcfMyra{Mc$=AXwCBTMAh zgm35dKD?cQLk?s#G7McV_btX-bA-LY4z|C(q0G+TkeLYWZnSISS67zx)z@CrZ+`L1 z`uvM8>FC*ux{8leb!IduXSFzp@MjH2;5B{z*%x&7(xL{=Q+OMht4P0&%Y0Pi`5b4# z6Z-D=%0e;5=7;ercH)6@AJ44=NE{KomxIT*8XIx%+hxo9 z>!A!Ybhq0rmGs$R-u;Th`4bt3F-H(9!8@i*0h(JajG%&Q4EKPOfW%yPIun|l#GvB< z7>)$1P{E?YAgjc#v~UC6V*@A2YQ_gcInvJ8V6<9lH{LLSp(s;d0K-$C^{F83 z;RNGBcIWqDoTtZ(zBV~&*#sq!Z~{US-O-2XcAv=jJCIdC-GDK!7fE4ZS`U2q{rc9A zeyi@AThPjDr}f&?&uZ!71=aL5gh>@$W#i*-Qo}!*as}k{jUXz+ovdJ41_=Bj?|K?D zNe9~s33j}&UY*W3GCJMlu5l{VM+f9=`kVjR|KXEO<~{l_75Atox|!SmCF%}f-RQiukAP+bpfhUSP=}h zxv{287ca7t87No_prly2q!}13Z|jAQY|`z(o8fmwY-TdErW)uK zh)1Wf$&ms|fC_Tuy_NNK1>UD4Ff-M4Zj9FeZ81O@X>39h3wtzk;Gl}>IbB}g)aRdh zUO)efzp7vQ`CrvDUwlEA&tFky=hupb#_+5?j50--;%juIkR>q~Pa|?9n&j5UlN2@u z%z;m_kq7u9H}lALCi%Dd(wY_-iqwGrJb}wSgH`7KetmgaYk=4lmh=^KP-N^YtLr-Y z>TCM#FMLU#`^pPCxws@v&Zx8JfF}1H*6hItHM9SSCKvA0_^v~$%1OO?_OiYJe~zC% zqX7on%zMvr>cx7i=%eAcEBq{Nu*3Fr$z8H|ymmt*abKc=dSzLvOT7ww+Ah>BK8}QW z5MvTl7;z)sgpLB5j4J2pPl208s;=V&iT?o>ZT@JxHDLPgnDMPHTo6-u>YOIE>$p=8L`GJVxEV|w(Pp3wdK_p{4IDGi1yX;WB` zc?XHQ9%}SBO%8!P4@bG9FIi2*dFgR4-^StfHLSI=xS}QaxM8M&?>bpiiOS9*$uNQj z9U@d?VEgQ*d)#+#4!aa5?OB-D0|)kN-|Vc$7PzOlNplCg3&VZ@%UH*1VZnc98d0e#0ee~Z5J;rDBN6^D##w6)Du^~)?jwWw*F8h!@80J0K^ z#Vo#pM@gBnItN;?%s=|7C|eqzm?S&MfB2P?xSt?# zPv3A)d-Kj}Jyj0UQzeVBK#=5~A`FGC4ewmYyc(=ihF2bXdK<{vnh0%)sqVppINAtC zojDbw*z4a6J(*;d|C1#RS-4lPT+-^wii9B5?qd4or3xuK7;SeIje8~WG3_$&I|&wp8G&tKL?x6&q1BVREV3J8(Ql<$lp z+A$()4u*1&=#z3aAThuO0`m|w8f!L>F?Jt+pqM7qd|R*65uhO`p%AkoxHh@(?`>>A zr&KadgO)0pE?iyJmpSJD;`1--)pM6nP;D*T|B$8`dwX`5)R}DfGDMaFyeh|MG`ahL zX7(P|N^hv=zj{oso;V3Fx;Sb`WPoKB>#a!XMGE0l*xApjG3r1Jo33N zFjNChCYn=QGfFT301yC4L_t&<==ZYQ1B25+hQlinlG<(`ymJx>r9Wg>Zj_frtEth3 zCSgeYHxLFGJ45cGMFswOy)4Fn-L5$L&+x6)>ZsirQ>M-kqrL;tT?P~MFE6fX zgK#qjZBi-u959v|5IpyKWfC+>epEi-*q{PhW2OAg|NcV{TE6GSgMA%KIgMD zliIg1ry1_+JB1<58DeWW{~?PhFx~Fs-SnZkVonXm(4CMvnATfa%b`+ddR&jAM-I0q z6>+kAy>+>x<^O*`9q678p#nY#?wKIMUsX6X{_`mQYlAiO0{5CaY94RFz^}zU;&}il z5PmsiT96}OOBM9B(Q7kv^XiOGocNnR{Z~J!d;Bomd)wpOkta_SgZ!y>B}If7?#h1_ zAS=P_5iLgG0Izf{Q`bLD`Sn?MFSXqnLfc3Jp=UYsf)Qh37{q2m2VaCS8eUybR~Huv z2NFuDX3Roq$;BLH7(xNMtXM=j9~vH*$;?QPUbENZqntbUHM<^nW?T;5!*=L#$ww!N z`krrQ9&Kp%wBF`pW=%`S;gHzP1Qc|v$VYh~7zzfDLV>Xh@;}|`zq?&b&0^^GYhw3a zy!MVxUs}>{e*Rhg!ms?Me(g6uua}RX;x2qsU6fD8FhrT$_y;DSz|fScnFW?%5|7Eu z6q&RrY2+ACrq`cLx#ytk$TZ*>@ zUOjnUzx&Kfdf~(ct#ev5&2E1N2op2vD{7#i8k!mf*?h8U@~Y5MISFre?^6$De*DxK zojiL^n}fbWEadb%p9L`)d1c3;p5L#7t0+ryTnZ(Uq6%qeP*zFVuyFt?$o4^bsjhp> zzBX@Ef&9D$WSGYZy(gmwGro+$>GD626+%>GzBA(~Z~(l%{|wELI)*X#c?vm46W)j{`50fS^^d&qs zoXu*Zr=I@+CRd%y7cS}2>9e|U@|4c84$tDGpMLeYPQCJ~UORSNCr_W(m8EqY^#O+> zSrNG$V@#cPo12b8MTj!ymyRWLq&%+Md)$coe*_d;h1#5Gw7B|lzE(IG{xbnX_&QFX z(?u!afLH5l%4wjQO5V2G3bUCi%Wy~XxoXrBsR4ZIbDui^q}&i^2BRHFyDU321*AuH zydJBLz!+o6)fG{>W9{`yi$CGvy#~bMUUw-?Kl;&67QOnZGQ%{4HzEERGT^n~Jp!Nx z)I2baYF@QT^6`2Eh`-6~=HTUT1ZJeKTGolXzxMGY*N*iPI^@E*_8HGSC;p@m6wt zVzB6E37!ow{!2^Cq6-;gTHEYntS{=z&wo|F_smyy>gtAUVwWcOaJ)Z%KyhqZ1C&{f zv6IsRE+;k8M^qxmwPaI!@8*${O)fjZv31|B~ZCk+)lIMGI=^phRL;8B2-*ei*|v738(R>*I9=zh3n< zyKlEX_~9qCXZLQE&|NYozMe;54JJ8pl*#0;AX0MKpzerfzD@@v+pn1^v9l{Ipv$r% zEe)f2_3EPh_`gqmH^>T6!$kW4a^}Qk3MB$pGBQ!iqG@(!O1|rV_<{R$X#ZYKkB`Zn zl|o4cG`R7PuoDdQBHBuzY;0!=QcZyyupxa`lvd!Z&E$E>k}{CwB|~Md8U$D#$hd#QoSayJ`m=WVVH=Br_(#_NW0&%M&fl?49SNVW`!!XH{_p=RbcV zgQ4)Qf;U5;W^O8E$>m90C8&iQlJ27)`kYu*Y$_7*X8z#wh{i0~Jc>5oj2w$8T`qTOECe}c1D#=@@x;i zOr)FeE`Pk6hCU*}DZm>JEWp5o8 zFyRTJ6GD~&mLS-YA_|M_Mo8YyP$0{j-yXp8oAu>_?9h}^Clet6xA#KI1J4WA(3^a& zNZ*Oq=!Lb(Zao7+aZ4ddMjz&r4i7UttV5LlFwzjiRk1s@i7AcE@8d+^pxRRl+NgqF zIdwt5^YpX&`Oo~SKK-k|rZ0cxIbFDPS$_Q67#WK!e#QgtAgM4ZGEk;a1#~$0TsKE` z>>!zHnHe>2?+vsGG%>pJFe5i^OqW+zIHBk(F{UMsA;0p{%lg8z&+DtNozg~%n%(~Z zW7@AcHiIHBRgst34H+uYsc_i~$YB)5f-vE9LZF;Kyj3Lk*%nX!QE}LY?g2^3(QQb<$KM=gF*SytM|K=f(;BB;^L4Uwz zY*U@qI6Ah&)kR+yFE4U}wx*TU4Nf$Yd>zThZ>9jf0lG`(-{&r{k{E$}1Cc1NgDIOv zazsCdQTCtsUa$Kal+uHbJ)*}Senh(`CKXu2#=JOBX)5t##<>lNxW8mKfz#fAXWRfr zI#>{kLtZDsO#e-WMV!60YQX7D!&y1~#uzemCi+g!apD7v&Je>pJvkx2?|%Tpzk6y@ z<3+$x$qLLh69?`DEm?E~y#6K#NXkm!sXfks66Dk#lAoccX+t{DUl6s~@mF!E6o$}M zbFz@-yZv$GsEv*m^0|Kbs?MH1t(RXtrmwv8RXy$S@=<;1wPSh?eRuTyS-pDYvQ929 z>g@W8&Tp*gB1h|&`y0A4*wmF_Pm6<|mNqtZ9$YWIdO~0P$_sk>*%$Q6iPKu-bfi0| z#m#~e1n1E}8dAkbbsLZKouSK5K>Rlm992H{+*sAiUwuI@zWAI@a0GS% z1-3b;SYlv6D3EJ}GASE^!QgUEGBXrxlnKmPxBr*~s@nqXcyglZln5E6lO|5e5zrNX(E0|Z=LH+g@zNjy;Bf7NK z)#SnfEgU|g*3^t*YfQeQuUQuvh+uFBDPYALKz=e@a7ejEslY#o^Z)KtMO($hj8ZwS z3(MjU3 z!IHOHWa5cIG23-sjwgI}LxXDrlc3QWo< z=XM#JIn0d96}N#pQNxvdO`gwXiB9o9mgITMxi3w1nm-qN|Lz5~IL7u9A)~J3v`Jec zCQ!-a+TsgDi*B!hr*8yrazU*9KZ%4*I=PyhS(Zj3;Y&ML$;{H zGHYUPQV($7|Ih(s!JS(=iRd1`tB+2j&$WjF38j65OV&iX-rhEJflae6CkFwRz#H&b@M6ufA|pF9R>&q(1lRYkL07X}xmwvd(rf z=E%flWa%pWTMg1iS!ffdY_nCU3-sEB`eUUAz>s)lQfQU3R;#%VS!UjsdjoAUM+0cA z;e}$50nL0u4^- zT?0~*OmZ;$cn4z+~2jGAtKg zr_Lv{0TLb(+_N|MOC9L8+YUan@|kb*Dz=-nyD}|nVACgRxapY zQ&(^M@r9B?QU%n2$E7qf#w?^jUtvpmxe{}rSTf0>s%!bxOSYf_MYP_dUev zzZ%}Waic5H48ss99#~|h(scrkRW+S zy0xbvD^u5H2fRZZ#UVT0E=qHKb3@O+cvMewBzorZl3FwKn%;Lni&=)nNi+SDv01yC4L_t*Z07N`VBb^l(evmss77WUeEyfW-fpZZx$OWeW^S~~h zy}Y6;I0A;^iXo^#OZxve;HMF-R$FGFS!q81Y91q<*Y>Go4(<1Vq636lP_{z@%(cyay)!SnVr_;^qe->g)VPw%72=h=||aSB?AMF zbcr!37`v}ca_D1J=EgotDrwNhxJ+Ph(ArwJp=J&)=z$~mYwy^UO8NvLlsIitRwgb% zj$m`s4``a#K-fT2qsjW3kMjr8u)SGZtWf?Z|_w5Su1B`MJ_bkY$d(N+zR*}P;oYz`FCCF@5` zpQQp0uCdX|3dlB#g>a}Aj_rh%vRYR?hazZb?^r~Jyc2cjMzQFYw`Rh1h z)Lk#z+Jv?>MfGUcwL)Es+C(SxW0YUf4T%rv&yD(!S#favmt(D7)#H?|r!M0i1gR>K zSLV^fR7B-M@(qgZCc%v7?=G$Hu+VnDk7F|+G4NVyUtXlUDxM;5dnRYJTM z$z0f3NKN8SeITVmB8Nqm|KxEUT*+nma*3`8Q9fk@Qjx@B9zj5n8j3WcAk8GF%`l<~ zzWQ$v#PK;zEF9F-z6TU%_G%HW^URB{=vTQK`1CJ+Mo)j?XQLd~LPE8Ai)GkVu zj9DlQKmM*6E2wq5PlQjravT-LV0C3#Cr~n9{L(Xe=DFu|mHX|n=~+$h*{fo5R{b0` zps!#R5>ctNq!I;KGR6{GifoFmh)}p0`f4}!p_F2f^B|{MAx0d4z;UWctqH}kY4uT7 ziyYxV? zbOchvm&E0-|8(^#^j*OjT3_#~{rE-x-=6^AL}sK`d^#OZQu?Gok7cr)WR z=nde^d~%>)!U)|Z6^jtm<@nznfV~gjuU&h0Yp!T(9KHyCi?)XRP+hPX$gED-VJ4b4 zK#F2gyjvOcw&NwAUa3PKaX0YYjSa1>tT3mQ`UM!GHL@F+2WqJf=mTUo(OYYxtvQ^N z-G}yRdTdM`aEwu@18pq}DhLh!`W~QG=rwXkDp1D_9X=kiBHGa>R2fc3Wc{>= zx53wzF#b|xqtZXOq{Uay>hw#m>G-h|I(6=X&fy$i0>K@`V@m+6!M*4_VBd zghUH6I`%PmETZj;6y(3rz!C*vmYFFcgE5vWil`W^$>XchlSe`G-4Qo^@}1oMJ2}BW z?p-Q_bEG~h@m!lS@M8HB-GzGRRl(tLeFJx8SSb>KN~4%cuHaU2dr z-{j4z*}WNyv_z_^epic^FY4^6<2rrvxRyBq=yGgy>EZ=#u-I!nM?W^IazKm=Wfn$- z(756qDL#`gT3@^wsqzVk2%~~2%SW7{B@-v~qystrdbEL^MZ_C^P;jfE<9aj5D4Ofa z!5vVba{8DMGjnp4q2EE*9*Ak;H1N7j{MJ7{YUfP_pGVG;=YSJ>++eu^bVr~86zFz3 zKu5$-`rky)@ZXI|#UihrAtPvp~xdlgnk;qUX&*H%1NtUKy=6HhMaC>^1$~7rvw~ zvFkr^`aE*b(ysjnG&R3l74lMJ@(p8a!Bj>y#y=o;(Z{C*Eov0H!P^YZMEx52l0!BG zpIo;9)E9MxV=zPkr%-5;oirO^RTzf^Z`@#ZyBkf~m6tbD(~g$(?=)n1><&mdd((~tj@e4nG>7CXza&ZJtMape$|;a`ta zlXW%+U7Qu6Tw@U#ohdU9tyZbB73H`RGH|UQ%hVxTYQl;&+~*Fy%~ABjA9zf=Q2OK9 z)Mjq28F!XHUOCC-u1W16nE~@QwV;V;Q|2M!%52D+O7KoRE*q+|HrCd)%n87N`5V$s zfXV+!fw6=b2-S>X@BY1d@R1{$ottLvr7?I@&>=PvqfUhJGHAsRB=C>}rI2B29){C| zMm-0o!P^(`*BxiBz15MajZ6ftzQ6clPS_UGEq%G4@xA&Mt7o+t8jfr^~0u`ABj!YOPu79K!vh7z% z`wt(`2Ooc2bDfS(ef1^1{LHgjWs_HPub<#UcAkPUT2u^?wFrP0(h$8Y*rZ^l;AO1g z5QCg(vOEw2lh2*k)7E3d{E4mTI~G9YJJxs8@lXGqPo8-C)l)z5($Nz-2V8#bls2zk zMNqTatgOH>O|(j`;0@skaIoOW$+F2O1(6{qeGM=x8iB5Rn5t1bCe+(|q33yM{vp3v zJj6Y8gCcegEnkJht`;wx(YfQVXyxKL)f;Q-v$)TlI;G2(uWFM;)kTu~&^N^B4DpPU z@Q{IE5n&7ngNZ6)z0iYHx*#GnLpTOdUZ=a*Hasds~TfhKBb@zrvn8?^ARX8h9$fx z*gZz#(LOzBf!I8S&SYzGZnNeM58Q) z7&SgUrS@2dcBDNY+WqGsLVH}1d5t-%MCJ;YKGs=kYMLTE9kW8?)EV?QnEy)tP+iW~ z20R1EwDB|r-a`W}9|`&s^oa39K%{f9rbUE~PLlc<$}0W;+57VtPm(Lo6Z<|h_umzF zW=7%gD-NB{^N}B3VV!)1RBUxgF1* zz000OM3#yzaLl8MXzF1Ia*N6_q)X_8tzJh3a^gSC7igC!DWhKAsIgxhuwU$}9|&nP zhlcOoh%6M0)ybmzDCMC~B1nj9Zh1Bar9b0HjqfKd|kESbBJ50)Hbcu@+a zp-PO|MU^4S_!8l-3QASGXb+=14HLBn^x?P(r*Ca>MN6Hwx)_?QtihwX21G6TI4=EA zs z4CmXR0KJ0}-x7H;RX3FKMjLMj82CICR8^-BF`mF#Rh}Wp&`SbJvd~OcBQle#Xw)^m ze_BsJa#s7YO!Ke*SQlRTuI6sNukFFQ@@A$1{J2r})NN$yo2o%_XcTdVoUh$3Ma%f$ zxKjcAgpl;9K(aP$EI6_ky8!BQGMU<%s z8A~0d#3W$ykc@@*R~UqTV+eTTK{e`?wMc#DX8^4d%tlQ~WrdwlX1w|52f=Go{Bz%C z-s`Y=f~LcqklA6+UgjXMFz`*#MqFlKMEwMQ)TU%uU({1|(J%9@vVX@1Ef7{yEQ2C6 zGX;7J5@`wj9tMf>`#^2+?)5>fLd$3^c&0Lt;2Jy+Eejm9UTs3f$R5@998zoMh~ng4 z_3}|&nOo3zUwKpi>i_fS`fvW5|F^#V(zkW%+AXP9Xtdr`Gq0h-=o!OrI0ZR$sR0oZ z)G@ZB{>Nx=1mRX`_fO4wgAo%b~gtk==gr*vTdK8;|2{an4j!AUmATQ#Rr{?GgOcca|< z@KoTPL@Dz~J}Jic8x)~Yf?WQ!#9(7C88hP8cUL#HiDR>&0nQ0+d(>u~d(5fh@|^|w z|LAx4@P7H9#XUe&VmwJL)+S;wlLPNVni-=TLk6W}ioTYVbXk*aq9Z-#CdM`9+Ghg=rMiE!P#bXnJCuj}URyYS?OZn4|H6{7C=4`333S?7$u9c$`= zYhjU5f&x#cgY#NYfkB1VqzOJuq!cJImhORBg}6kigRixJGGyCu0)+83dJWLis|FY; zqe;fx05FWs%s!N^!@{hju^#_W7W!VRD`;{T_r+@mVv-$L{uirozzt6eo%dfqz z_b*)4(yc{lYoJjWu~rHps~$>7{-7=uU|+&SLx?P20Pdwmz5?E>boidAe;;5rV~OOD zBzQpedR@8i7}yG?AM;dM={bz6hq z`r)hkfByG>uK)Ib_zS)A`dhknXIZ^6C`Hy9-t3i8?S9hQR#&&~F6-S(SM|MDUelld z#b4{2-~EARIc6wX;i)})HRV5fj=?R^1PMBV$|rUwdV-~$t$?ItUP2+aua#IcV@#ux z|IwYT%`L?kRAYBMz+o|}2QzVd3qW#*&y{qB!Rcdc3L+hn=_8jn{z&1^E?~H-!T@<{ zx7#YRrTkpK_o)+1S#h%oHQ3FIxgtQC?pwK{xf3XZ2g8fAL zV7Y%ALMT}hUnvI-r|(QFuEc`%+8sQ(f$DjVVMtnAUe?w3FX`IVD_X;gPOd2Ong&^> zzNbJB0UF>j7lAVR9k9wJeJA?dx$oy)h*5MtR;tpHnpS~O5+$H}mn&@tG0!2~`|t}& z%%BZ@`7%~YMd52Uc#SrB?>1`YqUR8omDIuqY zBF=h^-F&r!#Hp*5K_$}E55`)OXYT!?ua)gJ9XNJa$4?y7SgkH`9I(2&BEzu>F{ihH zlvxGdcj2A~c*7IYU^asclHfQp>vhzMz@AW_d8pznhQU%b0l0Xd0G zf9SMHCExl%8brPH;{WDr-}tnU0+c=-sC@08eQh@G+xx=IBaf)YE@^IYO>e$^Ua$Z7 z4b5^SkWf}l7gi68;Xw;WAj0sKu)vx5hd>CC0XwPZ0ahU-L!cZIX{r{>L<-2ma)?5P z5%%Bh_h0dwW!Haw_Nq2k*F-T3Y3hK(HjBK?Vz*%&cL4fXO??bXcMw#P%A)9845y?A zFH^7}gCWew3!rYsE<{vzBPGf!(7nsw$?o*y=?+l*iQkSx$^1N(Kva^Uz<5eNu%y7d ze=ng0ub-*gOv)`(yTwdV2Ja-Ggg=Ths0;KgQ)-N;zI(66_8(Sj-$8}(U0T^5=AU*NZ@sMV{O~pX2#58Z%Qtj;c~d2Ze{$xaCf$i>j;lc7_F3PO zwKjMqu-*yUIt?MUhH0u&9v`2cHe8zIDc~I>f3RTms#KPW%y|e=gYH1>&29Av|CT7L z8V26iw1mkGjcp+1-8f!GyBcBPE)jqM&Oz1f$9g1c?*Q3*xAz~Rhesc_Hn(MrV@4@5 zWg*DSFmk2bNl5)tt1D~lrct((m*N|93Nb3j0bon5Fenwvz>G~L{ia|VU?4Yp+uAI; zI`hOi?ccLUV@BUwZLM#vDI+Q%#!K`!1aA){N3x@ZVTp*F1*p z7CqjECd)C`I{es9b@huHq=VNPmnD}&>9l~xsRC8=;yLlLRmk%rddCJ_G>!?%*dKX#L$oQBhMn<)FYD&*P@uZIK-><6kP?Pdz_SM%_9T*+wy3zWAQrd;4|G-<(yi+fhJa8@qlSbUePGUd*)3 zYHurQI|X$~`^r>UWR#4B4x~w9ydij=SHDd%{sr0xluw{d7BczewOqLPT(zv;`QRxD zeMeEC{dmFOcWRV@>A3cP1|u0F=5s>7F`@Af{^ zP&~bZWYus3*DQ2bvYj|`JMv2=(y!UwR#O%maL@j z(qzdc%aCz#R8wfJ-`4oyy?XrFCv{+QO07r*~DpcecKQqP}f*I}T-^^4Yhb}R= zeg~i|{U=4n%76&-C+awaBsDn>N*ishEUjp1b5k4eR+s(;e9TcU2M$i_)QKZHv~NZe zjk+=%stDxtlQHM$^HPFW62@QZWC$i0C z!WCV4cUD)g%<204lIGX8v_RcuoRAfqjrFXiHZZ^eNa(AWaz)PMZ8%vCuLM1G4-9O` zn*?bA1El2i@ye7Z2hqlo%6qBPBe9efczNIpsWzpAdm!)QKCJxXmU%xu9b=tJ z=(J75C}(Y)?{akh{)7AV#V>tP&p!Q>YUqiJZ@jJ_e*2rca{fK!Xj8_TWE>47*0{&Y z^+@|s@-;7<20m{G=^Ow2pMC9D_D6pnMt9JASn;o}te<87GK^2mjuRf+b?T(j)TG`a zg!MPy{XR~>dG!&lX17$cTPUhfKkmRl1LAK*hOk8Pln-_w@O}?i7S0L>6-0zH!*h!O zYCx&ax!AQUdh7KcY4*xRt*?8h4j@Mrp1*@pN2?O zAFO(o2t<|nL?1rOUo4nPDHO~J!0QhPE%Gl{z*RV$`P5j?426K+Rf!TU(10m#DrZOE z;E14w12BqHFurS_MyF;piUTk*wMVfrroPos05$cUFW{Y!H&j|wZ&0X!X8wmm7~^Y82W&3SdfEimU2!rs$JokV2wwV|Q$FBi%!pZ?Xy}pM6Xx&z{s?9J#oZG%Gk^i&TU3|x^(CSeE8Vs>&@Ej_wMI<` z_U+MWjv)^3n^p_Qg)oeA74nUTsgz+P3!sk?af3g|q#`Djmgu3xSt#XWj`XE*EK8$P zYOu7f<+Z?RH4K_DtSXs{*K_N z{G!28F%SKk3OQWeDMo<`t%uf;l&^R?Kk!I4}YXP z*Kc4adP2ORaiJaz(r0(ti%A7~#p%Pc&i+^b>_7d~oc;Tp9fChTP4MCi|7^As%df9m zsbbf-rp}zy_|YS}v~X8{^X>2IjUT?Q&09-qvLkV}gHqPO_XQv5RvPR=rn000mGNkl+j^kC36O3-}gXD1AL%P->rcr6E! zr{rVY-oW9*O{x;c^Yx2aO*zoVkS}2Xdwppfr=?9;>-^=by0f^V9=ppF!13PtNAk`a zp7z;&7XajumpKeaOLD#$=6}l5=Lsa};Gj~-pupmPD&}S=f4_g z)EcdgXnSK*E9)B?1VbK+ik5|>8p^tk%=KBXHsOsN{*5>@#?E@Jy`lP^aXs~==QXoy zm&OWH|L%%J$h>UiO07&G2JOtn=-a9Zicynx@{UV_Ho{@4gL`uzW!(gSr0Vy{b7&cR z;0#e$JmkK%qHQMG_njgA@0*#?*%QZgaAuFj>a2s~$@eTI3H?*TNj>yUC-pSQ1(^Dn zP5+E4Q?1(cNMl%~Rwt>yu&SjS^Sbi>tlqnHSr>23>H5~X=HTDi5OgcAX(elF+Zq~_ zHJp<=&O@epPx3$Yn^7+WsE6)!-WOvT^S%$MWd3C}|6oamJX8SQ8TI@(vMeN_3I%%$ zyD4Ly6)jCwSwSBI2PIXG0OfSZiVbfwli3i4B_Ho;Hf(G5@zRHl!c^2?GPRhgq`x9Y z<)bZiutVz_XbTHo1Zn@wUVZtQ=ksWOT=3TI27I^!=cro{78cK_%9@&Ec8DbHW(VobMkn0Eg@O`klab(GI5ufC}_ zUVR&{>#ll)y_%&oiqaTG(Pr!pN|eMn?P9yD3}N(8S!0moKTTUlcxy668&11~IXAbr zb@SFOy?61VE?>Q>jW$93dR>M94d}Or7`XS`wp<XQ2It{`{7F1m zbd;R|Xh-=DC5E5?@U^ z_VBEkV=??@!$h7I7}c%KEp||aA`VlcULRT~>IB9X?@FecMGY|8+Zt;+f7E8sYwXR80_z*yWY8uOIs zQp}bW4Jhs)*h5w%xlD#6g~Akiq@~z|)YL#1G_VdMY!23OX6DwGwT}7jW>T!xwRd_( zNA@4kzKKb-knlX5VvvEO7+c^ekPZxBfHY8s6XU!5v3}6lAgJCh)LmH9^3A)t zadl4Dh}zs(UXlMz{y7ZKE!y49qBd(SZRHKMv%37(`MM#;07n(fECJ5n?g04T2&iGK zGRBIGZ%ovvC1e2N3E@`4sWOhqYT#IBEow=U9|d)LD#5QD^i@(o-zh6`G6ZuN zbB!@7hEUPEWX`Tz9O#WI-l^w43P#Z<3Jmp;{wQO!t{iy~>s){{EC<_}QZ17^aqXBi zh#*7VhB6)9e@MUo<*(=qPd%l8oq6TE-_y(A{jQdm7I9{wg*$c7Y(ScnUV8D5zV@jK z{WFv54%PEh_$d0t3uTDf2DdIkG=Au)_8h|r7)U>S>w>=XgCFbSo9}6L_LjD9E-I|H zHQEbmqR2+Nrcn-I{Jq7-qE4(n+vq8+5t3V8)7_%9xQcI%GT zIz9DpB)XxdZFYK_WzZIjy`@|Z?g(^MLtN^JOAW@&WYiaR;TBO)LBg>R%pXV{jC_f( zBpAP9J_Lz|^dZ1xF;f5}7|xX+4CsBCM2S9h?Ue1LANu$)U(CFh4;sPS_g|9Zz}rrl zfr%OTQSRRY!fX~)GB>Jyg16DvFBW`GNz8>|-0-`wONnulm!gl80`vSyutY5fYUyF% z9RZ1N4yvxi)1g#gjIhHKq*kqdqP8mVr@uMqV_r;!J`B87;ctv3znF)wSHzJn;VrRd zZu}QkR+TkJfe9H-Kxm9=esNV7uUu1?2tmpECh89G1Pa<^$YR29b-P_yGkMQ4^HHm; zKFAHQ~qm-E(30)Tmg%*&Bvs^fLh4Z#i?IjU007brNwH7 z?ld==b=C76yb38A)ls<^v-R$l#`a9;3!i^pj~qU#NtbKne{*X~RwiFn{X%!{t zR&vzS2T(G|DqCkcgk;K*&m0^AI-wjSj9pS2=f!{hx7Y3oq|^hy+Z-ESUcRlR)RndB z+Ouc3&K^IZlLrrL96gp}V3@27eP9^YlE_DZ4>AZ6F}#s+b9kf9>_#B-7||J9+0w?X z1&tfI56M5;JHn?^?AoVt z_b;vhd@6503Q(_nABNh!YFgMxPlcNVHrSCW9- z`2o!H!2Q(Ab^tzbWgFx@7WaBA;hCgiSt)@54<*J))Nt_kj|-LzzJTAVRP?0kD!^rc zqR%i6u%PDe+^v2^Eyn>UV+;AJt2Qzv%bU7_Jj~r$(4asmgHHk9I?v|tCsR=$rSE)U zW^#TY(ckS#Rkf-<4{q=?dEf2rHse7X3M(fC%9sV&u!YPFlNN~*lv-R`QoD!2jeG|% zAF8vHZ{#((vMn)Yu^zNlbhOgj(y4Q2_571h>csdS#r3upnEN`r?M!m(Ey`ULu z!1@i>vt;z7$oPyvJ}COLkoxVuHdZ$@$C>+uYuEJlwX1sV{fl}Zr|C9j*Vs^;pSz}u zYj?EOFzr8nL{C2YsE+U3uVxZE`cgUa52oma;kX%d%OGsV8r6}x7SP1NM`N?;nlc^%!81|0UG4c&<^WH`=I=Ca?KSa~5e(5?RKkJz1n0Mh|x#^(T-e$khi4aXs^gn|KGKpzB( zsyb%oS*j&jg=eVpLV&6dpP7fxBY<}H0iZy|dM1Tx|5I3Gt)u$P)1enBrx|8Z#(WLU~CqwFpqFN=hh`|RMV_H-0ccn1CjhQWEm%gyN6 zGnAW;=lP!kW*^kBs*cIb)WNX$ANVOjD9N0ZipYb=S7j!NdI1@W7Ug(tZf_}h8A{r5 zsj-`HH84H&(PxKRu=`)_Z>hF>R8N2I8J%F`Fx3m{%rEKg!h$xfP&rapojK=akYV%^ z2FVA{f+-6@k^Zv~l`wK!Ya6$YYu&+5WE=k@k`=k>;=i~8}Ei+c6a1-<^>yL$cg zH}vDzUeg;_FRGjx(UB(~(aA?n>%g9AwG=sbFGXxvh9U931N|Fx3rfl))x4YtWVyDk zHoM2gi5?wpysn$8OS*+aGf!mUZY;G_3tF#b>aeECYRZaSjb4p&{-8+M zITG5>*@)yA^U4S(DN6=-2bIvR%N)0ZX(N+5QGy8E1uQwh59({s@5=w*lR*!o5AWo( zZ$wi=rVLsIoPf|p#}WM~H{0q$qxSNuHi@oo-d)nh!m_q`@2zjC*zU@D0|jWASR*r2 zWQ}78kerV7%e~A@W*-LFUOkoSGsgl*I8Z*ndLcEHvmML?S;Z7k{h4~TT$?dzBlTpg z!{}&Bj}Qg;gXg}e-+AOojdhbQ|M;!hfAP=%uV4S=&hF!l7Cv6}UvBNMz3{?p9O&y- zn1q2GWHoIP>gtV+Y6{Oj966+m?JZr{T+{84GzY`a_B*=N-q6MEbzSak>KgC2@a*np zL930LJVe&Dj9pKzL9D4CYU-p6MVPCHAQf4Ya-0CX={C#WvAX)yD}e$>q@-;^pg{ zfb1uBn=vl%unt|Z?m+e3s^|TOnaOcY%zDKML)erlL^LAs`TvApr-_oE3jzDW{TzBp+>c-rhw%TpU=UpaM zzXCWYRH7J@&xvyX#&lF&)nHZbLyuvxKPd*6s*PvC&tKQqFnEkZm(?dTlOgLR^5RNZ ze4xY=r&EJ4Q1RS3J{Rb)q8IZ{N}F}(P3p|^PwUvDXEes{Fe3kk z5pANkqQL>^05Mizei3UD+3g#8qC+@;@y1QvB*cFMmXp(m0G z=p6c`X2z%V51#vie&_LLG+i70)Q0|j{tm$(pJwhY~#Yp7#U?E(hJ z2Tv%Sg8mB(x?uW%A~3$^Awy9_Maq#oY9u9<)xTs2Nkm?Xp0eEJA^%*`D!Yt}uf3tW zL?gNjcNI}OEtJwo*;k9YwGO)zLU?s6)Gyj9@}xcuJychsAW~5ZK4lzJ2_C#DkvdfG zd@SKp2ZsB5-DbvEzyrOQ@eb@1p@XYz2ti3D@>=qq6k~w#3t&*ai*G~V)c|Vy^V0^7 znt_{vuY(YH$GbFU2cVrm*;Rw8Y=8i=RK#H!Yz?%v z+*Yp$+6tKlF)Em81Vdb4Sc@oiG>}LiJ$*)pcJGm&RbRh-OSjwW>NSIEBMmi}Yoko6 zvFo0|@gIdod8broc}oj77j*6N4GjM!y}|L~`*U}6lVid=DEJk0$3|AuwndfbqJFAr z0YkAEg7QO$_1N=Y(DB2^G(o*u(N!%+H`F@nr8TA4SG0i|80Il>9pQ{PUszT9%1td_ zzOFkS?#Ge1y0)r0>fB;|?lQ)D98sri%KG3v7*T7`RO~T9*Ih~SKVnnxTB1)9+tf^@ zV#U(~)~csL1oFO$tfwM$p?#N#LszYSUn9)F+373i96##}l(h?Kv#0LLme%ep>lQ~s zv)ATy8RzN3tvkB7w4!S`Y)g4mZ||hip*^afJfN|&N44j%)7tm=Ssi`$ah-hbNu7B5 zaUFg1wDug@r$EG|kIq}eshP()xI?sdgQ#r}htAhHnz%~IN>)8F0|#DZmHy3HpJ2HR zBb*e((#f5M9dHM?pk-lcNDulc0i&l3TI4Wp1Og3>dL`q*n$SZP_-J)_vw?HcY!759 zXq&oyoX)J)(17(Cz=Qs$oEe_)Ng4bXRZ|Z?Sp)2ZHPz?iW;W2|?vZc2^!krJl|T5W z2d?ly{!^QctLtAMDazSKkyK;lvJwS1ND{(v?BrSP*>gbct-iK4IxLiFAXA|XVXLc# z;>*>Lm33I5p%TFyAXo(gTZE_-9Rkc`vSdmudPJe*!73pNFilQb&bW~@z)aMZ!7N4y zsI-A(r4r?dx;OFSUw!3O&2!xF{(JA~&DUPl8?XLIS1!DZVwu$%im1S=kKL}aL0?Ub zs1G8g_=fMliJ?w>naB60z2RfvsX0F*?X6r z>R?cUdjL!cd2m^AvXk+0ZNRU zQ1HP5{q_~0KN$`CO)8_wkbwdNwZ60=|8>i4-UrAppVaaQ0GR=^+URz4@bFf#;UWe!{QT-rUQ@e{|i|4EGd7rvlVU;4ZbW8{y2{wea$ z>9ODaf}Z*ZU)D2U{j#3_>hJ0cfAD+y%0K)M^dJ1wf2=?JpZ*j5;XnOH`h!3Cs-8J_ zP6s9@WfowYIt<{~^;7{GEf6CCN+%Lnr88z}a~RXhY~6iTUbQbir}Iz}B+l3~oHtlt%wY07Fu%5H0$IyN$* z8vK$NcSzHM`eEt z8dqfoI77 zDsMOHjz$0gb5Bu)4d$4?1 z^Y8cc{vOhUub<%9TCJ`|qp64iOS~6EnuHt$3%oh!Q7c5%h^{G^76@ryo4cvi%?-s` zO^LED;b8QrXdw9`>4U2pKOq&vBL4(S^I{A`IkHm%2?)%8zz%d{V^i)_81ibN>Og#= zO{!AQ2ObGLOPpi>4~hH_0`>bSVlM|mmZ{ZhYJ^>;J1J&CPd@XkW@h$cR5oed%pba6FPVNkj8m$u?uf7Rs%V%fnN?jE#k)E)|$Fk@96gX z*YrLP#+x^9=(Xj$dZ)9gi$n*nQGe0u+O#J6s)cc?<6H!FGSgO;v_YhBGcVN!{CCU? z8-Pv0QUR5I8_11cuO8H|M~MwZ9-?afqzp$!i)-rLTG0B{o4S4Bif){LU+#ch!3bZx zb5qybt6FRnYVRFUJhNY8&$4^}0tWj_kL%P|p3{@R|3y9jyI<7jzWBVp@cHNTl`ni= zUw-~MeevmMxC-k}Jp40DrpVujzroA)Ms>R6WZ9@wmcFtu1(?k4saiA4_?Nz5={LcP5LV ziq}j+KLZl9Fh%H{fKpLAP_TO`C>uhgoi`3LoQqJJG8~MsJy6{;wQ)-4SC^HIj;Jv@ z3VwN29u;Isf|(UzY|j_ig~a;wYeAc}QrqA;Na-_S{FwjM@&AkyLdk;V!ZJD4b$YrDD;^$alvHp)F8OuEg$Q&?CzA>1UtQZB$p-$H?0ymUq9-#M>$e)PJw?ks7-a@82o@o*(6&wvMmQ%c6) z9mxK&ZlBu@h8UR;}F;d4rtlI0WWB4MnBx%x9;WneE;_T{KZoXMiqbp1JK{fP{_Sb zU&||N?B)jwG5Qxk!@BetLXbNteIgHi&h|^r>III=OUv5Y+O9aSbiSl*W2B{kBj8TL zk)ua+{Md2z&;bj`q|d8MUY4mu;orV}S8u%Wmfm>tZC$*6T^AN^>naNW4!hkAklR3C zEOSh^(%w=pmCEXw@>*0x`3UtU-yl-|)Tl+K-)YcbZf9-C@gi{r`>oQ)>C z^-Q3{JWKfzNIM&7J0GYM2TEDVw}CQ_7VE4@jkJd2F#_Gj20``Bfd(8etk2%k?e{M0 z>YMNB68h#6=kc?5<~7&Z&~j61&x|P^ozd<`j_cTSkLvN?`JA5ly)WvSuY5tze))6y z!dJeiFMjz8`uyjf)w54Mt|!l()wxs0b^6F5o!+-k$M;O@;P|Abnw;tUjTis-|NK)Q`ezE&9jfPNw)(&XO#+H?4b4#Vu{@R%p|&So4yH5yyc<0U>e_DoHL1;HuwVpLz;J45TCJ>$sDe znPNaT9#}_;lwuGcwBBcDW7a(oiD2Z$K#O7^bKiIA<6cGb|PQCt>zf_7redn)EY8KU2`M;gOlGO|_ZV z!fO{a``uUd_TPL@?|lD9x^?Zk+R!kY99QG;UbP-MqFv9Q)4tz(R!9Ev@943A^85Po zKl-Y^{CmHvFR`ot(o@gqcOH2{zjNxGo73uZpmtp#(Nomb9_Dq!x?V z@WoK6{enmECpHtvmZM0uXEUKSBmT-x&c)pA@ljncd{EbQufr<_p6yp|L zNtaz+y?RCOyz`DOT)2RuDO9gDbc+at{{YR#EkgYLsD?FE^h%UXU+t8XPfh5^W9Ntt zoYjFx&u9(dzOpzkKdOLN8PO4}Fk~E6*S)uoT;3I*}{q7MmvmqvCCNn4ls`tM? zG~*S13&Vx{<K{KN@R3%Z4-y1(AdLDjFhG(I<83{Bdp03j?2V$PjwucHevHC zZ@;DQedk;H%fI8DQX$kXR^;^{|q`U%4I zL@1x&T>kMBr}gN;qdL21pH7cY>u5fz1L&@O9n-#!G~F(=%f|%9`h`YVn;MQzB$^p; zL=1)uE8dH#jm=oi&@BnAG8e2le>5NA$!akLUmz zChIrOan1f+Q#!V9pY}~nY0TG*`FFQE92snB08b`pBAG%6G7AzTxNeT{PxNv9I}QpT zxRVU-4S;{osZ#?g+tKOk_O*H4SzK0q_ih;uK*V7xiGr2PApv-35_r1 z9H{rxXUh1C%PB_k*}4DZProsH<;}NW9KiUU&ZZWY@2G=!7}z01X44pJX=2Y5JAeiY zdP5sGZfOJuVALYK;p!_1*{Te`vG_i7GxOvluv3~~pppd^jFSMd=mjCS>dgPbd3e63 zBS((N8My(|9)MvFOie38XhM6Sob#oz*0{#TCh;g+8f2O3Q{c2d(e&wK8aXhn zvX;SPNvT56N4bYmA_eRoFgyi+|2r{%2Q!o1>(0!O9|pSj?qj7&h;|A-NQZ5}mwvqT zUgFuR54s}nef<`OtNF{PLxOjQ;O5oGFIgLh;kx;n`L5dyt%DoYE{eT}Jatg++bHx7 z3g4Z9K1#5|yVvvi5KX5q?~`S&?jc81*?W&f5hEcrB_)YnRFXMAl~Sk8UYeb$^LvQ` zcMQF6nhfj1Ksm;;*=VSq*NGb-;B+|X4dmzYeeg^n%E4;6TBD=dyLT^gHUdAJdf-36 z=)33+FdV83L{J~cpa6%6k!fT#cGJ?<(z<#ZZH?4N2t!XP%X0<1{XUJ?w>GqiuCaR5 zq2ovN+>_7f+`eOKb)-h8R1*iHv%0CfL>4Yzp4IuQv$}BWmM$-?aIW6ff<_3 z(~tl9CB5`lf2r^P@cX(ne@&f{sJ+jg)xOW210K~LjwdD`J*M%~huCnO(%GjT(=*RJ zt2)OEUJ?yD?FZq_3Lj@FuNG&s22nsP$B`G66hVypxzybNs5q#n4XY|;qGdjre z!Z_>G?3bF1H656k(t)Wxnrw|Iho%FZkseNfN00{K=6t4T+&~&A$E9@0$XCElJ)^7w zBEkVDC5O*z%&Bgs;C%us9N}~`+=M!W9lZEoeeK1U z^clOtXAIv8-7BxZ`!6rN_WDaVuU*s9+Ok$R*VJ_{9B-{h$dB!d?q0vCTbHlu$~*6A zg&lbdCagQ-!qCwI%V!b`80azZmS9$N3r`9jxTz&GtI9)!dFdy?)XRkPE?s(GyLauv z_#f8sW5+bJXOG5ELeslhLjRxuOd~`BMs`nXV&Am( z9XqV%_^7%tWr$H`jLBFr1O-VZeHh?F{hU{#-v_}MBQoZLW!|+JI8-y2La=IlFZZF1 zOb7vml~fc3f=mH-px#bDW?svI55K*CWfbuBFmo6E_CNQ23-D*D zJ_^;t{SI9WX&0lLu>%Gi3azcI zse@4q82N#Jk3kkqK>{}^%cAmHCNpHf7z^1AbQnXdeqd~AWknG~O?S$h4R-Al8bgNi zJgSdl;SRz8nfLZFW-4i0ii`dV`f|h3M;JSu9)&^8#)w8n#@U%R#02HPqPVfTraq3! z-l<&}rziBxxyN*DVp>z}sQO}8?b$`$K7UHmoWUXqgd-I?7$~DC~{^2h@rKzV+Y2vYC01ZL%z8X7oSTkpj z>C_YF^z<`N>e(lr(38hb>J%Y;-{Buhb?xg0?dc?qb_+G3MaDW5Oun6!@*is3s2AF( zmD(nb--)FL=srO^0}y`6OaXcZQVYhbsf0C3^La^pWFC~6;F}Wjt4gDkj3fFo>V|tz z&oWK4S~@hdS5KZfqh}v|Oi!LXqceLCYIok!NEtPP{v1J%jyK2DYP1w%jq!}W>0cr* zx-FFvk*RS^oN-)|6@6tG{JerfHO9>=C{zc55`%x|_HEr>SX39hB2lJLt54&K? zFI6CDBcB2Fn38gEZw~a*fAPov=dbHCc!kd(&idD{zdn2Q_4i)*@%LZWJMX@$YqxG{ zd2Ll|c--&*=uLh9zyEhyB?JfW>Ndv!@4frBwwIPPiKeLIP5Mu8y4RV-s1imVV8B?% zAt`)Shq?RD0%aItlq{fF2HNKA{^re_8pUfph}U}f-~o+|z>Ku5=UK&o{zEEZtRXcs z6?l_oirBJ2`2cS@w-PK@FC}ugl*?|9o`joly9`c^ihwioZ)O%3HEuM2IC3rfaHWT%d zzbJxBfx%hD0qFG*%77t^K(MNSr;KgR5`E8N=NRu{wHVh4I^ z|6x6U_6a?H=(r{^LYs37ntT76uARTCOYdIMr3>V5+|@1r+XkO6 zC#+-TJn~mEPeBP|^N~aQ2D0HH=q9)}*`dbN(e~D&Zq8rQTNmHZ_uu-le)#rldi%nA znwz`HhO@7UU6ZPhk7_OTwU#AKoIa+bUwl@3+1*dFk(fESSEo-N*K?0PsxRWOJb(10 zo?xecI5ss?1WmHbZ}v*%M0PxUZRjR3XR%Wj2G3Wkuu^?itskUbOzLK&nW;zGjmGhX zsetYYaQY8HDyi?stX0Zd4D<9DN_tDIg@dZg*TGlZSfl_gVlqY7wcyo=nGWpQqcew( z>Lm2vH@-_P^kpnefzI=o;~>J5=+O{riqP7B5Zn1FW|5rW1U;PkK$a^Bn)<{ z#t=GR(0pTkU01GL)jJn2=f8V7&voPFH+85!se^=z zYPDPoi??+ZCm<)p*FyM`D-9SR!FUcOj35}<8FB~i(*>sVGT#Zwf}GJ;*VeSXy+ycj zM&o0n8XavZM<|@3JcTdR3j=M2zP3Z5K5pUwN}Eg>eZ)ah4kJgxhqbuic4XQwq%K~&U$@UY85)%0E)68S|hwDQ~#ypBUbhn^1m zF6kx6!}uF(n{uUXA<7V%>U&bvM=*K=Tow}T#TEGC<#|?9n@Hca8`rf-TJX&%3t3<~ zrG&%aZ7R^PL!K%tzD>iFHjYze=rC+H+EiK3wBBiJd3{YC&ej@` zeeSNVzkNX$i7LK#`LZq&=D$gVU0JATS`A7{6J_N=}@c>g)h^dDpAe4;g> z14X8(&AxVT7npe%YV zsG9?pluI{mqN8)|I(k_94j$C5=9tEDGRA-$8YH4gB}cx7!x-r| z-~_AJQz8YM7ysFR@#1GL^sjqYxR-t=ligz@FWtFz`=wX@>N|SrU;PjI=AZo=ExdX` zCr4(qzcHp!yu^uCi*Q?kw|-um^S4#U^A4^EiW25cGMFf!Xn1)z4NS6_sqc(H-6ZMm z;-W^!#x%lN)EH?dLs6OnLfva`%S9q<(FTF2l4^IG>g@D$I61q%&Pa_K0ezN z*ja0d^<+)Wg51#7QMiq~p)6!5>42h58ekwyCcotu^_?F)`!H}rYexMtDFKnZ{{bNX zA;9Dx;j2U$pJZkgct4JlN9a5?3Ig9U>elldrj!;JmbALDf!V?sP@bq%USZ zl^Bth_pl==-I>6e1$Kpby{3AzDNhIN6xWF=tZc5!__k}$ZjIpFd;9f`b=}}dahCJ_ z+x`c9wl*|hwsjZ7xm*ictwpWTW(~RCMy5-vDaRp*L<4HueT}<}6N=s_Hq^_zs*PqE z8G)wIaSP*g^ZX^f^}Sd0!*6nq|L!Fbsn-4jhjr%M<9ht*r*-!6$8_}M3GF+4NJXCM zjcb>5nXrHT$N?RB{%P$yby^3BPCR=2Q9W_|5q8W6wQqb}qcZtLIG=cC>VRUbk+|X>oH^tDSAFaU8PAI`#RU z^12j>+E}Im8>)=_SZiGC9KU>zBa&}^>)ZOl4}YjvUwy6GAin(FAL@tS`;lfZ-Ox5` z5@SpC(J8fd&1moO(>nI#6Pm^WXf|8Qw|W}c8fdaF&xb5uQp4azRKMp!tk`0y_{;1D7OE z&vly(docTGYGyLC`~AA|O}v#A9^xA~dmcqHYts~ZwKx{21PCfNDs^}0|WTHV^x{L-Qp zHrBLB{VfbZ8)5F%Ozl`|3+H}YQYU5_4 zb&d|+(vM&Nk-q=hD|+M7`&wc5yZ6LNJ^B2X^w_gs)QQuN>%hSy+BGr7=3rE#oOk>2 z%iO{p&2!wj@61V^;9UO@JN{Fr&guC_p3!rMACX5sTm7UOyZj7eZ;W4pZy%-82?cl- z>hV4RN(?`{7O+n;XpLkmxg!9bN=quuDoA)%W~P91lNCV~a1>%}b!1Tz4IHSadq8u2 z0Q8>#YNiK(Y;_x>=Y;4RwVIsARyeM`arvrlu`^#@SFM&-YkKYVxApG37vYlyZEbYaX%93Q1l5R)>^*cs zryhG+`%a!weQLMDXiH7jeKOWGMV*mu(r5~*3(9ml^55{-y1S@Vq6hOF8_d3Q=>gaI7|^#-<(v9MKc&Nl%|o8YnqVG2+Vc;`gP3_HtZtQ z8H{8wkbqz&Gh>w{o2j1C0_b-ftmO8)0;XD4pj~CWolp^Dx7?HFETUUs~QhHtjc?xD)ZaNFOdRHdxyW{ zkiLHKTveL|H5NIGrW~I2l}vx$?{>AiysB%n*VOhu z3gq|)ag{R05^^n1t>ifbG1tggUajzc2oJZBXKi&fhB4cN@tO@mJK*gSl7z+Yr6UEAL{1!eymGB zd{vjupV#8%nzU<7`<{AKN5A;I4m|y&CQqDDeR8+5`nVd|i0XvcBj@!QyYIWV@9Gwz z?5Vvodh&D6>#=8^))R#LpMK;qotZwUeQIhK2#?~q_|FyiIW^7IjiuJZK&x3_>$Otb z^`u@d70ktPHHq2F$-|UP{U~*6fqK$5imRQQ`Z!Hu4YE>d42&p2s2l=Iq|hM%284U> zbSbZIGWoYyee*X}mnrcj@ofm^>wyzCe|=6@h_uXN{O5^|EV9cK1ia})!3 zkpP95rb>D<*2^qb>#=M1w4Q$YS^dF3{Kxv-mw#6$&zw_hY)WnanG+nnTk{LLGJ8{3 z80XrpJ6hV@)<$oj<;^YKzI#V=*RJd4#Vfk{)_I+O<#oOF%B%X(%RkgBzz@FrvcCJ$ zcl7PQ`If%_t?z#IPyg)CW*Po7eM8pIH2xLxfAZGL--xx^i|7@VzQcAVEZ_%}LW$6a zMqQ($V@i01cduSmXW_20UPpql#O~ac4g81@MCHm92O-cWHWZOf5XgW~-$La|VkH140Op3tRzCB}Tt+_C>IG zRpeWSlfux9XF_Hxkbx!@`m9s|xbqO1mq#27?C?JHlb0EdezNc(@7261-$}_n{DuVR zKDoq2MNkSpvI4jpxUqGUz{&WM5OhN!Cg$vsjfZ#m$(K4^J`R8|NHDQ#Ccns(VScXr z;W+tPIXM2FRse6Rg!+V?g9B?6EF+i^Inp5 zl9oA-UB}1{kj)aK8L8i(ZNShW!V*3Bub>6r#ez=>1NKr^TkQ>Xw$@d$BTJpGR&LE} zZDA3qHPs0V_t?ez?}z6hO0+AOb3Y~(yt=FyJL5hEzl|~I5+yYFbi6TwJh#-0b><$V zg3-|-*nenXN|q^MNec|0QZA_u;HaO((hu)eyT4b3ku z=n@9}cDt)II+ZV3s;O5l#DqOK6tARdTK1=Z1?rv#=F#I;g--^C=jkI)_2wRWc|6O)&J><>hr-#w(Z+EM) z923XSe+DOI#@X+-)|Xb)Ti;f0xms)rS|el1^O}NXl7i%fV*G@#~K!qRvuZq&{d%U{|c4-^EC+IHpJ1CBg4^`5-`Rz#%9uzOacP$f%2>rMpo#dM4G^< zl;|%3r4_6x@2rcL`5OB;rUbvyCIott3Y?-oP%u8Ep}iEG7va4~c>_Ka5ty~PdELEs zLkqVTvq(#&ZSC#wgt zVQfT=UAxsH8q*w|Qhju{vgN^`7XSbd07*naREcS6V)sigzx(Rf^=op4Uz2(0PhNj% z78UkFsWh8dKntN_M!_OML6yM6Q2vvlcQ`lPTw7JMR+lroV9-MY6zan0q@39KuddPW_0NAVNJRxUyBms8w$?M@klZ=`DI4R zop?$2#w&f7BU7MFvIIJbF;!hmg<6goue9TN|Mouazx+KiW+pTHnLU{JM;$Yxp5s@7 zqo)7@4cZFTCx0(0x(3=%`8W9fq*mwy5}KLJ>;u}vAZA`B%6+^Y#q4=6lS+8nd5j!& zf+BpGp)3RK1}I`z)I+Br4Zr76hU3Zu-V35fzyyQJFIVVP^y~wW?&ZOaT=iyTV2lDB zQ!x6Vr(o*zdRpDwP|7g&(d4^yKercky@`1Asoe$qS7-;|zrC@kF2>%Y2xGOTP8>O= zr%~$pXiGQNmbC1yb*m=lIm6k`Q&5dLxU<~rcC@;*EbqgR_3@EW?V6ZSv)+&!k3qbL zhvNwN_%iUqspu2&81!+Bkaaj-MJgF1v6%>JFs}wvYy`$44MYG|iAF3hE~>rVA@twT zBIDlX+<2R4#l)Unnm({sd-ly}1m`2rj_wrcF$k&1D4z!=q1gyarE zJ6GDt+)Itrf#gHCmpPQss~1h(O!AG5I~xsR^CKAX7GZaHii(D+6fIM$)=&e+*jiuL%EFQc z2xaQ`h14mc6e9}5%#=*9X`;9gS$RnD24{_4XL5W@{__N`Nct#-JdfH#*#EGH;tuXt zjgaQRC6I_Fo0;$TE{yo8}0lWbJ;t!M)Ks}3uZLFSIf*~X7}DcnmA1o!cHY!x#9xEdZFAeCo#Ly=2D;cqfrmbFEt6Lk|0$=C9@m*6o^~hO0`s9;3 z%WnA4>62R6T-R!6TSDG~Oa}iY&JjEku)V$|hkm!K5q8@%?CQowMkK~7F!0`IWxyEA zqim2sGLy4<4!-pmm4kD}_Yiz0(Akd;yNpNZS`Ih~3GnD(K?DNQgMCn&v-=H>8%pRl zH9e#0y?co|j6l|)$|1uzWr_;?E5Nyr(I3cELi>VoH#v^EyRoD=lI!f_XZ1ODtS62g z(FDrV$IlNeVY^Gftv(#``R{#yhd;WSGM z;89aji#cRbaARNFWk-I5<4#iO_SIhA(ERmVx_guEPRyYdiZEh3R!?P0?JE7!4grHMh0~lB^kHLMbON&~?i$8wo2qFGGLvIku z<UOaXB`j7>4 zELF;95}02AZ}Sg7v)51RWBS+~Iqo03(a+Vuar?O719=-yKMV;vdr81J#TctdRff_u zE8zv_T1*Ie!x2cRUaub`28T6qgTKDMA^$Cw?X7JUoQKB%|HFfMmMexR!BozlJ~}#? z9!xAHLz=|m`;t2EdYb$pzxYO;r@wp{C^Y1#Y8#}2dMkukeMcNI{tDp6U`ffVfP*Px zoGeJb`(9aF(aQF^2DPBk1Jl}n_Joc<^{6Hf?~{!*G;{c1^%LK>IbVnUlre_^hOxcX z))w;ZdEe>p<+xyCY_#fszz(}W{vvdVwOsCK5Yew>z61}SS0s(#>c-5d8=#*%wF&1W zLX}n&QGle}X^qoTVjxQ7KCwP4YiqhrlyPxoRiWP0zQf0Ka&(y2Yx0RkXp*sh zW)LVrc8F}e=Qt^1$JwQ93p`^p(}^<}sOO*2sguVvH9CUMGD6@31?HL2t|n8Ic+yB* ztf3wNLn`$k-2VoVQFca4{T!FKGx~CqXQ{!cG&gb z*6h`5x;l49a~s=QqI^Bnv>of}#v1K1AgGEALElt${&#j>Wv$UL{g5NIHqSl_pF|H`f;KG(<;GxYjs5h&T7K>v|i`M@4Wod zOMiDPJ{vW|X9LZThvlEV`kk+rDqc#i5oqcTfHP^ds76*(KxkI(ENFvswPuJ2R8+!? zb_bwh9)w&Wp_{Dw3FGT9>sYI$ncch8L}5@{g28GC>a`$diCExy*5MFxr zY}L*)-rm!n1v_=j=Q<23y^@G*M-iK+}%(cGCLRJ{^C_)3HAxOs6`%A|FDg{^aIlTO3L% zk$0JL9`*TWwOl3bdxY3an84?mn48#%N9dL@|42Sy{3EpW9d>k{=Uc(9e~S?D`s$Jv z?#{D|y`oE3-q-t=FY4V3=XLYW9SmU9=M`iDdc zF)HAUF|0c1q5r^fZzV~^!v{2yXY$>C1`Ts?%b;fl@j@_a(<_8F@?wl3 z(6it7-i_2~Lp2SdBrWNIN<2T_?LElVB%70<;}h(4Kaw z(Up!iZ`{^R&iiNCh0hW4zr&ylV6mYb2dpM5Gg%SDgjDcwpvup6GgUB38n9-4XzG16 zIeIz5j{nS~kE%63t}EBB>CN-+YIg1>5sH2l1^CkMd{L)~EY;b-b+2YbqQCuk`(>orYd{2PtB3Y2c2T>!kUz>X}?PacD^s&WDc zut8BLRlKb}3b8}@&W{(&aWj-3Z3H-@dOfNl0{*-Q&e3G4UNN?(>BBYn`t#jp2987c z$sQb+l5t8#ElC9)u1JE0^|L(69iIUTCV{^J!8@VeaJJsVCkL*qKiV$?GK7EoN#Kjj zhPZm(VOYm=55T?l50rc;i!S@n`v))fLH1|ne1H!sI1c^>Y88H-|M!8JRdu~>A4AaT zbhNd#sV&abw^3Fdj8m78<@WZLwg|ItZ@1MYfUA4l>>M&JAH6{@D|)SVCb?!D!^rZZC!~n(Qeqk z6odJjx)_BGc3zw7Yf7omH0R}Ko_tKFo_Rvkr;n(rq;}E%^W(Yb7vma6Q|Fq zyF_GTX-z%ks&7(2HX9=?Wes+e^wYxtl+K?f3{67W7b@Uq29+U~I|jZ={T;+Ia7^Ia z+1S(?XZee_Z|m;doaS%M>Gthgx^?H4uFqZ9?Dbh)yLnyni+7c^n%Z~th>o5)qdkWY zt2QyA0nru5N!0~i=(_}5c4LIH;8gO>os&K~B+D}GJFs8p&Ysbur;h9B-WiQ#QPGba zp<^ASE1g9jG-Vd0`pj*>i#s(5T_omg%y0)po{7o=UdXw!{gDWri~mA=2yvv1wF`BnXzUg6htUiy=lU!JwH_-e9J zCBjg+P%zmHMbpIlccrw-F+iVCVGgrIfQs_Os1ssTAAwuMo1f&IXxAjWd_FouXBH~Y zcMlPPX?7=N*++p%0bwo?#DqZk&L-o%<%%{&1^P?~ocI6vba<}6<1j)>XAkaW*_wE@)-|21> z;&a;edcA7bOBF?E>vJ5ohn~Jx%>Ti6ChS!|7J$Y>>w_Km2^fy$Yvr_rs?eEdHNLBU zaXFoK>S3CgkK@IM`?r}@d7qPq@mE(@wZblWeSN+9^*olZ>UV$e~dfb?*%?p>xneJ?LBYHee!G9Kgm_Gmw0@BPP* zXbc`}PL6A8W?CmsoYWIfJgG+?c}ypdl7HlJL{Spu zW~L62i4`2K#kEy!_Pc5k-amAL;|Yw$o`VOJkBunRh%z7t{)3E#K}XsdxWqsnJe(IC zFGv9$z=!Bf#Av!sIdS}$&Kx_Y!@Krqj5Vw=ZUb~1Y+^E;fE=nth@NPPL@F_kn6IkB z0ZA!0?iElcghY!(nUDj?0wbdvSr+mplN7*<1QDPf_!*_3>qQ?0V`TbB01e=gQW3|n z1_bC-c(R8uc`XOJ{DA1WSpoBr2MxP zybu4OPownX-aeNA-OPrq6KMe-aa5;+ z@4S|9LU-?_61AL7hoLh>dz3b`@=-kWsbjHBF6=s6l`3R2V|$kif4* zYFw6T)TyIz;XRe~DV3ZY43L*nfeJaEDAg+iRU*`e(8U;J4rcyo2=tf0%a0SPdVKRE z2jr7chgCj_r`ZEz{zQj~kqTf~obD%yHX|69(a8yQ*u4}%>ayGF5ZwqdQqTMRBq1n- zC^LhmiKCRhw%Z+TVzkjla*;=iwplal)Aq2JiO<7}7$uT0^6w<7%}=Rp%T!8pg=AA8W|8mEdm& z!+7_I8f@T1Y;J7Ow#$G6__VJCUe4QfXyrS+z){#WW9mZ zD^#HTyW8DrP7(euXy<3}s~FSUD~l?kG<{&74xBow1CO56^oc{-wQpKS4;|8zrykL> zC(r58LnpL%WJ>jv=^Dy+E*4(?6n z)SzySH0N7oE0NW#mZ{NfY83u<`ntS2KV6@@rHixIbd89>AO;=8P@f{Ce2CC;oilm^ z7ln)-ub<2RF5^~A>zE5qKVpc!aPg#TOTLgBMsPCCB8Wx z1025{`b5?`1VbkkO0Y^Q87qzg@|w(-*gY^c;pikqw;WT$+r@rcVY9Lst;AG)#SJYGk;Y^ z{g7pfFrccPLk(3ZgrpZ|5fKG9wU5V63n>u>>ghqEw zYHQF@K#>I;qXBc4SNep&>0o9uvx>w$jL9~NWs{wm8^bP6#{ebi^f5E}<;JSV&a#bj zu+8qa&02SWVyB)=Ap}*`eNHGb{glhhWcR+AnasSMr>0>arkyLn55qP0PW6%R{>{u} zW^&$g@IIVJ{F~$LxURv2%gf8vPT!5bM>U*3CMPE~Gc%*zyLVSR{5-3Pp5UDversxM zbVAL>C~SlwLXmoZ1!;narQ?Re?y<|?;`qSB?!}-F>WMy*5`3D&e+j3x2hDmoR|7)x zZY(qKt!T&E4*Q}Rt^D~=NKy<@HH#Ab>VLg&9Y>?2pB}W{9Q3rfy&?Z|uQt-qkw;GJ z(a$}lbI(1YC%*Wcp8ouEdg{q1^z6CE^xUa)IytphwauQczja=(e(#5R=e-LWAU6kT zms$;N;Q%mA=pAK&^^q!PKWF^t%!=U@5Ewf;VEi(o26c>S$y}H2-X_FzM%RS(b1&y>?Dp~bKjsdpUHxL`a z3{pW^p67Vdz3S|L%zuc8bJGH)Xebm_SJD(`d%LD4lqgRqHuuKmLSab(ek&<+;rDv} zLxQz>j-t;I+Q{yw07@DC)o}u{m?^-JAw&fn3SK1ofQRK5Szb*=QA%cndwW`Ex6{Vp z?bOjP@bWPnN|rjsEuK@96t)zN+`GU)B1c zt6c{V>d~j4)l;AQg3dkloQ|J>qlq|!52GxkVgusZUK|2RkW7im~CcXpMIOrt_ zB~b>cZ-uf(u04Bq>)7!_I>g!YSUp#bXjj(AQg|^l@T3uY%!aPR!#vR?fKCii(ceSv%rj5;xwz~t!VoKm|@R9S`&f2;> z`B<`*1L{i9HL1XXTD@~ux2|5-P5)EA3(H!BzRL<)f;X30^G)Lz$m$w| z8V*RPWGpfWT2T(BLb6gN3+VJI4gB=+FQW-v=sx&6;J5Rx)30P*64Z4Fv97!}rhXaK z?xmOh5C7Z$Ap&p7X#-I$QUqy**5D8G} zY#@@dQfk|q+ge{))2{IeBI~$lrtalEqE74 zV?=v)@6qVUsIppA4Z}D(I;IT8B^W;-*)atANRmuuCJEp*Ay`z=&NXnwi81J~8*^jk zhN!>+@#<9FsrHGdU_-2jHUj7MUV`d-f zEkL&zqdaw?b|_f~PrahkZYS^W4waH)_wPIDO(F+wc-}26d{l>|%gakzU0c!C_NMwyH~MSg zpibaq`VQZX{?yczJPhqdKgL)M;dAkfF{lnd#%L0FCc*ouPzj$o^&vngq}HfPES7mgmzC(Nysx3Wk+n4 z5w0sJ_Dc=6+S!6NTJT$a~Zo?gg}E%AsbMPm_)|+oo!x#8u=O| z%4FchM`$s?zz^6x=dHT-9@?wJ#|~8yfL0?@z}d)%jO4x^KHPUCx;4L~&CRx4mt--@41)`<&eP&bzSg8ac^l;!x~Iw7)^VhY^-V3$-`15YmvwF7 zwytlk>LwemJK(<(qS~RRfzwMh33?UqWuLJI7<*$zIsE58=TJw_2HF&qcQVPr9RYoe z3v#)dekcgMA-_EJtW?#eh*24+Oxf)J=|BC?ej|*(xL_%szXA83ou8d;jE#JCyW5_< zTm7KX2Kr&3Efn&``Wic-KCEDJA@INa)nRv1pa}hsh6HmmrAlF%nMOxj?7}9|F@<6X zGK&hN0Y>+_h5*G7V8Dc4irPE9M+Xn=)qX;@`}ghDp+g7dJ2o@GdltacB`HEQnZ`H~M?RPxj&O4m;ge`3e<(I7p7RxEx<&zKvE> z2M!-n)@*1K`X#2t=li$!;k5Ar?_mf0-NCj75(5*0RZij9=&0P_@7gt`sj0~-KQc0^ zdaYiK3nQpx2kmq4ywkwUDm=_cLl93vBB1ook_9=BWzn60QUe^3MB^M|>4rbc{Cm>y z2gC{`H%c6rbl8>SiKvC+JHq&5I2QXRrgUJ>w8k4v_*(MOyl-RF1TdJY%c|%7he7Wk zsl*7bK$AN}6E<*ix)km`aaf=K{om35c()(DPCt6OUQ z+AUpv^KG4f>n+XSo>$7Fax>bpk^le@07*naRF2;U;NFFq{O3+eun{;oy(T9np;=Bz zQcS`DNHUBHM9~(JzPW4H_5OS3F_7nV(uLa6!T22@iBQc>-ZyQ^w@Jx z>yf9Q(t#7lRinJmTJ@k`$yf>IPVBf#jE3jMh@vbcQ+5yh2iSIgpb+A#9F)j3HR-1h zPNgNy?48!Zg9lji2{sg2g>S|f8IG8xl4-%G7(;iW93Us_70^>T^NZ?l_q4vasyo-_ zb?yDD82_u9y*j7a>$h}+-Ts~1%UWFC($eZSyZ)Xwwz^tc+0wO}cl7pq@9VX<-qX7m zuV@9@BzW84^AaX9lN^RRDalgF>4GgFBo z=&G+GH;qn>YjSd22}MHjupV_xLdhhaKm9hwF|&_U@RAq;UxmIbtM7q&UgF1cks_}{ zq5=VRJpJ93!AQ*I=c+UNW;AnnzqYed+bDqoBPZw=U8!IsgGwd9=i6uQ4h19m9K3(; zgV&#Wr<9nVV<2|BQH@4jP5)u3)`%L-5sYa=!3`d>C`zwujOW0dAPZ=ypbziC0_99# zKw*cqT4x8ovau>ZTJiTC@b~R@Tbme0H|)NHcZ2R>`bBu>?%lic)Ze|_>uljrc$8s) zgC>+tj5ReIBRGDOnws3B-MePAYie3kQ+u?F_wn&5qD13rP$$poU=Lz20Bj(`$VB)o z54kdf&hVG`=8D>nFG}c}P@4CFc`|dsI4W%*G_Nf9Y7JzT1Y0YR2B_>#Zl81WF7xYe zb>#n9PqQbD_cQHNOGol?Ju*I{GZWJ~J~FA@d0mm+X~v>uDJqmvrU<{7N~bIGlR(q266&|G<6wl6{m9@XcKJ_cnIDb6g^qZ zv{|&ZUbZzD%`|yvuTDPxsGfZ8d41^*zN*jvqd(B+|KT6%(Jy{c=P3L9SH7w*eChXf z_RQnjGdZI?G?Zbq^4L&h9i3MLyqqBe7KJR7a( zb#TOJr5U0hCl4Laq1}_(g)`e^4RhvIQ&cE3`k^gj;FU_fKF0#m2zWFHeE0v9hO_o- zSLXEI#aX>a*!{g5bGo>&pgEk{JK(p-8ZSrF1~T0tLbS>HwBbenJNbQR*Nt`EAtG|# zckbxHb?WzGl<&$*q7#mFjL96)Pkrv`7ao1~ zF`atyto9t*tI*03U>Fq14z4&5wzcJD)oZz$>}CUsq(wepC?>|owSWJ9MFds|RUalT z9AG9-OBh!uMx0yKD67Nhb=ZHhnQLaUsp-iPO^&wcJEIh(jBp}aj1S8ODdSxT!@wl; zzGMlZL)aLb5_M37eY9WJ$W*Ik6cq?oDXOD3tV;Ee%P9K*k^=7`NjtzWlvxGH0{!xl z+Xcmpa`KVh$sw$gM;#Mofk)k92c~?w5V<7)hC2oa_p5h+L}^bC&s6BYHV63TDx^(Y>)P4O^kS{8f%;p@(th- zz{yxVgWr>L4)bt+m6ft|4?wz}f)YE=c^su2WB2+0=pX8z{xAO*`s#o9hkE9Tb2_zm zkM?62Cox)0&f~}0eT~pZZ6xUUnIn4o>Bn{I_)$$Xnu-|JAm+zp`|w=foge1g7-?#1 zdY29pRX9T3W5B@!`_ydK)W*>-u&nv6QSCl+iG%x<@1Id0Ua9Zd zt^LPO>A<05YBk4|A;A#`Ckz7PMg^a5A!SK?;elQ#k;fvG^51zH3sOvAWQ3 zKv^Go*a|^w(6!Cn3dezOfq9tvDnX_jI_ieDLM+r`K5=DTTSR8&FW=C$D>rrR)}n52 z^t9LyTE;o)gb`%BslutmJm^{_b567`jmgok_YX_TLGVstL4ELWD9H!fW$>0engRNK z+&dXOLOs{-TRb0+zZo4=1jH2 zT&ZHPWJ!UK8O)ahSui2vCE-mv30pTB zbrgRSLp!1tXT+6rK!|VJ{p) zMKfq8Uo_hKI+MuL#Hu8^O!VvZO6s9EWkLQ017?_VQx5Oo*{H|ad3#|=oyBDps~ZX! z`%xlQ(^*{yM#eSWV9tbK_l-^J$n=c%`aD}iR>0rjmqb4m&PsB_#&C=iDoEl27c=9X zJay#tX;%i*+|mNDs6&L)zx0*g)w54Otpj`Zs738MV`iP696SU(s;CK!{h{4^b!c*r zPVCyNFPwc$rw<-Rj)FQElU^%R#GvoOc{p+Sh@L(9h`#vP6MFvXC-vMjPwLqxAJgZb zc}h<|_NWd_Ppgu}7*&HmkT?zC)6%4E92)%fWW|nbdsV4!#J?dw|1Lcw-E6pQ8`wgMIt=>g1^t+P`l`qm8L#on@ zej@pdv2y_HKoq|Q%F3VtPSKtDyL$if6`j9yNtdr**G-(a6(Sy6jIo7|YJ*^(F-vqu zVjbvGBITjs1$Mk!RY&TxK6$gQJ;h}oT9SF7N#$+(>uj$OY z_Jg+vs(r#)#(43?Kl;rx{!Yf=kKaW2PyYG;TWy}tmR^}uCj_uopCYl)F@oOYnB za7z_lUNM2_!$9zksg#4DNTRYE_Z|9tI5lcW=xbc9gkc-dvS2-uA~+b~^d82(x3sE0 zXT@P{OA~}|4rA~i+jmHhP9M}`dk^c}%mMAIkLu9aBt~~iqcKwse!+2}A2;}phcRcS zQSn^K_Y^>`nl{`|-3l4Xy~vn>nW=-E+*rYIAVa&6pAnp+45Od-3e|e0>J9^8bW9C8 ztd&xYjbg*kjC-bxt?7u>)XZv%>|&c!qnbXpUnd?rqsIv4KXLlB9zT3kM|bbhF64Q% zLERX%n>9Rj=&+tReMb8xCKZu6Kki7(CuX?{vnr~yLNuf~GNN5LboF{ozFwh{gD{j& zrWkxq4&+^acGkcsbNlY!J&p94!RK!Ho%dDCR0L_WyTy2=_GA1X0pBD0_o;!kl9|@QVLN~W{r90=NqdG@B+Zyh1xdW=({{b|U;fe8 zzW!-L>X(HNOZLke>eDyQ_Ga&2rZT;hqjc)ACd;GN3F*ykE-PTPa%P^dl}albMlY6jxpt(O^;jLu>x`04hZ+ACW5DUi1FAiGl}ie|0G7+rfu@5a>pt ze@~M?yfcZ~JpA304}mE|krfzzA2XQAVfU_Gnntl0%D7$hHQ=K~6pSmLB$Ue#+!5fD z{HpK>p8NQ~unHjn_Yl6PC58Gm~U88$t*XT;c=of%p34AHM(q5CBO;K~&*4vtg=-UJ$1u2u$HrhdC1O z6>aXP61Ks=4#9ZG_?9YVQOct!BVP~}-db2z=k9`XLh$?QEuG#yqen6J=cW(n-1L4O z9huO<#;A5FSF_u%A^~F-Fu;W}_%vb=eASHpLokKtZ$4hC42k1aF*x6yE>2fDkxNy@ z(So*cLT;_CDr+@0F)^twM-7AZEj9b5#z2gfl;aT8n3NwU)htsJgFhOYiqyGt>yF-d z;|<-Izon6hQ5`3|??(bBjvmmVebbfEZ$OD0N3+4)Yovi)sl#|)(_tKgqx&$pI5jaz zS;)X$THV;t0>?c$4&4|>M>!5`k}1Zh5CS<3uQW_TmB2fsg+T+>g9O=4fB-5&2MbBQ zW&^Bl0fTS#wzUcD2G;{b#tsts*i~z21l>~S7$ZPq|5=OzN6A>X8vWP%rEIIG^@TOf z5pJKoHm5s_D_ZMxwF%#DDrw6BT6@|xT4rdfJPI<(kv9pPhCo(kCP5!J{)&Q@Bgp6; z?ZZ)@kSVyFf^V;G67`xmUt@PVYMft`-k8(YpavqG05Ljsk$oA=gmg%X`Y&Nc~fQ z=>yCk5WHZXL+W_%5q9h&$j0{}sOl!MLlH^=nU))zp|F_D02GodSU#Y@q6{U_*=`dB zAOheOP5$JOhV}LEiy^6(?tk>$4$x4Eu|!`YWmdZX&0E|rcLA?nmz+k08<(|%}VH!SQ`g{ zJ=37Cq@7eM^+6qP=if~JkBX4@fwEyEFRseHotahdo_829mmQ~|;Ow_>We)iGLm2j# zSX8g?Fsw76pY+qg_y!+|IncxK${VTB!z0ECk+1VOAGKs-E zL^SK{^gca;;XgjUTYIyHJQ6UDtI%L4p4K)r!m-qJv#BP0*kbr5{WhRX4L}6s5Cs48 zPSuJg6$0Q?jSrSa-l~)@^r2+h+-hqHTv}t}8f%TIv$COLZCjz!R|890cLy9C>lo{p z#kmZ`yIV@;XS>7_m*t5u0ux;>*)RiI>3B(rAWBCRL+_bu28apQoo~| zb*RGw5eLSdfI6s5G)I*qgSD8eV9l0SR<*vhC1cEqUAq*C43+X(m;&Rgx{{aGYrHu)?-XBYsv?**jJrzqv)X8T3a^1s`dA7XzkK%wHMo3 z<(&WKc1Lq`wnP+XGv?|-zaB7P-3pumktXnPS~66HhSMihFQ1}^Y4$hW`1@*n>ej`P z`enkO{^_61#(w;2G@JD>Rk^;Y(C(@=KB}^jsR#EqS`8gPc34xB7KzpJwtaF}|li4ZNPiL%)Ml_3d8j`C)%X^?Trju3;$gzpGG3 z2|z<7@JcAy2yLd>>FhgnP!1kJSrN}a{wnFH@i2!nQ3RY2VpDPS4tAv`Fy>;e|Io5`V0>Rv*9T?o>U85AxKvp%Qu)61(lK)fH_oE~>_k ze*eg*9wR({X77F-!1zvD&?vi0Kl{&d#+tlmtXF}fFkY)`G|SZl$sD@*?mv=SjxJE>H{e{{~}B6 zZdZ3!m-OD{OZx7QU(vt)<~Q}<{pJ6t@4fS;HZ#+{)5mn`u}5_5@FC4KM>JKMnmFyT z+kxl$7{O8yK^el9Gj_e21N4Fh{vQzam|uxQPylOd>snk~R?KSJvv;pzEhnET%j+^T z^`T1vO%N*?Q*a;|-d@ODnO61guK{`l#)W~D#h{XZeRp~X9c^xJYHV~=XV09H|NPK2 zyX_W+GSa`(-H$>(aE6mgoUx77b5U`Pe{ z8f6*ZB94^pAO?1hH=oH#Ufd_bPz2}{O?fTTt^V`Cz)~?R! z#-;1Jxw5J&+uOR(ZR?f=wewN+Qxh6x95)5^%bdb(fBeU@eWkAoAs*aW)kH@K1`Yt{T16(!5X#=Y zds2;N4W2GFAXF9*^n&`%*hwyGs-6|&OXN+i4Be0n0b%v}R9)ZLRG*M}2vKEGDhnBD zLElL+1fp*Se@a4nUD=AV01R*(yf5#!jZ$s1%d5&!(gTzNN7&vysB4>KRdbE@devr5fOF-H0IV11|OPMJGb zzFT*vz|26+{L@2Wr;ndZy|UQ{?LPPp@%-bz2ajl1(f@G!JgQ?LWTy; z;E(>s8oRY6wfKHwdPZmV9n`+&m?kZ%L7h5$SI0@K0Xdzz@#?g!+D#R$KpUZ$KGc)CTBJNd26u9g?qbmhWT zZIWKQctt<_-VgN6m%gb#|9AgR|LV{GfBJv@`M=Ts_iz46-+t{?y)}PbTjO=jaL#|~ zxhHkf|C61`Nww&^#ytFtyv3L~q%b*0K?7*x#y^9PYv7bykpFt){2a#Y`gIL3x=Wm& zuMv&#p97hkn$k#XM0r-1QMX{uK8MKM0(~tbH zbyn|l+;L-lMRz;f^0W0eIJKE;pR~Y%^`DwA!OrmvAmolz1*Iqz7nS{H9u{y)$d@q) zWg{V+iSzr29&6yJv`9z#Z5hWQTb%LVeCNDoXRqnX=BnNpZ0U_!sS9H@-6E>jNezwm zMl{iDDj(!HaIu0sWjGL#K#VB^gD8oip-__Rr6Qh|HSSH25l0r9lqC8R@cGlX|>MUWw&!4Mhmn?jR$llzcRFnz|32)}-g zhylvHRY4mWKi-%3Q^21TF$0qyQ?0FSsyQ;Dyx!8r`nK-QFX|Ew zzj)o%wZ!W(YjYB<^xoRd$UJfef2dqRhwI;R8d`X^g0)o~_rjHO2=N^lss z;m7(#XynE(^Y6%E&d3R;#bxBy{A~>CRn0Hl&v=Pv~{7srE7G&(9E^Y5lK7NHAppCStHVt13@{COI}7&y9WXnXvBEpa=tCi znCB1@^rHAzd4Kh{#{UEIhYzy9&HYzjdvSJYt9`sd_`Y6=B<;n4I(Fi? zte$K51A#&zNRWsJ6=+v=290>5<qKobby>TD(a`Dgp9C*!nKm#&*H;#GW$BLQ+10MIbM#*yD{1G35jGh11^@Xh z9b4zW0N+}Y*8Eow3Bw;deO8Y=_LPpFI;;JMj%s{zw?-x>HNr7bZE8|2&c^HP$V(i? z`oyI6V>oAyAJ*8e3DtrrCp6x~^0k<6mPKKM)XN1QC3r8XR#Imm#ljR*RC}wV8*}rz zeC?WU-o2|ijzqTE={82kG&(V%MzgLMq5`~Z8eWhDo;4>8V64Hg+~)`><>1F{C3B6; zub$=9Pja4jc?+ydx9DmeI*m+>>e#WPI(p!srdf+-cc2!KanvCksxp+&nErhjoSN;W zH7(w_qdT`3w9GmCdS=?@Xrjkl3;HcXkbnd-&IUtV1)hMo5&&L~G^qKDBJBe0Q-ogh z*H@CSZIiiAPmF7(HL5XmoGou^{l=n}@2=_g`lhZCb(o{C736h0uc>c2-DW^gN$0(2 zI3ojUSm6Fg;Ty*;rC8Dm$$M$c=n;vov%l!#BVi=bBTk~37S&{Ed zGjPwCZ>1Pe*!2+AtTl9qUC^OpM^Gk7U3MWQO2my#fe|X{gXuFiw0p>ypY0<1_~^q+ zl8a`5EJk~)R->XRIj3>Xi|^!(-TJnl`Eyo0X_?N>9MGZBNi{K;IXqHxhXJ07&^$ov zWFjA=LlHO|XNLM{=hn7@5j zS8mSf+Twy1x7$PpOud+E04M~|3o`!6aRVZFMpC{|nkY`H9EwmX1t~CA3?{Rng7LcG z)o1NJ`c{MP`-#S#!8tiNIi)(g|CPJTy0)~g+nrJ?oO!MeqP74d+@IrV<|tz&?$5z6(nR;$Uc z(iJ3h$pFtcaiR_%J)-((tBL}+Lc;cl&+={p4{lX2`s6M%qsxziWcoB;3H*{1hja%| z3sTOG&Uc)N;~x=`TEl2`32W~e8PUm^{W>}|tx0zDBbI3d!=B-(dU!l2(G6>dvfeQk zFsvdLEdxebpL(sXO&ov$bvNO)8_Ns2j`8n|)HHJZpfo+HZXT52MMn+WM@_or37KUE zk1$Mtr+q!UosL$C05mc9`Q)Vh2iZ2jc>{ywf4a9Dqyffqv)`A&!v~HW)A19hH9j(- zS}$p0J87hw)TH06mZ_|nx)}cgC%`ahnS$ye2gpGjinYGFq4zIb)_dnK>BjuLme$s_ zjKe*(cUqP4Z?%+jY?iTmcLDStx*HmP^NFZ>G6$x_`%cDFshTqtBJ!0325s{YRLgVq zIF`A)w4fRW_88HN)2B{q-^{dHSuS^8GGne;Qbwj#hxl|5Fwq{FL(F_~NhqXD@zZa?jKYHa!lr z!@?*bXK6RmW+p@M`xs$~ytw?M=g|^|LKXRh01hFkL|eAIoyq~IegF^yoy;Wa`A)b* zj0=}``f=qcuH>Id{Wi0z4^iKv00~9le|>Id|9+HC6TdsLQ%Uj#HR>c3M5zxhy8YmV z?+?6uBx^sW>sHkED1X4mvS+gU-&po`sW_Q+X596ka|C$><(gpulj^( zHIwPo#1Lf@xh|17Zg={y|$8+~^OWp8Y5 zV|aI|HZrE3Sl&EWs^toVq020*^1?VZ3C-``wMXNlYUr71}0T z(a%jG51cTOr&I?eIrl--z*Dijg4ym;e$GT=9EsHII5XF(?sR! zO6>jzYn$qBcD3DO9-QxQhlV;?9USUXtSbXo=chW6iW=j_av+dW83rmKd@m-(O>)Ph z#QE^?oyUCI^6~Y-WgldJnYrH@|DQo^SmtMZ{SE*0PyXZo)i**Tyoip$z}kn{8M%kQ zOZeO&2~Eg%$%WBDh88x2B=spX6^h#2>{mYoB{WI^Tu~;JNQpvsW9UBv;L0v{X-0v1 z$j*Nds}dXxX{k~JwmJT5O&Nx>iQ%p9o>De3Cc03Lm+pVUGlqb&E!06k`yqi>lJ^@y z%*Qp?)YSxZ|Ek8|%^e34dFtT#|37nS?02QQRCK_n?X?CR9mgf6g6wbC5X1j5>XN;h-E6sQo`(O9=A!OB$@YtE`f31QyE zJJ5yt3S~+{gJ@&5))>j3dm?ScACalp$Db__L z7y1JxuvBCnnsOA%!CpsOtnbFetXi?H&<_gGDB2Ghq`zF)#rrS8yYpa2uYKn=edjN~ zt&KZ(6i4gXUG`gm9n7IXHVrYmAivL3g2ygd54I!x#AMhuUXcrMuk$F{2 zDGoeSreP~lJ~OJNlZ!fca#?4HW-cHrT8&i9yvm|4ABS&e9Mac7rAlb^-|Xly-T}|{ z00W|zm+&ADwPPn5hq{q(esZ6eXheX z2S^GJ=TYVt5mp1NyNVEfZ);PjEC2CHU6@(WJZr0sH{4>+y-7RYXAX+|Nfq-~0+lgQ zN|^ryg?$9U%M&>?q zJS;ze@Q9*F3n}eI?d^8dCET8kPN-QnsGG^xp}!>P`{)VE#o$hWu1uK*2R&VXZ(Z-c z^_G0!-rs(A<>PO^S3E=BN8lMqSw|xlz$tY%x3zKYrtV(9r8^HE=;q;`?w~La9QV|u za-t(u+E7Sx#+bs+AjpIRSf};JRwRUe>Uhpl%*P! zrHu6f#zrR&v~YSw{-W!`G{$g9HBy-x&RXw-K}Y&&VR)!J(AK?8!v1%4b9-OgL#c=7 zU8$+Wnl+8Egt@DLf@gjR|4Y?!q?RQ|c?+V<#I!M(SQgT5Hw_52L7B-Mq`-D z9hH=Iks$Cw3^G7qi8YuH;{yofK3YP-&+>HT|2r?f^0VyXmjSoW%P+$?ex_rXTY2tf zlU?~}C-tL4(W8{?!_!Dhgi3U&VnO~u_;$h0?v8rhUadBq@x~pm>jf1`s>EyKY3K2& zOTpr2l$j|26@n+Vga<9EjW<1s0g!~Cv4^3}C?PN^9K^1Y01lK#1Volw_34*&<}-Xa zKFKSBpq83Zku0AQH;oU+m!`Segl?BsPbh7*hytwZ&b|9;;3Z7Dz(5#Q2*3}j~QtK_h+h4xteg6d@2R6dMKQT{$fJ|B-p<-I#TPd|i*cexQlV4bi& z$yZe9;MHz-4x~n^_SB>j=CYy=x0xL(kg4&rrp=53$*;x1E%akllB6h+gf%$O-oYW! zgEsgQ4Xu!6k|mN*LdIH7+>nhyBdnhmka4rXv%gCO;GOH&w1-j}WG1S}@b*)ToF)pV z5feqm`(S5BZ?o;>-a98l4!Z3>{f56+aUJ4BDs$pz`SChxxPqkgbOT7fxyR%&NL7Fp4?~Fe4;()GtjO_a^~tkv$^dCrTa5 z=Y^lB!_U^3vAb}sPz*lT{xh-puvBEPcKBdhdAF~V6Vp03w*LIf@01axzU>OYJgMMacZ-%?*A3-S_m~=6&7i9cp9HmH&=Ft2v^{ zu^CN{PSQ_X!4jGO)$~Fk*D`4IG$bk+V-NaH1y}PH&7FW z-i}EfN%Nl)zIpGi<`x(A?DJpK((;l>3fK#BU=&&r^~&3$d$-PG-^ zZQV!t?}GafCEU+~xD78WmBWFC-L6XH1ZWhJOm5I5Swo3OF?h~pK^3)0#1P2Sj!0rj zB%;wwb;85_r&qPMw4jCYQH?=U1A3aQl{RS$nljRyIhdTE)Cxwz0>;u*nq#y`IrkbE z2_D^XUIh9QMa$w0(^5r{Cy7E>2=RTizM#AT9@Kvt=d5i$V6RHqAB~Qu#0e<)H zeQj^;$SlZeV5y??#6u;r6;FGCfb|Dk2nLzSbwGmra{*XUHxUOyJI4!{K}e6ZTS%&? z(N(8&uIw8uX6>#x18ExesZA?4AawUPzXpei)R7Qm&ap3n#as)I1>Q|4o& zmV)%DVBCBB^szZSUiPE&kFa?9dErN6|4D0<%$D!P1_XCN5SM!gsxYQ(M-9i03K#@TB13tq;oFsPy%6i08P~JCqs?wV9g=CCO4# z$-Fu(L)PYSFu-{)6qrLZkb$KN^$M=%E8{azADylTd%NnPNJhuTl{GU}d7&y9Xrz_u z)Y813JatMJS5Ijk#nWQl)JihV)nFZ0;NM4~Z}$)6-oM`)Wp8`jat-%-x^eB6-gxh- z-n(~CcQIP-p)@-v@`ii;IJY*41H)p_Y z+BD^Lt-dFevm2)=^Dx45N%BE>-aayO&oz#=3Pw6br;T%R>%cQ{ECgzf5$TM5Y-$O_-vOI0E zMKi%OIEnEx2Jh=g0OKHtpeA&s^cEP0g!(r=;bjFK74&}TPQw1j%K!7t3M=aKHTbI6 zb|WCN9S~u}4}*$_s8m3An|5~gba!Jz{!f6!!z1HK0=&IHmg2I)doK`}(LE3DfgU+x zU@(95a0bCxMudT>ftM5zW)Y#}MnaS8(eW|$P@)4KRztuUcoK{T^G+lo*T6%D(~BZ~ z{xEQw{Jysn^h8;W!>=Pa9IsI!RE!Ztl;Noq&=>GjR!^VN#QcKp?eFW|Ywzn|8_yMb zD%44pRPbcT{%ENav$HyX{;Zla<2uk#LwGgXvmO$Xm)?@uxZK zG-Wl3^%PaWhzeq{ljPv**V7}2s$g1>)Wnr;f+s=gRv6Omk(WG670*~E<33} zuMjAuOb_t<@AIIU@lbP3v-(N$I*!Ak0#AwH6i4-NRss^(xE5_9_#%SXZyAEYe@kF@ zcTYlSxB)OSHl{w_N4FZv1w@F#+BbRqXmm%$Q~BZ7q*QrC%27%m6~HaY$0OB8U9WC$ z)blHfNlg5C>TVjHMmjKhOtM__OUs&ASExq&pyDEtY1T=bqx1WKfML2Sd@m)M~ zM)RkZRWuU~7_h&yP>+>5W~TZ;um<{~$UkeKThX2BluDkG{K(UB!wo@7jmLX<92DaR z<$?YkK61bA5yJJ8gEvuX3e~ z;gzt~K1$~&vm#Pl3#Vf`Ze&7 zh~R@NGRC}ziqcZ*Ft%&Lc3!^OaS? ziX5_SwVlyCOkVp@!z|7SG(|K zUqTVH)}2Pj3(w5QI4J2ODrHQel}qp|5;=(&CSoiMS5yb5R53LoGLeqt8ef>wB1(Vd z?5gHZ&TDFFOp_Kg0^K?NWyUOHh4fTtkW_q@a>u~ORP1+j|Mp#7zi~tNF>Lm#zWNA` z0{jK-3g?q(FC9HEWDWkRKCJ%yl6+h~R{lTUOfc0yUc;|?tEHvqUqM&AP@$ZR2Tt|# z5J^m4Rw*bVK)ZzWT=^dybd}(ph%%Ze!a%dSkt&qGd;deinMnvL5bl1TXAvx3QFVPc z03wY?5YiC=aPR(o4G^?r?Y3HDBPtPgLuRz9(IQNsCIF(J3UDE-`W`qx;q*SexME;t zrT{IjFpoeIWqwZ&kATn?w3c{>$cvg*Pb0!-Y;+8Pw5-;`lx}X_LvUYJ0j>UTW=A~K zim+^<1SVQdojP?wv#X2hg-TsKD5tY>I>B!;1|R*ihKF5$B>$-F)6ygQBexADLESr-1!^qvlJKsa8k4()d&6*l-j_SdB!w*3`=EoMw}z8vUp}!sgem-PFzdo7%t_ z+Bd16W%9SuV@T130DYscG~`~vbKuW{e^aHjN>wOTFvgNSawV0T?OfB-6FRZFtf$t_ z>bVnVw1NT9Oj4B=<(_-NI`p-jGtdV7=&>)qvAd;myrJa_r*&%Wgr+CQl?KDmE|i&+ z!HM8TJO)+ZIb}I(Z+ll88~3%_>ua+dsFMa281M=KQlcuhbz-NdlxHS~mkC<}70-4T zgol}Qm{vN3{=-nz&qqah(#kY7GpQ3R%Q}Y02ck zbefrr>x{V?cKf<}XG0rXTiV7b-@)WgtIbO>GDS3d{?>9B}!{ zZ@+l_k9~g3xnPQa%$CQ^w6yyCD~-Hy*~5e-)^>qXE3o)oNmmGtqB7m);ol?z&}_C; z@Q@>2;oM_v=SkvuQi^*DK|4(s|Il(LtOroJ38yTMiGjn za@wfb;Fk3V8c7n(O%ax4pR_`#z47)Ny34-pNUzfPP^w>E17V8sGdn)1vxJ44vy(bR z5LuooWpC2Q>ojL5r6GIJp#HJWi1IcwB?#C+pFTg4eocNvaSJd7GD$F*1VrS2r3!)W zGZI4Je$dfzR72GI=|PZLA~REv>d=3O(0@B5TA7*E!q}LalqV=AL(#Y-_BoBzcc?8J z#`$;}xptu0$Ii%gRAfx9Y##Iv{*uYTD*b-WvU()LQqWY}Wn^&*v>bvi2{pvN{ymMFgwzt%? zQe`6XsgcpPR+koZVtP(vRib^o=j(+0uTpp8@KA@Wt6r9CfKgP0RF(70`vxq5b37*z z1sZF7)sf=YPzevi-0YMtoI9;&pS-B&pLtSWd*YHVo;;;#_9sdV^uumPC3FYYA8ch3 z3N;w78`-xoR0c#5Rxg~XmyebI zPc{iGx1S8bSH1Pz(lf8(xU2;pm<;6{c#xAM@Gv91@EWe)xTW2_eWeJ(l84dE#B(J- zJ`~Oo<6wBCYQYLZ&HT zd>9I3oQoW#vB%iA@Z1jeceTH@tvheOqs_P8mF?`wLzBZ{SN`AZCgIKU+`LYoKP%zg zY#r`n$P_B+sAP;)Rmu5Fr=R;Y1Ifcy&Trpl8Q{_O5X#-|p$7eqhFnLC7-K9Uc>Pil zgG}`8Wn93*5-d^mWN@XQ0>Da1&P0=$DTF`@AjB&?-4x}!I6AHqlhYav3H&WkvK4hI zMXbIk{!AYno^_!3gR+lj7=gUs%8g9~zW*%m9Sq4Ho~4a8S<0f1AVZNC-J$%~{Cb0- zN^j)60RrHCI6r-@Iyll)iAqA*_nGT-WK^RwGs+t+4LSo|ednrP`_Ain>&j@+}x+&80GRIvw44a8G?JbP|R8#1l{G^z6LmEmH%dwDi8xQiH5i z3{j0r8XZdGgyIJ5{olhFx;N-)Cse8k?fc8FSeB}IKKcg-YA^?RGZT7QzWw3fo#8a2_=oK1czA~JUK`OZi z?{giXC+9)RF-UR@fCffY#g@v>&b}UCd_2Ht*$F{ic-n`j14~o@B@iuquzU^ z^Uqv9R{lTvBz!o-KRK?iEQY1E=hv0mT104+2q{mINxDbKa+45Xg^(`@y%`Ug`9tO5 z%E!1B$q#5(dz)PF%!=pj11sAF(hvb>Nzf=>RbzZqRU=WvTEDY#SGR86Qj({dz%VEi zQ;DG<2uMI=;NwK7$x+rocQtOm_XEI91A-*uPaH>ezyeO=+D{kyQI$OPF=u9KC5c*o zbN^5q?;z;^?Atnc>zZbes;9QvyW|HdE>Z;CPyr7&& z#vZ~rGG1ehUFv#256STsp9#z!QOnp-U{ z%EuE?{>VjzU@{nR6rm}_)o1Gavs8-E^cab<5Wp{$hh5ozSM%(FuZ~Y?65a%ifdu*k z%E1&t9~Gg?6>s3uvm)h2jl-`U{M!4e=u2jtv^EKKr6FUxcetzVvZsha>2IT_jfN^$ zB6!3Fio3$N^tP4EmlRz@CA2egl>@jUcoj|PNbT<)Xb;{tS*J$irsrE=JO}IRH+6?} zk8pPfh1H`?(MUDSQyt)e7Od~(*%>{1dQD3h4F`Ac>)QG?-MaTcJ4BayM3K5#qOJvu z;V7!JhD`#GulYf4@|W)d3Uzd(rN#MKojrX*PowakfAW$poLtokVgE^RjPjf|SqH`D zrZ#zY?%ltq4m5Z)L_rEXtL`;N44((vo7yXSTERH|+Bd$g(+kTQtAbiY{2GM%6PRCF zr2+#|JP#hxYE&lmN*$nNcMdx0aM`Ed$g}B$4=_Fl72_NwM!JCY-egrjB1wc zkF(3Gdg}BUJ;(F#+@%Y8hW^iE94%t#Pvi}?CFK~388kXH$hRuW=9~FiBO$RIy5>n8DWs2ny7%aX3N;`t-zy=fLF`f11`M9V>sB z@Y6WMPXoy@4ryuW`Sn=XTAHNmfro7yFZb51yXqq-k|fiR2h*Ue7k~)Tfbtv5TRMN{td>u$;)O&6)Ig<4N=&YV zBH7P##(31i>}Z+00WiRr7@|;Ip_lYK91hecYT`yfr?aoagT4BCNK~VOkHQd;N(Nmj z!f)BC~u~=ZpY}!Rx&gBTv1hC zyW-k$*4G$_yBNHYvXRM2#U#NKFVyW98bE0gG8KgGhhC0{Kft)#8}?K%J+mnP^QTT} zk;p-LZ&UZLUDsXTpT>JWu&Bc%s89WpXDe8$REd0A+%WEjs9rP3MjM)$nbO+H6M7n@ z`6Np7VSZ?j_%YR&{upkALFh)lT zj|O<&1;%ER?SRtzZ6?a*#x=XPqBD5+PqD@D?AirAx4Nch=2vunVp=P#gQ?0i%DQME zQ&QwfVExvboOS8Zhk7AMN~xDr0b1zOf(G!eo0@uAsv&$g42V2$D_AD?6(Z4rq)e2Q zaJ9|{dHdivqRGH1J|puAYEdjt^+g}9{QYhJ<30bM8t21L4bw3qEfEQbnO)xA+gpG4 zJ(S#o2PiIQj6XsEk}~qJ3Lz1ri;AOr$Tw9y&=KKKmr&2sM?h8ZM1>^L1cEhhwv;xS zYL1O4%QDSOPiY+GKSRhh0CkdjzzBr;sT#lPg!} zi`Poi1b!OjVoVkHN@VF!1wyq?pEY3A6bSa{I1uUqpfU{}cK0bsr2Nx>TOXcOGBeRR zXt{vL5gr6Q@JaTi=UZcHBWV*9PQwyZkJU0BJ;i()pc=$s4xsRqtVAXP;-HvM$-5F| z9yy3yY_rv}ixH5a{4LKELr@3LwA<}#NQk;Zp+T5@{LVXXW2`|mhr}9o0G*?hz$fV6 z-Puu+=4y|Qa%mI?A0D{V8iv#x!uugc#{psgDoZpwH>)QuoR`1raES81!?UwVx`U_e zzmnHyee`KpddMGT7A(LAlhb8fHz!6ky|SRw=ht)zkN^CslUgMLFinWRl^BX`s0OPi zBZA_;g6H0HvAd_;yBpfV!|z0l6qV{S_m0z}EQj3h5-FNqoYVQIpU@H!gl3jvKvZhh z3z0PgzJzD30iW_RDr5W+s8}ak2m9K_0O)gn$liR86OdwfboUQbV1!1Ks#I#vjBDxa zNv&Nvr?XF-*SWK&b#h@zD=7U%jOIBM^2|V*Afhlrn7_?h@Rw(E`bm5az=Q4YjXZD|3ho_59y=EtDMF@+G>&LV$XJE?SzK#X|LB&lIKbp zllNJ|3-qE2cvRp)#Ux1Pv_1sSJILf7U4)-PX&C%Sl2npOUIs)MVU~b9lh20>@IH#) z!4<%vZw=sumKZ2Bl1VjySiWUb14KEV$ekYm8qm$7?iBU#Aj2&Qlg$4FI32=o$^#Gn9(nTTeT8M&i0np9a-<-I{zLYly!gXA7DlG*&1Xsaa-9szeqNywFOU<(>}Ho~9Qj>oE7ztEZIpE8V(zNAGPu z&>g(xo^r*cMPHdE7)Jy0N{Z9Sp8$fw81OP?81h5fQkxb5D~CQ+kR$_b1r%n3h7SN$@AJ2d#ac+YUbEqQsQTn?1h z3Eh7|=c`x#=8J2`d;ULtZik;f%43W#{M)~|-su-FR7rY82u{TyXEgFl!6HHs9#LSf zxDdPnYrR7FRVaXfut+2K>xa_h4_%-ft*#~tVRUR<34aF8Cdz(fc1jPn@2iW)G16#i zX?8&ck8c;nF*MqPKcRgJ1O!Kg0*EM8eadmvbwuDsc^)}ar&SVF1TaH?s7!(WBXk>r zZF_S|Z~oPHboY&Sgn6PT7EfxfIWFs$iu6l0A&-?p%OnQDB1XV;XsCPpo^E{iHSJuz zs?a@9sCvrN3fVT)_;_1SVgxLmI#GLO2Ph@~ftkpdLIOYGlflpeVK>ARi_!Fh2jka6 zU@%FNC`~iwAW;Q>y^UES<1ca_mQgMxA*oT7;2xle+z{xpwb92ADcCZIlp`G#>RL4P z)cnIsNue=50xrFV1KFCFo_ci$1`8fz>;kBTLJ4?{qe-e99O8(Kw;=COkqV)0@ZV_Z z{dk$DCJsJ)=4zfZ&rubM&Ag_nSgN4xPy^kywnYm701yC4L_t*A-_%Cg(E#sx$awoG z|NcITmF*FK;jM=<=!K{vqFT70S^}4X%#}?Diu|SYmB9O4WFY;Z+tV)5lcd#BNK*xG z3$Ln7A^qP6!x zS}K5Ke5$48(~G+J8viTuIlvoq-Nk*i>N{q54#04Q2Ggmr6?;Idjd@st$t5o zV{=FM+2ZJ7z*@!F3gf9tw1#1UH z%9Qm4#1JIr=U{hV9iViho+c&6L8k|AFp>xGEsnP|a&l2ijQ!+UwmmSI=9Ov|88CtI z*Qy2z)>ml*)rvV(Dgn<|%t^G!vZ(-&i-Uw>nL^ETbNUkII*BT%n@DQ$9Qw}y=k>N$ zUn%<+P3FnJ({%p^{6cw*2N zJP+AUR^X;V|NiE-4ln{nvxZJCENYA`iiC1k6P1fg7r+QLsT2`kwFx&*<9&>fZr!=1 zx4(<=u>QV8$TvX&HR-3xSSF_?2)|y^%K6jsA7JqxDq)*aE@&&l#Ku;>#}z{ za3cVZ+Nc{)j^4(o_d%!hS6}{+564RFv>ONNqDm z5+q9;qJm2w1b;;+Jz@|v@t77S$2HYzYJcONuD|i7c5dEM+Uc_;fVW6{Lr_i4Oz7lA z1pUP`I>d9`8{(zn&E;vT3@P6zq#QTJqB$#N&_qsjeFO&oPzo?vNs=f>x#Wb1(=3&l z!Pkmgk$iwgZz;JU5NYE)Gdn7C9u_Dx|3y51k#K;g=5r>P8W7fA&1=#dMc|{{^G9V; zoyMb7j~{-+uRV1nb)E}wQoU~HsneoI_o?%6xT>gHaqR{`X_ZPC;2~Rz9#%IM7>gBS zFMu8PweRijst?k<)m8=XdcA@AK#2irG9}@N7YSM^Wu7vWagtiipM-qs6B|MBaEz)6 zsE9(L-0|dx7)vGmaU;MaW#GV4(C(n4147W#3p4T;-B!k?G>QS2Gnh6Z=_S13b5A~@ z3n<>@r6o1S8g=4_c{4j2F#^yA2?NTec2IKfK6 zEg(m!yU}=v7rwc(tplPA0e1h5$Kb4d)7Y6@6>*;lwe$kwks-FcP-y@KZgE0$!^qHeB24CUpysoV; z+j05$cm03%QxbmmT^#rDN5AvpE9kN{_R-d(B0{$Wc)}mC1_4wNt~65PVSF?xF7zE- z&;%EDIjR=nd#9UL5Ja(Yq|cr6EbVnHV%1B}hye z3~3{j7sn?xmF4PhZ0hzKZ>xtfkl{f#x!<4z^o^z%)059Vsaf`WheQ+(dWWJ5HQ_ziTp4NN|RI;QXvmjA7(PjMOt6;VL*f~ zj{x{L2YLw5ArT8#oV7mWzSpUN0;l0|;I{mprymC|H#1p)T7yf>T~!l98I=uu^&Eh~ zxUNq!3{OmpTK6xnLYR9DxL{K74~yFE?CIX#1C36P z>)iQsTAZH2W0poxb7`brmU3@G%coX!_S{)rymVft)=sF=&XwjtCr>Wx{Fzf)VUK@$ zl(h!lgfM?bKP_l!1C2^}xRuI4|MY2+aJm)XLzA%%+2+_GTD8x!F%+Fbv(sIW-`d+% z36B>sJkFgxtNGbkr50#gv1MK=rB2548JL#{pCkQCrofg>BL+37A))}=c>n%2$N_bS z&~Py9X>ZU~Kb2Z@lUh8ntdmR2S{j?sq-7exh)wCYQiYjQ%G>$82WWH9=RM3DnCKeX z^$?0Ie+77y@<#8r=YhD3l=X z1@r2NHXnx=hbe?aX-JhQ$cB}gG1dIYlxFiWl>fG_e&;pq-ngX<4<$jtHsHAw9lRcpFvyH~_2^Zzm1&B) zXNd@`u?N4pJg?KMOIn;^(>D2rWcr%ghbE6w_&XaVV=PU=M*0c*`6)Fu zt!t>ug#C|y+E-u9fynt{Iy&w}s*QjoTviCLm_Lj>;Zgh{T-*SN2%EqoZ#D2x!n9Ze zp9teD&lO0GG9ff7Aj6v-86DNw__%J|x~Y2)?rU;lLhW`7EVKhZ83h^z9bxz8p9Zdw z%uHtXVHHQimC65(6k}Ci>qx*rcZpXi!oE$$m3n6`lr2$`G@u|+fe>~tQ!#^zoG@yx zM$%MT22CU*S{|R%R2b3L`YpZlSKrarwHs;^R&5UpwgtMHWb8|mQ~LUI&*|*>vl?P~ zYz{lxPpewMf`)$JC7%$*XO#GKupl`}p05ZQ_;rfMnKyEr;Fc2dMOi^(RktWmNDifs z3vf)vxU5FvR}uDCwSm=V?|Ohjam7oackuz3SFJHdI2S*phk5f#5gtX9Rz&0@@nxnO z#8G10RotgYK_*wS0TeY<^6GN@HBcYDQJ2;CspsV+DnNPwp4%v;t2>+8RHtT;I5@0rT|qGf(IY;qNK$i%~f{&2gC&%BX6ucq9jJV;@TLv8%mz~nt)1WWCnX3ZEx-90etAvkMl;1kz3l@ z-O)5k|LnOno%VhCsVNzKB|cv2dft2;Q06paFR}hA*03orq-MWTNp#`f)|MW04s}31 z_vHIL4@1^co2bVsxR*|@5S<&-DEw_fK#uX%tfW*&CW~+1)JCl2LjjE3XFEZ6w9-6N z;}f3(2S8b1PBKb-du9tGXp=}q2l@t-6S$zQ>F0dBTEFT4;*0A)XF9%8-G{H#Xukx5 zz4DtZ@SvAN60TGT81d-2w{%GOzKJ&)y^M!4AQ&TQlBOso#2bRB)oLM_Qw{wE9_kqn z^niyrNpsB;x|A7FfIC`XpRv)%wTGqeO3U%njcS|-GS7b+E6l9Ebwrzv+R&h+f38$T zBo=l2z6)umHvwlONrjA8#)?W3p%V~_xD^HNMa0XOGDXX95Cb?6cz8`>(=(5`8bGVc zHP@Qb()f%z_qX)=zy1ro|CirWd4F3|gjh#Qa8n0X zpy;9sMI{;H6yB8xULS~&hZxQ%qrlAQKLQnFOVohpO_WQ6Xn?=`6#~Rl)6+m(v$_@S zeM}Bfe9Fvd@AskxL|bVfv>@QMvN54Ym_^5d>4!<$1w2ijvRwCP|KjjUtCQr z!SD$3?GS$tWoLKKRj~%NIe;JUUVC4$VS3_J(onqs01yC4L_t*RPwCv+DJ_nSsmVAS z&{r}z|Dm0VG>87YG-a@<3QO`xp!b#zn>bKKQTSxurJIrHdEYav%aQH94+89}Y>B zGRBwEZ!l95g8Z;RpR6^12c%)CB9W}aoda#$y|2A)M~4_Bn~eRi8fboHiS3L_I(_nl z7G`JElZjQnUt zfAYQK(w`9r+z9x4zxm?JmEx7a!{*AWKq!*^;K4KF0p?*Z5&kYH0|L-J){rC$Jmf(< zpggsaG{K`7n;6&V_=Gm@-;)c7sp%<|F6?;FMIBI4PL8$)=rbN>^dqZlKl(&=s^Y>y z2*N-oe< zbmRMPur=^NZ4^$5x7{>pY_zG9XHV;?Z#=89r5SAx4%K0tRT2xLEDGEy#>M=_+ za-u!`0J!IQGo>h*X0xeAqoEX`oTiz~XafZ3b#VSw2x@(B#G{;%`pLZk^z~2xt}p{k zNrA+1Ku2J#5;z~yVU&rnds&n@7bWJ_=wBhIQx0oI;``YVJe4?-AgWSVa-~@z+@ljF z$!J?yP`@ZOkQBjl$Xwjn-__mTfessq2BS^&@6G0ihzVT6?^_7fkPT9l1 z5KVNdz>Ng(RE*h;fQq@<+ufJ%fBTDNyL)@uJv`8@dw0|;`?~PdMV-HRR?{OR8iU3* zgYnRRQ5Ev-91EtvI1-c*VHMC863QtDf6f>i4rNqMKPhDiU^N$_d=2^we+62uP^Aj3 z=gK=$$Gv+&YuOp-!A*ZIOb7JYXD9`*vAapMLGrDJ)ujbZj*Tj(zW1FfD zmiHGdD0O~Fidmw4wt==DY^(n1WFjDYY+dbHscd#q3ny1~%J=*yCNXrS9J+lBPGcQa zf!;@B@jgIVuh~e(yKo@;nNM%$bVivBzVZo?T*2QVTUG}LhuXzRJAg*t8Y+l_6s!ZP zf95`ZniWmDg6IFjPZQ@cuFuW^9{nsRDN`XzZvBvW5%-8G%+JF0r<;mm1tex z<1RG1M^}5acvBNuOUsipY9RFQtgq|(J8x^3G2tRBqrZTG(Qc$VeR@^TJo6+Gk3|$s zp)PnLhEhy|1}H*$fTo9vz8*7;AEk^*fR866iLyps^Eb;IN>H8==G6)o3Xd@Z*0{s{{bH>rcC^HK(t8?|dP%PswghbZN(!)cb65lm5$q!Ll0K@xPod!QS;Te^L?tAo^}i4oaY zn>z4=e(7CKpPVLcOHKiV`3YW69Q;`0;n?uw5}P^XM*Cpo}VhlI`( z@(wAB9bJEG6H7XK z=9K29C+n}Lri?wMQ3^j2fTxBQk}LiS#1-RbDJx-Z8qcB6$$r19-MxJc*akR6!S45a zD#u!yJiVet41mSSY0V@}x#9EZMa3wnrJssEB7J&4HSP4quv8g+lK;G?GJWL{&3;vc!r;2-^X3U zfh)iH(mEc><#JG5@%{WH1mtohAqC9pr+Ww}7B62uu2 zyb69bRJ5V0#~~DA^IyiELC7l7Qh|Ju#rh?$wxybCk7|}Z|B|hTx88nJ|MFY^N?SYk z<-gaTU;vCVwmHJ~7nWA^^=F>dGU4TJHPl|OtB6+9-uAjv@g)4F?02c!U*m=Jn0Y z6R#?>M|q#kx>yv-phJ14es1!29JY(DI`FZF*6W9e_Z~G&q!J!GxB~LG&ASY&1JC!Y z(w)w}-rKx~^54^bW~#=U(r8N_A#sD!z1NJ5Uw>iKAj2uB0UR}R6cyl~NFhp65V}fC zqJkw#Ln32Lo@T7OJv{zfw{B^F|4_X_p}Rz()^FWVJ~pB!o_ku$ON$ys$)wIqcwI3s zkubO$0SS1VpKi$bx-l~~WO&L(zubW)K0lzuOJRcoi?B{1+*p)GnvnXeLA+vOGOh=` zfwtN6KIp*5ZcoBU+D2h-boVv6JWqK3qL$`o)yh)^)|D%M2g5+|F$SoB994W&7F0o> z8y8kdDP!_?Ko0f~kQ$8F@m8wjMsneZ z7+s*BdOYAY@)aM^f&Aw=VS~IaXu$emOHgURdi3qXf_bPIM+N;Q09hZY`(dk}S?*}R z|F>T}e%bG5#^TS0*w@`>t9e}OXU2hl^v!>`{^qy7{c^k6zLFD;u233S0WufDK;-dE zDED^+Dwo7Oz)nm+C>elEp+;0|q^;$ZW$od4-QBpWNZlb1eTC5V`i~$!Z2#!`QTl1A znTc-XM=fB;R1`ge!o%;XRC^y5RVC6eR4O%8Bp_YIK6m?E_f=S5!`M^5?5JNHYFPCs z9{_y~QFZ~E8_^L&mCzlNpfU^cAB6d5fATN%?$x(dv9H}^(Ahyxv(!Deva094{xzMs zbfFfi54N|!TPi`Q%V~$+`h{fzt9i zb>sTO+twLAVvWO$839KH-5v?(5gHy6k%=DKrq;*LkI#c&myEwei}eZB?+`w}iwC~} z`1i{9xpw7W<*2A;rjj~+tF+Ia^DbKkcYB9=hyDI{@7>k?K}UmjCL3>SK>fp55G5H( z(BfhB76t&e*JC94cpiad_J~s^Q>e!m(MSpTEJ;wVQW^s2O5M1ALpN{T)_|x-M2mF? zLp4U*y7=tVy7=@{8f`Y!q`yWI6fjJch|`o3hj$vxOa)^eCQ*^~ld%2`sK`G8FL&z1 zK`~Sag{?YF&-Oog@Hu@{s+B79$Rf}o;qC|9+uFkWHTrjh_`$)R8go;+^vqK_xw@*! zu`%Ts3;`Y`tkl3-5L*StTnKy-4n%0IfRZ^bp+Sl0QyL!N{Xf{!=GK<(?eA!qCz|o7 z!i5VuvAC=W)=>jmvKZxSHfLN$A3m;vfmGm$;3Hd)C6M>i$6J}_J|8%)NIM5XJRAi< zl*^5Y$qBXE{@V``)>Ja+ssR5gpctE(k$ey zaNwW*lYja$9_ShmN&R~PCQ~F_SK*Dc2<EaG}>)ym(Q2w5{=paZM2B z9jjqnV+fn^$uUh%j%adfL=%&vnwSD8^K^`|(aE+(Ct4btXsbDrE1&>MLc~>3$-Szr z-5oV1Ms@a?Cv^U6PpW}3d*}Uk_22!;ztr8U>zWCfCJSgf>}raBpLyD6iwoiCF^E1G%AzEH>8*{hbjUEi0_A?2TdK96{P3EdN`5=S5pg8J zxHh@R)niM*|5IaRj?9d^oHTP~UqOSXlt|p7xHft!3$l?WiaU`T0PkwGZrssLl-qu&RIygdc0+v>uK$<6f^`H|<(4R8 zo27xv-Jl=U45DWP^r6ZyGp-(66IGI(`l07O+76MKZ#-+KEUZQR?$xC=@e4P~QE zJ&i&3-1E$X|G~B$3mbjkW7FwR+;DW+o<; z@*pKDnbt}X#UF_T1c-EyPYi&%q?SRQR3$+kbupqHS#PNB*1k5`{^;aEQ~r;Ao_s=Q zR!(XjdoK*qJwS_89eqrKZM6SeT?ak7k}QoX@tqjdWMir+1#~ZlzS0rj;N}o1Uy2p7!ss`3*ka0bAgp+%E!mGc=n7E6tc#~ zG)Ph%B6y<<8l*r%Qv-hR{IKMM`v{Lmd79QjB&FY^6un6<#G;iN8)<1~dR)_UlbVs*}HK^&@! z@`+FzVxj=;LO@nQa>bjVloO<12v!TrAs(MApgu~#L+eSNDMz8T@`jp8#@=*-k}$v# zNWw)k63M_PAjr@q_WG6jmg#_d$;_lqonO<^?39{BEt*AN8GL9EDQaN|l$*P{PsHG@ciz$Ut2eZH zV?)E;4s&4YB_?AX%jb#?4T}7P!XsXB;iUOBq*2BQsO0$}X~F%(qYXnTqMTjf9}@b{ zF`%~&540Z#^7j%>q5PLxW13+xw7Y!;=o_RK(j0>T01yC4L_t)gI%$C+Qz@hG6hjn7 z)XILCNd?wMtV+2zpYsf)7*Yj&Y=d{3=fGHV(`Qd4k(CMcehAbS2 zJu|74$XsKvuMzc?RshdSU=7KEwh=mxAmQo=#JtyFs_|AYZ%w6H4~9&X07h>YMzay34{N!B|B( z4uAM7Jf~O#f(nr#@lt@qS0+2(%VL zeCgzZE`I%KEuKHE0bbb-d%pgaui!*75QF~UgF>lQD5Y3cGB04zr7A_z3gulXIHNnfiEcU0@p?-b7Hp*WK0YI~#g)@4jx7UG0r$DrUx1O^>S>MVz-9 z>Z7QOBvn+Rq+TPY1Vb|7h5Ac_gG4GArx>pRL1JtUfi$Q>!HKr8hQgo%Z_y=rv)Pcr zH&?Pfl=kayy{89z2TIx_s+w(e=y&Yoil*jgG|oIvz?Tu)Ht^{4{!pDe_x09aen-V!Ne@ zg;}khT-NH`gr<_A^4^{@w&GF@fCN5REENGc9n6PVhc&PsB8Idu&LSi5=OkLiFe*ih zHL!U64j{78m&}zv`t28wzv8Da=i%$)%SqW+oGLd0{`gP-}UjkQNFex1jnZI!P{sQ^wRlur;~oMps%%E=VIZ zR?hp;mI5L5z_{dtlqoO|;=VY%+$@j-AD=b9j66`Fe1>dC6!h(LCCX}`-Y1^m8N$yF zA?7kkl#h?8IX$U#Y!stF+9c}nfT+MBywpfjfk;n4u(JqMB|!z{1?@|~=ZMR?JMUH; z3{_5_gO@Qalz!~=8=|J)v`QuO6&Py^UgVaj!u!3wv7tBKeOE;c8Xccd3_)qDr8x|= ziScpe^y>ehr;XNYSDEfyyRHB4Kl^9;7q5Ls8%0m!%Ztj|qw=55{od=ZY2(g4R7@h@ zG6~=^+B*q){4g#6r#+H=RK>3o_y(y8&{v|oceb|GJ?N-_54ZLk<$_His8T~_kQ9tMMT8|*g%r5`oZ@>65 z#mDEDlmp>QO4l)24>|DP{^g&(9P_xAHS(TeTIVvSPt=F-8$vNmC7R&T-FM-764{-eRDxf zUZg@82hvnVm@xvSavG;KPE!ntcJadAyL(@6+`g+W3T@%JbDBDLLS+L*!zkMbEbA9) z;B~bLGv`UJxy5B&_}X)7FvfQ`ALv`(`@X*a-8aFHqApn`Gpu7H7L7ppNJ0Kyogf=Eo zMHQ7wAbLH@N`$^EoJUYqQ56cPK>2&>!!~2wwDJ5WftewOgG%tF&6e8CWr`ux2lsBT zrvr?MGELN)99NsDh_xEfVhZqC;EnX>bEL-(zy3k+3UuPh2N20QRHUWM$~vtu08*?Z z-wJ7mT!#l;{l)jbqx)>tj89I;`0KN$KZ21mzc{Zp^xCkLE7e9{y8Zq4^e_JDztms7 z_pbI)#3z5{YkKNeenr3X8^5ll)2Fn9L34nz_U*0`!zU1Jq2rI7PL;!>LL=iqBI8C< zNtsWIf;xo#4|exez^mQPp>~N5&EWN(JB^YapVYXetgKS~M1aYv0IgQ9F$2#aMD;?Y z0p?WBeETz%JsJ<|u-ZJ(U;fo!=`FTl7N5JMrE_aqoSD;vGBr{55q_1@= z0F+MZ7XU^dKL}>5E9mgG=N|mf1Ed8vg`0Aqvx2MPvF3}zWwHUV`St73wFg7OchHxL`78? z1BEITco_lZ-(deUFOh}_tH=XCy*Q_l*=dw)hc=ZGW6DuVAMNH7u4r37-N=y;_^7Ky zdW>~WoCDI#dlSJX198Ui+={G&1o6X-uO0Hgzy#=Cd!>do8t)M<|B#Pd&U z=He+0T0vcviY6t{8wODZ!RuEwac@`C zJ6%nq)GwS_)vx`HU)9;?FUlsH+8Q2er@ODg084?f1q^|RkavY!D5^?K6=SOCysEgS zFLVLCOO&}0kmXW9L~^eHN~Z(<`)>~vC}0PQYa9_gW>)iuYMraR%0EawA0@OY zB!ID$p|vcOL;(6q;ma8PS+~*~ufL`5z5TAT(KcRuQ+=lkBgcP+dJ^xy5i+%^03^D9 z?T-H2|Ia_s>uH$QB}2@3XM!wNg*T(DRe`D9v;yiWj z;An#P(}n=~U&v`@GBa$%C^rJUQ~&Dlh4W|i|D4EC09k-}_sC z_vJx3SgWdXz2X5xHSs7`8X`3*li}Gmc>+C@?RhV2j*V(|Wl{c}<9<05l1_g3qf1qN z8dQpk2n!^JYLM}K*R4cGo~u)ojVqmp1+{Q-1?1r6F&+sVzi%igLx@@gHw6cHeed14 zrMGY0QNPjD(i2Z=?(}KJ5Y#UQN>I#}1Put)hBQ^Qpa;9VTEBBgZ{N6~_4|a3CngB< zKdrT2{RZtXX`i}py!pDWe*blKuHV!MUgZ?t{Rx!KGfzFC^Upt})`>YC=F%QQ?trkT zE6*fP6_NzqQ7K`p$bSXs^z`W36hfj5<)32&q|hdw9z#(zTTP9?XNOk1t-P7b71+@z zBkck@E>p@F#NoVg){_0iGr}W9(I!_TYf6>ru-nzn{(*+jlw=L1C>tfID(9~SMWE)l z=+z{OB{~6(>Vv#re;lHyd{m+SJi4r87P*df8kx7MD!eb_ zrfG-){YDW%N5# zif3+&$rQzCc`OX31y%6Lfh#rWAcukyli(l@6fOoj75)dv3OKrkyKWJK}x{O)Isgf?~D-O{PlHcW~vPv)><>qw02@ym(H*0B*xkJ zXrnd+5?B6=JJC=MuKbUH2pT+rTa3-e2cs0%Eym0L!EgQE%Rj#n9{2otIlzkkyiJbl z|2#Qx<;5%Ov5J?gSYAPDsp3zGDLA-ijpv<3Azp|b{VJs#Uiv6hy| zI~@Vl<=_g$!Smdz&pMB=@V=tg@&X_tOuT~e5fLITOdgd{^wINCeNrJZB0!)*cOGo& zjce;VjM6N7`co%PLZ2yM$mEQmBrPSm$GZ#d*S7BK>b(u!JJ?r7`1{293pxp`ES=Qi z?2;Bwozl`%7Zg@!b+f;#cW%6=yH~F&-QLqwx70k!^xWEMo%`BTnmKzyHqp|4)z|Le zPy;+1A0{$ehVt=u05Y^_3ax?E$7yEpIZ>KrN>GGmDF#8NX0xSsdqg9nV>OI=Ix?y@ z(7;&8^Au%Hn>1o&>DsnGPb2XZ6rD+2UCQ;AK zahpW_M*v&(Xg)pvanJ_`?fhna?U@MPSflY35-|W`Ju_0T)75)#y|23)57eF*moYv! z(A?8*5RI9boWw}T)rL1|ztpX(*Y(EwRYjD0V{u7C41+9bXhgX(Jn2Ie^(OO~PLC^R zPTlwm@Ymm8s99Ch^wE#Vr2zpzzxvXI15nDEbXu)9RLK^_;o%|S{ivl2Yg#$AsxgeS zjK(S6xiP|k5s*3GDpVQUJ`pUHz)T%~mnN_dGOwFZo6eFX(H7fOfA#wJwABt;c>1D7 z;KAI;xY~q|G7QZOn%wIjpgaqRD^pYD;5g`$+dsE6!k>!u;{*f@Er*I{*WXVG(6)^6 zc9E@&v!_Szccq22w2v?K-3gf~OoXqt8)?HnD zZ(W@m8*1+NHIw8zv$UkApLh;k-`5`Y z`nBSjnau16{pb38yp1V34<61l2vy?Y_xe3uC#v%P z``1)KW1~GPS)x86?g5G_aOFioxj8(fE(9+1t_H{Y~|TeNC?} zYVq`{mgeTvX5H1lMhJfYje!h2Df|oYwIW{xZ-2!16wHxpQEu$vk6W(cUPrIJ^Omla z2P)6ZYvRm`=BH*f6*GPXm5>r1j`8x%Xd5*028uYU~YOc`1M^iI{ zEc-}#fW}}x(8$_KH9Iq{3+K)f4LGYaCs#EyF{&o?i|v3&v`j=14dcd$o+PnaPhxTT z@BP+GFN5^>`~~EI&-^bS*~cV12YjE+hQrH2@rqnvC#Hljb3nief$R#*VYJoKNxb8j zCGwDWOdp?ieQ+2#2O*?{urg19x=dF8Q7ke#I$Tl2qilWSVU~nDavCCDe2ow9sgD5K z$FtqQGyC>??`Z>X^6WFuXyxoVrO+tqWR@x&R0=4y0>OKgQ2)EPZpg+*b((PNi3{g7 zv$CQlN+yIvsS@^?Q?=N?%@OdSkt>~@Qk#AMu_w+bJGG>{y#sywjqmH7*WOU~_5)2* z_w?wLo;ho63$UmWN#kLuzo4nvglAjb%37gD1#`7i_H$kGhY zzo|UWm4rY&1N0CKN{%3b#sq`GERiVvOn~8$&{0ZT;brx@9rf8R@qXLQwpz`Wa{5oe zS1UghPe3~cda%y{RDF0xQU{XyG`dAyQRF=KF&KUMOM18O+|v%;b)!9|lx>J!r?11q zu9z-WX;8D()Z)anCM?qfws+oNUzgbLFQ+H9#Tu=^Go6iUN)7ce0v~KXP}0mbK0ToX zL(HR7l`45^`tXQkYoHLyR09RhlZ5$@uXjoV_dA6Cw>R%AYv)=$cM3ysUTt{UAnfEu zRb^2HkifH)^=PCu{j|;CeS@;Vx*sr}L1N07`>8V5_3yr|H#TmoTw74<;t7q+O=^<) zn56L-bKrW?zd~3!5#WMkRVdd16EjF8s(8`Q5lm)B@F5YV5P|~p#Zs$@Y&M$mZwEYi z>7t%_;*!puKB@WXDYYA^5{pCwN{rL$m4E+t{{6K-{Ovz{RmaCKEC*Pczp%6)6AlNy z>Cu4S{QZ~9UjOo-7_65rW_Uf}i^#@pKL>}lwAXd5VZ@VYd4sJysQ zmm?@>@1LO-8iA|8&7;#S$jpSyMZdc^epF7`QC10Qps%7Bs+6hBa@|BYf9s96bhq2r z#b5bV&7EY=KhKn-I6T@=3`->;)7I9u-hA_I?PDaI#6WoZYhTm!;=EepV=@aW2aJ@E zcpyApg1v903XF{eA>XbtjW@DuN!z46`e>gIP|SN32}GkwCt zjkca40`Y5q<2Q8b>z6c~ZRuvYug%cM2&m+P$WaIl!paFDUgs49Rss!qnnPzz>3v>GTPED!y#!>KLdwWsyC$pl94 z>)4^+Inc(vJ33@rXL@l)Cr+$r9OJTCs(u!HbS3N7K}@d?+shu>c1kfMGRhgUx>l;s z+8tG{gLiJ}jX(d64o90BfAXy6&!5!z*eEzFH5k9QtJs1otirf(1X2Q0F6qWpK%;8V zm+$aU2ZpzWj7U$Simea9fHw1d>cpy^eewx?<7?09`DdTi#dBxY7w6|Mzx20Xe&NeE zkYkEIF9*WsZE#%w%gKQ&zyF7?-n)PA^4-n*S2~0~Q-p0mA!aDVsyEQ&`j5Hz$9XxLcu7D>bRtvJ1D=+a%QiZZ|#T_v;^bn7k$@2+mLbZ_4 zkbUr*+uQo?HA4CQ0ekgNYk~dyvXLPiBZ6m$QbTwm#joDKr}sDRsxdJkSNs>wUsNk= z$%au0;9=Va#5D2$GX&KjskE140Fbs(O4Hq->BCeLWlN*YNlh%TXzuAtnz?jF6_F_acq4yCQ*f?&~oy=^;<_IgW}(?g6_(>9H-*>32{fm9l1r*PY4|%qlvpjtG&_ zjd4w)&j(dq|D!~L0fi8fgyNux5{vM(*&NfvDMDaMUX-zwPUIRW9BdZyFb%*ucw(ar?hnPgqB7oHKs&iIH=cdiKZu~0;9ok zB;ZUj9JA7tmO%+TG`jlPpY*~rA?EP2LP>pW7)7E0i5qsLENKktLS<1X&oV8|&*|d% zv#FCY%YkGj^zxu?Jnq51ieQ(!5DHzzQLRJlR{q8N@>g;QLbwy{M zdQvkB3kYdbg$JIY{2PVTDvkYCQ_2=WMHHYv>}g;_RS4mf@#aYHCUMu+AX7ff#HU6k zCN;NqN;4NusdZ{z_XhiV^ZmE=&f9P3!Ofd;5C24aTo-2+^y}v@>1!7+Y88WN*iLn; zI@Gn$)lO0p$QOFUg7xi!eG4hxn^UW?GrD!KrPtqhT{ph}rgU>t ziyi4wGN!N1t?C;XD(9cQr1Z?3HfDnEU$bt-rkQyw#vas?iYfDSNdhGSXk6*Sk{IU(`csxfO_(wzo z{^39P&t87e@2_?G{a0eY)I@tyGm9(gy4QjAglP4%?OEOi~&^8{_U%Y))Z?mUA^Ylf{ zJ#kJ&D^Xl-&g<0bNwxC^>ORPYdyvu1Lx5&8Q>~v+L}|03 zM%GluUrLLb5|oqERtZAr#|?#)t&o&4w6d1Q@Ze@vPH6ebCpG`{6B^D<=*^vb`qmrY z)3?9x8QA^Ve`AOeZg;zJ1J83doG1?X|5mMbbDBGD^)y4%~+)P<9}^lQ&)d1Xm67$_?v zqiS}$YyoT$I^WZ+_pa$XfBLVrzi~rL9!h3SbT&4%vA?VK>1CZfc~TxRFt)2Anz_J> zb|a3dQpEtBA_uaJ*K?9VoGb6(wD|am0v&+!#`T-JhSAY!HkHlIshS*9CrdB}#oCpc zEC>H?zqc!`RKa`=lS+N&ujJa_l^N|v&H5G+R>0o%4gJOUzo+|6srA&F#!jzlZfp`` zCgAx$qJ_MfK*j2_dTzww^cB$OsT&v$ZrB{nl(+q?V0C6#000mGNkl}muB+w>4H`>s0}1Aq4BRoy)3>C`vAuF(_A>Sj?TLf5QRD+wxy z4&1|Yt3s~%lczK@x1cupWGIBTDRCn4%!Fhr5nyFNTw$bC4&JW=AxlMwLOHq|GwKPU zR%xh81>h~C%#DD7sad4-(bmZLw5Cs=)*ME_$fY$MOto~ayQ5eC^}o^^-~Enuu3l5T zy`h=IzJ7gqRsYtn|B6mOcL7C!a+yfA7dtwrda6)bF*9MUsIn-?kf6_bqWUmC9|75q zoRO=?0G9YaVJ~)qX=?I@I zRa}0Ls08RqCFUW@pebdmra@WBrv8rlI`b>fY6>sDs0LaZ8`FuAG4=7nx2|1>&rz$C zZQr`Cx4!eII=K3ts#_cC+`ccvaGPd7eQ|P5qoLu`t11n6(^3I3ZUjW;oUUd5QIAY= zo>q#gpn>HIo4F#gw7YRncON{^eQ4L%lp0g>s#+uJfYaaRPc76D2*X$~#t|~G2Jo;> ze4SUM8DpO8Nu&3=+I#JqzWeR(>1t<3`NdP3xNutIQ&U8g4CP;`NTLebM@LgJ^~_7g zyihDZoc<*E9Efl~M05w(VlK>i+4 zhy37_3%N%?6(P@#fLexfF8ob=(vW88s{+How0W?vKYiy-UGME{^@$63dZ*MtNjFgJ zjZ(@$xVyin+xPFOi`HH`b&}}Bf~;2N5^`0abW{jw2d7IY2>-ztR|cO{_feVGtxyaR z5Et-4{(@Fx7&R7iElGK!rx1pc9^n@naQt89T-f#fgsEBcdM$!-k<9!F{tNZke8t9ywb(RlqGXw*;;Pko? zUPy>xJmNl{v`K0-8p_};*`NFA^f=v}Zby^j<9hDGlj`gpXmfj8%gZa8Bm!`E{kqoI z*Og_d&P^|AwK=Y>>(}+>pZuxb{`0@kI)*?lqoO7S~ZpS(6E5N73)5*9nqkV zb`dl-3~I(qhr1o!ym3e0fAcN(T?`AN;6J*@?vnK6urfB__xP{4dc&o9|| zWpVjyg!`X=<(F(+$HVy)4unrBKQ8_y&qi zfm<1sR6;qbP7OumXNjiXfXW}iB% z=~Juf66S9s+Xm_HaJKcS4-`iBVv7==BP?I6k z>eQ?*oII_kE?v^{;*ts!ivP$`zhGeW;d|2++bjWvVKr6W<0GgG>JKx6raJqP5BKVa z!=ZYF|3i|f)oRrS+K=MLN09u47oXmjX~16nVW+Q)=Pzj_-&_n=kupO~i>&P0r70vN@{uK*|SEsZ4zocbS?J4330Gfibvo zFp8_5?hUjQ!yw8LvKbiE#Of26>`1%Vl`n zu5`=pKo1^ls!9_roI0V|)g>7;lqdtIL7#F2lzDVsF=B?N((P&l@^- z3M1gjC$zY<2|g9fO_laX?h2C<HL>7D-Dep(+Ln+YAvV_PKCIgWb-%=S#8WLMn zhnu^)fAyxWzV*It+_{E{Ec_^O@$Y<$(md^}!B-~QuQ*Vn)M`pcXB{maVJDVWya?xf)oPvz~NqYiRW!*b(uclZ~^U} zv@6mIFVm}-nZ((*N1y z)!p7zLX;zu2w&$~TAbIVCogJxZUzNY)?n;AD^Ni&gmKDKzx)V17U}T&D1S4Pnc)zZ z8el*<{jU5|_~UtRZhU+aQ2b+^Q25F1KB*JR+(_&8`WSSX4mure>}_d!c|mJyr!>N# z-~Ws6=mADS6PoihRff_`dqepz<;}p?HshOFnAQmlzqT7U@F$fiBthn2q+G)eK6L~T zSzmvUdPZW}@cyF`@HEFp)SMhwb81o};}go0T)rotflpLVLc-oXbunI2=6HC})5hI< zdi(vWdJE70{?@KK+`F^4ud6%T%9a+ibn&7lre`&hHZ>Y@HCRI)qY~Q)^sZm<<4Exz zd;P!Q)A*B-AAYjt$E|GUzP|M@?ExvY}4qNuL8piQFH`=%z7A6yEjSDyNU z2?$j8&O8;aZ`x?q-z@mE?|)z0gu$QwYrmn%<4&Ki1n^702 zP}2Sz?ME+YMP-bmT_qZ2KX`1AXxwt8G117>lop@7py9%VUcY%&*WPiI~e{iwV_-+=9j660&Hd#L8(l+HeLQKy-2+uqmh*WOeg4|)`(*+?=uKMcj` z5tEF4_hGlI=I98~gE{uygUbFuMUAhLQxI5li2|P{Uk?s}I|lfAtMgnKMqq@8L!u=8 zVo<+(BKY4N8Br6D-M1eS=D>kPQuouwC>RS(4fndbdF`g&UcaXI@88qLu&eu>L+wD@ zgC5%ybhLEwyiS}rrLnZBF=c8(Q#l;?rxJshU)<9bzv1=2{EIt;W6FP89PsDqr-kbn z&o2xIevr^tzxC?+KmGGRdD)U2x7}V5o|`M~h$t3H2j?6)S}M5ot4bxomk!=4X$JxR z+WJ+M?Y7pQeO3_x-?N|$CE8@qzhS975^!zjfgV(SO`KlQ#PWjtHvkf1@veORYl_C6 zyt$R3Pd;3Un;CROJmi6TY&Y~##;zPoi~?`vO4CzU`n`Tn?m;&Q*L!)y+jJa$-^b)* zBZ#4bF_b72$&^!&9Sk&H1dU(-Bq*_o6DwML{wY{^h zyVvggsr#TW(CNb?l6@ScBngbA`XDV)suAVi?R6PjUv2o=Y&KOgHpdSF)wn(L1%UTu zW_3?a!w}+(h7f!CKnKx^tnr5j2d@y>6thtJ{<^4MQDEfefe*XgiufJpAb&R>I$aLUY&Roq- zPHU9@dl-H_==E=^csqaTvsp+zLjE_`uj|I0yV}^_*X_Zf?pmQu{?!Xf8rKkSuSR%Mp_JT{i>6wT1(#@eUj?4s0D=7g@2Q_A zY7qu+&d#Wx=ISAB{5Qo@_SgNrf`fyO4iLzVsR@lzM`?z`7x40e5)^Gp{X~h3KYhyK z%G`ykd;hK^0>aP-R+fIxJ?F+l#qR?9BPbbET1pKR0{UBJkoqX90Z=fu z97U2*m=~t33JQdnM`ourxj3sGwlD5IxTgwbA^1Xvig=-sv7ydobp#`-fU$6KKkdXo_;E-A)MO8=`U7|fa=w2hy#L~RRM@MCY z5~mkshQ~sP0;z6Zwt9$Dyc|< zDlx7aDEP%V*CG1y((?1iU+eqDn6EFCN?*-iXott0{roxbSt$79Z+&b1|M#!|)n!wB z0iCw)LY6#o)f8N~N6EdbKGMa1KDCdRcfe}8$G-5bt?gRC4%k!8EYXlX;O)&VHPT$O zGjjw3GPwkg;4w3Whkpr$$bChW;17d<(00$b)oRI)nl|uwdZav~%uEShX@X%O4l}d` zspCZ}C`b9XfD9pD1;c2OI+>{plxd2$S}9-vq?IWpDlj!VspZvG^~!;6-n*>jxEFwxfJ!h*({Bg&PiG6RGWcmzxb*HNG(m3RLE)I)d1 z0I;vV8AN-Ozth&tGmSPHN)i(j!5j`HjE<3zXovxN=k{G)y>kmizoV`0K;6nzr41D! z)ehS^5Bi6iTAkPGsTIwPG}NSR3u7#0ES01JT1yMi6S$%eKEK50I)uJ}$AA2S-+hXyod|s4a|u zoA>VP-J3V`=GFIg=3 zkZWOnUinyC8{7A_-`j`gN>rCow_qy*$5`-)R#A|k$R7o&JLL1|jFO2X>JiOc)F-Ud zkWjO4Yh-w{DTas~QGNI5!>n$6bnz2@)|i}}PXG_GoA{%@1O-|Zr3Q>KG=e4;CbfG0 zgq}QqMr$!w`R#Xf?Z5q(I=p@hqp4sFeRO{zGgF3=PvBvP_V)$D7iToFxS&?r#IOoP zUrfFR5*Ac|4$fNnmnN}96iI7%I93xim^%}HWJT(~L*d~3YPA{|PMJi6CS_jyUjSFT z9o@ZtTkE%O>Bio+Ho4bH6Po5~s-bjX+S=KbMjKi>v#OcdakU{PDS9#+LPx2{b;V>C zmZ((Fm-zVb&HmNrkH63VC7!4+18HCCmHcI3&@UeipMONJzWS~8fBq+b`m(9K9L%mD z*ViK-WeBPosa7wX)7n!{XyyDlEv}u`>e?C2udGTVExm`g{Pz3rX)I~z+~lHWV?!eY zDJxVfZx0c(%_`*o5n(2Tu6-i7A{U1%dJ(|T!-K2(+RP}i z5Bl^g3M4YoAqf(=EF=p2C1@g#1dOsLKRG|6-R^-N>}<)sbwg`cDD9%xS3={!wV|Y9 z4U+1<^xUW$KPd|U9V;$X(rsmM&{GLad^|d9r7xMmWoYBG_Vi0U^$de4 zL9rL*P(~zWG^9!ndb;!W`?_=ehBo$gwP~denu!KP3xsjfK=~(oeI4xWYjSB$bE^xQ zVvG&yC26T5DKtzfIZcWfES8WDAEilOiUY-OSC&@3e!S2BrI?Oi3?jlW2ARiHei=CM z>bJhV{_3~>^yMteE~D#Sh-T}?{`lPTvgTG+l#h-`2;lMQDXpG7snv_;w0P;fTI1u| zzQ3jJolQ0O2Ws_7A_7Qq!ovQuuR}W$uu)e3ecD)q1qB2@r`&sXFf#=djX2C`50L$# zXEcc|egr!yd6(v{fHI`Ke|0ROLSg{}A*w);pl47hQGv+A#Qd}dgzgUx_mu?bp(lSK zupopS91(53e)Mv#JtfOY&H!>Bov4_4t7|LcRwSuA89-C0bzI-FeRC+&*DZn>}=sbf@T*^V8FkGY1!TA%329!h> zBHn(Qr^04atcLY(R1EhHw0+}_?gN`_LG80`bC@OCPfUI0E>)^#Un=hJE5jIA@|QiA z7leI4B%`mGl~3fARY3x_;xP zgz)#>`v%H2WzRTOR7F^}F#JY~kmx{CpN6OfB7ngV@WvAbgD}%QlnJI$TtfZn3ONPqk+ge^*)$+)cw*KM` zUH#+#zb1DMb$)V6Gp)9UgF=0#qR1L5(Zr6^Fl;uffv)A1rp}zwsS~SeCsI}Q6q<$# zE+C`SAfXIm5+)|VpAf}(D)<*wFrdn;rkzsi2Nf37gUH7Uz6HtZoa1~tY4sZ-DY6aiE}>^xTdKa4xa_dXK`c(guKz2i#99QXkaeEZvP zuD`c_>y`ig|L*^CIdrNQvLd|NvRo-1UqG3pc~cr0(+JxE%g;Wg-dIE5yYZgheE)5A zc6Zdma~mxyHOo=~0bV&ESiK_`y0KPNEKx$Q0Z4#=azGZNFrzAjvnwfzMO6h9fP&ArkVM7TK36tq_9Uv=UJVXZI27up-PQMbUPB02g zJ@5qhSM`SQZHO_EXdDA{e7{it?v`%d+tB*(Q17DTcSlAw2yM2W8fsUW8Ypp9U2P7w zbVnV{tS;)*^t{FfL1Qsj?%1Nyo5)24CNzbfChc<8X<$7>@->hbeDgdDQ=bmv-e(7+ z>L;A*iuS^BsQ(inuQ9}Hvyd_j0T-(Nl|xtT6#~~Gq8y@* z&|%GEY=u#8iwofl$MU8?d#}Eqa<+nbXrxKcSV$ zX>GrCRl9G#ud`#*Ix#V=CUZT+a2X5+O6aj|nR3SN{q#bm9St?Nc0#Alp3%(oq;mQa z%G97Tp)a3zIbq_UUkKL+@IfYikn>R~T3yG-Q5FS5i%JvbhBddhv!}bPt-D)W+F?s& zm?Rp|&yevX@V3H>H~J5(x$T{8#XQmC%CaUW#?{O+Rp5h#DE$Cg4n_)XdPtlnT-whV z>h9>{;rXHBKYqFIAxn;vpOgdPCq;1F{+I)gbKvj3_y@22(eM23V*(Vu>Lo-;Ph+QiIOy> z^aJVWTuL|t20@xN6w+KJv{oVDqe~seG)Sn=TxBS5V=O)P&0{Oqd6fT!6Q^Ylw$*v( zea$nz^Ye>DZ=^7+>RA+)submY*H z&`%O2r%xOa+Ci-vWR#fPknkMU>T+=y3cYTJ=Up2Bl=;oW-HvYDy{BsrHnr93ifF#l zfMEcxl4wH_SW7IP5KJZf-rd>J?EH)tSC^DEb5$4zC5By4Py!Kd1c|;P;B$SnBE7RO z3gRmYwzlLi_Z=(#FN*13nSR1oW~9dh)fdTvE5G^T`jx-=w_g~o;@U_|t|XP#6Fjle z*=e1)cupsuxTpu+1AXhS{!;5#*Od%QwMwylP$*GUKp6#61CZT9r>H^`xIRTLQxud^ zn6c-Zkje}7;~y4}+S*Yf`8D9z4uSF`Z?%%F6gow6B?6skmT7KoPFbT-|2}?%I%BNn z%1z$V^ht+#F^Z+Y1n*@x6VGz?Ly0dLtAWETZKzBHptPd}+?jE2tJ>l%Hqqi1Iq z*)JcFVYnoWqucN6u-8$uk*R@aouYZYkNYUyWMN8YpSYlz`B?=jn;Bd&MfUDl7bseN zpyVN2u4wD66O&)oV<1Z&^MHw}s6lIG7HDho=t;>q2EBncH#T)+)F(U$6v=yrW;d!z3Lj zc4cw(>$bG=jTe@do?rjjc6W^ZtI7eE^H+7~$K(DYIPl_&FRuSPf9r3(+)k3qd1>_v zgBglsW_4Moo_<2Zkz8+HyQ+8IeMc3_WF*Ozp@4FXFk0$V37~{F*BI~ zGKn-gpx`~c-=)8Ua)`HIs@F)>M_~^jNzjwq|!=7!Vg0t8@qeE68+3#m^9lBb|u(C000mGNklSmBn*sY9m17Exoz^zTSN29rX?lFaW55 zf=E#iZVV&{ABH2{M)Ls5*j+d6gTw8rPAbr=g2Y;hEl2M78`5quFcYfwbaM@5vetBoQ1 z>>dTk^IUnBbC*V+=Zr#u3U62yk5pSh&55HxuAhGzDAIzKj{85Dnt2Om(HTb)B~ z_PdH~6(~>fvKf17dQkQ?oEew*Gch%;EQJWh95F7OZ<#AvXbkij7{eppOI(P<#9jNq zUV>xH0{5us^|MN~Q8px$-+_ladwbg2+SUR1dyLs%&Md(YptWLKsrK-bqxLSI#_nX?=8MUhiz& z)VE)MO^4gNN=qqaZ@Gao%klR7-2m4FE?`~p57Ew#i~!#Os0Hfh;4Lz_ZW6(bCPsP- z^sz2WxS6n@kHg1xg))dJ)F|O)9^Bum)5+1%`h~H8@=x4Q!9(=-04_ZHgvJ)8wTJQ> zgh~-bNK14|6oOvfQA7z+oo-PnX6m3o18K9_qE4uR57nR_eL$}0N~tUHN=jw?1$SUo zfOp7PI`pZLwwBJH)04mPYkKD2_-lIRum6Ug1C~#oR4d8V#sJ7Ybk8{V2OTx0Ms()M zb2_oOpy8eSYU2H`r7bNsT1q_7hSX^Dz%+!P$;PwCRnA6U`{#UuNM;q)r z6uzmVe{c6NAqy>f>@d~+L@GGwvBT;+_E<@sT{jWtU-`PZx4(28=C7G@{PD$dAbhdi z9ryP0;=q@&-*5irOY8sHZ@={N#Kh#~rAudC7(cu6%C)TxedoL1)7HIB_V`T+Mu6dI z=0+ajD#USx2N+qo6;N_rx|G(BbJ&NCq7c&nX@Yhhu&3=Wm!-iJt5N|~(mU+v*7chj zK#$>7_If=ON1`01>qbfw4}NiVSx-Fwj2aVd9iT*qt{7!91CNeUGgFDe;!?d{NB#m} zo@Pf81?~lUGs$o3vmS1Uq%fp`LWVsWLUV~FGXxIdk>TZ^xO84m{razJ?Wrd?!B|FlVz%td0Q!={e&oizg)Lp z7P{B(shb6rc>jGo>~1^Jt?E$4+_+AE?HSE2E@&i6mC`2oxB*v^k+DWb3H#u#^XKEE zg$vOz%mhb7ohXJ#TFIDmHwq)P4e;jsjJJoEKV@F$eU`7hhWcuU`5Gulzs%H~*&><}a^+ zDeeDRE=3V|KH@ni!Bn7VZr-}3jR%_=pnTrHaZ@*L+`>o+@<>JK7tm?S^GwSpmv!dC zn(}r-1NPf31bL{ug8quOHOXiS$Gb<^x+sRU%L$QZQbn)ysn2mZ?MadiO3w znY$qd2!RYm`m-ugaOKY|7mO=s+yNs5>quKWduq(iY5@aaqS;hVc}Aas`SF)A=|Yiq z5x!LBo%0bziPeffNaQ~=tW&>!sjv3-kKg9k@$u#00PFJ0LC7%`KT8h$;^6+r|L~Rd zZ@u~_ul$pL^6KU9zV+R;JGbv#X@>OG{{ErRD+v8c?nMR^YlCaZr;4E*WP|h`OK77@T7N8l5f8Eu5Mntt|n@# z6;TWr301F)V(sb7sgqhmfkq6LKINrrT%%VytE{3EslsFRxBm@gl{XsleqH%gURPAs zXFKYVyTXU&zAB|C{Mc%!LtiE7(z!LAf97e;EG-f)uP|nYYOqnzE)sI>;K7r`Cq>Lzj*O?FK0^E(yDx=VL=Tvr7M%*KRkoym*TYrnaq9= zeq=|FggL2E*})Z55`xks(f;19-hS&Xz5m{ON(dRB!INHj;-VJTPN_G3^4HgsYGd3hr{}2){klE1UARZe`|NAqy>nadz4MMXHy>zvc1GteT_S`&uY~>dxz$xIq2PtW zfA^iYb?x2vRCT(_2qib@(0?y}acW8zE}YZU%#;R%`TGF3=r+nd{loo(I&C%@sNz6g zpi5-nq{b1kk9`{X$AL>uC^tiOM->SvimMZc8lle%&pxG#&pxfGg*hdJzblknP>_f3 zbF^FDD^zXmtFyJE0ZO||yRH?Qn48j5PhL{q9jLl>PjmgK$x)TLhX=pHx8MpbzK-NTL!4*V^LsE9%45r1zPXywXU9Ntgxdqu&PfYikRXpN3) zdT~Jwj5?#drjNRj`3vwLR>`+1yf4QGdE6C-51@}6>%!A_sFnVSZ@j#8;#Xh&NR8u* zV-9>tIS{_2bRCm*%z;njz)LT_wEhRb^?R?p^f&*`3$dB5Rav-P#p)Fl+Ik}0RLM4e)BKV5)-T9&+T`l~9T_&(JJMRVt~5$}B3Ujw^!R;el?fuj}1+-_^D2*VISp zEuhd&pF5-Z=@~Ua=HdP^l=kexjF!)?DjUtUe)VnLeDe+UuHRPkpr?FLsDZaXe`1j> zgENZM+3k1gJ@NtGzbl$P9`s1o(rDU{8#9m~^s@p=To%L-h#o|FsGR{wg_Ko^avvvj zgh(A4l++!VX^;dA0cl}zMc?@Puj;8Mp3(fwf&%p$c)YIs4WvH)qwx;3eebpg+nYo% zy6OxMb!dH^y|AXI7nYS=+fZ|3SCjM;pgFZlzE5r#4?7TZJNC5g-#ctI)rrz=A~FY4 z6FT|Ve@$x`A5#-;wKPyt_9T;B>GO$Fk0{azT>_$#NLm3@2#mwbC>Q0GicqPaNEKxX zV|K5e?nPEc^k;8-tN!t&@*y8$Olcc{NS6it9%zsjUMT@_uY4N>o32p-M8OYgMDTr6p!xJ%!zrOK&fgZ)Bdol!lZ-4t|GWb z^OhRyol}HPi73fJCH<6mq85Tek^m^RkSGn%PaC8E1TC0&+<@{@22MlS1vSRTbnz0N z|M`oW8ktl>xdIxi!XSz4gA4Ulm3;|se^_+2aqpI@?xC7N7&|?s(D*do|8%$1*v3BI zzrYc}UnmKN0TAF}sym%ST^;PHH`>qvyt$8o@L;s5iLXDSOMm0vtl#`^VYM{Xm)W3J zZR(RrXasps!S^NQ>nDTI0D-UL9VYW$1xmgZ+lz&-Hry} zD9eP383sq9z;(1jgJh^)I;6a03nQw85BqVT=G>U(Mq6rQ5CwXxFmftV!E-kNoQeQN z9z}2ih3oZbx>7|C?|-d+gMaD7^T(dPj*nkt4uoH1;*V)R=D_0|c&PnYNEX>JXcDQ!B!}xVTB@N9Ap*f-%KjG*Qac5WksjYol|RMOz1X=GB+IjXMIzi z_LM#n+GphVQJ!~h-Io92U*9SnR(&n6EbIL0N%ikMP^&l8M4Br@^94Fj=m^^b+Ed-% z+0*vkq4LRb-HBb@Yz#H_>{&hkKm51!^tlU~x28tXyNQ(&1_I>$*rz>gX$;t)qfRL+ zFy!|3_T(@i`runP6mwFEPX;eYr&W~Gt{5?L1{e@*Y0TjLH*@8T$#K@zKmHNLp1xg) zpB!Y|E8jtBCI9QdjhdUW6q|JLul{O@55jD>7%e021M zX0!PU`|z*c-FcvEc=_*KTh|-!zo**|?#ocp^Cy;d^6Z+X=I1ryFNdX>3bbf}Hy%*_ zAv2XI?vky95KI}&-Mn*0`}gjvi3iyRW||}N@c*^%zoE^m*NFrS)nu=FDsSlQ>WZc( z#x!KVy)`+ai1IJ;M8ibFp_fobeBkLPN>ThJ{d(Wn1l*_ZgvT{cWmy6pMl`#W|5iZ= zL6L|@#XkSsiB;v}qZ+u85={Q*`WMns)cBB6J+&DTTy~1*2YGmpsoH;J485245@9j=WQD*JGY`||Hpqr=L!9fz_%8b zLc5S+AG3z``kV(O$ibV@1^o*+DoMBl%Ty2nIA9zHhld&ty)y=)M+b%!?DCR8v&4=r zxFCPR>T?F*=|A)G((#LYHFJ(Xe)%}SQ}N3;sN)eG zbKr3f`2QpLz2EwsS6=#^7hm{~{?5O5dHu%Cwfmb}FT_~AvaqnIv**rh^~4E{jgHAs z6o!ZFK=fG+OO*?CgVG!J`if+um1>}H6Y6bTyQz3c=$!lg?Oj!b_Qz?H5p}rt<~!Pa z>s__EKYiU(Wx>Q4RADHfW;HrHqj`)BYvd~M{Bxoi2HtAe7hMq=FO_1@_;=p-Fj{V7094V` zCp550G-P&VQBRybtLpxintL6M4=c5bN(qV<5}_){|8Ky?K}WX-eLZLdy=i{^FqnRqe=#?Refzm9>HMWrpRjdtw!%l}l zOP_6%ikjSb?5w;Z*MUsRU0Q3m$GC{<%*{@-B^1`Js$K~euRQYd7gkXCc=|LuK94!@ zOV5GuOFzP|^Z<_s|4AJ9$N%V+^?&?dz4FTc{XhRdyzqbg=KtDeN5|J%N?vGKcy%mm zs*Q%uODXgUC7nWfuTq;0>{OLFg%Lf*fxn69)ZY%)ms|+97m*XH2;toHvAyZ={yP;F}VEd9UN-+ zU|$`)`pOyL_Zgqp7kqOZTq^MlhD;I5^_|0mSEpy^u1t)NUH+BXi)&{WF1)a`gwj9u z^5Nd``BmdU{j7Y|27NrDayYEk@*yC5p;6f@*|2;y z=@&`~hi51fLy4w|Y1r#&j@mRWVc}1U0CT5QaClA|g?YdpB?C`g`xme}t%FtKpy+Xl!~? zXHT6}?&G{?EmQB(AWR^Z)kOFS_^7m zD2XV+K)Y2>{_FytB?_f{eeqWUv1&*JJ zMjhC%%FILhuVgV?sfzOQOMmC@t^HsBxBvC!|Ma*1@a6yM+Fv__uB^X=LGbVX-77ErdoTY-m($AD%2-_<4*M^}qI|^&xp(nC zx9)E$@K-sY=pxEgq}T^|y%NTQT4=2vp7xEm-q8SKphexKDMIy=lM)I!ps4*_g4}{! z(Pl&+0)~NQsUpgJph|{EePZpje*NG4H+1gOMU{jIjIe!)haXVP{+k7(qazw0pHQA< zg!!Y|X{Mwolwc@?!9c=bNuYasV?#II#~|6>K~0*v;2^l6b7$6cZgE-V#ske((j?=^ zp)JL;&M z0E5N~JP`xpvqB{)($3(Yhw!G%KeSmDFBbAesP>X=D;xrevkt%ee;{^ zSHAh5zWNXT!GH0}H(&lQUikZe^q<@A?%vw|-u?@_dk3!!WA*AF3G3Uvt`6B6aD`pE z5<~$H@lN;gRIk4GzTSQNZ4Em;jWt@DADdKO1*Mhbd*rU@OeQ}Hlzs<1|2=!x^4mrc z#(y)rYddjLlZSDfI7wGY`bcQfD6J6OR^or)gv3hRfkTl%0@+dzoDf`q1Bc~^grIN) zRa%LMLP;x^q^;XD$);)IQko*az24nfzHud_O3i0dy<2C}s z`}=Ts?gcEKSi*367y%JFF*gxm2(Bm;G!XPL!~o^q(|!Gvw(z?95H!-=L*vDX9OEG% zH|8~o^f%WxP~I(rh!RR7hKAQd@<0;Pxg1h{h(x7M?*f|FfyP1Y1biAJRAZ=+rW<|> z_4ENuoS4Var3H*-N05pqx`s|y7p+?!f+W)pv^;c(t!3&$uIo{Rrc5X>>cZ|t0H}g4 z$Ql~k5uR%|s+D}TUNe3x@3XUC{h)vR)(6JvH%}GvZ=Z_D9#M%9csK&|h(Ekh)FMJ4 zLg0P`EbEN(YjMLmf8(}!c4Nmlv%aAR{fb^=uF--p1C?27OtW1foZHn3w##L#lM!%h zuME}+(97e93q}S2gYA2A1G4%KJbV{!ocK0Aw7Olwxedz6(JBI@FWo6Phya0j0k}Uf&WLK*ls%|lI5LU>-(RV z3!hkDn&19-*0{L2sjpnULXIud$=fwLEHqsnnoS;B2S(D?)a)3a;vAMd2wdHfTR=kLvtJkqjo_~4gCfbcAnCwSW zZFxQ#l+Pd0bC}jAfl3YGt$hrJ480-1R0erikx)4VMI`p?4Y--X{ z$0W@`Bf|IKp+(kAN9HsrTA-4Vq6y|GBs}CcBM%-@LUaip9C~(ak&)=x(M5yaO_D-3 zw`gU@7bBu4u%atM;AtbkpSFT%{0M;vfd?b7Tq-&%Yejo?wP=-!MQiuMMf1Wp-Ie>} z?fG`i@7Rg-0cZf1|ByXCGh+=84O%fp$@3t{bDDz!m_mW!lZ4#UGr1*}k>ozQppRwe z^-=QTyW~E$_%Fd{qo3V9_)8JpjSzSS2>b&80RR6oBu5$m000I_L_t&o03}$ Date: Sun, 25 Jan 2026 09:13:27 +0800 Subject: [PATCH 09/13] fix: bug for nav counter for pomodoro --- lib/features/tasks/tasks_di.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/tasks/tasks_di.dart b/lib/features/tasks/tasks_di.dart index eae21ec..c52cfe6 100644 --- a/lib/features/tasks/tasks_di.dart +++ b/lib/features/tasks/tasks_di.dart @@ -80,8 +80,8 @@ void setupTasksDependencies() { return PomodoroSessionRepositoryImpl(dataSource); }); - // Pomodoro controllers - locator.registerFactory(() { + // Pomodoro controllers - use singleton so nav indicator and timer share state + locator.registerLazySingleton(() { final repo = locator.get(); final supabaseService = locator.get(); return PomodoroSessionController(repo, supabaseService.userId!); From a5520a972630d96f5a5cd0144a115dfe71b31d8e Mon Sep 17 00:00:00 2001 From: Khesir Date: Sun, 25 Jan 2026 09:38:46 +0800 Subject: [PATCH 10/13] feat: added build workflow and update v0.7.4 --- .github/workflows/.gitkeep | 0 .github/workflows/build.yml | 44 +++++++++++++++ .github/workflows/release.yml | 102 ++++++++++++++++++++++++++++++++++ pubspec.yaml | 2 +- 4 files changed, 147 insertions(+), 1 deletion(-) delete mode 100644 .github/workflows/.gitkeep create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/.gitkeep b/.github/workflows/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..3acb742 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,44 @@ +name: Build + +on: + push: + branches: [main, khesir/dev] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-android: + name: Build Android + runs-on: ubuntu-latest + environment: production + steps: + - uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.29.3' + channel: 'stable' + cache: true + + - name: Get dependencies + run: flutter pub get + + - name: Build APK + run: | + flutter build apk --release \ + --dart-define="PROD=true" \ + --dart-define="SUPABASE_URL=${{ vars.SUPABASE_URL }}" \ + --dart-define="SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}" \ + --dart-define="GOOGLE_WEB_CLIENT_ID=${{ vars.GOOGLE_WEB_CLIENT_ID }}" + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..de59406 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,102 @@ +name: Release + +on: + push: + tags: + - 'v*' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-android: + name: Build Android + runs-on: ubuntu-latest + environment: production + steps: + - uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.29.3' + channel: 'stable' + cache: true + + - name: Get dependencies + run: flutter pub get + + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Build APK + run: | + flutter build apk --release \ + --dart-define="PROD=true" \ + --dart-define="SUPABASE_URL=${{ vars.SUPABASE_URL }}" \ + --dart-define="SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}" \ + --dart-define="GOOGLE_WEB_CLIENT_ID=${{ vars.GOOGLE_WEB_CLIENT_ID }}" + + - name: Build App Bundle + run: | + flutter build appbundle --release \ + --dart-define="PROD=true" \ + --dart-define="SUPABASE_URL=${{ vars.SUPABASE_URL }}" \ + --dart-define="SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}" \ + --dart-define="GOOGLE_WEB_CLIENT_ID=${{ vars.GOOGLE_WEB_CLIENT_ID }}" + + - name: Rename artifacts + run: | + mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/KeepTrack-${{ steps.version.outputs.VERSION }}.apk + mv build/app/outputs/bundle/release/app-release.aab build/app/outputs/bundle/release/KeepTrack-${{ steps.version.outputs.VERSION }}.aab + + - name: Upload APK artifact + uses: actions/upload-artifact@v4 + with: + name: android-apk + path: build/app/outputs/flutter-apk/KeepTrack-${{ steps.version.outputs.VERSION }}.apk + + - name: Upload AAB artifact + uses: actions/upload-artifact@v4 + with: + name: android-aab + path: build/app/outputs/bundle/release/KeepTrack-${{ steps.version.outputs.VERSION }}.aab + + create-release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: [build-android] + permissions: + contents: write + steps: + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Display downloaded files + run: ls -R artifacts + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + name: Keep Track v${{ steps.version.outputs.VERSION }} + draft: true + generate_release_notes: true + files: | + artifacts/android-apk/*.apk + artifacts/android-aab/*.aab + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/pubspec.yaml b/pubspec.yaml index bfac747..0442992 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.7.3-alpha.4+38 +version: 0.7.4+40 environment: sdk: ^3.9.2 From bb30948dcab056afdc9ac9df1293ae773da8c752 Mon Sep 17 00:00:00 2001 From: Khesir Date: Sun, 25 Jan 2026 09:44:21 +0800 Subject: [PATCH 11/13] fix: update local version for workflows --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3acb742..628ac9f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.29.3' + flutter-version: '3.35.4' channel: 'stable' cache: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de59406..9412dc2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.29.3' + flutter-version: '3.35.4' channel: 'stable' cache: true From 52c88bec2032eb21e4f3aa45e324fd8bb3001ba9 Mon Sep 17 00:00:00 2001 From: Khesir Date: Sun, 25 Jan 2026 09:56:45 +0800 Subject: [PATCH 12/13] fix: remove uncessary assets and security risk issue on workflow --- .github/workflows/build.yml | 4 ++-- .github/workflows/release.yml | 8 ++++---- pubspec.yaml | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 628ac9f..04552d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: run: | flutter build apk --release \ --dart-define="PROD=true" \ - --dart-define="SUPABASE_URL=${{ vars.SUPABASE_URL }}" \ + --dart-define="SUPABASE_URL=${{ secrets.SUPABASE_URL }}" \ --dart-define="SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}" \ - --dart-define="GOOGLE_WEB_CLIENT_ID=${{ vars.GOOGLE_WEB_CLIENT_ID }}" + --dart-define="GOOGLE_WEB_CLIENT_ID=${{ secrets.GOOGLE_WEB_CLIENT_ID }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9412dc2..8f9027f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,17 +41,17 @@ jobs: run: | flutter build apk --release \ --dart-define="PROD=true" \ - --dart-define="SUPABASE_URL=${{ vars.SUPABASE_URL }}" \ + --dart-define="SUPABASE_URL=${{ secrets.SUPABASE_URL }}" \ --dart-define="SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}" \ - --dart-define="GOOGLE_WEB_CLIENT_ID=${{ vars.GOOGLE_WEB_CLIENT_ID }}" + --dart-define="GOOGLE_WEB_CLIENT_ID=${{ secrets.GOOGLE_WEB_CLIENT_ID }}" - name: Build App Bundle run: | flutter build appbundle --release \ --dart-define="PROD=true" \ - --dart-define="SUPABASE_URL=${{ vars.SUPABASE_URL }}" \ + --dart-define="SUPABASE_URL=${{ secrets.SUPABASE_URL }}" \ --dart-define="SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}" \ - --dart-define="GOOGLE_WEB_CLIENT_ID=${{ vars.GOOGLE_WEB_CLIENT_ID }}" + --dart-define="GOOGLE_WEB_CLIENT_ID=${{ secrets.GOOGLE_WEB_CLIENT_ID }}" - name: Rename artifacts run: | diff --git a/pubspec.yaml b/pubspec.yaml index 0442992..6155bc0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -78,7 +78,6 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - .env - assets/icon/app_icon.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images From aac0c17e46079dc2a84171cfab6cd4c9b79936b5 Mon Sep 17 00:00:00 2001 From: Khesir Date: Sun, 25 Jan 2026 11:12:49 +0800 Subject: [PATCH 13/13] chore: fixed windows inno script to include new plugins and landing site app icon public img --- installers/desktop_inno_script.iss | 15 ++++++++------- landing/{ => public}/app_icon.png | Bin 2 files changed, 8 insertions(+), 7 deletions(-) rename landing/{ => public}/app_icon.png (100%) diff --git a/installers/desktop_inno_script.iss b/installers/desktop_inno_script.iss index 7ef6daf..cf91428 100644 --- a/installers/desktop_inno_script.iss +++ b/installers/desktop_inno_script.iss @@ -2,16 +2,16 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! ; Non-commercial use only -#define MyAppName "Keep Track" -#define MyAppVersion "7.3" +#define MyAppName "KeepTrack" +#define MyAppVersion "0.7.4" #define MyAppPublisher "Khesir" -#define MyAppURL "https://khesir.com" +#define MyAppURL "https://keep-track.khesir.com/" #define MyAppExeName "keep_track.exe" [Setup] ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) -AppId={{A1C21F3D-EFFE-4448-B387-C9D8414C7960} +AppId={{A8947B06-4B99-45BB-8600-8A7D4B7D06D1} AppName={#MyAppName} AppVersion={#MyAppVersion} ;AppVerName={#MyAppName} {#MyAppVersion} @@ -33,10 +33,10 @@ DisableProgramGroupPage=yes ; Uncomment the following line to run in non administrative install mode (install for current user only). ;PrivilegesRequired=lowest OutputDir=C:\Users\ajriz\Documents\Projects\Personal-Codex\personal_codex\installers -OutputBaseFilename=KeepTrack-v.7.3 +OutputBaseFilename=KeepTrack-v0.7.4 SetupIconFile=C:\Users\ajriz\Documents\Projects\Personal-Codex\personal_codex\app_icon.ico SolidCompression=yes -WizardStyle=modern +WizardStyle=modern dynamic [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" @@ -46,9 +46,10 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ [Files] Source: "C:\Users\ajriz\Documents\Projects\Personal-Codex\personal_codex\build\windows\x64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion -Source: "C:\Users\ajriz\Documents\Projects\Personal-Codex\personal_codex\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "C:\Users\ajriz\Documents\Projects\Personal-Codex\personal_codex\build\windows\x64\runner\Release\app_links_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "C:\Users\ajriz\Documents\Projects\Personal-Codex\personal_codex\build\windows\x64\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "C:\Users\ajriz\Documents\Projects\Personal-Codex\personal_codex\build\windows\x64\runner\Release\permission_handler_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "C:\Users\ajriz\Documents\Projects\Personal-Codex\personal_codex\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "C:\Users\ajriz\Documents\Projects\Personal-Codex\personal_codex\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files diff --git a/landing/app_icon.png b/landing/public/app_icon.png similarity index 100% rename from landing/app_icon.png rename to landing/public/app_icon.png