mivita/app/Http/Controllers/Admin/IncentiveController.php
2026-04-10 17:15:27 +02:00

411 lines
16 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Incentive;
use App\Models\IncentiveNewAbo;
use App\Models\IncentiveNewPartner;
use App\Models\IncentiveParticipant;
use App\Models\IncentivePointsLog;
use App\Models\UserAboOrder;
use App\Models\UserSalesVolume;
use App\Services\Incentive\IncentiveCalculationService;
use App\User;
use Request;
class IncentiveController extends Controller
{
public function __construct()
{
$this->middleware('admin');
}
public function index()
{
return view('admin.incentive.index');
}
public function create()
{
return view('admin.incentive.create', [
'languages' => config('localization.supportedLocales'),
]);
}
public function store()
{
$data = Request::validate([
'name' => 'required|string|max:255',
'subtitle' => 'nullable|string|max:255',
'description' => 'nullable|string',
'image' => 'nullable|string|max:255',
'terms' => 'nullable|string',
'qualification_start' => 'required|date',
'qualification_end' => 'required|date|after_or_equal:qualification_start',
'calculation_end' => 'required|date|after_or_equal:qualification_end',
'points_partner_onetime' => 'required|integer|min:0',
'points_abo_onetime' => 'required|integer|min:0',
'min_direct_partners' => 'required|integer|min:0',
'min_customer_abos' => 'required|integer|min:0',
'max_winners' => 'required|integer|min:1',
'status' => 'required|integer|in:0,1,2',
]);
$data = array_merge($data, $this->extractTranslations());
Incentive::create($data);
\Session()->flash('alert-success', __('incentive.created'));
return redirect(route('admin_incentives'));
}
public function show($id)
{
$incentive = Incentive::findOrFail($id);
$participants = IncentiveParticipant::where('incentive_id', $incentive->id)
->with('user', 'user.account')
->orderByIncentiveLeaderboard()
->get();
return view('admin.incentive.show', [
'incentive' => $incentive,
'participants' => $participants,
]);
}
public function edit($id)
{
$incentive = Incentive::findOrFail($id);
return view('admin.incentive.edit', [
'incentive' => $incentive,
'languages' => config('localization.supportedLocales'),
]);
}
public function update($id)
{
$data = Request::validate([
'name' => 'required|string|max:255',
'subtitle' => 'nullable|string|max:255',
'description' => 'nullable|string',
'image' => 'nullable|string|max:255',
'terms' => 'nullable|string',
'qualification_start' => 'required|date',
'qualification_end' => 'required|date|after_or_equal:qualification_start',
'calculation_end' => 'required|date|after_or_equal:qualification_end',
'points_partner_onetime' => 'required|integer|min:0',
'points_abo_onetime' => 'required|integer|min:0',
'min_direct_partners' => 'required|integer|min:0',
'min_customer_abos' => 'required|integer|min:0',
'max_winners' => 'required|integer|min:1',
'status' => 'required|integer|in:0,1,2',
]);
$data = array_merge($data, $this->extractTranslations());
$incentive = Incentive::findOrFail($id);
$incentive->update($data);
\Session()->flash('alert-success', __('incentive.updated'));
return redirect(route('admin_incentive_show', [$id]));
}
/**
* @return array{trans_name: array<string, string>, trans_description: array<string, string>, trans_terms: array<string, string>}
*/
private function extractTranslations(): array
{
$transName = [];
$transDescription = [];
$transTerms = [];
$transSubtitle = [];
foreach (config('localization.supportedLocales') as $locale => $localeData) {
if ($locale !== 'de') {
$transName[$locale] = Request::get('trans_name_'.$locale, '');
$transSubtitle[$locale] = Request::get('trans_subtitle_'.$locale, '');
$transDescription[$locale] = Request::get('trans_description_'.$locale, '');
$transTerms[$locale] = Request::get('trans_terms_'.$locale, '');
}
}
return [
'trans_name' => $transName,
'trans_subtitle' => $transSubtitle,
'trans_description' => $transDescription,
'trans_terms' => $transTerms,
];
}
public function recalculate($id)
{
$incentive = Incentive::findOrFail($id);
$service = new IncentiveCalculationService;
$stats = $service->recalculate($incentive, Request::has('force'));
\Session()->flash('alert-success', __('incentive.recalculated', [
'participants' => $stats['participants'],
'errors' => $stats['errors'],
]));
return redirect(route('admin_incentive_show', [$id]));
}
public function participantDetails($participant_id)
{
$participant = IncentiveParticipant::with('incentive', 'user', 'user.account')
->findOrFail($participant_id);
$data = self::buildParticipantDetailData($participant);
return view('admin.incentive._participant_details', $data);
}
/**
* Baut die Detail-Daten fuer einen Teilnehmer auf.
* Wird von Admin und User Controller genutzt.
*/
public static function buildParticipantDetailData(IncentiveParticipant $participant): array
{
$incentive = $participant->incentive;
$calculation_months = $incentive->getCalculationMonths();
// Alle Logs dieses Teilnehmers (ohne Stornos)
$all_logs = IncentivePointsLog::where('participant_id', $participant->id)
->where('is_storno', false)
->with('salesVolume')
->orderBy('created_at')
->get();
// UserSalesVolume-IDs -> User-ID Mapping aufbauen (fuer akkumulierte Partner-Punkte)
$sv_ids = $all_logs->whereNotNull('user_sales_volume_id')->pluck('user_sales_volume_id')->unique()->toArray();
$sv_user_map = ! empty($sv_ids)
? UserSalesVolume::whereIn('id', $sv_ids)->pluck('user_id', 'id')->toArray()
: [];
// Partner aus Tracking-Tabelle
$new_partners = IncentiveNewPartner::where('participant_id', $participant->id)
->with('user', 'user.account')
->orderBy('registered_at')
->get();
$partner_logs = $all_logs->where('type', 'partner');
$partner_sources = $new_partners->map(function ($np) use ($partner_logs, $sv_user_map, $calculation_months, $incentive) {
$monthly = [];
$transactions = [];
foreach ($calculation_months as $period) {
// Akkumulierte Logs: source_type=UserSalesVolume, deren USV.user_id == partner user_id
$month_logs = $partner_logs
->where('month', $period['month'])
->where('year', $period['year'])
->filter(function ($log) use ($np, $sv_user_map) {
if ($log->source_type === User::class) {
return false; // Einmal-Punkte nicht in Monatsspalte
}
if ($log->incentive_new_partner_id) {
return (int) $log->incentive_new_partner_id === (int) $np->id;
}
// Legacy: USV.user_id muss zum Partner gehoeren
return isset($sv_user_map[$log->user_sales_volume_id])
&& $sv_user_map[$log->user_sales_volume_id] === $np->user_id;
});
$month_points = (int) $month_logs->sum('points_accumulated');
$monthly[] = $month_points;
foreach ($month_logs as $log) {
$transactions[] = [
'date' => $log->created_at->format('d.m.Y'),
'month' => $period['month'],
'year' => $period['year'],
'label' => $log->source_label ?: 'KP #'.($log->user_sales_volume_id ?? $log->source_id),
'points' => $log->points_accumulated,
'type' => 'accumulated',
];
}
}
// Einmal-Punkte als Transaktion hinzufuegen
$onetime_log = $partner_logs
->where('source_type', User::class)
->first(function ($log) use ($np) {
if ($log->incentive_new_partner_id) {
return (int) $log->incentive_new_partner_id === (int) $np->id;
}
return (int) $log->source_id === (int) $np->user_id;
});
if ($onetime_log) {
array_unshift($transactions, [
'date' => $onetime_log->created_at->format('d.m.Y'),
'month' => $onetime_log->month,
'year' => $onetime_log->year,
'label' => __('incentive.onetime_registration'),
'points' => $onetime_log->points_onetime,
'type' => 'onetime',
]);
}
return [
'id' => $np->id,
'label' => $np->user ? ($np->user->getFullName() ?: $np->user->email ?: 'User #'.$np->user_id) : 'User #'.$np->user_id,
'month' => $np->registered_at->month,
'year' => $np->registered_at->year,
'onetime' => $incentive->points_partner_onetime,
'monthly' => $monthly,
'total' => $incentive->points_partner_onetime + array_sum($monthly),
'transactions' => $transactions,
];
});
// Abos aus Tracking-Tabelle
$new_abos = IncentiveNewAbo::where('participant_id', $participant->id)
->with('userAbo')
->orderBy('activated_at')
->get();
$abo_logs = $all_logs->where('type', 'abo');
// Legacy-Fallback: USV -> user_abo_id (Logs ohne incentive_new_abo_id)
$sv_user_abo_map = [];
$needs_legacy_abo_map = $abo_logs->whereNull('incentive_new_abo_id')->whereNotNull('user_sales_volume_id')->isNotEmpty();
if ($needs_legacy_abo_map && ! empty($sv_ids)) {
$sv_rows = UserSalesVolume::query()
->whereIn('id', $sv_ids)
->whereNotNull('shopping_order_id')
->get(['id', 'shopping_order_id']);
$order_ids = $sv_rows->pluck('shopping_order_id')->unique()->filter()->values();
if ($order_ids->isNotEmpty()) {
$user_abo_id_by_order_id = UserAboOrder::query()
->whereIn('shopping_order_id', $order_ids)
->get(['shopping_order_id', 'user_abo_id'])
->keyBy('shopping_order_id');
foreach ($sv_rows as $sv) {
$link = $user_abo_id_by_order_id->get($sv->shopping_order_id);
if ($link) {
$sv_user_abo_map[$sv->id] = (int) $link->user_abo_id;
}
}
}
}
$abo_sources = $new_abos->map(function ($na) use ($abo_logs, $sv_user_abo_map, $calculation_months, $incentive) {
$monthly = [];
$transactions = [];
$tracked_user_abo_id = (int) $na->user_abo_id;
foreach ($calculation_months as $period) {
$month_logs = $abo_logs
->where('month', $period['month'])
->where('year', $period['year'])
->filter(function ($log) use ($na, $tracked_user_abo_id, $sv_user_abo_map) {
if ($log->source_type !== UserSalesVolume::class) {
return false;
}
if ($log->incentive_new_abo_id) {
return (int) $log->incentive_new_abo_id === (int) $na->id;
}
$usv_id = $log->user_sales_volume_id;
if (! $usv_id || ! isset($sv_user_abo_map[$usv_id])) {
return false;
}
return $sv_user_abo_map[$usv_id] === $tracked_user_abo_id;
});
$month_points = (int) $month_logs->sum('points_accumulated');
$monthly[] = $month_points;
foreach ($month_logs as $log) {
$transactions[] = [
'date' => $log->created_at->format('d.m.Y'),
'month' => $period['month'],
'year' => $period['year'],
'label' => $log->source_label ?: 'SV #'.($log->user_sales_volume_id ?? $log->source_id),
'points' => $log->points_accumulated,
'type' => 'accumulated',
];
}
}
// Einmal-Punkte als Transaktion
$onetime_log = $abo_logs
->where('source_type', '!=', UserSalesVolume::class)
->first(function ($log) use ($na) {
if ($log->incentive_new_abo_id) {
return (int) $log->incentive_new_abo_id === (int) $na->id;
}
return (int) $log->source_id === (int) $na->user_abo_id;
});
if ($onetime_log) {
array_unshift($transactions, [
'date' => $onetime_log->created_at->format('d.m.Y'),
'month' => $onetime_log->month,
'year' => $onetime_log->year,
'label' => __('incentive.onetime_abo_activation'),
'points' => $onetime_log->points_onetime,
'type' => 'onetime',
]);
}
$label = $na->userAbo?->email ?: ('Abo #'.$na->user_abo_id);
return [
'id' => $na->id,
'label' => $label,
'month' => $na->activated_at->month,
'year' => $na->activated_at->year,
'onetime' => $incentive->points_abo_onetime,
'monthly' => $monthly,
'total' => $incentive->points_abo_onetime + array_sum($monthly),
'transactions' => $transactions,
];
});
return [
'incentive' => $incentive,
'participant' => $participant,
'calculation_months' => $calculation_months,
'partner_sources' => $partner_sources,
'abo_sources' => $abo_sources,
];
}
public function datatable()
{
$query = Incentive::query()->select('incentives.*');
return \DataTables::eloquent($query)
->addColumn('action', function (Incentive $incentive) {
return '<a href="'.route('admin_incentive_show', [$incentive->id]).'" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-eye"></span></a>
<a href="'.route('admin_incentive_edit', [$incentive->id]).'" class="btn icon-btn btn-sm btn-warning"><span class="fa fa-edit"></span></a>';
})
->addColumn('status_label', function (Incentive $incentive) {
return '<span class="badge badge-'.$incentive->getStatusColor().'">'.$incentive->getStatusType().'</span>';
})
->addColumn('period', function (Incentive $incentive) {
return $incentive->qualification_start->format('d.m.Y').' - '.$incentive->qualification_end->format('d.m.Y');
})
->addColumn('participants_count', function (Incentive $incentive) {
return $incentive->participants()->count();
})
->orderColumn('name', 'name $1')
->orderColumn('status_label', 'status $1')
->rawColumns(['action', 'status_label'])
->make(true);
}
}