From e3efa2f5f06bf033b4b4934d7fb15d9945800d0d Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 18 Sep 2026 12:10:27 +0000 Subject: [PATCH] Incentive (E1): Recruiting-Challenge mit Geldpraemie, Auszahlungs-Export und parallelen Incentives Neue Felder amount_per_partner, bonus_rank_1..3 und include_sales_points an incentives. Mit ausgeschalteten Umsatzpunkten zaehlt die Wertung nur die Einmalpunkte je Neupartner, die Rangliste entspricht damit der Anzahl Neupartner mit Starterpaket. IncentivePayoutService rechnet Neupartner mal Betrag plus Rang-Bonus fuer qualifizierte Gewinner; Admin-Detailseite bekommt Praemienspalte, Summen und XLSX-Export fuer die Auszahlung. Sidenav und Dashboard zeigen alle aktiven Incentives, damit Montenegro und die Challenge parallel laufen koennen. Teaser, Rangliste und Details zeigen bei Geldpraemie die Praemien-Kachel und "Deine Praemie bisher" und blenden Abo-Bloecke aus, wenn Abos keine Rolle spielen. Montenegro bleibt unveraendert. Co-Authored-By: Claude Fable 5.1 --- .../Controllers/Admin/IncentiveController.php | 56 +++++ app/Http/Controllers/HomeController.php | 20 +- .../Controllers/User/IncentiveController.php | 13 ++ app/Models/Incentive.php | 112 ++++++++++ app/Models/IncentiveParticipant.php | 5 + .../Incentive/IncentivePayoutService.php | 137 ++++++++++++ .../IncentivePointsLogRepairService.php | 2 +- app/Services/Incentive/IncentiveTracker.php | 7 +- ..._add_payout_fields_to_incentives_table.php | 51 +++++ ...twicklungsplan-protokoll-september-2026.md | 52 ++++- resources/lang/de/incentive.php | 28 +++ resources/lang/en/incentive.php | 28 +++ resources/lang/es/incentive.php | 28 +++ resources/lang/fr/incentive.php | 28 +++ .../views/admin/incentive/_form.blade.php | 37 ++++ .../views/admin/incentive/show.blade.php | 54 ++++- .../views/dashboard/_incentive.blade.php | 50 +++-- .../layouts/includes/layout-sidenav.blade.php | 12 +- .../user/incentive/_payout_bullets.blade.php | 7 + .../user/incentive/_payout_tile.blade.php | 31 +++ .../views/user/incentive/details.blade.php | 63 ++++-- resources/views/user/incentive/show.blade.php | 146 ++++++++---- .../views/user/incentive/teaser.blade.php | 90 +++++--- routes/domains/crm.php | 1 + .../Incentive/IncentiveAdminPayoutTest.php | 147 +++++++++++++ .../Feature/Incentive/IncentivePayoutTest.php | 207 ++++++++++++++++++ .../IncentiveSalesPointsToggleTest.php | 135 ++++++++++++ 27 files changed, 1418 insertions(+), 129 deletions(-) create mode 100644 app/Services/Incentive/IncentivePayoutService.php create mode 100644 database/migrations/2026_09_18_130000_add_payout_fields_to_incentives_table.php create mode 100644 resources/views/user/incentive/_payout_bullets.blade.php create mode 100644 resources/views/user/incentive/_payout_tile.blade.php create mode 100644 tests/Feature/Incentive/IncentiveAdminPayoutTest.php create mode 100644 tests/Feature/Incentive/IncentivePayoutTest.php create mode 100644 tests/Feature/Incentive/IncentiveSalesPointsToggleTest.php diff --git a/app/Http/Controllers/Admin/IncentiveController.php b/app/Http/Controllers/Admin/IncentiveController.php index 39befaa7..7744b020 100644 --- a/app/Http/Controllers/Admin/IncentiveController.php +++ b/app/Http/Controllers/Admin/IncentiveController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Admin; +use App\Exports\xExport; use App\Http\Controllers\Controller; use App\Models\Incentive; use App\Models\IncentiveNewAbo; @@ -12,8 +13,11 @@ use App\Models\UserAbo; use App\Models\UserAboOrder; use App\Models\UserSalesVolume; use App\Services\Incentive\IncentiveCalculationService; +use App\Services\Incentive\IncentivePayoutService; use App\User; +use Maatwebsite\Excel\Facades\Excel; use Request; +use Symfony\Component\HttpFoundation\BinaryFileResponse; class IncentiveController extends Controller { @@ -50,8 +54,14 @@ class IncentiveController extends Controller 'min_direct_partners' => 'required|integer|min:0', 'min_customer_abos' => 'required|integer|min:0', 'max_winners' => 'required|integer|min:1', + 'amount_per_partner' => 'nullable|numeric|min:0|max:999999', + 'bonus_rank_1' => 'nullable|numeric|min:0|max:999999', + 'bonus_rank_2' => 'nullable|numeric|min:0|max:999999', + 'bonus_rank_3' => 'nullable|numeric|min:0|max:999999', + 'include_sales_points' => 'nullable|boolean', 'status' => 'required|integer|in:0,1,2', ]); + $data = $this->normalizePayoutFields($data); $data = array_merge($data, $this->extractTranslations()); @@ -70,12 +80,52 @@ class IncentiveController extends Controller ->orderByIncentiveLeaderboard() ->get(); + $payoutService = new IncentivePayoutService; + $payoutByParticipantId = $incentive->hasCashPayout() + ? $participants->mapWithKeys(fn (IncentiveParticipant $participant) => [ + $participant->id => $payoutService->forParticipant($incentive, $participant), + ]) + : collect(); + return view('admin.incentive.show', [ 'incentive' => $incentive, 'participants' => $participants, + 'payoutByParticipantId' => $payoutByParticipantId, + 'payoutTotals' => $incentive->hasCashPayout() + ? $payoutService->totals($payoutService->rows($incentive)) + : null, ]); } + /** + * Auszahlungsliste der Geldpraemie als XLSX (Februar-Auszahlung der Recruiting-Challenge). + */ + public function payoutExport($id): BinaryFileResponse + { + $incentive = Incentive::findOrFail($id); + $service = new IncentivePayoutService; + + $filename = 'incentive-'.$incentive->slug.'-auszahlung-'.now()->format('Y-m-d').'.xlsx'; + + return Excel::download(new xExport($service->exportRows($incentive), $service->exportHeaders()), $filename); + } + + /** + * Leere Betragsfelder als NULL speichern, Checkbox als bool. + * + * @param array $data + * @return array + */ + private function normalizePayoutFields(array $data): array + { + foreach (['amount_per_partner', 'bonus_rank_1', 'bonus_rank_2', 'bonus_rank_3'] as $field) { + $data[$field] = isset($data[$field]) && $data[$field] !== '' ? round((float) $data[$field], 2) : null; + } + $data['include_sales_points'] = filter_var($data['include_sales_points'] ?? false, FILTER_VALIDATE_BOOLEAN); + + return $data; + } + public function edit($id) { $incentive = Incentive::findOrFail($id); @@ -102,8 +152,14 @@ class IncentiveController extends Controller 'min_direct_partners' => 'required|integer|min:0', 'min_customer_abos' => 'required|integer|min:0', 'max_winners' => 'required|integer|min:1', + 'amount_per_partner' => 'nullable|numeric|min:0|max:999999', + 'bonus_rank_1' => 'nullable|numeric|min:0|max:999999', + 'bonus_rank_2' => 'nullable|numeric|min:0|max:999999', + 'bonus_rank_3' => 'nullable|numeric|min:0|max:999999', + 'include_sales_points' => 'nullable|boolean', 'status' => 'required|integer|in:0,1,2', ]); + $data = $this->normalizePayoutFields($data); $data = array_merge($data, $this->extractTranslations()); diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 78bc8255..33ccf126 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -57,18 +57,24 @@ class HomeController extends Controller $user = Auth::user(); - $activeIncentive = null; - $incentiveParticipant = null; + // Mehrere Incentives koennen parallel aktiv sein (E1): je eine Dashboard-Karte. + $activeIncentives = collect(); + $incentiveParticipants = collect(); if ($user->isActiveAccount()) { - $activeIncentive = Incentive::active()->first(); - if ($activeIncentive) { - $incentiveParticipant = IncentiveParticipant::where('incentive_id', $activeIncentive->id) + $activeIncentives = Incentive::activeForNavigation(); + if ($activeIncentives->isNotEmpty()) { + $incentiveParticipants = IncentiveParticipant::query() + ->whereIn('incentive_id', $activeIncentives->pluck('id')) ->where('user_id', $user->id) - ->first(); + ->get() + ->keyBy('incentive_id'); } } + $activeIncentive = $activeIncentives->first(); + $incentiveParticipant = $activeIncentive ? $incentiveParticipants->get($activeIncentive->id) : null; + // Eigenes Abo des Beraters für die Einmalprodukte-Dashboard-Card (nur wenn // das Zeitfenster offen ist und das Feature für den User sichtbar ist). $oneTimeAbo = null; @@ -96,6 +102,8 @@ class HomeController extends Controller 'dashboardNews' => DashboardNews::getActiveNews(), 'activeIncentive' => $activeIncentive, 'incentiveParticipant' => $incentiveParticipant, + 'activeIncentives' => $activeIncentives, + 'incentiveParticipants' => $incentiveParticipants, 'oneTimeAbo' => $oneTimeAbo, 'monthlyStats' => $monthlyStats, ]; diff --git a/app/Http/Controllers/User/IncentiveController.php b/app/Http/Controllers/User/IncentiveController.php index bd130c8d..7f743942 100644 --- a/app/Http/Controllers/User/IncentiveController.php +++ b/app/Http/Controllers/User/IncentiveController.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Controller; use App\Models\Incentive; use App\Models\IncentiveParticipant; use App\Models\UserAbo; +use App\Services\Incentive\IncentivePayoutService; use App\Services\Incentive\IncentivePointsLogRepairService; use App\Services\Incentive\IncentiveTracker; use App\User; @@ -68,6 +69,16 @@ class IncentiveController extends Controller $participateHasTrackableAbos = $this->userHasTrackableAbosForIncentive($user, $incentive); } + $payoutService = new IncentivePayoutService; + $participantPayout = $incentive->hasCashPayout() && $participant + ? $payoutService->forParticipant($incentive, $participant) + : null; + $rankingPayouts = $incentive->hasCashPayout() + ? $ranking->getCollection()->mapWithKeys(fn (IncentiveParticipant $row) => [ + $row->id => $payoutService->forParticipant($incentive, $row), + ]) + : collect(); + return view('user.incentive.show', [ 'incentive' => $incentive, 'participant' => $participant, @@ -75,6 +86,8 @@ class IncentiveController extends Controller 'ranking' => $ranking, 'participateHasTrackableAbos' => $participateHasTrackableAbos, 'isVipView' => $user->isVIP(), + 'participantPayout' => $participantPayout, + 'rankingPayouts' => $rankingPayouts, ]); } diff --git a/app/Models/Incentive.php b/app/Models/Incentive.php index 9bdf1b93..f0197a9f 100644 --- a/app/Models/Incentive.php +++ b/app/Models/Incentive.php @@ -31,6 +31,11 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @property int $min_direct_partners * @property int $min_customer_abos * @property int $max_winners + * @property float|null $amount_per_partner + * @property float|null $bonus_rank_1 + * @property float|null $bonus_rank_2 + * @property float|null $bonus_rank_3 + * @property bool $include_sales_points * @property int $status * @property Carbon|null $created_at * @property Carbon|null $updated_at @@ -59,6 +64,11 @@ class Incentive extends Model 'min_direct_partners' => 'int', 'min_customer_abos' => 'int', 'max_winners' => 'int', + 'amount_per_partner' => 'float', + 'bonus_rank_1' => 'float', + 'bonus_rank_2' => 'float', + 'bonus_rank_3' => 'float', + 'include_sales_points' => 'bool', 'status' => 'int', 'qualification_start' => 'date', 'qualification_end' => 'date', @@ -83,9 +93,19 @@ class Incentive extends Model 'min_direct_partners', 'min_customer_abos', 'max_winners', + 'amount_per_partner', + 'bonus_rank_1', + 'bonus_rank_2', + 'bonus_rank_3', + 'include_sales_points', 'status', ]; + /** + * Anzahl der Plaetze, fuer die ein Rang-Bonus hinterlegt werden kann. + */ + public const RANK_BONUS_SLOTS = 3; + public static $statusTypes = [ 0 => 'draft', 1 => 'active', @@ -121,6 +141,14 @@ class Incentive extends Model return $query->where('status', 1); } + /** + * Incentives, bei denen Umsatzpunkte der Neupartner/Neuabos mitzaehlen. + */ + public function scopeIncludingSalesPoints($query) + { + return $query->where('include_sales_points', true); + } + public function scopeInQualificationPeriod($query, ?Carbon $date = null) { $date = $date ?: Carbon::now(); @@ -149,6 +177,20 @@ class Incentive extends Model // Helpers + /** + * Aktive Incentives fuer Sidenav und Dashboard: mehrere koennen parallel laufen + * (z. B. Montenegro bis Oktober, Recruiting-Challenge ab Oktober), aelteste zuerst. + * + * @return Collection + */ + public static function activeForNavigation(): Collection + { + return static::active() + ->orderBy('qualification_start') + ->orderBy('id') + ->get(); + } + public function isActive(): bool { return $this->status === 1; @@ -198,6 +240,76 @@ class Incentive extends Model return self::$statusColors[$this->status] ?? 'default'; } + /** + * Wird eine Geldpraemie je Neupartner ausgezahlt (Recruiting-Challenge)? + */ + public function hasCashPayout(): bool + { + return (float) $this->amount_per_partner > 0 || $this->rankBonuses() !== []; + } + + /** + * Rang-Boni je Platz (nur gesetzte Betraege > 0, Platz => Betrag). + * + * @return array + */ + public function rankBonuses(): array + { + $bonuses = []; + for ($rank = 1; $rank <= self::RANK_BONUS_SLOTS; $rank++) { + $amount = (float) $this->{'bonus_rank_'.$rank}; + if ($amount > 0) { + $bonuses[$rank] = $amount; + } + } + + return $bonuses; + } + + /** + * Rang-Bonus fuer einen Platz, 0 wenn keiner hinterlegt oder der Platz ausserhalb der Gewinnerplaetze liegt. + */ + public function rankBonus(?int $rank): float + { + if ($rank === null || $rank < 1 || $rank > $this->max_winners) { + return 0.0; + } + + return $this->rankBonuses()[$rank] ?? 0.0; + } + + /** + * Zaehlen Umsatzpunkte der Neupartner/Neuabos mit? Aus = Wertung nur nach Einmalpunkten. + */ + public function includesSalesPoints(): bool + { + return $this->include_sales_points === null || (bool) $this->include_sales_points; + } + + /** + * Gibt es eine Mindestqualifikation (direkte Partner oder Kundenabos)? + */ + public function hasMinimumRequirements(): bool + { + return $this->min_direct_partners > 0 || $this->min_customer_abos > 0; + } + + /** + * Spielen Abos in diesem Incentive eine Rolle (Punkte oder Mindestanzahl)? + */ + public function usesAbos(): bool + { + return $this->points_abo_onetime > 0 || $this->min_customer_abos > 0; + } + + /** + * Rangliste nach Anzahl Neupartner: Einmalpunkte je Partner, keine Abo- und Umsatzpunkte. + */ + public function ranksByPartnerCount(): bool + { + return ! $this->includesSalesPoints() && ! $this->usesAbos() && $this->points_partner_onetime > 0; + } + /** * Get specific translation for a field and locale. */ diff --git a/app/Models/IncentiveParticipant.php b/app/Models/IncentiveParticipant.php index 0889df28..07f94525 100644 --- a/app/Models/IncentiveParticipant.php +++ b/app/Models/IncentiveParticipant.php @@ -365,6 +365,11 @@ class IncentiveParticipant extends Model } // C. Akkumulierte Punkte NUR von Neupartnern und Neuabos + // (entfaellt bei Incentives ohne Umsatzpunkte, z. B. Recruiting-Challenge) + if (! $incentive->includesSalesPoints()) { + return $this->recalculateFromTrackingTables(); + } + $calculation_months = $incentive->getCalculationMonths(); $new_partner_user_ids = $this->newPartners()->pluck('user_id')->toArray(); $abo_shopping_user_ids = UserAbo::whereIn('id', $this->newAbos()->pluck('user_abo_id')) diff --git a/app/Services/Incentive/IncentivePayoutService.php b/app/Services/Incentive/IncentivePayoutService.php new file mode 100644 index 00000000..1a73e917 --- /dev/null +++ b/app/Services/Incentive/IncentivePayoutService.php @@ -0,0 +1,137 @@ +qualified_partners; + $perPartner = (float) $incentive->amount_per_partner; + $partnerAmount = round($partners * $perPartner, 2); + $rankBonus = $participant->is_qualified ? $incentive->rankBonus($participant->rank) : 0.0; + + return [ + 'partners' => $partners, + 'per_partner' => $perPartner, + 'partner_amount' => $partnerAmount, + 'rank_bonus' => $rankBonus, + 'total' => round($partnerAmount + $rankBonus, 2), + ]; + } + + /** + * Auszahlungszeilen: alle Teilnehmer mit Praemie > 0, nach Rang. + * + * @return Collection + */ + public function rows(Incentive $incentive): Collection + { + return IncentiveParticipant::query() + ->where('incentive_id', $incentive->id) + ->with('user', 'user.account') + ->orderByRankNullsLast() + ->orderByDesc('qualified_partners') + ->get() + ->map(fn (IncentiveParticipant $participant) => [ + 'participant' => $participant, + 'payout' => $this->forParticipant($incentive, $participant), + ]) + ->filter(fn (array $row) => $row['payout']['total'] > 0) + ->values(); + } + + /** + * Summen ueber alle Auszahlungszeilen. + * + * @param Collection $rows + * @return array{recipients: int, partners: int, partner_amount: float, rank_bonus: float, total: float} + */ + public function totals(Collection $rows): array + { + return [ + 'recipients' => $rows->count(), + 'partners' => (int) $rows->sum(fn (array $row) => $row['payout']['partners']), + 'partner_amount' => round((float) $rows->sum(fn (array $row) => $row['payout']['partner_amount']), 2), + 'rank_bonus' => round((float) $rows->sum(fn (array $row) => $row['payout']['rank_bonus']), 2), + 'total' => round((float) $rows->sum(fn (array $row) => $row['payout']['total']), 2), + ]; + } + + /** + * Spaltenueberschriften des XLSX-Exports. + * + * @return list + */ + public function exportHeaders(): array + { + return [ + __('incentive.rank'), + __('incentive.export_user_id'), + __('incentive.consultant'), + 'E-Mail', + __('incentive.admin_terms_accepted'), + __('incentive.partners'), + __('incentive.amount_per_partner'), + __('incentive.payout_partner_amount'), + __('incentive.payout_rank_bonus'), + __('incentive.payout_total'), + __('incentive.total_points'), + __('incentive.qualified'), + ]; + } + + /** + * Zeilen des XLSX-Exports (Betraege als Zahlen, damit Excel summieren kann). + * + * @return list> + */ + public function exportRows(Incentive $incentive): array + { + return $this->rows($incentive)->map(function (array $row) { + $participant = $row['participant']; + $payout = $row['payout']; + $user = $participant->user; + $name = $user?->account + ? trim($user->account->first_name.' '.$user->account->last_name) + : ($user?->getFullName() ?: ''); + + return [ + $participant->rank ?? '', + (int) $participant->user_id, + $name, + (string) ($user?->email ?? ''), + $participant->accepted_terms_at + ? $participant->accepted_terms_at->format('d.m.Y H:i') + : __('incentive.admin_terms_pending'), + $payout['partners'], + $payout['per_partner'], + $payout['partner_amount'], + $payout['rank_bonus'], + $payout['total'], + (int) $participant->total_points, + $participant->is_qualified ? __('incentive.yes') : __('incentive.no'), + ]; + })->all(); + } + + public static function formatAmount(float $amount): string + { + return number_format($amount, 2, ',', '.').' €'; + } +} diff --git a/app/Services/Incentive/IncentivePointsLogRepairService.php b/app/Services/Incentive/IncentivePointsLogRepairService.php index a87265d0..8c7f184d 100644 --- a/app/Services/Incentive/IncentivePointsLogRepairService.php +++ b/app/Services/Incentive/IncentivePointsLogRepairService.php @@ -350,7 +350,7 @@ class IncentivePointsLogRepairService public function syncMissingSalesVolumeLogs(IncentiveParticipant $participant): int { $incentive = $participant->incentive; - if (! $incentive) { + if (! $incentive || ! $incentive->includesSalesPoints()) { return 0; } diff --git a/app/Services/Incentive/IncentiveTracker.php b/app/Services/Incentive/IncentiveTracker.php index 7c7be3de..ddb37d9a 100644 --- a/app/Services/Incentive/IncentiveTracker.php +++ b/app/Services/Incentive/IncentiveTracker.php @@ -136,7 +136,8 @@ class IncentiveTracker /** * Track accumulated sales volume points. * Punkte werden NUR gezaehlt wenn der Umsatz von einem gettrackten - * Neupartner oder Neuabo stammt. + * Neupartner oder Neuabo stammt. Incentives mit `include_sales_points = 0` + * (Recruiting-Challenge) bekommen keine Umsatzpunkte. */ public static function trackSalesVolume(UserSalesVolume $user_sales_volume): void { @@ -155,7 +156,7 @@ class IncentiveTracker // A. Pruefen ob der User ein gettrackter Neupartner ist $partner_trackings = IncentiveNewPartner::where('user_id', $user_sales_volume->user_id) - ->whereHas('participant.incentive', fn ($q) => $q->active()) + ->whereHas('participant.incentive', fn ($q) => $q->active()->includingSalesPoints()) ->with('participant.incentive') ->get(); @@ -210,7 +211,7 @@ class IncentiveTracker $abo_trackings = $userAboFromOrder ? IncentiveNewAbo::query() ->where('user_abo_id', $userAboFromOrder->id) - ->whereHas('participant.incentive', fn ($q) => $q->active()) + ->whereHas('participant.incentive', fn ($q) => $q->active()->includingSalesPoints()) ->with('participant.incentive') ->get() : collect(); diff --git a/database/migrations/2026_09_18_130000_add_payout_fields_to_incentives_table.php b/database/migrations/2026_09_18_130000_add_payout_fields_to_incentives_table.php new file mode 100644 index 00000000..316c3a7f --- /dev/null +++ b/database/migrations/2026_09_18_130000_add_payout_fields_to_incentives_table.php @@ -0,0 +1,51 @@ +decimal('amount_per_partner', 8, 2)->nullable()->after('max_winners') + ->comment('Geldpraemie je Neupartner in EUR (NULL = keine)'); + } + if (! Schema::hasColumn('incentives', 'bonus_rank_1')) { + $table->decimal('bonus_rank_1', 8, 2)->nullable()->after('amount_per_partner')->comment('Zusatz-Bonus Platz 1 in EUR'); + $table->decimal('bonus_rank_2', 8, 2)->nullable()->after('bonus_rank_1')->comment('Zusatz-Bonus Platz 2 in EUR'); + $table->decimal('bonus_rank_3', 8, 2)->nullable()->after('bonus_rank_2')->comment('Zusatz-Bonus Platz 3 in EUR'); + } + if (! Schema::hasColumn('incentives', 'include_sales_points')) { + $table->boolean('include_sales_points')->default(true)->after('bonus_rank_3') + ->comment('Umsatzpunkte der Neupartner/Neuabos mitzaehlen (0 = nur Einmalpunkte)'); + } + }); + } + + public function down(): void + { + Schema::table('incentives', function (Blueprint $table) { + $columns = array_values(array_filter( + ['amount_per_partner', 'bonus_rank_1', 'bonus_rank_2', 'bonus_rank_3', 'include_sales_points'], + fn (string $column) => Schema::hasColumn('incentives', $column) + )); + if ($columns !== []) { + $table->dropColumn($columns); + } + }); + } +}; diff --git a/dev/_tickets/entwicklungsplan-protokoll-september-2026.md b/dev/_tickets/entwicklungsplan-protokoll-september-2026.md index f8d28d6b..1754f62d 100644 --- a/dev/_tickets/entwicklungsplan-protokoll-september-2026.md +++ b/dev/_tickets/entwicklungsplan-protokoll-september-2026.md @@ -1,6 +1,6 @@ # Entwicklungsplan – Protokoll Zoom-Meeting 08.09.2026 -**Stand:** 18.09.2026 · **Status:** C1/C2 live seit 14.09., A4, A3 und A1+A2 umgesetzt (committet, Deploy offen); Abo-Paket damit bis auf A5 komplett. Audit vom 18.09. ([2026-09-18-audit-commits-16-09-2026.md](2026-09-18-audit-commits-16-09-2026.md)) geprüft, alle Befunde umgesetzt — Stellungnahme und Deploy-Folgen dort. +**Stand:** 18.09.2026 · **Status:** C1/C2 live seit 14.09., A4, A3 und A1+A2 umgesetzt (committet, Deploy offen); Abo-Paket damit bis auf A5 komplett. Audit vom 18.09. ([2026-09-18-audit-commits-16-09-2026.md](2026-09-18-audit-commits-16-09-2026.md)) geprüft, alle Befunde umgesetzt — Stellungnahme und Deploy-Folgen dort. **E1 (Recruiting-Challenge) umgesetzt 18.09., Deploy offen** — Konfiguration im Admin, Details unten. **Quellen:** `2026 09 Protokoll September-komprimiert.pdf` (Protokoll), `2026-09-14.md` (Ticket-Entwurf Abos #1–#3) **Verwandte Pläne:** `dev/2026-07-22/entwicklungsplan-2026-09-07.md` (K11–K14 behandeln dieselben Themen aus dem Kundenbrief; die dortigen Rückfragen F4–F7 sind weiterhin offen), `dev/2026-09-10-business-plan/` (Ledger-Plan, löst A07/K11 langfristig) @@ -21,7 +21,7 @@ | **C2** | Laufstand-Race: Provision liest zu niedrigen Monatsstand bei zeitgleichen Buchungen – **Stufe 1+2 live seit 14.09.**, Stufe 3 offen | Befund aus C1 | 0,5 + 1 PT | hoch (Provision) | – (keine Nachzahlung, Entscheid 14.09.) | | **D1** | Neuer Reiter „Events“ mit Foto-Galerien (wie News-Archiv) | Protokoll | 2–3 PT | niedrig | Fotos liegen vor | | **D2** | Top 3 firmenweit im Backoffice | Protokoll | 0,5 PT | rechtlich | Network-Anwalt (F3) | -| **E1** | Recruiting-Challenge Okt–Jan (50 €/Neupartner, Top 3) | Protokoll | 0,5 PT Konfiguration + 1–2 PT Erweiterung | niedrig | Details Dani/Alois | +| **E1** | Recruiting-Challenge Okt–Jan (50 €/Neupartner, Top 3) – **umgesetzt 18.09., Deploy offen (Migration + Anlage im Admin)** | Protokoll | 0,5 PT Konfiguration + 1–2 PT Erweiterung | niedrig | Bonushöhe/Texte Dani/Alois | **Reihenfolge (festgelegt 14.09.):** @@ -427,7 +427,7 @@ Die Zeilen entstehen in [`Payment::paymentStatusPaidAction()`](../../app/Service ## 5. Aktionen -### E1 – Recruiting-Challenge Oktober–Januar +### E1 – Recruiting-Challenge Oktober–Januar — umgesetzt 18.09., Deploy offen **Ziel (Protokoll):** Pro Neupartner mit Paket 50 € im Qualifikationszeitraum, Top 3 erhalten zusätzlich einen Bonus, Auszahlung gesammelt im Februar, wieder ein Ranking. Details klären Dani und Alois. @@ -441,6 +441,52 @@ Die Zeilen entstehen in [`Payment::paymentStatusPaidAction()`](../../app/Service **Risiko:** niedrig. Offene Details (Bonushöhe Top 3, Anrechnung von Stornos/Rücktritten innerhalb des Zeitraums – `IncentivePointsLog.is_storno` existiert) mit Dani/Alois klären. +**Umsetzung (18.09.2026)** + +Das Incentive-Modul trägt jetzt optional eine Geldprämie; Montenegro bleibt unverändert (alle neuen Felder leer bzw. Schalter an). + +- **Neue Felder** (`incentives`, Migration `2026_09_18_130000_add_payout_fields_to_incentives_table`): `amount_per_partner` (€ je Neupartner, NULL = keine Prämie), `bonus_rank_1/2/3` (Zusatz-Bonus je Platz), `include_sales_points` (Standard 1). Admin-Formular: neue Karte „Prämie / Auszahlung“. +- **Wertung nach Neupartnern:** Mit `include_sales_points = 0` schreibt der Tracker keine Umsatzpunkte der Neupartner/Neuabos mehr ([`IncentiveTracker::trackSalesVolume`](../../app/Services/Incentive/IncentiveTracker.php), Neuaufbau und Reparatur ebenso). Mit `points_partner_onetime = 1` und `points_abo_onetime = 0` entspricht der Punktestand damit der **Anzahl Neupartner mit Starterpaket**; die Rangliste bleibt die vorhandene (`updateRanking`). Neupartner = bezahlte Registrierung mit Starterpaket (`ShoppingOrder::wherePaidRegistrationIncludesStarterKit`), reine Mitgliedschaft zählt nicht. +- **Prämie:** [`IncentivePayoutService`](../../app/Services/Incentive/IncentivePayoutService.php): `qualified_partners × amount_per_partner` plus Rang-Bonus, letzterer nur für qualifizierte Teilnehmer auf Platz 1–`max_winners`. Grundlage ist der gespeicherte Stand des Teilnehmers (derselbe wie in der Rangliste). +- **Auszahlungs-Export:** Admin-Detailseite → Button „Auszahlungs-Export (XLSX)“ (`/admin/incentives/{id}/payout-export`): Rang, Berater-ID, Name, E-Mail, Teilnahme bestätigt, Neupartner, Betrag je Neupartner, Prämie Neupartner, Rang-Bonus, Prämie gesamt, Punkte, qualifiziert. Nur Zeilen mit Prämie > 0. Dazu Summenblock (Empfänger, Summen) und Prämienspalte in der Admin-Rangliste. **Gutschrift über `Credit` ist nicht gebaut** – die Februar-Auszahlung läuft über den Export in die Buchhaltung; Gutschriften ließen sich später aus derselben Liste erzeugen. +- **Mehrere aktive Incentives:** Sidenav listet alle aktiven Incentives (bei mehreren mit Namen), Dashboard zeigt je Incentive eine Karte mit eigenen Modal-IDs; `HomeController` liefert `activeIncentives`/`incentiveParticipants` (die alten Einzelvariablen bleiben befüllt). Montenegro kann so bis zum Abschluss parallel stehen bleiben. +- **Berater-Ansichten:** Teaser und Rangliste zeigen bei Geldprämie ohne Mindestqualifikation eine Prämien-Kachel statt „Dein Ticket“, blenden die Abo-Blöcke aus, wenn Abos keine Rolle spielen, und ergänzen die Rangliste um die Spalte „Prämie“; Header/Details zeigen „Deine Prämie bisher“. Der Punkte-Boost-Text erscheint nur, wenn Umsatzpunkte zählen. +- **Tests:** [`IncentivePayoutTest`](../../tests/Feature/Incentive/IncentivePayoutTest.php), [`IncentiveSalesPointsToggleTest`](../../tests/Feature/Incentive/IncentiveSalesPointsToggleTest.php), [`IncentiveAdminPayoutTest`](../../tests/Feature/Incentive/IncentiveAdminPayoutTest.php). Incentive-Suite: 48 grün, 1 vorbestehender Fehlschlag (`IncentiveTeaserPageTest`, Hero-Bild in der Galerie – unabhängig von E1). + +**Konfiguration der Challenge (Admin → Incentives → Neu):** + +| Feld | Wert | +|---|---| +| Qualifikationsbeginn / -ende | 01.10.2026 / 31.01.2027 | +| Berechnungsende | 31.01.2027 (ohne Umsatzpunkte gibt es keinen Nachlauf; 28.02. wäre nur nötig, wenn Umsätze zählen) | +| Einmalpunkte pro Partner / pro Abo | 1 / 0 | +| Mind. direkte Partner / Kundenabos | 0 / 0 | +| Max. Gewinner | 3 | +| Prämie je Neupartner | 50,00 | +| Bonus Platz 1 / 2 / 3 | **offen – Dani/Alois** | +| Umsatzpunkte mitzählen | **aus** | +| Status | Aktiv (ab 01.10.; früher anlegen ist unschädlich, getrackt wird nur im Zeitraum) | +| Bild, Untertitel, Beschreibung, Teilnahmebedingungen | **Dani/Alois** – ohne Beschreibung erscheint der Montenegro-Fallbacktext („Adria“), also Beschreibung unbedingt füllen | + +**Nachtragen nach dem 01.10.:** `php artisan incentive:calculate {id}` zieht Neupartner ab Qualifikationsbeginn nach (läuft ohnehin täglich 05:00 für alle aktiven Incentives). + +**Offen / bewusst nicht gebaut** + +- Bonushöhe Top 3, Beschreibungstexte, Bild (Dani/Alois). +- **Punktgleichstand** in den Top 3: Rangfolge ist dann bestätigte Teilnahme vor anonym, sonst Datenbankreihenfolge. Falls es auf die Boni ankommt, Regel festlegen (z. B. wer die Anzahl zuerst erreicht hat) – Umsetzung 0,5 PT. +- **Stornos/Rücktritte:** Ein einmal getrackter Neupartner bleibt gezählt, auch wenn die Registrierung später storniert wird (Verhalten wie Montenegro). Vor der Auszahlung die Liste gegen stornierte Registrierungsbestellungen prüfen oder Regel mit Dani/Alois klären. +- Gutschrift-Erzeugung aus dem Export (siehe oben). +- Firmenweite Rangliste (D2) unverändert offen; die Challenge-Rangliste ist wie Montenegro für alle aktiven Berater sichtbar. + +**Deploy** + +```bash +php artisan migrate # incentives: amount_per_partner, bonus_rank_1..3, include_sales_points +php artisan view:clear && php artisan route:clear +``` + +Danach Challenge im Admin anlegen (Tabelle oben). Kein Neuaufbau, keine Datenänderung an Montenegro. + --- ## 6. Rückfragen an die Kundin / Alois (vor Start des jeweiligen Pakets) diff --git a/resources/lang/de/incentive.php b/resources/lang/de/incentive.php index e75e61fa..0ea6b172 100644 --- a/resources/lang/de/incentive.php +++ b/resources/lang/de/incentive.php @@ -181,4 +181,32 @@ return [ // Galerie 'gallery_title' => 'Impressionen', + + // Praemie / Auszahlung (Recruiting-Challenge, E1) + 'payout_section' => 'Prämie / Auszahlung (Recruiting-Challenge)', + 'payout_section_help' => 'Optional. Leer lassen für ein reines Punkte-Incentive. Die Prämie wird aus der Anzahl Neupartner mit Starterpaket berechnet, der Rang-Bonus nur für qualifizierte Teilnehmer innerhalb der Gewinnerplätze.', + 'amount_per_partner' => 'Prämie je Neupartner (€)', + 'amount_per_partner_help' => 'z. B. 50,00 – wird je direkt gesponsertem Neupartner mit Starterpaket ausgezahlt.', + 'bonus_rank' => 'Bonus Platz :n (€)', + 'include_sales_points' => 'Umsatzpunkte der Neupartner/Abos mitzählen', + 'include_sales_points_help' => 'Aus: Die Wertung besteht nur aus den Einmalpunkten (z. B. 1 Punkt je Neupartner = Rangliste nach Anzahl Neupartner).', + 'payout_export' => 'Auszahlungs-Export (XLSX)', + 'payout_recipients' => 'Empfänger', + 'payout_partner_amount' => 'Prämie Neupartner', + 'payout_rank_bonus' => 'Rang-Bonus', + 'payout_total' => 'Prämie gesamt', + 'payout_column' => 'Prämie', + 'payout_incl_bonus' => 'inkl. :amount Rang-Bonus', + 'payout_so_far' => 'Deine Prämie bisher', + 'payout_tile_title' => 'Deine Prämie', + 'payout_per_partner_label' => 'je direkt gesponsertem Neupartner mit Starterpaket', + 'payout_per_partner_text' => 'Prämie: :amount je direkt gesponsertem Neupartner mit Starterpaket.', + 'payout_rank_bonus_label' => 'Zusatz-Bonus für Platz :n', + 'payout_top_bonus_title' => 'Zusatz-Bonus für die Top :n', + 'payout_top_bonus_text' => 'Zusatz-Bonus für die Top :n: :list.', + 'payout_rank_label' => 'Platz :n', + 'payout_ranking_hint' => 'Die Rangliste zählt deine Neupartner mit Starterpaket im Qualifikationszeitraum. Die Auszahlung erfolgt gesammelt nach Ende des Zeitraums.', + 'new_partners_short' => 'Neupartner', + 'badge_challenge' => 'Challenge', + 'export_user_id' => 'Berater-ID', ]; diff --git a/resources/lang/en/incentive.php b/resources/lang/en/incentive.php index 21213488..62c88c4b 100644 --- a/resources/lang/en/incentive.php +++ b/resources/lang/en/incentive.php @@ -181,4 +181,32 @@ return [ // Gallery 'gallery_title' => 'Impressions', + + // Praemie / Auszahlung (Recruiting-Challenge, E1) + 'payout_section' => 'Reward / payout (recruiting challenge)', + 'payout_section_help' => 'Optional. Leave empty for a points-only incentive. The reward is calculated from the number of new partners with a starter kit; the rank bonus only applies to qualified participants within the winner ranks.', + 'amount_per_partner' => 'Reward per new partner (€)', + 'amount_per_partner_help' => 'e.g. 50.00 – paid per directly sponsored new partner with a starter kit.', + 'bonus_rank' => 'Bonus rank :n (€)', + 'include_sales_points' => 'Count sales points of new partners/subscriptions', + 'include_sales_points_help' => 'Off: The score consists of one-time points only (e.g. 1 point per new partner = ranking by number of new partners).', + 'payout_export' => 'Payout export (XLSX)', + 'payout_recipients' => 'Recipients', + 'payout_partner_amount' => 'New partner reward', + 'payout_rank_bonus' => 'Rank bonus', + 'payout_total' => 'Total reward', + 'payout_column' => 'Reward', + 'payout_incl_bonus' => 'incl. :amount rank bonus', + 'payout_so_far' => 'Your reward so far', + 'payout_tile_title' => 'Your reward', + 'payout_per_partner_label' => 'per directly sponsored new partner with a starter kit', + 'payout_per_partner_text' => 'Reward: :amount per directly sponsored new partner with a starter kit.', + 'payout_rank_bonus_label' => 'Extra bonus for rank :n', + 'payout_top_bonus_title' => 'Extra bonus for the top :n', + 'payout_top_bonus_text' => 'Extra bonus for the top :n: :list.', + 'payout_rank_label' => 'Rank :n', + 'payout_ranking_hint' => 'The ranking counts your new partners with a starter kit during the qualification period. Payout is made collectively after the period ends.', + 'new_partners_short' => 'New partners', + 'badge_challenge' => 'Challenge', + 'export_user_id' => 'Consultant ID', ]; diff --git a/resources/lang/es/incentive.php b/resources/lang/es/incentive.php index d9d71f2e..482b6363 100644 --- a/resources/lang/es/incentive.php +++ b/resources/lang/es/incentive.php @@ -181,4 +181,32 @@ return [ // Galería 'gallery_title' => 'Impresiones', + + // Praemie / Auszahlung (Recruiting-Challenge, E1) + 'payout_section' => 'Prima / pago (reto de reclutamiento)', + 'payout_section_help' => 'Opcional. Déjalo vacío para un incentivo solo de puntos. La prima se calcula por el número de nuevos socios con paquete de inicio; el bono por puesto solo aplica a participantes cualificados dentro de los puestos ganadores.', + 'amount_per_partner' => 'Prima por nuevo socio (€)', + 'amount_per_partner_help' => 'p. ej. 50,00 – se paga por cada nuevo socio patrocinado directamente con paquete de inicio.', + 'bonus_rank' => 'Bono puesto :n (€)', + 'include_sales_points' => 'Contar puntos de ventas de nuevos socios/suscripciones', + 'include_sales_points_help' => 'Desactivado: la puntuación consta solo de los puntos únicos (p. ej. 1 punto por nuevo socio = ranking por número de nuevos socios).', + 'payout_export' => 'Exportación de pagos (XLSX)', + 'payout_recipients' => 'Destinatarios', + 'payout_partner_amount' => 'Prima nuevos socios', + 'payout_rank_bonus' => 'Bono por puesto', + 'payout_total' => 'Prima total', + 'payout_column' => 'Prima', + 'payout_incl_bonus' => 'incl. :amount bono por puesto', + 'payout_so_far' => 'Tu prima hasta ahora', + 'payout_tile_title' => 'Tu prima', + 'payout_per_partner_label' => 'por nuevo socio patrocinado directamente con paquete de inicio', + 'payout_per_partner_text' => 'Prima: :amount por nuevo socio patrocinado directamente con paquete de inicio.', + 'payout_rank_bonus_label' => 'Bono adicional para el puesto :n', + 'payout_top_bonus_title' => 'Bono adicional para el top :n', + 'payout_top_bonus_text' => 'Bono adicional para el top :n: :list.', + 'payout_rank_label' => 'Puesto :n', + 'payout_ranking_hint' => 'El ranking cuenta tus nuevos socios con paquete de inicio durante el periodo de cualificación. El pago se realiza de forma conjunta al finalizar el periodo.', + 'new_partners_short' => 'Nuevos socios', + 'badge_challenge' => 'Reto', + 'export_user_id' => 'ID del asesor', ]; diff --git a/resources/lang/fr/incentive.php b/resources/lang/fr/incentive.php index 6e743aad..65e722d1 100644 --- a/resources/lang/fr/incentive.php +++ b/resources/lang/fr/incentive.php @@ -156,4 +156,32 @@ return [ 'onetime_abo_activation' => 'Unique : activation abonnement', 'accumulated' => 'Chiffre d’affaires', 'gallery_title' => 'Impressions', + + // Praemie / Auszahlung (Recruiting-Challenge, E1) + 'payout_section' => 'Prime / versement (challenge de recrutement)', + 'payout_section_help' => 'Facultatif. Laisser vide pour un incentive à points uniquement. La prime est calculée d’après le nombre de nouveaux partenaires avec kit de démarrage ; le bonus de classement ne s’applique qu’aux participants qualifiés dans les places gagnantes.', + 'amount_per_partner' => 'Prime par nouveau partenaire (€)', + 'amount_per_partner_help' => 'p. ex. 50,00 – versée par nouveau partenaire parrainé directement avec kit de démarrage.', + 'bonus_rank' => 'Bonus place :n (€)', + 'include_sales_points' => 'Compter les points de vente des nouveaux partenaires/abonnements', + 'include_sales_points_help' => 'Désactivé : le score ne comprend que les points uniques (p. ex. 1 point par nouveau partenaire = classement par nombre de nouveaux partenaires).', + 'payout_export' => 'Export des versements (XLSX)', + 'payout_recipients' => 'Bénéficiaires', + 'payout_partner_amount' => 'Prime nouveaux partenaires', + 'payout_rank_bonus' => 'Bonus de classement', + 'payout_total' => 'Prime totale', + 'payout_column' => 'Prime', + 'payout_incl_bonus' => 'dont :amount de bonus de classement', + 'payout_so_far' => 'Ta prime jusqu’ici', + 'payout_tile_title' => 'Ta prime', + 'payout_per_partner_label' => 'par nouveau partenaire parrainé directement avec kit de démarrage', + 'payout_per_partner_text' => 'Prime : :amount par nouveau partenaire parrainé directement avec kit de démarrage.', + 'payout_rank_bonus_label' => 'Bonus supplémentaire pour la place :n', + 'payout_top_bonus_title' => 'Bonus supplémentaire pour le top :n', + 'payout_top_bonus_text' => 'Bonus supplémentaire pour le top :n : :list.', + 'payout_rank_label' => 'Place :n', + 'payout_ranking_hint' => 'Le classement compte tes nouveaux partenaires avec kit de démarrage pendant la période de qualification. Le versement est effectué groupé après la fin de la période.', + 'new_partners_short' => 'Nouveaux partenaires', + 'badge_challenge' => 'Challenge', + 'export_user_id' => 'ID du conseiller', ]; diff --git a/resources/views/admin/incentive/_form.blade.php b/resources/views/admin/incentive/_form.blade.php index f2edc6d5..17414ba2 100644 --- a/resources/views/admin/incentive/_form.blade.php +++ b/resources/views/admin/incentive/_form.blade.php @@ -121,6 +121,43 @@ +{{-- ===== PRAEMIE / AUSZAHLUNG (Recruiting-Challenge) ===== --}} +
+
+ + {{ __('incentive.payout_section') }} +
+
+

{{ __('incentive.payout_section_help') }}

+ +
+
+ + + {{ __('incentive.amount_per_partner_help') }} +
+ @for ($bonusRank = 1; $bonusRank <= \App\Models\Incentive::RANK_BONUS_SLOTS; $bonusRank++) +
+ + +
+ @endfor +
+ +
+ + includesSalesPoints() : 1)) checked @endif> + +
+ {{ __('incentive.include_sales_points_help') }} +
+
+ {{-- ===== INHALTE: DEUTSCH (Standard) ===== --}}
diff --git a/resources/views/admin/incentive/show.blade.php b/resources/views/admin/incentive/show.blade.php index 3ae470e0..88a0fd4c 100644 --- a/resources/views/admin/incentive/show.blade.php +++ b/resources/views/admin/incentive/show.blade.php @@ -8,6 +8,12 @@ {{ __('incentive.edit') }} + @if ($incentive->hasCashPayout()) + + {{ __('incentive.payout_export') }} + + @endif {{--
@csrf
{{ __('incentive.min_direct_partners') }}: {{ $incentive->min_direct_partners }}
- {{ __('incentive.min_customer_abos') }}: {{ $incentive->min_customer_abos }} + {{ __('incentive.min_customer_abos') }}: {{ $incentive->min_customer_abos }}
+ {{ __('incentive.include_sales_points') }}: + {{ $incentive->includesSalesPoints() ? __('incentive.yes') : __('incentive.no') }}
+ @if ($incentive->hasCashPayout()) +
+
+
+ {{ __('incentive.amount_per_partner') }}: + {{ \App\Services\Incentive\IncentivePayoutService::formatAmount((float) $incentive->amount_per_partner) }} +
+
+ @foreach ($incentive->rankBonuses() as $bonusRank => $bonusAmount) + {{ __('incentive.bonus_rank', ['n' => $bonusRank]) }}: + {{ \App\Services\Incentive\IncentivePayoutService::formatAmount($bonusAmount) }}
+ @endforeach +
+ @if ($payoutTotals) +
+ {{ __('incentive.payout_recipients') }}: {{ $payoutTotals['recipients'] }}
+ {{ __('incentive.payout_partner_amount') }}: + {{ \App\Services\Incentive\IncentivePayoutService::formatAmount($payoutTotals['partner_amount']) }}
+ {{ __('incentive.payout_rank_bonus') }}: + {{ \App\Services\Incentive\IncentivePayoutService::formatAmount($payoutTotals['rank_bonus']) }}
+ {{ __('incentive.payout_total') }}: + {{ \App\Services\Incentive\IncentivePayoutService::formatAmount($payoutTotals['total']) }} +
+ @endif +
+ @endif @@ -68,6 +102,9 @@ {{ __('incentive.partners') }} {{ __('incentive.abos') }} {{ __('incentive.qualified') }} + @if ($incentive->hasCashPayout()) + {{ __('incentive.payout_total') }} + @endif {{ __('incentive.status') }} @@ -117,6 +154,19 @@ {{ __('incentive.no') }} @endif + @if ($incentive->hasCashPayout()) + @php $payout = $payoutByParticipantId[$p->id] ?? null; @endphp + + @if ($payout && $payout['total'] > 0) + {{ \App\Services\Incentive\IncentivePayoutService::formatAmount($payout['total']) }} + @if ($payout['rank_bonus'] > 0) + {{ __('incentive.payout_incl_bonus', ['amount' => \App\Services\Incentive\IncentivePayoutService::formatAmount($payout['rank_bonus'])]) }} + @endif + @else + — + @endif + + @endif @if ($isWinner) {{ __('incentive.winner') }} @@ -136,7 +186,7 @@ @empty - {{ __('incentive.no_participants') }} + {{ __('incentive.no_participants') }} @endforelse diff --git a/resources/views/dashboard/_incentive.blade.php b/resources/views/dashboard/_incentive.blade.php index 66c510bb..4afc07f4 100644 --- a/resources/views/dashboard/_incentive.blade.php +++ b/resources/views/dashboard/_incentive.blade.php @@ -1,6 +1,19 @@ -@if (isset($activeIncentive) && $activeIncentive) +@php + // Mehrere aktive Incentives parallel (E1): Fallback auf die alte Einzelvariable. + $dashIncentives = isset($activeIncentives) && $activeIncentives + ? $activeIncentives + : collect(isset($activeIncentive) && $activeIncentive ? [$activeIncentive] : []); +@endphp +@foreach ($dashIncentives as $activeIncentive) @php + $incentiveParticipant = isset($incentiveParticipants) && $incentiveParticipants + ? $incentiveParticipants->get($activeIncentive->id) + : ($incentiveParticipant ?? null); $hasConfirmedDash = $incentiveParticipant && $incentiveParticipant->accepted_terms_at !== null; + $dashSuffix = '-' . $activeIncentive->id; + $dashPayout = $activeIncentive->hasCashPayout() && $incentiveParticipant + ? (new \App\Services\Incentive\IncentivePayoutService)->forParticipant($activeIncentive, $incentiveParticipant) + : null; @endphp
@@ -12,7 +25,7 @@
- Incentive VIP + {{ $activeIncentive->hasCashPayout() ? __('incentive.badge_challenge') : 'Incentive VIP' }}

{{ $activeIncentive->getLang('name') ?: $activeIncentive->name }} @@ -25,7 +38,7 @@ @else
- Incentive VIP + {{ $activeIncentive->hasCashPayout() ? __('incentive.badge_challenge') : 'Incentive VIP' }}

{{ $activeIncentive->getLang('name') ?: $activeIncentive->name }} @@ -51,8 +64,14 @@
{{ number_format($incentiveParticipant->total_points, 0, ',', '.') }} - {{ __('incentive.points_short') }} + {{ $activeIncentive->ranksByPartnerCount() ? __('incentive.new_partners_short') : __('incentive.points_short') }}
+ @if ($dashPayout) +
+ {{ \App\Services\Incentive\IncentivePayoutService::formatAmount($dashPayout['total']) }} + {{ __('incentive.payout_so_far') }} +
+ @endif
@if ($incentiveParticipant->is_qualified) {{ __('incentive.qualified') }} @@ -80,7 +99,7 @@ @endif @if ($activeIncentive->isActive()) @@ -145,12 +164,12 @@ {{-- Teilnahme-Modal --}} @if (!$hasConfirmedDash && $activeIncentive->isActive()) -