import os, sys, json from datetime import datetime, date from pathlib import Path import gradio as gr # Make src importable sys.path.append("src") from src.latest_froschgruppe.tools.organizer_tool import OrganizerTool from src.latest_froschgruppe.tools.payroll_tool import PayrollTool from src.latest_froschgruppe.tools.roadkill_gbif_tool import RoadkillGBIFTool from src.latest_froschgruppe.tools.calendar_tool import CalendarTool # ICS generator from src.latest_froschgruppe.tools.gbif_occurrence_tool import GBIFOccurrenceTool # Optional: if you have folium map tool # from latest_froschgruppe.tools.map_tool import build_map DATA_DIR = Path(os.getenv("DATA_DIR", "data")) DATA_DIR.mkdir(parents=True, exist_ok=True) EVENTS_FILE = DATA_DIR / "events.json" organizer = OrganizerTool() payroll = PayrollTool() gbif = GBIFOccurrenceTool() roadkill = RoadkillGBIFTool() cal = CalendarTool() def _load_events(): if not EVENTS_FILE.exists(): return [] return json.loads(EVENTS_FILE.read_text(encoding="utf-8")) def _save_events(events): EVENTS_FILE.write_text(json.dumps(events, ensure_ascii=False, indent=2), encoding="utf-8") def add_event(title: str, start_date: str, start_time: str, end_time: str, location: str): events = _load_events() # store ISO-ish strings start = f"{start_date}T{start_time}" end = f"{start_date}T{end_time}" ev = { "id": f"E{len(events)+1:04d}", "title": title.strip(), "start": start, "end": end, "location": location.strip(), "created_at": datetime.utcnow().isoformat() + "Z", } events.append(ev) _save_events(events) return list_events() def delete_event(event_id: str): events = _load_events() events = [e for e in events if e["id"] != event_id.strip()] _save_events(events) return list_events() def list_events(): events = _load_events() events_sorted = sorted(events, key=lambda e: e.get("start", "")) # dataframe-like list rows = [] for e in events_sorted[:200]: rows.append([e["id"], e["start"], e.get("end",""), e["title"], e.get("location","")]) return rows def dashboard_snapshot(): # Todos todos_md = organizer._run("list_todos", "{}") vols_md = organizer._run("list_volunteers", "{}") # Payroll report payroll_md = payroll._run("report", "{}") # Events events_rows = list_events() events_md = "## Nächste Events\n" if not events_rows: events_md += "Keine Events gespeichert." else: # show only next 10 events_md += "\n".join([f"- {r[1]} | {r[3]} @ {r[4]} (ID {r[0]})" for r in events_rows[:10]]) # Next migration window (simple heuristic) today = date.today() year = today.year # Typical window for DE: Feb 15 – Apr 30 (configurable) # If already past, show next year. window_start = date(year, 2, 15) window_end = date(year, 4, 30) if today > window_end: window_start = date(year + 1, 2, 15) window_end = date(year + 1, 4, 30) mig_md = ( "## Nächste Wanderphase (operatives Fenster)\n" f"- Fenster: **{window_start.isoformat()} bis {window_end.isoformat()}**\n" "- Trigger (praktisch): milde, feuchte Abende/Nächte + Regen → Einsatzwahrscheinlichkeit hoch.\n" "- Tipp: täglich 17:00 Wettercheck + bei Trigger Nachtteam aktivieren." ) return mig_md, events_md, todos_md, vols_md, payroll_md def add_todo(title: str, priority: str): payload = json.dumps({"title": title, "priority": priority}, ensure_ascii=False) msg = organizer._run("add_todo", payload) return msg, organizer._run("list_todos", "{}") def add_volunteer(name: str, phone: str, availability: str): payload = json.dumps({"name": name, "phone": phone, "availability": availability}, ensure_ascii=False) msg = organizer._run("add_volunteer", payload) return msg, organizer._run("list_volunteers", "{}") def payroll_plan(hourly_rate: float, monthly_cap: float): payload = json.dumps({"hourly_rate": hourly_rate, "monthly_cap": monthly_cap}, ensure_ascii=False) return payroll._run("plan", payload) def payroll_add_worker(name: str, hourly_rate: float, monthly_cap: float): payload = json.dumps({"name": name, "hourly_rate": hourly_rate, "monthly_cap": monthly_cap}, ensure_ascii=False) msg = payroll._run("add_worker", payload) rep = payroll._run("report", "{}") return msg, rep def payroll_log_hours(name: str, date_str: str, hours: float, note: str): payload = json.dumps({"name": name, "date": date_str, "hours": hours, "note": note}, ensure_ascii=False) msg = payroll._run("log_hours", payload) rep = payroll._run("report", "{}") return msg, rep def fetch_hotspots(species: str, country: str, year_from: int, year_to: int): # Tools return markdown tables + google maps links gbif_md = gbif._run(scientific_name=species, country=country, year_from=year_from, year_to=year_to, limit=300) road_md = roadkill._run(country=country, scientific_name=species, year_from=year_from, year_to=year_to, limit=300) return gbif_md, road_md def generate_migration_ics(region: str, year: int, start_month: int, start_day: int, end_month: int, end_day: int): # CalendarTool returns a path in text; we want a file output txt = cal._run(region=region, year=year, start_month=start_month, start_day=start_day, end_month=end_month, end_day=end_day) # Extract file path from tool output # expected: "✅ ICS erstellt: " path = None for token in txt.split(): if token.endswith(".ics"): path = token break return txt, path with gr.Blocks(title="Teich – Management Partner (Froschrettung)") as demo: gr.Markdown("# 🐸 Teich – Management Partner (Froschrettung)\n**Dashboard • Kalender • Einsätze • Daten • Team • Budget**") with gr.Tab("Dashboard"): btn_refresh = gr.Button("🔄 Dashboard aktualisieren") mig_md = gr.Markdown() events_md = gr.Markdown() todos_md = gr.Markdown() vols_md = gr.Markdown() payroll_md = gr.Markdown() btn_refresh.click(fn=dashboard_snapshot, inputs=[], outputs=[mig_md, events_md, todos_md, vols_md, payroll_md]) demo.load(fn=dashboard_snapshot, inputs=[], outputs=[mig_md, events_md, todos_md, vols_md, payroll_md]) with gr.Tab("Kalender & Schichten"): gr.Markdown("## Events\nEvents werden lokal in `data/events.json` gespeichert.") events_table = gr.Dataframe(headers=["ID", "Start", "Ende", "Titel", "Ort"], interactive=False) with gr.Row(): ev_title = gr.Textbox(label="Titel", placeholder="z.B. Zaunkontrolle / Nachtpatrouille") ev_location = gr.Textbox(label="Ort", placeholder="z.B. Straße X / Teich Y") with gr.Row(): ev_date = gr.Textbox(label="Datum (YYYY-MM-DD)", value=str(date.today())) ev_start = gr.Textbox(label="Start (HH:MM)", value="20:00") ev_end = gr.Textbox(label="Ende (HH:MM)", value="23:00") with gr.Row(): btn_add_ev = gr.Button("➕ Event anlegen") del_id = gr.Textbox(label="Event ID löschen", placeholder="E0001") btn_del_ev = gr.Button("🗑️ Löschen") btn_reload_ev = gr.Button("📋 Events neu laden") btn_reload_ev.click(fn=list_events, inputs=[], outputs=[events_table]) demo.load(fn=list_events, inputs=[], outputs=[events_table]) btn_add_ev.click(fn=add_event, inputs=[ev_title, ev_date, ev_start, ev_end, ev_location], outputs=[events_table]) btn_del_ev.click(fn=delete_event, inputs=[del_id], outputs=[events_table]) gr.Markdown("## Wanderfenster (ICS)\nErzeugt einen Standard-ICS (Wettercheck, Zaunkontrolle, Patrouillenblock).") with gr.Row(): region = gr.Textbox(label="Region", value="Deutschland") year = gr.Number(label="Jahr", value=date.today().year, precision=0) with gr.Row(): sm = gr.Number(label="Start Monat", value=2, precision=0) sd = gr.Number(label="Start Tag", value=15, precision=0) em = gr.Number(label="Ende Monat", value=4, precision=0) ed = gr.Number(label="Ende Tag", value=30, precision=0) btn_ics = gr.Button("📅 ICS generieren") ics_status = gr.Markdown() ics_file = gr.File(label="ICS Download") btn_ics.click(fn=generate_migration_ics, inputs=[region, year, sm, sd, em, ed], outputs=[ics_status, ics_file]) with gr.Tab("Einsatz planen"): gr.Markdown("## Einsatzplanung\nHier kannst du wie bisher einen Einsatz planen (später: Crew-Kickoff anbinden).") user_prompt = gr.Textbox(label="Auftrag / Situation", lines=5, placeholder="z.B. Plane nächste Woche Froschrettung in Region X ...") country = gr.Textbox(label="Land (GBIF Country Code)", value="DE") region2 = gr.Textbox(label="Region (Freitext)", value="Deutschland") species = gr.Dropdown(["Rana temporaria", "Bufo bufo", "Lissotriton vulgaris", "Triturus cristatus"], value="Rana temporaria", label="Zielart") year_from = gr.Number(label="Jahr von", value=2020, precision=0) year_to = gr.Number(label="Jahr bis", value=date.today().year, precision=0) # Optional: Hier später Crew kickoff (run_manager) wieder einhängen. gr.Markdown("➡️ (Nächster Schritt) Crew-Kickoff als Button: erzeugt Plan + Tasks + Kalender + Budget automatisch.") with gr.Tab("Daten & Hotspots (GBIF/Roadkill)"): gr.Markdown("## Amphibien-Sichtungen & Roadkill-Hotspots\nTop-Cluster inkl. Google-Maps Links.") with gr.Row(): d_species = gr.Dropdown(["Rana temporaria", "Bufo bufo", "Lissotriton vulgaris", "Triturus cristatus"], value="Rana temporaria", label="Art") d_country = gr.Textbox(label="Land", value="DE") d_y1 = gr.Number(label="Jahr von", value=2020, precision=0) d_y2 = gr.Number(label="Jahr bis", value=date.today().year, precision=0) btn_data = gr.Button("📡 Daten laden (Hotspots)") out_gbif = gr.Markdown() out_road = gr.Markdown() btn_data.click(fn=fetch_hotspots, inputs=[d_species, d_country, d_y1, d_y2], outputs=[out_gbif, out_road]) with gr.Tab("Team & Budget (450€)"): gr.Markdown("## To-Dos / Volunteers / Payroll\nAlles wird lokal gespeichert (DATA_DIR).") with gr.Accordion("To-Dos", open=True): todo_title = gr.Textbox(label="Neue Aufgabe", placeholder="z.B. Materialliste erstellen") todo_prio = gr.Dropdown(["L", "M", "H"], value="M", label="Priorität") btn_todo = gr.Button("➕ To-Do anlegen") todo_msg = gr.Markdown() todo_list = gr.Markdown() btn_todo.click(fn=add_todo, inputs=[todo_title, todo_prio], outputs=[todo_msg, todo_list]) demo.load(fn=lambda: organizer._run("list_todos", "{}"), inputs=[], outputs=[todo_list]) with gr.Accordion("Volunteers", open=False): v_name = gr.Textbox(label="Name") v_phone = gr.Textbox(label="Telefon") v_avail = gr.Textbox(label="Verfügbarkeit", placeholder="z.B. Mo/Di ab 18 Uhr") btn_vol = gr.Button("➕ Volunteer hinzufügen") vol_msg = gr.Markdown() vol_list = gr.Markdown() btn_vol.click(fn=add_volunteer, inputs=[v_name, v_phone, v_avail], outputs=[vol_msg, vol_list]) demo.load(fn=lambda: organizer._run("list_volunteers", "{}"), inputs=[], outputs=[vol_list]) with gr.Accordion("450€ Plan & Payroll", open=True): p_rate = gr.Number(label="Stundenlohn €", value=12.0, precision=2) p_cap = gr.Number(label="Monatscap €", value=450.0, precision=2) btn_plan = gr.Button("🧮 450€ Plan berechnen") plan_md = gr.Markdown() btn_plan.click(fn=payroll_plan, inputs=[p_rate, p_cap], outputs=[plan_md]) gr.Markdown("### Worker anlegen") w_name = gr.Textbox(label="Name") btn_add_w = gr.Button("➕ Worker hinzufügen") w_msg = gr.Markdown() rep_md = gr.Markdown() btn_add_w.click(fn=payroll_add_worker, inputs=[w_name, p_rate, p_cap], outputs=[w_msg, rep_md]) demo.load(fn=lambda: payroll._run("report", "{}"), inputs=[], outputs=[rep_md]) gr.Markdown("### Stunden loggen") l_name = gr.Textbox(label="Name (wie Worker)") l_date = gr.Textbox(label="Datum", value=str(date.today())) l_hours = gr.Number(label="Stunden", value=3.0, precision=2) l_note = gr.Textbox(label="Notiz", placeholder="z.B. Nachtpatrouille") btn_log = gr.Button("⏱️ log_hours") log_msg = gr.Markdown() btn_log.click(fn=payroll_log_hours, inputs=[l_name, l_date, l_hours, l_note], outputs=[log_msg, rep_md]) gr.Markdown("---\n**Hinweis:** In Hugging Face empfehle ich Persistent Storage aktivieren und `DATA_DIR=/data` setzen, dann bleiben Tasks/Events auch nach Restart erhalten.") demo.queue() demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))