Statistiken: Kundenfeedback 2026-08-19 komplett (N1-N8) + Review-Fixes + VP-/Incentive-Repair gehärtet

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>
This commit is contained in:
Kevin 2026-08-21 10:44:44 +00:00
parent 841d87e161
commit 867dc21b57
87 changed files with 4480 additions and 240 deletions

View file

@ -27,7 +27,7 @@ return [
'reverse_charge_procedure' => 'Reverse Charge Verfahren',
'tax_number' => 'Steuernummer',
'taxable_sales_1' => 'umsatzsteuerpflichtig (Ich mache eine UST-Voranmeldung pro Monat / Quartal / Jahr ans Finanzamt)',
'taxable_sales_2' => 'nicht umsatzsteuerpflichtig (Kleinunternehmer im Sinne von $ 19',
'taxable_sales_2' => 'nicht umsatzsteuerpflichtig (Kleinunternehmer im Sinne von § 19 UStG)',
'validator_creditcard' => 'Geben Sie bitte eine gültige Kreditkarten-Nummer ein',
'validator_date' => 'Bitte geben Sie ein gültiges Datum ein.',
'validator_digits' => 'Geben Sie bitte nur Ziffern ein.',
@ -45,4 +45,12 @@ return [
'language_settings' => 'Spracheinstellungen',
'preferred_language' => 'Bevorzugte Sprache',
'language_hint' => 'Diese Sprache wird für Ihre Rechnungen, Provisionsabrechnungen und Lieferscheine verwendet.',
// N6: Berater-Profilfoto. Der finale Einwilligungstext wird von der Kundin
// nachgeliefert — profile_image_consent ist bis dahin ein Platzhaltertext.
'profile_image' => 'Profilfoto',
'profile_image_copy' => 'Dein Foto wird teamintern angezeigt, z. B. in Ranglisten und Übersichten.',
'profile_image_consent' => 'Ich bin einverstanden, dass mein Foto teamintern in Ranglisten und Übersichten angezeigt wird.',
'profile_image_consent_required' => 'Bitte bestätige zuerst die Einwilligung zur teaminternen Anzeige deines Fotos.',
'profile_image_delete' => 'Foto löschen',
];

View file

@ -70,4 +70,32 @@ return [
'own' => 'Eigene',
'live_calculation_hint' => 'Live-Berechnung (noch nicht abgeschlossen)',
'live_calculation_hint_text' => 'Wird erst zum Monatsende berechnet.',
'stats_group_team' => 'Team',
'firstlines_count' => 'Firstlines',
'team_partner_total' => 'Teampartner gesamt',
'payline_partner_count' => 'Teampartner in der Payline',
'payline_abo_count' => 'Abos in der Payline',
'level_current' => 'Aktuelles Level',
'level_career' => 'Karriere-Level',
'level_next_missing' => 'Zum nächsten Level :level fehlen noch :points Payline-Punkte.',
'level_next_missing_personal' => 'Zusätzlich fehlen :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_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).',
// N6: "Die besten Ergebnisse des Monats" (teamintern)
'top_results_title' => 'Die besten Ergebnisse des Monats (Team)',
'top_results_volume' => 'Top 3 persönliches Volumen',
'top_results_recruiters' => 'Top 3 Neupartner',
'top_results_new_abos' => 'Top 3 neue Abos',
'top_results_empty' => 'Noch keine Ergebnisse in diesem Monat.',
'top_results_unit_points' => 'Punkte',
'top_results_unit_partners' => 'Neupartner',
'top_results_unit_abos' => 'Abos',
];

View file

@ -77,6 +77,7 @@ return [
'dhl_cockpit' => 'DHL Cockpit',
'revenue' => 'Umsatz',
'level_reports' => 'Level Reports',
'annual_fee' => 'Jahresgebühr',
'dashboard_news' => 'Dashboard News',
'teamabos' => 'Team Abos',
'team_customer_abos' => 'Team Kunden-Abos',

View file

@ -126,4 +126,5 @@ return [
'free_shipping_reached' => 'Ab :amount € versandkostenfrei',
'free_shipping_info' => 'Noch :missing € bis zum versandkostenfreien Versand (ab :amount €)',
'source_comment_required' => 'Bitte schreib uns kurz, wie du auf uns aufmerksam geworden bist - z. B. der Name der Person, die dich empfohlen hat, oder die genaue Messe/Veranstaltung.',
];

View file

@ -154,4 +154,19 @@ return [
'not_available' => 'N/A',
'growth_bonus_details_description' => 'L1 = Direkte Partner (bekommt immer den Tiefenbonus), L2 = Members (ab hier wird der Tiefenbonus geteilt, wenn der Member qualifiziert ist), etc. | Zeigt Provision (€) und Volumen (Pkt) pro Ebene.',
// Jahresgebühr-Nachfassliste (N8)
'annual_fee_title' => 'Jahresgebühr ausstehend',
'annual_fee_hint' => 'Hinweis: Teampartner mit abgelaufenem Account werden ca. 2 Wochen nach Ablauf automatisch deaktiviert und nach 2 Monaten aus dem System entfernt. Überfällige Einträge siehst du hier daher nur für begrenzte Zeit — fasse am besten frühzeitig nach.',
'annual_fee_no_data' => 'Aktuell läuft bei keinem deiner Teampartner die Account-Gültigkeit in den nächsten 3 Monaten ab.',
'annual_fee_group_overdue' => 'Überfällig',
'annual_fee_group_due_1_month' => 'Fällig in ≤ 1 Monat',
'annual_fee_group_due_2_months' => 'Fällig in ≤ 2 Monaten',
'annual_fee_group_due_3_months' => 'Fällig in ≤ 3 Monaten',
'firstlines' => 'Firstlines',
'firstlines_copy' => 'Deine direkten Teampartner mit Umsatz und Kontaktdaten - sortiert nach Punkten des gewählten Monats.',
'firstlines_none' => 'Du hast noch keine direkten Teampartner.',
'firstlines_points' => 'Umsatz (Punkte)',
'active_since' => 'Aktiv seit',
'account_inactive' => 'Account inaktiv',
];

View file

@ -45,4 +45,12 @@ return [
'preferred_language' => 'Preferred language',
'language_hint' => 'This language will be used for your invoices, commission statements and delivery notes.',
'' => '',
// N6: consultant profile photo. Final consent wording will be provided by
// the customer — profile_image_consent is a placeholder text until then.
'profile_image' => 'Profile photo',
'profile_image_copy' => 'Your photo is shown within your team, e.g. in rankings and overviews.',
'profile_image_consent' => 'I agree that my photo may be shown within my team in rankings and overviews.',
'profile_image_consent_required' => 'Please confirm your consent to showing your photo within your team first.',
'profile_image_delete' => 'Delete photo',
];

View file

@ -70,4 +70,32 @@ return [
'live_calculation_hint' => 'Live calculation (not yet finalized)',
'live_calculation_hint_text' => 'Will be calculated at the end of the month.',
'' => '',
'stats_group_team' => 'Team',
'firstlines_count' => 'Firstlines',
'team_partner_total' => 'Team partners total',
'payline_partner_count' => 'Team partners in the payline',
'payline_abo_count' => 'Subscriptions in the payline',
'level_current' => 'Current level',
'level_career' => 'Career level',
'level_next_missing' => ':points payline points missing to reach the next level :level.',
'level_next_missing_personal' => 'Additionally :points points of personal volume are missing.',
'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_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).',
// N6: "Best results of the month" (team-internal)
'top_results_title' => 'Best results of the month (team)',
'top_results_volume' => 'Top 3 personal volume',
'top_results_recruiters' => 'Top 3 new partners',
'top_results_new_abos' => 'Top 3 new subscriptions',
'top_results_empty' => 'No results yet this month.',
'top_results_unit_points' => 'points',
'top_results_unit_partners' => 'new partners',
'top_results_unit_abos' => 'subscriptions',
];

View file

@ -73,6 +73,7 @@ return [
'dhl_cockpit' => 'DHL Cockpit',
'revenue' => 'Revenue',
'level_reports' => 'Level Reports',
'annual_fee' => 'Annual Fee',
'marketingplan' => 'Marketing Plan',
'dashboard' => 'Dashboard',
'dashboard_news' => 'Dashboard News',

View file

@ -125,4 +125,5 @@ return [
'reorder_info_2' => 'Your delivery country is: :country<br>If you want a different country, update your billing or delivery address under <a class="text-primary" href=":link">My details</a>',
'reorder_abo_not_allowed' => 'Subscription orders cannot be repeated via “Reorder”. Please use your subscription area or the shop for one-off orders.',
'number' => 'Item no.',
'source_comment_required' => 'Please tell us briefly how you heard about us - e.g. the name of the person who recommended us or the exact trade fair/event.',
];

View file

@ -154,4 +154,19 @@ return [
'growth_bonus_details_description' => 'L1 = Direct Partner (gets the growth bonus always), L2 = Members (from here the growth bonus is divided if the member is qualified), etc. | Shows commission (€) and volume (points) per level.',
'' => '',
'open' => 'Open',
// Jahresgebühr-Nachfassliste (N8)
'annual_fee_title' => 'Annual fee due',
'annual_fee_hint' => 'Note: Team partners with an expired account are automatically deactivated about 2 weeks after expiry and removed from the system after 2 months. Overdue entries therefore only appear here for a limited time — best follow up early.',
'annual_fee_no_data' => 'Currently none of your team partners has an account validity expiring within the next 3 months.',
'annual_fee_group_overdue' => 'Overdue',
'annual_fee_group_due_1_month' => 'Due in ≤ 1 month',
'annual_fee_group_due_2_months' => 'Due in ≤ 2 months',
'annual_fee_group_due_3_months' => 'Due in ≤ 3 months',
'firstlines' => 'Firstlines',
'firstlines_copy' => 'Your direct team partners with turnover and contact details - sorted by points of the selected month.',
'firstlines_none' => 'You do not have any direct team partners yet.',
'firstlines_points' => 'Turnover (points)',
'active_since' => 'Active since',
'account_inactive' => 'Account inactive',
];

View file

@ -45,4 +45,12 @@ return [
'preferred_language' => 'Idioma preferido',
'language_hint' => 'Este idioma se utilizará para sus facturas, extractos de comisiones y albaranes.',
'' => '',
// N6: foto de perfil. El texto de consentimiento definitivo lo entregará la
// clienta — profile_image_consent es un texto provisional hasta entonces.
'profile_image' => 'Foto de perfil',
'profile_image_copy' => 'Tu foto se muestra internamente en tu equipo, p. ej. en clasificaciones y resúmenes.',
'profile_image_consent' => 'Acepto que mi foto se muestre internamente en mi equipo en clasificaciones y resúmenes.',
'profile_image_consent_required' => 'Confirma primero el consentimiento para mostrar tu foto dentro de tu equipo.',
'profile_image_delete' => 'Eliminar foto',
];

View file

@ -70,4 +70,32 @@ return [
'live_calculation_hint' => 'Cálculo en vivo (no finalizado)',
'live_calculation_hint_text' => 'Se calculará al final del mes.',
'' => '',
'stats_group_team' => 'Equipo',
'firstlines_count' => 'Firstlines',
'team_partner_total' => 'Socios en total',
'payline_partner_count' => 'Socios en la payline',
'payline_abo_count' => 'Suscripciones en la payline',
'level_current' => 'Nivel actual',
'level_career' => 'Nivel de carrera',
'level_next_missing' => 'Para el siguiente nivel :level faltan :points puntos de payline.',
'level_next_missing_personal' => 'Además 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_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).',
// N6: "Los mejores resultados del mes" (interno del equipo)
'top_results_title' => 'Los mejores resultados del mes (equipo)',
'top_results_volume' => 'Top 3 volumen personal',
'top_results_recruiters' => 'Top 3 nuevos socios',
'top_results_new_abos' => 'Top 3 nuevas suscripciones',
'top_results_empty' => 'Aún no hay resultados este mes.',
'top_results_unit_points' => 'puntos',
'top_results_unit_partners' => 'nuevos socios',
'top_results_unit_abos' => 'suscripciones',
];

View file

@ -70,6 +70,7 @@ return [
'dhl_cockpit' => 'Cabina DHL',
'revenue' => 'Ingresos',
'level_reports' => 'Informes de Nivel',
'annual_fee' => 'Cuota anual',
'marketingplan' => 'Plan de Marketing',
'dashboard' => 'Panel',
'dashboard_news' => 'Noticias del Panel',

View file

@ -126,4 +126,5 @@ return [
'reorder_info' => '¿Quieres volver a pedir este artículo?<br>Al hacer clic en el botón, los artículos se añadirán de nuevo al carrito y serás redirigido a la página del carrito.',
'reorder_info_2' => 'Tu país de entrega es: :country<br>Si quieres que tu pedido se entregue en otro país, cambia tu dirección de facturación o de entrega en <a class="text-primary" href=":link">Mis datos</a>',
'reorder_abo_not_allowed' => 'Los pedidos de suscripción no se pueden repetir mediante «Volver a pedir». Utiliza la gestión de tu suscripción o la tienda para pedidos individuales.',
'source_comment_required' => 'Cuéntanos brevemente cómo nos conociste - p. ej. el nombre de la persona que te recomendó o la feria/el evento exacto.',
];

View file

@ -154,4 +154,19 @@ return [
'growth_bonus_details_description' => 'L1 = Socio Directo (obtiene el bono de crecimiento siempre), L2 = Miembros (a partir de aquí el bono de crecimiento se divide si el miembro está calificado), etc. | Muestra comisión (€) y volumen (puntos) por nivel.',
'' => '',
'open' => 'Abierto',
// Jahresgebühr-Nachfassliste (N8)
'annual_fee_title' => 'Cuota anual pendiente',
'annual_fee_hint' => 'Nota: Los socios del equipo con una cuenta caducada se desactivan automáticamente unas 2 semanas después del vencimiento y se eliminan del sistema después de 2 meses. Por eso, las entradas vencidas solo aparecen aquí durante un tiempo limitado — es mejor hacer el seguimiento pronto.',
'annual_fee_no_data' => 'Actualmente, la validez de la cuenta de ninguno de tus socios del equipo caduca en los próximos 3 meses.',
'annual_fee_group_overdue' => 'Vencida',
'annual_fee_group_due_1_month' => 'Vence en ≤ 1 mes',
'annual_fee_group_due_2_months' => 'Vence en ≤ 2 meses',
'annual_fee_group_due_3_months' => 'Vence en ≤ 3 meses',
'firstlines' => 'Firstlines',
'firstlines_copy' => 'Tus socios directos con volumen y datos de contacto - ordenados por puntos del mes seleccionado.',
'firstlines_none' => 'Aún no tienes socios directos.',
'firstlines_points' => 'Volumen (puntos)',
'active_since' => 'Activo desde',
'account_inactive' => 'Cuenta inactiva',
];

View file

@ -27,7 +27,7 @@ return [
'reverse_charge_procedure' => 'Procédure d’autoliquidation',
'tax_number' => 'Numéro fiscal',
'taxable_sales_1' => 'assujetti à la TVA (je dépose une déclaration de TVA mensuelle / trimestrielle / annuelle auprès du fisc)',
'taxable_sales_2' => 'non assujetti à la TVA (petite entreprise au sens du § 19',
'taxable_sales_2' => 'non assujetti à la TVA (petite entreprise au sens du § 19 UStG)',
'validator_creditcard' => 'Veuillez saisir un numéro de carte de crédit valide',
'validator_date' => 'Veuillez saisir une date valide.',
'validator_digits' => 'Veuillez saisir uniquement des chiffres.',
@ -45,4 +45,12 @@ return [
'language_settings' => 'Paramètres de langue',
'preferred_language' => 'Langue préférée',
'language_hint' => 'Cette langue est utilisée pour vos factures, relevés de commissions et bons de livraison.',
// N6 : photo de profil. Le texte de consentement définitif sera fourni par
// la cliente — profile_image_consent est un texte provisoire d'ici là.
'profile_image' => 'Photo de profil',
'profile_image_copy' => 'Ta photo est affichée en interne dans ton équipe, p. ex. dans les classements et les aperçus.',
'profile_image_consent' => "J'accepte que ma photo soit affichée en interne dans mon équipe dans les classements et les aperçus.",
'profile_image_consent_required' => "Merci de confirmer d'abord le consentement à l'affichage de ta photo au sein de ton équipe.",
'profile_image_delete' => 'Supprimer la photo',
];

View file

@ -70,4 +70,32 @@ return [
'own' => 'Propres',
'live_calculation_hint' => 'Calcul en direct (pas encore terminé)',
'live_calculation_hint_text' => 'Ne sera calculé qu’à la fin du mois.',
'stats_group_team' => 'Équipe',
'firstlines_count' => 'Firstlines',
'team_partner_total' => 'Partenaires au total',
'payline_partner_count' => 'Partenaires dans la payline',
'payline_abo_count' => 'Abonnements dans la payline',
'level_current' => 'Niveau actuel',
'level_career' => 'Niveau de carrière',
'level_next_missing' => 'Pour le niveau suivant :level, il manque encore :points points de payline.',
'level_next_missing_personal' => 'Il manque en plus :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_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).',
// N6 : « Les meilleurs résultats du mois » (interne à l'équipe)
'top_results_title' => 'Les meilleurs résultats du mois (équipe)',
'top_results_volume' => 'Top 3 volume personnel',
'top_results_recruiters' => 'Top 3 nouveaux partenaires',
'top_results_new_abos' => 'Top 3 nouveaux abonnements',
'top_results_empty' => 'Pas encore de résultats ce mois-ci.',
'top_results_unit_points' => 'points',
'top_results_unit_partners' => 'nouveaux partenaires',
'top_results_unit_abos' => 'abonnements',
];

View file

@ -74,6 +74,7 @@ return [
'dhl_cockpit' => 'Cockpit DHL',
'revenue' => 'Chiffre d’affaires',
'level_reports' => 'Rapports de niveau',
'annual_fee' => 'Cotisation annuelle',
'dashboard_news' => 'Actualités du tableau de bord',
'teamabos' => 'Abonnements équipe',
'team_customer_abos' => 'Abonnements clients de l’équipe',

View file

@ -125,4 +125,5 @@ return [
'free_shipping' => 'Livraison gratuite',
'free_shipping_reached' => 'Livraison gratuite à partir de :amount €',
'free_shipping_info' => 'Encore :missing € jusqu’à la livraison gratuite (à partir de :amount €)',
'source_comment_required' => 'Dis-nous brièvement comment tu as entendu parler de nous - p. ex. le nom de la personne qui t\'a recommandé ou le salon/l\'événement exact.',
];

View file

@ -133,4 +133,19 @@ return [
'no' => 'Non',
'not_available' => 'N/A',
'growth_bonus_details_description' => 'L1 = partenaires directs (reçoit toujours le bonus de profondeur), L2 = membres (à partir d’ici, le bonus de profondeur est partagé si le membre est qualifié), etc. | Affiche la commission (€) et le volume (pts) par niveau.',
// Jahresgebühr-Nachfassliste (N8)
'annual_fee_title' => 'Cotisation annuelle en attente',
'annual_fee_hint' => 'Remarque : les partenaires dont le compte a expiré sont automatiquement désactivés environ 2 semaines après l’expiration et supprimés du système après 2 mois. Les entrées en retard n’apparaissent donc ici que pour une durée limitée — mieux vaut relancer tôt.',
'annual_fee_no_data' => 'Actuellement, la validité du compte d’aucun de tes partenaires n’expire dans les 3 prochains mois.',
'annual_fee_group_overdue' => 'En retard',
'annual_fee_group_due_1_month' => 'Échéance dans ≤ 1 mois',
'annual_fee_group_due_2_months' => 'Échéance dans ≤ 2 mois',
'annual_fee_group_due_3_months' => 'Échéance dans ≤ 3 mois',
'firstlines' => 'Firstlines',
'firstlines_copy' => 'Tes partenaires directs avec chiffre d’affaires et coordonnées - triés par points du mois sélectionné.',
'firstlines_none' => 'Tu n’as pas encore de partenaires directs.',
'firstlines_points' => 'Chiffre d’affaires (points)',
'active_since' => 'Actif depuis',
'account_inactive' => 'Compte inactif',
];

View file

@ -209,12 +209,16 @@
<div class="text-muted small">{{ __('order.points_total') }}</div>
{{ $shopping_order->getFormattedPoints() }}
</div>
@if ((int) $shopping_order->payment_for === 6 && $shopping_order->customer_order_source)
@if ((int) $shopping_order->payment_for === 6 && ($shopping_order->customer_order_source_comment || $shopping_order->customer_order_source))
<div class="col-md-3 mb-3">
<div class="text-muted small">Herkunft</div>
{{ $shopping_order->getCustomerOrderSourceLabel() }}
@if ($shopping_order->customer_order_source_comment)
<div class="small text-muted mt-1">{{ $shopping_order->customer_order_source_comment }}</div>
@if ($shopping_order->customer_order_source)
{{ $shopping_order->getCustomerOrderSourceLabel() }}
@if ($shopping_order->customer_order_source_comment)
<div class="small text-muted mt-1">{{ $shopping_order->customer_order_source_comment }}</div>
@endif
@else
{{ $shopping_order->customer_order_source_comment }}
@endif
</div>
@endif

View file

@ -0,0 +1,15 @@
{{-- Wiederverwendbarer runder Avatar (N6): Profilfoto oder Initialen-Fallback.
Parameter: imageUrl (string|null), firstName, lastName, size (px, default 48). --}}
@php
$avatarSize = (int) ($size ?? 48);
$avatarInitials = mb_strtoupper(mb_substr(trim($firstName ?? ''), 0, 1) . mb_substr(trim($lastName ?? ''), 0, 1));
$avatarInitials = $avatarInitials !== '' ? $avatarInitials : '?';
@endphp
@if (!empty($imageUrl))
<img src="{{ $imageUrl }}" alt="{{ trim(($firstName ?? '') . ' ' . ($lastName ?? '')) }}" class="rounded-circle"
style="width: {{ $avatarSize }}px; height: {{ $avatarSize }}px; object-fit: cover; flex: 0 0 auto;">
@else
<div class="rounded-circle bg-secondary text-white d-inline-flex align-items-center justify-content-center font-weight-bold"
style="width: {{ $avatarSize }}px; height: {{ $avatarSize }}px; font-size: {{ (int) round($avatarSize * 0.4) }}px; flex: 0 0 auto;">
{{ $avatarInitials }}</div>
@endif

View file

@ -59,6 +59,30 @@
],
],
],
[
'title' => __('home.stats_group_team'),
'col' => 'col-sm-6 col-lg-3',
'tiles' => [
[
'label' => __('home.firstlines_count'), 'value' => $fmt($s['firstline_count'] ?? 0),
'icon' => 'ion-md-git-branch', 'color' => 'info',
'link' => route('user_team_firstlines', ['month' => $m, 'year' => $y]), 'new' => null, 'live' => false,
],
[
'label' => __('home.team_partner_total'), 'value' => $fmt($s['team_partner_count'] ?? 0),
'icon' => 'ion-md-people', 'color' => 'secondary', 'link' => null, 'new' => null, 'live' => false,
],
[
'label' => __('home.payline_partner_count'), 'value' => $fmt($s['payline_member_count'] ?? 0),
'icon' => 'ion-md-podium', 'color' => 'success', 'link' => null, 'new' => null, 'live' => false,
],
[
'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,
],
],
],
[
'title' => __('home.stats_group_subscriptions'),
'col' => 'col-sm-6 col-lg-3',
@ -161,6 +185,42 @@
</div>
@endforeach
@if (! empty($s['level']))
@php($level = $s['level'])
<h6 class="text-muted text-uppercase font-weight-bold small mb-3 mt-2 pt-3 border-top">
{{ __('home.level_current') }}
@if ($level['is_live'])
<span class="badge badge-pill badge-info ml-1" title="{{ __('home.live_calculation_hint') }}">Live</span>
@endif
<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'] }}
</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'])]) }}
</div>
@endif
@else
<div class="text-success">{{ __('home.level_top_reached') }}</div>
@endif
@if ($level['career_name'] !== '')
<div class="text-muted small">{{ __('home.level_career') }}: {{ $level['career_name'] }}</div>
@endif
</div>
</div>
@endif
@if (! empty($s['payline_forecast']['days']))
<h6 class="text-muted text-uppercase font-weight-bold small mb-3 mt-2 pt-3 border-top">
{{ __('home.payline_forecast_title') }}
@ -190,6 +250,43 @@
</div>
@endforeach
</div>
@if (! empty($s['level_forecast']))
@php($lf = $s['level_forecast'])
<div class="alert alert-light border mb-0">
<div class="small text-muted mb-1">
{{ __('home.forecast_level_title') }}
<span class="badge badge-pill badge-warning ml-1"
title="{{ __('home.forecast_level_disclaimer') }}">{{ __('home.forecast_unverbindlich') }}</span>
</div>
@if ($lf['next_name'] !== null && $lf['next_name'] !== '')
@if ($lf['reached'])
<div>
{!! __('home.forecast_level_reached', [
'level' => '<strong>'.e($lf['next_name']).'</strong>',
'volume' => '<strong>'.$fmt($lf['personal_required']).'</strong>',
]) !!}
</div>
@else
<div>
{!! __('home.forecast_level_missing', [
'level' => '<strong>'.e($lf['next_name']).'</strong>',
'points' => '<strong>'.$fmt($lf['missing_pp']).'</strong>',
]) !!}
@if ($lf['missing_personal'] > 0)
{{ __('home.forecast_level_missing_personal', ['points' => $fmt($lf['missing_personal'])]) }}
@endif
</div>
@endif
@endif
<div>
{!! __('home.forecast_commission', [
'amount' => '<strong>'.number_format((float) $lf['estimated_commission'], 2, ',', '.').' €</strong>',
'level' => e($lf['estimated_level_name']),
]) !!}
</div>
</div>
@endif
@endif
</div>
</div>

View file

@ -0,0 +1,59 @@
{{-- "Die besten Ergebnisse des Monats" (N6, teamintern): drei Top-3-Ranglisten
aus TopResultsService via $monthlyStats['top_results']. Karte erscheint nur,
wenn mindestens eine Liste Einträge hat. --}}
@php
$topResults = $monthlyStats['top_results'] ?? [];
$topColumns = [
['key' => 'top_volume', 'title' => __('home.top_results_volume'), 'unit' => __('home.top_results_unit_points'), 'decimals' => 0],
['key' => 'top_recruiters', 'title' => __('home.top_results_recruiters'), 'unit' => __('home.top_results_unit_partners'), 'decimals' => 0],
['key' => 'top_new_abos', 'title' => __('home.top_results_new_abos'), 'unit' => __('home.top_results_unit_abos'), 'decimals' => 0],
];
$hasTopResults = collect($topColumns)->contains(fn($column) => !empty($topResults[$column['key']]));
@endphp
@if (!empty($monthlyStats) && $hasTopResults)
<div class="d-flex col-xl-12 align-items-stretch">
<div class="card w-100 mb-4">
<h5 class="card-header">
<i class="ion ion-md-trophy mr-2"></i>{{ __('home.top_results_title') }} -
{{ HTMLHelper::getMonth($monthlyStats['month']) }} {{ $monthlyStats['year'] }}
</h5>
<div class="card-body">
<div class="row">
@foreach ($topColumns as $column)
<div class="col-md-4 mb-3 {{ !$loop->last ? 'border-md-right' : '' }}">
<h6 class="text-muted text-uppercase font-weight-bold small mb-3">{{ $column['title'] }}
</h6>
@forelse ($topResults[$column['key']] ?? [] as $row)
<div class="d-flex align-items-center mb-3">
<div class="position-relative" style="flex: 0 0 auto;">
@include('components._avatar', [
'imageUrl' => $row['profile_image_url'],
'firstName' => $row['first_name'],
'lastName' => $row['last_name'],
'size' => 48,
])
@if ($loop->first)
<i class="ion ion-md-trophy text-warning position-absolute"
style="right: -4px; bottom: -4px; font-size: 1rem;"
title="{{ $column['title'] }}"></i>
@endif
</div>
<div class="ml-3">
<div class="font-weight-bold">
{{ $row['first_name'] }} {{ $row['last_name'] }}</div>
<div class="text-muted small">
{{ number_format((float) $row['value'], $column['decimals'], ',', '.') }}
{{ $column['unit'] }}</div>
</div>
</div>
@empty
<div class="text-muted small">{{ __('home.top_results_empty') }}</div>
@endforelse
</div>
@endforeach
</div>
</div>
</div>
</div>
@endif

View file

@ -46,6 +46,8 @@
@if (Auth::user()->isActiveAccount())
@include('dashboard._statistics')
@include('dashboard._top_results')
@include('dashboard._points')
@include('dashboard._activities')

View file

@ -105,6 +105,12 @@
<div>{{ __('navigation.level_reports') }}</div>
</a>
</li>
<li class="sidenav-item{{ Request::is('user/team/annual-fee') ? ' active' : '' }}">
<a href="{{ route('user_team_annual_fee') }}" class="sidenav-link"><i
class="sidenav-icon ion ion-md-alarm"></i>
<div>{{ __('navigation.annual_fee') }}</div>
</a>
</li>
<li class="sidenav-item{{ Request::is('user/team/marketingplan') ? ' active' : '' }}">
<a href="{{ route('user_team_marketingplan') }}" class="sidenav-link"><i
class="sidenav-icon ion ion-ios-trending-up"></i>

View file

@ -102,6 +102,7 @@
@elseif ($details['metric'] === 'new_partners')
<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 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
@ -169,6 +170,7 @@
@elseif ($details['metric'] === 'new_partners')
<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>
@else
@ -211,6 +213,7 @@
@elseif ($details['metric'] === 'new_partners')
<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>
<td></td>
<td></td>
@else

View file

@ -432,6 +432,109 @@
</div>
</div>
@php
$recruiterRanking = $statistics['recruiter_ranking'] ?? null;
@endphp
<div class="row">
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">Recruiter des Monats</h5>
<div class="text-muted small"
title="Neupartner wie in der Neupartner-Kennzahl (aktiver Account, im Monat aktiv geworden). Neues Volumen = Monats-Gesamtpunkte der Neupartner (Eigen- und Kundenpunkte, inkl. Starterpaket) plus Kettenvolumen: Punkte der Neupartner, die diese Neupartner im selben Monat selbst geworben haben.">
{{ $selectedMonthName }} {{ $selectedYear }} · nach Anzahl gesponserter Neupartner
<span class="ion ion-md-information-circle-outline"></span>
</div>
</div>
<span class="display-4 font-weight-bold text-primary mb-0">
{{ number_format((float) ($statistics['totals']['new_partners'] ?? 0), 0, ',', '.') }}
</span>
</div>
@if ($recruiterRanking === null)
<div class="card-body">
<p class="text-muted mb-0">
Für diesen Monat liegt noch ein älterer Snapshot ohne diese Liste vor.
</p>
</div>
@elseif (count($recruiterRanking) === 0)
<div class="card-body">
<p class="text-muted mb-0">In diesem Monat wurden keine Neupartner gesponsert.</p>
</div>
@else
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Sponsor</th>
<th class="text-right">Neupartner</th>
<th class="text-right" title="Monats-Gesamtpunkte der Neupartner inkl. Kettenvolumen">neues Volumen</th>
</tr>
</thead>
<tbody>
@foreach ($recruiterRanking as $row)
<tr data-toggle="collapse" data-target="#recruiter-partners-{{ $loop->index }}"
role="button" style="cursor: pointer;"
title="Klick: geworbene Neupartner anzeigen">
<td>
<div class="font-weight-semibold">
{{ $row['name'] }}
@if (!empty($row['is_self']))
<span class="badge badge-pill badge-info ml-1">ich</span>
@endif
</div>
<div class="text-muted small">{{ !empty($row['is_self']) ? 'Eigene Neupartner' : 'Linie '.$row['line'] }} · {{ $row['career_level'] }}</div>
</td>
<td class="text-right align-middle">
<span class="badge badge-pill badge-success backoffice-statistics-click-badge">
+{{ number_format((float) $row['new_partner_count'], 0, ',', '.') }}
</span>
</td>
<td class="text-right align-middle font-weight-bold">
{{ \App\Services\Util::formatNumber($row['volume_points']) }} P.
</td>
</tr>
<tr class="collapse bg-light" id="recruiter-partners-{{ $loop->index }}">
<td colspan="3" class="py-2">
<ul class="list-unstyled small mb-0">
@foreach ($row['partners'] ?? [] as $partner)
<li class="{{ $loop->last ? 'mb-0' : 'mb-1' }}">
<span class="font-weight-semibold">{{ $partner['name'] }}</span>
<span class="text-muted">· {{ $partner['career_level'] }}</span>
· {{ \App\Services\Util::formatNumber($partner['total_points']) }} P.
@if (!empty($partner['children']))
<ul class="list-unstyled text-muted ml-3 mb-0">
@foreach ($partner['children'] as $child)
<li>
↳ {{ $child['name'] }}
· {{ $child['career_level'] }}
· {{ \App\Services\Util::formatNumber($child['total_points']) }} P.
</li>
@endforeach
</ul>
@endif
</li>
@endforeach
</ul>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="card-body pt-2 pb-3">
<p class="text-muted small mb-0">
<strong>neues Volumen</strong> = Monats-Gesamtpunkte (Eigen- und Kundenpunkte, inkl. Starterpaket)
der geworbenen Neupartner — plus rekursiv die Punkte der Neupartner, die diese im selben Monat
selbst geworben haben. Klick auf eine Zeile zeigt, wer geworben wurde.
</p>
</div>
@endif
</div>
</div>
</div>
@php
$topCustomerAbos = $statistics['top_customer_abos'] ?? null;
@endphp

View file

@ -0,0 +1,65 @@
{{-- Berater-Profilfoto (N6): eigener Upload-Block (eigene Form, daher außerhalb
des Profil-Formulars eingebunden). Slim-Cropper-Muster wie beim Shop-Bild. --}}
<div class="card mb-4">
<h5 class="card-header">{{ __('account.profile_image') }}<br>
<small>{{ __('account.profile_image_copy') }}</small>
</h5>
<div class="card-body p-3">
@if ($user->account->getProfileImageUrl())
<div class="text-center">
@include('components._avatar', [
'imageUrl' => $user->account->getProfileImageUrl(),
'firstName' => $user->account->first_name,
'lastName' => $user->account->last_name,
'size' => 120,
])
<br><br>
<a href="{{ route('user_profile_delete_image') }}" class="btn btn-danger"
onclick="return confirm('{{ __('actions.really_delete_picture') }}');"><i class="fa fa-trash"></i>
{{ __('account.profile_image_delete') }}</a>
</div>
@else
<form method="POST" action="{{ route('user_profile_upload_image') }}" accept-charset="UTF-8" class="avatar"
enctype="multipart/form-data">
@csrf
<div class="slim_holder text-center">
<div class="slim" style="margin:20px auto; max-width: 320px;"
data-label='<span class="text-green">{{ __('actions.upload_photo') }}</span><br>({{ __('actions.search_file_or_drag_drop') }})'
data-size="400,400"
data-min-size="200,200"
data-max-file-size="10"
data-status-image-too-small="{{ __('actions.image_too_small') }}"
data-status-file-type="{{ __('actions.invalid_file') }}"
data-status-file-size="{{ __('actions.file_is_too_big') }}"
data-button-confirm-label="{{ __('actions.confirm') }}"
data-button-cancel-label="{{ __('actions.cancel') }}"
data-button-confirm-title="{{ __('actions.confirm') }}"
data-button-cancel-title="{{ __('actions.cancel') }}"
data-button-rotate-title="{{ __('actions.rotate') }}"
data-ratio="1:1">
<input type="file" name="images[]" required />
</div>
<div class="form-group text-left mx-auto" style="max-width: 480px;">
<label class="custom-control custom-checkbox m-0">
<input type="checkbox" class="custom-control-input" name="profile_image_consent"
id="profile_image_consent" value="1">
<span class="custom-control-label">{{ __('account.profile_image_consent') }}</span>
</label>
</div>
<button class="btn btn-primary" type="submit" id="profile-image-submit"
disabled>{{ __('actions.save_image') }}</button>
</div>
</form>
<script type="application/javascript">
$(document).ready(function() {
// Ohne Einwilligung kein Upload (zusätzlich serverseitig geprüft).
$('#profile_image_consent').on('change', function() {
$('#profile-image-submit').prop('disabled', !this.checked);
});
});
</script>
@endif
</div>
</div>

View file

@ -18,6 +18,10 @@
<h4 class="font-weight-bold py-2 mb-2">
{{ __('navigation.my_data') }}
</h4>
{{-- Profilfoto (N6): eigene Upload-Form, daher außerhalb des Profil-Formulars --}}
@include('user.components.user_profile_image')
{!! Form::open(['action' => route('user_edit'), 'method' => 'POST', 'class' => 'form-horizontal', 'id'=>'lead-form-validation']) !!}
<input type="hidden" name="user_id" id="user_id" value="@if($user->id>0){{$user->id}}@else new @endif">
@include('user.user_form')

View file

@ -0,0 +1,68 @@
<div class="card">
<h5 class="card-header">
{{ __('team.annual_fee_title') }}
<span class="badge badge-info ml-2">{{ $totalCount }}</span>
</h5>
<div class="card-body p-0">
<div class="alert alert-info m-4 mb-3">
<i class="ion ion-md-information-circle-outline mr-1"></i>
{{ __('team.annual_fee_hint') }}
</div>
@if ($totalCount === 0)
<div class="px-4 pb-4 text-muted">
{{ __('team.annual_fee_no_data') }}
</div>
@else
@php
$groupMeta = [
\App\Services\Team\AnnualFeeDueService::GROUP_OVERDUE => ['label' => __('team.annual_fee_group_overdue'), 'badge' => 'badge-danger'],
\App\Services\Team\AnnualFeeDueService::GROUP_DUE_1_MONTH => ['label' => __('team.annual_fee_group_due_1_month'), 'badge' => 'badge-warning'],
\App\Services\Team\AnnualFeeDueService::GROUP_DUE_2_MONTHS => ['label' => __('team.annual_fee_group_due_2_months'), 'badge' => 'badge-info'],
\App\Services\Team\AnnualFeeDueService::GROUP_DUE_3_MONTHS => ['label' => __('team.annual_fee_group_due_3_months'), 'badge' => 'badge-secondary'],
];
@endphp
@foreach (\App\Services\Team\AnnualFeeDueService::GROUP_ORDER as $groupKey)
@php $rows = $groups[$groupKey] ?? []; @endphp
@if (count($rows) > 0)
<h6 class="px-4 pt-2 mb-2">
<span class="badge {{ $groupMeta[$groupKey]['badge'] }}">{{ $groupMeta[$groupKey]['label'] }}</span>
<span class="text-muted small ml-1">{{ count($rows) }}</span>
</h6>
<div class="table-responsive mb-4">
<table class="table table-striped table-hover mb-0">
<thead class="thead-light">
<tr>
<th>{{ __('tables.firstname') }}</th>
<th>{{ __('tables.lastname') }}</th>
<th>{{ __('tables.email') }}</th>
<th>{{ __('tables.phone') }}</th>
<th>{{ __('tables.mobil') }}</th>
<th>{{ __('tables.line') }}</th>
<th>{{ __('tables.level') }}</th>
<th>{{ __('tables.account_to') }}</th>
</tr>
</thead>
<tbody>
@foreach ($rows as $row)
<tr>
<td>{{ $row['first_name'] }}</td>
<td>{{ $row['last_name'] }}</td>
<td><a href="mailto:{{ $row['email'] }}">{{ $row['email'] }}</a></td>
<td>{{ $row['phone'] !== '' ? $row['phone'] : '-' }}</td>
<td>{{ $row['mobil'] !== '' ? $row['mobil'] : '-' }}</td>
<td>{{ $row['line'] }}</td>
<td>{{ $row['level_name'] !== '' ? $row['level_name'] : '-' }}</td>
<td class="no-line-break">{{ formatDate($row['due_date']) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
@endforeach
@endif
</div>
</div>

View file

@ -0,0 +1,5 @@
@extends('layouts.layout-2')
@section('content')
@include('user.team._annual_fee_groups', ['groups' => $groups, 'totalCount' => $totalCount])
@endsection

View file

@ -0,0 +1,78 @@
@extends('layouts.layout-2')
@section('content')
<h4 class="font-weight-bold py-2 mb-2 d-flex justify-content-between align-items-center w-100">
<div>{{ __('team.firstlines') }}</div>
</h4>
<div class="card mb-4">
<div class="card-header">
<div class="row align-items-center">
<div class="col-md-8">
<span class="font-weight-bold">{{ __('team.firstlines') }} ({{ count($rows) }})</span>
<div class="text-muted small">{{ __('team.firstlines_copy') }}</div>
</div>
<div class="col-md-4 mt-2 mt-md-0">
{!! Form::open([
'url' => route('user_team_firstlines'),
'method' => 'GET',
'class' => 'form-inline justify-content-md-end',
]) !!}
{!! Form::select('month', $filterMonths, (int) $selectedMonth, ['class' => 'form-control custom-select form-control-sm mr-2', 'onchange' => 'this.form.submit()']) !!}
{!! Form::select('year', array_combine($filterYears, $filterYears), (int) $selectedYear, ['class' => 'form-control custom-select form-control-sm', 'onchange' => 'this.form.submit()']) !!}
<noscript><button type="submit" class="btn btn-sm btn-secondary ml-2">{{ __('abo.filter') }}</button></noscript>
{!! Form::close() !!}
</div>
</div>
</div>
@if (count($rows) === 0)
<div class="card-body">
<p class="text-muted mb-0">{{ __('team.firstlines_none') }}</p>
</div>
@else
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr class="text-muted small">
<th>{{ __('tables.adviser') }}</th>
<th>{{ __('team.career_level') }}</th>
<th class="text-right">{{ __('team.firstlines_points') }}</th>
<th>{{ __('tables.phone') }}</th>
<th>{{ __('tables.email') }}</th>
<th>{{ __('team.active_since') }}</th>
</tr>
</thead>
<tbody>
@foreach ($rows as $row)
<tr>
<td>
<div class="font-weight-semibold">{{ $row['name'] }}</div>
@unless ($row['is_active_account'])
<span class="badge badge-outline-danger">{{ __('team.account_inactive') }}</span>
@endunless
</td>
<td class="align-middle small text-muted">{{ $row['career_level'] }}</td>
<td class="align-middle text-right font-weight-bold">
{{ \App\Services\Util::formatNumber($row['total_points']) }}
</td>
<td class="align-middle">
@if ($row['phone'] !== '')
<a href="tel:{{ $row['phone'] }}">{{ $row['phone'] }}</a>
@else
<span class="text-muted">-</span>
@endif
</td>
<td class="align-middle">
<a href="mailto:{{ $row['email'] }}">{{ $row['email'] }}</a>
</td>
<td class="align-middle small text-muted">
{{ $row['active_since'] ? formatDate($row['active_since']) : '-' }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
@endsection

View file

@ -553,24 +553,10 @@
</div>
</div>
<div class="row mt-3">
<div class="col-md-12 col-sm-12">
<div class="form-group {{($errors->has('customer_order_source') ? 'error' : '')}}">
<label class="mt-0 fs-14 fw-400" for="customer_order_source">Wie bist du auf uns aufmerksam geworden? *</label>
<select id="customer_order_source" name="customer_order_source" class="form-control custom-select">
<option value="">Bitte auswählen</option>
@foreach($customer_order_source_options as $sourceKey => $sourceLabel)
<option value="{{ $sourceKey }}" {{ old('customer_order_source') === $sourceKey ? 'selected' : '' }}>{{ $sourceLabel }}</option>
@endforeach
</select>
@if ($errors->has('customer_order_source'))
<label for="customer_order_source" class="error text-danger small" style="display: block;">{{ $errors->first('customer_order_source') }}</label>
@endif
</div>
</div>
<div class="col-md-12 col-sm-12">
<div class="form-group {{($errors->has('customer_order_source_comment') ? 'error' : '')}}">
<label class="mt-0 fs-14 fw-400" for="customer_order_source_comment">Ergänzung zur Herkunft (optional)</label>
{!! Form::textarea('customer_order_source_comment', old('customer_order_source_comment'), ['class' => 'form-control '.($errors->has('customer_order_source_comment') ? 'error' : ''), 'id'=>'customer_order_source_comment', 'rows' => 3, 'maxlength' => 500]) !!}
<label class="mt-0 fs-14 fw-400" for="customer_order_source_comment">Wie bist du auf uns aufmerksam geworden? *</label>
{!! Form::textarea('customer_order_source_comment', old('customer_order_source_comment'), ['class' => 'form-control '.($errors->has('customer_order_source_comment') ? 'error' : ''), 'id'=>'customer_order_source_comment', 'rows' => 3, 'maxlength' => 500, 'required' => 'required', 'placeholder' => 'Bitte konkret: z. B. Name der Person, die dich empfohlen hat, oder die genaue Messe/Veranstaltung']) !!}
@if ($errors->has('customer_order_source_comment'))
<label for="customer_order_source_comment" class="error text-danger small" style="display: block;">{{ $errors->first('customer_order_source_comment') }}</label>
@endif