Build staff management, store details editing, and a forced PIN change

Wires the last two dead buttons in Settings and closes the loop on the
credential work: hashed PINs are only worth having if a shop can actually
change them.

Users & roles (Manage)
- Add, rename, re-role and remove staff. Admin-only at the door, because
  anyone who can edit staff can make themselves an admin.
- PIN and confirmation are both required and must match. There is no email to
  reset a PIN with, so a typo nobody can verify locks the account out until an
  admin intervenes.
- Editing someone leaves their PIN alone unless a new one is typed. An admin
  setting another person's PIN counts as a reset and re-arms must-change.
- Removal is a deactivation with a confirmation that explains why: bills
  already rung keep the cashier's name, so shift reports stay correct.
- Anyone still on a shipped PIN is flagged in the list and in Settings.

Store details (Edit)
- Name, address, GSTIN and phone now editable and persisted. GSTIN is format
  and state-code validated; it prints on every invoice as a legal requirement,
  so a typo is a compliance problem across hundreds of bills.
- Admin-only: changing the GSTIN changes what every future invoice claims
  about who collected the tax.

Forced PIN change
- Shown once after sign-in while must-change is set, and not dismissable. The
  seeded PINs are in the source of the build, so a terminal still running one
  is effectively unprotected.

Fixed while testing: the role dropdown laid its items out at natural width and
"Manager — Sales, inventory and reports" overflowed the dialog by 222px. Now
isExpanded with the description spelled out below, where it is readable.

Tests: 160 -> 168. Covers both role guards, the mismatched and too-short PIN
paths, the default-PIN flag, and GSTIN and seller-name validation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Suriya
2026-08-01 13:10:49 +05:30
parent e17937e8f1
commit 3513281a11
5 changed files with 950 additions and 6 deletions

View File

@@ -6,11 +6,13 @@ import '../../../core/services/barcode_service.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/theme/app_layout.dart';
import '../../auth/providers/auth_controller.dart';
import '../../modules/screens/customers_view.dart';
import '../../modules/screens/events_view.dart';
import '../../modules/screens/product_import_view.dart';
import '../../modules/screens/promos_view.dart';
import '../../modules/screens/settings_view.dart';
import '../../modules/widgets/staff_dialogs.dart';
import '../../sync/providers/sync_controller.dart';
import '../providers/cart_controller.dart';
import '../providers/catalog_providers.dart';
@@ -45,10 +47,18 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
final FocusNode _searchFocus = FocusNode();
late final BarcodeService _barcode;
/// Guards against re-opening the PIN dialog on every rebuild.
bool _promptedForPin = false;
@override
void initState() {
super.initState();
// Anyone still on a PIN this build shipped with is asked to choose their
// own before ringing a sale. Deferred to the first frame because it opens
// a dialog, which needs a Navigator that exists.
WidgetsBinding.instance.addPostFrameCallback((_) => _maybePromptForPin());
// The scanner behaves like a keyboard, so listen globally rather than
// depending on any one field holding focus. A scan from another module
// jumps back to billing, which is what a cashier expects.
@@ -65,6 +75,14 @@ class _PosDashboardScreenState extends ConsumerState<PosDashboardScreen> {
)..attach();
}
Future<void> _maybePromptForPin() async {
if (_promptedForPin || !mounted) return;
if (!ref.read(mustChangePinProvider)) return;
_promptedForPin = true;
await showForcedPinChange(context);
}
@override
void dispose() {
_barcode.dispose();