mivita/resources/views/user/recognitions/index.blade.php
Kevin Adametz 7bc551ca1f Anerkennungen: firmenweite Monats-Ranglisten im Salescenter
- Neue Seite Anerkennungen (/user/recognitions, Menue unter News Archiv) fuer
  alle Berater mit aktivem Konto: oben die Anerkennungen des Vormonats in den
  fuenf Kategorien der Monatsmail, Filter zurueck bis Januar 2026; darunter
  Live-Ranglisten (Top 10 je Kategorie) des laufenden Monats, 15 Min. Cache.
- Kategorien: Top-Recruiter (ab 2 Neupartnern), neue Kunden-Abos,
  Kundenabo-Punkte, Kunden-Abos gesamt, Gesamtpunkte (1.000-Punkte-Shop,
  mit Platz 4). Regeln wie TopResultsService/VIP-Statistik, nur firmenweit.
- Firmenkonto (6) und Grosskundin (3) nie aufgefuehrt:
  config mivita.recognition_excluded_user_ids.
- Abgeschlossene Monate sind bis zum 3. des Folgemonats vorlaeufig und werden
  dann eingefroren: neue Tabelle recognition_snapshots, Command
  recognitions:freeze (taeglich 06:15, idempotent, holt offene Monate nach).
- September-Anerkennungen firmenweit geprueft und korrigiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:13:22 +00:00

324 lines
15 KiB
PHP

@extends('layouts.layout-2')
@section('styles')
<style>
.recognition-hero {
position: relative;
overflow: hidden;
border-radius: .75rem;
background: linear-gradient(135deg, #6b7758 0%, #4f5940 100%);
color: #fff;
}
.recognition-hero .hero-trophy {
position: absolute;
right: -1rem;
bottom: -2.5rem;
font-size: 13rem;
line-height: 1;
color: rgba(255, 255, 255, .08);
pointer-events: none;
}
.recognition-hero .hero-kicker {
letter-spacing: .08em;
color: #f4d77a;
}
.recognition-hero .badge-status {
background: rgba(255, 255, 255, .18);
color: #fff;
font-weight: 500;
}
.recognition-card .card-header {
border-bottom: 0;
padding-bottom: .25rem;
}
.recognition-card .category-icon {
font-size: 1.6rem;
line-height: 1;
}
.recognition-place {
display: flex;
align-items: center;
border-radius: .5rem;
padding: .6rem .75rem;
margin-bottom: .5rem;
background: #f7f7f5;
}
.recognition-place.place-1 {
background: #fff7dc;
box-shadow: inset 3px 0 0 #e0b537;
}
.recognition-place.place-2 {
background: #f1f3f5;
box-shadow: inset 3px 0 0 #a9b0b8;
}
.recognition-place.place-3 {
background: #fbefe6;
box-shadow: inset 3px 0 0 #c98a5a;
}
.recognition-place .place-medal {
flex: 0 0 2.25rem;
font-size: 1.6rem;
line-height: 1;
text-align: center;
}
.recognition-place .place-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.8rem;
height: 1.8rem;
border-radius: 50%;
background: #dfe2d8;
color: #4f5940;
font-weight: 700;
font-size: .85rem;
}
.recognition-place .place-members {
flex: 1 1 auto;
min-width: 0;
}
.recognition-place .place-member {
display: flex;
align-items: center;
}
.recognition-place .place-member + .place-member {
margin-top: .35rem;
}
.recognition-place .place-value {
flex: 0 0 auto;
text-align: right;
padding-left: .75rem;
}
.recognition-live .live-dot {
display: inline-block;
width: .6rem;
height: .6rem;
border-radius: 50%;
background: #d9534f;
margin-right: .4rem;
animation: recognition-pulse 1.6s infinite;
}
@keyframes recognition-pulse {
0% { box-shadow: 0 0 0 0 rgba(217, 83, 79, .6); }
70% { box-shadow: 0 0 0 .5rem rgba(217, 83, 79, 0); }
100% { box-shadow: 0 0 0 0 rgba(217, 83, 79, 0); }
}
.recognition-live .nav-pills .nav-link {
font-size: .85rem;
}
.recognition-live .nav-pills .nav-link.active,
.recognition-live .nav-pills .nav-link.active:hover,
.recognition-live .nav-pills .nav-link.active:focus {
background-color: #d7d700;
color: #565600;
}
.recognition-live .table td {
vertical-align: middle;
}
</style>
@endsection
@section('content')
@php
$viewerId = (int) auth()->id();
$medals = [1 => '🥇', 2 => '🥈', 3 => '🥉'];
$categoryIcons = [
'recruiters' => '🏆',
'new_customer_abos' => '🌟',
'customer_abo_points' => '💙',
'customer_abos_total' => '⭐',
'total_points' => '🚀',
];
$formatValue = fn ($value) => number_format((float) $value, floor((float) $value) == (float) $value ? 0 : 1, ',', '.');
$periodOptions = collect($periods)->mapWithKeys(fn (array $period) => [
sprintf('%d-%02d', $period['year'], $period['month']) => HTMLHelper::getMonth($period['month']).' '.$period['year'],
])->all();
$selectedPeriod = $selected ? sprintf('%d-%02d', $selected['year'], $selected['month']) : null;
@endphp
<h4 class="font-weight-bold py-2 mb-2 d-flex flex-wrap justify-content-between align-items-center w-100">
<div class="mb-2 mb-sm-0">
<i class="ion ion-md-trophy text-warning mr-1"></i>{{ __('recognitions.title') }}
</div>
@if ($periodOptions !== [])
<form method="GET" action="{{ route('user_recognitions') }}" class="form-inline">
<label for="period" class="small text-muted font-weight-normal mr-2">{{ __('recognitions.period_label') }}</label>
{{ html()->select('period', $periodOptions, $selectedPeriod)->id('period')->class('custom-select custom-select-sm mr-2')->attribute('onchange', 'this.form.submit()') }}
<noscript><button type="submit" class="btn btn-sm btn-primary">{{ __('recognitions.show') }}</button></noscript>
</form>
@endif
</h4>
@if ($recognitions === null)
<div class="alert alert-info">{{ __('recognitions.no_months') }}</div>
@else
<div class="recognition-hero p-4 p-md-5 mb-4">
<i class="ion ion-md-trophy hero-trophy"></i>
<div class="position-relative">
<div class="hero-kicker text-uppercase small font-weight-bold mb-2">{{ __('recognitions.hero_kicker') }}</div>
<h2 class="font-weight-bold text-white mb-2">
{{ __('recognitions.hero_title', ['month' => HTMLHelper::getMonth($recognitions['month']).' '.$recognitions['year']]) }}
🥳💙
</h2>
<p class="mb-3" style="max-width: 46rem; opacity: .9;">{{ __('recognitions.hero_text') }}</p>
@if ($recognitions['is_final'])
<span class="badge badge-status px-2 py-1">
<i class="ion ion-md-lock mr-1"></i>{{ __('recognitions.status_final', ['date' => $recognitions['frozen_at']?->format('d.m.Y')]) }}
</span>
@else
<span class="badge badge-status px-2 py-1">
<i class="ion ion-md-time mr-1"></i>{{ __('recognitions.status_preliminary', ['date' => $recognitions['freeze_date']->format('d.m.Y')]) }}
</span>
@endif
</div>
</div>
<div class="row">
@foreach (\App\Services\Team\RecognitionService::CATEGORIES as $category)
@php $podium = $recognitions['categories'][$category] ?? []; @endphp
<div class="col-md-6 col-xl-4 d-flex align-items-stretch mb-4">
<div class="card recognition-card w-100">
<div class="card-header bg-transparent d-flex align-items-start">
<span class="category-icon mr-2">{{ $categoryIcons[$category] }}</span>
<div>
<h6 class="font-weight-bold mb-1">{{ __('recognitions.categories.'.$category.'.title') }}</h6>
<div class="text-muted small">{{ __('recognitions.categories.'.$category.'.description') }}</div>
</div>
</div>
<div class="card-body pt-2">
@forelse ($podium as $place)
<div class="recognition-place place-{{ $place['place'] }}">
<div class="place-medal">
@if (isset($medals[$place['place']]))
<span title="{{ __('recognitions.place', ['place' => $place['place']]) }}">{{ $medals[$place['place']] }}</span>
@else
<span class="place-number" title="{{ __('recognitions.place', ['place' => $place['place']]) }}">{{ $place['place'] }}</span>
@endif
</div>
<div class="place-members ml-2">
@foreach ($place['members'] as $member)
<div class="place-member">
@include('components._avatar', [
'imageUrl' => $member['profile_image_url'] ?? null,
'firstName' => $member['first_name'],
'lastName' => $member['last_name'],
'size' => 36,
])
<span class="ml-2 font-weight-semibold text-truncate">
{{ $member['first_name'] }} {{ $member['last_name'] }}
@if ((int) $member['user_id'] === $viewerId)
<span class="badge badge-pill badge-info ml-1">{{ __('recognitions.self') }}</span>
@endif
</span>
</div>
@endforeach
</div>
<div class="place-value">
<div class="font-weight-bold" style="font-size: 1.1rem;">{{ $formatValue($place['value']) }}</div>
<div class="text-muted small">{{ __('recognitions.categories.'.$category.'.unit') }}</div>
</div>
</div>
@empty
<div class="text-muted small">{{ __('recognitions.empty') }}</div>
@endforelse
@if ($category === 'recruiters')
<div class="text-muted small mt-2">{{ __('recognitions.recruiters_min_hint') }}</div>
@endif
</div>
</div>
</div>
@endforeach
</div>
@endif
<div class="card recognition-live mb-4">
<div class="card-header">
<h5 class="mb-1 d-flex align-items-center">
<span class="live-dot"></span>
{{ __('recognitions.live_title', ['month' => HTMLHelper::getMonth($live['month']).' '.$live['year']]) }}
</h5>
<div class="text-muted small">
{{ __('recognitions.live_text', ['limit' => \App\Services\Team\RecognitionService::LIVE_LIMIT]) }}
· {{ __('recognitions.live_updated', ['time' => $live['calculated_at']->format('d.m.Y H:i')]) }}
</div>
</div>
<div class="card-body">
<ul class="nav nav-pills flex-wrap mb-3" role="tablist">
@foreach (\App\Services\Team\RecognitionService::CATEGORIES as $category)
<li class="nav-item mb-1">
<a class="nav-link {{ $loop->first ? 'active' : '' }}" data-toggle="tab" href="#live-{{ $category }}" role="tab">
{{ $categoryIcons[$category] }} {{ __('recognitions.categories.'.$category.'.title') }}
</a>
</li>
@endforeach
</ul>
<div class="tab-content">
@foreach (\App\Services\Team\RecognitionService::CATEGORIES as $category)
<div class="tab-pane fade {{ $loop->first ? 'show active' : '' }}" id="live-{{ $category }}" role="tabpanel">
@if (empty($live['categories'][$category]))
<div class="text-muted small">{{ __('recognitions.live_empty') }}</div>
@else
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th style="width: 5rem;">{{ __('recognitions.live_rank') }}</th>
<th>{{ __('recognitions.live_name') }}</th>
<th class="text-right">{{ __('recognitions.categories.'.$category.'.unit') }}</th>
</tr>
</thead>
<tbody>
@foreach ($live['categories'][$category] as $row)
<tr class="{{ (int) $row['user_id'] === $viewerId ? 'table-info' : '' }}">
<td class="font-weight-bold">
{{ $medals[$row['rank']] ?? $row['rank'].'.' }}
</td>
<td>
<div class="d-flex align-items-center">
@include('components._avatar', [
'imageUrl' => $row['profile_image_url'] ?? null,
'firstName' => $row['first_name'],
'lastName' => $row['last_name'],
'size' => 32,
])
<span class="ml-2">
{{ $row['first_name'] }} {{ $row['last_name'] }}
@if ((int) $row['user_id'] === $viewerId)
<span class="badge badge-pill badge-info ml-1">{{ __('recognitions.self') }}</span>
@endif
</span>
</div>
</td>
<td class="text-right font-weight-bold">{{ $formatValue($row['value']) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
@endforeach
</div>
</div>
</div>
@endsection