- 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>
324 lines
15 KiB
PHP
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
|