diff --git a/web/src/padelnomics/app.py b/web/src/padelnomics/app.py index 51a0a64..e54a817 100644 --- a/web/src/padelnomics/app.py +++ b/web/src/padelnomics/app.py @@ -1,6 +1,7 @@ """ Padelnomics - Application factory and entry point. """ +import time from pathlib import Path from quart import Quart, Response, abort, g, redirect, request, session, url_for @@ -9,6 +10,8 @@ from .analytics import close_analytics_db, open_analytics_db from .core import close_db, config, get_csrf_token, init_db, setup_request_id from .i18n import LANG_BLUEPRINTS, SUPPORTED_LANGS, get_translations +_ASSET_VERSION = str(int(time.time())) + def _detect_lang() -> str: """Detect preferred language from cookie then Accept-Language header.""" @@ -220,6 +223,7 @@ def create_app() -> Quart: "ab_tag": getattr(g, "ab_tag", None), "lang": effective_lang, "t": get_translations(effective_lang), + "v": _ASSET_VERSION, } # ------------------------------------------------------------------------- diff --git a/web/src/padelnomics/locales/de.json b/web/src/padelnomics/locales/de.json index eb5ca46..8a44bad 100644 --- a/web/src/padelnomics/locales/de.json +++ b/web/src/padelnomics/locales/de.json @@ -224,6 +224,7 @@ "planner_quote_cta_check_4": "Nur passende Anbieter sehen deine Anfrage", "planner_quote_cta_btn": "Angebote einholen →", "planner_quote_cta_hint": "Dauert ca. 2 Minuten", + "planner_export_inline": "Teile diese Analyse mit Partnern oder Investoren", "planner_export_btn": "Geschäftsplan exportieren (PDF) →", "planner_export_hint": "99 € einmalig · Bankfertig", "planner_signup_bar_msg": "Erstelle ein Konto, um Szenarien zu speichern und Pläne zu vergleichen.", @@ -802,12 +803,12 @@ "card_total_courts": "Plätze gesamt", "card_floor_area": "Grundfläche", "card_court_area": "Platzfläche", - "card_total_capex": "Gesamt-CAPEX", + "card_total_capex": "Gesamtinvestition", "card_per_court": "Pro Platz", "card_per_sqm": "Pro m²", "budget_over": "BUDGET ÜBERSCHRITTEN", "budget_under": "IM BUDGET", - "table_total_capex": "GESAMT-CAPEX", + "table_total_capex": "GESAMTINVESTITION", "th_item": "Position", "th_amount": "Betrag", "card_net_rev_mo": "Nettoumsatz/Monat", @@ -1109,14 +1110,14 @@ "planner_section_util": "Auslastung & Betrieb", "planner_step3_title": "Investition & Baukosten", "planner_step3_sub": "Konfiguriere Baukosten, Glas- und Beleuchtungsoptionen sowie Dein Budgetziel.", - "planner_section_capex": "Bau & CAPEX", + "planner_section_capex": "Investitionskosten (CAPEX)", "planner_hint_adjust": "Nach Szenario anpassen", "planner_step4_title": "Betrieb & Finanzierung", "planner_step4_sub": "Monatliche Betriebskosten, Kreditkonditionen und Exit-Annahmen.", "planner_section_opex": "Monatliche Betriebskosten", "planner_section_financing": "Finanzierung", "planner_section_exit": "Exit-Annahmen", - "planner_chart_capex": "CAPEX-Aufschlüsselung", + "planner_chart_capex": "Kostenaufschlüsselung", "planner_chart_cf": "Monatlicher Netto-Cashflow (60 Monate)", "planner_chart_cum": "Kumulierter Cashflow", "planner_section_annual": "Jahresübersicht", @@ -1151,7 +1152,7 @@ "about_meta_desc": "Padelnomics ist eine kostenlose Finanzplanungsplattform für Padel-Unternehmer. Modelliere deine Investition, finde Anbieter und plane dein Padel-Business mit professionellen Tools.", "about_og_desc": "Entwickelt für Padel-Unternehmer, die professionelle Finanztools ohne Beratungskosten benötigen. Kostenloser Planer, 60+ Variablen, Anbieterverzeichnis und mehr.", "about_h1": "Über Padelnomics", - "about_body_p1": "Padel ist der am schnellsten wachsende Sport in Europa, doch für die meisten Unternehmer ist die Eröffnung einer Paddelhalle immer noch ein Sprung ins Ungewisse. Die Finanzen sind komplex: Der CAPEX variiert stark je nach Anlagentyp, der Standort bestimmt die Auslastung, und der Unterschied zwischen 60 % und 75 % Belegung kann über Erfolg oder Misserfolg einer Investition entscheiden.", + "about_body_p1": "Padel ist eine der am schnellsten wachsenden Sportarten weltweit, doch für die meisten Unternehmer ist die Eröffnung einer Paddelhalle immer noch ein Sprung ins Ungewisse. Die Finanzen sind komplex: Der CAPEX variiert stark je nach Anlagentyp, der Standort bestimmt die Auslastung, und der Unterschied zwischen 60 % und 75 % Belegung kann über Erfolg oder Misserfolg einer Investition entscheiden.", "about_body_p2": "Wir haben Padelnomics gebaut, weil wir kein ausreichend gutes Finanzplanungstool gefunden haben. Vorhandene Rechner sind entweder zu simpel (5 Eingaben, ein Ergebnis) oder hinter teuren Beratungsmandaten verborgen. Wir wollten etwas mit der Tiefe eines professionellen Finanzmodells, aber der Zugänglichkeit einer Web-App.", "about_body_p3": "Das Ergebnis ist ein kostenloser Finanzplaner mit 60+ anpassbaren Variablen, 6 Analyse-Tabs, Sensitivitätsanalyse und den professionellen Kennzahlen, die Banken und Investoren sehen müssen. Jede Annahme ist transparent und anpassbar. Keine Blackboxen.", "about_why_p": "Der Planer ist kostenlos, weil wir glauben, dass bessere Planung zu besseren Padelanlagen führt — und das ist gut für die gesamte Branche. Wir verdienen Geld, indem wir Unternehmer mit Platz-Anbietern und Finanzierungspartnern verbinden, wenn sie bereit sind, von der Planung zum Bau überzugehen.", @@ -1191,8 +1192,8 @@ "landing_faq_a2": "Nein. Der Planer funktioniert sofort ohne Registrierung. Erstelle ein Konto, um Szenarien zu speichern, Konfigurationen zu vergleichen und PDF-Berichte zu exportieren.", "landing_faq_a3": "Wenn du über den Planer Angebote anforderst, teilen wir deine Projektdetails (Anlagentyp, Platzzahl, Glas, Beleuchtung, Land, Budget, Zeitplan) mit passenden Anbietern aus unserem Verzeichnis. Diese kontaktieren dich direkt mit ihren Angeboten.", "landing_faq_a4": "Das Durchsuchen des Verzeichnisses ist für alle kostenlos. Anbieter erhalten standardmäßig einen Basiseintrag. Kostenpflichtige Pläne (Basic ab 39 €/Monat, Growth ab 199 €/Monat, Pro ab 499 €/Monat) schalten Anfrageformulare, vollständige Beschreibungen, Logos, verifizierte Badges und Prioritätsplatzierung frei.", - "landing_faq_a5": "Das Modell verwendet reale Standardwerte auf Basis europäischer Marktdaten. Jede Annahme ist anpassbar, sodass du deine lokalen Gegebenheiten abbilden kannst. Die Sensitivitätsanalyse zeigt, wie sich die Ergebnisse in verschiedenen Szenarien verändern, und hilft dir, die Bandbreite möglicher Ergebnisse zu verstehen.", - "landing_seo_p1": "Padel ist der am schnellsten wachsende Sport in Europa — die Nachfrage nach Plätzen übersteigt das Angebot in Deutschland, Österreich, der Schweiz und darüber hinaus bei weitem. Eine Paddelhalle zu eröffnen kann eine attraktive Investition sein, aber die Zahlen müssen stimmen. Eine typische Indoorhalle mit 6–8 Plätzen erfordert zwischen 300.000 € (Anmietung eines Bestandsgebäudes) und 2–3 Mio. € (Neubau), mit Amortisationszeiten von 3–5 Jahren für gut gelegene Anlagen.", + "landing_faq_a5": "Das Modell verwendet reale Standardwerte auf Basis globaler Marktdaten. Jede Annahme ist anpassbar, sodass du deine lokalen Gegebenheiten abbilden kannst. Die Sensitivitätsanalyse zeigt, wie sich die Ergebnisse in verschiedenen Szenarien verändern, und hilft dir, die Bandbreite möglicher Ergebnisse zu verstehen.", + "landing_seo_p1": "Padel ist eine der am schnellsten wachsenden Racketsportarten weltweit — die Nachfrage nach Plätzen übersteigt das Angebot von Deutschland, Spanien und Schweden bis in die USA und den Nahen Osten. Eine Paddelhalle zu eröffnen kann eine attraktive Investition sein, aber die Zahlen müssen stimmen. Eine typische Indoorhalle mit 6–8 Plätzen erfordert zwischen 300.000 € (Anmietung eines Bestandsgebäudes) und 2–3 Mio. € (Neubau), mit Amortisationszeiten von 3–5 Jahren für gut gelegene Anlagen.", "landing_seo_p2": "Die entscheidenden Faktoren für den Erfolg sind Standort (treibt die Auslastung), Baukosten (CAPEX), Miet- oder Grundstückskosten sowie die Preisstrategie. Unser Finanzplaner ermöglicht es dir, alle diese Variablen interaktiv zu modellieren und die Auswirkungen auf IRR, MOIC, Cashflow und Schuldendienstdeckungsgrad in Echtzeit zu sehen. Ob du als Unternehmer deine erste Anlage prüfst, als Immobilienentwickler Padel in ein Mixed-Use-Projekt integrierst oder als Investor eine bestehende Paddelhalle bewertest — Padelnomics gibt dir die finanzielle Klarheit für fundierte Entscheidungen.", "landing_final_cta_sub": "Modelliere deine Investition und lass dich mit verifizierten Platz-Anbietern aus {total_countries} Ländern zusammenbringen.", "landing_jsonld_org_desc": "Professionelle Planungsplattform für Padelplatz-Investitionen. Finanzplaner, Anbieterverzeichnis und Marktinformationen für Padel-Unternehmer.", diff --git a/web/src/padelnomics/locales/en.json b/web/src/padelnomics/locales/en.json index 1aa318f..dbd6bc0 100644 --- a/web/src/padelnomics/locales/en.json +++ b/web/src/padelnomics/locales/en.json @@ -224,6 +224,7 @@ "planner_quote_cta_check_4": "Only matching suppliers see your request", "planner_quote_cta_btn": "Get Quotes →", "planner_quote_cta_hint": "Takes ~2 minutes", + "planner_export_inline": "Share this analysis with partners or investors", "planner_export_btn": "Export Business Plan (PDF) →", "planner_export_hint": "€99 one-time · Bank-ready", "planner_signup_bar_msg": "Create an account to save scenarios and compare plans.", @@ -1151,7 +1152,7 @@ "about_meta_desc": "Padelnomics is a free financial planning platform for padel entrepreneurs. Model your investment, find suppliers, and plan your padel court business with professional-grade tools.", "about_og_desc": "Built for padel entrepreneurs who need professional financial tools without consulting fees. Free planner, 60+ variables, supplier directory, and more.", "about_h1": "About Padelnomics", - "about_body_p1": "Padel is the fastest-growing sport in Europe, but opening a padel hall is still a leap of faith for most entrepreneurs. The financials are complex: CAPEX varies wildly depending on venue type, location drives utilization, and the difference between a 60% and 75% occupancy rate can mean the difference between a great investment and a money pit.", + "about_body_p1": "Padel is one of the fastest-growing sports worldwide, but opening a padel hall is still a leap of faith for most entrepreneurs. The financials are complex: CAPEX varies wildly depending on venue type, location drives utilization, and the difference between a 60% and 75% occupancy rate can mean the difference between a great investment and a money pit.", "about_body_p2": "We built Padelnomics because we couldn't find a financial planning tool that was good enough. Existing calculators are either too simplistic (5 inputs, one output) or locked behind expensive consulting engagements. We wanted something with the depth of a professional financial model but the accessibility of a web app.", "about_body_p3": "The result is a free financial planner with 60+ adjustable variables, 6 analysis tabs, sensitivity analysis, and the professional metrics that banks and investors need to see. Every assumption is transparent and adjustable. No black boxes.", "about_why_p": "The planner is free because we believe better planning leads to better padel venues, and that's good for the entire industry. We make money by connecting entrepreneurs with court suppliers and financing partners when they're ready to move from planning to building.", @@ -1191,8 +1192,8 @@ "landing_faq_a2": "No. The planner works instantly with no signup. Create an account to save scenarios, compare configurations, and export PDF reports.", "landing_faq_a3": "When you request quotes through the planner, we share your project details (venue type, court count, glass, lighting, country, budget, timeline) with relevant suppliers from our directory. They contact you directly with proposals.", "landing_faq_a4": "Browsing the directory is free for everyone. Suppliers have a basic listing by default. Paid plans (Basic at €39/mo, Growth at €199/mo, Pro at €499/mo) unlock enquiry forms, full descriptions, logos, verified badges, and priority placement.", - "landing_faq_a5": "The model uses real-world defaults based on European market data. Every assumption is adjustable so you can match your local conditions. The sensitivity analysis shows how results change across different scenarios, helping you understand the range of outcomes.", - "landing_seo_p1": "Padel is the fastest-growing sport in Europe, with demand for courts far outstripping supply in Germany, the UK, Scandinavia, and beyond. Opening a padel hall can be a lucrative investment, but the numbers need to work. A typical indoor padel venue with 6-8 courts requires between €300K (renting an existing building) and €2-3M (building new), with payback periods of 3-5 years for well-located venues.", + "landing_faq_a5": "The model uses real-world defaults based on global market data. Every assumption is adjustable so you can match your local conditions. The sensitivity analysis shows how results change across different scenarios, helping you understand the range of outcomes.", + "landing_seo_p1": "Padel is one of the fastest-growing racket sports globally, with demand for courts outstripping supply across markets from Germany, Spain, and Sweden to the US and Middle East. Opening a padel hall can be a lucrative investment, but the numbers need to work. A typical indoor padel venue with 6-8 courts requires between €300K (renting an existing building) and €2-3M (building new), with payback periods of 3-5 years for well-located venues.", "landing_seo_p2": "The key variables that determine success are location (driving utilization), construction costs (CAPEX), rent or land costs, and pricing strategy. Our financial planner lets you model all of these variables interactively, seeing the impact on your IRR, MOIC, cash flow, and debt service coverage ratio in real time. Whether you're an entrepreneur exploring your first venue, a real estate developer adding padel to a mixed-use project, or an investor evaluating a padel hall acquisition, Padelnomics gives you the financial clarity to make informed decisions.", "landing_final_cta_sub": "Model your investment, then get matched with verified court suppliers across {total_countries} countries.", "landing_jsonld_org_desc": "Professional padel court investment planning platform. Financial planner, supplier directory, and market intelligence for padel entrepreneurs.", diff --git a/web/src/padelnomics/planner/calculator.py b/web/src/padelnomics/planner/calculator.py index cf38776..96e75c2 100644 --- a/web/src/padelnomics/planner/calculator.py +++ b/web/src/padelnomics/planner/calculator.py @@ -20,11 +20,11 @@ DEFAULTS = { "venue": "indoor", "own": "rent", "dblCourts": 4, - "sglCourts": 2, - "sqmPerDblHall": 336, - "sqmPerSglHall": 240, - "sqmPerDblOutdoor": 312, - "sqmPerSglOutdoor": 216, + "sglCourts": 0, + "sqmPerDblHall": 250, + "sqmPerSglHall": 160, + "sqmPerDblOutdoor": 230, + "sqmPerSglOutdoor": 150, "ratePeak": 50, "rateOffPeak": 35, "rateSingle": 30, diff --git a/web/src/padelnomics/planner/routes.py b/web/src/padelnomics/planner/routes.py index 598a7f7..a3c9cff 100644 --- a/web/src/padelnomics/planner/routes.py +++ b/web/src/padelnomics/planner/routes.py @@ -19,7 +19,7 @@ from ..core import ( waitlist_gate, ) from ..i18n import get_translations -from .calculator import COUNTRY_CURRENCY, CURRENCY_DEFAULT, DEFAULTS, calc, validate_state +from .calculator import COUNTRY_CURRENCY, CURRENCY_DEFAULT, calc, validate_state bp = Blueprint( "planner", @@ -99,7 +99,11 @@ def augment_d(d: dict, s: dict, lang: str) -> None: "#EC4899", "#06B6D4", "#84CC16", "#F97316", "#475569", "#0EA5E9", "#A78BFA", ] - _cap_items = [i for i in d["capexItems"] if i["amount"] > 0] + _cap_items = sorted( + [i for i in d["capexItems"] if i["amount"] > 0], + key=lambda i: i["amount"], + reverse=True, + ) d["capex_chart"] = { "type": "doughnut", "data": { @@ -114,7 +118,7 @@ def augment_d(d: dict, s: dict, lang: str) -> None: "responsive": True, "maintainAspectRatio": False, "cutout": "60%", - "plugins": {"legend": {"position": "right", "labels": {"boxWidth": 10, "font": {"size": 10}}}}, + "plugins": {"legend": {"position": "right", "labels": {"boxWidth": 10, "font": {"size": 12}, "padding": 8}}}, }, } @@ -322,7 +326,15 @@ async def index(): default = None if g.user: scenario_count = await count_scenarios(g.user["id"]) - default = await get_default_scenario(g.user["id"]) + # Load specific scenario if ?scenario= is set, else default + scenario_id = request.args.get("scenario", type=int) + if scenario_id: + default = await fetch_one( + "SELECT * FROM scenarios WHERE id = ? AND user_id = ? AND deleted_at IS NULL", + (scenario_id, g.user["id"]), + ) + if not default: + default = await get_default_scenario(g.user["id"]) initial_state = json.loads(default["state_json"]) if default else {} s = validate_state(initial_state) lang = g.get("lang", "en") @@ -339,8 +351,9 @@ async def index(): lang=lang, active_tab="capex", country_presets=COUNTRY_PRESETS, - defaults=DEFAULTS, currency_sym=cur["sym"], + step=1, + total_steps=TOTAL_WIZARD_STEPS, ) @@ -353,7 +366,7 @@ async def calculate(): d = calc(s, lang=lang) augment_d(d, s, lang) active_tab = form.get("activeTab", "capex") - if active_tab not in {"capex", "operating", "cashflow", "returns", "metrics"}: + if active_tab not in {"capex", "operating", "cashflow", "returns"}: active_tab = "capex" cur = COUNTRY_CURRENCY.get(s["country"], CURRENCY_DEFAULT) g.currency_sym = cur["sym"] @@ -368,6 +381,9 @@ async def calculate(): ) +TOTAL_WIZARD_STEPS = 4 + + @bp.route("/scenarios", methods=["GET"]) @login_required async def scenario_list(): @@ -379,24 +395,23 @@ async def scenario_list(): @login_required @csrf_protect async def save_scenario(): - data = await request.get_json() - name = data.get("name", "Untitled Scenario") - state_json = data.get("state_json", "{}") - location = data.get("location", "") - scenario_id = data.get("id") + form = await request.form + name = form.get("scenario_name", "Untitled Scenario") + # Build state_json from form data (same form as the planner) + state_json = json.dumps(form_to_state(form)) + location = form.get("location", "") + scenario_id = form.get("scenario_id") now = datetime.utcnow().isoformat() is_first_save = not scenario_id and (await count_scenarios(g.user["id"])) == 0 if scenario_id: - # Update existing await execute( "UPDATE scenarios SET name = ?, state_json = ?, location = ?, updated_at = ? WHERE id = ? AND user_id = ? AND deleted_at IS NULL", - (name, state_json, location, now, scenario_id, g.user["id"]), + (name, state_json, location, now, int(scenario_id), g.user["id"]), ) else: - # Create new scenario_id = await execute( "INSERT INTO scenarios (user_id, name, state_json, location, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", (g.user["id"], name, state_json, location, now, now), @@ -416,8 +431,9 @@ async def save_scenario(): except Exception as e: print(f"[NURTURE] Failed to add {g.user['email']} to audience: {e}") - count = await count_scenarios(g.user["id"]) - return jsonify({"ok": True, "id": scenario_id, "count": count}) + lang = g.get("lang", "en") + t = get_translations(lang) + return f'
✓ {t.get("scenario_saved", "Saved")}
' @bp.route("/scenarios/", methods=["GET"]) diff --git a/web/src/padelnomics/planner/templates/partials/calculate_response.html b/web/src/padelnomics/planner/templates/partials/calculate_response.html index 0d4742e..a68f022 100644 --- a/web/src/padelnomics/planner/templates/partials/calculate_response.html +++ b/web/src/padelnomics/planner/templates/partials/calculate_response.html @@ -6,3 +6,5 @@
{% include "partials/court_summary.html" %}
{% include "partials/wizard_preview.html" %}
+ +
{{ d.capex | fmt_k }} {{ t.planner_cta_estimated|default('estimated') }}
diff --git a/web/src/padelnomics/planner/templates/partials/scenario_list.html b/web/src/padelnomics/planner/templates/partials/scenario_list.html index fe9aa61..be3c0c5 100644 --- a/web/src/padelnomics/planner/templates/partials/scenario_list.html +++ b/web/src/padelnomics/planner/templates/partials/scenario_list.html @@ -10,7 +10,7 @@
{{ s.name }}
{% if s.is_default %}{{ t.scenario_badge_default }}{% endif %} - + {{ t.scenario_btn_load }} +{% else %} +
+{% endif %} +{% if step >= total_steps %} + +{% else %} + +{% endif %} diff --git a/web/src/padelnomics/planner/templates/planner.html b/web/src/padelnomics/planner/templates/planner.html index 08c3e5c..88340a0 100644 --- a/web/src/padelnomics/planner/templates/planner.html +++ b/web/src/padelnomics/planner/templates/planner.html @@ -7,7 +7,7 @@ - + {% endblock %} @@ -50,14 +50,30 @@ hx-swap="innerHTML"> {{ t.btn_my_scenarios }} ({{ scenario_count }}) - + +
+ {% endif %}