diff --git a/.auto-build/memory/gotchas.json b/.auto-build/memory/gotchas.json index d78d248..e69de29 100644 --- a/.auto-build/memory/gotchas.json +++ b/.auto-build/memory/gotchas.json @@ -1,115 +0,0 @@ -{ - "gotchas": [ - { - "id": "got_20251222_182000", - "timestamp": "2025-12-22T18:20:00Z", - "title": "Import Path Hell: Default vs Named Exports", - "problem": "Build failed with 'apiService is not exported' errors even though the module exports default api. Legacy code was using import { apiService } from 'api.js' which doesn't work with export default api.", - "solution": "Changed all imports from import { apiService } to import api, then updated all references from apiService.get to api.get. Also renamed imports to avoid conflicts (e.g., import apiClient from 'api').", - "context": "Encountered during build when migrating stores that expected named exports but API services used default exports", - "tags": ["javascript", "imports", "exports", "build-errors", "migration"], - "feature": "unified-app" - }, - { - "id": "got_20251222_182001", - "timestamp": "2025-12-22T18:20:01Z", - "title": "Pinia Store Factory Pattern Not Auto-Exported", - "problem": "Build failed with 'useCompanyStore is not exported by companies.js' because the shared stores are factory functions (createCompaniesStore), not direct exports (useCompanyStore).", - "solution": "Created module-specific sharedStores.js files that instantiate the factory functions with the module's API service, then export the actual store instances. Components import from module's sharedStores.js, not from @shared directly.", - "context": "Shared stores were designed as factories but modules were trying to import them as if they were regular stores", - "tags": ["pinia", "stores", "factory-pattern", "imports", "architecture"], - "feature": "unified-app" - }, - { - "id": "got_20251222_182002", - "timestamp": "2025-12-22T18:20:02Z", - "title": "Sed Command Quote Mismatch in Bulk Find-Replace", - "problem": "Bulk sed commands using single quotes in pattern didn't match imports using double quotes, and vice versa. Commands like sed 's|from '@/stores/'|...' didn't replace from \"@/stores/\" lines.", - "solution": "Always use the quote style that matches the target files. For Vue/JS files with ESLint using double quotes, use double quotes in sed patterns. Better yet: use find -exec with separate sed for each file to handle both quote styles.", - "context": "Spent significant time debugging why sed replacements weren't working during mass import path updates", - "tags": ["sed", "regex", "scripting", "find-replace", "migration"], - "feature": "unified-app" - }, - { - "id": "got_20251222_182003", - "timestamp": "2025-12-22T18:20:03Z", - "title": "Circular Reference in API Wrapper", - "problem": "receiptsStore.js failed to build with 'Identifier api has already been declared' because it imported api and then declared const api = { ... } wrapper object using the same name.", - "solution": "Renamed the import to apiClient (import apiClient from 'api') and used it in the wrapper: const api = { get: (url) => apiClient.get('/receipts${url}') }. This keeps the wrapper name 'api' for internal use while avoiding the conflict.", - "context": "Store was creating a scoped API wrapper for DRY principle but shadowed the import name", - "tags": ["javascript", "naming", "scope", "imports", "build-errors"], - "feature": "unified-app" - }, - { - "id": "got_20251222_182004", - "timestamp": "2025-12-22T18:20:04Z", - "title": "CSS Import Paths Breaking Build in Unified Structure", - "problem": "Build failed with 'Unable to resolve @import \"../../../../../shared/frontend/styles/layout/header.css\"' because the CSS files were copied but their import paths still pointed to old shared/frontend location.", - "solution": "Commented out the problematic @import statements in main.css since those styles are already imported in App.vue. Alternatively, could have updated paths to use @shared alias or relative paths from new location.", - "context": "CSS files from reports-app referenced shared styles using relative paths that became invalid in unified structure", - "tags": ["css", "imports", "build-errors", "migration", "paths"], - "feature": "unified-app" - }, - { - "id": "got_20251222_182005", - "timestamp": "2025-12-22T18:20:05Z", - "title": "Module Component Utilities Not Copied During Migration", - "problem": "Build failed with 'Could not resolve ../utils/exportUtils' because views referenced utils/ and components/ directories that weren't copied during initial migration (only views and stores were copied).", - "solution": "Copied the entire utils/ and components/ directories from source apps to module directories. These supporting files are essential dependencies of the views and must be migrated together.", - "context": "Initial migration focused on views/stores but missed the supporting utilities and components they depend on", - "tags": ["migration", "dependencies", "file-structure", "build-errors"], - "feature": "unified-app" - }, - { - "id": "got_20251222_182006", - "timestamp": "2025-12-22T18:20:06Z", - "title": "Vite Build Transform Count is Progress Indicator", - "problem": "Hard to tell if build is making progress when fixing import issues. Each fix revealed new errors, causing frustration.", - "solution": "Watch the 'transforming... ✓ N modules transformed' count - it increases with each successful fix even if build ultimately fails. Going from 200→573→1490→1492 modules meant we were getting close to success. Use this as encouragement!", - "context": "During iterative import path fixing, the transform count showed we were making real progress toward a successful build", - "tags": ["vite", "build", "debugging", "progress-tracking", "developer-experience"], - "feature": "unified-app" - }, - { - "id": "got_20251224_001000", - "timestamp": "2025-12-24T00:10:00Z", - "title": "Menu Structure Mismatch: Flat Array vs Nested Sections", - "problem": "Hamburger menu appeared completely empty (no menu items visible) even though enabledMenuItems computed property returned data. Used .flatMap() to create flat array [{item1}, {item2}] but SlideMenu component expected nested structure [{title: 'Section', items: [...]}, ...].", - "solution": "Removed .flatMap() transformation and returned the nested structure directly from getEnabledMenuSections(). Component's v-for=\"section in menuItems\" now properly iterates over sections, then v-for=\"item in section.items\" shows all items.", - "context": "User reported 'MENIUL HAMBURGER ARE IN CONTINUARE TEXT ALB PE FUNDAL ALB' and provided screenshot showing menu with only user profile at bottom, no menu sections/items visible", - "tags": ["vue", "data-structure", "component-contract", "v-for", "ux"], - "feature": "unified-app-ux" - }, - { - "id": "got_20251224_001001", - "timestamp": "2025-12-24T00:10:01Z", - "title": "TypeError: useAuthStore is not a function - Store Timing Issue", - "problem": "Period store threw 'TypeError: useAuthStore is not a function' when trying to call useAuthStore() in getStorageKey() function. Stores were passed as factory parameters but weren't callable in that context/timing.", - "solution": "Wrap store access in try-catch with lazy instantiation. Call useAuthStore() inside the function that needs it, not at module level. Return null if stores aren't ready yet. This allows graceful degradation when stores haven't been initialized by Pinia yet.", - "context": "accountingPeriod.js tried to access auth/company stores to generate localStorage key for persisting user's period selection", - "tags": ["pinia", "stores", "timing", "initialization", "error-handling"], - "feature": "unified-app-ux" - }, - { - "id": "got_20251224_001002", - "timestamp": "2025-12-24T00:10:02Z", - "title": "Missing Auth Token in API Requests Causes 500 Errors", - "problem": "Backend returned 500 Internal Server Error when frontend tried to load accounting periods. Console showed no Authorization header in requests even though user was logged in and JWT token existed in localStorage.", - "solution": "Add axios request interceptor to automatically inject token: authApi.interceptors.request.use(config => { const token = localStorage.getItem('access_token'); if (token) config.headers.Authorization = `Bearer ${token}`; return config; }). Place this AFTER creating axios instance but BEFORE making any API calls.", - "context": "App.vue created authApi axios instance with only baseURL and Content-Type, but didn't configure automatic token injection from localStorage", - "tags": ["axios", "jwt", "authentication", "api", "interceptor"], - "feature": "unified-app-ux" - }, - { - "id": "got_20251224_001003", - "timestamp": "2025-12-24T00:10:03Z", - "title": "Period Auto-Load Never Triggered Despite Handler Exists", - "problem": "Period dropdown stayed on 'Selectare perioada' placeholder even after manually selecting company. handleCompanyChanged() function existed and logged messages, but periods.value and selectedPeriod.value remained empty. No automatic loading occurred.", - "solution": "Add Vue watch() on companyStore.selectedCompany to automatically call periodStore.loadPeriods() when company changes. Handler alone isn't enough - need reactive watcher with { immediate: true } to handle both initial load and subsequent changes. Watch triggers for ALL company changes (auto-select on login + manual selection).", - "context": "User explicitly reported 'dar si dupa ce aleg o firma MARIUS M AUTO, AR TREBUI SA SE SELECTEZE AUTOMAT ULTIMA LUNA, SI NU SE SELECTEAZA' after manually selecting company", - "tags": ["vue", "watch", "reactive", "auto-load", "ux"], - "feature": "unified-app-ux" - } - ], - "updated": "2025-12-24T00:10:00Z" -} diff --git a/.auto-build/memory/patterns.json b/.auto-build/memory/patterns.json index 2cbb323..e69de29 100644 --- a/.auto-build/memory/patterns.json +++ b/.auto-build/memory/patterns.json @@ -1,140 +0,0 @@ -{ - "patterns": [ - { - "id": "pat_20251222_182000", - "timestamp": "2025-12-22T18:20:00Z", - "title": "Unified Vue SPA with Module Isolation via Error Boundaries", - "description": "Consolidate multiple Vue apps into a single SPA using lazy-loaded modules wrapped in error boundaries. Each module has its own layout component with ErrorBoundary wrapper to prevent crashes from propagating across modules.", - "context": "Implemented while unifying Reports App and Data Entry App into single deployment", - "example": { - "file": "src/modules/reports/ReportsLayout.vue", - "lines": "1-7", - "snippet": "\n\n" - }, - "tags": ["vue", "spa", "error-boundary", "module-isolation", "architecture"], - "feature": "unified-app", - "usageCount": 0 - }, - { - "id": "pat_20251222_182001", - "timestamp": "2025-12-22T18:20:01Z", - "title": "Dual API Proxy Pattern in Vite for Microservices", - "description": "Configure Vite dev server to proxy multiple backend microservices under different paths. Allows unified frontend to communicate with separate backend services while maintaining CORS and authentication.", - "context": "Needed to route /api/reports to port 8001 and /api/data-entry to port 8003 from single frontend", - "example": { - "file": "vite.config.js", - "lines": "38-62", - "snippet": "proxy: {\n '/api/reports': {\n target: 'http://localhost:8001',\n changeOrigin: true,\n rewrite: (path) => path.replace(/^\\/api\\/reports/, '/api'),\n configure: (proxy) => {\n proxy.on('proxyReq', (proxyReq, req) => {\n if (req.headers.authorization) {\n proxyReq.setHeader('Authorization', req.headers.authorization);\n }\n });\n }\n },\n '/api/data-entry': {\n target: 'http://localhost:8003',\n changeOrigin: true,\n rewrite: (path) => path.replace(/^\\/api\\/data-entry/, '/api')\n }\n}" - }, - "tags": ["vite", "proxy", "microservices", "api", "configuration"], - "feature": "unified-app", - "usageCount": 0 - }, - { - "id": "pat_20251222_182002", - "timestamp": "2025-12-22T18:20:02Z", - "title": "Pinia Store Factory Pattern for Shared Stores", - "description": "Create shared Pinia stores as factory functions that accept API service instances. Each module instantiates the shared stores with its own API service, ensuring proper module isolation while sharing store logic.", - "context": "Auth, companies, and accounting period stores needed to work with both Reports (port 8001) and Data Entry (port 8003) APIs", - "example": { - "file": "src/shared/stores/auth.js", - "lines": "21-32", - "snippet": "export function createAuthStore(apiService) {\n return defineStore('auth', () => {\n const accessToken = ref(localStorage.getItem('access_token'))\n // ... state\n\n const login = async (credentials) => {\n const response = await apiService.post('/auth/login', credentials)\n // ... handle response\n }\n\n return { login, logout, isAuthenticated, currentUser }\n })\n}" - }, - "tags": ["pinia", "stores", "factory-pattern", "module-isolation", "vue"], - "feature": "unified-app", - "usageCount": 0 - }, - { - "id": "pat_20251222_182003", - "timestamp": "2025-12-22T18:20:03Z", - "title": "Module-Specific Shared Store Instances", - "description": "Instantiate shared store factories in each module's dedicated file to ensure proper API service binding. Prevents import confusion and ensures each module uses its own API base URL.", - "context": "Needed to prevent modules from accidentally using wrong API service when importing shared stores", - "example": { - "file": "src/modules/reports/stores/sharedStores.js", - "lines": "1-18", - "snippet": "import { createAuthStore } from '@shared/stores/auth'\nimport { createCompaniesStore } from '@shared/stores/companies'\nimport { createAccountingPeriodStore } from '@shared/stores/accountingPeriod'\nimport api from '@reports/services/api'\n\n// Create instances with Reports API service\nexport const useAuthStore = createAuthStore(api)\nexport const useCompanyStore = createCompaniesStore(api, useAuthStore)\nexport const useAccountingPeriodStore = createAccountingPeriodStore(api)\n\n// All reports components import from this file, not directly from @shared" - }, - "tags": ["pinia", "stores", "module-isolation", "api", "architecture"], - "feature": "unified-app", - "usageCount": 0 - }, - { - "id": "pat_20251222_182004", - "timestamp": "2025-12-22T18:20:04Z", - "title": "Vite Alias Strategy for Module Organization", - "description": "Use Vite path aliases to create clear module boundaries: @shared for shared code, @reports and @data-entry for module-specific code. Makes imports explicit and prevents accidental cross-module dependencies.", - "context": "Needed clear import paths when consolidating two apps with different import patterns", - "example": { - "file": "vite.config.js", - "lines": "19-26", - "snippet": "resolve: {\n alias: {\n '@': fileURLToPath(new URL('./src', import.meta.url)),\n '@shared': fileURLToPath(new URL('./src/shared', import.meta.url)),\n '@reports': fileURLToPath(new URL('./src/modules/reports', import.meta.url)),\n '@data-entry': fileURLToPath(new URL('./src/modules/data-entry', import.meta.url))\n },\n dedupe: ['vue', 'vue-router', 'pinia', 'primevue']\n}" - }, - "tags": ["vite", "aliases", "imports", "module-organization", "architecture"], - "feature": "unified-app", - "usageCount": 0 - }, - { - "id": "pat_20251222_182005", - "timestamp": "2025-12-22T18:20:05Z", - "title": "IIS URL Rewrite Rules for SPA with Multiple API Backends", - "description": "Configure IIS web.config to proxy different API paths to different backend ports while serving SPA for all other routes. Enables single IIS site to route to multiple microservices.", - "context": "Production deployment needs unified frontend to communicate with both backend services through IIS", - "example": { - "file": "public/web.config", - "lines": "5-28", - "snippet": "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n" - }, - "tags": ["iis", "deployment", "spa", "microservices", "proxy"], - "feature": "unified-app", - "usageCount": 0 - }, - { - "id": "pat_20251224_001000", - "timestamp": "2025-12-24T00:10:00Z", - "title": "Vue Watcher for Auto-Loading Dependent Data", - "description": "Use Vue watch() to automatically trigger data loading when dependent selections change. Watch company selection changes to auto-load accounting periods, ensuring UI stays synchronized without manual intervention.", - "context": "Users manually selected company but period dropdown stayed on placeholder instead of auto-selecting current period", - "example": { - "file": "src/App.vue", - "lines": "88-100", - "snippet": "watch(\n () => companyStore.selectedCompany,\n async (newCompany, oldCompany) => {\n if (newCompany && newCompany.id_firma && newCompany !== oldCompany) {\n console.log('[App] Company changed via watch, loading periods for:', newCompany.id_firma)\n await periodStore.loadPeriods(newCompany.id_firma)\n console.log('[App] Periods auto-loaded successfully')\n }\n },\n { immediate: true }\n)" - }, - "tags": ["vue", "watch", "reactive", "auto-load", "ux"], - "feature": "unified-app-ux", - "usageCount": 0 - }, - { - "id": "pat_20251224_001001", - "timestamp": "2025-12-24T00:10:01Z", - "title": "Axios Request Interceptor for JWT Token Injection", - "description": "Add axios request interceptor to automatically inject JWT Bearer token from localStorage into all API requests. Eliminates manual token handling in every API call and prevents 401/500 authentication errors.", - "context": "API requests were failing with 500 errors because JWT token wasn't being sent in Authorization header", - "example": { - "file": "src/App.vue", - "lines": "61-68", - "snippet": "authApi.interceptors.request.use(config => {\n const token = localStorage.getItem('access_token')\n if (token) {\n config.headers.Authorization = `Bearer ${token}`\n }\n return config\n})" - }, - "tags": ["axios", "jwt", "authentication", "interceptor", "api"], - "feature": "unified-app-ux", - "usageCount": 0 - }, - { - "id": "pat_20251224_001002", - "timestamp": "2025-12-24T00:10:02Z", - "title": "Pinia Store Factory with Lazy Instantiation", - "description": "When store factories need to access other stores, use lazy instantiation with try-catch to avoid timing issues. Access stores inside functions (not at module level) and gracefully handle cases where stores aren't ready yet.", - "context": "Period store tried to access auth/company stores for localStorage key generation but got 'useAuthStore is not a function' error", - "example": { - "file": "src/shared/stores/accountingPeriod.js", - "lines": "52-64", - "snippet": "const getStorageKey = () => {\n try {\n const authStore = useAuthStore();\n const companyStore = useCompanyStore();\n const username = authStore.user?.username;\n const companyId = companyStore.selectedCompany?.id_firma;\n if (!username || !companyId) return null;\n return `selected_period_${username}_${companyId}`;\n } catch (e) {\n // Stores not yet initialized, skip localStorage\n return null;\n }\n};" - }, - "tags": ["pinia", "stores", "lazy-initialization", "try-catch", "timing"], - "feature": "unified-app-ux", - "usageCount": 0 - } - ], - "updated": "2025-12-24T00:10:00Z" -} diff --git a/.auto-build/memory/sessions/20251222-182000-unified-app.json b/.auto-build/memory/sessions/20251222-182000-unified-app.json deleted file mode 100644 index ded58a7..0000000 --- a/.auto-build/memory/sessions/20251222-182000-unified-app.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "session_id": "ses_20251222_182000", - "timestamp": "2025-12-22T18:20:00Z", - "feature": "unified-app", - "duration": "2h 30m", - "insights_saved": [ - {"type": "pattern", "id": "pat_20251222_182000", "title": "Unified Vue SPA with Module Isolation via Error Boundaries"}, - {"type": "pattern", "id": "pat_20251222_182001", "title": "Dual API Proxy Pattern in Vite for Microservices"}, - {"type": "pattern", "id": "pat_20251222_182002", "title": "Pinia Store Factory Pattern for Shared Stores"}, - {"type": "pattern", "id": "pat_20251222_182003", "title": "Module-Specific Shared Store Instances"}, - {"type": "pattern", "id": "pat_20251222_182004", "title": "Vite Alias Strategy for Module Organization"}, - {"type": "pattern", "id": "pat_20251222_182005", "title": "IIS URL Rewrite Rules for SPA with Multiple API Backends"}, - {"type": "gotcha", "id": "got_20251222_182000", "title": "Import Path Hell: Default vs Named Exports"}, - {"type": "gotcha", "id": "got_20251222_182001", "title": "Pinia Store Factory Pattern Not Auto-Exported"}, - {"type": "gotcha", "id": "got_20251222_182002", "title": "Sed Command Quote Mismatch in Bulk Find-Replace"}, - {"type": "gotcha", "id": "got_20251222_182003", "title": "Circular Reference in API Wrapper"}, - {"type": "gotcha", "id": "got_20251222_182004", "title": "CSS Import Paths Breaking Build in Unified Structure"}, - {"type": "gotcha", "id": "got_20251222_182005", "title": "Module Component Utilities Not Copied During Migration"}, - {"type": "gotcha", "id": "got_20251222_182006", "title": "Vite Build Transform Count is Progress Indicator"} - ], - "files_created": [ - "package.json", - "vite.config.js", - "src/router/index.js", - "src/config/menu.js", - "src/config/features.js", - "src/App.vue", - "src/main.js", - "src/shared/components/ErrorBoundary.vue", - "src/modules/reports/ReportsLayout.vue", - "src/modules/data-entry/DataEntryLayout.vue", - "src/modules/reports/services/api.js", - "src/modules/data-entry/services/api.js", - "src/modules/reports/stores/sharedStores.js", - "src/modules/data-entry/stores/sharedStores.js", - "public/web.config", - ".env.example", - "index.html" - ], - "files_migrated": [ - "src/modules/reports/views/*.vue (6 files)", - "src/modules/reports/stores/*.js (5 files)", - "src/modules/reports/components/** (dashboard cards, layout)", - "src/modules/reports/utils/*.js", - "src/modules/data-entry/views/receipts/*.vue (2 files)", - "src/modules/data-entry/components/ocr/*.vue (3 files)", - "src/modules/data-entry/stores/receiptsStore.js", - "src/shared/components/** (LoginView, CompanySelector, PeriodSelector, AppHeader, SlideMenu)", - "src/shared/stores/** (auth.js, companies.js, accountingPeriod.js)", - "src/shared/styles/**", - "src/assets/css/** (complete CSS system from reports-app)" - ], - "summary": "Successfully consolidated Reports App and Data Entry App into single unified SPA. Implemented module isolation via error boundaries, dual API proxy configuration, and lazy loading. Build completed successfully with 1492 modules transformed, generating 12M dist with proper code splitting.", - "build_stats": { - "modules_transformed": 1492, - "bundle_size": "12M", - "chunks_generated": 63, - "largest_chunks": [ - "vendor-export.js (704KB)", - "vendor-primevue.js (524KB)", - "vendor-charts.js (207KB)" - ] - }, - "next_steps": [ - "Test dev server with npm run dev", - "Update App.vue to use module-specific store instances", - "Deploy dist/ to IIS and verify API proxies", - "Test module switching and error boundary isolation", - "Create unified-app README documentation" - ] -} diff --git a/.auto-build/memory/sessions/20251224-001000-unified-app-ux.json b/.auto-build/memory/sessions/20251224-001000-unified-app-ux.json deleted file mode 100644 index 04646ce..0000000 --- a/.auto-build/memory/sessions/20251224-001000-unified-app-ux.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "session_id": "20251224-001000-unified-app-ux", - "timestamp": "2025-12-24T00:10:00Z", - "feature": "unified-app-ux", - "branch": "feature/ab-unified-app", - "summary": "Fixed critical UX issues: period auto-selection after company change and empty hamburger menu display", - "goals": [ - "Fix period dropdown auto-selection when company is manually selected", - "Fix empty hamburger menu (no menu items visible)", - "Ensure JWT token is sent in all API requests", - "Resolve Pinia store timing issues" - ], - "outcomes": { - "success": true, - "patterns_learned": 3, - "gotchas_encountered": 4, - "files_modified": 4, - "tests_created": 3, - "commits": 1 - }, - "key_files": [ - "src/App.vue", - "src/shared/stores/accountingPeriod.js", - "src/shared/stores/companies.js", - "src/shared/components/layout/AppHeader.vue" - ], - "commits": [ - { - "hash": "287b9a9", - "message": "fix: Implement complete auto-selection and fix hamburger menu display", - "files_changed": 4, - "insertions": 97, - "deletions": 68 - } - ], - "technical_insights": [ - "Vue watch() with { immediate: true } is essential for reactive data loading when dependent selections change", - "Axios request interceptors must be configured AFTER creating instance but BEFORE making API calls", - "Pinia store factories accessing other stores require lazy instantiation with try-catch to avoid timing issues", - "Component data structure contracts must be honored - flatMap() broke nested section structure expected by SlideMenu", - "Handler functions alone aren't enough for reactive updates - need watchers to trigger automatic data loading" - ], - "problems_solved": [ - { - "problem": "Period dropdown stayed on placeholder after company selection", - "root_cause": "Missing Vue watcher to trigger loadPeriods() and missing JWT token in API requests", - "solution": "Added watch() on companyStore.selectedCompany + axios request interceptor for JWT token", - "files": ["src/App.vue"] - }, - { - "problem": "Hamburger menu completely empty (no menu items)", - "root_cause": "enabledMenuItems used flatMap() creating flat array but SlideMenu expected nested sections", - "solution": "Removed flatMap() and returned nested structure directly from getEnabledMenuSections()", - "files": ["src/App.vue"] - }, - { - "problem": "TypeError: useAuthStore is not a function", - "root_cause": "Period store tried to access stores before Pinia initialization", - "solution": "Wrapped store access in try-catch with lazy instantiation in getStorageKey()", - "files": ["src/shared/stores/accountingPeriod.js"] - }, - { - "problem": "API requests returning 500 errors", - "root_cause": "No Authorization header with JWT token in requests", - "solution": "Added axios request interceptor to inject Bearer token from localStorage", - "files": ["src/App.vue"] - } - ], - "patterns_added": [ - "pat_20251224_001000 - Vue Watcher for Auto-Loading Dependent Data", - "pat_20251224_001001 - Axios Request Interceptor for JWT Token Injection", - "pat_20251224_001002 - Pinia Store Factory with Lazy Instantiation" - ], - "gotchas_added": [ - "got_20251224_001000 - Menu Structure Mismatch: Flat Array vs Nested Sections", - "got_20251224_001001 - TypeError: useAuthStore is not a function - Store Timing Issue", - "got_20251224_001002 - Missing Auth Token in API Requests Causes 500 Errors", - "got_20251224_001003 - Period Auto-Load Never Triggered Despite Handler Exists" - ], - "testing": { - "approach": "Playwright E2E tests", - "tests_created": [ - "tests/e2e/unified-app/manual-company-select-test.spec.js - Verified period auto-selection", - "tests/e2e/unified-app/hamburger-menu-visual-test.spec.js - Checked for white-on-white issues", - "tests/e2e/unified-app/hamburger-menu-items-test.spec.js - Verified menu sections/items visible" - ], - "all_tests_passed": true, - "cleanup": "All test files deleted per user request after verification" - }, - "user_feedback": [ - "dar si dupa ce aleg o firma MARIUS M AUTO, AR TREBUI SA SE SELECTEZE AUTOMAT ULTIMA LUNA, SI NU SE SELECTEAZA", - "MENIUL HAMBURGER ARE IN CONTINUARE TEXT ALB PE FUNDAL ALB", - "iata un screenshot cu meniul hamburger complet blank", - "commit cu toate fix-urile", - "sterge toate testele si rapoartele si screenshot-urile" - ], - "lessons_learned": [ - "Always use Vue watchers for reactive data dependencies, not just event handlers", - "Component props must match expected data structure - verify with component source", - "Pinia stores must be lazily instantiated when accessing other stores to avoid timing issues", - "Axios interceptors are critical for automatic JWT token injection - don't rely on manual headers", - "Test automation helps verify fixes but manual testing with real scenarios catches UX issues" - ], - "next_steps": [], - "tags": ["vue", "pinia", "axios", "ux", "auto-selection", "authentication", "watchers", "stores"] -} diff --git a/.auto-build/specs/ultrathin-monolith/spec.md b/.auto-build/specs/ultrathin-monolith/spec.md deleted file mode 100644 index e13969c..0000000 --- a/.auto-build/specs/ultrathin-monolith/spec.md +++ /dev/null @@ -1,535 +0,0 @@ -# Feature: Ultrathin Monolith Backend Architecture - -## Overview - -Transform ROA2WEB from 3 separate backend services (reports-app:8001, data-entry-app:8003, telegram-bot:8002) into a single unified backend process running on port 8000. This architectural transformation simplifies deployment, centralizes logs, and reduces operational complexity for solo developer workflow while maintaining clear module boundaries. - -## Problem Statement - -**Current Pain Points:** -- Running 3 separate backend processes is complex to manage (start/stop/monitor) -- Windows deployment requires 3 NSSM services, significantly increasing deployment time -- Debugging is difficult with fragmented logs across multiple processes -- Resource overhead of multiple Python processes and connection pools -- Frontend already unified into single SPA, but backend remains fragmented - -**Who Benefits:** -- Solo developer: Easier development workflow, simplified debugging -- Windows deployment: Single service instead of 3 (faster deployment) -- System resources: Single Oracle pool, single process overhead - -## User Stories - -- As a developer, I want to start the entire backend with a single command (`python main.py`) so that I can begin working faster -- As a developer, I want centralized logs in a single stream so that I can debug issues across all modules more easily -- As a deployer, I want to install a single Windows service so that deployment takes less time -- As a system administrator, I want a single backend process so that resource usage is more efficient -- As a developer, I want to keep separate module directories (reports/, data-entry/, telegram-bot/) so that code organization remains clear - -## Functional Requirements - -### Core Requirements - -1. **Single Entry Point** - - One `main.py` file at project root (`/backend/main.py`) - - Single FastAPI application instance - - Single uvicorn process running on port 8000 - -2. **Module Prefix Routing** - - Reports endpoints: `/api/reports/*` (e.g., `/api/reports/invoices`, `/api/reports/dashboard`) - - Data Entry endpoints: `/api/data-entry/*` (e.g., `/api/data-entry/receipts`, `/api/data-entry/ocr`) - - Telegram endpoints: `/api/telegram/*` (e.g., `/api/telegram/auth/verify-user`) - - Shared endpoints: `/api/auth/*`, `/api/companies`, `/api/calendar` (no prefix change) - -3. **Integrated Telegram Bot** - - Telegram bot runs in same process (background thread/task) - - Internal API endpoints accessible via `/api/telegram/*` - - Shared database access (no separate SQLite for bot) - -4. **Unified Startup/Shutdown** - - Single lifespan manager initializing all modules - - Parallel initialization where possible (Oracle pool + PaddleOCR) - - Graceful shutdown for all components - - Centralized logging configuration - -5. **Module Directory Structure** - - Keep existing directories: `backend/modules/reports/`, `backend/modules/data-entry/`, `backend/modules/telegram/` - - Each module retains its own routers, services, models - - Shared code remains in `shared/` (auth, database, routes) - -### Secondary Requirements - -1. **Environment Variable Consolidation** - - Single `.env` file at `backend/.env` - - Merge variables from all 3 backends - - Namespace conflicts resolved (e.g., `REPORTS_*`, `DATA_ENTRY_*` prefixes) - -2. **Single Virtual Environment** - - Merged `requirements.txt` with all dependencies - - De-duplicated dependencies (single version of FastAPI, etc.) - - Optional dependency groups for OCR/Telegram - -3. **Health Check Aggregation** - - Single `/health` endpoint reporting status of all modules - - Check Oracle connection, SQLite database, Telegram bot status - - Return comprehensive health information - -4. **Code Cleanup** - - Remove duplicate auth router implementations (use shared router factory) - - Remove duplicate company/calendar router implementations - - Consolidate logging configuration - - Remove unused imports and code - -## Technical Requirements - -### Files to Create - -| File | Purpose | -|------|---------| -| `/backend/main.py` | Single unified entry point - creates FastAPI app, registers all module routers, manages lifecycle | -| `/backend/config.py` | Unified configuration management (merges settings from all apps) | -| `/backend/requirements.txt` | Consolidated dependencies from all 3 backends | -| `/backend/.env.example` | Merged environment variables template | -| `/backend/modules/__init__.py` | Module package marker | -| `/backend/modules/reports/__init__.py` | Reports module package | -| `/backend/modules/data-entry/__init__.py` | Data entry module package | -| `/backend/modules/telegram/__init__.py` | Telegram module package | - -### Files to Modify - -| File | Changes | -|------|---------| -| `reports-app/backend/app/routers/*.py` | Move to `backend/modules/reports/routers/` | -| `reports-app/backend/app/services/*.py` | Move to `backend/modules/reports/services/` | -| `reports-app/backend/app/models/*.py` | Move to `backend/modules/reports/models/` | -| `reports-app/backend/app/cache/**/*.py` | Move to `backend/modules/reports/cache/` (reports-specific caching) | -| `data-entry-app/backend/app/routers/*.py` | Move to `backend/modules/data-entry/routers/` | -| `data-entry-app/backend/app/services/*.py` | Move to `backend/modules/data-entry/services/` | -| `data-entry-app/backend/app/db/**/*.py` | Move to `backend/modules/data-entry/db/` | -| `data-entry-app/backend/migrations/` | Move to `backend/modules/data-entry/migrations/` | -| `reports-app/telegram-bot/app/bot/*.py` | Move to `backend/modules/telegram/bot/` | -| `reports-app/telegram-bot/app/db/*.py` | Move to `backend/modules/telegram/db/` | -| `reports-app/telegram-bot/app/internal_api.py` | Integrate into main FastAPI app as router | -| All router files | Update imports to reflect new paths | -| `vite.config.js` | Update proxy configuration to single backend (port 8000) | -| `public/web.config` (IIS) | Update proxy rules to single backend (port 8000) | - -### New Directory Structure - -``` -roa2web/ -├── backend/ # UNIFIED BACKEND -│ ├── main.py # Single entry point -│ ├── config.py # Unified configuration -│ ├── requirements.txt # Merged dependencies -│ ├── .env # Merged environment variables -│ ├── .env.example # Environment template -│ ├── venv/ # Single virtual environment -│ │ -│ ├── modules/ # Module-specific code -│ │ ├── __init__.py -│ │ │ -│ │ ├── reports/ # Reports module (from reports-app) -│ │ │ ├── __init__.py -│ │ │ ├── routers/ # API endpoints (invoices, dashboard, treasury, etc.) -│ │ │ ├── services/ # Business logic -│ │ │ ├── models/ # Pydantic models -│ │ │ ├── schemas/ # Response schemas -│ │ │ └── cache/ # Caching system (L1+L2) -│ │ │ -│ │ ├── data-entry/ # Data entry module (from data-entry-app) -│ │ │ ├── __init__.py -│ │ │ ├── routers/ # Receipts, OCR, nomenclature routers -│ │ │ ├── services/ # Business logic + OCR service -│ │ │ ├── db/ # SQLModel models + CRUD -│ │ │ ├── schemas/ # Pydantic schemas -│ │ │ └── migrations/ # Alembic migrations -│ │ │ -│ │ └── telegram/ # Telegram bot module (from telegram-bot) -│ │ ├── __init__.py -│ │ ├── bot/ # Command handlers, formatters -│ │ ├── db/ # Bot-specific database (auth codes, sessions) -│ │ └── routers/ # Internal API endpoints (auth code management) -│ │ -│ └── data/ # Data files -│ ├── cache/ # SQLite cache files (reports module) -│ ├── receipts/ # SQLite DB + uploads (data-entry module) -│ └── telegram/ # SQLite DB for bot (telegram module) -│ -├── shared/ # Shared components (unchanged) -│ ├── auth/ # JWT authentication -│ ├── database/ # Oracle pool -│ ├── routes/ # Shared router factories -│ └── frontend/ # Shared Vue components -│ -├── frontend/ # Unified frontend (unchanged structure) -│ ├── src/ -│ │ ├── modules/ -│ │ │ ├── reports/ -│ │ │ └── data-entry/ -│ │ └── shared/ -│ └── vite.config.js # UPDATE: Single proxy to localhost:8000 -│ -├── deployment/ # Deployment scripts -│ └── windows/ # Windows deployment -│ └── install-service.ps1 # UPDATE: Single NSSM service -│ -└── docs/ # Documentation -``` - -### Dependencies Consolidation - -**Common Dependencies** (deduplicated): -``` -fastapi>=0.109.0 -uvicorn[standard]>=0.27.0 -pydantic>=2.5.3 -python-dotenv>=1.0.0 -PyJWT>=2.8.0 -python-jose[cryptography]>=3.3.0 -oracledb>=2.0.1 -aiosqlite>=0.19.0 -httpx>=0.27.0 -python-multipart>=0.0.6 -``` - -**Reports-Specific**: -``` -openpyxl>=3.1.0 # Excel export -fpdf2>=2.7.0 # PDF generation -python-dateutil>=2.8.2 # Date utilities -``` - -**Data Entry-Specific**: -``` -sqlmodel>=0.0.14 # ORM -alembic>=1.13.1 # Migrations -aiofiles>=23.2.1 # Async file handling -Pillow>=10.2.0 # Image processing -paddleocr>=2.7.0 # OCR engine -paddlepaddle>=2.5.0 # PaddleOCR backend -opencv-python>=4.8.0 # Image processing -pytesseract>=0.3.10 # Tesseract OCR -pdf2image>=1.16.0 # PDF to image conversion -numpy>=1.24.0 # Array operations -``` - -**Telegram-Specific**: -``` -python-telegram-bot>=20.7 # Telegram bot SDK -aiosmtplib>=3.0.0 # Email (2FA) -sentry-sdk>=1.40.0 # Monitoring (optional) -``` - -### Database Changes - -**No schema changes** - databases remain separate: -- Oracle: Shared via `oracle_pool` (reports + data-entry nomenclatures + auth) -- SQLite (`receipts.db`): Data entry module receipts data -- SQLite (`cache.db`): Reports module caching -- SQLite (`telegram.db`): Telegram bot auth codes and sessions - -**Database Access Patterns**: -- Oracle Pool: Single shared instance initialized at startup, used by all modules -- SQLite databases: Separate per module, stored in `backend/data/` - -### API Changes - -**URL Migration** (frontend must update): - -| Old URL | New URL | Module | -|---------|---------|--------| -| `http://localhost:8001/api/invoices` | `http://localhost:8000/api/reports/invoices` | Reports | -| `http://localhost:8001/api/dashboard` | `http://localhost:8000/api/reports/dashboard` | Reports | -| `http://localhost:8001/api/treasury` | `http://localhost:8000/api/reports/treasury` | Reports | -| `http://localhost:8001/api/trial-balance` | `http://localhost:8000/api/reports/trial-balance` | Reports | -| `http://localhost:8001/api/cache/*` | `http://localhost:8000/api/reports/cache/*` | Reports | -| `http://localhost:8003/api/receipts` | `http://localhost:8000/api/data-entry/receipts` | Data Entry | -| `http://localhost:8003/api/ocr` | `http://localhost:8000/api/data-entry/ocr` | Data Entry | -| `http://localhost:8003/api/nomenclature` | `http://localhost:8000/api/data-entry/nomenclature` | Data Entry | -| `http://localhost:8002/api/telegram/*` | `http://localhost:8000/api/telegram/*` | Telegram | -| `http://localhost:8001/api/auth/login` | `http://localhost:8000/api/auth/login` | Shared (no change) | -| `http://localhost:8001/api/companies` | `http://localhost:8000/api/companies` | Shared (no change) | -| `http://localhost:8001/api/calendar` | `http://localhost:8000/api/calendar` | Shared (no change) | - -**Frontend Configuration Updates**: - -```javascript -// vite.config.js - BEFORE (3 proxies) -proxy: { - '/api/reports': { target: 'http://localhost:8001', rewrite: (path) => path.replace(/^\/api\/reports/, '/api') }, - '/api/data-entry': { target: 'http://localhost:8003', rewrite: (path) => path.replace(/^\/api\/data-entry/, '/api') }, - '/api/telegram': { target: 'http://localhost:8002', rewrite: (path) => path.replace(/^\/api\/telegram/, '/api') } -} - -// vite.config.js - AFTER (1 proxy) -proxy: { - '/api': { target: 'http://localhost:8000', changeOrigin: true } -} -``` - -```xml - - - - - - - - - - - - - - - - - - - -``` - -## Design Decisions - -### Approach: Module-Based Monolith with Clear Boundaries - -**Why This Approach:** -1. **Maintains Module Isolation**: Each module (reports, data-entry, telegram) keeps its own directory structure -2. **Simplifies Deployment**: Single process, single service, single log stream -3. **Preserves Code Organization**: Module boundaries remain clear in file structure -4. **Enables Gradual Migration**: Can move one module at a time -5. **Reduces Complexity**: Solo developer doesn't need microservices orchestration - -**Key Architectural Patterns:** -- **Single FastAPI App**: One application instance with multiple routers registered with prefixes -- **Module Routers**: Each module exports a function that returns its configured router -- **Parallel Initialization**: Oracle pool and PaddleOCR load concurrently at startup -- **Unified Logging**: Single logging configuration with module-specific loggers -- **Shared Singleton**: Oracle pool shared across all modules - -### Alternatives Considered - -**Alternative 1: Keep Microservices Architecture** -- **Rejected**: Adds complexity without benefits for solo developer -- **Rejected**: Windows deployment remains slow (3 services) -- **Rejected**: Debugging remains difficult (fragmented logs) - -**Alternative 2: Merge Everything into Flat Structure** -- **Rejected**: Loses module boundaries, code becomes harder to navigate -- **Rejected**: Difficult to understand which code belongs to which feature - -**Alternative 3: Use Docker Compose for Orchestration** -- **Rejected**: Adds Docker overhead without solving core issues -- **Rejected**: Windows deployment still needs Docker Desktop or manual setup -- **Rejected**: Doesn't simplify solo developer workflow - -## Acceptance Criteria - -- [ ] Single `python backend/main.py` command starts entire backend -- [ ] All endpoints accessible on port 8000 with module prefixes (`/api/reports/*`, `/api/data-entry/*`, `/api/telegram/*`) -- [ ] Telegram bot runs in same process (background task), receives/sends messages -- [ ] Oracle pool initialized once, shared across all modules -- [ ] PaddleOCR loads in parallel with Oracle pool at startup (non-blocking) -- [ ] All logs appear in single console stream with module prefixes -- [ ] `/health` endpoint returns comprehensive status (Oracle, SQLite, Telegram bot) -- [ ] Frontend can access all functionality through unified backend -- [ ] Windows deployment requires only 1 NSSM service (not 3) -- [ ] All existing tests pass after migration -- [ ] Startup script simplified to single command (no parallel process management) -- [ ] Development mode: `uvicorn backend.main:app --reload` auto-reloads on code changes -- [ ] Module directories clearly separated (`modules/reports/`, `modules/data-entry/`, `modules/telegram/`) - -## Out of Scope - -**Explicitly NOT Included in This Transformation:** - -- ❌ Frontend code changes (only configuration updates: vite.config.js, web.config) -- ❌ Business logic modifications (pure architectural refactoring) -- ❌ Database schema changes (no migrations needed) -- ❌ Authentication system changes (JWT logic stays same) -- ❌ API contract changes (same endpoints, just different base URL) -- ❌ Oracle database migration (stays on Oracle) -- ❌ SQLite schema changes (data-entry and telegram databases unchanged) -- ❌ UI/UX modifications (frontend functionality stays identical) -- ❌ Performance optimizations (beyond parallel initialization) -- ❌ New features (pure consolidation work) - -## Risks and Mitigations - -| Risk | Likelihood | Impact | Mitigation | -|------|------------|--------|------------| -| Import path issues after moving files | High | Medium | Use absolute imports with module prefixes (`from backend.modules.reports.services import ...`), update all imports systematically | -| Oracle pool initialization delays startup | Medium | Low | Use parallel initialization (Oracle + PaddleOCR concurrently), add timeout handling | -| Telegram bot blocks main thread | Medium | High | Run bot in background thread with `asyncio.create_task()` or `threading.Thread(daemon=True)` | -| PaddleOCR memory usage in same process | Low | Medium | Keep background initialization, monitor memory usage, add configuration to disable OCR if needed | -| Port 8000 conflicts with existing service | Low | Low | Make port configurable via environment variable, document port change | -| Frontend cache issues with URL changes | Medium | Low | Clear browser cache, document URL changes, use cache-busting headers | -| Windows service migration breaks existing deployment | Medium | High | Create new service name (`ROA2WEB-Unified`), test thoroughly before removing old services, document rollback procedure | -| Circular imports between modules | Low | Medium | Keep module dependencies one-way (reports/data-entry/telegram should not import each other), use shared code for common functionality | -| Environment variable conflicts | Low | Medium | Use namespaced prefixes (`REPORTS_*`, `DATA_ENTRY_*`, `TELEGRAM_*`), document all variables in `.env.example` | -| Lost logs during migration | Low | High | Keep old logs in archive, test logging thoroughly in dev environment first | - -## Open Questions - -1. **Telegram Bot Threading**: Should we use `asyncio.create_task()` (async) or `threading.Thread()` (sync) for Telegram bot polling? - - **Recommendation**: Use `asyncio.create_task()` to stay in async context, avoid GIL issues - -2. **Module Import Strategy**: Should modules import from each other, or only through shared code? - - **Recommendation**: NO cross-module imports (reports ↔ data-entry ↔ telegram), only via `shared/` - -3. **Health Check Depth**: How detailed should `/health` endpoint be (simple up/down vs full diagnostics)? - - **Recommendation**: Return dict with per-module status, keep fast (<500ms) - -4. **Backward Compatibility**: Should we support old URLs temporarily (redirect 8001→8000)? - - **Recommendation**: NO - clean break, update frontend configuration once - -5. **Development vs Production Config**: Should we have separate startup modes? - - **Recommendation**: Use environment variables (`ENV=development|production`), single entry point - -6. **Migration Strategy**: Big bang (all at once) or incremental (one module at a time)? - - **Recommendation**: Big bang - easier to test, cleaner migration, no production deployment yet - -7. **Logging Format**: Should we prefix all log messages with module name `[REPORTS]`, `[DATA-ENTRY]`, `[TELEGRAM]`? - - **Recommendation**: YES - use Python logging with module-specific loggers - -## Estimated Complexity - -**HIGH** - Major architectural transformation requiring: - -**Justification:** -- Moving and reorganizing ~50+ files across 3 backends -- Updating ~200+ import statements -- Merging 3 separate lifecycle managers into one -- Integrating Telegram bot from separate process into main app -- Testing all functionality after migration -- Updating deployment scripts and documentation -- Ensuring no regressions across all modules - -**Estimated Effort:** -- File reorganization: 4-6 hours -- Import path updates: 3-4 hours -- Unified main.py + lifecycle: 3-4 hours -- Telegram bot integration: 2-3 hours -- Testing (manual + automated): 4-6 hours -- Frontend configuration updates: 1-2 hours -- Deployment script updates: 2-3 hours -- Documentation updates: 2-3 hours -- **Total: 21-31 hours (~3-4 working days)** - -**Recommended Approach:** -1. Create new structure, copy files (don't delete old ones yet) -2. Update imports module by module -3. Test each module independently -4. Integrate Telegram bot last -5. Full integration testing -6. Update frontend configuration -7. Archive old backend directories after verification - -## Implementation Notes - -### Startup Sequence - -```python -# backend/main.py - Startup lifespan -@asynccontextmanager -async def lifespan(app: FastAPI): - """Unified startup/shutdown for all modules""" - - # 1. Initialize logging - setup_logging() - - # 2. Parallel initialization (Oracle + PaddleOCR) - await asyncio.gather( - init_oracle_pool(), - init_paddle_ocr(), # Background initialization - init_data_entry_db(), - init_telegram_db() - ) - - # 3. Initialize module-specific components - await init_reports_cache() - - # 4. Start Telegram bot (background task) - telegram_task = asyncio.create_task(run_telegram_bot()) - - logger.info("🚀 ROA2WEB Unified Backend started on port 8000") - - yield - - # Shutdown - telegram_task.cancel() - await close_all_resources() -``` - -### Router Registration Pattern - -```python -# backend/main.py - Router registration -from modules.reports.routers import create_reports_router -from modules.data_entry.routers import create_data_entry_router -from modules.telegram.routers import create_telegram_router - -# Register module routers with prefixes -app.include_router(create_reports_router(), prefix="/api/reports", tags=["reports"]) -app.include_router(create_data_entry_router(), prefix="/api/data-entry", tags=["data-entry"]) -app.include_router(create_telegram_router(), prefix="/api/telegram", tags=["telegram"]) - -# Register shared routers (no prefix) -app.include_router(auth_router, prefix="/api/auth", tags=["auth"]) -app.include_router(companies_router, prefix="/api/companies", tags=["companies"]) -app.include_router(calendar_router, prefix="/api/calendar", tags=["calendar"]) -``` - -### Module Router Factory Pattern - -```python -# backend/modules/reports/routers/__init__.py -from fastapi import APIRouter -from .invoices import router as invoices_router -from .dashboard import router as dashboard_router -from .treasury import router as treasury_router -# ... other routers - -def create_reports_router() -> APIRouter: - """Create and configure reports module router""" - router = APIRouter() - - # Include all sub-routers (no prefix - already in main.py) - router.include_router(invoices_router, prefix="/invoices") - router.include_router(dashboard_router, prefix="/dashboard") - router.include_router(treasury_router, prefix="/treasury") - # ... other routers - - return router -``` - -### Critical Files List - -**Top 10 Files That Will Be Affected:** - -1. `/backend/main.py` - **NEW** - Single entry point, replaces 3 main.py files -2. `/backend/config.py` - **NEW** - Unified configuration -3. `/frontend/vite.config.js` - Update proxy configuration -4. `/frontend/public/web.config` - Update IIS rewrite rules -5. `reports-app/backend/app/main.py` - Move logic to unified main.py -6. `data-entry-app/backend/app/main.py` - Move logic to unified main.py -7. `reports-app/telegram-bot/app/main.py` - Move logic to unified main.py -8. `shared/database/oracle_pool.py` - Ensure singleton pattern for shared pool -9. `/deployment/windows/install-service.ps1` - Update to install single service -10. `/start-dev.sh` - Simplify to start single backend process - -**Additional Critical Files:** -- All router files in `reports-app/backend/app/routers/` (~10 files) -- All router files in `data-entry-app/backend/app/routers/` (~4 files) -- Telegram bot handlers in `reports-app/telegram-bot/app/bot/handlers.py` -- Cache system in `reports-app/backend/app/cache/` (~9 files) -- OCR service in `data-entry-app/backend/app/services/ocr_service.py` -- Database models in `data-entry-app/backend/app/db/` - ---- - -**Document Version**: 1.0 -**Created**: 2025-12-24 -**Author**: Auto-Build System -**Status**: Draft - Ready for Review diff --git a/.auto-build/specs/ultrathin-monolith/status.json b/.auto-build/specs/ultrathin-monolith/status.json deleted file mode 100644 index 7fd843a..0000000 --- a/.auto-build/specs/ultrathin-monolith/status.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "feature": "ultrathin-monolith", - "status": "spec_created", - "created_at": "2025-12-24T00:00:00Z", - "updated_at": "2025-12-24T00:00:00Z", - "spec_version": "1.0", - "complexity": "high", - "estimated_hours": "21-31", - "estimated_days": "3-4", - "key_changes": [ - "Single backend entry point (backend/main.py) replacing 3 separate main.py files", - "Module-based directory structure (backend/modules/reports, backend/modules/data-entry, backend/modules/telegram)", - "API URL changes with module prefixes (/api/reports/*, /api/data-entry/*, /api/telegram/*)", - "Telegram bot integrated into main process (background task)", - "Parallel initialization (Oracle pool + PaddleOCR)", - "Unified logging with module prefixes", - "Single virtual environment with merged dependencies", - "Single Windows service for deployment (instead of 3)" - ], - "modules_affected": [ - "reports-app/backend", - "data-entry-app/backend", - "reports-app/telegram-bot", - "frontend (vite.config.js, web.config)", - "shared/database", - "shared/auth", - "deployment/windows" - ], - "files_to_create": [ - "/backend/main.py", - "/backend/config.py", - "/backend/requirements.txt", - "/backend/.env.example", - "/backend/modules/__init__.py", - "/backend/modules/reports/__init__.py", - "/backend/modules/data-entry/__init__.py", - "/backend/modules/telegram/__init__.py" - ], - "files_to_move": [ - "reports-app/backend/app/routers/* -> backend/modules/reports/routers/", - "reports-app/backend/app/services/* -> backend/modules/reports/services/", - "reports-app/backend/app/models/* -> backend/modules/reports/models/", - "reports-app/backend/app/cache/* -> backend/modules/reports/cache/", - "data-entry-app/backend/app/routers/* -> backend/modules/data-entry/routers/", - "data-entry-app/backend/app/services/* -> backend/modules/data-entry/services/", - "data-entry-app/backend/app/db/* -> backend/modules/data-entry/db/", - "data-entry-app/backend/migrations/* -> backend/modules/data-entry/migrations/", - "reports-app/telegram-bot/app/bot/* -> backend/modules/telegram/bot/", - "reports-app/telegram-bot/app/db/* -> backend/modules/telegram/db/" - ], - "critical_files": [ - "/backend/main.py (NEW - single entry point)", - "/backend/config.py (NEW - unified configuration)", - "/frontend/vite.config.js (UPDATE - single proxy)", - "/frontend/public/web.config (UPDATE - single IIS rule)", - "reports-app/backend/app/main.py (MIGRATE to unified)", - "data-entry-app/backend/app/main.py (MIGRATE to unified)", - "reports-app/telegram-bot/app/main.py (MIGRATE to unified)", - "shared/database/oracle_pool.py (VERIFY singleton)", - "/deployment/windows/install-service.ps1 (UPDATE single service)", - "/start-dev.sh (SIMPLIFY single process)" - ], - "risks": [ - { - "risk": "Import path issues after moving files", - "likelihood": "high", - "impact": "medium", - "mitigation": "Use absolute imports, update systematically" - }, - { - "risk": "Telegram bot blocks main thread", - "likelihood": "medium", - "impact": "high", - "mitigation": "Run in background task with asyncio.create_task()" - }, - { - "risk": "Windows service migration breaks deployment", - "likelihood": "medium", - "impact": "high", - "mitigation": "Create new service, test thoroughly, document rollback" - }, - { - "risk": "Frontend cache issues with URL changes", - "likelihood": "medium", - "impact": "low", - "mitigation": "Clear browser cache, cache-busting headers" - } - ], - "acceptance_criteria_count": 14, - "out_of_scope_count": 10, - "dependencies": [ - "Frontend unified app (already completed)", - "SSH tunnel (unchanged)", - "Oracle database (unchanged)", - "Shared modules (auth, database, routes) - unchanged" - ], - "testing_strategy": [ - "Module-by-module verification during migration", - "Integration testing after all modules migrated", - "Frontend connectivity testing", - "Telegram bot functionality testing", - "Performance testing (startup time, memory usage)", - "Windows deployment testing" - ], - "migration_approach": "big_bang", - "migration_notes": "Create new structure alongside old, copy files, update imports, test thoroughly, then archive old directories after verification", - "next_steps": [ - "Review and approve specification", - "Create backend/ directory structure", - "Copy files to new structure (keep old as backup)", - "Update imports module by module", - "Create unified main.py", - "Test module routers independently", - "Integrate Telegram bot", - "Update frontend configuration", - "Full integration testing", - "Update deployment scripts", - "Update documentation", - "Archive old backend directories" - ] -} diff --git a/.auto-build/specs/unified-app/MIGRATION_CHECKLIST.md b/.auto-build/specs/unified-app/MIGRATION_CHECKLIST.md deleted file mode 100644 index 03cc403..0000000 --- a/.auto-build/specs/unified-app/MIGRATION_CHECKLIST.md +++ /dev/null @@ -1,406 +0,0 @@ -# Unified App Migration Checklist - -**Track your progress** as you implement the unified app. - ---- - -## Phase 1: Project Setup (0.5 days) - -### Directory Structure -- [ ] Create directory structure in root (`/mnt/e/proiecte/roa2web/`) -- [ ] Create `src/` subdirectories: - - [ ] `src/modules/reports/views/` - - [ ] `src/modules/reports/stores/` - - [ ] `src/modules/reports/services/` - - [ ] `src/modules/data-entry/views/receipts/` - - [ ] `src/modules/data-entry/components/ocr/` - - [ ] `src/modules/data-entry/stores/` - - [ ] `src/modules/data-entry/services/` - - [ ] `src/shared/components/` - - [ ] `src/shared/stores/` - - [ ] `src/shared/styles/` - - [ ] `src/config/` - - [ ] `src/router/` - - [ ] `src/assets/css/` - -### Core Configuration Files -- [ ] Create `package.json` (merge dependencies from both apps) -- [ ] Create `vite.config.js` (dual proxy + lazy loading) -- [ ] Create `src/main.js` (PrimeVue setup) -- [ ] Create `.env.example` (environment variables) -- [ ] Create `.gitignore` -- [ ] Create `README.md` - -### Copy Shared Resources -- [ ] Copy `shared/frontend/components/` → `src/shared/components/` -- [ ] Copy `shared/frontend/stores/` → `src/shared/stores/` -- [ ] Copy `shared/frontend/styles/` → `src/shared/styles/` -- [ ] Copy `reports-app/frontend/src/assets/css/` → `src/assets/css/` - -### Verification -- [ ] Run `npm install` - succeeds -- [ ] Run `npm run dev` - starts on port 3000 -- [ ] No console errors - ---- - -## Phase 2: Module Migration (1 day) - -### Reports Module - Views -- [ ] Copy `DashboardView.vue` → `src/modules/reports/views/` -- [ ] Copy `InvoicesView.vue` → `src/modules/reports/views/` -- [ ] Copy `BankCashRegisterView.vue` → `src/modules/reports/views/` -- [ ] Copy `TrialBalanceView.vue` → `src/modules/reports/views/` -- [ ] Copy `TelegramView.vue` → `src/modules/reports/views/` -- [ ] Copy `CacheStatsView.vue` → `src/modules/reports/views/` -- [ ] Update imports in all views (if needed) - -### Reports Module - Stores -- [ ] Copy `dashboard.js` → `src/modules/reports/stores/` -- [ ] Copy `invoices.js` → `src/modules/reports/stores/` -- [ ] Copy `treasury.js` → `src/modules/reports/stores/` -- [ ] Copy `trialBalance.js` → `src/modules/reports/stores/` -- [ ] Copy `cacheStore.js` → `src/modules/reports/stores/` -- [ ] Update imports in all stores (if needed) - -### Reports Module - Services -- [ ] Copy `api.js` → `src/modules/reports/services/` (in root) -- [ ] Update base URL to `/api/reports/` in api.js -- [ ] Test API calls route to localhost:8001 - -### Data Entry Module - Views -- [ ] Copy `ReceiptsListView.vue` → `src/modules/data-entry/views/receipts/` -- [ ] Copy `ReceiptCreateView.vue` → `src/modules/data-entry/views/receipts/` -- [ ] Update imports in views (if needed) - -### Data Entry Module - Components -- [ ] Copy `OCRUploadZone.vue` → `src/modules/data-entry/components/ocr/` -- [ ] Copy `OCRPreview.vue` → `src/modules/data-entry/components/ocr/` -- [ ] Copy `OCRConfidenceIndicator.vue` → `src/modules/data-entry/components/ocr/` -- [ ] Update imports in components (if needed) - -### Data Entry Module - Stores -- [ ] Copy `receiptsStore.js` → `src/modules/data-entry/stores/` -- [ ] Update imports in store (if needed) - -### Data Entry Module - Services -- [ ] Copy `api.js` → `src/modules/data-entry/services/` (in root) -- [ ] Update base URL to `/api/data-entry/` in api.js -- [ ] Test API calls route to localhost:8003 - -### CSS Merge -- [ ] Review `data-entry-app/frontend/src/assets/css/main.css` -- [ ] Merge unique styles into `src/assets/css/` (in root) -- [ ] Resolve any style conflicts -- [ ] Test responsive design - -### Verification -- [ ] All Reports views render without errors -- [ ] All Data Entry views render without errors -- [ ] No import errors in console -- [ ] CSS loads correctly -- [ ] No style conflicts - ---- - -## Phase 3: Routing & Navigation (0.5 days) - -### Router Configuration -- [ ] Create `src/router/index.js` (in root) -- [ ] Add `/login` route (eager loaded) -- [ ] Add `/reports/*` routes (lazy loaded) -- [ ] Add `/data-entry/*` routes (lazy loaded) -- [ ] Add redirect `/` → `/reports/dashboard` -- [ ] Add authentication guard -- [ ] Add 404 handling -- [ ] Test all routes work - -### Menu Configuration -- [ ] Create `src/config/menu.js` (in root) -- [ ] Define "Rapoarte" section -- [ ] Define "Introduceri Date" section -- [ ] Define "Sistem" section -- [ ] Export menu configuration - -### Feature Flags -- [ ] Create `src/config/features.js` (in root) -- [ ] Add reports.enabled flag -- [ ] Add dataEntry.enabled flag -- [ ] Add module-level flags -- [ ] Export isFeatureEnabled function - -### Root Component -- [ ] Create `src/App.vue` (in root) -- [ ] Integrate AppHeader with unified menu -- [ ] Integrate SlideMenu with menu sections -- [ ] Add router-view -- [ ] Add Toast and ConfirmDialog -- [ ] Test menu navigation - -### Verification -- [ ] Can navigate to all routes -- [ ] Menu highlights active route -- [ ] Authentication guard works -- [ ] Login redirects to dashboard -- [ ] 404 redirects work - ---- - -## Phase 4: Error Boundaries & Resilience (0.25 days) - -### Error Boundary Component -- [ ] Create `src/shared/components/ErrorBoundary.vue` (in root) -- [ ] Implement onErrorCaptured hook -- [ ] Add user-friendly error display -- [ ] Add retry functionality -- [ ] Add navigate away option -- [ ] Test with intentional error - -### Module Layouts -- [ ] Create `src/modules/reports/ReportsLayout.vue` (in root) -- [ ] Wrap with ErrorBoundary component -- [ ] Create `src/modules/data-entry/DataEntryLayout.vue` (in root) -- [ ] Wrap with ErrorBoundary component - -### Error Isolation Testing -- [ ] Introduce error in Reports module -- [ ] Verify Data Entry still works -- [ ] Introduce error in Data Entry module -- [ ] Verify Reports still works -- [ ] Test retry functionality -- [ ] Test navigate away functionality - -### Feature Flags Testing -- [ ] Disable Reports module -- [ ] Verify menu items hidden -- [ ] Disable Data Entry module -- [ ] Verify menu items hidden -- [ ] Re-enable all modules - -### Loading States -- [ ] Add loading spinner for lazy routes -- [ ] Test module switching loading -- [ ] Add skeleton screens (optional) - -### Verification -- [ ] Error boundary catches component errors -- [ ] User sees friendly error message -- [ ] Can retry or navigate away -- [ ] Module isolation works (error in one doesn't crash other) -- [ ] Feature flags work - ---- - -## Phase 5: Build & Deploy (0.25 days) - -### Production Build -- [ ] Run `npm run build` -- [ ] Build succeeds without errors -- [ ] Check `dist/` output -- [ ] Verify chunks created: - - [ ] vendor-core.[hash].js - - [ ] vendor-primevue.[hash].js - - [ ] vendor-utils.[hash].js - - [ ] reports.[hash].js (lazy) - - [ ] data-entry.[hash].js (lazy) - - [ ] main.[hash].js - - [ ] main.[hash].css - -### Bundle Analysis -- [ ] Install `rollup-plugin-visualizer` -- [ ] Analyze bundle sizes -- [ ] Verify total size ≤ sum of current apps -- [ ] Check for duplicate dependencies - -### Local Preview -- [ ] Run `npm run preview` -- [ ] Test all routes work -- [ ] Test API calls work -- [ ] Test error boundaries -- [ ] Test lazy loading - -### IIS Configuration -- [ ] Create `web.config` (in root) with URL rewrite rules -- [ ] Add SPA routing rule (all → index.html) -- [ ] Add proxy rule: `/api/reports/*` → `http://localhost:8001/api/*` -- [ ] Add proxy rule: `/api/data-entry/*` → `http://localhost:8003/api/*` -- [ ] Add proxy rule: `/uploads/*` → `http://localhost:8003/uploads/*` - -### Staging Deployment -- [ ] Deploy to staging IIS site -- [ ] Test all routes -- [ ] Test API calls -- [ ] Test error boundaries -- [ ] Test on different browsers -- [ ] Test on mobile devices - -### Production Deployment -- [ ] Backup current IIS configuration -- [ ] Backup current builds -- [ ] Deploy unified app -- [ ] Test all routes -- [ ] Test API calls -- [ ] Monitor error logs - -### Verification -- [ ] Build succeeds -- [ ] Chunks split correctly -- [ ] IIS deployment works -- [ ] API routing correct -- [ ] All features work in production - ---- - -## Testing Checklist - -### Manual Testing -- [ ] Login flow works -- [ ] Navigate to all Reports views -- [ ] Navigate to all Data Entry views -- [ ] Switch between modules -- [ ] Test company selector -- [ ] Test period selector -- [ ] Test logout -- [ ] Test on mobile (375px) -- [ ] Test on tablet (768px) -- [ ] Test on desktop (1920px) - -### E2E Tests -- [ ] Create `tests/e2e/login.spec.js` (in root) -- [ ] Create `tests/e2e/reports-navigation.spec.js` (in root) -- [ ] Create `tests/e2e/data-entry-navigation.spec.js` (in root) -- [ ] Create `tests/e2e/module-switching.spec.js` (in root) -- [ ] Create `tests/e2e/error-isolation.spec.js` (in root) -- [ ] Run all E2E tests - pass - -### Performance Testing -- [ ] Run Lighthouse audit -- [ ] Performance score ≥ 90 -- [ ] Initial load < 2 seconds -- [ ] Module switching < 500ms (cached) - ---- - -## Post-Implementation Checklist - -### Documentation Updates -- [ ] Update `CLAUDE.md` - Architecture section -- [ ] Update `CLAUDE.md` - Deployment section -- [ ] Update `README.md` - Quick start -- [ ] Update `README.md` - URL structure -- [ ] Update `DEPLOYMENT_GUIDE.md` - IIS configuration -- [ ] Update `docs/ARCHITECTURE_SCHEMA.md` - Diagrams -- [ ] Update `deployment/windows/README.md` - Deployment steps - -### Cleanup (After 1 Week) -- [ ] Archive `reports-app/frontend/` → `reports-app/frontend-archived/` -- [ ] Archive `data-entry-app/frontend/` → `data-entry-app/frontend-archived/` -- [ ] Update `start-test.sh` to use root directory -- [ ] Update `start-data-entry.sh` to use root directory -- [ ] Update CI/CD pipelines (if any) -- [ ] Document migration in CHANGELOG.md - -### Monitoring (First Week) -- [ ] Day 1: Review error logs -- [ ] Day 2: Review error logs -- [ ] Day 3: Review error logs -- [ ] Day 4: Review error logs -- [ ] Day 5: Review error logs -- [ ] Day 6: Review error logs -- [ ] Day 7: Review error logs + user feedback - -### Optimization (First Month) -- [ ] Week 2: Analyze bundle sizes -- [ ] Week 2: Optimize images/assets -- [ ] Week 3: Consider further code splitting -- [ ] Week 3: Add performance monitoring -- [ ] Week 4: Evaluate feature flag usage -- [ ] Week 4: Plan next improvements - ---- - -## Common Issues & Solutions - -### Issue: CSS Conflicts -**Solution**: Use CSS modules, check `docs/CSS_PATTERNS.md` - -### Issue: Import Errors -**Solution**: Update import paths, check alias configuration in vite.config.js - -### Issue: API Calls Failing -**Solution**: Verify proxy configuration, check backend is running - -### Issue: Error Boundary Not Catching -**Solution**: Check onErrorCaptured implementation, add global error handler - -### Issue: Large Bundle Size -**Solution**: Review manualChunks, enable tree shaking, lazy load more - -### Issue: IIS Routing Not Working -**Solution**: Check web.config URL rewrite rules, verify IIS URL Rewrite module installed - -### Issue: Store Contamination -**Solution**: Use module-scoped stores, check for global state - -### Issue: PrimeVue Theme Conflicts -**Solution**: Use single theme (saga-blue), override in vendor CSS - ---- - -## Progress Tracking - -**Phase 1: Setup** -- Started: ___________ -- Completed: ___________ -- Time Spent: ___________ hours - -**Phase 2: Migration** -- Started: ___________ -- Completed: ___________ -- Time Spent: ___________ hours - -**Phase 3: Routing** -- Started: ___________ -- Completed: ___________ -- Time Spent: ___________ hours - -**Phase 4: Error Boundaries** -- Started: ___________ -- Completed: ___________ -- Time Spent: ___________ hours - -**Phase 5: Build & Deploy** -- Started: ___________ -- Completed: ___________ -- Time Spent: ___________ hours - -**Total Time**: ___________ hours (Estimated: 20 hours) - ---- - -## Success Metrics - -### Deployment -- [ ] Single IIS site running (not 2) -- [ ] Single build process (not 2) -- [ ] Zero downtime during deployment - -### Performance -- [ ] Initial load < 2 seconds -- [ ] Module switching < 500ms -- [ ] Lighthouse score ≥ 90 -- [ ] Bundle size ≤ sum of old apps - -### Quality -- [ ] 100% feature parity -- [ ] All E2E tests passing -- [ ] Zero user-reported bugs (first week) -- [ ] Error isolation verified - ---- - -**Status**: ⬜ Not Started | 🔵 In Progress | ✅ Complete - -**Last Updated**: ___________ -**Completed By**: ___________ -**Production Deploy Date**: ___________ diff --git a/.auto-build/specs/unified-app/SUMMARY.md b/.auto-build/specs/unified-app/SUMMARY.md deleted file mode 100644 index 0c118f3..0000000 --- a/.auto-build/specs/unified-app/SUMMARY.md +++ /dev/null @@ -1,346 +0,0 @@ -# Unified App Specification - Executive Summary - -**Created**: 2025-12-22 -**Status**: Implementation-Ready -**Estimated Effort**: 2.5 days - ---- - -## What We're Building - -A single unified SPA that consolidates the Reports App and Data Entry App into one application with: -- Unified menu navigation between modules -- Module isolation via error boundaries -- Lazy loading for optimal performance -- Single build and deployment process - ---- - -## Key Requirements (Top 5) - -1. **Unified Navigation**: Single menu with Reports and Data Entry sections -2. **Module Isolation**: Error in one module doesn't crash the other -3. **Lazy Loading**: Modules loaded on-demand, not upfront -4. **Simplified Deployment**: Single IIS site instead of 2 -5. **Zero Backend Changes**: Both backends (8001, 8003) remain unchanged - ---- - -## Technical Approach - -### Architecture: Pragmatic Monolith - -**NOT using micro-frontends** because: -- Only 1 developer (not 20+ team) -- Weekly deploys (not multiple times per day) -- 1-5 concurrent users (not millions) -- Same tech stack (Vue 3 only) - -**Using instead**: -- Error boundaries for isolation (50-70% blast radius reduction) -- Lazy loading for performance -- Feature flags for module control -- Shared dependencies for smaller bundles - -### URL Structure - -``` -/login → Login -/ → Redirect to /reports/dashboard - -/reports/dashboard → Dashboard -/reports/invoices → Invoices -/reports/bank-cash → Bank/Cash -/reports/trial-balance → Trial Balance -/reports/telegram → Telegram Bot -/reports/cache-stats → Cache Stats - -/data-entry → Receipts List -/data-entry/create → New Receipt -/data-entry/:id → View Receipt -/data-entry/:id/edit → Edit Receipt -``` - -### API Routing - -**Vite Dev Proxy**: -- `/api/reports/*` → `http://localhost:8001/api/*` -- `/api/data-entry/*` → `http://localhost:8003/api/*` -- `/uploads/*` → `http://localhost:8003/uploads/*` - -**IIS Production Proxy**: -- Same routing via web.config URL rewrite rules - ---- - -## Critical Files (Top 10) - -### To Create (in root directory) - -1. `package.json` - Merged dependencies -2. `vite.config.js` - Dual proxy + lazy loading -3. `src/main.js` - App initialization -4. `src/App.vue` - Root with unified menu -5. `src/router/index.js` - Unified router -6. `src/config/menu.js` - Menu configuration -7. `src/shared/components/ErrorBoundary.vue` - Error isolation -8. `src/modules/reports/ReportsLayout.vue` - Reports wrapper -9. `src/modules/data-entry/DataEntryLayout.vue` - Data Entry wrapper -10. `web.config` - IIS configuration - -### To Migrate - -**Reports Module** (7 views, 5 stores, 1 service): -- Views: Dashboard, Invoices, BankCash, TrialBalance, Telegram, CacheStats -- Stores: dashboard, invoices, treasury, trialBalance, cacheStore -- Service: api.js (change base URL to `/api/reports/`) - -**Data Entry Module** (2 views, 3 components, 1 store, 1 service): -- Views: ReceiptsList, ReceiptCreate -- Components: OCRUploadZone, OCRPreview, OCRConfidenceIndicator -- Store: receiptsStore -- Service: api.js (change base URL to `/api/data-entry/`) - -**Shared** (5 components, 3 stores): -- Components: LoginView, AppHeader, SlideMenu, CompanySelector, PeriodSelector -- Stores: auth, companies, accountingPeriod (factories) - -**CSS**: Copy entire `reports-app/frontend/src/assets/css/` structure - ---- - -## Implementation Phases - -### Phase 1: Setup (0.5 days) -- Create directory structure -- Setup package.json, vite.config.js -- Copy shared components and CSS - -**Verify**: `npm install` and `npm run dev` work - -### Phase 2: Migration (1 day) -- Migrate all views, components, stores -- Update API service base URLs -- Merge CSS - -**Verify**: All views render, no import errors - -### Phase 3: Routing & Navigation (0.5 days) -- Create unified router with lazy loading -- Create menu configuration -- Create error boundaries -- Integrate AppHeader and SlideMenu - -**Verify**: Navigation works, lazy loading works - -### Phase 4: Error Boundaries & Resilience (0.25 days) -- Test error isolation -- Test feature flags -- Add loading states - -**Verify**: Error in one module doesn't crash other - -### Phase 5: Build & Deploy (0.25 days) -- Production build -- IIS configuration -- Deploy to staging -- Test all routes - -**Verify**: Build succeeds, IIS works, APIs route correctly - ---- - -## Expected Build Output - -``` -dist/ -├── index.html -├── assets/ -│ ├── vendor-core.[hash].js (~150KB) - Vue, Router, Pinia -│ ├── vendor-primevue.[hash].js (~200KB) - PrimeVue components -│ ├── vendor-utils.[hash].js (~80KB) - Axios, date-fns -│ ├── vendor-charts.[hash].js (~150KB) - Chart.js (lazy) -│ ├── vendor-export.[hash].js (~200KB) - XLSX, jsPDF (lazy) -│ ├── reports.[hash].js (~150KB) - Reports module (lazy) -│ ├── data-entry.[hash].js (~100KB) - Data Entry module (lazy) -│ ├── main.[hash].js (~50KB) - App shell -│ └── main.[hash].css (~80KB) - Global CSS -``` - ---- - -## Success Criteria - -### Must Have (Before Production) -- [ ] All Reports views work correctly -- [ ] All Data Entry views work correctly -- [ ] Navigation preserves auth and company/period -- [ ] Error in one module doesn't crash other -- [ ] Single IIS site deployment works -- [ ] API routing to both backends works - -### Performance Targets -- [ ] Initial load < 2 seconds -- [ ] Module switching < 500ms (cached) -- [ ] Bundle size ≤ sum of current apps -- [ ] Lighthouse score ≥ 90 - -### Testing -- [ ] E2E tests pass for login -- [ ] E2E tests pass for Reports navigation -- [ ] E2E tests pass for Data Entry navigation -- [ ] E2E tests pass for module switching -- [ ] E2E tests verify error isolation - ---- - -## Risks & Mitigations - -| Risk | Mitigation | -|------|------------| -| CSS conflicts | Use design tokens, test thoroughly | -| Large bundle size | Lazy loading, code splitting, tree shaking | -| Error boundary gaps | Test error scenarios, add global handler | -| IIS deployment complexity | Document config, test on staging first | -| Store contamination | Module-scoped stores, test isolation | - ---- - -## Rollback Plan - -**If deployment fails**: - -1. **Keep both apps running** (zero downtime) - - Leave `/roa2web/` and `/data-entry/` running - - Add unified app at `/unified/` for testing - -2. **Quick rollback** (15 minutes) - - Restore IIS config from backup - - Restore builds from `dist-backup/` - -3. **Git rollback** - - Tag before merge: `v1.0-pre-unified` - - Revert if needed: `git reset --hard v1.0-pre-unified` - ---- - -## Post-Implementation - -### After 1 Week of Stability - -**Archive old frontends**: -```bash -mv reports-app/frontend reports-app/frontend-archived -mv data-entry-app/frontend data-entry-app/frontend-archived -``` - -**Update documentation**: -- CLAUDE.md - Architecture and deployment -- DEPLOYMENT_GUIDE.md - IIS configuration -- README.md - Quick start and commands - -**Update scripts**: -- `./start-test.sh` - Point to root directory -- `./start-data-entry.sh` - Point to root directory - -### Monitoring (First Month) - -**Week 1**: Daily error log review -**Week 2-4**: Performance optimization -- Bundle size optimization -- Further code splitting -- Cache optimization - ---- - -## Open Questions & Recommendations - -### 1. PrimeVue Theme -**Question**: Use `saga-blue` (reports-app) or `lara-light-blue` (data-entry-app)? -**Recommendation**: `saga-blue` (reports-app is primary) - -### 2. Feature Flags -**Question**: Config file or environment variables? -**Recommendation**: Config file for simplicity, env vars for override - -### 3. Module Activation -**Question**: All active by default or opt-in? -**Recommendation**: All active by default (disable via config if needed) - -### 4. Monitoring -**Question**: Console logs only or add Sentry/similar? -**Recommendation**: Console logs for MVP, add monitoring later - ---- - -## Key Decisions Made - -1. **Architecture**: Pragmatic monolith (not micro-frontends) -2. **Error Isolation**: Error boundaries per module -3. **Code Splitting**: Lazy loading with manual chunks -4. **URL Structure**: `/reports/*` and `/data-entry/*` -5. **API Routing**: Proxy via Vite (dev) and IIS (prod) -6. **CSS System**: Use reports-app CSS structure -7. **PrimeVue Theme**: saga-blue (from reports-app) -8. **Shared Components**: Use existing from `shared/frontend/` -9. **Deployment**: Single IIS site at root `/` -10. **Backends**: No changes (remain at 8001, 8003) - ---- - -## Documentation Locations - -**Complete Spec**: `.auto-build-data/specs/unified-app/spec.md` -**Critical Files**: `.auto-build-data/specs/unified-app/critical-files.md` -**This Summary**: `.auto-build-data/specs/unified-app/SUMMARY.md` - -**Note**: All implementation files will be created in the project root directory (`.` or `/mnt/e/proiecte/roa2web/`) - -**Reference Docs**: -- `IMPLEMENTATION_PLAN_UNIFIED_APP.md` - Original plan -- `CLAUDE.md` - Project documentation (update after) -- `docs/ONBOARDING_CSS.md` - CSS system guide -- `docs/CSS_PATTERNS.md` - Available CSS patterns - ---- - -## Next Steps - -1. **Read the complete spec**: `spec.md` (detailed technical specification) -2. **Review critical files**: `critical-files.md` (files to migrate/create) -3. **Start Phase 1**: Project setup (0.5 days) -4. **Follow implementation plan**: 5 phases over 2.5 days -5. **Test thoroughly**: E2E tests before production -6. **Deploy to staging**: Test IIS configuration -7. **Deploy to production**: Single site deployment -8. **Monitor for 1 week**: Daily error log review -9. **Archive old apps**: After stability confirmed -10. **Update docs**: Complete documentation updates - ---- - -## Quick Stats - -- **Files to create**: ~15 -- **Files to migrate**: ~20 -- **CSS files to copy**: ~30 -- **Total files affected**: ~65 -- **Estimated time**: 2.5 days (20 hours) -- **Complexity**: Medium -- **Risk level**: Medium-Low (with mitigations) - ---- - -**Specification Status**: ✅ Implementation-Ready -**All Technical Decisions**: ✅ Made -**Rollback Plan**: ✅ Defined -**Success Criteria**: ✅ Defined - -**Ready to implement!** 🚀 - ---- - -**Version**: 1.0 -**Created**: 2025-12-22 -**Author**: Claude (Specification Agent) -**For**: ROA2WEB Unified App Feature diff --git a/.auto-build/specs/unified-app/critical-files.md b/.auto-build/specs/unified-app/critical-files.md deleted file mode 100644 index 80d63e8..0000000 --- a/.auto-build/specs/unified-app/critical-files.md +++ /dev/null @@ -1,477 +0,0 @@ -# Unified App - Critical Files Reference - -**Quick Reference**: Files that will be most affected during implementation - ---- - -## Files to Analyze (Before Implementation) - -### Configuration Files - -**Reports App**: -- `/mnt/e/proiecte/roa2web/reports-app/frontend/package.json` - Dependencies to merge -- `/mnt/e/proiecte/roa2web/reports-app/frontend/vite.config.js` - Proxy config, build settings -- `/mnt/e/proiecte/roa2web/reports-app/frontend/src/main.js` - PrimeVue setup - -**Data Entry App**: -- `/mnt/e/proiecte/roa2web/data-entry-app/frontend/package.json` - Dependencies to merge -- `/mnt/e/proiecte/roa2web/data-entry-app/frontend/vite.config.js` - Proxy config -- `/mnt/e/proiecte/roa2web/data-entry-app/frontend/src/main.js` - PrimeVue setup - -### Application Entry Points - -**Reports App**: -- `/mnt/e/proiecte/roa2web/reports-app/frontend/src/App.vue` - Root component, menu integration -- `/mnt/e/proiecte/roa2web/reports-app/frontend/src/router/index.js` - Router config - -**Data Entry App**: -- `/mnt/e/proiecte/roa2web/data-entry-app/frontend/src/App.vue` - Root component, menu integration -- `/mnt/e/proiecte/roa2web/data-entry-app/frontend/src/router/index.js` - Router config - -### Shared Components (Already Created) - -- `/mnt/e/proiecte/roa2web/shared/frontend/components/LoginView.vue` -- `/mnt/e/proiecte/roa2web/shared/frontend/components/layout/AppHeader.vue` -- `/mnt/e/proiecte/roa2web/shared/frontend/components/layout/SlideMenu.vue` -- `/mnt/e/proiecte/roa2web/shared/frontend/components/CompanySelector.vue` -- `/mnt/e/proiecte/roa2web/shared/frontend/components/PeriodSelector.vue` - -### Shared Stores (Factories) - -- `/mnt/e/proiecte/roa2web/shared/frontend/stores/auth.js` -- `/mnt/e/proiecte/roa2web/shared/frontend/stores/companies.js` -- `/mnt/e/proiecte/roa2web/shared/frontend/stores/accountingPeriod.js` - ---- - -## Views to Migrate - -### Reports Module (7 views) - -Source: `/mnt/e/proiecte/roa2web/reports-app/frontend/src/views/` -Destination: `/mnt/e/proiecte/roa2web/src/modules/reports/views/` - -1. `DashboardView.vue` - Main dashboard with metrics -2. `InvoicesView.vue` - Invoices table and filters -3. `BankCashRegisterView.vue` - Bank and cash register transactions -4. `TrialBalanceView.vue` - Trial balance report -5. `TelegramView.vue` - Telegram bot management -6. `CacheStatsView.vue` - Cache statistics -7. ~~`LoginView.vue`~~ - USE SHARED VERSION - -### Data Entry Module (2 views + 3 components) - -**Views**: -Source: `/mnt/e/proiecte/roa2web/data-entry-app/frontend/src/views/receipts/` -Destination: `/mnt/e/proiecte/roa2web/src/modules/data-entry/views/receipts/` - -1. `ReceiptsListView.vue` - List of receipts with filters -2. `ReceiptCreateView.vue` - Create/edit receipt form - -**Components**: -Source: `/mnt/e/proiecte/roa2web/data-entry-app/frontend/src/components/ocr/` -Destination: `/mnt/e/proiecte/roa2web/src/modules/data-entry/components/ocr/` - -1. `OCRUploadZone.vue` - File upload zone for receipts -2. `OCRPreview.vue` - Preview uploaded receipt image -3. `OCRConfidenceIndicator.vue` - OCR confidence indicator - ---- - -## Stores to Migrate - -### Reports Module (5 stores) - -Source: `/mnt/e/proiecte/roa2web/reports-app/frontend/src/stores/` -Destination: `/mnt/e/proiecte/roa2web/src/modules/reports/stores/` - -1. `dashboard.js` - Dashboard data and metrics -2. `invoices.js` - Invoices data and filters -3. `treasury.js` - Bank/cash register data -4. `trialBalance.js` - Trial balance data -5. `cacheStore.js` - Cache statistics - -**SKIP** (use shared): -- ~~`auth.js`~~ - Use `/mnt/e/proiecte/roa2web/shared/frontend/stores/auth.js` -- ~~`companies.js`~~ - Use `/mnt/e/proiecte/roa2web/shared/frontend/stores/companies.js` -- ~~`accountingPeriod.js`~~ - Use `/mnt/e/proiecte/roa2web/shared/frontend/stores/accountingPeriod.js` - -### Data Entry Module (1 store) - -Source: `/mnt/e/proiecte/roa2web/data-entry-app/frontend/src/stores/` -Destination: `/mnt/e/proiecte/roa2web/src/modules/data-entry/stores/` - -1. `receiptsStore.js` - Receipts data and CRUD operations - -**SKIP** (use shared): -- ~~`auth.js`~~ - Use shared -- ~~`companies.js`~~ - Use shared -- ~~`accountingPeriod.js`~~ - Use shared - ---- - -## Services to Migrate - -### Reports Module - -Source: `/mnt/e/proiecte/roa2web/reports-app/frontend/src/services/` -Destination: `/mnt/e/proiecte/roa2web/src/modules/reports/services/` - -1. `api.js` - **MODIFY**: Change base URL to `/api/reports/` - -**Current**: -```javascript -const api = axios.create({ - baseURL: '/api' -}) -``` - -**New**: -```javascript -const api = axios.create({ - baseURL: '/api/reports' -}) -``` - -### Data Entry Module - -Source: `/mnt/e/proiecte/roa2web/data-entry-app/frontend/src/services/` -Destination: `/mnt/e/proiecte/roa2web/src/modules/data-entry/services/` - -1. `api.js` - **MODIFY**: Change base URL to `/api/data-entry/` - -**Current**: -```javascript -const api = axios.create({ - baseURL: '/api' -}) -``` - -**New**: -```javascript -const api = axios.create({ - baseURL: '/api/data-entry' -}) -``` - ---- - -## CSS to Migrate - -### Reports Module CSS (COPY ENTIRE STRUCTURE) - -Source: `/mnt/e/proiecte/roa2web/reports-app/frontend/src/assets/css/` -Destination: `/mnt/e/proiecte/roa2web/src/assets/css/` - -**Copy ALL files** (this is the main CSS system): -- `core/` - Design tokens (colors, spacing, typography) -- `components/` - Reusable UI patterns (buttons, forms, cards, tables, stats) -- `patterns/` - Interactive patterns (animations, dashboard, interactive) -- `layout/` - Page structure (containers, grid, navigation) -- `utilities/` - Utility classes (colors, spacing, flex, text, display) -- `vendor/` - PrimeVue overrides -- `global.css` - Global styles -- `main.css` - Main entry point -- `mobile.css` - Mobile responsive styles - -### Data Entry Module CSS (MERGE) - -Source: `/mnt/e/proiecte/roa2web/data-entry-app/frontend/src/assets/css/` - -**Analyze and merge unique styles**: -- `main.css` - Merge with reports-app main.css -- Any component-specific styles - Integrate into unified system - -### Shared CSS - -Source: `/mnt/e/proiecte/roa2web/shared/frontend/styles/` -Destination: `/mnt/e/proiecte/roa2web/src/shared/styles/` - -1. `login.css` - Login page styles -2. `layout/header.css` - Header styles -3. `layout/navigation.css` - Navigation styles - ---- - -## Critical Implementation Files - -### 1. Package Configuration - -**File**: `/mnt/e/proiecte/roa2web/package.json` - -**Dependencies to merge**: -- From reports-app: axios, chart.js, date-fns, jspdf, jspdf-autotable, qrcode.vue, xlsx -- From data-entry-app: All covered by reports-app -- Shared: vue@^3.4.0, vue-router@^4.2.5, pinia@^2.1.7, primevue@^3.46.0 - -### 2. Vite Configuration - -**File**: `/mnt/e/proiecte/roa2web/vite.config.js` - -**Key sections**: -- Dual proxy for `/api/reports/` → `http://localhost:8001` -- Dual proxy for `/api/data-entry/` → `http://localhost:8003` -- Lazy loading configuration (manualChunks) -- Alias configuration (@, @shared, @reports, @data-entry) - -### 3. Application Entry - -**File**: `/mnt/e/proiecte/roa2web/src/main.js` - -**Key setup**: -- PrimeVue configuration (theme: saga-blue) -- Global components registration -- Router, Pinia, ToastService, ConfirmationService -- CSS imports - -### 4. Root Component - -**File**: `/mnt/e/proiecte/roa2web/src/App.vue` - -**Key elements**: -- AppHeader with unified menu -- SlideMenu with module sections -- router-view -- Toast, ConfirmDialog - -### 5. Router Configuration - -**File**: `/mnt/e/proiecte/roa2web/src/router/index.js` - -**Key routes**: -- `/login` - LoginView (eager loaded) -- `/reports/*` - ReportsLayout (lazy loaded) with children -- `/data-entry/*` - DataEntryLayout (lazy loaded) with children -- Authentication guard - -### 6. Menu Configuration - -**File**: `/mnt/e/proiecte/roa2web/src/config/menu.js` - -**Sections**: -- Rapoarte (Reports) -- Introduceri Date (Data Entry) -- Sistem (System) - -### 7. Feature Flags - -**File**: `/mnt/e/proiecte/roa2web/src/config/features.js` - -**Flags**: -- reports.enabled -- dataEntry.enabled -- Module-level flags - -### 8. Error Boundary Component - -**File**: `/mnt/e/proiecte/roa2web/src/shared/components/ErrorBoundary.vue` - -**Key features**: -- onErrorCaptured hook -- User-friendly error display -- Retry functionality -- Navigate away option - -### 9. Module Layouts - -**File**: `/mnt/e/proiecte/roa2web/src/modules/reports/ReportsLayout.vue` - -**Wrapper**: -```vue - - - -``` - -**File**: `/mnt/e/proiecte/roa2web/src/modules/data-entry/DataEntryLayout.vue` - -**Wrapper**: -```vue - - - -``` - ---- - -## Deployment Files - -### IIS Configuration - -**File**: `/mnt/e/proiecte/roa2web/web.config` - -**Key rules**: -- SPA routing (all routes → index.html) -- API proxy: `/api/reports/*` → `http://localhost:8001/api/*` -- API proxy: `/api/data-entry/*` → `http://localhost:8003/api/*` -- Uploads proxy: `/uploads/*` → `http://localhost:8003/uploads/*` - -### Environment Variables - -**File**: `/mnt/e/proiecte/roa2web/.env.example` - -**Variables**: -```bash -# API Endpoints (dev only, production uses IIS proxy) -VITE_REPORTS_API=http://localhost:8001 -VITE_DATA_ENTRY_API=http://localhost:8003 - -# Feature Flags -VITE_ENABLE_REPORTS=true -VITE_ENABLE_DATA_ENTRY=true -``` - ---- - -## Testing Files - -### E2E Tests - -**New files to create**: -- `/mnt/e/proiecte/roa2web/tests/e2e/login.spec.js` -- `/mnt/e/proiecte/roa2web/tests/e2e/reports-navigation.spec.js` -- `/mnt/e/proiecte/roa2web/tests/e2e/data-entry-navigation.spec.js` -- `/mnt/e/proiecte/roa2web/tests/e2e/module-switching.spec.js` -- `/mnt/e/proiecte/roa2web/tests/e2e/error-isolation.spec.js` - -**Existing tests to adapt**: -- `/mnt/e/proiecte/roa2web/reports-app/frontend/tests/` - Update routes -- `/mnt/e/proiecte/roa2web/data-entry-app/frontend/tests/` - Update routes - ---- - -## Documentation Files to Update - -### Primary Documentation - -1. **CLAUDE.md** (root) - - Update architecture diagram - - Add unified-app section - - Mark old apps as archived - - Update deployment instructions - -2. **README.md** (root) - - Update quick start - - Update deployment section - - Update URL structure - -3. **DEPLOYMENT_GUIDE.md** - - Update IIS configuration - - Update build process - - Add rollback instructions - -4. **docs/ARCHITECTURE_SCHEMA.md** - - Update architecture diagrams - - Document module structure - - Add error boundary architecture - -### Deployment Documentation - -5. **deployment/windows/README.md** - - Update deployment steps - - Update IIS configuration - - Update proxy rules - -6. **deployment/windows/docs/WINDOWS_DEPLOYMENT.md** - - Complete Windows guide updates - ---- - -## File Count Summary - -**To Create**: ~15 new files -- 9 core files (package.json, vite.config.js, main.js, App.vue, router, menu, features, ErrorBoundary, 2 layouts) -- 3 configuration files (.env.example, web.config, README.md) -- 3 documentation updates - -**To Migrate**: ~20 files -- 7 Reports views -- 2 Data Entry views -- 3 Data Entry components -- 5 Reports stores -- 1 Data Entry store -- 2 API services - -**To Copy**: ~30 CSS files -- Entire reports-app CSS structure - -**Total Files Affected**: ~65 files - ---- - -## Time Estimates per File Type - -| Task | Files | Avg Time | Total | -|------|-------|----------|-------| -| Create core files | 9 | 30 min | 4.5 hours | -| Migrate views | 9 | 20 min | 3 hours | -| Migrate components | 3 | 15 min | 45 min | -| Migrate stores | 6 | 20 min | 2 hours | -| Migrate services | 2 | 30 min | 1 hour | -| Copy CSS | 1 | 1 hour | 1 hour | -| Configuration | 3 | 30 min | 1.5 hours | -| Documentation | 6 | 30 min | 3 hours | -| Testing & fixes | - | - | 4 hours | - -**Total**: ~20 hours (~2.5 days) - ---- - -## Verification Checklist - -After migrating each category: - -### Configuration Files ✓ -- [ ] package.json has all dependencies -- [ ] vite.config.js has dual proxy -- [ ] main.js initializes correctly -- [ ] npm install succeeds -- [ ] npm run dev starts - -### Views ✓ -- [ ] All Reports views render -- [ ] All Data Entry views render -- [ ] No import errors -- [ ] Routes work - -### Stores ✓ -- [ ] All stores import correctly -- [ ] No duplicate store instances -- [ ] Shared stores work -- [ ] Module stores isolated - -### Services ✓ -- [ ] API calls route to correct backend -- [ ] /api/reports/ → :8001 -- [ ] /api/data-entry/ → :8003 -- [ ] Auth headers preserved - -### CSS ✓ -- [ ] No style conflicts -- [ ] Design tokens work -- [ ] PrimeVue theme consistent -- [ ] Responsive works - -### Router ✓ -- [ ] All routes accessible -- [ ] Lazy loading works -- [ ] Auth guard works -- [ ] 404 redirects - -### Error Boundaries ✓ -- [ ] Catches component errors -- [ ] Displays user message -- [ ] Module isolation works -- [ ] Can retry/navigate - -### Build ✓ -- [ ] npm run build succeeds -- [ ] Chunks split correctly -- [ ] Bundle size acceptable -- [ ] Preview works - ---- - -**Last Updated**: 2025-12-22 -**For**: Unified App Implementation -**Reference**: See spec.md for complete specification diff --git a/.auto-build/specs/unified-app/plan.md b/.auto-build/specs/unified-app/plan.md deleted file mode 100644 index b300d5a..0000000 --- a/.auto-build/specs/unified-app/plan.md +++ /dev/null @@ -1,1174 +0,0 @@ -# Implementation Plan: unified-app - -## Overview - -This plan consolidates the Reports App and Data Entry App frontends into a single unified SPA. The implementation follows a pragmatic monolith approach with module isolation through error boundaries and lazy loading. - -**Key principles:** -- Single build, single IIS site deployment -- Module isolation via error boundaries (errors in one module don't crash the other) -- Lazy loading for each module (only load what the user navigates to) -- Shared components from `src/shared/` (auth, companies, period selectors) -- CSS system from reports-app preserved and enhanced - -**Worktree location:** `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app` - ---- - -## Phase 1: Project Setup (0.5 days) - -### Task 1: Create Root Directory Structure - -**Objective**: Set up the foundational directory structure for the unified app at the repository root. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/` (create directory) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/` (create directory) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/` (create directory) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/data-entry/` (create directory) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/` (create directory) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/config/` (create directory) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/router/` (create directory) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/assets/` (create directory) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/public/` (create directory) - -**Description**: -Create the following directory structure at the repository root (not inside reports-app or data-entry-app): -``` -src/ - modules/ - reports/ - views/ - stores/ - services/ - data-entry/ - views/ - receipts/ - components/ - ocr/ - stores/ - services/ - shared/ - components/ - layout/ - stores/ - styles/ - layout/ - config/ - router/ - assets/ - css/ - core/ - components/ - patterns/ - layout/ - utilities/ - vendor/ -public/ -``` - -**Dependencies**: None - -**Completion Criteria**: -- [x] All directories created -- [x] Directory structure matches specification - ---- - -### Task 2: Create package.json with Merged Dependencies - -**Objective**: Create unified package.json combining dependencies from both apps. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/package.json` (create) - -**Description**: -Merge dependencies from both `reports-app/frontend/package.json` and `data-entry-app/frontend/package.json`: - -**Dependencies to include:** -- `vue`: ^3.4.0 -- `vue-router`: ^4.2.5 -- `pinia`: ^2.1.7 -- `axios`: ^1.6.5 (use higher version) -- `primevue`: ^3.48.0 (use higher version) -- `primeicons`: ^6.0.1 -- `chart.js`: ^4.5.0 (reports only) -- `vue-chartjs`: ^5.3.2 (reports only) -- `date-fns`: ^2.30.0 -- `jspdf`: ^3.0.1 -- `jspdf-autotable`: ^5.0.2 -- `xlsx`: ^0.18.5 -- `qrcode.vue`: ^3.6.0 - -**DevDependencies:** -- `@vitejs/plugin-vue`: ^5.0.0 -- `vite`: ^5.0.10 -- `@playwright/test`: ^1.54.2 -- `eslint`: ^8.56.0 -- `eslint-plugin-vue`: ^9.20.0 -- `prettier`: ^3.1.1 - -**Scripts:** -- `dev`: vite -- `build`: vite build -- `preview`: vite preview -- `lint`: eslint src/ --ext .vue,.js --fix -- `test:e2e`: playwright test - -**Dependencies**: Task 1 - -**Completion Criteria**: -- [x] package.json created with all dependencies -- [x] Scripts defined correctly -- [x] `npm install` succeeds - ---- - -### Task 3: Create vite.config.js with Dual Proxy and Lazy Loading - -**Objective**: Configure Vite for unified app with proxies to both backends. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/vite.config.js` (create) - -**Description**: -Create Vite configuration with: - -1. **Dual proxy configuration:** - - `/api/reports/*` -> `http://localhost:8001/api/*` - - `/api/data-entry/*` -> `http://localhost:8003/api/*` - - `/uploads` -> `http://localhost:8003` - -2. **Aliases:** - - `@` -> `./src` - - `@shared` -> `./src/shared` - - `@reports` -> `./src/modules/reports` - - `@data-entry` -> `./src/modules/data-entry` - -3. **Build configuration:** - - `base`: `/` (single site, no subdirectory) - - Manual chunks for vendors (vue, primevue, charts, exports) - - Source maps enabled - - Cache busting with hashes - -4. **dedupe** for Vue, vue-router, pinia, primevue - -5. **WSL2 file watching** (usePolling: true) - -Reference: `reports-app/frontend/vite.config.js` for htmlTimestampPlugin and build settings. - -**Dependencies**: Task 1 - -**Completion Criteria**: -- [x] Vite config created with dual proxy -- [x] All aliases defined -- [x] Manual chunks configured -- [x] `npm run dev` starts successfully - ---- - -### Task 4: Copy CSS System from Reports App - -**Objective**: Migrate the complete CSS design system to the unified app. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/assets/css/` (copy entire directory) - -**Description**: -Copy the entire CSS directory from `reports-app/frontend/src/assets/css/` to `src/assets/css/`: -- `core/` - Design tokens (tokens.css) -- `components/` - Component patterns (cards.css, badges.css, stats.css, etc.) -- `patterns/` - Interactive patterns -- `layout/` - Page structure -- `utilities/` - Utility classes -- `vendor/` - PrimeVue overrides -- `main.css` - Main import file -- `global.css` - Global styles -- `mobile.css` - Mobile responsive styles - -This is the authoritative CSS system that both modules will use. - -**Dependencies**: Task 1 - -**Completion Criteria**: -- [x] All CSS files copied -- [x] Directory structure preserved -- [x] `main.css` imports all other CSS files correctly - ---- - -### Task 5: Copy Shared Frontend Components and Stores - -**Objective**: Migrate shared components to unified app's shared directory. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/components/LoginView.vue` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/components/CompanySelector.vue` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/components/PeriodSelector.vue` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/components/layout/AppHeader.vue` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/components/layout/SlideMenu.vue` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/stores/auth.js` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/stores/companies.js` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/stores/accountingPeriod.js` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/styles/login.css` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/styles/layout/header.css` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/styles/layout/navigation.css` (copy) - -**Description**: -Copy from `shared/frontend/`: -1. Components: `LoginView.vue`, `CompanySelector.vue`, `PeriodSelector.vue`, `layout/AppHeader.vue`, `layout/SlideMenu.vue` -2. Stores: `auth.js`, `companies.js`, `accountingPeriod.js` -3. Styles: `login.css`, `layout/header.css`, `layout/navigation.css` - -Update import paths in components to use relative paths within `src/shared/`. - -**Dependencies**: Task 1 - -**Completion Criteria**: -- [x] All shared components copied -- [x] All shared stores copied -- [x] All shared styles copied -- [x] Import paths updated to work from new location - ---- - -### Task 6: Create .env.example and public/index.html - -**Objective**: Create environment template and HTML entry point. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/.env.example` (create) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/public/index.html` (create) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/public/favicon.ico` (copy from reports-app if exists) - -**Description**: -1. Create `.env.example`: -``` -# API URLs (development) -VITE_REPORTS_API_URL=http://localhost:8001/api -VITE_DATA_ENTRY_API_URL=http://localhost:8003/api - -# Feature flags -VITE_FEATURE_REPORTS=true -VITE_FEATURE_DATA_ENTRY=true -``` - -2. Create `public/index.html` (or use Vite's default index.html in root): -```html - - - - - - ROA2WEB - Unified App - - - - - -
- - - -``` - -**Dependencies**: Task 1 - -**Completion Criteria**: -- [x] .env.example created -- [x] index.html created with proper meta tags -- [x] Build timestamp placeholder present - ---- - -## Phase 2: Module Migration (1 day) - -### Task 7: Migrate Reports Module Views - -**Objective**: Copy and adapt Reports views to the modules directory. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/views/DashboardView.vue` (copy and adapt) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/views/InvoicesView.vue` (copy and adapt) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/views/BankCashRegisterView.vue` (copy and adapt) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/views/TrialBalanceView.vue` (copy and adapt) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/views/TelegramView.vue` (copy and adapt) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/views/CacheStatsView.vue` (copy and adapt) - -**Description**: -Copy views from `reports-app/frontend/src/views/` to `src/modules/reports/views/`: -- DashboardView.vue -- InvoicesView.vue -- BankCashRegisterView.vue -- TrialBalanceView.vue -- TelegramView.vue -- CacheStatsView.vue - -**DO NOT copy LoginView.vue** - use shared LoginView. - -Update imports: -- Change `@/stores/xxx` to `@reports/stores/xxx` for module-specific stores -- Change `@/stores/auth` to `@shared/stores/auth` -- Change `@/stores/companies` to `@shared/stores/companies` -- Change `@/stores/accountingPeriod` to `@shared/stores/accountingPeriod` - -**Dependencies**: Task 4, Task 5 - -**Completion Criteria**: -- [x] All 6 views copied -- [x] Import paths updated -- [x] No references to old shared path (`../../../shared/`) - ---- - -### Task 8: Migrate Reports Module Stores - -**Objective**: Copy and adapt Reports stores to the modules directory. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/stores/dashboard.js` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/stores/invoices.js` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/stores/treasury.js` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/stores/trialBalance.js` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/stores/cacheStore.js` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/stores/index.js` (copy and adapt) - -**Description**: -Copy module-specific stores from `reports-app/frontend/src/stores/`: -- dashboard.js -- invoices.js -- treasury.js -- trialBalance.js -- cacheStore.js -- index.js (barrel export) - -**DO NOT copy auth.js, companies.js, accountingPeriod.js** - these are in shared. - -Update any internal imports to use module paths. - -**Dependencies**: Task 5 - -**Completion Criteria**: -- [x] All 6 store files copied -- [x] No references to shared stores (auth, companies, period) -- [x] index.js exports all module stores - ---- - -### Task 9: Create Reports Module API Service - -**Objective**: Create API service for Reports module with `/api/reports/` prefix. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/services/api.js` (create/adapt) - -**Description**: -Create or adapt the API service from `reports-app/frontend/src/services/api.js`: - -1. Base URL: `/api/reports` (proxied to port 8001) -2. Include auth token interceptor -3. Include response error handler -4. Export configured axios instance - -Example structure: -```javascript -import axios from 'axios' - -const api = axios.create({ - baseURL: '/api/reports', - headers: { 'Content-Type': 'application/json' } -}) - -// Request interceptor for auth token -api.interceptors.request.use((config) => { - const token = localStorage.getItem('access_token') - if (token) { - config.headers.Authorization = `Bearer ${token}` - } - return config -}) - -export default api -``` - -**Dependencies**: Task 1 - -**Completion Criteria**: -- [x] API service created with `/api/reports` base URL -- [x] Auth token interceptor configured -- [x] Error handling interceptor configured - ---- - -### Task 10: Migrate Data Entry Module Views - -**Objective**: Copy and adapt Data Entry views to the modules directory. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/data-entry/views/receipts/ReceiptsListView.vue` (copy and adapt) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/data-entry/views/receipts/ReceiptCreateView.vue` (copy and adapt) - -**Description**: -Copy views from `data-entry-app/frontend/src/views/receipts/`: -- ReceiptsListView.vue -- ReceiptCreateView.vue - -**DO NOT copy LoginView.vue** - use shared LoginView. - -Update imports: -- Change `@/stores/xxx` to `@data-entry/stores/xxx` for module-specific stores -- Change `@/stores/auth` to `@shared/stores/auth` -- Change `@/stores/companies` to `@shared/stores/companies` -- Change `@/stores/accountingPeriod` to `@shared/stores/accountingPeriod` -- Change `@/components/xxx` to `@data-entry/components/xxx` -- Change `@/services/api` to `@data-entry/services/api` - -**Dependencies**: Task 4, Task 5 - -**Completion Criteria**: -- [x] Both receipt views copied -- [x] Import paths updated -- [x] No references to old shared path - ---- - -### Task 11: Migrate Data Entry Module Components - -**Objective**: Copy Data Entry specific components (OCR). - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/data-entry/components/ocr/OCRUploadZone.vue` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/data-entry/components/ocr/OCRPreview.vue` (copy) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/data-entry/components/ocr/OCRConfidenceIndicator.vue` (copy) - -**Description**: -Copy OCR components from `data-entry-app/frontend/src/components/ocr/`: -- OCRUploadZone.vue -- OCRPreview.vue -- OCRConfidenceIndicator.vue - -Update any imports to use the new module paths. - -**Dependencies**: Task 1 - -**Completion Criteria**: -- [x] All 3 OCR components copied -- [x] Import paths updated -- [x] Components work independently - ---- - -### Task 12: Migrate Data Entry Module Stores - -**Objective**: Copy Data Entry specific stores. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/data-entry/stores/receiptsStore.js` (copy and adapt) - -**Description**: -Copy from `data-entry-app/frontend/src/stores/`: -- receiptsStore.js - -**DO NOT copy auth.js, companies.js, accountingPeriod.js** - these are in shared. - -Update imports: -- API service import to `@data-entry/services/api` - -**Dependencies**: Task 5 - -**Completion Criteria**: -- [x] receiptsStore.js copied -- [x] Import paths updated -- [x] No duplicate shared stores - ---- - -### Task 13: Create Data Entry Module API Service - -**Objective**: Create API service for Data Entry module with `/api/data-entry/` prefix. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/data-entry/services/api.js` (create/adapt) - -**Description**: -Create or adapt the API service from `data-entry-app/frontend/src/services/api.js`: - -1. Base URL: `/api/data-entry` (proxied to port 8003) -2. Include auth token interceptor -3. Include `X-Selected-Company` header injection -4. Include response error handler -5. Export configured axios instance - -Reference the existing `data-entry-app/frontend/src/services/api.js` for company header logic. - -**Dependencies**: Task 1 - -**Completion Criteria**: -- [x] API service created with `/api/data-entry` base URL -- [x] Auth token interceptor configured -- [x] Company header interceptor configured - ---- - -### Task 14: Merge and Adapt CSS from Data Entry - -**Objective**: Integrate any unique Data Entry CSS into the unified CSS system. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/assets/css/modules/data-entry.css` (create if needed) - -**Description**: -1. Review `data-entry-app/frontend/src/assets/css/main.css` for unique styles -2. Extract any styles not already covered by the reports-app CSS system -3. Add to a new `modules/data-entry.css` file if needed -4. Import in main.css - -Note: Data Entry currently uses `lara-light-blue` theme while Reports uses `saga-blue`. -Decision: Use `saga-blue` (reports-app theme) for consistency as per spec. - -**Dependencies**: Task 4 - -**Completion Criteria**: -- [x] Data Entry unique styles identified -- [x] Styles merged without conflicts -- [x] PrimeVue theme standardized to saga-blue - ---- - -## Phase 3: Routing & Navigation (0.5 days) - -### Task 15: Create Unified Router Configuration - -**Objective**: Create unified Vue Router with lazy loading for both modules. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/router/index.js` (create) - -**Description**: -Create unified router with: - -1. **Login route** (not lazy loaded - immediate access needed): -```javascript -{ - path: '/login', - name: 'Login', - component: () => import('@shared/components/LoginView.vue'), - meta: { requiresAuth: false, title: 'Autentificare - ROA2WEB' } -} -``` - -2. **Reports module routes** (lazy loaded): -```javascript -{ - path: '/reports', - component: () => import('@/modules/reports/ReportsLayout.vue'), - children: [ - { path: 'dashboard', name: 'Dashboard', component: () => import('@reports/views/DashboardView.vue') }, - { path: 'invoices', name: 'Invoices', component: () => import('@reports/views/InvoicesView.vue') }, - { path: 'bank-cash', name: 'BankCash', component: () => import('@reports/views/BankCashRegisterView.vue') }, - { path: 'trial-balance', name: 'TrialBalance', component: () => import('@reports/views/TrialBalanceView.vue') }, - { path: 'telegram', name: 'Telegram', component: () => import('@reports/views/TelegramView.vue') }, - { path: 'cache-stats', name: 'CacheStats', component: () => import('@reports/views/CacheStatsView.vue') }, - ] -} -``` - -3. **Data Entry module routes** (lazy loaded): -```javascript -{ - path: '/data-entry', - component: () => import('@/modules/data-entry/DataEntryLayout.vue'), - children: [ - { path: '', name: 'ReceiptsList', component: () => import('@data-entry/views/receipts/ReceiptsListView.vue') }, - { path: 'create', name: 'ReceiptCreate', component: () => import('@data-entry/views/receipts/ReceiptCreateView.vue') }, - { path: ':id', name: 'ReceiptDetail', component: () => import('@data-entry/views/receipts/ReceiptCreateView.vue') }, - { path: ':id/edit', name: 'ReceiptEdit', component: () => import('@data-entry/views/receipts/ReceiptCreateView.vue') }, - ] -} -``` - -4. **Redirects**: -- `/` -> `/reports/dashboard` -- `/:pathMatch(.*)*` -> `/reports/dashboard` - -5. **Navigation guards**: -- Check authentication before protected routes -- Set page title from route meta -- Scroll to top after navigation - -**Dependencies**: Task 7, Task 10 - -**Completion Criteria**: -- [x] All routes defined with lazy loading -- [x] Navigation guards implemented -- [x] Redirects configured -- [x] Page titles set from meta - ---- - -### Task 16: Create Menu Configuration - -**Objective**: Create unified menu configuration for all modules. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/config/menu.js` (create) - -**Description**: -Create menu configuration with sections: - -```javascript -export const menuSections = [ - { - title: 'Rapoarte', - items: [ - { to: '/reports/dashboard', icon: 'pi pi-home', label: 'Dashboard' }, - { to: '/reports/invoices', icon: 'pi pi-file', label: 'Facturi' }, - { to: '/reports/bank-cash', icon: 'pi pi-money-bill', label: 'Casa si Banca' }, - { to: '/reports/trial-balance', icon: 'pi pi-calculator', label: 'Balanta de Verificare' } - ] - }, - { - title: 'Introduceri Date', - items: [ - { to: '/data-entry', icon: 'pi pi-list', label: 'Lista Bonuri' }, - { to: '/data-entry/create', icon: 'pi pi-plus', label: 'Bon Nou' } - ] - }, - { - title: 'Sistem', - items: [ - { to: '/reports/telegram', icon: 'pi pi-telegram', label: 'Telegram Bot' }, - { to: '/reports/cache-stats', icon: 'pi pi-chart-bar', label: 'Statistici Cache' } - ] - } -] -``` - -**Dependencies**: None - -**Completion Criteria**: -- [x] Menu configuration created -- [x] All routes represented -- [x] Icons assigned correctly - ---- - -### Task 17: Create Feature Flags Configuration - -**Objective**: Create feature flags for module enable/disable. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/config/features.js` (create) - -**Description**: -Create feature flags configuration: - -```javascript -export const features = { - reports: { - enabled: import.meta.env.VITE_FEATURE_REPORTS !== 'false', - modules: { - dashboard: true, - invoices: true, - bankCash: true, - trialBalance: true, - telegram: true, - cacheStats: true - } - }, - dataEntry: { - enabled: import.meta.env.VITE_FEATURE_DATA_ENTRY !== 'false', - modules: { - receipts: true, - ocr: true - } - } -} - -export function isFeatureEnabled(module, subModule = null) { - if (!features[module]?.enabled) return false - if (subModule && !features[module]?.modules?.[subModule]) return false - return true -} - -export function getEnabledMenuSections(menuSections) { - return menuSections.filter(section => { - if (section.title === 'Rapoarte') return features.reports.enabled - if (section.title === 'Introduceri Date') return features.dataEntry.enabled - return true // System section always visible - }) -} -``` - -**Dependencies**: Task 16 - -**Completion Criteria**: -- [x] Feature flags created -- [x] Environment variable support -- [x] Helper functions for filtering menu - ---- - -### Task 18: Create App.vue Root Component - -**Objective**: Create unified App.vue with menu integration. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/App.vue` (create) - -**Description**: -Create App.vue that: - -1. Shows AppHeader and SlideMenu when authenticated -2. Uses unified menu configuration from `config/menu.js` -3. Filters menu based on feature flags -4. Handles company/period changes -5. Handles user logout (clears all stores) -6. Uses Toast and ConfirmDialog globally - -Reference `reports-app/frontend/src/App.vue` for structure but adapt for: -- Using `@shared/` components -- Using unified menu -- Using shared stores via aliases - -**Dependencies**: Task 5, Task 15, Task 16, Task 17 - -**Completion Criteria**: -- [x] App.vue created -- [x] Header and SlideMenu integrated -- [x] Menu sections from config -- [x] Company/period handlers work -- [x] Logout clears all stores - ---- - -### Task 19: Create main.js Entry Point - -**Objective**: Create unified main.js with PrimeVue setup. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/main.js` (create) - -**Description**: -Create main.js that: - -1. Creates Vue app -2. Sets up Pinia store -3. Sets up Vue Router -4. Configures PrimeVue with `saga-blue` theme -5. Registers ToastService, ConfirmationService -6. Registers common PrimeVue components globally -7. Imports unified CSS (`./assets/css/main.css`) - -Merge component registrations from both apps: -- From reports: Button, InputText, Password, DataTable, Column, Card, Toast, ConfirmDialog, Menu, Menubar, Badge, Tag, Dropdown, AutoComplete, Calendar, ProgressSpinner, Dialog -- From data-entry: InputNumber, Textarea, FileUpload, Image, TabView, TabPanel, Checkbox, RadioButton, Toolbar, Divider, Message - -**Dependencies**: Task 2, Task 4, Task 15 - -**Completion Criteria**: -- [x] main.js created -- [x] All PrimeVue components registered -- [x] PrimeVue theme set to saga-blue -- [x] CSS imports correct -- [x] App mounts successfully - ---- - -## Phase 4: Error Boundaries & Resilience (0.25 days) - -### Task 20: Create ErrorBoundary Component - -**Objective**: Create reusable error boundary component. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/components/ErrorBoundary.vue` (create) - -**Description**: -Create ErrorBoundary.vue component that: - -1. Uses `onErrorCaptured` to catch child component errors -2. Displays user-friendly error message in Romanian -3. Provides "Retry" button (reloads page) -4. Provides "Go to Dashboard" button -5. Logs error details to console -6. Returns `false` from onErrorCaptured to prevent propagation - -Template structure: -```vue - -``` - -**Dependencies**: Task 5 - -**Completion Criteria**: -- [x] ErrorBoundary component created -- [x] Catches errors from children -- [x] Shows user-friendly message -- [x] Retry and navigation buttons work - ---- - -### Task 21: Create ReportsLayout Module Wrapper - -**Objective**: Create layout wrapper with error boundary for Reports module. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/reports/ReportsLayout.vue` (create) - -**Description**: -Create ReportsLayout.vue that wraps module routes with ErrorBoundary: - -```vue - - - -``` - -This ensures errors in Reports views don't crash the entire app. - -**Dependencies**: Task 20 - -**Completion Criteria**: -- [x] ReportsLayout created -- [x] ErrorBoundary wraps router-view -- [x] Module name set correctly - ---- - -### Task 22: Create DataEntryLayout Module Wrapper - -**Objective**: Create layout wrapper with error boundary for Data Entry module. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/modules/data-entry/DataEntryLayout.vue` (create) - -**Description**: -Create DataEntryLayout.vue that wraps module routes with ErrorBoundary: - -```vue - - - -``` - -**Dependencies**: Task 20 - -**Completion Criteria**: -- [x] DataEntryLayout created -- [x] ErrorBoundary wraps router-view -- [x] Module name set correctly - ---- - -### Task 23: Add Loading States for Lazy Routes - -**Objective**: Add loading indicators during module loading. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/components/ModuleLoading.vue` (create) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/router/index.js` (update) - -**Description**: -1. Create ModuleLoading.vue component with ProgressSpinner -2. Configure router to show loading state during async component loading - -Using Vue Router's built-in async component handling: -```javascript -component: defineAsyncComponent({ - loader: () => import('@reports/views/DashboardView.vue'), - loadingComponent: ModuleLoading, - delay: 200, - timeout: 10000 -}) -``` - -Or use Suspense in layout components. - -**Dependencies**: Task 15, Task 20 - -**Completion Criteria**: -- [x] Loading component created -- [x] Shown during module lazy loading -- [x] Reasonable delay before showing - ---- - -## Phase 5: Build & Deployment (0.25 days) - -### Task 24: Create IIS web.config for Production - -**Objective**: Create web.config with URL rewrite rules for IIS deployment. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/public/web.config` (create) - -**Description**: -Create web.config with: - -1. **URL Rewrite Rules**: - - `/api/reports/*` -> `http://localhost:8001/api/*` - - `/api/data-entry/*` -> `http://localhost:8003/api/*` - - `/uploads/*` -> `http://localhost:8003/uploads/*` - - SPA fallback: all other routes -> `/index.html` - -2. **Static file handling** -3. **Cache headers for assets** -4. **MIME types for modern files** - -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -**Dependencies**: None - -**Completion Criteria**: -- [x] web.config created -- [x] API proxy rules correct -- [x] SPA fallback configured - ---- - -### Task 25: Verify Build and Bundle Splitting - -**Objective**: Run production build and verify output. - -**Files**: -- (verification task, no file modifications) - -**Description**: -1. Run `npm run build` -2. Verify `dist/` output: - - `assets/vendor-core.[hash].js` (vue, router, pinia) - - `assets/vendor-primevue.[hash].js` (primevue components) - - `assets/vendor-utils.[hash].js` (axios, date-fns) - - `assets/vendor-charts.[hash].js` (chart.js - lazy) - - `assets/vendor-export.[hash].js` (xlsx, jspdf - lazy) - - Module chunks for reports and data-entry - - CSS chunk - -3. Check total bundle size <= sum of old apps -4. Verify source maps generated - -**Dependencies**: Tasks 1-23 - -**Completion Criteria**: -- [x] Build completes without errors -- [x] Expected chunks generated -- [x] Bundle size acceptable -- [x] Source maps present - ---- - -### Task 26: Create README Documentation - -**Objective**: Create README for the unified app. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/unified-app-README.md` (create, will be moved to README.md later) - -**Description**: -Create documentation covering: - -1. **Overview**: Unified SPA combining Reports and Data Entry -2. **Quick Start**: - - `npm install` - - `npm run dev` (requires both backends running) - - `npm run build` -3. **Architecture**: - - Module structure - - Shared components - - Error boundaries -4. **URL Structure** -5. **Deployment**: - - IIS configuration - - Proxy setup -6. **Feature Flags** -7. **Development Guide**: - - Adding new routes - - Adding new components - - CSS system reference - -**Dependencies**: None - -**Completion Criteria**: -- [x] README covers all sections -- [x] Quick start instructions work -- [x] Architecture explained clearly - ---- - -### Task 27: Update Shared Stores for Dual API Support - -**Objective**: Ensure shared stores work with both API endpoints. - -**Files**: -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/stores/auth.js` (update) -- `/mnt/e/proiecte/ab-worktrees/roa2web-unified-app/src/shared/stores/companies.js` (update) - -**Description**: -The shared stores (auth, companies) need to work with both backends. Since authentication is handled by the Reports backend (port 8001), update: - -1. **auth.js**: Use `/api/reports/auth/` for login/logout -2. **companies.js**: Use `/api/reports/companies/` for company list - -Ensure the API base URL is correctly configured for the unified app's proxy setup. - -**Dependencies**: Task 5, Task 3 - -**Completion Criteria**: -- [x] Auth store uses correct API path -- [x] Companies store uses correct API path -- [x] Login/logout flow works - ---- - -### Task 28: End-to-End Integration Test - -**Objective**: Verify the complete unified app works. - -**Files**: -- (verification task, manual testing) - -**Description**: -Manual testing checklist: - -1. **Dev Server**: - - [ ] `npm run dev` starts on port 3000 - - [ ] No console errors on load - -2. **Login Flow**: - - [ ] Navigate to `/login` - - [ ] Login with valid credentials - - [ ] Redirected to `/reports/dashboard` - -3. **Reports Module**: - - [ ] Dashboard loads with data - - [ ] Navigate to Invoices - - [ ] Navigate to Bank/Cash - - [ ] Navigate to Trial Balance - - [ ] Navigate to Telegram - - [ ] Navigate to Cache Stats - -4. **Data Entry Module**: - - [ ] Navigate to `/data-entry` (Receipts List) - - [ ] Navigate to `/data-entry/create` (Create Receipt) - - [ ] Company selector preserved from Reports - -5. **Module Switching**: - - [ ] Switch from Reports to Data Entry - - [ ] Switch from Data Entry to Reports - - [ ] Selected company/period preserved - -6. **Error Boundary**: - - [ ] Intentionally break a component - - [ ] Error shows in that module only - - [ ] Other module still works - -7. **Logout**: - - [ ] Logout works - - [ ] Redirected to login - - [ ] All stores cleared - -**Dependencies**: All previous tasks - -**Completion Criteria**: -- [ ] All manual tests pass -- [ ] No console errors -- [ ] No visual regressions - ---- - -## Testing Strategy - -### During Implementation -- After each task, verify no import/build errors -- Test changed components individually -- Check browser console for errors - -### Final Validation -- Run `npm run build` - must succeed -- Run `npm run preview` - test production build -- Test on different screen sizes (mobile, tablet, desktop) -- Verify lazy loading with Network tab (modules load on demand) - ---- - -## Risk Mitigation - -| Risk | Detection | Response | -|------|-----------|----------| -| CSS conflicts between modules | Visual testing shows wrong styles | Use scoped styles or module-specific CSS files | -| Import path errors | Build fails or runtime errors | Check all import aliases in vite.config.js | -| PrimeVue theme inconsistency | Different component styles | Ensure only saga-blue theme imported | -| Shared store contamination | Data appears in wrong module | Keep module stores separate, only use shared for auth/company/period | -| Lazy loading not working | Large initial bundle | Verify dynamic imports in router | -| Error boundary not catching | App crashes on error | Test with intentional errors | - ---- - -## Notes for Implementation - -1. **Always use absolute paths** from worktree root when creating files -2. **Test incrementally** - don't wait until the end to test -3. **Preserve existing functionality** - this is a migration, not a rewrite -4. **Reference existing files** - use reports-app as the primary reference -5. **Use saga-blue theme** consistently (reports-app theme) -6. **Login always uses Reports API** (port 8001) for authentication -7. **Keep module stores isolated** - no cross-module store dependencies diff --git a/.auto-build/specs/unified-app/spec.md b/.auto-build/specs/unified-app/spec.md deleted file mode 100644 index d86b782..0000000 --- a/.auto-build/specs/unified-app/spec.md +++ /dev/null @@ -1,1114 +0,0 @@ -# Feature: Unified App - Pragmatic Monolith Consolidation - -## Overview - -Consolidate the two separate frontend applications (Reports App and Data Entry App) into a single unified SPA with both modules accessible from one menu. This pragmatic monolith approach maintains module isolation while simplifying deployment and providing a unified user experience with a single build and single IIS site. - -**Status**: Implementation-Ready -**Complexity**: Medium -**Estimated Effort**: 2.5 days - ---- - -## Problem Statement - -### Current Issues -- Two separate frontend deployments in IIS (`/roa2web/` and `/data-entry/`) -- Complex deployment process (2 builds, 2 IIS configurations) -- CSS conflicts when using shared components (white text on white background) -- No unified navigation between modules -- Duplicate setup and configuration code - -### Benefits of Unification -- Simplified deployment (single build, single IIS site) -- Unified user experience with single menu -- Shared component consistency -- Module isolation preserved through error boundaries -- Optimized bundle loading via lazy loading - ---- - -## User Stories - -### Primary Users -- As a **user**, I want to access both Reports and Data Entry modules from a single menu so that I can navigate seamlessly between functionalities. -- As a **developer**, I want simplified deployment with a single build process so that I can reduce deployment complexity and maintenance overhead. -- As a **user**, I want the app to remain functional even if one module encounters an error so that my work is not interrupted. -- As an **administrator**, I want a single IIS site configuration so that I can manage deployment more easily. - ---- - -## Functional Requirements - -### Core Requirements - -#### 1. Unified Navigation -- Single top-level menu with sections for Reports and Data Entry modules -- Menu structure: - - **Rapoarte** (Reports) section with Dashboard, Invoices, Bank/Cash, Trial Balance - - **Introduceri Date** (Data Entry) section with Receipts List, New Receipt - - **Sistem** section with Telegram Bot, Cache Stats -- Active route highlighting -- Responsive slide menu (mobile + desktop) - -#### 2. Module Isolation -- Error boundary per module (Reports, Data Entry) -- Bug in one module does not crash the other -- Independent lazy loading per module -- Separate stores per module (no cross-contamination) - -#### 3. URL Structure -``` -/login → Login page -/ → Redirect to /reports/dashboard - -/reports/dashboard → Dashboard (Reports module) -/reports/invoices → Invoices view -/reports/bank-cash → Bank/Cash register view -/reports/trial-balance → Trial Balance view -/reports/telegram → Telegram Bot management -/reports/cache-stats → Cache statistics - -/data-entry → Receipts list (Data Entry module) -/data-entry/create → Create new receipt -/data-entry/:id → View receipt details -/data-entry/:id/edit → Edit receipt -``` - -#### 4. Shared Components Integration -- Use existing shared components from `shared/frontend/`: - - `LoginView.vue` - Login page - - `AppHeader.vue` - Top navigation bar - - `SlideMenu.vue` - Sidebar menu - - `CompanySelector.vue` - Company dropdown - - `PeriodSelector.vue` - Accounting period selector -- Maintain consistent styling and behavior across modules - -#### 5. Authentication & Authorization -- Unified JWT authentication -- Single login flow for both modules -- Preserve existing auth store factory pattern -- Session persistence across module navigation - -### Secondary Requirements - -#### 1. Feature Flags -- Ability to enable/disable modules via configuration -- Feature flag configuration in `src/config/features.js` -- UI hides disabled module menu items - -#### 2. Loading States -- Module-level loading spinners -- Skeleton screens for lazy-loaded routes -- Progress indication during module switching - -#### 3. Analytics & Monitoring -- Track module switching events -- Error boundary triggers logged -- Performance metrics per module - ---- - -## Technical Requirements - -### Project Structure - -``` -. -├── public/ -│ └── favicon.ico -├── src/ -│ ├── main.js # App entry point -│ ├── App.vue # Root component with unified menu -│ │ -│ ├── router/ -│ │ └── index.js # Unified router with lazy loading -│ │ -│ ├── modules/ -│ │ ├── reports/ # ISOLATED REPORTS MODULE -│ │ │ ├── ReportsLayout.vue # Error boundary wrapper -│ │ │ ├── views/ # Reports views (migrated) -│ │ │ │ ├── DashboardView.vue -│ │ │ │ ├── InvoicesView.vue -│ │ │ │ ├── BankCashRegisterView.vue -│ │ │ │ ├── TrialBalanceView.vue -│ │ │ │ ├── TelegramView.vue -│ │ │ │ └── CacheStatsView.vue -│ │ │ ├── stores/ # Module-specific stores -│ │ │ │ ├── dashboard.js -│ │ │ │ ├── invoices.js -│ │ │ │ ├── treasury.js -│ │ │ │ ├── trialBalance.js -│ │ │ │ └── cacheStore.js -│ │ │ └── services/ # Module API services -│ │ │ └── api.js # Reports API client -│ │ │ -│ │ └── data-entry/ # ISOLATED DATA ENTRY MODULE -│ │ ├── DataEntryLayout.vue # Error boundary wrapper -│ │ ├── views/ # Data Entry views (migrated) -│ │ │ └── receipts/ -│ │ │ ├── ReceiptsListView.vue -│ │ │ └── ReceiptCreateView.vue -│ │ ├── components/ # Module-specific components -│ │ │ └── ocr/ -│ │ │ ├── OCRUploadZone.vue -│ │ │ ├── OCRPreview.vue -│ │ │ └── OCRConfidenceIndicator.vue -│ │ ├── stores/ # Module-specific stores -│ │ │ └── receiptsStore.js -│ │ └── services/ # Module API services -│ │ └── api.js # Data Entry API client -│ │ -│ ├── shared/ # SHARED COMPONENTS & LOGIC -│ │ ├── components/ # Shared UI components -│ │ │ ├── layout/ -│ │ │ │ ├── AppHeader.vue -│ │ │ │ └── SlideMenu.vue -│ │ │ ├── LoginView.vue -│ │ │ ├── CompanySelector.vue -│ │ │ ├── PeriodSelector.vue -│ │ │ └── ErrorBoundary.vue # NEW: Error boundary component -│ │ ├── stores/ # Shared stores (factories) -│ │ │ ├── auth.js # Auth store factory -│ │ │ ├── companies.js # Companies store factory -│ │ │ └── accountingPeriod.js # Period store factory -│ │ └── styles/ # Shared CSS -│ │ ├── login.css -│ │ ├── layout/ -│ │ │ ├── header.css -│ │ │ └── navigation.css -│ │ └── shared.css -│ │ -│ ├── config/ -│ │ ├── menu.js # Unified menu configuration -│ │ └── features.js # Feature flags -│ │ -│ └── assets/ -│ └── css/ # Global CSS (from reports-app) -│ ├── core/ # Design tokens -│ ├── components/ # Reusable UI patterns -│ ├── patterns/ # Interactive patterns -│ ├── layout/ # Page structure -│ ├── utilities/ # Utility classes -│ └── vendor/ # PrimeVue overrides -│ -├── vite.config.js # Unified Vite config (root) -├── package.json # Merged dependencies (root) -├── .env.example # Environment template (root) -└── README.md # Unified app documentation (root) -``` - -### Files to Create - -| File | Purpose | -|------|---------| -| `package.json` | Merged dependencies from both apps | -| `vite.config.js` | Dual proxy config, lazy loading, base path `/` | -| `src/main.js` | App initialization, PrimeVue setup, global components | -| `src/App.vue` | Root component with unified menu | -| `src/router/index.js` | Unified router with lazy loading | -| `src/config/menu.js` | Menu configuration | -| `src/config/features.js` | Feature flags | -| `src/shared/components/ErrorBoundary.vue` | Error boundary component | -| `src/modules/reports/ReportsLayout.vue` | Reports error boundary wrapper | -| `src/modules/data-entry/DataEntryLayout.vue` | Data Entry error boundary wrapper | -| `.env.example` | Environment variables template | -| `README.md` | Documentation | - -### Files to Migrate - -#### From `reports-app/frontend/` - -**Views** (→ `modules/reports/views/`): -- DashboardView.vue -- InvoicesView.vue -- BankCashRegisterView.vue -- TrialBalanceView.vue -- TelegramView.vue -- CacheStatsView.vue -- ~~LoginView.vue~~ (use shared) - -**Stores** (→ `modules/reports/stores/`): -- dashboard.js -- invoices.js -- treasury.js -- trialBalance.js -- cacheStore.js -- ~~auth.js~~ (use shared) -- ~~companies.js~~ (use shared) -- ~~accountingPeriod.js~~ (use shared) - -**Services** (→ `modules/reports/services/`): -- api.js (adapt for `/api/reports/` prefix) - -**CSS** (→ `assets/css/`): -- Copy entire `src/assets/css/` directory structure -- All design tokens, components, patterns, utilities - -#### From `data-entry-app/frontend/` - -**Views** (→ `modules/data-entry/views/receipts/`): -- ReceiptsListView.vue -- ReceiptCreateView.vue -- ~~LoginView.vue~~ (use shared) - -**Components** (→ `modules/data-entry/components/`): -- ocr/OCRUploadZone.vue -- ocr/OCRPreview.vue -- ocr/OCRConfidenceIndicator.vue - -**Stores** (→ `modules/data-entry/stores/`): -- receiptsStore.js -- ~~auth.js~~ (use shared) -- ~~companies.js~~ (use shared) -- ~~accountingPeriod.js~~ (use shared) - -**Services** (→ `modules/data-entry/services/`): -- api.js (adapt for `/api/data-entry/` prefix) - -**CSS** (→ merge with `assets/css/`): -- main.css (merge with reports-app main.css) -- Any component-specific styles - -#### From `shared/frontend/` - -**Components** (→ `shared/components/`): -- LoginView.vue -- layout/AppHeader.vue -- layout/SlideMenu.vue -- CompanySelector.vue -- PeriodSelector.vue - -**Stores** (→ `shared/stores/`): -- auth.js (factory) -- companies.js (factory) -- accountingPeriod.js (factory) - -**Styles** (→ `shared/styles/`): -- login.css -- layout/header.css -- layout/navigation.css - -### Dependencies - -#### Unified package.json (root directory) - -Merge all dependencies from both apps: - -**From reports-app**: -- axios: ^1.6.2 -- chart.js: ^4.5.0 -- date-fns: ^2.30.0 -- jspdf: ^3.0.1 -- jspdf-autotable: ^5.0.2 -- pinia: ^2.1.7 -- primeicons: ^6.0.1 -- primevue: ^3.46.0 -- qrcode.vue: ^3.6.0 -- vue: ^3.4.0 -- vue-chartjs: ^5.3.2 -- vue-router: ^4.2.5 -- xlsx: ^0.18.5 - -**From data-entry-app**: -- All duplicates already covered -- Additional: @primevue/themes: ^4.0.0 (optional, may remove) - -**DevDependencies**: -- @playwright/test: ^1.54.2 -- @vitejs/plugin-vue: ^5.0.0 -- eslint: ^8.56.0 -- eslint-plugin-vue: ^9.20.0 -- prettier: ^3.1.1 -- vite: ^5.0.10 - -### API Routing - -#### Vite Dev Server Proxy - -```javascript -// vite.config.js -server: { - port: 3000, - proxy: { - '/api/reports': { - target: 'http://localhost:8001', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api\/reports/, '/api') - }, - '/api/data-entry': { - target: 'http://localhost:8003', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api\/data-entry/, '/api') - }, - '/uploads': { - target: 'http://localhost:8003', - changeOrigin: true - } - } -} -``` - -#### IIS Production Proxy - -```xml - - - - - - - - - - - - - - - -``` - -### Build Configuration - -#### Vite Config - Lazy Loading & Bundle Splitting - -```javascript -// vite.config.js -export default defineConfig({ - plugins: [vue(), htmlTimestampPlugin()], - base: process.env.NODE_ENV === 'production' ? '/' : '/', - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - '@shared': fileURLToPath(new URL('./src/shared', import.meta.url)), - '@reports': fileURLToPath(new URL('./src/modules/reports', import.meta.url)), - '@data-entry': fileURLToPath(new URL('./src/modules/data-entry', import.meta.url)) - }, - dedupe: ['vue', 'vue-router', 'pinia', 'primevue'] - }, - build: { - outDir: 'dist', - sourcemap: true, - rollupOptions: { - output: { - manualChunks: { - // Core vendors - 'vendor-core': ['vue', 'vue-router', 'pinia'], - 'vendor-primevue': ['primevue/config', 'primevue/button', 'primevue/datatable'], - 'vendor-utils': ['axios', 'date-fns'], - - // Charts (reports only) - 'vendor-charts': ['chart.js', 'vue-chartjs'], - - // Excel/PDF exports - 'vendor-export': ['xlsx', 'jspdf', 'jspdf-autotable'], - - // Module-specific chunks (lazy loaded) - // Reports module loaded via dynamic import - // Data Entry module loaded via dynamic import - }, - entryFileNames: `assets/[name].[hash].js`, - chunkFileNames: `assets/[name].[hash].js`, - assetFileNames: `assets/[name].[hash].[ext]` - } - } - } -}) -``` - -**Expected Build Output**: -``` -dist/ -├── index.html -├── assets/ -│ ├── vendor-core.[hash].js (~150KB) - Vue, Router, Pinia -│ ├── vendor-primevue.[hash].js (~200KB) - PrimeVue components -│ ├── vendor-utils.[hash].js (~80KB) - Axios, date-fns -│ ├── vendor-charts.[hash].js (~150KB) - Chart.js (lazy) -│ ├── vendor-export.[hash].js (~200KB) - XLSX, jsPDF (lazy) -│ ├── reports.[hash].js (~150KB) - Reports module (lazy) -│ ├── data-entry.[hash].js (~100KB) - Data Entry module (lazy) -│ ├── main.[hash].js (~50KB) - App shell -│ └── main.[hash].css (~80KB) - Global CSS -``` - ---- - -## Design Decisions - -### Approach: Pragmatic Monolith - -**Why NOT Micro-frontends (Module Federation / Single-SPA)?** - -| Micro-Frontend Criterion | ROA2WEB Reality | Justification | -|-------------------------|-----------------|---------------| -| 20+ developers on separate teams | 1 developer | Overkill - no team coordination needed | -| Deploy multiple times per day | Weekly deploys | No benefit from independent deployments | -| Millions of users, high scale | 1-5 concurrent users | No scaling justification | -| Different frameworks per team | Vue 3 only | No technical diversity | -| Independent release cycles | Single release cycle | Adds unnecessary complexity | - -**Pragmatic Monolith Benefits**: -- Simple build process (single `npm run build`) -- Shared dependencies (smaller bundle size) -- Error boundaries provide 50-70% isolation (close to separate apps) -- Lazy loading prevents loading unused modules -- Feature flags allow disabling modules without redeploy - -### Error Boundary Implementation - -**ErrorBoundary.vue Component**: -```vue - - - -``` - -**Usage in Module Layouts**: -```vue - - - - - -``` - -### Lazy Loading Strategy - -**Router Configuration**: -```javascript -const routes = [ - { - path: '/login', - name: 'Login', - component: LoginView, - meta: { requiresAuth: false } - }, - { - path: '/reports', - component: () => import('@/modules/reports/ReportsLayout.vue'), - children: [ - { - path: 'dashboard', - name: 'Dashboard', - component: () => import('@/modules/reports/views/DashboardView.vue'), - meta: { requiresAuth: true, title: 'Dashboard - ROA Reports' } - }, - { - path: 'invoices', - name: 'Invoices', - component: () => import('@/modules/reports/views/InvoicesView.vue'), - meta: { requiresAuth: true, title: 'Facturi - ROA Reports' } - }, - // ... more routes - ] - }, - { - path: '/data-entry', - component: () => import('@/modules/data-entry/DataEntryLayout.vue'), - children: [ - { - path: '', - name: 'ReceiptsList', - component: () => import('@/modules/data-entry/views/receipts/ReceiptsListView.vue'), - meta: { requiresAuth: true, title: 'Lista Bonuri' } - }, - // ... more routes - ] - }, - { - path: '/', - redirect: '/reports/dashboard' - } -] -``` - -### Menu Configuration - -**config/menu.js**: -```javascript -export const menuSections = [ - { - title: 'Rapoarte', - items: [ - { to: '/reports/dashboard', icon: 'pi pi-home', label: 'Dashboard' }, - { to: '/reports/invoices', icon: 'pi pi-file', label: 'Facturi' }, - { to: '/reports/bank-cash', icon: 'pi pi-money-bill', label: 'Casa și Banca' }, - { to: '/reports/trial-balance', icon: 'pi pi-calculator', label: 'Balanță de Verificare' } - ] - }, - { - title: 'Introduceri Date', - items: [ - { to: '/data-entry', icon: 'pi pi-list', label: 'Lista Bonuri' }, - { to: '/data-entry/create', icon: 'pi pi-plus', label: 'Bon Nou' } - ] - }, - { - title: 'Sistem', - items: [ - { to: '/reports/telegram', icon: 'pi pi-telegram', label: 'Telegram Bot' }, - { to: '/reports/cache-stats', icon: 'pi pi-chart-bar', label: 'Statistici Cache' } - ] - } -] -``` - -### Feature Flags - -**config/features.js**: -```javascript -export const features = { - reports: { - enabled: true, - modules: { - dashboard: true, - invoices: true, - bankCash: true, - trialBalance: true, - telegram: true, - cacheStats: true - } - }, - dataEntry: { - enabled: true, - modules: { - receipts: true, - ocr: true - } - } -} - -export function isFeatureEnabled(module, subModule = null) { - if (!features[module]?.enabled) return false - if (subModule && !features[module]?.modules?.[subModule]) return false - return true -} -``` - -### CSS Architecture - -**Preserve Reports App CSS System**: -- Use complete CSS structure from `reports-app/frontend/src/assets/css/` -- Design tokens in `core/tokens.css` -- Component patterns in `components/` -- PrimeVue overrides in `vendor/primevue-overrides.css` - -**Merge Data Entry CSS**: -- Extract unique data-entry styles -- Integrate into existing pattern system -- Ensure no conflicts with reports CSS - -**Shared Styles**: -- Import from `src/shared/styles/` for LoginView, AppHeader, SlideMenu -- Maintain consistency across modules - ---- - -## Acceptance Criteria - -### Functionality -- [ ] User can log in and access both Reports and Data Entry modules from unified menu -- [ ] All Reports views (Dashboard, Invoices, Bank/Cash, Trial Balance, Telegram, Cache Stats) work correctly -- [ ] All Data Entry views (Receipts List, Create Receipt, Edit Receipt) work correctly -- [ ] Navigation between modules preserves authentication and selected company/period -- [ ] Error in Reports module does not crash Data Entry module (and vice versa) -- [ ] Logout works correctly and clears all stores - -### Performance -- [ ] Initial page load < 2 seconds on 3G connection -- [ ] Module switching (Reports ↔ Data Entry) < 500ms (already loaded) or < 1s (first load) -- [ ] Build produces separate chunks for each module (reports.js, data-entry.js) -- [ ] Total bundle size ≤ sum of current apps (no regression) -- [ ] Lighthouse score ≥ 90 for Performance - -### Build & Deployment -- [ ] `npm run build` succeeds without errors -- [ ] Build output contains expected chunks (vendor-core, reports, data-entry) -- [ ] IIS deployment with single site works correctly -- [ ] API proxy routes correctly to both backends (8001, 8003) -- [ ] Production build works on Windows IIS -- [ ] Cache busting works (new builds force reload) - -### Error Handling -- [ ] Error boundary catches component errors -- [ ] Error boundary displays user-friendly message -- [ ] User can retry or navigate away from error -- [ ] Console logs error details for debugging -- [ ] Error in one module doesn't affect the other - -### Responsive Design -- [ ] Works on mobile (375px width) -- [ ] Works on tablet (768px width) -- [ ] Works on desktop (1920px width) -- [ ] Slide menu works on mobile -- [ ] Company/Period selectors work on mobile - -### Testing -- [ ] E2E tests pass for login flow -- [ ] E2E tests pass for Reports module navigation -- [ ] E2E tests pass for Data Entry module navigation -- [ ] E2E tests pass for module switching -- [ ] E2E tests verify error boundary isolation - ---- - -## Out of Scope - -The following are explicitly NOT included in this implementation: - -### Backend Changes -- No changes to `reports-app/backend/` (port 8001) -- No changes to `data-entry-app/backend/` (port 8003) -- Backends remain separate microservices - -### Shared Database -- Each backend keeps its own database (Oracle for Reports, SQLite for Data Entry) -- No database consolidation - -### API Consolidation -- APIs remain at separate ports (8001, 8003) -- No unified API gateway (using IIS/Vite proxy instead) - -### Telegram Bot -- Telegram bot remains in `reports-app/telegram-bot/` -- No changes to bot architecture - -### Advanced Features -- No server-side rendering (SSR) -- No progressive web app (PWA) features -- No offline mode -- No real-time updates (WebSockets) - -### Future Enhancements -- Module-level permissions (show/hide based on user role) -- Module analytics dashboard -- A/B testing framework -- Module versioning -- Micro-frontend migration (if team grows to 20+) - ---- - -## Risks and Mitigations - -| Risk | Likelihood | Impact | Mitigation | -|------|------------|--------|------------| -| **CSS conflicts between modules** | Medium | Medium | Use CSS modules or scoped styles; test thoroughly; maintain design token system from reports-app | -| **Large bundle size** | Medium | Medium | Implement aggressive code splitting; lazy load modules; use tree shaking; monitor with webpack-bundle-analyzer | -| **Error boundary not catching all errors** | Low | High | Test error scenarios thoroughly; add global error handler; log errors to monitoring service | -| **Deployment complexity on IIS** | Medium | High | Document IIS proxy configuration; create deployment scripts; test on staging first | -| **Store contamination between modules** | Low | High | Keep stores module-scoped; use namespaced modules; test isolation | -| **PrimeVue theme conflicts** | Low | Medium | Use single PrimeVue theme (saga-blue from reports-app); override in vendor CSS | -| **Breaking existing E2E tests** | High | Medium | Update test selectors; test both modules independently; add new tests for unified navigation | -| **Shared component changes breaking both modules** | Medium | High | Version shared components; add component tests; minimize changes to shared code | - ---- - -## Implementation Plan - -### Phase 1: Project Setup (0.5 days) - -**Tasks**: -1. Create directory structure in root (`.`) -2. Create `package.json` with merged dependencies -3. Create `vite.config.js` with dual proxy and lazy loading -4. Create `src/main.js` with PrimeVue setup -5. Create `.env.example` with environment variables -6. Copy `shared/frontend/` to `src/shared/` -7. Copy `reports-app/frontend/src/assets/css/` to `src/assets/css/` - -**Verification**: -- `npm install` succeeds -- `npm run dev` starts dev server on port 3000 -- No build errors - -### Phase 2: Module Migration (1 day) - -**Tasks**: -1. Create module structure (`modules/reports/`, `modules/data-entry/`) -2. Migrate Reports views to `modules/reports/views/` -3. Migrate Reports stores to `modules/reports/stores/` -4. Migrate Reports services, adapt API base URL to `/api/reports/` -5. Migrate Data Entry views to `modules/data-entry/views/` -6. Migrate Data Entry components to `modules/data-entry/components/` -7. Migrate Data Entry stores to `modules/data-entry/stores/` -8. Migrate Data Entry services, adapt API base URL to `/api/data-entry/` -9. Merge CSS from data-entry-app into main CSS system - -**Verification**: -- All views render without errors -- API calls route to correct backend -- No import errors - -### Phase 3: Routing & Navigation (0.5 days) - -**Tasks**: -1. Create unified router in `src/router/index.js` -2. Configure lazy loading for module layouts -3. Add authentication guard -4. Create `config/menu.js` with unified menu structure -5. Create `App.vue` with AppHeader and SlideMenu integration -6. Create `ReportsLayout.vue` with error boundary -7. Create `DataEntryLayout.vue` with error boundary -8. Create `ErrorBoundary.vue` component - -**Verification**: -- Navigation works between all routes -- Lazy loading chunks load correctly -- Error boundary catches and displays errors -- Menu highlights active route - -### Phase 4: Error Boundaries & Resilience (0.25 days) - -**Tasks**: -1. Test error boundary with intentional errors -2. Verify module isolation (error in one doesn't crash other) -3. Add global error handler -4. Test feature flags (enable/disable modules) -5. Add loading states for lazy-loaded routes - -**Verification**: -- Error boundary displays user-friendly message -- User can retry or navigate away -- Other module continues working -- Feature flags hide disabled modules - -### Phase 5: Build & Deployment (0.25 days) - -**Tasks**: -1. Run production build: `npm run build` -2. Verify bundle splitting (check `dist/assets/`) -3. Test production build locally: `npm run preview` -4. Create IIS web.config with URL rewrite rules -5. Deploy to staging IIS site -6. Test all routes and API calls on staging -7. Document deployment process - -**Verification**: -- Build succeeds without errors -- Separate chunks generated (vendor-core, reports, data-entry) -- IIS deployment works -- API proxy routes correctly -- All features work in production - ---- - -## Testing Strategy - -### Unit Tests -- Store actions and mutations -- Service API methods -- Utility functions -- Error boundary component - -### Integration Tests -- Router navigation -- Store integration with components -- API service integration -- Error boundary with child components - -### E2E Tests (Playwright) - -**Critical Paths**: -1. **Login Flow** - - Navigate to /login - - Enter credentials - - Verify redirect to /reports/dashboard - -2. **Reports Module Navigation** - - Navigate to each Reports view - - Verify data loads - - Test filters and actions - -3. **Data Entry Module Navigation** - - Navigate to receipts list - - Navigate to create receipt - - Verify form validation - -4. **Module Switching** - - Navigate from Reports to Data Entry - - Verify state persistence (company, period) - - Navigate back to Reports - -5. **Error Isolation** - - Trigger error in Reports module - - Verify Data Entry still works - - Retry error module - -6. **Logout** - - Logout from any module - - Verify redirect to login - - Verify stores cleared - -### Performance Tests -- Lighthouse audit (target: 90+ Performance) -- Bundle size analysis (webpack-bundle-analyzer) -- Load time measurement (initial load, module switching) - ---- - -## Rollback Plan - -### If Deployment Fails - -**Option 1: Keep Both Apps Running** (Zero Downtime) -- Leave existing `/roa2web/` and `/data-entry/` sites running -- Add new unified app at `/unified/` for testing -- Switch over when stable - -**Option 2: Quick Rollback** (15 minutes) -- Keep backup of current IIS configuration -- Keep backup of current builds in `dist-backup/` -- Restore IIS sites from backup -- Restore builds from backup - -### Git Strategy -- Create feature branch: `feature/unified-app-pragmatic-monolith` -- Tag last stable version before merge: `v1.0-pre-unified` -- Can revert to tag if needed: `git reset --hard v1.0-pre-unified` - -### Monitoring -- Check IIS logs: `C:\inetpub\logs\LogFiles\` -- Check application errors in browser console -- Monitor backend logs (both 8001 and 8003) -- Track user feedback in first 48 hours - ---- - -## Documentation Updates - -### Files to Create/Update - -1. **README.md** (in root directory) - - Project overview - - Setup instructions - - Development commands - - Deployment guide - - Architecture overview - -2. **CLAUDE.md** (root) - - Update architecture diagram - - Update deployment instructions - - Mark old apps as archived - - Document new root structure - -3. **DEPLOYMENT_GUIDE.md** - - Update IIS configuration - - Update build process - - Update URL structure - - Add rollback instructions - -4. **docs/ARCHITECTURE_SCHEMA.md** - - Update architecture diagrams - - Document module structure - - Add error boundary architecture - -5. **deployment/windows/README.md** - - Update deployment steps - - Update IIS configuration - - Update proxy rules - ---- - -## Post-Implementation Tasks - -### Archiving Old Apps - -After successful deployment and 1 week of stability: - -1. Archive old frontends: - ```bash - mv reports-app/frontend reports-app/frontend-archived - mv data-entry-app/frontend data-entry-app/frontend-archived - ``` - -2. Update start scripts to use root directory: - ```bash - # ./start-test.sh (update frontend path to root) - # ./start-data-entry.sh (update frontend path to root) - ``` - -3. Update CI/CD pipelines (if any) - -4. Document migration in CHANGELOG.md - -### Monitoring & Optimization - -**First Week**: -- Monitor error logs daily -- Track bundle load times -- Gather user feedback -- Fix critical bugs - -**First Month**: -- Optimize bundle sizes -- Add performance monitoring -- Consider further code splitting -- Evaluate feature flag usage - ---- - -## Dependencies on Other Work - -### Prerequisites -- None - can start immediately - -### Blocking -- No other work blocked by this - -### Nice to Have (Not Required) -- Updated E2E tests (can do after deployment) -- Performance monitoring setup (can add later) -- Analytics integration (can add later) - ---- - -## Success Metrics - -### Deployment Success -- ✅ Single IIS site running instead of 2 -- ✅ Single build command instead of 2 -- ✅ Zero downtime during deployment - -### User Experience -- ✅ 100% feature parity with old apps -- ✅ < 2s initial load time -- ✅ < 500ms module switching (cached) -- ✅ Zero user-reported bugs in first week - -### Technical -- ✅ Bundle size ≤ sum of old apps -- ✅ Error isolation working (test scenarios) -- ✅ All E2E tests passing -- ✅ Lighthouse score ≥ 90 - ---- - -## Open Questions - -1. **PrimeVue Theme Standardization**: Use `saga-blue` (reports-app) or `lara-light-blue` (data-entry-app)? - - **Recommendation**: Use `saga-blue` (reports-app is primary) - -2. **Feature Flag Storage**: Config file or environment variables? - - **Recommendation**: Config file for simplicity, env vars for production override - -3. **Module Activation Strategy**: All modules active by default or opt-in? - - **Recommendation**: All active by default (can disable via config) - -4. **Monitoring Solution**: Console logs only or add Sentry/similar? - - **Recommendation**: Console logs for MVP, add monitoring later - -5. **Progressive Enhancement**: Load Reports first (most used) or parallel? - - **Recommendation**: Load on-demand (lazy loading), whichever user navigates to first - ---- - -## Estimated Complexity - -**Medium Complexity** - Justification: - -**Factors Increasing Complexity**: -- Merging two apps with different structures -- Ensuring error isolation between modules -- CSS conflicts and PrimeVue theme differences -- IIS proxy configuration complexity -- Testing both modules thoroughly - -**Factors Decreasing Complexity**: -- No backend changes required -- Shared components already exist -- Clear architectural pattern (pragmatic monolith) -- Lazy loading well-supported by Vue Router -- Both apps use same tech stack (Vue 3, PrimeVue) - -**Time Estimate**: 2.5 days (as per implementation plan) - ---- - -## References - -### Related Documents -- `IMPLEMENTATION_PLAN_UNIFIED_APP.md` - Initial plan (this spec expands on it) -- `CLAUDE.md` - Project documentation (update after implementation) -- `docs/ONBOARDING_CSS.md` - CSS system guide -- `docs/CSS_PATTERNS.md` - Available CSS patterns -- `docs/ARCHITECTURE_SCHEMA.md` - Current architecture (update after) - -### Key Files to Reference During Implementation -- `reports-app/frontend/vite.config.js` - Proxy config, build settings (reference for new root `vite.config.js`) -- `reports-app/frontend/src/router/index.js` - Router patterns (reference for new `src/router/index.js`) -- `reports-app/frontend/src/App.vue` - AppHeader/SlideMenu integration (reference for new `src/App.vue`) -- `data-entry-app/frontend/src/App.vue` - Alternative integration pattern -- `shared/frontend/components/layout/AppHeader.vue` - Header API (copy to `src/shared/components/layout/`) -- `shared/frontend/components/layout/SlideMenu.vue` - Menu API (copy to `src/shared/components/layout/`) - ---- - -## Handover Notes - -This specification is **implementation-ready**. All technical decisions have been made. The implementation plan provides a clear path with verification steps at each phase. - -**Critical Files to Create First** (in root directory): -1. `package.json` - Merged dependencies -2. `vite.config.js` - Dual proxy config -3. `src/router/index.js` - Unified router with lazy loading -4. `src/shared/components/ErrorBoundary.vue` - Error isolation - -**After Implementation**: -- Archive old frontends after 1 week of stability -- Update all documentation (CLAUDE.md, DEPLOYMENT_GUIDE.md) -- Consider adding monitoring/analytics -- Optimize bundle sizes based on real usage - ---- - -**Specification Version**: 1.0 -**Created**: 2025-12-22 -**Status**: Ready for Implementation -**Estimated Effort**: 2.5 days diff --git a/.auto-build/specs/unified-app/status.json b/.auto-build/specs/unified-app/status.json deleted file mode 100644 index 1e9167f..0000000 --- a/.auto-build/specs/unified-app/status.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "feature": "unified-app", - "status": "IMPLEMENTATION_COMPLETE", - "created": "2025-12-22T01:09:00Z", - "updated": "2025-12-23T21:25:00Z", - "complexity": "medium", - "estimated_effort": "2.5 days", - "worktree": "/mnt/e/proiecte/ab-worktrees/roa2web-unified-app", - "branch": "feature/ab-unified-app", - "totalTasks": 28, - "currentTask": 28, - "completedTasks": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], - "tasksCompleted": 28, - "testing": { - "playwrightTestsCreated": 6, - "testsExecuted": 40, - "testsPassed": 4, - "testsBlocked": 36, - "blockingReason": "Backend authentication error (Oracle DB/SSH tunnel configuration)", - "frontendQuality": "100/100 - EXCELLENT", - "visualRegressionIssues": 0, - "consoleErrors": 0, - "consoleWarnings": 0 - }, - "history": [ - { - "status": "SPEC_DRAFT", - "at": "2025-12-22T01:09:00Z" - }, - { - "status": "SPEC_COMPLETE", - "at": "2025-12-22T01:13:00Z" - }, - { - "status": "PLANNING", - "at": "2025-12-22T09:30:00Z" - }, - { - "status": "PLANNING_COMPLETE", - "at": "2025-12-22T09:35:00Z" - }, - { - "status": "IMPLEMENTING", - "at": "2025-12-22T18:25:00Z", - "task": 1 - }, - { - "status": "IMPLEMENTING", - "at": "2025-12-22T20:45:00Z", - "task": 28, - "started": true - }, - { - "status": "TESTING", - "at": "2025-12-23T21:00:00Z", - "description": "Comprehensive Playwright E2E testing" - }, - { - "status": "IMPLEMENTATION_COMPLETE", - "at": "2025-12-23T21:25:00Z", - "task": 28, - "completed": true, - "note": "All 28 tasks completed. Frontend fully functional. Backend auth requires Oracle/SSH configuration." - } - ], - "files": { - "spec": "spec.md", - "summary": "SUMMARY.md", - "critical_files": "critical-files.md", - "migration_checklist": "MIGRATION_CHECKLIST.md", - "plan": "plan.md", - "test_report": "../../UNIFIED_APP_TEST_REPORT.md" - }, - "stats": { - "files_to_create": 15, - "files_to_migrate": 20, - "css_files_to_copy": 30, - "total_files_affected": 65 - }, - "deployment": { - "ready": true, - "frontendStatus": "PRODUCTION_READY", - "backendStatus": "NEEDS_CONFIGURATION", - "blockers": ["Oracle DB authentication", "SSH tunnel configuration"] - } -} diff --git a/.gitignore b/.gitignore index 366ce95..9f94141 100644 --- a/.gitignore +++ b/.gitignore @@ -249,17 +249,17 @@ quick_test.* .dockerignore .eggs/ .eggs/ -.env +# Environment files - ignore ALL .env files (security best practice) .env .env.* .env.*.local .env.local -.env.production -.env.production -.env.test -# Allow .env.example files (configuration templates) + +# Allow only .env.example templates (no credentials) !.env.example !**/.env.example +!.env.*.example +!**/.env.*.example # Allow .dockerignore files (Docker build configuration) !.dockerignore !**/.dockerignore @@ -428,9 +428,6 @@ playwright-report/ profile_default/ quick_test.* quick_test.* -# Removed duplicate - already covered in deployment section above -roa2web/reports-app/telegram-bot/data/*.db -roa2web/reports-app/telegram-bot/data/*.db-* run_tests.* run_tests.* scan_*.json @@ -512,6 +509,16 @@ secrets-backup/**/.env.* !secrets-backup/**/*.gpg .playwright-mcp/* -data-entry-app/backend/data/* # Auto-Build local data (worktrees, cache) .auto-build-data/ + +# ============================================================================ +# 🏗️ ULTRATHIN MONOLITH BACKEND DATA - DO NOT COMMIT +# ============================================================================ +# Backend unified data directories (cache, receipts, telegram) +backend/data/cache/*.db +backend/data/receipts/*.db +backend/data/telegram/*.db +backend/data/receipts/uploads/* +!backend/data/*/.gitkeep + diff --git a/CLAUDE.md b/CLAUDE.md index e286ed7..1ccb9ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,9 +4,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## 🚀 Project Overview -**ROA2WEB** - Modern ERP Application with two main modules: -1. **Reports App** (`reports-app/`) - Read-only reports from Oracle (raportări) -2. **Data Entry App** (`data-entry-app/`) - Data input with approval workflow (introduceri date) +**ROA2WEB** - Modern ERP Application with ultrathin monolith architecture: +1. **Reports Module** (`backend/modules/reports/`) - Read-only reports from Oracle (raportări) +2. **Data Entry Module** (`backend/modules/data_entry/`) - Data input with approval workflow (introduceri date) +3. **Telegram Bot Module** (`backend/modules/telegram/`) - Telegram bot integration **Main Branch**: `main` (use for PRs) **Working Directory**: Repository root @@ -15,57 +16,74 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co --- -## 📁 Application-Specific Instructions +## 📁 Module-Specific Instructions -> **IMPORTANT**: When working on a specific application, ALWAYS read its dedicated CLAUDE.md first! +> **IMPORTANT**: When working on a specific module, read its documentation first! -| Application | CLAUDE.md Location | Description | -|-------------|-------------------|-------------| -| **Data Entry** | `data-entry-app/CLAUDE.md` | Bonuri fiscale, chitanțe, workflow aprobare | +| Module | Documentation Location | Description | +|--------|----------------------|-------------| +| **Data Entry** | `docs/data-entry/DATA-ENTRY-MODULE.md` | Bonuri fiscale, chitanțe, workflow aprobare | | **Reports** | This file (below) | Rapoarte Oracle read-only | -| **Telegram Bot** | `reports-app/telegram-bot/README.md` | Bot Telegram | +| **Telegram Bot** | `docs/telegram/README.md` | Bot Telegram | ### When to Use Which Instructions -**Working on `data-entry-app/`**: -→ **FIRST read `data-entry-app/CLAUDE.md`** for: +**Working on Data Entry** (`backend/modules/data_entry/` or `src/modules/data-entry/`): +→ **FIRST read `docs/data-entry/DATA-ENTRY-MODULE.md`** for: - SQLModel + Alembic patterns (NOT Oracle) - SQLite database (NOT Oracle pool) - Workflow states (DRAFT → PENDING → APPROVED) - Receipt/Attachment/AccountingEntry models - Expense types and auto-generation logic + - Oracle nomenclatures integration -**Working on `reports-app/` or `shared/`**: +**Working on Reports Module** (`backend/modules/reports/` or `src/modules/reports/`): → Use instructions from this file (below) **Working on shared components** (`shared/auth/`, `shared/database/`, `shared/frontend/`): -→ These are used by BOTH apps - be careful with changes! +→ These are used by ALL modules - be careful with changes! → `shared/frontend/` contains: LoginView.vue, auth store factory, login styles --- ## 🏗️ Architecture -### Microservices Structure +> **Important Architecture Decisions:** See `docs/ARCHITECTURE-DECISIONS.md` for critical decisions about: +> - Why single worker (`--workers 1`) is required for Telegram bot +> - Ultrathin monolith vs microservices rationale +> - IIS sub-application deployment strategy +> - Performance characteristics and scaling considerations + +### Ultrathin Monolith Structure ``` . -├── shared/ # Shared components (DB pool, auth, frontend) -│ ├── database/ # Oracle pool (used by both apps) -│ ├── auth/ # JWT auth (used by both apps) -│ └── frontend/ # Shared Vue components, stores, styles -│ ├── components/ # LoginView.vue -│ ├── stores/ # auth.js (Pinia store factory) -│ └── styles/ # login.css +├── backend/ # Unified FastAPI backend (port 8000/8001) +│ ├── modules/ # Business logic modules +│ │ ├── reports/ # Reports module (Oracle read-only) +│ │ ├── data_entry/ # Data entry module (SQLite + workflow) +│ │ ├── telegram/ # Telegram bot module +│ │ └── data/ # Shared data (telegram_bot.db) +│ ├── config.py # Centralized configuration +│ └── main.py # FastAPI app entry point │ -├── reports-app/ # READ-ONLY reports from Oracle -│ ├── backend/ # FastAPI API (port 8001) -│ ├── frontend/ # Vue.js 3 UI (port 3000-3005) -│ └── telegram-bot/ # Telegram bot (port 8002 internal) +├── src/ # Unified Vue.js 3 frontend +│ ├── modules/ # Feature modules +│ │ ├── reports/ # Reports frontend +│ │ └── data-entry/ # Data entry frontend +│ ├── shared/ # Shared frontend components +│ │ ├── components/ # Reusable Vue components +│ │ └── stores/ # Pinia stores +│ ├── assets/ # Global CSS, images +│ ├── router/ # Vue Router +│ └── App.vue # Root component │ -├── data-entry-app/ # DATA INPUT with approval workflow -│ ├── backend/ # FastAPI API (port 8003) - SQLite + SQLModel -│ ├── frontend/ # Vue.js 3 UI (port 3010) -│ └── CLAUDE.md # ⚠️ READ THIS for data-entry work! +├── shared/ # Shared backend components +│ ├── database/ # Oracle pool +│ ├── auth/ # JWT auth & middleware +│ └── frontend/ # Shared frontend assets +│ ├── components/ # LoginView.vue +│ ├── stores/ # Auth store factory +│ └── styles/ # Login CSS │ ├── docs/ # Architecture & style guides ├── deployment/ # Production deployment scripts @@ -74,39 +92,49 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ### Starting Services -**Quick Start** (All services with parallel backend startup): +**Quick Start** (Unified backend + frontend): ```bash -./start-dev.sh # Dev: Backend :8001, :8003, Bot :8002, Frontend :3000 (~11s) -./start-test.sh # Test: Same ports (~33s - Oracle pool init takes longer) +./start-prod.sh # Prod env: Backend :8001, Frontend :3000 +./start-test.sh # Test env: Backend :8001, Frontend :3000 ``` -**Individual Service Control** (for quick development iterations): +**Individual Service Control**: ```bash -./frontend.sh restart # Restart frontend only (~7s - fastest!) -./backend-reports.sh start # Start Reports backend :8001 -./backend-data-entry.sh stop # Stop Data Entry backend :8003 -./bot.sh status # Check Telegram bot :8002 status -./status.sh # Show all services status + health checks +./start-frontend.sh restart # Restart frontend only (~7s - fastest!) +./start-backend.sh # Start unified backend :8000 or :8001 +./status.sh # Show services status + health checks ``` **Infrastructure**: ```bash -./ssh_tunnel.sh start # Oracle DB tunnel (production: 10.0.20.36) -./ssh-tunnel-test.sh start # Oracle TEST tunnel (LXC: 10.0.20.121) +./ssh-tunnel-prod.sh start # Oracle DB tunnel (production: 10.0.20.36) +./ssh-tunnel-test.sh start # Oracle TEST tunnel (LXC: 10.0.20.121) ``` **Benefits**: -- **87% faster frontend restart**: 7s vs 53s full restart -- **38% faster full startup**: 33s vs 53s (test) via parallel backend init -- **Granular control**: Restart individual services without affecting others +- **Single backend process**: All modules in one FastAPI app +- **Faster startup**: No multi-service coordination overhead +- **Easier debugging**: Single process to monitor +- **Shared resources**: Connection pools, cache, and middleware ### Key Architectural Decisions + +**Core Architecture:** +- **Ultrathin Monolith**: Single backend process with modular structure (`backend/modules/`) +- **Single Worker Mode**: `--workers 1` (required for Telegram bot - see ADR-002 in `docs/ARCHITECTURE-DECISIONS.md`) - **Shared Database Pool**: Singleton `OraclePool` in `shared/database/oracle_pool.py` (python-oracledb with connection pooling) - **Centralized Auth**: JWT-based auth in `shared/auth/` with middleware auto-injecting `request.state.user` -- **Two-Tier Cache System**: Hybrid L1 (Memory) + L2 (SQLite) cache in `backend/app/cache/` - **MANDATORY for all new endpoints** + +**Module-Specific:** +- **Module-Based Cache**: Each module can have its own cache strategy (Reports uses L1+L2 hybrid cache) +- **Telegram Bot**: Integrated module with SQLite database (`backend/modules/data/telegram_bot.db`) +- **FastAPI Structure**: Modules in `backend/modules/*/`, each with services, routers, models/schemas + +**Infrastructure:** - **SSH Tunnel**: Required for Oracle DB connections (development/Linux) - see Database Setup -- **FastAPI Structure**: Services in `backend/app/services/` with `@cached` decorator, routers in `backend/app/routers/`, models in `backend/app/models/` or `schemas/` -- **Telegram Bot**: Standalone SQLite database for bot data, communicates with backend via HTTP API +- **IIS Sub-Application**: Deployed at `/roa2web` path, not root (production) + +> **For detailed rationale and trade-offs**, see `docs/ARCHITECTURE-DECISIONS.md` --- @@ -117,14 +145,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ### SSH Tunnel (Development/Linux) ```bash -./ssh_tunnel.sh start # localhost:1526 → remote:1521 -./ssh_tunnel.sh status # Check tunnel -./ssh_tunnel.sh stop # Stop tunnel +./ssh-tunnel-prod.sh start # localhost:1526 → remote:1521 +./ssh-tunnel-prod.sh status # Check tunnel +./ssh-tunnel-prod.sh stop # Stop tunnel ``` **IMPORTANT**: Always ensure SSH tunnel is running before starting backend services. -### Environment Variables (`reports-app/backend/.env`) +### Environment Variables (`backend/.env`) ```bash # Oracle Database (through SSH tunnel) ORACLE_USER=CONTAFIN_ORACLE @@ -138,11 +166,14 @@ JWT_SECRET_KEY=your_secret_key JWT_ALGORITHM=HS256 JWT_EXPIRE_MINUTES=30 -# Telegram Bot Integration -TELEGRAM_BOT_INTERNAL_API=http://localhost:8002 +# Module Configuration +MODULE_REPORTS_ENABLED=true +MODULE_DATA_ENTRY_ENABLED=true +MODULE_TELEGRAM_ENABLED=true ``` -**Windows Production**: Direct Oracle connection, no SSH tunnel required. Ensure `TELEGRAM_BOT_INTERNAL_API` is set for auth code management. +**Windows Production**: Direct Oracle connection, no SSH tunnel required. +**Environment Files**: Multiple .env files available (.env.dev, .env.test, .env.prod) - see `backend/ENV-SETUP.md` for details. --- @@ -156,7 +187,7 @@ TELEGRAM_BOT_INTERNAL_API=http://localhost:8002 **Key Files**: - `shared/auth/middleware.py` - FastAPI middleware with rate limiting (5 req/5 min) - `shared/auth/jwt_handler.py` - Token creation/validation -- `reports-app/backend/app/main.py` - Auth router inline definition +- `backend/main.py` - Main FastAPI app with auth router registration --- @@ -165,10 +196,10 @@ TELEGRAM_BOT_INTERNAL_API=http://localhost:8002 ### Adding a New API Endpoint **IMPORTANT**: Always use the cache system for database queries to improve performance. -1. Create **service** in `reports-app/backend/app/services/your_service.py` (NOT in router!) -2. Define Pydantic schemas in `app/schemas/` or `app/models/` +1. Create **service** in `backend/modules/reports/services/your_service.py` (NOT in router!) +2. Define Pydantic schemas in `modules/*/schemas/` or `modules/*/models/` 3. **Add caching** using `@cached` decorator in service methods -4. Create router in `reports-app/backend/app/routers/your_router.py` (calls service) +4. Create router in `backend/modules/reports/routers/your_router.py` (calls service) 5. Register router in `app/main.py`: `app.include_router(your_router, prefix="/api/your_prefix")` **Service Example with Caching** (RECOMMENDED): @@ -278,7 +309,7 @@ ttl_your_data: int = int(os.getenv('CACHE_TTL_YOUR_DATA', '600')) # 10 min defa ### Adding a New Telegram Bot Command **IMPORTANT**: Follow established command patterns and formatting. -**Before coding**: Read **`reports-app/telegram-bot/TELEGRAM_COMMANDS.md`** for command patterns → See "Documentation Index" for complete guides. +**Before coding**: Read **`docs/telegram/TELEGRAM_BUTTON_INTERFACE_PLAN.md`** for command patterns → See "Documentation Index" for complete guides. **Standard Pattern** (add handler in `app/bot/handlers.py`): ```python @@ -338,14 +369,12 @@ const response = await api.get('/endpoint'); → **`README.md`** - Project overview, setup, development commands, testing, deployment ### Data Entry App (Bonuri Fiscale) -- **`data-entry-app/CLAUDE.md`** - ⚠️ **READ FIRST** when working on data-entry -- `data-entry-app/README.md` - Quick start guide +- **`docs/data-entry/CLAUDE.md`** - ⚠️ **READ FIRST** when working on data-entry +- `docs/data-entry/README.md` - Quick start guide - `docs/data-entry/REQUIREMENTS.md` - Functional requirements - `docs/data-entry/ARCHITECTURE.md` - Technical architecture (SQLModel, workflow) ### Architecture & Planning -- **`docs/ARCHITECTURE_SCHEMA.md`** - Architecture diagrams, cache system, and schemas -- `docs/MICROSERVICES_GUIDE.md` - Microservices architecture details - `DEVELOPMENT_BLUEPRINT.md` - Detailed development plan ### Frontend Development @@ -355,20 +384,19 @@ const response = await api.get('/endpoint'); - `docs/STYLING_GUIDELINES.md` - CSS best practices and conventions - `docs/COMPONENT_STYLING.md` - Component-specific styling guide - `docs/FORM_TEMPLATE.md` - Standardized form template -- `reports-app/frontend/README.md` - Frontend architecture and setup -- `reports-app/frontend/tests/README.md` - Playwright E2E testing guide +- `Frontend module documentation in src/modules/` - Frontend architecture and setup +- `Playwright E2E testing (see tests/ directory)` - Playwright E2E testing guide ### Backend Development -- `reports-app/backend/README.md` - Backend architecture and API details +- `Backend module documentation in backend/modules/` - Backend architecture and API details - API endpoints documented in `README.md` (Authentication, Companies, Dashboard, Invoices, Treasury, Telegram) ### Telegram Bot Development -- **`reports-app/telegram-bot/README.md`** - Complete bot architecture and development guide (START HERE) -- **`reports-app/telegram-bot/TELEGRAM_COMMANDS.md`** - Command reference and patterns +- **`docs/telegram/README.md`** - Complete bot architecture and development guide (START HERE) +- **`docs/telegram/TELEGRAM_BUTTON_INTERFACE_PLAN.md`** - Command reference and patterns - `tests/MANUAL_TESTING_CHECKLIST.md` - Manual testing procedures ### Deployment -- **`DEPLOYMENT_GUIDE.md`** - Production deployment (Linux/Docker & Windows/IIS) - `deployment/windows/README.md` - Windows deployment quick start - `deployment/windows/docs/WINDOWS_DEPLOYMENT.md` - Complete Windows guide - `deployment/windows/docs/TELEGRAM_BOT_TROUBLESHOOTING.md` - Bot troubleshooting diff --git a/IMPLEMENTATION_PLAN_RECEIPT_OCR.md b/IMPLEMENTATION_PLAN_RECEIPT_OCR.md deleted file mode 100644 index 55919b4..0000000 --- a/IMPLEMENTATION_PLAN_RECEIPT_OCR.md +++ /dev/null @@ -1,386 +0,0 @@ -# Plan: Receipt Scanning Workflow Improvements - -> **Context Handover Document** - Created for session continuity -> **Date**: 2025-12-15 -> **Status**: Ready for implementation - -## Overview -Improve the data-entry-app receipt scanning to: -1. Save supplier name, CUI, and OCR text in drafts -2. Make supplier validation assistive (not blocking) -3. Unify create/edit forms with OCR rescan capability -4. Fix image resize bug (>4000px) -5. **NEW: Extract payment methods (CARD/NUMERAR) from OCR** - -## Requirements Summary -- **Drafts**: Save `cui` + `partner_name` + `ocr_raw_text` + `payment_methods` from OCR -- **Supplier match**: Auto-fill but editable (for assistance, not validation) -- **No match**: Show warning only, allow saving draft -- **Edit mode**: Allow OCR rescan on existing drafts -- **Approval**: Requires valid `cui` only (NOT partner_id) - ROA has stored procedure for supplier lookup -- **Image resize**: Cap at 4000px BEFORE upscaling -- **Payment methods**: Extract CARD/NUMERAR amounts (after TOTAL LEI, before TOTAL TVA) - ---- - -## Part 1: Backend Model & Database - -### 1.1 Add Fields to Receipt Model -**File**: `data-entry-app/backend/app/db/models/receipt.py` - -Add after line 66 (after `partner_name`): -```python -cui: Optional[str] = Field(default=None, max_length=20) # Fiscal code from OCR -ocr_raw_text: Optional[str] = Field(default=None) # Raw OCR text for debugging -payment_methods: Optional[str] = Field(default=None, max_length=500) # JSON: [{"method":"CARD","amount":"50.00"}] -``` - -### 1.2 Create Alembic Migration -**File**: `data-entry-app/backend/migrations/versions/XXXX_add_ocr_fields.py` - -```python -def upgrade(): - with op.batch_alter_table('receipts') as batch_op: - batch_op.add_column(sa.Column('cui', sa.String(20), nullable=True)) - batch_op.add_column(sa.Column('ocr_raw_text', sa.Text(), nullable=True)) - batch_op.add_column(sa.Column('payment_methods', sa.String(500), nullable=True)) -``` - -### 1.3 Update Pydantic Schemas -**File**: `data-entry-app/backend/app/schemas/receipt.py` - -**Add PaymentMethodSchema** (after TvaEntrySchema ~line 75): -```python -class PaymentMethodSchema(BaseModel): - """Payment method entry (CARD/NUMERAR).""" - method: str = Field(description="Payment method: CARD or NUMERAR") - amount: Decimal = Field(description="Amount paid with this method") -``` - -**ReceiptBase** (after line 97): -```python -cui: Optional[str] = Field(default=None, max_length=20) -ocr_raw_text: Optional[str] = Field(default=None) -payment_methods: Optional[List[PaymentMethodSchema]] = Field(default=None, description="Payment methods from OCR") -``` - -**ReceiptUpdate** (after line 125): -```python -cui: Optional[str] = Field(default=None, max_length=20) -ocr_raw_text: Optional[str] = Field(default=None) -payment_methods: Optional[List[PaymentMethodSchema]] = Field(default=None) -``` - -**ReceiptResponse**: Add validator to parse `payment_methods` from JSON (similar to `parse_tva_breakdown`) - ---- - -## Part 2: Fix Image Resize Bug - -**File**: `data-entry-app/backend/app/services/image_preprocessor.py` - -### 2.1 Update `preprocess_light()` (after line 55) -Add downscale BEFORE upscale: -```python -# 2a. Scale DOWN if any side exceeds 4000px (PaddleOCR limit) -height, width = gray.shape -max_side = max(height, width) -if max_side > 4000: - scale = 4000 / max_side - gray = cv2.resize(gray, None, fx=scale, fy=scale, interpolation=cv2.INTER_AREA) - height, width = gray.shape - -# 2b. Scale UP if too small -if width < 1500: - scale = 1500 / width - gray = cv2.resize(gray, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC) -``` - -### 2.2 Update `preprocess_heavy()` (after line 82) -Same downscale logic before the existing upscale at lines 85-88. - ---- - -## Part 3: Backend OCR Endpoint - Return Raw Text - -**File**: `data-entry-app/backend/app/routers/ocr.py` - -Ensure the OCR extraction endpoint returns `raw_text` in the response (verify this is already included in the OCR service output). - ---- - -## Part 4: Frontend Form Unification - -### 4.1 Unify OCR Zone for Create & Edit -**File**: `data-entry-app/frontend/src/views/receipts/ReceiptCreateView.vue` - -**Change line 19** from: -```vue -
-``` -to: -```vue -
-``` - -**Update header text** (around line 23): -```vue -

- - {{ isEditMode ? 'Re-scanare OCR (opțional)' : 'Poză Bon (obligatoriu)' }} -

-``` - -### 4.2 Add CUI Field to Form State -**File**: `data-entry-app/frontend/src/views/receipts/ReceiptCreateView.vue` - -Add to form ref initialization: -```javascript -cui: '', -ocr_raw_text: '', -``` - -### 4.3 Add CUI Display Field -Add after Furnizor dropdown (around line 210): -```vue -
- - - - - CUI negăsit în nomenclator - -
-``` - -### 4.4 Change Supplier Dialog to Warning Banner -**Current behavior** (lines 555-563): When CUI not found, opens blocking dialog. - -**New behavior**: Show non-blocking warning message. - -Replace the `else` block in `applyOCRData()`: -```javascript -} else { - // Not found - show warning but allow continuing - supplierWarning.value = { - show: true, - cui: data.cui, - name: data.partner_name || '' - } - // Still set form values from OCR - form.value.cui = data.cui - form.value.partner_name = data.partner_name || '' - - toast.add({ - severity: 'warn', - summary: 'Furnizor negăsit', - detail: `CUI ${data.cui} nu a fost găsit în nomenclator`, - life: 5000 - }) -} -``` - -Add ref for warning state: -```javascript -const supplierWarning = ref({ show: false, cui: '', name: '' }) -``` - -### 4.5 Update `applyOCRData()` to Save Raw Text -Add to the function: -```javascript -if (data.cui) form.value.cui = data.cui -if (data.raw_text) form.value.ocr_raw_text = data.raw_text -``` - -### 4.6 Update `loadReceipt()` for Edit Mode -Add to existing field mapping: -```javascript -cui: receipt.value.cui || '', -ocr_raw_text: receipt.value.ocr_raw_text || '', -``` - ---- - -## Part 5: Backend Approval Validation - -**File**: `data-entry-app/backend/app/services/receipt_service.py` - -In `approve_receipt()` method, add validation: -```python -if not receipt.cui: - return False, "Trebuie completat codul fiscal (CUI) pentru aprobare", None -``` - -**Note**: At approval, only `cui` (fiscal code) is required, NOT `partner_id`. -The ROA ERP has a stored procedure that searches/creates suppliers based on `cui`. -The `partner_id` is only populated later during Oracle import phase. - ---- - -## Part 6: OCR Payment Methods Extraction - -### 6.1 Update ExtractionResult Dataclass -**File**: `data-entry-app/backend/app/services/ocr_extractor.py` - -Add to `ExtractionResult` (after line 24, after `items_count`): -```python -payment_methods: List[dict] = field(default_factory=list) # [{"method":"CARD","amount":Decimal}] -``` - -### 6.2 Add Payment Method Patterns -**File**: `data-entry-app/backend/app/services/ocr_extractor.py` - -Add new patterns (after TVA_PATTERNS ~line 184): -```python -# Payment method patterns - appears after TOTAL LEI, before TOTAL TVA -# Format: "CARD: 50.00" or "NUMERAR 100.00" or "PLATA CARD: 50.00" -PAYMENT_METHOD_PATTERNS = [ - # CARD with amount - (r'(?:PLATA\s+)?CARD\s*:?\s*([\d\s.,]+)', 'CARD', 0.95), - # NUMERAR (cash) with amount - (r'NUMERAR\s*:?\s*([\d\s.,]+)', 'NUMERAR', 0.95), - # CASH alternative spelling - (r'CASH\s*:?\s*([\d\s.,]+)', 'NUMERAR', 0.90), -] -``` - -### 6.3 Add Extraction Method -**File**: `data-entry-app/backend/app/services/ocr_extractor.py` - -Add new method `_extract_payment_methods()` (after `_extract_address` ~line 996): -```python -def _extract_payment_methods(self, text: str) -> List[dict]: - """ - Extract payment methods (CARD/NUMERAR) from receipt. - These appear after TOTAL LEI and before TOTAL TVA section. - - Returns list of: {'method': 'CARD'/'NUMERAR', 'amount': Decimal} - """ - payment_methods = [] - seen_methods = set() - - # Normalize spaces in numbers - normalized_text = re.sub(r'(\d+)[.,]\s+(\d{2})', r'\1.\2', text) - - # Find the region between TOTAL LEI and TOTAL TVA - total_lei_match = re.search(r'TOTAL\s+LEI\s*([\d\s.,]+)', normalized_text, re.IGNORECASE) - total_tva_match = re.search(r'TOTAL\s+T[VU][AR]', normalized_text, re.IGNORECASE) - - # Define search region (after TOTAL LEI, before TOTAL TVA if exists) - if total_lei_match: - start_pos = total_lei_match.end() - end_pos = total_tva_match.start() if total_tva_match else len(normalized_text) - search_region = normalized_text[start_pos:end_pos] - else: - search_region = normalized_text # Fallback to full text - - for pattern, method, confidence in self.PAYMENT_METHOD_PATTERNS: - for match in re.finditer(pattern, search_region, re.IGNORECASE): - try: - amount_str = match.group(1).replace(' ', '') - amount_str = self._normalize_number(re.sub(r'[^\d.,]', '', amount_str)) - amount = Decimal(amount_str) - if amount > 0 and method not in seen_methods: - payment_methods.append({ - 'method': method, - 'amount': amount - }) - seen_methods.add(method) - except (InvalidOperation, ValueError): - continue - - return payment_methods -``` - -### 6.4 Call Extraction in `extract()` Method -**File**: `data-entry-app/backend/app/services/ocr_extractor.py` - -Add to `extract()` method (after line 255, after `result.address = ...`): -```python -result.payment_methods = self._extract_payment_methods(text_upper) -``` - -### 6.5 Frontend - Add Payment Methods Display -**File**: `data-entry-app/frontend/src/views/receipts/ReceiptCreateView.vue` - -Add to form ref: -```javascript -payment_methods: [], -``` - -Add to `applyOCRData()`: -```javascript -if (data.payment_methods) form.value.payment_methods = data.payment_methods -``` - -Add UI display (after TVA breakdown section): -```vue - -
- -
- -
-
-``` - ---- - -## Implementation Order - -| Step | Task | Files | -|------|------|-------| -| 1 | Add `cui`, `ocr_raw_text`, `payment_methods` to model | `models/receipt.py` | -| 2 | Create migration | `migrations/versions/...` | -| 3 | Update schemas | `schemas/receipt.py` | -| 4 | Fix image resize | `services/image_preprocessor.py` | -| 5 | Add payment methods extraction to OCR | `services/ocr_extractor.py` | -| 6 | Unify frontend form + add new fields | `views/receipts/ReceiptCreateView.vue` | -| 7 | Add approval validation | `services/receipt_service.py` | -| 8 | Test full workflow | Manual testing | - ---- - -## Files to Modify - -### Backend -- `data-entry-app/backend/app/db/models/receipt.py` - Add cui, ocr_raw_text, payment_methods fields -- `data-entry-app/backend/app/schemas/receipt.py` - Add PaymentMethodSchema, update schemas -- `data-entry-app/backend/app/services/image_preprocessor.py` - Fix resize bug (cap at 4000px) -- `data-entry-app/backend/app/services/ocr_extractor.py` - Add payment methods extraction -- `data-entry-app/backend/app/services/receipt_service.py` - Add approval validation -- `data-entry-app/backend/migrations/versions/` - New migration - -### Frontend -- `data-entry-app/frontend/src/views/receipts/ReceiptCreateView.vue` - Unify form, add CUI + payment methods fields, change dialog to warning - ---- - -## Expected Behavior After Implementation - -1. **OCR Scan**: Extracts supplier name, CUI, raw text, payment methods → all saved to draft -2. **Payment Methods**: CARD/NUMERAR amounts extracted (after TOTAL LEI, before TOTAL TVA) -3. **CUI Match**: Auto-fills supplier name from ROA, user can edit -4. **CUI No Match**: Shows warning toast, allows saving draft with OCR data -5. **Edit Mode**: Can re-scan OCR to update extracted data -6. **Approval**: Requires valid `cui` (fiscal code) - NOT partner_id -7. **Oracle Import** (later): Uses `cui` to find/create supplier via ROA stored procedure -8. **Large Images**: Automatically resized to max 4000px before OCR - ---- - -## Romanian Receipt Structure Reference -``` -NUME FIRMA S.R.L. -CIF: RO12345678 -STR. EXEMPLU NR. 1 - -[Product lines...] - -TOTAL LEI 150.00 ← Total amount -CARD 50.00 ← Payment method 1 (NEW) -NUMERAR 100.00 ← Payment method 2 (NEW) -TOTAL TVA A-19% 23.95 ← TVA breakdown -``` diff --git a/IMPLEMENTATION_PLAN_UNIFIED_APP.md b/IMPLEMENTATION_PLAN_UNIFIED_APP.md deleted file mode 100644 index f7d589c..0000000 --- a/IMPLEMENTATION_PLAN_UNIFIED_APP.md +++ /dev/null @@ -1,285 +0,0 @@ -# Plan: Consolidare ROA2WEB - Pragmatic Monolith - -> **Branch**: `feature/unified-app-pragmatic-monolith` -> **Status**: APROBAT - Ready for Implementation -> **Efort estimat**: ~2.5 zile - ---- - -## Context și Problema - -### Situația Curentă -- 2 aplicații frontend separate în IIS: `/roa2web/` și `/data-entry/` -- Deploy greoi (2 build-uri, 2 configurații IIS separate) -- Componentele shared cauzează probleme CSS cross-app (text alb pe fundal alb) -- Nu există meniu unificat între aplicații - -### Obiectiv -- Un singur meniu cu ambele aplicații (Reports + Data Entry) -- Deploy simplificat (un build, un site IIS) -- Izolare între module (bug în Reports să nu afecteze DataEntry) -- URL-uri pe root: `/reports/*`, `/data-entry/*` - ---- - -## Decizie Arhitecturală: Pragmatic Monolith - -### De ce NU Micro-frontends? -| Criteriu pentru MFE | ROA2WEB | Necesită MFE? | -|---------------------|---------|---------------| -| 20+ dezvoltatori | 1 dev | ❌ Nu | -| Deploy de multe ori/zi | Săptămânal | ❌ Nu | -| Milioane de utilizatori | 1-5 concurenți | ❌ Nu | -| Framework-uri diferite | Vue only | ❌ Nu | - -**Verdict**: Module Federation / Single-SPA = OVERKILL - -### Abordare Aleasă: Monolith cu Mecanisme de Izolare -- **Error Boundaries** per modul (bug în Reports nu strică DataEntry) -- **Lazy Loading** (bundle-uri separate, încărcate la nevoie) -- **Stores izolate** per modul -- **Feature flags** pentru control - -**Blast radius cu protecții: 50-70%** (aproape ca 2 apps separate!) - ---- - -## Arhitectura Finală - -``` -┌─────────────────────────────────────────────────────────────┐ -│ ROA2WEB Unified SPA │ -│ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │ -│ │ Reports Module │ │ DataEntry Module │ │ Shared │ │ -│ │ /reports/* │ │ /data-entry/* │ │ - Auth │ │ -│ │ (lazy loaded) │ │ (lazy loaded) │ │ - Company │ │ -│ │ │ │ │ │ - Period │ │ -│ │ ErrorBoundary │ │ ErrorBoundary │ │ - Header │ │ -│ └────────┬────────┘ └────────┬─────────┘ └──────────────┘ │ -│ │ │ │ -│ ┌────────┴────────────────────┴─────────┐ │ -│ │ Vue Router + Global Error Handler │ │ -│ └────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ - ┌─────────┴─────────┐ - │ IIS Proxy │ - └─────────┬─────────┘ - ┌───────────────┴───────────────┐ - │ │ - ┌────────▼────────┐ ┌──────────▼────────┐ - │ Reports Backend │ │ DataEntry Backend │ - │ port 8001 │ │ port 8003 │ - │ (Oracle RO) │ │ (SQLite + Oracle) │ - └─────────────────┘ └───────────────────┘ -``` - ---- - -## Structura Proiect Unified - -``` -roa2web/ -├── src/ -│ ├── main.js -│ ├── App.vue # Meniu unificat -│ ├── router/index.js # Rute unificate cu lazy loading -│ │ -│ ├── modules/ -│ │ ├── reports/ # MODUL IZOLAT -│ │ │ ├── ReportsLayout.vue # Error boundary pentru modul -│ │ │ ├── views/ -│ │ │ │ ├── DashboardView.vue -│ │ │ │ ├── InvoicesView.vue -│ │ │ │ ├── BankCashRegisterView.vue -│ │ │ │ ├── TrialBalanceView.vue -│ │ │ │ ├── TelegramView.vue -│ │ │ │ └── CacheStatsView.vue -│ │ │ ├── stores/ -│ │ │ └── services/ -│ │ │ -│ │ └── data-entry/ # MODUL IZOLAT -│ │ ├── DataEntryLayout.vue -│ │ ├── views/ -│ │ │ ├── ReceiptsListView.vue -│ │ │ └── ReceiptCreateView.vue -│ │ ├── stores/ -│ │ └── services/ -│ │ -│ ├── shared/ # Shared între module -│ │ ├── components/ # AppHeader, SlideMenu, CompanySelector, etc. -│ │ ├── stores/ # Auth, Company, Period -│ │ └── styles/ -│ │ -│ └── config/ -│ ├── menu.js # Configurație meniu unificat -│ └── features.js # Feature flags -│ -├── vite.config.js -└── package.json -``` - ---- - -## Pași Implementare - -### Faza 1: Setup Proiect (0.5 zile) -1. Creează directorul `roa2web/` -2. Setup `package.json` cu dependencies combinate din ambele apps -3. Setup `vite.config.js` cu: - - Dual proxy: `/api/reports/*` → `:8001`, `/api/data-entry/*` → `:8003` - - Lazy loading chunks configuration - - Base path `/` -4. Setup `main.js` cu Pinia, PrimeVue, Router - -### Faza 2: Migrare Module (1 zi) -1. Copiază views din `reports-app/frontend/src/views/` → `modules/reports/views/` -2. Copiază views din `data-entry-app/frontend/src/views/` → `modules/data-entry/views/` -3. Copiază stores specifice fiecărui modul -4. Creează `ReportsLayout.vue` și `DataEntryLayout.vue` cu error boundaries -5. Setup router unificat: - ```javascript - const routes = [ - { path: '/login', component: LoginView }, - { - path: '/reports', - component: () => import('./modules/reports/ReportsLayout.vue'), - children: [ - { path: 'dashboard', component: () => import('./modules/reports/views/DashboardView.vue') }, - { path: 'invoices', component: () => import('./modules/reports/views/InvoicesView.vue') }, - // ... - ] - }, - { - path: '/data-entry', - component: () => import('./modules/data-entry/DataEntryLayout.vue'), - children: [ - { path: '', component: () => import('./modules/data-entry/views/ReceiptsListView.vue') }, - { path: 'create', component: () => import('./modules/data-entry/views/ReceiptCreateView.vue') }, - ] - }, - { path: '/', redirect: '/reports/dashboard' } - ] - ``` - -### Faza 3: Izolare și Resilience (0.5 zile) -1. Implementează `ErrorBoundary.vue`: - ```vue - - ``` -2. Separă stores per modul (nu global) -3. Adaugă feature flags în `config/features.js` -4. Testează izolarea: introduce bug în Reports, verifică că DataEntry funcționează - -### Faza 4: Build & Deploy (0.5 zile) -1. Verifică bundle splitting cu `npm run build`: - ``` - dist/assets/ - ├── index-[hash].js # Shell + shared (~150KB) - ├── reports-[hash].js # Reports module (~200KB) - └── data-entry-[hash].js # DataEntry module (~100KB) - ``` -2. Update IIS web.config pentru SPA routing (toate rutele → index.html) -3. Update deployment scripts pentru single app -4. Test end-to-end pe server -5. Deploy în producție - ---- - -## Meniu Unificat - -```javascript -// config/menu.js -export const menuSections = [ - { - title: 'Rapoarte', - items: [ - { to: '/reports/dashboard', icon: 'pi pi-home', label: 'Dashboard' }, - { to: '/reports/invoices', icon: 'pi pi-file', label: 'Facturi' }, - { to: '/reports/bank-cash', icon: 'pi pi-money-bill', label: 'Casa și Banca' }, - { to: '/reports/trial-balance', icon: 'pi pi-calculator', label: 'Balanță' }, - ] - }, - { - title: 'Introduceri Date', - items: [ - { to: '/data-entry', icon: 'pi pi-list', label: 'Lista Bonuri' }, - { to: '/data-entry/create', icon: 'pi pi-plus', label: 'Bon Nou' }, - ] - }, - { - title: 'Sistem', - items: [ - { to: '/reports/telegram', icon: 'pi pi-telegram', label: 'Telegram' }, - { to: '/reports/cache-stats', icon: 'pi pi-chart-bar', label: 'Cache Stats' }, - ] - } -]; -``` - ---- - -## Fișiere de Referință - -### Fișiere Existente (sursă pentru copiere) -- `reports-app/frontend/src/views/*.vue` - Views Reports -- `reports-app/frontend/src/stores/*.js` - Stores Reports -- `reports-app/frontend/src/services/api.js` - API service Reports -- `data-entry-app/frontend/src/views/*.vue` - Views DataEntry -- `data-entry-app/frontend/src/stores/*.js` - Stores DataEntry -- `data-entry-app/frontend/src/services/api.js` - API service DataEntry -- `shared/frontend/components/` - Componente shared (AppHeader, SlideMenu, etc.) -- `shared/frontend/stores/` - Stores shared (auth, companies, accountingPeriod) - -### Configurații de Referință -- `reports-app/frontend/vite.config.js` - Pentru proxy și build config -- `reports-app/frontend/package.json` - Pentru dependencies -- `data-entry-app/frontend/vite.config.js` - Pentru proxy config -- `data-entry-app/frontend/package.json` - Pentru dependencies - ---- - -## URL-uri Finale - -| Path | Descriere | -|------|-----------| -| `/` | Redirect la `/reports/dashboard` | -| `/login` | Pagină login | -| `/reports/dashboard` | Dashboard principal | -| `/reports/invoices` | Facturi | -| `/reports/bank-cash` | Casa și Banca | -| `/reports/trial-balance` | Balanță de Verificare | -| `/reports/telegram` | Telegram Bot | -| `/reports/cache-stats` | Statistici Cache | -| `/data-entry` | Lista Bonuri | -| `/data-entry/create` | Bon Nou | -| `/data-entry/:id` | Detalii Bon | -| `/data-entry/:id/edit` | Editare Bon | - ---- - -## Note Importante - -1. **Backend-urile rămân separate** - port 8001 (Reports) și port 8003 (DataEntry) -2. **IIS proxy routing** - trebuie configurat pentru a ruta API calls corect -3. **Error Boundaries** - critice pentru izolarea modulelor -4. **Lazy Loading** - asigură bundle splitting corect -5. **Feature flags** - permit dezactivarea unui modul fără redeploy - ---- - -## Handover Notes - -Această implementare va înlocui cele 2 aplicații separate cu o aplicație unificată. -După implementare, directoarele `reports-app/frontend/` și `data-entry-app/frontend/` -pot fi arhivate/șterse, păstrând doar `unified-app/`. - -Backend-urile rămân neschimbate în `reports-app/backend/` și `data-entry-app/backend/`. diff --git a/QUICK-START.md b/QUICK-START.md new file mode 100644 index 0000000..4d71587 --- /dev/null +++ b/QUICK-START.md @@ -0,0 +1,140 @@ +# ROA2WEB Ultrathin Monolith - Quick Start 🚀 + +## Pornire Rapidă + +### PROD Environment (server PRODUCȚIE 10.0.20.36) +```bash +./start-prod.sh # Pornește tot: SSH tunnel + backend + frontend +./start-prod.sh stop # Oprește toate serviciile +``` + +### TEST Environment (server TEST 10.0.20.121) +```bash +./start-test.sh # Pornește tot: SSH tunnel + backend + frontend +./start-test.sh stop # Oprește toate serviciile +``` + +## Verificare Status + +```bash +./status.sh # Arată status-ul tuturor serviciilor +``` + +## Ce s-a Schimbat? + +### Înainte (3 Backend-uri Separate) +``` +Reports Backend → Port 8001 +Data Entry Backend → Port 8003 +Telegram Bot → Port 8002 +``` + +### Acum (1 Backend Unificat) +``` +Unified Backend → Port 8000 + ├── Reports API: /api/reports/* + ├── Data Entry: /api/data-entry/* + ├── Telegram: /api/telegram/* + └── Bot: Running ca background task +``` + +## Scripturi Disponibile + +| Script | Descriere | +|--------|-----------| +| `./start-prod.sh` | Pornește tot pentru PROD (Oracle PROD: 10.0.20.36) | +| `./start-test.sh` | Pornește tot pentru TEST (Oracle TEST: 10.0.20.121) | +| `./status.sh` | Verifică status-ul serviciilor | +| `./start-backend.sh start/stop/restart` | Control granular backend | +| `./start-frontend.sh restart` | Restart rapid frontend (~7s) | +| `./test-unified-backend.sh` | Rulează testele comprehensive | + +## API Endpoints + +**Backend Unificat**: http://localhost:8000 + +- **API Docs**: http://localhost:8000/docs +- **Health Check**: http://localhost:8000/health +- **Reports**: http://localhost:8000/api/reports/* +- **Data Entry**: http://localhost:8000/api/data-entry/* +- **Telegram**: http://localhost:8000/api/telegram/* + +**Frontend**: http://localhost:3000 + +## Log Files + +```bash +# Backend logs +tail -f /tmp/unified_backend_dev.log # DEV +tail -f /tmp/unified_backend_test.log # TEST + +# Frontend logs +tail -f /tmp/unified_frontend_dev.log # DEV +tail -f /tmp/unified_frontend_test.log # TEST +``` + +## Troubleshooting + +### Backend nu pornește +```bash +# Verifică logurile +tail -n 50 /tmp/unified_backend_dev.log + +# Verifică dacă portul este ocupat +lsof -i :8000 + +# Oprește procesul vechi +./start-prod.sh stop +``` + +### Frontend nu pornește +```bash +# Verifică logurile +tail -n 50 /tmp/unified_frontend_dev.log + +# Reinstalează dependențele +rm -rf node_modules package-lock.json +npm install +``` + +### SSH Tunnel nu se conectează +```bash +# DEV (PRODUCȚIE) +./ssh-tunnel-prod.sh stop +./ssh-tunnel-prod.sh start + +# TEST +./ssh-tunnel-test.sh stop +./ssh-tunnel-test.sh start +``` + +## Configurare Inițială + +1. **Backend**: Creează `backend/.env` din `backend/.env.example` +2. **Configurează variabilele** pentru mediul dorit (DEV/TEST) +3. **Pornește serviciile**: `./start-prod.sh` sau `./start-test.sh` + +## Diferențe DEV vs TEST + +| Aspect | DEV | TEST | +|--------|-----|------| +| SSH Tunnel | `./ssh-tunnel-prod.sh` | `./ssh-tunnel-test.sh` | +| Server Oracle | 10.0.20.36 (PROD) | 10.0.20.121 (TEST) | +| Schema Test | ROMFAST (id=114) | MARIUSM_AUTO (id=110) | +| .env File | `backend/.env` | `backend/.env.test` → `backend/.env` | +| Hot Reload | DA (--reload) | NU (pentru stabilitate) | + +## Avantaje + +✅ **1 comandă** în loc de 3 pentru pornire +✅ **1 proces** în loc de 3 de monitorizat +✅ **Loguri unificate** într-un singur fișier +✅ **Debugging mai ușor** - tot în același proces +✅ **Resurse optimizate** - Oracle pool partajat +✅ **Pornire mai rapidă** - inițializare paralelă + +## Documentație Completă + +- **Implementare**: `ULTRATHIN-MONOLITH-IMPLEMENTATION.md` +- **Teste**: `./test-unified-backend.sh` +- **Plan Detaliat**: `.auto-build/specs/ultrathin-monolith/plan.md` diff --git a/README.md b/README.md index 6092ac9..8681eb4 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ -# ROA2WEB - Modern ERP Reports Application +# ROA2WEB - Modern ERP Application **FastAPI Backend + Vue.js 3 Frontend + Telegram Bot** -Modern microservices-based ERP reporting application for managing invoices, payments, and financial data with Oracle database integration. +Modern ultrathin monolith ERP application for managing reports, data entry, and financial data with Oracle database integration. --- ## Project Overview -ROA2WEB is a comprehensive financial reporting platform built with modern technologies: +ROA2WEB is a comprehensive financial platform built with modern technologies: -- **Backend**: FastAPI (Python) - High-performance async API -- **Frontend**: Vue.js 3 + PrimeVue - Rich, responsive web interface -- **Telegram Bot**: Alternative command-based interface -- **Database**: Oracle Database with connection pooling -- **Architecture**: Microservices with shared components +- **Backend**: FastAPI (Python) - Unified async API with modular architecture +- **Frontend**: Vue.js 3 + PrimeVue - Single-page application with lazy-loaded modules +- **Telegram Bot**: Alternative command-based interface (integrated module) +- **Database**: Oracle Database + SQLite (hybrid approach) +- **Architecture**: Ultrathin monolith with clear module boundaries --- @@ -35,16 +35,16 @@ git clone cd roa2web # Start all services with one command -./start-dev.sh +./start-prod.sh ``` -This starts SSH tunnel, backend (port 8001), and frontend (port 3000-3005). +This starts SSH tunnel, unified backend (port 8001), and frontend (port 3000). -**For individual service setup or troubleshooting**: See "Development & Testing" section below or component-specific READMEs. +**For individual service setup or troubleshooting**: See "Development & Testing" section below. ### Access the Application -- **Frontend**: http://localhost:3000 (or 3001-3005 if 3000 is busy) +- **Frontend**: http://localhost:3000 - **Backend API Docs**: http://localhost:8001/docs (Swagger UI) - **Backend ReDoc**: http://localhost:8001/redoc - **Health Check**: http://localhost:8001/health @@ -53,29 +53,39 @@ This starts SSH tunnel, backend (port 8001), and frontend (port 3000-3005). ## Architecture -### Directory Structure +### Ultrathin Monolith Structure ``` - -├── shared/ # Shared components -│ ├── database/ # Oracle connection pool (singleton) -│ ├── auth/ # JWT authentication & middleware -│ └── utils/ # Common utilities +. +├── backend/ # Unified FastAPI backend (port 8001) +│ ├── modules/ # Business logic modules +│ │ ├── reports/ # Reports module (Oracle read-only) +│ │ ├── data_entry/ # Data entry module (SQLite + workflow) +│ │ └── telegram/ # Telegram bot module +│ ├── config.py # Centralized configuration +│ └── main.py # FastAPI app entry point │ -├── reports-app/ # Main reports application -│ ├── backend/ # FastAPI backend (port 8001) -│ ├── frontend/ # Vue.js 3 frontend (port 3000-3005) -│ └── telegram-bot/ # Telegram bot (port 8002) +├── src/ # Unified Vue.js 3 frontend +│ ├── modules/ # Feature modules +│ │ ├── reports/ # Reports frontend +│ │ └── data-entry/ # Data entry frontend +│ ├── shared/ # Shared frontend components +│ ├── assets/ # Global CSS, images +│ └── router/ # Vue Router │ -├── nginx/ # Nginx reverse proxy config -├── ssh-tunnel/ # SSH tunnel for Oracle DB -├── deployment/ # Deployment scripts (Linux & Windows) -└── scripts/ # Utility scripts +├── shared/ # Shared backend components +│ ├── database/ # Oracle connection pool +│ ├── auth/ # JWT authentication +│ └── frontend/ # Shared frontend assets +│ +├── docs/ # Documentation +├── deployment/ # Deployment scripts +└── ssh-tunnel/ # SSH tunnel for Oracle DB ``` ### Key Features -- **Shared Database Pool**: Singleton Oracle connection pool shared across microservices +- **Shared Database Pool**: Singleton Oracle connection pool shared across all modules - **Two-Tier Cache System**: Hybrid L1 (Memory) + L2 (SQLite) for optimal performance - **JWT Authentication**: Secure token-based auth with middleware - **Microservices**: Independent services with clear separation of concerns @@ -98,21 +108,21 @@ This starts SSH tunnel, backend (port 8001), and frontend (port 3000-3005). ## Development & Testing -**Quick Start**: Use `./start-dev.sh` to start all services (SSH tunnel + Backend + Frontend). +**Quick Start**: Use `./start-prod.sh` to start all services (SSH tunnel + Backend + Frontend). **For detailed development commands, testing procedures, and troubleshooting**: See `CLAUDE.md` and component-specific READMEs: -- Backend: `reports-app/backend/README.md` -- Frontend: `reports-app/frontend/README.md` & `reports-app/frontend/tests/README.md` +- Backend: `backend/ modules and CLAUDE.md` +- Frontend: `src/ and docs/MONOLITH_ARCHITECTURE.md` & `E2E testing guide in docs/` - Telegram Bot: `reports-app/telegram-bot/README.md` **Key Commands**: ```bash # Start All Services (FAST with parallel backend startup - ~11s dev, ~33s test) -./start-dev.sh # Start all (SSH tunnel + Backends + Bot + Frontend) +./start-prod.sh # Start all (SSH tunnel + Backends + Bot + Frontend) ./start-test.sh # Start all (TEST environment) # Individual Service Control (NEW - for quick restarts!) -./frontend.sh start|stop|restart|status # Frontend only (~7s restart!) +./start-frontend.sh start|stop|restart|status # Frontend only (~7s restart!) ./backend-reports.sh start|stop|status # Reports backend only ./backend-data-entry.sh start|stop|status # Data Entry backend only ./bot.sh start|stop|status # Telegram bot only @@ -121,53 +131,53 @@ This starts SSH tunnel, backend (port 8001), and frontend (port 3000-3005). ./status.sh # Show all services status + health checks # Infrastructure Only -./ssh_tunnel.sh start|stop|status # Oracle DB tunnel (production) +./ssh-tunnel-prod.sh start|stop|status # Oracle DB tunnel (production) ./ssh-tunnel-test.sh start|stop|status # Oracle TEST tunnel ``` **💡 Pro Tips**: -- **Frontend changes?** Use `./frontend.sh restart` instead of restarting everything (87% faster!) +- **Frontend changes?** Use `./start-frontend.sh restart` instead of restarting everything (87% faster!) - **Check what's running:** `./status.sh` shows everything at a glance -- **Backend-uri pornesc în paralel** în start-dev.sh și start-test.sh pentru pornire mai rapidă +- **Backend-uri pornesc în paralel** în start-prod.sh și start-test.sh pentru pornire mai rapidă ### 📖 Usage Flow -**Individual scripts (`frontend.sh`, `backend-*.sh`, `bot.sh`) are environment-neutral:** +**Individual scripts (`start-frontend.sh`, `start-backend.sh`, `backend-*.sh`, `bot.sh`) are environment-neutral:** - They DON'T change `.env` files - They use whatever `.env` is already present - Use them for **quick restarts** when working on a specific service -**Master scripts (`start-dev.sh`, `start-test.sh`) set the environment:** -- `start-dev.sh` → uses existing `.env` files (DEV mode) +**Master scripts (`start-prod.sh`, `start-test.sh`) set the environment:** +- `start-prod.sh` → uses existing `.env` files (DEV mode) - `start-test.sh` → copies `.env.test` → `.env` (TEST mode) **Recommended workflow:** ```bash # Morning: Start full stack with environment selection -./start-dev.sh # DEV mode - sets up .env files +./start-prod.sh # DEV mode - sets up .env files # During development: Quick service restarts -./frontend.sh restart # Frontend only (~7s) +./start-frontend.sh restart # Frontend only (~7s) ./backend-reports.sh restart # Reports backend only (~30s) -# ⚠️ Individual scripts inherit the environment set by start-dev.sh +# ⚠️ Individual scripts inherit the environment set by start-prod.sh # End of day: Stop everything -./start-dev.sh stop +./start-prod.sh stop ``` **Common scenarios:** ```bash # Scenario 1: Working on frontend only -./start-dev.sh # Start everything once -./frontend.sh restart # Restart frontend multiple times (fast!) +./start-prod.sh # Start everything once +./start-frontend.sh restart # Restart frontend multiple times (fast!) # Scenario 2: Debugging a single backend -./start-dev.sh stop # Stop all -./ssh_tunnel.sh start # Infrastructure only +./start-prod.sh stop # Stop all +./ssh-tunnel-prod.sh start # Infrastructure only ./backend-reports.sh start # Just the backend you need -./frontend.sh start # Just the frontend +./start-frontend.sh start # Just the frontend # Scenario 3: Testing mode ./start-test.sh # Starts everything in TEST mode @@ -295,9 +305,9 @@ BACKEND_API_URL=http://localhost:8001 ### Component-Specific - `README.md` - Main application README -- `reports-app/backend/README.md` - Backend specifics -- `reports-app/frontend/README.md` - Frontend guide -- `reports-app/frontend/tests/README.md` - Frontend testing +- `backend/ modules and CLAUDE.md` - Backend specifics +- `src/ and docs/MONOLITH_ARCHITECTURE.md` - Frontend guide +- `E2E testing guide in docs/` - Frontend testing - `reports-app/telegram-bot/README.md` - Telegram bot guide - `reports-app/telegram-bot/TELEGRAM_COMMANDS.md` - Bot commands diff --git a/TESTING_CHECKLIST.md b/TESTING_CHECKLIST.md deleted file mode 100644 index d195353..0000000 --- a/TESTING_CHECKLIST.md +++ /dev/null @@ -1,214 +0,0 @@ -# ROA2WEB Unified App - Integration Testing Checklist - -## Pre-Test Setup - -- [ ] Stop any previously running services: `./start-test.sh stop` -- [ ] Verify SSH tunnel is configured: `./ssh-tunnel-test.sh status` -- [ ] Start all services: `./start-test.sh` -- [ ] Wait for all services to start (check logs if needed) - -## Service Health Checks - -### Backend Services -- [ ] Reports Backend (8001): http://localhost:8001/health -- [ ] Reports API Docs: http://localhost:8001/docs -- [ ] Data Entry Backend (8003): http://localhost:8003/health -- [ ] Data Entry API Docs: http://localhost:8003/docs -- [ ] Telegram Bot Internal API (8002): Should be running (check logs: `/tmp/telegram_bot.log`) - -### Frontend -- [ ] Unified Frontend loads: http://localhost:3000 -- [ ] No console errors in browser DevTools -- [ ] Login page displays correctly - -## Authentication Flow - -### Login -- [ ] Navigate to http://localhost:3000 -- [ ] Should redirect to `/login` automatically -- [ ] Enter valid test credentials -- [ ] Login succeeds and redirects to dashboard -- [ ] User info displays in header (username, company selector) -- [ ] Access token stored in localStorage -- [ ] JWT contains correct user info and companies - -### Session Persistence -- [ ] Refresh page - user remains logged in -- [ ] Close tab and reopen - user remains logged in -- [ ] Open in new tab - user is already logged in - -## Reports Module (http://localhost:3000/reports) - -### Navigation -- [ ] Click "Rapoarte" in menu -- [ ] Dashboard loads at `/reports/dashboard` -- [ ] No console errors -- [ ] Company selector works (change company) -- [ ] Period selector works (change accounting period) - -### Dashboard Widgets -- [ ] Metrics cards display (Sales, Purchases, etc.) -- [ ] Charts render correctly -- [ ] Data loads from Reports API (8001) -- [ ] Check Network tab: requests go to `/api/reports/*` - -### Reports Pages -- [ ] Navigate to "Facturi Clienți" (`/reports/invoices/sales`) -- [ ] Table loads with data -- [ ] Filters work (date range, company, status) -- [ ] Pagination works -- [ ] Export buttons work (Excel, PDF) -- [ ] Invoice details modal opens -- [ ] Navigate to "Facturi Furnizori" (`/reports/invoices/purchases`) -- [ ] Verify same functionality as sales invoices - -### Treasury Reports -- [ ] Navigate to "Trezorerie" (`/reports/treasury`) -- [ ] Cash flow data loads -- [ ] Charts display correctly -- [ ] Date filters work - -### Error Boundary Testing -- [ ] Manually trigger an error in Reports module (e.g., bad API call) -- [ ] ErrorBoundary catches the error -- [ ] Error message displays: "A apărut o eroare în modulul Rapoarte" -- [ ] Other modules (Data Entry) remain functional - -## Data Entry Module (http://localhost:3000/data-entry) - -### Navigation -- [ ] Click "Introduceri" in menu -- [ ] Receipts list loads at `/data-entry/receipts` -- [ ] No console errors -- [ ] Check Network tab: requests go to `/api/data-entry/*` - -### Receipts List -- [ ] Table displays receipts -- [ ] Status badges display correctly (DRAFT, PENDING, APPROVED) -- [ ] Filters work (date range, status, user) -- [ ] Create new receipt button visible - -### Create Receipt -- [ ] Click "Adaugă Bon Fiscal" -- [ ] Form displays at `/data-entry/receipts/new` -- [ ] Partner/Supplier dropdown loads from Data Entry API -- [ ] Expense type dropdown works -- [ ] Date picker works -- [ ] Amount fields accept input -- [ ] File upload works (image/PDF) -- [ ] Save as DRAFT works -- [ ] Receipt appears in list with DRAFT status - -### Edit Receipt -- [ ] Click edit on a DRAFT receipt -- [ ] Form loads with existing data -- [ ] Modify fields -- [ ] Save changes - updates successfully -- [ ] Delete receipt - removes from list - -### Submit for Review -- [ ] Open a DRAFT receipt -- [ ] Click "Trimite spre aprobare" -- [ ] Status changes to PENDING_REVIEW -- [ ] Accounting entries auto-generated -- [ ] Receipt is read-only in PENDING state - -### Approval Workflow (Accountant Role) -- [ ] Login as accountant user -- [ ] See pending receipts -- [ ] Open PENDING receipt -- [ ] Review accounting entries -- [ ] Approve receipt - status changes to APPROVED -- [ ] Receipt becomes fully read-only - -### Error Boundary Testing -- [ ] Manually trigger an error in Data Entry module -- [ ] ErrorBoundary catches the error -- [ ] Error message displays: "A apărut o eroare în modulul Introduceri" -- [ ] Other modules (Reports) remain functional - -## Cross-Module Testing - -### Module Switching -- [ ] Start in Reports module -- [ ] Navigate to Data Entry module -- [ ] Return to Reports module -- [ ] Company selection persists across modules -- [ ] No console errors during switching -- [ ] No memory leaks (check DevTools Memory tab) - -### Shared State -- [ ] Login state shared (logout in one module logs out everywhere) -- [ ] Company selection shared (change company affects both modules) -- [ ] Period selection shared (for modules that use it) - -### API Isolation -- [ ] Reports module only calls `/api/reports/*` -- [ ] Data Entry module only calls `/api/data-entry/*` -- [ ] No cross-contamination of API calls -- [ ] Auth headers included in all requests - -## Logout Flow - -- [ ] Click logout button in header -- [ ] User redirected to `/login` -- [ ] Access token removed from localStorage -- [ ] Cannot access protected routes without re-login -- [ ] Attempting to access `/reports` or `/data-entry` redirects to login - -## Browser Compatibility - -- [ ] Chrome/Edge (latest) -- [ ] Firefox (latest) -- [ ] Safari (if available) - -## Responsive Design - -- [ ] Desktop (1920x1080) -- [ ] Laptop (1366x768) -- [ ] Tablet (768x1024) -- [ ] Mobile (375x667) - -## Performance - -- [ ] Initial page load < 3s -- [ ] Navigation between modules smooth -- [ ] No unnecessary re-renders (check React DevTools) -- [ ] API responses cached appropriately -- [ ] Bundle sizes reasonable (check Network tab) - -## Build & Production - -- [ ] Run `npm run build` -- [ ] Build completes without errors -- [ ] Dist folder created with proper structure -- [ ] index.html exists -- [ ] Assets folder has JS/CSS bundles -- [ ] Chunk splitting works (separate bundles for modules) -- [ ] Source maps generated (for debugging) - -## Cleanup - -- [ ] Stop all services: `./start-test.sh stop` -- [ ] Verify all ports released (8001, 8003, 8002, 3000) -- [ ] SSH tunnel stopped -- [ ] No lingering processes - -## Issues Found - -Document any issues discovered during testing: - -| Issue | Module | Severity | Description | Status | -|-------|--------|----------|-------------|--------| -| | | | | | - -## Sign-Off - -- **Tester**: _______________ -- **Date**: _______________ -- **Environment**: TEST / DEV / PROD -- **Status**: PASS / FAIL / NEEDS FIXES - -## Notes - -Additional observations or comments: diff --git a/backend-data-entry.sh b/backend-data-entry.sh deleted file mode 100644 index 4aed10e..0000000 --- a/backend-data-entry.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/bin/bash -# Backend Data Entry Service Control Script for ROA2WEB Unified App -# Manages the FastAPI Data Entry backend on port 8003 - -# Script directory -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$SCRIPT_DIR" - -# Source helper functions -source "$SCRIPT_DIR/scripts/service-helpers.sh" - -# Service configuration -SERVICE_NAME="Data Entry Backend" -PORT=8003 -LOG_FILE="/tmp/data-entry-backend.log" -BACKEND_DIR="$ROOT_DIR/data-entry-app/backend" -VENV_DIR="$BACKEND_DIR/venv" -ENV_FILE="$BACKEND_DIR/.env" - -# Function to start backend -start_backend() { - print_header "Starting $SERVICE_NAME" - - # Check if port is already in use - if ! check_port_available $PORT "$SERVICE_NAME"; then - print_warning "$SERVICE_NAME may already be running" - print_info "Use './backend-data-entry.sh status' to check or './backend-data-entry.sh stop' to stop it" - return 1 - fi - - # Check backend directory - if [ ! -d "$BACKEND_DIR" ]; then - print_error "Backend directory not found: $BACKEND_DIR" - return 1 - fi - - # Check virtual environment - if [ ! -d "$VENV_DIR" ]; then - print_error "Virtual environment not found: $VENV_DIR" - print_info "Please run setup first: cd data-entry-app/backend && python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt" - return 1 - fi - - # Check .env file - if [ ! -f "$ENV_FILE" ]; then - print_warning ".env file not found: $ENV_FILE" - print_info "Database connection may fail" - fi - - # Start backend - print_info "Starting FastAPI server..." - cd "$BACKEND_DIR" - - # Activate venv and start uvicorn in background - ( - source venv/bin/activate - nohup uvicorn app.main:app --host 0.0.0.0 --port $PORT > "$LOG_FILE" 2>&1 & - echo $! > /tmp/data-entry-backend.pid - ) - - local pid=$(cat /tmp/data-entry-backend.pid 2>/dev/null) - print_info "Started with PID: $pid" - print_info "Log file: $LOG_FILE" - - # Wait for port to be ready (Data Entry takes longer due to SQLite migrations) - if wait_for_port $PORT "$SERVICE_NAME" 35; then - # Check health endpoint - print_info "Checking health endpoint..." - sleep 2 - local health_status=$(curl -s http://localhost:$PORT/health 2>&1 | head -1) - - if echo "$health_status" | grep -q "ok"; then - print_success "$SERVICE_NAME started successfully and is healthy!" - else - print_success "$SERVICE_NAME started (health check inconclusive)" - fi - - echo "" - print_info "🌐 API URLs:" - echo " • API Docs: http://localhost:$PORT/docs" - echo " • Health: http://localhost:$PORT/health" - echo "" - print_info "📄 View logs: tail -f $LOG_FILE" - return 0 - else - print_error "Failed to start $SERVICE_NAME (timeout waiting for port $PORT)" - print_info "Check logs: tail -20 $LOG_FILE" - rm -f /tmp/data-entry-backend.pid - return 1 - fi -} - -# Function to stop backend -stop_backend() { - print_header "Stopping $SERVICE_NAME" - - kill_port $PORT "$SERVICE_NAME" - - # Clean up PID file - if [ -f "/tmp/data-entry-backend.pid" ]; then - rm /tmp/data-entry-backend.pid - fi - - return 0 -} - -# Function to show backend status -status_backend() { - print_header "$SERVICE_NAME Status" - - if check_service_status $PORT "$SERVICE_NAME"; then - echo "" - - # Check health endpoint - print_info "Health check:" - local health=$(curl -s http://localhost:$PORT/health 2>&1) - if echo "$health" | grep -q "ok"; then - print_success "Health endpoint: OK" - else - print_warning "Health endpoint: Not responding" - fi - - # Show recent logs - if [ -f "$LOG_FILE" ]; then - echo "" - tail_logs "$LOG_FILE" 10 - fi - return 0 - else - echo "" - print_warning "Service is not running" - - # Show last logs if available - if [ -f "$LOG_FILE" ]; then - echo "" - print_info "Last logs before shutdown:" - tail_logs "$LOG_FILE" 10 - fi - return 1 - fi -} - -# Main script logic -case "${1:-}" in - start) - start_backend - ;; - stop) - stop_backend - ;; - status) - status_backend - ;; - *) - echo "Usage: $0 {start|stop|status}" - echo "" - echo "Commands:" - echo " start - Start the Data Entry backend API server" - echo " stop - Stop the Data Entry backend API server" - echo " status - Show backend status and health check" - echo "" - echo "Examples:" - echo " ./backend-data-entry.sh start # Start backend" - echo " ./backend-data-entry.sh status # Check if running" - echo " ./backend-data-entry.sh stop # Stop backend" - exit 1 - ;; -esac diff --git a/backend-reports.sh b/backend-reports.sh deleted file mode 100644 index cbd40f3..0000000 --- a/backend-reports.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/bin/bash -# Backend Reports Service Control Script for ROA2WEB Unified App -# Manages the FastAPI Reports backend on port 8001 - -# Script directory -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$SCRIPT_DIR" - -# Source helper functions -source "$SCRIPT_DIR/scripts/service-helpers.sh" - -# Service configuration -SERVICE_NAME="Reports Backend" -PORT=8001 -LOG_FILE="/tmp/reports-backend.log" -BACKEND_DIR="$ROOT_DIR/reports-app/backend" -VENV_DIR="$BACKEND_DIR/venv" -ENV_FILE="$BACKEND_DIR/.env" - -# Function to start backend -start_backend() { - print_header "Starting $SERVICE_NAME" - - # Check if port is already in use - if ! check_port_available $PORT "$SERVICE_NAME"; then - print_warning "$SERVICE_NAME may already be running" - print_info "Use './backend-reports.sh status' to check or './backend-reports.sh stop' to stop it" - return 1 - fi - - # Check backend directory - if [ ! -d "$BACKEND_DIR" ]; then - print_error "Backend directory not found: $BACKEND_DIR" - return 1 - fi - - # Check virtual environment - if [ ! -d "$VENV_DIR" ]; then - print_error "Virtual environment not found: $VENV_DIR" - print_info "Please run setup first: cd reports-app/backend && python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt" - return 1 - fi - - # Check .env file - if [ ! -f "$ENV_FILE" ]; then - print_warning ".env file not found: $ENV_FILE" - print_info "Database connection may fail" - fi - - # Start backend - print_info "Starting FastAPI server..." - cd "$BACKEND_DIR" - - # Activate venv and start uvicorn in background - ( - source venv/bin/activate - nohup uvicorn app.main:app --host 0.0.0.0 --port $PORT > "$LOG_FILE" 2>&1 & - echo $! > /tmp/reports-backend.pid - ) - - local pid=$(cat /tmp/reports-backend.pid 2>/dev/null) - print_info "Started with PID: $pid" - print_info "Log file: $LOG_FILE" - - # Wait for port to be ready - if wait_for_port $PORT "$SERVICE_NAME" 30; then - # Check health endpoint - print_info "Checking health endpoint..." - sleep 2 - local health_status=$(curl -s http://localhost:$PORT/health 2>&1 | head -1) - - if echo "$health_status" | grep -q "ok"; then - print_success "$SERVICE_NAME started successfully and is healthy!" - else - print_success "$SERVICE_NAME started (health check inconclusive)" - fi - - echo "" - print_info "🌐 API URLs:" - echo " • API Docs: http://localhost:$PORT/docs" - echo " • Health: http://localhost:$PORT/health" - echo "" - print_info "📄 View logs: tail -f $LOG_FILE" - return 0 - else - print_error "Failed to start $SERVICE_NAME (timeout waiting for port $PORT)" - print_info "Check logs: tail -20 $LOG_FILE" - rm -f /tmp/reports-backend.pid - return 1 - fi -} - -# Function to stop backend -stop_backend() { - print_header "Stopping $SERVICE_NAME" - - kill_port $PORT "$SERVICE_NAME" - - # Clean up PID file - if [ -f "/tmp/reports-backend.pid" ]; then - rm /tmp/reports-backend.pid - fi - - return 0 -} - -# Function to show backend status -status_backend() { - print_header "$SERVICE_NAME Status" - - if check_service_status $PORT "$SERVICE_NAME"; then - echo "" - - # Check health endpoint - print_info "Health check:" - local health=$(curl -s http://localhost:$PORT/health 2>&1) - if echo "$health" | grep -q "ok"; then - print_success "Health endpoint: OK" - else - print_warning "Health endpoint: Not responding" - fi - - # Show recent logs - if [ -f "$LOG_FILE" ]; then - echo "" - tail_logs "$LOG_FILE" 10 - fi - return 0 - else - echo "" - print_warning "Service is not running" - - # Show last logs if available - if [ -f "$LOG_FILE" ]; then - echo "" - print_info "Last logs before shutdown:" - tail_logs "$LOG_FILE" 10 - fi - return 1 - fi -} - -# Main script logic -case "${1:-}" in - start) - start_backend - ;; - stop) - stop_backend - ;; - status) - status_backend - ;; - *) - echo "Usage: $0 {start|stop|status}" - echo "" - echo "Commands:" - echo " start - Start the Reports backend API server" - echo " stop - Stop the Reports backend API server" - echo " status - Show backend status and health check" - echo "" - echo "Examples:" - echo " ./backend-reports.sh start # Start backend" - echo " ./backend-reports.sh status # Check if running" - echo " ./backend-reports.sh stop # Stop backend" - exit 1 - ;; -esac diff --git a/backend/.env.dev.example b/backend/.env.dev.example new file mode 100644 index 0000000..13923d5 --- /dev/null +++ b/backend/.env.dev.example @@ -0,0 +1,147 @@ +# ============================================================================ +# ROA2WEB Unified Backend - Environment Configuration (Development) +# ============================================================================ +# Single backend process serving Reports, Data Entry, and Telegram modules +# IMPORTANT: Never commit this file to git! + +# ============================================================================ +# ORACLE DATABASE CONFIGURATION (REQUIRED - Shared by all modules) +# ============================================================================ +# Connection to CONTAFIN_ORACLE schema for authentication and user management +# Each company is a separate schema in Oracle Database +# Development: Through SSH tunnel (localhost:1526) + +ORACLE_USER=CONTAFIN_ORACLE +ORACLE_PASSWORD=your_oracle_password_here +ORACLE_HOST=localhost +ORACLE_PORT=1526 +ORACLE_SID=ROA + +# Development: Start SSH tunnel before running backend +# ./ssh_tunnel.sh start (production) or ./ssh-tunnel-test.sh start (test) + +# ============================================================================ +# JWT AUTHENTICATION (REQUIRED - Shared by all modules) +# ============================================================================ +# Used for JWT token generation and validation (shared/auth/jwt_handler.py) + +JWT_SECRET_KEY=generate_with_secrets_token_urlsafe_32 +JWT_ALGORITHM=HS256 + +# Token expiration settings (used by shared/auth/jwt_handler.py) +ACCESS_TOKEN_EXPIRE_MINUTES=30 +REFRESH_TOKEN_EXPIRE_DAYS=7 + +# ============================================================================ +# SESSION SECURITY - EMAIL 2FA (REQUIRED for Telegram email login) +# ============================================================================ +# Used by Telegram module for session token validation +# Must match between backend and Telegram bot + +AUTH_SESSION_SECRET=generate_with_secrets_token_urlsafe_32 + +# ============================================================================ +# SERVER CONFIGURATION +# ============================================================================ +# Unified backend server settings + +API_HOST=0.0.0.0 +API_PORT=8000 +DEBUG=true + +# CORS Origins (comma-separated, includes both old and new frontend ports) +CORS_ORIGINS=http://localhost:3000,http://localhost:3010,http://localhost:5173 + +# ============================================================================ +# REPORTS MODULE - CACHE CONFIGURATION (OPTIONAL - defaults provided) +# ============================================================================ +# Two-tier hybrid cache system (L1: in-memory LRU, L2: SQLite persistent) +# Used by backend/modules/reports/cache/config.py + +# Core Settings +CACHE_ENABLED=True +CACHE_TYPE=hybrid +CACHE_SQLITE_PATH=./data/cache/roa2web_cache.db +CACHE_MEMORY_MAX_SIZE=1000 +CACHE_DEFAULT_TTL=900 + +# TTL per Cache Type (seconds) +CACHE_TTL_SCHEMA=86400 +CACHE_TTL_COMPANIES=1800 +CACHE_TTL_DASHBOARD_SUMMARY=1800 +CACHE_TTL_DASHBOARD_TRENDS=1800 +CACHE_TTL_INVOICES=600 +CACHE_TTL_INVOICES_SUMMARY=900 +CACHE_TTL_TREASURY=600 + +# Maintenance +CACHE_CLEANUP_INTERVAL=3600 + +# Event-Based Invalidation (experimental) +CACHE_AUTO_INVALIDATE=False +CACHE_CHECK_INTERVAL=300 + +# Performance Tracking +CACHE_TRACK_PERFORMANCE=True +CACHE_BENCHMARK_ON_STARTUP=False + +# ============================================================================ +# DATA ENTRY MODULE - CONFIGURATION +# ============================================================================ +# Data Entry module settings (receipts, OCR, etc.) + +# Environment identifier (dev/test/prod) +ORACLE_ENV=dev + +# SQLite Database (development) +SQLITE_DATABASE_PATH=data/receipts/receipts_dev.db + +# File uploads +UPLOAD_PATH=data/receipts/uploads +MAX_UPLOAD_SIZE_MB=10 + +# Test company (for development testing) +TEST_COMPANY_ID=110 +TEST_COMPANY_SCHEMA=MARIUSM_AUTO + +# ============================================================================ +# TELEGRAM MODULE - BOT CONFIGURATION (REQUIRED for Telegram features) +# ============================================================================ +# Obtain bot token from @BotFather on Telegram + +TELEGRAM_BOT_TOKEN=your_bot_token_from_botfather + +# Backend URL for bot to communicate with API +BACKEND_URL=http://localhost:8000 + +# Internal API port (bot's internal API for backend callbacks) +INTERNAL_API_PORT=8002 + +# Enable internal API documentation (development only) +ENABLE_DOCS=false + +# ============================================================================ +# TELEGRAM MODULE - EMAIL AUTHENTICATION (SMTP) (REQUIRED for email 2FA) +# ============================================================================ +# Required for email-based 2FA authentication flow +# Users can login with email + password instead of web app linking + +# SMTP Server Configuration +SMTP_HOST=mail.romfast.ro +SMTP_PORT=587 +SMTP_USER=ups@romfast.ro +SMTP_PASSWORD=your_smtp_password_here +SMTP_FROM_EMAIL=ups@romfast.ro +SMTP_FROM_NAME=ROA2WEB +SMTP_USE_TLS=true + +# Email Retry Settings +EMAIL_MAX_RETRIES=3 +EMAIL_RETRY_DELAY=2.0 + +# ============================================================================ +# TELEGRAM MODULE - DATABASE (SQLite for bot data) +# ============================================================================ +# Separate SQLite database for Telegram bot auth codes and sessions + +TELEGRAM_SQLITE_DATABASE_PATH=data/telegram/telegram.db diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..ef4916e --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,146 @@ +# ============================================================================ +# ROA2WEB Unified Backend - Environment Configuration Template +# ============================================================================ +# Single backend process serving Reports, Data Entry, and Telegram modules +# +# SETUP INSTRUCTIONS: +# 1. Copy this template: cp .env.example .env.dev +# 2. Fill in your actual values in .env.dev +# 3. Run: ./start-dev.sh (auto-copies .env.dev to .env) +# +# ENVIRONMENT FILES: +# - .env.dev → Development config (committed to git with real values) +# - .env.test → Test config (committed to git) +# - .env.prod → Production config template (committed, use placeholders!) +# - .env → Active config (auto-generated, NOT committed) +# +# IMPORTANT: Never manually edit .env - edit .env.dev instead! + +# ============================================================================ +# ORACLE DATABASE CONFIGURATION (REQUIRED - Shared by all modules) +# ============================================================================ +# Connection to CONTAFIN_ORACLE schema for authentication and user management +# Each company is a separate schema in Oracle Database +# Development: Through SSH tunnel (localhost:1526) +# Windows Production: Direct connection to Oracle server + +ORACLE_USER=CONTAFIN_ORACLE +ORACLE_PASSWORD=SET_IN_PRODUCTION_ENV +ORACLE_HOST=localhost +ORACLE_PORT=1526 +ORACLE_SID=ROA + +# Development Only: Start SSH tunnel before running backend +# ./ssh_tunnel.sh start +# ./ssh_tunnel.sh status + +# ============================================================================ +# JWT AUTHENTICATION (REQUIRED - Shared by all modules) +# ============================================================================ +# Used for JWT token generation and validation (shared/auth/jwt_handler.py) +# Generate strong secret: python3 -c "import secrets; print(secrets.token_urlsafe(32))" + +JWT_SECRET_KEY=GENERATE_STRONG_SECRET_IN_PRODUCTION +JWT_ALGORITHM=HS256 + +# Token expiration settings (used by shared/auth/jwt_handler.py) +ACCESS_TOKEN_EXPIRE_MINUTES=30 +REFRESH_TOKEN_EXPIRE_DAYS=7 + +# ============================================================================ +# SESSION SECURITY - EMAIL 2FA (REQUIRED for Telegram email login) +# ============================================================================ +# Used by Telegram module for session token validation +# Generate with: python3 -c "import secrets; print(secrets.token_urlsafe(32))" + +AUTH_SESSION_SECRET=your-secure-random-secret-here-min-32-chars + +# ============================================================================ +# SERVER CONFIGURATION +# ============================================================================ +# Unified backend server settings + +API_HOST=0.0.0.0 +API_PORT=8000 +DEBUG=false + +# CORS Origins (comma-separated) +CORS_ORIGINS=http://localhost:3000,http://localhost:5173 + +# ============================================================================ +# REPORTS MODULE - CACHE CONFIGURATION (OPTIONAL - defaults provided) +# ============================================================================ +# Two-tier hybrid cache system (L1: in-memory LRU, L2: SQLite persistent) +# Used by backend/modules/reports/cache/config.py + +# Core Settings +REPORTS_CACHE_ENABLED=True +REPORTS_CACHE_TYPE=hybrid +REPORTS_CACHE_SQLITE_PATH=./data/cache/roa2web_cache.db +REPORTS_CACHE_MEMORY_MAX_SIZE=1000 +REPORTS_CACHE_DEFAULT_TTL=900 + +# TTL per Cache Type (seconds) +REPORTS_CACHE_TTL_SCHEMA=86400 +REPORTS_CACHE_TTL_COMPANIES=1800 +REPORTS_CACHE_TTL_DASHBOARD_SUMMARY=1800 +REPORTS_CACHE_TTL_DASHBOARD_TRENDS=1800 +REPORTS_CACHE_TTL_INVOICES=600 +REPORTS_CACHE_TTL_INVOICES_SUMMARY=900 +REPORTS_CACHE_TTL_TREASURY=600 + +# Maintenance +REPORTS_CACHE_CLEANUP_INTERVAL=3600 + +# Event-Based Invalidation (experimental) +REPORTS_CACHE_AUTO_INVALIDATE=False +REPORTS_CACHE_CHECK_INTERVAL=300 + +# Performance Tracking +REPORTS_CACHE_TRACK_PERFORMANCE=True +REPORTS_CACHE_BENCHMARK_ON_STARTUP=False + +# ============================================================================ +# DATA ENTRY MODULE - CONFIGURATION +# ============================================================================ +# Data Entry module settings (receipts, OCR, etc.) + +# SQLite Database +DATA_ENTRY_SQLITE_DATABASE_PATH=data/receipts/receipts.db + +# File uploads +DATA_ENTRY_UPLOAD_PATH=data/receipts/uploads +DATA_ENTRY_MAX_UPLOAD_SIZE_MB=10 + +# ============================================================================ +# TELEGRAM MODULE - BOT CONFIGURATION (REQUIRED for Telegram features) +# ============================================================================ +# Obtain bot token from @BotFather on Telegram + +TELEGRAM_BOT_TOKEN=your_bot_token_here + +# ============================================================================ +# TELEGRAM MODULE - EMAIL AUTHENTICATION (SMTP) (REQUIRED for email 2FA) +# ============================================================================ +# Required for email-based 2FA authentication flow +# Users can login with email + password instead of web app linking + +# SMTP Server Configuration +TELEGRAM_SMTP_HOST=mail.romfast.ro +TELEGRAM_SMTP_PORT=587 +TELEGRAM_SMTP_USER=ups@romfast.ro +TELEGRAM_SMTP_PASSWORD=your_smtp_password_here +TELEGRAM_SMTP_FROM_EMAIL=ups@romfast.ro +TELEGRAM_SMTP_FROM_NAME=ROA2WEB +TELEGRAM_SMTP_USE_TLS=true + +# Email Retry Settings +TELEGRAM_EMAIL_MAX_RETRIES=3 +TELEGRAM_EMAIL_RETRY_DELAY=2.0 + +# ============================================================================ +# TELEGRAM MODULE - DATABASE (SQLite for bot data) +# ============================================================================ +# Separate SQLite database for Telegram bot auth codes and sessions + +TELEGRAM_SQLITE_DATABASE_PATH=data/telegram/telegram.db diff --git a/backend/.env.prod.example b/backend/.env.prod.example new file mode 100644 index 0000000..a4561c0 --- /dev/null +++ b/backend/.env.prod.example @@ -0,0 +1,139 @@ +# ============================================================================ +# ROA2WEB Unified Backend - Environment Configuration (PRODUCTION) +# ============================================================================ +# Single backend process serving Reports, Data Entry, and Telegram modules +# IMPORTANT: This is a TEMPLATE - fill in production values before deploying! + +# ============================================================================ +# ORACLE DATABASE CONFIGURATION (REQUIRED - Shared by all modules) +# ============================================================================ +# Connection to CONTAFIN_ORACLE schema for authentication and user management +# PRODUCTION: Direct connection to Oracle server (no SSH tunnel) + +ORACLE_USER=CONTAFIN_ORACLE +ORACLE_PASSWORD=CHANGE_IN_PRODUCTION +ORACLE_HOST=your_oracle_server_ip_or_hostname +ORACLE_PORT=1521 +ORACLE_SID=ROA + +# ============================================================================ +# JWT AUTHENTICATION (REQUIRED - Shared by all modules) +# ============================================================================ +# CRITICAL: Generate new secrets for production! +# python3 -c "import secrets; print(secrets.token_urlsafe(32))" + +JWT_SECRET_KEY=GENERATE_NEW_SECRET_FOR_PRODUCTION +JWT_ALGORITHM=HS256 + +# Token expiration settings +ACCESS_TOKEN_EXPIRE_MINUTES=30 +REFRESH_TOKEN_EXPIRE_DAYS=7 + +# ============================================================================ +# SESSION SECURITY - EMAIL 2FA (REQUIRED for Telegram email login) +# ============================================================================ +# CRITICAL: Generate new secret for production! +# python3 -c "import secrets; print(secrets.token_urlsafe(32))" + +AUTH_SESSION_SECRET=GENERATE_NEW_SECRET_FOR_PRODUCTION + +# ============================================================================ +# SERVER CONFIGURATION +# ============================================================================ +# Unified backend server settings + +API_HOST=0.0.0.0 +API_PORT=8000 +DEBUG=false + +# CORS Origins (comma-separated) - Update with production frontend URL +CORS_ORIGINS=https://your-production-domain.com,http://localhost:3000 + +# ============================================================================ +# REPORTS MODULE - CACHE CONFIGURATION (OPTIONAL - defaults provided) +# ============================================================================ +# Two-tier hybrid cache system (L1: in-memory LRU, L2: SQLite persistent) + +# Core Settings +CACHE_ENABLED=True +CACHE_TYPE=hybrid +CACHE_SQLITE_PATH=./data/cache/roa2web_cache_prod.db +CACHE_MEMORY_MAX_SIZE=1000 +CACHE_DEFAULT_TTL=900 + +# TTL per Cache Type (seconds) +CACHE_TTL_SCHEMA=86400 +CACHE_TTL_COMPANIES=1800 +CACHE_TTL_DASHBOARD_SUMMARY=1800 +CACHE_TTL_DASHBOARD_TRENDS=1800 +CACHE_TTL_INVOICES=600 +CACHE_TTL_INVOICES_SUMMARY=900 +CACHE_TTL_TREASURY=600 + +# Maintenance +CACHE_CLEANUP_INTERVAL=3600 + +# Event-Based Invalidation (experimental) +CACHE_AUTO_INVALIDATE=False +CACHE_CHECK_INTERVAL=300 + +# Performance Tracking +CACHE_TRACK_PERFORMANCE=True +CACHE_BENCHMARK_ON_STARTUP=False + +# ============================================================================ +# DATA ENTRY MODULE - CONFIGURATION +# ============================================================================ +# Data Entry module settings (receipts, OCR, etc.) + +# Environment identifier +ORACLE_ENV=prod + +# SQLite Database (production) +SQLITE_DATABASE_PATH=data/receipts/receipts_prod.db + +# File uploads +UPLOAD_PATH=data/receipts/uploads +MAX_UPLOAD_SIZE_MB=10 + +# ============================================================================ +# TELEGRAM MODULE - BOT CONFIGURATION (REQUIRED for Telegram features) +# ============================================================================ +# Obtain bot token from @BotFather on Telegram +# CRITICAL: Use production bot token, not development! + +TELEGRAM_BOT_TOKEN=your_bot_token_from_botfather + +# Backend URL for bot to communicate with API +BACKEND_URL=http://localhost:8000 + +# Internal API port (bot's internal API for backend callbacks) +INTERNAL_API_PORT=8002 + +# Enable internal API documentation (DISABLE in production!) +ENABLE_DOCS=false + +# ============================================================================ +# TELEGRAM MODULE - EMAIL AUTHENTICATION (SMTP) (REQUIRED for email 2FA) +# ============================================================================ +# CRITICAL: Update with production SMTP credentials + +# SMTP Server Configuration +SMTP_HOST=mail.romfast.ro +SMTP_PORT=587 +SMTP_USER=ups@romfast.ro +SMTP_PASSWORD=CHANGE_IN_PRODUCTION +SMTP_FROM_EMAIL=ups@romfast.ro +SMTP_FROM_NAME=ROA2WEB +SMTP_USE_TLS=true + +# Email Retry Settings +EMAIL_MAX_RETRIES=3 +EMAIL_RETRY_DELAY=2.0 + +# ============================================================================ +# TELEGRAM MODULE - DATABASE (SQLite for bot data) +# ============================================================================ +# Separate SQLite database for Telegram bot auth codes and sessions + +TELEGRAM_SQLITE_DATABASE_PATH=data/telegram/telegram_prod.db diff --git a/backend/.env.test.example b/backend/.env.test.example new file mode 100644 index 0000000..f5f04a0 --- /dev/null +++ b/backend/.env.test.example @@ -0,0 +1,147 @@ +# ============================================================================ +# ROA2WEB Unified Backend - Environment Configuration (TEST) +# ============================================================================ +# TEST environment using Oracle TEST server (10.0.20.121) +# Single backend process serving Reports, Data Entry, and Telegram modules +# IMPORTANT: Never commit this file to git! + +# ============================================================================ +# ORACLE DATABASE CONFIGURATION (REQUIRED - Shared by all modules) +# ============================================================================ +# Connection to CONTAFIN_ORACLE schema for authentication and user management +# TEST: Through SSH tunnel to 10.0.20.121 (localhost:1526) + +ORACLE_USER=CONTAFIN_ORACLE +ORACLE_PASSWORD=your_oracle_password_here +ORACLE_HOST=localhost +ORACLE_PORT=1526 +ORACLE_SID=roa + +# TEST: Start SSH tunnel before running backend +# ./ssh-tunnel-test.sh start + +# ============================================================================ +# JWT AUTHENTICATION (REQUIRED - Shared by all modules) +# ============================================================================ +# Used for JWT token generation and validation (shared/auth/jwt_handler.py) + +JWT_SECRET_KEY=generate_with_secrets_token_urlsafe_32 +JWT_ALGORITHM=HS256 + +# Token expiration settings (used by shared/auth/jwt_handler.py) +ACCESS_TOKEN_EXPIRE_MINUTES=480 +REFRESH_TOKEN_EXPIRE_DAYS=7 + +# ============================================================================ +# SESSION SECURITY - EMAIL 2FA (REQUIRED for Telegram email login) +# ============================================================================ +# Used by Telegram module for session token validation +# Must match between backend and Telegram bot + +AUTH_SESSION_SECRET=generate_with_secrets_token_urlsafe_32 + +# ============================================================================ +# SERVER CONFIGURATION +# ============================================================================ +# Unified backend server settings + +API_HOST=0.0.0.0 +API_PORT=8000 +DEBUG=true + +# CORS Origins (comma-separated, includes both old and new frontend ports) +CORS_ORIGINS=http://localhost:3000,http://localhost:3010,http://localhost:5173 + +# ============================================================================ +# REPORTS MODULE - CACHE CONFIGURATION (OPTIONAL - defaults provided) +# ============================================================================ +# Two-tier hybrid cache system (L1: in-memory LRU, L2: SQLite persistent) +# Used by backend/modules/reports/cache/config.py + +# Core Settings +CACHE_ENABLED=True +CACHE_TYPE=hybrid +CACHE_SQLITE_PATH=./data/cache/roa2web_cache_test.db +CACHE_MEMORY_MAX_SIZE=1000 +CACHE_DEFAULT_TTL=900 + +# TTL per Cache Type (seconds) +CACHE_TTL_SCHEMA=86400 +CACHE_TTL_COMPANIES=1800 +CACHE_TTL_DASHBOARD_SUMMARY=1800 +CACHE_TTL_DASHBOARD_TRENDS=1800 +CACHE_TTL_INVOICES=600 +CACHE_TTL_INVOICES_SUMMARY=900 +CACHE_TTL_TREASURY=600 + +# Maintenance +CACHE_CLEANUP_INTERVAL=3600 + +# Event-Based Invalidation (experimental) +CACHE_AUTO_INVALIDATE=False +CACHE_CHECK_INTERVAL=300 + +# Performance Tracking +CACHE_TRACK_PERFORMANCE=True +CACHE_BENCHMARK_ON_STARTUP=False + +# ============================================================================ +# DATA ENTRY MODULE - CONFIGURATION +# ============================================================================ +# Data Entry module settings (receipts, OCR, etc.) + +# Environment identifier (dev/test/prod) +ORACLE_ENV=test + +# SQLite Database (test) +SQLITE_DATABASE_PATH=data/receipts/receipts_test.db + +# File uploads +UPLOAD_PATH=data/receipts/uploads +MAX_UPLOAD_SIZE_MB=10 + +# Test company (for testing) +TEST_COMPANY_ID=110 +TEST_COMPANY_SCHEMA=MARIUSM_AUTO + +# ============================================================================ +# TELEGRAM MODULE - BOT CONFIGURATION (REQUIRED for Telegram features) +# ============================================================================ +# Obtain bot token from @BotFather on Telegram + +TELEGRAM_BOT_TOKEN=your_bot_token_from_botfather + +# Backend URL for bot to communicate with API +BACKEND_URL=http://localhost:8000 + +# Internal API port (bot's internal API for backend callbacks) +INTERNAL_API_PORT=8002 + +# Enable internal API documentation (development only) +ENABLE_DOCS=false + +# ============================================================================ +# TELEGRAM MODULE - EMAIL AUTHENTICATION (SMTP) (REQUIRED for email 2FA) +# ============================================================================ +# Required for email-based 2FA authentication flow +# Users can login with email + password instead of web app linking + +# SMTP Server Configuration +SMTP_HOST=mail.romfast.ro +SMTP_PORT=587 +SMTP_USER=ups@romfast.ro +SMTP_PASSWORD=your_smtp_password_here +SMTP_FROM_EMAIL=ups@romfast.ro +SMTP_FROM_NAME=ROA2WEB +SMTP_USE_TLS=true + +# Email Retry Settings +EMAIL_MAX_RETRIES=3 +EMAIL_RETRY_DELAY=2.0 + +# ============================================================================ +# TELEGRAM MODULE - DATABASE (SQLite for bot data) +# ============================================================================ +# Separate SQLite database for Telegram bot auth codes and sessions + +TELEGRAM_SQLITE_DATABASE_PATH=data/telegram/telegram_test.db diff --git a/backend/ENV-SETUP.md b/backend/ENV-SETUP.md new file mode 100644 index 0000000..83a7c8c --- /dev/null +++ b/backend/ENV-SETUP.md @@ -0,0 +1,212 @@ +# Environment Configuration Guide + +## Overview + +The unified backend uses environment-specific configuration files that are automatically loaded by startup scripts. + +**SECURITY**: All `.env*` files (except `.env*.example`) contain real credentials and are **NEVER committed to git**. + +## File Structure + +``` +backend/ +├── .env.prod.example # Production template (COMMITTED - no credentials) +├── .env.test.example # Test template (COMMITTED - no credentials) +├── .env.prod.example # Production template (COMMITTED - no credentials) +├── .env.example # Generic template (COMMITTED) +├── .env.prod # Production config (IGNORED - real credentials) +├── .env.test # Test config (IGNORED - real credentials) +├── .env.prod # Production config (IGNORED - real credentials) +└── .env # Active config (IGNORED - auto-generated) +``` + +## First-Time Setup + +### Production +```bash +# 1. Copy template +cp backend/.env.prod.example backend/.env.prod + +# 2. Edit with your credentials +vim backend/.env.prod + +# 3. Fill in: +# - ORACLE_PASSWORD +# - JWT_SECRET_KEY (generate with: python3 -c "import secrets; print(secrets.token_urlsafe(32))") +# - AUTH_SESSION_SECRET (generate with: python3 -c "import secrets; print(secrets.token_urlsafe(32))") +# - TELEGRAM_BOT_TOKEN (from @BotFather) +# - SMTP_PASSWORD + +# 4. Start +./start-prod.sh +``` + +### Test +```bash +# Same process with .env.test +cp backend/.env.test.example backend/.env.test +vim backend/.env.test +# Fill in TEST credentials (separate from dev!) +./start-test.sh +``` + +### Production +```bash +# Same process with .env.prod +cp backend/.env.prod.example backend/.env.prod +vim backend/.env.prod +# Fill in PRODUCTION credentials (generate NEW secrets!) +./start-backend.sh start +``` + +## How It Works + +### Production +```bash +./start-prod.sh # Checks for .env.prod → copies to .env → starts backend +``` + +### Test +```bash +./start-test.sh # Checks for .env.test → copies to .env → starts backend +``` + +### Production +```bash +# Manual setup (one-time) +cp .env.prod.example .env.prod +vim .env.prod # Fill in credentials +# Then start +./start-backend.sh start +``` + +## Important Rules + +### ✅ DO +- Copy `.env.*.example` to `.env.*` and fill in real credentials +- Edit `.env.prod` for production changes +- Edit `.env.test` for test environment changes +- Edit `.env.prod` for production +- Generate **new** secrets for each environment +- Keep `.env.prod`, `.env.test`, `.env.prod` **local only** (never commit!) + +### ❌ DON'T +- Don't commit `.env`, `.env.prod`, `.env.test`, or `.env.prod` (they're in .gitignore) +- Don't manually edit `.env` (it's auto-generated!) +- Don't use same secrets across environments +- Don't share credentials via git (use secure channels) +- Don't put real credentials in `.env*.example` files + +## Environment Differences + +| Setting | .env.prod | .env.test | .env.prod | +|---------|----------|-----------|-----------| +| Oracle SID | `ROA` | `roa` | `ROA` | +| JWT Expire | 30 min | 480 min | 30 min | +| DEBUG | `true` | `true` | `false` | +| Cache DB | `roa2web_cache.db` | `roa2web_cache_test.db` | `roa2web_cache_prod.db` | +| Receipts DB | `receipts_dev.db` | `receipts_test.db` | `receipts_prod.db` | +| Telegram DB | `telegram.db` | `telegram_test.db` | `telegram_prod.db` | + +## Security Notes + +### Template Files (.env.*.example) +These contain **placeholders only**: +- ✅ Safe to commit to git +- ✅ Shared across team +- ✅ No real credentials +- 📖 Used as reference for first-time setup + +### Actual Config Files (.env.prod, .env.test, .env.prod) +These contain **real credentials**: +- ❌ **NEVER commit to git** (in .gitignore) +- ❌ Never share via email/chat +- ✅ Keep local only +- ✅ Generate unique secrets per environment +- 🔐 Share securely if needed (encrypted vault, 1Password, etc.) + +### Active Config (.env) +This is **auto-generated** and **ignored by git**: +- ❌ Never commit to git +- 🔄 Auto-overwritten by startup scripts +- 📝 Edit source files (.env.prod, .env.test) instead + +## Generating Secrets + +For `JWT_SECRET_KEY` and `AUTH_SESSION_SECRET`: +```bash +python3 -c "import secrets; print(secrets.token_urlsafe(32))" +``` + +Generate **different** secrets for dev, test, and production! + +## Quick Reference + +### First Time Setup +```bash +# 1. Copy template +cp backend/.env.prod.example backend/.env.prod + +# 2. Fill credentials +vim backend/.env.prod + +# 3. Start +./start-prod.sh +``` + +### Changing Configuration +```bash +# 1. Edit source file +vim backend/.env.prod + +# 2. Restart to apply +./start-prod.sh +``` + +### Production Deployment +```bash +# 1. Copy template +cp backend/.env.prod.example backend/.env.prod + +# 2. Fill in PRODUCTION values +vim backend/.env.prod + +# 3. Generate NEW secrets +python3 -c "import secrets; print(secrets.token_urlsafe(32))" + +# 4. Start backend +./start-backend.sh start +``` + +## Troubleshooting + +### "Wrong database" error +Check that you're using the correct startup script: +- Production: `./start-prod.sh` (uses `.env.prod`) +- Test: `./start-test.sh` (uses `.env.test`) + +### ".env.prod not found" error +First-time setup required: +```bash +cp backend/.env.prod.example backend/.env.prod +vim backend/.env.prod # Fill in your credentials +``` + +### Changes not taking effect +The `.env` file is regenerated on each start. Edit the source file (`.env.prod` or `.env.test`) instead. + +### Checking what will be committed +```bash +git status backend/.env* +# Should show: +# modified: .env.prod.example (if you changed template) +# nothing else! +``` + +## Team Sharing + +**Templates only** are committed to git: +- Share configuration structure via `.env*.example` +- Each developer creates their own `.env.prod` from template +- Never commit actual credentials +- Use secure channels for sharing sensitive values (1Password, encrypted vault, etc.) diff --git a/backend/QUICK-ENV-REFERENCE.md b/backend/QUICK-ENV-REFERENCE.md new file mode 100644 index 0000000..8b4becd --- /dev/null +++ b/backend/QUICK-ENV-REFERENCE.md @@ -0,0 +1,102 @@ +# Quick Environment Reference + +## 🔒 SECURITY FIRST + +**All `.env*` files (except `.env*.example`) contain real credentials and are NEVER committed to git!** + +## 🚀 First-Time Setup + +```bash +# 1. Copy template with real credentials +cp backend/.env.prod.example backend/.env.prod + +# 2. Edit with YOUR credentials +vim backend/.env.prod + +# 3. Fill in the placeholders: +# - ORACLE_PASSWORD +# - JWT_SECRET_KEY +# - AUTH_SESSION_SECRET +# - TELEGRAM_BOT_TOKEN +# - SMTP_PASSWORD + +# 4. Start production +./start-prod.sh +``` + +## 📋 Daily Usage + +```bash +# Production (uses .env.prod automatically) +./start-prod.sh + +# Test Environment (uses .env.test automatically) +./start-test.sh + +# Quick Restart (uses existing .env) +./start-backend.sh restart +``` + +## ✏️ Changing Configuration + +```bash +# 1. Edit the source file (NOT .env!) +vim backend/.env.prod # Production +vim backend/.env.test # Test + +# 2. Restart to apply changes +./start-prod.sh +``` + +## 📁 Which File to Edit? + +| You Want To... | Edit This File | +|----------------|----------------| +| Change dev database password | `backend/.env.prod` | +| Update test server settings | `backend/.env.test` | +| Add new environment variable | Templates: `.env*.example` + your `.env.prod`/`.env.test` | +| Create production config | Copy `.env.prod.example` to `.env.prod` and fill secrets | + +## 🔑 Generating Secrets + +```bash +# For JWT_SECRET_KEY and AUTH_SESSION_SECRET +python3 -c "import secrets; print(secrets.token_urlsafe(32))" +``` + +**Generate DIFFERENT secrets for each environment (dev, test, prod)!** + +## ⚠️ Important + +- **Never edit** `backend/.env` directly (it's auto-generated!) +- **Always edit** `backend/.env.prod` or `.env.test` +- **Never commit** `.env`, `.env.prod`, `.env.test`, `.env.prod` +- **Only commit** `.env*.example` (templates with placeholders) +- Restart after changes for them to take effect + +## 🛡️ Git Behavior + +| File | Git Status | Contains | +|------|-----------|----------| +| `.env.prod.example` | ✅ Committed | Template (placeholders) | +| `.env.test.example` | ✅ Committed | Template (placeholders) | +| `.env.prod.example` | ✅ Committed | Template (placeholders) | +| `.env.example` | ✅ Committed | Generic template | +| `.env.prod` | ❌ Ignored | **Real dev credentials** | +| `.env.test` | ❌ Ignored | **Real test credentials** | +| `.env.prod` | ❌ Ignored | **Real prod credentials** | +| `.env` | ❌ Ignored | Auto-generated (current) | + +## ✅ Quick Check + +```bash +# See what git will commit +git status backend/.env* + +# Should show ONLY .env*.example files +# If .env.prod or .env.test appear, they're NOT properly ignored! +``` + +## 📖 More Info + +See `backend/ENV-SETUP.md` for complete documentation. diff --git a/reports-app/backend/app/__init__.py b/backend/__init__.py similarity index 100% rename from reports-app/backend/app/__init__.py rename to backend/__init__.py diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..f97cae2 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,173 @@ +""" +Unified Configuration for ROA2WEB Backend +Consolidates settings from Reports, Data Entry, and Telegram modules +""" + +import os +from pathlib import Path +from typing import List +from pydantic_settings import BaseSettings +from functools import lru_cache + + +class UnifiedSettings(BaseSettings): + """Unified application settings for all modules.""" + + # ============================================================================ + # GENERAL APPLICATION SETTINGS + # ============================================================================ + app_name: str = "ROA2WEB Unified Backend" + app_version: str = "1.0.0" + debug: bool = False + api_host: str = "0.0.0.0" + api_port: int = 8000 + + # ============================================================================ + # ORACLE DATABASE (Shared by all modules) + # ============================================================================ + oracle_user: str = "" + oracle_password: str = "" + oracle_host: str = "localhost" + oracle_port: int = 1526 + oracle_sid: str = "ROA" + + # ============================================================================ + # JWT AUTHENTICATION (Shared by all modules) + # ============================================================================ + jwt_secret_key: str = "change-me-in-production" + jwt_algorithm: str = "HS256" + access_token_expire_minutes: int = 30 + refresh_token_expire_days: int = 7 + + # ============================================================================ + # SESSION SECURITY - EMAIL 2FA (Telegram module) + # ============================================================================ + auth_session_secret: str = "change-me-in-production" + + # ============================================================================ + # CORS + # ============================================================================ + cors_origins: str = "http://localhost:3000,http://localhost:5173" + + # ============================================================================ + # REPORTS MODULE - CACHE CONFIGURATION + # ============================================================================ + reports_cache_enabled: bool = True + reports_cache_type: str = "hybrid" + reports_cache_sqlite_path: str = "./data/cache/roa2web_cache.db" + reports_cache_memory_max_size: int = 1000 + reports_cache_default_ttl: int = 900 + + # Cache TTL per type (seconds) + reports_cache_ttl_schema: int = 86400 + reports_cache_ttl_companies: int = 1800 + reports_cache_ttl_dashboard_summary: int = 1800 + reports_cache_ttl_dashboard_trends: int = 1800 + reports_cache_ttl_invoices: int = 600 + reports_cache_ttl_invoices_summary: int = 900 + reports_cache_ttl_treasury: int = 600 + + # Cache maintenance + reports_cache_cleanup_interval: int = 3600 + reports_cache_auto_invalidate: bool = False + reports_cache_check_interval: int = 300 + reports_cache_track_performance: bool = True + reports_cache_benchmark_on_startup: bool = False + + # ============================================================================ + # DATA ENTRY MODULE - CONFIGURATION + # ============================================================================ + data_entry_sqlite_database_path: str = "data/receipts/receipts.db" + data_entry_upload_path: str = "data/receipts/uploads" + data_entry_max_upload_size_mb: int = 10 + data_entry_allowed_mime_types: List[str] = [ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "application/pdf", + ] + + # ============================================================================ + # TELEGRAM MODULE - BOT CONFIGURATION + # ============================================================================ + telegram_bot_token: str = "" + telegram_smtp_host: str = "" + telegram_smtp_port: int = 587 + telegram_smtp_user: str = "" + telegram_smtp_password: str = "" + telegram_smtp_from_email: str = "" + telegram_smtp_from_name: str = "ROA2WEB" + telegram_smtp_use_tls: bool = True + telegram_email_max_retries: int = 3 + telegram_email_retry_delay: float = 2.0 + telegram_sqlite_database_path: str = "data/telegram/telegram.db" + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + extra = "ignore" + case_sensitive = False + + # ============================================================================ + # COMPUTED PROPERTIES + # ============================================================================ + + @property + def oracle_dsn(self) -> str: + """Get Oracle DSN string.""" + return f"{self.oracle_host}:{self.oracle_port}/{self.oracle_sid}" + + @property + def cors_origins_list(self) -> List[str]: + """Get CORS origins as list.""" + return [origin.strip() for origin in self.cors_origins.split(",")] + + # Data Entry properties + @property + def data_entry_database_url(self) -> str: + """Get SQLite database URL for async (Data Entry).""" + return f"sqlite+aiosqlite:///{self.data_entry_sqlite_database_path}" + + @property + def data_entry_sync_database_url(self) -> str: + """Get SQLite database URL for sync operations (Alembic).""" + return f"sqlite:///{self.data_entry_sqlite_database_path}" + + @property + def data_entry_upload_path_resolved(self) -> Path: + """Get resolved upload path.""" + path = Path(self.data_entry_upload_path) + path.mkdir(parents=True, exist_ok=True) + return path + + @property + def data_entry_max_upload_size_bytes(self) -> int: + """Get max upload size in bytes.""" + return self.data_entry_max_upload_size_mb * 1024 * 1024 + + # Reports cache properties + @property + def reports_cache_sqlite_path_resolved(self) -> Path: + """Get resolved cache SQLite path.""" + path = Path(self.reports_cache_sqlite_path) + path.parent.mkdir(parents=True, exist_ok=True) + return path + + # Telegram properties + @property + def telegram_sqlite_path_resolved(self) -> Path: + """Get resolved Telegram SQLite path.""" + path = Path(self.telegram_sqlite_database_path) + path.parent.mkdir(parents=True, exist_ok=True) + return path + + +@lru_cache() +def get_settings() -> UnifiedSettings: + """Get cached settings instance.""" + return UnifiedSettings() + + +# Convenience instance +settings = get_settings() diff --git a/backend/data/README.md b/backend/data/README.md new file mode 100644 index 0000000..83249c8 --- /dev/null +++ b/backend/data/README.md @@ -0,0 +1,45 @@ +# Backend Runtime Data + +This directory contains runtime data generated by the unified backend. + +## Directory Structure + +``` +data/ +├── cache/ # Reports module cache (hybrid L1+L2) +│ └── *.db # SQLite L2 cache database +├── receipts/ # Data Entry module data +│ ├── *.db # SQLite receipts database +│ └── uploads/ # User-uploaded files (receipts, attachments) +└── telegram/ # Telegram bot data + └── *.db # SQLite bot auth/session database +``` + +## Git Behavior + +- **Ignored**: All `*.db` files and `uploads/` contents +- **Committed**: Only `.gitkeep` files to preserve directory structure + +## Environment-Specific Databases + +Different environments use separate databases: + +- **Development** (`.env.prod`): + - Cache: `roa2web_cache.db` + - Receipts: `receipts_dev.db` + - Telegram: `telegram.db` + +- **Test** (`.env.test`): + - Cache: `roa2web_cache_test.db` + - Receipts: `receipts_test.db` + - Telegram: `telegram_test.db` + +- **Production** (`.env.prod`): + - Cache: `roa2web_cache_prod.db` + - Receipts: `receipts_prod.db` + - Telegram: `telegram_prod.db` + +## Auto-Created + +All databases and directories are created automatically on first run. +No manual setup required. diff --git a/reports-app/telegram-bot/data/.gitkeep b/backend/data/cache/.gitkeep similarity index 100% rename from reports-app/telegram-bot/data/.gitkeep rename to backend/data/cache/.gitkeep diff --git a/reports-app/backend/app/models/__init__.py b/backend/data/receipts/.gitkeep similarity index 100% rename from reports-app/backend/app/models/__init__.py rename to backend/data/receipts/.gitkeep diff --git a/reports-app/backend/app/routers/__init__.py b/backend/data/telegram/.gitkeep similarity index 100% rename from reports-app/backend/app/routers/__init__.py rename to backend/data/telegram/.gitkeep diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..839cc11 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,428 @@ +""" +ROA2WEB Unified Backend - Single FastAPI Application +Consolidates Reports, Data Entry, and Telegram modules into one process +""" + +import asyncio +import logging +import os +import sys +from datetime import datetime +from pathlib import Path +from contextlib import asynccontextmanager + +from dotenv import load_dotenv +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +# Load environment variables +load_dotenv() + +# Add project root and shared modules to path +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) # Enable 'from backend.xxx import yyy' +sys.path.insert(0, str(project_root / "shared")) # Enable 'from shared.xxx import yyy' + +# Import configuration +from backend.config import settings + +# Import shared infrastructure +from shared.database.oracle_pool import oracle_pool +from shared.auth.middleware import AuthenticationMiddleware +from shared.auth.routes import create_auth_router +from shared.routes.companies import create_companies_router +from shared.routes.calendar import create_calendar_router + +# Import module router factories +from backend.modules.reports.routers import create_reports_router +from backend.modules.data_entry.routers import create_data_entry_router +from backend.modules.telegram.routers import create_telegram_router + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + datefmt='%H:%M:%S' +) +logger = logging.getLogger(__name__) + +# Global variables for background tasks +telegram_bot_task = None + + +# ============================================================================ +# INITIALIZATION FUNCTIONS +# ============================================================================ + +async def init_oracle_pool(): + """Initialize Oracle connection pool (shared by all modules).""" + logger.info("[ORACLE] Initializing connection pool...") + await oracle_pool.initialize() + logger.info("[ORACLE] ✅ Pool initialized successfully") + + +async def init_reports_cache(): + """Initialize Reports cache system.""" + logger.info("[REPORTS] Initializing cache system...") + try: + from backend.modules.reports.cache import init_cache, init_event_monitor, get_cache + from backend.modules.reports.cache.config import CacheConfig + + cache_config = CacheConfig.from_env() + await init_cache(cache_config) + logger.info(f"[REPORTS] ✅ Cache initialized: type={cache_config.cache_type}, enabled={cache_config.enabled}") + + # Initialize event monitor + cache = get_cache() + await init_event_monitor(cache, cache_config) + if cache_config.auto_invalidate_enabled: + logger.info("[REPORTS] Event-based auto-invalidation ENABLED") + else: + logger.info("[REPORTS] Event-based auto-invalidation DISABLED") + + except Exception as e: + logger.error(f"[REPORTS] ⚠️ Cache initialization error: {e}", exc_info=True) + logger.warning("[REPORTS] Continuing without cache") + + +async def init_data_entry_db(): + """Initialize Data Entry SQLite database.""" + logger.info("[DATA-ENTRY] Initializing SQLite database...") + try: + from backend.modules.data_entry.db.database import init_db + await init_db() + logger.info(f"[DATA-ENTRY] ✅ Database initialized: {settings.data_entry_sqlite_database_path}") + + # Ensure upload directory exists + settings.data_entry_upload_path_resolved + logger.info(f"[DATA-ENTRY] Upload path: {settings.data_entry_upload_path_resolved}") + + except Exception as e: + logger.error(f"[DATA-ENTRY] ❌ Database initialization error: {e}", exc_info=True) + raise + + +async def init_telegram_db(): + """Initialize Telegram SQLite database.""" + logger.info("[TELEGRAM] Initializing SQLite database...") + try: + from backend.modules.telegram.db import init_database, cleanup_expired_codes, cleanup_expired_sessions, cleanup_expired_email_codes + + await init_database() + logger.info(f"[TELEGRAM] ✅ Database initialized: {settings.telegram_sqlite_database_path}") + + # Cleanup expired data + expired_codes = await cleanup_expired_codes() + expired_sessions = await cleanup_expired_sessions() + expired_email_codes = await cleanup_expired_email_codes() + logger.info(f"[TELEGRAM] Cleanup: {expired_codes} codes, {expired_sessions} sessions, {expired_email_codes} email codes removed") + + except Exception as e: + logger.error(f"[TELEGRAM] ❌ Database initialization error: {e}", exc_info=True) + raise + + +def init_paddle_ocr_background(): + """Initialize PaddleOCR in background thread (takes 15-20s).""" + try: + logger.info("[DATA-ENTRY] Pre-loading OCR engine (background)...") + from backend.modules.data_entry.services.ocr_service import ocr_service + ocr_service.ocr_engine._init_paddle_lazy() + logger.info("[DATA-ENTRY] ✅ OCR engine ready") + except Exception as e: + logger.warning(f"[DATA-ENTRY] ⚠️ OCR engine pre-load failed: {e}") + + +async def run_telegram_bot(): + """Run Telegram bot as background task.""" + logger.info("[TELEGRAM] Starting bot...") + try: + from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, filters + from backend.modules.telegram.bot.handlers import ( + start_command, help_command, clear_command, companies_command, + unlink_command, selectcompany_command, dashboard_command, sold_command, + facturi_command, trezorerie_command, menu_command, trezorerie_casa_command, + trezorerie_banca_command, clienti_command, furnizori_command, evolutie_command, + clearcache_command, togglecache_command, handle_text_message, button_callback, + error_handler + ) + from backend.modules.telegram.bot.email_handlers import email_login_handler + + # Create Telegram application + application = Application.builder().token(settings.telegram_bot_token).build() + + # Register handlers + application.add_handler(email_login_handler) + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("menu", menu_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("unlink", unlink_command)) + application.add_handler(CommandHandler("clear", clear_command)) + application.add_handler(CommandHandler("companies", companies_command)) + application.add_handler(CommandHandler("selectcompany", selectcompany_command)) + application.add_handler(CommandHandler("dashboard", dashboard_command)) + application.add_handler(CommandHandler("sold", sold_command)) + application.add_handler(CommandHandler("facturi", facturi_command)) + application.add_handler(CommandHandler("trezorerie", trezorerie_command)) + application.add_handler(CommandHandler("trezorerie_casa", trezorerie_casa_command)) + application.add_handler(CommandHandler("trezorerie_banca", trezorerie_banca_command)) + application.add_handler(CommandHandler("clienti", clienti_command)) + application.add_handler(CommandHandler("furnizori", furnizori_command)) + application.add_handler(CommandHandler("evolutie", evolutie_command)) + application.add_handler(CommandHandler("clearcache", clearcache_command)) + application.add_handler(CommandHandler("togglecache", togglecache_command)) + application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text_message)) + application.add_handler(CallbackQueryHandler(button_callback)) + application.add_error_handler(error_handler) + + # Initialize and start + await application.initialize() + await application.start() + await application.updater.start_polling(drop_pending_updates=True) + + bot_info = await application.bot.get_me() + logger.info(f"[TELEGRAM] ✅ Bot running: @{bot_info.username}") + + # Keep bot running + while True: + await asyncio.sleep(1) + + except asyncio.CancelledError: + logger.info("[TELEGRAM] Bot task cancelled, stopping...") + if 'application' in locals(): + await application.updater.stop() + await application.stop() + await application.shutdown() + logger.info("[TELEGRAM] ✅ Bot stopped") + raise + except Exception as e: + logger.error(f"[TELEGRAM] ❌ Bot error: {e}", exc_info=True) + raise + + +# ============================================================================ +# FASTAPI APPLICATION +# ============================================================================ + +app = FastAPI( + title="ROA2WEB Unified Backend", + description="Unified FastAPI backend for Reports, Data Entry, and Telegram modules", + version="1.0.0" +) + + +# ============================================================================ +# STARTUP/SHUTDOWN EVENT HANDLERS +# ============================================================================ + +@app.on_event("startup") +async def startup_event(): + """Application startup - Initialize all resources.""" + global telegram_bot_task + + logger.info("=" * 80) + logger.info("[STARTUP] ROA2WEB Unified Backend") + logger.info("=" * 80) + + try: + # Step 1: Initialize Oracle pool (shared by all modules) + await init_oracle_pool() + + # Step 2: Parallel initialization of module-specific resources + logger.info("[STARTUP] Initializing module resources in parallel...") + await asyncio.gather( + init_reports_cache(), + init_data_entry_db(), + init_telegram_db(), + ) + + # Step 3: Start PaddleOCR initialization in background thread + import threading + threading.Thread(target=init_paddle_ocr_background, daemon=True).start() + + # Step 4: Start Telegram bot as background task + if settings.telegram_bot_token: + telegram_bot_task = asyncio.create_task(run_telegram_bot()) + logger.info("[STARTUP] ✅ Telegram bot task created") + else: + logger.warning("[STARTUP] ⚠️ TELEGRAM_BOT_TOKEN not set, bot disabled") + + logger.info("=" * 80) + logger.info("[STARTUP] ✅ All modules initialized successfully") + logger.info(f"[STARTUP] ✅ Server running on http://{settings.api_host}:{settings.api_port}") + logger.info("=" * 80) + + except Exception as e: + logger.error(f"[STARTUP] ❌ Initialization failed: {e}", exc_info=True) + raise + + +@app.on_event("shutdown") +async def shutdown_event(): + """Application shutdown - Cleanup resources.""" + global telegram_bot_task + + logger.info("=" * 80) + logger.info("[SHUTDOWN] Stopping ROA2WEB Unified Backend...") + logger.info("=" * 80) + + try: + # Stop Telegram bot + if telegram_bot_task and not telegram_bot_task.done(): + logger.info("[SHUTDOWN] Stopping Telegram bot...") + telegram_bot_task.cancel() + try: + await telegram_bot_task + except asyncio.CancelledError: + pass + + # Stop Reports cache event monitor + try: + from backend.modules.reports.cache import close_cache, get_event_monitor + monitor = get_event_monitor() + if monitor: + await monitor.stop() + logger.info("[SHUTDOWN] Reports cache monitor stopped") + + await close_cache() + logger.info("[SHUTDOWN] Reports cache closed") + except Exception as e: + logger.error(f"[SHUTDOWN] Cache error: {e}") + + # Close Oracle pool + await oracle_pool.close_pool() + logger.info("[SHUTDOWN] Oracle pool closed") + + logger.info("=" * 80) + logger.info("[SHUTDOWN] ✅ Shutdown complete") + logger.info("=" * 80) + + except Exception as e: + logger.error(f"[SHUTDOWN] Error during shutdown: {e}", exc_info=True) + + +# ============================================================================ +# MIDDLEWARE +# ============================================================================ + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Allow all origins for production deployment + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Authentication middleware +app.add_middleware( + AuthenticationMiddleware, + excluded_paths=[ + "/", "/docs", "/health", "/redoc", "/openapi.json", + "/api/auth/login", "/api/auth/refresh", + "/api/telegram/auth/verify-user", + "/api/telegram/auth/verify-email", + "/api/telegram/auth/login-with-email", + "/api/telegram/auth/refresh-token", + "/api/telegram/health" + ] +) + + +# ============================================================================ +# ROUTER REGISTRATION +# ============================================================================ + +# Module routers with prefixes +app.include_router(create_reports_router(), prefix="/api/reports", tags=["reports"]) +app.include_router(create_data_entry_router(), prefix="/api/data-entry", tags=["data-entry"]) +app.include_router(create_telegram_router(), prefix="/api/telegram", tags=["telegram"]) + +# Shared routers +auth_router = create_auth_router(prefix="", tags=["authentication"]) +app.include_router(auth_router, prefix="/api/auth") + +companies_router = create_companies_router(oracle_pool, tags=["companies"]) +app.include_router(companies_router, prefix="/api/companies") + +calendar_router = create_calendar_router(oracle_pool, tags=["calendar"]) +app.include_router(calendar_router, prefix="/api/calendar") + + +# ============================================================================ +# ROOT & HEALTH ENDPOINTS +# ============================================================================ + +@app.get("/") +async def root(): + """Root endpoint - API information.""" + return { + "name": settings.app_name, + "version": settings.app_version, + "status": "running", + "modules": ["reports", "data-entry", "telegram"], + "docs": "/docs", + "health": "/health" + } + + +@app.get("/health") +async def health_check(): + """Health check endpoint with module status.""" + health_status = { + "api": "healthy", + "timestamp": datetime.utcnow().isoformat(), + "modules": {} + } + + # Check Oracle connection + try: + async with oracle_pool.get_connection() as conn: + with conn.cursor() as cursor: + cursor.execute("SELECT 1 FROM DUAL") + health_status["modules"]["oracle"] = "connected" + except Exception as e: + health_status["modules"]["oracle"] = f"error: {str(e)}" + + # Check Reports cache + try: + from backend.modules.reports.cache import get_cache + cache = get_cache() + health_status["modules"]["reports_cache"] = "initialized" if cache else "disabled" + except Exception as e: + health_status["modules"]["reports_cache"] = f"error: {str(e)}" + + # Check Data Entry DB + try: + db_path = Path(settings.data_entry_sqlite_database_path) + health_status["modules"]["data_entry_db"] = "exists" if db_path.exists() else "missing" + except Exception as e: + health_status["modules"]["data_entry_db"] = f"error: {str(e)}" + + # Check Telegram bot + global telegram_bot_task + if telegram_bot_task: + if telegram_bot_task.done(): + health_status["modules"]["telegram_bot"] = "stopped" + else: + health_status["modules"]["telegram_bot"] = "running" + else: + health_status["modules"]["telegram_bot"] = "disabled" + + return health_status + + +# ============================================================================ +# MAIN ENTRY POINT +# ============================================================================ + +if __name__ == "__main__": + import uvicorn + + uvicorn.run( + "backend.main:app", + host=settings.api_host, + port=settings.api_port, + reload=False, + log_level="info" + ) diff --git a/reports-app/backend/app/schemas/__init__.py b/backend/modules/__init__.py similarity index 100% rename from reports-app/backend/app/schemas/__init__.py rename to backend/modules/__init__.py diff --git a/reports-app/backend/app/services/__init__.py b/backend/modules/data_entry/__init__.py similarity index 100% rename from reports-app/backend/app/services/__init__.py rename to backend/modules/data_entry/__init__.py diff --git a/data-entry-app/backend/app/config.py b/backend/modules/data_entry/config.py similarity index 97% rename from data-entry-app/backend/app/config.py rename to backend/modules/data_entry/config.py index b6c2a18..15514cc 100644 --- a/data-entry-app/backend/app/config.py +++ b/backend/modules/data_entry/config.py @@ -20,7 +20,7 @@ class Settings(BaseSettings): api_port: int = 8003 # SQLite Database - sqlite_database_path: str = "data/receipts.db" + sqlite_database_path: str = "data/receipts/receipts.db" # File uploads upload_path: str = "data/uploads" diff --git a/data-entry-app/backend/app/db/__init__.py b/backend/modules/data_entry/db/__init__.py similarity index 100% rename from data-entry-app/backend/app/db/__init__.py rename to backend/modules/data_entry/db/__init__.py diff --git a/data-entry-app/backend/app/db/crud/__init__.py b/backend/modules/data_entry/db/crud/__init__.py similarity index 100% rename from data-entry-app/backend/app/db/crud/__init__.py rename to backend/modules/data_entry/db/crud/__init__.py diff --git a/data-entry-app/backend/app/db/crud/accounting_entry.py b/backend/modules/data_entry/db/crud/accounting_entry.py similarity index 97% rename from data-entry-app/backend/app/db/crud/accounting_entry.py rename to backend/modules/data_entry/db/crud/accounting_entry.py index 8051f1e..f321295 100644 --- a/data-entry-app/backend/app/db/crud/accounting_entry.py +++ b/backend/modules/data_entry/db/crud/accounting_entry.py @@ -6,8 +6,8 @@ from typing import Optional, List from sqlalchemy import select, delete from sqlalchemy.ext.asyncio import AsyncSession -from app.db.models.accounting_entry import AccountingEntry, EntryType -from app.schemas.receipt import AccountingEntryCreate, AccountingEntryUpdate +from backend.modules.data_entry.db.models.accounting_entry import AccountingEntry, EntryType +from backend.modules.data_entry.schemas.receipt import AccountingEntryCreate, AccountingEntryUpdate class AccountingEntryCRUD: diff --git a/data-entry-app/backend/app/db/crud/attachment.py b/backend/modules/data_entry/db/crud/attachment.py similarity index 97% rename from data-entry-app/backend/app/db/crud/attachment.py rename to backend/modules/data_entry/db/crud/attachment.py index f746550..82d57e9 100644 --- a/data-entry-app/backend/app/db/crud/attachment.py +++ b/backend/modules/data_entry/db/crud/attachment.py @@ -11,8 +11,8 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from fastapi import UploadFile -from app.db.models.receipt import ReceiptAttachment -from app.config import settings +from backend.modules.data_entry.db.models.receipt import ReceiptAttachment +from backend.modules.data_entry.config import settings class AttachmentCRUD: diff --git a/data-entry-app/backend/app/db/crud/receipt.py b/backend/modules/data_entry/db/crud/receipt.py similarity index 98% rename from data-entry-app/backend/app/db/crud/receipt.py rename to backend/modules/data_entry/db/crud/receipt.py index e4e33f7..054a8a4 100644 --- a/data-entry-app/backend/app/db/crud/receipt.py +++ b/backend/modules/data_entry/db/crud/receipt.py @@ -8,8 +8,8 @@ from sqlalchemy import select, func, or_ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload -from app.db.models.receipt import Receipt, ReceiptStatus -from app.schemas.receipt import ReceiptCreate, ReceiptUpdate, ReceiptFilter +from backend.modules.data_entry.db.models.receipt import Receipt, ReceiptStatus +from backend.modules.data_entry.schemas.receipt import ReceiptCreate, ReceiptUpdate, ReceiptFilter def _serialize_tva_breakdown(tva_breakdown: Optional[List[Any]]) -> Optional[str]: diff --git a/data-entry-app/backend/app/db/database.py b/backend/modules/data_entry/db/database.py similarity index 95% rename from data-entry-app/backend/app/db/database.py rename to backend/modules/data_entry/db/database.py index d502019..3d02315 100644 --- a/data-entry-app/backend/app/db/database.py +++ b/backend/modules/data_entry/db/database.py @@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import sessionmaker from sqlmodel import SQLModel -from app.config import settings +from backend.modules.data_entry.config import settings # Create async engine diff --git a/data-entry-app/backend/app/db/models/__init__.py b/backend/modules/data_entry/db/models/__init__.py similarity index 100% rename from data-entry-app/backend/app/db/models/__init__.py rename to backend/modules/data_entry/db/models/__init__.py diff --git a/data-entry-app/backend/app/db/models/accounting_entry.py b/backend/modules/data_entry/db/models/accounting_entry.py similarity index 100% rename from data-entry-app/backend/app/db/models/accounting_entry.py rename to backend/modules/data_entry/db/models/accounting_entry.py diff --git a/data-entry-app/backend/app/db/models/nomenclature.py b/backend/modules/data_entry/db/models/nomenclature.py similarity index 100% rename from data-entry-app/backend/app/db/models/nomenclature.py rename to backend/modules/data_entry/db/models/nomenclature.py diff --git a/data-entry-app/backend/app/db/models/receipt.py b/backend/modules/data_entry/db/models/receipt.py similarity index 100% rename from data-entry-app/backend/app/db/models/receipt.py rename to backend/modules/data_entry/db/models/receipt.py diff --git a/data-entry-app/backend/migrations/env.py b/backend/modules/data_entry/migrations/env.py similarity index 87% rename from data-entry-app/backend/migrations/env.py rename to backend/modules/data_entry/migrations/env.py index 8f8d2d0..063cb99 100644 --- a/data-entry-app/backend/migrations/env.py +++ b/backend/modules/data_entry/migrations/env.py @@ -14,16 +14,16 @@ from sqlmodel import SQLModel load_dotenv() # Import all models to ensure they're registered with SQLModel -from app.db.models.receipt import Receipt, ReceiptAttachment -from app.db.models.accounting_entry import AccountingEntry -from app.db.models.nomenclature import SyncedSupplier, LocalSupplier, SyncedCashRegister +from backend.modules.data_entry.db.models.receipt import Receipt, ReceiptAttachment +from backend.modules.data_entry.db.models.accounting_entry import AccountingEntry +from backend.modules.data_entry.db.models.nomenclature import SyncedSupplier, LocalSupplier, SyncedCashRegister # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config # Override sqlalchemy.url from environment variable if set -db_path = os.getenv("SQLITE_DATABASE_PATH", "data/receipts.db") +db_path = os.getenv("SQLITE_DATABASE_PATH", "data/receipts/receipts.db") config.set_main_option("sqlalchemy.url", f"sqlite:///{db_path}") # Interpret the config file for Python logging. diff --git a/data-entry-app/backend/migrations/script.py.mako b/backend/modules/data_entry/migrations/script.py.mako similarity index 100% rename from data-entry-app/backend/migrations/script.py.mako rename to backend/modules/data_entry/migrations/script.py.mako diff --git a/data-entry-app/backend/migrations/versions/001_initial_receipts.py b/backend/modules/data_entry/migrations/versions/001_initial_receipts.py similarity index 100% rename from data-entry-app/backend/migrations/versions/001_initial_receipts.py rename to backend/modules/data_entry/migrations/versions/001_initial_receipts.py diff --git a/data-entry-app/backend/migrations/versions/20251212_140422_add_tva_breakdown_to_receipt.py b/backend/modules/data_entry/migrations/versions/20251212_140422_add_tva_breakdown_to_receipt.py similarity index 100% rename from data-entry-app/backend/migrations/versions/20251212_140422_add_tva_breakdown_to_receipt.py rename to backend/modules/data_entry/migrations/versions/20251212_140422_add_tva_breakdown_to_receipt.py diff --git a/data-entry-app/backend/migrations/versions/20251213_002805_add_nomenclature_tables.py b/backend/modules/data_entry/migrations/versions/20251213_002805_add_nomenclature_tables.py similarity index 100% rename from data-entry-app/backend/migrations/versions/20251213_002805_add_nomenclature_tables.py rename to backend/modules/data_entry/migrations/versions/20251213_002805_add_nomenclature_tables.py diff --git a/data-entry-app/backend/migrations/versions/20251215_add_ocr_fields_to_receipt.py b/backend/modules/data_entry/migrations/versions/20251215_add_ocr_fields_to_receipt.py similarity index 100% rename from data-entry-app/backend/migrations/versions/20251215_add_ocr_fields_to_receipt.py rename to backend/modules/data_entry/migrations/versions/20251215_add_ocr_fields_to_receipt.py diff --git a/data-entry-app/backend/migrations/versions/20251215_remove_partner_id.py b/backend/modules/data_entry/migrations/versions/20251215_remove_partner_id.py similarity index 100% rename from data-entry-app/backend/migrations/versions/20251215_remove_partner_id.py rename to backend/modules/data_entry/migrations/versions/20251215_remove_partner_id.py diff --git a/data-entry-app/backend/migrations/versions/20251216_add_payment_mode.py b/backend/modules/data_entry/migrations/versions/20251216_add_payment_mode.py similarity index 100% rename from data-entry-app/backend/migrations/versions/20251216_add_payment_mode.py rename to backend/modules/data_entry/migrations/versions/20251216_add_payment_mode.py diff --git a/backend/modules/data_entry/routers/__init__.py b/backend/modules/data_entry/routers/__init__.py new file mode 100644 index 0000000..fab0364 --- /dev/null +++ b/backend/modules/data_entry/routers/__init__.py @@ -0,0 +1,30 @@ +"""Data Entry module router factory.""" + +from fastapi import APIRouter + + +def create_data_entry_router() -> APIRouter: + """ + Create and configure Data Entry module router. + + Includes all data entry endpoints: + - /receipts - Receipt CRUD and workflow + - /ocr - OCR processing for receipts + - /nomenclature - Nomenclature syncing from Oracle + + Returns: + APIRouter: Configured router for data entry module + """ + router = APIRouter() + + # Import routers here to avoid circular imports + from .receipts import router as receipts_router + from .ocr import router as ocr_router + from .nomenclature import router as nomenclature_router + + # Include all sub-routers (no prefix - already prefixed in main.py with /api/data-entry) + router.include_router(receipts_router, prefix="/receipts", tags=["data-entry-receipts"]) + router.include_router(ocr_router, prefix="/ocr", tags=["data-entry-ocr"]) + router.include_router(nomenclature_router, prefix="/nomenclature", tags=["data-entry-nomenclature"]) + + return router diff --git a/data-entry-app/backend/app/routers/nomenclature.py b/backend/modules/data_entry/routers/nomenclature.py similarity index 94% rename from data-entry-app/backend/app/routers/nomenclature.py rename to backend/modules/data_entry/routers/nomenclature.py index 1971453..84b6821 100644 --- a/data-entry-app/backend/app/routers/nomenclature.py +++ b/backend/modules/data_entry/routers/nomenclature.py @@ -5,17 +5,18 @@ from fastapi import APIRouter, Depends, HTTPException, Header from sqlalchemy.ext.asyncio import AsyncSession from pydantic import BaseModel -from app.db.database import get_session -from app.services.sync_service import SyncService +from backend.modules.data_entry.db.database import get_session +from backend.modules.data_entry.services.sync_service import SyncService # Import auth dependencies import sys from pathlib import Path -project_root = Path(__file__).parent.parent.parent.parent.parent -sys.path.insert(0, str(project_root / "shared")) +# Path setup handled by main.py - this is redundant +# project_root = Path(__file__).parent.parent.parent.parent.parent +# sys.path.insert(0, str(project_root / "shared")) -from auth.dependencies import get_current_user -from auth.models import CurrentUser +from shared.auth.dependencies import get_current_user +from shared.auth.models import CurrentUser router = APIRouter() diff --git a/data-entry-app/backend/app/routers/ocr.py b/backend/modules/data_entry/routers/ocr.py similarity index 93% rename from data-entry-app/backend/app/routers/ocr.py rename to backend/modules/data_entry/routers/ocr.py index ca30406..d1ad183 100644 --- a/data-entry-app/backend/app/routers/ocr.py +++ b/backend/modules/data_entry/routers/ocr.py @@ -7,15 +7,15 @@ from pathlib import Path from fastapi import APIRouter, HTTPException, UploadFile, File, Depends from sqlalchemy.ext.asyncio import AsyncSession -from app.db.database import get_session -from app.db.crud.attachment import AttachmentCRUD -from app.services.ocr_service import ocr_service -from app.services.ocr_engine import OCREngine -from app.schemas.ocr import OCRResponse, OCRStatusResponse, ExtractionData, TvaEntry, PaymentMethod +from backend.modules.data_entry.db.database import get_session +from backend.modules.data_entry.db.crud.attachment import AttachmentCRUD +from backend.modules.data_entry.services.ocr_service import ocr_service +from backend.modules.data_entry.services.ocr_engine import OCREngine +from backend.modules.data_entry.schemas.ocr import OCRResponse, OCRStatusResponse, ExtractionData, TvaEntry, PaymentMethod # Auth integration (will be protected by middleware) -from auth.dependencies import get_current_user -from auth.models import CurrentUser +from shared.auth.dependencies import get_current_user +from shared.auth.models import CurrentUser router = APIRouter() diff --git a/data-entry-app/backend/app/routers/receipts.py b/backend/modules/data_entry/routers/receipts.py similarity index 95% rename from data-entry-app/backend/app/routers/receipts.py rename to backend/modules/data_entry/routers/receipts.py index 5e2fd1b..492165d 100644 --- a/data-entry-app/backend/app/routers/receipts.py +++ b/backend/modules/data_entry/routers/receipts.py @@ -7,13 +7,13 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession -from app.db.database import get_session -from app.db.crud.receipt import ReceiptCRUD -from app.db.crud.attachment import AttachmentCRUD -from app.db.crud.accounting_entry import AccountingEntryCRUD -from app.services.receipt_service import ReceiptService -from app.services.nomenclature_service import NomenclatureService -from app.schemas.receipt import ( +from backend.modules.data_entry.db.database import get_session +from backend.modules.data_entry.db.crud.receipt import ReceiptCRUD +from backend.modules.data_entry.db.crud.attachment import AttachmentCRUD +from backend.modules.data_entry.db.crud.accounting_entry import AccountingEntryCRUD +from backend.modules.data_entry.services.receipt_service import ReceiptService +from backend.modules.data_entry.services.nomenclature_service import NomenclatureService +from backend.modules.data_entry.schemas.receipt import ( ReceiptCreate, ReceiptUpdate, ReceiptResponse, @@ -29,11 +29,11 @@ from app.schemas.receipt import ( CashRegisterOption, ExpenseTypeOption, ) -from app.db.models.receipt import ReceiptStatus, ReceiptDirection +from backend.modules.data_entry.db.models.receipt import ReceiptStatus, ReceiptDirection # Auth integration -from auth.dependencies import get_current_user -from auth.models import CurrentUser +from shared.auth.dependencies import get_current_user +from shared.auth.models import CurrentUser router = APIRouter() diff --git a/data-entry-app/backend/app/schemas/__init__.py b/backend/modules/data_entry/schemas/__init__.py similarity index 100% rename from data-entry-app/backend/app/schemas/__init__.py rename to backend/modules/data_entry/schemas/__init__.py diff --git a/data-entry-app/backend/app/schemas/ocr.py b/backend/modules/data_entry/schemas/ocr.py similarity index 100% rename from data-entry-app/backend/app/schemas/ocr.py rename to backend/modules/data_entry/schemas/ocr.py diff --git a/data-entry-app/backend/app/schemas/receipt.py b/backend/modules/data_entry/schemas/receipt.py similarity index 98% rename from data-entry-app/backend/app/schemas/receipt.py rename to backend/modules/data_entry/schemas/receipt.py index 6c96426..11b542c 100644 --- a/data-entry-app/backend/app/schemas/receipt.py +++ b/backend/modules/data_entry/schemas/receipt.py @@ -6,8 +6,8 @@ from decimal import Decimal from typing import Optional, List, Any, Union from pydantic import BaseModel, Field, ConfigDict, field_validator -from app.db.models.receipt import ReceiptType, ReceiptDirection, ReceiptStatus -from app.db.models.accounting_entry import EntryType +from backend.modules.data_entry.db.models.receipt import ReceiptType, ReceiptDirection, ReceiptStatus +from backend.modules.data_entry.db.models.accounting_entry import EntryType # ============ Accounting Entry Schemas ============ diff --git a/data-entry-app/backend/app/services/__init__.py b/backend/modules/data_entry/services/__init__.py similarity index 100% rename from data-entry-app/backend/app/services/__init__.py rename to backend/modules/data_entry/services/__init__.py diff --git a/data-entry-app/backend/app/services/expense_types.py b/backend/modules/data_entry/services/expense_types.py similarity index 100% rename from data-entry-app/backend/app/services/expense_types.py rename to backend/modules/data_entry/services/expense_types.py diff --git a/data-entry-app/backend/app/services/image_preprocessor.py b/backend/modules/data_entry/services/image_preprocessor.py similarity index 100% rename from data-entry-app/backend/app/services/image_preprocessor.py rename to backend/modules/data_entry/services/image_preprocessor.py diff --git a/data-entry-app/backend/app/services/nomenclature_service.py b/backend/modules/data_entry/services/nomenclature_service.py similarity index 97% rename from data-entry-app/backend/app/services/nomenclature_service.py rename to backend/modules/data_entry/services/nomenclature_service.py index d25546b..16d3af3 100644 --- a/data-entry-app/backend/app/services/nomenclature_service.py +++ b/backend/modules/data_entry/services/nomenclature_service.py @@ -6,14 +6,14 @@ from decimal import Decimal from sqlmodel import select from sqlalchemy.ext.asyncio import AsyncSession -from app.schemas.receipt import ( +from backend.modules.data_entry.schemas.receipt import ( PartnerOption, AccountOption, CashRegisterOption, ExpenseTypeOption, ) -from app.services.expense_types import EXPENSE_TYPES -from app.db.models.nomenclature import SyncedSupplier, LocalSupplier, SyncedCashRegister +from backend.modules.data_entry.services.expense_types import EXPENSE_TYPES +from backend.modules.data_entry.db.models.nomenclature import SyncedSupplier, LocalSupplier, SyncedCashRegister class NomenclatureService: diff --git a/data-entry-app/backend/app/services/ocr_engine.py b/backend/modules/data_entry/services/ocr_engine.py similarity index 100% rename from data-entry-app/backend/app/services/ocr_engine.py rename to backend/modules/data_entry/services/ocr_engine.py diff --git a/data-entry-app/backend/app/services/ocr_extractor.py b/backend/modules/data_entry/services/ocr_extractor.py similarity index 100% rename from data-entry-app/backend/app/services/ocr_extractor.py rename to backend/modules/data_entry/services/ocr_extractor.py diff --git a/data-entry-app/backend/app/services/ocr_service.py b/backend/modules/data_entry/services/ocr_service.py similarity index 99% rename from data-entry-app/backend/app/services/ocr_service.py rename to backend/modules/data_entry/services/ocr_service.py index f771915..0a983a1 100644 --- a/data-entry-app/backend/app/services/ocr_service.py +++ b/backend/modules/data_entry/services/ocr_service.py @@ -14,9 +14,9 @@ from decimal import Decimal from pathlib import Path from typing import Optional, Tuple -from app.services.ocr_engine import OCREngine -from app.services.ocr_extractor import ReceiptExtractor, ExtractionResult -from app.services.image_preprocessor import ImagePreprocessor +from backend.modules.data_entry.services.ocr_engine import OCREngine +from backend.modules.data_entry.services.ocr_extractor import ReceiptExtractor, ExtractionResult +from backend.modules.data_entry.services.image_preprocessor import ImagePreprocessor # Setup logging logger = logging.getLogger(__name__) diff --git a/data-entry-app/backend/app/services/receipt_service.py b/backend/modules/data_entry/services/receipt_service.py similarity index 96% rename from data-entry-app/backend/app/services/receipt_service.py rename to backend/modules/data_entry/services/receipt_service.py index 278eed4..8f85191 100644 --- a/data-entry-app/backend/app/services/receipt_service.py +++ b/backend/modules/data_entry/services/receipt_service.py @@ -5,11 +5,11 @@ from typing import List, Optional, Tuple from sqlalchemy.ext.asyncio import AsyncSession -from app.db.models.receipt import Receipt, ReceiptStatus, ReceiptDirection -from app.db.models.accounting_entry import EntryType -from app.db.crud.receipt import ReceiptCRUD -from app.db.crud.accounting_entry import AccountingEntryCRUD -from app.schemas.receipt import ( +from backend.modules.data_entry.db.models.receipt import Receipt, ReceiptStatus, ReceiptDirection +from backend.modules.data_entry.db.models.accounting_entry import EntryType +from backend.modules.data_entry.db.crud.receipt import ReceiptCRUD +from backend.modules.data_entry.db.crud.accounting_entry import AccountingEntryCRUD +from backend.modules.data_entry.schemas.receipt import ( ReceiptCreate, ReceiptUpdate, ReceiptFilter, @@ -17,7 +17,7 @@ from app.schemas.receipt import ( ReceiptListResponse, AccountingEntryCreate, ) -from app.services.expense_types import EXPENSE_TYPES, get_expense_type +from backend.modules.data_entry.services.expense_types import EXPENSE_TYPES, get_expense_type # Payment mode to accounting account mapping diff --git a/data-entry-app/backend/app/services/sync_service.py b/backend/modules/data_entry/services/sync_service.py similarity index 97% rename from data-entry-app/backend/app/services/sync_service.py rename to backend/modules/data_entry/services/sync_service.py index a3599c5..3f59694 100644 --- a/data-entry-app/backend/app/services/sync_service.py +++ b/backend/modules/data_entry/services/sync_service.py @@ -9,12 +9,12 @@ import logging from sqlmodel import select from sqlalchemy.ext.asyncio import AsyncSession -# Add shared modules path -project_root = Path(__file__).parent.parent.parent.parent.parent -sys.path.insert(0, str(project_root / "shared")) +# Path setup handled by main.py - this is redundant +# project_root = Path(__file__).parent.parent.parent.parent.parent +# sys.path.insert(0, str(project_root / "shared")) -from database.oracle_pool import oracle_pool -from app.db.models.nomenclature import SyncedSupplier, LocalSupplier, SyncedCashRegister +from shared.database.oracle_pool import oracle_pool +from backend.modules.data_entry.db.models.nomenclature import SyncedSupplier, LocalSupplier, SyncedCashRegister logger = logging.getLogger(__name__) diff --git a/reports-app/frontend/src/services/__init__.py b/backend/modules/reports/__init__.py similarity index 100% rename from reports-app/frontend/src/services/__init__.py rename to backend/modules/reports/__init__.py diff --git a/reports-app/backend/app/cache/__init__.py b/backend/modules/reports/cache/__init__.py similarity index 100% rename from reports-app/backend/app/cache/__init__.py rename to backend/modules/reports/cache/__init__.py diff --git a/reports-app/backend/app/cache/benchmarks.py b/backend/modules/reports/cache/benchmarks.py similarity index 100% rename from reports-app/backend/app/cache/benchmarks.py rename to backend/modules/reports/cache/benchmarks.py diff --git a/reports-app/backend/app/cache/cache_manager.py b/backend/modules/reports/cache/cache_manager.py similarity index 100% rename from reports-app/backend/app/cache/cache_manager.py rename to backend/modules/reports/cache/cache_manager.py diff --git a/reports-app/backend/app/cache/config.py b/backend/modules/reports/cache/config.py similarity index 97% rename from reports-app/backend/app/cache/config.py rename to backend/modules/reports/cache/config.py index 08e6257..0ba07c9 100644 --- a/reports-app/backend/app/cache/config.py +++ b/backend/modules/reports/cache/config.py @@ -46,7 +46,7 @@ class CacheConfig: # Core Settings enabled=os.getenv('CACHE_ENABLED', 'True').lower() == 'true', cache_type=os.getenv('CACHE_TYPE', 'hybrid'), - sqlite_path=os.getenv('CACHE_SQLITE_PATH', './cache_data/roa2web_cache.db'), + sqlite_path=os.getenv('CACHE_SQLITE_PATH', './data/cache/roa2web_cache.db'), memory_max_size=int(os.getenv('CACHE_MEMORY_MAX_SIZE', '1000')), default_ttl=int(os.getenv('CACHE_DEFAULT_TTL', '900')), diff --git a/reports-app/backend/app/cache/decorators.py b/backend/modules/reports/cache/decorators.py similarity index 100% rename from reports-app/backend/app/cache/decorators.py rename to backend/modules/reports/cache/decorators.py diff --git a/reports-app/backend/app/cache/event_monitor.py b/backend/modules/reports/cache/event_monitor.py similarity index 98% rename from reports-app/backend/app/cache/event_monitor.py rename to backend/modules/reports/cache/event_monitor.py index 1dbc4a4..c7306e9 100644 --- a/reports-app/backend/app/cache/event_monitor.py +++ b/backend/modules/reports/cache/event_monitor.py @@ -9,8 +9,8 @@ import sys import os from typing import Optional -# Add shared to path for Oracle pool access -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../..'))) +# Path setup handled by main.py - this is redundant but kept for module isolation +# sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../..'))) logger = logging.getLogger(__name__) diff --git a/reports-app/backend/app/cache/keys.py b/backend/modules/reports/cache/keys.py similarity index 100% rename from reports-app/backend/app/cache/keys.py rename to backend/modules/reports/cache/keys.py diff --git a/reports-app/backend/app/cache/memory_cache.py b/backend/modules/reports/cache/memory_cache.py similarity index 100% rename from reports-app/backend/app/cache/memory_cache.py rename to backend/modules/reports/cache/memory_cache.py diff --git a/reports-app/backend/app/cache/sqlite_cache.py b/backend/modules/reports/cache/sqlite_cache.py similarity index 100% rename from reports-app/backend/app/cache/sqlite_cache.py rename to backend/modules/reports/cache/sqlite_cache.py diff --git a/reports-app/frontend/src/utils/__init__.py b/backend/modules/reports/models/__init__.py similarity index 100% rename from reports-app/frontend/src/utils/__init__.py rename to backend/modules/reports/models/__init__.py diff --git a/reports-app/backend/app/models/calendar.py b/backend/modules/reports/models/calendar.py similarity index 100% rename from reports-app/backend/app/models/calendar.py rename to backend/modules/reports/models/calendar.py diff --git a/reports-app/backend/app/models/dashboard.py b/backend/modules/reports/models/dashboard.py similarity index 100% rename from reports-app/backend/app/models/dashboard.py rename to backend/modules/reports/models/dashboard.py diff --git a/reports-app/backend/app/models/invoice.py b/backend/modules/reports/models/invoice.py similarity index 100% rename from reports-app/backend/app/models/invoice.py rename to backend/modules/reports/models/invoice.py diff --git a/reports-app/backend/app/models/treasury.py b/backend/modules/reports/models/treasury.py similarity index 100% rename from reports-app/backend/app/models/treasury.py rename to backend/modules/reports/models/treasury.py diff --git a/reports-app/backend/app/models/trial_balance.py b/backend/modules/reports/models/trial_balance.py similarity index 99% rename from reports-app/backend/app/models/trial_balance.py rename to backend/modules/reports/models/trial_balance.py index c370411..23741df 100644 --- a/reports-app/backend/app/models/trial_balance.py +++ b/backend/modules/reports/models/trial_balance.py @@ -69,7 +69,7 @@ class TrialBalanceResponse(BaseModel): data: dict = Field(description="Trial balance data with items, pagination, and filters") class Config: - schema_extra = { + json_schema_extra = { "example": { "success": True, "data": { diff --git a/backend/modules/reports/routers/__init__.py b/backend/modules/reports/routers/__init__.py new file mode 100644 index 0000000..1df06cb --- /dev/null +++ b/backend/modules/reports/routers/__init__.py @@ -0,0 +1,36 @@ +"""Reports module router factory.""" + +from fastapi import APIRouter + + +def create_reports_router() -> APIRouter: + """ + Create and configure Reports module router. + + Includes all report-related endpoints: + - /invoices - Invoice management + - /dashboard - Dashboard and metrics + - /treasury - Treasury operations + - /trial-balance - Trial balance reports + - /cache - Cache management + + Returns: + APIRouter: Configured router for reports module + """ + router = APIRouter() + + # Import routers here to avoid circular imports + from .invoices import router as invoices_router + from .dashboard import router as dashboard_router + from .treasury import router as treasury_router + from .trial_balance import router as trial_balance_router + from .cache import router as cache_router + + # Include all sub-routers (no prefix - already prefixed in main.py with /api/reports) + router.include_router(invoices_router, prefix="/invoices", tags=["reports-invoices"]) + router.include_router(dashboard_router, prefix="/dashboard", tags=["reports-dashboard"]) + router.include_router(treasury_router, prefix="/treasury", tags=["reports-treasury"]) + router.include_router(trial_balance_router, prefix="/trial-balance", tags=["reports-trial-balance"]) + router.include_router(cache_router, prefix="/cache", tags=["reports-cache"]) + + return router diff --git a/reports-app/backend/app/routers/cache.py b/backend/modules/reports/routers/cache.py similarity index 98% rename from reports-app/backend/app/routers/cache.py rename to backend/modules/reports/routers/cache.py index 2b1b588..f33f395 100644 --- a/reports-app/backend/app/routers/cache.py +++ b/backend/modules/reports/routers/cache.py @@ -4,15 +4,14 @@ API Router pentru managementul cache-ului from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from typing import Optional, Dict, Any -import sys +# import sys # Removed - no longer needed import os import time from datetime import datetime, timedelta -sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) -from auth.dependencies import get_current_user -from auth.models import CurrentUser +from shared.auth.dependencies import get_current_user +from shared.auth.models import CurrentUser from ..cache import get_cache, get_event_monitor, toggle_event_monitor router = APIRouter(prefix="/cache", tags=["cache"]) diff --git a/reports-app/backend/app/routers/dashboard.py b/backend/modules/reports/routers/dashboard.py similarity index 99% rename from reports-app/backend/app/routers/dashboard.py rename to backend/modules/reports/routers/dashboard.py index 8a55f7c..7be4b7b 100644 --- a/reports-app/backend/app/routers/dashboard.py +++ b/backend/modules/reports/routers/dashboard.py @@ -1,11 +1,10 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Request from typing import Optional -import sys +# import sys # Removed - no longer needed import os -sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) -from auth.dependencies import get_current_user -from auth.models import CurrentUser +from shared.auth.dependencies import get_current_user +from shared.auth.models import CurrentUser import logging logger = logging.getLogger(__name__) diff --git a/reports-app/backend/app/routers/invoices.py b/backend/modules/reports/routers/invoices.py similarity index 96% rename from reports-app/backend/app/routers/invoices.py rename to backend/modules/reports/routers/invoices.py index 54d581b..d91056e 100644 --- a/reports-app/backend/app/routers/invoices.py +++ b/backend/modules/reports/routers/invoices.py @@ -4,12 +4,11 @@ API Router pentru facturi from fastapi import APIRouter, Depends, HTTPException, Query from typing import List, Optional from datetime import date -import sys +# import sys # Removed - no longer needed import os -sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) -from auth.dependencies import get_current_user, require_company_access -from auth.models import CurrentUser +from shared.auth.dependencies import get_current_user, require_company_access +from shared.auth.models import CurrentUser from ..models.invoice import InvoiceFilter, InvoiceListResponse, InvoiceSummary from ..services.invoice_service import InvoiceService diff --git a/reports-app/backend/app/routers/treasury.py b/backend/modules/reports/routers/treasury.py similarity index 96% rename from reports-app/backend/app/routers/treasury.py rename to backend/modules/reports/routers/treasury.py index f39cd1e..e41f3b9 100644 --- a/reports-app/backend/app/routers/treasury.py +++ b/backend/modules/reports/routers/treasury.py @@ -1,12 +1,11 @@ from fastapi import APIRouter, Depends, HTTPException, Query from typing import Optional, List from datetime import date -import sys +# import sys # Removed - no longer needed import os -sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) -from auth.dependencies import get_current_user -from auth.models import CurrentUser +from shared.auth.dependencies import get_current_user +from shared.auth.models import CurrentUser from ..models.treasury import RegisterFilter, RegisterListResponse from ..services.treasury_service import TreasuryService diff --git a/reports-app/backend/app/routers/trial_balance.py b/backend/modules/reports/routers/trial_balance.py similarity index 95% rename from reports-app/backend/app/routers/trial_balance.py rename to backend/modules/reports/routers/trial_balance.py index 9f0b792..91445df 100644 --- a/reports-app/backend/app/routers/trial_balance.py +++ b/backend/modules/reports/routers/trial_balance.py @@ -5,12 +5,11 @@ Refactored to use service layer with caching from fastapi import APIRouter, Depends, HTTPException, Query from typing import Optional from datetime import date -import sys +# import sys # Removed - no longer needed import os -sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) -from auth.dependencies import get_current_user -from auth.models import CurrentUser +from shared.auth.dependencies import get_current_user +from shared.auth.models import CurrentUser from ..models.trial_balance import TrialBalanceResponse from ..services.trial_balance_service import TrialBalanceService import logging diff --git a/reports-app/telegram-bot/app/__init__.py b/backend/modules/reports/schemas/__init__.py similarity index 100% rename from reports-app/telegram-bot/app/__init__.py rename to backend/modules/reports/schemas/__init__.py diff --git a/reports-app/telegram-bot/app/agent/__init__.py b/backend/modules/reports/services/__init__.py similarity index 100% rename from reports-app/telegram-bot/app/agent/__init__.py rename to backend/modules/reports/services/__init__.py diff --git a/reports-app/backend/app/services/calendar_service.py b/backend/modules/reports/services/calendar_service.py similarity index 95% rename from reports-app/backend/app/services/calendar_service.py rename to backend/modules/reports/services/calendar_service.py index e1dd318..db4a72f 100644 --- a/reports-app/backend/app/services/calendar_service.py +++ b/backend/modules/reports/services/calendar_service.py @@ -1,11 +1,10 @@ """ Calendar service for fetching available accounting periods """ -import sys +# import sys # Removed - no longer needed import os -sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) -from database.oracle_pool import oracle_pool +from shared.database.oracle_pool import oracle_pool from ..models.calendar import CalendarPeriod, CalendarPeriodsResponse from ..cache.decorators import cached import logging diff --git a/reports-app/backend/app/services/dashboard_service.py b/backend/modules/reports/services/dashboard_service.py similarity index 99% rename from reports-app/backend/app/services/dashboard_service.py rename to backend/modules/reports/services/dashboard_service.py index 91c9c29..00973bd 100644 --- a/reports-app/backend/app/services/dashboard_service.py +++ b/backend/modules/reports/services/dashboard_service.py @@ -1,8 +1,7 @@ -import sys +# import sys # Removed - no longer needed import os -sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) -from database.oracle_pool import oracle_pool +from shared.database.oracle_pool import oracle_pool from ..models.dashboard import DashboardSummary, TreasuryAccount, TrendData from ..cache.decorators import cached from decimal import Decimal diff --git a/reports-app/backend/app/services/invoice_service.py b/backend/modules/reports/services/invoice_service.py similarity index 99% rename from reports-app/backend/app/services/invoice_service.py rename to backend/modules/reports/services/invoice_service.py index e5b44f6..7912cae 100644 --- a/reports-app/backend/app/services/invoice_service.py +++ b/backend/modules/reports/services/invoice_service.py @@ -1,11 +1,10 @@ """ Service pentru logica facturi - Portează query-urile din aplicația Flask """ -import sys +# import sys # Removed - no longer needed import os -sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) -from database.oracle_pool import oracle_pool +from shared.database.oracle_pool import oracle_pool from typing import List, Tuple from ..models.invoice import Invoice, InvoiceFilter, InvoiceListResponse, InvoiceSummary from ..cache.decorators import cached diff --git a/reports-app/backend/app/services/treasury_service.py b/backend/modules/reports/services/treasury_service.py similarity index 99% rename from reports-app/backend/app/services/treasury_service.py rename to backend/modules/reports/services/treasury_service.py index 9f46cc9..0d936b0 100644 --- a/reports-app/backend/app/services/treasury_service.py +++ b/backend/modules/reports/services/treasury_service.py @@ -1,9 +1,8 @@ -import sys +# import sys # Removed - no longer needed import os -sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) import oracledb -from database.oracle_pool import oracle_pool +from shared.database.oracle_pool import oracle_pool from ..models.treasury import BankCashRegister, RegisterFilter, RegisterListResponse, AccountingPeriod from ..cache.decorators import cached from decimal import Decimal diff --git a/reports-app/backend/app/services/trial_balance_service.py b/backend/modules/reports/services/trial_balance_service.py similarity index 98% rename from reports-app/backend/app/services/trial_balance_service.py rename to backend/modules/reports/services/trial_balance_service.py index 7fd8132..23b19f4 100644 --- a/reports-app/backend/app/services/trial_balance_service.py +++ b/backend/modules/reports/services/trial_balance_service.py @@ -2,11 +2,10 @@ Service pentru Trial Balance (Balanță de Verificare) - Query VBAL VIEW Refactored to use caching system for optimal performance """ -import sys +# import sys # Removed - no longer needed import os -sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) -from database.oracle_pool import oracle_pool +from shared.database.oracle_pool import oracle_pool from typing import Dict, Any from ..models.trial_balance import ( TrialBalanceItem, diff --git a/reports-app/telegram-bot/app/api/__init__.py b/backend/modules/telegram/__init__.py similarity index 100% rename from reports-app/telegram-bot/app/api/__init__.py rename to backend/modules/telegram/__init__.py diff --git a/reports-app/telegram-bot/app/auth/__init__.py b/backend/modules/telegram/agent/__init__.py similarity index 100% rename from reports-app/telegram-bot/app/auth/__init__.py rename to backend/modules/telegram/agent/__init__.py diff --git a/reports-app/telegram-bot/app/agent/session.py b/backend/modules/telegram/agent/session.py similarity index 99% rename from reports-app/telegram-bot/app/agent/session.py rename to backend/modules/telegram/agent/session.py index 250eed7..211faec 100644 --- a/reports-app/telegram-bot/app/agent/session.py +++ b/backend/modules/telegram/agent/session.py @@ -10,7 +10,7 @@ import json from typing import Dict, Any, Optional from datetime import datetime -from app.db.operations import ( +from backend.modules.telegram.db.operations import ( create_session, get_user_active_session, update_session_state, diff --git a/reports-app/telegram-bot/app/bot/__init__.py b/backend/modules/telegram/api/__init__.py similarity index 100% rename from reports-app/telegram-bot/app/bot/__init__.py rename to backend/modules/telegram/api/__init__.py diff --git a/reports-app/telegram-bot/app/api/client.py b/backend/modules/telegram/api/client.py similarity index 97% rename from reports-app/telegram-bot/app/api/client.py rename to backend/modules/telegram/api/client.py index ed37c33..77f10e0 100644 --- a/reports-app/telegram-bot/app/api/client.py +++ b/backend/modules/telegram/api/client.py @@ -358,7 +358,7 @@ class BackendAPIClient: headers['X-Include-Cache-Metadata'] = 'true' response = await self.client.get( - "/api/dashboard/summary", + "/api/reports/dashboard/summary", params={"company": str(company_id)}, headers=headers ) @@ -393,7 +393,7 @@ class BackendAPIClient: headers['X-Include-Cache-Metadata'] = 'true' response = await self.client.get( - f"/api/dashboard/treasury-breakdown?company={company_id}", + f"/api/reports/dashboard/treasury-breakdown?company={company_id}", headers=headers ) @@ -429,7 +429,7 @@ class BackendAPIClient: headers['X-Include-Cache-Metadata'] = 'true' response = await self.client.get( - f"/api/dashboard/detailed-data?company={company_id}&data_type={data_type}", + f"/api/reports/dashboard/detailed-data?company={company_id}&data_type={data_type}", headers=headers ) @@ -465,7 +465,7 @@ class BackendAPIClient: headers['X-Include-Cache-Metadata'] = 'true' response = await self.client.get( - f"/api/dashboard/maturity?company={company_id}&period={period}", + f"/api/reports/dashboard/maturity?company={company_id}&period={period}", headers=headers ) @@ -499,7 +499,7 @@ class BackendAPIClient: headers['X-Include-Cache-Metadata'] = 'true' response = await self.client.get( - f"/api/dashboard/performance?company={company_id}", + f"/api/reports/dashboard/performance?company={company_id}", headers=headers ) @@ -535,7 +535,7 @@ class BackendAPIClient: headers['X-Include-Cache-Metadata'] = 'true' response = await self.client.get( - f"/api/dashboard/monthly-flows?company={company_id}&months={months}", + f"/api/reports/dashboard/monthly-flows?company={company_id}&months={months}", headers=headers ) @@ -571,7 +571,7 @@ class BackendAPIClient: headers['X-Include-Cache-Metadata'] = 'true' response = await self.client.get( - f"/api/dashboard/trends?company={company_id}&period={period}", + f"/api/reports/dashboard/trends?company={company_id}&period={period}", headers=headers ) @@ -619,7 +619,7 @@ class BackendAPIClient: params.update(filters) response = await self.client.get( - "/api/invoices/", + "/api/reports/invoices/", params=params, headers=self._get_auth_headers(jwt_token) ) @@ -660,7 +660,7 @@ class BackendAPIClient: self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT) response = await self.client.get( - "/api/invoices/summary", + "/api/reports/invoices/summary", params={ "company": str(company_id), "partner_type": partner_type @@ -698,7 +698,7 @@ class BackendAPIClient: self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT) response = await self.client.get( - "/api/treasury/bank-cash-register", + "/api/reports/treasury/bank-cash-register", params={ "company": str(company_id), "page": 1, @@ -794,7 +794,7 @@ class BackendAPIClient: request_data['cache_type'] = cache_type response = await self.client.post( - "/api/cache/invalidate", + "/api/reports/cache/invalidate", json=request_data, headers=self._get_auth_headers(jwt_token) ) @@ -827,7 +827,7 @@ class BackendAPIClient: self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT) response = await self.client.post( - "/api/cache/toggle-user", + "/api/reports/cache/toggle-user", json={"enabled": enabled}, headers=self._get_auth_headers(jwt_token) ) @@ -858,7 +858,7 @@ class BackendAPIClient: self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT) response = await self.client.get( - "/api/cache/stats", + "/api/reports/cache/stats", headers=self._get_auth_headers(jwt_token) ) diff --git a/reports-app/telegram-bot/app/utils/__init__.py b/backend/modules/telegram/auth/__init__.py similarity index 100% rename from reports-app/telegram-bot/app/utils/__init__.py rename to backend/modules/telegram/auth/__init__.py diff --git a/reports-app/telegram-bot/app/auth/email_auth.py b/backend/modules/telegram/auth/email_auth.py similarity index 98% rename from reports-app/telegram-bot/app/auth/email_auth.py rename to backend/modules/telegram/auth/email_auth.py index 0070c7b..bc31a26 100644 --- a/reports-app/telegram-bot/app/auth/email_auth.py +++ b/backend/modules/telegram/auth/email_auth.py @@ -114,7 +114,7 @@ async def verify_email_in_oracle(email: str) -> Optional[str]: NOTE: Uses backend API endpoint /api/telegram/auth/verify-email """ try: - from app.api.client import get_backend_client + from backend.modules.telegram.api.client import get_backend_client backend_client = get_backend_client() diff --git a/reports-app/telegram-bot/app/auth/linking.py b/backend/modules/telegram/auth/linking.py similarity index 98% rename from reports-app/telegram-bot/app/auth/linking.py rename to backend/modules/telegram/auth/linking.py index 8a8f050..c958341 100644 --- a/reports-app/telegram-bot/app/auth/linking.py +++ b/backend/modules/telegram/auth/linking.py @@ -12,7 +12,7 @@ from datetime import datetime, timedelta from telegram import User as TelegramUser -from app.db.operations import ( +from backend.modules.telegram.db.operations import ( get_user, create_or_update_user, link_user_to_oracle, @@ -20,7 +20,7 @@ from app.db.operations import ( verify_and_use_auth_code, is_user_linked ) -from app.api.client import get_backend_client +from backend.modules.telegram.api.client import get_backend_client logger = logging.getLogger(__name__) @@ -315,7 +315,7 @@ async def unlink_user(telegram_user_id: int) -> bool: """ try: # Set Oracle username and tokens to NULL - from app.db.database import DB_PATH + from backend.modules.telegram.db.database import DB_PATH import aiosqlite async with aiosqlite.connect(DB_PATH) as db: diff --git a/reports-app/frontend/src/utils/index.js b/backend/modules/telegram/bot/__init__.py similarity index 100% rename from reports-app/frontend/src/utils/index.js rename to backend/modules/telegram/bot/__init__.py diff --git a/reports-app/telegram-bot/app/bot/email_handlers.py b/backend/modules/telegram/bot/email_handlers.py similarity index 98% rename from reports-app/telegram-bot/app/bot/email_handlers.py rename to backend/modules/telegram/bot/email_handlers.py index 98e2210..bb11db1 100644 --- a/reports-app/telegram-bot/app/bot/email_handlers.py +++ b/backend/modules/telegram/bot/email_handlers.py @@ -14,7 +14,7 @@ import logging from datetime import datetime import asyncio -from app.auth.email_auth import ( +from backend.modules.telegram.auth.email_auth import ( is_valid_email_format, verify_email_in_oracle, generate_email_code, @@ -22,8 +22,8 @@ from app.auth.email_auth import ( check_rate_limit, clear_rate_limit ) -from app.utils.email_service import get_email_service -from app.db.operations import ( +from backend.modules.telegram.utils.email_service import get_email_service +from backend.modules.telegram.db.operations import ( create_email_auth_code, get_email_auth_code, get_pending_email_code, @@ -34,7 +34,7 @@ from app.db.operations import ( link_user_to_oracle, create_or_update_user ) -from app.api.client import get_backend_client +from backend.modules.telegram.api.client import get_backend_client logger = logging.getLogger(__name__) @@ -635,8 +635,8 @@ async def receive_password(update: Update, context: ContextTypes.DEFAULT_TYPE): clear_rate_limit(f"resend_{user_id}") # Get session and active company BEFORE editing message - from app.agent.session import get_session_manager - from app.bot.menus import create_main_menu, pad_message_for_wide_buttons + from backend.modules.telegram.agent.session import get_session_manager + from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons session_manager = get_session_manager() session = await session_manager.get_or_create_session(user_id) diff --git a/reports-app/telegram-bot/app/bot/formatters.py b/backend/modules/telegram/bot/formatters.py similarity index 100% rename from reports-app/telegram-bot/app/bot/formatters.py rename to backend/modules/telegram/bot/formatters.py diff --git a/reports-app/telegram-bot/app/bot/handlers.py b/backend/modules/telegram/bot/handlers.py similarity index 91% rename from reports-app/telegram-bot/app/bot/handlers.py rename to backend/modules/telegram/bot/handlers.py index a514f87..a391abc 100644 --- a/reports-app/telegram-bot/app/bot/handlers.py +++ b/backend/modules/telegram/bot/handlers.py @@ -13,15 +13,15 @@ from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import ContextTypes from telegram.constants import ParseMode -from app.auth.linking import ( +from backend.modules.telegram.auth.linking import ( link_telegram_account, check_user_linked, get_user_auth_data, get_user_companies ) -from app.agent.session import get_session_manager -from app.db.operations import update_user_last_active -from app.api.client import get_backend_client +from backend.modules.telegram.agent.session import get_session_manager +from backend.modules.telegram.db.operations import update_user_last_active +from backend.modules.telegram.api.client import get_backend_client logger = logging.getLogger(__name__) @@ -107,7 +107,7 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): # Get cache status cache_enabled = None try: - from app.api.client import get_backend_client + from backend.modules.telegram.api.client import get_backend_client client = get_backend_client() async with client: cache_stats = await client.get_cache_stats(jwt_token=jwt_token) @@ -115,7 +115,7 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): except Exception as e: logger.warning(f"Could not get cache status in /start: {e}") - from app.bot.menus import create_main_menu, pad_message_for_wide_buttons + from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons keyboard = create_main_menu(company_name, company_cui, is_authenticated=True, cache_enabled=cache_enabled) # EDIT message to show menu with company @@ -169,7 +169,7 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): company_cui = company.get('cui') if company else None # Create main menu - from app.bot.menus import create_main_menu, get_menu_message + from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message keyboard = create_main_menu(company_name, company_cui, is_authenticated=True) menu_text = get_menu_message(company_name, company_cui) @@ -181,7 +181,7 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): else: # User not linked - show main menu with Login button - from app.bot.menus import create_main_menu, pad_message_for_wide_buttons + from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons keyboard = create_main_menu( company_name=None, @@ -364,7 +364,7 @@ async def clearcache_command(update: Update, context: ContextTypes.DEFAULT_TYPE) else: # Get active company session_manager = get_session_manager() - from app.bot.helpers import get_active_company_or_prompt + from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: @@ -636,7 +636,7 @@ async def dashboard_command(update: Update, context: ContextTypes.DEFAULT_TYPE): # Get active company session_manager = get_session_manager() - from app.bot.helpers import get_active_company_or_prompt + from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: @@ -683,7 +683,7 @@ async def facturi_command(update: Update, context: ContextTypes.DEFAULT_TYPE): # Get active company session_manager = get_session_manager() - from app.bot.helpers import get_active_company_or_prompt + from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: @@ -712,11 +712,11 @@ async def facturi_command(update: Update, context: ContextTypes.DEFAULT_TYPE): ) # Format response - from app.bot.formatters import format_invoices_response + from backend.modules.telegram.bot.formatters import format_invoices_response response = format_invoices_response(invoices, company['name']) # FAZA 3: Add action buttons - from app.bot.menus import create_action_buttons + from backend.modules.telegram.bot.menus import create_action_buttons keyboard = create_action_buttons("facturi", show_export=True) await update.message.reply_text( @@ -752,7 +752,7 @@ async def trezorerie_command(update: Update, context: ContextTypes.DEFAULT_TYPE) # Get active company session_manager = get_session_manager() - from app.bot.helpers import get_active_company_or_prompt + from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: @@ -763,7 +763,7 @@ async def trezorerie_command(update: Update, context: ContextTypes.DEFAULT_TYPE) jwt_token = auth_data['jwt_token'] # ✅ MODIFICARE: Folosim treasury_breakdown_split ca în Casa/Banca - from app.bot.helpers import get_treasury_breakdown_split + from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split treasury_data = await get_treasury_breakdown_split( company_id=company['id'], jwt_token=jwt_token @@ -790,12 +790,12 @@ async def trezorerie_command(update: Update, context: ContextTypes.DEFAULT_TYPE) content += "Foloseste butoanele pentru detalii:" # Apply company header formatting - from app.bot.menus import format_response_with_company + from backend.modules.telegram.bot.menus import format_response_with_company text = format_response_with_company(content, company['name']) # Add performance footer if response_time_ms > 0: - from app.bot.formatters import add_performance_footer + from backend.modules.telegram.bot.formatters import add_performance_footer text = add_performance_footer(text, cache_hit, response_time_ms, cache_source) # Add buttons to view details @@ -857,7 +857,7 @@ async def menu_command(update: Update, context: ContextTypes.DEFAULT_TYPE): # Get cache status for user cache_enabled = None try: - from app.api.client import get_backend_client + from backend.modules.telegram.api.client import get_backend_client client = get_backend_client() async with client: cache_stats = await client.get_cache_stats(jwt_token=auth_data['jwt_token']) @@ -866,7 +866,7 @@ async def menu_command(update: Update, context: ContextTypes.DEFAULT_TYPE): logger.warning(f"Could not get cache status: {e}") # Create main menu (user is authenticated if they passed the is_linked check) - from app.bot.menus import create_main_menu, get_menu_message + from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message keyboard = create_main_menu(company_name, company_cui, is_authenticated=True, cache_enabled=cache_enabled) menu_text = get_menu_message(company_name, company_cui) @@ -906,7 +906,7 @@ async def trezorerie_casa_command(update: Update, context: ContextTypes.DEFAULT_ # Get active company session_manager = get_session_manager() - from app.bot.helpers import get_active_company_or_prompt + from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: @@ -917,7 +917,7 @@ async def trezorerie_casa_command(update: Update, context: ContextTypes.DEFAULT_ jwt_token = auth_data['jwt_token'] # Get treasury breakdown split - from app.bot.helpers import get_treasury_breakdown_split + from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split treasury_data = await get_treasury_breakdown_split( company_id=company['id'], jwt_token=jwt_token @@ -928,8 +928,8 @@ async def trezorerie_casa_command(update: Update, context: ContextTypes.DEFAULT_ return # Format response - from app.bot.formatters import format_treasury_casa_response, add_performance_footer - from app.bot.menus import create_action_buttons, format_response_with_company + from backend.modules.telegram.bot.formatters import format_treasury_casa_response, add_performance_footer + from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_treasury_casa_response(treasury_data['casa']) response = format_response_with_company(content, company['name']) @@ -979,7 +979,7 @@ async def trezorerie_banca_command(update: Update, context: ContextTypes.DEFAULT # Get active company session_manager = get_session_manager() - from app.bot.helpers import get_active_company_or_prompt + from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: @@ -990,7 +990,7 @@ async def trezorerie_banca_command(update: Update, context: ContextTypes.DEFAULT jwt_token = auth_data['jwt_token'] # Get treasury breakdown split - from app.bot.helpers import get_treasury_breakdown_split + from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split treasury_data = await get_treasury_breakdown_split( company_id=company['id'], jwt_token=jwt_token @@ -1001,8 +1001,8 @@ async def trezorerie_banca_command(update: Update, context: ContextTypes.DEFAULT return # Format response - from app.bot.formatters import format_treasury_banca_response, add_performance_footer - from app.bot.menus import create_action_buttons, format_response_with_company + from backend.modules.telegram.bot.formatters import format_treasury_banca_response, add_performance_footer + from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_treasury_banca_response(treasury_data['banca']) response = format_response_with_company(content, company['name']) @@ -1053,7 +1053,7 @@ async def clienti_command(update: Update, context: ContextTypes.DEFAULT_TYPE): # Get active company session_manager = get_session_manager() - from app.bot.helpers import get_active_company_or_prompt + from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: @@ -1064,7 +1064,7 @@ async def clienti_command(update: Update, context: ContextTypes.DEFAULT_TYPE): jwt_token = auth_data['jwt_token'] # Get clients with maturity data - from app.bot.helpers import get_clients_with_maturity + from backend.modules.telegram.bot.helpers import get_clients_with_maturity clients_data = await get_clients_with_maturity( company_id=company['id'], jwt_token=jwt_token @@ -1080,8 +1080,8 @@ async def clienti_command(update: Update, context: ContextTypes.DEFAULT_TYPE): cache_source = clients_data.get('cache_source', None) # Format response - from app.bot.formatters import format_clients_balance_response, add_performance_footer - from app.bot.menus import create_client_list_keyboard, format_response_with_company + from backend.modules.telegram.bot.formatters import format_clients_balance_response, add_performance_footer + from backend.modules.telegram.bot.menus import create_client_list_keyboard, format_response_with_company content = format_clients_balance_response( clients_data['clients'], @@ -1132,7 +1132,7 @@ async def furnizori_command(update: Update, context: ContextTypes.DEFAULT_TYPE): # Get active company session_manager = get_session_manager() - from app.bot.helpers import get_active_company_or_prompt + from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: @@ -1143,7 +1143,7 @@ async def furnizori_command(update: Update, context: ContextTypes.DEFAULT_TYPE): jwt_token = auth_data['jwt_token'] # Get suppliers with maturity data - from app.bot.helpers import get_suppliers_with_maturity + from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity suppliers_data = await get_suppliers_with_maturity( company_id=company['id'], jwt_token=jwt_token @@ -1159,8 +1159,8 @@ async def furnizori_command(update: Update, context: ContextTypes.DEFAULT_TYPE): cache_source = suppliers_data.get('cache_source', None) # Format response - from app.bot.formatters import format_suppliers_balance_response, add_performance_footer - from app.bot.menus import create_supplier_list_keyboard, format_response_with_company + from backend.modules.telegram.bot.formatters import format_suppliers_balance_response, add_performance_footer + from backend.modules.telegram.bot.menus import create_supplier_list_keyboard, format_response_with_company content = format_suppliers_balance_response( suppliers_data['suppliers'], @@ -1210,7 +1210,7 @@ async def evolutie_command(update: Update, context: ContextTypes.DEFAULT_TYPE): # Get active company session_manager = get_session_manager() - from app.bot.helpers import get_active_company_or_prompt + from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: @@ -1221,7 +1221,7 @@ async def evolutie_command(update: Update, context: ContextTypes.DEFAULT_TYPE): jwt_token = auth_data['jwt_token'] # Get cash flow evolution data - from app.bot.helpers import get_cashflow_evolution_data + from backend.modules.telegram.bot.helpers import get_cashflow_evolution_data evolution_data = await get_cashflow_evolution_data( company_id=company['id'], jwt_token=jwt_token @@ -1232,8 +1232,8 @@ async def evolutie_command(update: Update, context: ContextTypes.DEFAULT_TYPE): return # Format response - from app.bot.formatters import format_cashflow_evolution_response, add_performance_footer - from app.bot.menus import create_action_buttons, format_response_with_company + from backend.modules.telegram.bot.formatters import format_cashflow_evolution_response, add_performance_footer + from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_cashflow_evolution_response( evolution_data['performance'], @@ -1340,7 +1340,7 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE company_name = company['name'] if company else None company_cui = company.get('cui') if company else None - from app.bot.menus import create_main_menu, pad_message_for_wide_buttons + from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons keyboard = create_main_menu(company_name, company_cui, is_authenticated=True) # Create menu text @@ -1420,7 +1420,7 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str) # If user is not authenticated and trying to access financial data, show auth required message if auth_data is None and action != "select_company": - from app.bot.menus import create_main_menu, pad_message_for_wide_buttons + from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons keyboard = create_main_menu(company_name=None, company_cui=None, is_authenticated=False) menu_text = pad_message_for_wide_buttons( "⚠️ **Autentificare necesară**\n\n" @@ -1461,7 +1461,7 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str) ) return - from app.bot.helpers import create_company_selection_keyboard_paginated + from backend.modules.telegram.bot.helpers import create_company_selection_keyboard_paginated keyboard = create_company_selection_keyboard_paginated(companies, page=0) await query.edit_message_text( @@ -1485,11 +1485,11 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str) elif action == "casa": # Trezorerie casa - from app.bot.helpers import get_treasury_breakdown_split + from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split treasury_data = await get_treasury_breakdown_split(company['id'], jwt_token) - from app.bot.formatters import format_treasury_casa_response, add_performance_footer - from app.bot.menus import create_action_buttons, format_response_with_company + from backend.modules.telegram.bot.formatters import format_treasury_casa_response, add_performance_footer + from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_treasury_casa_response(treasury_data['casa']) response = format_response_with_company(content, company['name']) @@ -1516,11 +1516,11 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str) elif action == "banca": # Trezorerie banca - from app.bot.helpers import get_treasury_breakdown_split + from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split treasury_data = await get_treasury_breakdown_split(company['id'], jwt_token) - from app.bot.formatters import format_treasury_banca_response, add_performance_footer - from app.bot.menus import create_action_buttons, format_response_with_company + from backend.modules.telegram.bot.formatters import format_treasury_banca_response, add_performance_footer + from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_treasury_banca_response(treasury_data['banca']) response = format_response_with_company(content, company['name']) @@ -1547,11 +1547,11 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str) elif action == "clienti": # Sold clienți + listă cu paginare - from app.bot.helpers import get_clients_with_maturity + from backend.modules.telegram.bot.helpers import get_clients_with_maturity clients_data = await get_clients_with_maturity(company['id'], jwt_token) - from app.bot.formatters import format_clients_balance_response, add_performance_footer - from app.bot.menus import create_client_list_keyboard, format_response_with_company + from backend.modules.telegram.bot.formatters import format_clients_balance_response, add_performance_footer + from backend.modules.telegram.bot.menus import create_client_list_keyboard, format_response_with_company content = format_clients_balance_response( clients_data['clients'], @@ -1576,11 +1576,11 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str) elif action == "furnizori": # Sold furnizori + listă cu paginare - from app.bot.helpers import get_suppliers_with_maturity + from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token) - from app.bot.formatters import format_suppliers_balance_response, add_performance_footer - from app.bot.menus import create_supplier_list_keyboard, format_response_with_company + from backend.modules.telegram.bot.formatters import format_suppliers_balance_response, add_performance_footer + from backend.modules.telegram.bot.menus import create_supplier_list_keyboard, format_response_with_company content = format_suppliers_balance_response( suppliers_data['suppliers'], @@ -1605,11 +1605,11 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str) elif action == "evolutie": # Evoluție cash flow - from app.bot.helpers import get_cashflow_evolution_data + from backend.modules.telegram.bot.helpers import get_cashflow_evolution_data evolution_data = await get_cashflow_evolution_data(company['id'], jwt_token) - from app.bot.formatters import format_cashflow_evolution_response, add_performance_footer - from app.bot.menus import create_action_buttons, format_response_with_company + from backend.modules.telegram.bot.formatters import format_cashflow_evolution_response, add_performance_footer + from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_cashflow_evolution_response( evolution_data['performance'], @@ -1739,7 +1739,7 @@ async def handle_action_callback(query, telegram_user_id: int, callback_data: st cache_enabled = None if is_authenticated: try: - from app.api.client import get_backend_client + from backend.modules.telegram.api.client import get_backend_client client = get_backend_client() async with client: cache_stats = await client.get_cache_stats(jwt_token=auth_data['jwt_token']) @@ -1747,7 +1747,7 @@ async def handle_action_callback(query, telegram_user_id: int, callback_data: st except Exception as e: logger.warning(f"Could not get cache status: {e}") - from app.bot.menus import create_main_menu, get_menu_message + from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message company_name = company['name'] if company else None company_cui = company.get('cui') if company else None keyboard = create_main_menu(company_name, company_cui, is_authenticated, cache_enabled) @@ -1784,7 +1784,7 @@ async def handle_action_callback(query, telegram_user_id: int, callback_data: st elif action_type == "help": # Show help message above menu (edit current message) - from app.bot.menus import pad_message_for_wide_buttons, create_main_menu + from backend.modules.telegram.bot.menus import pad_message_for_wide_buttons, create_main_menu # Get auth status and company info session_manager = get_session_manager() @@ -1820,7 +1820,7 @@ async def handle_action_callback(query, telegram_user_id: int, callback_data: st elif action_type == "logout": # Show logout confirmation - from app.bot.menus import pad_message_for_wide_buttons + from backend.modules.telegram.bot.menus import pad_message_for_wide_buttons confirmation_text = pad_message_for_wide_buttons( "**Confirmare Deconectare**\n\n" "Ești sigur că vrei să deconectezi contul?\n\n" @@ -1873,7 +1873,7 @@ async def handle_details_callback(query, telegram_user_id: int, callback_data: s if detail_type == "client": # Get client details (from clients list) # entity_name might be truncated to 40 chars, so search by startswith - from app.bot.helpers import get_clients_with_maturity + from backend.modules.telegram.bot.helpers import get_clients_with_maturity clients_data = await get_clients_with_maturity(company['id'], jwt_token) # Find client by full or partial name match @@ -1887,12 +1887,12 @@ async def handle_details_callback(query, telegram_user_id: int, callback_data: s full_client_name = client['name'] # Get client invoices by FULL name - from app.bot.helpers import get_client_invoices + from backend.modules.telegram.bot.helpers import get_client_invoices invoices = await get_client_invoices(company['id'], full_client_name, jwt_token) # Format response - from app.bot.formatters import format_client_detail_response - from app.bot.menus import create_action_buttons, format_response_with_company + from backend.modules.telegram.bot.formatters import format_client_detail_response + from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_client_detail_response(client, invoices) response = format_response_with_company(content, company['name']) @@ -1910,7 +1910,7 @@ async def handle_details_callback(query, telegram_user_id: int, callback_data: s elif detail_type == "supplier": # Get supplier details (from suppliers list) # entity_name might be truncated to 40 chars, so search by startswith - from app.bot.helpers import get_suppliers_with_maturity + from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token) # Find supplier by full or partial name match @@ -1924,12 +1924,12 @@ async def handle_details_callback(query, telegram_user_id: int, callback_data: s full_supplier_name = supplier['name'] # Get supplier invoices by FULL name - from app.bot.helpers import get_supplier_invoices + from backend.modules.telegram.bot.helpers import get_supplier_invoices invoices = await get_supplier_invoices(company['id'], full_supplier_name, jwt_token) # Format response - from app.bot.formatters import format_supplier_detail_response - from app.bot.menus import create_action_buttons, format_response_with_company + from backend.modules.telegram.bot.formatters import format_supplier_detail_response + from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_supplier_detail_response(supplier, invoices) response = format_response_with_company(content, company['name']) @@ -2058,7 +2058,7 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): companies = await client.get_user_companies(jwt_token=jwt_token) # Create paginated keyboard for requested page - from app.bot.helpers import create_company_selection_keyboard_paginated + from backend.modules.telegram.bot.helpers import create_company_selection_keyboard_paginated keyboard = create_company_selection_keyboard_paginated(companies, page=page) await query.edit_message_text( @@ -2103,7 +2103,7 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): await session_manager.save_session(telegram_user_id) # Show main menu directly (no confirmation message) - from app.bot.menus import create_main_menu, get_menu_message + from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message keyboard = create_main_menu( company_name=company_name, company_cui=company_cui @@ -2124,7 +2124,7 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): elif callback_data == "logout_confirm": # Logout user (same as unlink but shows menu after) - from app.auth.linking import unlink_user + from backend.modules.telegram.auth.linking import unlink_user success = await unlink_user(telegram_user_id) @@ -2134,7 +2134,7 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): await session_manager.delete_session(telegram_user_id) # Show login menu (non-authenticated) - from app.bot.menus import create_main_menu, get_menu_message, pad_message_for_wide_buttons + from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message, pad_message_for_wide_buttons keyboard = create_main_menu(company_name=None, company_cui=None, is_authenticated=False) menu_text = pad_message_for_wide_buttons( "**Deconectat cu succes**\n\n" @@ -2163,7 +2163,7 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): auth_data = await get_user_auth_data(telegram_user_id) is_authenticated = auth_data is not None - from app.bot.menus import create_main_menu, get_menu_message + from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message company_name = company['name'] if company else None company_cui = company.get('cui') if company else None keyboard = create_main_menu(company_name, company_cui, is_authenticated) @@ -2179,7 +2179,7 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): elif callback_data == "unlink_confirm": # Unlink user - from app.auth.linking import unlink_user + from backend.modules.telegram.auth.linking import unlink_user success = await unlink_user(telegram_user_id) @@ -2383,11 +2383,11 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): company = session.get_active_company() # Get clients with maturity - from app.bot.helpers import get_clients_with_maturity + from backend.modules.telegram.bot.helpers import get_clients_with_maturity clients_data = await get_clients_with_maturity(company['id'], jwt_token) - from app.bot.formatters import format_clients_balance_response - from app.bot.menus import create_client_list_keyboard, format_response_with_company + from backend.modules.telegram.bot.formatters import format_clients_balance_response + from backend.modules.telegram.bot.menus import create_client_list_keyboard, format_response_with_company content = format_clients_balance_response( clients_data['clients'], @@ -2415,11 +2415,11 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): company = session.get_active_company() # Get suppliers with maturity - from app.bot.helpers import get_suppliers_with_maturity + from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token) - from app.bot.formatters import format_suppliers_balance_response - from app.bot.menus import create_supplier_list_keyboard, format_response_with_company + from backend.modules.telegram.bot.formatters import format_suppliers_balance_response + from backend.modules.telegram.bot.menus import create_supplier_list_keyboard, format_response_with_company content = format_suppliers_balance_response( suppliers_data['suppliers'], @@ -2452,27 +2452,27 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): # Get invoices for this partner if partner_type == "CLIENTI": - from app.bot.helpers import get_client_invoices, get_clients_with_maturity + from backend.modules.telegram.bot.helpers import get_client_invoices, get_clients_with_maturity invoices = await get_client_invoices(company['id'], partner_name, jwt_token) # Get client details clients_data = await get_clients_with_maturity(company['id'], jwt_token) partner = next((c for c in clients_data['clients'] if c['name'] == partner_name), None) - from app.bot.formatters import format_client_detail_response + from backend.modules.telegram.bot.formatters import format_client_detail_response content = format_client_detail_response(partner, invoices) else: - from app.bot.helpers import get_supplier_invoices, get_suppliers_with_maturity + from backend.modules.telegram.bot.helpers import get_supplier_invoices, get_suppliers_with_maturity invoices = await get_supplier_invoices(company['id'], partner_name, jwt_token) # Get supplier details suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token) partner = next((s for s in suppliers_data['suppliers'] if s['name'] == partner_name), None) - from app.bot.formatters import format_supplier_detail_response + from backend.modules.telegram.bot.formatters import format_supplier_detail_response content = format_supplier_detail_response(partner, invoices) - from app.bot.menus import create_invoice_list_keyboard, format_response_with_company + from backend.modules.telegram.bot.menus import create_invoice_list_keyboard, format_response_with_company response = format_response_with_company(content, company['name']) keyboard = create_invoice_list_keyboard(invoices, partner_type, partner_name, page=page) @@ -2555,7 +2555,7 @@ async def _handle_expired_auth(query_or_update, telegram_user_id: int, auth_data await query_or_update.answer("Sesiunea a expirat. Te rog să te reconectezi.", show_alert=True) # Transform the current message (menu) to Login menu - from app.bot.menus import create_main_menu, pad_message_for_wide_buttons + from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons keyboard = create_main_menu(company_name=None, company_cui=None, is_authenticated=False) menu_text = pad_message_for_wide_buttons( "⚠️ **Sesiunea a expirat**\n\n" @@ -2615,8 +2615,8 @@ async def _handle_sold_view( await query_or_update.message.reply_text(error_msg) return - from app.bot.formatters import format_dashboard_response, add_performance_footer - from app.bot.menus import create_action_buttons, format_response_with_company + from backend.modules.telegram.bot.formatters import format_dashboard_response, add_performance_footer + from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company # Extract cache metadata cache_hit = data.get('cache_hit', False) @@ -2684,7 +2684,7 @@ async def _handle_selectcompany_view( # Apply search filter if provided if search_term: - from app.bot.helpers import search_companies_by_name + from backend.modules.telegram.bot.helpers import search_companies_by_name companies = await search_companies_by_name(search_term, jwt_token) if not companies: @@ -2710,7 +2710,7 @@ async def _handle_selectcompany_view( ) return - from app.bot.helpers import create_company_selection_keyboard_paginated + from backend.modules.telegram.bot.helpers import create_company_selection_keyboard_paginated keyboard = create_company_selection_keyboard_paginated(companies, page=page) message = f"**Selecteaza Compania**\n\n" diff --git a/reports-app/telegram-bot/app/bot/helpers.py b/backend/modules/telegram/bot/helpers.py similarity index 98% rename from reports-app/telegram-bot/app/bot/helpers.py rename to backend/modules/telegram/bot/helpers.py index 3137ccc..e52196a 100644 --- a/reports-app/telegram-bot/app/bot/helpers.py +++ b/backend/modules/telegram/bot/helpers.py @@ -7,9 +7,9 @@ import logging from typing import Optional, Dict, List, Any from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup -from app.api.client import get_backend_client -from app.agent.session import SessionManager -from app.bot.menus import pad_message_for_wide_buttons +from backend.modules.telegram.api.client import get_backend_client +from backend.modules.telegram.agent.session import SessionManager +from backend.modules.telegram.bot.menus import pad_message_for_wide_buttons logger = logging.getLogger(__name__) @@ -44,7 +44,7 @@ async def get_active_company_or_prompt( if not company: # Get auth data and companies - from app.auth.linking import get_user_auth_data + from backend.modules.telegram.auth.linking import get_user_auth_data auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] diff --git a/reports-app/telegram-bot/app/bot/keyboards.py b/backend/modules/telegram/bot/keyboards.py similarity index 100% rename from reports-app/telegram-bot/app/bot/keyboards.py rename to backend/modules/telegram/bot/keyboards.py diff --git a/reports-app/telegram-bot/app/bot/menus.py b/backend/modules/telegram/bot/menus.py similarity index 100% rename from reports-app/telegram-bot/app/bot/menus.py rename to backend/modules/telegram/bot/menus.py diff --git a/reports-app/telegram-bot/app/main.py b/backend/modules/telegram/bot_main.py similarity index 97% rename from reports-app/telegram-bot/app/main.py rename to backend/modules/telegram/bot_main.py index 3a36630..17dd327 100644 --- a/reports-app/telegram-bot/app/main.py +++ b/backend/modules/telegram/bot_main.py @@ -30,7 +30,7 @@ from telegram.ext import ( ) # Import database initialization -from app.db import ( +from backend.modules.telegram.db import ( init_database, cleanup_expired_codes, cleanup_expired_sessions, @@ -38,7 +38,7 @@ from app.db import ( ) # Import bot handlers -from app.bot.handlers import ( +from backend.modules.telegram.bot.handlers import ( start_command, help_command, clear_command, @@ -67,10 +67,10 @@ from app.bot.handlers import ( ) # Import email authentication handler -from app.bot.email_handlers import email_login_handler +from backend.modules.telegram.bot.email_handlers import email_login_handler # Import internal API -from app.internal_api import internal_api +from backend.modules.telegram.internal_api import internal_api # Configure logging logging.basicConfig( diff --git a/reports-app/telegram-bot/app/db/__init__.py b/backend/modules/telegram/db/__init__.py similarity index 100% rename from reports-app/telegram-bot/app/db/__init__.py rename to backend/modules/telegram/db/__init__.py diff --git a/reports-app/telegram-bot/app/db/database.py b/backend/modules/telegram/db/database.py similarity index 100% rename from reports-app/telegram-bot/app/db/database.py rename to backend/modules/telegram/db/database.py diff --git a/reports-app/telegram-bot/app/db/operations.py b/backend/modules/telegram/db/operations.py similarity index 100% rename from reports-app/telegram-bot/app/db/operations.py rename to backend/modules/telegram/db/operations.py diff --git a/backend/modules/telegram/routers/__init__.py b/backend/modules/telegram/routers/__init__.py new file mode 100644 index 0000000..153af18 --- /dev/null +++ b/backend/modules/telegram/routers/__init__.py @@ -0,0 +1,32 @@ +"""Telegram module router factory.""" + +from fastapi import APIRouter + + +def create_telegram_router() -> APIRouter: + """ + Create and configure Telegram module router. + + Includes all Telegram bot internal API endpoints: + - /auth/verify-user - Verify Telegram user authentication + - /auth/generate-code - Generate auth code for linking + - /auth/verify-code - Verify auth code + - /stats - Bot database statistics + + Returns: + APIRouter: Configured router for Telegram module + """ + router = APIRouter() + + # Import routers here to avoid circular imports + from .auth_codes import router as auth_codes_router + from .internal_api import internal_api as internal_api_router + + # Include all sub-routers (no prefix - already prefixed in main.py with /api/telegram) + # Auth codes router provides /auth/* endpoints + router.include_router(auth_codes_router, tags=["telegram-auth"]) + + # Internal API router provides additional endpoints like /stats + router.include_router(internal_api_router, tags=["telegram-internal"]) + + return router diff --git a/reports-app/backend/app/routers/telegram.py b/backend/modules/telegram/routers/auth_codes.py similarity index 99% rename from reports-app/backend/app/routers/telegram.py rename to backend/modules/telegram/routers/auth_codes.py index e00ef06..b60ab20 100644 --- a/reports-app/backend/app/routers/telegram.py +++ b/backend/modules/telegram/routers/auth_codes.py @@ -4,7 +4,7 @@ Furnizează endpoint-uri pentru autentificare, linking și export rapoarte pentr """ from fastapi import APIRouter, Depends, HTTPException, Request from typing import List, Optional, Dict, Any -import sys +# import sys # Removed - no longer needed import os import secrets import string @@ -12,12 +12,11 @@ import httpx from datetime import datetime, timedelta from pydantic import BaseModel, Field -sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) -from auth.dependencies import get_current_user -from auth.models import CurrentUser -from auth.jwt_handler import jwt_handler -from database.oracle_pool import oracle_pool +from shared.auth.dependencies import get_current_user +from shared.auth.models import CurrentUser +from shared.auth.jwt_handler import jwt_handler +from shared.database.oracle_pool import oracle_pool # Telegram bot internal API URL (running on same server) TELEGRAM_BOT_INTERNAL_API = os.getenv("TELEGRAM_BOT_INTERNAL_API", "http://localhost:8002") diff --git a/reports-app/telegram-bot/app/internal_api.py b/backend/modules/telegram/routers/internal_api.py similarity index 87% rename from reports-app/telegram-bot/app/internal_api.py rename to backend/modules/telegram/routers/internal_api.py index a49c7be..c67c5c7 100644 --- a/reports-app/telegram-bot/app/internal_api.py +++ b/backend/modules/telegram/routers/internal_api.py @@ -14,23 +14,17 @@ import os from datetime import datetime from typing import Optional -from fastapi import FastAPI, HTTPException, status +from fastapi import APIRouter, HTTPException, status from fastapi.responses import JSONResponse from pydantic import BaseModel, Field -from app.db.operations import create_auth_code, get_auth_code -from app.db.database import get_database_stats +from backend.modules.telegram.db.operations import create_auth_code, get_auth_code +from backend.modules.telegram.db.database import get_database_stats logger = logging.getLogger(__name__) -# Initialize FastAPI app -internal_api = FastAPI( - title="ROA2WEB Telegram Bot - Internal API", - description="Internal API for backend communication (auth code management)", - version="1.0.0", - docs_url="/internal/docs" if os.getenv("ENABLE_DOCS", "false") == "true" else None, - redoc_url=None -) +# Initialize APIRouter (converted from FastAPI app for unified backend) +internal_api = APIRouter() # ============================================================================ @@ -333,43 +327,27 @@ async def get_stats(): # EXCEPTION HANDLERS # ============================================================================ -@internal_api.exception_handler(Exception) -async def global_exception_handler(request, exc): - """ - Global exception handler for uncaught exceptions. - """ - logger.error(f"Unhandled exception: {exc}", exc_info=True) - - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content={ - "success": False, - "error": "Internal server error", - "detail": str(exc) if os.getenv("DEBUG", "false") == "true" else "An error occurred" - } - ) - - # ============================================================================ -# STARTUP/SHUTDOWN EVENTS +# NOTE: Exception handlers and startup/shutdown events removed +# These are FastAPI-specific and don't work with APIRouter +# The unified backend (main.py) handles these at the app level # ============================================================================ -@internal_api.on_event("startup") -async def startup_event(): - """ - Startup event handler. - """ - logger.info("Internal API starting up...") - logger.info(f"Internal API ready on port {os.getenv('INTERNAL_API_PORT', '8002')}") +# @internal_api.exception_handler(Exception) - Not supported by APIRouter +# async def global_exception_handler(request, exc): +# """Global exception handler - moved to main.py""" +# pass + +# @internal_api.on_event("startup") - Not supported by APIRouter +# async def startup_event(): +# """Startup event - handled by main.py lifespan""" +# pass + +# @internal_api.on_event("shutdown") - Not supported by APIRouter +# async def shutdown_event(): +# """Shutdown event - handled by main.py lifespan""" +# pass -@internal_api.on_event("shutdown") -async def shutdown_event(): - """ - Shutdown event handler. - """ - logger.info("Internal API shutting down...") - - -# Export the FastAPI app +# Export the APIRouter __all__ = ['internal_api'] diff --git a/backend/modules/telegram/utils/__init__.py b/backend/modules/telegram/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/reports-app/telegram-bot/app/utils/email_service.py b/backend/modules/telegram/utils/email_service.py similarity index 100% rename from reports-app/telegram-bot/app/utils/email_service.py rename to backend/modules/telegram/utils/email_service.py diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..423a58d --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,93 @@ +# ============================================================================ +# ROA2WEB Unified Backend - Dependencies +# ============================================================================ +# Merged from reports-app, data-entry-app, and telegram-bot +# All three modules now run in a single backend process + +# ============================================================================ +# FASTAPI CORE (Common to all modules) +# ============================================================================ +fastapi>=0.109.0 +uvicorn[standard]>=0.27.0 +python-multipart>=0.0.6 +pydantic>=2.5.3 +pydantic-settings>=2.1.0 +email-validator>=2.1.0 + +# ============================================================================ +# AUTHENTICATION (Shared across all modules) +# ============================================================================ +PyJWT>=2.8.0 +python-jose[cryptography]>=3.3.0 + +# ============================================================================ +# DATABASE - ORACLE (Shared: Reports + Data Entry nomenclatures + Auth) +# ============================================================================ +oracledb>=2.0.1 + +# ============================================================================ +# DATABASE - SQLITE (Data Entry + Telegram + Reports Cache) +# ============================================================================ +aiosqlite>=0.19.0 + +# ============================================================================ +# DATABASE - SQLMODEL + ALEMBIC (Data Entry only) +# ============================================================================ +sqlmodel>=0.0.14 +sqlalchemy[asyncio]>=2.0.25 +alembic>=1.13.1 + +# ============================================================================ +# HTTP CLIENT (Shared) +# ============================================================================ +httpx>=0.27.0 + +# ============================================================================ +# UTILITIES (Shared) +# ============================================================================ +python-dotenv>=1.0.0 +python-dateutil>=2.8.2 + +# ============================================================================ +# FILE HANDLING (Data Entry) +# ============================================================================ +aiofiles>=23.2.1 +Pillow>=10.2.0 + +# ============================================================================ +# REPORTS MODULE - Exports (Excel, PDF) +# ============================================================================ +openpyxl>=3.1.0 +fpdf2>=2.7.0 + +# ============================================================================ +# DATA ENTRY MODULE - OCR Dependencies +# ============================================================================ +# PaddleOCR for receipt text extraction +paddleocr>=2.7.0 +paddlepaddle>=2.5.0 +opencv-python>=4.8.0 +pytesseract>=0.3.10 +pdf2image>=1.16.0 +numpy>=1.24.0 + +# ============================================================================ +# TELEGRAM MODULE - Bot SDK +# ============================================================================ +python-telegram-bot>=20.7 + +# ============================================================================ +# TELEGRAM MODULE - Email (SMTP for 2FA) +# ============================================================================ +aiosmtplib>=3.0.0 + +# ============================================================================ +# MONITORING (Optional - Telegram module) +# ============================================================================ +# sentry-sdk>=1.40.0 # Uncomment if needed + +# ============================================================================ +# TESTING +# ============================================================================ +pytest>=8.0.0 +pytest-asyncio>=0.23.3 diff --git a/bot.sh b/bot.sh deleted file mode 100644 index 8acb10c..0000000 --- a/bot.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/bin/bash -# Telegram Bot Service Control Script for ROA2WEB Unified App -# Manages the Telegram Bot on port 8002 - -# Script directory -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$SCRIPT_DIR" - -# Source helper functions -source "$SCRIPT_DIR/scripts/service-helpers.sh" - -# Service configuration -SERVICE_NAME="Telegram Bot" -PORT=8002 -LOG_FILE="/tmp/telegram-bot.log" -BOT_DIR="$ROOT_DIR/reports-app/telegram-bot" -VENV_DIR="$BOT_DIR/venv" -ENV_FILE="$BOT_DIR/.env" - -# Function to start bot -start_bot() { - print_header "Starting $SERVICE_NAME" - - # Check if port is already in use - if ! check_port_available $PORT "$SERVICE_NAME"; then - print_warning "$SERVICE_NAME may already be running" - print_info "Use './bot.sh status' to check or './bot.sh stop' to stop it" - return 1 - fi - - # Check bot directory - if [ ! -d "$BOT_DIR" ]; then - print_error "Bot directory not found: $BOT_DIR" - return 1 - fi - - # Check virtual environment - if [ ! -d "$VENV_DIR" ]; then - print_error "Virtual environment not found: $VENV_DIR" - print_info "Please run setup first: cd reports-app/telegram-bot && python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt" - return 1 - fi - - # Check .env file - if [ ! -f "$ENV_FILE" ]; then - print_warning ".env file not found: $ENV_FILE" - print_info "Bot may not start without TELEGRAM_BOT_TOKEN" - fi - - # Start bot - print_info "Starting Telegram bot..." - cd "$BOT_DIR" - - # Activate venv and start bot in background - ( - source venv/bin/activate - nohup python -m app.main > "$LOG_FILE" 2>&1 & - echo $! > /tmp/telegram-bot.pid - ) - - local pid=$(cat /tmp/telegram-bot.pid 2>/dev/null) - print_info "Started with PID: $pid" - print_info "Log file: $LOG_FILE" - - # Wait for port to be ready - if wait_for_port $PORT "$SERVICE_NAME" 5; then - print_success "$SERVICE_NAME started successfully!" - echo "" - print_info "🤖 Bot is running and listening for commands" - print_info "📄 View logs: tail -f $LOG_FILE" - return 0 - else - print_error "Failed to start $SERVICE_NAME (timeout waiting for port $PORT)" - print_info "Check logs: tail -20 $LOG_FILE" - rm -f /tmp/telegram-bot.pid - return 1 - fi -} - -# Function to stop bot -stop_bot() { - print_header "Stopping $SERVICE_NAME" - - kill_port $PORT "$SERVICE_NAME" - - # Clean up PID file - if [ -f "/tmp/telegram-bot.pid" ]; then - rm /tmp/telegram-bot.pid - fi - - return 0 -} - -# Function to show bot status -status_bot() { - print_header "$SERVICE_NAME Status" - - if check_service_status $PORT "$SERVICE_NAME"; then - echo "" - print_info "Service is running" - - # Show recent logs - if [ -f "$LOG_FILE" ]; then - echo "" - tail_logs "$LOG_FILE" 10 - fi - return 0 - else - echo "" - print_warning "Service is not running" - - # Show last logs if available - if [ -f "$LOG_FILE" ]; then - echo "" - print_info "Last logs before shutdown:" - tail_logs "$LOG_FILE" 10 - fi - return 1 - fi -} - -# Main script logic -case "${1:-}" in - start) - start_bot - ;; - stop) - stop_bot - ;; - status) - status_bot - ;; - *) - echo "Usage: $0 {start|stop|status}" - echo "" - echo "Commands:" - echo " start - Start the Telegram bot" - echo " stop - Stop the Telegram bot" - echo " status - Show bot status" - echo "" - echo "Examples:" - echo " ./bot.sh start # Start bot" - echo " ./bot.sh status # Check if running" - echo " ./bot.sh stop # Stop bot" - exit 1 - ;; -esac diff --git a/data-entry-app/CLAUDE.md b/data-entry-app/CLAUDE.md deleted file mode 100644 index 336a3ae..0000000 --- a/data-entry-app/CLAUDE.md +++ /dev/null @@ -1,244 +0,0 @@ -# CLAUDE.md - Data Entry App - -## IMPORTANT - Reguli pentru Claude - -- **Pentru teste folosește DOAR `./start-data-entry-test.sh`** (server TEST 10.0.20.121) -- **NU folosi `./start-data-entry-dev.sh`** decât la instrucțiuni explicite de la utilizator (server PRODUCȚIE 10.0.20.36) - -## Scop - -Aplicatie pentru introducere date in ERP (bonuri fiscale, chitante) cu workflow de aprobare. - -## Documentatie de Referinta - -- **Cerinte**: `docs/data-entry/REQUIREMENTS.md` -- **Arhitectura**: `docs/data-entry/ARCHITECTURE.md` -- **Quick Start**: `README.md` - -## Decizii Tehnice - -- **ORM**: SQLModel (Pydantic + SQLAlchemy) -- **Migrari**: Alembic -- **Database**: SQLite (Faza 1) → Oracle (Faza 2) -- **Frontend**: Vue.js 3 + PrimeVue (consistent cu reports-app) - -## Workflow Bonuri - -``` -1. DRAFT → utilizator completeaza + upload poza -2. PENDING_REVIEW → sistem genereaza note contabile -3. APPROVED/REJECTED → contabil aproba sau respinge -4. SYNCED → (Faza 2) date in Oracle -``` - -## Structura Directoare - -``` -data-entry-app/ -├── backend/ # FastAPI API (port 8003) -│ ├── app/ -│ │ ├── db/ # SQLModel models + CRUD -│ │ ├── schemas/ # Pydantic schemas -│ │ ├── services/ # Business logic -│ │ └── routers/ # API endpoints -│ ├── migrations/ # Alembic migrations -│ └── data/ # SQLite DB + uploads -├── frontend/ # Vue.js UI (port 3010) -│ └── src/ -│ ├── views/ # Page components -│ ├── components/ # Reusable components -│ └── stores/ # Pinia stores -└── docs/ # Documentatie specifica -``` - -## Componente Partajate - -### Backend -- `shared/database/oracle_pool.py` - Conexiune Oracle pentru nomenclatoare si autentificare -- `shared/auth/` - JWT authentication (middleware, routes, service) - -### Frontend -- `shared/frontend/components/LoginView.vue` - Componenta login partajata -- `shared/frontend/stores/auth.js` - Pinia auth store factory -- `shared/frontend/styles/login.css` - Stiluri login - -## Servere Oracle (Producție vs Test) - -**IMPORTANT**: Există două servere Oracle separate. Verifică întotdeauna la care ești conectat! - -| Server | IP Oracle | Tunel SSH | Schema Verificare | Company ID | -|--------|-----------|-----------|-------------------|------------| -| **PRODUCȚIE** | `10.0.20.36` | `./ssh_tunnel.sh` | `ROMFAST` | 114 | -| **TEST** | `10.0.20.121` | `./ssh-tunnel-test.sh` | `MARIUSM_AUTO` | 110 | - -### Scripturi de Pornire - -**IMPORTANT**: Folosește scriptul corespunzător mediului dorit! - -```bash -# Pentru PRODUCȚIE (10.0.20.36) -./start-data-entry-dev.sh # Pornește tot (tunel + backend + frontend) -./start-data-entry-dev.sh stop # Oprește tot -./start-data-entry-dev.sh status # Verifică status - -# Pentru TEST (10.0.20.121) -./start-data-entry-test.sh # Pornește tot (tunel + backend + frontend) -./start-data-entry-test.sh stop # Oprește tot -./start-data-entry-test.sh status # Verifică status -``` - -Scripturile fac automat: -1. Opresc tunelul celuilalt mediu (dacă rulează) -2. Pornesc tunelul corect -3. Copiază `.env.prod` sau `.env.test` în `.env` -4. Rulează migrările pe baza de date corectă -5. Pornesc frontend și backend - -### Fișiere .env - -| Fișier | Server | Database | Schema Test | -|--------|--------|----------|-------------| -| `.env.prod` | PRODUCȚIE (10.0.20.36) | `receipts_prod.db` | ROMFAST (id=114) | -| `.env.test` | TEST (10.0.20.121) | `receipts_test.db` | MARIUSM_AUTO (id=110) | - -### Verificare conexiune - -```bash -# Verifică la ce server ești conectat -./ssh_tunnel.sh status # Dacă rulează = PRODUCȚIE -./ssh-tunnel-test.sh status # Dacă rulează = TEST - -# Verifică schema disponibilă (din backend/) -python3 -c " -import asyncio -from dotenv import load_dotenv -load_dotenv() -import sys; sys.path.insert(0, '../../shared') -from database.oracle_pool import oracle_pool - -async def check(): - await oracle_pool.initialize() - async with oracle_pool.get_connection() as conn: - with conn.cursor() as cur: - cur.execute('SELECT SCHEMA, NUME FROM CONTAFIN_ORACLE.V_NOM_FIRME ORDER BY NUME') - for row in cur.fetchall()[:10]: - print(f'{row[0]}: {row[1]}') -asyncio.run(check()) -" -``` - -### Sincronizare Nomenclatoare - -Query-ul pentru furnizori folosește `CORESP_TIP_PART`: -- `id_tip_part = 17` → Furnizori -- `id_tip_part = 22` → Casa LEI -- `id_tip_part = 23` → Casa Valută -- `id_tip_part = 24` → Bancă LEI -- `id_tip_part = 25` → Bancă Valută - -```sql --- Exemplu query furnizori pentru schema MARIUSM_AUTO (TEST) -SELECT B.ID_PART, B.DENUMIRE, B.COD_FISCAL, B.ADRESA -FROM MARIUSM_AUTO.CORESP_TIP_PART A -INNER JOIN MARIUSM_AUTO.VNOM_PARTENERI B ON A.ID_PART = B.ID_PART -WHERE A.ID_TIP_PART = 17 AND (B.INACTIV = 0 OR B.INACTIV IS NULL) -ORDER BY B.DENUMIRE; -``` - -## Comenzi Dezvoltare - -```bash -# Backend -cd data-entry-app/backend -pip install -r requirements.txt -alembic upgrade head -uvicorn app.main:app --reload --port 8003 - -# Frontend -cd data-entry-app/frontend -npm install -npm run dev -- --port 3010 - -# Migrari -cd data-entry-app/backend -alembic revision --autogenerate -m "description" -alembic upgrade head -``` - -## Tipuri Cheltuieli (hardcoded in Faza 1) - -| Cod | Tip | Cont | TVA | -|-----|-----|------|-----| -| FUEL | Combustibil | 6022 | 19% | -| MATERIALS | Materiale | 6028 | 19% | -| OFFICE | Rechizite | 6024 | 19% | -| PHONE | Telefonie | 626 | 19% | -| PARKING | Parcare | 6022 | 19% | -| FOOD | Alimentatie | 6028 | 0% | -| TRANSPORT | Transport | 624 | 19% | -| OTHER | Altele | 628 | 19% | - -## Integrare Oracle (Faza 2) - -Vezi `docs/PACK_CONTAFIN.pck` pentru procedurile stocate: -- `pack_contafin.init_scriere_act_rul_local()` -- `INSERT INTO ACT_TEMP (...)` -- `pack_contafin.finalizeaza_scriere_act_rul()` - -## API Endpoints Summary - -### Receipts CRUD -- `POST /api/receipts/` - Create -- `GET /api/receipts/` - List (filterable) -- `GET /api/receipts/{id}` - Detail -- `PUT /api/receipts/{id}` - Update (DRAFT only) -- `DELETE /api/receipts/{id}` - Delete (DRAFT only) - -### Workflow -- `POST /api/receipts/{id}/submit` - Send for review -- `POST /api/receipts/{id}/approve` - Approve -- `POST /api/receipts/{id}/reject` - Reject -- `POST /api/receipts/{id}/resubmit` - Resubmit after rejection - -### Attachments -- `POST /api/receipts/{id}/attachments` - Upload -- `GET /api/attachments/{id}/download` - Download -- `DELETE /api/attachments/{id}` - Delete - -### Nomenclatures -- `GET /api/receipts/partners` - Partners from Oracle -- `GET /api/receipts/accounts` - Accounts from Oracle -- `GET /api/receipts/cash-registers` - Cash registers from Oracle -- `GET /api/receipts/expense-types` - Expense types (hardcoded) - -## Testing - -```bash -# Backend tests -cd backend && pytest - -# Frontend tests -cd frontend && npm run test -``` - -## Common Issues - -### Nomenclatoare goale / furnizori lipsă -- Verifică la ce server Oracle ești conectat: `./ssh_tunnel.sh status` -- Verifică dacă schema firmei selectate există pe acel server -- Sincronizează manual: `POST /api/nomenclature/sync/all` - -### Conectat la serverul greșit -- PRODUCȚIE are schema `ROMFAST`, TEST are schema `MARIUSM_AUTO` -- Oprește tunelul curent și pornește cel corect (vezi secțiunea "Servere Oracle") - -### SQLite locked -- Asigura-te ca nu ai multiple procese care acceseaza DB-ul - -### Upload fails -- Verifica permisiuni pe `data/uploads/` -- Verifica MIME type (doar image/*, application/pdf) - -### Migration errors -- `alembic downgrade -1` pentru rollback -- Sterge migration file si regenereaza diff --git a/data-entry-app/backend/.env.example b/data-entry-app/backend/.env.example deleted file mode 100644 index c2f2bfb..0000000 --- a/data-entry-app/backend/.env.example +++ /dev/null @@ -1,26 +0,0 @@ -# SQLite Database -SQLITE_DATABASE_PATH=data/receipts.db - -# File uploads -UPLOAD_PATH=data/uploads -MAX_UPLOAD_SIZE_MB=10 - -# Oracle Database (for nomenclatures - through SSH tunnel) -ORACLE_USER=CONTAFIN_ORACLE -ORACLE_PASSWORD=your_password -ORACLE_HOST=localhost -ORACLE_PORT=1526 -ORACLE_SID=ROA - -# JWT Authentication (shared with reports-app) -JWT_SECRET_KEY=your_secret_key_here -JWT_ALGORITHM=HS256 -JWT_EXPIRE_MINUTES=480 - -# API Settings -API_HOST=0.0.0.0 -API_PORT=8003 -DEBUG=true - -# CORS -CORS_ORIGINS=http://localhost:3010,http://localhost:3000 diff --git a/data-entry-app/backend/alembic.ini b/data-entry-app/backend/alembic.ini deleted file mode 100644 index 4af7b4b..0000000 --- a/data-entry-app/backend/alembic.ini +++ /dev/null @@ -1,106 +0,0 @@ -# Alembic Configuration for Data Entry App - -[alembic] -# path to migration scripts -script_location = migrations - -# template used to generate migration file names -file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -prepend_sys_path = . - -# timezone to use when rendering the date within the migration file -# as well as the filename. -timezone = UTC - -# max length of characters to apply to the "slug" field -truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; This defaults -# to migrations/versions. When using multiple version -# directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" below. -# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions - -# version path separator; As mentioned above, this is the character used to split -# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. -# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. -# Valid values for version_path_separator are: -# -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -version_path_separator = os # Use os.pathsep. Default configuration used for new projects. - -# set to 'true' to search source files recursively -# in each "version_locations" directory -# recursive_version_locations = false - -# the output encoding used when revision files -# are written from script.py.mako -output_encoding = utf-8 - -sqlalchemy.url = sqlite:///data/receipts.db - - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME - -# lint with attempts to fix using "ruff" - use the exec runner, execute a binary -# hooks = ruff -# ruff.type = exec -# ruff.executable = %(here)s/.venv/bin/ruff -# ruff.options = --fix REVISION_SCRIPT_FILENAME - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/data-entry-app/backend/app/__init__.py b/data-entry-app/backend/app/__init__.py deleted file mode 100644 index 2b808a5..0000000 --- a/data-entry-app/backend/app/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Data Entry App - Backend diff --git a/data-entry-app/backend/app/main.py b/data-entry-app/backend/app/main.py deleted file mode 100644 index 19191e1..0000000 --- a/data-entry-app/backend/app/main.py +++ /dev/null @@ -1,173 +0,0 @@ -"""FastAPI application entry point for Data Entry App.""" - -import sys -import logging -import threading -from pathlib import Path -from contextlib import asynccontextmanager - -# Load .env file BEFORE any imports that use os.getenv() -from dotenv import load_dotenv -load_dotenv() - -from fastapi import FastAPI - -# Configure logging to show INFO level messages -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - datefmt='%H:%M:%S' -) -from fastapi.middleware.cors import CORSMiddleware -from fastapi.staticfiles import StaticFiles - -# Add shared modules to path -# Development: data-entry-app/backend/app/main.py -> 4 parents to project root -# Production: data-entry-backend/app/main.py -> 3 parents to roa2web root -def find_shared_path(): - """Find shared folder - works in both dev and production.""" - current = Path(__file__).parent # app/ - - # Try different parent levels to find shared folder - for levels in range(1, 6): - candidate = current - for _ in range(levels): - candidate = candidate.parent - shared_path = candidate / "shared" - if shared_path.exists() and (shared_path / "auth").exists(): - return shared_path - - # Fallback to original logic - return Path(__file__).parent.parent.parent.parent / "shared" - -shared_path = find_shared_path() -sys.path.insert(0, str(shared_path)) - -from app.config import settings -from app.db.database import init_db - -# Import Oracle pool for auth service -from database.oracle_pool import oracle_pool - - -@asynccontextmanager -async def lifespan(app: FastAPI): - """Application lifespan - startup and shutdown events.""" - # Startup - print(f"Starting {settings.app_name} v{settings.app_version}") - - # Initialize Oracle pool (required for authentication) - try: - await oracle_pool.initialize() - print("Oracle pool initialized") - except Exception as e: - print(f"Warning: Oracle pool initialization failed: {e}") - print("Authentication will not work without Oracle connection") - - # Initialize SQLite database - await init_db() - print("Database initialized") - - # Ensure upload directory exists - settings.upload_path_resolved - print(f"Upload path: {settings.upload_path_resolved}") - - # Pre-initialize OCR engine in background (PaddleOCR takes 15-20s) - def init_ocr_background(): - try: - from app.services.ocr_service import ocr_service - ocr_service.ocr_engine._init_paddle_lazy() - print("OCR engine ready") - except Exception as e: - print(f"Warning: OCR engine pre-load failed: {e}") - - print("Starting OCR engine pre-load (background)...") - threading.Thread(target=init_ocr_background, daemon=True).start() - - yield - - # Shutdown - print("Shutting down...") - try: - await oracle_pool.close() - print("Oracle pool closed") - except Exception as e: - print(f"Warning: Oracle pool close failed: {e}") - - -# Create FastAPI app -app = FastAPI( - title=settings.app_name, - version=settings.app_version, - description="API pentru introducere bonuri fiscale cu workflow de aprobare", - lifespan=lifespan, -) - -# CORS middleware -app.add_middleware( - CORSMiddleware, - allow_origins=settings.cors_origins_list, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Authentication middleware -from auth.middleware import AuthenticationMiddleware - -app.add_middleware( - AuthenticationMiddleware, - excluded_paths=["/docs", "/redoc", "/openapi.json", "/health", "/", "/api/auth/login", "/api/auth/refresh"] -) - -# Mount static files for uploads (optional - can serve through nginx in prod) -uploads_path = Path(settings.upload_path) -if uploads_path.exists(): - app.mount("/uploads", StaticFiles(directory=str(uploads_path)), name="uploads") - - -# Health check endpoint -@app.get("/health") -async def health_check(): - """Health check endpoint.""" - return { - "status": "healthy", - "app": settings.app_name, - "version": settings.app_version, - } - - -# Import and include routers -from app.routers import receipts, ocr, nomenclature - -app.include_router(receipts.router, prefix="/api/receipts", tags=["receipts"]) -app.include_router(ocr.router, prefix="/api/ocr", tags=["ocr"]) -app.include_router(nomenclature.router, prefix="/api/nomenclature", tags=["nomenclature"]) - -# Auth router -from auth.routes import create_auth_router - -auth_router = create_auth_router(prefix="") # No prefix - we set it in include_router -app.include_router(auth_router, prefix="/api/auth", tags=["auth"]) - -# Shared routes (companies, calendar) -from routes.companies import create_companies_router -from routes.calendar import create_calendar_router - -companies_router = create_companies_router(oracle_pool) # No cache for data-entry -calendar_router = create_calendar_router(oracle_pool) - -app.include_router(companies_router, prefix="/api/companies", tags=["companies"]) -app.include_router(calendar_router, prefix="/api/calendar", tags=["calendar"]) - - -# Root endpoint -@app.get("/") -async def root(): - """Root endpoint - API information.""" - return { - "name": settings.app_name, - "version": settings.app_version, - "docs": "/docs", - "health": "/health", - } diff --git a/data-entry-app/backend/app/routers/__init__.py b/data-entry-app/backend/app/routers/__init__.py deleted file mode 100644 index 6a8f0e9..0000000 --- a/data-entry-app/backend/app/routers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# API routers -from . import receipts, nomenclature - -__all__ = ["receipts", "nomenclature"] diff --git a/data-entry-app/backend/requirements.txt b/data-entry-app/backend/requirements.txt deleted file mode 100644 index e201548..0000000 --- a/data-entry-app/backend/requirements.txt +++ /dev/null @@ -1,42 +0,0 @@ -# FastAPI -fastapi>=0.109.0 -uvicorn[standard]>=0.27.0 - -# Database - SQLModel + Alembic -sqlmodel>=0.0.14 -sqlalchemy[asyncio]>=2.0.25 -aiosqlite>=0.19.0 -alembic>=1.13.1 - -# Pydantic -pydantic>=2.5.3 -pydantic-settings>=2.1.0 -email-validator>=2.1.0 - -# File handling -python-multipart>=0.0.6 -aiofiles>=23.2.1 -Pillow>=10.2.0 - -# Authentication (shared) -PyJWT>=2.8.0 -python-jose[cryptography]>=3.3.0 - -# Oracle (for nomenclatures) -oracledb>=2.0.1 - -# Utils -python-dotenv>=1.0.0 -httpx>=0.26.0 - -# Testing -pytest>=8.0.0 -pytest-asyncio>=0.23.3 - -# OCR Dependencies -paddleocr>=2.7.0 -paddlepaddle>=2.5.0 -opencv-python>=4.8.0 -pytesseract>=0.3.10 -pdf2image>=1.16.0 -numpy>=1.24.0 diff --git a/data-entry-app/docs/PLAN_DATA_ENTRY_RECEIPTS.md b/data-entry-app/docs/PLAN_DATA_ENTRY_RECEIPTS.md deleted file mode 100644 index c57b85e..0000000 --- a/data-entry-app/docs/PLAN_DATA_ENTRY_RECEIPTS.md +++ /dev/null @@ -1,667 +0,0 @@ -# Plan: Introducere Bonuri Fiscale - Faza 1 (MVP SQLite) - -> **Plan Handover** - Acest document conține planul complet pentru implementare. -> Creat: 2025-12-11 | Status: Ready for implementation - -## Obiectiv -Sistem de introducere bonuri fiscale cu: -- **Upload poze** bonuri de la utilizatori -- **Generare automată** note contabile (staging area) -- **Aprobare de contabil** înainte de finalizare -- SQLite + ORM (SQLModel) + Migrări (Alembic) -- Pregătit pentru integrare Oracle în Faza 2 - ---- - -## Setup Proiect - -### Branch de dezvoltare -```bash -git checkout -b feature/data-entry-receipts -``` - -### Structură Directoare (SEPARAT de reports-app) -``` -. -├── reports-app/ # EXISTENT - Raportări (read-only din Oracle) -│ ├── backend/ -│ ├── frontend/ -│ └── telegram-bot/ -│ -├── data-entry-app/ # NOU - Introduceri date (write în SQLite → Oracle) -│ ├── backend/ # FastAPI pentru introduceri -│ ├── frontend/ # Vue.js pentru introduceri -│ └── docs/ # Documentație și cerințe -│ -├── shared/ # EXISTENT - Componente partajate -│ ├── database/ -│ └── auth/ -│ -└── docs/ # Documentație generală proiect - └── data-entry/ # Documentație specifică data-entry - ├── REQUIREMENTS.md # Cerințe inițiale (acest plan) - └── ARCHITECTURE.md # Decizii arhitecturale -``` - -### Documentație Salvată -La finalizarea planului, se vor crea: -1. `docs/data-entry/REQUIREMENTS.md` - Cerințe funcționale și tehnice -2. `docs/data-entry/ARCHITECTURE.md` - Decizii arhitecturale (ORM, workflow) -3. `data-entry-app/README.md` - Quick start pentru dezvoltare - ---- - -## Workflow Principal - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 1. UTILIZATOR uploadează bon │ -│ ├─ Poză bon fiscal / chitanță │ -│ ├─ Date de bază: sumă, dată, furnizor │ -│ └─ Status: DRAFT │ -└──────────────────────┬──────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 2. SISTEM generează propunere note contabile │ -│ ├─ Debit: Cont cheltuială (6022, 6024, etc.) │ -│ ├─ Credit: Casă (5311) sau Bancă (5121) │ -│ └─ Status: PENDING_REVIEW │ -└──────────────────────┬──────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 3. CONTABIL revizuiește │ -│ ├─ Verifică poza + datele │ -│ ├─ Ajustează conturi dacă e nevoie │ -│ ├─ APROBĂ → Status: APPROVED │ -│ └─ RESPINGE → Status: REJECTED (cu motiv) │ -└──────────────────────┬──────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 4. FAZA 2: Sync în Oracle │ -│ ├─ INSERT ACT_TEMP │ -│ ├─ pack_contafin.finalizeaza_scriere_act_rul() │ -│ └─ Status: SYNCED │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Decizie Tehnică: ORM + Migrări - -### Recomandare: **SQLModel + Alembic** - -**Motivație:** -1. **Creat de autorul FastAPI** (Sebastian Ramirez) - integrare perfectă -2. **Un model = Pydantic + SQLAlchemy** - nu duplici definiții -3. **Async support** nativ -4. **Alembic** - standard industrial pentru migrări -5. **Validare automată** - Pydantic validează input, SQLAlchemy gestionează DB - ---- - -## Arhitectură Propusă - -### Backend Structure (`data-entry-app/backend/`) -``` -data-entry-app/backend/ -├── app/ -│ ├── __init__.py -│ ├── main.py # FastAPI app entry point -│ ├── config.py # Settings & env vars -│ │ -│ ├── db/ # Database layer (SQLModel) -│ │ ├── __init__.py -│ │ ├── database.py # Engine, SessionLocal, init -│ │ ├── models/ -│ │ │ ├── __init__.py -│ │ │ ├── receipt.py # Receipt, ReceiptAttachment -│ │ │ └── accounting_entry.py # AccountingEntry -│ │ └── crud/ -│ │ ├── __init__.py -│ │ ├── receipt.py -│ │ ├── attachment.py -│ │ └── accounting_entry.py -│ │ -│ ├── schemas/ -│ │ └── receipt.py # Request/Response Pydantic schemas -│ │ -│ ├── services/ -│ │ ├── receipt_service.py # Business logic + workflow -│ │ └── nomenclature_service.py # Nomenclatoare din Oracle -│ │ -│ └── routers/ -│ └── receipts.py # API endpoints -│ -├── migrations/ # Alembic migrations -│ ├── env.py -│ ├── alembic.ini -│ └── versions/ -│ └── 001_initial_receipts.py -│ -├── data/ -│ ├── receipts.db # SQLite database -│ └── uploads/ # Poze bonuri -│ -├── requirements.txt -└── README.md -``` - -### Frontend Structure (`data-entry-app/frontend/`) -``` -data-entry-app/frontend/ -├── src/ -│ ├── views/receipts/ -│ │ ├── ReceiptsListView.vue -│ │ ├── ReceiptCreateView.vue -│ │ ├── ReceiptDetailView.vue -│ │ └── ReceiptApprovalView.vue -│ │ -│ ├── components/receipts/ -│ │ ├── ReceiptForm.vue -│ │ ├── ReceiptImageUpload.vue -│ │ └── AccountingEntriesTable.vue -│ │ -│ ├── stores/ -│ │ └── receiptsStore.js -│ │ -│ └── router/ -│ └── index.js -│ -├── package.json -└── vite.config.js -``` - ---- - -## Modele de Date - -### 1. Receipt (Bon Fiscal / Chitanță) - -```python -# app/db/models/receipt.py -from sqlmodel import SQLModel, Field, Relationship -from datetime import datetime, date -from decimal import Decimal -from enum import Enum -from typing import Optional, List - -class ReceiptType(str, Enum): - BON_FISCAL = "bon_fiscal" - CHITANTA = "chitanta" - -class ReceiptDirection(str, Enum): - CHELTUIALA = "cheltuiala" # Plată (bon primit de la furnizor) - INCASARE = "incasare" # Încasare (bon emis către client) - -class ReceiptStatus(str, Enum): - DRAFT = "draft" # Utilizator completează - PENDING_REVIEW = "pending_review" # Așteaptă aprobare contabil - APPROVED = "approved" # Aprobat de contabil - REJECTED = "rejected" # Respins de contabil - SYNCED = "synced" # Sincronizat în Oracle (Faza 2) - -class Receipt(SQLModel, table=True): - """Bon fiscal sau chitanță cu workflow aprobare""" - __tablename__ = "receipts" - - id: Optional[int] = Field(default=None, primary_key=True) - - # Identificare document - receipt_type: ReceiptType - direction: ReceiptDirection - receipt_number: Optional[str] = None - receipt_series: Optional[str] = None - - # Date principale - receipt_date: date - amount: Decimal - description: Optional[str] = None - - # Referințe Oracle (nomenclatoare) - company_id: int - partner_id: Optional[int] = None - partner_name: Optional[str] = None # Cache pentru display - cash_register_id: Optional[int] = None # ID casă/bancă Oracle - cash_register_name: Optional[str] = None # Cache pentru display - - # Workflow - status: ReceiptStatus = Field(default=ReceiptStatus.DRAFT) - created_by: str # Username creator - created_at: datetime = Field(default_factory=datetime.utcnow) - updated_at: datetime = Field(default_factory=datetime.utcnow) - submitted_at: Optional[datetime] = None # Când a fost trimis spre aprobare - - # Aprobare - reviewed_by: Optional[str] = None # Username contabil - reviewed_at: Optional[datetime] = None - rejection_reason: Optional[str] = None # Motiv respingere - - # Faza 2 - Oracle sync - oracle_synced_at: Optional[datetime] = None - oracle_act_id: Optional[int] = None - oracle_error: Optional[str] = None - - # Relații - attachments: List["ReceiptAttachment"] = Relationship(back_populates="receipt") - entries: List["AccountingEntry"] = Relationship(back_populates="receipt") -``` - -### 2. ReceiptAttachment (Poze bonuri - OBLIGATORIU) - -```python -class ReceiptAttachment(SQLModel, table=True): - """Poză sau PDF bon fiscal""" - __tablename__ = "receipt_attachments" - - id: Optional[int] = Field(default=None, primary_key=True) - receipt_id: int = Field(foreign_key="receipts.id") - - filename: str # Nume original fișier - stored_filename: str # Nume pe disk (UUID) - file_path: str # Cale relativă - file_size: int # Bytes - mime_type: str # image/jpeg, application/pdf - uploaded_at: datetime = Field(default_factory=datetime.utcnow) - - receipt: Optional["Receipt"] = Relationship(back_populates="attachments") -``` - -### 3. AccountingEntry (Note Contabile - Staging) - -```python -class EntryType(str, Enum): - DEBIT = "debit" - CREDIT = "credit" - -class AccountingEntry(SQLModel, table=True): - """Notă contabilă propusă pentru bon""" - __tablename__ = "accounting_entries" - - id: Optional[int] = Field(default=None, primary_key=True) - receipt_id: int = Field(foreign_key="receipts.id") - - # Cont - entry_type: EntryType # Debit sau Credit - account_code: str # Ex: 6022, 5311, 4426 - account_name: Optional[str] # Cache: "Cheltuieli combustibil" - - # Valori - amount: Decimal - - # Analitice (opțional) - partner_id: Optional[int] = None - cost_center_id: Optional[int] = None - - # Auto-generat sau modificat manual - is_auto_generated: bool = True - modified_by: Optional[str] = None - modified_at: Optional[datetime] = None - - receipt: Optional["Receipt"] = Relationship(back_populates="entries") -``` - -### Exemplu Note Contabile Generate - -``` -BON FISCAL BENZINĂ - 200 RON: -┌──────────┬────────┬──────────────────────────┬─────────┐ -│ Tip │ Cont │ Denumire │ Sumă │ -├──────────┼────────┼──────────────────────────┼─────────┤ -│ DEBIT │ 6022 │ Cheltuieli combustibil │ 168.07 │ -│ DEBIT │ 4426 │ TVA deductibilă │ 31.93 │ -│ CREDIT │ 5311 │ Casă în lei │ 200.00 │ -└──────────┴────────┴──────────────────────────┴─────────┘ -``` - ---- - -## API Endpoints - -### Bonuri (CRUD + Workflow) -``` -POST /api/receipts/ # Creare bon nou (cu upload poză) -GET /api/receipts/ # Listă bonuri (filtrare, paginare) -GET /api/receipts/{id} # Detalii bon + note contabile -PUT /api/receipts/{id} # Modificare bon (doar DRAFT) -DELETE /api/receipts/{id} # Ștergere bon (doar DRAFT) - -# Workflow -POST /api/receipts/{id}/submit # Trimite spre aprobare (DRAFT → PENDING) -POST /api/receipts/{id}/approve # Aprobă (PENDING → APPROVED) [Contabil] -POST /api/receipts/{id}/reject # Respinge (PENDING → REJECTED) [Contabil] -POST /api/receipts/{id}/resubmit # Re-trimite după corecții (REJECTED → PENDING) -``` - -### Note Contabile -``` -GET /api/receipts/{id}/entries # Liste note contabile propuse -PUT /api/receipts/{id}/entries # Modificare note (contabil ajustează conturi) -POST /api/receipts/{id}/entries/regenerate # Re-generare automată -``` - -### Atașamente -``` -POST /api/receipts/{id}/attachments # Upload poză/PDF -GET /api/receipts/{id}/attachments # Listă atașamente -GET /api/attachments/{id}/download # Download fișier -DELETE /api/attachments/{id} # Ștergere atașament -``` - -### Nomenclatoare (din Oracle - read only) -``` -GET /api/receipts/partners # Furnizori/Clienți pentru dropdown -GET /api/receipts/accounts # Conturi sintetice (6xxx, 7xxx, etc.) -GET /api/receipts/cash-registers # Case și bănci -GET /api/receipts/expense-types # Tipuri cheltuieli predefinite (cu cont asociat) -``` - ---- - -## Frontend Views - -``` -frontend/src/views/receipts/ -├── ReceiptsListView.vue # Listă bonuri cu filtrare pe status -├── ReceiptCreateView.vue # Form creare + upload poză -├── ReceiptDetailView.vue # Vizualizare + editare note contabile -└── ReceiptApprovalView.vue # View pentru contabil (aprobare în masă) -``` - -### ReceiptCreateView - Form utilizator -``` -┌──────────────────────────────────────────────────────┐ -│ UPLOAD POZĂ BON [Drag & Drop sau Click] │ -│ ┌────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ [Previzualizare imagine] │ │ -│ │ │ │ -│ └────────────────────────────────────────────┘ │ -├──────────────────────────────────────────────────────┤ -│ Tip document: ○ Bon fiscal ○ Chitanță │ -│ Direcție: ○ Cheltuială ○ Încasare │ -├──────────────────────────────────────────────────────┤ -│ Data: [DatePicker] │ -│ Suma: [InputNumber] RON │ -│ Furnizor: [Dropdown - din Oracle] │ -│ Tip cheltuială:[Dropdown - Benzină, Materiale...] │ -│ Casă/Bancă: [Dropdown - din Oracle] │ -│ Descriere: [Textarea] │ -├──────────────────────────────────────────────────────┤ -│ [Salvează Draft] [Trimite spre aprobare]│ -└──────────────────────────────────────────────────────┘ -``` - -### ReceiptApprovalView - View Contabil -``` -┌────────────────────────────────────────────────────────────────┐ -│ BONURI DE APROBAT (3) [Aprobă selectate] │ -├────────────────────────────────────────────────────────────────┤ -│ ☑ │ Data │ Furnizor │ Sumă │ Status │ Acțiuni │ -├───┼──────────┼───────────────┼─────────┼─────────┼───────────┤ -│ ☑ │ 10.12.24 │ OMV Petrom │ 200 RON │ PENDING │ [👁️][✓][✗]│ -│ ☑ │ 09.12.24 │ Dedeman │ 450 RON │ PENDING │ [👁️][✓][✗]│ -│ ☐ │ 08.12.24 │ Kaufland │ 85 RON │ PENDING │ [👁️][✓][✗]│ -└────────────────────────────────────────────────────────────────┘ - -[👁️] = Deschide detalii + poză + note contabile editabile -[✓] = Aprobă -[✗] = Respinge (cu motiv) -``` - ---- - -## Pași Implementare - -### Etapa 0: Setup Proiect și Documentație -1. [ ] Creare branch: `git checkout -b feature/data-entry-receipts` -2. [ ] Creare structură directoare: `data-entry-app/{backend,frontend,docs}` -3. [ ] Creare documentație: - - `docs/data-entry/REQUIREMENTS.md` - Cerințe funcționale - - `docs/data-entry/ARCHITECTURE.md` - Decizii tehnice (ORM, workflow) -4. [ ] Creare `data-entry-app/README.md` - Quick start -5. [ ] Creare `data-entry-app/CLAUDE.md` - Instrucțiuni pentru Claude Code - -### Etapa 1: Setup Backend (SQLModel + Alembic) -6. [ ] Creare `data-entry-app/backend/requirements.txt`: - - `fastapi`, `uvicorn`, `sqlmodel`, `alembic`, `python-multipart` - - `aiosqlite`, `pydantic`, `python-dotenv` -7. [ ] Creare `app/main.py` - FastAPI app cu CORS, lifespan -8. [ ] Creare `app/config.py` - Settings (DB path, upload path) -9. [ ] Creare `app/db/database.py` - engine async, session factory -10. [ ] Setup Alembic: `alembic init migrations` - -### Etapa 2: Modele și Migrări -11. [ ] Creare `app/db/models/receipt.py` - Receipt, ReceiptAttachment -12. [ ] Creare `app/db/models/accounting_entry.py` - AccountingEntry -13. [ ] Prima migrare: `001_initial_receipts.py` -14. [ ] Creare folder `data/uploads/` pentru fișiere - -### Etapa 3: Backend CRUD + Upload -15. [ ] Creare `app/db/crud/receipt.py` - operații CRUD -16. [ ] Creare `app/db/crud/attachment.py` - upload/download fișiere -17. [ ] Creare `app/db/crud/accounting_entry.py` - note contabile -18. [ ] Creare `app/schemas/receipt.py` - request/response Pydantic - -### Etapa 4: Business Logic + Workflow -19. [ ] Creare `app/services/receipt_service.py`: - - `create_receipt()` - creare + upload poză - - `generate_accounting_entries()` - generare automată note - - `submit_for_review()` - DRAFT → PENDING - - `approve_receipt()` - PENDING → APPROVED - - `reject_receipt()` - PENDING → REJECTED - -### Etapa 5: API Endpoints -20. [ ] Creare `app/routers/receipts.py` - toate endpoint-urile -21. [ ] Register router în `main.py` -22. [ ] Middleware pentru upload fișiere - -### Etapa 6: Nomenclatoare Oracle -23. [ ] Creare `app/services/nomenclature_service.py`: - - `get_partners()` - furnizori/clienți din Oracle - - `get_expense_accounts()` - conturi 6xxx - - `get_cash_registers()` - case/bănci - - `get_expense_types()` - tipuri cheltuieli predefinite - -### Etapa 7: Frontend Setup -24. [ ] `npm create vite@latest frontend -- --template vue` -25. [ ] Instalare dependențe: `pinia`, `vue-router`, `primevue`, `axios` -26. [ ] Copy configurație PrimeVue din reports-app -27. [ ] Copy CSS shared din reports-app (design tokens, patterns) - -### Etapa 8: Frontend Views -28. [ ] Creare `views/receipts/ReceiptsListView.vue` - listă cu filtre -29. [ ] Creare `views/receipts/ReceiptCreateView.vue` - form + upload -30. [ ] Creare `views/receipts/ReceiptDetailView.vue` - detalii + note -31. [ ] Creare `views/receipts/ReceiptApprovalView.vue` - view contabil -32. [ ] Creare `stores/receiptsStore.js` - Pinia store -33. [ ] Configurare router și layout - -### Etapa 9: Testing & Finalizare -34. [ ] Unit tests pentru CRUD -35. [ ] Integration tests pentru API -36. [ ] Manual testing checklist -37. [ ] Actualizare documentație -38. [ ] Commit și push pe branch -39. [ ] Creare PR către main - ---- - -## Fișiere de Creat - -### Documentație (Etapa 0): -``` -docs/data-entry/ -├── REQUIREMENTS.md # Cerințe funcționale (din acest plan) -└── ARCHITECTURE.md # Decizii tehnice (ORM, workflow) - -data-entry-app/ -├── CLAUDE.md # Instrucțiuni pentru Claude Code -└── README.md # Quick start pentru dezvoltare -``` - -### Conținut `data-entry-app/CLAUDE.md`: -```markdown -# CLAUDE.md - Data Entry App - -## Scop -Aplicație pentru introducere date în ERP (bonuri fiscale, chitanțe) cu workflow de aprobare. - -## Documentație de Referință -- **Cerințe**: `docs/data-entry/REQUIREMENTS.md` -- **Arhitectură**: `docs/data-entry/ARCHITECTURE.md` -- **Quick Start**: `README.md` - -## Decizii Tehnice -- **ORM**: SQLModel (Pydantic + SQLAlchemy) -- **Migrări**: Alembic -- **Database**: SQLite (Faza 1) → Oracle (Faza 2) -- **Frontend**: Vue.js 3 + PrimeVue (consistent cu reports-app) - -## Workflow Bonuri -1. DRAFT → utilizator completează + upload poză -2. PENDING_REVIEW → sistem generează note contabile -3. APPROVED/REJECTED → contabil aprobă sau respinge -4. SYNCED → (Faza 2) date în Oracle - -## Structură Directoare -- `backend/` - FastAPI API (port 8003) -- `frontend/` - Vue.js UI (port 3010) -- `docs/` - Documentație specifică - -## Componente Partajate -- `shared/database/oracle_pool.py` - Conexiune Oracle -- `shared/auth/` - JWT authentication - -## Comenzi Dezvoltare -```bash -# Backend -cd backend && pip install -r requirements.txt -uvicorn app.main:app --reload --port 8003 - -# Frontend -cd frontend && npm install && npm run dev - -# Migrări -cd backend && alembic upgrade head -``` - -## Integrare Oracle (Faza 2) -Vezi `docs/PACK_CONTAFIN.pck` pentru procedurile stocate: -- `pack_contafin.init_scriere_act_rul_local()` -- `INSERT INTO ACT_TEMP (...)` -- `pack_contafin.finalizeaza_scriere_act_rul()` -``` - ---- - -## Tipuri Cheltuieli Predefinite - -Pentru dropdown "Tip cheltuială" - mapare automată la conturi: - -| Tip Cheltuială | Cont Debit | TVA | Descriere | -|----------------|------------|-----|-----------| -| Combustibil | 6022 | 4426 (19%) | Benzină, motorină | -| Materiale consumabile | 6028 | 4426 (19%) | Diverse materiale | -| Rechizite birou | 6024 | 4426 (19%) | Papetărie, toner | -| Telefonie | 626 | 4426 (19%) | Telefon, internet | -| Parcare | 6022 | 4426 (19%) | Taxe parcare | -| Alimentație | 6028 | - | Fără TVA deductibilă | -| Transport | 624 | 4426 (19%) | Taxi, transport | -| Altele | 628 | 4426 (19%) | Alte cheltuieli | - -**Logica generare note:** -```python -def generate_entries(receipt): - expense_type = EXPENSE_TYPES[receipt.expense_type_code] - - entries = [] - - if expense_type.has_vat: - net_amount = receipt.amount / 1.19 - vat_amount = receipt.amount - net_amount - - entries.append(AccountingEntry( - entry_type=EntryType.DEBIT, - account_code=expense_type.account_code, # ex: 6022 - amount=net_amount - )) - entries.append(AccountingEntry( - entry_type=EntryType.DEBIT, - account_code="4426", # TVA deductibilă - amount=vat_amount - )) - else: - entries.append(AccountingEntry( - entry_type=EntryType.DEBIT, - account_code=expense_type.account_code, - amount=receipt.amount - )) - - # Credit - casă sau bancă - entries.append(AccountingEntry( - entry_type=EntryType.CREDIT, - account_code=receipt.cash_register_account, # 5311 sau 5121 - amount=receipt.amount - )) - - return entries -``` - ---- - -## Faza 2 Preview (Oracle Integration) - -După ce Faza 1 funcționează, Faza 2 va adăuga: - -```python -# receipt_service.py - metodă nouă -async def sync_to_oracle(receipt_id: int): - """ - Sincronizează bon APPROVED în Oracle: - - 1. pack_contafin.init_scriere_act_rul_local() - 2. Pentru fiecare AccountingEntry: - INSERT INTO ACT_TEMP ( - ID_ACT, DATAIREG, DATAACT, SCD, ASCD, SCC, ASCC, - SUMA, ID_CTR, ID_PARTD, EXPLICATIA, ... - ) - 3. pack_contafin.finalizeaza_scriere_act_rul() - → SCRIE_IN_ACT() - → SCRIE_IN_RUL() - → Actualizare situații (BV, BP, TVA, etc.) - 4. Update receipt.status = SYNCED, oracle_act_id = ... - """ - pass -``` - ---- - -## Riscuri și Mitigări - -| Risc | Impact | Mitigare | -|------|--------|----------| -| SQLModel e relativ nou | Mediu | Fallback la SQLAlchemy pur dacă e nevoie | -| Upload fișiere mari | Mic | Limit 10MB, compresie imagini | -| Workflow complex | Mediu | Începem cu workflow simplu, adăugăm features gradual | -| Generare note greșite | Mare | Contabilul poate edita înainte de aprobare | - ---- - -## Success Criteria (Faza 1) - -- [ ] Utilizator poate uploada poză bon + date de bază -- [ ] Sistem generează automat note contabile -- [ ] Contabil poate vedea, edita și aproba note -- [ ] Bonurile aprobate sunt vizibile în listă -- [ ] Migrările Alembic funcționează corect -- [ ] Poze bonuri se salvează și se afișează corect - ---- - -## Context Handover - -**Pentru sesiunea următoare:** -1. Citește acest fișier `PLAN_DATA_ENTRY_RECEIPTS.md` -2. Începe cu Etapa 0 - creare branch și structură directoare -3. Referință pentru proceduri Oracle: `docs/PACK_CONTAFIN.pck`, `docs/PACK_FACTURARE.pck` -4. Pattern-uri existente pentru SQLite: `reports-app/telegram-bot/app/db/` diff --git a/data-entry-app/docs/abonament kineterra.pdf b/data-entry-app/docs/abonament kineterra.pdf deleted file mode 100644 index 76fa159..0000000 Binary files a/data-entry-app/docs/abonament kineterra.pdf and /dev/null differ diff --git a/data-entry-app/docs/benzina 14 august.pdf b/data-entry-app/docs/benzina 14 august.pdf deleted file mode 100644 index 9ae5fd8..0000000 Binary files a/data-entry-app/docs/benzina 14 august.pdf and /dev/null differ diff --git a/data-entry-app/docs/benzina 27 octombrie .pdf b/data-entry-app/docs/benzina 27 octombrie .pdf deleted file mode 100644 index 3143458..0000000 Binary files a/data-entry-app/docs/benzina 27 octombrie .pdf and /dev/null differ diff --git a/data-entry-app/docs/igiena 11 octombrie .pdf b/data-entry-app/docs/igiena 11 octombrie .pdf deleted file mode 100644 index ed8e621..0000000 --- a/data-entry-app/docs/igiena 11 octombrie .pdf +++ /dev/null @@ -1,2086 +0,0 @@ -%PDF-1.3 -%âãÏÓ -1 0 obj -<> -endobj -2 0 obj -<> -endobj -3 0 obj -<>/Font<>>>>>/Rotate 0/AF[6 0 R]/Type/Page>> -endobj -4 0 obj<>stream -q 195.5755 0.0000 0.0000 792.0000 0.0000 0.0000 cm /Im0 Do Q q 0.2020 0.0000 0.0000 0.2020 0.0000 0.0000 cm BT -3 Tr -/Ft0 1 Tf --0.035 Tc -17.4086 0 0 40 9.07 3497 Tm -(NIVS: ) Tj -28.5839 0 0 40 60 3497 Tm -(1360760 ) Tj -21.8954 0 0 41.25 9.77 3449 Tm -(OPERATOR:FLORENT ) Tj -20.682 0 0 41.25 227.85 3449 Tm -(INA ) Tj -22.0059 0 0 41.25 278.77 3449 Tm -(CUSMIR ) Tj -13.3033 0 0 48.75 10.06 3400 Tm -(NR ) Tj -24.6151 0 0 48.75 42.88 3400 Tm -(POS: ) Tj -26.7823 0 0 48.75 97.71 3400 Tm -(C3POS-) Tj -24.7714 0 0 48.75 181.74 3400 Tm -(C12 ) Tj -21.9939 0 0 47.5 2.77 3311 Tm -(QLIENT ) Tj -27.2511 0 0 47.5 94.71 3311 Tm -(C.U. ) Tj -45.1441 0 0 47.5 154.67 3311 Tm -(I./ ) Tj -36.9724 0 0 47.5 217.6 3311 Tm -(C.IE. ) Tj -27.5038 0 0 47.5 292.3 3311 Tm -(:RO1879855 ) Tj -21.1264 0 0 43.75 -1.11 3219 Tm -(DETERGENT ) Tj -20.5004 0 0 43.75 135.79 3219 Tm -(GEAMURI ) Tj -16.3336 0 0 43.75 246.77 3219 Tm -(5L ) Tj -23.6223 0 0 43.75 287.89 3219 Tm -(PH ) Tj -21.3702 0 0 43.75 329.9 3219 Tm -(MISAUON ) Tj -27.2716 0 0 50 0.96 3167 Tm -(U79005130 ) Tj -32.6134 0 0 50 136.58 3167 Tm -(6422 ) Tj -27.1727 0 0 50 198.7 3167 Tm -(768027152 ) Tj -22.2987 0 0 41.25 -3.12 3128 Tm -(PROSOP ) Tj -22.7218 0 0 41.25 92.89 3128 Tm -(HARTIE ) Tj -0 Tc -41.25 0 0 41.25 189.74 3128 Tm -(2 ) Tj --0.035 Tc -22.5766 0 0 41.25 217.58 3128 Tm -(STR ) Tj -25.232 0 0 41.25 272.68 3128 Tm -(640G/ROLA ) Tj -20.5545 0 0 41.25 412.9 3128 Tm -(MLI ) Tj -28.1421 0 0 36.25 2.79 3081 Tm -(IZ4441011047 ) Tj -29.5604 0 0 36.25 174.62 3081 Tm -(6422465006429 ) Tj -24.0428 0 0 50 196.89 3634 Tm -(BINE ) Tj -30.2041 0 0 50 269.93 3634 Tm -(ATI ) Tj -22.4532 0 0 50 320.95 3634 Tm -(VENIT ) Tj -20.8894 0 0 50 406.85 3634 Tm -(IN ) Tj -17.926 0 0 50 450.92 3634 Tm -(MAGAZ ) Tj -18.5243 0 0 50 517.87 3634 Tm -(INUL ) Tj -23.1722 0 0 50 580.89 3634 Tm -(BRICK ) Tj -29.905 0 0 40 345.68 3595 Tm -(CIF: ) Tj -26.6236 0 0 40 400.87 3595 Tm -(RO10562600 ) Tj -21.106 0 0 40 9.9 3032 Tm -(ROSOP ) Tj -23.5846 0 0 40 90.88 3032 Tm -(HORTIE ) Tj -0 Tc -40 0 0 40 188.75 3032 Tm -(2 ) Tj --0.035 Tc -23.1555 0 0 40 216.57 3032 Tm -(STR ) Tj -26.3735 0 0 40 273.88 3032 Tm -(4506/ROLA ) Tj -20.8502 0 0 40 413.9 3032 Tm -(ML6 ) Tj -28.4894 0 0 47.5 10.9 2980 Tm -(Z677101186146 ) Tj -29.8907 0 0 47.5 202.62 2980 Tm -(6422465006719 ) Tj -25.7316 0 0 56.25 -0.49 2926 Tm -(SACI ) Tj -21.374 0 0 56.25 64.9 2926 Tm -(MENAJ ) Tj -23.7919 0 0 56.25 147.86 2926 Tm -(EXIRA ) Tj -24.2523 0 0 56.25 232.71 2926 Tm -(351. ) Tj -26.2895 0 0 56.25 296.63 2926 Tm -(50BUC/SET ) Tj -27.6479 0 0 56.25 427.65 2926 Tm -(60 ) Tj -28.0802 0 0 45 10.8 2876 Tm -(IZ1022100021038102 ) Tj -29.7256 0 0 45 260.62 2876 Tm -(6422768020146 ) Tj -27.1352 0 0 57.5 4.5 2822 Tm -(SACI ) Tj -21.374 0 0 57.5 66.9 2822 Tm -(MENAJ ) Tj -26.6117 0 0 57.5 150.68 2822 Tm -(35L ) Tj -24.7164 0 0 57.5 213.1 2822 Tm -(NEGRI ) Tj -23.4361 0 0 57.5 289.75 2822 Tm -(C2 ) Tj -21.4677 0 0 57.5 331.7 2822 Tm -(50BLUC/ROL ) Tj -30.27 0 0 47.5 1.89 2776 Tm -(Z974101075372 ) Tj -29.2302 0 0 47.5 206.63 2776 Tm -(6425385000492 ) Tj -17.0747 0 0 42.5 10.68 2725 Tm -(SOLUT ) Tj -23.5085 0 0 42.5 72.83 2725 Tm -(IE ) Tj -21.2999 0 0 42.5 109.77 2725 Tm -(CURATARE ) Tj -21.9856 0 0 42.5 234.59 2725 Tm -(SPRAY ) Tj -22.3616 0 0 42.5 317.89 2725 Tm -(MOBILA ) Tj -28.713 0 0 42.5 414.66 2725 Tm -(300 ) Tj -26.3394 0 0 43.75 10.96 2675 Tm -(UZ102010108824 ) Tj -32.0192 0 0 43.75 206.59 2675 Tm -(642276802669 ) Tj -18.2461 0 0 52.5 9.66 2625 Tm -(SACI ) Tj -21.0596 0 0 52.5 68.9 2625 Tm -(MENAJ ) Tj -21.4029 0 0 52.5 151.87 2625 Tm -(EXTRA ) Tj -25.9464 0 0 52.5 234.69 2625 Tm -(35L ) Tj -22.2452 0 0 52.5 296.69 2625 Tm -(5OBUC ) Tj -27.7155 0 0 52.5 365.99 2625 Tm -(/SET ) Tj -27.6479 0 0 52.5 428.65 2625 Tm -(60 ) Tj -27.1421 0 0 42.5 9.96 2577 Tm -(UZ1022100021038102 ) Tj -28.3557 0 0 42.5 262.64 2577 Tm -(6422768O20146 ) Tj -18.2461 0 0 45 12.66 2526 Tm -(SACI ) Tj -21.6883 0 0 45 67.9 2526 Tm -(MENAJ ) Tj -25.9464 0 0 45 151.69 2526 Tm -(35L ) Tj -21.9701 0 0 45 207.09 2526 Tm -(NEGRI ) Tj -23.4361 0 0 45 289.75 2526 Tm -(C2 ) Tj -23.9292 0 0 45 331.66 2526 Tm -(50BUC/ROL ) Tj -27.8066 0 0 46.25 10.8 2477 Tm -(IZ974101075372 ) Tj -29.3953 0 0 46.25 206.63 2477 Tm -(6425385000492 ) Tj -0 Tc -55 0 0 55 3.71 2424 Tm -(+ ) Tj --0.035 Tc -22.1574 0 0 55 25.89 2424 Tm -(BEC ) Tj -21.5083 0 0 55 81.88 2424 Tm -(LED ) Tj -20.8861 0 0 55 137.88 2424 Tm -(ECO ) Tj -24.2986 0 0 55 192.95 2424 Tm -(A50 ) Tj -26.1926 0 0 55 248.84 2424 Tm -(E27 ) Tj -23.2279 0 0 55 303.75 2424 Tm -(7N ) Tj -21.2557 0 0 55 345.88 2424 Tm -(LUMINA ) Tj -21.3938 0 0 55 442.9 2424 Tm -(REC ) Tj -28.6299 0 0 46.25 11.83 2377 Tm -(EL13801101103 ) Tj -29.6074 0 0 46.25 206.58 2377 Tm -(5949054916415 ) Tj -23.8603 0 0 46.25 9.88 2327 Tm -(+BEC ) Tj -21.5083 0 0 46.25 81.88 2327 Tm -(LED ) Tj -26.1926 0 0 46.25 137.84 2327 Tm -(E27 ) Tj -22.2026 0 0 46.25 193.22 2327 Tm -(15W ) Tj -28.8879 0 0 46.25 248.99 2327 Tm -(175-250V ) Tj -21.2557 0 0 46.25 373.88 2327 Tm -(LUMINA ) Tj -0 Tc -46.25 0 0 46.25 470.77 2327 Tm -(R ) Tj --0.035 Tc -26.3974 0 0 48.75 -0.17 2280 Tm -(ELO9081109 ) Tj -29.2809 0 0 48.75 151.47 2280 Tm -(8680985523118 ) Tj -24.0125 0 0 42.5 320.89 3775 Tm -(FIVE-HOLDING ) Tj -26.8276 0 0 42.5 497.5 3775 Tm -(S.0. ) Tj -25.8903 0 0 50 246.85 3725 Tm -(JUD. ) Tj -22.8536 0 0 50 307.76 3725 Tm -(CONSIANTA, ) Tj -18.6428 0 0 50 457.91 3725 Tm -(MUN, ) Tj -21.216 0 0 50 525.78 3725 Tm -(CONSTANTA ) Tj -26.4193 0 0 42.5 306.51 3686 Tm -(SIR. ) Tj -20.0911 0 0 42.5 378.85 3686 Tm -(ION ) Tj -20.0019 0 0 42.5 428.9 3686 Tm -(RONTA ) Tj -20.694 0 0 42.5 511.09 3686 Tm -(NR ) Tj -0 Tc -42.5 0 0 42.5 566.49 3686 Tm -(3 ) Tj -47.5 0 0 47.5 9.75 2228 Tm -(+ ) Tj --0.035 Tc -21.617 0 0 47.5 26.9 2228 Tm -(BEC ) Tj -22.046 0 0 47.5 81.87 2228 Tm -(LED ) Tj -24.2986 0 0 47.5 137.95 2228 Tm -(A70 ) Tj -26.1926 0 0 47.5 193.84 2228 Tm -(E27 ) Tj -22.2026 0 0 47.5 249.22 2228 Tm -(18W ) Tj -21.5181 0 0 47.5 304.88 2228 Tm -(LUMINA ) Tj -21.6796 0 0 47.5 401.89 2228 Tm -(RECE ) Tj -0 Tc -47.5 0 0 47.5 470.39 2228 Tm -(6 ) Tj --0.035 Tc -25.5244 0 0 45 9.85 2180 Tm -(EL9001612 ) Tj -29.6074 0 0 45 137.58 2180 Tm -(5849054922850 ) Tj -19.501 0 0 45 10.91 1555 Tm -(BTOTAL ) Tj -0 Tc -47.5 0 0 47.5 9.75 2129 Tm -(+ ) Tj --0.035 Tc -21.617 0 0 47.5 27.9 2129 Tm -(BEC ) Tj -22.046 0 0 47.5 82.87 2129 Tm -(LED ) Tj -20.8861 0 0 47.5 138.88 2129 Tm -(ECO ) Tj -22.0301 0 0 47.5 194.89 2129 Tm -(R5O ) Tj -26.1926 0 0 47.5 249.84 2129 Tm -(E27 ) Tj -27.648 0 0 47.5 305.7 2129 Tm -(74 ) Tj -21.2557 0 0 47.5 346.88 2129 Tm -(LUMINA ) Tj -21.3938 0 0 47.5 442.9 2129 Tm -(REC ) Tj -16.4921 0 0 46.25 9.9 2080 Tm -(EL1 ) Tj -29.532 0 0 46.25 42.65 2080 Tm -(38011001103 ) Tj -29.2766 0 0 46.25 208.58 2080 Tm -(5949054916415 ) Tj -21.2837 0 0 46.25 10.9 2031 Tm -(HARTIE ) Tj -25.351 0 0 46.25 102.82 2031 Tm -(IGIENICA ) Tj -23.8639 0 0 46.25 223.89 2031 Tm -(BRICK ) Tj -0 Tc -46.25 0 0 46.25 306.44 2031 Tm -(3 ) Tj --0.035 Tc -22.582 0 0 46.25 333.58 2031 Tm -(STRATURI ) Tj -28.6635 0 0 45 10.96 1984 Tm -(UZ905335 ) Tj -29.2302 0 0 45 142.63 1984 Tm -(6425667001247 ) Tj -21.5713 0 0 50 8.89 1938 Tm -(HARTIE ) Tj -29.6861 0 0 50 103.78 1938 Tm -(IGIENI ) Tj -19.5283 0 0 50 187.79 1938 Tm -(CA ) Tj -23.8639 0 0 50 222.89 1938 Tm -(BRICK ) Tj -0 Tc -50 0 0 50 305.4 1938 Tm -(3 ) Tj --0.035 Tc -22.7991 0 0 50 332.58 1938 Tm -(STRATURI ) Tj -26.3919 0 0 43.75 -0.05 1898 Tm -(UZ9005335 ) Tj -28.8999 0 0 43.75 134.63 1898 Tm -(6425667001247 ) Tj -21.2837 0 0 45 0.9 1853 Tm -(HARTIE ) Tj -25.5902 0 0 45 94.81 1853 Tm -(IGIENICA ) Tj -23.518 0 0 45 216.89 1853 Tm -(BRICK ) Tj -0 Tc -45 0 0 45 299.46 1853 Tm -(3 ) Tj --0.035 Tc -22.7991 0 0 45 328.58 1853 Tm -(STRATURI ) Tj -30.9941 0 0 41.25 10.89 1805 Tm -(Z900535 ) Tj -29.8907 0 0 41.25 130.62 1805 Tm -(6425667001247 ) Tj -22.181 0 0 45 10.95 1755 Tm -(ARTIE ) Tj -25.8294 0 0 45 89.81 1755 Tm -(IGIENICA ) Tj -24.2098 0 0 45 212.88 1755 Tm -(BRICK ) Tj -0 Tc -45 0 0 45 296.46 1755 Tm -(3 ) Tj --0.035 Tc -23.2334 0 0 45 325.57 1755 Tm -(STRATURI ) Tj -25.9555 0 0 42.5 10.91 1707 Tm -(Z9005335 ) Tj -29.5604 0 0 42.5 125.62 1707 Tm -(6425667001247 ) Tj -26.5189 0 0 45 10.87 1656 Tm -(RTIE ) Tj -26.0685 0 0 45 85.81 1656 Tm -(IGIENICA ) Tj -24.5556 0 0 45 209.88 1656 Tm -(BRICK ) Tj -0 Tc -45 0 0 45 294.46 1656 Tm -(3 ) Tj --0.035 Tc -23.4505 0 0 45 322.57 1656 Tm -(STRATURI ) Tj -29.6537 0 0 45 10.81 1607 Tm -(2905335 ) Tj -30.3861 0 0 45 122.61 1607 Tm -(6425667001247 ) Tj -23.8736 0 0 100 11.83 1404 Tm -(IA ) Tj -20.4648 0 0 100 60.88 1404 Tm -(LE ) Tj -20.081 0 0 38.75 -0.13 1256 Tm -(EST ) Tj -18.5277 0 0 41.25 9.81 1160 Tm -(OTAL ) Tj -21.3464 0 0 41.25 71.81 1160 Tm -(TUA ) Tj -0 Tc -41.25 0 0 41.25 127.91 1160 Tm -(A ) Tj -41.25 0 0 41.25 156.5 1160 Tm -(-) Tj --0.035 Tc -23.5079 0 0 41.25 183.85 1160 Tm -(21% ) Tj -23.6523 0 0 38.75 -2.25 1120 Tm -(OTAL ) Tj -20.3299 0 0 38.75 74.82 1120 Tm -(TUA ) Tj -20.3131 0 0 38.75 129.9 1120 Tm -(BON ) Tj -14.0302 0 0 33.75 9.06 1079 Tm -(NR. ) Tj -24.2857 0 0 33.75 51.88 1079 Tm -(POZ. ) Tj -22.9273 0 0 33.75 120.95 1079 Tm -(ART. ) Tj -23.8736 0 0 33.75 193.83 1079 Tm -(IN ) Tj -22.5624 0 0 33.75 231.89 1079 Tm -(BON: ) Tj -29.6324 0 0 40 5.89 508 Tm -(Z:0146 ) Tj -27.8509 0 0 40 101.87 508 Tm -(BF:0171 ) Tj -21.8841 0 0 43.75 11.84 454 Tm -(ID ) Tj -24.8686 0 0 43.75 48.88 454 Tm -(BF: ) Tj -31.0124 0 0 36.25 7.42 362 Tm -(S/N:D8470001107 ) Tj -23.8351 0 0 43.75 10.75 307 Tm -(CASIER ) Tj -31.3154 0 0 43.75 105.9 307 Tm -(1: ) Tj -27.937 0 0 38.75 322.7 759 Tm -(C3POS-CT2:1360760 ) Tj -0 Tc -52.5 0 0 52.5 594.16 3168 Tm -(1 ) Tj --0.035 Tc -20.3832 0 0 52.5 609.9 3168 Tm -(BUC ) Tj -0 Tc -52.5 0 0 52.5 665.89 3168 Tm -(X ) Tj --0.035 Tc -27.5601 0 0 52.5 693.83 3168 Tm -(27. ) Tj -25.8839 0 0 52.5 734.7 3168 Tm -(93= ) Tj -32.5188 0 0 52.5 790.8 3168 Tm -(27.93 ) Tj -0 Tc -52.5 0 0 52.5 873.88 3168 Tm -(A ) Tj -52.5 0 0 52.5 578.16 3076 Tm -(1 ) Tj --0.035 Tc -20.2856 0 0 52.5 595.9 3076 Tm -(ROLA ) Tj -0 Tc -52.5 0 0 52.5 666.89 3076 Tm -(X ) Tj --0.035 Tc -27.0613 0 0 52.5 695.05 3076 Tm -(13. ) Tj -22.26 0 0 52.5 741.76 3076 Tm -(00 ) Tj -31.3942 0 0 52.5 792.9 3076 Tm -(13.00 ) Tj -0 Tc -52.5 0 0 52.5 874.88 3076 Tm -(A ) Tj -58.75 0 0 58.75 618.94 2970 Tm -(1 ) Tj --0.035 Tc -20.8928 0 0 58.75 637.9 2970 Tm -(BUC ) Tj -0 Tc -58.75 0 0 58.75 693.88 2970 Tm -(X ) Tj --0.035 Tc -27.5324 0 0 58.75 721.68 2970 Tm -(9. ) Tj -30.6923 0 0 58.75 749.64 2970 Tm -(96-) Tj -33.3314 0 0 58.75 805.61 2970 Tm -(9,95 ) Tj -0 Tc -58.75 0 0 58.75 874.87 2970 Tm -(A ) Tj --0.035 Tc -19.8177 0 0 36.25 381.91 279 Tm -(BON ) Tj -23.0709 0 0 36.25 435.89 279 Tm -(FISCAL ) Tj -28.9694 0 0 36.25 378.66 231 Tm -(9000221498 ) Tj -0 Tc -55 0 0 55 621.07 2872 Tm -(1 ) Tj --0.035 Tc -20.8928 0 0 55 636.9 2872 Tm -(BUC ) Tj -0 Tc -55 0 0 55 692.88 2872 Tm -(X ) Tj --0.035 Tc -27.9084 0 0 55 720.6 2872 Tm -(5. ) Tj -29.6546 0 0 55 748.87 2872 Tm -(49-) Tj -27.9084 0 0 55 803.6 2872 Tm -(5, ) Tj -27.3771 0 0 55 831.88 2872 Tm -(49 ) Tj -0 Tc -55 0 0 55 873.88 2872 Tm -(A ) Tj -48.75 0 0 48.75 606.29 2778 Tm -(1 ) Tj --0.035 Tc -21.0511 0 0 48.75 620.9 2778 Tm -(ROLA ) Tj -0 Tc -48.75 0 0 48.75 689.9 2778 Tm -(X ) Tj --0.035 Tc -40.3527 0 0 48.75 717.43 2778 Tm -(5.81 ) Tj -35.3862 0 0 48.75 805.5 2778 Tm -(5.81 ) Tj -0 Tc -48.75 0 0 48.75 870.89 2778 Tm -(A ) Tj -47.5 0 0 47.5 590.34 2679 Tm -(1 ) Tj --0.035 Tc -20.3832 0 0 47.5 606.9 2679 Tm -(BUC ) Tj -0 Tc -47.5 0 0 47.5 662.9 2679 Tm -(X ) Tj --0.035 Tc -27.9633 0 0 47.5 691.02 2679 Tm -(10, ) Tj -24.3622 0 0 47.5 731.89 2679 Tm -(49= ) Tj -32.3752 0 0 47.5 786.87 2679 Tm -(10.49 ) Tj -0 Tc -47.5 0 0 47.5 869.89 2679 Tm -(A ) Tj -51.25 0 0 51.25 622.2 2576 Tm -(1 ) Tj --0.035 Tc -19.8736 0 0 51.25 634.91 2576 Tm -(BUC ) Tj -0 Tc -51.25 0 0 51.25 689.89 2576 Tm -(X ) Tj --0.035 Tc -35.0821 0 0 51.25 716.5 2576 Tm -(5.49: ) Tj -33.5238 0 0 51.25 798.52 2576 Tm -(5.49 ) Tj -0 Tc -51.25 0 0 51.25 868.88 2576 Tm -(A ) Tj -48.75 0 0 48.75 608.29 2481 Tm -(1 ) Tj --0.035 Tc -20.6683 0 0 48.75 620.9 2481 Tm -(ROLA ) Tj -0 Tc -48.75 0 0 48.75 688.9 2481 Tm -(X ) Tj --0.035 Tc -27.9084 0 0 48.75 716.6 2481 Tm -(5. ) Tj -26.065 0 0 48.75 743.53 2481 Tm -(81= ) Tj -31.6613 0 0 48.75 797.55 2481 Tm -(5,81 ) Tj -0 Tc -48.75 0 0 48.75 866.89 2481 Tm -(A ) Tj -46.25 0 0 46.25 618.38 2380 Tm -(1 ) Tj --0.035 Tc -20.3832 0 0 46.25 633.9 2380 Tm -(BUC ) Tj -0 Tc -46.25 0 0 46.25 688.9 2380 Tm -(X ) Tj --0.035 Tc -35.0821 0 0 46.25 715.5 2380 Tm -(5.54: ) Tj -34.1446 0 0 46.25 797.51 2380 Tm -(5.54 ) Tj -0 Tc -46.25 0 0 46.25 866.9 2380 Tm -(A ) Tj -51.25 0 0 51.25 622.2 2279 Tm -(1 ) Tj --0.035 Tc -19.8736 0 0 51.25 633.91 2279 Tm -(BUC ) Tj -0 Tc -51.25 0 0 51.25 687.89 2279 Tm -(X ) Tj --0.035 Tc -18.838 0 0 51.25 721.78 2279 Tm -(9. ) Tj -26.7509 0 0 51.25 745.52 2279 Tm -(80= ) Tj -33.3314 0 0 51.25 796.61 2279 Tm -(9.80 ) Tj -0 Tc -51.25 0 0 51.25 865.88 2279 Tm -(A ) Tj -50 0 0 50 592.25 2175 Tm -(1 ) Tj --0.035 Tc -20.3832 0 0 50 607.9 2175 Tm -(BUC ) Tj -0 Tc -50 0 0 50 662.9 2175 Tm -(X ) Tj --0.035 Tc -30.3905 0 0 50 688.94 2175 Tm -(13.61= ) Tj -30.9036 0 0 50 783.92 2175 Tm -(13.61 ) Tj -0 Tc -50 0 0 50 867.89 2175 Tm -(A ) Tj -51.25 0 0 51.25 616.2 2078 Tm -(1 ) Tj --0.035 Tc -20.3832 0 0 51.25 632.9 2078 Tm -(BUC ) Tj -0 Tc -51.25 0 0 51.25 687.89 2078 Tm -(X ) Tj --0.035 Tc -29.9234 0 0 51.25 714.58 2078 Tm -(5.54+ ) Tj -32.9029 0 0 51.25 796.53 2078 Tm -(5,54 ) Tj -0 Tc -51.25 0 0 51.25 865.88 2078 Tm -(A ) Tj -51.25 0 0 51.25 594.2 1978 Tm -(1 ) Tj --0.035 Tc -20.3832 0 0 51.25 606.9 1978 Tm -(BUC ) Tj -0 Tc -51.25 0 0 51.25 661.89 1978 Tm -(X ) Tj --0.035 Tc -30.3905 0 0 51.25 688.94 1978 Tm -(13.54= ) Tj -32.8658 0 0 51.25 783.85 1978 Tm -(13.54 ) Tj -0 Tc -51.25 0 0 51.25 866.88 1978 Tm -(A ) Tj -45 0 0 45 589.42 1894 Tm -(1 ) Tj --0.035 Tc -20.3832 0 0 45 606.9 1894 Tm -(BUC ) Tj -0 Tc -45 0 0 45 661.91 1894 Tm -(X ) Tj --0.035 Tc -30.7801 0 0 45 688.92 1894 Tm -(13.54= ) Tj -32.8658 0 0 45 786.85 1894 Tm -(13.54 ) Tj -0 Tc -45 0 0 45 869.9 1894 Tm -(A ) Tj -45 0 0 45 589.42 1804 Tm -(1 ) Tj --0.035 Tc -20.3832 0 0 45 606.9 1804 Tm -(BUC ) Tj -0 Tc -45 0 0 45 662.91 1804 Tm -(X ) Tj --0.035 Tc -31.1697 0 0 45 689.91 1804 Tm -(13.54= ) Tj -33.8468 0 0 45 787.82 1804 Tm -(13.54 ) Tj -0 Tc -45 0 0 45 872.9 1804 Tm -(A ) Tj --0.035 Tc -22.3236 0 0 48.75 3.89 663 Tm -(PRETUL ) Tj -22.7923 0 0 48.75 100.95 663 Tm -(ARTICOLELOR ) Tj -20.1525 0 0 48.75 266.9 663 Tm -(MARCATE ) Tj -19.5283 0 0 48.75 377.79 663 Tm -(CU ) Tj -21.4767 0 0 48.75 450.84 663 Tm -(INCLUD ) Tj -23.3562 0 0 48.75 541.79 663 Tm -(TAKA ) Tj -19.4762 0 0 48.75 617.95 663 Tm -(VERDE ) Tj -19.9516 0 0 48.75 4.79 614 Tm -(CONFORM ) Tj -23.3952 0 0 48.75 120.95 614 Tm -(ART. ) Tj -26.9368 0 0 48.75 178.68 614 Tm -(34 ) Tj -23.6624 0 0 48.75 211.88 614 Tm -(DIN ) Tj -18.8065 0 0 48.75 267.81 614 Tm -(OUG ) Tj -35.6967 0 0 48.75 322.82 614 Tm -(R.5/2015 ) Tj -20.8894 0 0 48.75 465.85 614 Tm -(IN ) Tj -20.4065 0 0 48.75 502.95 614 Tm -(VALOARE ) Tj -20.3055 0 0 48.75 611.9 614 Tm -(DE ) Tj -31.9784 0 0 48.75 653.8 614 Tm -(2.45 ) Tj -0 Tc -52.5 0 0 52.5 588.16 1704 Tm -(1 ) Tj --0.035 Tc -20.8928 0 0 52.5 605.9 1704 Tm -(BUC ) Tj -0 Tc -52.5 0 0 52.5 662.89 1704 Tm -(X ) Tj --0.035 Tc -35.0938 0 0 52.5 689.77 1704 Tm -(13.54-) Tj -33.8468 0 0 52.5 789.82 1704 Tm -(13.54 ) Tj -0 Tc -52.5 0 0 52.5 875.88 1704 Tm -(A ) Tj -57.5 0 0 57.5 589.98 1601 Tm -(1 ) Tj --0.035 Tc -20.8928 0 0 57.5 605.9 1601 Tm -(BUC ) Tj -0 Tc -57.5 0 0 57.5 662.88 1601 Tm -(X ) Tj --0.035 Tc -30.6694 0 0 57.5 690.93 1601 Tm -(13. ) Tj -31.7383 0 0 57.5 734.55 1601 Tm -(54-) Tj -34.3374 0 0 57.5 790.8 1601 Tm -(13.54 ) Tj -0 Tc -57.5 0 0 57.5 877.87 1601 Tm -(A ) Tj --0.035 Tc -21.4056 0 0 41.25 242.9 422 Tm -(DATA: ) Tj -31.7787 0 0 41.25 324.89 422 Tm -(11-10-2025 ) Tj -21.6838 0 0 41.25 476.78 422 Tm -(ORA: ) Tj -31.5049 0 0 41.25 545.9 422 Tm -(12:51:01 ) Tj -33.9512 0 0 45 805.81 1555 Tm -(186.16 ) Tj -15.6712 0 0 45 210.98 181 Tm -(UA ) Tj -21.5162 0 0 45 245.89 181 Tm -(RUGAM ) Tj -20.6877 0 0 45 327.62 181 Tm -(SA ) Tj -25.0296 0 0 45 373.88 181 Tm -(PASTRATI ) Tj -20.1195 0 0 45 492.9 181 Tm -(BONUL ) Tj -35.5044 0 0 45 573.83 181 Tm -(FISCAL! ) Tj -20.3315 0 0 43.75 162.95 132 Tm -(VOCEA ) Tj -25.3923 0 0 43.75 244.73 132 Tm -(CLIENTULU|: ) Tj -28.2245 0 0 43.75 410.7 132 Tm -(08008 ) Tj -24.2548 0 0 43.75 492.88 132 Tm -(BRICK: ) Tj -28.9489 0 0 43.75 588.69 132 Tm -(0800827425 ) Tj -25.808 0 0 47.5 178.77 82 Tm -(TIPARIT ) Tj -23.6624 0 0 47.5 286.88 82 Tm -(DIN ) Tj -22.6358 0 0 47.5 341.76 82 Tm -(COGITO ) Tj -21.7249 0 0 47.5 438.87 82 Tm -(ERP ) Tj -22.3374 0 0 47.5 493.92 82 Tm -(WW. ) Tj -20.7218 0 0 47.5 547.78 82 Tm -(COGI ) Tj -24.7121 0 0 47.5 603.78 82 Tm -(TO-ERP. ) Tj -18.5089 0 0 47.5 698.91 82 Tm -(RO ) Tj -34.3506 0 0 42.5 807.8 1300 Tm -(186.16 ) Tj -34.4956 0 0 41.25 836.63 1251 Tm -(0.00 ) Tj -30.3893 0 0 37.5 821.64 1159 Tm -(32.31 ) Tj -29.907 0 0 35 820.64 1117 Tm -(32.31 ) Tj -29.0126 0 0 52.5 450.66 463 Tm -(90002214982025101112510101460171 ) Tj -24.6196 0 0 30 863.14 1080 Tm -(17 ) Tj -19.7698 0 0 42.5 725.08 516 Tm -(NR. ) Tj -22.618 0 0 42.5 765.95 516 Tm -(ANEF: ) Tj -25.0882 0 0 42.5 835.73 516 Tm -(0001 ) Tj -26.5139 0 0 55 741.76 363 Tm -(TD:00029241 ) Tj -23.8351 0 0 43.75 781.75 319 Tm -(CASIER ) Tj -0 Tc -43.75 0 0 43.75 878.47 319 Tm -(1 ) Tj -ET - Q -endstream -endobj -5 0 obj -<>stream -JFIF  - -   ++&.%#%.&D5//5DNB>BN_UU_wqw  - -   ++&.%#%.&D5//5DNB>BN_UU_wqwP"1 (l@8@&n.e&S0R@@&Ra @38N`#@`&ȃ"N4441b&ԲDd@"biĀE"U8ܵ &2#F,I -`BRTQi $10)(qBh b`$H@!)*8@LӠj` r&ЁPf͠粨]vN:Èh llJ4CB`& -IDBj:L$ITc04@*I@2`& (l@" LbA -i(Hhd6@dY&j44K H Di&n#IH%@*i,TD` 2Qp!(h@MiD0lp bY!<dZ*dg*i{OP_B -]Xb 4 0HE(8j S!0  @FB$8@dR4, L%$# dIC4 1Ȕ*Lch+vjƾU-z$ 0bQŃi B-0 @@Q`6Dh ` C"ڠ BpІb h000pLW%؍}#c5  R@ -L@ %$ iSp8F 4M@!)DI: a89D&%$`Fi+&c_[12 L J#d!I& ` 4k=iWzOжkh Ӷ}k$(v=K.ƥaCwnUפao5[X7١\EUmŒ p,(H%(`@&FI,$@) `&PhͰ>L10dI"#)h4) 9DV(j_07ޒm u{0|69&}n+vTMn矠@!c N"&0m@Ii4ԉ! 4mM~57#_T1 -I LA1i(h$! - R(@Bpi7h4$쁮Ad!VGM)zɔa{DR#dP'C>?Q p9P@ ) @!3XKX#$4!cC -Iuv4pclje]v˫)bK3W$뜮zu%4{WJ-TLLJ}Vok.kȩy+vX.etD'5!J2LL&V0P4 0L e1RHT3]elߤVbG44(׏3鱵4`mؒ+'Rz[6dY=hW=ޣNifY}MK:Y-M6VWCA2ᱎmsUψcbAw4+c Hf b` - P h!c48@ъP1 -QC7 0hA=LrGp^^r}^ͳ^(iM O쭲W0*hP%g)!ZlE" L<BF`CL0\zZ4!&$q $0bGiڬ]v(so,o[;j_tUO]I>jxH-rlL]K=΃ѣd,jP"T  044H@4LQ8.Ap#(`hbV)`PFE![Y5r3gsUJKYt:eMjvl7OғǡI.*Ysqw.CꥴEcCklI{n^Xy4Ԛg'HFIC=#tT|'m/J)|6͞\MmS^Tn -edk:֯gNtɺ@HNV`&Ӏ hLLa1.SBDmSC$ " 3pMŲZF<>_z8˿Ǭk+xcTK߅[ =[}iӝM!8Lihm8D "1Z|d ˄Hc@&'@`H)$'hiiZ -kNJj/0kf_aԨy'֧z(?Du{]n.!!`(@`bFIB!0!0 L(ohb$$4, -`X6 궮;Mݴ;VLej6*2l[ݑQ̱[]B+^6-;p ` $0@ L0*4,jh@$H!!dd]8LIBRn,D1%(8'9bD=r̐_RA=%BD$0I#!a GWx#6]>:N5tuq,~5-4Իc.>Z -tuѣWD/o -'N3`Z.^a"$*``M 1 `Q` R!`b-c +6`Fv/|l9e`^kuwM֦+2z_̴H{W(C+v |v9tl񥞷e"=X#8`0ĉ B`q 4``& I1`e@ CBa@ -Iy[\s7jyZU5U{)xYۂpUQ˲8(N5J)6hwVl1i7;S=c٤U,(o(4 l`h+N( BPLA4@LLJ2! bb40D-bsbR6aì{ү^l!jkbKKnҏuSV? -mi$ F@ h04b@LI4 ĥ B@M0&5@e&hmMXOGuz- -6VI=⃸6܏o5};:B*mŗG|q `Q!@$ @0M(  h6@1`KO&[X 5>;EܔVlznh16r+ѱvч]3QE_i`M I1c@$P,&RX1H@` L"LD0'C@!Ӛ*ʼs8Pr2xČojy "yՑ&yzeA=pVyG$5dDZ)BU-NeafK]5iąslF[0Yc -̳VxHR=e8Q>I?,K6f:"#/Q44 RJj0jK[c$ -5D 2*ۍ[qޅFd L[-AFlFz/FS=F6K>A ce$yb1=y{) -1Zb2!1A)be4ynr{GNq|sa(AALc%S L( LC@1LQcqk&@T@Em&$Mx,S,D+%q3.y9q1,|9˿sM]q,{:!dD @&4 ZIJ3Y4'&Ӕa -0 s@kR] Ss-ASzn\d4i٩`&&&JVЪ -x76LUֿ}ʵԎMF^gju< -L(?^vӑui?;&cQOާ(B. EI -$@Ć I!:=!)e(ZDVhbjC@ $I1 CcYf+{k} z+:28ivJj,C4Y-p}YB,ʎ΀;ʫzS _8bzPSf `HI`D0b#WRqc“@?΁KKɺKcۑʞ]<^RgߕTr^S? -Et)ǑU6EBzzTGA4J5ܮYio}naxYc\r ^G1ecGyVgvZ 6?6uf -ւ4na5S>:f*LBd!I  `(ށ8N^fټ>e5{{.T;7Yh=wEםu.]]B*9RZVF_Oyu*dmzVc:O8_n̺rz6Jd[rI@07UЧT]gTcoe>*uUOkMs[:=#7d]*լv] (^x}/W,ctӺ@rQ"LCQV@5(2mD.mUsmFMg_uǕ3묎 c/2t{9cXFJGpYe;W,;~%(R2Cz -lu%s=muYeƛTg\12V)>[nU:W5 z(>/q]ΕLf:g80DHI`!!i.Q8S &`I5A@ÙL]뻨]vo;=IƽMzsΣO7l/'HUet{ZӬ֍G,xNAzsr5]CI^V= -Mi7̻qW\xWk/7T2t.q!@7- X&dLdh#(d$ #0@ M(0Av5N˲ȣd\]3rjUUR7m9(ʕN -wV,Z2:uutybaDN{gL[EJY:kCȤ^y=O9~L~,~Ay}2W=%^ODef,1mDw=hֵ5l)9xcm]԰eeQmg4٧Dދ0|yu).9ζOn_t#%t C+!}cGG==׊2%2:\FA&*OeR,F>w%29ʽ/ *|*H.Q=RI %cN{d,OJh6f!G3 `2]iK-+gm$l:!0qS*6ZMMΗk_j[#DUߚt|GaP3!bV‡`0 NG"ƽVrwP8gS#!!q 4Ɔ% CCbN,|[d.y.{?R[#:nɥ5ݨʥ6=+;Zl2p_k~/׺h"2?P')+Ut\tZ8y?}sؔuki8"] =ow`=yE(,͑4޻eU.s~AIt|i/{5yrbili]qYߵ$"Cb4 R@4ƀMZj -Zſ4^ Nܾgە]j7kK5kt{}l5j4{OU[h7iUT%Q/Dm0ZXSTBxZO.ծlX*%S׊{ł5sts YJ=^>Ω^[TiʋgEUu|u GX6>Y\7>ldYwd$@S! @!b!"B`D"5*!LrrH -ZEļޔzhr|j'D E]N(kCԞ;,=i{/D^9rvMQ-UkuIc諗X).TNiw>~r{T5 -OC_\8C_xts L`%  ),F`q24 -@ DdfIg16R$Agļg;aE}[(-v6Ĭ+Ʋ/A[Nu|&Ff0噶lspv/)L|}%.Vyc<͆rë>m^tNw`=sy*&r_ʀl$%$ iFI"0CdTD%0RA)T}ې$&a8pX>Ql XFyHX^I1c FJ=O8bxŬz{G&A&G^C!0R"*H*H`hO!"qL#" 'Ke!=:&)R'!c(˽|wZR.iy`+&M=E,Fxbzֱ6L3;Qйzmj";]~RC/~ċRoi'W&cћIk<0™9L0)*h`&1)ȩI" A=vlj5V֣7Qs=A`Q1lsg(j~^ -ߑ[S$W,6Woɒ6Fh×aNTW?mK'<څȨf]ɺ=7X^%֧j.exYiL,>ǰi v,Zgkjѕ^6T0hǖTkAn&ʱei覆z -zsA1 -Lb"I=dm(9cE֩m.UhQ(mh:}dm=7ZmR4afYЛlK(<5^T@ kL\!>91|  q1fcbl= IyDc3#ǟxm~>֙+\C;d#q#_ bezym -Ֆ,0ІR IldS%o87.ҷb+v[0F~1iZ9Vf 8ֻNf̔6ZẸdd ku*Ek^wLno놛3u!YB#UM>5d&Nmc[ѡ8Ʈ٬B9ټ3I:W?[m V/$"Ir[h![dI! aN*D0R6H=bJd'!Phk=E[ - vςxYQ GΉBmՕ=E^? lZb٪4o]46s:KwMNL-58J(#_9^n{r`^~& 1` -ACh)q?o?I}%b``!'ʹGM>i0Eo,s] ~}rpĪjz)96=n4rߴ]E@sK^E"U5 -1mF&U-Ih#Q - [m=Gegkx8=rIt:mה_S#'\sK[ -^ەͬyPӧaU=o+D:O5LG8X {m&@QH14aeYz7,a_'ˎ.OF/'iRl[.u{O5X n狕Rϣ::]ӘkO3K9λ3E[L кm~{k4d`l3#~~'nT;V.4Zo{*o2]#jKOz+6e^xDƸL]ά\h]/oM{v݀2L(4nrt}v\P8BP:w(2G+꫇z&N#]r+'K#FglzɗΏ]Gb¡9D㽋W X"1Ԋat/U+IWh\ηﶾ#=L*GEB)ٖ {} -6xK:篤Cr$9V{B@h0J2D %T+zĢWz9WSR/9K_l$LxwLs9Mg?YILBʥ+ͮPX 64< h}yE^im7TL*Z#K\}h3}yv/UMyyQTuԽ9xwsqJ-q:JkNSҝ5TrT[":$y:cRZ%Lם#[".q-~&"Q& 441 --*@$0E1,8=a0`'g[x*( -lP/sٰ[)ƙofiO-Hl6IkMkW2tS+um!UdLvdS.Tct~d_#7,٪6c᝖Ftf^tꙺ|˦2-0&!8ib$?O/RM%OYCT41@ 5ÑjvC\l._<{Yھ9Ős Х+wLtP.tgs9mf=/㜾ղo+R^Oݰ>gák&c+n3HtDJ'K Fqa"& zb1"sͩB&EpGI/9"9@,~Z]M\'m%U[)s]w7>-jl曂{ʧ^NE3'˯F5}b8KjZC:)'K/EdYlEًv;^tHU?0`0JHCю4& ̡1l+hL]gͺ)(V xi -цhAD<)^r=M^]W}zOr1STc5 ]Gsuge^gvnM]?rd^Hjv'> -7}ApsG9Sʵ6fW`]uZ2  4 44|!99FѶOLR־ӦTRV|&ͺRܢ>Rgp -/G Wu)c -Mη m\Z4/Q*,d(1DD=eU|^ʏt P7yR䷹UID[Ťh0iUkjG@*\X-y*ޕKXȺ`4!R0L@*"$ ?_?D3_IyHj4  + JF6zϯ˟ɹb.8ɦX˾ʭ!^]U29 W?ضߡf=cxl$Y6t2csϬ{aoͷ6뱢ft[I:b @ LAhpV& =}<"$Щj iZ YV_+[\ꕢ^Y9&# 0=AKy[|1 qPmіÏl8Wl%hh & S "bm=LM46/5[BFc G_[5dx{x,S(3&\e dc9b~>fYE^G$/0 M2@C^GCy$-/vDe^D6 hh&10!D< M&z`g{xT)$^jFm}6f^FGnu蜏n0"IkL\ͧ\O;yƻe;{@̫k*^-5+qήzf"Ӥ#L `hIĐR$m-@zI6@?XJYb&82@F6L hYGVce}6I~xg<[A Wm$o߹'_5nSZ> 7פr{7UN4 # #$Iq1M0M4T -Pb`I`&P:^m|qgRͽNWߠͬ5 )P{==q̬)j-adoYikŻO&/UKmDs떐A%kyɻm/?Kװ Sє@[Fi+v~dahln -&Hh"%S@69FH~^2H4 4 9)甪ualQ0ui~ew's"e -!*}, hkk7C#Me' -2yTo2؁&g薇tȐDN0ݬse{Ђh -3 Mtaf46@DLm1i\%F$1Y7#ZzxK4А9 !HL WB[k2z%Ƴ5xD*wMU(ӣ39ڹW\wWn MW2Fڽe FNd`M6M_;-7+4E4j#@#(D@(187u'͎>e/ڇ|:wSג,\hmm@mC3S:#΍{ãwegpiy4m*VЋ@H"`n}+h} nI*Zףwꑢi}_=dS];A:FVǦmU6^skZQ-ˡ6д ck7ʭXrJP"IMRJFHIi`D`ע.MֹF"7fk%ZdfpƎ3ֱo2 -{ey;{ө*ak#c.EgFI8*U -UzXR4U^<=+VTr{G8QYm'Fr8a -NHT")t˸r%ܤqB#O^ͪq`IDJ LQ p$0Orާr.@vL4tOsm+Gtv6r(3X.aH˶3%b2fs"2 `diwu -c6uv n9f nFzY <[ WL۟^q`("Z,};}KYnM粒qQatߙ}7Xjv,纼R6>7*[ -zN 1 4%!D%j!'PJ! smhUmpP6:T`97^,?| uJ{躭 -._tA1\BWJ9^Otr CAG<^ǭ}j9ƃ5wFrGm &ݴ,P8jr!q?xp8oVڃm76}UqU^FPls^J7N) !1F@ -L ` L0 LiU,*}nkVT\\LbS;5aes 5d 4'8 Wq~>NLSQH:k*U<=G5xtג[oNq\3&]^S˒'[}-mF~8Pþ._a-3.o_;JDNJŷGN|㣂P5r2 4@CY$MRQ D(hh(0C0I -H18Ejo?k}Mhx'rs.̺i-vcxK{6ܚ-%q\+Vprs:a(V&'F m5KY;7ɇz35rҏz_M'B]RVȧ\MS-{m҄ @@)A9M=%&@ І↣"Q@HZ"I>+iWe«lĭ7=Nܣ%E?@k fqg [‹pInxƛw A?A['x9OevO5} m\Tv]Яy  m 2hzyDLL@hQ@D4ĢQ@4'\,zc>dtqm54mAӥG`|{gzeQ[)_;:s@ƿG:rΏ*-P>'l|dK@0@M14 4.#L &%$!`z3rE'mf< lÙV1+&#(֬ɺA޹ظ /9&E_?"C>h4+ɤWmf/esu(.p4ZOMykeTe:r7֎Al<52,N|e_=pU$EQdI ` 4J.#+#p=НxqNet,םvZ`tn/=;p>x\d?j}tFQ35E-f'bUS+',Kɨ4xͮg*!29LJ\>mt fqV-|qXD%×&oAZ""[ n,b@ `0ç㠧.c.N-h]]s׉+9^^Q'ǒ[rQQmye7Cv[o/_?;"Naaz "%b`4pN&3:{5m\;cYFR',I|o#stCm*:aȯe9T2N8N)SGdߋ@o⧻6f\Z*EqEӗ//`M@ Hbhd&B(&'Ȧ(Ѐ M -BHS@fgJn+UULmn>8Ǧ/:G,|T3rx~iҫ6,{3z<+ƎFegM/v6\ScU:/:.9Jx̼Y7<\Ci}>{aTzJ(pՋͳY*EZ8rݚ{@]&R5; -̝k9w]9BoͦF7%.&sZ, 8Ѹ&1n%H `ǧޢ>W<:,6ەiʎ#[t o\%[EX XrΦr}=:* ̏·:-.,7

~XjŁ&zR.@$@414i! ^{<o^[^wg0n#O]>}saY -Vmj7]B:>NZNEsX1+νFlgTkhcu^gD2QIq DD@ -`PbDLn!!L/_?I'21PI*Mb`1'N>r<èjQp -7[~Wu=؏=\M߾4{0;{;Ѹ0c]{Ѿ߷>)НS}#7Cd=[MѬ끅U.M.:&p&A4MՉzN_x9=H)q?/AyN"P@'1! HdXN@>ZOsh,&ٔ2Ut p xMz#nZ𵅀6.6oS&|͈H-$@m$1:9yHM BH9Bd|}K^oΈY"$@eMEN$BQA'&|eAxeAH }.VԯػbBZNe)_h7nU(mZ975J6 Qf rbٞ)oFuɐ=̘DlRHijdF+c& &,D~r0uO/O\%ODSKZw;n<#j=׸U_n|<>t#^WY8N/wܹ#T &ϜLPx EWW$56[,-pSt9:f\:p}WJiKUlwKUdzzRyi}}kԩn̞g躵 RxfY嚙m"kFQY0 80jLC5.v10LxsnN\vtJ&f 8jOO/;I_ښO *^듢FyH -o14өC2}xxGRIrd<|aYZNs_1~-'ЭQV*p(,SVv}~Ssk-.,ZnT= J6ڥC9Ұ^4s:.XC9eiXtZY8ط~C`o2)X\y(biˮM_*[#3}Gu?uCsj@Z: XXuyTV6UX۾wEkr]ٴ| -Zmrzb奙/ehWKkgJR[rx`:P/=8۝-tjϾn !ʕ)k'[zOVV͠} U)8N:[G9-NJjWW2[BX§e -%(*mi0Y`jp`W!Da ,!AUzܿpY ^KCw - |]jvyl½ -(4{E ^0;^dz] mźuTtΰ\^ߤsb&eE4\.t4|G\ uɪݿ4<\]WGulϪZNi dն)9\_ dÂ]0r,ۂϻQj]G6IA?l[v@ޝX(֒J,܁h97Fr -AטU'hjpqYc:ci!TVlFwÏuLЭX}*9SU-a_G)5x>Q{muUp:oĵgvӱNY _mF:=w\@C>ʕ[%hVNc9Ee~')=?ӋKW/4]jzJ߹՘]tjNms4[.Z+xeƓrչ'W3tq -_iw3罂 -7uOz-Nc_]~a -mݢ,>=}v_?rxO6Sը` b`""rM_3;]N"twnIȶ==ts_^3S8GNbϪ<ǬwÛt kCG4wTTLs%G7]G:]D0Ci]λqj8w.'p'k6\;x˒#g{=i;&w>GAB:[f:T9WZ^[n:˛RRG2 hHhAML6`~|WJBн6~|v #Kϡ_C™[:ʵr$,\W}l6O˧ZTwBu5j^|=!X}~xŤYD *x|E_Yոe݅5=3fVR3rc}r>+fϧ|$6q:Pq`&EJpa(d104! `beSUmto*wmΟE|@{7dyGYQ:vJ>t7OWspzhqN._yO>~*9S;&H3+@9 8'"} 9/~9vywe?_"۝Y6Ω@<z_d<\ssɜBѦz2"4H0` DChuU;xD/Aĺi`08j^މ䞻tA{Аqdb$&EC,dBQ@n 4Im  & D`$$2 b J3QZh&P҆DcpOm,\`])#^YP+'ՄDfysߴ?7E}K#H Q !dBH6'"b0J,D&DD%A$88&&11qcDXک[ܹ"4tͭsNQt=$I ` b YY!!ɨȒIm04CbP $1DXZLpdE!!$%qlMԬLbNJ!)L&8 8qc@9EJ7 ' (Ќ2+` b ŃIHPA2*&! Ɔqh bJbj 0DD`!D dĩXED"$Hh`&K̓٥ߎQ/g3:#W:Ne'Qb\ -,C6F%( "!'` n D!HP0  $Iq E@Ѐ@8ȑ ' H2 ^=Q2 /@q"U21DI"1cƗ25[q9EY",b+0PHH" X D4$15!!DZ`$6LY$#QBD@q "H BqdG($X " @Y"&yd$DA"1Dd'` hJ=1ŒFHd&0",dQ"-dBH !šMH$d7D,n, D $D=X ĩ$J! 7M$`$DpcI HA$$M -L M)""ա8 @X Hr2(@a DL -,RDDdX#*48Hh' "Dd74!<X4 dІ!yB@#qk5n($8h z(dAb8MJ8"!$R6MZLM Lm"N%ĢPdbL@$D$2!!0 />)>9qYI' RM"+"щH$H7dBI7%iBBC$OH$&E i!$ BBq JPgMUfvqi]bCWqtNQ%ga8jD1"$ Œn2CB E@ @ b!H@48$@8bDZN,`$Iŀ!"   Ay^oyoQ (}[BsZ|REZ(a1 -@@!I`1"I D Z$m(Ba2C $10q h (h}u~)ZRHP:fXш |+GoU+TLH@` Q$ mD<" XȰ@M($ ` B8 HX,I b8rN N $ @ȱT$I+Hq9t8 --\o!S@9DVGk-ݔCh$ q@HI F 81Q$ IC" BDbi2BBN i0@ 2DA` C D DBqA",i@Dȇ:hUii#3p:yCIcP57w`!q@ $X"b474$Hn!'dY(dX "I!N("! @$9D($q`UiW3 -.V5~Y"bpz#ɠb hC@F&4!!cH$1$F b X  #Bh@ȱ$12N!$T$B$IȺdd 1dB:ݖ1wU!V׌nbao,Z}gmm7͉֖vFIze+ -l:b5lBZ4fN}*diw5#N8.Z#Y.U-ɼ+ڋy ^m l,:R(HȢfjٿ -exP7 {r)mv'EXK` CBP;zӠcpc#!VEHI ib C14"dZ4DlJЉ$BB Id$ "BH0uR0P3TmdXB #Rnmu];ǟt>HPz.6@DBB`D]{.l&gR|r *VwI8R.XΩ%gW.Ѣn -!@@ BhE0C2,DDIū Lh h@=W0阜 <܋@Y"-jyJe1Y<lvyzxzޗaZC)՞s1` ! ` h@@BBi44EbP:<8I` -6)F$3gdڬ }S1m1Q{hb4& 4 IiD0@D6`@h{XVmb7{.qd -b`"xmd^{ЕǞ }H01V(fZyoJ6p -Ĝ={(ؠP & - B b hԨN10&1Y(.^]N%"Uٱ6;gE(>*!)ϗ@*3{LN';ٽN|rsj뇡Tm}!*@ &&h b%`@cq`@ m缵Em1 )KŌMi `'3!! 0<&z)AY:~y&'J/=Op1!@k2hS[U  Y5hK -K_W6p3!9GP=Rn>dc~a&J,`@ Lg=6%225|J -yD7U,Jm,\jw V6@x9Qdu fjW(QR"zeݒ C@ E$6"! 0@ 40-(CMҰC @ R@ h8qpC@ @J,b `@X0 "V|LF h4E5@` "#$II +5,T@@H@7$,F$ H"H"S"Ir-$$ I$`%1uqhƌC$0IC3dkM3!<Qb59&l}+lf2 mQU{a=*LyWZߧogٙgwf>7KQXa9crh%9c=2m|ߞW7"f<<ʎܪ턒G'y{{HY<vT;<,]0<5{~ҵ9Կj63q%Qqo9FW_]~19OFA~Ydy瞯q׬?1|t iסpx{_;w'?S|Aͯ[.SԹLWt^^moE.|ZI]S>inYE[饚Xߝ'/>Q^yNGhN?XݸWw+Fql6$Q2DdHb$k,ex{eO({ņO}sjF&v>On=3|_5^{s~ϣwйCc|u/UVI=7{^{r+j_K̕o/O5|xev@g%}QG2އfo)<ߥ3URԢy^kD^R|}K00ks.]V2,ۭ.ںwV|s'$o׭o+ssNIHC@0``Ĥ6E[&Upd@PhYxϖs\F\9oS+xoШzloz ^~C&jm9wFn[w:}=T%+}jQOS:9grNzo44u mJ.ЎtYlۄssΥZVtشl[|U:>')Y%rK.*mκ,o2-II  JA؆rD[*e2)$ 1zAxrg~k8<MVι:~BOκ1`D""@W&JIU -D2`0gzw+O2ot |7cGM=ˣ/~=;znlΎaX@T锬z/~Œÿm=3/>QNi}t3.vgYTo5esT:M֗W.ϱ|霛zbWz?6F6Q>cw5WM9nuW\}qx{>bt~qы5M0CId c!*ˆi/!/sSʮG9i:G#|zB:/FtMy*N ͺ|_~2Cp̲\ۢsvʵubC&П;Xylntg)ֿ&}`s -G+\ uG/?L:Q;>EZxmtJ %s?ϾQet{E~\ sno>G!'͖#s -`& @ B`6m<h"Hg*_Lr$U^g;J;k|@Wzs;eG ^eʥs#MUr/ԛd*%cQ @V5yhDxk02Yf9R1|d}0 yd2818手0ɐa, V XDbb4=1pqcDdĉD @ h- iaPȀ4!$` 6 @I18BhƆ`40hL`^$D$F@ PR\F" H@ĕ1 H`BD$ @!$D@Z``H 1+ 0M0@A4`i1 `1D -!@B4"I "`hafPr RttrY:aR.+GK2RO>uurܹ̼2!`0P0BPh`łm<2I @ȱ+I! M4 bG9i!Z^Dz~P^( -Ym<Ļ-Y_\;UhZ8gF_c?XcvǑWp=Έ&MLLbcI łh$ih`#@`hq110 Hq@ +47Vzu/aEIY^yїOPU?,}w3i &VY˽9g8ɾ[cfϵGȣ{ݽ 1@Ƒ T4h0 - L`q - АhBI&  IGOĘy,>DH)d\ G$d&B Cpi!1` h4&0`!mqȄi  -PN!q`J$EI -QbJ"VFCdh4 h`@ &1 -$ ci0@ 0h !HC@ LdZu!J1VEB4q!HJ4"@EV& 84!bb@ȍ & XNd4i#IiC@Nt5d@B15Chb@&lD" @od'e(15f i˩yD}\NsWIϺX7/[ͶKv9v.z= Lhh1IȲB4N֙LkYGwZ~ҹ]01L\Fa~ܢnl"G Լs~A6WU`c{Utz}MٵO} Y;9g+kҍ=|ܟڈyԎ)&=m_F[BŲkۖaju9`n#kй?X*yӭD<|%V3Y+ch`M!F 1HR;X 5;;2|6柡HL,uCϺpn4iܙ?.S/ܣP4תEfG}3^jX6LoR#+#+/tes~@,M>aѹM0Q'™e*_n%,ϷZy)FKS{%RqD)= 5a{nέObРokY붋@iL(I%VR\-1ۑmB7lYݣ\ls([S W7vh<,4XFkن00\xgI;1^B<<̈́O#!aaYIqdKK]/C`H[W?:nSZƵT#fYi 4՚CjS-N@CA4Ոh!I:`V5y/ei#g=m:lZ軞EW -lyյihM]MMv_Y/ mRv~E*)ro:PV?K-Cr ?D}v>QW?ǵmE[۔wnhV -t=3󎉳9V=6r]&fa -f}#d=X&` ` hLbdMyfkeH `ˢrm~r%~"[36:z較3?X$tB_^q`գ&˓u6B-+i1t;-~nf^} -e.vI836Ek1CԊţaSѶIλv9t}`ڍa9ppD^Epvf/qҩtەb` 00D N2*λTOV -~B+[:;eXU=Վfs2]ݕ{D&iڢh`G6B) i#FxF#,0XYƹ|#;:gL68zzz}%(W8&L hҰ@BqhЁL# n3h$HLl s]9c,biyXW*t~j׭x)>Byqᨶ֦BMɁZ.u{)̦2=cI{:ן:ǕP~Ǚ]';PhS -ř0bA MI@`  VcAz31 MwRzҶ%th4eKSbNޒZoMn]E?"ˮ% يR=yO%ij*l,55`j4k:?57=^%(7$Lb4/kFR/sƻf i EiX"hTILDM 1P F%/4{#1"gdx,=A8F{*e*{cm-t ɀ!FDyz7Bl1ǎHy ) ƈhjI` 0!$#1LA%QF& I`4 K̡lzbwjnjޫjK/":KyM:)VQx+W\9F8B5׺ZtnHZFtcdKiQѓ?ÂZ55|ɑ鳗Q261  & -I%` ` SjHh6Z9y@n(bj4ƀIiy|fadoϲsND o yܹ,_*l6u.uǪ^'q3 1[ Q/sW{[ce*;U6e[WyTnA b!̋@@ `6!(E$N$vڭ<B` -J Q4s*ҲU Jajwi]FRtLs$< 2c~fỳ@lcg!) LM(ҏ^n Ѡz^r$HMhF&HB10pCRȭ5Ȱ$.Yk,64&-;󮒹z.]/ `ȾcӬz4@dQ"," @& `4q J-X JĆ 8I'(H`B0CCLbs[.|_+)`2u~EBxL+-D݅8]/Z a$PidDթld ~gF:6hsƕglkXҳϺ6Rkbj2  Gs^І 0@ bB@22 b) bС$)ō)+0@i#$sΉHYr0zPv/ y%u;U՛.zPp1>^i7;j.MtߜqjYŷsߓuj K]{Umdg&_># hh&1@11MHSL1К@E"@0$"7Lc4@^bRFU# g@4PnVlC -jҬ=|[_(7ur9oH2Ejv4hb,qJ 4!C@@B)BC7$9E+$&L`g(mI04T j)!W,TMzY4gTYgj<#*KA4 j+5)nfے4K -QhHL @`$DG(ɐRq $ "11JIDI(zd HRrDHUKĂU !)WZ]j s =,lhm:/;ZQb5h-ƶ5uf9?^<=2wQS3J2"b h` 0  H@!7IƾXc fkMLʷ2Qi7 ո~G B.!ED҉CB@3#9exmk7Aѡ(δ1F2@Hբj7E 4!$D%hh10 6( n2rO>G`,$JQh @И1h( D$Ќ*P -@R@&(ILHBqdC@8 D h 4 @!,8M}lε ->zԮ$J tk-vb dP+Y)&# 8h`@B0r Io9 -`@& &B!@uhR go9B}A6Z]^÷)}2s&eG*PrДAY8łm  X  4cH"E$_9CTAF@Dcb&@8141g%UwJo~Q49~*٧LT[{nVdq¹ׁI &@ r @j4clV4h   &7L`Y?31P b@҆@$D*n hʽAڳM`q\ocl>ũK֎Mg*ۚShfffōhj0%($ML4!c!W@1r:\4CI -b` mλCcU}k-Ю o}]$YCkyY\q m"~՞ZR9zyJJRRǶNq8#(8[./ƯC`ۚ.9zJ9A -a0BCHaJ4 @ I$ Vq$4 1Ũqj75tMMOz+> W1wUsƓys}fuLOm}t}УCFXqGeC -ek?5Qc5n o L%rʊgmܕ_,K>;tZmֱh H@A+@(88b J $NS(H!0h"ADT\iV=Z΂>blY?hȁ?1D6IzqaxRk7RjciC_))U2lP=%LDhbbHhHHbJ IN$D&!8Oc㽆8H 1& P*X][tcjѴҳ^v|/.!Han#qWlOjO:TFA]aoѧچWhub)sGWn1Y.3J78laBJZ=p5LMFI->N C h5HĄ≤ IhȄ&A/$%i1F3́z%H n,I& Jv+uξJgIBseh,aM,6fK -Eaeį.WߛnaӸAתgZtxujf^LLU˷1<3uxt>/g$I[hWiݙwh8LE!"ᨳPu(lR& cp`'H&2,X ƧpQztե3HJ:_5\:ʣsAۣR׸Yƺө:E+rd^ul(gy-QGsO-uFIm:O66jKeG35Y%sT:i.]hEkztOr$A@"E"d1e>`C_)I!1±gkUnѪYIgFDI [4y1r[c{T\LSA(9cmgd&FRbM$Df"G!ِf XfkP@ DQ'6N"L|/OV4 -@XȱƄ2!@&A"H&D&AĥPHA! EzJD7OD @"$@Ij$$I J$I%dBDBIqc"ISD *iƇ %KSǑBD!"$H=6L$E"q6DBI"IHR&ERpi2,ߛ&#ЄƔ@qȑ)=BHCQD($ 1 !'A! n dbz8J$ELH@"}AelGY=6LHHIg{?MdQD$D&I85I"ɑN,pd%1} Vg+ Drw,WMB52&ydȄCQ =T -M@ dddQ@>fT6\wMG˰>;l>];!Ã!t=DB?QO}N|@ςWH} |쏢Ϝ}|}(p>7%󃯤'wYvILyώ-U5ӴQ9dl6_G./uN&]w:y_5/\\Ot^iNM]LЎ;ˍyh#s8N5wp7vٗ gt8<˄A. SqGЇG[Sc<!5 014@"$%26AP#3`&QDaEFpBCqҾsʿS//~E=NSn\^-<܏9L뮽"v@۵h7r7yG:3E>EwUO`G7G\C9%_{Y.jƥS۽-/K@[L_ce ~Jo2tmMϗ;2dL#Šx(!*&5OJ 䦪ϒĩnp/e_yO/u_!I* OxAazJ1@#Dg=s)%crQ2GmU2vdItq9Y),9]i^b$k㜿\أ%rX2>:QJ8\t' ;tcZ)z퉎r5!Z\kEOTD`' +wkW/z姕\=_cTA5|_d%X{Q1oXJ%WZ/"fSO^-B涄cG`ܙl Y Nܸ{Y)1-މ%XjY* -c||UI5)˖uR:/T`PWnDˎA;ZPe=k3+܁b7|ϵ@UlJvi0,)͆Ly$&O- ?鑓}E9x]FXF89=t@b=/VG!,ZGAr[e# ŧM6G dث%a;ǘV4c;I_z|[Id$JV)ށ b/i"x q9Ǔ|5L\0M1Mzhdz;z1.0hPX؍kqk|"yj䫾+QqኈM::8f -ƐocưH$\!$Rr"^cynM,Ehfbĸioe~CHYp ZӬtґ1alkE-JtwҬCNVLzPJTW6[zTE*d,{NoJdE9N̾2p+_iӏcڍ;rczv7z#\F D>ʢ6iڷ*hկr>SOohѸW1sC|gugBrs+<2&hd15B f_Uiu 2a5]VuFA*ؽiY\ೖ\vS52ylr/?#eu=d*^jAO`e7^Q#Yd3rvbk)Ζ7#eͅptLjҲʤUV).r}+!M}_#a.qUzLvq,$L23UzxZsAe U4X+]V-FQVdhHl4ȫٗȫaUsAͮ?n#6P+NIdPYόPCs"Y9@3'g$v|ɢkUlbP:-g 9Ֆr*ST@5S"ȄueR2ȫxյbF|x\r"9(%/ -5rτl)j-oI-#:Mq17$"wIYY -2EJn^ϒJIX| o{-c^pI=Sѧ&tJf < dFV]ٲbWUSeAo\ЍLьdaf$@6b1cX7YǕqS9[不D{9[Ώ,O"y;*"츉DW9Sj/*ק+=eFXV1D_d`r^U!Y%@VY7Tр<ꓚ HyhYk%X;y#)ICoEO4N0΍t#lO`F&7ڎOo5!w*_ -˯Oib~ $VV!r"%;$JK#{ڵ˷눩cN*ZU4~*2 FvvxH缐Q5tTf^J+X(Q'kK /qwԨ\~0x~ˑ}6# ߅IН(cxOPzLyܦӤQ|h(q_U&jL&YTtf}`TsL7[zdD[V4s)M Ȋ=/f]רTf7(dN9݀3a#}%#`E9{/p_].2v m0$tX5ѻroY(bv:ʐQ[iXd'pEZ[I%xLļxd;?͵.Z>5, >F.&TMXQV8f5k׵ˇ킐#ny?Ǧj]\- :_A|XKp%Z#AlRBYn{e K c/o(D'vmIc:IÒ9 -6n&4-$@ ؁Ej#dtq rdF  D4њ2RL~6+lvt2; t$H?i=}nMtS?> wઉ*s]y̙o|߂{#_) 2M` mOVYQ:gpYLOAH5;J8y~J" lRK5`xZr7XgY-B8x8ꪙ13g)\!B$>܌fG{4Wd+cRw ܇!ϟ4\/' !9'tbiD,/9$Č#Pk ̳4"#f!tyL!N2{X6j)̚6$2|=U3|ru0F"ʒs|p../획 &MrUg)~r7S|6.'}C!F uem&>QNNa+cuxs4=VArwtƤM/$fQ [.έwȗ;ߎRQ*×hkQj k<ԉ1e <%AvvV|v8WVd"ͮ",]F_(B2t g`XR5y ->|81W.rNELZL34=- 79IlZ+k#55U[=A2jFE3?lsͬ.oZ<6,@֒"'Lwu|edP) -8˖VUYt6q65u!vMN'|G1\4(vB3R|2U!ň|cO&jOLvVzQX΍-,\g HIHYu*{zǤF1N u -|~S][a)a0}liJR7wڱLdUV9GnD12>W79d?*VQ6]y5,~Wiʭ:.I2T@A!+G-bb{9. -7,93\]ssdjۍ_Tf*%8RtR×j5U JN#嚣ٖa`tZ! wfxn f_Z'Kq2u8LqO `D\|4tY͖00-b"]PEbkrΈaVT;uNY~o{|S>9".؊+QqG&#ӟTƙ]҆njg7Un5{Q|QS9|Oup{UU^GFH@ )9Uی?ƀ {'<9~;Vk≳ʆd).%?T8Iw"0{@ ͍.LY`0[b$GSMh܌Y.%exJ`GXњ%<_%:[cxJz7RFGIc?*K|kv_*/?,5BzYH2 ά FJDWNE>Кز;~~[u7Vt=665wLOٻ:nk͔(=kdӫ%TZafz>IņWeڅ W9 cdG\J4۾ o0ܠ+~1WԪRИnƱȭr|1S$词+D+#4SK守Wc;ŒFOV!NБ&, ΍\HF0Ո+aƢˆ4y Y;rB~(_ƣZHA1 ⧱o6OY>*|U78oﹱ89șΜ";߃&#q8*.wOisok|ߎr"7UX63orA`$(ODDeUj㼚QC5L1c8F|?I*tlALlA5~ cl1{˟΃,n8mrD IΘh6#&c&m#9\H#y\֣ZL3u\a{qGc柷",/.X^֢StwOE_e@5zoYgU#XgQ$L_O.)Xf~Ɍ+\c]j:E9liZ'.!嬎⼋r@h<5IHq֠tVnw9UOR+cJ{b8sI(r: Ȓ@]́cftd!,o!:#?f(dOBdOM_N&t1`ʗsN\۩}AA/-TWG7b:_LX=Ydwu$GQ;M"ԟ'L~V 侵3ew5~\zYr޹2G7bT}v̓3 -Oӫ\Jb Y%')7Cr-Hc7vR"71Z^ݢj X/KI?-0^p[V| V9\JQ̖x<,|5||c5+ӂ AXGcT!جgɬwiAx}wc2,uy`^6#Ƨ_mR?~\<qq7[߆aL& ~GG+UkG͍7&c"Gk؆kw+ybcsՈY#2 zN!VµĮTeIۛT]Ic?0| !{Z#sѭr1˲bI6إj*&=qio3BVXQdLC K dRZ2aõR|I,"XQ-왓$S7SY +Vّ?uW0֝ʬ\wR$ICeLaNu{vF$5-CBGL `EcG\2D^xUH{nʠ!m󵹨ܣl!HFfSK+(dSʥ%ەr/1Cz'g*,<׎dj)_n?򠼥n ұ;`E } Ƿ.O~LHJRb}k2/r67&yB.R2e䍟c]ԫrOMg3$hK+aELt$I`˕#-z5+qՒ#2fSmg /^U2$Wmd -`R -H\o,6=~F\ózF^[J'T1F&s:*B:sͨ[W#V\Mֳ"}ebs=95eXD[bCb_ȕr-lEbl˖_jʻ7< d$B=la\ohe ||)A[a屮^?## j,dž5YS%tB}:~Svy縄ů}9cX[ 咅%h\pT49łL^C ke|G/wE۴HjPّ"b1XEш&e7ɑcbZgցd F@&d,$ˈ!VXMHbL';ٱL5 -T74oL0 Nj^; ]WՌp=8r8p ,X-%m>|SnӎoBHYa7? myMՁB%-e^quuwb.E"HY!f-#cayUqW%1 p :蒣uo9:Sy\~nui㼘ӿ(l9$suiM!ޚL -\Hf4/wMZueto=pS9+Ŋؤlc\Hv.j֌C#Ka /?*: -BR`eۄZ^TeaUz ޺ɜŒ[=4ȼJt<50ѕ?|%zw}WE^A-эpG!.6rCk W!FKg<( DlpdO s.k7lv%;csFIuCߎ˛pU^.6oW7̘؎͉W7ߏ7\ߏ7lE๿89v#qGp{n苋\SyHn"SisUVVܪ/=թr=SWmwΫQx9Ͷ Lk~N -ƻ=&!~=W`\x^o-~ \؋GmT٬2{˟M6L!qy~JBdoNl29Ho'NNVt%}4b0~޺7Mq'w6k G}.=l7'5BiPxDUj67~T2+7ͅbn> @6roG1MֻN;'q8'VO#QSe?n -$UWDۢN%~j;oV&Muco.B o~;'3l߂͛؜؋vٿ.sby\jYψUs2gQ؋r.#q51EĐǙ7cQ,hNLiyav1wj/Hd{$~E2;\)BHDW -FVX;IH|djc;ֆ;B$GV 0 \ݶ(V# 8vr< VL//7.60 `yX HeV+l61n5ܻ'76r7Ȋx&lG1?d)?S|Ok~*7.o]\LN -W98ow"U9W6#ઉɿRsx9v*:osyQ6]ӂ7ƽx9v+q8+1G|1˲oV'|\R";lcjndMDakx.OwڪTTL7Ycv5?q#kMs& ~tNCATjoLcvQMl?<~%Rc~9rw*oULO.˶ ]rw?n7!Uw±^E\n&+7 -v ]'|{ S~J^ -ϊbD*=ˀcE‹Sr;j[L9G9+svVpxyGoG&&56j'v#tDZ}qƷ\鳎ɛ" M<͑>gHhg9~;oo͛Iخvss&#s9ezb/9 -b㞉Gu[MϊDLG""s,iV;;#+yU7#&/ %*rn#HV+ލJ!.]pȏ? 5TI#pzMoMIH|CNLCBr{*@F{\:L+́,I%J%rA>@QdWs\dt,NLحaI$}a1d2XqgN7+[jFh`K|RV#0Ìg)1H4!gI~ B=f!11\T:B#!Hڷ"۫zV({\zޡϳzKc.\i*~# JnXTȮWIU2kKi㻵,w8Oʟ*ĔǢrP$d5.,QLrZ'F+0a(ܻ18VSݐ=RlVG1zbܸ~r`X*C~VWL*эjwQ=TE2!k=:/PΚȚҺLs(X Vu,k[QGn\i29aZHl#'oVI 6s.y8e"c뗬xnClBX8*1+]҄CLhj$hƅXJ*޷/:h[' Oe^@Zd@8wXdW#5j{yp%b#Fj}̻&xNOūsP'#]i*~+!nٛXdwÐȏluʱU=%GHfn#gEܱ%ddIB_6o-!r 6㕥nP#_VU; ృ׹[TdjpteNntkEYliF{?IH2UG;q 3gbHaGX"s,'9xK є(Nl Ee[XQ.)tT&L@(Ǟ&nK 4(^<`Z`W5cHG3p9PaDrWj5܎ֺ(\E~28؛ ѳn$q⅛ slb9M_/frlTMec|7|U刻6sbqߎos}|U苜͉W7t/s{*ssb;99Ɉm?+9E૎+Zlr/44̉;IfU"#wƝǑJCiG3-rhf+I ypZvG+Lf2<ǖЁcGl3:Hl52uȅ^m)p5T " jmוqN11M\t㗪P{V" (N?߃if;"eFc8I,qN@ ȏYFxHaB&L##}ɓp2 DFcI%GgQ@dkc#ri iI|Ht#W,ܤ'QQ\)Cxð|vccXv;RYb#NY  j5׫8Eh0qz#:BHUcH:tf;/vrW4mn7v`ҿrqRr^\8%4;'/DՆq D[nX2-<.D;"յRds5e4,ІBeX({҈%\~1 -qcI@28p̲֜:Ȑ v| БR@!#H%H3g3fIHj{ɍ$G4U!.ɽoW/vTF׹^ -W}Y֑ɒ@F~@{Mʖ$Ȳ|xr>[c !nC|07#Nh B4y+#E1JGb`/fdFabg 4/?c/Β8%C7oŊc4M,/tȻS,8F r>?or*aK;遂)J V#QeާSǿXbl+ƍo*vgd1ycd%@9!*1"!dDN2,0ZT_4TSd跤 Iv\F"(^8Ψƨ1۷ᣰ(}nʄɷ3D֮(nDjl15SlA1Qj"+Z>77tῳ7~;7||9*/]˷ū.!qDz.9蘏:Yn8ȋ~T]ڬG"["=7COT+y7@Ll5.[rGf7pf Qd}Ucw2p%M-nნtt-'QȓuĞ5 -dG#pduE'y-|>9"V҅0XL{h,C8B/\/-a@?xG΋i\q1!(3Y"l4r,6iF#yVQ+h X/,I WVV$E vJmEG³Xz5*-Sb|wƨw.D%KI 0<.>bT5G8nA+#[~Vr*_F'"@8"9G$\sjnXad s-P˅_BՂ\?#')jƍP"ف >y8I]V8+o9.ei/=4<+1kW~K#Q5Ղ3w | ܨ5z ש&megr,;6RǛha9)yfx(p·2&NTeG:Ԉ:)-crDÁZsQχ"#QF8+ֶdgW*ƘX!RC1|ULM#&ΉY -s&]镍V@/7$ k *+|y h!4(Tou{Ƅ("pxT -ƀ\3Gk͝Z3L>`:<'At"+3bL2y{^l-#V(\+QB1#R8R[KDcZ1/S6r؏cj#g.nmmp<o"\|^ I"xk؀I\FAUBV-#M%T44Ym(Q<4{piخlh!KGc[xvr -h"X )Q,^>h.dKyz$z$dU 9mHC1gy]66-sK" 9z-N8i6h= dwR*,z;繳"Nf3Clf -A(KFrHimѝGLf9< g+]+GxWZع1$r:j# M/+v@s\~X,wG y-^u}W6m0 R7HQ˩)^|"!c9O?Y}]"*7/]^\/[ "'/Za~}R1O<֋(ME35W1Q+; -ځ&7?ɏRu!dsk&ò@Eʿ9x4V_r:uS)jgu`ʡ#dX#6{T˧܉ ˊ%tWwv>q2I&MF-rB\UQyp˰1jQ7!nYV5 QlW<'L6@b*~T|4uo̐F+L|ς΁W 8]**)[LooF\C|0b&i%nIOweVp**qOeQ,R7#op鵹27be yI5-Wq`7)$F5im]5J"C$9e@ +^@VSKϑTÍ|9mL6)XvKGAX QQ*1*UC_%P5l^wbQs,̔tHFѪH4crY%h$;d-<-m;ɼk`9b˳*hQ"MfEDN;^C6e|za2BU㍲|e֮O64߯k[L2F2!̬,fQpLG H]r5У&PtO2IҠ9 5r x~("H2W^`\A9P{W35iނ:w:axX5 ؕ)v븸 -*/(PiXsc[¼OSg#<:?cQF0F120Zm$ l4],ƒb1bpXWk> -,A8hHMBGcmG9q# 7ۑE ۙk~uk0[D<Ƒb4LjܻZM=׫U^"l獯v΀W7b1g_qjŖ0aׁߎ9]FV0^>g5j0X½ІX Sh(6aeG6M@{lB7s3|LWɈ*9&5鈨:DrgU_cY!Z+rb6!9ȟz/_4^+[a4xXfS`8T1^*,#e)&C! d[e(lKo=! NK9fp%?} P{-)`ck9C@G(ʮ\eg$$Cl^q -BȰta׾H!DYL{{2C'D(FD;d\d܁HQ#W/m ShbcM=a{D_:Jk;0Icm9| N3af/uYˆ!$Y+a~ê-zuo.MAxRTH`a$zXl(f<-T\ʢ7ùH.AS-Iӳ\;Hِϲ*D2* zVɫ&\uK{ ZDUL+F+]bDF0GJd. 9v&I!Atcx8+D\r6,yr&_͑QR߳\r p)E)~2Y1]ӪG$/|"A?wP}Ɓ!Ezp7F7 "8Jv.Wj^q"FqBqUnboY;/5xFƱ#"keWBd砘JG=(Qڭvv(۴\c]adcZ߆K:N{\E91pg ŐU5ldTB5V@i$#Ш;#n0ϹY䶢U'-.[!G6![jr2=W5"cAܫ|%UmYS" TWM~4JD_o-_:DOC'"%>?5$S1;4%cc= "CU`*")ھD.$jA"YV(@ar+hnmx%$"LEGWrV0ђ$w -l,WJs"|7sg6sg6sg6sg6sg6CNi [qGk\x A Eӓ9y:b${cbZF)BVG.WօVIPbNzur-LniU6mМzxܳ%tэW %uÿKuJxVwˑ Z&Э%zæ@z-+~,OŬqvb6BNJr& 6HE[p'rO߬YQF-\]#~AXqPƪz_r]|J]F <-2UYO@=2*sBSA;r7re7Dr-HѓzK/<1" >{[qO ڙccNgs+5Cb0#bJoR(,˘<'rɯ2)S.Szԭc2b1@WEzLrp]aY,Z"aBܶ2ZJ4Dv\& FcΏFիF+ddZqdp"(IQ՝`\ZmtPhZM܉ K U>YiM =̄szDZݽ?rL'U#O9Pmt~)i1\V4F[ܲ]qWrʫZNG1LIĎxdMɅ!0)2 `Wp^8unK2;`=UFAapݢ;*>nDž:p#C=F[0LcU1# > ->fDscc X <܅J7 o#t@9ʪ5ˉ-~(LN?.zblQ0{bGaH%v!Vrcb5xa?/8@nX #cɍݦ ."VHE7v +sov2@ |xt,( QBY -$mqЈk $ϕ -$$I.X F b]䘋ݲC!'z%ez -9_edXk?\eUCll8 4'n."sK"F+ADQ6#㌎&Lck rj|=f͛obp7s&sb.owlE|q7~;o~_Eg8lU̎}v=C84ǧWrNkP24i])SBl ^Gya&(:;h.Z#Y gOBKd}Kė1+ݴ "?MĐV[5]Y2Z6l\=1x "=sK@>3W9Fd PSYhC9.{0RbSKhˑN9 l|4J\<<,ьJ8,-qZBtEs,Ow*̋,R7I=d16e| l0ՂcuOX`{;!0>Z#fz1F5P;1yLDvmc?m2qU*\ K||WXd@s=9gY+~,EpPS&i,+$ W2#% -fNӎEfX^gEf[7 RJv}o#3[rl[[5h2y@f'h~5 -{t5e2`,ȩܐ)~sL^Lt|X˨LQ z'.'x92m 9dY,35jփ -ະ]XJ+K=IfA+˱#Dl7MQ|q?+x^7s";3&f/G!՝x9r9™`FPQ@;.j9r&K'"'=zwT@_cm 39\%t!\XokH%X4{`4 - X؍VW lY ""AIs&BȔM& -<Мya6dp:ʯ'c%@Hɚ"aY2T -.︌CE2(ĥB/HR|QRolp&ǒlU_SOu$,\b{XfypTEo=s>&.ʘDe#[֪O&jb96VAs~.*5vƕNtELrfv"㈉kɈ4.!SHv53ls_lGoDo,G"lGqjcW~˜M7W&#v7v+^]LW7͛\&m|U8."{Vו&RTdpbl5{ _Ґx\9,<#8l;rIzOY4&,ME`{+k`dLq(zr*Q@6Bc!FhɓLBu`.!$ֶ$~8r)̑chFh[=-D`;ʲ la*oT4ZR(fkBmbe';5 yycr\XQ\ջ lg+Sۻew=*2${ rWXFz߂5rCr"@VȔN&O_ԅ29VQv{ n~SD2f hs!k1)* pxj"3''J:8Bj`"@(5}jxcڽ%uϜʴAͰf -KhPp/Jŋ_!p))<.n{[ -7w#mkS^a b52Sc;r@'rdG!%הDݫ-澖)B]|qc"Gj,PW|xFr;8rF -;6n.ev+XQ ?)D2`χfĿaǵj̈ M&3Wc`۳z,롰axj1/3qQY!9^&@kQ1Z-Hni )ypƭk7X - @4>UEv,5 wREb -/.j˅YMa(>h3Q x.FwQ_R$2 &cZtpTmjNZm/d7P˟szY/}0LSw62fIhԆ#w~Gk\׸LOwv"*@藍S1_ Ƞbm;DT,V|(mxf#Gz"quaLê^YEDngN3Vqs|#;+"p}.S 9|jsrGټf LmIi ?sRXΏJ\ʱ @@e&A:_Wcsk?q#+wFEtAKtqg5U+ݵ>rƍѫ -*T{H}i2$6:Лc8mt>Uf,FVHP P }~~ɉ5൶F 䬓co-C/ 1G`(L{8S ~o w%k J]Ov -b\H;eL'XNiijY -Dd&Eqd+l$5J8( -'>),iĤ+H1ƂgIaMj9auV1]C Uq0;+\%,S!BU/eNKkv^B2D`JGˆKؑ>/ Yc# :2V_- ,X -iN٬C$!`TZ}s5c$6YQEp;C˒4w>Cįc@T1k)8}7~߆~o|77^&yp'{){w{\ߊsȧ <4 V1gQ8Qb9kv)$3 -D]˶6XRj+eʨ!+UX g!Xv.^|R e mrnoz7DDVC}Fs+ƙcs'\ݹj.=.u}N]lO0oG1|VsljsD\kx*X -DU1L٫]2F fp%f#Q͏=&HxJke63"LC9E]+īXȀF>+Xy# ` c]P-CG2Zٖ5`$beL& 9&DgXBdxK87tXxE Åō\` - 'W)_[ |hZr qB11C - #D4Z.5 Ln$h/ )!_ -YSI(-ke6LI -Jqg|=08R1H$xdiCCgD*iʮNV;LZr,d9|iԷ01,S5+Tx7q\VYIs@Mjg2NEkGķ2]{BKP@'-R/JsJ+8ĀJ1tاƣ술_ kYYp@]]$u?*ӆ]gqndƁ򼑅rD[ u»zee.>'-sqmfmVCư,#[񡔌D#Fk~ivؙar2#O94.Md@,DQcI*[C˖3^t(d~J`+%|D_tIJۛh֯~Sw,w0cKӈv!KeDHaEkgdprr@VRA)S›r/@б5q&O +*2SY]3kj=yI-;kLBXf7?If7,vs+R9U%(}gƼ; ɐ\u7$BV,'o -bH`J;I@?-]ΡIoF+u\eB}Ln!G\6l T;^d8E UP0r:1JNAWA7AX|jE \K =1#tE徝Q2GsUn3;eHSȨ}֯5}XlĄ)QVdžTᏽ*K/~i? ''Vj 4;\/ȞQK{V͒JR-#Kb Ed~x'rvq:Bd"6Ҥej~|9l#؄j-vNh"7b~%gƜhVP7A+LI9YabN:"c!,vɋ0Oi%͎9\{QV F2VS,w dB -DWXGaYA=c,:=7M!,ǰ^l*øXjАaҋ\HN .HT$V+O1YZT "ʦpd=cGQ"Ϯ\mY&r;vͫ~7drCnXHȒ+Gx0M&l0S&W_b1P -C*ds”{HƐObKP6VĎ$8Rʼ!HuFCFN4u3$V1Wujfl F53Zֳ*j'G&˝6|qQg֯FwT1n5obqQq7M,A1k($sdۗLEc[c]j#Sd1qZ7(ڤkc|pr;*&lb16n.1ƣZj"tTUhؘ14|ԎĒgQsˌBm _a]6D(z0aB7#iὍF5Or33|G_<9os&#૜㝷ǨDvɈv#s397߂lLLW"b=vrb.oWlW|3̛.MWl|E͛;cWS|% __%&DAIc1V,f#$>)#6uiH<@ "K'HD&=vdd 93XC}q >orXŖb 3Ύy863IᴩyMJ dqT )0Q'D;} -FRI8Ma~[$E6Jz㲣lr?kzc߷Dze"1Vp$jN~D6x>딿7fFM~אr+Hib ;X.d]<{(r rzn9-ՐL5;Bjm.`v;{? ުRc!) QR"tf}䌾^\ȗho{…ػ]m:{aU2}UmNYlw*u+|vSՖdZʿP zPhe!rhW5%E^f/&WWeUܶmࣤG3f̐ŧ#Q g0DFUۑ5mc:lO#`&2ڳ[dEOD/0q\)R2[I֣[._|VjU;!\n+oԜqr/w G_DSu˖?R"_5S~id"EY BVǺrvZdC7`[jgc/ d3szrXXlN#$Xk?OVMY+$WJii )I(Fx1W6;J&/emxP鹰Bn20!BFҨBa1ى/Fa,4j(bF!leLV[;UQ)҂@f$H3IIr̬_=f")qi61D)!OS;G=%;[FI|͔T*0, e=XlL5*@҉lN]We #BQT{sbX%$iR'xY=fÈG[9Q 12%*ix&"$zR$+ӱ:GG䡠+#h>c?Jy^$!Áx_3]/Zݡ K$s:Pjf PCN`Xg}Qww=le_3V@;kAm|&i3OPs[Tvhzc1̎%)4!ݓh$}@~ jFpԦ!QnIXkHrФ2HPtg6b0-@/fKuN -W+U&t;ɔ]G!8 W(qŠRMmU+zOMfS($.WYɇ/9P -޹USS)Se#|-g3 F˯DZ˘bG\}GȨ)"qUHje(td8ɟ|RT/])T~ձ!:WIfl9mUO.$Tj~,AfO5¹p_HVU6286o/ $t2(eB.< ЖSFZ_K1M3e|d5Exa4  -NȿaWrcVm4dWȧ&?䑸H3cpƫU6Y?K28d z׌UPy:ӫZh#%(gnI%٧1!VVhҥerdwfV">ò>BO;h K# ʬVVr'`)1s -Oblzm7CGJj:۔GzKiT3^) -[UюԠ;]* Gd9)qݑjGqo#Ŭ l{IX#71ylKhFjtKe'/mu쇱6D[rµ!ݻ,$%"Sdd_9jX"uY@fb&9rUAUXs;O#Ƹivn["~?9pz>sy헍WVaxCڱGYʏz|DDpm_;[tp{2Cf` VR#Ͱ4v?9HyXs_Z7Pcic4fCȣs.R` ,{Qbjlʁ梷aF#9*GHq֒:hdȣVs+)& WV)!փxs؍F2[FYdxjpb'rc$Xi\#ulALHj 镣^HS/;mHNೕ'TocUHvHugCS8 ,g2bގ%}'4ȐJx+[A-`{ -C*&ȉ¾ ɘW';e$REL3 Y,O<Դ1*l&ɑ&wF@rC&cX5c\76|߆Qs|9o߂پn+7|3|7|3|77?dw0v4&^fjd _k+[)o6a'l&9㩱?S^SRHnr:D.NFl{lf,60ppV:ĶSI)6kE;ȶ@HE.:mzY,eMȩ[ -AT*B9Hd# =j =$%Z9y1'𻤋w`D[6lCD_,q۲,FxNXbZLVn{E-ƎX6BgOe~PzarDGt82uci1LUXcKuZz%dW5G59ٷKKK`P|CGfXOTv9G^XJ3Ⱥy>VG+ -HPiGpzeEY\r#O Eں1 NfGl}kӖ_>|18j|h,H{V*[V?-Ee)*^q2>94鲞91" -Jsa)r.Ubcôb(OQ)k@]QُfwK_\;BM;dZ.|#%Ab8NّH6ϷdEV~I5㏥\8YcʵHOe}j{iz˛o~;>^77Kk#` NuB(o3WrT.vO;p"WR>"]\ԻhNT=F&哜hYV?%$ eA3mNU(pB"lYS#)_ |7;՗䥎),KkU+XZ+L|.w5.V'eL\TJ2>̬ŋ˓Q1Ӆ1e&&O"]1G=*=09Cx"jF\ݸ6w3'.%&S]rl;b%]"dՍKJMMb˽0_\wmC˖/#gd.:>&.߮ 9i@/rۡ&&DOYt#ƫq$OSi5in{}BqlƗsQ5<1됼DDSmA -!>PWd?T͉jG&k~Yyj(cifea9˓hr*_7Foyu)'l&Ս4@T2+BVXL1b1$mi+ ^Ai碸ھ`ЫcVhVLUdW.MjOfTV,*ǮE/叔C:r}FT'3XyUTAnU4 -lr8&#ql]Mk+1סb ҽ3Qјr\zVjof\'&Bk-=>^T*-@p;])8&Wι,i_'z:2#ժ> 9dS8cW -2*[pIE!x@?#9xƽz75"|ymj⫲oע3t΢o#ڹM{☟]DYk -ﴁGK3d]ˀ':,c.O qy0z8h&HڸwPt%a:3܄C@R5x\MFϻi -ĕí޳ZU"prvrՈ#1rj9fCkȭhI6EV\ɔ ,^mMuk#yh -}6_뙑jFIj -l/GdIl͘x9_iOS^a&qY(O+d^Cߧo(L G1%ZV]vm rGcqr黤<3zIfWv\ӛ'2%)c[;#^<_7C)Xu(\qՄy"UUxI]]7+a"O?xe1˴b5Ǘ&jC)1Z9\^cIݗV3 30_MQ`˥,ȿ1C ʏ^c"A!U4QH2_j+JKѶYz%t:#5ʏ[ܽ+qbh:Ź\W"V*rxh57`u":XG5*#%'#3DqjS=Frpd-sdvU2_(J||@tٍ1cXܐԺÐ{y=?8G*UeL$hrDGBI2%qݩv#m="NBjuJbG㴈'E(Ef[ŀF6t9b,) $˚p W/B/-JⵯR±ţ5+7 q!/[6m"b2ututJnG%@+iL}ZX Pu'`{>F^C@MX1ꐭ뺂Bn;RAXgb9<*"Wᜍ]ֵƻ֣ZFM_. -֯>1p[ib>K:gtBU_uD̟2yb#HkdX@2%w -[@{IhB;ؙ'$9ɈNg^5M;U-^db.T@1u -3*Fk} b~,6fW/q*"B]͎k4z߲݃!zdz,2fUō[$i7s9FR"jodpg-r߰u2N1T}E2MkBډS#)zؼ/ 4> -;@Z#"B_JfZDt8yc%ˇ1);j++MDSli >Lop5ˇ/=(6~ HmKiQJYR0 f^]G% 0.:iwOvLW"4ccV%u,ƀTG)bka(Q,H $pQ"^lLda汰 A!>@Zծ&5$F"%SŅͬ`W*(+tJm&H$3Kzg _L_a"xil&ucn6|ɚvK0,ZtҶ_>;'ϯįj\ӏWV|7 -S-R匇GΐA0}T׬j4\A`sm= RWFN|l%"4jp J}9EL7ܶ2Hb#livQ&:c.v!d\0s(3=Bk$#@Qj"90-%2L8#ڎoY3EH-GȵȾE3L=H[nj7htƙ(6GwZ9Xz"b1z8i!y-{dS9`dP)M"[,=donhRGa ֮6ڦBeZ*W/*^Z?:DkU6^1ZMƯ#ymyZ{ТSL"6(ODF"#.n$8;|H!($ruR Dr98,rXֱ{Z-|C4m#+!1{M1܎ @ (֪ -_% dQ.IR7QGSaŦB@UCB|Wy\-tbDX6ߩ{{2\61{yw?R+"RLj=0\#d_(zFOe=mPyEÂ8:U%n]*zt<$̵.ObX$- Qeݘu >XGNPBKaKsQQ\pc4`&ei+W4w n)ı-\i'mєTN7<1@2Om~V=l&0WbŜ2ː)0nֱ݃9#3s C u$F$-\(bEuΣerO؄!=֬O"VvmzkNIWə`Sq+·YKg4ZN6] yhkB#ΤTd5K`X -JȆ,QHA &lx+b\HlG m;d<_I湨dOY2Ðm&s^El>{%5Mz4ѲUC+8S0*F%E!wĽaNF7s^|VͶ+1ˊdU{k"%[maLUR$߈ƚlcj%ꍲ!EB|v_~{ j)1Z73n_n0 ܷ Õ L]yG]>rW5q+E e RE$PDFo4QMTtGKyWr;osH<[W=cIg5!y2#]W5Ohխe!~k_ C,!?,&X|_Ó\Rhܠ-Z7E~UUrҋRmtF߹rҍ-孋_3YV2K:+Llܸ5#M<2lTS9k EeQҠ[qlԩL$OX7P au`bb 3adP浞?ѳS1I*m H5okY{ZĿO"dx/3w׹b#8jWjr_eCay٧=(9(b]^U0?^ysPz< {ӊJWd +)UΊM긁R$J@'ɔU}s{YU cO\T6rS*&jLNA`4F|-]ZC\ƎC1 32dC -3Pdf.27pQFxqN?jMmw*9\f4}\mQzBd:PY qݒ(Tkk3|5x'Ѣ -aѤ<1 ]w<:+.dz"ܢ] 'o%MK]QIEWK8ɫQU麑KgPZ#>R/AXޗ;\Hj&2iN 'r3h8y޹eTi' QvJi* p.UӖRŚzDK.ebƯ-e9"ʖU}T[*"&}23jUӣi 䌰_JSvD8'U+P;h] ļ( %P<J)DmcB|JLwnY^Ե5/ |u mgV\ȃ:y_S8'ۡl9礑WV6*9W'6T( -=$Mp -+bGh'}J-J0mi*WY  <\&Y%%ĄTzw0)x% -]CC/ pEy8x$eeQcUurәk! ^+\ȠcEyn>| @bh!iKX3mG\ŊHs0g,y]*Z=;5ZyMlK{ERF >TAOKQ5նnP|1S#5#F|b埝l̍Ӄ%X5/5.Ǘ.'$6MD_ilXبG"#Q֣Z7[2u[qEɓ C-yG6־,KT_0eXD ̂Bcd ̕81Z#d<~ˆ0JФ\m!ZHL$(濫$[rZ)SEۭP$%tȏbR"tXްgD?oDs;yEmr6+# -{aHkюL_s^{)o5\E (2'2u!ʎWVj6/% ,#q $Mya gY`juK†tq#(*>['V yG-:bdV0pA y<Hpsąn,mDy5!ά{X@HcLx[pH:aͪ7$Ru@$9V֝fF(th^HpeNP$Em`Ӟ6O9֜_yQ Ic`VR"> [>A 1,|W>I:hGa"<&JdH;قl8fO(V%,WX$) FoaHifA0va;_ дIoe) Pu51H k'gTp^?|;j۲7N{sw.Z:, ̅>tsƐNyjʭkPЏ:äeGKJ:|E5yx$C `1 !ʟهR -=|9BrI-c&\QC-?W|nH pX9qnX - 3۔|8CzOp`nK4$10kjӾ, ob7duXSj:F`䷅;Je|i | gd٧XSA|(/srٓjOH6KNe#¶f؊@ƻtf"@)&Xztg3m4g'ccn1dF˴sU;P@$YRx\l?ዋ*QGE_V` ֲ,q…[X-~Hw=Cݔ>5/ʃ/}$Ĭ$T7UbBc\\LcU:,yxFT|t~8r7/HIX\,mҬH9l+a[嚞+IIJ];bQR(餿 UFFՊ\5tzվX~*2I᜜.]i9JbSXv3yUD ;H3_adymvǵ账Nh2=8Ķ3yvoȟՉ&Z [uiU d _3D;vRƏb-̱Ft-_ `B٨߇L{XưmF_(D5ʽ lͩ8l}Ыi]68QY(-|W)-bnI$#t9gTj!U )FqV,AtkW|qutH%$H!@A'cƣYkV^ϟ5o 𳢅ܥFV#[8a/HChc!DQ&P|:+4G qfiQIqGbzEZ;j/<~yoopưEYMdq~:0]dkO#Η`$ѥ맦Dmk51)KuS(U"eo[5?Izi7-{o}./D_bl Z]fH\F70aQ:cu1QFW$8PngeuU&F AD3 x1iaH9:P(0Q׃g*E+H  E :0BL1!"x|ǫ@Gl|PcG&Xj)$]ň3 U3^:h*zsGD/OxKY\#\樨vtvs?/|7N; , ?pk^y_JpU~ɗ.)Fw#y:]CɌ$ĵZsP|3^YB(ΝW8@-+'7F͈;cLa\l Uf3RQjKM QZm0^{Z49r@ƇW2XȌx0qye;Y^ 'YJtԇ%AqJI\l[ }&Ck$ZLl -:7mOIA^da]o/}IK+#B'8;cH!E#7+ 1mE/H{ֹac毵?2ضetnڌqu*Ћ r@-niYtC[>,_JHTD)n#D[°#7&+^cezܬ2X_ E #m$-i&.m4^үsSO ï(u((YdБk6erymyJlVVoyVߎ3yc`"WϞlzFzej0`+s7#R] +bī3$Z5"#NW5-<ŌzVUIouL Y0Nǩv>LcpPD[x]1W(5/K69* sUMS0cӴЎ% s5+FTTku3!&!q'=ƒw %@Qx1k+^7wsW??|XԼk`Μ `Ioo JsU35+Z)}ֱ_(G}p5Fgn\VŝZrkHJ5sܮSr34IL!.LkgW=5`U;pnkIkZƣ[_-6K'D*ޥ|6X+!b6u'M-2HXb]V.C(~Hl9wNQʒcUu,^ȭz*zDSSdz -@(.bS2j1\\\Zgee"T\/<&b?PSji61?g%|P6(_Yx(^R*rP -40-6椆YP?HV1f!6 'T\oo VU HҤWx_'N:盔 -ռೞKɯd<"A,k L`넷.9wi1ȳ&PfʳZ'91æ^]lvG"'_R2ծӇ۰>u%>\zg,g> 9*cŠsDXv" yFz'"cXc[,pC,eRcH^"tG@`kXLI C4Jؑrޏ,hA`+")c`湳 Eư*@s22b{{:]>&*Mӱ&KtâV&r*x Y/ dӣ NÈf,#XeuuP+?*H _AaD1σVƢKCZmcg2oSӯVvӨf f<ڡF5j4X`МCl׍%7Oy\/ t&*MN> u5t%1eʸI H}w"2E@ccXT 9y}/O哂{T?u&pCZ;Di7O_tl%UpXjͺ< S,7V!Dre,c _ԟi5&v}8ŏ`a59eḏc]?*ܗ1Raы5Ȱe*WFl/,%kG})-UeWO:kiSL|Gرsklc#b -5զ %M\肒ӣh hkJQc+W#d:ioIL3qOy|#XOezWIZ ,^eb\y6oH֑-]ӵⶶ,WGԲ@=MTiTN!]oO_u\>-q\:i+:Z%h0s#.Yz~.2'\& -9Ac?{^˨b4ňt*zX/Ii̦A˜IvpJŋ&{dyZ;*QҫZԅÝS|x}iС8XaDJi$J殞T4[ǹDR2JiOM~iߟH1aJBLnV桕ZU"6"d˛>$M5EiG_&'_ymqRֿ uO}X@}!qTn$:Iv/Eb6u'TK_jX@W=7-U=nUnVA !+j5o)xHK/m[ǭ.lwֽ:iJF.iv++|ʏ©;eWd)6rԄs,&vl%6WVSRQ?|N)W:h٤}-C7x=U10qR.1/w.cG:V]ӷ+i[ou~;4 *w3n}wolb񱫏`ѡbĊaʠ*S:-4hiZzFHaQe@,T -Fir.IӱIS:PE"@USmlec*N3L̸0y\0 -;B-oRr}wxX / e8uWK/J$fŌ 3,ieɲI@U<Gh, t$z?-)ijT3ʈadKK1,Z`M? -DD`F<CudP<(I:Ԥ8 kRa1dɎQmf$O:s - 5NpN)W|p4' kS%TY[Ȑ0b4#2k4+fBٹJ4׊CBvQi4lHD-c'#G;ȳ*21kV#Ҿs' lكV 2|gZs\,QH(2SUi4Y8R$wm"')PY$Lg<T\A6haYQa,+0/y!%!^Q*eMkdztO/߂{ۊXyefEj~(RcܣƘo#7VJG{Lmȕi;4ޘ *LNm6% įc-l~1]Th툔OKWEc_<]a.'8svүa.Bq h#$Ɗt 0e0IH$wG40hc -0ioUuT]go!c>ŤoKۚw:#:--X_dA0j ,vMCWXdDkQQ'> *'nMD3?H9 ek*Tfov1ah08]n+QwE}uCmAj? L\MJLfWf})7HyӫS4+Mbܹnrή>ier{sNY.ً's1$P#eDH -LATZvQE \*N9-L춘V5.e 5:o- 4"+Mt"+RQVjG65-;&W2*^?2Ы~Y%VI'^B=2 ܅4e84Q7*BQLԽ@@i_c+gw^H~L i&y}'xbڸ -nVJvr[BUBSN|HWJdw6铍MԾ#fFtlr6z)MUnXqo+=9wY1u ڈoXr.;N$BL$:%{tWc#qvwԆp9fY֎4pjvEcHxV7xOWh`=he2UY(yB]CS46)ܥp_*,Ĩs6~ЏBfYymR;52%$%aBCLVԤE{޺z`~dH rꄃxDK -9/Pe6 -a~ pe8.<3,z)lE.A¨ڼx-ı -2E &YḰYU/4ud*J†-u[$LO0k -B:1'Mk̲%(Qcꠤ(L=YZ+XV,>|o2rcȥ&`8NcjG_ਊ+"@c'lQpƵkX^er#ڭsQDkU~(O5G9ɊW5UȊ.o.I3d -m#o7I|^;>Xa8e7,]w"Kl:U X{aHްhm -X cI(S.ٖ` 0(Iָ#J&)N! J."rL X 1|iq>VHĦ`2+xisTywt#nXAس"ȍTsQ|]SMGQbGu̩ì.~ON)mvtatꝕO*-ZJR^ <9eVTzHsNGHg Xi%OSÅޫwt9 -S~jQKȔS#rS[Ccj6./+jȫBK#ɥw|ꆲ4mhM(l`aNσc6@i4g92G+*"rwǬVxJmP]TEt7WKB`l.ZREt4)MJY$S]њwrjtJ); Cg׈(j9+w udIqp^)"k,у^(1{HSYZ&X|T}a:j.Py:y_OLkALOԻ5W&E?u>[P#kT|O`[wg\&C8kS6\y:i[u7k)r2mpjq!H)7R9LxMf/b0zU6124FiafXf( i]S7j6oqʻ8U ԧ,/EN 1=<3Pi$\Ud'hڶV8@W9C|<ÔqV.6 I9@ZƦi٧?L[qQGnjVV_X&[Xv'a|rȵ\5_ySpzタqiIHc;nr9_CV^dK,"aGAuPynC'"FZGOs%q=YE{&\FFrU٥-kvVoqix[] ra'}V_LLo^$H/ß$ȤBZ% yR2X@*$r Ops5PƝ(iCU -\Ca+qҪ:BcDIVWD(j 5#-EzcGtbؑ/#X> lK):]u}/Da !/"iB;;K H:7w@1$^eDWC]7 _@ť!ƽ m/*,` Gzie= -><8XbX~lۇ l6ͽZ9,ˎɾ&<7Wg6D5bs&?|a$$鬆ƽ`<*QtmCA{O0$D -!M'M5Xrta`G/I͒_V$s»4!ucVsd Q &thi tIlW5 N@d`& y d*]o\¸NؒW`|19o:lI cUjieKnsXs";9{xoJ><K -e$]EmT6Vr32EUl"Ķu RCUD$AIV;oxyI0",PE> IhOq]*䪪&JxbDPUN(6[y#îty}ůN5QJA>$H&A^RWqtXOtȡV}`#TmEdV`YCQ++EldJ%2wt T:`2odBryg$ד LvHn,^ʐɏq dr%vŝexbO\|[;{̪S>j tA$H=J\R \p.!E ^~h纂P[,udW%53L:tHky])\rXt;/ X*ss2޷xï=CݕOk*i[&KϬ׈Αy5V}HSa/s™0dY*'|-ʙ'l -zKdp;66VwMHXRA+Z >sǥnFdWo+HGM~@xj͒9{' <x:Dh-RIlk,>g-iWxsiGH8&|#GVQ=t:Y/BCMڵP$y Q['(U=#î/ʅ32Ż@AҲz6eqHG®( w#$1#P`GDtYHʩQh1i\+aJLH; -oO\C=]s`h %bUpKup>'h%$j6x́e,%H-"ǵs j -4_zѸ8E4 8Ap -$-2O0o3^0:8Dɜf!eCR)AGH[] 1*M{O_Y^&uD2!N;Kkh/{¶TƑǬ|4‘ i?oms[jT|ǰ`'pRE\]LsKs 0ZN+Xd];SG90ƪUJVYd\t+X rv&łSB5a5\Z'.M"3_CD{04 wZ(yc^vˎ;|q n֕FTsQS$_+Ncu-d zw1\3-m̃=0QԱ[wbLu)Ӫ9Т(6x;(K㫀7-ӚƩ^4Q7 ۚAkrf]ܱU20tC+?jSa5˕pHc.FexcVett..T1%U!l'rXQcW( 0Ց9o6j4I+B8P,~%j*ݹ,fGͦӞQ;FZzVP&= *VҶ(dJY0ƿWW8$vVX>3%sNoڙ{j:\h yG,s=tULvR#Jy6&4kP@jF!R0vHa+COڍ99}O'!'$[!ee'JTc9`U;J4=tH-#Od94Zڠ"lz6vibF^b9jG%}̈r+]&,9Wn4񱲦5hE1̋>x)^U#>ߵ\?ul2 `[-_D|ƕ:O)%43X[FVs`-V]Jg6(dpI ax[P+mITQiטǛ%t~F&VF3 yYvׇjXU 5 Ϩ@z@coK0\/-\!:,Z -DKZH$j[`@I)xS]>YX+2\0n-ŵ]ifjb6q@lxbu[=6[EwѢ0(z{&Q] &jfd&;*\/w!DDMnN.ck(ؿDOHH8 -Rtr| Ǝ,QP JWoV+a<5Aʢ4p VrF 113AWsѣ~ƵF$FeoT [,TEMa {9cDTgحkQFyR!;(AVP0u؞'`2hUM06CC 6"P8$x Ltq⋪`V yQ#L pȘ#)9t4s:<,)QnAdo0E^ ` nFj+x#xo./7.1wCXUo%X2(M[PF4!$"L(W5:Lr ťJ޹^E{M-# dŸ KDt#cڈDL|qXj c -൰l&Tw/hV!%"WӊW8,muWL6>Tuy"XWň <56z ˋz~xR,b$ ^uU ʊ}U NԀrNU;y; +@Qz\pSd9%c$W(=ǥtHc]g,F鏮tB 1D(%q+ʺH =lPd$KlA1JWT  Ceya-nk6([ٱ7%dEZk:l68VX4 XosZ;jsU孋$II- TKa 2s :H7e}=GqH]C`$=]8A&\gڲMHj81Y_ܸZG2E E,k dcVS*lI:7]o25}&{ȯxK0$aڊx{Hf90]o!#j~rcԴ<fLsq T-$aF3,.AV\z=mTѽ4@!DNhB( !2SH-bC_#7ȗO#k'|d8u7/ގ=[%h-W!+hh䀻r5zq$.iDױK R: jeASާ6o,${)sOTG2֨e|\L)@wA&7M-Iaɲ;TeZ[0XH {CNcٛ- D^:u"9qr)Sa,IL+Z75 xϜ"uxƲ}K٩|FPj.gVFE~jTޞNb"-]9S.Lh -HfTY_]-rӇ*)" 4-7=R57$Ծ|z^O&I&mזV@3K{5̺KwNZ<86ͥIUdW &00 Qͪeeo+n_N{WWSYHQL<&\nʆ&O@4bZo:l[TF5.LDeo*HѽirDIٓjkkѢ\fX<| ]:߸#0VߚR7Ji^Eo0FʺaX"Ok8 -f |,vf"2c\!/*yuv6P%I'H^y=\ǺPTr"o,bƷqtδȐ{1QMQĵ + -Њl˶SS֛.]Y2jš#fVr5 -y%Zfz~̸j\zYˏF.p,=,U-sM4_+dڲv),È8`hF1bȌ_B :" Z# -EA:&A"$#׍ X4*h R3ʸ1fk|Ҷ+M4ű`6,Kje&b2.0x0զS2Qd71*EyL]"QC*Q+^7/x -Xg| -I:utix,c,s2,$IK+`ֹJTyUTy>gT!Ɇ]#!dq7z-Đl5k G*V=݌b'[IFUe (%L$2N3HhK ֯koP{[Lp%,Ę)Ydv;Ċԗ6,eݴ,?l-tkkY҈:-<`1V([B4egi-$k/Fj19ie+'sz>ߣZ)cqLtsb0䭐S)Vi$-ΖqHwV*"]G  ]?T -gT|,ݼc}`tFs-e y6mc/%uM5=+ŧlYZ~2$Rr6d:U[1aZ2du`h ;#>Jٲq捖A+s2YlO}SwnsSj+dOM9V[8pLY뉯qd^{Ys[jD NHpH; ]1ۉe e&_!ꭍeC5ʸf t<!V2mĶPNs$[3cH[!C[i! $3\NGESMڨۚN g&W@\G47%9la9jcdIQaeX~z>NrUNYE-YХ5o#ղT2||j(e_z%*O Āgm!{3GRThD_8B I L)kc, ĥ2/Dzh::)'2cdPMfeeh#8 9&)~?jgr>nHSd>O(%&H$D+vɁ ]gf~TA2>L9*>e~ aDA9aȯt -@Ts'e}Go1ʭ39:DFRdbKj" YM>cֹba. ȌxpXԄmBv+>jvsx`Q-4 QgT8ڑLOnTTEL2Lz4|r@P?L1fW-d 8)* .s9#TLxxUmEpw$}hWT$|>qz |~ %ȉybByou7űl"y F\H,emգЪ@g[E\ -X6Xm20:\ycSm(v.ci%uStYY:hY,4QVHR#_LY3ce^ճ2k$6mBݬw4>ހ%*ʴqR*WO{ttxqFyV=r'E#lɪb{gL(X6U0m,xPW3hM:8zC)0У2tlb!:5Y 4XQ5ӕyFu坈'Ǎ R5*H'G0H0wxђLw:ӳeK&UοsLYylZP˞E˻g` YCm:c!@]~^J4J"IgOH -$ a -)qk,urޞE<Ԉ0:ZmHvGrUL}F' KŔç"fOҦj Z*T|+䬸B>io}푣ҴnztM+ToѦjjd$\;c|) O:}%r$LRꦵvg^Io] /勱-K5H)qJk8Hs"wL}WA4wrlQ2ZX W74ru)L"vc_6gdW'T.;`tzލ"+7% Pzz*y8 밺4x%46Ӣˏ$SCJ %ReR/-+:"&;+_-+H,,+]|)i~#vL^]<3H]E=2{&bq+lY.d)WIOFVږvA=SN.y`Y9_A d&-}U+tVf@l++ Fc! -1G+UpEYF*3l"0hj(pd*|ug&ZՆ `ajAXd̑ ﶪvҵ9 ) "a"BHHaG_`歪Z`0c=Ni3dlx "SI]RK  C!I,9&22(k! 3kFtLxv*'FvMI(R+o$iWU9qh݂ m[\1ޖIIlD|F)%T"zj+յ4SFj;cr5TQLdVN8QrH@WFCԒ{d4,Hsl̦两``CR+{ʳH \KGD^qi9#ʗycޘ){5Сuǣ׶}C)ea.I#IxEyK(c6Gmbʴ|>Gg-РKl{qE@@ߤ)w#_dkS&ȝ.I(!Id(jgO{&(Uv$ċ'-Hd{5sWvVãj_c LPj9:R./]l#Ϋ*]lg,l\䉨S2=Ձ)NQ>a"PtRS u ㊳Ln`m:MVDT͋i =SJUofM8v}X45官즲X$%L8BFrM;Մ -fkY>Ч9氍4dCeOƆQE(+6ԲeԱњu=QM \@C[dJf) XP -Y.`KR4Cy>lQ0Mz,NU3UsQ+tb 5KɽN5]ITV}j@Bx]>Ǘ4oYYL}|5A *IN=r) -kiQ$ٗM'6Kda;[4`s'$ -tM~0{nEFَ35xZy˔ )ҍDsHD65ql,Y'#($W4uD-!v)M*,WGfVaædmskH;D)r‚& pҢv-ԐY8Q;_*g!.姚:GѩME .f PWTFַy!47Gf  T~Qe -]J$J'=g2ٔ6ccF0Y~ӅL)u78OH5ȎH6F%3Hz.b25B҆!e[٩eJ.~#A~&r>ťWgh%S :La) -siiw8$|nV,8͇ -9C@hCm ;''|UM7"<lts/WH18~بQմIsB+'`ߒ (e|@ H;re9%\I[ş )Zm Q[Trf@62,],+:qXH$ M[kFv&*oUZ ~-l {Ur{,(Fa<0r=x](Z7H-]=?Ѯ~ -|rE[b6R@X}h92"M&;2,JOh(?荫vr#}{`2%1 .j})TڒGR" 9E,r#d]Y`&"7LuYEH[CJKO>S72fRvǃM=ʺa]U+Y<4i:-f}Q[>sTFk^;ʶ)N*ᘤ FT⭯;$)xpꡦuĐ(<VLk)azpc3R:e׭Ҧi¶BP5#Zd)8IiJTe\51S.^;QD,a)r&fo-ReZ+Üw -qPդs+jW 6`|*% .vԦȮ1=-٩M!s))8XE/RLz#ǽ<+dH$X"+C;Nԫɀ\jMװj/I>V&S/4G4؛Au9jfMEy1>֥i͌(3M"[j'o{p5!5 uXI*̧`'\w ZAjf|<b RiIȒ_(H"0ɖ@eCݩyDnx[`rI["I$<K@Զ3'4c0(:vIW'oݤq=Fx]QF%dX;ǵ͡ -IsJ Noe8oZSzAEAE˥ڦr!)»ϟ9c@aGy%~YeV xG}u X:rQUk;SΙq,vxW-RK,3VP#N]ױEkR~4|:z4LQ)Il\;`2<X6>QmoF7mJy%-L{YUxtJF5 $gj+xD/nBpēaW -X-Hho#bg$TALx HIKsoDTdɲc7!%#PGy,M{.(#NI?\8Z@8A2=xJdHT:hɖƹ"窲/,7ޜ֧V`{EYv s.` 95qזO}mz~|P[](Jq?b4M?SEB-,P\,z4Iqϵ( 2Mdmar(b&BsGLL+$X9R//!tvjY0eE JÛqkW!ӫ,-b>x!,Y -z=ߨxl.Ʀׂ,3 -T{`1fOeeSfw@G>QF[] R7W6=@;eR -0H]kVXr=1׆$`\QF"MDw!܊ֿ1$*D\ƱX1MD*v#XVcyYw߹QQZj#[TcwL8~cV`rvTDj/JS:bUqk8c`ѳE"{B aËUC '&+/oOxp߆Um8Wr,.dMЦuk2C2UmiK3L$YʋE}1n;I`!9o2Q3wqpNHr& P6vFlv}mk 4koId2r@$$ SH'3X1W5y$ .djeOZ'Mu2?m>]l8ɉh:U?ߊ{{6a#d L^5_8dU9o8qgT%ɗѡ@hFFIwq&j큽XO0}I ,P<NraM#/Jr%k[}l͛k[/+3f쉜0nvǎ"ws3:AΐrbcZ ,8TR6״ B -@9^/ 7}^<|{Lާs7Q~{-s\UP_9JTM}^p_xXT3 ].;3Jjک'|l̖Vpr8to|EDF߶b7Oش D_y{K7oi}ƥX݃F\$sK6C$y/e -WE;-5Xaxo &ɵJ,` -Ď@/vi6_2S*ډ["~+my_L^ /LVw.`f$z殞oIv+e:qͦm!ۍV[/mOBTVG?#M|r '[⹿%*V~r]jlC<>16L2^K)󨅚]d9j9f?G/-cR%dN}=٦~H}R! "*Rz}b >W|,bmW,MFl&l#2Srʢ&0VN)={ߩ%e-ZAvq>8J +LsQzmF&z'/EY=> &8 LeNArC h/5D7(٥=Lls$O+5jsW2]DHԲ9#~z*#eHʯM“ԭ_}o'j;4ͨţUʾe?иZzdܡ򦋚_2{uoSMCOFGN“ԭWwo}"po2Ѣc{pɹ@E3)Lgˎ4K.tKmuy~HygrFoSb1z,;9:Dئy]Ma‰vv,)V(b %),R-1ө:sSIO"tVF- kHbȐ*Cֶ'jʀ2;H|4PoeUY3v2HxFp^P ձjڧ*Ӥ嬭[gzO_t4ܵjo]74ʐ0O:+0X5:vfis$ٗ+h+({=< ӬctrB՗%[;h7]0Fƭ9K@$ڶrLfP6J66sDy;w stec-6)O8^&T\%DR@d,epM8 -TV$OЦmHq|lz{g M0at PC{={7Ìn2*96BQ+-4]v=-nZN@B>x?ʕm1Q]SWMM%5y5$mQβ*rPٍŒT9ܶ+O {Ȟp /_,PzDLO ]A[ODj[?t2²d'6$xiq|: -MdiNDs#Sr5͎=Q3{:#ܿ+}BʢkQF䀁 ƒƽV~A-𑵵i+%,<m\DQBpz&,ud39/)mH<=8wE(!!k#I;O{F=-hA%prehn -mQP3`%Cɖta Zh*.9A%1uL- -{q!$HRsb -v#ca a -1 $Y%7p(MESqTDW,i7p'?O.4nD367xtC"f@cg-YQQQ1$Q3y8G{y#LSUV\&G57neDZܛb^?RS9 -zDkƌllB"1 <!*`0L\tz`ӉybH%q@3U)[D SPY !|8ÿ:Pp#s*a6?E  t IRJ_Mcs2P^L4XrV1cL&*c!<SXUC!lS1!SE%2Tw壂#[;"YdPSStÏ GC5xM4RH8_ق1"%es`0}p*M+J ֵ裍 [\X9Kȣ9.Yԭ>tAV_>X -W֗j筍^u{#.c.Rp^ab$ Q -pض3/.XVǰ YRXI(r;+dY%N5"+KdŊe}Ąu9d(Vz_f<mi86Rn[͆9nPU(Zg`HavjFԎ{b ٶtGm͍̑j7ወ֢6F54lGs9,7FrEkڭp K-qQ.k&2$CОb#qэ!I)=rlA{s8TxcJ#ɸ*0Wb4pԍm&0G3eF H҉hC+XR)GYjퟲ/܃ǔcsbtD]m&Fv8o/11^/wt]4KNٜlZGKFGpMH E -kD!\s!$TT^Ʃn#\1=Kث;AE׭!2 :*&uP{O,q!@3WIKyOEȶ#sB{iK$VtW%04\'nHrJBoNFIEm'!f"'Co8h21L02,A~fy,kc!ІnkG6} -Ǽp֒΢.v)H5}` mQׯ ?݌dY w2XfȚ ,v.js&܍Ɋ)8YN 9P@f.|5os*ڸ8,Ñ9۲*Ny]C$g ׵|Jm67}P -X))<ծ(4J[-n#ͧ]/D&N!LdT#"rc37wM^([vR+1c\菨9Sn""y {eX1pFjG&˷֣n(Znɲ8,z2o"ʭkZkS%цRF#j5?|s[~<ߗ<ۃ#yH7y4DUFjzTDUQ%M5ɺc&"8*"znֽo3q H^fs#xs\j9b9A&9QQ1&ERX V7$ڊ^2D160#a25KGq.H;w~Tj*|F3%Olb x(3{69e'$ -C9dž%Adl#Q\ʎt~\*0zNssAN -Q#tn -QTO ۾]7TT|ݦ$tXF2pHHJ繭`ΐ! B') Gd]QW%l\ꖼ6,́"YLɒґC ΨH<C\vM*6@3ȰDtk_"9Ƣ5ą+䘂81C8y" dr -{;ǻ᝜~K#qѳuW'&L j*&s#9$8g+&h1y(~pi^\ck ) >=p@~rHXp!4mWG!Wǂdd>Ah!0i B,'CXDo(@ΘXvn;q6๷n;gÎGQ_wXDH-6+lNC+ƞ<vͳl6l6 mfٶmfٶmfٶmfٷ l6ͳl6ͳl6\ f+3otmW?V4!QUBVM#a0RgI$kU;,y"$ Ey\F $N -Ѽ|f*` q1?Y UGNq#XA6y!l*Sc+Fkam[Bve!taqAFR0q!d#V!OdƯ) k -~%)$`D3'=~㕟)͋a"jYp,l{.Lcٶmf6˄pIUJHܯpJ)]rmA4I)k7I()4Ϧ1sQ{Z.s&ԐV¯\{ӈܻ] 5g燔>5O WNVβ>5OɃ+,ӌV׵3Sz .[rm7ƭ1߾ cWS||j6ZM)N63ײj"WE1˿T4˯pC3 5_i?>\R"9ګBF -$_8; ÚGҲd4eeg/4niߕDۺiKnjW"ڽ?ׄ:O OM~|t LWQAӆMw9?"yI3QzˏPx_7kY8 vq>}ͳl6mRzQrp>ܢeʤO LȼW(eڧ4Q=nȶLe&\\ꕘ#|sSPLhLQ_%'*V6c6\.r/ɏ4˯Wb_ '>j4C;mUAmPw/=, ? iJ$=4畬ի_)U13Pļv~.g$bZ5r~yjߒjό,HS+?O+}66½@Spצ'cNpixU7]ޟnWNQ=oclTKFLVљ^<)9TJȻ |2f2'eToߏ(>M}bV iǮiP˯\}R _j.%{NjT>"Xg2rc忩ȟ$ӟ)#/_S4\ҊՄM5WGQO-ToT|qHVY;4ߪJ'fo,yL~Wx٤HݭxJ8 +h}&6d__'?U#o9WWB=>>Td<=Kl8CTC/Z^HӱU|l\S8'6RZLf͢.љ^]q!U"t ~;/隝߱]Z]CvM2w2G> -tMi/!|3MQbw)7(3U|lLTeK٪27$O ~.\&E4I?O?ÚWdeϩ'IyAgy?46zEi/ ?dJpef?U#JJ_JJOpMC5٩=IqߦER>^'1?lyy#sTl^o{bh'L_z@]Lsrv٥|w5KWf&y-l;9jK諒}^ _n6jO#`M4-=>^TzTl_5+.}R^A6Y[".;+?P#<\ҋEK JM.=t9meMC$埧ߦW揗~/4eevHZ?<MGi5pElpɐJ6^.jOTrЧ'%|Ԍޝ)}.&TzԾ,̅asQ7kSH4,?%*6'BFL"'E˟MksK٦Z >n3T2"4嘆-?yn_DYT|\n\oe -ć< 2?3r%niϖ~Vo)a.j!I WkR0u~[zIӣf9e2rGY~+ref,M5o燚CfL6,#+SjzNB(|vjD_T)}~QH2gIKQjmQ3R2AwU?lHm#s~j^ f~R.?+rңeצkW4?4.^|VjQQ/4>jXf{WNe<7Wjt\LofW}@ yOS4deߪJniϖS8V?f$<-#5j] -B8kk%MC$eKߦ__+4ev%5g燚C67/~jzf@Dy~Z&Sz\Սp)= -((7%fe'Dʯ2Rr\5/ lY=:6UzdRq}XOӹ3 /=6FA4Q;-6ni\8yY}IWxY=b^=v*Ozdԫ'rJڙSܺEO3Oz`[~Y٥2edniJzߨ(?4,\(Z_+Wz˂<ѿ n4̳MIbVF'`T8$Kfb'(eVj4IYm4M5?DM?3 |L"<_-)N.jp,2ҁ~vVjF34RSzrFv7M~jM> /rq}XOө}4Kqr$?] -忦4,߽k}IVXrueKWߢcO(=A0^iVSzTlҿ瑗+ |sM^]w9nMC SVPa3T+=:6E_K~/EvT\lҭ_;z}NIjKyQ~jU+1gVtT՟hԇiBVz\lO"GEWhO2Óz^jJ\Ӟ+?QJKX鐾R>_xjTv/fQpgH]0[0VWE$d?D|ɹ-OPMꋚθy|\ozidvD~Wew·5Rnrб͔?9MQK|oni) Y_fZ*&ͫsG=0pbY8l͹sH\٥]ٓRNO]fTߚKGOJ/˴YY}MDҢpyWQ?MOv\?%*}J.jD%UCa=/54ߣNVb&jQL_ l8߂fGg[fjN f.[ߧY0Yr$d/PL_Wr+3Sz#VdӦ;)KHz/~VsT+F& ? 񫶡jeJoJ[/ |sM\_ #f ݣ{M.j>Z&_p󪍚u۹(NVJw61+f<\E5c3V2nWi}Rt,uޟ5S15_be)%PzXrwH~iVFE٩I2/|q/z&7+sSzd\_S?ebXn+<烈!\^[l,|ҿ;Y)'5LLVit稦X~I"eo·5OG<\FurOJ6GC'.=2viTK9k5=O)?+5L@>iRL!L?ɑVP3T|+rѢO(}AÖ!/*=*>im<˯T$ܠ%j?O55gh͚ӡOI 6_7RzTlA/&,(m#4H?0U+3GNjRp+tTuT'%OE^c5'fMZe,R|}٧VffDc~ 2yj%GTFvP;"ٶmX||=~K]deڬOq2 R>GE'.2vio.iu| -J۔2NrMBvdF -?? r%e7G52ec~~V>.4MmghK>joOuW%"xA 8\'QTlC+$&"+?L4ߩfGe[9>0GFjÙ>WeG>a!5YsTztl3V~xy}6VPzXr/=sOzLlOՙ~p)#5_VLerfxS`\% LT2MR>FE'.W5WL|3K"ŖcW&G3&w[ͩ_G_jf$֪҉pK.iP yM1M쌻IyPiLK5A-["ս8i?GK>j_Nޙ' -}1 kMI@p]M\PRÔ -Ak+d6aiUGѼ5\AA lȄ2p1Bd~-"(aʋK FGʲ!4 $i_UdHTu2i j;_bN?ռ+WL.<#&iGIOH˚LBj4ldQbKgҡ-` \3KH&O5g=`Lej/pV W'̚YVH͒["ǻGG͏'\ףʠsdYUA<DQV0]!葎ұޤ4(5b#O!qɣ$1@GD8 ӓRQUĬ:Z,L9&@/ #o0VhqcF$c+w" czh<1#(aݻb g*4)᢮툈wf3uotrbinyb&+|[Mk|pݓljmb;ᢦ#OfҦmrmLۂ.mS8fnfةn 7}/w{+W฿[/*/\dTT?OdPIF* D9(3zt6:Qo6Q5+>+W;%`YCGR qE!%o69=sjc⅍Zt#Sɓ&LD)C(N`;c=l+ a^c]&`FS丕.p*u[(0S[r9rTN -RF]ҙ!VtO WBeS\gENTdP6%rsZ)Au1kdw-}39㖫ny0$tnr6i5"ve77njqS]Z8QV(A"ģ{]?dd(#Go(2ŧVO:ؙO.C!yWܗ"e=2ձ>/.vBmC1VRg -r?z)e7}E .[eUfYaȪ"Rm-bEjo7G%&cV|OK5#1 Lܺ{; G&LnLLq2RHze-z8kLg%dD~{ncA7Q J8uÕ(ki7eH 4}:|䬘wH_}m$+fȐ'̟ ( aSHO OOM$!>mslZ$Ē|rycXz4`Ѳ4QAC%pjㄈFq)ƣX}t:TP"F8ʛ<򌑂Ez7lO*fة"p6"fW6Ϳ?xO -gKNۛ -~?lOC -"2yY=XVcpZ8RCAWcgf@tI 'e̕Dl|I2o+`dÌ1ca[0rƱ.RX 4ϐnw1@7r?hE ʒG|ɯB%EǑ㲜@SdڌP!:` -R\"䊹 -EIk -7ͬ\P B&]yVR%,Se^(h7Ó(^D%tз#J?"$f c!) # -H#I%hǽpo$hl<{ܰ6l c$7&a7Nn^7L"A+Gh|$D.P/$䗪2@yƭ#pM덟>RX9ILBz ߋVW/=04b(d4C#0&r5[Xhi̧jhz%kpv0܃O5 XFk+j6`.);^TM:ʇ-Wj9ybq#(X2u+"o>pCQ~vfɢlt2"0L'~iodD˷"՛`\Z>CFث%\Pܘu"ًTu ] GWR7m|xcjW)=$U3H;4.~~ץC0"ЂP;#w M.;2Gi*@WGrߦӏz5k$ OuS @SA^AUux9Qs J'0mjci =;)k\8r'`;{9$ 3h؍-X*O`b4=F5ʋ-.VB=./ &jFctс-;$e5ʋ.},_reUO=iO젌;n6 -d_z_ 4afܰ -%l{$V9H54OT|+$F=9C5]dD8\Z匮+͐y' <\8VrPܷ#K<H,Te {D񰫜78*B{gICG@bLBG -j$XD%b*UmPR d+Qa1'`IXR,b;+6615ij_L"dp 1'=RdU,KjčφZ^j?\;v߅Iڦ>FkbdD~P>+6esՕ4d]ײK- ";˯ymdCOӋ6*Clf51{hF-ZapԉYL*5mXJ O,X2":7.9rmPZ깯{9EϗqJvOA4: s nsr<6/vLUr4%9E1[8qH;ogk lI_.Dsߒ w XI"ܔtXEr_,;̲)_e$ -ioexɒ,bE"r*q&V+mN7!ʓcۊLhfˢ1UJydbLlse`J\ZB"eJBɶe;KDckƕ,$ъ[Wݴ2?QU-iy<;${ - -6~La €ҫ?|}3%?P"xsWt-ǖm \D4 䀥Q-}6VFML&9d/aXGdsrrb30c@W䃲8^WIN㺾Y{et#*ꂀ$nNꑕqj؏ly]; Y OnUGZTiBR5rQcʈOl\K19氫rVOQyVư#krd1JA/C_ UU' H ~IŐQrKoૐI%DQ\]qG K\9 %fNy \?J?*T16#$LܧhF^$HnqLqB a:cb2UrCw~Gb3*2'L -qV)v[#d1h!9\ܺ0o*ᅭfv1n\PУ-*B{H"lO#G>can\ӔUd$1鋐3(RB[F/5v\Vֽ"Qy5~CTr5dҖϔXQ[3al}r2.j/KQ.3XetI\=rIgYJTA;T*f٨xa%R`Hx{:kt%@Ԙb;QDX6wg\E 7fOx%FǑlJWoY>J5<*6Er2ʇ. +c2FtIJљ%&p"ͼd7:,!3Z4v^5W*@/+ Mlua\2d>-Y z&Hg\ep FjdE4,液:hHڶ.^q2IZo{"?X Zm1=}5*ZagUo ݝ;R|EX s:REQo4|yrFa/=2FKM8 Y@RXlUldbkeYms1}-],vId I.4 mv75:*b%~[ -9U+VJ; I, b*H0 m1t43f'y -L!c2|eBHAF@8X9xbTIg`<-% Pr|3]c ъӅ@c+@NR - ʎ9:(zs&$6v+ GAH3 2bF(F @w1,{ " - + Q# a xϋ I#JJ"29][G(TS$;W$Z6N -@s\ &)3FplHL:PPp!_ݓ  *9MHPqFs|2ꃈM$t6hzo}BLl0L<@Q_va37o2m[㘎TUQ1HW|3EMsˊDLT^;fٶ'L6ͳl6ͳn{[qۂp3l꼽 DOפtc#l9“ --ㅔULV\k'#>1?$9cUmo(ěi"츊{f5{y{6?L%61gj0` Pi% ^Nx)"X@ LR*ܱ̬86Xz;d%U -p,' -^8R&W7$KY fG^A1J!=\!0i,&)DW)$CĐB0Y:GJ+F?>4$cRRCLFb䧊&˯NE -ȧq -8}pb -|kW(Đ9-C7:_-JJ)e^hG-tgX;-v9nSJ;-# եASM%,\WnUwNd^vT<0(=z岳Ow&^/:ETx%JbjOv_5k({ Us/ljֻဎy-,K2vNmې - c շmMSius:7j2jTSE!r:nc; }s<$8}H@߻R;/|)Ts2$ٳι2<&Syb,, {roLO܋sW+;D2oQdk̭!VVN.D_F~ٰenu򶱨Pq@ݵ SD˯\_/X5366LBME{ -7I w+:tT\sƫ9!!hk2/*;b=2nZ -T`a'}cn\c^y'8s7wI[tCy/7sSO\Ǧ?ٙbeDo^T=h"1|Ɂ\<3#cWs7\Gz`YѰ%W5s9}:ss[0ԉ Ļs1w"s;B<)Y6Yy,c^ 0NIjGUd,޺>AJ٭hF)̒HлzЮW~} POv$p (X -c19,Blx$>&!q9g#c;(|F "l1LsiWئܳnG9uku%f =GV 9O@H1c0ye 11ucF>BF͵{|;]Z"*Q&/ tPWC'hMmQXXw[|fjOLb-ZAzw.w% 놆kiL&UG_TCEKD>vwBuk7$IjO1߾H1ș%eyNhQ8P恕 W\A($kQ=Pt SJj(mV@]A&V=#ZO+Wm:BHC/埧N]Þc$vfY_=095MgPo-'TrCsDLBM" ьkQ/DD['G'-&527O`_ "lVWo --n7{IAmQ`e\<< rP&aVtwЬFƚgI$/Ė@+FR1QJw0G #%'U8v8b7GD BA\Dw6 #*3b#`gLr`KĊ8ҒN~##C$}Py]l9+L胋 (4p*D0sTg!/wNJZ9]HT'W=Of'!lY`J8S:yl[:,&9"qr+S/럅{j57o P[Sr"pVDDT6b" {%s&`ўMٜƍ"yOw G#T҃i69Ltحvc-GVHt+Q\kT0Λe"tY-E-;w̺j&˲X+W:kghV/+X$N#ɒ-2s|L՞9)&?則l -*&YKn{,ھ{,sHՓtsy81 hEnCLb@Ű$DDA#նMjs)@SL@N.flS9۬pioZ _E};0X\uo!_5r*Q;>;RV<8R{7Ȼ6/kq %S3cj4yk$%G\+(W-׸wVrd赭ژ[VV{1Z,c?  -o Hc+$A D9E i2*vw'-rRa=\THM?1sUm* 5TfJdD%mf^峝[a1B$v 1rUbVzG|^몺G~|#e0",{*1S-?aB x8ͫr mq7Wlzc2rQ#|: -`pM&L&PAF>RXh;4ɛ243ėJ˹X FHjZ?K#q+ .PvW CX&."_>ݼt~k C9FjH]|Vje$8-JvGN"-/ Mڧe\>Dy<]x6݁`Q7dEvdiG5Pԕ e'QGk SDY8v#/FO(WHT1HXZpw\+^ҙ{@aoe8wV;ő`[+W@bQo+B'/ Iq=t"'t>#n 9c ͷaRH"#O&1Ws v=SlXUEVpQW|F&r&r16N m ]_&"fW6χbMޓ*c vE*|sUV"ώ*|sTsW5W;jr5k{_+FcWb9k^ .pոުKՌe8\#-d v5fa.CV$Gl< xn+꣹kTI2LY8bk约q$-qhV3TSXfJdsl{[ y؞mjE(3,'t(."0w-g0B둯طd},쬒L -ƙj6͇}co \u!;0žU2l۲#sJt%/sMV@\ d)9>HYpKW+~ܴNV9g ӌP衦i/ԀgUVO{J(X1[P=K;.D A.aPq+ѶU#a"5G84uqk$O Յd+e dEʍ|jT:쮘%aZ/1hW.ܽrX.3mF\TPe3ߊW*bh6i(kZ9 Y֭0k_=4Ⳋj_Y1 TɽF]Ufgg;Z9ePU7lŏl>J{*\R)=Na:y;J;5&[+C1 Q_$|1ͨ%j3/zB)i]Z%)!.jH>E8c|-SY 7ˏDU/k  lbAyJ%jc׭n-"DTa{WYt "!eTW-pK&<`x+_w^(vE\lz} ^CV3hc֤A Ů{$D޴H:-jen+Ǚ28drX%[*} n z!8'D9Z֫Tc؎If0B4k`rc7#D)`$e{@CW/GvMrB5YeBIlpZ||V=nʺ^V 2e:9/,W6Ǹmπі%t̘&%Q"_aTIfhǻE+@K.Ob$BpyW1:.T|M"g@Nx[vERg;!"wT{is(=/W(DSzTOD}ls([T:+~9g2!SʹŒ-/gFFi7+2_w,rQգ\w omzX}w_SO oh&պ."㲀XCy@+r`ԱN4Ƅ_Pw -ՋVav0J%V`+6~_{{s6ͳn+6ͳn;{?Mӫ,\y()3#FVK%X5p66#`k%|䴒Y8,oUdlH'V4H6]N|>@bId4-9} zVZ|dQjy6ݲS(zy{ [UUy*#_! Jw{ d[ǍsX=P3][bZGnm{7ѯ!+GŝV)'l`d("tpY8W, -]y=g;e 0 al@ˊtڻ57V=a*"*/=!H6l#6seEDE^}E p-DKp9z )Vg}Żzm H9v -|Dw@B[JeV.-G{k 9_Y˧<)E' @ m - FJF0GkOFl=N atj)iIeW\3#2'-uX !fΦR;좈#)Igi<3qbYCy=:oHV#!OWOԵeg؛x/ gtru#^tUv5qwh~Ļ2bCR`,:X%K#L _Tu/_wZR2(7i=c-VWE[~dOI =+k23kg>2䢨bH*Q kTŒ9@9 vfxF;uzX׮چ%s%.iX5շҮd#`0` Pdst~!ddM:2Ĉ;5 @6|DC;.=J5B/5rls5,=U*Q5<$GF+r;Ǿ|UgeFMjoZf_c1l2@@SLc[Z) YAO`4#QygHʟKaT]M4Fpd^+Θ&ځVxj%)Ɇk[,u!܁>󗽨?ML!QU+)QlNʞ_˥eWaEBխSrjo]$eZ<$dPz Ҵ*vќqhXG!^:KyZ'7/8қuB~ΠܰGsLˉ̓)e[vֵ0#ם>;(\'t -R߉oi1ђFsM@rܴn6@I3}nOXo!m9;i=RpWoWV+09\BYՍ-"KY+2$Mɖ*c?%joyᢑM !`ӡuv -8BG=c= >/""SONY9N>ezM]@<6U$_O/#ui R_|2,ƱdGI\ -);K(,}:nRJh` Sl˹M]<,ڿtiDTTG2Ej5Ư(W Ή$f(FfL]Ev--3QD++@'ԵʳɗpB*=>j#Q&n 0K4xniHjo^zQX ?QGW"6DJxi5nsL)=B2?yy7%nj6! C"Y33J'NQ9ROfc[rξ[&j#+>'a73/D%}D=zm CRŐ6=6Vaw%H.RN -.j?F: -FSO95>VwMX`u42 -giGW3'~A{_F zS4PH9fi(X &ݭ("pǚ]AnUF]"^OWWGf]zMԌem6, -][So9jd$ !VvR%Z,qjrsYf"Ρ[s ᨘx10mO!څVNt b"D-:EFV"cTq[[CһRYVs ?"אwRENc2TSƦ ,)::\ث4,CbiPi`YD%fyN"ub)P`]݅&?5/#j ՔNK$&A u-6d7P/OksAm㢯\h",y#X@ ,zZFYN0Jtuv dYI(FyA1'8qdj#Zn+mfɋ76ͳn;fٷ Ϳʃb|6{jaGs$ HR%b4:YPTmW?'OC*LY6؟Kdd䰏_?ῲ=Opb5.|E Jc%{|߇VCOc|o?ow"].A |lg1Gk Δԁd s[~;oHb#3P;2JOͽ˞y˛.l"ls6\sχlٲ˞yٲ6ͳls<7;~ch\58QfyجYI<#To32uE_qo&jU.jRvUSKz/M54;5Mu-/-շr9ⶽ>-ՙ)p8;g&-ݶ6gcnuqkĿ]9%~[Nn\+,X_[U"1̵W*5T[NWU[ՏkVo.>2yc뻖"ث*5Y7Lf_ډWkѷj{ay0w饕2=Q7!Iv^6/,Dosq,o6{ܺϼEg˶K}n%+iR6m,uެǖwT7N !1"2AQSBRaq#Tb 03CP`@rc$4DUpE ?~wx຃vPy[r[[p ̿!:'_s~wzM+]c&b +zB5tߨx#{˃RaZ:SoOcƺ[xRrI?99 0x.M95,,ZG4PbXP']juHZҤ\a#cnA/ysd⯟tǑWQ?Ozh]fq[6#ڋKK9udsn#aqX}Q^\.m`kH_;X4!VB:C5›Ieru,HCaMU*~{LiUӼ i,LMC#.+/C Mj``-Sy>)$b&2`QrnWr(s9e)0e759]>W<*^΢ekF3ACuwCN:.<~=|||S9cpG]pi>Hjbܠ_gQO7Xi EINfx+Y> -4F'jts+2[o]6ңe.??[Ԭ+f<8x.\Ϛ%j[rß܎ ]Gyq]F .m䴿u{IBܳ^t[aϬEv!s,[p^ 5 ke{^+e+Uv ? O r V,u7Z5{⛃tw]} uJ9cAXTmx෎[ԔrڽRܳ^39[.. 9Et,yj/~ഇ*|x,M.[> Ǜ=6S3MKPziϮ_`GR#=tJh -y2а|y ^p3||̝&C\-inӊKѕu,ǒɽWE1/(f4 XaY ~MM 5 I>)2\&Pc kZQ@_!z`߄M(d?Dţ Qp2`(Ykׂ0%Jf!6\!fCj`rADKqF7n?^Zꏽ;0>o~I{x2{8_'PǢjWbzh!nۓP-TYa\&Ԯa`~]iEQObx|?V%=.8Ի+ü2nAByoCth-Vn'B r N ߒ菡?Śϩ_ފlN~monuB Z/{]㓫ꇴ?`%*C-xթn&PѦBfkq>wC:B"3 vy+d,)ge ݫN|A F͔`jCGhi4Bi }4bvxeCj_%c27wc gh.3豅Ɠt)s BB 5JdXZˍ7Y+H NGH0Հ]!)䌛4Y%r7G02]⌹=~`CصܝB9i5t[+]0x iS0!\B\J0BBbWޛ_8<UojUw{]Atͥɹw}V$zf𐺁nݻ -\+ԂΞs?o:2%tu/|I-˹ v}Wd܌Іh䗮+Y: %nUo|\VyC4/zl zQSelosI!iߵu[ hqJ?.88 ֣six'M -5u78ryQU&3J6H L7KЦXZYw_Hx7#-C/lp[MCҷK.r-2 Br1 M<|S.6Jam!}aر[殫xT|:|\uPWJ?˾j2CWYZ3V:4]*NUG.?Sk ]7=7wnfū&.l,X_wP?'JAXvUl;:A [,X} dy5}@BXYLv!{pW:!2a!tptf3Gڮ^X Û.~&QG&9 9 -(-V0M|Ob2 -V,9%h]L-^hG*<ۼQkl3nG>Qq>_Ner2NO~qغC^·n+WD+v=2O7d]m7/{zkޫy-VhIieC'`ܷ% ׷4yrݎ o{ -=3$eKqzZn]vt[kդ'{(|-"_m^CE^±ŬZFVr .>hfd?+QӨ+b{fvy/*LQ_iORX֙M~+q3 -nq/o?`а9&Z6~kd:bMIa` dd:G&)KP#sb򯵓Rڵe-B0{[r GSdG!2`Xd=lɣ7#:ᐐahIɱ87skomɌ#;rfpl -Htr ;ԌMe#ea9$K(&EݫB!DZd/?YjL2Vr ,Miɩ ɩ h/X&!j/ẅ́/95!+El衣]k ?<+4?̽u9y-&,wzuv[p]P0{?ئC`ꟲߵt[+J?$a>hC;A[7/YkB9jޣpdv#xTzݻZ!́VÉ[^եP8K-7^!}ۖX] AW˭-MgQqv#}m|;BJBƛG!ߞMO6sڄ ~e~yx04AڍQGSON: BxnOe 0r8vɛE[$+[41ౠAC ُ! DRQu[hJTkC^3JڌV졤ɀiq_g^nZ\=(4smGZ-mo?e{Ve6kf|<π්vkism}F{ -K?e/`g]ng,yw9KM^ Uv'{ҷunO Bb - o[U%uFL腭"mS޶j-nhGFkɻw ~8Jmm -^6,_UĬE_5#wɮ[PĽnܷM5\-TǢo7ܞcIxF{g-]k<9n-WY̨[> -޵N]px-M[hⴹK -6]V[-gf\{tF4k0d4*IZt}0ha X(ςÇXûX,ZTOIi6w.t8cJր Kũ!u:V!ୁaNiX1rt -.Fiڝhܯ|@XwHZ% 0&oGGMQ{Ng~ S'B7y Y,)\%vG\nX';2:GxJppS}g_vɄhUuCV[ՎsJrr7AFo!}i4-*CUc&RHt#`*.B*I/ꅱoO Ow>-ZpC=^_ߓ+F6Q!`t]`XXrA VB̕*l^88X水~kDFNDՋDq \}(icXց](HɃu,]b_ugQ 2~/ VC{2^ g^~Z7,F\s~97},}#5_}e@}1}38y糏(u8Zص,&?"lM0W9M'ݘBAtZ|4n&بܛ -C Y[<ؼi) r@/h՚zjIh/FW"@iН4Z(Ȥ\{":<@>KZߚؠI2|"uG, ##.96C!+9u@ZEroZnM[2ųcX9qɱchSf x}#uJzWݴb >s-6C5-5cb]R;㓮FIo)q=^97]Pc&-uop˽(|rl+[&Eذ4I.[·_b> l Zmj\u>ěA}ry BTw|/vpv|Uťt\Ҵ4\Sro.uf\EBVx-F2oJ+kBP\}v93=4J-.a 5t=$dMS0Bu\IX~CspE+JdSbْrknAءqA6DAi+Q-$APdd2LPɯwPyݟG}Lߔ*1Vk9Hˇ՟(CCr9 -%r`rRSCFQyݻ>Ӑe7({.9LX 7vlLCԎ$x.S-:$Lg7d.ZG=۸*^ɵ,#"% &h.E΋ͦ K=]ickObƝHNy[1幖#hX'pֶ*Z- sz.|*A=mɽ+c8-Aa`Sww$|p[.*-V=vq^"xX ]et]溃-֪\n=-rd۩4(Lq 2mq[+v{+ NmgmoO=۸-Bd'֌Ž+(5LM.}-l-w'ǵ ]BCl *_ީǺ_[zPVeݭzV[+r HHr#7Bw>KNkNhZ*W.>-!U^%<^ж-343A>&p!w XaC(̪(\g)Ȼkh[$s͕ŕ!b@](ޯPV90sDd뒺u''@%`lZ W(kCLXBBwy 4B`LE PB菤?9O'J9?R~ErnNnF`v~ũ`Q :w6x. ٔ>i녁5 MX'&e΀c#\-Tkˉ >E胭cjZ94R 8&n%v-l+\YTFI]v{D"34_<4Վl JGyZ siKE8žM{b,ۼ6j7]Q]Gb&2>K4ݰ1i5|ԗX[ź ZU1J˥Q\$.Z e}o(h g;k/ ,lDkɬBX9䅋޵ \ZՃYrѤ! d}WT]jg-TXQr*º-jrݮMZ J8ZZ\}74sٍf< C\cP]'Kqsi,q?n.˻ B-o -3.FFA.y+C]{10p -[b1`)F9M2DFWfԼ P&PU+!kÙ3ܷCNJ+Ri'`a&r~szĈ+yN/ai0M.^2!iUVGX[eSZB뜃uaXcQr"l_fY(]LZ^/h[T`طec̈́Hq}Sk3s*b/ ?9^+S!` ]&~MGÙqZVa;Nr7mrt"Ci\ :e 9iYBLYPBr-tD{BbW}V†&P@\@,A9rF8pԴQL-yM #5'A[%HlŢ.Gg5K$'f¤[M;&%R) Ѥ6&I*crbxWl6 [sU 5 - 4@IOpiqbmDUc=ΥWDpGQG9ܧod(H~jUoob-uX!i->.zcBho+\ aLY}#ZrMT.XFMW͵ʬ]V=* ylp4<ոhq]PĭWzcܴ_ھ.+|VEky]pa }д(˼Zh-brn4Bҷ!_ax3 =WXX幏$џɲ<֫nB3NJ^!؄ ZDwk$!o|2Kٿ ʬNR8yڶ90cKѪG]qشTB:W-&}BZF >e 0TV`s҆k]U kɶPZ%`Dk)Dnk|VFVG - MZh 0Mj"PM/p`g/DABB&+IJ&m dd &~AH\fsVbz9~~kU!~'!'!'!$ϘUg8nB n#a25l{>j0SjA#A!U%_7O/erf|w(gm8Bƹj{EՐfڞyP=.pUȴ<2KroUΔ#}-zWg(sŐEdƭӱAⷫ)jhs^IOs#s\<F2 jҩiS(G=Rߙ]O>M[ǟ#=ɵSs9cu?F %}oՅa}sdboU(]vVmomOta-@ksˀZGMYpFYz_f;Qs{ϪGud5cUUBWw`[>KwMM [+Hro]w~A赳.a t_h#5&b; vUoCa P.>J>WJԄoi!`!bZBoT2'&6Z徝\V [І{HsV=SL(GOjd`,h,c,K -ZL0LE;6E{TmBB -9(_̐ }7nB_L*<5@;V4\ tZǕ2g5/ ڏ4!V.(a[)MY⵶|8:-<W&쨗з(}- 6X+#'EMOOJ֊r]rtrQ?"DFCsziftej8!{X1q zT-TrVK]()WR|pZQZWpɭ ]kWٰ_g9hW*3(]%{oBOt]7ݓ] N -8=lsv ~i5`OooX3ߔfdbBRpX2dw& /ˌpBD+oިb\ dWX\Uym;JƭM`3oP78!Eթ]V1Y)iTom;`-@vH^5%is >*jx-+0Ex"TuaϱlQϦZCMxG9iCHͪ+V6<8(}8ިu@V7nູ;8޵sGt,NMA4[m`Dnɨ!D0x,L5 `5rdWu BM4]hw_9e`B!1q#@fAh녆@Ma!1Hf 8 n(\BZ3pM0Mb vd&w4h *͠ #\#covc/ -ujɤVKYȈ݂R3ka[-KWpFKMfO Y0Xke;f? 遚v^jhA]A2˔C$[>K]f1-{iXɻɏ|YNG5{~A'm If [rek.[!j҅0|S42|%nl QPRܷ[t ;ZPlBݷ!C0^x-Bԥ !Aj+f`]:ȿ{M[2o__-7m ,9(&@=qކcQs!`hB7^pp+[x!m .V k Cxp)7 2h{н{-!^Tj0dPQ56v*rމږo(uo_z#3Di(Y`c(Md6X3Gz]Nj)M中688&.jy$¤ *lɧMʛmd& 2NgX2H He  3O(?@'Gj}mSbUΑlB04UƜoN!ݳ -s^lů*ԣڐ?uU!jюbIUZMŦQͱr9 -JaGjuf -6 ŧI;6ZO0U9H95*flbX lr86&¹I+7#i~ Mo(g֎^GMy0P2%Zn+b% ,8À4cWzaa%];_%xoviDǸ!0yp`Ή71m#}ѽ5ƛqr7!W[trVNMkf2{Ǟ~xTXI>h\ skV[ e {B5+^r{h96aQL\h jW>-Jl!},"Ta>,-9i [cnkfOkIs| c=\1JxC.BWxZjxV.G y!}*/u+ W8T+]"U/NNf\{W݃>*ڄ->fҩ⌹wyG |QゐYFGy('U(ƣhjtǂ#Hrڄ܎hZ%moq}!r:Q SWݷ&,H'odo/Zt6%͵\X+22՚ش!'>#fBe^C+R?0.9:v}~ 'l!# M& $dhh##v)!!  a -+0!4Zڀ/0Y#(1Zn+DlJjln] `}FA-$!Ed -Ԍ=E{tg WDYFHhTڝ/ZF{-2[jt5eڢyC@ɤ,Y)Muޅr,6K) 2fu- -x$t&@ -Yi8!6͊'Z֍BA@!kp<6q. Ҧe9"@ *7QTTLKH WeiuB. 5JČّۘTҤ&ۈ) s԰tY+5&*C蒝_v,N!䆍fMd, a{]ET{ -൰{C $-VɬB2%E[0u-7N[v=2c}ͧd8]ֶ4]*+ēmo<Q&C-;EJ bnmBTiG)G5ͽo2K|pF&0co2|>Y0J+9D\{#t -%ü * L! ;n]Ml:.;PX  4 -+Z[Ew ]'K}tlaεucb94~AثƠi<\N,i֝/A׮)InOvŨөmbY R{\nU}W*S퉼d!uoOYV4D3UKټsbr{QγlMm<= TiPg5A3}5ըka #iLZ3l2f l8~K؍М,UY=ܜUwjx1.iFrZ(8'#vglp=\Q$B'b?DO)QOx:rn%H VB@дΫpTзbwyz dj$ \HX4>e C< I>kZ'3bΰ%t 3D٨%:,'j ZMoSa67VDZjigo}QL!a<ڎkcw'.Iǜ y/cZ~J84}ZD9`6汲HGNLE7yʹ>*tе9E*7Fүx=:$s%[-k:ۓ*jp{\=IXchٔ֎옆]j(^Aˈ5U떍6X:/jj@KWhX6w,,?+oD|${0$vZiXdVFɻwىv&_u+l+Jyw#.~HiNkmMizxZeP\/B?^[д ݯbX45-ɺwjPR5W&ԭifaKHCpum+t0 -ҋ]0r`^=<"{/NxgKBѲ8. ,@tޭꕪ_iUr [^!t*qtM۸.վ#ae{G1ibJ]v=iP{iSp*@^nG[֬J[VrvkIF?ڴ[v y p.xv9z铢WP2+y*.[n[XK2nVu]~UÛHQc Ik X6qMZB¢[cD>˼ab !S- ec%s_JauL*o:M!Rq,7!6CZSbSh,;mͻ3Lv|I Xp ӏjiC(2`d 22.C Ajh#QrhZNdd9X3MB``n)r!-ەkشaН4Zjvae9[E9 -#5jU#'DJpc}@#})(EV' $͖j|)0 *]EjWZ %:ќri<6\sox!f6{ sG4Nuh&- (ڣTrxmviymZVtԨVKrsFjõ4s.9X%r\%~@ݕse ^e(>7w/'X-t#.Ł-+Ou \[ź -.. -s7㕲qPlM+U_UabE\rc ӓ`j?}зÂ@z.\.88.Ǟ5]5'˪ծwo?o -cJܞ txdޭ`- )@[-+WO m꥗ --'|t[JhŶ4*Ž/kN5N a]rvq[opZШ%V-VaeNjܑ[_'C:܍eWYaiUH+ 0ɬ7s?{g&6'i0X "\e^jk`-j[5,XY`F%<4^#'ٖWo{q+tg!Y^x8Ǣ[m6DU භ?v V%>"5yc@9SlDBtSh ņ˓d FV30-CG3a78tjbņB|/|)`dl0/2VyMk.+Xd` o A`c,i`+"q3rl @;S2/}}Vjcpl%Beꍪ#Zr';u-i]<sPm*T7Dߩ2FL+td\pҨZvä{GjPMZ!N$&Fb&هB]tBG;ԩ,u)ԩ2+N&"c2٩R)+Rq`#ݕXwZ#,LBM*8NQ_tbrF,0rօY`'@ns:m6Zrջ:9yW7&{`ھ꺜Vq[R'hjڶ]4W7rgtB|Ecǚt&|z؅G[LZbuGD+SaD -96ofsSX|_ޱޅƻKh4lϊŌ6>kUЩ?5+t98N -t-u%h5%Տ/ #kGsqZq[2Ûwk`[& ;1ϳN@?eB]52C2u[&5V/hźw1|y-~cx{ cڼs-XlN{v,Tl-To] V zatBŌt1TaD3Jw~LM7pF,9˧ TX ru`I8VBVYX>|BW' ؑV>%l]6 DPʽR⺿GSFHJUhy,K佴\u]On|[QܛuJBhVǹ72 *yMjEʝl2oɉ<8iRo\G0"V!bI9Zn9Ŭ!tB3"FGYT֥؁9Ux%j HB$sB -CUgR{kGM˼'a -$:Os]'IؕqJ$TbqsݬUWٜ%`̝:qY^@yچ8k^o -8fHhTp#(D@N$].N -}wgmǼ v'(?@[I5 i2 -אf -˟N*ooxXҩe7:jWB-V7DB9mSt繂LjGl(Y7i{ʛrc6$,`.L2ɅP.;Pp.>Ё 5;oG!Y{֤ 5&v*o*n䁃!~ h8= i4ގo:,ܷlɮSjkAt<~1i T-zWM+2 5oJ O\vFX9*;-NsO%˯6F0jĔ5+OA:z!>jYJ:A8sxؾYB\lXoɎ-U4'#epB,74~@?k: &?k܏ubpD}nXiU@(ܪ H )*N' IПh'gаOb6@M7扊Ͽ|T1pLC|ٽbţ:]F _kM?m{u=vD7>VSтϲ-A ,i>Y:uGHFaWMAӢ'#헎1dV/N65H['fҩ21O3ndw<>|հZ4޷nxݏE oܷc{V]P,[[k -`~h8w _PPV[(ߓt rڮ{;W@B WJƫAjШb6(g!5W7tCIVM*7v+.Www|xxdŵY U8,j9p6rn7Ʉ3NZmd'۫/7.sM#s{tx-<0Z+ xGY[}V -V-"$xCl.{wi6[ އTj7]@ +ݽ-1]+ 5 | +*1,up]WqCe2cRW7'QvqɡDYyp]>Jb=o[TBhW(j+ts[n]M~nl98K ̘فV/hxɁo[)tw{nFϵ⪼f9ZbBw+9:}Uof6Չl%8 c8`ƢLiX!'e[Zu׊r>Ҝ8wM2{Sm4PʲM@Xѵ؍pLB, 3iԗ,qozΝA2)6,M |95ӵh*x Lq;'U.¥a6璾֋*\G&keQ'b_IδM&W\gmYTK)6ڛ.*`_KgP-uptZ[xx5W0Adi%ii޳}x@ݓ+тÓJ}-]|&ْS%34TK¾ҫ -UxUKV%tp*K#e M3[ ?!)Jvl :M/;ZM؈M CO NUV،P6ep`dp ࣆ)[pr'ݩ2' :fR9J(?@9-C9a -O?͞d`}+nPupصV fM9֚&aõjV -yl[֌mQZ2^<`BsqG.I?~]<:WC<*f{VZe1JلhVUc*>2yUtktkO.aeeV"'ƣť9?%PVTW:r}&΂KHn<'Ry3̵]8gYM[2{SlO1-ھ򇴻dtb%VfLuQq%?,Bk ޽S\C~kvV!isbkVA4H@>xd~ʩu*c{/lX<M!tXVyZ_ymy^8;Ds$vڵT!aoɽPqзlɿ V^#{iHm|̥Oչ>u l|5+z웗L-aq.u{]RJVsEiۿ%ty&~d1au&VS 2uGM> vd7mJWY>ZiS;4eJa fP9X8B޺u<hSjp2,HEu'4UZ&DCe mq+Jʽ$n&%+J|UkJ>JyTpRֹ͈]}ASLo!gsBH:|VosnΦ%\yNTiU],6;?{v sUlٗwd?ݻ;&/wty]fq^8M+r׬֦eZdaܮor26\,FQA M Ɛ4ɢY$1e֍OѢؘhɅ|@??T(F]K^تCoUgGJb~GfB,B v'ۣ_W5#,c=^ZMLK6+?$m=+^oj79+PFd5=DN!br@&%4ZĘZ>R1!4{ -[z8ӛʏd -9ҴڥOVFÑq};WXBigDSk5M4pskӁQf -9؟gڒF! u2<7>oDxXx J#&D^ -ܭNZ/jC_ yr(*~rɃ[(_Y%ںv=Ž(*ulU7m+vhԨ̡n|ɸ֓!oJ;r89c;B2Adz&}pn؛>؄3[dmJYKݽY n -sޕo]r LHWj|Wz,CYn{G Jyp.(`д(f,gG?%a[{hu\7S?D6$3x~oۺ|ʭm.ţ.FoJrq-d`: -I)}Ŝ;ї1ldub>ATIڇ$ևpNu+1ڶd|UOiLRU45nx܎Ϟ%8: U9QĺSjXqv]MV6' `\397CHګE/trWC| u<%nFi_'?llh -67 - D*,6mM^xׯ(2%6 -{`M$M2WR8Jg:uc+v;ڲOrq+ˤ憬sH>+peY4rQ7v9uW+<8Bu_\^5:;!MJ$̎bnNV -D{HZD1g9JW%.78,qcU+9:-%r`1x\(6yrj9]X6NQcljN&RsH_fex _NIj;f"em9HxXk-QhjFa`mRp0}EF+})}ºC꺵Ć}d|3qGIĔ.{HoQh-m֕N6I j_% ݽcε?5.*Cd,iLp_lڲkLhcH[ϊgVpWӸktyRLEQʎ9&5JǴ&Bܢ.(f|,43|8.xEi*@k ,0< -g\fل%t[掌̣W!U6e`ݐl,Z*#rs/ksV/t:x^i4(\/'T3'ZUk4f̕[7`n6T 4bқ5YH&. !ɤ٬M9]9odT,S}FX,6P.uKN -Ѭ4ϑ=&s TVι0r*.P(ďkQ]ڴWUnUmÿ&ddtE@btu*~M`p~V`{.60;hՑѭS%N̴X??Q/6e /a4J_a\;Ī.u=nNe\Zӊ6o֘X6[:.$w* K -Hִ\2`7o5ݢ愆Z/lEFů S^%̵f1Z'0ِ=8XW?"N4D|HOD'ŷY<3SŲ$5ThqJxhshħ 125Ѱ0^Ht=hCLyG(rݔ*4 ɹɾ(jWk!t~MU<8 W4& ՟<aD[6[K8K{@1[pTyM*ċ'c[wN\W6@ځ5ڷ_MKr }MkyZ7ٝ]HWJO<>s84ɸ+킴]?ݻҐϴmёG'w]jjdS`q-Wk^趻@ pTYKsB v^+C茚k=Qp ӛ;1잰h!)Ƃkw#ߺ9:'#&&<]V/֏&zd:ndl/g0aʉ]SroZMϓsW0b -Z{ -'?{62ش]NGUjЬlON ֫76[TwwvRo6IUNq[5ṣ䫵ƽЪ:W\3 -<5Ӎ,Bl\E H2 -3EVqT>˝[F-|o5qڎxu*ywr,ЩP5*u8Dl_nl*$1DӦC:I$'.@Y`zV+HB 4(}k?ClpBB6; D LPA &GfA TRp0n980Av']qԞ,D}\.G*UL쯉 NXLbt4kUA;K3xpVU_U fP-75ð(ܪjp:ZSA1~J: TkAS2i3^ VV`vx-& njֹN]~M {H\+W4m}lXɣu;VVa6ÛP 2j8^:U,Xa7ZTGWhv KS<:>pPoZ:l3M).of'9lVR¨Zvuhca[]4Sg-&.<܌vnwL9U;;BѤe`Q*4r׈rb9aM\ū\IEawKB8ZzlUGr'-n{r:9R_h2v]M[,#&[\l=c@FS$bC08{oi脆r?4BBc:^/U%j7S龈fad' #M<;YD<W:WXx=堵 !4Fұu7 W|YP -P#u?qwnquNS|ߗ nNt)A;I_.+]R -qC6lgW!k \%h>ڹ3ͬ;". ݓpyI ӢoַwdP ǚܳnMQmNxp[@KZ9x7D3jճSkcx-zUi$o -dFP7Ptࣝ)-ࣝhmE -IxO&[]oSsgtj^<4__zl9 Ҹ*g7e'[Vdnk\Z<'q^OS.2hA\V94-+A |_N{88}Vܛ0LjqA)Z, B*PYj~XmhԇܖU Ncd;}'6@"ڵA!O9]\Rm;@iaR,[+M]GP6lJmpSx{U#T4ZU Ap1օA9bNGv[֬_FE­0C{Po/[iވ,TV7?4uu;biT"N5*Ty\LZh` nsCA-x]cNu°kCG'2qL&v`ؘ08vmopM `#4_B]/U=89BT9vl99M?5Y7JFvephSÇbN8NWQ>Q.%ԢA؄٫rmsfOy׍WeLEq { cR|Jl䶻@lwp@5%G:W2SK4f˗O<7wwy:',~%xy>w/Wj97m emU3%QV~I2J퓓p F|Ӧ^/T!tMtV -8&rdCM o]7X+u#A&qʓ!@G^+W%\WC쫆Ӯ<W&ԭ-!{%k例ҝrKIهˆ k|==ȹ^\}^z?v =JɌM&鰔fb?nºcUo\S9rw@1uMu3%%QwL0Ph ^SF6I>9KK׵]*M ǹiTrqZ@qIƧ` Zç2қ-%q^J߹`˿eںr XKVcΛNѿZ-Td6}{vZk&<*,?^ReI?@ %2JhLq0`hn"{7:"rQeyhSs,iSdmh;1㕲! * .ښ91ؙAh@8T\D*J8E}$ !EL򁜴,x)'!};$) wKSՠƤ7؟RXf'& -شf%6s8"2Q~owTۃ+burUSRSV,>h˨ܫIc{tYcljx2W٧iW"30 -F bNm_gWR]މ}Z *I'a{$ MwbZ5Ӝ/uQYō>(m[v3rbD*f.n};;U0h|pr 4*a字٭LC0FX7vU5 -.hIG0e:Zf$#,xU\ rp8t]UeDFI\k6w*|NmI74m*jk"{uj9Ô%L L M.B=Ni)ԼӃX ᑤR3HKHXRcD0MC{ߩ]fG@]%r{`Ws6Fieɘ^-PXOJ@BE>i9&溱xWy#e x*ڶnoYjgVKm|r4x[{- -ra\搞Jn}06dmoɫT-H͚ h6m6φvݤT2ي[ٵ:Z1?m[㐀FvLkYc~'J[pU&'RP'Y*Ψ]2Yvku~_DV -';'س.Q`Us)K'CBihG C¿yqX7;8ZLU,T99ZG8CjzT3!eF`چp F>K]ތa BłHkߧ`lVQV!Ҏ5 -&.Pfk[bUq6O+U6_vDv&ŗr kXBʚl/kx.NX 2,[:ZJi!=+`|P$6yL*m'H2O2VIWnJqmC2".Zliλg6[7l֮R ֹ\Je«maZ -宲cR.+P=Ψ  ګ1ѰS^׀aI(AݐI,CU vJsKJ'a} I4*w}+`2\&yNgK:]hb5@3o@T:E}@s>Ɇ?8Qz/gُi<;65'b7[f.LB{04K,U 8׈r2gTyAXp?%ɽvc:BƫiiJۋy ڛfc'"/n tJi2Dwrl5,iZFgӶ{DJeđTih m`t*|8c얦 PZVcr28ga՚Nŋ@>y4A|'fSd$6<4N :GA?4A/VVYc论~k9 [q8GL9Cw0E7آ@{¡7#E7!*\՟;(49{rn2oWй'կn^,%8/{SGI5N ̝rU,1 -}Nާ#ڥnfռ[p_9)"5A** k>XsQÂޞ%[.U0^l/y/rՓۻƥ!kp aY[:zsv'<9Ml;UZU GD^ S VJŜڹ][ԟ)I\:ےXlYU,/ޫO#:,-6!`ђfdv^7[by\'228d<)~Jic"I\-9<;WiiABΒUqMNfTXcrpLV,9Ki;ϥXGP!=Qch\$|)NrH(pڹ1ac$B 7&U T*\ճ - &\Ƌ]ӳH۩p&ŭJcH5f'f2`6 =iu"Jcٹ"㒍BX)6n Sxe:'IUN <h#4EZ|#XFnUēm)X5{0db2 +ӮU=#s4̘*/o}V˷}]ܱlEy!&d4U,T{Bl( -TxhάQh!4}!Ҏsj^+Hn}/AGBkCL_񭇎G4{I9a׳l*@.bu1ɰ=yZ0ؚ,`G2sG6W3#i9krPaf2KP"qF35`-vB@t00OL;I^ M8O2쿬g-W57x9HriTl.Z<Ťn>M>QJ{8tG6Gn\e'52uJNqщG>-8eܻolhYyR˄bXX"HqOs\VϪ\ԍ>*,u|\rlQdUZ'mp[o=J;UJO>HZt:h5C2FFڢ~eR'q=Vީԇ' tj_gjA[R.j$rCOh-o紹)ݨ浑+WYW*wcIgbֳ@hr=8.<B/m\ G=Ow jX١zJ }J>!Mf܆Ee:dy'TMٻ\48!{35`&WsM9τ!dd14ţ!U5i3xjZ2̛P'x-"'1ki ;'S_yi;௵oOn_u܍V+[b+{mZk8%qlMÕ6 mLUpCeM }9M$v\6#ECZ[f]fqX `l*.lr+g'>k;rf֚^VLV>9\BĚ9݋~ `XBgR24d46oU4Cg ד_frw ؟Mb?,9|HVP$VW63A^ ۵rI?5:FhWl j4㤟mr8#P]Zio UnM%G8,Z/Z_,ZNa 2@c*Gzt6\3dd|==_ob"Usqeùf 2FsU+ܙ,owzi ځ)5bmLfKk;Qhͷ8bIm}]ͻr{ a[? 9FPlG&6{$6;,퀘}?)BwÓS9U#iR3:^le~p@Zfn¢i fݓrl=0`¢YBMLAo}VkA"vL Dj --vY]ʋRkF(i AB%Zq B24N܀0B~iwPŹMRo9gR [Zi8ZT9-ԩTMM I<8\*`?8U-F(:ĴSd.EG:-: Cާ8ppqk [2׃1zt1z `5Oi[;J :8,0Òkv -_)ۘUXхipUQ -ƞҪ58UVڞ-ŨNpU3NJo\]^㇚nu>ISiBK L;*Oة˾)2(-&:+G̐U<=6S-W|l9dTk!c OBZtF{C.[m,=JlGӪmZ^(Nųa8;?8Sh {BlU!_Ͷ35^]c>{h:UO 2ңoec݊ceSy-ZΗX\?c]UqTnڇ6]YU%[P\9 @{>ڗ(kgڻx[*\Dm xWb[ | ]˷M*9-;Ɋ6yC[9 [mY0hkufiBlR*}C>kOv[jUC]]GF"-1 ʚS[^h x \?C{VC%oeJ.C*fg |\Tfr˵vxd߹_Hg7>YV8 ~|=-.kMqwFڰ0M\Nhɂ< X-MSichUǽνIந^M[πBj_^|sN-H{pXsNcij'yiBjTqrlofY7*+ Zh0HMCgTYhlύIʟ"mĶ)M;$v'DKχ | -mk C 9I`] zmehL; -i4fILY-DeXqdROah"C4gE.p|&#B L -e:=<&m-30iX :~/ې'R :-14Z8_ PB*2 &=R|:YH:Zc婮iZy$cAʭ|lH->Z׃H:ઇ_SZ )Ufr -\b`*Jp ]=J# *N)Aߔbj8PGɠ - qqS\c ^.*@К>QɄw T/&W6aMڵugsьI&6I'L;7;C9O'm%fӮK{Xlr vmY]NCSȷjVJ/v&SlѣLާ:jvs6Hu76j3ض*w:C{|s]fz{ǻo&Љs4l&ik{#erl3pj+,G 99aصQ|C)0nn#d -{N7%`*fXhx,N -4;wkX - c k 틔7m}]`cQ釚ݨDP-%qZTl@ZHMYCzf״T24rKܿOZJKH3ؙ5:|V!쨂i7:E=+oPj6*T;&sVC~i+Nj8 syp%sIMPʸu܆ci=Gp|__O6-3[ xNNCڸQL|QB&6Miܢgc1B#!w9iK_@\c6㒑 wH2#Z>ҶQԞ*ە[% kiF5L 퉹 Fxrl>k]Rwz)?0< X}}TGc8fJ{Ea\MQQcLܯcW8O7ඹg QV`pTae%VAo< ɝmrE|W vyU֋Jcd d.~kJ~'ɩ3IοQCs+!{S,sOf~+t N.bK<hTt_̴l|y_o35uB{NA#s -OηͶvdsGب[s*^qҿh.XNtU@J6W(4Fr8k\(jϲ{m`^duaSjT H>ABͦrW>Ls5ɟIiU%8Vfױ;&T& sN9ؤF NGt[CD0/(_R{`ʲu·i&.ӭWcaT]a~*iO-bx~ 0{ѐUOmg89Mc'bt5@5:;}NvՏ%#u'Uw8M:\_X _h뒵jBVx.fU|{/OrHg5؅*?1+?2,klw Ů[R k;7jykcU28yܛ}(,,(iU)`EѨ9Ra.*p۫C{NRs_z%ԨՊduo- -}52M2\mJe-ZU+\qWoDs6a>K<;r0:%o+-*^T#mc0]bRbܗ%qN.O>պNq~ȥsVխ^WFL,|*d;JE?!Cb' Ha:T&ojmgzXf\#7?;a ̦n&y"6֧c]2\[J[4)GSd4u%!qKv l 5Sgd|ܣ&:E*ƽ XrtH¥I4uH3% һ$x=Ñj+'XmP+:JZ\sOlP_/CLQ?L]+A -B DT0Bn"ָT^E%-kKLF \Kp ^ņ["`*mxZD֍@@\JJ4" E-F -h2ɘm SlL 7*,ݥPؚƈ&qsi֨1m -ZbM0I&6NTCo)u7wz?*E"$;WkjXNi:,*kvP-Q]Uf2vQ"mjʙ挃J`*^;Gi 2*Y8I]vSGiQS=Lʩ#r{LcQA2jb&g9uITÙVEiP(Xw!nSM8\6GjZ-G7;isJtvӀ'C]e6Y\ѩ`kPdyS&J4b70vMgʑ]]:'\Hu¬njM6ʬvg9!Sp熲S[Pay Ul}{F>4ffvyA|=IxYF1Ӎ+Q}it:m.QIc$s,ii(6*f9bhc܄5jْ)Q{@lNM\ΐqc UrFJ7g4'o`2I%t[ 9jv^Fw)<_vl.H r2y~JKT- By59hPW{W A0 N+X[ܫ-ķZla 'K %] -6)gtSN|׻{Q4U@e}lx@NvMӸ*eI>m,JQBL9ԌSW*qEZ u䌑ΒNmZUHme{599ٜ,@a9)]OIE=;ٖTgrbvBݷ!us*]NZ#lC-gDv:HgzG^m(b7+^@pNoJ+hz.?5>EWF P\fRhŝ>JW~&Ҍ'dx_⁚-oWKՃtV9B&*?ۓ@+:̋rZNyʣfmFXXu -kЮm9B{l<ӣh8BʆApع4Y-9s ޘZ}}VEZT6VISR+"9] \u/i\C\Jݿ/SRݹeJ?HTsZif45'7.IRbT_Hќ Jc&vZְU7ksQژOƗMsZM >}{ -,V@\Xdd Fo}J9gv[>w"# }6%{% ssu,NJh%ksGB/LM=C:e}jrd1xA64pMΪbcr<>/΅ \ld1sE#NrEҮ<{M.8V]Y`t_]UcP8-b!1ifv9w*QmHCZ.[ĭӸ.*tS]Yj8E;,]q\p}sxCg+;ynͿܾ.9kZ:Y`g&ZɅ+U]B&KhwrDF̸ѹð0r83oG^еKksQXo]:5VgOfm[]!n\D:rV?䴹=>k]w#7 - -Fe=FC[hObcKg+O.VUņAmB;pV s&6jrط1c%Pi)Jvgg96s,*sy& Qϡv#-{VYuP ,y.i^n%nܵ򶫛u;[/xjwpUۙJ].Y+{*ܻ pRO7bj.yV EEAք)S&r`EѾfY*y0 nB܃KX&3Ik +E%SsdnLm ;h!jȝe6#LkF6\UuI*ةg&Tgt{f'#mLkȾM:.)4> -l21oZ05^ 6&9;5DtDnZT]7i@޲mQRO1L{ʬKHƵ.=-xRenhMu -eI_hͲ0Ԝ ]yjC Nmst|ЎSBŗnŖCչ3ڹ}ě&Tԛim({:βHs*J#EbbkcoX1HbA6] Z(e3,0[]Ty܂^֒ҳsId*v-!\}1ؙb3aL=ϫb - qՑ"6>shҔrC5b@:A!.\`e`.nñ ҄[GxAժ4gF,Tn>bS. -!rr{.{hr2)Ig8>yP❗;LLf䶀\] &d uj9yTsˊS\YrI ܅y΅A6m01ٯK t[wzӮPh?;&lc[[H\\VJy0.yV¯eENm4vMˑ5,K亭sa>ɠK-"yꙇ4 Fmcy9`:<䩽~Cc[2+Vc` -n7"KC߷'(4eםb3`4mW%j7NT! 0Wra5n[ -t$YPPPjpZ-(<4CCcn\ kZ|SҼI7y o7x5;74CNҝ6(-M ! r`CD͐_Z.ؘɚ%d 2TmfWAsPfe;Z uƟ&$*Y[8m /5ZX/඿Mø#n5FR*wUU 5>c8aɢƗ%Qi4X?ɺ97OI|ՙKi'8YqUKVN֝ErZQ} =GA;URzҰaW&܈+1hꭎU!)7 \O;qM65CU#ynTDYToINcNTrBv H8 ¥%9}VXu&T@z= /%Slʣdxi!.=6ZDg  ̪ -slULɔ%U9e  K\ ئف&r kd¹6pILc O6RYǤo94^Plaߓ kS S09GGM&i>csX&Kx98 t=`*vZ^≰ɉ[5]ґR#w(U< -\o:&li;U6 a͝W'js'D2*cwj PqNtdD>̤p@dtiU_س:!SZ .vΌGYxdԉj|Z>@s2W{>QJ洹Oi!bѱ6{V@g4|V͚@VX&/օWG6{gR j츎0[jnY [.u[&Fne*dYȓG]?tOf]]``b2)RǸ-OhQ c1rvVԭT&@WFV&Q`Y/ ~P|X(ڷ%ti7ޥ:D`"EQ̑zܜ*ۢ'7Es/>ScI8 0iJ3Uɿj}bEǔt]M?%8Y;G7Gq7iF˛}@8zʜZLiu/r,Sص]NZ0*o%iJ6ܽ忑:4?e+yKNi]zdU͌|(U%+^SÜ"Óz/֞x5Ʈո?Rzoch"nPޫs}[-_w -.Uto\ظ,u(/s@\m;a W#Aߗ[A䮠EH ;#ڥ!j%~d->P4gr|HXU`8vJC7-ZL&'--p%AqDo8b_ 7ɪ';S24y==6!s0ō&ب֛JQfLRpKްDT /U,vkKe{ڪChԴ[Zfl<# Xԩ.P@/M˔:@Ɵt9M0r<7z1J&?fm+~3ik)#&~Lv QLzUE6\X -4I#<턪ewi*܈եͶ|6]n@%6Úa͜ˡQL d/PA]QZOt ܹ5[mamis vX4y8k.iMZhX2Gbʽ) .pp mkWgW]`6C4]IoCLIL\Q6Yl}VvM c\E o@!ڴ+~^OҬl-M/w|\rn_6+ڴh> `r?N`D9`Yٚ e4C(}od5,Mi}Mh|I˺g5ŃG41Z6_gHT@rw29uG^&?as -*8u`@Yn5J-ڳ7",Bp )S;Q0dxқin~a{Ut_d7O}*=H=Ewl=3:309փ\V^{P< UfA/Z~Sx j?T (y-ު4/BMWԳ{3Moܙa' -tbٽi40&n}:<ա0Tw.}ۓ4aZx;ٌ.Q'J,Z.ahvߐ}sccͷr8Sr`DX*`]|P)nXiyjsZ9^>K4f?rp3UU->+fCG85γ\赆틗8.S8Bw9QN\"ta jsFN m*wTҪۭ ?I˔myW'j6fʭ͖ ˗d46T\ ~cJt rhrTk:);wU 2f=<*ƫƌH;D'ڳ9R}R6ejB4RUCVeV9 M!r׊6ƈ Md<Yڛ-r{3mVuTլfGP6ceޜLA=cAҋs^`YF͙qI1ri ¥W#K\cSsZ gmDVsH vS?0S-Ka2[w4 6u'CF3SsEkNL꯳&l!rWE7ڔvMhԳdhT]Mzdm2ۜk$HْXp'fqhĦ/vr796.Bӂm\M}JmlThiF\3x V&O>H#:OiT4;"S5-kU~K7>To#RmM]`KSs"S[.o}ΰ>JgUxgB t+>sL"4~8ulASVP8H[ai` M;\;NGAsd[P-s7&|U)ʣx\ʅSּbW pj56A%\G,^9Zx5rRqlBk&%Z6[2D ]f`2ZQÓ+q]K?[mgR_؅ :U9]o[o}N]rن^Xw~Cf/3sWuvZFjg6%VnWʪCSVN+sD-] -}]EZ/7.{ÓCX)亳^8-tj9xsյTYg( o&䖼l^YHMRi6w:,>+Aw}EZS[&h-#Cmա|.%鸼x,ML]=jߕPĹ>K@S -/a4ٸwRlA  S.~fhpSlבγ,TU p*G9ergstQQs+'Rwa!R0(g;TT2r_/besVHT2nrmϪ Sk1;J/ -QQS-.n)i9QMڬMR(0$&{-0{K|ӅJ ǪC{oK杛J% l+?e6.6C@u& ca -=illUfBd8q@s],@s~EV?6~i7jxpUݡVc]UFJ4=ª5xsv]/'/@Cx|aVc'iU[́6$il<5YUer4s -C'&W*er>ՇYwzZ7J jw+|}ڏ4 )aCI؟fۃGzpk$ U`+ÚuP8uvª5UVv$KSNH)n$ xmu!qז0&v=L9-jųsT| U,in4M∴YĖ#T+Gq*Т.!8بlѢ*fpU qM܏ V~wzzve7=i,jux*\Hw'=՚DǢ ,V7v i`3N8MuŧE柙G۵ mv KN< -a`ʿ*4G4?U_V&ev>va+84<pYܦhT.Vg'%'>Y 9ƹlrFSީO<(nM':+A99]p4@;Jl=Ô:-i(\`(h(\>{E;֝fT?$|i;gy9亮[c9݁cHYaG6l=@iӯPOpX1ɺ!32]+`Z? X5 o-Ygɸ^,IRObc3X1Eir"m+uvm ؈6v亩*G/ھה:ςd+ݽ + ayw!wyҠ~2zҺ-{ĞY1"'Pj!bp-k#fS}ق3J[ 3`W_iVCCNIZwP ]Gq[ҝ;Yi*P#,s5|4~#u2uSAi{qZMyFW-JA%(Y5[ߒ9/-͹>p6ޫ{!+Ԇ-}p$WdMbq>Uk9NSÀE'rڂi o5SqeV] -t@Tk%kW*nlX4ʁT_.9ɥy -SmIM{o PߊC +'.ꄒ9k~@* - I:"ToC60l-\$mk3\{B-T9 %razdHq IdìXAzmGraͰSp (pA%slouXrzӗO>eЩءɯRmXԤɟҐLz*Pl K=pNQyl*Fէ>`iEʝ5\:Yq)0w4ONN+p5RkhL &ѩrZ`͔ZF -{`svI`BF)*,.X݅Pc##E\sBcZ;.ONʛ[ M0[" `#FF*mFFh˻S@x YX2-=X\d6:/M#d,h>(2E頴& a8Z26\ h43 S'vi. -5Jg %ڝhmBoڙn`)4=\̵G;6~sZ*52VU}CS7F%|?o S>K+z޷m"F!I3Sf`wBԬYJ4]`e^(֚;њU]oMYv]Ok>K+w+{詇82A@طg'qe'M.ٜݠq{V#>+Si*iVm]k]*Qm`+|Wo w~: F-‚vXб#$l9[@:Ӭ~A_^x.L -ޕn&X؟5]FFF#{`!`R!Z܄9A:f~ˬ>Eoz -ޭ` ;o=OxX>Ior{^?]`Aɺ+vuR!x}V[g愎}*f$vzeطp+4ෞϼ NQ}㏒ǝ >2Me&˨VV'Ke̓} c&ܦs>+ˣ~xܻnu.[н]CoOnN=q]O\]Wq[_ x/w<wl3Zї5Beqϼi::D:;fksW*{74HyX=/;u}@ 3Fo8dS8U*2%bC]$jU^yĕ:'iUB[|fNBMS7b+<*OxD[k-6o}74x!|ՀKse6x%Tʫ];Ga>s1)EaBc/NTsb76 f 8&Y_()mBn3FE !dOrd8iUl -m6|QmG6<0q;J,kaCiL2遬wnG>iH +}mo/y6uqQ_ob-=a޵(Jd9Dx&K7Uyd?z!6UspcQt -Csھҥýl-# y#^MWAN6Y0[[Db٪WÙ<~HM -h6d*dZUJTm1O.$*0}N{ ҳZ{jU\[mUMJunj\ϴC&J[I-%;u VtN@sF|)nN~qB[fː0+QlɮB}rt(]*$;Yi0knB(FopT@fFFK i`3J RnJAp7nY&T@X63L 2`QS'U饰f-)gI/tߑH@t%+F$]3lJiJ<2 nzZq:ՒlM,J-ۑsb=[܆-4 \ΔI\NK5*ZNNHEޞ^K8٤I-ګm˙#Pu{(5K4C%K]gJ/cc%zfErTe1~܏*ًZv.=+R eOM80{W+DMjdf .ZD.8/,"pU&EېĿ;rzxXyd%}a Θ8ta .#KI ȣE֪9s KH}ʣ-2N=+z&n;8|>nnv^Sqjّ c@)ֹqOxIX0O. a8d-}` -ҰZ\g?% TỸK۱PM%nf)+(<6%՟+V̎ ޣ~FJϒ݅|;+v+4 ke)v!-FriTw\ c6_| ִM [6WX=_gSn\%0ZjYT*r^טbeReo[: Q ngճ ڨ=]Rv*.:jUس\өqJv`RXs%20 n-*Ti׷P԰kRFUT֞Ҍ q13yo˫H^+if+ -ܜ[]b+uLGst+ܾ͌/q䰫AQZ֝6Me*Xp2c)*<<ʋFц߫0;DU4/L(68k]CѬ˻Ɲ .(ߐfĄֱ9}E[@?hfE{ -o97\-O'cbu\ӶE`M_1C:12SE"(A*AJKDbB6ᑤ[)ЦdY0VSA';Vޞ vNmBA)෱Vi= VsF2dV`# OMw6G4 7cgO6Z9еbeJsePeٵbXlNN^69Bݗٿ\&.uYX#kܑe<"m -OܴӴ]ڍ![G{CF -lFLCm9;sW(lE:֝k꩒*5ڻTM^NDz>Ը0cs|:ֺtCo.k]X@dhrAڰ=x]>ƭ8i݌j2CC>A0TCvJWam 0 xځP˽ b;6DOcZOg.IxYǕC> Jv).">|'6;֡Y$؃;i-}2U29Slxp`xH.d_uP'苻UqpT" ve2 TvMw +E.9q@Z\+Al #AC7b!~JnxmMKCz9NW Fvە$c~xhF# 8^`d7Pg#q' 8v0x#:[d{t4bUAgj2CzpTm%f!Tip8*̍IfGěV`_-_;*ͼU)wUMNaIU@*C@\- T-fl.d!^-lG!M@mW7B^lu]l6Z{9Vs@%Txh"鑖Kn:R^Bln*0,pSˉ2InkbsCf&楤1Dאl7 0a6l@KOy$Erff~DdM| p(Y9!pd s$vKLȲpihK4{1iAIP20ZT\U6CFVQfS0B2Sis3vdv{[rN\evpɁV蕃DdݪhdqDªx5DwT́9 \FQh6Vi+v玕t\`#p-+a`ZU0f sxhfEG5> -emG9Y ck@D_xBn-'t -1WݘB B漵 >A# mɩ(eJ. /,QjXXZC nBCF HAh֕^ io-LQK'4r24.XX˻2պk x'ԌZ7 .QhT #BrM(77w`=f>od!;[nݜV%[·n.+c؎G~XE;SQq)) =~ɚ kGĭV!>Hdzv[p]Eup[ozTK+c`+u]Cjgn[+́:M.Ҿ^U9]b 6W J['zؽcuY53[V{ݹnꅴp[[:R?-UX于XB޷;3x5nܺ%lj뭌[-;豠^ n_r^űi຋Qn I>k|8gg]@.דYC|H@⺍fq]GǢ+vV]R5orq(Fh]|{ລ||2o`m/-_U^꺅u8,,.[juǭ=6q]Wz.޷ܻ38+j8/h[PBUR-E]*/{bu -1n-Vu곊ضy6 KZ%gv+S@n^趻ݹuV]RoJ-VBGu -/d7n*-Anꮨ[ŰdKк z3uVܷ.සuGo-ۗkݼ[S_ VʋtGގn[.xm m/Փ8..!u?*u%ދkvչ(dġjw?H[V|\.kcH+ݝWFO~WQv-]~x-\'Z[[P]U?ܻ돒ꅹov e5 1{]u\ށuurnz-D5rݷ/m<P|+x+zn[c?Rv m/Փht_n?_ -{vѤ8.Ýy(j5|?.>!ug> Tik_7[ݹlVq v]Uj{ZP׽5n[Į8/]Cn< ລoY|^|nu}WӸ-]uڷǂ zuG[-븭:亁n[ljܿ⺋躭^o[ށn,wOz-x-O[^- aⷮⷣ[Kv'J[n[/tq[-?#"_zopZ50-Lq[Z|_jչb/CLN;]Eڷoro*[ж?`B޻ ˨p[/ດuZˬо5[8[EoF -WX]B?@[o_oW-Wo[Xf~[pg~~8-Z9%l[[+[P P-!b(.]z\bx-7ܮɖq]8.տ+XdUkv=WUy-Xr<ܞ v-[]J|V hອ[ cn[į2uu[p[֮z-[j~q[]v[m]n|'P-8V?ݻc]Wgo?]}-x kx!u=Wg[.hz-x|nloZҷ-M[ƭxg]G~.+|+W\5od%lyr7Ǘ=CXu}WnnBܹm[j]gW].PMh=nϪꕷp+ݏ]_P;9:~8孏]u]ET/u_oX+ {W_l뮡_NN_[]g[ގo|~zxoH|VuG8WoMWV a~׮8.݃涷ݷܭy0oXp]P|_ɠ[ -7Z{ພn;+o@"aoߎ&z-⴯]u]quWT/u]Vڿl]G]G-WT-_~_u -zOoԏ%[8oO9ˮ3[ŭ井xj[q+SS_z]v.[Kv r-]w5j:VWXpXru<6lNܱ]U[W[7.K|Vpɼղz.+|ſ`XW[{9ʲ4Ш\<ٔ'S9SshM̗TaĺعZRJό@[m0 yohXQiT,ZI5&ZjGze=C 6Hؙj2C{ ]}PxeF:"U7ةE )p-.+S3?^qD[ai+HkXXy'\D#*Jp͢Jc4)ҹEg * ]˔gV-9؝m`E[nRiAN#iXvViL7ojiݪkp]7Z)#=9{" -gICuEcp\` \|B1cbnlD!e A`ed3?uygCnYkFִ ; ^&Aը̪m`9T01T7א DAMeiJmMhs-!;GsbQM|PeT6H U,\mMb%)׃en7* Mا/d1+nRmc6Z̷\uKǒQi:Mڅļ|ӤSdyۛB } pZj_im%ԲҦw'R `Y`H`d-&0"pX1lsm }k Zn!%{,`e^LcG6'\q+Vi_mZT,]E9 =5%nZZG9jV3g{Gޭx'U_PqpZG>˳pP# p SeZ]qģp.!andY֍GahUʋ gLh+*Zq$YsbiSs3˄2T9 -Vx%` ZPgW5KpMΒ{1Zh 3PMژL 7*mq  dOv(G#8JxUZzp= QΉُ[7ͥ -Bac*:չ"G7}ίk 5kfUM}iw3Zyh-ɣT\o'i?Tp }-}. ŷuNp -[k WsMR{lIfA#tA]ZLM$-[pE-;U0K0+[V lM8k&6Eh!،Ȝ[{6Cyn?⠅{M|<'g1X t.Gỵ-s]5\ǎ g !klǛ-# ڼ5I/]pDP 5t5j:a>Ӗ[p[rRC8M7jݞ 5l t }q^z8x-N9a toZwpG.:G-5# Mj5i ݽ ySd$rE>k7_oEaw+rƸgQǒ❚iJ6ZW/U|ϢGir[霤;zUX}VsRqtD`GfMlhUoz4-ZYuq;XgWQ;FNo(_Ry4|ӄuâ0UZ=֎c_޳ݩ3Z=7{!@ڽMni{|u*NNblO_ވA=4mT 8ۓNSuڴ 6tG,D! 2D]rvMȹ:ɉL:\ -ςт`2;SAPlm n|BҧP,xքa -n$.4٩:ilU^!i -!8k#ѲD:Pͳũ7&fܛ.n8`|Bδubޘ,Tp֚-Ort&ٲC7Pdl95&̞?pQUUࣝŬ9E F7T@m \ :[PYyL26'5鸁BJo wq}3:K-{J6Lw n:*wjSQZ8or}2d1:4hOXӉZE$KZ6ܮc4\_+1BENC$Ų<' 5D24ʧff -m:NN'E FC6*nc[ehrtB76j5Ąb(cOrut/FՑh;#FnSk闾.F'"m7*U|w -6ڌSNc,jC{dFךަf]]fDO7ݷOyrǘq'/zn!u>ha|ܳTYhFKSdݕ8.n 幍diVsy&t{B*'Res=-np[<44hO_-5ٔ|Dx85 >]-9[[`2L5҆yx j,(-mkP Z~6oEp^M|eB#&>Bq97nU9$~hdx&1SQbޕ+_'ٵܮ<wmo"`+vܛՀ|9!;&-tͶ@Z\[.^̻氱>IדbBϠnJֈkK">H^.XXoo:N[毻ϘX.L"VZG C)fj0 -@y.!QMr>A_C.sC+AᓳØ.+X-MRiaɵWjUoڶ3y ИɮQuZXi?rVThVmiUy+N0+:ƣ-[2o=B޵ ݷ!uicՍ`_Rܷ|NXTr7Pcm?ArJ0ņ|Qru -{Cnm')dشPG5ОZ5"H{_:sJЋ3G -#Wǟd Ku̴[Fb^+kx@X/uɭ40x-ø- -]pm !!isy7';2vqWԪƶmx-XTawAF^6!smy5Դj{ &g57pZ\,p]&橻d@#2gp\fF*g]%h -4}'KSi09b)J .c<slXX5lt%R0%4 FJiqeF1P,,ޘ* -T42YR6ޯ{V-`HU-TrI(dB .>]ƥP|\r6 ̻c.r x&[xBEɷeh`? (9* 8'Av rlkb81 ICS !%oY Xr%Rˏٕ'{n)sΡE6 U -$äEBذke>M!tGJ$ƴXYmZjvu'}f`=AL&̘n$sjWOݲm:6\Lʛo)[JvorЁ=鹑pS,ӫ݄ͤk[[ !8OQ;x=?}hc_PJKzVWRѢ -lNLS˹b&5#,u`GoGToy_gN6Ո{ky/oj)!ˆMӸ-x.<iGEm3.Ž+Lھҥ*:G1e஋ ]Ehy.[F?wZQcV_8d+E00Ug]Qn[a.IҷɫSV|()ZQG)FQ#v#Nm( -AJxtlr%c@_kH&ypIڌ -Ͷ}iByڳ-Us4M@j6EN!J9ٽS_Qi|IV,V{V%-c#;UKR-ڵy{Kj͌ -M,Ky;Bѫfg.V:/j02XUt29bkeHuJ¶.-+N5+媉nEWT!9 |JOV ,jwhdڴޅ6z,6n(ļTi݂4+SY`j}UTk )fZ?Խv\lr"rC=AOza(2r̲HgA͒GjL̋1S.gJ?FPA1#hT`uC.C>͟)i -b2Ojj[F/{mHK*c0@1~F&a4ZnfF&aS亿L.-ba.N%UKiѻ6ӭbC` Tt٣j{Qz)k9R/ԅH.O4XtPELڔZHë  jS3ذǟj¶z\صطnXPUVԵXRi959bN} ;@CIW4envG8g0ߐg{P)?)uuQӵuTfcE*mPk.]ĭӸ*.x 8,*=ܝ]p_9Cǵ8SC7!p2n_lMq;?)c\d!M&Ҫ&@ X[eGU`^ϔ ˺>ilwNo)HOϳa{o)w+ -X7NຩS7S ! S9E;s!}3-ˤ*_ RH.+mJҞMҜH!e07 ``ƀ<>.Q:-4w0$)5n{\ӈYf)٣Mc(9[=ت89oVߚulo_öm6Ȫ$ _TWmGe竁tq^8u%QFNg+ mF'sUh0^x sf-Zn:GYD<Ssi @ TT3y+ϢM+Džoݻ;M"?i9>hr.. I ]>hy#}~q }Hr2;y-mt?n3*=]8S׵TYk`FKf|LO\gG(Oms]U'1BM)?bom MN&.m \I\:+:)Ou*{Ugլ5t;2 ;nJaôJַ`Ѩ!y=@Z$RPO"eDŭIjkANM*[k SD'5kC5fS8c ֩ݣڌ4 %4p&0N.UgiNB퓒KJ'ܪ6F7'~E< <*/b&fU-\2*<4ېMGreIrGltkT.<൩=׸}(qA֬D;d^;Ta&,yoj=Ş웗_^Qnvsikո;޵n<}_(,-٩5:WGU0lSz}%u(K]WIW$ywp-&{xdܿSͳcPoH}<׮|zܷV&AZj?px-x-oqZ~_y]! E󴴬>|WQ^8:nw&tn?dܿ"Ƞ%ZJ.T P[[r72@ *56l(8ɻ$ '-|~Ǵ p=Ò9ukLjL)oUcY{ܴ=}TZ\ѷ}U,{N3Ly=V;j,]z`!m aı&]e7O඿?^?%mN(k=lM!<4mYzjg6t*9tm3(m*NQtCõ¨Wg02[j/~+jN4GAR63E1}74xbF ki (FN\Nnb xpɜx:*Ђ &-X{,K,4.LK69EGsv䕹C7*FLb -q;3)ѐkRj99֊s5i'URAwV)B2Lm*<*2˚5*|)kJRq%k ܨ1u,ddBMl -3wA9i4#Z6va})SniݪMᴄ `~.(ja vL|[X֋T-*!1)ԟ ڍ)Pԟh K'>&%rUZp$ +jkv!:-U8M4*ݢs1wF} mlؘXHӫ''@rn ?4`?5F,ǚ%^f 7Z厵g -w+U\o_omWYCnMQ,M6AXp=Z.Pꡎ97/;8B8u6o4Z[ mx-e y9+thR <S.*m& -u;O/T2W){yDܾޝRV SS <8 kܻZK'A!S. cxD9/ati4yӴXޣ&pn]F{Vr\p+O t7ǂl>h8/rƒ-E|?-x.OE_g kjtո&Y5:0cǢrz.$cp -&|x+qCYUT3~Mj;(diUK-h73'Zgj~[*--K~oI͆:t4gQ9ls,S5 2nzt}Vlf(oZd[I~x#_ _#֭Up<3 W u."u&6qd'v:M/{bPq(CyT,rPLiU۩nJ\3ζPm.*ot%׳=lv½z/x?~UmwWPq[xmoۇ%Li=2nG{MzC:ce<SXw!61ds,*) cS-T`L...M-p`I1.;0G<89148JժMGh Gz?p) -7kC뺥Ԛcڠִ)8mo'وX=ܹ`t -BSTMcju?j8ҦET\lcq\sl W'}:TbЌfqT*@&#qC6r1u=W+bƞk֔"nObjx{)1đ?a_y' Nͮdpo3Rׂ0]#)t,w\#lۧ#ݕO4*@2 4Znbh0nL;c#K̸Leü&a*mpD dD?-[Ufq^D ׈3ŋ|!`6ܵqoDOGI;?$T95=;@qU{x\2>m.Yoml'RKD@~>Ck 0 MM]ލ*᭴D"$\U8A\\k\\tU2I\ג䫐;FJw**.TvCET(akSs}< {Eʵ?tYy*ٹrkpxglks`osG>`t+ޜMW nLx{R0FuΧ4O0w8/VwS}0F_!1:QT]qVBp+P), -{ctc3Oލ3w5B)CG q,.Pܿ\yxObvDoNe.ǘw:BZ( y̬QYF\Me D3j]cܾBm O.g1x*^*u WsW@ --;1sjP*W30bw(cJ&lz.1幚;MU@AV.b{&sp0\s6cgs5:3r:x, 9 77<9tr]-]\'-Bx13.TQ ]~į>LcPW:'acjn_u;1e+&&1YD*`fQg>{a̳qS0 rLT{S!)ވr\nxǘa1K8&jOp4Z^^::|26Ow xLL޸q. -2CI>cnAq\j|#/SK[7GZ ;=vNc XX&'z1ÿs`W穆=\ _3ԯsJsE'p&f癰7&qVgM֥n:&X{|ņq=Lq_LjoSqT̵z+!2c7bap}z-c鏂Z|E%+"s<3i32_L-F\t&{<7:: ˉAbtU}3!*?8 -&#|]C_O_OPwN߃7 33{d,%̌oda EY~\TIx.fF2įs UᖕW]H7*KA*-ZRd +.`Usq,"MB|CH~N4Ua"r_O[`>EZ;ܚ|W$yFBe߂zFMulv,f.HF%TvSK+U8898 xΞs_;t3o5Uc1YrfY&j{uq0$ zI3;%~& S׷Ezj"w_ɵn7) ^S }CY)kDE?3)\BhRp;UW7sb9φjBWPqf 'Qp3:Ծ;gBfm=W <}9&78J̮:@횎]usU:>Iw=MgC.GP8(ce=Fߗ;1`#+/>āIU~O^B<;v)\NqxvM^cWPeGp8cX:g3rOb_[3؉h?h@+,Pa$K m\K*`fp)`u;&1u/f*gIQII2 )9Wt))e)/pZ|0Fɼ od>E#V7-բN(4AbKqָ1ܭL13QM%,hxs=1t!Rَ"ঈ1Dkbk,j3&j3u$̰3 -q:*:Ř.|m%dezfY;J [<%:JI!LPru,qZ ChxjePHh*-@C+H3t0B YT9ΠB3q3Iny 9 㪎0O4xY^LK2E sCu;"qڒljN s!;8~.tBU H5ωE("6oDDeMΥ 0ZB,̤f_RqQ t9/̫ڠDZ8j>-_L -ļ|<̝XX*5y -N$ R/S& j.Q.TppyWћψ&|w0X? 񊝐eyx:YYz^T.7:1U,c\;Y$]ׅgLRZLdнa槂Tb&HtL(pncAƣWLK rF$zLbg'p߄Ou4[bnw,M CBfg蟝9jOՓIK -qÊyļNPFu=Fs/5<1Ǐy8_\Odˋ3Xgw-ܼTψzɨK#f'XnT&ZV- -G9!A$zIz=dhW08^VDh\@ښ>Ilj1b(a(FzW5 - s.l?cq Z#G_'mc/-)L̇OD <\,V5؟|rZXp .#,I5YTk%frdKc,j#g2J&2-B[=[;犅NqVf._^ O0*A/\rӉ 7A49!/4K`/HdC&m`a$e5]9[0H' -,ȯ^_˝c1c$WpT1 &3^; -j^8=q3e;8'&s/S7d>gWa\̇]GF'PaU:O"m 'ciz w;ZPgp[y`E`b4ijBAU,M.fV#ad  2hy*?Hz:z`!b|WOc:5 $V317Yf!;gv6&QOZwxPo3bO=9⌳ǘ Kp.66<'1ozO!'dG*S/x1نK%=]֥.c4 *,neW&~I4g+TP~I!>9fŎвB -Y=\MO7tO&>йY-fgKcWvs>=;(J"TG*Jsa jjff|tOgK6F+f`C7r_PMKGqe꥖\L[2>ne4xB -&Q-}C>P(V Qq%ER1/@qyJw/-2ԭ -.*gP,Lf`bgs7 bf&*^~rN A72CsCfcDsEC6*H3 cAw&@s?l2WUlӉyeG*{Y *i/q?L8~իGccyqUļu+q~Pe2D:aQCrM0.-Q3]V9ьX~2A2mc!k;:(glrftbx;ԣ{QA̘,Rdl|[~|2O7glPm_ߌqF?,/WTPm}CU|7`ЗUQ}bucOf#^Y~rCX l Q;ZRV3!w|𩧃pEZwc~ҡ+L=37˝k2m\3ygQNzU1ZJ[_)zwhEX 1 rh?G.|4AJ.uVBgl Vά_߀`aS*l+ig"V/\!uLh*VLIjk$$0wɿ7K\'r3nPv>—[ȸ'ݩ)/)kjaogm}u/b@8y3%P/w*o5,ĭ}:x(*l1 [ nw;*/ -s@09(m(8 h‰-fF̮r|iu?".yu9|(ao3qm5[ -a%Z5oP ,T_!($YƢ@"B:!,6жT1|ܩa|Lڤ9[`䟈MK-'&(~6`r,FA =e,iH>ORmD&eO|QM£fkIJb:px Gy+b%|.83j_#kEJыɤ+; bD\KIc 7ddЌ;18YΦa}yqia |N13SeO[8pqL\DLJ:b7 {^x+<1Uzȃ4v$b/~\y/MrfsR\s.PTV!&fJ(<(/p*DZX9aߘӹȃY=mMefU*٩1θD'=xtܿя0) ̼Jr) 05烃o<~/Wwˮ:*$gI}1W۟a$1#=bJpʾ6_!f3>apj( Z8a"Mx5>WqFxbJ+38;&9|& ^7S{^15XGfe=OBٌFԈSiՙ GyY!Ϋ W+R/>f{,:!ዌ1Fy4{fĺœ㏈1;Bwd~g1z}ñP/si<\FƨoZ:][>XC,}ra,~<$v(h<=B0BfGtr}0 -V%{n%[NX߄\ oRd7˖U:sk0Yc'O<P% $P1USS|sca ?N>x'UWqˆUq<:Ĭ@B; -A>53f`feq-Ĭ穂V*9 =b]1e]T௢>ϙCpA Y:wo b'9\p2;)xYZff e;E7,0a},)(ޡ& L2"AಮtqrGrl̓ No0#, 0/pMqL-xVAFNWr7c+<}Fm>BF9Ttzu[²+#WQ]Q<EaTƫXZEޅSwU37o:|*bFsl ⊶{UwXY̆%ghZ[\!KJ1_ +d3f*s%ÁÙ3FQ[U#Q(J궮f(*u٫vp`*XW]5xn#ٗ!*,,1"ͅ=qP`.oY%hXT`YwTh7å2N2'>p( x8Q!7xh\4~2GVs m20LxI*e)mKVn5c K Wc`gvHc\ aLL&>3nq 1rS4PAT+ \njNS,*w2c*Ccp e(K28YX*b -^^QZ.a0(_B -s30!V4D(bi L -E\ J8P+-y/3ܼpW4UBPVp&4ʸ^J%@Z6D(mV7;" :ķ -H&B I}77M\"Jb[XǛ)䋍3:o-59^4N`ftL :澪TN> z  ̦- &AʀJ871,oc4gcx:8 -8`JNXn3>gO?Î+|O+,1>sgv0_7H@u ElD1:0bx!OG;IlT/Qs|]idvzڢzV^?y?)uvc`5]ONo,Dlj1븺s}pǯ㧌\x&ys9u*O|2:)StF1_I^ҥ2"ٔ"Rh")E--;9٥gGɟcccN=r(};Q>'\cu}Gן?[1foˢ/jˀh&bD]S+3 -Qr6cٗr,y\˧rw b 색d\%࿥?GS>biygS?W|aseVb*[lT'dt(V0ypOu2УJ7,\<`Ja`i xngI7T=SKX[f&v_gMf)/g)cuѨ{gt8]?\-ٞZ#ɇ -v ̳jOj~!K[/(> >r=vf|06($yɶ_&(v?xl_gU#]~D}w6 U`O+ vUY;?ęJc%`aD:3sIT=q&{8wJ0F)x~ɇLQݩ 7-LϻgqEժƽee+Dx6f*%x|(,戸KFۨBŐ|0C>4([OGSdx;Gk -s Uw#7_5sq*oA{8y;܋3p'Zc]=pP jمe -ʀ75zT4.=>H7Ī -*UV -!aWžDvؗ..nd ZoFh@Ue^儻5@?17Q?)yJr <Tw;s<}wpy8Sp5ʄiA/]K0'H=ƢHj-*.CRcCrK< -"\X!_Kd"Be8/]\EbFלB±}u*bQc"B3 ,[c&#ȅf,e -XjSk2MƵ Qc_Z^jR^%R;JtyiL:⧹S:gG\ wxx!8x_$Ґ~8|CPrV?Na h4FžUbEOGQ})\;Iw+\ opd*f~w_ l/e #_G8@} -c,"}#Fw8\꿫'rQᙙ\?pqw3_w[X;͙5k rkn1l#3jJf yyv>' 2`DY Co -dªi01riG@&e)1)솹gQ3ǎkdS_E3Ԭ?^z qr,c3ӪCMA1nwJ e K% (0HE'd|G,*Xq gi"PCS3+_AsFxw&s+뿮+l Vqb3䙿_F81ν' -LLϘo뷎e.c/gX8a[ \/8yCfNNoKԻuo9O?C1.W3 d;fܽK/0g_"z͏sS]e¸<<WpgXC8& TA/|RS318`Zx.e ,+. 2n*ș[Fe)7}vual)a-L2.|W5 E]"0Hs5ӥ߹*WsMOoBgN!g|j31\sq+Ibfy㿯}G'G|7 |s'\nnWћsۏbrA,p@2(QEG\xߒ|r2AX8Ĭ/! }d_#p &;=\[f @4Pp' 0ᗫ%DeS23AT[P{˫\Rф,&(3wX*-乬a1cX_q -EW>X"N?pnwx<_R;T$I6Z,oFTnj"1tTƻcO0d1\<#8.g{9֘3$nkl&e?^m+kf_$ٴ2e)O1O2&<5yO0kA5~%sGx=J2"#W:,f2(EA1ujHJ pO"1Y«Z 6 -qPKusU>3.fHqy>.?e̠W욥[.?f~K>Y~Y b.ަ3]NU̯ a W)]p6~o'|&bz;M>qehL#[cYy@/HxVWJ%r(jj? -@f,;"_D<_j,ncҧ1+H(jPO!'B4N!l؛BŲ>cB2B)c H}˿`Fn.HIs6x1*@`_Hv1L|dArb)7:Ej'"?f!OWPH FC) ւ획eOŧ-USw}!3>AP$ZGVK6B^ }(b B'O࿣p5×3LIx1(58xza/'ϋ9/\\ 8entLg âbc3|b_-aξo!qs q|Թs\bx8G*QDT,D=|,cMFm(\5Zj^z13CLălpKӆEa%Ƈib2+'A -+¼;ܵ$ GQ(N+Ҷ㙆.gWYNm_U0\eP?% -S)'xc .-I%쀥!0KS;OzJ(Ors{1N"QTx P-UEy膁+2^?O<\g1P\!\]pE:5pX}o,00qɈC'9qY<^C ;k/.{;\/&)C^^/ -H?Gd>8T1/\R);K+k`ar̼C1Xŗ-wKb<͌ X j_G֡?eINvpYu[a> ɒQ1D[d -D.#V_QE*-C(f~!{b֣Ja3R:.*fMQUuS;bO5EE{>KWB5#ԭŤ>̠An1ѵm->mG%w$q{E}:0Bh vE]+uᖤ 7 '숲;:,k[潡P.p(ױy.Y2a5gjc6g yo!x~)zIw5uÜtJZ9FV&r2ԗh(0. R.+_FrBvя4~[!p"/}8N* +TEUV-q>W>0S)Ub.*g٬=D!D!?V '>a1QGqk"Jjbª;XVF9-\FV>)Gڋw5E'c0}XтyS򂞹BvL},bax^/ǟ8\W_POsz& lY> ɶ聫z]g#gl-Q'cU-ji01XA P˚[>Ѝ/n'ħ EA=W%'D +̨_2#KГ 22S+(J ?žYZf ccoI`%\Ӫ 6~{'M:n[6E#x+oMs1OuO<CQ@%7PFѣs_̨Z bYYB%`OֹvC+ܹ~Ykl0C\x|7\ eK .^N>%53x NSW R#cYb:Ow/d<2"خjV0WX@@ WQbpp yƝD@RR(‚k Jyn+x>ᙾs Pp$Xm -Xb3[\h58.M +TfRF8-3(f1Cf%舲AJcQ<@]n*9,gT  i71%@(;Rɛ1t7!DtVӬ#P,*$oF-pa9}= &_7fgTԷ08#dYBf6@!3?$@}CEDV"b YZoQq!.Rx G nOC-=2 pEqlTnM-pcOF4v -ā= W0 u(<̌†? MB)Hu2fzWhn-ŰG Orį{VݱbcROAz5x߉QLVRX(%9C@N f\.Ili]3 -zzQ֛i.Ķ#ϱനy,w"v_s7qb5 lOWߝxx;g}R\dvu38iDWINEј>XD*pA(9!W댾 ʌv&"l3+C$3TFO|垤UۏTuЋqrHe̦;GT*4Oe0+(,־a x ]/8 \dsANɀD @^9||/)>{`D"&` FKB_pABŝB%_lgOn|S5y,C|?XN-0R5F'Ak4&#-IZ<Rm"%C4W*)yO~:q>*EȚeA@QJl; 5kS62PɁuTLķG;:㪗|qQ }XL}1GSMsmeAV -lX]#6G# sD1-_ǎ^')zG61sz>&D0`ۘjTp -$zXe5R@kVmYi`zfYRsqU׆;wpLrSL\˕v!Kte;lPU5}#7բ!WjJ=\ 7:1*\zO|}Wћ8>y8>s d!vh1||F54Q:CT{Sk;BCɩ{UŐR%3_=Q@9{2$nn6) 5D?e`YP\W2D+#50WA -$sd`VfXKv1 -$jG،[,aD?Ax`~$1 &~6j*cw?sΧu>yO|a1C[*cC,$s5 NTn`S,v/zN|}o~yebbK -wP4R ;// MS4 #p8_0h"܃%ܳ4Dm -^[_[Ex?*=dwØ (YE.o_K_u(9c6@^w+XuoW`1AWIH]^V跣ǩA1>ezAsllN>\cPLSZ.\SW.Ur>3 -UW -ƞ㸳º~6L!T˙PVl -iբGKt)v`|a(M~Pm ,OXՒd,K0j 8%W/%]F2i HSQDD"2eM1*M3)FŀI HE]V8]k` R; Xq3(e #l\aU}ˁ"EmDr\VpIecHewˏ2d -HBQ2L2} xIL,$a J꺇\C20% $k81#S]7$f"W1H |AXĠ6tAI /2^c,M0B"mhCmDOJZҌTR2鯽#CY #8+֔ވ|!q]y}Egq㿣?F%38x@M]03zg沪|ҠN9>$bCy2T}uL}\!>b PC6S9)Sh)&_,=1r2d# bO I1ÐύQ>ez CѾΊW G2)uZ56>`Z1^ߵc' .9G_4Lh;Kr;jgNh'ѻX+V'(=V1@;̆J`y&hC^˩~7OKI2 (yrs#> -gMXԁsmÇ螊ྂ~0e qP -gو,Z#pp/bwuưr[^[JP -훥 ?,~'oŰ$qJQa& ?,jw%Ƙ'ј5ܩW -(APD;VgOόQ*owkMF!=F,l+wdac[aoJ0EOF Ve&/DS [n&>L=Ҹ6=#', Y_,wF9;z㵋J  +#(V#,?v>+~ee0B!ՠ`!K14vvڰ3_+!(.An1pm& =zOnU%-ƄAeGhCH2h@AE aAB/~c?6Ri`7AYeA3Qy): @b4,~TLO D@>TID:O4CC|A\=qr}()Oa(M1ͿɆ*tÄ}b:.TYhX VU>!DkdDGjp9%Ul^~/bJ^)>I@qV"hct(0]1~EFTgHvDR-}/=⡪怄95nK+Nkr>%0Pfw(9R!eF&8^C`wJOi%K10p ?rּexbqyhEAdF[12Ec-6d7ȩ@.zfX/1faS,Ll87}wL.gJ>o蒿 jBx!PA,K9Yߒܳo0*f|Tma[ /Ç(c>j`d`Dٌ,]QAN)%LDtJ? E`3bve,3*AEFfWe!P&1jnģbdJ'܂xqGdm -KGX _,'D7MG.%F CDf\2ՌpKr/ -K3ԮV˗ qaA/8P+^n:Kw - `{8qQxNbyq0<bbJc$!{PrhwW*'.'M) .$:'ũjhXԴFJ\(S pa- e_Y\F2 % WjY1HߋS}e/ A ̾K1,.00xc>}G7a!DǑT[ @>D%D pјa1['`)µ~rD…bx[EpG1IFC/h -4QG.qƔ.g 6[HP䝥J"/-.C:bmq˰RڬFSԯQfeCp [E̶{ x/0j`R'DC N^-LpO O6ϔ-Ȍi[yatEBLNxX1I%RSÌ\Yo;_`E ⤑`S8x~Ϙ$ "(w $HD"O\R~L/ந$ճrJ*ID"A2z罣5O -ఉM S/Ī-!FX ezc0 1%bj3JhbX.FVW5G؋I%uѓ@XSg.u'&?s`lŌ;0+p"#L &LY- *ofT3gg#g/H9^XX lW -M)y¢gB09BUae2LFFt儺RH F&u0xN>yza11ȹ7 -Yl=@ 9bKEO!CF x*jVR ?$ u_#NKFC dKk0'rn"#nuBp6<~ȕOْ3ᙧsjw+!$Wo -mrQ:F4Le!/r8 -)]yfo)05]B6W30o${xzQ4]` -  _ -١M1U{o}\m+mbvjn8Q+@ iDva" E9%(ġY|7! ,8+@85qcصYFgkTFP1ytWjxa߫p4xmv@ky X3*!hr23 -bQ%~FԊ2f4?ߣ -fR,%r"c!cLC:bXjo)dYsQ|p,LHp8q¦tx[6#.ƺ#|-@fy!y&'S뎳uR0RG-(3P6T!e!E\A.׈PA. 7u NVqWj14RR)&DIX\-E-UT#qm9PuoњoֱHS9Rz[C$09!( ܕL+pKBߙKH)WJv/_ה_Xx,b"3]jKysLWz$yx3〿b!\eBll}x:gMLʈ81EpkR/!=awV7301oɏ?;m7LLМ!塖KA -bj03g@RAP./h %>Z?&VetXz, s/6ZZ#XfQAwGkpt^`zrE0*# wiO)u~vbD`#f)sG g`?Pj׾$Sr3}e󹛘qqD19ǭEɩp>e_r`_ -RMWE zʚ=~ M`0 )~N%ؚ-ȴ%f,"dGGqsƛpNC)yu|ښxcQk G05|̋bAqӐ!lOfU36Kގ8HH#lߒe&j&['E@+Bxx4ʆ :&p`LJfhX _@ok+M67eaq2D4FN)#Gr2%`l~st~Q!TP7}cN("@prR*,f*@JQ_1V t0GRኹQ†x;M1#,GGG ^q UL?~{'LvP4yb*~\~BVUK+eu1;%O9TQwjbftJMMpB/^a׈G5 fWP3t^I>ʈY?FW@-QyLԣ$ 3ycn*ķ0ੰ"N{)Z:gp -0@6ƒ &HE䥣ЊO 5aL3u.OoB *AZT;ZD͡˚Ykr5W^ &WU ea~ ,.27hQ=e3 h %y scO\1EeZE#X"J9+<^31u|x>xO/r|xBQp8 w6<Ԭ]c2bj.%ˊ n'R}Ư9IGA2Y,CZ13rC -j5K̲,e*P&ILE@ L - \{%a` &pJ0Li;sAyB@9A0p"DyO,'Zfd=1Z>Qa.1q\k3ϼ7/RFܥ̿p0NWP `1 aC ^iҙ@Y#Q7ZƵ&Ha0"h V/P4 Aĥr-AB.*˘EDA6'0 n#N`ͪ^ -0AвS)2 j} jJ8{%Db2޸ w xU)Q\tR:jEa&+0etB4Fn(넲.^uc"CHzs _p 𸻇\#hj7dQ+^hn50+04K<9n؏;;$Tjl3xJ[j+La1Bq,2Yψ=䡵2vDxEhAucĸ(;!,}$(5@ -Jp^Q < $>ְ 4(\A.<8+*X/)b;C' % AF$\'{} O q%2:xljIe9~K@4Tl(fŠ~jh)]ؘGt-Df! Ca^dNEgsCj{jSR#掅0o±Jp`UpeC*.4+06q72 bՂU2;LUPcީ4P7P¿&߀+nkL}4]RP*aζ( a/RS<-_!ZfTBnsn;)bB/^k^=YW+=7YØN -"E̾SAuo -{iSH0xK_65AX?-uraI@Bb,EPetet3z8&QZqc"EXϾGaU|Sxq6aPEЫf @f 4 -7Ěfjn%jU3=L.S1d %m^R@|&ClϖNp[)" î+<l]=PF (;{ *_,b6MƢ6MVO*NVA^0$2*$pwaL2XߜBӘmA~1Y^ch mZF`G[bG@K.afk=Fc.ybT2e:v*w2e2 KMBEK)(3֓q6FoU션sS19玥p_M?U0.J>lE_̾zŘ1(|Gm#ؾz -AM20.hǡrb:I{T4v5<}2G耽MyvpDzFc܀LeX7!_Ed%.ZXvWdc sd# VWe1XV֢_ #l - Km9QA6;TRƕR_Hp"2<OD cSSkmGnԓ%A~扁| -3oFxŊQ%ḑG *2 ' z &jf,Ź2/ YՙR,.7`5k51̓3.aǸfI^UKgPxN%pJ{! />f='>`n3.=w6H[[]X]P r' .6l(-^* F -69SXI- i4:[U4b/WPfE h"-n!  -Λ,õe QmqנMLr>)YB -/[|4` Q -#(r`ALkUS P M| NwL2ܫ! -$E`J-ki1)KDFk= MC*6UVS\F8\&gɜphm5Mb5aqDs^WVmuuLB4 Cp|!ٖ#)聀ĸY@Z6G -܇ - -u,5\55@@ 0>*Ԫ\2up3>3M6EC3bK E!I+uLX,8Y@{gB+&+b&|R2ZL@֦xbfJ2^y̷8  ϙg|T.e*qga;T.-/z= Έ 9-K-u0>Ɉ"df&(ځ\C>J$qcvo%6Rt.^tiȤ.mNlEk+19H?dKFjoMma~ET,Rj1)BԕJf)"Gb ~YY%~ R* b¨7`zp/pgQ 舋+ Gq=JYg\ _xca9<7b&azc 7ҡ-)C7Vnlu7.;j5P@D gThfY!q15 =dGKsO`P}E.rvP"SACQIXq "6!4e?c. (/{glʗp)u "Ve O ;1Wfc1PpeT+TǞ{̨Qy}1Bw rf;z8&Y\k!)%:B}؞Ou3N%W N}u -q`~ˌv& w7oNYL( ]NѰLUM9Uuaia$ [Q49U~) -0]O* XN #9HWSo]yWUOp gʄ ń!T'u2^ aDRK3f>ȵR: l%OQQj̧0%Y#5F/d1L }wH`JU}}J*T) Q(b}RlE7\XZ'Er"Z[c1k;fu#O|e,D[ awKKd-Nd%|My?A>.\qf80KK<L>! #/# n\|<8L==ՌV:)c&K-/A.f6Z *p`qW6+z^' ˨)0jcw1;BK3`Ƥ[VVR5*Me+< %\.5R$QY^B^:cǑS'lbMw6h%H#Lq pSt^A;FB$A0xbڵ=C(K6&ĚZTC`$^+P )n5 -LTfK+VRDQ<3$&t˗)]3$ǒ1(ڳ%xgp4Yu7D;lsTω] BrGT7[| B?_?S-,ыdZ-όK&3R.m!]i5dgGW.G)REิݚ.rn'N['vYSlqEw:8>=p MbEFPgj`\BCb+~t7zD۪zD6 ~9kh^ Po7.b3Ly_gK1GB l%$)ߘg'fxjxqqNprA({L;e^L<+xB)Ņ(rXeqSL+ УuEi5RD&tDB%jR%teÙBen TŐ'Dg`A0|h33fk( -ED4@z[=c//N;<>iVzӨj2׋0W#[RL? .3/&*@Vwea˽GU_Fia D?qɄ4_g%th.Tʳ7d9$#g -(A]G VՌ;+HvRGLE?Ѭ"J&v-).9l):0;~ L\k5<1T=W /8 OAI]m@HM}0Z? i5BQ;39_qU}4xklK]1Xt+lXO^Ƭ(`UF! AH@<¥EǠWΒSRjZ)Qr%&10+QЈ;XZ剰qlBX3[dxDeG̮+Kxs<8|3+ZEXv5vCkq|~iE -Y$L 0 IL6H]\Hj.XmV%o4N3' YF^U -\AaѩA./D -s erۂb`P 0G"IYK1'i "ʋ86$.p|!!PYE1%JdFū(bYA,@|bNᏣLT!B^ % dVAXT^̢E,7l?s/x(NT'Iġr( s:^4Mj&r@ qw)e^ *ˈ J\̻vQ=AXn̹C3R08/pki2+C4*@-rC.xTkBkDw*Y[  ]"& -ٸw9= ZqO}f-ωP~,ijcU3c !?OR2wGhβѳ+KPp[: =1K(z fi11l`&0V5qt*n⾎yk3^l>gSn<aj}/@kƊ>v1@~s!Vy;̓,m&1+DZ|Ca,eO"$`3hWEXQJw0 7cnT}RASAr/J7ֲ\}%Y9Ձʏh:ΨB~VL}v yKϒhe_ =u̾\p1yYc0: ( B(`>3k2 =q zgG\y>N3;qǹ3gSP;' Qg҉ūOwbe nMB,SFM~tCY_(74hiM¿l9ahl ŕibjuo^To](1HCp%߸F;/? IB,О3Obo-)+1&:#k㗑_py#~|=}&`boz`8._"A8 _$>+*@; -ɭu Kq¬*[`CMJlYt? oFfse~Uk $AV L]uxh ڲH3LE2zI@#<>U̓ kGNWŘty @<,>&?Z;09u. PIU^YL*I(>0}*,a1;a7LVn{Ų[sPq|:^pA}x@Qzd6]E- aBQy3Sbr_wBk+!,RdBүHzaV?k dAe{5|Oʉҁ?/7,$7V 9H‰psv\%/v#ˍ@¯_:1*=AhrF,62U¥"j$, x`$"Qj W2o8h#Lg1 /FY?I}Lq#Ӻq] Բ PZU| A`ze - HpIE۫&uY3qwC1@(m f*RU U^x gB1le艓X* IjEDT*QHq?ctj^A]O㾐UhO~Qy_g| D%bҝ' ^X[ńT}NoB96q m?X6Xv$̪_"BODr0+.ب=2\|SW_,/ _?S]6NfU튪Y~^;O Z'"E{\]Pp0 ]RJ%eaB- `*ǩQ>01:w>-)(D!|E8J]B}R0)ffBMduX彏A.) DtyI2L+Smӂ@DaRUF8'˟ð 1Dkh35O %{N -B$!OqH`01eg͑ܽ\ÜDk -t~i Ӑ.Q6L6{5+xSV -pC!,j@\vĘu l#E|BMs2 -픫Ꮅ+{H=`@ -A%'SW,u5Ҙ1%F3l8I\A͡d)h9^2c#~0$=1:]M9!`13s0 wqCP,2pdS^|wru\+OL -C~I]"IYˈއF Y(h Pnkڅ9˖+̪l[(1HH&D CJ([( ~a&p[J%ͫEt`bZcLL'}@XbG™k-eS-W#1xO^/{Bgy?KqT}fRnp Sw ?J)r>%5( - -ϳʀ2<$~N*]pKߪ-cGa!ڟI|>-C -  ~-_4kBOH -z Q1znG8J!W'6gl:~"=Ed/qhv$2/T nH' A{\ 2_?a~!Jb\Eo=gz3jw;8\ p\}0!ǜK>K}*ܦ0f" 7!`!+L%R__L*T}Z6[<Q=@"13Bå5<X c3*4}vXh<ӏ wv^8!I+f5j(+3w+h1M$0jE 1>T#MX.^.sZ ,t"ЅyJl3ȪpoeãVx&2æ]hs7aԩl1Ժup烃K=!a/w+b ľE+z.\n\EJY/pBX5:%^e!5=L\,J 86]O3C|bx|w>иC- e.fKtA1v'mY(d. ?&j -`?52țdE"y+1XA`\HMC љa0+YHqzf8$` hAkD:h\O >HE C p<P;ܬN F + a.!2vCP`ۢZ; YXa`r) ZmT1 RtMNju>ч=utJPWVY2jp͌V7 eHH 2|%ؐ=6mD2J]2$3a"| -0&L>p̤@iM -6\DRC1ܫ/BV^RJF= ɉoC ]TRAE@X@ Nlg 650;w1)(m" 2bT?%CP4TN F#-F}@6%Ws X%HVWJlƝ|xDqJ\?Ҙ@wA5 ,~ZXzE~>Pϭi@QJ;@Z#\c -pUfn(-')fQcLMF`9+2tq9e@}1Xў(w!w/eiYGa%j4͉7B-n*hP3qZrSQ_i4,sdd]NPC>+>H8O.p<(6p4<).9K|tCxV)ߖ7<;CJ/3#D+X ^!k ( ?,4~ᇹdu^8 u,ta_,{qĵ*comz[X~sbCzX]3ЭF]2#_lT+ -ɩT# bB G! b` -gLa0;|>Rm"yj``?r]G+Օ̘<+ eOhUhke1!t mN@S=O,0(8t fs :ݨT%weL5F4q5T؈<W߄jmYFj=]F -㕍# -9?(dd |z(96*H'Tpn3 -z>[)NT:]\2SF<`hQBYf;["5>7 3 \0t"ZJ0e-)@J,2oH0-0Siމ*>S+!0*-гx%fPp䔸e C镏ﵐ=!BވB[ݡFvAxn Gv֢>`0ڼRD`u'a4w0 ٖx܍sC ϑ^a L̈́* ,ȋ3>JCe ,dD-Z竅cBbqQ_Qp/򒀣ەpQ.+ޥfCecyٽ_dwY#>_i<⡕L@/w{+/Q%),xf5kU92@v@bmfc/@ꇗ -5:Y5r9X=N~"xsr Ǩ,_1B]50Lg$i34Ob>yרA.bf&`sLN3D+$1f -q1|}U6ή(=J_Ja)$OP\6M6ZIɦ|TjbV*^|V!\w}TĬy5eQC p=VrJˢ \2-cVBEJZޕ>xF2cb°,^d"c* 2ZqHVw*A`CEH@QkĢN`ss9/’@OŨ+7AY@\;#j'@͹wPp\,mC|\j_ XfYMC 0H@u.mN;3:|WN pX_*kV0|wXNX]!i%vL57:6BTa{Ab#|F@6^}"Ƒl)iG`h_˩eG.V*6|伻lRW -O B+h>me%G &"V $jFj5)XD=jhꡉ6uFwȳw3Ji u L3J^>ҡUqOO]q%EcUrT̈́_A|\#(fkQWpG #&y",\ ]VD1{x *ߺ_g:LGU_b`F~IRǘ7`O|Bmj, d6['/1oop,;C`rd!D cŃ\i4~kӧu%_Ty&eL3E˙aèǩÌg>p,8S%Fm5):xG*4Ї*T5Q P,L0C)WOqiPœLԻvX.!S& Qhn'C\Y# +C&F _D3[(GLG=B3gbYGulgBu(,8-iv AOvbC]z7{֧Q!!7Q:bHb9b!읂P',m# -nP\OO1!!mP\@eFmWX+mΟ• D )7v2jwah0[Su^.Xϼ dǙGc0iDJdA C`"lJ@(L$UfH-K`UuO V`>4` EDS0ݍM =+j aD#Ŋ}"ŕM M&jй/JHj za㌀cAP9†f2Cd.(XY ⑖:F[=1Swdѝ%^#!8'r\~q)މُy+ptq;k(ZՍ\30x \bXLaf]o - a*V g &K7a6^G͕K5"[/1Mʚln!7(+ qrj$.ʛ%iYpa!Y@ pTTB@8 w(0n;iDr|3,Ԥy:\pK dirIS.^;i% Z;4z h'Tr0"i8;>X?hֹ]p3nTxźD^)!bbAPoW8"D: -6dy>\ ld3E 0Aec5śgN71xXٙixTqjrau]1g<=d0嘨͉#vƓ)SR[4(V`Jw{֤[-px:F!.zao|& bAb2VGm˝=:T "kQBSmo,+^}K䂷/; ' -b:@ͿC#5^pv+tx=K y:`ȁ%Z+b\z3<1 [DŒy1a`ך?i͖D*~Ah.Xr p6Z3y{F:PοZD=߯0+OQ~x9W*Xq;w3W jk+:&S 9?&G21]fҖͱ_9Jt2‘LيL|&dyw#|qib2z];!,ܑ E3Ir8d?a[uǕTxVn/\9*:Cve_Ch,xn]H4 -5q`l5&m`_ePQ 2y90kld@t`:Z8/%~$%0%,$Xta*wbUK+1 S-#W/1X؍z [K*Ffs 8/A5e6u&°x^[W|ǁ+qݲ2!aڰ`+0]U8xz*{C],u.]Evݪ>A gD808 &֋H='%Y;،N$B!Q%"!-&Yy-xix4J %3QJ(.K_mA X }$*6 M,|C*3vYtRElPV$%˥Ə\X22C 60Etk *bVP*jW {O4|JUe -lFHX@OtKтvͫ\8c{/De[_B˸NL6QGw2 O܆kfW^$0*jg1bV1V*o;"0XyX5#ma PсQ0)!V"df 4I&`mpZp/(@C XAFn ^W,-S .M qpL ~yCieD15*]U3SYv̮s1rKR 8^Eu$ g|B"CC8>X(2 #|Y_̠-UOr8LK^WPu,gKdž00Y-J@Yz hbW 1KAUS̝>ɂv1ʘ[!Hbda -Z"Ę} %^Q+RL'Td(J@*>%|)h®p`Ji?v -{ &?E_,0 9v@>ƎN*UZ8}3fsd+Np`rtJHIu f\PqLopljw{l,A2êij$>b^AȈ%(|&? (٠t'# |2Il6.)tl?WiG+. -T0ڼ•ƋC"'fV3<6`!@:7'\|+/y11>'Y~b dfmmX5i:Unc~}=ab< |n7:BV=Q3QʘIL;uwPK=*x*`pnP1<0u7ǒd!wd YD.{52LÒB0wz?\,&n,O]F D Q>-Rl10 ka/&?ύP,߄?ݡɒ̾c煹Hn^;YL7~*H"mTԂU24'ߴ,̥03b_lÊ!am(VOl5'NܠHy$-]ʽ:3Pnq=Df5Bf/S3_3A!pL"^a^!Φf揣>&lEe ebz!%L0F|e, ,33!Rɫ9Ծ.^%gfb:nQeg&U^O5,sLߒo"~g'i^KEjg/Kx*&g%z#QB=c.rw)X9:^eQPBJIVr^/Un~CSHXd 3> ->32˿l\eݍcnxjiţTh kgx3 eޞI/zY9cL> >RTՓ0fs?5k`:fɰ7{_Qj!Wec/GޟxCկ}J<8mN~| ?vQ3 :OjꟇ3)MR!VԬ r.- kX /<5vЕEU)5թŞ23=E=GzfbF+3CLHxkg_iwa#ĿGqN<тRǫa!ļhv,Lm%QĻ氂#7,j<#)a`ZD `V&)o(z3e/rKݤYUf A,f~):mGץ wۈT` 3F6,dHۚ +;]ڼ5Qlūuu; ~&q7$Ķvp N w $}ζń"i);p9$r4!.ui۳?!t:GdGEM[Qvdۇ~B $NvfJE&Yc+ `j GG:)q1KsO3/QBpB^- ].R2t!%̯;0*)Ұ8$9;&`l~&'s̪8 g˦*R遖2!0"1j z5dǘԡ -Q|f=ykM06BiA1zkdz`QP9AT0i; D[ܱw& -%cmB(f5e="Bs6Cn^q"bqClE -\wAf﨤 )Mƌv,e/SKuM8AˈPIѸK\u+Po㋄퇇S7r%݁ ļ$cŃD\SFFZ#W Ѽ!Գݻ㦡9hae2BVP[T6 _F0l7\@U܍;( H fbh;PG%jifZ 8S.TF.̞Dë0"z L*Mȸ,Z, -]E|$#5=Հ ˢ+>ŅgPfY1^dM2Ά;58u1‘%pq^9>8- -/qҕ_vGOS\=gAJeG䆊'P8%vGJW ; -x`\5Nj#l5ޘLK-OIJ=Ldp\GFր>w76qՍ: Q86DZ}_ŜJ OF_KbFa{I-|$-OJ@np \+n `*P &b1Njܬ2`e&kO`KhxZM|_ȁha|)59P2_ -,)Pt_ sr-4R lrAʰR-b -.x`D5B|c#/0y!M& Fc_؍MEFWD&+!Q: WX+I^a:dXt;aG|'-㋌am\\y(/s݈"MTvr* V$Ma"_{y1gd+#s0-fR s к7(Q1ؗ :%'Al34H[ZX{<-ڝE;jeDt,:m%c[EgR=| -gt& S0tcwoڰ {Zf1>bϮăTFrfZ ¬GTŷfJΆ/6@ߖX$4CqR{H=ũX$fc#a/3kn 8s\ަ#*w<1Gp8IN2̬aEؼfw5/3+mq3,/c-eX%NK.MNbZp-`$(P)d%@l\|\ ,5- ;Hp*%~/ ;DCypGk*</Y1P}TԲ9]YDB,kzҀ@;1[* [}B0UQD -9|kT'; u[d5;-GWlt@jaIm"769U%] Xf*)f30yMhzn+bf>f`W feLjɟjϥڗ1#@6bh%!*>,X(;ȧ7Tj L9>dtn! :Vdyp3~Ij'"0*w/ ƌ`|,,.s.-HqeMݭ[PY$l.2펬cr,"e^ Ļ"1/tqp -W;V\{Q &gJ{$0\UPB.]2qmbٙL-& } tҝjUaS2)0-jhXz8TᶭECQ?i%"S,\#nX' .?&X͞RgDF\ -2 -ϻQ -/73-.uaZxʣ"<熫,~7_y[raEgeFɀk󕺲FKyLآa4B5e#\+ל<)mV[`H]@X!2,X!`!Mr -7FqlN -8#ma o71J%zQaQ7peKUaXsuLxܲɸ$:J虙4.4YA4,Sa?X׈U1\nIgp)֜G08 g`fV^}L$uQ"/gK5<͑aHCǔW}8$9&(%xǥ9W:|k<}~ (t|XDb)4¿0ߺXpxh -‡Ŧ` 0r!},A_t?E>rbnwŔI()!gZЭ=Ƞ` sԬ~xqgR ^$&&\Q4>gDZU.{3/j>x=ƥ4!,-q2x 3Rz% [Tw-Y'%,])AjTˢO , -%"HL˅At#]B\mRSpaԨ5+5~Q|UHUӖģQŚZ7[b]qӠGP 47dX.{Q_z@W\g] H$Ɩàҏc>,B=AA-( -r^" oS?3Ĺ S[1"El<+h"YY>s~aXsJVzoՓ.l(;0*˩t3X3U bdhw378x`S\Eƪ/\W|L~Vb t>#iآ,GWj.Wkњ9<F_=ʆS2m ėIJ錯f*DX_%؋aQ'F@ Za"k`˽vDfı0YXgd - +E,xtKrNU0f/<. \Ş#7 [A֦5;43J VN̙e`z}j[/ Vi#\AbYw "c&"ހy,^cu 遥 qy+Fp^2HjMi-c*Zڶt5Sզr{cSeB[[@`¢k -D8n57( wYxAejcRp?,j3%,w`OH~l ZQ]6Q|s+Q2ٴ'Dw&moȾcĨ5kw -#BXB|^kY*qjOwÎ1S^Xl_ jELT2/.k^21Ltc| NJUhCY#D6?!`b?lVE !0[_ÿ > Yj -/lq)nNZ?n0Qk5⤤0o53Ɯ\'k@y'͞ߋ0u%,DМcl2$ƍh.VeITlWF ->34Lxq`k\^I11 }ʃ]h5c%/s(}=iW/P24 ^p[$UJ4aP\ Du -Ʉ,6 ldj[8G82k)eht ʖ t`Fxܚ;cq b/\oFcr>lTdrI`+H#RfZ*p,F_C8 Ʊ鶂-fpD A@|VJ0ƪ}㎦%6͑**:<^B{pxm#J1iʆ!jg,ܲ=('TĊ_@vmc-mb.^y zWN -q(&n]~'|\eVW5W~цɊI]+Qw"-XЁe|!a%.=WHG(e\BunZڊG3ƑPk l@Vow.^!\¢f4[#8[@raHQjJ"IPPB} WyHk>egIMAéb8Hx6W8|O3^8xv³Y \1D d&G[Heu,2Kq.T dNT  -.6KQ6i)]¥|E$ sF]a+<7D0bGte!:xee̲-]<rxY{"07>/=;0\!El#WMbz1Kՙay7o$URA*7>Ha ?Q'Q$j;=I|sI&i,rm~X*[]Cež[,+\m8ܴ6":%yْ"dp+/,^f\x/)>‡L+И|xsJ,&P',$ 7XAyCM{jMSܢaOVDɱj]󖲦Tf9Sl{E2[x")0+闀_Hro38angS&o3:&k !0φ1(㘛#QT7q GVܫ_MfH(I=*O3dtSxƢGDק/O'rφI_:C?Ms?I0@ta0Akq{q⑌Mg˫ݕDy|a܉/j(jgD_W3?\4!@K-Ed.nM}1کOE 5DhVwk -6#KPj0lG6#,Gjv*TIY⯃2GG|hvE x%T‚;kFsG)Cr@Id -"QKm !3/"}QT<$D̮Fuv4DJhA v/xd}wJܿ,fvq1[8[3=1/IEΙfC`\%vQ0BqB]  P=-_0=ҡd*57쾘V z2&#;GnP|'P"ޒ$sZ ?Fb>fJo@ !pJǁ R"qqVCQ~b-&1XhxOO >"[]KlKߒ]oinEu SO,Y^{s|u7ΖxIiN;T1xQpb9)Ɏh~.pʁIAbGi{>W9 '@.&ya,uS 'x̨|Nͳ83sϷo*욹Գ⊍^MB ~udwRB kuSot[lyIޢzϝp 4=T%<5jn^xwer׈%bABw40$ DA4"n4w.c e.!_zʚDfUڤ-i 1+2˖Âk#.[u@0ӂk#3F}lcF3 K-a%zydr`e.p<:]^Rq*.aȅF(pE~`9ˆwrT|;l_V0Hkl,KJ2r_!D'S6+7*>Y -nbgpC0 NEZƼ -.0%⩆Z3),1` *vʹ@@V_ڂ/- BTeRm\((LHy1E8ǨPMpl 7 BiWHZ{rݻ\mIeL?|gcifׁ -T4eͩi!:N 6"bk?l Z !Zpv@ewFb-tۍ^ -]+#R \6I" - (]|ڟm'qu_ J2XTzclrpZ 1Dk/*^&8q$mLglW!O)TKwV-!f(~Xf@cz>ZzT@q JlP˫(#kb?79&fc2L&"Ֆb(f?vux2I4#+f'{pa. <2 04DkTŖ")0';{Yw6_8dFSq_+&UVuyt|)mff]ad +Qc-`2啋֞0r3$zu4bGa 묉+,qCo|ݬT-.DGNB i0ASffs)%c -ji1Z5Vu"s+ - ,4) )=+31:臉 S -|x =SQBb_XK6UK*KĪwƦ+XwP_΋_ E8 ojZsi Zy#U -rf%*+E]hPQb+b+r,4NG8I`|8^Qo\'Ki3.!lbiBs 5cFzR7GEMa!*ni$ƹ|7+gR;kUkܙ}-;!Ē\IV'ܫ nRr|^ CayKJ/9ɔϛLDVsZl*AP彟ړ:+t@6o$~3++#`S'! ;kbYfg*A/gDun>U5<I^8Y3:{ߝ^6+z9X= k}Z-pf#0Rc89Ap|D/j.C|tqS1 -5 _-S#dDt܃ˆ " ny P} `៥1zonjy;4D+/̡`PxS眙.e;fgZ5Z3>eKcݳ0]KfH+c36hex^'ktj^%f VyS"ǹ| y>E?ZvGvjW4N -`#})7 T -;{<[FPEͶ:V.٦(gZL=Ef!b>I`P,H,E( p?1(UԳ13oK nX'\ÙKA-^(Bl+c(%l n|[oXdX5QtT9` $Px5ACωo]7r<u1!q QQ /j"ebۆ&>#Ž A¡s*bi3%AhKY ƥk( a0= IdZLa$:׈GEjмA2">!Z38n`*ʇT9.&.|l -3+q\S342oW$\ÜK8/||D%,s"EX= f, Ԡ j? )xZ+"V2Baę9ڒPL=2P <Rt⤌V>7 YJpCHV5WE ,E~&[>jScS=R"XV9 kg ߩ92=&gfFb7p y -$-GyV.w*P$j߼~f/d$MHk_nS ":Tt  Q5Ao-_YEW0ifߓ7|?`iixk"rt/lK#/f`N. VLKQ~yM7+_\// ;%*|\ @3]J.?r&rL,%Π9\OL!3/3˞zfy-^ zRe[ -hM~dlqK;^rĨ\ -Ksp}9G@} Jbe.U}ު%%jE-P'@4dqK BQBR-K2FkRM̨'߬ĽzH:V8i9U=q|44CZ`L _b3̇^9&/a'Ju,1+3ԭf7c|QB$  jp lbDbr*o7w=M88H%*Y<,F/9yPQ[VNA4 *#0,.Mt]`xv8:M@m CApP1BE䪛! *HN-:=x61w? F )qQm'pI3dHc} QBS]~rmtFqL!BS.J6.=Q%}ȵ1"@-?S#V1E)!'rCc3%bC:5N5+^&]FB*X81s&NF=f8d )f:u/%RXY!|el11Bsureb,;XԷFlq.pي -^Q)/\WY`E,BclQw/Xᘨ93`pBb 1˗x!;r31DB e J`!~RŸU̥15\tl[t t,A= 9qC7s)(J2#+D 2@iFxm(- c?:Io hV#bpP&!Ѭ2$`cC bȴ5R}k@Wbx:!*c :jc ff  Զ.fmS*f\q&|<}ⓥ/×PIps͑CvMbag,fovZOA8ດJ-Z#M:1nBhh5':|aB:CS -Ed> %BJp}0XDhmw/,ȏaOXrES[9˸5^ #<١j W!TŇcV&bIk -6¦ KP@fL჊;%fvq|Nd.b 1%D.#k7OMG%Z^XhXj,cxDOeQfHxNy& -VLH'"3z:ycypz8ح_Wy%s)=<ܗB1T\|VV+Ig;08 ܸh}b&378jM).? -dbgkR0SaAJtmD &? mD@JW姞i. Kb+Y*=jv)"o)G_˩Q \2׷l$1Ekt¶GfA]ƄDe1ڑ3F R40ht'_p9b#E,ǜe, E() ޒh-k{wVyGDKܩ5EErZau2)]O>Dgd +|fe ~!363RoRkgYo&(P -_ڔ~2zv"gmt$O|@̤EyeA8 G aw=yY 5NPvF JI:"3 ;T Z;'R_bʩT,$r4^ )3!5 sio؆MkŰ) Z)+, RL$Ȏ{iBKZ-YFZa!YPќ"ԩ\NKWqd핒o4f`֦qii;2O$Aq&ҷ1o <˦ ǒ?HCC ~fp v w0IbUT6Ŗ+u|Ly36LP?BkҎHyLA]G9>&,'{C3*m*)%lnH`V"1ͬ++ȧP.+a0 OK@4TR$隕 XP2ޥڏyS0|.R6"F#9`,[4axfC 0ڑ9JFY4F>L/oh -FRXCc SzKaUo=+q'6oncGR}y{%U,$ 4SL]s~"l7sb*4qj3J< wJ`cix %$L $QIjO=`M_b ^0Pʸ8Riu&~![ۅrDPڮUw.R10/|Aғ.<zڂvu/haoִ+|Uq [zIS0B s=WY!WuP}V&1K0Rg]Hq6/5 Kq`Vr5zȶ}!è m#]ԴS59(]t*vLXvƷ ɋ/_P 3v:R,4d#!qo01']ݖ_s!eӊ $8mnJ4ݦԺakw .f|`vzl"mZ_VLϢ͖w54x,2#ARDTZОBlKKrlszB3r+#]hr -[( 1Lwˮ#7;g^ -ˏ`.lQJơZA Ϡխ0n?v*V ~:DYY%Q-aSxGێJz -*$2&r^ -`I8,PmӺ -B6(:>TI#jqXwcP[Gy}NA%]MJ(TG~e6L5P-yn!O\fWOtBp>\L6EgZ!e 2< ̃Tߊ}5ZrwTNn1#IL;//XY> 9{$V/:%7%úl*a vLE]7$wzmuC4h -¸ _D/'!FC\q_F1ޏ,/tB` CI@(mP'7}Qv GpI#v.)A_.$Ғ2(@t;qppa|W:ݴ -^ -+MND_z?.{ 琔2fd%-p[# jg8L>#)^!(|'(0z4'ʼn6 `!@m|+}5~!gf+OW -XF:GPUڂ։DԤ?Qz,f`K4 -*`j .{FY,JH R lX ЋF-QU*! o:ob*jAZ'WCTj,*YWBbv 2j -D]"hWfj+?5,kIh {h_{|l!kG4-)q% ǻ"Dp  C};z9+O=Ќ*R}~V.*O7̹Ae-D^ u,pD)_'u&AzS{hRLYIICaHS\fVn Y%7\+3djN;*&C8_4 lm#[?%pc Vg3ږr,%"+GD7%kk`˴%w: -|~qO40#f9lMbSUq>])X$ -&ֿqa$'&aŽ~x# r?tFYpi8̘:+zĠg*|,2 =tTs!ۣg@fu` -xwL%wsCFS1`qa/U ƶQD -ؙv+*~Y+/cMW9~x5&a`b⏽r%7AL[ˁQ3ǡy8ʍq$ F%D >LTP$Z+ - -6C]Vff2ݺH2eN⥙-7U=Fi*{B;E{AoOqTX,ĭp`d|q0yǘA:ꗌ"=~% Dt{(ih#@c}YC9lB p",KT>VUn-16/$eq =c%,mqլ%Nuro2ƴA |&H!fPFRP8 7#UPKCXuw+x`Җ -+į"FX!^ --D [4 >WS,U7)M!F{2Q#eڎSE ˻)[JB?/م/EUN4V>2M 7@_0\R 2#a5 #l tጢcEo5` I;St@V)ؖI{̿&kƯ%eʄu/Xvh3b?}+޹NQjحҏ)PphVrqM*4 @jt˂\#֣Ɉ=BwDZgoͼ]TɠASԘ@^M[ćnlWE.0#=؀$!mz[WJeƏPUsJ|dF?5xj1."l'v,Ezk"QYg7q@f_y"bKP -".#b>gd,B`BpTٔi%2˓Y+P8£ wV!-LhV$2Q VWRYZ2>Cm q%zKh$%7v#<5ʊ׆DFk܋,j0 &HdGX4v8BڲD\_#՛лqR־yM5%@~~ j"` Ez - A?& qJ4*;łr by-k1gg+df-e -{s{:lh-]"xU4EjV+V)PbT~N jk+<6+d@+B3GY% %wVCMN`STKt+`@mXEºeM]@%7aPAT7zEnZdK-S"G/fbZ($[.|+ࣸ)N -thzASRmVPΗ|Gڋ?,Nƒj\ͧ6Q&hRwV"hłQDBO ?ӷF>,)pI(:t6-F#W/+.epqaB_.$FQ3==8z `WP8LyJYij"F a<%‡ab!~ {1`T]Sf4ů=΄9I!!:66`xЕo - k<=pg"Р+4+}8M3}ђ/W[PvMOoqkDlž d3kBB,"g4tXH5sk -78_g)Hyׁ"1BJAb;fFRg+ʊFL+9%_LBl/`M~fpqBm=1@fBOT@}EwhEUZlSxdLEwҸ?@QLZL"pAT_1KC|*q<eX2Ca&o; - v&~ r(Ycf0\X* *19=5Ey0a - r[5%pj0`ś~RŹbt + (rf^_x@L"-&4$ҫcRB$SO9zHwǘ]L;·N!TcY\? *P t<;X|SɬranHCDR_;߇oiAMP8^9^{f5Ijjj VVey! 8g$ *+m$7[H`> WK!@{u* -|Ab0Pa@aZ*G\}b@k - iD#G-7:T\cӼ 3ꈟ Qa Y`"h|2CJ$_FۜT^&L|P[PPUCkyX[hTi#rUjT t4!ԲE Zp';|E`*)nCuVF(Шiki0C, -gX!쫮T|MilmhJʙy"`a5s`dbZRcƄ+U{0<@Zx-"pZF1-050=!EȑԈ@|^+pNJ 2$`\S(bb [Qkt_@\U_•yVuQ 0clwgdJt$HL 7MlB1X_cZkC<¶ad6 -|\8=qpY-RD5|#J2BRKoĜpBjLq5ȥ?o&%s-9 'ɧw:V=rpPZ8H~rwq~+B|n`y-'S>dcݲ?Q4{_?* b^ -Z*RRUKMp{t咭[p<֋ng:HZD1_楻_/RfI>{,z -&n4CK=k`"_#q1Zg ,D~z!Ub =1yO<?:|Sw8\! QX|AWʦԸ ‚މu+tjW9D)nIHLCH3ſuy.2 Ϋ{G.oBM'dYiǼַ-](2˸<),-x2-a7teec$P[Pl&D+,FN2q}q?Q!]*Ve8/ װ`X-AFQ5, rjZ_FeWEԿw%`EüYEPk ֡׳ăM/pǍEs~vJɓE]!u 㾲)@-du\)vU~!!Sr6C߇OWk`쇿Qb*n6^k2 P![ǒT{`{KH0"ȭ4exNPE%yj7Kp#_L;D*I@ꋒcMy8) ΎW-M@+xW8QM8 W,$],QP~RK*[~x"d SBdpKgK{؇nRw-xyaLh57@q P@{|2 42PhŬv3d@-*5op7\řPw&C7 jQ{vB8u)Xbm!Aj[?ĄKK Jx`c8v2uA*$G2.ulʳAbhb:~~[;R ʡ/N -rb^v5tW1qvc*Ig#|x8=eY|ϴ*0_ 燅7(UgG~}{T~O)ݠaQR**rd ߟ0g\^`!_cEKo sRM۬2 k%Lbdxx /WrXeB-l5K3`ĀKh5G$Fswwɗ3ʾ/*N*{?^iϡʕ!2G>S漢D Y*O[Eþ (/m;]|>TZ| ?qYajQ>2MHpv1` S=ebkHUrY -n5J -vks\q'}8a>)۟C`e/q7ߟ _ D\15-]`LJN :_?4ݑ?**Mk+uւk^I X<Ʀgۂs\*Z]ٮ ya+OxPA(VueT,+mb?2i xIkU_x&P /ω♾(ȬD~>t"P4!-_IqƁ(:7ZQgOdc`}dZ<>m.9PŲ۸Jh5ƹ)+]An`1 -&yx735 cDʶlZu& (G婗_Z3җGw7cd6;sC+q{()P9h,\at@vl7*| -pkaN -qW=^я%ʴ>& ne{ Rp7dšXוVmr{eC>/N?r܎,.޶:V /?JAZ3WW?B ^hh)zIbQffeei~c f.^r&QYvŠx&0%9b M;>j3~?g53Գa3R Zz0ڰ1l ߎXve猩{Yi(lg†3wf(]B ϕfO8*T~,"ˏqn®_Om ;'!k}gTO"3?hBpeT)!UH_z|UCM ۑ/3|v#ۢ}c?hpUUl(ceCŹݐqڭT{pCcj緂$gț栵%-r/|_!N\E8}]:z?[dw@plb}5"xZ\U4kh/?n 5cT ;nxF tˬ RիUOyM>BF9zDh]`)].Pot$-qHxO BՅ^(l7AOTD_٪PG_*98+w[HZҬ(e`RcPJ_5Qb*&o.Z6^Ìsj豃GIĩwק@%v1.`-FJ3LT.HT0vL(Y]1hnۄ!ipar9N^]lq7x-2+rTT. f HNT_qweD%SRA0%psJ2gP*T9e+a-3.ᗨE WcWK%gp>?FaŏNgk\S NcO]*^ hH4C)bQhFcߊl+U#ij-pДz&_(=1^^ѠvT,cxgP1Lyh+6%̈ӌ'S%9Hяb*&znfdNIY*8\B!gqx0Πy㶾r,ZGyW\ w"֥*ä[Rڦ\N29Ĭ|jjڋZ&f% $1zAdGU:m[\FLh8(/;%rG('n -_pL 4+{FI|ZV8/td/b ǝHVժŧQ?;Dj_yxOB@H]JKsVc[uH o%_">Pn&_H,n󔭟N:~|xo&d -Ĕ"/p\Xg\TJcx5,ホTQ"z~Ycr‡8Ӻ \$4se] l+i_0SlTdӈ۪ɉ%yk{kloz8L9zy]dyULZ -0#Ҫ{xyĹ.o|L:P DhIRaI~dooosqiy) \(Äf4mʙt]f^ lHJ iTr,np.l_$?L}(d1y߱ 2Q1_tmb=͇LჂpb:BfjV'+TBC%i|(p\yt~!~Fa+L~g-ř[jbr`dTF#/… -LR*_1R;ƥv]o1\]|ze.NSNqo?W,AWFD^AK{pyr &Upvc"+}hzt +RQ}U,{^cv$A>U '\91VN_9>b%Os`gR%{6MD(>*#֖ٗ|hgh CXFDSɟzO5.6"E& -@BTnS -,Ioleu a00PK"1Х6*"@{) !#P(uh? -!% Y(-o#&%TԱ,Ev y`@1q*ola B/bu\a腜!8* /.|rJWW;#~6 -e4*lc: AQMz]rdy#W,VYT~c`oUtRUj7< -J+go/.CWrQIpfC=LD(.`aKθ2rks@K F`"] -sSȡ\k2FT~37bvIN71xE K`0ת=]j#Qf;HN t˗<[i.67 O⻗ -u^x LKcu ͬM[~y.:3gEf8w:화ߩx^(h0B ;er*ygګ."QŘcUޅZ G+R'\@:S-YEbdC$&77`,sf!rcMO2ϙ٬ 8ܾ rfS"33X>eqcPr!Ԭ/E7`3yXEinjI6j+WP4řn\hH.o{/LEScs9nTJK%.&L:zArO8RDz, vpZysx,r YK!,&nԸJ~%s5xPn[#Կ5 j_Ӄ/i*S")<q]JSi9-xp3 -atK7ôLIfxTp a B1T[X|"/,teyR/$CR2%ᅍqw+1{mp -|2|hrRyP̐7-x0JOܸ8E2.O|wp&9Uc1d̻[bgdn!D%[-u77/7=1S05;!T81.dw,]θ xe|&Qv{ʠ9W*f>B9SY&:fy5W,. zP^'(L~.YQbn[ \j嫩fbԮkpq/]J/j} qq37P\Zˀe_0$ψ \Ը=J{V[0QtT!L̓%i#+ Ƨ̐!co4Ex|._D %d8bPuS•e{5B ,JK`ހјtDjk0aa.:B;-O:cω2GYܨgZ’'߹WN̮4BUy/N!0K !3ɹ\`#< Qb4<1\AᗌgF k7099 I#P?QK73-'^)iON:̾D뚝*(!Szޥ&J!ŵZ ;?-B&L0zTϩ~%f]8l Mv(꠸$Fe0Ll !.WNa ppn]P/c_=XG?boK~Upgp2333ι5 ??J#bDA?LYB;0<5DZψԼTIJ/ E ;<_SzG멂aLک,'Ba M˝q*k ,ܰ(C&s!3Þ5Qx\&D&0DRʪoPx 7b;aR3r^W|}MQ&?2V?&etl+|^lj灕;5IqU.h>⻙\quS:ˈji%ٌ;y3 eX˭Kn:,RlA9X23ymO@0m3ucq̩9x~8\'V3iY¿RaQi\#2[U'@̷j)H2i#3:F>Fש@MrK L3ĸkh/yþ:qΗGUPL!}w[xvK!.(O>̳A4K5r sKcN&|C.]K,M :`+ z3 A>j0%BZּ"͛C)x2f]U8x#Puhh;6dV%KQkS@t0?U*8A!lWi( q^>&w1íC9 gT nhdĺrwzwos,2k{"}˼ [tqXk 5/4 W!Kq.8Q3U<*T'D^@e} _10.].@)5;g uSfw/sZlqd$pc ouxb+|KΥ*;d* lT/C)x3 -V  γ~5 }x ܿFk00rǸlp^.,7s;LpK3+g&oxqus|&> T&غisR+ܾ.8`*^LKjfWPZ&Q[繒Zᫌg3, ᗹ<<x0 /KT8Rۻ>W;aL屮21 ަ#f]|qx{Y]KxF ^E%8ŗP\v쇹ef k\n5Y:pP1㏃LNH|GXa C x+8.|O 8HΦX;7Ze\Ͳ*fqL37GKKe]pJe}éKsq&)̬JǾ*%g1; ]Ou/.Cs Ax7N,$hj^#ϴ+3/3\bolIr{Yf|̸rE_]q,q-=Gyr238vf}Kno$&~%Sd1 b[ ,'=LK]J1:4;؜B"k -x!jd4pYUa[ x.mfbk ?+vڊc<>Mu.zగy'Ngr3\_xY!%7X>9yŨ$Grzxejx_Gg5Nxh K&6kBoCHaY{R?%)^ۙ| }fܴ\Hp6%>܁0SP2Ѹk07]G0nI|82¸=Kn~%}ͼ֡-S-8?iY._DʎlKLx8r .e\%g5 53kcX/=jW\g :i,`b^!.Z?h|3o|yX/qb 6fw5L -G32 -0+IxC ^pAB{E(ݽQuz"}pec5(uxRKGh0\ze-̼p^>cz;}k8Y㖦f9[ȝdy/8Npq: ,s̶tB|1e{ ꡒޱ#`wUQ6M[8di C-K/:q \`|_yq17Q<-;,̾.^x%1;1TuͿ^7D~E`n3dljwωtASXXpB}7rN,^BsnJ8X/ !14y/hY 66 -llx4MQi4MDˎg:̪2Gv\+ -46MF -JC\8GX*"lIr0)xP,+`H0I2Yq]:Eli:`d:w(*gPX%d\V,^+w0?3#F$w/RCd O"**)K\UA lm"QB/) -M/gBSf~`AF!m!.oަi-B'µU0pE4s2-`S i 4[+UyU슴ZˠTh7`4qA|˝O1{9F -ʪ;b`2S²ncjLߊ7AYuX+ @XBQw혉_<$|ۅT)hDk,NBAM#( P -G&ᘹcH֑ܚYR(wSs x؅y̻"ͬ 4 [A fS)qG`FwWRqZب.7Pm="&Np'ѨUq1u0z17!1QU+2#<8{18ePnlw -0sC3kiLw˗>1.g_j>!|u«\8g|^8eъexI#>nw.hŇ 3!.u1\-ISԮoǸ43!eׅ -tp<\@͘( ;U<&sH YhRBG0("6#珣sEe\x}N^]C&,.^B^q2q|/3fn<<Kyr-0j)夶4/ P,d]BEb50.Z$=s翬iW:_ - b~.5D[zC~Pm& -J@~Xa0 gX \j $lN4-X4O^pdAvF*/_:*KOWF'۞57%BCÄj`k -b&3h(Qfuϴ߹>SaU5YqN0gyDŽiiUct'*[\[; ဵq.!Cǡ1[TRO06CfwO̭O|\<}x_B{Vx_\\@"ƠT LF{c{ va[ p</];+q(եP@e6ՒSC; jVΜl#`0L@fcP0&< .&p+A[a߳iXT_[__8ψCcqq1ǟ375qc\,X a,&W %(|6\K_}!&>}/+UsVH glHdEoD:P,:*э.ʌ]~pW3tA A_@ E(duw3#Cӎ'A=B *C.ś88OÊ\q*PW5aFЋsJ{8 z/a;8!eH<hP`,7̭@Q3e3h"p Cױ4671kgH@}]?__~atRʙb6 :%4Ι.\(bL 43AҿjF̎;+%Y?A<ƦjuC޵\q]bEL2v~xp5)I55iC?ƒ_ 2רanqJ7>yxz|qş=$?>?X{3<"I_6FNω+ó "\ZC5$S?l-)%P_!h]X tEaU̐ 3Qv^>fc^:Zhҿ.%k3Fih3-bth/B_!U\U?O\K'nx+(L~UQN~ 60 >I_D0" KЖ#` 22 R Z7{(!Qv)F%T*41dĢbz,T +N__Y8㿤}tz, hP}RBx늕L>gs\iTJ w=W> q~NY?36VN#o ZUW( AH6 7e] 5v"jq.=wR - -}6M(4J܂`j`])$,ql EKta *WAqJ#L9@C<"t$Ep- rp)$[f7Rdtc{3f+V"R -,l=l;f?dBXeƖtB,>B v0@L` -AAID1>VRt{^vC gV@xhMxӪ"A` (KJbZD5|ףǎ |h Ո?K/qk;*PݼžVnXfW.n(륋!WC4)f5(qB )8jnAw@fO3^/(<"VGD;!a -(8 - -2!OB<`Pqjח^@8}hv*SI~iȍB*׊K2cE^i -Ṍ֊VҸ{CZ!ZpR5\R2\F*z4cQ W-⸸C1>1Þ*!9C[(ĩ+Q1 -xrJRr%eN:%}}\by⹿ls>L|o3b}?Q3Q>R*;!f85uSwy鯤;#cLr?!8rqrSygDwgu*m)@ Q"2'RMW)SV*WUTF) iUx 8)߁8 Çk.eKO K*0iIxw/&e8T&ix-yW%~-}C Б5|\Uڗ f*UevDd OU9[T\*!Zp9vL[_\&(c>HrA _W6F^«kY >`"ڮC#n]fm)K,2۶'$Ũ% ܴ||T$hڸ -bՐ눗WPpҿEԾUbt.^Q]uY~]Ӫڛı;V@UZ(DR7GݝM7sAy.R嚃)Kx7M5Sy7t  R' D-h0r\U)͐WYp`D)x?s,f5Sc<@EOD$0,|́3#1>т80DQE]+Zw_=0+*ho*"@OԳM'њ"/K[otHRl|,t6Rqw1m_숔I"wgeoTvP_li6Z~z{JsA3fQ*.BݹQG`jyabT~HFUٛyq'eR}{1~`ιy|,/ްj5dR;zIV,s!lScjR -O r F'oP?qY/Q³Q^Ո"Ɛ-,B|̼$Z?'ha__0;-uya Aـ|?1~|n?Ĩp^ -c_.P]|8L7~D1 -;o=+̈́y?܋U80̽?¹c1FNʃ`>9L8Ni3Ůd28g5@<ސWlczޕ^ Ўz6̌RەcCS')sH)OO25/(FPt؁~_@bysp]dUp^e I,kW 0B$SDLN'߇>g;2|L?xOT@$e)Y7>}@`< ?iM|1t -Pń?>lJ2/(H*n9_T"O(o%{a^>)ʉ"(M(X툈%WS @ n0ſAsc@]y!Xg؁?ΓW _eO2~sur8S_ij~['O H~ؓ[?bk艾D$_xrݴ6 A~?xŏӆkhCC;k~$@_W=P{q"g c(2|ڸ{1Ks[< a A3i>DeMv ,~A0T_e/C|<=Ń{5uI,śVx.T,y)z|L׌?ejWqV8>8t=B ̲p{=?0SG`ܠ+/T &aNJ Ow'v)qA(~|gS.g?0) c_nɓ~\JԮLnT sA|u]J<|LԮ2;_ ^^J0weEKdc,07q)5(<~ɥ+,8G_۟>gOXԩq_%V%Jytg1S'߆:f~ _?Cy =ܽGl[yg8[Dy&Y0_igJog? /N<2dCX=9T k! ٓ?Nk?e>/abxo -38?tď??wRxL2FpHa_/zz|!t?G0%pu)eR!+ SXanxf]P|^̭MF:i#!f 0Nަal.[ě]݌ǿ󢻥eL2V>@Pfh3[a=?cN8ۛfUWLYX`KS/>)m$C%cZXp_>5?iWE9?xdig'1{3oROO<Ʉ9%eIXoWp/*F`54?Ms[% 9QC(X3|u$t>*MZm/Gꌿˌ9K/&)?MGYD2>f\,~rOŒ??ٳT?<ׅx|?"rwk=!~|w?aV-i{Ч\7B5BL,9S(,F?00Ge|!\ +fi o£@Av #_uf|ZA~ 1<~P*?Â,W?x? 6~T'e|a_Y?~+d7\=0O~QNy75_6*%`D zmO>_Uud0|!| e3? Ã(ȫc<#N.y+/;8{ s/0 Kq*|u( IjO%TL>94'R#"t|{zY:PQ;f_[CFq y/:`${J{wp!l_e35I<2F3Y?3Ca -KnpM1z=/x{ Cx;L'c)LJOWk1I^Jm? Os#[Fak?IsŦ~~=Me,# -gsɂx Ȓv?টrcs)ߜJ_$qgUnP4d"g'hL9gǏW>C2_$VsSX 0d yG+>x~q<GG>YGʄ`7|΄a |w/guc?@ߒS @C3sc2?eovTR(S5lUc'0v39xG-p0Vgs|uc`A|WjWp,6}ٔs.agwǁYȭlQs&ǼT s >rj~BOO'Ӄ>c2LoM4vGf'3od')X;3lXHnucF?p~~?R ef -_Qe8$nsp7WHXnZ+Ys9_̾hk$ ^ɛKHCpp_ -ק߀~!{g?ś_?Ǜzk1_?-Oӹ&aRWuxe$@YD-k1񑩔,͔tr!??ی&kZPVG'^08 ?v_D?! B>s'6Si ae61H?bM#a_i\\oq,?zh}~n B)W1cM)>;@K:\CU&?&06!p{@8 sEPX6'l'7q1^1 -FBuҢz-7 -&+"1 o^ߙ-P5 -1Jn/ZܭE(BVMdTϋ_e6C˨(T0Aɯc8PA!P4$-GTSЯ~G~YaX3ǪcR8L+Vf6%j#lPKʆ!nPR- I - #fb0f)  -s櫝qj/v𼳪0!:RV`K>6f IQ6bUjʁaIEbV#x ʇb'R#[ĕ(%f}@ \'dP 8>KƢYTPqX`KeD_\ϓ&u~H8cA<):Ǭ{P,yHpp{_Bq9U=Dgi|$ԹW YX8gp6ǃs\y -EriE h`%ŗyD._nƥޠz25A2zrg, `.bU3ԯs~u0 F} \~4P;\\}Bu/e.ǞOqN?0ׂ5A; ,6ڑ ӕ [j9'U2լ$?7!9MeĊ_lTΈDžfeRTY&޲L:mͳefGK 3Ԩf>e/ sn;'R>Q~'rǞR9Y lDm𹂡l {ҾNӏT葨nTႀ4Wo0ēĢd0؋ne(c$E[\b!қ4@ZFArPIe=.7VNGV*R/5>agCG= 2ϒuǘ-/<_?yʜߘL_N4xD5r^՝\9gpMLȅGn7H+~Ew>LB|.z"0rj=5i Jdȹhb} ~ -оR,p&eK/K5s$m(F?^&N/xyA15\t4N{9(6F2;{AϽ)gXc24Ama@aj=T"R@ju{#Gl^1M_ RB_9KXhji/}ո4&ɹW̩1+q5dϽIz}Y%1a55\Y&8gR -Ll{#ku @b>vMofK%B,Nxsk4#2u~!f2M&Ð̮54ax/7*ӌǎ53=(P34)WbKA EJek( 833ˆ&u4& !Aw|_[F`I3 -é < c4BWw6J FtLNxĢW%r°(rcow13O N#?hKeW72&gY&(s MDa)j1!~wW2Wy%o/;x㯥aNg1nÇ13$;epkqg0g^ -x>#׸A.bx:s/~YL؎x\^efT7/|x?GuƸ;DYsCJM(^c~8xۨ}+gf*PxB**qH30&~GIk2;**x7K.\qxsFweCRכf<2\RJk3L-4RJkSUKx\ȑ UµpC1DٜG -ʸVt'm4C$SWs-^vze"8o"8 y`GLJ/أS 罡-;7ܛ"f"H*7osUx塇c\Pcc!SpQXhxӬʁ5kM 37X!S¥]_b#p*VN7jaK-®k\??@Е8)kd2ĺ~/@C5s⚫`Րp6|"p k̅ |g UpkHdh =`ʯ}])yϢަ'EΫ*04QDF-IK߈&6rJ.Rb#f^?."W >t*D:Ij|0 d{!̝brD)8}P[Xs+1oPS+> `( emm5nQ..3/o\<^$Uyg‘PѪ0I@1#Ij} }%Dh>?~,5LwS:6/3:! e2ΨMOICvdW8`zvslrN=~,LRc'Y 7Rs|ϋʸs␠gH`9|PJG7&)`#3cE&BY/~3xx|Gb9L -yC}#ho -MߔD#9Po@3,_*kg/VC@/. G#SJm*Zu?fA&O8*(vk -[*Әx;A&Ȱa)xZmO~Ś)@*>djۏ&"W#%Whi+= n)R ) I> q~|穨MbrAxYpsYD33~/Bb:8'ldU$Mx_*20QȨ`qڸ> J #AUd%XƃD X -oDo{5;0h;$]GJw!f! \$"QHo5Ԫ0إҠ+)窛|[J<Qe !:`sԢ;i6cm^\{U(y*xTqE-T ke4̺#1VzEoCd!*œ[CْVJ&EF@-j`Ea8֌b5,|B*PbbW5'|PiBD#`6IY0 `qOAk71A53:Ñݕf\Mr!*\U<@' -rPRTeX!"}qMVE! F,eTguKy t1*R\gQLVfeN7 ~|X[6Y4A|htcњ: ]D&5/M3xXqGg:r_Gt)D-DS6$c99{ORξuI7\-Ը`C4kWTͩ&S6!z@QIcSp,;1}y+ddI`݃9͚-#߈I>PGmXz7cCm;BWh?LKz]Fx n&ew^)<N?lVS C3+J+k+},N 󘤋t7ݣl@ ̪_r~*gl*Rv1 Ln/ tBԌ܉D$gpbޠ?&Xw`W#.?AcS!QjHE&@1] &8qW*8ʢf G1 Wo.NoCHc?UssVxܼiƈla2?i}1IQSE}T_f-y2pxb79LNY}Ef%QIZPE0%K!ɻ@^z .@@ Ka̰Z?4}SsO >g>DXO`?u`ҟWȩg$RƷ1%T%O:?1O0Y/ z!?* S -j>m®ل0} -FtZ]ZEu،UUgq1iYZ?S?<ק52P ^|H%&`svajmebO7O*/ -mbbP%Z\ YaD02$gip;`IpY䩋/Ǣxw Myļ+ F6*,c6,\m4XjAXbYSZ Me|Fq~tc\B,]EU;!,17>x~aQLZXVNB=,-X(#fu(̸O|\&710U)U ,>} 7;JFՕ#ڿ Y'm(g6%~fL(eq~Gs]hK+ljY*5YOAjU0gHM$|ZTTRӑe܋尮N\G/ -3nC0_k;,E6âތ4@؊|E%W YZ/N [ Uv=#.x)b ko@s>W*|,)mNm,2`X2rMZ93)auQ`#`pLIoafe BS2޴y`ϬA4 I|!`fu ];pL2 -a@ሙCV)? !9WWl:5cLsx`>Rq4Lje~2\g3qXagܞ&f<& ]rʻ3 >p4C#c񂔺Ǐ\Q tN|on^/+\*Y]e*D8Zvwnh:g11).[eYlc\C`) a8iO`tq[.ILy 0,lD dG:m"-bPdQN+<@ ĬE։iUc?V%bmQ{#J%F`Bn17pPbO-=an`yR._ ʿ#z=EӃP*2,xL|0erg:vٗf^ّ^q#-n\pEL0^/W5?xnTbw11 sz~OD@jPi;wi@98+$fMX4ya<U݉ 0[7Ԍ0/cM%z%L.P)loݵU-[LL&MRE@3ʨa`ߨ%&3 _iwD 1y 6tTC4,2wzcƹϞYFx>LAs>%p֡.e*RRl8$F2Cv[p  -4/R:Qb5> b-dԄܪˆىnP}ۺ: -'DZ@ZP5(bZ3Ș`"sȝeV0 Ѿ52p,!0db}ptCfYg cdk;DxcIJ)7 ]!-@f#,H#14 i2ϗ,~K2if+sвFVkTW[*C@6?0E$=<_̹s]J\.>Nsb8p}n08^xJd)h1oB$By^ǁV`2U-E D0nlXw^>SsEǭH3-Pb - 퇜+_&| I*IF.VDً DT W:M"^C;/JWgKo*3(;V^?0ӘT_hjPIh04]ib)ag<Ca2#HX00lEnO& C\W10~mfc&1䱵 e{ Ntn?6h1n~5 G>L"L xS0](yC)  G@  oj~E\w?rb+:\BI -_5 2 - k8 oQ7 ;.|;KgljL/w -sw1 S&'KÌ;>B=]) _MP~1j-OD!J|c -vGj -J$7XԮpFQ*Sٯ8ZI\-:8>\1 HqD6fRsgW3Үb:F3}>ؾ }!Z]>s5 ,5nRH0S6H;(Pń}87! -S32+1+{XsH| *:*5?vJk uJ7{`{b SV=Od&eŏcSkTQ q$<#_iDYlEVU*dK>@cL(^TU{^xTzWfu 'BElXA`>X[Z2͙ù.T!_hڐK %{G3;ܓ+Htz =m[&Rΐp,4KJ>?Ŝ8~ xQp構SMG8sܔ*t&8*VĦbSX-18@ZD"I)U1*%%bqE5`+*WC\R \&S|2U#7 %]-j\^j: _F%_7nSg$x3fqjW|+y|fb"$oQtXEZ"_1VM$ 32G@5+67FJHe { yj/<>5wGre?pY+L C SdWRBS*WW=' dQ*x%guO7{#+n+=kNΦfAV+FkQ4o W7hx]L-˭GY1sJ$솷fخ9SU -w}fDIELidt3J]گPժz]RP؝_i@[Rp#ؿ-eYI41L.Kѕ:f!&U1' 74S`2wM1GeqC?sĨđ{3L78fq* -R5 P6 -}L(&a3f|! hk0+b;,2bf/,uemjrT>biZ`s:dl<ݢoʾ@PJ8E@3?Ɩ5A5Xctn>|u|~\Lgֹ."e t~a)ӛH1|Lǘs̸}Z;W#1|xGs[$fY0p|C۹%Ê #S<*ʠkžD hIAjC` K/@R;pcxxb^@ -9:/ $]oߍ:<S /Ό?AkW;> n|q+2L8cUaA'dNG'1.|^4~z #q#ZeW͙%X1D*SxbLeb(((>a 0ܽ @`+..53AAX 7Sra)[k+,rr٦uؘqg9MzCXI'\,> -AF_V _D"c8qF-Z?NvX&;=L,~DR]keߐK Үj;Pe1Ł zCR@9. -eӣv P xi`ͪavb( M#.'VԸ_@RBHmoB6.o)~JgF[֔a#.lO*2`,ia@A+ڂl k|h;Fe4\3;!\\*<Vc1P72ZBP.Y篭3S!H!En@-/,"Y3U6R^NOPnkSNj~B(XG J/Ľg-, -4y0]zd)yok4\@O용ugC|lÁOG"tGr-fBΥ_QTn<0gDKxtCŜ:hpK0>x/P|"H,:E1& `'X7"j6E[H&#bwf. Lj6p|1\Y-,<ǙՄQTˋʀ^hi3]Tnb]v~$e0~{c$4v-Oأoh[Ќ3O3RLW끀""qo@^=\d-Ρ˩l4$ك^Xa\L ?iVc2jD| 'sƯ܆;6ねb> -~]/z74-2m/nOTy"s. -`;M\[Rzj1pUd QH &squ]iϚ_Nb E'o!\uqDbްj:'dS/>n|L^5x2COpM1FUNw$aJ8,7fSv@ eZ@S)B}цg+8%s43|1+:涰\Jw{9S3ZPE |%P -OIGI,E`mBh%!.M VVxaKe4n\!%7ٲx;scMN\c(8f8gCCġƚ\IJQ䘰(1GƯIXGq2y*G(kmIHPJ3 [xRq.W5;~M'U)'\1A{zqgG9>p3DB7"U *ق E.Q5Ln؊3k^W]w J"R3^ĄBT]UWn&"c0MZ\f^98L(9Yz(ܐ珙F'sԹMk9*s-ľ!  xXXp3 ?-& gLC@Ĩio."1sH&a=j|@bcX# e3P`U@LP\c\<2q=jE_5!+V(-"߃Yk/g;ggGa%KE͖1ou {0s+; :ʕ#QcS_u/0p  :#4#Osy T8:oQ!evH.;PX/ qjK>"ń8`R,Tԃ(x`OԧO-k|ns5*O=[#x|9u++ -A̹DXЩ@`˛ua9u_0Dcst~f'qB:Ƹwdj -0ʔAt5W‚ФǬOhBBQXX|\ 63.>XU|C69/E3:s3թJ%J/ʔ5T$D!nԔ22Y!pH-l<)TIyin-jOETBA} + LKg&:<Řf7_U:ISd!* qf9m3ufg<kƲ,jVgQxI}^4ǜG0=IXų>XZSquU MB\^bb|:j8!,sÌuθf*b\&J @%J^/S>GC)w\b[*&/eU׈ϘLY;!7 uN:= 51c&7?:8}K&"@5/& .Y^f! f!s3S| -mw*fĿpJnɧ\ Vȁa^Vj#45_0eZM!X1DSJd 'q{Gpa4ޮY~) L`...,#wÜsΎmޢ"qa=s - Nx] لk|8Z9.55l3MRP3  %f[ `ͧQryp|scc,ĿgQC<!oq/{'r8*}wU33|&^YqܪevT~8PH3F@F}B17h *5s,+sYț['Vk?PmCA*C9^ ꫧT60ֶg枸K1bpRx <\ -c5@tJ`dqG $ q -_(HyYxL0Jq|W&*{U܀-b[D|P^@r^)%fUQ'۾RˉP\Z!,8h7K >_Pp2g !CHT $O2YVws`N:3S5|K uvX;b6 h6oJih_zДoY`bB0x2lyD:aOV|i3|Lslx^+?V,|<|qῧ2^@[V!dCC!pq^-oɁ2 }_q4򨃚ffwÄcpR ԽL-Ral v%ddcԐKɡsBQsD'h\H`#p7Ӹוr =k(|e[ 6{_$ڣDqSvhfBuқB?TxTLO0_ҏxl,\A$j@˄8& ;֧IJT0Dj(c%W`0Ӆp|Xۣ5D5JTq3o4@=_V\ib\NѦ"_P%6# {cܲ"X`>!X.Q%Cg?*g_gHa5"E03YP4w;O^ %p/ū-(R;:WbaM0isaf9/뾧DIl!+*lY+|ffe\uy}g6* E_ i ٹ o|YR:Pa'f6X $5/OlL*wҗinC3ki`,j{Qr65 }Rp*,u/{-HP\L--XӀr]4X]t@` -=޸QH֧kEE0/7/ -WF/wCV>pmC^!2\jˏro9_.jxN6CQs qbc-yqe@<(VVH -qV'Fa1K,0 >l&&GqQP.`1dF7cg&05ԆP;=;Lՙg{;NFJa|< x۟fG2Uv;+aI˯u#ܮr2>:CrSq0`( CB-vC=)%6.Š;F`K)#U5T*yݩ)pLKa^~9KeKOfyqpfzܶ1,>etHRZG7lQSqXK8t3zP܆)SF[%<'+f%+,G(al 6s+aE+qm/lsx;v- Q`0?7痐P܏] /?`.Bʒ #eUM -IeP:Ƥz8Kh tCD4Zz-Gq0үJ}Q I2£dsaCӑpKx%R\];ŵd%Fu::1 ĝk|>x2C Oԯ7.yΈK~KK.5SLq]jXb)3:|r˖*_pE?hLHB/^zlNα92yWG_ Smb-Bʬ?ebsoN>SFP -Ixm.WI;zRJ*yl% YyۧWUJc&z m>Lb%ƿpTiwz BX# _PZ@^b3$<@Wbk]#c,:LXr JH|"!hk0{YܙϱkIjR1_*3cey\ˊ>3 ;1Yg/ T!:nreT>i!02mט3_Ī#{pAFߜWլ鍶ꈟ _%Z9w1 5BZ9Km0 ))xT YuRSyZCa^1C4:r•:6{lt5BS$liiAw`Q˨4 -\ר{V%Zյu:lxM0#*o<6f]?qz0br5dlb{Ds%$?bFJSa7=Q !@mQ,{b8HNdC<^8]<]0j/Tq\30n]B*&g˲xAqQ倬D:ZaN.pA!F> V D.X'C l8?faZhtT r#p_=;.jVX1iOYq 0'ixAʖƱv#|njPHﺂ]Y…iE"]ʡ^#49#`[ pV-~&qD/rBU?D%TGww XF47Qd7mqǙq/ njc3^nn;Bf0SovKj\j^'ԽA%>bRֺ5Mu&h~=QdĽbdk9ٴO'bܵӺp[g!wcփYj_+d7HGEK+_X< -WJ/Bj`%7X"x4-Te(BtW3s?ɌB$ OA,F3q?1p"DH4Tp0h!Af" ~X6\␯:\PŇ1֪/mYl#pq:G =T'S.C[˓#8-_uq3gH/ 7Q%iaWa*07L[9 H LtdA`E5~墘[P|F7>$d<|c_c1##̭m CJܔ* OYBU jFQʕQH3REܙ$$ DPOوLgoN~ԙ-g6!qto9k췪:_,R}F|%9;H\\O-qYCs3Ts .dY"a&?<ʗfŖָqygo\r8~GL˄P/% ݎ@/i;}F -Z1:JGXꄅ1' hXg@[%OXb#Bŭ 82,EtƏdE=)s;їQ?-p2C93:ή_ sdxp85q\B]`̭H=ff$rGq+)C -8My" =i3뗄F9VgTzm918Ơ4X-baֿ* Pyy%pU 6EZ̸.,%U`>PN?SasR6obJsťxU=EG7}X2]UE|hȰCS%JBun[iHzwK0V+Pb_ LlRABႥ`*6Va+i @#!G}* La -xLuHHf4F4#܅ `޴0Qpr{#/){.r.wzK.3n`ss,)뎥|y%<*x/ *{\w$>q4DBJ'ju^'lV_ ˞3rx=f/H?Fat˗-M3s\,\u;\WbL.d}zpu8|D,ZU˗2Y;<_KK,o\+a?x r.[rj r\ ż\u>.˗._/"^pLQPDf&xew,⡢^xu1^U.MLBYk~`-ypIrrR=2Pr1q[12ye lQ1.Y.|_,x2.,pl]Ka x/3Xnfٟ3*,%rӀ- PKn_6SO/rY{2fX\Zsr LK uSV&z͒lymcX߉LKbX4C9˃55[]2g C3ψ0[BЖF,] SH8]7[[yRdKZ^|KǸ2KܸT]wj6f[.^Ip6˧R.j_J!s'C\).]bvBSzcp0eM%y >>s5j_ShXkeTualK% IR eXKrn Iy˖̹r_>\g_ucwͻK/]1P'VEv>9m V U>w|gĶSVL0SqmO\|sOD9ZS7y%x2Ve7+2,1=ɃRB ZKKR!c&#" AKnvfE/9SrtXw*jfgDjٛnUAz-쎕/b/>e[ mM!aJ|J4F(()z%kS/ؖxگ2qg, {X(tDEZaBRqXV'{LHAuPQ.kzql#%dƌlA}PpUCL21C%blLcDsp땵Y%X]5z Dɘ_9؂PӥreJhט,犬/ŠK,cq!$c'Va"+&Siwrʶ(yRc*CZeHv&1u1΍jG((i4b,kpe 5w _]krwOS4 u"\,wKE 7/[φ;]\1h-: -U~WP&R0D -n)2}LRpaˋ*LcRah^sUIqꍽG;V`6"IUWY/%6VmaFH:H^)U3,k>~W| -}5Lb +q+#1_ (#dq& A0P1@!`pBQ?~}e4?=lĻ7롉?l1#652Ym~J7,~!nԭ] c~GCc౗n?lVTQEW?Q`B/"p$ПȼZPFVTQSF- -p7Ѕ?ش!ӾUN,2C?H*+ࢾJXPv[#Fv,gYcwܨN X9PؘŃ7bu&6'!bjBt6'QqeKR(/:}E5~Ee,PŊ.._^o UX5ĢhJ(yqTPEbмV -X̿%C(^WEeb&_b5 ^Fբ5EBe1((+䢊(hʰ8J*W"ЧF,U c,4jh?c/"'7E׃G [-߂PT7eceyQE4%bEPІUT$QCE"BC6~MQ/ɣ؟iM h^K+e#S"7&eŗ9V,,6YeYeY~,J^%ʸ?iu5iEn7>_z]]CvwxO]]k]vY.ǚW,_e(_q}Kx.c_Uy7_ oĸ+s?/iSyuՕ'_[w߸[/pM$?8]2ѫ+MsCn+<:P /x>7*u.fk5 y5 zF&>=Cнכp3Bj2fhPй4(\8~eecg}"W,5g}Wk>_⩲ o诧N*ƺEYQ}Mwx!v_\?{BzBX- y>ˡP^ C sq~+YWP:X xlq\|:ɋ}`(+ -o, 칲,e$ @P0A`p1!Q?݊Avz8S)C#:A#jUPAAWEA8]<;8T V(܋X3DbQŖoF8 Ub.Y㓾NYz7` aߪ,|#Ď?~>qC:3t6/0C;CyQE QP(Eۨ!D"D"((((xߏΏ7 -շ;ay~gμNh(<A|.1cxBQwGЋ-e=wxcUp >/F(pageEntities.json)/Type/Filespec/UF(pageEntities.json)>> -endobj -7 0 obj -<>/Subtype/application#2Fjson/Type/EmbeddedFile>>stream -{ "type": "Document", "isBackSide": false } -endstream -endobj -8 0 obj -<> -endobj -xref -0 9 -0000000001 65535 f -0000000021 00000 n -0000000070 00000 n -0000000124 00000 n -0000000357 00000 n -0000017801 00000 n -0000356550 00000 n -0000356681 00000 n -0000356879 00000 n -trailer -<> -startxref -357016 -%%EOF diff --git a/data-entry-app/docs/igiena 14 decembrie five-holding.pdf b/data-entry-app/docs/igiena 14 decembrie five-holding.pdf deleted file mode 100644 index 66c5449..0000000 --- a/data-entry-app/docs/igiena 14 decembrie five-holding.pdf +++ /dev/null @@ -1,2312 +0,0 @@ -%PDF-1.3 -%âãÏÓ -1 0 obj -<> -endobj -2 0 obj -<> -endobj -3 0 obj -<>/Font<>>>>>/Rotate 0/AF[6 0 R]/Type/Page>> -endobj -4 0 obj<>stream -q 264.0000 0.0000 0.0000 792.0000 0.0000 0.0000 cm /Im0 Do Q q 0.2062 0.0000 0.0000 0.2062 0.0000 0.0000 cm BT -3 Tr -/Ft0 1 Tf --0.035 Tc -32.688 0 0 45 1.13 3176 Tm -(NDS: ) Tj -40.701 0 0 45 78.57 3176 Tm -(1378448 ) Tj -31.8171 0 0 50 0.67 3128 Tm -(OPERATOR: ) Tj -30.037 0 0 50 173.85 3128 Tm -(DORINA ) Tj -27.8093 0 0 50 301.86 3128 Tm -(ROMULUS ) Tj -0.05 Tc -28.5108 0 0 27.5 -0.89 3064 Tm -(NR ) Tj -31.5197 0 0 27.5 57.85 3064 Tm -(POS:C3POS-CT2 ) Tj --0.035 Tc -31.7547 0 0 65 263.85 3379 Tm -(BINE ) Tj -29.5748 0 0 65 354.94 3379 Tm -(ATI ) Tj -30.5222 0 0 65 427.92 3379 Tm -(VENIT ) Tj -32.8262 0 0 65 545.76 3379 Tm -(IN ) Tj -26.2087 0 0 65 600.87 3379 Tm -(MAGAZINUL ) Tj -31.8185 0 0 65 772.85 3379 Tm -(BRICK ) Tj -38.7006 0 0 66.2501 458.59 3311 Tm -(CIF: ) Tj -34.7092 0 0 66.2501 538.83 3311 Tm -(RO10562600 ) Tj -33.9852 0 0 57.5 0.64 2939 Tm -(CLIENT ) Tj -36.9025 0 0 57.5 135.6 2939 Tm -(C.U. ) Tj -55.3299 0 0 57.5 214.06 2939 Tm -(1./ ) Tj -43.0693 0 0 57.5 297.54 2939 Tm -(C.1.F. ) Tj -39.9795 0 0 57.5 393.97 2939 Tm -(:R01879855 ) Tj -36.1842 0 0 106.2501 6.68 1674 Tm -(TUTA ) Tj -37.3959 0 0 106.2501 119.78 1674 Tm -(LEI ) Tj -32.3647 0 0 62.5 423.85 3572 Tm -(FIVE-HOLDING ) Tj -37.8406 0 0 62.5 660.3 3572 Tm -(S.A. ) Tj -31.4745 0 0 63.75 316.81 3506 Tm -(JUD. ) Tj -29.6711 0 0 63.75 405.68 3506 Tm -(CONSTANTA, ) Tj -25.1272 0 0 63.75 608.88 3506 Tm -(MUN. ) Tj -28.5198 0 0 63.75 697.7 3506 Tm -(CONSTANTA ) Tj -32.4333 0 0 60 408.4 3443 Tm -(STR. ) Tj -28.3639 0 0 60 504.79 3443 Tm -(ION ) Tj -26.9722 0 0 60 572.87 3443 Tm -(ROATA ) Tj -28.698 0 0 60 681.12 3443 Tm -(NR. ) Tj -0 Tc -60 0 0 60 753.28 3443 Tm -(3 ) Tj --0.035 Tc -30.3737 0 0 60 2.85 2814 Tm -(BURETE ) Tj -31.3595 0 0 60 135.42 2814 Tm -(SUPRAFETE ) Tj -31.8083 0 0 60 321.84 2814 Tm -(DELICATE ) Tj -31.0879 0 0 60 488.93 2814 Tm -(ARGINTI ) Tj -38.3719 0 0 50 1.94 2754 Tm -(UZ1024010809 ) Tj -39.7992 0 0 50 247.49 2754 Tm -(6422768014664 ) Tj -27.709 0 0 63.75 2.87 2691 Tm -(BANDA ) Tj -28.4138 0 0 63.75 116.86 2691 Tm -(MASCARE ) Tj -29.4468 0 0 63.75 267.82 2691 Tm -(25MNX45M ) Tj -39.0569 0 0 63.75 433.44 2691 Tm -(520703 ) Tj -26.3134 0 0 63.75 561.94 2691 Tm -(ANK ) Tj -36.7077 0 0 48.75 3.78 2632 Tm -(LVO501020402 ) Tj -39.5316 0 0 48.75 248.44 2632 Tm -(5948941004730 ) Tj -29.3877 0 0 61.25 2.86 2568 Tm -(PRONO ) Tj -32.2815 0 0 61.25 117.4 2568 Tm -(SACI ) Tj -32.363 0 0 61.25 225.4 2568 Tm -(SUPER ) Tj -34.2524 0 0 61.25 336.83 2568 Tm -(REZISTENTI ) Tj -45.236 0 0 61.25 537.46 2568 Tm -(35/15 ) Tj -38.5402 0 0 52.5 1.94 2507 Tm -(UZ2210109185 ) Tj -32.6734 0 0 52.5 250.84 2507 Tm -(BKOOO194086 ) Tj -27.9441 0 0 66.2501 3.87 2442 Tm -(PROMO ) Tj -36.4921 0 0 66.2501 124.32 2442 Tm -(SACI ) Tj -31.0004 0 0 66.2501 214.42 2442 Tm -(SUPER ) Tj -35.5626 0 0 66.2501 332.82 2442 Tm -(REZISTENTI ) Tj -42.8552 0 0 66.2501 527.48 2442 Tm -(35/15 ) Tj -38.5402 0 0 52.5 2.94 2383 Tm -(UZ2210109185 ) Tj -32.6734 0 0 52.5 251.84 2383 Tm -(BKOOO194086 ) Tj -30.9161 0 0 61.25 4.85 2316 Tm -(BURETE ) Tj -29.117 0 0 61.25 139.69 2316 Tm -(CANELAT ) Tj -27.9122 0 0 61.25 290.7 2316 Tm -(COLOR ) Tj -34.0652 0 0 61.25 399.81 2316 Tm -(10BUC/SET ) Tj -37.7115 0 0 61.25 582.55 2316 Tm -(33 ) Tj -38.4177 0 0 57.5 6.94 2254 Tm -(UZ102401100109 ) Tj -38.9735 0 0 57.5 290.5 2254 Tm -(6422768022638 ) Tj -31.2182 0 0 62.5 9.85 2188 Tm -(PROSOP ) Tj -34.6149 0 0 62.5 143.84 2188 Tm -(4STR. ) Tj -31.203 0 0 62.5 256.85 2188 Tm -(PROFESIONAL ) Tj -28.578 0 0 62.5 476.47 2188 Tm -(STAR ) Tj -38.4177 0 0 58.75 12.94 2124 Tm -(UZ434101137344 ) Tj -37.4359 0 0 58.75 295.52 2124 Tm -(64256670O0530 ) Tj -33.076 0 0 57.5 14.84 2062 Tm -(HARTIE ) Tj -33.787 0 0 57.5 151.82 2062 Tm -(1GIENICA ) Tj -32.1644 0 0 57.5 315.84 2062 Tm -(BRICK ) Tj -0 Tc -57.5 0 0 57.5 425.31 2062 Tm -(3 ) Tj --0.035 Tc -29.7474 0 0 57.5 461.45 2062 Tm -(STRATURI ) Tj -37.6085 0 0 58.75 18.94 2000 Tm -(UZ9005335 ) Tj -38.6432 0 0 58.75 208.51 2000 Tm -(6425667001247 ) Tj -32.5007 0 0 58.75 23.84 1941 Tm -(HARTIE ) Tj -34.2 0 0 58.75 159.75 1941 Tm -(IGIENICA ) Tj -31.4727 0 0 58.75 320.85 1941 Tm -(BRICK ) Tj -0 Tc -58.75 0 0 58.75 429.29 1941 Tm -(3 ) Tj --0.035 Tc -29.3131 0 0 58.75 464.46 1941 Tm -(STRATURI ) Tj -36.7288 0 0 42.5 21.95 1894 Tm -(UZ9005335 ) Tj -37.9827 0 0 42.5 209.52 1894 Tm -(6425667001247 ) Tj -30.5764 0 0 46.25 13.43 1841 Tm -(SUBTOTAL ) Tj -29.4406 0 0 57.5 4.69 1536 Tm -(CARD ) Tj -33.409 0 0 51.25 3.83 1479 Tm -(REST ) Tj -30.1519 0 0 53.75 5.73 1365 Tm -(TOTAL ) Tj -28.4619 0 0 53.75 122.75 1365 Tm -(TUA ) Tj -0.0299 Tc -53.75 0 0 53.75 198.88 1365 Tm -(A-) Tj --0.035 Tc -32.1083 0 0 53.75 275.8 1365 Tm -(21% ) Tj -30.4727 0 0 51.25 5.73 1304 Tm -(TOTAL ) Tj -28.9701 0 0 51.25 121.74 1304 Tm -(TUA ) Tj -28.2402 0 0 51.25 198.86 1304 Tm -(BON ) Tj -31.249 0 0 50 6.13 1248 Tm -(NR. ) Tj -33.2069 0 0 50 83.84 1248 Tm -(POZ. ) Tj -31.8175 0 0 50 179.93 1248 Tm -(ART. ) Tj -31.8315 0 0 50 280.77 1248 Tm -(IN ) Tj -29.6408 0 0 50 331.86 1248 Tm -(BON: ) Tj -41.115 0 0 51.25 10.85 633 Tm -(Z:0217 ) Tj -40.7052 0 0 51.25 141.8 633 Tm -(BF:0126 ) Tj -43.7682 0 0 63.75 17.68 559 Tm -(ID ) Tj -34.816 0 0 63.75 67.83 559 Tm -(BF: ) Tj --0.0088 Tc -37.5 0 0 37.5 9.3 449 Tm -(S/N:DB4700011007 ) Tj --0.035 Tc -33.965 0 0 52.5 9.64 382 Tm -(CASIER ) Tj -44.7362 0 0 52.5 143.43 382 Tm -(1: ) Tj -34.631 0 0 42.5 442.63 827 Tm -(C3POS-CT2N1378448 ) Tj -0 Tc -65 0 0 65 835.72 2745 Tm -(1 ) Tj --0.035 Tc -25.479 0 0 65 854.88 2745 Tm -(BUC ) Tj -0 Tc -65 0 0 65 926.86 2745 Tm -(X ) Tj --0.035 Tc -45.643 0 0 65 961.45 2745 Tm -(3.99-) Tj -42.286 0 0 65 1074.49 2745 Tm -(3.99 ) Tj -0 Tc -65 0 0 65 1164.85 2745 Tm -(A ) Tj -63.75 0 0 63.75 839.76 2625 Tm -(1 ) Tj --0.035 Tc -24.9694 0 0 63.75 856.88 2625 Tm -(BUC ) Tj -0 Tc -63.75 0 0 63.75 926.86 2625 Tm -(X ) Tj --0.035 Tc -44.5517 0 0 63.75 962.2 2625 Tm -(8.14-) Tj -42.2387 0 0 63.75 1074.24 2625 Tm -(8.14 ) Tj -0 Tc -63.75 0 0 63.75 1165.86 2625 Tm -(A ) Tj --0.035 Tc -25.2676 0 0 57.5 516.88 326 Tm -(BON ) Tj -28.5644 0 0 57.5 588.87 326 Tm -(FISCAL. ) Tj -0 Tc -61.25 0 0 61.25 479.61 265 Tm -(2 ) Tj --0.035 Tc -37.7675 0 0 61.25 510.56 265 Tm -(9000221498 ) Tj -0 Tc -61.25 0 0 61.25 831.85 2506 Tm -(1 ) Tj --0.035 Tc -30.5124 0 0 61.25 857.43 2506 Tm -(SET ) Tj -0 Tc -61.25 0 0 61.25 928.87 2506 Tm -(X ) Tj --0.035 Tc -40.2265 0 0 61.25 964.27 2506 Tm -(8.37= ) Tj -41.6176 0 0 61.25 1075.25 2506 Tm -(8,37 ) Tj -0 Tc -61.25 0 0 61.25 1167.86 2506 Tm -(A ) Tj -62.5 0 0 62.5 841.81 2381 Tm -(1 ) Tj --0.035 Tc -33.5038 0 0 62.5 867.38 2381 Tm -(SET ) Tj -0 Tc -62.5 0 0 62.5 930.87 2381 Tm -(X ) Tj --0.035 Tc -40.2265 0 0 62.5 966.27 2381 Tm -(8.37= ) Tj -41.6176 0 0 62.5 1078.25 2381 Tm -(8.37 ) Tj -0 Tc -62.5 0 0 62.5 1169.86 2381 Tm -(A ) Tj -65 0 0 65 806.72 2252 Tm -(1 ) Tj --0.035 Tc -25.9886 0 0 65 821.88 2252 Tm -(BUC ) Tj -0 Tc -65 0 0 65 895.86 2252 Tm -(X ) Tj --0.035 Tc -44.5092 0 0 65 929.44 2252 Tm -(10.49-) Tj -42.6764 0 0 65 1058.5 2252 Tm -(10.49 ) Tj -0 Tc -65 0 0 65 1169.85 2252 Tm -(A ) Tj -63.75 0 0 63.75 796.76 2129 Tm -(1 ) Tj --0.035 Tc -25.9886 0 0 63.75 820.88 2129 Tm -(BUC ) Tj -0 Tc -63.75 0 0 63.75 893.86 2129 Tm -(X ) Tj --0.035 Tc -44.0812 0 0 63.75 927.46 2129 Tm -(19.55-) Tj -42.6764 0 0 63.75 1055.5 2129 Tm -(19,55 ) Tj -0 Tc -63.75 0 0 63.75 1164.86 2129 Tm -(A ) Tj -61.25 0 0 61.25 802.85 2007 Tm -(1 ) Tj --0.035 Tc -25.9886 0 0 61.25 818.88 2007 Tm -(BUC ) Tj -0 Tc -61.25 0 0 61.25 890.87 2007 Tm -(X ) Tj --0.035 Tc -43.6533 0 0 61.25 924.47 2007 Tm -(13.54-) Tj -42.6764 0 0 61.25 1050.5 2007 Tm -(13.54 ) Tj -0 Tc -61.25 0 0 61.25 1156.86 2007 Tm -(A ) Tj -51.25 0 0 51.25 794.2 1896 Tm -(1 ) Tj --0.035 Tc -27.0078 0 0 51.25 817.87 1896 Tm -(BUC ) Tj -0 Tc -51.25 0 0 51.25 890.89 1896 Tm -(X ) Tj --0.035 Tc -39.7414 0 0 51.25 924.61 1896 Tm -(13.54= ) Tj -43.6575 0 0 51.25 1050.47 1896 Tm -(13.54 ) Tj -0 Tc -51.25 0 0 51.25 1158.88 1896 Tm -(A ) Tj --0.035 Tc -29.0011 0 0 55 331.86 521 Tm -(DATA: ) Tj -41.8264 0 0 55 440.53 521 Tm -(14-12-2025 ) Tj -28.3217 0 0 55 639.71 521 Tm -(ORA: ) Tj -42.9331 0 0 55 727.5 521 Tm -(13:02:56 ) Tj -43.3757 0 0 46.25 1092.22 1838 Tm -(85.99 ) Tj -43.3757 0 0 47.5 1095.22 1540 Tm -(85.99 ) Tj -43.1195 0 0 51.25 1114.54 1479 Tm -(0.00 ) Tj -24.9575 0 0 61.25 277.94 209 Tm -(VA ) Tj -25.1488 0 0 61.25 333.88 209 Tm -(RUGAM ) Tj -28.1352 0 0 61.25 444.48 209 Tm -(SA ) Tj -30.1242 0 0 61.25 498.85 209 Tm -(PASTRATI ) Tj -25.6067 0 0 61.25 659.88 209 Tm -(BONUL ) Tj -31.0688 0 0 61.25 764.85 209 Tm -(FISCAL ) Tj -0 Tc -61.25 0 0 61.25 888.96 209 Tm -(! ) Tj --0.035 Tc -27.9179 0 0 60 221.93 145 Tm -(VOCEA ) Tj -33.798 0 0 60 334.64 145 Tm -(CLIENTULU|: ) Tj -37.6326 0 0 60 552.6 145 Tm -(08008 ) Tj -30.9568 0 0 60 659.85 145 Tm -(BRICK: ) Tj -37.0975 0 0 60 782.6 145 Tm -(0800827425 ) Tj -35.7981 0 0 67.5001 241.68 76 Tm -(TIPARIT ) Tj -31.3527 0 0 67.5001 391.84 76 Tm -(DIN ) Tj -32.6962 0 0 67.5001 471.65 76 Tm -(COGITO ) Tj -29.7288 0 0 67.5001 590.82 76 Tm -(ERP ) Tj -52.1135 0 0 67.5001 661.8 76 Tm -(W. ) Tj -30.2193 0 0 67.5001 730.68 76 Tm -(COGI ) Tj -32.8611 0 0 67.5001 805.71 76 Tm -(TO-ERP.RO ) Tj -44.148 0 0 51.25 1093.45 1362 Tm -(14.92 ) Tj -43.167 0 0 51.25 1092.49 1304 Tm -(14.92 ) Tj --0.0276 Tc -37.5 0 0 37.5 604.56 593 Tm -(90002214982025121413025602170126 ) Tj -0 Tc -42.5 0 0 42.5 1162.23 1253 Tm -(8 ) Tj --0.035 Tc -28.0603 0 0 56.25 958.12 651 Tm -(NR. ) Tj -32.5071 0 0 56.25 1011.93 651 Tm -(AMEF:0001 ) Tj -30.2126 0 0 47.5 974.73 471 Tm -(TD: ) Tj -38.303 0 0 47.5 1028.59 471 Tm -(00046924 ) Tj -31.5815 0 0 53.75 1029.66 409 Tm -(CASIER ) Tj -0 Tc -53.75 0 0 53.75 1154.12 409 Tm -(1 ) Tj -ET - Q -endstream -endobj -5 0 obj -<>stream -JFIF  - -   ++&.%#%.&D5//5DNB>BN_UU_wqw  - -   ++&.%#%.&D5//5DNB>BN_UU_wqw"1 ~sxߺh@<@XkrŽZ*#|8H^+A0گZJJEzD֤ղFZ=kJzh%҉rCF=zV.J)z~Zyy:l慫Sʉ|%`5=p_]JpXxz.o;KJм-j^hMkbV(D5R0"CҴZ:z5cK"=vEM\w->OZ+eh!iEKE$3Ũb؀a\"Aʗ@- -Ujע pU Y5\`Q Ȣ,Drkr[[]\/N=l*I!-ʲH+,aجOD@h1\ex"evTn,WBPWk+r2Xj4 BHĭ*" 00YaX<$ejڤd`B+T/UOMK@zW%c64528YM8i5`^gKY -V XJ@KeV0ua(-##°T]E4l&9mX=pmVMUH*ExbAaQ00@J0*x+BX0S EadB@8VHc!RRV6-itJUU`ՔD!bY іPD,*#)$!RJ@0Ԕc[[z @IY6J@GEqK!eafkyhB#XQmnW )aJYYeSXDh##DF-CՕ )(abi %BA dae P02V [Ta#,Ĵy*dWPidhV! P -b00Q"Yc:mh Vt U U YlZ,\pb0bg|KPPj0%X;o" SbS)e,H W `%Z9Xz%HT5ŕ @cV5qRFXe+[*),YPla2@kidhV %a*dF"Ս 0²MQ#oZaRGW,/2!~'1PB\k[X4 - D`RY[*1m`XT+|" {X|zJeq\<w^tI:nZ$&ї#:z5WX0w镵y ~ ^J5^˺eixF$*(ajHT0#G6fT!}o۫uHt.#x&r7O -XEC+`5]w`N,^z-(4}';Ը},uhnMչEuGk׫shZnΏʶ\|ko`. -JΥ GF@HQbkz*$tx@$Jk" COenJl6VF5AZEZް(DEkFVB\ 5, U1BDP)*exOO[.;&tN>3rȅ7Q>yVqϡV4 >7iӴ1m8Ӓ;V4x u|Q.X Yͅƙ1t,A򮱆Kux ..&fءFeaB21) JD0$ WP%L W@ !Gl붍AMZK2EC%EjʎR [PΐUIL0Ix[_g#ڷw^Rsjlcm[0< Xl(|Ӻ; X0abGX}{t6G4|}-'ͱz̿3ɡo>Y];539ΐtW&dX ߏjQm^YPbf@8 .": %Y$#)#,`XayصU B%TIZDe* Um 2XՌk!Y R55p)B@,X!X2B-b"A*bMCp'$2j[UZf%Ƣ?JR:A-U:cUP|ʏM`?<~z[ji`gЎ?J1zS `t[ D&HB -C 4¡`1T: `HQU%V*VXȪ0D )CAX*dXDd(,j5Q$Czx,YʙQ{5< -U74WW:+5]K7O5ZN#,,!x|ܮ^ݬl;~l4?,utõe_&W^Ӻz9滿jA`:iǺ1G7ěvNE !G4Y:v:-۲_d*e*Y%@a k#D%,2U\@ ]1 -XdBՋ]&ƌՂUS$0 H VWUJ^X,R/ڸ{BqMK4'=;yS_6s%yk9GΓk#?=st γ8]GL,{hv;Ow:qƱힹ}ӱr^mp[rM'?ܴ{V7lvN+b}Y`(^{_5 nmI\d:ŷp*bXW`(J VH "dR!& -Z)aYaJ)ڄWXkzȵFWP#VUq[+X"ZQ]ȨFUc؇wJa>܎aCfg`szkmxrmܔ>AcE߈jmsV NF<, -=Pj_8-4`TArQlCf7z[=vRAJbԖc=;UqsV"8 $$d"RU2\P !VaյVc*(ȦEnXO86KP -!kD^ŐNJu_OϑJwyD` T (@H@Caq8t. ۆ/(6~@[]͋'.|x]պvP}UfOz^p{;yl6Eu - Zo]%0(aH$*Ra - " 0!!$%IR[\JÈD m=S,\At([ILa9*mkAipɐWRIgEE5,,p$bhA:'9k?kg[G w+7nYvW:а`o<+x:ln'm46ߪ׷8yXFφyrŖ袅zo$C tPF*&ŌCÉ+G md*։ -+$K*!F [q$aΠ&YK)*gVXEy!q`%i0BEECG#P 4 W!ZXb`8ӪVunQ9 e|31:2ׄuI<~LuZf%LћOٵSs[6k3:"bŅ,-bB$11VŰ: oY!J؂ ,p䲫Bh,[!Th2PUa [$"B4lE0X`b«,Y, -Q+0\JXڥ ],@dH@ʤq 8U@ XEj油@hIS9(K0Z1T,(h T вt 2X`aZƅQPbBJHR0X&X@H)j (epb\ -C) ŌF$%HLG,5TImz؈VPƷ`)е.#G $z\J(krN$,z--XY|УS%b)l72,<4o@h]mbpy:%,s\@jI5Am -AՁ [ZW&3U1f WP6\lє?1'V5DqqކQh;zq~QjF]0޼njd[&'jv-X_"z+9{l%Vy3Ź`oNaӰ qw;>$]&s*$Xyq}ZV/ #bdy} ˖_/39MA=| BDž}d[sIH/;m~4^_R>utN#PY2D]5fhKJA7_G9kW&?9һG&4.ڼ:k\6l~s&W]QF"-pa!-WHX%tU@E+ (F \.(alAcY !$½ a [ݘM=CIX,$(P0 @)}`#VF@*9)`dDt#,kD+ 2e -sLoZbN7/5kd׵[O/~V{W9nd^eHtmMk\Sԥ@*XX@-4.:]̾2\,gV!uY }eePp,rSs@ ]يK-Ў.%B@eJ -VnJ*(E@X"+@k -:#]D*О,)]TYYW#6:q,V3V5ΩZ7˾+'NagKQڪz18/sx۷.ux^w%dz0y>S:W-:P] J_hCh盲XOb _5Nr.羉<"%hi #ekAPEYaEvf@Y Cʘ+(,"H! - ;W 1V *!b:1dV,A&(  (,Pc -JHlT&EB#XHUWJb#bs/_Wϻ'u5ͺ)fcr}9'CE&mM7f,50`ġ -" -%7SUH טYs5+6UK&YIeV2<6Ed% Xbb D1, -BE`I $ 0*Cd @aB@*1!D,WT!uUu(c_G.:\+ 绱}g-ïLcaV$,H  -䌲!H! ZZWdx#+FRE"3+-°kI=ZVd9`gZ͎;'&:ɷ {㽛|s0O~0<'eq9b;W<9=Z6\MƯYo󣕫̂A@HGQQ*TAcC. a%HP,@+BjY1`UZX,0݇oֲH^Al6myO`95tsv~>v N >/E'ι>5tݤJoHr9+ 05l76R kCg'LWm,,bޖ]؁MÂ;+N9Ok} R\ԢPVWZłǀB mvC0+ZlV R9K *$ XaiT\]E#^K,Vba"¢bXafU/:r>N] OT='[yؽh5qERPުj@ -B$3$DX<# -LnicwZB;\VQmN9}%EX#K̺^u%xfcyWF&θ)o -)]R]] $u#PL-ky+mcvQS82IPTY"YZE+( -֥W>T*Itky o3T.Gt#,$yGGdx ,z숮t*k+bWe@ -H \Xšİ6)L.XFG؄ d+V`BaJa`MI6B`Um)We *)QW -Q+eh mgz3^6<޺4[:Ea6;?S68?@7E$[:5CqգpIzEazf}M/;hx_z9<>pn63zr{5xp&fu1߮e#1!`6E ,"(:C 9GLVE@xD 1e"DFYna $VP"I $!ZZypV!c!mܯ̺> 9Fj'_?<#ǰ͇vrGָaz/3+y7¨r.qgsvk9gA9I=dϏ2l\sSri;qyO~G0%vnsѹ溽нQұ f@V SVK -]n"0%=%x0A^Ftd$FS2 -#:PbxVCM *; jD.Z8D[ -9Xqܮ*6 ogKb>s Y"pVn]U]Ap,bj;x4 VVkyoi"==+<Ul?JgcK9grf#o_OST9eK|W@4,1zJ^X\;%I/o-O-Go#5|{EMqa1\eR*ed" -B @e0ec0ٓs.`珉V s+C D{/d=[Zc[3"rޫ]io][+0|57n ktRdl`@dYn:ɋ'a`/|x|f2xFn29&m\tj~c Rhоlk>,m\fkKt_vi\ænFx -WtŪm8}洌QгGQ~?g 6=SuCq^"ti5߹u]0 }sҎ{GΧ5[Qz6׍.8B-)*qPb[j5um*8V蕑 JA,. ť -,[P %M[4W2[të[>0Y:c/ir9ONiej!8wU܃`0ƇϚ3V>j>5_5R|wfL2{/5Ϛ#z%*ra嘾FC" 'Pzv-0{-Ik;G$`5,Vpܳnj>9nӭt#mzr]OR[|e*ZW Ml@ -$XV m̳) -VbU6)BIHXh2h!++) X0`e`GCCyf7L[v\ܸ'zr+v*s惏@A0n+^n\SK9b Ju>{q7&{/w>wm.+{sÌy}8Sz'$ZP7M3s=/3ӠnFĭ\_?Yw]7tUC{?dZ)I]?Ŗ$0Cxd)]:u^O89 Xoo#qٳn.M݋mO<>~=H2Wht؜ֺoVpg3ͻ">9g'N1^9wKs\sLm<ۀWj=LX2BUbEz$,!dW [ -GXMqX+Sz$;:7پkM̦PhΔg [*u:UWnڎ/uiG\wM8|~o-r!<^e;?7s·wg_v6n79k0;ϣVUH I:6o'?m2{45=+ǭ: [!LKnJ[Ge zrn@."7NV+qnǣw:Yp'X3;z1cVa¥zQ HdX)RuaB`L -i"bVYeJBҫG ! -17Kj/"DDFT{ݵicz.ܼ3gӧU/R:DH.BE$y_<ظ&6ܼl6q⎟98>Upssɸm3uwvaRFM;'|\2Gt+x#4/kJp߇6豣ۣvӏGkpDq~~Ersx.6wq׎xj$,(w].̇5'\eGy/T}1WLfQ2矎}+HSNG׹/[/xlHU^D@ Xsa<~us-?9Sz-NKH"mĆ[&@q#]0! -yo Б #0(Jh!#%Cwf ]B SMpU`]BT8ÃT>4sZ.=~cxں^|Ǩ|Nf_GS%y6cqGCF|<_VrrQq]:ty& R7@ݶu{ֹAoO97PrӋ̜kb2FL8ݴLaL>vjΦѡj5$ -Z-ImJ1ՉX.h -jtSZ45:l[@䀤 j^Y(VPYaHeSFrweԱ>JҲ< L PX -FDdiK)IIuIOP۹^4h옎`v̳&F6W ֖#(:SNp;4)(Uz e֖:Y pd!dCrڲKExj.EY.E{[q{=]V#K uʣQ -` +@3Av 1ޙ=Z֎g ϟp=yo k1]OC=,*ݧJmBn7ɺ&5b2F-G]bM/9YWWf+Kh0o{[Ca7:aXzķۖ'[y=aCl PY/92rޏ=Y _]9uzxhFS!Rt{o[޳9/=f{NuK_ -D">o뛲ބb]]u]/kij\NMxy`s[Gg-ܬe,FjjaڧMQ -Yx+"SE՞=#QeWXHKhX59槳\U87kyFT[%}=^oBYLJų̻*9&ᶚ6l\~D)dQa+4XΪ9ظvXQg2w;]m;W'r'PL&^_zfug;׃NS{<ZZ6GI⽛QեݓvC.?Ra.1*g#c\Q{s?ŹUiCW+]U\ʉhR*B;Wb7 +X,ZT@ V -0!X1А 6H@VLK (9=^mWayv|<{JGa=U٣){xMp!jx\Cګ8CPic],eIe={=<[UI`B&n+Xدu=!IɺH*EZUѾ}Z5.9бUVXShZZU\RQX)=t0Pʈ)yВH9HJD` #4,P$`)\ۋb=+cp~c؃d獫<7oD֓UȜ -\63g6D3tV>\|Gk~# ׫nǻ=ǭ8;E֫o9gS+^ADZ)$T5AR›+%jhX*t%91&k:=zEJ'pZӗgs:FS9?VC3Q?l u5X2R\ P mP5VPLz}w0?`9׫⪍/qz7@QvY}YmNZGtg1^w8mdKZ/KƦ-PHKj2,B+_@Eť |WtNQcH* ޴qbobL7z{7.+7$V̻Oz?k_{H܍x.ŻGG2GYz;^qzgLދfd.7oۯgCEJ I#ʬ4Jk^#Sӌ9u9C~)ٸR{Ga&z)+\?񬑝빳ePPVj̴unWMgq7m+/0Hyey_?z4KT9ԭԚt%RRjRKk⫒PaG9V+0e`*ڌ&orv\雟ڌ9bVv=}~c48J7 A4掛Eů柠5$;!Yk8~fن5[km~USb4ͦOhdt}kc1]NYق2pJ|u"q O*598ˍk9`=f&W!ߎ{{)Ѷ]`̦ughx ˨S욏F5J},__Ms̞FbIt=p?ꉤ5ެbv hp7Fl~k\^ݐFHA Ǩ԰fT kb[ISApѽ9|܅us7G%#x2|:>Cjt̺w&6Uy avW|14 RÓLv -kcΗ5 Va:/6G35| _f0Bt sdxN !2 -u^ko# -U{cpmC,fgMH'Swk'rN?yևGYJ7IuS.ήKIi`E[泒\tt,yռM,8o.ny|ܟ9tV^nG qRd=l9_EYy$ l ,Ց`u@9F$EX@#H0@ -Ց YHUadB0Pr !e#It`X2Ғ[+ٸlf{9ii;okb-[NuMCaAxOG׌_aӵ#p]X2&ώкF7:we#Ӷyo1&`NͲӣyM4W&Ӄk&;}z!Rrx9|75Ys@զ ˩솕Թxo8LbWFH0R@ -@FdPBFQ)a `j -a!XIX1(Y`3FK%p0)l R 3Vɵp?8wL :;zW, 2'fp* Ƿn\-Szהѷ-HrtM4b͇ʝGq]c#:$Fy7[|9Wϕ1{G[]o L7S4pçQ9h(a)ּu# i:z".: \,(ƻW:/3\rSsN^:;?{ƑS9t^1x=^9uq8OA+GY~bt݆й99Q\gbś6}3r8&Lj^CDX|w?StbtfA3ln|xcmkf0!p)x"إr$v*Gf -Ū$h$xk>jjZ6)zA(/߹B=ќu&ynikb3h;EQp ^dnFN}st 3Gmx#zda@5em5g^ 7ɾz=Wj'M@@U" YI$ d|::63!uMAj}dw=ͺ)_nfTq0;'7mtcXش=2O<u&լftV+Uƛw0b2zIhA;niGmSZu9fOgzM5!_(:/V\ft qu;\!sΆBHDtaXr -cV*@YCpj SXhQBU,U`p1 `Ml:H:`e'_[írNz.G-|Sr7noѹyΏ6Ht^h{zxNlunz1ӡt n/,gTڵSHf3WvhۦZE UMj6uc.ϭʌwMt*'㲣s5gl{nqH RB=LXj#ʈML̿On3ԏfϧFd72E5s%qC^ڲ{ }ߥ;ecOuu^W2]Pxt}B 4"2 Gv83gatLg4ӖGY~hy /^lmy4gg$Sݵ߾H [8v3HmIks/>9KzGE}= kcwlAzLW@T1$h,`,x, bBId\dzmGȞlNv:J1cH1kjAf4o)y&'y_Y%й5 i8E^.o盟v.pe3Ն=ٯo0J|z_A_d}ׇY}w휢2=:_\\CpOyyN{.g.'7L汻[O54»JdMZw tQq9-M@蜗l֍`кExgecSvZϡI'KcϒMup snr:xYYc@뇖zkPbHA#¹tZˊ+2/=q31p{jKר0Yyцaz=˲^'{t<# Cƾx[$=u~|َgFrnHEUEi6}Ѹs#~lqwGfSɘ5m}"qOf/p7}-\},kwgarvm&sƟwH#_T3^ǔAk瞀R= ygGzIKDCnQ=XL _c1tv5 /.;sފ֎ ݹsQq>;b8wmp.}muWOO>ͤt#߿hؤۇ8pߢ89#cزXvݫ{ivo~=1`U oŒMӞm<6 !nuIٶխMZ!5~kT}4 ɲ _1"|Z s1f][߸uӹ/X%y)s]sGnڑ7>rpc8w/Y -ؑCC -    ,@ -LIasV9& ][w!(1H -rs})j{rÓux@D$n8f+xa恿'5MϜ>7餘#QW@QLaC\Avty`p5cg_9>ZԴcO[:kMHI!$  `I!$!$HI!$I,HI!$IdFI!b ,*b$H"$ CV4aP4$Z9ҧI79{! `#Aa>NsrڗSW?2]K9Ck؎sپ}܍7^Y" DHB!$HI!$HI!$!$HI!$HI$BAXV $ BF - @(UisY$ID "C 0efm/.pLΡ~t:N?xtHHC z{8ӹ<ջL\HTWg(pד>}3OįUm?pI$$@ ђI@ ",1H<1`1H`VR F A`,!h AeHHV F ea!  !aBV63h盹pf?ΝpkY>3耄h YH`*M󮄏 hh``$V ep$, 5@`TGd -T:8XXir@; %dsT, DuiQ[J+["\dx`a`H0P0"@XV4VrjD R@V B0M6e2LO7d׺q×{:84|T'ؐ6䌁 Mpʞ0J0(HMn+DB\," 5a@@"XVY+%PH̰( T -1mXa"0ʁ\FY!F2 FZZI5Y!fBqԉC]tBe`B]/ -2QAa(!BC4 (h!P6S`ՈYЌ(0Xk,Ҧ0LnPR{21ZzdR43}~u60v"Kj -ftۉ'$.Im,_'Q64|4&'#cwU[6Fcڱ7o2 o}k]J&Ҿ-Kk{ҳW܀7_ZZkGcLS:E%>+vsQkZiƭurYڜ晝±9ϲ^0Σz<įNooTz43{<>lجrqSxkg_Z6̬9o *2 `!RrN„8|4,K!Ucv0yW#a([k_-^$u72$p' +sU@DBE`"[PX!dBTjadR#DR3h`A"1mʃ H)1HV"Y§WQKY,-[+- [Q<,by^VSV\YiO$䆰p2 ! -fnXj`pcF: -4VXT+uF"Y4+G -EGA`a -\mo.@$bL$V- 1 r:gK@ c. CQ QA 9MEBY%LbJ $ -Jˈʥ 90է?buMߴg[D oN$ooqgLjhz9Zn;G=ϛ M -e=BYo!oqbsײ&eNH J@H DX L9B * ł ze("JTGPB mn#A.KG Jd$8J S:bmlu5yTCqj@; P:A\ͣt^vc9Qum7o.8]2++ :gp1hƨm/No؁{u dUtZ VeFDU"+»  MaZHV$9" `* qa#V%*#@ ed%UG"ee @H$X RIj",6LyFy|ѫzK&QE! -+ٰxU= +\g+hqTj+t5BAJS   uC%Xf8D21#!8 ʬ"5c`V)Wi[H!c`@ b)C b!#XH"#Jc3,@ -ʱXh*) -Ն`Ŋ{yv-OW6`=F3ٚ 6e8t֪"ļ6~ʷ2k;Ϣ6G@@Qp"<++WI`B+p0T-)h#֤)EFfHA S@:ژ+ZA-"*lIБH`V18Ni{Զtx=3p8Ԋ2ף>4iu?׹ ?WSd#_vb)|s/I*|1Sj9lY\($00 HF"TՌDh,p@kT\Tm%V؀XVKjH+)X*Qb4`t daPvE 8,z Pi!AZ oq=8a汗9q HEx%^|✣aI3Ta+$f,+6E2l(/aZflæ5~w7M\*^6s&VO^Y h>׭lWo@ .VF%NJj+KUTABV:#b#! 0#D%E(c`@A!"Xq24 X01R=bBfmFAoAXb!2%&O}JHj6MG-A2,gc!M.]u3ugԊZAÏv.MffV[kI7a׶#D !e@Hq${LTC@vYMBD ` S $+V -4B1Ei\KBY-9,VDdb CTUb80XRrXZn (0M3t֏ft Ȭ ˩ jrBHUk>TT&bVn樶5}ɤVZc6뼟}\h:1 6r@Uߢprh&]!=^TԻG}~k[?͛ Izg4鵭"\s0Y8L!R#ttjY֏<}s/}!ҧ.JW7w|)5]M+sNft]@]t|wW9Ju7$ss;5Wۏg=(snh[A=ɡo[ZfyHɿEz+Ԭ5p#H*a - )$,<I -TzE%+TDCrqy|>Ӧs^s.9jNXax.4i~.l̖69?K}2VgJMb3nDirc8`0?t/tØ{|Ӝџ:}s=>__y6}snwN8|^uWpYEߦcE߹scrnNm ɯ쑬Xk5mNWκ78+:@N{[[윀^[yJ&C t]4nk9gXc -W FX%A"4!h`" ~CRI뼰 -޺G?nz^ZN߫mj[g+Ry}s.Sy##Va3;'<é',<é/8tоU}I[Y;uΖk>952iOu#ͺW=]NTn.v? s/Cw7κ;Eߴ=w];зtw),Se+FK V}9<:j3R\QfHfŰFRC QI$ e20/@޿>Hv R *KkF&śXm=kuH+tsLҷ )VS7_8#ovѬmw7Kdph=+;'M5ZrHԳx,r{AꜻG܏W$|s]оsU51>Ö'|'_Co4^rlϩsEyWVG[3z|9#nzi0ѷzK9F΍.i;z.nr3s~ #-4o8; /QRUrj+ "+`A ˑF`Al8(WWU`qD%F d*R T2n XaӹR9B}M7Et2κM^<'5?_5>N׭m&iѝ${uozgs3m6rظ_g^5p7QV9/XRCz_й_3}.s*n,SQ̾~<h[;ygp9F2\_J";~G_nvຎ/#MxѷsNK=/t5RQu yOW'FMunFO;}ӟot&y:+P58"i 0wapVEVׯ8bzL\Z0皦׫TSjrޫzZW?4ywX]3͓>sgގժmƳ 3|17;8vkhOڣlҼgf9OVM9H9/9ՠ/O'g,?O| `:w1ҿ5})o2d<&3ɡgMu^m|ǩN%8}^?i%;?f}h[ͺ3.4 Ct5-Uw=Wn|s>%O?C2ԾNz)SMWQ \V3 -D! +dITjE>fP TJ]PVD,c&fcp6ɵkRzԜãhZN~%j64Y[>\rwCPFNjc fuܸbYu|6_h58pL7T}V^YyTNCd+oJ91}?תUWQ//`W9R鳙ssX3ƅJ6]yq>͏xmgrUyK!V&WszYz79_^nfYm:n_ԏ>3<Ϻo3f˺,]h}{ -Nzs{9E4/+soyܴ4Wٗi;~cquܿ-=~;)4_~x*9NhtHu^W0V-S )9bsC*)jVbIP,g}^Jp(YU\mA!b - -i\Gjf6㽟sv;c[u=yxd~o -;Nˢsh}cusrgp1g=Ż;>ï˨bs=VWw^S[ꜷɺ(3?hۣs:%?1\]zkuONAg,O rz~k엍Yu.W3ϧbrkcSLuݘ[t9wSl|˪rGw w\;*s_Z:7<1_TNcyLSMM'e솵ə#Fߝo/|Se3?Ϡה]DN,QՍ`U*ۨq$$AJ#$g+#('투R(!,WTbWkygu*k6*Ccw|Q~^I56ul~ۈ>hX-CV_l7yۗi>hzF9^vU_[N)}v|Ѷ||/)~SX|/&?>v+Ϧeq]8WN،b5.tJ [_YoGS=5p#/uuCD|&c5-wII/87E2N0wDZDϒCd0G>)~ Ԝg̭!d3FF$@ D̢r‡\PW 0+23 2:,!<$DX, X( d+6¦b1QlbRWj:TZ -ָ]0OV^a}e6(_KY`*+ R -' ,b=Jy(kz!IfrWJUVid% -9H_YBYXcX#*+ B(Fla`YY, AZ8$1Jypձ/3Lfsct'@s ]F9kW\'/Nn/QaR$ BQR{"ڂ_L)oL1 -JJĨZ -V-R69J=\ˍ= h+f!UrV,aU+X) -V+H-Wy,`:AAcG@H"@A4ÎBPS4tv ;և7SKQ렿GMewmy_umg$杬8[J`  -[+׶/97-N gYݪl;GjxɳS5sn^5h=XSF퍳teQa2 `,J͵RBbkC,A)P"WeEA$prYSfFHHl4HGX[A{^&7={`0t O\^ζ=x/{w -M{ Qf|>tG-XVB3+FRP(ϩʺW.1Ln7m9_Gה{Q׹gUkyMftBuOӹ ?njx,]8J}<:=4I*,D`QШbMK)#BT$,Vd*BE%B,`QǬ)`Y+#ݪYlbJ9"y)b \1U``5ն92;70Mc|NOZy9q jǻbK-znG-m{9{4lN]թ^IW2d晫uȸ+VWd! +tzHV;$ $y -TR@* ]VxUA^tgߒw|W3ʛp1zٻO8ɪLM[Ƨ=GP8J2ǑKcN5%zZRk^ɳz9Q,F׎o%QBMtK˧JCVW Ayպ-ů:ZRXk@Ѻ;nSuW .buvWx&?v;إKGdr2 $=/g>޹OF9IѱԃS.-o?:->s$f EfB)QB`@b#  k#@DʥJAK` L^SLLc6}so^W8kN}h/ 8?fg0Y.Nz{fnZ%cgf5~D}Ә[y;zQ׎uչnXJV2 ҽf7rrjSSw &R^zJrMUլ Uڵϵs̶K?腊* $,ub$܏\ޅZ#1y=^E~߾ه|[_9~9x,DY(ApGF $5$XD,jD1BL:j+ .td`M5ˆ֫{8=6Y'!.oe?s -0k=ţvJ7eژ,Տ.Sf4mƙE繝V+Hy -BWl'Y1~`%^UWE֧y b -tZ0>7kٶZL+'UږÑ@Dr*) -*S@0X9%I#I$ !z DdR /b{I]^UQҴL*BMU\5-H~0;vEUގgдM -4\*y}gʬ ,$ E(*HȨ!%vF uŲ+6Y P04Hb ,"-V Cb`J鼈 6RHZ)+z[* kjqA, Ha$RH½d1.Vb -ŀ@@BXP@$E`YL>zq}_sNQ*2X]_n{Zf_7J"Jq=h$U=`,:Xq  -eWt0[YaH*(i*"3CBh#(*ju =n[mA VDd*XRHXk4 AQ!G ] [! ua+D hGP"5%uiI D]2Ttn\ݜ&SjS匓sJ(3Vj^{Mh8өZ07}njV5B +z)B,"FkI,J(Vy%,0UrVb`BR-tRHZ ŽK5\bX !Ņ漱jQEaĂ0%pD+AI+ -?o=dδN+t%XrO.ys=6{wN7o0^o'M9 }ÙaL912)ʟ'+KԞ'}~R40Q% +%VKmąQQƄ2H bW $ `Cc$@V -J쨊( ̰#F$6#Eb  -Y`je+a1?ͱVW{p\GM#s9FSoZ9ji={ ^oY:77柇 +/b:{Ǧ.<I}2yD - hVZ,W h4q 0B@"@C*BKX#ŋ. E jQᬭ, K* -2F -JBI,Pâz%DҢ\j>ug4:ұ|\y {R٥:G9o -h[T)d=vJ@zzV_-+G -h\CgQ paKdBD!*]XBVCI :P@ -yjAQ,A#(T"(V*Y+:BY#"8 P0d6a/+̛,;^' ]yEfXϑ\z jqrv]&WZlntեa^s~|iy:L&uD:j~3u^Pdz`Q]_z /)4_?W5 u"J,5-[q`ȕkjkV%vVHR %()p(p"RIĘO-w,T)kd"5bWD[$%@CLD"(;.&?[Xno/Ec]Vwa9 KicFֶ8z^]4ljyaѸSs7CvL'?7 À7>s9Cշق=7tVm1շ-<]كbvbz+5;WM=L;5ȺHU9IЎmwʃu6aZ1\uHHXCTт+ -C*WBՂ85說FlCH`Q-" $h#漕lF2pͯ+d1>UmF?FW.[] ؏75h]b>ێwfy3_mݵ.:/5NƷhGmYeY3Ti~y[39yeӷCwU=x_-(6oWny%{j2YaqG EJ@-Qi@V$\`)"p#F8Y %e,0w)+B)KV,Bd e@j/Ԅ$Rhb ~1CziI˩Lv{:"45uhtLy˭Խp9X^ow:O6: e`H bAp -؎I!"* CKJ DYXM(E5a `nAa`3GڍC r=Q:vV.yT^SָcJ9LGN]O/e}?C3~}Tt=XCҹF9z3^#i{ezD]3ͺ?7MI׳˞aiGV#fRiDƟ Fv.yߔzc.7%6zMSՑxA4lAvl&dNhe{1>lύ2:'B .9OWӏՊ_>xw T~nFɓ9V9~q5O^sz Zji+K+H 5Na6PF "@ ^=q7(jQAjj$*:[ !laSd\1K׶;՟л\rlצiFn<}ޞ{]/L)9yr:ɰ~<^^BRUMg'SS|f/J_]uP-TaApvJIi@ZkRЌ{<^ -붒p h$ĪYksуr\=v 0*-U-w *,%qVjԡ ,s0}֋OIج.0旓ד} # -FhV/%V,F#'N gאQc<-5ZYPABBi UVa - -4ԦADh"[XKVmbĸ6b -;(k x !B$V CCA`bj-<ղ"ǰsj*=9#:Heq F@,B`NR-*\?4d<ya X \, Z 1TƢ !HDxI m+#iJX -Ajث}dxYd Z,EڠbX3VaB2!d0h$(B[?#-N[u]fX[`F@IM p1b R4Z· 'EC@@hi $#i 8`hPC0S=V jGbE_ftd%[]ʭc"A@+,jj - P;) B!IHKIZpDENz^K2% S= eU [W{K< 0.I ƒX1=y/JPШ`r=dKMNY+ab\UiTIQڲ\QJlB=$?ԠT"AX[jK\JPP ah,_:X3+FFd^c&MklW`3xAC,D<>+ד6s5gz^ZT-̈́`/?Pz[0V  D*q *=3VjT|engTF6=am ^gLx}cz9RlVsi_!вT@匫{f y|?534!12 #$05P`"@A%EpDBCF4O>Fuٮ5ƿ;_u `#dO?G덓=D|#4{s~)?Ƕ^'Ou=;]mOݧ}_YN7]~??ܵk g|>I~nOS4G_Om?/?/4$ɧg8d(c32)::ikD{׷[zC˺an*a ˬ2T K_3q϶#3~Uů6X۠$wsIֹ#HV^J]kOLo0BYs/l/AA:YY_TmN[76*uZT wr鑊LeXg-._,T~bSV2US΢ŗE }~6?OoRB+ s ͽ?,<۠/fik<8YgG gR1v5j9gCQ'v}^#VZc1П3^^8(zAlXr褔! -횤+l?겦U3-\f +y6-̎b≨H(q}^-R6~ORZqfXV !v`,b-@Q(@HPlw8G5ٯ5~mq5"7Ip0ٮ']1 .aNEkE.:Z?O"q51 -'K IW2Æiū0v0Zd1k*/%P(/bo\STo&`ZZX鲒\:2E r.)>cό(ݥ#\#ViO{YݰBN]uJi-UםJSN n2v3?v˗ʦde^~ybU9[~e}[c OuIݿ,4V6D7.t)Z.oGc<.nto/, [QᤃH\f"҈ -IJėӇ7k/_, -*XLӥ UTabu>*f$P5$U-P:%+`F>ݯf W]ZsqI3sYx}T{YnfrXr:ZJV D@LDX:^l~Xd5&q;#?G``-s!`C^iL'Rx,б"{aF qgd8+3#1155Qfɳf#sG<.ٖ5ۦ܏/*K. 3<ôbkE`tӿ^`^^ŕ/e}+l[PU:j2/fM*\kD|5soZ.]@B/fxΞSzu3P1n>.8Пz%ر?KZf13ezE V\3w 9jCُޮj VJ[zr,0YVi]MPU.48O= úg.\؉.w%?Θg;exlF4,o1Ӡ`jMpNۛaEK嫎2Jeywkzwy5rtv`@ĦyI]XSQ+Myc8oml|cRHjͰԱ(mDrl+\?)O&TS(E,|-.2Xg|}]>v ܨbٛGXfS˻C+B|= j@e{Qai%Gnw{sR:)~yl{}|}T]_]8Ү3"1oJvfu>ťO¾[z "N\N9[^7ob{` Ƴwn n|d/c0\nT -MswEDZ@J7BGERK.ֶ+qĪ8^'Vbmwuƿk=vw]va@-W$%ke -l7Kc7>"C]=}kgLkDlaVěž"ڤ|EpqYMF+6Z([lw]y(Uk].N"ia-;+mWca6K]1;'A8.D;"Ao;[VZS,@` -" X"5-GdUW`+ 2'[0Zzɬh&)udrGئ}i[φ[poXk2$='+X`&lƘ, )a,FXIl/j<`ǂZa(]l,*YLX2cIb͌H0]mR) M"3KSmFؚę,U\XwC\CMC -F6BK %a`+ o -$Dil.*JZ#;.f́R8$Q11t˯6۳͖`Uk nq81Ɉk-7̸bGde]G~q?\kkvkq]\Ic\L{dn&pVD\׈l \kqq@BY 4T!nxjt9E)q̈́q4^['3weOεzg`۳+]O/mM%e3g 1lp\&[s*66Q~ O%/*^tSf -E[R9wiı -s{đD4U.m&a(eT'Z\ǹ-z '}&ɪq*Y.l.,(\G1"sL"kO˵fZ6qe}щ[y>Y 1ċ,scTe728^, -HEJ.&@~"G 1SU!.f:޾Z2(%( %PY-+*ۅ"Un]q5Ƹ?LA̻c4bhDkH8τGf]`"`Zl8+Fhl';5a\T:Ƶ+Y#AHrËӌĤTUf]ٴʶZ.v>kk ۯ8\kh[g2fp|~="he2 k*旻V]ɣkxwT3}gCv{O٪p}#K*}FNu\ _Kh7ſtqWM-٦>fYe33܀iYԩ׶JMtB7Kwʺ9!(ΖB9b\ kRJm]_:vDaԏ*O5 _#Z0kZV7 JW%MN +WKehRөV] QfOll.tS9kC&Px3c3 &sxH$5"aFcm Ř|2[.*fԠqYR6*bV_>qq-?-ZܧVWv:߁DF}Z(0cc*xČ.j|kekMq}0V1'Y1Vw؋CnG g.QP%7voVM0%Wwh^8Ba g6}cVZ=`usSf5ICNL͢8V{iU oF5gE6?;KN`D6,oݮ^0T9lQ;[C(hb3 kN[-p6i{S>Ql6YMb팷`fm}D^pk|XN/|1zavwNGZ׬;/C;>_(:,2@&nfVLE(]\Q/9>m຋  -*a;\xRQrJrjQa%3ևi߭zٕPʪoNvY#ƃPR +McO-1ypiXH"BarRK٤[X@_iF"8/Azyl8 2LX#lX1ɉKNG&0!s(gF^#Vv;: t lj""6pSZv1*f&0g"]%lb+ XvMTj`gx{},pQQڂle8kXk&'{@c,MdND#aU9"rS*qgƸ]:vۋI 8X(Ł#f*hlSZ`"`6smd`^jfBbël]s!\$}:^((.$X$C15ٮpEz'pƊwE1kjv/Rgt_ZNp?TSسt+fkôDMz;^YdĕB%A^ΡKp*8zS}6^:.p1?uFpw[)P`yQc22SՓZ5prf̧6^'f}{e+{Mt&\,HVm}FC*_9rqocH - -~7-}rt&ۅKB彦'W5WI!鈈yeY#Ӳ};'=e6Y򾁼gmPFjەgnZzEX] y+쫃p hA6˔)[Z:z;F\bīe3_vi9iE,+_R.R,؆E_զqӔzso(s,oFx8wǻn/*>"Nv b Tq 2M{9_-Ǹ̼ew&ܭE|:%9wl3SŇE]\+} -< ʣ\YWG6džm|G^Qn[&c…Vm1 rqvVʮ7'ne$0YH&Bj(h5Ux47yo7ZGS7AtVSLdܲh07:f%'8;ӡQ5ze]%D&[n>ª$7N%HRB'55Ɉ -3:ru[T*)S)duҕWQ\eqSZ%HZp->}u.@twAk_Fdc_5ƾ|'ۏε5ƻgUs(GVk-p2328/$ ݽ;^%#sm)ڰ)ztɟ pZ͖U8H0^ +jZgȘ -0Rc-r <epbl[ҩN5sH&`7㈽bv3gLq`Ҳ(G`h-al}Ld8N(L1\Z<[|,d썓1^Σ05N"#ܷqqDcHN!)X29*Ӆ(; -Jy*Zp_ Jk *֍"q^";ti SXZC2Stvx#X Xⵒt[W$7f 1+lrNө,>M)l _ak~fu5Ƹ]bN6X)7dY_pvvcAX#ؙuƾָZ\N55,)dDS0<fr0U2 >?m,- ?ӪM=YʽX3ϸ! -ks+J6>9Ҷ9*6ȉmƾ1рB@q'!F&Myk+˒1K?3 %޼[~'aG6MҳUER)VWߓ3k hf5Uc ˀg]@p u\kq5ٮvq3D#Ȯ@XˁYo,@N8p- &49m .hg0Nan.( Ƃ9@8ScŗG!`$$ - -0E}vLgnhff:pv,zՅ^Q턇 6uh[2:`('y` nRP&*J+@Ս4mI!`5 e#b?(规ziզMXe{ _Agc02 !vx,x -2 (8ÌUUWlwAzG:nbh..Ncٓ5uA`bդ[ o\&P1Jٹ-}{-Yt$23mp#Sͼ Vlx"- - -RtRf= */ҹf_k3e>{d=Ohh#B Cmƛl(`C;\Qnu,ǵSZ\fWyVA s$lYD5%umHT|G'7Z`fv|S7qx"͇2b:3{ϷiV\߰Qv,!y}}>5Ľ;֭Ƒ[0/Î~ݤJ)0WO\#z7YF2޵H:Аqjw1(k5ƸٚtKVqhnV@澫wBSI[rVe1؍keJjv5^czΪ`9zᵯ Ֆ\!/Pwl(o1˒A[eMmpB${n7O -o /m:IO;9zAyekTcryq;3]xJlhq2yiQnSBH/#bS齳5@p+Frޯ#!bSUgrܡbdi`{+ ӪqmfyǗ˻L;zsl}y: .Uf76Ts_1{w- t[XȚ W~Ɍ;5 B-$`JZFh՜ -+;j-taTUV%Co!ҕҫR-h©jf۶f$tUe=ݸ 2OC8K5kP}yB ⢯Jh&`V3֮[UF50nWg6Q)rޫH+D ː}u?`H,tt}~v:_jDK85VŃF<1P -P2xKOYx6X4pG/CVh4KӪ3\i5ƸLN78&'X\ K^fZ z۰HލF$I"~Vλ&vm"LM:ċ-'V/jY,qŘǹcn3'F-=zn2iפVC.au<eSJ[/Yף2\fbi7e=jÙRIڵ7 TIek187X)X%I,ő8lsE@iF!ߍZϳu58SsU48R!7,-Z`nZc~B-S}kt1vnPyzM.VĘElvfm@yivY,(;fXk_w_[@YZzT[ě - {xIVޜL3Pʩ_=…%kgBT}Ֆ}VjS]^aWC~_G쿾V-IDufCSt}6 Eq{KZbĮ%ֱ%^d]+ýR lP>`yhV+Z'J9g`0 {r D#Ifm$I ِ̮@VqmW-C9u'.{`}סXwQ7Qž \*]fl]:^g/C,mua{)Fa|YFZt`Dvq"A_eZt])HĝeZ!=vU zL'e{Df68A^d,[BC34L,:uY{.K_eq`gUf_͖ŊXkDj9O&xY0809Xܻpxצ 5kZ:\Wٜ|5֨x9Wf;~[ڈ9K\ZkCDaS>YbA8:VR.'Rٙ3~$S=l{JbT*)U-Xb4FB'Rt&`ao:3yJu(G<8ُj}MtF9ne}ye>yK)Y[vt%۸1PJ$)1?#I$u]B$_*YìwբjeF8G UL `BoW:1 3ZO!/4ṕsك2~,Cۙ^-ʧq)gm?{v3cW-٘kxo>I@Χ%{VկffDnv_)MWq4t3&e{|f=.#<ӄ|=Up.c[;S`3OS˅v#- ٗsTdTyW+nUu*sAe[}&Ň^NUCG{r\0;6 qίkoS0(ZzOcOlrA8j XWUT!15"bx^8u[SdFs26]C3aڼj;.-KRt.jc f5)W ܸGq}ۈl (q%o1 -ʫ]ƒ9Qxvج`5໶U]Ļ23`\N) 0w8rf'(H ]KcfݒnVWӉ:vR;aaj­B#q9FVQTa?Tڷ7C/1bqN~?1|wõaKN8dkDsSZm28,.aڷ -P/0j*C/nџhkΫy: -QjMnSj<}9W>{sD@ -,OBǿ'L%.]˻OG'sLsd|SU+<ӣ__rNk }Bo0X챧p8?M^Xz׹17Fham[EWxi$:ޭZӔ)lUGsU^ -KWTeXYmXж8\7*9 UL6ecq1Rx0WGzӣXk(e3sl^[A[a"}_Nόe()q-@Ň*[YRIu!dl8+nJFkNXv+FQՉ*(Z,̘i bbb}һ:PVkabiU>ƻ5bv56Xb";u7c3fdgf"k׍PstKt-M0؛v!\`q˝ioՁ?0-QA]v`İF eo۶xA6n*ST]֙]hSBz""~#YE E_jfe4!zPC*W -yO59y.̭v_G5󋊔e^swf5'Kَ.(Wne{z6nzrv!9y@M9ٛtB\͑L9NW,G-5ۙ8Q c;-^^h-o鸜^|?bz?H{0 mb^_Tw+w*&f< (f}4goʻ|Ӭ.]L2Ǐ0lwyaM vq%b[gfiA)A@6 - -?f4F@-}BC*$hk)֤D&gcn\NK/b sYnaNuB9눣jA@K^o[/\08CYY/3&j3pꐪi̢jM̭v'yKrm­иʹhnF6l+f>fiOCa_N,YO5裷(5zәujM4J9w.b_͡8Jy:T'+~?J^ 3^漥W a|yLkki~l6GSl=<]4;3nm9zՉʺoMцzײf:M漶Bo09 b|>fk?VajQLD.9X 8 '{unʺOWL]ikNڜe]LМ9pu&y3vق*pe In>k?[naY`pb[ñ­ւLlj3rb4fuٮub婯 Dھ%bqjŪfiJ6FLeqc0)'YcYbtA"Sp_UuWJ8?S٘3uFgЫ;I㦺Im˧#ý :Ҳ =xs nx9}m `Gyaf rcV-󮼐f {A`gm}j9deUSC=k`3_)s?yZ#[SҷWe}Ro|o)}S\k`y|LV_ շs$]o\f} -ݵp8mzPCɱV< 3?_UᑣT<8ke] U%_+~Śt -uY'p/4,]BUu%h+}W-`%مhY)eh˫p팫z?D'4q*~iz1bts4z9T֝0<ƾ.>RJk]fQQ5l+\{ZkBF1e4/FVGo0>a.r1mgtrxeٚtY( PfK9B,O+Ӌlq< ]6Cy{׭ -kUhm3m֌nS;#(,ڨ _&EJ%Wm2`ƽ~\6"цN+!2|@[11 q8q0-8V^15IV( WYa^el^6h׃]vVu8~~bb8 -K4W9};VT\PVs_YupSCHx~#]kk}}5 -ׁJJ#B{M կB/Q![5; ,V+֖*vO2*Wfe%zWDl_t\EB 3XQf2NŹb7X`Pg}.+.6 }閺^_l*9{e5-L"Ͷ"E̟O-i*S@6jojP56k -ݷb7+T"uR*1s`3-f ¡-yw_M-m3;-Mv\:um^۸)V] u6hAY [.XB%P¦9LZtQvֻx/Xf?PK5ʫv24+n->Lr]pQҠ.%Owtb%Hf^k2gCypuer<Fs-*.xN3 ƻZÜul2e!\c3fl'cA"+tӢRbepk<^_E0h?WJLp6Fܝ+rJ)|>grWrVi?3^ca|TgMj+*94j5u+Rկˮ}lw2!~n730>ԓRĥQ)f{f7CwrC`#4(b[iNϠbplJ^diHŝ!>Ɇ+wu%,Mp2eș48Lo1b=W+oDEY+T -Wkv>>l{ h(($z@ ^k< hViѭ\"#2egmW;e!^@b -3/{\^rGpMg7{ƪʔn/e^ٱƑV;6^}Dp.wۚtgmR /L^v/f'Ŏ+!yX9y~bVw%83)D 06s' EnV}/otkv־eЕ;z FH -_G2OU:5oU-[Ϻ/w\²bwлK^W{Z( j}p&~u&JbМҲ.\b/YJ`.baȦF&b홧Ae#˂HhU Jcov;M:p&Fw9rI~_ڳ\Hme}&2m e+ƵȽe։totjM[S `@\9 -]Jh2pi8I(̈` ;v\Ox[-r󭊎SB Y|g,tJ<2L#0,u`,ODM@bT- fqDNbIiW= ]1cDDŽFR兡B -gN%IR"U``DLKVWWrײ*[Gj\ݓKT#-EP3}i1eu/(/;F"eK*uP n4ţ<5SA\c0叞u,kLcp8ZD VlFbuTN#O5ƻ56"}q%N} Slo'%Ćf[&t3ioū+2-떉;w$ ?1n&.uva57]U -M߇UVSjC`e&y~8tH][/i 6\Œgqk2J3B7ksV s6-ɒiw5w }G/K vZpH?R5L:{G:b˥AҵyuDܪX/rգy㈀TO-_mҽɔp2bSwOYgϿ̺(_Ӎ>LCo(^8ޟ;:=c)@b,]^)n]Hܢ*HFKONjd|uإԱeƻ5!yvRˢxV]Z6[ӗCnǡG.0V\krv\~Ca\:3o2f^֏k`./x^Jx.~lS3b#R|,*l}JlJ"8ܯi&V+Ʊ]:E'Qk_S÷w_ ը)eN-J>UM*=k8lخ;{81Tf]%PI/YFm2g=].e侌&[F1Sz5evЏH4nN"0-S0߷F5/mӡ$j42_]\kq7vk_N/{KKX2|_qi}Jv[O,{fO۳kWڰjLyF3J:;ǣVu1Ӟ e W-r := Ӳ1W*SvV'5B1VܵeD]JYc-lg-Ap qt+Z,Uu(jZzJo+@-Vc>8xyb5X:f -Id~Ʊ\D{`#1Xbn1>EՑ0pKl ZF°!1#ۼ"8 #1)5`\xWm"8(f|@Z`{x@qUeF9qmN-Xľ">%۱+¦I-`i6O,xe6ZXQ)+=Ū1Sg R*'cİXS-y`M))[ &v*@œ> wH5!RHQX=#.+ѭjJ7e{rb(xk3nL -ޛgOuTl~V7"* mIxkxF!vۥ}1<Gڳg0Rj4Va[6X1*}v` -N,nKӝOCE4}z x!{9N.)dž"꽉`Tcs!A+.}Qը9g;,)N>leSY!vO=[g?.G-aҭ:־z.׹B~煖an35̋s//t)Xҫ-vnټ'WnvI;YC] _&j( `f|l EY 6yNU =jS5H]a@@n-X_ -H +K.L"l -k1Ww]i.)꽅Ʃ}(,kl[[,#LXP3ɥiv:]2Hab.=jݐ`$]kP^-o{6CV-kۀ -̳V -Ⱥ/x]E+$ui8is(7* SX"U||X1ej);+Y$*֬=緣OD,.IWaX;+1# u׉Ja#EqaRkO[ ]ѩ>d=z'kݖyZ)@+8AٳX5c|pU{lKUO80zX -h4-p -WeCJ+#-_uV #Mu RێMx]&KX*vt@#8YcAxik\H Y``C]\l2\ X-eCDHF!-Ŏ"`D`ސ7_ ji#+1`Uvn~K0dA. wTdJ@gb`G.){VKl8 lύ \h*|TV"r/K8b߾wO|N+ zƱzt`[SōzCA[ȳoSVZ~IL8^e U^{KTQ0OAtoA1j(jYW. MQ)9i9|ķ0+.b8gjR+f*p(-i.4WJu[z1f$M$ -=]|V=hc\G;lq+9kП8`XC{&"au FoOevR]'Z+ZZT*(JvFFB`8w;F!+L8+ŒoO֣lϳdun2ΰw<l* ]qzCSԤ9\Džͥ;$P -ݮ>~ -]%yX.N˴)U+ }>^ݻ@n61+_(=;z]PҋrmN/8*⻸fV\=0;@u)m8Q j}q;V۷У9lyWeh -ma)5~0`bMIDQDK냣1"yIB jZ"mR Ymbۇ*t+%p` 4b6^ pÈ , FP¦Z+ F<@!>HO?/O>|D'`8'BX&,g"bv@h& mF"Jq#Y!^! lQ,,k5L %Xc ޅgv64`Kυ-3XJAQiC+sV 8;9 #JIpUOXGwWP+W-'WyTa Zqlmyby_e I%~/Dꇳiq,ʧ7ujddӃ]y :H#{jR 5~9%côpV@Uia$)!m>#v!p{[L9D\*a&(:qR* pktqgWHV64  0:jDģYa>wNXrEJ>  -ŶY$uƪ -|Vc\*n`o.uP${kE!\1[%1{N]Cղ!,\5#?M+`O=<\ Vݘ)oN;j!Ud [vizi`W+KkpthlЫљݲn-Rd MW&x )td -[Z[Jx ;GAyӉm-ؤ2lO|aMgwT˧fk##+l.,|+ Y8jt -:QHo[ {*EE]<5ׯ+чFkd(9zuTuHm? I.:y*k#Yp -AC_$]/Ul];=ub6FV= -1 - Ox)+>7 cǯOc:j9TəlÕ [Ë=9m:o7t(q75jAo\aDK0ΡO:U'bJk^&5ץ?>HKCk7mX(UX'z&n8CKJ?~~6"!tc 1sػOR>BCeqԵu%M"WX*Ok X.,O2MgͿҨ$0X&ȧe")"tzi[AL8%@J֤?cZVb)WIUK1]Ǿ5eŞ>Eβ -᥺F. -i-žaB%)'SPF\c(#hDUX,q31 LH`Dd07B'd1 kxc\xkkUE 4(N"FJ +#9!`EB8UA%C+rmiDF-뢢ajV$6qщwr:s(aV.ڙ8Li . -&$yoaV!U2v9¬sNs! |LcN%Q mfv88' lYQrb)0=NH/ǃ1Z( *q*=Y_%Ff9:҃j01_ZD-#MZO]\vZL$1{hYE3R HyơG/"p 2ŚNR -E~6++ hˤT^8j`(*IʀYa*|j9v/><YZ(kC:<0bF}<Ζd`ڍQRֵLuZ{v)7N~wN"8*8A' pM6 Dez:3ܘᷦ}:SmwSXQ\1£EYho؋8>o(=#) W.c0VL8Ž'_{)M,ѩ\ -BP0VeQSg} -](Io'ޒN UnT|a}'LpR{-#L Ӳ|V~Щѱ3󻉶Rk>ड/} QM -Y"@bJk? ﱘ` ̖3@֫Jq:bM`C;Iw5j$LHUfK(kص۶2K(ŕ˿ O*6Q,ܮhHNOcĮBW^ -7 Bmm7;=e3襋zJ78$bFbհp~,,RwV7_k x?>Ο/M~3X]0GpX -#Չp#1xA73Zlە{B}SPof)x_r@zd53np9 {m(7į-dde< qmL)ݹ7-8IXnk`EG[ q)b$-w~e {-k4)UVRD ؓ*DiP#oyq6AqH#ewت1EZ ߾ SR8WjD뀗w1iF!]MYXnMe޽" x]U/vIpl6t ZgzM7+D9<(tӇv)T Xl؍ԧ%y58-4wiF``f' -اq&ߤw`3OgOcOoO14ƛ4ƘcLi14ƘƘcLi14ƘcLi6i14٦4Ƙfyhn-.LY$J9;65]c0pY=V|3c;WTR!*Ro¤tm۸iH^`v0Ч$j q*:4Uwu犪Lͫ$hu3 :&]]\f?/LiLiiLiLi6147q47q7q7qnw7qnw7qnw7171ns717'T' –,fx1&I jOryf>[1GB~oO;GOGL 10LUrej7ä^0\MZZ!:8‹ٌ{ -ar~ѣۄcӋ^hPߨO6iLi14ƘlcLi14ƘƘ7qnw7qinwcLi1nwcw7qnw8x㇍ncsdGwcwƘcLi14ۦ4ƘcLi14ƘcLi14ƘcwcLi14Ƙnݜi14ƘlcLiLi14ۦ4ۦ4ƛ4Ƙc٦4ƘcLi14ƘKi5eFz`%`Bcm,ΑWEX}3;j=f8~͈nw&$p,IXNCV1Si)jVF1wY*&X Ϳrc4ƘkA nŜiJY3;7.J͵Q4kwM}tZQx̔L(,3C -V%Ѳ5bl~٦L -KiDTTccLi14ƘcLi=6i>^4t٦<"/^?mFt٦4FcI^y o5e:sQ/CإncwEĜE̵/H"Om1ءbzj=sXxT%/lsfDE;Qeez o\'Jp;K&_`vkύ+ڴe -Rʾg0Ni6r֥ƌ8#(ֳ,̄A.Pi)WeqB8Z>,I-?f]Z:RkA+B3+2׬ ѤfBSii >6f1S]3~clFˍUuNWs ·j>7+8UMyefQ! {xFW V-+tnؗK7l1VTϸ -V-jfdo:/rhuZ&V1Hn+!Vo0fjZ]e6R -\9m ޠ):2(ZMeШJ`1ksj(^iG3fݞ_p6\Dnrk֨fu*jB@;e̲H(Ummn\jrƵ71͌)U=$_o-&Łuv+u(sl5H0>w7Z7Ҵ1j-ٜ\U`]:2n!u -spib#/+I5r/*-̼,ಷ9$rLb$V)U-DD#E1gej9TAXUAR?K:5 *%*#LEZ8* -VPKh Ҫ`DF&"`Tjf5 vRUYRo_ՕX0Tol6x鬙B~jCcHLɰV"bbv@^wZ+tmf7 -Z+ (~cddk+"rY;*Ј@G53&l)Ѱ@Wr T;llPP(?~kac.6h{Fmt*坝O1)ivYh89j\[^*J\l̷j^oc2ucKbYKG2ԬbxˡN!Oh5=ڔvqfeخ?(WAՓk˕dU/gfi,_r ծQ̃5]XY&;uMTW6N:UOGAY7s>qEM.GgՓ5*3 c7=V/pT1`k @xºԲ21S1|Y%P2N,&)`L,M56̹LLei^T]cEkaySãlսʏ16 Waּ:Ub Mz10jS0 -Zmm8 l-OO@+e]9Sumfc8,,3Ac3żp۬Uf4^aYW*Ūł!Ma<>zj ݪdVͺ `u:_-vxbc]Z T<~W5p4)2NKs#B(aO X~k_qvk ޵z:_mb*kXaX`u6@3}JøHz6ܶH,n%[ˌğ_5u6j"7er?j\j~7+˻-çu4͘u"5;_[j٘]Q6f67 ;k&R"fmg3^֗ikfmg2lid2VQE3V^kƇ0̫.JkM0ߙiU㙗Qͻj~Y>wTvkv(Kzs:Vk⿩9WwT^oЩڇox}X -a-R>ۯk}~^'2/ khX/1ivʼ:DV傼ݺ\~WoS}빱x7>-}5?.$*)Lw/l#e'~9Ok. ìv]s):T];kk-1?36V9kOԶfInhZ,e~'\km8_Ye=*}m[Tٗi6ۯƻ޿t|3="ciK]J)0Yo_zҏZoVc`]N3/e}ο3\kLJۧ~;}i=?zb?'it{Zl6޿x}>?7]$kA c1l)ntIO̟x}\x:cV"K1#BcT$Z-n& p:MUW;䇩DV Zk;[2+x(~^a ZQyq n|.YOxfkaOxk|BasϺ:/1emZl֩-r_HFd-ebRZF,(7C ,\M{|L_vC5˴ -`SW ?}YLE֝lq pԣvSPE5YahcE[bF5(W-78k p4ꐑR(Jf&0 -vk_u/uK//^Ⱦ,Zw2rt.]jUk4ʵy)W>ï[l46DG\iL)g/2"jr38e<Փ^Yg0ǻOq~^c]je5+vN2MNu ec>-ԕCg4u޿31rmSvvUᭅ_c,^PΕ ^{oyEUY&@_tV"Nͩ'_^% qzʹ.i䋄p -ɧM|l-KTRRθ8i 6O<b%OPH -`EoS1&0{T"&b n'ep[,>Wt1Yݼ2ӥB8Mk6KeСs PWc[t(^-V^)NM#Lace -Řm-9&ouN"V`bfAf51c,l!&Rt$~06rUq>yf㗊䝀v̙mw1bNݢTm#a0G`pKTD1f«s)FX`xbaLAR Z〝톰<A,#H b#`,a΢3-xbfnծf)c\Am1P Cg5 ZNd b#qR)g0Xذ8捌*2j۱LPW T$[૨5֝ɚVdA;"0@$K~G< ; ĔVZa!dèHb* :0$!,1OsUf!nOoI.0bqb&5XV8!5bh0YAnD&w*&|m-dt6 n 5(YM^D嚁èS5SD B:>'v)~b#mx:"uѪ$8a|tWtX}YJoD{Dt=6:9K1.::eoJ(˷!ǥ_|ix~u ҷoO'i,]eʓ5|EPNn,EJ&F%  -"ɸdk"舁d4?S5cr&DG%o ̧ 3 kU'bQb$9{t861+ s;*"^S3xe?Xۦ -6[lyFn*pAImcr*UzH.uo -E{>4vA<,x`Ć[t(Ԟz0a/T`wå_N2cѫV/:Gp;J]<^ޘkN߽v-w_ vdt?(<闺idƱå#'gU{=-5F@*ƕ>aEeB'4#Vt?(?CDKeMuOϿѯ ˺Fl@;JLW Jm;]c-p]h[{p* rVa }On11Aqg -=XXOFa3c{O Eq)*_ sXeA9rP< V$jZD Dg-~>[òfBX3bgۉY)vqk5[SI-KKٲP+RZ'b&I 0yA>2S wQcX[Qhgu8C]E[V"kbmh G.>dSJ8t1Pƈf=-zNґ._ӬaDH2Jwz:DH.; -]bi,(g߄_`wj}׵kz2[ԩAic~Aޅ{$(-?>ݳVӘzJ ݰ*-t5WwU(ޗjoٷ:W6to`oۨ"1>z5kmN:I^A̻p~v8t^Ң1*hn1 ػb{{Si[>xwML.c Ln[?o>E -k6b2JVL~%vXe,t7jr>vVÒ9Փ5hk`dUD>1[K._+Nq#AcXk<"F -q %DG,%j˪vT*K!d-czt3УlXB -m[ Ll[V^`-(,.v:&p$JY ] 61#(0Ĕ VA$#&&>>șw{0Qjv5 flb(܉ -0<}-pn ]t"6;- Wb[[;-rQE.Qrup£dU^SC*c+,80\;  yF`ʗ"`*^"0#+dN"<\HYT H>1<e2 bӲ]9 ]Œ%1"b`}?:|D 6s; - oX.$l -| NY6,9cc%LTQ);"LU5rE"lt^lT 9;/`QXT{9qDDD ->Ve"%B6kL* -5^ ]D91T]B{n Kf6n,~t_O|G/z|El~*Y熸F(d@0f `KCذN9,=eM#T0rM0Z .WPrjq2J Ҏfݩ(JIw'DP)ֱn9, O0[a -mv΋b>i+]W036+5 kfY&}2#`x1Pa>{[X8nK=E"]ҒsV`6 -.{ P iK'l=$| -I/)י$"g|ScŮqORsy=;(c/a~Ÿi]NX{WY맣uwzJ^Q?GW.q{w|ҿ[zTXoOW}wj֧[%L]2U^Bmq{GgSU~K=tp]]}LkJ^[5* = -ZnFu]E~kOޥe e18WVqj}5G»* hq[g+j/tѽңwVYL[| ;W/{ыS}ӗw:ر/q{KcW_/^X~>u_U]}ַň.Fw8b{/}L]_eK=u˝ -,wS/tuu]YybGΣEtmqZ['vz1KgZA}L=sL~3-1c0/8ֿOVS -t-ubF]=nw{[=UzF/}W|WVBF<}7kb~W%KDw;lY?*]k=zoC^Rh8Wk#Zv\RYU.z=s[rfs䮬ֿ* XҳOF1[+CU^zsw>]FյЫ;:^-tF y/#Щbu8JZ^mc\Li4|Gj#o5z{ڟo{_.qsf]0.oegC.vé_jv8GF]=nu{S=tcƯPQ{w]fkt*#:^:v4za//dc_PꧾЧb߅aF^^lP}SQ?][vb;{`pNmKׯbBw8N3U.z=s[YO0P_pn>okҎ+lNOF1W_+GޥTEΥ[΍)t1^޸vƧmoѿ֯bFw8c1R/;\.c2fs䮤nܟ>bsWWFN⧨C꽁^WR]{}NbuG̽Qb<.thu>^^G:t:3 }>gv0y_SKoK:[ԏ?t=mQ7ƕHkq0=^޸vŸmoпׯc/ΏD=yԟ;\-c_N|ԜYq>p}+:qΧ^|E%ƋӣQO9`=WԳwF^oWVy<8ҡ y{}JB|>3ӌ}z:{]^#~(uluTmt0=>v\e }03/Uz q{es -]{z=0#ywlgŸkoƾ#^=|>1*X:;·A~a2ye˱{N!y+>vǩ[}>p uUH}\},CGʟ|ԾRz;*v:Fufc/?0ǝ^eϖ#YFp?RJ|zozՏyolgŸkw~= -;\Ǹ,\g{eCc_q{c,e-Y䮤yku? p>upt:#0PH0zv^ެh*>c1׽93f,?RЏ=9C^K?^[]Գ|iFiVmԻ};=ڇke{̽T:A}KW11yq2WԜZל̺[] W4rUF)s Ga衂Q -~!>k_/{=WoT/QЌQ⏧]}Vww?Ќ  1eaއk2'hv?Xz[.yg֝k^qb~Gscs\<՗יzԜf\-^[2,2% ˫[uqos./\*w"na8QROK^܎ -:13h?aU_byK.~S^k3<t2h2?^Yz{}~O(;ZcVqt.wwf~^e'W21l,n3/9_Rqs^k2U=|Gөv{1_ިz1OOπƃDž N|Կ_RfƷ,)u_◠|Y>-?rު{w8ˣu4;FC3/-zoV^c_cusO.intC{ݖ2ޅqs/9gU?R1sf~_e#\hƕ?>匷 eK={=nmus;jcVq;ž|?]9e3?Réy徜ӈWTwyqM]YbA.F2:]LcRRϚj^W3Q解>:Կ˼̈&g:s;;i3[}Nu/,eLeu5k?evt{K}1k?ooqG/Ԝf\,ezrex1>JN.w_1`~Kܳ~ʝSusY(zKGO/տԯvڏ_0#+Ǭ[˽_af}YշUKyY]R[~*쿹2isf=y eݽ#3?Zfqb51c4/^wcs]~c}6mwgaWt?=%ӥz=[ZsvzAey>c]ޮa}_af#e:"tL0+(&1n4LOt-x11q<~qF(zݶeť031q=AW*Srf9˴2ƭ~_U*14$Čn. DMj$\V#h4/UM|WDta+k =Z`5pP)` `+F `iLicH2DDDG;cnt٧'fޘ ?xG{pBSnbVm(anf0כtSa ->D ?NոCl].r.(nYQ*-f*[N˖NNb9cj\b|ͩxAf9o3( 1^ՊZJ,븆pG/Oݳ9Hj,~}c leV"+F#3bR5GTP<n0QLᢀȦU? bNm\n⬯6\6rea'ٞ?0jQY$ב=1i]_|r+ ~'-MۈPeCV*nlF_;AXPlbA$x0 -KOlgu`y|8fM~>s?J G_5"81?ON9kS700g<4U2/k-*NCGw9/( !=H9 -¬Fr؊)–*>_g=?6Q%83͞daހ^ŠԞ.Wt#HÕ %"053DK5"#dƱ0;H"#O'd@80Ymk9QaX-5»8Z,HʄegkYخlrxP81c -fƋ ^+>.`Y.@4JxAF! -smv8cABz򇁥:hɇ," 5"Q8a34Ƙ'114Ƙ]?>_9oKñ9cAlO6G`{E+CD6>*#o ee6CTv+_^3}S1F5NO֪x޶#ouf~-I -q\( Ƶj¬R!'xfR=oP)zٷ3?11_pƖĘ6Rrtbn gqκKvֹ˺W{ϞpC kPeЁǼ3mYaRIVx[.^# ULae-u K0NR5h@?}1s QIc*6f~i+E.3lFõ#kzn9 wNSjߺ5J\)U'KvpD7k짧33, Xƹz'QXBQrx-Z8\/vULb'\f^qMUk[Pa,i<ooj1-ea UҀ)M :5|[m"#d aW}ѣqSṞ1\gUnŸ%9gKK %# PTJVf? - ^* -XXURR5%{k!uԹ! - -cF'+|M -0%/vfA\ã_)݊ݴM\`V?QmwӅ7kNrΏ61x+Q^l]C\ky`cdf=n&nWY1mcKs,f,(+8:35$\vJDŽAF%F!8< Ɵ> ("5м0^X_tmM)@36iLi ǎ `2Vam#1Xđ9‘病0Yּ)2(0ŗFJ j')l`W1^d:֏ aZ|84ky(G1 ߵbf:hر` -֌6,]|f5og%W)L[ -D`X -(|# A.hpNћ`&[ [`SL&f@u(^߅+PM}.ߧˆ&W+x4lYa4ަ5yw.Di"=oکo6b̼#8|.(Fo*F`iJjeǽa${:u/8kb-;d4}v K\SPT<|U*i'§o nȽ): q$`8vwO[rTlVӱ>ݮڗom4\:V]{FWM!{&kSfUk5*\%/ª1W`[ݫ߿fSݘp(~6X6X[Z`8uĠm"[~!V{j=_(XYF5”\{}>ׄ.DZ#BXk//.UmyǤ "<]b۰po۲`f*Ջѩ8b X$w{wxO^0yyݾ_wF̣w] 2ųD,-tOհ +u2> ar@žڟAܹb؅AF ֿBiQ̺1[e>~fxyn.݊^T6: a_zh&fk@5ѽ lL۷h\6|!'ZaqT l`qZ'-w2;SDބ{bA*_X"ڢyV#wBy;39[ejBC ROg*t7wl\>|a5Vq )Eo۱ 3JHFEi(,QXi);dv|HKۊd RHہ'oUR^r ۊ+\D 4Nɶ~qHb`cqm;а5 .+@^)Q Q .F0BLi14lik@*֎bq B˺8hjПnƱnn4*p]Pl6ja+L\:,~,a7ecvTyoW|8>f s -D/M]p8\Km+~ͷc.-<EhI2]% D"N `"Ȼ!V×En y,q ⭦6~42Ղ]E챂ek -d6!#[i)CU]ƛlxu*fie^rǸ^y; `X˧]F+Pc'qM&fXQœAtG#ܾ{<#SGFyy ^H7rt!ockO-=B؍rfv):"Cku!w}n=>*_[j헗3 ?@5Ů˸+) -χy -H{߹nt)?Uf?_hζ bs.uS,چsvwOi -~y"XҘLtduE''zv3fJB/ժcj se3+N[1x}0U$x^8֓ \>wOyr -=4sF4xZx[Sܟq/95N1fMh,+3VpsSx} kAFz 8Zeظh1iYRwT4&눧xj}@-@V3 Gytѩ5"aiZ𱀿;rxH/.>t*_LCT]աݲl_MӇVߏ*-!X EF'{ldJR, -6G0Nq6`N! -5%V -5 -0Q#1"6>3M m+%mh/w BVUc]xHbűTIZrlkGP -^FIUcfA9x 빈 'e.7! Tݴw1rXo.;w_|6`f -%`,ˣ__:צ[ՑU -wrŰ-0hȣp"akG;% mQN`8/΋1`յ-+6!<&4MmbȪDhXBcᐞ>%Z+ GC,ǣ_[ʥ;|w~#g2WNSˣݻHfm&A0UJi-oz\(M^Y(eUkPw Y-"$`t*6d6U -Ƞm"#75VHOvfb8*\)U4k3N? O|vc֭eѭF0=X@+8,7073W-`d$N)E119iF -,nX̌t~,ֈpF]fs7*W%6NodJ~> {"7F Ss2W -%is^ԗON-k]#xUwƗ`b#R#uNKz:Sb 1-X9+n:hUB&Q Xi"D8Vא!1{l=O ,#W:tXPǸ׈0lw.x~Ku4׸۟gOcOcOFN`e㘞ߙo2_ӷ O׉TvAPL;YFcP^c`85bqs\l@Tx <#Ne8_"lڜOn?5{2<͉Rakpk XfhQT `& D`Fc4tum{ cl6-[< -껌~;LZhоb510V%f]cT0$R8 "qOcOkM~N_:nvZIF.UeJbx_я:e#E81B+DZޟ?8m۴$-1swBJ)*&,mcdzt`LK` cl< Eђ{"|cI֊MF-zݼV_:z`9K=Oy -$K`;`9uAP2$]lOeMXtH6Yoo9S}uMiY͋s}E7KnU)[yyx\̆ -[R+ДXnNOZ  6D %Jײqbp-Գ,WX ƘbVŒr!'twL=Ȑj bí…lex xV2on[`}]ҷf|V/|* 0 *$#밁mY<]'KE)EYZLgdJG֘l}Xپ㊨c@8`~[.Oblq[RuJ* hpaNPtTMa$fK*?.F] [*}-.Gh_]Q+M!1lAZ].voOraR4EO -KW;jQf>/ulZks\bU>Mlq!s?W;e\ᦚpd/bBCzlA-,q˃̙Wbdj0T *! -0*x`!aF  1X pu4cLiv#F9b-"pL8>.k@S3w`֢SX]4!u,oY'=K,{m}b1 }Wy8TJ5:!/ Vl{$8V_ -ej8qqcnDa-ebI1\dl,0>=N/bYW%`"c0oKcyufaү0#]ҷ݁HwQѭf"W۽RMTzXze2u++̙@qqdLߔiٮs;' $bk[&$:j&(\; ~o\4RR"o?JQR -V,1V`pC(ވ 8V,T4 @x+' mr,"W,XFD_ ;({ -5F1#cLi1#lFΛ4ۧݢ?ycqf"`l@yG5ǰF!`x-x*q77S$DK +` Le%2x>הq;H.#>qT]6O*Ż| V+ -̓Y -XtLasj!(n)XD.6^l\Q1v:$%*)\)hn[͈LsoeZlU,1q򸚦+"ZOlϞ3?(dEAnߵ:W0UX&Ztm?-V_3*ZXfx+ OqCDzrak[],~|*3S]RF.veSTعR7-~4.d!?tŇ& -XyXƒ/[BU l vrP .V*x}?7]໵X'qv]zr;,6gfa^& {g>x_з'Ɏa8fU_M ց&tmv8uʪD1s+3(@KvmKLbدaYsY.F4;c]T-&ɘ}P9slVj.D-]ڻ뽹?ں ܝ,|,bZvfsY0Rdo^#%bmc7;ݻO칧38}pv`GeU*5qӇFk?{b} ]Y־.vmZrZ.v{x0Qo+RYT@2Xw@0utܹdܯM|4djSxY}4n)>t.۟ktIeqrDžmg nN8˶O/gX+{\yw|T0N'W2=*g闉4|\iˠuuq*g֏mpedƙSc2pszWg`EsyO^7RrGc -QU'.&?8ak4}l TqUwF^3V"g0Xm5^[jb-(Ŝ[Ŋ]ř(eM#$qvbⅩcmYo -H*ic@;0hm uxꮄLٗ˚VNd\%iŸ<̀r DW(lHJ Sj g44p]fau\ k,MCAġ5~ZVYwk]DX6l0 N*)G,*\-mH6M@SY1ɌpU0;'l{cLicr7161 X9ĀM  -%,F4'O6i1|>ϙǞv Bpd"#y2P#p$&&[^lfsvCd-Rpbqwp. f & -Df#0XqRr 18k8Mv5%"]U˴qJ͎8Pne4 -|Xlu. 6ڦb6ȭ_>$LX8dVw1<[kb32kn&إf^fd:V1A8ݗT@~o -bšUE2:ۗo r]xb0c~Wlu[ -)T&R@ݽ.i)2lBVwA -jԗUs#N-պ];1Xr{A) [U a*{ڔD]Ѽ+Ej.fteB A?G}~~e,F;Z1QeǺ];[]n2yJczۊY{ں=]ͱ,cz4gLWbn \4vt̶&1^(ػ; -d#SF.U]_ƩFuf#jFt+wJ{zP9n=5i$uaöjSwG̶eެ 4'WO85/@v -u-O0euwC.úK[`U|C;b`0#@ixeqtkC]h{ݭ#{gCP*PAqTk1B5P·GUGk-k9r#[l~ģE -( m\^c%|'rKNwHǂ2ޮ/vVf>eѣTAZkk^]+K'ٙ 92"vwm™JQ˦%wZ]O "ZMgt2E"X -!q+yOw.buLMi v;wLwF+2 `G.|MTZFBp֕qctP}adS2BV*p&82^Q& 4K3YZIh+8iRbi2nU݊^ mmEYw+#5Yu9Kp)+TTE65+Iźx.,a)*28X'b8뮾nV̎OpX@\kqoc\kqq|?=Fii14ƞΛt٦4Ƙl{ZcO%coc{}q'f$kiJ>$%'\kk,k_5Ƹ\O#y{G~Ů75ƻ5q?c\k]?ϟ&q5ƿjF'Ȍiq?lf&1>_f\ N%Ɉ,maXdmc:1V :F8sWTc^92C8߱}v>94E8X UcMF9cF9b,'/8X'1Na3*^ qI!1AQqr"2a B3R`b0P#@psS4Cc°?W'd97Nx^6rW/KjV1TpwJ{ N%)a򣺣 O`_âO[1K7sѿY*j9(;Ԟy-0O\S-+[{--: -}J|\.TvROfSIG929VTT-4x"+7{SRvžf>Ts+}T;/1>]xԽHs~jRW[)y;3ܓ=/2,Mz"zՇ[qQ"=J|lK{s#ⵏbWZ]/ʜ'OE'2sv֩;'h!M͞+9mJ乿WUGhE~Ԋ\Ug͚)QMd(CʎRf<Њ0FWYj[N;ՉR6&PVd;D*Xl]hq^շéFVՕ*>ݐS_[Тݳ#%2WEKEXXhG$`9FKxVkv^hy5[ GTd#Å&I֊0CQ.v/s LxbE:ȌӸd>n3ԈΤ<,g/aC3Bvr>^_/3g IVHq /;B%1Svl)s1h2S#!Ŕ~#$1ì11)Cc'7{'wTv -ey*W)MmMTv)Z"~+֢(Y^n#sA=Y[.o[Z!?vտd=L YI|;_y_y ^["q[ODMŔ:;o{b/_ -۬_ w(+)zT7;'XJZYzm_\ -ܬ -7͇lkا܊Uٓ>2)Vq%rH>df-E'QE~CQORnQ4Q|=E)SP/rեpߏErRV㶽)ԶqCWc)ڭ>oU}Bvo뢧OrJ|pVCZF2(}Q#(eR5S(t*r|fzT;7)SKz.^r^h4''BqWE"VvY&VdЕX7¿ͅ^g6c?/21Q^X7wd8~$95}GgY[цZ۹Fw3GhٸD)-rn_ uF;$܉6XΊK"qܙ^vQv_GGo^Rډ]| %$j_xO}FbpވҖcV:V:)0Jk%<e5ELEn?3qddI;AGKdtCx(r ^fo#wk\E(*h(j?g*X--%7SGh\.TTdHȊ -QlN梽)kd͚)W_.(R?-)ZhvPbNڟ#oS0ow) o\'4[dehC2n_)f1׳%*/ -ܬNx ay?_b'ХV:BKW -ffbK0)-V*]ySQ.?LWbȩ?5zCʾ -IqDU(s$8iRQ;пͅ^\E giC#Hmr| -KS(*S{C2)%[1G⠗1ڸ)ru>"*r|z[̭cDx"Cb*$M%7-za;ebVQԴvk)2]Y-d0#8l"XR+j--&և OrFbZW,T"M*47v;kd[-z2+YI5S#mfvҭ|&/huܧDZ$$7Wy -ԇ1Ԏţ BKRyg٢i- -[EFWe3z2 -rG=z8ݵؼG=š9KPobt'a+-D)^L'zQBN>1_+)+h;=_'.bhG&Eks:Ǚ;.<"~R"?V6ôRkoۈb0˙ڒ/buepeGh}HʴRmG;EOu?lV.,dv -Z:U7#k((um)}<J?OIqEx -S-#J&GϨЫFZ+aR rȣyYOE:{?=LKyoE~tCZc9/G(حŝ\ -ƵB[吸OsEܫAn;wKᐩ.#ȬE.UįBT/qVFAL"kz0xwr-aO}ӼN'u dтSXN`sI7;Կ%| -KS)=uK^نLjҡW -[/yzHDkJRݰCgk;RޡL%%=1íe|/ҷڙS-ONzE:0=cH܎&c:qM0_1ElDҤ-$aMx٢vc;H:akcِr7zhPr͗lCl4թdsږ1='9RSۨ_8\K JWz{ݥ -+fE0kJY4(Zpcɯ.FaOv0rMV{+Kx٦ceצgv޶F;J~'/^$jQGz,d8^\f4թ4 %CCµQWcmY&=%CB]LW*WR^#E=g%-ڬN!Ky7-(,&?A)#Tm9S2XdQ9dC-U!Q'{^Hй悔ݑw<_D6PŧZ_1ӶV* n[r0o)'N׌uTaB^f( C;g)( o"Qތ-0vUW=t(iJ^=d$N;BK)/8Oq[Qy]QSGqtN_#_4Z̸{_jq?jCZ#QEn}GV/6řhyH_cRjZwVkYIF+'S۔9Jڌ-Gh)mȉ+sc~=eN.JZ˩G°+zI;7+Hc_(Mlv)8jWN~lQvW;ԗE8+'QRUfҊ$ގS_켈++9TRU;NHvmbO;j+el+)Sgfǘr&Z'JE5O;~+DvܪQ^g#xSÇnL{+a#D8+*s9 k!Im#,.Z;R/@R[;"3L [{" -Vvx&"Gj:;'t# -G7<0/8\ca/8x#{ëi9Knm+*} Bp -t\ڳBűL=fyn8IxDȽ(/EZHb B5~bh7Bh~ -n9|%|%*9mz#aQe5}ȼ^9C*YTI"o -Rm1_+H%imR,묷S~r~4RqvBVm1$J6aYGтJJ['M^,T!9U-zaӞ-–kS%NY;F_GjF[q^T*,-d^ُ1ft4y4Z+dJإs"1(>\Y*xN1k(ir̛ >*sYOc -/nGg8~̌M# -zsvDgug9= RIxt4}h/ `E3ұl5?Ii5PW㉴?i1K\4&w5sȌ\q vGƭJMfb{:xvd܊y/)?Jۨ=3_#}ؔ^VM2=t#F=ۺCd\)fG./BR<4^+Ec0ݦVزtҖIf7h_=IJZ#9,֊W^e/4r%vgt*gg 2 %.,R+\YO <+r2|)Џ]5ݾQC#?a?RQhd=8Sp{ -mg`:)68On:ŋaR޻G{'QkxNQY=W0IdkbUIˣ: ;vĜg󓃅a)NOYF54WkITS'+nwщ3W-Vxe_!-zp_28ixGmcuKZj告FwfxKF)k,.y-W6AV'm _es -ߧ=w5zirG[mOaV*2g~6t-9]ŎW16 <_G٢񍞜N<o{hik~1ϱŽyX8jqkӛFOMe%}S)'3Ϧg^gbvu GQVKMLvi :N6gFNeQM-/ KQhbl"iF٢~gMyL1rէ >c´p5W-E-9WS>`~"FZ|M#̕඘c9ѾD+SW<|u./ >?i1M\tZ{hx-f,r̦iJ¦]ZRʟ*p -D䝠M-iu)U'-j -WUj'QoD*ȣ*<$|ث&^|{,~8cdXxY -Oi/# V"ڙ)%V h&uh^.u>?Ə :*Rvrފ -081О-"fTbOiySGhTש;}=+8H;J#PDȩܧ}_DR!vwY)J3* R[bWOІ eX?™#蒎BP#*T]#'SQ:w75K<KJŪؔm!F_*&T)BFe"qĵϱy>V/Rm4zvbi7rէUcYg-\HӔseDKEN齤i>nV}E;g,6+K\ʜ'}UQҖY/9pcŭdV:YXbE=9YSԃ֣]֕VyPͅLNEZ벱x]Zq%^dC=Mܫʫ#:iX0]JW<4=u)zQDjeo TeNaFRɿٕ9YUշP[Mz~nV}O>b0ֳ!mi>(QB;ҕJȟR> VZ{URZHʊ -JP}vu1(+- JO5,밢M'ą4 *9\]E?ӧ+p [E< EdG}T 2"*'OqBQyU驪B+? FHm>}ʜ oJnԧʊ܌\_J+NL R;1Jv4d,*b;OpiiB;U&Wvu~w)rmNd-̚ifclM|?2Zv/y;+DI_#{r.Z!)Os={%Y֢*s:\qWs9+ݾcy_ -ԑhcU#zlQEB fT ^6%gcS#~Q5<&L)2hc e8ղ*^iP&뵒.N - -axn*z'E: ~b١ -vܣZ™BW2R/bhN![tWN*Nnգإ[$*z#NSXP+ נeN?a,v6[4 j-!:Yl&hFԉUZޟ)K{Xqӝo#"*DaKUŝ/=V%*Mk~+Ghx^EO㝊wh" ^\՝޻lkʕKs:y - fUZيPFs,uM\O{2]jk;9RK!v?5tyKEY k3(Z:-ba{20_J~*|V["TN9gQNXs#%-;>JyӲzia!Nֹ7X}Re֜'7_D5}71AsybO"Kj(C9.G|U}N2w55;BoT搒٢Bw-jKVB%M/>wZ9?,:'LQoj&ŝ8=rzh#{mIjµ?ab -U)j)ɾZ~7J%zoo#]K(t+rIl -[aq)Y_׊^U)#wԊu#/HODBPԊ4o4JꐝY -3Zծ5v܇RzY^bjnxxȥ^XٝHKjfZeE]9HJq4vWbxvZiu;et睋EY+}^y?GW;}Qw5|, ;ٶN_43ӖdqZ0)AZ>b*+qevS~oTv-en䍊TMlţ~^cRGh0&/~rTBK!Էiĥʎݎ9# -wJ\vbnv]"tW;w.,.݌8|=VQ2xWM`6U3Agh9j;?q_#I/[b&V Oq*EhNY|v~+r2Jkn hvQ'hVU^M6ys;Tk'^yV9 =*W>(SR~Z)q)eפʫ -dT>[̵?”wіbQڟQu6N{9YXS*čdUǕܑJ흅h휄x|*͟bpj(NK(1՞&S)(MXte?! -rGif,0Ed8k 7g215|%X88崍j)Єg):U eZ쩍xd(E E>b";ULRhU T^EQsÏ;Rۀ_%ŕEDVRxvyWIoEXճRyOxvCˢvY+"W'-QȊ^v]EjٙRRy"?qhT_}RKij;b*lIon)asb!Gk%'!ԌJOb1ZّK*1Iغէ/į%A)B~S}%;L+ޣìHSB(l,Ա^;`F9M,_"0TGz#KnxIG=E'-r8&+!QEgʨeZ*N~i;o<_*U-e&is} 7/K*w?ЇA-Xq̭OiUӣ|cOwIC/4s7z# -5Tjs~&;v+BʉRz9%E]xӻz+M)Mk;=HN;;A;l‡'[΅{-d]yaliYNyE)uYJ+d0Y{cu8J2`QC(r[GQH?!,=J6F)}}mT6}mdN+rK捈z禝eXPd4gr]%,6ݖ3)劣;ak wZ{o=hP2Ql/4tU^GgGm:Q׷q׾gn=΅n'ċ[/"QەQE-m_. r`[,v>z|^Zޭd*E9SsWgN2;G)pLmn%ES%,_ 3K)>2.ܖ)_6CnR5QdšW½zeiIqE>d*B_|ʤ ;hVoY y蕵GGOR'V{yVqBr܉Ҿ_գԏ IqEvG+dU%{Qҗ\US)I[T.,ܛPXoWE'y=FkaQmYHQ髊p!f*BY9fuN* +7ω(=LȝJHRE+[Zb^T_e.^)zSOT͚Y](Ӌ+=r)s%v3+Q{*#S&Fmj*oUӠ˝eDeLy<aە2_ :F7Q8Lx6bFQ7rz>QRJ.ţ,Q~1Eeb -Wċ6\ ldp D\v0bb tYydžhdz1KF V:BBV={mr'U't:e'fxk]ݕR}wJrm01Mm/fWR׋܍ZZ22޴Ok:xZ,(,bRvbįRWJQVXXeWeUSISB3w;VVs_Xa ".ֺ*_2#=䠡qo'6c% {EJ -$Qvn+A']<ƎМjxy[$ -4%Fm̫v_\޷!gWюn#hAbJlK&7VwuG\(u:h'|)m QŻD"hu\m;l6~N;W ѓzَ/ÿfȹFHOјUg9J6"vxK.,WZ(gE^Ne -үzIɬkR%RY=3FMdW)*j]^GDd /\_OV2w;FYv|Wh? {̼V⒎1꩷-lxP&BYeaU1 u#}ţ]zz7"*+pgig(r+%ͧrONQݑ6vBnjkqdS"gy)ΟCUz{Rɕlr8cvIK^"˴fQJ~(feU."(-E8ZF:/XW[n$ێYEww0wvȯХ=؏O> -!|v6AwF˩Vb'%O-:{8OR<_lK 6W\埑(ʊҎ2()U`=[OTE87vu;DjC'.;2XuؓkWQ̼"ZqOxW-8=9Eա+8%(G9h~ ,KqZo Bh5-Zlb_u)r/9[8'%#d+XdRF+bڊmEYI_N)[ֳOR E EfJlŁ[5!ҩ;?O~gUq;z#*t:еŵHMXUo/2ɤ}Q4}?Rњ|4k*ifJ{5UvXW^ٲUgz?:՘ÜlJMeNc<[e8;5k>*TrdT(:Sr1N[qXX\pÕ+#|ggQ <8gZXNi٥6yT)x9FRշLtSO,T\j^hi/w鐟g+aIjKGuqet}N1ʒx}?:!DA},_1)'Gc'MG dPwXrvKRCHR+!SkdJZ_Ua(TV0)KRW8/V~6Emz-X;7}'iyVeNbcEnQ RVKvYz% -zE9pnly3ElLZ(u(K8ث^ڴ1Äwuaqd؊3-掞t#üm\KiN/Zoԥʎ|*RfSVTܨ^8|)Fs bjٕY>ba;^Tr8_WJeB2X9G)V"&3K;7.k^ܣSP՘ik$S؊}&v8S -֖EHHS-eP|De+/͒%,K^H|0ɰ5y̾6/r،mvL݈}E.DTVK硒L!*Y)l>%.Tu4bKŸ޽?4M?&^]s)(\Xx"aVs9VޤecQ;D̫٪kEk{GgU/I-h6r85F߱[W)==cYNIB2RvM'y,'h{/-w?V.( =b2kM>b/\1NYlGw:M=-}idJ)xo^F:)PSZkԣʎ'cz\O/(R[> -)>bcU7lrк^Y\PsK"ux䓇*1ҰD)R4ʘlԧD%xb*eu_qͽOS'W8]a -,NSķe_twccoӍҲ(A-B dXJvW̏ 5'% -5 ޻gí;VLWڊ+zW)mkw**|Dvj{DlYdyZ6/R\(Xd|v{L'kvq_YRX|9Ĕ^O1T5=m _P?Cs~$z>?h#W_쳊ыf,Z;Kĵ2ud~s}O&ſ_B_y1+y2Vrrgv^uN7ǬrY^giqiM&^.{{&b_K)8|RHI>!F3[^ט+4]SXQ'~^?C }[} ^uU*weTOq;en=JU:sÖSmew9Uvi)Z؃wFBF$Fiٮ:qMfxkS/&i\Um0y(!d'oH,SZ{:W)G'%o왲ziY -Qrz,aınF(צIf)E/в{FF<058B & 霣qOn! -RBf:IP#|7x=H)݌rYgE*(J{J9c; Sn 1+j;%YNOlVUK'=RM'c\"^}߷fC:75]C;hJtaGuW^8qgG#[wn^!2;Z&##Sj1IDdw2y3|̭ y#*ʣ#*zRqClekorњoMNTvCQArI-)p*Ԯ2!r)WR{dl'^1I4{oўv,:^'""IQ^!EN>gs;=/MnVK2ՋN;+ Vڬ(-M_)%(Gq[̴ޤxtV;8M-ZqM V.TC&)SMJ7y,}29xjnE&*C/S>y&j+pD9\o{e^JT"6-F9K4NNԧ,SڴTcYؕ#g9J4KԫQGX?sí; Ivfmf<9VrRRÜȴ3Ց -ܧL{5/-'ⷦF1PTˏ!GM[/Dv;i5xc>qjM$*]B{b*! -:XsL ;Ztu -ܤ)WNn1Ju~:Zάlyȩ%\s/юj+sh\X_ĥeNr.?W(#^؍EdnM76R;#ekП9Cr"F61̈uWO/dehɻg9'8H# |;شRzʔ7/4u/R|H";垗%b*)}#J۫B,5EjD{<`[9\^9<!>([*%NS8ܞ7e=:{2|$Vק(4O{$j'U%8򱥭fOYcrKc)ׁ ҝ\هm;K޿rQe7/(-4-Vl>™B4e^>iQ_A_fZ*Ekqd5(~Q|yIŴbМNqZj#m+Y.b̿Q%Nim)R>s21[ -❤أݲu "jEd=KELhqQMoQRV:rQzIXsj1'BR뀒ԴJ̪; -"uod-q{͢Id7Y -R7/$I6o^KJٖ>?'(db$wFMjJZz$'ļ)j%xf,px.M -+Rey`NTNFI|B q=5[2J2DߩR|Qq۰j;YqY=u'EʹͳosA/VeݷqF97{ -c3VI^;ߔb rq$עª+<߱uz0s fQІ}5-:vvZ0eaU^<oAxTBR~$dqN$*&*V;m+p~!IXiiB_ӆ>gwԞJtޯQӟ$'fS12.Z;u;QYNj(j,o*^9_uFߡYzH:su -=Q[#UCm!B:C{G-'.(wn*/vSNѾ+pyR}&Յ5vFqf%}gv^rx)։ZJm5!'{[!RKp ^x -V| q0?4~ -Qb)[*q#5ܣ (浔lZ8k2(: -~/[9t#Őq3rsu4}U 04aS%QdԦg_SX7uȊWrr~{Zea-mITʼnGS~_ۭpqZ3jv3!S"JYZZ\d'aRRIx".%8Yk1sGCܲ(0X*me> | oBXTN;q!|YYsַuiޱdwƶ܉CRrhG"Zb#Ժrr;=MEK%SȔKi;HW -1eb|t'-N܇=} zn⮉){?M7۰)МYKE("2ٱS~'mxRlF_ԫʽ -;:E7F?tO`Iw(m$E!m*z܄JdK2ޤY"*a6d[E$ě~Yjz9UV#*sEģ)=H0B?~=D백XIjKNR|tƿCeFCIr0Qs&SBtlDS،6I?YO.&}i*T؏s)맯H_TeNy GTO\T*0٬P[5tSdxq#E_ܗAz'ԏ1KՓGjɸٔtG,b4ֶ%ha^R܎>>,)QEu2=G{-S*qmm,b/ - u*E?[,NޑQq&St'X)GnĹLaw:SZ%x~M2˘c_t'XW8XŋfjԊrKVIJJ{WԳ&֬:~D0kQ& Q_= LųY&1)<;2٨sK!Zb(`-wZ~ܫuArj&j)? 77 -'Iz«(1F1w>*p%KRJtE$)iY9J6FQO;| +p31bm5s`7ODκxX{&O'1DnQA.u,5MI.#VDǴq_y ▉@^uqtn<:#$Z;>-s-cE aњfQ-c(%tќVS<1HrpVk (Oy_[3yij#O4?y\GyyX6v+d8of -{IDj'R(y2Q?q -<Rv`2} B,YdC"Icf$SE)BrQI~5;lD/NkMRNԮU\>x-#Su e}V;JZKП],XUTKШ)>ac+p>%NR}:gg$ -޿܊R E>1*e^[}Q5Yc?Bi -"xVa”^CC%~yJH?T"RY -Dc-c>S*|J|.o2[S%utn+.?屗Z}Ļ;IfRa[kvVt';"qSJ|Jqvby~s(pweOS3-:Dȭ?/o+Aq!Q"REE>xkLʜЄ=q>O;H%'d`Oi>8cK)yYQz!/D[Q_ -& v/RǓ'fId#ħf.B /Dy| hhԊl).(R*rFKfTZGʽ -]E-6S1J?yK)}B{bX٢q[QQZⷙ_Q1f<_l|RJ3Z?PD*}ŒQYw7db"Ubs<;x%5sE - R| -RDWX6 t]YQ׾d*_$J;qū,K=I#$I##iM/Oo[f$<߰1IŢ،woزgV<2OFR_ڜV}(ķ -iht&IY8.ByoXHi;O*wy)f'w(־YmJOa)7m?4aZ^LZV(CR#7n#ymC,\Ml#+E9>+Kxވ۰K#-Q[Iޢ&KJ+3wHzي&*Qi^4{?0#z4U&73R/^T-0FSq!D4Ռ~9'du8 C{OQÙg鳑tKKTYcQp'-؜o - -{˙QG{D-lֽ zI ⶡN=LT'- kRDCP#aKudr)Cˬs6LR mS V+m-w#'r\$.:aĎ+`\JG_1.3[x.a·W!*G 3|HDOCK<1COy&6ه֥ԥʅm'VG"Q,YQ)Kr(֞héD #N;F\uҭFاo=Z'¼z.$Ao.u*G?\⥿1ITO$NkQjSyO*.HGY.%.eTq":eF(1DtZK!$:K[#ħ>1ihENVZ& D[uy\ -# QhR&xYvgO̫Чԏ}Hοƈs{oE_EB[LMj⓻!(y"~ [*z-$q)K%NR\>0BK1K| -Q'_xHˢ/ԡ9Is`G?6adX(;I_BpۥDGwNEg >%z"["&>cTz݉q-2')hie*l˨%QF~5ufyq$_4Z%/ZJ+k#Qei.Aq$ћSpV*t!8G'f9=Z>]IP2,_+&8K}rKIz?<.Q{%Џ>%^VTZ:g\JIS5A_"]Hh1Ȩ֫S8=ODO D=;tҠ4O;F>:[*t)*rG>r.S"}Hkf-v/+tBк[FQ6ݗV+mmlpXcS_0MŮaMj2I![1;jxScѼ.b¯Z:E>,)>>=QGxa$z/i\Q"<9Jܬ|EJZu. ՈEw*}Z)KU}N#+gr)%R%ćQx"hp)R";Bz>I_6S)M,Eb)S Nϋj\$&hs3?m2dy\ -WbU(ەd("q QBY1IjbY-ҎnVV^*ljۙ G-['V/k}jjceQ/NqzB - -Rr{ s-k3_*696`[$S(d"F\$0ŵSSY,=p݄R6ݢPz5+zEmO{K;El*;|8|^ّĬcwF2zQ'U;ml/YQW#r+5S(EXz Ut'W}bԇWF)C1$/%^Tkdf~sVzQӗ/_-RJ9<-٢9OBhZ,[JN%ݽzUhyrDy;^R'M;\&6k-]ik+u JTJz)eGMxu$Ԟ}OMjt<ȃ7Da'7vmH{BWCdʊSEVdRw RJ;6lI+Y J gk nTt1d0)=|HRjr#?)O%s}Y̍IYcEG;o'(J؆)g}į:gͼIj#v{#ȭ_>F=[ 'ZwJ7vޤzS(wpdZiJl**.ɊQwX(?Ȏʼ )`;$z$%/rd:EVoR'ڪm򝮔JIѩyq:W%YVvN)O&.Og]^?WڪzJd{=?Y -kb(׷ kQW,b*sQp[JyPCו«@R5lĩ/{IK-)Fk4ѕn(Ֆ-VQׯU5iP^b4vaV2('$e/bղ8s4wR`סW|]Е~cn.C =dÄNNTg{骷ܿݑI_$U*SRMzj4_8~燰ϫrTeYb-df/c;QM*ZT\B\s+Q.n<9ʇitgUJXf0Fڦ/zpԅ1VZh/B#Ծ.&q0)/CMb[t4uF wEaqb**Cj#"?xz?Ԧħ|4F :8ݧD!Oh 販O$)GS)w1T&*Kc#PvxJ}*5/!ҫ 5Q(wi [c]aݴXWeN21kн9_K- -z]jhIڕ7m/FGUFLEY}ȝzwaU=IlKYgmsksÕALNtjqIiCa_w!xej J>5w -Nj7Eov_yآ):GˮdQ_fo| b&vY"q\irg,TOf,8J2Yu;U/RS4e -+tI7/{)^I,'}%4Uov\KZyoe(vĪveW#foƵz{'Eys$_5o,;i}הS55Xe;;W+jgGitNKLQZ8v؍ -;[YݮFax0 MtT!ӓv{thj$1T|QX6gBGE▴xrWh?Ќ"iUW2I#:GSX7l0-ޘUUqURbuPwO v̿sN4hw5̂k5̕s!~bcO#XK A8z}Kwa9e=-*vDr5 -J)}5Ɣ~r$y.yx"#"Tu -P_EnVJ-{V7+ZFK<1Vߊ:_3':VR*4JЦ1GW -UmC|~F &}ƒųQWV3^}eHĴ56a+t.T}Kث -^VUq׈޶7qmPQ^cWx1ožs'P*SG,<V9j 1%dB[%="|bk^QWi/A~Ekd`QB̜b)9j29eՏ'擹^N1vm)qw{^}IFoGQ=*# ٨-7U7Ɯo1(gX#k"S|9I)xdcGHpv( wÙtf}Q#)o#JZ' O%iHX#5_u*xߧC%Щ~0㐢zqSZ\()SWy}:*Ì{1+y<;Ld_Fu<#&vrMz0[2njkO;}bӷĔ`9n/o~T޽i)+]3)v3}ü%$@_ܜܷ byw#JEQKeHu?hB|MzO -IlU*t)r?y+X裡]mu[jvbO -vCOu)./GUu]L1a x/ -QS -YSHG#tߕB)%颧 -vki;hnvN򢵅V̌%XQ[?BwY~pExb>?xIޜRvE5|֜.YRZ^O#'(_7*[LJj%cޘɟ+2wZV8XuwJ6iw $?b=ػG''嶙awR~1kz!|,qڊ_L|2Ȍw-S;r{7]1>a9ŸV7w{boAIjԏ 7WҮݻ"ј=7e^s}qZ# -~]c -zVU.==UiTQSqQ%x8 lKJ;tӶ9ze)Oh-QJ٘ir'_Pjb }5 uib)`WZV%{2Y쵙Z*^lҧ-ڋ4a)JzUKx"m-[l9XQeB=)Z1Z%-&\(++Ʀ.RۦP[EͼM´<:>$xa䣛xqE+-.fxoI]~#&⮴z8#ӍxEhW-lQI##<2LXkzc/)X/]aNR&7k - ] O|F:)p:^VOʾS)(&ŕziW|m=W*C ;G)̇ 6eZ.T}qSb ()pzNVMz"ֻ#U]ߘxhqڬ_R(E^VUJOaR^ˉosQQԈ/R #cby|z1O㽢v9|>hi$NzD8ĝXLT?Bxfe4Ue^e*m8Q7o`b[Kt+mbkR~ׇ1OJoSGu -94$ZtoIG+,+Čդ棖DU^{ܬN+h׷q9C[ WQlTܠB1<^gCKZ/g~_R漇f٢<8Q>28뢧\-O -{J&oƶXj|I:Y -kS_ -Vͽ>r]62\> A-ezQ?lG G z6/ B4z~ Rwo#ooj|%{SdJG;zGG{O<$ױJWd);Dn/OQZK[d&qRrы༵1Yx|Fjbfy -E<1HMh4 -"^MZى78bܶ9Ry>i|Eg})GYL'"i^-e-z?T`brȔiEӿJ]=-)j"鼌(Gf8oŏۡ-%mTF*iv蔷#q)ƚzE;$/K{H'{-0Q1S)锧=tCt?AEl8r.,ٹZ0a^JWwb,Eh{fxg%)z#_,g^alQɊ20u3Ú%RZ^,igOdL,ԿFcy -kSk;o;B[։M> -qN^gXucFŧRYp$eZ--mL7TahVy[)e#cwyݙJIjZ/b1èriu^fpchi,G?9¬Z+;{>aCHS6kn,hqzLql,] -|]/I>.:hD8HqDs-H:x"BQ(l#DtLħEO1O%| -IqKE1C9EC6Np!E2Chq'ܑ hDy:ap!4L\ "GJ П]J}=R -88Je^4t**)eOrq DeN%>*}E^y|1.WKkE1CyE^Q%ćtS!Ip*u$G]=S"z R}R -8!hS*!W*)E<q D| -J|4TQb*!ė|J -IqOE>STd92:*}Dyp!KC.N뢗O}\BdH 'M%ė|HGB#'ЏS'GRӢ -xxS'ħUOhChR=G.QS#SD8O>~#㢘}H(9Y$S$.4S)Ohq%B9?OREDOM.R*W*^9\1>+b GDGEN8KhR/h}M/}\L\HGЗ| q%uCG>S%GR$@TR룡[WOZ(>yJ|NCPZ)p*sVJWb*!ğ\I"!8yY}W!ğ\DSS)yNN:鞊}HDL\J|#K>O:|'ܗ8Iu.b%2V2B] -WKE܏)OCC"<\ -̥ʴT|J}sQ%N -下L\Hp!̊(]~rJ qLE2.ݣt'ꊄz^)u"pK:O.#Oܖ!ԟOs\E>]2$s- -e^= - - -+xZ(>!؇R)p*sVWĦQ%NT+\H@2(*tUq*p%E> R'y_}Z)O.>t.OG)'R]2%2%2E܏p"I \ -.UOy>%N -BC$OWr+ K!ST%E> D -Db!K* /D8Υ.8\ d>N$8"_rz:zR+%DOOE. GL -en= - )u2G!\4KC2=4RTe.U+)u)WxC$8K* >()*HOy_R뢛G!q)}>8h\Y>TC!ܞ|m#ŕyOQ>'P}PE. C)#hS+t)uиp)-~r< -|ECTGp*s2._Iq)^*q!U@|Q+=%Ĩ=")172G]HE]J]=p| %ďSK>#>C6ʼxD9GGK*|qZ([Knn.Ch})#ˡHu*颟)S;GQUS[DxC#U@|HhФT:Licn*[ARD,K\ q&O.N$z飡:h7ʤxD(pKTSC\[K~n.ChS.]JHT .~%.Hq*Qs"qDq)}>^tVE.B)DS;0TO}#)p!p%ę>$8\ qZ:$to!ԩ܏}(բ|E)p*SC\ -= _WGЇ2r< -|H7TGE.TTS\ĥԏ+t%ĨUdC(!ħ8#SUPĤS*pc/aP]:{ԏBJ\p# q&O%T."\VI4gfC*ϥO{ćS!̴vrVR\tt++t(Vϻ/r -|EˡHu*t#**s2<4v%~%$EzW.%BqD8DxST.y|*t)u* pLyp&T!E)usW.8GB\JTGKGMJqgҊVP}P\=SUGL2@]t.~zs->".?NtR*q#Evv%.)*T*2 CSCP\V -hG#ĢH8/%C=R"<䟡W?B< -|颡>fOSK$}:?ϼCP}( hQ}Q}SH2)Z;9K*.~| -|Gg\=| -|Hԩ؆\NfGR"RJ!̈p!̅ā>GʴTRU!̴G%Sb| :D9p*)2:hO&TGKt#\^8ENԡdSCh. Чp++}%BE܇<%m)*颗)SZ+"QGĪUdCO8O:)r:2OhKQ T࿻C}uGGsTṘEB|Y"3!УʉqZ)ΚST!QSu(}>(>)*E>9R -lH\ - -ICZ;?Or -|ytKC*颗)Sᢳ;GQFU* -|\ -|Ht!SZ*t)peRGGDT࿻C>_R]'ĥʎhK23(ET>+B#?>N8ENԣ褖墟(2D\ -_CЇ2ٸS.T#E.R3)Q"R.%R!̈p)#S@}$y|EN>RDxPA:vĩ{}tQ}Q8p)!P3&Te(ȇh\x2G}}ʄ8E^ԣs"eK*>:#ЯСD9CO.J}JK9Kh}gh^)u*.%R2ȃŸ;ebo´TĬSj7'u\]hRJl,rENݩ*r\QGO̿z!OSK ̇ER\JNfQGйHeSyOS,Ob hQ}P<^$9KeRVSp+WPKDb-!_" hRyXԇFk;39;%DYcj:{=_u*_s(K((Z!;Չ8wj"MOFS%w{xrOM܅$Q Ρܘ]w>s;Cq(o# y*r؍lIlugі:l0qGf)Zb8-+ϲihx| ~--µ>ĥ+%QITҔro|ݴ[ #<`n]V/(a}O"tj&}BZ,4m|jsIEZldw_Sކ>ٟk -HŎca -|*'UVF.+kݧx iFo20^]HSzBbJz]*^aөDaOŎ[^Ū#4؝lk&Ȥlvb^VCqҩaڗ2~-*p%^VTbOEفO=,lj,FM~|EvRyo -KZC -o²#,Y!QjЏFU}E -Hmjqj~Y%q4&`JFKi*t#'ch1K 'Z_rmʿ}*)E;2}u(>%'厈_|J/͑͞aBܶr)A+jDy\ȇhsdcآ}ЫSW!Jdc[a[’v-9d+ny;'<ћ3Z3vӉfNO'\1MZK¼R2Cq9X:{(  -4Mz ҡO[5刢yI,ݯ$D{/b+%RVc)_3QMWq[%垭4nËJ{}މzBMp*^yQ^ף-9<b]+dOS~U%厭//PQKQJQ{Lw^fJOb*Ֆ܇Q5fASE^)Rcy\N[ђRvbc™G2>-KډBQϡQ_]{%;+8.)j#L5Z[(tK -VD,֡<>>O܂;5I(Ya+O1m,YI\B=LM 9گռO!SߧQ4d5*U-1Ni8VeRN(^1$ThX-搤ƅhpUd=%vWe'-qGz9W -^mbZJ2` LtyXLđgY~Y)<%UZiu<.^ډhq{Fv_yzS_zQY:}PrJrWwC䔦$fdz%A;/JImvRhQj#L- FZm%NzÊDdCltgxucseB.M;E^$i1FK)OII%Q -G -SOZ3Gyrnfo)bGBբ8"RrN6b:o0M7J/"o~_2Kc*r|J]H-a:~E.1%'*]>bT.%/D O"Imqm^ҤJt1[c—IkcL?6$FnҟSV+[(_;܂ƌ1R}OA[̳%yB1w -xUĿkRo7™EJR=K=fuMNVM%Y#$ -8B5YJڶ"ʫø+lm#Qх_\MEVw0vTCx%rZɔrZYuWnNU -kև9%ojKnh0e -j\jn+}^HQ(+~r#%͢S[nB/Zi+^}^VKEI~f`N2xQ}{$Ue?Z,֌1MPlM{oq;-K yd$*+l_O[Y?J?F7?̕9+ - 'fSew^MN;,XRԵzh|Zߊ')*^^j/pj{Y"#%b+}^jZ ޻i -\ŕzS#b|О'bC0C[.K+tYgvqz+B<(ro.'ʝ Z㼧Ċ]=Άk$d.R -y+pZ0o2QDЃ(Cz̩OEM MkF~eh'ݢYegRFa}Knhoxc.klJ-\lRZ;jJawk0Lx'`kdע[J׼)O4 $e,:7wtI[NN# A/3W@P:G!Ԅ핍v_DTWR;Sdx.}%v*)u0?}̽OF`)ubZbϡ["6z_{>xԿ׌+ѝ๘u20 dzu>x0B`Qme;;F[5dMɼ?_~ ((rObG#{YN.E-vY6[cBíu_+~oTAP)r*;J.b:riD!˯#K;~~^RdcZ_{2~*Ci٦IX_5rv쉯A-?,tSIlйJ;<}BzHl%ۺx]EQNS/+b^VOM*(I[뢧)(_;܄﨩ԏ1Kh9O jɿS'qM-D1kÙ5btN-셶T>mx?}2Bׇ16#FE}H?^RJ6;u?2Pﰭ̊\\? Ud"yDdcyIoGq<[H4ͅ{Rt~dMcDygKgNֽE_1 _+U{S%S\*~_)YV/dN 7WE?#vn*o{؟B!vnX|yY.mu#}GvF"RO\r(3W4h=0ÿjN)%^VT^UNYYTl޲Yf`q{nUS/a>zVJY ȩR<.#RE^FO܈F.TTޣtN;c|J|bT%͢Lqz%IܩubPSG"X_yR+lGQ(޼amBNJ!DkfrcqJ0EkyZeii-E*bedgX-%/yI[Ռ7!;1,xr]AnEFXilK{L+CZl,>?ؤURvGRu^Rżޘmk6,6!TbXeY/"1o9jhIr.:bEQWg،5cg^%QY=|'y|cW#%8j%CKQ)cx[B(AmF,^!b{lEhP_%-Sŗ]rgyPwoAZ$3B;z z#82OН _{?dS[E ,98hܪ#%ģŋn:-cHt+Yb搛yC͆d;̕ʝ=u,VhT映oVRYFyUu+yV]/(V#ԯmMHö0춈j9=J.PI)e|:uj W( e Ы?BM>^j}F)nJ:i:.RW'Ж)W)p^hm*2|HQBOjF$Inؓ'ħЩ >i%}ƿͅN:{k%F? -}]Jrob1NʷyJ|B-DnYSԇ^[­kkdiG#Hxu8,㨌?R?̿T%SLBG7X[xU'!E^e.]4 'KEzxR)>bO]=ʼJ==Fqi9JI&IS%E?ԇ\Q jOdY:b!h)ܵi-ճ+>!)Ga[Ľ -W^bw&FbH"))KR8;)m/N8zD]p;T׫&IRzS_z}n*+Z#8̴U -)Y~KSLQG|tycȏ"­|{ rvv%)mZ;:N/Uʎ?'|ZvI{u -KRA`fgyQ_+n*O*1+T+}'Z*Bb{eW.2KЩԧR;%rIz*!8,aZSTen89[\M^&9a#V}fv$F)p;W-MD9\|CVd";$ =eU1Z"2EHfbÞ4:=D*;Gzhoq7h}nV-tUd}RʹF/?ƊУK|˧?sObGim.(ԩʽpMyYSbTE>Kq(Bs\܊v(BVxr9zdR'k~3->>V\ --G[HWSvB^LU\m̮֯vU|4/ ɢk|Z*nJIe8Y~c-gy.QO6O|_?c F99z21ܬUx%ڌO~5MДS~"*J̚ޙW^ -͒rh[Q+Sy &?N>dPbX 7&g)_ӽ j[HB[TXx8* qYkuP$af =1mv_KCkbV0܅]Sh{X/{k^T7u&>gzePo<s#i9ԏYMt7xbvfS_iK h~>?#M~?6T+>Pg~~~t}c~3\Q9Oe%Hƶyc?OGDyOd+!1 AQaq0@P`?!Xi5VޤފMI$7F"k$ri%$ؚK$I$fK$I$a4c7hS=KHčNLbIDCtc7T&sq6$j9 c$DzoEJT6I"'D"i4"cdI$&I$I$KEcLzLlluqFKVIבߤFuII-TZiacN4 -7m+ґIֵȍ#&c0N% 4[NMf:檋[$n1\S 3J5(v؉՝]6H[V):.mY7CIcJ1I0*\RUNj&k7^$ޒMjizƍEMUfK,1Q#/EGҽ55jڗޣbb5:M{jy HњԶ'EjwE:#n TzƍzXowKаYiFȕHbTA蚱4oSqՌsG\zLzKa=sK詽/PV):1*o[! -T-UVn֍*-%64O$:$K -[R:R"鉣/IM3dNNtD:72OxqET- -)$6&ԑ2)HΆɢѰBF1ڌ׮uuI&7$dX;\mhDXBB$*E*`DqmDojoKVFFͩ4ԩ:dbdc(=P76Rt-Oz*"}UMǥVN؜V $괌NCZ $)7\C6ҍlb*1UoZ &M$^uoEꥮ+tdz$ܑVF:E:_њA"4uz1OqP֔ϺstbXJ:N"nL><&,I3Wм=#Ь o7t^ B"]AR5v&說MDR:vM7Ԩ訬::$\·Lъz:HhGSNcz2 -hfx"IR /e~ %:[,]#nX?LE}f| 'BD k4n{ʮzDɅl(^瑥m CI4KB ZQbڝZ4-PF[NdBf9 @~D62L\]w?qk[C~|`r6'Ɛ~.D3 q/? {σoKPh{WQMkhS#н'ER dz"1"2i! r7z$y4Q¥&~#9 fXE sVDŽc:Sd$XtD9LO6%/Xߘ3zJ}IZ`~ $_T3hǡTZ֩ޗhfY2̳"ҷÂWe˛'l# quE3M#1J8)B2@ZIhق;{($/bN}Pȓ6 E +!큮4̲0N,$cK8b3Rq\s R#VΘƆߤbh7I4=SIPCtFѶ9w$`MdIv%u!Q*+Q:&$2q\-/dXH{iAh?U+4EђIDiIͨ:ksc!!FYamBhqhUH)hB\$I$)# -$ɈgW0ፕ^☌^a7<*ԃC}R!Ҹ~n??ʫ&:9{hk$}61`UCcлVi8d3Ln B*Ns,ͅBM=Fi4[^I4zǥ1 X7tB# ;߳'^獐nTdN-C`ӂΩ&3EۖxlLD,r`A!w"t-#qTGq1YZ;EB$6?2zg}{OUƼɗU_Gka%_/K3zej$Fgp}&^7$~t~ 4<:IQdk;W$R#֚N^^Xv<&سqа@߱$sEKѝw#A-ݣUCWC~14v>v頄6]9ҏ@ן1'Q"$5$C,ϲM?gF6F^o_cM}E*=-_AwO1Pz? -7 fޓmKuomTg5BX5az\Q^BVt:9l<-O ;F'TcvfD(# j--9hE#˱#๡sJE$9d^&#˙NQ|߻7#v)OFY[ UFr~^Kyf˞ZKia3 HY}2]9wo\")I'Næg:]#̝MF12=ߏOO?Dy?m0ycs}l=w1U$0@j/i~X̓~<ɸ;e6;{ y̟ wm{$vO0]'"2_%<}w&Ks/3O߂e| Nb_fuCFBSQn=zmW*G$hެtc#&k"0:8425oW:\X~yrwt2M~PT[UmK®x%BԙrЈM}Oy^SP۔Q -n av6Ja̾EV3Aф;< u;iBF}Lݸv<ʫ©1oXY0딤96PRN#$} ϩю tc}g묉D2 b:!aJz !vV{ճXdNra򡉷 ~:MlibD'7,DI$"Rw&j8ĉiDHZ'Z7_TI61XhA-)VN-z[9tؾ:$nK&d&-LTTv*Cv&¦$z,pN'${Z:&-}V][/U=7$M&ȎdoO`t:j!J!7s"刔ܹH[E(ur! =Kc \5|dPG)d(f:JtkFޝWj*1hcqNSϤNևvHc{JpD*<87 -z |?D.oJ9ܑ~SU_"{)6c-^sҢl"QooV7o0}H 3[EFG4ᨤ4%qhdCia*b-::i}NuG,F!VPƁr;9uZRf?P׺J92OemɭV!3?oBr']"yB{R730#LG<*ΉSnF)XcfЕTOrh! nQ"lD;C:텡wqR -u%!<:mt|"fea>?{)~K@Yռ^%-=寀b,/rrqŽ#cc$Nޓoz ci НO& Q!-OCV .Wa ԺՉSjG(!'ymOp\ -H%~NN1ӥ[\#$TJ. n-THZfӱyMm1Hu4/AR'L [b찾P#^ߖح(γ⩛B,þbYu 9rfd!IWYыr_6eY!UfY߰Y;w$ONq_|6,l_gp~Z#x<H7QiٱOMeߋjs?3ߑJeM?[{ ,O謜6S퉱gČ VE|VgȥNpCgxɢ&Epl$mus7opűyx$)x\M%YbdiDu]LRG"Аl-|8a(.C>B6BIv`љ %|Xg~iʿyM2F؏1gTCOq/o.ǁIQT.B[6/4/pMfoqH`B\R]]S#a9iN7< ټV;ԠS;U]#\ܾ.`5,Iʻ$߸e2|lKG5E(#$ݎGD|k9q]"0(h2s1 #MJ!ʒ[*2tژcFձiڐoBDhHK -D\C" GwN#Fb2tdd!hXG#ئ0W9/";شY8fSP`qq{#H2L]B$2eD Ao&\5v-\\P0D-Ču$,ali' Ȓb(HK:J۾ -D.G$"ЩzmETZuqhz:R(Ȭ hTHJb R%۴B .XhB!"%1q0Vb$I3j2G'a2hGԹԛNU#smnz'UԴĉ71'?&C)BRgd0{&X65H.ǃw ]ĝt Kִ@r[5RtošG" hڑH0%r%Tn&lA1HD!.[Jt?RG|0K)2۰GMgya }!M<}M~avɁ vaǬN_271a˳g l[e䜍K I3Y2!zRIbMĽ[0=,,4VEU HJbŮ4*l!CN~ΧrGavZ"ġRTPڌL1irȂ 彯?.ጄdJJ+o`Vym7EEA<ԓuJ@Ђ4EU:n*eX/[hUtnF[,MFa~ڢDD•\.ԯK!"Ni¢>d$ɔuFw46o;޾FofH^3lӃSm -Cw7U1z.NY$ME _DQsYMA1 Iv~D-PÝ ,̧M -:R _#" }$5N<Ø{Wt",{mYk/=E0eA"7"zAR4&!U鉬WqVi$lIъ5aUz$AqdU) -la!`)D`]Fe_ZPѾ&'D7IM$!i4I4I5UY-*:V})ރcI$Q%ŵI$YI$I$k}2MfV$l"i$D7I$EQ=oZ"#Dѽzl1XI&b{Sz7ULw2cFoQI&K$hr$nBvmi$ѲnHNj9&Iҭv$::gEztN4nH4ލF؉$x7IIچ@^ o~ 'Lѽ Ck՝2z"C&D-+ׂ+:'DII:\:ph݌=ll ##jU7)?)D,C-&!-/aV?3Y&.Gޫj:-+CB -K3މ$WD)21+,~%" IBpEp!c/*1Xдw̪'$H`ͽ auGD$]mh%TJ?RhRR1D3}%EX"XпLjn=KTѺI$HmM{QA 6m1Lnb#fo>$i .薔g䝈q|b\x '[cD5WaUQ!UzDCgdtnK҆HőTp WF;@c6Q pR1-l:k{ɒV`j5vQΟ{|ok j ßbqaSO ƿo -kSzhK՝ daCjofQez -HF4L6,hН; j[ȏA$:\cF -fOD&p aٻ6]}?p?Ȅ_KKqB6-r+VHN%Iu_lG7i1 Be>G^'Rf-[O,94 G?G]o!<,dc| 6MI $R)"uY ؽGLmQWvŽѓh!4Ȍ#DAWJҊW2P,Ы2tv6<ʰ쓟5إ3k|>d2'.chBV6H۸\'fg NBh .-e;&Zj LQ07acC5@\Qb\APCG2&Bř%hM]*.FyG -劂`PIdܳɌ"F&I:ѽ&DzMU64DQ-*dEqzUtmFAJi=AQ<ۈ%jV$T\.]Bh@Ԧń\mܷO!)$$}df»0\?Z9t77Ez?ƅ:LhAFbuv0`U ldM5H&K["I$wȓݎ vlO$gXyHK-:FUV c}.6b= &6I#ؗ:,97۱kk{ar1t{Q}nH/i -{lcۨ )\!y5z=iC7CiZWSjǬ(#adKtL_)'bڲ2+ȜCJPu[x]k?,c5f[}"d;Q)X$O8f4%AcFC*uuo4NNHଋӚN국4oMī#[[hFB&Q-Eu+[o ifq\<-^VJVa'Xs -:%F:M$Hx$TɪtH:N$rH(\h2!d -W>e 0W,wxO#p"HNdLC$q6!N8)wd.!Epϴ4ŕn(9܉cg\[,E=J",mqIUizkȪX}Ek}E}GF鸅F/e|+vE˕ gzذtSL.''%NH{!4*؂{” \묛OCԝH0 3acVޓ)T{ ЏE-IQ`ؒtdkНJɎ!Ҋ K ;ҍmJ8yDRSKvԫ5.:j~ϣOF4&%bA -J\=5L^EďVĉVũm}U>D6NҞZEE6BI)$MH"FZ& ͟BXdHBi[y4%.h$DQѻ /qM$lQT+CI>^dz\xKiފTEH$,'V<"@vۉEal͊U kr6`a+H'%, vE2ԑ5$ѩOK~؊*/Xޮ"ܬĹb91/F(nB R_ZlIW8n-n|!6jfkLT$]u|/&|1RQA0PuhVJ#I)m"sbQļNm]z LZ6Viv'[4H)jEe16MII@ +bS2J9Oz`_q-BeI7TEd2ԓs/OQVv_cVD*нx SwI1"@m]B.I ^%۩q xąB~XJK5_Fk:gR#T걝Nҫ4!=3VC- v#-ȩ QLҡIҬy{Ǽ?jܥt'w9 Sd!xsaIly`H24|38?wBF@ǼIw!vR(Ijt}D~~#Fxk(Gkg㙹$w ynxvLTi"])5/}v,ZMɦ,lM/m{kY^ǭ,XZW申DZ;B&Sa/pǏa챬s\0%؊L΢Q<µEGq<_cnX=919EN'XX'lq[1K?OCuGfnLP{Cӽ7$̋C^dUB"{ W؎Y*H%jԪǙz1iz#~һ XF+yjnx]14W8$ND({d,_@@ĭtR>Pol& C98)Fw3y%{!. v-T24H|E3B{VRB% -蔑|Խq"#bR{\/fF} -CAK,n$n.39 BʗkINߡ:.twKHڏJb(xЃp?WQ>)beMC%xe"\Zk>}&ooZ1:;ԬK")3q$$P.62\mWUFjIM!Ia54@BLI*1wSzdgzNaƍGC#Եy? v 'wa{uI -䚴Qo'[2ċ(I`F۷hNF!,Qz XTK77QTBT T7!lTM0:-*).øm"s \R]$I$I5-Y&NhއH#afN ⻓IQ#:QWbI2*Xc-H6QΩ$6hDɤ6H#I$ XtNY7$dEuXެuUǡQ KuhEI:#ԍdUe: vp*nXǦ$#xT)t'vs (HIܓy pwIVnHuM7.#j6#ܛo[7Ԓuk$y5:<Ķ-A,ܕimbR4/^'zPӓYj,S6n윶]_m%!&nqxhUlr&/bXI-Y*^Z"D|*>r/5"QN!if{ 0Е$NIw'EZ=NcКZ޶r$jEl $1Jl.ɱ -8 -DkD -Xcd/F$ܚM$v 'D6$i$C2I&'"5C}WЅ:sD#I2[DKV(rbS!\,;$ :VŽK-c}S%s]т$].2.u]|0Z$dŚHؘ" ؘJtaoEqViMеlnGDӦ/UAUR*=whRR_SX5L _ :m)(؞_qȔF.NXơn25d$:dbA\C4vӳ"G>oi0DbܣgaW-NH1ָŏ>坋WZ.Y5\la7lD1!9`*p<1B bՒ( } v"Uq .E${ ?B *1trIS gD(@ T9V8?lAԻ.nj" +p{~YKcq3KCXm$f  d/J짙?2EWHa[|Lu@)`?$UqI*?pV)%AX\VO؆ %;y.Ws@r ӔcI]zbm>FIU-дFWr)kBд@EU#C&BBH#̇T3ܟ3vzq8,3",l)-D -7[/5cד.nG2gYq ϽD|e d - )~$D:Ĥ)F/.A|nT_ H~1GRi,N]ڵp/7GMۧ;h?wB -V:MKNO`]#c˴[ha7tnI:6:$UUM]1VIQQQzAIeDM -<;B!|MP~(2,`VPӯL?_`IĘzY؂__f$u|a2k-1N+$? kՠ)LJGYǑPVsA?3qO߰ӦO]EGI8[Ip{b<6-˴;uQjZЎ*TM$MfH"Cubt䲶~FpS܄nA[,tL=m}%䶰eڂz}}JFfЕ p_C e)Jی0&H -I4@LyR1^lj:6 @?kO^:iO-RȷbB#yG/V[dܓ"0rF-?9I̤?k@Ojw7nT z^uTp1\k:M[GRTa'G= G=iMQ?FdüIH);>" `{i"'X= <aܵeo37dE[qdBͅ$ՎJpIX$q7¬Л "Z 6.|G"¾A=&B߰!aeq-rFmiOw^ǽ-I FSMId$I$a!kldz-g+?8巴M nD+wDAtr0&qTV 0]`lmUd$gszIu&i7&N&dɁeFf#[\I&dCzQ!hJUz4\Gqѷ5tMv$Qԃ.~dPlLV$%#bDոIDG -St%вz0,JE@dU$ a-Ii߰`%)h$^is-\J ͛4I"b ђu"-W# B['bFDH,Nw$KA9''ZІ7I&>$M&DF¦ ;W܁FB]W{Y LpJNn/rU edw10nNP7Jo"PqВMJ7ɥ]KٌQg0s $S/d%o~Ꮟ6?bqs?=! [C$z- Z`x"51cjXa#:!Nsn*2I! {d'oH C}ќqڝX_Dͽ|&I MLFRn8%i&qdOk[!u1ba/KKY1{bX/fF~s,m5/cn3A? ۭ/uÝȌ0_)BgN/̣gV[h1[W)"n쾙K$Qy,OL0vuͻadvDdAB,M >o69VYk;PZ[(o!_?ԧ/d׳R;[ ;[VЩH[B d\ 6FNI8b7YS- DM2L608jbp/#wCs_`"6\9vg%ld#7"`l=6۷7b"bXQaJ&Mάg?" -A A\/abaQO1xl:tceϑ: | $Kawh⮹?IpsU4-UH7dDJ5gn鵆gRjz j"\gؕ(՟W̏ſ2< 3,/'1 N6)o 1 X/g p̑2a<a*4@s"b(pZOG<ūKv?z(֪(+%CCAhC,~+7XTSkB;\ҿ-'n>O&?Gń)[L+e]rgboɸ,6/{|1evvk?Qd+Z&/@{TٔK>}2g'Q%*{KաEn~rsB8w$Û~ -'_D O0%MH~V] ^ n}?cx(W e2JŇ{ J "> -16kBc9=}ػl[x$AyàXb%o.m{ $H#?cMʥtyiJs vϹ' Ȯw7 d -a= c6\0‰'؁,ahҖxhE-[IZv : ;q>?aJU͸eCbl'$&V2d -Hd!$/A-,,zIRD-ha7?=|G8 -$nE`pp(J׻*Bv:E }m"FȺȢweQ -Yؤ]EGEEbA֛Ғ'I2*͉ztt Bilc!9*#qv-Ɂ[ - G6P ,#Z cb`iv<&"Iذ"IHF 'qQ ,˭2<墓C\~%mh2)/a]p+I0؃x%f&׸0fZaݙlveKMsԜ-{C|es vdV6S?C)Zٲ+w\+H̑Ë R끸ZKb1o5~ETMu6b ud"i$ZIDܘi~M4$Lt,Da")Y-*hCX̖ LݟwP5Ib}!j=) Y0;nǸ@5mÓͶcyr= D|l= }}s)]FpE%!C{LVxbY%P.ldsFbJZ}d6d87p4FMcM/o %gK,g3m5\b5tB2GE'4lM+_lm1W0l+ĀP @Qьڊ&jn\BdM&'K biXfY$b}q #%WaHkx+' V'| vbpt_فN|B3Kց\8g EH?"Z?qۢVfP%dm?|F5K ݑw66g oȉ,_tE7\es; " ?(́ne]$V"XngpJ+xa~. `ap lyx+ȇqšۢǢ&hl4oLղtG]^`bPҦBt3TwSDUmo r$E yRxPeygt7RHGsa%dgSzb" -FqpIBݷ]jĖ.X0b_~' I%~RLMy)-HOaZl> WCģYl2.yLn 7:O툒i?BԘLMz3_Ҿ<5;#-۴WD`tC.-wM8$&=ˈzz*tNF>ndX# ȼ>F٦QoպAvo$Gq,M~Ҏx/? Qåx!lB/>I6G">\<%)Hֿl~ 7 O'd%p"FaxK w6i'%cQ1k~^ZF"\ʟo`S~ZeաYm>F{.WS_G cVe9EShhFuXlзxw_{ Xw%$TʦM77o8LRc*IGZAs o\Q< -Q 1 Y"*03$raJ%!Fr}Gi5/鑦+'rt <zCeg۵̩~RnlKG"[QYtb'.{ch e.$RNE r} Q);S:"q9Xǩ_:L1+ -s˧M&IrDI5I(Hl@/I{AD9ܥUDH$HL18I ʤ7DVj - -$oR#q4EЪuM2-9W -%/~wXcRMI ?0 w]џq;$UMOܞjWR RCB~N\ -S%:3vcb6=ˣ!Fۢ GK,qC&CD*f%ShI^ڛN7!ivpAf^T&N `Ni&\!4DЖHK)L{&l#y .IRRcE4 Vnt>x|ҥ|$IAt;>bΔ;iHAmXiELp7P!LZL '>&VM` &ĭhB/D<90I$U<%}%V ̡7q-&nF7 SKS3 -THvpA?.[ ԪKDlJRHE>[_$ūǡU$l%7cDLeer2۬1=@YK*!ɜ{yv| -;?WbC"}ٖɾ F+x^~)#7^&[* iqXĎ;neo{~?2͐&De -ŔLg.%D#N "FBJXjwؒR˟qJTK)YjdYb7l/ RԵ2K =d@ڙ mlny.S<ܔ ћlJbkkSc"[ݞS)c<~fԛWү4ͦ /\OcMfv&LȀXOywQt 5E9O{l'BY7ۊ曐;([wϑI#uGGvB_:6@w'EGԥىBHsd&Ꮳl{4ziť[>W}0'y;x_H[?[9p[]伛Ӈ}HZӖ[6}M]coŊH: cP!J CC?z$_qw#6ZwVI~?z%y"P \MYG'Xݟlgw -eLiTSݼa"er& ;wkQl僣K5Jf/K1}q)O;~,'*\O܄')ed+E4q5< -̼1G3!L/a*?Hg@,gi>aqѸ%O8Ii,&Gn]ᵘOW1x0Ig  bmR9į#Hh0}/1.]ϒQںk(4s7QLbV2 -tâq@H<_Y< b>Bl flB8aż-Uz6liDzt!uArQ[CD,MDVeXVR5wԟ"Ȃ؁#8;E^ <3y3MbdnܦmݳaeDlf#ס5*͞ (ہ,#ؒz!,(Kvbh򋓱(^I5DrI{&r'w%W¤RēއJ5*Hg_f-夲\@;4涇4-쁛K6R% -w&ҖNa"_I4|=O}a`pHZBZp'3'+v&#kT(+V1ϗ 2e.MRiO Q z#cZAf |m6Nv;sOn'{kPRI|K$T2lHp@$lM(#2(0L˱UOY6кގ\6$ f$URdbD6TRՒIcw?!fGqy`c]DĜU?]O ,.= E/~ u @Ac!]vĒ}&GěFD-4S`bC}2օf5v;lnmg#n{g̚#vY6;P7\n&BӚ}Mpmp9?q0AȍI#܉6ȕ$6iގD{Vh*މ$aвNcRjS3(2bܨفi\5oh,nI=I%0$VMB)KLq5M@H7PSzam *:xAjr\Ay5uq/BS܉-5J|AK_<ﰏmAlc|W%ISyyv"#W#Iӂ⶯]L܄)i3IY1C×˹ )Eô*dGodSG~!45́Q`]:m/ѓ6ZAPp1bB%DFG}j\C$i>䗏JGRлsZq+v -)>G -MYRYmJQ>9A7{XG+cea(2vHM]LeZ9?wWmHtz^k&lsh 4DI1K [չ4Ct2*p0V"ZȆǥo5VR.}I/ +cq>YyD*M'; -Ÿ!_R @r_v~ ȹgŏ+3m"<]$ѳ.s;p'fcrI}5U@R,&%’y9' FNIU'@m ^ 6v{%'%-)Xw#zX,! dB(Ԝr9i+g~"({(~֧KSuY{T}42I;J -f=67-h|UQ8\`yφ4˪Mߙgp2 f~8znt96?gs9#Mݞ+ؒ S<;8be)'`+):~Drϐ˥, u_f,kl7!̇7"f>hR -ab'ߋ6=Ƞo7{{q`¤^k.;y_M<\/io rgQ0$VEbm-b(rƗ]Geݎy2X{oO}y3aD JX5jd^_DCVC6 -^ؗJ f"XiTǾNaWbcL2$,N * ڮXA=?zQ ssMoDbd͹l?[: ^%+r%R'],ߩ dy# -{Ӊ⿒$^ ;,*ZdK$41Eݩn7Q0D'&9vlp@b+EA$q("z1&1Չ ݇r.ʲ!čKu=EEH`zPޮKt-uN(_s#Dvd~ XAnVŲKoag%+4@1q^JHa$;ĪI/Ad<"5HbI& ¢&tЇD7H6Ѷ؝ozIIªdZ'B%Rk}fj[ZԅbkD)J_+/C'C22jnčN);6@#w3~);d԰!)!W-SF.:@ؐHp28'$ ,WϺ=>©Xӹl+5>&Rn5me -+3B7M#bDXBISU,ecIr%C8)pr|#,'Mw ]QL&ԛamEdlzz2`I%!Hl"#"%Õ 7F8n2.8j N%-#[([e^5)6B$ic\,WtL3C!(3"Jk'BII~l be?dOxB>!( ?y<ۯxԎ-_I$-ӝHMhctBնl,dUKIrK*ވmm\E,9DϠ1>ĩ7aL06tDBIlGQyHttA'-rơhC;L yg""&FfēQ-K -&"]iٟLw#”$HAXn?w>cKo&q0L3,mP - ?:I%uvߢ]]2O˳2l?%qcKեH `j?7FNJOqnF6p't,9Y/%%ڏ}U<_O[G7 [4eyٲ) LnНbpw&C\юB BT=',Q/RҋOyF,^ P$c }-^ Ov.P8rmn"&n\S#}$'2NpȎLU瀙n$;2i"O$?*mrmN*K@7TUYc*Lςw,&q,s&AgX3ϰݩ1q=ѐC6Kzcl;{#dm~$UiFBr.rdy/.4K';f&%5 #v*x:Vw!c9}4DJs:p#Izw$96FCW|"(i6VsG :3-\ۻyM+ -OuϹ -J-!oX[1Ci:Q7@mr"!H a "^Jɯ\Q=k# 'XbE{hU/c-e=B=sIRf7%$$˺^I ؒ#w"S<>YJvL_і IL= ѩҞab{vVb[E= DPDBJ8n/21X2h|ʹW\ *9D?r]ld6A_$A\o-2HqC{Ѥx*؉a$M$$& dHڛ 22{I1rEC,7~ͭA" MI68q6cf&rILQI3?;u6c|=gV\chÌl[4n浰ilblUx?Wr-a( ڐ, ="w.~Ҕqw PJXTK9&Y$M M q1$̊g8،6fzKas QA9t:Cd3rZ$I4\NiqE-s#$CF -j}|;/ď؏hpvsJ}bD9z~D'-r!s)LEk'6߃bVIŻ7u+NEtޖD@K>"-ݓ^m瑈x&@rrynL6KeeDa&T }DٍK+ -2[#lD#;eMcJ,oc0'"J`?fC2M9ML܎OVTJdGRTI:[$[bVAhKGUtmr`@W5aIѿ},O= -U3pcWuIRGS9t6z.Hc/c;A$%>jzs d>~AW"xOacb|+=W[`K1ǣɰ2i`iqn^%>|~JJvȣ8Hፊ̮1%_Ѭew'hTB WyiHT]`{Ans&ė8W EwU.w$Dč4EZoKz"k (DT%t[^#҃doy,*j;nb{{/6v"Gx3tE"'+>fCѸ鎉X> -s)rfok<OJZ&~ }ݒǘVUg\u<5 7l.12I@WNxgF7>%>V-)v>3[P; -AUgvEKD){' 9-qeÝQ*7Y ]h$g#5މEEj, Iu6۫7m#xbͤ^G2ᓩko?;KĤn1)|Y$MgMq$7#qeʻ{Lڞޜ/wpNF\ -S{]}(]H_^*†1Ϸ_(Ympe$!0jomc۵ AsD+:za҅2~/ _C0= BtUI$nI} Xr{6nnMe[3 [qA<W$RrI%* R$!6~ ;_9'%ع{*c7 w^Y82߰8E2D̴1JԾ̈ K>c36bs}#yv6y }r%ܓԳnoh5+#~Ј5/D͂7BbL.DĨdT\ jM/ۂ}ԉ268B -M?/Arp ) Jҥ/dZ>YzPr%W,Yqg,O績µ3u~tYruDqeǘyYexSz|<9 !s6۴|F ɰ8O22YSP3wz⴦N iFFn$ڃ");+FCcaL02?Au7Hh -59i7H53}սE者rr3[d]l]𨉉n%+SVyv_t"dP!Y0#btv" 龉$+,isLNVŏ|W'P"iʼnAu@y$$|mQT O  (C'x_(\ni"dM0I"ъ*CnhޙcF*u#nV$u39"N#b$M[,M=s`r6\W T,M *@Y4DK^F"6NhI2h&XXުhmNņMĔI½*Dtx!RW$+&jT$nM-3]4dîbfEV%ܮ6{puM($pÑ1 eȆ_gľ-v6}WFgČ -&96tO]ym17@s+%;i¡H\Ìd?7F7X)r[mqIS/zʠ2ٶDCs$ɍDM-I:Z2BUSrI/Vbԙ[$hL!>;G'c˥fȖ}jTyD$IN i90X#GI6=1ES }mAE Ws'oo[3;R(%&$&\t/֦zI}erNiu(d(_g[g QΫ^$bt34}c_A i6)sohS)FIJur lcǻ`F@~ނYZœMvC].WÓgK&c;L$#|i٨C3_v. pbpI`HO`DŏLNA@Rd7tJ&Dbl(q*'AM/%ŏb.X\]SYw<&_G,%XZDn2&@g#2ǮK4>'*|hl+DB^ -\nXXLQ]i1coOH5><XΦ0nD+t=R6=ؗIf.( V.#=,u|ڲk$]XB -'8b%Dҩ2ܶCpm2ZpYљԈ S->FS$0lp\nG98u_(_P i`:Zmѥ$6|ɴt -Bցk ğ2|rd1RlY1d-J ڋrc5R1tloZN -_,== m -SٿNG<J!P\ȴŶ.O { -w]xs%"tV(+0LgJ )ݻ_ȳ9DA$ 0%u+R;rrg52BTYA2[@䜲4AzVxeDI&e% BEAoCX7C Xod{u0HF{FUxɸ)M$R$$4I:4)ĸ$v[!1$"A&CB,jT;8s҈a?4 -"w|aCLah3?M$;3qױ>Y:8Ua- ɳa*nnOA)+$KR"Id'tr2Eb&"]I*tMi6d[*K⑁0\F7n7DtD6*In2l\$lwCdj ð*YCpdYP-#a"vHF'"V.4bd#>J؄7X ",cd,M'LrId[anlvzLJhI@r -[E>_"bXi0f6Hލ6&NΗ4I3:ت"=zM̒7IhjI4TMU#;ȄFK4 $$Dݱ,k@fBAw^»0itfRh)n1R7#ؑh M,M@畹2:Ҽ E(#bo6 ~ c M|3\[F -w]MpCk lN&[ =ul9ޑ8#.Xi85՝Ծ]ap 4FlY$G(\N`ӆ _ Qz;bVr|7)1:KoIsDMD&ؒx.L$[\#bhlLLK4I5T)$ؒlKcd&=F$)b"`I1i#H&Đֺ .0F *L#DV)ℎV@'NQ[ܬGu1/#O8E`W`M(rK?GF~W p&wlaQg5K8= ؑuNl&F?_ADkc/sb4|dVJQ>Ġzlٓ'y|}/q-d!mEȍ^0Yr)q~$\:-lZIEMIE-OQ2I$ܒQ.t$PH$UI&Ol$ |ɱ$!4G/QPud~@䇳7#w=/ \U5Eb -;'nZM0Lqw+b8ؑLA_BIrj?'F~dLy?c| Dy$ C60+OE>'F_2a~UJpAD>kbl=QM%[vq>ɌzqvܐF~.?oR )O-]$=$pLkmPX6ɱ dBEE.I!Қz?"&%,ʣKN madvo JI)zMdq%˙eH_Ր臙I):#q}BDfL?}R:GMI+C&) &5LH$I"u#.H?_+V.gW -k:%w7yoם[1} 7,9xE bDRG"<#M5=ZZ| SN1 @},-)P!Sw"T"D˭1ʈܢm!SA!yV~Bv%kA%AQESFȝ0)h.Xڐ&bE2Y}'׊ܢZ,߸d3}^C'\ sWfKS# m"zRF4L@i.IDF&M$bIpI$7֍ޤ{ɂd'E$&aRdDbȠ(HD`,`_^ ZUQ46&&6&I$QI7M&oI2&[$trIv6>ؒLڸ/_Ecf`:Q ВhY[dIѱ8&IR$I&&TXI(K+I::Mi$*H:J=6Ae98z$LFJ{H[cC>ocla׋"HgrOֈˋvDU->9$m}`gE"D""m<2hꨯYtaDM$lEI&JtM[aYf pف8 fz >4jQmRNՍmur"XX,'4Djt_oeEH$K$2kWK*UhY /'MILI3v9|E1j!YugK"*-oV9Ӡ;X0yǫw؉e=4 -wl&y -96t y]fpZ[p? -~N]~Ԧ!zfo$XS%>KXQEALʒ?d(6|Vo"aw/7D|v'h"ݧ2[8J ơ*IMwВQͤ"lEoJd 7&)~^%6&|;ѯu}!<1e^'ۏʛ'/q FcaJH(m񓅲ړKClOt -D*.II {u/? ÑbiCvt{KMVwdINlLԌ]p$Ewq-p)Hy|'ܑ{#g?WSpP^): +hK!?#ْR)*R@a`̚nl}E=ؔ#0AŻ))g0䯓$ czoCT!WT-Ei27zɘԃz"k:IjIvڜ܌?숭CdP-5F2C2]$ri}ޫ5Ͱ1<>$_+ o +ɍ pMq凉=逛$.$LCE| -P3fuºN28K=-޳=XC="7|]2}ZQD_ѓ&FSP=CpmOm ]=),ڒHaCaOь&?蓲 -Y'> ~'7~LODnWNIqOeHx'hs9OgBP@2Um%+9]vY?`Ѿmn yf ;ZY/9h|!ß~:9{Fbn8h wIWe]Ķ1n)7-YlE<I<6d`۸?.- [Gh\,¸ȴ_*6T7\X oV}2cHbβW;{RxB7wKh^VxϦM!,]q$ xo%:F6 -hJ,~S>t"Oo+'T|Oe{nȸ!<9gu!e/~ҩ ¯{<-cl^*~O,hB&{;;j2u6I2J~D3>TOzdN翲׏ṉ I -n>ٛnxhhA9 ?K\H*Td'_#np"IN篲_CwPfۓCݵ}n6&+mLhxaM|/*;k !}x Wo/d1J̋; ad:g5b|?ef<@>z/mLbG%]Ja17:kHdm+{SP!7ˡq߻MܱG[Y>(٨`r^“y';^'v*: -VP"hS!O +e/;aOa2[W-W2bcQ„ݐ>UM6Mr*2If<%<ĀY䕶rb,$١J 176-'"fn-í"04?SkɶRbHBV&G)a -nв~RnbZ$_:)(lWR~(ϛ1IƄMi=_ M{x b2zlxu:ۛ/{YAK.%CDn1*L@k}-gw$vwXЦymwy,qeXj[9.r{ӖҜY ItqrbV̓_U/e-3Iߥ#сX#oՃj5=SY0gHCL#~$ IgaW -tFD{v9wjQrYss;lVҹYm#-.E<țINjh8?wB"ƥdk.Zþ|d{?<Q(qٷz_1Fof3i>ya\7 _ z{{4)&j } Cj-Sc[Ϡ4kD.tCy,ğ.OǤ -|XHڤ8xEȽN]caᐚg6b5b)bƜ05%C?gAa!JScI)nyߡKhr[/ūeI<: -#9u0Kv!ptf& ~.] 'Яys[TсR*SGmF_cpJ\Č)}bYY:ə0sæ& ZBJHI^3{I>$o;BAv=ǟ?SeeFr=E>^E5'9gD wP?W<L?fK=na+ab^_m=J]ߎY2L~%4ycJ~dd^jֽiS. x#+ -;I:GܝR7I7[19+Ma2cEȴmdKJ@f˰#/`&7,0AD4>J˒s8Kǡ6.eͩ&"XDx`^O#NxxxPJa)б"8PHcEqf<@!dbAef[t'OmؑaȒ̔X[V7Iqφf̏^uN.%dI$B>RHMx$M'FhoC$fInZ eW!IToi1Ґ+ FA9p-{6GK::[>I b{N0[C|^,622Z ߺ3w~G HC1cjY*Yw %*.OнfQmXyee|n1q6%J-L=U>`iTYP51)-,Rzs_`#TS)8rA4D;ȱAA3~Ofca 6d[p*{'nc*rٜ7m݋K9mAHl=bl>%w knAml,i srfFLv$] \;hMn8A<=LW{ol&qXtJKsP 𾨜2Q;hNN* - ~%K#3< - ^6aVĞܗNgs4'RךΙcv[濺E7Mn\zj>IJ [' )i2PyoL=H7XwѸݧ1FxO).r5V:osZ-0Ԫ伒}I'jAw2X,ʵud\-S 5gm{Zp/F$2vsYߒL[[{_r}! 1Y!ݶk?wd?/]GQGm^BRVEd>ƺ d 'pk A&'&حWxs!k%=ͱ;..$Dr KGqSi?"3!I %7c"X0!z93]D?cl9Qɑ$l3Ώq^Єc&;-%@ Vڒ1{0O>""?c8-NN&dCdk'd?C݈Gx.Kx q|h,S?ksrQO^e)#w Nsf|m^$ ddOi9' sض)tj/Ml{"o}\1['6Tɥv!g+&bkuuqC<1VbF -rO ٸ${IRХ{DV)yc}O~f1dmT,~ -FktA-S$_A?^RGϵc\&h>j\] ]=w1 ^mJ>d}aZ.euՙz0|?8[õw+ǎM{B3"C{ Vscށ -D~Wåy?GBK>`[/*k Ğv-2[J..)K胐{hcYd8;{r,G4h+  -u/Q/b\'=n>X t:RKܷo$0ԓ!V wK7k翺9BC0~n]6ICܔt&m5.vhq9aW#gy -e*9sJ%K-*Q"_)aHG[I$P,!='3lJ!C=(j%KOѿz:;GtOȯCK eClWI(W #$R Y8B< WB; l~^RJ׃&~n@7$]a7W#Y.fT[.I4d_(՝>FMLLiN=i&b,,+~̟da|sGF-l.H]q*uQ3bEwf˨@3l; i#״pofK.Lj@t EE-*N;Q̲npZ]E&2Zbؓ`ƞWH YQFH9䑶2jhy8 wR&i5nv0' -.8b90 D?`#ytF.4#gIj] )z&ab b^'abfy, sG)DR.+>a@HX&6.fXܚv7Ga ɧ H$w) EhI5v(.!p ‡J㥀'Q䫔$Q(AC -y= MOab/'D8i~,(D!,!=J7s˹o < ӘI/v7ǣz:/UhN%mH޼RIu>.[(]\! JΕq tA -P.!r:ka$a7 AeV_6dq F7:n1joMSzܟG=1XF#M>I4%Ϡ$g81y1;PS%62{ЉdY!'t:ϕ⒟7 n0 dېpe1O/w&zyR!"x$\[U7,{}{Ir/&tt]l/cYe͋$/4No-;.eXC>U([ZZ|g{se><]L\F4EI編$tNI5l} $kSIUH4 -7Dݽ] 6o. P<o-S0G9e/Kh>UKrL{1uj:a\MA31c>}Y oɢr[6#!W.+e/&/-x&Qx>ͮ&" GSVL0o囋d!dվ){vf|~ĴJnF)=SpЅ9Ad&.t%2=~AzY4$Z_>; Sd?c"||dg0ǎkyn1fS<'$\v<*9:1O4\V@v,FYe+WbوX,Q?k~ .5+Ⱥ4g3 D\F]ީ&wv;qCrCL#Sߘgyq!N,syI4UmFi4Ed2N$QI!xR6BE5΢vD v쥣-_n )( F(WxfC%jΑɽV?n'W܅~ǘ'u$uSD'qӑ{/ܳɒ<?/F2Е8Vh0ޓ0aa?am 6\&<~-R~\X˾YeFb轲q`>ǐȉvh<{=>Y5uxtgq]+el4_ثjrq_$NĔ9aAY[PYk32^A"0ߍ691".X I&\aU%gD\&ɟb+f|JF蹃 Q2BgTн"NYfhvO'a0ý@ N3,=dR(ٮPV.%pKá+GW^Fm6&HAqv$DׄY $M}萛a[ؗ%lwMВ{M l!JdEI$ZYq=s ˱%.< "yρ+{ua3xz[QsOL JDc-=tt.\Ei:8[$ܙ#wlyC&qPh$>\Oq }[yZg9$<Y_Dxa(KZHNK؉mI$%17|FPƌ-Z!eio3L5sȜm] Xm -GϢE|~ڛ Hsv!,I!5>%$ Hn^[r[})9(zB9:$DMget)Q',xٰ?BJ:'D(n@@,t 'lcu(GlL!t)P;ImA'͑xID.e Ů,Jx̹b8EgkFPԶS%2ȬKarB$%d^vRn~ĒI% lggQn O#L%mG(w(0H,!I.ғҒOJDjSix %nyiY%h ,\\$s ƥxQ-pnKl{h۲KYn7\r,\M\b#o6,1,UsG͏j<aNid%2kV( AV )6,u9I{F4-Н#aLrv#3!r6_n)7*wJj)7d&I?bĉ uIfEu6"0w#b=ۓ Ycl`ҪbՐG.'. rkF&@9:φ/5/yŽsIN1,i $K.CEAAH R "Xj)GҶSВ,FO Br }bcƏ2p iKtvߗ;qei$~"Hh9DKXē߶ZՕ.շRV"׸4.a}-yqʲK&g'zZ'"W$3Kr}+FAoQD6%W.{>e0z{|?aBtEADi" ))AAAAT(EA  ( -fWXݩE*] QxuL 1ϋp~Nx"$ra#Ivfx[NXvegz"n0X+L>ig"]g<6]QaWE2#,GoLKڅò>-cpr}V.QWqnM!:5Ջ2G(VRD Oya?N8Kr3:w ^ (( (AATvizKAoR %B(H[~|$3E ,D"LV %+oHl -ͳ_01 ¾G>"]6 Ie!3Q8ϴE>Nց$C^JӢXȐ"ΞWȿaI oP_! -,H^}F[5Ú{> dJg/abCTٞ5~ނ }tg.R]$"Ht)ATAAc@-v-i*WUZ4 ; . "d{3{aMq6=dvI6Aׁjrz`~rĔAG6>YejtOK1%dz+d}ne) w4P\.ha!X2 7b\ GIM {$Z}K-,4E7 -`Mg$&h-$t~<jo3B_H9iag3cA.o%W7C3e)Á. /DQ&)>K%*da^&>H v~i#g/.^ 08)TotX[x2Rt%F󑫲i -[!ζ!"pKRa2xja$& Ƿ- wLu:1eU2_?; ~1gA1E&3qtȖTa߂ѥI Q:WCӥM'#r۠*6,2#;]Y$Z7䅡=z]8z1S"S[. uՕ"nv$N/ę$}z-l ȳ57t-@rKax=dH B'"O{Jt1pXwhPu3,;EItЭ#˜NetM"Av?y.AhK-"D{GCfq{ߘ.$JPl^K)Ed6VX*VJʜ"a!$[gb,n0qSG aNEWCtʅ !.G:'E `QLS c[~2Hl2YCQ" oR.D  AD}H 4El=$A1&/DG']@-bb\S3*zEt&MQ$[]Ԙ"CBD- l"CB)\U<vU,@qʥ:%$rYۑ;[*iΘ'ՀD, ІA# 8;PIlHȞl"ScD3*>M3je[;tu2R e8] 2DNsZbI1Dpm~,l桘/#ѝ̏ĉD-hƺ!KtLFsI;rzd#jYRBB>bլyE;ߒ׼n`ndAb2(Sr2"۹*[)uCE#HH]؜m7ϽzE!j+")V$G2"z?̔ Żu2`oyw9oHkUQ|8 F~l6F-"o7XO)fps -MVdPhVROxR!7"g6;1(+’9*S1s ~++02h,kgK/Q -}\cGvR}>%L¾3&w( -JK6ȥҚ bH %lj٢:HKOdrdXH'],2`3ȑkĻt%,.%jNM;NW(^hɟF,~L rbx!p:!34Z;Ϳo3+؁VX(%i3eUߤet,TCw܅7aD&~;aq9'W9{81-r;} --Roؙ%,>p,/=NJ޻&/ldl g?tr`?-RN.] ߈dk"'\K12{il^7hUl_aїL59aӒm#B5M:vv0#lHEoc^ux`؂1Q /_|-m\ ?5r2QUG2sI9fU)SiH=]e}/z0$ 6C >6Jd|Qr)sqEvsA<6* :Sa(Ipn9򰻼M27Oϡ5IzsO*=D-3&tmqwd7"&@xiq"3EQZ܉I/jV2YKI&lmYTN'Cz&It=/DjmOz[UKBAdl7 it`H95o-V#$A[0t6\F ^N>ot'鶭%I'WotZ&2I$rI&I'\tM&RuN4rI5ˤI$ugUvuM=zmkoRtI4D2H:I'ҹD'Z$]=ȡ't!&tr}&M/tM3F$.H{Ej=SMfRMͨOt!!tg5I5-R uΙfL5fRNI4Ή$I:gВI6$H$bFac вegaapBɔLw[&ӜuelD3\%\o[Գdk:'Re>} Wb}+I$霒I5I$nOI6.I:dbk:/2I4IF$$Y$Rɬ+b}*O:$I$^Q*I2I$ܒ`li$I$\{$L$h&ԚX&7I$Mɬk'.MfHg[d蝩4mVI,EWaRI¤I$K&J: dԖ@N:I&Vɤ/I:-Y$ږ$n(M$Vi4I$^J[ՒYVK.I$HI$&ēIF*$n*HДNܒI'Zy!Du&J5LI4I& j4L$lH˾G"E#Xe"Z) oqV] $3 b9e aͅr]e"i'% Й424Ii4I$lkaK=$lI551p6bM- h.K$ڳG$\4'm"ueI&$HԗQ2挡o'C6^9'[zHZ|gpȚ[~e:6WCd cgD nDXqIDXHlK䕥YВk#6$osMU$L* hI$F Ҷ,MdMA:$k,,\n&Y>uԐ"`RIDd')Ӳ'#ۮ6Fzw(_u;I4*$d8qTsav&QXG?[l_~mb]5RIto0{#e3C$d$urW=75C2IҚCLEKH@=d(خ$I$\lH$D0OZo~$ hjٮ&r*>e)Gc{H-C)> IJD]UtT,¾R+W6cst;56aH-s@nӓmO=; wOcq u2Y"S&ek}S'?[Xwv,GM&Od5G`C&}'IhA)DW-EЃ -1@IձQF,H\VSah$W0U2I&Ղ*Eő氨LRjM2$Φٰڑ4ȩe $wE,MlXM$dRK$K"֫}l6M8$W-$" RKAbj=2^"SAb-PEjI/IDщF-GK"DJ:QaΎ+rM$fMe\80mW: }˛QOb.Eh\IܙbP%@I$M/4;R;Ѿ :M*C{FzЩaIA})*!V,Z -wbrcJm遝!Ahg4ڏet^DjtDQޓIeI5GUGlFSzt,q4Ax"綷j"dIBb,7ijI$iU$oZzlMv#VēIިD_c$2\l|4lp`G n\՚n\Pz^\z`=p"Y: APS rcG'R .F'm3Y.7Y3z N2bQRIЇR$LLH#Cu&LR`4܂S˴$Ήd!HKbHdf=^RI$V!Xa@ciZGXa钱j[h)$z&ȝ&k#92+ Η LYj^SL]&4LƈI:i$ز҇It"m r+N;9,̫͆{&,=8''_I2JNQVfմ`$=Kan"O]\ޒIY44I'L4izvM=ƦJbir)&.M$'d'E5"ly/-Jr0[,HD_A6Ē) ԡ#/o5|#.hb:d.6M`mh~dЖ$O^4J&a@6:nnIr@(6$Ni6ݍ5v$M.KDwobDLw:3Qj[\5gIM$lDKh0I:$P(؉z7bx$vͩ8DI7 ر,=|lVїQ*mMܚI%؄_8~;xިXrhDZ-o$w$ Ym%Kaܐ= Fڕ$oJ%Q=I&uڙɢDi:$<EjMoTɱ# \4ړFJG3[G&i"w ȫigc 9P$Y:Ex4tYLf&~xo#IDv-0w}4$4ǂwњ7ҳH&Iu#>irKR0&*M QlbH82ve湬&H$BO%Kyp\)d~e8A_ˑ/ ՑX|WɰV8[C 6s,yw&LnB| >6NRAa.sd&Pܺ&`hjګНpE/dBmfI,t49zRI4LUh؂3 ŸIŴ %6/8g60#neS|Hm4:QGKRHڝ&¢zت+As5fcѵ`}&F&<4$2v `BGq[}n_űGȉ.7c 66/4YM鴄YiIԿrKfL3Z&"b -UA䝹wB;ƍ$6{+j/A}>\gH!ݿ߫ ≝9 =X -J`et6Rz+' J>ˏ-.)1/{vS*p dBm4挞IJfE&&:RKވtrM"2L"V ]˄r2WeG,1\8?ѽQ"*7(Il14`L\vD7^O4;sڍľH1C.dR"?龉t%"FԷ=4'I)7}NYcBս%M&5z:FO4QI:UTIJ4mIЉ/!Y$Hxtzs=9.$Ws7ڲZ=K$Ot=8R^WЁ Zب1H"2o[YNЙ:fI+tf1hdN'i !mIbIBevՃ6[6GFd%"U#%G`٤sNkp7# -W-|ꞄBԶ h./I2")Dݍu7w;1ܱX ΘC3S#dxtIY]ˤ -$˹ZCj(\7X;;DUk ,H/2e7l,'kIyRo:gƺr7%.ΩVn82hO lryd͙1(%ȸKDnh!FIҴ.$ɬ7jF|&L.2(4g]j'\س}MN룵:5;E܂Hy>uCh%*B?gATd%D)RQ?Ap7Y`QF8!'f}e1i$A-Ei>tNVIF-M ,QD՛"bIMCw%0yZ|QH|7b~ ?8< .`YjAb)K@˝nXFo~Q֎lZuhoM\d{/Kp^]"Xu~}р$hE1H!F1")&`UL%TZ)&LA= {mm: 6 W+(< [n &l9hBWb+dLg)Wpd8Smig4dvBܖ0,0L}Y_"WJkz xk߁Dp+X>w~֧)nXoĚ:4Adz[(hzjo"DIՋQBڲ6"Dzӵ+6B`e(7h  V,h-5V^P ۿX턽dix+{ > =9Ii; Q]ɍF37"o[s}Q|[`{M$Ev]aPý6&/#vN/!nzRo|C|*F4oI,n\DhbDC|μ=# -6D]3J.W%Rf{-K1q)NB/Yh].L^.sa~؄=!4O{06bZ@KIpnDlWU&/ij3zbi؁SXOrKMEYoEV7Q2Dv ֩>ޛԚ2dBHF迩jMb걢 t7#dzέƨVtX\!imEG_$蛓%:K%XtnFVC,LVa9,;smζ=+֟^=yrf2+gdP8:N:HҲЇ_?'2_ hn]."AY V2}'pΝx۹ڰr`^cl fpl; ;2~D?qb׃q7(m'LOj"tJz&"T5\pA;JeGLJ$~Nk3;Isژlu(i;z 5+dI;A{rE37[D{6]b"-tKL"P-L$>%fCM F^aEYzuoG'D5ڨz=ς5@!iM˓1Vf>1qT7Cԕ'\c G=IdYZVi!DҒ]6ir=y?[M""- hbY:TH" -ѷ}nI~=U&\g+',nI#NURl@~6JKmֈ'P#qhI'W2ɵ2L?Úh\̊ƃ_#L3W[c:8bF67ZM4Q-j7'ÖˤʒIK;HW]TӘi,-MՊcDR9U^"]$Vd$)qOㄚ %fKI*2C +nRggH#U0 gAt Sr=rLl~K&Zd)בdlnRp?eɄٕ>u/.׌aUNz0joeZr}9곒ʆcwސOt6.bv\"1 @KؙPH= X˻g=') @iQm > YWʱBMGZ3W]S L+!.KJ*fF-z_27Ix'S$>"H93D9#\ؿm$B 4[Ѝ)zR芙5VɬsFhMX,n{4H/KOoN)iV6䌒ΫVH%[Ҩ^ړ#M!;W~~W&lG1t3}ߺx!IpO#~Az/uV8}SSy?{!,~3:vG}g7v?S3'qq?nsiR%~~7顳GVy),՞Z>9<翺;;ї{]cO~DUTi"rѰdC'Sٙ݇}w!a(^/CbsT4R|>/~N><>x%u޵QIhɘ}}B̏OҾ՞PYC 4^}Fw/y?wJ{~'<~=:yϽ*OZgozR;cRh?]7~>I˱ܟYϸwz7a}_/QhG/=՞K?iϲc_ycLǀp1|п+I?`pj(O.\_ى='a}]GWB 5cQ\#IvP?OߙȉP˖#L!V^ro 'Mp~Y>yb<gpy'r~NUE8N˱>R,A RzIm3izt>ȥ_LX>;Nlaޏdq#hYb<#W;ڂO!wvf{z~5XBؔGf}4p=X<_Oq%0?s*a~ۧaNfr<%< 3_E%|'/>Pg<Ʀ^!zsWhFz(/ЌCQL; eE<7ًQBrxi ,xA?n -5ɏZ? -i&)7x$x44wޛ!o=SZ.iVa^3ub`c>XO9*I儕tpi"yꖓ#CXːFQSUڭJ Hz tUTP1zcZcVZyW#'x#<Oxa*OW"|QOeQOd0Y}HX M!2O~nDҝ<.36r_Ƶ'΢Di^ODWyj%?7CWYXG;ħtG9eLy?<\ ?hJӇo~~]?9 Uѯǔ\ZNa%YT$tG]!Ág?+W<翣}[JF <"nU'z+DhoT=F~y>*#_^% (p O<ԤTLO?V"|j\O[Яo};Ţ.fB&ݢ?{ϟԳHn~hX;?3KLS,QO՟*>=C~'? ~$vL& G ?"</xB#T V4" Ljc (@1|+NMC%WA<<ٙcV{?)$d}ģtG9B<ú<E?WE>+'rd+l~δy?SK_\0CbZcp3yry=47!>ጆ0ˆmRh솴|(yG4!ƗB?ڈC$QW'>O#AM/(?F"|?{ht|L~|_ٽCFFi^O}tGOi~&GRG?K8Oܟ3Z= -_۫W*FՐseH2?'F~'ϱ߱Ж/\0ngn Rt$Cd[ݾ?3?NC210bH}JOЇ~'pNtv<:r!=G~':%Kݳ#͐grmf/7|dPͰ~>ə>$xc*3Z-~$<6W۠'<7]$4~KhgHr}}3ch{ɽ^>ӹabi:-'F~jtEAuu3?Wcb?wJbE.D?ɑ>O៣'œW -?Q~)H΂_,yO~$zk-?<}ϣ6?SnyCd~δy;~._3sƞO'7w-<\>b*; FP1?"x#OyH25+!gNsFNOs%L. dId`ݝ -c=kXM6J^-FibD&Z=,{z1YYsrz2;4?WCZt`c;7Oi||RZP>i?&%gI<<3찾$G!FbDj p4Y/#jirb!ݖ 8AQTHqVXCr-,I$!]pY%w3t9$G0Z>N i;a@],, -∔,@aj7pXܿ)LkoNʞWaa(ii7k: |C ($B7`%X;7 MU͙ p^p5anb Oa`HrŅّ~¦젂J{3G#K]v 6Y]BY/ޔ.<~p#Oﲃ C ǑT-<ߑdRKӞ ,.ow jʒ8Ӡ*'7R&# -nuޛjZW $Q+TV qHj2Mh&$/]͈ؒӄjO'xDsF$ȡ$: a -h-*CC;%LS$mvz]$鶝еǡeGZcu&ftIIhڈHtqI-hpގtU[SF^#C/]>ǬN cckJ>i:!'Z:{Rk4hlE[%WRUk LI& 1dRNW6Mp#ru*GNǢ(9tI5KXtN;z Ӌ`aiMҧ>ivtH.=M]=ǢsJcC8h7'74IӸՋKOrL6"D1ri6ԒGzҶj?Q3AoU>Z^ ս1iqî"媙C`OÓ8z=rdp&ԑ4Ѭt&"6#6 ,L -ֹ\=3}Y_њN,1+S:_s+qMpȠc3Ɏf佄,!_1L, G;]yEAv;@Ʀ,!mA,&v9voA^j-'@(ٻ}dؚ"v[=i`^+Mg֝Z/j/Hj$M&C$:n%1֧؂zIܥ{jNp~Q,w@~ M̄`#G'z _! ffH$ٞd-lte$^9"6blvi } h囄ɘQ"86߻ZmrlMdP*ū:fsNj"JF;wO |1~'[|D4tD6uZ> #rg} _?̳]gg~`m>Y(;{1qYuB.b06BY^*EAFf՚_D]* Rckc:ش6?FI'NKv7URxx\3K"sobQ% i##.BJ-:= -2ͲGWsm}_:ct.kߢ?somKG6rn5 Cb1jN:G nIo{ m P.hI7Idx8&u蚷*/MaB>X$޹/E"5'!ͷ/jHv"t4$CӠ!+B,p/"\o"E"i)$۰FgDH6e,5$*hMCC= ?^Bdu=E߉߰iȖԿ$/Bڗ]/q qmjOFHMS:Y&޲nXj&kxi4z&y^mbȇt g,B:蘟o\؋VX/]#ozcC)A(BP"QS~t*7ITj9FFp7z,((*^W {Qi:],ZTI'TջןmDEj֒krIdjNk"ǤMTLzZ:&ӍR:v@Ѻ,M$Iزj2G"$SzM'Bu<32E:&gCz&^UrF#c$$67ϣ5DLtI%$i7W֓+ҟSjGväjmt6Ork:UMU1tؒI$"I^QzQm 4UE]z$Ebl"zfԒLdbIH߭:rRiUVF&oB׽W"ъEbAZz&M$=1M$Gh$E -I:j#З-4Fk:4(SR]duZ:1*A࡭)i-cJ^RlNH:&YzhdDmTM$%صS kW$QjoB+ԑGDR+E:]6ю}RdHة5lVm +YzLiZo4UގNzzDkR) t!RQi[EY-.幤Īs#+JҵlM7蘬gւGiTZ`~ӹ-T1H(ΐFDŐ+% 8K\3->0M"GJQky JKJ2xbffv@\&J!iAn{OԟbTHi+5Uw <77jA&N*=SjQ&C7@A2+)ːAG l$ 6pƨ9vH#N(4c?oJϺFYtcs\4o&a:OuVV+lI/`" -bjScBn¦ܵ$$"T(DRHVEz0:*ЍwI&ƄnA -3$/M)~̍h%g]Cڳ\%ȐP65a>KMw=nw,} ˼50"ӟP7Npǎ~(K@Cb5.WQjm3Bٟ,ZNXb6đ~#̺=п1iCDhX*΢SH$oҗ4Ƭ<+cjǚ?>MHډզo ȅb,Lr؉ࣜ1w8Qc" J4@HP !+4ƜiUYhZ`#C\EԘ vhAA;)AR"@޽T5GI MhDX0 di1HVHrV5Ecf2/, ^VO',emBrM̶ji0 չn!%0bоDNY2l33j$YD{rM -v>lFx1r\/2rA/AQ A"XAH- H؁%[#ҁ#Qau/C9--d&bo0;-e鈾ɒ=#,{Ռc~d-lwa&v$e-u؆[&d$2OqhJαH "؂ ܢ$Gq7hr8+C% Dd29MI]:ū[9֩}jcsa`?ftǐ`<Fj&%E6RYqȁzZ #fCo #6EnnB~| 66Tځeɀ mT'mV?Z%QIm#|KcRFq&JBYX5k)097Q\6Cd_LIˌ,], R? udGHƷV--Q^hz~3ݩBxx{︩ |I]EOLK\ ;" ; dy?G ޖuS :Cս -2Ņ e~ {8~ftH?z/t$:F^GwZh}繍 ֫dVn,zY'Kz'ax&io+r6BJx<_K> ge"%!e"29( SSEnw}-%xL,p-E1 FxE"Zr~G'?av3'?KH(dĉP,H83qGљ8?x{N磆#g_/ڵc:gf!K_ˊz;VtsEt剓[kZbzMKė*oRa$!F$KА qCa j7{}>/GIwỹ$!Sܸ#"%sk:ȵ&(I,VĒI!G_#cF߱&2M Ii):r|x-+Be#.b:L؆)h?JϪH޼j΋zK\\jHE CKR4-2苓39r{z[|]Z$ w-%msgpJ.[^I{ {1H':2@i?n./g΁&8`tؗoúˣvj'ș4B&U;RXȚMNXٮ"PR/a/QCREYL12L[hiixtTQe [R --{& - WwD"Ir` K>IJBp\C9dG#R+\;PjI>}Ѻ/A\r0%MdgR|n\]p68HE(0 - J' -ܗH^Y܌:BIIt !ұfw6R1kx'i#K K~REr+[)E0C\Jqʤ~@@f&K]>t~үs:!P8_#Dz B \ӵY&v$lM&-TZ'T QN!̄rIxmt ݮIo0!n6X59%W 3/E^_H']Ge6; ?wzClAMGQQӚ5%$caH(Z67ܒο肜*dU5<Py%3.ςx""#[8Ӕ$C_hH&P.6ҽ$/hUWUĴ"|dK| J )!S}OZ7kxaq;Vq./4& QaOw^dzͱXM~+O{aãBKsd%Rd/B#KbDKbn,Z+xybGU=Ep~2Lj`ã"9D? Xt j:n:[ri5t:^NܝOV,7~ '(X_:{~?&9dy'ya8ّl&U䌸\H̼O"GR?a+ĥ!r~~ꌱ˦:n~΃~]MBW%h]$sŎ }wcn*L[ !b -B3~۽O>fO0~Y#_'5cXEF{ѱztZ?Uw_q=H.g@l8ѱKBƽ:mK·!RWK1L4D']GIϏy72D%RZ,^ɲ{ȋtJWE$"r^AL{- ;E?^Gr؏&f# .}5̾7o}XM˿tM2aWHnC;)`n'w9?rg-jN`47j'䳉QZjq]ãaXG?J%ii/fsW)&ā-dyoBнh&\ M \VSN2;^ }X)}v%;+t0;y?b︂d BwaƧ!52+ -N 2KvZMlm9;a_\ȟ5lM[3hcIH -JP#4toB(IDhX4 ( X{1>Z -i $Ei,°IBQBQ$,16ZuǓ?~,ǀaI뒫F 4\9)z#&Vij;Ts. pjCD8Ff%PEwC(%; Kc&T 0X(- J#zd (T25{@,zWң/ŐE`1~0;Q붶.lr")uED#LUBūoMzJFhSK2[y4".d|yNJlo%h*BmrI>Z`zlnsTH7CVEi}YÚ@SZ[HGD В&5޳ JѸ/Ǭԉ F14lzdءFr<)3MռA#}$R#`{~mv.dBZa7V+rܺ#w+s?p@nR] bBBTF= :n{O5($k3 ֩j< X>wImmؚldϱb|\\'qӏƆv\0; ݣ4Ԛbs)6(!O `$od,ݐɷsN "lބF 2q>鯄XKCf<*\KjXC Y&QCh∎Vx$1=퇵^`X<Hޯr`Ȫ`Do3-GEH DQhڍ GD.eUhT>g).F/~Gi}{Gw_j*%beDԱl.n㤱)^7ȇ՛1XoGjs{Qg'+H_JBb"H-cPvg~k\w,V/JG@ ?v!S)/L-ZVt!qr(= -te:=*]3oؗa6Qsb:߹ 瀣k &Ձ}l75b+%4RLjYsm`y 5 ,j{/=wGpq?O1̽7ܙܤfTI-R)*z#O_#dΓWKe)3oǁѵݿb]"۔2Km.ZA]ޗ塘 .kX#f?aXTF"U%QT;CIM9<$K} b|0~3}ǻ_ x,sxO:?'ā$gAU^X}>ҿ; @~qwZ9~r#3y˂F'ɤ_3 y$f$I$I56 Mݹvd熡sGN %Di= F{C7! p{EXoCzhi&ޓǮjX?#kq\x}x 뜣mm -?%Z ϓITu|R~D!ϒ$ĮF{4(:~kI9=Č:C]ۤЙUǫpܓ_$L:xQ.Cs;wm Bm̉ TVB b=TiG:$tUVup=qT-ޱ.H07A#=AQDX|EN|[rYFrF2u!#~ Š1!" 5:b%=jt itB&E R=DF)T- ŒXq#e$FG%T w9b/IܻYpjFLRtAdqb3"Qi`QR݅gEXToZi0$4$@O};zPA2U-/B ȕE [rQ3ˎ4HW׳8WvE#0nMa׶_cчO6Fmu.p8Krȸh;o0zūzES&Tu=/7Q=hfGic*Czw4B)m3I7B Wq\>} mXS0'p&.4vj 'IF/Ԍ/ۡ&OK7%XW-$60ЄTcҢ7ܴ)`66pD_A, ~2$aJߡjBQ,Ḳa.:mձ#S#dcW2DĮSِf@ǻ_%vל1{ 3Χ&F3GQ滍MjDQ [$g~~'!1XX8G$HQe\G>3$% l[T[g1nqw[]VH--&q a$^7"D$߇FՓwSN{wGb8ZI2d=&Y]i ;J+ -9'俔vG]97'ܱO9#e0|yľ^|(VJoJ6ֻFQqhoFXI~7balm= xs{! ӈPb,%.H& ?,Kr:\]owe\Uɶ8Oq(o7dC1-_A$ۨB{ ?ӏp$7{dFS!tvH[#-Г.ya'?'Sm ȇFbw}ɄNJGQ֓zB&hqO\εD4!Xk'OqIuE ˶՗9"3yQ65ÅZj"rDD,r7+0S"ubۂHLsR@-K<\O\o=1fY#x@czAn'`[; <: -<MkfKu܏bk=SM:M"MϦ}i"WD" "XG} JІ@IJ3g`YD)_gcH0 --/n\7nb1*$2eȻ?xeqIM(~GEo/Wys - -@CCP$]2$`bSSȑ$غD 6$a2S1LM? +fI)ǭQ21-HX8zytC9_aKm7ʙD^q^u7f%x -ws'pEg:MPN|M~%. ~yƉf]du:3%Sy$Х1%UÀލ4߰H6z_DV.;tzkluCƆ}$sԈ'2F9&B p{ _B|6ɧ+ +|::!˼(FZ&Hy"r,:"P^F ,b?32lJW>ahvN{ǡ[4z,Mui0(Кu /}X!$adP0i-tV1")( BZxR_iԃ%[cx 7jYq!'%@GݓUWDfZftZF< $JM'K9P"Vд"U$#'z6#6"~ٷjPFH?UQךN"\׍l~:z xODэ:UWj"hnDҴ Vb xL8ܻd -WTKǠkmcw7[ؓS}FQ@dbf(79עFt?w!j2A0"cZКEy]R&0fʓ:CC5ͲSuVŚ'E -6m ڍՕzubґjDEWEN/ؘ:ͼboPCaϻi&G[盯ьCNA%^G]F[-tܲϕ!#:-(Fb/X]d~zc=7m7/F=-Sθԕ]vpM<ϱ'6&_6DCR3;>I'H/Gy_a%0V $}1> Y'CҰ,W8W]/OC&Djo4I:UǧmTi}A\C.[I kO#3u1K7C:/`GiGGgKe -&tΈ=eE#F_HhtEW7F붉"{aJn>*>?$}dOhNSytG"b81e|m -juwԄ*} ~ ZWaA*hI3ފcSp)3b߉dıoǙ2ya\;:|`[9t䇁fa=/#O([OTd4Ԏgz -advFڰ=PAX -薔E;QiF#K$dt1 I ?PD|8%bm/bL $.>.A%*,zґbi)ZFM5z6>'RߥR( y 5MU^9Ђ4=@ ^= TV=c/4NƎu[Љu:u A1I۴t{ nwn"lpdk@d "wbH+tYxewxDžics($xćQE6gc[u2{Tfl-joI'D\TV8+Zlt~ܾ#tad$L]/Hd[Hp/a^6osȮtͯ0")!wk^ohdmw/g&{!n7 Xs"<{r&{Q.6r<2kX]^jJ޼.Nc:[VoT"=-LCMP$$||0(׌_#n}:n#!uIl*K'SV$!kgnb_v;'awiuFA|lFR#CLA[т3H붋I:Vi#> ZmWfF'LW \UHJr)rs;.GCDЖBCByvj>@)??Mm#(VNwۛ>˗(rHbl[Rmٶ;xJ~E^)JM$ZN'R ǭoV(VgR,{ ]ΙzcyM my3ÎOP¹F@R=L'{mv":r/w Kx,61?> 2gvH%Wҿga-? ڈX>*lbDtz6%5,'KĨiz$'C[ѹbh3HRP>I>rOVw7! sS/ v: e ݼ>[ˠwsZG5n%ѹ P䕾pX"=K i%tMDUja Tq@FM'[:`3ȑAA-,hHhCSSU -a/Us-4[%RzBu:IFɊM'WvGLz*KaH!9Bu6/0`KD06uI.)f7l!A[ȝԮ{uAz:n?fZޜ'GM̞d\z/Klm -(o !m+A'$/Iqa 7xbvc-I+et>]s˭͆,),nBU'4sY((RJXxb\ ӣm?t^mC`NfBڨLe2&NsWԳ6.L.Dq`WOOTs>fO~o.P_O?F~O# [ ߰^tGya| IqO+6#EA{_&L?;.ԸVz׬X1M3EzCIF:EE|SߡzIW,6ґogHC]3t=A<'$쾅0Zt- IvL.ٖ&Y Ec>vp)nX,OBھ/3-rxt~BdRAG/#~&tRIb~{n{ݝFAcE1lBBB"f[@-'^p~M ޲Ļ ۰Kgw>ǔ<89I(r dP>1r&L?̃MOKSg Lq5m*:{䋷#]{} A:uEnK!~X?SʟF%݆O7ljZO ~$!S8,Q[u܋7+'EtJv4l~ّs'cFELA!$BR#=8sNcbޕ%کm%"G-2|ȳ%Ņ>px2+M6 dE#8PO;GZf*myy )ྙzg{!I-h -nO2rBd~.'ߦZ(8#bI{wEg'TY{rE[DCč |7|@.DDGB6GCSgUks¼v_Ǥ붇GHͽ'F5~x'vIaљG7nJ9"ȂVݕLk/B2dŭ똸H% (e-EČiNEBP$?qe66d PZFSWBH thRЈ*9b- -ޔ Tzu~=;\UHHVGr)H솄QVX HBT;hz`;e3),r̶=@r3lz&> SLU~ՕT&EðՔR$2!%I!g%eaNC*,QHSwފuO)Md%'OmH&FZ_F?b$_: *jgaކ@e;S9)}Swdq$Dd{LO 'ɚF?',d,[? (,_~D&I_˱=v1U-m\o ϋOКk?ΉՄixJ+5C9qpDXZm1LA^s$! -x|y1YߤC%|!ϥÜg{DlVq[px4b朱s`=K5/2wC|d{)|h4,[n{hy Nw۰߽l67_31"M>oه,4ѵ"<]Oai$]K#*[0qOq4 -EM2++Ac4ͥn9{c^|? l坓(_ %6*$6swb1=Mf鹑#$tEUZ?CAӗx= N3o LW?Q`_GD$L,e:)E- ED\ϑokkl f_<(7rO&_J_8DecC'c>k݃ @iN!Toy~ hJA+2z T~{n_`LPNo/)b >'bIh`4ԱL-[zVN7[UVX& 9o.㋮GWd\u݉2p\D(b,@_#2Ą; 7q$Ґ*H+mcX -;[ -ܸ -†c;r:Rmw'F&Jhko6 -e x?$nh|,9z`B9ɈXm-rw%b]IE`YI[JF{Ib;# vf/ގDػ]d8knaT&Obà?$] 1~؂Lc l"ܒr< -# -6b,GɌ9MX~KgMG!$|6FX6H#ڛޛkM:n1Hht6^H = -cWQ[(O+ Bp:e+D%JB$21B:(]A)wfMn?[VTԛ*54EE Q!Pn%ZC#b HJA"(ƽEIoADMXɶ/B ff!IbQi*MTz&V)N=F;ht'TjM~Ѕ/C {{1l_Ȁ\:+TȦ@9863S/u|ź r/V r{96:!bN(l&%arI4. BhmrL6wtub5埅nbSMlI6ԋw&8%F+;ercW~op'cz #]\ -.,9؍T-¼ep$Xjzm+]"K$\qt3X N7F$6IРtp^1*,VIj.K<%#grI]Gf\6 ,󱲈~~py2<[S7 ~!m?;&B r)2#7o)#D'q;w4y6Aqbu#f"~ngE{LP큹 _cED8iI73(c0JOKQ<?;߭,KOzFwQؗk`K.S9OqMeWig܇[Q츗la~oژ~pGX7(~#Dgǫ5Ɔ&Df7cvPL o/!S2b zQ#ROzHfI/TM55l;*oy; ڍٮIvB[1=6d2L'DerKXPO陆~qW1ݸ$~)g6Rww R} &yY'@/B@I4tU/K-S˫ƴ#B׵1V"Ċ$F+*E͆2؂*;!BB d""0wRBDB"BCSR(P !D!]Ý=bI2#dԊF蛏.ZW#ZF=:VuzvЗꅣĴ.`Uz2DO4ҒhRnd&MiZ`z4պ?NimD7Kic1b-+NڣNڤ:ų$Ee4äzz*^-s-Z3M/ίa~4=&:EEΈ#Eص*Ɖ&- h$B2I4Z#RE4nז*I:'*$ЌjcQmDBD}Izj:*O4L2ԚƋF]$֏j/Et7nF^t?AQKԹ%5LtpJME!GcґKFtbu/ERif2hCхTbdDtR24mQM>%j2"Tb#\U [z&댰ؒI%ig -ȉcGjڈl$ToCfF[kϫ$M`5=TM$ sG (7HJItK8M'T^މ2Li6(HKU'Bd[ -I$C$nFYNj C"M/Y;i4DRNۛMR14 5Sm7i$k&h0+H)FWi"AO3LXa!7fS#$vLF?%oU/kBW t!] +xqq+!&s)@8d.9$\rCd6f|!4[J[pni܇?KGݧg?5DddnKJT:ʻ @ionQ?Hk/?&R9'D -[ICE)&O/zg@ e*!1AQaq @P`? @B(~b. R`us*"Dc:24\i;DSZ_[DTŇˎH c˲ \R4^cVXAjSUpX:K+Y38",R̜S%Wv^P %O3lJS;EKe9en#K/XK.ue]@Zʃgdd%x|s#n˄QDDE}5K.-b]t[ap.r5ȁ{bb">1u q/?xlr$O1i:ىK d݂2H>`. Ñ*Bl|.]WL FGK6KFF eڱhߌBXCdpZCbZT%(/Y#5/gR%2K.=1$3^[2Y5,|l+S(beɬYQ5\p] -_Ws) R;,3 n`TVXt؉<-"]C_-1:z thx2]ej;TcKKvC#ŗȾiW-M%B˩w.+LTN+4'YpvȋNGbA6kv} 9l6kK{,v5.<@XR@SM)ˇK<9)J+!plՖ \Q-%,{cU!b25ڶ.NZ‹Y{> GXGyPߋʁ^em]F|DI3kv .V -Z+̺`#oĽgbϸ -qKD Ip6s#<.^fe5/T3KGBJuN-*vGQk.^/_̲|FJYQ]zQsfe, -gb"me(RSd(Tش,W,b Wyu1Y%E-_Jd*rt(Q)3n څ,%gbе ۖa)!+pik^5jy,g]Tb=,TPۗe |\i9<˷/RQ[_s\@`6㩵S&x6D[mJ+2ET -*&(R[B,9,";6~:v=eă tKkeUBe& "gp - B94Aq Y*QyK0ˢeZA;>|M1!ٝy\ G'?Q䬘M:sbxE$Zr=#[טYF[/60J:r`B,)8(>f Y^ :UB*\-g \bnv? -#ZTU f6"-vup=UB.nY2 ܢ"e8"i0`P /tkȋkFd1AA,D̍]T+>7܏nYr1<$zYwJ -Bš_&`0T\9~3fCRc*c --kp*sN/=&ّDQ(ܾ˃{g.02PJۘIDgԮ(CG[ kf* +E_FO 2"8ƣq4 -mY"9S֥#vUDWƥ,I(1ܵȉ<%)c -VFl%N2% :K+eF9|4%| ][|Q>*J=ʧ; *2i"eξ-s`\E^\tK7#?SDLB@ s"f?)]Er*_n"o#Jj0?S9)`#q)nF9ExrY<d^[n[uPiaٲhȯYkQe[ rG&TȔn#("*A*8H'Vskvw-mhNӵp -BZ(PU^%ɫ J -au+!aq\ZCl.t.OrnUTf; g!: -xmO=TWbܵC82_9{cb"jB2e"PD.q|Ty؂۔e༟|d̋kp<~ 䥰<pHSJAr=f#w-7<'.-栱)RE̳'iKAQ qc8%UP& DܽX )H)?}SKk8 lDj%a.veBX†.ƣf_y:% ظc9Rߏsk."(fKlx*\M-Xn -{LJ*elǰe|Dˌ0 ѳn*k]e R l#dY9<ʷb -V#A(V.(dCOQUF1¬Z&oM<fbkD -yd$3ZL\Tqƀ/!V͋P4|7cw|KaT\^!6F -ge[+Q.(q;OWY6İ,1?oŊ05ly,eʠb*TF[ -J%遷}\TV݄v+`@Qs7ȵPbEBt# -`][Yñ8T@ԍR6`HT}'ʞFԽO rA@;i|]Jrje6"Q 6ضn"+X׹e&_.Ta-rFe 0bTr&x-rtC Q#E]A,-5؁LVP`VJzMK#]BU`/ER -6XuTTƘe]Mq "Ȧ[Ɩg?CKEI[p}+fUZπP949nC'a6"Yp؊bŧ&C*Fnug[~A癴^€rv)(eْY2g ->.V&T\uK c@gB('ht.4UF 6PaUO6kQVt.!j啋 ZJ%3q&c]>) El uj,9,sBV U$D cLzORX*)/!Q`gk%0Qeh@!UY˨\yk|%Z0[:୏*H\c)yTR\ jȠ[.C;K eJfDʕseB\v ۖ](Dc c^6X^K"*1S^3Fܼ%`WȫW3bj[ZeK"EbQj-SrSg7!+: @ lOQS&Pmv.UNJ1τD%? D̽I G. -UU+"o%Fi KUVͬlh*ڿqJ)8qPZۍ{ q |W+Rʼ Z9: -٫/.0^T<-Q- - -/`*(!l e4y*A|Je DS:MDDRS4"fn9 p4ȖmHۄP* .(ŀx!v,$C+6 .Un+2j|]Nbog&g/-.]gh%n,F:*Iu,\VT*"1nQ#"ݱǚoj>HAMr+zJƔ2UPu"| $Z[`D]W>+!30kZ`)ͺ\%'nԋ -k.>[ BIaݸ"v6V\#ggG*T< -8B, mʁڋl*(Dz>"l Yo>XPCu<3V0]Nv+6uJG'A AQ*%l<ī(BgL߹[ՆUwBD9ߊf@jTvQ@pKJo(`\u5 b{ >%Lj07~]hܺ+--KbN'8O51 >.%1@s.IgRv)g7ԯnReO uWʜ[ rz׈ llbN*1`A^*~[vDLE_"BxELOk,lE6,..CFZS}N1ωT85Żs -V> 2wu*>7aQ`F6\`T}5*-Ͻ@[.ޘE4SH6Op6Kmt^dEKv)3J {-:#`~^@2r 扈c7bբՁr 8>ʊ1րe#+Fa.ʕσyU-5fl-9Qsei-єuD@[,ioeJm[[/v}WA)nXE*KmyRSgTTZ%J}J28dȦN .M\KNv@ wi] ba;P+MX"2"ͺS'CJ`Ky >1K@ EvfdX+B`e&ޔn6\%bhsׯSm65x$M'z`&R_ T(c!j{vo=p1#XV"VQlض:@IFO=]J[3"s艐e۞I3r>3k%<.TJ+&ln7Zm K26A}ōp/x Z^i\ Vbꡈ,2}mʏ(ׄ@kKWeMjp´W'~'F yG2-Zl-eT>"w.)wpdu\pW]`PGL5iywp ޶Ť#g/y_q=dۢ_s :JM`D0?ہABF硎?owxsu96ZF+J>15.*~'g5(bql`ħ`Ok2adƮL9):U˙K')/x2Fn {PĔ/"P<Puw-ME~X?(YEeMa"q -.̶D87'eQ?aVũ¨AԮ,aw #U\bۀV%ʿV"C9D>sdxHU}tƭ|TZCn ?ޛwҘ%;TJh o(!@,\9JB Ơ[^jT}ܿ?2ؕC3e~KO2<*8W ն.}C c , -e2C9/j[q/.qOQ@@>.m\܏!n yvg\&Ԥ~Xcz*7*pGY oܦÐYQi#TE6?e}l2]y,$k6Yh.WYPGa)!x -a[["h^ H O*H9˚ήQsj]ŬTaB7R^5>7IJr-e :y+1S(բ ѱLR,=cPM -mʊC .rڛz_Pc*:G¾K۔R*-vRLUyةjW_̪ύ{+QduPp؏39 ~! -nމs)XA.Ψl UJ\ԯ&2cw?hdrpY)6_WlCp[Aflr cT|Uš!% -EܻڝgD2d -14AB ?(1t؟}2-vyOrF%P\#CVM`R -;8[7+rx)*6חZ.]-2 FV0=!,}y\gY*︈0 :bBBQ[Ь> =4cTּ~|4(}n+R1B FpʅJwTl3Ǟuʩ˛kLg~\SYrc')b "ʏI{ryC˨y>-Щ|bˁ8K!]M@DD%莜BlȶDU8"ű1|6 1rYp"p9nnbKg`M :GLo`J_$n86dWJPC)ĕy[kͨЎn&aY^ı)tU!غ=@JNڸybz`،f%z%c;b_w`2Wopaϴe-a5P[ܥŶv#b5CKs.[& a{^UTLy\Z{3>a[IeL\v~R,6v2A -XR!в_nd5AܬȚ!ߋ9=[ypm~ONŰia@}P1e}?+9יtYǩ9'n T.S< s%`Tkp@CUo/qe[T/|`zAqF\ )/AؿB-b쪸,&z_7k;lWUN[2^ |,%kFZ:F(96=O'b?iTUl߆JkHٞ"Z V0T<)q3gl"m<E]i*oKLLRWCv"9bhq<JAs[_ /8 - 7e11зxcPPژs)[EC'ξ՝kWy~w -bG>PԢm>-H<6Krްy㓬ķ?zocm ٗy8(1=_+֑ގG Qj -ۗ3~訁I˞" pa,ԽH{yv̈a`CRPo&%[ ar]X"p% C.רQ:>߆'e IizNcp1a-JC Ijpe@|E, VʁڢA%lT%Mkbk[#V+d)50iIqED#C"g(HQ|=lU -࠻Qi,nrsy23/̓?lǧ+~:nR56|ZKo2rpovF={BQ2d<"ih'y_:~ʨE>0rjHE;y fm&hr/VP^C}:6G!rbJKV1}:j jQlk1vgˉeZb-vՆ4x I_>u%Lx"_IdXU*}GP<|(vYO2,%|8ˎF{eƦL:<ѵԖi~QE,1`BaȽƢpNvwq5ŶM KC[15.n0˽l2EFNEm œ ~s/MX۪B -71y A3#~I8m[bagbW*:fѧ֥p #._Q͑XQE -mMNy!^'v-C- 6zZa B%|_\؈RS:Uy4Ej}lJEG̏>+"n\r<?0X n?D|3yc=2"#-`1gg]aU6]s,L;).Tޑv3`@j) -y@Zi|"ԕּPJLT1%:RAkeB pR[r]( -J״]Cq$v<#]ZW M 7~P?#S})F(jl4@kj^8j"eN}ukjf4)99<Ő -h+qEgJ'PoedbQ4KYQ6矆<ihyED2AP%TSp}&ur>+r61?ܙMUJ<ܦ70 -vYckE0_n+%.y';( LhmeN\īkczZ0Ǵ2>%tƣ)F-o -\zTxs7!},4-2NS|nZItL崾@'U[xmE|`vza\p!W Q16P2h~*3v y"ʉY@`~}ˎ*2|rŖmBJ.͙-;:b\Oǝb|" z'PYIV2lkY<_S%eȼe ,{a#p'(.tk3}OŔSߒӂ]O_iLtw<1 ,Yl@?%]W);yiʜ~XVKlH-#s"8-źO7aF9p͏Ubj0`Z]?Rq -,PڏX>*9!TyvQk+U܀B[SJU8vUTT{YL -$x 9RRƆJW%[45u.mX'L`63G RؚܳZ/tʡX}G + ݌X,iJ7wPK 88[&R#fZ)Śf4HO %AP98~\`m7~/0yA"DTH{'n1mQL\ˍ@ ښ6+eC0OR,;*LGa~ &KϿ_ŗw\vAPlEAvG, 3S&[clt%jb͈UʗͅT^F˃g*dԾcʍ#٘Gw-<@_7dC#~IdYڃ{t6б"ax* -7d[X ^.iVJ `B"4 -5fߴUCމ 0 -&:Tԥk \5uԥ̓rYY/K= Sm"73& gE '<͗pb_w/%2Ww.I,.e6 aBש,xgb.hL\RbʞKJ'(7Tt"c2^%`d|XvmP\A`#B$kĻ`Y_JR8[7⼐%0$2FDbĀJTVa\1T+S76e=˛qKAo}]%8/E~"*LS6i eYXmb? ʕ)">b|²<~ BZЂXŤ6DJİc>vT Hg=GP8, wN !dPW%1b>݅磢1S]Ԇ؍i>a5% -ĢG.cy"#k9BPfIkqO&.W: qnϘEܺV㇒qI>Gn`5WLg~&#k9ێv^n̔:Qw ~½Q -J>hlP>*=A/b`q݋K,PjQca -˝b+[gp&"1*J "/U/qj#ѩ[pgJF|+JmcrD_b֠X@+*Vφ ciGW|Yޘ>@DQ? Nx3qL_S~&_6vҋB7PH l6< !_l_'a䂂XKc's?|R/J}Fft.58Gp#bbr 7Q()@q#AN?CP~ --3[Uҿ^;eߗ JyWʏp[r;N'ꐖO2."kiwG+%kypKE<KTMz%򽟔Q{d#-h&fwjm7v>/n7_ -LVH.1W Ck ] KPA,5q/J <ԖRK텾 j;.Tk߽.6ye?ĄJ!"?hj{ȮNw)vꠠa!_ (Mߤ@RbN\|E7/&&%?qst?(xio2all61d׎_Q#l匒V"Iwkn [F% p < 6OܞzV;PA䥑yG] Wc2awTD.YM>+ YAN!.dKZK+gNUL JG$J#6p@W Xrֳta$\ @PU\Bޣn֬j_\tH-Rc次Ҁp}$@b.9("aP~~[y7q[t>=DK-c./V"uMʞXS!_3 YEߘu9y*؄j j+iJ5Q WvA ``zl:>G> S-U"WH -bcrədP7!F]tV[]%V[id牌v  guN %SғNdc+嗇*7 -y" apgJà#|t1aZ-hY;1 (KU\Y-Rʀ薆*QM Eڹg]h#l]MKCSi/vw];HK:5 "6F7u.$6HY.S"˃Q`6Xe0ZS-!o,W7#7< -SC -~p&Kٞ!eqy -SK;/Vs|"TJzٗ71xؕQ7j\Of]g+enJȊTcifr'hkJnm #B;Yq/Bj{:cmA_R 멣Hܚ )er+ҙbcFS[CPTrwh5Q>݊EZp^Ōh>\,2@[f9LKɺ%R+T.kSY P|$|kGUznġ6_܇rwʗkJ)@PAܚʂ'l}8 gj-F{ {~`ͰYQb%˞J>zEY[,'f*( ײÒW(A*dkEyˀJiQɨ ba-EZRD%E&, -Wyf%-Nx࿁b8L:5 Vƒ":_bB(͗mO_ܯPWŊD̜%2$_yuw?jWAXkfmh MMkDL7i g1?ێuk1C,.%L+o~.`2{^"\DYcwUX4aLlMj8(5Nc~eunxtFUKe? ?@ >)x4(%} n]VD JʫS8Jd `SY+R2b[ۈQ;WJfn\pZљ>c͔+*j[ˎl+}RGg>wwaV`ƍH \{пNBS}N̹ibȉ -#oS3TvԸ QSy_*C[>,/cϪ>ȫ?J,>D#@ {+jWPڰȱK+[/ʉL -ga%X:x#伲TqSQjuZ2x_#K `F oƤTǜ0qLm S !2ka$#KP%7ǘi`Uz8G/,i]}\ҫ kp eK2madA\ߋhKc!@JX0 -"B'Hzgi6B&au@frSF|l##0/։eEw)bI]%r?rبaUc .R;_3X%S,ged%.0QYv|:JΓY.f @Ð'$:AaQ؉KX1Dపe+2xO-Q2\W*%/P\dEmJ#WU-n˨Su(m - %#czA֪UlqK \>J՟z(/Ov^B*XCg'Dr+D\M(ﲖŸD"r C/ܱV8DQu!W%|Ti1 O-*=W&MX.RT|Ddtw@0(b|]ٮ-WCF0|O!ʀ*9:5(xns./ULVqRQ4a!~[r) (|@J%ʃT -ur΄<^& 97b] -V@=PrY0cU~+3 l_Ʒ'Ľ#`rdv#kd%٘qJ&\RmzG!Dx4N!:FVAv WENG-(+!]+ue~*ِ^O+)T|yiR4ax*>)5ȩ8JPߊM@Ig|\?$TlMy1X -q.V3¶_w!U|rXۈ.ArɐB^Lj9a] j,' 0IҲΥe4Qr*-50T|/ԞKQÑ N5p˹:Ans*ɲ|k.% tsaq6H(DO˶AVR[:K(?02QU0?=ьUPy+!?fW/ȩz|mƗ.ƊRݍeeBR!A\)s/%*r>+ a -Yo`(r)l.8*X1cn;,z]\Ĺu zv v,W(] -7܅{MKDU1R[[ yCeOF4 \->j)qv(Xu]qL^|oƁ._V,nOC-mʢ)sK 3J4*~E`V\)rRB@p<4@X7/D:n)t%jǡjxDcأGqV^IRTSb%/ hu!&~8ˍzԤhJԠFnuWABp -EF WPPT1:}3o NzYRHNv٧V>Wun)Bfa;H]C[HYKKVD6 ZgwVuR -J"+P٘˵V퍇Pa]N6i}Z&RG&\KPikqA*!SQ;* 8KY.THTĔ P@O|%@meN~Ro.GRB~BI}G^Bv}RhNr%A( 7e'v$I=:X)NnpQʎ2*pS]zBM؉WqQa*,[?Ranl_R!jA%|eKpKe -٥eزeZRݞm1߇=eAeB4).\fo2eŮlP`7lI0ښ?qQh-!^E!͚GeHexgiE^ ŝJT& /fE>&D_N- Uqp-qll_OzKqe[.-`"Ĩ~og) M˸{k.:c؝ʉr+KENp1їiLL[0'K@eZJ{|DZ -ƻ7lJt([iKA/S -`Q߅2M pr raSɓA;;.&J*Pe˔ve# -6Sf[o1gR@) .!Q[w˘mˀwY2UvEAR`j r.\ydY -|GWR{: qdՓ-rv$'az[5qr693u{ ݜl іE@g'< enMY+ZyWĸO0)dGZ!GIKC'Zsܭ;o˹̏e\TK3eԳ˽joY_\ڹ1QA擤)D -/bL[rt^J A[(Q.WeWg5-p/n)#.]PfffhU_s Ů1nEȐK'ܺςu|D9rꞰųx+ %r_œ: ŋDFW.ڞb7a`%`RˇxRR}ȐeQ۾& ..1nlBۿqfȭ5KGEkfLy_%J\O,I˂ B!j/[V{f|7U5㌥r{ڜ]K(7ZYS|~.v\5ݛlPie|bkK"laæVn a4\ZЍ_bZTЗcK1uJ.7y`uAEQy#u_ g -6?Lld T 7I;k)Z ǬE.I ^]W|!r/qVX vܨӉ&[h?#F%~*5 B0irejl; +kK;|TEf%hAۖ;:l,S{&L}KV -s,b'"ECJ֑Ε,J]Nr^Ɇ_$ Yw%5D|J./QYEK`Og@7`k"pulSv~eǷmRu:oDSpYc.f6T IuNLX\a *?sŜBXa7 -8*%LZr) LX:unT ,6"4e A'&r0JE"]T DOH L *UKڬ#q!F u=JwpVv*2˕_d^rǢL x!ڈb* lKQ"/>CbryڕaWU+BdUN!ZR bK QQl/S)SŽ8B9;O* }@EYzf%Adq+/jYQ"Ke Oı|ش\+X0L%NNwWq`K;) ՉQZ.%VRJe2Z$ڕQqVoEa(]BrC{.zfOE_7(rP*E|vyj)+e5vj-ʔ`epn"lByb,F<ώ4PdjظFusjt?P9:~"rRfvi6z!:쫻<_$SxȬ{NB -iZZ6ľ1 +cYC* ͈VWE Y2uD Kq֫܆T {H؂/72ݕn$Gd -z䕫V}!=/t % .^uHCUCDs -D?pW^l~#g?];UX'd&?IKyA}Īo#P=*zNSi/TMK\Uem,dIq[/)O\%׸,~hiPO0Y-!{ަ% 3b %RMa?F$_ښVFK;b ^цʼnɄ*nc>(L,ąVE{dyy2_s?@~/)ְ~h=Eb A֚4z%iD~${M]_A&Ef#^vp\* -pRhGK(g5V9bc%7p'24nLC~lAH]Ta,*~f \!#w!ط*ߍy>-UXr(JR?1niD:! i"Z P9"[+""ݩg ?p(˅+7G[ -yێ|*e0YLC3)dxZSː> `WG[ozց~T:) yPjfjW2Iژ WPaAW`;!pinVٗԂcpҡ4I( DѢhxC/=aFPpJȩtu`-J 켨6RDYs.5q'څ Mlhq]di:A{hPu:=VC.Z ]\;j (7g!ڕ1X>/&vX[O?xJbeTK@aʔ rZ\B-J^e /J\i?C_3߲;d7Xt2im,j;+;X5TSFYWi+D(Yel "wyR\rݚx*Ymu. |^",fKYrP9'f-kܯVܡcp*U׉I(bgedTxʪآU.Ǖ(0l;/efd):ƈqHKV#M:DZib7HK䩽wP)ȏP唪Wܢĺh!ҟ,4Գ-c0z\5f|ر Nz"XaE56WCrl~%u& vk1w^"UM'Pr+|;H/"|RT% '(QڍD}vX7[KyIG>S%*lTk~{`Xf -N&ҠV7 u Pc>(]" ڀ ]auT*"/YP+erǨ2,W*W]Go+~-l2ԗuw3z!0]/T380wJ5cc >*{#u"`ңd57t_l{P+n#nJ:H_]ܼj%E}6LiО@0+S֦QwojJFU MPUOrygQi|0 ͍"rJrO˰&\fe0yj!wP& 9rHn^L /¸o%*q7> 7,n&MsXńPEP^)*ӑP>|+?rD^go|mxP(Xp|)[Ȋzae°8Ҭ*Ws#mdy)ӧu~||iQ CXIcz(TB~1%Ց-g+0]jW֯ ks7;b2嵑_˕}ɨݞD52K/XϯXue|z` ٗ6Y])q/#eV 'mJF7*<"spK`5gXuŀbܫ+ar|uvz{f[˛Q7{֒a)Ժȹ4,Zfi%o;q -/g) [ɑd_cU|dL݄d# -x0tx?r5>RWaUB{aJSh)R~gENupuO BRاBrTgѧEV鱚0]#-W4o2]節mRÏ<߈gj2)~T[ga0-m1Qơ(=͉eJ3t@6xDvi/'rqPf\ Idy kPEdqW# -5ߋ%9Sv7);k{*ls>qה*mcfʕq^ -),,! -sjo(9%n婀yoUf^0?s0JWAy'cfo59S l~H~ R&?J$Ba˖EV|h\-NG5` ج!vKj씺ruQPipPr] |\\zF8#w/.d_>!A #Q<\SVx&Pćy-We܄Ci]6-[̕&ŝ;(dG\܁JA5C 0j=M ]*5rd@īp% =$֗_H|bKm|0EOY!ױ82 ,,qPB -W$Qjʲ Z9P5,s<9~k,۝!W 4H%eSYwpylj>wd҈#LTTZ(p\'16k>'?)d5n_S3v"[[ɩs%RP3a8r%ؿQ_;R4~JpW&*Re|=AY>UʁR@!߀yX- ]NgRR.-cw*c;c4ԑ67@_S-M'$O G=EWL`'iFϊh;Ռ1̧_Ak Gf`65s~huh6TÕW߃Ք^l*+,}\<^Ÿ -LO[Po"Ի8ߙ+xS*=b+CT;S 2)SOvػ<3ļezDQjBeTW,x͉p h>9SS,B>f9+'#e@gbǰ+R̽P,JQErZ9,XAˮJ_QEj-5m. +)%AhKXpU_{T[WIN*UDІnUTnd.K[K,R/Q/ hĶTol#3|g-Nas |WܕG ZADRЋsY5%F<ܺaϟ6 :xZܰ%-?~Ҳ r_W -y -*P@՗-L*>eQ0H( HyeTؕXñP[0yտt-1i -ګ R{;zFNj?QKjѐp%Ծ\T{X)&cynDЄeE]f۱"*\QA6@W[c2"m| e -~ e$$;-Y qTZ."jTBxϪ}RvYnE>/.㈏12n[l郋0XTb*A[%@ש} WmH\F5 j,ҕ:'<,ܶyJ0|&vUvxDwuJX+Qʩises~&'EK09 B6g#p|Lp.U7\6ESi?QUCAGh6W&`K[ 9qfdۨlp|%yړ_jgSc۔lǘD~>V_~@]3o &yqP(^^@%n8*,B ۿ8Tl#,/*0!A,&x@;fD7D_zԽRk)!^M$( )bjŴ.E -:; RsyH1uM"MLc4ֲ[if-yx_m7TԼTۄ-&T} "h\/?D߈Dtf?XtHXjR{%@+uXA_7ZlYۍ&AB:Z"!_)}D˥~Y ږUu搨*Yk4F࠴J0 n1Zh 8TSε67)mqn*iwn), ry@͉?oزeYBٺw6듄9D 2Rrtֱ|bq"2IK!E];>@7oc .f|d;k#PRV-]F>r# -᧷'l⮋ʁ%ŚI \gԣj!RʠvvʍʹTAj־Tn:K6b0}o5Ep"EWbKEF&%۷*ʊOf`˻Z`4.r!휞*#sZ\˶#rqQ$L[hL 7?M!V `5U x+PNK֌öhVU,(L U`4ƈ#RND7 SڂQtLBP-E}D),gڵ L۵K"~EJBv,\[HqQZr-%kìxc.yO #8P(,BXTg~Pwb~[9u* r}]AnM6ξ;[t̨Vm3Ql!ž+=vZyAQbL%SnrhvZKe LP".->U_@&A+̺řDvnǵ7#`^{na^+0hXQYQT-KO:(`h\ZT/Y)yoqRlhUDݘū!!ܽ)əkJeMqA!6jPTBq1r`2x!dYb{Ge , oXCjx[.%c''ۗ['fd[q_,&i%*)S*lYd(|CY-QԵ໰mGvfGJ\_oxT7*Y鋦Gˋ,*Wu-2cԼE#{/okd5WCy8#ID`Ʃ6vB-у,\UX0 -?pVay5b"ȹ6{VZUd-:v&2Sq_0 z~x쾓h$!# -rZ ˶]\<܉)Nd -᪔_y42W`3r!;pnnlaMȻW.vU2 [_Ĵ`&r(Nv!bE0 T`amK\+w%B:*rP>*/z{t]֢6J -5=1Փg{͐2ljE'Jz˓ʽluRTR|eEbuӭf]U11!iŢ -g| `GQZE'hQhM0q1%!#*.yٰԾШV1?URy~-];a9dgYrγ%NJBze_yGe#*bʿS-L*-uW,ggû+ Zj~H>($(Ũ\5n?m6yJ.Th,.Rų -YF5jbX'QOPD* -$yDӵD{4kxC6?[rȂ^pmbz_@=gJU3XmT.7>|1 -vZq: EP -`N6 ;m:gmZT Ft + -u(ę?#FUe\DW*`FesS{*.KyP`|#*=LBMZ}cTA\kouA?B9 -h CβSlr^ңQIHRJ %Ltu0vyr+rt0%!ر K]g݋Aթ\*7 n#_F7r˻Cn~݇VKYrJ E:sLk=3+dZY<x?2doˠJ~ -`_2R`K+v iYV,b/͂-sn(V~E*`:f[ܵEPy@`X!?R0%v/]P/bdG\_QI,</Lo*#L&O n-r[r^`U2D^Or"2A@jqr ucRaUELgTB!tVP̢4 5~N7fq„"qoUs|ݕ?ecvp&.k%5?=M6ee!ioP^? wo6 K8`G6Фb}#¡ዩH$LHzX_4mZdުu]6023JO Sw0?p:=*.ܽųHT2vxJnOnu@/Ř8^GKV-Ru[QRj י$/<\B@.`(j0ADxԾ*^c ^$;fLJf皉b%ݱ#a^%--rܵ>˓]???[~{K:w#;PaG -+ŀb"/z]䤫(pcIO 3q/C,N<My`oI? "G \o.+$DsrgZ "t2 -55jΦ 4` IXVZ#F9ˢdHR~ VAz@q.T;;jLJ %׉pd*v;~h;Dl/-IZBYt*8 k~ -F/|BC+KsC$>U 9)rS+nT5R=|ٹ-l> _ʞIa8ƣQsVUMK_/] z01(2ؽ -~/[=\ǒj0U(:ѹ@ȸ%<R[ņZNya>Y q/̍-U#I%*P< 5s~OV*+TÐ( B;,mܸ ^F/$DK^ceBNy :QRwhG,"*Be(6_ s$mK2PVĢ)sYe-{aˋip\Z.=T8K#\[XY wYU9*<%%c |J#]@_fع!o0!Ր -lF*&"<½Ij$ ^KՏz1c#QLK -Eϻ*ǃ̵Tpk]R3@wp-r -m- YT<$TNe0y˓^zQG ^` 1lJnKAZc*ڞfQ- %#[,:sYUj1Y4 -T2_BA:P.j%j(@DCI~嵴|*xX#p7u3A9 \35 -tlqbӗ텊й&܃e|9&a& -D(`G!\PUl\,Yⶾ2Ȳ/&_R}hHRYKac9z)u U -'c -~KJx&2[W^c⬆x {Q@8ܫgrY]?^%,ZU{QiLp Qu _im3"k2cUWJTf[+]#ˎԬ ,B"Ep 4CCG -,,)_nڋRhHĴ(M$V-vzB6IR{0e]|ι6j͟ Pbv/HzE0&VI R-?ضa-)v -1 ܘsS @IʩukMpPwDEq +? kV`d@/_Kh.W \El3n>o(Oūmpc w,i-e"_Bhfae0~>`4'˄[a"sLd,|Qw 8˽sYi"(^M2T.cL$"L -J2BO-aea.+Xu8ofy%D]luԩ˿؀+B[7lx("idk!^.q R`d%m_G^FNʛDzw w%bRamh.͟5ɩ8v\ۗ])J(S -IWU07)j!Q܎=f1KK; R `ODm%£z#ى4 W YiYP71UyǚYYGJݕ@,;Dn@a5l4eLSRW@x A*W}ƈml76L Jj-}NƎ@ѳv; Jf"C@@5ĖrY򍾋,{D-#d6Oҹ:DX aêINIٵ҄H§;rg-˫?P A7EZMP%*FhTS.iZxl$oΉqCF$V,|%ԡ =s=WiQe:$]} -"c@v]"EwMڤA`X0OłQ4K@pN\ed~ cTθAmGA!¥+vtFR13`+Q7P8塬 P"<M*ZmK X- ? LѥɃO%Ah95D.r帒)2Db -nmvcsOհih KEbHhߦQLw➙EM"E$kW/J -J.H0bO({H\hjU^ -(>4vG-xAn75RaR6KUC?e Sg(璟Inی)ՑBW4?>IaD42E -g7꿫kX@ӆ^^dWB1`ž6Tu į6EGۉh{,:D)mjV(QlXˢmph6\ XZʨJs_pQ +><ʫ0 v/EE#%YS}TX"?s_Hj6U J2f+ʞ&fQڀH[Ua\%4K>@ԥԴؒƂ7_Rb^nV`"UnC-:z>ȱ=5Ң݈hUT37eL3C,ph}Iɲ #fblK X{ ;P -]~a -\-j߈6[gZhļ+o8``C,{5"BGR w%=.vQ=ڹ pE*0KmQyW%]˗؋̖ļ|Pє=5(d8K+Yca~?%>VY."|Jr6U*(n]Pa3?B^S\Rī톙%`"&=ȤsAWB6eWV*+(>S]Af -N㉠[|Nw#Xl+y%q:¾0-9 D%`UFI?8~Iv:BViEt6°mO/I)x kT:Zxu7 aJ'Z5&%g[H V6^{/UY>r$7v*meJmEbC_gg:T W0=dx px8{e>OTVJRvϣ.ȹcw%dË 8r*PUDmpZj-<شu)m8"DbIdb`3`dhNCIXKp,4[UQ. [U?rO̟k 7k`T -:fYS +8,ʘF_#-l` r඾+`aE=1F*XrQϨwˢ$&EEA. - -طQNA;>>u|ԖDQɑnexq3.͎Kb&8ҳPCZ] ՋREpo_@H.iD$"!&\Y@PLjT-l", ˰W@>%DZjZmJYdQ{K`a{/{D3{̬#e#H+j$CS N5T/)7#臶-MK ^LhT8Ưc|a\kYW:u/W ،rU\ؖDyNV|ɦpķt - K Y~ jŢ[V[PU,W٬~%F!Gr#]ŎSR̃v6J BpJ\,_,Qȝå=h@`_'hmty.Jg^/%J*cPZ/z}L6yD/-r".(0F q}"3b (Z N> ኶n{ -t\/i%uy,.<7Z[Nh?UmyOʰgV IJ}O:i!p%-?ija^$ V -^ تVNDd4XNӿ MUiȡLUBVA?q<+dpAVrЙݧ*rѨ= MA^KUK+S(M,^%Yv"Vˎur!LԨS4B2d{ ڋXgV>-?6w,|SlkXR&t*5jR ~8zDeQJcpQAE%CSOld.6 -[6 شL6wn%yOL1urUEKIΌGI0JX|# kM]t#/=^ m>˖T -{nWTf[yq:jR{a)fK=(D,'c8 e-Xk2p HKfd!Q}&mA{MJQ%j1Ҋ ~a7*w$""iQyA,4ا9N8+ t4m?PaaU7l)VLb-/DPDq -kePOj^h?L^M+`nxzYy/'rM ?3Lpbhy2F -2͢Ӭ*E'޲R?5^bB9edBE2/ODfJDF|C¸@'AaU< v IטV^ެNTKJ!-5U-b۝EWAqTkRhpb_XMCl`G9JHx my*\U6]K.ls*XUŋ[/.`Qmy\\Ŷ#j_l#f[񐙥1=ޥצz0zQ5oWTJJEĠ=D%s -O!Z2 R&(VF#M}k2XRMAu)qnm'\[LT\e-*)<03Qa j[h>ؕVC_\d{IS.TzIF~z#2J oVr?}V#e -+ ڪ99w0<[ |/ԿS7CZjZCר2hY*+ʁG6S0]3-n82je"h%ihPı֪5+e#/f/p-1k O:a-\L|5*(0^k|L|S{7ǐ WwPod!i-s;pZe޽i9 4oa3t/[, i*5d?H 7j96+.ڞQoxe,~-Xw4nbCI,Z#Jb_qy& - } -^'s+;e[jn.@11%Id$r&P_{^0~*ꡤJSvZ͗OIe6~6K(0fY -jJ2<{0DT&-Ly.ay36y@JY,II*,Y - #0&!ʃa|JkqIJ,EZ@y P6=pMe1'Ձ&/1DC oecgk-HO#B4SR B` n!e j.7Wk٫gMqDVOD.&oŰűaONJmFFQlGG`a(fl_=Iv:~ԥͅmw^撦 DCfT^샳+MvV1}Ҽ1zbjME}3j$CQ)lԹv#e%-[mlဤ9Ϊ1 yfs[L -EaQv wm l>ȡBtb֓*[u]Xka= 8 -ZC Ur67܀A6?r["Yy;It]y7=Խ|q57*d\^Mv-!)lj@̹Rׂ-lf6?(rΦYnV, ZȶؖhvJmi p HEMSj -%1#RS".yv0bh -#F] jR&k/boC/!*/r#p{ls*jfyQxQAH7 $[la<3`ljT"C*U"֕,(e }Edk (dQ^+?x@,-dUC m>W"5-6eRN0+ek ^Tu9%=))VJ*poenOY? ~si*[g7_9YG"bkv= R zSA(dT1P6" o 7n&vk vT -!R.Y[OD-a(x`]BFGxE|& KXXX1 ; Q(툝;-aKd)*]J˃L`QiP.TTة`e. Rj"YN|V-@K蕐 *7u-aA/`Crn(-v!rg԰+P-bYU2cnNS`vaUŹD9Fհ.>L@G|.;5%z -yVhPؠ|#=C۔u)`uQdDp,V)! VK;Ii(3moԱcSMUr9BbV^YBd M PȾsY˭6/d a$i7@JgtnD\m;Aً* JjviJtgX'vܲbB6EEU1.y+B#B `;ܨ)۲嚚,2%.S,k/8ΡWOYd0,n<"b^ і 9ӄ S'KY>fռ%5Z)fw$P3*W -O=~P_~%RC)+tx%5D4:lл%X. @lj^PE E>Tl=Eفr./Op +2{W` _YɦY]*kiח@ DX):cdT$_ 4ϚzԻu>(H(Ǿ!^9oVzTU/=U:\͏#ۭ1T *5k5UP='-5U|[Io!n @h*+Ye"vߞenG$ƕo o]^R__(: j#K5jw "/j\/MQ8Xi2.mrPBgv g:ˁ'l?kYA/.O!uQJ* U=vr -ƼN''n}DP9%طˀn°Lđyỗ tDU`^=+S 쳑Qj [P_%+,eF?taz˷&..[V:tj]*E 8/0"yJGQ7׎(P@2q! -)i%atʻF}?я-./Hfw}g+:Ř a~Mlv{ ORhEZʫFa+f`}a/x@)QVe4#YCv!k0|1Di:=V!v rp\T4`x 󳄉d% zUyða]9 s̸R|@ -X[ۈ xϼvX/qVonXA{~قԥuK ~嫲ˉjq` -"lJ"S,Bv>˰"&%|p -r<)eS?i7̖yk\1_\/y[LK)O Dɲ- HcݏNo?,y6McD&+gb)q%le):97(FgpET,F}|I~Y-e.6%ϊSp7W/T ښ]Xg/5 T:#/\Qz Gva' {XFևP2~ߖxbT5/ ~s^3C}Z$Tt% -9#N#TP *o@[ - -핛jS;uMP K>kI夅>a|0A A8s'u[Q h+42 aɳ_98!*bk9W 4Wר-dc否`d|/V`5 BZvo%Wɝ^>.]䬳P0v LO/,[R !͇A' ފd g|Ƚ۳nlH*SX1)|7KYS -),;ydHG_i|,a_=%niq+T&q ,Oz #̅m qܼe̱ѰF?}'JVq6T,'1BVxao%U&7zf -uS{-Z.P:#H?J(A,._j!-`jThPKǴ2p˒i<նGmFFKP._c%_#.Y^YHYPiq?:6j~g?`PtE6kȶ( >Uщ?tߎDwR2N eSqV H%%6s՛ 4& -<ܱ_PgƧԬ.9KB4((ӆ 0w*⋹ -)>Ga9/%*".S ]-Xl6A:̎hDpc.=}=:\\d~ۭc69@c,T}պ,fȢ&HR_p}*bG 67hFqR[KQJbL]>l=D=\)AkPepZ6(Qs*^oy.9nXሲXe -1.5L]\f;yL +** - 'Uh@ME#7 JH2Bތ:_|,܈ o`@y,^y Ğ"̼e -o*fTS Pjڂ kAi\WlXE -.Kd6湕v[1Z.-}@8[W#TQ *-z1lzT؅ȳW[ BAoy ) ^^A9u87/+^jK -x4)LA/L}͛ 6U E(K6|ĥ?{TMF`, e4K7WDQb׫p=%ؘ(fnG E6m\ # -ib1z  -FW6镬ݭi*of*YAB bdywD󋫑rKjfmph+͂ _'Mo@ZS{#%c, -蕵H 3&F-XT+ĴU.cy)e}n)?O=%m - N0*$J ~6&ٸWy9sL.]DZ ek8Qa}c0׈FʤLO+Rv +!); 咋;zOu3oƢD,Iy6v-zukr'W?!dUr,.Z;jG -宓K9>x` T)oeez٫B^r)~#/%ĸ! .m#Sg"eٕs L,O%dE8P _ pn@/խyTrV24 Hpumcc[ʍ} -ܥ:kfղ%<4(cL\~>@<4T$X`jm~pIW -qgk>1 lK`ʔֲlr,jJPZߢcLETprڧ*k{ j3j*M5$g/„ЈۙV&/t}vtXsSVU8^қ*~CPS7JhP(W' C/D,?;,c)_pTQ؈ibu _G[c` i撰k[K[{B=iBMk-PhRnL6q%hbT.YH'k}v@%N+ 'AusK/۱h^g-_6/R)<H/X&; V~ EoFzUr~g|7J/Y)E7؏A2RP*? >\5O,)1FT>ߕSm(ED>\jPb[+5/x {tM8I*#W#1 d Hvh!o[ٯ?,^xCա~BITݕFXW:a-A_uNA&&ʙW2oԺW4__ k_A`ԓte97m^TQ`*\웗l/:"]Ax',2TJw]RNS5RSfyfK 4f(;QC'%iX\Ջ Al@em -CK=İO"#CKEE,/U\u 5Y7eo<PRٔ=qcĈ쫦k?˚sT'*)Ȼ4M_pu?]&B"pJfIo֍M 򠳩 0"f>~'HЎvLy \Pcm" <\5Ki+A9dp0{O1Ub U~LtzgpTtd 11({npP}+zˣ붇f}S-M ̀۷#^0ndwxQ,SSU}0ZR\u}")4Cećb.gYc$Nz}e" S>dA>[`0(r-1ViT!(.\}zC Pw)P%ږe3ȅ&J2'P R*"l(KH5aPڐOΟX#cb5ܞwT*<ᙂg/D֣TW/?x@K?ϙyzZ5N)sد.]A&`}t"y˖ G-X2_x۟._Ǹ9SHyflP #)h_BQ: ¿*P߀ڄլNp؁ו 3Ce<Pܚ'b"F`3* Kh)Kc6~[j2pUB 4mbr1[pls3a,k*EN@̓?UelClnv 184nvY-S )j,TSj,[ ]%WqRl`iQ_E;@+jP˃Q/hBԥuD+@ Yp7 eS&<[MkK@ӳڹ% Œaiz2)[A" ((s8%Kj>e[CQ2Ԇ5(k+Zĝ -SPm)+: ʛQs>/%ctx ؊5=L8,#uhEsv\iȴ?nL,"u`X1W  uZہ* ղԭ.HV/ U^ -p -Ɣ[4I'.6qj+]}\J/H*U1zCzNۆ AV -5S(sQa&.!nb*)&dL:{t+)H}/8bV )Q3]'IEjO%dUtЬ r̍/@ Hjr&Wʌ*VL!yxX# -b%n= -!t"<ƒp -bZ˛Mf WTeY>r6 Y0UA]췒/ܶ[` `DzGxkJ/jDzsl.)]>q] 62ոݕZ_U bDLi*kEˬ[X9)-="h17*TV@G jGuAV$3RۭG{Ag{Kq;%"=ZuX8u"i@3i2/_(SCUV.MbX],Ǯ[mrg/8?Q8A}y~ -*,GruTX_(<ѳQ9ax2^\xkaB\SHE^g] _"1 -՟38>N3$bBe7Jd6Y܈GR9)mϺUۈ07VYLZt/ܪ#A\ڢpLh L+v厐Gَ D[glBl薱Xd2r쵘m@REaXM6^ʩx> -sHꊒک^RZ<1z $ĥZVW~캫5UUw㒐9GfX󺵶{ Qr][k>3؆ER"nTR\v?(+#LpV7O!n@X^ -,d]-M*m&]H'_6gwCBr3虇 .ۨ՟Y*E[v![fB̶-4gR/b<]Y5dJ7G1IJK#EҩXڦSC-NmEPSDW Yl6b.r!O*FVW\^`9)8jO\J6㬑*'OQre*T7ՄtS䦊cE2?Rr{tEم@rZ ["*=L(`t5lq"8c+ -EQ#wL#qK1[xQNi\< j} -.C!~B~` *WP|f>Y -]̎l; -DJHߝ{Mx ҳxO!a,S}k1S tš i"! ^\3"85JsywSȫkye~2ۏ-*I/W @}boHV;uf9ZjKCc@GyE1O,9,] dmңwZv 0GHTR#rTX. hvoƝ#ݎTQ v42M?( &D_Ѡ?k D^>wO_~YE-Y3$/.sAAYYv"F˥^7{` }Oo(vL3`h"W1h׬/V)Ѐ”\qZʏ}eeKjU 3?-(0/P? %[PW* 4Φ95X]#p&&n2A"q䫈Y ]G 9Eľ#Fż% --$mGRvMa@K,[Tqj/K{{\\o1>d27xųz*]sX;%YR&d*"(:_H:Zt= ̚C;}U(2h84Uce6@6cDRliQ._R?'4 ?{[QִI\L}ۚ<*Snr;#I UK$cFBDau(|#0ў!lR2 -da᩹ -4M]W~,UM6B,5!tGBpzh lj.[Q Wstk*ʚ l|K ӱxˬte%_wp(%5@Ez*P ٌ;@pP2US0 -[h3e7-O^JnHS-^`mi@.ˢm%¦[>&E]e |% d!iY,b~`ד 39A@ -iI8<^m JZircY`e[Xы5*ՔYDRu}?W~‘ :nCϞp"Zx2i5]UAk*˵LgPݟo}$ Wt\+D'jLd/ &4GZ ->8mX\f&1&!OZ|6E!ձhv -#>b>4RՉJs\b[lmYgJCBjJf)KPHzCpݚz$j>M^Xit0`#&"? DpLrˆ6*8eTTv; LQEQrb W`UD$MHHfRQ(QQV%8Q T:M(3 -cU8۳YS9,q/% O[,47EUTL/@R Zi忔X(ykX}X=[T -WE]IsMϲ&aK0 F 7UN+ Yo !sVc+!/tūGf]5|g*3,1_">KE-&oTիrD+*fLW1TH*lnb[~bٻS7gU%1J"hasV`/X͔/,Mb"\nAhNZT T2MJy![S%D7>9y(KH&pd/ -\iY -P[ۏ gn懜0Dox P9ەK`"&pB((h -iRڣnN jʞX.10 GER‰kCgrUS.m [z?Db}JV.^$UiM~!#2`Sp\:(%qN̄NNxN&5*֭C`sU`eQ%yTA:U%Ve~|/tGNDqʬWP 6Ո+m̄C#򡮕T%҃c!́>EF4-Hy.cMԵ^0j YW8{R3/?g5GkĵlG䈟kl*[5(6˨„_072gU)"\pn$e>!JA+!bH/GU@5Q(SBV@vq*VDen"4ME`g/ D]oD7"iңL~Ւ*B)Q*Td?[̒cce SRfؖ-)-G:Usө\[jsI+O+j4PWA3?35\mOPK.y)Eᓵ7q+B!Kmz0*2jZnTJ*>X#G@#F/̝ؓ$o#O9z9rEeC&>H\ZeAq2*Rr-rSOb]tK{,/&K ,^2EijMpBˈ[sNZ\8!)¬hAr%Fدk+D;DW@-D +P2}Ye((%QnPt3rkT#mB[cEK\n\u4uN--)P(x ;,c,mv»2`T xw ~NKd6Fp^@, -]=֣{H҈Z.0+qZ2IuԸpðS:Ou+Nد@^K%86C*צS Q:e稀GɀP Xw@Wԏ nlm,\~+=d'O:cUhF`eݤlmv-9^2~9(eE{ũ~>  #l*91"4ե -53PM%Qi`;µZHּƗsT+=ˈ%Th(0Ah%eKد.RP'kD@fڅf,Qvр6%w%O>_\l^KZan RtisTn)DB6B:P)2YQڜ{%a -+׈]B݆.Bl#r"߉X_(<wPwx\3B\ǒR/L$+X,RDy.,*$J€ 1ltO[JPC#o=Mg[DeR=ub,!n*܇kz^|[RH h>q8~p6#)K(xH?(*/rR\/W/3aBe#:UEʑ|nna_P#DZU츸jC{|usj읙֗g̶r5m 2!T&}Uu4[.kU;U2Qи0]FPK#j/G:!/T 6'%*JV("aCCahu`%LeA+!JUpe .hv;PI[nN䫪.rb *KQSW3]elk=Fnn "Z› rvW#7˂[ie^\~W8dQ -i  C2iUՖ"#~゗lҷV8/<aPB!-/?Z؞3V5vK +wkl}%Bc`L^ֈk`DZYD->9}}j#jBƄz1w"]W{X551Uow=u n7Q~׌Dy[g*Y -3YѪ_{6L+HκB!*o -'Itr)Q -ЂEơ' [v]gv+WDU`D,ȕ&VA췉@O n&9+Q˺Cx UNju~` iW/_QYavPia9YY4P\'CԾZA>';P1u S׉-q>vPj`?Ύ[  &ljiD,L2YU@z!Rp(7ǒL%hQOEz %Љ\}æ'+ p%*YwmDE %jmf'g>5fOU+aroF:Um -u?a8/T[5U]w5%?ʶ +nXV}r>*ۜ}@Qzœ)0\brV_.)\-V}CyN U -lBFʮL¯ckYR[;q+PK ^JṄ`#Vb[~C& r![ȿ)L1)6w#?-^C^ vo!9ڀZ -_(OdU -Gw]MJm__>i˽BB.b5*]UMhM4ʀoCrhHVxv:m ?AwWB9Օ }~8+Lf>*)/~]ŶDٛEs$?싫 +替S!6fGqFo@~eno~RtJ!Q;Eae=2gS:|5 zf`w<*TCTZ{ -2b1|irGKn9e -uɅن3B 2<6T4˥૔/ 0oYd1'R\lPeIJ e#FCae춠ˎւW9ZOԫǔ /LVG?xP^w3J#&VsJ?6SU:k8iwP֘G^5]8FGggQ20agnK-N$>MRnD.Q1rMDZVUޣ6[+S~m0*Ċ뱡d$WP6]nhT{6*Tu<9G`8YTFL[ /qyk/C6XE_"|xDpz)5f)'s5$VKvVI1e@?ke1 :=場|h}v.qPo[E-c>Xo#"*leK,[0nۃ:_k`XXT<dG7yKiY "~T@LeBvRJkERȶK.XT1 %F^L`T/OЇwSHdY%A-ODb2iʊSk^xCau7)u*Pa5)K1j\]˚]bݴx]!b UηȐyT NH$ϧ@Kf(b^Ъҗ{V -n1˖Ee.VvZ6J ]e:GR\p- -b&<{%%=*[y+n7WpbTKlpBtE.UxCA[G{-O{-Z -&\w l ~P,% e)b >|ۜ @R:I^nX{V /#BڔTA,-@^!PGmzح%;snpD]Jf4#`Ye+̾Ӈ#aؠeծ%v7$ vvZ=%{vsO3Wry^!<R_1v!nG eZ NyyT*lO3R` ܕhKgn`̟MԠZR&d -K(JQՇuƨVAUVcJ#P[񁬦P}ϥ.e!aImɧ")VlrsebO%gXj*b#E QHv7.T7lU(~&™@!MR*2\Ф!9r V;2h`Zej}T:bT muD%4[F ո.ΜcrӵUBvjryu¢z#egk6}*6,4xATUp *HU?؆^+ILbGK2Ɖ䥲*KjDVjxdTFװR#YHlodPU-癕[;FfV!6[9C/k:4%i4TKlvл05qYT*ml&*b[^]Sey05Ȋ&o:_yzxm/VRjiّTPQ|rq*Vl/rPEQ&`beKT>,JC CGn4>R'ªҊ.8­zpE{sٟUGN,^M΢ppj߶wO^'7.үߍˋU{5a{F kZ[F,pU rp%}U,VlS:i#d*M-{f mE`U^#lq@-4[k镾|.)Tlԕ?\W}0#KN|ڔ6h(}Q,T%%aTe{wXIh&ܢBסU*VLH\QtD >-1Gyb4<=i.(V>QYظ "֘5H˷3 Y([)-H* Ҹ/O}N(Z?Gփ~<VDH #yVgWqq'Dh|QZoo,«`rfLU!*@|Έƫ2_lr_2z/Jlo_c7IK(U9e;lFqэU`k1`@+ Ne_ ! 03DWf)NAd"n6H԰)n,^_"v-wߺMrlSw&JA{ckz]ɇ;('E+e'ʌ(='ZEdY}.!ֺjL'Ķ4B67mčwþeh5Pr( ]Kxs+T+p7EN^ 9ovFV譅`5ap - -@;Rxp۴,%g%]Z9_ű̽eNgN+,԰.gw ̩U+6T|-eV˛D@Bajc -&?/%?cD g%gN~ }V!geEfy K|VTf h(+MN4% յ4B)_vc記olHXto#yӹG\* ޑz>X@ Yr<\xE^B-_LLHG_,SpRӺFZPˈrnx7̶% S8N%ZJ@qa~] Var6 ]S*yS6 -֪)E[a?Hzi&uKBɷnUal3XU~T~$Lߪ\<ב˛h/'>T,| vR=rYx=m_BT7.> $ҰcnFCU9(7y ky3uթzM"?2/Ț-6ZF_`Ty?Z}vp9D>H4 -'tH}/am~'Ur?KP?RVq Ϡ!Hs_#_@g$wuX]Jil+LIۡh oMk 7OĄޮht,n+yVrE*glJgNx%3M>!nM[+4b J u^z͂je5&6`fUo5ThFT=˱aTðLJ.IuX ufQ_2>Wܖ,E]vaLX@K˥ezdo4Q/"ר$2%¶vDv)`T0u-+#=H_cyYGVi@hH5*cլ#ocͶ.xy@<+BW5eb4s|m׊8^A)j(/胭>`8̊ݘ h_~UWcb׉UۋwPY= 5U=ucҳ#H5s.>!P9T2^$.~kLv+|kADfF'[b#溓W7ãh>/uy,e5qK+51"% -v0 Y'Pf1- ǤaҢUص]FjLu}`%Wjj:b ,^Q٠T-pL7vGb8ʪ 9WLzv\~)p5 P k*X4c%t ?L^,wm3XQ͖ g yjwwnZhvUV^eaq-([xE+!b3e4CFTP5MAkSGdH\%PYZmga}imgvM%o,MH̯ݵEV^k,L&r *c4(;0T@}c&V=1>/F۩7v˭a]3NW\ NeJKO &p$WuqRV&V>4(F)G#Kb+XV*vmEZ^Fy[lJ1K{q>,5%E1[tJVSRe'lMS|J^K=@w:E -K`uaBW%K M\"}KVJUeFՊ̾:cfb|YLt2ȆŌ1kn٭F߈- -G<S:r,Uk` [p# wvWu!Ӄf8F^%ZM-M53[ʭcvES g XA}pmDjeEs&*Vysy1ѝ˥b`ш"} #TzD*jbEALCB, R鞛Pf/R]si@6d^r E"Ud@7'(L3KC) CL0K JZ!S jlYb9)˹Y8EQEj @ 8 ؂T.>5P-M_(-Kp%;EFҐRp:KvzejSMny&J0|&V'h%F"CRr׬j 0<oVD^LK+X˺ XQhiKoglfaT+-Vĺ8˜l'zzKv:۶G6[< *\A-L R +%p*U3y4e~ y.rHbP -]oW4f-@K4Q=K57rVyRf~Q`pqsikh|8;҈Dy4he9LNܗ9Zw[Kvtl9R¯YvPW[!TW{P4bQΗ3HǶ bfP?4u5ԙ׶чX]kV૔Π6x"%*X=Q{^@oH/_Bw3Х\׾ ^OTӿ/ih(C0eJzi`D9/qS-oıl.v璋2r3=Zh( 7Ns,Bت>^as, -҈SHHl)i0rJpX4Tib\o}|A%3^K;}-zd͐E!W/{Nm*O c5r:*2KV!xx)iFAP(՚UƪC6$4QԜ7ga,|/~uy_l/EX %ⴇi~  (:#MRU5VJ|Q. OXQ]ISiGP*O_a?IQkҜܥ}d߁{?>ܒI5?]O?]$0ܿR50: w ? -(ǥstxJ% lK*`wDs{xk(AO: ,2 'PJL>߈hwUlUijlBF;ZnFm1{s򸅎ܸZ"y?6<-;0/Pإ`I}K7%dR "K|Cv蔰$,=/͇%vϷ*~DYP^#ci.Ey-̀ZbJiWvK?O/Q~iZ% uG|ZDPHhgAw/v>[#_qA/gEד6ĕ{*KlM( M;3 Ygg)e>a,mmEճ,A:ˈ=G^]kXZlYw5u%څI`'+-]lx^kq]5+TB.Խ5Pn d.-2쫁F("4M n]J6 (umWլY?%~nX,)h :&J>lSX-0뭸w(o4;ns(.IUX -} -"fzMZBM-#s.";gI(d~r討m3?7EÃ%D -"Tԕ/tn-Eċ j耏fŢQ8C.iB0wqw/ԙAEZVyE`Nv4 rT] Dŗ>jzb? jg^B j6.;̄,M<A&b|GW[((P>%ua咠 xA>õ2X0Qe:\(bhzbdJ=%*.TÓ̭G?v;ƣ ZGl AJԲFt죲Yvo}wbBF^?C,Z+V9%˯ru9{pOMuZZŘ4/o^aHw -\rGte0BU/Si.ى0Yt!peǪrѪoKL&EZ0[l@W,,8xV-\ fS9l,_ tZKiR.E6cG D -ǥݛQNVE|˪L#Jv_z C5ٳCԹo{1Eek dԸJE>. -]}lRmU-ys;R*`egjn6kW[0^iREc"E1VKҘ&F M.Z52&;QvwgBY:&j&PKf )?QQ#(,Mw8Am ٸ>WH -6PyX+٩5mPzkH`?z%?5< M:Z\xS -@uUvX ԟsNƋؘherT0=93K26 _$b9!gh ͊Ş!CRkFe+CYRpY$-Ba]on7^oܪ*\^4Qu X 6$."vE6^\ %> 6ܸjB]9P/F+fb'Zʇ6RB_ʻ,H ; .2-̋C )`C|jeC'/$ڧPzu-E)SfU獓m h1\)>B4+qޫu#]ܪ}@Rs=[\tze7_;b -@9~{^hYLQU?Ej@\/(+Z4Hnz9h+ax>ze3l%}1ktpjJ`*-*)y_ʏ_~dmC%R+A!^a E\4'Fi*$%5u yc^_/)ۍ)lQV{#`Oc.ieeFU/IF.[ ve9[ :dbgg`n52tE˯1w4vZYu([~Qkwu6,dFWo؏?QmVRNw| RangKUe TFgvTa>o£ܥTܧveaFSu"V*'#֣~m*7{{dJQ|ŴPί+@U -= ˸9"`n$Tlܕ9Q?MU1J'}Wլt+IY/Y02[z<޷EbGRGħk -U-ZyV {&e~@U/ΘR {nدỰ$3+੸)L+d`?qcg7v7#y4ydHGk0V/X@g28+qYxNc1*"R]x@Qnj/_ͯ)ܘ+iS .@ - [?nu[.^[[Urǥ <.Xe +2=L&)}TҧFy6!~e.v(hS/AcJtZ}6YڧEK,17m*X%EbIfƻ{Ym/˶Z?A?YwX@&~9<=E~:ǿڪ**H$Qjn-DR -`nȬoBd)~U7=FWڥ8E~.rVM"Ąk@w=; t+l.c -@9tˢ:S~,m,dÛD#DƲF$9noR-?%_ő}-2 -~57⨱{S=Er -y,mDCPSw.=n*$Ris"7*ulٜlf^E̞C-r~^L# 6QN{eu_WԽoא>-Q'O^%8W<,=,v+5(L;SXyH5KߍuNqWЁ(Fn `凯/2*y8v|Zò:({ew`߸eYY\e䒯?;TLDRl}/XEPD$x_7D-?+oG/^Ub}v%_5:CmKM{a:9g1auj~_dGEKQAJ]WQi8ݲΊ*Ve,e櫼[3lZ?͙x@50:h^QF^=?Ŋlվq1߂$6/&=Od 1Bt

HK1츾kSIH=gS͋)`#€iQ{Raj5|AZRsjwjIPǂ,E;,Fۯ7xL j,( JX )A1<9ڄj$\+% pj%ѫz>9M#v 4lJ -!Ez[(Fa?iྠ;", -{kT͔yNYcQ X]tۊ;6M )lc.j[*uׂ'B)z8|}5۹.yQ)1U)Do/m8 -^-vB菉HGV}fʷ]m֥pi*(wTlq9e)4*1O-#-? (GUtK~ve@A # %H%>vnfMY2x=8c蜜;y -߄TK* -%eTR9#ogg -lW+>l13o ]\,߉~XpO>(.\sMN+>2wgrJgq/Hz\+U*#|2 rŎYY 8> 6HLeKۨ+`i򯍸%NV|VˌbQr$0 .%gc)> P(UPlY~*xnዩgFg8 e|N*/O&>eXUC퀶e -1h^6V iS}B5~ -[ْw BNPJhX.e+-eӺ.sq,”ĉl'z͗ÐqMcAo `agKG -{O֜hH?#?Q":69KB0vj #F.&Dk_V*%$jW/?&lߏ2L{Qu%-ra(BmA^a[ώ3Y{SooaZK@JyZY1{rDYe2GU2_L\6Z)a@cG/H -ndGvd;?l6=017ftGHw`ypI>m*!T(-beq֌~& 4m&p)"%ěO{Ni} ? Vԟ‚p١{ɒ>icw!emDHgRbr%[QWdƯ.`''E_pFD~ϟH>>"Yݤ#x&7Q4+E7*)/M &fn0^),2α^y%D+n.EW ؙP N~<#ŗQvIuipؘ'np.#UP>|YKw~S'_"r_6#%͏S/Z"k1ڨyOMXch"Trkp}Db{E@fY3Mgbaժ$Aֈ?<Ϥz_(`'u;=~ 1?}UsPVerM['9HKcƸ\ohg(|}4#RcPU?jnMO!z^~y?PzO5}$w2=tBJ?d;91q!SVםŻ?L{E~%?h6C jkYEA,-d ޑxŬ{Q J`]v'CK3E^?r_ -"h5CƝ2$\R8%q_FQ.rzcG&i}PeU]Dڦ`-PW`L5Y,i4S;-/ef{V2y䫙ŒZ.[eT.qp|qUó1wb=c2.FS̽ʥUڝA_(ѽIk T{¼*#l屻Z{pD˥ŽZ P+p>0hyZnTKݲ!Q c0mW,FSqWeXo3:*cdKzG^_U%1=F$^9Ac=Ґ2xY.uJQPbOXmGR%7Qp6c5F-,5#^鋑B-Zu*+sqe"xF0J"Rm)E~o􂙉?zB@~(wM<1uX.\j=Qol`E,EI2X0H*gweewp|vn/&yܳge牌!.SU^ŅW<́z}!7A& kNE劖_}7+W0J+ -VeWGWb-ݵST~dZN7ңKfk^шu^U?/+dq_h~1mj(J.hE,H^*!uŴQXkBj~geu7v֪6ͨ)hh2ҵ{$\t F`AU V/׊%[*. kvs -cfb?FFiJPـ+ kPVػ+ܰbƥ˗ eED#l*]'rQ*Pk:9sNBROg|'irEhʌl[2t&x~.?:˄"xsb>WjC]vY]M.:ʖ*b{rgi#hQD(&x9bb>,cj,d\*U4qb)RTw-X.g5ȗC.8!loX.3,/H-2Fֲ7xˌ>@¢O1M|KOkĶeeEhFܨ3ͲnSk-! E+!r/K\Qqp.\l͞HU_BJ(Jx0IoQSK9쁮^b~# L7l=Od4eSKɄ骔B 1"7v Z<5dMVfQP5fʃi/ҥu O>a*aB )v)znD' Ub@jm.5(rhO1VdڔٛsQSfʅõ 'F`hjiBdž}ˏ*j7 KldYaXz`iUE-\ -f] q=T%`|.-.Gz F+)M4pK8+ws.,#OV$yaUJVr.?Wa/\?aVKSb!^"`3.(pk-P-Cf5PTWVx#ԕMZh_q-7R[\@=``ĨXuKhl`ùAr{%ZQ Vz"Q$UDG+gIl-03e AŸĻՕ+^Hx -T ;p@+++vVJGʉ3^ a5T'Щ_ -&B! /TO"1`lEXilD ,E!;,<~ ]>ڎlUltWAL*G|6,0v0-lY)B2M?d ۄxohT\PQ|0]O#`(ԥ+2_2 {rB @׎0ܰ'B:_% -!>O-|Y ږa5-DRQ%;=y* -;-7˽GUqUsJĦk{^:l\-tWJ`UB?EDnBpO|6b#z }}CJ_%^%e ^XJfWT~L5Q Du+""ѵ6^ @Y%%^vsעZ) Q}F/)uN(͟DiEx)5bvSB:jk0߅*W\ -KTA>/̟hjB埊?o+&Nһ*uBM!bEOtsf90:¿.FS[Ba0kS{?k c}5ʤFCʒ@/ƶTN+d `OjrT_@E>Ă=*_VA֟#3^}Qo(WX.ɳj܈m9gc{3O1 -krs(^ |J ܩ_VP.ZŒԁqRJM1f>? >%yj HDe9Pt>L% ߻qoSşz@ )**|)=@]RB BXIڭ_jK,tuMp4sxBqDDeOJP@xOi 7:WOK2~Tk w/Y>vK"'tjZ8?X Y.CiLORYtc!-M*陡yh"T~>G Ly?_ɔ5okA*[ѿлex@+(~oT:e/(1-o{g.2:u!  U~1S4@gMe`"Ө~@XH@P,]`VC%5}r -GcnZRV{=$5PCh6ȅTxJ@IT"M^BJewF|{?<.Lqv,"K:ĹR>V-X#H&hq۴UR`0?|+SR%9.t*Tz߈9VK &>ɦ?u_mgc/W%㤼o5jZ_ )gK@RkKp/Q U`"moֹG.kE**ʕj* c&%?(% Q6ڇ 7ꮮ!m=U.g.j o!/ss׺$_ e,x?h"*2ۃ$R"nu/@1 BJ%_cK0(1e/1rF73"u:l~r+'en*%zKXidD`*"cq))NFQ ^.͉JZn:H.K6{=DDD`\Fu%0?>|_ޓW4GV9J 3A&Vx)OgE}Ѡ&]H2o0D?Ӱ@`1Xƽγ=M_ Ĺo%KT%lg= j*5(n }JTE`$E1[55)6PWp~ɮNXj.zdFc6f }2HghA}Qҩ >Ŋ-ms@olTlG q,j= -*2f lPE {"l@Ge\P,F>PJdN*U1RpV* xOFJQaGbSt,43}_=cURjNjf^E69x9CQVJ)aۃGgkH5rQwSROW v)Vʕ!Of,2Y2.ly`RZ[:˨Zo`\ʉpjK߇ CPojdVBra?ŗE|`iA=WIze״мc<*}GqN(j `mjS}}(aUbƤtŶ)tUc.QwlP!ψܡIĞ.{WY\a۸Zd˷tƁ0U6 -?$5~^)J Fa]NFo]>7ԹeGр]ۂwy..˵Lv NBY+ȸ@\O]KVKKMW~CHZōG h^2P^oy&D<-۹P2 8^}I)`Qn. Փ 3< -#>H⟼wG@CkzNr2/EKX!*bSWEJK*foҍo#+TYgۧShq-zԌګKrèG)V^5Qp vu PnoWS/J-,XQ䝗ӌ.t]<]Qm\e ACi dd}THP"?$4pk/U]y-uء-H.[K0v\ L qc6OĠׂvr‹҉p>J9EKҗq3h&G (dKXR+Jb(V[UuwI ڪoi?'"DH7###ꦿ _c&YE60ͪڪ4\ -?@D\%&JOB -3g=aF2N+}יwm̉CϺ32O5qעXOQiU'/EUG<KbSn*&`dv,Ũ%<| Pr]ȭ2Iiu-{:/~|9l%VrGU*]2.\ی%˩qf% " /Q`A.<@Fޥi[7͠|(\^ljOdeD:ؓJtcf0riCd[?V3v:HWĔ̠eUgEx1"/OHat~1st@+=#p|S)~Q"eƫW>@:,Q1L y /HI ĆZJ"*ڡVM 3jMG($5-vU1~n.rR⺗(WWu'JgfNr>*_\$2,[]_ú*װ kl[e[][ -T4 j(sX?~T0CAgIBģ87{% *Zk ,N_ mPA(X'-Ej -T)furp -UJ(]^`:/@5B저V-DntWL)RUZJ.n6a(daxO^8eL706=/\ Kk0{y»/-!  jGo#/[RQ{ ,}Gek'&1V_ŗe>5Ϝ~{G\ۨݰ6Ų0gK LD稾#0VchŅA4YX\OX)SI64i㼊 ZTo[MEhe[IgشKgj͔\X컉ylg1zPxRr%ꥮ%YUc6fˆyAH5ˏ'aC&O5h7)G t,We^&f,UOA- h>;j瘝U@ܸ'fS0@!A=:F*?l"Wf f§&)xnZ-B׋|_ǘ"V|$6Z 2U3D`se7 >T'mKo+Z0洩kx-i;o0L9J{b.j]3Q}h=zKaA[`DybYJ"FE*#ݟQbnC5Piոb5#؄7[}M#o3*K5K'%ЖpW+WKyevh@ ZټG(CJ+r)\4X"|lWhG]\YiIJuH<Gf9*ʎܨH/(w>āc) ? QYB10+Vx,ʛi[ %^.c|F1B}TgejA'g1<'ŅrGkcݖ [oža5W -Y) weuU$GFZ|yq;UOكIc0(=\" -165Z2jUen ʮȒ[#JXhTF-Zqkp~6$"o6{5vD:TP0,Ϊ;{yEA)T]yhjlmIWE=YG%̴ !5y*$_.v5'y>rG!Ⱥ*pJ/%g+<0*f=F@Bx02v_F@'Dl#|(TBy/e@7XވC`6&}7 mk K}30!ExZl<:!"bqPHBFN,_UiH{t:^H]咔B62lŦ?Rl5Xu44>v\5%˯Wa5l'L,r^_Z38Qկ(Gm;>U,GooΈoqsM%6njC%2nq2vŐ3'yGvϟ? ~o NAmO:tOH3'*mz oEG?P"@U? 8*u$;̈́sN*ʈ >QB<JjE*qɵ8DkgV kޠ! -j ܘ#I/2?ȿ -dDByO4)U~TG,v<ʶ'^ G-UG邏lKK \zO@ʣQLhĠ~m*5p)µ Q,NT4ptPa+CW1ߣac5Cv[_@(VZ -QgJ?.Mo?d4}VP -.2}M% ;h*8hh0х.jeV GS5{\5)eV j߹v\ ^II\* d̨Mp_PFVJ''n䤹|+&Skm*τ/mgU%p* jE)X^ռ3Tׇϥڽ`,%dz梠I"^CQ;zZPڥs'$jR*X_ghg}bȜ\*}JUY2W5c4t6B|^"'u6nOFFܣ)ּ!hДG7(d jۈ9>m~TFj0WgC0aRu+Vqd%)-7[od[.aTBPk۔@#DJ-]-PUQRE%HE_5S>hN_}۔A'T%8!pҏx-ꐢڛ]I(b@1D -:+aeb,zZb> y jRϸYHC! -a9m\ ّ GK' G#w\Zn-9U+^ūEԴ6cB -g_@a4 -Z2w -Xa6'Y ӗʎ5w:leDTKKN%&TYT|NsOn(ZPb,%K,;`ޭ8Jw`C?oRq5H^@KSOL ӊb"1t5إ ̒4XYĆݐ"#V,6- _ @B`?F%@q=Gw1؞FWl*&%wܫl3"r4/Į3HTiꡏT^Z0ȋ@!`|HqUOBPJx #% 1C@%ᣴSbD|*NۆOk_*ʯ?.<d &:w⛙ɇQ*+;*+3e FR(YMbTPʜ<h-CnhzigFY(٨ܫߕ Ӳǃ?1'Z>"X*!nGes0^WYAf,s+be\_&9B5rQDaqx&Ft @P#QYu*# #%DIQZ뇫3p𢍱,TnҠ-je;.U<ܕnz2<=D% -@2^TķK mR1'!ٻbbRB R̪T1 Uc+R -gjoa7\Kю|*1BmBG{A)3L{D(g.^bp u;v2k%R/aWD]1>XYU-*PҦX,_gP. -A*L{lҬ?$fl'ZVBh{e,{3RQQR 4&8뼸]xGk`/PȑD…1.rU;Q2KƽcJ j3}XORhQRz:|"xHr#7Y[)\ioNߋZB74J[Ӿرe˲|PKBybW,r%mEEL%dE\6aJeЍ~')񩏂:)-<|*$P_QQ)((iSĨD l1U*TB>Uv^݆K%xE3u\C6P1i\]\}|IrHDd"*ilv)1N{Ժ}ԩYA/ƚv|^(5|/wW0,'G4l%! ^dMa׭e^ *J>wVmTD$s֊ _*E.H$;%2¿BCOGjo#'”BA 1^[+Hi5ڈJ%aOxCDy1 ٝLQW5|X)5AG{>q5?X{SsC#q~8v0fX[28/5GrGywvmBs' {w%ȍ4BS""6V( zE多FK.GJ+=jOqol: g5p}94< %G,B8m*ԧc'ؑC)lm}/auUq)N -ş|X}rx8\<.򈢺AW)OjBnmSm\"+~!&q}.\e#'ЕN>eJfFVKۑQex oQd%"ĹE->}Od!%=E*j.ܗjX^[Ծ~ĆRɾq;qU~h7:?Z.uDRZ+8='іP *KP C4 WǧVhVVedB:4RoehiLQiS U&+}qZUD )W[ - U(2 ^{a]Bԧ|?Ĥ?ǚ>[Ъۅy0 /1.`aLaLӐo%*K6`%PkK~EdFTQy742v4j}OZ432(閊F&{H+2Yв +-+.u~,E.PԌ&ُV"˲=ǐ?Rpٟv'JvB`kK/`X!qkۏ_ -.G*1:޷ 9$-g)GzO~-MV4_.\ mB:A%۠D`ږ2"4ǕQ2 un\UL0?rjv,$aM촶t9U]|Iճ"|UR-c59pKjbD\|%vVFSŨWĽېw:!OR#Ɵis D5 v%*p,TNYQAN & SU b"4Kޮ?[߉PJz>eoS%HJS IL έD|BW z\&"hL?Y?Nq -| Wapl*$G7[b\ (a 㔙1LIFJ7d]>_f/m_xZ5p/rܡTHsR[DK̯gIpxs(`+bz0H9+OQ|0SVد!9{H; n73!E Bɒ`ҙѐ\t|%'pl fivC !Я͉hޭ)fD#/y;JpMNɹ;'!0J2iVjRbp)EYQ0'U:ҡLUO :s``KS*~{[)*!TK̩Qdu*؇^앗03a^B ;5Ĺk5{ToTn{J7%T &R[),9S&|KʥCIOS6XPpp<H\Efҧ[%Z$b(WT, - ppɬ>1#!]K*)PS* -Hmɷ bPlY<@+B-&40Gě4##kIWȩL;SVmpC:.iVF2Quin'+ {Hf((֒]D!S+%~\o5 jr &#Q- -_ gڿ(U`ߊj갱|#2T#>< ku9{^/767BBެ$?o,_(}ir~XV3je% '̩ZBK${-[(&@,°Hs;%mXuJgRʕ-}Aj -m|˖zƲڅј6{"6 7| X3iJOE\_XN{g(y&%Zg(ڴ2ze8ai3D64Hr<:%KB>ea -ҒZ-\ya|gNb4ےC `݆T=Y S'.H&96QLOR/Vd`aM؋N/:ve,;+ďTsl@Ru -헳. K >Hy]`ݮL~0\ݞo׉Ej\Hj75Z@5 yL;9?#P^-Ed - ٩>_JP_XQ RyGiJ/KA/3ssW#wq)3(]}gQ 7 O> ^!GxNTy*2 GYeJAPO؋uۄ":.)*/h3dD (bP<I!0oᄣ; %-ø{2Օ-2PV{g*|MebJS1pjjP(e7Ȕtֳd0DXoAxJkr9]$b@ FƂ?_R>B0jShU8e0Tz. /ȵ\l(k>ɉ l-^Fncc -?a-( `wGUW3i^Y* A\yQ`K[UYp5ۧwCSf;:~@ǚ @;yBlH~`t\Ѓ/,}#0Z*@C538_wzO^UyA˭{4@丹Wsܿ,{-cXL_A{eYw~rф+`[*񂞙Ōݞr!K=Mub16&&g]ɓ}|~a={}ev.ͬd;hSt>4&|WufR\u2X២AlsuY@V|Y<B>+<_}4Wp{$'q+ u`ڹb -{%Ũ_e2_l}蹵r[Op+y~/*쳌; ۨ5όMnͩ/NE:>]-|"1:YUUDyy~, RW$Z:Ř$uWPxY|[ b|ʴoO3 *}aBoFT_18}\#_HrmmG YK<2ɕXv)[SU v,ڣm,c!Ī M+L7yߋ!xe4 -äNMlϲcUjGm2 \҅JiiL+7D,=!jwl9p6̃ CK.~ 4"7iѯȳ-~R:'(қm1}9{gvS p+Cԙ~W6ye.-A_S.۝HT rZ%gϋInoj_ع [۲g'3b'˙vg49pwId-Z1FYp6U=;F<qkȠ5_fk a~#X,Zon*SZT5Y۩$5R/W#u%Bs#W"~/~%ũCx9>eG-qYY(閸%ȋey -|Ɛw.&~إ -YiO2o.-K Ź,FxrRx!Q}h.l~-Er?i qdFReݞ *_`-ңDȶqqҠ/Xv^⇻!TZ3o?ȋs0˩d 6t\=K94 0Sm)*m!Ֆx&YXE7[U%/T}2,Bo t``-..CH%mf|`Q.,*8Pn\ i{.,.\7./Z{kk>{.sp _lcI.d<d[a6˹e.$GbAy>1/?3Tj䵛X*{jاhy췕Y,f &cxozRکnSb=S5XBb98Z:/e1j2˟I -Sl܌>tn*^v[ -/^e"c]iqv :˃佃;rd->P\l<[Y -c'fܻVkĸ5.ŝ G˖ۊE\ lЋ;n[,Wq}C2̩VJ\`{Y+^qEeiR{{4T 'ڠYaS\!l/pkX{ax;eDAB%]]\`]M@m]e:,/cbK1EmCI#5.ۗP`aUQ 1lD;w -F[ -j]K@!S#0i/v%6ٛ|Ks[(R0% a*]vc%n _*L5Oam2Bbg|*\"wJ`^fP|,XIGFػW:B˪*l5N6vYZPl]`E7SV.:e2T ` X,#I%oyR!S~\GUt*bW_UBfK.5AX;-mVA͗P_@VX YoJ~Ѱ.`KĆAUVL(v^%YJl*5b-Ĭ`Ur |yGe\©D<;-c7HZolB7QlZMA|31;EШ} 8A|Ǡm*{95J lS#ZMȠ٤K.jqyl.b2͘qrYfH jcn -r[6,1D`Jr P>ٶHfG~&Y@_6eڕHT

x2 -DCk n_xrţB fyXi HURx䧈'X ~r=ы,^AcLN/!|]٧]edgXIaQR@2Q>r}DeV`,ctKSLa5Go\*+jb_1|;ld+ܾlRE]쳷qXEWă}LV-)% -2H\au5d mHS;kXa@(_LBotOQhҚx问SxnyRa|`TYRϡrʈumU_UϕUU(VGǡT/ʢʨ[8Zx#ffAs(v/W3`-9-LATBX/m2#3Ư5#U\7:O 2EFA؀.qcɇ6{J)gTƒ6%X*aح!0vmdha, %aW*a}Hu0Ʈ]Zݎ킱0eb+m[Rř C7{Yu5}kJ*S"QdD3}3ߓȰ]ZʋVx<:Q+mպˀTmFe>V.\|% CFWpd`?R>?\?x,}"`xYL+b J%KҚ-Cr1exYq[梀*[ñgKzüj]a ķ% ԲE(\kıy. -v -\r1}3ʼnPElVS ټ􊨖w.` YuPܱr肓Z&y;CƫԺL- MvYkeKnEk-4m,hW-h.Z,b1k<Dca;WۊgL.g\ qmS#m5™x|;f|^UzmBa;%56;f>c1` %Z=\W*]5K6t$y\)Yؤ2Y`lx`-Ő)bRؗاYw)S"B}ALW'G.mp P Ɠ^uPPj'`$E 0Mp߆,`ehy"@ `ViW> 8s̲xTD(&(1YߢNX~"̑Ϧŭv?A/SqjAPĻ^1@aner^rEݳo#aFˑOQDt-"R@ZVhF@~Sm ӿ³ /{;Za\8,2^7]XEp)K2")fwQH+Ba&%Т&{YP܌IYQq,T,x%nyX7"6`7[ܾdG -= ܩyL'WD[d< .~l̋;Y2!,ƹvѨ n\[9QٸC"IS{PDZbb"mbT._U[r(@.1ER`]k~" vQKڰx,tWU1YEW)ڂWy !E5Y5Uƈ`0%lZA๜c{")DÔA;tEJJ6l?kAr,_L/I} - kB-˶0{ vh-Ma" yKJ?(kش*.F|7X L!kHTZ1#Ciqh؞ͫ73BiDB2 ZX;*'ej]TJ54`oED-f&3Uq -?xJŮs'18l*މfTǨ-eCW)uan\JxKxoB዇!{B|?-_#['D{K&F UȥA\F@^nA8] w!*FϸBz\[,d%Jȩ44Q<28FưEr(Q*Uʜ%>26 H Z8ZRKr -y+.J%Ѡ+@<˸i\] j'%D-W=EMl )ԶP+`' tO%RbDf 1> ]jFVVloal_i?hJ]웡ZR*')tNŦg$ ]ٷ{.*pߎGEM/n_BYa7.QK,ߨέj'lCݾ%* >–_V᱄y-y*<";:'%V?Y]QT?j=֬&QQR>.L'kExERELH( -sv>}_Ê,H,AQ"qi^&4<$QVYq-uyZ""+Mij[B€y5[FaVVW"~@0nNf"_AU-˪|6]EZλKʹITq/ Q2ZaPE8&Qh6EA\s{JYbur6@GIip/ YV%] k.ƢV>}Wc(уQMPvWRbۘJ=% -peu-b j6*h#hk֥X8,b(%iḻdv~"lRY e6 = c$eI*fhAEl<ģ%Eu5),oujVxV]|QG-U., Ywb]S: @e-?-d!z[AkS vژ# -)ۚ c%QA;;,^%j_?rb%|[Kavx).gAT%2SVR{e(IUoqJj{ŀ6fͦ]tFM dȪ~+LH)u87حXO B4KgA׈eE=/uZʣr\s}:3I/]O(ENUPT\Pj(uPP)-A%,,ʋ.\] -˜&va̖+Q-TUY*QL.8,56GNxYƱۂ^xn+8{RՄAKZDۖpy\Ac'EE6ԽX-g@&uql5ffr&1c;sz|<ʴA uurŽԱvS<6Ӭ̹~Wyșp!w4b_n{9lU\u-Vy-ȱԖj) 2T.kQP K_1E+#JVF"1dKWM94H=5?]ZBi %ȠSU(gI^@^*-XF}M.zTʸ''Ur -qc`*nhQn}KP\Je`61EȠ6VawB G. ƒIP|;R۪AN؏4Wܨ -9XDni(cg?sD,YJzF=v -J i/TVs¤3l:l\QsF6^P.T\n(lWVqF^|OXZl,#.Z1V[, ~y8bj d{(4C%JpC)jV>h2՞SpU9QF6]]*jFqnhO743*޷ٗB5\:Kbry3ĥqu*2Ee"%&\sXD 2Rew,jj P{3T#kTkm1Q*y+LU[cN4e w\.CΑ6mNy6DҺ*teF - %f.٧ b>haPtn H1#TWR+rn+bN" '.G+ȭc YȷĶr Fp lnB:4ZOJps%]JQKTeKrz؃XKf~Ęv t )/JAiv]ؔUν>%1>V^T6k|F#RaIl- cd]fv {4˼{QᨁvZEр&7{Υ1$K̨2D{6.*.GZ{TʰЎMMyF'"`'7>w^l`4yf -mh];0 (yC6cWb -VJ\tO 1>|E*J͇P:Q!BGk - {Iڢ\\{?,r췗+Ш Uu| `ч/6ۚ$"T0'pKjVbUA صؿPoQnA6?H`P Yg*d5t@Jed:1j%PŸ.@"[%C+}ܷiaG. e>iP-y:1h4PR08=9.w9qeW(KVB_.Qɖ.VCC+uJ*+dA^%cR߇5]lanUrwX/ڪzFDU; t7 WNK9Q`c1dzB.> XAk+" 4 djx[H%[PA4%u4.`%*&L|QDtsAc7Xnŷ^N Dn}0cU߈R~Yށ?L+%/-y+<0{e_e"Jnl.$V٦"j]4L`' /+ 0Vc\D*UU1L'~t1+iFƊ FjtGe".l 1 J@'5Ph3VX|KocAIJ\-8ʵe-/`WKSqk5 fԾJʋ'%O%EՐSf[67ua/e2KcZc2ؘW.qt!zD1O5X b1"0{%Y'@*_iZ\%On`Ę6@Ty^ =~.Rx\gRX~ WвT})RV-'^eQCk8K:@ Ib_ :KUf/ rseLTI͸489mpMg, "{RS9W(#@EE^vYۨUŖ:G-i K2͕hfS(j֙Ak=ۃ>(;1b=BARY|v:)gU-cGQD'ٗ,vK@<Ƙ첹SDC}P=ʂToD V+\% -%LφlUؽ3/*B8?}%GV/ꥆ1ey0*vu|""fݗ+xAPHeIw/c/z\v(.j=ҝKe$V/G -גoj]0P# ut[ QMo4cprV]Tǒ},XM}0JU{XDbD0M -55:Y*#NY0j- =N `UR -WP8,|]LŗMw.9.? K/ijW%zXw5(yK}[Kxiz@5mj4I̘6.̲H-ƙP#'o9+tj qʴ_;Ue*vc ¬:SQU|ε`]FPvD.oYW_HuiGUp>H--r/B[0zrUn{_!OWea S|k?KW ͳXFj+)VS=jQ,K-v* -cgP6R{pl,Eu!j.X-vRuc+(ۊ.TUu9=Nq,[*Zjف@[Tc -(൪Pˈc-4q]#hf,4ci.V-[6-C?sek,--Ok%+PnYa&QEQx#y ycpE %Z[-(^x^EjoMWȎ2xl -YDm b(QPen!.]=«`ptb58.{U8v⯐>W~bͩ"߈YQ3j˴NQ;tBഺe-\7Q|VLBqKcNBE]0N W\QF -@rUJacB@֑'4\mzѪ\b[(26EyIZȀRؐD %챞eK0/jr}ƚxFi-`ݬqD^*%캀x{EUER>'Z9)ׂaj-dp]l܅BrόW#0Wj}"QJαh02>JvӫHKb_;)kyKb\͆ ێQ-TlIpMmNB۽}f,j"`2 -r7VO )Fm5Pe۲~mCPn64G"Sɼ -qDH۴@:!KJ^IEIR"V(ULRc`KW{ʕm;+md, -P{"ok^"j?J[eEscU[EvS]E6_z|Sym_}\ yGC &8@:h+g&-Pw|Fv4Ø5.,heKU= u@:W"p p{Caq/NdÒxiۆq_ru+Y]*lif5u*.RKCEW} -cEPeʋ]*rXE[lKC_CU جXr8Xv1 Z+D#VO;u+}pUD{.T&cp'yHj#$3 dSR* *Udp~ga֢\6ߩy  703K7UcGRJ.96[kyUdٯ9dweJ*WHRA2-ςĻYcmS(*\.-3 #nKr̠{smPAԪUkfG6\X E}^ ZP? bRv˜ Wp^7 -C~aMbϲ u0 R U G7OdCApIvYR9.' +#-‰e+6T(%[ˇ^L%~/WvUC%bᜆ~]穝)0>eRƘF`dLP]eb@*[/QS FQ.)0ߓĨ >&*eX|.>frD:ǀC<Ϥq eǽ}D"@ti骊I,WO!o@51 -A!n>,$ogЅmD36Vme){P~qiC0ϲSbSPEU:1!lFk1el%O+c]d'/.^|D8fRr]—Оw<īfQK(E\`$RoH6Cܲ4fmZ\Ȝ\ 쪸% `>c`g|V53 EWb e7߁Ĕ]ri0&Y8:~u]=ޒ%Bu -1O3=9-'_uQ26* ̝5OF -'6;mIv#Cl %[phMX+ {R2Wbce0`K~Lڶ7Q TRɶ{фv0R\4J_pyX!{H.W{'r_ ,Cf IcU U\-PDQْ\vf[ - (l%Udp`x九a'ȦNFɖ%oru^8UsaALEN[er7Szȏ%%6cQDF ؗ )x&8+ Yz OLgs}ܹ.6caB[,hĽ?p dorȶ e*69 WQZ3H /'єd$ q/pqUGmsNk,ddU/+ VJ.@]݀.E+" JMgŧtʘRE˩?$~hWZ[K*hٌ(Z?T&Zky -o 4D2= -\pN՘%K=`Jj틘_о;6b)cmw_0 5 `{oAR -xSFW*[D3UZ*y^o.p"kGtg@i]pHd!ljUf+/ܬT SE-H3npU(Zk2 -r<ƥ0RR `,{g.u⡣O*Vm|> UQ6"ZTJE VzPq3LZ)l|\CPDOH(j!+4;j)CL5Fb6k7mpԂc|+bh@M/,jԨC!# Ä JF$7>+٧{w|U+=З&٩ P&LO('iT+tZjSa) ,6Z6m7¾;dѮ3HZm*`)LWQ\ab|5R֬Sv3j03Oq+vIZ[r[PKVJfi -$t -*Mf 8֛Y:ߔ]a̢(Ħ IBX20ׂ[Ez&eAJbvVyeFhJeGc4&^+DJ *ƥ|c[j`cw*(Xvvl%Y83)0'DtˉP{Rdlbe٪ʘV_lv2 KݎuAwJ*_䥨Ok }CS|SUxEF66ɞXhyUuرn4rHщBЍ.ޔ[*3rrߢ]?{}(U,V-)xDqTC>AkA !s2dp`fk2hb! +ZvƒgZעi8FF5Z5__S L KQgDg,BY`_0u0L 6ql'Y(e -XK&TqlIB@ # aSJܵ[bmEK\ #3ޱdO`J UJx/-1- sf]0cNL_BiLfjB]x.Kݖ-.n0db0}i ULҮsAvrv&Km\y*%J.A˨QR -Z1]c+sHܥ1(k1GUq)uwem˨8-. -W:Y TaQ"+۞Lϝra <ԼOK؈r]P -Y -^&]݁JQٰ7+F`AQ~0“A1]UƓe/;. nS+- -B|zKNE*e: o 1ܥebLdX غ-Y.]{svJ f;=/ ^±Z=Ü"DP ؊Y~e#(Q0FmB/K.[ndS- -q KF^e }tvX*U,nYqM ,jq\lU -jdGo>&rYR䭹m3EKh5bv d`dL{2\O WM5p2 / fNm\{Jnh=Q #ʶY#\)BU&Q#6/`3V$ى\BQd5$;.^/mWœ=JfјQ>EG\M -BYlo#?r|,`A*] -0yJfh/:Z~/s?*tXb" %Z۰M& -&Buljײ[r_ "8^bfS {"AVYrۇ%ɖxCqB4ibSşfD- L/Ri <*W6CltT"Rwr!R_RĠ@ڢ, EErYa;_e:DB1RlݟS0ub&\éDvX6# -!?.Dco~ZW*lL j%@]:w \uպz-T{b>N1+VA{[=p5V -'ڠϑ4)e샄vY"4UiA%̡JZTruYETO`lg8tW!BErlDR!R"r^[X1KE|QȅE< DeK9&W6}l:=b'a^eY]CP*WP<WD-K}nOgZ1q+X?PWue86j3IcQeGѸ@r@nU!5E'0~Z{S-rw - i 'akij4?5~PJ5LDMJ LjA!.7wLRGUÑe" O}7:tFVHgWo'V}`G-p\ 7b:~+r~Co&f3LFT@{Pd]S15[\}4O"/ -0Ur-*-)\څ2gF&M<춾 - B%v)<.E+^`li72*EbPnJ"]㰳M@mZ"MrUG!eʢ'SdW#{ ػ̙XNe91`xa[QY? [}Sw?S {_MXmWV -d=/lRtlNk ~%T KnWeŇl+U/yX8}"1Ku<ݭD0AYGVsKfݍvšu -m݊b!GX8E> aVpӟ VNLYsoujin@ہW/kjZi!^K`>X~ ~.^i{WN4`2R*UlJV3*ضiX@Z - -*ên _2q樋V^MVOy"d(*Re͟SLji3TnT97Y _*0~=FϿL&RLc* S]LRt}O@:%ʏZ?1?ew V6.a}x4TpWeFiA[B -DN_ &͖l~%CvF|jJ`-7Ijv^Tⷑ%˅*JX/P25K1 PQJ^W%F;(ZKKSK ݶD`.g./*DVəmJeIKE? >Ľƥ+e,߆5'`(#P>J`}9/Wg\'or<~clJ(?SMa|BuQ3؈68JG6Vtwe22Jy:Jߨ[;-,0&<=9yʦ/*:PEG\wPr8}-x-n-85sGN];(~vpC)*QAiO*`RW( Hku,@jgOإT~:Q.IJ麩XyR3@Ђpjr=`P^#nYYG'VmK){b-ey -!uab둊XGU6/O4sNENRڅQ?~n1Xܧoe]CrQ -eTP+PV3Rs̷{ JߋPf޴ -+R .)2!E 9QOi`pb˯Sבֶ%m!~eS˫eW-z%+(B31!xɡ~,p)~,(TBr §7"U nk\DAn Xѝ2ZdшԥKvȠJ*iO${qBg%cV58B-(bFʎ(#M -2AeH3 ue Ɛ<`lZO[%Qmó2 =-`q2kp["/ Oe'{E/jPfK.ed Gv *9QǀDlX.gQڙYJɶ|JLHYEC#J/) -vTlp_ -ܦ-DggrcДz(w0s"8ugn՚uZaUa"Wѩf޶֚ -TVExrRya{PKeԽ|rӴ -|6ğD)ض(RF\v}e%jɃ vV@Lܫ6u9Jؑ&?#k,VW#sT,%a<5{6ыK/b<76,TG(;:1xڛڦA-J"%_@ċ;-\j]j^U'ʈBMX!p*\)'"69,jমq#DȔm٦ܦ伯&u`J?T<jnY T/0|),2^!/i<~; PNooڲ"E[(?;fM%}-֙ Z"f6# 7r͇Kws#%‘g0d<v 7qo)m+ -M\7Tk%]7,lKUyɊƕ0w: L)R͵>;r*:4jA C<PPw%@[ u-4x'̬ų>)ڡK8ވeA5<?xj4k,xyU2bŋ-ON.?Tc av[/"YQIY{2/%ʹCvēKa+nVwBeъBz PNFlaS&XBʝRdl(/c;)"H[9*P )q(^E^A/+T[`j%3H5Z[+YNA* _D^0ߨ܅;ZYSEjl0 _, %_f -/Y]릎%j_o̓rPLye,McXdL<,z益 ]yG_tHnR],Kp.(mFK+bE9-jY`+ ,ШpaK[pr&"ZJp ˗!ֹ0 QAAun _y|nتb3&SlYU[L("JzL,EqedS tW!A_Z2UKe {m߆Fۨ擄]>X_%o" @R^32f:ߪ$Ҭ0P Y:r -=*xl.8 -P&:}0j~6ZE+*&lF4Zm -oOQ,ڙY-YD%XńUُܥ4u׭_{0/J7}ڲq(|0z }TU&,8A+/>*Er~ XLts`~ -٤Ae׈m20V `zJ> -<'M .ȆlCrǒ1i J { 2XW#98Ku.Q2BXxqSWl`7e41YY2j"4r ׳L;0aBEtɔls%<6z7%;.JȬ-j!g?s<ؖC/y5f\a>.Z(v-Zy3Q#ӰTţ̪}KeY.ʁ8GyS=>˵R";d. J/zT-F;H(q,[D%*gPK5hL^˔&߸%@9lQ¢^Q - ߀%&2V\ Jk@W6צ\p狟eԖu>N@ղ;?Y(trkEتGcA׍ڗKQ\ \o+%cvAjyJ_')xEW%$݇ab0^m]BOdw)zFW4շ/ "T"\yȜ,JjC9իDu[,0ZM 6"3@bp%)ud@%%2SMV g]z>/,x݊(]<Tp-jxܴsgk;˲ȁr vR/aZ܋XU죇>H[<\iOq×.piٶM5]lKr\~ mƮ]VB$RTq W" L aH8ܩ]@L#f>7SM9l@Kfukʺu/-jVLD+~ KhǕ*I[+%=|@D,AAJ.eǒcP;iAg0P.dj-[lVLWLU0<}4!5R[jwQ*j?P+Eʕ?TkCP -a{{9(Kj~(8əH(ZePch&ӌ,լٿ3AY)wAV}26w?p;Oew'ZLE4_ہQ| Y5-vF8egb[߆UūBRT,RnqéM.Ũ4EG [-ȷ,2I _^|(}[DH }%H\|#UEE{BהQ+'#W[]A(˶cr.mJ#allD}&G^PbT"-}A؆&~~焒CjݚWمۖ.GEYy[ooה2m/.rw5B,kV5&i5**yʗeQYx NKvԤSȾcβ2o"c~9/n@ٜk^ '3Yr"MYۊ(afeÐke/LnGƜByRe!ƣHm?#:Wwfr{zP/3P +3oS?±l?'ߨ~X4v<ڍ`*ԣĎ/XEF:JIF*5=A_Ĵa;sB.ࢡ:`2+ir\\!]1q'&ˍJ&_ ɵ8K lK-#ض b3A*`oa5l˿#w˔, -渐PFG ݽeIe|m.6FY;_7WELQ5U)y# WŤ+* j/`J|B8?s@]ZSUztňMj12*c*,ۛ?b(D}0UzE"NL[zEO ~\,}cym}f"iXJuP~ #m {5%sp5GF |M65/8~HB7BԯQ-mb LYwljXI &ORg"ӱȯ,PEݜY,9T?ecp͔ˏHudVR9F@*5nrRZEd.?Y~%!˒̖@\ l# vsp< `O4^>"i^χñV -?gN F~%F`P#/(첱6Dwћس/Ҙ]:*ށ ޿&;L+X(7Oi12gQڞnV4-]9"mx\k8& FSU*dS |fѧ -=A_(Y]L^-8jޡ-8H8A - OLlp%L,I T Q9-J%2P^0̶quB :FQLnʨEy7&KK[< q\Ay˙6+5ʖ=Ĥۉw~ !fq?"]d-j4VCkJ"ٹ7P\tcLb:*'BTtjJ1`x lE[ 1CcZn uk {DVM! lm_]E -Pxku \^+ˋu\*} *Yl_{Ukn`@Kl@!ikj0$"ހeƣ#j/ui3Jv&#Ҡ:uGԽax,7m)괎hl+ ZyUvM~Quq݁B JI ڵBgeHO҉w5nFʹ:FGwǔw8hkaEa,>Who -o*lCmԻolysJ$s[G-)`b#)n:DC -gkŧ!g* E;  r&.mxKn*-^14Vv@^\u[xP bPTt"dCƘN펥~J[ym /!H /.Æ;\qrG"-Uo\ ~al7DU A9*~<@* -v*0揶o vJ7in6*0j iVk[ - - -.d<ꮫ<Q0Myv@Xy.\ K0/4CDE j 3dj!v+W%W6TG%ҥWano]>\:r -[#៘_mTu1bxB!p\=VɩDO$%Ѱ DT]Wab [6G40RV[ \/n5\͕.{,>iK~V"Ifmv5Z5*Nʎ3 q{Y~M1˿.۪} FfP~d^ r,bo erQԥ|i)/UlI?|X8vxn>&~O% }L;05qZr[l+o|Wg9SX+^-UJ,\ó*0<ۇN3ñ kijVo+xZJf1EHZˍrz8bL@jS).xbd/̴ǘ$V|aՄ8(1t#YrÄ̰'L.^lZEc,rus>.F-02+Zh'Ioj&DYebx#rwL̲QaԔϨ%}ˉ͎Fc2T>.rEK+jUj"y ?"j[tQ ,wȊ[,+`/[eA)DRSlԫb%7| dgڦ.]|a_2nT -7'{ZD*ړ4 B/[_}LmaR˗l.+˗aBWjIoWMbuHP]Kd(o!.SpѢ_W 0.سgYߨKiql2/Ό+eEG".g3Un@xaor%XYɑItC 扦0iĺ_BV-@G& -}C?1N^Ař<> }\SvxDX ~;/gc_rda@S=V}K`n/ԺE.~f`{/SC~iG]{".J|yl*x% [ɾHreR+)eE\%%kS4'7ʋ?2ۦ- ꧛f:v]TT#a.^O y=ᗛ. n r/-V܈aP-b e U5R"\R9FK;1O,_QFUl!/>\<h1,ʟbcPidhxķ7Vo* B -p(0~媈وyW9g͍f|=+%KmM.< ۋ[Ķ6{5TԿeAqɮ -7 O(X 0= N;ʈK.wb P ocl""Zw):eY*_ISΙ1| ]J/xF L ~<4f E2#̢] \U).M-`vP @rٱ 5LmU.3v/XP4| 0Ƿ :vU؛ʁˍHIImL&8yP56O~6#υLO:7 4y<Z7\!ە́|UUEdzarȌ'^B+&c Wv_P.&KΥz\:)XT-zZ+@Ts⠓Ic - ʂQ{GJ T{/)Qj<ԔeE}츰BO_nU\rdN&n+*Hd"49YA|LJ),W ӴJtP+V@ZcL/N -Ɵ.0eh!VXrъETo4e*r5m閄[*:]J1q+R.2*oKY"PTDhBmS .. ;uT W6XWk/F* ktE"9E  mrԟHwgj -j ٻ{11Lo V/04SC;˃؍gUj}![f >p5jXuz:2zÌ>%ݏĆޒ/*>?bo@6J-`p!UJZeV@Ceu34)jߌH`Ypl,EF30$4 ij*7aqx_X DZ]\`ߣ!x[e2%aJKtQp!o*%Yuz%s>& ZpD7&P \剈L*9iښ+mP*h[BxRE-I[[#W-Dz_cWe1vAҁH~HRm!$JZս >04|!U.$xePzn~(}␼F`u#=H /`P??g> |wTo4H@B?t2Ą}x,>hL1E)ӝ߷~6=4),/`Wb/XTd.*=#Bʣt S-^Vj\R},jΊ#cN9X5Y FoPwu.~g,H6<7 <"N@)Jw R1h>ýHk+RH}4~~&Ϳ05S7ԍ}G*^g Kq;T,2Y`(Ïp}9pP?j YN?ÐJNʟ3toſW*V?űÄ4nS7"xEKzU0?Cgb' Y[5\Y/<4@DlPO.⫔rnhC%~7n$%[sJcbG|R|;"WAh~22e]NQo4O pSQW|B%?^$O^[gdG~,W $~+!HgGX?,D;#R_$c&v^Ev{?ʍ?3uד)Z7ihn~l6ާo#8lD |P{ [F|lȯ`B$S$E?|:Dcuc[Մ:0Pa*[ieГbpG`_ng'X>4s"Y?> T|ڿ#PxS?}0?oH ,N?Gs"*%YY4ނ7FO ;''QGj &ai=IWA E)q{$WdJ$-LROCUu?FCX|?A&؟O7_ZU{TSר'#u1I{^ f:X^TQ(Ɗn%% 8*9w}@*O ]\<n&fEP_fp(oX/bA_}"9B~?mkDŽGI <|Ks|lJ|& 6n3B _T~ j/З`[e|MQgCܕaіCJ}gbCc_ Mc~?F,/bЇDuBe]%}G? |~[TƯz'DJȁOPu,b~A!5z Wl,B1rv{E -F4-AArgmIJ>{"ے+ylnZd?`7f?7Y Y -JR&è۝v$Y*6Teo#-m nu"&o4Sר+.dU&ŹOZ|4 Q^o_nȟ6.FT&Qg$~&W&Ge֖^F%P~)_Q\nBf(tNťU1 %vaˆmJuZ7߸oQ_h,X{%E)Ȫd2{qgc鍸|߆?љw|#jYʝ ϛ'!'" -^g~ -jFc~{0*#?0ŸkY"(,O<| 3?vi84CLL‰GxOj_{AB ~?r?^_H|n=YV4}1,=?{l򽏏Q6XA~"J+{+B}EeFws5o^R@\6s+fRU!jA_iLKZ:~ + H DZ_CQO*WAG⶿j?ѿ򰣝4>?DO{5AQV;"(oFOF \/?(B~^EWWQ_V󋭀:,Q6s"M.?g嫆ÉkzAe3 -nwtx9We %iv, ۋ2=PA`,R٧a? eۖL~U­Zɝf?4nQT>veYo##O6_H@s 2WYb}MʟК)dM )`sÊO5ngR;1FyHj>]_q '/_yX1kJ+ HqO[4'ڏpQoSL?]O1Ÿ5=N~!V&4R#tzA*=I/g}SH?9`1 u$q? 5OLςm˦ӛȄJ}&yiװq oe_pu#HA GC.˗lMpnb=% 'Ma._;nH H9F4Y1t@v8i?.k ɿ&Vd*[VP `{Lqۏk's?7`a] -sAq)~TqpDO}(} -}}uBZ~,yfxݧpeUvǀX%kWP?R)?>3ݟ"22!MIG~[t 2]iMfQsKbFlJ"7 m;TQ<[ղn"ʂA/Qy -܁6瘁SԨD#C*-J#f?:߈9zLQ0 XkZ*473`Ɔ7*@z`c𿭕|=O75N[/*[vq>*g*6ο;W Hsu*ɓ?J0:q~7xk?Ko>TAqM߫=7騟-0(AG Ca>@E -~+ȟ{Jl VJ~F(cEmWmC^sL9Sson9? -HU `}cb5K0/v6G`cnNk*\JYRLR/+"'Liqx @8"6S35.ܢ -7+GPZ23|X&+ouiܟop⋶ -8C_|H֗ӉhY.LkeyuCF//M||To?3??UoWl@ū-={pxJy:'/z9a1u*سJ%?6?2qB$ j7=K{x Z$Ԧ<,eyy`6Q+x=˩K.;Q\eDY!t.T2\.5 ReJhK_?*ZvgܘMd}B:^=;+_Spmn -*V>h$nߕhW㮷w?> -$27M*TDBm?y>6oFyEz3owSgK'=CdY5<(=tx"\vw,e/툟ܾâ+?KWyk=,jwlB}L~GٚſϏEM,7}Z -˖~S Z,?J?#]24ռ??=+@PNUG2_{'u_;__7~PeFU;}.Gg0#,{aH>'DLxg Jy;D*RPJ!hv˴ۗETT(U2v[(ݨ0B @}NrpO7 )gz̼4I3Zw!|km3oA_/s _CVSgyrQ ??ɃW^YVp5b_3M?OP3d([?҈ .fO#O..28_71Lu6ϐG)aHc9c!Oü~"?="k#BJ|%~>>e QPi'oȕML &x%'!t6a\ˠlBr[\EZ\(eWeC%!aj}؆ơP+W‰_RQ(%amI8k+JIr[قIO#U~!?t1jWg?i*+ޙw/ }?yMo;efVbш/ݧg C7QGS?[B~㘿eK%+'/Q嶯 =aCJdvG -DLar邿ߘn]=;/A?~D̃T 9dWzː.b ?$}9tS V|zyaۙkPWN_fJo7ϤhC ߊf~MIw-䎟΀3!L=Zue#:T4V=AȄl`ߴ!sPJ6Cnh~]{5q'i8߃ٔ^Hm_+I/mE?˘?1}A Z?d2,{LPe 8W  -_:-lk o9O*U_="  #VC'k6.$@ȪZ"쵊a.=Ì - eי^1U;"]c -vQr MiUQ?GS_hwg>U x"ˊ߄"Hg_tm|_撑/WH>>t6faUgeJi'*Ȋ>3;Du =&>~ CɈW]KU7w?Iwܟ}! r̀[)E?-vaiDQ˵3\?4bkd PO-cO/}>l -YAY U]d[(skyg\? DƑqحTVmۄRd灄p*o!||d$ ^~s*<;GLCg}_ʿ=|:R9E?pI5:7G\'Ck5=I%o{'Ke9oY 9?d1A~3>SqRo`>}b?O+[;u5Na>?t*A𗸅|Z`PCRO<.E?߉_%?L [pYu/1@.lV*iql#b%^aUa.^Kb.4ȔfC_ʼn$JIR6Q#l\6nا%O?C'~u Ho_Xs>D#{>?U f j!|g5OO&Lj *όwh=%b6_xg_Lk2➾ԎTX7``>kY'SD_,~lǴu1ڟl 'X}vXCi_A3[2%1i0A[}EpkVumH#˨T+DB+{ܥj2>xg~uq&RWH̜?ϓOOyE^_n. Y?E_O~8W?ahWXmY5?Jƣ\|:W+ȭV K1e/X]%6#A3T3LHQ!UiJVaC,'hkƦ>'+wS}ޗKE?'iց0 X_OXW\VSpHEW2B Pb0j[^Pkog  ˔FbU'Z(,-*Ohp99}°<\E -A`2e*Pۂ}f1NKbScs(R뼗dW ۖ#}Hs%Ss/,aR 5I -*kmw*"|PECjMJ򔄢aT7Z~Y&fdI0NUE( --@=e9!%xԨ6&>2uuJ^bZe9Z l{FFV-4EyXËevO2\7_«\{(1&'ز;KL7+Y%[l1e\^\Q=3~Zc cEDg\w(# ^ E@DQh ܡ94.'%lř5]=K@b%Rʍ1- YCܡWෙ)w cS2 |ѵn41G:%]7Zi} 'u6HTiUU-}B=؇j6ܬm.-E%xrزpeHzq_"J|e%FJnVTU([oX`M_O6;R(JBrMcݨ80B-cr[NDĺ"K`n#˩tVW(D;6Mv-nڹ1rc.~-S+e銡WD򾧂S(7/KD~eeG嘖xZO1R~ķҶZ]K|#GWDWE3JVBx& S ՕrUr]}z豞Yz\Zr˗}NKI̊x.Mdn[sc-w&~It|vU2 1c.}З^\:mw7q p12Yezv: Aeklȯ0jYcRi.pYز5\)w%ab "K&[FY6<{Pe˕O!ew&䧫r]^!Qj>0Ҳ#ȝ>ςg"ZbԢ{*eœ-#Wuz.u|oܯ7 W>Tt|\ƪ_JD—_Qh}S-JaE)`RT] xfڛ17RQegpMa5!^I!   j"چ7Qb]N-ߨ%#K~EeAe17VGg?06zJRCKe2P%p^ō,-%Xcd &S+>RP_Ly%k%ܻyl+j=}佔ūP2&U^%{n8r!cȨElo,.[ Y)MYC (?q"Ѝ9_RTk04e u^|C^Mm%[~xu.TˁY[p W>D_?yQ(~?\v~,' -]a0'my 聭@L²`u6*[<_%:ve0,Jv.Hk3%9(E Uk)\%h-rɮU郢nQت)}h|3WF(Ŧk)z܂Y -E:jpg/:J't-9]u'K#+&lĹevi->29GepiQyU+2*S`6{nd6#)xru6Z "킽 ȝ%";-jOgI7cَCwU ,K_QEs,.˾ -+l[fu,_DD3ڢR9z1+z;@e}m LfU,F>P ^%Y5|[apR7eO՗/ޒ`LNzPV:O]+g.?{-qXug퍴VA6=\-I' --VF@KqAPI(*?A7i_ؕ]^\ho#oY]_yn - -&'Z F Y蛑Co+ʘx\"<#+EEZKQwKЎlP&Vor8B rfvAGKזSZKAˢpܕ,|ƃ*x# m.4P^̋{ )?~U`HrBښ@,VLe+@}/lJԳkҺ -ۍӳ(8]0;W, p!6#tWaAX G_\Qva16纁A=.> ]LLG#Mim#Q{KeMbߚG\.yYm;*`7:[N~f5Qr[#AJT:/-MVjWȘP^gUˆ2B>gDJ{X[qWbVKCɕNJmˇT#dW#& IoS:RFO7"[E%IbQhPR(Z*|>ՓyS -R㊣cg3E1苧D?pIi*jkx y=Zo}YMJ%lgKJqHߙp@v`^ykṷ+TKsI*(TZ;*q QqM1K{qf\HiMTtK3OR%^N {KUKbWfZHQiTW[xG`e*Xdۊ-_j)r!U˦juYR - jTXhZ~^!'V_鹬42>,C\刖Ճ٨:R؎LXmύAw6==x%'P} ]CMO +$cXn\"SLbyR!ʇq0Bz_!(TVFL୯nq9p^xעa@q»RlREo.<{RU#l?D.uLA1L U)ɾ~9T)q~"_Ѹnxr eED]΢p0^K:8eŅ\^DͦW]\%K=<,DMT^RE rse~/F`t#p§pr)AQ#b-#Cj?@Ĩ$bKub BJXKѥSp -£JTPZ' ZPOxu]4>joJQ*`!R%0O)̖L;Uv3>[__){k Q]92){-% C%Mr[.]Ը˃e˗s-| K`יݶ(1Xˣ.]UAW# mgYdb췿 ;#fZez2;Q i/K,\`2?zKrDTPufɐP`~ײ#j@K7 r@$χl~D%="6YLVlT+En]JL寐P+[}ԳሴkFS-#o;/D$Ư!Cb]*2&C[glv!J}'XGEs -"A2T2qj5vl|pvbb ٨kܞC aD1#U ef\YiGlir-Tc&$1{^o !E^Ķ2,j2,)^eٷ܊J$7ڜKX#Ii/֥,.3wXKZp>ˋr-7Uv˘q.)?_ k6莺OKa8,zkҧ.wcwu17SarU\@j8{L cU"}DX-*S>\`ql勊y pH1X!vhy ŗۖTl,.^A`xC qƙd[k7gT,;,7'w_Rń݅,> ˎ Oߊah**9Q%)zdJ'LeŔUወ[v6],,; B\U5Mqr.Q.U]K2Ywq| &Knὗ;&,Uר@/LYEKغ}sU[98E%K!jWl'!r.9",ƣ茢fCfA6 nTnG̹rQh;4ȔUu7qAl'IMT B"]KI`ܣ%##,e!lع~`nϳ",٤ I}1%ʹK\{RTu# ES%Gr_hɽr]OXhCj.xbayyzl!K~%Kqa?]^WǙ?sY_n,M[[A[g%Ke@XLڝA>Q}@qT%Q)( |0*SPo)*WGԱn/EdXSAL,nò<1Xy%+LY&L{R\z%]JYG?12lR6VN\M@JgM]IJx%>ZLhSp`DP@ڌ`ad"i\RQȝvNPQ[c|4Ri?rn@r˟#N1ob]BɿvqC{j/E3&6X{c%#Uw`_1j)EDr$]xEe/}Ȓ]>*&~ 2E3#+*r9>;:vX\ rTjTa+V@"US28v;D ZP(*χ;1o&Kn5+\RʔDZHb2YD/.ljg8俨lheC^`oi,0jW .+ieXK|Oh,* K#0mmZK۩w/vQKv+[xqt]g2TK6<;nvj^6W2l -rJr=Ě2YQg*V4@>+␆6;(ZfL-͗ U -/lQrU@1-'&l2";bpڸm",E -OEeq [U(E0wR𙖾`LKK#ev},ԡc4*a_oM\zKĿAo/#NAQH5L7)6ȡU*ioQZ@ `}N;eYrPDWl SJJ P .Qmiʋ}Asҕe -6]ů H -<B὏q7y^xWٞY+1ّ8<{;u+M0[glfQ,0bEEE׽ O4,A>bbYSb? -ϐY .5r`! j,۟dk*,eRUSwN( M1˰AaDc( - `Dʝ1 QJlʊQ2F\ -0bqf jf*Q%3>ԩW{;+d4ADKrS2ɶRZ :1980n0 V,I^+TVxv \l|—[h+KbQC06'e^,m%i 6T\? ܭجvƕTGV!BȊ#wK7\TX)!6G TUQ -$SC#nR*XROi6eePq]noUHտ%VkR#cM0QjJ Ă2Eգc2> -%\g#B DNGx@']ۤǿò΋pZog%qu;JѺY{@)ښnW`IG*YQmDvWR%MdXYɲ+)["OͤT;sn/U'h;Z -`M"UT!U},BD7jlKn\}@h> mbNU@J.QIrQ!13ia)VBZLC 1Ĺj@[eDtJsȵʕ^D",VvTyKB Uu:'M] -6LBbT%oDo -HJt䪇̚\ȕLХ`@yĶ@CXǒyL<~@ #ڟP7T\ vRVqXN 8a)jdF5S/`e_bYg5 -Nd=<.P l%\DF9n:}<UjK.v@T WUQDKCo㌞dksMU`%.tnY ̶?yE Rr+~n4K%&k9#_db -?qDs*=hd: o3<2ߋ1Xl O%:ʀVSD*:rmDi G,2,mUñ4Q˹G!êۈW#~PT3' Z`o>W#sЁ~ lFOdԦvmn`YL x|BF'<íľJYUZǷ6 -4FgS~l φyԯPb\ 6*%bܢr;HZH l^c2ճ(:R=(RͰz Wʔh W./Sl^֘?E}>XJe.R"n;kRz.qk?R3ǫDfں5^W -mG\FvMǐgqMGH&@l-o*6y)V.\$hr\d!doԯ^t 2Ul -g)lhFX)Uʜ` ܔ\ET3L -=JbP,W Yq@p6}p[ǻ =Ɛ!UYY+fFger&x2Y_rޠnPeJY\S2]2?ɘ*#Nijд~yJDE2cݑ8?<Д7: -k˯jh -#`T&-L ,AH%%Bki-:$Hd1Lj> Z.3Qk wTEp\/DQQT 3Fb'={졫-?*ĵCnY]NTA/c% 'ukO)+ `Guq]ש[56t<e^DAF`lU'|<&&]>!?a\&=-_GF<@aR~R-"f9%3)]= O;:o>Q0@ml ƕZnfK zl rPg Ȱԗ__̴ , d6lqᖆ|P>.~C3GH=#N2K*Qa;?;?krn%q#s -^~Ի 1l4Z8 -{iVMY}}rPl;ҩӀ?5+};U-c0!T:Dry 1eDh oR1]RuY~-?.53xٯeY 0WZ@_A؋$d ie&,FY0rkX%r' -W)ipj0qr*tUUEXbĿ0Ycq^%16;=Km2+p~Bx>*2T˻;tbx<~ cREVYSOE^(S,;zv -j ӿo"m'bHZ;^KFV aM72'$KwxljH,btF!"8J`|xb)KyC;_oYKI/ V&ScBO=f>tnV~":GV(8ߚTJw\j%*dp#DScb^ _J~Z¦q7ś6z cƽdEYtuOQT> |y-UqS ñ&F>R lW[~+5ۉdl -wYls]Kfe -&.[[c"#?y;}\e].^L*DKD/* (0*J:*yٗˋH2XUd.ulltLTT-4|fRJ1yC?qJ,9 _oT!F&9 ^UZ.ŸR)Cx^mc)xT=i9#eo*&7еe*ќH, -8`Y^+b? f gN(ژ_e࿏?3F#\!<4T_7dAw<\YE]:iU8Bx}`E*R]`*XJo!} d>!&Mځ1rDEԹgT7J"C]MdK8v,e~ZK*`l(:Q0K'3"z.T\k=JUXAxk :B:KW(̡bP4FL؃Rvc-R6Q-hD@}!.2ʨ j}EJ&ǽz$*C?AQW!U8[48V)J(݉Bq+QL3}e,Z fM(a[C< ?)Ic͇HZ2O02jk*.*bۗSFx!!ۗe-l -@*`B"CaDeZSUÓ} WGZR{{7ǁP[,u U" hOX,Gn{x.`{[ O5fULu즚:xR݈zţ5߮yFgJC{M6MEk%;P# i ӧR;D1ݖܻl!(jU[E(O)'6@ pw!nC<"\1`W>}HWG9.a7F(žܗQASB=^ 6U!POVAwnR؝\دr{(%kUo/Y`\_y~[M~ؼQMw"}2iE!q~ucӁ [6aw; -_Q t#}ٿsSe5N,p!?b+\oP*ظA&*W l-(osN) mED,ũpm,04W6 -R&jQ#UhIziDӔ.vO%l7.2,?l*`Jna*)6QCZ,TYtKV1Y 09@E1@kTA 3lp.U,l# -i \J.uR#P#`_ʧ#-Xmݟ` Dpr4~MOP ->Y -imSCږ'&73_k5Ŝ}aPX&چ;rO"mP,+~cT|v(V Ц+L8W>coSZCCc$~3b;[/5_*Qn؞#RNJG-0Áesqd(l!zV_ v>v ISUA _4oz*Om%.A;t\wپz? -ud”OQWfH}wbW¶Q+f_ -f'dӏnLEWY❍r6[}FC`| [J˖of U4 -,k%*ŗGţ-,oTQV]EJ - ]\ԹYw-mAԿ HUF;/#>^J_,5sU?4L'ظ;!O?@>+bV>ShTs9<5B#;`X/Х.@*y/ƃ.MʦиD}jg/fSp"L_R PCև.TU/*k.!Dv~Zٖ5PlT/.E>M6 XLj]9)f`*'`,˥P`.غE1싐kkjG?*y-|CHiLdKqjbFX|{~⦈KJ˨jmXźAQXy%Ub|>.Zڞe%%Fe! 5yʁQHZ F؇%D*"QӒq.(F$U7RsIC W>gXWQ Wb3Y3*yYc9.3.& Jy -؃_M -2mp`;mM+-< uXke,pn=}VfV/W7QzjbՕs "-џG {9w;5bZ\.⢼vye >|E)|;<Z \%B Ǚ_Qf*6P EPF%%\,ZD%5VřZBmmHta&kT#ҤK8e lX D> -P \)~t]w<EJke2)(3d:27cQK|Ua-\Igf*Ȓ|0T.UPܽnRJKXgb ĵQ\]/H|#g!*- - Ex?]`PjP>^Y.\,+8d-NW𸱝D+ -Р,Z OjjĈ>8AŅiPl"vCi#ױ3 -ukjGVa@("|]QI6r#hn/KJ OtŔMuVk%0H3߳RhRhHcIKo"-Գ!PjG'Fy)> ~ل(Y -|2N*u}z#m .D2:HnŖ4Efe|(fO&!_;4@}(%xXZ~MSfUmL*|W!35EU!H$ӮV*V`BcO,7(\0ѺU_?97JhV]^xo_D},tT? \Á3[ ijP\wd-֐o,r,~ujطd6#1jPT/ÐY;_+dk&÷2U>A~eKc'čdVƊjS~&dn;qk4Pw@WaY2/~Ʈ嘽bakw,BK(@v//c.S[U|T\[)LƁA,,JW\}:Q|lܩ={W(Tnm b3W9kFPxӬ!P}{H3Ju\\:Q,ʰ„b8>WwRag^ʂ nBܱ_mYȍBܿ[ѓJȰ jaߔ0(WV `p0bKcUm + um{a -^Rnv0qJ֖P=ߏPh n<'1E ©nYr<ߋMM~.&;,/䤬%MʁJ:FM;9-Trp.ԗp\ce\q"g aJ.T%uزp6+n8w?U=ry+ǬhPX^%瘆̏gڥڿTzgSV1} ӍШ^$[[NKSPnbly:VnEsJ;PC6Qʆh7ۄ,,S֌žvf!1x@HmU0\ -}Z+> %ؚjk D%%Z3ISN)ktFC2XJ̅x*V a}u|E^ Oc|鋷<ÓZɄ}E.hd U&6k&L;cy -gh<=QԻ mۅ*{Xk =`tg04@ʉϊV~Tqp]أE39!\|_"vX7O@f5+!Qt{7-RҲ%i -Z_?4(E -AbD} 5q.0 U.Q FCRHIJPFW YVVe(Ey!dSȿQ@5zWRqI_]5Ie_\@@@.CަR +n?Ȫ-8J\DlFV^x4!jejG$jmJ< /$ ?LIB6U nx|OP-ws2ד+)R5~* u{b..] ׈92*aMAscȚ(7i(/lI: [5(D0-F)~+e6\^=c]E(TO[)r -Q%Quv;*|rܺ*]E> -azg -fJGfJjrpkA>a}+VGrӷH(ДiIvO#-\iqS -iD*&2! 9u Yo2j ^M'.ŭ|Qtba]!8 Fy{LD6Vp VLYe" CY:„]^\YIUu7nYmG4ر+6E[%|Q%Ts Q%KRTP_Ja{Y,ScKe=̀g򆲴%"u`nv2+xrk>-Ql2ke d.+ FA-|r{z?3OȿrEl}r٤u0.83]B)TBdwhJn/<<Ũ-6L^PX[-ĥ}YNmdLq% -!( - 'Ezg cXlJg `VpEǕ6`ߑ=i("t X4F/2YdwȺO7h;{֟*'C.5Mư>y^^bXt vð;j -¾(~ ]P0~ᱲi"r&myL7 #@? Rܨ]miXqgU" L8!$ vcc>%sH:D!J\S+8\2Z_OKT=9X ,*_6f3\z#<Ѥ%ܞL-[e> +CUUi -?AppZpߖsPTOP0c,;'`<"/Vqxj@ܙzTۯ?[4 Ig[5 L"1Mo.~寚sQ!r'*W'L -X,.xzy.yH>۞)vW-]&3;sSՏlcEm̹qCjT HE:>%1U$0Gs-ғK%T(J#G>*-)/XwvzeVکkר8u -@Ld vxsl&FJ=)Z7Yls/鳬E(pKP -3JQȇYdrC/e5*3~0sRdS2X58j獬@o: 됔 o"MR56IkX˿eqg6!/{?eolv%i_ -_YRxAxn_7}-P(.}@VԬI1++ĜW`CzH`T`q@uuJ [. O ԧc a< z>rX[qe2 Q+eO1$*\ԌS,`Ge#ۇAP +{++.)Gܰ +%rL`PrX`KWiK,Bˈq0R*y}ER+\7EAh+gթ'op .ڙ"p8U|o*U)"?&&vpoA,,kF_&+z>^ ,uKAV -`U0t~!60יL)YU[K*Uʬ.}NH82'nRXm*Jd%.ξ_&R7Y̝%W -lsc1k {YȀ=E-[:kOKۖQ9eI:[\V2VN3^xKOLOJ-.!ƹZZW]銞*R ƯۘHj/jE)AQmUsG?}eBbG2ޗ`S>Vn%Vw[pO&靋֏-F - *W")]"5&"T'ʩnj3Q@]4V JCڀ4k++B *7ɫqݍSPD|KڨQqlE-0.O5HxW[w!r4D⋨#(l"\>r/^D -|8D c ~o. rS<͛m˲p.Zܱ?_#HGYw,vNس5׾(}>(0⽋!KG~^2•yl[Zb.JxZJ:(fP=uMHAd|XPJ%VW/U`ֹrse^wSuacs{h7i>uE$to~0>D.)PKDT5mQSN|b$UFʃd1s.=bRJ.q4Bi`\-$~(I$..!Qc9u5O͖;..Y~.Q~&A(Oe!/5iTş##>S -heĂ嚸ģe'A.R6W)PAX\u쾏ʣb\v+V 1GVcq2SEowP-_&"(H\ߊCLMa[7`Wwt3fR0\7aȉyq{TaieV}D-(?@0 m@ILJ_E  C -TC}G]'࠺JLn 1L -/}#Ux(Ab$<6*D pXC8\bnjja)gLǀ4kؾ5s,LaPo"-rN7 Wy܊E+-Y0~਌Bې-rWcx)+JŀՍ %lie\1^34'w\+ !骾w# .%a,c~,]h gNqII`[8Cq|]ʥ}T@wĈYXQ.ݔ ;<*踱Ge5U -+y"/ d#.V)rSP,ȷ ^U9ʘ* f@݃1vP瘄..Bhw%- vڝmS5nQv)  90~v9Up.4ظCl[lK"ٷ.e`4ªmCN(͔n,ObogE -{:i 5c e v_8%555 r!0 J'77!b@ [W)4߄)\6 -f6qZ*l .-Q :A6~W`5DjƐap1}pUGTT&V|S0>FpnY&L> j~eN]n&ᓄ9&KA;*|Ul9ՙ_h&Yd -V.狽_˗I[\3(Q@|؃l˗l#e*r 6K 0 /|So6_r 0i͕tURcɥ$n%ܢrT+-% -\@r,.a8>pi/Wl - 2⍸}x6&U)nƃg6TUM=R~P_VC6´ݪ5tU, N -T5K;U]l&dPJ1 -:QMw1+)aB]/~WM)iRܸ6ke`LƸա5.Hmjgfõ*'Mj*>%N.\nk^#VV^~)yIK_$.RV"̉ _r,AUVåv9Rbh-7XEN9vQQzGT5#ATò`)gV=HyX"%fFXB%Ao%ybD)';%&5r }<{lۺCNeArkG#SLwM_jƆ&<>lʮ8 -'|~Ѕlۨ)hvS`,IpLj[Ud isNʸ+tG.rMO2S|WsIS ?nYP*p9M*d -5BT ԟ/d#t@ +5<d5pKzDQ(ȯXTy&pU3*vf-̈Y춳pKc+H yrgM:ͻ=KLhzDŽXK%.˨(Xs6b{n@˷r!iw#c?yq?Ww8Ș+A,PCFb'oQ jU.PJ^K8]lba鉄lȕ4B MHJkbW G apLe\x! A k?0Ѐ=d]2Y{.jRiqvS @>R[q%=A -coJ``YKMġj2Yr'~/X?Q+^K\_yEMK ]w:U=A -VZg9 v╡Mk(O኎yW6鿽"Bߐ -b~;h-`Pl(*(2 [Lȳ+gb\YJ5,_YbLcvVeM+9KWa[ЛS{vpb54]B -c3>`rx}]¤/y,2ݐ%@f _c8KOB\}MF ȵ*Wy (Cܻ0cϿTX3sc~{ x`'h>e2= -: "Ax+?$qIeirE_{2~PWR+#=XGIwP@hd*\/ȦM_Y5aq -߄*ەEԺ߂t{r*n]5Y(yVu)'eդ|NGx ĩqh^+L*Y+o~Q}g)|i,t7\bT؎pJr#+nipL[VUq\ -09tk;r9Ʉv|^9s=H'㉗rƾJbUGsl(J6P(`:YY(b<Ʈt'XzBY,ٿ'v/lo.6iK(}˺ʂ<"tڸZ⽍_"nR/}|쨔v}1ȏ] ;WyH r)~ajס4O -^M,h YazBׇ9L-LtغV1+*yfW|+26tR7G;MuITxUll1vG㊨典 -rXB {%Q) O0% l(~猏3bh#:Ed/bbQC@pYfeŋ5 ^,saq_fGy +~ʝ䴸Ùk[si-*N`•.kyf-b.9Q`儯 >$Di%AL΁YP*ZEaQv Dȿ ,7bk.~R?DBe p inxjaiH ݓhEET5b2Uʱ& -U0 M!uu Ff (n(RlIo ș[ _82^Y^K3b]6ª,W"Jϊ.R&@Kkl='`>*ni۬0GUT'"[D -ms²YX++2n^¡ ζW`1k@~wVH75G_UT6XYzHXBIݚX!#k䌹\s_łw[ !U_V E/Sðis +eR 5 ט|P$v 6xb\o%4ljUA^*62Gd{d%%apTyoicoaLm,rO\+ẫJd -( iRV%Eu<|jTƔv[̸4M3C 1;K6@XX J̢~б Xˋjuޑ8Y8:ߎpY+bᄊEc1[dSr`|\.y/{:uBW&䆦79v_XjC҉A6653 -eTO'UEVLԯo T;TGH,Ie}Kw xʦJʯĮdf~*\|Fb\^YB-%*"[+Q4mȗSAL#mF8ů#E6&bm{rE]s_w4a\\KlmGGm$9w<.\ -OnKB 4v:q$}b$b!yGu|R23_O~*8_0t2n|?<[ՒUC -)D ^O*͡OK0B>(YI'&0ۏ`.y<((qK`djq~(}L)>E܆O D -j :lKuX(e,sO'+ԹyP;,ʂ\+dcjTW6'Vy2+b0>,Pz6(%Zad*)rXw7eK~nq{!>9y+)87Q.{bD*ʛ 'A C d/06%@p\=oƦ#+AmQu矆\6|4>w35)u{?TK0sny*fBǂ(ܸH5|U l?T th4*N )L!lq\a<.$ɽֿ ڐjȦ +\DgWu0J\unnW򖋫S Q/z8 -L51 m,8" -aqI&EĊyQ\Բgʋ&\nQKOcZE@ƾЈ`_U PƸBlቷAݶ}!+J~$/6V@8EXBl:',j(ra+d,(T$>_ 1zc]smiS>h|iB3JLPha?C-Яq܁S?(JOS`>%O#aa5-c{k Wo -F2 * lj._ .| [)̎Ce+*9  j ,>*N\iˈĻ_ gd#ʗ|`>|W>R|/7Gn) -k&>Ю|eD<_"zoyJƘZTbj-s{ (4{qu(]^h -uO^]Bh)'B -_TKj bm!g~c%(r&dSkЈ jWe0i:.v3{. )iwap2^US9C m[<|l_~v;2~Pf!طar2&DX KE`Gug%Ua >/%"2tQ b 7ɖ&P AStKCA5[Ȓ2g/ nVD+k)l Km'c~dR UeakK55_.(%2_(C+f]^fiZ j)1}Z9dŕ}\v۴C - -myU=] q2 L|˥؟}|] Kc{1Y&8y'D"dʆ1hܶ:h+ܭq! X7ΟP](uׇ`V`Z3rM`z߶wk G#( -  -;"+A([!v<߮6b2öeZzCHW `FۍBxՍ.?Yk,oZ&l7=4>_Ŕ{OHYԋuR0jGn%yR.0yE)n],ːJBE‰qc춶\|)>Lc. nqՓ"W-ov7q<5ZWˑx!ިCйIo cuJ )&xmX?ĔklQeguRKq  w1]N1U9o#W*iUc/sLL<%7iAMkdtו&UbHNo~`RDrVEG(<\X9U}@A֠?(*\@AuCP?d>s$} -ʕXC2Jb/`\cDŽ)+DB;ҿR]/!4<@LG;p9'T(0}5\j.ƕ)t%>oLjoKUWLL AMw>6͹O){Ř*żOyL%[ -2#(iBbxR/}5P+4QxA/4y1p.*|SrKŪa.Y&S:PYr /mK3X é.>%x~22V\m QeX/N,a`oF}P0cYPJ}o9>F~Թ3[pGTINhWc-BxXP~C@o:7{AG`0T9嘱i3<+ 27$;?F=r m)`7<]K.~%-@-~ # .awdiXJP2gRV,1v)a1칳5,e{ &^l\uhVHl'/2 kg7G^~IdVHҟKʕ`*7qkz,ԉQ "?%HȢmy +?H>YWQnx?>嗠%ܱiHk[2Е}H{>>w0(a R /Q?pE64;&bp\DjIN#}Lyt:̆.vT[z&#Y*;S~\O#@ao5i_PƌK9[v1W -\,(~$n CM#(vG>UibxcaZwnqQ;+Yj£̎ -Cl} -bٵ@nų8C97K@ ̶\ج,hV(j)]\ۙV -3#jAỐ\O ªM"I"_Yh¸M@*wH\FqiWU!( yU-zpVF8^@s^uUPU%~ Km/gB.ޱ[K+QV%CY' d00,XUu \9!&yHk-%9[单c5(P^؞t -}FW ~Ztˡ?{X h/p6ҫV2ZҎzym'$Lja 1NKVDdœ#fVYdB4z>BTZ+t"q Yk9$\}Uel*կ%R$+zW -XkܱE֣ܨyo -*^UM)JCMH…TxCC}7-A -TsHpIQWC+  P2T*70&e#vlZSAJ~@@QA(PۂfDZ: be䊺FWSEANB*AL]Z C.F֔(xd$(J)DcU[TAV%{\FnÙU)>.sSR}ԟ:^~``;t4T .^T -, ;S,.EqX -%s` -e@< ]ƣj_Qeшk!F85e kqEOYue,fDK΋٢^!7c {?;X-0P1pUs. c-QgY.7 -nmZЖfJ4RE|suP6cDaW'QEnIؐT@Up%.%ٜ0.r7ၭYUنRۓfǞDᶟjF ̄Ju$va-|u@ Z ZT"-%Y{刏&-1p{'v26j$kvBRP<xPjVm _/ץI(kϧ얥3CժYTlLp~b>B>WuQ|^JW)ժ -כdS.P0xBmM41~62m7`^gu] `mM{eJMb5yPhE9QyDZx&qR9xfai@v>~9R=bkھdZ3VRڂ,٦RDG9eT]u׷)Ϊ- \ @ iG?2E<#c֋gfήk -{2B9ʯƗHc(nWZa v]ڙgHV/?{pf GXt] -IJk_hfKN*=Իb?@aZ jцNqs>tDeڥe'* W/Ӄ)J? ]|CH[?yfh-ګ?n'TMVRMӟ'D ^עJ =_ e"\$Vbs$ċz@12y@ e0&¹N"1\J\9(,jUK.x'5i]캨v?Y߅7wyn@j(B¦W zژ0>U*!ȿ^](g?^_1SᴵT&bu_"8J?ZKCs}nw3 w;-B\JBT,X(HOF/b|R?qjf >m.)!6~~͵w*=O֭ĥ' -gjlɗ[`& q}|XE?5~IfMCU.0 &K1q?U=!DChj3Nz!L_ķW4*x{,cȱ ,0"ٶY 'D-Yq9<ܽ_Wٱyea+Xya ;OCE>@F"uKBCI03K\U} T_!Z^NGo?XCJٯ}gGI9+u_1FpFvoQNVO+{:ʉ7Q)=Jk<ܫ~P=T .pPB͞򱚶bS砳FM^ct'Eni.C̽=NKTa'`6T@/KGؠMcŸ%5RqFh#ÚQ62ҖгSl`SOǔϼ[~<`UZ?ZmtDEpĽ-aP#-9UH.KWBRT*i)#x_ugG0JёGh[ 6#qr.LIaUxFϪ3?g/ Eʖ/)b*WRcWDr}~c` jNԬAe5yon?ˉ9l/%mVyG픶Zlb}6eu{Sv{!`k-N.eف%iL -ejV]1~]ed3գ*Pښ/`bP8?ee%笠BW*6^Ŷ| ՌUً(V-.TD1p_RځyQW#2|+tjYfEBoC+jy62ʕCIgy9Ҙ fU~%e#~ɿ#ASΙW*⒪tIߺ!L"-)jDXJ. YR%FoS[1].T~7^Lаlv"TM}Cna{`״ :JܹE6jTk7e3S)"i3(/uեW(li V" q!]EDYf5/x3tz0BM/VXK2(a% J j4’VVv (Ʒ]V&HǒGTy3Gژ|_@ -a/^"mGWYB|ܖ\@طq^ưi{߇%Q b/͏I4ǐ+ 6WPƩ -Vv".BDFV |\"'"[ʸQȧv%A .Q/,ո/%&Mw~5nmN]RT?"Sa)*HTbJe  0 Mk}QMRʝyRPTo b -}D]a"cDA| X.%*]kbBpXy-FȠx%J|0ؼ(ܦ -ԳYL!HvQXls=Ţ rJ_joL<,瓑l#n 9 gV=.ʺYBX+%ș< #7b=Ĕ/R!lTe`B: `l\Cb- &1r44j+ܲvUmT7/X}Ŵ!rZAY߄='g%#[1qpҹTMmf^%Q2XPdlMkfD.)u /cY>eU?u`EJ7GglX5 tQ e\)1i(N{ d0.젩i1ͩ~!||]CK(Jv? *l]A -*݊[*iM0JJ!^ .S`;+² VF%,Z6 ە 6+9o4l{d@K6%VCQ IehZA (oOoD@s&'IW -b*<}‡6 KPke#lFvY, dہM =}5!7v).Zͫ).*㒭s̋.7-@ m*Qrꊋk?1J]ԣZ)F#rAh5rLegǓ|nS#KGkn -'cLRQRMb.b@u>ˍ2'GbDnh)cvõIU=x6ys(XY -mpli܋9RonPP+[᥷*ط˳ ]r7epw- ثR%aQJNAwrkUeK^J - (v_ʍF,Cd%"%[^!bϹpZK,IMv6=L1w%59)e F?Sĥ_I% ՞e<*c*j,.CN=%˖]Ki"-,av'a-9cNE󿩐pqLπN]V0Y.` mpA8qy)0?i8\3F L̩ܕ iy~bŋBuf SSY< -A.\I^.AH Ư"Lc.<7.-C㊽QW\P~ b]/nRTدRY-1E]rmƽFϱ sЀ*_ _ -6VZCe|~b.Ї"WYP2_#.??Ə btY_ӲĦQ[w7|@:?Čk<3e*&Tl=ϸJ"n';psnG7ݸ-Y;--SPįy[/6YUY3Q"*qi-؇D s"H!B)HV) ,UU-tj=@BxEct[y"@ln˙v<>'Mڈ!b7$U|EP -!VË1L8\i"FQ0^ -2-ܵ%bcZT7S܉7 v%Jd+MIrnm4>JfXHY.yakqlY*_emɨEШ% <7d]ܦm[T!+v=v KR1b.90>6{RRV LK:0%F1u2 D\ xa!wy)T;qɥI1*%l}-pv]`SE;`P*zYQ#EhS(latѕvzFW[CM TQ8uYG<XpZj+kbu~4OпO=pjyOawax-M$kߺ])#WHdQL7a?1%_7WG -2ڊaY U%=`$UaxY"e?bi@;% @AP`0p!1Q?_;~~P k\U9jh9^#<f>ִ&Z~ړ L!Ds|@V&,ݛ PBszvOz}Iޞm< 4f oCp{#*11up(Pf +5%lvB -(PaB -(P^*J^Çx8u8su=;:N/ ~.4 ߝf+&cb%bWy>sA+ΌL@&|&fCn=,_<\9% -JƮl ᐬi݈*ݞ5MzpwnnNS]9ϩk\@> 3:xWǞ/F -ϒ7noHu -+ĭ -֌T.N>tJb.4G~/TUR䇁d&d$'(y6$YffF FLb0cLLjE׍Fµ"A 0@P`p? _ u/Ʌhm/#"%_s-$I$I$I$,fgAG{/ܜ-OwV(H}R沅Z챕ᖻ,xl/eieV/7Fᲊzo|/7> νǏ" -endstream -endobj -6 0 obj -<>/F(pageEntities.json)/Type/Filespec/UF(pageEntities.json)>> -endobj -7 0 obj -<>/Subtype/application#2Fjson/Type/EmbeddedFile>>stream -{ "type": "Document", "isBackSide": false } -endstream -endobj -8 0 obj -<> -endobj -xref -0 9 -0000000001 65535 f -0000000021 00000 n -0000000070 00000 n -0000000124 00000 n -0000000357 00000 n -0000010064 00000 n -0000445565 00000 n -0000445696 00000 n -0000445894 00000 n -trailer -<> -startxref -446031 -%%EOF diff --git a/data-entry-app/docs/rechizite 12 decembrie pictus.pdf b/data-entry-app/docs/rechizite 12 decembrie pictus.pdf deleted file mode 100644 index f24ab0d..0000000 Binary files a/data-entry-app/docs/rechizite 12 decembrie pictus.pdf and /dev/null differ diff --git a/data-entry-app/frontend/index.html b/data-entry-app/frontend/index.html deleted file mode 100644 index d474926..0000000 --- a/data-entry-app/frontend/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Data Entry - Bonuri Fiscale - - -

- - - diff --git a/data-entry-app/frontend/package.json b/data-entry-app/frontend/package.json deleted file mode 100644 index cdc69f4..0000000 --- a/data-entry-app/frontend/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "data-entry-frontend", - "version": "1.0.0", - "description": "Data Entry App - Vue.js Frontend", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix" - }, - "dependencies": { - "vue": "^3.4.0", - "vue-router": "^4.2.5", - "pinia": "^2.1.7", - "axios": "^1.6.5", - "primevue": "^3.48.0", - "primeicons": "^6.0.1", - "@primevue/themes": "^4.0.0" - }, - "devDependencies": { - "@vitejs/plugin-vue": "^5.0.0", - "vite": "^5.0.10", - "eslint": "^8.56.0", - "eslint-plugin-vue": "^9.20.0" - } -} diff --git a/data-entry-app/frontend/src/App.vue b/data-entry-app/frontend/src/App.vue deleted file mode 100644 index 059fd86..0000000 --- a/data-entry-app/frontend/src/App.vue +++ /dev/null @@ -1,163 +0,0 @@ - - - - - diff --git a/data-entry-app/frontend/src/components/ocr/OCRConfidenceIndicator.vue b/data-entry-app/frontend/src/components/ocr/OCRConfidenceIndicator.vue deleted file mode 100644 index f4e8c5f..0000000 --- a/data-entry-app/frontend/src/components/ocr/OCRConfidenceIndicator.vue +++ /dev/null @@ -1,125 +0,0 @@ - - - - - diff --git a/data-entry-app/frontend/src/components/ocr/OCRPreview.vue b/data-entry-app/frontend/src/components/ocr/OCRPreview.vue deleted file mode 100644 index afa8830..0000000 --- a/data-entry-app/frontend/src/components/ocr/OCRPreview.vue +++ /dev/null @@ -1,699 +0,0 @@ - - - - - diff --git a/data-entry-app/frontend/src/components/ocr/OCRUploadZone.vue b/data-entry-app/frontend/src/components/ocr/OCRUploadZone.vue deleted file mode 100644 index e027932..0000000 --- a/data-entry-app/frontend/src/components/ocr/OCRUploadZone.vue +++ /dev/null @@ -1,281 +0,0 @@ - - - - - diff --git a/data-entry-app/frontend/src/main.js b/data-entry-app/frontend/src/main.js deleted file mode 100644 index 925124f..0000000 --- a/data-entry-app/frontend/src/main.js +++ /dev/null @@ -1,89 +0,0 @@ -import { createApp } from 'vue' -import { createPinia } from 'pinia' -import PrimeVue from 'primevue/config' -import ToastService from 'primevue/toastservice' -import ConfirmationService from 'primevue/confirmationservice' - -import App from './App.vue' -import router from './router' - -// PrimeVue components -import Button from 'primevue/button' -import InputText from 'primevue/inputtext' -import InputNumber from 'primevue/inputnumber' -import Password from 'primevue/password' -import Dropdown from 'primevue/dropdown' -import Calendar from 'primevue/calendar' -import Textarea from 'primevue/textarea' -import DataTable from 'primevue/datatable' -import Column from 'primevue/column' -import Dialog from 'primevue/dialog' -import Toast from 'primevue/toast' -import ConfirmDialog from 'primevue/confirmdialog' -import FileUpload from 'primevue/fileupload' -import Image from 'primevue/image' -import Tag from 'primevue/tag' -import Card from 'primevue/card' -import TabView from 'primevue/tabview' -import TabPanel from 'primevue/tabpanel' -import Checkbox from 'primevue/checkbox' -import RadioButton from 'primevue/radiobutton' -import ProgressSpinner from 'primevue/progressspinner' -import Badge from 'primevue/badge' -import Toolbar from 'primevue/toolbar' -import Divider from 'primevue/divider' -import Tooltip from 'primevue/tooltip' -import Message from 'primevue/message' - -// PrimeVue styles -import 'primevue/resources/themes/lara-light-blue/theme.css' -import 'primevue/resources/primevue.min.css' -import 'primeicons/primeicons.css' - -// Custom styles -import './assets/css/main.css' - -const app = createApp(App) - -// Pinia store -app.use(createPinia()) - -// Router -app.use(router) - -// PrimeVue -app.use(PrimeVue, { ripple: true }) -app.use(ToastService) -app.use(ConfirmationService) - -// Register PrimeVue components globally -app.component('Button', Button) -app.component('InputText', InputText) -app.component('InputNumber', InputNumber) -app.component('Password', Password) -app.component('Dropdown', Dropdown) -app.component('Calendar', Calendar) -app.component('Textarea', Textarea) -app.component('DataTable', DataTable) -app.component('Column', Column) -app.component('Dialog', Dialog) -app.component('Toast', Toast) -app.component('ConfirmDialog', ConfirmDialog) -app.component('FileUpload', FileUpload) -app.component('Image', Image) -app.component('Tag', Tag) -app.component('Card', Card) -app.component('TabView', TabView) -app.component('TabPanel', TabPanel) -app.component('Checkbox', Checkbox) -app.component('RadioButton', RadioButton) -app.component('ProgressSpinner', ProgressSpinner) -app.component('Badge', Badge) -app.component('Toolbar', Toolbar) -app.component('Divider', Divider) -app.component('Message', Message) - -// Register PrimeVue directives -app.directive('tooltip', Tooltip) - -app.mount('#app') diff --git a/data-entry-app/frontend/src/router/index.js b/data-entry-app/frontend/src/router/index.js deleted file mode 100644 index 78b3323..0000000 --- a/data-entry-app/frontend/src/router/index.js +++ /dev/null @@ -1,64 +0,0 @@ -import { createRouter, createWebHistory } from 'vue-router' -import { useAuthStore } from '@/stores/auth' - -const routes = [ - { - path: '/login', - name: 'Login', - component: () => import('../views/LoginView.vue'), - meta: { title: 'Conectare', requiresAuth: false } - }, - { - path: '/', - name: 'ReceiptsList', - component: () => import('../views/receipts/ReceiptsListView.vue'), - meta: { title: 'Lista Bonuri', requiresAuth: true } - }, - { - path: '/create', - name: 'ReceiptCreate', - component: () => import('../views/receipts/ReceiptCreateView.vue'), - meta: { title: 'Bon Nou', requiresAuth: true } - }, - { - path: '/receipt/:id', - name: 'ReceiptDetail', - component: () => import('../views/receipts/ReceiptCreateView.vue'), - meta: { title: 'Detalii Bon', requiresAuth: true } - }, - { - path: '/receipt/:id/edit', - name: 'ReceiptEdit', - component: () => import('../views/receipts/ReceiptCreateView.vue'), - meta: { title: 'Editare Bon', requiresAuth: true } - }, -] - -const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), - routes -}) - -// Authentication guard and page title -router.beforeEach((to, from, next) => { - // Update page title - document.title = to.meta.title - ? `${to.meta.title} | Data Entry` - : 'Data Entry - Bonuri Fiscale' - - // Check authentication - const authStore = useAuthStore() - const requiresAuth = to.meta.requiresAuth !== false - - if (requiresAuth && !authStore.isAuthenticated) { - // Redirect to login if not authenticated - next({ name: 'Login', query: { redirect: to.fullPath } }) - } else if (to.name === 'Login' && authStore.isAuthenticated) { - // Redirect to home if already authenticated - next({ name: 'ReceiptsList' }) - } else { - next() - } -}) - -export default router diff --git a/data-entry-app/frontend/src/services/api.js b/data-entry-app/frontend/src/services/api.js deleted file mode 100644 index 4c3871e..0000000 --- a/data-entry-app/frontend/src/services/api.js +++ /dev/null @@ -1,100 +0,0 @@ -import axios from "axios"; - -// Create axios instance with base configuration -const apiService = axios.create({ - baseURL: import.meta.env.BASE_URL + "api", - timeout: 10000, - headers: { - "Content-Type": "application/json", - }, -}); - -// Request interceptor to add auth token and selected company -apiService.interceptors.request.use( - (config) => { - const token = localStorage.getItem("access_token"); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - - // Add X-Selected-Company header from localStorage - // The company store saves the selected company per user - const user = JSON.parse(localStorage.getItem("user") || "null"); - if (user?.username) { - const savedCompany = localStorage.getItem(`selected_company_${user.username}`); - if (savedCompany) { - try { - const company = JSON.parse(savedCompany); - if (company?.id_firma) { - config.headers["X-Selected-Company"] = company.id_firma; - } - } catch (e) { - // Invalid JSON, ignore - } - } - } - - return config; - }, - (error) => { - return Promise.reject(error); - }, -); - -// Response interceptor for handling errors and token refresh -apiService.interceptors.response.use( - (response) => { - return response; - }, - async (error) => { - const originalRequest = error.config; - - // Handle 401 Unauthorized errors - if (error.response?.status === 401 && !originalRequest._retry) { - originalRequest._retry = true; - - try { - const refreshToken = localStorage.getItem("refresh_token"); - if (refreshToken) { - const response = await axios.post( - import.meta.env.BASE_URL + "api/auth/refresh", - { refresh_token: refreshToken }, - ); - - const { access_token } = response.data; - localStorage.setItem("access_token", access_token); - - apiService.defaults.headers.common["Authorization"] = `Bearer ${access_token}`; - originalRequest.headers["Authorization"] = `Bearer ${access_token}`; - - return apiService(originalRequest); - } - } catch (refreshError) { - localStorage.removeItem("access_token"); - localStorage.removeItem("refresh_token"); - localStorage.removeItem("user"); - - const loginPath = import.meta.env.BASE_URL + "login"; - if (window.location.pathname !== loginPath) { - window.location.href = loginPath; - } - - return Promise.reject(refreshError); - } - } - - if (error.response) { - const message = error.response.data?.detail || error.response.data?.message || `Server error: ${error.response.status}`; - console.error("API Error:", { status: error.response.status, message, url: error.config.url }); - } else if (error.request) { - console.error("Network Error:", error.message); - } else { - console.error("Request Error:", error.message); - } - - return Promise.reject(error); - }, -); - -export { apiService }; -export default apiService; diff --git a/data-entry-app/frontend/src/stores/accountingPeriod.js b/data-entry-app/frontend/src/stores/accountingPeriod.js deleted file mode 100644 index 6455013..0000000 --- a/data-entry-app/frontend/src/stores/accountingPeriod.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Accounting Period Store for Data Entry App - * - * Uses the shared accounting period store factory from shared/frontend/stores/accountingPeriod.js - * Configured with the data-entry API service (port 8003) - */ - -import { createAccountingPeriodStore } from "../../../../shared/frontend/stores/accountingPeriod"; -import { apiService } from "../services/api"; -import { useAuthStore } from "./auth"; -import { useCompanyStore } from "./companies"; - -export const useAccountingPeriodStore = createAccountingPeriodStore( - apiService, - useAuthStore, - useCompanyStore -); diff --git a/data-entry-app/frontend/src/stores/auth.js b/data-entry-app/frontend/src/stores/auth.js deleted file mode 100644 index 307dfb9..0000000 --- a/data-entry-app/frontend/src/stores/auth.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Auth Store for Data Entry App - * - * Uses the shared auth store factory from shared/frontend/stores/auth.js - * Configured with the data-entry API service (port 8003) - */ - -import { createAuthStore } from "../../../../shared/frontend/stores/auth"; -import { apiService } from "../services/api"; - -export const useAuthStore = createAuthStore(apiService); diff --git a/data-entry-app/frontend/src/stores/companies.js b/data-entry-app/frontend/src/stores/companies.js deleted file mode 100644 index e71a60f..0000000 --- a/data-entry-app/frontend/src/stores/companies.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Companies Store for Data Entry App - * - * Uses the shared companies store factory from shared/frontend/stores/companies.js - * Configured with the data-entry API service (port 8003) - */ - -import { createCompaniesStore } from "../../../../shared/frontend/stores/companies"; -import { apiService } from "../services/api"; -import { useAuthStore } from "./auth"; - -export const useCompanyStore = createCompaniesStore(apiService, useAuthStore); diff --git a/data-entry-app/frontend/src/stores/receiptsStore.js b/data-entry-app/frontend/src/stores/receiptsStore.js deleted file mode 100644 index 979171b..0000000 --- a/data-entry-app/frontend/src/stores/receiptsStore.js +++ /dev/null @@ -1,445 +0,0 @@ -import { defineStore } from 'pinia' -import { apiService } from '../services/api' - -// Create receipts-specific API wrapper -const api = { - get: (url, config) => apiService.get(`/receipts${url}`, config), - post: (url, data, config) => apiService.post(`/receipts${url}`, data, config), - put: (url, data, config) => apiService.put(`/receipts${url}`, data, config), - delete: (url, config) => apiService.delete(`/receipts${url}`, config), -} - -export const useReceiptsStore = defineStore('receipts', { - state: () => ({ - receipts: [], - currentReceipt: null, - pendingReceipts: [], - stats: null, - loading: false, - error: null, - pagination: { - page: 1, - pageSize: 20, - total: 0, - pages: 1, - }, - filters: { - status: null, - search: '', - direction: null, - dateFrom: null, - dateTo: null, - }, - // Nomenclatures - partners: [], - accounts: [], - cashRegisters: [], - expenseTypes: [], - }), - - getters: { - hasReceipts: (state) => state.receipts.length > 0, - hasPendingReceipts: (state) => state.pendingReceipts.length > 0, - pendingCount: (state) => state.pendingReceipts.length, - }, - - actions: { - // ============ Receipts CRUD ============ - - async fetchReceipts() { - this.loading = true - this.error = null - try { - const params = { - page: this.pagination.page, - page_size: this.pagination.pageSize, - } - - if (this.filters.status) { - params.status = this.filters.status - } - if (this.filters.search) { - params.search = this.filters.search - } - if (this.filters.direction) { - params.direction = this.filters.direction - } - if (this.filters.dateFrom) { - params.date_from = this.filters.dateFrom - } - if (this.filters.dateTo) { - params.date_to = this.filters.dateTo - } - - const response = await api.get('/', { params }) - this.receipts = response.data.items - this.pagination.total = response.data.total - this.pagination.pages = response.data.pages - } catch (error) { - this.error = error.response?.data?.detail || 'Failed to fetch receipts' - throw error - } finally { - this.loading = false - } - }, - - async fetchReceiptById(id) { - this.loading = true - this.error = null - try { - const response = await api.get(`/${id}`) - this.currentReceipt = response.data - return response.data - } catch (error) { - this.error = error.response?.data?.detail || 'Failed to fetch receipt' - throw error - } finally { - this.loading = false - } - }, - - async createReceipt(data) { - this.loading = true - this.error = null - try { - const response = await api.post('/', data) - return response.data - } catch (error) { - this.error = error.response?.data?.detail || 'Failed to create receipt' - throw error - } finally { - this.loading = false - } - }, - - async updateReceipt(id, data) { - this.loading = true - this.error = null - try { - const response = await api.put(`/${id}`, data) - return response.data - } catch (error) { - this.error = error.response?.data?.detail || 'Failed to update receipt' - throw error - } finally { - this.loading = false - } - }, - - async deleteReceipt(id) { - this.loading = true - this.error = null - try { - await api.delete(`/${id}`) - } catch (error) { - this.error = error.response?.data?.detail || 'Failed to delete receipt' - throw error - } finally { - this.loading = false - } - }, - - // ============ Workflow Actions ============ - - async submitReceipt(id) { - this.loading = true - this.error = null - try { - const response = await api.post(`/${id}/submit`) - return response.data - } catch (error) { - this.error = error.response?.data?.detail || 'Failed to submit receipt' - throw error - } finally { - this.loading = false - } - }, - - async approveReceipt(id) { - this.loading = true - this.error = null - try { - const response = await api.post(`/${id}/approve`) - return response.data - } catch (error) { - this.error = error.response?.data?.detail || 'Failed to approve receipt' - throw error - } finally { - this.loading = false - } - }, - - async rejectReceipt(id, reason) { - this.loading = true - this.error = null - try { - const response = await api.post(`/${id}/reject`, { reason }) - return response.data - } catch (error) { - this.error = error.response?.data?.detail || 'Failed to reject receipt' - throw error - } finally { - this.loading = false - } - }, - - async resubmitReceipt(id) { - this.loading = true - this.error = null - try { - const response = await api.post(`/${id}/resubmit`) - return response.data - } catch (error) { - this.error = error.response?.data?.detail || 'Failed to resubmit receipt' - throw error - } finally { - this.loading = false - } - }, - - async unapproveReceipt(id) { - this.loading = true - this.error = null - try { - const response = await api.post(`/${id}/unapprove`) - return response.data - } catch (error) { - this.error = error.response?.data?.detail || 'Failed to unapprove receipt' - throw error - } finally { - this.loading = false - } - }, - - // ============ Pending Receipts ============ - - async fetchPendingReceipts() { - this.loading = true - this.error = null - try { - const response = await api.get('/pending') - this.pendingReceipts = response.data - return response.data - } catch (error) { - this.error = error.response?.data?.detail || 'Failed to fetch pending receipts' - throw error - } finally { - this.loading = false - } - }, - - // ============ Attachments ============ - - async uploadAttachment(receiptId, file) { - const formData = new FormData() - formData.append('file', file) - - try { - const response = await api.post(`/${receiptId}/attachments`, formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - }) - return response.data - } catch (error) { - throw new Error(error.response?.data?.detail || 'Failed to upload attachment') - } - }, - - async deleteAttachment(attachmentId) { - try { - await api.delete(`/attachments/${attachmentId}`) - } catch (error) { - throw new Error(error.response?.data?.detail || 'Failed to delete attachment') - } - }, - - getAttachmentUrl(attachmentId) { - return `/api/receipts/attachments/${attachmentId}/download` - }, - - async fetchAttachmentBlob(attachmentId) { - try { - const response = await apiService.get(`/receipts/attachments/${attachmentId}/download`, { - responseType: 'blob', - }) - return URL.createObjectURL(response.data) - } catch (error) { - console.error('Failed to fetch attachment:', error) - return null - } - }, - - async downloadAttachment(attachmentId, filename) { - try { - const response = await apiService.get(`/receipts/attachments/${attachmentId}/download`, { - responseType: 'blob', - }) - // Create download link - const url = URL.createObjectURL(response.data) - const link = document.createElement('a') - link.href = url - link.download = filename || 'attachment' - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - URL.revokeObjectURL(url) - return true - } catch (error) { - console.error('Failed to download attachment:', error) - throw new Error(error.response?.data?.detail || 'Failed to download attachment') - } - }, - - // ============ Accounting Entries ============ - - async fetchEntries(receiptId) { - try { - const response = await api.get(`/${receiptId}/entries`) - return response.data - } catch (error) { - throw new Error(error.response?.data?.detail || 'Failed to fetch entries') - } - }, - - async updateEntries(receiptId, entries) { - try { - const response = await api.put(`/${receiptId}/entries`, { entries }) - return response.data - } catch (error) { - throw new Error(error.response?.data?.detail || 'Failed to update entries') - } - }, - - async regenerateEntries(receiptId) { - try { - const response = await api.post(`/${receiptId}/entries/regenerate`) - return response.data - } catch (error) { - throw new Error(error.response?.data?.detail || 'Failed to regenerate entries') - } - }, - - // ============ Nomenclatures ============ - - async fetchPartners(search = '') { - try { - const response = await api.get('/nomenclature/partners', { - params: { search }, - }) - this.partners = response.data - return response.data - } catch (error) { - console.error('Failed to fetch partners:', error) - return [] - } - }, - - async fetchAccounts(prefix = '') { - try { - const response = await api.get('/nomenclature/accounts', { - params: { prefix }, - }) - this.accounts = response.data - return response.data - } catch (error) { - console.error('Failed to fetch accounts:', error) - return [] - } - }, - - async fetchCashRegisters() { - try { - const response = await api.get('/nomenclature/cash-registers') - this.cashRegisters = response.data - return response.data - } catch (error) { - console.error('Failed to fetch cash registers:', error) - return [] - } - }, - - async fetchExpenseTypes() { - try { - const response = await api.get('/nomenclature/expense-types') - this.expenseTypes = response.data - return response.data - } catch (error) { - console.error('Failed to fetch expense types:', error) - return [] - } - }, - - async fetchAllNomenclatures() { - await Promise.all([ - this.fetchPartners(), - this.fetchCashRegisters(), - this.fetchExpenseTypes(), - ]) - }, - - async searchSupplier(fiscalCode) { - try { - const response = await apiService.get('/nomenclature/suppliers/search', { - params: { fiscal_code: fiscalCode }, - }) - return response.data - } catch (error) { - console.error('Supplier search failed:', error) - return { found: false, source: 'error' } - } - }, - - async createLocalSupplier(data) { - try { - const response = await apiService.post('/nomenclature/suppliers/local', data) - // Add to local partners list - this.partners.push({ - id: response.data.id, - name: response.data.name, - code: response.data.fiscal_code, - }) - return response.data - } catch (error) { - throw new Error(error.response?.data?.detail || 'Failed to create supplier') - } - }, - - // ============ Stats ============ - - async fetchStats() { - try { - const response = await api.get('/stats') - this.stats = response.data - return response.data - } catch (error) { - console.error('Failed to fetch stats:', error) - return null - } - }, - - // ============ Filters & Pagination ============ - - setFilters(filters) { - this.filters = { ...this.filters, ...filters } - this.pagination.page = 1 - }, - - clearFilters() { - this.filters = { - status: null, - search: '', - direction: null, - dateFrom: null, - dateTo: null, - } - this.pagination.page = 1 - }, - - setPage(page) { - this.pagination.page = page - }, - - clearCurrentReceipt() { - this.currentReceipt = null - }, - }, -}) diff --git a/data-entry-app/frontend/src/utils/constants.js b/data-entry-app/frontend/src/utils/constants.js deleted file mode 100644 index 72404e6..0000000 --- a/data-entry-app/frontend/src/utils/constants.js +++ /dev/null @@ -1,47 +0,0 @@ -// Constants for the application - -export const EXPENSE_TYPES = { - FUEL: 'Combustibil', - MATERIALS: 'Materiale consumabile', - OFFICE: 'Rechizite birou', - PHONE: 'Telefonie / Internet', - PARKING: 'Parcare', - FOOD: 'Alimentatie', - TRANSPORT: 'Transport', - OTHER: 'Altele', -} - -export const RECEIPT_TYPES = { - bon_fiscal: 'Bon Fiscal', - chitanta: 'Chitanta', -} - -export const RECEIPT_DIRECTIONS = { - cheltuiala: 'Cheltuiala', - incasare: 'Incasare', -} - -export const RECEIPT_STATUSES = { - draft: { label: 'Ciorna', class: 'status-draft', severity: 'info' }, - pending_review: { label: 'In asteptare', class: 'status-pending', severity: 'warning' }, - approved: { label: 'Aprobat', class: 'status-approved', severity: 'success' }, - rejected: { label: 'Respins', class: 'status-rejected', severity: 'danger' }, - synced: { label: 'Sincronizat', class: 'status-synced', severity: 'success' }, -} - -export const formatDate = (dateStr) => { - if (!dateStr) return '-' - return new Date(dateStr).toLocaleDateString('ro-RO') -} - -export const formatDateTime = (dateStr) => { - if (!dateStr) return '-' - return new Date(dateStr).toLocaleString('ro-RO') -} - -export const formatAmount = (amount, currency = 'RON') => { - return new Intl.NumberFormat('ro-RO', { - style: 'currency', - currency, - }).format(amount) -} diff --git a/data-entry-app/frontend/src/views/LoginView.vue b/data-entry-app/frontend/src/views/LoginView.vue deleted file mode 100644 index 17a2597..0000000 --- a/data-entry-app/frontend/src/views/LoginView.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/data-entry-app/frontend/src/views/receipts/ReceiptCreateView.vue b/data-entry-app/frontend/src/views/receipts/ReceiptCreateView.vue deleted file mode 100644 index fa6c66a..0000000 --- a/data-entry-app/frontend/src/views/receipts/ReceiptCreateView.vue +++ /dev/null @@ -1,2939 +0,0 @@ -