2 Commits

Author SHA1 Message Date
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
14 changed files with 428 additions and 5 deletions

View File

@@ -1 +1 @@
{"task_id": "32788f42-8544-80e1-a13a-c26114cf9b34", "token": "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", "readme_path": "readme.md", "session_start_time": "2026-04-12T19:57:10.454150"}
{"task_id": "34288f42-8544-800e-b866-dfcbc22bd4e5", "token": "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", "readme_path": "readme.md", "session_start_time": "2026-04-14T08:37:49.545740"}

View File

@@ -1,6 +1,6 @@
# Fotograf.de Scraper & Management UI
**Status:** Production-Ready Microservice (Core Feature: PDF List Generation, QR Cards, Shooting Schedule & **Gmail API Integration**)
**Status:** Production-Ready Microservice (Core Feature: PDF List Generation, QR Cards, Shooting Schedule, **Siblings List** & **Gmail API Integration**)
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.
@@ -48,9 +48,15 @@ Identifizierung von potenziellen Käufern und automatisierter Kontakt.
### Feature 4: Verkaufs-Statistiken (Vollständig)
* Detaillierte Analyse des Kaufverhaltens pro Album mit Echtzeit-Fortschrittsanzeige im Browser.
### Feature 5: Geschwisterliste (Einrichtungsintern) (Vollständig)
Spezielles Tool zur Identifizierung von Geschwistergruppen innerhalb einer Einrichtung.
* **Intelligente Erkennung:** Nutzt die "Email der Eltern (1)" aus der Fotograf.de-Anmeldeliste für einen automatischen Abgleich (Zählenwenn > 1).
* **Calendly-Cross-Check:** Gleicht die identifizierten Familien mit allen aktuellen Calendly-Buchungen ab, um Nachmittags-Termine automatisch in der Liste zu vermerken.
* **Optimiertes PDF:** Generiert eine alphabetisch nach Nachnamen sortierte Liste mit Kindern, deren Gruppen, Online-Wunsch-Status und Termin-Uhrzeit (inkl. Datum) sowie einem Erledigt-Feld für die manuelle Kontrolle vor Ort.
---
## 🎯 Nächste Session: "Freigabeanfragen" (Feature 5)
## 🎯 Nächste Session: "Freigabeanfragen" (Feature 6)
Das nächste große Ziel ist der automatische Versand von Freigabeanfragen via Gmail.

View File

@@ -1194,3 +1194,93 @@ async def generate_pdf(job_id: str, account_type: str, db: Session = Depends(get
if driver:
logger.debug("Closing driver.")
driver.quit()
@app.get("/api/jobs/{job_id}/siblings-list")
async def generate_siblings_list(job_id: str, account_type: str, event_type_name: str = "", db: Session = Depends(get_db)):
logger.info(f"API Request: Generate siblings list for job {job_id}")
username = os.getenv(f"{account_type.upper()}_USER")
password = os.getenv(f"{account_type.upper()}_PW")
api_token = os.getenv("CALENDLY_TOKEN")
if not api_token:
raise HTTPException(status_code=400, detail="Calendly API token missing.")
# Get Calendly events
from qr_generator import get_calendly_events_raw
try:
# Fetch ALL events to ensure we don't miss siblings due to event name mismatches
calendly_events = get_calendly_events_raw(api_token, event_type_name=None)
logger.info(f"Fetched {len(calendly_events)} total events from Calendly for siblings check.")
except Exception as e:
logger.error(f"Error fetching Calendly events: {e}")
calendly_events = []
with tempfile.TemporaryDirectory() as temp_dir:
logger.debug(f"Using temp directory: {temp_dir}")
driver = setup_driver(download_path=temp_dir)
try:
if not login(driver, username, password):
raise HTTPException(status_code=401, detail="Login failed.")
job_url = f"https://app.fotograf.de/config_jobs_settings/index/{job_id}"
driver.get(job_url)
wait = WebDriverWait(driver, 30)
try:
institution = driver.find_element(By.TAG_NAME, "h1").text.strip()
except:
institution = "Fotoauftrag"
personen_tab = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "[data-qa-id='link:photo-jobs-tabs-names_list']")))
driver.execute_script("arguments[0].click();", personen_tab)
export_btn = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, SELECTORS["export_dropdown"])))
driver.execute_script("arguments[0].scrollIntoView(true);", export_btn)
time.sleep(1)
driver.execute_script("arguments[0].click();", export_btn)
time.sleep(2)
try:
csv_btn = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, SELECTORS["export_csv_link"])))
driver.execute_script("arguments[0].click();", csv_btn)
except TimeoutException:
raise HTTPException(status_code=500, detail="CSV Export Button nicht gefunden.")
timeout = 45
start_time = time.time()
csv_file = None
while time.time() - start_time < timeout:
files = os.listdir(temp_dir)
csv_files = [f for f in files if f.endswith('.csv')]
if csv_files:
csv_file = os.path.join(temp_dir, csv_files[0])
break
time.sleep(1)
if not csv_file:
raise HTTPException(status_code=500, detail="CSV Download fehlgeschlagen.")
output_pdf_name = f"Geschwisterliste_{job_id}.pdf"
output_pdf_path = os.path.join(temp_dir, output_pdf_name)
from siblings_logic import generate_siblings_pdf_from_csv
generate_siblings_pdf_from_csv(
csv_path=csv_file,
institution=institution,
calendly_events=calendly_events,
list_type=account_type,
output_path=output_pdf_path
)
final_storage = os.path.join("/tmp", output_pdf_name)
shutil.copy(output_pdf_path, final_storage)
return FileResponse(path=final_storage, filename=output_pdf_name, media_type="application/pdf")
except HTTPException as he:
raise he
except Exception as e:
logger.exception("Error generating siblings list")
raise HTTPException(status_code=500, detail=str(e))
finally:
if driver: driver.quit()

View File

@@ -0,0 +1,125 @@
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"))
# Allow all events for siblings logic, regardless of date, just to be sure we match them
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}")

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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

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>frontend</title>
<script type="module" crossorigin src="/fotograf-de/assets/index-BPS0v1Hk.js"></script>
<link rel="stylesheet" crossorigin href="/fotograf-de/assets/index-CMFolvzs.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

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

@@ -38,6 +38,7 @@ function App() {
const [eventTypes, setEventTypes] = useState<any[]>([]);
const [selectedEventType, setSelectedEventType] = useState<string>("");
const [isListGenerating, setIsListGenerating] = useState(false);
const [isSiblingsGenerating, setIsSiblingsGenerating] = useState(false);
const [reminderTaskId, setReminderTaskId] = useState<string | null>(null);
const [reminderProgress, setReminderProgress] = useState<string>('');
const [isReminderRunning, setIsReminderRunning] = useState(false);
@@ -321,6 +322,23 @@ function App() {
}
};
const handleGenerateSiblingsList = async (job: Job) => {
setIsSiblingsGenerating(true);
setError(null);
try {
const downloadUrl = `${API_BASE_URL}/api/jobs/${job.id}/siblings-list?account_type=${activeTab}&event_type_name=${encodeURIComponent(selectedEventType)}`;
window.open(downloadUrl, '_blank');
setTimeout(() => {
setIsSiblingsGenerating(false);
fetchLatestFile();
}, 3000);
} catch (err: any) {
setError(`Geschwisterlisten-Fehler (${job.name}): ${err.message}`);
setIsSiblingsGenerating(false);
}
};
const handleGenerateAppointmentList = async (job: Job) => {
setIsListGenerating(true);
setError(null);
@@ -763,6 +781,22 @@ function App() {
{isListGenerating ? 'Generiere...' : 'PDF Liste generieren'}
</button>
</div>
{/* Action 3: Siblings List */}
<div className="bg-gray-50 p-4 rounded-lg flex flex-col justify-between sm:col-span-2">
<div>
<h6 className="font-bold text-sm text-gray-800 mb-1">👨👩👧👦 Geschwisterliste (Einrichtungsintern)</h6>
<p className="text-xs text-gray-600 mb-3">Sucht nach Geschwisterkindern in der Einrichtung und gleicht diese mit Calendly ab.</p>
</div>
<button
onClick={() => handleGenerateSiblingsList(selectedJob)}
disabled={!selectedEventType || isSiblingsGenerating}
className="w-full px-3 py-2 bg-teal-600 text-white text-xs font-bold rounded-lg hover:bg-teal-700 disabled:opacity-50 transition-all flex justify-center items-center gap-2 mt-auto"
>
{isSiblingsGenerating ? 'Generiere...' : 'Geschwisterliste generieren'}
</button>
</div>
</div>
</div>
</div>

View File

@@ -277,3 +277,12 @@ Investierte Zeit in dieser Session: 00:23
Arbeitszusammenfassung:
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.
```
## 🤖 Status-Update (2026-04-14 10:37 Berlin Time)
```yaml
Investierte Zeit in dieser Session: 01:43
Arbeitszusammenfassung:
Keine Zusammenfassung angegeben.
```