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

@ -391,6 +391,9 @@ class BusinessUserItem
}
public function __get($property) {
if($this->b_user === null){
return null;
}
if (property_exists($this->b_user, $property)) {
return $this->b_user->$property;
}

View file

@ -0,0 +1,797 @@
<?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;
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 $user_level_active_pos;
private $needsQualificationRecalculation = false;
public function __construct($date)
{
$this->date = $date;
$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 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}");
}
/**
* 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 = $user->account;
// 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 : '';
// 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();
\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());
}
}
/**
* 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 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];
// 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)
*/
public function readParentsBusinessUsers($forceLiveCalculation = false): void
{
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) {
$businessUserItem = new BusinessUserItemOptimized($this->date);
$businessUserItem->makeUserFromModel($user, $forceLiveCalculation);
$businessUserItem->addUserID();
$this->businessUserItems[] = $businessUserItem;
}
}
// Rekursiver Aufruf für alle Child-Items
foreach ($this->businessUserItems as $businessUserItem) {
$businessUserItem->readParentsBusinessUsers($forceLiveCalculation);
}
} catch (\Exception $e) {
Log::error("BusinessUserItem: Error reading parent users for {$this->b_user->user_id}: " . $e->getMessage());
}
}
/**
* Lädt Parent Business Users aus gespeicherter Struktur (Original-Implementation)
*/
public function readStoredParentsBusinessUsers($structure): void
{
try {
$parents = $this->findParentsBusinessOnStored($this->b_user->user_id, $structure);
if ($parents) {
foreach ($parents as $obj) {
$businessUserItem = new BusinessUserItemOptimized($this->date);
$businessUserItem->makeUser($obj->user_id);
$businessUserItem->addUserID();
$this->businessUserItems[] = $businessUserItem;
}
foreach ($this->businessUserItems as $businessUserItem) {
$businessUserItem->readStoredParentsBusinessUsers($parents);
}
}
} catch (\Exception $e) {
Log::error("BusinessUserItem: Error reading stored parent users: " . $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;
}
}

View file

@ -0,0 +1,198 @@
<?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');
}
/**
* 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',
'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)
->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);
}
}
}
/**
* 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

@ -34,7 +34,7 @@ class TreeCalcBot
}
public function initStructureAdmin($check = true)
public function initStructureAdmin($check = true, $forceLiveCalculation = false)
{
//check is month is saved.
if($check && $UserBusinessStructure = self::isFromStored($this->date->month, $this->date->year)){
@ -46,7 +46,6 @@ class TreeCalcBot
$this->readParentsUsers();
$this->readParentlessUser();
}
}
public function initStructureUser($user_id)

View file

@ -0,0 +1,910 @@
<?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);
$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->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;
}
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);
// 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);
$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);
$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->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);
$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);
$businessUserItem->makeUser($obj->user_id);
$this->parentless[] = $businessUserItem;
}
}
}
/**
* Gespeicherten Sponsor laden
*/
private function loadStoredSponsorUser(int $userId): void
{
$this->sponsor = new BusinessUserItemOptimized($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, 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
*/
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");
}
}
/**
* 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,183 @@
<?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-danger';
return '<span class="badge ' . $badgeClass . '"> KU ' . $qualKP . '</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-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-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;
}
}

View file

@ -1,172 +0,0 @@
<?php
namespace App\Services\BusinessPlan;
use App\User;
use stdClass;
use Carbon\Carbon;
use App\Models\UserLevel;
use App\Models\UserBusiness;
class TreeUserItem
{
public $items = [];
private $date;
public $lines = [];
public $user_level_active;
public function __construct($date)
{
$this->date = $date;
return $this;
}
public function makeUser(User $user){
$this->user_level_active = $user->user_level ? $user->user_level : null;
$this->b_user = new UserBusiness();
$fill = [
'user_id' => $user->id,
'month' => $this->date->month,
'year' => $this->date->year,
'm_level' => $user->m_level,
'm_sponsor' => $user->m_sponsor,
'user_level_name' => $user->user_level ? $user->user_level->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,
'sales_volume_points' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_points'),
'sales_volume_points_shop' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_points_shop'),
'sales_volume_points_sum' => $user->getUserSalesVolumeBy($this->date->month, $this->date->year, 'sales_volume_points_sum'),
'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->user_level_active ? $user->user_level_active->margin : 0,
'margin_shop' => $user->user_level_active ? $user->user_level_active->margin_shop : 0,
'qual_kp' => $user->user_level_active ? $user->user_level_active->qual_kp : 0,
'qual_tp' => $user->user_level_active ? $user->user_level_active->qual_tp : 0,
];
$this->b_user->fill($fill);
}
public function addUserID(){
TreeCalcBot::addUserID($this->user_id);
}
public function isQualKP(){
return ($this->sales_volume_points_sum >= $this->qual_kp) ? true : false;
}
public function getRestQualKP(){
return $this->sales_volume_points_sum - $this->qual_kp;
}
public function checkSponsor(){
if($this->m_sponsor === null){
$this->m_sponsor_name = 'Keinen Sponsor zugewiesen';
return;
}
$user = User::find($this->m_sponsor);
if($user){
if($user->account){
$this->m_sponsor_name = substr('Sponsor: '.$user->account->first_name.' '.$user->account->last_name.' | '.$user->email.' | '.$user->account->m_account, 0, 190);
}else{
$this->m_sponsor_name = 'Sponsor: '.$user->email;
}
return;
}
$this->m_sponsor_name = 'Sponsor wurde gelöscht.';
return;
}
/*
'total_tp' => ,
'total_qual_tp' => ,
'commission_total' => ,
'lines',
'items',
'qual_user_level_id'
*/
public function readParentsUser(){
$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->user_id) //<- need the id for parents / sponsors
->where('users.payment_account', "!=", null)
->where('users.active_date', "<=", $this->date->end_date)
->get();
dd($users);
if($users){
foreach($users as $user){
$TreeUserItem = new TreeUserItem($this->date);
$TreeUserItem->makeUser($user);
$TreeUserItem->addUserID();
$this->items[] = $TreeUserItem;
}
}
foreach($this->items as $item){
$item->readParentsUser();
}
}
public function calcUserTP($line){
if(!isset($this->lines[$line])){
$this->lines[$line] = new stdClass();
$this->lines[$line]->points = 0;
}
foreach($this->items as $item){
if(count($item->items) > 0){
$this->calcUserTP($line+1);
}
$this->lines[$line]->points += $item->sales_volume_points_sum;
$this->total_tp += $item->sales_volume_points_sum;
}
}
public function calcQualTP(){
if($this->isQualKP()){
$this->total_qual_tp = $this->total_tp + $this->getRestQualKP();
$this->qual_user_level = UserLevel::where('qual_tp', '<=', $this->total_qual_tp)->where('pos', '<=', $this->user->user_level->pos)->orderBy('qual_tp', 'desc')->first();
$this->commission_total = 0;
if($this->qual_user_level){
foreach($this->lines as $line => $values){
$values->margin = $this->qual_user_level->{'pr_line_'.$line};
if($line > 6){
//wachstumsbonus
$values->margin = $this->qual_user_level->growth_bonus;
}
$values->commission = round($values->points / 100 * $values->margin, 2);
$this->commission_total += $values->commission;
$this->lines[$line] = $values;
}
}
}
}
public function __get($property) {
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,296 @@
<?php
namespace App\Services;
use App\Models\UserShop;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
/**
* Domain Service - Centralized domain and subdomain management
*
* This service provides a centralized way to handle domain resolution,
* subdomain validation, and domain-specific configuration management.
*/
class DomainService
{
private const CACHE_TTL = 3600; // 1 hour
private const CACHE_TAG_USER_SHOPS = 'user_shops';
private const CACHE_TAG_DOMAIN_PARSING = 'domain_parsing';
private const FIXED_SUBDOMAINS = ['my', 'in', 'checkout'];
private array $domainConfig;
public function __construct(?array $domainConfig = null)
{
$this->domainConfig = $domainConfig ?? config('domains');
}
/**
* Determine the type of subdomain
*/
public function getSubdomainType(string $subdomain): string
{
// Frühe Validierung: Prüfe reservierte Subdomains aus Konfiguration
$reservedSubdomains = $this->domainConfig['reserved_subdomains'] ?? self::FIXED_SUBDOMAINS;
if (in_array($subdomain, $reservedSubdomains)) {
return match($subdomain) {
'my' => 'crm',
'in' => 'portal',
'checkout' => 'checkout',
default => 'unknown' // Andere reservierte Subdomains sind ungültig
};
}
// Frühe Validierung: Prüfe auf ungültige Zeichen für UserShop-Slugs
if (!preg_match('/^[a-z0-9-]+$/', $subdomain) || strlen($subdomain) < 3) {
return 'unknown';
}
// Check if it's a valid user shop
if ($this->isValidUserShop($subdomain)) {
return 'user-shop';
}
return 'unknown';
}
/**
* Check if a subdomain represents a valid user shop
*/
public function isValidUserShop(string $slug): bool
{
$cacheKey = "user_shop_valid_{$slug}";
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($slug) {
// Optimierte Query mit allen Validierungen in einem DB-Call
$userShop = UserShop::where('slug', $slug)
->where('active', true)
->whereHas('user', function ($query) {
$query->whereNotNull('payment_shop')
->where('payment_shop', '>', now());
})
->exists();
return $userShop;
});
}
/**
* Get user shop by slug with caching
*/
public function getUserShop(string $slug): ?UserShop
{
$cacheKey = "user_shop_{$slug}";
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($slug) {
// Optimierte Query mit allen Validierungen in einem DB-Call
return UserShop::where('slug', $slug)
->where('active', true)
->whereHas('user', function ($query) {
$query->whereNotNull('payment_shop')
->where('payment_shop', '>', now());
})
->with('user')
->first();
});
}
/**
* Parse domain from request and determine context
*/
public function parseDomain(string $host): array
{
// Normalisiere den Host (lowercase)
$host = strtolower(trim($host));
$parts = explode('.', $host);
// Handle different TLD scenarios
if (count($parts) < 2) {
\Log::warning('Invalid host format', ['host' => $host]);
return [
'type' => 'invalid',
'domain' => $host,
'subdomain' => null,
'tld' => null,
'host' => $host
];
}
// Extract TLD and domain
$tld = '.' . end($parts);
$domain = $parts[count($parts) - 2];
// Check for subdomain
$subdomain = null;
if (count($parts) > 2) {
$subdomain = $parts[0];
\Log::debug('DomainService: Using extracted subdomain', ['subdomain' => $subdomain, 'host' => $host]);
}
// Determine domain type based on subdomain and host
$type = $this->determineDomainType($host, $subdomain);
return [
'type' => $type,
'domain' => $domain,
'subdomain' => $subdomain,
'tld' => $tld,
'host' => $host,
'default_user_shop' => $this->domainConfig['domains']['shop']['default_user_shop'] ?? null
];
}
/**
* Determine domain type based on full host and subdomain
*/
private function determineDomainType(string $host, ?string $subdomain): string
{
// Check against configured domains
foreach ($this->domainConfig['domains'] as $type => $config) {
if (isset($config['host'])) {
// Handle wildcard user-shop pattern
if ($type === 'user-shop') {
$pattern = str_replace('{subdomain}', '([a-z0-9-]+)', $config['host']);
if (preg_match("/^{$pattern}$/", $host)) {
return 'user-shop';
}
} else {
// Exact match for other domains
if ($host === $config['host']) {
return $type;
}
}
}
}
// Additional check for subdomain-based detection
if ($subdomain) {
$subdomainType = $this->getSubdomainType($subdomain);
if ($subdomainType !== 'unknown') {
return $subdomainType;
}
}
return 'unknown';
}
/**
* Build URL for specific domain type
*/
public function buildUrl(string $type, ?string $path = null, ?string $slug = null): string
{
$protocol = $this->domainConfig['protocol'] ?? 'https://';
$domainConfig = $this->domainConfig['domains'][$type] ?? null;
if (!$domainConfig) {
throw new \InvalidArgumentException("Unknown domain type: {$type}");
}
$host = $domainConfig['host'];
// Handle user-shop wildcard
if ($type === 'user-shop') {
if (!$slug) {
throw new \InvalidArgumentException('Slug required for user-shop URLs');
}
$host = str_replace('{subdomain}', $slug, $host);
}
$url = $protocol . $host;
if ($path) {
$url .= '/' . ltrim($path, '/');
}
return $url;
}
/**
* Get domain configuration
*/
public function getDomainConfiguration(): array
{
return $this->domainConfig;
}
/**
* Clear user shop cache
*/
public function clearUserShopCache(string $slug): void
{
Cache::forget("user_shop_valid_{$slug}");
Cache::forget("user_shop_{$slug}");
}
/**
* Clear all user shop caches
*/
public function clearAllUserShopCaches(): void
{
// In Laravel mit Cache-Tags würde das eleganter funktionieren
// Für jetzt eine einfache Lösung für häufig verwendete Shops
$commonSlugs = ['aloevera']; // Füge häufig verwendete Slugs hinzu
foreach ($commonSlugs as $slug) {
$this->clearUserShopCache($slug);
}
}
/**
* Get default user shop for main domain (fallback)
*/
public function getDefaultUserShop(): ?UserShop
{
$defaultSlug = $this->domainConfig['domains']['shop']['default_user_shop'] ?? 'aloevera';
return $this->getUserShop($defaultSlug);
}
/**
* Validate domain configuration
*/
public function validateConfiguration(): array
{
$errors = [];
// Validate main domains
$requiredDomains = ['main', 'shop', 'crm', 'portal', 'checkout', 'user-shop'];
foreach ($requiredDomains as $domain) {
if (empty($this->domainConfig['domains'][$domain]['host'])) {
$errors[] = "Domain '{$domain}' not configured";
}
}
// Validate protocol
if (empty($this->domainConfig['protocol'])) {
$errors[] = 'Protocol not configured';
}
// Validate reserved subdomains
if (empty($this->domainConfig['reserved_subdomains'])) {
$errors[] = 'Reserved subdomains not configured';
}
// Validate shop default
$defaultShop = $this->domainConfig['domains']['shop']['default_user_shop'] ?? null;
if (!$defaultShop) {
$errors[] = 'Default user shop not configured for shop domain';
}
return $errors;
}
/**
* Check if domain configuration is valid
*/
public function isConfigurationValid(): bool
{
return empty($this->validateConfiguration());
}
}

View file

@ -32,7 +32,7 @@ class HTMLHelper
private static $roles = [
0 => 'Kunde',
0 => 'Berater',
1 => 'VIP',
2 => 'Admin',
3 => 'SuperAdmin',
@ -314,7 +314,7 @@ class HTMLHelper
}
public static function getSalutation($id){
$values = array('mr' => __('MR'), 'ms' => __('MS'));
$values = array('mr' => __('MR'), 'ms' => __('MS'), 'di' => __('DIV'));
$ret = "";
$ret .= '<option value="">'.__('please select').'</option>\n';
foreach ($values as $key => $value){
@ -325,7 +325,7 @@ class HTMLHelper
}
public static function getSalutationLang($id){
$values = array('mr' => __('MR'), 'ms' => __('MS'));
$values = array('mr' => __('MR'), 'ms' => __('MS'), 'di' => __('DIV'));
return (!empty($values[$id]) ? $values[$id] : '');
}

View file

@ -1,188 +0,0 @@
<?php
namespace App\Services;
//use FPDI in myMerge v2
//use FPDF;
//use FPDI;
class MyPDFMerger
{
private $_files; //['form.pdf'] ["1,2,4, 5-19"]
private $_fpdi;
public function __construct()
{
/* if(!class_exists("FPDF")) {
require_once(__DIR__.'/fpdf/fpdf.php');
}
if(!class_exists("FPDI")) {
require_once(__DIR__.'/fpdi/fpdi.php');
}*/
}
public function addPDF($filepath, $pages = 'all')
{
if (file_exists($filepath)) {
if (strtolower($pages) != 'all') {
$pages = $this->_rewritepages($pages);
}
$this->_files[] = array($filepath, $pages);
} else {
throw new \exception("Could not locate PDF on '$filepath'");
}
return $this;
}
public function myMerge($outputmode = 'browser', $outputpath = 'newfile.pdf', $theme = false)
{
if (!isset($this->_files) || !is_array($this->_files)): throw new \exception("No PDFs to merge."); endif;
$fpdi = new \setasign\Fpdi\Fpdi();
$first = 1;
//
//merger operations
foreach ($this->_files as $file) {
$filename = $file[0];
$filepages = $file[1];
$count = $fpdi->setSourceFile($filename);
//add the pages
if ($filepages == 'all') {
for ($i = 1; $i <= $count; $i++) {
$count = $fpdi->setSourceFile($filename);
$template = $fpdi->importPage($i);
$size = $fpdi->getTemplateSize($template);
$orientation = ($size['height'] > $size['width']) ? 'P' : 'L';
$fpdi->AddPage($orientation, array($size['width'], $size['height']));
if($theme){
$fpdi->setSourceFile(__DIR__ . '/../../public/pdf/'.$theme.'-'.$first.'.pdf');
if($first == 1){
$first = 2;
}
$backId = $fpdi->importPage(1);
$fpdi->useTemplate($backId);
}
$fpdi->useTemplate($template);
}
} else {
foreach ($filepages as $page) {
$count = $fpdi->setSourceFile($filename);
if (!$template = $fpdi->importPage($page)): throw new \exception("Could not load page '$page' in PDF '$filename'. Check that the page exists."); endif;
$size = $fpdi->getTemplateSize($template);
$orientation = ($size['h'] > $size['w']) ? 'P' : 'L';
$fpdi->AddPage($orientation, array($size['w'], $size['h']));
if($theme){
$fpdi->setSourceFile(__DIR__ . '/../../public/pdf/'.$theme.'-'.$first.'.pdf');
if($first == 1){
$first = 2;
}
$backId = $fpdi->importPage(1);
$fpdi->useTemplate($backId);
}
$fpdi->useTemplate($template);
}
}
//after first file (invoice) on bpaper
$slug = false;
}
//output operations
$mode = $this->_switchmode($outputmode);
if ($mode == 'S') {
return $fpdi->Output($outputpath, 'S');
} else {
if ($fpdi->Output($outputpath, $mode) == '') {
return true;
} else {
throw new \exception("Error outputting PDF to '$outputmode'.");
return false;
}
}
}
/**
* FPDI uses single characters for specifying the output location. Change our more descriptive string into proper format.
* @param $mode
* @return Character
*/
private function _switchmode($mode)
{
switch (strtolower($mode)) {
case 'download':
return 'D';
break;
case 'browser':
return 'I';
break;
case 'file':
return 'F';
break;
case 'string':
return 'S';
break;
default:
return 'I';
break;
}
}
/**
* Takes our provided pages in the form of 1,3,4,16-50 and creates an array of all pages
* @param $pages
* @return array
* @throws exception
*/
private function _rewritepages($pages)
{
$pages = str_replace(' ', '', $pages);
$part = explode(',', $pages);
//parse hyphens
foreach ($part as $i) {
$ind = explode('-', $i);
if (count($ind) == 2) {
$x = $ind[0]; //start page
$y = $ind[1]; //end page
if ($x > $y): throw new \exception("Starting page, '$x' is greater than ending page '$y'.");
return false; endif;
//add middle pages
while ($x <= $y): $newpages[] = (int)$x;
$x++; endwhile;
} else {
$newpages[] = (int)$ind[0];
}
}
return $newpages;
}
}
/*
$pdf = new PDFMerger;
$pdf->addPDF('samplepdfs/one.pdf', '1, 3, 4')
->addPDF('samplepdfs/two.pdf', '1-2')
->addPDF('samplepdfs/three.pdf', 'all')
->merge('file', 'samplepdfs/TEST2.pdf');
//REPLACE 'file' WITH 'browser', 'download', 'string', or 'file' for output options
//You do not need to give a file path for browser, string, or download - just the name.
*/

View file

@ -0,0 +1,125 @@
<?php
namespace App\Services;
use App\Models\UserBusiness;
/**
* Helper-Klasse für die optimierte Generierung von Next-Level-Badges
*
* Diese Klasse nutzt ausschließlich bereits berechnete und gespeicherte Daten
* aus der UserBusiness-Tabelle, anstatt für jeden User eine neue TreeCalcBot-Instanz
* zu erstellen. Dies führt zu erheblichen Performance-Verbesserungen bei DataTables.
*/
class NextLevelBadgeHelper
{
/**
* Generiert Badge für nächsten Level Qualifikation basierend auf UserBusiness-Daten
*
* @param UserBusiness $userBusiness Bereits gespeicherte Business-Daten
* @return string HTML Badge
*/
public static function generateBadgeFromUserBusiness(UserBusiness $userBusiness): string
{
return self::renderBadge($userBusiness);
}
/**
* Generiert Badge basierend auf BusinessUser-Objekt (für TreeCalcBot-Kompatibilität)
*
* @param object $businessUser Business-User-Objekt mit Level-Daten
* @return string HTML Badge
*/
public static function generateBadgeFromBusinessUser($businessUser): string
{
return self::renderBadge($businessUser);
}
/**
* Zentrale Badge-Rendering-Logik
*
* @param mixed $source UserBusiness Model oder BusinessUser Objekt
* @return string HTML Badge
*/
private static function renderBadge($source): string
{
// Prüfe ob User für den nächsten Level qualifiziert ist (grün)
if (!empty($source->next_qual_user_level)) {
return self::renderQualifiedBadge($source);
}
// Prüfe ob User den Level erreichen könnte, aber noch nicht qualifiziert ist (gelb)
if (!empty($source->next_can_user_level)) {
return self::renderCanReachBadge($source);
}
// Kein nächster Level verfügbar (rot)
return self::renderNoLevelBadge();
}
/**
* Rendert Badge für qualifizierte User (grün)
*/
private static function renderQualifiedBadge($source): string
{
$level = $source->next_qual_user_level;
$ku = formatNumber($source->sales_volume_points_KP_sum ?? 0, 0);
$ku_required = formatNumber($level['qual_kp'] ?? 0, 0);
$tp = formatNumber($source->payline_points_qual_kp ?? 0, 0);
$tp_required = formatNumber($level['qual_pp'] ?? 0, 0);
$levelName = TranslationHelper::transUserLevelName($level['name'] ?? 'Unbekannt');
return '<span class="badge badge-outline-success" title="Qualifiziert für nächsten Level">
<i class="fa fa-check"></i> ' . e($levelName) . '<br/>
<small>KU: ' . $ku . '/' . $ku_required . ' | TP: ' . $tp . '/' . $tp_required . '</small>
</span>';
}
/**
* Rendert Badge für User die den Level erreichen könnten (gelb)
*/
private static function renderCanReachBadge($source): string
{
$level = $source->next_can_user_level;
$ku = formatNumber($source->sales_volume_points_KP_sum ?? 0, 0);
$ku_required = formatNumber($level['qual_kp'] ?? 0, 0);
$tp = formatNumber($source->payline_points_qual_kp ?? 0, 0);
$tp_required = formatNumber($level['qual_pp'] ?? 0, 0);
$levelName = TranslationHelper::transUserLevelName($level['name'] ?? 'Unbekannt');
return '<span class="badge badge-outline-warning-dark" title="Noch nicht qualifiziert">
<i class="fa fa-clock"></i> ' . e($levelName) . '<br/>
<small>KU: ' . $ku . '/' . $ku_required . ' | TP: ' . $tp . '/' . $tp_required . '</small>
</span>';
}
/**
* Rendert Badge wenn kein nächster Level verfügbar ist (rot)
*/
private static function renderNoLevelBadge(): string
{
return '<span class="badge badge-outline-warning" title="Kein nächster Level verfügbar">
<i class="fa fa-times"></i>
</span>';
}
/**
* Fallback-Badge bei Fehlern oder fehlenden Daten
*/
public static function renderErrorBadge(string $message = 'Fehler bei der Berechnung'): string
{
return '<span class="badge badge-outline-danger" title="' . e($message) . '">
<i class="fa fa-exclamation"></i> Fehler
</span>';
}
/**
* Badge für fehlende Daten
*/
public static function renderNoDataBadge(): string
{
return '<span class="badge badge-outline-secondary" title="Keine Daten verfügbar">
<i class="fa fa-question"></i> Keine Daten
</span>';
}
}

View file

@ -7,6 +7,7 @@ use App\Models\Country;
use App\Models\Product;
use App\Models\Setting;
use App\Models\ShippingCountry;
use App\Models\ShoppingInstance;
use App\Models\ShoppingUser;
use App\Services\dbip\MyDBIP;
use App\Services\IPinfo\IPinfo;
@ -46,7 +47,7 @@ class Shop
}
public static function getLangChange()
public static function getLangChange($instance = 'shopping')
{
$ret = [];
$countries = Country::whereActive(true)->whereSwitch(true)->get();
@ -60,11 +61,11 @@ class Shop
$ret[strtolower($country->code)] = $country;
}
}
Shop::getUserShopLang($first_country);
Shop::getUserShopLang($first_country, $instance);
return $ret;
}
public static function getUserShopLang($country = null)
public static function getUserShopLang($country = null, $instance = 'shopping')
{
if (\Session::has('user_shop_lang')) {
if ($user_shop_lang = \Session::get('user_shop_lang')) {
@ -72,21 +73,23 @@ class Shop
}
}
if ($country) {
Shop::initUserShopLang($country);
Shop::initUserShopLang($country, $instance);
return strtolower($country->code);
}
return false;
}
public static function initUserShopLang($country)
//init User Shop Lang for Webshop
public static function initUserShopLang($country, $instance = 'shopping')
{
Yard::instance('shopping')->destroy();
Yard::instance($instance)->destroy();
\Session::put('user_shop_lang', strtolower($country->code));
//init Yard
self::initUserShopYard($country);
self::initUserShopYard($country, $instance);
}
public static function initUserShopYard($country)
//init Yard for user shop Webshop
public static function initUserShopYard($country, $instance = 'shopping')
{
//Lieferadresse im Drittland?
self::$user_tax_free = $country->supply_country ? true : false;
@ -95,8 +98,8 @@ class Shop
self::$shipping_country = $ShippingCountry;
self::$user_country = $country;
Yard::instance('shopping')->setShippingCountryWithPrice($ShippingCountry->id);
Yard::instance('shopping')->setUserPriceInfos(Shop::getShopYardInfo());
Yard::instance($instance)->setShippingCountryWithPrice($ShippingCountry->id);
Yard::instance($instance)->setUserPriceInfos(Shop::getShopYardInfo());
}
@ -126,6 +129,23 @@ class Shop
}
return $shopping_user;
}
//prüfe ob checkout bereits gestartet wurde, und wenn ja, dann lösche die Instanz
public static function deleteCheckoutInstance(){
if(Yard::instance('checkout')->count() > 0){
Yard::instance('checkout')->destroy();
}
if(\Session::has('user_shop_identifier')){
ShoppingInstance::where('identifier', \Session::get('user_shop_identifier'))->delete();
\Session::forget('user_shop_identifier');
}
\Session::forget('user_shop_payment');
\Session::forget('auth_user');
\Session::forget('back_link');
\Session::forget('new_session');
}
public static function checkShoppingCountry($for, $id = null)
{

View file

@ -1,9 +1,10 @@
<?php
namespace App\Services;
use Yard;
use App\User;
use App\Models\ShippingCountry;
use App\User;
use Illuminate\Support\Str;
use Yard;
class UserService
{
@ -12,7 +13,7 @@ class UserService
public static $shipping_free = false;
public static $user_tax_free = false;
public static $user_reverse_charge = false;
public static $instance = 'shopping';
public static function getTransChange(){
@ -24,7 +25,11 @@ class UserService
return $ret;
}
public static function setInstance($instance){
self::$instance = $instance;
}
//init Yard for user order Customer
public static function initCustomerYard($shopping_user, $for){
self::$user_tax_free = false;
if($shopping_user->same_as_billing){
@ -40,15 +45,16 @@ class UserService
$ShippingCountry = ShippingCountry::whereCountryId(self::$shipping_country->id)->first();
self::$shipping_free = $ShippingCountry->shipping ? $ShippingCountry->shipping->free : false;
self::$shipping_free = self::$shipping_free !== null ? self::$shipping_free : false;
Yard::instance('shopping')->setShippingCountryWithPrice($ShippingCountry->id, $for);
Yard::instance('shopping')->setUserPriceInfos(self::getYardInfo());
Yard::instance(self::$instance)->setShippingCountryWithPrice($ShippingCountry->id, $for);
Yard::instance(self::$instance)->setUserPriceInfos(self::getYardInfo());
}
//init Yard for user order Berater
public static function initUserYard(User $user, $shipping_country_id, $for){
self::$shipping_free = false;
self::checkUserTaxShippingCountry($user, $shipping_country_id,);
Yard::instance('shopping')->setShippingCountryWithPrice($shipping_country_id, $for);
Yard::instance('shopping')->setUserPriceInfos(self::getYardInfo());
Yard::instance(self::$instance)->setShippingCountryWithPrice($shipping_country_id, $for);
Yard::instance(self::$instance)->setUserPriceInfos(self::getYardInfo());
}
@ -142,7 +148,7 @@ class UserService
public static function createConfirmationCode() {
$unique = false;
do{
$confirmation_code = str_random(30);
$confirmation_code = Str::random(30);
if(User::where('confirmation_code', '=', $confirmation_code)->count() == 0){
$unique = true;
}

View file

@ -143,11 +143,9 @@ class UserUtil
$user->save();
}
public static function deleteUser(User $user)
public static function deleteUser(User $user, $complete = false)
{
if($user->account){
$user->account->delete();
}
//shop wird gelöscht
if($user->shop){
$subdomain_name = $user->shop->slug.'.mivita.care';
$user->shop->name = "delete".$user->shop->id;
@ -163,19 +161,32 @@ class UserUtil
$kas->action('delete_subdomain', $pra);
}
}
$user->email = "delete".time().mt_rand(1000000, 9999999);
//user soll nicht komplett gelöscht werden
$user->email = "delete-".$user->email;
//password wird gelöscht
$user->password = "delete".time();
$user->confirmed = 0;
$user->confirmation_code = "delete".time();
$user->confirmation_date = null;
$user->confirmation_code_to = null;
$user->confirmation_code_remider = 2;
$user->agreement = null;
// $user->agreement = null;
$user->active = 0;
$user->remember_token = '';
$user->active_date = null;
$user->admin = 0;
$user->deleted_at = now();
$user->pre_deleted_at = now();
//user soll komplett gelöscht werden
if($complete){
$user->email = "delete-".time()."-".rand(1000, 9999);
if($user->account){
$user->account->delete();
}
$user->pre_deleted_at = null;
}
$user->save();
return true;

View file

@ -1,12 +1,13 @@
<?php
namespace App\Services;
use Yard;
use App\Models\Country;
use App\Models\UserHistory;
use Illuminate\Support\Str;
use App\Models\ShippingCountry;
use App\Models\UserHistory;
use App\Models\UserShop;
use Illuminate\Support\Str;
use Request;
use Yard;
class Util
{
@ -15,7 +16,7 @@ class Util
public static function getToken()
{
return hash_hmac('sha256', str_random(40), config('app.key'));
return hash_hmac('sha256', Str::random(40), config('app.key'));
}
public static function uuidToken()
@ -122,10 +123,17 @@ class Util
}
public static function getUserShop(){
if(\Session::has('user_shop')){
if($user_shop = \Session::get('user_shop')){
return $user_shop;
}
$shop = session('user_shop');
if (empty($shop) || !is_object($shop)) {
return null;
}
return $shop;
}
public static function getDefaultUserShop(){
$user = \App\User::find(6);
if($user && $user->shop){
return $user->shop;
}
return false;
}
@ -220,17 +228,6 @@ class Util
return array_merge($p, $b);
}
public static function isCheckout(){
if(\Session::has('isCheckout')){
if(\Session::get('isCheckout') == true){
return true;
}
}
return false;
}
public static function checkUserLandIsNot($user){
if(isset($user->account->country_id)){
@ -243,14 +240,35 @@ class Util
return false;
}
public static function getMyMivitaShopUrl($add_url = ""){
if(\Session::has('user_shop_domain')){
$url = \Session::get('user_shop_domain').$add_url;
if (!str_starts_with($url, 'http')) {
$url = 'https://' . ltrim($url, '/');
}
return $url;
}
//alois sein shop
$user = \App\User::find(6);
if($user && $user->shop){
return config('app.protocol').$user->shop->slug.".".config('app.domain').config('app.tld_care').$add_url;
}
}
public static function getMyMivitaPortalUrl($protocol = true){
$pro = $protocol ? config('app.protocol') : "";
return $pro.config('app.pre_url_portal').config('app.domain').config('app.tld_care');
}
public static function getMyMivitaUrl($protocol = true){
$pro = $protocol ? config('app.protocol') : "";
return $pro.config('app.pre_url_crm').config('app.domain').config('app.tld_care');
}
public static function getUserPaymentFor(){
if(Yard::instance('shopping')->getYardExtra('user_shop_payment')){
return Yard::instance('shopping')->getYardExtra('user_shop_payment');
public static function getUserPaymentFor($instance = 'shopping'){
if(Yard::instance($instance)->getYardExtra('user_shop_payment')){
return Yard::instance($instance)->getYardExtra('user_shop_payment');
}
if(\Session::has('user_shop_payment')){
return \Session::get('user_shop_payment');
@ -268,20 +286,20 @@ class Util
return config('app.protocol').$user_shop->slug.".".config('app.domain').config('app.tld_care')."/back/to/shop/".$reference;
}
}
return url("/");
return config('app.protocol').config('app.domain').config('app.tld_care');
}
public static function getUserCardBackUrl($uri){
public static function getUserCardBackUrl($uri, $instance = 'shopping'){
if(\Session::has('user_shop')){
if(\Session::has('user_shop_domain')){
if(\Session::has('back_link')){
return \Session::get('back_link');
}
if(self::getUserPaymentFor() === 3){
if(self::getUserPaymentFor($instance) === 3){
return \Session::get('user_shop_domain')."/user/membership";
}
if(self::getUserPaymentFor() === 2){
if(self::getUserPaymentFor($instance) === 2){
return \Session::get('user_shop_domain')."/user/orders";
}
return \Session::get('user_shop_domain');
@ -290,7 +308,7 @@ class Util
return config('app.protocol').$user_shop->slug.".".config('app.domain').config('app.tld_care').$uri;
}
}
return url($uri);
return config('app.protocol').config('app.domain').config('app.tld_care');
}
public static function isMivitaShop(){

View file

@ -21,40 +21,49 @@ class Yard extends Cart
private $shipping_country_id = 0; //default de
private $shipping_is_for;
private $num_comp;
private $ysession;
private $user_tax_free;
private $shipping_free;
private $user_reverse_charge;
private $user_country_id;
private $user_country;
private $shopping_data = [];
private $events;
private $initShippingExtras = false;
public function __construct(SessionManager $session, Dispatcher $events)
{
$this->ysession = $session;
$this->yinstance = sprintf('%s.%s', 'cart', 'shipping_extras');
parent::__construct($session, $events);
}
public function instance($instance = null)
{
parent::instance($instance);
if(!$this->initShippingExtras){
$this->initShippingExtras = true; //erst true, sonst wird es immer wieder aufgerufen
$this->makeShippingExtras($instance);
}
return $this;
}
private function makeShippingExtras($instance){
if($this->getYardExtra('shipping_price')){
$this->shipping_price = (float) ($this->getYardExtra('shipping_price'));
}
if($this->getYardExtra('shipping_price_net')){
$this->shipping_price_net = (float) ($this->getYardExtra('shipping_price_net'));
}
if($this->getYardExtra('shipping_tax_rate')){
$this->shipping_tax_rate = (float) ($this->getYardExtra('shipping_tax_rate'));
}
if($this->getYardExtra('shipping_tax')){
$this->shipping_tax = (float) ($this->getYardExtra('shipping_tax'));
}
if($this->getYardExtra('shipping_country_id')){
$this->shipping_country_id = $this->getYardExtra('shipping_country_id');
}
if($this->getYardExtra('shipping_is_for')){
$this->shipping_is_for = $this->getYardExtra('shipping_is_for');
}
@ -77,33 +86,27 @@ class Yard extends Cart
$this->user_country = $this->getYardExtra('user_country');
}
$this->events = $events;
parent::__construct($session, $events);
if(gettype($this->shipping_country_id) !== 'object' && $this->shipping_country_id == 0){
$shippingCountry = ShippingCountry::first();
if($shippingCountry){
$this->shipping_country_id = $shippingCountry->id;
}
}
if($this->shipping_price == 0){
self::instance('shopping')->setShippingCountryWithPrice($this->shipping_country_id, $this->shipping_is_for);
self::instance($instance)->setShippingCountryWithPrice($this->shipping_country_id, $this->shipping_is_for);
}
}
public static function getTaxRate()
public function getTaxRate()
{
return config('cart.tax');
}
public function putYardExtra($key, $value){
$content = $this->getYContent();
$content->put($key, $value);
$this->ysession->put($this->yinstance, $content);
$this->putShippingExtras($content);
//$this->ysession->put($this->yinstance, $content);
}
public function getYardExtra($key){
@ -114,6 +117,15 @@ class Yard extends Cart
return false;
}
public function getYContent()
{
return $this->getShippingExtras();
/* if (is_null($this->ysession->get($this->yinstance))) {
return new Collection([]);
}
return $this->ysession->get($this->yinstance);*/
}
public function getShippingCountryName(){
$shippingCountry = ShippingCountry::find($this->shipping_country_id);
@ -143,13 +155,7 @@ class Yard extends Cart
}
public function getYContent()
{
if (is_null($this->ysession->get($this->yinstance))) {
return new Collection([]);
}
return $this->ysession->get($this->yinstance);
}
public function reCalculateShippingPrice(){
@ -168,22 +174,23 @@ class Yard extends Cart
}
public function setUserPriceInfos($setUserPriceInfos = [])
public function setUserPriceInfos($user_price_infos = [])
{
$this->shipping_free = isset($setUserPriceInfos['shipping_free']) ? $setUserPriceInfos['shipping_free'] : false;
$this->shipping_free = isset($user_price_infos['shipping_free']) ? $user_price_infos['shipping_free'] : false;
$this->putYardExtra('shipping_free', $this->shipping_free);
$this->user_tax_free = $setUserPriceInfos['user_tax_free'];
$this->user_tax_free = $user_price_infos['user_tax_free'];
$this->putYardExtra('user_tax_free', $this->user_tax_free);
$this->user_reverse_charge = $setUserPriceInfos['user_reverse_charge'];
$this->user_reverse_charge = $user_price_infos['user_reverse_charge'];
$this->putYardExtra('user_reverse_charge', $this->user_reverse_charge);
$this->user_country_id = $setUserPriceInfos['user_country_id'];
$this->user_country_id = $user_price_infos['user_country_id'];
$this->putYardExtra('user_country_id', $this->user_country_id);
$this->user_country = Country::findOrFail($setUserPriceInfos['user_country_id']);
$this->user_country = Country::findOrFail($user_price_infos['user_country_id']);
$this->putYardExtra('user_country', $this->user_country);
}
public function getUserPriceInfos(){
@ -476,9 +483,13 @@ class Yard extends Cart
}
$price = $product->price;
if($set_price === 'with'){
$cartItem = $this->getCartItem($product->id, $product->getLang('name'), 1, $price, ['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'show_on' => $product->show_on]);
$price = $product->getPriceWith(false, true, $this->getUserCountry());
}
$cartItem = $this->getCartItem($product->id, $product->getLang('name'), 1, $price, ['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points]);
if($set_price === 'withTaxFree'){
$cartItem = $this->getCartItem($product->id, $product->getLang('name'), 1, $price, ['image' => $image, 'slug' => $product->slug, 'weight' => $product->weight, 'points' => $product->points, 'no_commission' => $product->no_commission, 'no_free_shipping' => $product->no_free_shipping, 'show_on' => $product->show_on]);
$price = $product->getPriceWith($this->getUserTaxFree(), false, $this->getUserCountry());
}
$content = $this->getContent();
if ($content->has($cartItem->rowId)){
@ -504,7 +515,7 @@ class Yard extends Cart
public function destroy()
{
$this->ysession->remove($this->yinstance);
// $this->ysession->remove($this->yinstance);
parent::destroy();
}
@ -662,7 +673,7 @@ class Yard extends Cart
'cartInstance' => $this->currentInstance(),
], $eventOptions);
$this->events->dispatch('cart.stored', $eventOptions);
// $this->events->dispatch('cart.stored', $eventOptions);
}
/**

View file

@ -1,12 +0,0 @@
1.0.0.0,1.0.0.254,AU,Australia,OC,Oceania
1.0.0.255,1.0.0.255,ID,Indonesia,AS,Asia
1.0.1.0,1.0.3.255,CN,China,AS,Asia
1.0.4.0,1.0.7.255,AU,Australia,OC,Oceania
1.0.8.0,1.0.15.255,CN,China,AS,Asia
1.0.16.0,1.0.31.255,JP,Japan,AS,Asia
1.0.32.0,1.0.63.255,CN,China,AS,Asia
1.0.64.0,1.0.127.255,JP,Japan,AS,Asia
1.0.128.0,1.0.218.40,TH,Thailand,AS,Asia
1.0.218.41,1.0.218.41,MY,Malaysia,AS,Asia
1.0.218.42,1.0.255.255,TH,Thailand,AS,Asia
1.1.0.0,1.1.0.255,CN,China,AS,Asia
1 1.0.0.0 1.0.0.254 AU Australia OC Oceania
2 1.0.0.255 1.0.0.255 ID Indonesia AS Asia
3 1.0.1.0 1.0.3.255 CN China AS Asia
4 1.0.4.0 1.0.7.255 AU Australia OC Oceania
5 1.0.8.0 1.0.15.255 CN China AS Asia
6 1.0.16.0 1.0.31.255 JP Japan AS Asia
7 1.0.32.0 1.0.63.255 CN China AS Asia
8 1.0.64.0 1.0.127.255 JP Japan AS Asia
9 1.0.128.0 1.0.218.40 TH Thailand AS Asia
10 1.0.218.41 1.0.218.41 MY Malaysia AS Asia
11 1.0.218.42 1.0.255.255 TH Thailand AS Asia
12 1.1.0.0 1.1.0.255 CN China AS Asia

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff