Initial commit

This commit is contained in:
jahongireshonqulov
2025-10-18 09:40:06 +05:00
commit 1bf3e41abe
352 changed files with 16315 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_response.dart';
abstract class AuthRemoteDataSource {
Future<LoginResponse> login(LoginRequest request);
Future<SignUpResponse> signUp(SignUpRequest request);
Future<AuthVerificationResponse> verify(AuthVerificationRequest request);
Future<FcmAddResponse> addFcm(FcmAddRequest request);
}

View File

@@ -0,0 +1,105 @@
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_response.dart';
import 'package:dio/dio.dart';
import '../../../../../constants/constants.dart';
import '../../../../../core/error/exceptions.dart';
import '../../../../../core/local_source/local_source.dart';
import '../../../../../injector_container.dart';
import 'auth_remote_data_source.dart';
class AuthRemoteDataSourceImpl extends AuthRemoteDataSource {
final Dio dio;
AuthRemoteDataSourceImpl(this.dio);
@override
Future<LoginResponse> login(LoginRequest request) async {
try {
final Response response = await dio.post(
Constants.baseUrl + Urls.login,
options: DioConstants.options,
data: request,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return LoginResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);
} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
@override
Future<SignUpResponse> signUp(SignUpRequest request) async {
try {
final Response response = await dio.post(
Constants.baseUrl + Urls.signUp,
options: DioConstants.options,
data: request,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return SignUpResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
@override
Future<AuthVerificationResponse> verify(
AuthVerificationRequest request,
) async {
try {
final Response response = await dio.post(
Constants.baseUrl + Urls.verify,
options: DioConstants.options
..headers = {
"Authorization": "Bearer ${sl<LocalSource>().getAccessToken()}",
},
data: request,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return AuthVerificationResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);
} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
@override
Future<FcmAddResponse> addFcm(FcmAddRequest request) async {
try {
final Response response = await dio.put(
Constants.baseUrl + Urls.addFcm,
options: DioConstants.options
..headers = {
"Authorization": "Bearer ${sl<LocalSource>().getAccessToken()}",
},
data: request,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return FcmAddResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);
} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
}

View File

@@ -0,0 +1,21 @@
class AuthVerificationRequest {
AuthVerificationRequest({this.phoneNumber, this.smsCode, this.email});
AuthVerificationRequest.fromJson(dynamic json) {
phoneNumber = json['phone_number'];
smsCode = json['smsCode'];
email = json['email'];
}
String? phoneNumber;
String? smsCode;
String? email;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['phone_number'] = phoneNumber;
map['smsCode'] = smsCode;
map['email'] = email;
return map;
}
}

View File

@@ -0,0 +1,16 @@
class AuthVerificationResponse {
AuthVerificationResponse({
this.accessToken,});
AuthVerificationResponse.fromJson(dynamic json) {
accessToken = json['access_token'];
}
String? accessToken;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['access_token'] = accessToken;
return map;
}
}

View File

@@ -0,0 +1,16 @@
class FcmAddRequest {
FcmAddRequest({
this.sfmToken,});
FcmAddRequest.fromJson(dynamic json) {
sfmToken = json['sfm_token'];
}
String? sfmToken;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['sfm_token'] = sfmToken;
return map;
}
}

View File

@@ -0,0 +1,51 @@
class FcmAddResponse {
FcmAddResponse({
this.message,
this.user,});
FcmAddResponse.fromJson(dynamic json) {
message = json['message'];
user = json['user'] != null ? User.fromJson(json['user']) : null;
}
String? message;
User? user;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['message'] = message;
if (user != null) {
map['user'] = user?.toJson();
}
return map;
}
}
class User {
User({
this.fullname,
this.phoneNumber,
this.email,
this.sfmToken,});
User.fromJson(dynamic json) {
fullname = json['fullname'];
phoneNumber = json['phone_number'];
email = json['email'];
sfmToken = json['sfm_token'];
}
String? fullname;
String? phoneNumber;
String? email;
String? sfmToken;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['fullname'] = fullname;
map['phone_number'] = phoneNumber;
map['email'] = email;
map['sfm_token'] = sfmToken;
return map;
}
}

View File

@@ -0,0 +1,24 @@
class LoginRequest {
LoginRequest({
this.email,
this.phone,
this.ucode,});
LoginRequest.fromJson(dynamic json) {
email = json['email'];
phone = json['phone'];
ucode = json['ucode'];
}
String? email;
String? phone;
String? ucode;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['email'] = email;
map['phone'] = phone;
map['ucode'] = ucode;
return map;
}
}

View File

@@ -0,0 +1,20 @@
class LoginResponse {
LoginResponse({
this.token,
this.refreshToken,});
LoginResponse.fromJson(dynamic json) {
token = json['token'];
refreshToken = json['refreshToken'];
}
String? token;
String? refreshToken;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['token'] = token;
map['refreshToken'] = refreshToken;
return map;
}
}

View File

@@ -0,0 +1,25 @@
class SignUpRequest {
SignUpRequest({
this.phoneNumber,
this.email,
this.fullname,});
SignUpRequest.fromJson(dynamic json) {
phoneNumber = json['phone_number'];
email = json['email'];
fullname = json['fullname'];
}
String? phoneNumber;
String? email;
String? fullname;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['phone_number'] = phoneNumber;
map['email'] = email;
map['fullname'] = fullname;
return map;
}
}

View File

@@ -0,0 +1,16 @@
class SignUpResponse {
SignUpResponse({
this.token,});
SignUpResponse.fromJson(dynamic json) {
token = json['token'];
}
String? token;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['token'] = token;
return map;
}
}

View File

@@ -0,0 +1,73 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_response.dart';
import 'package:dartz/dartz.dart';
import '../../../../core/error/exceptions.dart';
import '../../domain/repository/auth_repository.dart';
import '../data_source/remote/auth_remote_data_source.dart';
class AuthRepositoryImpl extends AuthRepository {
final AuthRemoteDataSource remoteDataSource;
AuthRepositoryImpl(this.remoteDataSource);
@override
Future<Either<Failure, LoginResponse>> login(LoginRequest request) async {
try {
final response = await remoteDataSource.login(request);
return Right(response);
} catch (e) {
if (e is ServerException) {
return Left(ServerFailure(message: e.message));
}
return Left(ServerFailure(message: e.toString()));
}
}
@override
Future<Either<Failure, SignUpResponse>> signUp(SignUpRequest request) async {
try {
final response = await remoteDataSource.signUp(request);
return Right(response);
} catch (e) {
if (e is ServerException) {
return Left(ServerFailure(message: e.message));
}
return Left(ServerFailure(message: e.toString()));
}
}
@override
Future<Either<Failure, AuthVerificationResponse>> verify(
AuthVerificationRequest request,
) async {
try {
final response = await remoteDataSource.verify(request);
return Right(response);
} catch (e) {
if (e is ServerException) {
return Left(ServerFailure(message: e.message));
}
return Left(ServerFailure(message: e.toString()));
}
}
@override
Future<Either<Failure, FcmAddResponse>> fcmAdd(FcmAddRequest request) async {
try {
final response = await remoteDataSource.addFcm(request);
return Right(response);
} catch (e) {
if (e is ServerException) {
return Left(ServerFailure(message: e.message));
}
return Left(ServerFailure(message: e.toString()));
}
}
}

View File

@@ -0,0 +1,23 @@
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_response.dart';
import 'package:dartz/dartz.dart';
import '../../../../core/error/failure.dart';
abstract class AuthRepository {
Future<Either<Failure, LoginResponse>> login(LoginRequest request);
Future<Either<Failure, SignUpResponse>> signUp(SignUpRequest request);
Future<Either<Failure, AuthVerificationResponse>> verify(
AuthVerificationRequest request,
);
Future<Either<Failure, FcmAddResponse>> fcmAdd(FcmAddRequest request);
}

View File

@@ -0,0 +1,18 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_response.dart';
import 'package:dartz/dartz.dart';
import '../repository/auth_repository.dart';
class FcmAddUseCase extends UseCase<FcmAddResponse, FcmAddRequest> {
final AuthRepository repository;
FcmAddUseCase(this.repository);
@override
Future<Either<Failure, FcmAddResponse>> call(FcmAddRequest params) async {
return await repository.fcmAdd(params);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_response.dart';
import 'package:dartz/dartz.dart';
import '../repository/auth_repository.dart';
class LoginUseCase extends UseCase<LoginResponse, LoginRequest> {
final AuthRepository repository;
LoginUseCase(this.repository);
@override
Future<Either<Failure, LoginResponse>> call(LoginRequest params) async {
final response = await repository.login(params);
return response;
}
}

View File

@@ -0,0 +1,19 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_response.dart';
import 'package:dartz/dartz.dart';
import '../repository/auth_repository.dart';
class SignUpUseCase extends UseCase<SignUpResponse, SignUpRequest> {
final AuthRepository repository;
SignUpUseCase(this.repository);
@override
Future<Either<Failure, SignUpResponse>> call(SignUpRequest params) async {
final response = await repository.signUp(params);
return response;
}
}

View File

@@ -0,0 +1,22 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_response.dart';
import 'package:dartz/dartz.dart';
import '../repository/auth_repository.dart';
class VerifyPhoneUseCase
extends UseCase<AuthVerificationResponse, AuthVerificationRequest> {
final AuthRepository repository;
VerifyPhoneUseCase(this.repository);
@override
Future<Either<Failure, AuthVerificationResponse>> call(
AuthVerificationRequest params,
) async {
final response = await repository.verify(params);
return response;
}
}

View File

@@ -0,0 +1,116 @@
import 'package:cargocalculaterapp/core/local_source/local_source.dart';
import 'package:cargocalculaterapp/generated/l10n.dart';
import 'package:cargocalculaterapp/router/app_routes.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../constants/constants.dart';
import '../../../../../injector_container.dart';
import '../../../../../router/name_routes.dart';
import '../../../data/model/login_request.dart';
import '../../../domain/usecases/login_usecase.dart';
import '../../pages/auth_confirm/argument/auth_confirm_argument.dart';
part 'auth_event.dart';
part 'auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc(this.loginUseCase)
: super(
const AuthState(
passwordHidden: true,
isLoading: false,
isButtonEnabled: false,
),
) {
on<PasswordVisibilityEvent>(_showHidePassword);
on<OnInputEnterEvent>(_onInputEnter);
on<SubmitEvent>(_onSubmit);
}
final LoginUseCase loginUseCase;
void _showHidePassword(
PasswordVisibilityEvent event,
Emitter<AuthState> emit,
) {
emit(state.copyWith(passwordHidden: !state.passwordHidden));
}
void _onInputEnter(OnInputEnterEvent event, Emitter<AuthState> emit) {
emit(
state.copyWith(
isButtonEnabled:
(isEmail(event.login) || isPhoneNumber(event.login)) &&
event.password.isNotEmpty,
),
);
}
Future<void> _onSubmit(SubmitEvent event, Emitter<AuthState> emit) async {
emit(state.copyWith(isLoading: true));
final response = await loginUseCase(
LoginRequest(
email: isEmail(event.login) ? event.login : "",
phone: isPhoneNumber(event.login)
? event.login.replaceAll("+", "")
: "",
ucode: event.password,
),
);
response.fold(
(l) {
emit(state.copyWith(isLoading: false));
showErrorSnackBar();
},
(r) {
emit(state.copyWith(isLoading: false));
sl<LocalSource>().setUCode(event.password);
Navigator.pushNamed(
rootNavigatorKey.currentContext!,
Routes.authConfirm,
arguments: AuthConfirmArgument(
fullName: "",
mail: isEmail(event.login) ? event.login : "",
phoneNumber: isPhoneNumber(event.login) ? event.login : "",
fromLoginPage: true,
password: event.password,
),
);
},
);
}
bool isEmail(String input) {
return RegExConst.emailRegex.hasMatch(input);
}
bool isPhoneNumber(String input) {
return RegExConst.phoneRegex.hasMatch(input);
}
void showErrorSnackBar() {
final snackBar = SnackBar(
backgroundColor: Colors.red,
content: Row(
children: [
const Icon(Icons.error, color: Colors.white),
const SizedBox(width: 8),
Expanded(
child: Text(
AppLocalization.current.login_error,
style: const TextStyle(color: Colors.white),
),
),
],
),
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 3),
);
ScaffoldMessenger.of(
rootNavigatorKey.currentContext!,
).showSnackBar(snackBar);
}
}

View File

@@ -0,0 +1,32 @@
part of 'auth_bloc.dart';
sealed class AuthEvent extends Equatable {
const AuthEvent();
}
final class PasswordVisibilityEvent extends AuthEvent {
const PasswordVisibilityEvent();
@override
List<Object?> get props => [];
}
final class OnInputEnterEvent extends AuthEvent {
final String login;
final String password;
const OnInputEnterEvent({required this.login, required this.password});
@override
List<Object?> get props => [login, password];
}
final class SubmitEvent extends AuthEvent {
final String login;
final String password;
const SubmitEvent({required this.login, required this.password});
@override
List<Object?> get props => [login, password];
}

View File

@@ -0,0 +1,28 @@
part of 'auth_bloc.dart';
class AuthState extends Equatable {
const AuthState({
required this.passwordHidden,
required this.isLoading,
required this.isButtonEnabled,
});
final bool passwordHidden;
final bool isLoading;
final bool isButtonEnabled;
AuthState copyWith({
bool? passwordHidden,
bool? isLoading,
bool? isButtonEnabled,
}) {
return AuthState(
isLoading: isLoading ?? this.isLoading,
passwordHidden: passwordHidden ?? this.passwordHidden,
isButtonEnabled: isButtonEnabled ?? this.isButtonEnabled,
);
}
@override
List<Object?> get props => [passwordHidden, isLoading, isButtonEnabled];
}

View File

@@ -0,0 +1,112 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../core/error/failure.dart';
import '../../../../../core/functions/base_finctions.dart';
import '../../../../../core/local_source/local_source.dart';
import '../../../../../injector_container.dart';
import '../../../../../service/notification_service.dart';
import '../../../data/model/auth_verification_request.dart';
import '../../../data/model/fcm_add_request.dart';
import '../../../data/model/login_request.dart';
import '../../../data/model/sign_up_request.dart';
import '../../../domain/usecases/fcm_add_usecase.dart';
import '../../../domain/usecases/login_usecase.dart';
import '../../../domain/usecases/sign_up_usecase.dart';
import '../../../domain/usecases/verify_phone_usecase.dart';
import '../../pages/auth_confirm/argument/auth_confirm_argument.dart';
part 'auth_confirm_event.dart';
part 'auth_confirm_state.dart';
class AuthConfirmBloc extends Bloc<AuthConfirmEvent, AuthConfirmState> {
AuthConfirmBloc(
this.signUpUseCase,
this.verifyPhoneUseCase,
this.loginUseCase,
this.fcmAddUseCase,
) : super(const InitialState(true)) {
on<TimerChangedEvent>(_timerChanged);
on<SmsCodeEnterEvent>(_codeChanged);
on<ResendCodeEvent>(_resendCode);
on<OnSubmitEvent>(_onSubmitCode);
}
final SignUpUseCase signUpUseCase;
final VerifyPhoneUseCase verifyPhoneUseCase;
final LoginUseCase loginUseCase;
final FcmAddUseCase fcmAddUseCase;
Future<void> _timerChanged(
TimerChangedEvent event,
Emitter<AuthConfirmState> emit,
) async {
emit(InitialState(event.isVisible));
}
void _codeChanged(SmsCodeEnterEvent event, Emitter<AuthConfirmState> emit) {
if (event.code.length == 4) {
emit(ButtonEnableState(state.isTimerVisible, event.code));
} else {
emit(InitialState(state.isTimerVisible));
}
}
Future<void> _resendCode(
ResendCodeEvent event,
Emitter<AuthConfirmState> emit,
) async {
add(const TimerChangedEvent(isVisible: true));
if (event.argument.fromLoginPage) {
await loginUseCase(
LoginRequest(
email: event.argument.mail,
phone: event.argument.phoneNumber?.replaceAll("+", ""),
ucode: event.argument.password,
),
);
} else {
await signUpUseCase(
SignUpRequest(
fullname: event.argument.fullName,
email: event.argument.mail,
phoneNumber: event.argument.phoneNumber?.replaceAll("+", ""),
),
);
}
}
Future<void> _onSubmitCode(
OnSubmitEvent event,
Emitter<AuthConfirmState> emit,
) async {
emit(LoadingState(state.isTimerVisible));
final response = await verifyPhoneUseCase(
AuthVerificationRequest(
phoneNumber: event.argument?.phoneNumber?.replaceAll("+", ""),
smsCode: event.code,
email: event.argument?.mail ?? "",
),
);
await response.fold(
(l) {
if (l is ServerFailure) {
Functions.showErrorSnackBar(l.message);
}
emit(InitialState(state.isTimerVisible));
},
(r) async {
sl<LocalSource>().setHasProfile(true);
sl<LocalSource>().setAccessToken(r.accessToken ?? "");
await _addFcmToken();
emit(SuccessSate(state.isTimerVisible));
},
);
}
Future<void> _addFcmToken() async {
final String fcmToken = await NotificationService.getFcmToken();
await fcmAddUseCase(FcmAddRequest(sfmToken: fcmToken));
}
}

View File

@@ -0,0 +1,42 @@
part of 'auth_confirm_bloc.dart';
sealed class AuthConfirmEvent extends Equatable {
const AuthConfirmEvent();
}
final class SmsCodeEnterEvent extends AuthConfirmEvent {
final String code;
const SmsCodeEnterEvent(this.code);
@override
List<Object?> get props => [code];
}
final class TimerChangedEvent extends AuthConfirmEvent {
final bool isVisible;
const TimerChangedEvent({required this.isVisible});
@override
List<Object?> get props => [isVisible];
}
final class OnSubmitEvent extends AuthConfirmEvent {
final String code;
final AuthConfirmArgument? argument;
const OnSubmitEvent({required this.code, required this.argument});
@override
List<Object?> get props => [code, argument];
}
final class ResendCodeEvent extends AuthConfirmEvent {
final AuthConfirmArgument argument;
const ResendCodeEvent({required this.argument});
@override
List<Object?> get props => [argument];
}

View File

@@ -0,0 +1,44 @@
part of 'auth_confirm_bloc.dart';
sealed class AuthConfirmState extends Equatable {
const AuthConfirmState(this.isTimerVisible);
final bool isTimerVisible;
}
class InitialState extends AuthConfirmState {
const InitialState(super.isTimerVisible);
@override
List<Object?> get props => [super.isTimerVisible];
}
class LoadingState extends AuthConfirmState {
const LoadingState(super.isTimerVisible);
@override
List<Object?> get props => [super.isTimerVisible];
}
class SuccessSate extends AuthConfirmState {
const SuccessSate(super.isTimerVisible);
@override
List<Object?> get props => [super.isTimerVisible];
}
class ErrorState extends AuthConfirmState {
const ErrorState(super.isTimerVisible);
@override
List<Object?> get props => [super.isTimerVisible];
}
class ButtonEnableState extends AuthConfirmState {
const ButtonEnableState(super.isTimerVisible, this.code);
final String code;
@override
List<Object?> get props => [super.isTimerVisible, code];
}

View File

@@ -0,0 +1,95 @@
import 'package:cargocalculaterapp/core/local_source/local_source.dart';
import 'package:cargocalculaterapp/generated/l10n.dart';
import 'package:cargocalculaterapp/router/app_routes.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../constants/constants.dart';
import '../../../../../core/error/failure.dart';
import '../../../../../core/functions/base_finctions.dart';
import '../../../../../injector_container.dart';
import '../../../../../router/name_routes.dart';
import '../../../data/model/sign_up_request.dart';
import '../../../domain/usecases/sign_up_usecase.dart';
import '../../pages/auth_confirm/argument/auth_confirm_argument.dart';
part 'sign_up_event.dart';
part 'sign_up_state.dart';
class SignUpBloc extends Bloc<SignUpEvent, SignUpState> {
SignUpBloc(this.signUpUseCase)
: super(
const SignUpState(
isLoading: false,
isButtonEnabled: false,
passwordHidden: true,
),
) {
on<PasswordVisibilityEvent>(_passwordVisibility);
on<OnInputEnterEvent>(_onDataEnter);
on<SubmitEvent>(_signUp);
}
final SignUpUseCase signUpUseCase;
void _passwordVisibility(
PasswordVisibilityEvent event,
Emitter<SignUpState> emit,
) {
emit(state.copyWith(isButtonEnabled: !state.isButtonEnabled));
}
void _onDataEnter(OnInputEnterEvent event, Emitter<SignUpState> emit) {
emit(
state.copyWith(
isButtonEnabled:
(isEmail(event.login) || isPhoneNumber(event.login)) &&
event.fullName.isNotEmpty,
),
);
}
bool isEmail(String input) {
return RegExConst.emailRegex.hasMatch(input);
}
bool isPhoneNumber(String input) {
return RegExConst.phoneRegex.hasMatch(input);
}
Future<void> _signUp(SubmitEvent event, Emitter<SignUpState> emit) async {
emit(state.copyWith(isLoading: true));
final response = await signUpUseCase(
SignUpRequest(
fullname: event.fullName,
email: isEmail(event.login) ? event.login : "",
phoneNumber: isPhoneNumber(event.login)
? event.login.replaceAll("+", "")
: "",
),
);
response.fold(
(l) {
emit(state.copyWith(isLoading: false));
if (l is ServerFailure) {
Functions.showErrorSnackBar(AppLocalization.current.phone_registered);
}
},
(r) {
sl<LocalSource>().setAccessToken(r.token ?? "");
emit(state.copyWith(isLoading: false));
Navigator.pushNamed(
rootNavigatorKey.currentContext!,
Routes.authConfirm,
arguments: AuthConfirmArgument(
fullName: event.fullName,
mail: isEmail(event.login) ? event.login : "",
phoneNumber: isPhoneNumber(event.login) ? event.login : "",
fromLoginPage: false,
),
);
},
);
}
}

View File

@@ -0,0 +1,32 @@
part of 'sign_up_bloc.dart';
sealed class SignUpEvent extends Equatable {
const SignUpEvent();
}
final class PasswordVisibilityEvent extends SignUpEvent {
const PasswordVisibilityEvent();
@override
List<Object?> get props => [];
}
final class OnInputEnterEvent extends SignUpEvent {
final String login;
final String fullName;
const OnInputEnterEvent({required this.login, required this.fullName});
@override
List<Object?> get props => [login, fullName];
}
final class SubmitEvent extends SignUpEvent {
final String login;
final String fullName;
const SubmitEvent({required this.login, required this.fullName});
@override
List<Object?> get props => [login, fullName];
}

View File

@@ -0,0 +1,28 @@
part of 'sign_up_bloc.dart';
class SignUpState extends Equatable {
const SignUpState({
required this.passwordHidden,
required this.isLoading,
required this.isButtonEnabled,
});
final bool passwordHidden;
final bool isLoading;
final bool isButtonEnabled;
SignUpState copyWith({
bool? passwordHidden,
bool? isLoading,
bool? isButtonEnabled,
}) {
return SignUpState(
isLoading: isLoading ?? this.isLoading,
passwordHidden: passwordHidden ?? this.passwordHidden,
isButtonEnabled: isButtonEnabled ?? this.isButtonEnabled,
);
}
@override
List<Object?> get props => [passwordHidden, isLoading, isButtonEnabled];
}

View File

@@ -0,0 +1,185 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:cargocalculaterapp/core/utils/app_utils.dart';
import 'package:cargocalculaterapp/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../../../core/widgets/loading/custom_loading.dart';
import '../../../../../core/widgets/text_filds/custom_text_field_name.dart';
import '../../../../../router/name_routes.dart';
import '../../bloc/auth/auth_bloc.dart';
import '../mixin/auth_mixin.dart';
class AuthPage extends StatefulWidget {
const AuthPage({super.key});
@override
State<AuthPage> createState() => _AuthPageState();
}
class _AuthPageState extends State<AuthPage> with AuthMixin {
@override
void initState() {
initControllers();
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(),
body: ListView(
padding: AppUtils.kPaddingHor16,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: SvgPicture.asset("assets/svg/ic_logo_auth.svg"),
),
AppUtils.kBoxHeight48,
Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
borderRadius: AppUtils.kBorderRadius12,
color: context.color.iconBackground,
),
padding: AppUtils.kPaddingAll6,
child: SvgPicture.asset("assets/svg/ic_user.svg"),
),
AppUtils.kBoxWidth12,
Text(
AppLocalization.current.auth_login,
style: context.text.authTitle,
),
],
),
AppUtils.kBoxHeight32,
CustomTextFieldName(
hint: AppLocalization.current.enter_phone_or_mail,
inputType: TextInputType.text,
name: AppLocalization.current.phone,
controller: loginController,
onchange: (value) {
context.read<AuthBloc>().add(
OnInputEnterEvent(
login: value,
password: passwordController.text,
),
);
},
),
AppUtils.kBoxHeight16,
CustomTextFieldName(
hint: "********",
prefixWidget: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"CT-",
style: context.text.profileCategory.copyWith(
fontWeight: FontWeight.w400,
),
),
],
),
inputType: TextInputType.number,
name: AppLocalization.current.password,
obscureText: state.passwordHidden,
maxLines: 1,
controller: passwordController,
suffix: IconButton(
onPressed: () {
context.read<AuthBloc>().add(
const PasswordVisibilityEvent(),
);
},
icon: Icon(
state.passwordHidden
? Icons.visibility_off_rounded
: Icons.visibility_rounded,
),
),
onchange: (value) {
context.read<AuthBloc>().add(
OnInputEnterEvent(
login: loginController.text,
password: value,
),
);
},
),
AppUtils.kBoxHeight48,
Padding(
padding: AppUtils.kPaddingHor34,
child: ElevatedButton(
onPressed: state.isButtonEnabled && !state.isLoading
? () {
context.read<AuthBloc>().add(
SubmitEvent(
login: loginController.text,
password: "CT-${passwordController.text}",
),
);
}
: null,
child: state.isLoading
? const CustomLoadingWidget()
: Text(AppLocalization.current.auth),
),
),
AppUtils.kBoxHeight16,
Text(
AppLocalization.current.no_account,
style: context.text.authDesc,
textAlign: TextAlign.center,
),
AppUtils.kBoxHeight16,
Padding(
padding: AppUtils.kPaddingHor34,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
Navigator.pushNamed(context, Routes.signUp);
},
borderRadius: AppUtils.kBorderRadius24,
child: Ink(
decoration: BoxDecoration(
color: context.color.scaffoldBackgroundColor,
borderRadius: AppUtils.kBorderRadius24,
border: Border.all(color: context.color.primaryColor),
),
height: 56,
padding: AppUtils.kPaddingHor16,
child: Center(
child: Text(
AppLocalization.current.sign_up,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: context.color.textColor,
),
),
),
),
),
),
),
],
),
);
},
);
}
@override
void dispose() {
disposeControllers();
super.dispose();
}
}

View File

@@ -0,0 +1,15 @@
class AuthConfirmArgument {
final String? fullName;
final String? mail;
final String? phoneNumber;
final bool fromLoginPage;
final String? password;
AuthConfirmArgument({
this.fullName,
required this.mail,
required this.phoneNumber,
required this.fromLoginPage,
this.password,
});
}

View File

@@ -0,0 +1,233 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:cargocalculaterapp/core/utils/app_utils.dart';
import 'package:cargocalculaterapp/core/widgets/loading/custom_loading.dart';
import 'package:cargocalculaterapp/features/auth/presentation/pages/auth_confirm/timer_widget.dart';
import 'package:cargocalculaterapp/generated/l10n.dart';
import 'package:cargocalculaterapp/router/name_routes.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:pin_code_fields/pin_code_fields.dart';
import '../../../../../core/theme/app_text_styles.dart';
import '../../../../../core/theme/colors/app_colors.dart';
import '../../bloc/auth_confirm/auth_confirm_bloc.dart';
import '../mixin/auth_confirm_mixin.dart';
import 'argument/auth_confirm_argument.dart';
class AuthConfirmPage extends StatefulWidget {
const AuthConfirmPage({super.key, required this.argument});
final AuthConfirmArgument? argument;
@override
State<AuthConfirmPage> createState() => _AuthConfirmPageState();
}
class _AuthConfirmPageState extends State<AuthConfirmPage>
with AuthConfirmMixin {
@override
void initState() {
initControllers();
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocConsumer<AuthConfirmBloc, AuthConfirmState>(
listener: (context, state) {
if (state is SuccessSate) {
Navigator.pushNamedAndRemoveUntil(
context,
Routes.main,
(route) => route.isFirst,
);
}
},
builder: (context, state) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: AppUtils.kPaddingAll16,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
borderRadius: AppUtils.kBorderRadius16,
color: context.color.iconBackground,
),
padding: AppUtils.kPaddingAll16,
child: SvgPicture.asset(
"assets/svg/ic_mail.svg",
width: 32,
height: 32,
),
),
AppUtils.kBoxHeight16,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
(widget.argument?.mail?.isNotEmpty ?? false)
? AppLocalization.current.confirm_email
: AppLocalization.current.confirm_phone_text,
style: context.text.authTitle,
textAlign: TextAlign.center,
),
),
AppUtils.kBoxHeight16,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
(widget.argument?.mail?.isNotEmpty ?? false)
? AppLocalization.current.enter_code_mail(
widget.argument?.mail ?? "",
)
: AppLocalization.current.enter_code_phone(
widget.argument?.phoneNumber ?? "",
),
style: context.text.authDesc,
textAlign: TextAlign.center,
),
),
AppUtils.kBoxHeight48,
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 296,
child: PinCodeTextField(
controller: confirmController,
appContext: context,
autoDisposeControllers: false,
length: 4,
animationType: AnimationType.scale,
enabled: true,
enablePinAutofill: true,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
onChanged: (code) {
context.read<AuthConfirmBloc>().add(
SmsCodeEnterEvent(code),
);
},
autoFocus: true,
keyboardType: const TextInputType.numberWithOptions(),
showCursor: true,
textStyle: context.text.bigTitle,
cursorColor: context.color.accentColor,
cursorHeight: 20,
enableActiveFill: true,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
pinTheme: PinTheme(
activeColor:
state is ErrorState
? ThemeColors.timerRed
: state is SuccessSate
? ThemeColors.successInputSMS
: context.color.borderColor,
selectedFillColor:
context.color.scaffoldBackgroundColor,
activeFillColor:
context.color.scaffoldBackgroundColor,
shape: PinCodeFieldShape.box,
inactiveFillColor:
context.color.scaffoldBackgroundColor,
borderRadius: const BorderRadius.all(
Radius.circular(12),
),
borderWidth: 1,
inactiveColor:
state is ErrorState
? ThemeColors.timerRed
: state is SuccessSate
? ThemeColors.successInputSMS
: context.color.borderColor,
fieldWidth: 56,
fieldHeight: 64,
selectedColor:
state is ErrorState
? ThemeColors.timerRed
: state is SuccessSate
? ThemeColors.successInputSMS
: context.color.primaryColor,
),
),
),
],
),
if (state is ErrorState) AppUtils.kBoxHeight16,
if (state is ErrorState)
Center(
child: Text(
AppLocalization.current.incorrect_code,
style: AppTextStyles.timerBlue,
),
),
AppUtils.kBoxHeight24,
if (state.isTimerVisible)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
AppLocalization.current.send_sms,
style: AppTextStyles.timerBlue,
),
const TimerWidget(),
],
),
if (!state.isTimerVisible)
Center(
child: GestureDetector(
onTap: () {
if (widget.argument != null) {
context.read<AuthConfirmBloc>().add(
ResendCodeEvent(argument: widget.argument!),
);
}
},
child: Text(
AppLocalization.current.send_sms,
style: AppTextStyles.timerBlue,
),
),
),
AppUtils.kBoxHeight48,
Padding(
padding: AppUtils.kPaddingHor34,
child: ElevatedButton(
onPressed:
(confirmController.text.length == 4 &&
state is! LoadingState)
? () {
context.read<AuthConfirmBloc>().add(
OnSubmitEvent(
code: confirmController.text,
argument: widget.argument,
),
);
}
: null,
child:
state is LoadingState
? const CustomLoadingWidget()
: Text(AppLocalization.current.confirm),
),
),
],
),
),
);
},
);
}
@override
void dispose() {
disposeController();
super.dispose();
}
}

View File

@@ -0,0 +1,49 @@
import 'dart:async';
import 'package:cargocalculaterapp/core/theme/app_text_styles.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/auth_confirm/auth_confirm_bloc.dart';
class TimerWidget extends StatefulWidget {
const TimerWidget({super.key});
@override
State<TimerWidget> createState() => _TimerWidgetState();
}
class _TimerWidgetState extends State<TimerWidget> {
Timer? timer;
int time = 60;
String timeText = "60";
@override
void initState() {
if (timer?.isActive ?? false) {
timer?.cancel();
}
timer = Timer.periodic(const Duration(seconds: 1), (Timer timer) async {
time--;
if (time == 0) {
timer.cancel();
context.read<AuthConfirmBloc>().add(
const TimerChangedEvent(isVisible: false),
);
}
setState(() {
timeText = time.toString().padLeft(2, "0");
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Text(" 00:$timeText", style: AppTextStyles.timerStyle);
}
@override
void dispose() {
timer?.cancel();
super.dispose();
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/cupertino.dart';
mixin AuthConfirmMixin {
late TextEditingController confirmController;
void initControllers() {
confirmController = TextEditingController();
}
void disposeController() {
confirmController.dispose();
}
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/cupertino.dart';
mixin AuthMixin {
late TextEditingController loginController;
late TextEditingController passwordController;
void initControllers() {
loginController = TextEditingController();
passwordController = TextEditingController();
}
void disposeControllers() {
loginController.dispose();
passwordController.dispose();
}
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/cupertino.dart';
mixin SignUpMixin {
late TextEditingController loginController;
late TextEditingController fullNameController;
void initControllers() {
loginController = TextEditingController();
fullNameController = TextEditingController();
}
void disposeControllers() {
loginController.dispose();
fullNameController.dispose();
}
}

View File

@@ -0,0 +1,158 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:cargocalculaterapp/features/auth/presentation/pages/mixin/sign_up_mixin.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import '../../../../../core/utils/app_utils.dart';
import '../../../../../core/widgets/loading/custom_loading.dart';
import '../../../../../core/widgets/text_filds/custom_text_field_name.dart';
import '../../../../../generated/l10n.dart';
import '../../bloc/sign_up/sign_up_bloc.dart';
class SignUpPage extends StatefulWidget {
const SignUpPage({super.key});
@override
State<SignUpPage> createState() => _SignUpPageState();
}
class _SignUpPageState extends State<SignUpPage> with SignUpMixin {
@override
void initState() {
initControllers();
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<SignUpBloc, SignUpState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(),
body: ListView(
padding: AppUtils.kPaddingAll16,
children: [
Center(
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
borderRadius: AppUtils.kBorderRadius16,
color: context.color.iconBackground,
),
padding: AppUtils.kPaddingAll16,
child: SvgPicture.asset(
"assets/svg/ic_add_user.svg",
width: 32,
height: 32,
),
),
),
AppUtils.kBoxHeight16,
Text(
AppLocalization.current.sign_up,
style: context.text.authTitle,
textAlign: TextAlign.center,
),
AppUtils.kBoxHeight24,
CustomTextFieldName(
hint: AppLocalization.current.full_name_hint,
inputType: TextInputType.name,
name: AppLocalization.current.full_name,
controller: fullNameController,
onchange: (value) {
context.read<SignUpBloc>().add(
OnInputEnterEvent(
login: loginController.text,
fullName: value,
),
);
},
),
AppUtils.kBoxHeight16,
CustomTextFieldName(
hint: AppLocalization.current.enter_phone_or_mail,
inputType: TextInputType.text,
name: AppLocalization.current.phone,
controller: loginController,
onchange: (value) {
context.read<SignUpBloc>().add(
OnInputEnterEvent(
login: value,
fullName: fullNameController.text,
),
);
},
),
AppUtils.kBoxHeight48,
Padding(
padding: AppUtils.kPaddingHor34,
child: ElevatedButton(
onPressed:
state.isButtonEnabled && !state.isLoading
? () {
context.read<SignUpBloc>().add(
SubmitEvent(
login: loginController.text,
fullName: fullNameController.text,
),
);
}
: null,
child:
state.isLoading
? const CustomLoadingWidget()
: Text(AppLocalization.current.sign_up),
),
),
AppUtils.kBoxHeight16,
Text(
AppLocalization.current.has_account,
style: context.text.authDesc,
textAlign: TextAlign.center,
),
AppUtils.kBoxHeight16,
Padding(
padding: AppUtils.kPaddingHor34,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
Navigator.pop(context);
},
borderRadius: AppUtils.kBorderRadius24,
child: Ink(
decoration: BoxDecoration(
color: context.color.scaffoldBackgroundColor,
borderRadius: AppUtils.kBorderRadius24,
border: Border.all(color: context.color.primaryColor),
),
height: 56,
padding: AppUtils.kPaddingHor16,
child: Center(
child: Text(
AppLocalization.current.auth,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: context.color.textColor,
),
),
),
),
),
),
),
],
),
);
},
);
}
@override
void dispose() {
disposeControllers();
super.dispose();
}
}

View File

@@ -0,0 +1,9 @@
import 'package:cargocalculaterapp/features/calculator/data/model/calculate_price_response.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/lead_create_response.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/lead_create_request.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/price_calculate_request.dart';
abstract class CalculatorRemoteDataSource {
Future<CalculatePriceResponse> priceCalculate(PriceCalculateRequest request);
Future<LeadCreateResponse> createLead(LeadCreateRequest request);
}

View File

@@ -0,0 +1,64 @@
import 'package:cargocalculaterapp/features/calculator/data/model/calculate_price_response.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/price_calculate_request.dart';
import 'package:dio/dio.dart';
import '../../../../constants/constants.dart';
import '../../../../core/error/exceptions.dart';
import '../../../../core/local_source/local_source.dart';
import '../../../../injector_container.dart';
import '../model/lead_create_response.dart';
import '../model/lead_create_request.dart';
import 'calculator_remote_data_source.dart';
class CalculatorRemoteDataSourceImpl extends CalculatorRemoteDataSource {
final Dio dio;
CalculatorRemoteDataSourceImpl(this.dio);
@override
Future<CalculatePriceResponse> priceCalculate(
PriceCalculateRequest request,
) async {
try {
final Response response = await dio.post(
Constants.baseUrl + Urls.calculatePrice,
options:
DioConstants.options
..headers = {
"Authorization": "Bearer ${sl<LocalSource>().getAccessToken()}",
},
data: request,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return CalculatePriceResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);
} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
@override
Future<LeadCreateResponse> createLead(LeadCreateRequest request) async {
try {
final Response response = await dio.post(
Constants.baseUrl + Urls.lead,
options:
DioConstants.options
..headers = {
"Authorization": "Bearer ${sl<LocalSource>().getAccessToken()}",
},
data: request,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return LeadCreateResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);
} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
}

View File

@@ -0,0 +1,15 @@
class CalculatePriceResponse {
CalculatePriceResponse({this.price});
CalculatePriceResponse.fromJson(dynamic json) {
price = double.tryParse(json['price'].toString());
}
double? price;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['price'] = price;
return map;
}
}

View File

@@ -0,0 +1,73 @@
class LeadCreateRequest {
LeadCreateRequest({
this.warehouseCode,
this.wharehouseUz,
this.wharehouseRu,
this.wharehouseZh,
this.nameUz,
this.nameRu,
this.nameZh,
this.userUcode,
this.phoneNumber,
this.gmail,
this.weight,
this.price,
this.status,
this.averageWeightKg,
this.m3,
});
LeadCreateRequest.fromJson(dynamic json) {
warehouseCode = json['warehouse_code'];
wharehouseUz = json['wharehouseUz'];
wharehouseRu = json['wharehouseRu'];
wharehouseZh = json['wharehouseZh'];
nameUz = json['nameUz'];
nameRu = json['nameRu'];
nameZh = json['nameZh'];
userUcode = json['user_ucode'];
phoneNumber = json['phone_number'];
gmail = json['gmail'];
weight = json['weight'];
price = json['price'];
status = json['status'];
averageWeightKg = json['average_weight_kg'];
m3 = json['status'];
}
int? warehouseCode;
String? wharehouseUz;
String? wharehouseRu;
String? wharehouseZh;
String? nameUz;
String? nameRu;
String? nameZh;
String? userUcode;
String? phoneNumber;
String? gmail;
String? weight;
String? price;
String? status;
String? averageWeightKg;
String? m3;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['warehouse_code'] = warehouseCode;
map['wharehouseUz'] = wharehouseUz;
map['wharehouseRu'] = wharehouseRu;
map['wharehouseZh'] = wharehouseZh;
map['nameUz'] = nameUz;
map['nameRu'] = nameRu;
map['nameZh'] = nameZh;
map['user_ucode'] = userUcode;
map['phone_number'] = phoneNumber;
map['gmail'] = gmail;
map['weight'] = weight;
map['price'] = price;
map['status'] = status;
map['average_weight_kg'] = averageWeightKg;
map['m3'] = m3;
return map;
}
}

View File

@@ -0,0 +1,118 @@
class LeadCreateResponse {
LeadCreateResponse({
this.userUcode,
this.phoneNumber,
this.gmail,
this.weight,
this.status,
this.nameUz,
this.nameRu,
this.nameZh,
this.wharehouseUz,
this.wharehouseRu,
this.wharehouseZh,
this.id,
this.createdAt,
this.updatedAt,
});
LeadCreateResponse.fromJson(dynamic json) {
userUcode = json['user_ucode'];
phoneNumber = json['phone_number'];
gmail = json['gmail'];
weight = json['weight'];
status = json['status'] != null ? Status.fromJson(json['status']) : null;
nameUz = json['nameUz'];
nameRu = json['nameRu'];
nameZh = json['nameZh'];
wharehouseUz = json['wharehouseUz'];
wharehouseRu = json['wharehouseRu'];
wharehouseZh = json['wharehouseZh'];
createdAt = json['createdAt'];
updatedAt = json['updatedAt'];
id = json['id'];
}
String? userUcode;
String? phoneNumber;
String? gmail;
String? weight;
Status? status;
String? nameUz;
String? nameRu;
String? nameZh;
String? wharehouseUz;
String? wharehouseRu;
String? wharehouseZh;
String? id;
String? createdAt;
String? updatedAt;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['user_ucode'] = userUcode;
map['phone_number'] = phoneNumber;
map['gmail'] = gmail;
map['weight'] = weight;
if (status != null) {
map['status'] = status?.toJson();
}
map['nameUz'] = nameUz;
map['nameRu'] = nameRu;
map['nameZh'] = nameZh;
map['wharehouseUz'] = wharehouseUz;
map['wharehouseRu'] = wharehouseRu;
map['wharehouseZh'] = wharehouseZh;
map['createdAt'] = createdAt;
map['updatedAt'] = updatedAt;
map['id'] = id;
return map;
}
}
class Status {
Status({this.code, this.translations});
Status.fromJson(dynamic json) {
code = json['code'];
translations =
json['translations'] != null
? Translations.fromJson(json['translations'])
: null;
}
String? code;
Translations? translations;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['code'] = code;
if (translations != null) {
map['translations'] = translations?.toJson();
}
return map;
}
}
class Translations {
Translations({this.uz, this.ru, this.zh});
Translations.fromJson(dynamic json) {
uz = json['uz'];
ru = json['ru'];
zh = json['zh'];
}
String? uz;
String? ru;
String? zh;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['uz'] = uz;
map['ru'] = ru;
map['zh'] = zh;
return map;
}
}

View File

@@ -0,0 +1,20 @@
class PriceCalculateRequest {
PriceCalculateRequest({
this.avg,
this.m3,});
PriceCalculateRequest.fromJson(dynamic json) {
avg = json['avg'];
m3 = json['m3'];
}
double? avg;
double? m3;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['avg'] = avg;
map['m3'] = m3;
return map;
}
}

View File

@@ -0,0 +1,37 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/calculate_price_response.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/lead_create_response.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/lead_create_request.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/price_calculate_request.dart';
import 'package:cargocalculaterapp/features/calculator/domain/repository/calculator_repository.dart';
import 'package:dartz/dartz.dart';
import '../data_source/calculator_remote_data_source.dart';
class CalculatorRepositoryImpl extends CalculatorRepository {
final CalculatorRemoteDataSource calculatorRemoteDataSource;
CalculatorRepositoryImpl(this.calculatorRemoteDataSource);
@override
Future<Either<Failure, CalculatePriceResponse>> calculatePrice(
PriceCalculateRequest request,
) async {
try {
final response = await calculatorRemoteDataSource.priceCalculate(request);
return Right(response);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
@override
Future<Either<Failure, LeadCreateResponse>> createLead(LeadCreateRequest request) async {
try {
final response = await calculatorRemoteDataSource.createLead(request);
return Right(response);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}

View File

@@ -0,0 +1,15 @@
import 'package:cargocalculaterapp/features/calculator/data/model/calculate_price_response.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/lead_create_response.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/lead_create_request.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/price_calculate_request.dart';
import 'package:dartz/dartz.dart';
import '../../../../core/error/failure.dart';
abstract class CalculatorRepository {
Future<Either<Failure, CalculatePriceResponse>> calculatePrice(
PriceCalculateRequest request,
);
Future<Either<Failure, LeadCreateResponse>> createLead(
LeadCreateRequest request,
);
}

View File

@@ -0,0 +1,21 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/calculate_price_response.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/price_calculate_request.dart';
import 'package:dartz/dartz.dart';
import '../repository/calculator_repository.dart';
class CalculatePriceUseCase
extends UseCase<CalculatePriceResponse, PriceCalculateRequest> {
final CalculatorRepository repository;
CalculatePriceUseCase(this.repository);
@override
Future<Either<Failure, CalculatePriceResponse>> call(
PriceCalculateRequest params,
) async {
return await repository.calculatePrice(params);
}
}

View File

@@ -0,0 +1,21 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/lead_create_request.dart';
import 'package:cargocalculaterapp/features/calculator/data/model/lead_create_response.dart';
import 'package:dartz/dartz.dart';
import '../repository/calculator_repository.dart';
class CreateLeadUseCase
extends UseCase<LeadCreateResponse, LeadCreateRequest> {
final CalculatorRepository repository;
CreateLeadUseCase(this.repository);
@override
Future<Either<Failure, LeadCreateResponse>> call(
LeadCreateRequest params,
) async {
return await repository.createLead(params);
}
}

View File

@@ -0,0 +1,17 @@
class CalculatorInfoArgument {
final String productName;
final String weraHouse;
final double weight;
final double size;
final double averageWeight;
final double deliveryPrice;
CalculatorInfoArgument({
required this.productName,
required this.weraHouse,
required this.weight,
required this.size,
required this.averageWeight,
required this.deliveryPrice,
});
}

View File

@@ -0,0 +1,76 @@
import 'package:cargocalculaterapp/router/app_routes.dart';
import 'package:cargocalculaterapp/router/name_routes.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../data/model/price_calculate_request.dart';
import '../../domain/usecase/calculate_price_usecase.dart';
import '../arguments/calculator_info_argument.dart';
part 'calculator_event.dart';
part 'calculator_state.dart';
class CalculatorBloc extends Bloc<CalculatorEvent, CalculatorState> {
CalculatorBloc(this.calculatePriceUseCase)
: super(const CalculatorState(isLoading: false)) {
on<WeightSizeEnterEvent>(_weightSizeEntered);
on<WareHouseSelectEvent>(_wareHouseSelect);
on<CalculateEvent>(_calculate);
}
final CalculatePriceUseCase calculatePriceUseCase;
void _weightSizeEntered(
WeightSizeEnterEvent event,
Emitter<CalculatorState> emit,
) {
emit(
state.copyWith(
size: double.tryParse(event.size.replaceAll(",", "")),
weight: double.tryParse(event.weight.replaceAll(",", "")),
),
);
}
void _wareHouseSelect(
WareHouseSelectEvent event,
Emitter<CalculatorState> emit,
) {
emit(state.copyWith(selectedWarehouse: event.wareHouse));
}
Future<void> _calculate(
CalculateEvent event,
Emitter<CalculatorState> emit,
) async {
emit(state.copyWith(isLoading: true));
final response = await calculatePriceUseCase(
PriceCalculateRequest(
m3: state.size,
avg: (state.weight ?? 0) / (state.size ?? 1),
),
);
response.fold(
(l) {
emit(state.copyWith(isLoading: false));
},
(r) {
emit(state.copyWith(isLoading: false));
Navigator.pushNamed(
rootNavigatorKey.currentContext!,
Routes.calculationInfo,
arguments: CalculatorInfoArgument(
productName: event.productName,
deliveryPrice: r.price ?? 0,
size: state.size ?? 0,
averageWeight: (state.weight ?? 0) / (state.size ?? 1),
weight: state.weight ?? 0,
weraHouse: state.selectedWarehouse ?? "",
),
);
},
);
}
}

View File

@@ -0,0 +1,33 @@
part of 'calculator_bloc.dart';
sealed class CalculatorEvent extends Equatable {
const CalculatorEvent();
}
final class WeightSizeEnterEvent extends CalculatorEvent {
final String weight;
final String size;
const WeightSizeEnterEvent({required this.weight, required this.size});
@override
List<Object?> get props => [weight, size];
}
final class WareHouseSelectEvent extends CalculatorEvent {
final String wareHouse;
const WareHouseSelectEvent({required this.wareHouse});
@override
List<Object?> get props => [wareHouse];
}
final class CalculateEvent extends CalculatorEvent {
const CalculateEvent({required this.productName});
final String productName;
@override
List<Object?> get props => [productName];
}

View File

@@ -0,0 +1,120 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../../constants/constants.dart';
import '../../../../../generated/l10n.dart';
import '../../../../../router/app_routes.dart';
import '../../../../../router/name_routes.dart';
import '../../../data/model/lead_create_request.dart';
import '../../../domain/usecase/create_lead_usecase.dart';
import '../../arguments/calculator_info_argument.dart';
import '../../pages/calculator_info/dialog/order_dialog.dart';
part 'calculator_info_event.dart';
part 'calculator_info_state.dart';
class CalculatorInfoBloc
extends Bloc<CalculatorInfoEvent, CalculatorInfoState> {
CalculatorInfoBloc(this.createLeadUseCase)
: super(const CalculatorInfoState(isLoading: false)) {
on<CreateLeadEvent>(_createLead);
on<ShowSuccessDialogEvent>(_showSuccessDialog);
}
final CreateLeadUseCase createLeadUseCase;
Future<void> _createLead(
CreateLeadEvent event,
Emitter<CalculatorInfoState> emit,
) async {
emit(state.copyWith(isLoading: true));
final local = Localizations.localeOf(
rootNavigatorKey.currentContext!,
).languageCode;
final response = await createLeadUseCase(
LeadCreateRequest(
status: "NOT_CONTACTED",
m3: "${event.argument.size}",
averageWeightKg: "${event.argument.averageWeight}",
weight: "${event.argument.weight}",
nameRu: local == "ru" ? event.argument.productName : "",
nameUz: local == "uz" ? event.argument.productName : "",
nameZh: local == "zh" ? event.argument.productName : "",
price: "${event.argument.deliveryPrice}",
warehouseCode: int.tryParse(event.argument.weraHouse),
wharehouseRu: AppConst.warehouseOptions[event.argument.weraHouse]?.ru,
wharehouseUz: AppConst.warehouseOptions[event.argument.weraHouse]?.uz,
wharehouseZh: AppConst.warehouseOptions[event.argument.weraHouse]?.zh,
),
);
response.fold(
(l) {
emit(state.copyWith(isLoading: false));
},
(r) {
emit(state.copyWith(isLoading: false));
add(ShowSuccessDialogEvent(isCall: event.isCall));
},
);
}
void _showSuccessDialog(
ShowSuccessDialogEvent event,
Emitter<CalculatorInfoState> emit,
) {
if (event.isCall) {
showCupertinoModalPopup(
context: rootNavigatorKey.currentContext!,
builder: (context) => CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
child: Text(
"+998 99-110-22-22",
style: TextStyle(color: context.color.primaryColor),
),
onPressed: () async {
final Uri url = Uri(scheme: 'tel', path: "+998991102222");
if (await canLaunchUrl(url)) {
await launchUrl(url);
}
Navigator.pop(rootNavigatorKey.currentContext!, true);
},
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(rootNavigatorKey.currentContext!);
},
child: Text(
AppLocalization.current.cancel,
style: TextStyle(color: context.color.textColor),
),
),
),
).then((value) {
if (value is bool) {
Navigator.pushNamedAndRemoveUntil(
rootNavigatorKey.currentContext!,
Routes.main,
(route) => false,
);
}
});
} else {
showDialog(
context: rootNavigatorKey.currentContext!,
builder: (context) => OrderDialog(isCall: event.isCall),
).then((value) {
Navigator.pushNamedAndRemoveUntil(
rootNavigatorKey.currentContext!,
Routes.main,
(route) => false,
);
});
}
}
}

View File

@@ -0,0 +1,24 @@
part of 'calculator_info_bloc.dart';
sealed class CalculatorInfoEvent extends Equatable {
const CalculatorInfoEvent();
}
final class CreateLeadEvent extends CalculatorInfoEvent {
const CreateLeadEvent({required this.argument, required this.isCall});
final bool isCall;
final CalculatorInfoArgument argument;
@override
List<Object?> get props => [argument, isCall];
}
final class ShowSuccessDialogEvent extends CalculatorInfoEvent {
const ShowSuccessDialogEvent({required this.isCall});
final bool isCall;
@override
List<Object?> get props => [isCall];
}

View File

@@ -0,0 +1,14 @@
part of 'calculator_info_bloc.dart';
class CalculatorInfoState extends Equatable {
const CalculatorInfoState({required this.isLoading});
final bool isLoading;
CalculatorInfoState copyWith({bool? isLoading}) {
return CalculatorInfoState(isLoading: isLoading ?? this.isLoading);
}
@override
List<Object?> get props => [isLoading];
}

View File

@@ -0,0 +1,32 @@
part of 'calculator_bloc.dart';
class CalculatorState extends Equatable {
const CalculatorState({
required this.isLoading,
this.weight,
this.size,
this.selectedWarehouse,
});
final bool isLoading;
final double? weight;
final double? size;
final String? selectedWarehouse;
CalculatorState copyWith({
bool? isLoading,
double? weight,
double? size,
String? selectedWarehouse,
}) {
return CalculatorState(
isLoading: isLoading ?? this.isLoading,
weight: weight ?? this.weight,
size: size ?? this.size,
selectedWarehouse: selectedWarehouse ?? this.selectedWarehouse,
);
}
@override
List<Object?> get props => [isLoading, weight, size, selectedWarehouse];
}

View File

@@ -0,0 +1,22 @@
import 'package:flutter/cupertino.dart';
mixin CalculatorMixin{
late TextEditingController weightController;
late TextEditingController sizeController;
late TextEditingController averageWeightController;
late TextEditingController productNameController;
void initControllers(){
weightController=TextEditingController();
sizeController=TextEditingController();
averageWeightController=TextEditingController();
productNameController=TextEditingController();
}
void disposeController(){
weightController.dispose();
sizeController.dispose();
averageWeightController.dispose();
productNameController.dispose();
}
}

View File

@@ -0,0 +1,155 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:cargocalculaterapp/core/functions/base_finctions.dart';
import 'package:cargocalculaterapp/core/utils/app_utils.dart';
import 'package:cargocalculaterapp/core/widgets/loading/progress_hud.dart';
import 'package:cargocalculaterapp/features/calculator/presentation/pages/calculator_info/widget/calculator_info_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../../../constants/constants.dart';
import '../../../../../generated/l10n.dart';
import '../../arguments/calculator_info_argument.dart';
import '../../bloc/calculator_info/calculator_info_bloc.dart';
class CalculatorInfoPage extends StatelessWidget {
const CalculatorInfoPage({super.key, required this.argument});
final CalculatorInfoArgument? argument;
@override
Widget build(BuildContext context) {
return BlocBuilder<CalculatorInfoBloc, CalculatorInfoState>(
builder: (context, state) {
return Scaffold(
backgroundColor: context.color.grayBackground,
appBar: AppBar(title: Text(AppLocalization.current.calculator)),
body: ModalProgressHUD(
inAsyncCall: state.isLoading,
child: ListView(
padding: AppUtils.kPaddingAll16,
children: [
Container(
margin: const EdgeInsets.only(bottom: 32),
padding: AppUtils.kPaddingAll16,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: AppUtils.kBorderRadius16,
color: context.color.statusBackground,
border: Border.all(color: context.color.lightBorder),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CalculatorInfoWidget(
title: AppLocalization.current.product_name,
name: argument?.productName ?? "-",
),
CalculatorInfoWidget(
title: AppLocalization.current.warehouse,
name: Functions.getTranslatedItem(
AppConst.warehouseOptions[argument?.weraHouse],
context,
),
),
CalculatorInfoWidget(
title: AppLocalization.current.weight,
name: "${argument?.weight} kg",
),
CalculatorInfoWidget(
title: AppLocalization.current.size,
name: "${argument?.size}",
),
CalculatorInfoWidget(
title: AppLocalization.current.average_weight,
name: "${argument?.averageWeight}",
),
CalculatorInfoWidget(
title: AppLocalization.current.delivery_price,
name: "${argument?.deliveryPrice}",
),
],
),
),
Row(
children: [
Expanded(
child: InkWell(
onTap: () {
if (argument != null) {
context.read<CalculatorInfoBloc>().add(
CreateLeadEvent(
isCall: true,
argument: argument!,
),
);
}
},
borderRadius: AppUtils.kBorderRadius24,
child: Ink(
decoration: BoxDecoration(
color: context.color.scaffoldBackgroundColor,
borderRadius: AppUtils.kBorderRadius24,
border: Border.all(
color: context.color.primaryColor,
),
),
height: 56,
padding: AppUtils.kPaddingHor16,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
"assets/svg/ic_phone.svg",
colorFilter: ColorFilter.mode(
context.color.textColor,
BlendMode.srcIn,
),
),
AppUtils.kBoxWidth4,
Text(
AppLocalization.current.phone_call,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: context.color.textColor,
),
),
],
),
),
),
),
AppUtils.kBoxWidth16,
Expanded(
child: ElevatedButton(
onPressed: () {
if (argument != null) {
context.read<CalculatorInfoBloc>().add(
CreateLeadEvent(
isCall: false,
argument: argument!,
),
);
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset("assets/svg/ic_list_1.svg"),
AppUtils.kBoxWidth4,
Text(AppLocalization.current.send),
],
),
),
),
],
),
],
),
),
);
},
);
}
}

View File

@@ -0,0 +1,45 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:cargocalculaterapp/core/utils/app_utils.dart';
import 'package:cargocalculaterapp/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class OrderDialog extends StatelessWidget {
const OrderDialog({super.key, required this.isCall});
final bool isCall;
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: context.color.scaffoldBackgroundColor,
insetPadding: AppUtils.kPaddingAll16,
child: Padding(
padding: AppUtils.kPaddingVer24Hor16,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset("assets/svg/ic_done.svg"),
AppUtils.kBoxHeight20,
Text(
isCall
? AppLocalization.current.done_phone
: AppLocalization.current.done_order,
style: context.text.authDesc.copyWith(
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
AppUtils.kBoxHeight20,
ElevatedButton(
onPressed: () {
Navigator.pop(context, true);
},
child: Text(AppLocalization.current.done_ready),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:flutter/material.dart';
import '../../../../../../core/utils/app_utils.dart';
class CalculatorInfoWidget extends StatelessWidget {
const CalculatorInfoWidget({super.key, required this.title, required this.name});
final String title;
final String name;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: Text(title, style: context.text.orderListTitle)),
AppUtils.kBoxWidth8,
Expanded(
child: Text(
name,
style: context.text.profileCategory,
textAlign: TextAlign.end,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,146 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:cargocalculaterapp/core/utils/app_utils.dart';
import 'package:cargocalculaterapp/core/widgets/loading/progress_hud.dart';
import 'package:cargocalculaterapp/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../../core/widgets/drop_down/custom_drop_down.dart';
import '../../../../core/widgets/text_filds/custom_text_field_name.dart';
import '../bloc/calculator_bloc.dart';
import '../mixin/calculator_mixin.dart';
class CalculatorPage extends StatefulWidget {
const CalculatorPage({super.key});
@override
State<CalculatorPage> createState() => _CalculatorPageState();
}
class _CalculatorPageState extends State<CalculatorPage> with CalculatorMixin {
@override
void initState() {
initControllers();
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocConsumer<CalculatorBloc, CalculatorState>(
listener: (context, state) {
if ((state.weight ?? 0) > 0 && (state.size ?? 0) > 0) {
averageWeightController.text =
"${(state.weight ?? 0) / (state.size ?? 1)}";
}
},
builder: (context, state) {
return Scaffold(
backgroundColor: context.color.grayBackground,
appBar: AppBar(title: Text(AppLocalization.current.calculator)),
body: ModalProgressHUD(
inAsyncCall: state.isLoading,
child: ListView(
padding: AppUtils.kPaddingAll16,
children: [
CustomTextFieldName(
hint: AppLocalization.current.weight,
name: AppLocalization.current.weight_in_kg,
isRequired: true,
format: [
FilteringTextInputFormatter.allow(
RegExp(r'^\d*[.,]?\d{0,}$'), // accepts both "." and ","
),
],
controller: weightController,
inputType: const TextInputType.numberWithOptions(
decimal: true,
),
onchange: (value) {
context.read<CalculatorBloc>().add(
WeightSizeEnterEvent(
weight: value,
size: sizeController.text,
),
);
},
),
AppUtils.kBoxHeight16,
CustomTextFieldName(
hint: AppLocalization.current.size,
name: AppLocalization.current.size_in_m3,
isRequired: true,
controller: sizeController,
format: [
FilteringTextInputFormatter.allow(
RegExp(r'^\d*[.,]?\d{0,}$'), // accepts both "." and ","
),
],
inputType: const TextInputType.numberWithOptions(
decimal: true,
),
onchange: (value) {
context.read<CalculatorBloc>().add(
WeightSizeEnterEvent(
weight: weightController.text,
size: value,
),
);
},
),
AppUtils.kBoxHeight16,
CustomTextFieldName(
hint: AppLocalization.current.average_weight,
name: AppLocalization.current.average_weight_in,
controller: averageWeightController,
readOnly: true,
),
AppUtils.kBoxHeight16,
CustomTextFieldName(
hint: AppLocalization.current.product_name,
isRequired: true,
name: AppLocalization.current.product_name,
controller: productNameController,
inputType: TextInputType.name,
),
AppUtils.kBoxHeight16,
CustomDropDown(
isRequired: false,
value: state.selectedWarehouse,
onChange: (value) {
context.read<CalculatorBloc>().add(
WareHouseSelectEvent(wareHouse: value ?? ""),
);
},
name: AppLocalization.current.select_warehouse,
),
AppUtils.kBoxHeight16,
ElevatedButton(
onPressed: ((state.weight ?? 0) > 0 && (state.size ?? 0) > 0)
? () {
context.read<CalculatorBloc>().add(
CalculateEvent(
productName: productNameController.text,
),
);
}
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset("assets/svg/ic_calculator_1.svg"),
AppUtils.kBoxWith8,
Text(AppLocalization.current.calculate),
],
),
),
],
),
),
);
},
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:flutter/material.dart';
import '../../../../../core/utils/app_utils.dart';
class OrderDataWidget extends StatelessWidget {
const OrderDataWidget({super.key, required this.title, required this.name});
final String title;
final String name;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: Text(title, style: context.text.profileCategory)),
AppUtils.kBoxWidth8,
Expanded(
child: Text(
name,
style: context.text.profileCategory,
textAlign: TextAlign.end,
),
),
],
),
const Padding(padding: AppUtils.kPaddingVer8, child: AppUtils.kDivider),
],
);
}
}

View File

@@ -0,0 +1,25 @@
import 'package:cargocalculaterapp/features/home/data/models/comment_request.dart';
import 'package:cargocalculaterapp/features/home/data/models/comment_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/notification_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/order_single_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/orders_list_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/read_notification_request.dart';
import 'package:cargocalculaterapp/features/home/data/models/read_notification_response.dart';
import '../../models/banner_response.dart';
abstract class HomeRemoteDataSource {
Future<OrdersListResponse> ordersList(Map<String, dynamic> request);
Future<OrderSingleResponse> orderSingle(String orderId);
Future<CommentResponse> comment(CommentRequest request);
Future<NotificationResponse> notification(Map<String, dynamic> request);
Future<ReadNotificationResponse> notificationRead(
ReadNotificationRequest request,
);
Future<List<BannerResponse>> bannersResponse();
}

View File

@@ -0,0 +1,157 @@
import 'package:cargocalculaterapp/features/home/data/models/banner_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/comment_request.dart';
import 'package:cargocalculaterapp/features/home/data/models/comment_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/notification_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/order_single_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/orders_list_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/read_notification_request.dart';
import 'package:cargocalculaterapp/features/home/data/models/read_notification_response.dart';
import 'package:dio/dio.dart';
import '../../../../../constants/constants.dart';
import '../../../../../core/error/exceptions.dart';
import '../../../../../core/local_source/local_source.dart';
import '../../../../../injector_container.dart';
import 'home_remote_data_source.dart';
class HomeRemoteDataSourceImpl extends HomeRemoteDataSource {
final Dio dio;
HomeRemoteDataSourceImpl(this.dio);
@override
Future<OrdersListResponse> ordersList(Map<String, dynamic> request) async {
try {
final Response response = await dio.get(
Constants.baseUrl + Urls.orderList,
options: DioConstants.options
..headers = {
"Authorization": "Bearer ${sl<LocalSource>().getAccessToken()}",
},
queryParameters: request,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return OrdersListResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);
} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
@override
Future<OrderSingleResponse> orderSingle(String orderId) async {
try {
final Response response = await dio.get(
"${Constants.baseUrl}${Urls.orderList}/$orderId",
options: DioConstants.options
..headers = {
"Authorization": "Bearer ${sl<LocalSource>().getAccessToken()}",
},
);
if (response.statusCode == 200 || response.statusCode == 201) {
return OrderSingleResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);
} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
@override
Future<CommentResponse> comment(CommentRequest request) async {
try {
final Response response = await dio.post(
"${Constants.baseUrl}${Urls.comment}",
options: DioConstants.options
..headers = {
"Authorization": "Bearer ${sl<LocalSource>().getAccessToken()}",
},
data: request,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return CommentResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);
} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
@override
Future<NotificationResponse> notification(
Map<String, dynamic> request,
) async {
try {
final Response response = await dio.get(
"${Constants.baseUrl}${Urls.notification}",
options: DioConstants.options
..headers = {
"Authorization": "Bearer ${sl<LocalSource>().getAccessToken()}",
},
queryParameters: request,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return NotificationResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);
} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
@override
Future<ReadNotificationResponse> notificationRead(
ReadNotificationRequest request,
) async {
try {
final Response response = await dio.put(
"${Constants.baseUrl}${Urls.notificationRead}",
options: DioConstants.options
..headers = {
"Authorization": "Bearer ${sl<LocalSource>().getAccessToken()}",
},
data: request,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ReadNotificationResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);
} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
@override
Future<List<BannerResponse>> bannersResponse() async {
try {
final Response response = await dio.get(
"${Constants.baseUrl}${Urls.banners}",
options: DioConstants.options
..headers = {
"Authorization": "Bearer ${sl<LocalSource>().getAccessToken()}",
},
);
if (response.statusCode == 200 || response.statusCode == 201) {
final List<BannerResponse> images = (response.data as List)
.map((json) => BannerResponse.fromJson(json))
.toList();
return images;
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);
} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
}

View File

@@ -0,0 +1,27 @@
class BannerResponse {
BannerResponse({this.id, this.image, this.createdAt, this.updatedAt, this.v});
BannerResponse.fromJson(dynamic json) {
id = json['_id'];
image = json['image'];
createdAt = json['createdAt'];
updatedAt = json['updatedAt'];
v = json['__v'];
}
String? id;
String? image;
String? createdAt;
String? updatedAt;
int? v;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['_id'] = id;
map['image'] = image;
map['createdAt'] = createdAt;
map['updatedAt'] = updatedAt;
map['__v'] = v;
return map;
}
}

View File

@@ -0,0 +1,24 @@
class CommentRequest {
CommentRequest({
this.message,
this.rate,
this.orderNumber,});
CommentRequest.fromJson(dynamic json) {
message = json['message'];
rate = json['rate'];
orderNumber = json['order_number'];
}
String? message;
int? rate;
String? orderNumber;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['message'] = message;
map['rate'] = rate;
map['order_number'] = orderNumber;
return map;
}
}

View File

@@ -0,0 +1,44 @@
class CommentResponse {
CommentResponse({
this.message,
this.rate,
this.userUcode,
this.orderNumber,
this.id,
this.createdAt,
this.updatedAt,
this.v,});
CommentResponse.fromJson(dynamic json) {
message = json['message'];
rate = json['rate'];
userUcode = json['user_ucode'];
orderNumber = json['order_number'];
id = json['_id'];
createdAt = json['createdAt'];
updatedAt = json['updatedAt'];
v = json['__v'];
}
String? message;
int? rate;
String? userUcode;
String? orderNumber;
String? id;
String? createdAt;
String? updatedAt;
int? v;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['message'] = message;
map['rate'] = rate;
map['user_ucode'] = userUcode;
map['order_number'] = orderNumber;
map['_id'] = id;
map['createdAt'] = createdAt;
map['updatedAt'] = updatedAt;
map['__v'] = v;
return map;
}
}

View File

@@ -0,0 +1,79 @@
import 'package:cargocalculaterapp/core/models/name.dart';
class NotificationResponse {
NotificationResponse({this.notifications, this.totalCount});
NotificationResponse.fromJson(dynamic json) {
if (json['notifications'] != null) {
notifications = [];
json['notifications'].forEach((v) {
notifications?.add(Notifications.fromJson(v));
});
}
totalCount = json['totalCount'];
}
List<Notifications>? notifications;
int? totalCount;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
if (notifications != null) {
map['notifications'] = notifications?.map((v) => v.toJson()).toList();
}
map['totalCount'] = totalCount;
return map;
}
}
class Notifications {
Notifications({
this.id,
this.userUcode,
this.orderNumber,
this.text,
this.status,
this.isRead,
this.createdAt,
this.updatedAt,
this.v,
});
Notifications.fromJson(dynamic json) {
id = json['_id'];
userUcode = json['user_ucode'];
orderNumber = json['order_number'];
text = json['text'] != null ? Name.fromJson(json['text']) : null;
status = json['status'];
isRead = json['is_read'];
createdAt = json['createdAt'];
updatedAt = json['updatedAt'];
v = json['__v'];
}
String? id;
String? userUcode;
String? orderNumber;
Name? text;
String? status;
bool? isRead;
String? createdAt;
String? updatedAt;
int? v;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['_id'] = id;
map['user_ucode'] = userUcode;
map['order_number'] = orderNumber;
if (text != null) {
map['text'] = text?.toJson();
}
map['status'] = status;
map['is_read'] = isRead;
map['createdAt'] = createdAt;
map['updatedAt'] = updatedAt;
map['__v'] = v;
return map;
}
}

View File

@@ -0,0 +1,145 @@
import 'package:cargocalculaterapp/core/models/name.dart';
class OrderSingleResponse {
OrderSingleResponse({
this.id,
this.userUcode,
this.phoneNumber,
this.gmail,
this.quantity,
this.nameUz,
this.nameRu,
this.nameZh,
this.weight,
this.m3,
this.averageWeightKg,
this.wharehouseUz,
this.wharehouseRu,
this.wharehouseZh,
this.deliveryPrice,
this.shipmentId,
this.leadId,
this.status,
this.partiallyArrived,
this.fullArrived,
this.orderNumber,
this.createdAt,
this.updatedAt,
this.v,
this.arrivalDate,
this.images,
});
OrderSingleResponse.fromJson(dynamic json) {
userUcode = json['user_ucode'];
phoneNumber = json['phone_number'];
gmail = json['gmail'];
quantity = json['quantity'];
nameUz = json['nameUz'];
nameRu = json['nameRu'];
nameZh = json['nameZh'];
weight = json['weight'];
m3 = json['m3'];
averageWeightKg = json['average_weight_kg'];
wharehouseUz = json['wharehouseUz'];
wharehouseRu = json['wharehouseRu'];
wharehouseZh = json['wharehouseZh'];
deliveryPrice = json['delivery_price'];
shipmentId = json['shipment_id'];
leadId = json['lead_id'];
status = json['status'] != null ? Status.fromJson(json['status']) : null;
partiallyArrived = json['partially_arrived'];
fullArrived = json['full_arrived'];
orderNumber = json['order_number'];
createdAt = json['createdAt'];
updatedAt = json['updatedAt'];
v = json['__v'];
id = json['id'];
arrivalDate = json['arrival_date'];
images = json['images'] != null ? json['images'].cast<String>() : [];
}
String? userUcode;
String? phoneNumber;
String? gmail;
int? quantity;
String? nameUz;
String? nameRu;
String? nameZh;
String? weight;
String? m3;
String? averageWeightKg;
String? wharehouseUz;
String? wharehouseRu;
String? wharehouseZh;
String? deliveryPrice;
String? shipmentId;
String? leadId;
Status? status;
bool? partiallyArrived;
bool? fullArrived;
String? orderNumber;
String? createdAt;
String? updatedAt;
int? v;
String? id;
String? arrivalDate;
List<String>? images;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['user_ucode'] = userUcode;
map['phone_number'] = phoneNumber;
map['gmail'] = gmail;
map['quantity'] = quantity;
map['nameUz'] = nameUz;
map['nameRu'] = nameRu;
map['nameZh'] = nameZh;
map['weight'] = weight;
map['m3'] = m3;
map['average_weight_kg'] = averageWeightKg;
map['wharehouseUz'] = wharehouseUz;
map['wharehouseRu'] = wharehouseRu;
map['wharehouseZh'] = wharehouseZh;
map['delivery_price'] = deliveryPrice;
map['shipment_id'] = shipmentId;
map['lead_id'] = leadId;
if (status != null) {
map['status'] = status?.toJson();
}
map['partially_arrived'] = partiallyArrived;
map['full_arrived'] = fullArrived;
map['order_number'] = orderNumber;
map['createdAt'] = createdAt;
map['updatedAt'] = updatedAt;
map['__v'] = v;
map['id'] = id;
map['arrival_date'] = arrivalDate;
map['images'] = images;
return map;
}
}
class Status {
Status({this.code, this.translations});
Status.fromJson(dynamic json) {
code = json['code'];
translations =
json['translations'] != null
? Name.fromJson(json['translations'])
: null;
}
String? code;
Name? translations;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['code'] = code;
if (translations != null) {
map['translations'] = translations?.toJson();
}
return map;
}
}

View File

@@ -0,0 +1,160 @@
import '../../../../core/models/name.dart';
class OrdersListResponse {
OrdersListResponse({this.totalCount, this.data});
OrdersListResponse.fromJson(dynamic json) {
totalCount = json['totalCount'];
if (json['data'] != null) {
data = [];
json['data'].forEach((v) {
data?.add(Data.fromJson(v));
});
}
}
int? totalCount;
List<Data>? data;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['totalCount'] = totalCount;
if (data != null) {
map['data'] = data?.map((v) => v.toJson()).toList();
}
return map;
}
}
class Data {
Data({
this.id,
this.userUcode,
this.phoneNumber,
this.gmail,
this.quantity,
this.nameUz,
this.nameRu,
this.nameZh,
this.size,
this.wharehouseUz,
this.wharehouseRu,
this.wharehouseZh,
this.deliveryPrice,
this.shipmentId,
this.leadId,
this.status,
this.partiallyArrived,
this.fullArrived,
this.orderNumber,
this.createdAt,
this.updatedAt,
this.v,
this.arrivalDate,
});
Data.fromJson(dynamic json) {
userUcode = json['user_ucode'];
phoneNumber = json['phone_number'];
gmail = json['gmail'];
quantity = json['quantity'];
nameUz = json['nameUz'];
nameRu = json['nameRu'];
nameZh = json['nameZh'];
size = json['size'];
wharehouseUz = json['wharehouseUz'];
wharehouseRu = json['wharehouseRu'];
wharehouseZh = json['wharehouseZh'];
deliveryPrice = json['delivery_price'];
shipmentId = json['shipment_id'];
leadId = json['lead_id'];
status = json['status'] != null ? Status.fromJson(json['status']) : null;
partiallyArrived = json['partially_arrived'];
fullArrived = json['full_arrived'];
orderNumber = json['order_number'];
createdAt = json['createdAt'];
updatedAt = json['updatedAt'];
v = json['__v'];
id = json['id'];
arrivalDate = json['arrival_date'];
}
String? userUcode;
String? phoneNumber;
String? gmail;
int? quantity;
String? nameUz;
String? nameRu;
String? nameZh;
String? size;
String? wharehouseUz;
String? wharehouseRu;
String? wharehouseZh;
String? deliveryPrice;
String? shipmentId;
String? leadId;
Status? status;
bool? partiallyArrived;
bool? fullArrived;
String? orderNumber;
String? createdAt;
String? updatedAt;
int? v;
String? id;
String? arrivalDate;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['_id'] = id;
map['user_ucode'] = userUcode;
map['phone_number'] = phoneNumber;
map['gmail'] = gmail;
map['quantity'] = quantity;
map['nameUz'] = nameUz;
map['nameRu'] = nameRu;
map['nameZh'] = nameZh;
map['size'] = size;
map['wharehouseUz'] = wharehouseUz;
map['wharehouseRu'] = wharehouseRu;
map['wharehouseZh'] = wharehouseZh;
map['delivery_price'] = deliveryPrice;
map['shipment_id'] = shipmentId;
map['lead_id'] = leadId;
if (status != null) {
map['status'] = status?.toJson();
}
map['partially_arrived'] = partiallyArrived;
map['full_arrived'] = fullArrived;
map['order_number'] = orderNumber;
map['createdAt'] = createdAt;
map['updatedAt'] = updatedAt;
map['__v'] = v;
map['id'] = id;
map['arrival_date'] = arrivalDate;
return map;
}
}
class Status {
Status({this.code, this.translations});
Status.fromJson(dynamic json) {
code = json['code'];
translations =
json['translations'] != null
? Name.fromJson(json['translations'])
: null;
}
String? code;
Name? translations;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['code'] = code;
if (translations != null) {
map['translations'] = translations?.toJson();
}
return map;
}
}

View File

@@ -0,0 +1,16 @@
class ReadNotificationRequest {
ReadNotificationRequest({
this.notificationIds,});
ReadNotificationRequest.fromJson(dynamic json) {
notificationIds = json['notification_ids'] != null ? json['notification_ids'].cast<String>() : [];
}
List<String>? notificationIds;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['notification_ids'] = notificationIds;
return map;
}
}

View File

@@ -0,0 +1,16 @@
class ReadNotificationResponse {
ReadNotificationResponse({
this.message,});
ReadNotificationResponse.fromJson(dynamic json) {
message = json['message'];
}
String? message;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['message'] = message;
return map;
}
}

View File

@@ -0,0 +1,88 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/features/home/data/models/banner_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/comment_request.dart';
import 'package:cargocalculaterapp/features/home/data/models/comment_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/notification_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/order_single_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/orders_list_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/read_notification_request.dart';
import 'package:cargocalculaterapp/features/home/data/models/read_notification_response.dart';
import 'package:dartz/dartz.dart';
import '../../domain/repository/home_repository.dart';
import '../data_source/remote/home_remote_data_source.dart';
class HomeRepositoryImpl extends HomeRepository {
final HomeRemoteDataSource remoteDataSource;
HomeRepositoryImpl(this.remoteDataSource);
@override
Future<Either<Failure, OrdersListResponse>> ordersList(
Map<String, dynamic> request,
) async {
try {
final response = await remoteDataSource.ordersList(request);
return Right(response);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
@override
Future<Either<Failure, OrderSingleResponse>> orderSingle(
String orderId,
) async {
try {
final response = await remoteDataSource.orderSingle(orderId);
return Right(response);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
@override
Future<Either<Failure, CommentResponse>> comment(
CommentRequest request,
) async {
try {
final response = await remoteDataSource.comment(request);
return Right(response);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
@override
Future<Either<Failure, NotificationResponse>> notification(
Map<String, dynamic> request,
) async {
try {
final response = await remoteDataSource.notification(request);
return Right(response);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
@override
Future<Either<Failure, ReadNotificationResponse>> notificationRead(
ReadNotificationRequest request,
) async {
try {
final response = await remoteDataSource.notificationRead(request);
return Right(response);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
@override
Future<Either<Failure, List<BannerResponse>>> bannersGet() async {
try {
final response = await remoteDataSource.bannersResponse();
return Right(response);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}

View File

@@ -0,0 +1,30 @@
import 'package:cargocalculaterapp/features/home/data/models/comment_request.dart';
import 'package:cargocalculaterapp/features/home/data/models/comment_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/notification_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/order_single_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/orders_list_response.dart';
import 'package:cargocalculaterapp/features/home/data/models/read_notification_request.dart';
import 'package:cargocalculaterapp/features/home/data/models/read_notification_response.dart';
import 'package:dartz/dartz.dart';
import '../../../../core/error/failure.dart';
import '../../data/models/banner_response.dart';
abstract class HomeRepository {
Future<Either<Failure, OrdersListResponse>> ordersList(
Map<String, dynamic> request,
);
Future<Either<Failure, OrderSingleResponse>> orderSingle(String orderId);
Future<Either<Failure, CommentResponse>> comment(CommentRequest request);
Future<Either<Failure, NotificationResponse>> notification(
Map<String, dynamic> request,
);
Future<Either<Failure, ReadNotificationResponse>> notificationRead(
ReadNotificationRequest request,
);
Future<Either<Failure, List<BannerResponse>>> bannersGet();
}

View File

@@ -0,0 +1,16 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:dartz/dartz.dart';
import '../../data/models/banner_response.dart';
import '../repository/home_repository.dart';
class BannersUseCase extends UseCase<List<BannerResponse>, NoParams> {
final HomeRepository repository;
BannersUseCase(this.repository);
@override
Future<Either<Failure, List<BannerResponse>>> call(NoParams params) async {
return await repository.bannersGet();
}
}

View File

@@ -0,0 +1,16 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:dartz/dartz.dart';
import '../../data/models/comment_request.dart';
import '../../data/models/comment_response.dart';
import '../repository/home_repository.dart';
class CommentUseCase extends UseCase<CommentResponse, CommentRequest>{
final HomeRepository repository;
CommentUseCase(this.repository);
@override
Future<Either<Failure, CommentResponse>> call(CommentRequest params) async {
return await repository.comment(params);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:cargocalculaterapp/features/home/data/models/read_notification_request.dart';
import 'package:cargocalculaterapp/features/home/data/models/read_notification_response.dart';
import 'package:dartz/dartz.dart';
import '../repository/home_repository.dart';
class NotificationReadUseCase extends UseCase<ReadNotificationResponse, ReadNotificationRequest>{
final HomeRepository repository;
NotificationReadUseCase(this.repository);
@override
Future<Either<Failure, ReadNotificationResponse>> call(ReadNotificationRequest params) async {
return await repository.notificationRead(params);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/features/home/data/models/notification_response.dart';
import 'package:dartz/dartz.dart';
import '../../../../core/usecase/usecase.dart';
import '../repository/home_repository.dart';
class NotificationUseCase
extends UseCase<NotificationResponse, Map<String, dynamic>> {
final HomeRepository repository;
NotificationUseCase(this.repository);
@override
Future<Either<Failure, NotificationResponse>> call(
Map<String, dynamic> params,
) async {
return await repository.notification(params);
}
}

View File

@@ -0,0 +1,17 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:cargocalculaterapp/features/home/data/models/order_single_response.dart';
import 'package:dartz/dartz.dart';
import '../repository/home_repository.dart';
class OrderSingleUseCase extends UseCase<OrderSingleResponse, String> {
final HomeRepository repository;
OrderSingleUseCase(this.repository);
@override
Future<Either<Failure, OrderSingleResponse>> call(String params) async {
return await repository.orderSingle(params);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:cargocalculaterapp/features/home/data/models/orders_list_response.dart';
import 'package:cargocalculaterapp/features/home/domain/repository/home_repository.dart';
import 'package:dartz/dartz.dart';
class OrdersListUseCase
extends UseCase<OrdersListResponse, Map<String, dynamic>> {
final HomeRepository repository;
OrdersListUseCase(this.repository);
@override
Future<Either<Failure, OrdersListResponse>> call(
Map<String, dynamic> params,
) async {
return await repository.ordersList(params);
}
}

View File

@@ -0,0 +1,53 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../router/app_routes.dart';
import '../../../data/models/comment_request.dart';
import '../../../domain/usecase/comment_usecase.dart';
part 'comment_event.dart';
part 'comment_state.dart';
class CommentBloc extends Bloc<CommentEvent, CommentState> {
CommentBloc(this.commentUseCase)
: super(const CommentState(rating: 0, isLoading: false)) {
on<RatingChangedEvent>(_ratingChange);
on<CommentEnterEvent>(_commentEnter);
on<SubmitEvent>(_commentSubmit);
}
final CommentUseCase commentUseCase;
void _ratingChange(RatingChangedEvent event, Emitter<CommentState> emit) {
emit(state.copyWith(rating: event.rating));
}
void _commentEnter(CommentEnterEvent event, Emitter<CommentState> emit) {
emit(state.copyWith(comment: event.comment));
}
Future<void> _commentSubmit(
SubmitEvent event,
Emitter<CommentState> emit,
) async {
emit(state.copyWith(isLoading: true));
final response = await commentUseCase(
CommentRequest(
orderNumber: event.orderId,
rate: state.rating.toInt(),
message: state.comment,
),
);
response.fold(
(l) {
emit(state.copyWith(isLoading: false));
},
(r) {
emit(state.copyWith(isLoading: false));
Navigator.pop(rootNavigatorKey.currentContext!);
},
);
}
}

View File

@@ -0,0 +1,32 @@
part of 'comment_bloc.dart';
sealed class CommentEvent extends Equatable {
const CommentEvent();
}
final class RatingChangedEvent extends CommentEvent {
final double rating;
const RatingChangedEvent({required this.rating});
@override
List<Object?> get props => [rating];
}
final class CommentEnterEvent extends CommentEvent {
final String comment;
const CommentEnterEvent({required this.comment});
@override
List<Object?> get props => [comment];
}
final class SubmitEvent extends CommentEvent {
const SubmitEvent({required this.orderId});
final String orderId;
@override
List<Object?> get props => [orderId];
}

View File

@@ -0,0 +1,24 @@
part of 'comment_bloc.dart';
class CommentState extends Equatable {
const CommentState({
required this.rating,
required this.isLoading,
this.comment,
});
final double rating;
final bool isLoading;
final String? comment;
CommentState copyWith({double? rating, bool? isLoading, String? comment}) {
return CommentState(
rating: rating ?? this.rating,
isLoading: isLoading ?? this.isLoading,
comment: comment ?? this.comment,
);
}
@override
List<Object?> get props => [rating, isLoading, comment];
}

View File

@@ -0,0 +1,127 @@
import 'package:cargocalculaterapp/constants/constants.dart';
import 'package:cargocalculaterapp/features/home/data/models/orders_list_response.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/usecase/usecase.dart';
import '../../data/models/banner_response.dart';
import '../../domain/usecase/banners_usecase.dart';
import '../../domain/usecase/orders_list_usecase.dart';
part 'home_event.dart';
part 'home_state.dart';
class HomeBloc extends Bloc<HomeEvent, HomeState> {
HomeBloc(this.ordersListUseCase, this.bannersUseCase)
: super(
const HomeState(
paginationLoading: false,
isLoading: false,
page: 1,
selectedStatus: AppConst.all,
),
) {
on<GetAllOrdersListEvent>(_getAllOrders);
on<GetOrdersEvent>(_getOrders);
on<OrderStatusEvent>(_changeOrderStatus);
on<PaginationEvent>(_paginationLoading);
on<GetBannersEvent>(_getBanner);
on<RefreshEvent>(_refresh);
}
final OrdersListUseCase ordersListUseCase;
final BannersUseCase bannersUseCase;
final Map<String, dynamic> productsRequest = {};
void _getAllOrders(GetAllOrdersListEvent event, Emitter<HomeState> emit) {
productsRequest.remove(AppKeys.status);
productsRequest[AppKeys.page] = 1;
productsRequest[AppKeys.limit] = AppConst.limit;
add(const GetOrdersEvent());
}
Future<void> _getOrders(GetOrdersEvent event, Emitter<HomeState> emit) async {
emit(state.copyWith(isLoading: event.isLoading));
final response = await ordersListUseCase(productsRequest);
response.fold(
(l) {
emit(state.copyWith(isLoading: false, paginationLoading: false));
},
(r) {
emit(
state.copyWith(
isLoading: false,
paginationLoading: false,
ordersList: r,
page: productsRequest[AppKeys.page],
pageCount: ((r.totalCount ?? 1) / 20).ceil(),
),
);
},
);
}
void _changeOrderStatus(OrderStatusEvent event, Emitter<HomeState> emit) {
if (event.status == state.selectedStatus) {
return;
}
emit(state.copyWith(selectedStatus: event.status));
productsRequest[AppKeys.page] = 1;
if (event.status == AppConst.all) {
productsRequest.remove(AppConst.status);
} else {
productsRequest[AppConst.status] = event.status;
}
add(const GetOrdersEvent());
}
Future<void> _paginationLoading(
PaginationEvent event,
Emitter<HomeState> emit,
) async {
productsRequest[AppKeys.page] = productsRequest[AppKeys.page] + 1;
emit(
state.copyWith(
paginationLoading: true,
page: productsRequest[AppKeys.page],
),
);
final response = await ordersListUseCase(productsRequest);
response.fold(
(l) {
emit(state.copyWith(isLoading: false, paginationLoading: false));
},
(r) {
emit(
state.copyWith(
isLoading: false,
paginationLoading: false,
ordersList: OrdersListResponse(
totalCount: r.totalCount,
data: state.ordersList?.data?..addAll(r.data ?? []),
),
page: productsRequest[AppKeys.page],
pageCount: ((r.totalCount ?? 1) / 20).ceil(),
),
);
},
);
}
Future<void> _getBanner(
GetBannersEvent event,
Emitter<HomeState> emit,
) async {
final response = await bannersUseCase(const NoParams());
response.fold((l) {}, (r) {
emit(state.copyWith(banners: r));
});
}
void _refresh(RefreshEvent event, Emitter<HomeState> emit) {
productsRequest[AppKeys.page] = 1;
add(const GetOrdersEvent(isLoading: false));
add(const GetBannersEvent());
}
}

View File

@@ -0,0 +1,51 @@
part of 'home_bloc.dart';
sealed class HomeEvent extends Equatable {
const HomeEvent();
}
final class GetAllOrdersListEvent extends HomeEvent {
const GetAllOrdersListEvent();
@override
List<Object?> get props => [];
}
final class GetOrdersEvent extends HomeEvent {
const GetOrdersEvent({this.isLoading = true});
final bool isLoading;
@override
List<Object?> get props => [isLoading];
}
final class OrderStatusEvent extends HomeEvent {
final String status;
const OrderStatusEvent({required this.status});
@override
List<Object?> get props => [status];
}
final class PaginationEvent extends HomeEvent {
const PaginationEvent();
@override
List<Object?> get props => [];
}
final class GetBannersEvent extends HomeEvent {
const GetBannersEvent();
@override
List<Object?> get props => [];
}
final class RefreshEvent extends HomeEvent {
const RefreshEvent();
@override
List<Object?> get props => [];
}

View File

@@ -0,0 +1,52 @@
part of 'home_bloc.dart';
class HomeState extends Equatable {
const HomeState({
required this.isLoading,
required this.paginationLoading,
required this.page,
this.ordersList,
this.pageCount,
this.banners,
required this.selectedStatus,
});
final bool isLoading;
final bool paginationLoading;
final int page;
final int? pageCount;
final OrdersListResponse? ordersList;
final String selectedStatus;
final List<BannerResponse>? banners;
HomeState copyWith({
bool? isLoading,
bool? paginationLoading,
int? page,
int? pageCount,
OrdersListResponse? ordersList,
String? selectedStatus,
List<BannerResponse>? banners,
}) {
return HomeState(
isLoading: isLoading ?? this.isLoading,
page: page ?? this.page,
pageCount: pageCount ?? this.pageCount,
paginationLoading: paginationLoading ?? this.paginationLoading,
ordersList: ordersList ?? this.ordersList,
selectedStatus: selectedStatus ?? this.selectedStatus,
banners: banners ?? this.banners,
);
}
@override
List<Object?> get props => [
isLoading,
paginationLoading,
page,
ordersList,
pageCount,
selectedStatus,
banners,
];
}

View File

@@ -0,0 +1,102 @@
import 'package:cargocalculaterapp/constants/constants.dart';
import 'package:cargocalculaterapp/features/home/data/models/read_notification_request.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../data/models/notification_response.dart';
import '../../../domain/usecase/notification_read_usecase.dart';
import '../../../domain/usecase/notification_usecase.dart';
part 'notification_event.dart';
part 'notification_state.dart';
class NotificationBloc extends Bloc<NotificationEvent, NotificationState> {
NotificationBloc(this.notificationUseCase, this.notificationReadUseCase)
: super(
const NotificationState(
isLoading: false,
currentPage: 1,
paginationLoading: false,
),
) {
on<GetNotificationsEvent>(_onGetNotifications);
on<NotificationPaginationEvent>(_notificationPagination);
on<ReadNotificationsEvent>(_notificationRead);
}
final NotificationUseCase notificationUseCase;
final NotificationReadUseCase notificationReadUseCase;
final Map<String, dynamic> notificationRequest = {};
Future<void> _onGetNotifications(
GetNotificationsEvent event,
Emitter<NotificationState> emit,
) async {
emit(state.copyWith(isLoading: true));
notificationRequest[AppKeys.page] = 1;
notificationRequest[AppKeys.limit] = AppConst.limit;
final response = await notificationUseCase(notificationRequest);
response.fold(
(l) {
emit(state.copyWith(isLoading: false));
},
(r) {
add(ReadNotificationsEvent(notifications: r.notifications ?? []));
emit(
state.copyWith(
isLoading: false,
currentPage: 1,
notifications: r.notifications,
pageCount: ((r.totalCount ?? 1) / 20).ceil(),
),
);
},
);
}
Future<void> _notificationPagination(
NotificationPaginationEvent event,
Emitter<NotificationState> emit,
) async {
notificationRequest[AppKeys.page] = event.page;
emit(state.copyWith(paginationLoading: true, currentPage: event.page));
final response = await notificationUseCase(notificationRequest);
response.fold(
(l) {
emit(state.copyWith(paginationLoading: false));
},
(r) {
add(ReadNotificationsEvent(notifications: r.notifications ?? []));
final List<Notifications> notifications = [];
notifications.addAll(List.of(state.notifications ?? []));
notifications.addAll(List.of(r.notifications ?? []));
emit(
state.copyWith(
paginationLoading: false,
pageCount: ((r.totalCount ?? 1) / 20).ceil(),
notifications: notifications,
),
);
},
);
}
Future<void> _notificationRead(
ReadNotificationsEvent event,
Emitter<NotificationState> emit,
) async {
final List<String> ids = [];
for (var element in event.notifications) {
if (!(element.isRead ?? false)) {
ids.add(element.id ?? "");
}
}
if (ids.isNotEmpty) {
await notificationReadUseCase(
ReadNotificationRequest(notificationIds: ids),
);
}
}
}

View File

@@ -0,0 +1,30 @@
part of 'notification_bloc.dart';
sealed class NotificationEvent extends Equatable {
const NotificationEvent();
}
final class GetNotificationsEvent extends NotificationEvent {
const GetNotificationsEvent();
@override
List<Object?> get props => [];
}
final class NotificationPaginationEvent extends NotificationEvent {
const NotificationPaginationEvent({required this.page});
final int page;
@override
List<Object?> get props => [page];
}
final class ReadNotificationsEvent extends NotificationEvent {
final List<Notifications> notifications;
const ReadNotificationsEvent({required this.notifications});
@override
List<Object?> get props => [notifications];
}

View File

@@ -0,0 +1,42 @@
part of 'notification_bloc.dart';
class NotificationState extends Equatable {
const NotificationState({
required this.isLoading,
required this.paginationLoading,
required this.currentPage,
this.pageCount,
this.notifications,
});
final bool isLoading;
final bool paginationLoading;
final int currentPage;
final int? pageCount;
final List<Notifications>? notifications;
NotificationState copyWith({
bool? isLoading,
bool? paginationLoading,
int? currentPage,
int? pageCount,
List<Notifications>? notifications,
}) {
return NotificationState(
isLoading: isLoading ?? this.isLoading,
paginationLoading: paginationLoading ?? this.paginationLoading,
currentPage: currentPage ?? this.currentPage,
pageCount: pageCount ?? this.pageCount,
notifications: notifications ?? this.notifications,
);
}
@override
List<Object?> get props => [
isLoading,
paginationLoading,
currentPage,
pageCount,
notifications,
];
}

View File

@@ -0,0 +1,34 @@
import 'package:cargocalculaterapp/features/home/data/models/order_single_response.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/usecase/order_single_usecase.dart';
part 'order_single_event.dart';
part 'order_single_state.dart';
class OrderSingleBloc extends Bloc<OrderSingleEvent, OrderSingleState> {
OrderSingleBloc(this.orderSingleUseCase)
: super(const OrderSingleState(isLoading: false)) {
on<GetOrderSingleEvent>(_getOrderInfo);
}
final OrderSingleUseCase orderSingleUseCase;
Future<void> _getOrderInfo(
GetOrderSingleEvent event,
Emitter<OrderSingleState> emit,
) async {
emit(state.copyWith(isLoading: true));
final response = await orderSingleUseCase(event.orderId);
response.fold(
(l) {
emit(state.copyWith(isLoading: false));
},
(r) {
emit(state.copyWith(isLoading: false, orderSingleResponse: r));
},
);
}
}

View File

@@ -0,0 +1,14 @@
part of 'order_single_bloc.dart';
sealed class OrderSingleEvent extends Equatable {
const OrderSingleEvent();
}
class GetOrderSingleEvent extends OrderSingleEvent {
const GetOrderSingleEvent({required this.orderId});
final String orderId;
@override
List<Object?> get props => [orderId];
}

View File

@@ -0,0 +1,21 @@
part of 'order_single_bloc.dart';
class OrderSingleState extends Equatable {
const OrderSingleState({required this.isLoading, this.orderSingleResponse});
final bool isLoading;
final OrderSingleResponse? orderSingleResponse;
OrderSingleState copyWith({
bool? isLoading,
OrderSingleResponse? orderSingleResponse,
}) {
return OrderSingleState(
isLoading: isLoading ?? this.isLoading,
orderSingleResponse: orderSingleResponse ?? this.orderSingleResponse,
);
}
@override
List<Object?> get props => [isLoading, orderSingleResponse];
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
mixin BannerMixin {
late PageController pageController;
void initController() {
pageController = PageController(viewportFraction: 0.94);
}
void disposeController() {
pageController.dispose();
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/cupertino.dart';
mixin CommentMixin {
late TextEditingController commentController;
void initControllers() {
commentController = TextEditingController();
}
void disposeControllers() {
commentController.dispose();
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
mixin HomeMixin {
late ScrollController scrollController;
void initControllers() {
scrollController = ScrollController();
}
void disposeControllers() {
scrollController.dispose();
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/cupertino.dart';
mixin ImagePageMixin {
late PageController pageController;
void initController(int initialPage) {
pageController = PageController(initialPage: initialPage + 1);
}
void disposeController() {
pageController.dispose();
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
mixin NotificationMixin {
late ScrollController scrollController;
void initControllers() {
scrollController = ScrollController();
}
void disposeControllers() {
scrollController.dispose();
}
}

View File

@@ -0,0 +1,5 @@
class CommentArgument {
final String orderId;
CommentArgument({required this.orderId});
}

View File

@@ -0,0 +1,6 @@
class ImagePageArgument {
final List<String>? images;
final int? currentPage;
ImagePageArgument({this.images, this.currentPage});
}

View File

@@ -0,0 +1,6 @@
class OrderInfoArgument{
final String orderId;
final String orderNumber;
OrderInfoArgument({required this.orderId, required this.orderNumber});
}

View File

@@ -0,0 +1,97 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:cargocalculaterapp/core/utils/app_utils.dart';
import 'package:cargocalculaterapp/core/widgets/loading/progress_hud.dart';
import 'package:cargocalculaterapp/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../core/widgets/rating/rating_bar_widget.dart';
import '../../../../../core/widgets/text_filds/custom_text_field_name.dart';
import '../../bloc/comment/comment_bloc.dart';
import '../../mixin/comment_mixin.dart';
import '../arguments/comment_argument.dart';
class CommentPage extends StatefulWidget {
const CommentPage({super.key, required this.argument});
final CommentArgument? argument;
@override
State<CommentPage> createState() => _CommentPageState();
}
class _CommentPageState extends State<CommentPage> with CommentMixin {
@override
void initState() {
initControllers();
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<CommentBloc, CommentState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(title: Text(AppLocalization.current.left_comment)),
body: ModalProgressHUD(
inAsyncCall: state.isLoading,
child: ListView(
padding: AppUtils.kPaddingAll16,
children: [
Text(
AppLocalization.current.comment_desc,
style: context.text.authDesc,
),
AppUtils.kBoxHeight16,
RatingBarWidget(
rating: state.rating,
separator: AppUtils.kBoxWith12,
iconsSize: 24,
onRatingChanged: (rating) {
context.read<CommentBloc>().add(
RatingChangedEvent(rating: rating),
);
},
),
AppUtils.kBoxHeight24,
CustomTextFieldName(
name: AppLocalization.current.comment,
hint: AppLocalization.current.comment_text,
minLines: 4,
controller: commentController,
onchange: (comment) {
context.read<CommentBloc>().add(
CommentEnterEvent(comment: comment),
);
},
),
AppUtils.kBoxHeight16,
Padding(
padding: AppUtils.kPaddingHor34,
child: ElevatedButton(
onPressed:
state.rating > 0 && (state.comment?.isNotEmpty ?? false)
? () {
context.read<CommentBloc>().add(
SubmitEvent(
orderId: widget.argument?.orderId ?? "",
),
);
}
: null,
child: Text(AppLocalization.current.send),
),
),
],
),
),
);
},
);
}
@override
void dispose() {
disposeControllers();
super.dispose();
}
}

View File

@@ -0,0 +1,139 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:cargocalculaterapp/core/local_source/local_source.dart';
import 'package:cargocalculaterapp/core/utils/app_utils.dart';
import 'package:cargocalculaterapp/core/widgets/loading/custom_loading.dart';
import 'package:cargocalculaterapp/features/home/presentation/pages/widgets/banners_widget.dart';
import 'package:cargocalculaterapp/features/home/presentation/pages/widgets/empty_order_widget.dart';
import 'package:cargocalculaterapp/features/home/presentation/pages/widgets/order_list_shimmer.dart';
import 'package:cargocalculaterapp/features/home/presentation/pages/widgets/orders_list_widget.dart';
import 'package:cargocalculaterapp/features/home/presentation/pages/widgets/status_list_widget.dart';
import 'package:cargocalculaterapp/generated/l10n.dart';
import 'package:cargocalculaterapp/router/name_routes.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../../injector_container.dart';
import '../bloc/home_bloc.dart';
import '../mixin/home_mixin.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with HomeMixin {
@override
void initState() {
context.read<HomeBloc>().add(const GetAllOrdersListEvent());
context.read<HomeBloc>().add(const GetBannersEvent());
initControllers();
_scrollListener();
super.initState();
}
void _scrollListener() {
scrollController.addListener(() {
final state = context.read<HomeBloc>().state;
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200 &&
!state.paginationLoading &&
state.page <= (state.pageCount ?? 1)) {
context.read<HomeBloc>().add(const PaginationEvent());
}
});
}
@override
Widget build(BuildContext context) {
return BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
return Scaffold(
backgroundColor: context.color.grayBackground,
appBar: AppBar(
title: Row(
children: [
GestureDetector(
onTap: () {
Clipboard.setData(
ClipboardData(text: sl<LocalSource>().getUCode()),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalization.current.text_copied),
),
);
},
child: Text(
sl<LocalSource>().getUCode(),
style: context.text.secondaryText14,
),
),
const Spacer(),
Text(AppLocalization.current.orders),
const Spacer(),
],
),
automaticallyImplyLeading: false,
actions: [
IconButton(
onPressed: () {
Navigator.pushNamed(context, Routes.notification);
},
icon: SvgPicture.asset(
"assets/svg/ic_notification.svg",
colorFilter: ColorFilter.mode(
context.color.textColor,
BlendMode.srcIn,
),
),
),
AppUtils.kBoxWith12,
],
),
body: CustomScrollView(
controller: scrollController,
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
slivers: [
StatusListWidget(selectedStatus: state.selectedStatus),
CupertinoSliverRefreshControl(
onRefresh: () async {
context.read<HomeBloc>().add(const RefreshEvent());
await Future<void>.delayed(
const Duration(milliseconds: 1500),
);
},
),
if (state.banners?.isNotEmpty ?? false)
BannersWidget(banners: state.banners),
state.isLoading
? const OrderListShimmer()
: (state.ordersList?.data?.length ?? 0) > 0
? OrdersListWidget(data: state.ordersList?.data)
: const EmptyOrderWidget(),
if (state.paginationLoading)
const SliverPadding(
padding: AppUtils.kPaddingAll24,
sliver: SliverToBoxAdapter(
child: Center(child: CustomLoadingWidget()),
),
),
],
),
);
},
);
}
@override
void dispose() {
disposeControllers();
scrollController.removeListener(_scrollListener);
super.dispose();
}
}

View File

@@ -0,0 +1,46 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../../../../../constants/constants.dart';
import '../../mixin/image_page_mixin.dart';
import '../arguments/image_page_argument.dart';
class ImagePage extends StatefulWidget {
const ImagePage({super.key, required this.argument});
final ImagePageArgument? argument;
@override
State<ImagePage> createState() => _ImagePageState();
}
class _ImagePageState extends State<ImagePage> with ImagePageMixin {
@override
void initState() {
initController(widget.argument?.currentPage ?? 0);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: PageView.builder(
controller: pageController,
itemCount: widget.argument?.images?.length ?? 0,
itemBuilder: (_, index) => InteractiveViewer(
scaleEnabled: true,
panEnabled: true,
constrained: true,
maxScale: 14,
clipBehavior: Clip.antiAlias,
child: CachedNetworkImage(
imageUrl: "${Constants.baseUrl}${widget.argument?.images?[index]}",
height: double.infinity,
width: double.infinity,
),
),
),
);
}
}

View File

@@ -0,0 +1,120 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:cargocalculaterapp/core/functions/base_finctions.dart';
import 'package:cargocalculaterapp/core/utils/app_utils.dart';
import 'package:cargocalculaterapp/core/widgets/loading/progress_hud.dart';
import 'package:cargocalculaterapp/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../../../core/widgets/loading/custom_loading.dart';
import '../../bloc/notification/notification_bloc.dart';
import '../../mixin/notification_mixin.dart';
class NotificationPage extends StatefulWidget {
const NotificationPage({super.key});
@override
State<NotificationPage> createState() => _NotificationPageState();
}
class _NotificationPageState extends State<NotificationPage>
with NotificationMixin {
@override
void initState() {
context.read<NotificationBloc>().add(const GetNotificationsEvent());
initControllers();
super.initState();
_scrollListener();
}
void _scrollListener() {
scrollController.addListener(() {
final state = context.read<NotificationBloc>().state;
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200 &&
!state.paginationLoading &&
state.currentPage <= (state.pageCount ?? 1)) {
context.read<NotificationBloc>().add(
NotificationPaginationEvent(page: state.currentPage + 1),
);
}
});
}
@override
Widget build(BuildContext context) {
return BlocBuilder<NotificationBloc, NotificationState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalization.current.notification),
elevation: 0.2,
),
body: ModalProgressHUD(
inAsyncCall: state.paginationLoading,
child: CustomScrollView(
controller: scrollController,
slivers: [
SliverPadding(
padding: AppUtils.kPaddingAll16,
sliver: SliverList.separated(
itemBuilder: (context, index) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Ink(
width: 36,
height: 36,
decoration: BoxDecoration(
color: context.color.iconBackground,
borderRadius: AppUtils.kBorderRadius24,
),
padding: AppUtils.kPaddingAll8,
child: SvgPicture.asset(
"assets/svg/ic_check_box.svg",
),
),
AppUtils.kBoxWith8,
Expanded(
child: Text(
Functions.getTranslatedNameModel(
state.notifications?[index].text,
context,
),
style: context.text.authDesc,
),
),
],
),
separatorBuilder: (_, _) => Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Divider(
thickness: 1,
height: 1,
color: context.color.lightBorder,
),
),
itemCount: state.notifications?.length ?? 0,
),
),
if (state.paginationLoading)
const SliverPadding(
padding: AppUtils.kPaddingAll24,
sliver: SliverToBoxAdapter(
child: Center(child: CustomLoadingWidget()),
),
),
],
),
),
);
},
);
}
@override
void dispose() {
disposeControllers();
super.dispose();
}
}

View File

@@ -0,0 +1,390 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:cargocalculaterapp/core/utils/app_utils.dart';
import 'package:cargocalculaterapp/core/widgets/loading/custom_loading.dart';
import 'package:cargocalculaterapp/features/home/presentation/pages/arguments/image_page_argument.dart';
import 'package:cargocalculaterapp/features/home/presentation/pages/order_info/widgets/delivery_info_widget.dart';
import 'package:cargocalculaterapp/features/home/presentation/pages/order_info/widgets/order_info_widget.dart';
import 'package:cargocalculaterapp/features/home/presentation/pages/order_info/widgets/status_item_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import '../../../../../constants/constants.dart';
import '../../../../../core/functions/base_finctions.dart';
import '../../../../../generated/l10n.dart';
import '../../../../../router/name_routes.dart';
import '../../bloc/order_single/order_single_bloc.dart';
import '../arguments/comment_argument.dart';
import '../arguments/order_info_argument.dart';
class OrderInfoPage extends StatefulWidget {
const OrderInfoPage({super.key, required this.argument});
final OrderInfoArgument? argument;
@override
State<OrderInfoPage> createState() => _OrderInfoPageState();
}
class _OrderInfoPageState extends State<OrderInfoPage> {
@override
void initState() {
context.read<OrderSingleBloc>().add(
GetOrderSingleEvent(orderId: widget.argument?.orderId ?? ""),
);
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<OrderSingleBloc, OrderSingleState>(
builder: (context, state) {
final status = _checkStatus(
state.orderSingleResponse?.status?.code ?? "",
);
return Scaffold(
backgroundColor: context.color.grayBackground,
appBar: AppBar(title: Text("#${widget.argument?.orderNumber ?? ""}")),
body: state.isLoading
? const Center(child: CustomLoadingWidget())
: ListView(
padding: AppUtils.kPaddingVer24Hor16,
children: [
Container(
padding: AppUtils.kPaddingAll16,
decoration: BoxDecoration(
borderRadius: AppUtils.kBorderRadius16,
color: context.color.statusBackground,
border: Border.all(color: context.color.lightBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppLocalization.current.order_delivery_info,
style: context.text.orderTitle,
),
AppUtils.kBoxHeight16,
DeliveryInfoWidget(
title: AppLocalization.current.order_number,
name: state.orderSingleResponse?.orderNumber ?? "",
icon: "assets/svg/ic_box_gray.svg",
),
DeliveryInfoWidget(
title: AppLocalization.current.warehouse,
name:
Localizations.localeOf(context).languageCode ==
"uz"
? (state.orderSingleResponse?.wharehouseUz ??
"")
: Localizations.localeOf(
context,
).languageCode ==
"ru"
? (state.orderSingleResponse?.wharehouseRu ??
"")
: (state.orderSingleResponse?.wharehouseZh ??
""),
icon: "assets/svg/ic_building.svg",
),
DeliveryInfoWidget(
title: AppLocalization.current.delivery_price,
name:
state.orderSingleResponse?.deliveryPrice ?? "",
icon: "assets/svg/ic_chek.svg",
),
DeliveryInfoWidget(
title:
AppLocalization.current.delivery_time_estimated,
name: DateFormat('dd-MM-yyyy').format(
DateTime.tryParse(
state.orderSingleResponse?.arrivalDate ??
"",
) ??
DateTime.now(),
),
icon: "assets/svg/ic_calendar.svg",
),
Divider(
height: 1,
thickness: 1,
color: Colors.black.withAlpha(20),
),
AppUtils.kBoxHeight16,
StatusItemWidget(
isCompleted: status >= 1,
dividerCompleted: status > 1,
title: Functions.getTranslatedItem(
AppConst.statusList[AppConst.waiting],
context,
),
number: "1",
),
StatusItemWidget(
isCompleted: status >= 2,
dividerCompleted: status > 2,
title: Functions.getTranslatedItem(
AppConst.statusList[AppConst.partReceived],
context,
),
number: "2",
),
StatusItemWidget(
isCompleted: status >= 3,
dividerCompleted: status > 3,
title: Functions.getTranslatedItem(
AppConst.statusList[AppConst.fullReceived],
context,
),
number: "3",
),
StatusItemWidget(
isCompleted: status >= 4,
dividerCompleted: status > 4,
title: Functions.getTranslatedItem(
AppConst.statusList[AppConst.shipped],
context,
),
number: "4",
),
StatusItemWidget(
dividerCompleted: status > 5,
isCompleted: status >= 5,
title: Functions.getTranslatedItem(
AppConst.statusList[AppConst.leftChine],
context,
),
number: "5",
),
StatusItemWidget(
isCompleted: status >= 6,
dividerCompleted: status > 6,
title: Functions.getTranslatedItem(
AppConst.statusList[AppConst.enteredUzbekistan],
context,
),
number: "6",
),
StatusItemWidget(
isCompleted: status >= 7,
dividerCompleted: status > 7,
title: Functions.getTranslatedItem(
AppConst.statusList[AppConst.tashkentWarehouse],
context,
),
number: "7",
),
StatusItemWidget(
isCompleted: status >= 8,
dividerCompleted: status > 8,
title: Functions.getTranslatedItem(
AppConst.statusList[AppConst.customsClearance],
context,
),
number: "8",
),
StatusItemWidget(
isCompleted: status >= 9,
dividerCompleted: status > 9,
title: Functions.getTranslatedItem(
AppConst.statusList[AppConst.delivered],
context,
),
showConnector: false,
number: "9",
),
],
),
),
Container(
margin: const EdgeInsets.symmetric(vertical: 16),
padding: AppUtils.kPaddingAll16,
decoration: BoxDecoration(
borderRadius: AppUtils.kBorderRadius16,
color: context.color.statusBackground,
border: Border.all(color: context.color.lightBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppLocalization.current.order_details,
style: context.text.orderTitle,
),
AppUtils.kBoxHeight12,
OrderInfoWidget(
title: AppLocalization.current.order_name,
name:
Localizations.localeOf(context).languageCode ==
"uz"
? (state.orderSingleResponse?.nameUz ?? "")
: Localizations.localeOf(
context,
).languageCode ==
"ru"
? (state.orderSingleResponse?.nameRu ?? "")
: (state.orderSingleResponse?.nameZh ?? ""),
),
OrderInfoWidget(
title: AppLocalization.current.weight,
name: state.orderSingleResponse?.weight ?? "",
),
OrderInfoWidget(
title: AppLocalization.current.size,
name: state.orderSingleResponse?.m3 ?? "",
),
OrderInfoWidget(
title: AppLocalization.current.average_weight,
name:
state.orderSingleResponse?.averageWeightKg ??
"",
),
AppUtils.kBoxHeight8,
if ((state.orderSingleResponse?.images?.length ?? 0) >
0)
Text(
AppLocalization.current.order_photo,
style: context.text.orderTitle,
),
if ((state.orderSingleResponse?.images?.length ?? 0) >
0)
AppUtils.kBoxHeight8,
if ((state.orderSingleResponse?.images?.length ?? 0) >
0)
SizedBox(
height: 72,
child: ListView.separated(
padding: EdgeInsets.zero,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => GestureDetector(
onTap: () {
Navigator.pushNamed(
context,
Routes.image,
arguments: ImagePageArgument(
currentPage: index,
images:
state.orderSingleResponse?.images,
),
);
},
child: ClipRRect(
borderRadius: AppUtils.kBorderRadius16,
child: CachedNetworkImage(
imageUrl:
"${Constants.baseUrl}${state.orderSingleResponse?.images?[index]}",
width: 72,
height: 72,
fit: BoxFit.cover,
errorWidget: (context, _, _) {
return const SizedBox(
width: 72,
height: 72,
child: Center(
child: Icon(Icons.warning),
),
);
},
),
),
),
separatorBuilder: (_, _) => AppUtils.kBoxWidth8,
itemCount:
state.orderSingleResponse?.images?.length ??
0,
),
),
],
),
),
if (status == 9)
Padding(
padding: AppUtils.kPaddingHor34,
child: InkWell(
onTap: () {
Navigator.pushNamed(
context,
Routes.comment,
arguments: CommentArgument(
orderId: widget.argument?.orderNumber ?? "",
),
);
},
borderRadius: AppUtils.kBorderRadius24,
child: Ink(
decoration: BoxDecoration(
color: context.color.scaffoldBackgroundColor,
borderRadius: AppUtils.kBorderRadius24,
border: Border.all(
color: context.color.primaryColor,
),
),
height: 56,
padding: AppUtils.kPaddingHor16,
child: Center(
child: Text(
AppLocalization.current.left_comment,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: context.color.textColor,
),
),
),
),
),
),
],
),
);
},
);
}
int _checkStatus(String status) {
switch (status) {
case AppConst.waiting:
{
return 1;
}
case AppConst.partReceived:
{
return 2;
}
case AppConst.fullReceived:
{
return 3;
}
case AppConst.shipped:
{
return 4;
}
case AppConst.leftChine:
{
return 5;
}
case AppConst.enteredUzbekistan:
{
return 6;
}
case AppConst.tashkentWarehouse:
{
return 7;
}
case AppConst.customsClearance:
{
return 8;
}
case AppConst.delivered:
{
return 9;
}
default:
{
return 1;
}
}
}
}

View File

@@ -0,0 +1,32 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:cargocalculaterapp/core/utils/app_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class DeliveryInfoWidget extends StatelessWidget {
const DeliveryInfoWidget({
super.key,
required this.icon,
required this.title,
required this.name,
});
final String icon;
final String title;
final String name;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
SvgPicture.asset(icon, width: 12, height: 12),
AppUtils.kBoxWidth4,
Expanded(child: Text(title, style: context.text.orderListTitle)),
Text(name, style: context.text.profileCategory),
],
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More