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 <cursoragent@cursor.com>
This commit is contained in:
parent
65b867f8ee
commit
4135726772
15 changed files with 967 additions and 174 deletions
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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<int, array{label: string, own_new: int, customer_new: int}>} $growth
|
||||
* @return array{labels: list<string>, own: list<int>, customer: list<int>, 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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
320
dev/2026-06-23-abo/ENTWICKLUNGSPLAN-ABO-UEBERSICHTEN.md
Normal file
320
dev/2026-06-23-abo/ENTWICKLUNGSPLAN-ABO-UEBERSICHTEN.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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:',
|
||||
|
|
|
|||
|
|
@ -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:',
|
||||
|
|
|
|||
|
|
@ -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:',
|
||||
|
|
|
|||
|
|
@ -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 :',
|
||||
|
|
|
|||
|
|
@ -211,20 +211,12 @@
|
|||
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidenav-item{{ Request::is('user/abos/team/show') ? ' active' : '' }}">
|
||||
<li class="sidenav-item{{ Request::is('user/abos/team/show', 'user/abos/team/customers', 'user/abos/team/customers/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('user_abos_team_show') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-refresh-circle"></i>
|
||||
<div>{{ __('navigation.teamabos') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('user/abos/team/customers', 'user/abos/team/customers/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('user_abos_team_customers') }}" class="sidenav-link"><i
|
||||
class="sidenav-icon ion ion-md-refresh-circle"></i>
|
||||
<div>{{ __('navigation.team_customers') }}</div>
|
||||
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
class="sidenav-item{{ Request::is('user/abos/me', 'user/abos/detail/me/*') ? ' active' : '' }}">
|
||||
<a href="{{ route('user_abos', ['me']) }}" class="sidenav-link"><i
|
||||
|
|
|
|||
|
|
@ -13,10 +13,115 @@
|
|||
@include('user.abo._abo_chart')
|
||||
@endif
|
||||
|
||||
@if($view === 'ot' && isset($filterMonth))
|
||||
<div class="card mb-4">
|
||||
<div class="card-body py-3">
|
||||
<form method="GET" action="{{ url()->current() }}" class="form-inline">
|
||||
<label class="text-muted small mr-2 mb-0">{{ __('abo.filter_added_period') }}</label>
|
||||
<select name="month" class="form-control form-control-sm mr-2 mb-1">
|
||||
<option value="0">{{ __('tables.all') }}</option>
|
||||
@foreach($chartMonths as $monthNum => $monthName)
|
||||
<option value="{{ $monthNum }}" {{ (int) $filterMonth === (int) $monthNum ? 'selected' : '' }}>{{ $monthName }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select name="year" class="form-control form-control-sm mr-2 mb-1">
|
||||
@foreach($chartYears as $y)
|
||||
<option value="{{ $y }}" {{ (int) $filterYear === (int) $y ? 'selected' : '' }}>{{ $y }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm btn-secondary mb-1">{{ __('abo.filter') }}</button>
|
||||
@if($filterMonth)
|
||||
<a href="{{ url()->current() }}?year={{ $filterYear }}" class="btn btn-sm btn-outline-secondary ml-2 mb-1">{{ __('abo.reset') }}</a>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (Session::has('alert-success'))
|
||||
<div class="alert alert-success">
|
||||
{{ Session::get('alert-success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if (Session::has('alert-error'))
|
||||
<div class="alert alert-danger">
|
||||
{{ Session::get('alert-error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@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)
|
||||
<div class="card mb-4 border-warning">
|
||||
<div class="card-body pb-2">
|
||||
<div class="row">
|
||||
<div class="col-sm-7">
|
||||
<h5 class="mb-1">
|
||||
<a href="{{ route('user_order_payment_links_detail', [$paymentLink->identifier]) }}">
|
||||
{{ __('navigation.abo') }} <span class="text-muted">#{{ $paymentLink->identifier }}</span>
|
||||
</a>
|
||||
{!! \App\Services\OrderPaymentService::getTypeBadge($paymentLink) !!}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-sm-5 text-right">
|
||||
<a href="{{ route('user_order_payment_links_detail', [$paymentLink->identifier]) }}" class="btn btn-sm btn-secondary">
|
||||
<span class="fa fa-edit"></span>{{ __('abo.abo_details') }}
|
||||
</a>
|
||||
@if(\App\Services\OrderPaymentService::canDeleteInstance($paymentLink))
|
||||
{!! Form::open(['action' => route('user_abos_payment_link_delete', [$paymentLink->identifier]), 'class' => 'd-inline-block ml-1']) !!}
|
||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('{{ __('confirm_delete') }}');">
|
||||
<span class="fa fa-trash"></span> {{ __('delete') }}
|
||||
</button>
|
||||
{!! Form::close() !!}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="m-0">
|
||||
<div class="card-body pb-1">
|
||||
@if(\App\Services\OrderPaymentService::isPaymentFailed($paymentLink))
|
||||
<div class="alert alert-danger mb-3">
|
||||
<strong>{{ __('abo.pending_payment_failed_title') }}</strong><br>
|
||||
{{ __('abo.payment_link_failed_copy') }}
|
||||
</div>
|
||||
@elseif(\App\Services\OrderPaymentService::isPaymentProcessing($paymentLink))
|
||||
<div class="alert alert-info mb-3">
|
||||
<strong>{{ __('abo.pending_payment_processing_title') }}</strong><br>
|
||||
{{ __('abo.payment_link_processing_copy') }}
|
||||
</div>
|
||||
@else
|
||||
<div class="alert alert-warning mb-3">
|
||||
<strong>{{ __('abo.payment_link_sent_title') }}</strong><br>
|
||||
{{ __('abo.payment_link_sent_copy') }}
|
||||
</div>
|
||||
@endif
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="text-muted small">{{ __('tables.date') }}</div>
|
||||
<div class="font-weight-bold">{{ $paymentLink->created_at->format('d.m.Y') }}</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="text-muted small">{{ __('tables.status') }}</div>
|
||||
{!! \App\Services\OrderPaymentService::getStatusBadge($paymentLink) !!}
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="text-muted small">{{ __('tables.amount') }}</div>
|
||||
{{ $paymentLink->getAmountFormatted() }} €
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="text-muted small">{{ __('tables.email') }}</div>
|
||||
{{ $paymentLink->shopping_data['billing_email'] ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
@if((!$user_abos || $user_abos->isEmpty()) && $abo_payment_links->isEmpty())
|
||||
<div class="card mb-4">
|
||||
<div class="card-body pb-2">
|
||||
<div class="row">
|
||||
|
|
@ -37,6 +142,29 @@
|
|||
|
||||
@foreach($user_abos as $user_abo)
|
||||
<div class="card mb-4">
|
||||
@if($user_abo->isPendingInitialPayment())
|
||||
<div class="card-body pb-0">
|
||||
@if($user_abo->hasFailedInitialPayment())
|
||||
<div class="alert alert-danger mb-3">
|
||||
<strong>{{ __('abo.pending_payment_failed_title') }}</strong><br>
|
||||
{{ __('abo.pending_payment_failed_copy') }}
|
||||
{!! Form::open(['action' => route('user_abos_delete_pending', [$view, $user_abo->id]), 'class' => 'd-inline-block ml-2']) !!}
|
||||
<button type="submit" class="btn btn-sm btn-danger">{{ __('abo.pending_delete_button') }}</button>
|
||||
{!! Form::close() !!}
|
||||
</div>
|
||||
@elseif($user_abo->hasInitialPaymentStatus())
|
||||
<div class="alert alert-info mb-3">
|
||||
<strong>{{ __('abo.pending_payment_processing_title') }}</strong><br>
|
||||
{{ __('abo.pending_payment_processing_copy') }}
|
||||
</div>
|
||||
@else
|
||||
<div class="alert alert-warning mb-3">
|
||||
<strong>{{ __('abo.pending_payment_waiting_title') }}</strong><br>
|
||||
{{ __('abo.pending_payment_waiting_copy') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
<div class="card-body pb-2">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
|
|
@ -136,4 +264,4 @@
|
|||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -1,133 +1,273 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
<h4 class="font-weight-bold py-2 mb-2 d-flex justify-content-between align-items-center w-100 ">
|
||||
<h4 class="font-weight-bold py-2 mb-2 d-flex justify-content-between align-items-center w-100">
|
||||
<div>
|
||||
{{ __('abo.team_subscriptions') }} / {{ __('navigation.overview') }}
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
@if(isset($chartData))
|
||||
@include('user.abo._abo_chart')
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
|
||||
@if(!$abos || $abos->isEmpty())
|
||||
<div class="card mb-4">
|
||||
<div class="card-body pb-2">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h5>{{ __('abo.team_subscriptions') }}</h5>
|
||||
<p class="text-muted">{{ __('tables.no_data_available') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{-- Gesamtübersicht (aktueller Bestand) --}}
|
||||
@php($totals = $overview['totals'])
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-4 col-12 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-muted small">{{ __('abo.team_total_abos') }}</div>
|
||||
<div class="display-5 font-weight-bold text-primary">{{ $totals['total_abos'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@foreach($abos as $user_abo)
|
||||
<div class="card mb-4">
|
||||
<div class="card-body pb-2">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<h5 class="mb-1">
|
||||
<a href="{{ route('user_abos_team_detail', [$user_abo->id]) }}">
|
||||
{{ __('navigation.abo') }} <span class="text-muted">{{ '#'.$user_abo->payone_userid }}</span>
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-sm-6 text-right">
|
||||
<a href="{{ route('user_abos_team_detail', [$user_abo->id]) }}" class="btn btn-sm btn-secondary float-right">
|
||||
<span class="fa fa-edit"></span>{{ __('abo.abo_details') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="m-0">
|
||||
<div class="card-body pb-1">
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="text-muted small">{{ __('tables.start_date') }}</div>
|
||||
<div class="font-weight-bold">{{ $user_abo->start_date }}</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="text-muted small">{{ __('tables.next_date') }} </div>
|
||||
<div class="font-weight-bold">{{ $user_abo->next_date }}</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="text-muted small">{{ __('tables.abo_delivery_day') }}</div>
|
||||
<div class="font-weight-bold">{{ \App\Services\HTMLHelper::getAboStrLang($user_abo->abo_interval) }}</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="text-muted small">{{ __('tables.last_date') }}</div>
|
||||
<div class="font-weight-bold">{{ $user_abo->last_date }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="m-0">
|
||||
<div class="card-body pb-1">
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="text-muted small">{{ __('tables.status') }} / {{ __('tables.active') }}</div>
|
||||
{!! $user_abo->getStatusFormated() !!} {!! get_active_badge($user_abo->active) !!}
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="text-muted small">{{ __('tables.abo_delivery') }}</div>
|
||||
{{ $user_abo->getCountOrders() }}
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="text-muted small">{{ __('navigation.points') }}</div>
|
||||
<strong class="text-primary">{{ $user_abo->getFormattedTotalPoints() }} Pkt.</strong>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="text-muted small">{{ __('tables.payment') }}</div>
|
||||
{{ $user_abo->getPaymentType() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="m-0">
|
||||
<div class="card-body pb-1">
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="text-muted small">{{ __('First name') }}</div>
|
||||
@if($user_abo->user && $user_abo->user->account)
|
||||
{{ $user_abo->user->account->first_name }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="text-muted small">{{ __('Last name') }}</div>
|
||||
@if($user_abo->user && $user_abo->user->account)
|
||||
{{ $user_abo->user->account->last_name }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="text-muted small">{{ __('E-Mail') }}</div>
|
||||
@if($user_abo->user)
|
||||
{{ $user_abo->user->email }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="text-muted small">{{ __('tables.adviser') }}</div>
|
||||
@if($user_abo->user && $user_abo->user->account)
|
||||
<a href="#" class="text-black" data-toggle="modal"
|
||||
data-target="#modals-load-content"
|
||||
data-id="{{ $user_abo->user->id }}"
|
||||
data-action="business-user-show"
|
||||
data-back=""
|
||||
data-modal="modal-md"
|
||||
data-init_from="member"
|
||||
data-route=" {{ route('modal_load') }}">
|
||||
<span class="mr-1 ion ion-ios-contact"></span> {{ $user_abo->user->account->first_name }} {{ $user_abo->user->account->last_name }} </a>
|
||||
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-6 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-muted small">{{ __('abo.team_own_abos') }}</div>
|
||||
<div class="display-5 font-weight-bold">{{ $totals['own_abos'] }}</div>
|
||||
<div class="small text-success">+{{ $totals['own_abos_new'] }} {{ __('abo.new_this_month') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-6 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-muted small">{{ __('abo.team_customer_abos') }}</div>
|
||||
<div class="display-5 font-weight-bold">{{ $totals['customer_abos'] }}</div>
|
||||
<div class="small text-success">+{{ $totals['customer_abos_new'] }} {{ __('abo.new_this_month') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
{{-- Linien-/Ebenen-Tabelle --}}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span class="font-weight-bold">{{ __('abo.team_by_level') }}</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr class="text-muted small">
|
||||
<th>{{ __('abo.level') }}</th>
|
||||
<th class="text-center">{{ __('abo.consultants') }}</th>
|
||||
<th class="text-center">{{ __('abo.team_own_abos') }}</th>
|
||||
<th class="text-center">{{ __('abo.team_customer_abos') }}</th>
|
||||
<th class="text-center">{{ __('abo.new_this_month') }} ({{ $currentMonthName }})</th>
|
||||
<th class="text-center">{{ __('abo.team_total') }}</th>
|
||||
<th class="text-right">{{ __('abo.abo_count_points') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($overview['lines'] as $line)
|
||||
@php($collapseId = 'team-line-'.$line['line'])
|
||||
<tr class="{{ $line['line'] === 1 ? 'table-active' : '' }}" style="cursor: pointer;"
|
||||
data-toggle="collapse" data-target="#{{ $collapseId }}">
|
||||
<td class="font-weight-bold">
|
||||
<span class="ion ion-md-arrow-dropright"></span>
|
||||
{{ $line['label'] }}
|
||||
</td>
|
||||
<td class="text-center">{{ $line['consultants'] }}</td>
|
||||
<td class="text-center">
|
||||
{{ $line['own_abos'] }}
|
||||
@if($line['own_abos_new'] > 0)<span class="badge badge-success">+{{ $line['own_abos_new'] }}</span>@endif
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ $line['customer_abos'] }}
|
||||
@if($line['customer_abos_new'] > 0)<span class="badge badge-success">+{{ $line['customer_abos_new'] }}</span>@endif
|
||||
</td>
|
||||
<td class="text-center text-success font-weight-bold">+{{ $line['own_abos_new'] + $line['customer_abos_new'] }}</td>
|
||||
<td class="text-center font-weight-bold text-primary">{{ $line['total_abos'] }}</td>
|
||||
<td class="text-right font-weight-bold">{{ \App\Services\Util::formatNumber($line['own_abo_points'] + $line['customer_abo_points']) }}</td>
|
||||
</tr>
|
||||
<tr class="p-0">
|
||||
<td colspan="7" class="p-0 border-0">
|
||||
<div class="collapse {{ $line['line'] === 1 ? 'show' : '' }}" id="{{ $collapseId }}">
|
||||
<div class="p-3 bg-lightest">
|
||||
<table class="table table-sm table-borderless mb-0">
|
||||
<thead>
|
||||
<tr class="text-muted small">
|
||||
<th>{{ __('tables.adviser') }}</th>
|
||||
<th>{{ __('abo.career_level') }}</th>
|
||||
<th class="text-center">{{ __('abo.own_abo') }}</th>
|
||||
<th class="text-center">{{ __('abo.customer_abos_count') }}</th>
|
||||
<th class="text-right">{{ __('navigation.points') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($line['members'] as $member)
|
||||
<tr>
|
||||
<td>
|
||||
<a href="#" class="text-black" data-toggle="modal"
|
||||
data-target="#modals-load-content"
|
||||
data-id="{{ $member['user_id'] }}"
|
||||
data-action="team-abo-member-show"
|
||||
data-back=""
|
||||
data-modal="modal-md"
|
||||
data-init_from="member"
|
||||
data-route="{{ route('modal_load') }}">
|
||||
<span class="mr-1 ion ion-ios-contact"></span>{{ $member['name'] }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="small text-muted">{{ $member['career_level'] }}</td>
|
||||
<td class="text-center">
|
||||
@if($member['has_own_abo'])
|
||||
<span class="badge badge-success">{{ __('abo.yes') }}</span>
|
||||
@else
|
||||
<span class="badge badge-outline-secondary">{{ __('abo.no') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-center">{{ $member['customer_abo_count'] }}</td>
|
||||
<td class="text-right">
|
||||
<span class="text-primary font-weight-bold">{{ \App\Services\Util::formatNumber($member['own_abo_points'] + $member['customer_abo_points']) }}</span> Pkt.
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="text-muted text-center py-4">{{ __('tables.no_data_available') }}</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
@if(!empty($overview['lines']))
|
||||
<tfoot>
|
||||
<tr class="font-weight-bold">
|
||||
<td>{{ $totals['label'] }}</td>
|
||||
<td class="text-center">{{ $totals['consultants'] }}</td>
|
||||
<td class="text-center">{{ $totals['own_abos'] }}</td>
|
||||
<td class="text-center">{{ $totals['customer_abos'] }}</td>
|
||||
<td class="text-center text-success">+{{ $totals['own_abos_new'] + $totals['customer_abos_new'] }}</td>
|
||||
<td class="text-center text-primary">{{ $totals['total_abos'] }}</td>
|
||||
<td class="text-right">{{ \App\Services\Util::formatNumber($totals['own_abo_points'] + $totals['customer_abo_points']) }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
@endif
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Wachstum: neue Abos je Monat im gewählten Jahr --}}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span class="font-weight-bold">{{ __('abo.team_growth') }}</span>
|
||||
<form method="GET" action="{{ url()->current() }}" class="form-inline">
|
||||
<select name="year" class="form-control form-control-sm" onchange="this.form.submit()">
|
||||
@foreach($filterYears as $y)
|
||||
<option value="{{ $y }}" {{ (int) $growthYear === (int) $y ? 'selected' : '' }}>{{ $y }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<noscript><button type="submit" class="btn btn-sm btn-secondary ml-2">{{ __('abo.filter') }}</button></noscript>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr class="text-muted small">
|
||||
<th>{{ __('abo.month') }}</th>
|
||||
<th class="text-center">{{ __('abo.new_own_abos') }}</th>
|
||||
<th class="text-center">{{ __('abo.new_customer_abos') }}</th>
|
||||
<th class="text-center">{{ __('abo.new_total') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($growth['months'] as $row)
|
||||
<tr class="{{ $row['total_new'] === 0 ? 'text-muted' : '' }}">
|
||||
<td>{{ $row['label'] }}</td>
|
||||
<td class="text-center">{{ $row['own_new'] }}</td>
|
||||
<td class="text-center">{{ $row['customer_new'] }}</td>
|
||||
<td class="text-center font-weight-bold {{ $row['total_new'] > 0 ? 'text-success' : '' }}">{{ $row['total_new'] }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="font-weight-bold">
|
||||
<td>{{ __('abo.team_total') }} {{ $growth['year'] }}</td>
|
||||
<td class="text-center">{{ $growth['totals']['own_new'] }}</td>
|
||||
<td class="text-center">{{ $growth['totals']['customer_new'] }}</td>
|
||||
<td class="text-center text-primary">{{ $growth['totals']['total_new'] }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer py-2">
|
||||
<small class="text-muted">{{ __('abo.growth_note') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Wachstums-Diagramm: zwei Kurven (neue Eigen-/Kunden-Abos je Monat) --}}
|
||||
<div class="card mb-4">
|
||||
<div class="card-body pb-2">
|
||||
<h6 class="font-weight-bold mb-2">
|
||||
{{ __('abo.team_growth') }}
|
||||
<span class="text-muted font-weight-normal ml-1">{{ $growth['year'] }}</span>
|
||||
</h6>
|
||||
</div>
|
||||
<hr class="m-0">
|
||||
<div class="card-body pt-3 pb-2">
|
||||
<canvas id="aboGrowthChart" height="90"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/vendor/libs/chartjs/chartjs.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var ctx = document.getElementById('aboGrowthChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: {!! json_encode($growthChart['labels']) !!},
|
||||
datasets: [
|
||||
{
|
||||
label: @json(__('abo.new_own_abos')),
|
||||
data: {!! json_encode($growthChart['own']) !!},
|
||||
backgroundColor: 'rgba(102, 110, 232, 0.1)',
|
||||
borderColor: 'rgba(102, 110, 232, 0.9)',
|
||||
borderWidth: 2,
|
||||
pointBackgroundColor: 'rgba(102, 110, 232, 0.9)',
|
||||
pointRadius: 3,
|
||||
lineTension: 0.3,
|
||||
fill: false
|
||||
},
|
||||
{
|
||||
label: @json(__('abo.new_customer_abos')),
|
||||
data: {!! json_encode($growthChart['customer']) !!},
|
||||
backgroundColor: 'rgba(40, 167, 69, 0.1)',
|
||||
borderColor: 'rgba(40, 167, 69, 0.9)',
|
||||
borderWidth: 2,
|
||||
pointBackgroundColor: 'rgba(40, 167, 69, 0.9)',
|
||||
pointRadius: 3,
|
||||
lineTension: 0.3,
|
||||
fill: false
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
legend: { display: true, position: 'top' },
|
||||
tooltips: { mode: 'index', intersect: false },
|
||||
layout: { padding: { top: 10 } },
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
stepSize: {{ $growthChart['step'] }},
|
||||
suggestedMax: {{ $growthChart['suggested_max'] }},
|
||||
callback: function (value) {
|
||||
return Number.isInteger(value) ? value : null;
|
||||
}
|
||||
},
|
||||
gridLines: { color: 'rgba(0,0,0,0.05)' }
|
||||
}],
|
||||
xAxes: [{
|
||||
gridLines: { display: false }
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
91
resources/views/user/team/modal_member_abos.blade.php
Normal file
91
resources/views/user/team/modal_member_abos.blade.php
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="ion ion-ios-contact"></i>
|
||||
{{ $summary['name'] }}
|
||||
<span class="d-block text-muted small font-weight-normal">{{ $summary['email'] }}</span>
|
||||
</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{-- Eigen-Abo --}}
|
||||
<h6 class="font-weight-semibold text-primary mb-2">
|
||||
<i class="ion ion-md-refresh-circle"></i> {{ __('abo.own_abo') }}
|
||||
</h6>
|
||||
@if ($summary['own_abo'])
|
||||
@php($own = $summary['own_abo'])
|
||||
<table class="table table-sm table-borderless mb-4">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-muted" style="width: 50%;">{{ __('abo.abo_count_points') }}</td>
|
||||
<td class="text-right font-weight-bold">{{ \App\Services\Util::formatNumber($own['points']) }} Pkt.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">{{ __('abo.abo_start_date') }}</td>
|
||||
<td class="text-right">{{ $own['start_date'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">{{ __('abo.next_execution') }}</td>
|
||||
<td class="text-right">{{ $own['next_date'] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">{{ __('abo.executions_count') }}</td>
|
||||
<td class="text-right">{{ $own['executions'] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<p class="text-muted mb-4">{{ __('abo.no_own_abo') }}</p>
|
||||
@endif
|
||||
|
||||
{{-- Kunden-Abos --}}
|
||||
@php($cust = $summary['customer_abos'])
|
||||
<h6 class="font-weight-semibold text-primary mb-2">
|
||||
<i class="ion ion-md-people"></i> {{ __('abo.team_customer_abos') }}
|
||||
</h6>
|
||||
<div class="row mb-3">
|
||||
<div class="col-6">
|
||||
<div class="bg-lightest rounded p-2 text-center">
|
||||
<div class="h4 mb-0 font-weight-bold">{{ $cust['count'] }}</div>
|
||||
<div class="text-muted small">{{ __('abo.customer_abos_count') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="bg-lightest rounded p-2 text-center">
|
||||
<div class="h4 mb-0 font-weight-bold text-primary">{{ \App\Services\Util::formatNumber($cust['total_points']) }}</div>
|
||||
<div class="text-muted small">{{ __('abo.abo_count_points') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($cust['count'] > 0)
|
||||
<table class="table table-sm mb-0">
|
||||
<thead>
|
||||
<tr class="text-muted small">
|
||||
<th>#</th>
|
||||
<th class="text-right">{{ __('abo.abo_count_points') }}</th>
|
||||
<th class="text-right">{{ __('abo.abo_start_date') }}</th>
|
||||
<th class="text-right">{{ __('abo.next_execution') }}</th>
|
||||
<th class="text-right">{{ __('abo.executions_count') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($cust['items'] as $i => $item)
|
||||
<tr>
|
||||
<td>{{ $i + 1 }}</td>
|
||||
<td class="text-right font-weight-bold">{{ \App\Services\Util::formatNumber($item['points']) }}</td>
|
||||
<td class="text-right">{{ $item['start_date'] }}</td>
|
||||
<td class="text-right">{{ $item['next_date'] }}</td>
|
||||
<td class="text-right">{{ $item['executions'] }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<p class="text-muted mb-0">{{ __('abo.no_customer_abos') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ __('team.close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
66
tests/Feature/AboOtMonthFilterTest.php
Normal file
66
tests/Feature/AboOtMonthFilterTest.php
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
use App\Models\UserAbo;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function createOtAbo(User $member, string $startDate): UserAbo
|
||||
{
|
||||
return UserAbo::create([
|
||||
'user_id' => $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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
18
tests/Feature/TeamCustomerAbosRedirectTest.php
Normal file
18
tests/Feature/TeamCustomerAbosRedirectTest.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\User\TeamController;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
it('leitet die alte Team-Kundenabos-Route auf die konsolidierte Übersicht um', function (): void {
|
||||
URL::resolveMissingNamedRoutesUsing(fn (string $name, array $parameters) => '/'.$name);
|
||||
|
||||
$controller = new TeamController;
|
||||
$response = $controller->showTeamCustomerAbos();
|
||||
|
||||
expect($response)->toBeInstanceOf(RedirectResponse::class)
|
||||
->and($response->getTargetUrl())->toContain('user_abos_team_show');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue