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 @@