From 72e328a3cd68be7d91d91112e817572ed0d61957 Mon Sep 17 00:00:00 2001 From: UserIsMH Date: Thu, 19 Jun 2025 15:44:46 +0200 Subject: [PATCH] Leads sind integriert --- JOURNAL.md | 42 ++ LEAD_MANAGEMENT.md | 176 +++++++ LIZENZSERVER.md | 24 + ...ocker_20250619_154355_encrypted.sql.gz.enc | 1 + v2_adminpanel/app.py | 7 + v2_adminpanel/apply_lead_migration.py | 52 ++ v2_adminpanel/leads/__init__.py | 6 + v2_adminpanel/leads/models.py | 48 ++ v2_adminpanel/leads/repositories.py | 298 +++++++++++ v2_adminpanel/leads/routes.py | 215 ++++++++ v2_adminpanel/leads/services.py | 146 ++++++ .../leads/templates/leads/contact_detail.html | 495 ++++++++++++++++++ .../templates/leads/institution_detail.html | 159 ++++++ .../leads/templates/leads/institutions.html | 178 +++++++ .../migrations/create_lead_tables.sql | 107 ++++ .../templates/customers_licenses.html | 35 +- 16 files changed, 1974 insertions(+), 15 deletions(-) create mode 100644 LEAD_MANAGEMENT.md create mode 100644 backups/backup_v2docker_20250619_154355_encrypted.sql.gz.enc create mode 100644 v2_adminpanel/apply_lead_migration.py create mode 100644 v2_adminpanel/leads/__init__.py create mode 100644 v2_adminpanel/leads/models.py create mode 100644 v2_adminpanel/leads/repositories.py create mode 100644 v2_adminpanel/leads/routes.py create mode 100644 v2_adminpanel/leads/services.py create mode 100644 v2_adminpanel/leads/templates/leads/contact_detail.html create mode 100644 v2_adminpanel/leads/templates/leads/institution_detail.html create mode 100644 v2_adminpanel/leads/templates/leads/institutions.html create mode 100644 v2_adminpanel/migrations/create_lead_tables.sql diff --git a/JOURNAL.md b/JOURNAL.md index 374e671..e9f167e 100644 --- a/JOURNAL.md +++ b/JOURNAL.md @@ -1,5 +1,47 @@ # v2-Docker Projekt Journal +## Letzte Änderungen (19.06.2025 - 15:07 Uhr) + +### Lead-Management System implementiert +- **Komplett neues CRM-Modul für potentielle Kunden**: + - Separates `leads` Modul ohne Navbar-Eintrag + - Zugang über "Leads" Button auf Kunden & Lizenzen Seite + - Vollständig getrennt vom bestehenden Kundensystem + +- **Refactoring-freie Architektur von Anfang an**: + - Service Layer Pattern für Business Logic + - Repository Pattern für Datenbankzugriffe + - RESTful API Design + - JSONB Felder für zukünftige Erweiterungen ohne Schema-Änderungen + - Event-System vorbereitet für spätere Integrationen + +- **Datenmodell (vereinfacht aber erweiterbar)**: + - `lead_institutions`: Nur Name erforderlich + - `lead_contacts`: Kontaktpersonen mit Institution + - `lead_contact_details`: Flexible Telefon/E-Mail Verwaltung (beliebig viele) + - `lead_notes`: Versionierte Notizen mit vollständiger Historie + +- **Features**: + - Institutionen-Verwaltung mit Kontakt-Zähler + - Kontaktpersonen mit Position (Freitext) + - Mehrere Telefonnummern/E-Mails pro Person mit Labels + - Notiz-Historie mit Zeitstempel und Benutzer-Tracking + - Notizen können bearbeitet werden (neue Version wird erstellt) + - Vollständige Audit-Trail Integration + +- **Migration bereitgestellt**: + - SQL-Script: `migrations/create_lead_tables.sql` + - Python-Script: `apply_lead_migration.py` + - Anwendung: `docker exec -it v2_adminpanel python apply_lead_migration.py` + +### Status: +✅ Lead-Management vollständig implementiert +✅ Refactoring-freie Architektur umgesetzt +✅ Keine Breaking Changes möglich durch Design +✅ Bereit für produktiven Einsatz + +--- + ## Letzte Änderungen (19.06.2025 - 13:15 Uhr) ### License Heartbeats Tabelle und Dashboard-Konsolidierung diff --git a/LEAD_MANAGEMENT.md b/LEAD_MANAGEMENT.md new file mode 100644 index 0000000..a254f31 --- /dev/null +++ b/LEAD_MANAGEMENT.md @@ -0,0 +1,176 @@ +# Lead-Management System - Dokumentation + +## Übersicht +Das Lead-Management System ist ein integriertes CRM-Modul für die Verwaltung potentieller Kunden (Leads). Es wurde mit einer zukunftssicheren Architektur entwickelt, die Erweiterungen ohne Refactoring ermöglicht. + +## Zugang +- **Kein Navbar-Eintrag** (bewusste Entscheidung) +- Zugang über **"Leads" Button** auf der Kunden & Lizenzen Seite +- URL: `/leads` + +## Architektur-Prinzipien + +### 1. **Modularer Aufbau** +``` +leads/ +├── __init__.py # Blueprint Definition +├── routes.py # HTTP Endpoints +├── models.py # Datenmodelle +├── services.py # Business Logic +├── repositories.py # Datenbankzugriffe +└── templates/leads/ # HTML Templates +``` + +### 2. **Service Layer Pattern** +```python +# Klare Trennung von Präsentation und Logik +class LeadService: + def create_institution(self, name, user): + # Validierung + # Business Logic + # Audit Log + # Repository Aufruf +``` + +### 3. **Repository Pattern** +```python +# Alle DB-Operationen gekapselt +class LeadRepository: + def get_institutions_with_counts(self): + # Optimierte SQL Queries + # Keine Business Logic +``` + +### 4. **RESTful API Design** +``` +GET /leads/api/institutions +POST /leads/api/institutions +PUT /leads/api/institutions/ +GET /leads/api/contacts +POST /leads/api/contacts +PUT /leads/api/contacts/ +POST /leads/api/contacts//phones +POST /leads/api/contacts//emails +POST /leads/api/contacts//notes +PUT /leads/api/notes/ +DELETE /leads/api/notes/ +``` + +## Datenmodell + +### Tabellen-Übersicht +1. **lead_institutions** - Organisationen/Firmen +2. **lead_contacts** - Kontaktpersonen +3. **lead_contact_details** - Telefon/E-Mail (flexibel) +4. **lead_notes** - Notizen mit Versionierung + +### Erweiterbarkeit ohne Schema-Änderungen +- **JSONB Felder**: `metadata` und `extra_fields` für zukünftige Attribute +- **Flexible Details**: `detail_type` erlaubt neue Kontaktarten (social, etc.) +- **Versionierte Notizen**: Vollständige Historie ohne Datenverlust + +## Features + +### Institutionen-Verwaltung +- Einfache Erfassung (nur Name erforderlich) +- Kontakt-Zähler in der Übersicht +- Schnelle Suche +- Umbenennung möglich + +### Kontaktpersonen +- Vorname, Nachname, Position (Freitext) +- Verknüpfung mit Institution +- Beliebig viele Telefonnummern +- Beliebig viele E-Mail-Adressen +- Labels für Kontaktdaten (Mobil, Geschäftlich, Privat) + +### Notiz-System +- **Historie**: Alle Notizen bleiben erhalten +- **Versionierung**: Bearbeitungen erstellen neue Version +- **Zeitstempel**: Automatisch bei Erstellung +- **Benutzer-Tracking**: Wer hat was wann geschrieben +- **Soft Delete**: Gelöschte Notizen bleiben in DB + +## Installation + +### 1. Migration anwenden +```bash +docker exec -it v2_adminpanel python apply_lead_migration.py +``` + +### 2. Verfügbare Tabellen prüfen +```sql +SELECT table_name FROM information_schema.tables +WHERE table_name LIKE 'lead_%'; +``` + +## Verwendung + +### Workflow +1. **Institution anlegen** (z.B. "Musterfirma GmbH") +2. **Kontakte hinzufügen** (Geschäftsführer, Vertrieb, etc.) +3. **Kontaktdaten pflegen** (Telefon, E-Mail) +4. **Notizen erfassen** (Gesprächsnotizen, Kontext) + +### Typische Szenarien +- **Messe-Kontakte**: Schnelle Erfassung vor Ort +- **Follow-Up**: Notizen zu Gesprächen +- **Kontakt-Historie**: Wer wurde wann getroffen + +## Technische Details + +### Audit-Trail Integration +Alle Aktionen werden automatisch geloggt: +- `lead.institution.create` +- `lead.contact.create` +- `lead.contact.update` +- `lead.contact.note.add` +- etc. + +### Performance-Optimierungen +- Indizes auf häufig gesuchten Feldern +- Eager Loading für Kontakt-Details +- Optimierte Queries mit COUNT für Übersichten + +### Sicherheit +- SQL Injection Prevention durch Parameterized Queries +- XSS Prevention durch Template Escaping +- CSRF Protection durch Flask-WTF +- Zugangskontrolle durch Login-Requirement + +## Zukünftige Erweiterungen (ohne Breaking Changes) + +### Mögliche Features +1. **Tags/Labels** für Institutionen (via metadata JSONB) +2. **Lead-Status** (Interessent, Verhandlung, etc.) +3. **Dokumente anhängen** (neue Tabelle lead_documents) +4. **Import/Export** (CSV, Excel) +5. **Duplikat-Erkennung** bei E-Mail/Telefon +6. **Konvertierung zu Kunde** (One-Click) + +### Event-System Ready +```python +# Vorbereitet für lose Kopplung +lead_events.publish('contact.created', contact_data) +# Andere Services können darauf reagieren +``` + +## Wichtige Hinweise + +### Trennung von Kunden-System +- **Keine Verknüpfung** zu existierenden Kunden +- **Eigene Tabellen** mit `lead_` Prefix +- **Eigene Navigation** (kein Navbar-Eintrag) +- **Unabhängige Entwicklung** möglich + +### Best Practices +1. **Notizen nutzen**: Kontext ist wichtig +2. **Labels vergeben**: Mobil vs. Geschäftlich +3. **Position ausfüllen**: Hilft bei Priorisierung +4. **Regelmäßig pflegen**: Aktuelle Daten sind wertvoll + +## Stand: 19.06.2025 - 15:07 Uhr +✅ Vollständig implementiert und einsatzbereit +✅ Refactoring-freie Architektur +✅ Erweiterbar ohne Breaking Changes +✅ Performance-optimiert \ No newline at end of file diff --git a/LIZENZSERVER.md b/LIZENZSERVER.md index f2039fd..d48c74d 100644 --- a/LIZENZSERVER.md +++ b/LIZENZSERVER.md @@ -757,6 +757,30 @@ Neuer Menüpunkt "Lizenzserver" mit folgenden Unterseiten: - Klare Trennung: Monitoring vs. Administration - Direkte Links zu Hauptfunktionen (kein unnötiges Klicken) +### ✅ UPDATE: Lead-Management System implementiert (19.06.2025 - 15:07 Uhr) + +**Neues CRM-Modul für potentielle Kunden**: + +1. **Architektur**: + - Separates `leads` Modul mit eigenem Blueprint + - Service Layer & Repository Pattern + - RESTful API von Anfang an + - Kein Refactoring nötig durch JSONB-Felder + +2. **Features**: + - Institutionen-Verwaltung (nur Name erforderlich) + - Kontaktpersonen mit flexiblen Details + - Mehrere Telefonnummern/E-Mails pro Person + - Versionierte Notiz-Historie mit Zeitstempel + - Vollständige Audit-Trail Integration + +3. **Zugang**: + - Über "Leads" Button auf Kunden & Lizenzen Seite + - Bewusst kein Navbar-Eintrag + - Komplett getrennt vom Kundensystem + +**Dokumentation**: Siehe `LEAD_MANAGEMENT.md` für Details + ### 📋 Noch zu implementieren: 1. **Erweiterte Anomalie-Erkennung** diff --git a/backups/backup_v2docker_20250619_154355_encrypted.sql.gz.enc b/backups/backup_v2docker_20250619_154355_encrypted.sql.gz.enc new file mode 100644 index 0000000..6e646b6 --- /dev/null +++ b/backups/backup_v2docker_20250619_154355_encrypted.sql.gz.enc @@ -0,0 +1 @@ +gAAAAABoVBQb0KpLMiCrRKW6itwu4YUHU0pcQkQgauQWEjCYNAjAbrqmV5KoR0memegbHRNFQKs6--vgkwXr3pTJmIFzJYrY6tmEe8Zy2pUdIPK7L_KNC_1wyHK5vOhrctOZ-T9OMiHh1-T0Z5HvGs8-qUlWyMgU1EP3eeErEePYWAI4y48Fb0osRitzQ2SJ4TOKZ0hFFQCuBzsIt0BFGPIONkTbLziEA2gnAHQA3xvpqJeq33iHmUenSOWCt8xQgXDlvVaU97ZcjKVHga7Up4qPzPDDtok8xOemUQdBTlIXbBw-Eis1_rsSagA96uF74UVG6JMZSygO8XwutJYyjCu9PvSV5Hbf26irZWey5dOqeJYhCbflkzRCF-rcRllcJms2ytrpzsgafiY36VODzNDxDuQQpMAc32buBQ3Rg6h7Qq9Iw9A1AO2frroX0b05ED9nB2D4hnGytwyA6lkTTZYur3JiVh4tpAi5prjYW5GPkTt2og37h-7Ay1unepsvANguvtDxwCpP2NlGLx3yVgdVgMD2Bgg2RWxWwBzWkJdYqGYjqWm_x6BTF8nHwInH3Msr2BxkpQVAgRHQ6uOJLgB18vQfl3XL6w8jUzrNnVCT8xLTeJJW2zVpbw51LOA-nlLU4I-dalhI8dZF4M6WEgbjOD429lsLustNRAutcgEWpwhgmESo53ITKAmLqC_l4oqN7uwg0W32TSOADFtsUSXWgeBs-cnLWeRHbSWWQl0abtSwKQdbaCTviy3KCEVlGX7IPNN4o9-gPR5SLpgy57F-riod9ruZJ13rBGRwf2ODnf1TtIYWTOI8oMxNNHlL6tdiN05pcAfQvL75TOAA-0llvjjEnODi2hREpFJPaK6ZOuFuTBzHzIC0BlAmOCZ_ERJ4JNjzEt36PBxbljFU-5xSrNIVmWSF8AsYuZC0Ss20JHDBaXcnPFMg5RjXQ7o-MGx_7lbPHdKBTDBtnYhpVtn6FFHnzkynIJT2EzcGO7nH1K3h9A3ncMs9bOw8eh5UWnX3ai76gk7cifKWtMgcdeMn1JVjWPNPjiA80Yf1T6bvQ-sP7a21q2b2hGpLGzYaRlzHe-J9jRf2B8AqKjkVPmveuTE2z1QwUPr45jPTQmFl9wCguK_AEw7pcb3H2OffXLl0yVrVjRr2gNaPenLDu8G0PVYOVG9BWaHwI-Pur5Tv7et4d2VLA0ExK4TGDv5IZwu43itL3UQcCEm7TTNXuTjkQI7NXGIbxZaaDA4boDHsEekiC1DV72xdG0aa9b_tL5V--Ys-5xyTh9F3UeyWYDxAFTWj4PUEF6ROmsRz35EMASxpHjspO9e05YuPFP-N-DBwFkHBzzcSPYxXeS48PmFvrXb28GSDVn0KRfpaCZNOUaTNFTvnwxsEswXELd37j8XjGAW-RNMSosEKn83RMR-BMkUSj_T26VhZciTH9lnFMtgwjGgqWeZ8BBCDar4L2RUoVNvDx2FRPh8m4G_-PgmJcx12dbM8HwUOctybbDuGqt4gBrf9tRusup6TPtldme6Xq6nkDB9RilsJyxwyy6DDQoNJmVCkwtswyNzsOf1DQd8q44u9qUbJ9tF14vYuygvmOt6bNOUiYvKuh0CjnTebKJWHOnpLLoe6DqIqQoX4AG85_d84T9fkLw6sgWPL5rr9HB0lB9xNRGhGyMPmoo9gyNGowHDyTGDLyLFi9eEhrtoQYkigVInFiXTlxB8DCDMy80XAlmSU_vb3drn0gvI_Ez4UXDIGg2qvZWcLvw8aiNp_cybDZBmFkntIJfAviY2QCYm4bj3DnhF3pxTp5aybGE5zOdF9A3XX4WDTwU0BhZPkE0Tuc3cuNBhE95ucA_t7PUi5qeshqswK6CSEeLOGKnLbjLiA_MfzVJl3bFLgFR2PIk7bEcJOLn41hmPX7rN09ecpMdn37HYR-bWQ9UZuCFoBNAFhSY850PzvC1lr3ttMDakFSpx9H0kBm-GaWLzvbzmX2RAytybth5HLcMVd7kW5g5eGmcalnov7PQ6TznI47dJF1ClTAwX7S3TC_zqVM5gUgCtQQcu7koqFRe_61dtKriYwnSHfmD5ZTuUomKOW5BiyO-nuH_kczQj2IbvXy1eVLsdF4m9-pyxPj2BUdKqBGo4zwz56iQpzTFAXmseGtOY-a6CLtw3WJIH9xJ2tiTBqtJer0QW4FjftixWN-iL7oBmSRepvLOoyTyu12s_fWVWSLJNIlkIpwBPbLGCP4DR_Zfd6-fN51pJhzGCuG78hgeFOJNvjpt7oev5mAuZMAPPEQw6xR_MjiUv_5Ms6NpifYZ08KkX5DlnGYrrGHeI3nVGJWzGVGdroBIE6QghlTdlGPDdesS0OKDE2k9DKI_lSIfrvM7jTsbQHxFdYfJKYcwABeXyARCZPHgY1TrarSKgq51sIE2TH5vGemcH9RgmFKiK_EION5FhZPKrLE3t30ddH-YyHJSQ84CtmpfFCjFsBM5VrHQ7NPCHGXu4n11L8NdiaMiCBxn7ohleNKmK4Vqqz9Thl6bBYNiJHUe2rN2W9_IpW28jVtwBtR70FLU_AzQhdRJD2PIKzZ73pC8l3EEdBm0NAoETP13od4Zzaa1q69Ll4PSbbLgeNgA_m7xDkKxo3b4D6VR_X1zN3_bk4bfPjd0EocCPPCKMAdscieF2OazIJiVReghb-4PuNkgIkrkterT9CnoxttiFgesYbz-ZxRZG8oUIoaB63HKdoKEP67CCOtPQvyI0dsVo7NlDlDkbIw_Kd3enH4zrfrS5gW8kItaJjxaYGAPu0AIOtHNpxxjTOnPEeiAwFRnZ4q1lhvn2Qb3nokp9g11SEPUoXhmMkJUpC1_3tgIZPWWb1clFrbYqvYOBEG6JgjUMJXQGIfxDHvnWmXoYI0xZ8XEQC8C0ZUvIpRnJxD6wL-YOUqpJjUl5yYO8xnRUXjm9juD7KlfB_R4BcU3sYTAFhbEgjv8HjNfDfiYigSC73vuWZRbjTSCkpNOmJx1Izw0r5bVQeTNVtlv_PorNSIK2wv0qwWnd1sEQl382Aq-_528coEraXzLMNX7vnDS9NEQZ2r2fgYcC4nE1nn6YlabaBSFvWPcfQUjMmo0egNtF8YwVny3kmxRKVuERVxBO_Zn-dr1Mbz3uWawYU4aY22bJ_w5waHyBOFcwu3a4BcikOsfqFUQVQdSUkaq5sUAG5Ez-VBc0URWKjxjbcDF30FDGnPB4vv8PSkaSBJzq_iC-H_kQCb48ADQcg1fVv775-ou6oGiRHp4z64DZqdpl2c__Iu8tZmHTeAxDGZzy1r2HSjY67pwoDiZMMn9UTCYFU2fujosiwqftcnyUyzn08uNGLD-vpse0RrD1YhjTqHJWNByc-NGTeAbFZuNlQZXHQVrkElxdsi6s7MXlhAddWwpAE0YUOof6sWgCXO6i-oqrQ3EfV7QymdCGNL0eipjv2euEEnleQ-i0ZYU7ff3zMY5QosnHZ3oCaubGysQ0aLwlvVVE5RVGRd2B_bjSjm_0L7WWQI_ecQhvJH_FU33fzWhvpyd6I0ZfSsW5hfUpCe0zO-4_LPFWEEjnVbvsrVZBjbDni7ordrIRVF4vY7YuxXBnPRd8b8Z_cPtjOqDjOgObQn7oL-sHrsujlfYVdiPT9jnHb1i6bt7JQU9ixnzxyUqDExzoXjMMqDQ0f3S_hRSakJ4OgeXFYQJVnr-2KX65riAZzHINLbFoIe5Po7ikieRaPreQAoRdixo57MTT4QXtd6fHsYbL_zWf7jQNNZM4YrEKg_Not0bT9TkkojeT8CQy3vGoqFE-yAOzW8aHEvm0lVQqdh8SHRPIaVzdg8H1CCpJJd3iPD9QmxnwVEYha7f87Inzl6iKwsa0iuXdj8OF5yQ0iL-_5eBH5n0scgj49-22H2soBxztAvO0LhD7NobWkejuhggq9DUBv1gdJEz9Pn_CpSH3G5b2rkZEKHHWj_pEAPd3qdpFtg6ZXvxAIQfev-QlPeCn-_51EK26WG2TZU1dhFW_oarz_suoQSU7KoJsbE3eXdDlCJMx8kXs2G8NwMdByR_m3XNQZCjFAkwCja5kSs1-hJ9YXjDOBV_brBpvIvKHAtLy29WxJA286JPbqhhY2OkKyLLQnucGo2vStvAsY2CaVWveyTCi56NmEFVQ3bYn2J2grybDtRnsHuDFXAByWP7TQwoTK_zV-dX5zi7XVY0w9DB3iNEIbuB1jUvaVWRWdt-IIqKQPQDVL_FG0EDJBxNlGSUWGup7zzfl0l-qG-hp_VQiOkYjnYj2oLWM9WvXOgbtte4O4ahfFl3oD2Ipj58jGr3SMA6P4sFUS20Wpp0D7gGilvSMRP0LwuBNMslfp1kWf4FsURie9nJjoCI2-gr6XOsGd_QHH26nf4FEpiGnsRB5V3SaLiJxhgG3nPAST4taNDOQLJjKWwKj8kbsD0IFzAwFGHkk5iSIfscEqtCbTeqEvPDG4b7bsgzky2oIwXmkNMsemFAYMdWPUsbtOQJDEDlNBIT659_XkhsqTcHrYwvLnfVr1EsdyzpPIAo8Bn9hfroz352JwH-wu0GDj5Oc6UG7OoxCjiBQdd2DZTW2Xe4gqoat_4OzGg59vXaLq3odL6nyngOiPfeDYAWCeQEIfUQE7-0HReaU5doTTcuu4aPUVd7cbAcAgX2Olrz9dtitCf4rhqCWG4iPo32krp3XJdtydjOuDdVqWdn27fSNn4XrARrd-pZCpKMuk3ADvVmttQ9lnUBub0Dt_amex5LfI-6el0iE0JavyCGFVDUGRGfulbWE3mQtZrlBKQER8WDFquDRlHWVZRdiou3orjhiNqIX6YtPs5Q9PulSkX9Ff7_wN20lTh2lsIz2O7H2ATy-xVthqbwtChMNPDaRJu13Sz5c-MgGXj8sHKoHh_c2sQSX-SQCeJ8-VTAjnu1Vy3kSq028PV0ZIUADoqO4TWe5n5Gk6jUI8AfqrrOeICh2pucmZs2rDueNGLVpNU2-6AiHq_ziDa5ac95Ni0ygm8r2jgqo2yi4RSBmX4ibxNsB3imu8ssaxjxSnVrB5DjvGjEPOn7VY8vZFvxLZpkAvHGFW--U3NrNhSg-lN6LxKeaShJUsTh7aiSR7ofKgOYwluRVKQOv35EMYRF2jEjFwsX1R-OyN_eAKJf80Vr2v8NeXJiqBWPSDbP-stSaOt6VZYyBRhVQPqe6WGmmkKXhAY1zNmCl6kmBYwMIkfPLKx5ioANpu2waCjNe2SIf8tPU9IYBDNXHrYDesxNxtKDQAR4BgEWhYtz7kT7GX2l7tZYxdgzu2enohiywvyF_-191LP9mBzJBNapQhJWjeFaEIinHOKbaY6EAMXGIzm-MV6hIEgbfoZ-Lt4Tf8MiUGqgTgJ1VR4P21AjGRENds33jDzKnMOOLGkanO0Ir43opWBqSmx4Pt2XLi9BS7U1g9-vj1EVi9mbv8oVhlC3ohiEwPU3PpoSLX-gkYtnFoaUaPX18AMMnbr3-5EOBxl22iRjDMvtyMGcZuU9fU4VTrUjIfdPwfF1wtvwhNLGKrbPXvK8n4FFMCMyOrJPLzfCKNQKYCqrLpuxsV5uNvslKDTzYEiJ15b18T_L6wLu_IXZoDr_y-i8JaR7lGFUmiBimE6sYCDjCBxRNxJggcpcX2eXGqkQz5Nav9WMGH5gIpUDLqIo7OMUmKPkF1C67W3R0cYVS3SxwFI0D4sCKbRmWAlDs2vJMLS22pVCPKEc6xozgAHr3Ie0oReKGRzaY-eQiQC1lUi7ubqFjIBMeG4Efz51V0FxsD6VThG9RbHXhcHvRhMt4Vbl2MpH_nZ2unszS5gZmZ6hhAssma34M5s0NTJmMfVDhZMG4SMwuvrtIaSDjRVpLW0I0pVO6cdBdOUEoe-Yd8Afwvt1VazWa2H_Su3KpxNJ0Mtopm5lOlfBrl6ae3033vgbRdm2HTcxepUJZGiVhXKpNN-f--egA6cOClC8HeWJihkBHU7Z0E-Hj_G13NQef5gzC-yPPxoX7y6scB30cp097T8LBVbvor-VkGJijJFbBDziwleDi3fHKsrlVOMw7FW12Aq7JMpofaqjO4Wh3nHILXmDzL__EZQ07p9m0sIgwTLE0bfWIUXsoliNa0uFjwKEW7Z9Mkdnt9KZs0Ijp1TT2SMdkcPYl3nahHs3yQjHAivJx8b5V6P-4ILLpZM-tgH21p0FzaqYmtrd_u-BgGZCK2dudKe8A9kij9ofuTwHhQfELxZjWQU_sCSRp6uT6eHVHQoY_wuX8O9XrY137vlRELmxankxf-n0l1elNZy7iWqMjtIQuP_GUnSjTY905aHHQqC-UwTtvlVAyTclPYaxSkGdeOx9_GHVW1EM_zJmCNQuUJMw1eXkIpCSfRglMarnxQStIBtBrq6moZfZxXtDhdbZ8WE91VY96SCIRnUA8D89x2FIoBW0cSNKOD0A9iW5x5kSwcBkODt9MYrpuzY52husIkZqkZoL2CYc29iduCGAOLaKRjPCR4YQcXKBOeac9BhJ3Z3R4vMaT0cS8BCaZqQymTfXyA4VRAZnlHtJtuEOFOTrcpL_rZhfYktzI-7voUWHTIHW5-bxX3dI_yimO4P-5E-FrClZl3HA6dbfiJzgJArlERk6M-IgtsWC2BDJugmuoUKNRRMSP8F3M3yE3LrRBkgx8o0zRD8iii4ed60us5_PWjHUihYH3ZN3uzNQpJh7LIUa8N33R2zlYOkOe1UVuJno2BjNiTW-ECS0g8HGg0x8AkMzAM8tlG7mYhvBZFaduwo8UO8d3l0AObAWnNaKfWXH2b-5fzltWRQ-_o4_4a1fzfVBx5JgqCGXuRb8Z_0yaXNr6hZK3Ny3VocX1Rp0Kn9W82sa7B5Bie-Dk44kAQ4UvrmLMIbjIuD7l3v9XJMe0R7r_KZFQzHrIhynQknH_EwRzOteG4vSMtYUJNSwSDieBiQHW8OzRa2YnAOW6z856dcNFHKw2PLPmbFH_8UoKoGrGmVy1GvHL9hK4TzmndmIMsvABaqGSGpzPScqw1HpakChjgewTU-RlLzmPL55Qf_PsfDQ_niSq1eEAydq8uf9Ttwq7bUl8Q5d2HauUK33fHRcNbA5-wE6QG5vveCsvndxJe2LIelBE_HNytoVeno0zk0Avh_sbrnGzcMdPlaaNYhzjto8rsLYl5sxUoikzXVzHGH7mrUYm8EOryqRVMuLByTsr-UXxQsSjy4jFvNz4KoAJxaCDmFFphaXRp8Bj6OZRjfwVW7BRvCCvOcZCITo9TKKkZK1LNqWT9s2pbANEqM_qciVhGaF6E5MjT5CiyLAEEUgrouo6_qgNFUJcs1V_ijaRWxZ2VtUByTVwLYQbAdgHT-XK8ZcXRI4HU0GMQ7L6bI4QeXVDFAh151R5uq5t6CyzHTUKEWrBcLdGeozlN4EPKGqK5GfvQ_QnToG128e2f-sfes9bJwSyZEAL7OCOWOyhavsTKD8tJs-9ATE7zaeFJKBjEJsj9cgQAal7dcmEprxdquqAz1Te_3igIlMnxyBosLyUV-drlj6ae6adv0jNnNk6533IcZpzHXhHIhumsd1J7Bj5a_uB01WB1VU-qGqDnXsp_mHrOiPfJSujBZpPIYnQaNt6EBAWWJIQ4tYi6GwUrSWqY-2S4xD0d6cASH2onWEhbI_9WrspaW9t7744Gm_OppDkv-8cwCq7r7zL8De2rL8o9uf6QWjqq1SFOUzPclrWgFpiSVwKFg-c6mcOjz7_Bqj9FDIwx9IGLXafYSTo6wdamQ_5BZqepnf1d_IN0Nvufn7_iu6h_Tmcfe8ixcUNBSHLKm4rcSEsfXAqO-KtZrUEyFVyvwiLP8wlj2XkPmICn9lSFMPrJQCUSIvFRolDO-ChGxoNKoKytrtJMeEcuBFJ0s2WNco9ISeF7o4GaxocMFFXZMXeUbnNhT7YjDgzvxmLpuygmMApOXCUPojebMBTMHlrFb9m3-r91KwiheaRtXK6OSpXXLv606igTAFmvYmoCwtN66nUFA6I8HqPvvaFaQa2wUF_AklwxmoOmodqdOXlcTXVEoFxglYBDj_NrA740QTiwVnJrxWgnCCWuH56KJM0fbzh2BembA9twWnchsy3AKVZBBkYtPKFz3RnWBRSZGsUyxI-0BvTQjPfsoNgKDCx-WerDdqzXEzFiYf7HxxFGfVzNJms4i1j-3dqVrkIBKyEDU9donLz97v8gmyyrhq-A_wdiIX_IOV1hFArEz8Rsa1yEqZXdnK4-z2gnwjtuK4kLpzmJqDuCGT7zxs2lnq1XUE-iKWx463ujldzUS_oAgZCMm8s-4UpIEoQkHRmLjo4iU6MMYN0J--hjtL-2Uche49DiZREuz9a0M9Up2XppCbbL9MYpeITIdzF432WqKCnVU6u4fWHtjB1mNcFVWe4xy8SQxh8RJCN3N7eq-0nJkmyB2_bZHWXj_ZW3FLTfBA6L9a6-3yPgllLal34PYTzafZUQspcsijj9YmBXSN2x-a9jcjv_W4VnmoWw2fIgTQVQW1635sutOCeutm3NN7mzTZBxLaktrP-jKx62080e_-p5tvRSR6SIE2cylBWNS2ClPA53bSvcnwDBSiWv3rBw7LvjsSjXlkOEeZb4eEs6l1cKkeT7XNtU_VpmUeKQWJcFNch4PlncldEe7T_Zc_3tAfZXPupkzagP2suobMDGEbvDCvRjJ7eKCbttUNoYs69dGkp7GHsaM0MPcCl0P-is9wTyFGKbXaqlmQd9yFI3CF8LK-Rf6YU3-2woSWJ3mLuqptTICq4pjP9E-LCIalveGON4gw87lpyUiboTglahd2bpNQP2G3jIgalMwm7Lw_nnougaPlHT4bVfGCBJ0UB01LIIU3l1OD9HrfJodeKg7_uEyN4vW4kXi9xP_x01iXg1pxNkVvXTYISMJkzHht-yHd_jdQrTTMGVTsLwCjiSgqefVp-am-uUoU5GLlSLi84GZ2RoyASHKoCki2XaCgE-SxJ96y40DjC9mal6joUU5AFZCH3yZYOBABJ02-3LQdM-714OKHrSfzGXVC84X1HRY5zWT66wUvMuYtO6D3MbWyFlqemgG3rur6zfNJj3FPxJ1wJeOVWze3cAsfZSgPJPaALUoR3MLls24UwDWncG9xs60C8mKV6zbzp56P41pnBoZdXYo4X8VTNxoyerzM-O4oLlWOpkniCkufRB6IbZX6hVv9Dmq0NwhMi0RdvNhNcTGOourMvf4AErwBP0g7gdyBQP6o1MBfLemKoKOjDxE49DaNxn84YFkVGRo31C0rnCBFCEKP0nkDXExDfYAeuFguz8v3KVTUYXRl8kNNB8GpPqZE07Q-YevrHu-zyKgTBFK4BxUQH_ZxSmUMqBFUNLyu3N7aL3_e_pc8JKS-yNWQLNEQHkl-ynwYckS4dI0HM26W8Tzi8s1MuwaS3BFD_xHfTiPEzBmSmuKAeQNFq5Jp2kRQSDlBavm4XvmEqKgYO7XiEZ7LOLu9ZSe2Dh1oKAmMEyWiBYntMjce973qNqd2jSHhG3zWhlrEmu0YQUcrM6q2AgVZBxHEBKySQYP6T8voj7InOxwxZvYrdA9frj3gdjgDnJ2flicOVNellc5HIL_qt3UbUECbJC3yqjTliV-X7fvGo1fQ8su_YBl6sBtvTEeCXi2w7iOs6fe0q5JfCLZBrCTXfDJY2pCb5aC910jUXa5cTcJNkd3rSdoLGfa6EftX4LYCpZnH9C2Wg4BWZAK-r2aEFECUTDWFk48CUwzX1vXFGQ1M6Crysr_sBeVcW2NfFEX1yH_CnJKyJ5WY0YQy4lb7i8roqPcxG1NdeAmt53A1_bW0rY_oM2jjSQ6PsgvKyB4yBclaf6msoCuwKzMKvRYNfq8K61R1QXhhM7HLBbRlzFpXSOX_CWseGTYa7HXlJYdU8at1nuABvHvqb70-7rrNktWAPx291R2F9DwJId0QK4TudcFeVLDP8vBOJP1ytfPcnh5SVWrPeueL1AeF2Z46p-1Qp4VBVNr4ZTjFYFuT2jH7pMuVVTk9fSWMUltBLE2C5wWmxdaQeA53SACxf5BiLqOCw49w9yfUSAOjyp_dGyRVDzgA9eDSaqtdT6pXKcbTw7qIr60guwzpUsGGPTjjtSBODyKbP2NnuWN6Ko8xvR5kDJxHXDukOv2hQBKHDRxeaJca_jguaSIKXnDP0ulLq906r_SxGTec-a0IDmjTznFL9bKBpl56qcXfvjnoYoinYjU5iZ5j4m68Dqls6UDCmlNeUeLrZSiL-hsYdrhVZkk2oGWF72GC6kgk453wplkFVO2LjuqmzhXd1_8Z4TRGdQyzynYzEzRjOGoZnsQGHc9yrdT3jQs_3tHNyb8QuLsliMUU8chXerKBCD98TkBRRUWjAbd9-0K1bh5LSAZdMXxzZc3_3m4AUUEsmmDMMBnCU4sgunYQ_rYZgTqULbP1N_Uijt4LwRk5FTk9ZBMKZ-rsdkgk1uSWHKBBNEHZbXbPmODQUBosnP3FCF_TA9aUcgiDWN4JswAb2wRpd_AERT5qU6sl3v1EFXLVykQc__ONiu2YVNcjsKSfQtnwN3UI8t2ugK_u1_Ic2h_2NsWKXnQWF_oGMGKlABB3rKAhTyEUoIBEZSipf730l8WRK1by0m4mfQ77frcZtMlvZjXsDgMCQD2NywqY2dNYrldBqkIb4mYDKjJQsZBOs5bsmS5EKiS2hvtrfaAO-0UaHRZ2ZSQ9MLcKGP5HsFPPWMzizk5oqFuM2nElHSktWhxij5kJ4TDArQXLQJD8udtgd7Wj49it-OaiEiKPK208nzlRhxrvVBJsQz-E-uYbkDHGjTR0xondHrSrhryIlFkIFlxABR1KT9L9LIgEPjbtox2VW_L1OY5--FK1hzzae600zF1nihqXk5KhHBUZxqeG5E5jEOTT3vuSrptRkHdhBRi2cRNlqS4QK7mSOI8SyvTeWNl3IGxM8URe4hum8ikzYwfJJWdZrs4Q4CYS2nfGV1y_ivc-Mq2gHGaaGeSnOLXdTQYg_tFLwI8hy52bncPXrC2chx7XKj8KNU3z1JHve0mKvpCqemPReMArtYD8tBnOBjbfzx4Q6Kumgwo9CiswsdlOGpvtxi8_jCGguPejEnlMzfsSkoffVRtgxuTzmiQUpBf9lTnaQ11tukX52ewp5HZxroUn_uqMuhI23eTj7DgFcE_2VcYCDqnkTr40wo-u0WVhRW5-GFxxQgcaW9TxbiY5UIhhnRGwgZqTSfdMSFUikETSz1UVPDofqGKxTrAb0aaTjsSi7sxecoI6kr9vEzLbD9bAJn-r4Vb4KAc8sP3bazZjIQKE36wTJoM75wn9e7GaAIG4G8UF_4TFwRex_wDOODQepAAsFmD8-CI8V1rqDRG1N7MmExdhjmnaYI8kgTlu_JjvPH7H_Tg_0ApmnumOhgeRQiZ4BMi7RCZPCTSp9sk9pgMp7Rj0t16VCILV_nVAQbVZuk89Tv1uV0a0BkmQba4Z2ZJlxmRlzRXxF7n1N478b74TKTBL1XcMFaX7QKIVVoKOQ0_lM8n1MLlulnE9pm6_XqvaqVj0d3pINC8wk5VMgf8cDas7oopGgMgzfCizlzfeOCVaRibYGnDIqbK6ReYzqtWJVNRbMmM-ezbL4esHxKQ0ahlBqAnPxGlesiWN1RAatfORkAH3SaXBjutmk7cDEFx--zyGCPUwoVUqEIbR2BbAcFXk-zWoJt245sXsyT_587qG7nughgvm2MIdw0FhnTj0ae4bwheh2PZXKdi3nEFKYG45eDy5je24-IQXzhA678v4Xw4AsHASqHPDQ7PWQbiGlbavBccaK-Y1MJCGX0nDQGMim_Iwea-E7upcMqbjesDX6TNq9wMxGT4YlwheKcX7Phc07HmUAOgLyuAuo1G9S-_FjosvWYfWkKhhuoHMu37GN3_dRbsS8eoCsl2KVoO8RUQy0BneMAtlN7J4UCg-9zXwKSz1kcQ68rDDpoxFm2nO2HAR5EpSFjtngnn0h2ClUOTRWvKc4Tizc93Xwa8EL_zt4X63NG9-GiO55ppvVAAERJiNh1r6LBMA_AmBWeVbzUnbaRCN1TG-k2bVIKL6WwhVE1Iy-gDnHeuneOlP8Nh5ZRTYs1u5orFsM8fHHZkbxbJiOtrZn1RGZJKeB4Ybh3Pw_nVUkLEeLcDy_MDL49zoUQoyBuIuDCDg5PhnIBTNS2-79_cqbuhX38qMplbcxCU0bAZXCBTwEtJHZ00zGyICU6lPhbS6dwHSJKRbXHMFDzRWR7IY2jCXPx8V7lSbgHCbUIgTc4TQYkfrfMNoq1BfBKdCx53TWCknm8kEo5vlegaXZRJ7iN3d21yc6WDEUNUXUJMpHrddkJs7OnzU1Q3bvIxoqdrwbAr6u2UFPXDwtBoj7WrctAVEw4wM6ZuPxtqSMxzIqqN-DTJzPxDOkXKUosrfxe99EyGFYJ-OU4LB8X7Wstr9fSGXKhuOy3oIcFDKyWkTgTCARtjJi5Q4sU57kbHqWihfp-mtAYR6vlrL8ak_0yiAKPBTdQ7D8LDCCYkjVK9OIIanQZL4g2eI5sUYYpd6LGevaZO2TsJbjpfLiff41OgDzNrD1CEqfkT0egpl3AH81SwJ5fLxShOuAnBpi6U7RbZh5l7TN6kcGaNS1l0_Vrda7WcIgn_k-V4rgEhy0i89rLW7NjXd3OonoA2FT79RoZ6ztwVn1UJvutNbtPA8wwFIg5PFnDITlUGTI4p2L7XhrAhlpVxrriRpVlzy6oKi-YYS-It_OAm-6JpAPgm0XXoha88yOjW3EzLByL7WgzD4ABx4m-4GwuREYVNQcElz00nPt4Tk6ZbLD8pt6rbqwgYTFrURXCp0qD1DpF7KS_6FDMCmFZZpAwrEUj5htCdBxm_lEsCVrhrCQvqc8i0HVRb4YGjCKbQrz2FfST4Fh4HqtyUemkhYSw8W8VapduVMuRDw4VoWJKOsyKRNN41NOCksAopO_KVBvTVcSfFZVeWx2j0J-78fLp1DMOFn3t7KM5GvcwgsLgyNAJZoHzzVzREhYX5eYUdCv2k7m7rSIY15jRJyz24ZpzrORGRen9bXGhpXBw-Fz8nOPvYWsMVTCjFeU8rwLZBa_2MzsscTvUMU1xBfUYV-uAMsbT40IzXAoZRDgnWy9_iFcMUAJC-Jp0ZyQx0mIS7R7RxkNJWKFhP1TEZSgFu57wOoe6yLYHer9DRK-bNDb4v6SaAUmYQ9AVCNDiD9SKJOQRYeWPC2lh8N9ubdLzxKeV3GwkdQ5FlVEXZNUh7AG8ywjgpAYptn9zhSc5aqhGinkFAfbp6s7vy53VxSTrawh-e7E4_mUqMlkbTi1Qo4aL6wg-NZDIMMkJBYgb9f6tBV-6Crgc__H81wj8qi-HHM-wsL5nvD7L7aR5CcIXNczAwwYObIDA1RXWaLOJxnT8OLE7paAvIIhXUjeFz2DPqWcTkJXSLUHNJgi7OdhtqmUxLim3khn0LG6LbXfea9KXIvmAtCLiALwJJj083PShhPV5CG7YE-lMbrvkrgD6iidZy-qY6zqeACVLJONuCO3IAAwmTfEAXSu3jRLw5nYKCXKDi4FK-OpMcW3Ju9C6KSg7qcTF4w2UfQEpPCImB8kfVpzfZGNL43qUd7Z-GJ7EaBaD-vclB48PsM70T26pt4LENupzJsaaj4_2cjpVbmbixqbVH5aNdF7JS4oNgJVzP4-gMLSr3Z4fSREFrEDHHExzuMI2ADAHwUNkmBmuiP726DJZNLMLbSXA14n--J8uG7y-AqqQmw18o6xW3KPeCTgBvUFmzxYN_kwSJSJe-em18Et9vQwfpEzjWe3586A5zrXVQIP3kfb7Tm4vVH3PSq69XMR-JhpqawIt5dXTrl1vNf52bHJ1VB6WLqJozEAC_J704yEvKZI2sa9t0WfC26zGaOWGohBYzNdx9yMI4QOGK6fc2E1J52AwwlCqEzoJE0fTdyj6boOE8sgzmW8wV4wAIcuJ-OzSAtslNsAKf5b8fEkvUjkNhdGwY0IRGI2UpG2AAsWmF4ujnBPGyoottWqg_gSuT30qEWMPseFcXiAwItQWUeVvkmcN6axSJdKaRSB_jRXxHz-DgIurdLk5aIe490G9DGZZJuDWND7HhSWjjZGuLavlOmlvB_CTWLiR-X9sBogALDW1CvJjdBPmzSzEmpJEMOtn6C0lygCqCK2-thcOB4MhKYOvixQt6ubuEZISVDcF8woWP29DWdx9MRHNHRg2OAFleNtp1WsqjzxVx8WYkNdBEGWzH8Nw1jUw2L7YpOrCsvUBxXsqzOHrTVMOZhWuk-KLnLggi1wv7b3vYzouPFSQXO2dY4bMR5Sk1w-tHOf6s6pOBK3S-Sw3r_BuClzYoqyTcM0MnWYI_9JD97OIk6GVAzOXA47G8fEUwiBhQqFn0JZo8vQWnzhyKn3-gUjD_b8KPPu0_eOlHx3j5hoDHqwav5LaiFWyyKrT4ZGBgSEcURQEaGxkeBitJCdhu4GNbgkCcqqfmFMdkBBnmNykN5qUk50aJ7_a3FxSVCD9NkZimn2kJB8kUdZer_UCWclrc2ZPYux7GxI8a5hadE-VNnBav9fn4a33BiCWSN8U7jdbxbnNPkcJXaqMsOKgg1Ejow7SgPrimxPwOHxEbdRnbuivW8-9O8ZbbFVEEOsZqpqHZzyYJ2ml0RT-E8A7e99R28Yghb8aCsvwGDfpkviR_2JMBOsxy_0zFUxvpE4uh1Ovx-byu7ued79e7qXOmWivEAo65RYc5uoq_wxydMAuujZx1zESzTrrlXE_IoG8Kl9Bf9Ev-9j-2Rr8bUiv-lYo31Gk1aHZmpjAs9zTuIvX06LZ-SbsU6mSpr5ZxvF1m3W7byMUZ-Pdi79YYglCcp4gR6PVtcYAo0I4F_Pg3gY2JDsuySC3SO-81Tkm61-biKyUbQG12C5ijA8Gh7IxxJZRJEtcQ6Vgtkw3nykNIPsSut9DsILLVeqnuU6ZdDJAiAtFHW_nN4XR_V9czDSvd10BhDSXkY1AA7g0Mtqgz2YO7Wb5IfVbhtvnUGilqr8x1BB3Uj9g4s5nupCW7g1_ots0MsSVbV9FsPcWfMV_H0QFFCyqA60Wh8Ya7fJOnPaMVSpNepPAYgVxHd4ePDAcsGujHl-QkXwIKLGorlYPnjoJ1Vt8hz8OvAofFWtSM3yR3j2UFl3ByTyuHfhYe8iESWIq4I3BP9mot9QqQHivysMiQm5Id1SUreQ8DKfuIuGEcEOxJMULIjLFpOC7eDIctTUyFj_9x_4h9JVKwyFj9lYvEZgIUf0iAc83byvsyv6ZZbvsLDY8pY0SiWHzHyQTiJo68bJlQedmh-oCABODlOiFXeCCPblY9bh5fLY5hLAd7bPqhibcJtmi0p8GEYZ0PC24Lp5hWwx3AtQ3-lZOM12n42QLN5bbMcW62rYLo0pYlSh-W1XikmEvIWKF8TDChbcw2cUO3W6Ek2Vksd08jeYcrot4NbaZhrgNo5rscTcQ1b6BbmooyEmqROi_Dn0bJV3_u92yx3kiOFmfaQMUucbnmIGwWemMzi73XOeCcZl8fhTGx9yySkfGFZPIWWzCZoUxrRneymkbUs2PEL0AcPvsHLO5Khyp6mdancD2b06oywI1vAj__BFOBMjWQI8EhFvIyG8BdQamcaQ_73yMCXNgmxCbPdSbJ2yoVI8zzr_1ccJgNwPQdz5Kqnj9nFLWLXMXM6fjjbKHes87kCPtZ7rRhT3x8oBGsQIU8ny7_mD42UEyiQ_6FZ8wYS6Y3XM0Zs6U3Be0jh-t95fmwPJL6t-M_tOs5WrDj45SuhlgeSFBzm1pqf2DZNq8K11RbVHtV6oXk8b7GgJUw_MBoYh2q7vlniZefXpHZmy-NJCwWZFEqA8TiD7DkXUTy6fuqRhoZcwPaRk3TtMyPNF7Fd4J9-rEQLZp-b37MChD4BUoZuvftCAPlwhzg9-pI7WLqD6I_ifowrD2Aq7MOo2w8CjL2U2TWg211dq7nMxJjs_jEZVpax6AawSAFC7dT3rCZMqxzgfTaUJZhcSShb-tCWtaaROFo6dj9RXFmMCnQknJLNdDJCaAGdAgr3lSpBDZ3YFwmJaGFAKbyFmpF6H4NRtHzpttaoARJyfSwc0Qo0KTj9o7oQLK-l6HAAjO72QHnrJKkmasSqxbSn4pouM2DzfI45F_KYATi_KjJU2ocf0X5rSDlR0eZiTHVz2XGGmmkRHeWNmNf_pfI8NDmP8PhqWT7TkIU5ILsDnR0LTZNBIaWodZEFssCLacmg9HtGQG2Uvi52k7a0HfEFD7T874dzp__RIRIIcZBueMdqzDztwCsielcSkEIme1VqX8qIiGjehGtipBPTuLQ_BfxYOFBAgajmgu-DqhStkKP-X0VW4YIsQfNJLhJDGft3UV2_wApobiN-Of1PTV9JgwZgsCY5yw_1brUUGlVuKjZdKa44okjBQ7m4KmB00MAp3cGGLP2HGhgJpgB9eNRiSrC2FAyrY-iF5MJ5i48BFvcmFojV6XsHwEkdv_qOJp5YdW0JMrxwZNF0Itt7eZdMvIZZYAvH-7jK1h9cSfgRTi0jP7eUxC3zzRgA7qIe-vYtLK_J8-o4Scc2wSCfG6iVX7fmU3PJaNrdCrgeWrHO2AWF2M6UqqSvoQH77z5IOge4ny_0Xlyk0A9VqUfCh_uf3sBy2_yOBmYYYq6ront0eWDVwsnLLlCRJIuF6F1-5JYFLuxKkpfxu-bRwCUICteHL38yGXGPY3lCBDYXV-KunxXdbIENiR3OIqqg9ZrnLi6Aj1wCEsJPOPrIWKOa6D-AGSJZNf3hQOZPwnmhfhObAY_5RUq-M-3DcPInWxGDYvoXv-yhocqqv-uVozYeh3bsRmDJqItqMw0fHxftt02p9SaTrsM6FV79oaEJxxoRBA0w1kHPKfq7vcdF9PZLb371b0UeFQJ0MjEl1hCaCDYIOuFdWeg-z27mtQtu1ogzvZF4Pu4EsiJT7Two7Za_bmbXdt-t3j0HM8CC3rfrOUy8Yp9UrUe8y3RDu2CnpgG5TcbH7MZxyfRR1k3XGbGYSEaonaxgxIKeC9tIr_jOcJrCW_ToGRvId-9AFpL8HWq0w6Q7dEnUR4CAl_qPM9zs7L90JO8fkLwomRd3I-NNWqAtFd3Jmie9edd1sfu6oIP5tXn6BcZYiwaf2S2xvNH6TgrSATxerSN9z7v322U3umHIugSGbmgi6b7sJSo1B4dAyIwqd5c8PF9_vWwcmpHT5WWdNB40BGY73ZJzqA7k-5B0Y9RnF471JGr0Ka4gG13K7vvUH6jpjBuN7maIocmJhJQDHKXnRsTltyTzKor0ygtLcxEUFw-SeH-DPETfSwMxI6JZlTfP94MYzpgDdIkA067Wb_oPp_6g-L5Zvvmqb7GdQdgsF-ZqWDLrNeTSjH3Cda0z_meTGg2tcuWlugAKcPpsjEaDWsxq4Y7BlPVc4dlsEgN9IVNNxBoiT2fU2q1laRPWYi1nhrjjK6q7zpju6nVN0pyx9KC5LQIBXzc_QCUd7uvDtRsjyl6XDHM1UzMPyESI5yCqwzYm0iR85EuL0xWWHHdYrChNgui645IVemTTmkkfc-zKj1NzgvK2UZ3aoODiPnpzZN_g9gtJk-bqEmz-d1tUZmu5K8-2CeZx7IQoDXcHXXXX3uXbl7JoqTpcA9dQrnxR155oNEvVq4CyQlbYEtOMjWgOwu3c_NkanhXhQ5UlnRc3e_BTeMVfSnA9Bg8qo5z07R5osdV52ypBJ-OtITGkM6In6-AdiqpHk70GUPS7Rr9MKTJxaBCGZDR51d_CvXJi1KUgkjnbbsOoDBgkOZmN12hMhAPOY2DSCAc912gkZOXdHW0W79IXt67_3J9_jj6l1nGvm5aOANjg30mL73eya4Z8ramAQ8jq09IwI_3-DEsFbdXl5FY4O6-aIAN10XzIy5ernxIwa_iefgj4-wijqXlRsH33uUp0mRaNIaUy-JmSIJZJcZZENEPMYw_T2FX8cIMr0jdAV6dpI54Z0M_N3mljQbX8BnJEkzwq7kjjeh2VSFe9hU_K5zDTL_2VM_El-YZxyhjnxVG3azpTcRcOKUykHO4Za42ajHkAuGaKH1p_spsLYF3iS6vKkLwIsxO_DjhK8iF-vKCQEALkxOYMxvaBRZ9Se0No9YremA5b-LXltT-ueQPT7KEP97Nufvbsw4Gfy0ssDkaFV1cZVil-9oKHntU8GGtklSYsey-l6f-S6BuQNOPwbNvsEpDxm4BT9XJbgmRVdKlYQ0hiPLsNpKTgzRRYGnViGrAgpSVXPLO-N16IDihf8CmyVSqfebeFaSTkC66uSlEHV0x6vYzT22NzoVNnBPqXuy4pjy72qxjL3AvLsUyx2z8bAzaPAjsh4OpXs0v6daI5xJvMbbYijRSP7LjqLlqpTPhTEyTHCXMMnx3VHsfYFrI83DjEHKHFHOOnSLvFxBOQ66LfdqnlzNo7JXcQxXdUBZpMMhezMyZwFCtKkM4F4fLB5lo_fpSCFsieP2Ny8Z-tG6-e84re0IMDottq9idRUpepajIqg46_-AgAbhRAx9WvRzNP7rKEvMRsXanhcBMhWVpgD6sb9BWTtZRfRJ8FfLXENFrsub5R3vqvO041RgApOMEVvgnyrpamLetr2iUPrLNjsMKe39BXrhbII5tpRrcLzMWP4hcYz07kfNhKH3ijWQpGMk4Ovd7h_x3u5rRWlbreK_gdI6rDyTGDhdHiHSGt_sZUat_VOxc2jMyzzMFo8lakHznOsbR_gTxtj035iTrw42auGsdfC9xpsBOg_p1nICAbhiMyVHzBP3GuibF2p5MuFQuELx7rh2LUe_YiIUlAWDM0CJEgR1NYIpDgwXDsHQv18tHjFTIDuAUZyPd11jPuoLwy6L2VGYLT-dyNm6cJ6ihYjURGsdRykC9Vd_pAL2FUtowyvCcWRp835hGFhzdddS0yjdVei7IBTn-OAxfOKKCC6tcJLSAQG-4v2BqnRZy27nwAF0rSwa9Ciuau1FTwPrpm2igzWTlEue03YykrOy-huLYq7jAoIGr5ES6IwZ_U0a9Pa06CA_3xASRC1oLtnzKBL3a4joZKazCbM8dPpsHXkWq4x8ekhItPnDAcLQjO0qurgs8ZKPzcL4MuRdn0ZkGNmc3eqSdOnq_TPgb3QbJPE66r8AsCLbZZaPLi7z_E2RzuJYaYUbnYMHxmO0OYc0D1ZcngEtxEKai55b0_y4hoDnIhKZ4-BN0nnyBz-VkfLMzD71r7jp04GScwp-MNXlWggGkVhLJho9Tixz6_72Uqipa2kANBsbds19wjnXjWAc9V9FsTm4yLwRdcc6t3dSz_VuvUtqdFZ8IB2Jwz5XYbbjYRXdGGUT8Upifos33RKFGAdg3ei2EpSAp2YO4Ltji-Z9eGcSUDVEaDf5Bw7kA_CPX0yHABMdJvYbN8CiOcXSa6Vp2Xss9f2CjynBOyElTVbjXjSaAC_-c8cp0A86BKoYLUvNi12HYQ0_5OnLwStjavNSqy_TzVb-k-01IXp3o2_Q2oySQwo_oEUvAsc0vFV2hqWV6B9lQaunsJCyKwDtkHhLcYYzdZloesRLuygCp-gGcYT6Ft1MmAleqcnq0IYYSmw3vVt5WbBqFo9pOT6nhUspM7gH7I0zbLVUFY6HapH0-cdcFW9H-v4ws4ZLqROeIgjMw__pGgl-hrz5xYnvuEi52y7cqAzo4aGtQu2u-daZBKcXF4s_fIVIxJiCme1JY3VUQlLFVNCsxhENhHl_kckdpC6cxLenO3bjchEh9D51RwLWV_uiNwhL_FlsX3ECMu-SlT_5QVbdFkZoWycFaFRI9MBmPUwNjnKUQVt5JAhsyFRYEFtm_pFaj2AhSnrKYzPwdw0JofleVUbpz9O48XX7IcIpGjTH59vGOLx8MnLxpvCUHcxcPGXs4v2I4ilKQxWUZ4INnagBjqv5Hb21dJOCZVVxSl5gbTL2hCqL3oNoRbNj1qXOSf0f0qMU-m3wjfcQCMNd0q1D7ImhQffZ59fdAwuuv5u_2Mnmw--sE7BAjIoaf5rpMon7RZQplJvcHcxMzOUN3F6cW1nTzCVJVuXWgMvRvEJkPXkzZ4y7tRBBap15oBc2aqqPCr2eLNJkp33l-R612IDQlGXqKYh7qMDAksprGaurK88ZtOc88mu36S8oM9rNo34eoESMG9zB9CcBEpcwYeR6k0QP1odFSzdrKmChRLk5CVWb2FYFatU2OBU-HttX1F2ffLjPqAsJIUwBLQV9NO8AG_R2Anj5vRJyZG4J1nga2X7GFM4qckaRDlrPjmPYnVuHELjXFRsN0t8egitHHL285XFWjFddRUr4FXRyvRdrgjWwPfOt9juKVo-8tFLWzgg6mv-s21NyjmVPAdKf1JptnoxR2t5A8ld0EAH0DMa2gg59XIjAmOpb5vpuojICTlA0qBdvE2NRrDk4zVVH2dhtpRdrVXdayoKWnOslqBwz_pzCyuxJchSp6bFWEfpj8QwqpvY5i_biPb2G-J0p1ElfdnmYMlT8k4mbJMkmATidkvsbg5kInRH7jeHiP6OdDAHzogHCkDjHVaxIizaRvhEBqSRsvUMt43Uwnvvgdw5RqKer2hL3J9GNGHp1vJGktzb2j5hulHTELfAOS99N4ipxcFbM43Oml0Flnra4ys0MMNNay87qgaPKG-4PzFp7ZJ3ylew_zyCpc2XHoIUqjoZzUwl35mtUTxNbDXQWE-YglKjzXju_PNtnLsuJJACAKd8P1Z55u2H6Yp9B2O-y4_A8MKnapFOaEgzSdDuMY55foNZA5VHtlg7XD98YRT761SSiMGFkv5jFnpNYwGenjKj4GQA6IzResJyKl5mBYyxsEbJJBT53jJoFXDROzHkbbMdFm2Fz0S1Jr4x9tN2PP6lxvn1deWZ4sfQj5sSZWNmkFuDXzGIxUl7xb_31wJ1q4GU8lcKqeNb71gtKibOfAtqpoPWcKqFviMTNhfSUlviS5bpa2Fs20ud_zOQA8wmIFkpv_Plva8WZTRWSA5ou2HgJ9gjJPxe81DsGREd5f1fQLH-CnMYRyBovcFNNoEt4wWVYFwUvzwBCpElP8tpj0f1gz7NUdPo2c9Zs6Y7PuVvtbpj5zp9dLWC2RM9DJWwdDoewfu_Q_I8FBgVtn6akoZepxnEup6OFE5tdWRIEaCByHVkzakAP_517Da1PbH-wQ3bJ-5y9jU4cEas1ZhfnsMnKdwYIfL3t9HhxhzwwXz3UgcuNjHTMaMeNknApj9mTQSg5wOPaPWuPnBOx6w2yhcjwDTZHQNRqw3OpGaflybtLcXhoDGfFsclzjOjQN8TiqnY7ozGxy8t7BphYCuuHjJymGjUXQ2-Ys18Tv5al82_eht_Fs7eE5B_7ION7ATo1LznTHE2BWFkuADJTw0TG87eJbZ_rlsf-PCUWT-0tndvm2UfyoJ6aJYAvUiq4VEFVI-8Sff2m4R2gryWpScQRS5PfLMss_pdptQD9B9nHVLBAx7TTTpGE-SYiDwNJEOAbGOad6Nh86gWQc6ezv4gde3liB5dEdKPKFAXacN8tqJPxmWjrdnuM3V2wKJbmY3MIKKmpq-YTUpJg9nid5TpBjo5B3ejVCoo_1ogzdvukWl_UGuqqsW5dXPvf4_FlJGYNKLQo8DvD-NOVf7fclSBp0NInvP3EvkOoXYnmCZr2o6tAq9G72hOcG0mujjmHAU1m87igep128e0ufOiGPc3U-sNvBsWsp7VEjRcYwq2E60ibW6jBb0qoFim8M-dXCzFv1y9WyfWlp5ESss2hdacFPlLTNCCeQWXwCHzeyBETxVcHUj8haGXHkJsIFanQWoqGnhgyJkVC0NMRJuXWVdyXMtab_6l992UPVCJCvmppj05XxMl0rFBmenjDzQM8ojiS1MMRZxIkRIB1VAI3ZYc-AHZDkfHdOIRzrR8KQqnXgviQ1IondPvDqWmlFf6niWQlZ0ooj_neEEoGNq24dfWH37vhB3Qj5pyTOSu_6i3YvHmfB5ZIBH2VTHQ13Guvfml6zLmWrZmATHXtZmiN6SqTY6YlPlk43FX0ruXAifGWymHyzO1pVEne4YnxJAVeWPA_YnpzzHgakrrpJ9WEPPpf4LN7ZNkhxN3CPHZjreZYz6uT5dUu4UfUR-4tHIL0djWOo0leD9ahdRVac_9ords1sNKt5fbmMZpuIeJ8_JhCKFeeZQZRqZl99J48TcVOQkiq3H02efzPN7R0Bvf525CUyObB9eBAe1Rhez08FzoLiXRU42ThIxINLiW4uT-C_3K5irocRpvIHjyPEQGaD2-CrvG5KBemNgdIHxgRxOeCyKLAwgd3v2eyHuTlmhnBpZBIM4xpkvmhSnm_0Ib-bgt7G7LZ745jsi4zMDN1WUBLNW7E26eYr28Fwbv06vhIOf2pQFzAfZQmz_IaiDK8nSS1GuoF_-CMTJIXfqa92TLZQaEGeDEcibORPyikb0DYYjGYIcoyhif2jTNhYOV2sFneFoJ1jVnFsK-L9QDRRFbRxJQn4xBTEoxjLDHGfJr1-r7VQ6uGtOz6qPz8cmq8vodLN6jjpT43fi67IcEOqmoqLK5q5PDkjb8rwxyvEnoyocwVl69KXVlufnbNdE91wIvNR_7XpmefZPvQFt3ygem0pqxuOfLn65RM51sHISxh67jgzl9dnaS4hQMfv8XO1_9kNDVGTNC81LthxC3DBymn7Q8SKuIvHyc8mBgBJB0xeGaBA5mK7yPt6MRWYa8Bt2_ttUPq8N1UeB5As4hDfs1VGk3RvW9C3aj4LtJeyGzaXcIQtYlDuuZBIrPz-1FQjk-LF93n_qFziycNwmh_6n6ihKn7XXqstRpkxK8cZKKmSq8LgVIFMDZFBervqm7zFt0Lqk_1Yh2_rz2zooh0i-hKHyrQYhJO0dofcpOnGkoKJoLcI9ukjnl5uprH66JHwOJw_Cv0XjHr60dq7NjoJjVRh0Yobleu5M4_FSgGjTFHust7Jg_L9eMj4Z-BUSI7D70OEDCw4eB0MtJJTB7Kcu-sz9tcldQVHXuce9gsFz7ZDPsGd0-U1oZhQPu9NOnhHtiV3OwvsgiGC1FTPabKm8xmLc6W9C5UnS1pZX0ux1quUCPg2DAprp_Ozn-f_2-cQAQT2ozeERuSYBwdSOaxsFSItp9yDJSyrkCmwvdVSNXszp0m_B9Dyh5gPcYYo0WXUyMX4sAUpLjZFQ39a4TWcmaPRn5mW4_zBIC4BdTYnZHrc8tmxZVeUI260jl3Wfp8IT-Otn6DhBoWh6XnbhNsg1fuhaUuq-B3s0SyU2C13eme_B7nDqbVWmDEWjW87f7imhLbz_C_h7xaptWSp6pzkdscHvxuyu7j-hKz6arMNGdrBYrmCdnLDvuq_a3xsPnEPRRkk4iumIeoJpLo8BoAIMiNy96gnq5mxgElP_dOkGwRxQmuXGd32lykQeniGESZEjF_jHBhVhQOfi-x7Gh4juNV5Lr0TiPIFH8bN6EUSP6RUODJNVlbyTshgwOb9lEt8IP8TQNuWjRj2hu9dUbv0iC1tmSewLeOgmoEQPfEIHyKEJA0y2QI_-tjJFNhvDc8UtiuXW3dnng8Nprn7h7q6mMe_PSdI734bBLIT6qqIGDpl7tXo5d4yehOwc3Mqj-aqINoFXwc2uGvZKUD4cAQUPBpye5Ezt8G_14bHGz7QmtA8I7d_V1cpH7PA8jmYykJ8XVbrr_NMxjcK0_Jbw_0Wb8IOBuyOvTweDQRiMAUKCnpG98b0jnVutijgsq-JsXVBxcX0Phk_xVaylrXvhuTOjODaRksNJlv_s4-PFdIWmcsNq3K3L4Q-Jhy9Ts54f2W_-m8SsvNlXSp6IzKEBi6CQOmuHs1m_b_6VxtTgzZeBjpGAIVe-zG0jJvXkfFZbmgb1Mi2fIt2CE49ku0oLIFnOc8hI-v3VzjPF-TqhE-UqJaXX--GX0228TNO7DBruvEuccRsPZdBETOW0ell15OGWaVuWRgGYHcrBOSbAFHLsnmN4SLA9mK0RgeZvcW4vd8pRfZm8gQX6Qdz6zcLU83sZseCn7SsVjjvktZ93l0XEOLoVPEeCG6oqJWGM3th60vVMsDq5rDjlqDG1z2BbhlETaQiKj5cGIIQQsRhY3Pi43UliZtZyaYYIsG57rtil5ggbK8M2fRad1tnYZwdL2Q3abXO7prigh3wsbEMVa4S01UpRA_qU_lZwVkYFtrPBn-rxuOL7xUw-wwQHUyf_sKfKij5T3GWbyzovjnLwOcypGJHiA_udvBaSL-OvBTZC9WIg_PQY2zjrQ1FyvOOMUgBir4oOsIj0LA_svKoUF2e2aSbJQPJ4dWxkVgLzw67GVkXMn2dacdhDPZKghCyij66wpTcHK0Oz9dugx_ggRkFCxPTLS2Eio4GbtiifZNt9DhZK7rFsy1Zh7W6kM2n757c7IRLZ6etAv8uOiDzNGq0GoXLPUW8-Jn_woV7vErwhFxMJjifP1OyMzf8Z6PZYCPJar3C76wwZdHJrdW1Y1muQcmdCjKY2PgzhXgUGyIvuKRMU4jJm5sk0LVArR1u-uS0ZlxnQrCv8XOXXWoZb05hbVLBpS_VKfj0H-N2wiXdO68_Z1yw7-rztukLX7NLBdTXUgCo_Ksq2dTbEQMG99xeifEZYJDC7MRiXsK7IWg77-Z8Eb9KT_wzwGqikWo4iKOVIU8ZMCEdp4b7s9F83EGX6ir3JIlJX-S07tjz4B1MM0qYFKCRR3VPQsrCavMvfbXQhC73wppUO18bz1NMdGsRI3s14oR6Y2HR0uTgMTBFjBTWWQUn8O63LlnLbp-o3-ESkTdHeXbiJAELTQzfF5hAZnQVSjlSrmi3TNOSlktfzf3IszkxoME2DHS-2HJ9700TYJ01ZO5G0CgnL4IEJ40sgqW5ziozeio2lKkoTDfNQR_8E92cq8RTAesyloaYxTGVMFpRwBoPr6d92IEQgtG7qGC3PFdo98FZRrX3DP5JiDwMaOE8yY8u-DanJi27PF2mtu4LLXBQTWIT_4gDytRWuxMSW4Ct3TnnlNJk3-YXzndj_rz3Z6Q7qOoCPSfXnQUuw-Anp-UU8_Id-9ByegCMnX7F4g55sYcUjwSqIU2RAj6l5kMYAFdetiwV8rcz7JuwhUBbE23b_PSOCKXB0zwpc2RcPgC8Fi72bLvhm1VQr5vyZIhkeLNMzOrrw6dPCqzzRhGS-3DYxspui_re5n00rzOmUJKEPMA84fELtY4P5V2rw9oEG5E2asUppZ1WzVT_xbhUAqafYqk4tCnkNegU8CWJ1KmybTmoIF01pY_iiOEHX0IUBBx9XQgLSyc_m4zD_cfuqK-4zd8-24UmWQJeZZAR_c9TFmkN30OSNx2mj2YiSdeRYVKqyNDXTvmbQkxxLcWX1CBZ7bw_SiSbTgkQlVp0e6yrt0whfhj1b5atd2pClUcQ6S_RWngm49MB0J2SatIJiOTNauEoxzr0ig-Kr9TERrq7uQrd7rpp3GsVVrHJcBxR-DHZM4Mx1fcWg2hNryIHw9kl3nR5QLxU_ybR9eLvEw8D_WJR51n4TOdOXEAmzEFpQ5AkDvNkWUifApBOuR4HFcIACQxgQBNnNMqJAN8JNB69O0yQa-Vh9bAq20M6MNoBvQVFgH31FAHkXZDSPp0corDgGTI4Yq_7HSBGPQI7-g0lT0NXk-suaQnv0wtHMYqeAy5EDorZoy6kEjQJL4mbyPDl-qnCKJk7esQe-AamGQxsWcdnmK0fccV2icargDOViJi39i0gpfqBK21TB-kUgSpBRVHaLE_iQu2MkMI2Z5-IVc_Tj7RBB3cu-6rLyOgZr2HNnP1ERul6rd_KlXGhwBNK2iXrAMfVijdRcxK97jWnFapE-McLukPEkcAerPpVNAiEJDL3PHGMUxHlcU-g2W6OYJQZ1Hd6vUx8hHhK4cwB8uEBq9g-KTe8ZCk6rAQKugAf_hHSVyF3Tw9nGWm8vh7ZapnwToozSCmJX59pYmOGaB282rKdCy3xgPCtHnRZXfsIXSGx6uITLxVHlzSNTuXQDDUJ4R-Ov7y1WQ7zUC8UBVHuw5yxunnfsO0EWKjv1drXNSJqQRYGe6FhEKrDwbgrcgb8zZjyVyheXu-RUwCVQjIN-EIAUAeV0oF9ozAdb6qIaX9z7Aa3gqxXjBY69k8SNKbEsP4wbLqK-TVJ3W5kQDB8VGpBPAb4xIVFYpLY91WKZfmrP2PT-6hqLrRkqpWkmx0L7tHP6dBDUgz6R_XUh7VHFZ5KX1l-L2Tlq1kL_snm_MOuPShNIihr4VEg4f8djldM9S0GH_F1hLXkC2W6TNBQNlRAjBAmrzN7sgeiKbKKuhu-GEjNt88fJyi3JSb8tzOXrWkwvrKL3oAaAinh3SbB1xNS-td_WY6j6-5jaCgg6Y_IXIjqloPxT_SazSKt8jUmfINzMooqRxCy122LCKPRw4ClUgiW5hs9kLYVsGG7ygDTbpky6ppfnYBAIM3ozNOH2sqe83OWb2sBmSO6spqQcE8m7XIw-VVvWqtj9i7Y82tIA4GygyNwiEoaGed0KueDTaV7qqFULGN1HX1HHBIPrqlJ8qVEri-CK5J4hzwt_RX2sYc_bgsmucued0BTNJgaOD8EmU2spgv3W2YqnOoS9yHTMrCaXOaYAZBkFN2nyladQ7wsIqHLrCrs4uyGnn3ybKVsEijp4OU4uXJH7iE74AGcoC-GF6LlHlctZS6opnq5QrLgzB1iT_W9bxkWeqPOTHSa3LpHq3qKhzSVJzD3o_-jdoT97Rqe99R1g7sNAjKCYLreKcRFmJ6FYTuyWlYR2eKc29gLvWeloLkvmXshsZtmDI-FxxrnSMuCmO-OiI2i2HuspL0tOtCDtERyfCVtm8nRUjk3a3Hdvqims4Y-C_yy9eDm9jMYzN565Cj5NyI2kZxE2tN6p5_WMlnCq6iUIEpif8UQSvLDN88hBEoQFMIsVypPWRJHN83hxHjbN-rET3HE5hdNThrB05nZGVvC0muzT8af3obH3AUGMeKdunVHvgkIRZQmpCtcrGVKi3sr-pDE1NtAqYDTMu218tLdI-1xOlCfUNLbp6Ib7KLb5nKRiH3lVoKy_ar_zLjhXGvJlYY-OtHi60QFHlz5w7sdVLT2jmWmDMkAsa02a9ardBPbSdsV0_bvlit0zfKBi86mYLktNbvoUK6ACEOJ-xd38jUJVQ7F4yYHMYw36X0XCD8nrTBdMdN9rbvD4gVPK3cXjp7nm9RszKEM2mBaMqbssSeG9AUKkEoRAR2aMGEVLGQjpiNAvDLtVHaXE7e2XggfHET9Y531YiAitDogn80vT9aWJvkiLkTPmICxQtGw7Gvk7S5YJGmfbwakhXSZIGaia6H--Jt6AGJpqcpBrFUe54k8rM60MYLdBbpDCWdrJUwInPKySPUhhWZ4sLyrlkPshHCUK7vWcA8npL3rTZo3gCc5oSelhhkaoolu4Qjnd6b_CPhkHWXtE62Yac0ZmK5pVm7MaKjA_qh3vRKr2h2EBFbdORLRt03db6nyMWkQVmHeJnDmEM64NHLI6OGN3JqTreAJXr1g7bEIXHasK0mfmGGIlBvKh6U7hN_HtXl7jyB4cWq7tznwT_OD6ncW7907iaQIFwIzx2CaN64mQ1d7v_lYdIcjliyMDkNeJWcbQfi1dJHdsKjJiCrOQg2_KhfVVbkXy-JAirCkG7_28fPuF3SyRlU48UWT-CFNsdaZKfO13ukNlWknSXV2rAgg6rJTvz_Is9MjQS11F37zEOnB96orFOHc9FbOQsutVHLiUr5SI_OFxews5IK7a0ETMTjYQaOJs6as2invZgzhB1pEXfZg9EVaTRXlRcwZkFqYQ-8N6o344_tg4loe5Iq5pOAJnc3zdyLPnoHzge5YxkmKXyCPAmcHHEEQ8-gj2DT7QLsO8Tff-G52zxaWIE0YPgBsvjSeUFXsLdF0pGNgHbklX-I4tIOgYXhCiUa-hw9HCCMToamiA7brsPGLplw3Ajh39La149ub4brV1jDtKcLM1Cf0qgEdEfa63uWslHn3NvdCLGQ1kDdVsoF56j0Jbr-JrhXf-jtWlhZJ5k7Jl2rSAn28BsY-j02ShZZ8tL30Y2-KaMmfXdGu-onKwivlFsnXdDqaV1ecOWZjn3hCuCLrDo8klA8H-oCU4bZMHKN7lRfQ92ZkFBSXEuarH0yv2huzDtBHlx4G4YMtVg4759xmUvFqkVLA3STpRm3zwE0q8tdhZWSCokP0fl8DDcnt3b8YQhJSdUMRev71n2-bmSy--agwFuuXNmi24VpMJ9MJgaZGxAqrlaGirVQVaBms1onU1QglxbcbYBAIlMgvjYMjAg6Bb10CzwDU39vjFhbXnwuBUa6-A51Hjh7wUNN5TM9EJWflYmHV9ysS_RztL8cTj61GVGskhe5PuUpoNwB7ASM16kuiS2kKTPFal5s_igG9bsimfuaGV1ijSLWovVHgLKKc7C7yFDTzj9qdIjW-D6WFOsARDTKCcpC2E7Ce7aAAWtq-VzxC1N1RdtBw25ZgqRgwNBItE9krkZ2PrUVq6hY_G7Jaee55MfI7hUzze1KrYz5U0bwBW3xMUeaj9kb_xi2y0MIx0CVvjIgI-HEPk8GUleKbVeeucgV6Po97l4i6AHorgeXWtEkd5aHipDPYruYTXNVJRIolIZRr4DRpbuq5JcN9nqCAe3oOOScjvsAt3e68CMEs6hqtGpXo2fk6g5m9SZMWDo4_X6uNAjuW_xKqmy_9vMO6Q-NoExLAAluvhYhodTQ-776dfLqvF6mV0ZlUZL_WP6ypgeUp1DB96W16MOF3v_hDkzAgzQCNNlV9H_iVoLdmm9gJNHaZfXfdswZu9kIgE9slhxy73YHSAwaDu1p83oa4uxCZosMVc3lvTyg9jGk9xRZKmO2tECCrJHdAyJsN655v6Wn_yXxucYlq89OAG8qsWZkn4gtUVmu_Wt2uaiiU26BKhEf_X6-Hn7joSj8XrjwAEofYGktI0S_vl6hgJ9HtPKC6Hy_n4f1gOtUllO5OWi66jzebvoGMpZmZci67YUZsXneDl9Er1_7u8TNJq_dCK1QbmVfygvKQM1rc3-7L4WkAo4Je1xL47x9xmZ_qz9ftjAc6EjmvejL77gIk22hES5-5KMYY8jA-wxr127POWB49EejkLnjJ4dey4JQ686LXWc1KuG26zQl130SlJPPvPO8Vya10TNm5W31xJhOTehDHzHieOx3FagzBBPNyswA1tmZ2hVzMe5_MxAJGWmUTLXAK3LPaI7wwwaLn2f-Ja8jNi0dT5inRJS8EwyaiMwnuclGWCAoVKnt4U52bpjxkOXHUM387z2GZsWPg3y53PE8vf1_xz66Tu02YdaL0sgh7RBEHJ8ACKaAWaYENgcoJ9oPzUlTAsyc8qH_3-EvoASUyuUei3FpNuSQZVzOwOCaSSjYxP5JeqlmoMt41lZeOtZgef0eA1NwDgg4l05mk2zd3G5MsFSjBEpJ_wPvfGVZMVOUT0NPFf8lIdkWlmT5ufEZjcbXMWT4G0ROpPBECegNVXH0DGmOa8Tlw4GS7IAqG7K__srDAvCSpWbqRgh0wnyTABqdaHBCN6dHObVE6LI_eOcXzf-BhBe6Qi_CfZsg8JI0q4GFWg4-IN1XVnm81ZYLWUZDx6owiv5nFpgv0KcBGh_b5zTDAiV2C69iu1V_0bXLZL_-_GQq2T2uZUl5jkDEw1lOsNBRgEX0rfHEDTXSAoIjssZcZCJG8GqiiFPb-7hJqp3R8DGtB47WR5tNOR1rjCvrsCj0JZfNuB4HUucqzmpDZkB1i6gH_MofxkQS9MqTYIT0iFzF0a3dumwW0l3WgBdiadwDWs4uzdFyQ6rWgJkyRMox0k0rzbFdGuTDz0j7Lx-NoNTmxovpUHv-_81aY5s4BHpA08DTTUUjMuv3i2kxdUXCFztQjgUbNn72GcScLwn0w2CG0yDnSNgufcWsygV8EnKyVRCIwmfRcgMNZ7WITAkPDmCQxLf12c1Qg89hmlgOLQEvLgEtiGDWu0mbdIq0tAsz8-k_eEXignVBoEmU0PPbcwODCloo0QAfD8sguNNsU4sStQpDRE7arn-RevwssD7QcD26FIiTa6yA04J0ii863VPlM4S7AB2EJ_87vfgoQQsjhVjvH-C6PkOFL7HPd8fCpSjlFpTm84n2O_7jh2A9j0nFVlFkvZU1PXwxw5vSsinOTPf5XE1cJOZDvnrPlW3RlqPCnBVMv3Xc9BKB7ip1WfjgHkVLhXU913eQHgBy5jdz_hdgfDKm9dzW4Ih-4gxvVuLvI-lq3Oy8dRS2KMu-Cgoho2hc1wRv4DPRorhvQF5Wq9wtJdMxfwaoabnDTK2XKwlGD-1UEHDKNHWLBX1OM-j7zf6VPZ8Oj3o6b4LfVvQQtwDuDCc02-fhc5SltiymqISwZysXyFeQfZHcPExtUxYc3mpqAbnyOxx9YqEihibB1jNHbjdeU6pZEuk1iQfCr5gKItwM3OT6zDwX1noK0gZkKuNB-hmS1g9y5FS-3D5hJpOT7Isk_GJBnefJKgMAGlf_AlRCt-W8w7XzNrjei7uX29YExL-Tp8s7imn-uVFgJNqEJe0OWa_jaKMGL-hXfxU-2A3FqRBLVJixZ2rXegpJFoRPhWfbiJErn3V2LHj3QahFsUrsNCgjKw75YZwsja3HV2WIm56z2qyZ2taDW-vreJSTgt4Wd6BONpyLEldkZF8ISy2g0eTafNqz5u158pj3cTwbP6c5FwOT6uqvB6a4PdwpKgsyEK9YCY_UB5TgxQxY1E1neb7P1gaMWoXFti4mo-cVArPcPNquwy4GNBlGjwNGECDnhyoEx3b6122YIQGIeru135Mm_xZeWjXeNoG-e5CFfHsj4BDyKl6iKh_HQugwfjx5W_NCIkU0zpnPQDecdNp79QreV7K0EIXNTnygnxJu-B2cpYzwxWawLF9lJaFiwnnPFSoVveaoh1Sw5S47LtizzraUiJsIfPGSCwBjoUlDlePFQeg4x91EkBacbw6pDbgS9FRp0xizA-tS6pjhGbnsPcb52axBVQfkCn9TefowXr5t6ZVcFOmDO7Kkaq1-sAijMS7tgFscKzaaNZM2_08l9H_RTpGqhERjjdwxfuQ_pQsJ69qCzKf7LDfAdtUtXHFzlmHKbtI1ndgrrEq9CXbbrnFTuPLnwCp6ElIkPPUWQZi97tcEq-ejOdHqE80P5KZ4LrQakG4c2mhHDEVWyAMlgyf6_hR0aBHselgoLPNEA36hCOdBH03fmTbT39c0tHc7_Qi6yk07z7eXbo4-wfd1MeU-lahRGztrVxsCUiceC43-LSfyhEIwtHNYzLgR3ZrZpnVdz0oIlUYhQCFuCVzCfRqS9NI7cqYeGXYrtRN6KyNIbEbsDMVHlv4lekYRkxkxHIBNUPBJeyH0mhST5Df3oRrE1wvBXVivivb3DzIjXUn7kfVxrU1f0hQzTlTcl1Sgb33UN9q4e2gu1QepRM_R3HTVRsClbPyeD6TV8iv35sfkNClKQ0XfX66vQZm6ecYNvQOt80MIZoSzdrlYJj3ynvo2eX75uzkbDg0rU7f-kiQEoPEPigA9rOoLkWyOX8gyfNjYCksafD94WTp5X1-c2U9yeZ1cBW2MA8vdqe66XTSYXOyrM4M1S5iEMJCdxby0uuE0MqjvmJQkpAgu_avKpjHQmcZbj2C3Bj9B2jeRVE0qgtiTezOJJUtvIm4zs8R4cEka3-g0WLJXO8RUzcyNc5SpH85Yxw04lrBLWxQia2jJvqJvD6NZZl7Q8xbiDN5Fa9K5A2mEHVrexTJt3p9LUecRvNCvA_KJhG5zFdbnjc-75996xFcSkbrQEigc3uQlTVoZjgbo0fhf23zDzCkSh4L8ymy6xVASLJrqxrGmsGDEhEVDQMmGvI0-gXtImxwGd9GN49mez2sxctTuI6rjOTIvC4X2AhYFyks-JSCSn80ok8ox81UjCzmrStKEKAKckZFKK5w-IlpWtJCFgnWKKgI71EfsOrGnlOR60fO5GeS8aly4tvNaBwhDH-O6JBP3qBIRL2slAf1BJAtYbuHBHEHzWZhpTxN7PZIJn_0fpPFiXgof6Xy-QOeH9v5OZBQPfwuwAj8w4osPN_Yj1-c2H0_PBLM3Zgb4fXYrhIfJtndA1rJ16pME11gN5WnFPjD9-iuV2IZ2_en2IByJlDPg7knoMof73fqndLaANQR8VjKzvEw5Cr-0_IU3pzCad1svVnHnVagbaJdjAegErQpXY7bmSmK6gN1I9VarYyDk8jICqKilh-3wna55fd-38RPaabUawPTXGkLJd_hYEdWxbX8gtgpvkNrK3P6jWvQM2EJunhxZF8nnvdkKdgpDG95rgCNPXzwDwZxGdQrY6s1kNj-zEPFt4vBxkQZi_m_CDjdZ8jagn_EftprjMR0XVL_hizuVflJvl-onDHl0Ab25AM_To_om-41fVQ6I2IUmM0tZpPULF6lMB4dt_9SVqPCxvrytWIB4P0EcHvYVeWNRSAzaU3amgKziE3hhVPMkf9xVah_cIKnZFRHaRg2boZtDtMD81RCwBXRkNacb5o7VMpgYzR4lAi2dM1Jzk9lpiZLDyDHDCL0Sjr0WJW7M-cjR905GgGlRPiRbN1XG1C7L2UoojVJi2OlreFmmEyRV6Y5JLRJC5WtfpdsxTf5Xhl_xDWyMFYcHxqCjUXpEPZKBdywbXkJfwmg_qbZlS2gMs75jXoFdIrOCwwMPv36PnJr5hoNPkfxvEILNbubM0Z0iYgpwtiBzI6XeLoHjj7FPNefC2KtVw6KlixVy9drZxWomZlKXIY10DPhUuIfoHlKM4o-G827Fa8PdcppeJiWuStSyeowkQjiMQe8JdhG1ob6OTs4obQ4Bdxo3CgFNZeD729yyrHbPxFvHlS35XonMnzdd8OVExM1agqscsPd7D3kABURtBqn30WnQh041ABkCRud9gA9y1NaNaPe_q1FTylr2ZVXWzm7o40trX9nBbv4tR_lzja5mBTFOq-MPflSuwkBo0vFE0auOqLOUXEqrD_4GvHjbX_-KpU3wnUKrj75wsriTYmttpnAt_14wh1DezZLzMipjlVBiFV11iMf-A2j_GBaWEmdqQrYkH-DdM9nyS6qG_jvwG_oIq6ZvzQYj9Uf3z1Ttgq-qwKJnT4JogvviT3EsAOjAO4IM9l3q4cYdp47hlNw-dITIAj2ZgQyUj1e47IifDlV5QkgtdTHEtgXd8jrRMmt1xBJ6Zx9kdRUvnuR3WkRuP1-sn3BTExfH4lTDzRtIBz3tmvz166WoDeLg3V9QGpH9YGE7ANnfOxaPj_yZEGEMe8MWCsJ7H7f-zzxy_twxBW68v_2q4S2xF-UxaGw5xcYue0fhiR8PWoJPXzJPdYXJGBGF8eYu0F6-qIRLvci-oEcSs4xONd5ogyvJaNwru58PwD7CKx9Gw3-yVhYrBhsNx8zMTy_0VZAQrY7h2I54-HDWT2GstZgFkIM1QUwJuvxDL2MfACsw_3Xatrphj-Z8slCncVnz1NfNKkfEkk9vRKZX3GT-7hlattp5Pzir3WINcDLhpQ0sVEN9Y79OlTWJv_8wp1tXQd_FnFQ-QXH9XYS1xGmdMFqFMJW24FUNEQlgyTQ--zMuvywophnnimuBIeMN5FRyFLVjnYP-EqUiQTzTdK31X2vkiJDE3x27e73a1tTO9-kS9AGl0Lgc19Elj_TO4LXwLHXqpmM1CjjFImMuPWdSLYoVfJnkzQ81NfdEPm3UTv5Zi7ige-8CQ8MAo2q4dZ0FW_4GJ-c8Fv9eOVn_vorb4fgo6MgslVXTBTArrVgU-H6AqmFVxhdlL3N3XEe2XiQ= \ No newline at end of file diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py index e690297..480185f 100644 --- a/v2_adminpanel/app.py +++ b/v2_adminpanel/app.py @@ -60,6 +60,7 @@ try: from routes.resource_routes import resource_bp from routes.session_routes import session_bp from routes.monitoring_routes import monitoring_bp + from leads import leads_bp print("All blueprints imported successfully!") except Exception as e: print(f"Blueprint import error: {str(e)}") @@ -77,7 +78,13 @@ app.register_blueprint(license_bp) app.register_blueprint(resource_bp) app.register_blueprint(session_bp) app.register_blueprint(monitoring_bp) +app.register_blueprint(leads_bp, url_prefix='/leads') +# Template filters +@app.template_filter('nl2br') +def nl2br_filter(s): + """Convert newlines to
tags""" + return s.replace('\n', '
\n') if s else '' # Debug routes to test @app.route('/test-customers-licenses') diff --git a/v2_adminpanel/apply_lead_migration.py b/v2_adminpanel/apply_lead_migration.py new file mode 100644 index 0000000..3030a05 --- /dev/null +++ b/v2_adminpanel/apply_lead_migration.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +""" +Apply Lead Management Tables Migration +""" +import psycopg2 +import os +from db import get_db_connection + +def apply_migration(): + """Apply the lead tables migration""" + try: + # Read migration SQL + migration_file = os.path.join(os.path.dirname(__file__), + 'migrations', 'create_lead_tables.sql') + + with open(migration_file, 'r') as f: + migration_sql = f.read() + + # Connect and execute + with get_db_connection() as conn: + cur = conn.cursor() + + print("Applying lead management tables migration...") + cur.execute(migration_sql) + + # Verify tables were created + cur.execute(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name LIKE 'lead_%' + ORDER BY table_name + """) + + tables = cur.fetchall() + print(f"\nCreated {len(tables)} tables:") + for table in tables: + print(f" - {table[0]}") + + cur.close() + + print("\n✅ Migration completed successfully!") + + except FileNotFoundError: + print(f"❌ Migration file not found: {migration_file}") + except psycopg2.Error as e: + print(f"❌ Database error: {e}") + except Exception as e: + print(f"❌ Unexpected error: {e}") + +if __name__ == "__main__": + apply_migration() \ No newline at end of file diff --git a/v2_adminpanel/leads/__init__.py b/v2_adminpanel/leads/__init__.py new file mode 100644 index 0000000..fa003bc --- /dev/null +++ b/v2_adminpanel/leads/__init__.py @@ -0,0 +1,6 @@ +# Lead Management Module +from flask import Blueprint + +leads_bp = Blueprint('leads', __name__, template_folder='templates') + +from . import routes \ No newline at end of file diff --git a/v2_adminpanel/leads/models.py b/v2_adminpanel/leads/models.py new file mode 100644 index 0000000..7671422 --- /dev/null +++ b/v2_adminpanel/leads/models.py @@ -0,0 +1,48 @@ +# Lead Management Data Models +from dataclasses import dataclass +from datetime import datetime +from typing import List, Optional, Dict, Any +from uuid import UUID + +@dataclass +class Institution: + id: UUID + name: str + metadata: Dict[str, Any] + created_at: datetime + updated_at: datetime + created_by: str + contact_count: Optional[int] = 0 + +@dataclass +class Contact: + id: UUID + institution_id: UUID + first_name: str + last_name: str + position: Optional[str] + extra_fields: Dict[str, Any] + created_at: datetime + updated_at: datetime + institution_name: Optional[str] = None + +@dataclass +class ContactDetail: + id: UUID + contact_id: UUID + detail_type: str # 'phone', 'email' + detail_value: str + detail_label: Optional[str] # 'Mobil', 'Geschäftlich', etc. + is_primary: bool + created_at: datetime + +@dataclass +class Note: + id: UUID + contact_id: UUID + note_text: str + version: int + is_current: bool + created_at: datetime + created_by: str + parent_note_id: Optional[UUID] = None \ No newline at end of file diff --git a/v2_adminpanel/leads/repositories.py b/v2_adminpanel/leads/repositories.py new file mode 100644 index 0000000..8859907 --- /dev/null +++ b/v2_adminpanel/leads/repositories.py @@ -0,0 +1,298 @@ +# Database Repository for Lead Management +import psycopg2 +from psycopg2.extras import RealDictCursor +from uuid import UUID +from typing import List, Optional, Dict, Any +from datetime import datetime + +class LeadRepository: + def __init__(self, get_db_connection): + self.get_db_connection = get_db_connection + + # Institution Methods + def get_institutions_with_counts(self) -> List[Dict[str, Any]]: + with self.get_db_connection() as conn: + cur = conn.cursor(cursor_factory=RealDictCursor) + + query = """ + SELECT + i.id, + i.name, + i.metadata, + i.created_at, + i.updated_at, + i.created_by, + COUNT(c.id) as contact_count + FROM lead_institutions i + LEFT JOIN lead_contacts c ON c.institution_id = i.id + GROUP BY i.id + ORDER BY i.name + """ + + cur.execute(query) + results = cur.fetchall() + cur.close() + + return results + + def create_institution(self, name: str, created_by: str) -> Dict[str, Any]: + with self.get_db_connection() as conn: + cur = conn.cursor(cursor_factory=RealDictCursor) + + query = """ + INSERT INTO lead_institutions (name, created_by) + VALUES (%s, %s) + RETURNING * + """ + + cur.execute(query, (name, created_by)) + result = cur.fetchone() + cur.close() + + return result + + def get_institution_by_id(self, institution_id: UUID) -> Optional[Dict[str, Any]]: + with self.get_db_connection() as conn: + cur = conn.cursor(cursor_factory=RealDictCursor) + + query = """ + SELECT * FROM lead_institutions WHERE id = %s + """ + + cur.execute(query, (str(institution_id),)) + result = cur.fetchone() + cur.close() + + return result + + def update_institution(self, institution_id: UUID, name: str) -> Dict[str, Any]: + with self.get_db_connection() as conn: + cur = conn.cursor(cursor_factory=RealDictCursor) + + query = """ + UPDATE lead_institutions + SET name = %s, updated_at = CURRENT_TIMESTAMP + WHERE id = %s + RETURNING * + """ + + cur.execute(query, (name, str(institution_id))) + result = cur.fetchone() + cur.close() + + return result + + # Contact Methods + def get_contacts_by_institution(self, institution_id: UUID) -> List[Dict[str, Any]]: + with self.get_db_connection() as conn: + cur = conn.cursor(cursor_factory=RealDictCursor) + + query = """ + SELECT + c.*, + i.name as institution_name + FROM lead_contacts c + JOIN lead_institutions i ON i.id = c.institution_id + WHERE c.institution_id = %s + ORDER BY c.last_name, c.first_name + """ + + cur.execute(query, (str(institution_id),)) + results = cur.fetchall() + cur.close() + + return results + + def create_contact(self, data: Dict[str, Any]) -> Dict[str, Any]: + with self.get_db_connection() as conn: + cur = conn.cursor(cursor_factory=RealDictCursor) + + query = """ + INSERT INTO lead_contacts + (institution_id, first_name, last_name, position, extra_fields) + VALUES (%s, %s, %s, %s, %s) + RETURNING * + """ + + cur.execute(query, ( + str(data['institution_id']), + data['first_name'], + data['last_name'], + data.get('position'), + psycopg2.extras.Json(data.get('extra_fields', {})) + )) + result = cur.fetchone() + cur.close() + + return result + + def get_contact_with_details(self, contact_id: UUID) -> Dict[str, Any]: + with self.get_db_connection() as conn: + cur = conn.cursor(cursor_factory=RealDictCursor) + + # Get contact base info + query = """ + SELECT + c.*, + i.name as institution_name + FROM lead_contacts c + JOIN lead_institutions i ON i.id = c.institution_id + WHERE c.id = %s + """ + + cur.execute(query, (str(contact_id),)) + contact = cur.fetchone() + + if contact: + # Get contact details (phones, emails) + details_query = """ + SELECT * FROM lead_contact_details + WHERE contact_id = %s + ORDER BY detail_type, is_primary DESC, created_at + """ + cur.execute(details_query, (str(contact_id),)) + contact['details'] = cur.fetchall() + + # Get notes + notes_query = """ + SELECT * FROM lead_notes + WHERE contact_id = %s AND is_current = true + ORDER BY created_at DESC + """ + cur.execute(notes_query, (str(contact_id),)) + contact['notes'] = cur.fetchall() + + cur.close() + + return contact + + def update_contact(self, contact_id: UUID, data: Dict[str, Any]) -> Dict[str, Any]: + with self.get_db_connection() as conn: + cur = conn.cursor(cursor_factory=RealDictCursor) + + query = """ + UPDATE lead_contacts + SET first_name = %s, last_name = %s, position = %s, + updated_at = CURRENT_TIMESTAMP + WHERE id = %s + RETURNING * + """ + + cur.execute(query, ( + data['first_name'], + data['last_name'], + data.get('position'), + str(contact_id) + )) + result = cur.fetchone() + cur.close() + + return result + + # Contact Details Methods + def add_contact_detail(self, contact_id: UUID, detail_type: str, + detail_value: str, detail_label: str = None) -> Dict[str, Any]: + with self.get_db_connection() as conn: + cur = conn.cursor(cursor_factory=RealDictCursor) + + query = """ + INSERT INTO lead_contact_details + (contact_id, detail_type, detail_value, detail_label) + VALUES (%s, %s, %s, %s) + RETURNING * + """ + + cur.execute(query, ( + str(contact_id), + detail_type, + detail_value, + detail_label + )) + result = cur.fetchone() + cur.close() + + return result + + def delete_contact_detail(self, detail_id: UUID) -> bool: + with self.get_db_connection() as conn: + cur = conn.cursor() + + query = "DELETE FROM lead_contact_details WHERE id = %s" + cur.execute(query, (str(detail_id),)) + + deleted = cur.rowcount > 0 + cur.close() + + return deleted + + # Notes Methods + def create_note(self, contact_id: UUID, note_text: str, created_by: str) -> Dict[str, Any]: + with self.get_db_connection() as conn: + cur = conn.cursor(cursor_factory=RealDictCursor) + + query = """ + INSERT INTO lead_notes + (contact_id, note_text, created_by) + VALUES (%s, %s, %s) + RETURNING * + """ + + cur.execute(query, ( + str(contact_id), + note_text, + created_by + )) + result = cur.fetchone() + cur.close() + + return result + + def update_note(self, note_id: UUID, note_text: str, updated_by: str) -> Dict[str, Any]: + with self.get_db_connection() as conn: + cur = conn.cursor(cursor_factory=RealDictCursor) + + # First, mark current version as not current + update_old = """ + UPDATE lead_notes + SET is_current = false + WHERE id = %s + """ + cur.execute(update_old, (str(note_id),)) + + # Create new version + create_new = """ + INSERT INTO lead_notes + (contact_id, note_text, created_by, parent_note_id, version) + SELECT contact_id, %s, %s, %s, version + 1 + FROM lead_notes + WHERE id = %s + RETURNING * + """ + + cur.execute(create_new, ( + note_text, + updated_by, + str(note_id), + str(note_id) + )) + result = cur.fetchone() + cur.close() + + return result + + def delete_note(self, note_id: UUID) -> bool: + with self.get_db_connection() as conn: + cur = conn.cursor() + + # Soft delete by marking as not current + query = """ + UPDATE lead_notes + SET is_current = false + WHERE id = %s + """ + cur.execute(query, (str(note_id),)) + + deleted = cur.rowcount > 0 + cur.close() + + return deleted \ No newline at end of file diff --git a/v2_adminpanel/leads/routes.py b/v2_adminpanel/leads/routes.py new file mode 100644 index 0000000..97cde9c --- /dev/null +++ b/v2_adminpanel/leads/routes.py @@ -0,0 +1,215 @@ +# Routes for Lead Management +from flask import render_template, request, jsonify, redirect, url_for, flash +from auth.decorators import login_required +from flask import session as flask_session +from . import leads_bp +from .services import LeadService +from .repositories import LeadRepository +from db import get_db_connection +from uuid import UUID +import traceback + +# Initialize service +lead_repository = LeadRepository(get_db_connection) +lead_service = LeadService(lead_repository) + +# HTML Routes +@leads_bp.route('/') +@login_required +def institutions(): + """List all institutions""" + try: + institutions = lead_service.list_institutions() + return render_template('leads/institutions.html', institutions=institutions) + except Exception as e: + flash(f'Fehler beim Laden der Institutionen: {str(e)}', 'error') + return render_template('leads/institutions.html', institutions=[]) + +@leads_bp.route('/institution/') +@login_required +def institution_detail(institution_id): + """Show institution with all contacts""" + try: + institution = lead_repository.get_institution_by_id(institution_id) + if not institution: + flash('Institution nicht gefunden', 'error') + return redirect(url_for('leads.institutions')) + + contacts = lead_service.list_contacts_by_institution(institution_id) + return render_template('leads/institution_detail.html', + institution=institution, + contacts=contacts) + except Exception as e: + flash(f'Fehler beim Laden der Institution: {str(e)}', 'error') + return redirect(url_for('leads.institutions')) + +@leads_bp.route('/contact/') +@login_required +def contact_detail(contact_id): + """Show contact details with notes""" + try: + contact = lead_service.get_contact_details(contact_id) + return render_template('leads/contact_detail.html', contact=contact) + except Exception as e: + flash(f'Fehler beim Laden des Kontakts: {str(e)}', 'error') + return redirect(url_for('leads.institutions')) + +# API Routes +@leads_bp.route('/api/institutions', methods=['POST']) +@login_required +def create_institution(): + """Create new institution""" + try: + data = request.get_json() + institution = lead_service.create_institution( + data['name'], + flask_session.get('username') + ) + return jsonify({'success': True, 'institution': institution}) + except ValueError as e: + return jsonify({'success': False, 'error': str(e)}), 400 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@leads_bp.route('/api/institutions/', methods=['PUT']) +@login_required +def update_institution(institution_id): + """Update institution""" + try: + data = request.get_json() + institution = lead_service.update_institution( + institution_id, + data['name'], + flask_session.get('username') + ) + return jsonify({'success': True, 'institution': institution}) + except ValueError as e: + return jsonify({'success': False, 'error': str(e)}), 400 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@leads_bp.route('/api/contacts', methods=['POST']) +@login_required +def create_contact(): + """Create new contact""" + try: + data = request.get_json() + contact = lead_service.create_contact(data, flask_session.get('username')) + return jsonify({'success': True, 'contact': contact}) + except ValueError as e: + return jsonify({'success': False, 'error': str(e)}), 400 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@leads_bp.route('/api/contacts/', methods=['PUT']) +@login_required +def update_contact(contact_id): + """Update contact""" + try: + data = request.get_json() + contact = lead_service.update_contact( + contact_id, + data, + flask_session.get('username') + ) + return jsonify({'success': True, 'contact': contact}) + except ValueError as e: + return jsonify({'success': False, 'error': str(e)}), 400 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@leads_bp.route('/api/contacts//phones', methods=['POST']) +@login_required +def add_phone(contact_id): + """Add phone to contact""" + try: + data = request.get_json() + detail = lead_service.add_phone( + contact_id, + data['phone_number'], + data.get('phone_type'), + flask_session.get('username') + ) + return jsonify({'success': True, 'detail': detail}) + except ValueError as e: + return jsonify({'success': False, 'error': str(e)}), 400 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@leads_bp.route('/api/contacts//emails', methods=['POST']) +@login_required +def add_email(contact_id): + """Add email to contact""" + try: + data = request.get_json() + detail = lead_service.add_email( + contact_id, + data['email'], + data.get('email_type'), + flask_session.get('username') + ) + return jsonify({'success': True, 'detail': detail}) + except ValueError as e: + return jsonify({'success': False, 'error': str(e)}), 400 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@leads_bp.route('/api/details/', methods=['DELETE']) +@login_required +def delete_detail(detail_id): + """Delete contact detail""" + try: + success = lead_service.delete_contact_detail( + detail_id, + flask_session.get('username') + ) + return jsonify({'success': success}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@leads_bp.route('/api/contacts//notes', methods=['POST']) +@login_required +def add_note(contact_id): + """Add note to contact""" + try: + data = request.get_json() + note = lead_service.add_note( + contact_id, + data['note_text'], + flask_session.get('username') + ) + return jsonify({'success': True, 'note': note}) + except ValueError as e: + return jsonify({'success': False, 'error': str(e)}), 400 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@leads_bp.route('/api/notes/', methods=['PUT']) +@login_required +def update_note(note_id): + """Update note""" + try: + data = request.get_json() + note = lead_service.update_note( + note_id, + data['note_text'], + flask_session.get('username') + ) + return jsonify({'success': True, 'note': note}) + except ValueError as e: + return jsonify({'success': False, 'error': str(e)}), 400 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@leads_bp.route('/api/notes/', methods=['DELETE']) +@login_required +def delete_note(note_id): + """Delete note""" + try: + success = lead_service.delete_note( + note_id, + flask_session.get('username') + ) + return jsonify({'success': success}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 \ No newline at end of file diff --git a/v2_adminpanel/leads/services.py b/v2_adminpanel/leads/services.py new file mode 100644 index 0000000..566b713 --- /dev/null +++ b/v2_adminpanel/leads/services.py @@ -0,0 +1,146 @@ +# Business Logic Service for Lead Management +from typing import List, Dict, Any, Optional +from uuid import UUID +from datetime import datetime +from .repositories import LeadRepository + +class LeadService: + def __init__(self, repository: LeadRepository): + self.repo = repository + + # Institution Services + def list_institutions(self) -> List[Dict[str, Any]]: + """Get all institutions with contact counts""" + return self.repo.get_institutions_with_counts() + + def create_institution(self, name: str, user: str) -> Dict[str, Any]: + """Create a new institution""" + # Validation + if not name or len(name.strip()) == 0: + raise ValueError("Institution name cannot be empty") + + # Create institution + institution = self.repo.create_institution(name.strip(), user) + + # Note: Audit logging removed as it requires different implementation + # Can be added later with proper audit system integration + + return institution + + def update_institution(self, institution_id: UUID, name: str, user: str) -> Dict[str, Any]: + """Update institution name""" + # Validation + if not name or len(name.strip()) == 0: + raise ValueError("Institution name cannot be empty") + + # Get current institution + current = self.repo.get_institution_by_id(institution_id) + if not current: + raise ValueError("Institution not found") + + # Update + institution = self.repo.update_institution(institution_id, name.strip()) + + return institution + + # Contact Services + def list_contacts_by_institution(self, institution_id: UUID) -> List[Dict[str, Any]]: + """Get all contacts for an institution""" + return self.repo.get_contacts_by_institution(institution_id) + + def create_contact(self, data: Dict[str, Any], user: str) -> Dict[str, Any]: + """Create a new contact""" + # Validation + if not data.get('first_name') or not data.get('last_name'): + raise ValueError("First and last name are required") + + if not data.get('institution_id'): + raise ValueError("Institution ID is required") + + # Create contact + contact = self.repo.create_contact(data) + + return contact + + def get_contact_details(self, contact_id: UUID) -> Dict[str, Any]: + """Get full contact information including details and notes""" + contact = self.repo.get_contact_with_details(contact_id) + if not contact: + raise ValueError("Contact not found") + + # Group details by type + contact['phones'] = [d for d in contact.get('details', []) if d['detail_type'] == 'phone'] + contact['emails'] = [d for d in contact.get('details', []) if d['detail_type'] == 'email'] + + return contact + + def update_contact(self, contact_id: UUID, data: Dict[str, Any], user: str) -> Dict[str, Any]: + """Update contact information""" + # Validation + if not data.get('first_name') or not data.get('last_name'): + raise ValueError("First and last name are required") + + # Update contact + contact = self.repo.update_contact(contact_id, data) + + return contact + + # Contact Details Services + def add_phone(self, contact_id: UUID, phone_number: str, + phone_type: str = None, user: str = None) -> Dict[str, Any]: + """Add phone number to contact""" + if not phone_number: + raise ValueError("Phone number is required") + + detail = self.repo.add_contact_detail( + contact_id, 'phone', phone_number, phone_type + ) + + return detail + + def add_email(self, contact_id: UUID, email: str, + email_type: str = None, user: str = None) -> Dict[str, Any]: + """Add email to contact""" + if not email: + raise ValueError("Email is required") + + # Basic email validation + if '@' not in email: + raise ValueError("Invalid email format") + + detail = self.repo.add_contact_detail( + contact_id, 'email', email, email_type + ) + + return detail + + def delete_contact_detail(self, detail_id: UUID, user: str) -> bool: + """Delete a contact detail (phone/email)""" + success = self.repo.delete_contact_detail(detail_id) + + return success + + # Note Services + def add_note(self, contact_id: UUID, note_text: str, user: str) -> Dict[str, Any]: + """Add a note to contact""" + if not note_text or len(note_text.strip()) == 0: + raise ValueError("Note text cannot be empty") + + note = self.repo.create_note(contact_id, note_text.strip(), user) + + return note + + def update_note(self, note_id: UUID, note_text: str, user: str) -> Dict[str, Any]: + """Update a note (creates new version)""" + if not note_text or len(note_text.strip()) == 0: + raise ValueError("Note text cannot be empty") + + note = self.repo.update_note(note_id, note_text.strip(), user) + + return note + + def delete_note(self, note_id: UUID, user: str) -> bool: + """Delete a note (soft delete)""" + success = self.repo.delete_note(note_id) + + return success \ No newline at end of file diff --git a/v2_adminpanel/leads/templates/leads/contact_detail.html b/v2_adminpanel/leads/templates/leads/contact_detail.html new file mode 100644 index 0000000..3316308 --- /dev/null +++ b/v2_adminpanel/leads/templates/leads/contact_detail.html @@ -0,0 +1,495 @@ +{% extends "base.html" %} + +{% block title %}{{ contact.first_name }} {{ contact.last_name }} - Kontakt-Details{% endblock %} + +{% block content %} +
+
+
+

+ {{ contact.first_name }} {{ contact.last_name }} +

+

+ {{ contact.position or 'Keine Position' }} + + + {{ contact.institution_name }} + +

+
+
+ + + Zurück + +
+
+ +
+ +
+ +
+
+
Telefonnummern
+ +
+
+ {% if contact.phones %} +
    + {% for phone in contact.phones %} +
  • +
    + {{ phone.detail_value }} + {% if phone.detail_label %} + {{ phone.detail_label }} + {% endif %} +
    + +
  • + {% endfor %} +
+ {% else %} +

Keine Telefonnummern hinterlegt.

+ {% endif %} +
+
+ + +
+
+
E-Mail-Adressen
+ +
+
+ {% if contact.emails %} +
    + {% for email in contact.emails %} +
  • +
    + {{ email.detail_value }} + {% if email.detail_label %} + {{ email.detail_label }} + {% endif %} +
    + +
  • + {% endfor %} +
+ {% else %} +

Keine E-Mail-Adressen hinterlegt.

+ {% endif %} +
+
+
+ + +
+
+
+
Notizen
+
+
+ +
+ + +
+ + +
+ {% for note in contact.notes %} +
+
+
+ + + {{ note.created_at.strftime('%d.%m.%Y %H:%M') }} + {% if note.created_by %} • {{ note.created_by }}{% endif %} + {% if note.version > 1 %} + v{{ note.version }} + {% endif %} + +
+ + +
+
+
+ {{ note.note_text|nl2br|safe }} +
+
+ + + +
+
+
+ {% endfor %} +
+ {% if not contact.notes %} +

Noch keine Notizen vorhanden.

+ {% endif %} +
+
+
+
+
+ + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/v2_adminpanel/leads/templates/leads/institution_detail.html b/v2_adminpanel/leads/templates/leads/institution_detail.html new file mode 100644 index 0000000..8dc6a4d --- /dev/null +++ b/v2_adminpanel/leads/templates/leads/institution_detail.html @@ -0,0 +1,159 @@ +{% extends "base.html" %} + +{% block title %}{{ institution.name }} - Lead-Details{% endblock %} + +{% block content %} +
+
+
+

+ {{ institution.name }} +

+ + Erstellt am {{ institution.created_at.strftime('%d.%m.%Y') }} + {% if institution.created_by %}von {{ institution.created_by }}{% endif %} + +
+
+ + + Zurück + +
+
+ + +
+
+
+ Kontakte + {{ contacts|length }} +
+
+
+
+ + + + + + + + + + + {% for contact in contacts %} + + + + + + + {% endfor %} + +
NamePositionErstellt amAktionen
+ + {{ contact.first_name }} {{ contact.last_name }} + + {{ contact.position or '-' }}{{ contact.created_at.strftime('%d.%m.%Y') }} + + Details + +
+ {% if not contacts %} +
+

Noch keine Kontakte für diese Institution.

+ +
+ {% endif %} +
+
+
+
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/v2_adminpanel/leads/templates/leads/institutions.html b/v2_adminpanel/leads/templates/leads/institutions.html new file mode 100644 index 0000000..4027981 --- /dev/null +++ b/v2_adminpanel/leads/templates/leads/institutions.html @@ -0,0 +1,178 @@ +{% extends "base.html" %} + +{% block title %}Lead-Verwaltung - Institutionen{% endblock %} + +{% block content %} +
+
+
+

+ Lead-Institutionen +

+
+
+ + + Zurück zu Kunden + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + + + + + + + + + + + {% for institution in institutions %} + + + + + + + + {% endfor %} + +
InstitutionAnzahl KontakteErstellt amErstellt vonAktionen
+ + {{ institution.name }} + + + {{ institution.contact_count }} + {{ institution.created_at.strftime('%d.%m.%Y') }}{{ institution.created_by or '-' }} + + Details + + +
+ {% if not institutions %} +
+

Noch keine Institutionen vorhanden.

+ +
+ {% endif %} +
+
+
+
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/v2_adminpanel/migrations/create_lead_tables.sql b/v2_adminpanel/migrations/create_lead_tables.sql new file mode 100644 index 0000000..aa6e3f2 --- /dev/null +++ b/v2_adminpanel/migrations/create_lead_tables.sql @@ -0,0 +1,107 @@ +-- Lead Management Tables Migration +-- This creates all necessary tables for the lead management system + +-- Enable UUID extension if not already enabled +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- 1. Lead Institutions (only name required) +CREATE TABLE IF NOT EXISTS lead_institutions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + -- Metadata for future extensions without schema changes + metadata JSONB DEFAULT '{}', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + UNIQUE(name) +); + +-- Index for fast lookups +CREATE INDEX IF NOT EXISTS idx_lead_institutions_name ON lead_institutions(name); + +-- 2. Lead Contacts +CREATE TABLE IF NOT EXISTS lead_contacts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + institution_id UUID NOT NULL REFERENCES lead_institutions(id) ON DELETE CASCADE, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + position VARCHAR(255), + -- Extra fields for future extensions + extra_fields JSONB DEFAULT '{}', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Indexes for performance +CREATE INDEX IF NOT EXISTS idx_lead_contacts_institution ON lead_contacts(institution_id); +CREATE INDEX IF NOT EXISTS idx_lead_contacts_name ON lead_contacts(last_name, first_name); + +-- 3. Flexible Contact Details (phones, emails, etc.) +CREATE TABLE IF NOT EXISTS lead_contact_details ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + contact_id UUID NOT NULL REFERENCES lead_contacts(id) ON DELETE CASCADE, + detail_type VARCHAR(50) NOT NULL, -- 'phone', 'email', 'social', etc. + detail_value VARCHAR(255) NOT NULL, + detail_label VARCHAR(50), -- 'Mobil', 'Geschäftlich', 'Privat', etc. + is_primary BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Indexes for fast queries +CREATE INDEX IF NOT EXISTS idx_lead_details_contact_type ON lead_contact_details(contact_id, detail_type); +CREATE INDEX IF NOT EXISTS idx_lead_details_value ON lead_contact_details(detail_value); + +-- 4. Versioned Notes with History +CREATE TABLE IF NOT EXISTS lead_notes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + contact_id UUID NOT NULL REFERENCES lead_contacts(id) ON DELETE CASCADE, + note_text TEXT NOT NULL, + version INTEGER DEFAULT 1, + is_current BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), + parent_note_id UUID REFERENCES lead_notes(id), + CHECK (note_text <> '') +); + +-- Indexes for note queries +CREATE INDEX IF NOT EXISTS idx_lead_notes_contact_current ON lead_notes(contact_id, is_current); +CREATE INDEX IF NOT EXISTS idx_lead_notes_created ON lead_notes(created_at DESC); + +-- Full text search preparation +CREATE INDEX IF NOT EXISTS idx_lead_contacts_search ON lead_contacts +USING gin(to_tsvector('german', + COALESCE(first_name, '') || ' ' || + COALESCE(last_name, '') || ' ' || + COALESCE(position, '') +)); + +-- Update timestamp trigger function +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- Apply update trigger to tables with updated_at +CREATE TRIGGER update_lead_institutions_updated_at + BEFORE UPDATE ON lead_institutions + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_lead_contacts_updated_at + BEFORE UPDATE ON lead_contacts + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- Add comments for documentation +COMMENT ON TABLE lead_institutions IS 'Organizations/Companies for lead management'; +COMMENT ON TABLE lead_contacts IS 'Contact persons within institutions'; +COMMENT ON TABLE lead_contact_details IS 'Flexible contact details (phone, email, etc.)'; +COMMENT ON TABLE lead_notes IS 'Versioned notes with full history'; + +COMMENT ON COLUMN lead_contact_details.detail_type IS 'Type of detail: phone, email, social, etc.'; +COMMENT ON COLUMN lead_notes.is_current IS 'Only current version is shown, old versions kept for history'; +COMMENT ON COLUMN lead_notes.parent_note_id IS 'References original note for version tracking'; \ No newline at end of file diff --git a/v2_adminpanel/templates/customers_licenses.html b/v2_adminpanel/templates/customers_licenses.html index ae2f1b1..a88f7d4 100644 --- a/v2_adminpanel/templates/customers_licenses.html +++ b/v2_adminpanel/templates/customers_licenses.html @@ -8,21 +8,26 @@