folder repair
This commit is contained in:
parent
7da04ab49d
commit
cf948158d4
17
lib/app/app.dart
Normal file
17
lib/app/app.dart
Normal file
@ -0,0 +1,17 @@
|
||||
import 'package:cb_prestige_qr/app/theme/app_theme.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/presentation/views/login_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'CB Prestige Banking',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.dark(),
|
||||
home: const LoginView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,18 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:cb_prestige_qr/core/utils/ScanShell.dart';
|
||||
import 'package:cb_prestige_qr/core/presentation/widgets/global_loading_overlay.dart';
|
||||
import 'package:cb_prestige_qr/core/widgets/CenterNavButton.dart';
|
||||
import 'package:cb_prestige_qr/core/widgets/NavItem.dart';
|
||||
import 'package:cb_prestige_qr/features/analysis/presentation/pages/analysis_page.dart';
|
||||
import 'package:cb_prestige_qr/features/history/presentation/pages/history_page.dart';
|
||||
import 'package:cb_prestige_qr/features/home/presentation/pages/home.dart';
|
||||
import 'package:cb_prestige_qr/features/settings/presentation/pages/settings_page.dart';
|
||||
import 'package:cb_prestige_qr/core/widgets/center_nav_button.dart';
|
||||
import 'package:cb_prestige_qr/core/widgets/nav_item.dart';
|
||||
import 'package:cb_prestige_qr/features/analysis/presentation/views/analysis_view.dart';
|
||||
import 'package:cb_prestige_qr/features/history/presentation/views/history_view.dart';
|
||||
import 'package:cb_prestige_qr/features/home/presentation/views/home_view.dart';
|
||||
import 'package:cb_prestige_qr/features/scan/presentation/views/scan_flow.dart';
|
||||
import 'package:cb_prestige_qr/features/settings/presentation/views/settings_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
final navIndexNotifierProvider = NotifierProvider<NavIndexNotifier, int>(
|
||||
final navIndexProvider = NotifierProvider<NavIndexNotifier, int>(
|
||||
NavIndexNotifier.new,
|
||||
);
|
||||
|
||||
@ -39,7 +39,9 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
|
||||
void _startLoading([Duration duration = const Duration(seconds: 1)]) {
|
||||
_timer?.cancel();
|
||||
if (!_isLoading) setState(() => _isLoading = true);
|
||||
if (!_isLoading) {
|
||||
setState(() => _isLoading = true);
|
||||
}
|
||||
_timer = Timer(duration, () {
|
||||
if (!mounted) return;
|
||||
setState(() => _isLoading = false);
|
||||
@ -49,8 +51,10 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final initialIndex = ref.read(navIndexNotifierProvider);
|
||||
if (initialIndex != 0) _startLoading();
|
||||
final initialIndex = ref.read(navIndexProvider);
|
||||
if (initialIndex != 0) {
|
||||
_startLoading();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -61,21 +65,20 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedIndex = ref.watch(navIndexNotifierProvider);
|
||||
final selectedIndex = ref.watch(navIndexProvider);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
ref.listen<int>(navIndexNotifierProvider, (previous, next) {
|
||||
if (previous == null || previous == next) return;
|
||||
if (next == 0) return;
|
||||
ref.listen<int>(navIndexProvider, (previous, next) {
|
||||
if (previous == null || previous == next || next == 0) return;
|
||||
_startLoading();
|
||||
});
|
||||
|
||||
final pages = [
|
||||
const HomeView(),
|
||||
const AnalysisPage(),
|
||||
const SizedBox(),
|
||||
const HistoryPage(),
|
||||
const SettingsPage(),
|
||||
const pages = <Widget>[
|
||||
HomeView(),
|
||||
AnalysisView(),
|
||||
SizedBox(),
|
||||
HistoryView(),
|
||||
SettingsView(),
|
||||
];
|
||||
|
||||
return Stack(
|
||||
@ -113,18 +116,16 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
child: NavItem(
|
||||
icon: Icons.home_rounded,
|
||||
isActive: selectedIndex == 0,
|
||||
onTap: () => ref
|
||||
.read(navIndexNotifierProvider.notifier)
|
||||
.setIndex(0),
|
||||
onTap: () =>
|
||||
ref.read(navIndexProvider.notifier).setIndex(0),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: NavItem(
|
||||
icon: Icons.analytics,
|
||||
isActive: selectedIndex == 1,
|
||||
onTap: () => ref
|
||||
.read(navIndexNotifierProvider.notifier)
|
||||
.setIndex(1),
|
||||
onTap: () =>
|
||||
ref.read(navIndexProvider.notifier).setIndex(1),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@ -144,18 +145,16 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
child: NavItem(
|
||||
icon: Icons.history_rounded,
|
||||
isActive: selectedIndex == 3,
|
||||
onTap: () => ref
|
||||
.read(navIndexNotifierProvider.notifier)
|
||||
.setIndex(3),
|
||||
onTap: () =>
|
||||
ref.read(navIndexProvider.notifier).setIndex(3),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: NavItem(
|
||||
icon: Icons.person_rounded,
|
||||
isActive: selectedIndex == 4,
|
||||
onTap: () => ref
|
||||
.read(navIndexNotifierProvider.notifier)
|
||||
.setIndex(4),
|
||||
onTap: () =>
|
||||
ref.read(navIndexProvider.notifier).setIndex(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
48
lib/app/theme/app_theme.dart
Normal file
48
lib/app/theme/app_theme.dart
Normal file
@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
const AppTheme._();
|
||||
|
||||
static ThemeData dark() {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xff896e4b),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
scaffoldBackgroundColor: const Color(0xff25262b),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color(0xff896e4b),
|
||||
foregroundColor: Colors.white,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: const Color(0xff2a2b31),
|
||||
labelStyle: const TextStyle(color: Colors.white70),
|
||||
hintStyle: const TextStyle(color: Colors.white38),
|
||||
prefixIconColor: Colors.white70,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(color: Colors.white.withOpacity(0.06)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: const BorderSide(color: Color(0xff896e4b), width: 1.2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: const BorderSide(color: Colors.redAccent),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: const BorderSide(color: Colors.redAccent),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/core/network/api_client.dart
Normal file
18
lib/core/network/api_client.dart
Normal file
@ -0,0 +1,18 @@
|
||||
import 'package:cb_prestige_qr/config.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
class ApiClient {
|
||||
static Dio create() {
|
||||
return Dio(
|
||||
BaseOptions(
|
||||
baseUrl: AppConfig.apiBaseUrl,
|
||||
connectTimeout: const Duration(seconds: 15),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2,10 +2,7 @@ import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GlobalLoadingOverlay extends StatelessWidget {
|
||||
const GlobalLoadingOverlay({
|
||||
super.key,
|
||||
required this.isLoading,
|
||||
});
|
||||
const GlobalLoadingOverlay({super.key, required this.isLoading});
|
||||
|
||||
final bool isLoading;
|
||||
|
||||
@ -22,12 +19,8 @@ class GlobalLoadingOverlay extends StatelessWidget {
|
||||
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
ColoredBox(
|
||||
color: Colors.black.withOpacity(0.55),
|
||||
),
|
||||
const Center(
|
||||
child: _AnimatedLoader(),
|
||||
),
|
||||
ColoredBox(color: Colors.black.withOpacity(0.55)),
|
||||
const Center(child: _AnimatedLoader()),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -59,30 +52,15 @@ class _AnimatedLoaderState extends State<_AnimatedLoader>
|
||||
|
||||
/// Main fade (smooth breathing)
|
||||
_mainOpacity = TweenSequence<double>([
|
||||
TweenSequenceItem(
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
weight: 50,
|
||||
),
|
||||
TweenSequenceItem(
|
||||
tween: Tween(begin: 1.0, end: 0.0),
|
||||
weight: 50,
|
||||
),
|
||||
]).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 50),
|
||||
TweenSequenceItem(tween: Tween(begin: 1.0, end: 0.0), weight: 50),
|
||||
]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||
|
||||
/// Glow fade (slightly offset feel)
|
||||
_glowOpacity = Tween<double>(
|
||||
begin: 0.2,
|
||||
end: 0.5,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
15
lib/core/providers/core_providers.dart
Normal file
15
lib/core/providers/core_providers.dart
Normal file
@ -0,0 +1,15 @@
|
||||
import 'package:cb_prestige_qr/core/network/api_client.dart';
|
||||
import 'package:cb_prestige_qr/core/services/device_id_service.dart';
|
||||
import 'package:cb_prestige_qr/core/storage/local_storage_service.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
final localStorageServiceProvider = Provider<LocalStorageService>(
|
||||
(ref) => LocalStorageService(),
|
||||
);
|
||||
|
||||
final deviceIdServiceProvider = Provider<DeviceIdService>(
|
||||
(ref) => DeviceIdService(),
|
||||
);
|
||||
|
||||
final dioProvider = Provider<Dio>((ref) => ApiClient.create());
|
||||
@ -1,16 +1,14 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class DeviceIdService {
|
||||
static const _channel = MethodChannel('cb_prestige_qr/device');
|
||||
|
||||
Future<String> getDeviceId() async {
|
||||
// final deviceId = await _channel.invokeMethod<String>('getDeviceId');
|
||||
// final deviceId = await const MethodChannel(
|
||||
// 'cb_prestige_qr/device',
|
||||
// ).invokeMethod<String>('getDeviceId');
|
||||
// if (deviceId == null || deviceId.trim().isEmpty) {
|
||||
// throw PlatformException(
|
||||
// code: 'device_id_unavailable',
|
||||
// message: 'Unable to retrieve device ID.',
|
||||
// );
|
||||
// }
|
||||
return "demo_device_id";
|
||||
return 'demo_device_id';
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,4 +20,9 @@ class LocalStorageService {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(key);
|
||||
}
|
||||
|
||||
Future<void> remove(String key) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CenterNavButton extends StatelessWidget {
|
||||
final bool isActive;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const CenterNavButton({
|
||||
super.key,
|
||||
required this.isActive,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool isActive;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@ -1,10 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NavItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final bool isActive;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const NavItem({
|
||||
super.key,
|
||||
required this.icon,
|
||||
@ -12,6 +8,10 @@ class NavItem extends StatelessWidget {
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final bool isActive;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@ -40,7 +40,7 @@ class AnalysisSummaryUiModel {
|
||||
|
||||
@immutable
|
||||
class AnalysisUiState {
|
||||
AnalysisUiState({
|
||||
const AnalysisUiState({
|
||||
int? rangeDays,
|
||||
String? rangeLabel,
|
||||
AnalysisSummaryUiModel? summary,
|
||||
@ -62,7 +62,6 @@ class AnalysisUiState {
|
||||
final String? rangeLabel;
|
||||
final AnalysisSummaryUiModel? summary;
|
||||
final List<AnalysisSeriesPointUiModel>? series;
|
||||
|
||||
final double? averageScansPerDay;
|
||||
final double? averagePointsPerDay;
|
||||
final double? pointsPerScan;
|
||||
@ -9,12 +9,12 @@ import 'analysis_chart_metric.dart';
|
||||
import 'analysis_range.dart';
|
||||
import 'analysis_ui_state.dart';
|
||||
|
||||
final analysisRangeNotifierProvider =
|
||||
final analysisRangeProvider =
|
||||
NotifierProvider<AnalysisRangeNotifier, AnalysisRangePreset>(
|
||||
AnalysisRangeNotifier.new,
|
||||
);
|
||||
|
||||
final analysisChartMetricNotifierProvider =
|
||||
final analysisChartMetricProvider =
|
||||
NotifierProvider<AnalysisChartMetricNotifier, AnalysisChartMetric>(
|
||||
AnalysisChartMetricNotifier.new,
|
||||
);
|
||||
@ -53,7 +53,7 @@ final analysisViewModelProvider =
|
||||
class AnalysisViewModel extends AsyncNotifier<AnalysisUiState> {
|
||||
@override
|
||||
Future<AnalysisUiState> build() async {
|
||||
final rangePreset = ref.watch(analysisRangeNotifierProvider);
|
||||
final rangePreset = ref.watch(analysisRangeProvider);
|
||||
final content = await ref.watch(_getAnalysisContentProvider)(
|
||||
rangeDays: rangePreset.days,
|
||||
);
|
||||
@ -1,20 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../manager/analysis_chart_metric.dart';
|
||||
import '../manager/analysis_range.dart';
|
||||
import '../manager/analysis_ui_state.dart';
|
||||
import '../manager/analysis_view_model.dart';
|
||||
import '../viewmodels/analysis_chart_metric.dart';
|
||||
import '../viewmodels/analysis_range.dart';
|
||||
import '../viewmodels/analysis_ui_state.dart';
|
||||
import '../viewmodels/analysis_view_model.dart';
|
||||
import '../widgets/analysis_line_chart.dart';
|
||||
|
||||
class AnalysisPage extends ConsumerWidget {
|
||||
const AnalysisPage({super.key});
|
||||
class AnalysisView extends ConsumerWidget {
|
||||
const AnalysisView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final rangePreset = ref.watch(analysisRangeNotifierProvider);
|
||||
final chartMetric = ref.watch(analysisChartMetricNotifierProvider);
|
||||
final rangePreset = ref.watch(analysisRangeProvider);
|
||||
final chartMetric = ref.watch(analysisChartMetricProvider);
|
||||
final stateAsync = ref.watch(analysisViewModelProvider);
|
||||
|
||||
return Scaffold(
|
||||
@ -34,9 +34,8 @@ class AnalysisPage extends ConsumerWidget {
|
||||
children: [
|
||||
_RangeSelector(
|
||||
selected: rangePreset,
|
||||
onSelected: (preset) => ref
|
||||
.read(analysisRangeNotifierProvider.notifier)
|
||||
.setRange(preset),
|
||||
onSelected: (preset) =>
|
||||
ref.read(analysisRangeProvider.notifier).setRange(preset),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
stateAsync.when(
|
||||
@ -44,7 +43,7 @@ class AnalysisPage extends ConsumerWidget {
|
||||
state: state,
|
||||
chartMetric: chartMetric,
|
||||
onMetricSelected: (metric) => ref
|
||||
.read(analysisChartMetricNotifierProvider.notifier)
|
||||
.read(analysisChartMetricProvider.notifier)
|
||||
.setMetric(metric),
|
||||
),
|
||||
loading: () => const _LoadingBody(),
|
||||
@ -0,0 +1,72 @@
|
||||
import 'package:cb_prestige_qr/core/storage/local_storage_service.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/domain/entities/user.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/domain/entities/user_profile.dart';
|
||||
|
||||
abstract class AuthLocalDataSource {
|
||||
Future<void> saveSession(User user);
|
||||
Future<UserProfile?> getSavedUserProfile();
|
||||
Future<void> clearSession();
|
||||
}
|
||||
|
||||
class AuthLocalDataSourceImpl implements AuthLocalDataSource {
|
||||
AuthLocalDataSourceImpl(this._storage);
|
||||
|
||||
final LocalStorageService _storage;
|
||||
|
||||
static const _usernameKey = 'logged_in_username';
|
||||
static const _displayNameKey = 'logged_in_display_name';
|
||||
static const _roleKey = 'logged_in_role';
|
||||
static const _branchKey = 'logged_in_branch';
|
||||
static const _tokenKey = 'logged_in_token';
|
||||
|
||||
@override
|
||||
Future<void> saveSession(User user) async {
|
||||
await _storage.setString(_usernameKey, user.username);
|
||||
await _storage.setString(_displayNameKey, user.displayName);
|
||||
await _storage.setString(_roleKey, user.roleLabel);
|
||||
await _storage.setString(_branchKey, user.branchLabel);
|
||||
await _storage.setString(_tokenKey, user.authToken);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserProfile?> getSavedUserProfile() async {
|
||||
final username = await _storage.getString(_usernameKey);
|
||||
if (username == null || username.trim().isEmpty) return null;
|
||||
|
||||
return UserProfile(
|
||||
username: username,
|
||||
displayName:
|
||||
await _storage.getString(_displayNameKey) ??
|
||||
_displayNameFromUsername(username),
|
||||
roleLabel: await _storage.getString(_roleKey) ?? 'Cashier',
|
||||
branchLabel: await _storage.getString(_branchKey) ?? 'Prestige Counter',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearSession() async {
|
||||
await _storage.remove(_usernameKey);
|
||||
await _storage.remove(_displayNameKey);
|
||||
await _storage.remove(_roleKey);
|
||||
await _storage.remove(_branchKey);
|
||||
await _storage.remove(_tokenKey);
|
||||
}
|
||||
|
||||
static String _displayNameFromUsername(String username) {
|
||||
final words = username
|
||||
.replaceAll(RegExp(r'[._-]+'), ' ')
|
||||
.trim()
|
||||
.split(RegExp(r'\s+'))
|
||||
.where((word) => word.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
if (words.isEmpty) return 'Cashier';
|
||||
|
||||
return words.map(_capitalize).join(' ');
|
||||
}
|
||||
|
||||
static String _capitalize(String value) {
|
||||
if (value.isEmpty) return value;
|
||||
if (value.length == 1) return value.toUpperCase();
|
||||
return '${value.substring(0, 1).toUpperCase()}${value.substring(1)}';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
import 'package:cb_prestige_qr/features/auth/data/models/login_request_model.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/data/models/login_response_model.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/domain/entities/user.dart';
|
||||
|
||||
abstract class AuthRemoteDataSource {
|
||||
Future<LoginResponseModel> login(LoginRequestModel request);
|
||||
}
|
||||
|
||||
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
||||
const AuthRemoteDataSourceImpl();
|
||||
|
||||
@override
|
||||
Future<LoginResponseModel> login(LoginRequestModel request) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 800));
|
||||
|
||||
final username = request.username.trim();
|
||||
if (username.isEmpty || request.password.trim().isEmpty) {
|
||||
throw Exception('Username and password are required.');
|
||||
}
|
||||
|
||||
final user = User(
|
||||
username: username,
|
||||
displayName: _displayNameFromUsername(username),
|
||||
roleLabel: 'Cashier',
|
||||
branchLabel: 'Prestige Counter',
|
||||
authToken: 'demo-token-${request.deviceId}-$username',
|
||||
);
|
||||
|
||||
return LoginResponseModel(user: user, token: user.authToken);
|
||||
}
|
||||
|
||||
static String _displayNameFromUsername(String username) {
|
||||
final words = username
|
||||
.replaceAll(RegExp(r'[._-]+'), ' ')
|
||||
.trim()
|
||||
.split(RegExp(r'\s+'))
|
||||
.where((word) => word.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
if (words.isEmpty) return 'Cashier';
|
||||
return words.map(_capitalize).join(' ');
|
||||
}
|
||||
|
||||
static String _capitalize(String value) {
|
||||
if (value.isEmpty) return value;
|
||||
if (value.length == 1) return value.toUpperCase();
|
||||
return '${value.substring(0, 1).toUpperCase()}${value.substring(1)}';
|
||||
}
|
||||
}
|
||||
19
lib/features/auth/data/models/login_request_model.dart
Normal file
19
lib/features/auth/data/models/login_request_model.dart
Normal file
@ -0,0 +1,19 @@
|
||||
class LoginRequestModel {
|
||||
const LoginRequestModel({
|
||||
required this.username,
|
||||
required this.password,
|
||||
required this.deviceId,
|
||||
});
|
||||
|
||||
final String username;
|
||||
final String password;
|
||||
final String deviceId;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'username': username,
|
||||
'password': password,
|
||||
'device_id': deviceId,
|
||||
};
|
||||
}
|
||||
}
|
||||
8
lib/features/auth/data/models/login_response_model.dart
Normal file
8
lib/features/auth/data/models/login_response_model.dart
Normal file
@ -0,0 +1,8 @@
|
||||
import 'package:cb_prestige_qr/features/auth/domain/entities/user.dart';
|
||||
|
||||
class LoginResponseModel {
|
||||
const LoginResponseModel({required this.user, required this.token});
|
||||
|
||||
final User user;
|
||||
final String token;
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
import 'package:cb_prestige_qr/core/services/device_id_service.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/data/data_sources/auth_local_data_source.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/data/data_sources/auth_remote_data_source.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/data/models/login_request_model.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/domain/entities/user_profile.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/domain/repositories/auth_repository.dart';
|
||||
|
||||
class AuthRepositoryImpl implements AuthRepository {
|
||||
const AuthRepositoryImpl(
|
||||
this._remoteDataSource,
|
||||
this._localDataSource,
|
||||
this._deviceIdService,
|
||||
);
|
||||
|
||||
final AuthRemoteDataSource _remoteDataSource;
|
||||
final AuthLocalDataSource _localDataSource;
|
||||
final DeviceIdService _deviceIdService;
|
||||
|
||||
@override
|
||||
Future<UserProfile> login({
|
||||
required String username,
|
||||
required String password,
|
||||
}) async {
|
||||
final deviceId = await _deviceIdService.getDeviceId();
|
||||
final response = await _remoteDataSource.login(
|
||||
LoginRequestModel(
|
||||
username: username,
|
||||
password: password,
|
||||
deviceId: deviceId,
|
||||
),
|
||||
);
|
||||
|
||||
await _localDataSource.saveSession(response.user);
|
||||
|
||||
return UserProfile(
|
||||
username: response.user.username,
|
||||
displayName: response.user.displayName,
|
||||
roleLabel: response.user.roleLabel,
|
||||
branchLabel: response.user.branchLabel,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserProfile?> getCurrentUserProfile() {
|
||||
return _localDataSource.getSavedUserProfile();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> logout() => _localDataSource.clearSession();
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
import 'package:cb_prestige_qr/core/storage/local_storage_service.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/domain/user_profile.dart';
|
||||
|
||||
class UserProfileRepository {
|
||||
UserProfileRepository(this._storage);
|
||||
|
||||
final LocalStorageService _storage;
|
||||
|
||||
static const _usernameKey = 'logged_in_username';
|
||||
static const _displayNameKey = 'logged_in_display_name';
|
||||
static const _roleKey = 'logged_in_role';
|
||||
static const _branchKey = 'logged_in_branch';
|
||||
|
||||
Future<UserProfile> load() async {
|
||||
final username = await _storage.getString(_usernameKey) ?? 'cashier';
|
||||
final displayName =
|
||||
await _storage.getString(_displayNameKey) ??
|
||||
_displayNameFromUsername(username);
|
||||
final roleLabel = await _storage.getString(_roleKey) ?? 'Cashier';
|
||||
final branchLabel =
|
||||
await _storage.getString(_branchKey) ?? 'Prestige Counter';
|
||||
|
||||
return UserProfile(
|
||||
username: username,
|
||||
displayName: displayName,
|
||||
roleLabel: roleLabel,
|
||||
branchLabel: branchLabel,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> saveFromUsername(String username) async {
|
||||
final normalizedUsername = username.trim();
|
||||
final safeUsername = normalizedUsername.isEmpty
|
||||
? 'cashier'
|
||||
: normalizedUsername;
|
||||
|
||||
await _storage.setString(_usernameKey, safeUsername);
|
||||
await _storage.setString(
|
||||
_displayNameKey,
|
||||
_displayNameFromUsername(safeUsername),
|
||||
);
|
||||
await _storage.setString(_roleKey, 'Cashier');
|
||||
await _storage.setString(_branchKey, 'Prestige Counter');
|
||||
}
|
||||
|
||||
static String _displayNameFromUsername(String username) {
|
||||
final words = username
|
||||
.replaceAll(RegExp(r'[._-]+'), ' ')
|
||||
.trim()
|
||||
.split(RegExp(r'\s+'))
|
||||
.where((word) => word.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
if (words.isEmpty) return 'Cashier';
|
||||
|
||||
return words.map(_capitalize).join(' ');
|
||||
}
|
||||
|
||||
static String _capitalize(String value) {
|
||||
if (value.isEmpty) return value;
|
||||
if (value.length == 1) return value.toUpperCase();
|
||||
return '${value.substring(0, 1).toUpperCase()}${value.substring(1)}';
|
||||
}
|
||||
}
|
||||
38
lib/features/auth/di/auth_providers.dart
Normal file
38
lib/features/auth/di/auth_providers.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'package:cb_prestige_qr/core/providers/core_providers.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/data/data_sources/auth_local_data_source.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/data/data_sources/auth_remote_data_source.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/data/repositories/auth_repository_impl.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/domain/repositories/auth_repository.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/domain/use_cases/get_current_user_profile_use_case.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/domain/use_cases/login_use_case.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/domain/use_cases/logout_use_case.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
final authLocalDataSourceProvider = Provider<AuthLocalDataSource>(
|
||||
(ref) => AuthLocalDataSourceImpl(ref.watch(localStorageServiceProvider)),
|
||||
);
|
||||
|
||||
final authRemoteDataSourceProvider = Provider<AuthRemoteDataSource>(
|
||||
(ref) => const AuthRemoteDataSourceImpl(),
|
||||
);
|
||||
|
||||
final authRepositoryProvider = Provider<AuthRepository>(
|
||||
(ref) => AuthRepositoryImpl(
|
||||
ref.watch(authRemoteDataSourceProvider),
|
||||
ref.watch(authLocalDataSourceProvider),
|
||||
ref.watch(deviceIdServiceProvider),
|
||||
),
|
||||
);
|
||||
|
||||
final loginUseCaseProvider = Provider<LoginUseCase>(
|
||||
(ref) => LoginUseCase(ref.watch(authRepositoryProvider)),
|
||||
);
|
||||
|
||||
final getCurrentUserProfileUseCaseProvider =
|
||||
Provider<GetCurrentUserProfileUseCase>(
|
||||
(ref) => GetCurrentUserProfileUseCase(ref.watch(authRepositoryProvider)),
|
||||
);
|
||||
|
||||
final logoutUseCaseProvider = Provider<LogoutUseCase>(
|
||||
(ref) => LogoutUseCase(ref.watch(authRepositoryProvider)),
|
||||
);
|
||||
15
lib/features/auth/domain/entities/user.dart
Normal file
15
lib/features/auth/domain/entities/user.dart
Normal file
@ -0,0 +1,15 @@
|
||||
class User {
|
||||
const User({
|
||||
required this.username,
|
||||
required this.displayName,
|
||||
required this.roleLabel,
|
||||
required this.branchLabel,
|
||||
required this.authToken,
|
||||
});
|
||||
|
||||
final String username;
|
||||
final String displayName;
|
||||
final String roleLabel;
|
||||
final String branchLabel;
|
||||
final String authToken;
|
||||
}
|
||||
12
lib/features/auth/domain/repositories/auth_repository.dart
Normal file
12
lib/features/auth/domain/repositories/auth_repository.dart
Normal file
@ -0,0 +1,12 @@
|
||||
import '../entities/user_profile.dart';
|
||||
|
||||
abstract class AuthRepository {
|
||||
Future<UserProfile> login({
|
||||
required String username,
|
||||
required String password,
|
||||
});
|
||||
|
||||
Future<UserProfile?> getCurrentUserProfile();
|
||||
|
||||
Future<void> logout();
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import '../entities/user_profile.dart';
|
||||
import '../repositories/auth_repository.dart';
|
||||
|
||||
class GetCurrentUserProfileUseCase {
|
||||
const GetCurrentUserProfileUseCase(this._repository);
|
||||
|
||||
final AuthRepository _repository;
|
||||
|
||||
Future<UserProfile?> call() => _repository.getCurrentUserProfile();
|
||||
}
|
||||
15
lib/features/auth/domain/use_cases/login_use_case.dart
Normal file
15
lib/features/auth/domain/use_cases/login_use_case.dart
Normal file
@ -0,0 +1,15 @@
|
||||
import '../entities/user_profile.dart';
|
||||
import '../repositories/auth_repository.dart';
|
||||
|
||||
class LoginUseCase {
|
||||
const LoginUseCase(this._repository);
|
||||
|
||||
final AuthRepository _repository;
|
||||
|
||||
Future<UserProfile> call({
|
||||
required String username,
|
||||
required String password,
|
||||
}) {
|
||||
return _repository.login(username: username, password: password);
|
||||
}
|
||||
}
|
||||
9
lib/features/auth/domain/use_cases/logout_use_case.dart
Normal file
9
lib/features/auth/domain/use_cases/logout_use_case.dart
Normal file
@ -0,0 +1,9 @@
|
||||
import '../repositories/auth_repository.dart';
|
||||
|
||||
class LogoutUseCase {
|
||||
const LogoutUseCase(this._repository);
|
||||
|
||||
final AuthRepository _repository;
|
||||
|
||||
Future<void> call() => _repository.logout();
|
||||
}
|
||||
38
lib/features/auth/presentation/viewmodels/login_state.dart
Normal file
38
lib/features/auth/presentation/viewmodels/login_state.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
@immutable
|
||||
class LoginState {
|
||||
const LoginState({
|
||||
this.username = '',
|
||||
this.password = '',
|
||||
this.obscurePassword = true,
|
||||
this.isSubmitting = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final String username;
|
||||
final String password;
|
||||
final bool obscurePassword;
|
||||
final bool isSubmitting;
|
||||
final String? errorMessage;
|
||||
|
||||
static const _sentinel = Object();
|
||||
|
||||
LoginState copyWith({
|
||||
String? username,
|
||||
String? password,
|
||||
bool? obscurePassword,
|
||||
bool? isSubmitting,
|
||||
Object? errorMessage = _sentinel,
|
||||
}) {
|
||||
return LoginState(
|
||||
username: username ?? this.username,
|
||||
password: password ?? this.password,
|
||||
obscurePassword: obscurePassword ?? this.obscurePassword,
|
||||
isSubmitting: isSubmitting ?? this.isSubmitting,
|
||||
errorMessage: identical(errorMessage, _sentinel)
|
||||
? this.errorMessage
|
||||
: errorMessage as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
import 'package:cb_prestige_qr/features/auth/di/auth_providers.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import 'login_state.dart';
|
||||
|
||||
final loginViewModelProvider = NotifierProvider<LoginViewModel, LoginState>(
|
||||
LoginViewModel.new,
|
||||
);
|
||||
|
||||
class LoginViewModel extends Notifier<LoginState> {
|
||||
@override
|
||||
LoginState build() => const LoginState();
|
||||
|
||||
void updateUsername(String value) {
|
||||
state = state.copyWith(username: value, errorMessage: null);
|
||||
}
|
||||
|
||||
void updatePassword(String value) {
|
||||
state = state.copyWith(password: value, errorMessage: null);
|
||||
}
|
||||
|
||||
void togglePasswordVisibility() {
|
||||
state = state.copyWith(obscurePassword: !state.obscurePassword);
|
||||
}
|
||||
|
||||
Future<bool> submit() async {
|
||||
if (state.isSubmitting) return false;
|
||||
|
||||
state = state.copyWith(isSubmitting: true, errorMessage: null);
|
||||
|
||||
try {
|
||||
await ref.read(loginUseCaseProvider)(
|
||||
username: state.username.trim(),
|
||||
password: state.password,
|
||||
);
|
||||
state = state.copyWith(isSubmitting: false);
|
||||
return true;
|
||||
} catch (error) {
|
||||
state = state.copyWith(
|
||||
isSubmitting: false,
|
||||
errorMessage: error.toString().replaceFirst('Exception: ', ''),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,50 +1,24 @@
|
||||
import 'package:cb_prestige_qr/core/storage/local_storage_service.dart';
|
||||
import 'package:cb_prestige_qr/core/utils/MainShell.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/data/user_profile_repository.dart';
|
||||
import 'package:cb_prestige_qr/app/navigation/main_shell.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/presentation/viewmodels/login_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
class LoginView extends ConsumerStatefulWidget {
|
||||
const LoginView({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
ConsumerState<LoginView> createState() => _LoginViewState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
class _LoginViewState extends ConsumerState<LoginView> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final UserProfileRepository _userProfileRepository = UserProfileRepository(
|
||||
LocalStorageService(),
|
||||
);
|
||||
bool _isSigningIn = false;
|
||||
|
||||
var _obscurePassword = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_usernameController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
final form = _formKey.currentState;
|
||||
if (form == null || !form.validate()) return;
|
||||
|
||||
setState(() {
|
||||
_isSigningIn = true;
|
||||
});
|
||||
|
||||
await Future<void>.delayed(const Duration(seconds: 2));
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
await _userProfileRepository.saveFromUsername(
|
||||
_usernameController.text.trim(),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
final didLogin = await ref.read(loginViewModelProvider.notifier).submit();
|
||||
if (!didLogin || !mounted) return;
|
||||
|
||||
Navigator.of(
|
||||
context,
|
||||
@ -55,6 +29,8 @@ class _LoginPageState extends State<LoginPage> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final state = ref.watch(loginViewModelProvider);
|
||||
final viewModel = ref.read(loginViewModelProvider.notifier);
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
@ -185,11 +161,12 @@ class _LoginPageState extends State<LoginPage> {
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
_AuthTextField(
|
||||
controller: _usernameController,
|
||||
initialValue: state.username,
|
||||
label: 'Username',
|
||||
hintText: 'Enter your username',
|
||||
keyboardType: TextInputType.text,
|
||||
prefixIcon: Icons.person_rounded,
|
||||
onChanged: viewModel.updateUsername,
|
||||
validator: (value) {
|
||||
final username = value?.trim() ?? '';
|
||||
if (username.isEmpty) {
|
||||
@ -203,11 +180,12 @@ class _LoginPageState extends State<LoginPage> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_AuthTextField(
|
||||
controller: _passwordController,
|
||||
initialValue: state.password,
|
||||
label: 'Password',
|
||||
hintText: 'Enter your password',
|
||||
prefixIcon: Icons.lock_rounded,
|
||||
obscureText: _obscurePassword,
|
||||
obscureText: state.obscurePassword,
|
||||
onChanged: viewModel.updatePassword,
|
||||
validator: (value) {
|
||||
final password = value ?? '';
|
||||
if (password.isEmpty) {
|
||||
@ -219,13 +197,9 @@ class _LoginPageState extends State<LoginPage> {
|
||||
return null;
|
||||
},
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscurePassword = !_obscurePassword;
|
||||
});
|
||||
},
|
||||
onPressed: viewModel.togglePasswordVisibility,
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
state.obscurePassword
|
||||
? Icons.visibility_off_rounded
|
||||
: Icons.visibility_rounded,
|
||||
color: Colors.white70,
|
||||
@ -240,11 +214,21 @@ class _LoginPageState extends State<LoginPage> {
|
||||
child: const Text('Forgot password?'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (state.errorMessage != null) ...[
|
||||
Text(
|
||||
state.errorMessage!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.error,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
] else
|
||||
const SizedBox(height: 6),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: _isSigningIn ? null : _submit,
|
||||
onPressed: state.isSubmitting ? null : _submit,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
@ -253,7 +237,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
),
|
||||
child: _isSigningIn
|
||||
child: state.isSubmitting
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
@ -288,21 +272,23 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
class _AuthTextField extends StatelessWidget {
|
||||
const _AuthTextField({
|
||||
required this.controller,
|
||||
required this.initialValue,
|
||||
required this.label,
|
||||
required this.hintText,
|
||||
required this.prefixIcon,
|
||||
required this.validator,
|
||||
required this.onChanged,
|
||||
this.keyboardType,
|
||||
this.obscureText = false,
|
||||
this.suffixIcon,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String initialValue;
|
||||
final String label;
|
||||
final String hintText;
|
||||
final IconData prefixIcon;
|
||||
final String? Function(String?) validator;
|
||||
final ValueChanged<String> onChanged;
|
||||
final TextInputType? keyboardType;
|
||||
final bool obscureText;
|
||||
final Widget? suffixIcon;
|
||||
@ -310,10 +296,11 @@ class _AuthTextField extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
initialValue: initialValue,
|
||||
keyboardType: keyboardType,
|
||||
obscureText: obscureText,
|
||||
validator: validator,
|
||||
onChanged: onChanged,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../manager/history_ui_state.dart';
|
||||
import '../manager/history_view_model.dart';
|
||||
import '../viewmodels/history_ui_state.dart';
|
||||
import '../viewmodels/history_view_model.dart';
|
||||
import '../widgets/history_section_card.dart';
|
||||
|
||||
class HistoryPage extends ConsumerWidget {
|
||||
const HistoryPage({super.key});
|
||||
class HistoryView extends ConsumerWidget {
|
||||
const HistoryView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../manager/history_ui_state.dart';
|
||||
import '../viewmodels/history_ui_state.dart';
|
||||
import 'history_item_tile.dart';
|
||||
|
||||
class HistorySectionCard extends StatelessWidget {
|
||||
@ -57,7 +57,7 @@ class HistorySectionCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'${section.totalTransactionsLabel} • ${section.totalAmountLabel}',
|
||||
'${section.totalTransactionsLabel} | ${section.totalAmountLabel}',
|
||||
style: textTheme.labelMedium?.copyWith(
|
||||
color: colorScheme.onPrimary.withOpacity(0.9),
|
||||
fontWeight: FontWeight.w700,
|
||||
|
||||
@ -28,4 +28,3 @@ class HomeLocalDataSourceImpl implements HomeLocalDataSource {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
import '../../domain/entities/recent_scan.dart';
|
||||
|
||||
class RecentScanModel extends RecentScan {
|
||||
const RecentScanModel({
|
||||
required super.title,
|
||||
required super.subtitle,
|
||||
});
|
||||
const RecentScanModel({required super.title, required super.subtitle});
|
||||
}
|
||||
|
||||
|
||||
@ -18,4 +18,3 @@ class HomeRepositoryImpl implements HomeRepository {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
import 'recent_scan.dart';
|
||||
|
||||
class HomeContent {
|
||||
const HomeContent({
|
||||
required this.carouselAssets,
|
||||
required this.recentScans,
|
||||
});
|
||||
const HomeContent({required this.carouselAssets, required this.recentScans});
|
||||
|
||||
final List<String> carouselAssets;
|
||||
final List<RecentScan> recentScans;
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
class RecentScan {
|
||||
const RecentScan({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
});
|
||||
const RecentScan({required this.title, required this.subtitle});
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
}
|
||||
|
||||
|
||||
@ -3,4 +3,3 @@ import '../entities/home_content.dart';
|
||||
abstract class HomeRepository {
|
||||
Future<HomeContent> getHomeContent({int recentLimit = 5});
|
||||
}
|
||||
|
||||
|
||||
@ -10,4 +10,3 @@ class GetHomeContent {
|
||||
return _repository.getHomeContent(recentLimit: recentLimit);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -42,11 +42,7 @@ class HomeViewModel extends AsyncNotifier<HomeUiState> {
|
||||
);
|
||||
}
|
||||
|
||||
void onSeeAllTapped() {
|
||||
// TODO: navigate to full list
|
||||
}
|
||||
void onSeeAllTapped() {}
|
||||
|
||||
void onRecentScanTapped(int index) {
|
||||
// TODO: navigate to details
|
||||
}
|
||||
void onRecentScanTapped(int index) {}
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:cb_prestige_qr/core/presentation/widgets/global_loading_overlay.dart';
|
||||
import 'package:cb_prestige_qr/features/history/presentation/widgets/history_item_tile.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../../../core/presentation/widgets/global_loading_overlay.dart';
|
||||
import '../manager/home_ui_state.dart';
|
||||
import '../manager/home_view_model.dart';
|
||||
import '../viewmodels/home_ui_state.dart';
|
||||
import '../viewmodels/home_view_model.dart';
|
||||
import '../widgets/home_today_summary_section.dart';
|
||||
|
||||
class HomeView extends ConsumerWidget {
|
||||
@ -39,17 +39,17 @@ class HomeView extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
body: stateAsync.when(
|
||||
data: (state) => _HomeBody(
|
||||
state: state,
|
||||
onSeeAll: viewModel.onSeeAllTapped,
|
||||
onRecentScanTap: viewModel.onRecentScanTapped,
|
||||
),
|
||||
loading: () => const GlobalLoadingOverlay(isLoading: true),
|
||||
error: (error, stackTrace) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
body: stateAsync.when(
|
||||
data: (state) => _HomeBody(
|
||||
state: state,
|
||||
onSeeAll: viewModel.onSeeAllTapped,
|
||||
onRecentScanTap: viewModel.onRecentScanTapped,
|
||||
),
|
||||
loading: () => const GlobalLoadingOverlay(isLoading: true),
|
||||
error: (error, stackTrace) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Failed to load Home'),
|
||||
@ -80,8 +80,7 @@ class _HomeBody extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shellBottomInset =
|
||||
70 + MediaQuery.of(context).padding.bottom;
|
||||
final shellBottomInset = 70 + MediaQuery.of(context).padding.bottom;
|
||||
final contentBottomPadding = shellBottomInset + 8;
|
||||
|
||||
return CustomScrollView(
|
||||
@ -84,7 +84,6 @@ class HomeContextSection extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
hasCustomer
|
||||
|
||||
@ -164,4 +164,3 @@ class _MiniStat extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
import 'package:cb_prestige_qr/features/scan/domain/entities/scan_submit_payload.dart';
|
||||
|
||||
abstract interface class ScanRemoteDataSource {
|
||||
Future<Map<String, dynamic>> submitScan({
|
||||
required ScanSubmitPayload payload,
|
||||
});
|
||||
Future<Map<String, dynamic>> submitScan({required ScanSubmitPayload payload});
|
||||
}
|
||||
|
||||
|
||||
@ -12,4 +12,3 @@ class FakeScanRemoteDataSource implements ScanRemoteDataSource {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -47,9 +47,7 @@ class HttpScanRemoteDataSource implements ScanRemoteDataSource {
|
||||
throw const FormatException('QR scan response must be a JSON object.');
|
||||
}
|
||||
|
||||
final data = decoded.map(
|
||||
(key, value) => MapEntry(key.toString(), value),
|
||||
);
|
||||
final data = decoded.map((key, value) => MapEntry(key.toString(), value));
|
||||
final isOk = data['OK'] == true || data['ok'] == true;
|
||||
if (!isOk) {
|
||||
throw HttpException(
|
||||
@ -78,7 +76,9 @@ class HttpScanRemoteDataSource implements ScanRemoteDataSource {
|
||||
|
||||
String _buildStatusMessage(int statusCode, String responseBody) {
|
||||
final normalizedBody = responseBody.trim();
|
||||
final bodySuffix = normalizedBody.isEmpty ? '' : ' Response: $normalizedBody';
|
||||
final bodySuffix = normalizedBody.isEmpty
|
||||
? ''
|
||||
: ' Response: $normalizedBody';
|
||||
|
||||
return switch (statusCode) {
|
||||
HttpStatus.badRequest =>
|
||||
@ -91,8 +91,7 @@ class HttpScanRemoteDataSource implements ScanRemoteDataSource {
|
||||
'Submit failed with status 404 (Not Found).$bodySuffix',
|
||||
HttpStatus.internalServerError =>
|
||||
'Submit failed with status 500 (Server Error).$bodySuffix',
|
||||
_ =>
|
||||
'Submit failed. Expected status 200 but got $statusCode.$bodySuffix',
|
||||
_ => 'Submit failed. Expected status 200 but got $statusCode.$bodySuffix',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import 'package:cb_prestige_qr/features/scan/domain/entities/scanned_qr.dart';
|
||||
|
||||
class ScannedQrModel {
|
||||
const ScannedQrModel({
|
||||
required this.rawValue,
|
||||
required this.scannedAt,
|
||||
});
|
||||
const ScannedQrModel({required this.rawValue, required this.scannedAt});
|
||||
|
||||
factory ScannedQrModel.fromJson(Map<String, dynamic> json) {
|
||||
return ScannedQrModel(
|
||||
@ -27,4 +24,3 @@ class ScannedQrModel {
|
||||
return ScannedQr(rawValue: rawValue, scannedAt: scannedAt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,9 +7,7 @@ class ScanSubmitPayload {
|
||||
required this.scannedByDeviceId,
|
||||
});
|
||||
|
||||
factory ScanSubmitPayload.fromRawValue({
|
||||
required String rawValue,
|
||||
}) {
|
||||
factory ScanSubmitPayload.fromRawValue({required String rawValue}) {
|
||||
final payload = ScanSubmitPayload(
|
||||
token: rawValue.trim(),
|
||||
merchantId: 'merchant-001',
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
class ScannedQr {
|
||||
const ScannedQr({
|
||||
required this.rawValue,
|
||||
required this.scannedAt,
|
||||
});
|
||||
const ScannedQr({required this.rawValue, required this.scannedAt});
|
||||
|
||||
final String rawValue;
|
||||
final DateTime scannedAt;
|
||||
}
|
||||
|
||||
|
||||
@ -10,4 +10,3 @@ class ProcessScanUseCase {
|
||||
return _repository.processRawValue(rawValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'scan_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(ScanController)
|
||||
final scanControllerProvider = ScanControllerProvider._();
|
||||
|
||||
final class ScanControllerProvider
|
||||
extends $NotifierProvider<ScanController, ScanState> {
|
||||
ScanControllerProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'scanControllerProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$scanControllerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
ScanController create() => ScanController();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(ScanState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<ScanState>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$scanControllerHash() => r'5984074ff43ae1904e2025b355a689580d402a09';
|
||||
|
||||
abstract class _$ScanController extends $Notifier<ScanState> {
|
||||
ScanState build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<ScanState, ScanState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<ScanState, ScanState>,
|
||||
ScanState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@ -3,22 +3,16 @@ import 'package:cb_prestige_qr/features/scan/data/data_sources/scan_remote_data_
|
||||
import 'package:cb_prestige_qr/features/scan/data/repositories/scan_repository_impl.dart';
|
||||
import 'package:cb_prestige_qr/features/scan/domain/repositories/scan_repository.dart';
|
||||
import 'package:cb_prestige_qr/features/scan/domain/use_cases/process_scan_use_case.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
part 'scan_providers.g.dart';
|
||||
final scanRemoteDataSourceProvider = Provider<ScanRemoteDataSource>(
|
||||
(ref) => HttpScanRemoteDataSource(),
|
||||
);
|
||||
|
||||
@riverpod
|
||||
ScanRemoteDataSource scanRemoteDataSource(Ref ref) {
|
||||
return HttpScanRemoteDataSource();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
ScanRepository scanRepository(Ref ref) {
|
||||
return ScanRepositoryImpl(ref.watch(scanRemoteDataSourceProvider));
|
||||
}
|
||||
|
||||
@riverpod
|
||||
ProcessScanUseCase processScanUseCase(Ref ref) {
|
||||
return ProcessScanUseCase(ref.watch(scanRepositoryProvider));
|
||||
}
|
||||
final scanRepositoryProvider = Provider<ScanRepository>(
|
||||
(ref) => ScanRepositoryImpl(ref.watch(scanRemoteDataSourceProvider)),
|
||||
);
|
||||
|
||||
final processScanUseCaseProvider = Provider<ProcessScanUseCase>(
|
||||
(ref) => ProcessScanUseCase(ref.watch(scanRepositoryProvider)),
|
||||
);
|
||||
@ -1,7 +1,9 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:cb_prestige_qr/features/scan/scan_providers.dart';
|
||||
import 'package:cb_prestige_qr/features/scan/presentation/providers/scan_providers.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
part 'scan_controller.g.dart';
|
||||
final scanControllerProvider = NotifierProvider<ScanController, ScanState>(
|
||||
ScanController.new,
|
||||
);
|
||||
|
||||
class ScanState {
|
||||
const ScanState({
|
||||
@ -41,8 +43,7 @@ class ScanState {
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class ScanController extends _$ScanController {
|
||||
class ScanController extends Notifier<ScanState> {
|
||||
@override
|
||||
ScanState build() => const ScanState();
|
||||
|
||||
@ -60,12 +61,12 @@ class ScanController extends _$ScanController {
|
||||
isProcessing: false,
|
||||
scannedValue: result.rawValue,
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
state = state.copyWith(
|
||||
isProcessing: false,
|
||||
isScanning: true,
|
||||
scannedValue: null,
|
||||
errorMessage: e.toString(),
|
||||
errorMessage: error.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -81,8 +82,11 @@ class ScanController extends _$ScanController {
|
||||
scannedValue: null,
|
||||
errorMessage: null,
|
||||
);
|
||||
} catch (e) {
|
||||
state = state.copyWith(isSubmitting: false, errorMessage: e.toString());
|
||||
} catch (error) {
|
||||
state = state.copyWith(
|
||||
isSubmitting: false,
|
||||
errorMessage: error.toString(),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import 'package:cb_prestige_qr/features/scan/presentation/pages/scan_page.dart';
|
||||
import 'package:cb_prestige_qr/features/scan/presentation/views/scan_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ScanFlow extends StatelessWidget {
|
||||
@ -1,8 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cb_prestige_qr/core/presentation/widgets/start_loading_overlay.dart';
|
||||
import 'package:cb_prestige_qr/features/scan/presentation/manager/scan_controller.dart';
|
||||
import 'package:cb_prestige_qr/features/scan/presentation/pages/scan_result_page.dart';
|
||||
import 'package:cb_prestige_qr/features/scan/presentation/viewmodels/scan_controller.dart';
|
||||
import 'package:cb_prestige_qr/features/scan/presentation/views/scan_result_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cb_prestige_qr/core/utils/MainShell.dart';
|
||||
import 'package:cb_prestige_qr/features/scan/presentation/manager/scan_controller.dart';
|
||||
import 'package:cb_prestige_qr/app/navigation/main_shell.dart';
|
||||
import 'package:cb_prestige_qr/features/scan/presentation/viewmodels/scan_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||
@ -150,9 +150,7 @@ class ScanResultPage extends ConsumerWidget {
|
||||
content: Text('QR submitted successfully.'),
|
||||
),
|
||||
);
|
||||
ref
|
||||
.read(navIndexNotifierProvider.notifier)
|
||||
.setIndex(0);
|
||||
ref.read(navIndexProvider.notifier).setIndex(0);
|
||||
ref
|
||||
.read(scanControllerProvider.notifier)
|
||||
.resumeScanning();
|
||||
@ -260,10 +258,17 @@ Map<String, dynamic>? _tryDecodeFromUri(String value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _tryDecodeBase64Json(String value, {required bool urlSafe}) {
|
||||
Map<String, dynamic>? _tryDecodeBase64Json(
|
||||
String value, {
|
||||
required bool urlSafe,
|
||||
}) {
|
||||
try {
|
||||
final normalized = urlSafe ? base64Url.normalize(value) : base64.normalize(value);
|
||||
final bytes = urlSafe ? base64Url.decode(normalized) : base64.decode(normalized);
|
||||
final normalized = urlSafe
|
||||
? base64Url.normalize(value)
|
||||
: base64.normalize(value);
|
||||
final bytes = urlSafe
|
||||
? base64Url.decode(normalized)
|
||||
: base64.decode(normalized);
|
||||
final decoded = utf8.decode(bytes);
|
||||
final payload = jsonDecode(decoded);
|
||||
if (payload is! Map) return null;
|
||||
@ -275,8 +280,12 @@ Map<String, dynamic>? _tryDecodeBase64Json(String value, {required bool urlSafe}
|
||||
|
||||
String? _tryDecodeBase64Text(String value, {required bool urlSafe}) {
|
||||
try {
|
||||
final normalized = urlSafe ? base64Url.normalize(value) : base64.normalize(value);
|
||||
final bytes = urlSafe ? base64Url.decode(normalized) : base64.decode(normalized);
|
||||
final normalized = urlSafe
|
||||
? base64Url.normalize(value)
|
||||
: base64.normalize(value);
|
||||
final bytes = urlSafe
|
||||
? base64Url.decode(normalized)
|
||||
: base64.decode(normalized);
|
||||
final decoded = utf8.decode(bytes).trim();
|
||||
if (decoded.isEmpty || decoded == value) return null;
|
||||
return decoded;
|
||||
@ -1,147 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'scan_providers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(scanRemoteDataSource)
|
||||
final scanRemoteDataSourceProvider = ScanRemoteDataSourceProvider._();
|
||||
|
||||
final class ScanRemoteDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
ScanRemoteDataSource,
|
||||
ScanRemoteDataSource,
|
||||
ScanRemoteDataSource
|
||||
>
|
||||
with $Provider<ScanRemoteDataSource> {
|
||||
ScanRemoteDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'scanRemoteDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$scanRemoteDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<ScanRemoteDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
ScanRemoteDataSource create(Ref ref) {
|
||||
return scanRemoteDataSource(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(ScanRemoteDataSource value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<ScanRemoteDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$scanRemoteDataSourceHash() =>
|
||||
r'f25d69a350b64c4dd0cf8ef124838bb351b5e4a1';
|
||||
|
||||
@ProviderFor(scanRepository)
|
||||
final scanRepositoryProvider = ScanRepositoryProvider._();
|
||||
|
||||
final class ScanRepositoryProvider
|
||||
extends $FunctionalProvider<ScanRepository, ScanRepository, ScanRepository>
|
||||
with $Provider<ScanRepository> {
|
||||
ScanRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'scanRepositoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$scanRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<ScanRepository> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
ScanRepository create(Ref ref) {
|
||||
return scanRepository(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(ScanRepository value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<ScanRepository>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$scanRepositoryHash() => r'3e719b962dbb1014143710238128234371ef141e';
|
||||
|
||||
@ProviderFor(processScanUseCase)
|
||||
final processScanUseCaseProvider = ProcessScanUseCaseProvider._();
|
||||
|
||||
final class ProcessScanUseCaseProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
ProcessScanUseCase,
|
||||
ProcessScanUseCase,
|
||||
ProcessScanUseCase
|
||||
>
|
||||
with $Provider<ProcessScanUseCase> {
|
||||
ProcessScanUseCaseProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'processScanUseCaseProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$processScanUseCaseHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<ProcessScanUseCase> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
ProcessScanUseCase create(Ref ref) {
|
||||
return processScanUseCase(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(ProcessScanUseCase value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<ProcessScanUseCase>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$processScanUseCaseHash() =>
|
||||
r'ce9f6a3d0de27fe0f7f8e7fa1ac9c1f88ad57e43';
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:cb_prestige_qr/core/storage/local_storage_service.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/data/user_profile_repository.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/data/data_sources/auth_local_data_source.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/domain/entities/user_profile.dart';
|
||||
|
||||
import '../../domain/entities/settings_content.dart';
|
||||
|
||||
@ -10,32 +11,46 @@ abstract class SettingsLocalDataSource {
|
||||
}
|
||||
|
||||
class SettingsLocalDataSourceImpl implements SettingsLocalDataSource {
|
||||
SettingsLocalDataSourceImpl(this._storage);
|
||||
SettingsLocalDataSourceImpl(this._storage, this._authLocalDataSource);
|
||||
|
||||
final LocalStorageService _storage;
|
||||
final AuthLocalDataSource _authLocalDataSource;
|
||||
|
||||
bool _notificationsEnabled = true;
|
||||
bool _hapticsEnabled = true;
|
||||
static const _notificationsKey = 'settings_notifications_enabled';
|
||||
static const _hapticsKey = 'settings_haptics_enabled';
|
||||
|
||||
@override
|
||||
Future<SettingsContent> getSettings() async {
|
||||
final userProfile = await UserProfileRepository(_storage).load();
|
||||
final userProfile = await _authLocalDataSource.getSavedUserProfile();
|
||||
|
||||
return SettingsContent(
|
||||
notificationsEnabled: _notificationsEnabled,
|
||||
hapticsEnabled: _hapticsEnabled,
|
||||
notificationsEnabled: await _storage.getBool(_notificationsKey) ?? true,
|
||||
hapticsEnabled: await _storage.getBool(_hapticsKey) ?? true,
|
||||
appVersionLabel: 'v1.0.0',
|
||||
userProfile: userProfile,
|
||||
userProfile: userProfile ?? const _FallbackUserProfileFactory().create(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setNotificationsEnabled(bool value) async {
|
||||
_notificationsEnabled = value;
|
||||
await _storage.setBool(_notificationsKey, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setHapticsEnabled(bool value) async {
|
||||
_hapticsEnabled = value;
|
||||
await _storage.setBool(_hapticsKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
class _FallbackUserProfileFactory {
|
||||
const _FallbackUserProfileFactory();
|
||||
|
||||
UserProfile create() {
|
||||
return const UserProfile(
|
||||
username: 'cashier',
|
||||
displayName: 'Cashier',
|
||||
roleLabel: 'Cashier',
|
||||
branchLabel: 'Prestige Counter',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import 'package:cb_prestige_qr/features/auth/domain/user_profile.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/domain/entities/user_profile.dart';
|
||||
|
||||
class SettingsContent {
|
||||
const SettingsContent({
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import 'package:cb_prestige_qr/features/auth/domain/entities/user_profile.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../../auth/domain/user_profile.dart';
|
||||
|
||||
@immutable
|
||||
class SettingsUiState {
|
||||
const SettingsUiState({
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:cb_prestige_qr/core/providers/core_providers.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/di/auth_providers.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../../../core/storage/local_storage_service.dart';
|
||||
import '../../data/data_sources/settings_local_data_source.dart';
|
||||
import '../../data/repositories/settings_repository_impl.dart';
|
||||
import '../../domain/entities/settings_content.dart';
|
||||
@ -10,12 +11,11 @@ import '../../domain/use_cases/set_haptics_enabled.dart';
|
||||
import '../../domain/use_cases/set_notifications_enabled.dart';
|
||||
import 'settings_ui_state.dart';
|
||||
|
||||
final _localStorageServiceProvider = Provider<LocalStorageService>(
|
||||
(ref) => LocalStorageService(),
|
||||
);
|
||||
|
||||
final _settingsLocalDataSourceProvider = Provider<SettingsLocalDataSource>(
|
||||
(ref) => SettingsLocalDataSourceImpl(ref.watch(_localStorageServiceProvider)),
|
||||
(ref) => SettingsLocalDataSourceImpl(
|
||||
ref.watch(localStorageServiceProvider),
|
||||
ref.watch(authLocalDataSourceProvider),
|
||||
),
|
||||
);
|
||||
|
||||
final _settingsRepositoryProvider = Provider<SettingsRepository>(
|
||||
@ -56,12 +56,16 @@ class SettingsViewModel extends AsyncNotifier<SettingsUiState> {
|
||||
}
|
||||
|
||||
Future<void> toggleNotifications(bool value) async {
|
||||
await ref.watch(_setNotificationsEnabledProvider)(value);
|
||||
await ref.read(_setNotificationsEnabledProvider)(value);
|
||||
state = state.whenData((s) => s.copyWith(notificationsEnabled: value));
|
||||
}
|
||||
|
||||
Future<void> toggleHaptics(bool value) async {
|
||||
await ref.watch(_setHapticsEnabledProvider)(value);
|
||||
await ref.read(_setHapticsEnabledProvider)(value);
|
||||
state = state.whenData((s) => s.copyWith(hapticsEnabled: value));
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await ref.read(logoutUseCaseProvider)();
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,14 @@
|
||||
import 'package:cb_prestige_qr/core/utils/MainShell.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/presentation/pages/login_page.dart';
|
||||
import 'package:cb_prestige_qr/app/navigation/main_shell.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/domain/entities/user_profile.dart';
|
||||
import 'package:cb_prestige_qr/features/auth/presentation/views/login_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../../auth/domain/user_profile.dart';
|
||||
import '../manager/settings_ui_state.dart';
|
||||
import '../manager/settings_view_model.dart';
|
||||
import '../viewmodels/settings_ui_state.dart';
|
||||
import '../viewmodels/settings_view_model.dart';
|
||||
|
||||
class SettingsPage extends ConsumerWidget {
|
||||
const SettingsPage({super.key});
|
||||
class SettingsView extends ConsumerWidget {
|
||||
const SettingsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@ -31,11 +31,14 @@ class SettingsPage extends ConsumerWidget {
|
||||
onNotificationsChanged: viewModel.toggleNotifications,
|
||||
onHapticsChanged: viewModel.toggleHaptics,
|
||||
onLogout: () {
|
||||
ref.read(navIndexNotifierProvider.notifier).setIndex(0);
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const LoginPage()),
|
||||
(route) => false,
|
||||
);
|
||||
viewModel.logout().then((_) {
|
||||
if (!context.mounted) return;
|
||||
ref.read(navIndexProvider.notifier).setIndex(0);
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const LoginView()),
|
||||
(route) => false,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
@ -1,6 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cb_prestige_qr/core/utils/MainShell.dart';
|
||||
import 'package:cb_prestige_qr/app/navigation/main_shell.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
@ -26,9 +26,9 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_timer = Timer(widget.duration, () {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const MainShell()),
|
||||
);
|
||||
Navigator.of(
|
||||
context,
|
||||
).pushReplacement(MaterialPageRoute(builder: (_) => const MainShell()));
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -43,12 +43,8 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SizedBox.expand(
|
||||
child: Image.asset(
|
||||
widget.imageAssetPath,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
child: Image.asset(widget.imageAssetPath, fit: BoxFit.cover),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import 'package:cb_prestige_qr/features/auth/presentation/pages/login_page.dart';
|
||||
import 'package:cb_prestige_qr/app/app.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
@ -6,56 +6,3 @@ void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(const ProviderScope(child: MyApp()));
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'CB Prestige Banking',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xff896e4b),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
scaffoldBackgroundColor: const Color(0xff25262b),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color(0xff896e4b),
|
||||
foregroundColor: Colors.white,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: const Color(0xff2a2b31),
|
||||
labelStyle: const TextStyle(color: Colors.white70),
|
||||
hintStyle: const TextStyle(color: Colors.white38),
|
||||
prefixIconColor: Colors.white70,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: BorderSide(color: Colors.white.withOpacity(0.06)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: const BorderSide(color: Color(0xff896e4b), width: 1.2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: const BorderSide(color: Colors.redAccent),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: const BorderSide(color: Colors.redAccent),
|
||||
),
|
||||
),
|
||||
),
|
||||
home: const LoginPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
18
pubspec.lock
18
pubspec.lock
@ -217,6 +217,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.9.2"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -287,7 +303,7 @@ packages:
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
flutter_svg:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9"
|
||||
|
||||
@ -42,6 +42,8 @@ dependencies:
|
||||
mobile_scanner: ^7.2.0
|
||||
shared_preferences: ^2.5.5
|
||||
jwt_decoder: ^2.0.1
|
||||
dio: ^5.9.0
|
||||
flutter_svg: ^2.2.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@ -55,7 +57,6 @@ dev_dependencies:
|
||||
flutter_lints: ^6.0.0
|
||||
riverpod_generator: ^4.0.3
|
||||
build_runner: ^2.13.1
|
||||
flutter_svg: ^2.2.4
|
||||
flutter_native_splash: 2.4.7
|
||||
|
||||
|
||||
|
||||
@ -5,17 +5,16 @@
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:cb_prestige_qr/app/app.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:cb_prestige_qr/main.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('shows login form fields', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
await tester.pumpWidget(const ProviderScope(child: MyApp()));
|
||||
|
||||
expect(find.text('Login'), findsOneWidget);
|
||||
expect(find.text('Phone Number'), findsOneWidget);
|
||||
expect(find.text('User ID (Username)'), findsOneWidget);
|
||||
expect(find.text('Secure Login'), findsOneWidget);
|
||||
expect(find.text('Username'), findsOneWidget);
|
||||
expect(find.text('Password'), findsOneWidget);
|
||||
expect(find.text('Sign In'), findsOneWidget);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user