feat : new main page inspection

This commit is contained in:
2025-07-24 16:57:34 +03:30
parent e5142a258c
commit 42727c7eec
30 changed files with 1784 additions and 238 deletions

View File

@@ -11,7 +11,7 @@ class SupervisionFilterPage extends GetView<InspectorFilterLogic> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: RAppBar.noBack(title: 'نقشه', additionalActions: [_searchButton(), _filterButton()]),
appBar: RAppBar(title: 'نقشه', additionalActions: [_searchButton(), _filterButton()]),
body: PopScope(
canPop: !controller.bottomSheetManager.isAnyVisible,
onPopInvokedWithResult: (didPop, result) {

View File

@@ -0,0 +1,151 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_inspection/data/utils/marker_generator.dart';
import 'package:rasadyar_inspection/presentation/widget/base_page/logic.dart';
import '../filter/view.dart';
class InspectionMapLogic extends GetxController with GetTickerProviderStateMixin {
final BaseLogic baseLogic = Get.find<BaseLogic>();
Rx<LatLng> currentLocation = LatLng(35.824891, 50.948025).obs;
RxList<LatLng> allMarkers = <LatLng>[].obs;
RxList<LatLng> markers = <LatLng>[].obs;
Timer? _debounceTimer;
RxBool isLoading = false.obs;
RxInt filterIndex = 0.obs;
RxInt showIndex = 0.obs;
bool showSlideHint = true;
late Rx<SlidableController> slidController;
Rx<MapController> mapController = MapController().obs;
late final AnimatedMapController animatedMapController;
late DraggableBottomSheetController filterBottomSheetController;
late DraggableBottomSheetController selectedLocationBottomSheetController;
late DraggableBottomSheetController detailsLocationBottomSheetController;
late final BottomSheetManager bottomSheetManager;
Future<void> determineCurrentPosition() async {
isLoading.value = true;
final position = await Geolocator.getCurrentPosition(
locationSettings: AndroidSettings(accuracy: LocationAccuracy.best),
);
final latLng = LatLng(position.latitude, position.longitude);
currentLocation.value = latLng;
markers.add(latLng);
animatedMapController.animateTo(
dest: latLng,
zoom: 18,
curve: Curves.easeInOut,
duration: const Duration(seconds: 1),
);
isLoading.value = false;
}
void debouncedUpdateVisibleMarkers({required LatLng center}) {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
final filtered = filterNearbyMarkers({
'markers': allMarkers,
'centerLat': center.latitude,
'centerLng': center.longitude,
'radius': 2000.0,
});
markers.addAll(filtered);
});
}
List<LatLng> filterNearbyMarkers(Map<String, dynamic> args) {
final List<LatLng> rawMarkers = args['markers'];
final double centerLat = args['centerLat'];
final double centerLng = args['centerLng'];
final double radiusInMeters = args['radius'];
final center = LatLng(centerLat, centerLng);
final distance = Distance();
return rawMarkers
.where((marker) => distance(center, marker) <= radiusInMeters)
.toList();
}
Future<void> generatedMarkers() async {
final generatedMarkers = await generateLocationsUsingCompute(100000);
allMarkers.value = generatedMarkers;
}
@override
void onInit() {
super.onInit();
animatedMapController = AnimatedMapController(
vsync: this,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
cancelPreviousAnimations: true,
);
filterBottomSheetController = DraggableBottomSheetController(
initialHeight: 350,
minHeight: 200,
maxHeight: Get.height * 0.5,
);
selectedLocationBottomSheetController = DraggableBottomSheetController(
initialHeight: 200,
minHeight: 100,
maxHeight: 200,
);
detailsLocationBottomSheetController = DraggableBottomSheetController(
initialHeight: Get.height * 0.5,
minHeight: Get.height * 0.37,
maxHeight: Get.height * 0.5,
);
slidController = SlidableController(this).obs;
bottomSheetManager = BottomSheetManager({
filterBottomSheetController:
() => filterWidget(filterIndex: filterIndex, showIndex: showIndex),
selectedLocationBottomSheetController:
() => selectedLocationWidget(
showHint:
selectedLocationBottomSheetController.isVisible.value &&
showSlideHint,
sliderController: slidController.value,
trigger: triggerSlidableAnimation,
toggle: selectedLocationBottomSheetController.toggle,
),
detailsLocationBottomSheetController: () => markerDetailsWidget(),
});
}
@override
void onReady() {
super.onReady();
determineCurrentPosition();
generatedMarkers();
}
Future<void> triggerSlidableAnimation() async {
await Future.delayed(Duration(milliseconds: 200));
await slidController.value.openEndActionPane();
await Future.delayed(Duration(milliseconds: 200));
await slidController.value.close();
showSlideHint = false;
}
@override
void onClose() {
slidController.close();
super.onClose();
}
}

View File

@@ -0,0 +1,650 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_inspection/presentation/routes/app_routes.dart';
import 'package:rasadyar_inspection/presentation/widget/base_page/view.dart';
import 'package:rasadyar_inspection/presentation/widget/search.dart';
import 'logic.dart';
class InspectionMapPage extends GetView<InspectionMapLogic> {
const InspectionMapPage({super.key});
@override
Widget build(BuildContext context) {
return BasePage(
hasSearch: true,
hasFilter: true,
hasBack: false,
defaultSearch: false,
filteringWidget: filterWidget(showIndex: 3.obs, filterIndex: 5.obs),
onSearchTap: () {
controller.baseLogic.isSearchSelected.value = !controller.baseLogic.isSearchSelected.value;
},
widgets: [_buildMap()],
floatingActionButton: _buildGpsButton(),
);
}
Widget _buildMap() {
return Expanded(
child: Stack(
fit: StackFit.expand,
children: [
ObxValue((currentLocation) {
return FlutterMap(
mapController: controller.animatedMapController.mapController,
options: MapOptions(
initialCenter: currentLocation.value,
initialZoom: 18,
onPositionChanged: (camera, hasGesture) {
controller.debouncedUpdateVisibleMarkers(center: camera.center);
},
),
children: [
TileLayer(urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'),
ObxValue((markers) {
return MarkerLayer(
markers: markers
.map(
(e) => markerWidget(
marker: e,
onTap: () {
Get.bottomSheet(
selectedLocationWidget(
showHint: false,
sliderController: controller.slidController.value,
trigger: () {},
toggle: () {},
),
isScrollControlled: true,
enableDrag: true,
backgroundColor: Colors.transparent,
);
},
),
)
.toList(),
);
}, controller.markers),
],
);
}, controller.currentLocation),
Positioned(
top: 10,
left: 20,
right: 20,
child: ObxValue((data) {
if (data.value) {
return SearchWidget(
onSearchChanged: (data) {
controller.baseLogic.searchValue.value = data;
},
);
} else {
return SizedBox.shrink();
}
}, controller.baseLogic.isSearchSelected),
),
],
),
);
}
Widget _buildGpsButton() {
return ObxValue((data) {
return RFab(
backgroundColor: AppColor.greenNormal,
isLoading: data.value,
icon: Assets.vec.gpsSvg.svg(width: 40.w, height: 40.h),
onPressed: () async => await controller.determineCurrentPosition(),
);
}, controller.isLoading);
}
}
Marker markerWidget({required LatLng marker, required VoidCallback onTap}) {
return Marker(
point: marker,
child: GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: SizedBox(
width: 36,
height: 36,
child: Assets.vec.mapMarkerSvg.svg(width: 30, height: 30),
),
),
);
}
Widget filterWidget({required RxInt filterIndex, required RxInt showIndex}) {
return BaseBottomSheet(
height: Get.height * 0.5,
child: Column(
spacing: 16,
children: [
SizedBox(height: 1),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
spacing: 16,
children: [
cardWithLabel(
title: 'اصناف',
count: 1234567,
icon: Assets.vec.shopSvg.svg(width: 24.w, height: 24.h),
backgroundColor: AppColor.greenLight,
backgroundBadgeColor: AppColor.greenLightActive,
),
cardWithLabel(
title: 'دامداران',
count: 1234567,
icon: Assets.vec.peopleSvg.svg(width: 24.w, height: 24.h),
backgroundColor: AppColor.blueLight,
backgroundBadgeColor: AppColor.blueLightActive,
),
cardWithLabel(
title: 'مرغداران',
count: 1234567,
icon: Assets.vec.profile2Svg.svg(width: 24.w, height: 24.h),
backgroundColor: AppColor.redLight,
backgroundBadgeColor: AppColor.redLightActive,
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [
Text(
'فیلتر نمایش',
textAlign: TextAlign.center,
style: AppFonts.yekan13.copyWith(color: AppColor.blueNormal),
),
ObxValue((data) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 8,
children: [
customChip(
isSelected: data.value == 0,
onTap: (data) {
filterIndex.value = data;
},
index: 0,
title: 'دامداران',
),
customChip(
isSelected: data.value == 1,
title: 'مرغداران',
onTap: (data) {
filterIndex.value = data;
},
index: 1,
),
customChip(
isSelected: data.value == 2,
title: 'اصناف',
onTap: (data) {
filterIndex.value = data;
},
index: 2,
),
],
);
}, filterIndex),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [
Text(
'نمایش',
textAlign: TextAlign.center,
style: AppFonts.yekan13.copyWith(color: AppColor.blueNormal),
),
ObxValue((data) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 8,
children: [
customChip(
isSelected: data.value == 0,
title: 'نمایش همه',
onTap: (data) {
showIndex.value = data;
},
index: 0,
),
customChip(
isSelected: data.value == 1,
title: 'دارای تراکنش',
onTap: (data) {
showIndex.value = data;
},
index: 1,
),
customChip(
isSelected: data.value == 2,
title: 'بازرسی شده ها',
onTap: (data) {
showIndex.value = data;
},
index: 2,
),
customChip(
isSelected: data.value == 3,
title: 'بازرسی نشده ها',
onTap: (data) {
showIndex.value = data;
},
index: 3,
),
customChip(
isSelected: data.value == 4,
title: 'متخلفین',
onTap: (data) {
showIndex.value = data;
},
index: 4,
),
],
),
);
}, showIndex),
],
),
],
),
);
}
Widget cardWithLabel({
required String title,
required int count,
String unit = 'عدد',
required Widget icon,
required Color backgroundColor,
required Color backgroundBadgeColor,
}) {
return SizedBox(
width: 114.w,
height: 115.h,
child: Stack(
clipBehavior: Clip.antiAlias,
alignment: Alignment.topCenter,
children: [
Positioned(
bottom: 0,
right: 0,
left: 0,
child: Container(
width: 114.w,
height: 91.h,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(width: 0.25, color: AppColor.blackLightHover),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 6,
children: [
Text(title, style: AppFonts.yekan12.copyWith(color: AppColor.textColor)),
Text(
count.separatedByComma,
style: AppFonts.yekan16.copyWith(color: AppColor.textColor),
),
Text(unit, style: AppFonts.yekan12.copyWith(color: AppColor.textColor)),
],
),
),
),
Positioned(
top: 5.h,
child: Container(
width: 32.w,
height: 32.h,
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: backgroundBadgeColor,
borderRadius: BorderRadius.circular(50),
border: Border.all(color: AppColor.borderColor, width: 0.25),
),
child: Center(child: icon),
),
),
],
),
);
}
Widget markerDetailsWidget() {
return Container(
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.all(35),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: ShapeDecoration(
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Column(
spacing: 15,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
spacing: 12,
children: [
Text(
'داود خرم پور',
textAlign: TextAlign.center,
style: AppFonts.yekan16.copyWith(color: AppColor.darkGreyDarkHover),
),
Spacer(),
vecWidgetWithOnTap(
child: Assets.vec.mapSvg.svg(),
onTap: () {
Get.toNamed(InspectionRoutes.inspectionLocationDetails);
},
width: 24,
height: 24,
color: AppColor.blueNormal,
),
vecWidgetWithOnTap(
child: Assets.vec.messageAddSvg.svg(),
width: 24,
height: 24,
color: AppColor.greenNormal,
onTap: () {
Get.toNamed(InspectionRoutes.inspectionAddSupervision);
},
),
vecWidgetWithOnTap(
child: Assets.vec.securityTimeSvg.svg(),
color: AppColor.warning,
height: 24,
width: 24,
onTap: () {},
),
],
),
Container(
height: 32,
clipBehavior: Clip.antiAlias,
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: ShapeDecoration(
color: AppColor.blueLight,
shape: RoundedRectangleBorder(
side: BorderSide(width: 1, color: AppColor.blueLightHover),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'باقی مانده',
textAlign: TextAlign.center,
style: AppFonts.yekan14.copyWith(color: AppColor.darkGreyDarkHover),
),
Text(
'0 کیلوگرم',
textAlign: TextAlign.center,
style: AppFonts.yekan14.copyWith(color: AppColor.darkGreyDarkHover),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'شماره همراه',
textAlign: TextAlign.center,
style: AppFonts.yekan14.copyWith(color: AppColor.darkGreyDarkHover),
),
Text(
'0326598653',
textAlign: TextAlign.center,
style: AppFonts.yekan14.copyWith(color: AppColor.darkGreyDarkHover),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'آخرین فعالیت',
textAlign: TextAlign.center,
style: AppFonts.yekan14.copyWith(color: AppColor.darkGreyDarkHover),
),
Text(
'1409/12/12',
textAlign: TextAlign.center,
style: AppFonts.yekan14.copyWith(color: AppColor.darkGreyDarkHover),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'موجودی',
textAlign: TextAlign.center,
style: AppFonts.yekan14.copyWith(color: AppColor.darkGreyDarkHover),
),
Text(
'5کیلوگرم',
textAlign: TextAlign.center,
style: AppFonts.yekan14.copyWith(color: AppColor.darkGreyDarkHover),
),
],
),
...List.generate(
5,
(index) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'فروش رفته',
textAlign: TextAlign.center,
style: AppFonts.yekan14.copyWith(color: AppColor.darkGreyDarkHover),
),
Text(
'0 کیلوگرم',
textAlign: TextAlign.center,
style: AppFonts.yekan14.copyWith(color: AppColor.darkGreyDarkHover),
),
],
),
),
],
),
);
}
Widget customChip({
bool isSelected = false,
required String title,
required int index,
required Function(int) onTap,
}) {
return GestureDetector(
onTap: () {
onTap.call(index);
},
child: Container(
height: 32.h,
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 8.h),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: AppColor.whiteGreyNormal,
borderRadius: BorderRadius.circular(8),
border: Border.all(
width: 1,
color: isSelected ? AppColor.blueNormal : AppColor.blackLightActive,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 12.w,
height: 12.h,
child: Transform.scale(
scale: 0.70,
child: Checkbox(
value: isSelected,
side: BorderSide(color: AppColor.whiteDarkHover, width: 1),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
overlayColor: WidgetStateProperty.all<Color>(AppColor.blueNormal),
fillColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColor.blueNormal;
} else {
return AppColor.whiteGreyNormal;
}
}),
onChanged: (value) {
onTap.call(index);
},
),
),
),
SizedBox(width: 8.w),
Text(
title,
textAlign: TextAlign.center,
style: AppFonts.yekan12.copyWith(
color: isSelected ? AppColor.blueNormal : AppColor.whiteDarkHover,
),
),
],
),
),
);
}
Widget selectedLocationWidget({
required bool showHint,
required SlidableController sliderController,
required VoidCallback trigger,
required VoidCallback toggle,
}) {
if (showHint) {
trigger.call();
}
return BaseBottomSheet(
height: 150.h,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
child: Slidable(
key: Key('selectedLocationWidget'),
controller: sliderController,
endActionPane: ActionPane(
motion: StretchMotion(),
children: [
CustomSlidableAction(
onPressed: (context) {
Get.toNamed(InspectionRoutes.inspectionLocationDetails);
},
backgroundColor: AppColor.blueNormal,
foregroundColor: Colors.white,
padding: EdgeInsets.all(16),
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(8),
topRight: Radius.circular(8),
),
child: Assets.vec.mapSvg.svg(width: 24, height: 24),
),
CustomSlidableAction(
onPressed: (context) {
Get.toNamed(InspectionRoutes.inspectionAddSupervision);
},
backgroundColor: AppColor.greenNormal,
padding: EdgeInsets.all(16),
child: Assets.vec.messageAddSvg.svg(),
),
CustomSlidableAction(
onPressed: (context) {},
backgroundColor: AppColor.warning,
padding: EdgeInsets.all(16),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(8),
topLeft: Radius.circular(8),
),
child: Assets.vec.securityTimeSvg.svg(),
),
],
),
child: GestureDetector(
onTap: toggle,
child: Container(
height: 58,
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(width: 1, color: AppColor.blackLightHover),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
Text(
'داود خرم مهری پور',
style: AppFonts.yekan10.copyWith(color: AppColor.blueNormal),
),
Text(
'گوشت و مرغ',
style: AppFonts.yekan12.copyWith(color: AppColor.darkGreyDarkHover),
),
],
),
Column(
children: [
Text(
'باقی مانده',
style: AppFonts.yekan10.copyWith(color: AppColor.blueNormal),
),
Text(
'0 کیلوگرم',
style: AppFonts.yekan12.copyWith(color: AppColor.darkGreyDarkHover),
),
],
),
Assets.vec.scanBarcodeSvg.svg(),
],
),
),
),
),
),
);
}

View File

@@ -2,13 +2,14 @@ import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart' ;
import 'package:rasadyar_inspection/presentation/action/view.dart';
import 'package:rasadyar_inspection/presentation/filter/view.dart';
import 'package:rasadyar_inspection/presentation/inspection_map/view.dart';
import 'package:rasadyar_inspection/presentation/profile/view.dart';
enum ErrorLocationType { serviceDisabled, permissionDenied, none }
class RootLogic extends GetxController {
RxInt currentIndex = 0.obs;
List<Widget> pages = [SupervisionFilterPage(), ActionPage(), ProfilePage()];
List<Widget> pages = [InspectionMapPage(), ActionPage(), ProfilePage()];
RxList<ErrorLocationType> errorLocationType = RxList();
Stream<bool> listenToLocationServiceStatus() {

View File

@@ -8,168 +8,49 @@ class RootPage extends GetView<RootLogic> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
ObxValue((errorType) {
if (errorType.isNotEmpty) {
if (errorType.contains(ErrorLocationType.serviceDisabled)) {
Future.microtask(() {
Get.defaultDialog(
title: 'خطا',
content: const Text('سرویس مکان‌یابی غیرفعال است'),
cancel: ROutlinedElevated(
text: 'بررسی مجدد',
width: 120,
textStyle: AppFonts.yekan16,
onPressed: () async {
var service = await controller.locationServiceEnabled();
eLog(service);
if (service) {
controller.errorLocationType.remove(
ErrorLocationType.serviceDisabled,
);
Get.back();
}
// Don't call Get.back() if service is still disabled
},
),
confirm: RElevated(
text: 'روشن کردن',
textStyle: AppFonts.yekan16,
width: 120,
onPressed: () async {
var res = await Geolocator.openLocationSettings();
if (res) {
var service =
await controller.locationServiceEnabled();
if (service) {
controller.errorLocationType.remove(
ErrorLocationType.serviceDisabled,
);
Get.back();
}
}
},
),
return ObxValue((currentIndex) {
return Scaffold(
body: ObxValue(
(currentIndex) => IndexedStack(index: currentIndex.value, children: controller.pages),
controller.currentIndex,
),
bottomNavigationBar: RBottomNavigation(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
items: [
RBottomNavigationItem(
label: 'نقشه',
icon: Assets.vec.mapSvg.path,
isSelected: currentIndex.value == 0,
onTap: () {
Get.nestedKey(1)?.currentState?.popUntil((route) => route.isFirst);
contentPadding: EdgeInsets.all(8),
onWillPop: () async {
return controller.errorLocationType.isEmpty;
},
barrierDismissible: false,
);
});
} else {
Future.microtask(() {
Get.defaultDialog(
title: 'خطا',
content: const Text(
' دسترسی به سرویس مکان‌یابی غیرفعال است',
),
cancel: ROutlinedElevated(
text: 'بررسی مجدد',
width: 120,
textStyle: AppFonts.yekan16,
onPressed: () async {
await controller.checkPermission();
},
),
confirm: RElevated(
text: 'اجازه دادن',
textStyle: AppFonts.yekan16,
width: 120,
onPressed: () async {
var res = await controller.checkPermission(
request: true,
);
if (res) {
controller.errorLocationType.remove(
ErrorLocationType.permissionDenied,
);
Get.back();
}
},
),
controller.changePage(0);
},
),
RBottomNavigationItem(
label: 'اقدام',
icon: Assets.vec.settingSvg.path,
isSelected: currentIndex.value == 1,
onTap: () {
Get.nestedKey(0)?.currentState?.popUntil((route) => route.isFirst);
controller.changePage(1);
},
),
contentPadding: EdgeInsets.all(8),
onWillPop: () async {
return controller.errorLocationType.isEmpty;
},
barrierDismissible: false,
);
});
}
}
return const SizedBox.shrink();
}, controller.errorLocationType),
RBottomNavigationItem(
label: 'پروفایل',
icon: Assets.vec.profileCircleSvg.path,
isSelected: currentIndex.value == 4,
onTap: () {
Get.nestedKey(1)?.currentState?.popUntil((route) => route.isFirst);
Get.nestedKey(0)?.currentState?.popUntil((route) => route.isFirst);
ObxValue(
(currentIndex) => IndexedStack(
index: currentIndex.value,
children: controller.pages,
controller.changePage(4);
},
),
controller.currentIndex,
),
],
),
bottomNavigationBar: WaveBottomNavigation(
items: [
WaveBottomNavigationItem(title: 'خانه', icon: Assets.vec.mapSvg.svg(
width: 32,
height: 32,
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
)),
WaveBottomNavigationItem(
title: 'عملیات',
icon: Assets.vec.userSvg.svg(
width: 32,
height: 32,
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
),
WaveBottomNavigationItem(
title: 'افزودن',
icon: Assets.vec.addSvg.svg(
width: 32,
height: 32,
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
),
WaveBottomNavigationItem(
title: 'آمار',
icon: Assets.vec.diagramSvg.svg(width: 32,height: 32,colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),),
),
WaveBottomNavigationItem(
title: 'تماس',
icon: Assets.vec.callSvg.svg(
width: 32,
height: 32,
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
),
WaveBottomNavigationItem(
title: 'مکان ',
icon: Assets.vec.gpsSvg.svg(
width: 32,
height: 32,
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
),
WaveBottomNavigationItem(
title: 'تاریخ',
icon: Assets.vec.calendarSvg.svg(
width: 32,
height: 32,
colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
),
],
onPageChanged: (index) {
controller.changePage(index);
},
),
);
],
),
);
}, controller.currentIndex);
}
}

View File

@@ -8,6 +8,7 @@ import 'package:rasadyar_inspection/presentation/add_supervision/view.dart';
import 'package:rasadyar_inspection/presentation/display_information/logic.dart';
import 'package:rasadyar_inspection/presentation/display_information/view.dart';
import 'package:rasadyar_inspection/presentation/filter/logic.dart';
import 'package:rasadyar_inspection/presentation/inspection_map/logic.dart';
import 'package:rasadyar_inspection/presentation/location_details/logic.dart';
import 'package:rasadyar_inspection/presentation/location_details/view.dart';
import 'package:rasadyar_inspection/presentation/profile/logic.dart';
@@ -17,18 +18,21 @@ import 'package:rasadyar_inspection/presentation/registration_of_violation/view.
import 'package:rasadyar_inspection/presentation/root/logic.dart';
import 'package:rasadyar_inspection/presentation/root/view.dart';
import 'package:rasadyar_inspection/presentation/routes/app_routes.dart';
import 'package:rasadyar_inspection/presentation/widget/base_page/logic.dart';
sealed class InspectionPages {
InspectionPages._();
static final pages = [
GetPage(
name: InspectionRoutes.inspection,
name: InspectionRoutes.init,
page: () => RootPage(),
middlewares: [AuthMiddleware()],
binding: BindingsBuilder(() {
Get.put(RootLogic());
Get.put(InspectorFilterLogic());
Get.lazyPut(()=>InspectionMapLogic());
Get.lazyPut(()=>BaseLogic());
Get.lazyPut(() => LocationDetailsLogic(), fenix: true);
Get.lazyPut(() => ActionLogic(), fenix: true);
Get.lazyPut(() => ProfileLogic(), fenix: true);

View File

@@ -1,10 +1,10 @@
sealed class InspectionRoutes {
InspectionRoutes._();
static const inspection = '/supervision';
static const inspectionAction = '$inspection/action';
static const inspectionUserProfile = '$inspection/userSettings';
static const inspectionLocationDetails = '$inspection/locationDetails';
static const init = '/supervision';
static const inspectionAction = '$init/action';
static const inspectionUserProfile = '$init/userSettings';
static const inspectionLocationDetails = '$init/locationDetails';
static const inspectionAddSupervision = '$inspectionLocationDetails/addSupervision';
static const inspectionAddMobileInspector = '$inspectionLocationDetails/addMobileInspector';
static const inspectionRegistrationOfViolation = '$inspectionAddSupervision/RegistrationOfViolation';

View File

@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_inspection/presentation/widget/base_page/logic.dart';
RAppBar inspectionAppBar({
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,23 @@
import 'package:rasadyar_core/core.dart';
class BaseLogic extends GetxController {
final RxBool isFilterSelected = false.obs;
final RxBool isSearchSelected = false.obs;
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,146 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_inspection/presentation/widget/app_bar/i_app_bar.dart';
import 'package:rasadyar_inspection/presentation/widget/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: inspectionAppBar(
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,81 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_inspection/presentation/widget/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);
}
}