merge: bp-and-articles — 12 cornerstone articles + KfW PDF overhaul
From worktree-bp-and-articles:
Content (12 articles, Batch 1):
C2 Cost Bible (DE+EN), C3 Business Plan for Banks (DE+EN),
C5 Location Guide (DE+EN), C6 Financing Guide (DE+EN),
C7 Risk Register (DE+EN), C8 Build Guide (DE+EN)
All written natively (linguistic-mediation for DE), frontmatter complete.
CMS fix:
Article form now includes language selector; seo_head generated +
stored for manually created articles; build path is lang-prefixed.
Business Plan PDF overhaul (KfW Gründerkredit-ready):
- compute_sensitivity() extracted as reusable function
- matplotlib SVG charts (P&L + 12-month cash flow)
- Opening balance sheet, use-of-funds, sensitivity analysis
- Market analysis auto-populated from DuckDB city data
- Pre-export details form (/planner/export/details)
- Migration 0020: bp_details_json on scenarios table
- Complete PDF redesign: Precision Finance aesthetic
(navy/gold, Georgia headings, cover page, TOC, 15 sections)
- 28 new translation keys in en.json + de.json
Docs:
SPORTPLATZWELT_RESEARCH.md + CUSTOMER_CHANNELS.md updated
with verified contacts and trade show dates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
# web/src/padelnomics/admin/routes.py
# web/src/padelnomics/locales/de.json
# web/src/padelnomics/locales/en.json
This commit is contained in:
@@ -37,6 +37,42 @@ from ..email_templates import EMAIL_TEMPLATE_REGISTRY, render_email_template
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _build_article_seo_head(
|
||||
url_path: str,
|
||||
title: str,
|
||||
meta_desc: str,
|
||||
language: str,
|
||||
published_at: str,
|
||||
*,
|
||||
base_url: str = "https://padelnomics.io",
|
||||
) -> str:
|
||||
"""Build SEO head block (canonical, OG, JSON-LD) for a manually created article."""
|
||||
def _esc(text: str) -> str:
|
||||
return text.replace("&", "&").replace('"', """).replace("<", "<")
|
||||
|
||||
full_url = f"{base_url}/{language}{url_path}"
|
||||
jsonld = json.dumps({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": title[:110],
|
||||
"description": meta_desc[:200],
|
||||
"url": full_url,
|
||||
"inLanguage": language,
|
||||
"datePublished": published_at,
|
||||
"dateModified": published_at,
|
||||
"author": {"@type": "Organization", "name": "Padelnomics", "url": "https://padelnomics.io"},
|
||||
"publisher": {"@type": "Organization", "name": "Padelnomics", "url": "https://padelnomics.io"},
|
||||
}, ensure_ascii=False)
|
||||
return "\n".join([
|
||||
f'<link rel="canonical" href="{full_url}" />',
|
||||
f'<meta property="og:title" content="{_esc(title)}" />',
|
||||
f'<meta property="og:description" content="{_esc(meta_desc)}" />',
|
||||
f'<meta property="og:url" content="{full_url}" />',
|
||||
'<meta property="og:type" content="article" />',
|
||||
f'<script type="application/ld+json">{jsonld}</script>',
|
||||
])
|
||||
|
||||
|
||||
# Blueprint with its own template folder
|
||||
bp = Blueprint(
|
||||
"admin",
|
||||
@@ -2138,6 +2174,7 @@ async def article_new():
|
||||
country = form.get("country", "").strip()
|
||||
region = form.get("region", "").strip()
|
||||
body = form.get("body", "").strip()
|
||||
language = form.get("language", "en").strip() or "en"
|
||||
status = form.get("status", "draft")
|
||||
published_at = form.get("published_at", "").strip()
|
||||
|
||||
@@ -2153,9 +2190,9 @@ async def article_new():
|
||||
body_html = mistune.html(body)
|
||||
body_html = await bake_scenario_cards(body_html)
|
||||
|
||||
BUILD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
build_path = BUILD_DIR / f"{article_slug}.html"
|
||||
build_path.write_text(body_html)
|
||||
build_dir = BUILD_DIR / language
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
(build_dir / f"{article_slug}.html").write_text(body_html)
|
||||
|
||||
# Save markdown source
|
||||
md_dir = Path("data/content/articles")
|
||||
@@ -2163,14 +2200,15 @@ async def article_new():
|
||||
(md_dir / f"{article_slug}.md").write_text(body)
|
||||
|
||||
pub_dt = published_at or utcnow_iso()
|
||||
seo_head = _build_article_seo_head(url_path, title, meta_description, language, pub_dt)
|
||||
|
||||
await execute(
|
||||
"""INSERT INTO articles
|
||||
(url_path, slug, title, meta_description, og_image_url,
|
||||
country, region, status, published_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
country, region, language, status, published_at, seo_head)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(url_path, article_slug, title, meta_description, og_image_url,
|
||||
country, region, status, pub_dt),
|
||||
country, region, language, status, pub_dt, seo_head),
|
||||
)
|
||||
from ..sitemap import invalidate_sitemap_cache
|
||||
invalidate_sitemap_cache()
|
||||
@@ -2202,6 +2240,7 @@ async def article_edit(article_id: int):
|
||||
country = form.get("country", "").strip()
|
||||
region = form.get("region", "").strip()
|
||||
body = form.get("body", "").strip()
|
||||
language = form.get("language", article.get("language", "en")).strip() or "en"
|
||||
status = form.get("status", article["status"])
|
||||
published_at = form.get("published_at", "").strip()
|
||||
|
||||
@@ -2215,8 +2254,9 @@ async def article_edit(article_id: int):
|
||||
if body:
|
||||
body_html = mistune.html(body)
|
||||
body_html = await bake_scenario_cards(body_html)
|
||||
BUILD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
(BUILD_DIR / f"{article['slug']}.html").write_text(body_html)
|
||||
build_dir = BUILD_DIR / language
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
(build_dir / f"{article['slug']}.html").write_text(body_html)
|
||||
|
||||
md_dir = Path("data/content/articles")
|
||||
md_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -2224,14 +2264,16 @@ async def article_edit(article_id: int):
|
||||
|
||||
now = utcnow_iso()
|
||||
pub_dt = published_at or article["published_at"]
|
||||
seo_head = _build_article_seo_head(url_path, title, meta_description, language, pub_dt)
|
||||
|
||||
await execute(
|
||||
"""UPDATE articles
|
||||
SET title = ?, url_path = ?, meta_description = ?, og_image_url = ?,
|
||||
country = ?, region = ?, status = ?, published_at = ?, updated_at = ?
|
||||
country = ?, region = ?, language = ?, status = ?, published_at = ?,
|
||||
seo_head = ?, updated_at = ?
|
||||
WHERE id = ?""",
|
||||
(title, url_path, meta_description, og_image_url,
|
||||
country, region, status, pub_dt, now, article_id),
|
||||
country, region, language, status, pub_dt, seo_head, now, article_id),
|
||||
)
|
||||
await flash("Article updated.", "success")
|
||||
return redirect(url_for("admin.articles"))
|
||||
|
||||
@@ -60,7 +60,14 @@
|
||||
<p class="form-hint">Use [scenario:slug] to embed scenario widgets. Sections: :capex, :operating, :cashflow, :returns, :full</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;" class="mb-4">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem;" class="mb-4">
|
||||
<div>
|
||||
<label class="form-label" for="language">Language</label>
|
||||
<select id="language" name="language" class="form-input">
|
||||
<option value="en" {% if data.get('language', 'en') == 'en' %}selected{% endif %}>English (en)</option>
|
||||
<option value="de" {% if data.get('language') == 'de' %}selected{% endif %}>German (de)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="status">Status</label>
|
||||
<select id="status" name="status" class="form-input">
|
||||
|
||||
@@ -4,12 +4,15 @@ Business Plan PDF generation engine.
|
||||
Renders an HTML template with planner data, converts to PDF via WeasyPrint.
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from .analytics import fetch_analytics
|
||||
from .core import fetch_one
|
||||
from .i18n import get_translations
|
||||
from .planner.calculator import COUNTRY_CURRENCY, CURRENCY_DEFAULT, calc, validate_state
|
||||
from .planner.routes import compute_sensitivity
|
||||
|
||||
TEMPLATE_DIR = Path(__file__).parent / "templates" / "businessplan"
|
||||
|
||||
@@ -43,7 +46,170 @@ def _fmt_months(idx: int, t: dict) -> str:
|
||||
return t["bp_years"].format(n=f"{years:.1f}")
|
||||
|
||||
|
||||
def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
|
||||
def _generate_pnl_chart_svg(annuals: list[dict], sym: str) -> str:
|
||||
"""Generate P&L trend chart as SVG string using matplotlib.
|
||||
|
||||
Returns an SVG string for embedding in WeasyPrint HTML.
|
||||
Returns empty string if matplotlib is unavailable.
|
||||
"""
|
||||
try:
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.ticker as mticker
|
||||
except ImportError:
|
||||
return ""
|
||||
|
||||
years = [f"Y{a['year']}" for a in annuals]
|
||||
revenues = [a["revenue"] / 1000 for a in annuals]
|
||||
ebitdas = [a["ebitda"] / 1000 for a in annuals]
|
||||
net_cfs = [a["ncf"] / 1000 for a in annuals]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(5.5, 2.8))
|
||||
x = range(len(years))
|
||||
width = 0.28
|
||||
|
||||
ax.bar([i - width for i in x], revenues, width, label=f"Revenue ({sym}k)", color="#1D4ED8", alpha=0.85)
|
||||
ax.bar([i for i in x], ebitdas, width, label=f"EBITDA ({sym}k)", color="#16A34A", alpha=0.85)
|
||||
ax.bar([i + width for i in x], net_cfs, width, label=f"Net CF ({sym}k)", color="#D97706", alpha=0.85)
|
||||
|
||||
ax.set_xticks(list(x))
|
||||
ax.set_xticklabels(years, fontsize=8)
|
||||
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{sym}{v:.0f}k"))
|
||||
ax.tick_params(axis="y", labelsize=7)
|
||||
ax.legend(fontsize=7, loc="upper left", framealpha=0.5)
|
||||
ax.spines["top"].set_visible(False)
|
||||
ax.spines["right"].set_visible(False)
|
||||
ax.grid(axis="y", linestyle="--", alpha=0.4)
|
||||
ax.set_facecolor("#FAFAFA")
|
||||
fig.patch.set_facecolor("white")
|
||||
fig.tight_layout(pad=0.5)
|
||||
|
||||
buf = io.BytesIO()
|
||||
fig.savefig(buf, format="svg", dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
buf.seek(0)
|
||||
return buf.read().decode("utf-8")
|
||||
|
||||
|
||||
def _generate_cashflow_chart_svg(months: list[dict], sym: str) -> str:
|
||||
"""Generate 12-month cumulative cash flow chart as SVG string using matplotlib.
|
||||
|
||||
Returns an SVG string for embedding in WeasyPrint HTML.
|
||||
Returns empty string if matplotlib is unavailable.
|
||||
"""
|
||||
try:
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.ticker as mticker
|
||||
except ImportError:
|
||||
return ""
|
||||
|
||||
m12 = months[:12]
|
||||
labels = [str(m["m"]) for m in m12]
|
||||
cumulatives = [m["cum"] / 1000 for m in m12]
|
||||
ncfs = [m["ncf"] / 1000 for m in m12]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(5.5, 2.6))
|
||||
ax.bar(range(len(labels)), ncfs, color=["#16A34A" if v >= 0 else "#EF4444" for v in ncfs], alpha=0.7, label=f"Monthly NCF ({sym}k)")
|
||||
ax.plot(range(len(labels)), cumulatives, color="#1D4ED8", linewidth=2, marker="o", markersize=4, label=f"Cumulative ({sym}k)")
|
||||
ax.axhline(0, color="#94A3B8", linewidth=0.8, linestyle="--")
|
||||
|
||||
ax.set_xticks(range(len(labels)))
|
||||
ax.set_xticklabels([f"M{lbl}" for lbl in labels], fontsize=7)
|
||||
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{sym}{v:.0f}k"))
|
||||
ax.tick_params(axis="y", labelsize=7)
|
||||
ax.legend(fontsize=7, loc="lower right", framealpha=0.5)
|
||||
ax.spines["top"].set_visible(False)
|
||||
ax.spines["right"].set_visible(False)
|
||||
ax.grid(axis="y", linestyle="--", alpha=0.4)
|
||||
ax.set_facecolor("#FAFAFA")
|
||||
fig.patch.set_facecolor("white")
|
||||
fig.tight_layout(pad=0.5)
|
||||
|
||||
buf = io.BytesIO()
|
||||
fig.savefig(buf, format="svg", dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
buf.seek(0)
|
||||
return buf.read().decode("utf-8")
|
||||
|
||||
|
||||
def _compute_opening_balance(d: dict, fmt) -> dict:
|
||||
"""Compute the opening balance sheet (Eröffnungsbilanz) from CAPEX + financing data."""
|
||||
# Assets side: CAPEX items + working capital buffer
|
||||
fixed_assets = sum(
|
||||
i["amount"] for i in d["capexItems"]
|
||||
if not any(kw in i.get("name", "").lower() for kw in ["working", "betrieb", "reserve", "puffer"])
|
||||
)
|
||||
working_capital = d["capex"] - fixed_assets
|
||||
|
||||
# Liabilities: loan; Equity: everything else
|
||||
total_assets = d["capex"]
|
||||
loan = d["loanAmount"]
|
||||
equity = d["equity"]
|
||||
|
||||
return {
|
||||
"total_assets": fmt(total_assets),
|
||||
"fixed_assets": fmt(fixed_assets),
|
||||
"working_capital": fmt(max(working_capital, 0)),
|
||||
"total_liabilities_equity": fmt(total_assets),
|
||||
"loan": fmt(loan),
|
||||
"equity": fmt(equity),
|
||||
"equity_ratio": f"{(equity / total_assets * 100):.1f}%" if total_assets > 0 else "-",
|
||||
}
|
||||
|
||||
|
||||
def _compute_use_of_funds(d: dict, fmt) -> list[dict]:
|
||||
"""Map loan to CAPEX line items (Mittelverwendungsplan)."""
|
||||
loan = d["loanAmount"]
|
||||
total = d["capex"]
|
||||
|
||||
rows = []
|
||||
for item in d["capexItems"]:
|
||||
if item["amount"] <= 0:
|
||||
continue
|
||||
share = item["amount"] / total if total > 0 else 0
|
||||
from_loan = min(item["amount"], loan * share * 1.1) # approx allocation
|
||||
from_equity = item["amount"] - from_loan
|
||||
rows.append({
|
||||
"item": item["name"],
|
||||
"total": fmt(item["amount"]),
|
||||
"from_loan": fmt(round(from_loan)),
|
||||
"from_equity": fmt(round(from_equity)),
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
async def _fetch_market_data(location: str, language: str) -> dict | None:
|
||||
"""Query DuckDB for city-level market data to auto-populate market analysis.
|
||||
|
||||
Returns None if no matching city is found.
|
||||
"""
|
||||
if not location:
|
||||
return None
|
||||
|
||||
# Try matching city slug (lowercase, spaces→hyphens)
|
||||
city_slug = location.lower().strip().replace(" ", "-").replace(",", "")
|
||||
|
||||
rows = await fetch_analytics(
|
||||
"""
|
||||
SELECT city_slug, city_name, country,
|
||||
padel_venue_count, venues_per_100k, market_score,
|
||||
median_peak_rate, median_offpeak_rate, median_occupancy_rate,
|
||||
population
|
||||
FROM serving.city_market_overview
|
||||
WHERE city_slug = ? OR lower(city_name) = lower(?)
|
||||
LIMIT 1
|
||||
""",
|
||||
[city_slug, location.strip()],
|
||||
)
|
||||
if not rows:
|
||||
return None
|
||||
return rows[0]
|
||||
|
||||
|
||||
def get_plan_sections(state: dict, d: dict, language: str = "en", bp_details: dict | None = None, market_data: dict | None = None) -> dict:
|
||||
"""Extract and format all business plan sections from planner data."""
|
||||
s = state
|
||||
t = get_translations(language)
|
||||
@@ -63,6 +229,23 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
|
||||
per_court_str = fmt(d["capexPerCourt"])
|
||||
per_sqm_str = fmt(d["capexPerSqm"])
|
||||
|
||||
details = bp_details or {}
|
||||
sens_rows, price_rows = compute_sensitivity(s, d)
|
||||
|
||||
# Format sensitivity rows for display
|
||||
def fmt_sens(rows):
|
||||
return [{**r, "rev_fmt": fmt(r["rev"]), "ncf_fmt": fmt(r["ncf"]), "annual_fmt": fmt(r["annual"]), "dscr_fmt": f"{r['dscr']:.2f}x"} for r in rows]
|
||||
|
||||
def fmt_price(rows):
|
||||
return [{**r, "adj_rate_fmt": fmt(r["adj_rate"]), "rev_fmt": fmt(r["rev"]), "ncf_fmt": fmt(r["ncf"])} for r in rows]
|
||||
|
||||
opening_balance = _compute_opening_balance(d, fmt)
|
||||
use_of_funds = _compute_use_of_funds(d, fmt)
|
||||
|
||||
# Generate SVG charts from data
|
||||
pnl_chart_svg = _generate_pnl_chart_svg(d["annuals"], sym)
|
||||
cashflow_chart_svg = _generate_cashflow_chart_svg(d["months"], sym)
|
||||
|
||||
sections = {
|
||||
"lang": language,
|
||||
"title": t["bp_title"],
|
||||
@@ -170,6 +353,53 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
|
||||
],
|
||||
},
|
||||
|
||||
# Charts (SVG strings, embedded directly in HTML)
|
||||
"charts": {
|
||||
"pnl": pnl_chart_svg,
|
||||
"cashflow": cashflow_chart_svg,
|
||||
},
|
||||
|
||||
# Sensitivity analysis
|
||||
"sensitivity": {
|
||||
"heading": t["bp_sensitivity"],
|
||||
"util_heading": t["bp_sensitivity_util"],
|
||||
"price_heading": t["bp_sensitivity_price"],
|
||||
"sens_rows": fmt_sens(sens_rows),
|
||||
"price_rows": fmt_price(price_rows),
|
||||
},
|
||||
|
||||
# Opening balance sheet (Eröffnungsbilanz)
|
||||
"opening_balance": {
|
||||
"heading": t["bp_opening_balance"],
|
||||
**opening_balance,
|
||||
},
|
||||
|
||||
# Use of funds (Mittelverwendungsplan)
|
||||
"use_of_funds": {
|
||||
"heading": t["bp_use_of_funds"],
|
||||
"rows": use_of_funds,
|
||||
"total_loan": fmt(d["loanAmount"]),
|
||||
"total_equity": fmt(d["equity"]),
|
||||
"total_capex": fmt(d["capex"]),
|
||||
},
|
||||
|
||||
# Narrative sections from bp_details form
|
||||
"narrative": {
|
||||
"company_name": details.get("company_name", ""),
|
||||
"legal_entity": details.get("legal_entity", ""),
|
||||
"founder_name": details.get("founder_name", ""),
|
||||
"founder_background": details.get("founder_background", ""),
|
||||
"planned_location_address": details.get("planned_location_address", ""),
|
||||
"planned_opening_date": details.get("planned_opening_date", ""),
|
||||
"facility_description": details.get("facility_description", ""),
|
||||
"marketing_concept": details.get("marketing_concept", ""),
|
||||
"operations_concept": details.get("operations_concept", ""),
|
||||
"staffing_plan": details.get("staffing_plan", ""),
|
||||
},
|
||||
|
||||
# Market data (auto-populated from DuckDB, or None)
|
||||
"market_data": market_data,
|
||||
|
||||
# Template labels
|
||||
"labels": {
|
||||
"scenario": t["bp_lbl_scenario"],
|
||||
@@ -229,6 +459,38 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
|
||||
"cumulative": t["bp_lbl_cumulative"],
|
||||
"disclaimer": t["bp_lbl_disclaimer"],
|
||||
"currency_sym": sym,
|
||||
# New section headings
|
||||
"sensitivity": t["bp_sensitivity"],
|
||||
"sensitivity_util": t["bp_sensitivity_util"],
|
||||
"sensitivity_price": t["bp_sensitivity_price"],
|
||||
"opening_balance": t["bp_opening_balance"],
|
||||
"use_of_funds": t["bp_use_of_funds"],
|
||||
"market_analysis": t["bp_market_analysis"],
|
||||
"risk_analysis": t["bp_risk_analysis"],
|
||||
"venture_description": t["bp_venture_description"],
|
||||
"founder_profile": t["bp_founder_profile"],
|
||||
# New table labels
|
||||
"utilization": t["bp_lbl_utilization"],
|
||||
"annual_ncf": t["bp_lbl_annual_ncf"],
|
||||
"dscr": t["bp_lbl_dscr"],
|
||||
"price_delta": t["bp_lbl_price_delta"],
|
||||
"hourly_rate": t["bp_lbl_hourly_rate"],
|
||||
"monthly_ncf": t["bp_lbl_monthly_ncf"],
|
||||
"assets": t["bp_lbl_assets"],
|
||||
"fixed_assets": t["bp_lbl_fixed_assets"],
|
||||
"working_capital": t["bp_lbl_working_capital"],
|
||||
"liabilities_equity": t["bp_lbl_liabilities_equity"],
|
||||
"equity_ratio": t["bp_lbl_equity_ratio"],
|
||||
"funded_by_loan": t["bp_lbl_funded_by_loan"],
|
||||
"funded_by_equity": t["bp_lbl_funded_by_equity"],
|
||||
"total": t["bp_lbl_total"],
|
||||
"venues_per_100k": t["bp_lbl_venues_per_100k"],
|
||||
"market_score": t["bp_lbl_market_score"],
|
||||
"median_peak_rate": t["bp_lbl_median_peak_rate"],
|
||||
"median_offpeak_rate": t["bp_lbl_median_offpeak_rate"],
|
||||
"median_occupancy": t["bp_lbl_median_occupancy"],
|
||||
"confidential": t["bp_lbl_confidential"],
|
||||
"table_of_contents": t["bp_lbl_table_of_contents"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -246,9 +508,22 @@ async def generate_business_plan(scenario_id: int, user_id: int, language: str =
|
||||
|
||||
state = validate_state(json.loads(scenario["state_json"]))
|
||||
d = calc(state)
|
||||
sections = get_plan_sections(state, d, language)
|
||||
|
||||
# Load optional bp_details from scenario
|
||||
bp_details = None
|
||||
if scenario.get("bp_details_json"):
|
||||
try:
|
||||
bp_details = json.loads(scenario["bp_details_json"])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
bp_details = None
|
||||
|
||||
# Attempt to fetch market data from DuckDB
|
||||
location = scenario.get("location", "") or ""
|
||||
market_data = await _fetch_market_data(location, language)
|
||||
|
||||
sections = get_plan_sections(state, d, language, bp_details=bp_details, market_data=market_data)
|
||||
sections["scenario_name"] = scenario["name"]
|
||||
sections["location"] = scenario.get("location", "")
|
||||
sections["location"] = location
|
||||
|
||||
# Read HTML + CSS template
|
||||
html_template = (TEMPLATE_DIR / "plan.html").read_text()
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
|
||||
{% block head %}
|
||||
<meta name="description" content="{{ article.meta_description or '' }}">
|
||||
<meta property="og:title" content="{{ article.title }}">
|
||||
<meta property="og:description" content="{{ article.meta_description or '' }}">
|
||||
<meta property="og:type" content="article">
|
||||
{% if article.og_image_url %}
|
||||
<meta property="og:image" content="{{ article.og_image_url }}">
|
||||
{% endif %}
|
||||
{% if article.seo_head %}
|
||||
{{ article.seo_head | safe }}
|
||||
{% else %}
|
||||
<meta property="og:title" content="{{ article.title }}">
|
||||
<meta property="og:description" content="{{ article.meta_description or '' }}">
|
||||
<meta property="og:type" content="article">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
@@ -18,20 +21,15 @@
|
||||
"description": {{ (article.meta_description or '') | tojson }},
|
||||
{% if article.og_image_url %}"image": {{ article.og_image_url | tojson }},{% endif %}
|
||||
"datePublished": "{{ article.published_at[:10] if article.published_at else '' }}",
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "Padelnomics"
|
||||
},
|
||||
"author": {"@type": "Organization", "name": "Padelnomics"},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Padelnomics",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "{{ url_for('static', filename='images/logo.png', _external=True) }}"
|
||||
}
|
||||
"logo": {"@type": "ImageObject", "url": "{{ url_for('static', filename='images/logo.png', _external=True) }}"}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -1525,7 +1525,42 @@
|
||||
"bp_lbl_opex": "OPEX",
|
||||
"bp_lbl_debt": "Schulden",
|
||||
"bp_lbl_cumulative": "Kumulativ",
|
||||
"bp_lbl_disclaimer": "<strong>Haftungsausschluss:</strong> Dieser Businessplan wurde auf Basis benutzerdefinierter Annahmen mit dem Padelnomics-Finanzmodell erstellt. Alle Prognosen sind Schätzungen und stellen keine Finanzberatung dar. Die tatsächlichen Ergebnisse können je nach Marktbedingungen, Umsetzung und anderen Faktoren erheblich abweichen. Konsultiere Finanzberater, bevor du Investitionsentscheidungen triffst. © Padelnomics — padelnomics.io",
|
||||
|
||||
"bp_lbl_disclaimer": "<strong>Haftungsausschluss:</strong> Dieser Businessplan wurde auf Basis benutzerdefinierter Annahmen mit dem Padelnomics-Finanzmodell erstellt. Alle Prognosen sind Sch\u00e4tzungen und stellen keine Finanzberatung dar. Die tats\u00e4chlichen Ergebnisse k\u00f6nnen je nach Marktbedingungen, Umsetzung und anderen Faktoren erheblich abweichen. Konsultiere Finanzberater, bevor du Investitionsentscheidungen triffst. \u00a9 Padelnomics \u2014 padelnomics.io",
|
||||
|
||||
"bp_sensitivity": "Sensitivit\u00e4tsanalyse",
|
||||
"bp_sensitivity_util": "Auslastungssensitivit\u00e4t",
|
||||
"bp_sensitivity_price": "Preissensitivit\u00e4t",
|
||||
"bp_opening_balance": "Er\u00f6ffnungsbilanz",
|
||||
"bp_use_of_funds": "Mittelverwendungsplan",
|
||||
"bp_market_analysis": "Marktanalyse",
|
||||
"bp_risk_analysis": "Risikoanalyse",
|
||||
"bp_venture_description": "Vorhaben und Gesch\u00e4ftsmodell",
|
||||
"bp_founder_profile": "Gr\u00fcnder- und Managementprofil",
|
||||
|
||||
"bp_lbl_utilization": "Auslastung",
|
||||
"bp_lbl_annual_ncf": "J\u00e4hrl. Net CF",
|
||||
"bp_lbl_dscr": "KDDB",
|
||||
"bp_lbl_price_delta": "Preis\u00e4nderung",
|
||||
"bp_lbl_hourly_rate": "Stundensatz",
|
||||
"bp_lbl_monthly_ncf": "Monatl. Net CF",
|
||||
"bp_lbl_assets": "Aktiva",
|
||||
"bp_lbl_fixed_assets": "Anlagenverm\u00f6gen (Halle & Ausstattung)",
|
||||
"bp_lbl_working_capital": "Umlaufverm\u00f6gen & R\u00fccklagen",
|
||||
"bp_lbl_liabilities_equity": "Passiva",
|
||||
"bp_lbl_equity_ratio": "Eigenkapitalquote",
|
||||
"bp_lbl_funded_by_loan": "Fremdfinanziert",
|
||||
"bp_lbl_funded_by_equity": "Eigenfinanziert",
|
||||
"bp_lbl_total": "Gesamt",
|
||||
"bp_lbl_venues_per_100k": "Anlagen je 100.000 Einwohner",
|
||||
"bp_lbl_market_score": "Markt-Score",
|
||||
"bp_lbl_median_peak_rate": "Median Hauptzeit-Preis",
|
||||
"bp_lbl_median_offpeak_rate": "Median Nebenzeit-Preis",
|
||||
"bp_lbl_median_occupancy": "Median-Auslastung",
|
||||
"bp_lbl_confidential": "Vertraulich",
|
||||
"bp_lbl_table_of_contents": "Inhaltsverzeichnis",
|
||||
|
||||
|
||||
"email_magic_link_heading": "Bei {app_name} anmelden",
|
||||
"email_magic_link_body": "Hier ist dein Anmeldelink. Er läuft in {expiry_minutes} Minuten ab.",
|
||||
"email_magic_link_btn": "Anmelden →",
|
||||
|
||||
@@ -1525,7 +1525,42 @@
|
||||
"bp_lbl_opex": "OPEX",
|
||||
"bp_lbl_debt": "Debt",
|
||||
"bp_lbl_cumulative": "Cumulative",
|
||||
"bp_lbl_disclaimer": "<strong>Disclaimer:</strong> This business plan is generated from user-provided assumptions using the Padelnomics financial model. All projections are estimates and do not constitute financial advice. Actual results may vary significantly based on market conditions, execution, and other factors. Consult with financial advisors before making investment decisions. © Padelnomics — padelnomics.io",
|
||||
|
||||
"bp_lbl_disclaimer": "<strong>Disclaimer:</strong> This business plan is generated from user-provided assumptions using the Padelnomics financial model. All projections are estimates and do not constitute financial advice. Actual results may vary significantly based on market conditions, execution, and other factors. Consult with financial advisors before making investment decisions. \u00a9 Padelnomics \u2014 padelnomics.io",
|
||||
|
||||
"bp_sensitivity": "Sensitivity Analysis",
|
||||
"bp_sensitivity_util": "Utilization Sensitivity",
|
||||
"bp_sensitivity_price": "Price Sensitivity",
|
||||
"bp_opening_balance": "Opening Balance Sheet",
|
||||
"bp_use_of_funds": "Use of Funds",
|
||||
"bp_market_analysis": "Market Analysis",
|
||||
"bp_risk_analysis": "Risk Analysis",
|
||||
"bp_venture_description": "Business Description",
|
||||
"bp_founder_profile": "Founder / Management Profile",
|
||||
|
||||
"bp_lbl_utilization": "Utilization",
|
||||
"bp_lbl_annual_ncf": "Annual Net CF",
|
||||
"bp_lbl_dscr": "DSCR",
|
||||
"bp_lbl_price_delta": "Price Change",
|
||||
"bp_lbl_hourly_rate": "Hourly Rate",
|
||||
"bp_lbl_monthly_ncf": "Monthly Net CF",
|
||||
"bp_lbl_assets": "Assets",
|
||||
"bp_lbl_fixed_assets": "Fixed Assets (Facility & Equipment)",
|
||||
"bp_lbl_working_capital": "Working Capital & Reserves",
|
||||
"bp_lbl_liabilities_equity": "Liabilities & Equity",
|
||||
"bp_lbl_equity_ratio": "Equity Ratio",
|
||||
"bp_lbl_funded_by_loan": "Funded by Loan",
|
||||
"bp_lbl_funded_by_equity": "Funded by Equity",
|
||||
"bp_lbl_total": "Total",
|
||||
"bp_lbl_venues_per_100k": "Venues per 100K population",
|
||||
"bp_lbl_market_score": "Market Score",
|
||||
"bp_lbl_median_peak_rate": "Median Peak Rate",
|
||||
"bp_lbl_median_offpeak_rate": "Median Off-Peak Rate",
|
||||
"bp_lbl_median_occupancy": "Median Occupancy",
|
||||
"bp_lbl_confidential": "Confidential",
|
||||
"bp_lbl_table_of_contents": "Table of Contents",
|
||||
|
||||
|
||||
"email_magic_link_heading": "Sign in to {app_name}",
|
||||
"email_magic_link_body": "Here's your sign-in link. It expires in {expiry_minutes} minutes.",
|
||||
"email_magic_link_btn": "Sign In →",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Add bp_details_json column to scenarios table.
|
||||
|
||||
Stores business plan narrative details (founder profile, company name, legal
|
||||
entity, facility description, etc.) collected via the pre-export form.
|
||||
|
||||
Used by generate_business_plan() to produce KfW Gründerkredit-ready PDFs with
|
||||
all required narrative sections.
|
||||
"""
|
||||
|
||||
|
||||
def up(conn):
|
||||
conn.execute(
|
||||
"ALTER TABLE scenarios ADD COLUMN bp_details_json TEXT"
|
||||
)
|
||||
@@ -91,6 +91,78 @@ def form_to_state(form) -> dict:
|
||||
return data
|
||||
|
||||
|
||||
def compute_sensitivity(s: dict, d: dict) -> tuple[list[dict], list[dict]]:
|
||||
"""Compute utilization and price sensitivity tables from state + calc result.
|
||||
|
||||
Returns (sens_rows, price_rows) — pure computation, no side effects.
|
||||
Used by both the web planner (augment_d) and the PDF generator (get_plan_sections).
|
||||
"""
|
||||
is_in = s["venue"] == "indoor"
|
||||
w_rate = d["weightedRate"]
|
||||
rev_per_hr = (
|
||||
w_rate * (1 - s["bookingFee"] / 100)
|
||||
+ (s["racketRentalRate"] / 100) * s["racketQty"] * s["racketPrice"]
|
||||
+ (s["ballRate"] / 100) * (s["ballPrice"] - s["ballCost"])
|
||||
)
|
||||
utils = [15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70]
|
||||
ancillary_per_court = (
|
||||
s["membershipRevPerCourt"]
|
||||
+ s["fbRevPerCourt"]
|
||||
+ s["coachingRevPerCourt"]
|
||||
+ s["retailRevPerCourt"]
|
||||
)
|
||||
sens_rows = []
|
||||
for u in utils:
|
||||
booked = d["availHoursMonth"] * (u / 100)
|
||||
rev = booked * rev_per_hr + d["totalCourts"] * ancillary_per_court * (
|
||||
u / max(s["utilTarget"], 1)
|
||||
)
|
||||
ncf = rev - d["opex"] - d["monthlyPayment"]
|
||||
annual = ncf * (12 if is_in else 6)
|
||||
ebitda = rev - d["opex"]
|
||||
dscr = (
|
||||
(ebitda * (12 if is_in else 6)) / d["annualDebtService"]
|
||||
if d["annualDebtService"] > 0
|
||||
else 999
|
||||
)
|
||||
sens_rows.append(
|
||||
{
|
||||
"util": u,
|
||||
"rev": round(rev),
|
||||
"ncf": round(ncf),
|
||||
"annual": round(annual),
|
||||
"dscr": round(min(dscr, 99), 2),
|
||||
"is_target": u == s["utilTarget"],
|
||||
}
|
||||
)
|
||||
|
||||
prices = [-20, -10, -5, 0, 5, 10, 15, 20]
|
||||
price_rows = []
|
||||
for delta in prices:
|
||||
adj_rate = w_rate * (1 + delta / 100)
|
||||
booked = d["bookedHoursMonth"]
|
||||
rev = (
|
||||
booked * adj_rate * (1 - s["bookingFee"] / 100)
|
||||
+ booked
|
||||
* (
|
||||
(s["racketRentalRate"] / 100) * s["racketQty"] * s["racketPrice"]
|
||||
+ (s["ballRate"] / 100) * (s["ballPrice"] - s["ballCost"])
|
||||
)
|
||||
+ d["totalCourts"] * ancillary_per_court
|
||||
)
|
||||
ncf = rev - d["opex"] - d["monthlyPayment"]
|
||||
price_rows.append(
|
||||
{
|
||||
"delta": delta,
|
||||
"adj_rate": round(adj_rate),
|
||||
"rev": round(rev),
|
||||
"ncf": round(ncf),
|
||||
"is_base": delta == 0,
|
||||
}
|
||||
)
|
||||
return sens_rows, price_rows
|
||||
|
||||
|
||||
def augment_d(d: dict, s: dict, lang: str) -> None:
|
||||
"""Add display-only derived fields to calc result dict (mutates d in-place)."""
|
||||
t = get_translations(lang)
|
||||
@@ -351,70 +423,8 @@ def augment_d(d: dict, s: dict, lang: str) -> None:
|
||||
}
|
||||
|
||||
# Sensitivity tables (pre-computed for returns tab)
|
||||
is_in = s["venue"] == "indoor"
|
||||
w_rate = d["weightedRate"]
|
||||
rev_per_hr = (
|
||||
w_rate * (1 - s["bookingFee"] / 100)
|
||||
+ (s["racketRentalRate"] / 100) * s["racketQty"] * s["racketPrice"]
|
||||
+ (s["ballRate"] / 100) * (s["ballPrice"] - s["ballCost"])
|
||||
)
|
||||
utils = [15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70]
|
||||
ancillary_per_court = (
|
||||
s["membershipRevPerCourt"]
|
||||
+ s["fbRevPerCourt"]
|
||||
+ s["coachingRevPerCourt"]
|
||||
+ s["retailRevPerCourt"]
|
||||
)
|
||||
sens_rows = []
|
||||
for u in utils:
|
||||
booked = d["availHoursMonth"] * (u / 100)
|
||||
rev = booked * rev_per_hr + d["totalCourts"] * ancillary_per_court * (
|
||||
u / max(s["utilTarget"], 1)
|
||||
)
|
||||
ncf = rev - d["opex"] - d["monthlyPayment"]
|
||||
annual = ncf * (12 if is_in else 6)
|
||||
ebitda = rev - d["opex"]
|
||||
dscr = (
|
||||
(ebitda * (12 if is_in else 6)) / d["annualDebtService"]
|
||||
if d["annualDebtService"] > 0
|
||||
else 999
|
||||
)
|
||||
sens_rows.append(
|
||||
{
|
||||
"util": u,
|
||||
"rev": round(rev),
|
||||
"ncf": round(ncf),
|
||||
"annual": round(annual),
|
||||
"dscr": min(dscr, 99),
|
||||
"is_target": u == s["utilTarget"],
|
||||
}
|
||||
)
|
||||
sens_rows, price_rows = compute_sensitivity(s, d)
|
||||
d["sens_rows"] = sens_rows
|
||||
|
||||
prices = [-20, -10, -5, 0, 5, 10, 15, 20]
|
||||
price_rows = []
|
||||
for delta in prices:
|
||||
adj_rate = w_rate * (1 + delta / 100)
|
||||
booked = d["bookedHoursMonth"]
|
||||
rev = (
|
||||
booked * adj_rate * (1 - s["bookingFee"] / 100)
|
||||
+ booked
|
||||
* (
|
||||
(s["racketRentalRate"] / 100) * s["racketQty"] * s["racketPrice"]
|
||||
+ (s["ballRate"] / 100) * (s["ballPrice"] - s["ballCost"])
|
||||
)
|
||||
+ d["totalCourts"] * ancillary_per_court
|
||||
)
|
||||
ncf = rev - d["opex"] - d["monthlyPayment"]
|
||||
price_rows.append(
|
||||
{
|
||||
"delta": delta,
|
||||
"adj_rate": round(adj_rate),
|
||||
"rev": round(rev),
|
||||
"ncf": round(ncf),
|
||||
"is_base": delta == 0,
|
||||
}
|
||||
)
|
||||
d["price_rows"] = price_rows
|
||||
|
||||
|
||||
@@ -617,6 +627,63 @@ async def export():
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/export/details", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@csrf_protect
|
||||
async def export_details():
|
||||
"""Business plan details form — collect narrative fields before PDF export.
|
||||
|
||||
GET: shows the form pre-populated from the scenario's bp_details_json.
|
||||
POST: saves bp_details_json to the scenario, redirects to /export/checkout.
|
||||
"""
|
||||
from quart import redirect, url_for
|
||||
|
||||
form = await request.form
|
||||
scenario_id = request.args.get("scenario_id") or form.get("scenario_id")
|
||||
language = request.args.get("language", "en") or form.get("language", "en")
|
||||
|
||||
if not scenario_id:
|
||||
return redirect(url_for("planner.export"))
|
||||
|
||||
scenario = await fetch_one(
|
||||
"SELECT id, name, bp_details_json FROM scenarios WHERE id = ? AND user_id = ? AND deleted_at IS NULL",
|
||||
(int(scenario_id), g.user["id"]),
|
||||
)
|
||||
if not scenario:
|
||||
return jsonify({"error": "Scenario not found."}), 404
|
||||
|
||||
if request.method == "POST":
|
||||
_FIELDS = [
|
||||
"company_name", "legal_entity", "founder_name", "founder_background",
|
||||
"planned_location_address", "planned_opening_date", "facility_description",
|
||||
"marketing_concept", "operations_concept", "staffing_plan",
|
||||
]
|
||||
details = {f: form.get(f, "").strip() for f in _FIELDS}
|
||||
await execute(
|
||||
"UPDATE scenarios SET bp_details_json = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?",
|
||||
(json.dumps(details), int(scenario_id), g.user["id"]),
|
||||
)
|
||||
return redirect(
|
||||
url_for("planner.export") + f"?scenario_id={scenario_id}&language={language}"
|
||||
)
|
||||
|
||||
# GET — pre-populate from existing details
|
||||
existing = {}
|
||||
if scenario.get("bp_details_json"):
|
||||
try:
|
||||
existing = json.loads(scenario["bp_details_json"])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
existing = {}
|
||||
|
||||
return await render_template(
|
||||
"export_details.html",
|
||||
scenario=scenario,
|
||||
scenario_id=scenario_id,
|
||||
language=language,
|
||||
details=existing,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/export/checkout", methods=["POST"])
|
||||
@login_required
|
||||
@csrf_protect
|
||||
|
||||
171
web/src/padelnomics/planner/templates/export_details.html
Normal file
171
web/src/padelnomics/planner/templates/export_details.html
Normal file
@@ -0,0 +1,171 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Business Plan Details — {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.bp-wrap { max-width: 680px; margin: 0 auto; padding: 3rem 0; }
|
||||
.bp-hero { margin-bottom: 2rem; }
|
||||
.bp-hero h1 { font-size: 1.5rem; margin-bottom: 0.375rem; }
|
||||
.bp-hero p { color: #64748B; font-size: 0.9rem; line-height: 1.5; }
|
||||
.bp-card { background: white; border: 1px solid #E2E8F0; border-radius: 14px; padding: 1.75rem; margin-bottom: 1.5rem; }
|
||||
.bp-card h2 { font-size: 1rem; font-weight: 700; color: #1E293B; margin: 0 0 1.25rem; padding-bottom: 0.75rem; border-bottom: 1px solid #F1F5F9; }
|
||||
.bp-card h2 span { font-size: 0.75rem; font-weight: 500; color: #94A3B8; margin-left: 8px; }
|
||||
.bp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||
.bp-grid--1 { grid-template-columns: 1fr; }
|
||||
.bp-field label { display: block; font-size: 0.8rem; font-weight: 600; color: #475569; margin-bottom: 4px; }
|
||||
.bp-field label .opt { font-weight: 400; color: #94A3B8; font-size: 0.75rem; }
|
||||
.bp-field input, .bp-field select, .bp-field textarea {
|
||||
width: 100%; box-sizing: border-box; border: 1px solid #CBD5E1; border-radius: 8px;
|
||||
padding: 0.5rem 0.75rem; font-size: 0.875rem; color: #1E293B;
|
||||
background: #FAFAFA; outline: none; font-family: inherit;
|
||||
}
|
||||
.bp-field input:focus, .bp-field select:focus, .bp-field textarea:focus {
|
||||
border-color: #1D4ED8; background: white; box-shadow: 0 0 0 3px rgba(29,78,216,0.1);
|
||||
}
|
||||
.bp-field textarea { resize: vertical; min-height: 80px; line-height: 1.5; }
|
||||
.bp-hint { font-size: 0.75rem; color: #94A3B8; margin-top: 4px; }
|
||||
.bp-skip { font-size: 0.8rem; color: #64748B; text-align: center; margin-top: 0.5rem; }
|
||||
.bp-skip a { color: #1D4ED8; text-decoration: none; }
|
||||
.bp-skip a:hover { text-decoration: underline; }
|
||||
.bp-note { background: #EFF6FF; border: 1px solid #BFDBFE; border-radius: 8px; padding: 0.875rem 1rem; font-size: 0.8125rem; color: #1E3A8A; margin-bottom: 1.5rem; }
|
||||
.bp-note strong { display: block; margin-bottom: 2px; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container-page">
|
||||
<div class="bp-wrap">
|
||||
|
||||
<div class="bp-hero">
|
||||
<h1>Business Plan Details</h1>
|
||||
<p>
|
||||
These details make your PDF ready for bank meetings and KfW applications.
|
||||
All fields are optional — skip any section and the PDF uses auto-generated placeholders.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bp-note">
|
||||
<strong>Scenario: {{ scenario.name }}</strong>
|
||||
Narrative details are saved to this scenario. You can update them anytime before generating a new PDF.
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/planner/export/details">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="scenario_id" value="{{ scenario_id }}">
|
||||
<input type="hidden" name="language" value="{{ language }}">
|
||||
|
||||
<!-- Company & Legal -->
|
||||
<div class="bp-card">
|
||||
<h2>Company & Legal Entity <span>Cover page + throughout</span></h2>
|
||||
<div class="bp-grid">
|
||||
<div class="bp-field">
|
||||
<label for="company_name">Company Name <span class="opt">(optional)</span></label>
|
||||
<input type="text" id="company_name" name="company_name"
|
||||
value="{{ details.get('company_name', '') }}"
|
||||
placeholder="e.g. Padel Cologne GmbH">
|
||||
</div>
|
||||
<div class="bp-field">
|
||||
<label for="legal_entity">Legal Entity <span class="opt">(optional)</span></label>
|
||||
<select id="legal_entity" name="legal_entity">
|
||||
{% set le = details.get('legal_entity', '') %}
|
||||
<option value="" {% if not le %}selected{% endif %}>— Select —</option>
|
||||
<option value="GmbH" {% if le == 'GmbH' %}selected{% endif %}>GmbH</option>
|
||||
<option value="UG (haftungsbeschränkt)" {% if le == 'UG (haftungsbeschränkt)' %}selected{% endif %}>UG (haftungsbeschränkt)</option>
|
||||
<option value="GmbH & Co. KG" {% if le == 'GmbH & Co. KG' %}selected{% endif %}>GmbH & Co. KG</option>
|
||||
<option value="GbR" {% if le == 'GbR' %}selected{% endif %}>GbR</option>
|
||||
<option value="Einzelunternehmen" {% if le == 'Einzelunternehmen' %}selected{% endif %}>Einzelunternehmen</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-grid">
|
||||
<div class="bp-field">
|
||||
<label for="planned_location_address">Planned Location / Address <span class="opt">(optional)</span></label>
|
||||
<input type="text" id="planned_location_address" name="planned_location_address"
|
||||
value="{{ details.get('planned_location_address', '') }}"
|
||||
placeholder="e.g. Gewerbepark Nord, Cologne">
|
||||
</div>
|
||||
<div class="bp-field">
|
||||
<label for="planned_opening_date">Planned Opening Date <span class="opt">(optional)</span></label>
|
||||
<input type="text" id="planned_opening_date" name="planned_opening_date"
|
||||
value="{{ details.get('planned_opening_date', '') }}"
|
||||
placeholder="e.g. Q3 2026">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Founder Profile -->
|
||||
<div class="bp-card">
|
||||
<h2>Founder / Management Profile <span>Gründerprofil — required by KfW</span></h2>
|
||||
<div class="bp-grid">
|
||||
<div class="bp-field">
|
||||
<label for="founder_name">Founder Name(s) <span class="opt">(optional)</span></label>
|
||||
<input type="text" id="founder_name" name="founder_name"
|
||||
value="{{ details.get('founder_name', '') }}"
|
||||
placeholder="e.g. Max Mustermann">
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-grid bp-grid--1">
|
||||
<div class="bp-field">
|
||||
<label for="founder_background">Background & Qualifications <span class="opt">(optional)</span></label>
|
||||
<textarea id="founder_background" name="founder_background"
|
||||
placeholder="Briefly describe relevant experience: sports management, hospitality, business background, relevant networks...">{{ details.get('founder_background', '') }}</textarea>
|
||||
<p class="bp-hint">2–4 sentences. Banks want to see relevant experience or a credible team composition.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Business Description -->
|
||||
<div class="bp-card">
|
||||
<h2>Business Description <span>Vorhabensbeschreibung</span></h2>
|
||||
<div class="bp-grid bp-grid--1">
|
||||
<div class="bp-field">
|
||||
<label for="facility_description">Facility Concept <span class="opt">(optional)</span></label>
|
||||
<textarea id="facility_description" name="facility_description"
|
||||
placeholder="Describe the concept: indoor/outdoor, number of courts, target customer segments, service level, opening hours, unique positioning...">{{ details.get('facility_description', '') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Operations -->
|
||||
<div class="bp-card">
|
||||
<h2>Operations <span>Betriebskonzept + Personalplanung</span></h2>
|
||||
<div class="bp-grid bp-grid--1">
|
||||
<div class="bp-field">
|
||||
<label for="operations_concept">Operations Concept <span class="opt">(optional)</span></label>
|
||||
<textarea id="operations_concept" name="operations_concept"
|
||||
placeholder="How will the facility be run day-to-day? Opening hours, booking system, court management, maintenance approach...">{{ details.get('operations_concept', '') }}</textarea>
|
||||
</div>
|
||||
<div class="bp-field">
|
||||
<label for="staffing_plan">Staffing Plan <span class="opt">(optional)</span></label>
|
||||
<textarea id="staffing_plan" name="staffing_plan"
|
||||
placeholder="Planned headcount, roles, hiring timeline. e.g. 1 facility manager, 2 part-time reception, 1 coach...">{{ details.get('staffing_plan', '') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Marketing -->
|
||||
<div class="bp-card">
|
||||
<h2>Marketing Concept <span>Marketingkonzept</span></h2>
|
||||
<div class="bp-grid bp-grid--1">
|
||||
<div class="bp-field">
|
||||
<label for="marketing_concept">Marketing Approach <span class="opt">(optional)</span></label>
|
||||
<textarea id="marketing_concept" name="marketing_concept"
|
||||
placeholder="How will you attract and retain customers? Launch strategy, digital channels, club partnerships, corporate clients, loyalty programs...">{{ details.get('marketing_concept', '') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<button type="submit" class="btn-primary" style="width: 100%; padding: 0.875rem; font-size: 1rem; border-radius: 10px;">
|
||||
Save & Continue to Export →
|
||||
</button>
|
||||
<p class="bp-skip">
|
||||
<a href="/planner/export?scenario_id={{ scenario_id }}&language={{ language }}">Skip — generate PDF with auto-filled sections</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@@ -1,105 +1,689 @@
|
||||
/* ============================================================
|
||||
Padelnomics Business Plan PDF — Precision Finance Aesthetic
|
||||
Rendered by WeasyPrint (A4, CSS3, no JavaScript)
|
||||
============================================================ */
|
||||
|
||||
/* ---------- Design tokens ---------- */
|
||||
:root {
|
||||
--navy: #0F2651;
|
||||
--navy-deep: #091a3a;
|
||||
--gold: #C9922C;
|
||||
--gold-lt: #EDD48A;
|
||||
--text: #1C2333;
|
||||
--muted: #5E6E8A;
|
||||
--border: #DDE3EE;
|
||||
--bg-light: #F6F8FC;
|
||||
--white: #FFFFFF;
|
||||
--green: #166534;
|
||||
--green-bg: #DCFCE7;
|
||||
--red: #991B1B;
|
||||
--red-bg: #FEE2E2;
|
||||
}
|
||||
|
||||
/* ---------- Page setup ---------- */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm 18mm;
|
||||
margin: 22mm 20mm 22mm 20mm;
|
||||
|
||||
@top-left {
|
||||
content: string(doc-company);
|
||||
font-family: 'Gill Sans', 'Trebuchet MS', Calibri, sans-serif;
|
||||
font-size: 7.5pt;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
@top-right {
|
||||
content: string(doc-confidential);
|
||||
font-family: 'Gill Sans', 'Trebuchet MS', Calibri, sans-serif;
|
||||
font-size: 7.5pt;
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
@bottom-center {
|
||||
content: "Page " counter(page) " of " counter(pages);
|
||||
content: counter(page);
|
||||
font-family: 'Gill Sans', 'Trebuchet MS', Calibri, sans-serif;
|
||||
font-size: 8pt;
|
||||
color: #94A3B8;
|
||||
color: var(--muted);
|
||||
}
|
||||
@bottom-right {
|
||||
content: "padelnomics.io";
|
||||
font-family: 'Gill Sans', 'Trebuchet MS', Calibri, sans-serif;
|
||||
font-size: 7pt;
|
||||
color: var(--border);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 10pt;
|
||||
line-height: 1.5;
|
||||
color: #1E293B;
|
||||
/* Cover page: no running headers/footers, no margins */
|
||||
@page cover {
|
||||
size: A4;
|
||||
margin: 0;
|
||||
@top-left { content: none; }
|
||||
@top-right { content: none; }
|
||||
@bottom-center { content: none; }
|
||||
@bottom-right { content: none; }
|
||||
}
|
||||
|
||||
/* TOC page: suppress page counter display */
|
||||
@page toc {
|
||||
@bottom-center { content: none; }
|
||||
}
|
||||
|
||||
/* ---------- Named strings for headers ---------- */
|
||||
.doc-company-anchor { string-set: doc-company content(); }
|
||||
.doc-confidential-anchor { string-set: doc-confidential content(); }
|
||||
|
||||
/* ---------- Base typography ---------- */
|
||||
body {
|
||||
font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
|
||||
font-size: 9.5pt;
|
||||
line-height: 1.55;
|
||||
color: var(--text);
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
h1 { font-size: 22pt; font-weight: 800; color: #0F172A; margin: 0 0 4pt; }
|
||||
h2 { font-size: 14pt; font-weight: 700; color: #0F172A; margin: 24pt 0 8pt; page-break-after: avoid; }
|
||||
h3 { font-size: 10pt; font-weight: 700; color: #64748B; text-transform: uppercase; letter-spacing: 0.06em; margin: 16pt 0 6pt; }
|
||||
p { margin: 0 0 6pt; }
|
||||
|
||||
.subtitle { font-size: 12pt; color: #64748B; margin-bottom: 20pt; }
|
||||
.meta { font-size: 8pt; color: #94A3B8; margin-bottom: 6pt; }
|
||||
|
||||
/* Summary grid */
|
||||
.summary-grid {
|
||||
/* ---------- Cover page ---------- */
|
||||
.cover {
|
||||
page: cover;
|
||||
page-break-after: always;
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8pt;
|
||||
margin: 10pt 0 16pt;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
.summary-card {
|
||||
flex: 1 1 140pt;
|
||||
border: 0.5pt solid #E2E8F0;
|
||||
border-radius: 6pt;
|
||||
padding: 8pt 10pt;
|
||||
background: #F8FAFC;
|
||||
}
|
||||
.summary-card .label { font-size: 7pt; color: #94A3B8; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
.summary-card .value { font-size: 14pt; font-weight: 800; color: #1E293B; }
|
||||
.summary-card .value--blue { color: #1D4ED8; }
|
||||
|
||||
/* Tables */
|
||||
.cover__sidebar {
|
||||
width: 68mm;
|
||||
background: var(--navy);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 18mm 10mm 14mm 14mm;
|
||||
}
|
||||
|
||||
.cover__logo {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 17pt;
|
||||
font-weight: 700;
|
||||
color: var(--white);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.cover__logo-dot {
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.cover__tagline {
|
||||
font-size: 7.5pt;
|
||||
color: rgba(255,255,255,0.5);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
margin-top: 5pt;
|
||||
}
|
||||
|
||||
.cover__sidebar-footer {
|
||||
font-size: 7pt;
|
||||
color: rgba(255,255,255,0.4);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.cover__sidebar-footer a {
|
||||
color: var(--gold);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cover__main {
|
||||
flex: 1;
|
||||
padding: 18mm 14mm 14mm 14mm;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.cover__type-label {
|
||||
font-size: 7.5pt;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
color: var(--gold);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
.cover__title {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 28pt;
|
||||
font-weight: 700;
|
||||
color: var(--navy);
|
||||
line-height: 1.2;
|
||||
margin: 0 0 16pt;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.cover__subtitle {
|
||||
font-size: 11pt;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4pt;
|
||||
}
|
||||
|
||||
.cover__scenario {
|
||||
font-size: 9.5pt;
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
.cover__divider {
|
||||
width: 40pt;
|
||||
height: 2.5pt;
|
||||
background: var(--gold);
|
||||
margin: 14pt 0;
|
||||
}
|
||||
|
||||
.cover__meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt 14pt;
|
||||
}
|
||||
|
||||
.cover__meta-item {}
|
||||
.cover__meta-label {
|
||||
font-size: 7pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
.cover__meta-value {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.cover__footer-strip {
|
||||
border-top: 1pt solid var(--border);
|
||||
padding-top: 10pt;
|
||||
font-size: 7.5pt;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* ---------- Table of Contents ---------- */
|
||||
.toc-page {
|
||||
page: toc;
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.toc-heading {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 16pt;
|
||||
font-weight: 700;
|
||||
color: var(--navy);
|
||||
margin: 0 0 20pt;
|
||||
padding-bottom: 10pt;
|
||||
border-bottom: 2pt solid var(--navy);
|
||||
}
|
||||
|
||||
.toc-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.toc-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0;
|
||||
padding: 5pt 0;
|
||||
border-bottom: 0.5pt dotted var(--border);
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
.toc-item--heading {
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
padding-top: 10pt;
|
||||
border-bottom: none;
|
||||
font-size: 9.5pt;
|
||||
color: var(--navy);
|
||||
padding-bottom: 3pt;
|
||||
}
|
||||
|
||||
.toc-num {
|
||||
font-size: 8pt;
|
||||
color: var(--gold);
|
||||
font-weight: 700;
|
||||
width: 22pt;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toc-text {
|
||||
flex: 1;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.toc-dots {
|
||||
flex: 1;
|
||||
border-bottom: 1pt dotted var(--border);
|
||||
margin: 0 6pt 2pt;
|
||||
min-width: 20pt;
|
||||
}
|
||||
|
||||
.toc-section-badge {
|
||||
font-size: 7pt;
|
||||
background: var(--bg-light);
|
||||
color: var(--muted);
|
||||
padding: 1pt 6pt;
|
||||
border-radius: 3pt;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ---------- Section headings ---------- */
|
||||
h1 {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 20pt;
|
||||
font-weight: 700;
|
||||
color: var(--navy);
|
||||
margin: 0 0 6pt;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 13.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--navy);
|
||||
margin: 0 0 10pt;
|
||||
padding: 8pt 0 8pt 12pt;
|
||||
border-left: 3.5pt solid var(--gold);
|
||||
page-break-after: avoid;
|
||||
break-after: avoid;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 8.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin: 14pt 0 5pt;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 22pt;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.section--break {
|
||||
page-break-before: always;
|
||||
break-before: page;
|
||||
margin-top: 0;
|
||||
padding-top: 6pt;
|
||||
}
|
||||
|
||||
/* ---------- Executive summary cards ---------- */
|
||||
.exec-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8pt;
|
||||
margin: 10pt 0 14pt;
|
||||
}
|
||||
|
||||
.exec-card {
|
||||
border: 1pt solid var(--border);
|
||||
border-top: 3pt solid var(--navy);
|
||||
border-radius: 0 0 5pt 5pt;
|
||||
padding: 10pt 10pt 8pt;
|
||||
background: var(--bg-light);
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.exec-card--accent {
|
||||
border-top-color: var(--gold);
|
||||
}
|
||||
|
||||
.exec-label {
|
||||
font-size: 7pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4pt;
|
||||
}
|
||||
|
||||
.exec-value {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 16pt;
|
||||
font-weight: 700;
|
||||
color: var(--navy);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.exec-value--gold { color: var(--gold); }
|
||||
|
||||
.exec-paragraph {
|
||||
font-size: 9pt;
|
||||
line-height: 1.6;
|
||||
color: var(--text);
|
||||
background: var(--bg-light);
|
||||
border-left: 3pt solid var(--border);
|
||||
padding: 8pt 10pt;
|
||||
margin: 0 0 6pt;
|
||||
}
|
||||
|
||||
/* ---------- Narrative text blocks ---------- */
|
||||
.narrative-block {
|
||||
font-size: 9pt;
|
||||
line-height: 1.65;
|
||||
color: var(--text);
|
||||
margin-bottom: 8pt;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.placeholder-block {
|
||||
font-size: 8.5pt;
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
background: var(--bg-light);
|
||||
border: 1pt dashed var(--border);
|
||||
border-radius: 4pt;
|
||||
padding: 8pt 10pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
/* ---------- Tables ---------- */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 6pt 0 16pt;
|
||||
font-size: 9pt;
|
||||
margin: 6pt 0 12pt;
|
||||
font-size: 8.5pt;
|
||||
page-break-inside: auto;
|
||||
}
|
||||
th {
|
||||
background: #F1F5F9;
|
||||
|
||||
thead th {
|
||||
background: var(--navy);
|
||||
color: var(--white);
|
||||
font-size: 7pt;
|
||||
font-weight: 700;
|
||||
color: #64748B;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 6pt 8pt;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 5.5pt 8pt;
|
||||
text-align: left;
|
||||
border-bottom: 1pt solid #E2E8F0;
|
||||
border: none;
|
||||
}
|
||||
td {
|
||||
|
||||
thead th:first-child { border-radius: 0; }
|
||||
|
||||
tbody tr:nth-child(even) { background: var(--bg-light); }
|
||||
tbody tr:nth-child(odd) { background: var(--white); }
|
||||
|
||||
tbody td {
|
||||
padding: 5pt 8pt;
|
||||
border-bottom: 0.5pt solid #F1F5F9;
|
||||
border-bottom: 0.5pt solid var(--border);
|
||||
color: var(--text);
|
||||
vertical-align: top;
|
||||
}
|
||||
tr:last-child td { border-bottom: none; }
|
||||
.total-row td { font-weight: 700; border-top: 1.5pt solid #E2E8F0; background: #F8FAFC; }
|
||||
|
||||
/* Metrics grid */
|
||||
tbody tr:last-child td { border-bottom: none; }
|
||||
|
||||
.total-row td {
|
||||
font-weight: 700;
|
||||
color: var(--navy);
|
||||
background: #E8EDF5 !important;
|
||||
border-top: 1.5pt solid var(--navy);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.highlight-row td {
|
||||
background: #FEF9EE !important;
|
||||
font-weight: 700;
|
||||
color: var(--navy);
|
||||
border-top: 1pt solid var(--gold);
|
||||
border-bottom: 1pt solid var(--gold);
|
||||
}
|
||||
|
||||
.positive-cell { color: var(--green); font-weight: 600; }
|
||||
.negative-cell { color: var(--red); font-weight: 600; }
|
||||
|
||||
td.r, th.r { text-align: right; }
|
||||
td.c, th.c { text-align: center; }
|
||||
|
||||
.note-cell {
|
||||
font-size: 7.5pt;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ---------- Key metrics grid ---------- */
|
||||
.metrics-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 6pt;
|
||||
margin: 8pt 0;
|
||||
margin: 8pt 0 12pt;
|
||||
}
|
||||
|
||||
.metric-box {
|
||||
border: 1pt solid var(--border);
|
||||
border-radius: 5pt;
|
||||
padding: 8pt 8pt 6pt;
|
||||
text-align: center;
|
||||
background: var(--white);
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 6.5pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4pt;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 14pt;
|
||||
font-weight: 700;
|
||||
color: var(--navy);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* ---------- Financing bar ---------- */
|
||||
.fin-wrap {
|
||||
margin: 8pt 0 12pt;
|
||||
}
|
||||
.metric-box {
|
||||
flex: 1 1 90pt;
|
||||
border: 0.5pt solid #E2E8F0;
|
||||
border-radius: 4pt;
|
||||
padding: 6pt 8pt;
|
||||
text-align: center;
|
||||
}
|
||||
.metric-box .label { font-size: 7pt; color: #94A3B8; }
|
||||
.metric-box .value { font-size: 12pt; font-weight: 700; color: #1E293B; }
|
||||
|
||||
/* Financing structure */
|
||||
.fin-bar {
|
||||
height: 12pt;
|
||||
border-radius: 6pt;
|
||||
height: 14pt;
|
||||
border-radius: 4pt;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
margin: 6pt 0 10pt;
|
||||
margin-bottom: 6pt;
|
||||
}
|
||||
.fin-bar__equity { background: #1D4ED8; }
|
||||
.fin-bar__loan { background: #93C5FD; }
|
||||
|
||||
/* Footer */
|
||||
.fin-bar__equity {
|
||||
background: var(--navy);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 6pt;
|
||||
}
|
||||
|
||||
.fin-bar__loan {
|
||||
background: var(--gold);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 6pt;
|
||||
}
|
||||
|
||||
.fin-bar__label {
|
||||
font-size: 6.5pt;
|
||||
font-weight: 700;
|
||||
color: rgba(255,255,255,0.9);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fin-legend {
|
||||
display: flex;
|
||||
gap: 14pt;
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
.fin-legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5pt;
|
||||
}
|
||||
|
||||
.fin-legend-dot {
|
||||
width: 8pt;
|
||||
height: 8pt;
|
||||
border-radius: 2pt;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ---------- Balance sheet 2-column ---------- */
|
||||
.balance-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin: 6pt 0 12pt;
|
||||
}
|
||||
|
||||
.balance-side {
|
||||
border: 1pt solid var(--border);
|
||||
border-radius: 5pt;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.balance-side__header {
|
||||
background: var(--navy);
|
||||
color: var(--white);
|
||||
font-size: 7pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
padding: 5pt 8pt;
|
||||
}
|
||||
|
||||
.balance-side table {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.balance-side tbody td {
|
||||
border-bottom: 0.5pt solid var(--border);
|
||||
}
|
||||
|
||||
.balance-total td {
|
||||
font-weight: 700;
|
||||
color: var(--navy);
|
||||
background: #E8EDF5 !important;
|
||||
border-top: 1.5pt solid var(--navy) !important;
|
||||
}
|
||||
|
||||
/* ---------- Charts ---------- */
|
||||
.chart-wrap {
|
||||
margin: 8pt 0 14pt;
|
||||
text-align: center;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.chart-wrap svg {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.chart-caption {
|
||||
font-size: 7.5pt;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
margin-top: 3pt;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ---------- Market data callout ---------- */
|
||||
.market-callout {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8pt;
|
||||
margin: 8pt 0 12pt;
|
||||
}
|
||||
|
||||
.market-stat {
|
||||
background: var(--bg-light);
|
||||
border: 1pt solid var(--border);
|
||||
border-radius: 5pt;
|
||||
padding: 8pt 10pt;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.market-stat-value {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 14pt;
|
||||
font-weight: 700;
|
||||
color: var(--navy);
|
||||
}
|
||||
|
||||
.market-stat-label {
|
||||
font-size: 7pt;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin-top: 2pt;
|
||||
}
|
||||
|
||||
/* ---------- Risk register ---------- */
|
||||
.risk-item {
|
||||
display: flex;
|
||||
gap: 8pt;
|
||||
padding: 6pt 0;
|
||||
border-bottom: 0.5pt solid var(--border);
|
||||
font-size: 8.5pt;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.risk-badge {
|
||||
font-size: 6.5pt;
|
||||
font-weight: 700;
|
||||
padding: 2pt 6pt;
|
||||
border-radius: 3pt;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
align-self: flex-start;
|
||||
margin-top: 1pt;
|
||||
}
|
||||
|
||||
.risk-badge--high { background: #FEE2E2; color: #991B1B; }
|
||||
.risk-badge--medium { background: #FEF3C7; color: #92400E; }
|
||||
.risk-badge--low { background: var(--green-bg); color: var(--green); }
|
||||
|
||||
.risk-text strong {
|
||||
display: block;
|
||||
color: var(--navy);
|
||||
font-size: 8.5pt;
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
/* ---------- Two-column layout helper ---------- */
|
||||
.two-col {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12pt;
|
||||
}
|
||||
|
||||
/* ---------- Disclaimer ---------- */
|
||||
.disclaimer {
|
||||
font-size: 7pt;
|
||||
color: #94A3B8;
|
||||
margin-top: 30pt;
|
||||
padding-top: 8pt;
|
||||
border-top: 0.5pt solid #E2E8F0;
|
||||
line-height: 1.4;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
padding-top: 10pt;
|
||||
border-top: 0.5pt solid var(--border);
|
||||
margin-top: 20pt;
|
||||
}
|
||||
|
||||
/* ---------- Stat row (financing detail) ---------- */
|
||||
.stat-table tbody tr:nth-child(even) { background: var(--bg-light); }
|
||||
.stat-table tbody tr:nth-child(odd) { background: var(--white); }
|
||||
|
||||
@@ -6,201 +6,668 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Cover -->
|
||||
<h1>{{ s.title }}</h1>
|
||||
<div class="subtitle">{{ s.subtitle }}</div>
|
||||
{% if s.scenario_name %}
|
||||
<p class="meta">{{ s.labels.scenario }}: {{ s.scenario_name }}{% if s.location %} — {{ s.location }}{% endif %}</p>
|
||||
{% endif %}
|
||||
<p class="meta">{{ s.courts }}</p>
|
||||
<p class="meta">{{ s.labels.generated_by }}</p>
|
||||
{# Named strings for running headers #}
|
||||
<span class="doc-company-anchor">{% if s.narrative.company_name %}{{ s.narrative.company_name }}{% else %}Padelnomics{% endif %}</span>
|
||||
<span class="doc-confidential-anchor">{{ s.labels.confidential }}</span>
|
||||
|
||||
<!-- Executive Summary -->
|
||||
<h2>{{ s.executive_summary.heading }}</h2>
|
||||
<div class="summary-grid">
|
||||
<div class="summary-card">
|
||||
<div class="label">{{ s.labels.total_investment }}</div>
|
||||
<div class="value">{{ s.executive_summary.total_capex }}</div>
|
||||
{# ============================================================ #}
|
||||
{# COVER PAGE #}
|
||||
{# ============================================================ #}
|
||||
<div class="cover">
|
||||
<div class="cover__sidebar">
|
||||
<div>
|
||||
<div class="cover__logo">Padel<span class="cover__logo-dot">.</span></div>
|
||||
<div class="cover__tagline">Business Plan</div>
|
||||
</div>
|
||||
<div class="cover__sidebar-footer">
|
||||
Generated by Padelnomics<br>
|
||||
<a href="https://padelnomics.io">padelnomics.io</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="label">{{ s.labels.equity_required }}</div>
|
||||
<div class="value">{{ s.executive_summary.equity }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="label">{{ s.labels.year3_ebitda }}</div>
|
||||
<div class="value">{{ s.executive_summary.y3_ebitda }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="label">{{ s.labels.irr }}</div>
|
||||
<div class="value value--blue">{{ s.executive_summary.irr }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="label">{{ s.labels.payback_period }}</div>
|
||||
<div class="value">{{ s.executive_summary.payback }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="label">{{ s.labels.year1_revenue }}</div>
|
||||
<div class="value">{{ s.executive_summary.y1_revenue }}</div>
|
||||
<div class="cover__main">
|
||||
<div>
|
||||
<div class="cover__type-label">{{ s.labels.confidential }} — Business Plan</div>
|
||||
<div class="cover__title">
|
||||
{% if s.narrative.company_name %}{{ s.narrative.company_name }}{% else %}{{ s.scenario_name or s.title }}{% endif %}
|
||||
</div>
|
||||
<div class="cover__subtitle">{{ s.subtitle }}</div>
|
||||
{% if s.location %}<div class="cover__subtitle">{{ s.location }}</div>{% endif %}
|
||||
<div class="cover__divider"></div>
|
||||
<div class="cover__meta">
|
||||
<div class="cover__meta-item">
|
||||
<div class="cover__meta-label">{{ s.labels.scenario }}</div>
|
||||
<div class="cover__meta-value">{{ s.scenario_name or "—" }}</div>
|
||||
</div>
|
||||
{% if s.narrative.legal_entity %}
|
||||
<div class="cover__meta-item">
|
||||
<div class="cover__meta-label">Rechtsform</div>
|
||||
<div class="cover__meta-value">{{ s.narrative.legal_entity }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if s.narrative.founder_name %}
|
||||
<div class="cover__meta-item">
|
||||
<div class="cover__meta-label">{{ s.labels.founder_profile }}</div>
|
||||
<div class="cover__meta-value">{{ s.narrative.founder_name }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if s.narrative.planned_opening_date %}
|
||||
<div class="cover__meta-item">
|
||||
<div class="cover__meta-label">Planned Opening</div>
|
||||
<div class="cover__meta-value">{{ s.narrative.planned_opening_date }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="cover__meta-item">
|
||||
<div class="cover__meta-label">Courts</div>
|
||||
<div class="cover__meta-value">{{ s.courts }}</div>
|
||||
</div>
|
||||
<div class="cover__meta-item">
|
||||
<div class="cover__meta-label">{{ s.labels.total_investment }}</div>
|
||||
<div class="cover__meta-value">{{ s.executive_summary.total_capex }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cover__footer-strip">
|
||||
<span>{{ s.labels.generated_by }}</span>
|
||||
<span>{{ s.labels.confidential }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>{{ s.labels.exec_paragraph }}</p>
|
||||
|
||||
<!-- Investment Plan (CAPEX) -->
|
||||
<h2>{{ s.investment.heading }}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>{{ s.labels.item }}</th><th style="text-align:right">{{ s.labels.amount }}</th><th>{{ s.labels.notes }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in s.investment['items'] %}
|
||||
<tr>
|
||||
<td>{{ item.name }}</td>
|
||||
<td style="text-align:right">{{ item.formatted_amount }}</td>
|
||||
<td style="color:#94A3B8;font-size:8pt">{{ item.info }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="total-row">
|
||||
<td>{{ s.labels.total_capex }}</td>
|
||||
<td style="text-align:right">{{ s.investment.total }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="font-size:9pt;color:#64748B">{{ s.labels.capex_stats }}</p>
|
||||
|
||||
<!-- Financing Structure -->
|
||||
<h2>{{ s.financing.heading }}</h2>
|
||||
<div class="fin-bar">
|
||||
<div class="fin-bar__equity" style="width:{{ (100 - (s.financing.loan_pct | replace('%','') | float)) }}%"></div>
|
||||
<div class="fin-bar__loan" style="width:{{ s.financing.loan_pct | replace('%','') }}%"></div>
|
||||
{# ============================================================ #}
|
||||
{# TABLE OF CONTENTS #}
|
||||
{# ============================================================ #}
|
||||
<div class="toc-page">
|
||||
<div class="toc-heading">{{ s.labels.table_of_contents }}</div>
|
||||
<ul class="toc-list">
|
||||
<li class="toc-item toc-item--heading"><span class="toc-num">1</span><span class="toc-text">{{ s.executive_summary.heading }}</span></li>
|
||||
{% if s.narrative.founder_background %}
|
||||
<li class="toc-item"><span class="toc-num">2</span><span class="toc-text">{{ s.labels.founder_profile }}</span><span class="toc-dots"></span></li>
|
||||
{% endif %}
|
||||
<li class="toc-item"><span class="toc-num">3</span><span class="toc-text">{{ s.labels.venture_description }}</span><span class="toc-dots"></span></li>
|
||||
{% if s.market_data %}
|
||||
<li class="toc-item"><span class="toc-num">4</span><span class="toc-text">{{ s.labels.market_analysis }}</span><span class="toc-dots"></span><span class="toc-section-badge">Data</span></li>
|
||||
{% endif %}
|
||||
<li class="toc-item"><span class="toc-num">5</span><span class="toc-text">{{ s.investment.heading }}</span><span class="toc-dots"></span></li>
|
||||
<li class="toc-item"><span class="toc-num">6</span><span class="toc-text">{{ s.use_of_funds.heading }}</span><span class="toc-dots"></span></li>
|
||||
<li class="toc-item"><span class="toc-num">7</span><span class="toc-text">{{ s.opening_balance.heading }}</span><span class="toc-dots"></span></li>
|
||||
<li class="toc-item"><span class="toc-num">8</span><span class="toc-text">{{ s.financing.heading }}</span><span class="toc-dots"></span></li>
|
||||
<li class="toc-item"><span class="toc-num">9</span><span class="toc-text">{{ s.operations.heading }}</span><span class="toc-dots"></span></li>
|
||||
<li class="toc-item"><span class="toc-num">10</span><span class="toc-text">{{ s.revenue.heading }}</span><span class="toc-dots"></span></li>
|
||||
<li class="toc-item"><span class="toc-num">11</span><span class="toc-text">{{ s.annuals.heading }}</span><span class="toc-dots"></span></li>
|
||||
<li class="toc-item"><span class="toc-num">12</span><span class="toc-text">{{ s.cashflow_12m.heading }}</span><span class="toc-dots"></span></li>
|
||||
<li class="toc-item"><span class="toc-num">13</span><span class="toc-text">{{ s.metrics.heading }}</span><span class="toc-dots"></span></li>
|
||||
<li class="toc-item"><span class="toc-num">14</span><span class="toc-text">{{ s.sensitivity.heading }}</span><span class="toc-dots"></span><span class="toc-section-badge">Bank</span></li>
|
||||
{% if s.narrative.marketing_concept %}
|
||||
<li class="toc-item"><span class="toc-num">15</span><span class="toc-text">{{ s.labels.market_analysis }} / Marketing</span><span class="toc-dots"></span></li>
|
||||
{% endif %}
|
||||
<li class="toc-item"><span class="toc-num">16</span><span class="toc-text">{{ s.labels.risk_analysis }}</span><span class="toc-dots"></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td>{{ s.labels.equity }}</td><td style="text-align:right">{{ s.financing.equity }}</td></tr>
|
||||
<tr><td>{{ s.labels.loan }} ({{ s.financing.loan_pct }})</td><td style="text-align:right">{{ s.financing.loan }}</td></tr>
|
||||
<tr><td>{{ s.labels.interest_rate }}</td><td style="text-align:right">{{ s.financing.interest_rate }}</td></tr>
|
||||
<tr><td>{{ s.labels.loan_term }}</td><td style="text-align:right">{{ s.financing.term }}</td></tr>
|
||||
<tr><td>{{ s.labels.monthly_payment }}</td><td style="text-align:right">{{ s.financing.monthly_payment }}</td></tr>
|
||||
<tr><td>{{ s.labels.annual_debt_service }}</td><td style="text-align:right">{{ s.financing.annual_debt_service }}</td></tr>
|
||||
<tr><td>{{ s.labels.ltv }}</td><td style="text-align:right">{{ s.financing.ltv }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Operating Costs -->
|
||||
<h2>{{ s.operations.heading }}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>{{ s.labels.item }}</th><th style="text-align:right">{{ s.labels.monthly }}</th><th>{{ s.labels.notes }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in s.operations['items'] %}
|
||||
<tr>
|
||||
<td>{{ item.name }}</td>
|
||||
<td style="text-align:right">{{ item.formatted_amount }}</td>
|
||||
<td style="color:#94A3B8;font-size:8pt">{{ item.info }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="total-row">
|
||||
<td>{{ s.labels.total_monthly_opex }}</td>
|
||||
<td style="text-align:right">{{ s.operations.monthly_total }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="font-size:9pt;color:#64748B">{{ s.labels.annual_opex }}: {{ s.operations.annual_total }}</p>
|
||||
{# ============================================================ #}
|
||||
{# 1. EXECUTIVE SUMMARY #}
|
||||
{# ============================================================ #}
|
||||
<div class="section section--break">
|
||||
<h2>1 {{ s.executive_summary.heading }}</h2>
|
||||
<div class="exec-grid">
|
||||
<div class="exec-card">
|
||||
<div class="exec-label">{{ s.labels.total_investment }}</div>
|
||||
<div class="exec-value">{{ s.executive_summary.total_capex }}</div>
|
||||
</div>
|
||||
<div class="exec-card">
|
||||
<div class="exec-label">{{ s.labels.equity_required }}</div>
|
||||
<div class="exec-value">{{ s.executive_summary.equity }}</div>
|
||||
</div>
|
||||
<div class="exec-card">
|
||||
<div class="exec-label">{{ s.labels.year1_revenue }}</div>
|
||||
<div class="exec-value">{{ s.executive_summary.y1_revenue }}</div>
|
||||
</div>
|
||||
<div class="exec-card exec-card--accent">
|
||||
<div class="exec-label">{{ s.labels.year3_ebitda }}</div>
|
||||
<div class="exec-value exec-value--gold">{{ s.executive_summary.y3_ebitda }}</div>
|
||||
</div>
|
||||
<div class="exec-card exec-card--accent">
|
||||
<div class="exec-label">{{ s.labels.irr }}</div>
|
||||
<div class="exec-value exec-value--gold">{{ s.executive_summary.irr }}</div>
|
||||
</div>
|
||||
<div class="exec-card">
|
||||
<div class="exec-label">{{ s.labels.payback_period }}</div>
|
||||
<div class="exec-value">{{ s.executive_summary.payback }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="exec-paragraph">{{ s.labels.exec_paragraph }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Revenue Model -->
|
||||
<h2>{{ s.revenue.heading }}</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td>{{ s.labels.weighted_hourly_rate }}</td><td style="text-align:right">{{ s.revenue.weighted_rate }}</td></tr>
|
||||
<tr><td>{{ s.labels.target_utilization }}</td><td style="text-align:right">{{ s.revenue.utilization }}</td></tr>
|
||||
<tr><td>{{ s.labels.gross_monthly_revenue }}</td><td style="text-align:right">{{ s.revenue.gross_monthly }}</td></tr>
|
||||
<tr><td>{{ s.labels.net_monthly_revenue }}</td><td style="text-align:right">{{ s.revenue.net_monthly }}</td></tr>
|
||||
<tr><td>{{ s.labels.monthly_ebitda }}</td><td style="text-align:right">{{ s.revenue.ebitda_monthly }}</td></tr>
|
||||
<tr><td>{{ s.labels.monthly_net_cf }}</td><td style="text-align:right">{{ s.revenue.net_cf_monthly }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{# ============================================================ #}
|
||||
{# 2. FOUNDER / MANAGEMENT PROFILE #}
|
||||
{# ============================================================ #}
|
||||
<div class="section section--break">
|
||||
<h2>2 {{ s.labels.founder_profile }}</h2>
|
||||
{% if s.narrative.founder_name %}
|
||||
<p style="font-weight: 700; color: var(--navy); margin-bottom: 6pt; font-size: 10pt;">{{ s.narrative.founder_name }}{% if s.narrative.legal_entity %} — {{ s.narrative.legal_entity }}{% endif %}</p>
|
||||
{% endif %}
|
||||
{% if s.narrative.founder_background %}
|
||||
<p class="narrative-block">{{ s.narrative.founder_background }}</p>
|
||||
{% else %}
|
||||
<p class="placeholder-block">
|
||||
[Gründerprofil: Beschreiben Sie hier Ihren beruflichen Hintergrund, relevante Erfahrungen im Sportbereich oder in der Gastronomie, unternehmerische Vorkenntnisse und Ihr persönliches Netzwerk. Banken möchten verstehen, warum Sie die richtige Person für dieses Vorhaben sind.]
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 5-Year Projection -->
|
||||
<h2>{{ s.annuals.heading }}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>{{ s.labels.year }}</th><th style="text-align:right">{{ s.labels.revenue }}</th><th style="text-align:right">{{ s.labels.ebitda }}</th><th style="text-align:right">{{ s.labels.debt_service }}</th><th style="text-align:right">{{ s.labels.net_cf }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for yr in s.annuals.years %}
|
||||
<tr>
|
||||
<td>{{ s.labels.year }} {{ yr.year }}</td>
|
||||
<td style="text-align:right">{{ yr.revenue }}</td>
|
||||
<td style="text-align:right">{{ yr.ebitda }}</td>
|
||||
<td style="text-align:right">{{ yr.debt_service }}</td>
|
||||
<td style="text-align:right">{{ yr.net_cf }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{# ============================================================ #}
|
||||
{# 3. BUSINESS DESCRIPTION / VENTURE #}
|
||||
{# ============================================================ #}
|
||||
<div class="section">
|
||||
<h2>3 {{ s.labels.venture_description }}</h2>
|
||||
{% if s.narrative.facility_description %}
|
||||
<p class="narrative-block">{{ s.narrative.facility_description }}</p>
|
||||
{% else %}
|
||||
<p class="narrative-block">
|
||||
Das geplante Vorhaben ist eine {{ s.executive_summary.facility_type }}-Padelhalle mit
|
||||
{{ s.executive_summary.courts }} Courts auf einer Nutzfläche von ca. {{ s.executive_summary.sqm }} m².
|
||||
Die Anlage richtet sich an Freizeitsportler, Unternehmenskunden und organisierte Vereinsspieler im lokalen Einzugsgebiet.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if s.narrative.planned_location_address %}
|
||||
<p style="font-size: 8.5pt; color: var(--muted); margin-bottom: 8pt;">
|
||||
<strong>Geplanter Standort:</strong> {{ s.narrative.planned_location_address }}
|
||||
{% if s.narrative.planned_opening_date %} — Eröffnung: {{ s.narrative.planned_opening_date }}{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Key Metrics -->
|
||||
<h2>{{ s.metrics.heading }}</h2>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-box">
|
||||
<div class="label">{{ s.labels.irr }}</div>
|
||||
<div class="value">{{ s.metrics.irr }}</div>
|
||||
{# ============================================================ #}
|
||||
{# 4. MARKET ANALYSIS (auto from DuckDB when available) #}
|
||||
{# ============================================================ #}
|
||||
<div class="section">
|
||||
<h2>4 {{ s.labels.market_analysis }}</h2>
|
||||
{% if s.market_data %}
|
||||
<p style="font-size: 9pt; color: var(--text); margin-bottom: 10pt;">
|
||||
<strong>{{ s.market_data.city_name }}{% if s.market_data.country %}, {{ s.market_data.country }}{% endif %}</strong>
|
||||
—
|
||||
Marktdaten aus dem Padelnomics Analytics-Datensatz (Playtomic-Buchungsdaten).
|
||||
</p>
|
||||
<div class="market-callout">
|
||||
{% if s.market_data.padel_venue_count %}
|
||||
<div class="market-stat">
|
||||
<div class="market-stat-value">{{ s.market_data.padel_venue_count }}</div>
|
||||
<div class="market-stat-label">Padel-Anlagen</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if s.market_data.venues_per_100k %}
|
||||
<div class="market-stat">
|
||||
<div class="market-stat-value">{{ "%.1f"|format(s.market_data.venues_per_100k) }}</div>
|
||||
<div class="market-stat-label">{{ s.labels.venues_per_100k }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if s.market_data.median_occupancy_rate %}
|
||||
<div class="market-stat">
|
||||
<div class="market-stat-value">{{ "%.0f"|format(s.market_data.median_occupancy_rate * 100) }}%</div>
|
||||
<div class="market-stat-label">{{ s.labels.median_occupancy }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if s.market_data.median_peak_rate %}
|
||||
<div class="market-stat">
|
||||
<div class="market-stat-value">{{ s.labels.currency_sym }}{{ "%.0f"|format(s.market_data.median_peak_rate) }}</div>
|
||||
<div class="market-stat-label">{{ s.labels.median_peak_rate }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if s.market_data.median_offpeak_rate %}
|
||||
<div class="market-stat">
|
||||
<div class="market-stat-value">{{ s.labels.currency_sym }}{{ "%.0f"|format(s.market_data.median_offpeak_rate) }}</div>
|
||||
<div class="market-stat-label">{{ s.labels.median_offpeak_rate }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if s.market_data.market_score %}
|
||||
<div class="market-stat">
|
||||
<div class="market-stat-value">{{ "%.0f"|format(s.market_data.market_score) }}/100</div>
|
||||
<div class="market-stat-label">{{ s.labels.market_score }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="label">{{ s.labels.moic }}</div>
|
||||
<div class="value">{{ s.metrics.moic }}</div>
|
||||
{% else %}
|
||||
<p class="placeholder-block">
|
||||
[Marktanalyse: Beschreiben Sie die Nachfragesituation im Einzugsgebiet: Anzahl aktiver Padelspieler, bestehende Anlagen und deren Auslastung, Wettbewerbsumfeld, demographische Zielgruppe. Sofern eine Playtomic-Auswertung für Ihren Standort vorliegt, fügen Sie die Daten hier ein.]
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ============================================================ #}
|
||||
{# 5. INVESTMENT PLAN (CAPEX) #}
|
||||
{# ============================================================ #}
|
||||
<div class="section section--break">
|
||||
<h2>5 {{ s.investment.heading }}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ s.labels.item }}</th>
|
||||
<th class="r">{{ s.labels.amount }}</th>
|
||||
<th>{{ s.labels.notes }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in s.investment['items'] %}
|
||||
<tr>
|
||||
<td>{{ item.name }}</td>
|
||||
<td class="r">{{ item.formatted_amount }}</td>
|
||||
<td class="note-cell">{{ item.info }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="total-row">
|
||||
<td>{{ s.labels.total_capex }}</td>
|
||||
<td class="r">{{ s.investment.total }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="font-size: 8pt; color: var(--muted);">{{ s.labels.capex_stats }}</p>
|
||||
</div>
|
||||
|
||||
{# ============================================================ #}
|
||||
{# 6. USE OF FUNDS (Mittelverwendungsplan) #}
|
||||
{# ============================================================ #}
|
||||
<div class="section">
|
||||
<h2>6 {{ s.use_of_funds.heading }}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ s.labels.item }}</th>
|
||||
<th class="r">{{ s.labels.total }}</th>
|
||||
<th class="r">{{ s.labels.funded_by_loan }}</th>
|
||||
<th class="r">{{ s.labels.funded_by_equity }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in s.use_of_funds.rows %}
|
||||
<tr>
|
||||
<td>{{ row.item }}</td>
|
||||
<td class="r">{{ row.total }}</td>
|
||||
<td class="r">{{ row.from_loan }}</td>
|
||||
<td class="r">{{ row.from_equity }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="total-row">
|
||||
<td>{{ s.labels.total }}</td>
|
||||
<td class="r">{{ s.use_of_funds.total_capex }}</td>
|
||||
<td class="r">{{ s.use_of_funds.total_loan }}</td>
|
||||
<td class="r">{{ s.use_of_funds.total_equity }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{# ============================================================ #}
|
||||
{# 7. OPENING BALANCE SHEET (Eröffnungsbilanz) #}
|
||||
{# ============================================================ #}
|
||||
<div class="section">
|
||||
<h2>7 {{ s.opening_balance.heading }}</h2>
|
||||
<div class="balance-grid">
|
||||
<div class="balance-side">
|
||||
<div class="balance-side__header">{{ s.labels.assets }}</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td>{{ s.labels.fixed_assets }}</td><td class="r">{{ s.opening_balance.fixed_assets }}</td></tr>
|
||||
<tr><td>{{ s.labels.working_capital }}</td><td class="r">{{ s.opening_balance.working_capital }}</td></tr>
|
||||
<tr class="balance-total">
|
||||
<td>{{ s.labels.total }}</td>
|
||||
<td class="r">{{ s.opening_balance.total_assets }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="balance-side">
|
||||
<div class="balance-side__header">{{ s.labels.liabilities_equity }}</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td>{{ s.labels.equity }}</td><td class="r">{{ s.opening_balance.equity }}</td></tr>
|
||||
<tr><td>{{ s.labels.loan }}</td><td class="r">{{ s.opening_balance.loan }}</td></tr>
|
||||
<tr class="balance-total">
|
||||
<td>{{ s.labels.total }}</td>
|
||||
<td class="r">{{ s.opening_balance.total_liabilities_equity }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="label">{{ s.labels.cash_on_cash }}</div>
|
||||
<div class="value">{{ s.metrics.cash_on_cash }}</div>
|
||||
<p style="font-size: 8pt; color: var(--muted);">
|
||||
{{ s.labels.equity_ratio }}: <strong>{{ s.opening_balance.equity_ratio }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# ============================================================ #}
|
||||
{# 8. FINANCING STRUCTURE #}
|
||||
{# ============================================================ #}
|
||||
<div class="section section--break">
|
||||
<h2>8 {{ s.financing.heading }}</h2>
|
||||
<div class="fin-wrap">
|
||||
<div class="fin-bar">
|
||||
<div class="fin-bar__equity" style="width:{{ (100 - (s.financing.loan_pct | replace('%','') | float)) }}%">
|
||||
<span class="fin-bar__label">{{ s.labels.equity }} {{ (100 - (s.financing.loan_pct | replace('%','') | float)) | round(0) | int }}%</span>
|
||||
</div>
|
||||
<div class="fin-bar__loan" style="width:{{ s.financing.loan_pct | replace('%','') }}%">
|
||||
<span class="fin-bar__label">{{ s.labels.loan }} {{ s.financing.loan_pct }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fin-legend">
|
||||
<div class="fin-legend-item">
|
||||
<div class="fin-legend-dot" style="background: var(--navy);"></div>
|
||||
<span>{{ s.labels.equity }}: {{ s.financing.equity }}</span>
|
||||
</div>
|
||||
<div class="fin-legend-item">
|
||||
<div class="fin-legend-dot" style="background: var(--gold);"></div>
|
||||
<span>{{ s.labels.loan }}: {{ s.financing.loan }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="label">{{ s.labels.payback }}</div>
|
||||
<div class="value">{{ s.metrics.payback }}</div>
|
||||
<table class="stat-table">
|
||||
<tbody>
|
||||
<tr><td>{{ s.labels.equity }}</td><td class="r">{{ s.financing.equity }}</td></tr>
|
||||
<tr><td>{{ s.labels.loan }} ({{ s.financing.loan_pct }})</td><td class="r">{{ s.financing.loan }}</td></tr>
|
||||
<tr><td>{{ s.labels.interest_rate }}</td><td class="r">{{ s.financing.interest_rate }}</td></tr>
|
||||
<tr><td>{{ s.labels.loan_term }}</td><td class="r">{{ s.financing.term }}</td></tr>
|
||||
<tr><td>{{ s.labels.monthly_payment }}</td><td class="r">{{ s.financing.monthly_payment }}</td></tr>
|
||||
<tr><td>{{ s.labels.annual_debt_service }}</td><td class="r">{{ s.financing.annual_debt_service }}</td></tr>
|
||||
<tr><td>{{ s.labels.ltv }}</td><td class="r">{{ s.financing.ltv }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{# ============================================================ #}
|
||||
{# 9. OPERATING COSTS (OPEX) #}
|
||||
{# ============================================================ #}
|
||||
<div class="section">
|
||||
<h2>9 {{ s.operations.heading }}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ s.labels.item }}</th>
|
||||
<th class="r">{{ s.labels.monthly }}</th>
|
||||
<th>{{ s.labels.notes }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in s.operations['items'] %}
|
||||
<tr>
|
||||
<td>{{ item.name }}</td>
|
||||
<td class="r">{{ item.formatted_amount }}</td>
|
||||
<td class="note-cell">{{ item.info }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="total-row">
|
||||
<td>{{ s.labels.total_monthly_opex }}</td>
|
||||
<td class="r">{{ s.operations.monthly_total }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="font-size: 8pt; color: var(--muted);">{{ s.labels.annual_opex }}: <strong>{{ s.operations.annual_total }}</strong></p>
|
||||
{% if s.narrative.operations_concept %}
|
||||
<h3>Betriebskonzept</h3>
|
||||
<p class="narrative-block" style="font-size: 8.5pt;">{{ s.narrative.operations_concept }}</p>
|
||||
{% endif %}
|
||||
{% if s.narrative.staffing_plan %}
|
||||
<h3>Personalplanung</h3>
|
||||
<p class="narrative-block" style="font-size: 8.5pt;">{{ s.narrative.staffing_plan }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ============================================================ #}
|
||||
{# 10. REVENUE MODEL #}
|
||||
{# ============================================================ #}
|
||||
<div class="section">
|
||||
<h2>10 {{ s.revenue.heading }}</h2>
|
||||
<table class="stat-table">
|
||||
<tbody>
|
||||
<tr><td>{{ s.labels.weighted_hourly_rate }}</td><td class="r">{{ s.revenue.weighted_rate }}</td></tr>
|
||||
<tr><td>{{ s.labels.target_utilization }}</td><td class="r">{{ s.revenue.utilization }}</td></tr>
|
||||
<tr><td>{{ s.labels.gross_monthly_revenue }}</td><td class="r">{{ s.revenue.gross_monthly }}</td></tr>
|
||||
<tr><td>{{ s.labels.net_monthly_revenue }}</td><td class="r">{{ s.revenue.net_monthly }}</td></tr>
|
||||
<tr><td>{{ s.labels.monthly_ebitda }}</td><td class="r">{{ s.revenue.ebitda_monthly }}</td></tr>
|
||||
<tr><td>{{ s.labels.monthly_net_cf }}</td><td class="r">{{ s.revenue.net_cf_monthly }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% if s.narrative.marketing_concept %}
|
||||
<h3>Marketingkonzept</h3>
|
||||
<p class="narrative-block" style="font-size: 8.5pt;">{{ s.narrative.marketing_concept }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# ============================================================ #}
|
||||
{# 11. 5-YEAR PROJECTION (P&L) + CHART #}
|
||||
{# ============================================================ #}
|
||||
<div class="section section--break">
|
||||
<h2>11 {{ s.annuals.heading }}</h2>
|
||||
{% if s.charts.pnl %}
|
||||
<div class="chart-wrap">
|
||||
{{ s.charts.pnl }}
|
||||
<p class="chart-caption">Revenue, EBITDA and Net Cash Flow — 5-Year Projection</p>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="label">{{ s.labels.break_even_util }}</div>
|
||||
<div class="value">{{ s.metrics.break_even_util }}</div>
|
||||
{% endif %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ s.labels.year }}</th>
|
||||
<th class="r">{{ s.labels.revenue }}</th>
|
||||
<th class="r">{{ s.labels.ebitda }}</th>
|
||||
<th class="r">{{ s.labels.debt_service }}</th>
|
||||
<th class="r">{{ s.labels.net_cf }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for yr in s.annuals.years %}
|
||||
<tr>
|
||||
<td>{{ s.labels.year }} {{ yr.year }}</td>
|
||||
<td class="r">{{ yr.revenue }}</td>
|
||||
<td class="r positive-cell">{{ yr.ebitda }}</td>
|
||||
<td class="r">{{ yr.debt_service }}</td>
|
||||
<td class="r {% if yr.net_cf.startswith('-') %}negative-cell{% else %}positive-cell{% endif %}">{{ yr.net_cf }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{# ============================================================ #}
|
||||
{# 12. 12-MONTH CASH FLOW + CHART #}
|
||||
{# ============================================================ #}
|
||||
<div class="section section--break">
|
||||
<h2>12 {{ s.cashflow_12m.heading }}</h2>
|
||||
{% if s.charts.cashflow %}
|
||||
<div class="chart-wrap">
|
||||
{{ s.charts.cashflow }}
|
||||
<p class="chart-caption">Monthly NCF and Cumulative Cash Position — Year 1</p>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="label">{{ s.labels.ebitda_margin }}</div>
|
||||
<div class="value">{{ s.metrics.ebitda_margin }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="label">{{ s.labels.dscr_y3 }}</div>
|
||||
<div class="value">{{ s.metrics.dscr_y3 }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="label">{{ s.labels.yield_on_cost }}</div>
|
||||
<div class="value">{{ s.metrics.yield_on_cost }}</div>
|
||||
{% endif %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ s.labels.month }}</th>
|
||||
<th class="r">{{ s.labels.revenue }}</th>
|
||||
<th class="r">{{ s.labels.opex }}</th>
|
||||
<th class="r">{{ s.labels.ebitda }}</th>
|
||||
<th class="r">{{ s.labels.debt }}</th>
|
||||
<th class="r">{{ s.labels.net_cf }}</th>
|
||||
<th class="r">{{ s.labels.cumulative }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in s.cashflow_12m.months %}
|
||||
<tr>
|
||||
<td>{{ m.month }}</td>
|
||||
<td class="r">{{ m.revenue }}</td>
|
||||
<td class="r">{{ m.opex }}</td>
|
||||
<td class="r">{{ m.ebitda }}</td>
|
||||
<td class="r">{{ m.debt }}</td>
|
||||
<td class="r {% if m.ncf.startswith('-') %}negative-cell{% else %}positive-cell{% endif %}">{{ m.ncf }}</td>
|
||||
<td class="r {% if m.cumulative.startswith('-') %}negative-cell{% else %}positive-cell{% endif %}">{{ m.cumulative }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{# ============================================================ #}
|
||||
{# 13. KEY METRICS #}
|
||||
{# ============================================================ #}
|
||||
<div class="section">
|
||||
<h2>13 {{ s.metrics.heading }}</h2>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-box">
|
||||
<div class="metric-label">{{ s.labels.irr }}</div>
|
||||
<div class="metric-value" style="color: var(--gold);">{{ s.metrics.irr }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="metric-label">{{ s.labels.moic }}</div>
|
||||
<div class="metric-value">{{ s.metrics.moic }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="metric-label">{{ s.labels.cash_on_cash }}</div>
|
||||
<div class="metric-value">{{ s.metrics.cash_on_cash }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="metric-label">{{ s.labels.payback }}</div>
|
||||
<div class="metric-value">{{ s.metrics.payback }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="metric-label">{{ s.labels.break_even_util }}</div>
|
||||
<div class="metric-value">{{ s.metrics.break_even_util }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="metric-label">{{ s.labels.ebitda_margin }}</div>
|
||||
<div class="metric-value">{{ s.metrics.ebitda_margin }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="metric-label">{{ s.labels.dscr_y3 }}</div>
|
||||
<div class="metric-value" style="color: var(--green);">{{ s.metrics.dscr_y3 }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="metric-label">{{ s.labels.yield_on_cost }}</div>
|
||||
<div class="metric-value">{{ s.metrics.yield_on_cost }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 12-Month Cash Flow -->
|
||||
<h2>{{ s.cashflow_12m.heading }}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>{{ s.labels.month }}</th><th style="text-align:right">{{ s.labels.revenue }}</th><th style="text-align:right">{{ s.labels.opex }}</th><th style="text-align:right">{{ s.labels.ebitda }}</th><th style="text-align:right">{{ s.labels.debt }}</th><th style="text-align:right">{{ s.labels.net_cf }}</th><th style="text-align:right">{{ s.labels.cumulative }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in s.cashflow_12m.months %}
|
||||
<tr>
|
||||
<td>{{ m.month }}</td>
|
||||
<td style="text-align:right">{{ m.revenue }}</td>
|
||||
<td style="text-align:right">{{ m.opex }}</td>
|
||||
<td style="text-align:right">{{ m.ebitda }}</td>
|
||||
<td style="text-align:right">{{ m.debt }}</td>
|
||||
<td style="text-align:right">{{ m.ncf }}</td>
|
||||
<td style="text-align:right">{{ m.cumulative }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{# ============================================================ #}
|
||||
{# 14. SENSITIVITY ANALYSIS #}
|
||||
{# ============================================================ #}
|
||||
<div class="section section--break">
|
||||
<h2>14 {{ s.sensitivity.heading }}</h2>
|
||||
|
||||
<!-- Disclaimer -->
|
||||
<h3>{{ s.sensitivity.util_heading }}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ s.labels.utilization }}</th>
|
||||
<th class="r">{{ s.labels.revenue }}</th>
|
||||
<th class="r">{{ s.labels.monthly_ncf }}</th>
|
||||
<th class="r">{{ s.labels.annual_ncf }}</th>
|
||||
<th class="r">{{ s.labels.dscr }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in s.sensitivity.sens_rows %}
|
||||
<tr {% if row.is_target %}class="highlight-row"{% endif %}>
|
||||
<td>{{ row.util }}%{% if row.is_target %} ★{% endif %}</td>
|
||||
<td class="r">{{ row.rev_fmt }}</td>
|
||||
<td class="r {% if row.ncf < 0 %}negative-cell{% endif %}">{{ row.ncf_fmt }}</td>
|
||||
<td class="r {% if row.annual < 0 %}negative-cell{% endif %}">{{ row.annual_fmt }}</td>
|
||||
<td class="r {% if row.dscr < 1.2 %}negative-cell{% elif row.dscr > 1.5 %}positive-cell{% endif %}">{{ row.dscr_fmt }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="font-size: 7.5pt; color: var(--muted);">★ = target utilization | DSCR threshold: 1.20× (bank covenant)</p>
|
||||
|
||||
<h3>{{ s.sensitivity.price_heading }}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ s.labels.price_delta }}</th>
|
||||
<th class="r">{{ s.labels.hourly_rate }}</th>
|
||||
<th class="r">{{ s.labels.revenue }}</th>
|
||||
<th class="r">{{ s.labels.monthly_ncf }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in s.sensitivity.price_rows %}
|
||||
<tr {% if row.is_base %}class="highlight-row"{% endif %}>
|
||||
<td>{% if row.delta > 0 %}+{% endif %}{{ row.delta }}%{% if row.is_base %} (base){% endif %}</td>
|
||||
<td class="r">{{ row.adj_rate_fmt }}</td>
|
||||
<td class="r">{{ row.rev_fmt }}</td>
|
||||
<td class="r {% if row.ncf < 0 %}negative-cell{% endif %}">{{ row.ncf_fmt }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{# ============================================================ #}
|
||||
{# 15. RISK ANALYSIS #}
|
||||
{# ============================================================ #}
|
||||
<div class="section section--break">
|
||||
<h2>15 {{ s.labels.risk_analysis }}</h2>
|
||||
|
||||
<div class="risk-item">
|
||||
<span class="risk-badge risk-badge--high">Hoch</span>
|
||||
<div class="risk-text">
|
||||
<strong>Anlaufphase / Cashflow-Risiko</strong>
|
||||
Die ersten 6–12 Betriebsmonate laufen unter Zielbetrieb. Entschärfung: Betriebsmittelreserve eingeplant, tilgungsfreie Anlaufjahre in der Finanzierung, konservative Anlaufkurve im Modell abgebildet.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="risk-item">
|
||||
<span class="risk-badge risk-badge--high">Hoch</span>
|
||||
<div class="risk-text">
|
||||
<strong>Baukostenüberschreitung</strong>
|
||||
Umbau- und Haustechnikkosten können bei Bestandshallen erheblich abweichen. Entschärfung: 10–15 % Puffer im CAPEX-Plan, Festpreisverträge mit Generalunternehmer.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="risk-item">
|
||||
<span class="risk-badge risk-badge--medium">Mittel</span>
|
||||
<div class="risk-text">
|
||||
<strong>Wettbewerbsrisiko / Angebotsübersättigung</strong>
|
||||
Schnell wachsender Markt mit erhöhter Neueröffnungsaktivität. Entschärfung: Standortwahl in unterversorgtem Marktgebiet, differenziertes Serviceangebot.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="risk-item">
|
||||
<span class="risk-badge risk-badge--medium">Mittel</span>
|
||||
<div class="risk-text">
|
||||
<strong>Zinsänderungsrisiko</strong>
|
||||
Steigende Zinsen erhöhen den Kapitaldienst bei variabel verzinsten Darlehen. Entschärfung: Festzins-Option prüfen; Sensitivitätsanalyse zeigt Tragfähigkeit bis +150 Bp.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="risk-item">
|
||||
<span class="risk-badge risk-badge--medium">Mittel</span>
|
||||
<div class="risk-text">
|
||||
<strong>Schlüsselpersonenrisiko</strong>
|
||||
Ausfall des Gründers / Hallenleiters würde Betrieb kurzfristig belasten. Entschärfung: frühzeitige Einbindung eines erfahrenen Betriebsleiters, Kranken-/Unfallversicherung.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="risk-item">
|
||||
<span class="risk-badge risk-badge--low">Niedrig</span>
|
||||
<div class="risk-text">
|
||||
<strong>Markttrend-Risiko</strong>
|
||||
Padel ist ein wachsendes Segment, aber kein strukturell gesicherter Markt. Entschärfung: Break-even bei {{ s.metrics.break_even_util }} Auslastung — weit unter typischer Marktauslastung; flexible Hallenkonzeption.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="risk-item">
|
||||
<span class="risk-badge risk-badge--low">Niedrig</span>
|
||||
<div class="risk-text">
|
||||
<strong>Regulatorische Risiken</strong>
|
||||
Baugenehmigung, Nutzungsänderung, Lärmschutzauflagen. Entschärfung: Vorab-Gespräch mit Bauordnungsamt, Lärmschutzgutachten bereits in Planung.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ============================================================ #}
|
||||
{# DISCLAIMER #}
|
||||
{# ============================================================ #}
|
||||
<div class="disclaimer">
|
||||
{{ s.labels.disclaimer }}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user