feat : first step request tagging

This commit is contained in:
2025-08-04 15:31:34 +03:30
parent 7a3061d9a4
commit 2c10800ce7
27 changed files with 1044 additions and 168 deletions

View File

@@ -44,6 +44,12 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="fullSensor"
android:theme="@style/Ucrop.CropTheme"/>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Ucrop.CropTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
</resources>

View File

@@ -16,7 +16,6 @@ class CustomNavigationObserver extends NavigatorObserver {
@override @override
void didPush(Route route, Route? previousRoute) async { void didPush(Route route, Route? previousRoute) async {
super.didPush(route, previousRoute);
final routeName = route.settings.name; final routeName = route.settings.name;
if (!_isWorkDone && (routeName == ChickenRoutes.init || routeName == ChickenRoutes.auth)) { if (!_isWorkDone && (routeName == ChickenRoutes.init || routeName == ChickenRoutes.auth)) {
_isWorkDone = true; _isWorkDone = true;
@@ -31,6 +30,7 @@ class CustomNavigationObserver extends NavigatorObserver {
_isWorkDone = true; _isWorkDone = true;
await setupLiveStockDI(); await setupLiveStockDI();
} }
super.didPush(route, previousRoute);
tLog('CustomNavigationObserver: didPush - $routeName'); tLog('CustomNavigationObserver: didPush - $routeName');
} }

View File

@@ -1,7 +1,12 @@
import 'package:rasadyar_chicken/presentation/routes/routes.dart';
import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_inspection/inspection.dart';
import 'package:rasadyar_livestock/injection/live_stock_di.dart';
import 'package:rasadyar_livestock/presentation/routes/app_pages.dart';
class ModulesLogic extends GetxController { class ModulesLogic extends GetxController {
TokenStorageService tokenService = Get.find<TokenStorageService>(); TokenStorageService tokenService = Get.find<TokenStorageService>();
RxBool isLoading = false.obs;
List<ModuleModel> moduleList = [ List<ModuleModel> moduleList = [
ModuleModel(title: 'بازرسی', icon: Assets.icons.inspection.path, module: Module.inspection), ModuleModel(title: 'بازرسی', icon: Assets.icons.inspection.path, module: Module.inspection),
@@ -25,4 +30,23 @@ class ModulesLogic extends GetxController {
tokenService.saveModule(module); tokenService.saveModule(module);
tokenService.appModule.value = module; tokenService.appModule.value = module;
} }
Future<void> navigateToModule(Module module) async {
if (module == Module.inspection) {
Get.offAllNamed(InspectionRoutes.init);
} else if (module == Module.liveStocks) {
await setupLiveStockDI();
Get.offAllNamed(LiveStockRoutes.init);
} else if (module == Module.chicken) {
Get.offAllNamed(ChickenRoutes.init);
}
}
void onTapCard(Module module, int index) async {
isLoading.value = true;
selectedIndex.value = index;
saveModule(module);
await Future.delayed(Duration(milliseconds: 800)); // Simulate loading delay
navigateToModule(module);
}
} }

View File

@@ -1,7 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rasadyar_chicken/chicken.dart';
import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_inspection/inspection.dart';
import 'logic.dart'; import 'logic.dart';
@@ -16,39 +15,38 @@ class ModulesPage extends GetView<ModulesLogic> {
centerTitle: true, centerTitle: true,
backgroundColor: AppColor.blueNormal, backgroundColor: AppColor.blueNormal,
), ),
body: GridView.builder( body: Stack(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 20), fit: StackFit.expand,
alignment: Alignment.center,
itemBuilder: (context, index) { children: [
final module = controller.moduleList[index]; GridView.builder(
return CardIcon( padding: EdgeInsets.symmetric(horizontal: 10, vertical: 20),
title: module.title, itemBuilder: (context, index) {
icon: module.icon, final module = controller.moduleList[index];
onTap: () { return CardIcon(
controller.selectedIndex.value = index; title: module.title,
controller.saveModule(module.module); icon: module.icon,
onTap: () => controller.onTapCard(module.module, index),
// Navigate to the appropriate route based on the selected module );
switch (module.module) {
case Module.inspection:
Get.toNamed(InspectionRoutes.init);
break;
case Module.liveStocks:
//TODO: Implement liveStocks module navigation
case Module.chicken:
Get.toNamed(ChickenRoutes.init);
break;
}
}, },
); gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
}, crossAxisCount: 3,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( mainAxisSpacing: 10,
crossAxisCount: 3, crossAxisSpacing: 10,
mainAxisSpacing: 10, ),
crossAxisSpacing: 10, physics: BouncingScrollPhysics(),
), itemCount: controller.moduleList.length,
physics: BouncingScrollPhysics(), ),
itemCount: controller.moduleList.length, ObxValue((loading) {
if (!controller.isLoading.value) return SizedBox.shrink();
return Container(
color: Colors.grey.withValues(alpha: 0.5),
child: Center(
child: CupertinoActivityIndicator(color: AppColor.greenNormal, radius: 30),
),
);
}, controller.isLoading),
],
), ),
); );
} }

View File

@@ -8,6 +8,7 @@ export 'package:dio/dio.dart';
export 'package:flutter_localizations/flutter_localizations.dart'; export 'package:flutter_localizations/flutter_localizations.dart';
export 'package:flutter_map/flutter_map.dart'; export 'package:flutter_map/flutter_map.dart';
export 'package:flutter_map_animations/flutter_map_animations.dart'; export 'package:flutter_map_animations/flutter_map_animations.dart';
export 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart';
export 'package:flutter_rating_bar/flutter_rating_bar.dart'; export 'package:flutter_rating_bar/flutter_rating_bar.dart';
export 'package:flutter_screenutil/flutter_screenutil.dart'; export 'package:flutter_screenutil/flutter_screenutil.dart';
export 'package:flutter_secure_storage/flutter_secure_storage.dart'; export 'package:flutter_secure_storage/flutter_secure_storage.dart';
@@ -21,6 +22,7 @@ export 'package:get/get.dart' hide FormData, MultipartFile, Response;
export 'package:get_it/get_it.dart'; export 'package:get_it/get_it.dart';
//local storage //local storage
export 'package:hive_ce_flutter/hive_flutter.dart'; export 'package:hive_ce_flutter/hive_flutter.dart';
export 'package:image_cropper/image_cropper.dart';
///image picker ///image picker
export 'package:image_picker/image_picker.dart'; export 'package:image_picker/image_picker.dart';
//encryption //encryption
@@ -36,7 +38,6 @@ export 'package:pretty_dio_logger/pretty_dio_logger.dart';
export 'package:rasadyar_core/presentation/common/common.dart'; export 'package:rasadyar_core/presentation/common/common.dart';
export 'package:rasadyar_core/presentation/utils/utils.dart'; export 'package:rasadyar_core/presentation/utils/utils.dart';
export 'package:rasadyar_core/presentation/widget/widget.dart'; export 'package:rasadyar_core/presentation/widget/widget.dart';
export 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart';
//models //models
export 'data/model/model.dart'; export 'data/model/model.dart';
@@ -57,5 +58,4 @@ export 'utils/map_utils.dart';
export 'utils/network/network.dart'; export 'utils/network/network.dart';
export 'utils/route_utils.dart'; export 'utils/route_utils.dart';
export 'utils/separator_input_formatter.dart'; export 'utils/separator_input_formatter.dart';
export 'utils/utils.dart'; export 'utils/utils.dart';

View File

@@ -10,7 +10,7 @@ class AppInterceptor extends Interceptor {
final RefreshTokenCallback? refreshTokenCallback; final RefreshTokenCallback? refreshTokenCallback;
final SaveTokenCallback saveTokenCallback; final SaveTokenCallback saveTokenCallback;
final ClearTokenCallback clearTokenCallback; final ClearTokenCallback clearTokenCallback;
late final Dio dio; late Dio dio;
dynamic authArguments; dynamic authArguments;
static Completer<String?>? _refreshCompleter; static Completer<String?>? _refreshCompleter;
static bool _isRefreshing = false; static bool _isRefreshing = false;
@@ -44,7 +44,7 @@ class AppInterceptor extends Interceptor {
@override @override
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async { Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) { if (err.response?.statusCode == 401 && err.response?.data['detail'] != "No active account found with the given credentials") {
final retryResult = await _handleUnauthorizedError(err); final retryResult = await _handleUnauthorizedError(err);
if (retryResult != null) { if (retryResult != null) {
handler.resolve(retryResult); handler.resolve(retryResult);
@@ -104,6 +104,7 @@ class AppInterceptor extends Interceptor {
return dio.fetch(newOptions); return dio.fetch(newOptions);
} }
//TODO
void _handleRefreshFailure() { void _handleRefreshFailure() {
ApiHandler.cancelAllRequests("Token refresh failed"); ApiHandler.cancelAllRequests("Token refresh failed");

View File

@@ -12,6 +12,7 @@ class DioRemote implements IHttpClient {
Future<void> init() async { Future<void> init() async {
dio = Dio(BaseOptions(baseUrl: baseUrl ?? '')); dio = Dio(BaseOptions(baseUrl: baseUrl ?? ''));
if (interceptors != null) { if (interceptors != null) {
interceptors!.dio = dio;
dio.interceptors.add(interceptors!); dio.interceptors.add(interceptors!);
} }

View File

@@ -725,6 +725,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.4" version: "4.5.4"
image_cropper:
dependency: "direct main"
description:
name: image_cropper
sha256: "4e9c96c029eb5a23798da1b6af39787f964da6ffc78fd8447c140542a9f7c6fc"
url: "https://pub.dev"
source: hosted
version: "9.1.0"
image_cropper_for_web:
dependency: transitive
description:
name: image_cropper_for_web
sha256: fd81ebe36f636576094377aab32673c4e5d1609b32dec16fad98d2b71f1250a9
url: "https://pub.dev"
source: hosted
version: "6.1.0"
image_cropper_platform_interface:
dependency: transitive
description:
name: image_cropper_platform_interface
sha256: "6ca6b81769abff9a4dcc3bbd3d75f5dfa9de6b870ae9613c8cd237333a4283af"
url: "https://pub.dev"
source: hosted
version: "7.1.0"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -18,7 +18,7 @@ dependencies:
##image_picker ##image_picker
image_picker: ^1.1.2 image_picker: ^1.1.2
image_cropper: ^9.1.0
#UI #UI
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8

View File

@@ -32,22 +32,16 @@ class DioErrorHandler {
_errorSnackBar( _errorSnackBar(
error.response?.data.keys.first == 'is_user' error.response?.data.keys.first == 'is_user'
? 'کاربر با این شماره تلفن وجود ندارد' ? 'کاربر با این شماره تلفن وجود ندارد'
: error.response?.data[error.response?.data.keys.first] ?? : '${error.response?.statusCode} - ${error.response?.data[error.response?.data.keys.first]}' ??
'خطا در برقراری ارتباط با سرور', 'خطا در برقراری ارتباط با سرور',
), ),
); );
} }
GetSnackBar _errorSnackBar(String message) { GetSnackBar _errorSnackBar(String message) {
return GetSnackBar( return GetSnackBar(
titleText: Text( titleText: Text('خطا', style: AppFonts.yekan14.copyWith(color: Colors.white)),
'خطا', messageText: Text(message, style: AppFonts.yekan12.copyWith(color: Colors.white)),
style: AppFonts.yekan14.copyWith(color: Colors.white),
),
messageText: Text(
message,
style: AppFonts.yekan12.copyWith(color: Colors.white),
),
backgroundColor: AppColor.error, backgroundColor: AppColor.error,
margin: EdgeInsets.symmetric(horizontal: 12, vertical: 8), margin: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
borderRadius: 12, borderRadius: 12,

View File

@@ -16,7 +16,6 @@ class AuthRemoteDataSourceImp extends AuthRemoteDataSource {
'${_BASE_URL}login/', '${_BASE_URL}login/',
data: authRequest, data: authRequest,
fromJson: AuthResponseModel.fromJson, fromJson: AuthResponseModel.fromJson,
headers: {'Content-Type': 'application/json'},
); );
return res.data; return res.data;
} }

View File

@@ -11,27 +11,17 @@ GetIt get diLiveStock => GetIt.instance;
Future<void> setupLiveStockDI() async { Future<void> setupLiveStockDI() async {
diLiveStock.registerSingleton(DioErrorHandler()); diLiveStock.registerSingleton(DioErrorHandler());
final tokenService = Get.find<TokenStorageService>(); final tokenService = Get.find<TokenStorageService>();
if (tokenService.baseurl.value == null) { if (tokenService.baseurl.value == null) {
await tokenService.saveBaseUrl('https://api.dam.rasadyar.net/'); await tokenService.saveBaseUrl('https://api.dam.rasadyar.net/');
} }
// First register AppInterceptor with lazy callbacks
diLiveStock.registerLazySingleton<AuthRemoteDataSource>(
() => AuthRemoteDataSourceImp(diLiveStock.get<DioRemote>()),
);
diLiveStock.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImp(diLiveStock.get<AuthRemoteDataSource>()),
);
diLiveStock.registerLazySingleton<AppInterceptor>( diLiveStock.registerLazySingleton<AppInterceptor>(
() => AppInterceptor( () => AppInterceptor(
refreshTokenCallback: () async { refreshTokenCallback: () async {
// Use lazy access to avoid circular dependency
final authRepository = diLiveStock.get<AuthRepository>(); final authRepository = diLiveStock.get<AuthRepository>();
final hasAuthenticated = await authRepository.hasAuthenticated(); final hasAuthenticated = await authRepository.hasAuthenticated();
if (hasAuthenticated) { if (hasAuthenticated) {
@@ -53,13 +43,26 @@ Future<void> setupLiveStockDI() async {
), ),
); );
// Register DioRemote with the interceptor
diLiveStock.registerLazySingleton<DioRemote>( diLiveStock.registerLazySingleton<DioRemote>(
() => DioRemote( () => DioRemote(
baseUrl: tokenService.baseurl.value, baseUrl: tokenService.baseurl.value,
interceptors: diLiveStock.get<AppInterceptor>(), // interceptors: diLiveStock.get<AppInterceptor>(),
), ),
); );
// Initialize DioRemote
await diLiveStock.get<DioRemote>().init(); await diLiveStock.get<DioRemote>().init();
// Now register the data source and repository
diLiveStock.registerLazySingleton<AuthRemoteDataSource>(
() => AuthRemoteDataSourceImp(diLiveStock.get<DioRemote>()),
);
diLiveStock.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImp(diLiveStock.get<AuthRemoteDataSource>()),
);
diLiveStock.registerLazySingleton<ImagePicker>(() => ImagePicker());
await diLiveStock.allReady();
} }

View File

@@ -122,7 +122,7 @@ class AuthLogic extends GetxController with GetTickerProviderStateMixin {
final loginRequestModel = _buildLoginRequest(); final loginRequestModel = _buildLoginRequest();
isLoading.value = true; isLoading.value = true;
await safeCall<AuthResponseModel?>( await safeCall<AuthResponseModel?>(
call: () async => authRepository.login(authRequest: loginRequestModel.toJson()), call: () async => await authRepository.login(authRequest: loginRequestModel.toJson()),
onSuccess: (result) async { onSuccess: (result) async {
await tokenStorageService.saveModule(_module); await tokenStorageService.saveModule(_module);
await tokenStorageService.saveRefreshToken(result?.refresh ?? ''); await tokenStorageService.saveRefreshToken(result?.refresh ?? '');

View File

@@ -17,27 +17,29 @@ class AuthPage extends GetView<AuthLogic> {
children: [ children: [
Assets.vec.bgAuthSvg.svg(fit: BoxFit.fill), Assets.vec.bgAuthSvg.svg(fit: BoxFit.fill),
Padding( Center(
padding: EdgeInsets.symmetric(horizontal: 10.r), child: Padding(
child: FadeTransition( padding: EdgeInsets.symmetric(horizontal: 10.r),
opacity: controller.textAnimation, child: FadeTransition(
child: Column( opacity: controller.textAnimation,
mainAxisSize: MainAxisSize.min, child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.start,
spacing: 12, crossAxisAlignment: CrossAxisAlignment.center,
children: [ spacing: 12,
Text( children: [
'به سامانه رصدیار خوش آمدید!', Text(
textAlign: TextAlign.right, 'به سامانه رصدیار خوش آمدید!',
style: AppFonts.yekan25Bold.copyWith(color: Colors.white), textAlign: TextAlign.right,
), style: AppFonts.yekan25Bold.copyWith(color: Colors.white),
Text( ),
'سامانه رصد و پایش زنجیره تامین، تولید و توزیع کالا های اساسی', Text(
textAlign: TextAlign.center, 'سامانه رصد و پایش زنجیره تامین، تولید و توزیع کالا های اساسی',
style: AppFonts.yekan16.copyWith(color: Colors.white), textAlign: TextAlign.center,
), style: AppFonts.yekan16.copyWith(color: Colors.white),
], ),
],
),
), ),
), ),
), ),

View File

@@ -1,8 +1,7 @@
import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_livestock/presentation/widgets/base_page/logic.dart';
class MapLogic extends GetxController { class MapLogic extends GetxController {
var ss = Get.find<DraggableBottomSheetController>(); var ss = Get.find<DraggableBottomSheetController>();
BaseLogic baseLogic = Get.find<BaseLogic>();
} }

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_livestock/presentation/page/map/widget/map_widget/view.dart'; import 'package:rasadyar_livestock/presentation/page/map/widget/map_widget/view.dart';
import 'package:rasadyar_livestock/presentation/widgets/base_page/view.dart';
import 'logic.dart'; import 'logic.dart';
@@ -9,6 +10,158 @@ class MapPage extends GetView<MapLogic> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold(body: Stack(children: [MapWidget()])); return BasePage(
hasSearch: true,
hasFilter: true,
hasBack: false,
defaultSearch: false,
filteringWidget: filterWidget(showIndex: 3.obs, filterIndex: 5.obs),
widgets: [MapWidget()],
);
}
Widget filterWidget({required RxInt filterIndex, required RxInt showIndex}) {
return BaseBottomSheet(
height: Get.height * 0.5,
child: Container(color: Colors.red),
);
}
BaseBottomSheet searchWidget() {
return BaseBottomSheet(
height: Get.height * 0.85,
rootChild: Column(
spacing: 8,
children: [
Row(
spacing: 12,
children: [
Expanded(
child: RTextField(
height: 40,
borderColor: AppColor.blackLight,
suffixIcon: ObxValue(
(data) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: (data.value == null)
? Assets.vec.searchSvg.svg(
width: 10,
height: 10,
colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn),
)
: IconButton(
onPressed: () {
controller.baseLogic.searchTextController.clear();
controller.baseLogic.searchValue.value = null;
controller.baseLogic.isSearchSelected.value = false;
//controller.mapLogic.hasFilterOrSearch.value = false;
//controller.searchedPoultryLocation.value = Resource.initial();
},
enableFeedback: true,
padding: EdgeInsets.zero,
iconSize: 24,
splashRadius: 50,
icon: Assets.vec.closeCircleSvg.svg(
width: 20,
height: 20,
colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn),
),
),
),
controller.baseLogic.searchValue,
),
hintText: 'جستجو کنید ...',
hintStyle: AppFonts.yekan16.copyWith(color: AppColor.blueNormal),
filledColor: Colors.white,
filled: true,
controller: controller.baseLogic.searchTextController,
onChanged: (val) => controller.baseLogic.searchValue.value = val,
),
),
GestureDetector(
onTap: () {
Get.back();
},
child: Assets.vec.mapSvg.svg(
width: 24.w,
height: 24.h,
colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn),
),
),
],
),
/* Expanded(
child: ObxValue((rxData) {
final resource = rxData.value;
final status = resource.status;
final items = resource.data;
final message = resource.message ?? 'خطا در بارگذاری';
if (status == ResourceStatus.initial) {
return Center(child: Text('ابتدا جستجو کنید'));
}
if (status == ResourceStatus.loading) {
return const Center(child: CircularProgressIndicator());
}
if (status == ResourceStatus.error) {
return Center(child: Text(message));
}
if (items == null || items.isEmpty) {
return Center(child: EmptyWidget());
}
return ListView.separated(
itemCount: items.length,
separatorBuilder: (context, index) => SizedBox(height: 8),
itemBuilder: (context, index) {
final item = items[index]; // اگر item استفاده نمیشه، می‌تونه حذف بشه
return ListItem2(
index: index,
labelColor: AppColor.blueLight,
labelIcon: Assets.vec.cowSvg.path,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
item.unitName ?? 'N/A',
style: AppFonts.yekan10.copyWith(color: AppColor.blueNormal),
),
Text(
item.user?.fullname ?? '',
style: AppFonts.yekan12.copyWith(color: AppColor.darkGreyDarkHover),
),
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'جوجه ریزی فعال',
style: AppFonts.yekan10.copyWith(color: AppColor.blueNormal),
),
Text(
(item.hatching != null && item.hatching!.isNotEmpty)
? 'دارد'
: 'ندراد',
style: AppFonts.yekan12.copyWith(color: AppColor.darkGreyDarkHover),
),
],
),
],
),
);
},
);
}, controller.searchedPoultryLocation),
),*/
],
),
);
} }
} }

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_livestock/presentation/routes/app_pages.dart';
import 'logic.dart'; import 'logic.dart';
@@ -172,8 +173,40 @@ class MapWidget extends GetView<MapWidgetLogic> {
.map( .map(
(element) => Marker( (element) => Marker(
point: element, point: element,
child: FaIcon(FontAwesomeIcons.locationPin, color: AppColor.error), child: IconButton(
onPressed: () {
Get.bottomSheet(
detailsBottomSheet(),
isScrollControlled: true,
isDismissible: true,
ignoreSafeArea: false,
);
},
icon: FaIcon(FontAwesomeIcons.locationPin, color: AppColor.error),
),
), ),
) )
.toList(); .toList();
Widget detailsBottomSheet() {
return BaseBottomSheet(
height: 250.h,
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 20,
children: [
Text('مشخصات محل', style: AppFonts.yekan16Bold),
// Add more details here
RElevated(
text: 'ایجاد بازرسی',
width: Get.width,
height: 40.h,
onPressed: () {
Get.toNamed(LiveStockRoutes.requestTagging);
},
),
],
),
);
}
} }

View File

@@ -1,10 +1,32 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_livestock/presentation/page/root/logic.dart'; import 'package:rasadyar_livestock/injection/live_stock_di.dart';
class RequestTaggingLogic extends GetxController { class RequestTaggingLogic extends GetxController {
RxInt currentIndex = 0.obs;
final int maxStep = 3;
RxBool nextButtonEnabled = true.obs;
final TextEditingController phoneController = TextEditingController();
final TextEditingController fullNameController = TextEditingController();
final TextEditingController addressController = TextEditingController();
ImagePicker imagePicker = diLiveStock.get<ImagePicker>();
Rxn<XFile> rancherImage = Rxn<XFile>(null);
@override
void onInit() {
super.onInit();
setUpTextControllerListeners();
setUpNextButtonListeners();
ever(rancherImage, (callback) {
setUpNextButtonListeners();
});
}
final TextEditingController phoneController = TextEditingController();
@override @override
void onReady() { void onReady() {
super.onReady(); super.onReady();
@@ -14,4 +36,68 @@ final TextEditingController phoneController = TextEditingController();
void onClose() { void onClose() {
super.onClose(); super.onClose();
} }
void onNext() {
if (currentIndex.value < maxStep) {
if (currentIndex.value == 0) {}
currentIndex.value++;
}
}
void onPrevious() {
if (currentIndex.value > 0) {
currentIndex.value--;
}
}
void setUpNextButtonListeners() {
if (currentIndex.value == 0) {
nextButtonEnabled.value =
phoneController.text.isNotEmpty &&
fullNameController.text.isNotEmpty &&
addressController.text.isNotEmpty &&
rancherImage.value != null;
return;
}
}
void setUpTextControllerListeners() {
phoneController.addListener(setUpNextButtonListeners);
fullNameController.addListener(setUpNextButtonListeners);
addressController.addListener(setUpNextButtonListeners);
}
Future<void> pickImage() async {
rancherImage.value = await imagePicker.pickImage(
source: ImageSource.camera,
imageQuality: 60,
maxWidth: 1080,
maxHeight: 720,
);
getFileSizeInKB(rancherImage.value?.path ?? '', tag: 'Picked');
}
Future<void> cropImage() async {
if (rancherImage.value == null) return;
final CroppedFile? cropped = await ImageCropper().cropImage(
sourcePath: rancherImage.value!.path,
maxWidth: 1080,
maxHeight: 720,
compressQuality: 60,
);
if (cropped == null) return;
rancherImage.value = XFile(cropped.path);
getFileSizeInKB(rancherImage.value?.path ?? '', tag: 'Cropped');
}
void getFileSizeInKB(String filePath, {String? tag}) {
final file = File(filePath);
final bytes = file.lengthSync();
var size = (bytes / 1024).ceil();
iLog('${tag ?? 'Picked'} image Size: $size');
}
} }

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_core/core.dart';
@@ -19,91 +21,272 @@ class RequestTaggingPage extends GetView<RequestTaggingLogic> {
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15), padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15),
child: Column( child: ObxValue((index) {
children: [ return Column(
RTextField( children: [
controller: controller.phoneController, Expanded(child: _buildStep(index.value)),
label: 'تلفن دامدار', nextOrPreviousWidget(),
],
);
}, controller.currentIndex),
),
);
}
Row nextOrPreviousWidget() {
return Row(
spacing: 10,
children: [
ObxValue(
(data) => Expanded(
flex: 2,
child: RElevated(
height: 40.h,
enabled: data.value,
onPressed: controller.onNext,
child: Text('بعدی'),
backgroundColor: AppColor.blueNormal,
), ),
),
controller.nextButtonEnabled,
),
Expanded(
child: ROutlinedElevated(
enabled: controller.currentIndex.value > 0,
onPressed: controller.onPrevious,
child: Text('قبلی'),
borderColor: AppColor.error,
),
),
],
);
}
SizedBox( Widget _buildStep(int index) {
width: Get.width, switch (index) {
height: 356, case 0:
child: Card( return firstStepWidget();
color: Colors.white, default:
child: Padding( return Center(
padding: const EdgeInsets.all(16.0), child: Text(
child: Column( 'مرحله $index در دست توسعه است',
children: [ style: AppFonts.yekan16.copyWith(color: AppColor.redNormal),
Expanded( textDirection: TextDirection.rtl,
child: Container( ),
width: Get.width, );
}
}
decoration: BoxDecoration( Widget firstStepWidget() {
color: AppColor.lightGreyNormal, return Form(
borderRadius: BorderRadius.circular(8), child: Column(
), spacing: 16,
child: Center( children: [
child: Assets.images.placeHolder.image( Container(
height: 150, padding: EdgeInsets.all(8),
width: 200, decoration: BoxDecoration(
), border: Border.all(color: AppColor.lightGreyNormal, width: 1),
), borderRadius: BorderRadius.circular(8),
), ),
child: Column(
spacing: 10,
children: [
Row(children: [Text('اطلاعات دامدار', style: AppFonts.yekan16Bold)]),
RTextField(controller: controller.fullNameController, label: 'نام ونام خانوادگی'),
RTextField(controller: controller.phoneController, label: 'تلفن'),
RTextField(controller: controller.addressController, label: 'ادرس'),
],
),
),
SizedBox(
width: Get.width,
height: 356.h,
child: Container(
padding: EdgeInsets.all(8.r),
decoration: BoxDecoration(
border: Border.all(color: AppColor.lightGreyNormal, width: 1.w),
borderRadius: BorderRadius.circular(8.r),
),
child: Column(
spacing: 8,
children: [
Row(children: [Text('تصویر دامدار', style: AppFonts.yekan16Bold)]),
Expanded(
child: Container(
width: Get.width,
decoration: BoxDecoration(
color: AppColor.lightGreyNormal,
borderRadius: BorderRadius.circular(8.r),
), ),
SizedBox(height: 15), child: Center(
Container( child: ObxValue((tmpImage) {
width: Get.width, if (tmpImage.value == null) {
height: 40, return Assets.vec.placeHolderSvg.svg(height: 150.h, width: 200.w);
clipBehavior: Clip.antiAlias, } else {
decoration: ShapeDecoration( return Image.file(File(tmpImage.value!.path), fit: BoxFit.cover);
color: AppColor.blueNormal, }
shape: RoundedRectangleBorder( }, controller.rancherImage),
borderRadius: BorderRadius.circular(8),
),
),
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
' تصویر گله',
style: AppFonts.yekan14.copyWith(
color: Colors.white,
),
),
Icon(
CupertinoIcons.arrow_up_doc,
color: Colors.white,
),
],
),
),
), ),
], ),
), ),
), GestureDetector(
onTap: () async {
await controller.pickImage();
await showCropDialog();
},
child: Container(
width: Get.width,
height: 40.h,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: AppColor.blueNormal,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Padding(
padding: EdgeInsets.all(10.r),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(' دوربین', style: AppFonts.yekan14.copyWith(color: Colors.white)),
Icon(CupertinoIcons.arrow_up_doc, color: Colors.white),
],
),
),
),
),
],
), ),
), ),
),
Spacer(), Spacer(),
/* RElevated(
text: 'ارسال تصویر گله',
onPressed: () {
Get.toNamed(LiveStockRoutes.tagging);
},
height: 40,
isFullWidth: true,
backgroundColor: AppColor.greenNormal,
textStyle: AppFonts.yekan16.copyWith(color: Colors.white),
),*/
],
),
);
}
Future<void> showCropDialog() async {
await Get.dialog(
Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'آیا نیازی به برش تصویر دارید؟',
style: AppFonts.yekan16Bold,
textAlign: TextAlign.center,
),
RElevated( const SizedBox(height: 24),
text: 'ارسال تصویر گله',
onPressed: () { Row(
Get.toNamed(LiveStockRoutes.tagging); mainAxisAlignment: MainAxisAlignment.end,
}, spacing: 12.w,
height: 40, children: [
isFullWidth: true, RElevated(
backgroundColor: AppColor.greenNormal, height: 40.h,
textStyle: AppFonts.yekan16.copyWith(color: Colors.white), onPressed: () async {
), Get.back();
], await controller.cropImage();
},
child: const Text('بله'),
),
ROutlinedElevated(
onPressed: () => Get.back(),
child: const Text('خیر'),
borderColor: AppColor.error,
),
],
),
],
),
), ),
), ),
); );
} }
Column secondStepWidget() {
return Column(
children: [
RTextField(controller: controller.phoneController, label: 'تلفن دامدار'),
SizedBox(
width: Get.width,
height: 356,
child: Card(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Expanded(
child: Container(
width: Get.width,
decoration: BoxDecoration(
color: AppColor.lightGreyNormal,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Assets.images.placeHolder.image(height: 150, width: 200),
),
),
),
SizedBox(height: 15),
Container(
width: Get.width,
height: 40,
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: AppColor.blueNormal,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(' تصویر گله', style: AppFonts.yekan14.copyWith(color: Colors.white)),
Icon(CupertinoIcons.arrow_up_doc, color: Colors.white),
],
),
),
),
],
),
),
),
),
Spacer(),
RElevated(
text: 'ارسال تصویر گله',
onPressed: () {
Get.toNamed(LiveStockRoutes.tagging);
},
height: 40,
isFullWidth: true,
backgroundColor: AppColor.greenNormal,
textStyle: AppFonts.yekan16.copyWith(color: Colors.white),
),
],
);
}
} }

View File

@@ -26,7 +26,7 @@ class RootLogic extends GetxController {
ProfilePage(), ProfilePage(),
]; ];
RxInt currentIndex = 1.obs; RxInt currentIndex = 0.obs;
@override @override
void onReady() { void onReady() {

View File

@@ -11,6 +11,7 @@ import 'package:rasadyar_livestock/presentation/page/root/logic.dart';
import 'package:rasadyar_livestock/presentation/page/root/view.dart'; import 'package:rasadyar_livestock/presentation/page/root/view.dart';
import 'package:rasadyar_livestock/presentation/page/tagging/logic.dart'; import 'package:rasadyar_livestock/presentation/page/tagging/logic.dart';
import 'package:rasadyar_livestock/presentation/page/tagging/view.dart'; import 'package:rasadyar_livestock/presentation/page/tagging/view.dart';
import 'package:rasadyar_livestock/presentation/widgets/base_page/logic.dart';
import 'package:rasadyar_livestock/presentation/widgets/captcha/logic.dart'; import 'package:rasadyar_livestock/presentation/widgets/captcha/logic.dart';
part 'app_routes.dart'; part 'app_routes.dart';
@@ -38,6 +39,7 @@ sealed class LiveStockPages {
Get.lazyPut(() => ProfileLogic()); Get.lazyPut(() => ProfileLogic());
Get.lazyPut(() => ProfileLogic()); Get.lazyPut(() => ProfileLogic());
Get.lazyPut(() => MapWidgetLogic()); Get.lazyPut(() => MapWidgetLogic());
Get.lazyPut(() => BaseLogic());
}), }),
children: [ children: [
/*GetPage( /*GetPage(

View File

@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_livestock/presentation/widgets/base_page/logic.dart';
RAppBar liveStockAppBar({
bool hasBack = true,
bool hasFilter = true,
bool hasSearch = true,
bool isBase = false,
VoidCallback? onBackPressed,
GestureTapCallback? onFilterTap,
GestureTapCallback? onSearchTap,
}) {
return RAppBar(
hasBack: isBase == true ? false : hasBack,
onBackPressed: onBackPressed,
leadingWidth: 155,
leading: Row(
mainAxisSize: MainAxisSize.min,
spacing: 6,
children: [
Text('رصددام', style: AppFonts.yekan16Bold.copyWith(color: Colors.white)),
Assets.vec.appBarInspectionSvg.svg(width: 24, height: 24),
],
),
additionalActions: [
if (!isBase && hasSearch) searchWidget(onSearchTap),
SizedBox(width: 8),
if (!isBase && hasFilter) filterWidget(onFilterTap),
SizedBox(width: 8),
],
);
}
GestureDetector filterWidget(GestureTapCallback? onFilterTap) {
return GestureDetector(
onTap: onFilterTap,
child: Stack(
alignment: Alignment.topRight,
children: [
Assets.vec.filterOutlineSvg.svg(
width: 20,
height: 20,
colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
Obx(() {
final controller = Get.find<BaseLogic>();
return Visibility(
visible: controller.isFilterSelected.value,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
),
);
}),
],
),
);
}
GestureDetector searchWidget(GestureTapCallback? onSearchTap) {
return GestureDetector(
onTap: onSearchTap,
child: Stack(
alignment: Alignment.topRight,
children: [
Assets.vec.searchSvg.svg(
width: 24,
height: 24,
colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
Obx(() {
final controller = Get.find<BaseLogic>();
return Visibility(
visible: controller.searchValue.value != null,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
),
);
}),
],
),
);
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/cupertino.dart';
import 'package:rasadyar_core/core.dart';
class BaseLogic extends GetxController {
final RxBool isFilterSelected = false.obs;
final RxBool isSearchSelected = false.obs;
final TextEditingController searchTextController = TextEditingController();
final RxnString searchValue = RxnString();
void setSearchCallback(void Function(String)? onSearchChanged) {
debounce<String?>(searchValue, (val) {
if (val != null && val.trim().isNotEmpty) {
onSearchChanged?.call(val);
}
}, time: const Duration(milliseconds: 600));
}
void toggleFilter() {
isFilterSelected.value = !isFilterSelected.value;
}
void toggleSearch() {
isSearchSelected.value = !isSearchSelected.value;
}
}

View File

@@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_livestock/presentation/widgets/app_bar/i_app_bar.dart';
import 'package:rasadyar_livestock/presentation/widgets/search.dart';
import 'logic.dart';
class BasePage extends StatefulWidget {
const BasePage({
super.key,
this.routes,
required this.widgets,
this.routesWidget,
this.floatingActionButtonLocation,
this.floatingActionButton,
this.onSearchChanged,
this.hasBack = true,
this.hasFilter = true,
this.hasSearch = true,
this.isBase = false,
this.defaultSearch = true,
this.onBackPressed,
this.onFilterTap,
this.onSearchTap,
this.filteringWidget,
});
final List<String>? routes;
final Widget? routesWidget;
final bool defaultSearch;
final List<Widget> widgets;
final FloatingActionButtonLocation? floatingActionButtonLocation;
final Widget? floatingActionButton;
final Widget? filteringWidget;
final void Function(String?)? onSearchChanged;
final bool hasBack;
final bool hasFilter;
final bool hasSearch;
final bool isBase;
final VoidCallback? onBackPressed;
final GestureTapCallback? onFilterTap;
final GestureTapCallback? onSearchTap;
@override
State<BasePage> createState() => _BasePageState();
}
class _BasePageState extends State<BasePage> {
BaseLogic get controller => Get.find<BaseLogic>();
Worker? filterWorker;
bool _isBottomSheetOpen = false;
@override
void initState() {
super.initState();
/* filterWorker = ever(controller.isFilterSelected, (bool isSelected) {
if (!mounted) return;
if (isSelected && widget.filteringWidget != null) {
// بررسی اینکه آیا bottomSheet از قبل باز است یا نه
if (_isBottomSheetOpen) {
controller.isFilterSelected.value = false;
return;
}
// بررسی اینکه آیا route فعلی current است یا نه
if (ModalRoute.of(context)?.isCurrent != true) {
controller.isFilterSelected.value = false;
return;
}
_isBottomSheetOpen = true;
Get.bottomSheet(
widget.filteringWidget!,
isScrollControlled: true,
isDismissible: true,
enableDrag: true,
).then((_) {
// تنظیم مقدار به false بعد از بسته شدن bottomSheet
if (mounted) {
_isBottomSheetOpen = false;
controller.isFilterSelected.value = false;
}
});
}
});*/
}
@override
void dispose() {
filterWorker?.dispose();
super.dispose();
}
void _onFilterTap() {
if (widget.hasFilter && widget.filteringWidget != null) {
final currentRoute = ModalRoute.of(context);
if (currentRoute?.isCurrent != true) {
return;
}
Get.bottomSheet(
widget.filteringWidget!,
isScrollControlled: true,
isDismissible: true,
enableDrag: true,
);
}
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) => widget.onBackPressed,
child: Scaffold(
backgroundColor: AppColor.bgLight,
appBar: liveStockAppBar(
hasBack: widget.isBase ? false : widget.hasBack,
onBackPressed: widget.onBackPressed,
hasFilter: widget.hasFilter,
hasSearch: widget.hasSearch,
isBase: widget.isBase,
onFilterTap: widget.hasFilter ? _onFilterTap : null,
onSearchTap: widget.hasSearch ? () => controller.toggleSearch() : null,
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//widget.routesWidget != null ? widget.routesWidget! : buildPageRoute(widget.routes!),
if (!widget.isBase && widget.hasSearch && widget.defaultSearch) ...{
SearchWidget(onSearchChanged: widget.onSearchChanged),
},
...widget.widgets,
],
),
floatingActionButtonLocation:
widget.floatingActionButtonLocation ?? FloatingActionButtonLocation.startFloat,
floatingActionButton: widget.floatingActionButton,
),
);
}
}

View File

@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
import 'base_page/logic.dart';
class SearchWidget extends StatefulWidget {
const SearchWidget({super.key, this.onSearchChanged});
final void Function(String?)? onSearchChanged;
@override
State<SearchWidget> createState() => _SearchWidgetState();
}
class _SearchWidgetState extends State<SearchWidget> {
late final BaseLogic controller;
final TextEditingController textEditingController = TextEditingController();
@override
void initState() {
super.initState();
controller = Get.find<BaseLogic>();
controller.setSearchCallback(widget.onSearchChanged);
}
@override
Widget build(BuildContext context) {
return ObxValue((data) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: data.value ? 40 : 0,
child: Visibility(
visible: data.value,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: RTextField(
height: 40,
borderColor: AppColor.blackLight,
suffixIcon: ObxValue(
(data) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: (data.value == null)
? Assets.vec.searchSvg.svg(
width: 10,
height: 10,
colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn),
)
: IconButton(
onPressed: () {
textEditingController.clear();
controller.searchValue.value = null;
controller.isSearchSelected.value = false;
widget.onSearchChanged?.call(null);
},
enableFeedback: true,
padding: EdgeInsets.zero,
iconSize: 24,
splashRadius: 50,
icon: Assets.vec.closeCircleSvg.svg(
width: 20,
height: 20,
colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn),
),
),
),
controller.searchValue,
),
hintText: 'جستجو کنید ...',
hintStyle: AppFonts.yekan16.copyWith(color: AppColor.blueNormal),
filledColor: Colors.white,
filled: true,
controller: textEditingController,
onChanged: (val) => controller.searchValue.value = val,
),
),
),
);
}, controller.isSearchSelected);
}
}

View File

@@ -749,6 +749,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.4" version: "4.5.4"
image_cropper:
dependency: transitive
description:
name: image_cropper
sha256: "4e9c96c029eb5a23798da1b6af39787f964da6ffc78fd8447c140542a9f7c6fc"
url: "https://pub.dev"
source: hosted
version: "9.1.0"
image_cropper_for_web:
dependency: transitive
description:
name: image_cropper_for_web
sha256: fd81ebe36f636576094377aab32673c4e5d1609b32dec16fad98d2b71f1250a9
url: "https://pub.dev"
source: hosted
version: "6.1.0"
image_cropper_platform_interface:
dependency: transitive
description:
name: image_cropper_platform_interface
sha256: "6ca6b81769abff9a4dcc3bbd3d75f5dfa9de6b870ae9613c8cd237333a4283af"
url: "https://pub.dev"
source: hosted
version: "7.1.0"
image_picker: image_picker:
dependency: transitive dependency: transitive
description: description: