Statistik-Änderungen 2026-08-19 (dev/2026-07-22/entwicklungsplan-2026-08-19.md, alle 8 Pakete gemäß beantworteten F-Fragen): - N1 Monatsstatistik: Team-Cluster (Firstlines klickbar -> neue Seite user_team_firstlines, Teampartner gesamt/Payline [nur aktive], Abos Payline mit +N) + Level-Kachel (Quali-Stufe, fehlende Payline-/persönliche Punkte, Karriere-Level); LiveCommissionService-Snapshot v3 reicht Level-/Quali-Daten durch - N2 Abo-Kacheln auf Home monatsbezogen (aboStockCount-Rückrechnung über cancel/deleted/Status-Historie; VIP-Statistik bleibt tagesaktuell) - N3 VIP-Statistik 'Recruiter des Monats' (Neupartner-Definition wie KPI, rekursives Ketten-Volumen als Gesamtpunkte inkl. Starterpaket) + Drilldown/CSV-Spalte 'Punkte gesamt' - N4 Prognose-Block 'Level & Provision laut Prognose (Stand 25.)' mit unverbindlich-Kennzeichnung (ebenengenaue offene Abo-Punkte + TP-Laufstände) - N5 Checkout-Herkunft: Dropdown raus, Pflicht-Freitext (lokalisierte Fehlermeldung) - N6 Berater-Profilfoto (user_accounts.profile_image, Einwilligung als Sprachkey-Platzhalter, DSGVO-Löschung) + teaminterne Top-3-Karte (TopResultsService, Scope-Schalter) - N7 USt-Optionen: 'nicht umsatzsteuerpflichtig' zuerst + '§ 19 UStG'-Textfix - N8 Jahresgebühr-Nachfassliste je Berater (AnnualFeeDueService, überfällig/1-3 Monate, Sidenav) Kontroll-Review (8 Perspektiven) angewendet: next-Level-Fallback im Steady-State (resolveNextLevel — sonst 'Höchstes Level erreicht' für Normalfälle), _calculated-Punktebasis bei mehr Paylines, keine Wiederbelebung deaktivierter Abos in der Monatsrückrechnung, Payline-Abo-Kachel monatskonsistent, Top-3-Nachrücker; Performance: BackofficeDashboardService als Singleton mit lineBuckets-Memo (+flushLineBucketsMemo), level_forecast 6h-Cache; Reuse: User-Scopes firstlinesOf/newPartnerIn, SalesVolumePoints::totalPointsByUser, Slim::storeCroppedImage (3 Upload-Kopien vereinigt), activeAbo-Scope statt Inline-Prädikate, Form::open-Konvention, Return-Types Repair-Härtung (Kundenentscheide): VP-Nachtrepair dedupliziert über shopping_order_id (Live-Pfad schreibt sie jetzt mit), zählende Alt-Zeilen-Erkennung, Vormonats-Scan für Monatswechsel, Firmenaccount-Buchungen (config mivita.company_user_id) werden automatisch auf den Berater umgebucht (in place bzw. per Gegenbuchung bei geschlossenem Monat; Echtfall Anna Schall Order 58797 -> Christina Schuder), Berater-zu-Berater nur Warnung; Incentive-Repair: m_sponsor=NULL löscht nicht mehr, FK-Repair vor Stale-Bereinigung, gebatchte Lookups, zentrales Incentive::qualificationWindow(); .env aus dem Git-Tracking entfernt (Pflege per SFTP) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
407 lines
15 KiB
PHP
407 lines
15 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
/**
|
|
* App\Models\UserAccount
|
|
*
|
|
* @property-read Country $company_country
|
|
* @property-read Country $company_pre_phone
|
|
* @property-read Country $country
|
|
* @property mixed $birthday
|
|
* @property-read mixed $company
|
|
* @property-read Country $pre_mobil
|
|
* @property-read Country $pre_phone
|
|
* @property-read User $user
|
|
*
|
|
* @method static bool|null forceDelete()
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount newModelQuery()
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount newQuery()
|
|
* @method static \Illuminate\Database\Query\Builder|\App\Models\UserAccount onlyTrashed()
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount query()
|
|
* @method static bool|null restore()
|
|
* @method static \Illuminate\Database\Query\Builder|\App\Models\UserAccount withTrashed()
|
|
* @method static \Illuminate\Database\Query\Builder|\App\Models\UserAccount withoutTrashed()
|
|
*
|
|
* @property int $id
|
|
* @property string|null $salutation
|
|
* @property string|null $first_name
|
|
* @property string|null $last_name
|
|
* @property string|null $address
|
|
* @property string|null $address_2
|
|
* @property string|null $zipcode
|
|
* @property string|null $city
|
|
* @property int $country_id
|
|
* @property int|null $pre_phone_id
|
|
* @property string|null $phone
|
|
* @property int|null $pre_mobil_id
|
|
* @property string|null $mobil
|
|
* @property string|null $tax_number
|
|
* @property string|null $tax_identification_number
|
|
* @property int $same_as_billing
|
|
* @property string|null $shipping_salutation
|
|
* @property string|null $shipping_company
|
|
* @property string|null $shipping_firstname
|
|
* @property string|null $shipping_lastname
|
|
* @property string|null $shipping_address
|
|
* @property string|null $shipping_address_2
|
|
* @property string|null $shipping_zipcode
|
|
* @property string|null $shipping_city
|
|
* @property int $shipping_country_id
|
|
* @property int|null $shipping_pre_phone_id
|
|
* @property string|null $shipping_phone
|
|
* @property string|null $website
|
|
* @property string|null $facebook
|
|
* @property string|null $facebook_fanpage
|
|
* @property string|null $instagram
|
|
* @property string|null $data_protection
|
|
* @property \Illuminate\Support\Carbon|null $created_at
|
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
|
* @property \Illuminate\Support\Carbon|null $deleted_at
|
|
* @property-read Country $shipping_country
|
|
* @property-read Country|null $shipping_pre_phone
|
|
*
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereAddress($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereAddress2($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereBirthday($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereCity($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereCompany($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereCountryId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereCreatedAt($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereDataProtection($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereDeletedAt($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereFacebook($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereFacebookFanpage($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereFirstName($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereInstagram($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereLastName($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereMobil($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount wherePhone($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount wherePreMobilId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount wherePrePhoneId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereSalutation($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereSameAsBilling($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereShippingAddress($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereShippingAddress2($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereShippingCity($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereShippingCompany($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereShippingCountryId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereShippingFirstname($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereShippingLastname($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereShippingPhone($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereShippingPrePhoneId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereShippingSalutation($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereShippingZipcode($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereTaxIdentificationNumber($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereTaxNumber($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereUpdatedAt($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereWebsite($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereZipcode($value)
|
|
*
|
|
* @property string|null $m_account
|
|
* @property string|null $m_salutation
|
|
* @property string|null $m_first_name
|
|
* @property string|null $m_last_name
|
|
* @property string|null $m_notes
|
|
* @property int|null $taxable_sales
|
|
*
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereMAccount($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereMFirstName($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereMLastName($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereMNotes($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereMSalutation($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereTaxableSales($value)
|
|
*
|
|
* @property array|null $payment_data
|
|
*
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount wherePaymentData($value)
|
|
*
|
|
* @property string|null $accepted_contract
|
|
* @property array|null $notice
|
|
*
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereAcceptedContract($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\UserAccount whereNotice($value)
|
|
*
|
|
* @property int $reverse_charge
|
|
* @property string|null $reverse_charge_code
|
|
* @property string|null $reverse_charge_valid
|
|
*
|
|
* @method static \Illuminate\Database\Eloquent\Builder|UserAccount whereReverseCharge($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|UserAccount whereReverseChargeCode($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|UserAccount whereReverseChargeValid($value)
|
|
*
|
|
* @property string|null $bank_owner
|
|
* @property string|null $bank_iban
|
|
* @property string|null $bank_bic
|
|
*
|
|
* @method static \Illuminate\Database\Eloquent\Builder|UserAccount whereBankBic($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|UserAccount whereBankIban($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|UserAccount whereBankOwner($value)
|
|
*
|
|
* @mixin \Eloquent
|
|
*/
|
|
class UserAccount extends Model
|
|
{
|
|
protected $table = 'user_accounts';
|
|
|
|
protected $fillable = [
|
|
'm_account',
|
|
'm_salutation',
|
|
'm_first_name',
|
|
'm_last_name',
|
|
'm_notes',
|
|
'company',
|
|
'salutation',
|
|
'first_name',
|
|
'last_name',
|
|
'address',
|
|
'address_2',
|
|
'zipcode',
|
|
'city',
|
|
'country_id',
|
|
'pre_phone_id',
|
|
'phone',
|
|
'pre_mobil_id',
|
|
'mobil',
|
|
'tax_number',
|
|
'tax_identification_number',
|
|
'taxable_sales',
|
|
'same_as_billing',
|
|
'shipping_salutation',
|
|
'shipping_company',
|
|
'shipping_firstname',
|
|
'shipping_lastname',
|
|
'shipping_address',
|
|
'shipping_address_2',
|
|
'shipping_zipcode',
|
|
'shipping_city',
|
|
'shipping_country_id',
|
|
'shipping_pre_phone_id',
|
|
'shipping_phone',
|
|
'shipping_postnumber',
|
|
'birthday',
|
|
'website',
|
|
'facebook',
|
|
'facebook_fanpage',
|
|
'instagram',
|
|
'bank_owner',
|
|
'bank_iban',
|
|
'bank_bic',
|
|
'notice',
|
|
'language',
|
|
'profile_image',
|
|
];
|
|
// 'reverse_charge', 'reverse_charge_valid'
|
|
|
|
protected $casts = [
|
|
'payment_data' => 'array',
|
|
'notice' => 'array',
|
|
// 'reverse_charge' => 'bool'
|
|
];
|
|
|
|
use SoftDeletes;
|
|
|
|
protected $dates = ['deleted_at'];
|
|
|
|
public function user()
|
|
{
|
|
return $this->hasOne('App\User', 'account_id');
|
|
}
|
|
|
|
public function country()
|
|
{
|
|
return $this->belongsTo('App\Models\Country', 'country_id');
|
|
}
|
|
|
|
public function shipping_country()
|
|
{
|
|
return $this->belongsTo('App\Models\Country', 'shipping_country_id');
|
|
}
|
|
|
|
public function pre_phone()
|
|
{
|
|
return $this->belongsTo('App\Models\Country', 'pre_phone_id');
|
|
}
|
|
|
|
public function pre_mobil()
|
|
{
|
|
return $this->belongsTo('App\Models\Country', 'pre_mobil_id');
|
|
}
|
|
|
|
public function shipping_pre_phone()
|
|
{
|
|
return $this->belongsTo('App\Models\Country', 'shipping_pre_phone_id');
|
|
}
|
|
|
|
public function getBirthdayAttribute($value)
|
|
{
|
|
if (! $value) {
|
|
return '';
|
|
}
|
|
|
|
return Carbon::parse($value)->format(\Util::formatDateDB());
|
|
}
|
|
|
|
public function setBirthdayAttribute($value)
|
|
{
|
|
$this->attributes['birthday'] = isset($value) ? (new Carbon($value))->format('Y-m-d') : null;
|
|
}
|
|
|
|
public function getDataProtectionFormat()
|
|
{
|
|
if (! $this->attributes['data_protection']) {
|
|
return '';
|
|
}
|
|
|
|
return Carbon::parse($this->attributes['data_protection'])->format(\Util::formatDateTimeDB());
|
|
}
|
|
|
|
public function getAcceptContractFormat()
|
|
{
|
|
if (! $this->attributes['accept_contract']) {
|
|
return '';
|
|
}
|
|
|
|
return Carbon::parse($this->attributes['accept_contract'])->format(\Util::formatDateTimeDB());
|
|
}
|
|
|
|
public function getReverseChargeValidFormat()
|
|
{
|
|
if (! $this->attributes['reverse_charge_valid']) {
|
|
return '';
|
|
}
|
|
|
|
return Carbon::parse($this->attributes['reverse_charge_valid'])->format(\Util::formatDateTimeDB());
|
|
}
|
|
|
|
public function getCountryAttrAs($attr, $as = false)
|
|
{
|
|
if ($this->country) {
|
|
$val = $this->country->getAttrByKey($attr);
|
|
|
|
if ($val) {
|
|
if ($as) {
|
|
return $as;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
public function setNotice($key, $value)
|
|
{
|
|
$notice = $this->notice;
|
|
$notice[$key] = $value;
|
|
$this->notice = $notice;
|
|
$this->save();
|
|
}
|
|
|
|
public function getNotice($key)
|
|
{
|
|
return isset($this->notice[$key]) ? $this->notice[$key] : false;
|
|
}
|
|
|
|
public function getPhoneNumber()
|
|
{
|
|
if ($this->mobil && $this->mobil !== '') {
|
|
return ($this->pre_mobil ? $this->pre_mobil->phone : '').' '.$this->mobil;
|
|
}
|
|
if ($this->phone && $this->phone !== '') {
|
|
return ($this->pre_phone ? $this->pre_phone->phone : '').' '.$this->phone;
|
|
}
|
|
}
|
|
|
|
public function getPhoneFull()
|
|
{
|
|
if ($this->phone && $this->phone !== '') {
|
|
return ($this->pre_phone ? $this->pre_phone->phone : '').' '.$this->phone;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
public function getMobilFull()
|
|
{
|
|
if ($this->mobil && $this->mobil !== '') {
|
|
return ($this->pre_mobil ? $this->pre_mobil->phone : '').' '.$this->mobil;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
public function getShippingPhoneFull()
|
|
{
|
|
if ($this->shipping_phone && $this->shipping_phone !== '') {
|
|
return ($this->shipping_pre_phone ? $this->shipping_pre_phone->phone : '').' '.$this->shipping_phone;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Accessor für das language Attribut.
|
|
* Gibt die App-Locale zurück, wenn keine Sprache gesetzt ist.
|
|
*
|
|
* @param string|null $value
|
|
* @return string Sprachcode (de, en, es)
|
|
*/
|
|
public function getLanguageAttribute($value): string
|
|
{
|
|
return $value ?: \App::getLocale();
|
|
}
|
|
|
|
/**
|
|
* Alias für getLanguageAttribute - für Konsistenz mit anderen Models.
|
|
*
|
|
* @return string Sprachcode (de, en, es)
|
|
*/
|
|
public function getLocale(): string
|
|
{
|
|
return $this->language;
|
|
}
|
|
|
|
/**
|
|
* URL zum Berater-Profilfoto (N6) oder null, wenn keins hochgeladen wurde
|
|
* bzw. die Datei nicht (mehr) existiert — Muster wie {@see UserShop::getImage()}.
|
|
*/
|
|
public function getProfileImageUrl(): ?string
|
|
{
|
|
$filename = $this->attributes['profile_image'] ?? null;
|
|
if (empty($filename)) {
|
|
return null;
|
|
}
|
|
|
|
$link = 'images/profile/'.$filename;
|
|
if (! \Storage::disk('public')->has($link)) {
|
|
return null;
|
|
}
|
|
|
|
return '/storage/'.$link.'?=lm='.\Storage::disk('public')->lastModified($link);
|
|
}
|
|
|
|
/**
|
|
* Liefert die verfügbaren Sprachen aus der Config als Array für Select-Felder.
|
|
*
|
|
* @return array ['code' => 'Native Name', ...]
|
|
*/
|
|
public static function getAvailableLanguages(): array
|
|
{
|
|
$locales = config('localization.supportedLocales', []);
|
|
$languages = [];
|
|
|
|
foreach ($locales as $code => $locale) {
|
|
$languages[$code] = $locale['native'] ?? $locale['name'] ?? $code;
|
|
}
|
|
|
|
return $languages;
|
|
}
|
|
}
|