| @@ -62,7 +62,7 @@ | |||
| android:value="2" /> | |||
| <provider | |||
| android:name="androidx.core.content.FileProvider" | |||
| android:authorities="au.com.homecarenet.caregiver.fileprovider" | |||
| android:authorities="vn.azteam.farmdemo.fileprovider" | |||
| android:exported="false" | |||
| android:grantUriPermissions="true"> | |||
| <meta-data | |||
| @@ -369,7 +369,7 @@ | |||
| CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; | |||
| CODE_SIGN_IDENTITY = "Apple Development"; | |||
| CODE_SIGN_STYLE = Automatic; | |||
| CURRENT_PROJECT_VERSION = 21; | |||
| CURRENT_PROJECT_VERSION = 22; | |||
| DEVELOPMENT_TEAM = C3DTD2JH94; | |||
| ENABLE_BITCODE = NO; | |||
| FRAMEWORK_SEARCH_PATHS = ( | |||
| @@ -385,7 +385,7 @@ | |||
| "$(inherited)", | |||
| "$(PROJECT_DIR)/Flutter", | |||
| ); | |||
| MARKETING_VERSION = 1.1.5; | |||
| MARKETING_VERSION = 1.1.6; | |||
| PRODUCT_BUNDLE_IDENTIFIER = vn.azteam.farmdemo; | |||
| PRODUCT_NAME = "$(TARGET_NAME)"; | |||
| PROVISIONING_PROFILE_SPECIFIER = ""; | |||
| @@ -511,7 +511,7 @@ | |||
| CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; | |||
| CODE_SIGN_IDENTITY = "Apple Development"; | |||
| CODE_SIGN_STYLE = Automatic; | |||
| CURRENT_PROJECT_VERSION = 21; | |||
| CURRENT_PROJECT_VERSION = 22; | |||
| DEVELOPMENT_TEAM = C3DTD2JH94; | |||
| ENABLE_BITCODE = NO; | |||
| FRAMEWORK_SEARCH_PATHS = ( | |||
| @@ -527,7 +527,7 @@ | |||
| "$(inherited)", | |||
| "$(PROJECT_DIR)/Flutter", | |||
| ); | |||
| MARKETING_VERSION = 1.1.5; | |||
| MARKETING_VERSION = 1.1.6; | |||
| PRODUCT_BUNDLE_IDENTIFIER = vn.azteam.farmdemo; | |||
| PRODUCT_NAME = "$(TARGET_NAME)"; | |||
| PROVISIONING_PROFILE_SPECIFIER = ""; | |||
| @@ -547,7 +547,7 @@ | |||
| CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; | |||
| CODE_SIGN_IDENTITY = "Apple Development"; | |||
| CODE_SIGN_STYLE = Automatic; | |||
| CURRENT_PROJECT_VERSION = 21; | |||
| CURRENT_PROJECT_VERSION = 22; | |||
| DEVELOPMENT_TEAM = C3DTD2JH94; | |||
| ENABLE_BITCODE = NO; | |||
| FRAMEWORK_SEARCH_PATHS = ( | |||
| @@ -563,7 +563,7 @@ | |||
| "$(inherited)", | |||
| "$(PROJECT_DIR)/Flutter", | |||
| ); | |||
| MARKETING_VERSION = 1.1.5; | |||
| MARKETING_VERSION = 1.1.6; | |||
| PRODUCT_BUNDLE_IDENTIFIER = vn.azteam.farmdemo; | |||
| PRODUCT_NAME = "$(TARGET_NAME)"; | |||
| PROVISIONING_PROFILE_SPECIFIER = ""; | |||
| @@ -25,12 +25,17 @@ import 'package:farm_tpf/presentation/screens/codes/models/stamp.dart'; | |||
| import 'package:farm_tpf/presentation/screens/codes/models/stamp_filter_request.dart'; | |||
| import 'package:farm_tpf/presentation/screens/codes/models/stamp_request.dart'; | |||
| import 'package:farm_tpf/presentation/screens/codes/models/stamp_timeline.dart'; | |||
| import 'package:farm_tpf/presentation/screens/task/models/employee.dart'; | |||
| import 'package:farm_tpf/presentation/screens/task/models/task_request.dart'; | |||
| import 'package:farm_tpf/utils/const_common.dart'; | |||
| import 'package:flutter/material.dart'; | |||
| import '../../presentation/screens/codes/models/stamp_type.dart'; | |||
| import '../../presentation/screens/login/models/request_user.dart'; | |||
| import '../../presentation/screens/login/models/response_user.dart'; | |||
| import '../../presentation/screens/task/models/task.dart'; | |||
| import '../../presentation/screens/task/models/task_filter_request.dart'; | |||
| import '../../presentation/screens/task/models/task_update_request.dart'; | |||
| import '../api/app_exception.dart'; | |||
| class Repository { | |||
| @@ -442,4 +447,75 @@ class Repository { | |||
| rethrow; | |||
| } | |||
| } | |||
| // Task | |||
| Future<List<Task>> tasks({ | |||
| int page = 0, | |||
| int size = 20, | |||
| required TaskFilterRequest filter, | |||
| }) async { | |||
| try { | |||
| // var url = '${ConstCommon.baseUrl}/api/tb-todo-lists/list?page=$page&size=$size&sort=createdDate,${filter.sort ?? 'asc'}'; | |||
| var url = '${ConstCommon.baseUrl}/api/tb-todo-lists/list?page=$page&size=$size'; | |||
| var res = await dio.post(url, data: { | |||
| // 'status': filter.status, | |||
| }); | |||
| return (res.data as List).map((e) => Task.fromJson(e)).toList(); | |||
| } catch (e) { | |||
| rethrow; | |||
| } | |||
| } | |||
| Future<void> createTask( | |||
| Function(dynamic) onSuccess, | |||
| Function(String) onError, { | |||
| required RequestTask item, | |||
| }) async { | |||
| try { | |||
| var url = '${ConstCommon.baseUrl}/api/tb-todo-lists'; | |||
| await dio.post(url, data: item).then( | |||
| (value) { | |||
| onSuccess(value); | |||
| }, | |||
| ).catchError((e) { | |||
| onError(AppException.handleError(e)); | |||
| }); | |||
| } catch (e) { | |||
| onError(AppException.handleError(e)); | |||
| } | |||
| } | |||
| Future<void> updateTask( | |||
| Function(dynamic) onSuccess, | |||
| Function(String) onError, { | |||
| required RequestTaskUpdate item, | |||
| }) async { | |||
| try { | |||
| var url = '${ConstCommon.baseUrl}/api/tb-todo-lists/'; | |||
| await dio.put(url, data: item).then( | |||
| (value) { | |||
| onSuccess(value); | |||
| }, | |||
| ).catchError((e) { | |||
| onError(AppException.handleError(e)); | |||
| }); | |||
| } catch (e) { | |||
| onError(AppException.handleError(e)); | |||
| } | |||
| } | |||
| Future<List<Employee>> getEmployees() async { | |||
| try { | |||
| var url = '${ConstCommon.baseUrl}/api/get-all-users-by-login-info'; | |||
| var res = await dio.get( | |||
| url, | |||
| ); | |||
| return (res.data as List).map((e) => Employee.fromJson(e)).toList(); | |||
| } catch (e) { | |||
| rethrow; | |||
| } | |||
| } | |||
| } | |||
| @@ -14,6 +14,7 @@ import 'app.dart'; | |||
| // import 'data/repository/auth_repository.dart'; | |||
| import 'data/repository/authentication_repository.dart'; | |||
| import 'environment/app_config.dart'; | |||
| import 'presentation/screens/task/bloc/task_bloc.dart'; | |||
| final GlobalKey<NavigatorState> globalNavigator = GlobalKey<NavigatorState>(); | |||
| Future<void> main() async { | |||
| @@ -46,6 +47,11 @@ Future<void> main() async { | |||
| Repository(), | |||
| ), | |||
| ), | |||
| BlocProvider( | |||
| create: (_) => TaskBloc( | |||
| Repository(), | |||
| ), | |||
| ), | |||
| BlocProvider( | |||
| create: (_) => DetailStampCubit(), | |||
| ), | |||
| @@ -0,0 +1,64 @@ | |||
| import 'package:flutter/material.dart'; | |||
| import '../../../themes/app_dimension.dart'; | |||
| import '../../../themes/app_colors.dart'; | |||
| import '../../../themes/styles_text.dart'; | |||
| class CheckboxWidget extends StatefulWidget { | |||
| final String title; | |||
| final bool isChecked; | |||
| final Function(bool) onChange; | |||
| final TextStyle? style; | |||
| const CheckboxWidget({ | |||
| Key? key, | |||
| required this.title, | |||
| required this.isChecked, | |||
| required this.onChange, | |||
| this.style, | |||
| }) : super(key: key); | |||
| @override | |||
| _CheckboxWidgetState createState() => _CheckboxWidgetState(); | |||
| } | |||
| class _CheckboxWidgetState extends State<CheckboxWidget> { | |||
| var checked = false; | |||
| @override | |||
| void initState() { | |||
| super.initState(); | |||
| checked = widget.isChecked; | |||
| } | |||
| @override | |||
| Widget build(BuildContext context) { | |||
| return InkWell( | |||
| onTap: () { | |||
| setState(() { | |||
| checked = !checked; | |||
| widget.onChange(checked); | |||
| }); | |||
| }, | |||
| child: Row( | |||
| children: [ | |||
| checked | |||
| ? Icon( | |||
| Icons.check_box, | |||
| color: AppColors.primary1, | |||
| ) | |||
| : Icon( | |||
| Icons.check_box_outline_blank, | |||
| color: AppColors.neutral1, | |||
| ), | |||
| SizedBox( | |||
| width: 8.w, | |||
| ), | |||
| Text( | |||
| '${widget.title}', | |||
| style: widget.style ?? StylesText.body1, | |||
| ), | |||
| ], | |||
| ), | |||
| ); | |||
| } | |||
| } | |||
| @@ -56,7 +56,7 @@ class _WidgetSearchState extends State<WidgetSearch> { | |||
| widget.onPressed(widget.searchController.text); | |||
| }, | |||
| ), | |||
| hintText: 'Tìm theo mã, tên lô', | |||
| hintText: 'Tìm kiếm ...', | |||
| hintStyle: TextStyle(color: Colors.grey[500])), | |||
| onSubmitted: (value) { | |||
| widget.onPressed(widget.searchController.text); | |||
| @@ -1,5 +1,6 @@ | |||
| import 'package:farm_tpf/presentation/screens/plot_detail/sc_plot_history.dart'; | |||
| import 'package:farm_tpf/presentation/screens/plot_detail/sc_plot_parameter.dart'; | |||
| import 'package:farm_tpf/presentation/screens/task/task_page.dart'; | |||
| import 'package:farm_tpf/utils/const_color.dart'; | |||
| import 'package:flutter/material.dart'; | |||
| import 'package:get/get.dart'; | |||
| @@ -69,7 +70,7 @@ class _HomeTabbarWidgetState extends State<HomeTabbarWidget> with TickerProvider | |||
| Container( | |||
| padding: EdgeInsets.only(top: 8, bottom: 8), | |||
| child: Text( | |||
| 'Chỉ số', | |||
| 'Công việc', | |||
| style: TextStyle(color: selectedTab.index == 0 ? AppColors.DEFAULT : Colors.black, fontSize: 12), | |||
| ), | |||
| ), | |||
| @@ -93,7 +94,10 @@ class _HomeTabbarWidgetState extends State<HomeTabbarWidget> with TickerProvider | |||
| body: TabBarView( | |||
| controller: tabbarController, | |||
| children: [ | |||
| PlotParameterScreen( | |||
| // PlotParameterScreen( | |||
| // cropId: widget.cropId ?? -1, | |||
| // ), | |||
| TaskPage( | |||
| cropId: widget.cropId ?? -1, | |||
| ), | |||
| PlotInformationScreen( | |||
| @@ -0,0 +1,129 @@ | |||
| import 'package:bloc/bloc.dart'; | |||
| import 'package:equatable/equatable.dart'; | |||
| import 'package:farm_tpf/presentation/screens/task/models/task_update_request.dart'; | |||
| import 'package:flutter/foundation.dart'; | |||
| import 'package:flutter/material.dart'; | |||
| import '../../../../data/api/app_exception.dart'; | |||
| import '../../../../data/repository/repository.dart'; | |||
| import '../../../../models/item_dropdown.dart'; | |||
| import '../../../../utils/const_common.dart'; | |||
| import '../../../../utils/helpers.dart'; | |||
| import '../../../../utils/utils.dart'; | |||
| import '../../../custom_widgets/widget_utils.dart'; | |||
| import '../models/task.dart'; | |||
| import '../models/task_filter_request.dart'; | |||
| part 'task_event.dart'; | |||
| part 'task_state.dart'; | |||
| class TaskBloc extends Bloc<TaskEvent, TaskState> { | |||
| final Repository repository; | |||
| int pageSize = 20; | |||
| TaskBloc(this.repository) : super(TaskInitial()); | |||
| var status = ValueNotifier( | |||
| TaskStatus.values | |||
| .map( | |||
| (e) => ItemDropDown(key: describeEnum(e), value: Helpers.getTaskStatus(describeEnum(e))), | |||
| ) | |||
| .toList(), | |||
| ); | |||
| var selectedStatus = ValueNotifier( | |||
| TaskStatus.values | |||
| .map( | |||
| (e) => ItemDropDown(key: describeEnum(e), value: Helpers.getTaskStatus(describeEnum(e))), | |||
| ) | |||
| .toList(), | |||
| ); | |||
| var sort = ValueNotifier(describeEnum(SortType.asc)); | |||
| @override | |||
| Stream<TaskState> mapEventToState( | |||
| TaskEvent event, | |||
| ) async* { | |||
| if (event is DataFetched && !(state is TaskSuccess && ((state as TaskSuccess).hasReachedMax ?? false))) { | |||
| try { | |||
| if (state is TaskInitial) { | |||
| yield (TaskLoading()); | |||
| final response = await getListTask(0); | |||
| yield TaskSuccess( | |||
| items: response, | |||
| page: 0, | |||
| hasReachedMax: response.length < pageSize ? true : false, | |||
| ); | |||
| } | |||
| if (state is TaskSuccess) { | |||
| final currentState = state as TaskSuccess; | |||
| if (currentState.hasReachedMax ?? false) { | |||
| return; | |||
| } | |||
| int page = (currentState.page ?? 0) + 1; | |||
| final response = await getListTask(page); | |||
| yield response.isEmpty | |||
| ? currentState.copyWith(hasReachedMax: true) | |||
| : TaskSuccess( | |||
| items: (currentState.items ?? []) + response, | |||
| page: (currentState.page ?? 0) + 1, | |||
| hasReachedMax: false, | |||
| ); | |||
| } | |||
| } catch (e) { | |||
| var errorString = AppException.handleError(e); | |||
| yield (TaskFailure(errorString: errorString)); | |||
| } | |||
| } | |||
| if (event is OnRefresh) { | |||
| try { | |||
| yield (TaskLoading()); | |||
| final response = await getListTask(0); | |||
| var items = <Task>[]; | |||
| response.forEach((element) { | |||
| items.add(Task.clone(element)); | |||
| }); | |||
| yield TaskSuccess( | |||
| items: items, | |||
| page: 0, | |||
| hasReachedMax: items.length < pageSize ? true : false, | |||
| ); | |||
| } catch (e) { | |||
| yield (TaskFailure(errorString: AppException.handleError(e))); | |||
| } | |||
| } else if (event is OnSearch) { | |||
| try { | |||
| yield (TaskLoading()); | |||
| final response = await getListTask(0); | |||
| yield TaskSuccess(items: response, page: 0, hasReachedMax: response.length < pageSize ? true : false); | |||
| } catch (e) { | |||
| yield (TaskFailure(errorString: AppException.handleError(e))); | |||
| } | |||
| } | |||
| } | |||
| Future<List<Task>> getListTask(int page) async { | |||
| var filter = TaskFilterRequest() | |||
| ..sort = sort.value | |||
| ..status = selectedStatus.value.map((e) => e.key ?? '').toList(); | |||
| return await repository.tasks(page: 0, filter: filter); | |||
| } | |||
| Future<void> updateStatusTask( | |||
| RequestTaskUpdate task, { | |||
| required Function onSuccess, | |||
| }) async { | |||
| print(task.toJson()); | |||
| UtilWidget.showLoading(); | |||
| await repository.updateTask( | |||
| (success) { | |||
| UtilWidget.hideDialog(); | |||
| Utils.showSnackBarSuccess(); | |||
| onSuccess(); | |||
| }, | |||
| (errorMessage) { | |||
| UtilWidget.hideDialog(); | |||
| Utils.showSnackBarError(); | |||
| }, | |||
| item: task, | |||
| ); | |||
| } | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| part of 'task_bloc.dart'; | |||
| abstract class TaskEvent extends Equatable { | |||
| const TaskEvent(); | |||
| @override | |||
| List<Object> get props => []; | |||
| } | |||
| class DataFetched extends TaskEvent {} | |||
| class OnRefresh extends TaskEvent {} | |||
| class OnSearch extends TaskEvent { | |||
| final String searchString; | |||
| OnSearch({required this.searchString}); | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| part of 'task_bloc.dart'; | |||
| abstract class TaskState extends Equatable { | |||
| const TaskState(); | |||
| @override | |||
| List<Object> get props => []; | |||
| } | |||
| class TaskInitial extends TaskState {} | |||
| class TaskLoading extends TaskState {} | |||
| class TaskFailure extends TaskState { | |||
| final String errorString; | |||
| TaskFailure({required this.errorString}); | |||
| } | |||
| class TaskSuccess<Task> extends TaskState { | |||
| final List<Task>? items; | |||
| final int? page; | |||
| final bool? hasReachedMax; | |||
| const TaskSuccess({this.items, this.page, this.hasReachedMax}); | |||
| TaskSuccess copyWith({List<Task>? items, int? page, bool? hasReachedMax}) { | |||
| return TaskSuccess( | |||
| items: items ?? this.items, | |||
| page: page ?? this.page, | |||
| hasReachedMax: hasReachedMax ?? this.hasReachedMax, | |||
| ); | |||
| } | |||
| } | |||
| @@ -0,0 +1,175 @@ | |||
| import 'package:cached_network_image/cached_network_image.dart'; | |||
| import 'package:farm_tpf/models/item_dropdown.dart'; | |||
| import 'package:farm_tpf/presentation/screens/codes/models/stamp_type.dart'; | |||
| import 'package:farm_tpf/presentation/screens/codes/widgets/item_column.dart'; | |||
| import 'package:flutter/material.dart'; | |||
| import 'package:flutter/scheduler.dart'; | |||
| import 'package:flutter/services.dart'; | |||
| import 'package:flutter/src/widgets/framework.dart'; | |||
| import 'package:flutter/src/widgets/placeholder.dart'; | |||
| import 'package:flutter_bloc/flutter_bloc.dart'; | |||
| import 'package:get/get.dart'; | |||
| import 'package:keyboard_dismisser/keyboard_dismisser.dart'; | |||
| import '../../../utils/utils.dart'; | |||
| import '../../custom_widgets/app_bar_widget.dart'; | |||
| import '../../custom_widgets/button_widget.dart'; | |||
| import '../../custom_widgets/date_picker/date_picker_widget.dart'; | |||
| import '../../custom_widgets/dropdown/dropdown_bottom_sheet.dart'; | |||
| import '../../custom_widgets/textfield/text_field_normal.dart'; | |||
| import 'cubit/create_task_cubit.dart'; | |||
| class CreateTaskPage extends StatefulWidget { | |||
| final int cropId; | |||
| const CreateTaskPage({ | |||
| super.key, | |||
| required this.cropId, | |||
| }); | |||
| @override | |||
| State<CreateTaskPage> createState() => _CreateTaskPageState(); | |||
| } | |||
| class _CreateTaskPageState extends State<CreateTaskPage> { | |||
| final bloc = CreateTaskCubit(); | |||
| @override | |||
| void initState() { | |||
| super.initState(); | |||
| bloc.preparedData(cropId: widget.cropId); | |||
| } | |||
| @override | |||
| void dispose() { | |||
| bloc.dispose(); | |||
| super.dispose(); | |||
| } | |||
| @override | |||
| Widget build(BuildContext context) { | |||
| return Scaffold( | |||
| appBar: AppBarWidget(), | |||
| body: BlocListener<CreateTaskCubit, CreateTaskState>( | |||
| bloc: bloc, | |||
| listener: ((context, state) { | |||
| if (state is CreateTaskLoading) { | |||
| SchedulerBinding.instance.addPostFrameCallback((timeTask) { | |||
| UtilWidget.showLoading(); | |||
| }); | |||
| } else if (state is CreateTaskFailure) { | |||
| SchedulerBinding.instance.addPostFrameCallback((timeTask) { | |||
| UtilWidget.hideLoading(); | |||
| // UtilWidget.showToastError(state.errorMessage); | |||
| }); | |||
| } else if (state is CreateTaskPrepareDataSuccessful) { | |||
| SchedulerBinding.instance.addPostFrameCallback((timeTask) { | |||
| UtilWidget.hideLoading(); | |||
| }); | |||
| } | |||
| }), | |||
| child: KeyboardDismisser( | |||
| child: Container( | |||
| child: Form( | |||
| key: bloc.formKey, | |||
| child: Column( | |||
| children: [ | |||
| Expanded( | |||
| child: _widgetBody(), | |||
| ), | |||
| Padding( | |||
| padding: const EdgeInsets.all(8.0), | |||
| child: ButtonWidget( | |||
| title: 'Cập nhật', | |||
| onPressed: () { | |||
| bloc.onSubmit(widget.cropId); | |||
| }, | |||
| ), | |||
| ), | |||
| ], | |||
| ), | |||
| ), | |||
| ), | |||
| ), | |||
| ), | |||
| ); | |||
| } | |||
| Widget _widgetBody() { | |||
| return Container( | |||
| padding: const EdgeInsets.all(16), | |||
| child: SingleChildScrollView( | |||
| child: Column( | |||
| crossAxisAlignment: CrossAxisAlignment.start, | |||
| children: [ | |||
| ItemColumnWidget( | |||
| title: 'Giao việc cho', | |||
| child: ValueListenableBuilder<String>( | |||
| valueListenable: bloc.selectedPEmployee, | |||
| builder: (context, selected, _) { | |||
| return ValueListenableBuilder<List<ItemDropDown>>( | |||
| valueListenable: bloc.employees, | |||
| builder: (context, employees, _) { | |||
| return DropdownBottomSheet( | |||
| dataSources: employees, | |||
| initValue: selected, | |||
| onSelected: (val) { | |||
| bloc.selectedPEmployee.value = val.key ?? ''; | |||
| }, | |||
| hint: 'Giao việc cho', | |||
| ); | |||
| }, | |||
| ); | |||
| }, | |||
| ), | |||
| ), | |||
| const SizedBox( | |||
| height: 8, | |||
| ), | |||
| ItemColumnWidget( | |||
| title: 'Tiêu đề', | |||
| child: TextFieldNormal( | |||
| controller: bloc.titleNameCtl, | |||
| maxLines: 1, | |||
| hint: 'Tiêu đề', | |||
| ), | |||
| ), | |||
| const SizedBox( | |||
| height: 8, | |||
| ), | |||
| ItemColumnWidget( | |||
| title: 'Mô tả công việc', | |||
| child: TextFieldNormal( | |||
| controller: bloc.detailCtl, | |||
| maxLines: 3, | |||
| hint: 'Mô tả công việc', | |||
| ), | |||
| ), | |||
| const SizedBox( | |||
| height: 8, | |||
| ), | |||
| const SizedBox(height: 8), | |||
| ValueListenableBuilder<DateTime>( | |||
| valueListenable: bloc.deadline, | |||
| builder: (context, dexuat, _) { | |||
| return ItemColumnWidget( | |||
| title: 'Hạn chót', | |||
| child: DatePickerWidget( | |||
| initDateTime: dexuat, | |||
| onUpdateDateTime: (selectedDate) { | |||
| if (selectedDate != null) { | |||
| bloc.deadline.value = selectedDate; | |||
| } | |||
| }, | |||
| ), | |||
| ); | |||
| }, | |||
| ), | |||
| const SizedBox( | |||
| height: 16, | |||
| ), | |||
| ], | |||
| ), | |||
| ), | |||
| ); | |||
| } | |||
| } | |||
| @@ -0,0 +1,87 @@ | |||
| import 'package:bloc/bloc.dart'; | |||
| import 'package:equatable/equatable.dart'; | |||
| import 'package:farm_tpf/presentation/screens/task/models/employee.dart'; | |||
| import 'package:farm_tpf/utils/formatter.dart'; | |||
| import 'package:flutter/material.dart'; | |||
| import 'package:get/get.dart'; | |||
| import '../../../../data/api/app_exception.dart'; | |||
| import '../../../../data/repository/repository.dart'; | |||
| import '../../../../models/item_dropdown.dart'; | |||
| import '../../../../utils/utils.dart'; | |||
| import '../../../custom_widgets/widget_utils.dart'; | |||
| import '../models/task_request.dart'; | |||
| part 'create_task_state.dart'; | |||
| class CreateTaskCubit extends Cubit<CreateTaskState> { | |||
| CreateTaskCubit() : super(CreateTaskInitial()); | |||
| final repository = Repository(); | |||
| final formKey = GlobalKey<FormState>(); | |||
| final titleNameCtl = TextEditingController(); | |||
| final detailCtl = TextEditingController(); | |||
| var deadline = ValueNotifier(DateTime.now()); | |||
| var employeeRaws = <Employee>[]; | |||
| var employees = ValueNotifier(<ItemDropDown>[]); | |||
| var selectedPEmployee = ValueNotifier(''); | |||
| // var existedCreateTask = UpdateCreateTask(); | |||
| void dispose() { | |||
| titleNameCtl.dispose(); | |||
| detailCtl.dispose(); | |||
| } | |||
| Future<void> preparedData({required int cropId}) async { | |||
| try { | |||
| await Future.delayed(const Duration(seconds: 0)); | |||
| emit(CreateTaskLoading()); | |||
| employeeRaws = await repository.getEmployees(); | |||
| employees.value = employeeRaws | |||
| .map( | |||
| (e) => ItemDropDown(key: e.id?.toString(), value: e.name), | |||
| ) | |||
| .toList(); | |||
| emit(CreateTaskPrepareDataSuccessful()); | |||
| } catch (e) { | |||
| emit(CreateTaskFailure(AppException.handleError(e))); | |||
| } | |||
| } | |||
| Future<void> onSubmit(int cropId) async { | |||
| if (formKey.currentState!.validate()) { | |||
| if (selectedPEmployee.value.isEmpty) { | |||
| Utils.showSnackBarWarning(message: 'Vui lòng chọn nhân viên'); | |||
| return; | |||
| } else if (titleNameCtl.text.trim().isEmpty) { | |||
| Utils.showSnackBarWarning(message: 'Vui lòng nhập tiêu đề'); | |||
| return; | |||
| } else if (detailCtl.text.trim().isEmpty) { | |||
| Utils.showSnackBarWarning(message: 'Vui lòng nhập nội dung'); | |||
| return; | |||
| } | |||
| var requestTask = RequestTask(); | |||
| requestTask | |||
| ..cropId = cropId | |||
| ..title = titleNameCtl.text | |||
| ..detail = detailCtl.text | |||
| ..userAssignedId = int.tryParse(selectedPEmployee.value) | |||
| ..deadline = deadline.value.convertLocalDateTimeToStringUtcDateTime(); | |||
| print(requestTask.toJson()); | |||
| UtilWidget.showLoading(); | |||
| await repository.createTask( | |||
| (success) { | |||
| UtilWidget.hideDialog(); | |||
| Get.back(result: 'ok'); | |||
| Utils.showSnackBarSuccess(); | |||
| }, | |||
| (errorMessage) { | |||
| UtilWidget.hideDialog(); | |||
| Utils.showSnackBarError(); | |||
| }, | |||
| item: requestTask, | |||
| ); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,22 @@ | |||
| part of 'create_task_cubit.dart'; | |||
| abstract class CreateTaskState extends Equatable { | |||
| const CreateTaskState(); | |||
| @override | |||
| List<Object> get props => []; | |||
| } | |||
| class CreateTaskInitial extends CreateTaskState {} | |||
| class CreateTaskLoading extends CreateTaskState {} | |||
| class CreateTaskFailure extends CreateTaskState { | |||
| final String errorMessage; | |||
| CreateTaskFailure(this.errorMessage); | |||
| } | |||
| class CreateTaskPrepareDataSuccessful extends CreateTaskState { | |||
| CreateTaskPrepareDataSuccessful(); | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| class Employee { | |||
| int? id; | |||
| String? name; | |||
| Employee({ | |||
| this.id, | |||
| this.name, | |||
| }); | |||
| Employee.fromJson(Map<String, dynamic> json) { | |||
| id = json['id']; | |||
| name = json['fullName']; | |||
| } | |||
| Map<String, dynamic> toJson() { | |||
| final Map<String, dynamic> data = new Map<String, dynamic>(); | |||
| data['id'] = this.id; | |||
| data['fullName'] = this.name; | |||
| return data; | |||
| } | |||
| } | |||
| @@ -0,0 +1,72 @@ | |||
| class Task { | |||
| int? id; | |||
| String? title; | |||
| String? description; | |||
| String? dueDate; | |||
| String? executeDate; | |||
| Assigned? assigned; | |||
| bool? isCompleted; | |||
| Task({ | |||
| this.id, | |||
| this.title, | |||
| this.description, | |||
| this.dueDate, | |||
| this.executeDate, | |||
| this.assigned, | |||
| this.isCompleted, | |||
| }); | |||
| Task.fromJson(Map<String, dynamic> json) { | |||
| title = json['title']; | |||
| id = json['id']; | |||
| description = json['detail']; | |||
| dueDate = json['deadline']; | |||
| executeDate = json['executeDate']; | |||
| assigned = json['assigned'] != null ? new Assigned.fromJson(json['assigned']) : null; | |||
| isCompleted = json['completed']; | |||
| } | |||
| Map<String, dynamic> toJson() { | |||
| final Map<String, dynamic> data = new Map<String, dynamic>(); | |||
| data['id'] = this.id; | |||
| data['title'] = this.title; | |||
| data['detail'] = this.description; | |||
| data['deadline'] = this.dueDate; | |||
| data['executeDate'] = this.executeDate; | |||
| if (this.assigned != null) { | |||
| data['assigned'] = this.assigned?.toJson(); | |||
| } | |||
| data['completed'] = this.isCompleted; | |||
| return data; | |||
| } | |||
| Task.clone(Task task) { | |||
| this.id = task.id; | |||
| this.title = task.title; | |||
| this.description = task.description; | |||
| this.dueDate = task.dueDate; | |||
| this.executeDate = task.executeDate; | |||
| this.assigned = task.assigned; | |||
| this.isCompleted = task.isCompleted; | |||
| } | |||
| } | |||
| class Assigned { | |||
| int? id; | |||
| bool? activated; | |||
| Assigned({this.id, this.activated}); | |||
| Assigned.fromJson(Map<String, dynamic> json) { | |||
| id = json['id']; | |||
| activated = json['activated']; | |||
| } | |||
| Map<String, dynamic> toJson() { | |||
| final Map<String, dynamic> data = new Map<String, dynamic>(); | |||
| data['id'] = this.id; | |||
| data['activated'] = this.activated; | |||
| return data; | |||
| } | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| class TaskFilterRequest { | |||
| List<String>? status; | |||
| String? sort; | |||
| TaskFilterRequest({this.status, this.sort}); | |||
| TaskFilterRequest.fromJson(Map<String, dynamic> json) { | |||
| status = json['status'].cast<String>(); | |||
| sort = json['sort']; | |||
| } | |||
| Map<String, dynamic> toJson() { | |||
| final Map<String, dynamic> data = new Map<String, dynamic>(); | |||
| data['status'] = this.status; | |||
| data['sort'] = this.sort; | |||
| return data; | |||
| } | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| class RequestTask { | |||
| String? title; | |||
| String? detail; | |||
| String? deadline; | |||
| int? userAssignedId; | |||
| int? cropId; | |||
| RequestTask({ | |||
| this.title, | |||
| this.detail, | |||
| this.deadline, | |||
| this.userAssignedId, | |||
| this.cropId, | |||
| }); | |||
| RequestTask.fromJson(Map<String, dynamic> json) { | |||
| title = json['title']; | |||
| detail = json['detail']; | |||
| deadline = json['deadline']; | |||
| userAssignedId = json['user_assigned_id']; | |||
| cropId = json['crop_id']; | |||
| } | |||
| Map<String, dynamic> toJson() { | |||
| final Map<String, dynamic> data = new Map<String, dynamic>(); | |||
| data['title'] = this.title; | |||
| data['detail'] = this.detail; | |||
| data['deadline'] = this.deadline; | |||
| data['user_assigned_id'] = this.userAssignedId; | |||
| data['crop_id'] = this.cropId; | |||
| return data; | |||
| } | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| class RequestTaskUpdate { | |||
| int? id; | |||
| bool? completed; | |||
| RequestTaskUpdate({this.id, this.completed}); | |||
| RequestTaskUpdate.fromJson(Map<String, dynamic> json) { | |||
| id = json['id']; | |||
| completed = json['completed']; | |||
| } | |||
| Map<String, dynamic> toJson() { | |||
| final Map<String, dynamic> data = new Map<String, dynamic>(); | |||
| data['id'] = this.id; | |||
| data['completed'] = this.completed; | |||
| return data; | |||
| } | |||
| } | |||
| @@ -0,0 +1,193 @@ | |||
| import 'package:farm_tpf/data/repository/repository.dart'; | |||
| import 'package:farm_tpf/presentation/custom_widgets/button/button_2_icon.dart'; | |||
| import 'package:farm_tpf/presentation/custom_widgets/button/second_button.dart'; | |||
| import 'package:farm_tpf/presentation/screens/codes/code_detail_page.dart'; | |||
| import 'package:farm_tpf/presentation/screens/codes/create_stamp_page.dart'; | |||
| import 'package:farm_tpf/presentation/screens/codes/models/stamp.dart'; | |||
| import 'package:farm_tpf/presentation/screens/codes/widgets/item_code.dart'; | |||
| import 'package:farm_tpf/presentation/screens/task/models/task_update_request.dart'; | |||
| import 'package:farm_tpf/themes/app_colors.dart'; | |||
| import 'package:farm_tpf/utils/const_common.dart'; | |||
| import 'package:flutter/cupertino.dart'; | |||
| import 'package:flutter/foundation.dart'; | |||
| import 'package:flutter/material.dart'; | |||
| import 'package:flutter_bloc/flutter_bloc.dart'; | |||
| import 'package:get/get.dart'; | |||
| import '../../../models/item_dropdown.dart'; | |||
| import '../../../themes/styles_text.dart'; | |||
| import '../../../utils/const_string.dart'; | |||
| import '../../../utils/helpers.dart'; | |||
| import '../../custom_widgets/bottom_loader.dart'; | |||
| import '../../custom_widgets/dropdown/multiple_select_bottom_sheet.dart'; | |||
| import '../../custom_widgets/loading_list_page.dart'; | |||
| import '../plot/widget_search.dart'; | |||
| import 'bloc/task_bloc.dart'; | |||
| import 'create_task_page.dart'; | |||
| import 'widgets/task_item.dart'; | |||
| class TaskPage extends StatefulWidget { | |||
| final int cropId; | |||
| const TaskPage({super.key, required this.cropId}); | |||
| @override | |||
| State<TaskPage> createState() => _TaskPageState(); | |||
| } | |||
| class _TaskPageState extends State<TaskPage> { | |||
| final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); | |||
| TaskBloc bloc = TaskBloc(Repository()); | |||
| final _scrollController = ScrollController(); | |||
| final _scrollThreshold = 250.0; | |||
| @override | |||
| void initState() { | |||
| bloc.add(DataFetched()); | |||
| _scrollController.addListener(() { | |||
| final maxScroll = _scrollController.position.maxScrollExtent; | |||
| final currentScroll = _scrollController.position.pixels; | |||
| if (maxScroll - currentScroll < _scrollThreshold) { | |||
| bloc.add(DataFetched()); | |||
| } | |||
| }); | |||
| super.initState(); | |||
| } | |||
| @override | |||
| void dispose() { | |||
| _scrollController.dispose(); | |||
| super.dispose(); | |||
| } | |||
| @override | |||
| Widget build(BuildContext context) { | |||
| return Scaffold( | |||
| backgroundColor: Colors.white, | |||
| key: _scaffoldKey, | |||
| body: SafeArea( | |||
| child: Column( | |||
| crossAxisAlignment: CrossAxisAlignment.start, | |||
| children: <Widget>[ | |||
| Row( | |||
| children: [ | |||
| ValueListenableBuilder<String>( | |||
| valueListenable: bloc.sort, | |||
| builder: (context, sort, _) { | |||
| return Button2Icon( | |||
| leftIcon: (sort == describeEnum(SortType.asc)) ? CupertinoIcons.arrow_up : CupertinoIcons.arrow_down, | |||
| title: 'Ngày tạo', | |||
| onPressed: () { | |||
| if (sort == describeEnum(SortType.asc)) { | |||
| bloc.sort.value = describeEnum(SortType.desc); | |||
| } else { | |||
| bloc.sort.value = describeEnum(SortType.asc); | |||
| } | |||
| bloc.sort.notifyListeners(); | |||
| bloc.add(OnRefresh()); | |||
| }, | |||
| ); | |||
| }, | |||
| ), | |||
| ValueListenableBuilder<List<ItemDropDown>>( | |||
| valueListenable: bloc.selectedStatus, | |||
| builder: (context, selecteds, _) { | |||
| return ValueListenableBuilder<List<ItemDropDown>>( | |||
| valueListenable: bloc.status, | |||
| builder: (context, status, _) { | |||
| return MultipleSelectBottomSheet( | |||
| dataSources: status, | |||
| initValue: selecteds, | |||
| onSelected: (val) { | |||
| bloc.selectedStatus.value = val; | |||
| Helpers.hideKeyboard(context); | |||
| bloc.add(OnRefresh()); | |||
| }, | |||
| hint: 'Trạng thái', | |||
| ); | |||
| }, | |||
| ); | |||
| }, | |||
| ), | |||
| const Spacer(), | |||
| SecondButton( | |||
| onPressed: () { | |||
| Get.to(() => CreateTaskPage( | |||
| cropId: widget.cropId, | |||
| ))?.then((value) { | |||
| if (value != null) { | |||
| bloc.add(OnRefresh()); | |||
| } | |||
| }); | |||
| }, | |||
| title: 'Thêm', | |||
| leftIcon: CupertinoIcons.add, | |||
| color: AppColors.primary1, | |||
| textColor: Colors.white, | |||
| borderColor: AppColors.primary1, | |||
| ), | |||
| ], | |||
| ), | |||
| Expanded( | |||
| child: mainBody(), | |||
| ), | |||
| ], | |||
| ), | |||
| ), | |||
| ); | |||
| } | |||
| Widget mainBody() { | |||
| return BlocBuilder<TaskBloc, TaskState>( | |||
| bloc: bloc, | |||
| builder: (context, state) { | |||
| if (state is TaskFailure) { | |||
| return Center(child: Text(state.errorString)); | |||
| } | |||
| if (state is TaskSuccess) { | |||
| if ((state.items ?? []).isEmpty) { | |||
| return Center(child: Text(label_list_empty)); | |||
| } | |||
| return RefreshIndicator( | |||
| child: ListView.builder( | |||
| physics: AlwaysScrollableScrollPhysics(), | |||
| itemBuilder: (BuildContext context, int index) { | |||
| return index >= (state.items ?? []).length | |||
| ? BottomLoader() | |||
| : ItemTask( | |||
| item: state.items?[index], | |||
| onPressed: () { | |||
| // Get.to( | |||
| // () => TaskDetailPage( | |||
| // stampId: state.items?[index].id, | |||
| // stampTask: state.items?[index].code, | |||
| // ), | |||
| // ); | |||
| }, | |||
| onChangedStatus: (status) { | |||
| bloc.updateStatusTask( | |||
| RequestTaskUpdate( | |||
| id: state.items?[index].id, | |||
| completed: status, | |||
| ), | |||
| onSuccess: () { | |||
| bloc.add(OnRefresh()); | |||
| }, | |||
| ); | |||
| }, | |||
| ); | |||
| }, | |||
| itemCount: (state.hasReachedMax ?? false) ? (state.items ?? []).length : (state.items ?? []).length + 1, | |||
| controller: _scrollController, | |||
| ), | |||
| onRefresh: () async { | |||
| bloc.add(OnRefresh()); | |||
| }); | |||
| } | |||
| return Center( | |||
| child: LoadingListPage(), | |||
| ); | |||
| }, | |||
| ); | |||
| } | |||
| } | |||
| @@ -0,0 +1,94 @@ | |||
| // ignore_for_file: public_member_api_docs, sort_constructors_first | |||
| import 'package:farm_tpf/presentation/custom_widgets/checkbox/checkbox_widget.dart'; | |||
| import 'package:farm_tpf/utils/helpers.dart'; | |||
| import 'package:flutter/material.dart'; | |||
| import 'package:farm_tpf/presentation/screens/codes/models/stamp.dart'; | |||
| import '../../../../themes/app_colors.dart'; | |||
| import '../../../../themes/styles_text.dart'; | |||
| import 'package:farm_tpf/utils/formatter.dart'; | |||
| import '../models/task.dart'; | |||
| class ItemTask extends StatelessWidget { | |||
| final Task item; | |||
| final Function onPressed; | |||
| final Function(bool) onChangedStatus; | |||
| ItemTask({ | |||
| Key? key, | |||
| required this.item, | |||
| required this.onPressed, | |||
| required this.onChangedStatus, | |||
| }) : super(key: key); | |||
| @override | |||
| Widget build(BuildContext context) { | |||
| var dueDate = item.dueDate?.format_DDMMYY().toString() ?? ''; | |||
| var completedDate = item.executeDate?.format_DDMMYY().toString() ?? ''; | |||
| var backgroundColor = Colors.white; | |||
| if (item.isCompleted ?? false) { | |||
| backgroundColor = AppColors.primary1.withOpacity(0.3); | |||
| } else { | |||
| var dueDateCompare = item.dueDate?.convertStringServerDateTimeToLocalDateTime() ?? DateTime.now(); | |||
| if (Helpers.isAfterToday(dueDateCompare)) { | |||
| backgroundColor = Colors.white; | |||
| } else { | |||
| backgroundColor = AppColors.semantic6; | |||
| } | |||
| } | |||
| return GestureDetector( | |||
| onTap: () { | |||
| onPressed(); | |||
| }, | |||
| child: Container( | |||
| padding: EdgeInsets.all(8), | |||
| margin: EdgeInsets.symmetric(vertical: 4, horizontal: 8), | |||
| decoration: BoxDecoration( | |||
| borderRadius: BorderRadius.circular(10), | |||
| border: Border.all( | |||
| color: Colors.grey.shade200, | |||
| width: 1, | |||
| ), | |||
| color: backgroundColor, | |||
| ), | |||
| child: Row( | |||
| children: [ | |||
| Expanded( | |||
| child: Column( | |||
| crossAxisAlignment: CrossAxisAlignment.start, | |||
| children: [ | |||
| Text( | |||
| '${item.title ?? ''}', | |||
| style: StylesText.body4, | |||
| ), | |||
| const SizedBox( | |||
| height: 4, | |||
| ), | |||
| Text( | |||
| 'Hạn chót: $dueDate', | |||
| style: StylesText.body6, | |||
| ), | |||
| const SizedBox( | |||
| height: 4, | |||
| ), | |||
| Text( | |||
| 'Hoàn thành: $completedDate', | |||
| style: StylesText.body6, | |||
| ), | |||
| ], | |||
| ), | |||
| ), | |||
| CheckboxWidget( | |||
| title: '', | |||
| isChecked: item.isCompleted ?? false, | |||
| onChange: (val) { | |||
| onChangedStatus(val); | |||
| }, | |||
| ) | |||
| ], | |||
| ), | |||
| ), | |||
| ); | |||
| } | |||
| } | |||
| @@ -34,3 +34,8 @@ enum SortType { | |||
| asc, | |||
| desc, | |||
| } | |||
| enum TaskStatus { | |||
| completed, | |||
| no_completed, | |||
| } | |||
| @@ -44,4 +44,19 @@ class Helpers { | |||
| } | |||
| return ''; | |||
| } | |||
| static String getTaskStatus(String status) { | |||
| if (status.toLowerCase() == describeEnum(TaskStatus.completed)) { | |||
| return 'Hoàn thành'; | |||
| } else if (status.toLowerCase() == describeEnum(TaskStatus.no_completed)) { | |||
| return 'Chưa hoàn thành'; | |||
| } | |||
| return ''; | |||
| } | |||
| static bool isAfterToday(DateTime compareDate) { | |||
| var now = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day); | |||
| var date = DateTime(compareDate.year, compareDate.month, compareDate.day); | |||
| return date.isAfter(now); | |||
| } | |||
| } | |||
| @@ -2,7 +2,7 @@ name: farm_tpf | |||
| description: A new Flutter project. | |||
| publish_to: 'none' | |||
| version: 1.1.6+21 | |||
| version: 1.1.7+23 | |||
| environment: | |||
| sdk: ">=2.17.0 <=3.0.0" | |||