first commit

This commit is contained in:
2026-04-29 12:53:22 +07:00
commit e6a30eddd3
394 changed files with 16408 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
import 'package:dio/dio.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:frontend_eccp_mobile/app/core/di/injection.dart';
import 'package:frontend_eccp_mobile/app/core/services/crashlytics_service.dart';
import 'package:frontend_eccp_mobile/app/core/utils/session_manager.dart';
import 'package:frontend_eccp_mobile/modules/auth/login/bloc/login_state.dart';
import 'package:frontend_eccp_mobile/modules/auth/login/data/model/login_request.dart';
import 'package:frontend_eccp_mobile/modules/auth/login/data/model/user_session.dart';
import 'package:frontend_eccp_mobile/modules/auth/login/data/repositories/login_repository.dart';
import 'package:injectable/injectable.dart';
@injectable
class LoginCubit extends Cubit<LoginState> {
LoginCubit(this._repository) : super(LoginInitial());
final LoginRepository _repository;
Future<void> login({
required String nrp,
required String password,
}) async {
emit(LoginLoading());
try {
final response = await _repository.login(
LoginRequest(
nrp: nrp,
password: password,
),
);
if (response.status != 'success') {
emit(LoginFailure(response.message));
return;
}
await CrashlyticsService.setUser(
id: response.data.user.nrp,
nrp: response.data.user.nrp,
name: response.data.user.name,
);
await SessionManager.saveToken(response.data.accessToken);
await SessionManager.saveRefreshToken(response.data.refreshToken);
final userMap = {
'name': response.data.user.name,
'nrp': response.data.user.nrp,
};
await SessionManager.saveUser(userMap);
final _ = getIt<UserSession>()
..token = response.data.accessToken
..user = userMap;
emit(LoginSuccess());
} on DioException catch (e) {
final handled = e.requestOptions.extra['handledGlobally'] == true;
if (handled) return;
emit(LoginFailure(e.error?.toString()));
} catch (e, s) {
await CrashlyticsService.recordError(
e,
s,
fatal: true,
reason: 'Login Unknown Error',
);
emit(const LoginFailure('Terjadi kesalahan saat login'));
}
}
}

View File

@@ -0,0 +1,32 @@
import 'package:equatable/equatable.dart';
import 'package:frontend_eccp_mobile/app/core/constants/enum/snackbar_type_enum.dart';
abstract class LoginState extends Equatable {
const LoginState();
@override
List<Object?> get props => [];
}
class LoginInitial extends LoginState {}
class LoginLoading extends LoginState {}
class LoginSuccess extends LoginState {}
class LoginFailure extends LoginState {
const LoginFailure([
this.message,
this.snackBarType = SnackBarType.error,
this.duration = 2000,
]);
final String? message;
final SnackBarType snackBarType;
final int duration;
bool get hasMessage => message != null && message!.isNotEmpty;
@override
List<Object?> get props => [message, snackBarType, duration];
}

View File

@@ -0,0 +1,29 @@
import 'package:dio/dio.dart';
import 'package:frontend_eccp_mobile/app/core/constants/dio_constant.dart';
import 'package:frontend_eccp_mobile/app/core/network/api_endpoint.dart';
import 'package:frontend_eccp_mobile/app/core/network/dio_client.dart';
import 'package:frontend_eccp_mobile/modules/auth/login/data/model/login_request.dart';
import 'package:frontend_eccp_mobile/modules/auth/login/data/model/login_response.dart';
import 'package:injectable/injectable.dart';
@lazySingleton
class LoginRemoteDataSource {
final Dio _dio = DioClient.getInstance();
Future<LoginResponse> login(LoginRequest request) async {
final response = await _dio.post<Map<String, dynamic>>(
ApiEndpoint.login,
data: request.toJson(),
options: Options(
extra: {DioExtraKey.noAuth: true},
),
);
final data = response.data;
if (data == null) {
throw Exception('EMPTY_RESPONSE');
}
return LoginResponse.fromJson(data);
}
}

View File

@@ -0,0 +1,29 @@
import 'package:frontend_eccp_mobile/modules/auth/login/data/model/login_user.dart';
class LoginData {
LoginData({
required this.accessToken,
required this.refreshToken,
required this.tokenType,
required this.expiresIn,
required this.user,
});
factory LoginData.fromJson(Map<String, dynamic> json) {
return LoginData(
accessToken: json['access_token']?.toString() ?? '',
refreshToken: json['refresh_token']?.toString() ?? '',
tokenType: json['token_type']?.toString() ?? '',
expiresIn: int.tryParse(json['expires_in']?.toString() ?? '0') ?? 0,
user: LoginUser.fromJson(
json['user'] as Map<String, dynamic>? ?? {},
),
);
}
final String accessToken;
final String refreshToken;
final String tokenType;
final int expiresIn;
final LoginUser user;
}

View File

@@ -0,0 +1,16 @@
class LoginRequest {
LoginRequest({
required this.nrp,
required this.password,
});
final String nrp;
final String password;
Map<String, dynamic> toJson() {
return {
'nrp': nrp,
'password': password,
};
}
}

View File

@@ -0,0 +1,23 @@
import 'package:frontend_eccp_mobile/modules/auth/login/data/model/login_data.dart';
class LoginResponse {
LoginResponse({
required this.status,
required this.message,
required this.data,
});
factory LoginResponse.fromJson(Map<String, dynamic> json) {
return LoginResponse(
status: json['status']?.toString() ?? '',
message: json['message']?.toString() ?? '',
data: LoginData.fromJson(
json['data'] as Map<String, dynamic>? ?? {},
),
);
}
final String status;
final String message;
final LoginData data;
}

View File

@@ -0,0 +1,16 @@
class LoginUser {
LoginUser({
required this.name,
required this.nrp,
});
factory LoginUser.fromJson(Map<String, dynamic> json) {
return LoginUser(
name: json['name']?.toString() ?? '',
nrp: json['nrp']?.toString() ?? '',
);
}
final String name;
final String nrp;
}

View File

@@ -0,0 +1,14 @@
class UserSession {
String? token;
Map<String, dynamic>? user;
bool get isLoggedIn => token != null && token!.isNotEmpty;
String get name => user?['name']?.toString() ?? '';
String get nrp => user?['nrp']?.toString() ?? '';
void clear() {
token = null;
user = null;
}
}

View File

@@ -0,0 +1,15 @@
import 'package:frontend_eccp_mobile/modules/auth/login/data/datasources/login_remote_datasource.dart';
import 'package:frontend_eccp_mobile/modules/auth/login/data/model/login_request.dart';
import 'package:frontend_eccp_mobile/modules/auth/login/data/model/login_response.dart';
import 'package:injectable/injectable.dart';
@lazySingleton
class LoginRepository {
LoginRepository(this._remoteDatasource);
final LoginRemoteDataSource _remoteDatasource;
Future<LoginResponse> login(LoginRequest request) async {
return _remoteDatasource.login(request);
}
}

View File

@@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:frontend_eccp_mobile/app/core/constants/constants.dart';
import 'package:frontend_eccp_mobile/app/core/constants/enum/snackbar_type_enum.dart';
import 'package:frontend_eccp_mobile/app/core/di/injection.dart';
import 'package:frontend_eccp_mobile/app/core/navigation/app_navigator.dart';
import 'package:frontend_eccp_mobile/app/core/navigation/app_routes.dart';
import 'package:frontend_eccp_mobile/app/core/services/crashlytics_service.dart';
import 'package:frontend_eccp_mobile/app/core/utils/app_validators.dart';
import 'package:frontend_eccp_mobile/app/core/utils/show_message.dart';
import 'package:frontend_eccp_mobile/app/core/widgets/app_button.dart';
import 'package:frontend_eccp_mobile/app/core/widgets/app_shad_textfield.dart';
import 'package:frontend_eccp_mobile/modules/auth/login/bloc/login_cubit.dart';
import 'package:frontend_eccp_mobile/modules/auth/login/bloc/login_state.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => getIt<LoginCubit>(),
child: const LoginUI(),
);
}
}
class LoginUI extends StatefulWidget {
const LoginUI({super.key});
@override
State<LoginUI> createState() => _LoginUIState();
}
class _LoginUIState extends State<LoginUI> {
final formKey = GlobalKey<ShadFormState>();
@override
void dispose() {
super.dispose();
}
Future<void> _onLogin() async {
if (!formKey.currentState!.saveAndValidate()) return;
await CrashlyticsService.log('Login attempt');
if (!mounted) return;
await context.read<LoginCubit>().login(
nrp: formKey.currentState!.value['nrp']?.toString() ?? '',
password: formKey.currentState!.value['password']?.toString() ?? '',
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.neutralWhite,
body: BlocConsumer<LoginCubit, LoginState>(
listener: (context, state) async {
await AppNavigator.clearAndPush(
context,
AppRoutes.home,
onOpened: () {
showMessage(
context,
'Selamat, anda berhasil masuk',
SnackBarType.success,
);
},
);
// if (state is LoginSuccess) {
// await AppNavigator.clearAndPush(
// context,
// AppRoutes.home,
// onOpened: () {
// showMessage(
// context,
// 'Selamat, anda berhasil masuk',
// SnackBarType.success,
// );
// },
// );
// } else if (state is LoginFailure && state.hasMessage) {
// showMessage(
// context,
// state.message ?? '',
// state.snackBarType,
// duration: state.duration,
// );
// }
},
builder: (context, state) {
return SafeArea(
child: ShadForm(
key: formKey,
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppSpacing.h48,
Padding(
padding: AppPadding.h16,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'E-CPP DIVPROPAM POLRI',
style: AppTextStyles.h1,
textAlign: TextAlign.start,
),
Text(
'Sign-in to continue',
style: AppTextStyles.h2Light,
textAlign: TextAlign.start,
),
],
),
),
Padding(
padding: AppPadding.h12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppSpacing.h36,
AppShadInput(
title: 'NRP',
hint: 'Enter your NRP',
isForm: true,
id: 'nrp',
validator: (v) => AppValidators.required(
v,
message: 'NRP tidak boleh kosong',
),
),
AppSpacing.h16,
const AppShadInput(
title: 'Password',
hint: 'Enter your password',
isForm: true,
isPassword: true,
id: 'password',
validator: AppValidators.password,
),
AppSpacing.h32,
AppButton(
label: 'Login',
isExpanded: true,
onPressed: _onLogin,
isLoading: state is LoginLoading,
),
],
),
),
const Spacer(),
Align(
alignment: Alignment.bottomCenter,
child: Text(
'© 2026 E-CPP MABES POLRI. All rights reserved.',
style: AppTextStyles.body.copyWith(
color: AppColors.neutral600,
),
),
),
AppSpacing.h40,
],
),
),
),
),
),
);
},
),
);
}
}