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 Spaltecustomer_filterwurden migriert, aber kein einziger Lese-Pfad in V2 wertet sie aus. Ergebnis: jeder authentifizierte User mit dem Rechtcustomers_viewsieht 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:143 –
isAllCustomers():
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_userzugewiesenen 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 = 1propagieren automatisch inmap_user(from_assigned_group = 1); SSO-Zuweisungen überfrom_sso. (AdminGroupCustomer::assignedCustomersToUser()) - Konsistente Anwendung auf allen Lese-Pfaden:
- Liste:
FrontendCustomer::search():729erzwingtmapusers.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 istKTAG– selbst eine UUID (BaseFrontendCustomer:113→Uuid::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– Tabellecustomers, PKid(UUID). - Pivot:
laravel/app/Models/MapUser.php–map_user(user_uID, customer_KTAG → customers.id, from_assigned_group, from_sso). laravel/app/Models/GroupCustomer.php–group_customer(group_id, KTAG).- Migration
SyncV1ToV2.php:56mappt V1KTAG → idundK4602 → customer_number. Damit istmap_user.customer_KTAG= Kunden-UUID V1-konform – nur die Benennung (_KTAGfü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:282 →
components/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¶
- Zentralen Helper einführen, z. B.
app/Services/Customer/CustomerVisibility.phpmitapplyScope(Builder $q, User $user): Builder– Analogon zu V1sisAllCustomers(): - global sichtbar, wenn
customer_global-Recht undenable_global/customer_filter === 'all'; - sonst
whereExists/joinaufmap_user.user_uID = $user->id. - In
CustomerController@index,@exportundLookupService::baseQuery()(fürresource === 'customers') anwenden. - Gruppen-Kaskade beim Zuweisen sicherstellen (bereits über
map_usermitfrom_assigned_groupmodellierbar). - Feature- und Unit-Tests: eingeschränkter User sieht nur zugewiesene
Kunden;
customer_global-User sieht alle; Export/Lookup identisch gescopt. - 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_userbereits ausschließt und paginiert/suchbar ist. - Client und Server-Cap angleichen (kein stiller Cut bei 100).
B3 – Fehler sichtbar machen¶
- In
AssignGrid.vueundLookupSelect.vueLadefehler in einen sichtbaren Zustand überführen (Toast/Inline-Fehler + Retry) statt leerer Liste.
B4 – SoftDeletes angleichen¶
- Entweder
SoftDeletes-Trait inCustomerergänzen (und Delete-/409-Logik + Bulk-Delete darauf prüfen) odersoftDeletes()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)