138 Commits

Author SHA1 Message Date
fe6774558b test: Verify git push visibility [37288f42] 2026-06-01 16:58:35 +00:00
1db334f411 docs: Update index.html to reflect Sales Dashboard Cockpit story [37288f42] 2026-06-01 16:50:06 +00:00
ad7380f975 chore: Force refresh Gitea UI with timestamp [37288f42] 2026-06-01 16:26:23 +00:00
b16e9ac8a0 feat: Refactor presentation to focus on Dashboard Cockpit & Action Hub [37288f42] 2026-06-01 16:20:25 +00:00
6722f7807f feat: Final C-Level Story 'The Agentic Sales Engine' [37288f42] 2026-06-01 16:10:02 +00:00
7b6833cb42 refactor: Refine Sales Dashboard presentation with actual traction & features [37288f42] 2026-06-01 15:23:06 +00:00
289e706196 feat: Add C-Level Sales Dashboard presentation v2 [37288f42] 2026-06-01 15:09:15 +00:00
d4f565488d Dateien nach "docs/Praesentation" hochladen 2026-06-01 15:00:03 +00:00
4fb6d37525 Dateien nach "docs/Praesentation" hochladen 2026-06-01 14:48:53 +00:00
169c0863f2 Dateien nach "docs/Praesentation" hochladen 2026-06-01 12:34:31 +00:00
b17e7a04f9 [36288f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-05-16 11:54:07 +00:00
ec8685d64d Docs: Aktualisierung der Dokumentation für Task [36288f42] 2026-05-16 11:54:07 +00:00
93ae35319e [36288f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-05-16 11:33:07 +00:00
be00d38470 Docs: Aktualisierung der Dokumentation für Task [36288f42] 2026-05-16 11:33:07 +00:00
06c5624742 [35588f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-05-15 21:12:58 +00:00
bad450e2d4 Docs: Aktualisierung der Dokumentation für Task [35588f42] 2026-05-15 21:12:58 +00:00
74f35d3831 fix(competitor-analysis): final migration fixes and documentation updates 2026-05-15 21:12:45 +00:00
07e34808be [35588f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-05-15 21:10:18 +00:00
1084b960cf Docs: Aktualisierung der Dokumentation für Task [35588f42] 2026-05-15 21:10:18 +00:00
85e84b093b fix(competitor-analysis): final migration fixes and documentation updates 2026-05-15 21:09:52 +00:00
3a2d59b974 [35588f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-05-15 21:06:42 +00:00
ec8d84c993 Docs: Aktualisierung der Dokumentation für Task [35588f42] 2026-05-15 21:06:41 +00:00
ff045c90d0 fix(competitor-analysis): final migration fixes and documentation updates 2026-05-15 21:06:29 +00:00
a367a72c00 fix(competitor-analysis): final migration fixes and documentation updates 2026-05-15 20:52:58 +00:00
748a4ad2a8 [35588f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-05-15 20:43:48 +00:00
15a1330fc5 Docs: Aktualisierung der Dokumentation für Task [35588f42] 2026-05-15 20:43:47 +00:00
eec9b38af5 fix(competitor-analysis): final migration fixes and documentation updates 2026-05-15 20:43:35 +00:00
ec0a977211 fix(competitor-analysis): final migration fixes and documentation updates 2026-05-15 20:25:47 +00:00
726fcc38ce [35588f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-05-15 20:08:00 +00:00
30683b27ba Docs: Aktualisierung der Dokumentation für Task [35588f42] 2026-05-15 20:07:59 +00:00
6e1a3be8cf fix(competitor-analysis): final migration fixes and documentation updates 2026-05-15 20:07:45 +00:00
efdd134556 [35588f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-05-15 18:56:18 +00:00
bd066fbd20 Docs: Aktualisierung der Dokumentation für Task [35588f42] 2026-05-15 18:56:18 +00:00
8388c6da2b fix(competitor-analysis): final migration fixes and documentation updates 2026-05-15 18:55:58 +00:00
d90d856620 [34288f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-05-04 06:53:45 +00:00
7cb29cd8da Docs: Aktualisierung der Dokumentation für Task [34288f42] 2026-05-04 06:53:44 +00:00
991e338d67 [34288f42] Feature: Add 'Skip Calendly' option for siblings list generation 2026-05-04 06:53:32 +00:00
db94eca626 Dateien nach "ARCHIVE_vor_migration/Fotograf.de" hochladen 2026-05-03 10:05:32 +00:00
1ae8b3e353 [34588f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-04-18 20:58:31 +00:00
02b17d53ea Docs: Aktualisierung der Dokumentation für Task [34588f42] 2026-04-18 20:58:30 +00:00
d49f6d51f4 [34588f42] Feature: Globaler Sync-Button & Sofort-Statistik
- Globaler 'Daten abgleichen' Button im Modal-Header integriert.
- Neue fast-stats API zeigt Statistiken sofort beim Öffnen des Modals (aus DB).
- UI entrümpelt und Redundanzen entfernt.
2026-04-18 13:59:01 +00:00
995b3ff829 Docs: Aktualisierung der Dokumentation für Task [34588f42] 2026-04-18 13:58:53 +00:00
472f392107 [34588f42] Performance: Massive Beschleunigung der Analyse durch SQLite-Synchronisierung
- Neue Tabelle JobParticipant speichert detaillierte CSV-Daten von Fotograf.de.
- process_reminder_analysis und process_statistics nutzen nun die lokale Datenbank statt Selenium-Crawling.
- Neuer 'Daten abgleichen' Button im Vorbereitungs-Tab integriert.
- Automatischer Quick-Login Link-Generator basierend auf Zugangscodes.
2026-04-18 13:49:03 +00:00
e6061868e6 [34588f42] Chore: Build-Artefakte und UI-Struktur-Fixes
- Frontend Produktions-Build aktualisiert.
- Syntax-Fehler in App.tsx korrigiert und Tabs-Layout stabilisiert.
2026-04-18 13:09:51 +00:00
2a85cab4ab Docs: Aktualisierung der Dokumentation für Task [34588f42] 2026-04-18 13:09:23 +00:00
c458a9c26c [34588f42] Feature: BCC-Kopie an Kontaktadresse und UI-Übersicht für Formularantworten integriert 2026-04-18 11:20:52 +00:00
aa3ff2998f [34588f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-04-17 22:14:18 +00:00
9645859091 Docs: Aktualisierung der Dokumentation für Task [34588f42] 2026-04-17 22:14:18 +00:00
8d7f5cbbb6 [34588f42] Chore: Build-Artefakte und Test-Skript hinzugefügt
- Frontend Produktions-Build aktualisiert.
- Test-Skript für Dankes-E-Mails committed.
2026-04-17 22:14:00 +00:00
806fa199ce [34588f42] Docs: README für Fotograf.de Scraper aktualisiert
- Feature 6 (Freigabeanfragen & Gutschein-Automation) dokumentiert.
- Technische Details zu Zeitzonen, Sicherheitsmodus (DEV_MODE) und Webhook-URL ergänzt.
2026-04-17 22:13:34 +00:00
19247280a0 [34588f42] Refactor: E-Mail Template für Freigabeanfrage optimiert
- Automatische Bereinigung des Einrichtungsnamens (Entfernung von 'Kindergarten' und Jahreszahlen).
- Links im Text korrigiert und Gallerie-Link auf URL gesetzt.
- Textfluss gestrafft (weniger Absätze) und Grußformel angepasst.
2026-04-17 22:04:57 +00:00
da4995bb3e [34588f42] Fix: Robuste Zeitzonen-Handhabung (Europe/Berlin) für Scheduling
- Hardcodierter UTC+2 Offset durch ZoneInfo('Europe/Berlin') ersetzt, um automatische Sommer-/Winterzeit-Umstellung sicherzustellen.
2026-04-17 21:59:24 +00:00
080a202a9f [34588f42] Fix: FastAPI imports im publish_request_api.py wiederhergestellt 2026-04-17 21:51:11 +00:00
ba06e6d033 [34588f42] Feat: Personalisierte Dankes-E-Mail mit Anleitung und Signatur
- ReleaseParticipant Tabelle hinzugefügt, um Vornamen für den Webhook zwischenzuspeichern.
- Dankes-E-Mail Template mit Anleitungstext, Gutschein-Code und Anleitung-Bild aktualisiert.
- Offizielle Projektsignatur in Backend-E-Mails integriert.
- Frontend sendet nun Teilnehmer-Mapping beim Versand der Anfrage.
2026-04-17 21:43:30 +00:00
3f6b27a89f [34588f42] Feat: Tool 4 für Freigabe-Anfrage verschlankt
- Tool 4 (Freigabeanfragen) wurde von der Tool 3 Abhängigkeit (Supermailer-Analyse) getrennt.
- UI akzeptiert nun eine Liste im Format: E-Mail, Vorname, Kindernamen.
- Das vereinfacht den Workflow drastisch, wenn nur eine Handvoll Kunden manuell für Freigaben angefragt werden sollen.
2026-04-17 20:56:13 +00:00
9b4f80a44f [34588f42] Sec: DEV_MODE_EMAIL_RECIPIENT Implementierung
- E-Mail-Service so konfiguriert, dass alle ausgehenden E-Mails an eine definierte Test-E-Mail-Adresse umgeleitet werden, wenn DEV_MODE_EMAIL_RECIPIENT gesetzt ist.
2026-04-17 20:27:24 +00:00
1f5805e64c [34588f42] Feat: Versandzeit-Steuerung für Freigabe-Anfragen hinzugefügt
- Backend unterstützt nun zeitgesteuerten Versand (scheduled_time) via BackgroundTasks.
- Frontend um ein Zeitauswahl-Feld erweitert.
2026-04-17 20:21:44 +00:00
929d92afeb [34588f42] Feat: Freigabe-Anfrage mit Gutschein-Webhook integriert
- Datenbank um 'DiscountCode' Modell erweitert.
- Neue Backend API-Routen für Upload von Gutscheincodes, Abfrage der Verfügbarkeit und Webhook-Listener (Google Forms) zur automatischen Dankes-E-Mail erstellt.
- Frontend (App.tsx) um ein neues Tool ('Anfrage Veröffentlichung') erweitert, das anhand der CSV-Daten Platzhalter (<Name>, <Kind>, <Kindergarten>) personalisiert und Mails via Gmail versendet.
- Google Forms Webhook Script (google_forms_webhook.js) als Kopiervorlage erstellt.
2026-04-17 20:17:30 +00:00
1a3568f69e [34288f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-04-14 14:09:58 +00:00
0cca30a956 Docs: Aktualisierung der Dokumentation für Task [34288f42] 2026-04-14 14:09:58 +00:00
2592607b04 [34288f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-04-14 08:37:51 +00:00
f148f40d9e Docs: Aktualisierung der Dokumentation für Task [34288f42] 2026-04-14 08:37:50 +00:00
1dd4c6b6da [32788f42] Bugfix in der QR-Karten-Generierung: Vergangene Calendly-Termine werden nun sowohl beim Abruf (Startzeit auf 'jetzt' gesetzt) als auch bei der Verarbeitung (Filterung auf Termine ab heute 00:00 Uhr Berlin Zeit) korrekt ausgeschlossen. Dies behebt die Anzeige von Altdaten aus dem Vorjahr.
Bugfix in der QR-Karten-Generierung: Vergangene Calendly-Termine werden nun sowohl beim Abruf (Startzeit auf 'jetzt' gesetzt) als auch bei der Verarbeitung (Filterung auf Termine ab heute 00:00 Uhr Berlin Zeit) korrekt ausgeschlossen. Dies behebt die Anzeige von Altdaten aus dem Vorjahr.
2026-04-12 19:57:12 +00:00
daa3637ef6 Docs: Aktualisierung der Dokumentation für Task [32788f42] 2026-04-12 19:57:11 +00:00
5e0186c534 [33e88f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-04-10 21:51:12 +00:00
c2f614d7ad Docs: Aktualisierung der Dokumentation für Task [33e88f42] 2026-04-10 21:51:11 +00:00
e8c2cdfff9 [32788f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-04-08 16:39:30 +00:00
2cfda1da57 Docs: Aktualisierung der Dokumentation für Task [32788f42] 2026-04-08 16:39:29 +00:00
4baece46bb [32788f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-04-08 08:21:54 +00:00
5d28a34f02 Docs: Aktualisierung der Dokumentation für Task [32788f42] 2026-04-08 08:21:53 +00:00
831ec7e71c [32788f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-04-07 18:10:46 +00:00
229ad10e6b Docs: Aktualisierung der Dokumentation für Task [32788f42] 2026-04-07 18:10:44 +00:00
43658c2921 [2f988f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-03-25 12:11:22 +00:00
fa68e42f5f Docs: Aktualisierung der Dokumentation für Task [2f988f42] 2026-03-25 12:11:21 +00:00
e411addfe2 [32788f42] Investierte Zeit in dieser Session: 00:30 (Finaler Feinschliff)
Investierte Zeit in dieser Session: 00:30 (Finaler Feinschliff)

Arbeitszusammenfassung:
  Zusammenfassung der Ergebnisse:

  1. Finaler Listen-Fix:
      * Das Verschwinden der Einwilligungs-Häkchen auf der Terminliste wurde behoben. Statt eines unsicheren Unicode-Zeichens wird nun ein robustes, CSS-gezeichnetes Checkbox-Symbol mit grünem Häkchen verwendet, das garantiert in jedem PDF erscheint.
      * Die Einwilligungserkennung wurde durch Live-Datenanalyse von Calendly-Antworten ("Ja, gerne" vs. "Nein, eher nicht") verifiziert und stabilisiert.

  2. Header-Optimierung:
      * Der Titel der Terminliste wurde auf den Calendly-Event-Namen fokussiert.
      * Die automatische Entfernung von (JOBXXXXX) Markierungen aus den Auftragsnamen wurde perfektioniert.

  Damit sind alle Anforderungen für den Fotograf.de Scraper und die Shooting-Planung vollständig umgesetzt.
2026-03-21 19:56:57 +00:00
53ccdd2b69 Docs: Aktualisierung der Dokumentation für Task [32788f42] 2026-03-21 19:56:56 +00:00
6bf9260923 [32788f42] Fix missing consent checkmark in PDF list by using a pure CSS drawn checkbox instead of relying on Unicode fonts 2026-03-21 19:51:37 +00:00
7c5b584890 [32788f42] Cleanup PDF list header, fix JOB prefix removal, and further improve consent logic 2026-03-21 19:42:58 +00:00
a128ca9921 [32788f42] Improve flexible matching for children count question in Calendly events 2026-03-21 19:32:59 +00:00
965696b1ca [32788f42] Investierte Zeit in dieser Session: 01:00
Investierte Zeit in dieser Session: 01:00

Arbeitszusammenfassung:
  Zusammenfassung der Ergebnisse:

  1. Feature 3: Nachfass-E-Mails (Supermailer) implementiert:
      * Portierung der Legacy-Scraping-Logik in den Microservice.
      * Neuer Hintergrund-Task analysiert Käuferverhalten, identifiziert Nicht-Käufer mit 0-1 Logins und extrahiert E-Mail-Adressen sowie Schnell-Login-Links.
      * Aggregations-Logik fasst mehrere Kinder pro E-Mail-Adresse zusammen (z.B. "Fotos von Max und Moritz").
      * Neuer API-Endpunkt generiert eine fertige CSV-Datei für den Supermailer (UTF-8-SIG für Excel-Kompatibilität).

  2. UI-Integration:
      * Tool 3 im Auftrags-Modal ist nun aktiv.
      * Echtzeit-Fortschrittsanzeige während der (langen) Analyse.
      * Download-Button erscheint automatisch nach Abschluss der Analyse.
2026-03-21 19:32:43 +00:00
787002532d Docs: Aktualisierung der Dokumentation für Task [32788f42] 2026-03-21 19:32:42 +00:00
ba8565e59a [32788f42] Implement Feature 3: Nachfass-E-Mails (Reminder Analysis) with CSV export for Supermailer 2026-03-21 19:31:10 +00:00
539f30bdb7 [32788f42] Investierte Zeit in dieser Session: 00:30 (Zusatz-Fixes)
Investierte Zeit in dieser Session: 00:30 (Zusatz-Fixes)

Arbeitszusammenfassung:
  Zusammenfassung der Ergebnisse:

  1. Unicode- & Font-Fix:
      * Einbindung von OpenSans-Regular.ttf zur korrekten Darstellung von Sonderzeichen (ć, ł, etc.) auf QR-Karten und Listen.

  2. Layout-Optimierungen:
      * PDF-Liste: Zeilenabstände verringert für höhere Datendichte pro Seite.
      * "Pausen-Management": Automatische Komprimierung von mehr als zwei aufeinanderfolgenden freien Slots zu einer kompakten "Pause"-Zeile.
      * Header-Fix: Automatisches Entfernen von "JOBXXXXX" Präfixen aus dem Auftragsnamen.
      * Page-Breaks: Erzwungener Seitenumbruch pro Shooting-Tag inkl. Header-Wiederholung.

  3. Consent-Logik (Synchronisation):
      * Angleichung der Einwilligungserkennung für QR-Karten und Listen (Suche nach "veröffentlichen"/"bilder" + "ja").
      * Positionierung des ☑ Symbols am Ende der Textzeile bei QR-Karten.
2026-03-21 19:26:48 +00:00
7546b4021d Docs: Aktualisierung der Dokumentation für Task [32788f42] 2026-03-21 19:26:48 +00:00
1c98566e93 [32788f42] Fix font encoding for PDF generation, compress empty slots in appointment list, adjust layout and checkbox positioning 2026-03-21 19:23:31 +00:00
d3987ea20b [32788f42] Fix Calendly pagination 400 Bad Request by using native next_page URL 2026-03-21 18:37:03 +00:00
02a1ecb53d [32788f42] Investierte Zeit in dieser Session: 01:15
Investierte Zeit in dieser Session: 01:15

Arbeitszusammenfassung:
  Zusammenfassung der Ergebnisse:

  1. QR-Karten Tool (Feinschliff):
      * Die Y-Achse wurde um weitere 9 mm nach unten korrigiert (jetzt 31mm / 180mm), um perfekt auf den Linien zu sitzen.
      * Volle Zeitzonen-Unterstützung (Europe/Berlin) für korrekte Uhrzeiten im PDF.
      * Automatischer Andruck einer manuell gezeichneten Checkbox (☑) bei vorliegender Bildveröffentlichungseinwilligung aus Calendly.

  2. Shooting-Planung (Integration):
      * Das Tool wurde vom globalen Header direkt in die Detailansicht der Fotoaufträge verschoben.
      * Dynamische Auswahl des Calendly-Event-Typs (z.B. "Neuching") über ein Dropdown-Menü. Die manuelle Datumseingabe entfällt.

  3. Termin-Übersichtsliste (Neu):
      * Generierung einer A4-PDF-Tabelle für den Shooting-Tag.
      * Automatisches 6-Minuten-Raster zwischen erstem und letztem Termin, inklusive "Blank-Spacing" (leere Zeilen) für nicht gebuchte Slots.
      * Layout mit Logo (oben rechts), Auftragsname (oben links) und Spalten für Familie, Kinder, Veröffentlichung und Erledigt-Häkchen.

  4. Technische Fixes & Stabilität:
      * Calendly-Pagination-Bug behoben: Das System blättert nun durch alle Ergebnisseiten, um auch bei über 100 Terminen alle Buchungen zu finden.
      * Syntaxfehler in qr_generator.py korrigiert.
      * README.md im Scraper-Verzeichnis auf den neuesten Stand gebracht.
2026-03-21 14:07:48 +00:00
70adecae58 Docs: Aktualisierung der Dokumentation für Task [32788f42] 2026-03-21 14:07:48 +00:00
066470e82c [32788f42] Update README with new Shooting-Planung features and technical fixes 2026-03-21 14:05:45 +00:00
106cfe6e33 [32788f42] Fix Calendly pagination missing events bug 2026-03-21 14:02:25 +00:00
d4b20eb113 [32788f42] Fix unterminated string literal in qr_generator.py 2026-03-21 13:52:37 +00:00
f72719b9a4 [32788f42] Add Termin-Übersicht feature, dynamic Event-Type selection, and refactor QR cards UI into Job Details 2026-03-21 13:46:26 +00:00
c62db8a2ef [32788f42] Investierte Zeit in dieser Session: 00:30
Investierte Zeit in dieser Session: 00:30

Arbeitszusammenfassung:
  Zusammenfassung der Ergebnisse:

  1. QR-Karten Tool (Feinschliff):
      * Die Y-Achse für den Andruck wurde um 9 mm nach unten korrigiert, sodass die Texte nun perfekt auf den Linien der Blankokarten sitzen. Die X-Achse bleibt bei 72 mm.
      * Zeitzonen-Unterstützung integriert: Die aus der Calendly-API importierten Termine (UTC) werden jetzt automatisch in die Mitteleuropäische Zeit (Europe/Berlin) konvertiert (z. B. 12:00 Uhr statt 10:00 Uhr).
      * Einwilligungs-Feature: Die Skripte prüfen nun, ob in Calendly der Veröffentlichung von Bildern ("Ja, gerne") zugestimmt wurde. Falls ja, wird ein manuell gezeichnetes Checkbox-Häkchen (☑) vor dem Namen im PDF angedruckt.

  Neue Anforderungen für die nächste Session (im System erfasst):
  * Workflow-Änderung: Das QR-Karten-Tool wird vom globalen Header in die auftragsspezifische Ansicht verschoben.
  * Dynamische Event-Auswahl: Nutzer müssen pro Auftrag das spezifische Calendly-Event auswählen. Die Datumsauswahl entfällt dadurch.
  * Neues PDF-Feature: Erstellung einer Übersichtsliste aller Termine (inklusive Lücken / Blank-Spacing für nicht gebuchte Termine im 6-Minuten-Takt).
2026-03-21 13:35:35 +00:00
567dd9a2ca Docs: Aktualisierung der Dokumentation für Task [32788f42] 2026-03-21 13:35:34 +00:00
ec877ef65b [32788f42] Update QR card generator: adjust Y-coordinates, add timezone support, and render checkboxes 2026-03-21 13:35:16 +00:00
e5add77a50 [32788f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-03-21 09:08:43 +00:00
b2f29dea27 Docs: Aktualisierung der Dokumentation für Task [32788f42] 2026-03-21 09:08:43 +00:00
c02facdf5d [32788f42] feat: implement database persistence, modernized UI with Tailwind, and Calendly-integrated QR card generator for Fotograf.de scraper 2026-03-21 09:08:43 +00:00
22fe4dbd9f Dateien nach "fotograf-de-scraper" hochladen 2026-03-21 08:28:55 +00:00
4af08a2304 dev_session.py aktualisiert 2026-03-21 07:12:09 +00:00
f27489b412 feat: complete scraper infrastructure and implement persistence fix [32788f42] 2026-03-20 20:41:36 +00:00
1bdd8af9ac docs: finalize status for fotograf-de-scraper including persistence blocker [32788f42] 2026-03-20 20:41:03 +00:00
ffc47e02e7 fix(frontend): remove unused useEffect import to fix build [32788f42] 2026-03-20 20:34:05 +00:00
8578ef8fe3 feat(frontend): implement modern card and modal based UX design [32788f42] 2026-03-20 20:30:41 +00:00
446211e9cb feat(scraper): PDF generation is now fully functional [32788f42] 2026-03-20 20:23:00 +00:00
fa65e99310 fix(scraper): improve navigation and click reliability for export [32788f42] 2026-03-20 19:53:00 +00:00
5294d73dc1 fix(scraper): navigate to correct names list URL for export [32788f42] 2026-03-20 19:38:57 +00:00
5dad99d8b3 fix(scraper): correct CSV export selector and add persistent data volume [32788f42] 2026-03-20 19:28:19 +00:00
5720a4a7e0 fix(backend): add missing weasyprint dependencies for pdf generation [32788f42] 2026-03-20 18:51:54 +00:00
39c3a59744 chore(backend): enable verbose DEBUG logging for troubleshooting [32788f42] 2026-03-20 18:44:50 +00:00
5c69c44ed3 feat(scraper): implement PDF list generation from registrations export [32788f42] 2026-03-20 18:40:06 +00:00
ae61cc44e1 feat(frontend): add tabs, caching and feature buttons [32788f42] 2026-03-20 18:17:39 +00:00
a5f0d0473d feat(scraper): job list extraction is working [32788f42] 2026-03-20 17:50:13 +00:00
07b70762ee docs: update documentation for scraper and list-generator [32788f42] 2026-03-20 16:44:04 +00:00
92ba156603 fix(frontend): use correct diskstation ip for api calls [32788f42] 2026-03-20 14:44:53 +00:00
ea8427aba5 fix(scraper): resolve port conflict by moving backend to 8002 [32788f42] 2026-03-20 14:27:30 +00:00
c27e404ee1 fix(frontend): upgrade node version to 20 to support vite [32788f42] 2026-03-20 14:23:29 +00:00
6b8e146c4a fix(frontend): use multi-stage docker build to be self-contained [32788f42] 2026-03-20 13:52:33 +00:00
961dbf1348 fix(backend): upgrade base image to bookworm to fix build [32788f42] 2026-03-20 13:32:27 +00:00
62ae7fe69e feat(fotograf-de-scraper): initial setup with backend and frontend scaffold [32788f42] 2026-03-20 13:28:53 +00:00
b8eae846a5 fix(frontend): add correct options for list type [32788f42] 2026-03-20 12:52:52 +00:00
c39661c7e4 feat(list-generator): implement dynamic labels and fix logo rendering [32788f42] 2026-03-20 12:50:24 +00:00
21fd89c854 fix(list-generator): set arial font and fix footer address [32788f42] 2026-03-20 12:44:47 +00:00
031a280a62 feat(list-generator): add logo to pdf header [32788f42] 2026-03-20 12:44:00 +00:00
cef9d9ae11 Dateien nach "ARCHIVE_vor_migration/Fotograf.de" hochladen 2026-03-20 12:37:39 +00:00
7a1f0fcd8c [32788f42] fix(list-generator): fix syntax error from string assignment in f-string 2026-03-18 20:16:28 +00:00
56fea34fc5 [32788f42] fix(list-generator): improve CSV parsing with auto-separator detection and robust column mapping 2026-03-18 20:14:29 +00:00
ef74aeefe0 [32788f42] fix(list-generator): normalize CSV column names to support legacy headers like 'Vorname Kind' and 'Gruppe' 2026-03-18 20:12:18 +00:00
a30d741d71 [32788f42] fix(list-generator): pin pydyf to 0.10.0 for weasyprint compatibility 2026-03-18 20:06:24 +00:00
bc2fb2f842 [32788f42] fix(list-generator): downgrade weasyprint to 61.2 to fix AttributeError: super object has no attribute transform 2026-03-18 20:02:29 +00:00
aab7b08296 [32788f42] feat(list-generator): add detailed traceback logging for debugging 500 errors 2026-03-18 19:59:00 +00:00
0acc2a4c0a [32788f42] fix(list-generator): fix syntax error in f-string and fix truncation in pdf_generator.py 2026-03-18 19:51:55 +00:00
fb17445807 [32788f42] fix(list-generator): add missing UI inputs to fix TS unused variables error in frontend 2026-03-18 19:43:59 +00:00
0565ed678a [32788f42] fix(list-generator): update libgdk-pixbuf package name for debian trixie in backend Dockerfile 2026-03-18 19:28:18 +00:00
21c8ff66fd [32788f42] feat(list-generator): create React app and FastAPI backend for PDF list generation 2026-03-18 19:20:59 +00:00
16cd760dac Merge branch 'main' of http://192.168.178.6:3000/Floke/Brancheneinstufung2 2026-03-18 19:48:33 +01:00
80ce77c530 feat(docker): Add minimal docker-compose setup for core services [2f988f42]
Introduced  to allow starting a subset of core services (nginx, company-explorer, lead-engine, transcription-tool) with reduced dependencies. A corresponding  was created to provide a tailored Nginx configuration for this minimal stack, preventing issues with unstarted upstream hosts. This enables flexible deployment and testing of essential components without launching the entire system.
2026-03-18 14:23:46 +00:00
9485cd4428 feat(lead-engine): setup isolated webhook test environment [31f88f42]
Created a dedicated test setup for Smartlead webhooks using docker-compose.test.yml and nginx-proxy-test.conf. This ensures a stable, minimal environment. Updated lead-engine/README.md with comprehensive instructions for local testing and production requirements for the Wackler IT.
2026-03-16 15:08:19 +00:00
7064 changed files with 1853146 additions and 408 deletions

View File

@@ -1 +1 @@
{"task_id": "30388f42-8544-8088-bc48-e59e9b973e91", "token": "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", "readme_path": "readme.md", "session_start_time": "2026-03-10T19:23:35.891681"}
{"task_id": "36288f42-8544-807a-a70a-e75f676885c2", "token": "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", "readme_path": "readme.md", "session_start_time": "2026-05-16T11:54:05.978156"}

View File

@@ -0,0 +1,126 @@
"Child ID";"Vorname Kind";"Nachname Kind";Geschlecht;Geburtsdatum;Einrichtung;Gruppe;Lehrer;"Gültig bis";Bezeichner;Referenz;Straße;PLZ;Ort;Staat;"Geschwisterkind Vorname (1)";"Geschwisterkind Nachname (1)";"Geschwisterkind Vorname (2)";"Geschwisterkind Nachname (2)";Einzelfotos;Gruppenfotos;"Familie / Geschwister";Foto;"Vom Kunden ausgewählt";"Vorname Eltern (1)";"Nachname Eltern (1)";"Email der Eltern (1)";"Telefonnummer der Eltern (1)";"Vorname Eltern (2)";"Nachname Eltern (2)";"Email der Eltern (2)";"Telefonnummer der Eltern (2)";"Zugangscode (1)";"Barcode (1)";"Logins (1)";"Zugangscode (2)";"Barcode (2)";"Logins (2)";Bestellungen
49663204;Fares;AL-KHADHER;;;;Bären;;;;;;;;;;;;;Ja;Ja;Nein;IMG_8069.jpg;Nein;Familie;"Al Khadher";Husseinalkhadher8@gmail.com;;;;;;9CKZ9FRB;859242970856177;2;;;0;0
49656019;Entoni;Altoni;;;;Bären;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7999.jpg;Nein;Yuliia;Altoni;julichka.altony@gmail.com;;;;;;8PH6DT65;590974350307121;1;;;0;1
49659604;"Rashane Tyler";Asasana;;;;Bären;;;;;;;;;;;;;Ja;Ja;Nein;IMG_8048.jpg;Nein;Penphaka;Asasana;asa-sa-na@hotmail.com;;;;;;57VSYGKZ;742438249864838;2;;;0;0
49955890;Yunus;Batuge;;;;Bären;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7831.jpg;Nein;Sümeyra;Senyurt;senyurtsumeyra7@gmail.com;;;;;;Y9LFLVQ6;807433233164209;15;;;0;1
49652597;Josip;Bungic;;;;Bären;;;;;;;;;;;;;Nein;Ja;Nein;;Nein;Mirela;"Marijan Bungic";m.bungic@web.de;;;;;;JYCSXJTX;967076735653408;0;;;0;0
50064753;Hazal;Cicek;;;;Bären;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7714.jpg;Nein;Familie;Cicek;uelke.ardak@hotmail.de;;;;;;VNFYB935;306933685807165;0;;;0;0
49601392;Levi;Damia;;;;Bären;;;;;;;;;;;;;Ja;Ja;Nein;;Nein;Louisa;Damian;damian.louisa@web.de;;;;;;3VP45KKX;107953830470294;0;;;0;0
50314236;Levi;Damian;;;;Bären;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7936.jpg;Nein;Louisa;Damian;damian.louisa@web.de;;;;;;DVXDP3PH;677393543795054;1;;;0;1
50211537;Gökhan;Dogan;;;;Bären;;;;;;;;;Eray;Dogan;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_8096.jpg;Nein;Familie;Dogan;goeksel_dogan@web.de;;;;;;V9FBBSMP;152677334111372;2;;;0;0
50220839;"Magdalena Personal";Forster;;;;Bären;;;;;;;;;;;;;Ja;Ja;Nein;;Nein;Magdalena;Forster;magdalenaforster@aol.de;;;;;;7NG54JNY;74394435366624;0;;;0;0
49629572;Philipp;Gabauer;;;;Bären;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";;Nein;Familie;Gabauer;luzia.gabauer@web.de;;;;;;4HP8FX8K;20692770537744;0;;;0;0
49652592;Emilia;"Herrmann Rodriguez";;;;Bären;;;;;;;;;;;;;Nein;Ja;Nein;;Nein;Lukas;Herrmann;Familie.Herrmann.Rodriguez@web.de;;;;;;7X7Y4BKV;73798042174951;0;;;0;0
50060415;Konstantin;Karl;;;;Bären;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7806.jpg;Nein;Katharina;Karl;katharina_karl@mailbox.org;;;;;;XNCV6XM7;810263015266358;0;;;0;0
50060407;Paulina;Karl;;;;Bären;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";;Nein;Katharina;Karl;katharina_karl@mailbox.org;;;;;;HSKMY37G;607082088640959;0;;;0;0
49901894;Salomia;Karpenko;;;;Bären;;;;;;;;;Miroslav;Karpenko;;;Ja;Ja;Nein;IMG_7786.jpg;Nein;Familie;Karpenko;denis.k88@web.de;;;;;;4P7TJXJL;826081492713003;4;;;0;0
49654259;Jan;Klyszcz;;;;Bären;;;;;;;;;Christoph;Klyszcz;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7859.jpg;Nein;Familie;Klyszcz;klyszcz.ewa92@gmail.com;;;;;;V9QQ3MHT;635050103722845;4;;;0;0
50220757;Personal;Lang;;;;Bären;;;;;;;;;;;;;Ja;Ja;Nein;;Nein;Susanne;Lang;susi67.sl@gmail.com;;;;;;J2B9F4FH;84529853902827;0;;;0;0
49663258;Tuldi;"Lennart & Hannes";;;;Bären;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7915.jpg;Nein;Familie;Tuldi;olga_tuldi@yahoo.de;;;;;;Z7D4PJHV;628485247329265;10;;;0;0
49727295;Leonardo;Liquori;;;;Bären;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7654.jpg;Nein;Elisa;Mandelli;e.mandelli1@icloud.com;;;;;;CZBSHZXD;112332574322427;3;;;0;0
49694659;Mara;Schmid;;;;Bären;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7737.jpg;Nein;Familie;Schmid;izuther@googlemail.com;;;;;;M2QWP8PN;693636596918854;4;;;0;0
49553844;Niklas;Schulze;;;;Bären;;;;;;;;;;;;;Ja;Ja;Nein;IMG_8027.jpg;Nein;Kristina;Schulze;m-k-ammersdorf@gmx.de;;Kristina;Schulze;kristina-anna-schulze@web.de;;TDJ47324;213569357182904;4;;;0;1
49605342;Zoe;Seget;;;;Bären;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7764.jpg;Nein;Sandra;Seget;sandra.seget@hotmail.de;;;;;;GTY9QMWP;335161472735404;3;;;0;1
50211319;Valentin;Slugocki;;;;Bären;;;;;;;;;;;;;Ja;Ja;Nein;;Nein;Bartek;Slugocki;bartek@slugocki.de;;;;;;24632X5S;557733375991183;0;;;0;0
50219244;"Hannes & Lennart";Tuldi;;;;Bären;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7959.jpg;Nein;Familie;Tuldi;olga_tuldi@yahoo.de;;;;;;SZ7D82KL;195111473283743;9;;;0;1
49697372;Xaver;Wego;;;;Bären;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7883.jpg;Nein;Luisa;Wego;luisa.wego@web.deq;;Luisa;Wego;luisa.wego@web.de;;JT22FL8Y;470837065491819;0;6XJVMHVQ;423156711490859;6;1
49655774;Maximilian;Wild;;;;Bären;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7977.jpg;Nein;Familie;Wild;wildramona@gmx.de;;;;;;XYCPRYX3;841975500351515;6;;;0;0
49613372;Anton;Adelberger;;;;Bienen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_8367.jpg;Nein;Catharina;Adelberger;catharina.adelberger@web.de;;;;;;GCSBWRRG;255252947890362;5;;;0;0
49655260;Ludwig;Baumgartner;;;;Bienen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";;Nein;Franziska;Baumgartner;franziwild@gmx.de;;;;;;JJDBSW2Y;1346656807317;0;;;0;0
49652607;Josip;Bungic;;;;Bienen;;;;;;;;;;;;;Nein;Ja;Nein;;Nein;Mirela;"Marijan Bungic";m.bungic@web.de;;;;;;XR3LFNTW;896957772773017;1;;;0;0
50064781;Havin;Cicek;;;;Bienen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_8198.jpg;Nein;Familie;Cicek;uelke.ardak@hotmail.de;;;;;;8X8GYWR3;847511991706072;1;;;0;0
50055747;Mattea;Fusarri;;;;Bienen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_8151.jpg;Nein;Nadia;Fusarri;nadia.fusarri@gmx.de;;;;;;ZRGWQM3W;439731985455440;3;;;0;0
50247238;Maliya;Gildner;;;;Bienen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_8341.jpg;Nein;Alisa;Gildner;gildner31@gmail.com;;;;;;KRTVJ4M5;910013114016383;2;;;0;0
49825283;Kilian;Hartl;;;;Bienen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_8410.jpg;Nein;Familie;Schreibauer;a.schreibauer@gmail.com;;;;;;NM92G8PK;534850382393461;3;;;0;0
50153154;"Elara Carolina";Hintermaier;;;;Bienen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_8289.jpg;Nein;Adriana;Hintermaier;adri.shunka@gmail.com;;;;;;N6G67PPZ;233647866343524;1;;;0;0
49700913;Luka;Loncar;;;;Bienen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_8435.jpg;Nein;Szilvia;Palinkas;silvijapalinkas@yahoo.com;;;;;;7FGK48GQ;345687401851686;5;;;0;1
50056989;Elias;Minksz;;;;Bienen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";;Nein;Carolin;Dirndorfer;c.dirndorfer@gmx.de;;;;;;GZ3DQSPL;632143387747513;0;;;0;0
49770856;Anna;Nguyen;;;;Bienen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_8219.jpg;Nein;Thi;"Hien Minh Nguyen";nthm30121996@gmail.com;;Anna;Nguyen;ging318@gmail.com;;CM9CMLBJ;122574286373832;2;;;0;0
50180008;Ilia;Nickl;;;;Bienen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_8390.jpg;Nein;Familie;Nickl;cela990@hotmail.com;;;;;;KHPY6LQV;652142151577775;9;;;0;0
49575500;Mika;Rubinstein;;;;Bienen;;;;;;;;;Mia;Rubinstein;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_8548.jpg;Nein;Familie;Rubinstein;n.d.rubinstein@googlemail.com;;;;;;K7PX4J8Y;415300008608215;2;;;0;0
49652538;Alina;Schillinger;;;;Bienen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_8244.jpg;Nein;Familie;Schillinger;schneggeno1@web.de;;;;;;X27P5L9Q;180935518874486;3;;;0;0
49663277;Malia;Schlesinger;;;;Bienen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_8126.jpg;Nein;Familie;Schlesinger;stefanie2011@gmx.net;;;;;;6672SN99;377539049099605;2;;;0;0
50257156;Marie;Schöberl;;;;Bienen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_8265.jpg;Nein;Michaela;Schöberl;michaela.schoeberl@gmx.de;;;;;;BKZWFCS4;504469516218803;2;;;0;0
50057519;Letizia;Stachanczyk;;;;Bienen;;;;;;;;;Leonardo;Stachanczyk;;;Ja;Ja;"Familien- / Geschwisterfotos";;Nein;Familie;Stachanczyk;Suzanna.Stachanczyk@web.de;;;;;;C7GX6BM2;268376387434609;0;;;0;0
49919594;Ela;Torres;;;;Bienen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_8313.jpg;Nein;Familie;Torres;ftorrestapia@me.com;;;;;;YGL954RX;63682236385188;4;;;0;0
49837810;Maximilian;Weber;;;;Bienen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_8522.jpg;Nein;Familie;Weber;mail.weber.melanie@googlemail.com;;;;;;4QS8GPWR;7954462978689;3;;;0;0
50006492;Musa;Yilmaz;;;;Bienen;;;;;;;;;Ömer;Yilmaz;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_8466.jpg;Nein;Familie;Yilmaz;merve-ymz@hotmail.com;;;;;;82YTD8FK;912359706713774;4;;;0;0
49652523;Nina;Zhang;;;;Bienen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_8174.jpg;Nein;Hua;Zhang;zhanghua0411@hotmail.com;;;;;;DTWR882P;201986576299456;3;;;0;1
49663112;Elias;Bonifati;;;;Fische;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_6891.jpg;Nein;Familie;Misiano;bonifati@hotmail.de;;;;;;BVMZFPHK;582378219071480;5;;;0;0
49652608;Mihael;Bungic;;;;Fische;;;;;;;;;;;;;Nein;Ja;Nein;;Nein;Mirela;"Marijan Bungic";m.bungic@web.de;;;;;;2664S6D3;848993713584313;1;;;0;0
50248018;Alina;Catak;;;;Fische;;;;;;;;;;;;;Ja;Ja;Nein;IMG_6424.jpg;Nein;Catak;Admir;catakadmir@gmail.com;;;;;;ML42KL42;532022002681433;7;;;0;1
50258123;Jakov;Ceko;;;;Fische;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7294.jpg;Nein;Familie;Ceko;brankoceko91@gmail.com;;;;;;CVBYPKBV;550993218062367;0;;;0;0
50258100;Luka;Ceko;;;;Fische;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7043.jpg;Nein;Familie;Ceko;brankoceko91@gmail.com;;;;;;DJKP3CBY;120492680122927;0;;;0;0
50222105;"Saide Mira";Cildir;;;;Fische;;;;;;;;;;;;;Ja;Ja;Nein;IMG_6617.jpg;Nein;Hasret;Cildir;hasretcildir@web.de;;;;;;M9B773FR;568587367870302;1;;;0;0
50218962;Valentin;Gabauer;;;;Fische;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7004.jpg;Nein;Sabine;Gabauer;sabine.gabauer@gmx.de;;;;;;QXWD3ZNS;377128045906117;2;;;0;1
50079276;Anika;Gaßner;;;;Fische;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_6555.jpg;Nein;Familie;Karyakina;veronika20@hotmail.de;;;;;;3BSPHDHW;851569123151027;1;;;0;0
50211546;Kilian;Glück;;;;Fische;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_6960.jpg;Nein;Familie;Glück;katja_glueck@web.de;;;;;;2LGTH3VN;783740741149373;2;;;0;0
50282221;Lisa;Gumberger;;;;Fische;;;;;;;;;;;;;Ja;Ja;Nein;IMG_6470.jpg;Nein;Sarah;Gumberger;sarah.gumberger@gmx.de;;;;;;2PHHTXT9;383948464807600;3;;;0;1
49616818;Nadine;Hamed;;;;Fische;;;;;;;;;;;;;Ja;Ja;Nein;IMG_6648.jpg;Nein;;Nadine;sarasadaby@gmail.com;;;;;;V3KTTVNV;319851445592837;3;;;0;1
50208499;Noela;Islami;;;;Fische;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_6524.jpg;Nein;Familie;Islami;Zineta.islami@gmx.de;;;;;;CK5B7WWN;410922852620998;2;;;0;0
49901895;Miroslav;Karpenko;;;;Fische;;;;;;;;;Salomia;Karpenko;;;Ja;Ja;Nein;IMG_6848.jpg;Nein;Familie;Karpenko;denis.k88@web.de;;;;;;922GC8BH;915901506033102;1;;;0;0
50221939;Merjem;Kaukovic;;;;Fische;;;;;;;;;;;;;Ja;Ja;Nein;IMG_6502.jpg;Nein;Edita;Porcic-Kaukovic;editta1996@hotmail.com;;;;;;KGVKW8HZ;305550439054156;1;;;0;0
50288355;Frauke;Klinge;;;;Fische;;;;;;;;;;;;;Nein;Ja;Nein;;Nein;Frauke;Klinge;fs.klinge@t-online.de;;;;;;K4GCTQJG;948438313552204;0;;;0;0
49653461;Max;Krämer;;;;Fische;;;;;;;;;;;;;Ja;Ja;Nein;IMG_6778.jpg;Nein;Michael;Krämer;m.k326@web.de;;;;;;PF5DCT5N;929652051737843;2;;;0;1
49663714;Casper;Mettig;;;;Fische;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";;Nein;Familie;Roder;stephanie.roder@gmail.com;;;;;;4L2ZBL3M;296176221032781;6;;;0;0
50208680;Emre;Mujic;;;;Fische;;;;;;;;;;;;;Ja;Ja;Nein;IMG_6927.jpg;Nein;Amra;Mujic;amra.mujic95@gmail.com;;;;;;QVBTGS73;16951665504680;2;;;0;0
49635375;Mila;Nickl;;;;Fische;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_6685.jpg;Nein;Familie;Nickl;cela990@hotmail.com;;;;;;S4Z3HPZY;736840096508400;8;;;0;0
49575499;Mia;Rubinstein;;;;Fische;;;;;;;;;Mika;Rubinstein;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_6443.jpg;Nein;Familie;Rubinstein;n.d.rubinstein@googlemail.com;;;;;;BGGHXNLC;908242966462743;3;;;0;0
49786711;Zoe;Scholpp;;;;Fische;;;;;;;;;;;;;Ja;Ja;Nein;IMG_6721.jpg;Nein;Sabrina;Scholpp;sabrinasch1107@gmail.com;;;;;;CXMT4R9T;585861217320080;2;;;0;0
49755578;Valerie;Schultze;;;;Fische;;;;;;;;;;;;;Ja;Ja;Nein;IMG_6743.jpg;Nein;Anita;Schultze;anitajuliane.schultze@gmail.com;;;;;;4WVF24SR;499960849175102;2;;;0;0
50057518;Leonardo;Stachanczyk;;;;Fische;;;;;;;;;Letizia;Stachanczyk;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7072.jpg;Nein;Familie;Stachanczyk;Suzanna.Stachanczyk@web.de;;;;;;BZC28W7T;900613948231467;7;;;0;0
50211898;Maya;Watanabe;;;;Fische;;;;;;;;;;;;;Ja;Ja;Nein;IMG_6591.jpg;Nein;Barbara;Watanabe;barbara.j@live.de;;;;;;ZPVJ8R5Q;341636130475078;1;;;0;0
50006491;Ömer;Yilmaz;;;;Fische;;;;;;;;;Musa;Yilmaz;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_6809.jpg;Nein;Familie;Yilmaz;merve-ymz@hotmail.com;;;;;;C4NB42PX;659038103936299;4;;;0;0
50078572;Aurelia;Adelsberger;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7493.jpg;Nein;Familie;Adelsberger;barbara.adelsberger@yahoo.de;;Christian;Godelmann;floke.com@gmail.com;;3NVRB2BM;230676178020824;1;;;0;0
50220084;Eymen;Baldir;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_6406.jpg;Nein;Seda;Baldir;seda.ay@icloud.com;;;;;;JDG5CDT8;381447885366279;1;;;0;0
49602297;Magdalena;Bauer;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7463.jpg;Nein;Familie;Bauer;bonprix29@yahoo.de;;;;;;V7W5ZPVY;411274063112493;8;;;0;0
49992146;Zoe;Cajic;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7414.jpg;Nein;Amar;Cajic;amar.cajic@gmail.com;;;;;;JMZD8SNN;53184897942750;3;;;0;0
50057147;Mario;Cakic;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7182.jpg;Nein;Lucija;Zivkovic;lucija.zivkovic16@gmail.com;;;;;;D8SZH5XL;508565079338980;3;;;0;1
50211538;Eray;Dogan;;;;Spatzen;;;;;;;;;Gökhan;Dogan;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7380.jpg;Nein;Familie;Dogan;goeksel_dogan@web.de;;;;;;LMLH64KS;61030672835452;2;;;0;0
49552513;Antonia;Freiwald;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7643.jpg;Nein;Stephanie;Freiwald;stephanie.freiwald@gmx.de;;;;;;4BV76XQS;224897626646913;1;;;0;1
49601982;Heidi;Götzberger;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7618.jpg;Nein;Familie;Götzberger;franziska.lanzinger@t-online.de;;;;;;KSH3Y552;141723213815881;2;;;0;0
50063666;Una;Hodzic;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7561.jpg;Nein;Hodžić;Aldin;menager21@hotmail.com;;;;;;9W4CYMRX;170368609363861;4;;;0;1
49603438;Liara;Honisch;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7519.jpg;Nein;Familie;Karayilan;yasemin.karayilan@yahoo.de;;;;;;MSNCXQ77;787504756287604;2;;;0;0
49623482;Matteo;Katterfeld;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7347.jpg;Nein;Familie;Ketterfeld;madlen.katterfeld@gmx.de;;;;;;YJ9MM349;888691047601122;6;;;0;0
49654260;Christoph;Klyszcz;;;;Spatzen;;;;;;;;;Jan;Klyszcz;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7320.jpg;Nein;Familie;Klyszcz;klyszcz.ewa92@gmail.com;;;;;;V3KSRPDM;389595058391936;6;;;0;0
50056841;Ludwig;Lacen;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7246.jpg;Nein;Michael;Lacen;michael.lacen@gmx.de;;;;;;GJFNWHMY;205672235649590;2;;;0;1
50056690;Emilia;Rodriguez;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7591.jpg;Nein;Daniela;Rodriguez;daniela-hinz-82@gmx.de;;;;;;9LQLW7YV;289213156745302;1;;;0;1
49652595;Vaiana;Slaiman;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;Nein;;Nein;;Slaiman;hadeer94hasan@web.de;;;;;;YB24BVQR;230552964517174;0;;;0;0
49838169;Raphael;Weber;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7214.jpg;Nein;Familie;Weber;mail.weber.melanie@googlemail.com;;;;;;W3VBKM3W;362639250675953;3;;;0;0
49906413;Ludwig;Welz;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;"Familien- / Geschwisterfotos";IMG_7149.jpg;Nein;Familie;Welz;eva_welz@gmx.de;;;;;;VPJYZ48P;785597492180163;3;;;0;0
49726920;Amy;Wieters;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7443.jpg;Nein;Janine;Wieters;janine28@gmx.de;;;;;;69KFXLBD;921506261142206;4;;;0;1
50453287;Familie;Adelsberger;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0728.jpg;Nein;Familie;Adelsberger;barbara.adelsberger@yahoo.de;;;;;;JVXV9T2M;916908422224646;1;;;0;1
50451311;Familie;"Al Khadher";;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0153.jpg;Nein;Familie;"Al Khadher";Husseinalkhadher8@gmail.com;;;;;;4VTSN5J6;437618998198555;2;;;0;1
50453454;Familie;Bauer;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0353.jpg;Nein;Familie;Bauer;bonprix29@yahoo.de;;;;;;N8DZBDLW;169896993687826;2;;;0;0
50491788;Familie;Baumgartner;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0451.jpg;Nein;Franziska;Baumgartner;franziwild@gmx.de;;;;;;ZRH36VRS;847462383118786;2;;;0;1
50463803;Amla;Bobo;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7689.jpg;Nein;Xhulia;Xhelci;xhuliaxhelci@gmail.com;;;;;;C35CQ55V;950839783885570;1;;;0;0
50451304;Familie;Ceko;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0099.jpg;Nein;Familie;Ceko;brankoceko91@gmail.com;;;;;;4NZXSHTW;798398153397116;2;;;0;0
50453898;Familie;Cicek;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_2693.jpg;Nein;Familie;Cicek;uelke.ardak@hotmail.de;;;;;;YC9KVV76;290706892284805;3;;;0;1
50453136;Familie;Dogan;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_9830.jpg;Nein;Familie;Dogan;goeksel_dogan@web.de;;;;;;GDBRDW6K;918773718877810;2;;;0;0
50453529;Familie;Gabauer;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0286.jpg;Nein;Familie;Gabauer;luzia.gabauer@web.de;;;;;;SQHNHMH6;49585341392454;6;;;0;1
50452028;Familie;Glück;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0406.jpg;Nein;Familie;Glück;katja_glueck@web.de;;;;;;7WTXJNDC;628871407778415;2;;;0;0
50453448;Familie;Götzberger;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_1262.jpg;Nein;Familie;Götzberger;franziska.lanzinger@t-online.de;;;;;;MPHRG7SP;954770009741299;2;;;0;1
50453434;Familie;Islami;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_1330.jpg;Nein;Familie;Islami;Zineta.islami@gmx.de;;;;;;KF7CNCYZ;620178179159158;2;;;0;0
50452019;Familie;Karayilan;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0200.jpg;Nein;Familie;Karayilan;yasemin.karayilan@yahoo.de;;;;;;6H73JV6B;765446752804075;1;;;0;0
50453439;Familie;Karpenko;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_2825.jpg;Nein;Familie;Karpenko;denis.k88@web.de;;;;;;LR87SQ8C;963707649418838;1;;;0;0
50453495;Familie;Karyakina;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0800.jpg;Nein;Familie;Karyakina;veronika20@hotmail.de;;;;;;RYK4BQLQ;638219110542782;1;;;0;0
50453488;Familie;Ketterfeld;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_1211.jpg;Nein;Familie;Ketterfeld;madlen.katterfeld@gmx.de;;;;;;PLX9G4V3;117752011222601;6;;;0;1
50453446;Familie;Klyszcz;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_2740.jpg;Nein;Familie;Klyszcz;klyszcz.ewa92@gmail.com;;;;;;LZR8WFP9;874820410323668;9;;;0;1
50452151;Familie;Misiano;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_1577.jpg;Nein;Familie;Misiano;bonifati@hotmail.de;;;;;;7ZW9V666;394112462489259;5;;;0;1
50451284;Familie;Nickl;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_1054.jpg;Nein;Familie;Nickl;cela990@hotmail.com;;;;;;35CH589Q;438824910404667;8;;;0;1
50452230;Familie;Roder;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_1138.jpg;Nein;Familie;Roder;stephanie.roder@gmail.com;;;;;;848D3SWY;745487201848290;6;;;0;0
50452913;Familie;Rubinstein;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0041.jpg;Nein;Familie;Rubinstein;n.d.rubinstein@googlemail.com;;;;;;G9H8YFC4;180523151134386;1;;;0;1
50453768;Familie;Schillinger;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_1404.jpg;Nein;Familie;Schillinger;schneggeno1@web.de;;;;;;SQJSP49C;593384265020703;3;;;0;1
50452236;Familie;Schlesinger;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_1512.jpg;Nein;Familie;Schlesinger;stefanie2011@gmx.net;;;;;;946G6HJH;269413107409936;3;;;0;1
50453894;Familie;Schmid;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0625.jpg;Nein;Familie;Schmid;izuther@googlemail.com;;;;;;W7W8P32C;486167508950250;4;;;0;1
50452248;Familie;Schreibauer;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0671.jpg;Nein;Familie;Schreibauer;a.schreibauer@gmail.com;;;;;;97R4TRBC;130440825414681;3;;;0;0
50452273;Familie;Stachanczyk;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_9974.jpg;Nein;Familie;Stachanczyk;Suzanna.Stachanczyk@web.de;;;;;;C334SSSL;733864213043388;5;;;0;0
50453485;Familie;Torres;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0967.jpg;Nein;Familie;Torres;ftorrestapia@me.com;;;;;;PCQ4CNV9;553742663210606;4;;;0;0
50452252;Familie;Tuldi;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_2788.jpg;Nein;Familie;Tuldi;olga_tuldi@yahoo.de;;;;;;B99BYYYF;657381798122682;11;;;0;0
50452022;Familie;Weber;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0984.jpg;Nein;Familie;Weber;mail.weber.melanie@googlemail.com;;;;;;7954G4C5;820357028620317;3;;;0;1
50451320;Familie;Welz;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_1459.jpg;Nein;Familie;Welz;eva_welz@gmx.de;;;;;;69N5WYFK;952025141929986;3;;;0;1
50452419;Familie;Wild;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0539.jpg;Nein;Familie;Wild;wildramona@gmx.de;;;;;;DVXKKJCZ;789239059675168;9;;;0;1
50452462;Familie;Wolf;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_0841.jpg;Nein;Familie;Wolf;anjamichi77@gmail.com;;;;;;FXPLQYH9;784676508389646;1;;;0;0
50410050;Jonas;Wolf;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_8489.jpg;Nein;Familie;Wolf;anjamichi77@gmail.com;;;;;;Q52NYB4N;18810570193338;0;;;0;0
50453882;Familie;Yilmaz;;;;;;;;;;;;;;;;;Ja;Ja;Nein;IMG_9907.jpg;Nein;Familie;Yilmaz;merve-ymz@hotmail.com;;;;;;TXK86QSB;467856804734432;4;;;0;0
49655787;Joseph;Wild;;;;Spatzen;;;;;;;;;;;;;Ja;Ja;Nein;IMG_7119.jpg;Nein;Familie;Wild;wildramona@gmx.de;;;;;;8C56662R;226166391326912;5;;;0;0
1 Child ID Vorname Kind Nachname Kind Geschlecht Geburtsdatum Einrichtung Gruppe Lehrer Gültig bis Bezeichner Referenz Straße PLZ Ort Staat Geschwisterkind Vorname (1) Geschwisterkind Nachname (1) Geschwisterkind Vorname (2) Geschwisterkind Nachname (2) Einzelfotos Gruppenfotos Familie / Geschwister Foto Vom Kunden ausgewählt Vorname Eltern (1) Nachname Eltern (1) Email der Eltern (1) Telefonnummer der Eltern (1) Vorname Eltern (2) Nachname Eltern (2) Email der Eltern (2) Telefonnummer der Eltern (2) Zugangscode (1) Barcode (1) Logins (1) Zugangscode (2) Barcode (2) Logins (2) Bestellungen
2 49663204 Fares AL-KHADHER Bären Ja Ja Nein IMG_8069.jpg Nein Familie Al Khadher Husseinalkhadher8@gmail.com 9CKZ9FRB 859242970856177 2 0 0
3 49656019 Entoni Altoni Bären Ja Ja Nein IMG_7999.jpg Nein Yuliia Altoni julichka.altony@gmail.com 8PH6DT65 590974350307121 1 0 1
4 49659604 Rashane Tyler Asasana Bären Ja Ja Nein IMG_8048.jpg Nein Penphaka Asasana asa-sa-na@hotmail.com 57VSYGKZ 742438249864838 2 0 0
5 49955890 Yunus Batuge Bären Ja Ja Familien- / Geschwisterfotos IMG_7831.jpg Nein Sümeyra Senyurt senyurtsumeyra7@gmail.com Y9LFLVQ6 807433233164209 15 0 1
6 49652597 Josip Bungic Bären Nein Ja Nein Nein Mirela Marijan Bungic m.bungic@web.de JYCSXJTX 967076735653408 0 0 0
7 50064753 Hazal Cicek Bären Ja Ja Nein IMG_7714.jpg Nein Familie Cicek uelke.ardak@hotmail.de VNFYB935 306933685807165 0 0 0
8 49601392 Levi Damia Bären Ja Ja Nein Nein Louisa Damian damian.louisa@web.de 3VP45KKX 107953830470294 0 0 0
9 50314236 Levi Damian Bären Ja Ja Nein IMG_7936.jpg Nein Louisa Damian damian.louisa@web.de DVXDP3PH 677393543795054 1 0 1
10 50211537 Gökhan Dogan Bären Eray Dogan Ja Ja Familien- / Geschwisterfotos IMG_8096.jpg Nein Familie Dogan goeksel_dogan@web.de V9FBBSMP 152677334111372 2 0 0
11 50220839 Magdalena Personal Forster Bären Ja Ja Nein Nein Magdalena Forster magdalenaforster@aol.de 7NG54JNY 74394435366624 0 0 0
12 49629572 Philipp Gabauer Bären Ja Ja Familien- / Geschwisterfotos Nein Familie Gabauer luzia.gabauer@web.de 4HP8FX8K 20692770537744 0 0 0
13 49652592 Emilia Herrmann Rodriguez Bären Nein Ja Nein Nein Lukas Herrmann Familie.Herrmann.Rodriguez@web.de 7X7Y4BKV 73798042174951 0 0 0
14 50060415 Konstantin Karl Bären Ja Ja Nein IMG_7806.jpg Nein Katharina Karl katharina_karl@mailbox.org XNCV6XM7 810263015266358 0 0 0
15 50060407 Paulina Karl Bären Ja Ja Familien- / Geschwisterfotos Nein Katharina Karl katharina_karl@mailbox.org HSKMY37G 607082088640959 0 0 0
16 49901894 Salomia Karpenko Bären Miroslav Karpenko Ja Ja Nein IMG_7786.jpg Nein Familie Karpenko denis.k88@web.de 4P7TJXJL 826081492713003 4 0 0
17 49654259 Jan Klyszcz Bären Christoph Klyszcz Ja Ja Familien- / Geschwisterfotos IMG_7859.jpg Nein Familie Klyszcz klyszcz.ewa92@gmail.com V9QQ3MHT 635050103722845 4 0 0
18 50220757 Personal Lang Bären Ja Ja Nein Nein Susanne Lang susi67.sl@gmail.com J2B9F4FH 84529853902827 0 0 0
19 49663258 Tuldi Lennart & Hannes Bären Ja Ja Familien- / Geschwisterfotos IMG_7915.jpg Nein Familie Tuldi olga_tuldi@yahoo.de Z7D4PJHV 628485247329265 10 0 0
20 49727295 Leonardo Liquori Bären Ja Ja Familien- / Geschwisterfotos IMG_7654.jpg Nein Elisa Mandelli e.mandelli1@icloud.com CZBSHZXD 112332574322427 3 0 0
21 49694659 Mara Schmid Bären Ja Ja Familien- / Geschwisterfotos IMG_7737.jpg Nein Familie Schmid izuther@googlemail.com M2QWP8PN 693636596918854 4 0 0
22 49553844 Niklas Schulze Bären Ja Ja Nein IMG_8027.jpg Nein Kristina Schulze m-k-ammersdorf@gmx.de Kristina Schulze kristina-anna-schulze@web.de TDJ47324 213569357182904 4 0 1
23 49605342 Zoe Seget Bären Ja Ja Familien- / Geschwisterfotos IMG_7764.jpg Nein Sandra Seget sandra.seget@hotmail.de GTY9QMWP 335161472735404 3 0 1
24 50211319 Valentin Slugocki Bären Ja Ja Nein Nein Bartek Slugocki bartek@slugocki.de 24632X5S 557733375991183 0 0 0
25 50219244 Hannes & Lennart Tuldi Bären Ja Ja Familien- / Geschwisterfotos IMG_7959.jpg Nein Familie Tuldi olga_tuldi@yahoo.de SZ7D82KL 195111473283743 9 0 1
26 49697372 Xaver Wego Bären Ja Ja Nein IMG_7883.jpg Nein Luisa Wego luisa.wego@web.deq Luisa Wego luisa.wego@web.de JT22FL8Y 470837065491819 0 6XJVMHVQ 423156711490859 6 1
27 49655774 Maximilian Wild Bären Ja Ja Familien- / Geschwisterfotos IMG_7977.jpg Nein Familie Wild wildramona@gmx.de XYCPRYX3 841975500351515 6 0 0
28 49613372 Anton Adelberger Bienen Ja Ja Nein IMG_8367.jpg Nein Catharina Adelberger catharina.adelberger@web.de GCSBWRRG 255252947890362 5 0 0
29 49655260 Ludwig Baumgartner Bienen Ja Ja Familien- / Geschwisterfotos Nein Franziska Baumgartner franziwild@gmx.de JJDBSW2Y 1346656807317 0 0 0
30 49652607 Josip Bungic Bienen Nein Ja Nein Nein Mirela Marijan Bungic m.bungic@web.de XR3LFNTW 896957772773017 1 0 0
31 50064781 Havin Cicek Bienen Ja Ja Familien- / Geschwisterfotos IMG_8198.jpg Nein Familie Cicek uelke.ardak@hotmail.de 8X8GYWR3 847511991706072 1 0 0
32 50055747 Mattea Fusarri Bienen Ja Ja Nein IMG_8151.jpg Nein Nadia Fusarri nadia.fusarri@gmx.de ZRGWQM3W 439731985455440 3 0 0
33 50247238 Maliya Gildner Bienen Ja Ja Nein IMG_8341.jpg Nein Alisa Gildner gildner31@gmail.com KRTVJ4M5 910013114016383 2 0 0
34 49825283 Kilian Hartl Bienen Ja Ja Familien- / Geschwisterfotos IMG_8410.jpg Nein Familie Schreibauer a.schreibauer@gmail.com NM92G8PK 534850382393461 3 0 0
35 50153154 Elara Carolina Hintermaier Bienen Ja Ja Nein IMG_8289.jpg Nein Adriana Hintermaier adri.shunka@gmail.com N6G67PPZ 233647866343524 1 0 0
36 49700913 Luka Loncar Bienen Ja Ja Nein IMG_8435.jpg Nein Szilvia Palinkas silvijapalinkas@yahoo.com 7FGK48GQ 345687401851686 5 0 1
37 50056989 Elias Minksz Bienen Ja Ja Familien- / Geschwisterfotos Nein Carolin Dirndorfer c.dirndorfer@gmx.de GZ3DQSPL 632143387747513 0 0 0
38 49770856 Anna Nguyen Bienen Ja Ja Nein IMG_8219.jpg Nein Thi Hien Minh Nguyen nthm30121996@gmail.com Anna Nguyen ging318@gmail.com CM9CMLBJ 122574286373832 2 0 0
39 50180008 Ilia Nickl Bienen Ja Ja Familien- / Geschwisterfotos IMG_8390.jpg Nein Familie Nickl cela990@hotmail.com KHPY6LQV 652142151577775 9 0 0
40 49575500 Mika Rubinstein Bienen Mia Rubinstein Ja Ja Familien- / Geschwisterfotos IMG_8548.jpg Nein Familie Rubinstein n.d.rubinstein@googlemail.com K7PX4J8Y 415300008608215 2 0 0
41 49652538 Alina Schillinger Bienen Ja Ja Familien- / Geschwisterfotos IMG_8244.jpg Nein Familie Schillinger schneggeno1@web.de X27P5L9Q 180935518874486 3 0 0
42 49663277 Malia Schlesinger Bienen Ja Ja Familien- / Geschwisterfotos IMG_8126.jpg Nein Familie Schlesinger stefanie2011@gmx.net 6672SN99 377539049099605 2 0 0
43 50257156 Marie Schöberl Bienen Ja Ja Nein IMG_8265.jpg Nein Michaela Schöberl michaela.schoeberl@gmx.de BKZWFCS4 504469516218803 2 0 0
44 50057519 Letizia Stachanczyk Bienen Leonardo Stachanczyk Ja Ja Familien- / Geschwisterfotos Nein Familie Stachanczyk Suzanna.Stachanczyk@web.de C7GX6BM2 268376387434609 0 0 0
45 49919594 Ela Torres Bienen Ja Ja Familien- / Geschwisterfotos IMG_8313.jpg Nein Familie Torres ftorrestapia@me.com YGL954RX 63682236385188 4 0 0
46 49837810 Maximilian Weber Bienen Ja Ja Familien- / Geschwisterfotos IMG_8522.jpg Nein Familie Weber mail.weber.melanie@googlemail.com 4QS8GPWR 7954462978689 3 0 0
47 50006492 Musa Yilmaz Bienen Ömer Yilmaz Ja Ja Familien- / Geschwisterfotos IMG_8466.jpg Nein Familie Yilmaz merve-ymz@hotmail.com 82YTD8FK 912359706713774 4 0 0
48 49652523 Nina Zhang Bienen Ja Ja Nein IMG_8174.jpg Nein Hua Zhang zhanghua0411@hotmail.com DTWR882P 201986576299456 3 0 1
49 49663112 Elias Bonifati Fische Ja Ja Familien- / Geschwisterfotos IMG_6891.jpg Nein Familie Misiano bonifati@hotmail.de BVMZFPHK 582378219071480 5 0 0
50 49652608 Mihael Bungic Fische Nein Ja Nein Nein Mirela Marijan Bungic m.bungic@web.de 2664S6D3 848993713584313 1 0 0
51 50248018 Alina Catak Fische Ja Ja Nein IMG_6424.jpg Nein Catak Admir catakadmir@gmail.com ML42KL42 532022002681433 7 0 1
52 50258123 Jakov Ceko Fische Ja Ja Nein IMG_7294.jpg Nein Familie Ceko brankoceko91@gmail.com CVBYPKBV 550993218062367 0 0 0
53 50258100 Luka Ceko Fische Ja Ja Nein IMG_7043.jpg Nein Familie Ceko brankoceko91@gmail.com DJKP3CBY 120492680122927 0 0 0
54 50222105 Saide Mira Cildir Fische Ja Ja Nein IMG_6617.jpg Nein Hasret Cildir hasretcildir@web.de M9B773FR 568587367870302 1 0 0
55 50218962 Valentin Gabauer Fische Ja Ja Nein IMG_7004.jpg Nein Sabine Gabauer sabine.gabauer@gmx.de QXWD3ZNS 377128045906117 2 0 1
56 50079276 Anika Gaßner Fische Ja Ja Familien- / Geschwisterfotos IMG_6555.jpg Nein Familie Karyakina veronika20@hotmail.de 3BSPHDHW 851569123151027 1 0 0
57 50211546 Kilian Glück Fische Ja Ja Familien- / Geschwisterfotos IMG_6960.jpg Nein Familie Glück katja_glueck@web.de 2LGTH3VN 783740741149373 2 0 0
58 50282221 Lisa Gumberger Fische Ja Ja Nein IMG_6470.jpg Nein Sarah Gumberger sarah.gumberger@gmx.de 2PHHTXT9 383948464807600 3 0 1
59 49616818 Nadine Hamed Fische Ja Ja Nein IMG_6648.jpg Nein Nadine sarasadaby@gmail.com V3KTTVNV 319851445592837 3 0 1
60 50208499 Noela Islami Fische Ja Ja Familien- / Geschwisterfotos IMG_6524.jpg Nein Familie Islami Zineta.islami@gmx.de CK5B7WWN 410922852620998 2 0 0
61 49901895 Miroslav Karpenko Fische Salomia Karpenko Ja Ja Nein IMG_6848.jpg Nein Familie Karpenko denis.k88@web.de 922GC8BH 915901506033102 1 0 0
62 50221939 Merjem Kaukovic Fische Ja Ja Nein IMG_6502.jpg Nein Edita Porcic-Kaukovic editta1996@hotmail.com KGVKW8HZ 305550439054156 1 0 0
63 50288355 Frauke Klinge Fische Nein Ja Nein Nein Frauke Klinge fs.klinge@t-online.de K4GCTQJG 948438313552204 0 0 0
64 49653461 Max Krämer Fische Ja Ja Nein IMG_6778.jpg Nein Michael Krämer m.k326@web.de PF5DCT5N 929652051737843 2 0 1
65 49663714 Casper Mettig Fische Ja Ja Familien- / Geschwisterfotos Nein Familie Roder stephanie.roder@gmail.com 4L2ZBL3M 296176221032781 6 0 0
66 50208680 Emre Mujic Fische Ja Ja Nein IMG_6927.jpg Nein Amra Mujic amra.mujic95@gmail.com QVBTGS73 16951665504680 2 0 0
67 49635375 Mila Nickl Fische Ja Ja Familien- / Geschwisterfotos IMG_6685.jpg Nein Familie Nickl cela990@hotmail.com S4Z3HPZY 736840096508400 8 0 0
68 49575499 Mia Rubinstein Fische Mika Rubinstein Ja Ja Familien- / Geschwisterfotos IMG_6443.jpg Nein Familie Rubinstein n.d.rubinstein@googlemail.com BGGHXNLC 908242966462743 3 0 0
69 49786711 Zoe Scholpp Fische Ja Ja Nein IMG_6721.jpg Nein Sabrina Scholpp sabrinasch1107@gmail.com CXMT4R9T 585861217320080 2 0 0
70 49755578 Valerie Schultze Fische Ja Ja Nein IMG_6743.jpg Nein Anita Schultze anitajuliane.schultze@gmail.com 4WVF24SR 499960849175102 2 0 0
71 50057518 Leonardo Stachanczyk Fische Letizia Stachanczyk Ja Ja Familien- / Geschwisterfotos IMG_7072.jpg Nein Familie Stachanczyk Suzanna.Stachanczyk@web.de BZC28W7T 900613948231467 7 0 0
72 50211898 Maya Watanabe Fische Ja Ja Nein IMG_6591.jpg Nein Barbara Watanabe barbara.j@live.de ZPVJ8R5Q 341636130475078 1 0 0
73 50006491 Ömer Yilmaz Fische Musa Yilmaz Ja Ja Familien- / Geschwisterfotos IMG_6809.jpg Nein Familie Yilmaz merve-ymz@hotmail.com C4NB42PX 659038103936299 4 0 0
74 50078572 Aurelia Adelsberger Spatzen Ja Ja Familien- / Geschwisterfotos IMG_7493.jpg Nein Familie Adelsberger barbara.adelsberger@yahoo.de Christian Godelmann floke.com@gmail.com 3NVRB2BM 230676178020824 1 0 0
75 50220084 Eymen Baldir Spatzen Ja Ja Nein IMG_6406.jpg Nein Seda Baldir seda.ay@icloud.com JDG5CDT8 381447885366279 1 0 0
76 49602297 Magdalena Bauer Spatzen Ja Ja Familien- / Geschwisterfotos IMG_7463.jpg Nein Familie Bauer bonprix29@yahoo.de V7W5ZPVY 411274063112493 8 0 0
77 49992146 Zoe Cajic Spatzen Ja Ja Nein IMG_7414.jpg Nein Amar Cajic amar.cajic@gmail.com JMZD8SNN 53184897942750 3 0 0
78 50057147 Mario Cakic Spatzen Ja Ja Nein IMG_7182.jpg Nein Lucija Zivkovic lucija.zivkovic16@gmail.com D8SZH5XL 508565079338980 3 0 1
79 50211538 Eray Dogan Spatzen Gökhan Dogan Ja Ja Familien- / Geschwisterfotos IMG_7380.jpg Nein Familie Dogan goeksel_dogan@web.de LMLH64KS 61030672835452 2 0 0
80 49552513 Antonia Freiwald Spatzen Ja Ja Nein IMG_7643.jpg Nein Stephanie Freiwald stephanie.freiwald@gmx.de 4BV76XQS 224897626646913 1 0 1
81 49601982 Heidi Götzberger Spatzen Ja Ja Familien- / Geschwisterfotos IMG_7618.jpg Nein Familie Götzberger franziska.lanzinger@t-online.de KSH3Y552 141723213815881 2 0 0
82 50063666 Una Hodzic Spatzen Ja Ja Nein IMG_7561.jpg Nein Hodžić Aldin menager21@hotmail.com 9W4CYMRX 170368609363861 4 0 1
83 49603438 Liara Honisch Spatzen Ja Ja Familien- / Geschwisterfotos IMG_7519.jpg Nein Familie Karayilan yasemin.karayilan@yahoo.de MSNCXQ77 787504756287604 2 0 0
84 49623482 Matteo Katterfeld Spatzen Ja Ja Familien- / Geschwisterfotos IMG_7347.jpg Nein Familie Ketterfeld madlen.katterfeld@gmx.de YJ9MM349 888691047601122 6 0 0
85 49654260 Christoph Klyszcz Spatzen Jan Klyszcz Ja Ja Familien- / Geschwisterfotos IMG_7320.jpg Nein Familie Klyszcz klyszcz.ewa92@gmail.com V3KSRPDM 389595058391936 6 0 0
86 50056841 Ludwig Lacen Spatzen Ja Ja Nein IMG_7246.jpg Nein Michael Lacen michael.lacen@gmx.de GJFNWHMY 205672235649590 2 0 1
87 50056690 Emilia Rodriguez Spatzen Ja Ja Nein IMG_7591.jpg Nein Daniela Rodriguez daniela-hinz-82@gmx.de 9LQLW7YV 289213156745302 1 0 1
88 49652595 Vaiana Slaiman Spatzen Ja Ja Nein Nein Slaiman hadeer94hasan@web.de YB24BVQR 230552964517174 0 0 0
89 49838169 Raphael Weber Spatzen Ja Ja Nein IMG_7214.jpg Nein Familie Weber mail.weber.melanie@googlemail.com W3VBKM3W 362639250675953 3 0 0
90 49906413 Ludwig Welz Spatzen Ja Ja Familien- / Geschwisterfotos IMG_7149.jpg Nein Familie Welz eva_welz@gmx.de VPJYZ48P 785597492180163 3 0 0
91 49726920 Amy Wieters Spatzen Ja Ja Nein IMG_7443.jpg Nein Janine Wieters janine28@gmx.de 69KFXLBD 921506261142206 4 0 1
92 50453287 Familie Adelsberger Ja Ja Nein IMG_0728.jpg Nein Familie Adelsberger barbara.adelsberger@yahoo.de JVXV9T2M 916908422224646 1 0 1
93 50451311 Familie Al Khadher Ja Ja Nein IMG_0153.jpg Nein Familie Al Khadher Husseinalkhadher8@gmail.com 4VTSN5J6 437618998198555 2 0 1
94 50453454 Familie Bauer Ja Ja Nein IMG_0353.jpg Nein Familie Bauer bonprix29@yahoo.de N8DZBDLW 169896993687826 2 0 0
95 50491788 Familie Baumgartner Ja Ja Nein IMG_0451.jpg Nein Franziska Baumgartner franziwild@gmx.de ZRH36VRS 847462383118786 2 0 1
96 50463803 Amla Bobo Ja Ja Nein IMG_7689.jpg Nein Xhulia Xhelci xhuliaxhelci@gmail.com C35CQ55V 950839783885570 1 0 0
97 50451304 Familie Ceko Ja Ja Nein IMG_0099.jpg Nein Familie Ceko brankoceko91@gmail.com 4NZXSHTW 798398153397116 2 0 0
98 50453898 Familie Cicek Ja Ja Nein IMG_2693.jpg Nein Familie Cicek uelke.ardak@hotmail.de YC9KVV76 290706892284805 3 0 1
99 50453136 Familie Dogan Ja Ja Nein IMG_9830.jpg Nein Familie Dogan goeksel_dogan@web.de GDBRDW6K 918773718877810 2 0 0
100 50453529 Familie Gabauer Ja Ja Nein IMG_0286.jpg Nein Familie Gabauer luzia.gabauer@web.de SQHNHMH6 49585341392454 6 0 1
101 50452028 Familie Glück Ja Ja Nein IMG_0406.jpg Nein Familie Glück katja_glueck@web.de 7WTXJNDC 628871407778415 2 0 0
102 50453448 Familie Götzberger Ja Ja Nein IMG_1262.jpg Nein Familie Götzberger franziska.lanzinger@t-online.de MPHRG7SP 954770009741299 2 0 1
103 50453434 Familie Islami Ja Ja Nein IMG_1330.jpg Nein Familie Islami Zineta.islami@gmx.de KF7CNCYZ 620178179159158 2 0 0
104 50452019 Familie Karayilan Ja Ja Nein IMG_0200.jpg Nein Familie Karayilan yasemin.karayilan@yahoo.de 6H73JV6B 765446752804075 1 0 0
105 50453439 Familie Karpenko Ja Ja Nein IMG_2825.jpg Nein Familie Karpenko denis.k88@web.de LR87SQ8C 963707649418838 1 0 0
106 50453495 Familie Karyakina Ja Ja Nein IMG_0800.jpg Nein Familie Karyakina veronika20@hotmail.de RYK4BQLQ 638219110542782 1 0 0
107 50453488 Familie Ketterfeld Ja Ja Nein IMG_1211.jpg Nein Familie Ketterfeld madlen.katterfeld@gmx.de PLX9G4V3 117752011222601 6 0 1
108 50453446 Familie Klyszcz Ja Ja Nein IMG_2740.jpg Nein Familie Klyszcz klyszcz.ewa92@gmail.com LZR8WFP9 874820410323668 9 0 1
109 50452151 Familie Misiano Ja Ja Nein IMG_1577.jpg Nein Familie Misiano bonifati@hotmail.de 7ZW9V666 394112462489259 5 0 1
110 50451284 Familie Nickl Ja Ja Nein IMG_1054.jpg Nein Familie Nickl cela990@hotmail.com 35CH589Q 438824910404667 8 0 1
111 50452230 Familie Roder Ja Ja Nein IMG_1138.jpg Nein Familie Roder stephanie.roder@gmail.com 848D3SWY 745487201848290 6 0 0
112 50452913 Familie Rubinstein Ja Ja Nein IMG_0041.jpg Nein Familie Rubinstein n.d.rubinstein@googlemail.com G9H8YFC4 180523151134386 1 0 1
113 50453768 Familie Schillinger Ja Ja Nein IMG_1404.jpg Nein Familie Schillinger schneggeno1@web.de SQJSP49C 593384265020703 3 0 1
114 50452236 Familie Schlesinger Ja Ja Nein IMG_1512.jpg Nein Familie Schlesinger stefanie2011@gmx.net 946G6HJH 269413107409936 3 0 1
115 50453894 Familie Schmid Ja Ja Nein IMG_0625.jpg Nein Familie Schmid izuther@googlemail.com W7W8P32C 486167508950250 4 0 1
116 50452248 Familie Schreibauer Ja Ja Nein IMG_0671.jpg Nein Familie Schreibauer a.schreibauer@gmail.com 97R4TRBC 130440825414681 3 0 0
117 50452273 Familie Stachanczyk Ja Ja Nein IMG_9974.jpg Nein Familie Stachanczyk Suzanna.Stachanczyk@web.de C334SSSL 733864213043388 5 0 0
118 50453485 Familie Torres Ja Ja Nein IMG_0967.jpg Nein Familie Torres ftorrestapia@me.com PCQ4CNV9 553742663210606 4 0 0
119 50452252 Familie Tuldi Ja Ja Nein IMG_2788.jpg Nein Familie Tuldi olga_tuldi@yahoo.de B99BYYYF 657381798122682 11 0 0
120 50452022 Familie Weber Ja Ja Nein IMG_0984.jpg Nein Familie Weber mail.weber.melanie@googlemail.com 7954G4C5 820357028620317 3 0 1
121 50451320 Familie Welz Ja Ja Nein IMG_1459.jpg Nein Familie Welz eva_welz@gmx.de 69N5WYFK 952025141929986 3 0 1
122 50452419 Familie Wild Ja Ja Nein IMG_0539.jpg Nein Familie Wild wildramona@gmx.de DVXKKJCZ 789239059675168 9 0 1
123 50452462 Familie Wolf Ja Ja Nein IMG_0841.jpg Nein Familie Wolf anjamichi77@gmail.com FXPLQYH9 784676508389646 1 0 0
124 50410050 Jonas Wolf Ja Ja Nein IMG_8489.jpg Nein Familie Wolf anjamichi77@gmail.com Q52NYB4N 18810570193338 0 0 0
125 50453882 Familie Yilmaz Ja Ja Nein IMG_9907.jpg Nein Familie Yilmaz merve-ymz@hotmail.com TXK86QSB 467856804734432 4 0 0
126 49655787 Joseph Wild Spatzen Ja Ja Nein IMG_7119.jpg Nein Familie Wild wildramona@gmx.de 8C56662R 226166391326912 5 0 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -58,6 +58,22 @@ Das System läuft stabil und ist für den Produktivbetrieb vorbereitet. Wesentli
### 5. DuckDNS & DNS Monitor
* **Erfolgreich reaktiviert:** Der DynDNS-Service läuft und aktualisiert die IP, die Netzwerk-Konnektivität ist stabil.
### 6. Fotograf.de Scraper (In Development)
* **Architektur:** Ein neuer, eigenständiger Microservice wurde unter `/fotograf-de-scraper` angelegt. FastAPI-Backend (Python/Selenium) + React-Frontend (TypeScript/Vite/Tailwind).
* **Status (20. März 2026):**
* **Grundgerüst & Login:** Erfolgreich implementiert. Der Selenium-Bot loggt sich stabil ein.
* **Auftragsliste:** Das Abrufen und Cachen der Aufträge (`/config_jobs/index`) im Frontend funktioniert.
* **Feature 1 (Teilnehmerliste PDF): ERFOLGREICH.** Der Scraper navigiert zum Auftrag, lädt die CSV-Anmeldeliste versteckt herunter, formatiert sie via WeasyPrint/Jinja2 (Logik aus List-Generator übernommen) und liefert das finale PDF an den Browser aus.
* **Frontend UI:** Erste Version eines kachelbasierten Dashboards mit Detail-Modal implementiert. (Benötigt weiteres Feintuning nach User-Feedback).
* **Kritischer Blocker (Infrastruktur):**
* **Datenbank-Persistenz fehlerhaft:** Trotz Umstellung der `docker-compose.yml` auf lokale Bind-Mounts (`./company-explorer/data:/data`, etc.) sind die SQLite-Datenbanken nach einem Container-Rebuild (`--force-recreate`) leer.
* *Verdacht:* Rechteprobleme auf der Synology Diskstation oder Initialisierungsskripte im Container, die die DB bei jedem Start überschreiben. **Muss offline vom User auf Host-Ebene geprüft werden.**
* **Nächste Schritte (Für die nächste Session):**
1. **Infrastruktur:** Verifizierung, dass das DB-Persistenz-Problem gelöst ist.
2. **Feature 4 (Statistik):** Reaktivierung der bestehenden Skript-Logik (`process_statistics_mode`) zur Auswertung der Verkaufszahlen. Integration in einen neuen API-Endpunkt und Anzeige im Frontend-Modal.
3. **Feature 3 (Nachfass-Emails):** Reaktivierung der bestehenden Logik (`process_reminder_mode`).
4. **Feature 2 (QR-Karten):** Backend-Endpunkt zur PDF-Manipulation (Overlay von Text) basierend auf CSV/Calendly-Daten.
---
## Git Workflow & Conventions

25
check_db_links.py Normal file
View File

@@ -0,0 +1,25 @@
import sqlite3
import os
db_path = "/app/fotograf-de-scraper/backend/data/fotograf_jobs.db"
if not os.path.exists(db_path):
print(f"Database not found at {db_path}")
else:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Check candidates missing links for the current job
job_id = "576228454"
cursor.execute("""
SELECT COUNT(*)
FROM job_participants
WHERE job_id = ?
AND has_orders = 0
AND digital_package_ordered = 0
AND logins <= 5
AND quick_login_url IS NULL
""", (job_id,))
missing = cursor.fetchone()[0]
print(f"Missing links for candidates in job {job_id}: {missing}")
conn.close()

9
check_tables.py Normal file
View File

@@ -0,0 +1,9 @@
import sqlite3
db_path = "/app/fotograf-de-scraper/backend/data/fotograf_jobs.db"
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = cursor.fetchall()
print(f"Tables: {[t[0] for t in tables]}")
conn.close()

View File

@@ -133,18 +133,18 @@
</header>
<div class="container">
<!-- B2B Marketing Assistant -->
<!-- B2B Marketing Assistant (Inactive)
<div class="card">
<span class="card-icon">🚀</span>
<h2>B2B Marketing Assistant</h2>
<p>
KI-gestützte Analyse von Unternehmens-Websites zur Erstellung von Personas, Pain-Points und Marketing-Botschaften.
</p>
<!-- WICHTIG: Relativer Link für Reverse Proxy -->
<a href="/b2b/" class="btn">Starten &rarr;</a>
</div>
-->
<!-- General Market Intelligence -->
<!-- General Market Intelligence (Inactive)
<div class="card">
<span class="card-icon">📊</span>
<h2>Market Intelligence</h2>
@@ -152,22 +152,22 @@
Allgemeine Marktanalyse und Recherche-Tool.
Nutzt Web-Scraping und KI für tiefe Einblicke.
</p>
<!-- WICHTIG: Relativer Link für Reverse Proxy -->
<a href="/market/" class="btn">Starten &rarr;</a>
</div>
-->
<!-- GTM Architect -->
<!-- GTM Architect (Inactive)
<div class="card">
<span class="card-icon">🏛️</span>
<h2>GTM Architect</h2>
<p>
Entwickelt eine komplette Go-to-Market-Strategie für neue technische Produkte, von der Analyse bis zum Sales-Kit.
</p>
<!-- WICHTIG: Relativer Link für Reverse Proxy -->
<a href="/gtm/" class="btn">Starten &rarr;</a>
</div>
-->
<!-- Content Engine -->
<!-- Content Engine (Inactive)
<div class="card">
<span class="card-icon">✍️</span>
<h2>Content Engine</h2>
@@ -176,19 +176,19 @@
</p>
<a href="/content/" class="btn">Starten &rarr;</a>
</div>
-->
<!-- Company Explorer (Robotics) -->
<!-- Company Explorer -->
<div class="card">
<span class="card-icon">🤖</span>
<h2>Company Explorer</h2>
<p>
Das zentrale CRM-Data-Mining Tool. Importieren, Deduplizieren und Anreichern von Firmenlisten mit Fokus auf Robotik-Potential.
</p>
<!-- Jetzt direkt zum Frontend -->
<a href="/ce/" class="btn">Starten &rarr;</a>
</div>
<!-- Competitor Analysis Agent -->
<!-- Competitor Analysis Agent (Inactive)
<div class="card">
<span class="card-icon">⚔️</span>
<h2>Competitor Analysis</h2>
@@ -197,8 +197,9 @@
</p>
<a href="/competitor/" class="btn">Starten &rarr;</a>
</div>
-->
<!-- Lead Engine: TradingTwins -->
<!-- Lead Engine: TradingTwins (Inactive)
<div class="card">
<span class="card-icon">📈</span>
<h2>Lead Engine: TradingTwins</h2>
@@ -207,6 +208,7 @@
</p>
<a href="/lead/" class="btn" target="_blank">Starten &rarr;</a>
</div>
-->
<!-- Meeting Assistant (Transcription) -->
<div class="card">
@@ -218,7 +220,7 @@
<a href="/tr/" class="btn">Starten &rarr;</a>
</div>
<!-- Heatmap Tool -->
<!-- Heatmap Tool (Inactive)
<div class="card">
<span class="card-icon">🗺️</span>
<h2>Heatmap Tool</h2>
@@ -227,10 +229,21 @@
</p>
<a href="/heatmap/" class="btn">Starten &rarr;</a>
</div>
-->
<!-- Fotograf.de Scraper -->
<div class="card">
<span class="card-icon">📸</span>
<h2>Fotograf.de ERP</h2>
<p>
Automatisierter Workflow zum Download und Formatieren der Anmeldelisten von fotograf.de als sortiertes PDF.
</p>
<a href="/fotograf-de/" class="btn">Starten &rarr;</a>
</div>
</div>
<footer>
&copy; 2025 Local AI Suite | Secured Access
&copy; 2026 Local AI Suite | Secured Access
</footer>
</body>
</html>

View File

@@ -394,6 +394,9 @@ def select_project(token: str) -> Optional[Tuple[Dict, Optional[str]]]:
print("Keine Projekte in der Datenbank gefunden.")
return None, None
# Sortiere Projekte alphabetisch nach Titel
projects.sort(key=lambda p: get_page_title(p).lower())
print("\nAn welchem Projekt möchtest du arbeiten?")
for i, project in enumerate(projects):
print(f"[{i+1}] {get_page_title(project)}")
@@ -428,6 +431,7 @@ def select_task(token: str, project_id: str, tasks_db_id: str) -> Optional[Dict]
print(f"[{i+1}] {get_page_title(task)}")
print(f"[{len(tasks)+1}] Neuen Task für dieses Projekt erstellen")
print("[0] Zurück zur Projektauswahl")
while True:
try:
@@ -436,6 +440,8 @@ def select_task(token: str, project_id: str, tasks_db_id: str) -> Optional[Dict]
return tasks[choice - 1]
elif choice == len(tasks) + 1:
return {"id": "new_task"} # Signal
elif choice == 0:
return {"id": "go_back"} # Signal
else:
print("Ungültige Auswahl.")
except ValueError:
@@ -450,34 +456,6 @@ BERLIN_TZ = ZoneInfo("Europe/Berlin")
# --- Git Summary Generation ---
def generate_git_summary() -> Tuple[str, str]:
"""Generiert eine Zusammenfassung der Git-Änderungen und Commit-Nachrichten seit dem letzten Push zum Main-Branch."""
try:
# Finde den aktuellen Main-Branch Namen (master oder main)
try:
main_branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).decode("utf-8").strip()
if main_branch not in ["main", "master"]:
# Versuche, den Remote-Tracking-Branch für main/master zu finden
result = subprocess.run(["git", "branch", "-r"], capture_output=True, text=True)
if "origin/main" in result.stdout:
main_branch = "origin/main"
elif "origin/master" in result.stdout:
main_branch = "origin/master"
else:
print("Warnung: Konnte keinen 'main' oder 'master' Branch finden. Git-Zusammenfassung wird möglicherweise unvollständig sein.")
main_branch = "HEAD~1" # Fallback zum letzten Commit, falls kein Main-Branch gefunden wird
except subprocess.CalledProcessError:
main_branch = "HEAD~1" # Fallback, falls gar kein Branch gefunden wird
# Git log --pretty
commit_log_cmd = ["git", "log", "--pretty=format:- %s", f"{main_branch}...HEAD"]
commit_messages = subprocess.check_output(commit_log_cmd).decode("utf-8").strip()
return "", commit_messages
except subprocess.CalledProcessError as e:
print(f"❌ FEHLER beim Generieren der Git-Zusammenfassung: {e}")
return "", ""
def git_push_with_retry() -> bool:
"""Versucht, Änderungen zu pushen, und führt bei einem non-fast-forward-Fehler einen Rebase und erneuten Push durch."""
print("\n--- Führe git push aus ---")
@@ -527,7 +505,9 @@ def report_status_to_notion(
session_data = json.load(f)
task_id = session_data.get("task_id")
token = session_data.get("token")
readme_path = session_data.get("readme_path", "readme.md") # Lade den Readme-Pfad
readme_path = session_data.get("readme_path") # Lade den Readme-Pfad
if readme_path is None:
readme_path = "readme.md"
if not (task_id and token):
print("❌ FEHLER: Session-Daten unvollständig. Kann keinen Statusbericht erstellen.")
@@ -560,22 +540,13 @@ def report_status_to_notion(
except requests.exceptions.RequestException as e:
print(f"❌ FEHLER beim Abrufen der Task-Details für die Zeiterfassung: {e}")
print(f"--- Erstelle automatischen Statusbericht für Task {task_id} ---")
print(f"--- Erstelle Statusbericht für Task {task_id} ---")
# Git-Zusammenfassung generieren
print("Generiere Git-Zusammenfassung...")
_, commit_log = generate_git_summary()
actual_commit_messages = commit_log
# Summary automatisch aus Commit-Nachrichten erstellen
actual_summary = summary_override or "Keine Zusammenfassung angegeben."
if not summary_override:
_, commit_log = generate_git_summary()
summary_from_commits = commit_log.replace("- ", "").strip()
if summary_from_commits:
actual_summary = summary_from_commits
else:
actual_summary = "Keine neuen Commits in dieser Session."
# Bessere Lesbarkeit für Notion durch explizite Zeilenumbrüche vor nummerierten Listen
# Formatiert " 1. " zu einem Zeilenumbruch und einer Einrückung
formatted_summary = re.sub(r'\s+(\d+\.)\s', r'\n\n \1 ', actual_summary)
# Kommentar zusammenstellen
report_lines = []
@@ -584,7 +555,7 @@ def report_status_to_notion(
report_lines.append(f"Investierte Zeit in dieser Session: {elapsed_hhmm}")
report_lines.append("\nArbeitszusammenfassung:")
report_lines.append(actual_summary)
report_lines.append(formatted_summary)
report_content = "\n".join(report_lines)
@@ -598,6 +569,19 @@ def report_status_to_notion(
# Notion aktualisieren (nur Kommentar/Block, kein Status)
append_blocks_to_notion_page(token, task_id, notion_blocks)
# Update der Readme.md Datei
if readme_path and os.path.exists(readme_path):
print(f"Hänge Status-Update an {readme_path} an...")
try:
with open(readme_path, "a", encoding="utf-8") as rf:
rf.write(f"\n\n## 🤖 Status-Update ({timestamp} Berlin Time)\n")
rf.write(f"```yaml\n{report_content}\n```\n")
print(f"✅ Status-Update an '{readme_path}' angehängt.")
except Exception as e:
print(f"❌ FEHLER beim Anhängen an '{readme_path}': {e}")
else:
print(f"⚠️ Readme-Pfad '{readme_path}' nicht gefunden. Überspringe lokales Doku-Update.")
# --- Doku & Git Operationen ---
print("\n--- Führe Git-Operationen aus ---")
try:
@@ -652,7 +636,8 @@ def generate_cli_context(project_title: str, task_title: str, task_id: str, read
description_part = ""
if task_description:
description_part = (
f"\n**Aufgabenbeschreibung:**\n"
f"\n**Aufgabenbeschreibung (inkl. Historie):**\n"
f"*(Hinweis: Bei längeren Tasks befinden sich die aktuellsten und relevantesten Status-Updates am Ende dieser Beschreibung. Ursprüngliche Probleme sind möglicherweise bereits gelöst. Priorisiere immer die jüngsten Informationen!)*\n"
f"```\n{task_description}\n```\n"
)
@@ -748,30 +733,37 @@ def start_interactive_session():
print("Kein Token angegeben. Abbruch.")
return
selected_project, readme_path = select_project(token)
if not selected_project:
return
project_title = get_page_title(selected_project)
print(f"\nProjekt '{project_title}' ausgewählt.")
tasks_db_id = find_database_by_title(token, "Tasks [UT]")
if not tasks_db_id:
return
user_choice = select_task(token, selected_project["id"], tasks_db_id)
if not user_choice:
print("Kein Task ausgewählt. Abbruch.")
return
selected_task = None
if user_choice.get("id") == "new_task":
selected_task = create_new_notion_task(token, selected_project["id"], tasks_db_id)
if not selected_task:
while True:
selected_project, readme_path = select_project(token)
if not selected_project:
return
else:
selected_task = user_choice
project_title = get_page_title(selected_project)
print(f"\nProjekt '{project_title}' ausgewählt.")
tasks_db_id = find_database_by_title(token, "Tasks [UT]")
if not tasks_db_id:
return
user_choice = select_task(token, selected_project["id"], tasks_db_id)
if not user_choice:
print("Kein Task ausgewählt. Abbruch.")
return
if user_choice.get("id") == "go_back":
print("\nGehe zurück zur Projektauswahl...")
continue
selected_task = None
if user_choice.get("id") == "new_task":
selected_task = create_new_notion_task(token, selected_project["id"], tasks_db_id)
if not selected_task:
return
else:
selected_task = user_choice
break # Exit the loop once a valid task is selected
task_title = get_page_title(selected_task)
task_id = selected_task["id"]

58
docker-compose.ce-v2.yml Normal file
View File

@@ -0,0 +1,58 @@
# TEMPORARY DOCKER-COMPOSE FOR STARTING COMPANY-EXPLORER - V2
version: '3.8'
services:
# --- GATEKEEPER (NGINX) ---
nginx:
image: nginx:alpine
container_name: gateway_proxy
restart: unless-stopped
ports:
- "8090:80"
volumes:
- ./nginx-proxy-ce.conf:/etc/nginx/nginx.conf:ro # Use the cleaned config
- ./.htpasswd:/etc/nginx/.htpasswd:ro
depends_on:
dashboard:
condition: service_started
company-explorer:
condition: service_healthy
# --- DASHBOARD ---
dashboard:
image: nginx:alpine
container_name: dashboard
restart: unless-stopped
volumes:
- ./dashboard:/usr/share/nginx/html:ro
# --- APPS ---
company-explorer:
build:
context: ./company-explorer
dockerfile: Dockerfile
container_name: company-explorer
restart: unless-stopped
ports:
- "8000:8000"
environment:
API_USER: "admin"
API_PASSWORD: "gemini"
PYTHONUNBUFFERED: "1"
DATABASE_URL: "sqlite:////data/companies_v3_fixed_2.db"
GEMINI_API_KEY: "${GEMINI_API_KEY}"
SERP_API_KEY: "${SERP_API}"
NOTION_TOKEN: "${NOTION_API_KEY}"
volumes:
- ./company-explorer:/app
- explorer_db_data:/data
- ./Log_from_docker:/app/logs_debug
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/docs"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
explorer_db_data: {}

66
docker-compose.ce.yml Normal file
View File

@@ -0,0 +1,66 @@
# TEMPORARY DOCKER-COMPOSE FOR STARTING COMPANY-EXPLORER
version: '3.8'
services:
# --- GATEKEEPER (NGINX) ---
nginx:
image: nginx:alpine
container_name: gateway_proxy
restart: unless-stopped
ports:
- "8090:80"
volumes:
- ./nginx-proxy-clean.conf:/etc/nginx/nginx.conf:ro
- ./.htpasswd:/etc/nginx/.htpasswd:ro
depends_on:
dashboard:
condition: service_started
company-explorer:
condition: service_healthy
# --- DASHBOARD ---
dashboard:
image: nginx:alpine
container_name: dashboard
restart: unless-stopped
volumes:
- ./dashboard:/usr/share/nginx/html:ro
# --- APPS ---
company-explorer:
build:
context: ./company-explorer
dockerfile: Dockerfile
container_name: company-explorer
restart: unless-stopped
ports:
- "8000:8000"
environment:
API_USER: "admin"
API_PASSWORD: "gemini"
PYTHONUNBUFFERED: "1"
DATABASE_URL: "sqlite:////data/companies_v3_fixed_2.db"
GEMINI_API_KEY: "${GEMINI_API_KEY}"
SERP_API_KEY: "${SERP_API}"
NOTION_TOKEN: "${NOTION_API_KEY}"
volumes:
- ./company-explorer:/app
- explorer_db_data:/data
- ./Log_from_docker:/app/logs_debug
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/docs"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
connector_db_data: {}
explorer_db_data: {}
lead_engine_data: {}
gtm_architect_data: {}
b2b_marketing_data: {}
transcription_uploads: {}
content_engine_data: {}
competitor_analysis_data: {}
market_intel_data: {}

View File

@@ -0,0 +1,96 @@
version: '3.8'
services:
# --- GATEKEEPER (NGINX) ---
nginx:
image: nginx:alpine
container_name: gateway_proxy
restart: unless-stopped
ports:
- "8090:80"
volumes:
- ./nginx-proxy.minimal.conf:/etc/nginx/nginx.conf:ro
- ./.htpasswd:/etc/nginx/.htpasswd:ro
depends_on:
company-explorer:
condition: service_healthy
lead-engine:
condition: service_started
transcription-tool:
condition: service_started
# --- APPS ---
company-explorer:
build:
context: ./company-explorer
dockerfile: Dockerfile
container_name: company-explorer
restart: unless-stopped
ports:
- "8000:8000"
environment:
API_USER: "admin"
API_PASSWORD: "gemini"
PYTHONUNBUFFERED: "1"
DATABASE_URL: "sqlite:////data/companies_v3_fixed_2.db"
GEMINI_API_KEY: "${GEMINI_API_KEY}"
SERP_API_KEY: "${SERP_API}"
NOTION_TOKEN: "${NOTION_API_KEY}"
volumes:
- ./company-explorer:/app
- explorer_db_data:/data
- ./Log_from_docker:/app/logs_debug
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/docs"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
lead-engine:
build:
context: ./lead-engine
dockerfile: Dockerfile
container_name: lead-engine
restart: unless-stopped
ports:
- "8501:8501" # UI (Streamlit)
- "8004:8004" # API / Monitor
- "8099:8004" # Direct Test Port
environment:
PYTHONUNBUFFERED: "1"
GEMINI_API_KEY: "${GEMINI_API_KEY}"
SERP_API: "${SERP_API}"
INFO_Application_ID: "${INFO_Application_ID}"
INFO_Tenant_ID: "${INFO_Tenant_ID}"
INFO_Secret: "${INFO_Secret}"
CAL_APPID: "${CAL_APPID}"
CAL_SECRET: "${CAL_SECRET}"
CAL_TENNANT_ID: "${CAL_TENNANT_ID}"
TEAMS_WEBHOOK_URL: "${TEAMS_WEBHOOK_URL}"
FEEDBACK_SERVER_BASE_URL: "${FEEDBACK_SERVER_BASE_URL}"
WORDPRESS_BOOKING_URL: "${WORDPRESS_BOOKING_URL}"
MS_BOOKINGS_URL: "${MS_BOOKINGS_URL}"
volumes:
- ./lead-engine:/app
- lead_engine_data:/app/data
transcription-tool:
build:
context: ./transcription-tool
dockerfile: Dockerfile
container_name: transcription-tool
restart: unless-stopped
ports:
- "8001:8001"
environment:
GEMINI_API_KEY: "${GEMINI_API_KEY}"
UPLOAD_DIR: "/app/uploads"
volumes:
- transcription_uploads:/app/uploads
- ./Log_from_docker:/app/logs_debug
volumes:
explorer_db_data: {}
lead_engine_data: {}
transcription_uploads: {}

View File

@@ -0,0 +1,11 @@
version: '3.8'
services:
nginx:
depends_on:
dashboard:
condition: service_started
company-explorer:
condition: service_healthy
lead-engine:
condition: service_started

89
docker-compose.test.yml Normal file
View File

@@ -0,0 +1,89 @@
version: '3.8'
services:
# --- GATEKEEPER (NGINX) ---
nginx:
image: nginx:alpine
container_name: gateway_proxy
restart: unless-stopped
ports:
- "8090:80"
volumes:
- ./nginx-proxy-test.conf:/etc/nginx/nginx.conf:ro
- ./.htpasswd:/etc/nginx/.htpasswd:ro
depends_on:
dashboard:
condition: service_started
company-explorer:
condition: service_healthy
lead-engine:
condition: service_started
# --- DASHBOARD (Required by Nginx) ---
dashboard:
image: nginx:alpine
container_name: dashboard
restart: unless-stopped
volumes:
- ./dashboard:/usr/share/nginx/html:ro
# --- COMPANY-EXPLORER (Required by Lead-Engine) ---
company-explorer:
build:
context: ./company-explorer
dockerfile: Dockerfile
container_name: company-explorer
restart: unless-stopped
ports:
- "8000:8000"
environment:
API_USER: "admin"
API_PASSWORD: "gemini"
PYTHONUNBUFFERED: "1"
DATABASE_URL: "sqlite:////data/companies_v3_fixed_2.db"
GEMINI_API_KEY: "${GEMINI_API_KEY}"
SERP_API_KEY: "${SERP_API}"
NOTION_TOKEN: "${NOTION_API_KEY}"
volumes:
- ./company-explorer:/app
- explorer_db_data:/data
- ./Log_from_docker:/app/logs_debug
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/docs"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# --- LEAD-ENGINE (Our Webhook Service) ---
lead-engine:
build:
context: ./lead-engine
dockerfile: Dockerfile
container_name: lead-engine
restart: unless-stopped
ports:
- "8501:8501" # UI (Streamlit)
- "8004:8004" # API / Monitor
- "8099:8004" # Direct Test Port
environment:
PYTHONUNBUFFERED: "1"
GEMINI_API_KEY: "${GEMINI_API_KEY}"
SERP_API: "${SERP_API}"
INFO_Application_ID: "${INFO_Application_ID}"
INFO_Tenant_ID: "${INFO_Tenant_ID}"
INFO_Secret: "${INFO_Secret}"
CAL_APPID: "${CAL_APPID}"
CAL_SECRET: "${CAL_SECRET}"
CAL_TENNANT_ID: "${CAL_TENNANT_ID}"
TEAMS_WEBHOOK_URL: "${TEAMS_WEBHOOK_URL}"
FEEDBACK_SERVER_BASE_URL: "${FEEDBACK_SERVER_BASE_URL}"
WORDPRESS_BOOKING_URL: "${WORDPRESS_BOOKING_URL}"
MS_BOOKINGS_URL: "${MS_BOOKINGS_URL}"
volumes:
- ./lead-engine:/app
- lead_engine_data:/app/data
volumes:
explorer_db_data: {}
lead_engine_data: {}

20
docker-compose.tr.yml Normal file
View File

@@ -0,0 +1,20 @@
version: '3.8'
services:
transcription-tool:
build:
context: ./transcription-tool
dockerfile: Dockerfile
container_name: transcription-tool-standalone
restart: unless-stopped
ports:
- "8001:8001"
environment:
GEMINI_API_KEY: "${GEMINI_API_KEY}"
UPLOAD_DIR: "/app/uploads"
volumes:
- transcription_uploads:/app/uploads
- ./Log_from_docker:/app/logs_debug
volumes:
transcription_uploads: {}

View File

@@ -19,23 +19,25 @@ services:
condition: service_started
company-explorer:
condition: service_healthy
connector-superoffice:
condition: service_healthy
lead-engine:
condition: service_started
gtm-architect:
condition: service_started
b2b-marketing-assistant:
condition: service_started
# connector-superoffice:
# condition: service_healthy
# lead-engine:
# condition: service_started
# gtm-architect:
# condition: service_started
# b2b-marketing-assistant:
# condition: service_started
transcription-tool:
condition: service_started
heatmap-frontend:
condition: service_started
competitor-analysis:
condition: service_started
content-engine:
condition: service_started
market-intelligence:
# heatmap-frontend:
# condition: service_started
# competitor-analysis:
# condition: service_started
# content-engine:
# condition: service_started
# market-intelligence:
# condition: service_started
fotograf-de-scraper-frontend:
condition: service_started
# --- DASHBOARD ---
@@ -47,60 +49,60 @@ services:
- ./dashboard:/usr/share/nginx/html:ro
# --- APPS ---
market-intelligence:
build:
context: .
dockerfile: general-market-intelligence/Dockerfile.fullstack
container_name: market-intelligence
restart: unless-stopped
ports:
- "8098:3001"
environment:
GEMINI_API_KEY: "${GEMINI_API_KEY}"
SERP_API_KEY: "${SERP_API}"
PYTHONUNBUFFERED: "1"
volumes:
- market_intel_data:/data
- ./Log_from_docker:/app/Log
# market-intelligence:
# build:
# context: .
# dockerfile: general-market-intelligence/Dockerfile.fullstack
# container_name: market-intelligence
# restart: unless-stopped
# ports:
# - "8098:3001"
# environment:
# GEMINI_API_KEY: "${GEMINI_API_KEY}"
# SERP_API_KEY: "${SERP_API}"
# PYTHONUNBUFFERED: "1"
# volumes:
# - market_intel_data:/data
# - ./Log_from_docker:/app/Log
content-engine:
build:
context: .
dockerfile: content-engine/Dockerfile
container_name: content-engine
restart: unless-stopped
ports:
- "8093:3000"
environment:
GEMINI_API_KEY: "${GEMINI_API_KEY}"
PYTHONUNBUFFERED: "1"
GTM_DB_PATH: "/gtm_data/gtm_projects.db"
CONTENT_DB_PATH: "/data/content_engine.db"
volumes:
- content_engine_data:/data
- gtm_architect_data:/gtm_data:ro
- ./Log_from_docker:/app/logs_debug
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3006"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# content-engine:
# build:
# context: .
# dockerfile: content-engine/Dockerfile
# container_name: content-engine
# restart: unless-stopped
# ports:
# - "8093:3000"
# environment:
# GEMINI_API_KEY: "${GEMINI_API_KEY}"
# PYTHONUNBUFFERED: "1"
# GTM_DB_PATH: "/gtm_data/gtm_projects.db"
# CONTENT_DB_PATH: "/data/content_engine.db"
# volumes:
# - content_engine_data:/data
# - gtm_architect_data:/gtm_data:ro
# - ./Log_from_docker:/app/logs_debug
# healthcheck:
# test: ["CMD", "curl", "-f", "http://localhost:3006"]
# interval: 10s
# timeout: 5s
# retries: 5
# start_period: 30s
competitor-analysis:
build:
context: ./competitor-analysis-app
dockerfile: Dockerfile
container_name: competitor-analysis
restart: unless-stopped
ports:
- "8097:3000"
environment:
GEMINI_API_KEY: "${GEMINI_API_KEY}"
PYTHONUNBUFFERED: "1"
volumes:
- competitor_analysis_data:/data
- ./Log_from_docker:/app/logs_debug
# competitor-analysis:
# build:
# context: ./competitor-analysis-app
# dockerfile: Dockerfile
# container_name: competitor-analysis
# restart: unless-stopped
# ports:
# - "8097:3000"
# environment:
# GEMINI_API_KEY: "${GEMINI_API_KEY}"
# PYTHONUNBUFFERED: "1"
# volumes:
# - competitor_analysis_data:/data
# - ./Log_from_docker:/app/logs_debug
transcription-tool:
build:
@@ -117,58 +119,58 @@ services:
- transcription_uploads:/app/uploads
- ./Log_from_docker:/app/logs_debug
heatmap-backend:
build:
context: ./heatmap-tool/backend
container_name: heatmap-backend
restart: unless-stopped
ports:
- "8002:8000"
environment:
ORS_API_KEY: "${ORS_API_KEY}"
PYTHONUNBUFFERED: "1"
# heatmap-backend:
# build:
# context: ./heatmap-tool/backend
# container_name: heatmap-backend
# restart: unless-stopped
# ports:
# - "8002:8000"
# environment:
# ORS_API_KEY: "${ORS_API_KEY}"
# PYTHONUNBUFFERED: "1"
heatmap-frontend:
build:
context: ./heatmap-tool/frontend
dockerfile: Dockerfile
container_name: heatmap-frontend
restart: unless-stopped
ports:
- "8096:80"
depends_on:
- heatmap-backend
# heatmap-frontend:
# build:
# context: ./heatmap-tool/frontend
# dockerfile: Dockerfile
# container_name: heatmap-frontend
# restart: unless-stopped
# ports:
# - "8096:80"
# depends_on:
# - heatmap-backend
b2b-marketing-assistant:
build:
context: .
dockerfile: b2b-marketing-assistant/Dockerfile
container_name: b2b-marketing-assistant
restart: unless-stopped
ports:
- "8092:3002"
environment:
GEMINI_API_KEY: "${GEMINI_API_KEY}"
PYTHONUNBUFFERED: "1"
volumes:
- b2b_marketing_data:/data
- ./Log_from_docker:/app/logs_debug
# b2b-marketing-assistant:
# build:
# context: .
# dockerfile: b2b-marketing-assistant/Dockerfile
# container_name: b2b-marketing-assistant
# restart: unless-stopped
# ports:
# - "8092:3002"
# environment:
# GEMINI_API_KEY: "${GEMINI_API_KEY}"
# PYTHONUNBUFFERED: "1"
# volumes:
# - b2b_marketing_data:/data
# - ./Log_from_docker:/app/logs_debug
gtm-architect:
build:
context: .
dockerfile: gtm-architect/Dockerfile
container_name: gtm-architect
restart: unless-stopped
ports:
- "8094:80"
environment:
GEMINI_API_KEY: "${GEMINI_API_KEY}"
VITE_API_BASE_URL: "/gtm/api"
GTM_DB_PATH: "/data/gtm_projects.db"
volumes:
- ./Log_from_docker:/app/logs_debug
- gtm_architect_data:/data
# gtm-architect:
# build:
# context: .
# dockerfile: gtm-architect/Dockerfile
# container_name: gtm-architect
# restart: unless-stopped
# ports:
# - "8094:80"
# environment:
# GEMINI_API_KEY: "${GEMINI_API_KEY}"
# VITE_API_BASE_URL: "/gtm/api"
# GTM_DB_PATH: "/data/gtm_projects.db"
# volumes:
# - ./Log_from_docker:/app/logs_debug
# - gtm_architect_data:/data
company-explorer:
build:
@@ -188,7 +190,7 @@ services:
NOTION_TOKEN: "${NOTION_API_KEY}"
volumes:
- ./company-explorer:/app
- explorer_db_data:/data
- ./company-explorer/data:/data # Local bind mount for guaranteed persistence
- ./Log_from_docker:/app/logs_debug
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/docs"]
@@ -197,69 +199,92 @@ services:
retries: 5
start_period: 30s
connector-superoffice:
# connector-superoffice:
# build:
# context: ./connector-superoffice
# dockerfile: Dockerfile
# container_name: connector-superoffice
# restart: unless-stopped
# ports:
# - "8003:8000"
# volumes:
# - ./connector-superoffice:/app
# - ./connector-superoffice/data:/data # Persistent local DB storage
# environment:
# PYTHONUNBUFFERED: "1"
# GEMINI_API_KEY: "${GEMINI_API_KEY}"
# SO_CLIENT_ID: "${SO_CLIENT_ID}"
# SO_CLIENT_SECRET: "${SO_CLIENT_SECRET}"
# SO_REFRESH_TOKEN: "${SO_REFRESH_TOKEN}"
# SO_ENVIRONMENT: "${SO_ENVIRONMENT}"
# SO_CONTEXT_IDENTIFIER: "${SO_CONTEXT_IDENTIFIER}"
# WEBHOOK_TOKEN: "${WEBHOOK_TOKEN}"
# WEBHOOK_SECRET: "${WEBHOOK_SECRET}"
# healthcheck:
# test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
# interval: 10s
# timeout: 5s
# retries: 5
# start_period: 30s
# lead-engine:
# build:
# context: ./lead-engine
# dockerfile: Dockerfile
# container_name: lead-engine
# restart: unless-stopped
# ports:
# - "8501:8501" # UI (Streamlit)
# - "8004:8004" # API / Monitor
# - "8099:8004" # Direct Test Port
# environment:
# PYTHONUNBUFFERED: "1"
# GEMINI_API_KEY: "${GEMINI_API_KEY}"
# SERP_API: "${SERP_API}"
# INFO_Application_ID: "${INFO_Application_ID}"
# INFO_Tenant_ID: "${INFO_Tenant_ID}"
# INFO_Secret: "${INFO_Secret}"
# CAL_APPID: "${CAL_APPID}"
# CAL_SECRET: "${CAL_SECRET}"
# CAL_TENNANT_ID: "${CAL_TENNANT_ID}"
# TEAMS_WEBHOOK_URL: "${TEAMS_WEBHOOK_URL}"
# FEEDBACK_SERVER_BASE_URL: "${FEEDBACK_SERVER_BASE_URL}"
# WORDPRESS_BOOKING_URL: "${WORDPRESS_BOOKING_URL}"
# MS_BOOKINGS_URL: "${MS_BOOKINGS_URL}"
# volumes:
# - ./lead-engine:/app
# - ./lead-engine/data:/app/data # Local persistent database
fotograf-de-scraper-backend:
build:
context: ./connector-superoffice
context: ./fotograf-de-scraper/backend
dockerfile: Dockerfile
container_name: connector-superoffice
restart: unless-stopped
ports:
- "8003:8000"
volumes:
- ./connector-superoffice:/app
- connector_db_data:/data
container_name: fotograf-de-scraper-backend
env_file:
- ./fotograf-de-scraper/.env
environment:
PYTHONUNBUFFERED: "1"
GEMINI_API_KEY: "${GEMINI_API_KEY}"
SO_CLIENT_ID: "${SO_CLIENT_ID}"
SO_CLIENT_SECRET: "${SO_CLIENT_SECRET}"
SO_REFRESH_TOKEN: "${SO_REFRESH_TOKEN}"
SO_ENVIRONMENT: "${SO_ENVIRONMENT}"
SO_CONTEXT_IDENTIFIER: "${SO_CONTEXT_IDENTIFIER}"
WEBHOOK_TOKEN: "${WEBHOOK_TOKEN}"
WEBHOOK_SECRET: "${WEBHOOK_SECRET}"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
- TZ=Europe/Berlin
ports:
- "8002:8000"
volumes:
- ./fotograf-de-scraper/backend:/app
- ./fotograf-de-scraper/backend/data:/app/data
restart: unless-stopped
lead-engine:
fotograf-de-scraper-frontend:
build:
context: ./lead-engine
context: ./fotograf-de-scraper/frontend
dockerfile: Dockerfile
container_name: lead-engine
restart: unless-stopped
args:
VITE_API_BASE_URL: "http://192.168.178.6:8002"
container_name: fotograf-de-scraper-frontend
ports:
- "8501:8501" # UI (Streamlit)
- "8004:8004" # API / Monitor
- "8099:8004" # Direct Test Port
environment:
PYTHONUNBUFFERED: "1"
GEMINI_API_KEY: "${GEMINI_API_KEY}"
SERP_API: "${SERP_API}"
INFO_Application_ID: "${INFO_Application_ID}"
INFO_Tenant_ID: "${INFO_Tenant_ID}"
INFO_Secret: "${INFO_Secret}"
CAL_APPID: "${CAL_APPID}"
CAL_SECRET: "${CAL_SECRET}"
CAL_TENNANT_ID: "${CAL_TENNANT_ID}"
TEAMS_WEBHOOK_URL: "${TEAMS_WEBHOOK_URL}"
FEEDBACK_SERVER_BASE_URL: "${FEEDBACK_SERVER_BASE_URL}"
WORDPRESS_BOOKING_URL: "${WORDPRESS_BOOKING_URL}"
MS_BOOKINGS_URL: "${MS_BOOKINGS_URL}"
volumes:
- ./lead-engine:/app
- lead_engine_data:/app/data
# --- INFRASTRUCTURE SERVICES ---
- "3009:80"
depends_on:
- fotograf-de-scraper-backend
restart: unless-stopped
volumes:
connector_db_data: {}
explorer_db_data: {}
lead_engine_data: {}
gtm_architect_data: {}
b2b_marketing_data: {}
transcription_uploads: {}

View File

@@ -0,0 +1,70 @@
# 🎤 Executive Briefing Guide: Sales Dashboard "Intelligence & yield"
Dieses Dokument dient als strategischer Leitfaden und Sprecherskript für die Vorstandspräsentation. Es enthält alle Hintergrundinformationen und Datenpunkte, um die Präsentation auf einem externen System ohne Dashboard-Zugriff zu finalisieren.
---
## 1. Strategischer Kontext (Das Mindset)
**Kern-Metapher:** "Vom Wiegen wird die Sau nicht fetter."
Wir präsentieren dem Vorstand keine "bunte Statistik". Wir präsentieren ein **aktives Steuerungsinstrument**.
* **Problem:** SuperOffice (SO) ist ein passives Archiv. Daten liegen dort in Silos, Korrelationen (z.B. "Welche Aktivität führt zum Abschluss?") erfordern Tage in Excel.
* **Lösung:** Das Dashboard macht sich "die Hände schmutzig". Es taucht in die Datentiefe ab, verbindet CRM-Aktivitäten mit echten Angebotspositionen (SKUs) und liefert Antworten auf Knopfdruck.
* **Ziel:** Wir messen nicht des Messens wegen, sondern um eine Basis für strategische Entscheidungen zu legen und den Erfolg von Maßnahmen (wie der Vor-Ort-Fokussierung) im Nachhinein hart zu belegen.
---
## 2. Sprecherskript (Phasen-Leitfaden)
### Phase 1: Die Vision (Intro)
> "Herr Vorstand, in SuperOffice verwalten wir Kontakte. In diesem Dashboard steuern wir den Erfolg. Wir haben aufgehört zu raten und angefangen zu wissen. Unser Motto: Vom Wiegen allein wird die Sau nicht fetter wir messen hier, um den Vertrieb proaktiv zum Yield zu führen."
### Phase 2: Risk Management (Clean Pipeline)
> "In SuperOffice sieht unsere Pipeline oft gewaltig aus 5,2 Mio. Euro. Aber das ist die 'Brutto-Hoffnung'. Unser Dashboard erkennt automatisch den 'Duplicate Bloat' also Angebotsvarianten für denselben Kunden. Wir ziehen den Vorhang beiseite und sehen die 'Clean Pipeline' von 3,6 Mio. Euro. Das ist die reale Wahrheit, auf der wir Investitionen planen können."
* **Key Fact:** ~31% der Pipeline in SO sind oft nur redundante Varianten (Bloat).
### Phase 3: Die Winning DNA (Yield-Beleg)
> "Wir wissen jetzt exakt, was funktioniert. Wer sich die Hände schmutzig macht und in die Daten eintaucht, findet Gold: Ein einziger Vor-Ort-Termin steigert die Abschlusswahrscheinlichkeit um 159%. Wir steuern unsere Sales-Manager jetzt nicht mehr nach 'Gefühl', sondern schicken sie dorthin, wo der Yield bewiesen ist."
* **Key Fact:** Win-Rate steigt von ~9% auf ~24% bei Demos.
* **Key Fact:** Follow-Up Speed von 37 Tagen auf 10 Tage gesenkt.
### Phase 4: Action Hub (Der 20% Hebel)
> "Das Dashboard ist kein Rückspiegel, es ist ein Gaspedal. Über den 'Magic Wand' generiert der Manager mit einem Klick eine Rückrufbitte, die die komplette CRM-Historie kennt. Wir haben den ROI gemessen: Jede fünfte Nachricht führt zu einer direkten Kundenreaktion. Das ist Effizienz, die man auf dem Konto sieht."
* **Key Fact:** >20% Reaktionsquote auf automatisierte Dashboard-Mails.
### Phase 5: The Machine (Scalability)
> "Abschließend geben wir Ihnen den Regler in die Hand. 'The Machine' übersetzt Ihre strategischen Monatsziele (z.B. 300k €) live in wöchentliche Lead-Vorgaben für jeden einzelnen Manager individuell berechnet auf deren persönlicher Hit-Rate. So skalieren wir Wachstum vorhersagbar."
---
## 3. Daten-Backup (für die manuelle Gestaltung)
Falls du auf dem Zielsystem Grafiken nachbauen willst, hier die harten Werte aus dem Backend:
| Bereich | Wert (Beispiel-Snapshot) | Herkunft / Logik |
| :--- | :--- | :--- |
| **Gross Pipeline** | 5.164.155 € | Alle offenen Angebote in SO (Hardware). |
| **Duplicate Bloat** | - 1.593.787 € | Differenz aus Gross vs. Netto (pro Firma). |
| **Clean Pipeline** | 3.570.368 € | Höchstes Einzelangebot pro Firma. |
| **Win-Rate (mit Demo)** | 23,8 % | Korrelation 'Termin Typ 148' ➔ 'Won'. |
| **Win-Rate (ohne Demo)** | 9,2 % | Deals ohne Vor-Ort-Aktivität. |
| **Median Follow-Up** | 10 Tage | Zeit zw. Angebot und nächster Aktion. |
| **CRM White Space** | 67 % | 10.000+ Accounts ohne jegliche Historie. |
| **Reaktionsquote** | > 20 % | Analysiert via 'analyze_dashboard_roi_fixed.py'. |
---
## 4. Visual Guide (Was man im Dashboard sieht)
Da du keinen Zugriff hast, hier die Beschreibung der UI-Elemente für Mocks:
1. **Header:** Dunkelblau (`#081734`), Neon-Cyan Akzente. Titel: "Sales Center Analytics".
2. **KPI-Karten:** 5 Karten nebeneinander (Volumen, Stagnation, Forecast, Cycle Time, Rote Liste).
3. **Pipeline Funnel:** Ein horizontaler Trichter. Jede Stufe (10%, 20%, ... 98%) ist ein Balken, dessen Breite das Volumen widerspiegelt.
4. **Manager-Table:** Zeigt Win-Rates und Cycle Times. Wichtig: Der "Expert Mode" zeigt hier auch den Deckungsbeitrag I (DB I) und die geschätzten Reisekosten/Stunden.
5. **Rote Liste:** Eine knallrote Tabelle oben, die "Deals in Gefahr" zeigt (Datum abgelaufen oder Stagnation > 14 Tage).
6. **Target Simulator:** Ein großer Schieberegler mit zwei "Tacho-Anzeigen" für Leads/Woche.
7. **Toby:** Der kleine Roboter-Avatar sitzt meist unten rechts in einer Sprechblase.
---
*Dokument erstellt am 01. Juni 2026 für Task [37288f42]*

View File

@@ -0,0 +1 @@
Git Push Test - Mon Jun 1 16:58:35 UTC 2026

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,384 @@
<!DOCTYPE html>
<html lang="de">
<head>
<!-- Updated: 16:26:23 -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sales Intelligence Dashboard: Executive Briefing</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@300;500;700&display=swap" rel="stylesheet">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
brandDark: '#081734',
brandLight: '#DDEEFE',
brandCyan: '#00E5FF',
brandGold: '#FFD42C',
brandSlate: '#1E293B'
},
fontFamily: {
'head': ['Space Grotesk', 'sans-serif'],
'body': ['Inter', 'sans-serif']
}
}
}
}
</script>
<style>
body {
background-color: #081734;
color: #DDEEFE;
font-family: 'Inter', sans-serif;
overflow-x: hidden;
line-height: 1.6;
}
h1, h2, h3, h4 { font-family: 'Space Grotesk', sans-serif; font-weight: 700; }
.glass-card {
background: rgba(221, 238, 254, 0.02);
backdrop-filter: blur(25px);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 28px;
}
.section-screen {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
padding: 4rem 2rem;
}
/* Toby Animation */
.toby-float { animation: float 6s ease-in-out infinite; }
@keyframes float { 0% { transform: translateY(0px) rotate(0deg); } 50% { transform: translateY(-15px) rotate(1deg); } 100% { transform: translateY(0px) rotate(0deg); } }
#toby-fixed {
position: fixed;
bottom: 40px;
right: 40px;
width: 160px;
z-index: 100;
pointer-events: none;
opacity: 0;
display: flex;
flex-direction: column;
align-items: center;
}
#toby-bubble {
background: #FFF;
color: #081734;
padding: 0.85rem 1.25rem;
border-radius: 14px;
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 12px;
position: relative;
text-align: center;
box-shadow: 0 10px 40px rgba(0,0,0,0.4);
max-width: 200px;
}
#toby-bubble::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
border-width: 8px 8px 0;
border-style: solid;
border-color: #FFF transparent transparent transparent;
}
/* Custom Progress Bar */
#progress-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: rgba(255,255,255,0.03);
z-index: 1000;
}
#progress-bar {
height: 100%;
width: 0%;
background: #00E5FF;
}
.text-gradient {
background: linear-gradient(135deg, #FFF 20%, #00E5FF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.stat-value { font-size: 4.5rem; font-weight: 700; line-height: 1; letter-spacing: -0.02em; }
.content-width { max-width: 1100px; width: 100%; }
.reveal { opacity: 0; transform: translateY(25px); }
/* Dashboard Elements Mocks */
.mock-kpi {
border-left: 4px solid #00E5FF;
padding: 1.5rem;
background: rgba(255,255,255,0.02);
}
</style>
</head>
<body class="antialiased">
<div id="progress-container"><div id="progress-bar"></div></div>
<!-- Toby Fixed -->
<div id="toby-fixed" class="toby-float">
<div id="toby-bubble">Präzision statt Vermutung.</div>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB9AAAAc2CAYAAABaJTzmAAAACXBIWXMAABsRAAAbEQEEnGAvAAAEvmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSfvu78nIGlkPSdXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQnPz4KPHg6eG1wbWV0YSB4bWxuczp4PSdhZG9iZTpuczptZXRhLyc+CjxyZGY6UkRGIHhtbG5zOnJkZj0naHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyc+CgogPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9JycKICB4bWxuczpBdHRyaWI9J2h0dHA6Ly9ucy5hdHRyaWJ1dGlvbi5jb20vYWRzLzEuMC8nPgogIDxBdHRyaWI6QWRzPgogICA8cmRmOlNlcT4KICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0nUmVzb3VyY2UnPgogICAgIDxBdHRyaWI6Q3JlYXRlZD4yMDI2LTAxLTEzPC9BdHRyaWI6Q3JlYXRlZD4KICAgICA8QXR0cmliOkV4dExtZD4zZjE1OTM5Ny04MWM3LTQyM2ItOWViMy02ODYyYzAxZjM4NGI8L0F0dHJpYjpFeHRJZD4KICAgICA8QXR0cmliOkZiSWQ+NTI1MjY1OTE0MTc5NTgwPC9BdHRyaWI6RmJJZD4KICAgICA8QXR0cmliOlRvdWNoVHlwZT4yPC9BdHRyaWI6VG91Y2hUeXBlPgogICAgPC9yZGY6bGk+CiAgIDwvJmRmOlNlcT4KICB8L0F0dHJpYjpBZHM+CiA8L3JkZjpEZXNjcmlwdGlvbj4KCiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0nJwogIHhtbG5zOmRjPSdodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyc+CiAgPGRjOnRpdGxlPgogICA8cmRmOkFsdD4KICAgIDxyZGY6bGkgeG1sOmxhbmc9J3gtZGVmYXVsdCc+Um9ib1BsYW5ldF9BdmF0YXJfaGVsbGJsYXUuYWkgLSAxPC9yZGY6bGk+CiAgIDwvcmRmOkFsdD4KICA8L2RjOnRpdGxlPgogPC9yZGY6RGVzY3JpcHRpb24+CgogPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9JycKICB4bWxuczpwZGY9J2h0dHA6Ly9ucy5hZG9iZS5jb20vcGRmLzEuMy8nPgogIDxwZGY6QXV0aG9yPldhY2tsZXJHcm91cDwvcGRmOkF1dGhvcm4+CiA8L3JkZjpEZXNjcmlwdGlvbj4KCiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0nJwogIHhtbG5zOnhtcD0naHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyc+CiAgPHhtcDpDcmVhdG9yVG9vbD5DYW52YSBkb2M9REFHLVN1Y0tTeXMgdXNlcj1VQUVUOG1KY3lhSSBicmFuZD1CQUVUOGd0OERsTSB0ZW1wbGF0ZT08L3htcDpDcmVhdG9yVG9vbD4KIDwvcmRmOkRlc2NyaXB0aW9uPgo8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSdyJz8+qsA63gAACAASURBVHic7N15uG5z/fj/zx+/6/urvqU5aVCSSFGhyJjMZAghY6aMoQxJEioaKBkjkTEJERWJFAopY6nOWvc+xzmpnNa695n4616v73qvc/o0knOcc9733vvxvK7HtU/lCnvf+17v4V7v9T//I0mSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmS... [truncated]" alt="Toby" class="w-full opacity-80">
</div>
<!-- 1. Intro: The Intelligence Cockpit -->
<section class="section-screen">
<div class="content-width text-center">
<h4 class="text-brandCyan uppercase tracking-[0.5em] mb-8 reveal font-head text-xs">Executive Summary</h4>
<h1 class="text-7xl md:text-8xl font-bold mb-10 reveal leading-[1.1] tracking-tight">Sales Intelligence <br> <span class="text-gradient">Cockpit</span></h1>
<p class="text-xl md:text-2xl text-brandLight/40 max-w-2xl mx-auto mb-16 reveal font-body font-light">
Transparenz auf Knopfdruck. <br>
Vom statischen Archiv zum aktiven Steuerungs-Hub.
</p>
<div class="flex justify-center gap-6 reveal">
<div class="h-[1px] w-12 bg-brandCyan/40 self-center"></div>
<span class="text-brandCyan font-mono uppercase text-xs tracking-widest">30 Tage Live-Betrieb</span>
<div class="h-[1px] w-12 bg-brandCyan/40 self-center"></div>
</div>
</div>
</section>
<!-- 2. The Efficiency: Adieu Excel -->
<section class="section-screen bg-brandSlate/10">
<div class="content-width">
<div class="grid grid-cols-1 md:grid-cols-2 gap-20 items-center">
<div class="reveal">
<h2 class="text-5xl mb-8">Echte Daten statt manueller Berichte.</h2>
<p class="text-lg text-brandLight/60 mb-10 font-light leading-relaxed">
Bisher kostete die Aufbereitung der Pipeline eine Stunde pro Woche. Das Dashboard liefert diese Erkenntnisse in <strong>Echtzeit</strong> fehlerfrei und interaktiv.
</p>
<div class="grid grid-cols-2 gap-4">
<div class="mock-kpi">
<p class="text-3xl font-bold text-white">-1h</p>
<p class="text-[10px] uppercase tracking-widest text-brandLight/50 mt-1">Manueller Aufwand / Woche</p>
</div>
<div class="mock-kpi">
<p class="text-3xl font-bold text-brandCyan">Live</p>
<p class="text-[10px] uppercase tracking-widest text-brandLight/50 mt-1">Pipeline-Truth 24/7</p>
</div>
</div>
</div>
<div class="glass-card p-12 reveal">
<h4 class="text-brandCyan uppercase text-[10px] tracking-widest font-bold mb-6 border-b border-white/5 pb-4">Vorteil: Daten-Integrität</h4>
<p class="text-xl text-white font-light leading-relaxed mb-6">
"Automatisierte Erkennung von Dubletten und Varianten eine Transparenz, die SuperOffice in dieser Form nicht bietet."
</p>
<div class="w-16 h-1 bg-brandCyan"></div>
</div>
</div>
</div>
</section>
<!-- 3. Transparency: The Winning DNA -->
<section class="section-screen">
<div class="content-width">
<div class="text-center mb-20 reveal">
<h2 class="text-5xl mb-6">Die Winning DNA</h2>
<p class="text-xl text-brandLight/40 font-light">Tiefe Einblicke in die Mechanik unseres Erfolgs.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 reveal">
<div class="glass-card p-10 hover:bg-brandCyan/[0.03] transition-all">
<p class="text-brandCyan font-mono text-[10px] uppercase mb-4">Kanäle</p>
<h4 class="text-lg mb-4">Winning Channels</h4>
<p class="text-sm text-brandLight/50 font-light leading-relaxed">Präzise Auswertung: Welche Lead-Quelle konvertiert wirklich?</p>
</div>
<div class="glass-card p-10 hover:bg-brandCyan/[0.03] transition-all">
<p class="text-brandCyan font-mono text-[10px] uppercase mb-4">Analyse</p>
<h4 class="text-lg mb-4">Absage-Gründe</h4>
<p class="text-sm text-brandLight/50 font-light leading-relaxed">Warum verlieren wir Deals? Daten statt Vermutungen.</p>
</div>
<div class="glass-card p-10 hover:bg-brandCyan/[0.03] transition-all">
<p class="text-brandCyan font-mono text-[10px] uppercase mb-4">Performance</p>
<h4 class="text-lg mb-4">Time to Sale</h4>
<p class="text-sm text-brandLight/50 font-light leading-relaxed">Abschlussquoten & Kontakthäufigkeit pro Sales Manager.</p>
</div>
</div>
</div>
</section>
<!-- 4. Proactivity: The Red List -->
<section class="section-screen bg-white/[0.01]">
<div class="content-width">
<div class="grid grid-cols-1 md:grid-cols-2 gap-20 items-center">
<div class="reveal relative order-2 md:order-1">
<div class="glass-card p-1 relative overflow-hidden border-red-500/20">
<div class="bg-red-500/10 p-10">
<div class="flex justify-between items-center mb-8">
<h4 class="text-red-500 font-bold uppercase text-xs tracking-widest">Rote Liste: Deals in Gefahr</h4>
<span class="bg-red-500 text-white text-[10px] px-2 py-1 rounded">Aktion Erforderlich</span>
</div>
<div class="space-y-4">
<div class="h-12 bg-white/5 rounded border border-white/5 flex items-center px-4 justify-between">
<div class="w-1/2 h-2 bg-white/20 rounded"></div>
<div class="w-12 h-2 bg-red-500/40 rounded"></div>
</div>
<div class="h-12 bg-white/5 rounded border border-white/5 flex items-center px-4 justify-between">
<div class="w-1/2 h-2 bg-white/20 rounded"></div>
<div class="w-12 h-2 bg-red-500/40 rounded"></div>
</div>
<div class="h-12 bg-white/5 rounded border border-white/5 flex items-center px-4 justify-between">
<div class="w-1/2 h-2 bg-white/20 rounded"></div>
<div class="w-12 h-2 bg-red-500/40 rounded"></div>
</div>
</div>
</div>
</div>
</div>
<div class="reveal order-1 md:order-2">
<h2 class="text-5xl mb-8 leading-tight">Wissen, wo <br> <span class="text-red-500">es brennt.</span></h2>
<p class="text-lg text-brandLight/60 font-light leading-relaxed mb-8">
Überschrittene Verkaufsdaten oder Stagnation > 14 Tage werden sofort markiert. Das Dashboard fungiert als <strong>Frühwarnsystem</strong> für die Pipeline.
</p>
<div class="flex items-center gap-4 text-brandCyan">
<span class="text-xl"></span>
<p class="text-sm uppercase tracking-widest font-bold">Prävention von Deal-Verlusten</p>
</div>
</div>
</div>
</div>
</section>
<!-- 5. Action Hub: From Insight to Interaction -->
<section class="section-screen">
<div class="content-width">
<div class="grid grid-cols-1 md:grid-cols-2 gap-20 items-center">
<div class="reveal">
<h4 class="text-brandCyan font-mono uppercase tracking-widest text-xs mb-6 font-bold">The Action Hub</h4>
<h2 class="text-6xl mb-8 leading-tight">Vom Insight <br> zur <span class="text-gradient">Tat.</span></h2>
<p class="text-xl text-brandLight/70 font-light mb-10 leading-relaxed">
Das Dashboard ist kein Rückspiegel. Mit einem Klick zum Account, zum Sale oder zur automatisierten <strong>Rückrufbitte</strong>.
</p>
<div class="glass-card p-10 bg-brandCyan/5 border-brandCyan/20">
<div class="stat-value text-white mb-2">>20%</div>
<p class="text-xs uppercase tracking-widest font-bold text-brandCyan">Response-Rate</p>
<p class="text-sm text-brandLight/60 mt-4">Belegter Erfolg: Jede 5. Nachricht führt zu einer direkten Reaktion.</p>
</div>
</div>
<div class="reveal">
<div class="glass-card p-12 border-brandCyan/30">
<h4 class="text-xs uppercase tracking-widest font-bold mb-6 border-b border-white/5 pb-4">Einfachheit gewinnt</h4>
<p class="text-lg text-brandLight/80 font-light leading-relaxed mb-8">
"Was in SuperOffice 5-10 Klicks braucht, liegt hier auf der Oberfläche. Das Team arbeitet lieber im Dashboard, weil es Zeit spart und Übersicht schafft."
</p>
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-brandCyan/20"></div>
<p class="text-xs text-brandLight/40 uppercase tracking-widest">Feedback aus dem Innendienst</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 6. Infrastructure: Scalable Asset -->
<section class="section-screen bg-brandCyan/5">
<div class="content-width text-center">
<h2 class="text-5xl md:text-7xl mb-12 reveal font-head tracking-tighter uppercase">Scalable <br> Asset</h2>
<p class="text-xl md:text-2xl text-brandLight/70 max-w-3xl mx-auto mb-16 reveal font-light leading-relaxed">
Dieses Dashboard ist kein isoliertes Tool. Es fußt auf einem robusten <strong>API-Connector</strong>, der die Grundlage für alle zukünftigen Sales-Automatisierungen bildet.
</p>
<div class="h-[1px] w-24 bg-brandCyan mx-auto opacity-30 reveal"></div>
</div>
</section>
<!-- 7. Conclusion -->
<section class="section-screen">
<div class="content-width text-center">
<h2 class="text-6xl mb-12 reveal font-head">Bereit für die <br> <span class="text-gradient">nächste Stufe.</span></h2>
<p class="text-brandLight/40 uppercase tracking-[0.3em] text-xs reveal">Vielen Dank für Ihre Aufmerksamkeit.</p>
</div>
</section>
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
<script>
gsap.registerPlugin(ScrollTrigger);
const tobyFixed = document.getElementById('toby-fixed');
const tobyBubble = document.getElementById('toby-bubble');
const progressBar = document.getElementById('progress-bar');
const tobyMessages = [
"Willkommen im Cockpit.",
"Live-Daten schlagen Excel.",
"Die Erfolgs-DNA im Blick.",
"Frühwarnsystem aktiviert.",
"Action: Jede 5. Mail ein Treffer.",
"Bereit zum Skalieren.",
"Das ist erst der Anfang."
];
function updateToby(index) {
if (tobyMessages[index]) {
tobyBubble.innerHTML = tobyMessages[index];
gsap.fromTo(tobyBubble, { scale: 0.8, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.4, ease: "back.out(1.7)" });
}
}
// Reveal Animations
gsap.utils.toArray('.reveal').forEach((el, i) => {
gsap.to(el, {
scrollTrigger: {
trigger: el,
start: "top 88%",
toggleActions: "play none none reverse"
},
opacity: 1,
y: 0,
duration: 0.8,
ease: "power2.out"
});
});
// Toby Visibility & Updates
ScrollTrigger.create({
trigger: "body",
start: "100px top",
onEnter: () => gsap.to(tobyFixed, { opacity: 1, duration: 0.5 }),
onLeaveBack: () => gsap.to(tobyFixed, { opacity: 0, duration: 0.5 })
});
const sections = gsap.utils.toArray('section');
sections.forEach((section, i) => {
ScrollTrigger.create({
trigger: section,
start: "top center",
onEnter: () => updateToby(i),
onEnterBack: () => updateToby(i)
});
});
// Progress Bar
gsap.to(progressBar, {
width: "100%",
ease: "none",
scrollTrigger: { scrub: 0.3 }
});
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,384 @@
<!DOCTYPE html>
<html lang="de">
<head>
<!-- Updated: 16:26:23 -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sales Intelligence Dashboard: Executive Briefing</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@300;500;700&display=swap" rel="stylesheet">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
brandDark: '#081734',
brandLight: '#DDEEFE',
brandCyan: '#00E5FF',
brandGold: '#FFD42C',
brandSlate: '#1E293B'
},
fontFamily: {
'head': ['Space Grotesk', 'sans-serif'],
'body': ['Inter', 'sans-serif']
}
}
}
}
</script>
<style>
body {
background-color: #081734;
color: #DDEEFE;
font-family: 'Inter', sans-serif;
overflow-x: hidden;
line-height: 1.6;
}
h1, h2, h3, h4 { font-family: 'Space Grotesk', sans-serif; font-weight: 700; }
.glass-card {
background: rgba(221, 238, 254, 0.02);
backdrop-filter: blur(25px);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 28px;
}
.section-screen {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
padding: 4rem 2rem;
}
/* Toby Animation */
.toby-float { animation: float 6s ease-in-out infinite; }
@keyframes float { 0% { transform: translateY(0px) rotate(0deg); } 50% { transform: translateY(-15px) rotate(1deg); } 100% { transform: translateY(0px) rotate(0deg); } }
#toby-fixed {
position: fixed;
bottom: 40px;
right: 40px;
width: 160px;
z-index: 100;
pointer-events: none;
opacity: 0;
display: flex;
flex-direction: column;
align-items: center;
}
#toby-bubble {
background: #FFF;
color: #081734;
padding: 0.85rem 1.25rem;
border-radius: 14px;
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 12px;
position: relative;
text-align: center;
box-shadow: 0 10px 40px rgba(0,0,0,0.4);
max-width: 200px;
}
#toby-bubble::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
border-width: 8px 8px 0;
border-style: solid;
border-color: #FFF transparent transparent transparent;
}
/* Custom Progress Bar */
#progress-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: rgba(255,255,255,0.03);
z-index: 1000;
}
#progress-bar {
height: 100%;
width: 0%;
background: #00E5FF;
}
.text-gradient {
background: linear-gradient(135deg, #FFF 20%, #00E5FF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.stat-value { font-size: 4.5rem; font-weight: 700; line-height: 1; letter-spacing: -0.02em; }
.content-width { max-width: 1100px; width: 100%; }
.reveal { opacity: 0; transform: translateY(25px); }
/* Dashboard Elements Mocks */
.mock-kpi {
border-left: 4px solid #00E5FF;
padding: 1.5rem;
background: rgba(255,255,255,0.02);
}
</style>
</head>
<body class="antialiased">
<div id="progress-container"><div id="progress-bar"></div></div>
<!-- Toby Fixed -->
<div id="toby-fixed" class="toby-float">
<div id="toby-bubble">Präzision statt Vermutung.</div>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB9AAAAc2CAYAAABaJTzmAAAACXBIWXMAABsRAAAbEQEEnGAvAAAEvmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSfvu78nIGlkPSdXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQnPz4KPHg6eG1wbWV0YSB4bWxuczp4PSdhZG9iZTpuczptZXRhLyc+CjxyZGY6UkRGIHhtbG5zOnJkZj0naHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyc+CgogPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9JycKICB4bWxuczpBdHRyaWI9J2h0dHA6Ly9ucy5hdHRyaWJ1dGlvbi5jb20vYWRzLzEuMC8nPgogIDxBdHRyaWI6QWRzPgogICA8cmRmOlNlcT4KICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0nUmVzb3VyY2UnPgogICAgIDxBdHRyaWI6Q3JlYXRlZD4yMDI2LTAxLTEzPC9BdHRyaWI6Q3JlYXRlZD4KICAgICA8QXR0cmliOkV4dExtZD4zZjE1OTM5Ny04MWM3LTQyM2ItOWViMy02ODYyYzAxZjM4NGI8L0F0dHJpYjpFeHRJZD4KICAgICA8QXR0cmliOkZiSWQ+NTI1MjY1OTE0MTc5NTgwPC9BdHRyaWI6RmJJZD4KICAgICA8QXR0cmliOlRvdWNoVHlwZT4yPC9BdHRyaWI6VG91Y2hUeXBlPgogICAgPC9yZGY6bGk+CiAgIDwvJmRmOlNlcT4KICB8L0F0dHJpYjpBZHM+CiA8L3JkZjpEZXNjcmlwdGlvbj4KCiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0nJwogIHhtbG5zOmRjPSdodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyc+CiAgPGRjOnRpdGxlPgogICA8cmRmOkFsdD4KICAgIDxyZGY6bGkgeG1sOmxhbmc9J3gtZGVmYXVsdCc+Um9ib1BsYW5ldF9BdmF0YXJfaGVsbGJsYXUuYWkgLSAxPC9yZGY6bGk+CiAgIDwvcmRmOkFsdD4KICA8L2RjOnRpdGxlPgogPC9yZGY6RGVzY3JpcHRpb24+CgogPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9JycKICB4bWxuczpwZGY9J2h0dHA6Ly9ucy5hZG9iZS5jb20vcGRmLzEuMy8nPgogIDxwZGY6QXV0aG9yPldhY2tsZXJHcm91cDwvcGRmOkF1dGhvcm4+CiA8L3JkZjpEZXNjcmlwdGlvbj4KCiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0nJwogIHhtbG5zOnhtcD0naHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyc+CiAgPHhtcDpDcmVhdG9yVG9vbD5DYW52YSBkb2M9REFHLVN1Y0tTeXMgdXNlcj1VQUVUOG1KY3lhSSBicmFuZD1CQUVUOGd0OERsTSB0ZW1wbGF0ZT08L3htcDpDcmVhdG9yVG9vbD4KIDwvcmRmOkRlc2NyaXB0aW9uPgo8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSdyJz8+qsA63gAACAASURBVHic7N15uG5z/fj/zx+/6/urvqU5aVCSSFGhyJjMZAghY6aMoQxJEioaKBkjkTEJERWJFAopY6nOWvc+xzmpnNa695n4616v73qvc/o0knOcc9733vvxvK7HtU/lCnvf+17v4V7v9T//I0mSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmS... [truncated]" alt="Toby" class="w-full opacity-80">
</div>
<!-- 1. Intro: The Intelligence Cockpit -->
<section class="section-screen">
<div class="content-width text-center">
<h4 class="text-brandCyan uppercase tracking-[0.5em] mb-8 reveal font-head text-xs">Executive Summary</h4>
<h1 class="text-7xl md:text-8xl font-bold mb-10 reveal leading-[1.1] tracking-tight">Sales Intelligence <br> <span class="text-gradient">Cockpit</span></h1>
<p class="text-xl md:text-2xl text-brandLight/40 max-w-2xl mx-auto mb-16 reveal font-body font-light">
Transparenz auf Knopfdruck. <br>
Vom statischen Archiv zum aktiven Steuerungs-Hub.
</p>
<div class="flex justify-center gap-6 reveal">
<div class="h-[1px] w-12 bg-brandCyan/40 self-center"></div>
<span class="text-brandCyan font-mono uppercase text-xs tracking-widest">30 Tage Live-Betrieb</span>
<div class="h-[1px] w-12 bg-brandCyan/40 self-center"></div>
</div>
</div>
</section>
<!-- 2. The Efficiency: Adieu Excel -->
<section class="section-screen bg-brandSlate/10">
<div class="content-width">
<div class="grid grid-cols-1 md:grid-cols-2 gap-20 items-center">
<div class="reveal">
<h2 class="text-5xl mb-8">Echte Daten statt manueller Berichte.</h2>
<p class="text-lg text-brandLight/60 mb-10 font-light leading-relaxed">
Bisher kostete die Aufbereitung der Pipeline eine Stunde pro Woche. Das Dashboard liefert diese Erkenntnisse in <strong>Echtzeit</strong> fehlerfrei und interaktiv.
</p>
<div class="grid grid-cols-2 gap-4">
<div class="mock-kpi">
<p class="text-3xl font-bold text-white">-1h</p>
<p class="text-[10px] uppercase tracking-widest text-brandLight/50 mt-1">Manueller Aufwand / Woche</p>
</div>
<div class="mock-kpi">
<p class="text-3xl font-bold text-brandCyan">Live</p>
<p class="text-[10px] uppercase tracking-widest text-brandLight/50 mt-1">Pipeline-Truth 24/7</p>
</div>
</div>
</div>
<div class="glass-card p-12 reveal">
<h4 class="text-brandCyan uppercase text-[10px] tracking-widest font-bold mb-6 border-b border-white/5 pb-4">Vorteil: Daten-Integrität</h4>
<p class="text-xl text-white font-light leading-relaxed mb-6">
"Automatisierte Erkennung von Dubletten und Varianten eine Transparenz, die SuperOffice in dieser Form nicht bietet."
</p>
<div class="w-16 h-1 bg-brandCyan"></div>
</div>
</div>
</div>
</section>
<!-- 3. Transparency: The Winning DNA -->
<section class="section-screen">
<div class="content-width">
<div class="text-center mb-20 reveal">
<h2 class="text-5xl mb-6">Die Winning DNA</h2>
<p class="text-xl text-brandLight/40 font-light">Tiefe Einblicke in die Mechanik unseres Erfolgs.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 reveal">
<div class="glass-card p-10 hover:bg-brandCyan/[0.03] transition-all">
<p class="text-brandCyan font-mono text-[10px] uppercase mb-4">Kanäle</p>
<h4 class="text-lg mb-4">Winning Channels</h4>
<p class="text-sm text-brandLight/50 font-light leading-relaxed">Präzise Auswertung: Welche Lead-Quelle konvertiert wirklich?</p>
</div>
<div class="glass-card p-10 hover:bg-brandCyan/[0.03] transition-all">
<p class="text-brandCyan font-mono text-[10px] uppercase mb-4">Analyse</p>
<h4 class="text-lg mb-4">Absage-Gründe</h4>
<p class="text-sm text-brandLight/50 font-light leading-relaxed">Warum verlieren wir Deals? Daten statt Vermutungen.</p>
</div>
<div class="glass-card p-10 hover:bg-brandCyan/[0.03] transition-all">
<p class="text-brandCyan font-mono text-[10px] uppercase mb-4">Performance</p>
<h4 class="text-lg mb-4">Time to Sale</h4>
<p class="text-sm text-brandLight/50 font-light leading-relaxed">Abschlussquoten & Kontakthäufigkeit pro Sales Manager.</p>
</div>
</div>
</div>
</section>
<!-- 4. Proactivity: The Red List -->
<section class="section-screen bg-white/[0.01]">
<div class="content-width">
<div class="grid grid-cols-1 md:grid-cols-2 gap-20 items-center">
<div class="reveal relative order-2 md:order-1">
<div class="glass-card p-1 relative overflow-hidden border-red-500/20">
<div class="bg-red-500/10 p-10">
<div class="flex justify-between items-center mb-8">
<h4 class="text-red-500 font-bold uppercase text-xs tracking-widest">Rote Liste: Deals in Gefahr</h4>
<span class="bg-red-500 text-white text-[10px] px-2 py-1 rounded">Aktion Erforderlich</span>
</div>
<div class="space-y-4">
<div class="h-12 bg-white/5 rounded border border-white/5 flex items-center px-4 justify-between">
<div class="w-1/2 h-2 bg-white/20 rounded"></div>
<div class="w-12 h-2 bg-red-500/40 rounded"></div>
</div>
<div class="h-12 bg-white/5 rounded border border-white/5 flex items-center px-4 justify-between">
<div class="w-1/2 h-2 bg-white/20 rounded"></div>
<div class="w-12 h-2 bg-red-500/40 rounded"></div>
</div>
<div class="h-12 bg-white/5 rounded border border-white/5 flex items-center px-4 justify-between">
<div class="w-1/2 h-2 bg-white/20 rounded"></div>
<div class="w-12 h-2 bg-red-500/40 rounded"></div>
</div>
</div>
</div>
</div>
</div>
<div class="reveal order-1 md:order-2">
<h2 class="text-5xl mb-8 leading-tight">Wissen, wo <br> <span class="text-red-500">es brennt.</span></h2>
<p class="text-lg text-brandLight/60 font-light leading-relaxed mb-8">
Überschrittene Verkaufsdaten oder Stagnation > 14 Tage werden sofort markiert. Das Dashboard fungiert als <strong>Frühwarnsystem</strong> für die Pipeline.
</p>
<div class="flex items-center gap-4 text-brandCyan">
<span class="text-xl"></span>
<p class="text-sm uppercase tracking-widest font-bold">Prävention von Deal-Verlusten</p>
</div>
</div>
</div>
</div>
</section>
<!-- 5. Action Hub: From Insight to Interaction -->
<section class="section-screen">
<div class="content-width">
<div class="grid grid-cols-1 md:grid-cols-2 gap-20 items-center">
<div class="reveal">
<h4 class="text-brandCyan font-mono uppercase tracking-widest text-xs mb-6 font-bold">The Action Hub</h4>
<h2 class="text-6xl mb-8 leading-tight">Vom Insight <br> zur <span class="text-gradient">Tat.</span></h2>
<p class="text-xl text-brandLight/70 font-light mb-10 leading-relaxed">
Das Dashboard ist kein Rückspiegel. Mit einem Klick zum Account, zum Sale oder zur automatisierten <strong>Rückrufbitte</strong>.
</p>
<div class="glass-card p-10 bg-brandCyan/5 border-brandCyan/20">
<div class="stat-value text-white mb-2">>20%</div>
<p class="text-xs uppercase tracking-widest font-bold text-brandCyan">Response-Rate</p>
<p class="text-sm text-brandLight/60 mt-4">Belegter Erfolg: Jede 5. Nachricht führt zu einer direkten Reaktion.</p>
</div>
</div>
<div class="reveal">
<div class="glass-card p-12 border-brandCyan/30">
<h4 class="text-xs uppercase tracking-widest font-bold mb-6 border-b border-white/5 pb-4">Einfachheit gewinnt</h4>
<p class="text-lg text-brandLight/80 font-light leading-relaxed mb-8">
"Was in SuperOffice 5-10 Klicks braucht, liegt hier auf der Oberfläche. Das Team arbeitet lieber im Dashboard, weil es Zeit spart und Übersicht schafft."
</p>
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-brandCyan/20"></div>
<p class="text-xs text-brandLight/40 uppercase tracking-widest">Feedback aus dem Innendienst</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 6. Infrastructure: Scalable Asset -->
<section class="section-screen bg-brandCyan/5">
<div class="content-width text-center">
<h2 class="text-5xl md:text-7xl mb-12 reveal font-head tracking-tighter uppercase">Scalable <br> Asset</h2>
<p class="text-xl md:text-2xl text-brandLight/70 max-w-3xl mx-auto mb-16 reveal font-light leading-relaxed">
Dieses Dashboard ist kein isoliertes Tool. Es fußt auf einem robusten <strong>API-Connector</strong>, der die Grundlage für alle zukünftigen Sales-Automatisierungen bildet.
</p>
<div class="h-[1px] w-24 bg-brandCyan mx-auto opacity-30 reveal"></div>
</div>
</section>
<!-- 7. Conclusion -->
<section class="section-screen">
<div class="content-width text-center">
<h2 class="text-6xl mb-12 reveal font-head">Bereit für die <br> <span class="text-gradient">nächste Stufe.</span></h2>
<p class="text-brandLight/40 uppercase tracking-[0.3em] text-xs reveal">Vielen Dank für Ihre Aufmerksamkeit.</p>
</div>
</section>
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
<script>
gsap.registerPlugin(ScrollTrigger);
const tobyFixed = document.getElementById('toby-fixed');
const tobyBubble = document.getElementById('toby-bubble');
const progressBar = document.getElementById('progress-bar');
const tobyMessages = [
"Willkommen im Cockpit.",
"Live-Daten schlagen Excel.",
"Die Erfolgs-DNA im Blick.",
"Frühwarnsystem aktiviert.",
"Action: Jede 5. Mail ein Treffer.",
"Bereit zum Skalieren.",
"Das ist erst der Anfang."
];
function updateToby(index) {
if (tobyMessages[index]) {
tobyBubble.innerHTML = tobyMessages[index];
gsap.fromTo(tobyBubble, { scale: 0.8, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.4, ease: "back.out(1.7)" });
}
}
// Reveal Animations
gsap.utils.toArray('.reveal').forEach((el, i) => {
gsap.to(el, {
scrollTrigger: {
trigger: el,
start: "top 88%",
toggleActions: "play none none reverse"
},
opacity: 1,
y: 0,
duration: 0.8,
ease: "power2.out"
});
});
// Toby Visibility & Updates
ScrollTrigger.create({
trigger: "body",
start: "100px top",
onEnter: () => gsap.to(tobyFixed, { opacity: 1, duration: 0.5 }),
onLeaveBack: () => gsap.to(tobyFixed, { opacity: 0, duration: 0.5 })
});
const sections = gsap.utils.toArray('section');
sections.forEach((section, i) => {
ScrollTrigger.create({
trigger: section,
start: "top center",
onEnter: () => updateToby(i),
onEnterBack: () => updateToby(i)
});
});
// Progress Bar
gsap.to(progressBar, {
width: "100%",
ease: "none",
scrollTrigger: { scrub: 0.3 }
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

View File

@@ -0,0 +1,71 @@
# Fotograf.de Scraper & Management UI
**Status:** Production-Ready Microservice (Core Feature: PDF List Generation, QR Cards, Shooting Schedule, **SQLite Data Sync**, **Gmail API Integration** & **Automated Release Requests**)
Dieser Service modernisiert die alten `Fotograf.de` Skripte, indem er eine robuste, web-basierte UI zur Verwaltung und Automatisierung von Foto-Aufträgen bereitstellt. Er ist als eigenständiger Microservice konzipiert, der unabhängig vom Haupt-Stack läuft.
## 🏗️ Architektur
Der Service besteht aus zwei Hauptkomponenten:
1. **Backend (Python / FastAPI / Selenium / SQLAlchemy):**
* **Automatisierung:** Nutzt Selenium für das Scraping von `fotograf.de`.
* **Persistenz:** Eine SQLite-Datenbank (`fotograf_jobs.db`) speichert die Auftragsliste, OAuth-Tokens (`GmailToken`), Gutscheincodes (`DiscountCode`), Teilnehmerdaten (`ReleaseParticipant`), **Auftragsteilnehmer (`JobParticipant`)** und die **Versand-Historie (`ReleaseHistory`)**.
* **PDF-Engine:** Nutzt WeasyPrint für Teilnehmerlisten und ReportLab/PyPDF2 für präzise PDF-Overlays (QR-Karten).
* **API-Integration:** Direkte Anbindung an die **Calendly API (v2)** sowie an die **Gmail API** für direkten E-Mail-Versand und automatisierte Webhook-Antworten.
2. **Frontend (TypeScript / React / Vite / TailwindCSS):**
* **Modernes UI:** Ein vollständig responsives Dashboard mit Tailwind CSS (Kachel-Layout, Tabs für Kiga/Schule).
* **Arbeitsfluss:** Tools sind in der Detailansicht eines Auftrags in logische Phasen (Vorbereitung, Follow-Up, Statistik) unterteilt.
## ✨ Core Features
### 🚀 Performance-Optimierung (SQLite Sync)
Statt wie früher jedes Mal mühsam durch alle Foto-Alben zu "crawlen", nutzt das System nun eine intelligente Synchronisierung:
* **One-Click Sync:** Über den Button "Daten von Fotograf.de abgleichen" lädt das System die detaillierte Namensliste (CSV) herunter.
* **Lokale Datenbank:** Alle relevanten Infos (E-Mail der Eltern, Login-Zahlen, Bestellstatus, Zugangscodes) werden in der Tabelle `job_participants` gespeichert.
* **Blitzschnelle Analyse:** Nachfass-Mails und Statistiken werden nun in Sekunden (statt Minuten) direkt aus der Datenbank generiert.
### Feature 1: Teilnehmerlisten (Vollständig)
Automatisierter Workflow zum Download und Formatieren der Anmeldelisten von `fotograf.de` als sortiertes PDF inkl. "Kinderfotos Erding" Branding.
### Feature 2: Shooting-Planung (QR-Karten & Terminliste) (Vollständig)
Spezielles Modul für Familien-Mini-Shootings:
* **QR-Karten-Andruck:** Präzises Overlay von Name, Kinderanzahl und Uhrzeit inkl. automatischer **Einwilligungs-Checkbox (☑)** aus Calendly-Daten.
* **Termin-Übersichtsliste:** Generiert eine A4-Tabelle für den Shooting-Tag im 6-Minuten-Takt inkl. Lückenfüller.
### Feature 3: Nachfass-E-Mails & Gmail Direkt-Versand (Optimiert)
Identifizierung von Nicht-Käufern (0-1 Logins, keine Bestellung) basierend auf den synchronisierten Datenbank-Daten.
* **Vorschau-Modus:** Ermöglicht das Durchklicken der personalisierten E-Mails an jeden Empfänger vor dem eigentlichen Versand.
* **Quick-Login Automation:** Komfortabler "One-Click" Login-Link. Das System nutzt bevorzugt den via 'Link Magic' gesammelten Direkt-Link (`/gc/xyz`) oder fällt sicher auf die generische Anmeldung (`/login/ZUGANGSCODE`) inkl. automatischer Code-Übergabe zurück.
### Feature 4: Verkaufs-Statistiken (Optimiert)
Detaillierte Analyse des Kaufverhaltens pro Gruppe/Klasse basierend auf den lokalen Datenbank-Einträgen.
### Feature 5: Geschwisterliste (Einrichtungsintern) (Vollständig)
Tool zur Identifizierung von Geschwistergruppen innerhalb einer Einrichtung inkl. Cross-Check mit Calendly-Buchungen und speziellen Geschwister-QR-Karten.
* **Flexibilität:** Optionaler Modus "Ohne Nachmittags-Shooting", um die Liste auch ohne Calendly-Abgleich (rein einrichtungsintern) zu generieren.
### Feature 6: Freigabeanfragen & Gutschein-Automation (Vollständig)
Vollautomatisierter DSGVO-Workflow zur Einholung von Veröffentlichungsgenehmigungen:
* **Schlanker Versand:** Manuelle Eingabe von Empfängern (E-Mail, Vorname, Kindernamen) mit **E-Mail-Vorschau**.
* **Versand-Planung:** Einstellbare Versandzeit (Berlin Timezone) via Hintergrund-Tasks.
* **Webhook-Integration:** Direkte Anbindung an **Google Forms**. Bei Absenden des Freigabe-Formulars wird automatisch ein Gutscheincode reserviert und eine Dankes-E-Mail versendet.
* **Antwort-Übersicht:** Tabelle aller eingegangenen Freigaben inkl. zugewiesenem Code und Zeitstempel.
---
## 🛠️ Technische Details & Sicherheit
* **BCC-Kontrolle:** Jede vom System versendete E-Mail sendet automatisch eine Blindkopie (BCC) an `kontakt@kinderfotos-erding.de`.
* **Versand-Historie:** Alle Aussendungen (Anzahl Empfänger, Zeitpunkt) werden in der Tabelle `release_history` protokolliert.
* **Sicherer Test-Modus:** Über `DEV_MODE_EMAIL_RECIPIENT` können alle E-Mails global an eine Test-Adresse umgeleitet werden.
* **Zeitzonen:** Durchgängige Verwendung von `Europe/Berlin`.
* **Gmail OAuth:** Persistente Speicherung der Refresh-Tokens in der Datenbank.
## 🚀 Deployment & Konfiguration
Der Service wird über die Haupt-`docker-compose.yml` des Projekts verwaltet.
### URLs
* **Frontend:** `https://floke-ai.duckdns.org/fotograf-de/`
* **Webhook für Google Forms:** `https://floke-ai.duckdns.org/fotograf-de-api/api/publish-request/webhook`

View File

@@ -0,0 +1,56 @@
# Use an official Python runtime as a parent image
FROM python:3.11-slim-bookworm
# Set the working directory in the container
WORKDIR /app
# Install system dependencies for Chrome and other tools
# Using a multi-stage build or a more specific base image could optimize this
RUN apt-get update && apt-get install -y --no-install-recommends \
chromium-driver \
chromium \
wget \
unzip \
fonts-liberation \
fontconfig \
libappindicator3-1 \
libasound2 \
libatk-bridge2.0-0 \
libcups2 \
libdrm-dev \
libgbm-dev \
libglvnd0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libxkbcommon0 \
libxshmfence-dev \
xdg-utils \
build-essential \
libcairo2 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libgdk-pixbuf-2.0-0 \
libffi-dev \
shared-mime-info \
&& rm -rf /var/lib/apt/lists/*
# Set Chromium as default browser for Selenium
ENV CHROME_BIN /usr/bin/chromium
ENV CHROME_PATH /usr/bin/chromium
# Copy the requirements file and install Python dependencies
COPY requirements.txt ./requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code
COPY . .
# Create directory for error screenshots
RUN mkdir -p /app/errors && chmod 777 /app/errors
# Expose the port FastAPI will run on
EXPOSE 8000
# Command to run the application with DEBUG logging
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--log-level", "debug"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

View File

@@ -0,0 +1,87 @@
from sqlalchemy import create_engine, Column, Integer, String, DateTime
from sqlalchemy.orm import declarative_base, sessionmaker
import datetime
import os
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:////app/data/fotograf_jobs.db")
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
class Job(Base):
__tablename__ = "jobs"
id = Column(String, primary_key=True, index=True)
name = Column(String, index=True)
url = Column(String)
status = Column(String)
date = Column(String)
shooting_type = Column(String)
account_type = Column(String, index=True) # 'kiga' or 'schule'
last_updated = Column(DateTime, default=datetime.datetime.utcnow)
class GmailToken(Base):
__tablename__ = "gmail_tokens"
id = Column(Integer, primary_key=True)
token_json = Column(String) # Stores the full credentials JSON
updated_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
class DiscountCode(Base):
__tablename__ = "discount_codes"
id = Column(Integer, primary_key=True)
code = Column(String, unique=True, index=True)
is_used = Column(Integer, default=0) # 0 for false, 1 for true
assigned_to_email = Column(String, nullable=True)
used_at = Column(DateTime, nullable=True)
class ReleaseParticipant(Base):
__tablename__ = "release_participants"
email = Column(String, primary_key=True)
first_name = Column(String)
last_updated = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
class ReleaseHistory(Base):
__tablename__ = "release_history"
id = Column(Integer, primary_key=True)
timestamp = Column(DateTime, default=datetime.datetime.utcnow)
recipient_count = Column(Integer)
scheduled_time = Column(String, nullable=True)
class ReminderHistory(Base):
__tablename__ = "reminder_history"
id = Column(Integer, primary_key=True)
job_id = Column(String, index=True)
timestamp = Column(DateTime, default=datetime.datetime.utcnow)
recipient_count = Column(Integer)
max_logins = Column(Integer)
recipients_json = Column(String) # JSON list of emails/names/children
scheduled_time = Column(String, nullable=True)
class JobParticipant(Base):
__tablename__ = "job_participants"
id = Column(Integer, primary_key=True)
job_id = Column(String, index=True)
child_id = Column(String, nullable=True)
vorname_kind = Column(String, nullable=True)
nachname_kind = Column(String, nullable=True)
vorname_eltern = Column(String, nullable=True)
nachname_eltern = Column(String, nullable=True)
email_eltern = Column(String, nullable=True)
zugangscode = Column(String, index=True)
gruppe = Column(String, nullable=True)
logins = Column(Integer, default=0)
has_orders = Column(Integer, default=0) # 0 for false, 1 for true
digital_package_ordered = Column(Integer, default=0) # 0 for false, 1 for true
quick_login_url = Column(String, nullable=True)
last_synced = Column(DateTime, default=datetime.datetime.utcnow)
Base.metadata.create_all(bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

View File

@@ -0,0 +1,68 @@
import os
from dotenv import load_dotenv
# We import directly from main to reuse the already configured functions
from main import setup_driver, login, get_jobs_list
def run_debug():
"""
Runs the scraper logic directly for debugging purposes inside the container.
"""
load_dotenv()
print("--- Starting Standalone Scraper Debug ---")
# --- Configuration ---
# Change this to 'schule' to test the other account
ACCOUNT_TO_TEST = "kiga"
username = os.getenv(f"{ACCOUNT_TO_TEST.upper()}_USER")
password = os.getenv(f"{ACCOUNT_TO_TEST.upper()}_PW")
if not username or not password:
print(f"!!! FATAL ERROR: Credentials for {ACCOUNT_TO_TEST} not found in .env file.")
print("Please ensure KIGA_USER, KIGA_PW, etc. are set correctly.")
return
print(f"Attempting to log in with user: {username}")
driver = None
try:
driver = setup_driver()
if not driver:
print("!!! FATAL ERROR: WebDriver initialization failed.")
return
# Perform the login
if login(driver, username, password):
print("\n✅ LOGIN SUCCESSFUL!")
print("-----------------------------------------")
print("Now attempting to fetch jobs from the dashboard...")
# Fetch the jobs
jobs = get_jobs_list(driver)
if jobs:
print(f"\n✅ SUCCESS: Found {len(jobs)} jobs!")
for i, job in enumerate(jobs):
print(f" {i+1}. Name: {job['name']}")
print(f" Status: {job['status']}")
print(f" Date: {job['date']}")
else:
print("\n⚠️ WARNING: Login seemed successful, but no jobs were found on the dashboard.")
print("This could be due to incorrect page selectors for the job list.")
else:
print("\n❌ LOGIN FAILED.")
print("Please check credentials in .env and the login selectors in main.py.")
print("A screenshot of the error might have been saved if the scraper has permission.")
except Exception as e:
print(f"\n\n!!! AN UNEXPECTED ERROR OCCURRED: {e}")
import traceback
traceback.print_exc()
finally:
if driver:
print("\n--- Debug script finished. Closing WebDriver. ---")
driver.quit()
if __name__ == "__main__":
run_debug()

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -0,0 +1,140 @@
import os
import json
import logging
import datetime
from typing import Optional, List, Dict, Any
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import Flow
from googleapiclient.discovery import build
from google.auth.transport.requests import Request
from sqlalchemy.orm import Session
from database import GmailToken
import base64
from email.mime.text import MIMEText
logger = logging.getLogger("gmail-service")
# Scopes required for sending emails
SCOPES = ['https://www.googleapis.com/auth/gmail.send']
class GmailService:
def __init__(self, db: Session):
self.db = db
self.client_id = os.getenv("google_fotograf_client_id")
self.client_secret = os.getenv("google_fotograf_secret")
# Redirect URI - must match what was configured in Google Console
# We try to detect the public URL, fallback to duckdns
self.redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", "https://floke-ai.duckdns.org/fotograf-de-api/api/auth/callback")
def _get_client_config(self) -> Dict[str, Any]:
return {
"web": {
"client_id": self.client_id,
"project_id": "fotograf-tool",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_secret": self.client_secret,
"redirect_uris": [self.redirect_uri]
}
}
def get_auth_url(self) -> str:
flow = Flow.from_client_config(
self._get_client_config(),
scopes=SCOPES,
redirect_uri=self.redirect_uri
)
auth_url, _ = flow.authorization_url(prompt='consent', access_type='offline')
return auth_url
def handle_callback(self, code: str):
flow = Flow.from_client_config(
self._get_client_config(),
scopes=SCOPES,
redirect_uri=self.redirect_uri
)
flow.fetch_token(code=code)
credentials = flow.credentials
self._save_token(credentials)
return credentials
def _save_token(self, credentials):
token_data = {
'token': credentials.token,
'refresh_token': credentials.refresh_token,
'token_uri': credentials.token_uri,
'client_id': credentials.client_id,
'client_secret': credentials.client_secret,
'scopes': credentials.scopes
}
db_token = self.db.query(GmailToken).first()
if not db_token:
db_token = GmailToken(token_json=json.dumps(token_data))
self.db.add(db_token)
else:
db_token.token_json = json.dumps(token_data)
self.db.commit()
logger.info("Gmail OAuth token saved to database.")
def get_credentials(self) -> Optional[Credentials]:
db_token = self.db.query(GmailToken).first()
if not db_token:
return None
token_data = json.loads(db_token.token_json)
creds = Credentials.from_authorized_user_info(token_data, SCOPES)
if creds and creds.expired and creds.refresh_token:
logger.info("Gmail token expired, refreshing...")
creds.refresh(Request())
self._save_token(creds)
return creds
def is_authenticated(self) -> bool:
try:
creds = self.get_credentials()
return creds is not None and creds.valid
except Exception as e:
logger.error(f"Auth check failed: {e}")
return False
def send_email(self, to: str, subject: str, body_html: str) -> bool:
creds = self.get_credentials()
if not creds:
logger.error("Cannot send email: Not authenticated.")
return False
try:
# DEV MODE OVERRIDE
dev_email = os.getenv("DEV_MODE_EMAIL_RECIPIENT")
original_to = to
if dev_email:
logger.warning(f"⚠️ DEV MODE ACTIVE: Redirecting email originally intended for {original_to} to {dev_email}")
to = dev_email
service = build('gmail', 'v1', credentials=creds)
message = MIMEText(body_html, 'html')
message['to'] = to
message['subject'] = subject
message['bcc'] = 'kontakt@kinderfotos-erding.de'
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
send_result = service.users().messages().send(
userId='me',
body={'raw': raw_message}
).execute()
if dev_email:
logger.info(f"Test-Email sent to {to} (Original target: {original_to}). Message ID: {send_result['id']}")
else:
logger.info(f"Email sent to {to}. Message ID: {send_result['id']}")
return True
except Exception as e:
logger.error(f"Failed to send email to {to}: {e}")
return False

View File

@@ -0,0 +1,49 @@
import os
import sys
from dotenv import load_dotenv
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from database import Job
from main import setup_driver, login
import time
load_dotenv()
engine = create_engine("sqlite:////app/data/fotograf_jobs.db")
Session = sessionmaker(bind=engine)
db = Session()
# Get latest job
job = db.query(Job).order_by(Job.last_updated.desc()).first()
if not job:
print("No jobs found in database.")
sys.exit(1)
print(f"Using Job ID: {job.id} ({job.name}), Account: {job.account_type}")
username = os.getenv(f"{job.account_type.upper()}_USER")
password = os.getenv(f"{job.account_type.upper()}_PW")
driver = setup_driver()
if not driver:
print("Failed to init driver")
sys.exit(1)
if not login(driver, username, password):
print("Login failed")
driver.quit()
sys.exit(1)
orders_url = f"https://app.fotograf.de/config_jobs_orders/index/{job.id}/customer_orders"
print(f"Navigating to {orders_url}")
driver.get(orders_url)
time.sleep(5) # wait for page to load
html = driver.page_source
with open("orders_page.html", "w", encoding="utf-8") as f:
f.write(html)
driver.save_screenshot("orders_page.png")
print("Saved orders_page.html and orders_page.png")
driver.quit()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
import sqlite3
import os
db_path = "/app/data/fotograf_jobs.db"
if not os.path.exists(db_path):
db_path = "fotograf-de-scraper/backend/data/fotograf_jobs.db"
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
try:
cursor.execute("ALTER TABLE job_participants ADD COLUMN digital_package_ordered INTEGER DEFAULT 0;")
print("Column 'digital_package_ordered' added successfully.")
except sqlite3.OperationalError:
print("Column 'digital_package_ordered' already exists.")
conn.commit()
conn.close()

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -0,0 +1,229 @@
from fastapi import APIRouter, Depends, HTTPException, Request, BackgroundTasks
from pydantic import BaseModel
from sqlalchemy.orm import Session
from database import get_db, DiscountCode, ReleaseParticipant, ReleaseHistory
import datetime
import logging
from gmail_service import GmailService
import re
import time
import asyncio
from typing import List, Dict, Optional
from zoneinfo import ZoneInfo
router = APIRouter(prefix="/api/publish-request", tags=["publish-request"])
logger = logging.getLogger("publish-request")
# Timezone for Berlin
TZ_BERLIN = ZoneInfo("Europe/Berlin")
# Official Project Signature
SIGNATURE_HTML = """
<br><br>
<span style="color: #888;">--</span><br>
<div dir="ltr">
<table border="0" cellspacing="0" cellpadding="0" style="border-collapse:collapse; margin-top: 5px;">
<tbody>
<tr>
<td width="220" valign="top" style="padding-right: 15px;">
<img width="200" src="https://lh3.googleusercontent.com/d/1K7RODOqKE2e1nRJ3D4dEWdjthoTMyXUq" alt="Kinderfotos Erding Logo" style="display: block;">
</td>
<td valign="bottom" style="padding-left: 15px; border-left: 1px solid #ddd; font-family: sans-serif; font-size: 13px; color: #333; line-height: 1.5;">
<p style="margin: 0;"><b>Kinderfotos Erding</b> | <a href="http://www.kinderfotos-erding.de/" target="_blank" style="color: #1155cc; text-decoration: none;">www.kinderfotos-erding.de</a></p>
<p style="margin: 0; color: #666;">Gartenstr. 10 | 85445 Oberding | 08122-8470867</p>
</td>
</tr>
</tbody>
</table>
</div>
"""
class CodesUpload(BaseModel):
codes: str # comma separated
class SendReleaseRequest(BaseModel):
emails: List[Dict[str, str]]
scheduled_time: Optional[str] = None # e.g. "10:00"
participants: Optional[List[Dict[str, str]]] = None # [{email, first_name}]
async def delayed_send(emails: List[Dict[str, str]], scheduled_time: str, db_session_factory):
try:
# Calculate delay using Berlin Timezone
now = datetime.datetime.now(TZ_BERLIN)
target_h, target_m = map(int, scheduled_time.split(":"))
target_time = now.replace(hour=target_h, minute=target_m, second=0, microsecond=0)
if target_time < now:
target_time += datetime.timedelta(days=1)
delay_seconds = (target_time - now).total_seconds()
logger.info(f"Scheduling {len(emails)} emails for {scheduled_time} Berlin Time (in {delay_seconds} seconds)")
await asyncio.sleep(delay_seconds)
# We need a fresh DB session for the background task
db = db_session_factory()
try:
service = GmailService(db)
success_count = 0
for email_data in emails:
if service.send_email(email_data["to"], email_data["subject"], email_data["body"]):
success_count += 1
await asyncio.sleep(1) # Rate limiting
logger.info(f"Scheduled send complete: {success_count}/{len(emails)} success.")
finally:
db.close()
except Exception as e:
logger.exception("Error in delayed_send background task")
@router.post("/send")
async def send_requests(data: SendReleaseRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
# Store participant names for later (webhook)
if data.participants:
for p in data.participants:
email = p.get("email", "").strip().lower()
first_name = p.get("first_name", "").strip()
if email and first_name:
existing = db.query(ReleaseParticipant).filter(ReleaseParticipant.email == email).first()
if existing:
existing.first_name = first_name
else:
db.add(ReleaseParticipant(email=email, first_name=first_name))
db.commit()
if data.scheduled_time:
# Pass a way to get a new session to the background task
from database import SessionLocal
# Log to history
db.add(ReleaseHistory(recipient_count=len(data.emails), scheduled_time=data.scheduled_time))
db.commit()
background_tasks.add_task(delayed_send, data.emails, data.scheduled_time, SessionLocal)
return {"status": "scheduled", "message": f"Versand für {data.scheduled_time} geplant."}
# Log immediate send to history
db.add(ReleaseHistory(recipient_count=len(data.emails), scheduled_time="Sofort"))
db.commit()
# Immediate send
service = GmailService(db)
success = 0
failed = []
for email_data in data.emails:
if service.send_email(email_data["to"], email_data["subject"], email_data["body"]):
success += 1
else:
failed.append(email_data["to"])
return {"status": "success", "success": success, "failed": failed}
@router.get("/history")
def get_history(db: Session = Depends(get_db)):
history = db.query(ReleaseHistory).order_by(ReleaseHistory.timestamp.desc()).all()
return [{"id": h.id, "timestamp": h.timestamp.isoformat(), "recipient_count": h.recipient_count, "scheduled_time": h.scheduled_time} for h in history]
@router.get("/stats")
def get_stats(db: Session = Depends(get_db)):
total = db.query(DiscountCode).count()
used = db.query(DiscountCode).filter(DiscountCode.is_used == 1).count()
available = total - used
return {"total": total, "used": used, "available": available}
@router.get("/responses")
def get_responses(db: Session = Depends(get_db)):
responses = db.query(DiscountCode).filter(DiscountCode.is_used == 1).all()
return [{"email": r.assigned_to_email, "code": r.code, "used_at": r.used_at.isoformat()} for r in responses]
@router.post("/codes")
def upload_codes(data: CodesUpload, db: Session = Depends(get_db)):
codes_list = [c.strip() for c in data.codes.split(",") if c.strip()]
added = 0
for code in set(codes_list):
existing = db.query(DiscountCode).filter(DiscountCode.code == code).first()
if not existing:
new_code = DiscountCode(code=code, is_used=0)
db.add(new_code)
added += 1
db.commit()
return {"status": "success", "added": added}
class WebhookData(BaseModel):
email: str
@router.post("/webhook")
async def handle_webhook(request: Request, db: Session = Depends(get_db)):
# Try to parse JSON from Google Forms webhook
try:
data = await request.json()
except:
raise HTTPException(status_code=400, detail="Invalid JSON")
# We expect {"email": "..."} or similar from the Google Apps Script
email = data.get("email") or data.get("Email")
if not email:
logger.error(f"Webhook received without email: {data}")
return {"status": "error", "message": "Email not found in webhook payload"}
email = email.strip().lower()
# Check if this email already got a code
already_assigned = db.query(DiscountCode).filter(DiscountCode.assigned_to_email == email).first()
if already_assigned:
logger.info(f"Email {email} already received code {already_assigned.code}")
return {"status": "success", "message": "Already sent"}
# Get a free code
free_code = db.query(DiscountCode).filter(DiscountCode.is_used == 0).first()
if not free_code:
logger.error("NO FREE DISCOUNT CODES LEFT!")
return {"status": "error", "message": "No codes available"}
# Look up participant name
participant = db.query(ReleaseParticipant).filter(ReleaseParticipant.email == email).first()
first_name = participant.first_name if participant else "Ihr Lieben"
# Mark as used
free_code.is_used = 1
free_code.assigned_to_email = email
free_code.used_at = datetime.datetime.utcnow()
db.commit()
# Send Thank You Email with GmailService
service = GmailService(db)
subject = "Dankeschön für Eure Freigabe & Euer Rabattcode"
# Image provided by user
INSTRUCTIONS_IMAGE_URL = "https://mail.google.com/mail/u/2?ui=2&ik=719adaa3c5&attid=0.1&permmsgid=msg-a:r7482671925923393616&th=196e322c399dbc7f&view=fimg&fur=ip&permmsgid=msg-a:r7482671925923393616&sz=s0-l75-ft&attbid=ANGjdJ9_U6ayMFgwbupt4HalTKO867IHx6N70eNbPfQmTLNzRXilJxI-n8a1gjM8xVcP5HEOgaVxfp3FnJPzTYEEYhK4gSU-Il_0a6OtzFYscp55_W4iyxuxjyPvK4&disp=emb&realattid=ii_maspzxv50&zw"
body_html = f"""
<p>Hallo {first_name},</p>
<p>Vielen Dank nochmal für die Freigabe zur Veröffentlichung, das ist super nett von Euch!</p>
<p>Hier ist euer Gutscheincode über 25 Euro: <strong style="font-size: 18px; color: #4F46E5;">{free_code.code}</strong></p>
<p>Um den Gutschein einzugeben, musst du auf den Preis des Warenkorbs drücken (über dem Button zur Kasse gehen):</p>
<p><img src="{INSTRUCTIONS_IMAGE_URL}" alt="Anleitung Gutschein einlösen" style="max-width: 100%; border: 1px solid #ddd; border-radius: 8px;"></p>
<p>Liebe Grüße,<br>das Team von Kinderfotos Erding</p>
{SIGNATURE_HTML}
"""
try:
success = service.send_email(email, subject, body_html)
if success:
logger.info(f"Successfully sent code {free_code.code} to {email}")
return {"status": "success", "message": "Email sent"}
else:
logger.error(f"Failed to send email to {email}")
free_code.is_used = 0
free_code.assigned_to_email = None
free_code.used_at = None
db.commit()
return {"status": "error", "message": "Failed to send email"}
except Exception as e:
logger.exception("Error sending webhook email")
free_code.is_used = 0
free_code.assigned_to_email = None
free_code.used_at = None
db.commit()
return {"status": "error", "message": str(e)}

View File

@@ -0,0 +1,353 @@
import os
import requests
import io
import datetime
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from PyPDF2 import PdfReader, PdfWriter
import logging
logger = logging.getLogger("qr-card-generator")
def get_calendly_event_types(api_token: str):
"""
Fetches available event types for the current user.
"""
headers = {
'Authorization': f'Bearer {api_token}',
'Content-Type': 'application/json'
}
# 1. Get current user info
user_url = "https://api.calendly.com/users/me"
user_response = requests.get(user_url, headers=headers)
if not user_response.ok:
raise Exception(f"Calendly API Error: {user_response.status_code}")
user_data = user_response.json()
user_uri = user_data['resource']['uri']
# 2. Get event types
event_types_url = "https://api.calendly.com/event_types"
params = {
'user': user_uri
}
types_response = requests.get(event_types_url, headers=headers, params=params)
if not types_response.ok:
raise Exception(f"Calendly API Error: {types_response.status_code}")
types_data = types_response.json()
return types_data['collection']
def get_calendly_events_raw(api_token: str, start_time: str = None, end_time: str = None, event_type_name: str = None):
"""
Debug function to fetch raw Calendly data without formatting.
"""
headers = {
'Authorization': f'Bearer {api_token}',
'Content-Type': 'application/json'
}
# Defaults: current time to +2 years
if not start_time:
start_time = datetime.datetime.utcnow().isoformat() + "Z"
if not end_time:
end_time = (datetime.datetime.utcnow() + datetime.timedelta(days=730)).isoformat() + "Z"
# 1. Get current user info to get the user URI
user_url = "https://api.calendly.com/users/me"
user_response = requests.get(user_url, headers=headers)
if not user_response.ok:
raise Exception(f"Calendly API Error: {user_response.status_code}")
user_data = user_response.json()
user_uri = user_data['resource']['uri']
# 2. Get events for the user
events_url = "https://api.calendly.com/scheduled_events"
params = {
'user': user_uri,
'status': 'active',
'min_start_time': start_time,
'max_start_time': end_time,
'count': 100
}
all_events = []
url = events_url
while url:
if url == events_url:
response = requests.get(url, headers=headers, params=params)
else:
response = requests.get(url, headers=headers)
if not response.ok:
raise Exception(f"Calendly API Error: {response.status_code} - {response.text}")
data = response.json()
all_events.extend(data.get('collection', []))
pagination = data.get('pagination', {})
url = pagination.get('next_page') # Use the full URL provided by Calendly
raw_results = []
# 3. Get invitees
for event in all_events:
event_name = event.get('name', '')
# Filter by event type if provided
if event_type_name and event_type_name.lower() not in event_name.lower():
continue
event_uri = event['uri']
event_uuid = event_uri.split('/')[-1]
invitees_url = f"https://api.calendly.com/scheduled_events/{event_uuid}/invitees"
invitees_response = requests.get(invitees_url, headers=headers)
if not invitees_response.ok:
continue
invitees_data = invitees_response.json()
for invitee in invitees_data['collection']:
raw_results.append({
"event_name": event_name,
"start_time": event['start_time'],
"invitee_name": invitee['name'],
"invitee_email": invitee['email'],
"questions_and_answers": invitee.get('questions_and_answers', [])
})
return raw_results
def get_calendly_events(api_token: str, start_time: str = None, end_time: str = None, event_type_name: str = None):
"""
Fetches events from Calendly API for the current user within a time range.
"""
from zoneinfo import ZoneInfo
raw_data = get_calendly_events_raw(api_token, start_time, end_time, event_type_name)
formatted_data = []
# Calculate midnight today in Berlin time for filtering
now_berlin = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
midnight_today = now_berlin.replace(hour=0, minute=0, second=0, microsecond=0)
for item in raw_data:
# Parse start time from UTC
start_dt = datetime.datetime.fromisoformat(item['start_time'].replace('Z', '+00:00'))
# Convert to Europe/Berlin (CET/CEST)
start_dt = start_dt.astimezone(ZoneInfo("Europe/Berlin"))
# Filter out past events
if start_dt < midnight_today:
logger.debug(f"Skipping past event: {item['invitee_name']} at {start_dt}")
continue
logger.info(f"Processing event: {item['invitee_name']} at {start_dt}")
# Format as HH:MM
time_str = start_dt.strftime('%H:%M')
name = item['invitee_name']
# Extract specific answers from the Calendly form
num_children = ""
additional_notes = ""
has_consent = False
questions_and_answers = item.get('questions_and_answers', [])
for q_a in questions_and_answers:
q_text = q_a.get('question', '').lower()
a_text = q_a.get('answer', '')
# Flexible matching for number of children
if any(kw in q_text for kw in ["wie viele kinder", "anzahl kinder", "wieviele kinder"]):
num_children = a_text
elif "nachricht" in q_text or "anmerkung" in q_text:
additional_notes = a_text
elif "veröffentlichen" in q_text or "bilder" in q_text:
if "ja" in a_text.lower():
has_consent = True
# Construct the final string: "Name, X Kinder // HH:MM Uhr ☑"
final_text = f"{name}"
if num_children:
final_text += f", {num_children}"
final_text += f" // {time_str} Uhr"
if additional_notes:
final_text += f" ({additional_notes})"
if has_consent:
final_text += ""
formatted_data.append(final_text)
logger.info(f"Processed {len(formatted_data)} invitees.")
return formatted_data
def overlay_text_on_pdf(base_pdf_path: str, output_pdf_path: str, texts: list):
"""
Target:
Element 1: X: 72mm, Y: 22mm + 9mm = 31mm
Element 2: X: 72mm, Y: 171mm + 9mm = 180mm
"""
# Convert mm to points (1 mm = 2.83465 points)
mm_to_pt = 2.83465
# A4 dimensions in points (approx 595.27 x 841.89)
page_width, page_height = A4
# User coordinates are from top-left.
# ReportLab uses bottom-left as (0,0).
# Element 1 (Top): X = 72mm, Y = 31mm (from top) -> Y = page_height - 31mm
# Element 2 (Bottom): X = 72mm, Y = 180mm (from top) -> Y = page_height - 180mm
x_pos = 72 * mm_to_pt
y_pos_1 = page_height - (31 * mm_to_pt)
y_pos_2 = page_height - (180 * mm_to_pt)
reader = PdfReader(base_pdf_path)
writer = PdfWriter()
total_pages = len(reader.pages)
max_capacity = total_pages * 2
if len(texts) > max_capacity:
logger.warning(f"Not enough pages in base PDF. Have {len(texts)} invitees but only space for {max_capacity}. Truncating.")
texts = texts[:max_capacity]
# We need to process pairs of texts for each page
text_pairs = [texts[i:i+2] for i in range(0, len(texts), 2)]
# Load OpenSans font to support UTF-8 extended characters
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase import pdfmetrics
font_path = os.path.join(os.path.dirname(__file__), "assets", "OpenSans-Regular.ttf")
pdfmetrics.registerFont(TTFont('OpenSans', font_path))
for page_idx, pair in enumerate(text_pairs):
if page_idx >= total_pages:
break # Safety first
# Create a new blank page in memory to draw the text
packet = io.BytesIO()
can = canvas.Canvas(packet, pagesize=A4)
# Draw the text.
def draw_text_with_checkbox(can, x, y, text):
can.setFont("OpenSans", 12)
if text.endswith(""):
clean_text = text[:-2] # remove the checkmark part
can.drawString(x, y, clean_text)
# Calculate width to place the checkbox right after the text
text_width = can.stringWidth(clean_text, "OpenSans", 12)
box_x = x + text_width + 8
size = 10
can.rect(box_x, y - 1, size, size)
can.setLineWidth(1.5)
can.line(box_x + 2, y + 3, box_x + 4.5, y + 0.5)
can.line(box_x + 4.5, y + 0.5, box_x + 8.5, y + 7)
can.setLineWidth(1)
else:
can.drawString(x, y, text)
if len(pair) > 0:
draw_text_with_checkbox(can, x_pos, y_pos_1, pair[0])
if len(pair) > 1:
draw_text_with_checkbox(can, x_pos, y_pos_2, pair[1])
can.save()
packet.seek(0)
# Read the text PDF we just created
new_pdf = PdfReader(packet)
text_page = new_pdf.pages[0]
# Get the specific page from the original PDF
page_to_merge = reader.pages[page_idx]
page_to_merge.merge_page(text_page)
writer.add_page(page_to_merge)
# If there are pages left in the base PDF that we didn't use, append them too?
# Usually you'd want to keep them or discard them. We'll discard unused pages for now
# to avoid empty cards, or you can change this loop to include them.
with open(output_pdf_path, "wb") as output_file:
writer.write(output_file)
logger.info(f"Successfully generated overlaid PDF at {output_pdf_path}")
def generate_siblings_qr_overlay(base_pdf_path: str, output_pdf_path: str, families: list):
import io
from PyPDF2 import PdfReader, PdfWriter
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import os
font_path = os.path.join(os.path.dirname(__file__), "assets", "OpenSans-Regular.ttf")
if os.path.exists(font_path):
pdfmetrics.registerFont(TTFont('OpenSans', font_path))
font_name = 'OpenSans'
else:
font_name = 'Helvetica'
mm_to_pt = 2.83465
page_width, page_height = A4
x_pos = 72 * mm_to_pt
y_pos_1 = page_height - (31 * mm_to_pt)
y_pos_2 = page_height - (180 * mm_to_pt)
reader = PdfReader(base_pdf_path)
writer = PdfWriter()
family_idx = 0
total_families = len(families)
for i in range(len(reader.pages)):
page = reader.pages[i]
if family_idx < total_families:
packet = io.BytesIO()
c = canvas.Canvas(packet, pagesize=A4)
c.setFont(font_name, 11)
# First card on the page
if family_idx < total_families:
text_top = f"Geschwisterbilder Familie {families[family_idx]['nachname']}"
c.drawString(x_pos, y_pos_1, text_top)
family_idx += 1
# Second card on the page
if family_idx < total_families:
text_bottom = f"Geschwisterbilder Familie {families[family_idx]['nachname']}"
c.drawString(x_pos, y_pos_2, text_bottom)
family_idx += 1
c.save()
packet.seek(0)
overlay_pdf = PdfReader(packet)
page.merge_page(overlay_pdf.pages[0])
writer.add_page(page)
with open(output_pdf_path, "wb") as output_file:
writer.write(output_file)

View File

@@ -0,0 +1,17 @@
fastapi==0.111.0
uvicorn==0.30.1
python-dotenv==1.0.1
selenium==4.22.0
webdriver-manager==4.0.1
pandas==2.2.2
weasyprint==62.1
jinja2==3.1.4
pydyf==0.10.0
sqlalchemy==2.0.31
requests==2.31.0
reportlab==4.0.9
PyPDF2==3.0.1
tzdata
google-api-python-client==2.122.0
google-auth-httplib2==0.2.0
google-auth-oauthlib==1.2.0

View File

@@ -0,0 +1,183 @@
import pandas as pd
import os
import logging
from jinja2 import Environment, FileSystemLoader
from collections import defaultdict
from main import get_berlin_now_str, get_logo_base64
from weasyprint import HTML
logger = logging.getLogger("fotograf-scraper")
def generate_siblings_pdf_from_csv(csv_path: str, institution: str, calendly_events: list, list_type: str, output_path: str):
logger.info(f"Generating Siblings PDF for {institution} from {csv_path}")
df = None
for sep in [";", ","]:
try:
test_df = pd.read_csv(csv_path, sep=sep, encoding="utf-8-sig", nrows=5)
if len(test_df.columns) > 1:
df = pd.read_csv(csv_path, sep=sep, encoding="utf-8-sig")
break
except Exception as e:
continue
if df is None:
try:
df = pd.read_csv(csv_path, sep=";", encoding="latin1")
except:
raise Exception("CSV konnte nicht gelesen werden.")
df.columns = df.columns.str.strip().str.replace('"', "")
# Identify Email Column
email_col = next((c for c in df.columns if "email" in c.lower()), None)
if not email_col:
email_col = next((c for c in df.columns if "e-mail" in c.lower()), None)
if not email_col:
logger.warning("No email column found. Siblings logic cannot run.")
families = []
else:
# Columns mappings
group_col = next((c for c in df.columns if c.lower() in ["gruppe", "klasse", "group", "class"]), None)
lastname_col = next((c for c in df.columns if "nachname" in c.lower()), None)
firstname_col = next((c for c in df.columns if "vorname" in c.lower()), None)
wunsch_col = next((c for c in df.columns if "familie" in c.lower() or "geschwister" in c.lower() and "fotos" in c.lower()), None)
if not wunsch_col:
wunsch_col = next((c for c in df.columns if "familie / geschwister" in c.lower()), None)
# Build Calendly Dictionary for fast lookup (Email -> Time)
from zoneinfo import ZoneInfo
import datetime
calendly_map = {}
now_berlin = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
midnight_today = now_berlin.replace(hour=0, minute=0, second=0, microsecond=0)
for event in calendly_events:
try:
start_dt = datetime.datetime.fromisoformat(event['start_time'].replace('Z', '+00:00'))
start_dt = start_dt.astimezone(ZoneInfo("Europe/Berlin"))
calendly_map[event['invitee_email'].lower().strip()] = start_dt.strftime("%d.%m. %H:%M")
except:
pass
families_dict = defaultdict(list)
df = df.fillna("")
# Group by email
for _, row in df.iterrows():
email = str(row[email_col]).strip().lower()
if email and "@" in email:
families_dict[email].append(row)
families = []
for email, rows in families_dict.items():
if len(rows) > 1: # SIBLINGS DETECTED
family_last_name = str(rows[0][lastname_col]).strip() if lastname_col else "Unbekannt"
children = []
for r in rows:
child_first = str(r[firstname_col]).strip() if firstname_col else ""
child_group = str(r[group_col]).strip() if group_col else ""
children.append({"vorname": child_first, "gruppe": child_group})
# Check fotograf wunsch
fotograf_wunsch = False
if wunsch_col:
for r in rows:
val = str(r[wunsch_col]).lower()
if "ja" in val or "familien" in val or "geschwister" in val:
fotograf_wunsch = True
break
calendly_time = calendly_map.get(email, None)
families.append({
"nachname": family_last_name,
"children": children,
"fotograf_wunsch": fotograf_wunsch,
"calendly_time": calendly_time
})
# Sort by last name
families.sort(key=lambda x: x["nachname"])
template_dir = os.path.join(os.path.dirname(__file__), "templates")
env = Environment(loader=FileSystemLoader(template_dir))
template = env.get_template("siblings_list.html")
current_time = get_berlin_now_str()
logo_base64 = get_logo_base64()
render_context = {
"institution": institution,
"current_time": current_time,
"logo_base64": logo_base64,
"families": families
}
html_out = template.render(render_context)
pdf = HTML(string=html_out).write_pdf()
with open(output_path, "wb") as f:
f.write(pdf)
logger.info(f"Siblings PDF saved to {output_path}")
def get_sibling_families_from_csv(csv_path: str, calendly_events: list = None) -> list:
df = None
for sep in [";", ","]:
try:
test_df = pd.read_csv(csv_path, sep=sep, encoding="utf-8-sig", nrows=5)
if len(test_df.columns) > 1:
df = pd.read_csv(csv_path, sep=sep, encoding="utf-8-sig")
break
except Exception as e:
continue
if df is None:
try:
df = pd.read_csv(csv_path, sep=";", encoding="latin1")
except:
raise Exception("CSV konnte nicht gelesen werden.")
df.columns = df.columns.str.strip().str.replace('"', "")
email_col = next((c for c in df.columns if "email" in c.lower()), None)
if not email_col:
email_col = next((c for c in df.columns if "e-mail" in c.lower()), None)
if not email_col:
return []
lastname_col = next((c for c in df.columns if "nachname" in c.lower()), None)
# Build Calendly Email Set for filtering
booked_emails = set()
if calendly_events:
for event in calendly_events:
email = event.get('invitee_email', '').lower().strip()
if email:
booked_emails.add(email)
families_dict = defaultdict(list)
df = df.fillna("")
for _, row in df.iterrows():
email = str(row[email_col]).strip().lower()
if email and "@" in email:
families_dict[email].append(row)
families = []
for email, rows in families_dict.items():
if len(rows) > 1: # SIBLINGS DETECTED
# FILTER OUT if they already have an appointment
if email in booked_emails:
logger.info(f"Family {email} already has Calendly appointment, skipping QR card.")
continue
family_last_name = str(rows[0][lastname_col]).strip() if lastname_col else "Unbekannt"
families.append({
"nachname": family_last_name
})
families.sort(key=lambda x: x["nachname"])
return families

View File

@@ -0,0 +1,202 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Terminübersicht</title>
<style>
@page {
size: A4 portrait;
margin: 20mm;
@bottom-right {
content: "Seite " counter(page) " von " counter(pages);
font-family: Arial, sans-serif;
font-size: 10pt;
color: #666;
}
}
body {
font-family: Arial, sans-serif;
font-size: 11pt;
color: #333;
line-height: 1.4;
}
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 2px solid #ddd;
padding-bottom: 10px;
margin-bottom: 20px;
}
.header-text {
flex: 1;
}
.header-logo {
width: 150px;
text-align: right;
}
.header-logo img {
max-width: 100%;
height: auto;
}
h1 {
font-size: 16pt;
margin: 0 0 5px 0;
color: #2c3e50;
}
h2 {
font-size: 14pt;
margin: 0 0 10px 0;
color: #34495e;
}
.date-header {
background-color: #ecf0f1;
padding: 8px 12px;
margin-top: 20px;
margin-bottom: 10px;
font-weight: bold;
font-size: 13pt;
border-left: 4px solid #3498db;
page-break-before: always;
}
.first-date-header {
page-break-before: avoid;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
page-break-inside: auto;
}
th, td {
border: 1px solid #bdc3c7;
padding: 6px 8px; /* Narrower rows */
text-align: left;
vertical-align: middle;
}
.empty-row td {
height: 25px; /* Narrower empty rows */
color: transparent;
}
.compressed-row td {
background-color: #fcfcfc;
color: #7f8c8d !important;
font-style: italic;
text-align: center;
}
.time-col { width: 14%; white-space: nowrap; font-weight: bold; }
.family-col { width: 33%; }
.children-col { width: 15%; text-align: center; }
.consent-col { width: 20%; text-align: center; }
.done-col { width: 18%; text-align: center; }
.empty-row td {
height: 35px; /* ensure enough space for writing */
color: transparent; /* visually hide "Empty" text but keep structure if any */
}
/* CSS Checkmark (Ja) */
.consent-yes {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #333;
border-radius: 3px;
position: relative;
background-color: #fcfcfc;
}
.consent-yes::after {
content: '';
position: absolute;
left: 4px;
top: 0px;
width: 5px;
height: 10px;
border: solid #27ae60;
border-width: 0 3px 3px 0;
transform: rotate(45deg);
}
/* The checkbox square */
.checkbox-square {
display: inline-block;
width: 18px;
height: 18px;
border: 1px solid #333;
background-color: #fff;
position: relative;
}
</style>
</head>
<body>
{% for date, slots in grouped_slots.items() %}
{% if not loop.first %}
<div style="page-break-before: always;"></div>
{% endif %}
<div class="header">
<div class="header-text">
<h1>{{ event_type_name }}</h1>
<p>Auftrag: {{ job_name }} | Stand: {{ current_time }}</p>
</div>
<div class="header-logo">
{% if logo_base64 %}
<img src="data:image/png;base64,{{ logo_base64 }}" alt="Logo">
{% endif %}
</div>
</div>
<div class="date-header first-date-header">{{ date }}</div>
<table>
<thead>
<tr>
<th class="time-col">Uhrzeit</th>
<th class="family-col">Familie</th>
<th class="children-col">Kinder</th>
<th class="consent-col">Veröffentlichung</th>
<th class="done-col">Erledigt</th>
</tr>
</thead>
<tbody>
{% for slot in slots %}
{% if slot.is_compressed %}
<tr class="compressed-row">
<td class="time-col" style="color: #7f8c8d;">{{ slot.time_str }}</td>
<td colspan="4">{{ slot.name }}</td>
</tr>
{% else %}
<tr class="{% if not slot.booked %}empty-row{% endif %}">
<td class="time-col" style="color: #333;">{{ slot.time_str }}</td>
<td class="family-col">{{ slot.name if slot.booked else '' }}</td>
<td class="children-col">{{ slot.children if slot.booked else '' }}</td>
<td class="consent-col">
{% if slot.booked and slot.consent %}
<span class="consent-yes"></span>
{% elif slot.booked %}
<!-- nein -->
{% else %}
<!-- leer -->
{% endif %}
</td>
<td class="done-col">
{% if slot.booked %}
<span class="checkbox-square"></span>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endfor %}
</body>
</html>

View File

@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html><head><meta charset="utf-8"><style>
@page { size: A4 portrait; margin: 20mm; }
body { font-family: Arial, sans-serif; font-size: 11pt; }
.header { margin-bottom: 20px; }
.institution-name { font-weight: bold; font-size: 14pt; }
.date-info { font-size: 12pt; }
.summary { margin-top: 30px; }
.summary h2 { font-size: 12pt; font-weight: normal; margin-bottom: 10px; }
.summary-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
.summary-table td { padding: 4px 0; }
.summary-total { margin-top: 10px; border-top: 1px solid black; padding-top: 10px; font-weight: bold; }
.class-section { page-break-before: always; }
.student-table { width: 100%; border-collapse: collapse; margin-top: 30px; }
.student-table th { text-align: left; border-bottom: 1px solid black; padding-bottom: 5px; font-weight: normal; }
.student-table td { padding: 5px 0; }
.class-summary { margin-top: 30px; font-weight: bold; }
.class-note { margin-top: 20px; font-size: 10pt; }
.footer { position: fixed; bottom: 0; left: 0; right: 0; display: flex; justify-content: space-between; font-size: 10pt; }
.footer-left { text-align: left; }
.footer-right { text-align: right; }
</style></head><body>
<div class="header" style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div class="institution-name">{{ institution }}</div>
<div class="date-info">{{ date_info }}</div>
</div>
{% if logo_base64 %}
<div>
<img src="data:image/png;base64,{{ logo_base64 }}" alt="Logo" style="max-height: 60px;">
</div>
{% endif %}
</div>
<div class="summary"><h2>Übersicht der Anmeldungen:</h2><table class="summary-table">
{% for count in class_counts %}
<tr><td style="width: 50%;">{{ group_label }} {{ count.name }}</td><td>{{ count.count }} Anmeldungen</td></tr>
{% endfor %}
</table><div class="summary-total">Gesamt: {{ total_students }} Anmeldungen</div></div>
{% for class_info in class_data %}
<div class="class-section">
<div class="header" style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div class="institution-name">{{ institution }}</div>
<div class="date-info">{{ date_info }}</div>
</div>
{% if logo_base64 %}
<div>
<img src="data:image/png;base64,{{ logo_base64 }}" alt="Logo" style="max-height: 60px;">
</div>
{% endif %}
</div>
<table class="student-table"><thead><tr><th style="width: 40%">Nachname</th><th style="width: 40%">Vorname</th><th style="width: 20%">{{ group_label }}</th></tr></thead><tbody>
{% for student in class_info.students %}
<tr><td>{{ student.Nachname }}</td><td>{{ student.Vorname }}</td><td>{{ student[group_column_name] }}</td></tr>
{% endfor %}
</tbody></table>
<div class="class-summary">{{ class_info.students|length }} angemeldete {{ person_label_plural }}</div>
<div class="class-note">Dies ist die Liste der bereits angemeldeten {{ person_label_plural }}. Bitte die noch fehlenden<br>{{ person_label_plural }} an die Anmeldung erinnern.</div>
</div>
{% endfor %}
<div class="footer"><div class="footer-left">Stand {{ current_time }}</div><div class="footer-right">Kinderfotos Erding<br>Gartenstr. 10 85445 Oberding<br>www.kinderfotos-erding.de<br>08122-8470867</div></div>
</body></html>

View File

@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
@page { size: A4 portrait; margin: 20mm; }
body { font-family: Arial, sans-serif; font-size: 11pt; }
.header { margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; }
.institution-name { font-weight: bold; font-size: 16pt; margin-bottom: 5px; }
.doc-title { font-size: 14pt; font-weight: bold; color: #4f46e5; margin-bottom: 15px; }
.date-info { font-size: 11pt; color: #555; }
table { width: 100%; border-collapse: collapse; margin-top: 15px; }
th { text-align: left; background-color: #f3f4f6; border-bottom: 2px solid #d1d5db; padding: 8px 5px; font-size: 10pt; }
td { padding: 8px 5px; border-bottom: 1px solid #e5e7eb; font-size: 10pt; vertical-align: top; }
.checkbox { width: 20px; height: 20px; border: 1.5px solid #000; border-radius: 3px; display: inline-block; }
.footer { position: fixed; bottom: 0; left: 0; right: 0; display: flex; justify-content: space-between; font-size: 9pt; color: #888; }
.badge { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 8.5pt; font-weight: bold; background-color: #e0e7ff; color: #3730a3; margin-left: 5px; }
.badge-time { background-color: #d1fae5; color: #065f46; font-size: 10pt; }
</style>
</head>
<body>
<div class="header" style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div class="institution-name">{{ institution }}</div>
<div class="doc-title">Geschwisterliste (Einrichtungsintern)</div>
<div class="date-info">Generiert am: {{ current_time }}</div>
</div>
{% if logo_base64 %}
<div>
<img src="data:image/png;base64,{{ logo_base64 }}" alt="Logo" style="max-height: 50px;">
</div>
{% endif %}
</div>
<table>
<thead>
<tr>
<th style="width: 20%">Nachname</th>
<th style="width: 35%">Kinder in der Einrichtung (Gruppe)</th>
<th style="width: 15%">Wunsch Online</th>
<th style="width: 20%">Termin (Calendly)</th>
<th style="width: 10%; text-align: center;">Erledigt</th>
</tr>
</thead>
<tbody>
{% for family in families %}
<tr>
<td style="font-weight: bold;">{{ family.nachname }}</td>
<td>
{% for child in family.children %}
<div style="margin-bottom: 4px;">
{{ child.vorname }} <span style="color: #666; font-size: 9pt;">({{ child.gruppe }})</span>
</div>
{% endfor %}
</td>
<td>
{% if family.fotograf_wunsch %}
<span style="color: #059669; font-weight: bold;">Ja</span>
{% else %}
<span style="color: #9ca3af;">-</span>
{% endif %}
</td>
<td>
{% if family.calendly_time %}
<span class="badge badge-time">{{ family.calendly_time }}</span>
{% else %}
<span style="color: #9ca3af;">-</span>
{% endif %}
</td>
<td style="text-align: center;">
<div class="checkbox"></div>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" style="text-align: center; padding: 20px; color: #666;">Keine internen Geschwisterkinder in dieser Einrichtung gefunden.</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="footer">
<div>Geschwisterliste</div>
<div>Kinderfotos Erding | www.kinderfotos-erding.de</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,44 @@
import sys
import os
sys.path.append('/app/fotograf-de-scraper/backend')
from database import SessionLocal, ReleaseParticipant, DiscountCode
from gmail_service import GmailService
from publish_request_api import SIGNATURE_HTML
def test_webhook_mail():
db = SessionLocal()
# Simulate data
test_email = "floke.com@gmail.com"
first_name = "Christian"
test_code = "M984AU-TEST"
# Simulate logic
service = GmailService(db)
subject = "Dankeschön für Eure Freigabe & Euer Rabattcode"
INSTRUCTIONS_IMAGE_URL = "https://mail.google.com/mail/u/2?ui=2&ik=719adaa3c5&attid=0.1&permmsgid=msg-a:r7482671925923393616&th=196e322c399dbc7f&view=fimg&fur=ip&permmsgid=msg-a:r7482671925923393616&sz=s0-l75-ft&attbid=ANGjdJ9_U6ayMFgwbupt4HalTKO867IHx6N70eNbPfQmTLNzRXilJxI-n8a1gjM8xVcP5HEOgaVxfp3FnJPzTYmEEyhK4gSU-Il_0a6OtzFYscp55_W4iyxuxjyPvK4&disp=emb&realattid=ii_maspzxv50&zw"
body_html = f"""
<p>Hallo {first_name},</p>
<p>Vielen Dank nochmal für die Freigabe zur Veröffentlichung, das ist super nett von Euch!</p>
<p>Hier ist euer Gutscheincode über 25 Euro: <strong style="font-size: 18px; color: #4F46E5;">{test_code}</strong></p>
<p>Um den Gutschein einzugeben, musst du auf den Preis des Warenkorbs drücken (über dem Button zur Kasse gehen):</p>
<p><img src="{INSTRUCTIONS_IMAGE_URL}" alt="Anleitung Gutschein einlösen" style="max-width: 100%; border: 1px solid #ddd; border-radius: 8px;"></p>
<p>Liebe Grüße,<br>das Team von Kinderfotos Erding</p>
{SIGNATURE_HTML}
"""
print(f"Sende Test-E-Mail an {test_email}...")
success = service.send_email(test_email, subject, body_html)
if success:
print("✅ E-Mail erfolgreich gesendet! Bitte prüfe dein Postfach.")
else:
print("❌ Fehler beim Senden. (Stelle sicher, dass Gmail Authentifiziert ist).")
db.close()
if __name__ == "__main__":
test_webhook_mail()

Binary file not shown.

View File

@@ -0,0 +1,30 @@
version: '3.8'
services:
fotograf-de-scraper-backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: fotograf-de-scraper-backend
env_file:
- ./.env
ports:
- "8002:8000" # Map internal 8000 to external 8002 to avoid conflicts
volumes:
- ./backend:/app # Mount the backend code for easier development
- ./backend/data:/app/data # Persistent data storage
restart: unless-stopped
fotograf-de-scraper-frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: fotograf-de-scraper-frontend
ports:
- "3009:80" # Map internal 80 to external 3009
depends_on:
- fotograf-de-scraper-backend
volumes:
- ./frontend:/app # Mount the frontend code for easier development
restart: unless-stopped

View File

@@ -0,0 +1,40 @@
# Stage 1: Build the React application
FROM node:20-alpine AS builder
WORKDIR /app
# Accept build arguments
ARG VITE_API_BASE_URL
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application source code
COPY . .
# Write the build arg to .env.production so Vite picks it up during build
RUN echo "VITE_API_BASE_URL=${VITE_API_BASE_URL}" > .env.production
# Build the application
RUN npm run build
# Stage 2: Serve the application with Nginx
FROM nginx:alpine
# Set working directory
WORKDIR /usr/share/nginx/html
# Remove default Nginx assets
RUN rm -rf ./*
# Copy built assets from the builder stage
COPY --from=builder /app/dist .
# Expose port 80
EXPOSE 80
# Start Nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<text y=".9em" font-size="90">📸</text>
</svg>

After

Width:  |  Height:  |  Size: 113 B

View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/fotograf-de/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fotograf.de ERP</title>
<script type="module" crossorigin src="/fotograf-de/assets/index-DnGj5v5p.js"></script>
<link rel="stylesheet" crossorigin href="/fotograf-de/assets/index-BnIZj8RP.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fotograf.de ERP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

1
fotograf-de-scraper/frontend/node_modules/.bin/acorn generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../acorn/bin/acorn

View File

@@ -0,0 +1 @@
../autoprefixer/bin/autoprefixer

View File

@@ -0,0 +1 @@
../baseline-browser-mapping/dist/cli.cjs

View File

@@ -0,0 +1 @@
../browserslist/cli.js

1
fotograf-de-scraper/frontend/node_modules/.bin/cssesc generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../cssesc/bin/cssesc

1
fotograf-de-scraper/frontend/node_modules/.bin/eslint generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../eslint/bin/eslint.js

1
fotograf-de-scraper/frontend/node_modules/.bin/jiti generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../jiti/bin/jiti.js

1
fotograf-de-scraper/frontend/node_modules/.bin/js-yaml generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../js-yaml/bin/js-yaml.js

1
fotograf-de-scraper/frontend/node_modules/.bin/jsesc generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../jsesc/bin/jsesc

1
fotograf-de-scraper/frontend/node_modules/.bin/json5 generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../json5/lib/cli.js

1
fotograf-de-scraper/frontend/node_modules/.bin/nanoid generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../nanoid/bin/nanoid.cjs

View File

@@ -0,0 +1 @@
../which/bin/node-which

1
fotograf-de-scraper/frontend/node_modules/.bin/parser generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../@babel/parser/bin/babel-parser.js

1
fotograf-de-scraper/frontend/node_modules/.bin/resolve generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../resolve/bin/resolve

1
fotograf-de-scraper/frontend/node_modules/.bin/rolldown generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../rolldown/bin/cli.mjs

1
fotograf-de-scraper/frontend/node_modules/.bin/semver generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../semver/bin/semver.js

1
fotograf-de-scraper/frontend/node_modules/.bin/sucrase generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../sucrase/bin/sucrase

View File

@@ -0,0 +1 @@
../sucrase/bin/sucrase-node

1
fotograf-de-scraper/frontend/node_modules/.bin/tailwind generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../tailwindcss/lib/cli.js

View File

@@ -0,0 +1 @@
../tailwindcss/lib/cli.js

1
fotograf-de-scraper/frontend/node_modules/.bin/tsc generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../typescript/bin/tsc

1
fotograf-de-scraper/frontend/node_modules/.bin/tsserver generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../typescript/bin/tsserver

View File

@@ -0,0 +1 @@
../update-browserslist-db/cli.js

1
fotograf-de-scraper/frontend/node_modules/.bin/vite generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../vite/bin/vite.js

View File

@@ -0,0 +1 @@
{"root":["../../src/App.tsx","../../src/main.tsx"],"version":"5.9.3"}

View File

@@ -0,0 +1 @@
{"root":["../../vite.config.ts"],"version":"5.9.3"}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -0,0 +1,770 @@
//#region \0rolldown/runtime.js
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
//#endregion
//#region node_modules/react/cjs/react.development.js
/**
* @license React
* react.development.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var require_react_development = /* @__PURE__ */ __commonJSMin(((exports, module) => {
(function() {
function defineDeprecationWarning(methodName, info) {
Object.defineProperty(Component.prototype, methodName, { get: function() {
console.warn("%s(...) is deprecated in plain JavaScript React classes. %s", info[0], info[1]);
} });
}
function getIteratorFn(maybeIterable) {
if (null === maybeIterable || "object" !== typeof maybeIterable) return null;
maybeIterable = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable["@@iterator"];
return "function" === typeof maybeIterable ? maybeIterable : null;
}
function warnNoop(publicInstance, callerName) {
publicInstance = (publicInstance = publicInstance.constructor) && (publicInstance.displayName || publicInstance.name) || "ReactClass";
var warningKey = publicInstance + "." + callerName;
didWarnStateUpdateForUnmountedComponent[warningKey] || (console.error("Can't call %s on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the %s component.", callerName, publicInstance), didWarnStateUpdateForUnmountedComponent[warningKey] = !0);
}
function Component(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
function ComponentDummy() {}
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
function noop() {}
function testStringCoercion(value) {
return "" + value;
}
function checkKeyStringCoercion(value) {
try {
testStringCoercion(value);
var JSCompiler_inline_result = !1;
} catch (e) {
JSCompiler_inline_result = !0;
}
if (JSCompiler_inline_result) {
JSCompiler_inline_result = console;
var JSCompiler_temp_const = JSCompiler_inline_result.error;
var JSCompiler_inline_result$jscomp$0 = "function" === typeof Symbol && Symbol.toStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object";
JSCompiler_temp_const.call(JSCompiler_inline_result, "The provided key is an unsupported type %s. This value must be coerced to a string before using it here.", JSCompiler_inline_result$jscomp$0);
return testStringCoercion(value);
}
}
function getComponentNameFromType(type) {
if (null == type) return null;
if ("function" === typeof type) return type.$$typeof === REACT_CLIENT_REFERENCE ? null : type.displayName || type.name || null;
if ("string" === typeof type) return type;
switch (type) {
case REACT_FRAGMENT_TYPE: return "Fragment";
case REACT_PROFILER_TYPE: return "Profiler";
case REACT_STRICT_MODE_TYPE: return "StrictMode";
case REACT_SUSPENSE_TYPE: return "Suspense";
case REACT_SUSPENSE_LIST_TYPE: return "SuspenseList";
case REACT_ACTIVITY_TYPE: return "Activity";
}
if ("object" === typeof type) switch ("number" === typeof type.tag && console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."), type.$$typeof) {
case REACT_PORTAL_TYPE: return "Portal";
case REACT_CONTEXT_TYPE: return type.displayName || "Context";
case REACT_CONSUMER_TYPE: return (type._context.displayName || "Context") + ".Consumer";
case REACT_FORWARD_REF_TYPE:
var innerType = type.render;
type = type.displayName;
type || (type = innerType.displayName || innerType.name || "", type = "" !== type ? "ForwardRef(" + type + ")" : "ForwardRef");
return type;
case REACT_MEMO_TYPE: return innerType = type.displayName || null, null !== innerType ? innerType : getComponentNameFromType(type.type) || "Memo";
case REACT_LAZY_TYPE:
innerType = type._payload;
type = type._init;
try {
return getComponentNameFromType(type(innerType));
} catch (x) {}
}
return null;
}
function getTaskName(type) {
if (type === REACT_FRAGMENT_TYPE) return "<>";
if ("object" === typeof type && null !== type && type.$$typeof === REACT_LAZY_TYPE) return "<...>";
try {
var name = getComponentNameFromType(type);
return name ? "<" + name + ">" : "<...>";
} catch (x) {
return "<...>";
}
}
function getOwner() {
var dispatcher = ReactSharedInternals.A;
return null === dispatcher ? null : dispatcher.getOwner();
}
function UnknownOwner() {
return Error("react-stack-top-frame");
}
function hasValidKey(config) {
if (hasOwnProperty.call(config, "key")) {
var getter = Object.getOwnPropertyDescriptor(config, "key").get;
if (getter && getter.isReactWarning) return !1;
}
return void 0 !== config.key;
}
function defineKeyPropWarningGetter(props, displayName) {
function warnAboutAccessingKey() {
specialPropKeyWarningShown || (specialPropKeyWarningShown = !0, console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)", displayName));
}
warnAboutAccessingKey.isReactWarning = !0;
Object.defineProperty(props, "key", {
get: warnAboutAccessingKey,
configurable: !0
});
}
function elementRefGetterWithDeprecationWarning() {
var componentName = getComponentNameFromType(this.type);
didWarnAboutElementRef[componentName] || (didWarnAboutElementRef[componentName] = !0, console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release."));
componentName = this.props.ref;
return void 0 !== componentName ? componentName : null;
}
function ReactElement(type, key, props, owner, debugStack, debugTask) {
var refProp = props.ref;
type = {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
props,
_owner: owner
};
null !== (void 0 !== refProp ? refProp : null) ? Object.defineProperty(type, "ref", {
enumerable: !1,
get: elementRefGetterWithDeprecationWarning
}) : Object.defineProperty(type, "ref", {
enumerable: !1,
value: null
});
type._store = {};
Object.defineProperty(type._store, "validated", {
configurable: !1,
enumerable: !1,
writable: !0,
value: 0
});
Object.defineProperty(type, "_debugInfo", {
configurable: !1,
enumerable: !1,
writable: !0,
value: null
});
Object.defineProperty(type, "_debugStack", {
configurable: !1,
enumerable: !1,
writable: !0,
value: debugStack
});
Object.defineProperty(type, "_debugTask", {
configurable: !1,
enumerable: !1,
writable: !0,
value: debugTask
});
Object.freeze && (Object.freeze(type.props), Object.freeze(type));
return type;
}
function cloneAndReplaceKey(oldElement, newKey) {
newKey = ReactElement(oldElement.type, newKey, oldElement.props, oldElement._owner, oldElement._debugStack, oldElement._debugTask);
oldElement._store && (newKey._store.validated = oldElement._store.validated);
return newKey;
}
function validateChildKeys(node) {
isValidElement(node) ? node._store && (node._store.validated = 1) : "object" === typeof node && null !== node && node.$$typeof === REACT_LAZY_TYPE && ("fulfilled" === node._payload.status ? isValidElement(node._payload.value) && node._payload.value._store && (node._payload.value._store.validated = 1) : node._store && (node._store.validated = 1));
}
function isValidElement(object) {
return "object" === typeof object && null !== object && object.$$typeof === REACT_ELEMENT_TYPE;
}
function escape(key) {
var escaperLookup = {
"=": "=0",
":": "=2"
};
return "$" + key.replace(/[=:]/g, function(match) {
return escaperLookup[match];
});
}
function getElementKey(element, index) {
return "object" === typeof element && null !== element && null != element.key ? (checkKeyStringCoercion(element.key), escape("" + element.key)) : index.toString(36);
}
function resolveThenable(thenable) {
switch (thenable.status) {
case "fulfilled": return thenable.value;
case "rejected": throw thenable.reason;
default: switch ("string" === typeof thenable.status ? thenable.then(noop, noop) : (thenable.status = "pending", thenable.then(function(fulfilledValue) {
"pending" === thenable.status && (thenable.status = "fulfilled", thenable.value = fulfilledValue);
}, function(error) {
"pending" === thenable.status && (thenable.status = "rejected", thenable.reason = error);
})), thenable.status) {
case "fulfilled": return thenable.value;
case "rejected": throw thenable.reason;
}
}
throw thenable;
}
function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
var type = typeof children;
if ("undefined" === type || "boolean" === type) children = null;
var invokeCallback = !1;
if (null === children) invokeCallback = !0;
else switch (type) {
case "bigint":
case "string":
case "number":
invokeCallback = !0;
break;
case "object": switch (children.$$typeof) {
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
invokeCallback = !0;
break;
case REACT_LAZY_TYPE: return invokeCallback = children._init, mapIntoArray(invokeCallback(children._payload), array, escapedPrefix, nameSoFar, callback);
}
}
if (invokeCallback) {
invokeCallback = children;
callback = callback(invokeCallback);
var childKey = "" === nameSoFar ? "." + getElementKey(invokeCallback, 0) : nameSoFar;
isArrayImpl(callback) ? (escapedPrefix = "", null != childKey && (escapedPrefix = childKey.replace(userProvidedKeyEscapeRegex, "$&/") + "/"), mapIntoArray(callback, array, escapedPrefix, "", function(c) {
return c;
})) : null != callback && (isValidElement(callback) && (null != callback.key && (invokeCallback && invokeCallback.key === callback.key || checkKeyStringCoercion(callback.key)), escapedPrefix = cloneAndReplaceKey(callback, escapedPrefix + (null == callback.key || invokeCallback && invokeCallback.key === callback.key ? "" : ("" + callback.key).replace(userProvidedKeyEscapeRegex, "$&/") + "/") + childKey), "" !== nameSoFar && null != invokeCallback && isValidElement(invokeCallback) && null == invokeCallback.key && invokeCallback._store && !invokeCallback._store.validated && (escapedPrefix._store.validated = 2), callback = escapedPrefix), array.push(callback));
return 1;
}
invokeCallback = 0;
childKey = "" === nameSoFar ? "." : nameSoFar + ":";
if (isArrayImpl(children)) for (var i = 0; i < children.length; i++) nameSoFar = children[i], type = childKey + getElementKey(nameSoFar, i), invokeCallback += mapIntoArray(nameSoFar, array, escapedPrefix, type, callback);
else if (i = getIteratorFn(children), "function" === typeof i) for (i === children.entries && (didWarnAboutMaps || console.warn("Using Maps as children is not supported. Use an array of keyed ReactElements instead."), didWarnAboutMaps = !0), children = i.call(children), i = 0; !(nameSoFar = children.next()).done;) nameSoFar = nameSoFar.value, type = childKey + getElementKey(nameSoFar, i++), invokeCallback += mapIntoArray(nameSoFar, array, escapedPrefix, type, callback);
else if ("object" === type) {
if ("function" === typeof children.then) return mapIntoArray(resolveThenable(children), array, escapedPrefix, nameSoFar, callback);
array = String(children);
throw Error("Objects are not valid as a React child (found: " + ("[object Object]" === array ? "object with keys {" + Object.keys(children).join(", ") + "}" : array) + "). If you meant to render a collection of children, use an array instead.");
}
return invokeCallback;
}
function mapChildren(children, func, context) {
if (null == children) return children;
var result = [], count = 0;
mapIntoArray(children, result, "", "", function(child) {
return func.call(context, child, count++);
});
return result;
}
function lazyInitializer(payload) {
if (-1 === payload._status) {
var ioInfo = payload._ioInfo;
null != ioInfo && (ioInfo.start = ioInfo.end = performance.now());
ioInfo = payload._result;
var thenable = ioInfo();
thenable.then(function(moduleObject) {
if (0 === payload._status || -1 === payload._status) {
payload._status = 1;
payload._result = moduleObject;
var _ioInfo = payload._ioInfo;
null != _ioInfo && (_ioInfo.end = performance.now());
void 0 === thenable.status && (thenable.status = "fulfilled", thenable.value = moduleObject);
}
}, function(error) {
if (0 === payload._status || -1 === payload._status) {
payload._status = 2;
payload._result = error;
var _ioInfo2 = payload._ioInfo;
null != _ioInfo2 && (_ioInfo2.end = performance.now());
void 0 === thenable.status && (thenable.status = "rejected", thenable.reason = error);
}
});
ioInfo = payload._ioInfo;
if (null != ioInfo) {
ioInfo.value = thenable;
var displayName = thenable.displayName;
"string" === typeof displayName && (ioInfo.name = displayName);
}
-1 === payload._status && (payload._status = 0, payload._result = thenable);
}
if (1 === payload._status) return ioInfo = payload._result, void 0 === ioInfo && console.error("lazy: Expected the result of a dynamic import() call. Instead received: %s\n\nYour code should look like: \n const MyComponent = lazy(() => import('./MyComponent'))\n\nDid you accidentally put curly braces around the import?", ioInfo), "default" in ioInfo || console.error("lazy: Expected the result of a dynamic import() call. Instead received: %s\n\nYour code should look like: \n const MyComponent = lazy(() => import('./MyComponent'))", ioInfo), ioInfo.default;
throw payload._result;
}
function resolveDispatcher() {
var dispatcher = ReactSharedInternals.H;
null === dispatcher && console.error("Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.");
return dispatcher;
}
function releaseAsyncTransition() {
ReactSharedInternals.asyncTransitions--;
}
function enqueueTask(task) {
if (null === enqueueTaskImpl) try {
var requireString = ("require" + Math.random()).slice(0, 7);
enqueueTaskImpl = (module && module[requireString]).call(module, "timers").setImmediate;
} catch (_err) {
enqueueTaskImpl = function(callback) {
!1 === didWarnAboutMessageChannel && (didWarnAboutMessageChannel = !0, "undefined" === typeof MessageChannel && console.error("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."));
var channel = new MessageChannel();
channel.port1.onmessage = callback;
channel.port2.postMessage(void 0);
};
}
return enqueueTaskImpl(task);
}
function aggregateErrors(errors) {
return 1 < errors.length && "function" === typeof AggregateError ? new AggregateError(errors) : errors[0];
}
function popActScope(prevActQueue, prevActScopeDepth) {
prevActScopeDepth !== actScopeDepth - 1 && console.error("You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one. ");
actScopeDepth = prevActScopeDepth;
}
function recursivelyFlushAsyncActWork(returnValue, resolve, reject) {
var queue = ReactSharedInternals.actQueue;
if (null !== queue) if (0 !== queue.length) try {
flushActQueue(queue);
enqueueTask(function() {
return recursivelyFlushAsyncActWork(returnValue, resolve, reject);
});
return;
} catch (error) {
ReactSharedInternals.thrownErrors.push(error);
}
else ReactSharedInternals.actQueue = null;
0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) : resolve(returnValue);
}
function flushActQueue(queue) {
if (!isFlushing) {
isFlushing = !0;
var i = 0;
try {
for (; i < queue.length; i++) {
var callback = queue[i];
do {
ReactSharedInternals.didUsePromise = !1;
var continuation = callback(!1);
if (null !== continuation) {
if (ReactSharedInternals.didUsePromise) {
queue[i] = callback;
queue.splice(0, i);
return;
}
callback = continuation;
} else break;
} while (1);
}
queue.length = 0;
} catch (error) {
queue.splice(0, i + 1), ReactSharedInternals.thrownErrors.push(error);
} finally {
isFlushing = !1;
}
}
}
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());
var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = Symbol.for("react.activity"), MAYBE_ITERATOR_SYMBOL = Symbol.iterator, didWarnStateUpdateForUnmountedComponent = {}, ReactNoopUpdateQueue = {
isMounted: function() {
return !1;
},
enqueueForceUpdate: function(publicInstance) {
warnNoop(publicInstance, "forceUpdate");
},
enqueueReplaceState: function(publicInstance) {
warnNoop(publicInstance, "replaceState");
},
enqueueSetState: function(publicInstance) {
warnNoop(publicInstance, "setState");
}
}, assign = Object.assign, emptyObject = {};
Object.freeze(emptyObject);
Component.prototype.isReactComponent = {};
Component.prototype.setState = function(partialState, callback) {
if ("object" !== typeof partialState && "function" !== typeof partialState && null != partialState) throw Error("takes an object of state variables to update or a function which returns an object of state variables.");
this.updater.enqueueSetState(this, partialState, callback, "setState");
};
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, "forceUpdate");
};
var deprecatedAPIs = {
isMounted: ["isMounted", "Instead, make sure to clean up subscriptions and pending requests in componentWillUnmount to prevent memory leaks."],
replaceState: ["replaceState", "Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236)."]
};
for (fnName in deprecatedAPIs) deprecatedAPIs.hasOwnProperty(fnName) && defineDeprecationWarning(fnName, deprecatedAPIs[fnName]);
ComponentDummy.prototype = Component.prototype;
deprecatedAPIs = PureComponent.prototype = new ComponentDummy();
deprecatedAPIs.constructor = PureComponent;
assign(deprecatedAPIs, Component.prototype);
deprecatedAPIs.isPureReactComponent = !0;
var isArrayImpl = Array.isArray, REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), ReactSharedInternals = {
H: null,
A: null,
T: null,
S: null,
actQueue: null,
asyncTransitions: 0,
isBatchingLegacy: !1,
didScheduleLegacyUpdate: !1,
didUsePromise: !1,
thrownErrors: [],
getCurrentStack: null,
recentlyCreatedOwnerStacks: 0
}, hasOwnProperty = Object.prototype.hasOwnProperty, createTask = console.createTask ? console.createTask : function() {
return null;
};
deprecatedAPIs = { react_stack_bottom_frame: function(callStackForError) {
return callStackForError();
} };
var specialPropKeyWarningShown, didWarnAboutOldJSXRuntime;
var didWarnAboutElementRef = {};
var unknownOwnerDebugStack = deprecatedAPIs.react_stack_bottom_frame.bind(deprecatedAPIs, UnknownOwner)();
var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner));
var didWarnAboutMaps = !1, userProvidedKeyEscapeRegex = /\/+/g, reportGlobalError = "function" === typeof reportError ? reportError : function(error) {
if ("object" === typeof window && "function" === typeof window.ErrorEvent) {
var event = new window.ErrorEvent("error", {
bubbles: !0,
cancelable: !0,
message: "object" === typeof error && null !== error && "string" === typeof error.message ? String(error.message) : String(error),
error
});
if (!window.dispatchEvent(event)) return;
} else if ("object" === typeof process && "function" === typeof process.emit) {
process.emit("uncaughtException", error);
return;
}
console.error(error);
}, didWarnAboutMessageChannel = !1, enqueueTaskImpl = null, actScopeDepth = 0, didWarnNoAwaitAct = !1, isFlushing = !1, queueSeveralMicrotasks = "function" === typeof queueMicrotask ? function(callback) {
queueMicrotask(function() {
return queueMicrotask(callback);
});
} : enqueueTask;
deprecatedAPIs = Object.freeze({
__proto__: null,
c: function(size) {
return resolveDispatcher().useMemoCache(size);
}
});
var fnName = {
map: mapChildren,
forEach: function(children, forEachFunc, forEachContext) {
mapChildren(children, function() {
forEachFunc.apply(this, arguments);
}, forEachContext);
},
count: function(children) {
var n = 0;
mapChildren(children, function() {
n++;
});
return n;
},
toArray: function(children) {
return mapChildren(children, function(child) {
return child;
}) || [];
},
only: function(children) {
if (!isValidElement(children)) throw Error("React.Children.only expected to receive a single React element child.");
return children;
}
};
exports.Activity = REACT_ACTIVITY_TYPE;
exports.Children = fnName;
exports.Component = Component;
exports.Fragment = REACT_FRAGMENT_TYPE;
exports.Profiler = REACT_PROFILER_TYPE;
exports.PureComponent = PureComponent;
exports.StrictMode = REACT_STRICT_MODE_TYPE;
exports.Suspense = REACT_SUSPENSE_TYPE;
exports.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = ReactSharedInternals;
exports.__COMPILER_RUNTIME = deprecatedAPIs;
exports.act = function(callback) {
var prevActQueue = ReactSharedInternals.actQueue, prevActScopeDepth = actScopeDepth;
actScopeDepth++;
var queue = ReactSharedInternals.actQueue = null !== prevActQueue ? prevActQueue : [], didAwaitActCall = !1;
try {
var result = callback();
} catch (error) {
ReactSharedInternals.thrownErrors.push(error);
}
if (0 < ReactSharedInternals.thrownErrors.length) throw popActScope(prevActQueue, prevActScopeDepth), callback = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, callback;
if (null !== result && "object" === typeof result && "function" === typeof result.then) {
var thenable = result;
queueSeveralMicrotasks(function() {
didAwaitActCall || didWarnNoAwaitAct || (didWarnNoAwaitAct = !0, console.error("You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"));
});
return { then: function(resolve, reject) {
didAwaitActCall = !0;
thenable.then(function(returnValue) {
popActScope(prevActQueue, prevActScopeDepth);
if (0 === prevActScopeDepth) {
try {
flushActQueue(queue), enqueueTask(function() {
return recursivelyFlushAsyncActWork(returnValue, resolve, reject);
});
} catch (error$0) {
ReactSharedInternals.thrownErrors.push(error$0);
}
if (0 < ReactSharedInternals.thrownErrors.length) {
var _thrownError = aggregateErrors(ReactSharedInternals.thrownErrors);
ReactSharedInternals.thrownErrors.length = 0;
reject(_thrownError);
}
} else resolve(returnValue);
}, function(error) {
popActScope(prevActQueue, prevActScopeDepth);
0 < ReactSharedInternals.thrownErrors.length ? (error = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(error)) : reject(error);
});
} };
}
var returnValue$jscomp$0 = result;
popActScope(prevActQueue, prevActScopeDepth);
0 === prevActScopeDepth && (flushActQueue(queue), 0 !== queue.length && queueSeveralMicrotasks(function() {
didAwaitActCall || didWarnNoAwaitAct || (didWarnNoAwaitAct = !0, console.error("A component suspended inside an `act` scope, but the `act` call was not awaited. When testing React components that depend on asynchronous data, you must await the result:\n\nawait act(() => ...)"));
}), ReactSharedInternals.actQueue = null);
if (0 < ReactSharedInternals.thrownErrors.length) throw callback = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, callback;
return { then: function(resolve, reject) {
didAwaitActCall = !0;
0 === prevActScopeDepth ? (ReactSharedInternals.actQueue = queue, enqueueTask(function() {
return recursivelyFlushAsyncActWork(returnValue$jscomp$0, resolve, reject);
})) : resolve(returnValue$jscomp$0);
} };
};
exports.cache = function(fn) {
return function() {
return fn.apply(null, arguments);
};
};
exports.cacheSignal = function() {
return null;
};
exports.captureOwnerStack = function() {
var getCurrentStack = ReactSharedInternals.getCurrentStack;
return null === getCurrentStack ? null : getCurrentStack();
};
exports.cloneElement = function(element, config, children) {
if (null === element || void 0 === element) throw Error("The argument must be a React element, but you passed " + element + ".");
var props = assign({}, element.props), key = element.key, owner = element._owner;
if (null != config) {
var JSCompiler_inline_result;
a: {
if (hasOwnProperty.call(config, "ref") && (JSCompiler_inline_result = Object.getOwnPropertyDescriptor(config, "ref").get) && JSCompiler_inline_result.isReactWarning) {
JSCompiler_inline_result = !1;
break a;
}
JSCompiler_inline_result = void 0 !== config.ref;
}
JSCompiler_inline_result && (owner = getOwner());
hasValidKey(config) && (checkKeyStringCoercion(config.key), key = "" + config.key);
for (propName in config) !hasOwnProperty.call(config, propName) || "key" === propName || "__self" === propName || "__source" === propName || "ref" === propName && void 0 === config.ref || (props[propName] = config[propName]);
}
var propName = arguments.length - 2;
if (1 === propName) props.children = children;
else if (1 < propName) {
JSCompiler_inline_result = Array(propName);
for (var i = 0; i < propName; i++) JSCompiler_inline_result[i] = arguments[i + 2];
props.children = JSCompiler_inline_result;
}
props = ReactElement(element.type, key, props, owner, element._debugStack, element._debugTask);
for (key = 2; key < arguments.length; key++) validateChildKeys(arguments[key]);
return props;
};
exports.createContext = function(defaultValue) {
defaultValue = {
$$typeof: REACT_CONTEXT_TYPE,
_currentValue: defaultValue,
_currentValue2: defaultValue,
_threadCount: 0,
Provider: null,
Consumer: null
};
defaultValue.Provider = defaultValue;
defaultValue.Consumer = {
$$typeof: REACT_CONSUMER_TYPE,
_context: defaultValue
};
defaultValue._currentRenderer = null;
defaultValue._currentRenderer2 = null;
return defaultValue;
};
exports.createElement = function(type, config, children) {
for (var i = 2; i < arguments.length; i++) validateChildKeys(arguments[i]);
i = {};
var key = null;
if (null != config) for (propName in didWarnAboutOldJSXRuntime || !("__self" in config) || "key" in config || (didWarnAboutOldJSXRuntime = !0, console.warn("Your app (or one of its dependencies) is using an outdated JSX transform. Update to the modern JSX transform for faster performance: https://react.dev/link/new-jsx-transform")), hasValidKey(config) && (checkKeyStringCoercion(config.key), key = "" + config.key), config) hasOwnProperty.call(config, propName) && "key" !== propName && "__self" !== propName && "__source" !== propName && (i[propName] = config[propName]);
var childrenLength = arguments.length - 2;
if (1 === childrenLength) i.children = children;
else if (1 < childrenLength) {
for (var childArray = Array(childrenLength), _i = 0; _i < childrenLength; _i++) childArray[_i] = arguments[_i + 2];
Object.freeze && Object.freeze(childArray);
i.children = childArray;
}
if (type && type.defaultProps) for (propName in childrenLength = type.defaultProps, childrenLength) void 0 === i[propName] && (i[propName] = childrenLength[propName]);
key && defineKeyPropWarningGetter(i, "function" === typeof type ? type.displayName || type.name || "Unknown" : type);
var propName = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;
return ReactElement(type, key, i, getOwner(), propName ? Error("react-stack-top-frame") : unknownOwnerDebugStack, propName ? createTask(getTaskName(type)) : unknownOwnerDebugTask);
};
exports.createRef = function() {
var refObject = { current: null };
Object.seal(refObject);
return refObject;
};
exports.forwardRef = function(render) {
null != render && render.$$typeof === REACT_MEMO_TYPE ? console.error("forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...)).") : "function" !== typeof render ? console.error("forwardRef requires a render function but was given %s.", null === render ? "null" : typeof render) : 0 !== render.length && 2 !== render.length && console.error("forwardRef render functions accept exactly two parameters: props and ref. %s", 1 === render.length ? "Did you forget to use the ref parameter?" : "Any additional parameter will be undefined.");
null != render && null != render.defaultProps && console.error("forwardRef render functions do not support defaultProps. Did you accidentally pass a React component?");
var elementType = {
$$typeof: REACT_FORWARD_REF_TYPE,
render
}, ownName;
Object.defineProperty(elementType, "displayName", {
enumerable: !1,
configurable: !0,
get: function() {
return ownName;
},
set: function(name) {
ownName = name;
render.name || render.displayName || (Object.defineProperty(render, "name", { value: name }), render.displayName = name);
}
});
return elementType;
};
exports.isValidElement = isValidElement;
exports.lazy = function(ctor) {
ctor = {
_status: -1,
_result: ctor
};
var lazyType = {
$$typeof: REACT_LAZY_TYPE,
_payload: ctor,
_init: lazyInitializer
}, ioInfo = {
name: "lazy",
start: -1,
end: -1,
value: null,
owner: null,
debugStack: Error("react-stack-top-frame"),
debugTask: console.createTask ? console.createTask("lazy()") : null
};
ctor._ioInfo = ioInfo;
lazyType._debugInfo = [{ awaited: ioInfo }];
return lazyType;
};
exports.memo = function(type, compare) {
type ?? console.error("memo: The first argument must be a component. Instead received: %s", null === type ? "null" : typeof type);
compare = {
$$typeof: REACT_MEMO_TYPE,
type,
compare: void 0 === compare ? null : compare
};
var ownName;
Object.defineProperty(compare, "displayName", {
enumerable: !1,
configurable: !0,
get: function() {
return ownName;
},
set: function(name) {
ownName = name;
type.name || type.displayName || (Object.defineProperty(type, "name", { value: name }), type.displayName = name);
}
});
return compare;
};
exports.startTransition = function(scope) {
var prevTransition = ReactSharedInternals.T, currentTransition = {};
currentTransition._updatedFibers = /* @__PURE__ */ new Set();
ReactSharedInternals.T = currentTransition;
try {
var returnValue = scope(), onStartTransitionFinish = ReactSharedInternals.S;
null !== onStartTransitionFinish && onStartTransitionFinish(currentTransition, returnValue);
"object" === typeof returnValue && null !== returnValue && "function" === typeof returnValue.then && (ReactSharedInternals.asyncTransitions++, returnValue.then(releaseAsyncTransition, releaseAsyncTransition), returnValue.then(noop, reportGlobalError));
} catch (error) {
reportGlobalError(error);
} finally {
null === prevTransition && currentTransition._updatedFibers && (scope = currentTransition._updatedFibers.size, currentTransition._updatedFibers.clear(), 10 < scope && console.warn("Detected a large number of updates inside startTransition. If this is due to a subscription please re-write it to use React provided hooks. Otherwise concurrent mode guarantees are off the table.")), null !== prevTransition && null !== currentTransition.types && (null !== prevTransition.types && prevTransition.types !== currentTransition.types && console.error("We expected inner Transitions to have transferred the outer types set and that you cannot add to the outer Transition while inside the inner.This is a bug in React."), prevTransition.types = currentTransition.types), ReactSharedInternals.T = prevTransition;
}
};
exports.unstable_useCacheRefresh = function() {
return resolveDispatcher().useCacheRefresh();
};
exports.use = function(usable) {
return resolveDispatcher().use(usable);
};
exports.useActionState = function(action, initialState, permalink) {
return resolveDispatcher().useActionState(action, initialState, permalink);
};
exports.useCallback = function(callback, deps) {
return resolveDispatcher().useCallback(callback, deps);
};
exports.useContext = function(Context) {
var dispatcher = resolveDispatcher();
Context.$$typeof === REACT_CONSUMER_TYPE && console.error("Calling useContext(Context.Consumer) is not supported and will cause bugs. Did you mean to call useContext(Context) instead?");
return dispatcher.useContext(Context);
};
exports.useDebugValue = function(value, formatterFn) {
return resolveDispatcher().useDebugValue(value, formatterFn);
};
exports.useDeferredValue = function(value, initialValue) {
return resolveDispatcher().useDeferredValue(value, initialValue);
};
exports.useEffect = function(create, deps) {
create ?? console.warn("React Hook useEffect requires an effect callback. Did you forget to pass a callback to the hook?");
return resolveDispatcher().useEffect(create, deps);
};
exports.useEffectEvent = function(callback) {
return resolveDispatcher().useEffectEvent(callback);
};
exports.useId = function() {
return resolveDispatcher().useId();
};
exports.useImperativeHandle = function(ref, create, deps) {
return resolveDispatcher().useImperativeHandle(ref, create, deps);
};
exports.useInsertionEffect = function(create, deps) {
create ?? console.warn("React Hook useInsertionEffect requires an effect callback. Did you forget to pass a callback to the hook?");
return resolveDispatcher().useInsertionEffect(create, deps);
};
exports.useLayoutEffect = function(create, deps) {
create ?? console.warn("React Hook useLayoutEffect requires an effect callback. Did you forget to pass a callback to the hook?");
return resolveDispatcher().useLayoutEffect(create, deps);
};
exports.useMemo = function(create, deps) {
return resolveDispatcher().useMemo(create, deps);
};
exports.useOptimistic = function(passthrough, reducer) {
return resolveDispatcher().useOptimistic(passthrough, reducer);
};
exports.useReducer = function(reducer, initialArg, init) {
return resolveDispatcher().useReducer(reducer, initialArg, init);
};
exports.useRef = function(initialValue) {
return resolveDispatcher().useRef(initialValue);
};
exports.useState = function(initialState) {
return resolveDispatcher().useState(initialState);
};
exports.useSyncExternalStore = function(subscribe, getSnapshot, getServerSnapshot) {
return resolveDispatcher().useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
};
exports.useTransition = function() {
return resolveDispatcher().useTransition();
};
exports.version = "19.2.4";
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error());
})();
}));
//#endregion
//#region node_modules/react/index.js
var require_react = /* @__PURE__ */ __commonJSMin(((exports, module) => {
module.exports = require_react_development();
}));
//#endregion
export { __commonJSMin as n, require_react as t };
//# sourceMappingURL=react-Na5-BvaJ.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,185 @@
import { n as __commonJSMin, t as require_react } from "./react-Na5-BvaJ.js";
//#region node_modules/react-dom/cjs/react-dom.development.js
/**
* @license React
* react-dom.development.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var require_react_dom_development = /* @__PURE__ */ __commonJSMin(((exports) => {
(function() {
function noop() {}
function testStringCoercion(value) {
return "" + value;
}
function createPortal$1(children, containerInfo, implementation) {
var key = 3 < arguments.length && void 0 !== arguments[3] ? arguments[3] : null;
try {
testStringCoercion(key);
var JSCompiler_inline_result = !1;
} catch (e) {
JSCompiler_inline_result = !0;
}
JSCompiler_inline_result && (console.error("The provided key is an unsupported type %s. This value must be coerced to a string before using it here.", "function" === typeof Symbol && Symbol.toStringTag && key[Symbol.toStringTag] || key.constructor.name || "Object"), testStringCoercion(key));
return {
$$typeof: REACT_PORTAL_TYPE,
key: null == key ? null : "" + key,
children,
containerInfo,
implementation
};
}
function getCrossOriginStringAs(as, input) {
if ("font" === as) return "";
if ("string" === typeof input) return "use-credentials" === input ? input : "";
}
function getValueDescriptorExpectingObjectForWarning(thing) {
return null === thing ? "`null`" : void 0 === thing ? "`undefined`" : "" === thing ? "an empty string" : "something with type \"" + typeof thing + "\"";
}
function getValueDescriptorExpectingEnumForWarning(thing) {
return null === thing ? "`null`" : void 0 === thing ? "`undefined`" : "" === thing ? "an empty string" : "string" === typeof thing ? JSON.stringify(thing) : "number" === typeof thing ? "`" + thing + "`" : "something with type \"" + typeof thing + "\"";
}
function resolveDispatcher() {
var dispatcher = ReactSharedInternals.H;
null === dispatcher && console.error("Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.");
return dispatcher;
}
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());
var React = require_react(), Internals = {
d: {
f: noop,
r: function() {
throw Error("Invalid form element. requestFormReset must be passed a form that was rendered by React.");
},
D: noop,
C: noop,
L: noop,
m: noop,
X: noop,
S: noop,
M: noop
},
p: 0,
findDOMNode: null
}, REACT_PORTAL_TYPE = Symbol.for("react.portal"), ReactSharedInternals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
"function" === typeof Map && null != Map.prototype && "function" === typeof Map.prototype.forEach && "function" === typeof Set && null != Set.prototype && "function" === typeof Set.prototype.clear && "function" === typeof Set.prototype.forEach || console.error("React depends on Map and Set built-in types. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills");
exports.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = Internals;
exports.createPortal = function(children, container) {
var key = 2 < arguments.length && void 0 !== arguments[2] ? arguments[2] : null;
if (!container || 1 !== container.nodeType && 9 !== container.nodeType && 11 !== container.nodeType) throw Error("Target container is not a DOM element.");
return createPortal$1(children, container, null, key);
};
exports.flushSync = function(fn) {
var previousTransition = ReactSharedInternals.T, previousUpdatePriority = Internals.p;
try {
if (ReactSharedInternals.T = null, Internals.p = 2, fn) return fn();
} finally {
ReactSharedInternals.T = previousTransition, Internals.p = previousUpdatePriority, Internals.d.f() && console.error("flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task.");
}
};
exports.preconnect = function(href, options) {
"string" === typeof href && href ? null != options && "object" !== typeof options ? console.error("ReactDOM.preconnect(): Expected the `options` argument (second) to be an object but encountered %s instead. The only supported option at this time is `crossOrigin` which accepts a string.", getValueDescriptorExpectingEnumForWarning(options)) : null != options && "string" !== typeof options.crossOrigin && console.error("ReactDOM.preconnect(): Expected the `crossOrigin` option (second argument) to be a string but encountered %s instead. Try removing this option or passing a string value instead.", getValueDescriptorExpectingObjectForWarning(options.crossOrigin)) : console.error("ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.", getValueDescriptorExpectingObjectForWarning(href));
"string" === typeof href && (options ? (options = options.crossOrigin, options = "string" === typeof options ? "use-credentials" === options ? options : "" : void 0) : options = null, Internals.d.C(href, options));
};
exports.prefetchDNS = function(href) {
if ("string" !== typeof href || !href) console.error("ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.", getValueDescriptorExpectingObjectForWarning(href));
else if (1 < arguments.length) {
var options = arguments[1];
"object" === typeof options && options.hasOwnProperty("crossOrigin") ? console.error("ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. It looks like the you are attempting to set a crossOrigin property for this DNS lookup hint. Browsers do not perform DNS queries using CORS and setting this attribute on the resource hint has no effect. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.", getValueDescriptorExpectingEnumForWarning(options)) : console.error("ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.", getValueDescriptorExpectingEnumForWarning(options));
}
"string" === typeof href && Internals.d.D(href);
};
exports.preinit = function(href, options) {
"string" === typeof href && href ? null == options || "object" !== typeof options ? console.error("ReactDOM.preinit(): Expected the `options` argument (second) to be an object with an `as` property describing the type of resource to be preinitialized but encountered %s instead.", getValueDescriptorExpectingEnumForWarning(options)) : "style" !== options.as && "script" !== options.as && console.error("ReactDOM.preinit(): Expected the `as` property in the `options` argument (second) to contain a valid value describing the type of resource to be preinitialized but encountered %s instead. Valid values for `as` are \"style\" and \"script\".", getValueDescriptorExpectingEnumForWarning(options.as)) : console.error("ReactDOM.preinit(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.", getValueDescriptorExpectingObjectForWarning(href));
if ("string" === typeof href && options && "string" === typeof options.as) {
var as = options.as, crossOrigin = getCrossOriginStringAs(as, options.crossOrigin), integrity = "string" === typeof options.integrity ? options.integrity : void 0, fetchPriority = "string" === typeof options.fetchPriority ? options.fetchPriority : void 0;
"style" === as ? Internals.d.S(href, "string" === typeof options.precedence ? options.precedence : void 0, {
crossOrigin,
integrity,
fetchPriority
}) : "script" === as && Internals.d.X(href, {
crossOrigin,
integrity,
fetchPriority,
nonce: "string" === typeof options.nonce ? options.nonce : void 0
});
}
};
exports.preinitModule = function(href, options) {
var encountered = "";
"string" === typeof href && href || (encountered += " The `href` argument encountered was " + getValueDescriptorExpectingObjectForWarning(href) + ".");
void 0 !== options && "object" !== typeof options ? encountered += " The `options` argument encountered was " + getValueDescriptorExpectingObjectForWarning(options) + "." : options && "as" in options && "script" !== options.as && (encountered += " The `as` option encountered was " + getValueDescriptorExpectingEnumForWarning(options.as) + ".");
if (encountered) console.error("ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property.%s", encountered);
else switch (encountered = options && "string" === typeof options.as ? options.as : "script", encountered) {
case "script": break;
default: encountered = getValueDescriptorExpectingEnumForWarning(encountered), console.error("ReactDOM.preinitModule(): Currently the only supported \"as\" type for this function is \"script\" but received \"%s\" instead. This warning was generated for `href` \"%s\". In the future other module types will be supported, aligning with the import-attributes proposal. Learn more here: (https://github.com/tc39/proposal-import-attributes)", encountered, href);
}
if ("string" === typeof href) if ("object" === typeof options && null !== options) {
if (null == options.as || "script" === options.as) encountered = getCrossOriginStringAs(options.as, options.crossOrigin), Internals.d.M(href, {
crossOrigin: encountered,
integrity: "string" === typeof options.integrity ? options.integrity : void 0,
nonce: "string" === typeof options.nonce ? options.nonce : void 0
});
} else options ?? Internals.d.M(href);
};
exports.preload = function(href, options) {
var encountered = "";
"string" === typeof href && href || (encountered += " The `href` argument encountered was " + getValueDescriptorExpectingObjectForWarning(href) + ".");
null == options || "object" !== typeof options ? encountered += " The `options` argument encountered was " + getValueDescriptorExpectingObjectForWarning(options) + "." : "string" === typeof options.as && options.as || (encountered += " The `as` option encountered was " + getValueDescriptorExpectingObjectForWarning(options.as) + ".");
encountered && console.error("ReactDOM.preload(): Expected two arguments, a non-empty `href` string and an `options` object with an `as` property valid for a `<link rel=\"preload\" as=\"...\" />` tag.%s", encountered);
if ("string" === typeof href && "object" === typeof options && null !== options && "string" === typeof options.as) {
encountered = options.as;
var crossOrigin = getCrossOriginStringAs(encountered, options.crossOrigin);
Internals.d.L(href, encountered, {
crossOrigin,
integrity: "string" === typeof options.integrity ? options.integrity : void 0,
nonce: "string" === typeof options.nonce ? options.nonce : void 0,
type: "string" === typeof options.type ? options.type : void 0,
fetchPriority: "string" === typeof options.fetchPriority ? options.fetchPriority : void 0,
referrerPolicy: "string" === typeof options.referrerPolicy ? options.referrerPolicy : void 0,
imageSrcSet: "string" === typeof options.imageSrcSet ? options.imageSrcSet : void 0,
imageSizes: "string" === typeof options.imageSizes ? options.imageSizes : void 0,
media: "string" === typeof options.media ? options.media : void 0
});
}
};
exports.preloadModule = function(href, options) {
var encountered = "";
"string" === typeof href && href || (encountered += " The `href` argument encountered was " + getValueDescriptorExpectingObjectForWarning(href) + ".");
void 0 !== options && "object" !== typeof options ? encountered += " The `options` argument encountered was " + getValueDescriptorExpectingObjectForWarning(options) + "." : options && "as" in options && "string" !== typeof options.as && (encountered += " The `as` option encountered was " + getValueDescriptorExpectingObjectForWarning(options.as) + ".");
encountered && console.error("ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `<link rel=\"modulepreload\" as=\"...\" />` tag.%s", encountered);
"string" === typeof href && (options ? (encountered = getCrossOriginStringAs(options.as, options.crossOrigin), Internals.d.m(href, {
as: "string" === typeof options.as && "script" !== options.as ? options.as : void 0,
crossOrigin: encountered,
integrity: "string" === typeof options.integrity ? options.integrity : void 0
})) : Internals.d.m(href));
};
exports.requestFormReset = function(form) {
Internals.d.r(form);
};
exports.unstable_batchedUpdates = function(fn, a) {
return fn(a);
};
exports.useFormState = function(action, initialState, permalink) {
return resolveDispatcher().useFormState(action, initialState, permalink);
};
exports.useFormStatus = function() {
return resolveDispatcher().useHostTransitionStatus();
};
exports.version = "19.2.4";
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error());
})();
}));
//#endregion
//#region node_modules/react-dom/index.js
var require_react_dom = /* @__PURE__ */ __commonJSMin(((exports, module) => {
module.exports = require_react_dom_development();
}));
//#endregion
export default require_react_dom();
export { require_react_dom as t };
//# sourceMappingURL=react-dom.js.map

Some files were not shown because too many files have changed in this diff Show More