diff --git a/.tool-versions b/.tool-versions index fc05c635..3e6df7d4 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -gradle 7.6.1 +gradle 8.10.2 diff --git a/integration_test/app_test.dart b/integration_test/app_test.dart index 404fdbae..819ec78c 100644 --- a/integration_test/app_test.dart +++ b/integration_test/app_test.dart @@ -7,9 +7,11 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:hub/components/atomic_components/shared_components_atoms/submit_button.dart'; import 'package:hub/components/molecular_components/throw_exception/throw_exception_widget.dart'; import 'package:hub/components/organism_components/bottom_arrow_linked_locals_component/bottom_arrow_linked_locals_component_widget.dart'; import 'package:hub/components/templates_components/card_item_template_component/card_item_template_component_widget.dart'; +import 'package:hub/components/templates_components/details_component/details_component_widget.dart'; import 'package:hub/features/backend/api_requests/index.dart'; import 'package:hub/features/local/index.dart'; import 'package:hub/features/menu/index.dart'; @@ -20,6 +22,8 @@ import 'package:hub/features/storage/index.dart'; import 'package:hub/flutter_flow/index.dart' as ff; import 'package:hub/flutter_flow/index.dart'; import 'package:hub/main.dart'; +import 'package:hub/pages/vehicles_on_the_property/vehicles_on_the_property.dart'; +import 'package:integration_test/integration_test.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; @@ -27,7 +31,6 @@ import 'app_test.dart'; import 'fuzzer/fuzzer.dart'; import 'package:patrol_finders/patrol_finders.dart'; -import 'package:integration_test/integration_test.dart'; export 'package:flutter_test/flutter_test.dart'; export 'package:patrol/patrol.dart'; @@ -56,6 +59,7 @@ part 'storage_test.dart'; part 'utils_test.dart'; part 'welcome_test.dart'; +part 'vehicle_test.dart'; late PatrolTester $; @@ -83,5 +87,9 @@ void main() { LocalsTest.setLocal(); LocalsTest.unlinkLocal(); - LocalsTest.attachLocal(); + + VehicleTest.vehiclePage(); + VehicleTest.historyScreen(); + VehicleTest.registerScreen(); + VehicleTest.updateScreen(); } diff --git a/integration_test/vehicle_test.dart b/integration_test/vehicle_test.dart new file mode 100644 index 00000000..c3c5c0fb --- /dev/null +++ b/integration_test/vehicle_test.dart @@ -0,0 +1,320 @@ +part of 'app_test.dart'; + +class VehicleTest { + static Future _initVehicleModule() async { + final vehicleParam = { + 'display': 'VISIVEL', + 'expirationDate': '', + 'startDate': '', + 'quantity': 0, + }; + final vehicleManagerParam = { + 'display': 'VISIVEL', + 'expirationDate': '', + 'startDate': '', + 'quantity': 0, + }; + await LicenseRepositoryImpl() + .setModule(LicenseKeys.vehicles.value, vehicleParam); + await LicenseRepositoryImpl() + .setModule(LicenseKeys.vehiclesManager.value, vehicleManagerParam); + } + + static Future vehiclePage() async { + patrolWidgetTest( + 'Vehicle Page', + (PatrolTester tester) async { + $ = tester; + $.tester.printToConsole('Vehicle Page'); + final PatrolFinder throwsException = $(Dialog).$(ThrowExceptionWidget); + + await _loggedWithMultiLocalsAccount(); + await _initVehicleModule(); + await $.pumpAndSettle(); + await $.pumpWidgetAndSettle(const App()); + + ff.navigatorKey.currentContext!.go('/vehiclesOnThePropertyPage'); + + final String title = MenuEntry.entries // + .where((entry) => entry.key == 'FRE-HUB-VEHICLES') // + .map((entry) => entry.name) + .first; + + final PatrolFinder appBar = await $(AppBar) // + .waitUntilExists(); + final PatrolFinder titleAppBar = await appBar // + .$(title) + .waitUntilVisible(); + expect(titleAppBar, findsOneWidget); + + final PatrolFinder tab1 = await $(#TabView_Tab1) // + .waitUntilExists(); + final PatrolFinder tab2 = await $(#TabView_Tab2) // + .waitUntilExists(); + + await tab2.tap(); + await Future.delayed(const Duration(milliseconds: 500)); + await tab1.tap(); + + final PatrolFinder listViewFinder = await $(VehicleHistoryScreen) // + .$(ListView) + .waitUntilVisible(); + + expect(listViewFinder, findsOneWidget); + + final PatrolFinder entriesFinder = await $(listViewFinder) + .$(CardItemTemplateComponentWidget) + .waitUntilVisible(); + + expect(entriesFinder, findsWidgets); + final int entriesCount = entriesFinder.evaluate().length; + await $.pumpAndSettle(); + + if (entriesCount > 0) + for (int i = 0; i < entriesCount; i++) { + await $(entriesFinder.at(i)).scrollTo(); + + await $(entriesFinder.at(i)) + .waitUntilVisible(timeout: const Duration(seconds: 1)) + .tap( + settleTimeout: const Duration(seconds: 1), + settlePolicy: SettlePolicy.noSettle, + ); + + await $.pumpAndSettle(duration: Duration(milliseconds: 500)); + final PatrolFinder detailsFinder = + await $(DetailsComponentWidget).waitUntilVisible(); + expect(detailsFinder, findsOneWidget); + + await _navigateBackUsingSystemGesture(); + + // await $.native.pressBack().then((_) => $.pumpAndSettle()); + } + }, + ); + patrolWidgetTest( + 'License', + (PatrolTester tester) async { + $ = tester; + $.tester.printToConsole('Vehicle Page'); + + await _loggedWithMultiLocalsAccount(); + await _initVehicleModule(); + await $.pumpAndSettle(); + + await $.pumpWidgetAndSettle(const App()); + + ff.navigatorKey.currentContext!.go('/vehiclesOnThePropertyPage'); + final String title = MenuEntry.entries // + .where((entry) => entry.key == 'FRE-HUB-VEHICLES') // + .map((entry) => entry.name) + .first; + + final PatrolFinder appBar = await $(AppBar) // + .waitUntilExists(); + final PatrolFinder titleAppBar = await appBar // + .$(title) + .waitUntilVisible(); + expect(titleAppBar, findsOneWidget); + + final PatrolFinder tab1 = await $(#TabView_Tab1) // + .waitUntilExists(); + final PatrolFinder tab2 = await $(#TabView_Tab2) // + .waitUntilExists(); + + await tab2.tap(); + await Future.delayed(const Duration(milliseconds: 500)); + await tab1.tap(); + + final PatrolFinder listViewFinder = await $(VehicleHistoryScreen) // + .$(ListView) + .waitUntilVisible(); + + expect(listViewFinder, findsOneWidget); + + final PatrolFinder entriesFinder = await $(listViewFinder) + .$(CardItemTemplateComponentWidget) + .waitUntilVisible(); + + expect(entriesFinder, findsWidgets); + await $.pumpAndSettle(); + await Future.delayed(const Duration(milliseconds: 1000)); + }, + ); + } + + static Future historyScreen() async { + patrolWidgetTest( + 'historyScreen', + (PatrolTester tester) async { + $ = tester; + $.tester.printToConsole('Vehicle Page'); + final PatrolFinder throwsException = $(Dialog).$(ThrowExceptionWidget); + + await _loggedWithMultiLocalsAccount(); + await _initVehicleModule(); + await $.pumpAndSettle(); + await $.pumpWidgetAndSettle(const App()); + + ff.navigatorKey.currentContext!.go('/vehiclesOnThePropertyPage'); + + final String title = MenuEntry.entries // + .where((entry) => entry.key == 'FRE-HUB-VEHICLES') // + .map((entry) => entry.name) + .first; + + final PatrolFinder appBar = await $(AppBar) // + .waitUntilExists(); + final PatrolFinder titleAppBar = await appBar // + .$(title) + .waitUntilVisible(); + expect(titleAppBar, findsOneWidget); + + final PatrolFinder listViewFinder = await $(VehicleHistoryScreen) // + .$(ListView) + .waitUntilVisible(); + + expect(listViewFinder, findsOneWidget); + + final PatrolFinder entriesFinder = await $(listViewFinder) + .$(CardItemTemplateComponentWidget) + .waitUntilVisible(); + + expect(entriesFinder, findsWidgets); + await $.pumpAndSettle(); + await $(entriesFinder.first) + .waitUntilVisible(timeout: const Duration(seconds: 1)) + .tap( + settleTimeout: const Duration(seconds: 1), + settlePolicy: SettlePolicy.noSettle, + ); + + await $.pumpAndSettle(duration: Duration(milliseconds: 500)); + final PatrolFinder detailsFinder = + await $(DetailsComponentWidget).waitUntilVisible(); + expect(detailsFinder, findsOneWidget); + + await _navigateBackUsingSystemGesture(); + + /// Iterable Test + // final int entriesCount = entriesFinder.evaluate().length; + // for (int i = 0; i < entriesCount; i++) { + // // await $(entriesFinder.at(i)).scrollTo(); + + // await $(entriesFinder.at(i)) + // .waitUntilVisible(timeout: const Duration(seconds: 1)) + // .tap( + // settleTimeout: const Duration(seconds: 1), + // settlePolicy: SettlePolicy.noSettle, + // ); + + // await $.pumpAndSettle(duration: Duration(milliseconds: 500)); + // final PatrolFinder detailsFinder = + // await $(DetailsComponentWidget).waitUntilVisible(); + // expect(detailsFinder, findsOneWidget); + + // await _navigateBackUsingSystemGesture(); + + // // await $.native.pressBack().then((_) => $.pumpAndSettle()); + // } + }, + ); + } + + static Future registerScreen() async { + patrolWidgetTest( + 'registerScreen', + (PatrolTester tester) async { + $ = tester; + $.tester.printToConsole('Vehicle Register Page'); + + await _loggedWithMultiLocalsAccount(); + await _initVehicleModule(); + await $.pumpAndSettle(); + await $.pumpWidgetAndSettle(const App()); + + ff.navigatorKey.currentContext!.go('/vehiclesOnThePropertyPage'); + + final PatrolFinder tab2 = await $(#TabView_Tab2) // + .waitUntilExists(); + + await tab2.tap(); + + final PatrolFinder licensePlateField = + await $(TextField).at(0).waitUntilVisible(); + final PatrolFinder modelField = + await $(TextField).at(1).waitUntilVisible(); + final PatrolFinder colorField = + await $(TextField).at(2).waitUntilVisible(); + final PatrolFinder submitButton = + await $(SubmitButtonUtil).waitUntilVisible(); + + await licensePlateField.enterText('ABC1234'); + await modelField.enterText('Voyage'); + await colorField.enterText('Black'); + await submitButton.tap(); + + await $.pumpAndSettle(); + final PatrolFinder successDialog = await $(Dialog).waitUntilVisible(); + expect(successDialog, findsOneWidget); + }, + ); + } + + static Future updateScreen() async { + patrolWidgetTest( + 'updateScreen', + (PatrolTester tester) async { + $ = tester; + $.tester.printToConsole('Vehicle Update Page'); + + await _loggedWithMultiLocalsAccount(); + await _initVehicleModule(); + await $.pumpAndSettle(); + await $.pumpWidgetAndSettle(const App()); + + ff.navigatorKey.currentContext!.go('/vehiclesOnThePropertyPage'); + + final PatrolFinder tab1 = await $(#TabView_Tab1) // + .waitUntilExists(); + + await tab1.tap(); + + final PatrolFinder listViewFinder = await $(VehicleHistoryScreen) // + .$(ListView) + .waitUntilVisible(); + + expect(listViewFinder, findsOneWidget); + + final PatrolFinder entriesFinder = await $(listViewFinder) + .$(CardItemTemplateComponentWidget) + .waitUntilVisible(); + + expect(entriesFinder, findsWidgets); + await $(entriesFinder.at(0)).tap(); + + final PatrolFinder editButton = + await $(FFButtonWidget).$('Edit').waitUntilVisible(); + await editButton.tap(); + + final PatrolFinder licensePlateField = + await $(TextField).at(0).waitUntilVisible(); + final PatrolFinder modelField = + await $(TextField).at(1).waitUntilVisible(); + final PatrolFinder colorField = + await $(TextField).at(2).waitUntilVisible(); + final PatrolFinder submitButton = + await $(SubmitButtonUtil).waitUntilVisible(); + + await licensePlateField.enterText('XYZ5678'); + await modelField.enterText('Fiesta'); + await colorField.enterText('Red'); + await submitButton.tap(); + + await $.pumpAndSettle(); + final PatrolFinder successDialog = await $(Dialog).waitUntilVisible(); + expect(successDialog, findsOneWidget); + }, + ); + } +} diff --git a/lib/components/atomic_components/shared_components_atoms/custom_input.dart b/lib/components/atomic_components/shared_components_atoms/custom_input.dart index cac159f4..cb02aba7 100644 --- a/lib/components/atomic_components/shared_components_atoms/custom_input.dart +++ b/lib/components/atomic_components/shared_components_atoms/custom_input.dart @@ -5,6 +5,15 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:hub/flutter_flow/flutter_flow_theme.dart'; import 'package:hub/shared/utils/limited_text_size.dart'; +class UpperCaseTextFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + return TextEditingValue( + text: newValue.text.toUpperCase(), selection: newValue.selection); + } +} + // ignore: must_be_immutable class CustomInputUtil extends StatefulWidget { final TextEditingController? controller; @@ -20,22 +29,25 @@ class CustomInputUtil extends StatefulWidget { final String? Function(String?)? validator; final bool haveMaxLength; final void Function(String)? onChanged; + final List? inputFormatters; - CustomInputUtil( - {super.key, - this.controller, - required this.labelText, - required this.hintText, - required this.suffixIcon, - this.autoFocus = false, - required this.focusNode, - this.onChanged, - this.textInputAction = TextInputAction.next, - this.keyboardType = TextInputType.text, - this.maxLength = 80, - this.validator, - this.obscureText, - required this.haveMaxLength}); + CustomInputUtil({ + super.key, + this.controller, + required this.labelText, + required this.hintText, + required this.suffixIcon, + this.autoFocus = false, + required this.focusNode, + this.onChanged, + this.textInputAction = TextInputAction.next, + this.keyboardType = TextInputType.text, + this.maxLength = 80, + this.validator, + this.obscureText, + this.inputFormatters, + required this.haveMaxLength, + }); @override State createState() => _CustomInputUtilState(); @@ -152,6 +164,7 @@ class _CustomInputUtilState extends State { keyboardType: widget.keyboardType, inputFormatters: [ LengthLimitingTextInputFormatter(widget.maxLength), + if (widget.inputFormatters != null) ...widget.inputFormatters! ], ), ], diff --git a/lib/components/atomic_components/shared_components_atoms/tabview.dart b/lib/components/atomic_components/shared_components_atoms/tabview.dart index 8a62cbf3..30348b8d 100644 --- a/lib/components/atomic_components/shared_components_atoms/tabview.dart +++ b/lib/components/atomic_components/shared_components_atoms/tabview.dart @@ -10,7 +10,7 @@ class TabViewUtil extends StatelessWidget { String labelTab1; String labelTab2; final TabController controller; - final Function(bool) onEditingChanged; + final Function([bool]) onEditingChanged; Widget widget1; Widget widget2; @@ -49,15 +49,17 @@ class TabViewUtil extends StatelessWidget { padding: const EdgeInsets.all(4.0), tabs: [ Tab( + key: ValueKey('TabView_Tab1'), text: labelTab1, ), Tab( + key: ValueKey('TabView_Tab2'), text: labelTab2, ), ], controller: controller, onTap: (i) async { - if (i == 1) onEditingChanged(false); + onEditingChanged(); [() async {}, () async {}][i](); }, ), diff --git a/lib/components/organism_components/bottom_arrow_linked_locals_component/bottom_arrow_linked_locals_component_widget.dart b/lib/components/organism_components/bottom_arrow_linked_locals_component/bottom_arrow_linked_locals_component_widget.dart index 6daec993..264db202 100644 --- a/lib/components/organism_components/bottom_arrow_linked_locals_component/bottom_arrow_linked_locals_component_widget.dart +++ b/lib/components/organism_components/bottom_arrow_linked_locals_component/bottom_arrow_linked_locals_component_widget.dart @@ -209,6 +209,7 @@ class _BottomArrowLinkedLocalsComponentWidgetState return CardItemTemplateComponentWidget( key: ValueKey(local['CLI_NOME']), imagePath: _imagePath(local), + icon: null, labelsHashMap: _labelsHashMap(local), statusHashMap: [_statusHashMap(local)], onTapCardItemAction: () async { diff --git a/lib/components/templates_components/card_item_template_component/card_item_template_component_widget.dart b/lib/components/templates_components/card_item_template_component/card_item_template_component_widget.dart index 26d90fa3..53c509aa 100644 --- a/lib/components/templates_components/card_item_template_component/card_item_template_component_widget.dart +++ b/lib/components/templates_components/card_item_template_component/card_item_template_component_widget.dart @@ -12,18 +12,74 @@ import 'card_item_template_component_model.dart'; export 'card_item_template_component_model.dart'; +class FreCardIcon extends StatelessWidget { + final double height; + final double width; + final Icon icon; + + const FreCardIcon({ + super.key, + required this.height, + required this.width, + required this.icon, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: height, + width: width, + child: icon, + ); + } +} + +class FreCardPin extends StatelessWidget { + final double height; + final double width; + final Color color; + + const FreCardPin({ + super.key, + required this.height, + required this.width, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + height: height, + width: width, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ), + ); + } +} + class CardItemTemplateComponentWidget extends StatefulWidget { const CardItemTemplateComponentWidget({ super.key, required this.labelsHashMap, required this.statusHashMap, - required this.imagePath, + this.imagePath, + this.icon, + this.pin, + this.itemWidthFactor = 0.25, required this.onTapCardItemAction, }); final Map? labelsHashMap; final List?> statusHashMap; final String? imagePath; + final FreCardIcon? icon; + final FreCardPin? pin; + final double itemWidthFactor; final Future Function()? onTapCardItemAction; @override @@ -125,9 +181,24 @@ class _CardItemTemplateComponentWidgetState ); } + Widget _generateIcon() { + return Column( + children: [ + widget.icon!, + ], + ); + } + + Widget _generatePin() { + return widget.pin!; + } + List _generateStatus() { double limitedBodyTextSize = LimitedFontSizeUtil.getBodyFontSize(context); + return statusLinkedHashMap.expand((statusLinked) { + log('statusHashMap: ${statusLinked.length}'); + return statusLinked.entries.map((entry) { final text = entry.key; final color = entry.value; @@ -136,7 +207,7 @@ class _CardItemTemplateComponentWidgetState message: text, child: Container( padding: const EdgeInsets.all(5), - width: MediaQuery.of(context).size.width * 0.25, + width: MediaQuery.of(context).size.width * widget.itemWidthFactor, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(5), @@ -162,38 +233,50 @@ class _CardItemTemplateComponentWidgetState return LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth > 360) { - return Row( + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - ..._generateLabels(), - SizedBox(height: 3), - Wrap( - spacing: 8, - runSpacing: 4, - children: _generateStatus(), + Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.max, + children: [if (widget.pin != null) _generatePin()]), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + ..._generateLabels(), + Wrap( + spacing: 8, + runSpacing: 4, + children: _generateStatus(), + ), + ] + .addToEnd(const SizedBox(height: 5)) + .divide(const SizedBox(height: 3)) + .addToStart(const SizedBox(height: 5)), ), - ] - .addToEnd(const SizedBox(height: 5)) - .divide(const SizedBox(height: 1)) - .addToStart(const SizedBox(height: 5)), - ), + ), + if (widget.icon != null) _generateIcon(), + if (widget.imagePath != null) _generateImage() + ] + .addToEnd(const SizedBox(width: 10)) + .addToStart(const SizedBox(width: 10)), ), - if (widget.imagePath != null) _generateImage(), - ] - .addToEnd(const SizedBox(width: 10)) - .addToStart(const SizedBox(width: 10)), + ].addToStart(SizedBox(height: 5)).addToEnd(SizedBox(height: 5)), ); } else { return Column( mainAxisSize: MainAxisSize.min, children: [ if (widget.imagePath != null) _generateImage(), + if (widget.icon != null) _generateIcon(), Container( padding: const EdgeInsets.all(8), child: Column( diff --git a/lib/components/templates_components/details_component/details_component_widget.dart b/lib/components/templates_components/details_component/details_component_widget.dart index 9754639d..67a21e19 100644 --- a/lib/components/templates_components/details_component/details_component_widget.dart +++ b/lib/components/templates_components/details_component/details_component_widget.dart @@ -3,6 +3,7 @@ import 'dart:collection'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:hub/components/templates_components/card_item_template_component/card_item_template_component_widget.dart'; import 'package:hub/components/templates_components/details_component/details_component_model.dart'; import 'package:hub/flutter_flow/flutter_flow_theme.dart'; import 'package:hub/flutter_flow/flutter_flow_util.dart'; @@ -14,6 +15,7 @@ class DetailsComponentWidget extends StatefulWidget { required this.labelsHashMap, required this.statusHashMap, this.imagePath, + this.icon, this.onTapCardItemAction, required this.buttons, }); @@ -23,6 +25,7 @@ class DetailsComponentWidget extends StatefulWidget { final String? imagePath; final Future Function()? onTapCardItemAction; final List? buttons; + final FreCardIcon? icon; @override State createState() => _DetailsComponentWidgetState(); @@ -64,225 +67,232 @@ class _DetailsComponentWidgetState extends State { // CachedNetworkImage.evictFromCache(widget.imagePath ?? ''); final double limitedBodyFontSize = LimitedFontSizeUtil.getBodyFontSize(context); - return Material( - type: MaterialType.transparency, - child: Container( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width, - maxHeight: MediaQuery.of(context).size.height, - ), - decoration: BoxDecoration( - color: FlutterFlowTheme.of(context).primaryBackground, - borderRadius: const BorderRadius.all(Radius.circular(25.0)), - ), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox(height: MediaQuery.of(context).size.height * 0.02), - if (widget.imagePath != null && widget.imagePath != '') - Container( - width: MediaQuery.of(context).size.width * 0.3, - height: MediaQuery.of(context).size.width * 0.3, - clipBehavior: Clip.antiAlias, - decoration: const BoxDecoration( - shape: BoxShape.circle, - ), - child: CachedNetworkImage( - fadeInDuration: const Duration(milliseconds: 100), - fadeOutDuration: const Duration(milliseconds: 100), - imageUrl: widget.imagePath!, - fit: BoxFit.cover, - useOldImageOnUrlChange: true, - ), + return Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width, + maxHeight: MediaQuery.of(context).size.height, + ), + decoration: BoxDecoration( + color: FlutterFlowTheme.of(context).primaryBackground, + borderRadius: const BorderRadius.all(Radius.circular(25.0)), + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox(height: MediaQuery.of(context).size.height * 0.02), + if (widget.imagePath != null && widget.imagePath != '') + Container( + width: MediaQuery.of(context).size.width * 0.3, + height: MediaQuery.of(context).size.width * 0.3, + clipBehavior: Clip.antiAlias, + decoration: const BoxDecoration( + shape: BoxShape.circle, ), - SizedBox(height: MediaQuery.of(context).size.height * 0.03), - Row( - children: statusLinkedHashMap.expand((linkedHashMap) { - return linkedHashMap.entries - .map((MapEntry item) { - return Expanded( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: MediaQuery.of(context).size.width * 0.05, - ), - child: TextFormField( - autofocus: false, - canRequestFocus: false, - readOnly: true, - obscureText: false, - decoration: InputDecoration( - isDense: true, - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10.0), - borderSide: BorderSide( - color: item.value, - ), - ), - filled: true, - fillColor: item.value, - labelText: item.key, - labelStyle: FlutterFlowTheme.of(context) - .labelMedium - .override( - fontFamily: FlutterFlowTheme.of(context) - .labelMediumFamily, - fontWeight: FontWeight.bold, - color: FlutterFlowTheme.of(context).info, - letterSpacing: 0.0, - useGoogleFonts: - GoogleFonts.asMap().containsKey( - FlutterFlowTheme.of(context) - .labelMediumFamily, - ), - fontSize: limitedBodyFontSize, - ), - hintStyle: FlutterFlowTheme.of(context) - .labelMedium - .override( - fontFamily: FlutterFlowTheme.of(context) - .labelMediumFamily, - color: FlutterFlowTheme.of(context).info, - letterSpacing: 0.0, - useGoogleFonts: - GoogleFonts.asMap().containsKey( - FlutterFlowTheme.of(context) - .labelMediumFamily, - ), - fontSize: limitedBodyFontSize, - ), - focusedBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - suffixIcon: Icon( - Icons.info, - color: FlutterFlowTheme.of(context).info, + child: CachedNetworkImage( + fadeInDuration: const Duration(milliseconds: 100), + fadeOutDuration: const Duration(milliseconds: 100), + imageUrl: widget.imagePath!, + fit: BoxFit.cover, + useOldImageOnUrlChange: true, + ), + ), + if (widget.icon != null && widget.icon != '') + Container( + width: MediaQuery.of(context).size.width * 0.3, + height: MediaQuery.of(context).size.width * 0.3, + clipBehavior: Clip.antiAlias, + decoration: const BoxDecoration( + shape: BoxShape.circle, + ), + child: widget.icon!, + ), + SizedBox(height: MediaQuery.of(context).size.height * 0.03), + Row( + children: statusLinkedHashMap.expand((linkedHashMap) { + return linkedHashMap.entries + .map((MapEntry item) { + return Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width * 0.05, + ), + child: TextFormField( + autofocus: false, + canRequestFocus: false, + readOnly: true, + initialValue: item.key, + obscureText: false, + decoration: InputDecoration( + isDense: true, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide( + color: item.value, ), ), - style: FlutterFlowTheme.of(context) - .bodyMedium + filled: true, + fillColor: item.value, + // labelText: item.key, + labelStyle: FlutterFlowTheme.of(context) + .labelMedium .override( fontFamily: FlutterFlowTheme.of(context) - .bodyMediumFamily, + .labelMediumFamily, + fontWeight: FontWeight.bold, color: FlutterFlowTheme.of(context).info, letterSpacing: 0.0, useGoogleFonts: GoogleFonts.asMap().containsKey( - FlutterFlowTheme.of(context).bodyMediumFamily, + FlutterFlowTheme.of(context) + .labelMediumFamily, ), fontSize: limitedBodyFontSize, ), - textAlign: TextAlign.center, - maxLines: null, - keyboardType: TextInputType.name, - validator: _model.textController1Validator - .asValidator(context), - ), - ), - ); - }).toList(); - }).toList(), - ), - SizedBox(height: MediaQuery.of(context).size.height * 0.03), - ListView.builder( - shrinkWrap: true, - itemCount: labelsLinkedHashMap.length, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - String key = labelsLinkedHashMap.keys.elementAt(index); - String value = labelsLinkedHashMap[key]!; - // return Text('key: $key, value: $value'); - return TextFormField( - readOnly: true, - initialValue: value, - style: FlutterFlowTheme.of(context).bodyMedium.override( - fontFamily: - FlutterFlowTheme.of(context).bodyMediumFamily, - color: FlutterFlowTheme.of(context).primaryText, - letterSpacing: 0.0, - useGoogleFonts: GoogleFonts.asMap().containsKey( - FlutterFlowTheme.of(context).bodyMediumFamily, + hintStyle: FlutterFlowTheme.of(context) + .labelMedium + .override( + fontFamily: FlutterFlowTheme.of(context) + .labelMediumFamily, + color: FlutterFlowTheme.of(context).info, + letterSpacing: 0.0, + useGoogleFonts: GoogleFonts.asMap().containsKey( + FlutterFlowTheme.of(context) + .labelMediumFamily, + ), + fontSize: limitedBodyFontSize, + ), + focusedBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + suffixIcon: Icon( + Icons.info, + color: FlutterFlowTheme.of(context).info, ), - fontSize: limitedBodyFontSize, ), - decoration: InputDecoration( - labelText: key, - filled: true, - fillColor: FlutterFlowTheme.of(context).primaryBackground, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10.0), - borderSide: BorderSide( - color: FlutterFlowTheme.of(context) - .primaryBackground, // Change border color here - ), - ), - labelStyle: FlutterFlowTheme.of(context) - .labelMedium - .override( - fontFamily: + style: FlutterFlowTheme.of(context) + .labelMedium + .override( + fontFamily: FlutterFlowTheme.of(context) + .labelMediumFamily, + fontWeight: FontWeight.bold, + color: FlutterFlowTheme.of(context).info, + letterSpacing: 0.0, + useGoogleFonts: GoogleFonts.asMap().containsKey( FlutterFlowTheme.of(context).labelMediumFamily, - color: FlutterFlowTheme.of(context).primaryText, - letterSpacing: 0.0, - useGoogleFonts: GoogleFonts.asMap().containsKey( - FlutterFlowTheme.of(context).labelMediumFamily, + ), + fontSize: limitedBodyFontSize, ), - ), - hintStyle: FlutterFlowTheme.of(context) - .labelMedium - .override( - fontFamily: - FlutterFlowTheme.of(context).labelMediumFamily, - color: FlutterFlowTheme.of(context).primaryText, - letterSpacing: 0.0, - useGoogleFonts: GoogleFonts.asMap().containsKey( - FlutterFlowTheme.of(context).labelMediumFamily, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10.0), - borderSide: BorderSide( - color: FlutterFlowTheme.of(context) - .primaryBackground, // Change border color here - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10.0), - borderSide: BorderSide( - color: FlutterFlowTheme.of(context) - .primaryBackground, // Change border color here - ), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10.0), - borderSide: BorderSide( - color: FlutterFlowTheme.of(context) - .primaryBackground, // Change border color here - ), - ), - focusedErrorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10.0), - borderSide: BorderSide( - color: FlutterFlowTheme.of(context) - .primaryBackground, // Change border color here - ), + textAlign: TextAlign.center, + maxLines: null, + keyboardType: TextInputType.name, + validator: _model.textController1Validator + .asValidator(context), ), ), ); - }, + }).toList(); + }).toList(), + ), + SizedBox(height: MediaQuery.of(context).size.height * 0.03), + ListView.builder( + shrinkWrap: true, + itemCount: labelsLinkedHashMap.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + String key = labelsLinkedHashMap.keys.elementAt(index); + String value = labelsLinkedHashMap[key]!; + // return Text('key: $key, value: $value'); + return TextFormField( + readOnly: true, + initialValue: value, + style: FlutterFlowTheme.of(context).bodyMedium.override( + fontFamily: + FlutterFlowTheme.of(context).bodyMediumFamily, + color: FlutterFlowTheme.of(context).primaryText, + letterSpacing: 0.0, + useGoogleFonts: GoogleFonts.asMap().containsKey( + FlutterFlowTheme.of(context).bodyMediumFamily, + ), + fontSize: limitedBodyFontSize, + ), + decoration: InputDecoration( + labelText: key, + filled: true, + fillColor: FlutterFlowTheme.of(context).primaryBackground, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide( + color: FlutterFlowTheme.of(context) + .primaryBackground, // Change border color here + ), + ), + labelStyle: FlutterFlowTheme.of(context) + .labelMedium + .override( + fontFamily: + FlutterFlowTheme.of(context).labelMediumFamily, + color: FlutterFlowTheme.of(context).primaryText, + letterSpacing: 0.0, + useGoogleFonts: GoogleFonts.asMap().containsKey( + FlutterFlowTheme.of(context).labelMediumFamily, + ), + ), + hintStyle: FlutterFlowTheme.of(context) + .labelMedium + .override( + fontFamily: + FlutterFlowTheme.of(context).labelMediumFamily, + color: FlutterFlowTheme.of(context).primaryText, + letterSpacing: 0.0, + useGoogleFonts: GoogleFonts.asMap().containsKey( + FlutterFlowTheme.of(context).labelMediumFamily, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide( + color: FlutterFlowTheme.of(context) + .primaryBackground, // Change border color here + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide( + color: FlutterFlowTheme.of(context) + .primaryBackground, // Change border color here + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide( + color: FlutterFlowTheme.of(context) + .primaryBackground, // Change border color here + ), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide( + color: FlutterFlowTheme.of(context) + .primaryBackground, // Change border color here + ), + ), + ), + ); + }, + ), + SizedBox(height: MediaQuery.of(context).size.height * 0.02), + if (widget.buttons!.isNotEmpty || widget.buttons != null) + OverflowBar( + overflowAlignment: OverflowBarAlignment.center, + alignment: MainAxisAlignment.center, + overflowSpacing: 2, + spacing: 2, + // mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: widget.buttons!, ), - SizedBox(height: MediaQuery.of(context).size.height * 0.02), - if (widget.buttons!.isNotEmpty || widget.buttons != null) - OverflowBar( - overflowAlignment: OverflowBarAlignment.center, - alignment: MainAxisAlignment.center, - overflowSpacing: 2, - spacing: 2, - // mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: widget.buttons!, - ), - SizedBox(height: MediaQuery.of(context).size.height * 0.02), - ], - ), + SizedBox(height: MediaQuery.of(context).size.height * 0.02), + ], ), ), ); diff --git a/lib/components/templates_components/regisiter_vistor_template_component/regisiter_vistor_template_component_widget.dart b/lib/components/templates_components/regisiter_vistor_template_component/regisiter_vistor_template_component_widget.dart index 3ebb0c26..7cf15002 100644 --- a/lib/components/templates_components/regisiter_vistor_template_component/regisiter_vistor_template_component_widget.dart +++ b/lib/components/templates_components/regisiter_vistor_template_component/regisiter_vistor_template_component_widget.dart @@ -34,6 +34,7 @@ class RegisiterVistorTemplateComponentWidget extends StatefulWidget { class _RegisiterVistorTemplateComponentWidgetState extends State { late RegisiterVistorTemplateComponentModel _model; + final bool _isLoading = false; final scaffoldKey = GlobalKey(); bool _isVisitorRegistered = false; diff --git a/lib/core/index.dart b/lib/core/index.dart deleted file mode 100644 index 8b137891..00000000 --- a/lib/core/index.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/core/meta/anotations.dart b/lib/core/meta/anotations.dart deleted file mode 100644 index 842e5064..00000000 --- a/lib/core/meta/anotations.dart +++ /dev/null @@ -1,5 +0,0 @@ -class DeadCode { - final String? desc; - - const DeadCode([this.desc = '']); -} diff --git a/lib/core/meta/index.dart b/lib/core/meta/index.dart deleted file mode 100644 index 166d574c..00000000 --- a/lib/core/meta/index.dart +++ /dev/null @@ -1 +0,0 @@ -export 'anotations.dart'; diff --git a/lib/features/backend/api_requests/api_calls.dart b/lib/features/backend/api_requests/api_calls.dart index 192bae1a..da746b83 100644 --- a/lib/features/backend/api_requests/api_calls.dart +++ b/lib/features/backend/api_requests/api_calls.dart @@ -75,6 +75,184 @@ class FreAccessWSGlobal extends Api { @override GetLicense getLicense = GetLicense(); static GetProvSchedules getProvSchedules = GetProvSchedules(); + static RegisterVehicle registerVehicle = RegisterVehicle(); + static UpdateVehicle updateVehicle = UpdateVehicle(); + static DeleteVehicle deleteVehicle = DeleteVehicle(); + static CancelDeleteVehicle cancelDelete = CancelDeleteVehicle(); +} + +class CancelDeleteVehicle { + Future call({ + required final int vehicleId, + required final String licensePlate, + required final String model, + required final String color, + }) async { + final String baseUrl = PhpGroup.getBaseUrl(); + final String devUUID = + (await StorageHelper().get(ProfileStorageKey.devUUID.key)) ?? ''; + final String userUUID = + (await StorageHelper().get(ProfileStorageKey.userUUID.key)) ?? ''; + final String cliID = + (await StorageHelper().get(ProfileStorageKey.clientUUID.key)) ?? ''; + const String atividade = 'cancelDeleteVehicleRequest'; + + return await ApiManager.instance.makeApiCall( + callName: atividade, + apiUrl: '$baseUrl/processRequest.php', + callType: ApiCallType.POST, + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + params: { + 'devUUID': devUUID, + 'userUUID': userUUID, + 'cliID': cliID, + 'atividade': atividade, + 'vehicleId': vehicleId, + 'licensePlate': licensePlate, + 'model': model, + 'color': color + }, + bodyType: BodyType.X_WWW_FORM_URL_ENCODED, + returnBody: true, + encodeBodyUtf8: false, + decodeUtf8: false, + cache: false, + isStreamingApi: false, + alwaysAllowBody: false, + ); + } +} + +class DeleteVehicle { + Future call({ + required final int vehicleId, + required final String licensePlate, + required final String model, + required final String color, + }) async { + final String baseUrl = PhpGroup.getBaseUrl(); + final String devUUID = + (await StorageHelper().get(ProfileStorageKey.devUUID.key)) ?? ''; + final String userUUID = + (await StorageHelper().get(ProfileStorageKey.userUUID.key)) ?? ''; + final String cliID = + (await StorageHelper().get(ProfileStorageKey.clientUUID.key)) ?? ''; + const String atividade = 'deleteVehicle'; + + return await ApiManager.instance.makeApiCall( + callName: 'deleteVehicle', + apiUrl: '$baseUrl/processRequest.php', + callType: ApiCallType.POST, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + params: { + 'devUUID': devUUID, + 'userUUID': userUUID, + 'cliID': cliID, + 'atividade': atividade, + 'vehicleId': vehicleId, + 'licensePlate': licensePlate, + 'model': model, + 'color': color + }, + bodyType: BodyType.X_WWW_FORM_URL_ENCODED, + returnBody: true, + encodeBodyUtf8: false, + decodeUtf8: false, + cache: false, + isStreamingApi: false, + alwaysAllowBody: false, + ); + } +} + +class RegisterVehicle { + Future call({ + final String? licensePlate, + final String? color, + final String? model, + }) async { + final String baseUrl = PhpGroup.getBaseUrl(); + + final String devUUID = + (await StorageHelper().get(ProfileStorageKey.devUUID.key)) ?? ''; + final String userUUID = + (await StorageHelper().get(ProfileStorageKey.userUUID.key)) ?? ''; + final String cliID = + (await StorageHelper().get(ProfileStorageKey.clientUUID.key)) ?? ''; + const String atividade = 'insertVehicle'; + + return await ApiManager.instance.makeApiCall( + callName: 'registerVehicle', + apiUrl: '$baseUrl/processRequest.php', + callType: ApiCallType.POST, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + params: { + 'devUUID': devUUID, + 'userUUID': userUUID, + 'cliID': cliID, + 'atividade': atividade, + 'licensePlate': licensePlate, + 'color': color, + 'model': model, + }, + bodyType: BodyType.X_WWW_FORM_URL_ENCODED, + returnBody: true, + encodeBodyUtf8: false, + decodeUtf8: false, + cache: false, + isStreamingApi: false, + alwaysAllowBody: false, + ); + } +} + +class UpdateVehicle { + Future call({ + required final int vehicleId, + final String? licensePlate, + final String? color, + final String? model, + }) async { + final String baseUrl = PhpGroup.getBaseUrl(); + final String devUUID = + (await StorageHelper().get(ProfileStorageKey.devUUID.key)) ?? ''; + final String userUUID = + (await StorageHelper().get(ProfileStorageKey.userUUID.key)) ?? ''; + final String cliID = + (await StorageHelper().get(ProfileStorageKey.clientUUID.key)) ?? ''; + const String atividade = 'updateVehicleToInsertRequest'; + + return await ApiManager.instance.makeApiCall( + callName: 'updateVehicle', + apiUrl: '$baseUrl/processRequest.php', + callType: ApiCallType.POST, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + params: { + 'devUUID': devUUID, + 'userUUID': userUUID, + 'cliID': cliID, + 'atividade': atividade, + 'licensePlate': licensePlate, + 'color': color, + 'model': model, + 'vehicleId': vehicleId + }, + bodyType: BodyType.X_WWW_FORM_URL_ENCODED, + returnBody: true, + encodeBodyUtf8: false, + decodeUtf8: false, + cache: false, + isStreamingApi: false, + alwaysAllowBody: false, + ); + } + static GetCategories getCategories = GetCategories(); static GetDocuments getDocuments = GetDocuments(); } diff --git a/lib/features/backend/api_requests/api_manager.dart b/lib/features/backend/api_requests/api_manager.dart index 72087153..d12c03dd 100644 --- a/lib/features/backend/api_requests/api_manager.dart +++ b/lib/features/backend/api_requests/api_manager.dart @@ -496,11 +496,11 @@ class ApiManager { result = ApiCallResponse(null, {}, -1, exception: e); } - log('API Call: $callName'); - log('URL: $apiUrl'); - log('Headers: $headers'); - log('Params: $params'); - log('Response: ${result.jsonBody}'); + print('API Call: $callName'); + print('URL: $apiUrl'); + print('Headers: $headers'); + print('Params: $params'); + print('Response: ${result.jsonBody}'); return result; } } diff --git a/lib/features/history/presentation/pages/acess_history_page_widget.dart b/lib/features/history/presentation/pages/acess_history_page_widget.dart index 2d5253b1..f49fb1d5 100644 --- a/lib/features/history/presentation/pages/acess_history_page_widget.dart +++ b/lib/features/history/presentation/pages/acess_history_page_widget.dart @@ -46,12 +46,15 @@ class _AccessHistoryState extends State { selectedTypeSubject.listen((value) {}); } + + @override void initState() { super.initState(); _model = createModel(context, () => AcessHistoryPageModel()); _accessFuture = fetchAccessHistoryService(); + _scrollController = ScrollController() ..addListener(() { if (_scrollController.position.atEdge && diff --git a/lib/features/local/data/repositories/locals_repository_impl.dart b/lib/features/local/data/repositories/locals_repository_impl.dart index a1cf5fdc..71956dae 100644 --- a/lib/features/local/data/repositories/locals_repository_impl.dart +++ b/lib/features/local/data/repositories/locals_repository_impl.dart @@ -80,7 +80,6 @@ class LocalsRepositoryImpl implements LocalsRepository { } } else { log('_handleLocal -> Local selected'); - return true; } diff --git a/lib/features/local/utils/local_util.dart b/lib/features/local/utils/local_util.dart index 04a042da..c555238e 100644 --- a/lib/features/local/utils/local_util.dart +++ b/lib/features/local/utils/local_util.dart @@ -115,6 +115,9 @@ class LocalUtil { .set(LocalsStorageKey.whatsapp.key, jsonBody['whatsapp'] ?? false); await StorageHelper().set( LocalsStorageKey.provisional.key, jsonBody['provisional'] ?? false); + await StorageHelper().set(LocalsStorageKey.vehicleAutoApproval.key, + jsonBody['vehicleAutoApproval'] ?? false); + await StorageHelper().set( LocalsStorageKey.pets.key, jsonBody['pet'] ?? false, @@ -141,6 +144,13 @@ class LocalUtil { jsonBody['petAmountRegister']?.toString().isEmpty ?? true ? '0' : jsonBody['petAmountRegister'].toString()); + + await StorageHelper().set( + LocalsStorageKey.vehicleAmountRegister.key, + jsonBody['vehicleAmountRegister']?.toString().isEmpty ?? true + ? '0' + : jsonBody['vehicleAmountRegister'].toString()); + await StorageHelper().set(ProfileStorageKey.userName.key, jsonBody['visitado']['VDO_NOME'] ?? ''); await StorageHelper().set(ProfileStorageKey.userEmail.key, diff --git a/lib/features/menu/data/data_sources/menu_local_data_source.dart b/lib/features/menu/data/data_sources/menu_local_data_source.dart index 796378cb..ad8502f8 100644 --- a/lib/features/menu/data/data_sources/menu_local_data_source.dart +++ b/lib/features/menu/data/data_sources/menu_local_data_source.dart @@ -17,10 +17,6 @@ abstract class MenuLocalDataSource { Future handleMenu(EnumMenuItem item, EnumDisplay display, MenuEntry opt, List entries); - - Future processStartDate(String startDate, MenuEntry entry); - - Future processExpirationDate(String expirationDate, MenuEntry entry); } class MenuLocalDataSourceImpl implements MenuLocalDataSource { @@ -92,30 +88,4 @@ class MenuLocalDataSourceImpl implements MenuLocalDataSource { log('Error processing display for module ${opt.key}: $e'); } } - - @override - Future processStartDate(String startDate, MenuEntry opt) async { - try { - if (startDate.isEmpty) return true; - final start = DateTime.tryParse(startDate); - if (start == null) return false; - return DateTime.now().isAfter(start); - } catch (e) { - log('Error processing start date for module ${opt.key}: $e'); - } - return false; - } - - @override - Future processExpirationDate( - String expirationDate, MenuEntry opt) async { - try { - if (expirationDate.isEmpty) return false; - final expiration = DateTime.tryParse(expirationDate); - return expiration != null && DateTime.now().isAfter(expiration); - } catch (e) { - log('Error processing expiration date for module ${opt.key}: $e'); - } - return false; - } } diff --git a/lib/features/menu/data/repositories/menu_repository_impl.dart b/lib/features/menu/data/repositories/menu_repository_impl.dart index 96eec795..0a73d855 100644 --- a/lib/features/menu/data/repositories/menu_repository_impl.dart +++ b/lib/features/menu/data/repositories/menu_repository_impl.dart @@ -4,6 +4,7 @@ import 'package:hub/features/menu/index.dart'; import 'package:hub/features/module/index.dart'; import 'package:hub/features/storage/index.dart'; import 'package:hub/flutter_flow/custom_functions.dart'; +import 'package:hub/shared/utils/datetime_util.dart'; class MenuRepositoryImpl implements MenuRepository { final MenuLocalDataSource menuDataSource = MenuLocalDataSourceImpl(); @@ -25,10 +26,9 @@ class MenuRepositoryImpl implements MenuRepository { final display = EnumDisplay.fromString(licenseMap['display']); final startDate = licenseMap['startDate'] ?? ''; final expirationDate = licenseMap['expirationDate'] ?? ''; - final isStarted = - await menuDataSource.processStartDate(startDate, entry); + final isStarted = await DateTimeUtil.processStartDate(startDate); final isExpired = - await menuDataSource.processExpirationDate(expirationDate, entry); + await DateTimeUtil.processExpirationDate(expirationDate); if (isStarted && !isExpired) { await menuDataSource.handleMenu(menuItem, display, entry, entries); } diff --git a/lib/features/menu/presentation/mappers/menu_entry.dart b/lib/features/menu/presentation/mappers/menu_entry.dart index ad09f824..4715118c 100644 --- a/lib/features/menu/presentation/mappers/menu_entry.dart +++ b/lib/features/menu/presentation/mappers/menu_entry.dart @@ -76,6 +76,16 @@ class MenuEntry implements BaseModule { route: '/residentsOnThePropertyPage', types: [MenuEntryType.Property], ), + // MenuEntry( + // key: 'FRE-HUB-VEHICLES-MANAGER', + // icon: Icons.directions_car, + // name: FFLocalizations.of(navigatorKey.currentContext!).getVariableText( + // ptText: 'Veículos', + // enText: 'Vehicles', + // ), + // route: '/vehiclesOnThePropertyPage', + // types: [MenuEntryType.Property], + // ), MenuEntry( key: 'FRE-HUB-VEHICLES', icon: Icons.directions_car, diff --git a/lib/features/module/data/data_sources/license_local_data_source.dart b/lib/features/module/data/data_sources/license_local_data_source.dart index cc377225..4cbe143f 100644 --- a/lib/features/module/data/data_sources/license_local_data_source.dart +++ b/lib/features/module/data/data_sources/license_local_data_source.dart @@ -94,7 +94,7 @@ class LicenseLocalDataSourceImpl implements LicenseLocalDataSource { Future isNewVersion() async { final String? reponse = await StorageHelper().get(LocalsStorageKey.isNewVersion.key); - final bool isNewVersion = reponse.toBoolean(); + final bool isNewVersion = reponse.toBoolean; return isNewVersion; } diff --git a/lib/features/module/domain/entities/license.dart b/lib/features/module/domain/entities/license.dart index b5b73407..91c88e43 100644 --- a/lib/features/module/domain/entities/license.dart +++ b/lib/features/module/domain/entities/license.dart @@ -10,6 +10,7 @@ enum LicenseKeys { access('FRE-HUB-ACCESS'), openedVisits('FRE-HUB-OPENED-VISITS'), vehicles('FRE-HUB-VEHICLES'), + vehiclesManager('FRE-HUB-VEHICLES-MANAGER'), residents('FRE-HUB-RESIDENTS'), about('FRE-HUB-ABOUT-SYSTEM'), pets('FRE-HUB-PETS'), @@ -63,7 +64,7 @@ class License { static Future _precessWpp() async { final bool whatsapp = await StorageHelper() .get(LocalsStorageKey.whatsapp.key) - .then((v) => v.toBoolean()); + .then((v) => v.toBoolean); if (whatsapp) return ModuleStatus.active.key; else @@ -73,7 +74,7 @@ class License { static Future _processProvisional() async { final bool provisional = await StorageHelper() .get(LocalsStorageKey.provisional.key) - .then((v) => v.toBoolean()); + .then((v) => v.toBoolean); if (provisional) return ModuleStatus.active.key; else @@ -83,7 +84,7 @@ class License { static Future _processPets() async { final bool pets = await StorageHelper() .get(LocalsStorageKey.pets.key) - .then((v) => v.toBoolean()); + .then((v) => v.toBoolean); if (pets) return ModuleStatus.active.key; else @@ -163,6 +164,13 @@ class License { startDate: '', quantity: 0, ), + Module( + key: LicenseKeys.vehicles.value, + display: ModuleStatus.inactive.key, + expirationDate: '', + startDate: '', + quantity: 0, + ), Module( key: LicenseKeys.residents.value, display: isNewVersionWithModule diff --git a/lib/features/notification/deep_link_service.dart b/lib/features/notification/deep_link_service.dart index 003f1903..aac0351c 100644 --- a/lib/features/notification/deep_link_service.dart +++ b/lib/features/notification/deep_link_service.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:developer'; import 'package:app_links/app_links.dart'; import 'package:flutter/material.dart'; import 'package:hub/features/storage/index.dart'; diff --git a/lib/features/storage/enums/database_storage_key.dart b/lib/features/storage/enums/database_storage_key.dart index f4028646..92442313 100644 --- a/lib/features/storage/enums/database_storage_key.dart +++ b/lib/features/storage/enums/database_storage_key.dart @@ -34,7 +34,9 @@ enum LocalsStorageKey implements DatabaseStorageKey { panic('fre_panic'), person('fre_person'), requestOSNotification('fre_requestOSnotification'), - isNewVersion('fre_isNewVersion'); + isNewVersion('fre_isNewVersion'), + vehicleAutoApproval('fre_vehicleAutoApproval'), + vehicleAmountRegister('fre_vehicleAmountRegister'); final String key; diff --git a/lib/flutter_flow/flutter_flow_util.dart b/lib/flutter_flow/flutter_flow_util.dart index bcb34fe5..1aa2f180 100644 --- a/lib/flutter_flow/flutter_flow_util.dart +++ b/lib/flutter_flow/flutter_flow_util.dart @@ -529,7 +529,7 @@ void setAppLanguage(BuildContext context, String language) => void setDarkModeSetting(BuildContext context, ThemeMode themeMode) => App.of(context).setThemeMode(themeMode); -void showSnackbar( +void showSnackbarMessenger( BuildContext context, String message, bool error, { @@ -537,38 +537,47 @@ void showSnackbar( int duration = 4, }) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - if (loading) - Padding( - padding: const EdgeInsetsDirectional.only(end: 10.0), - child: SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - color: FlutterFlowTheme.of(context).info, - ), + ScaffoldMessenger.of(context) + .showSnackBar(showSnackbar(context, message, error)); +} + +SnackBar showSnackbar( + BuildContext context, + String message, + bool error, { + bool loading = false, + int duration = 4, +}) { + return SnackBar( + content: Row( + children: [ + if (loading) + Padding( + padding: const EdgeInsetsDirectional.only(end: 10.0), + child: SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: FlutterFlowTheme.of(context).info, ), ), - Text( - message, - style: TextStyle( - color: FlutterFlowTheme.of(context).info, - fontSize: LimitedFontSizeUtil.getBodyFontSize(context), - ), ), - ], - ), - duration: Duration(seconds: duration), - backgroundColor: error - ? FlutterFlowTheme.of(context).error - : FlutterFlowTheme.of(context).success, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), - ), + Text( + message, + style: TextStyle( + color: FlutterFlowTheme.of(context).info, + fontSize: LimitedFontSizeUtil.getBodyFontSize(context), + ), + ), + ], + ), + duration: Duration(seconds: duration), + backgroundColor: error + ? FlutterFlowTheme.of(context).error + : FlutterFlowTheme.of(context).success, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), ), ); } diff --git a/lib/flutter_flow/nav/nav.dart b/lib/flutter_flow/nav/nav.dart index 26396286..e42b6c74 100644 --- a/lib/flutter_flow/nav/nav.dart +++ b/lib/flutter_flow/nav/nav.dart @@ -198,7 +198,7 @@ GoRouter createRouter(AppStateNotifier appStateNotifier) { FFRoute( name: 'vehiclesOnThePropertyPage', path: '/vehiclesOnThePropertyPage', - builder: (context, params) => const VehicleOnTheProperty()), + builder: (context, params) => const VehiclePage()), FFRoute( name: 'receptionPage', path: '/receptionPage', diff --git a/lib/initialization.dart b/lib/initialization.dart index 13a47538..82902a36 100644 --- a/lib/initialization.dart +++ b/lib/initialization.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:app_tracking_transparency/app_tracking_transparency.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; @@ -29,7 +27,8 @@ Future initializeApp() async { Future _initializeTracking() async { log('Requesting tracking authorization...'); await AppTrackingTransparency.requestTrackingAuthorization(); - log('Tracking authorization requested'); + + print('Tracking authorization requested'); } Future _initializeFirebase() async { @@ -51,12 +50,14 @@ void _initializeUrlStrategy() { } Future _initializeSystemSettings() async { - log('Initializing System Settings...'); - final crashlyticsInstance = FirebaseCrashlytics.instance; + print('Initializing System Settings...'); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + final crashlyticsInstance = FirebaseCrashlytics.instance; + if (kDebugMode) { - log('Debug mode'); + print('Debug mode'); + await crashlyticsInstance.setCrashlyticsCollectionEnabled(false); } else { log('Release mode'); @@ -72,10 +73,22 @@ Future _initializeSystemSettings() async { // } await crashlyticsInstance.setCrashlyticsCollectionEnabled(true); - // if (crashlyticsInstance.isCrashlyticsCollectionEnabled) { - FlutterError.onError = crashlyticsInstance.recordFlutterError; - log('Crashlytics enabled'); - // } + if (crashlyticsInstance.isCrashlyticsCollectionEnabled) { + // Configura o tratamento de erros não capturados + FlutterError.onError = crashlyticsInstance.recordFlutterError; + + crashlyticsInstance.checkForUnsentReports().then((unsentReports) { + if (unsentReports) { + crashlyticsInstance.sendUnsentReports(); + print('Existem relatórios de falhas não enviados.'); + } else { + print('Todos os relatórios de falhas foram enviados.'); + } + }).catchError((error) { + print('Erro ao verificar ou enviar relatórios não enviados: $error'); + }); + } + print('Crashlytics enabled'); } } diff --git a/lib/pages/liberation_history/liberation_history_widget.dart b/lib/pages/liberation_history/liberation_history_widget.dart index f596ff8f..5a199022 100644 --- a/lib/pages/liberation_history/liberation_history_widget.dart +++ b/lib/pages/liberation_history/liberation_history_widget.dart @@ -299,7 +299,7 @@ class _LiberationHistoryWidgetState extends State { ) .then((message) { if (message != null || message != '') { - showSnackbar( + showSnackbarMessenger( context, FFLocalizations.of(context).getVariableText( enText: 'Successfully resolved visit', @@ -308,7 +308,7 @@ class _LiberationHistoryWidgetState extends State { false, ); } else { - showSnackbar(context, message, true); + showSnackbarMessenger(context, message, true); } }).whenComplete(() { safeSetState(() { diff --git a/lib/pages/pets_page/pets_page_model.dart b/lib/pages/pets_page/pets_page_model.dart index 4d277121..a0c2fd31 100644 --- a/lib/pages/pets_page/pets_page_model.dart +++ b/lib/pages/pets_page/pets_page_model.dart @@ -478,7 +478,7 @@ class PetsPageModel extends FlutterFlowModel { context.pop(value); if (value == false) { - showSnackbar( + showSnackbarMessenger( context, FFLocalizations.of(context).getVariableText( ptText: 'Erro ao excluir pet', @@ -487,7 +487,7 @@ class PetsPageModel extends FlutterFlowModel { true, ); } else if (value == true) { - showSnackbar( + showSnackbarMessenger( context, FFLocalizations.of(context).getVariableText( enText: 'Success deleting pet', @@ -498,7 +498,7 @@ class PetsPageModel extends FlutterFlowModel { } }).catchError((err, stack) { context.pop(); - showSnackbar( + showSnackbarMessenger( context, FFLocalizations.of(context).getVariableText( enText: 'Error deleting pet', diff --git a/lib/pages/pets_page/pets_page_widget.dart b/lib/pages/pets_page/pets_page_widget.dart index 8816c468..0fb09390 100644 --- a/lib/pages/pets_page/pets_page_widget.dart +++ b/lib/pages/pets_page/pets_page_widget.dart @@ -86,9 +86,9 @@ class _PetsPageWidgetState extends State ); } - void onEditingChanged(bool value) { + void onEditingChanged([bool? value]) { setState(() { - _model.handleEditingChanged(value); + _model.handleEditingChanged(value!); }); } diff --git a/lib/pages/reception_page/reception_page_widget.dart b/lib/pages/reception_page/reception_page_widget.dart index 98760970..ba986784 100644 --- a/lib/pages/reception_page/reception_page_widget.dart +++ b/lib/pages/reception_page/reception_page_widget.dart @@ -1,3 +1,4 @@ +import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; diff --git a/lib/pages/schedule_complete_visit_page/schedule_complete_visit_page_model.dart b/lib/pages/schedule_complete_visit_page/schedule_complete_visit_page_model.dart index 18647b22..3da22d1f 100644 --- a/lib/pages/schedule_complete_visit_page/schedule_complete_visit_page_model.dart +++ b/lib/pages/schedule_complete_visit_page/schedule_complete_visit_page_model.dart @@ -472,7 +472,7 @@ class ScheduleCompleteVisitPageModel context.pop(value); if (value == false) { - showSnackbar( + showSnackbarMessenger( context, FFLocalizations.of(context).getVariableText( enText: 'Error blocking visit', @@ -481,7 +481,7 @@ class ScheduleCompleteVisitPageModel true, ); } else if (value == true) { - showSnackbar( + showSnackbarMessenger( context, FFLocalizations.of(context).getVariableText( enText: 'Success canceling visit', @@ -492,7 +492,7 @@ class ScheduleCompleteVisitPageModel } }).catchError((err, stack) { context.pop(); - showSnackbar( + showSnackbarMessenger( context, FFLocalizations.of(context).getVariableText( enText: 'Error blocking visit', diff --git a/lib/pages/vehicles_on_the_property/index.dart b/lib/pages/vehicles_on_the_property/index.dart new file mode 100644 index 00000000..f159a317 --- /dev/null +++ b/lib/pages/vehicles_on_the_property/index.dart @@ -0,0 +1,2 @@ +export 'vehicles_on_the_property.dart'; +export 'vehicle_model.dart'; diff --git a/lib/pages/vehicles_on_the_property/vehicle_history_screen.dart b/lib/pages/vehicles_on_the_property/vehicle_history_screen.dart new file mode 100644 index 00000000..ddbf6271 --- /dev/null +++ b/lib/pages/vehicles_on_the_property/vehicle_history_screen.dart @@ -0,0 +1,246 @@ +part of 'vehicles_on_the_property.dart'; + +class VehicleHistoryScreen extends StatefulWidget { + final VehicleModel model; + const VehicleHistoryScreen(this.model, {super.key}); + + @override + State createState() => _VehicleHistoryScreenState(); +} + +class _VehicleHistoryScreenState extends State + with Pageable { + final apiCall = PhpGroup.getVehiclesByProperty; + int totalOwnerVehicles = 0; + final PagingController _pagingController = + PagingController(firstPageKey: 1); + bool _isSnackble = true; + + @override + void initState() { + super.initState(); + _pagingController.addPageRequestListener( + (int pageKey) => fetchPage( + dataProvider: () async { + final newItems = await apiCall.call(pageKey.toString()); + if (newItems.jsonBody == null) return (false, null); + final List vehicles = + (newItems.jsonBody['vehicles'] as List?) ?? []; + + safeSetState(() { + totalOwnerVehicles = newItems.jsonBody['total_owner_vehicles'] ?? 0; + }); + + return (vehicles.isNotEmpty, vehicles); + }, + onDataUnavailable: (vehicles) { + setState(() {}); + final bool isFirst = pageKey == 2; + if (!isFirst && _isSnackble) showNoMoreDataSnackBar(context); + + _pagingController.appendLastPage(vehicles); + }, + onDataAvailable: (vehicles) { + setState(() {}); + _pagingController.appendPage(vehicles, pageKey + 1); + }, + onFetchError: (e, s) { + DialogUtil.errorDefault(context); + LogUtil.requestAPIFailed( + "proccessRequest.php", "", "Consulta de Veículo", e, s); + setState(() {}); + }, + ), + ); + _pagingController.addStatusListener(_showError); + } + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } + + Future _showError(PagingStatus status) async { + if (status == PagingStatus.subsequentPageError) { + final message = FFLocalizations.of(context).getVariableText( + enText: 'Something went wrong while fetching a new page.', + ptText: 'Algo deu errado ao buscar uma nova página.', + ); + final retry = FFLocalizations.of(context).getVariableText( + enText: 'Retry', + ptText: 'Recarregar', + ); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + action: SnackBarAction( + label: retry, + onPressed: () => _pagingController.retryLastFailedRequest(), + ), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _buildHeader(context), + _buildBody(context), + ], + ); + } + + Widget _buildHeader(BuildContext context) { + final bodyFontSize = LimitedFontSizeUtil.getBodyFontSize(context); + final headerTitle = FFLocalizations.of(context).getVariableText( + ptText: "Meus Veículos: ", + enText: "My Vehicles: ", + ); + final totalRegisteredVehicles = widget.model.amountRegister; + + return SizedBox( + height: 30, + child: Center( + child: Text( + (widget.model.amountRegister == '0' || + widget.model.amountRegister == null) + ? '' + : "$headerTitle $totalOwnerVehicles/$totalRegisteredVehicles", + textAlign: TextAlign.right, + style: TextStyle( + fontFamily: 'Nunito', + fontSize: bodyFontSize, + ), + ), + ), + ); + } + + Expanded _buildBody(BuildContext context) { + final noDataFound = FFLocalizations.of(context).getVariableText( + ptText: "Nenhum veículo encontrado!", + enText: "No vehicle found", + ); + return buildPaginatedListView( + noDataFound, + _pagingController, + _generateItems, + ); + } + + Widget _generateItems( + BuildContext context, + dynamic item, + int index, + ) { + log('item: $item'); + final bool? isOwner = item['isOwnerVehicle']; + final IconData iconData = + isOwner == true ? Symbols.garage : Symbols.directions_car; + + final FreCardIcon? cardIcon = isOwner != null + ? FreCardIcon( + height: 50, + width: 100, + icon: Icon(iconData, size: 80, opticalSize: 10), + ) + : null; + + final String? tag = item['tag']; + final bool containTag = tag.isNotNullAndEmpty; + final Map labelsHashMap = + _generateLabelsHashMap(context, item, tag, containTag); + + final List> statusHashMapList = + _generateStatusHashMapList(item); + + Future onTapCardItemAction() async { + await _handleCardItemTap(context, cardIcon, item); + } + + final statusLinkedHashMap = statusHashMapList + .map((map) => LinkedHashMap.from(map)) + .toList(); + final length = statusLinkedHashMap.expand((e) => [e.length]); + final double itemWidthFactor = length == 1 ? 0.25 : 0.50; + + return CardItemTemplateComponentWidget( + icon: cardIcon, + labelsHashMap: labelsHashMap, + statusHashMap: statusHashMapList, + onTapCardItemAction: onTapCardItemAction, + itemWidthFactor: itemWidthFactor, + ); + } + + Map _generateLabelsHashMap( + BuildContext context, + Map item, + String? tag, + bool containTag, + ) { + final localization = FFLocalizations.of(context); + return { + '${localization.getVariableText(ptText: "Placa", enText: "License Plate")}:': + item['licensePlate'] ?? '', + '${localization.getVariableText(ptText: "Modelo", enText: "Model")}:': + item['model'] ?? '', + '${localization.getVariableText(ptText: "Proprietário", enText: "Owner")}:': + item['personName'] ?? '', + if (containTag) + '${localization.getVariableText(ptText: "Tag", enText: "Tag")}:': + tag ?? '', + }; + } + + List> _generateStatusHashMapList( + Map item) { + final statusHashMap = widget.model.generateStatusColorMap(item, false); + return statusHashMap != null ? [statusHashMap] : []; + } + + Future _handleCardItemTap( + BuildContext context, + FreCardIcon? cardIcon, + Map item, + ) async { + try { + final dialogContent = widget.model.buildVehicleDetails( + icon: cardIcon, + item: item, + context: context, + model: widget.model, + ); + + await showDialog( + useSafeArea: true, + context: context, + builder: (context) => Dialog( + alignment: Alignment.center, + child: dialogContent, + ), + ) // + .then((response) async { + if (response == true) { + _isSnackble = false; + _pagingController.refresh(); + } else { + _isSnackble = true; + } + }) // + .whenComplete(() {}); + } catch (e, s) { + DialogUtil.errorDefault(context); + LogUtil.requestAPIFailed( + "proccessRequest.php", "", "Consulta de Veículos", e, s); + safeSetState(() { + // _hasData = false; + // _loading = false; + }); + } + } +} diff --git a/lib/pages/vehicles_on_the_property/vehicle_model.dart b/lib/pages/vehicles_on_the_property/vehicle_model.dart index 826095f5..61a53dc5 100644 --- a/lib/pages/vehicles_on_the_property/vehicle_model.dart +++ b/lib/pages/vehicles_on_the_property/vehicle_model.dart @@ -1,54 +1,558 @@ -import 'package:flutter/material.dart'; -import 'package:hub/components/templates_components/details_component/details_component_widget.dart'; -import 'package:hub/flutter_flow/flutter_flow_model.dart'; -import 'package:hub/flutter_flow/internationalization.dart'; -import 'package:hub/pages/vehicles_on_the_property/vehicles_on_the_property.dart'; +import 'dart:developer'; -class VehicleModel extends FlutterFlowModel { +import 'package:flutter/material.dart'; +import 'package:hub/components/templates_components/card_item_template_component/card_item_template_component_widget.dart'; +import 'package:hub/components/templates_components/details_component/details_component_widget.dart'; +import 'package:hub/features/backend/index.dart'; +import 'package:hub/features/storage/index.dart'; +import 'package:hub/flutter_flow/index.dart'; +import 'package:hub/pages/vehicles_on_the_property/vehicles_on_the_property.dart'; +import 'package:hub/shared/extensions/index.dart'; +import 'package:hub/shared/utils/index.dart'; + +/// [VehicleModel] is a class that contains the business logic of the vehicle page. +class VehicleModel extends FlutterFlowModel + with + _BaseVehiclePage, + _VehicleHistoryScreenModel, + _VehicleRegisterScreenModel, + _VehicleUpdateScreenModel { + /// [VehicleModel] is a singleton class that contains the business logic of the vehicle page. static VehicleModel? _instance = VehicleModel._internal(); VehicleModel._internal(); factory VehicleModel() => _instance ?? VehicleModel._internal(); static void resetInstance() => _instance = null; - dynamic item; + final GlobalKey registerFormKey = GlobalKey(); + final GlobalKey updateFormKey = GlobalKey(); @override void initState(BuildContext context) { + log('VehicleModel -> initState'); resetInstance(); - initAsync(); + initializeControllers(context); + WidgetsBinding.instance.addPostFrameCallback((_) async { + amountRegister = '0'; + }); + } + + void initializeControllers(BuildContext context) { + tabBarController = TabController( + vsync: Navigator.of(context), + length: 2, + ); + + textFieldFocusLicensePlate = FocusNode(); + textFieldControllerLicensePlate = TextEditingController(); + + textFieldFocusColor = FocusNode(); + textFieldControllerColor = TextEditingController(); + + textFieldFocusModel = FocusNode(); + textFieldControllerModel = TextEditingController(); } @override - void dispose() {} + void dispose() { + disposeControllers(); + } - Future initAsync() async {} + void disposeControllers() { + tabBarController.dispose(); + textFieldFocusLicensePlate!.dispose(); + textFieldControllerLicensePlate!.dispose(); + textFieldFocusColor!.dispose(); + textFieldControllerColor!.dispose(); + textFieldFocusModel!.dispose(); + textFieldControllerModel!.dispose(); + } - Widget buildVehicleDetails({ - required dynamic item, - required BuildContext context, - required VehicleModel model, - }) { - return DetailsComponentWidget( - buttons: [], - labelsHashMap: Map.from({ - if (item['model'] != null && item['model'] != '') - '${FFLocalizations.of(context).getVariableText(ptText: "Modelo", enText: "Model")}:': - item['model'].toString().toUpperCase(), - if (item['licensePlate'] != null && item['licensePlate'] != '') - '${FFLocalizations.of(context).getVariableText(ptText: "Placa", enText: "License Plate")}:': - item['licensePlate'].toString().toUpperCase(), - if (item['color'] != null && item['color'] != '') - '${FFLocalizations.of(context).getVariableText(ptText: "Cor", enText: "Color")}:': - item['color'].toString().toUpperCase(), - if (item['personName'] != null && item['personName'] != '') - '${FFLocalizations.of(context).getVariableText(ptText: "Proprietário", enText: "Owner")}:': - item['personName'].toString().toUpperCase(), - if (item['tag'] != null && item['tag'] != '') - '${FFLocalizations.of(context).getVariableText(ptText: "Tag", enText: "Tag")}:': - item['tag'].toString().toUpperCase(), - }), - statusHashMap: [], + Future initAsync() async { + amountRegister = + await StorageHelper().get(LocalsStorageKey.vehicleAmountRegister.key); + autoApproval = + await StorageHelper().get(LocalsStorageKey.vehicleAutoApproval.key); + } + + bool isFormValid(BuildContext context, GlobalKey formKey) { + if (formKey.currentState == null) return false; + return formKey.currentState!.validate(); + } +} + +/// [_BaseVehiclePage] is a mixin that contains the base logic of the vehicle page. +mixin class _BaseVehiclePage { + int count = 0; + late final TabController tabBarController; + // dynamic item; + late int vehicleId; + BuildContext context = navigatorKey.currentContext!; + bool isEditing = false; + ApiCallResponse? vehicleResponse; + String? amountRegister = '0'; + late final String? autoApproval; + + VoidCallback? onUpdateVehicle; + VoidCallback? onRegisterVehicle; + VoidCallback? safeSetState; + + FocusNode? textFieldFocusLicensePlate; + TextEditingController? textFieldControllerLicensePlate; + String? textControllerLicensePlateValidator( + BuildContext context, String? value) { + final validationMessage = validateField( + context, value, 'Placa é obrigatória', 'License Plate is required'); + if (validationMessage != null) return validationMessage; + + final brazilianPlateRegex = RegExp(r'^[A-Z]{3}\d{4}$'); + final mercosurPlateRegex = RegExp(r'^[A-Z]{3}\d[A-Z0-9]\d{2}$'); + + if (!brazilianPlateRegex.hasMatch(value!) && + !mercosurPlateRegex.hasMatch(value)) { + return FFLocalizations.of(context).getVariableText( + ptText: 'Placa inválida', + enText: 'Invalid license plate', + ); + } + + return null; + } + + FocusNode? textFieldFocusColor; + TextEditingController? textFieldControllerColor; + String? textControllerColorValidator(BuildContext contexnt, String? value) { + return validateField( + context, value, 'Cor é obrigatória', 'Color is required'); + } + + FocusNode? textFieldFocusModel; + TextEditingController? textFieldControllerModel; + String? textControllerModelValidator(BuildContext contexnt, String? value) { + return validateField( + context, value, 'Modelo é obrigatório', 'Model is required'); + } + + String? validateField( + BuildContext context, String? value, String ptText, String enText) { + if (value == null || value.isEmpty) { + return FFLocalizations.of(context).getVariableText( + ptText: ptText, + enText: enText, + ); + } + return null; + } + + Future switchTab(int index) async { + tabBarController.animateTo(index); + if (index == 0) await handleEditingChanged(false); + safeSetState?.call(); + } + + Future clearFields() async { + textFieldControllerLicensePlate = TextEditingController(text: ''); + textFieldControllerColor = TextEditingController(text: ''); + textFieldControllerModel = TextEditingController(text: ''); + } + + Future handleEditingChanged(bool editing) async { + isEditing = editing; + await clearFields(); + } + + Future setEditForm(dynamic item) async { + if (item != null) { + log('vehicleId: ${item['vehicleId']}'); + vehicleId = item['vehicleId']; + + textFieldControllerLicensePlate = TextEditingController( + text: item != null ? item['licensePlate'] ?? '' : ''); + textFieldControllerColor = + TextEditingController(text: item != null ? item['color'] ?? '' : ''); + textFieldControllerModel = + TextEditingController(text: item != null ? item['model'] ?? '' : ''); + } + } + + Future handleVehicleResponse(ApiCallResponse response, + String successMessage, String errorMessage) async { + if (response.jsonBody['error'] == false) { + await DialogUtil.success(context, successMessage).then((_) async { + await switchTab(0); + }); + } else { + String errorMsg; + try { + errorMsg = response.jsonBody['error_msg']; + } catch (e) { + errorMsg = errorMessage; + } + await DialogUtil.error(context, errorMsg); + } + } +} + +/// [_VehicleRegisterScreenModel] is a mixin that contains the business logic of the vehicle register page. +mixin _VehicleRegisterScreenModel on _BaseVehiclePage { + Future registerVehicle() async { + final response = await PhpGroup.registerVehicle.call( + licensePlate: textFieldControllerLicensePlate!.text, + color: textFieldControllerColor!.text, + model: textFieldControllerModel!.text, + ); + await handleVehicleResponse( + response, + FFLocalizations.of(context).getVariableText( + ptText: 'Veículo cadastrado com sucesso', + enText: 'Vehicle registered successfully', + ), + FFLocalizations.of(context).getVariableText( + ptText: 'Erro ao cadastrar veículo', + enText: 'Error registering vehicle', + ), + ); + } +} + +/// [_VehicleUpdateScreenModel] is a mixin that contains the business logic of the vehicle update page. +mixin _VehicleUpdateScreenModel on _BaseVehiclePage { + Future updateVehicle() async { + final response = await PhpGroup.updateVehicle.call( + licensePlate: textFieldControllerLicensePlate!.text, + color: textFieldControllerColor!.text, + model: textFieldControllerModel!.text, + vehicleId: vehicleId, + ); + await handleVehicleResponse( + response, + FFLocalizations.of(context).getVariableText( + ptText: 'Veículo atualizado com sucesso', + enText: 'Vehicle updated successfully', + ), + FFLocalizations.of(context).getVariableText( + ptText: 'Erro ao atualizar veículo', + enText: 'Error updating vehicle', + ), + ); + } +} + +/// [_VehicleHistoryScreenModel] is a mixin that contains the business logic of the vehicle history page. +mixin _VehicleHistoryScreenModel on _BaseVehiclePage { + Map? generateStatusColorMap(dynamic uItem, bool isDetail) { + if (autoApproval.toBoolean == true) return null; + final theme = FlutterFlowTheme.of(context); + final localization = FFLocalizations.of(context); + + final status = uItem['status']; + final isOwner = uItem['isOwnerVehicle']; + + if (isOwner == null && status == null) return null; + + String byLanguage(String en, String pt) => + localization.getVariableText(enText: en, ptText: pt); + + final vehicleStatusMap = { + "ATI": { + "text": byLanguage('Active', 'Ativo'), + "color": theme.success, + }, + "INA": { + "text": byLanguage('Inactive', 'Inativo'), + "color": theme.accent2, + }, + "APR_CREATE": { + "text": byLanguage('Awaiting Creation', 'Aguardando Criação'), + "color": theme.success, + }, + "APR_DELETE": { + "text": byLanguage('Awaiting Deletion', 'Aguardando Exclusão'), + "color": theme.error, + }, + "APR_UPDATE": { + "text": byLanguage('Awaiting Update', 'Aguardando Atualização'), + "color": theme.accent2, + }, + "AGU_CHANGE": { + "text": byLanguage('Awaiting Change', 'Aguardando Alteração'), + "color": theme.warning, + }, + }; + + if (vehicleStatusMap.containsKey(status)) { + final statusMap = { + vehicleStatusMap[status]!['text'] as String: + vehicleStatusMap[status]!['color'] as Color, + }; + + if (!isDetail && isOwner && (status != 'ATI' && status != 'INA')) { + statusMap[byLanguage('My Vehicle', 'Meu Veículo')] = theme.accent4; + } + + return statusMap; + } + + return {}; + } + + List generateActionButtons(dynamic item) { + final Color iconButtonColor = FlutterFlowTheme.of(context).primaryText; + final FFButtonOptions buttonOptions = FFButtonOptions( + height: 40, + color: FlutterFlowTheme.of(context).primaryBackground, + elevation: 0, + textStyle: TextStyle( + color: FlutterFlowTheme.of(context).primaryText, + fontSize: LimitedFontSizeUtil.getNoResizeFont(context, 15), + ), + splashColor: FlutterFlowTheme.of(context).success, + borderSide: BorderSide( + color: FlutterFlowTheme.of(context).primaryBackground, + width: 1, + ), + ); + if (item['status'] == null) return []; + + final updateText = FFLocalizations.of(context) + .getVariableText(ptText: 'Editar', enText: 'Edit'); + final updateIcon = Icon(Icons.edit, color: iconButtonColor); + Future updateOnPressed() async { + context.pop(); + isEditing = true; + + await switchTab(1); + await setEditForm(item); + } + + final cancelText = FFLocalizations.of(context) + .getVariableText(ptText: 'Cancelar', enText: 'Cancel'); + final cancelIcon = Icon(Icons.close, color: iconButtonColor); + Future cancelOnPressed() async { + showAlertDialog( + context, + FFLocalizations.of(context).getVariableText( + ptText: 'Cancelar Solicitação', + enText: 'Cancel Request', + ), + FFLocalizations.of(context).getVariableText( + ptText: 'Você tem certeza que deseja cancelar essa solicitação?', + enText: 'Are you sure you want to delete this request?', + ), + () async => await processCancelRequest(item['status'], item)); + } + + final deleteText = FFLocalizations.of(context) + .getVariableText(ptText: 'Excluir', enText: 'Delete'); + final deleteIcon = Icon(Icons.delete, color: iconButtonColor); + Future deleteOnPressed() async { + showAlertDialog( + context, + FFLocalizations.of(context).getVariableText( + ptText: 'Excluir Veículo', + enText: 'Delete Vehicle', + ), + FFLocalizations.of(context).getVariableText( + ptText: 'Você tem certeza que deseja excluir esse veículo?', + enText: 'Are you sure you want to delete this vehicle?', + ), + () async => processDeleteRequest(item), + ); + } + + final bool containStatus = item['status'] == null; + final bool isOwnerVehicle = item['isOwnerVehicle']; + final bool isAGU = item['status'].contains('AGU'); + final bool isAPR = item['status'].contains('APR'); + final bool isATI = item['status'].contains('ATI'); + + if (containStatus) + return [ + FFButtonWidget( + text: deleteText, + icon: deleteIcon, + onPressed: deleteOnPressed, + options: buttonOptions, + ), + ]; + + return [ + if (isAGU && isOwnerVehicle) + FFButtonWidget( + text: updateText, + icon: updateIcon, + onPressed: updateOnPressed, + options: buttonOptions, + ), + if ((isAPR || isAGU) && (isOwnerVehicle)) + FFButtonWidget( + text: cancelText, + icon: cancelIcon, + onPressed: cancelOnPressed, + options: buttonOptions, + ), + if (isATI && isOwnerVehicle) + FFButtonWidget( + text: deleteText, + icon: deleteIcon, + onPressed: deleteOnPressed, + options: buttonOptions, + ), + ]; + } + + Future processDeleteRequest(dynamic item) async { + log('processDeleteRequest -> item[$item]'); + bool result = await PhpGroup.deleteVehicle + .call( + vehicleId: item['vehicleId'], + licensePlate: item['licensePlate'], + model: item['model'], + color: item['color'], + ) + .then((value) { + // ignore: unrelated_type_equality_checks + if (value.jsonBody['error'] == true) { + final String errorMsg = value.jsonBody['error_msg']; + showSnackbarMessenger( + context, + FFLocalizations.of(context).getVariableText( + ptText: errorMsg, + enText: 'Error deleting vehicle', + ), + true, + ); + return false; + // ignore: unrelated_type_equality_checks + } + showSnackbarMessenger( + context, + FFLocalizations.of(context).getVariableText( + enText: 'Success deleting vehicle', + ptText: 'Succeso ao excluir veículo', + ), + false, + ); + return true; + }) // + .catchError((err, stack) { + showSnackbarMessenger( + context, + FFLocalizations.of(context).getVariableText( + enText: 'Error deleting vehicle', + ptText: 'Erro ao excluir veículo', + ), + true, + ); + return false; + }); + + context.pop(result); + context.pop(result); + + return result; + } + + Future processCancelRequest(String status, dynamic item) async { + try { + final ApiCallResponse value; + switch (status) { + case 'APR_CREATE': + value = await processCancelDeleteRequest(item); + break; + case 'AGU_CHANGE': + value = await processCancelUpdateRequest(item); + break; + case 'APR_DELETE': + value = await processCancelCreateRequest(item); + break; + default: + throw ArgumentError('Status inválido: $status'); + } + + final bool isError = value.jsonBody['error'] == true; + final String message = FFLocalizations.of(context).getVariableText( + ptText: value.jsonBody['error_msg'] ?? 'Erro ao cancelar solicitação', + enText: + isError ? 'Error canceling request' : 'Success canceling request', + ); + showSnackbarMessenger(context, message, isError); + context.pop(!isError); + context.pop(!isError); + return !isError; + } catch (err) { + final String errorMessage = FFLocalizations.of(context).getVariableText( + ptText: 'Erro ao cancelar solicitação', + enText: 'Error canceling request', + ); + showSnackbarMessenger(context, errorMessage, true); + context.pop(false); + context.pop(false); + return false; + } + } + + Future processCancelDeleteRequest(dynamic item) async { + return await PhpGroup.deleteVehicle.call( + vehicleId: item['vehicleId'], + licensePlate: item['licensePlate'], + model: item['model'], + color: item['color'], + ); + } + + Future processCancelUpdateRequest(dynamic item) async { + return await PhpGroup.deleteVehicle.call( + vehicleId: item['vehicleId'], + licensePlate: item['licensePlate'], + model: item['model'], + color: item['color'], + ); + } + + Future processCancelCreateRequest(dynamic item) async { + return await PhpGroup.cancelDelete.call( + vehicleId: item['vehicleId'], + licensePlate: item['licensePlate'], + model: item['model'], + color: item['color'], + ); + } + + Map generateLabelsHashMap(dynamic item) { + return { + if (item['model'] != null && item['model'] != '') + '${FFLocalizations.of(context).getVariableText(ptText: "Modelo", enText: "Model")}:': + item['model'].toString().toUpperCase(), + if (item['licensePlate'] != null && item['licensePlate'] != '') + '${FFLocalizations.of(context).getVariableText(ptText: "Placa", enText: "License Plate")}:': + item['licensePlate'].toString().toUpperCase(), + if (item['color'] != null && item['color'] != '') + '${FFLocalizations.of(context).getVariableText(ptText: "Cor", enText: "Color")}:': + item['color'].toString().toUpperCase(), + if (item['personName'] != null && item['personName'] != '') + '${FFLocalizations.of(context).getVariableText(ptText: "Proprietário", enText: "Owner")}:': + item['personName'].toString().toUpperCase(), + if (item['tag'] != null && item['tag'] != '') + '${FFLocalizations.of(context).getVariableText(ptText: "Tag", enText: "Tag")}:': + item['tag'].toString().toUpperCase(), + }; + } + + DetailsComponentWidget buildVehicleDetails({ + required dynamic item, + required BuildContext context, + required VehicleModel model, + required FreCardIcon? icon, + }) { + final status = generateStatusColorMap(item, true); + final buttons = generateActionButtons(item); + final labels = generateLabelsHashMap(item); + return DetailsComponentWidget( + icon: icon, + buttons: buttons, + labelsHashMap: labels, + statusHashMap: [status], ); } } diff --git a/lib/pages/vehicles_on_the_property/vehicle_register_screen.dart b/lib/pages/vehicles_on_the_property/vehicle_register_screen.dart new file mode 100644 index 00000000..ff32d4ab --- /dev/null +++ b/lib/pages/vehicles_on_the_property/vehicle_register_screen.dart @@ -0,0 +1,141 @@ +part of 'vehicles_on_the_property.dart'; + +/// [VehicleRegisterScreen] is a StatefulWidget that displays a form to register a vehicle. + +// ignore: must_be_immutable +class VehicleRegisterScreen extends StatefulWidget { + VehicleRegisterScreen(this.model, {super.key}); + late VehicleModel model; + + @override + State createState() => _VehicleRegisterScreenState(); +} + +class _VehicleRegisterScreenState extends State { + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + _buildHeader(context), + _buildBody(context), + ], + ), + ); + } + + Form _buildBody(BuildContext context) { + return Form( + key: widget.model.registerFormKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildCustomInput( + context: context, + controller: widget.model.textFieldControllerLicensePlate!, + validator: widget.model.textControllerLicensePlateValidator, + focusNode: widget.model.textFieldFocusLicensePlate!, + labelText: FFLocalizations.of(context) + .getVariableText(ptText: 'Placa', enText: 'License Plate'), + hintText: FFLocalizations.of(context) + .getVariableText(ptText: 'Placa', enText: 'License Plate'), + suffixIcon: Symbols.format_color_text, + inputFormatters: [UpperCaseTextFormatter()], + maxLength: 7, + ), + _buildCustomInput( + context: context, + controller: widget.model.textFieldControllerModel!, + validator: widget.model.textControllerModelValidator, + focusNode: widget.model.textFieldFocusModel!, + labelText: FFLocalizations.of(context) + .getVariableText(ptText: 'Modelo', enText: 'Model'), + hintText: FFLocalizations.of(context).getVariableText( + ptText: 'Ex: Voyage, Ford', enText: 'e.g. Voyage, Ford'), + suffixIcon: Symbols.car_repair, + inputFormatters: [], + ), + _buildCustomInput( + context: context, + controller: widget.model.textFieldControllerColor!, + validator: widget.model.textControllerColorValidator, + focusNode: widget.model.textFieldFocusColor!, + labelText: FFLocalizations.of(context) + .getVariableText(ptText: 'Cor', enText: 'Color'), + hintText: FFLocalizations.of(context).getVariableText( + ptText: 'Ex: Preto, Amarelo, Branco', + enText: 'e.g. Black, Yellow, White'), + suffixIcon: Symbols.palette, + inputFormatters: [], + ), + Padding( + padding: const EdgeInsets.fromLTRB(70, 20, 70, 30), + child: SubmitButtonUtil( + labelText: FFLocalizations.of(context) + .getVariableText(ptText: 'Cadastrar', enText: 'Register'), + onPressed: widget.model + .isFormValid(context, widget.model.registerFormKey) + ? widget.model.registerVehicle + : null, + ), + ), + ], + ), + ); + } + + Widget _buildCustomInput({ + required BuildContext context, + required TextEditingController controller, + required String? Function(BuildContext, String?) validator, + required FocusNode focusNode, + required String labelText, + required String hintText, + required IconData suffixIcon, + required final List? inputFormatters, + int maxLength = 80, + }) { + return CustomInputUtil( + controller: controller, + validator: (value) => validator(context, value), + focusNode: focusNode, + labelText: labelText, + hintText: hintText, + suffixIcon: suffixIcon, + haveMaxLength: true, + onChanged: (value) => setState(() {}), + inputFormatters: inputFormatters, + maxLength: maxLength, + ); + } + + Align _buildHeader(BuildContext context) { + double limitedHeaderFontSize = + LimitedFontSizeUtil.getHeaderFontSize(context); + + return Align( + alignment: const AlignmentDirectional(-1.0, 0.0), + child: Padding( + padding: const EdgeInsetsDirectional.fromSTEB(24.0, 20, 0.0, 15), + child: Text( + FFLocalizations.of(context).getVariableText( + ptText: + 'Preencha o formulário de cadastro com os dados do seu veículo', + enText: 'Fill out the registration form with your vehicle data', + ), + textAlign: TextAlign.start, + style: FlutterFlowTheme.of(context).bodyMedium.override( + fontFamily: FlutterFlowTheme.of(context).bodyMediumFamily, + letterSpacing: 0.0, + useGoogleFonts: GoogleFonts.asMap() + .containsKey(FlutterFlowTheme.of(context).bodyMediumFamily), + fontSize: limitedHeaderFontSize, + ), + ), + ), + ); + } +} diff --git a/lib/pages/vehicles_on_the_property/vehicle_update_screen.dart b/lib/pages/vehicles_on_the_property/vehicle_update_screen.dart new file mode 100644 index 00000000..1f746b50 --- /dev/null +++ b/lib/pages/vehicles_on_the_property/vehicle_update_screen.dart @@ -0,0 +1,140 @@ +part of 'vehicles_on_the_property.dart'; + +/// [VehicleUpdateScreen] is a StatefulWidget that displays a form to update a vehicle. + +// ignore: must_be_immutable +class VehicleUpdateScreen extends StatefulWidget { + VehicleUpdateScreen(this.model, {super.key}); + late VehicleModel model; + + @override + State createState() => _VehicleUpdateScreenState(); +} + +class _VehicleUpdateScreenState extends State { + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + _buildHeader(context), + buildBody(context), + ], + ), + ); + } + + Form buildBody(BuildContext context) { + return Form( + key: widget.model.updateFormKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildCustomInput( + context: context, + controller: widget.model.textFieldControllerLicensePlate!, + validator: widget.model.textControllerLicensePlateValidator, + focusNode: widget.model.textFieldFocusLicensePlate!, + labelText: FFLocalizations.of(context) + .getVariableText(ptText: 'Placa', enText: 'License Plate'), + hintText: FFLocalizations.of(context) + .getVariableText(ptText: 'Placa', enText: 'License Plate'), + suffixIcon: Symbols.format_color_text, + inputFormatters: [UpperCaseTextFormatter()], + maxLength: 7, + ), + _buildCustomInput( + context: context, + controller: widget.model.textFieldControllerModel!, + validator: widget.model.textControllerModelValidator, + focusNode: widget.model.textFieldFocusModel!, + labelText: FFLocalizations.of(context) + .getVariableText(ptText: 'Modelo', enText: 'Model'), + hintText: FFLocalizations.of(context).getVariableText( + ptText: 'Ex: Voyage, Ford', enText: 'e.g. Voyage, Ford'), + suffixIcon: Symbols.car_repair, + inputFormatters: [], + ), + _buildCustomInput( + context: context, + controller: widget.model.textFieldControllerColor!, + validator: widget.model.textControllerColorValidator, + focusNode: widget.model.textFieldFocusColor!, + labelText: FFLocalizations.of(context) + .getVariableText(ptText: 'Cor', enText: 'Color'), + hintText: FFLocalizations.of(context).getVariableText( + ptText: 'Ex: Preto, Amarelo, Branco', + enText: 'e.g. Black, Yellow, White'), + suffixIcon: Symbols.palette, + inputFormatters: [], + ), + _buildSubmitButton(context), + ], + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return Align( + alignment: const AlignmentDirectional(-1.0, 0.0), + child: Padding( + padding: const EdgeInsetsDirectional.fromSTEB(24.0, 20, 0.0, 15), + child: Text( + FFLocalizations.of(context).getVariableText( + ptText: + 'Preencha o formulário de alteração com os dados do seu veículo', + enText: 'Fill out the update form with your vehicle data', + ), + textAlign: TextAlign.start, + style: FlutterFlowTheme.of(context).bodyMedium.override( + fontFamily: FlutterFlowTheme.of(context).bodyMediumFamily, + letterSpacing: 0.0, + useGoogleFonts: GoogleFonts.asMap() + .containsKey(FlutterFlowTheme.of(context).bodyMediumFamily), + ), + ), + ), + ); + } + + Widget _buildCustomInput({ + required BuildContext context, + required TextEditingController controller, + required String? Function(BuildContext, String?) validator, + required FocusNode focusNode, + required String labelText, + required String hintText, + required IconData suffixIcon, + required List? inputFormatters, + int maxLength = 80, + }) { + return CustomInputUtil( + controller: controller, + validator: (value) => validator(context, value), + focusNode: focusNode, + labelText: labelText, + hintText: hintText, + suffixIcon: suffixIcon, + haveMaxLength: true, + onChanged: (value) => setState(() {}), + inputFormatters: inputFormatters, + maxLength: maxLength, + ); + } + + Widget _buildSubmitButton(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(70, 20, 70, 30), + child: SubmitButtonUtil( + labelText: FFLocalizations.of(context) + .getVariableText(ptText: 'Salvar', enText: 'Save'), + onPressed: widget.model.isFormValid(context, widget.model.updateFormKey) + ? () => widget.model.updateVehicle() + : null, + ), + ); + } +} diff --git a/lib/pages/vehicles_on_the_property/vehicles_on_the_property.dart b/lib/pages/vehicles_on_the_property/vehicles_on_the_property.dart index f40576ce..9e51e429 100644 --- a/lib/pages/vehicles_on_the_property/vehicles_on_the_property.dart +++ b/lib/pages/vehicles_on_the_property/vehicles_on_the_property.dart @@ -1,149 +1,184 @@ +import 'dart:collection'; +import 'dart:developer'; + +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:hub/components/atomic_components/shared_components_atoms/custom_input.dart'; +import 'package:hub/components/atomic_components/shared_components_atoms/submit_button.dart'; +import 'package:hub/components/atomic_components/shared_components_atoms/tabview.dart'; import 'package:hub/components/templates_components/card_item_template_component/card_item_template_component_widget.dart'; import 'package:hub/features/backend/index.dart'; -import 'package:hub/flutter_flow/flutter_flow_icon_button.dart'; -import 'package:hub/flutter_flow/flutter_flow_theme.dart'; -import 'package:hub/flutter_flow/flutter_flow_util.dart'; +import 'package:hub/features/module/index.dart'; +import 'package:hub/flutter_flow/index.dart'; import 'package:hub/pages/vehicles_on_the_property/vehicle_model.dart'; +import 'package:hub/shared/extensions/index.dart'; +import 'package:hub/shared/mixins/pageable_mixin.dart'; import 'package:hub/shared/utils/dialog_util.dart'; +import 'package:hub/shared/utils/license_util.dart'; import 'package:hub/shared/utils/limited_text_size.dart'; import 'package:hub/shared/utils/log_util.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:material_symbols_icons/symbols.dart'; -class VehicleOnTheProperty extends StatefulWidget { - const VehicleOnTheProperty({super.key}); +part 'vehicle_history_screen.dart'; +part 'vehicle_register_screen.dart'; +part 'vehicle_update_screen.dart'; + +/// [VehiclePage] is a StatefulWidget that displays the vehicle screens. +class VehiclePage extends StatefulWidget { + const VehiclePage({super.key}); @override - _VehicleOnThePropertyState createState() => _VehicleOnThePropertyState(); + // ignore: library_private_types_in_public_api + _VehiclePageState createState() => _VehiclePageState(); } -class _VehicleOnThePropertyState extends State +class _VehiclePageState extends State with TickerProviderStateMixin { - late ScrollController _scrollController; - - int _pageNumber = 1; - bool _hasData = false; - bool _loading = false; int count = 0; - late final VehicleModel model; - - late Future _future; - List _wrap = []; + late final VehicleModel _model; @override void initState() { super.initState(); - model = createModel(context, () => VehicleModel()); - _future = _fetchVisits(); - _scrollController = ScrollController() - ..addListener(() { - if (_scrollController.position.atEdge && - _scrollController.position.pixels != 0) { - _loadMore(); - } + _model = createModel(context, () => VehicleModel()); + + _model.updateOnChange = true; + _model.onUpdateVehicle = () { + safeSetState(() { + _model.clearFields(); }); + }; + _model.onRegisterVehicle = () { + safeSetState(() { + _model.clearFields(); + }); + }; + _model.safeSetState = () { + safeSetState(() {}); + }; } - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } + // @override + // void dispose() { + // super.dispose(); + // } @override Widget build(BuildContext context) { - late final limitedHeaderTextSize = - LimitedFontSizeUtil.getHeaderFontSize(context); - + final backgroundColor = FlutterFlowTheme.of(context).primaryBackground; return Scaffold( - backgroundColor: FlutterFlowTheme.of(context).primaryBackground, - appBar: _appBar(context), - body: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (_hasData == false && _pageNumber <= 1 && _loading == false) - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Center( - child: Text( - FFLocalizations.of(context).getVariableText( - ptText: "Nenhum veículo encontrado!", - enText: "No vehicle found", - ), - style: TextStyle( - fontFamily: 'Nunito', - fontSize: limitedHeaderTextSize, - ), - ), - ) - ], - ), - ) - else if (_hasData == true || _pageNumber >= 1) - Expanded( - child: FutureBuilder( - future: _future, - builder: (context, snapshot) { - return ListView.builder( - shrinkWrap: true, - physics: const BouncingScrollPhysics(), - controller: _scrollController, - itemCount: _wrap.length + 1, - itemBuilder: (context, index) { - if (index == 0) { - // Add your item here - return Padding( - padding: const EdgeInsets.only(right: 30, top: 10), - child: Text( - '', - textAlign: TextAlign.right, - ), - ); - } else { - final item = _wrap[index - 1]; - return _item(context, item); - } - }); - }, - )), - if (_hasData == true && _loading == true) - Container( - padding: const EdgeInsets.only(top: 15, bottom: 15), - child: Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - FlutterFlowTheme.of(context).primary, - ), - ), - ), - ) - ].addToStart(const SizedBox(height: 0)), - ), + backgroundColor: backgroundColor, + appBar: _buildHeader(context), + body: _buildBody(context), ); } - PreferredSizeWidget _appBar(BuildContext context) { + /// [Body] of the page. + FutureBuilder _buildBody(BuildContext context) { + Widget progressIndicator() { + return CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + FlutterFlowTheme.of(context).primary, + ), + ); + } + + return FutureBuilder( + future: _initializeModule(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return progressIndicator(); + } else if (snapshot.hasError) { + return progressIndicator(); + } else if (snapshot.hasData && snapshot.data == true) { + return _buildVehicleManager(context); + } else { + return _buildVehicleHistory(context); + } + }, + ); + } + + Future _initializeModule() async { + try { + final module = + await LicenseRepositoryImpl().getModule('FRE-HUB-VEHICLES-MANAGER'); + return await LicenseUtil.processModule(module); + } catch (e) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + context.pop(); + await DialogUtil.errorDefault(navigatorKey.currentContext!); + }); + return false; + } + } + + void onEditingChanged([bool? value]) { + bool isFirst = _model.tabBarController.index == 0; + + if (_model.isEditing & isFirst) { + _model.handleEditingChanged(false); + } + if (isFirst) { + setState(() {}); + } + } + + Widget _buildVehicleHistory(BuildContext context) { + return VehicleHistoryScreen(_model); + } + + Widget _buildVehicleManager(BuildContext context) { + final vehicleHistoryScreenLabel = FFLocalizations.of(context) + .getVariableText(ptText: 'Consultar', enText: 'History'); + final vehicleRegisterScreenLabel = FFLocalizations.of(context) + .getVariableText(ptText: 'Cadastrar', enText: 'Register'); + final vehicleUpdateScreenLabel = FFLocalizations.of(context) + .getVariableText(ptText: 'Editar', enText: 'Edit'); + + return TabViewUtil( + context: context, + model: _model, + labelTab1: vehicleHistoryScreenLabel, + labelTab2: _model.isEditing + ? vehicleUpdateScreenLabel + : vehicleRegisterScreenLabel, + controller: _model.tabBarController, + widget1: VehicleHistoryScreen(_model), + widget2: _model.isEditing + ? VehicleUpdateScreen(_model) + : VehicleRegisterScreen(_model), + onEditingChanged: onEditingChanged, + ); + } + + /// ----------------------------------- + /// [Header] of the page. + PreferredSizeWidget _buildHeader(BuildContext context) { + final theme = FlutterFlowTheme.of(context); + final backgroundColor = theme.primaryBackground; + final primaryText = theme.primaryText; + final title = FFLocalizations.of(context) + .getVariableText(enText: 'Vehicles', ptText: 'Veículos'); + final titleStyle = theme.headlineMedium.override( + fontFamily: theme.headlineMediumFamily, + color: primaryText, + fontSize: 16.0, + fontWeight: FontWeight.bold, + letterSpacing: 0.0, + useGoogleFonts: + GoogleFonts.asMap().containsKey(theme.headlineMediumFamily), + ); + final backButton = _backButton(context, theme); + return AppBar( - backgroundColor: FlutterFlowTheme.of(context).primaryBackground, + backgroundColor: backgroundColor, automaticallyImplyLeading: false, - title: Text( - FFLocalizations.of(context) - .getVariableText(enText: 'Vehicles', ptText: 'Veículos'), - style: FlutterFlowTheme.of(context).headlineMedium.override( - fontFamily: FlutterFlowTheme.of(context).headlineMediumFamily, - color: FlutterFlowTheme.of(context).primaryText, - fontSize: 16.0, - fontWeight: FontWeight.bold, - letterSpacing: 0.0, - useGoogleFonts: GoogleFonts.asMap().containsKey( - FlutterFlowTheme.of(context).headlineMediumFamily), - ), - ), - leading: _backButton(context, FlutterFlowTheme.of(context)), + title: Text(title, style: titleStyle), + leading: backButton, centerTitle: true, elevation: 0.0, actions: [], @@ -151,131 +186,22 @@ class _VehicleOnThePropertyState extends State } Widget _backButton(BuildContext context, FlutterFlowTheme theme) { + final Icon icon = Icon( + Icons.keyboard_arrow_left, + color: theme.primaryText, + size: 30.0, + ); + onPressed() => Navigator.of(context).pop(); return FlutterFlowIconButton( key: ValueKey('BackNavigationAppBar'), borderColor: Colors.transparent, borderRadius: 30.0, borderWidth: 1.0, buttonSize: 60.0, - icon: Icon( - Icons.keyboard_arrow_left, - color: theme.primaryText, - size: 30.0, - ), - onPressed: () => Navigator.of(context).pop(), + icon: icon, + onPressed: onPressed, ); } - Future _fetchVisits() async { - try { - setState(() => _loading = true); - - var response = await FreAccessWSGlobal.getVehiclesByProperty - .call(_pageNumber.toString()); - - final List vehicles = response.jsonBody['vehicles'] ?? []; - safeSetState(() => count = response.jsonBody['total_rows'] ?? 0); - - if (vehicles.isNotEmpty) { - setState(() { - _wrap.addAll(vehicles); - _hasData = true; - _loading = false; - }); - - return response; - } - - _showNoMoreDataSnackBar(context); - - setState(() { - _hasData = false; - _loading = false; - }); - - return null; - } catch (e, s) { - DialogUtil.errorDefault(context); - LogUtil.requestAPIFailed( - "proccessRequest.php", "", "Consulta de Veículo", e, s); - setState(() { - _hasData = false; - _loading = false; - }); - } - return null; - } - - void _loadMore() { - if (_hasData == true) { - _pageNumber++; - - _future = _fetchVisits(); - } - } - - void _showNoMoreDataSnackBar(BuildContext context) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - FFLocalizations.of(context).getVariableText( - ptText: "Não há mais dados.", - enText: "No more data.", - ), - style: TextStyle( - color: Colors.white, - fontSize: LimitedFontSizeUtil.getBodyFontSize(context), - ), - ), - duration: const Duration(seconds: 3), - backgroundColor: FlutterFlowTheme.of(context).primary, - ), - ); - } - - Widget _item(BuildContext context, dynamic uItem) { - return CardItemTemplateComponentWidget( - imagePath: null, - labelsHashMap: { - '${FFLocalizations.of(context).getVariableText(ptText: "Placa", enText: "License Plate")}:': - uItem['licensePlate'] ?? '', - '${FFLocalizations.of(context).getVariableText(ptText: "Modelo", enText: "Model")}:': - uItem['model'] ?? '', - '${FFLocalizations.of(context).getVariableText(ptText: "Tag", enText: "Tag")}:': - uItem['tag'] ?? '', - }, - statusHashMap: [], - onTapCardItemAction: () async { - await showDialog( - useSafeArea: true, - context: context, - builder: (context) { - return Dialog( - alignment: Alignment.center, - child: model.buildVehicleDetails( - item: uItem, - context: context, - model: model, - ), - ); - }, - ).whenComplete(() { - safeSetState(() { - _pageNumber = 1; - _wrap = []; - _future = _fetchVisits() - .then((value) => value!.jsonBody['vehicles'] ?? []); - }); - }).catchError((e, s) { - DialogUtil.errorDefault(context); - LogUtil.requestAPIFailed( - "proccessRequest.php", "", "Consulta de Veículos", e, s); - safeSetState(() { - _hasData = false; - _loading = false; - }); - }); - }, - ); - } + /// ----------------------------------- } diff --git a/lib/pages/visits_on_the_property/model.dart b/lib/pages/visits_on_the_property/model.dart index 449f4177..b263e717 100644 --- a/lib/pages/visits_on_the_property/model.dart +++ b/lib/pages/visits_on_the_property/model.dart @@ -6,7 +6,7 @@ import 'package:hub/flutter_flow/flutter_flow_theme.dart'; import 'package:hub/flutter_flow/internationalization.dart'; import 'package:hub/pages/vehicles_on_the_property/vehicles_on_the_property.dart'; -class VisitsModel extends FlutterFlowModel { +class VisitsModel extends FlutterFlowModel { static VisitsModel? _instance; VisitsModel._internal({this.onRefresh}); diff --git a/lib/shared/extensions/string_extensions.dart b/lib/shared/extensions/string_extensions.dart index d8d36c65..45bde526 100644 --- a/lib/shared/extensions/string_extensions.dart +++ b/lib/shared/extensions/string_extensions.dart @@ -1,7 +1,7 @@ import 'dart:ui'; extension StringNullableExtensions on String? { - bool toBoolean() { + bool get toBoolean { if (this == null) return false; return this!.toLowerCase() == 'true'; } @@ -11,10 +11,16 @@ extension StringNullableExtensions on String? { if (this == '') return true; return false; } + + bool get isNotNullAndEmpty { + if (this == null) return false; + if (this == '') return false; + return true; + } } extension StringExtensions on String { - bool toBoolean() { + bool get toBoolean { return toLowerCase() == 'true'; } } diff --git a/lib/shared/mixins/pageable_mixin.dart b/lib/shared/mixins/pageable_mixin.dart new file mode 100644 index 00000000..7e967cf8 --- /dev/null +++ b/lib/shared/mixins/pageable_mixin.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:hub/flutter_flow/index.dart'; +import 'package:hub/shared/utils/limited_text_size.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; + +extension PagedListViewExtension + on PagedSliverList {} + +mixin Pageable on State { + Expanded buildPaginatedListView( + String noDataFound, + PagingController pg, + Widget Function(BuildContext, Y, int) itemBuilder) { + final theme = FlutterFlowTheme.of(context); + + return Expanded( + child: RefreshIndicator( + backgroundColor: theme.primaryBackground, + color: theme.primary, + onRefresh: () async => pg.refresh(), + child: PagedListView( + pagingController: pg, + builderDelegate: PagedChildBuilderDelegate( + animateTransitions: true, + + itemBuilder: (context, item, index) => + itemBuilder(context, item, index), + // noMoreItemsIndicatorBuilder: , + newPageProgressIndicatorBuilder: (context) => + buildLoadingIndicator(context), + firstPageProgressIndicatorBuilder: (context) => + buildLoadingIndicator(context), + noItemsFoundIndicatorBuilder: (context) => + buildNoDataFound(context, noDataFound), + // firstPageErrorIndicatorBuilder: (context) => const Placeholder(), + // newPageErrorIndicatorBuilder: (context) => const Placeholder(), + ), + ), + ), + ); + } + + Future fetchPage({ + required Future<(bool, dynamic)> Function() dataProvider, + required void Function(dynamic data) onDataAvailable, + required void Function(dynamic data) onDataUnavailable, + required void Function(Object error, StackTrace stackTrace) onFetchError, + }) async { + try { + final (bool isDataAvailable, dynamic data) = await dataProvider(); + if (isDataAvailable) { + onDataAvailable(data); + } else { + onDataUnavailable(data); + } + } catch (error, stackTrace) { + onFetchError(error, stackTrace); + } + } + + void showNoMoreDataSnackBar(BuildContext context) { + final message = FFLocalizations.of(context).getVariableText( + ptText: "Não há mais dados.", + enText: "No more data.", + ); + + showSnackbarMessenger(context, message, true); + } + + Widget buildNoDataFound(BuildContext context, String title) { + final headerFontSize = LimitedFontSizeUtil.getHeaderFontSize(context); + // final bodyFontSize = LimitedFontSizeUtil.getBodyFontSize(context); + return Expanded( + child: Center( + child: Text( + title, + style: TextStyle( + fontFamily: 'Nunito', + fontSize: headerFontSize, + ), + ), + ), + ); + } + + Widget buildLoadingIndicator(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 15), + child: Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + FlutterFlowTheme.of(context).primary, + ), + ), + ), + ); + } +} diff --git a/lib/shared/utils/datetime_util.dart b/lib/shared/utils/datetime_util.dart new file mode 100644 index 00000000..cbbc5cb9 --- /dev/null +++ b/lib/shared/utils/datetime_util.dart @@ -0,0 +1,26 @@ +import 'dart:developer'; + +class DateTimeUtil { + static Future processStartDate(String startDate) async { + try { + if (startDate.isEmpty) return true; + final start = DateTime.tryParse(startDate); + if (start == null) return false; + return DateTime.now().isAfter(start); + } catch (e) { + log('Error processing start date for module: $e'); + } + return false; + } + + static Future processExpirationDate(String expirationDate) async { + try { + if (expirationDate.isEmpty) return false; + final expiration = DateTime.tryParse(expirationDate); + return expiration != null && DateTime.now().isAfter(expiration); + } catch (e) { + log('Error processing expiration date for module: $e'); + } + return false; + } +} diff --git a/lib/shared/utils/license_util.dart b/lib/shared/utils/license_util.dart new file mode 100644 index 00000000..4959f1af --- /dev/null +++ b/lib/shared/utils/license_util.dart @@ -0,0 +1,18 @@ +import 'package:hub/features/module/index.dart'; +import 'package:hub/flutter_flow/index.dart'; +import 'package:hub/shared/utils/datetime_util.dart'; + +class LicenseUtil { + static Future processModule(String? module) async { + if (module == null) return false; + final moduleMap = await stringToMap(module); + final startDate = moduleMap['startDate'] ?? ''; + final expirationDate = moduleMap['expirationDate'] ?? ''; + final isStarted = await DateTimeUtil.processStartDate(startDate); + final isExpired = await DateTimeUtil.processExpirationDate(expirationDate); + if (isStarted && !isExpired) + return EnumDisplay.fromString(moduleMap["display"]) == EnumDisplay.active; + if (isExpired) return false; + return false; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 3badfc91..0999c417 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,8 +3,9 @@ name: hub description: . # Descrição do projeto (adicione mais detalhes se necessário) publish_to: "none" # Destino de publicação -# Versão do aplicativo -version: 1.3.5+24 +publish_to: "none" + +version: 1.4.0+27 # Restrições de versão do SDK Dart environment: @@ -113,7 +114,9 @@ dependencies: url_launcher_platform_interface: 2.3.2 permission_handler: ^11.3.1 awesome_notifications: ^0.10.0 - app_tracking_transparency: ^2.0.6 + app_tracking_transparency: ^2.0.6+1 + # dio: ^5.7.0 + # crypto: ^3.0.5 freezed_annotation: ^2.4.4 package_info_plus: ^8.1.1 sliver_tools: ^0.2.12 diff --git a/scripts/httpie.sh b/scripts/httpie.sh new file mode 100755 index 00000000..c9e4e2dc --- /dev/null +++ b/scripts/httpie.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Define the base URL for the API endpoint +BASE_URL="https://freaccess.com.br/freaccess/processRequest.php" + +# Define common parameters +DEV_UUID="6b7b81849c115a8a" +USER_UUID="678aa05b0c2154.50583237" +CLI_ID="7" +ACTIVITY="getVehiclesByProperty" +PAGE_SIZE="10" + +# Function to perform the HTTP request +perform_request() { + local page=$1 + http --form POST "$BASE_URL" \ + devUUID="$DEV_UUID" \ + userUUID="$USER_UUID" \ + cliID="$CLI_ID" \ + atividade="$ACTIVITY" \ + page="$page" \ + pageSize="$PAGE_SIZE" \ + --check-status \ + --ignore-stdin \ + --timeout=10 +} + +# Perform requests for pages 1 and 2 +perform_request 1 +perform_request 2