api intigreted of scan Qr and recent scan histoy ,scan history,scan history details

This commit is contained in:
Raj.Ghag
2026-04-17 15:55:54 +05:30
parent f301585948
commit 94cd74a135
60 changed files with 3678 additions and 1119 deletions

File diff suppressed because one or more lines are too long

View File

@@ -45,6 +45,9 @@ PODS:
- SDWebImage (5.21.3):
- SDWebImage/Core (= 5.21.3)
- SDWebImage/Core (5.21.3)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
DEPENDENCIES:
@@ -53,6 +56,7 @@ DEPENDENCIES:
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
SPEC REPOS:
trunk:
@@ -72,16 +76,19 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/mobile_scanner/darwin"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
PODFILE CHECKSUM: 251cb053df7158f337c0712f2ab29f4e0fa474ce

View File

@@ -6,6 +6,20 @@ import '../login/blocs/reset_password/reset_password_bloc.dart';
import '../splash/bloc/splash_bloc.dart';
import '../profile/blocs/profile/profile_bloc.dart';
import '../profile/repository/profile_repository.dart';
import '../support/blocs/support_details/support_details_bloc.dart';
import '../support/repository/support_details_repository.dart';
import '../support/blocs/raise_ticket/raise_ticket_bloc.dart';
import '../support/repository/raise_ticket_repository.dart';
import '../scan/bloc/submit_qr_code/submit_qr_code_bloc.dart';
import '../scan/repository/submit_qr_code_repository.dart';
import '../scan_history/blocs/scan_history/scan_history_bloc.dart';
import '../scan_history/blocs/scan_history_details/scan_history_details_bloc.dart';
import '../scan_history/repositories/scan_history_repository.dart';
import '../scan/bloc/recent_scan_history/recent_scan_history_bloc.dart';
import '../scan/repository/recent_scan_history_repository.dart';
class AllBlocProviders {
AllBlocProviders._(); // Private constructor — not meant to be instantiated
@@ -32,6 +46,29 @@ class AllBlocProviders {
BlocProvider<ProfileBloc>(
create: (_) => ProfileBloc(profileRepository: ProfileRepository()),
),
// ─── Support ─────────────────────────────────────────────────────────
BlocProvider<SupportDetailsBloc>(
create: (_) => SupportDetailsBloc(repository: SupportDetailsRepository()),
),
BlocProvider<RaiseTicketBloc>(
create: (_) => RaiseTicketBloc(raiseTicketRepository: RaiseTicketRepository()),
),
// ─── Scan History ────────────────────────────────────────────────────
BlocProvider<ScanHistoryBloc>(
create: (_) => ScanHistoryBloc(repository: ScanHistoryRepository(),),
),
BlocProvider<ScanHistoryDetailsBloc>(
create: (_) => ScanHistoryDetailsBloc(ScanHistoryRepository()),
),
BlocProvider<RecentScanHistoryBloc>(
create: (_) => RecentScanHistoryBloc(RecentScanHistoryRepository()),
),
BlocProvider<SubmitQrCodeBloc>(
create: (_) => SubmitQrCodeBloc(repository: SubmitQrCodeRepository()),
),
];
}
}

View File

@@ -61,9 +61,10 @@ class AppRouter {
case splashScreen:
return MaterialPageRoute(builder: (_) => const SplashScreen());
case scanHistoryDetailPage:
final passId = settings.arguments as int? ?? 0;
return MaterialPageRoute(
builder: (_) => const ScanHistoryDetailPage(
passId: 'P214125125',
builder: (_) => ScanHistoryDetailPage(
passId: passId,
));
case selectedTimeSlotPage:
return MaterialPageRoute(builder: (_) => const SelectedTimeSlotPage());

View File

@@ -44,6 +44,11 @@ class LocalPreference {
return prefs.getString(_keyAccessToken) ?? "";
}
static Future<void> clearAccessToken() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove(_keyAccessToken);
}
// -------------------- REFRESH TOKEN --------------------
static Future<void> setRefreshToken(String token) async {

View File

@@ -1,82 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
// ---------------- EVENTS ----------------
abstract class ForgotPasswordEvent {}
class EmailChanged extends ForgotPasswordEvent {
final String email;
EmailChanged(this.email);
}
class SendResetLink extends ForgotPasswordEvent {}
// ---------------- STATE ----------------
class ForgotPasswordState {
final String email;
final bool isValidEmail;
final bool isLoading;
final bool isSuccess;
final String message;
const ForgotPasswordState({
required this.email,
required this.isValidEmail,
required this.isLoading,
required this.isSuccess,
required this.message,
});
ForgotPasswordState copyWith({
String? email,
bool? isValidEmail,
bool? isLoading,
bool? isSuccess,
String? message,
}) {
return ForgotPasswordState(
email: email ?? this.email,
isValidEmail: isValidEmail ?? this.isValidEmail,
isLoading: isLoading ?? this.isLoading,
isSuccess: isSuccess ?? this.isSuccess,
message: message ?? this.message,
);
}
factory ForgotPasswordState.initial() => const ForgotPasswordState(
email: '',
isValidEmail: false,
isLoading: false,
isSuccess: false,
message: '',
);
}
// ---------------- BLOC ----------------
class ForgotPasswordBloc
extends Bloc<ForgotPasswordEvent, ForgotPasswordState> {
ForgotPasswordBloc() : super(ForgotPasswordState.initial()) {
// when email changes
on<EmailChanged>((event, emit) {
final isValid = _isValidEmail(event.email);
emit(state.copyWith(email: event.email, isValidEmail: isValid));
});
// when button clicked
on<SendResetLink>((event, emit) async {
if (!state.isValidEmail) return;
emit(state.copyWith(isLoading: true, message: ''));
await Future.delayed(const Duration(seconds: 2)); // simulate API delay
emit(state.copyWith(
isLoading: false,
isSuccess: true,
message: 'Reset link sent successfully!',
));
});
}
bool _isValidEmail(String email) {
final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
return regex.hasMatch(email);
}
}

View File

@@ -1,7 +1,8 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../local_peference/local_preference.dart';
import '../../models/login.dart';
import '../../models/login_model.dart';
import '../../repositories/login_repository.dart';
part 'login_event.dart';
part 'login_state.dart';
@@ -32,7 +33,10 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
password: event.password,
rememberMe: event.rememberMe,
);
if (kDebugMode) {
print('🔍 Login response accessToken: ${loginData.accessToken}');
print('🔍 Login response refreshToken: ${loginData.refreshToken}');
}
// ── Save to local preference ──────────────────────────────────────
await Future.wait([
LocalPreference.setAccessToken(loginData.accessToken),

View File

@@ -1,103 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
// --------------------- EVENTS ---------------------
abstract class OtpEvent {}
/// Triggered when user types or pastes OTP
class OtpChanged extends OtpEvent {
final String otp;
OtpChanged(this.otp);
}
/// Triggered when user presses Verify button
class OtpVerify extends OtpEvent {}
/// (Optional for later) Triggered when user taps "Resend OTP"
class OtpResend extends OtpEvent {}
// --------------------- STATE ---------------------
class OtpState {
final String otp;
final bool isVerified;
final bool isLoading;
final bool isResending;
final String message;
/// Computed property to know if OTP input is complete (6 digits)
bool get isOtpFilled => otp.length == 6;
const OtpState({
required this.otp,
required this.isVerified,
required this.isLoading,
required this.isResending,
required this.message,
});
OtpState copyWith({
String? otp,
bool? isVerified,
bool? isLoading,
bool? isResending,
String? message,
}) {
return OtpState(
otp: otp ?? this.otp,
isVerified: isVerified ?? this.isVerified,
isLoading: isLoading ?? this.isLoading,
isResending: isResending ?? this.isResending,
message: message ?? this.message,
);
}
factory OtpState.initial() => const OtpState(
otp: '',
isVerified: false,
isLoading: false,
isResending: false,
message: '',
);
}
// --------------------- BLOC ---------------------
class OtpBloc extends Bloc<OtpEvent, OtpState> {
OtpBloc() : super(OtpState.initial()) {
// Handle typing input
on<OtpChanged>((event, emit) {
emit(state.copyWith(otp: event.otp, message: ''));
});
// Handle Verify
on<OtpVerify>((event, emit) async {
if (!state.isOtpFilled) return; // no action if OTP incomplete
emit(state.copyWith(isLoading: true, message: ''));
await Future.delayed(const Duration(seconds: 2)); // simulate API delay
// Mock success condition — replace with API later
if (state.otp == "123456") {
emit(state.copyWith(
isVerified: true,
isLoading: false,
message: "OTP verified successfully!",
));
} else {
emit(state.copyWith(
isVerified: false,
isLoading: false,
message: "Invalid OTP. Please try again.",
));
}
});
// Handle Resend OTP (optional for later)
on<OtpResend>((event, emit) async {
emit(state.copyWith(isResending: true, message: 'Resending OTP...'));
await Future.delayed(const Duration(seconds: 3)); // simulate resend
emit(state.copyWith(isResending: false, message: 'OTP resent successfully!'));
});
}
}

View File

@@ -1,93 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
/// EVENTS
abstract class ResetPasswordEvent {}
class PasswordChanged extends ResetPasswordEvent {
final String password;
PasswordChanged(this.password);
}
class ConfirmPasswordChanged extends ResetPasswordEvent {
final String confirmPassword;
ConfirmPasswordChanged(this.confirmPassword);
}
/// STATE
class ResetPasswordState {
final String password;
final String confirmPassword;
final bool hasMinLength;
final bool hasUppercase;
final bool hasNumber;
final int strengthLevel; // 04 based on password strength
const ResetPasswordState({
required this.password,
required this.confirmPassword,
required this.hasMinLength,
required this.hasUppercase,
required this.hasNumber,
required this.strengthLevel,
});
factory ResetPasswordState.initial() => const ResetPasswordState(
password: '',
confirmPassword: '',
hasMinLength: false,
hasUppercase: false,
hasNumber: false,
strengthLevel: 0,
);
ResetPasswordState copyWith({
String? password,
String? confirmPassword,
bool? hasMinLength,
bool? hasUppercase,
bool? hasNumber,
int? strengthLevel,
}) {
return ResetPasswordState(
password: password ?? this.password,
confirmPassword: confirmPassword ?? this.confirmPassword,
hasMinLength: hasMinLength ?? this.hasMinLength,
hasUppercase: hasUppercase ?? this.hasUppercase,
hasNumber: hasNumber ?? this.hasNumber,
strengthLevel: strengthLevel ?? this.strengthLevel,
);
}
}
/// BLOC
class ResetPasswordBloc extends Bloc<ResetPasswordEvent, ResetPasswordState> {
ResetPasswordBloc() : super(ResetPasswordState.initial()) {
on<PasswordChanged>((event, emit) {
final pwd = event.password;
final hasMin = pwd.length >= 8;
final hasUp = pwd.contains(RegExp(r'[A-Z]'));
final hasNum = pwd.contains(RegExp(r'[0-9]'));
// count how many validations passed (max 3)
int passed = [hasMin, hasUp, hasNum].where((e) => e).length;
// convert to 4-box level (04)
int level = 0;
if (passed == 1) level = 1;
if (passed == 2) level = 2;
if (passed == 3) level = 4; // all conditions passed => full bar
emit(state.copyWith(
password: pwd,
hasMinLength: hasMin,
hasUppercase: hasUp,
hasNumber: hasNum,
strengthLevel: level,
));
});
on<ConfirmPasswordChanged>((event, emit) {
emit(state.copyWith(confirmPassword: event.confirmPassword));
});
}
}

View File

@@ -11,9 +11,14 @@ class VerifyOtpBloc extends Bloc<VerifyOtpEvent, VerifyOtpState> {
VerifyOtpBloc({OtpRepository? otpRepository})
: _otpRepository = otpRepository ?? OtpRepository(),
super(const VerifyOtpState()) {
on<OtpChanged>(_onOtpChanged);
on<VerifyOtpSubmitted>(_onVerifyOtpSubmitted);
}
void _onOtpChanged(OtpChanged event, Emitter<VerifyOtpState> emit) {
emit(state.copyWith(otp: event.otp));
}
Future<void> _onVerifyOtpSubmitted(
VerifyOtpSubmitted event,
Emitter<VerifyOtpState> emit,

View File

@@ -7,6 +7,15 @@ abstract class VerifyOtpEvent extends Equatable {
List<Object?> get props => [];
}
class OtpChanged extends VerifyOtpEvent {
final String otp;
const OtpChanged({required this.otp});
@override
List<Object?> get props => [otp];
}
class VerifyOtpSubmitted extends VerifyOtpEvent {
final String emailAddress;
final String otp;

View File

@@ -5,22 +5,26 @@ enum VerifyOtpStatus { initial, loading, success, failure }
class VerifyOtpState extends Equatable {
final VerifyOtpStatus status;
final String? errorMessage;
final String otp;
const VerifyOtpState({
this.status = VerifyOtpStatus.initial,
this.errorMessage,
this.otp = '',
});
VerifyOtpState copyWith({
VerifyOtpStatus? status,
String? errorMessage,
String? otp,
}) {
return VerifyOtpState(
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
otp: otp ?? this.otp,
);
}
@override
List<Object?> get props => [status, errorMessage];
List<Object?> get props => [status, errorMessage, otp];
}

View File

@@ -12,18 +12,29 @@ class LoginModel {
});
factory LoginModel.fromJson(Map<String, dynamic> json) {
// Debug logs (remove in production)
// print("ACCESS TOKEN: ${json['accessToken']}");
// print("REFRESH TOKEN: ${json['partner_refresh_token']}");
return LoginModel(
accessToken: json['accessToken'] ?? '',
refreshToken: json['refreshToken'] ?? '',
refreshMaxAge: json['refreshMaxAge'] ?? 0,
partner: PartnerModel.fromJson(json['partner']),
refreshToken: json['partner_refresh_token'] ?? '', // fixed key
refreshMaxAge: json['refreshMaxAge'] ?? 0, // may not come from API
partner: json['partner'] != null
? PartnerModel.fromJson(json['partner'])
: const PartnerModel(
id: 0,
email: '',
name: '',
roleXid: 0,
),
);
}
Map<String, dynamic> toJson() {
return {
'accessToken': accessToken,
'refreshToken': refreshToken,
'partner_refresh_token': refreshToken, // match API
'refreshMaxAge': refreshMaxAge,
'partner': partner.toJson(),
};

View File

@@ -1,6 +1,6 @@
import '../../network_api_service/api_service/api_service.dart';
import '../../network_api_service/api_urls/api_urls.dart';
import '../models/login.dart';
import '../models/login_model.dart';
class LoginRepository {
final ApiService _apiService = ApiService();

View File

@@ -9,18 +9,11 @@ import '../../core/app_router.dart';
import '../../custome_widgets/custom_button.dart';
import '../blocs/verify_otp/verify_otp_bloc.dart';
class OtpVerificationPage extends StatefulWidget {
class OtpVerificationPage extends StatelessWidget {
final String email;
const OtpVerificationPage({super.key, required this.email});
@override
State<OtpVerificationPage> createState() => _OtpVerificationPageState();
}
class _OtpVerificationPageState extends State<OtpVerificationPage> {
String _otp = "";
@override
Widget build(BuildContext context) {
return GestureDetector(
@@ -34,7 +27,7 @@ class _OtpVerificationPageState extends State<OtpVerificationPage> {
Navigator.pushReplacementNamed(
context,
AppRouter.resetPassword,
arguments: widget.email,
arguments: email,
);
} else if (state.status == VerifyOtpStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -68,7 +61,7 @@ class _OtpVerificationPageState extends State<OtpVerificationPage> {
),
SizedBox(height: 8.h),
Text(
"Partners App",
"Partner's App",
style: GoogleFonts.poppins(
color: AppColors.primaryRed,
fontSize: 20.sp,
@@ -92,7 +85,7 @@ class _OtpVerificationPageState extends State<OtpVerificationPage> {
),
SizedBox(height: 12.h),
Text(
"Weve sent an OTP to your registered email. Please enter it below.",
"We've sent an OTP to your registered email. Please enter it below.",
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
color: AppColors.textGrey,
@@ -118,17 +111,17 @@ class _OtpVerificationPageState extends State<OtpVerificationPage> {
color: AppColors.black,
),
onCodeChanged: (String code) {
setState(() {
_otp = code;
});
context.read<VerifyOtpBloc>().add(
OtpChanged(otp: code),
);
},
onSubmit: (String verificationCode) {
setState(() {
_otp = verificationCode;
});
context.read<VerifyOtpBloc>().add(
OtpChanged(otp: verificationCode),
);
context.read<VerifyOtpBloc>().add(
VerifyOtpSubmitted(
emailAddress: widget.email,
emailAddress: email,
otp: verificationCode,
),
);
@@ -143,12 +136,12 @@ class _OtpVerificationPageState extends State<OtpVerificationPage> {
CustomButton(
text: "Verify",
isLoading: isLoading,
onPressed: _otp.length == 6
onPressed: state.otp.length == 6
? () {
context.read<VerifyOtpBloc>().add(
VerifyOtpSubmitted(
emailAddress: widget.email,
otp: _otp,
emailAddress: email,
otp: state.otp,
),
);
}

View File

@@ -8,11 +8,14 @@ class ApiService {
static const String _baseUrl = 'https://your-api-base-url.com/api';
static final ApiService _instance = ApiService._internal();
late Dio _dio;
late Dio _tokenDio; // ✅ Separate Dio for token refresh (no interceptors)
factory ApiService() => _instance;
ApiService._internal() {
// ================= MAIN DIO =================
_dio = Dio(
BaseOptions(
baseUrl: _baseUrl,
@@ -25,7 +28,21 @@ class ApiService {
),
);
// ================= RETRY INTERCEPTOR =================
// ================= TOKEN DIO (No interceptors — used only for refresh) =================
_tokenDio = Dio(
BaseOptions(
baseUrl: _baseUrl,
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
// ================= 1. RETRY INTERCEPTOR =================
// ✅ Added FIRST so it only retries network errors
_dio.interceptors.add(
InterceptorsWrapper(
onError: (err, handler) async {
@@ -42,7 +59,7 @@ class ApiService {
if (shouldRetry) {
if (kDebugMode) {
print(
'🔁 Retrying request (${currentRetry + 1}) => ${options.uri}',
'🔁 Retrying request (${currentRetry + 1}/$maxRetries) => ${options.uri}',
);
}
options.extra['retry'] = currentRetry + 1;
@@ -54,12 +71,13 @@ class ApiService {
}
}
return handler.reject(err);
return handler.next(err);
},
),
);
// ================= MAIN INTERCEPTOR (Queued for concurrency) =================
// ================= 2. MAIN INTERCEPTOR (Auth + Token Refresh) =================
// ✅ Added SECOND — handles auth and token refresh
_dio.interceptors.add(
QueuedInterceptorsWrapper(
onRequest: (options, handler) async {
@@ -70,6 +88,10 @@ class ApiService {
handler.next(options);
},
onResponse: (response, handler) {
handler.next(response);
},
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
final requestOptions = error.requestOptions;
@@ -80,13 +102,18 @@ class ApiService {
if (refreshed) {
final newToken = await LocalPreference.getAccessToken();
requestOptions.headers['Authorization'] = 'Bearer $newToken';
// ✅ Retry original request with new token
final response = await _dio.fetch(requestOptions);
return handler.resolve(response);
} else {
await _forceLogout();
return handler.reject(error);
}
} catch (_) {
} catch (e) {
if (kDebugMode) {
print('❌ Error during token refresh flow: $e');
}
await _forceLogout();
return handler.reject(error);
}
@@ -97,7 +124,8 @@ class ApiService {
),
);
// ================= LOGGING INTERCEPTOR =================
// ================= 3. LOGGING INTERCEPTOR =================
// ✅ Added LAST so it captures the final state of all requests/responses
if (kDebugMode) {
_dio.interceptors.add(
LogInterceptor(
@@ -105,7 +133,9 @@ class ApiService {
requestHeader: true,
requestBody: true,
responseBody: true,
responseHeader: false,
error: true,
logPrint: (log) => print('📡 $log'),
),
);
}
@@ -198,26 +228,67 @@ class ApiService {
}
// ================= REFRESH TOKEN =================
// ✅ Uses _tokenDio (no interceptors) to avoid QueuedInterceptor deadlock
Future<bool> _refreshToken() async {
try {
final refreshToken = await LocalPreference.getRefreshToken();
if (refreshToken == null) return false;
final response = await _dio.post(
if (kDebugMode) print('🔍 Refresh token from storage: $refreshToken');
if (refreshToken == null || refreshToken.isEmpty) {
if (kDebugMode) print('❌ No refresh token found');
return false;
}
if (kDebugMode) print('🔄 Attempting token refresh...');
final response = await _tokenDio.post(
ApiUrls.refreshToken,
data: {"refreshToken": refreshToken},
options: Options(headers: {'Authorization': null}),
data: '', // ✅ Empty body — server reads token from Cookie header
options: Options(
headers: {
// ✅ Manually inject refresh token as cookie header
'Cookie': 'partner_refresh_token=$refreshToken',
},
validateStatus: (status) => status != null && status < 500,
),
);
await LocalPreference.setAccessToken(response.data['accessToken']);
if (kDebugMode) {
print("🔄 Refresh response status: ${response.statusCode}");
print("✅ REFRESH RESPONSE => ${response.data}");
}
if (response.statusCode != 200 && response.statusCode != 201) {
if (kDebugMode) {
print('❌ Refresh failed with status: ${response.statusCode}');
}
return false;
}
final newAccessToken = response.data['accessToken'];
if (newAccessToken == null || (newAccessToken as String).isEmpty) {
if (kDebugMode) print('❌ Access token missing in refresh response');
return false;
}
await LocalPreference.setAccessToken(newAccessToken);
if (kDebugMode) print('✅ Token refreshed successfully');
return true;
} catch (_) {
} on DioException catch (e) {
if (kDebugMode) print('❌ Refresh token DioException: ${e.message}');
return false;
} catch (e) {
if (kDebugMode) print('❌ Refresh token unexpected error: $e');
return false;
}
}
// ================= FORCE LOGOUT =================
Future<void> _forceLogout() async {
if (kDebugMode) print('🚪 Force logout triggered');
await LocalPreference.clearAll();
await LocalPreference.setLogin(false);
}
@@ -241,10 +312,8 @@ class ApiService {
responseData['error'] ??
"Invalid status code: ${error.response?.statusCode}";
}
if (responseData is String) {
return responseData.isNotEmpty
? responseData
: "Invalid status code: ${error.response?.statusCode}";
if (responseData is String && responseData.isNotEmpty) {
return responseData;
}
return "Invalid status code: ${error.response?.statusCode}";
} catch (_) {

View File

@@ -4,15 +4,16 @@ class ApiUrls {
static const baseUrl = "https://testingapi.citycards.betadelivery.com"; // Test API
// static const baseUrl = "https://uatapi.citycard.betadelivery.com"; // Production Lvl API
static const refreshToken = "$baseUrl/auth/refresh";
static const refreshToken = "$baseUrl/partner/auth/refresh";
// ================= GET APIs =================
static const authUserDetails = "$baseUrl/partner/auth";
static const supportDetails = "$baseUrl/mobile/partners/support";
static const scanHistory = "$baseUrl/mobile/partners/scan-history";
// ================= POST APIs =================
static const login = "$baseUrl/partner/auth/login";
static const forgotPassword = "$baseUrl/partner/auth/forgot-password";
static const verifyOtp = "$baseUrl/partner/auth/verify-otp";
static const resetPassword = "$baseUrl/partner/auth/set-password";
static const redeem = "$baseUrl/mobile/partners/redeem";
}

View File

@@ -68,7 +68,9 @@ class _OnboardingPageState extends State<OnboardingPage> {
child: Align(
alignment: Alignment.topRight,
child: OutlinedButton(
onPressed: () {},
onPressed: () {
_skip();
},
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.white),
foregroundColor: Colors.white,

View File

@@ -0,0 +1,26 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import '../../repository/recent_scan_history_repository.dart';
import '../../models/recent_scan_history_model.dart';
part 'recent_scan_history_event.dart';
part 'recent_scan_history_state.dart';
class RecentScanHistoryBloc extends Bloc<RecentScanHistoryEvent, RecentScanHistoryState> {
final RecentScanHistoryRepository _repository;
RecentScanHistoryBloc(this._repository) : super(RecentScanHistoryInitial()) {
on<FetchRecentScanHistory>(_onFetchRecentScanHistory);
}
Future<void> _onFetchRecentScanHistory(
FetchRecentScanHistory event, Emitter<RecentScanHistoryState> emit) async {
emit(RecentScanHistoryLoading());
try {
final history = await _repository.fetchRecentScanHistory();
emit(RecentScanHistoryLoaded(history));
} catch (e) {
emit(RecentScanHistoryError(e.toString()));
}
}
}

View File

@@ -0,0 +1,10 @@
part of 'recent_scan_history_bloc.dart';
abstract class RecentScanHistoryEvent extends Equatable {
const RecentScanHistoryEvent();
@override
List<Object> get props => [];
}
class FetchRecentScanHistory extends RecentScanHistoryEvent {}

View File

@@ -0,0 +1,30 @@
part of 'recent_scan_history_bloc.dart';
abstract class RecentScanHistoryState extends Equatable {
const RecentScanHistoryState();
@override
List<Object?> get props => [];
}
class RecentScanHistoryInitial extends RecentScanHistoryState {}
class RecentScanHistoryLoading extends RecentScanHistoryState {}
class RecentScanHistoryLoaded extends RecentScanHistoryState {
final List<RecentScanHistory> history;
const RecentScanHistoryLoaded(this.history);
@override
List<Object?> get props => [history];
}
class RecentScanHistoryError extends RecentScanHistoryState {
final String message;
const RecentScanHistoryError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,70 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import '../../repository/submit_qr_code_repository.dart';
part 'submit_qr_code_event.dart';
part 'submit_qr_code_state.dart';
class SubmitQrCodeBloc
extends Bloc<SubmitQrCodeEvent, SubmitQrCodeState> {
final SubmitQrCodeRepository _repository;
SubmitQrCodeBloc({SubmitQrCodeRepository? repository})
: _repository = repository ?? SubmitQrCodeRepository(),
super(const SubmitQrCodeInitial()) {
on<SubmitQrCodeEventTriggered>(_onSubmitQrCode);
on<ResetSubmitQrCodeEvent>(_onReset);
}
Future<void> _onSubmitQrCode(
SubmitQrCodeEventTriggered event,
Emitter<SubmitQrCodeState> emit,
) async {
if (event.qrCode.trim().isEmpty) {
emit(const SubmitQrCodeFailure(
errorMessage: 'QR code cannot be empty'));
return;
}
emit(const SubmitQrCodeLoading());
try {
final response = await _repository.submitQrCode(
qrCode: event.qrCode,
);
final success = response['success'] == true;
final message = response['message']?.toString();
final error = response['error']?.toString();
if (success) {
emit(SubmitQrCodeSuccess(
data: response,
message: message,
));
} else {
emit(SubmitQrCodeFailure(
errorMessage: message ?? 'Failed to submit QR Code',
error: error,
));
}
} on Exception catch (e) {
emit(SubmitQrCodeFailure(
errorMessage: e.toString().replaceFirst('Exception: ', ''),
));
} catch (e) {
emit(SubmitQrCodeFailure(
errorMessage: 'Unexpected error: $e',
));
}
}
void _onReset(
ResetSubmitQrCodeEvent event,
Emitter<SubmitQrCodeState> emit,
) {
emit(const SubmitQrCodeInitial());
}
}

View File

@@ -0,0 +1,21 @@
part of 'submit_qr_code_bloc.dart';
abstract class SubmitQrCodeEvent extends Equatable {
const SubmitQrCodeEvent();
@override
List<Object?> get props => [];
}
class SubmitQrCodeEventTriggered extends SubmitQrCodeEvent {
final String qrCode;
const SubmitQrCodeEventTriggered({required this.qrCode});
@override
List<Object?> get props => [qrCode];
}
class ResetSubmitQrCodeEvent extends SubmitQrCodeEvent {
const ResetSubmitQrCodeEvent();
}

View File

@@ -0,0 +1,42 @@
part of 'submit_qr_code_bloc.dart';
abstract class SubmitQrCodeState extends Equatable {
const SubmitQrCodeState();
@override
List<Object?> get props => [];
}
class SubmitQrCodeInitial extends SubmitQrCodeState {
const SubmitQrCodeInitial();
}
class SubmitQrCodeLoading extends SubmitQrCodeState {
const SubmitQrCodeLoading();
}
class SubmitQrCodeSuccess extends SubmitQrCodeState {
final Map<String, dynamic> data;
final String? message;
const SubmitQrCodeSuccess({
required this.data,
this.message,
});
@override
List<Object?> get props => [data, message];
}
class SubmitQrCodeFailure extends SubmitQrCodeState {
final String errorMessage;
final String? error;
const SubmitQrCodeFailure({
required this.errorMessage,
this.error,
});
@override
List<Object?> get props => [errorMessage, error];
}

View File

@@ -0,0 +1,100 @@
import 'package:intl/intl.dart';
class RecentScanHistoryResponse {
final List<RecentScanHistory> data;
RecentScanHistoryResponse({
required this.data,
});
factory RecentScanHistoryResponse.fromJson(Map<String, dynamic> json) {
return RecentScanHistoryResponse(
data: (json['data'] as List<dynamic>?)
?.map((e) => RecentScanHistory.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
}
}
class RecentScanHistory {
final int id;
final String passId;
final String qrCode;
final String bookingNumber;
final int attractionId;
final String attractionTitle;
final String customerName;
final String customerEmail;
final String cardType;
final int? scannedByPartnerStaffId;
final String scannedByPartnerStaffName;
final String status;
final String reason;
final DateTime? checkedInDatetime;
final DateTime? activatedAt;
final DateTime? qrExpiresAt;
final DateTime? validUpto;
final DateTime? createdAt;
RecentScanHistory({
required this.id,
required this.passId,
required this.qrCode,
required this.bookingNumber,
required this.attractionId,
required this.attractionTitle,
required this.customerName,
required this.customerEmail,
required this.cardType,
this.scannedByPartnerStaffId,
required this.scannedByPartnerStaffName,
required this.status,
required this.reason,
this.checkedInDatetime,
this.activatedAt,
this.qrExpiresAt,
this.validUpto,
this.createdAt,
});
factory RecentScanHistory.fromJson(Map<String, dynamic> json) {
return RecentScanHistory(
id: json['id'] ?? 0,
passId: (json['passId'] ?? '').toString(),
qrCode: (json['qrCode'] ?? '').toString(),
bookingNumber: (json['bookingNumber'] ?? '').toString(),
attractionId: json['attractionId'] ?? 0,
attractionTitle: (json['attractionTitle'] ?? '').toString(),
customerName: (json['customerName'] ?? '').toString(),
customerEmail: (json['customerEmail'] ?? '').toString(),
cardType: (json['cardType'] ?? '').toString(),
scannedByPartnerStaffId: json['scannedByPartnerStaffId'],
scannedByPartnerStaffName:
(json['scannedByPartnerStaffName'] ?? '').toString(),
status: (json['status'] ?? '').toString(),
reason: (json['reason'] ?? '').toString(),
checkedInDatetime: _parseDate(json['checkedInDatetime']),
activatedAt: _parseDate(json['activatedAt']),
qrExpiresAt: _parseDate(json['qrExpiresAt']),
validUpto: _parseDate(json['validUpto']),
createdAt: _parseDate(json['createdAt']),
);
}
static DateTime? _parseDate(dynamic date) {
if (date == null) return null;
try {
return DateTime.parse(date.toString());
} catch (e) {
return null;
}
}
static final DateFormat _formatter = DateFormat('dd MMM yyyy, hh:mm a');
String formatTime(DateTime? date) {
if (date == null) return 'N/A';
return _formatter.format(date);
}
}

View File

@@ -0,0 +1,18 @@
import '../../network_api_service/api_service/api_service.dart';
import '../../network_api_service/api_urls/api_urls.dart';
import '../models/recent_scan_history_model.dart';
class RecentScanHistoryRepository {
final ApiService _apiService = ApiService();
Future<List<RecentScanHistory>> fetchRecentScanHistory() async {
try {
final response = await _apiService.get(ApiUrls.scanHistory);
final RecentScanHistoryResponse scanResponse =
RecentScanHistoryResponse.fromJson(response.data);
return scanResponse.data;
} catch (e) {
throw Exception('Failed to fetch recent scan history: $e');
}
}
}

View File

@@ -0,0 +1,29 @@
import 'package:dio/dio.dart';
import '../../network_api_service/api_service/api_service.dart';
import '../../network_api_service/api_urls/api_urls.dart';
class SubmitQrCodeRepository {
final ApiService _apiService = ApiService();
Future<Map<String, dynamic>> submitQrCode({
required String qrCode,
}) async {
try {
final response = await _apiService.post(
ApiUrls.redeem,
data: {
"code": qrCode,
},
);
return response.data as Map<String, dynamic>;
} on DioException catch (e) {
if (e.response?.data != null && e.response?.data is Map<String, dynamic>) {
return e.response?.data as Map<String, dynamic>;
}
rethrow;
} catch (e) {
throw Exception('$e');
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,206 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import '../../models/scan_history_model.dart';
import '../../repositories/scan_history_repository.dart';
part 'scan_history_event.dart';
part 'scan_history_state.dart';
class ScanHistoryBloc extends Bloc<ScanHistoryEvent, ScanHistoryState> {
final ScanHistoryRepository _repository;
ScanHistoryBloc({ScanHistoryRepository? repository})
: _repository = repository ?? ScanHistoryRepository(),
super(ScanHistoryInitial()) {
on<FetchScanHistoryEvent>(_onFetchScanHistory);
on<RefreshScanHistoryEvent>(_onRefreshScanHistory);
on<UpdateScanHistoryDateEvent>(_onUpdateDate);
on<UpdateScanHistoryStatusEvent>(_onUpdateStatus);
on<SearchScanHistoryEvent>(_onSearchScanHistory);
on<SelectScanHistoryEvent>(_onSelectScanHistory);
on<ClearSelectedScanHistoryEvent>(_onClearSelectedScanHistory);
}
// ─── Fetch ────────────────────────────────────────────────────────────────
Future<void> _onFetchScanHistory(
FetchScanHistoryEvent event,
Emitter<ScanHistoryState> emit,
) async {
DateTime? selectedDate = event.clearDate ? null : (event.date ?? state.selectedDate);
String selectedStatus = event.status ?? state.selectedStatus;
emit(ScanHistoryLoading(
selectedDate: selectedDate,
selectedStatus: selectedStatus,
));
try {
String? formattedDate;
if (selectedDate != null) {
formattedDate = DateFormat('yyyy-MM-dd').format(selectedDate);
}
final List<ScanHistory> items = await _repository.fetchScanHistory(
date: formattedDate,
status: selectedStatus,
);
emit(ScanHistoryLoaded(
allItems: items,
filteredItems: items,
selectedDate: selectedDate,
selectedStatus: selectedStatus,
));
} catch (e) {
emit(ScanHistoryError(
errorMessage: e.toString(),
selectedDate: selectedDate,
selectedStatus: selectedStatus,
));
}
}
// ─── Refresh ──────────────────────────────────────────────────────────────
Future<void> _onRefreshScanHistory(
RefreshScanHistoryEvent event,
Emitter<ScanHistoryState> emit,
) async {
if (state is! ScanHistoryLoaded) return;
final currentState = state as ScanHistoryLoaded;
emit(currentState.copyWith(isRefreshing: true));
try {
String? formattedDate;
if (currentState.selectedDate != null) {
formattedDate = DateFormat('yyyy-MM-dd').format(currentState.selectedDate!);
}
final List<ScanHistory> items = await _repository.fetchScanHistory(
date: formattedDate,
status: currentState.selectedStatus,
);
emit(ScanHistoryLoaded(
allItems: items,
filteredItems: items,
selectedDate: currentState.selectedDate,
selectedStatus: currentState.selectedStatus,
));
} catch (e) {
emit(ScanHistoryError(
errorMessage: e.toString(),
selectedDate: currentState.selectedDate,
selectedStatus: currentState.selectedStatus,
));
}
}
// ─── Update Filters ───────────────────────────────────────────────────────
Future<void> _onUpdateDate(
UpdateScanHistoryDateEvent event,
Emitter<ScanHistoryState> emit,
) async {
add(FetchScanHistoryEvent(
date: event.date,
clearDate: event.date == null,
status: state.selectedStatus,
));
}
Future<void> _onUpdateStatus(
UpdateScanHistoryStatusEvent event,
Emitter<ScanHistoryState> emit,
) async {
add(FetchScanHistoryEvent(
date: state.selectedDate,
status: event.status,
));
}
// ─── Search ───────────────────────────────────────────────────────────────
void _onSearchScanHistory(
SearchScanHistoryEvent event,
Emitter<ScanHistoryState> emit,
) {
List<ScanHistory> allItems = [];
String query = event.query.trim().toLowerCase();
if (state is ScanHistoryLoaded) {
allItems = (state as ScanHistoryLoaded).allItems;
} else if (state is ScanHistoryDetailState) {
allItems = (state as ScanHistoryDetailState).allItems;
} else {
return;
}
final filtered = query.isEmpty
? allItems
: allItems.where((item) {
return item.customerName.toLowerCase().contains(query) ||
item.customerEmail.toLowerCase().contains(query) ||
item.bookingNumber.toLowerCase().contains(query) ||
item.attractionTitle.toLowerCase().contains(query) ||
item.status.toLowerCase().contains(query) ||
item.cardType.toLowerCase().contains(query);
}).toList();
if (state is ScanHistoryLoaded) {
emit((state as ScanHistoryLoaded).copyWith(
filteredItems: filtered,
searchQuery: event.query,
));
} else if (state is ScanHistoryDetailState) {
final s = state as ScanHistoryDetailState;
emit(ScanHistoryDetailState(
selectedItem: s.selectedItem,
allItems: s.allItems,
filteredItems: filtered,
searchQuery: event.query,
selectedDate: s.selectedDate,
selectedStatus: s.selectedStatus,
));
}
}
// ─── Select Detail ────────────────────────────────────────────────────────
void _onSelectScanHistory(
SelectScanHistoryEvent event,
Emitter<ScanHistoryState> emit,
) {
if (state is ScanHistoryLoaded) {
final s = state as ScanHistoryLoaded;
emit(ScanHistoryDetailState(
selectedItem: event.selectedItem,
allItems: s.allItems,
filteredItems: s.filteredItems,
searchQuery: s.searchQuery,
selectedDate: s.selectedDate,
selectedStatus: s.selectedStatus,
));
}
}
// ─── Clear / Back ─────────────────────────────────────────────────────────
void _onClearSelectedScanHistory(
ClearSelectedScanHistoryEvent event,
Emitter<ScanHistoryState> emit,
) {
if (state is ScanHistoryDetailState) {
final s = state as ScanHistoryDetailState;
emit(ScanHistoryLoaded(
allItems: s.allItems,
filteredItems: s.filteredItems,
searchQuery: s.searchQuery,
selectedDate: s.selectedDate,
selectedStatus: s.selectedStatus,
));
}
}
}

View File

@@ -0,0 +1,70 @@
part of 'scan_history_bloc.dart';
abstract class ScanHistoryEvent extends Equatable {
const ScanHistoryEvent();
@override
List<Object?> get props => [];
}
/// Triggered to fetch the scan history list from the API
class FetchScanHistoryEvent extends ScanHistoryEvent {
final DateTime? date;
final String? status;
final bool clearDate; // Explicitly set to true if we want to clear the date filter
const FetchScanHistoryEvent({this.date, this.status, this.clearDate = false});
@override
List<Object?> get props => [date, status, clearDate];
}
/// Triggered to refresh the scan history list (pull-to-refresh)
class RefreshScanHistoryEvent extends ScanHistoryEvent {
const RefreshScanHistoryEvent();
}
/// Triggered to update the selected date filter
class UpdateScanHistoryDateEvent extends ScanHistoryEvent {
final DateTime? date; // Null means 'All'
const UpdateScanHistoryDateEvent({this.date});
@override
List<Object?> get props => [date];
}
/// Triggered to update the selected status filter
class UpdateScanHistoryStatusEvent extends ScanHistoryEvent {
final String status;
const UpdateScanHistoryStatusEvent({required this.status});
@override
List<Object?> get props => [status];
}
/// Triggered to search/filter scan history by keyword
class SearchScanHistoryEvent extends ScanHistoryEvent {
final String query;
const SearchScanHistoryEvent({required this.query});
@override
List<Object?> get props => [query];
}
/// Triggered to select a specific scan history item for detail view
class SelectScanHistoryEvent extends ScanHistoryEvent {
final ScanHistory selectedItem;
const SelectScanHistoryEvent({required this.selectedItem});
@override
List<Object?> get props => [selectedItem];
}
/// Triggered to clear the selected item (navigate back from detail)
class ClearSelectedScanHistoryEvent extends ScanHistoryEvent {
const ClearSelectedScanHistoryEvent();
}

View File

@@ -0,0 +1,96 @@
part of 'scan_history_bloc.dart';
abstract class ScanHistoryState extends Equatable {
final DateTime? selectedDate;
final String selectedStatus;
const ScanHistoryState({
this.selectedDate,
required this.selectedStatus,
});
@override
List<Object?> get props => [selectedDate, selectedStatus];
}
class ScanHistoryInitial extends ScanHistoryState {
ScanHistoryInitial({DateTime? date, String? status})
: super(
selectedDate: date, // Null means 'All'
selectedStatus: status ?? 'all',
);
}
class ScanHistoryLoading extends ScanHistoryState {
const ScanHistoryLoading({super.selectedDate, required super.selectedStatus});
}
class ScanHistoryLoaded extends ScanHistoryState {
final List<ScanHistory> allItems;
final List<ScanHistory> filteredItems;
final String searchQuery;
final bool isRefreshing;
const ScanHistoryLoaded({
super.selectedDate,
required super.selectedStatus,
required this.allItems,
required this.filteredItems,
this.searchQuery = '',
this.isRefreshing = false,
});
ScanHistoryLoaded copyWith({
List<ScanHistory>? allItems,
List<ScanHistory>? filteredItems,
String? searchQuery,
bool? isRefreshing,
DateTime? selectedDate,
bool clearDate = false,
String? selectedStatus,
}) {
return ScanHistoryLoaded(
allItems: allItems ?? this.allItems,
filteredItems: filteredItems ?? this.filteredItems,
searchQuery: searchQuery ?? this.searchQuery,
isRefreshing: isRefreshing ?? this.isRefreshing,
selectedDate: clearDate ? null : (selectedDate ?? this.selectedDate),
selectedStatus: selectedStatus ?? this.selectedStatus,
);
}
@override
List<Object?> get props => [allItems, filteredItems, searchQuery, isRefreshing, selectedDate, selectedStatus];
}
class ScanHistoryDetailState extends ScanHistoryState {
final ScanHistory selectedItem;
final List<ScanHistory> allItems;
final List<ScanHistory> filteredItems;
final String searchQuery;
const ScanHistoryDetailState({
super.selectedDate,
required super.selectedStatus,
required this.selectedItem,
required this.allItems,
required this.filteredItems,
required this.searchQuery,
});
@override
List<Object?> get props => [selectedItem, allItems, filteredItems, searchQuery, selectedDate, selectedStatus];
}
class ScanHistoryError extends ScanHistoryState {
final String errorMessage;
const ScanHistoryError({
super.selectedDate,
required super.selectedStatus,
required this.errorMessage,
});
@override
List<Object?> get props => [errorMessage, selectedDate, selectedStatus];
}

View File

@@ -1,6 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../viewmodels/scan_history_viewmodel.dart';
import '../models/scan_history_model.dart';
import '../models/scan_history_model_old.dart';
// EVENTS
abstract class ScanHistoryEvent {}

View File

@@ -0,0 +1,25 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repositories/scan_history_repository.dart';
import '../../models/scan_history_details_model.dart';
import 'package:equatable/equatable.dart';
part 'scan_history_details_event.dart';
part 'scan_history_details_state.dart';
class ScanHistoryDetailsBloc extends Bloc<ScanHistoryDetailsEvent, ScanHistoryDetailsState> {
final ScanHistoryRepository _repository;
ScanHistoryDetailsBloc(this._repository) : super(ScanHistoryDetailsInitial()) {
on<FetchScanHistoryDetails>(_onFetchScanHistoryDetails);
}
Future<void> _onFetchScanHistoryDetails(
FetchScanHistoryDetails event, Emitter<ScanHistoryDetailsState> emit) async {
emit(ScanHistoryDetailsLoading());
try {
final details = await _repository.fetchScanHistoryDetails(event.id);
emit(ScanHistoryDetailsLoaded(details));
} catch (e) {
emit(ScanHistoryDetailsError(e.toString()));
}
}
}

View File

@@ -0,0 +1,19 @@
part of 'scan_history_details_bloc.dart';
abstract class ScanHistoryDetailsEvent extends Equatable {
const ScanHistoryDetailsEvent();
@override
List<Object> get props => [];
}
class FetchScanHistoryDetails extends ScanHistoryDetailsEvent {
final int id;
const FetchScanHistoryDetails(this.id);
@override
List<Object> get props => [id];
}

View File

@@ -0,0 +1,30 @@
part of 'scan_history_details_bloc.dart';
abstract class ScanHistoryDetailsState extends Equatable {
const ScanHistoryDetailsState();
@override
List<Object?> get props => [];
}
class ScanHistoryDetailsInitial extends ScanHistoryDetailsState {}
class ScanHistoryDetailsLoading extends ScanHistoryDetailsState {}
class ScanHistoryDetailsLoaded extends ScanHistoryDetailsState {
final ScanHistoryDetails details;
const ScanHistoryDetailsLoaded(this.details);
@override
List<Object?> get props => [details];
}
class ScanHistoryDetailsError extends ScanHistoryDetailsState {
final String message;
const ScanHistoryDetailsError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,121 @@
class ScanHistoryDetails {
final int id;
final String qrCode;
final String qrNumber;
final String status;
final String reason;
final int partnerId;
final int attractionId;
final String attractionTitle;
final String cityName;
final String bookingNumber;
final String customerName;
final String customerMobile;
final String customerEmail;
final String cardType;
final int scannedByPartnerStaffId;
final String scannedByPartnerStaffName;
final String validUpto;
final String activatedAt;
final String checkedInDatetime;
final String qrExpiresAt;
final String createdAt;
final String updatedAt;
ScanHistoryDetails({
required this.id,
required this.qrCode,
required this.qrNumber,
required this.status,
required this.reason,
required this.partnerId,
required this.attractionId,
required this.attractionTitle,
required this.cityName,
required this.bookingNumber,
required this.customerName,
required this.customerMobile,
required this.customerEmail,
required this.cardType,
required this.scannedByPartnerStaffId,
required this.scannedByPartnerStaffName,
required this.validUpto,
required this.activatedAt,
required this.checkedInDatetime,
required this.qrExpiresAt,
required this.createdAt,
required this.updatedAt,
});
/// ✅ Factory with N/A fallback
factory ScanHistoryDetails.fromJson(Map<String, dynamic> json) {
return ScanHistoryDetails(
id: json['id'] ?? 0,
qrCode: _value(json['qrCode']),
qrNumber: _value(json['qrNumber']),
status: _value(json['status']),
reason: _value(json['reason']),
partnerId: json['partnerId'] ?? 0,
attractionId: json['attractionId'] ?? 0,
attractionTitle: _value(json['attractionTitle']),
cityName: _value(json['cityName']),
bookingNumber: _value(json['bookingNumber']),
customerName: _value(json['customerName']),
customerMobile: _value(json['customerMobile']),
customerEmail: _value(json['customerEmail']),
cardType: _value(json['cardType']),
scannedByPartnerStaffId: json['scannedByPartnerStaffId'] ?? 0,
scannedByPartnerStaffName:
_value(json['scannedByPartnerStaffName']),
validUpto: _value(json['validUpto']),
activatedAt: _value(json['activatedAt']),
checkedInDatetime: _value(json['checkedInDatetime']),
qrExpiresAt: _value(json['qrExpiresAt']),
createdAt: _value(json['createdAt']),
updatedAt: _value(json['updatedAt']),
);
}
/// ✅ Convert back to JSON (optional but useful)
Map<String, dynamic> toJson() {
return {
'id': id,
'qrCode': qrCode,
'qrNumber': qrNumber,
'status': status,
'reason': reason,
'partnerId': partnerId,
'attractionId': attractionId,
'attractionTitle': attractionTitle,
'cityName': cityName,
'bookingNumber': bookingNumber,
'customerName': customerName,
'customerMobile': customerMobile,
'customerEmail': customerEmail,
'cardType': cardType,
'scannedByPartnerStaffId': scannedByPartnerStaffId,
'scannedByPartnerStaffName': scannedByPartnerStaffName,
'validUpto': validUpto,
'activatedAt': activatedAt,
'checkedInDatetime': checkedInDatetime,
'qrExpiresAt': qrExpiresAt,
'createdAt': createdAt,
'updatedAt': updatedAt,
};
}
/// ✅ Common helper for null / empty / "null"
static String _value(dynamic val) {
if (val == null) return "N/A";
if (val.toString().trim().isEmpty) return "N/A";
if (val.toString().toLowerCase() == "null") return "N/A";
return val.toString();
}
}

View File

@@ -1,13 +1,148 @@
class ScanHistoryModel {
final String passId;
final String? reason;
final String time;
final String status; // "Success" or "Failed"
import 'package:intl/intl.dart';
ScanHistoryModel({
required this.passId,
this.reason,
required this.time,
required this.status,
class ScanHistoryResponse {
final List<ScanHistory> data;
ScanHistoryResponse({
required this.data,
});
factory ScanHistoryResponse.fromJson(Map<String, dynamic> json) {
return ScanHistoryResponse(
data: (json['data'] as List<dynamic>?)
?.map((e) => ScanHistory.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
}
}
class ScanHistory {
final int id;
final String passId;
final String qrCode;
final String bookingNumber;
final int attractionId;
final String attractionTitle;
final String customerName;
final String customerEmail;
final String cardType;
final int? scannedByPartnerStaffId;
final String scannedByPartnerStaffName;
final String status;
final String reason; // ✅ ADDED
final DateTime? checkedInDatetime;
final DateTime? activatedAt;
final DateTime? qrExpiresAt;
final DateTime? validUpto;
final DateTime? createdAt;
ScanHistory({
required this.id,
required this.passId,
required this.qrCode,
required this.bookingNumber,
required this.attractionId,
required this.attractionTitle,
required this.customerName,
required this.customerEmail,
required this.cardType,
this.scannedByPartnerStaffId,
required this.scannedByPartnerStaffName,
required this.status,
required this.reason, // ✅ ADDED
this.checkedInDatetime,
this.activatedAt,
this.qrExpiresAt,
this.validUpto,
this.createdAt,
});
factory ScanHistory.fromJson(Map<String, dynamic> json) {
return ScanHistory(
id: json['id'] ?? 0,
passId: (json['passId'] ?? 'N/A').toString(),
qrCode: (json['qrCode'] ?? '').toString(),
bookingNumber: (json['bookingNumber'] ?? 'N/A').toString(),
attractionId: json['attractionId'] ?? 0,
attractionTitle: (json['attractionTitle'] ?? '').toString(),
customerName: (json['customerName'] ?? '').toString(),
customerEmail: (json['customerEmail'] ?? '').toString(),
cardType: (json['cardType'] ?? '').toString(),
scannedByPartnerStaffId: json['scannedByPartnerStaffId'],
scannedByPartnerStaffName:
(json['scannedByPartnerStaffName'] ?? '').toString(),
status: (json['status'] ?? '').toString(),
reason: (json['reason'] ?? '').toString(), // ✅ ADDED
checkedInDatetime: _parseDate(json['checkedInDatetime']),
activatedAt: _parseDate(json['activatedAt']),
qrExpiresAt: _parseDate(json['qrExpiresAt']),
validUpto: _parseDate(json['validUpto']),
createdAt: _parseDate(json['createdAt']),
);
}
/// 🔥 Safe Date Parser
static DateTime? _parseDate(dynamic date) {
if (date == null) return null;
try {
return DateTime.parse(date.toString());
} catch (e) {
return null;
}
}
/// 🔥 Date Formatter
static final DateFormat _formatter =
DateFormat('dd MMM yyyy, hh:mm a');
String _formatDate(DateTime? date) {
if (date == null) return 'N/A';
return _formatter.format(date);
}
/// ✅ UI Friendly Getters
String get displayCheckedIn => _formatDate(checkedInDatetime);
String get displayActivatedAt => _formatDate(activatedAt);
String get displayQrExpiresAt => _formatDate(qrExpiresAt);
String get displayValidUpto => _formatDate(validUpto);
String get displayCreatedAt => _formatDate(createdAt);
String get displayStaffName =>
scannedByPartnerStaffName.isNotEmpty
? scannedByPartnerStaffName
: 'N/A';
String get displayStatus =>
status.isNotEmpty ? status : 'N/A';
String get displayReason =>
reason.isNotEmpty ? reason : 'N/A';
String get displayCustomerName =>
customerName.isNotEmpty ? customerName : 'N/A';
String get displayEmail =>
customerEmail.isNotEmpty ? customerEmail : 'N/A';
String get displayAttraction =>
attractionTitle.isNotEmpty ? attractionTitle : 'N/A';
/// 🎯 STATUS HELPERS (VERY USEFUL FOR UI)
bool get isSuccess => status.toLowerCase() == 'success';
bool get isFailed => status.toLowerCase() == 'failed';
/// 🎨 Optional: Status Color Helper (UI usage)
String get statusColorHex {
if (isSuccess) return '#4CAF50'; // green
if (isFailed) return '#F44336'; // red
return '#9E9E9E'; // grey
}
}

View File

@@ -0,0 +1,13 @@
class ScanHistoryModel {
final String passId;
final String? reason;
final String time;
final String status; // "Success" or "Failed"
ScanHistoryModel({
required this.passId,
this.reason,
required this.time,
required this.status,
});
}

View File

@@ -1,18 +1,49 @@
import '../../network_api_service/api_service/api_service.dart';
import '../../network_api_service/api_urls/api_urls.dart';
import '../models/scan_history_model.dart';
import '../models/scan_history_details_model.dart';
class ScanHistoryRepository {
Future<List<ScanHistoryModel>> fetchScanHistory(DateTime date, String statusFilter) async {
await Future.delayed(const Duration(milliseconds: 500));
final ApiService _apiService = ApiService();
// Mock data
List<ScanHistoryModel> data = [
ScanHistoryModel(passId: '#P214125125', reason: 'Already Used', time: '05/11/24 on 11:00PM', status: 'Failed'),
ScanHistoryModel(passId: '#P214125125', time: '05/11/24 on 11:00PM', status: 'Success'),
ScanHistoryModel(passId: '#P214125125', reason: 'Invalid Code', time: '05/11/24 on 11:00PM', status: 'Failed'),
ScanHistoryModel(passId: '#P214125125', time: '05/11/24 on 11:00PM', status: 'Success'),
];
/// Fetch Scan History List with filters
Future<List<ScanHistory>> fetchScanHistory({String? date, String? status}) async {
try {
Map<String, dynamic> queryParameters = {};
if (date != null) queryParameters['date'] = date;
if (status != null && status != 'all') queryParameters['status'] = status.toLowerCase();
if (statusFilter == 'All Status') return data;
return data.where((item) => item.status == statusFilter).toList();
final response = await _apiService.get(
ApiUrls.scanHistory,
queryParameters: queryParameters,
);
final ScanHistoryResponse scanResponse =
ScanHistoryResponse.fromJson(response.data);
return scanResponse.data;
} catch (e) {
throw Exception('Failed to fetch scan history: $e');
}
}
}
/// Fetch Scan History Details by ID
Future<ScanHistoryDetails> fetchScanHistoryDetails(int id) async {
try {
final response = await _apiService.get("${ApiUrls.scanHistory}/$id");
// Check if data is wrapped in a 'data' key or is at the root
final dynamic rawData = response.data;
if (rawData is Map<String, dynamic>) {
if (rawData.containsKey('data')) {
return ScanHistoryDetails.fromJson(rawData['data']);
}
return ScanHistoryDetails.fromJson(rawData);
}
throw Exception('Invalid response format');
} catch (e) {
throw Exception('Failed to fetch scan history details: $e');
}
}
}

View File

@@ -0,0 +1,18 @@
import '../models/scan_history_model_old.dart';
class ScanHistoryRepository {
Future<List<ScanHistoryModel>> fetchScanHistory(DateTime date, String statusFilter) async {
await Future.delayed(const Duration(milliseconds: 500));
// Mock data
List<ScanHistoryModel> data = [
ScanHistoryModel(passId: '#P214125125', reason: 'Already Used', time: '05/11/24 on 11:00PM', status: 'Failed'),
ScanHistoryModel(passId: '#P214125125', time: '05/11/24 on 11:00PM', status: 'Success'),
ScanHistoryModel(passId: '#P214125125', reason: 'Invalid Code', time: '05/11/24 on 11:00PM', status: 'Failed'),
ScanHistoryModel(passId: '#P214125125', time: '05/11/24 on 11:00PM', status: 'Success'),
];
if (statusFilter == 'All Status') return data;
return data.where((item) => item.status == statusFilter).toList();
}
}

View File

@@ -1,5 +1,5 @@
import '../repositories/scan_history_repository.dart';
import '../models/scan_history_model.dart';
import '../repositories/scan_history_repository_old.dart';
import '../models/scan_history_model_old.dart';
class ScanHistoryViewModel {
final ScanHistoryRepository repository;

View File

@@ -2,12 +2,11 @@ import 'package:citycards_partner_flutter/core/app_router.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/scan_history_detail_bloc.dart';
import '../viewmodels/scan_history_detail_viewmodel.dart';
import '../blocs/scan_history_details/scan_history_details_bloc.dart';
class ScanHistoryDetailPage extends StatelessWidget {
final String passId;
final int passId;
const ScanHistoryDetailPage({super.key, required this.passId});
TextStyle _headerStyle() => const TextStyle(
@@ -67,18 +66,18 @@ class ScanHistoryDetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ScanHistoryDetailBloc()..add(LoadScanHistoryDetail(passId)),
return BlocProvider.value(
value: context.read<ScanHistoryDetailsBloc>()..add(FetchScanHistoryDetails(passId)),
child: Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: BlocBuilder<ScanHistoryDetailBloc, ScanHistoryDetailState>(
child: BlocBuilder<ScanHistoryDetailsBloc, ScanHistoryDetailsState>(
builder: (context, state) {
if (state is ScanHistoryDetailLoading) {
if (state is ScanHistoryDetailsLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is ScanHistoryDetailLoaded) {
final vm = PassDetailViewModel.fromMap(state.data);
if (state is ScanHistoryDetailsLoaded) {
final data = state.details;
return Column(
children: [
Padding(
@@ -101,21 +100,22 @@ class ScanHistoryDetailPage extends StatelessWidget {
Expanded(
child: Center(
child: Text(
'#${vm.displayPassId}',
'#${data.bookingNumber}',
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 28,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
const SizedBox(width: 12),
InkWell(
onTap: (){
Navigator.pushNamed(context, AppRouter.profileScreen);
},
child: Image.asset("assets/app/profile.png",scale: 4)),
InkWell(
onTap: () {
Navigator.pushNamed(context, AppRouter.profileScreen);
},
child: Image.asset("assets/app/profile.png", scale: 4)),
],
),
),
@@ -130,21 +130,21 @@ class ScanHistoryDetailPage extends StatelessWidget {
// Pass Summary
Text('Pass Summary', style: _headerStyle()),
_doubleDivider(),
_twoColumnRow('Card Type', vm.cardType, 'Validity', vm.validity),
_twoColumnRow('Card Type', data.cardType, 'Validity', data.status),
const SizedBox(height: 16),
Text('Customer Details', style: _headerStyle()),
_doubleDivider(),
_twoColumnRow('Customer Name', vm.customerName, 'Phone', vm.phone),
_twoColumnRow('Customer Name', data.customerName, 'Phone', data.customerMobile),
_divider(),
Text('Email', style: _labelStyle()),
const SizedBox(height: 6),
Text(vm.email, style: _valueStyle()),
Text(data.customerEmail, style: _valueStyle()),
const SizedBox(height: 16),
Text('Attraction Details', style: _headerStyle()),
_doubleDivider(),
_twoColumnRow('City', vm.city, 'Attraction Name', vm.attractionName),
_twoColumnRow('City', data.cityName, 'Attraction Name', data.attractionTitle),
const SizedBox(height: 60),
],
@@ -155,7 +155,7 @@ class ScanHistoryDetailPage extends StatelessWidget {
);
}
if (state is ScanHistoryDetailError) {
if (state is ScanHistoryDetailsError) {
return Center(child: Text(state.message));
}

View File

@@ -1,23 +1,29 @@
import 'package:citycards_partner_flutter/core/app_router.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
import '../blocs/scan_history_bloc.dart';
import '../repositories/scan_history_repository.dart';
import '../viewmodels/scan_history_viewmodel.dart';
import 'package:intl/intl.dart';
import '../blocs/scan_history/scan_history_bloc.dart';
import '../models/scan_history_model.dart';
class ScanHistoryPage extends StatelessWidget {
class ScanHistoryPage extends StatefulWidget {
const ScanHistoryPage({super.key});
@override
State<ScanHistoryPage> createState() => _ScanHistoryPageState();
}
class _ScanHistoryPageState extends State<ScanHistoryPage> {
@override
void initState() {
super.initState();
// ✅ API call when page opens
context.read<ScanHistoryBloc>().add(const FetchScanHistoryEvent());
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ScanHistoryBloc(
viewModel: ScanHistoryViewModel(repository: ScanHistoryRepository()),
)..add(LoadScanHistory()),
child: const _ScanHistoryView(),
);
return const _ScanHistoryView();
}
}
@@ -32,7 +38,7 @@ class _ScanHistoryView extends StatelessWidget {
backgroundColor: Colors.white,
elevation: 0,
centerTitle: true,
title: Text(
title: const Text(
"Scan History",
style: TextStyle(
fontWeight: FontWeight.w600,
@@ -40,15 +46,13 @@ class _ScanHistoryView extends StatelessWidget {
color: Colors.black,
),
),
leading: Padding(
padding: EdgeInsets.only(left: 12),
leading: Padding(
padding: const EdgeInsets.only(left: 12),
child: InkWell(
onTap: () {
Navigator.pop(context);
},
child: CircleAvatar(
maxRadius: 44,
backgroundColor: Color(0xffF95F62),
onTap: () => Navigator.pop(context),
child: const CircleAvatar(
maxRadius: 44,
backgroundColor: Color(0xffF95F62),
child: Icon(Icons.arrow_back, color: Colors.white)),
),
),
@@ -56,13 +60,11 @@ class _ScanHistoryView extends StatelessWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: InkWell(
onTap: (){
Navigator.pushNamed(context, AppRouter.profileScreen);
},
child: Image.asset("assets/app/profile.png",scale: 4),
onTap: () => Navigator.pushNamed(context, AppRouter.profileScreen),
child: Image.asset("assets/app/profile.png", scale: 4),
),
),
SizedBox(width: 2,)
const SizedBox(width: 2)
],
),
body: Padding(
@@ -74,108 +76,100 @@ class _ScanHistoryView extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
const Text(
"View a detailed log of all scanned QR codes, including timestamps, results, and customer details for easy tracking and verification.",
style: TextStyle(
fontSize: 14,
color: Colors.black,fontWeight: FontWeight.w400
),
style: TextStyle(fontSize: 14, color: Colors.black, fontWeight: FontWeight.w400),
),
const SizedBox(height: 16),
// Date + Status Row
// Filters
Row(
children: [
// Date Filter
Expanded(
child: GestureDetector(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: state.selectedDate,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.light(
primary: Color(0xffF95F62),
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("Date", style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(color: const Color(0xffF8F8F8), borderRadius: BorderRadius.circular(8)),
child: Row(
children: [
Expanded(
child: Text(
state.selectedDate == null ? "All" : DateFormat('dd/MM/yy').format(state.selectedDate!),
style: const TextStyle(fontSize: 13),
),
),
child: child!,
);
},
);
if (picked != null) {
bloc.add(UpdateDate(picked));
}
},
child: Row(
children: [
Text("Date",
style: TextStyle(
fontSize: 13, color: Colors.black,fontWeight: FontWeight.w400)),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xffF8F8F8),
borderRadius: BorderRadius.circular(50),
),
child: Row(
children: [
const Icon(Icons.calendar_month,
color: Color(0xffF95F62), size: 18),
const SizedBox(width: 8),
Text(
"${state.selectedDate.day.toString().padLeft(2, '0')}/${state.selectedDate.month.toString().padLeft(2, '0')}/${state.selectedDate.year.toString().substring(2)}",
style: TextStyle(fontSize: 13),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
icon: const Icon(Icons.calendar_month, color: Color(0xffF95F62), size: 20),
onPressed: () async {
final picked = await showDatePicker(
context: context,
initialDate: state.selectedDate ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.light(
primary: Color(0xffF95F62), // header & selected day bg
onPrimary: Colors.white, // header text & selected day text
onSurface: Colors.black, // calendar day text
),
),
child: child!,
);
},
);
if (picked != null) bloc.add(UpdateScanHistoryDateEvent(date: picked));
},
),
if (state.selectedDate != null)
IconButton(
padding: const EdgeInsets.only(left: 8),
constraints: const BoxConstraints(),
icon: const Icon(Icons.close, color: Colors.grey, size: 18),
onPressed: () => bloc.add(const UpdateScanHistoryDateEvent(date: null)),
),
],
),
],
),
],
),
),
],
),
),
const SizedBox(width: 16),
const SizedBox(width: 12),
// Status Filter
Expanded(
child: Row(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Status",
style: TextStyle(
fontSize: 14, color: Colors.black,fontWeight: FontWeight.w400)),
const SizedBox(width: 12),
Expanded(
child: Container(
height: 35,
decoration: BoxDecoration(
color: const Color(0xffF8F8F8),
borderRadius: BorderRadius.circular(50),
),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
dropdownColor: Colors.white,
value: state.selectedStatus,
icon: const Icon(Icons.keyboard_arrow_down_rounded,size: 26,
color: Color(0xffF95F62)),
items: const [
DropdownMenuItem(
value: 'All Status',
child: Text("All Status")),
DropdownMenuItem(
value: 'Failed', child: Text("Failed")),
DropdownMenuItem(
value: 'Success',
child: Text("Success")),
],
onChanged: (val) {
if (val != null) bloc.add(UpdateStatus(val));
},
style: TextStyle(
fontSize: 14, color: Colors.black,fontWeight: FontWeight.w400),
),
const Text("Status", style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Container(
height: 40,
decoration: BoxDecoration(color: const Color(0xffF8F8F8), borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 10),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
value: state.selectedStatus,
icon: const Icon(Icons.keyboard_arrow_down, color: Color(0xffF95F62)),
items: const [
DropdownMenuItem(value: 'all', child: Text("All", style: TextStyle(fontSize: 13))),
DropdownMenuItem(value: 'success', child: Text("Success", style: TextStyle(fontSize: 13))),
DropdownMenuItem(value: 'failed', child: Text("Failed", style: TextStyle(fontSize: 13))),
],
onChanged: (val) {
if (val != null) bloc.add(UpdateScanHistoryStatusEvent(status: val));
},
),
),
),
@@ -186,9 +180,7 @@ class _ScanHistoryView extends StatelessWidget {
),
const SizedBox(height: 20),
// Data Section
Expanded(child: _buildStateUI(state,context)),
Expanded(child: _buildStateUI(state, context)),
],
);
},
@@ -197,43 +189,63 @@ class _ScanHistoryView extends StatelessWidget {
);
}
Widget _buildStateUI(ScanHistoryState state,BuildContext context) {
if (state.isLoading) {
return const Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
);
} else if (state.error != null) {
return Center(child: Text(state.error!));
} else if (state.history.isEmpty) {
return Center(
child: Text("No records found",
style: TextStyle(color: Colors.black54)),
);
Widget _buildStateUI(ScanHistoryState state, BuildContext context) {
if (state is ScanHistoryLoading) {
return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62)));
} else if (state is ScanHistoryError) {
return Center(child: Text(state.errorMessage));
}
List<ScanHistory> displayItems = [];
if (state is ScanHistoryLoaded) displayItems = state.filteredItems;
else if (state is ScanHistoryDetailState) displayItems = state.filteredItems;
if (displayItems.isEmpty && state is! ScanHistoryInitial) {
return const Center(child: Text("No records found", style: TextStyle(color: Colors.black54)));
}
return ListView.builder(
itemCount: state.history.length,
itemBuilder: (context, index) {
final item = state.history[index];
return _buildCard(item,context);
},
return RefreshIndicator(
onRefresh: () async => context.read<ScanHistoryBloc>().add(const RefreshScanHistoryEvent()),
child: ListView.builder(
itemCount: displayItems.length,
itemBuilder: (context, index) => _buildCard(displayItems[index], context),
),
);
}
Widget _buildCard(ScanHistoryModel item,BuildContext context) {
final isSuccess = item.status == "Success";
Widget _buildCard(ScanHistory item, BuildContext context) {
final status = item.status.toLowerCase();
Color bgColor = const Color(0xffE9F9EF);
Color borderColor = const Color(0xffB5E5C1);
IconData icon = Icons.check_circle;
Color iconColor = const Color(0xff2DCC70);
if (status == "failed") {
bgColor = const Color(0xffFCEAEA);
borderColor = const Color(0xffF5B1B1);
icon = Icons.cancel;
iconColor = const Color(0xffF95F62);
} else if (status == "pending") {
bgColor = Colors.orange.shade50;
borderColor = Colors.orange.shade200;
icon = Icons.pending;
iconColor = Colors.orange;
}
return InkWell(
onTap: (){
Navigator.pushNamed(context, AppRouter.scanHistoryDetailPage);
onTap: () {
context.read<ScanHistoryBloc>().add(SelectScanHistoryEvent(selectedItem: item));
Navigator.pushNamed(context, AppRouter.scanHistoryDetailPage, arguments: item.id).then((_) {
if (context.mounted) context.read<ScanHistoryBloc>().add(const ClearSelectedScanHistoryEvent());
});
},
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSuccess ? const Color(0xffE9F9EF) : const Color(0xffFCEAEA),
color: bgColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSuccess ? const Color(0xffB5E5C1) : const Color(0xffF5B1B1)),
border: Border.all(color: borderColor),
),
child: Row(
children: [
@@ -241,38 +253,17 @@ class _ScanHistoryView extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Pass ID: ${item.passId}",
style: TextStyle(
fontWeight: FontWeight.w600, fontSize: 13)),
if (item.reason != null) ...[
const SizedBox(height: 4),
Text("Reason: ${item.reason!}",
style: TextStyle(
fontSize: 12, color: Colors.black87)),
],
Text("Pass ID: ${item.bookingNumber}", style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13)),
const SizedBox(height: 4),
Text("Time: ${item.time}",
style: TextStyle(
fontSize: 12, color: Colors.black87)),
Text("Reason: ${item.reason}", style: const TextStyle(fontSize: 12, color: Colors.black87)),
const SizedBox(height: 4),
Text("Time: ${item.displayCreatedAt}", style: const TextStyle(fontSize: 12, color: Colors.black87)),
const SizedBox(height: 8),
Row(
children: [
Icon(
isSuccess ? Icons.check_circle : Icons.cancel,
color: isSuccess
? const Color(0xff2DCC70)
: const Color(0xffF95F62),
size: 20,
),
Icon(icon, color: iconColor, size: 20),
const SizedBox(width: 6),
Text(
item.status,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 24,
),
),
Text(item.status.toUpperCase(), style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w600, fontSize: 24)),
],
),
],
@@ -284,4 +275,4 @@ class _ScanHistoryView extends StatelessWidget {
),
);
}
}
}

View File

@@ -0,0 +1,99 @@
import 'dart:io';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart'; // Import ImagePicker
import '../../repository/raise_ticket_repository.dart';
part 'raise_ticket_event.dart';
part 'raise_ticket_state.dart';
class RaiseTicketBloc extends Bloc<RaiseTicketEvent, RaiseTicketState> {
final RaiseTicketRepository _raiseTicketRepository;
final ImagePicker _picker = ImagePicker(); // Initialize ImagePicker
static const int _maxFileSizeInBytes = 5 * 1024 * 1024; // 5 MB limit
RaiseTicketBloc({RaiseTicketRepository? raiseTicketRepository})
: _raiseTicketRepository =
raiseTicketRepository ?? RaiseTicketRepository(),
super(const RaiseTicketState()) {
on<RaiseTicketSubjectChanged>(_onSubjectChanged);
on<RaiseTicketDescriptionChanged>(_onDescriptionChanged);
on<RaiseTicketAttachmentPicked>(_onAttachmentPicked);
on<RaiseTicketAttachmentRemoved>(_onAttachmentRemoved);
on<RaiseTicketSubmitted>(_onRaiseTicketSubmitted);
on<RaiseTicketReset>(_onReset);
}
void _onSubjectChanged(
RaiseTicketSubjectChanged event, Emitter<RaiseTicketState> emit) {
emit(state.copyWith(subject: event.subject));
}
void _onDescriptionChanged(
RaiseTicketDescriptionChanged event, Emitter<RaiseTicketState> emit) {
emit(state.copyWith(description: event.description));
}
Future<void> _onAttachmentPicked(
RaiseTicketAttachmentPicked event, Emitter<RaiseTicketState> emit) async {
// The event now passes the File directly after it's picked in the UI
final File file = event.attachment;
final int fileSize = await file.length();
if (fileSize > _maxFileSizeInBytes) {
emit(state.copyWith(
attachment: null,
fileSizeExceeded: true,
errorMessage: "File size exceeds 5MB limit.",
));
} else {
emit(state.copyWith(
attachment: file,
fileSizeExceeded: false,
errorMessage: null,
));
}
}
void _onAttachmentRemoved(
RaiseTicketAttachmentRemoved event, Emitter<RaiseTicketState> emit) {
emit(state.copyWith(
clearAttachment: true,
fileSizeExceeded: false,
errorMessage: null,
));
}
Future<void> _onRaiseTicketSubmitted(
RaiseTicketSubmitted event, Emitter<RaiseTicketState> emit) async {
if (state.subject.isEmpty || state.description.isEmpty) {
emit(state.copyWith(
errorMessage: "Subject and description cannot be empty."));
return;
}
if (state.fileSizeExceeded) {
emit(state.copyWith(errorMessage: "Cannot submit: file size exceeded."));
return;
}
emit(state.copyWith(status: RaiseTicketStatus.loading, errorMessage: null));
try {
await _raiseTicketRepository.raiseTicket(
subject: state.subject,
description: state.description,
attachment: state.attachment,
);
emit(state.copyWith(status: RaiseTicketStatus.success));
} catch (e) {
emit(state.copyWith(
status: RaiseTicketStatus.failure,
errorMessage: e.toString(),
));
}
}
void _onReset(
RaiseTicketReset event, Emitter<RaiseTicketState> emit) {
emit(const RaiseTicketState());
}
}

View File

@@ -0,0 +1,44 @@
part of 'raise_ticket_bloc.dart';
abstract class RaiseTicketEvent extends Equatable {
const RaiseTicketEvent();
@override
List<Object?> get props => [];
}
class RaiseTicketSubjectChanged extends RaiseTicketEvent {
final String subject;
const RaiseTicketSubjectChanged(this.subject);
@override
List<Object> get props => [subject];
}
class RaiseTicketDescriptionChanged extends RaiseTicketEvent {
final String description;
const RaiseTicketDescriptionChanged(this.description);
@override
List<Object> get props => [description];
}
class RaiseTicketAttachmentPicked extends RaiseTicketEvent {
final File attachment;
const RaiseTicketAttachmentPicked(this.attachment);
@override
List<Object> get props => [attachment];
}
class RaiseTicketAttachmentRemoved extends RaiseTicketEvent {
const RaiseTicketAttachmentRemoved();
}
class RaiseTicketSubmitted extends RaiseTicketEvent {
const RaiseTicketSubmitted();
}
class RaiseTicketReset extends RaiseTicketEvent {
const RaiseTicketReset();
}

View File

@@ -0,0 +1,43 @@
part of 'raise_ticket_bloc.dart';
enum RaiseTicketStatus { initial, loading, success, failure }
class RaiseTicketState extends Equatable {
final RaiseTicketStatus status;
final String subject;
final String description;
final File? attachment;
final bool fileSizeExceeded;
final String? errorMessage;
const RaiseTicketState({
this.status = RaiseTicketStatus.initial,
this.subject = '',
this.description = '',
this.attachment,
this.fileSizeExceeded = false,
this.errorMessage,
});
RaiseTicketState copyWith({
RaiseTicketStatus? status,
String? subject,
String? description,
File? attachment,
bool clearAttachment = false,
bool? fileSizeExceeded,
String? errorMessage,
}) {
return RaiseTicketState(
status: status ?? this.status,
subject: subject ?? this.subject,
description: description ?? this.description,
attachment: clearAttachment ? null : attachment ?? this.attachment,
fileSizeExceeded: fileSizeExceeded ?? this.fileSizeExceeded,
errorMessage: errorMessage,
);
}
@override
List<Object?> get props => [status, subject, description, attachment, fileSizeExceeded, errorMessage];
}

View File

@@ -0,0 +1,30 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../model/support_details_model.dart';
import '../../repository/support_details_repository.dart';
part 'support_details_event.dart';
part 'support_details_state.dart';
class SupportDetailsBloc extends Bloc<SupportDetailsEvent, SupportDetailsState> {
final SupportDetailsRepository _repository;
SupportDetailsBloc({SupportDetailsRepository? repository})
: _repository = repository ?? SupportDetailsRepository(),
super(const SupportDetailsInitial()) {
on<FetchSupportDetailsEvent>(_onFetchSupportDetails);
}
Future<void> _onFetchSupportDetails(
FetchSupportDetailsEvent event,
Emitter<SupportDetailsState> emit,
) async {
emit(const SupportDetailsLoading());
try {
final SupportDetailModel data = await _repository.fetchSupportDetails();
emit(SupportDetailsLoaded(supportDetail: data));
} catch (e) {
emit(SupportDetailsError(message: e.toString()));
}
}
}

View File

@@ -0,0 +1,12 @@
part of 'support_details_bloc.dart';
abstract class SupportDetailsEvent extends Equatable {
const SupportDetailsEvent();
@override
List<Object?> get props => [];
}
class FetchSupportDetailsEvent extends SupportDetailsEvent {
const FetchSupportDetailsEvent();
}

View File

@@ -0,0 +1,38 @@
part of 'support_details_bloc.dart';
abstract class SupportDetailsState extends Equatable {
const SupportDetailsState();
@override
List<Object?> get props => [];
}
// Initial state before any event is fired
class SupportDetailsInitial extends SupportDetailsState {
const SupportDetailsInitial();
}
// Loading state while API call is in progress
class SupportDetailsLoading extends SupportDetailsState {
const SupportDetailsLoading();
}
// Success state with fetched data
class SupportDetailsLoaded extends SupportDetailsState {
final SupportDetailModel supportDetail;
const SupportDetailsLoaded({required this.supportDetail});
@override
List<Object?> get props => [supportDetail];
}
// Error state with a message
class SupportDetailsError extends SupportDetailsState {
final String message;
const SupportDetailsError({required this.message});
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,190 @@
// ================= RESPONSE WRAPPER =================
class SupportDetailResponse {
final SupportDetailModel data;
SupportDetailResponse({
required this.data,
});
factory SupportDetailResponse.fromJson(Map<String, dynamic>? json) {
return SupportDetailResponse(
data: json != null
? SupportDetailModel.fromJson(json)
: SupportDetailModel.empty(),
);
}
}
// ================= MAIN MODEL =================
class SupportDetailModel {
final int id;
final int partnerXid;
final int userXid;
final int roleXid;
final String fullName;
final String email;
final String phone;
final String round;
final bool isActive;
final bool isDeleted;
final String lastLoginAt;
final String createdAt;
final String updatedAt;
final SupportRoleModel role;
final PartnerModel partner;
SupportDetailModel({
required this.id,
required this.partnerXid,
required this.userXid,
required this.roleXid,
required this.fullName,
required this.email,
required this.phone,
required this.round,
required this.isActive,
required this.isDeleted,
required this.lastLoginAt,
required this.createdAt,
required this.updatedAt,
required this.role,
required this.partner,
});
factory SupportDetailModel.fromJson(Map<String, dynamic> json) {
return SupportDetailModel(
id: json['id'] ?? 0,
partnerXid: json['partnerXid'] ?? 0,
userXid: json['userXid'] ?? 0,
roleXid: json['roleXid'] ?? 0,
fullName: json['fullName'] ?? "N/A",
email: json['email'] ?? "N/A",
phone: json['phone'] ?? "N/A",
round: json['round'] ?? "N/A",
isActive: json['isActive'] ?? false,
isDeleted: json['isDeleted'] ?? false,
lastLoginAt: json['lastLoginAt'] ?? "N/A",
createdAt: json['createdAt'] ?? "N/A",
updatedAt: json['updatedAt'] ?? "N/A",
role: json['role'] != null
? SupportRoleModel.fromJson(json['role'])
: SupportRoleModel.empty(),
partner: json['partner'] != null
? PartnerModel.fromJson(json['partner'])
: PartnerModel.empty(),
);
}
factory SupportDetailModel.empty() {
return SupportDetailModel(
id: 0,
partnerXid: 0,
userXid: 0,
roleXid: 0,
fullName: "N/A",
email: "N/A",
phone: "N/A",
round: "N/A",
isActive: false,
isDeleted: false,
lastLoginAt: "N/A",
createdAt: "N/A",
updatedAt: "N/A",
role: SupportRoleModel.empty(),
partner: PartnerModel.empty(),
);
}
}
// ================= ROLE MODEL =================
class SupportRoleModel {
final int id;
final String name;
final bool isActive;
final String createdAt;
final String updatedAt;
SupportRoleModel({
required this.id,
required this.name,
required this.isActive,
required this.createdAt,
required this.updatedAt,
});
factory SupportRoleModel.fromJson(Map<String, dynamic> json) {
return SupportRoleModel(
id: json['id'] ?? 0,
name: json['name'] ?? "N/A",
isActive: json['isActive'] ?? false,
createdAt: json['createdAt'] ?? "N/A",
updatedAt: json['updatedAt'] ?? "N/A",
);
}
factory SupportRoleModel.empty() {
return SupportRoleModel(
id: 0,
name: "N/A",
isActive: false,
createdAt: "N/A",
updatedAt: "N/A",
);
}
Map<String, dynamic> toJson() {
return {
"id": id,
"name": name,
"isActive": isActive,
"createdAt": createdAt,
"updatedAt": updatedAt,
};
}
}
// ================= PARTNER MODEL =================
class PartnerModel {
final int id;
final String businessName;
final String emailAddress;
final String phoneNumber;
PartnerModel({
required this.id,
required this.businessName,
required this.emailAddress,
required this.phoneNumber,
});
factory PartnerModel.fromJson(Map<String, dynamic> json) {
return PartnerModel(
id: json['id'] ?? 0,
businessName: json['businessName'] ?? "N/A",
emailAddress: json['emailAddress'] ?? "N/A",
phoneNumber: json['phoneNumber'] ?? "N/A",
);
}
factory PartnerModel.empty() {
return PartnerModel(
id: 0,
businessName: "N/A",
emailAddress: "N/A",
phoneNumber: "N/A",
);
}
Map<String, dynamic> toJson() {
return {
"id": id,
"businessName": businessName,
"emailAddress": emailAddress,
"phoneNumber": phoneNumber,
};
}
}

View File

@@ -0,0 +1,38 @@
import 'dart:io';
import 'package:dio/dio.dart';
import '../../network_api_service/api_service/api_service.dart';
import '../../network_api_service/api_urls/api_urls.dart';
class RaiseTicketRepository {
final ApiService _apiService = ApiService();
Future<void> raiseTicket({
required String subject,
required String description,
File? attachment,
}) async {
try {
final Map<String, dynamic> formMap = {
"subject": subject,
"description": description,
};
if (attachment != null) {
final fileName = attachment.path.split('/').last;
formMap["attachment"] = await MultipartFile.fromFile(
attachment.path,
filename: fileName,
);
}
final formData = FormData.fromMap(formMap);
await _apiService.post(
ApiUrls.supportDetails,
data: formData,
);
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,22 @@
import '../../local_peference/local_preference.dart';
import '../../network_api_service/api_service/api_service.dart';
import '../../network_api_service/api_urls/api_urls.dart';
import '../model/support_details_model.dart';
class SupportDetailsRepository {
final ApiService _apiService = ApiService();
Future<SupportDetailModel> fetchSupportDetails() async {
try {
final userId = await LocalPreference.getUserId();
final response = await _apiService.get(
'${ApiUrls.supportDetails}/$userId',
);
return SupportDetailModel.fromJson(response.data);
} catch (e) {
throw Exception('Failed to fetch support details: $e');
}
}
}

View File

@@ -1,258 +1,355 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
import '../blocs/ticket_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:image_picker/image_picker.dart';
class SupportFormPage extends StatelessWidget {
import '../blocs/raise_ticket/raise_ticket_bloc.dart';
import '../blocs/support_details/support_details_bloc.dart';
class SupportFormPage extends StatefulWidget {
const SupportFormPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => TicketBloc(),
child: const _SupportFormView(),
);
}
State<SupportFormPage> createState() => _SupportFormPageState();
}
class _SupportFormView extends StatelessWidget {
const _SupportFormView();
class _SupportFormPageState extends State<SupportFormPage> {
final TextEditingController _subjectController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
final ImagePicker _picker = ImagePicker();
@override
void initState() {
super.initState();
// Trigger the fetch event for support details once when the page is initialized
context.read<SupportDetailsBloc>().add(FetchSupportDetailsEvent());
// Listen to existing state if we are coming back or resuming (optional)
final state = context.read<RaiseTicketBloc>().state;
_subjectController.text = state.subject;
_descriptionController.text = state.description;
}
@override
void dispose() {
_subjectController.dispose();
_descriptionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final bloc = context.read<TicketBloc>();
return Scaffold(
backgroundColor: Colors.white,
bottomNavigationBar: BlocBuilder<TicketBloc, TicketState>(
builder: (context, state) {return
Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0.0,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton(
onPressed: state.isLoading
? null
: () => bloc.add(SubmitTicket()),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
return BlocConsumer<RaiseTicketBloc, RaiseTicketState>(
listener: (context, state) {
if (state.status == RaiseTicketStatus.success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Ticket submitted successfully!")),
);
_subjectController.clear();
_descriptionController.clear();
context.read<RaiseTicketBloc>().add(const RaiseTicketReset());
Navigator.pop(context);
} else if (state.status == RaiseTicketStatus.failure &&
state.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error: ${state.errorMessage}")),
);
} else if (state.fileSizeExceeded && state.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error: ${state.errorMessage}")),
);
// Auto-remove the invalid attachment
// context.read<RaiseTicketBloc>().add(const RaiseTicketAttachmentRemoved());
}
},
builder: (context, state) {
final raiseTicketBloc = context.read<RaiseTicketBloc>();
return Scaffold(
backgroundColor: Colors.white,
bottomNavigationBar: Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0.0,
child: Padding(
padding: EdgeInsets.all(16.w),
child: SizedBox(
width: double.infinity,
height: 52.h,
child: ElevatedButton(
onPressed: state.status == RaiseTicketStatus.loading ||
state.fileSizeExceeded ||
state.subject.isEmpty ||
state.description.isEmpty
? null
: () => raiseTicketBloc.add(const RaiseTicketSubmitted()),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r)),
),
child: state.status == RaiseTicketStatus.loading
? SizedBox(
width: 20.w,
height: 20.h,
child: const CircularProgressIndicator(
color: Colors.white, strokeWidth: 2),
)
: Text(
"Submit Ticket",
style: TextStyle(
color: Colors.white,
fontSize: 20.sp,
fontWeight: FontWeight.w600),
),
),
child: state.isLoading
? const CircularProgressIndicator(color: Colors.white)
: Text("Submit Ticket",
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w600)),
),
),
),
);
}),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: Padding(
padding: EdgeInsetsGeometry.symmetric(horizontal: 8),
child: InkWell(
onTap: (){
Navigator.pop(context);
},
child: Container(
width: 44,
height: 44,
decoration: const BoxDecoration(
color: Color(0xFFF95F62),
shape: BoxShape.circle,
),
child: const Icon(Icons.arrow_back, color: Colors.white),
),
),
),
forceMaterialTransparency: true,
centerTitle: true,
title: Text("Support",
style: TextStyle(fontWeight: FontWeight.w700, color: Colors.black,fontSize: 32)),
),
body: BlocBuilder<TicketBloc, TicketState>(
builder: (context, state) {
return SingleChildScrollView(
padding: const EdgeInsets.all(20),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: InkWell(
onTap: () {
Navigator.pop(context);
},
child: Container(
width: 44.w,
height: 44.h,
decoration: const BoxDecoration(
color: Color(0xFFF95F62),
shape: BoxShape.circle,
),
child: Icon(Icons.arrow_back, color: Colors.white, size: 24.sp),
),
),
),
forceMaterialTransparency: true,
centerTitle: true,
title: Text("Support",
style: TextStyle(
fontWeight: FontWeight.w700,
color: Colors.black,
fontSize: 32.sp)),
),
body: SingleChildScrollView(
padding: EdgeInsets.all(20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Align(
alignment: Alignment.center,
child: Text(
textAlign: TextAlign.center,
"Need help? Were here for you. Raise a ticket and our support team will get back to you shortly",
style: TextStyle(color: Colors.black, fontSize: 13,),
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black, fontSize: 13.sp),
),
),
const SizedBox(height: 20),
Text("Subject", style: TextStyle(fontWeight: FontWeight.w500,fontSize: 14)),
const SizedBox(height: 6),
SizedBox(height: 20.h),
Text("Subject",
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14.sp)),
SizedBox(height: 6.h),
TextField(
onChanged: (v) => bloc.add(SubjectChanged(v)),
decoration: InputDecoration(
controller: _subjectController,
onChanged: (v) => raiseTicketBloc.add(RaiseTicketSubjectChanged(v)),
decoration: InputDecoration(
fillColor: Colors.black.withOpacity(0.04),
hintText: "Enter Subject",
filled: true,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.24), width: 1.0),
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.black.withOpacity(0.24), width: 1.0.w),
borderRadius: BorderRadius.circular(8.r),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.24), width: 1.0),
borderRadius: BorderRadius.circular(8),
),
disabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.24), width: 1.0),
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.black.withOpacity(0.24), width: 1.0.w),
borderRadius: BorderRadius.circular(8.r),
),
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.24), width: 1.0),
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.black.withOpacity(0.24), width: 1.0.w),
borderRadius: BorderRadius.circular(8.r),
),
),
),
const SizedBox(height: 20),
Text("Description", style: TextStyle(fontWeight: FontWeight.w500,fontSize: 14)),
const SizedBox(height: 6),
SizedBox(height: 20.h),
Text("Description",
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14.sp)),
SizedBox(height: 6.h),
TextField(
onChanged: (v) => bloc.add(DescriptionChanged(v)),
controller: _descriptionController,
onChanged: (v) => raiseTicketBloc.add(RaiseTicketDescriptionChanged(v)),
maxLines: 4,
decoration: InputDecoration(
decoration: InputDecoration(
fillColor: Colors.black.withOpacity(0.04),
hintText: "Enter Description",
filled: true,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.24), width: 1.0),
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.black.withOpacity(0.24), width: 1.0.w),
borderRadius: BorderRadius.circular(8.r),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.24), width: 1.0),
borderRadius: BorderRadius.circular(8),
),
disabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.24), width: 1.0),
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.black.withOpacity(0.24), width: 1.0.w),
borderRadius: BorderRadius.circular(8.r),
),
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.24), width: 1.0),
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.black.withOpacity(0.24), width: 1.0.w),
borderRadius: BorderRadius.circular(8.r),
),
),
),
const SizedBox(height: 20),
Text("File upload", style: TextStyle(fontWeight: FontWeight.w500,fontSize: 14)),
const SizedBox(height: 6),
SizedBox(height: 20.h),
Text("File upload",
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14.sp)),
SizedBox(height: 6.h),
GestureDetector(
onTap: () => bloc.add(UploadFile()),
onTap: () async {
// pickMedia allows both images and videos
final XFile? pickedFile = await _picker.pickMedia();
if (pickedFile != null) {
raiseTicketBloc.add(RaiseTicketAttachmentPicked(File(pickedFile.path)));
}
},
child: Container(
height: 45,
height: 45.h,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.04),
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(6),
borderRadius: BorderRadius.circular(6.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
state.selectedFile != null
? state.selectedFile!.name
: "Upload File",
style: TextStyle(
color: state.selectedFile != null
? Colors.black
: Colors.black54,
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Text(
state.attachment != null
? state.attachment!.path.split('/').last
: (state.fileSizeExceeded ? "File too large (>5MB).Pick another" : "Upload File"),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: state.attachment != null
? Colors.black
: (state.fileSizeExceeded ? Colors.red : Colors.black54),
fontSize: 13.sp,
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Image.asset("assets/support/icon/upload.png",scale: 4,),
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: Image.asset(
"assets/support/icon/upload.png",
scale: 4,
width: 24.w,
height: 24.h,
),
),
],
),
),
),
const SizedBox(height: 30),
if (state.attachment != null)
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => raiseTicketBloc.add(const RaiseTicketAttachmentRemoved()),
child: Text(
"Remove File",
style: TextStyle(color: Colors.red, fontSize: 12.sp),
),
),
),
SizedBox(height: 30.h),
Text("Contact Details",
style: TextStyle(
fontWeight: FontWeight.w600, fontSize: 24)),
const SizedBox(height: 12),
Divider(),
Row(
children: [
Expanded(
flex: 1,
child: Row(
children: [
const Icon(Icons.email_outlined, color: Colors.black87),
const SizedBox(width: 12),
Text("Email", style: TextStyle(fontSize: 13)),
],
),
),
Expanded(
flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(width: 12),
Text("Lila Hart", style: TextStyle(fontSize: 13,color: Colors.black)),
],
),
),
],
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 24.sp)),
SizedBox(height: 12.h),
const Divider(),
BlocBuilder<SupportDetailsBloc, SupportDetailsState>(
builder: (context, supportState) {
String email = "Loading...";
String phone = "Loading...";
if (supportState is SupportDetailsLoaded) {
email = supportState.supportDetail.partner.emailAddress;
phone = supportState.supportDetail.partner.phoneNumber;
} else if (supportState is SupportDetailsError) {
email = "Error loading";
phone = "Error loading";
}
return Column(
children: [
Row(
children: [
Expanded(
flex: 1,
child: Row(
children: [
Icon(Icons.email_outlined, color: Colors.black87, size: 20.sp),
SizedBox(width: 12.w),
Text("Email", style: TextStyle(fontSize: 13.sp)),
],
),
),
Expanded(
flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(width: 12.w),
Text(email,
style: TextStyle(
fontSize: 13.sp, color: Colors.black)),
],
),
),
],
),
const Divider(),
SizedBox(height: 12.h),
Row(
children: [
Expanded(
flex: 1,
child: Row(
children: [
Icon(Icons.phone_outlined, color: Colors.black87, size: 20.sp),
SizedBox(width: 12.w),
Text("Phone", style: TextStyle(fontSize: 13.sp)),
],
),
),
Expanded(
flex: 2,
child: Row(
children: [
SizedBox(width: 12.w),
Text(phone, style: TextStyle(fontSize: 13.sp)),
],
),
),
],
),
],
);
},
),
Divider(),
const SizedBox(height: 12),
Row(
children: [
Expanded(
flex: 1,
child: Row(
children: [
const Icon(Icons.phone_outlined, color: Colors.black87),
const SizedBox(width: 12),
Text("Phone", style: TextStyle(fontSize: 13)),
],
),
),
Expanded(
flex: 2,
child: Row(
children: [
const SizedBox(width: 12),
Text("(+971) 050 4245 564",
style: TextStyle(fontSize: 13)),
],
),
),
],
),
Divider(),
const SizedBox(height: 80),
const Divider(),
SizedBox(height: 80.h),
],
),
);
},
),
),
);
},
);
}
}
}

View File

@@ -89,6 +89,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cookie_jar:
dependency: "direct main"
description:
name: cookie_jar
sha256: "963da02c1ef64cb5ac20de948c9e5940aa351f1e34a12b1d327c83d85b7e8fff"
url: "https://pub.dev"
source: hosted
version: "4.0.9"
cross_file:
dependency: transitive
description:
@@ -137,6 +145,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.9.2"
dio_cookie_manager:
dependency: "direct main"
description:
name: dio_cookie_manager
sha256: "0db1a7b997a0455e488ac35744c68eed3f2a4280d3ab531835a65641b0a08744"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
dio_web_adapter:
dependency: transitive
description:
@@ -185,6 +201,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "10.3.3"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev"
source: hosted
version: "0.9.5"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
flutter:
dependency: "direct main"
description: flutter
@@ -296,6 +344,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.4"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622"
url: "https://pub.dev"
source: hosted
version: "0.8.13+16"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
url: "https://pub.dev"
source: hosted
version: "0.8.13+6"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.dev"
source: hosted
version: "0.2.2"
intl:
dependency: "direct main"
description:
@@ -702,5 +814,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.9.2 <4.0.0"
flutter: ">=3.35.1"
dart: ">=3.10.0 <4.0.0"
flutter: ">=3.38.0"

View File

@@ -49,6 +49,9 @@ dependencies:
shared_preferences: ^2.5.4
dio: ^5.9.2
flutter_screenutil: ^5.9.3
dio_cookie_manager: ^3.4.0
cookie_jar: ^4.0.9
image_picker: ^1.2.1
dev_dependencies:
flutter_test: