import 'dart:developer'; 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; 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() { disposeControllers(); } void disposeControllers() { tabBarController.dispose(); textFieldFocusLicensePlate!.dispose(); textFieldControllerLicensePlate!.dispose(); textFieldFocusColor!.dispose(); textFieldControllerColor!.dispose(); textFieldFocusModel!.dispose(); textFieldControllerModel!.dispose(); } 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], ); } }