[34288f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
This commit is contained in:
@@ -1 +1 @@
|
|||||||
{"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"}
|
{"task_id": "34288f42-8544-800e-b866-dfcbc22bd4e5", "token": "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", "readme_path": "readme.md", "session_start_time": "2026-04-14T14:09:57.182458"}
|
||||||
@@ -53,6 +53,7 @@ Spezielles Tool zur Identifizierung von Geschwistergruppen innerhalb einer Einri
|
|||||||
* **Intelligente Erkennung:** Nutzt die "Email der Eltern (1)" aus der Fotograf.de-Anmeldeliste für einen automatischen Abgleich (Zählenwenn > 1).
|
* **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.
|
* **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.
|
* **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.
|
||||||
|
* **Geschwister-QR-Karten:** Erzeugt automatisch Zugangskarten ("Geschwisterbilder Familie [Nachname]") für alle identifizierten Geschwisterfamilien, die *keinen* Termin am Nachmittag gebucht haben.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1274,7 +1274,10 @@ async def generate_siblings_list(job_id: str, account_type: str, event_type_name
|
|||||||
|
|
||||||
final_storage = os.path.join("/tmp", output_pdf_name)
|
final_storage = os.path.join("/tmp", output_pdf_name)
|
||||||
shutil.copy(output_pdf_path, final_storage)
|
shutil.copy(output_pdf_path, final_storage)
|
||||||
return FileResponse(path=final_storage, filename=output_pdf_name, media_type="application/pdf")
|
|
||||||
|
# Since the frontend has trouble triggering a blob download, return a JSON with a download link
|
||||||
|
download_url = f"/api/jobs/download-qr/{job_id}/{output_pdf_name}"
|
||||||
|
return JSONResponse(content={"status": "success", "download_url": download_url, "filename": output_pdf_name})
|
||||||
|
|
||||||
except HTTPException as he:
|
except HTTPException as he:
|
||||||
raise he
|
raise he
|
||||||
@@ -1284,3 +1287,102 @@ async def generate_siblings_list(job_id: str, account_type: str, event_type_name
|
|||||||
finally:
|
finally:
|
||||||
if driver: driver.quit()
|
if driver: driver.quit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/jobs/{job_id}/siblings-qr-cards")
|
||||||
|
async def generate_siblings_qr_endpoint(
|
||||||
|
job_id: str,
|
||||||
|
account_type: str,
|
||||||
|
pdf_file: UploadFile = File(...),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
logger.info(f"API Request: Generate siblings QR cards for job {job_id}")
|
||||||
|
username = os.getenv(f"{account_type.upper()}_USER")
|
||||||
|
password = os.getenv(f"{account_type.upper()}_PW")
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
input_pdf_path = os.path.join(temp_dir, "input.pdf")
|
||||||
|
with open(input_pdf_path, "wb") as buffer:
|
||||||
|
shutil.copyfileobj(pdf_file.file, buffer)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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"Geschwister_QR_{job_id}.pdf"
|
||||||
|
output_pdf_path = os.path.join(temp_dir, output_pdf_name)
|
||||||
|
|
||||||
|
from siblings_logic import get_sibling_families_from_csv
|
||||||
|
|
||||||
|
# Fetch Calendly events to exclude those who already have a meeting
|
||||||
|
api_token = os.getenv("CALENDLY_TOKEN")
|
||||||
|
from qr_generator import get_calendly_events_raw
|
||||||
|
try:
|
||||||
|
calendly_events = get_calendly_events_raw(api_token, event_type_name=None)
|
||||||
|
except:
|
||||||
|
calendly_events = []
|
||||||
|
|
||||||
|
families = get_sibling_families_from_csv(csv_file, calendly_events=calendly_events)
|
||||||
|
|
||||||
|
if not families:
|
||||||
|
raise HTTPException(status_code=404, detail="Keine Geschwisterkinder für QR-Karten gefunden.")
|
||||||
|
|
||||||
|
from qr_generator import generate_siblings_qr_overlay
|
||||||
|
generate_siblings_qr_overlay(input_pdf_path, output_pdf_path, families)
|
||||||
|
|
||||||
|
final_storage = os.path.join("/tmp", output_pdf_name)
|
||||||
|
shutil.copy(output_pdf_path, final_storage)
|
||||||
|
|
||||||
|
# Since the frontend has trouble triggering a blob download, return a JSON with a download link
|
||||||
|
download_url = f"/api/jobs/download-qr/{job_id}/{output_pdf_name}"
|
||||||
|
return JSONResponse(content={"status": "success", "download_url": download_url, "filename": output_pdf_name})
|
||||||
|
|
||||||
|
except HTTPException as he:
|
||||||
|
raise he
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error generating siblings QR cards")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
finally:
|
||||||
|
if driver: driver.quit()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/jobs/download-qr/{job_id}/{filename}")
|
||||||
|
async def download_generated_qr(job_id: str, filename: str):
|
||||||
|
file_path = os.path.join("/tmp", filename)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
return FileResponse(path=file_path, filename=filename, media_type="application/pdf")
|
||||||
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
|||||||
@@ -292,3 +292,62 @@ def overlay_text_on_pdf(base_pdf_path: str, output_pdf_path: str, texts: list):
|
|||||||
writer.write(output_file)
|
writer.write(output_file)
|
||||||
|
|
||||||
logger.info(f"Successfully generated overlaid PDF at {output_pdf_path}")
|
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)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ def generate_siblings_pdf_from_csv(csv_path: str, institution: str, calendly_eve
|
|||||||
except:
|
except:
|
||||||
raise Exception("CSV konnte nicht gelesen werden.")
|
raise Exception("CSV konnte nicht gelesen werden.")
|
||||||
|
|
||||||
df.columns = df.columns.str.strip().str.replace("\"", "")
|
df.columns = df.columns.str.strip().str.replace('"', "")
|
||||||
|
|
||||||
# Identify Email Column
|
# Identify Email Column
|
||||||
email_col = next((c for c in df.columns if "email" in c.lower()), None)
|
email_col = next((c for c in df.columns if "email" in c.lower()), None)
|
||||||
@@ -56,8 +56,6 @@ def generate_siblings_pdf_from_csv(csv_path: str, institution: str, calendly_eve
|
|||||||
try:
|
try:
|
||||||
start_dt = datetime.datetime.fromisoformat(event['start_time'].replace('Z', '+00:00'))
|
start_dt = datetime.datetime.fromisoformat(event['start_time'].replace('Z', '+00:00'))
|
||||||
start_dt = start_dt.astimezone(ZoneInfo("Europe/Berlin"))
|
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")
|
calendly_map[event['invitee_email'].lower().strip()] = start_dt.strftime("%d.%m. %H:%M")
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
@@ -123,3 +121,63 @@ def generate_siblings_pdf_from_csv(csv_path: str, institution: str, calendly_eve
|
|||||||
with open(output_path, "wb") as f:
|
with open(output_path, "wb") as f:
|
||||||
f.write(pdf)
|
f.write(pdf)
|
||||||
logger.info(f"Siblings PDF saved to {output_path}")
|
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
|
||||||
27
fotograf-de-scraper/frontend/dist/assets/index-0MFhRTsS.js
vendored
Normal file
27
fotograf-de-scraper/frontend/dist/assets/index-0MFhRTsS.js
vendored
Normal file
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
1
fotograf-de-scraper/frontend/dist/assets/index-uaZdwVxZ.css
vendored
Normal file
1
fotograf-de-scraper/frontend/dist/assets/index-uaZdwVxZ.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 113 B |
6
fotograf-de-scraper/frontend/dist/index.html
vendored
6
fotograf-de-scraper/frontend/dist/index.html
vendored
@@ -4,9 +4,9 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/fotograf-de/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/fotograf-de/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>frontend</title>
|
<title>Fotograf.de ERP</title>
|
||||||
<script type="module" crossorigin src="/fotograf-de/assets/index-BPS0v1Hk.js"></script>
|
<script type="module" crossorigin src="/fotograf-de/assets/index-0MFhRTsS.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/fotograf-de/assets/index-CMFolvzs.css">
|
<link rel="stylesheet" crossorigin href="/fotograf-de/assets/index-uaZdwVxZ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>frontend</title>
|
<title>Fotograf.de ERP</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 113 B |
@@ -39,6 +39,7 @@ function App() {
|
|||||||
const [selectedEventType, setSelectedEventType] = useState<string>("");
|
const [selectedEventType, setSelectedEventType] = useState<string>("");
|
||||||
const [isListGenerating, setIsListGenerating] = useState(false);
|
const [isListGenerating, setIsListGenerating] = useState(false);
|
||||||
const [isSiblingsGenerating, setIsSiblingsGenerating] = useState(false);
|
const [isSiblingsGenerating, setIsSiblingsGenerating] = useState(false);
|
||||||
|
const [isSiblingsQrGenerating, setIsSiblingsQrGenerating] = useState(false);
|
||||||
const [reminderTaskId, setReminderTaskId] = useState<string | null>(null);
|
const [reminderTaskId, setReminderTaskId] = useState<string | null>(null);
|
||||||
const [reminderProgress, setReminderProgress] = useState<string>('');
|
const [reminderProgress, setReminderProgress] = useState<string>('');
|
||||||
const [isReminderRunning, setIsReminderRunning] = useState(false);
|
const [isReminderRunning, setIsReminderRunning] = useState(false);
|
||||||
@@ -152,7 +153,13 @@ function App() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setEventTypes(data.event_types || []);
|
setEventTypes(data.event_types || []);
|
||||||
if (data.event_types && data.event_types.length > 0) {
|
if (data.event_types && data.event_types.length > 0) {
|
||||||
setSelectedEventType(data.event_types[0].name);
|
const savedType = localStorage.getItem('fotograf_selected_event_type');
|
||||||
|
const typeExists = data.event_types.some((et: any) => et.name === savedType);
|
||||||
|
if (savedType && typeExists) {
|
||||||
|
setSelectedEventType(savedType);
|
||||||
|
} else {
|
||||||
|
setSelectedEventType(data.event_types[0].name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -323,6 +330,47 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleGenerateSiblingsQrCards = async (job: Job, file: File) => {
|
||||||
|
if (!file) {
|
||||||
|
setError("Bitte wähle eine PDF-Vorlage aus.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSiblingsQrGenerating(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('pdf_file', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/jobs/${job.id}/siblings-qr-cards?account_type=${activeTab}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
throw new Error("Keine Geschwisterkinder für QR-Karten gefunden.");
|
||||||
|
}
|
||||||
|
const errData = await response.json();
|
||||||
|
throw new Error(errData.detail || 'Generierung fehlgeschlagen');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.status === "success" && result.download_url) {
|
||||||
|
window.open(`${API_BASE_URL}${result.download_url}`, '_blank');
|
||||||
|
setTimeout(fetchLatestFile, 2000);
|
||||||
|
} else {
|
||||||
|
throw new Error("Download URL could not be retrieved from server.");
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setIsSiblingsQrGenerating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleGenerateSiblingsList = async (job: Job) => {
|
const handleGenerateSiblingsList = async (job: Job) => {
|
||||||
setIsSiblingsGenerating(true);
|
setIsSiblingsGenerating(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -727,7 +775,10 @@ function App() {
|
|||||||
<label className="block text-sm font-bold text-gray-700 mb-2">Calendly Event auswählen</label>
|
<label className="block text-sm font-bold text-gray-700 mb-2">Calendly Event auswählen</label>
|
||||||
<select
|
<select
|
||||||
value={selectedEventType}
|
value={selectedEventType}
|
||||||
onChange={(e) => setSelectedEventType(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setSelectedEventType(e.target.value);
|
||||||
|
localStorage.setItem('fotograf_selected_event_type', e.target.value);
|
||||||
|
}}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-sm"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-sm"
|
||||||
>
|
>
|
||||||
{eventTypes.length === 0 && <option value="">Lade Events...</option>}
|
{eventTypes.length === 0 && <option value="">Lade Events...</option>}
|
||||||
@@ -788,13 +839,37 @@ function App() {
|
|||||||
<h6 className="font-bold text-sm text-gray-800 mb-1">👨👩👧👦 Geschwisterliste (Einrichtungsintern)</h6>
|
<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>
|
<p className="text-xs text-gray-600 mb-3">Sucht nach Geschwisterkindern in der Einrichtung und gleicht diese mit Calendly ab.</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="grid grid-cols-2 gap-3 mt-auto">
|
||||||
onClick={() => handleGenerateSiblingsList(selectedJob)}
|
<button
|
||||||
disabled={!selectedEventType || isSiblingsGenerating}
|
onClick={() => handleGenerateSiblingsList(selectedJob)}
|
||||||
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"
|
disabled={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"
|
||||||
{isSiblingsGenerating ? 'Generiere...' : 'Geschwisterliste generieren'}
|
>
|
||||||
</button>
|
{isSiblingsGenerating ? 'Generiere...' : '📄 PDF Liste'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".pdf"
|
||||||
|
id={`siblings-qr-upload-${selectedJob.id}`}
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.files && e.target.files.length > 0) {
|
||||||
|
handleGenerateSiblingsQrCards(selectedJob, e.target.files[0]);
|
||||||
|
e.target.value = ''; // reset
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => document.getElementById(`siblings-qr-upload-${selectedJob.id}`)?.click()}
|
||||||
|
disabled={isSiblingsQrGenerating}
|
||||||
|
className="w-full px-3 py-2 bg-emerald-600 text-white text-xs font-bold rounded-lg hover:bg-emerald-700 disabled:opacity-50 transition-all flex justify-center items-center gap-2 h-full"
|
||||||
|
>
|
||||||
|
{isSiblingsQrGenerating ? 'Generiere...' : '📇 QR-Karten drucken'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user