From 413572677272ad092d636407746b23479832483b Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 23 Jun 2026 14:07:29 +0000 Subject: [PATCH] Abo-Uebersichten: Team-Ansicht konsolidiert, Monatsfilter, Wachstum & Dashboard-KPIs - /user/abos/ot: Filter nach Eingangsmonat (start_date) ueber neuen UserAbo-Scope - Konsolidierte Team-Abo-Ansicht nach Ebenen/Linien (Bestand, neue Abos im Monat, Eigen-/Kundenabos, Punkte je Ebene + Summen) via TeamAboOverviewService - team/customers leitet auf konsolidierte Ansicht um; Navigation bereinigt - Mitglieder-Detail-Modal mit Abo-Zusammenfassung (Eigen-Abo, Kundenabos, Punkte, Beginn, naechste Ausfuehrung, Ausfuehrungen) - Wachstums-Tabelle (Jahr x Monate) + Liniendiagramm (neue Eigen-/Kundenabos) - Dashboard-KPI-Kachel (Gesamtabos, Kundenabos, 1000-Punkte-Shops) - i18n de/en/es/fr, Tests (Filter, Overview, Wachstum, Modal, Redirect) Co-authored-by: Cursor --- app/Http/Controllers/ModalController.php | 19 + app/Http/Controllers/User/TeamController.php | 93 +++-- .../Backoffice/BackofficeDashboardService.php | 4 +- .../ENTWICKLUNGSPLAN-ABO-UEBERSICHTEN.md | 320 +++++++++++++++ resources/lang/de/abo.php | 1 + resources/lang/en/abo.php | 1 + resources/lang/es/abo.php | 1 + resources/lang/fr/abo.php | 1 + .../layouts/includes/layout-sidenav.blade.php | 10 +- resources/views/user/abo/index.blade.php | 132 +++++- resources/views/user/team/abos.blade.php | 376 ++++++++++++------ .../user/team/modal_member_abos.blade.php | 91 +++++ tests/Feature/AboOtMonthFilterTest.php | 66 +++ tests/Feature/TeamAboOverviewTest.php | 8 + .../Feature/TeamCustomerAbosRedirectTest.php | 18 + 15 files changed, 967 insertions(+), 174 deletions(-) create mode 100644 dev/2026-06-23-abo/ENTWICKLUNGSPLAN-ABO-UEBERSICHTEN.md create mode 100644 resources/views/user/team/modal_member_abos.blade.php create mode 100644 tests/Feature/AboOtMonthFilterTest.php create mode 100644 tests/Feature/TeamCustomerAbosRedirectTest.php diff --git a/app/Http/Controllers/ModalController.php b/app/Http/Controllers/ModalController.php index 0cb97aab..eb61c17a 100644 --- a/app/Http/Controllers/ModalController.php +++ b/app/Http/Controllers/ModalController.php @@ -87,6 +87,16 @@ class ModalController extends Controller $ret = view('user.shop.sales.modal_api_order_shipping_detail', compact('shopping_order', 'isAdmin', 'data'))->render(); } + if ($data['action'] === 'user-order-dhl-info') { + $user = \Auth::user(); + $shopping_order = ShoppingOrder::with('dhlShipments')->findOrFail($data['id']); + if (! $user->isAdmin() && $shopping_order->auth_user_id !== $user->id && $shopping_order->member_id !== $user->id) { + abort(404); + } + $isAdmin = false; + $ret = view('user.order.modal_dhl_shipping_info', compact('shopping_order', 'isAdmin'))->render(); + } + if ($data['action'] === 'user-order-my-delivery-show') { $user = \Auth::user(); $ret = view('admin.modal.show_user_customers', compact('user', 'data'))->render(); @@ -130,6 +140,15 @@ class ModalController extends Controller $ret = view('admin.modal.business_user_detail', compact('TreeCalcBot', 'user', 'data'))->render(); } + if ($data['action'] === 'team-abo-member-show') { + $summary = app(\App\Services\Abo\TeamAboOverviewService::class) + ->memberAboSummary(\Auth::user(), (int) $data['id']); + if ($summary) { + $ret = view('user.team.modal_member_abos', compact('summary', 'data'))->render(); + } else { + $ret = view('admin.modal.business_user_notfound', compact('data'))->render(); + } + } if ($data['action'] === 'business-user-show') { $user = User::with(['account', 'account.country', 'account.shipping_country', 'user_level', 'user_sponsor.account'])->find($data['id']); \Log::info('business-user-show', ['user' => $user]); diff --git a/app/Http/Controllers/User/TeamController.php b/app/Http/Controllers/User/TeamController.php index 2156e4d4..a6819570 100644 --- a/app/Http/Controllers/User/TeamController.php +++ b/app/Http/Controllers/User/TeamController.php @@ -703,68 +703,73 @@ class TeamController extends Controller } /** - * Zeigt die Abos der Team-Mitglieder an + * Konsolidierte Team-Abo-Übersicht: Abos im Team nach Ebenen/Linien gruppiert, + * inkl. Eigen-Abos, Kundenabos und neuer Abos im gewählten Monat. */ - public function showAbos() + public function showAbos(\App\Services\Abo\TeamAboOverviewService $overviewService) { $user = User::find(\Auth::user()->id); - $this->setFilterVars(); + $now = now(); - // Hole Team-Mitglieder-IDs effizient via Sponsor-Hierarchie - $teamUserIds = AboHelper::getTeamUserIds($user->id); + // Bestandsübersicht: immer aktueller Bestand; "neu"-Badges = laufender Monat. + $overview = $overviewService->linesOverview($user, (int) $now->month, (int) $now->year); - $selectedYear = (int) Request::get('year', now()->year); - $baseQuery = \App\Models\UserAbo::whereIn('user_id', $teamUserIds) - ->where('is_for', 'me') - ->where('status', '>', 1); - - // Hole Abos der Team-Mitglieder - $abos = (clone $baseQuery) - ->with(['user', 'user.account', 'user_abo_items', 'user_abo_items.product']) - ->orderBy('next_date', 'asc') - ->get(); + // Wachstums-Tabelle: wählbares Jahr (GET-Param), Default aktuelles Jahr. + $growthYear = (int) Request::get('year', $now->year); + $growth = $overviewService->monthlyGrowth($user, $growthYear); $data = [ - 'filter_months' => HTMLHelper::getTransMonths(), - 'filter_years' => HTMLHelper::getYearRange(2022), - 'abos' => $abos, - 'chartData' => AboHelper::getMonthlyAboCounts($baseQuery, $selectedYear, 'team_abos', $user->id), - 'chartYear' => $selectedYear, - 'chartYears' => HTMLHelper::getYearRange(2026), - 'chartMonths' => HTMLHelper::getTransMonths(), + 'currentMonthName' => HTMLHelper::getTransMonths()[(int) $now->month] ?? '', + 'overview' => $overview, + 'growth' => $growth, + 'growthChart' => $this->buildGrowthChart($growth), + 'growthYear' => $growthYear, + 'filterYears' => HTMLHelper::getYearRange(2022), ]; return view('user.team.abos', $data); } /** - * Zeigt eine Übersicht der Kunden-Abos aller Team-Mitglieder (anonymisiert) + * Bereitet die Datenreihen für das Wachstums-Liniendiagramm auf. + * + * @param array{months: array} $growth + * @return array{labels: list, own: list, customer: list, step: int, suggested_max: int} */ - public function showTeamCustomerAbos(): \Illuminate\View\View + private function buildGrowthChart(array $growth): array { - $user = User::find(\Auth::user()->id); - $teamUserIds = AboHelper::getTeamUserIds($user->id); + $labels = array_map(fn (array $row): string => $row['label'], $growth['months']); + $own = array_map(fn (array $row): int => $row['own_new'], $growth['months']); + $customer = array_map(fn (array $row): int => $row['customer_new'], $growth['months']); - $selectedYear = (int) Request::get('year', now()->year); - $baseQuery = \App\Models\UserAbo::whereIn('member_id', $teamUserIds) - ->where('is_for', 'ot') - ->where('status', '>', 1); + $max = max(array_merge($own, $customer, [0])); + $step = match (true) { + $max <= 5 => 1, + $max <= 10 => 2, + $max <= 20 => 5, + $max <= 50 => 10, + $max <= 100 => 20, + $max <= 200 => 50, + default => 100, + }; - $abos = (clone $baseQuery) - ->with(['member', 'member.account', 'user_abo_items', 'user_abo_items.product']) - ->orderBy('member_id') - ->orderBy('next_date', 'asc') - ->get(); + return [ + 'labels' => array_values($labels), + 'own' => array_values($own), + 'customer' => array_values($customer), + 'step' => $step, + 'suggested_max' => $max + $step, + ]; + } - $groupedByMember = $abos->groupBy('member_id'); - - return view('user.team.customer_abos', [ - 'groupedByMember' => $groupedByMember, - 'chartData' => AboHelper::getMonthlyAboCounts($baseQuery, $selectedYear, 'team_cust_abos', $user->id), - 'chartYear' => $selectedYear, - 'chartYears' => HTMLHelper::getYearRange(2026), - 'chartMonths' => HTMLHelper::getTransMonths(), - ]); + /** + * Die Kundenabos im Team sind in die konsolidierte Team-Abo-Übersicht + * (`user_abos_team_show`) eingeflossen. Diese Route bleibt für Bookmarks/Altlinks + * bestehen und leitet dauerhaft dorthin weiter. + */ + public function showTeamCustomerAbos(): \Illuminate\Http\RedirectResponse + { + return redirect()->route('user_abos_team_show'); } /** diff --git a/app/Services/Backoffice/BackofficeDashboardService.php b/app/Services/Backoffice/BackofficeDashboardService.php index 2f2019f5..ebc45f2c 100644 --- a/app/Services/Backoffice/BackofficeDashboardService.php +++ b/app/Services/Backoffice/BackofficeDashboardService.php @@ -347,9 +347,11 @@ class BackofficeDashboardService } /** + * Anzahl der Berater mit >= 1000 Punkten Kundenumsatz (KP + Shop) im Monat. + * * @param int[] $userIds */ - private function shop1000Count(array $userIds, int $month, int $year): int + public function shop1000Count(array $userIds, int $month, int $year): int { return UserSalesVolume::query() ->whereIn('user_id', $userIds) diff --git a/dev/2026-06-23-abo/ENTWICKLUNGSPLAN-ABO-UEBERSICHTEN.md b/dev/2026-06-23-abo/ENTWICKLUNGSPLAN-ABO-UEBERSICHTEN.md new file mode 100644 index 00000000..60834a3b --- /dev/null +++ b/dev/2026-06-23-abo/ENTWICKLUNGSPLAN-ABO-UEBERSICHTEN.md @@ -0,0 +1,320 @@ +# Entwicklungsplan: Abo-Übersichten erweitern (eigene Kundenabos, Team-Abos, Dashboard) + +> **Status:** In Planung · **Erstellt:** 2026-06-23 · **Briefing:** Kundenanfrage + `docs/salescenter/Todos Backoffice.md` + +## Dokumentations-Hinweis (verbindlich) + +Dieser Plan wird **Schritt für Schritt** abgearbeitet. Für **jede Phase** gilt: + +- Der **Umsetzungsstatus** in diesem Dokument wird nach Abschluss eines Schritts auf `ERLEDIGT` gesetzt (mit Datum, betroffenen Dateien, Zeilenangaben und kurzer Begründung). +- **Alle Phasen, Hinweise und getroffenen Detail-Entscheidungen** werden sauber dokumentiert – auch Abweichungen vom Plan und Verworfenes (mit Begründung). +- Jede Änderung wird **programmatisch getestet** (Pest), bevor die Phase als erledigt gilt. +- Nach Code-Änderungen läuft `./vendor/bin/pint --dirty`. + +--- + +## 1. Ziel & Briefing-Zusammenfassung + +Die Darstellung/Auflistung der Abos im Sales Center (`my.mivita.care`) soll erweitert werden. Vier Bausteine: + +1. **Eigene Kundenabos** (`/user/abos/ot`): Filter nach **Monat & Jahr**, in dem das Abo hinzugekommen ist (Eingangsmonat). +2. **Team-Abo-Ansichten zusammenführen** (`/user/abos/team/show` + `/user/abos/team/customers`): Beide Seiten überschneiden sich stark und sollen zu **einer** Ansicht „Abos im Team" verschmelzen. Inhalt: + - Wer im Team hat wie viele **Kundenabos** abgeschlossen? + - **Sortierung nach Monaten:** Wer im Team hat diesen Monat (Monatsfilter) wie viele **neue** Abos (Kunden- und Eigen-Abos) abgeschlossen? + - Wer im Team hat **selbst** ein Abo abgeschlossen? + - **Gesamtübersicht:** Wie viele Abos haben Team-Mitglieder insgesamt? + - **Sortierung nach Ebenen (Linien):** Wichtigste Ebene ist die **1. Ebene** (Wer hat dort ein Abo, wer nicht?), optional ausbaubar auf den gesamten Baum. +3. **Dashboard ergänzen:** Anzahl der **„1000-Punkte-Shops"** und **Gesamtabos im Team**. + +--- + +## 2. Ist-Zustand (Analyse-Ergebnis) + +### 2.1 Eigene Kundenabos `/user/abos/ot` + +| Bereich | Status heute | Schlüsseldateien | +| ------- | ------------ | ---------------- | +| Controller | `AboController@index($view='ot')` lädt `UserAbo` (`member_id = Auth`, `is_for=ot`, `status>=1`), liefert bereits `chartData` (Jahresfilter) | `app/Http/Controllers/User/AboController.php:62-83` | +| View | Card-Liste pro Abo, oben Monats-Chart (`_abo_chart`) mit **Jahres**-Umschaltung | `resources/views/user/abo/index.blade.php`, `resources/views/user/abo/_abo_chart.blade.php` | +| Monatszählung | `AboHelper::getMonthlyAboCounts()` (Snapshot vergangener Monate + Live aktueller Monat) | `app/Services/AboHelper.php:514-565` | +| Filter | **Nur Jahr** (Chart). Keine Filterung der Card-Liste nach Eingangsmonat. | – | + +**Fazit:** Jahresfilter existiert über den Chart. Es fehlt ein **Monat+Jahr-Filter auf die Liste** (Abos, deren `start_date` im gewählten Monat liegt). + +### 2.2 Team-Abos `/user/abos/team/show` & `/user/abos/team/customers` + +| Route | Methode | Inhalt heute | +| ----- | ------- | ------------ | +| `user_abos_team_show` | `TeamController@showAbos` | Flache Card-Liste **aller eigenen Team-Abos** (`is_for=me`, `user_id ∈ Team`), Jahres-Chart `team_abos`. Eine Card pro Abo. | +| `user_abos_team_customers` | `TeamController@showTeamCustomerAbos` | Kundenabos im Team (`is_for=ot`, `member_id ∈ Team`), **gruppiert pro Team-Mitglied** (collapsible), anonymisiert, Jahres-Chart `team_cust_abos`. | + +- Beide nutzen `AboHelper::getTeamUserIds($userId)` (rekursive Downline über `m_sponsor`, **flach**, ohne Linien-Information). +- Beide nutzen denselben `_abo_chart`-Partial (nur Jahr). +- **Überschneidung:** Beide listen Team-Abos, nur unterschiedlich (eigene Abos vs. Kundenabos) und unterschiedlich gruppiert. Keine Ebenen-/Linien-Sortierung, keine „neue Abos diesen Monat"-Aggregation pro Person. +- Detailseite: `TeamController@detailAbo` (`user_abos_team_detail`). + +### 2.3 Dashboard + +| Bereich | Status heute | Datei | +| ------- | ------------ | ----- | +| Dashboard-Home | Karten-Grid; Abo-Bezug nur: Einmalprodukt-Card (`_abo_onetime`) | `resources/views/home.blade.php`, `app/Http/Controllers/HomeController.php:45-92` | +| Team-/Abo-KPIs | **Nicht vorhanden** auf dem Dashboard | – | + +### 2.4 Bereits vorhandenes Backoffice-Statistik-Modul (WICHTIG – Wiederverwendung!) + +Es existiert ein **VIP-only** Modul, das den Großteil der gewünschten Team-Logik bereits abbildet: + +| Komponente | Datei | Liefert | +| ---------- | ----- | ------- | +| Controller | `app/Http/Controllers/User/BackofficeStatisticsController.php` | Übersicht / Drilldown / CSV-Export, Monat+Jahr-Filter (Session-persistiert), nur `isVIP()` | +| Dashboard-Service | `app/Services/Backoffice/BackofficeDashboardService.php` | **`lineBuckets($rootUserId)`** = Downline gruppiert nach **Linien/Ebenen** (1..n); pro Linie: Berater-Zahl, Neupartner, **Teamabos** (`team_partner_abos`), **neue Teamabos** (`team_partner_abos_new`), **Teamkundenabos** (`team_customer_abos`), **neue** (`team_customer_abos_new`), Punkte-Aufschlüsselung, **`shop_1000`** (1000-Punkte-Shop-Zähler), Umsatz; plus **`totals`**-Zeile. Snapshots für abgeschlossene Monate. | +| Drilldown-Service | `app/Services/Backoffice/BackofficeDrilldownService.php` | Personenlisten je Linie+Metrik (Namen, Punkte, Status, Besteht-seit, nächste Ausführung). | +| Snapshot-Modell | `app/Models/BackofficeStatisticsSnapshot.php` | Eingefrorene Monatswerte pro User. | + +> **Schlüssel-Erkenntnis:** Die in der Kundenanfrage gewünschte Ebenen-/Monats-/Gesamtlogik **existiert technisch schon** in `BackofficeDashboardService`. Wir bauen **nicht neu**, sondern (a) machen diese Datenquelle für die Abo-Ansicht wiederverwendbar und (b) liefern eine schlankere, für **alle Berater** zugängliche Abo-fokussierte Ansicht + Dashboard-Widgets. + +Definition **„1000-Punkte-Shop"** (laut Briefing, bereits im Service implementiert): Teampartner, die **mind. 1000 Punkte Kundenumsatz** im gewählten Monat erzielen (`shop1000Count()`). + +--- + +## 3. Offene Fragen / Annahmen + +| # | Thema | Annahme (sofern nicht anders entschieden) | +| - | ----- | ----------------------------------------- | +| A1 | „Eingangsmonat" eines Abos | = `start_date` (Anlage-/Aktivierungsdatum). Filter zeigt Abos mit `start_date` im gewählten Monat/Jahr. | JA +| A2 | Zugänglichkeit der neuen Team-Ansicht | Für **alle aktiven Berater** (nicht nur VIP), da rein abo-bezogen und über die eigene Downline. Das umfangreiche Punkte-/Umsatz-Backoffice bleibt VIP-only. | JA +| A3 | Umfang Ebenen | **1. Ebene zwingend** als Default sichtbar; weitere Linien aufklappbar; „gesamter Baum" als Summenzeile/optionaler Modus. | JA +| A4 | „selbst ein Abo abgeschlossen" | Eigen-Abo = `UserAbo is_for=me` des Team-Mitglieds (Spalte/Badge pro Person: ja/nein + Punkte). | JA +| A5 | Zusammenführen der zwei Routen | Neue konsolidierte Seite unter `user_abos_team_show`; `team/customers` wird Weiterleitung/Tab oder entfällt aus der Navigation (Route bleibt vorerst als Redirect). | Redirect +| A6 | Dashboard-Widgets-Zielgruppe | Alle aktiven Berater sehen „Gesamtabos im Team" + „1000-Punkte-Shops"; klickbar → neue Team-Abo-Ansicht. | JA + +> **Diese Annahmen bitte vor Umsetzungsbeginn bestätigen oder korrigieren.** + +--- + +## 4. Lösungsvorschlag (Architektur) + +### 4.1 Gemeinsame Datenquelle: `TeamAboOverviewService` (neu) + +Neuer Service `app/Services/Abo/TeamAboOverviewService.php`, der die Linien-/Monats-/Gesamtlogik **abo-fokussiert** kapselt und intern `BackofficeDashboardService::lineBuckets()` (oder eine extrahierte, gemeinsam genutzte Helper-Methode) wiederverwendet. + +Liefert je Linie: +- `line` (1..n), `consultants` (Anzahl Berater dieser Ebene) +- `members_with_own_abo` / `members_total` (wer hat selbst ein Abo) +- `team_partner_abos` (Summe Eigen-Abos der Ebene) + `_new` (neu im Monat) +- `team_customer_abos` (Summe Kundenabos der Ebene) + `_new` +- pro Person (für Drilldown/aufgeklappte Ebene): Name, Karriere-Level, Eigen-Abo (ja/Punkte), #Kundenabos + Punkte, #Teamabos darunter +- `totals`-Zeile über alle Linien + +Vorteil: Eine zentrale, getestete Datenquelle für **Team-Ansicht** und **Dashboard-Widgets**. Refactoring-Hinweis: Falls `lineBuckets()`/`newAboCount()`/`shop1000Count()` aus `BackofficeDashboardService` extrahiert werden, in einen gemeinsamen `Trait`/Helper auslagern, um Duplikate zu vermeiden. + +### 4.2 Phase-Übersicht + +| Phase | Inhalt | Aufwand | +| ----- | ------ | ------- | +| **P1** | `/user/abos/ot`: Monat+Jahr-Filter auf die Liste | klein | +| **P2** | `TeamAboOverviewService` (Datenquelle + Tests) | mittel | +| **P3** | Konsolidierte Team-Abo-Ansicht (Linien/Ebenen, Monatsfilter, neue Abos, Eigen-Abo-Spalte, Summen) | mittel-groß | +| **P4** | Zusammenführung `team/customers` → Redirect/Tab, Navigation anpassen | klein | +| **P5** | Dashboard-Widgets „Gesamtabos im Team" + „1000-Punkte-Shops" (klickbar) | klein-mittel | +| **P6** | i18n (de/en/es/fr), Pint, Gesamttest | klein | + +--- + +## 5. Detailplanung pro Phase + +### Phase 1 — `/user/abos/ot`: Monat- & Jahr-Filter + +**Status:** ERLEDIGT (2026-06-23) + +**Umsetzung:** +- `app/Models/UserAbo.php`: Query-Scope `scopeAddedInMonth($query, $year, $month)` (Filter nach `start_date`; `month=0` lässt Abfrage unverändert). +- `app/Http/Controllers/User/AboController.php` (`index('ot')`): wertet `month`-Param aus, nutzt `addedInMonth()`, übergibt `filterMonth`/`filterYear` an die View. +- `resources/views/user/abo/index.blade.php`: Filterleiste (Monats-Dropdown + Jahr + Filtern/Zurücksetzen) über der Card-Liste (nur `view==='ot'`). +- i18n: `abo.filter`, `abo.reset`, `abo.filter_added_period` in de/en/es/fr. +- Test: `tests/Feature/AboOtMonthFilterTest.php` (2 Tests grün). + +- `AboController@index('ot')`: zusätzlich `month`-Parameter (Default „alle") auswerten. +- Liste filtern: `whereYear('start_date', $year)` + optional `whereMonth('start_date', $month)`. +- View `user/abo/index.blade.php`: Filterleiste (Monats-Dropdown + Jahr) über der Card-Liste; Chart bleibt. Wiederverwendung von `HTMLHelper::getTransMonths()` / `getYearRange()`. +- Filter-Persistenz analog zu bestehenden Mustern (Query-Param genügt; Session optional). +- **Test:** `tests/Feature` – Abos mit `start_date` in verschiedenen Monaten anlegen, Filter prüft korrekte Teilmenge. + +### Phase 2 — `TeamAboOverviewService` + +**Status:** ERLEDIGT (2026-06-23) + +**Umsetzung:** +- `app/Models/UserAbo.php`: Scope `scopeActiveAbo()` (active=true, status ∉ {4,5,6}) – konsistent mit `BackofficeDashboardService`. +- `app/Services/Abo/TeamAboOverviewService.php`: `linesOverview(User,$month,$year)` liefert je Ebene Berateranzahl, „mit Eigen-Abo", Eigen-Abos (+Punkte +neu), Kundenabos (+Punkte +neu), pro-Mitglied-Details und eine Summenzeile. Wiederverwendung von `BackofficeDashboardService::lineBuckets()` (keine Duplizierung der Baum-Traversierung). +- i18n: `abo.team_total`, `abo.team_line` in de/en/es/fr. +- Test: `tests/Feature/TeamAboOverviewTest.php` (2 Tests, 19 Assertions grün). Hinweis: Test-Schema hat keine `products.slug`-Spalte → `beforeEach` ergänzt sie (etabliertes Muster aus `MembershipQtyProtectionTest`). + +- Neuer Service mit Methoden: + - `linesOverview(User $user, int $month, int $year): array` (Linien + Totals, abo-fokussiert) + - `lineMembers(User $user, int $line, int $month, int $year): Collection` (Personen einer Ebene mit Abo-Kennzahlen) +- Wiederverwendung der vorhandenen `lineBuckets()`-Logik (gemeinsam nutzbar machen). +- „neue Abos im Monat": Logik aus `BackofficeDashboardService::newAboCount()` wiederverwenden (Definition: `start_date` im Monat, aktiver Status). +- **Test:** Unit/Feature – Downline mit mehreren Ebenen + Abos in verschiedenen Monaten; prüft Linien-Zuordnung, neue-Abo-Zähler, Eigen-Abo-Flag, Summen. + +### Phase 3 — Konsolidierte Team-Abo-Ansicht + +**Status:** ERLEDIGT (2026-06-23) + +**Umsetzung:** +- `TeamController@showAbos` nutzt `TeamAboOverviewService` + Monat/Jahr-Filter (`selectedAboPeriod()`, Session-persistiert). +- Neue View `resources/views/user/team/abos.blade.php`: Monats-/Jahresfilter, Summen-Cards (Gesamtabos, Eigen-Abos, Kundenabos, Mitglieder mit Abo), Linien-/Ebenen-Tabelle (1. Ebene hervorgehoben + standardmäßig offen), aufklappbare Mitgliederliste je Ebene (Eigen-Abo ja/nein, #Kundenabos, Punkte), Summenzeile. +- i18n: viele neue `abo.*`-Keys (filter_period, team_total_abos, team_own_abos, team_members_with_abo, team_by_level, new_this_month, with_own_abo, consultants, level, own_abo, customer_abos_count, career_level, of, yes, no) in de/en/es/fr. +- Test: Render-Test in `tests/Feature/TeamAboOverviewTest.php` (3 Tests grün). + +- `TeamController@showAbos` umstellen auf `TeamAboOverviewService` + Monat/Jahr-Filter (Session-persistiert, analog `BackofficeStatisticsController::selectedPeriod()`). +- Neue View `resources/views/user/team/abos.blade.php` (umbauen): + - **Kopf:** Monats-/Jahresfilter + Summen-Card (Gesamt-Teamabos, Gesamt-Kundenabos im Team, neue diesen Monat, #1000-Punkte-Shops). + - **Linien-Tabelle:** Zeile je Ebene (1. Linie zuerst, hervorgehoben): Berater, „mit Eigen-Abo", Eigen-Abos (Punkte), Kundenabos (Anzahl+Punkte), neue Abos (Monat). Summenzeile unten. + - **Aufklappbar pro Ebene:** Personenliste (Name, Karriere-Level, Eigen-Abo ja/nein, #Kundenabos, Punkte). 1. Ebene standardmäßig offen. + - Detail-Link je Abo → `user_abos_team_detail` bleibt erhalten. +- Wiederverwendung des `_abo_chart`-Partials für den Monatsverlauf (optional beibehalten). +- **Test:** Feature – View rendert Linien korrekt, 1. Ebene sichtbar, Summen stimmen. + +### Phase 4 — Zusammenführung `team/customers` + +**Status:** ERLEDIGT (2026-06-23) + +**Umsetzung:** +- `TeamController@showTeamCustomerAbos` leitet dauerhaft auf `user_abos_team_show` um (Route bleibt für Altlinks). Kundenabos sind als Spalte/Drilldown in die konsolidierte Ansicht eingeflossen. +- Navigation `layout-sidenav.blade.php`: Menüpunkt „Team Kunden" entfernt; „Teamabos" markiert nun auch die `team/customers`-URL aktiv. +- `resources/views/user/team/customer_abos.blade.php` bleibt vorerst bestehen (nicht mehr verlinkt), um nichts ungewollt zu löschen. +- Test: `tests/Feature/TeamCustomerAbosRedirectTest.php` (Redirect grün). + +- Kundenabos-Inhalt als **Tab/Abschnitt** in die konsolidierte Ansicht integrieren (gruppiert pro Mitglied bleibt als Drilldown erhalten). +- `user_abos_team_customers` → Redirect auf `user_abos_team_show` (Route bleibt für Bookmarks bestehen). +- Navigation `layout-sidenav.blade.php`: Punkt „Team Kunden" entfernen/zusammenführen; Label gem. Briefing „**Kundenabos im Team**" / „Teamkundenabos" verwenden (Terminologie-Korrektur). +- **Test:** Redirect-Test; Navigation-Smoke. + +### Phase 5 — Dashboard-Widgets + +**Status:** ERLEDIGT (2026-06-23) + +**Umsetzung:** +- `BackofficeDashboardService::shop1000Count()` auf `public` gesetzt (Wiederverwendung). +- `TeamAboOverviewService::teamKpis(User,$month,$year)`: leichtgewichtige KPIs (Gesamtabos, Teamabos, Teamkundenabos, 1000-Punkte-Shops) ohne teure Punkte-/Mitglieder-Aggregation. +- `HomeController@show`: berechnet `teamAboKpis` für aktive Berater (aktueller Monat). +- Neues Partial `resources/views/dashboard/_abo_team_kpis.blade.php`: klickbare KPI-Card (→ `user_abos_team_show`), eingebunden in `home.blade.php`. +- i18n: `abo.shop_1000` in de/en/es/fr. +- Test: KPI-Test in `tests/Feature/TeamAboOverviewTest.php` (4 Tests grün). + +- `HomeController@show`: für aktive Berater via `TeamAboOverviewService` (bzw. `BackofficeDashboardService::overview()`-Totals für den aktuellen Monat) zwei KPIs berechnen: + - **Gesamtabos im Team** (`team_partner_abos` + `team_customer_abos` der Totals, oder getrennt anzeigen). + - **1000-Punkte-Shops** (`shop_1000` Totals). +- Neuer Partial `resources/views/dashboard/_abo_team_kpis.blade.php`: zwei klickbare KPI-Cards → Link auf neue Team-Abo-Ansicht (bzw. Backoffice-Statistik bei VIP). +- Performance: Für das Dashboard nur **eine** Monatsberechnung; bei großen Teams ggf. Snapshot/Cache des aktuellen Monats nutzen (kein Live-Vollscan bei jedem Dashboard-Aufruf – ggf. Caching mit kurzer TTL). +- **Test:** Feature – KPIs erscheinen für aktiven Berater mit Team-Abos; Werte stimmen. + +### Phase 6 — i18n & Abschluss + +**Status:** ERLEDIGT (2026-06-23) + +**Umsetzung:** +- i18n: alle neuen `abo.*`-Keys in de/en/es/fr gepflegt; `LocalizationSyncTest` grün (Sprachen synchron). +- Pint: `--dirty` → pass. +- Tests: 29 relevante Tests grün (Phase 1–5 + bestehende Abo-Tests), `LocalizationSyncTest` (3 Tests) grün. + +- Neue Übersetzungsschlüssel in `resources/lang/{de,en,es,fr}/abo.php` + `navigation.php`/`team.php` (Filter-Labels, Spaltenköpfe, KPI-Titel, „1000-Punkte-Shop", „Kundenabos im Team", „neue Abos"). +- `./vendor/bin/pint --dirty`. +- Gesamttest der betroffenen Feature-/Unit-Dateien. + +--- + +## 6. Zu beachtende Punkte / Risiken + +1. **Keine Logik-Duplikate:** `BackofficeDashboardService` ist die Referenz für Linien/neue Abos/1000-Punkte-Shop. Gemeinsame Methoden extrahieren statt kopieren. +2. **Performance:** Downline-Traversierung (`lineBuckets`) kann bei großen Teams teuer sein. Dashboard-Widgets cachen oder Snapshot des aktuellen Monats nutzen; nicht bei jedem Request live voll durchrechnen. +3. **Zugriffsrechte/Datenschutz:** Kundenabos im Team sind heute **anonymisiert** dargestellt (`customer_privacy_info`). Diese Anonymisierung beibehalten; Klarnamen nur dort, wo bisher erlaubt (Team-Mitglieder = Berater). VIP-/Datenschutz-Hinweise aus dem bestehenden Backoffice-Modul übernehmen. +4. **Status-Definition Abo aktiv:** Konsistent mit `getMonthlyAboCounts()`/`activeAboQuery()` halten (Terminal-Status 4/5 ausschließen). +5. **Snapshots:** Vergangene Monate aus `AboChartSnapshot` bzw. `BackofficeStatisticsSnapshot` lesen (nicht neu berechnen). Aktueller Monat live. +6. **Route-Kompatibilität:** `user_abos_team_customers` nicht hart entfernen (Bookmarks/Links) → Redirect. +7. **Multi-Tenant/Routing:** Änderungen liegen in `routes/domains/crm.php` (CRM-Domain). Keine Auswirkungen auf andere Domains. + +--- + +## 7. Betroffene Dateien (Vorschau) + +**Neu:** +- `app/Services/Abo/TeamAboOverviewService.php` +- `resources/views/dashboard/_abo_team_kpis.blade.php` +- `tests/Feature/TeamAboOverviewTest.php`, `tests/Feature/AboOtMonthFilterTest.php`, `tests/Feature/DashboardAboKpisTest.php` + +**Geändert:** +- `app/Http/Controllers/User/AboController.php` (P1) +- `app/Http/Controllers/User/TeamController.php` (P3/P4) +- `app/Http/Controllers/HomeController.php` (P5) +- `app/Services/Backoffice/BackofficeDashboardService.php` (gemeinsame Methoden extrahieren) +- `resources/views/user/abo/index.blade.php` (P1) +- `resources/views/user/team/abos.blade.php` (P3) +- `resources/views/user/team/customer_abos.blade.php` (P4 – Integration/Redirect) +- `resources/views/home.blade.php` (P5) +- `resources/views/layouts/includes/layout-sidenav.blade.php` (P4) +- `routes/domains/crm.php` (P4 – Redirect) +- `resources/lang/{de,en,es,fr}/abo.php`, `navigation.php`, `team.php` (P6) + +--- + +## 8. Umsetzungsreihenfolge (empfohlen) + +P1 (schneller Mehrwert, isoliert) → P2 (Datenbasis) → P3 (Hauptansicht) → P4 (Konsolidierung/Navi) → P5 (Dashboard) → P6 (i18n/Abschluss). + +--- + +## 9. Nachtrag — Mitglieder-Detail-Modal (2026-06-23) + +**Status:** ERLEDIGT + +Statt des generischen `business-user-show`-Modals (Sponsor/Firma) zeigt der Klick auf einen Namen in der Team-Abo-Übersicht jetzt eine abo-fokussierte Zusammenfassung. + +**Umsetzung:** +- `TeamAboOverviewService::memberAboSummary(User $owner, int $memberId)`: liefert Name + E-Mail, Eigen-Abo (Punkte, Beginn, nächste Ausführung, Anzahl Ausführungen) sowie Kunden-Abos (Anzahl, Gesamtpunkte, Liste je Abo). Team-Autorisierung via `AboHelper::isUserInTeam()` → `null` (→ „nicht gefunden"-Modal), wenn Mitglied nicht in Downline. +- `ModalController@load`: neuer Branch `team-abo-member-show`. +- Neue View `resources/views/user/team/modal_member_abos.blade.php`. +- `user/team/abos.blade.php`: Namens-Link nutzt `data-action="team-abo-member-show"`. +- i18n: `abo.next_execution`, `abo.executions_count`, `abo.abo_count_points`, `abo.no_own_abo`, `abo.no_customer_abos` in de/en/es/fr. +- Tests: 2 zusätzliche Tests in `tests/Feature/TeamAboOverviewTest.php` (gesamt 6 grün). + +--- + +## 10. Nachtrag — Zeitraum-Logik: Bestand + Wachstums-Tabelle (2026-06-23) + +**Status:** ERLEDIGT + +**Entscheidung (mit Kunde):** Variante **A + C** statt historischem Filter (Variante B verworfen, da „aktiv" ein Jetzt-Zustand ist und eine historische Rekonstruktion fehleranfällig/irreführend wäre). + +**Umsetzung:** +- Oberer Bereich (Cards + Linien-Tabelle) zeigt immer den **aktuellen Bestand**; die „+neu"-Badges beziehen sich fest auf den **laufenden Monat** (Spaltenkopf zeigt den Monatsnamen). Der frühere, missverständliche Zeitraum-Filter wurde entfernt. +- Neue **Wachstums-Tabelle** unter der Bestandsübersicht: wählbares Jahr (GET-Param `year`), Zeilen = Monate, Spalten = neue Eigen-Abos / neue Kunden-Abos / Summe + Jahres-Summenzeile. +- `TeamAboOverviewService::monthlyGrowth(User,$year)` + privater Helfer `newAbosByMonth()` (DB-agnostisch via `toBase()->pluck('start_date')` + PHP-Gruppierung, kein `MONTH()`/`strftime`). Definition „neu" konsistent mit Badges (aktives Abo, `start_date` im Monat). +- `TeamController@showAbos` vereinfacht (kein Session-Filter mehr): Bestand = aktueller Monat, Wachstum = `year`-Param. +- i18n: `abo.team_growth`, `abo.month`, `abo.new_own_abos`, `abo.new_customer_abos`, `abo.new_total` in de/en/es/fr. +- Tests: 1 zusätzlicher Test (`monthlyGrowth`) + angepasster Render-Test (gesamt 7 grün). + +--- + +## 11. Nachtrag — Tabellen-Feinschliff Ebenen (2026-06-23) + +**Status:** ERLEDIGT + +- Linien-Tabelle: redundante Spalte „Mit Eigen-Abo" entfernt (= identisch zu „Eigen-Abos", da max. 1 Eigen-Abo pro Berater). +- Neue Spalte **„Punkte"** (Gesamtpunkte je Ebene = Eigen- + Kundenabo-Punkte) inkl. Gesamtsumme im Footer (nutzt vorhandene `own_abo_points`/`customer_abo_points` aus `linesOverview`). +- Hinweis-Fußzeile unter der Wachstums-Tabelle (`abo.growth_note`) erklärt die Differenz Bestand vs. Jahres-Wachstum. +- **Klarstellung 124 vs. 123:** kein Bug. 124 = aktueller Bestand aller aktiven Eigen-Abos; 123 = davon in 2026 gestartet. 1 Eigen-Abo stammt aus einem Vorjahr und ist weiterhin aktiv. +- i18n: `abo.growth_note` in de/en/es/fr. + +--- + +## 12. Nachtrag — Wachstums-Liniendiagramm (2026-06-23) + +**Status:** ERLEDIGT + +- Unter der Wachstums-Tabelle: Chart.js-**Liniendiagramm** (X = Monate, Y = Mengen) mit zwei Kurven (neue Eigen-Abos, neue Kunden-Abos) für das gewählte Jahr. +- Datenaufbereitung im Controller (`TeamController::buildGrowthChart()`) statt im Blade `@php`-Block – sauberere MVC-Trennung und umgeht ein Blade-Kompilierungsproblem mit `@php`-Block + Arrow-Funktionen in diesem Template. +- Nutzt dieselbe Chart.js-v2-Lib wie `_abo_chart.blade.php` (`/vendor/libs/chartjs/chartjs.js`), feste Canvas-ID `aboGrowthChart`. +- 2. Kachel-Dublette „Mit eigenem Abo" oben entfernt; verbleibende drei Kacheln auf `col-md-4`. +- Render-Test entsprechend erweitert (gesamt 7 grün), Pint pass. diff --git a/resources/lang/de/abo.php b/resources/lang/de/abo.php index 62fa2974..334b5206 100644 --- a/resources/lang/de/abo.php +++ b/resources/lang/de/abo.php @@ -219,6 +219,7 @@ return [ 'new_own_abos' => 'Neue Eigen-Abos', 'new_customer_abos' => 'Neue Kunden-Abos', 'new_total' => 'Neu gesamt', + 'growth_note' => 'Zählt aktive Abos nach Eingangsjahr (start_date). Der Gesamtbestand oben kann höher sein, da er auch noch aktive Abos aus Vorjahren enthält.', 'filter' => 'Filtern', 'reset' => 'Zurücksetzen', 'filter_added_period' => 'Hinzugekommen im Zeitraum:', diff --git a/resources/lang/en/abo.php b/resources/lang/en/abo.php index c000e6ba..fabd7f2e 100644 --- a/resources/lang/en/abo.php +++ b/resources/lang/en/abo.php @@ -220,6 +220,7 @@ return [ 'new_own_abos' => 'New own subscriptions', 'new_customer_abos' => 'New customer subscriptions', 'new_total' => 'New total', + 'growth_note' => 'Counts active subscriptions by their start year (start_date). The total stock above can be higher because it also includes still-active subscriptions from previous years.', 'filter' => 'Filter', 'reset' => 'Reset', 'filter_added_period' => 'Added during period:', diff --git a/resources/lang/es/abo.php b/resources/lang/es/abo.php index 355f770b..8558d4b9 100644 --- a/resources/lang/es/abo.php +++ b/resources/lang/es/abo.php @@ -193,6 +193,7 @@ return [ 'new_own_abos' => 'Nuevas suscripciones propias', 'new_customer_abos' => 'Nuevas suscripciones de clientes', 'new_total' => 'Nuevas en total', + 'growth_note' => 'Cuenta las suscripciones activas por su año de inicio (start_date). El total de arriba puede ser mayor porque también incluye suscripciones aún activas de años anteriores.', 'filter' => 'Filtrar', 'reset' => 'Restablecer', 'filter_added_period' => 'Añadido en el período:', diff --git a/resources/lang/fr/abo.php b/resources/lang/fr/abo.php index aa2d87e6..b29b13fb 100644 --- a/resources/lang/fr/abo.php +++ b/resources/lang/fr/abo.php @@ -192,6 +192,7 @@ return [ 'new_own_abos' => 'Nouveaux abonnements propres', 'new_customer_abos' => 'Nouveaux abonnements clients', 'new_total' => 'Nouveaux au total', + 'growth_note' => 'Compte les abonnements actifs selon leur année de début (start_date). Le stock total ci-dessus peut être plus élevé, car il inclut aussi des abonnements encore actifs des années précédentes.', 'filter' => 'Filtrer', 'reset' => 'Réinitialiser', 'filter_added_period' => 'Ajouté durant la période :', diff --git a/resources/views/layouts/includes/layout-sidenav.blade.php b/resources/views/layouts/includes/layout-sidenav.blade.php index 925fb94b..710ba372 100755 --- a/resources/views/layouts/includes/layout-sidenav.blade.php +++ b/resources/views/layouts/includes/layout-sidenav.blade.php @@ -211,20 +211,12 @@ -
  • +
  • {{ __('navigation.teamabos') }}
  • -
  • - -
    {{ __('navigation.team_customers') }}
    - -
    -
  • + + + @endif + + @if (Session::has('alert-success')) +
    + {{ Session::get('alert-success') }} +
    + @endif + @if (Session::has('alert-error')) +
    + {{ Session::get('alert-error') }} +
    + @endif +
    + @php($abo_payment_links = $abo_payment_links ?? collect()) - @if(!$user_abos || $user_abos->isEmpty()) + @if($view === 'ot' && $abo_payment_links->isNotEmpty()) + @foreach($abo_payment_links as $paymentLink) +
    +
    +
    +
    +
    + + {{ __('navigation.abo') }} #{{ $paymentLink->identifier }} + + {!! \App\Services\OrderPaymentService::getTypeBadge($paymentLink) !!} +
    +
    +
    + + {{ __('abo.abo_details') }} + + @if(\App\Services\OrderPaymentService::canDeleteInstance($paymentLink)) + {!! Form::open(['action' => route('user_abos_payment_link_delete', [$paymentLink->identifier]), 'class' => 'd-inline-block ml-1']) !!} + + {!! Form::close() !!} + @endif +
    +
    +
    +
    +
    + @if(\App\Services\OrderPaymentService::isPaymentFailed($paymentLink)) +
    + {{ __('abo.pending_payment_failed_title') }}
    + {{ __('abo.payment_link_failed_copy') }} +
    + @elseif(\App\Services\OrderPaymentService::isPaymentProcessing($paymentLink)) +
    + {{ __('abo.pending_payment_processing_title') }}
    + {{ __('abo.payment_link_processing_copy') }} +
    + @else +
    + {{ __('abo.payment_link_sent_title') }}
    + {{ __('abo.payment_link_sent_copy') }} +
    + @endif +
    +
    +
    {{ __('tables.date') }}
    +
    {{ $paymentLink->created_at->format('d.m.Y') }}
    +
    +
    +
    {{ __('tables.status') }}
    + {!! \App\Services\OrderPaymentService::getStatusBadge($paymentLink) !!} +
    +
    +
    {{ __('tables.amount') }}
    + {{ $paymentLink->getAmountFormatted() }} € +
    +
    +
    {{ __('tables.email') }}
    + {{ $paymentLink->shopping_data['billing_email'] ?? '-' }} +
    +
    +
    +
    + @endforeach + @endif + + @if((!$user_abos || $user_abos->isEmpty()) && $abo_payment_links->isEmpty())
    @@ -37,6 +142,29 @@ @foreach($user_abos as $user_abo)
    + @if($user_abo->isPendingInitialPayment()) +
    + @if($user_abo->hasFailedInitialPayment()) +
    + {{ __('abo.pending_payment_failed_title') }}
    + {{ __('abo.pending_payment_failed_copy') }} + {!! Form::open(['action' => route('user_abos_delete_pending', [$view, $user_abo->id]), 'class' => 'd-inline-block ml-2']) !!} + + {!! Form::close() !!} +
    + @elseif($user_abo->hasInitialPaymentStatus()) +
    + {{ __('abo.pending_payment_processing_title') }}
    + {{ __('abo.pending_payment_processing_copy') }} +
    + @else +
    + {{ __('abo.pending_payment_waiting_title') }}
    + {{ __('abo.pending_payment_waiting_copy') }} +
    + @endif +
    + @endif
    @@ -136,4 +264,4 @@ @endforeach
    -@endsection \ No newline at end of file +@endsection diff --git a/resources/views/user/team/abos.blade.php b/resources/views/user/team/abos.blade.php index 5614e948..153acf87 100644 --- a/resources/views/user/team/abos.blade.php +++ b/resources/views/user/team/abos.blade.php @@ -1,133 +1,273 @@ @extends('layouts.layout-2') @section('content') -

    +

    {{ __('abo.team_subscriptions') }} / {{ __('navigation.overview') }}

    - @if(isset($chartData)) - @include('user.abo._abo_chart') - @endif - -
    -
    - - @if(!$abos || $abos->isEmpty()) -
    -
    -
    -
    -
    {{ __('abo.team_subscriptions') }}
    -

    {{ __('tables.no_data_available') }}

    -
    -
    + {{-- Gesamtübersicht (aktueller Bestand) --}} + @php($totals = $overview['totals']) +
    +
    +
    +
    +
    {{ __('abo.team_total_abos') }}
    +
    {{ $totals['total_abos'] }}
    - @endif - - @foreach($abos as $user_abo) -
    - -
    -
    -
    -
    -
    {{ __('tables.start_date') }}
    -
    {{ $user_abo->start_date }}
    -
    -
    -
    {{ __('tables.next_date') }}
    -
    {{ $user_abo->next_date }}
    -
    -
    -
    {{ __('tables.abo_delivery_day') }}
    -
    {{ \App\Services\HTMLHelper::getAboStrLang($user_abo->abo_interval) }}
    -
    -
    -
    {{ __('tables.last_date') }}
    -
    {{ $user_abo->last_date }}
    -
    -
    -
    -
    -
    -
    -
    -
    {{ __('tables.status') }} / {{ __('tables.active') }}
    - {!! $user_abo->getStatusFormated() !!}  {!! get_active_badge($user_abo->active) !!} -
    -
    -
    {{ __('tables.abo_delivery') }}
    - {{ $user_abo->getCountOrders() }} -
    -
    -
    {{ __('navigation.points') }}
    - {{ $user_abo->getFormattedTotalPoints() }} Pkt. -
    -
    -
    {{ __('tables.payment') }}
    - {{ $user_abo->getPaymentType() }} -
    -
    -
    -
    -
    -
    -
    -
    {{ __('First name') }}
    - @if($user_abo->user && $user_abo->user->account) - {{ $user_abo->user->account->first_name }} - @endif -
    -
    -
    {{ __('Last name') }}
    - @if($user_abo->user && $user_abo->user->account) - {{ $user_abo->user->account->last_name }} - @endif -
    -
    -
    {{ __('E-Mail') }}
    - @if($user_abo->user) - {{ $user_abo->user->email }} - @endif -
    -
    -
    {{ __('tables.adviser') }}
    - @if($user_abo->user && $user_abo->user->account) - - {{ $user_abo->user->account->first_name }} {{ $user_abo->user->account->last_name }} - - @endif -
    -
    +
    +
    +
    +
    +
    {{ __('abo.team_own_abos') }}
    +
    {{ $totals['own_abos'] }}
    +
    +{{ $totals['own_abos_new'] }} {{ __('abo.new_this_month') }}
    +
    +
    +
    +
    +
    +
    +
    {{ __('abo.team_customer_abos') }}
    +
    {{ $totals['customer_abos'] }}
    +
    +{{ $totals['customer_abos_new'] }} {{ __('abo.new_this_month') }}
    - @endforeach
    -@endsection + {{-- Linien-/Ebenen-Tabelle --}} +
    +
    + {{ __('abo.team_by_level') }} +
    +
    + + + + + + + + + + + + + + @forelse($overview['lines'] as $line) + @php($collapseId = 'team-line-'.$line['line']) + + + + + + + + + + + + + @empty + + + + @endforelse + + @if(!empty($overview['lines'])) + + + + + + + + + + + + @endif +
    {{ __('abo.level') }}{{ __('abo.consultants') }}{{ __('abo.team_own_abos') }}{{ __('abo.team_customer_abos') }}{{ __('abo.new_this_month') }} ({{ $currentMonthName }}){{ __('abo.team_total') }}{{ __('abo.abo_count_points') }}
    + + {{ $line['label'] }} + {{ $line['consultants'] }} + {{ $line['own_abos'] }} + @if($line['own_abos_new'] > 0)+{{ $line['own_abos_new'] }}@endif + + {{ $line['customer_abos'] }} + @if($line['customer_abos_new'] > 0)+{{ $line['customer_abos_new'] }}@endif + +{{ $line['own_abos_new'] + $line['customer_abos_new'] }}{{ $line['total_abos'] }}{{ \App\Services\Util::formatNumber($line['own_abo_points'] + $line['customer_abo_points']) }}
    +
    +
    + + + + + + + + + + + + @foreach($line['members'] as $member) + + + + + + + + @endforeach + +
    {{ __('tables.adviser') }}{{ __('abo.career_level') }}{{ __('abo.own_abo') }}{{ __('abo.customer_abos_count') }}{{ __('navigation.points') }}
    + + {{ $member['name'] }} + + {{ $member['career_level'] }} + @if($member['has_own_abo']) + {{ __('abo.yes') }} + @else + {{ __('abo.no') }} + @endif + {{ $member['customer_abo_count'] }} + {{ \App\Services\Util::formatNumber($member['own_abo_points'] + $member['customer_abo_points']) }} Pkt. +
    +
    +
    +
    {{ __('tables.no_data_available') }}
    {{ $totals['label'] }}{{ $totals['consultants'] }}{{ $totals['own_abos'] }}{{ $totals['customer_abos'] }}+{{ $totals['own_abos_new'] + $totals['customer_abos_new'] }}{{ $totals['total_abos'] }}{{ \App\Services\Util::formatNumber($totals['own_abo_points'] + $totals['customer_abo_points']) }}
    +
    +
    + + {{-- Wachstum: neue Abos je Monat im gewählten Jahr --}} +
    +
    + {{ __('abo.team_growth') }} +
    + + +
    +
    +
    + + + + + + + + + + + @foreach($growth['months'] as $row) + + + + + + + @endforeach + + + + + + + + + +
    {{ __('abo.month') }}{{ __('abo.new_own_abos') }}{{ __('abo.new_customer_abos') }}{{ __('abo.new_total') }}
    {{ $row['label'] }}{{ $row['own_new'] }}{{ $row['customer_new'] }}{{ $row['total_new'] }}
    {{ __('abo.team_total') }} {{ $growth['year'] }}{{ $growth['totals']['own_new'] }}{{ $growth['totals']['customer_new'] }}{{ $growth['totals']['total_new'] }}
    +
    + +
    + + {{-- Wachstums-Diagramm: zwei Kurven (neue Eigen-/Kunden-Abos je Monat) --}} +
    +
    +
    + {{ __('abo.team_growth') }} + {{ $growth['year'] }} +
    +
    +
    +
    + +
    +
    + + + +@endsection diff --git a/resources/views/user/team/modal_member_abos.blade.php b/resources/views/user/team/modal_member_abos.blade.php new file mode 100644 index 00000000..07e79826 --- /dev/null +++ b/resources/views/user/team/modal_member_abos.blade.php @@ -0,0 +1,91 @@ + diff --git a/tests/Feature/AboOtMonthFilterTest.php b/tests/Feature/AboOtMonthFilterTest.php new file mode 100644 index 00000000..9eda208e --- /dev/null +++ b/tests/Feature/AboOtMonthFilterTest.php @@ -0,0 +1,66 @@ + $member->id, + 'member_id' => $member->id, + 'shopping_user_id' => 0, + 'email' => 'cust-'.uniqid('', true).'@example.com', + 'is_for' => 'ot', + 'payone_userid' => 0, + 'clearingtype' => 'cc', + 'amount' => 5000, + 'status' => 2, + 'abo_interval' => 5, + 'start_date' => $startDate, + 'last_date' => $startDate, + 'next_date' => '2026-12-10', + ]); +} + +it('filtert Abos nach Eingangsmonat (start_date)', function (): void { + $member = User::forceCreate([ + 'email' => 'member-'.uniqid('', true).'@example.com', + 'password' => bcrypt('secret'), + 'lang' => 'de', + ]); + + $june = createOtAbo($member, '2026-06-15'); + $july = createOtAbo($member, '2026-07-03'); + $juneOtherYear = createOtAbo($member, '2025-06-20'); + + $filtered = UserAbo::where('member_id', $member->id) + ->where('is_for', 'ot') + ->addedInMonth(2026, 6) + ->pluck('id'); + + expect($filtered)->toContain($june->id) + ->not->toContain($july->id) + ->not->toContain($juneOtherYear->id); +}); + +it('lässt die Abfrage unverändert, wenn kein Monat gewählt ist (month=0)', function (): void { + $member = User::forceCreate([ + 'email' => 'member-'.uniqid('', true).'@example.com', + 'password' => bcrypt('secret'), + 'lang' => 'de', + ]); + + createOtAbo($member, '2026-06-15'); + createOtAbo($member, '2026-07-03'); + + $count = UserAbo::where('member_id', $member->id) + ->where('is_for', 'ot') + ->addedInMonth(2026, 0) + ->count(); + + expect($count)->toBe(2); +}); diff --git a/tests/Feature/TeamAboOverviewTest.php b/tests/Feature/TeamAboOverviewTest.php index f95d14a2..ac3e64c3 100644 --- a/tests/Feature/TeamAboOverviewTest.php +++ b/tests/Feature/TeamAboOverviewTest.php @@ -160,6 +160,13 @@ it('rendert die Team-Abo-Linienansicht mit Ebenen und Summen', function (): void 'currentMonthName' => HTMLHelper::getTransMonths()[6], 'overview' => $overview, 'growth' => $growth, + 'growthChart' => [ + 'labels' => array_map(fn ($r) => $r['label'], $growth['months']), + 'own' => array_map(fn ($r) => $r['own_new'], $growth['months']), + 'customer' => array_map(fn ($r) => $r['customer_new'], $growth['months']), + 'step' => 1, + 'suggested_max' => 5, + ], 'growthYear' => 2026, 'filterYears' => HTMLHelper::getYearRange(2022), ])->render(); @@ -168,6 +175,7 @@ it('rendert die Team-Abo-Linienansicht mit Ebenen und Summen', function (): void ->toContain(__('abo.team_by_level')) ->toContain(__('abo.team_total_abos')) ->toContain(__('abo.team_growth')) + ->toContain('aboGrowthChart') ->toContain($anna->email); }); diff --git a/tests/Feature/TeamCustomerAbosRedirectTest.php b/tests/Feature/TeamCustomerAbosRedirectTest.php new file mode 100644 index 00000000..daad9986 --- /dev/null +++ b/tests/Feature/TeamCustomerAbosRedirectTest.php @@ -0,0 +1,18 @@ + '/'.$name); + + $controller = new TeamController; + $response = $controller->showTeamCustomerAbos(); + + expect($response)->toBeInstanceOf(RedirectResponse::class) + ->and($response->getTargetUrl())->toContain('user_abos_team_show'); +});