*/ private array $aboBadges = []; 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 '
Keine Business-User gefunden.
'; } $this->loadAboBadges($this->collectUserIds($businessUsers)); $html = '
    '; foreach (self::sortByActivation($businessUsers) as $businessUser) { $html .= $this->renderUserItem($businessUser, 0); } $html .= '
'; return $html; } /** * Sortiert Business-User-Items nach Partner-Aktivierung (active_date, dann user_id). * * Die Loader liefern die Downline in users.id-Reihenfolge (= Konto-Registrierung); * gespeicherte Monats-Snapshots konservieren diese Reihenfolge zusaetzlich. * Bestandskunden, die erst spaeter zum Berater upgraden, stuenden damit mitten in * der Liste statt am Ende - deshalb wird hier zentral beim Rendern sortiert. * * Achtung: kein `$item->active_date ?? null` verwenden - BusinessUserItem(Optimized) * hat __get ohne __isset, damit liefert `??` immer null. * * @param array $items * @return array */ public static function sortByActivation(array $items): array { usort($items, function ($a, $b): int { $aDate = $a->active_date; $bDate = $b->active_date; $aKey = $aDate ? strtotime((string) $aDate) : PHP_INT_MAX; $bKey = $bDate ? strtotime((string) $bDate) : PHP_INT_MAX; return $aKey <=> $bKey ?: ((int) $a->user_id <=> (int) $b->user_id); }); return $items; } /** * Rendert parentlose User als HTML */ public function renderParentless(array $parentless): string { if (empty($parentless)) { return '
Keine parentlosen User gefunden.
'; } $this->loadAboBadges($this->collectUserIds($parentless)); $html = ''; foreach ($parentless as $item) { $html .= $this->renderParentlessItem($item); } return $html; } /** * Rendert Sponsor-Information als HTML */ public function renderSponsor($sponsor): string { if (! $sponsor) { return '
'.__('team.no_sponsor_assigned').'
'; } return '
  • '. '
    '. $this->renderUserInfo($sponsor, false, true). '
    '. '
  • '; } /** * Rendert User Team Tree (für UserTeamCalcBot) */ public function renderUserTeamTree(array $teamMembers): string { if (empty($teamMembers)) { return '
    Keine Team-Mitglieder gefunden.
    '; } $html = '
      '; foreach ($teamMembers as $member) { $html .= $this->renderTeamMemberItem($member, 0); } $html .= '
    '; return $html; } /** * Rendert User Sponsor (für UserTeamCalcBot) */ public function renderUserSponsor(User $sponsor): string { if (! $sponsor || ! $sponsor->account) { return '
    Kein Sponsor gefunden.
    '; } $html = '
    '; $html .= '
    '; $html .= '
    '; // Sponsor Info $html .= '
    '; $html .= ''.e($sponsor->account->first_name.' '.$sponsor->account->last_name).'
    '; $html .= ''.e($sponsor->email).''; $html .= '
    '; // Account Info $html .= '
    '; $html .= ''.e($sponsor->account->m_account ?? '').''; $html .= '
    '; // Level Info $html .= '
    '; if ($sponsor->user_level) { $html .= ''.e($sponsor->user_level->getLang('name')).''; } $html .= '
    '; // Status $html .= '
    '; $html .= get_active_badge($sponsor->isActiveAccount()); $html .= '
    '; $html .= '
    '; $html .= '
    '; $html .= '
    '; return $html; } /** * Rendert einzelnes Team-Mitglied */ private function renderTeamMemberItem($member, int $depth): string { $html = '
  • '; $html .= '
    '; $html .= '
    '; // Einrückung basierend auf Tiefe $indent = str_repeat('    ', $depth); // Name und Email $html .= '
    '; $html .= $indent; $html .= ''.e(($member->first_name ?? '').' '.($member->last_name ?? '')).'
    '; $html .= $indent.''.e($member->email ?? '').''; $html .= '
    '; // Account ID $html .= '
    '; $html .= ''.e($member->m_account ?? '').''; $html .= '
    '; // Level $html .= '
    '; if (! empty($member->user_level_name)) { $html .= ''.e($member->user_level_name).''; if ($member->next_qual_user_level) { $html .= ''; } } $html .= '
    '; // Qualifikation $html .= '
    '; if (! empty($member->qual_kp)) { $pointsSum = (float) ($member->sales_volume_points_KP_sum ?? 0); $qualKP = (float) $member->qual_kp; $isQual = $pointsSum >= $qualKP; $badgeClass = $isQual ? 'badge-success' : 'badge-warning'; $html .= 'KU '.$qualKP.''; } $html .= '
    '; // Status $html .= '
    '; $html .= get_active_badge($member->active_account ?? 0); $html .= '
    '; $html .= '
    '; $html .= '
    '; // Kinder rendern if (! empty($member->businessUserItems) && is_array($member->businessUserItems)) { $html .= '
      '; foreach ($member->businessUserItems as $child) { $html .= $this->renderTeamMemberItem($child, $depth + 1); } $html .= '
    '; } $html .= '
  • '; 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 = '
      '; foreach (self::sortByActivation($item->businessUserItems) as $child) { $childrenHtml .= $this->renderUserItem($child, $deep + 1); } $childrenHtml .= '
    '; } return '
  • '. '
    '. $this->renderUserCardWithDepth($item, $deep). '
    '. $childrenHtml. '
  • '; } /** * Rendert parentlosen User-Item */ private function renderParentlessItem($item): string { return '
  • '. '
    '. $this->renderUserInfo($item, true, false). '
    '. '
  • '; } /** * Rendert User-Card mit Tiefe-Anzeige */ private function renderUserCardWithDepth($item, int $deep): string { $depthBadge = ''; if ($deep > 0) { $depthBadge = '
    '. '
    '.$deep.'
    '. '
    '; } return '
    '. $depthBadge. '
    '. $this->renderUserInfo($item, false, false). '
    '. '
    '; } /** * 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 = ''; // User Link $html .= ''. ' '. ''.e($item->first_name.' '.$item->last_name).''. ''; // Email $html .= ' '.e($item->email).''; // Optional: Geburtstag $birthday = $item->user_birthday; // Magic Method __get() verwenden if ($birthday && trim($birthday) !== '') { $html .= ' | '.e($birthday); } // Optional: Telefon $phone = $item->user_phone; // Magic Method __get() verwenden if ($phone && trim($phone) !== '') { $html .= ' | '.e($phone); } // Level Badge $levelName = $item->user_level_name ? TranslationHelper::transUserLevelName($item->user_level_name) : ''; $account = $item->m_account ?: ''; $html .= ' '.e($levelName.' | '.$account).''; // Karriere-Aufstiegs-Icon für qualifizierte User (nur in Struktur-Ansicht)# if ($item->next_qual_user_level) { $html .= ''; } // Abo-Kennzeichen (eigenes Abo + eigene Kundenabos) $html .= $this->renderAboBadges((int) $item->user_id); // Details für aktive Accounts if ($item->active_account) { $html .= '
    '; 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 .= ''; } else { // Inaktive Accounts $paymentDate = $item->payment_account_date ?: ''; $html .= '
    '.__('team.account_to').': '.e($paymentDate).''; } // Sponsor für parentlose User if ($showSponsor && $item->m_sponsor_name) { $html .= '
    '.e($item->m_sponsor_name); } $html .= '
    '; 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 ''.__('team.total_points').': '.formatNumber($totalPoints).' | '. __('team.e').': '.formatNumber($ePoints).' | '. __('team.s').': '.formatNumber($sPoints).' | '. __('team.net_turnover').': '.formatNumber($totalSum).' € | '. __('team.e').': '.formatNumber($eSum).' € | '. __('team.s').': '.formatNumber($sSum).' €'; } /** * Rendert Action-Button für User-Details */ private function renderActionButton(int $userId): string { return ' | '; } /** * 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'; } } /** * Lädt die Abo-Kennzeichen für alle übergebenen User-IDs gebatcht * (zwei Group-by-Queries statt Einzelabfragen pro Knoten). * * Zählweise konsistent zur Backoffice-Statistik: aktive Abos = active=true * und status NOT IN (4,5,6) via {@see UserAbo::scopeActiveAbo()}. * * @param array $userIds */ public function loadAboBadges(array $userIds): void { $userIds = array_values(array_unique(array_filter($userIds))); if (empty($userIds)) { return; } $ownAboUserIds = UserAbo::query() ->activeAbo() ->where('is_for', 'me') ->whereIn('user_id', $userIds) ->distinct() ->pluck('user_id'); foreach ($ownAboUserIds as $userId) { $this->aboBadges[(int) $userId]['own'] = true; } $customerAboCounts = UserAbo::query() ->activeAbo() ->where('is_for', 'ot') ->whereIn('member_id', $userIds) ->groupBy('member_id') ->selectRaw('member_id, COUNT(*) as abo_count') ->pluck('abo_count', 'member_id'); foreach ($customerAboCounts as $userId => $count) { $this->aboBadges[(int) $userId]['customer_count'] = (int) $count; } } /** * Rendert die Abo-Kennzeichen für einen User: * grünes "A"-Badge bei eigenem aktiven Abo, rotes "(n)"-Badge bei n > 0 eigenen aktiven Kundenabos. */ public function renderAboBadges(int $userId): string { $hasOwnAbo = $this->aboBadges[$userId]['own'] ?? false; $customerAboCount = $this->aboBadges[$userId]['customer_count'] ?? 0; $html = ''; if ($hasOwnAbo) { $html .= ' A'; } if ($customerAboCount > 0) { $html .= ' ('.$customerAboCount.')'; } return $html; } /** * Sammelt rekursiv alle User-IDs aus den Business-User-Items. * * @return array */ private function collectUserIds(array $items): array { $userIds = []; foreach ($items as $item) { $userId = (int) $item->user_id; if ($userId > 0) { $userIds[] = $userId; } if (isset($item->businessUserItems) && is_array($item->businessUserItems) && count($item->businessUserItems) > 0) { $userIds = array_merge($userIds, $this->collectUserIds($item->businessUserItems)); } } return $userIds; } /** * Setzt den Kontext für die Darstellung */ public function setInitFrom(string $initFrom): self { $this->initFrom = $initFrom; return $this; } }