api intigreted of scan Qr and recent scan histoy ,scan history,scan history details
This commit is contained in:
663
android/build/reports/problems/problems-report.html
Normal file
663
android/build/reports/problems/problems-report.html
Normal file
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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!'));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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; // 0–4 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 (0–4)
|
||||
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));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
"Partner’s 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(
|
||||
"We’ve 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (_) {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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];
|
||||
}
|
||||
70
lib/scan/bloc/submit_qr_code/submit_qr_code_bloc.dart
Normal file
70
lib/scan/bloc/submit_qr_code/submit_qr_code_bloc.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
21
lib/scan/bloc/submit_qr_code/submit_qr_code_event.dart
Normal file
21
lib/scan/bloc/submit_qr_code/submit_qr_code_event.dart
Normal 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();
|
||||
}
|
||||
42
lib/scan/bloc/submit_qr_code/submit_qr_code_state.dart
Normal file
42
lib/scan/bloc/submit_qr_code/submit_qr_code_state.dart
Normal 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];
|
||||
}
|
||||
100
lib/scan/models/recent_scan_history_model.dart
Normal file
100
lib/scan/models/recent_scan_history_model.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
18
lib/scan/repository/recent_scan_history_repository.dart
Normal file
18
lib/scan/repository/recent_scan_history_repository.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
29
lib/scan/repository/submit_qr_code_repository.dart
Normal file
29
lib/scan/repository/submit_qr_code_repository.dart
Normal 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
206
lib/scan_history/blocs/scan_history/scan_history_bloc.dart
Normal file
206
lib/scan_history/blocs/scan_history/scan_history_bloc.dart
Normal 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,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
70
lib/scan_history/blocs/scan_history/scan_history_event.dart
Normal file
70
lib/scan_history/blocs/scan_history/scan_history_event.dart
Normal 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();
|
||||
}
|
||||
96
lib/scan_history/blocs/scan_history/scan_history_state.dart
Normal file
96
lib/scan_history/blocs/scan_history/scan_history_state.dart
Normal 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];
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
121
lib/scan_history/models/scan_history_details_model.dart
Normal file
121
lib/scan_history/models/scan_history_details_model.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
13
lib/scan_history/models/scan_history_model_old.dart
Normal file
13
lib/scan_history/models/scan_history_model_old.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
99
lib/support/blocs/raise_ticket/raise_ticket_bloc.dart
Normal file
99
lib/support/blocs/raise_ticket/raise_ticket_bloc.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
44
lib/support/blocs/raise_ticket/raise_ticket_event.dart
Normal file
44
lib/support/blocs/raise_ticket/raise_ticket_event.dart
Normal 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();
|
||||
}
|
||||
43
lib/support/blocs/raise_ticket/raise_ticket_state.dart
Normal file
43
lib/support/blocs/raise_ticket/raise_ticket_state.dart
Normal 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];
|
||||
}
|
||||
30
lib/support/blocs/support_details/support_details_bloc.dart
Normal file
30
lib/support/blocs/support_details/support_details_bloc.dart
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
12
lib/support/blocs/support_details/support_details_event.dart
Normal file
12
lib/support/blocs/support_details/support_details_event.dart
Normal 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();
|
||||
}
|
||||
38
lib/support/blocs/support_details/support_details_state.dart
Normal file
38
lib/support/blocs/support_details/support_details_state.dart
Normal 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];
|
||||
}
|
||||
190
lib/support/model/support_details_model.dart
Normal file
190
lib/support/model/support_details_model.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
38
lib/support/repository/raise_ticket_repository.dart
Normal file
38
lib/support/repository/raise_ticket_repository.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
lib/support/repository/support_details_repository.dart
Normal file
22
lib/support/repository/support_details_repository.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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? We’re 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
116
pubspec.lock
116
pubspec.lock
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user