Skip to content

Kundenhandling – Review V2 vs. V1

Stand: 2026-07-14 · Scope: „Kunden" (customers) global in calServer V2, verglichen mit der V1-Referenzimplementierung (Yii 1.1).

Kurzfazit: V1s feingranulares, zuweisungsbasiertes Sichtbarkeitsmodell für Kunden ist in V2 nicht verdrahtet. Die Tabellen (map_user, group_customer, user_group) und die Spalte customer_filter wurden migriert, aber kein einziger Lese-Pfad in V2 wertet sie aus. Ergebnis: jeder authentifizierte User mit dem Recht customers_view sieht alle Kunden. Das ist zugleich eine Funktionsregression und eine Datenschutz-/Mandanten-Lücke. Dazu kommen einige handfeste Bugs in den Kunden-Pickern (Zuweisung + Lookup).


1. Die V1-Referenz („das Ideal")

V1 kapselt die Sichtbarkeit in einer Entscheidungsfunktion, die überall konsistent verwendet wird:

httpdocs/protected/modules/frontend/components/InventoryHelper.php:143isAllCustomers():

public static function isAllCustomers($all = true, $useGlobal = true) {
    $result = false;
    if ($all == true && user()->checkAccess('customer_global')) {
        if ($useGlobal == true) {
            $enableGlobal = FrontendProperty::getSettingOfUser('enable_global');
        }
        if ($useGlobal == false || !empty($enableGlobal)) {
            $result = true;
        }
    }
    return $result;
}

Regeln in V1

  • Default: Ein User sieht nur die ihm über map_user zugewiesenen Kunden. Auch Admin/Superadmin sehen nicht automatisch alles.
  • „Alle sehen" ist Opt-in und braucht beides:
  • das RBAC-Recht customer_global, und
  • die User-Einstellung enable_global (der „Global anzeigen"-Schalter).
  • Gruppen-Kaskade: group_customer + user_group.assigned = 1 propagieren automatisch in map_user (from_assigned_group = 1); SSO-Zuweisungen über from_sso. (AdminGroupCustomer::assignedCustomersToUser())
  • Konsistente Anwendung auf allen Lese-Pfaden:
  • Liste: FrontendCustomer::search():729 erzwingt mapusers.user_uID = user_id(), sofern nicht global.
  • Inventar-Picker: InventoryHelper::getMyCustomers():167.
  • Autocomplete: FrontendCustomerController::actionAutoComplete():1207.
  • FrontendUser::getAssignedCustomers():958.
  • Suche: Suchfelder konfigurierbar über settings/customer/search_customer_fields, plus Relevanz-Ranking (exakt → Prefix → Contains → Suffix).
  • Inventar-Formular adaptiv: ≤ 10 Kunden → lokales Dropdown,

    10 → Remote-Typeahead (frontendInventory/ajaxCustomer) – beide bereits user-gescopt.

Identität / Schlüssel

  • V1-Kundentabelle heißt customers, PK ist KTAGselbst eine UUID (BaseFrontendCustomer:113Uuid::create()).
  • Kundennummer (das für Menschen sichtbare Feld) ist K4601/K4602.
  • map_user.customer_KTAG = Kunden-KTAG (UUID).

2. Der V2-Ist-Zustand

Datenmodell (korrekt / V1-kompatibel)

  • Model: laravel/app/Models/Customer.php – Tabelle customers, PK id (UUID).
  • Pivot: laravel/app/Models/MapUser.phpmap_user(user_uID, customer_KTAG → customers.id, from_assigned_group, from_sso).
  • laravel/app/Models/GroupCustomer.phpgroup_customer(group_id, KTAG).
  • Migration SyncV1ToV2.php:56 mappt V1 KTAG → id und K4602 → customer_number. Damit ist map_user.customer_KTAG = Kunden-UUID V1-konform – nur die Benennung (_KTAG für eine UUID) ist irreführend, kein Bug.

Lese-Pfade (hier liegt das Problem)

Pfad Datei Scoping?
GET /customers (Liste) CustomerController@index:48 (Customer::query()) keins – liefert alle
GET /lookup/customers (Suche) LookupService::baseQuery():168 keins – liefert alle
GET /customers/export CustomerController@export:194 (Customer::query()) keins – exportiert alle

Keiner dieser Pfade joint map_user, prüft customer_global/enable_global oder liest customer_filter. Es gibt keine Global Scopes und keine Tenant-Spalte. Sichtbarkeit ist damit rein durch das Alles-oder-nichts-Recht customers_view gesteuert.

customer_filter – inert

User.customer_filter (User.php:36,84; Default 'all') wird gespeichert und im UserResource ausgegeben, aber nirgends zur Filterung von Kunden-Queries konsultiert. Es ist der vorgesehene, aber nicht angeschlossene Scoping-Haken.

Zuweisungs-UI ist wirkungslos

Der „Kunden"-Tab am User (pages/admin/users/[id].vue:282components/AssignGrid.vue) schreibt korrekt in map_user (POST /users/{id}/customers). Da aber kein Lese-Pfad map_user auswertet, hat die Zuweisung keinerlei Effekt auf das, was der User tatsächlich sieht. Die gesamte Zuweisungsfunktion ist heute kosmetisch.


3. Befunde (priorisiert)

🔴 B1 – Keine Kunden-Sichtbarkeitsscopes (Kernregression + Datenleck)

index, lookup und export liefern jedem User alle Kunden. V1s Modell (map_user + Gruppen-Kaskade + customer_global/enable_global) ist nicht implementiert; customer_filter ist inert.

  • Wirkung: Ein eingeschränkter User sieht in V2 fremde Kunden (in V1 nicht). In einer geteilten Installation ist das eine Mandanten-/Datenschutzlücke.
  • Betroffen: CustomerController@index:48, @export:194, LookupService::baseQuery():168.

🟠 B2 – „Verfügbare Kunden" bricht ab 100 Kunden ab

components/AssignGrid.vue:76 fordert ?page[size]=200, aber CustomerController@index:55 klemmt hart auf min(size, 100). Zusätzlich keine Server-Suche – der Filter im Dialog ist ein reiner Client-Filter über die geladenen ≤ 100 Einträge. → Kunde Nr. 101+ ist nicht zuweisbar.

🟠 B3 – Fehler werden stumm verschluckt

components/AssignGrid.vue:83 und components/LookupSelect.vue:99 fangen Ladefehler nur mit console.error ab und rendern leere Listen. Ein leerer Picker ist damit nicht von einem echten Request-Fehler unterscheidbar – genau das Bild aus den Screenshots („No available options" / „No results found"). Erschwert die Diagnose erheblich.

🟡 B4 – SoftDeletes-Inkonsistenz

Migration create_customer_table.php:37 legt softDeletes() an, aber Customer.php nutzt den SoftDeletes-Trait nicht. → $customer->delete() ist ein Hard-Delete; ein evtl. gesetztes deleted_at würde beim Lesen nicht gefiltert. Entweder Trait ergänzen oder softDeletes() aus der Migration entfernen – die beiden müssen konsistent sein.

🟢 B5 – Kein Bug: customer_KTAG = UUID

Siehe §1/§2: V1-KTAG ist selbst eine UUID; SyncV1ToV2 mappt KTAG → id. Konsistent, nur irreführend benannt.


4. Zu den Screenshots

Da V2 Kunden ungefiltert ausliefert, ist der leere Zustand am ehesten (a) ein Datenstand zum Aufnahmezeitpunkt oder (b) ein stumm geschluckter Request-Fehler (B3). Die Kunden-Liste zeigt 10 Einträge, während der Zuweisungs-Dialog und der Inventar-Lookup leer sind – das deutet auf einen gescheiterten Request in genau diesen beiden Komponenten (B3) oder auf einen abweichenden Datenstand hin, nicht auf fehlende Kunden. Die strukturellen Befunde B1–B4 stehen unabhängig vom Live-Datenstand fest im Code.


5. Umsetzungsplan (Vorschlag)

Reihenfolge nach Risiko/Nutzen. B1 ist architektonisch bedeutsam und sicherheitsrelevant → sollte vor der Umsetzung fachlich bestätigt werden (Soll V2 V1s Scoping exakt nachbauen, oder ist Admin-sieht-alles gewollt?).

B1 – Sichtbarkeits-Scoping nachbauen

  1. Zentralen Helper einführen, z. B. app/Services/Customer/CustomerVisibility.php mit applyScope(Builder $q, User $user): Builder – Analogon zu V1s isAllCustomers():
  2. global sichtbar, wenn customer_global-Recht und enable_global/customer_filter === 'all';
  3. sonst whereExists/join auf map_user.user_uID = $user->id.
  4. In CustomerController@index, @export und LookupService::baseQuery() (für resource === 'customers') anwenden.
  5. Gruppen-Kaskade beim Zuweisen sicherstellen (bereits über map_user mit from_assigned_group modellierbar).
  6. Feature- und Unit-Tests: eingeschränkter User sieht nur zugewiesene Kunden; customer_global-User sieht alle; Export/Lookup identisch gescopt.
  7. DB-agnostisch halten (Eloquent/Query Builder, kein DB::raw).

B2 – AssignGrid-Zuweisung skalierbar machen

  • Server-seitige Suche im Dialog nutzen (Lookup-Endpoint statt clientseitigem Filter über 200er-Batch) oder einen dedizierten „verfügbare/unzugewiesene Kunden"-Endpoint bereitstellen, der map_user bereits ausschließt und paginiert/suchbar ist.
  • Client und Server-Cap angleichen (kein stiller Cut bei 100).

B3 – Fehler sichtbar machen

  • In AssignGrid.vue und LookupSelect.vue Ladefehler in einen sichtbaren Zustand überführen (Toast/Inline-Fehler + Retry) statt leerer Liste.

B4 – SoftDeletes angleichen

  • Entweder SoftDeletes-Trait in Customer ergänzen (und Delete-/409-Logik + Bulk-Delete darauf prüfen) oder softDeletes() aus der Migration nehmen. Konsistenz ist Pflicht.

6. Referenzierte Dateien

V2 (Laravel): - app/Models/Customer.php, app/Models/MapUser.php, app/Models/GroupCustomer.php - app/Http/Controllers/Api/V2/CustomerController.php (index:48, export:194, users:220, assignUser:261) - app/Http/Controllers/Api/V2/UserController.php (customers:366, assignCustomer:402, revokeCustomer:433) - app/Services/Lookup/LookupService.php (baseQuery:168, Config :58) - app/Console/Commands/SyncV1ToV2.php (customers-Mapping :51)

V2 (Frontend): - pages/customer/index.vue, pages/admin/users/[id].vue:282 - components/AssignGrid.vue (loadData:70), components/LookupSelect.vue (search:88), components/DynamicForm.vue

V1 (Yii, Referenz): - httpdocs/protected/modules/frontend/components/InventoryHelper.php (isAllCustomers:143, getMyCustomers:167) - httpdocs/protected/modules/frontend/models/FrontendCustomer.php (search:717, assignCustomersToUser:640) - httpdocs/protected/modules/adminpanel/models/AdminGroupCustomer.php (Gruppen-Kaskade :39)