update 20.10.2025

This commit is contained in:
Kevin Adametz 2025-10-20 17:42:08 +02:00
parent 8c11130b5d
commit a939cd51ef
616 changed files with 84821 additions and 4121 deletions

View file

@ -0,0 +1,405 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
use stdClass;
use Carbon\Carbon;
use App\Models\UserLevel;
use App\Models\UserBusiness;
use App\Services\TranslationHelper;
use App\Models\UserBusinessStructure;
class BusinessUserItem
{
public $businessUserItems = [];
private $date;
private $b_user;
private $user_level_active_pos;
public function __construct($date)
{
$this->date = $date;
return $this;
}
public function makeUser($user_id){
//check for user an load is saved
$this->b_user = UserBusiness::where('user_id', $user_id)->where('month', $this->date->month)->where('year', $this->date->year)->first();
if($this->b_user !== null){
return;
}
//read User here, can delete in stored data.
$user = User::find($user_id);
if(!$user){
return;
}
$user_level_active = $user->user_level ? $user->user_level : null;
$this->user_level_active_pos = $user_level_active ? $user_level_active->pos : 0;
$this->b_user = new UserBusiness();
$fill = [
'user_id' => $user->id,
'month' => $this->date->month,
'year' => $this->date->year,
'm_level_id' => $user->m_level,
'user_level_name' => $user_level_active ? $user_level_active->name : '',
'active_account' => $user->payment_account ? Carbon::parse($user->payment_account)->gt(Carbon::parse($this->date->start_date)) : false,
'payment_account_date' => $user->payment_account ? $user->getPaymentAccountDateFormat(false) : NULL,
'active_date' => $user->active_date ? $user->active_date : NULL,
'm_account' => $user->account->m_account,
'email' => $user->email,
'first_name' => $user->account->first_name,
'last_name' => $user->account->last_name,
'user_birthday' => $user->account->birthday,
'user_phone' => $user->account->getPhoneNumber(),
'sales_volume_KP_points' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_KP_points'),
'sales_volume_TP_points' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_TP_points'),
'sales_volume_points_shop' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_points_shop'),
'sales_volume_points_KP_sum' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_points_KP_sum'), //KP + Shop Points
'sales_volume_points_TP_sum' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_points_TP_sum'), //TP + Shop Points
'sales_volume_total' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_total'),
'sales_volume_total_shop' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_total_shop'),
'sales_volume_total_sum' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_total_sum'),
'margin' => $user_level_active ? $user_level_active->margin : 0, //is fix Rabatt für Kundenbestellungen
'margin_shop' => $user_level_active ? $user_level_active->margin_shop : 0, //is fix Rabatt für Shopbestellungen
'qual_kp' => $user_level_active ? $user_level_active->qual_kp : 0, //KP Kundenpoints from level
'qual_pp' => $user_level_active ? $user_level_active->qual_pp : 0, //PP Payline Points from level
'payline_points' => 0,
'commission_pp_total' => 0,
'commission_shop_sales' => 0,
'commission_growth_total' => 0,
'version' => 2,
];
$this->b_user->fill($fill);
$this->b_user->business_lines = [];
$this->b_user->user_items = [];
$this->b_user->commission_shop_sales = round($this->b_user->sales_volume_total_shop / 100 * $this->b_user->margin_shop, 2);
}
public function getSalesVolumeTotalMargin(){
return $this->b_user->getSalesVolumeTotalMargin();
}
public function addUserID(){
TreeCalcBot::addUserID($this->b_user->user_id);
}
public function getBUser(){
return $this->b_user;
}
public function addBusinessLineToUser($line, $obj){
$this->b_user->business_lines[$line] = $obj;
}
public function addBusinessLinePoints($line, $points){
$obj = $this->business_lines[$line];
$obj->points += $points;
$this->b_user->business_lines[$line] = $obj;
}
public function addTotalTP($points){
$this->b_user->total_pp += $points;
}
public function isQualKP(){
return ($this->sales_volume_points_KP_sum >= $this->qual_kp) ? true : false;
}
public function isQualLevel(){
return ($this->qual_user_level) ? true : false;
}
public function isQualEqualLevel(){
if($this->qual_user_level){
return ($this->m_level_id == $this->qual_user_level['id']) ? true : false;
}
return false;
}
public function getQualLevelPaylines(){
if($this->qual_user_level){
return $this->qual_user_level['paylines'];
}
return 0;
}
public function isQualLevelGrowth($line){
if(isset($this->business_lines[$line])){
$object = $this->business_lines[$line];
if(isset($object->growth_bonus)){
return true;
}
}
return false;
}
public function getRestQualKP(){
$ret = $this->sales_volume_points_KP_sum - $this->qual_kp;
return $ret > 0 ? $ret : 0;
}
public function getCommissionTotal(){
return round($this->commission_shop_sales + $this->commission_pp_total + $this->commission_growth_total, 2);
}
//Provisierungslevel brechnen, Berechnung der Provisionen nach Level
public function calcQualPP(){
//das ist der erreichte Provisierungslevel, nach paylinePoints und KP
$qualUserLevel = $this->calcuQualLevel();
if($qualUserLevel !== NULL){
//prüfe einen Aufsieg im KarriereLevel
$this->setNextUserLevel();
$this->b_user->qual_user_level = $qualUserLevel->toArray();
//setzen nächsten ProvisionsLevel wenn not isQualEqualLevel
$this->setQualNextLevel();
//Berechnung der Provisionen in der Payline
$commission_pp_total = 0;
$commission_growth_total = 0;
for ($i=1; $i <= $qualUserLevel->paylines ; $i++) {
if(isset($this->business_lines[$i])){
$object = $this->business_lines[$i];
$object->margin = $this->qual_user_level['pr_line_'.$i]; //provision in %
$object->commission = round($object->points / 100 * $object->margin, 2); //provision in points/euro
$object->payline = true;
$commission_pp_total += $object->commission;
$this->b_user->business_lines[$i] = $object;
}
}
//Berechnung der Provisionen nach WB
if($qualUserLevel->growth_bonus){
//['growth_bonus'] //
$payline = (int) $this->b_user->qual_user_level['paylines'] + 1;
$maxlines = count($this->business_lines) + 1;
$growth_bonus = (float) $this->b_user->qual_user_level['growth_bonus'];
for ($i=$payline; $i <= $maxlines ; $i++) {
if(isset($this->business_lines[$i])){
$object = $this->business_lines[$i];
$object->margin = $growth_bonus; //provision in %
$object->commission = round($object->points / 100 * $object->margin, 2); //provision in points/euro
$object->growth_bonus = true;
$commission_growth_total += $object->commission;
$this->b_user->business_lines[$i] = $object;
}
}
}
$this->b_user->commission_pp_total = $commission_pp_total;
$this->b_user->commission_growth_total = $commission_growth_total;
}else{
//erste Provisierungslevel als next setzen, hat keine oder wenig points
$qualUserLevelNext = UserLevel::where('pos', '=', 1)->orderBy('qual_pp', 'asc')->first();
if($qualUserLevelNext){
$this->b_user->qual_user_level_next = $qualUserLevelNext->toArray();
}
}
}
//qualifikation nach qual_kp (KundenPoints) und qual_pp (PaylinePoints)
public function calcuQualLevel(){
//alle levels wo die qual_kp erreicht ist, sortiert nach Rang >
$qualUserLevels = UserLevel::where('qual_kp', '<=', $this->sales_volume_points_KP_sum)->where('pos', '<=', $this->user_level_active_pos)->orderBy('qual_pp', 'desc')->get();
foreach($qualUserLevels as $qualUserLevel){
//brechnet die Points nach der Payline
$payline_points = $this->getPointsforPayline($qualUserLevel->paylines);
$payline_points_qual_kp = $payline_points + $this->getRestQualKP();
if($payline_points_qual_kp >= $qualUserLevel->qual_pp){
//match payline_points erreicht, ist der akutelle Level für die Provision
$this->b_user->payline_points = $payline_points;
$this->b_user->payline_points_qual_kp = $payline_points_qual_kp;
return $qualUserLevel;
}
}
return NULL;
}
// PaylinePoints nach paylines / welche ebenen gezählt werden, 3,4,5,6 ...
private function getPointsforPayline($paylines){
$payline_points = 0;
for ($i=1; $i <= $paylines ; $i++) {
if(isset($this->business_lines[$i])){
$payline_points += $this->business_lines[$i]->points;
}
}
return $payline_points;
}
//wenn nicht erreicht, was wäre der nächste Provisionslevel?
private function setQualNextLevel(){
if(!$this->isQualEqualLevel()){
$qualUserLevelNext = UserLevel::where('id', '=', $this->b_user->qual_user_level['next_id'])->orderBy('qual_pp', 'asc')->first();
if($qualUserLevelNext){
$this->b_user->qual_user_level_next = $qualUserLevelNext->toArray();
}
}
}
private function setNextUserLevel(){
// $this->b_user->payline_points_qual_kp // das sind die Payline Points + Rest KP
//$this->b_user->total_qual_pp = $this->total_pp + $this->getRestQualKP(); //hier werden alle Linien TP gezähle
//$this->b_user->total_qual_pp = $this->total_pp + $this->getRestQualKP(); //hier werden alle Linien TP gezähle
$nextQualUserLevel = UserLevel::where('qual_pp', '<=', $this->payline_points_qual_kp)->where('pos', '>', $this->user_level_active_pos)->orderBy('qual_pp', 'desc')->first();
if($nextQualUserLevel && $this->isQualKP()){
$this->b_user->next_qual_user_level = $nextQualUserLevel->toArray();
}else{
//wenn nicht erreicht, was wäre der nächste Karrierelevel?
$nextCanUserLevel = UserLevel::where('pos', '>', $this->user_level_active_pos)->orderBy('qual_pp', 'asc')->first();
if($nextCanUserLevel){
$this->b_user->next_can_user_level = $nextCanUserLevel->toArray();
}
}
}
/*public function storeUser(){
$this->b_user->user_items = $this->storeUserItems($this->businessUserItems, 1);
$this->b_user->save();
}
private function storeUserItems($userItems, $line){
$ret = [];
foreach($userItems as $userItem){
$temp = null;
if(count($userItem->businessUserItems) > 0){
$temp = $this->storeUserItems($userItem->businessUserItems, $line+1);
}
$obj = new stdClass();
$obj->user_id = $userItem->user_id;
$obj->line = $line;
$obj->points = $userItem->sales_volume_points_sum;
$obj->parents = $temp;
$ret[] = $obj;
}
return $ret;
}*/
public function readParentsBusinessUsers(){
$users = User::with('account')->select('users.*')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', "<", 4)
->where('users.m_level', "!=", null)
->where('users.m_sponsor', "=", $this->b_user->user_id) //<- need the id for parents / sponsors
->where('users.payment_account', "!=", null)
->where('users.active_date', "<=", $this->date->end_date) // wurde in dem Monat freigeschaltet
->get();
if($users){
foreach($users as $user){
$BusinessUserItem = new BusinessUserItem($this->date);
$BusinessUserItem->makeUser($user->id);
$BusinessUserItem->addUserID();
$this->businessUserItems[] = $BusinessUserItem;
}
}
foreach($this->businessUserItems as $businessUserItem){
$businessUserItem->readParentsBusinessUsers();
}
}
public function readStoredParentsBusinessUsers($structure){
$parents = $this->findParentsBusinessOnStored($this->b_user->user_id, $structure);
if($parents){
foreach($parents as $obj){
$BusinessUserItem = new BusinessUserItem($this->date);
$BusinessUserItem->makeUser($obj->user_id);
$BusinessUserItem->addUserID();
$this->businessUserItems[] = $BusinessUserItem;
}
foreach($this->businessUserItems as $businessUserItem){
$businessUserItem->readStoredParentsBusinessUsers($parents);
}
}
}
private function findParentsBusinessOnStored($user_id, $structures){
if($structures){
foreach($structures as $obj){
if($user_id === $obj->user_id){
return $obj->parents;
}
if($obj->parents){
if($ret = $this->findParentsBusinessOnStored($user_id, $obj->parents)){
return $ret;
}
}
}
}
return null;
}
public function checkSponsor($user){
//check is store? has ID
if($this->b_user->isSave()){
return;
}
$sponsor = new stdClass();
$sponsor->is_sponsor = false;
$sponsor->user_id = false;
$sponsor->first_name = '';
$sponsor->last_name = '';
$sponsor->email = '';
$sponsor->m_account = '';
$sponsor->full_name = 'Keinen Sponsor zugewiesen';
if($user->m_sponsor){
if($user->user_sponsor){
$sponsor->is_sponsor = true;
$sponsor->user_id = $user->user_sponsor->id;
if($user->user_sponsor->account){
$sponsor->full_name = substr('Sponsor: '.$user->user_sponsor->account->first_name.' '.$user->user_sponsor->account->last_name.' | '.$user->user_sponsor->email.' | '.$user->user_sponsor->account->m_account, 0, 250);
$sponsor->first_name = $user->user_sponsor->account->last_name;
$sponsor->last_name = $user->user_sponsor->account->first_name;
$sponsor->m_account = $user->user_sponsor->account->m_account;
}else{
$sponsor->full_name = 'Sponsor: '.$user->user_sponsor->email;
}
$sponsor->email = $user->user_sponsor->email;
}else{
$sponsor->full_name = 'Sponsor wurde gelöscht.';
}
}
$this->b_user->sponsor = $sponsor;
return;
}
public function isSave(){
return $this->b_user->isSave();
}
public function __get($property) {
if($this->b_user === null){
return null;
}
if (property_exists($this->b_user, $property)) {
return $this->b_user->$property;
}
if (isset($this->b_user->{$property})) {
return $this->b_user->{$property};
}
}
}

View file

@ -0,0 +1,947 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
use stdClass;
use Carbon\Carbon;
use App\Models\UserLevel;
use App\Models\UserBusiness;
use App\Models\UserAccount;
use App\Models\UserSalesVolume;
use App\Services\TranslationHelper;
use App\Models\UserBusinessStructure;
use Illuminate\Support\Facades\Log;
/**
* Optimierte Version der BusinessUserItem Klasse
*
* Hauptverbesserungen:
* - makeUserFromModel() für bereits geladene User-Objekte
* - Bessere Error-Behandlung mit Logging
* - Optimierte Datenbankzugriffe durch Relations-Nutzung
* - Input-Validierung und Boundary-Checks
*/
class BusinessUserItemOptimized
{
public $businessUserItems = [];
private $date;
private $b_user;
private ?TreeCalcBotOptimized $treeCalcBot = null;
private $user_level_active_pos;
private $needsQualificationRecalculation = false;
public function __construct($date, ?TreeCalcBotOptimized $treeCalcBot = null)
{
$this->date = $date;
$this->treeCalcBot = $treeCalcBot;
$this->businessUserItems = []; // Initialize array
return $this;
}
/**
* Erstellt BusinessUser aus User-ID (Original-Methode für Rückwärtskompatibilität)
*
* @param int $user_id Die User-ID
* @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten
*/
public function makeUser($user_id, bool $forceLiveCalculation = false): void
{
try {
// Prüfe nur nach gespeicherten Daten, wenn keine Live-Berechnung erzwungen wird
if (!$forceLiveCalculation) {
$this->b_user = UserBusiness::where('user_id', $user_id)
->where('month', $this->date->month)
->where('year', $this->date->year)
->first();
if ($this->b_user !== null) {
\Log::debug("BusinessUserItem: Using stored data for user {$user_id} ({$this->date->month}/{$this->date->year})");
// WICHTIG: Auch bei gespeicherten Daten User-Model laden für Grunddaten
$user = User::with(['account', 'user_level'])->find($user_id);
if ($user) {
$this->enrichStoredDataWithUserModel($user);
// Prüfe ob Level-Qualifikationsdaten nachberechnet werden müssen
if ($this->needsQualificationRecalculation) {
\Log::debug("BusinessUserItem: Triggering qualification recalculation for user {$user_id}");
$this->calcQualPP(); // Berechne fehlende Level-Qualifikationsdaten
}
}
return; // Bereits gespeicherte Daten verwenden
}
} else {
\Log::debug("BusinessUserItem: Force live calculation for user {$user_id} ({$this->date->month}/{$this->date->year})");
}
// Lade User mit Relations (weniger effizient als makeUserFromModel)
$user = User::with(['account', 'user_level'])->find($user_id);
if (!$user) {
\Log::warning("BusinessUserItem: User not found: {$user_id}");
return;
}
$this->initializeFromUserModel($user);
// WICHTIG: Bei Live-Berechnung auch Level-Qualifikationsdaten berechnen
// (nicht bei forceLiveCalculation=false, da dort gespeicherte Daten bevorzugt werden)
if ($forceLiveCalculation) {
$this->calcQualPP();
}
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error creating user {$user_id}: " . $e->getMessage());
throw $e;
}
}
/**
* NEUE OPTIMIERTE METHODE: Erstellt BusinessUser aus bereits geladenem User-Objekt
* Konsistent zur ursprünglichen makeUser Logik - prüft explizit nach bereits berechneten Daten
*
* @param User $user Das User-Model
* @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten
*/
public function makeUserFromModel(User $user, bool $forceLiveCalculation = false): void
{
try {
if (!$user || !$user->id) {
throw new \InvalidArgumentException('Invalid user model provided');
}
// Prüfe nur nach gespeicherten Daten, wenn keine Live-Berechnung erzwungen wird
if (!$forceLiveCalculation) {
$this->b_user = UserBusiness::where('user_id', $user->id)
->where('month', $this->date->month)
->where('year', $this->date->year)
->first();
if ($this->b_user !== null) {
\Log::debug("BusinessUserItem: Using stored data for user {$user->id} ({$this->date->month}/{$this->date->year})");
// WICHTIG: Auch bei gespeicherten Daten User-Grunddaten anreichern
$this->enrichStoredDataWithUserModel($user);
// Prüfe ob Level-Qualifikationsdaten nachberechnet werden müssen
if ($this->needsQualificationRecalculation) {
\Log::debug("BusinessUserItem: Triggering qualification recalculation for user {$user->id}");
$this->calcQualPP(); // Berechne fehlende Level-Qualifikationsdaten
}
return; // Bereits berechnete Daten verwenden
}
} else {
\Log::debug("BusinessUserItem: Force live calculation for user {$user->id} ({$this->date->month}/{$this->date->year})");
}
// Erstelle neuen User und führe Live-Berechnung durch
$this->initializeFromUserModel($user);
// WICHTIG: Bei Live-Berechnung auch Level-Qualifikationsdaten berechnen
// (nicht bei forceLiveCalculation=false, da dort gespeicherte Daten bevorzugt werden)
if ($forceLiveCalculation) {
$this->calcQualPP();
}
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error creating user from model {$user->id}: " . $e->getMessage());
throw $e;
}
}
/**
* Initialisiert BusinessUser aus User-Model (gemeinsame Logik)
*/
private function initializeFromUserModel(User $user): void
{
// Nutze geladene Relations wenn verfügbar
$user_level_active = null;
if ($user->relationLoaded('user_level')) {
$user_level_active = $user->user_level;
} else {
$user_level_active = $user->user_level; // Fallback auf Original-Relation
}
$this->user_level_active_pos = $user_level_active ? $user_level_active->pos : 0;
// Neues UserBusiness Objekt erstellen
$this->b_user = new UserBusiness();
// Account-Daten (mit intelligentem Laden und Error-Handling)
$account = $this->getAccountForUser($user);
$fill = [
'user_id' => $user->id,
'month' => $this->date->month,
'year' => $this->date->year,
'm_level_id' => $user->m_level,
'user_level_name' => $user_level_active ? $user_level_active->name : '',
'active_account' => $this->calculateActiveAccount($user),
'payment_account_date' => $user->payment_account ? $user->getPaymentAccountDateFormat(false) : null,
'active_date' => $user->active_date,
// Account-Daten mit korrekten Fallback-Werten
'm_account' => $account ? ($account->m_account ?? null) : null,
'email' => $user->email,
'first_name' => $account ? ($account->first_name ?? '') : '',
'last_name' => $account ? ($account->last_name ?? '') : '',
'user_birthday' => $account ? $account->birthday : null,
'user_phone' => $account ? ($account->getPhoneNumber() ?? '') : '',
// Sales Volume (mit Caching falls möglich)
'sales_volume_KP_points' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_KP_points'),
'sales_volume_TP_points' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_TP_points'),
'sales_volume_points_shop' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_points_shop'),
'sales_volume_points_KP_sum' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_points_KP_sum'),
'sales_volume_points_TP_sum' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_points_TP_sum'),
'sales_volume_total' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_total'),
'sales_volume_total_shop' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_total_shop'),
'sales_volume_total_sum' => $this->getUserSalesVolumeOptimized($user, 'sales_volume_total_sum'),
// Level-Daten mit Boundary-Checks
'margin' => $user_level_active ? max(0, $user_level_active->margin) : 0,
'margin_shop' => $user_level_active ? max(0, $user_level_active->margin_shop) : 0,
'qual_kp' => $user_level_active ? max(0, $user_level_active->qual_kp) : 0,
'qual_pp' => $user_level_active ? max(0, $user_level_active->qual_pp) : 0,
// Initialisierung
'payline_points' => 0,
'commission_pp_total' => 0,
'commission_shop_sales' => 0,
'commission_growth_total' => 0,
'version' => 2,
];
$this->b_user->fill($fill);
$this->b_user->business_lines = [];
$this->b_user->user_items = [];
// Shop-Provision berechnen (mit verbessertem Logging)
$shopVolume = (float) $this->b_user->sales_volume_total_shop;
$shopMargin = (float) $this->b_user->margin_shop;
$calculatedCommission = round($shopVolume / 100 * $shopMargin, 2);
$this->b_user->commission_shop_sales = $calculatedCommission;
\Log::debug("BusinessUserItem: Created optimized user {$user->id} for {$this->date->month}/{$this->date->year} - Shop commission: {$calculatedCommission} (Volume: {$shopVolume}, Margin: {$shopMargin}%)");
}
/**
* Ergänzt gespeicherte UserBusiness-Daten mit aktuellen User-Grunddaten
* Erweitert um Level-Qualifikationsdaten-Validierung für Struktur-Ansicht
*/
private function enrichStoredDataWithUserModel(User $user): void
{
try {
$account = $this->getAccountForUser($user);
// Ergänze fehlende User-Grunddaten in gespeicherten UserBusiness-Daten
$this->b_user->user_id = $user->id;
$this->b_user->email = $user->email;
$this->b_user->first_name = $account ? ($account->first_name ?? '') : '';
$this->b_user->last_name = $account ? ($account->last_name ?? '') : '';
$this->b_user->user_birthday = $account ? $account->birthday : null;
$this->b_user->user_phone = $account ? ($account->getPhoneNumber() ?? '') : '';
$this->b_user->m_account = $account ? ($account->m_account ?? null) : null;
// Berechne aktiven Account-Status
$this->b_user->active_account = $this->calculateActiveAccount($user);
$this->b_user->payment_account_date = $user->payment_account;
// User-Level Informationen
$user_level_active = $user->user_level;
if ($user_level_active) {
$this->b_user->user_level_name = $user_level_active->name;
$this->user_level_active_pos = $user_level_active->pos;
}
// WICHTIG: Validiere Level-Qualifikationsdaten für Struktur-Ansicht
$this->validateLevelQualificationData();
// Prüfe ob Sales Volume Felder aktualisiert werden müssen
$this->updateSalesVolumeFields($user);
\Log::debug("BusinessUserItem: Enriched stored data for user {$user->id} with current user model data");
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error enriching stored data for user {$user->id}: " . $e->getMessage());
}
}
/**
* Aktualisiert Sales Volume und Commission Felder bei gespeicherten Daten
*/
private function updateSalesVolumeFields(User $user): void
{
try {
// Prüfe ob Sales Volume Felder leer sind
$fieldsToUpdate = [
'sales_volume_KP_points',
'sales_volume_TP_points',
'sales_volume_points_shop',
'sales_volume_points_KP_sum',
'sales_volume_points_TP_sum',
'sales_volume_total',
'sales_volume_total_shop',
'sales_volume_total_sum'
];
$needsUpdate = false;
foreach ($fieldsToUpdate as $field) {
if (!isset($this->b_user->{$field}) || $this->b_user->{$field} === null || $this->b_user->{$field} === 0) {
$newValue = $this->getUserSalesVolumeOptimized($user, $field);
$this->b_user->{$field} = $newValue;
if ($newValue > 0) {
$needsUpdate = true;
\Log::debug("BusinessUserItem: Updated {$field} for user {$user->id}: {$newValue}");
}
}
}
// Aktualisiere Shop Commission falls nötig
if (!isset($this->b_user->commission_shop_sales) || $this->b_user->commission_shop_sales === 0) {
$shopVolume = (float) $this->b_user->sales_volume_total_shop;
$shopMargin = (float) $this->b_user->margin_shop;
if ($shopVolume > 0 && $shopMargin > 0) {
$calculatedCommission = round($shopVolume / 100 * $shopMargin, 2);
$this->b_user->commission_shop_sales = $calculatedCommission;
$needsUpdate = true;
\Log::debug("BusinessUserItem: Updated commission_shop_sales for user {$user->id}: {$calculatedCommission}");
}
}
if ($needsUpdate) {
\Log::info("BusinessUserItem: Updated sales volume fields for user {$user->id}");
}
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error updating sales volume fields for user {$user->id}: " . $e->getMessage());
}
}
/**
* Validiert und aktualisiert Level-Qualifikationsdaten wenn nötig
* Stellt sicher, dass next_qual_user_level und next_can_user_level für Struktur-Ansicht verfügbar sind
*/
private function validateLevelQualificationData(): void
{
try {
// Prüfe ob Level-Qualifikationsdaten vorhanden sind
$hasNextQual = !empty($this->b_user->next_qual_user_level);
$hasNextCan = !empty($this->b_user->next_can_user_level);
$hasQualUserLevel = !empty($this->b_user->qual_user_level);
// Wenn Level-Qualifikationsdaten fehlen, führe Neuberechnung durch
if (!$hasNextQual && !$hasNextCan && !$hasQualUserLevel) {
\Log::debug("BusinessUserItem: Level qualification data missing for user {$this->b_user->user_id}, triggering recalculation");
// Setze Flag für notwendige Neuberechnung
$this->needsQualificationRecalculation = true;
}
} catch (\Exception $e) {
\Log::warning("BusinessUserItem: Error validating level qualification data for user {$this->b_user->user_id}: " . $e->getMessage());
}
}
/**
* Berechnet ob Account aktiv ist (mit Error-Handling)
*/
private function calculateActiveAccount(User $user): bool
{
try {
if (!$user->payment_account) {
return false;
}
// Verwende aktuelles Datum, nicht das Berechnungs-Startdatum
return Carbon::parse($user->payment_account)->gt(Carbon::now());
} catch (\Exception $e) {
\Log::warning("BusinessUserItem: Error calculating active account for user {$user->id}: " . $e->getMessage());
return false;
}
}
/**
* Optimierte Sales Volume Abfrage mit detailliertem Logging
*/
private function getUserSalesVolumeOptimized(User $user, string $field)
{
try {
// Direkter Aufruf mit detailliertem Logging
$value = $user->getUserSalesVolumeBy($this->date->month, $this->date->year, $field);
// Log nur bei ersten Aufruf für diesen User (Performance)
static $loggedUsers = [];
if (!isset($loggedUsers[$user->id])) {
$loggedUsers[$user->id] = true;
// Prüfe ob UserSalesVolume Daten existieren
$userSalesVolume = $user->getUserSalesVolume($this->date->month, $this->date->year, 'first');
if (!$userSalesVolume) {
\Log::info("BusinessUserItem: No UserSalesVolume found for user {$user->id} in {$this->date->month}/{$this->date->year}");
// Prüfe neueste verfügbare Daten
$latestVolume = \App\Models\UserSalesVolume::where('user_id', $user->id)
->orderBy('year', 'desc')
->orderBy('month', 'desc')
->first();
if ($latestVolume) {
\Log::info("BusinessUserItem: Latest UserSalesVolume for user {$user->id}: {$latestVolume->month}/{$latestVolume->year}");
} else {
\Log::warning("BusinessUserItem: No UserSalesVolume records found for user {$user->id} at all");
}
} else {
\Log::debug("BusinessUserItem: UserSalesVolume found for user {$user->id} in {$this->date->month}/{$this->date->year}");
}
}
return $value;
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error getting sales volume {$field} for user {$user->id}: " . $e->getMessage());
return 0; // Sicherer Fallback
}
}
// ===== ORIGINALE METHODEN (unverändert für Kompatibilität) =====
public function getSalesVolumeTotalMargin()
{
return $this->b_user->getSalesVolumeTotalMargin();
}
public function addUserID()
{
if ($this->treeCalcBot) {
$this->treeCalcBot->addProcessedUserId($this->b_user->user_id);
} else {
// Fallback für Rückwärtskompatibilität - sollte in Logs sichtbar sein
\Log::warning("BusinessUserItemOptimized: TreeCalcBotOptimized Referenz fehlt für User ID: " . $this->b_user->user_id);
}
}
public function getBUser()
{
return $this->b_user;
}
public function addBusinessLineToUser($line, $obj)
{
$this->b_user->business_lines[$line] = $obj;
}
public function addBusinessLinePoints($line, $points)
{
if (!isset($this->b_user->business_lines[$line])) {
\Log::warning("BusinessUserItem: Trying to add points to non-existent line {$line}");
return;
}
$obj = $this->b_user->business_lines[$line];
// Handle both array and object types (JSON deserialization inconsistency)
if (is_array($obj)) {
$obj['points'] = ($obj['points'] ?? 0) + (float) $points;
} else {
// Ensure it's an object
if (!is_object($obj)) {
$obj = (object) $obj;
}
$obj->points = ($obj->points ?? 0) + (float) $points;
}
$this->b_user->business_lines[$line] = $obj;
}
public function addTotalTP($points)
{
$this->b_user->total_pp += (float) $points; // Type-Safety
}
public function isQualKP(): bool
{
return ($this->b_user->sales_volume_points_KP_sum >= $this->b_user->qual_kp);
}
public function isQualLevel(): bool
{
return !empty($this->b_user->qual_user_level);
}
public function isQualEqualLevel(): bool
{
if (!$this->b_user->qual_user_level) {
return false;
}
return ($this->b_user->m_level_id == $this->b_user->qual_user_level['id']);
}
public function getQualPaylines(): int
{
if (!$this->b_user->qual_user_level) {
return 0;
}
return (int) $this->b_user->qual_user_level['paylines'];
}
public function getRestQualKP(): float
{
$ret = $this->b_user->sales_volume_points_KP_sum - $this->b_user->qual_kp;
return max(0, $ret); // Boundary-Check
}
public function getCommissionTotal(): float
{
return round(
$this->b_user->commission_shop_sales +
$this->b_user->commission_pp_total +
$this->b_user->commission_growth_total,
2
);
}
// ===== PROVISIONSBERECHNUNG (Original-Logik) =====
public function calcQualPP($force = false): void
{
try {
$qualUserLevel = $this->calcuQualLevel();
if ($qualUserLevel !== null) {
//das erreichte level setzen
$this->b_user->qual_user_level = $qualUserLevel->toArray();
//next_qual_user_level nächster qualifizierten level
$this->setNextUserLevel($force);
//qual_user_level_next nächste Provisions-Stufe,
$this->setQualNextLevel($force);
//provisionen berechnen
$this->calculateCommissions($qualUserLevel);
} else {
$this->setFirstQualLevel();
}
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error calculating qualifications for user {$this->b_user->user_id}: " . $e->getMessage());
}
}
/**
* Berechnet Provisionen mit Error-Handling
* Erweitert um Array/Object-Kompatibilität für business_lines
*/
private function calculateCommissions($qualUserLevel): void
{
$commission_pp_total = 0;
$commission_growth_total = 0;
// Payline-Provisionen
for ($i = 1; $i <= $qualUserLevel->paylines; $i++) {
if (isset($this->b_user->business_lines[$i])) {
$object = $this->b_user->business_lines[$i];
$margin = (float) $this->b_user->qual_user_level['pr_line_' . $i];
// Handle both array and object types (JSON deserialization inconsistency)
if (is_array($object)) {
$points = (float) ($object['points'] ?? 0);
$object['margin'] = $margin;
$object['commission'] = round($points / 100 * $margin, 2);
$object['payline'] = true;
$commission_pp_total += $object['commission'];
} else {
$points = (float) ($object->points ?? 0);
$object->margin = $margin;
$object->commission = round($points / 100 * $margin, 2);
$object->payline = true;
$commission_pp_total += $object->commission;
}
$this->b_user->business_lines[$i] = $object;
}
}
// Growth Bonus
if (!empty($qualUserLevel->growth_bonus)) {
$payline = (int) $this->b_user->qual_user_level['paylines'] + 1;
$maxlines = count($this->b_user->business_lines) + 1;
$growth_bonus = (float) $this->b_user->qual_user_level['growth_bonus'];
for ($i = $payline; $i <= $maxlines; $i++) {
if (isset($this->b_user->business_lines[$i])) {
$object = $this->b_user->business_lines[$i];
// Handle both array and object types (JSON deserialization inconsistency)
if (is_array($object)) {
$points = (float) ($object['points'] ?? 0);
$object['margin'] = $growth_bonus;
$object['commission'] = round($points / 100 * $growth_bonus, 2);
$object['growth_bonus'] = true;
$commission_growth_total += $object['commission'];
} else {
$points = (float) ($object->points ?? 0);
$object->margin = $growth_bonus;
$object->commission = round($points / 100 * $growth_bonus, 2);
$object->growth_bonus = true;
$commission_growth_total += $object->commission;
}
$this->b_user->business_lines[$i] = $object;
}
}
}
$this->b_user->commission_pp_total = $commission_pp_total;
$this->b_user->commission_growth_total = $commission_growth_total;
}
// ===== WEITERE ORIGINAL-METHODEN (gekürzt, vollständige Implementation in Original) =====
//aktuelles level berechnen, max das eigene level, wenn weniger Points dann darunter
public function calcuQualLevel()
{
$qualUserLevels = UserLevel::where('qual_kp', '<=', $this->b_user->sales_volume_points_KP_sum)
->where('pos', '<=', $this->user_level_active_pos)
->orderBy('qual_pp', 'desc')
->get();
foreach ($qualUserLevels as $qualUserLevel) {
$payline_points = $this->getPointsforPayline($qualUserLevel->paylines);
$payline_points_qual_kp = $payline_points + $this->getRestQualKP();
if ($payline_points_qual_kp >= $qualUserLevel->qual_pp) {
$this->b_user->payline_points = $payline_points;
$this->b_user->payline_points_qual_kp = $payline_points_qual_kp;
return $qualUserLevel;
}
}
return null;
}
private function getPointsforPayline($paylines): float
{
$payline_points = 0;
for ($i = 1; $i <= $paylines; $i++) {
if (isset($this->b_user->business_lines[$i])) {
$line = $this->b_user->business_lines[$i];
// Handle both array and object types (JSON deserialization inconsistency)
if (is_array($line)) {
$payline_points += (float) ($line['points'] ?? 0);
} else {
$payline_points += (float) ($line->points ?? 0);
}
}
}
return $payline_points;
}
/**
* Setzt das nächste Provision-Level
* Wenn das aktuelle Level nicht erreicht ist, dann wird bei aktuelle Provisions-Stufe die erreichte level angezeigt und berechnet
* Zur Info wird das nächste level angezeigt, der folgt, sonst leer
*/
private function setQualNextLevel($force = false): void
{
//ist der level nicht das aktuelle level, dann sucht es den nächsten level
//isQualEqualLevel wenn das erreichte level das akutelle user level ist.
if (!$this->isQualEqualLevel() && $this->b_user->qual_user_level['next_id'] != null) {
$qualUserLevelNext = UserLevel::where('id', '=', $this->b_user->qual_user_level['next_id'])
->orderBy('qual_pp', 'asc')
->first();
if ($qualUserLevelNext) {
$this->b_user->qual_user_level_next = $qualUserLevelNext->toArray();
} else {
$this->b_user->qual_user_level_next = null;
}
} else {
$this->b_user->qual_user_level_next = null;
}
}
private function setNextUserLevel($force = false): void
{
//sucht den nächsten level, der mehr points hat als das aktuelle level
$nextQualUserLevel = UserLevel::where('qual_pp', '<=', $this->b_user->payline_points_qual_kp)
->where('pos', '>', $this->user_level_active_pos)
->orderBy('qual_pp', 'desc')
->first();
//wenn der nächste level qualifiziert ist und die KP-Qualifikation erfüllt ist, dann setzt es den nächsten level
if ($nextQualUserLevel && $this->isQualKP()) {
$this->b_user->next_qual_user_level = $nextQualUserLevel->toArray();
$this->b_user->next_can_user_level = null;
} else {
//wenn der nächste level nicht qualifiziert ist, dann sucht es den nächsten level, nach pos
$nextCanUserLevel = UserLevel::where('pos', '>', $this->user_level_active_pos)
->orderBy('qual_pp', 'asc')
->first();
if ($nextCanUserLevel) {
$this->b_user->next_can_user_level = $nextCanUserLevel->toArray();
}
$this->b_user->next_qual_user_level = null;
}
}
private function setFirstQualLevel(): void
{
$qualUserLevelNext = UserLevel::where('pos', '=', 1)
->orderBy('qual_pp', 'asc')
->first();
if ($qualUserLevelNext) {
$this->b_user->qual_user_level_next = $qualUserLevelNext->toArray();
}
}
// Magic Methods für Property-Zugriff (Rückwärtskompatibilität)
public function __get($name)
{
if (isset($this->b_user->$name)) {
return $this->b_user->$name;
}
// Legacy-Properties
$legacyMap = [
'sales_volume_points_KP_sum' => 'sales_volume_points_KP_sum',
'sales_volume_points_TP_sum' => 'sales_volume_points_TP_sum',
'business_lines' => 'business_lines',
'user_id' => 'user_id'
];
if (isset($legacyMap[$name]) && isset($this->b_user->{$legacyMap[$name]})) {
return $this->b_user->{$legacyMap[$name]};
}
return null;
}
/**
* Prüft und setzt Sponsor-Informationen (Original-Implementation)
*/
public function checkSponsor($user): void
{
try {
// Check if already stored
if ($this->isSave()) {
return;
}
$sponsor = new stdClass();
$sponsor->is_sponsor = false;
$sponsor->user_id = false;
$sponsor->first_name = '';
$sponsor->last_name = '';
$sponsor->email = '';
$sponsor->m_account = '';
$sponsor->full_name = 'Keinen Sponsor zugewiesen';
if ($user->m_sponsor) {
if ($user->user_sponsor) {
$sponsor->is_sponsor = true;
$sponsor->user_id = $user->user_sponsor->id;
if ($user->user_sponsor->account) {
$sponsor->full_name = substr(
'Sponsor: ' . $user->user_sponsor->account->first_name . ' ' .
$user->user_sponsor->account->last_name . ' | ' .
$user->user_sponsor->email . ' | ' .
$user->user_sponsor->account->m_account,
0,
250
);
$sponsor->first_name = $user->user_sponsor->account->first_name;
$sponsor->last_name = $user->user_sponsor->account->last_name;
$sponsor->m_account = $user->user_sponsor->account->m_account;
} else {
$sponsor->full_name = 'Sponsor: ' . $user->user_sponsor->email;
}
$sponsor->email = $user->user_sponsor->email;
} else {
$sponsor->full_name = 'Sponsor wurde gelöscht.';
}
}
$this->b_user->sponsor = $sponsor;
} catch (\Exception $e) {
Log::error("BusinessUserItem: Error checking sponsor for user {$user->id}: " . $e->getMessage());
}
}
/**
* Lädt Parent Business Users rekursiv (Original-Implementation mit Optimierungen)
* BUGFIX: Schutz vor unendlicher Rekursion durch zirkuläre Referenzen
*/
public function readParentsBusinessUsers($forceLiveCalculation = false, $depth = 0): void
{
// Schutz vor zu tiefer Rekursion (maximale Tiefe: 20 Levels)
$maxDepth = 20;
if ($depth > $maxDepth) {
Log::warning("BusinessUserItem: Maximale Rekursionstiefe ({$maxDepth}) erreicht für User {$this->b_user->user_id}");
return;
}
try {
// Optimiert: Lade mit Relations
$users = User::with(['account'])
->select('users.*')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', '<', 4)
->where('users.m_level', '!=', null)
->where('users.m_sponsor', '=', $this->b_user->user_id)
->where('users.payment_account', '!=', null)
->where('users.active_date', '<=', $this->date->end_date)
->get();
if ($users->isNotEmpty()) {
foreach ($users as $user) {
// KRITISCHER BUGFIX: Prüfe ob User bereits verarbeitet wurde
if ($this->treeCalcBot && $this->treeCalcBot->isUserProcessed($user->id)) {
Log::debug("BusinessUserItem: Überspringe bereits verarbeiteten User {$user->id} (zirkuläre Referenz verhindert)");
continue;
}
$businessUserItem = new BusinessUserItemOptimized($this->date, $this->treeCalcBot);
$businessUserItem->makeUserFromModel($user, $forceLiveCalculation);
$businessUserItem->addUserID();
$this->businessUserItems[] = $businessUserItem;
}
}
// Rekursiver Aufruf für alle Child-Items mit Tiefenprüfung
foreach ($this->businessUserItems as $businessUserItem) {
$businessUserItem->readParentsBusinessUsers($forceLiveCalculation, $depth + 1);
}
} catch (\Exception $e) {
Log::error("BusinessUserItem: Error reading parent users for {$this->b_user->user_id} at depth {$depth}: " . $e->getMessage());
}
}
/**
* Lädt Parent Business Users aus gespeicherter Struktur (Original-Implementation)
* BUGFIX: Schutz vor unendlicher Rekursion durch zirkuläre Referenzen
*/
public function readStoredParentsBusinessUsers($structure, $depth = 0): void
{
// Schutz vor zu tiefer Rekursion (maximale Tiefe: 50 Levels)
$maxDepth = 50;
if ($depth > $maxDepth) {
Log::warning("BusinessUserItem: Maximale Rekursionstiefe ({$maxDepth}) erreicht für gespeicherte User {$this->b_user->user_id}");
return;
}
try {
$parents = $this->findParentsBusinessOnStored($this->b_user->user_id, $structure);
if ($parents) {
foreach ($parents as $obj) {
// KRITISCHER BUGFIX: Prüfe ob User bereits verarbeitet wurde
if ($this->treeCalcBot && $this->treeCalcBot->isUserProcessed($obj->user_id)) {
Log::debug("BusinessUserItem: Überspringe bereits verarbeiteten gespeicherten User {$obj->user_id} (zirkuläre Referenz verhindert)");
continue;
}
$businessUserItem = new BusinessUserItemOptimized($this->date, $this->treeCalcBot);
$businessUserItem->makeUser($obj->user_id);
$businessUserItem->addUserID();
$this->businessUserItems[] = $businessUserItem;
}
foreach ($this->businessUserItems as $businessUserItem) {
$businessUserItem->readStoredParentsBusinessUsers($parents, $depth + 1);
}
}
} catch (\Exception $e) {
Log::error("BusinessUserItem: Error reading stored parent users at depth {$depth}: " . $e->getMessage());
}
}
/**
* Findet Parent Business Items in gespeicherter Struktur (Original-Implementation)
*/
private function findParentsBusinessOnStored($user_id, $structures)
{
if (!$structures) {
return null;
}
foreach ($structures as $obj) {
if ($user_id === $obj->user_id) {
return $obj->parents ?? null;
}
if (!empty($obj->parents)) {
$result = $this->findParentsBusinessOnStored($user_id, $obj->parents);
if ($result) {
return $result;
}
}
}
return null;
}
/**
* Prüft ob User bereits gespeichert ist
* Konsistent zur ursprünglichen BusinessUserItem Implementation
*/
public function isSave(): bool
{
return $this->b_user && $this->b_user->isSave();
}
/**
* Gibt die Anzahl der qualifizierten Paylines zurück
*/
public function getQualLevelPaylines()
{
if ($this->b_user && isset($this->b_user->qual_user_level) && $this->b_user->qual_user_level) {
return $this->b_user->qual_user_level['paylines'] ?? 0;
}
return 0;
}
/**
* Prüft ob eine Line für Growth-Bonus qualifiziert ist
*/
public function isQualLevelGrowth($line)
{
if ($this->b_user && isset($this->b_user->business_lines[$line])) {
$object = $this->b_user->business_lines[$line];
if (isset($object->growth_bonus)) {
return $object->growth_bonus > 0;
}
}
return false;
}
/**
* Intelligentes Laden des UserAccount für einen User
* Prüft zuerst geladene Relations, lädt nach wenn nötig
*/
private function getAccountForUser(User $user): ?UserAccount
{
try {
// Prüfe ob Account-Relation bereits geladen ist
if ($user->relationLoaded('account')) {
$account = $user->account;
if ($account instanceof UserAccount) {
\Log::debug("BusinessUserItem: Using pre-loaded account for user {$user->id}");
return $account;
}
}
// Wenn User keine account_id hat, gibt es definitiv kein Account
if (!$user->account_id) {
\Log::info("BusinessUserItem: User {$user->id} has no account_id - no account available");
return null;
}
// Account nachladen falls nötig
\Log::info("BusinessUserItem: Loading account for user {$user->id} (account_id: {$user->account_id})");
$account = UserAccount::find($user->account_id);
if (!$account) {
\Log::warning("BusinessUserItem: Account {$user->account_id} not found for user {$user->id}");
return null;
}
\Log::debug("BusinessUserItem: Successfully loaded account {$account->id} for user {$user->id}");
return $account;
} catch (\Exception $e) {
\Log::error("BusinessUserItem: Error loading account for user {$user->id}: " . $e->getMessage());
return null;
}
}
}

View file

@ -0,0 +1,215 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
use App\Models\UserBusinessStructure;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Facades\Log;
/**
* Repository für effiziente Datenbankabfragen im Business-Kontext
* Löst N+1 Probleme durch optimierte Eager Loading Strategien
*/
class BusinessUserRepository
{
private $startDate;
private $endDate;
private $month;
private $year;
public function __construct(int $month, int $year)
{
$this->month = $month;
$this->year = $year;
$date = Carbon::parse($year . '-' . $month . '-1');
$this->startDate = $date->format('Y-m-d H:i:s');
$this->endDate = $date->endOfMonth()->format('Y-m-d H:i:s');
\Log::info("BusinessUserRepository: Start Date: " . $this->startDate);
\Log::info("BusinessUserRepository: End Date: " . $this->endDate);
}
/**
* Lädt Root-User mit optimiertem Eager Loading und Caching
*/
public function getRootUsers(): Collection
{
$cacheKey = "root_users_{$this->month}_{$this->year}";
//root hat keinen parent m_sponsor, hat
return cache()->remember($cacheKey, 3600, function () {
return User::with([
'account',
'user_level',
'userBusiness' => function ($query) {
$query->where('month', $this->month)
->where('year', $this->year);
}
])
->select('users.*')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', '<', 4)
->where('users.m_level', '!=', null)
->where('users.m_sponsor', '=', null)
->where('users.payment_account', '!=', null)
->where('users.active_date', '<=', $this->endDate)
->where('users.payment_account', '>', $this->endDate)
->get();
});
}
/**
* Lädt User ohne Parent-Zuordnung (Lazy Loading für Memory-Effizienz)
*/
public function getParentlessUsers(array $excludeUserIds = []): LazyCollection
{
$query = User::with([
'account',
'user_level',
'userBusiness' => function ($query) {
$query->where('month', $this->month)
->where('year', $this->year);
}
])
->select('users.*')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', '<', 4)
->where('users.payment_account', '!=', null)
->where('users.active_date', '<=', $this->endDate);
if (!empty($excludeUserIds)) {
$query->whereNotIn('users.id', $excludeUserIds);
}
return $query->lazy(100);
}
/**
* Lädt einen einzelnen User mit Relations und Caching
*/
public function getUserWithRelations(int $userId): ?User
{
$cacheKey = "user_relations_{$userId}_{$this->month}_{$this->year}";
return cache()->remember($cacheKey, 1800, function () use ($userId) {
\Log::debug("BusinessUserRepository: Loading user {$userId} with relations (cache miss)");
return User::with([
'account',
'user_level',
'userBusiness' => function ($query) {
$query->where('month', $this->month)
->where('year', $this->year);
}
])->find($userId);
});
}
/**
* Lädt Sponsor für einen User
*/
public function getSponsorForUser(int $userId): ?User
{
$user = $this->getUserWithRelations($userId);
if (!$user || !$user->m_sponsor) {
return null;
}
return $this->getUserWithRelations($user->m_sponsor);
}
/**
* Prüft ob gespeicherte Struktur existiert (mit Caching)
*/
public function getStoredStructure(): ?UserBusinessStructure
{
$cacheKey = "stored_structure_{$this->month}_{$this->year}";
return cache()->remember($cacheKey, 7200, function () {
\Log::debug("BusinessUserRepository: Loading stored structure (cache miss)");
$structure = UserBusinessStructure::where('year', $this->year)
->where('month', $this->month)
->first();
return ($structure && $structure->completed) ? $structure : null;
});
}
/**
* Lädt User-IDs aus gespeicherter Struktur
*/
public function getUserIdsFromStoredStructure(UserBusinessStructure $structure): array
{
$userIds = [];
if ($structure->structure) {
$this->extractUserIdsFromStructure((array) $structure->structure, $userIds);
}
if ($structure->parentless) {
foreach ($structure->parentless as $item) {
$userIds[] = $item->user_id;
}
}
return array_unique($userIds);
}
/**
* Rekursive Extraktion von User-IDs aus Struktur
*/
private function extractUserIdsFromStructure(array $structure, array &$userIds): void
{
foreach ($structure as $item) {
$userIds[] = $item->user_id;
if (isset($item->parents) && is_array($item->parents)) {
$this->extractUserIdsFromStructure($item->parents, $userIds);
}
}
}
/**
* Löscht alle Cache-Einträge für den aktuellen Monat/Jahr
*/
public function clearCache(): void
{
$cacheKeys = [
"root_users_{$this->month}_{$this->year}",
"stored_structure_{$this->month}_{$this->year}"
];
foreach ($cacheKeys as $key) {
cache()->forget($key);
\Log::info("BusinessUserRepository: Cache cleared for key: {$key}");
}
}
/**
* Batch-Loading für User-Kollektionen
*/
public function loadUsersInBatches(array $userIds, int $batchSize = 100): \Generator
{
$chunks = array_chunk($userIds, $batchSize);
foreach ($chunks as $chunk) {
yield User::with([
'account',
'user_level',
'userBusiness' => function ($query) {
$query->where('month', $this->month)
->where('year', $this->year);
}
])
->whereIn('id', $chunk)
->get()
->keyBy('id');
}
}
}

View file

@ -0,0 +1,121 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
use stdClass;
use Carbon;
class ExportBot
{
public $date;
private $init_from;
private $order;
public $user_tree;
public $user_list;
private $root_user;
public function __construct($init_from = 'member')
{
$this->date = Carbon::now();
$this->init_from = $init_from;
}
public function initStructureUser(User $user, $order = 'list') //tree or list
{
$line = 0;
$this->order = $order;
$this->root_user = $user;
if ($this->order === 'tree') {
$this->user_tree = $this->setUserValues($user, $line);
$this->user_tree->childs = $this->readChildUsers($this->user_tree, $line + 1);
}
if ($this->order === 'list') {
$this->user_list = $this->setUserValues($user, $line);
$this->readChildUsers($this->user_list, $line + 1);
}
}
private function readChildUsers($parent_user, $line)
{
$childUsers = [];
$users = User::with('account')->select('users.*')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', "<", 4)
->where('users.m_level', "!=", null)
->where('users.m_sponsor', "=", $parent_user->user_id) //<- need the id for childs / sponsors
->where('users.payment_account', "!=", null)
->where('users.active', "=", 1)
->get();
if ($users) {
foreach ($users as $user) {
$user_values = $this->setUserValues($user, $line);
$childUsers[] = $user_values;
if ($this->order === 'list') {
$this->user_list->childs[] = $user_values;
$this->readChildUsers($user_values, $line + 1);
}
}
}
return $childUsers;
}
private function setUserValues(User $user, $line)
{
$sponsor_name = "";
if ($user->m_sponsor && $user->user_sponsor->account) {
$sponsor_name = $user->user_sponsor->account->m_first_name . " " . $user->user_sponsor->account->m_last_name;
}
$obj = new stdClass();
$obj->line = $line;
$obj->user_id = $user->id;
$obj->m_level = $user->m_level;
$obj->level_name = $user->user_level ? $user->user_level->getLang('name') : '';
$obj->m_sponsor = $user->m_sponsor;
$obj->sponsor_name = $sponsor_name;
$obj->m_account = $user->m_account;
$obj->email = $user->email;
$obj->active_account = $user->payment_account ? Carbon::parse($user->payment_account)->gt(Carbon::parse($this->date->format('Y-m-d H:i:s'))) : 0;
$obj->payment_account_date = $user->payment_account ? $user->getPaymentAccountDateFormat(false) : "";
$obj->first_name = $user->account->first_name;
$obj->last_name = $user->account->last_name;
$obj->address = $user->account->address;
$obj->address_2 = $user->account->address_2;
$obj->zipcode = $user->account->zipcode;
$obj->city = $user->account->city;
$obj->country_id = $user->account->country_id ? $user->account->country->getLocated() : "";
$pre_phone = $user->account->pre_phone_id != "" ? $user->account->pre_phone->phone . " " : "";
$pre_mobil = $user->account->pre_mobil_id != "" ? $user->account->pre_mobil->phone . " " : "";
$obj->phone = $pre_phone . $user->account->phone;
$obj->mobil = $pre_mobil . $user->account->mobil;
$obj->birthday = $user->account->birthday;
$obj->partner_since = $user->active_date ? $user->getActiveDateFormat(false) : "";
if ($this->order === 'tree') {
$obj->childs = $this->readChildUsers($obj, $line + 1);
}
if ($this->order === 'list') {
$obj->childs = [];
}
return $obj;
}
public function getUser()
{
return $this->root_user;
}
}

View file

@ -0,0 +1,257 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
use stdClass;
use App\Services\Util;
use App\Models\ShoppingOrder;
use App\Models\UserSalesVolume;
class SalesPointsVolume
{
public static function changeSalesPointsVolumeUser(ShoppingOrder $shoppingOrder, $to_user_id){
if($shoppingOrder->user_sales_volume){
$to_user_id = intval($to_user_id);
if($shoppingOrder->user_sales_volume->user_id === $to_user_id){
\Session()->flash('alert-error', 'Keine Änderung: selber Berater');
return;
}
if(!$shoppingOrder->user_sales_volume->isCurrentMonthYear()){
\Session()->flash('alert-error', 'Änderung muss im selben Monat sein');
return;
}
$month = $shoppingOrder->user_sales_volume->month;
$year = $shoppingOrder->user_sales_volume->year;
$form_user_id = $shoppingOrder->user_sales_volume->user_id;
$to_user = User::find($to_user_id);
$form_user = User::find($form_user_id);
$shoppingOrder->user_sales_volume->user_id = $to_user_id;
$shoppingOrder->user_sales_volume->message = 'zugewiesen: '.date('d.m.Y');
$syslog = $shoppingOrder->user_sales_volume->syslog;
$from_email = $form_user ? $form_user->email : '';
$to_email = $to_user ? $to_user->email : '';
$syslog[date('d.m.Y-h:i:s')] = 'change form: #'.$form_user_id.' '.$from_email.' to: #'.$to_user_id.' '.$to_email;
$shoppingOrder->user_sales_volume->syslog = $syslog;
$shoppingOrder->user_sales_volume->save();
//recalculate
self::reCalculateSalesPointsVolume($to_user_id, $month, $year);
self::reCalculateSalesPointsVolume($form_user_id, $month, $year);
\Session()->flash('alert-save', true);
}
}
private static function add_KP_TP_Points($userSalesVolume, $month_points){
if($userSalesVolume->status_points === 2) { //KP
$month_points->KP += $userSalesVolume->points;
}else{
// === 1 //TP + KP
$month_points->KP += $userSalesVolume->points;
$month_points->TP += $userSalesVolume->points;
}
return $month_points;
}
public static function reCalculateSalesPointsVolume($user_id, $month, $year){
$userSalesVolumes = UserSalesVolume::where('user_id', $user_id)->where('month', $month)->where('year', $year)->orderBy('id', 'ASC')->get();
$month_points = new stdClass();
$month_points->KP = 0;
$month_points->TP = 0;
$month_total_net = 0;
$month_shop_points = 0;
$month_shop_total_net = 0;
//TDOO Status === 3???
foreach($userSalesVolumes as $userSalesVolume){
switch ($userSalesVolume->status) {
case 1: //Bestellung Berater
$month_points = self::add_KP_TP_Points($userSalesVolume, $month_points);
$month_total_net += $userSalesVolume->total_net;
break;
case 2: //Shop
$month_shop_points += $userSalesVolume->points;
$month_shop_total_net += $userSalesVolume->total_net;
break;
case 4: //Gutschrift
$month_points = self::add_KP_TP_Points($userSalesVolume, $month_points);
if($userSalesVolume->status_turnover === 2){
$month_shop_total_net += $userSalesVolume->total_net;
//ggf hier zu den Shop Points zählen wäre aber immer KP + TP kann nicht keine trennung bei month_shop_points
}else{
$month_total_net += $userSalesVolume->total_net;
}
break;
case 5: //Registrierung
$month_points = self::add_KP_TP_Points($userSalesVolume, $month_points);
$month_total_net += $userSalesVolume->total_net;
break;
}
$userSalesVolume->month_shop_points = $month_shop_points;
$userSalesVolume->month_shop_total_net = $month_shop_total_net;
$userSalesVolume->month_KP_points = $month_points->KP;
$userSalesVolume->month_TP_points = $month_points->TP;
$userSalesVolume->month_total_net = $month_total_net;
$userSalesVolume->save();
}
}
public static function addSalesPointsVolumeUser(ShoppingOrder $shoppingOrder){
/*
status
1 => 'hinzugefügt aus Bestellung',
2 => 'hinzugefügt aus Shop',
3 => 'hinzugefügt aus Shop / pending',
*/
$status = self::getStatusByOrderPaymentFor($shoppingOrder);
$user_id = $shoppingOrder->auth_user_id ? $shoppingOrder->auth_user_id : $shoppingOrder->member_id;
//akuteller tag / Monat.
$month = date('m');
$year = date('Y');
$date = date('d.m.Y');
if($status === 3){ //shop bestellung User pending if is_like
$user_id = NULL;
}
$user_sales_volume = UserSalesVolume::create([
'user_id' => $user_id,
'shopping_order_id' => $shoppingOrder->id,
'month' => $month,
'year' => $year,
'date' => $date,
'points' => $shoppingOrder->points,
'total_net' => $shoppingOrder->subtotal,
'status_points' => 1, //KP + TP
'message' => '',
'status' => $status,
]);
if($status !== 3){
self::reCalculateSalesPointsVolume($user_sales_volume->user_id, $user_sales_volume->month, $user_sales_volume->year);
}
return $user_sales_volume;
}
public static function setToUserAndReCalculate(UserSalesVolume $user_sales_volume, $user_id){
//set month year date new, calculate it in the currently month!
//If the month has changed, it can no longer be added to the month before
$month = date('m');
$year = date('Y');
$date = date('d.m.Y');
$user_sales_volume->user_id = $user_id;
$user_sales_volume->month = $month;
$user_sales_volume->year = $year;
$user_sales_volume->date = $date;
$user_sales_volume->status = 2; //hinzugefügt aus Shop can only Pending
$user_sales_volume->save();
self::reCalculateSalesPointsVolume($user_id, $month, $year);
}
public static function getStatusByOrderPaymentFor(ShoppingOrder $shoppingOrder){
if($shoppingOrder->payment_for){
if($shoppingOrder->payment_for === 6){ //Kunde-Shop
if($shoppingOrder->shopping_user && $shoppingOrder->shopping_user->is_like){
return 3; //shop Kunden, berater zuordnen <- need?
}
return 2;
}
return 1;
}
return 0;
}
public static function editSalesPointsVolume($data){
$user_sales_volume = UserSalesVolume::findOrFail($data['id']);
if(!$user_sales_volume->isCurrentMonthYear()){
\Session()->flash('alert-error', 'Änderung muss im selben Monat sein');
return;
}
$old_points = $user_sales_volume->points;
$old_total_net = $user_sales_volume->total_net;
$user_sales_volume->total_net = Util::reFormatNumber($data['total_net']);
$user_sales_volume->points = intval($data['points']);
$user_sales_volume->message = 'geändert: '.date('d.m.Y');
$user_sales_volume->info = $data['info'];
$user_sales_volume->status_points = $data['status_points'];
$user_sales_volume->status_turnover = isset($data['status_turnover']) ? intval($data['status_turnover']) : null;
$syslog = $user_sales_volume->syslog;
$syslog[date('d.m.Y-h:i:s')] = 'edit points: #'.$old_points.' '.$user_sales_volume->points .' total: #'.$old_total_net.' '.$user_sales_volume->total_net;
$user_sales_volume->syslog = $syslog;
$user_sales_volume->save();
self::reCalculateSalesPointsVolume($user_sales_volume->user_id, $user_sales_volume->month, $user_sales_volume->year);
\Session()->flash('alert-success', "Points geändert");
return;
}
public static function addSalesPointsVolume($data){
if(!isset($data['user_id'])){
\Session()->flash('alert-error', 'Kein Berater ausgewählt');
return;
}
$user = User::findOrFail($data['user_id']);
$month = date('m');
$year = date('Y');
$date = date('d.m.Y');
$total_net = isset($data['total_net']) ? Util::reFormatNumber($data['total_net']) : 0;
$points = isset($data['points']) ? intval($data['points']) : 0;
$syslog[date('d.m.Y-h:i:s')] = 'add points: #'.$points.' total: #'.$total_net;
$status = isset($data['status']) ? intval($data['status']) : 4;
$status_turnover = isset($data['status_turnover']) ? intval($data['status_turnover']) : null;
$user_sales_volume = UserSalesVolume::create([
'user_id' => $user->id,
'shopping_order_id' => null,
'month' => $month,
'year' => $year,
'date' => $date,
'points' => $points,
'status_points' => $data['status_points'],
'status_turnover' => $status_turnover,
'total_net' => $total_net,
'message' => 'hinzugefügt: '.date('d.m.Y'),
'info' => $data['info'],
'syslog' => $syslog,
'status' => $status,
]);
self::reCalculateSalesPointsVolume($user_sales_volume->user_id, $user_sales_volume->month, $user_sales_volume->year);
\Session()->flash('alert-success', "Points hinzugefügt");
}
}

View file

@ -0,0 +1,178 @@
<?php
namespace App\Services\BusinessPlan;
use App\Models\UserSalesVolume;
class SalesPointsVolumeHelper
{
//protected static $business_users_table = [];
/*
$sort = 'structur' || 'line'
structur: nach baumstruktur sortiert, wird übergeben
line: nach reihenfolge sortiert,
*/
private static $business_users_line = [];
private static $totalcommission = [];
private static $totalpoints = [];
private static $cbot = null;
public static function getBusinessUsersTable($cbot, $sort = 'structur'){
self::$cbot = $cbot;
if($sort == 'structur'){
return self::getBusinessUsersTableStructur();
}
if($sort == 'line'){
return self::getBusinessUsersTableLine();
}
}
/* getBusinessUsersTableStructur */
private static function getBusinessUsersTableStructur(){
$deep = 0;
$ret = "";
foreach(self::$cbot->business_users as $business_user){
$ret .= self::addTableItemStructur($business_user, $deep);
}
return $ret;
}
private static function addTableItemStructur($item, $deep) {
$ret = self::setTableHTMLItemStructur($item, $deep);
if($item->businessUserItems){
foreach($item->businessUserItems as $parent){
$ret .= self::addTableItemStructur($parent, $deep+1);
}
}
return $ret;
}
private static function setTableHTMLItemStructur($item, $deep){
$pp = '';
$margin = 0;
$points = $item->sales_volume_points_KP_sum;
$commission = 0;
if($deep > 0){
$pp = str_repeat('&nbsp;&nbsp;&nbsp;', $deep-1).'<div class=" line-height-1 my-2 badge badge-outline-success text-dark font-weight-bolder">'.$deep.'. '.__('team.PP').'</div>';
$margin = self::$cbot->getKeybyLine($deep, 'margin');
$commission = $points / 100 * $margin;
}
$ret = '<tr>
<td><div class="no-line-break">'.$pp.'</div></td>
<td><span class="mr-1 ion ion-ios-contact '.($item->active_account ? 'text-primary' : 'text-danger').'"></span>'.$item->first_name.' '.$item->last_name.'
</td>
<td><div class="no-line-break">'.formatNumber($points, 0).'</span></td>
<td>'.formatNumber($margin, 1).' %</td>
<td><div class="no-line-break">'.formatNumber($commission, 2).' &euro;</span></td>
<td><span class="small">'.$item->user_level_name.'</span></td>
</tr>';
return $ret;
}
private static function getBusinessUsersTableLine(){
$deep = 0;
$ret = "";
foreach(self::$cbot->business_users as $business_user){
self::addTableItemLine($business_user, $deep);
}
foreach(self::$business_users_line as $deep => $items){
self::$totalcommission[$deep] = 0;
self::$totalpoints[$deep] = 0;
foreach($items as $item){
$ret .= self::setTableHTMLItemLine($item, $deep);
}
if($deep > 0){
$ret .= self::addTableHTMLTotalItemLine($deep, 'line');
}
}
$ret .= self::addTableHTMLTotalItemLine($deep, 'end');
return $ret;
}
private static function addTableItemLine($item, $deep) {
$item->deep = $deep;
self::$business_users_line[$deep][] = $item;
if($item->businessUserItems){
foreach($item->businessUserItems as $parent){
self::addTableItemLine($parent, $deep+1);
}
}
}
private static function addTableHTMLTotalItemLine($deep, $type){
$points = 0;
$commission = 0;
if($type == 'end'){
$pp = '<div class=" line-height-1 my-2 badge badge-outline-success text-dark font-weight-bolder">'.__('team.PP').'</div>';
$style = ' style="background-color:#d7d700;"';
$text = __('order.total');
foreach(self::$totalpoints as $key => $value){
$points += $value;
$commission += self::$totalcommission[$key];
}
}else{
$pp = '<div class=" line-height-1 my-2 badge badge-outline-success text-dark font-weight-bolder">'.$deep.'. '.__('team.PP').'</div>';
$style = 'style="background-color:#e5e4e4"';
$text = __('order.sum');
$points = self::$totalpoints[$deep];
$commission = self::$totalcommission[$deep];
}
$ret = '<tr '.$style.'>
<td><div class="no-line-break">'.$pp.'</div></td>
<td><b>'.$text.'</b></td>
<td><div class="no-line-break"><b>'.formatNumber($points, 0).'</b></span></td>
<td>&nbsp;</td>
<td><div class="no-line-break"><b>'.formatNumber($commission, 2).' &euro;</b></span></td>
<td>&nbsp;</td>
</tr>';
return $ret;
}
private static function setTableHTMLItemLine($item, $deep){
$pp = '';
$margin = 0;
$points = $item->sales_volume_points_KP_sum;
$commission = 0;
if($deep > 0){
$pp = '<div class=" line-height-1 my-2 badge badge-outline-success text-dark font-weight-bolder">'.$deep.'. '.__('team.PP').'</div>';
$margin = self::$cbot->getKeybyLine($deep, 'margin');
$commission = $points / 100 * $margin;
self::$totalcommission[$deep] += $commission;
self::$totalpoints[$deep] += $points;
}
//
/*
<th>{{__('tables.line')}}</th>
<th>{{ __('shop.name') }}</th>
<th>{{__('tables.points')}}</th>
<th>{{__('tables.commission')}} %</th>
<th>{{__('tables.commission')}} &euro;</th>
<th>{{ __('tables.level') }}</th>
*/
$ret = '<tr>
<td><div class="no-line-break">'.$pp.'</div></td>
<td><span class="mr-1 ion ion-ios-contact '.($item->active_account ? 'text-primary' : 'text-danger').'"></span>'.$item->first_name.' '.$item->last_name.'
</td>
<td><div class="no-line-break">'.formatNumber($points, 0).'</span></td>
<td>'.formatNumber($margin, 1).' %</td>
<td><div class="no-line-break">'.formatNumber($commission, 2).' &euro;</span></td>
<td><span class="small">'.$item->user_level_name.'</span></td>
</tr>';
return $ret;
}
}

View file

@ -0,0 +1,391 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
use stdClass;
use Carbon;
use App\Models\UserBusinessStructure;
class TreeCalcBot
{
public $date;
public $business_user;
public $business_users = [];
public $parentless = [];
private $sponsor;
private $init_from;
private static $userIDs = [];
public static function addUserID($id){
self::$userIDs[$id] = $id;
}
public function __construct($month, $year, $init_from = 'member')
{
$this->date = new stdClass();
$date = Carbon::parse($year.'-'.$month.'-1');
$this->date->month = $month;
$this->date->year = $year;
$this->date->start_date = $date->format('Y-m-d H:i:s');
$this->date->end_date = $date->endOfMonth()->format('Y-m-d H:i:s');
$this->init_from = $init_from;
}
public function initStructureAdmin($check = true, $forceLiveCalculation = false)
{
//check is month is saved.
if($check && $UserBusinessStructure = self::isFromStored($this->date->month, $this->date->year)){
$this->readStoredRootUsers($UserBusinessStructure);
$this->readStoredParentsUsers($UserBusinessStructure);
$this->readStoredParentlessUser($UserBusinessStructure);
}else{
$this->readRootUsers();
$this->readParentsUsers();
$this->readParentlessUser();
}
}
public function initStructureUser($user_id)
{
$BusinessUserItem = new BusinessUserItem($this->date);
$BusinessUserItem->makeUser($user_id);
$BusinessUserItem->addUserID();
$this->business_users[] = $BusinessUserItem;
//check is month is saved.
if($UserBusinessStructure = self::isFromStored($this->date->month, $this->date->year)){
$this->readStoredParentsUsers($UserBusinessStructure);
if(isset($this->business_users[0]) && $this->business_users[0]->sponsor){
$this->readStoredSponsorUser($this->business_users[0]->sponsor->user_id);
}
}else{
$this->readParentsUsers();
$this->readSponsorUser($user_id);
}
}
public function initBusinesslUserDetail($user)
{
$this->business_user = new BusinessUserItem($this->date);
$this->business_user->makeUser($user->id);
$this->business_user->checkSponsor($user);
if(!$this->business_user->isSave()){
//Aufbau der Struktur für den User in die unendliche Tiefe.
$this->business_user->readParentsBusinessUsers();
//calculate Points in Lines
if(count($this->business_user->businessUserItems) > 0){
$this->calcUserPoints($this->business_user->businessUserItems, 1);
}
//qualifikation nach qual_kp (KundenPoints) und qual_pp (PaylinePoints)
$this->business_user->calcQualPP();
}
}
/*public function storeBusinesslUser()
{
$this->business_user->storeUser();
}*/
public static function isFromStored($month, $year){
//when is stored an completed
$UserBusinessStructure = UserBusinessStructure::where('year', $year)->where('month', $month)->first();
if($UserBusinessStructure && $UserBusinessStructure->completed){
return $UserBusinessStructure;
}
return false;
}
private function calcUserPoints($businessUserItems, $line){
if(!isset($this->business_user->business_lines[$line])){
$obj = new stdClass();
$obj->points = 0;
$this->business_user->addBusinessLineToUser($line, $obj);
}
foreach($businessUserItems as $business_user_item){
if(count($business_user_item->businessUserItems) > 0){
$this->calcUserPoints($business_user_item->businessUserItems, $line+1);
}
//business_lines points nach line
$this->business_user->addBusinessLinePoints($line, $business_user_item->sales_volume_points_TP_sum); //TP + Shop Points
//total_pp gesamte Punkte
$this->business_user->addTotalTP($business_user_item->sales_volume_points_TP_sum); //TP + Shop Points
}
}
public function getGrowthBonus(){
if(count($this->business_user->business_lines) > 6){
$b_lines = $this->business_user->business_lines->toArray();
return array_slice($b_lines, 6);
}
return [];
}
public function getKeybyLine($line, $key){
if($this->business_user->business_lines){
$b_lines = $this->business_user->business_lines;
if(isset($b_lines[$line])){
if($b_lines[$line] instanceof stdClass){
if(isset($b_lines[$line]->{$key})){
return $b_lines[$line]->{$key};
}
}else{
if(isset($b_lines[$line][$key])){
return $b_lines[$line][$key];
}
}
}
}
return 0;
}
//* reading from current*//
private function readRootUsers(){
$users = User::with('account')->select('users.*')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', "<", 4)
->where('users.m_level', "!=", null)
->where('users.m_sponsor', "=", null)
->where('users.payment_account', "!=", null)
->where('users.active_date', "<=", $this->date->end_date)
->get();
if($users){
foreach($users as $user){
$BusinessUserItem = new BusinessUserItem($this->date);
$BusinessUserItem->makeUser($user->id);
$BusinessUserItem->addUserID();
$this->business_users[] = $BusinessUserItem;
}
}
}
private function readParentsUsers(){
foreach($this->business_users as $business_user){
$business_user->readParentsBusinessUsers();
}
}
private function readParentlessUser(){
$users = User::with('account')->select('users.*')
->where('users.deleted_at', '=', null)
->where('users.id', '!=', 1)
->where('users.admin', "<", 4)
->where('users.payment_account', "!=", null)
->where('users.active_date', "<=", $this->date->end_date)
->get();
foreach($users as $user){
if(!isset(self::$userIDs[$user->id])){
$BusinessUserItem = new BusinessUserItem($this->date);
$BusinessUserItem->makeUser($user->id);
$this->parentless[] = $BusinessUserItem;
}
}
}
//* reading from stored*//
private function readStoredRootUsers(UserBusinessStructure $userBusinessStructure){
//first level is root
if($userBusinessStructure->structure){
foreach($userBusinessStructure->structure as $obj){
$BusinessUserItem = new BusinessUserItem($this->date);
$BusinessUserItem->makeUser($obj->user_id);
$BusinessUserItem->addUserID();
$this->business_users[] = $BusinessUserItem;
}
}
}
private function readStoredParentsUsers(UserBusinessStructure $userBusinessStructure){
foreach($this->business_users as $business_user){
$business_user->readStoredParentsBusinessUsers($userBusinessStructure->structure);
}
}
private function readStoredParentlessUser(UserBusinessStructure $userBusinessStructure){
if($userBusinessStructure->parentless){
foreach($userBusinessStructure->parentless as $obj){
if(!isset(self::$userIDs[$obj->user_id])){
$BusinessUserItem = new BusinessUserItem($this->date);
$BusinessUserItem->makeUser($obj->user_id);
$this->parentless[] = $BusinessUserItem;
}
}
}
}
public function readSponsorUser($user_id){
$user = User::find($user_id);
$userSponsor = User::find($user->m_sponsor);
if($userSponsor){
$this->sponsor = new BusinessUserItem($this->date);
$this->sponsor->makeUser($userSponsor->id);
}
}
public function readStoredSponsorUser($user_id){
$this->sponsor = new BusinessUserItem($this->date);
$this->sponsor->makeUser($user_id);
}
public function getItems(){
return $this->business_users;
}
public function makeHtmlTree(){
$deep = 0;
$ret = '<ol class="dd-list">';
foreach($this->business_users as $business_user){
$ret .= $this->addItem($business_user, $deep);
}
$ret .= '</ol>';
return $ret;
}
private function addItem($item, $deep){
$button = '';
if(($this->init_from === 'admin' && \Auth::user()->isAdmin()) || ($this->init_from === 'member')){ // && \Auth::user()->id === $item->user_id
$button = ' | <button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="'.$item->user_id.'"
data-action="business-user-detail"
data-back=""
data-modal="modal-xl"
data-init_from="'.$this->init_from .'"
data-route="'.route('modal_load').'"><span class="fa fa-calculator"></span></button>';
}
return '<li class="dd-item dd-nodrag" data-id="'.$item->user_id.'">'.
'<div class="dd-handle">
<div class="media align-items-center">
<div class="d-flex flex-column justify-content-center align-items-center">
'.(($deep > 0) ? '<div class="text-large font-weight-bolder line-height-1 my-2 text-secondary badge badge-outline-secondary">'.$deep.'</div>' : '').'
</div>
<div class="media-body ml-2">
<span class="'.($item->active_account ? '' : 'text-muted').'">
<a href="#" class="text-black" data-toggle="modal" data-target="#modals-load-content" data-id="'.$item->user_id.'" data-action="business-user-show" data-back="" data-modal="modal-md" data-init_from="'.$this->init_from .'" data-route="'.route('modal_load').'">
<span class="mr-1 ion ion-ios-contact '.($item->active_account ? 'text-primary' : 'text-danger').'"></span> <strong>'.$item->first_name.' '.$item->last_name.'</strong>
</a>
<a href="mailto: '.$item->email.'">'.$item->email.'</a>
'.($item->user_birthday ? ' | <i class="ion ion-ios-gift text-primary"></i> '.$item->user_birthday : '').'
'.($item->user_phone ? ' | <i class="ion ion-ios-call text-primary"></i> '.$item->user_phone : '').'
<span class="badge badge-outline-default '.($item->active_account ? '' : 'text-muted').'">'.\App\Services\TranslationHelper::transUserLevelName($item->user_level_name).' | '.$item->m_account.'</span>
<br><span class="small">'.
($item->active_account ?
'<strong>'.__('team.total_points').': '.$item->sales_volume_points_KP_sum.'</strong> | '.__('team.e').': '.$item->sales_volume_KP_points.' | '.__('team.s').': '.$item->sales_volume_points_shop.' <strong>
| '.__('team.net_turnover').': '.formatNumber($item->sales_volume_total_sum).' &euro;</strong> | '.__('team.e').': '.formatNumber($item->sales_volume_total).' &euro; | '.__('team.s').': '.formatNumber($item->sales_volume_total_shop).' &euro;'.
$button
:
__('team.account_to').': '.$item->payment_account_date).
'</span>
</span>
</div>
</div>
</div>'.
$this->addParentItem($item, $deep).
'</li>';
}
private function addParentItem($item, $deep){
if($item->businessUserItems){
$ret = '<ol class="dd-list dd-nodrag">';
foreach($item->businessUserItems as $parent){
$ret .= $this->addItem($parent, $deep+1);
}
$ret .='</ol>';
return $ret;
}
return;
}
public function isParentless(){
return $this->parentless ? true : false;
}
public function makeParentlessHtml(){
$ret = "";
foreach($this->parentless as $item){
$ret .= '<li class="dd-item dd-nodrag" data-id="'.$item->user_id.'">'.
'<div class="dd-handle">
<span class="'.($item->active_account ? '' : 'text-muted').'">
<a href="#" class="text-black" data-toggle="modal" data-target="#modals-load-content" data-id="'.$item->user_id.'" data-action="business-user-show" data-back="" data-modal="modal-md" data-init_from="'.$this->init_from .'" data-route="'.route('modal_load').'">
<span class="mr-1 ion ion-ios-contact '.($item->active_account ? 'text-primary' : 'text-danger').'"></span> <strong>'.$item->first_name.' '.$item->last_name.'</strong>
</a>
<a href="mailto: '.$item->email.'">'.$item->email.'</a>
'.($item->user_birthday ? ' | <i class="ion ion-ios-gift text-primary"></i> '.$item->user_birthday : '').'
'.($item->user_phone ? ' | <i class="ion ion-ios-call text-primary"></i> '.$item->user_phone : '').'
<span class="badge badge-outline-default '.($item->active_account ? '' : 'text-muted').'">'.\App\Services\TranslationHelper::transUserLevelName($item->user_level_name).' | '.$item->m_account.'</span>
<br><span class="small">'.
($item->active_account ?
'<strong>'.__('team.total_points').': '.$item->sales_volume_points_KP_sum.'</strong> | '.__('team.e').': '.$item->sales_volume_KP_points.' | '.__('team.s').': '.$item->sales_volume_points_shop.' <strong>
| '.__('team.net_turnover').': '.formatNumber($item->sales_volume_total_sum).' &euro;</strong> | '.__('team.e').': '.formatNumber($item->sales_volume_total).' &euro; | '.__('team.s').': '.formatNumber($item->sales_volume_total_shop).' &euro;'.
' | <button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="'.$item->user_id.'"
data-action="business-user-detail"
data-back=""
data-modal="modal-xl"
data-route="'.route('modal_load').'"><span class="fa fa-calculator"></span></button>'
:
__('team.account_to').' '.$item->payment_account_date).
'<br>'.$item->m_sponsor_name.
'</span>
</span>
</div>'.
'</li>';
}
return $ret;
}
public function makeSponsorHtml(){
if($this->sponsor){
//' | <a href="' . route('admin_business_user_detail', [$this->sponsor->id]) . '" class="btn icon-btn btn-xs btn-secondary"><span class="fa fa-calculator"></span></a>'
$ret = '<li class="dd-item dd-nodrag" data-id="">'.
'<div class="dd-handle">
<span class="'.($this->sponsor->active_account ? '' : 'text-muted').'">
<a href="#" class="text-black" data-toggle="modal" data-target="#modals-load-content" data-id="'.$this->sponsor->user_id.'" data-action="business-user-show" data-back="" data-init_from="'.$this->init_from .'" data-modal="modal-md" data-route="'.route('modal_load').'">
<span class="mr-1 ion ion-ios-contact '.($this->sponsor->active_account ? 'text-primary' : 'text-danger').'"></span> <strong>'.$this->sponsor->first_name.' '.$this->sponsor->last_name.'</strong>
</a>
<a href="mailto: '.$this->sponsor->email.'">'.$this->sponsor->email.'</a>
'.($this->sponsor->user_birthday ? ' | <i class="ion ion-ios-gift text-primary"></i> '.$this->sponsor->user_birthday : '').'
'.($this->sponsor->user_phone ? ' | <i class="ion ion-ios-call text-primary"></i> '.$this->sponsor->user_phone : '').'
<span class="badge badge-outline-default '.($this->sponsor->active_account ? '' : 'text-muted').'">'.\App\Services\TranslationHelper::transUserLevelName($this->sponsor->user_level_name).' | '.$this->sponsor->m_account.'</span>';
if($this->init_from === 'admin'){
$ret .= '<br><span class="small">'.
($this->sponsor->active_account ?
'<strong>'.__('team.total_points').': '.$this->sponsor->sales_volume_points_KP_sum.'</strong> | '.__('team.e').': '.$this->sponsor->sales_volume_KP_points.' | '.__('team.s').': '.$this->sponsor->sales_volume_points_shop.' <strong>
| '.__('team.net_turnover').': '.formatNumber($this->sponsor->sales_volume_total_sum).' &euro;</strong> | '.__('team.e').': '.formatNumber($this->sponsor->sales_volume_total).' &euro; | '.__('team.s').': '.formatNumber($this->sponsor->sales_volume_total_shop).' &euro;'
:
__('team.account_to').' '.$this->sponsor->payment_account_date).
'</span>';
}
$ret .= '</span>
</div>
</li>';
return $ret;
}
return __('team.no_sponsor_assigned');
}
}

View file

@ -0,0 +1,912 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
use stdClass;
use Carbon\Carbon;
use App\Models\UserBusinessStructure;
use Psr\Log\LoggerInterface;
/**
* Optimierte Version der TreeCalcBot Klasse
*
* Verbesserungen:
* - Trennung von Datenzugriff (Repository Pattern)
* - Trennung von HTML-Rendering (Renderer Pattern)
* - Optimierte Datenbankabfragen (N+1 Problem gelöst)
* - Memory-effiziente Verarbeitung großer Datenmengen
* - Robuste Fehlerbehandlung mit Logging
* - Dependency Injection für bessere Testbarkeit
*/
class TreeCalcBotOptimized
{
private stdClass $date;
private string $initFrom;
private array $businessUsers = [];
private array $parentless = [];
private ?BusinessUserItemOptimized $businessUser = null;
private ?BusinessUserItemOptimized $sponsor = null;
private array $processedUserIds = [];
private bool $forceLiveCalculation = false;
private BusinessUserRepository $repository;
private TreeHtmlRenderer $renderer;
private LoggerInterface $logger;
public function __construct(
int $month,
int $year,
string $initFrom = 'member',
bool $forceLiveCalculation = false,
?BusinessUserRepository $repository = null,
?TreeHtmlRenderer $renderer = null,
?LoggerInterface $logger = null
) {
$this->validateInput($month, $year);
$this->initializeDate($month, $year);
$this->initFrom = $initFrom;
$this->forceLiveCalculation = $forceLiveCalculation;
// Dependency Injection mit Fallback
$this->repository = $repository ?? new BusinessUserRepository($month, $year);
$this->renderer = $renderer ?? new TreeHtmlRenderer($initFrom, $forceLiveCalculation);
$this->logger = $logger ?? app(LoggerInterface::class);
}
/**
* Initialisiert die Business-Struktur für Admin-Ansicht
*
* @param bool $check Prüft auf gespeicherte Struktur
* @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten
*/
public function initStructureAdmin(bool $check = true, bool $forceLiveCalculation = false): void
{
try {
$this->forceLiveCalculation = $forceLiveCalculation;
if ($forceLiveCalculation) {
$this->logger->info("Building fresh business structure for {$this->date->month}/{$this->date->year} with forced live calculation");
$this->buildFreshStructure();
return;
}
$storedStructure = null;
if ($check) {
$storedStructure = $this->repository->getStoredStructure();
}
if ($storedStructure) {
$this->logger->info("Loading stored business structure for {$this->date->month}/{$this->date->year}");
$this->loadStoredStructure($storedStructure);
} else {
$this->logger->info("Building fresh business structure for {$this->date->month}/{$this->date->year}");
$this->buildFreshStructure();
}
} catch (\Exception $e) {
$this->logger->error("Error initializing admin structure: " . $e->getMessage());
throw $e;
}
}
/**
* Initialisiert die Struktur für einen spezifischen User
*
* @param int $userId Die User-ID
* @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten
*/
public function initStructureUser(int $userId, bool $forceLiveCalculation = false): void
{
try {
$this->forceLiveCalculation = $forceLiveCalculation;
if ($forceLiveCalculation) {
$this->logger->info("Initializing structure for user: {$userId} with forced live calculation");
} else {
$this->logger->info("Initializing structure for user: {$userId}");
}
$user = $this->repository->getUserWithRelations($userId);
if (!$user) {
$this->logger->warning("User not found: {$userId}");
return;
}
$businessUserItem = new BusinessUserItemOptimized($this->date, $this);
$businessUserItem->makeUserFromModel($user); // Erst User-Model laden, ohne forceLiveCalculation
$this->addUserIdToProcessed($userId);
$this->businessUsers[] = $businessUserItem;
$this->logger->info("Created businessUserItem for user {$userId}, total businessUsers: " . count($this->businessUsers));
// Prüfe gespeicherte Struktur nur, wenn Live-Berechnung nicht erzwungen wird
$storedStructure = null;
if (!$forceLiveCalculation) {
$storedStructure = $this->repository->getStoredStructure();
$this->logger->info("Stored structure " . ($storedStructure ? "found" : "not found"));
}
if ($storedStructure && !$forceLiveCalculation) {
$this->loadStoredParentsUsers($storedStructure);
if (isset($this->businessUsers[0]) && $this->businessUsers[0]->sponsor) {
$this->loadStoredSponsorUser($this->businessUsers[0]->sponsor->user_id);
}
} else {
if ($forceLiveCalculation) {
$this->logger->info("Forcing live calculation - skipping stored structure for user {$userId}");
}
$this->loadParentsUsers();
$this->loadSponsorUser($userId);
$totalSubUsers = 0;
foreach ($this->businessUsers as $businessUser) {
$totalSubUsers += count($businessUser->businessUserItems);
}
$this->logger->info("After loadParentsUsers: {$totalSubUsers} total sub-users loaded across " . count($this->businessUsers) . " business users");
// WICHTIG: calcQualPP() erst NACH loadParentsUsers() aufrufen, da Points benötigt werden
if ($forceLiveCalculation) {
$this->logger->info("Calculating qualification levels for all business users");
foreach ($this->businessUsers as $businessUser) {
$businessUser->calcQualPP();
}
//wird nicht benötigt, da hier nur die Points berechnet werden
//$this->calculateQualPPForAllUsers(); // Auch für alle Sub-User
}
}
} catch (\Exception $e) {
$this->logger->error("Error initializing user structure for {$userId}: " . $e->getMessage());
throw $e;
}
}
/**
* Initialisiert detaillierte Business-User-Informationen
*
* @param User $user Das User-Model
* @param bool $forceLiveCalculation Erzwingt Live-Berechnung und überspringt gespeicherte Daten
*/
public function initBusinesslUserDetail(User $user, bool $forceLiveCalculation = false): void
{
try {
$this->logger->info("Initializing business user details for: {$user->id}");
$this->businessUser = new BusinessUserItemOptimized($this->date, $this);
$this->businessUser->makeUserFromModel($user, $forceLiveCalculation); // ✅ Nutzt bereits User-Objekt
$this->businessUser->checkSponsor($user);
// Führe vollständige Berechnung durch, wenn:
// 1. Daten nicht gespeichert sind ODER
// 2. Live-Berechnung erzwungen wird
if (!$this->businessUser->isSave() || $forceLiveCalculation) {
if ($forceLiveCalculation) {
$this->logger->info("Forcing live calculation for user {$user->id}");
}
// Aufbau der Struktur für den User in die unendliche Tiefe
$this->businessUser->readParentsBusinessUsers($forceLiveCalculation);
// Calculate Points in Lines (optimiert für Memory-Effizienz)
if (count($this->businessUser->businessUserItems) > 0) {
$this->calculateUserPointsOptimized($this->businessUser->businessUserItems, 1, $this->businessUser);
}
// Qualifikation nach qual_kp (KundenPoints) und qual_pp (PaylinePoints)
$this->businessUser->calcQualPP();
}
} catch (\Exception $e) {
$this->logger->error("Error initializing business user details for {$user->id}: " . $e->getMessage());
throw $e;
}
}
/**
* Gibt Growth Bonus zurück (ab Linie 6)
* Erweitert um Array/Object-Kompatibilität für business_lines
*/
public function getGrowthBonus(): array
{
if (!$this->businessUser || !$this->businessUser->business_lines) {
return [];
}
if (count($this->businessUser->business_lines) > 6) {
// Handle both array and object types (JSON deserialization inconsistency)
if (is_array($this->businessUser->business_lines)) {
$bLines = $this->businessUser->business_lines;
} else {
$bLines = $this->businessUser->business_lines->toArray();
}
return array_slice($bLines, 6);
}
return [];
}
/**
* Gibt Wert für spezifische Linie zurück
*/
public function getKeybyLine(int $line, string $key)
{
if (!$this->businessUser || !$this->businessUser->business_lines) {
return 0;
}
$bLines = $this->businessUser->business_lines;
if (!isset($bLines[$line])) {
return 0;
}
$lineData = $bLines[$line];
if ($lineData instanceof stdClass) {
return $lineData->{$key} ?? 0;
}
if (is_array($lineData)) {
return $lineData[$key] ?? 0;
}
return 0;
}
/**
* HTML-Rendering Methoden (Delegation an Renderer)
*/
public function makeHtmlTree(): string
{
return $this->renderer->renderTree($this->businessUsers);
}
public function makeParentlessHtml(): string
{
return $this->renderer->renderParentless($this->parentless);
}
public function makeSponsorHtml(): string
{
return $this->renderer->renderSponsor($this->sponsor);
}
/**
* Getter-Methoden (Rückwärtskompatibilität)
*/
public function getItems(): array
{
return $this->businessUsers;
}
/**
* Zählt die Gesamtanzahl aller User in der Struktur (rekursiv)
*/
public function getTotalUserCount(): int
{
$totalCount = 0;
// Zähle alle Root-User
$totalCount += count($this->businessUsers);
// Zähle alle Unter-User rekursiv
foreach ($this->businessUsers as $businessUser) {
$totalCount += $this->countBusinessUserItems($businessUser);
}
// Zähle parentless User
$totalCount += count($this->parentless);
return $totalCount;
}
/**
* Zählt BusinessUserItems rekursiv
*/
private function countBusinessUserItems($businessUserItem): int
{
$count = 0;
if (isset($businessUserItem->businessUserItems) && is_array($businessUserItem->businessUserItems)) {
$count += count($businessUserItem->businessUserItems);
// Rekursiv durch alle Unter-Items zählen
foreach ($businessUserItem->businessUserItems as $subItem) {
$count += $this->countBusinessUserItems($subItem);
}
}
return $count;
}
public function isParentless(): bool
{
return !empty($this->parentless);
}
/**
* Static Methoden (Rückwärtskompatibilität)
*/
public static function isFromStored(int $month, int $year): ?UserBusinessStructure
{
$structure = UserBusinessStructure::where('year', $year)
->where('month', $month)
->first();
return ($structure && $structure->completed) ? $structure : null;
}
/**
* Öffentliche Methode zum Hinzufügen einer User ID zu den verarbeiteten IDs
*/
public function addProcessedUserId(int $id): void
{
$this->addUserIdToProcessed($id);
}
public static function addUserID(int $id): void
{
// Deprecated: Statische Methode kann nicht auf Instanz-Variable zugreifen
// Verwende stattdessen die Instanz-Methode addProcessedUserId()
throw new \BadMethodCallException('addUserID ist deprecated. Verwende Instanz-Methode addProcessedUserId() stattdessen.');
}
// ===== Private Methoden =====
/**
* Validiert Eingabeparameter
*/
private function validateInput(int $month, int $year): void
{
if ($month < 1 || $month > 12) {
throw new \InvalidArgumentException("Invalid month: {$month}");
}
$currentYear = (int) date('Y');
if ($year < 2020 || $year > $currentYear + 1) {
throw new \InvalidArgumentException("Invalid year: {$year}");
}
}
/**
* Initialisiert Datums-Objekt
*/
private function initializeDate(int $month, int $year): void
{
$this->date = new stdClass();
$date = Carbon::parse($year . '-' . $month . '-1');
$this->date->month = $month;
$this->date->year = $year;
$this->date->start_date = $date->format('Y-m-d H:i:s');
$this->date->end_date = $date->endOfMonth()->format('Y-m-d H:i:s');
}
/**
* Lädt gespeicherte Struktur
*/
private function loadStoredStructure(UserBusinessStructure $structure): void
{
$this->loadStoredRootUsers($structure);
$this->loadStoredParentsUsers($structure);
$this->loadStoredParentlessUsers($structure);
// Prüfe ob gespeicherte Daten vollständig sind, ansonsten berechne neu
$this->validateAndRecalculateIfNeeded();
$this->validateAndRecalculateParentlessIfNeeded();
}
/**
* Baut frische Struktur auf
*/
private function buildFreshStructure(): void
{
$this->loadRootUsers();
$this->loadParentsUsers();
$this->loadParentlessUsers();
// WICHTIG: Berechne Punkte und Qualifikationen für alle Business-Users
$this->calculateAllBusinessUsers();
$this->calculateAllParentlessUsers();
}
/**
* Lädt Root-Users (optimiert mit Memory-Monitoring)
*/
private function loadRootUsers(): void
{
$startMemory = memory_get_usage();
$users = $this->repository->getRootUsers();
foreach ($users as $user) {
// Memory-Check vor jeder User-Verarbeitung
$this->checkMemoryUsage('loadRootUsers', $user->id);
$businessUserItem = new BusinessUserItemOptimized($this->date, $this);
$businessUserItem->makeUserFromModel($user, $this->forceLiveCalculation); // ✅ Nutzt bereits geladene Relations mit forceLiveCalculation
$this->addUserIdToProcessed($user->id);
$this->businessUsers[] = $businessUserItem;
}
$endMemory = memory_get_usage();
$memoryUsed = $this->formatBytes($endMemory - $startMemory);
$this->logger->info("Loaded " . count($users) . " root users with optimized relations. Memory used: {$memoryUsed}");
}
/**
* Lädt Parent-Users für alle Business-Users
*/
private function loadParentsUsers(): void
{
$this->logger->info("Loading parent users for " . count($this->businessUsers) . " business users");
foreach ($this->businessUsers as $businessUser) {
$businessUser->readParentsBusinessUsers($this->forceLiveCalculation);
$this->logger->debug("Loaded " . count($businessUser->businessUserItems) . " parent users for user " . ($businessUser->b_user->user_id ?? 'unknown'));
}
}
/**
* Lädt parentlose Users (Memory-optimiert)
*/
private function loadParentlessUsers(): void
{
$count = 0;
$excludeIds = array_keys($this->processedUserIds);
foreach ($this->repository->getParentlessUsers($excludeIds) as $user) {
$businessUserItem = new BusinessUserItemOptimized($this->date, $this);
$businessUserItem->makeUserFromModel($user, $this->forceLiveCalculation); // ✅ Nutzt bereits geladene Relations mit forceLiveCalculation
$this->parentless[] = $businessUserItem;
$count++;
}
$this->logger->info("Loaded {$count} parentless users with optimized relations");
}
/**
* Berechnet Punkte und Qualifikationen für alle Business-Users
*/
private function calculateAllBusinessUsers(): void
{
$startTime = microtime(true);
$this->logger->info("Starting calculation for " . count($this->businessUsers) . " business users");
foreach ($this->businessUsers as $businessUser) {
try {
// Berechne Punkte in Linien (wie bei initBusinesslUserDetail)
if (count($businessUser->businessUserItems) > 0) {
$this->calculateUserPointsOptimized($businessUser->businessUserItems, 1, $businessUser);
}
// Qualifikation nach qual_kp und qual_pp berechnen
$businessUser->calcQualPP();
} catch (\Exception $e) {
$this->logger->error("Error calculating business user {$businessUser->__get('user_id')}: " . $e->getMessage());
// Weiter mit dem nächsten User, nicht abbrechen
continue;
}
}
$endTime = microtime(true);
$executionTime = round(($endTime - $startTime) * 1000, 2);
$this->logger->info("Completed calculations for all business users in {$executionTime}ms");
}
/**
* Berechnet Punkte und Qualifikationen für alle Parentless-Users
*/
private function calculateAllParentlessUsers(): void
{
if (empty($this->parentless)) {
return;
}
$startTime = microtime(true);
$this->logger->info("Starting calculation for " . count($this->parentless) . " parentless users");
foreach ($this->parentless as $parentlessUser) {
try {
// Berechne Punkte in Linien
if (count($parentlessUser->businessUserItems) > 0) {
$this->calculateUserPointsOptimized($parentlessUser->businessUserItems, 1, $parentlessUser);
}
// Qualifikation berechnen
$parentlessUser->calcQualPP();
} catch (\Exception $e) {
$this->logger->error("Error calculating parentless user {$parentlessUser->__get('user_id')}: " . $e->getMessage());
continue;
}
}
$endTime = microtime(true);
$executionTime = round(($endTime - $startTime) * 1000, 2);
$this->logger->info("Completed calculations for all parentless users in {$executionTime}ms");
}
/**
* Validiert gespeicherte Daten und berechnet bei Bedarf neu
*/
private function validateAndRecalculateIfNeeded(): void
{
$incompleteUsers = 0;
foreach ($this->businessUsers as $businessUser) {
// Prüfe ob grundlegende Berechnungen vorhanden sind
if ($this->isBusinessUserIncomplete($businessUser)) {
$incompleteUsers++;
try {
// Führe fehlende Berechnungen durch
if (count($businessUser->businessUserItems) > 0) {
$this->calculateUserPointsOptimized($businessUser->businessUserItems, 1, $businessUser);
}
$businessUser->calcQualPP();
} catch (\Exception $e) {
$this->logger->error("Error recalculating business user {$businessUser->__get('user_id')}: " . $e->getMessage());
}
}
}
if ($incompleteUsers > 0) {
$this->logger->info("Recalculated {$incompleteUsers} incomplete business users from stored data");
}
}
/**
* Prüft ob ein BusinessUser unvollständige Daten hat
* Erweitert um Level-Qualifikationsdaten für Struktur-Ansicht
*/
private function isBusinessUserIncomplete($businessUser): bool
{
// Prüfe grundlegende Felder die nach Berechnungen vorhanden sein sollten
$salesVolumeSum = $businessUser->__get('sales_volume_points_sum');
$qualKp = $businessUser->__get('qual_kp');
// Prüfe Level-Qualifikationsdaten für Struktur-Ansicht
$nextQualUserLevel = $businessUser->__get('next_qual_user_level');
$nextCanUserLevel = $businessUser->__get('next_can_user_level');
$hasLevelQualificationData = !empty($nextQualUserLevel) || !empty($nextCanUserLevel);
// User ist unvollständig wenn:
// 1. Grundlegende berechnete Werte fehlen ODER
// 2. Level-Qualifikationsdaten fehlen (wichtig für Struktur-Ansicht mit grünen Pfeilen)
$missingBasicData = ($salesVolumeSum === null || $salesVolumeSum === 0) &&
($qualKp === null || $qualKp === 0);
$missingLevelData = !$hasLevelQualificationData;
return $missingBasicData || $missingLevelData;
}
/**
* Validiert und berechnet parentless Users bei Bedarf neu
*/
private function validateAndRecalculateParentlessIfNeeded(): void
{
if (empty($this->parentless)) {
return;
}
$incompleteUsers = 0;
foreach ($this->parentless as $parentlessUser) {
if ($this->isBusinessUserIncomplete($parentlessUser)) {
$incompleteUsers++;
try {
if (count($parentlessUser->businessUserItems) > 0) {
$this->calculateUserPointsOptimized($parentlessUser->businessUserItems, 1, $parentlessUser);
}
$parentlessUser->calcQualPP();
} catch (\Exception $e) {
$this->logger->error("Error recalculating parentless user {$parentlessUser->__get('user_id')}: " . $e->getMessage());
}
}
}
if ($incompleteUsers > 0) {
$this->logger->info("Recalculated {$incompleteUsers} incomplete parentless users from stored data");
}
}
/**
* Lädt Sponsor für User
*/
private function loadSponsorUser(int $userId): void
{
try {
$sponsorUser = $this->repository->getSponsorForUser($userId);
if ($sponsorUser) {
$this->sponsor = new BusinessUserItemOptimized($this->date, $this);
$this->sponsor->makeUser($sponsorUser->id);
$this->logger->info("Loaded sponsor {$sponsorUser->id} for user {$userId}");
}
} catch (\Exception $e) {
$this->logger->warning("Could not load sponsor for user {$userId}: " . $e->getMessage());
}
}
/**
* Gespeicherte Root-Users laden
*/
private function loadStoredRootUsers(UserBusinessStructure $structure): void
{
if (!$structure->structure) {
return;
}
foreach ($structure->structure as $obj) {
$businessUserItem = new BusinessUserItemOptimized($this->date, $this);
$businessUserItem->makeUser($obj->user_id);
$this->addUserIdToProcessed($obj->user_id);
$this->businessUsers[] = $businessUserItem;
}
}
/**
* Gespeicherte Parent-Users laden
*/
private function loadStoredParentsUsers(UserBusinessStructure $structure): void
{
foreach ($this->businessUsers as $businessUser) {
$businessUser->readStoredParentsBusinessUsers($structure->structure);
}
}
/**
* Gespeicherte parentlose Users laden
*/
private function loadStoredParentlessUsers(UserBusinessStructure $structure): void
{
if (!$structure->parentless) {
return;
}
foreach ($structure->parentless as $obj) {
if (!isset($this->processedUserIds[$obj->user_id])) {
$businessUserItem = new BusinessUserItemOptimized($this->date, $this);
$businessUserItem->makeUser($obj->user_id);
$this->parentless[] = $businessUserItem;
}
}
}
/**
* Gespeicherten Sponsor laden
*/
private function loadStoredSponsorUser(int $userId): void
{
$this->sponsor = new BusinessUserItemOptimized($this->date, $this);
$this->sponsor->makeUser($userId);
}
/**
* Optimierte Punkte-Berechnung (Stack-basiert mit korrekter Depth-First Reihenfolge)
*
* KRITISCH: Stack muss gleiche Reihenfolge wie Original-Rekursion produzieren
* Original: Depth-First Traversierung (erst tief, dann Punkte addieren)
* Stack: Muss umgekehrt arbeiten - erst alle Kinder sammeln, dann von tief zu flach verarbeiten
*/
private function calculateUserPointsOptimized(array $businessUserItems, int $startLine, BusinessUserItemOptimized $businessUserToUpdate): void
{
$processingStack = [];
$collectionStack = []; // Sammelt Items in korrekter Reihenfolge
// Phase 1: Sammle alle Items in Depth-First Reihenfolge
foreach ($businessUserItems as $item) {
$collectionStack[] = ['item' => $item, 'line' => $startLine, 'depth' => 0];
}
// Expandiere alle Kinder (Depth-First)
$processedItems = [];
while (!empty($collectionStack)) {
$current = array_shift($collectionStack); // FIFO für Breadth-First Sammlung
$item = $current['item'];
$line = $current['line'];
$depth = $current['depth'];
// Markiere für Verarbeitung (mit Tiefe für spätere Sortierung)
$processingStack[] = [
'item' => $item,
'line' => $line,
'depth' => $depth,
'id' => $item->user_id ?? uniqid()
];
// Füge Kinder hinzu (werden später verarbeitet = Depth-First)
if (isset($item->businessUserItems) && count($item->businessUserItems) > 0) {
// Kinder in umgekehrter Reihenfolge hinzufügen für korrekte Stack-Verarbeitung
$children = array_reverse($item->businessUserItems);
foreach ($children as $childItem) {
array_unshift($collectionStack, [
'item' => $childItem,
'line' => $line + 1,
'depth' => $depth + 1
]);
}
}
}
// Phase 2: Sortiere nach Tiefe (tiefste zuerst, wie bei Rekursion)
usort($processingStack, function ($a, $b) {
return $b['depth'] <=> $a['depth']; // Tiefste zuerst
});
// Phase 3: Verarbeite in korrekter Reihenfolge (von tief zu flach)
foreach ($processingStack as $current) {
$item = $current['item'];
$line = $current['line'];
try {
// Business Line initialisieren falls nötig
if (!isset($businessUserToUpdate->business_lines[$line])) {
$obj = new stdClass();
$obj->points = 0;
$businessUserToUpdate->addBusinessLineToUser($line, $obj);
}
// Punkte hinzufügen (mit Validierung)
$points = (float) ($item->sales_volume_points_TP_sum ?? 0);
if ($points > 0) {
$businessUserToUpdate->addBusinessLinePoints($line, $points);
$businessUserToUpdate->addTotalTP($points);
}
$this->logger->debug("Processed user {$current['id']} at line {$line} with {$points} points");
} catch (\Exception $e) {
$this->logger->error("Error processing user points for {$current['id']}: " . $e->getMessage());
}
}
$this->logger->info("Processed " . count($processingStack) . " business user items in depth-first order");
}
/**
* User-ID zu verarbeiteten IDs hinzufügen
*/
private function addUserIdToProcessed(int $id): void
{
$this->processedUserIds[$id] = true;
}
/**
* Prüft ob User bereits verarbeitet wurde (Public für BusinessUserItemOptimized)
*/
public function isUserProcessed(int $id): bool
{
return isset($this->processedUserIds[$id]);
}
/**
* Memory-Monitoring Methoden
*/
private function checkMemoryUsage(string $operation, $identifier = null): void
{
$currentMemory = memory_get_usage();
$memoryLimit = $this->parseMemoryLimit(ini_get('memory_limit'));
$memoryPercent = ($currentMemory / $memoryLimit) * 100;
if ($memoryPercent > 80) {
$currentFormatted = $this->formatBytes($currentMemory);
$limitFormatted = $this->formatBytes($memoryLimit);
$this->logger->warning("High memory usage detected in {$operation}", [
'identifier' => $identifier,
'current_memory' => $currentFormatted,
'memory_limit' => $limitFormatted,
'usage_percent' => round($memoryPercent, 2)
]);
// Garbage Collection bei hohem Memory-Verbrauch
if ($memoryPercent > 90) {
$this->logger->warning("Critical memory usage - forcing garbage collection");
gc_collect_cycles();
}
}
}
private function parseMemoryLimit(string $limit): int
{
$limit = trim($limit);
$last = strtolower($limit[strlen($limit) - 1]);
$number = (int) $limit;
switch ($last) {
case 'g':
$number *= 1024;
case 'm':
$number *= 1024;
case 'k':
$number *= 1024;
}
return $number;
}
private function formatBytes(int $bytes, int $precision = 2): string
{
$units = array('B', 'KB', 'MB', 'GB', 'TB');
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision) . ' ' . $units[$i];
}
/**
* Public Properties für Rückwärtskompatibilität
*/
public function __get(string $name)
{
switch ($name) {
case 'date':
return $this->date;
case 'business_user':
return $this->businessUser;
case 'business_users':
return $this->businessUsers;
case 'parentless':
return $this->parentless;
default:
throw new \InvalidArgumentException("Property {$name} does not exist");
}
}
/**
* Berechnet calcQualPP() für alle BusinessUsers rekursiv
* Muss NACH loadParentsUsers() aufgerufen werden, da Points benötigt werden
*/
private function calculateQualPPForAllUsers(): void
{
$this->logger->info("Starting recursive calcQualPP for all users");
$totalCalculated = 0;
foreach ($this->businessUsers as $businessUser) {
$totalCalculated += $this->calculateQualPPRecursive($businessUser);
}
$this->logger->info("Completed calcQualPP for {$totalCalculated} users");
}
/**
* Rekursive Hilfsmethode für calcQualPP
*/
private function calculateQualPPRecursive($businessUser): int
{
$calculated = 0;
if (isset($businessUser->businessUserItems) && is_array($businessUser->businessUserItems)) {
foreach ($businessUser->businessUserItems as $subBusinessUser) {
if ($subBusinessUser->b_user && $subBusinessUser->b_user->user_id) {
try {
$subBusinessUser->calcQualPP();
$calculated++;
$this->logger->debug("Calculated calcQualPP for user " . $subBusinessUser->b_user->user_id);
} catch (\Exception $e) {
$this->logger->warning("Error calculating calcQualPP for user " . $subBusinessUser->b_user->user_id . ": " . $e->getMessage());
}
// Rekursiver Aufruf
$calculated += $this->calculateQualPPRecursive($subBusinessUser);
}
}
}
return $calculated;
}
public function __set(string $name, $value)
{
switch ($name) {
case 'business_users':
$this->businessUsers = $value;
break;
case 'parentless':
$this->parentless = $value;
break;
default:
throw new \InvalidArgumentException("Property {$name} cannot be set");
}
}
}

View file

@ -0,0 +1,185 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
use App\Models\UserBusiness;
/**
* Klasse für die HTML-Darstellung von Business-Trees
* Trennt Präsentationslogik von Geschäftslogik
*/
class TreeHelperOptimized
{
/**
* Generiert QualKP Badge für UserBusiness
*/
public static function generateQualKPBadge(UserBusiness $userBusiness): string
{
if (!$userBusiness->m_level_id) {
return '-';
}
$qualKP = (int) $userBusiness->qual_kp;
$pointsSum = (int) $userBusiness->sales_volume_points_KP_sum;
$isQual = $pointsSum >= $qualKP;
$badgeClass = $isQual ? 'badge-outline-success' : 'badge-outline-info';
return '<span class="badge ' . $badgeClass . '"> KU ' . $qualKP . "/" . $pointsSum . '</span>';
}
/**
* Generiert QualKP Badge für User
*/
public static function generateQualKPBadgeForUser(User $user, int $month, int $year): string
{
if (!$user->user_level) {
return '-';
}
$qualKP = (int) $user->user_level->qual_kp;
$pointsSum = (int) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_KP_sum');
$isQual = $pointsSum >= $qualKP;
$badgeClass = $isQual ? 'badge-outline-success' : 'badge-outline-warning-dark';
return '<span class="badge ' . $badgeClass . '"> KU ' . $qualKP . '</span>';
}
/**
* Generiert Sales Volume Display für UserBusiness
*/
public static function generateSalesVolumeDisplay(UserBusiness $userBusiness, string $type): string
{
if ($type === 'points') {
$total = (int) $userBusiness->sales_volume_points_KP_sum;
$individual = (int) $userBusiness->sales_volume_KP_points;
$shop = (int) $userBusiness->sales_volume_points_shop;
} else {
$total = (float) $userBusiness->sales_volume_total_sum;
$individual = (float) $userBusiness->sales_volume_total;
$shop = (float) $userBusiness->sales_volume_total_shop;
$suffix = ' &euro;';
}
$totalFormatted = $type === 'points' ? $total : formatNumber($total);
$individualFormatted = $type === 'points' ? $individual : formatNumber($individual);
$shopFormatted = $type === 'points' ? $shop : formatNumber($shop);
$suffix = $type === 'points' ? '' : ' &euro;';
return '<div class="no-line-break">' . $totalFormatted . $suffix . '</div>' .
'<span class="small no-line-break">E: ' . $individualFormatted . ' | S: ' . $shopFormatted . $suffix . '</span>';
}
/**
* Generiert Sales Volume Display für User
*/
public static function generateSalesVolumeDisplayForUser(User $user, string $type, int $month, int $year): string
{
if ($type === 'points') {
$total = (int) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_KP_sum');
$individual = (int) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_KP_points');
$shop = (int) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_points_shop');
} else {
$total = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_total_sum');
$individual = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_total');
$shop = (float) $user->getUserSalesVolumeBy($month, $year, 'sales_volume_total_shop');
}
$totalFormatted = $type === 'points' ? $total : formatNumber($total);
$individualFormatted = $type === 'points' ? $individual : formatNumber($individual);
$shopFormatted = $type === 'points' ? $shop : formatNumber($shop);
$suffix = $type === 'points' ? '' : ' &euro;';
return '<div class="no-line-break">' . $totalFormatted . $suffix . '</div>' .
'<span class="small no-line-break">E: ' . $individualFormatted . ' | S: ' . $shopFormatted . $suffix . '</span>';
}
/**
* Generiert Action Buttons (mit XSS-Schutz)
*/
public static function generateActionButtons($userId): string
{
$userId = (int) $userId; // Sicherheit: Nur Integer
$html = '<button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="' . $userId . '"
data-action="business-user-detail"
data-back=""
data-modal="modal-xl"
data-init_from="admin"
data-optimized="true"
data-route="' . route('modal_load') . '"><span class="fa fa-calculator"></span></button>';
if (config('app.debug') === true) {
$html .= '<a href="' . route('admin_business_optimized_user_detail', [$userId]) . '" class="btn icon-btn btn-xs btn-primary"><span class="fa fa-calculator"></span></a>';
}
return $html;
}
/**
* Generiert Sponsor Display für UserBusiness
*/
public static function generateSponsorDisplay(UserBusiness $userBusiness): string
{
if (!$userBusiness->sponsor || !$userBusiness->sponsor->is_sponsor) {
return '-';
}
$sponsor = $userBusiness->sponsor;
$html = e($sponsor->first_name . ' ' . $sponsor->last_name);
$html .= ' &nbsp;<button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="' . (int) $sponsor->user_id . '"
data-action="business-user-detail"
data-back=""
data-modal="modal-xl"
data-init_from="admin"
data-optimized="true"
data-route="' . route('modal_load') . '"><span class="fa fa-calculator"></span></button><br>';
$html .= '<span class="small no-line-break">' . e($sponsor->email);
$html .= ' | ' . e($sponsor->m_account);
$html .= '</span>';
return $html;
}
/**
* Generiert Sponsor Display für User
*/
public static function generateSponsorDisplayForUser(User $user): string
{
if (!$user->user_sponsor) {
return '-';
}
$sponsor = $user->user_sponsor;
$html = '';
if ($sponsor->account) {
$html .= e($sponsor->account->first_name . ' ' . $sponsor->account->last_name);
$html .= ' &nbsp;<button type="button" class="btn icon-btn btn-xs btn-secondary" data-toggle="modal" data-target="#modals-load-content"
data-id="' . (int) $sponsor->id . '"
data-action="business-user-detail"
data-back=""
data-modal="modal-xl"
data-init_from="admin"
data-route="' . route('modal_load') . '"><span class="fa fa-calculator"></span></button><br>';
}
$html .= '<span class="small no-line-break">' . e($sponsor->email);
if ($sponsor->account) {
$html .= ' | ' . e($sponsor->account->m_account);
}
$html .= '</span>';
return $html;
}
}

View file

@ -0,0 +1,391 @@
<?php
namespace App\Services\BusinessPlan;
use App\Services\TranslationHelper;
/**
* Klasse für die HTML-Darstellung von Business-Trees
* Trennt Präsentationslogik von Geschäftslogik
*/
class TreeHtmlRenderer
{
private string $initFrom;
private bool $forceLiveCalculation;
public function __construct(string $initFrom = 'member', bool $forceLiveCalculation = false)
{
$this->initFrom = $initFrom;
$this->forceLiveCalculation = $forceLiveCalculation;
}
/**
* Rendert den kompletten Business-Tree als HTML
*/
public function renderTree(array $businessUsers): string
{
if (empty($businessUsers)) {
return '<div class="alert alert-info">Keine Business-User gefunden.</div>';
}
$html = '<ol class="dd-list">';
foreach ($businessUsers as $businessUser) {
$html .= $this->renderUserItem($businessUser, 0);
}
$html .= '</ol>';
return $html;
}
/**
* Rendert parentlose User als HTML
*/
public function renderParentless(array $parentless): string
{
if (empty($parentless)) {
return '<div class="alert alert-info">Keine parentlosen User gefunden.</div>';
}
$html = '';
foreach ($parentless as $item) {
$html .= $this->renderParentlessItem($item);
}
return $html;
}
/**
* Rendert Sponsor-Information als HTML
*/
public function renderSponsor($sponsor): string
{
if (!$sponsor) {
return '<div class="alert alert-warning">' . __('team.no_sponsor_assigned') . '</div>';
}
return '<li class="dd-item dd-nodrag" data-id="">' .
'<div class="dd-handle">' .
$this->renderUserInfo($sponsor, false, true) .
'</div>' .
'</li>';
}
/**
* Rendert User Team Tree (für UserTeamCalcBot)
*/
public function renderUserTeamTree(array $teamMembers): string
{
if (empty($teamMembers)) {
return '<div class="alert alert-info">Keine Team-Mitglieder gefunden.</div>';
}
$html = '<ol class="dd-list">';
foreach ($teamMembers as $member) {
$html .= $this->renderTeamMemberItem($member, 0);
}
$html .= '</ol>';
return $html;
}
/**
* Rendert User Sponsor (für UserTeamCalcBot)
*/
public function renderUserSponsor(\App\User $sponsor): string
{
if (!$sponsor || !$sponsor->account) {
return '<div class="alert alert-info">Kein Sponsor gefunden.</div>';
}
$html = '<div class="dd-item">';
$html .= '<div class="dd-handle">';
$html .= '<div class="row">';
// Sponsor Info
$html .= '<div class="col-md-3">';
$html .= '<strong>' . e($sponsor->account->first_name . ' ' . $sponsor->account->last_name) . '</strong><br>';
$html .= '<small>' . e($sponsor->email) . '</small>';
$html .= '</div>';
// Account Info
$html .= '<div class="col-md-2">';
$html .= '<span class="badge badge-secondary">' . e($sponsor->account->m_account ?? '') . '</span>';
$html .= '</div>';
// Level Info
$html .= '<div class="col-md-2">';
if ($sponsor->user_level) {
$html .= '<span class="badge badge-primary">' . e($sponsor->user_level->getLang('name')) . '</span>';
}
$html .= '</div>';
// Status
$html .= '<div class="col-md-2">';
$html .= get_active_badge($sponsor->isActiveAccount());
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
return $html;
}
/**
* Rendert einzelnes Team-Mitglied
*/
private function renderTeamMemberItem($member, int $depth): string
{
$html = '<li class="dd-item" data-id="' . ($member->user_id ?? 0) . '">';
$html .= '<div class="dd-handle">';
$html .= '<div class="row">';
// Einrückung basierend auf Tiefe
$indent = str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;', $depth);
// Name und Email
$html .= '<div class="col-md-3">';
$html .= $indent;
$html .= '<strong>' . e(($member->first_name ?? '') . ' ' . ($member->last_name ?? '')) . '</strong><br>';
$html .= $indent . '<small>' . e($member->email ?? '') . '</small>';
$html .= '</div>';
// Account ID
$html .= '<div class="col-md-2">';
$html .= '<span class="badge badge-secondary">' . e($member->m_account ?? '') . '</span>';
$html .= '</div>';
// Level
$html .= '<div class="col-md-2">';
if (!empty($member->user_level_name)) {
$html .= '<span class="badge badge-primary">' . e($member->user_level_name) . '</span>';
if ($member->next_qual_user_level) {
$html .= '<span class="badge badge-outline-success ml-2"><i class="fa fa-arrow-up text-success" title="Karriere-Level erreicht!"></i></span>';
}
}
$html .= '</div>';
// Qualifikation
$html .= '<div class="col-md-2">';
if (!empty($member->qual_kp)) {
$pointsSum = (int) ($member->sales_volume_points_KP_sum ?? 0);
$qualKP = (int) $member->qual_kp;
$isQual = $pointsSum >= $qualKP;
$badgeClass = $isQual ? 'badge-success' : 'badge-warning';
$html .= '<span class="badge ' . $badgeClass . '">KU ' . $qualKP . '</span>';
}
$html .= '</div>';
// Status
$html .= '<div class="col-md-2">';
$html .= get_active_badge($member->active_account ?? 0);
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
// Kinder rendern
if (!empty($member->businessUserItems) && is_array($member->businessUserItems)) {
$html .= '<ol class="dd-list">';
foreach ($member->businessUserItems as $child) {
$html .= $this->renderTeamMemberItem($child, $depth + 1);
}
$html .= '</ol>';
}
$html .= '</li>';
return $html;
}
/**
* Rendert einen einzelnen User-Item mit Hierarchie
*/
private function renderUserItem($item, int $deep): string
{
$childrenHtml = '';
if ($item->businessUserItems && count($item->businessUserItems) > 0) {
$childrenHtml = '<ol class="dd-list dd-nodrag">';
foreach ($item->businessUserItems as $child) {
$childrenHtml .= $this->renderUserItem($child, $deep + 1);
}
$childrenHtml .= '</ol>';
}
return '<li class="dd-item dd-nodrag" data-id="' . $item->user_id . '">' .
'<div class="dd-handle">' .
$this->renderUserCardWithDepth($item, $deep) .
'</div>' .
$childrenHtml .
'</li>';
}
/**
* Rendert parentlosen User-Item
*/
private function renderParentlessItem($item): string
{
return '<li class="dd-item dd-nodrag" data-id="' . $item->user_id . '">' .
'<div class="dd-handle">' .
$this->renderUserInfo($item, true, false) .
'</div>' .
'</li>';
}
/**
* Rendert User-Card mit Tiefe-Anzeige
*/
private function renderUserCardWithDepth($item, int $deep): string
{
$depthBadge = '';
if ($deep > 0) {
$depthBadge = '<div class="d-flex flex-column justify-content-center align-items-center">' .
'<div class="text-large font-weight-bolder line-height-1 my-2 text-secondary badge badge-outline-secondary">' . $deep . '</div>' .
'</div>';
}
return '<div class="media align-items-center">' .
$depthBadge .
'<div class="media-body ml-2">' .
$this->renderUserInfo($item, false, false) .
'</div>' .
'</div>';
}
/**
* Rendert die Basis-User-Informationen
*/
private function renderUserInfo($item, bool $showSponsor = false, bool $isSponsor = false): string
{
$statusClass = $item->active_account ? '' : 'text-muted';
$iconClass = $item->active_account ? 'text-primary' : 'text-danger';
\Log::debug("TreeHtmlRenderer: Rendering user info for user {$item->user_id}");
$html = '<span class="' . $statusClass . '">';
// User Link
$html .= '<a href="#" class="text-black" data-toggle="modal" data-target="#modals-load-content" ' .
'data-id="' . $item->user_id . '" data-action="business-user-show" data-back="" ' .
'data-modal="modal-md" data-init_from="' . $this->initFrom . '" data-route="' . route('modal_load') . '">' .
'<span class="mr-1 ion ion-ios-contact ' . $iconClass . '"></span> ' .
'<strong>' . e($item->first_name . ' ' . $item->last_name) . '</strong>' .
'</a>';
// Email
$html .= ' <a href="mailto:' . e($item->email) . '">' . e($item->email) . '</a>';
// Optional: Geburtstag
$birthday = $item->user_birthday; // Magic Method __get() verwenden
if ($birthday && trim($birthday) !== '') {
$html .= ' | <i class="ion ion-ios-gift text-primary"></i> ' . e($birthday);
}
// Optional: Telefon
$phone = $item->user_phone; // Magic Method __get() verwenden
if ($phone && trim($phone) !== '') {
$html .= ' | <i class="ion ion-ios-call text-primary"></i> ' . e($phone);
}
// Level Badge
$levelName = $item->user_level_name ? TranslationHelper::transUserLevelName($item->user_level_name) : '';
$account = $item->m_account ?: '';
$html .= ' <span class="badge badge-outline-default ' . $statusClass . '">' . e($levelName . ' | ' . $account) . '</span>';
// Karriere-Aufstiegs-Icon für qualifizierte User (nur in Struktur-Ansicht)#
if ($item->next_qual_user_level) {
$html .= '<span class="badge badge-outline-success ml-2"><i class="fa fa-arrow-up text-success" title="Karriere-Level erreicht!"></i></span>';
}
// Details für aktive Accounts
if ($item->active_account) {
$html .= '<br><span class="small">';
if(!$isSponsor){
$html .= $this->renderAccountDetails($item);
}
// Action Button (außer für Sponsor-Ansicht)
if (!$isSponsor && $this->shouldShowActionButton()) {
$html .= $this->renderActionButton($item->user_id);
}
$html .= '</span>';
} else {
// Inaktive Accounts
$paymentDate = $item->payment_account_date ?: '';
$html .= '<br><span class="small">' . __('team.account_to') . ': ' . e($paymentDate) . '</span>';
}
// Sponsor für parentlose User
if ($showSponsor && $item->m_sponsor_name) {
$html .= '<br>' . e($item->m_sponsor_name);
}
$html .= '</span>';
return $html;
}
/**
* Rendert Account-Details (Punkte, Umsatz)
*/
private function renderAccountDetails($item): string
{
$totalPoints = $item->sales_volume_points_KP_sum ?: 0;
$ePoints = $item->sales_volume_KP_points ?: 0;
$sPoints = $item->sales_volume_points_shop ?: 0;
$totalSum = $item->sales_volume_total_sum ?: 0;
$eSum = $item->sales_volume_total ?: 0;
$sSum = $item->sales_volume_total_shop ?: 0;
return '<strong>' . __('team.total_points') . ': ' . $totalPoints . '</strong> | ' .
__('team.e') . ': ' . $ePoints . ' | ' .
__('team.s') . ': ' . $sPoints . ' <strong> | ' .
__('team.net_turnover') . ': ' . formatNumber($totalSum) . ' &euro;</strong> | ' .
__('team.e') . ': ' . formatNumber($eSum) . ' &euro; | ' .
__('team.s') . ': ' . formatNumber($sSum) . ' &euro;';
}
/**
* Rendert Action-Button für User-Details
*/
private function renderActionButton(int $userId): string
{
return ' | <button type="button" class="btn icon-btn btn-xs btn-secondary" ' .
'data-toggle="modal" data-target="#modals-load-content" ' .
'data-id="' . $userId . '" data-action="business-user-detail" ' .
'data-back="" data-modal="modal-xl" ' .
'data-init_from="' . $this->initFrom . '" ' .
'data-live="' . $this->forceLiveCalculation . '" ' .
'data-optimized="1" ' .
'data-route="' . route('modal_load') . '">' .
'<span class="fa fa-calculator"></span>' .
'</button>';
}
/**
* Prüft ob Action-Button angezeigt werden soll
*/
private function shouldShowActionButton(): bool
{
try {
return ($this->initFrom === 'admin' && \Auth::check() && \Auth::user()->isAdmin()) ||
($this->initFrom === 'member');
} catch (\Exception $e) {
// Fallback for tests or when no user is authenticated
return $this->initFrom === 'member';
}
}
/**
* Setzt den Kontext für die Darstellung
*/
public function setInitFrom(string $initFrom): self
{
$this->initFrom = $initFrom;
return $this;
}
}