commit 08-2025

This commit is contained in:
Kevin Adametz 2025-08-12 18:01:59 +02:00
parent 9ae662f63e
commit 480fdc65ed
404 changed files with 65310 additions and 2600431 deletions

View file

@ -0,0 +1,461 @@
<?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;
/**
* 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 $user_level_active_pos;
public function __construct($date)
{
$this->date = $date;
return $this;
}
/**
* Erstellt BusinessUser aus User-ID (Original-Methode für Rückwärtskompatibilität)
*/
public function makeUser($user_id): void
{
try {
// Prüfe ob bereits gespeicherte Business-Daten existieren
$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; // Bereits gespeicherte Daten verwenden
}
// Lade User mit Relations (weniger effizient als makeUserFromModel)
$user = User::with(['account', 'userLevel'])->find($user_id);
if (!$user) {
\Log::warning("BusinessUserItem: User not found: {$user_id}");
return;
}
$this->initializeFromUserModel($user);
} 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
* Nutzt bereits geladene Relations und vermeidet zusätzliche DB-Abfragen
*/
public function makeUserFromModel(User $user): void
{
try {
if (!$user || !$user->id) {
throw new \InvalidArgumentException('Invalid user model provided');
}
// Prüfe ob bereits gespeicherte Business-Daten existieren
$existingBusiness = null;
if ($user->relationLoaded('userBusiness')) {
$existingBusiness = $user->userBusiness->first();
}
if ($existingBusiness) {
$this->b_user = $existingBusiness;
return;
}
$this->initializeFromUserModel($user);
} 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('userLevel')) {
$user_level_active = $user->userLevel;
} 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 Error-Handling)
$account = $user->relationLoaded('account') ? $user->account : null;
if (!$account) {
\Log::warning("BusinessUserItem: No account found for user {$user->id}");
}
$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 Fallback
'm_account' => $account ? $account->m_account : '',
'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 Boundary-Check)
$shopVolume = (float) $this->b_user->sales_volume_total_shop;
$shopMargin = (float) $this->b_user->margin_shop;
$this->b_user->commission_shop_sales = round($shopVolume / 100 * $shopMargin, 2);
\Log::debug("BusinessUserItem: Created optimized user {$user->id} for {$this->date->month}/{$this->date->year}");
}
/**
* Berechnet ob Account aktiv ist (mit Error-Handling)
*/
private function calculateActiveAccount(User $user): bool
{
try {
if (!$user->payment_account) {
return false;
}
return Carbon::parse($user->payment_account)->gt(Carbon::parse($this->date->start_date));
} catch (\Exception $e) {
\Log::warning("BusinessUserItem: Error calculating active account for user {$user->id}: " . $e->getMessage());
return false;
}
}
/**
* Optimierte Sales Volume Abfrage (mit potenziellem Caching)
*/
private function getUserSalesVolumeOptimized(User $user, string $field)
{
try {
// Hier könnte Caching implementiert werden
$cacheKey = "sales_volume_{$user->id}_{$this->date->month}_{$this->date->year}_{$field}";
// Für jetzt: Direkter Aufruf (später durch Cache ersetzen)
return $user->getUserSalesVolumeBy($this->date->month, $this->date->year, $field);
} 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()
{
TreeCalcBotOptimized::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)
{
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];
$obj->points += (float) $points; // Type-Safety
$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(): void
{
try {
$qualUserLevel = $this->calcuQualLevel();
if ($qualUserLevel !== null) {
$this->setNextUserLevel();
$this->b_user->qual_user_level = $qualUserLevel->toArray();
$this->setQualNextLevel();
$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
*/
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];
$points = (float) $object->points;
$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];
$points = (float) $object->points;
$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) =====
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])) {
$payline_points += (float) $this->b_user->business_lines[$i]->points;
}
}
return $payline_points;
}
private function setQualNextLevel(): void
{
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(): void
{
$nextQualUserLevel = UserLevel::where('qual_pp', '<=', $this->b_user->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 {
$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();
}
}
}
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;
}
// Weitere Original-Methoden (checkSponsor, readParentsBusinessUsers, etc.)
// Diese bleiben unverändert für vollständige Kompatibilität
public function checkSponsor($user) { /* Original-Implementation */ }
public function readParentsBusinessUsers() { /* Original-Implementation */ }
public function readStoredParentsBusinessUsers($userBusinessStructure) { /* Original-Implementation */ }
public function isSave(): bool { return $this->b_user && $this->b_user->exists; }
}

View file

@ -0,0 +1,197 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
use App\Models\UserBusinessStructure;
use Carbon\Carbon;
use Illuminate\Support\Collection;
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');
}
/**
* Lädt Root-User mit optimiertem Eager Loading und Caching
*/
public function getRootUsers(): Collection
{
$cacheKey = "root_users_{$this->month}_{$this->year}";
return cache()->remember($cacheKey, 3600, function() {
\Log::info("BusinessUserRepository: Loading root users from database (cache miss)");
return User::with([
'account',
'userLevel',
'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)
->get();
});
}
/**
* Lädt User ohne Parent-Zuordnung (Lazy Loading für Memory-Effizienz)
*/
public function getParentlessUsers(array $excludeUserIds = []): \Generator
{
$query = User::with([
'account',
'userLevel',
'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',
'userLevel',
'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($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);
}
}
}
/**
* 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',
'userLevel',
'userBusiness' => function($query) {
$query->where('month', $this->month)
->where('year', $this->year);
}
])
->whereIn('id', $chunk)
->get()
->keyBy('id');
}
}
}

View file

@ -0,0 +1,599 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
use stdClass;
use Carbon\Carbon;
use App\Models\UserBusinessStructure;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Logging\Log as LogContract;
/**
* 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 ?BusinessUserItem $businessUser = null;
private ?BusinessUserItem $sponsor = null;
private array $processedUserIds = [];
private BusinessUserRepository $repository;
private TreeHtmlRenderer $renderer;
private LogContract $logger;
public function __construct(
int $month,
int $year,
string $initFrom = 'member',
?BusinessUserRepository $repository = null,
?TreeHtmlRenderer $renderer = null,
?LogContract $logger = null
) {
$this->validateInput($month, $year);
$this->initializeDate($month, $year);
$this->initFrom = $initFrom;
// Dependency Injection mit Fallback
$this->repository = $repository ?? new BusinessUserRepository($month, $year);
$this->renderer = $renderer ?? new TreeHtmlRenderer($initFrom);
$this->logger = $logger ?? app(LogContract::class);
}
/**
* Initialisiert die Business-Struktur für Admin-Ansicht
*/
public function initStructureAdmin(bool $check = true): void
{
try {
$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
*/
public function initStructureUser(int $userId): void
{
try {
$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);
$businessUserItem->makeUserFromModel($user); // ✅ Nutzt bereits geladene Relations
$this->addUserIdToProcessed($userId);
$this->businessUsers[] = $businessUserItem;
$storedStructure = $this->repository->getStoredStructure();
if ($storedStructure) {
$this->loadStoredParentsUsers($storedStructure);
if (isset($this->businessUsers[0]) && $this->businessUsers[0]->sponsor) {
$this->loadStoredSponsorUser($this->businessUsers[0]->sponsor->user_id);
}
} else {
$this->loadParentsUsers();
$this->loadSponsorUser($userId);
}
} catch (\Exception $e) {
$this->logger->error("Error initializing user structure for {$userId}: " . $e->getMessage());
throw $e;
}
}
/**
* Initialisiert detaillierte Business-User-Informationen
*/
public function initBusinesslUserDetail(User $user): void
{
try {
$this->logger->info("Initializing business user details for: {$user->id}");
$this->businessUser = new BusinessUserItemOptimized($this->date);
$this->businessUser->makeUserFromModel($user); // ✅ Nutzt bereits User-Objekt
$this->businessUser->checkSponsor($user);
if (!$this->businessUser->isSave()) {
// Aufbau der Struktur für den User in die unendliche Tiefe
$this->businessUser->readParentsBusinessUsers();
// Calculate Points in Lines (optimiert für Memory-Effizienz)
if (count($this->businessUser->businessUserItems) > 0) {
$this->calculateUserPointsOptimized($this->businessUser->businessUserItems, 1);
}
// 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)
*/
public function getGrowthBonus(): array
{
if (!$this->businessUser || !$this->businessUser->business_lines) {
return [];
}
if (count($this->businessUser->business_lines) > 6) {
$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;
}
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;
}
public static function addUserID(int $id): void
{
// Deprecated: Wird durch Instanz-Methode ersetzt
// Bleibt für Rückwärtskompatibilität erhalten
}
// ===== 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);
}
/**
* Baut frische Struktur auf
*/
private function buildFreshStructure(): void
{
$this->loadRootUsers();
$this->loadParentsUsers();
$this->loadParentlessUsers();
}
/**
* 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);
$businessUserItem->makeUserFromModel($user); // ✅ Nutzt bereits geladene Relations
$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
{
foreach ($this->businessUsers as $businessUser) {
$businessUser->readParentsBusinessUsers();
}
}
/**
* 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);
$businessUserItem->makeUserFromModel($user); // ✅ Nutzt bereits geladene Relations
$this->parentless[] = $businessUserItem;
$count++;
}
$this->logger->info("Loaded {$count} parentless users with optimized relations");
}
/**
* Lädt Sponsor für User
*/
private function loadSponsorUser(int $userId): void
{
try {
$sponsorUser = $this->repository->getSponsorForUser($userId);
if ($sponsorUser) {
$this->sponsor = new BusinessUserItem($this->date);
$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 BusinessUserItem($this->date);
$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 BusinessUserItem($this->date);
$businessUserItem->makeUser($obj->user_id);
$this->parentless[] = $businessUserItem;
}
}
}
/**
* Gespeicherten Sponsor laden
*/
private function loadStoredSponsorUser(int $userId): void
{
$this->sponsor = new BusinessUserItem($this->date);
$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): 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($this->businessUser->business_lines[$line])) {
$obj = new stdClass();
$obj->points = 0;
$this->businessUser->addBusinessLineToUser($line, $obj);
}
// Punkte hinzufügen (mit Validierung)
$points = (float) ($item->sales_volume_points_TP_sum ?? 0);
if ($points > 0) {
$this->businessUser->addBusinessLinePoints($line, $points);
$this->businessUser->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
*/
private 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");
}
}
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,598 @@
<?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 ?BusinessUserItem $sponsor = null;
private array $processedUserIds = [];
private BusinessUserRepository $repository;
private TreeHtmlRenderer $renderer;
private LoggerInterface $logger;
public function __construct(
int $month,
int $year,
string $initFrom = 'member',
?BusinessUserRepository $repository = null,
?TreeHtmlRenderer $renderer = null,
?LoggerInterface $logger = null
) {
$this->validateInput($month, $year);
$this->initializeDate($month, $year);
$this->initFrom = $initFrom;
// Dependency Injection mit Fallback
$this->repository = $repository ?? new BusinessUserRepository($month, $year);
$this->renderer = $renderer ?? new TreeHtmlRenderer($initFrom);
$this->logger = $logger ?? app(LoggerInterface::class);
}
/**
* Initialisiert die Business-Struktur für Admin-Ansicht
*/
public function initStructureAdmin(bool $check = true): void
{
try {
$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
*/
public function initStructureUser(int $userId): void
{
try {
$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);
$businessUserItem->makeUserFromModel($user); // ✅ Nutzt bereits geladene Relations
$this->addUserIdToProcessed($userId);
$this->businessUsers[] = $businessUserItem;
$storedStructure = $this->repository->getStoredStructure();
if ($storedStructure) {
$this->loadStoredParentsUsers($storedStructure);
if (isset($this->businessUsers[0]) && $this->businessUsers[0]->sponsor) {
$this->loadStoredSponsorUser($this->businessUsers[0]->sponsor->user_id);
}
} else {
$this->loadParentsUsers();
$this->loadSponsorUser($userId);
}
} catch (\Exception $e) {
$this->logger->error("Error initializing user structure for {$userId}: " . $e->getMessage());
throw $e;
}
}
/**
* Initialisiert detaillierte Business-User-Informationen
*/
public function initBusinesslUserDetail(User $user): void
{
try {
$this->logger->info("Initializing business user details for: {$user->id}");
$this->businessUser = new BusinessUserItemOptimized($this->date);
$this->businessUser->makeUserFromModel($user); // ✅ Nutzt bereits User-Objekt
$this->businessUser->checkSponsor($user);
if (!$this->businessUser->isSave()) {
// Aufbau der Struktur für den User in die unendliche Tiefe
$this->businessUser->readParentsBusinessUsers();
// Calculate Points in Lines (optimiert für Memory-Effizienz)
if (count($this->businessUser->businessUserItems) > 0) {
$this->calculateUserPointsOptimized($this->businessUser->businessUserItems, 1);
}
// 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)
*/
public function getGrowthBonus(): array
{
if (!$this->businessUser || !$this->businessUser->business_lines) {
return [];
}
if (count($this->businessUser->business_lines) > 6) {
$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;
}
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;
}
public static function addUserID(int $id): void
{
// Deprecated: Wird durch Instanz-Methode ersetzt
// Bleibt für Rückwärtskompatibilität erhalten
}
// ===== 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);
}
/**
* Baut frische Struktur auf
*/
private function buildFreshStructure(): void
{
$this->loadRootUsers();
$this->loadParentsUsers();
$this->loadParentlessUsers();
}
/**
* 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);
$businessUserItem->makeUserFromModel($user); // ✅ Nutzt bereits geladene Relations
$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
{
foreach ($this->businessUsers as $businessUser) {
$businessUser->readParentsBusinessUsers();
}
}
/**
* 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);
$businessUserItem->makeUserFromModel($user); // ✅ Nutzt bereits geladene Relations
$this->parentless[] = $businessUserItem;
$count++;
}
$this->logger->info("Loaded {$count} parentless users with optimized relations");
}
/**
* Lädt Sponsor für User
*/
private function loadSponsorUser(int $userId): void
{
try {
$sponsorUser = $this->repository->getSponsorForUser($userId);
if ($sponsorUser) {
$this->sponsor = new BusinessUserItem($this->date);
$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 BusinessUserItem($this->date);
$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 BusinessUserItem($this->date);
$businessUserItem->makeUser($obj->user_id);
$this->parentless[] = $businessUserItem;
}
}
}
/**
* Gespeicherten Sponsor laden
*/
private function loadStoredSponsorUser(int $userId): void
{
$this->sponsor = new BusinessUserItem($this->date);
$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): 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($this->businessUser->business_lines[$line])) {
$obj = new stdClass();
$obj->points = 0;
$this->businessUser->addBusinessLineToUser($line, $obj);
}
// Punkte hinzufügen (mit Validierung)
$points = (float) ($item->sales_volume_points_TP_sum ?? 0);
if ($points > 0) {
$this->businessUser->addBusinessLinePoints($line, $points);
$this->businessUser->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
*/
private 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");
}
}
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,241 @@
<?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;
public function __construct(string $initFrom = 'member')
{
$this->initFrom = $initFrom;
}
/**
* 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 einen einzelnen User-Item mit Hierarchie
*/
private function renderUserItem($item, int $deep): string
{
$childrenHtml = '';
if (isset($item->businessUserItems) && $item->businessUserItems) {
$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';
$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
if (isset($item->user_birthday) && $item->user_birthday) {
$html .= ' | <i class="ion ion-ios-gift text-primary"></i> ' . e($item->user_birthday);
}
// Optional: Telefon
if (isset($item->user_phone) && $item->user_phone) {
$html .= ' | <i class="ion ion-ios-call text-primary"></i> ' . e($item->user_phone);
}
// Level Badge
$levelName = isset($item->user_level_name) ? TranslationHelper::transUserLevelName($item->user_level_name) : '';
$account = isset($item->m_account) ? $item->m_account : '';
$html .= ' <span class="badge badge-outline-default ' . $statusClass . '">' . e($levelName . ' | ' . $account) . '</span>';
// Details für aktive Accounts
if ($item->active_account) {
$html .= '<br><span class="small">';
$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 = isset($item->payment_account_date) ? $item->payment_account_date : '';
$html .= '<br><span class="small">' . __('team.account_to') . ': ' . e($paymentDate) . '</span>';
}
// Sponsor für parentlose User
if ($showSponsor && isset($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 = isset($item->sales_volume_points_KP_sum) ? $item->sales_volume_points_KP_sum : 0;
$ePoints = isset($item->sales_volume_KP_points) ? $item->sales_volume_KP_points : 0;
$sPoints = isset($item->sales_volume_points_shop) ? $item->sales_volume_points_shop : 0;
$totalSum = isset($item->sales_volume_total_sum) ? $item->sales_volume_total_sum : 0;
$eSum = isset($item->sales_volume_total) ? $item->sales_volume_total : 0;
$sSum = isset($item->sales_volume_total_shop) ? $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-route="' . route('modal_load') . '">' .
'<span class="fa fa-calculator"></span>' .
'</button>';
}
/**
* Prüft ob Action-Button angezeigt werden soll
*/
private function shouldShowActionButton(): bool
{
return ($this->initFrom === 'admin' && \Auth::user()->isAdmin()) ||
($this->initFrom === 'member');
}
/**
* Setzt den Kontext für die Darstellung
*/
public function setInitFrom(string $initFrom): self
{
$this->initFrom = $initFrom;
return $this;
}
}