diff --git a/api/lib/cecr_unwomen_web/controllers/contribution_controller.ex b/api/lib/cecr_unwomen_web/controllers/contribution_controller.ex index 09f7b5a..bbbfaa6 100644 --- a/api/lib/cecr_unwomen_web/controllers/contribution_controller.ex +++ b/api/lib/cecr_unwomen_web/controllers/contribution_controller.ex @@ -307,7 +307,8 @@ defmodule CecrUnwomenWeb.ContributionController do end today = Helper.get_vietnam_date_today() - {start_month, end_month} = {Date.beginning_of_month(today), Date.end_of_month(today)} + # {start_month, end_month} = {Date.beginning_of_month(today), Date.end_of_month(today)} + {start_month, end_month} = {Date.new!(2024, 11, 01), Date.end_of_month(today)} overall_data_one_month = get_overall_contribution_one_month(user_id, role_id, start_month, end_month) sum_factors = if role_id == 2 do @@ -346,7 +347,8 @@ defmodule CecrUnwomenWeb.ContributionController do scraper_overall_data = Helper.aggregate_with_fields(OverallScraperContribution, keys) |> Map.put(:count_scraper, count_scraper_user) today = Helper.get_vietnam_date_today() - {start_month, end_month} = {Date.beginning_of_month(today), Date.end_of_month(today)} + # {start_month, end_month} = {Date.beginning_of_month(today), Date.end_of_month(today)} + {start_month, end_month} = {Date.new!(2024, 11, 01), Date.end_of_month(today)} {overall_scrapers_one_month, overall_households_one_month} = get_user_contributions_by_range(start_month, end_month) {scraper_total_kgco2e_seven_days, household_total_kgco2e_seven_days} = get_total_kgco2e_seven_days() diff --git a/api/lib/cecr_unwomen_web/controllers/user_controller.ex b/api/lib/cecr_unwomen_web/controllers/user_controller.ex index 5b70b90..f7e9ca8 100644 --- a/api/lib/cecr_unwomen_web/controllers/user_controller.ex +++ b/api/lib/cecr_unwomen_web/controllers/user_controller.ex @@ -23,7 +23,7 @@ defmodule CecrUnwomenWeb.UserController do is_ready_to_insert = !is_nil(first_name) && !is_nil(last_name) && is_pass_password response = cond do - !is_pass_phone_number -> Helper.response_json_message(false, "Số điện thoại không đúng hoặc đã tồn tại!", 279) + !is_pass_phone_number -> Helper.response_json_message(false, "Số điện thoại đã tồn tại hoặc đã bị xoá khỏi hệ thống", 279) !is_ready_to_insert -> Helper.response_json_message(false, "Bạn nhập thiếu các thông tin cần thiết! Vui lòng kiểm tra lại!", 301) !is_pass_admin_role -> Helper.response_json_message(false, "Bạn không thể đăng ký làm admin!", 301) true -> @@ -89,7 +89,7 @@ defmodule CecrUnwomenWeb.UserController do !is_pass_phone_number -> Helper.response_json_message(false, "Không tìm thấy số điện thoại!", 280) !is_pass_password_length -> Helper.response_json_message(false, "Sai số điện thoại hoặc mật khẩu", 301) true -> - from(u in User, where: u.phone_number == ^phone_number, select: u) + from(u in User, where: u.phone_number == ^phone_number and u.is_removed != true, select: u) |> Repo.one |> case do nil -> Helper.response_json_message(false, "Không tìm thấy tài khoản!", 302) @@ -130,6 +130,22 @@ defmodule CecrUnwomenWeb.UserController do json conn, res end + def delete_user(conn, _params) do + user_id = conn.assigns.user.user_id + res = Repo.get_by(User, %{id: user_id, is_removed: false}) + |> case do + nil -> Helper.response_json_message(false, "Không tìm thấy người dùng!", 300) + user -> + Ecto.Changeset.change(user, %{is_removed: true, refresh_token: nil}) + |> Repo.update + |> case do + {:ok, _} -> Helper.response_json_message(true, "Xóa người dùng thành công!") + _ -> Helper.response_json_message(false, "Có lỗi xảy ra!", 303) + end + end + json conn, res + end + def logout(conn, params) do user_id = params["user_id"] diff --git a/api/lib/cecr_unwomen_web/models/user.ex b/api/lib/cecr_unwomen_web/models/user.ex index abd4aa7..acedfd1 100644 --- a/api/lib/cecr_unwomen_web/models/user.ex +++ b/api/lib/cecr_unwomen_web/models/user.ex @@ -16,13 +16,14 @@ defmodule CecrUnwomenWeb.Models.User do field :location, :string field :refresh_token, :string field :verified, :boolean + field :is_removed, :boolean, default: false belongs_to :role, Role, foreign_key: :role_id, type: :integer timestamps() end @required_fields [:id, :first_name, :last_name, :phone_number, :password_hash, :role_id, :refresh_token] - @optional_fields [:avatar_url, :email, :gender, :date_of_birth, :location, :verified] + @optional_fields [:avatar_url, :email, :gender, :date_of_birth, :location, :verified, :is_removed] def changeset(user, params \\ %{}) do user diff --git a/api/lib/cecr_unwomen_web/router.ex b/api/lib/cecr_unwomen_web/router.ex index 7c3f127..52674a8 100644 --- a/api/lib/cecr_unwomen_web/router.ex +++ b/api/lib/cecr_unwomen_web/router.ex @@ -23,6 +23,7 @@ defmodule CecrUnwomenWeb.Router do pipe_through :token post "/logout", UserController, :logout + post "/delete_user", UserController, :delete_user post "/get_info", UserController, :get_info post "/update_info", UserController, :update_info post "/change_password", UserController, :change_password diff --git a/api/priv/repo/migrations/20250102035453_add_field_is_removed_user_table.exs b/api/priv/repo/migrations/20250102035453_add_field_is_removed_user_table.exs new file mode 100644 index 0000000..b4fa802 --- /dev/null +++ b/api/priv/repo/migrations/20250102035453_add_field_is_removed_user_table.exs @@ -0,0 +1,9 @@ +defmodule CecrUnwomen.Repo.Migrations.AddFieldIsRemovedUserTable do + use Ecto.Migration + + def change do + alter table(:user) do + add :is_removed, :boolean, default: false + end + end +end diff --git a/mobile/.gitignore b/mobile/.gitignore index 0d5bd2c..172f766 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -44,6 +44,7 @@ app.*.map.json pubspec.lock */Podfile.lock +/android/key.properties # FVM Version Cache .fvm/ \ No newline at end of file diff --git a/mobile/lib/features/authentication/bloc/authentication_bloc.dart b/mobile/lib/features/authentication/bloc/authentication_bloc.dart index 7d11e51..a3305c1 100644 --- a/mobile/lib/features/authentication/bloc/authentication_bloc.dart +++ b/mobile/lib/features/authentication/bloc/authentication_bloc.dart @@ -11,6 +11,7 @@ class AuthenticationBloc extends Bloc on(_onLogoutRequest); on(_onAutoLogin); on(_onUpdateInfo); + on(_onDeleteAccount); } Future _onAuthSubscription(AuthSubscription event, Emitter emit) async { @@ -61,6 +62,10 @@ class AuthenticationBloc extends Bloc } } + void _onDeleteAccount(DeleteAccount event, Emitter emit) { + AuthRepository.deleteAccount(); + } + Future _onUpdateInfo(UpdateInfo event, Emitter emit) async { try { emit(state.copyWith(status: AuthenticationStatus.authorized, user: event.user)); diff --git a/mobile/lib/features/authentication/bloc/authentication_event.dart b/mobile/lib/features/authentication/bloc/authentication_event.dart index b82cc93..6e144ef 100644 --- a/mobile/lib/features/authentication/bloc/authentication_event.dart +++ b/mobile/lib/features/authentication/bloc/authentication_event.dart @@ -10,6 +10,8 @@ class AuthSubscription extends AuthenticationEvent {} class LogoutRequest extends AuthenticationEvent {} +class DeleteAccount extends AuthenticationEvent {} + class AutoLogin extends AuthenticationEvent {} class UpdateInfo extends AuthenticationEvent { diff --git a/mobile/lib/features/authentication/repository/authentication_api.dart b/mobile/lib/features/authentication/repository/authentication_api.dart index f92ae22..73a50eb 100644 --- a/mobile/lib/features/authentication/repository/authentication_api.dart +++ b/mobile/lib/features/authentication/repository/authentication_api.dart @@ -25,4 +25,10 @@ class AuthenticationApi { const String url = "/user/logout"; await dioConfigInterceptor.post(url, data: {"user_id": userId}); } + + static Future deleteUser() async { + const String url = "/user/delete_user"; + final Response response = await dioConfigInterceptor.post(url); + return response.data; + } } \ No newline at end of file diff --git a/mobile/lib/features/authentication/repository/authentication_repository.dart b/mobile/lib/features/authentication/repository/authentication_repository.dart index c5e5c43..dd98859 100644 --- a/mobile/lib/features/authentication/repository/authentication_repository.dart +++ b/mobile/lib/features/authentication/repository/authentication_repository.dart @@ -54,6 +54,23 @@ class AuthRepository { } } + static Future deleteAccount() async { + try { + final Map res = await AuthenticationApi.deleteUser(); + final bool isSuccess = res["success"]; + if (isSuccess) { + await AuthRepository.logoutNoCredentials(); + _controller.add(AuthenticationStatus.unauthorized); + } else { + _controller.add(AuthenticationStatus.authorized); + // _controller.add(AuthenticationStatus.authorized); + } + } catch (e) { + print('gndkjf:$e'); + _controller.add(AuthenticationStatus.error); + } + } + static Future logout() async { await AuthRepository.logoutNoCredentials(); // await AuthenticationApi.logout(userId); diff --git a/mobile/lib/features/user/view/user_info.dart b/mobile/lib/features/user/view/user_info.dart index 8d78b2d..1a9384b 100644 --- a/mobile/lib/features/user/view/user_info.dart +++ b/mobile/lib/features/user/view/user_info.dart @@ -140,6 +140,15 @@ class _UserInfoState extends State { context.read().add(LogoutRequest()); }, ), + NavigationButton( + text: "Xóa tài khoản", + isWarning: true, + icon: PhosphorIcons.regular.userMinus, + onTap: () { + context.read().add(DeleteAccount()); + // context.read().add(LogoutRequest()); + }, + ), ], ), ), diff --git a/mobile/lib/widgets/confirm_card.dart b/mobile/lib/widgets/confirm_card.dart new file mode 100644 index 0000000..a40f46f --- /dev/null +++ b/mobile/lib/widgets/confirm_card.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; + +class ConfirmCard extends StatelessWidget { + const ConfirmCard({ + super.key, + this.user, + this.bigIcon, + required this.title, + required this.subtitle, + required this.dangerousTextInButton, + this.onClick, + }); + + final Map? user; + final Widget? bigIcon; + final String title; + final String subtitle; + final String dangerousTextInButton; + final VoidCallback? onClick; + + // use with showDialog, showCupertinoDialog + + @override + Widget build(BuildContext context) { + const Color backgroundColorHeader = Colors.white; + const Color colorBackButton = Color(0xFF1D2939); + + return Material( + color : Colors.transparent, + child: Dialog( + child: Wrap( + children: [ + Container( + // height: 270, + width: 327, + decoration: BoxDecoration( + color: backgroundColorHeader, + borderRadius: BorderRadius.circular(8) + ), + padding: const EdgeInsets.only(left: 20, right: 20, top: 16, bottom: 10), + child: Column(children: [ + // else if (bigIcon != null) bigIcon! + // else Container(), + + const SizedBox(height: 14), + + Text(title, + style: const TextStyle(fontSize: 16, + fontWeight: FontWeight.w700, + )), + const SizedBox(height: 12), + Text(subtitle, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Color(0xFF667085) + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + + SizedBox( + height: 44, + width: 300, + child: ElevatedButton( + onPressed: () { + if (onClick != null) { + onClick!.call(); + } + }, + style: ButtonStyle( + backgroundColor: const WidgetStatePropertyAll(Color(0xFFFF3048)), + shape: WidgetStatePropertyAll( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)) + ), + padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 10)) + ), + child: Text(dangerousTextInButton, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ) + ) + ) + ), + const SizedBox(height: 14), + + SizedBox( + height: 44, + width: 300, + child: TextButton( + onPressed: () { + Navigator.pop(context); + }, + style: ButtonStyle( + shape: WidgetStatePropertyAll( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)) + ), + padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 10)) + ), + child: const Text("Hủy", + style: TextStyle( + color: colorBackButton, + fontSize: 16, + fontWeight: FontWeight.w600, + ) + ), + ), + ), + ]), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/widgets/navigation_button.dart b/mobile/lib/widgets/navigation_button.dart index c415902..77749d9 100644 --- a/mobile/lib/widgets/navigation_button.dart +++ b/mobile/lib/widgets/navigation_button.dart @@ -1,3 +1,4 @@ +import 'package:cecr_unwomen/widgets/confirm_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_switch/flutter_switch.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; @@ -61,38 +62,56 @@ class NavigationButton extends StatelessWidget { margin: const EdgeInsets.only(bottom: 12), child: InkWell( borderRadius: BorderRadius.circular(12), - onTap: () => onTap == null ? null : onTap!(), + onTap: () async { + if (isWarning) { + final bool? isConfirm = await showDialog( + context: context, + builder: (context) => ConfirmCard( + title: "Xác nhận ${text.toLowerCase()}", + subtitle: "Bạn có chắc chắn muốn thực hiện hành động này?", + dangerousTextInButton: "Xác nhận", + onClick: () { + Navigator.pop(context, true); + }, + ), + ); + if (isConfirm == null) return; + onTap?.call(); + } else { + onTap?.call(); + } + }, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - crossAxisAlignment: subTitleWidget != null ? CrossAxisAlignment.start : CrossAxisAlignment.center, - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: iconBgColor, - shape: BoxShape.circle, - ), - child: Icon(icon, size: 20, color: iconColor), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: subTitleWidget != null ? CrossAxisAlignment.start : CrossAxisAlignment.center, + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: iconBgColor, + shape: BoxShape.circle, ), - const SizedBox(width: 14), - Flexible( - child: subTitleWidget != null - ? Column( - children: [ - option, - subTitleWidget ?? Container(), - ], - ) - : option, - ), - ], - )), + child: Icon(icon, size: 20, color: iconColor), + ), + const SizedBox(width: 14), + Flexible( + child: subTitleWidget != null + ? Column( + children: [ + option, + subTitleWidget ?? Container(), + ], + ) + : option, + ), + ], + )), ), ), ); diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index b2b3a75..5fd9bae 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -3,7 +3,7 @@ description: "WazNet (Waste zero, Net zero) is a mobile app that helps users tra # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.0+1 +version: 1.0.0+16 environment: sdk: ^3.5.3