added login

This commit is contained in:
2026-08-07 17:07:49 +05:30
parent ad44402232
commit 4a2474ee6c
8 changed files with 908 additions and 383 deletions

View File

@@ -15,42 +15,91 @@ class AppRoutes {
static const String login = '/login';
/// The terminal itself. Signing in lands here directly — customer capture
/// happens at checkout, not before the sale.
/// The admin shell: billing plus catalogue import, promos, staff, settings.
static const String adminDashboard = '/admin';
/// The cashier shell: billing, and nothing else.
static const String cashierDashboard = '/cashier';
/// Alias for "wherever this session lives".
///
/// Kept because everything that returns to billing — finishing a receipt,
/// abandoning a payment — should land on the caller's own dashboard without
/// having to know which one that is. It never renders; [routerProvider]
/// resolves it to one of the two above.
static const String pos = '/';
static const String payment = '/payment';
static const String receipt = '/receipt';
/// Drawer count and hand-over. Reached from the session-end chooser, never
/// linked to directly, and guarded like every other signed-in route.
static const String endShift = '/end-shift';
static const String receipt = '/receipt';
/// Which dashboard a session owns.
///
/// The single place the role-to-screen decision is written down. `null` —
/// nobody signed in — resolves to the cashier till, which is the smaller of
/// the two; the guard sends an unauthenticated terminal to [login] before
/// this is ever reached.
static String homeFor(TerminalLogin? login) =>
login == TerminalLogin.admin ? adminDashboard : cashierDashboard;
/// True for the two dashboards, so the guard can spot a session sitting on
/// the wrong one.
static bool isDashboard(String location) =>
location == adminDashboard || location == cashierDashboard;
}
/// Router with an authentication guard.
/// Router with an authentication and role guard.
///
/// Every route except [AppRoutes.login] requires a signed-in store, and an
/// already-signed-in terminal is bounced away from the login screen.
/// Three rules, in order:
///
/// 1. Every route except [AppRoutes.login] requires a signed-in session.
/// 2. A signed-in terminal is bounced off the login screen onto its own
/// dashboard — admin for Admin/Supervisor/Manager/Owner accounts, cashier
/// for everything else. See [PosSession.isCashier].
/// 3. A session on the *other* role's dashboard is moved to its own. Typing
/// `/admin` on a cashier till must not open the back office, and the guard
/// is what makes that true regardless of how the route was reached.
final routerProvider = Provider<GoRouter>((ref) {
// GoRouter re-evaluates `redirect` whenever this notifier fires.
final authChanged = ValueNotifier<bool>(
ref.read(authControllerProvider).isAuthenticated,
// GoRouter re-evaluates `redirect` whenever this notifier fires. It carries
// the destination rather than a bare bool, so a role change — a cashier
// signing out and a supervisor signing in — also moves the terminal, which
// watching `isAuthenticated` alone would miss.
String? home(AuthState state) => state is Authenticated
? AppRoutes.homeFor(state.login)
: null;
final destination = ValueNotifier<String?>(
home(ref.read(authControllerProvider)),
);
ref.listen<AuthState>(
authControllerProvider,
(_, next) => authChanged.value = next.isAuthenticated,
(_, next) => destination.value = home(next),
);
ref.onDispose(authChanged.dispose);
ref.onDispose(destination.dispose);
return GoRouter(
initialLocation: AppRoutes.login,
refreshListenable: authChanged,
refreshListenable: destination,
debugLogDiagnostics: false,
redirect: (context, state) {
final signedIn = ref.read(authControllerProvider).isAuthenticated;
final atLogin = state.matchedLocation == AppRoutes.login;
final auth = ref.read(authControllerProvider);
final location = state.matchedLocation;
final atLogin = location == AppRoutes.login;
if (auth is! Authenticated) return atLogin ? null : AppRoutes.login;
final myHome = AppRoutes.homeFor(auth.login);
// Signed in and still on the login screen, or on the `/` alias.
if (atLogin || location == AppRoutes.pos) return myHome;
// On the other role's dashboard.
if (AppRoutes.isDashboard(location) && location != myHome) return myHome;
if (!signedIn) return atLogin ? null : AppRoutes.login;
if (atLogin) return AppRoutes.pos;
return null;
},
routes: [
@@ -59,12 +108,35 @@ final routerProvider = Provider<GoRouter>((ref) {
name: 'login',
pageBuilder: (context, state) => _fade(state, const LoginScreen()),
),
// Redirect-only. `/` is an alias, never a screen — the guard above has
// already resolved it, and this exists so the path matches a route at
// all rather than falling through to [errorBuilder].
GoRoute(
path: AppRoutes.pos,
name: 'pos',
name: 'home',
redirect: (context, state) => AppRoutes.homeFor(
ref.read(terminalLoginProvider) ?? TerminalLogin.cashier,
),
),
// Both dashboards are the same shell. It reads `isCashierModeProvider`
// and hides the sidebar, the catalogue and the back-office modules in
// cashier mode — so the two routes are the *addresses* of two shapes of
// one screen, not two screens to keep in step with each other.
GoRoute(
path: AppRoutes.adminDashboard,
name: 'adminDashboard',
pageBuilder: (context, state) =>
_fade(state, const PosDashboardScreen()),
),
GoRoute(
path: AppRoutes.cashierDashboard,
name: 'cashierDashboard',
pageBuilder: (context, state) =>
_fade(state, const PosDashboardScreen()),
),
GoRoute(
path: AppRoutes.endShift,
name: 'endShift',