This commit is contained in:
2026-08-07 15:31:20 +05:30
parent 09e5e29df2
commit ad44402232
17 changed files with 598 additions and 1187 deletions

View File

@@ -146,87 +146,6 @@ class StaffDao {
);
}
/// Writes a staff member the back office owns, creating or updating the row.
///
/// Deliberately skips [_assertPinIsAcceptable] and the duplicate-PIN check.
/// Those rules exist to stop *this terminal* from accepting a weak PIN
/// someone typed at the counter; they are not this terminal's to enforce on
/// a list the back office has already published. Applying them here would
/// mean a shop whose head office issued `1111` simply never receives its
/// staff, and the till falls back to seeded demo accounts — a worse outcome
/// than a guessable PIN.
///
/// [id] is derived from the server's `user_id` rather than minted, so a
/// second sign-in updates the same row instead of duplicating the person.
///
/// A null [pin] leaves an existing PIN alone, and on a new row stores an
/// unusable hash: the person appears on the staff list and can be attributed
/// bills, but nothing typed at the keypad will ever match them. That is the
/// honest representation of "the back office did not give us their PIN".
Future<StaffUser> upsertFromServer({
required String id,
required String name,
required StaffRole role,
String? pin,
bool isActive = true,
}) async {
final trimmed = name.trim().isEmpty ? 'Staff' : name.trim();
final now = DateTime.now().millisecondsSinceEpoch;
final existing = await findById(id);
// Minted once and reused for both columns. PinHasher.newSalt() is random,
// so calling it twice in one statement would store a hash the stored salt
// cannot reproduce.
final salt = PinHasher.newSalt();
if (existing != null) {
await _db.update(
Tables.staff,
{
'name': trimmed,
'role': role.name,
'is_active': isActive ? 1 : 0,
if (pin != null) ...{
'pin_hash': PinHasher.hash(pin, salt),
'pin_salt': salt,
'must_change_pin': 0,
},
'updated_at': now,
},
where: 'id = ?',
whereArgs: [id],
);
return StaffUser(id: id, name: trimmed, role: role, isActive: isActive);
}
// No PIN from the server means no PIN that can ever be entered: hashing a
// random value is how that is stored, rather than a sentinel a future
// reader might treat as "any PIN accepted".
final secret = pin ?? _uuid.v4();
await _db.insert(
Tables.staff,
{
'id': id,
'name': trimmed,
'role': role.name,
'pin_hash': PinHasher.hash(secret, salt),
'pin_salt': salt,
'must_change_pin': 0,
'is_active': isActive ? 1 : 0,
'created_at': now,
'updated_at': now,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
return StaffUser(id: id, name: trimmed, role: role, isActive: isActive);
}
/// A stable local id for a back-office user, so a second sign-in updates the
/// same row instead of duplicating the person.
static String serverId(int userId) => 'srv-$userId';
Future<void> updateDetails({
required String id,
String? name,