Statistik: uebrige Audit-Korrekturen (Abo-Regeln, Prognose, Sortierung)
A01 — Abo-Regeln waren ueber das Aktiv-Feld umgehbar. Das Selbstbedienungsformular hat gar keinen Aktiv-Schalter, es spiegelt den Zustand nur in einem versteckten Feld zurueck; `updateStatus()` hat den Wert trotzdem uebernommen. Wer den Request manipulierte, konnte sein eigenes Abo dauerhaft deaktivieren und so die Mindestlaufzeit umgehen, am Pausenpfad mit seinen Sperren vorbei. Das Feld wirkt jetzt nur noch im Admin-Pfad. A16 — Statuswechsel wurden nur aus dem Formular protokolliert. Recovery-Zahlung, Retry, externe Erledigung und die DSGVO-Anonymisierung schreiben `active` direkt auf das Modell. Das ist heikel, weil die historische Bestandsrueckrechnung ein Abo verwirft, solange eine protokollierte Deaktivierung nicht durch eine spaetere protokollierte Reaktivierung aufgehoben ist — ein stiller `active = true` nimmt es dauerhaft aus allen Folgemonaten. Der Eintrag liegt deshalb jetzt am Model (`UserAbo::logStatusTransition()`) und nicht in den einzelnen Diensten: so kann ihn ein kuenftiger Schreibpfad nicht vergessen, und er entsteht in derselben Transaktion wie die Statusaenderung. Nebeneffekt: Die DSGVO-Anonymisierung setzt `active = false` ohne `cancel_date` — bisher fiel ein so abgeschaltetes Abo rueckwirkend aus allen vergangenen Monaten, jetzt bleibt die Historie korrekt. A13 — Ein heute faelliges Abo galt ab Mitternacht als erledigt, der Ausfuehrungsjob laeuft aber erst um 04:00. In diesem Fenster fehlten die Punkte in der Prognose UND im Ist. Der neue Scope `UserAbo::scopeOpenExecutionUntil()` zaehlt den heutigen Liefertag mit, solange keine Ausfuehrung angelegt ist — genau diese Bedingung verhindert die Doppelzaehlung. Alle drei Prognosen nutzen jetzt denselben Scope statt je einer eigenen Kopie der Datumsregel. A11 — Der Namens-Tiebreak der Top 3 lief nach dem Abschneiden auf 25 Kandidaten, bei Gleichstand entschied also die Reihenfolge der Abfrage. Die Auswahlgrenze wird jetzt um alle Gleichstaende an dieser Grenze erweitert, und Kandidaten ohne Account fallen vorher weg statt die Plaetze zu blockieren. Als letzte Regel entscheidet die user_id, damit eine gecachte Rangliste reproduzierbar bleibt. A14 — Sortiert wurde ueber den angezeigten Zellentext. Unter `en` liefert die Anwendung `Y-m-d` und `1,000.00`, damit ergab jedes Datum 0 und `1,000.00` wurde zur 1. Die Zellen tragen jetzt `data-sort-value` mit dem unformatierten Wert; ohne Attribut gilt weiter der Textweg. Dabei mitgenommen: Umlaute landeten hinter dem Z (jetzt localeCompare), leere Werte lagen als 0 zwischen den negativen Stornowerten, und `next_date` wurde doppelt formatiert. Ausserdem A08 (Prognose auch ohne erreichte Qualifikationsstufe), A09 (Summenzeile im Neupartner-CSV), A10 (Lieferungszahl vorgeladen), A17 (Faelligkeitsgruppen ohne Monatsueberlauf), A18 (Formelinhalte im CSV als Text) und A19 (fehlgeschlagene Provisionsberechnung ist als solche erkennbar statt 0,00 EUR). Audit-Befunde A01, A08, A09, A10, A11, A13, A14, A16, A17, A18, A19. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8d7a98c685
commit
94e2b13384
31 changed files with 1931 additions and 241 deletions
|
|
@ -49,7 +49,13 @@ return [
|
|||
'payline_target_label' => 'Soll (Punkte pro Zyklus)',
|
||||
'payline_actual_label' => 'Ist (im Monat abgerechnet)',
|
||||
'payline_hint' => 'Payline = :lines Ebenen · :members Berater',
|
||||
'payline_abo_counts' => ':team_abos Teamabos + :customer_abos Kundenabos in der Payline',
|
||||
'payline_abo_counts' => ':team_abos Teamabos + :customer_abos Kundenabos in der Payline = :total Abos gesamt',
|
||||
'abo_order_short' => '(Abo)',
|
||||
'points_order_legend' => 'Bestellungen: blau = eigene Bestellung · grau = Kundenbestellung · türkis mit „(Abo)" = Abo-Lieferung.',
|
||||
'customer_abo_forecast_title' => 'Punkte aus meinen Kundenabos zum 5. / 10. / 20. / 25.',
|
||||
'customer_abo_forecast_hint' => 'Kumuliert: die Punkte aller eigenen Kundenabos, deren Lieferung bis zum jeweiligen Termin dieses Monats noch aussteht. Ohne dein eigenes Abo und ohne Team.',
|
||||
'customer_abo_list_title' => 'Meine Kundenabos (:count)',
|
||||
'customer_abo_list_search' => 'Kundenabos durchsuchen...',
|
||||
'payline_none' => 'Noch keine Payline vorhanden (kein Provisionslevel erreicht).',
|
||||
'abo_points_per_cycle' => 'Punkte pro Lieferung (Bestand)',
|
||||
'abo_points_billed' => 'abgerechnet (:month)',
|
||||
|
|
|
|||
|
|
@ -49,7 +49,9 @@ return [
|
|||
'welcome_back' => 'Willkommen zurück',
|
||||
'your_shop' => 'Dein Shop',
|
||||
'commission_today' => 'Provision (Stand heute)',
|
||||
'commission_unavailable' => 'Die Provisionsberechnung ist gerade nicht verfuegbar. Der Wert wird nachgetragen, sobald sie wieder laeuft.',
|
||||
'payline_forecast_title' => 'Payline-Prognose zum 5. / 10. / 20. / 25.',
|
||||
'payline_forecast_basis' => 'Basis: Paylinepunkte + persönliches Volumen',
|
||||
'payline_forecast_hint' => 'Basis: aktueller Kundenumsatz + Teamumsatz (Payline). Dazu kommen kumuliert die bis zum jeweiligen Termin noch anstehenden Abo-Punkte deiner Payline (inkl. deiner eigenen Abos und Kundenabos). Prognose: Pausen, Kündigungen oder fehlgeschlagene Zahlungen können den Wert verändern.',
|
||||
'forecast_day' => 'Prognose zum :date',
|
||||
'forecast_upcoming' => 'Abo-Punkte offen',
|
||||
|
|
@ -75,19 +77,24 @@ return [
|
|||
'team_partner_total' => 'Teampartner gesamt',
|
||||
'payline_partner_count' => 'Teampartner in der Payline',
|
||||
'payline_abo_count' => 'Abos in der Payline',
|
||||
'payline_career_fallback_hint' => 'Gezählt nach den Ebenen deines Karriere-Levels: deine Qualifikation für diesen Monat steht noch aus. Sobald sie feststeht, zählt die Kachel nach den Ebenen der Qualifikationsstufe.',
|
||||
'level_current' => 'Qualifikation in diesem Monat',
|
||||
'level_career' => 'Karriere-Level',
|
||||
'level_next_missing' => 'Für die Qualifikation als :level in diesem Monat fehlen dir noch :points Payline-Punkte.',
|
||||
'level_next_missing_personal' => 'Zusätzlich fehlen :points Punkte persönliches Volumen.',
|
||||
'level_not_qualified_yet' => 'Noch nicht qualifiziert',
|
||||
'level_next_missing_personal_only' => 'Für die Qualifikation als :level in diesem Monat fehlen dir noch :points Punkte persönliches Volumen.',
|
||||
'level_top_reached' => 'Höchstes Level erreicht.',
|
||||
'level_hint' => 'Monatliche Qualifikationsstufe aus dem Live-Lauf. Payline-Punkte = Teampunkte deiner bezahlten Ebenen inkl. deines Kundenpunkte-Überschusses.',
|
||||
'forecast_level_title' => 'Level & Provision laut Prognose (Stand 25.)',
|
||||
'forecast_unverbindlich' => 'unverbindlich',
|
||||
'forecast_level_disclaimer' => 'Schätzung auf Basis der heutigen Punkte plus der noch offenen Abo-Lieferungen. Rang-/Strukturänderungen im Team sind nicht berücksichtigt - verbindlich ist erst der Monatsabschluss.',
|
||||
'forecast_level_needs_qualification' => 'Level- und Provisions-Prognose erscheint, sobald deine Qualifikation für diesen Monat feststeht — die Provisionssätze hängen an der Qualifikationsstufe.',
|
||||
'forecast_level_reached' => 'Laut Prognose erreichst du das nächste Level :level - sofern dein persönliches Volumen von :volume Punkten erreicht wird.',
|
||||
'forecast_level_missing' => 'Laut Prognose fehlen zum nächsten Level :level noch :points Payline-Punkte.',
|
||||
'forecast_level_missing_personal' => 'Zusätzlich fehlen :points Punkte persönliches Volumen.',
|
||||
'forecast_commission' => 'Voraussichtliche Provision laut Prognose: :amount (auf Basis Level :level).',
|
||||
'forecast_commission_no_level' => 'Voraussichtliche Provision laut Prognose: :amount. Team-Provision kommt hinzu, sobald die erste Qualifikationsstufe erreicht ist.',
|
||||
|
||||
// N6: "Die besten Ergebnisse des Monats" (teamintern)
|
||||
'top_results_title' => 'Die besten Ergebnisse des Monats (Team)',
|
||||
|
|
|
|||
|
|
@ -175,7 +175,13 @@ return [
|
|||
'payline_target_label' => 'Target (points per cycle)',
|
||||
'payline_actual_label' => 'Actual (billed this month)',
|
||||
'payline_hint' => 'Payline = :lines levels · :members consultants',
|
||||
'payline_abo_counts' => ':team_abos team subscriptions + :customer_abos customer subscriptions in the payline',
|
||||
'payline_abo_counts' => ':team_abos team subscriptions + :customer_abos customer subscriptions in the payline = :total subscriptions in total',
|
||||
'abo_order_short' => '(subscr.)',
|
||||
'points_order_legend' => 'Orders: blue = your own order · grey = customer order · turquoise with "(subscr.)" = subscription delivery.',
|
||||
'customer_abo_forecast_title' => 'Points from my customer subscriptions for the 5th / 10th / 20th / 25th',
|
||||
'customer_abo_forecast_hint' => 'Cumulative: the points of all your own customer subscriptions whose delivery is still due by the respective date this month. Without your own subscription and without your team.',
|
||||
'customer_abo_list_title' => 'My customer subscriptions (:count)',
|
||||
'customer_abo_list_search' => 'Search customer subscriptions...',
|
||||
'payline_none' => 'No payline yet (no commission level reached).',
|
||||
'abo_points_per_cycle' => 'Points per delivery (current stock)',
|
||||
'abo_points_billed' => 'billed (:month)',
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ return [
|
|||
'welcome_back' => 'welcome back',
|
||||
'your_shop' => 'your shop',
|
||||
'commission_today' => 'Commission (as of today)',
|
||||
'commission_unavailable' => 'The commission calculation is currently unavailable. The value will appear once it runs again.',
|
||||
'payline_forecast_title' => 'Payline forecast for the 5th / 10th / 20th / 25th',
|
||||
'payline_forecast_basis' => 'Basis: payline points + personal volume',
|
||||
'payline_forecast_hint' => 'Basis: current customer turnover + team turnover (payline). Plus, cumulatively, the subscription points of your payline still due up to the respective date (incl. your own subscriptions and customer subscriptions). Forecast: pauses, cancellations or failed payments can change the value.',
|
||||
'forecast_day' => 'Forecast for :date',
|
||||
'forecast_upcoming' => 'subscription points due',
|
||||
|
|
@ -75,19 +77,24 @@ return [
|
|||
'team_partner_total' => 'Team partners total',
|
||||
'payline_partner_count' => 'Team partners in the payline',
|
||||
'payline_abo_count' => 'Subscriptions in the payline',
|
||||
'payline_career_fallback_hint' => 'Counted across the levels of your career rank: your qualification for this month is still pending. Once it is set, the tile counts across the levels of the qualification rank.',
|
||||
'level_current' => 'Qualification this month',
|
||||
'level_career' => 'Career level',
|
||||
'level_next_missing' => 'You still need :points payline points to qualify as :level this month.',
|
||||
'level_next_missing_personal' => 'Additionally :points points of personal volume are missing.',
|
||||
'level_not_qualified_yet' => 'Not yet qualified',
|
||||
'level_next_missing_personal_only' => 'You still need :points points of personal volume to qualify as :level this month.',
|
||||
'level_top_reached' => 'Highest level reached.',
|
||||
'level_hint' => 'Monthly qualification level from the live run. Payline points = team points of your paid levels including your customer points surplus.',
|
||||
'forecast_level_title' => 'Level & commission according to forecast (as of the 25th)',
|
||||
'forecast_unverbindlich' => 'non-binding',
|
||||
'forecast_level_disclaimer' => 'Estimate based on today\'s points plus the still open subscription deliveries. Rank/structure changes in the team are not taken into account - only the monthly closing is binding.',
|
||||
'forecast_level_needs_qualification' => 'The level and commission forecast appears as soon as your qualification for this month is set — the commission rates depend on the qualification rank.',
|
||||
'forecast_level_reached' => 'According to the forecast you will reach the next level :level - provided your personal volume of :volume points is reached.',
|
||||
'forecast_level_missing' => 'According to the forecast, :points payline points are still missing for the next level :level.',
|
||||
'forecast_level_missing_personal' => 'Additionally :points points of personal volume are missing.',
|
||||
'forecast_commission' => 'Expected commission according to forecast: :amount (based on level :level).',
|
||||
'forecast_commission_no_level' => 'Projected commission: :amount. Team commission is added once the first qualification level is reached.',
|
||||
|
||||
// N6: "Best results of the month" (team-internal)
|
||||
'top_results_title' => 'Best results of the month (team)',
|
||||
|
|
|
|||
|
|
@ -158,7 +158,13 @@ return [
|
|||
'payline_target_label' => 'Objetivo (puntos por ciclo)',
|
||||
'payline_actual_label' => 'Real (facturado este mes)',
|
||||
'payline_hint' => 'Payline = :lines niveles · :members asesores',
|
||||
'payline_abo_counts' => ':team_abos suscripciones de asesores + :customer_abos suscripciones de clientes en la payline',
|
||||
'payline_abo_counts' => ':team_abos suscripciones de equipo + :customer_abos suscripciones de clientes en la payline = :total suscripciones en total',
|
||||
'abo_order_short' => '(susc.)',
|
||||
'points_order_legend' => 'Pedidos: azul = pedido propio · gris = pedido de cliente · turquesa con «(susc.)» = entrega de suscripción.',
|
||||
'customer_abo_forecast_title' => 'Puntos de mis suscripciones de clientes para el 5 / 10 / 20 / 25',
|
||||
'customer_abo_forecast_hint' => 'Acumulado: los puntos de todas tus suscripciones de clientes cuya entrega aún está pendiente hasta la fecha correspondiente de este mes. Sin tu propia suscripción y sin el equipo.',
|
||||
'customer_abo_list_title' => 'Mis suscripciones de clientes (:count)',
|
||||
'customer_abo_list_search' => 'Buscar suscripciones de clientes...',
|
||||
'payline_none' => 'Aún no hay payline (no se ha alcanzado ningún nivel de comisión).',
|
||||
'abo_points_per_cycle' => 'Puntos por entrega (cartera actual)',
|
||||
'abo_points_billed' => 'facturado (:month)',
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ return [
|
|||
'welcome_back' => 'bienvenido de nuevo',
|
||||
'your_shop' => 'tu tienda',
|
||||
'commission_today' => 'Comisión (a día de hoy)',
|
||||
'commission_unavailable' => 'El calculo de comisiones no esta disponible en este momento. El valor aparecera cuando vuelva a ejecutarse.',
|
||||
'payline_forecast_title' => 'Pronóstico de payline para el 5 / 10 / 20 / 25',
|
||||
'payline_forecast_basis' => 'Base: puntos de payline + volumen personal',
|
||||
'payline_forecast_hint' => 'Base: volumen de clientes actual + volumen del equipo (payline). Más, de forma acumulada, los puntos de suscripción de tu payline pendientes hasta la fecha correspondiente (incl. tus propias suscripciones y las de tus clientes). Pronóstico: pausas, cancelaciones o pagos fallidos pueden cambiar el valor.',
|
||||
'forecast_day' => 'Pronóstico para el :date',
|
||||
'forecast_upcoming' => 'puntos de suscripción pendientes',
|
||||
|
|
@ -75,19 +77,24 @@ return [
|
|||
'team_partner_total' => 'Socios en total',
|
||||
'payline_partner_count' => 'Socios en la payline',
|
||||
'payline_abo_count' => 'Suscripciones en la payline',
|
||||
'payline_career_fallback_hint' => 'Contado según los niveles de tu nivel de carrera: tu calificación de este mes aún está pendiente. En cuanto esté fijada, la tarjeta contará según los niveles del nivel de calificación.',
|
||||
'level_current' => 'Calificación de este mes',
|
||||
'level_career' => 'Nivel de carrera',
|
||||
'level_next_missing' => 'Para calificar como :level este mes te faltan :points puntos de payline.',
|
||||
'level_next_missing_personal' => 'Además faltan :points puntos de volumen personal.',
|
||||
'level_not_qualified_yet' => 'Aún no calificado',
|
||||
'level_next_missing_personal_only' => 'Para calificar como :level este mes te faltan :points puntos de volumen personal.',
|
||||
'level_top_reached' => 'Nivel más alto alcanzado.',
|
||||
'level_hint' => 'Nivel de calificación mensual del cálculo en vivo. Puntos de payline = puntos de equipo de tus niveles pagados incluyendo tu excedente de puntos de clientes.',
|
||||
'forecast_level_title' => 'Nivel y comisión según la previsión (al día 25)',
|
||||
'forecast_unverbindlich' => 'sin compromiso',
|
||||
'forecast_level_disclaimer' => 'Estimación basada en los puntos de hoy más las entregas de suscripción pendientes. No se consideran cambios de rango/estructura en el equipo - solo el cierre mensual es vinculante.',
|
||||
'forecast_level_needs_qualification' => 'La previsión de nivel y comisión aparece en cuanto se fija tu calificación de este mes: los porcentajes de comisión dependen del nivel de calificación.',
|
||||
'forecast_level_reached' => 'Según la previsión alcanzarás el siguiente nivel :level - siempre que se alcance tu volumen personal de :volume puntos.',
|
||||
'forecast_level_missing' => 'Según la previsión faltan :points puntos de payline para el siguiente nivel :level.',
|
||||
'forecast_level_missing_personal' => 'Además faltan :points puntos de volumen personal.',
|
||||
'forecast_commission' => 'Comisión prevista según la previsión: :amount (según nivel :level).',
|
||||
'forecast_commission_no_level' => 'Comision prevista segun el pronostico: :amount. La comision de equipo se anade en cuanto se alcanza el primer nivel de cualificacion.',
|
||||
|
||||
// N6: "Los mejores resultados del mes" (interno del equipo)
|
||||
'top_results_title' => 'Los mejores resultados del mes (equipo)',
|
||||
|
|
|
|||
|
|
@ -49,7 +49,13 @@ return [
|
|||
'payline_target_label' => 'Objectif (points par cycle)',
|
||||
'payline_actual_label' => 'Réel (facturé ce mois-ci)',
|
||||
'payline_hint' => 'Payline = :lines niveaux · :members conseillers',
|
||||
'payline_abo_counts' => ':team_abos abonnements conseillers + :customer_abos abonnements clients dans la payline',
|
||||
'payline_abo_counts' => ':team_abos abonnements équipe + :customer_abos abonnements clients dans la payline = :total abonnements au total',
|
||||
'abo_order_short' => '(abo)',
|
||||
'points_order_legend' => 'Commandes : bleu = commande personnelle · gris = commande client · turquoise avec « (abo) » = livraison d’abonnement.',
|
||||
'customer_abo_forecast_title' => 'Points de mes abonnements clients pour le 5 / 10 / 20 / 25',
|
||||
'customer_abo_forecast_hint' => 'Cumulé : les points de tous tes abonnements clients dont la livraison est encore à venir jusqu’à la date concernée ce mois-ci. Sans ton propre abonnement et sans l’équipe.',
|
||||
'customer_abo_list_title' => 'Mes abonnements clients (:count)',
|
||||
'customer_abo_list_search' => 'Rechercher des abonnements clients...',
|
||||
'payline_none' => 'Pas encore de payline (aucun niveau de commission atteint).',
|
||||
'abo_points_per_cycle' => 'Points par livraison (portefeuille actuel)',
|
||||
'abo_points_billed' => 'facturé (:month)',
|
||||
|
|
|
|||
|
|
@ -49,7 +49,9 @@ return [
|
|||
'welcome_back' => 'Bon retour',
|
||||
'your_shop' => 'Votre boutique',
|
||||
'commission_today' => 'Commission (à ce jour)',
|
||||
'commission_unavailable' => 'Le calcul des commissions n\'est pas disponible actuellement. La valeur apparaitra des qu\'il fonctionnera de nouveau.',
|
||||
'payline_forecast_title' => 'Prévision payline pour le 5 / 10 / 20 / 25',
|
||||
'payline_forecast_basis' => 'Base : points payline + volume personnel',
|
||||
'payline_forecast_hint' => 'Base : chiffre d’affaires clients actuel + chiffre d’affaires équipe (payline). S’y ajoutent, de manière cumulée, les points d’abonnement de ta payline encore à venir jusqu’à la date concernée (y compris tes propres abonnements et abonnements clients). Prévision : pauses, résiliations ou paiements échoués peuvent modifier la valeur.',
|
||||
'forecast_day' => 'Prévision pour le :date',
|
||||
'forecast_upcoming' => 'points d’abonnement à venir',
|
||||
|
|
@ -75,19 +77,24 @@ return [
|
|||
'team_partner_total' => 'Partenaires au total',
|
||||
'payline_partner_count' => 'Partenaires dans la payline',
|
||||
'payline_abo_count' => 'Abonnements dans la payline',
|
||||
'payline_career_fallback_hint' => 'Compté sur les niveaux de ton niveau de carrière : ta qualification pour ce mois n\'est pas encore établie. Dès qu\'elle le sera, la tuile comptera sur les niveaux du niveau de qualification.',
|
||||
'level_current' => 'Qualification de ce mois',
|
||||
'level_career' => 'Niveau de carrière',
|
||||
'level_next_missing' => 'Pour te qualifier comme :level ce mois-ci, il te manque encore :points points de payline.',
|
||||
'level_next_missing_personal' => 'Il manque en plus :points points de volume personnel.',
|
||||
'level_not_qualified_yet' => 'Pas encore qualifié',
|
||||
'level_next_missing_personal_only' => 'Pour te qualifier comme :level ce mois-ci, il te manque encore :points points de volume personnel.',
|
||||
'level_top_reached' => 'Niveau le plus élevé atteint.',
|
||||
'level_hint' => 'Niveau de qualification mensuel issu du calcul en direct. Points de payline = points d’équipe de tes niveaux payés, y compris ton excédent de points clients.',
|
||||
'forecast_level_title' => 'Niveau et commission selon la prévision (au 25)',
|
||||
'forecast_unverbindlich' => 'sans engagement',
|
||||
'forecast_level_disclaimer' => 'Estimation basée sur les points actuels plus les livraisons d’abonnement encore ouvertes. Les changements de rang/structure dans l’équipe ne sont pas pris en compte - seule la clôture mensuelle fait foi.',
|
||||
'forecast_level_needs_qualification' => 'La prévision de niveau et de commission apparaît dès que ta qualification pour ce mois est établie : les taux de commission dépendent du niveau de qualification.',
|
||||
'forecast_level_reached' => 'Selon la prévision, tu atteindras le niveau suivant :level - à condition que ton volume personnel de :volume points soit atteint.',
|
||||
'forecast_level_missing' => 'Selon la prévision, il manque encore :points points de payline pour le niveau suivant :level.',
|
||||
'forecast_level_missing_personal' => 'Il manque en plus :points points de volume personnel.',
|
||||
'forecast_commission' => 'Commission prévisionnelle selon la prévision : :amount (sur la base du niveau :level).',
|
||||
'forecast_commission_no_level' => 'Commission prevue selon la prevision : :amount. La commission d\'equipe s\'ajoute des que le premier niveau de qualification est atteint.',
|
||||
|
||||
// N6 : « Les meilleurs résultats du mois » (interne à l'équipe)
|
||||
'top_results_title' => 'Les meilleurs résultats du mois (équipe)',
|
||||
|
|
|
|||
143
resources/views/components/_sortable_table.blade.php
Normal file
143
resources/views/components/_sortable_table.blade.php
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
{{-- Sortier- und Suchverhalten für einfache Tabellen ohne Serverpaging.
|
||||
|
||||
Erwartet:
|
||||
$tableId id der <table>
|
||||
$searchId id eines optionalen Suchfeldes (leer lassen, wenn keins da ist)
|
||||
$minWidth optionale Mindestbreite der Tabelle (Default 980px)
|
||||
|
||||
Sortierbare Spalten bekommen `data-sortable="true"` und optional
|
||||
`data-sort-type="number|date"`. Für große Listen ist das nicht gedacht —
|
||||
dort gehört Yajra-Datatables hin.
|
||||
|
||||
Sortierwerte (A14): Zellen mit Zahlen, Datum oder mehrzeiligem Inhalt tragen
|
||||
`data-sort-value` mit dem UNFORMATIERTEN Wert — Zahl mit Punkt als
|
||||
Dezimaltrenner, Datum als `Y-m-d`. Der Zellentext ist lokalisiert
|
||||
(`Util::_thousands_separator()` und `formatDate()` schalten bei `en` auf
|
||||
`1,000.00` und `Y-m-d` um) und taugt deshalb nicht als Sortiergrundlage:
|
||||
der Datumsparser erkennt nur `dd.mm.yyyy`, und der Zahlenparser macht aus
|
||||
`1,000.00` eine 1. Ohne Attribut wird weiterhin der Text gelesen — das
|
||||
stimmt nur im deutschen Format. --}}
|
||||
@php
|
||||
$sortableMinWidth = $minWidth ?? '980px';
|
||||
$sortableSearchId = $searchId ?? null;
|
||||
@endphp
|
||||
|
||||
<style>
|
||||
#{{ $tableId }} {
|
||||
min-width: {{ $sortableMinWidth }};
|
||||
}
|
||||
|
||||
#{{ $tableId }} th,
|
||||
#{{ $tableId }} td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#{{ $tableId }} th[data-sortable="true"] {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#{{ $tableId }} .sortable-label {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
gap: .25rem;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#{{ $tableId }} .sort-icon {
|
||||
color: #adb5bd;
|
||||
flex: 0 0 auto;
|
||||
font-size: .875rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#{{ $tableId }} th[data-sort-direction="asc"] .sort-icon,
|
||||
#{{ $tableId }} th[data-sort-direction="desc"] .sort-icon {
|
||||
color: #4e73df;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var tableSelector = '#{{ $tableId }}';
|
||||
var searchSelector = @json($sortableSearchId ? '#'.$sortableSearchId : null);
|
||||
|
||||
if (searchSelector) {
|
||||
$(searchSelector).on('keyup search', function() {
|
||||
var search = $(this).val().toLowerCase();
|
||||
|
||||
$(tableSelector + ' tbody tr').each(function() {
|
||||
var rowText = $(this).text().toLowerCase();
|
||||
$(this).toggle(rowText.indexOf(search) !== -1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(tableSelector + ' th[data-sortable="true"]').css('cursor', 'pointer').on('click', function() {
|
||||
var $header = $(this);
|
||||
var columnIndex = $header.index();
|
||||
var sortType = $header.data('sort-type') || 'text';
|
||||
var direction = $header.data('sort-direction') === 'asc' ? 'desc' : 'asc';
|
||||
var rows = $(tableSelector + ' tbody tr').get();
|
||||
|
||||
rows.sort(function(a, b) {
|
||||
var aValue = getSortValue($(a).children('td').eq(columnIndex), sortType);
|
||||
var bValue = getSortValue($(b).children('td').eq(columnIndex), sortType);
|
||||
var result;
|
||||
|
||||
if (sortType === 'number' || sortType === 'date') {
|
||||
result = aValue < bValue ? -1 : (aValue > bValue ? 1 : 0);
|
||||
} else {
|
||||
// Umlaute und Akzente gehören alphabetisch einsortiert, nicht
|
||||
// hinter das Z — das leistet nur ein Sprachvergleich.
|
||||
result = aValue.localeCompare(bValue, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
});
|
||||
}
|
||||
|
||||
return direction === 'asc' ? result : -result;
|
||||
});
|
||||
|
||||
$(tableSelector + ' th[data-sortable="true"]')
|
||||
.removeData('sort-direction')
|
||||
.removeAttr('data-sort-direction');
|
||||
$header.data('sort-direction', direction).attr('data-sort-direction', direction);
|
||||
$.each(rows, function(index, row) {
|
||||
$(tableSelector + ' tbody').append(row);
|
||||
});
|
||||
});
|
||||
|
||||
function getSortValue($cell, sortType) {
|
||||
var raw = $cell.attr('data-sort-value');
|
||||
var hasRaw = typeof raw !== 'undefined';
|
||||
var value = $.trim(hasRaw ? raw : $cell.text());
|
||||
|
||||
if (sortType === 'number') {
|
||||
// Leer heißt "kein Wert" und gehört ans Ende, nicht zwischen die
|
||||
// Stornos: eine 0 stünde sonst mitten in negativen Punkteständen.
|
||||
if (value === '') {
|
||||
return Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
|
||||
return hasRaw
|
||||
? (parseFloat(value) || 0)
|
||||
: (parseFloat(value.replace(/\./g, '').replace(',', '.').replace(/[^\d.-]/g, '')) || 0);
|
||||
}
|
||||
|
||||
if (sortType === 'date') {
|
||||
if (hasRaw) {
|
||||
var iso = value.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
|
||||
return iso ? new Date(iso[1], iso[2] - 1, iso[3]).getTime() : Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
|
||||
var parts = value.match(/(\d{2})\.(\d{2})\.(\d{4})/);
|
||||
|
||||
return parts ? new Date(parts[3], parts[2] - 1, parts[1]).getTime() : Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
|
||||
return value.toLowerCase();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
@ -20,6 +20,15 @@
|
|||
|
||||
$fmt = fn ($value) => number_format((float) $value, 0, ',', '.');
|
||||
|
||||
// Solange die Monatsqualifikation aussteht, zaehlen die beiden
|
||||
// Payline-Struktur-Kacheln nach den Ebenen des Karriere-Levels — das
|
||||
// wird an der Kachel ausgewiesen, damit die Zahl einordenbar bleibt.
|
||||
$paylineFallbackNote = ! empty($s['payline_is_career_fallback'])
|
||||
? __('home.payline_career_fallback_hint')
|
||||
: null;
|
||||
|
||||
$commissionFailed = (bool) ($s['commission']['failed'] ?? false);
|
||||
|
||||
$clusters = [
|
||||
[
|
||||
'title' => __('home.stats_group_turnover'),
|
||||
|
|
@ -39,9 +48,15 @@
|
|||
],
|
||||
[
|
||||
'label' => __('home.commission_today'),
|
||||
'value' => number_format((float) ($s['commission']['total'] ?? 0), 2, ',', '.') . ' €',
|
||||
'icon' => 'ion-md-cash', 'color' => 'warning', 'link' => null, 'new' => null,
|
||||
'live' => (bool) ($s['commission']['is_live'] ?? false),
|
||||
// Eine fehlgeschlagene Berechnung darf nicht wie ein Nullmonat aussehen.
|
||||
'value' => $commissionFailed
|
||||
? '—'
|
||||
: number_format((float) ($s['commission']['total'] ?? 0), 2, ',', '.') . ' €',
|
||||
'icon' => 'ion-md-cash',
|
||||
'color' => $commissionFailed ? 'secondary' : 'warning',
|
||||
'link' => null, 'new' => null,
|
||||
'live' => ! $commissionFailed && (bool) ($s['commission']['is_live'] ?? false),
|
||||
'note' => $commissionFailed ? __('home.commission_unavailable') : null,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
|
@ -75,11 +90,13 @@
|
|||
[
|
||||
'label' => __('home.payline_partner_count'), 'value' => $fmt($s['payline_member_count'] ?? 0),
|
||||
'icon' => 'ion-md-podium', 'color' => 'success', 'link' => null, 'new' => null, 'live' => false,
|
||||
'note' => $paylineFallbackNote,
|
||||
],
|
||||
[
|
||||
'label' => __('home.payline_abo_count'), 'value' => $fmt($s['payline_abo_count'] ?? 0),
|
||||
'icon' => 'ion-md-repeat', 'color' => 'success', 'link' => $teamShow,
|
||||
'new' => $s['payline_abos_new'] ?? 0, 'live' => false,
|
||||
'note' => $paylineFallbackNote,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
|
@ -160,6 +177,10 @@
|
|||
@if ($tile['link'])
|
||||
<i class="ion ion-md-open text-primary ml-1" style="font-size: .8rem;"></i>
|
||||
@endif
|
||||
@if (! empty($tile['note']))
|
||||
<i class="ion ion-md-information-circle-outline text-warning ml-1"
|
||||
style="font-size: .9rem;" title="{{ $tile['note'] }}"></i>
|
||||
@endif
|
||||
</div>
|
||||
<div class="font-weight-bold text-large">
|
||||
{{ $tile['value'] }}
|
||||
|
|
@ -195,20 +216,34 @@
|
|||
<span class="ion ion-md-information-circle-outline ml-1" title="{{ __('home.level_hint') }}"></span>
|
||||
</h6>
|
||||
<div class="d-flex align-items-center flex-wrap mb-2">
|
||||
<span class="badge badge-primary mr-3 mb-2" style="font-size: 1rem; padding: .5rem .9rem;">
|
||||
{{ $level['name'] }}
|
||||
@php($isQualified = $level['is_qualified'] ?? true)
|
||||
<span class="badge {{ $isQualified ? 'badge-primary' : 'badge-secondary' }} mr-3 mb-2"
|
||||
style="font-size: 1rem; padding: .5rem .9rem;">
|
||||
{{ $isQualified ? $level['name'] : __('home.level_not_qualified_yet') }}
|
||||
</span>
|
||||
<div class="mb-2">
|
||||
@if ($level['next_name'] !== null && $level['next_name'] !== '')
|
||||
<div>
|
||||
{!! __('home.level_next_missing', [
|
||||
'level' => '<strong>'.e($level['next_name']).'</strong>',
|
||||
'points' => '<strong>'.$fmt($level['missing_pp']).'</strong>',
|
||||
]) !!}
|
||||
</div>
|
||||
@if ($level['missing_personal'] > 0)
|
||||
<div class="text-muted small">
|
||||
{{ __('home.level_next_missing_personal', ['points' => $fmt($level['missing_personal'])]) }}
|
||||
{{-- Ohne Monatsquali fehlen typischerweise nur persoenliche Punkte
|
||||
(das erste Level hat qual_pp = 0) — dann darf hier kein Satz
|
||||
mit "0 Payline-Punkte" stehen. --}}
|
||||
@if ($level['missing_pp'] > 0)
|
||||
<div>
|
||||
{!! __('home.level_next_missing', [
|
||||
'level' => '<strong>'.e($level['next_name']).'</strong>',
|
||||
'points' => '<strong>'.$fmt($level['missing_pp']).'</strong>',
|
||||
]) !!}
|
||||
</div>
|
||||
@if ($level['missing_personal'] > 0)
|
||||
<div class="text-muted small">
|
||||
{{ __('home.level_next_missing_personal', ['points' => $fmt($level['missing_personal'])]) }}
|
||||
</div>
|
||||
@endif
|
||||
@elseif ($level['missing_personal'] > 0)
|
||||
<div>
|
||||
{!! __('home.level_next_missing_personal_only', [
|
||||
'level' => '<strong>'.e($level['next_name']).'</strong>',
|
||||
'points' => '<strong>'.$fmt($level['missing_personal']).'</strong>',
|
||||
]) !!}
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
|
|
@ -228,6 +263,9 @@
|
|||
<span class="badge badge-pill badge-info ml-1"
|
||||
title="{{ __('home.payline_forecast_hint') }}">Live</span>
|
||||
</h6>
|
||||
{{-- Woraus die Basis der vier Kacheln besteht (Kundenwunsch 2026-09-07):
|
||||
HomeController uebergibt customer_points + team_payline_points. --}}
|
||||
<div class="text-muted small mb-3 mt-n2">{{ __('home.payline_forecast_basis') }}</div>
|
||||
<div class="row">
|
||||
@foreach ($s['payline_forecast']['days'] as $forecastDay)
|
||||
<div class="col-sm-6 col-lg-3 mb-3">
|
||||
|
|
@ -281,12 +319,28 @@
|
|||
@endif
|
||||
@endif
|
||||
<div>
|
||||
{!! __('home.forecast_commission', [
|
||||
'amount' => '<strong>'.number_format((float) $lf['estimated_commission'], 2, ',', '.').' €</strong>',
|
||||
'level' => e($lf['estimated_level_name']),
|
||||
]) !!}
|
||||
@if ($lf['estimated_level_name'] !== '')
|
||||
{!! __('home.forecast_commission', [
|
||||
'amount' => '<strong>'.number_format((float) $lf['estimated_commission'], 2, ',', '.').' €</strong>',
|
||||
'level' => e($lf['estimated_level_name']),
|
||||
]) !!}
|
||||
@else
|
||||
{{-- Noch keine Stufe erreicht: die Prognose nennt einen Betrag,
|
||||
aber keine Stufe, auf deren Saetzen er beruht. --}}
|
||||
{!! __('home.forecast_commission_no_level', [
|
||||
'amount' => '<strong>'.number_format((float) $lf['estimated_commission'], 2, ',', '.').' €</strong>',
|
||||
]) !!}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@elseif (! empty($s['level']) && ! ($s['level']['is_qualified'] ?? true))
|
||||
{{-- Ohne Qualifikationsstufe gibt es keine Provisionssaetze, mit denen
|
||||
sich eine Prognose rechnen liesse. Statt den Block wortlos
|
||||
wegzulassen, wird der Grund genannt. --}}
|
||||
<div class="alert alert-light border mb-0 text-muted small">
|
||||
<i class="ion ion-md-information-circle-outline mr-1"></i>
|
||||
{{ __('home.forecast_level_needs_qualification') }}
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
110
resources/views/user/abo/_customer_abo_list.blade.php
Normal file
110
resources/views/user/abo/_customer_abo_list.blade.php
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
{{-- Eigene Kundenabos als sortierbare Liste (Kundenwunsch 2026-09-07: die
|
||||
Kartenansicht war bei vielen Abos unübersichtlich). Spalten und Sortierung
|
||||
nach ihrer Vorgabe: Kunde, Liefertag, Abo-Punkte, nächste Ausführung,
|
||||
Startdatum, Ausführungen.
|
||||
|
||||
Erwartet $listAbos (Collection<UserAbo>) und $customerAboForecast|null. --}}
|
||||
@if (! empty($customerAboForecast['days']))
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted text-uppercase font-weight-bold small mb-1">
|
||||
{{ __('abo.customer_abo_forecast_title') }}
|
||||
</h6>
|
||||
<div class="text-muted small mb-3">{{ __('abo.customer_abo_forecast_hint') }}</div>
|
||||
<div class="row">
|
||||
@foreach ($customerAboForecast['days'] as $forecastDay)
|
||||
<div class="col-sm-6 col-lg-3 mb-2">
|
||||
<div class="card h-100 {{ $forecastDay['is_past'] ? 'bg-light' : '' }}">
|
||||
<div class="card-body py-3 text-center">
|
||||
<div class="text-muted small">
|
||||
{{ __('home.forecast_day', ['date' => $forecastDay['date']]) }}
|
||||
</div>
|
||||
<div class="font-weight-bold text-large">
|
||||
{{ \App\Services\Util::formatNumber($forecastDay['total']) }}
|
||||
</div>
|
||||
<div class="small {{ $forecastDay['upcoming'] > 0 ? 'text-success' : 'text-muted' }}">
|
||||
{{ $forecastDay['is_past'] && $forecastDay['upcoming'] <= 0 ? __('home.forecast_done') : __('navigation.points') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body pb-2">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<h5 class="mb-0">{{ __('abo.customer_abo_list_title', ['count' => $listAbos->count()]) }}</h5>
|
||||
</div>
|
||||
<div class="col-md-6 mt-2 mt-md-0">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><span class="ion ion-ios-search"></span></span>
|
||||
</div>
|
||||
<input type="search" id="customer-abo-search" class="form-control"
|
||||
placeholder="{{ __('abo.customer_abo_list_search') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0" id="customer-abo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sortable="true"><span class="sortable-label">{{ __('tables.customer') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th data-sortable="true" data-sort-type="number"><span class="sortable-label">{{ __('tables.abo_delivery_day') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">{{ __('navigation.points') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th data-sortable="true" data-sort-type="date"><span class="sortable-label">{{ __('tables.next_date') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th data-sortable="true" data-sort-type="date"><span class="sortable-label">{{ __('tables.start_date') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">{{ __('tables.abo_delivery') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th data-sortable="true"><span class="sortable-label">{{ __('tables.status') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($listAbos as $listAbo)
|
||||
<tr>
|
||||
@php
|
||||
// Sortiert wird über data-sort-value, nicht über den
|
||||
// angezeigten Text — der ist lokalisiert (A14). Der Name
|
||||
// steht zusätzlich getrennt da, weil die Zelle auch die
|
||||
// E-Mail enthält und sonst beides zusammen sortiert würde.
|
||||
$listAboName = $listAbo->shopping_user
|
||||
? trim($listAbo->shopping_user->billing_firstname.' '.$listAbo->shopping_user->billing_lastname)
|
||||
: (string) $listAbo->email;
|
||||
@endphp
|
||||
<td data-sort-value="{{ $listAboName }}">
|
||||
<div class="font-weight-semibold">
|
||||
{{ $listAboName }}
|
||||
</div>
|
||||
@if ($listAbo->shopping_user)
|
||||
<div class="text-muted small">{{ $listAbo->shopping_user->billing_email }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td data-sort-value="{{ (int) $listAbo->abo_interval }}">{{ \App\Services\HTMLHelper::getAboStrLang($listAbo->abo_interval) }}</td>
|
||||
<td class="text-right font-weight-bold text-primary" data-sort-value="{{ $listAbo->getTotalPoints() }}">{{ $listAbo->getFormattedTotalPoints() }}</td>
|
||||
<td data-sort-value="{{ $listAbo->getRawOriginal('next_date') }}">{{ $listAbo->next_date }}</td>
|
||||
<td data-sort-value="{{ $listAbo->getRawOriginal('start_date') }}">{{ $listAbo->start_date }}</td>
|
||||
<td class="text-right" data-sort-value="{{ $listAbo->getCountPaidOrders() }}">{{ $listAbo->getCountPaidOrders() }}</td>
|
||||
<td data-sort-value="{{ $listAbo->getStatusType() }}">{!! $listAbo->getStatusFormated() !!} {!! get_active_badge($listAbo->active) !!}</td>
|
||||
<td class="text-right">
|
||||
<a href="{{ route('user_abos_detail', ['ot', $listAbo->id]) }}" class="btn btn-sm btn-secondary">
|
||||
<span class="fa fa-edit"></span> {{ __('abo.abo_details') }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include('components._sortable_table', [
|
||||
'tableId' => 'customer-abo-table',
|
||||
'searchId' => 'customer-abo-search',
|
||||
'minWidth' => '900px',
|
||||
])
|
||||
|
|
@ -100,9 +100,10 @@
|
|||
<th class="text-right" data-sortable="true" data-sort-type="number" style="background-color:#d4edda;"><span class="sortable-label justify-content-end">Kundenpunkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">Gesamtpunkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
@elseif ($details['metric'] === 'new_partners')
|
||||
<th data-sortable="true"><span class="sortable-label">Telefon <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th data-sortable="true"><span class="sortable-label">Sponsor <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">Kundenpunkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th class="text-right" data-sortable="true" data-sort-type="number" title="Eigen- und Kundenpunkte des Monats zusammen — inkl. Starterpaket"><span class="sortable-label justify-content-end">Punkte gesamt <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th class="text-right" data-sortable="true" data-sort-type="number" title="Punkte aus den Bestellungen SEINER Kunden (Shop und Abo) — ohne seine eigenen Bestellungen."><span class="sortable-label justify-content-end">Punkte seiner Kunden <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th class="text-right" data-sortable="true" data-sort-type="number" title="Punkte seiner Kunden PLUS seine eigenen Bestellungen — hier steckt vor allem das Starterpaket drin."><span class="sortable-label justify-content-end">Gesamt (inkl. Eigenbestellung) <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th data-sortable="true" data-sort-type="date"><span class="sortable-label">Aktiv seit <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
<th data-sortable="true" data-sort-type="date"><span class="sortable-label">Account gültig bis <i class="ion ion-ios-swap sort-icon"></i></span></th>
|
||||
@else
|
||||
|
|
@ -119,8 +120,8 @@
|
|||
<td>{{ $row['email'] ?? '-' }}</td>
|
||||
<td>{{ $row['career_level'] ?? '-' }}</td>
|
||||
@if ($details['metric'] === 'team_customer_abos')
|
||||
<td class="text-right">{{ number_format($row['abo_count'], 0, ',', '.') }}</td>
|
||||
<td class="text-right">
|
||||
<td class="text-right" data-sort-value="{{ $row['abo_count'] }}">{{ number_format($row['abo_count'], 0, ',', '.') }}</td>
|
||||
<td class="text-right" data-sort-value="{{ $row['points'] }}">
|
||||
<span class="font-weight-bold">{{ \App\Services\Util::formatNumber($row['points']) }}</span>
|
||||
@if (! empty($row['abos']))
|
||||
<a class="d-block small" data-toggle="collapse"
|
||||
|
|
@ -145,37 +146,38 @@
|
|||
</td>
|
||||
@elseif ($details['metric'] === 'team_partner_abos')
|
||||
<td>{{ $row['consultant_name'] ?? $row['name'] }}</td>
|
||||
<td class="text-right">{{ \App\Services\Util::formatNumber($row['points']) }}</td>
|
||||
<td>
|
||||
<td class="text-right" data-sort-value="{{ $row['points'] }}">{{ \App\Services\Util::formatNumber($row['points']) }}</td>
|
||||
<td data-sort-value="{{ $row['status_label'] ?? '' }}">
|
||||
{!! $row['status_badge'] ?? '' !!}
|
||||
@if (! empty($row['status_reason']))
|
||||
<div class="small text-muted mt-1">{{ $row['status_reason'] }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<td data-sort-value="{{ $row['start_date_sort'] ?? '' }}">
|
||||
{{ $row['start_date'] ?? '-' }}
|
||||
@if (! empty($row['is_new_this_month']))
|
||||
<span class="badge badge-success ml-1">Neu</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ $row['next_date'] ?? '-' }}</td>
|
||||
<td class="text-right">{{ number_format($row['deliveries'], 0, ',', '.') }}</td>
|
||||
<td data-sort-value="{{ $row['next_date_sort'] ?? '' }}">{{ $row['next_date'] ?? '-' }}</td>
|
||||
<td class="text-right" data-sort-value="{{ $row['deliveries'] }}">{{ number_format($row['deliveries'], 0, ',', '.') }}</td>
|
||||
@elseif (in_array($details['metric'], ['own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000'], true))
|
||||
<td class="text-right"><span class="badge" style="background-color:#d4edda;color:#155724;font-size:90%;">{{ \App\Services\Util::formatNumber($row['own_points']) }}</span></td>
|
||||
<td class="text-right">{{ \App\Services\Util::formatNumber($row['customer_single_order_points']) }}</td>
|
||||
<td class="text-right">{{ \App\Services\Util::formatNumber($row['customer_abo_points']) }}</td>
|
||||
<td class="text-right">{{ \App\Services\Util::formatNumber($row['customer_other_points']) }}</td>
|
||||
<td class="text-right font-weight-bold"><span class="badge" style="background-color:#d4edda;color:#155724;font-size:90%;">{{ \App\Services\Util::formatNumber($row['external_points']) }}</span></td>
|
||||
<td class="text-right font-weight-bold">{{ \App\Services\Util::formatNumber($row['total_points']) }}</td>
|
||||
<td class="text-right" data-sort-value="{{ $row['own_points'] }}"><span class="badge" style="background-color:#d4edda;color:#155724;font-size:90%;">{{ \App\Services\Util::formatNumber($row['own_points']) }}</span></td>
|
||||
<td class="text-right" data-sort-value="{{ $row['customer_single_order_points'] }}">{{ \App\Services\Util::formatNumber($row['customer_single_order_points']) }}</td>
|
||||
<td class="text-right" data-sort-value="{{ $row['customer_abo_points'] }}">{{ \App\Services\Util::formatNumber($row['customer_abo_points']) }}</td>
|
||||
<td class="text-right" data-sort-value="{{ $row['customer_other_points'] }}">{{ \App\Services\Util::formatNumber($row['customer_other_points']) }}</td>
|
||||
<td class="text-right font-weight-bold" data-sort-value="{{ $row['external_points'] }}"><span class="badge" style="background-color:#d4edda;color:#155724;font-size:90%;">{{ \App\Services\Util::formatNumber($row['external_points']) }}</span></td>
|
||||
<td class="text-right font-weight-bold" data-sort-value="{{ $row['total_points'] }}">{{ \App\Services\Util::formatNumber($row['total_points']) }}</td>
|
||||
@elseif ($details['metric'] === 'new_partners')
|
||||
<td>{{ ($row['phone'] ?? '') !== '' ? $row['phone'] : '-' }}</td>
|
||||
<td>{{ $row['sponsor'] ?? '-' }}</td>
|
||||
<td class="text-right font-weight-bold">{{ \App\Services\Util::formatNumber($row['customer_points'] ?? 0) }}</td>
|
||||
<td class="text-right font-weight-bold">{{ \App\Services\Util::formatNumber($row['total_points'] ?? 0) }}</td>
|
||||
<td>{{ $row['active_date'] ?? '-' }}</td>
|
||||
<td>{{ $row['payment_account'] ?? '-' }}</td>
|
||||
<td class="text-right font-weight-bold" data-sort-value="{{ $row['customer_points'] ?? 0 }}">{{ \App\Services\Util::formatNumber($row['customer_points'] ?? 0) }}</td>
|
||||
<td class="text-right font-weight-bold" data-sort-value="{{ $row['total_points'] ?? 0 }}">{{ \App\Services\Util::formatNumber($row['total_points'] ?? 0) }}</td>
|
||||
<td data-sort-value="{{ $row['active_date_sort'] ?? '' }}">{{ $row['active_date'] ?? '-' }}</td>
|
||||
<td data-sort-value="{{ $row['payment_account_sort'] ?? '' }}">{{ $row['payment_account'] ?? '-' }}</td>
|
||||
@else
|
||||
<td>{{ $row['active_date'] ?? '-' }}</td>
|
||||
<td>
|
||||
<td data-sort-value="{{ $row['active_date_sort'] ?? '' }}">{{ $row['active_date'] ?? '-' }}</td>
|
||||
<td data-sort-value="{{ $row['payment_account_sort'] ?? '' }}">
|
||||
{{ $row['payment_account'] ?? '-' }}
|
||||
@if (isset($row['is_account_active']))
|
||||
<span
|
||||
|
|
@ -211,6 +213,7 @@
|
|||
<td class="text-right" style="background-color:#d4edda;">{{ \App\Services\Util::formatNumber($details['summary']['external_points']) }}</td>
|
||||
<td class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['total_points']) }}</td>
|
||||
@elseif ($details['metric'] === 'new_partners')
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['customer_points'] ?? 0) }}</td>
|
||||
<td class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['total_points'] ?? 0) }}</td>
|
||||
|
|
@ -229,104 +232,9 @@
|
|||
</div>
|
||||
|
||||
@if ($details['rows'] !== [])
|
||||
<style>
|
||||
#backoffice-statistics-detail-table {
|
||||
min-width: 980px;
|
||||
}
|
||||
|
||||
#backoffice-statistics-detail-table th,
|
||||
#backoffice-statistics-detail-table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#backoffice-statistics-detail-table th[data-sortable="true"] {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#backoffice-statistics-detail-table .sortable-label {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
gap: .25rem;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#backoffice-statistics-detail-table .sort-icon {
|
||||
color: #adb5bd;
|
||||
flex: 0 0 auto;
|
||||
font-size: .875rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#backoffice-statistics-detail-table th[data-sort-direction="asc"] .sort-icon,
|
||||
#backoffice-statistics-detail-table th[data-sort-direction="desc"] .sort-icon {
|
||||
color: #4e73df;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#backoffice-statistics-detail-search').on('keyup search', function() {
|
||||
var search = $(this).val().toLowerCase();
|
||||
|
||||
$('#backoffice-statistics-detail-table tbody tr').each(function() {
|
||||
var rowText = $(this).text().toLowerCase();
|
||||
$(this).toggle(rowText.indexOf(search) !== -1);
|
||||
});
|
||||
});
|
||||
|
||||
$('#backoffice-statistics-detail-table th[data-sortable="true"]').css('cursor', 'pointer').on('click',
|
||||
function() {
|
||||
var $header = $(this);
|
||||
var columnIndex = $header.index();
|
||||
var sortType = $header.data('sort-type') || 'text';
|
||||
var direction = $header.data('sort-direction') === 'asc' ? 'desc' : 'asc';
|
||||
var rows = $('#backoffice-statistics-detail-table tbody tr').get();
|
||||
|
||||
rows.sort(function(a, b) {
|
||||
var aValue = getSortValue($(a).children('td').eq(columnIndex).text(), sortType);
|
||||
var bValue = getSortValue($(b).children('td').eq(columnIndex).text(), sortType);
|
||||
|
||||
if (aValue < bValue) {
|
||||
return direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
|
||||
if (aValue > bValue) {
|
||||
return direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
$('#backoffice-statistics-detail-table th[data-sortable="true"]')
|
||||
.removeData('sort-direction')
|
||||
.removeAttr('data-sort-direction');
|
||||
$header.data('sort-direction', direction).attr('data-sort-direction', direction);
|
||||
$.each(rows, function(index, row) {
|
||||
$('#backoffice-statistics-detail-table tbody').append(row);
|
||||
});
|
||||
});
|
||||
|
||||
function getSortValue(value, sortType) {
|
||||
value = $.trim(value);
|
||||
|
||||
if (sortType === 'number') {
|
||||
return parseFloat(value.replace(/\./g, '').replace(',', '.').replace(/[^\d.-]/g, '')) || 0;
|
||||
}
|
||||
|
||||
if (sortType === 'date') {
|
||||
var parts = value.match(/(\d{2})\.(\d{2})\.(\d{4})/);
|
||||
|
||||
if (!parts) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return new Date(parts[3], parts[2] - 1, parts[1]).getTime();
|
||||
}
|
||||
|
||||
return value.toLowerCase();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@include('components._sortable_table', [
|
||||
'tableId' => 'backoffice-statistics-detail-table',
|
||||
'searchId' => 'backoffice-statistics-detail-search',
|
||||
])
|
||||
@endif
|
||||
@endsection
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue