Take staff from the back office, and let the seeded PINs die when it has any
Suriya/4821, Divya/5093, Rahul/6274 were compiled into the app — the same three logins on every install, readable by anyone with the APK, and unreplaceable. Sign-in now imports the outlet's real staff and deactivates everything it didn't import, so the built-in PINs stop working the moment a shop has anyone recorded. That deactivation is the point: merging would have left the hardcoded logins alive alongside the real ones for ever. The seeds stay, and that is not a hedge. Only 116 of 596 accounts on the platform have a PIN set, and outlet 1135 — the one this build ships pointed at — has none at all. Deleting them would hand 33 of 34 tenants a till nobody can sign in to. So: back office first, local database once synced, seeds only when there is nothing else. Rows are keyed on the back office user id, so a re-sync updates one account rather than creating a second. A leaver removed upstream loses the till on the next sign-in. Accounts are deactivated rather than deleted, because bills carry the cashier's name and shifts settle against it. An import that writes nobody is treated exactly like an empty answer — a back office full of `pin = 0` rows must not deactivate the seeds and strand the counter. That is a real shape in the data, not a hypothetical. An imported PIN is not flagged for change; the shop already chose it. The flag belongs to the seeds, which everyone shares. Role names are mapped by name and fall back to cashier. `app_roles` holds six rows for four roles — Admin and Manager appear twice each — and most accounts carry a roleid absent from the table entirely, so an unrecognised role must not quietly become an admin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../app/providers.dart';
|
||||
import '../../../data/local/staff_dao.dart';
|
||||
import '../../../data/remote/pos_auth_api.dart';
|
||||
import '../../../domain/entities/pos_session.dart';
|
||||
import '../../../domain/entities/store_account.dart';
|
||||
@@ -163,6 +164,19 @@ class AuthController extends StateNotifier<AuthState> {
|
||||
phone: session.phone,
|
||||
);
|
||||
|
||||
// Who may ring a bill here, per the back office.
|
||||
//
|
||||
// This is what retires the seeded logins. The till ships with three names
|
||||
// and three PINs compiled into it — the same three on every install — and
|
||||
// they exist only so a shop whose back office has no staff recorded can
|
||||
// still trade on day one. The moment real staff arrive they are
|
||||
// deactivated, which is the whole point of importing rather than merging.
|
||||
//
|
||||
// Empty is the common case rather than an error: most outlets have nobody
|
||||
// recorded, including the one this build ships pointed at. The import
|
||||
// no-ops, the seeds survive, and the shop keeps selling.
|
||||
await _importStaff(session);
|
||||
|
||||
_ref.invalidate(storeAccountProvider);
|
||||
final store = await _ref.read(storeAccountProvider.future);
|
||||
final staff = store.staff;
|
||||
@@ -184,6 +198,43 @@ class AuthController extends StateNotifier<AuthState> {
|
||||
state = Authenticated(store: store, user: opener);
|
||||
}
|
||||
|
||||
/// Writes the back office's staff over this terminal's.
|
||||
///
|
||||
/// Failures are swallowed. A shop must be able to open its till even when the
|
||||
/// staff import fails — the seeded or previously-synced accounts are still
|
||||
/// there, and refusing the sign-in would trade a working counter for a
|
||||
/// tidier database.
|
||||
Future<void> _importStaff(PosSession session) async {
|
||||
if (session.staff.isEmpty) return;
|
||||
|
||||
try {
|
||||
await _ref.read(localStoreProvider).staff.replaceFromBackOffice([
|
||||
for (final member in session.staff)
|
||||
StaffImportRecord(
|
||||
localId: member.localId,
|
||||
name: member.fullName,
|
||||
role: _roleFor(member.role),
|
||||
pin: member.pin,
|
||||
),
|
||||
]);
|
||||
} on Object {
|
||||
// Deliberately silent — see above.
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps the back office's role names onto the till's three.
|
||||
///
|
||||
/// `app_roles` holds six rows for four distinct roles — Admin and Manager are
|
||||
/// each in there twice — and most accounts carry a `roleid` that is not in
|
||||
/// the table at all. So this matches on the name and falls back to the least
|
||||
/// privileged answer: an unrecognised role must not silently become an admin.
|
||||
StaffRole _roleFor(String backOfficeRole) =>
|
||||
switch (backOfficeRole.trim().toLowerCase()) {
|
||||
'super admin' || 'admin' => StaffRole.admin,
|
||||
'manager' || 'operations' => StaffRole.manager,
|
||||
_ => StaffRole.cashier,
|
||||
};
|
||||
|
||||
/// Switches the active operator, checking their PIN.
|
||||
///
|
||||
/// Every bill is stamped with whoever is active, so this is the boundary that
|
||||
|
||||
Reference in New Issue
Block a user