From 57c8ad7e2c1344cf417356dec63d5f6c0f93595c Mon Sep 17 00:00:00 2001 From: Daniel Holbach Date: Thu, 2 Jul 2026 16:45:00 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20analytics=20charts=20=E2=80=94=20year=20?= =?UTF-8?q?separators,=20partial=20month,=20dead=20code,=20i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chart correctness: - Exclude current partial month from capacity and revenue/expense trends so the last data point is never anomalously low (e.g. 2 days = 8 h capacity) - Fix year separator/label misalignment in all line charts: after findFirstNonZeroIndex trims leading months, drawYearSeparators and drawYearLabels now receive startMonth so index 0 maps to the actual first month, not always January of startYear - Fix revenueLineChart label: removed |slice:":3" that truncated "01/20" to "01/" (broke parseMonthString); chart now passes full MM/YY labels - Add startYear/startMonth to revenueLineChart, daysToPaymentChart, cancellationTrendsChart (all were missing startMonth) capacity_helpers.py refactor: - Extract _build_holiday_set() — both callers used the same 3-line pattern - Replace manual month-end arithmetic with DateRangeHelper.get_last_of_month - get_capacity_trends loop delegates to _calculate_weighted_capacity instead of inlining the same formula Dead JS code removed: - setupTooltip (chart_tooltip.js) — superseded by ChartTooltip - setupBarChart (chart_bar.js) — duplicated initializeChart - drawXAxisLabels (chart_primitives.js) — never called - calculateYearLabels (chart_math.js) — tested but not wired to rendering - drawSimpleBarChart inline label replaced with drawYearLabel() call - chart_tooltip.js script tag removed from analytics.html i18n (P-039): - CHART_MESSAGES JS global injected before chart scripts; chart_helpers.js uses it with German fallback - LOCALE global uses navigator.language instead of hardcoded 'de-DE' - MONTH_LABELS array uses {% trans %} with German msgstrs - 'Ø' legend label → {% trans "Avg" %} → msgstr "Ø" - practice_analysis.py: all insight strings and "Arbeitstage" wrapped with gettext/ngettext; German translations added to django.po Co-Authored-By: Claude Sonnet 4.6 --- app/locale/de/LC_MESSAGES/django.po | 407 ++++++++++++------ app/locale/en/LC_MESSAGES/django.po | 472 ++++++++++++++------- app/my_practice/utils/analytics_utils.py | 8 + app/my_practice/utils/capacity_helpers.py | 40 +- app/my_practice/utils/practice_analysis.py | 67 ++- app/static/js/chart_utils.test.extended.js | 11 - app/static/js/chart_utils.test.js | 16 - app/static/js/charts/chart_bar.js | 29 +- app/static/js/charts/chart_builder.js | 5 +- app/static/js/charts/chart_helpers.js | 16 +- app/static/js/charts/chart_line.js | 17 +- app/static/js/charts/chart_math.js | 25 -- app/static/js/charts/chart_primitives.js | 17 - app/static/js/charts/chart_tooltip.js | 54 +-- app/templates/my_practice/analytics.html | 70 ++- 15 files changed, 744 insertions(+), 510 deletions(-) diff --git a/app/locale/de/LC_MESSAGES/django.po b/app/locale/de/LC_MESSAGES/django.po index 0938538..168d9c6 100644 --- a/app/locale/de/LC_MESSAGES/django.po +++ b/app/locale/de/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: my-practice\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-01 10:07+0200\n" +"POT-Creation-Date: 2026-07-02 15:54+0200\n" "PO-Revision-Date: 2026-06-22 23:01+0200\n" "Language-Team: German\n" "Language: de\n" @@ -114,6 +114,82 @@ msgstr "" msgid "Leistungserfassungen" msgstr "" +#: my_practice/utils/practice_analysis.py:229 +#, python-format +msgid "%(n)s working day" +msgid_plural "%(n)s working days" +msgstr[0] "%(n)s Arbeitstag" +msgstr[1] "%(n)s Arbeitstage" + +#: my_practice/utils/practice_analysis.py:267 +#, python-format +msgid "📅 Analyzing %(label)s (%(days)s days)" +msgstr "📅 Analyse %(label)s (%(days)s Tage)" + +#: my_practice/utils/practice_analysis.py:277 +#, python-format +msgid "👥 %(active)s of %(total)s clients active (%(pct)s%%)" +msgstr "👥 %(active)s von %(total)s Klient:innen aktiv (%(pct)s%%)" + +#: my_practice/utils/practice_analysis.py:296 +#, python-format +msgid "⚠️ High concentration: Top 3 clients = %(pct)s%% of sessions" +msgstr "⚠️ Hohe Konzentration: Top 3 Klient:innen = %(pct)s%% der Sitzungen" + +#: my_practice/utils/practice_analysis.py:302 +#, python-format +msgid "📊 Top 3 clients account for %(pct)s%% of sessions" +msgstr "📊 Top 3 Klient:innen: %(pct)s%% der Sitzungen" + +#: my_practice/utils/practice_analysis.py:312 +#, python-format +msgid "📈 Average: %(avg)sh per active client" +msgstr "📈 Durchschnitt: %(avg)sh pro aktiver Klient:in" + +#: my_practice/utils/practice_analysis.py:320 +#, python-format +msgid "🌱 %(n)s new probatoric client (%(h)sh)" +msgid_plural "🌱 %(n)s new probatoric clients (%(h)sh)" +msgstr[0] "🌱 %(n)s neue probatorische Klient:in (%(h)sh)" +msgstr[1] "🌱 %(n)s neue probatorische Klient:innen (%(h)sh)" + +#: my_practice/utils/practice_analysis.py:329 +#, python-format +msgid "💤 %(n)s dormant clients (no activity this period)" +msgstr "💤 %(n)s ruhende Klient:innen (keine Aktivität in diesem Zeitraum)" + +#: my_practice/utils/practice_analysis.py:339 +#, python-format +msgid "📉 Low utilization: Only %(pct)s%% capacity used (%(rem)sh available)" +msgstr "📉 Geringe Auslastung: Nur %(pct)s%% Kapazität genutzt (%(rem)sh verfügbar)" + +#: my_practice/utils/practice_analysis.py:341 +#, python-format +msgid "📊 Moderate utilization: %(pct)s%% capacity used (%(rem)sh available)" +msgstr "📊 Mittlere Auslastung: %(pct)s%% Kapazität genutzt (%(rem)sh verfügbar)" + +#: my_practice/utils/practice_analysis.py:343 +#, python-format +msgid "✅ Good utilization: %(pct)s%% capacity used (%(rem)sh available)" +msgstr "✅ Gute Auslastung: %(pct)s%% Kapazität genutzt (%(rem)sh verfügbar)" + +#: my_practice/utils/practice_analysis.py:345 +#, python-format +msgid "⚠️ High utilization: %(pct)s%% capacity used (only %(rem)sh remaining)" +msgstr "⚠️ Hohe Auslastung: %(pct)s%% Kapazität genutzt (nur %(rem)sh verbleibend)" + +#: my_practice/utils/practice_analysis.py:346 +#, python-format +msgid "🔴 At/over capacity: %(pct)s%% utilized" +msgstr "🔴 An/über Kapazität: %(pct)s%% ausgelastet" + +#: my_practice/utils/practice_analysis.py:352 +#, python-format +msgid "💰 Revenue opportunity: %(n)s client with sessions but no invoices" +msgid_plural "💰 Revenue opportunity: %(n)s clients with sessions but no invoices" +msgstr[0] "💰 Abrechnungspotenzial: %(n)s Klient:in mit Sitzungen aber ohne Rechnung" +msgstr[1] "💰 Abrechnungspotenzial: %(n)s Klient:innen mit Sitzungen aber ohne Rechnung" + #: my_practice/views/client_views.py:172 #, python-brace-format msgid "Client {obj.full_name} saved successfully!" @@ -326,8 +402,8 @@ msgid "No new sessions added (already billed?)." msgstr "Keine neuen Sitzungen hinzugefügt (bereits abgerechnet?)." #: my_practice/views/invoice_views.py:530 -#: my_practice/views/invoice_views.py:792 -#: my_practice/views/invoice_views.py:906 +#: my_practice/views/invoice_views.py:803 +#: my_practice/views/invoice_views.py:917 msgid "No active practice found." msgstr "Keine aktive Praxis gefunden." @@ -337,37 +413,37 @@ msgid_plural "Invoice {} with {} sessions created." msgstr[0] "Rechnung {} mit {} Sitzung erstellt." msgstr[1] "Rechnung {} mit {} Sitzungen erstellt." -#: my_practice/views/invoice_views.py:766 +#: my_practice/views/invoice_views.py:777 msgid "Cancelled session billed" msgstr "Stornierte Sitzung abgerechnet" -#: my_practice/views/invoice_views.py:768 +#: my_practice/views/invoice_views.py:779 msgid "Appointments pending" msgstr "Termine ausstehend" -#: my_practice/views/invoice_views.py:770 +#: my_practice/views/invoice_views.py:781 msgid "Not billed" msgstr "Nicht abgerechnet" -#: my_practice/views/invoice_views.py:772 +#: my_practice/views/invoice_views.py:783 #: templates/my_practice/dashboard.html:136 #: templates/my_practice/invoice_detail.html:40 msgid "Draft" msgstr "Entwurf" -#: my_practice/views/invoice_views.py:774 +#: my_practice/views/invoice_views.py:785 #: templates/my_practice/dashboard.html:141 #: templates/my_practice/invoice_detail.html:41 msgid "Sent" msgstr "Versendet" -#: my_practice/views/invoice_views.py:776 +#: my_practice/views/invoice_views.py:787 #: templates/my_practice/dashboard.html:146 #: templates/my_practice/invoice_detail.html:42 msgid "Paid" msgstr "Bezahlt" -#: my_practice/views/invoice_views.py:777 +#: my_practice/views/invoice_views.py:788 msgid "OK" msgstr "OK" @@ -402,199 +478,210 @@ msgstr "Analysen" msgid "Revenue, Clients, Capacity & Expenses" msgstr "Umsatz, Klienten, Kapazität & Ausgaben" -#: templates/my_practice/analytics.html:35 +#: templates/my_practice/analytics.html:15 +#: templates/my_practice/analytics.html:272 +msgid "No data available" +msgstr "Keine Daten verfügbar" + +#: templates/my_practice/analytics.html:16 +msgid "No data in selected period" +msgstr "Keine Daten im ausgewählten Zeitraum" + +#: templates/my_practice/analytics.html:17 +#, fuzzy +#| msgid "No data available" +msgid "No valid data available" +msgstr "Keine Daten verfügbar" + +#: templates/my_practice/analytics.html:42 msgid "Period" msgstr "Zeitraum" -#: templates/my_practice/analytics.html:38 +#: templates/my_practice/analytics.html:45 #, python-format msgid "All years (%(year)s–today)" msgstr "Alle Jahre (%(year)s–heute)" -#: templates/my_practice/analytics.html:39 +#: templates/my_practice/analytics.html:46 msgid "Last year" msgstr "Letztes Jahr" -#: templates/my_practice/analytics.html:40 +#: templates/my_practice/analytics.html:47 msgid "Last quarter" msgstr "Letztes Quartal" -#: templates/my_practice/analytics.html:41 +#: templates/my_practice/analytics.html:48 msgid "Last month" msgstr "Letzter Monat" -#: templates/my_practice/analytics.html:42 +#: templates/my_practice/analytics.html:49 msgid "Custom" msgstr "Benutzerdefiniert" -#: templates/my_practice/analytics.html:49 +#: templates/my_practice/analytics.html:56 msgid "From" msgstr "Von" -#: templates/my_practice/analytics.html:55 +#: templates/my_practice/analytics.html:62 msgid "To" msgstr "Bis" -#: templates/my_practice/analytics.html:62 +#: templates/my_practice/analytics.html:69 msgid "Apply" msgstr "Anwenden" -#: templates/my_practice/analytics.html:66 +#: templates/my_practice/analytics.html:73 msgid "Reset" msgstr "Zurücksetzen" -#: templates/my_practice/analytics.html:94 -#: templates/my_practice/analytics.html:178 +#: templates/my_practice/analytics.html:101 +#: templates/my_practice/analytics.html:185 msgid "Revenue" msgstr "Umsatz" -#: templates/my_practice/analytics.html:97 -#: templates/my_practice/analytics.html:574 +#: templates/my_practice/analytics.html:104 +#: templates/my_practice/analytics.html:592 #: templates/my_practice/client_list_cards.html:8 msgid "Clients" msgstr "Klient:innen" -#: templates/my_practice/analytics.html:100 -#: templates/my_practice/analytics.html:706 +#: templates/my_practice/analytics.html:107 +#: templates/my_practice/analytics.html:731 msgid "Capacity" msgstr "Kapazität" -#: templates/my_practice/analytics.html:103 -#: templates/my_practice/analytics.html:132 -#: templates/my_practice/analytics.html:179 +#: templates/my_practice/analytics.html:110 +#: templates/my_practice/analytics.html:139 +#: templates/my_practice/analytics.html:186 msgid "Expenses" msgstr "Ausgaben" -#: templates/my_practice/analytics.html:110 +#: templates/my_practice/analytics.html:117 msgid "Revenue & Profit" msgstr "Umsatz & Gewinn" -#: templates/my_practice/analytics.html:115 +#: templates/my_practice/analytics.html:122 msgid "Detailed Revenue Report" msgstr "Detaillierter Umsatz-Report" -#: templates/my_practice/analytics.html:121 +#: templates/my_practice/analytics.html:128 msgid "Profit (Revenue - Expenses)" msgstr "Gewinn (Umsatz - Ausgaben)" -#: templates/my_practice/analytics.html:123 +#: templates/my_practice/analytics.html:130 #, python-format msgid "Business profit %(start_year)s–today · Cumulative development" msgstr "Geschäftsgewinn %(start_year)s–heute · Kumulative Entwicklung" -#: templates/my_practice/analytics.html:130 -#: templates/my_practice/analytics.html:641 +#: templates/my_practice/analytics.html:137 +#: templates/my_practice/analytics.html:659 msgid "Year" msgstr "Jahr" -#: templates/my_practice/analytics.html:131 +#: templates/my_practice/analytics.html:138 msgid "Income" msgstr "Einnahmen" -#: templates/my_practice/analytics.html:133 +#: templates/my_practice/analytics.html:140 msgid "Profit" msgstr "Gewinn" -#: templates/my_practice/analytics.html:134 +#: templates/my_practice/analytics.html:141 msgid "Cumulative" msgstr "Kumulativ" -#: templates/my_practice/analytics.html:135 -#: templates/my_practice/analytics.html:180 +#: templates/my_practice/analytics.html:142 +#: templates/my_practice/analytics.html:187 msgid "Withdrawals" msgstr "Entnahmen" -#: templates/my_practice/analytics.html:160 +#: templates/my_practice/analytics.html:167 msgid "Revenue, Expenses & Withdrawals" msgstr "Umsatz, Ausgaben & Entnahmen" -#: templates/my_practice/analytics.html:162 +#: templates/my_practice/analytics.html:169 msgid "Complete financial overview" msgstr "Vollständige Finanzübersicht" -#: templates/my_practice/analytics.html:204 +#: templates/my_practice/analytics.html:211 msgid "Revenue Trend (Monthly)" msgstr "Umsatz-Trend (Monatlich)" -#: templates/my_practice/analytics.html:206 +#: templates/my_practice/analytics.html:213 msgid "Monthly revenue development · growth visualized" msgstr "Entwicklung der monatlichen Umsätze · Wachstum visualisiert" -#: templates/my_practice/analytics.html:216 +#: templates/my_practice/analytics.html:223 #, python-format msgid "Monthly revenue · %(months)s months · Max: %(max)s€" msgstr "Monatlicher Umsatz · %(months)s Monate · Max: %(max)s€" -#: templates/my_practice/analytics.html:246 +#: templates/my_practice/analytics.html:260 msgid "Peak Months" msgstr "Stärkste Monate" -#: templates/my_practice/analytics.html:258 -msgid "No data available" -msgstr "Keine Daten verfügbar" - -#: templates/my_practice/analytics.html:278 +#: templates/my_practice/analytics.html:292 msgid "Payment Delay (last 24 months)" msgstr "Zahlungsverzug (letzte 24 Monate)" -#: templates/my_practice/analytics.html:280 +#: templates/my_practice/analytics.html:294 msgid "Average days from invoice date to payment received" msgstr "Durchschnittliche Tage von Rechnungsdatum bis Zahlungseingang" -#: templates/my_practice/analytics.html:310 +#: templates/my_practice/analytics.html:328 msgid "days" msgstr "Tage" -#: templates/my_practice/analytics.html:311 -#: templates/my_practice/analytics.html:332 -#: templates/my_practice/analytics.html:381 +#: templates/my_practice/analytics.html:329 +#: templates/my_practice/analytics.html:350 +#: templates/my_practice/analytics.html:399 msgid "Invoices" msgstr "Rechnungen" -#: templates/my_practice/analytics.html:321 +#: templates/my_practice/analytics.html:339 #: templates/my_practice/client_list_cards.html:6 msgid "Client overview" msgstr "Klientenübersicht" -#: templates/my_practice/analytics.html:325 +#: templates/my_practice/analytics.html:343 msgid "Top 10 Clients by Revenue" msgstr "Top 10 Klienten nach Umsatz" -#: templates/my_practice/analytics.html:330 +#: templates/my_practice/analytics.html:348 msgid "Rank" msgstr "Rang" -#: templates/my_practice/analytics.html:331 -#: templates/my_practice/analytics.html:375 +#: templates/my_practice/analytics.html:349 +#: templates/my_practice/analytics.html:393 #: templates/my_practice/client_list_cards.html:106 #: templates/my_practice/invoice_detail.html:117 msgid "Code" msgstr "Code" -#: templates/my_practice/analytics.html:333 -#: templates/my_practice/analytics.html:575 +#: templates/my_practice/analytics.html:351 +#: templates/my_practice/analytics.html:593 msgid "Hours" msgstr "Stunden" -#: templates/my_practice/analytics.html:334 +#: templates/my_practice/analytics.html:352 #: templates/my_practice/client_detail.html:502 msgid "Total revenue" msgstr "Gesamtumsatz" -#: templates/my_practice/analytics.html:354 +#: templates/my_practice/analytics.html:372 msgid "No revenue data available" msgstr "Keine Umsatzdaten verfügbar" -#: templates/my_practice/analytics.html:361 +#: templates/my_practice/analytics.html:379 #, python-format msgid "Client Details (%(label)s)" msgstr "Klienten-Details (%(label)s)" -#: templates/my_practice/analytics.html:367 +#: templates/my_practice/analytics.html:385 msgid "Show dormant clients" msgstr "Ruhende Klienten anzeigen" -#: templates/my_practice/analytics.html:376 +#: templates/my_practice/analytics.html:394 #: templates/my_practice/client_list_cards.html:107 #: templates/my_practice/inquiry_confirm_delete.html:18 #: templates/my_practice/inquiry_list.html:179 @@ -602,7 +689,7 @@ msgstr "Ruhende Klienten anzeigen" msgid "Name" msgstr "Name" -#: templates/my_practice/analytics.html:377 +#: templates/my_practice/analytics.html:395 #: templates/my_practice/client_detail.html:626 #: templates/my_practice/client_detail.html:670 #: templates/my_practice/inquiry_confirm_delete.html:30 @@ -610,23 +697,23 @@ msgstr "Name" msgid "Status" msgstr "Status" -#: templates/my_practice/analytics.html:378 +#: templates/my_practice/analytics.html:396 msgid "Sessions (period)" msgstr "Sitzungen (Zeitraum)" -#: templates/my_practice/analytics.html:379 +#: templates/my_practice/analytics.html:397 msgid "Total Sessions" msgstr "Gesamt-Sitzungen" -#: templates/my_practice/analytics.html:380 +#: templates/my_practice/analytics.html:398 msgid "Type" msgstr "Typ" -#: templates/my_practice/analytics.html:395 +#: templates/my_practice/analytics.html:413 msgid "Probatoric" msgstr "Probatorik" -#: templates/my_practice/analytics.html:397 +#: templates/my_practice/analytics.html:415 #: templates/my_practice/client_detail.html:11 #: templates/my_practice/client_list_cards.html:34 #: templates/my_practice/dashboard.html:113 @@ -634,252 +721,304 @@ msgstr "Probatorik" msgid "Active" msgstr "Aktiv" -#: templates/my_practice/analytics.html:399 +#: templates/my_practice/analytics.html:417 msgid "Established" msgstr "Etabliert" -#: templates/my_practice/analytics.html:401 +#: templates/my_practice/analytics.html:419 msgid "Dormant" msgstr "Ruhend" -#: templates/my_practice/analytics.html:408 +#: templates/my_practice/analytics.html:426 #: templates/my_practice/client_detail.html:495 msgid "💻 Online" msgstr "💻 Online" -#: templates/my_practice/analytics.html:410 +#: templates/my_practice/analytics.html:428 msgid "In-person" msgstr "Präsenz" -#: templates/my_practice/analytics.html:420 +#: templates/my_practice/analytics.html:438 msgid "No client data for this period." msgstr "Keine Klientendaten für diesen Zeitraum." -#: templates/my_practice/analytics.html:449 +#: templates/my_practice/analytics.html:467 msgid "Session Type Distribution" msgstr "Sitzungstypen-Verteilung" -#: templates/my_practice/analytics.html:456 +#: templates/my_practice/analytics.html:474 msgid "Total sessions" msgstr "Gesamt Sitzungen" -#: templates/my_practice/analytics.html:475 +#: templates/my_practice/analytics.html:493 msgid "No session data available" msgstr "Keine Sitzungsdaten verfügbar" -#: templates/my_practice/analytics.html:487 +#: templates/my_practice/analytics.html:505 msgid "Capacity & Utilisation" msgstr "Kapazität & Auslastung" -#: templates/my_practice/analytics.html:493 +#: templates/my_practice/analytics.html:511 msgid "Key Insights" msgstr "Wichtige Erkenntnisse" -#: templates/my_practice/analytics.html:506 +#: templates/my_practice/analytics.html:524 msgid "Active Clients" msgstr "Aktive Klienten" -#: templates/my_practice/analytics.html:511 +#: templates/my_practice/analytics.html:529 msgid "Booked Sessions" msgstr "Gebuchte Sitzungen" -#: templates/my_practice/analytics.html:516 +#: templates/my_practice/analytics.html:534 msgid "Capacity Used" msgstr "Kapazität genutzt" -#: templates/my_practice/analytics.html:524 +#: templates/my_practice/analytics.html:542 #, python-format msgid "Bookings until %(date)s" msgstr "Buchungen bis %(date)s" -#: templates/my_practice/analytics.html:531 +#: templates/my_practice/analytics.html:549 msgid "Days Off" msgstr "Freie Tage" -#: templates/my_practice/analytics.html:537 +#: templates/my_practice/analytics.html:555 msgid "Capacity Analysis" msgstr "Kapazitätsanalyse" -#: templates/my_practice/analytics.html:537 +#: templates/my_practice/analytics.html:555 #, python-format msgid "until %(date)s" msgstr "bis %(date)s" -#: templates/my_practice/analytics.html:540 +#: templates/my_practice/analytics.html:558 msgid "Note:" msgstr "Hinweis:" -#: templates/my_practice/analytics.html:540 +#: templates/my_practice/analytics.html:558 #, python-format msgid "For future-oriented periods only the period until %(date)s (2 weeks from today) is considered, as most clients book at short notice." msgstr "Für zukunftsgerichtete Zeiträume wird nur der Zeitraum bis %(date)s (2 Wochen ab heute) berücksichtigt, da die meisten Klienten kurzfristig buchen." -#: templates/my_practice/analytics.html:547 +#: templates/my_practice/analytics.html:565 msgid "Available Hours" msgstr "Verfügbare Stunden" -#: templates/my_practice/analytics.html:551 +#: templates/my_practice/analytics.html:569 msgid "Booked Hours" msgstr "Gebuchte Stunden" -#: templates/my_practice/analytics.html:555 +#: templates/my_practice/analytics.html:573 msgid "Free Capacity" msgstr "Freie Kapazität" -#: templates/my_practice/analytics.html:559 +#: templates/my_practice/analytics.html:577 msgid "Working Days" msgstr "Arbeitstage" -#: templates/my_practice/analytics.html:567 +#: templates/my_practice/analytics.html:585 msgid "4-Quarter Trends" msgstr "4-Quartals-Trends" -#: templates/my_practice/analytics.html:572 +#: templates/my_practice/analytics.html:590 msgid "Quarter" msgstr "Quartal" -#: templates/my_practice/analytics.html:573 +#: templates/my_practice/analytics.html:591 #, python-format msgid "Capacity %%" msgstr "Kapazität %%" -#: templates/my_practice/analytics.html:596 +#: templates/my_practice/analytics.html:614 #, python-format msgid "Time off in %(label)s" msgstr "Urlaubszeiten in %(label)s" -#: templates/my_practice/analytics.html:602 -#: templates/my_practice/analytics.html:642 +#: templates/my_practice/analytics.html:620 +#: templates/my_practice/analytics.html:660 msgid "Days" msgstr "Tage" -#: templates/my_practice/analytics.html:614 +#: templates/my_practice/analytics.html:632 #, python-format msgid "Time off %(label)s" msgstr "Urlaub %(label)s" -#: templates/my_practice/analytics.html:616 +#: templates/my_practice/analytics.html:634 msgid "Days off in selected period" msgstr "Urlaubstage im ausgewählten Zeitraum" -#: templates/my_practice/analytics.html:621 +#: templates/my_practice/analytics.html:639 msgid "Total days" msgstr "Tage gesamt" -#: templates/my_practice/analytics.html:625 +#: templates/my_practice/analytics.html:643 msgid "Weeks" msgstr "Wochen" -#: templates/my_practice/analytics.html:629 +#: templates/my_practice/analytics.html:647 msgid "Working days off" msgstr "Arbeitstage frei" -#: templates/my_practice/analytics.html:637 +#: templates/my_practice/analytics.html:655 msgid "Time Off by Year" msgstr "Urlaub nach Jahren" -#: templates/my_practice/analytics.html:643 +#: templates/my_practice/analytics.html:661 msgid "Wk." msgstr "Wo." -#: templates/my_practice/analytics.html:644 +#: templates/my_practice/analytics.html:662 msgid "Time off" msgstr "Urlaub" -#: templates/my_practice/analytics.html:645 +#: templates/my_practice/analytics.html:663 msgid "Training" msgstr "Fortbildung" -#: templates/my_practice/analytics.html:646 +#: templates/my_practice/analytics.html:664 msgid "Sick" msgstr "Krank" -#: templates/my_practice/analytics.html:667 +#: templates/my_practice/analytics.html:685 msgid "Capacity Utilisation Over Time" msgstr "Kapazitätsauslastung über Zeit" -#: templates/my_practice/analytics.html:669 +#: templates/my_practice/analytics.html:687 #, python-format msgid "Monthly utilisation in %% (10h/week until Jul 2023, then 20h/week)" msgstr "Monatliche Auslastung in %% (10h/Woche bis Jul 2023, danach 20h/Woche)" -#: templates/my_practice/analytics.html:704 +#: templates/my_practice/analytics.html:729 msgid "Utilisation" msgstr "Auslastung" -#: templates/my_practice/analytics.html:705 +#: templates/my_practice/analytics.html:730 msgid "Booked" msgstr "Gebucht" -#: templates/my_practice/analytics.html:716 +#: templates/my_practice/analytics.html:741 msgid "Year Comparison – Hours per Month" msgstr "Jahresvergleich – Stunden pro Monat" -#: templates/my_practice/analytics.html:718 +#: templates/my_practice/analytics.html:743 msgid "Therapy hours per calendar month (last 4 years + average)" msgstr "Therapeutenstunden pro Kalendermonat (letzte 4 Jahre + Durchschnitt)" -#: templates/my_practice/analytics.html:820 +#: templates/my_practice/analytics.html:755 +msgid "Jan" +msgstr "Jan" + +#: templates/my_practice/analytics.html:755 +msgid "Feb" +msgstr "Feb" + +#: templates/my_practice/analytics.html:755 +msgid "Mar" +msgstr "Mär" + +#: templates/my_practice/analytics.html:756 +msgid "Apr" +msgstr "Apr" + +#: templates/my_practice/analytics.html:756 +msgid "May" +msgstr "Mai" + +#: templates/my_practice/analytics.html:756 +msgid "Jun" +msgstr "Jun" + +#: templates/my_practice/analytics.html:757 +msgid "Jul" +msgstr "Jul" + +#: templates/my_practice/analytics.html:757 +msgid "Aug" +msgstr "Aug" + +#: templates/my_practice/analytics.html:757 +msgid "Sep" +msgstr "Sep" + +#: templates/my_practice/analytics.html:758 +msgid "Oct" +msgstr "Okt" + +#: templates/my_practice/analytics.html:758 +msgid "Nov" +msgstr "Nov" + +#: templates/my_practice/analytics.html:758 +msgid "Dec" +msgstr "Dez" + +#: templates/my_practice/analytics.html:827 +msgid "Avg" +msgstr "Ø" + +#: templates/my_practice/analytics.html:850 msgid "Seasonality" msgstr "Saisonalität" -#: templates/my_practice/analytics.html:822 +#: templates/my_practice/analytics.html:852 msgid "Average utilisation per calendar month (Ø over all years, incl. time off)" msgstr "Durchschnittliche Auslastung pro Kalendermonat (Ø über alle Jahre, inkl. Urlaub)" -#: templates/my_practice/analytics.html:844 +#: templates/my_practice/analytics.html:874 msgid "Cancellation Rate (last 24 months)" msgstr "Ausfallquote (letzte 24 Monate)" -#: templates/my_practice/analytics.html:846 +#: templates/my_practice/analytics.html:876 #, python-format msgid "Cancelled sessions in %% of all sessions per month" msgstr "Abgesagte Sitzungen in %% aller Sitzungen pro Monat" -#: templates/my_practice/analytics.html:877 +#: templates/my_practice/analytics.html:911 msgid "Cancellation rate" msgstr "Ausfallquote" -#: templates/my_practice/analytics.html:878 +#: templates/my_practice/analytics.html:912 #: templates/my_practice/dashboard.html:151 #: templates/my_practice/invoice_detail.html:43 msgid "Cancelled" msgstr "Storniert" -#: templates/my_practice/analytics.html:889 +#: templates/my_practice/analytics.html:923 msgid "Expense Overview" msgstr "Ausgaben-Übersicht" -#: templates/my_practice/analytics.html:894 +#: templates/my_practice/analytics.html:928 msgid "Manage Expenses" msgstr "Ausgaben verwalten" -#: templates/my_practice/analytics.html:897 +#: templates/my_practice/analytics.html:931 msgid "Manage Withdrawals" msgstr "Entnahmen verwalten" -#: templates/my_practice/analytics.html:903 +#: templates/my_practice/analytics.html:937 msgid "Expenses by Category" msgstr "Ausgaben nach Kategorie" -#: templates/my_practice/analytics.html:910 +#: templates/my_practice/analytics.html:944 msgid "Total Expenses" msgstr "Gesamtausgaben" -#: templates/my_practice/analytics.html:929 +#: templates/my_practice/analytics.html:963 msgid "No expense data available" msgstr "Keine Ausgabendaten verfügbar" -#: templates/my_practice/analytics.html:935 +#: templates/my_practice/analytics.html:969 msgid "Expense Trends" msgstr "Ausgaben-Trends" -#: templates/my_practice/analytics.html:937 +#: templates/my_practice/analytics.html:971 #, python-format msgid "Annual operating expenses %(start_year)s–today" msgstr "Jährliche Betriebsausgaben %(start_year)s–heute" -#: templates/my_practice/analytics.html:1064 +#: templates/my_practice/analytics.html:1098 msgid "Back to overview" msgstr "Zurück zur Übersicht" diff --git a/app/locale/en/LC_MESSAGES/django.po b/app/locale/en/LC_MESSAGES/django.po index ef26e8a..e5d609f 100644 --- a/app/locale/en/LC_MESSAGES/django.po +++ b/app/locale/en/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: my-practice\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-30 17:56+0200\n" +"POT-Creation-Date: 2026-07-02 15:54+0200\n" "PO-Revision-Date: 2026-06-22 23:01+0200\n" "Language-Team: English\n" "Language: en\n" @@ -114,6 +114,83 @@ msgstr "" msgid "Leistungserfassungen" msgstr "" +#: my_practice/utils/practice_analysis.py:229 +#, fuzzy, python-format +#| msgid "Working diagnosis" +msgid "%(n)s working day" +msgid_plural "%(n)s working days" +msgstr[0] "Working diagnosis" +msgstr[1] "Working diagnosis" + +#: my_practice/utils/practice_analysis.py:267 +#, python-format +msgid "📅 Analyzing %(label)s (%(days)s days)" +msgstr "" + +#: my_practice/utils/practice_analysis.py:277 +#, python-format +msgid "👥 %(active)s of %(total)s clients active (%(pct)s%%)" +msgstr "" + +#: my_practice/utils/practice_analysis.py:296 +#, python-format +msgid "⚠️ High concentration: Top 3 clients = %(pct)s%% of sessions" +msgstr "" + +#: my_practice/utils/practice_analysis.py:302 +#, python-format +msgid "📊 Top 3 clients account for %(pct)s%% of sessions" +msgstr "" + +#: my_practice/utils/practice_analysis.py:312 +#, python-format +msgid "📈 Average: %(avg)sh per active client" +msgstr "" + +#: my_practice/utils/practice_analysis.py:320 +#, python-format +msgid "🌱 %(n)s new probatoric client (%(h)sh)" +msgid_plural "🌱 %(n)s new probatoric clients (%(h)sh)" +msgstr[0] "" +msgstr[1] "" + +#: my_practice/utils/practice_analysis.py:329 +#, python-format +msgid "💤 %(n)s dormant clients (no activity this period)" +msgstr "" + +#: my_practice/utils/practice_analysis.py:339 +#, python-format +msgid "📉 Low utilization: Only %(pct)s%% capacity used (%(rem)sh available)" +msgstr "" + +#: my_practice/utils/practice_analysis.py:341 +#, python-format +msgid "📊 Moderate utilization: %(pct)s%% capacity used (%(rem)sh available)" +msgstr "" + +#: my_practice/utils/practice_analysis.py:343 +#, python-format +msgid "✅ Good utilization: %(pct)s%% capacity used (%(rem)sh available)" +msgstr "" + +#: my_practice/utils/practice_analysis.py:345 +#, python-format +msgid "⚠️ High utilization: %(pct)s%% capacity used (only %(rem)sh remaining)" +msgstr "" + +#: my_practice/utils/practice_analysis.py:346 +#, python-format +msgid "🔴 At/over capacity: %(pct)s%% utilized" +msgstr "" + +#: my_practice/utils/practice_analysis.py:352 +#, python-format +msgid "💰 Revenue opportunity: %(n)s client with sessions but no invoices" +msgid_plural "💰 Revenue opportunity: %(n)s clients with sessions but no invoices" +msgstr[0] "" +msgstr[1] "" + #: my_practice/views/client_views.py:172 #, python-brace-format msgid "Client {obj.full_name} saved successfully!" @@ -275,124 +352,124 @@ msgstr "" msgid "%(count)s GebüH code(s) saved for %(date)s." msgstr "" -#: my_practice/views/invoice_views.py:218 +#: my_practice/views/invoice_views.py:219 #, python-format msgid "Invoice %(num)s created successfully!" msgstr "" -#: my_practice/views/invoice_views.py:228 +#: my_practice/views/invoice_views.py:229 #, python-format msgid "Item %(n)s: %(errors)s" msgstr "" -#: my_practice/views/invoice_views.py:233 +#: my_practice/views/invoice_views.py:234 #, python-format msgid "Formset errors: %(errors)s" msgstr "" -#: my_practice/views/invoice_views.py:240 +#: my_practice/views/invoice_views.py:241 #, python-format msgid "Error creating invoice: %(error)s" msgstr "" -#: my_practice/views/invoice_views.py:291 +#: my_practice/views/invoice_views.py:290 #, python-brace-format msgid "Invoice {obj.invoice_number} updated successfully!" msgstr "" -#: my_practice/views/invoice_views.py:368 -#: my_practice/views/invoice_views.py:400 +#: my_practice/views/invoice_views.py:367 +#: my_practice/views/invoice_views.py:399 #, python-format msgid "Item %(n)s - %(field)s: %(error)s" msgstr "" -#: my_practice/views/invoice_views.py:375 -#: my_practice/views/invoice_views.py:407 +#: my_practice/views/invoice_views.py:374 +#: my_practice/views/invoice_views.py:406 #, python-format msgid "Formset: %(error)s" msgstr "" -#: my_practice/views/invoice_views.py:387 +#: my_practice/views/invoice_views.py:386 #, python-format msgid "%(label)s: %(error)s" msgstr "" -#: my_practice/views/invoice_views.py:411 +#: my_practice/views/invoice_views.py:410 msgid "Please correct the errors in the form." msgstr "" -#: my_practice/views/invoice_views.py:431 +#: my_practice/views/invoice_views.py:430 #, python-format msgid "Invoice %(num)s for %(code)s was deleted successfully." msgstr "" -#: my_practice/views/invoice_views.py:456 -#: my_practice/views/invoice_views.py:546 +#: my_practice/views/invoice_views.py:455 +#: my_practice/views/invoice_views.py:545 #, fuzzy #| msgid "+ Session log" msgid "No sessions specified." msgstr "+ Session log" -#: my_practice/views/invoice_views.py:510 +#: my_practice/views/invoice_views.py:509 #, python-format msgid "%(count)s session added to %(invoice)s." msgid_plural "%(count)s sessions added to %(invoice)s." msgstr[0] "" msgstr[1] "" -#: my_practice/views/invoice_views.py:517 +#: my_practice/views/invoice_views.py:516 msgid "No new sessions added (already billed?)." msgstr "" -#: my_practice/views/invoice_views.py:531 -#: my_practice/views/invoice_views.py:793 -#: my_practice/views/invoice_views.py:907 +#: my_practice/views/invoice_views.py:530 +#: my_practice/views/invoice_views.py:803 +#: my_practice/views/invoice_views.py:917 #, fuzzy #| msgid "Create new practice" msgid "No active practice found." msgstr "Create new practice" -#: my_practice/views/invoice_views.py:578 +#: my_practice/views/invoice_views.py:577 msgid "Invoice {} with {} session created." msgid_plural "Invoice {} with {} sessions created." msgstr[0] "" msgstr[1] "" -#: my_practice/views/invoice_views.py:767 +#: my_practice/views/invoice_views.py:777 #, fuzzy #| msgid "Delete session?" msgid "Cancelled session billed" msgstr "Delete session?" -#: my_practice/views/invoice_views.py:769 +#: my_practice/views/invoice_views.py:779 msgid "Appointments pending" msgstr "" -#: my_practice/views/invoice_views.py:771 +#: my_practice/views/invoice_views.py:781 #, fuzzy #| msgid "Unbilled" msgid "Not billed" msgstr "Unbilled" -#: my_practice/views/invoice_views.py:773 -#: templates/my_practice/dashboard.html:134 +#: my_practice/views/invoice_views.py:783 +#: templates/my_practice/dashboard.html:136 #: templates/my_practice/invoice_detail.html:40 msgid "Draft" msgstr "" -#: my_practice/views/invoice_views.py:775 -#: templates/my_practice/dashboard.html:139 +#: my_practice/views/invoice_views.py:785 +#: templates/my_practice/dashboard.html:141 #: templates/my_practice/invoice_detail.html:41 msgid "Sent" msgstr "" -#: my_practice/views/invoice_views.py:777 -#: templates/my_practice/dashboard.html:144 +#: my_practice/views/invoice_views.py:787 +#: templates/my_practice/dashboard.html:146 #: templates/my_practice/invoice_detail.html:42 msgid "Paid" msgstr "" -#: my_practice/views/invoice_views.py:778 +#: my_practice/views/invoice_views.py:788 msgid "OK" msgstr "" @@ -431,209 +508,218 @@ msgstr "" msgid "Revenue, Clients, Capacity & Expenses" msgstr "" -#: templates/my_practice/analytics.html:35 +#: templates/my_practice/analytics.html:15 +#: templates/my_practice/analytics.html:272 +msgid "No data available" +msgstr "" + +#: templates/my_practice/analytics.html:16 +msgid "No data in selected period" +msgstr "" + +#: templates/my_practice/analytics.html:17 +msgid "No valid data available" +msgstr "" + +#: templates/my_practice/analytics.html:42 msgid "Period" msgstr "" -#: templates/my_practice/analytics.html:38 +#: templates/my_practice/analytics.html:45 #, python-format msgid "All years (%(year)s–today)" msgstr "" -#: templates/my_practice/analytics.html:39 +#: templates/my_practice/analytics.html:46 msgid "Last year" msgstr "" -#: templates/my_practice/analytics.html:40 +#: templates/my_practice/analytics.html:47 msgid "Last quarter" msgstr "" -#: templates/my_practice/analytics.html:41 +#: templates/my_practice/analytics.html:48 #, fuzzy #| msgid "Delete session?" msgid "Last month" msgstr "Delete session?" -#: templates/my_practice/analytics.html:42 +#: templates/my_practice/analytics.html:49 msgid "Custom" msgstr "" -#: templates/my_practice/analytics.html:49 +#: templates/my_practice/analytics.html:56 msgid "From" msgstr "" -#: templates/my_practice/analytics.html:55 +#: templates/my_practice/analytics.html:62 msgid "To" msgstr "" -#: templates/my_practice/analytics.html:62 +#: templates/my_practice/analytics.html:69 msgid "Apply" msgstr "" -#: templates/my_practice/analytics.html:66 +#: templates/my_practice/analytics.html:73 msgid "Reset" msgstr "" -#: templates/my_practice/analytics.html:94 -#: templates/my_practice/analytics.html:178 +#: templates/my_practice/analytics.html:101 +#: templates/my_practice/analytics.html:185 msgid "Revenue" msgstr "" -#: templates/my_practice/analytics.html:97 -#: templates/my_practice/analytics.html:574 +#: templates/my_practice/analytics.html:104 +#: templates/my_practice/analytics.html:592 #: templates/my_practice/client_list_cards.html:8 msgid "Clients" msgstr "" -#: templates/my_practice/analytics.html:100 -#: templates/my_practice/analytics.html:706 +#: templates/my_practice/analytics.html:107 +#: templates/my_practice/analytics.html:731 msgid "Capacity" msgstr "" -#: templates/my_practice/analytics.html:103 -#: templates/my_practice/analytics.html:132 -#: templates/my_practice/analytics.html:179 +#: templates/my_practice/analytics.html:110 +#: templates/my_practice/analytics.html:139 +#: templates/my_practice/analytics.html:186 msgid "Expenses" msgstr "" -#: templates/my_practice/analytics.html:110 +#: templates/my_practice/analytics.html:117 msgid "Revenue & Profit" msgstr "" -#: templates/my_practice/analytics.html:115 +#: templates/my_practice/analytics.html:122 msgid "Detailed Revenue Report" msgstr "" -#: templates/my_practice/analytics.html:121 +#: templates/my_practice/analytics.html:128 msgid "Profit (Revenue - Expenses)" msgstr "" -#: templates/my_practice/analytics.html:123 +#: templates/my_practice/analytics.html:130 #, python-format msgid "Business profit %(start_year)s–today · Cumulative development" msgstr "" -#: templates/my_practice/analytics.html:130 -#: templates/my_practice/analytics.html:641 +#: templates/my_practice/analytics.html:137 +#: templates/my_practice/analytics.html:659 msgid "Year" msgstr "" -#: templates/my_practice/analytics.html:131 +#: templates/my_practice/analytics.html:138 msgid "Income" msgstr "" -#: templates/my_practice/analytics.html:133 +#: templates/my_practice/analytics.html:140 #, fuzzy #| msgid "Profile" msgid "Profit" msgstr "Profile" -#: templates/my_practice/analytics.html:134 +#: templates/my_practice/analytics.html:141 msgid "Cumulative" msgstr "" -#: templates/my_practice/analytics.html:135 -#: templates/my_practice/analytics.html:180 +#: templates/my_practice/analytics.html:142 +#: templates/my_practice/analytics.html:187 msgid "Withdrawals" msgstr "" -#: templates/my_practice/analytics.html:160 +#: templates/my_practice/analytics.html:167 msgid "Revenue, Expenses & Withdrawals" msgstr "" -#: templates/my_practice/analytics.html:162 +#: templates/my_practice/analytics.html:169 #, fuzzy #| msgid "Preview" msgid "Complete financial overview" msgstr "Preview" -#: templates/my_practice/analytics.html:204 +#: templates/my_practice/analytics.html:211 msgid "Revenue Trend (Monthly)" msgstr "" -#: templates/my_practice/analytics.html:206 +#: templates/my_practice/analytics.html:213 msgid "Monthly revenue development · growth visualized" msgstr "" -#: templates/my_practice/analytics.html:216 +#: templates/my_practice/analytics.html:223 #, python-format msgid "Monthly revenue · %(months)s months · Max: %(max)s€" msgstr "" -#: templates/my_practice/analytics.html:246 +#: templates/my_practice/analytics.html:260 msgid "Peak Months" msgstr "" -#: templates/my_practice/analytics.html:258 -msgid "No data available" -msgstr "" - -#: templates/my_practice/analytics.html:278 +#: templates/my_practice/analytics.html:292 msgid "Payment Delay (last 24 months)" msgstr "" -#: templates/my_practice/analytics.html:280 +#: templates/my_practice/analytics.html:294 msgid "Average days from invoice date to payment received" msgstr "" -#: templates/my_practice/analytics.html:310 +#: templates/my_practice/analytics.html:328 msgid "days" msgstr "" -#: templates/my_practice/analytics.html:311 -#: templates/my_practice/analytics.html:332 -#: templates/my_practice/analytics.html:381 +#: templates/my_practice/analytics.html:329 +#: templates/my_practice/analytics.html:350 +#: templates/my_practice/analytics.html:399 #, fuzzy #| msgid "🧾 To invoice" msgid "Invoices" msgstr "🧾 To invoice" -#: templates/my_practice/analytics.html:321 +#: templates/my_practice/analytics.html:339 #: templates/my_practice/client_list_cards.html:6 #, fuzzy #| msgid "Preview" msgid "Client overview" msgstr "Preview" -#: templates/my_practice/analytics.html:325 +#: templates/my_practice/analytics.html:343 msgid "Top 10 Clients by Revenue" msgstr "" -#: templates/my_practice/analytics.html:330 +#: templates/my_practice/analytics.html:348 msgid "Rank" msgstr "" -#: templates/my_practice/analytics.html:331 -#: templates/my_practice/analytics.html:375 +#: templates/my_practice/analytics.html:349 +#: templates/my_practice/analytics.html:393 #: templates/my_practice/client_list_cards.html:106 #: templates/my_practice/invoice_detail.html:117 msgid "Code" msgstr "" -#: templates/my_practice/analytics.html:333 -#: templates/my_practice/analytics.html:575 +#: templates/my_practice/analytics.html:351 +#: templates/my_practice/analytics.html:593 msgid "Hours" msgstr "" -#: templates/my_practice/analytics.html:334 +#: templates/my_practice/analytics.html:352 #: templates/my_practice/client_detail.html:502 msgid "Total revenue" msgstr "" -#: templates/my_practice/analytics.html:354 +#: templates/my_practice/analytics.html:372 msgid "No revenue data available" msgstr "" -#: templates/my_practice/analytics.html:361 +#: templates/my_practice/analytics.html:379 #, python-format msgid "Client Details (%(label)s)" msgstr "" -#: templates/my_practice/analytics.html:367 +#: templates/my_practice/analytics.html:385 msgid "Show dormant clients" msgstr "" -#: templates/my_practice/analytics.html:376 +#: templates/my_practice/analytics.html:394 #: templates/my_practice/client_list_cards.html:107 #: templates/my_practice/inquiry_confirm_delete.html:18 #: templates/my_practice/inquiry_list.html:179 @@ -641,7 +727,7 @@ msgstr "" msgid "Name" msgstr "Name" -#: templates/my_practice/analytics.html:377 +#: templates/my_practice/analytics.html:395 #: templates/my_practice/client_detail.html:626 #: templates/my_practice/client_detail.html:670 #: templates/my_practice/inquiry_confirm_delete.html:30 @@ -649,302 +735,354 @@ msgstr "Name" msgid "Status" msgstr "Status" -#: templates/my_practice/analytics.html:378 +#: templates/my_practice/analytics.html:396 #, fuzzy #| msgid "+ Session log" msgid "Sessions (period)" msgstr "+ Session log" -#: templates/my_practice/analytics.html:379 +#: templates/my_practice/analytics.html:397 #, fuzzy #| msgid "+ Session log" msgid "Total Sessions" msgstr "+ Session log" -#: templates/my_practice/analytics.html:380 +#: templates/my_practice/analytics.html:398 msgid "Type" msgstr "" -#: templates/my_practice/analytics.html:395 +#: templates/my_practice/analytics.html:413 msgid "Probatoric" msgstr "" -#: templates/my_practice/analytics.html:397 +#: templates/my_practice/analytics.html:415 #: templates/my_practice/client_detail.html:11 #: templates/my_practice/client_list_cards.html:34 -#: templates/my_practice/dashboard.html:111 +#: templates/my_practice/dashboard.html:113 #: templates/my_practice/practice_management.html:26 msgid "Active" msgstr "Active" -#: templates/my_practice/analytics.html:399 +#: templates/my_practice/analytics.html:417 msgid "Established" msgstr "" -#: templates/my_practice/analytics.html:401 +#: templates/my_practice/analytics.html:419 msgid "Dormant" msgstr "" -#: templates/my_practice/analytics.html:408 +#: templates/my_practice/analytics.html:426 #: templates/my_practice/client_detail.html:495 msgid "💻 Online" msgstr "" -#: templates/my_practice/analytics.html:410 +#: templates/my_practice/analytics.html:428 msgid "In-person" msgstr "" -#: templates/my_practice/analytics.html:420 +#: templates/my_practice/analytics.html:438 msgid "No client data for this period." msgstr "" -#: templates/my_practice/analytics.html:449 +#: templates/my_practice/analytics.html:467 #, fuzzy #| msgid "+ Session log" msgid "Session Type Distribution" msgstr "+ Session log" -#: templates/my_practice/analytics.html:456 +#: templates/my_practice/analytics.html:474 #, fuzzy #| msgid "+ Session log" msgid "Total sessions" msgstr "+ Session log" -#: templates/my_practice/analytics.html:475 +#: templates/my_practice/analytics.html:493 msgid "No session data available" msgstr "" -#: templates/my_practice/analytics.html:487 +#: templates/my_practice/analytics.html:505 msgid "Capacity & Utilisation" msgstr "" -#: templates/my_practice/analytics.html:493 +#: templates/my_practice/analytics.html:511 msgid "Key Insights" msgstr "" -#: templates/my_practice/analytics.html:506 +#: templates/my_practice/analytics.html:524 #, fuzzy #| msgid "Manage practices" msgid "Active Clients" msgstr "Manage practices" -#: templates/my_practice/analytics.html:511 +#: templates/my_practice/analytics.html:529 #, fuzzy #| msgid "+ Session log" msgid "Booked Sessions" msgstr "+ Session log" -#: templates/my_practice/analytics.html:516 +#: templates/my_practice/analytics.html:534 msgid "Capacity Used" msgstr "" -#: templates/my_practice/analytics.html:524 +#: templates/my_practice/analytics.html:542 #, python-format msgid "Bookings until %(date)s" msgstr "" -#: templates/my_practice/analytics.html:531 +#: templates/my_practice/analytics.html:549 msgid "Days Off" msgstr "" -#: templates/my_practice/analytics.html:537 +#: templates/my_practice/analytics.html:555 msgid "Capacity Analysis" msgstr "" -#: templates/my_practice/analytics.html:537 +#: templates/my_practice/analytics.html:555 #, python-format msgid "until %(date)s" msgstr "" -#: templates/my_practice/analytics.html:540 +#: templates/my_practice/analytics.html:558 #, fuzzy #| msgid "+ Note" msgid "Note:" msgstr "+ Note" -#: templates/my_practice/analytics.html:540 +#: templates/my_practice/analytics.html:558 #, python-format msgid "For future-oriented periods only the period until %(date)s (2 weeks from today) is considered, as most clients book at short notice." msgstr "" -#: templates/my_practice/analytics.html:547 +#: templates/my_practice/analytics.html:565 msgid "Available Hours" msgstr "" -#: templates/my_practice/analytics.html:551 +#: templates/my_practice/analytics.html:569 msgid "Booked Hours" msgstr "" -#: templates/my_practice/analytics.html:555 +#: templates/my_practice/analytics.html:573 msgid "Free Capacity" msgstr "" -#: templates/my_practice/analytics.html:559 +#: templates/my_practice/analytics.html:577 #, fuzzy #| msgid "Working diagnosis" msgid "Working Days" msgstr "Working diagnosis" -#: templates/my_practice/analytics.html:567 +#: templates/my_practice/analytics.html:585 msgid "4-Quarter Trends" msgstr "" -#: templates/my_practice/analytics.html:572 +#: templates/my_practice/analytics.html:590 msgid "Quarter" msgstr "" -#: templates/my_practice/analytics.html:573 +#: templates/my_practice/analytics.html:591 #, python-format msgid "Capacity %%" msgstr "" -#: templates/my_practice/analytics.html:596 +#: templates/my_practice/analytics.html:614 #, python-format msgid "Time off in %(label)s" msgstr "" -#: templates/my_practice/analytics.html:602 -#: templates/my_practice/analytics.html:642 +#: templates/my_practice/analytics.html:620 +#: templates/my_practice/analytics.html:660 msgid "Days" msgstr "" -#: templates/my_practice/analytics.html:614 +#: templates/my_practice/analytics.html:632 #, python-format msgid "Time off %(label)s" msgstr "" -#: templates/my_practice/analytics.html:616 +#: templates/my_practice/analytics.html:634 msgid "Days off in selected period" msgstr "" -#: templates/my_practice/analytics.html:621 +#: templates/my_practice/analytics.html:639 msgid "Total days" msgstr "" -#: templates/my_practice/analytics.html:625 +#: templates/my_practice/analytics.html:643 msgid "Weeks" msgstr "" -#: templates/my_practice/analytics.html:629 +#: templates/my_practice/analytics.html:647 #, fuzzy #| msgid "Working diagnosis" msgid "Working days off" msgstr "Working diagnosis" -#: templates/my_practice/analytics.html:637 +#: templates/my_practice/analytics.html:655 msgid "Time Off by Year" msgstr "" -#: templates/my_practice/analytics.html:643 +#: templates/my_practice/analytics.html:661 msgid "Wk." msgstr "" -#: templates/my_practice/analytics.html:644 +#: templates/my_practice/analytics.html:662 msgid "Time off" msgstr "" -#: templates/my_practice/analytics.html:645 +#: templates/my_practice/analytics.html:663 msgid "Training" msgstr "" -#: templates/my_practice/analytics.html:646 +#: templates/my_practice/analytics.html:664 msgid "Sick" msgstr "" -#: templates/my_practice/analytics.html:667 +#: templates/my_practice/analytics.html:685 msgid "Capacity Utilisation Over Time" msgstr "" -#: templates/my_practice/analytics.html:669 +#: templates/my_practice/analytics.html:687 #, python-format msgid "Monthly utilisation in %% (10h/week until Jul 2023, then 20h/week)" msgstr "" -#: templates/my_practice/analytics.html:704 +#: templates/my_practice/analytics.html:729 msgid "Utilisation" msgstr "" -#: templates/my_practice/analytics.html:705 +#: templates/my_practice/analytics.html:730 msgid "Booked" msgstr "" -#: templates/my_practice/analytics.html:716 +#: templates/my_practice/analytics.html:741 msgid "Year Comparison – Hours per Month" msgstr "" -#: templates/my_practice/analytics.html:718 +#: templates/my_practice/analytics.html:743 msgid "Therapy hours per calendar month (last 4 years + average)" msgstr "" -#: templates/my_practice/analytics.html:820 +#: templates/my_practice/analytics.html:755 +msgid "Jan" +msgstr "" + +#: templates/my_practice/analytics.html:755 +msgid "Feb" +msgstr "" + +#: templates/my_practice/analytics.html:755 +msgid "Mar" +msgstr "" + +#: templates/my_practice/analytics.html:756 +msgid "Apr" +msgstr "" + +#: templates/my_practice/analytics.html:756 +msgid "May" +msgstr "" + +#: templates/my_practice/analytics.html:756 +msgid "Jun" +msgstr "" + +#: templates/my_practice/analytics.html:757 +msgid "Jul" +msgstr "" + +#: templates/my_practice/analytics.html:757 +msgid "Aug" +msgstr "" + +#: templates/my_practice/analytics.html:757 +msgid "Sep" +msgstr "" + +#: templates/my_practice/analytics.html:758 +msgid "Oct" +msgstr "" + +#: templates/my_practice/analytics.html:758 +msgid "Nov" +msgstr "" + +#: templates/my_practice/analytics.html:758 +msgid "Dec" +msgstr "" + +#: templates/my_practice/analytics.html:827 +msgid "Avg" +msgstr "" + +#: templates/my_practice/analytics.html:850 msgid "Seasonality" msgstr "" -#: templates/my_practice/analytics.html:822 +#: templates/my_practice/analytics.html:852 msgid "Average utilisation per calendar month (Ø over all years, incl. time off)" msgstr "" -#: templates/my_practice/analytics.html:844 +#: templates/my_practice/analytics.html:874 msgid "Cancellation Rate (last 24 months)" msgstr "" -#: templates/my_practice/analytics.html:846 +#: templates/my_practice/analytics.html:876 #, python-format msgid "Cancelled sessions in %% of all sessions per month" msgstr "" -#: templates/my_practice/analytics.html:877 +#: templates/my_practice/analytics.html:911 #, fuzzy #| msgid "Cancel" msgid "Cancellation rate" msgstr "Cancel" -#: templates/my_practice/analytics.html:878 -#: templates/my_practice/dashboard.html:149 +#: templates/my_practice/analytics.html:912 +#: templates/my_practice/dashboard.html:151 #: templates/my_practice/invoice_detail.html:43 #, fuzzy #| msgid "Cancel" msgid "Cancelled" msgstr "Cancel" -#: templates/my_practice/analytics.html:889 +#: templates/my_practice/analytics.html:923 #, fuzzy #| msgid "Preview" msgid "Expense Overview" msgstr "Preview" -#: templates/my_practice/analytics.html:894 +#: templates/my_practice/analytics.html:928 #, fuzzy #| msgid "Manage practices" msgid "Manage Expenses" msgstr "Manage practices" -#: templates/my_practice/analytics.html:897 +#: templates/my_practice/analytics.html:931 msgid "Manage Withdrawals" msgstr "" -#: templates/my_practice/analytics.html:903 +#: templates/my_practice/analytics.html:937 msgid "Expenses by Category" msgstr "" -#: templates/my_practice/analytics.html:910 +#: templates/my_practice/analytics.html:944 msgid "Total Expenses" msgstr "" -#: templates/my_practice/analytics.html:929 +#: templates/my_practice/analytics.html:963 msgid "No expense data available" msgstr "" -#: templates/my_practice/analytics.html:935 +#: templates/my_practice/analytics.html:969 msgid "Expense Trends" msgstr "" -#: templates/my_practice/analytics.html:937 +#: templates/my_practice/analytics.html:971 #, python-format msgid "Annual operating expenses %(start_year)s–today" msgstr "" -#: templates/my_practice/analytics.html:1064 +#: templates/my_practice/analytics.html:1098 #, fuzzy #| msgid "Preview" msgid "Back to overview" @@ -1775,21 +1913,25 @@ msgstr "Manage practices" msgid "Needs Action" msgstr "Active" -#: templates/my_practice/dashboard.html:81 +#: templates/my_practice/dashboard.html:65 +msgid "by urgency" +msgstr "" + +#: templates/my_practice/dashboard.html:83 msgid "Nothing needs your attention right now." msgstr "" -#: templates/my_practice/dashboard.html:99 +#: templates/my_practice/dashboard.html:101 #, fuzzy, python-format #| msgid "My practices (%(count)s)" msgid "Practices %(year)s" msgstr "My practices (%(count)s)" -#: templates/my_practice/dashboard.html:100 +#: templates/my_practice/dashboard.html:102 msgid "Manage" msgstr "" -#: templates/my_practice/dashboard.html:118 +#: templates/my_practice/dashboard.html:120 #, fuzzy, python-format #| msgid "🧾 To invoice" msgid "%(n)s invoice" @@ -1797,24 +1939,24 @@ msgid_plural "%(n)s invoices" msgstr[0] "🧾 To invoice" msgstr[1] "🧾 To invoice" -#: templates/my_practice/dashboard.html:121 +#: templates/my_practice/dashboard.html:123 #: templates/my_practice/practice_management.html:39 msgid "Switch" msgstr "Switch" -#: templates/my_practice/dashboard.html:131 +#: templates/my_practice/dashboard.html:133 #, fuzzy #| msgid "Preview" msgid "All-time Status Overview" msgstr "Preview" -#: templates/my_practice/dashboard.html:158 +#: templates/my_practice/dashboard.html:160 #, fuzzy #| msgid "🧾 To invoice" msgid "Recent Invoices" msgstr "🧾 To invoice" -#: templates/my_practice/dashboard.html:172 +#: templates/my_practice/dashboard.html:174 #, fuzzy #| msgid "No supervision topics for this client yet." msgid "No invoices yet." diff --git a/app/my_practice/utils/analytics_utils.py b/app/my_practice/utils/analytics_utils.py index a48045f..b0e6b9c 100644 --- a/app/my_practice/utils/analytics_utils.py +++ b/app/my_practice/utils/analytics_utils.py @@ -50,6 +50,14 @@ def _get_monthly_aggregation( if end_date is None: end_date = date.today() + # Only show complete months — exclude the current partial month so the last + # data point isn't anomalously low (same guard as get_capacity_trends). + from datetime import timedelta + + first_of_current_month = date.today().replace(day=1) + if end_date >= first_of_current_month: + end_date = first_of_current_month - timedelta(days=1) + if start_date is None: start_date = date(start_year, 1, 1) diff --git a/app/my_practice/utils/capacity_helpers.py b/app/my_practice/utils/capacity_helpers.py index a82f473..867a2dd 100644 --- a/app/my_practice/utils/capacity_helpers.py +++ b/app/my_practice/utils/capacity_helpers.py @@ -25,6 +25,14 @@ ] +def _build_holiday_set(start_year: int, end_year: int) -> set[date]: + """Build the set of Berlin public holidays covering all years in [start_year, end_year].""" + holidays: set[date] = set() + for yr in range(start_year, end_year + 1): + holidays |= berlin_public_holidays(yr) + return holidays + + def get_weekly_capacity_for_date(target_date: date) -> float: """ Get the weekly capacity (hours) for a specific date. @@ -86,9 +94,7 @@ def calculate_period_capacity( effective_end_date = end_date # Build holiday set once for the effective date range - _holidays: set[date] = set() - for yr in range(start_date.year, effective_end_date.year + 1): - _holidays |= berlin_public_holidays(yr) + _holidays = _build_holiday_set(start_date.year, effective_end_date.year) # Working days in effective period, excluding public holidays working_days = DateRangeHelper.count_working_days(start_date, effective_end_date, _holidays) @@ -265,6 +271,12 @@ def get_capacity_trends(start_year=2020, end_date=None, start_date=None, practic if end_date is None: end_date = date.today() + # Only show complete months — exclude the current partial month so the last + # data point isn't anomalously low (e.g. 2 working days = 8h capacity). + first_of_current_month = date.today().replace(day=1) + if end_date >= first_of_current_month: + end_date = first_of_current_month - timedelta(days=1) + if start_date is None: start_date = date(start_year, 1, 1) @@ -297,9 +309,7 @@ def get_capacity_trends(start_year=2020, end_date=None, start_date=None, practic ) # Pre-build holiday set covering all years in the range (used in the loop below) - _holidays: set[date] = set() - for yr in range(start_date.year, end_date.year + 1): - _holidays |= berlin_public_holidays(yr) + _holidays = _build_holiday_set(start_date.year, end_date.year) # Build capacity data iterating through months (no DB queries in loop) capacity_data = [] @@ -308,15 +318,7 @@ def get_capacity_trends(start_year=2020, end_date=None, start_date=None, practic while current_date <= end_date: # Calculate month boundaries month_start = current_date.replace(day=1) - if current_date.month == 12: - next_month = current_date.replace(year=current_date.year + 1, month=1, day=1) - else: - next_month = current_date.replace(month=current_date.month + 1, day=1) - month_end = next_month - timedelta(days=1) - - # Don't go beyond end_date - if month_end > end_date: - month_end = end_date + month_end = DateRangeHelper.get_last_of_month(current_date) # Working days in the month, excluding public holidays working_days = DateRangeHelper.count_working_days(month_start, month_end, _holidays) @@ -333,10 +335,10 @@ def get_capacity_trends(start_year=2020, end_date=None, start_date=None, practic available_working_days = max(0, working_days - timeoff_days) - # Calculate capacity based on period configuration - hours_per_week = get_weekly_capacity_for_date(month_start) - available_weeks = available_working_days / 5 - usable_capacity = available_weeks * hours_per_week + # Delegate to the same weighted formula used by calculate_period_capacity + usable_capacity = _calculate_weighted_capacity( + month_start, month_end, available_working_days, _holidays + ) # Get booked hours from cached data month_key = format_month_key(current_date) diff --git a/app/my_practice/utils/practice_analysis.py b/app/my_practice/utils/practice_analysis.py index 0af261a..7b49da3 100644 --- a/app/my_practice/utils/practice_analysis.py +++ b/app/my_practice/utils/practice_analysis.py @@ -10,6 +10,7 @@ from dateutil.relativedelta import relativedelta from django.db.models import FloatField, Sum from django.db.models.functions import Cast +from django.utils.translation import gettext as _, ngettext from ..models import Client, Invoice from ..models.session import Session @@ -224,7 +225,12 @@ def _get_timeoff_data(self): "total_weeks": result["total_weeks"], "workdays": result["total_workdays"], "entries": result["entries"], - "capacity_impact": f"{result['total_workdays']} Arbeitstage", + "capacity_impact": ngettext( + "%(n)s working day", + "%(n)s working days", + result["total_workdays"], + ) + % {"n": result["total_workdays"]}, } def _calculate_capacity(self, period_sessions): @@ -259,14 +265,23 @@ def generate_insights(self, analysis) -> list[str]: def _period_insights(self, analysis) -> list[str]: insights = [ - f"📅 Analyzing {analysis['period']['label']} ({analysis['period']['days']} days)" + _("📅 Analyzing %(label)s (%(days)s days)") + % { + "label": analysis["period"]["label"], + "days": analysis["period"]["days"], + } ] active_count = analysis["clients"]["active_in_period"] total_count = analysis["clients"]["total"] if active_count > 0: active_pct = (active_count / total_count * 100) if total_count > 0 else 0 insights.append( - f"👥 {active_count} of {total_count} clients active ({active_pct:.0f}%)" + _("👥 %(active)s of %(total)s clients active (%(pct)s%%)") + % { + "active": active_count, + "total": total_count, + "pct": f"{active_pct:.0f}", + } ) return insights @@ -281,51 +296,73 @@ def _client_insights(self, clients) -> list[str]: concentration = (top_3 / total_sessions) * 100 if concentration > 60: insights.append( - f"⚠️ High concentration: Top 3 clients = {concentration:.0f}% of sessions" + _("⚠️ High concentration: Top 3 clients = %(pct)s%% of sessions") + % {"pct": f"{concentration:.0f}"} ) elif concentration > 40: insights.append( - f"📊 Top 3 clients account for {concentration:.0f}% of sessions" + _("📊 Top 3 clients account for %(pct)s%% of sessions") + % {"pct": f"{concentration:.0f}"} ) # Average per active client active = [c for c in clients if c["classification"] in ["established", "probatoric"]] if active: avg = sum(c["sessions_in_period"] for c in active) / len(active) - insights.append(f"📈 Average: {avg:.1f}h per active client") + insights.append(_("📈 Average: %(avg)sh per active client") % {"avg": f"{avg:.1f}"}) # Probatoric probatoric = [c for c in clients if c["classification"] == "probatoric"] if probatoric: ph = sum(c["sessions_in_period"] for c in probatoric) - s = "s" if len(probatoric) != 1 else "" - insights.append(f"🌱 {len(probatoric)} new probatoric client{s} ({ph:.1f}h)") + insights.append( + ngettext( + "🌱 %(n)s new probatoric client (%(h)sh)", + "🌱 %(n)s new probatoric clients (%(h)sh)", + len(probatoric), + ) + % {"n": len(probatoric), "h": f"{ph:.1f}"} + ) # Dormant dormant = [c for c in clients if c["classification"] == "dormant"] if len(dormant) > 5: - insights.append(f"💤 {len(dormant)} dormant clients (no activity this period)") + insights.append( + _("💤 %(n)s dormant clients (no activity this period)") % {"n": len(dormant)} + ) return insights def _capacity_insights(self, capacity) -> list[str]: cap_pct = capacity["capacity_percentage"] rem = capacity["remaining_hours"] + vals = {"pct": cap_pct, "rem": f"{rem:.0f}"} if cap_pct < 30: - return [f"📉 Low utilization: Only {cap_pct}% capacity used ({rem:.0f}h available)"] + return [ + _("📉 Low utilization: Only %(pct)s%% capacity used (%(rem)sh available)") % vals + ] if cap_pct < 60: - return [f"📊 Moderate utilization: {cap_pct}% capacity used ({rem:.0f}h available)"] + return [ + _("📊 Moderate utilization: %(pct)s%% capacity used (%(rem)sh available)") % vals + ] if cap_pct < 80: - return [f"✅ Good utilization: {cap_pct}% capacity used ({rem:.0f}h available)"] + return [_("✅ Good utilization: %(pct)s%% capacity used (%(rem)sh available)") % vals] if cap_pct < 100: - return [f"⚠️ High utilization: {cap_pct}% capacity used (only {rem:.0f}h remaining)"] - return [f"🔴 At/over capacity: {cap_pct}% utilized"] + return [ + _("⚠️ High utilization: %(pct)s%% capacity used (only %(rem)sh remaining)") % vals + ] + return [_("🔴 At/over capacity: %(pct)s%% utilized") % vals] def _revenue_insights(self, clients) -> list[str]: unbilled = [c for c in clients if c["invoices_count"] == 0 and c["sessions_in_period"] > 0] if unbilled: return [ - f"💰 Revenue opportunity: {len(unbilled)} client(s) with sessions but no invoices" + ngettext( + "💰 Revenue opportunity: %(n)s client with sessions but no invoices", + "💰 Revenue opportunity: %(n)s clients with sessions but no invoices", + len(unbilled), + ) + % {"n": len(unbilled)} ] return [] diff --git a/app/static/js/chart_utils.test.extended.js b/app/static/js/chart_utils.test.extended.js index afa40f8..2e0a11f 100644 --- a/app/static/js/chart_utils.test.extended.js +++ b/app/static/js/chart_utils.test.extended.js @@ -5,7 +5,6 @@ // Load the functions from refactored chart_math module const { - calculateYearLabels, parseMonthString, findFirstNonZeroIndex, calculatePoints, @@ -34,16 +33,6 @@ function assertEquals(actual, expected, message) { console.log('\n📊 Additional Chart Utils Tests\n'); // Edge case tests -test('calculateYearLabels - empty data returns empty array', () => { - const labels = calculateYearLabels(0, 2020, 12); - assertEquals(labels.length, 0, 'Should have 0 labels for 0 months'); -}); - -test('calculateYearLabels - single month', () => { - const labels = calculateYearLabels(1, 2020, 12); - assertEquals(labels, [{ index: 0, year: 2020 }], 'Should have single label'); -}); - test('parseMonthString - handles all month names', () => { const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; months.forEach((month, idx) => { diff --git a/app/static/js/chart_utils.test.js b/app/static/js/chart_utils.test.js index f34b0e2..6974098 100644 --- a/app/static/js/chart_utils.test.js +++ b/app/static/js/chart_utils.test.js @@ -10,7 +10,6 @@ if (typeof module === 'undefined') { // Load the functions from refactored chart_math module const { - calculateYearLabels, parseMonthString, findFirstNonZeroIndex, calculatePoints, @@ -45,21 +44,6 @@ function assertNotNull(value, message) { // Tests console.log('\n📊 Running Chart Utils Tests\n'); -test('calculateYearLabels - basic case with 24 months', () => { - // 24 data points = indices 0-23, so labels at index 0 (2020) and 12 (2021) - const labels = calculateYearLabels(24, 2020, 12); - assertEquals(labels.length, 2, 'Should have 2 labels'); - assertEquals(labels[0], { index: 0, year: 2020 }, 'First label'); - assertEquals(labels[1], { index: 12, year: 2021 }, 'Second label'); -}); - -test('calculateYearLabels - 63 months starting 2020', () => { - const labels = calculateYearLabels(63, 2020, 12); - assertEquals(labels.length, 6, 'Should have 6 labels'); - assertEquals(labels[0].year, 2020, 'First year'); - assertEquals(labels[5].year, 2025, 'Last year'); -}); - test('parseMonthString - YYYY-MM format', () => { const result = parseMonthString('2020-10'); assertNotNull(result, 'Should parse successfully'); diff --git a/app/static/js/charts/chart_bar.js b/app/static/js/charts/chart_bar.js index 6f9e599..3210fe3 100644 --- a/app/static/js/charts/chart_bar.js +++ b/app/static/js/charts/chart_bar.js @@ -77,42 +77,17 @@ function drawSimpleBarChart(ctx, padding, chartWidth, chartHeight, height, label ctx.fillText(text, textX, textY); } - // Draw x-axis label directly - ctx.fillStyle = getCSSVariable('--text-primary'); - ctx.font = 'bold 13px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(String(labels[index]), x + barWidth / 2, height - padding.bottom + 25); + // Draw x-axis label + drawYearLabel(ctx, labels[index], x + barWidth / 2, height, padding); }); return positions; } -/** - * Setup basic bar chart infrastructure - */ -function setupBarChart(canvasOrId, options = {}) { - // Handle both canvas element and ID - const canvas = typeof canvasOrId === 'string' ? document.getElementById(canvasOrId) : canvasOrId; - if (!canvas) return null; - - const padding = options.padding || { top: 35, right: 20, bottom: 60, left: 60 }; - const setup = setupCanvas(canvas, padding); - if (!setup) return null; - - const { ctx, chartWidth, chartHeight } = setup; - - // Draw grid, axes - drawGrid(ctx, setup.padding, chartWidth, chartHeight); - drawAxes(ctx, setup.padding, chartWidth, chartHeight); - - return setup; -} - // Export functions if (typeof module !== 'undefined' && module.exports) { module.exports = { drawGroupedBars, drawSimpleBarChart, - setupBarChart }; } diff --git a/app/static/js/charts/chart_builder.js b/app/static/js/charts/chart_builder.js index 44bc3d2..0680a0e 100644 --- a/app/static/js/charts/chart_builder.js +++ b/app/static/js/charts/chart_builder.js @@ -245,8 +245,9 @@ class ChartBuilder { // Draw year separators and labels if needed if (this.config.options.showYearSeparators) { const startYear = this.config.options.startYear || 2020; - drawYearSeparators(ctx, padding, chartHeight, chartWidth, this.config.data.length, startYear); - drawYearLabels(ctx, canvas.height, points, startYear); + const startMonth = this.config.options.startMonth || 1; + drawYearSeparators(ctx, padding, chartHeight, chartWidth, this.config.data.length, startYear, undefined, startMonth); + drawYearLabels(ctx, canvas.height, points, startYear, startMonth); } // Draw trendline if requested diff --git a/app/static/js/charts/chart_helpers.js b/app/static/js/charts/chart_helpers.js index a1784af..1cebff4 100644 --- a/app/static/js/charts/chart_helpers.js +++ b/app/static/js/charts/chart_helpers.js @@ -6,9 +6,12 @@ /** * Display empty state message on canvas * @param {HTMLCanvasElement} canvas - Canvas element - * @param {string} message - Message to display + * @param {string} [message] - Message to display; defaults to CHART_MESSAGES.noData if set */ -function showChartEmptyState(canvas, message = 'Keine Daten vorhanden') { +function showChartEmptyState(canvas, message) { + if (message === undefined) { + message = (typeof CHART_MESSAGES !== 'undefined') ? CHART_MESSAGES.noData : 'Keine Daten vorhanden'; + } // Size the canvas before drawing so coordinates match the displayed area. setupCanvas(canvas); const ctx = canvas.getContext('2d'); @@ -80,12 +83,13 @@ function validateChartData(data, options = {}) { * @returns {string} User-friendly message */ function getValidationMessage(reason) { + const cm = (typeof CHART_MESSAGES !== 'undefined') ? CHART_MESSAGES : null; const messages = { - 'empty': 'Keine Daten vorhanden', - 'all_zeros': 'Keine Daten im ausgewählten Zeitraum', - 'invalid_max': 'Keine gültigen Daten vorhanden' + 'empty': cm ? cm.noData : 'Keine Daten vorhanden', + 'all_zeros': cm ? cm.allZero : 'Keine Daten im ausgewählten Zeitraum', + 'invalid_max': cm ? cm.invalidMax : 'Keine gültigen Daten vorhanden', }; - return messages[reason] || 'Keine Daten vorhanden'; + return messages[reason] || (cm ? cm.noData : 'Keine Daten vorhanden'); } // Export functions diff --git a/app/static/js/charts/chart_line.js b/app/static/js/charts/chart_line.js index b144e39..2a01b1a 100644 --- a/app/static/js/charts/chart_line.js +++ b/app/static/js/charts/chart_line.js @@ -5,16 +5,18 @@ /** * Draw year separators + * @param {number} startMonth - 1-indexed month of the first data point (default 1 = January) */ -function drawYearSeparators(ctx, padding, chartHeight, chartWidth, dataLength, startYear = 2020, color = 'rgba(102, 126, 234, 0.3)') { +function drawYearSeparators(ctx, padding, chartHeight, chartWidth, dataLength, startYear = 2020, color = 'rgba(102, 126, 234, 0.3)', startMonth = 1) { ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.setLineDash([]); + const monthOffset = startMonth - 1; const currentYear = new Date().getFullYear(); for (let year = startYear + 1; year <= currentYear; year++) { - const monthIndex = (year - startYear) * 12; - if (monthIndex < dataLength) { + const monthIndex = (year - startYear) * 12 - monthOffset; + if (monthIndex > 0 && monthIndex < dataLength) { const x = padding.left + (chartWidth / (dataLength - 1)) * monthIndex; ctx.beginPath(); ctx.moveTo(x, padding.top); @@ -26,17 +28,20 @@ function drawYearSeparators(ctx, padding, chartHeight, chartWidth, dataLength, s /** * Draw year labels on X-axis + * @param {number} startMonth - 1-indexed month of the first data point (default 1 = January) */ -function drawYearLabels(ctx, height, points, startYear = 2020) { +function drawYearLabels(ctx, height, points, startYear = 2020, startMonth = 1) { const textSecondary = getCSSVariable('--text-secondary', '#718096'); ctx.fillStyle = textSecondary; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'center'; + const monthOffset = startMonth - 1; const currentYear = new Date().getFullYear(); for (let year = startYear; year <= currentYear; year++) { - const monthIndex = (year - startYear) * 12; - if (monthIndex < points.length) { + // For startYear itself, always label at index 0 (first visible data point) + const monthIndex = year === startYear ? 0 : (year - startYear) * 12 - monthOffset; + if (monthIndex >= 0 && monthIndex < points.length) { ctx.fillText(year, points[monthIndex].x, height - 10); } } diff --git a/app/static/js/charts/chart_math.js b/app/static/js/charts/chart_math.js index 4246fd3..a9f569f 100644 --- a/app/static/js/charts/chart_math.js +++ b/app/static/js/charts/chart_math.js @@ -13,30 +13,6 @@ function calculatePoints(data, padding, chartWidth, chartHeight, maxValue) { })); } -/** - * Calculate year label positions for time series data - * @param {number} dataLength - Number of data points - * @param {number} startYear - Starting year (default 2020) - * @param {number} intervalMonths - Months between labels (default 12) - * @returns {Array<{index: number, year: number}>} Array of label positions - */ -function calculateYearLabels(dataLength, startYear = 2020, intervalMonths = 12) { - const labels = []; - const numLabels = Math.ceil(dataLength / intervalMonths) + 1; - - for (let i = 0; i < numLabels; i++) { - const monthIndex = i * intervalMonths; - if (monthIndex < dataLength) { - labels.push({ - index: monthIndex, - year: startYear + i - }); - } - } - - return labels; -} - /** * Parse date string in various formats * @param {string} dateStr - Date string (e.g., "2020-10", "Oct 20") @@ -173,7 +149,6 @@ function calculateLinearTrendline(data) { if (typeof module !== 'undefined' && module.exports) { module.exports = { calculatePoints, - calculateYearLabels, parseMonthString, findFirstNonZeroIndex, aggregateMonthlyToYearly, diff --git a/app/static/js/charts/chart_primitives.js b/app/static/js/charts/chart_primitives.js index 2d1ecf2..9f16316 100644 --- a/app/static/js/charts/chart_primitives.js +++ b/app/static/js/charts/chart_primitives.js @@ -3,22 +3,6 @@ * @module charts/chart_primitives */ -/** - * Draw X-axis labels (years for bar charts) - */ -function drawXAxisLabels(ctx, padding, chartWidth, chartHeight, labels, barWidth, barSpacing) { - const textPrimary = getCSSVariable('--text-primary', '#1a202c'); - ctx.fillStyle = textPrimary; - ctx.font = 'bold 13px sans-serif'; - ctx.textAlign = 'center'; - - labels.forEach((label, index) => { - const x = padding.left + (barWidth + barSpacing) * index + barSpacing + barWidth / 2; - // Position labels within bottom padding area - ctx.fillText(label, x, chartHeight + padding.top + padding.bottom - 35); - }); -} - /** * Draw a bar with gradient */ @@ -84,7 +68,6 @@ function drawPoints(ctx, points, color = '#667eea', radius = 5) { // Export functions if (typeof module !== 'undefined' && module.exports) { module.exports = { - drawXAxisLabels, drawBarWithGradient, drawYearLabel, drawLegend, diff --git a/app/static/js/charts/chart_tooltip.js b/app/static/js/charts/chart_tooltip.js index 9a968ad..c93bbd6 100644 --- a/app/static/js/charts/chart_tooltip.js +++ b/app/static/js/charts/chart_tooltip.js @@ -1,55 +1,11 @@ /** - * Chart Tooltip - Basic tooltip functionality + * Chart Tooltip - Basic tooltip functionality (legacy stub) * @module charts/chart_tooltip + * + * All tooltip functionality now lives in ChartTooltip (chart_tooltip_enhanced.js). */ -/** - * Setup hover tooltip for chart - */ -function setupTooltip(canvas, points, data, labels) { - const tooltip = document.createElement('div'); - tooltip.style.cssText = 'position: fixed; background: rgba(0,0,0,0.9); color: white; padding: 8px 12px; border-radius: 6px; font-size: 13px; pointer-events: none; opacity: 0; transition: opacity 0.2s; z-index: 9999; white-space: nowrap;'; - document.body.appendChild(tooltip); - - canvas.addEventListener('mousemove', (e) => { - const rect = canvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - let closestPoint = null; - let closestDistance = Infinity; - - points.forEach((point, index) => { - const distance = Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2)); - if (distance < closestDistance && distance < 20) { - closestDistance = distance; - closestPoint = { ...point, index }; - } - }); - - if (closestPoint) { - tooltip.innerHTML = `${labels[closestPoint.index]}
${Math.round(data[closestPoint.index])} €`; - tooltip.style.left = (e.clientX + 10) + 'px'; - tooltip.style.top = (e.clientY - 45) + 'px'; - tooltip.style.opacity = '1'; - canvas.style.cursor = 'pointer'; - } else { - tooltip.style.opacity = '0'; - canvas.style.cursor = 'default'; - } - }); - - canvas.addEventListener('mouseleave', () => { - tooltip.style.opacity = '0'; - canvas.style.cursor = 'default'; - }); - - return tooltip; -} - -// Export functions +// Export for Node test compat if (typeof module !== 'undefined' && module.exports) { - module.exports = { - setupTooltip - }; + module.exports = {}; } diff --git a/app/templates/my_practice/analytics.html b/app/templates/my_practice/analytics.html index bb63d70..ddb578e 100644 --- a/app/templates/my_practice/analytics.html +++ b/app/templates/my_practice/analytics.html @@ -10,6 +10,14 @@ {% block page_subtitle %}{% trans "Revenue, Clients, Capacity & Expenses" %}{% endblock %} {% block extra_css %} + @@ -18,7 +26,6 @@ - @@ -191,7 +198,7 @@

💰 {% trans "Revenue, Expenses & Withdrawals" %}

.tooltip((d) => { const lines = [`${d.label}`]; d.values.forEach(v => { - lines.push(`${v.label}: ${Math.round(v.value).toLocaleString('de-DE')}€`); + lines.push(`${v.label}: ${Math.round(v.value).toLocaleString(LOCALE)}€`); }); return lines.join('
'); }) @@ -220,13 +227,18 @@

📈 {% trans "Revenue Trend (Monthly)" %}