From 8c4294e67b3fa0fdf3fcf952b2de17e46dfd2179 Mon Sep 17 00:00:00 2001 From: SIO Admin Date: Sun, 18 Jan 2026 03:09:03 +0000 Subject: [PATCH] feat: Codigo fuente SIO Mobile - App Android para Operadores Aplicacion movil Android para el sistema SIO (Sistema Integral de Operaciones) de Drenax. Permite a los operadores gestionar sus servicios diarios. Funcionalidades principales: - Login y autenticacion JWT - Checklist de vehiculos - Gestion de jornada laboral - Lista de servicios asignados - Captura de evidencias fotograficas - Firma digital del cliente - Encuestas de satisfaccion - Notificaciones push (Firebase) - Almacenamiento offline (ObjectBox) - Geolocalizacion y mapas Stack tecnologico: - Kotlin - Android SDK 33 - Retrofit + OkHttp - ObjectBox - Firebase (FCM, Crashlytics, Analytics) - Google Maps Co-Authored-By: Claude Opus 4.5 --- .gitignore | 75 + README.md | 663 ++++++++ app/.gitignore | 1 + app/build.gradle | 109 ++ app/google-services.json | 48 + app/objectbox-models/default.json | 1153 +++++++++++++ app/objectbox-models/default.json.bak | 1148 +++++++++++++ app/proguard-rules.pro | 21 + .../siodrenax/ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 95 ++ app/src/main/assets/images/logo_drenax.png | Bin 0 -> 49934 bytes app/src/main/ic_launcher-playstore.png | Bin 0 -> 64069 bytes .../java/com/iesoluciones/siodrenax/App.kt | 116 ++ .../siodrenax/PreferencesHelper.kt | 65 + .../siodrenax/activities/CameraActivity.kt | 262 +++ .../activities/ConfirmationActivity.kt | 157 ++ .../activities/HerramientaSurveyActivity.kt | 112 ++ .../siodrenax/activities/LoginActivity.kt | 119 ++ .../activities/MaterialSurveyActivity.kt | 61 + .../activities/NextServiceActivity.kt | 126 ++ .../siodrenax/activities/OperatorsActivity.kt | 124 ++ .../activities/OrderDetailActivity.kt | 1439 +++++++++++++++++ .../activities/OrderProgressActivity.kt | 373 +++++ .../siodrenax/activities/OrdersActivity.kt | 379 +++++ .../activities/OrdersManagerActivity.kt | 104 ++ .../siodrenax/activities/PdfViewerActivity.kt | 110 ++ .../activities/RevisionSurveyActivity.kt | 155 ++ .../siodrenax/activities/SignatureActivity.kt | 122 ++ .../siodrenax/activities/SplashActivity.kt | 104 ++ .../siodrenax/activities/SurveyActivity.kt | 162 ++ .../siodrenax/activities/WorkdayActivity.kt | 137 ++ .../activities/WorkdayManagerActivity.kt | 105 ++ .../adapters/HerramientaSurveyAdapter.kt | 111 ++ .../adapters/MaterialSurveyAdapter.kt | 76 + .../adapters/MultipleViewHolderAdapter.kt | 355 ++++ .../adapters/NextDayOrdersAdapter.kt | 159 ++ .../siodrenax/adapters/OperatorAdapter.kt | 68 + .../siodrenax/adapters/OrdersAdapter.kt | 201 +++ .../adapters/OrdersManagerAdapter.kt | 144 ++ .../siodrenax/adapters/PdfViewerAdapter.kt | 59 + .../adapters/RevisionSurveyAdapter.kt | 185 +++ .../siodrenax/entities/BusinessAnswer.kt | 54 + .../siodrenax/entities/BusinessQuestion.kt | 54 + .../siodrenax/entities/CheckListQuestion.kt | 51 + .../siodrenax/entities/DomesticAnswer.kt | 52 + .../siodrenax/entities/DomesticQuestion.kt | 54 + .../siodrenax/entities/Evidence.kt | 48 + .../entities/NegativeServiceReason.kt | 29 + .../siodrenax/entities/NextDayOrder.kt | 167 ++ .../siodrenax/entities/Operator.kt | 31 + .../iesoluciones/siodrenax/entities/Order.kt | 190 +++ .../siodrenax/entities/OrderProgress.kt | 54 + .../siodrenax/entities/SavedAnswer.kt | 21 + .../iesoluciones/siodrenax/entities/User.kt | 32 + .../siodrenax/entities/Vehicle.kt | 10 + .../dialogs/IncidentsInputFragment.kt | 82 + .../dialogs/MileageInputDialogFragment.kt | 86 + .../dialogs/VehicleIncidentsFragment.kt | 66 + .../interfaces/OnIncidentListener.kt | 6 + .../interfaces/OnIncidentShowListener.kt | 6 + .../siodrenax/interfaces/OnMileageListener.kt | 5 + .../iesoluciones/siodrenax/models/Answer.kt | 10 + .../siodrenax/models/AnswerPOJO.kt | 30 + .../siodrenax/models/CheckBoxAnswer.kt | 11 + .../siodrenax/models/EvidenceRequest.kt | 18 + .../models/EvidenceSignatureLocal.kt | 6 + .../siodrenax/models/FinishOrderRequest.kt | 44 + .../siodrenax/models/GenericResponse.kt | 8 + .../iesoluciones/siodrenax/models/KeyValue.kt | 18 + .../siodrenax/models/LoginResponse.kt | 23 + .../siodrenax/models/OpenAnswer.kt | 11 + .../iesoluciones/siodrenax/models/OrderPdf.kt | 25 + .../iesoluciones/siodrenax/models/Question.kt | 9 + .../siodrenax/models/QuestionPOJO.kt | 33 + .../iesoluciones/siodrenax/models/Reason.kt | 6 + .../siodrenax/models/StartWorkdayResponse.kt | 47 + .../siodrenax/models/ValidationOrderAnswer.kt | 5 + .../models/VehicleIncidenceResponse.kt | 18 + .../iesoluciones/siodrenax/models/Workday.kt | 26 + .../com/iesoluciones/siodrenax/network/Api.kt | 118 ++ .../siodrenax/network/HttpError.kt | 35 + .../siodrenax/network/TokenInterceptor.kt | 166 ++ .../siodrenax/network/UnprocessableEntity.kt | 43 + .../siodrenax/receivers/UpdateUIReceiver.kt | 34 + .../repositories/CheckListRepository.kt | 76 + .../repositories/EvidenceRepository.kt | 15 + .../repositories/IncidenceRepository.kt | 91 ++ .../siodrenax/repositories/LoginRepository.kt | 62 + .../repositories/ManagerRepository.kt | 121 ++ .../repositories/OrdersRepository.kt | 349 ++++ .../repositories/SurveyRepository.kt | 104 ++ .../repositories/WorkdayRepository.kt | 251 +++ .../siodrenax/services/NotificationService.kt | 71 + .../utils/CircularProgressButton.java | 206 +++ .../utils/CircularProgressDrawable.java | 419 +++++ .../iesoluciones/siodrenax/utils/Constants.kt | 98 ++ .../siodrenax/utils/CustomDialogFragment.kt | 128 ++ .../utils/CustomHtmlToPdfConvertor.kt | 96 ++ .../siodrenax/utils/Extensions.kt | 76 + .../siodrenax/utils/HelperUtil.kt | 464 ++++++ .../siodrenax/utils/ImageUtils.java | 129 ++ .../siodrenax/utils/ListPaddingDecoration.kt | 34 + .../siodrenax/utils/NotificationUtils.kt | 91 ++ .../utils/NumberFormatterTextWatcher.kt | 34 + .../siodrenax/utils/StatusDrawable.kt | 39 + .../siodrenax/utils/TouchImageView.java | 1120 +++++++++++++ .../viewmodels/CheckListViewModel.kt | 33 + .../siodrenax/viewmodels/EvidenceViewModel.kt | 31 + .../viewmodels/IncidenceViewModel.kt | 74 + .../siodrenax/viewmodels/LoginViewModel.kt | 34 + .../siodrenax/viewmodels/ManagerViewModel.kt | 103 ++ .../viewmodels/OrderProgressViewModel.kt | 102 ++ .../siodrenax/viewmodels/OrdersViewModel.kt | 77 + .../siodrenax/viewmodels/SplashViewModel.kt | 73 + .../viewmodels/WorkdayStatusViewModel.kt | 110 ++ .../siodrenax/workers/SyncWorker.kt | 74 + app/src/main/res/anim/scale_out_airbnb.xml | 14 + .../main/res/anim/slide_in_rigth_airbnb.xml | 10 + .../main/res/anim/slide_out_right_airbnb.xml | 10 + .../main/res/drawable-hdpi/chevron_right.png | Bin 0 -> 350 bytes .../main/res/drawable-hdpi/circle_border.xml | 5 + .../main/res/drawable-hdpi/drenax_logo.png | Bin 0 -> 20629 bytes .../drawable-hdpi/ic_map_outline_white.png | Bin 0 -> 1444 bytes .../rounded_background_accent.xml | 6 + app/src/main/res/drawable-hdpi/torch.png | Bin 0 -> 438 bytes app/src/main/res/drawable-hdpi/torch_off.png | Bin 0 -> 633 bytes app/src/main/res/drawable-mdpi/capture.xml | 6 + .../main/res/drawable-mdpi/capture_active.xml | 15 + .../res/drawable-mdpi/capture_inactive.xml | 15 + .../main/res/drawable-mdpi/chevron_right.png | Bin 0 -> 252 bytes .../main/res/drawable-mdpi/drenax_logo.png | Bin 0 -> 14031 bytes .../drawable-mdpi/ic_map_outline_white.png | Bin 0 -> 911 bytes app/src/main/res/drawable-mdpi/torch.png | Bin 0 -> 300 bytes app/src/main/res/drawable-mdpi/torch_off.png | Bin 0 -> 409 bytes .../main/res/drawable-xhdpi/chevron_right.png | Bin 0 -> 325 bytes .../main/res/drawable-xhdpi/drenax_logo.png | Bin 0 -> 29088 bytes .../drawable-xhdpi/ic_map_outline_white.png | Bin 0 -> 1694 bytes app/src/main/res/drawable-xhdpi/torch.png | Bin 0 -> 428 bytes app/src/main/res/drawable-xhdpi/torch_off.png | Bin 0 -> 615 bytes .../res/drawable-xxhdpi/chevron_right.png | Bin 0 -> 511 bytes .../main/res/drawable-xxhdpi/drenax_logo.png | Bin 0 -> 46641 bytes .../drawable-xxhdpi/ic_map_outline_white.png | Bin 0 -> 2938 bytes app/src/main/res/drawable-xxhdpi/torch.png | Bin 0 -> 628 bytes .../main/res/drawable-xxhdpi/torch_off.png | Bin 0 -> 890 bytes .../res/drawable-xxxhdpi/chevron_right.png | Bin 0 -> 280 bytes .../drawable-xxxhdpi/ic_map_outline_white.png | Bin 0 -> 2091 bytes app/src/main/res/drawable-xxxhdpi/torch.png | Bin 0 -> 407 bytes .../main/res/drawable-xxxhdpi/torch_off.png | Bin 0 -> 544 bytes app/src/main/res/drawable/border_dotted.xml | 12 + .../main/res/drawable/button_ripple_bg.xml | 16 + .../res/drawable/circle_border_accent.xml | 5 + .../res/drawable/circle_border_primary.xml | 5 + app/src/main/res/drawable/ic_camera_black.xml | 13 + .../res/drawable/ic_launcher_background.xml | 74 + .../res/drawable/ic_location_on_blue_24dp.xml | 5 + app/src/main/res/drawable/ic_pdf.xml | 5 + .../main/res/layout-land/activity_workday.xml | 201 +++ app/src/main/res/layout/activity_camera.xml | 199 +++ .../main/res/layout/activity_confirmation.xml | 59 + .../layout/activity_herramienta_survey.xml | 75 + app/src/main/res/layout/activity_login.xml | 121 ++ .../res/layout/activity_material_survey.xml | 36 + .../main/res/layout/activity_next_service.xml | 98 ++ .../main/res/layout/activity_operators.xml | 97 ++ .../main/res/layout/activity_order_detail.xml | 934 +++++++++++ .../res/layout/activity_order_progress.xml | 531 ++++++ app/src/main/res/layout/activity_orders.xml | 100 ++ .../res/layout/activity_orders_manager.xml | 97 ++ .../main/res/layout/activity_pdf_viewer.xml | 36 + .../res/layout/activity_revision_survey.xml | 76 + .../main/res/layout/activity_signature.xml | 55 + app/src/main/res/layout/activity_splash.xml | 20 + app/src/main/res/layout/activity_survey.xml | 86 + app/src/main/res/layout/activity_workday.xml | 199 +++ .../res/layout/fragment_incidents_input.xml | 78 + .../layout/fragment_mileage_input_dialog.xml | 82 + .../res/layout/fragment_vehicle_incidents.xml | 75 + .../res/layout/viewholder_answer_open.xml | 59 + .../res/layout/viewholder_checkbox_answer.xml | 25 + .../res/layout/viewholder_herramienta.xml | 69 + .../main/res/layout/viewholder_material.xml | 69 + .../main/res/layout/viewholder_operator.xml | 102 ++ app/src/main/res/layout/viewholder_order.xml | 372 +++++ app/src/main/res/layout/viewholder_pdf.xml | 15 + .../main/res/layout/viewholder_revision.xml | 91 ++ app/src/main/res/menu/orders.xml | 35 + app/src/main/res/menu/survey.xml | 19 + app/src/main/res/menu/workday.xml | 13 + app/src/main/res/mipmap-anydpi | 0 .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2713 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 4710 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3724 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1760 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2553 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2281 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3950 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 7474 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5476 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6952 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 15005 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 9466 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10980 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 24897 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 14312 bytes app/src/main/res/values/colors.xml | 31 + app/src/main/res/values/dimen.xml | 26 + app/src/main/res/values/strings.xml | 239 +++ app/src/main/res/values/styles.xml | 65 + .../iesoluciones/siodrenax/ExampleUnitTest.kt | 17 + build.gradle | 31 + gradle.properties | 20 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 234 +++ gradlew.bat | 89 + mylibrary/.gitignore | 1 + mylibrary/build.gradle | 34 + mylibrary/proguard-rules.pro | 21 + .../mylibrary/ExampleInstrumentedTest.java | 26 + mylibrary/src/main/AndroidManifest.xml | 1 + .../mylibrary/activities/ToolbarActivity.kt | 26 + .../mylibrary/interfaces/IToolbar.kt | 11 + mylibrary/src/main/res/layout/toolbar.xml | 10 + mylibrary/src/main/res/values/strings.xml | 3 + .../mylibrary/ExampleUnitTest.java | 17 + settings.gradle | 1 + 228 files changed, 20912 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/google-services.json create mode 100644 app/objectbox-models/default.json create mode 100644 app/objectbox-models/default.json.bak create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/iesoluciones/siodrenax/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/assets/images/logo_drenax.png create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/App.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/PreferencesHelper.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/CameraActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/ConfirmationActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/HerramientaSurveyActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/LoginActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/MaterialSurveyActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/NextServiceActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/OperatorsActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/OrderDetailActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/OrderProgressActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/OrdersActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/OrdersManagerActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/PdfViewerActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/RevisionSurveyActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/SignatureActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/SplashActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/SurveyActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/WorkdayActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/activities/WorkdayManagerActivity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/adapters/HerramientaSurveyAdapter.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/adapters/MaterialSurveyAdapter.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/adapters/MultipleViewHolderAdapter.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/adapters/NextDayOrdersAdapter.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/adapters/OperatorAdapter.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/adapters/OrdersAdapter.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/adapters/OrdersManagerAdapter.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/adapters/PdfViewerAdapter.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/adapters/RevisionSurveyAdapter.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/entities/BusinessAnswer.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/entities/BusinessQuestion.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/entities/CheckListQuestion.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/entities/DomesticAnswer.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/entities/DomesticQuestion.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/entities/Evidence.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/entities/NegativeServiceReason.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/entities/NextDayOrder.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/entities/Operator.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/entities/Order.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/entities/OrderProgress.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/entities/SavedAnswer.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/entities/User.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/entities/Vehicle.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/fragments/dialogs/IncidentsInputFragment.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/fragments/dialogs/MileageInputDialogFragment.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/fragments/dialogs/VehicleIncidentsFragment.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/interfaces/OnIncidentListener.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/interfaces/OnIncidentShowListener.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/interfaces/OnMileageListener.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/Answer.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/AnswerPOJO.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/CheckBoxAnswer.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/EvidenceRequest.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/EvidenceSignatureLocal.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/FinishOrderRequest.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/GenericResponse.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/KeyValue.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/LoginResponse.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/OpenAnswer.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/OrderPdf.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/Question.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/QuestionPOJO.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/Reason.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/StartWorkdayResponse.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/ValidationOrderAnswer.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/VehicleIncidenceResponse.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/models/Workday.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/network/Api.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/network/HttpError.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/network/TokenInterceptor.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/network/UnprocessableEntity.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/receivers/UpdateUIReceiver.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/repositories/CheckListRepository.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/repositories/EvidenceRepository.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/repositories/IncidenceRepository.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/repositories/LoginRepository.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/repositories/ManagerRepository.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/repositories/OrdersRepository.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/repositories/SurveyRepository.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/repositories/WorkdayRepository.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/services/NotificationService.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/utils/CircularProgressButton.java create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/utils/CircularProgressDrawable.java create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/utils/Constants.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/utils/CustomDialogFragment.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/utils/CustomHtmlToPdfConvertor.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/utils/Extensions.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/utils/HelperUtil.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/utils/ImageUtils.java create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/utils/ListPaddingDecoration.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/utils/NotificationUtils.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/utils/NumberFormatterTextWatcher.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/utils/StatusDrawable.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/utils/TouchImageView.java create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/viewmodels/CheckListViewModel.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/viewmodels/EvidenceViewModel.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/viewmodels/IncidenceViewModel.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/viewmodels/LoginViewModel.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/viewmodels/ManagerViewModel.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/viewmodels/OrderProgressViewModel.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/viewmodels/OrdersViewModel.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/viewmodels/SplashViewModel.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/viewmodels/WorkdayStatusViewModel.kt create mode 100644 app/src/main/java/com/iesoluciones/siodrenax/workers/SyncWorker.kt create mode 100644 app/src/main/res/anim/scale_out_airbnb.xml create mode 100644 app/src/main/res/anim/slide_in_rigth_airbnb.xml create mode 100644 app/src/main/res/anim/slide_out_right_airbnb.xml create mode 100644 app/src/main/res/drawable-hdpi/chevron_right.png create mode 100644 app/src/main/res/drawable-hdpi/circle_border.xml create mode 100644 app/src/main/res/drawable-hdpi/drenax_logo.png create mode 100644 app/src/main/res/drawable-hdpi/ic_map_outline_white.png create mode 100644 app/src/main/res/drawable-hdpi/rounded_background_accent.xml create mode 100644 app/src/main/res/drawable-hdpi/torch.png create mode 100644 app/src/main/res/drawable-hdpi/torch_off.png create mode 100644 app/src/main/res/drawable-mdpi/capture.xml create mode 100644 app/src/main/res/drawable-mdpi/capture_active.xml create mode 100644 app/src/main/res/drawable-mdpi/capture_inactive.xml create mode 100644 app/src/main/res/drawable-mdpi/chevron_right.png create mode 100644 app/src/main/res/drawable-mdpi/drenax_logo.png create mode 100644 app/src/main/res/drawable-mdpi/ic_map_outline_white.png create mode 100644 app/src/main/res/drawable-mdpi/torch.png create mode 100644 app/src/main/res/drawable-mdpi/torch_off.png create mode 100644 app/src/main/res/drawable-xhdpi/chevron_right.png create mode 100644 app/src/main/res/drawable-xhdpi/drenax_logo.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_map_outline_white.png create mode 100644 app/src/main/res/drawable-xhdpi/torch.png create mode 100644 app/src/main/res/drawable-xhdpi/torch_off.png create mode 100644 app/src/main/res/drawable-xxhdpi/chevron_right.png create mode 100644 app/src/main/res/drawable-xxhdpi/drenax_logo.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_map_outline_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/torch.png create mode 100644 app/src/main/res/drawable-xxhdpi/torch_off.png create mode 100644 app/src/main/res/drawable-xxxhdpi/chevron_right.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_map_outline_white.png create mode 100644 app/src/main/res/drawable-xxxhdpi/torch.png create mode 100644 app/src/main/res/drawable-xxxhdpi/torch_off.png create mode 100644 app/src/main/res/drawable/border_dotted.xml create mode 100644 app/src/main/res/drawable/button_ripple_bg.xml create mode 100644 app/src/main/res/drawable/circle_border_accent.xml create mode 100644 app/src/main/res/drawable/circle_border_primary.xml create mode 100644 app/src/main/res/drawable/ic_camera_black.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_location_on_blue_24dp.xml create mode 100644 app/src/main/res/drawable/ic_pdf.xml create mode 100644 app/src/main/res/layout-land/activity_workday.xml create mode 100644 app/src/main/res/layout/activity_camera.xml create mode 100644 app/src/main/res/layout/activity_confirmation.xml create mode 100644 app/src/main/res/layout/activity_herramienta_survey.xml create mode 100644 app/src/main/res/layout/activity_login.xml create mode 100644 app/src/main/res/layout/activity_material_survey.xml create mode 100644 app/src/main/res/layout/activity_next_service.xml create mode 100644 app/src/main/res/layout/activity_operators.xml create mode 100644 app/src/main/res/layout/activity_order_detail.xml create mode 100644 app/src/main/res/layout/activity_order_progress.xml create mode 100644 app/src/main/res/layout/activity_orders.xml create mode 100644 app/src/main/res/layout/activity_orders_manager.xml create mode 100644 app/src/main/res/layout/activity_pdf_viewer.xml create mode 100644 app/src/main/res/layout/activity_revision_survey.xml create mode 100644 app/src/main/res/layout/activity_signature.xml create mode 100644 app/src/main/res/layout/activity_splash.xml create mode 100644 app/src/main/res/layout/activity_survey.xml create mode 100644 app/src/main/res/layout/activity_workday.xml create mode 100644 app/src/main/res/layout/fragment_incidents_input.xml create mode 100644 app/src/main/res/layout/fragment_mileage_input_dialog.xml create mode 100644 app/src/main/res/layout/fragment_vehicle_incidents.xml create mode 100644 app/src/main/res/layout/viewholder_answer_open.xml create mode 100644 app/src/main/res/layout/viewholder_checkbox_answer.xml create mode 100644 app/src/main/res/layout/viewholder_herramienta.xml create mode 100644 app/src/main/res/layout/viewholder_material.xml create mode 100644 app/src/main/res/layout/viewholder_operator.xml create mode 100644 app/src/main/res/layout/viewholder_order.xml create mode 100644 app/src/main/res/layout/viewholder_pdf.xml create mode 100644 app/src/main/res/layout/viewholder_revision.xml create mode 100644 app/src/main/res/menu/orders.xml create mode 100644 app/src/main/res/menu/survey.xml create mode 100644 app/src/main/res/menu/workday.xml create mode 100644 app/src/main/res/mipmap-anydpi create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimen.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/com/iesoluciones/siodrenax/ExampleUnitTest.kt create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 mylibrary/.gitignore create mode 100644 mylibrary/build.gradle create mode 100644 mylibrary/proguard-rules.pro create mode 100644 mylibrary/src/androidTest/java/com/iesoluciones/mylibrary/ExampleInstrumentedTest.java create mode 100644 mylibrary/src/main/AndroidManifest.xml create mode 100644 mylibrary/src/main/java/com/iesoluciones/mylibrary/activities/ToolbarActivity.kt create mode 100644 mylibrary/src/main/java/com/iesoluciones/mylibrary/interfaces/IToolbar.kt create mode 100644 mylibrary/src/main/res/layout/toolbar.xml create mode 100644 mylibrary/src/main/res/values/strings.xml create mode 100644 mylibrary/src/test/java/com/iesoluciones/mylibrary/ExampleUnitTest.java create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7562f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +*.idea + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# OS X +.DS_STORE diff --git a/README.md b/README.md new file mode 100644 index 0000000..66f62b4 --- /dev/null +++ b/README.md @@ -0,0 +1,663 @@ +# SIO Mobile - Aplicacion Android para Operadores + +## Drenax - Sistema Integral de Operaciones + +--- + +## Informacion General + +| Campo | Valor | +|-------|-------| +| Nombre App | SIO Drenax | +| Package ID | com.iesoluciones.siodrenax | +| Version | 1.00 (versionCode: 3) | +| Plataforma | Android | +| SDK Minimo | 28 (Android 9.0 Pie) | +| SDK Target | 33 (Android 13) | +| Lenguaje | Kotlin | + +--- + +## Arquitectura del Sistema + +``` +┌─────────────────────────────────────────────────────────┐ +│ APP MOVIL │ +│ Android (Kotlin) │ +│ com.iesoluciones.siodrenax │ +└─────────────────────┬───────────────────────────────────┘ + │ HTTPS (REST API) + ▼ +┌─────────────────────────────────────────────────────────┐ +│ BACKEND │ +│ Laravel 5.5 (PHP 7.4) │ +│ https://sio-api.consultoria-as.com │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## Estructura del Proyecto + +``` +SIO-Mobile/ +├── app/ +│ ├── build.gradle # Configuracion de compilacion +│ └── src/ +│ └── main/ +│ ├── AndroidManifest.xml # Configuracion de la app +│ ├── java/com/iesoluciones/siodrenax/ +│ │ ├── activities/ # Pantallas de la app +│ │ │ ├── SplashActivity.kt +│ │ │ ├── LoginActivity.kt +│ │ │ ├── WorkdayActivity.kt +│ │ │ ├── OrdersActivity.kt +│ │ │ ├── OrderDetailActivity.kt +│ │ │ ├── OrderProgressActivity.kt +│ │ │ ├── CameraActivity.kt +│ │ │ ├── SignatureActivity.kt +│ │ │ ├── SurveyActivity.kt +│ │ │ └── ConfirmationActivity.kt +│ │ ├── adapters/ # Adaptadores RecyclerView +│ │ ├── entities/ # Entidades ObjectBox (DB local) +│ │ ├── interfaces/ # Interfaces +│ │ ├── models/ # Modelos de datos +│ │ ├── network/ # Configuracion API (Retrofit) +│ │ ├── services/ # Servicios (notificaciones) +│ │ │ └── NotificationService.kt +│ │ └── utils/ # Utilidades +│ └── res/ # Recursos +│ ├── layout/ # Layouts XML +│ ├── values/ # Strings, colors, styles +│ ├── drawable/ # Iconos e imagenes +│ └── mipmap/ # Iconos de app +├── gradle/ +├── mylibrary/ # Libreria auxiliar +├── build.gradle # Configuracion proyecto +├── settings.gradle +└── google-services.json # Configuracion Firebase +``` + +--- + +## Pantallas de la Aplicacion + +| Activity | Descripcion | +|----------|-------------| +| SplashActivity | Pantalla de inicio/carga | +| LoginActivity | Inicio de sesion | +| WorkdayActivity | Inicio/fin de jornada laboral | +| OrdersActivity | Lista de servicios asignados | +| OrderDetailActivity | Detalle de un servicio | +| OrderProgressActivity | Progreso del servicio | +| CameraActivity | Captura de fotografias | +| SignatureActivity | Captura de firma del cliente | +| SurveyActivity | Encuestas de satisfaccion | +| ConfirmationActivity | Confirmacion de servicio | +| NextServiceActivity | Servicios del dia siguiente | +| PdfViewerActivity | Visualizador de PDF | +| OperatorsActivity | Lista de operadores (supervisor) | +| WorkdayManagerActivity | Gestion de jornada (supervisor) | +| OrdersManagerActivity | Gestion de ordenes (supervisor) | +| HerramientaSurveyActivity | Encuesta de herramientas | +| MaterialSurveyActivity | Encuesta de materiales | +| RevisionSurveyActivity | Encuesta de revision | + +--- + +## Flujos de la Aplicacion + +### Flujo Operador + +``` +1. Splash Screen + ↓ +2. Login (credenciales) + ↓ +3. Checklist Vehiculo + ↓ +4. Iniciar Jornada + ├── Seleccionar vehiculo + ├── Registrar kilometraje inicial + └── Capturar ubicacion GPS + ↓ +5. Ver Lista de Servicios del Dia + ↓ +6. Seleccionar Servicio + ↓ +7. Ver Detalle del Servicio + ├── Informacion del cliente + ├── Direccion (con mapa) + ├── Tipo de servicio + └── Observaciones + ↓ +8. Iniciar Servicio + └── Registra ubicacion GPS + ↓ +9. Realizar Trabajo + ↓ +10. Capturar Evidencias + ├── Foto antes + ├── Foto durante + └── Foto despues + ↓ +11. Encuestas + ├── Herramientas utilizadas + ├── Materiales empleados + └── Revision general + ↓ +12. Capturar Firma del Cliente + ↓ +13. Finalizar Servicio + ├── Registrar litraje + └── Comentarios + ↓ +14. Siguiente Servicio o Fin de Jornada + ├── Mas servicios → Paso 6 + └── Fin jornada → Registrar kilometraje final +``` + +### Flujo Supervisor + +``` +1. Login + ↓ +2. Iniciar Jornada Supervisor + ↓ +3. Ver Lista de Operadores + ↓ +4. Seleccionar Operador + ↓ +5. Ver Servicios del Operador + ├── Estatus de cada servicio + ├── Ubicacion actual + └── Progreso del dia + ↓ +6. Monitorear en Tiempo Real + ↓ +7. Fin de Jornada +``` + +--- + +## Configuracion de API + +### URLs de Conexion + +```kotlin +// En app/build.gradle - buildTypes + +// Produccion +buildConfigField("String", "BASE_URL", '"https://sio-api.consultoria-as.com/api/"') +buildConfigField("String", "STORAGE_URL", '"https://sio-api.consultoria-as.com/storage/"') + +// Desarrollo (opcional) +buildConfigField("String", "BASE_URL", '"http://192.168.1.100:8000/api/"') +buildConfigField("String", "STORAGE_URL", '"http://192.168.1.100:8000/storage/"') +``` + +### Archivo de Configuracion + +Ubicacion: `app/build.gradle` + +```gradle +android { + defaultConfig { + applicationId "com.iesoluciones.siodrenax" + minSdkVersion 28 + targetSdkVersion 33 + versionCode 3 + versionName "1.00" + } + + buildTypes { + debug { + buildConfigField("String", "BASE_URL", '"https://sio-api.consultoria-as.com/api/"') + buildConfigField("String", "STORAGE_URL", '"https://sio-api.consultoria-as.com/storage/"') + } + release { + minifyEnabled false + buildConfigField("String", "BASE_URL", '"https://sio-api.consultoria-as.com/api/"') + buildConfigField("String", "STORAGE_URL", '"https://sio-api.consultoria-as.com/storage/"') + } + } +} +``` + +--- + +## API Endpoints Utilizados + +### Autenticacion + +| Metodo | Endpoint | Descripcion | +|--------|----------|-------------| +| POST | /login | Iniciar sesion | + +### Operador + +| Metodo | Endpoint | Descripcion | +|--------|----------|-------------| +| GET | /operador/checklist | Obtener checklist del vehiculo | +| GET | /operador/checklist/vehiculos | Obtener vehiculos disponibles | +| POST | /operador/checklist | Enviar checklist completado | +| POST | /operador/iniciarjornada | Iniciar jornada laboral | +| POST | /operador/finalizarjornada/{id} | Finalizar jornada | +| GET | /operador/solicitud_servicios | Obtener servicios asignados | +| GET | /operador/solicitud_servicios/{id} | Detalle de servicio | +| POST | /operador/solicitud_servicios/iniciar | Iniciar servicio | +| POST | /operador/solicitud_servicios/finalizar-new | Finalizar servicio | +| POST | /operador/solicitud_servicios/verificar/servicios | Verificar servicios | +| GET | /operador/servicios/diasiguiente | Servicios del dia siguiente | +| POST | /operador/vehiculos_incidencias | Reportar incidencia vehiculo | +| POST | /operador/vehiculos_incidencias/ultima_incidencia | Ultima incidencia | +| PUT | /operador/vehiculos_incidencias/{id}/resolver | Resolver incidencia | + +### Supervisor de Operaciones + +| Metodo | Endpoint | Descripcion | +|--------|----------|-------------| +| GET | /supervisoroperaciones/asesores | Listar operadores | +| GET | /supervisoroperaciones/asesores/{id}/servicios | Servicios de operador | +| POST | /supervisoroperaciones/iniciarjornada | Iniciar jornada supervisor | +| POST | /supervisoroperaciones/finalizarjornada/{id} | Finalizar jornada | + +--- + +## Dependencias Principales + +### Networking (Retrofit + OkHttp) + +```gradle +implementation 'com.squareup.retrofit2:retrofit:2.5.0' +implementation 'com.squareup.retrofit2:converter-gson:2.4.0' +implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0' +implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1' +``` + +### Base de Datos Local (ObjectBox) + +```gradle +implementation "io.objectbox:objectbox-kotlin:$objectboxVersion" +debugImplementation "io.objectbox:objectbox-android-objectbrowser:$objectboxVersion" +``` + +Permite almacenamiento offline de: +- Datos de sesion +- Servicios descargados +- Fotos pendientes de subir +- Respuestas de encuestas + +### Firebase + +```gradle +implementation 'com.google.firebase:firebase-messaging:22.0.0' +implementation 'com.google.firebase:firebase-crashlytics:18.2.0' +implementation 'com.google.firebase:firebase-core:19.0.0' +``` + +Servicios utilizados: +- **FCM:** Notificaciones push +- **Crashlytics:** Reportes de errores +- **Analytics:** Estadisticas de uso + +### UI/UX + +```gradle +implementation 'com.google.android.material:material:1.5.0' +implementation 'androidx.constraintlayout:constraintlayout:2.1.3' +implementation 'com.squareup.picasso:picasso:2.71828' +implementation 'com.github.bumptech.glide:glide:4.11.0' +``` + +### Mapas + +```gradle +implementation 'com.google.android.gms:play-services-maps:18.0.2' +``` + +### Camara y Firma + +```gradle +implementation 'io.fotoapparat:fotoapparat:2.7.0' // Camara +implementation 'com.github.gcacace:signature-pad:1.3.1' // Firma digital +``` + +### PDF + +```gradle +implementation 'io.github.nvest-solutions:html-to-pdf-convertor:1.0.0' +implementation 'com.github.naya-aastra:SkewPdfView:1.1' +``` + +--- + +## Permisos Requeridos + +| Permiso | Uso | +|---------|-----| +| INTERNET | Conexion al API | +| CALL_PHONE | Llamar al cliente | +| FOREGROUND_SERVICE | Servicios en segundo plano | +| VIBRATE | Notificaciones | +| WAKE_LOCK | Mantener pantalla activa | +| READ_PHONE_STATE | Identificar dispositivo | +| READ_EXTERNAL_STORAGE | Leer fotos | +| WRITE_EXTERNAL_STORAGE | Guardar fotos/archivos | +| ACCESS_FINE_LOCATION | GPS preciso | +| ACCESS_COARSE_LOCATION | GPS aproximado | +| ACCESS_NOTIFICATION_POLICY | Gestionar notificaciones | +| CAMERA | Tomar fotografias | + +--- + +## Claves y Configuracion + +### Google Maps API Key + +```xml + + +``` + +### Firebase + +Archivo de configuracion: `app/google-services.json` + +**Nota:** Este archivo contiene las credenciales de Firebase y debe mantenerse privado. + +--- + +## Compilacion + +### Requisitos + +- Android Studio Arctic Fox o superior +- JDK 8 o superior +- Gradle 7.x +- Android SDK 33 + +### Compilar Debug APK + +```bash +cd SIO-Mobile +./gradlew assembleDebug +``` + +APK generado en: `app/build/outputs/apk/debug/app-debug.apk` + +### Compilar Release APK + +```bash +./gradlew assembleRelease +``` + +APK generado en: `app/build/outputs/apk/release/app-release.apk` + +**Nota:** Para release necesitas configurar signing en `build.gradle`: + +```gradle +android { + signingConfigs { + release { + storeFile file("keystore.jks") + storePassword "password" + keyAlias "alias" + keyPassword "password" + } + } + buildTypes { + release { + signingConfig signingConfigs.release + } + } +} +``` + +### Instalar en Dispositivo + +```bash +# Via ADB +adb install app/build/outputs/apk/debug/app-debug.apk + +# O directamente desde Android Studio +# Run > Run 'app' +``` + +--- + +## Notificaciones Push (Firebase) + +### Servicio + +```kotlin +// com.iesoluciones.siodrenax.services.NotificationService +``` + +### Tipos de Notificaciones + +| Tipo | Descripcion | +|------|-------------| +| Nuevo servicio | Servicio asignado al operador | +| Cambio estatus | Actualizacion de estatus de servicio | +| Recordatorio | Recordatorio de servicio proximo | +| Mensaje | Mensaje del sistema/supervisor | + +### Configuracion en Backend + +El token FCM se registra en el login y se actualiza automaticamente. + +--- + +## Almacenamiento Local (ObjectBox) + +### Entidades Almacenadas + +```kotlin +// Sesion del usuario +@Entity +data class UserSession( + @Id var id: Long = 0, + var userId: Int, + var token: String, + var nombre: String, + var email: String +) + +// Servicio en cache +@Entity +data class CachedService( + @Id var id: Long = 0, + var serviceId: Int, + var jsonData: String, + var syncStatus: Int +) + +// Evidencia pendiente +@Entity +data class PendingEvidence( + @Id var id: Long = 0, + var serviceId: Int, + var photoPath: String, + var type: String, + var uploaded: Boolean +) +``` + +### Sincronizacion + +La app sincroniza automaticamente cuando detecta conexion a internet: +- Sube evidencias pendientes +- Descarga servicios actualizados +- Envia encuestas completadas + +--- + +## Troubleshooting + +### App no conecta al servidor + +**Causa:** URL del API incorrecta o sin HTTPS + +**Solucion:** +1. Verificar `BASE_URL` en `app/build.gradle` +2. Asegurar que sea HTTPS: `https://sio-api.consultoria-as.com/api/` +3. Recompilar la app + +### Error de certificado SSL + +**Causa:** App configurada para HTTP, servidor usa HTTPS + +**Solucion:** +1. Actualizar URLs a HTTPS +2. Verificar en `AndroidManifest.xml`: +```xml +android:usesCleartextTraffic="false" +``` + +### Google Maps no carga + +**Causa:** API Key invalida o sin permisos + +**Solucion:** +1. Verificar API Key en Google Cloud Console +2. Habilitar Maps SDK for Android +3. Agregar restriccion de paquete: `com.iesoluciones.siodrenax` + +### Notificaciones no llegan + +**Causa:** Token Firebase no registrado + +**Solucion:** +1. Verificar `google-services.json` actualizado +2. Revisar permisos en Firebase Console +3. Verificar que el token se envie al backend en login + +### Fotos no se suben + +**Causa:** Conexion intermitente o timeout + +**Solucion:** +1. Las fotos se guardan localmente (ObjectBox) +2. Se suben automaticamente cuando hay conexion +3. Verificar en el log si hay errores de red + +### App crashea al iniciar + +**Causa:** Posible corrupcion de datos locales + +**Solucion:** +1. Limpiar datos de la app (Settings > Apps > SIO Drenax > Clear Data) +2. Si persiste, revisar Crashlytics para el error especifico + +--- + +## Actualizacion de la App + +### Cambiar Version + +En `app/build.gradle`: + +```gradle +defaultConfig { + versionCode 4 // Incrementar para cada release + versionName "1.01" // Version visible al usuario +} +``` + +### Generar APK Firmado (Android Studio) + +1. Build > Generate Signed Bundle/APK +2. Seleccionar APK +3. Usar keystore existente o crear nuevo +4. Seleccionar release +5. Generar + +### Distribucion + +- **Play Store:** Subir AAB (Android App Bundle) +- **Distribucion interna:** Compartir APK directamente +- **Firebase App Distribution:** Para pruebas beta + +--- + +## Estructura de Respuestas API + +### Login Exitoso + +```json +{ + "success": true, + "data": { + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", + "user": { + "id": 1, + "nombre": "Operador 1", + "email": "operador@mail.com", + "tipo_empleado_id": 2 + } + } +} +``` + +### Lista de Servicios + +```json +{ + "success": true, + "data": [ + { + "id": 123, + "cliente": "Juan Perez", + "direccion": "Calle 123, Colonia Centro", + "servicio": "Desazolve", + "hora": "09:00", + "estatus_id": 4, + "estatus_nombre": "Pendiente" + } + ] +} +``` + +### Error + +```json +{ + "success": false, + "error": "Token expirado", + "code": 401 +} +``` + +--- + +## Tecnologias Utilizadas + +- **Kotlin** - Lenguaje principal +- **Android SDK 33** - Target API +- **Retrofit** - Cliente HTTP +- **ObjectBox** - Base de datos local +- **Firebase** - Notificaciones y analytics +- **Google Maps** - Mapas y geolocalizacion +- **Fotoapparat** - Camara +- **Glide/Picasso** - Carga de imagenes +- **Material Design** - Componentes UI + +--- + +## Contacto + +| Campo | Valor | +|-------|-------| +| Sistema | SIO (Sistema Integral de Operaciones) | +| Empresa | Drenax | +| Package | com.iesoluciones.siodrenax | +| Desarrollo | IE Soluciones | +| Dominio | consultoria-as.com | + +--- + +**Version:** 1.0 +**Ultima actualizacion:** 2026-01-17 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..e3c940a --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,109 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-kapt' +} + +apply plugin: 'com.google.gms.google-services' +apply plugin: 'com.google.firebase.crashlytics' + +android { + compileSdkVersion 33 + defaultConfig { + applicationId "com.iesoluciones.siodrenax" + minSdkVersion 28 + targetSdkVersion 33 + versionCode 3 + //versionName "0.48" + versionName "1.00" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true // To accept variables colors in vectors + } + + buildTypes { + debug { + // -------- API -------- + buildConfigField("String", "BASE_URL", '"https://sio-api.consultoria-as.com/api/"') //PROD - Cloudflare Tunnel + buildConfigField("String", "STORAGE_URL", '"https://sio-api.consultoria-as.com/storage/"') //PROD STORAGE + //buildConfigField("String", "BASE_URL", '"http://107.170.231.250/v1/api/"') //PROD OLD + //buildConfigField("String", "STORAGE_URL", '"http://107.170.231.250/storage/"') //PROD STORAGE OLD + } + + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + // -------- API -------- + buildConfigField("String", "BASE_URL", '"https://sio-api.consultoria-as.com/api/"') //PROD - Cloudflare Tunnel + buildConfigField("String", "STORAGE_URL", '"https://sio-api.consultoria-as.com/storage/"') //PROD STORAGE + //buildConfigField("String", "BASE_URL", '"http://107.170.231.250/v1/api/"') //PROD OLD + //buildConfigField("String", "STORAGE_URL", '"http://107.170.231.250/storage/"') //PROD STORAGE OLD + } + } + + buildFeatures { + viewBinding true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'androidx.work:work-runtime:2.8.0' + implementation 'com.google.android.material:material:1.5.0' + + implementation 'com.squareup.picasso:picasso:2.71828' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' // Android Jetpack + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' //Android Jetpack + implementation "android.arch.work:work-runtime:1.0.1" //Android Jetpack //Android Jetpack + + implementation 'com.squareup.retrofit2:retrofit:2.5.0' + implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' //RxAndroid + implementation 'io.reactivex.rxjava2:rxjava:2.2.2' //RxJava + implementation 'com.squareup.retrofit2:converter-gson:2.4.0' //GSON Converter for Retrofit + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0' //Rx Requests for Retrofit + implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1' //Logging interceptor for every Request + implementation 'com.github.bumptech.glide:glide:4.11.0' + kapt 'com.github.bumptech.glide:compiler:4.11.0' + implementation 'com.google.android.gms:play-services-maps:18.0.2'// Google Maps Android API + + // ----- ObjectBox Data Browser ----- + debugImplementation "io.objectbox:objectbox-android-objectbrowser:$objectboxVersion" + releaseImplementation "io.objectbox:objectbox-android:$objectboxVersion" + implementation "io.objectbox:objectbox-kotlin:$objectboxVersion" + + // ----- Testing ----- + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + + // ----- Firebase ----- + implementation 'com.google.firebase:firebase-messaging:22.0.0' + implementation 'com.google.firebase:firebase-crashlytics:18.2.0' + implementation 'com.google.firebase:firebase-core:19.0.0' + implementation platform('com.google.firebase:firebase-bom:28.3.1') + implementation 'com.google.firebase:firebase-crashlytics-ktx' + implementation 'com.google.firebase:firebase-analytics-ktx' + + // ----- Camera & Sign ----- + implementation 'io.fotoapparat:fotoapparat:2.7.0' + implementation 'com.github.gcacace:signature-pad:1.3.1' + + // ----- HTML TO PDF ----- + implementation 'io.github.nvest-solutions:html-to-pdf-convertor:1.0.0' + implementation 'com.github.naya-aastra:SkewPdfView:1.1' + implementation project(':mylibrary') +} + +apply plugin: 'io.objectbox' // Apply last. \ No newline at end of file diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..27b198a --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,48 @@ +{ + "project_info": { + "project_number": "108152813393", + "firebase_url": "https://drenaxx-f561e.firebaseio.com", + "project_id": "drenaxx-f561e", + "storage_bucket": "drenaxx-f561e.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:108152813393:android:6a2ce608fab88e98", + "android_client_info": { + "package_name": "com.iesoluciones.siodrenax" + } + }, + "oauth_client": [ + { + "client_id": "108152813393-sui8jok0le3f873uoceguvrss61r8e8s.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.iesoluciones.siodrenax", + "certificate_hash": "43d5f941fa659b3f3e874fce11ff528d5ce26f1e" + } + }, + { + "client_id": "108152813393-g09ea87ufqm21om9qt2v45fgl079d704.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyB05HH7lJlT0AIlILWlit467ArJ11W5vTc" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "108152813393-g09ea87ufqm21om9qt2v45fgl079d704.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/objectbox-models/default.json b/app/objectbox-models/default.json new file mode 100644 index 0000000..d8b4d71 --- /dev/null +++ b/app/objectbox-models/default.json @@ -0,0 +1,1153 @@ +{ + "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", + "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", + "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", + "entities": [ + { + "id": "1:5665101293431379024", + "lastPropertyId": "15:208191760973984819", + "name": "User", + "properties": [ + { + "id": "1:5321525569462566807", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:5712919134554378309", + "name": "email", + "type": 9 + }, + { + "id": "11:2978276056752430768", + "name": "nombre", + "type": 9 + }, + { + "id": "12:5508122438903068159", + "name": "apellido_paterno", + "type": 9 + }, + { + "id": "13:2241675499279008452", + "name": "apellido_materno", + "type": 9 + }, + { + "id": "14:8281964782181768919", + "name": "telefono", + "type": 9 + }, + { + "id": "15:208191760973984819", + "name": "tipo_empleado_id", + "type": 6 + } + ], + "relations": [] + }, + { + "id": "2:4372468123517637909", + "lastPropertyId": "16:1177766786685605044", + "name": "CheckListQuestion", + "properties": [ + { + "id": "1:3400228994932651467", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:8650406753973225582", + "name": "nombre", + "type": 9 + }, + { + "id": "6:1643483388542391711", + "name": "tipo", + "type": 9 + }, + { + "id": "10:8688927534108840815", + "name": "fecha", + "type": 9 + }, + { + "id": "11:2400276669962255845", + "name": "tipo_radio_btn", + "type": 5 + }, + { + "id": "12:6562151703877694550", + "name": "tipo_text", + "type": 5 + }, + { + "id": "13:602620057792595382", + "name": "tipo_checkbox", + "type": 5 + }, + { + "id": "14:2026798835348264681", + "name": "respuesta_radio_btn", + "type": 9 + }, + { + "id": "15:7423604211200632430", + "name": "respuesta_text", + "type": 9 + }, + { + "id": "16:1177766786685605044", + "name": "respuesta_checkbox", + "type": 1 + } + ], + "relations": [] + }, + { + "id": "3:4377792362946807137", + "lastPropertyId": "2:670135943743083929", + "name": "Vehicle", + "properties": [ + { + "id": "1:4705575517894212104", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:670135943743083929", + "name": "nombre", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "4:9026394323396024465", + "lastPropertyId": "65:2818775822293115291", + "name": "Order", + "properties": [ + { + "id": "1:4790167818510396202", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:5822431497542564601", + "name": "idService", + "type": 6 + }, + { + "id": "3:368051226835786410", + "name": "idRequest", + "type": 6 + }, + { + "id": "4:5299115277318286719", + "name": "serviceName", + "type": 9 + }, + { + "id": "5:8509828799642636144", + "name": "idServiceStatus", + "type": 6 + }, + { + "id": "6:758228903305954550", + "name": "serviceStatusName", + "type": 9 + }, + { + "id": "7:3565593439842136067", + "name": "serviceStatusColor1", + "type": 9 + }, + { + "id": "8:1414901339937410644", + "name": "serviceStatusColor2", + "type": 9 + }, + { + "id": "9:766211398812222140", + "name": "idPayMethod", + "type": 6 + }, + { + "id": "10:5147361690223074723", + "name": "payMethodName", + "type": 9 + }, + { + "id": "11:4500258100657127431", + "name": "idServiceType", + "type": 6 + }, + { + "id": "12:8196932021437273167", + "name": "comments", + "type": 9 + }, + { + "id": "13:5865529873029354021", + "name": "cost", + "type": 9 + }, + { + "id": "14:4506031685435688370", + "name": "serviceTypeName", + "type": 9 + }, + { + "id": "15:8767456587919863166", + "name": "dateScheduled", + "type": 9 + }, + { + "id": "16:390465234933298875", + "name": "dateServiceRequest", + "type": 9 + }, + { + "id": "17:2232773430973000819", + "name": "idUserSchedule", + "type": 6 + }, + { + "id": "18:16974732538945366", + "name": "userScheduleName", + "type": 9 + }, + { + "id": "19:8555992252505178313", + "name": "userScheduleLastName", + "type": 9 + }, + { + "id": "20:6193392468828583706", + "name": "getUserScheduleMothersLastName", + "type": 9 + }, + { + "id": "21:4660796289579119889", + "name": "duracion", + "type": 9 + }, + { + "id": "22:3784527562475848084", + "name": "clientDefined", + "type": 6 + }, + { + "id": "23:622745567829234352", + "name": "idClient", + "type": 6 + }, + { + "id": "24:3488955318550079798", + "name": "clientDenomination", + "type": 9 + }, + { + "id": "25:1043990105761854142", + "name": "clientContactName", + "type": 9 + }, + { + "id": "26:2306458271923532534", + "name": "getClientContactCellphone", + "type": 9 + }, + { + "id": "27:8427015918175385673", + "name": "idClientAddress", + "type": 6 + }, + { + "id": "28:5613227685156295965", + "name": "streetAddress", + "type": 9 + }, + { + "id": "29:626834541446943773", + "name": "extNumberAddress", + "type": 9 + }, + { + "id": "30:3097111174238127635", + "name": "intNumberAddress", + "type": 9 + }, + { + "id": "31:2579197447570912033", + "name": "neighborhoodNameAddress", + "type": 9 + }, + { + "id": "32:8725626942246743515", + "name": "clientPostalCodeAddress", + "type": 9 + }, + { + "id": "33:7167249096611278992", + "name": "clientPhone", + "type": 9 + }, + { + "id": "34:1717487907822141185", + "name": "clientLat", + "type": 9 + }, + { + "id": "35:3923208556696213877", + "name": "clientLng", + "type": 9 + }, + { + "id": "36:2587769480300510869", + "name": "idOperator", + "type": 6 + }, + { + "id": "37:7098221334791768219", + "name": "operatorName", + "type": 9 + }, + { + "id": "38:2569311124845582577", + "name": "operatorLastName", + "type": 9 + }, + { + "id": "39:2199904261882308305", + "name": "operatorMothersName", + "type": 9 + }, + { + "id": "40:7770932078561947689", + "name": "idVehicle", + "type": 6 + }, + { + "id": "41:98427989004918261", + "name": "vehicleCodeName", + "type": 9 + }, + { + "id": "42:2451788547455986315", + "name": "vehicleOfficeName", + "type": 9 + }, + { + "id": "43:7831510545763858022", + "name": "idVehicleOffice", + "type": 6 + }, + { + "id": "44:8113716121116249544", + "name": "operatorOfficeName", + "type": 9 + }, + { + "id": "45:5857477213726475000", + "name": "idOperatorOffice", + "type": 6 + }, + { + "id": "46:1843499857429967030", + "name": "idAssistant1", + "type": 6 + }, + { + "id": "47:4275027260571981678", + "name": "idAssistant2", + "type": 6 + }, + { + "id": "48:7964662182008538023", + "name": "assistant1Name", + "type": 9 + }, + { + "id": "49:8566831797326428251", + "name": "assistant2Name", + "type": 9 + }, + { + "id": "50:9132735670991254606", + "name": "assistant1OfficeName", + "type": 9 + }, + { + "id": "51:1462071515349543367", + "name": "assistant2OfficeName", + "type": 9 + }, + { + "id": "52:3238931045124412371", + "name": "SurveyRequired", + "type": 5 + }, + { + "id": "53:2207836726115018568", + "name": "isSent", + "type": 1 + }, + { + "id": "57:2645779966994040528", + "name": "assistant1LastName", + "type": 9 + }, + { + "id": "58:3608510605234859928", + "name": "assistant1MothersName", + "type": 9 + }, + { + "id": "59:992561270306835037", + "name": "clientCity", + "type": 9 + }, + { + "id": "60:3211907636147776442", + "name": "negativeCost", + "type": 9 + }, + { + "id": "61:5525637547111531029", + "name": "pdfPath", + "type": 9 + }, + { + "id": "63:7095836277252619975", + "name": "sketchName", + "type": 9 + }, + { + "id": "64:7631710765117535428", + "name": "sketchPath", + "type": 9 + }, + { + "id": "65:2818775822293115291", + "name": "branchName", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "5:2910966717665207605", + "lastPropertyId": "2:359659400093111854", + "name": "NegativeServiceReason", + "properties": [ + { + "id": "1:8047838382872767093", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:359659400093111854", + "name": "descripcion", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "6:6355541814294916142", + "lastPropertyId": "5:1116445808186977890", + "name": "BusinessQuestion", + "properties": [ + { + "id": "1:9037400062941888607", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:231310261271246192", + "name": "nombre", + "type": 9 + }, + { + "id": "3:2611867171191406507", + "name": "orden", + "type": 5 + }, + { + "id": "4:4644663952015206374", + "name": "mostrar_numero", + "type": 5 + }, + { + "id": "5:1116445808186977890", + "name": "obligatorio", + "type": 1 + } + ], + "relations": [] + }, + { + "id": "7:2859876280723130335", + "lastPropertyId": "5:8986637898173850281", + "name": "BusinessAnswer", + "properties": [ + { + "id": "1:1827935606743061935", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:8469863193806391937", + "name": "pregunta_id", + "type": 6 + }, + { + "id": "3:8600426673309812655", + "name": "nombre", + "type": 9 + }, + { + "id": "4:4263203625425115275", + "name": "orden", + "type": 5 + }, + { + "id": "5:8986637898173850281", + "name": "tipo_campo", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "8:6173653981125352089", + "lastPropertyId": "5:4524742060668120515", + "name": "DomesticAnswer", + "properties": [ + { + "id": "1:1246178104144041976", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:9022364118858654246", + "name": "pregunta_id", + "type": 6 + }, + { + "id": "3:1328613281292912347", + "name": "nombre", + "type": 9 + }, + { + "id": "4:6284466592499946392", + "name": "orden", + "type": 5 + }, + { + "id": "5:4524742060668120515", + "name": "tipo_campo", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "9:5835999145885427316", + "lastPropertyId": "5:7827790482540953055", + "name": "DomesticQuestion", + "properties": [ + { + "id": "1:7983234636383478791", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:3482813846550658400", + "name": "nombre", + "type": 9 + }, + { + "id": "3:9092368960227262920", + "name": "orden", + "type": 5 + }, + { + "id": "4:2553545940366448952", + "name": "mostrar_numero", + "type": 5 + }, + { + "id": "5:7827790482540953055", + "name": "obligatorio", + "type": 1 + } + ], + "relations": [] + }, + { + "id": "10:5265773309037872977", + "lastPropertyId": "11:4204834214255679706", + "name": "Evidence", + "properties": [ + { + "id": "1:5097679613150378396", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:7852987476761877434", + "name": "evidenceNo", + "type": 6 + }, + { + "id": "3:6189591302802867443", + "name": "idOrder", + "type": 6 + }, + { + "id": "4:861441360886912220", + "name": "idRequest", + "type": 6 + }, + { + "id": "5:1047576737599164892", + "name": "type", + "type": 5 + }, + { + "id": "6:8546869156481956087", + "name": "path", + "type": 9 + }, + { + "id": "7:2483671667245694693", + "name": "lat", + "type": 9 + }, + { + "id": "8:6829820330214152801", + "name": "lng", + "type": 9 + }, + { + "id": "9:5161753908652537972", + "name": "viewRef", + "type": 5 + }, + { + "id": "10:2272502976273257358", + "name": "isSent", + "type": 1 + }, + { + "id": "11:4204834214255679706", + "name": "name", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "11:6475205932376807337", + "lastPropertyId": "53:1912263222050268605", + "name": "NextDayOrder", + "properties": [ + { + "id": "1:3264868661678103871", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:4456053841841817525", + "name": "idService", + "type": 6 + }, + { + "id": "3:7997711242381267225", + "name": "idRequest", + "type": 6 + }, + { + "id": "4:8271086619232851703", + "name": "serviceName", + "type": 9 + }, + { + "id": "5:7245352167591810796", + "name": "idServiceStatus", + "type": 6 + }, + { + "id": "6:5241727878047629545", + "name": "serviceStatusName", + "type": 9 + }, + { + "id": "7:4800279603731351311", + "name": "serviceStatusColor1", + "type": 9 + }, + { + "id": "8:1503098083362916520", + "name": "serviceStatusColor2", + "type": 9 + }, + { + "id": "9:4162194526312159096", + "name": "idPayMethod", + "type": 6 + }, + { + "id": "10:2343353027460528639", + "name": "payMethodName", + "type": 9 + }, + { + "id": "11:6561915191130038168", + "name": "idServiceType", + "type": 6 + }, + { + "id": "12:6839695974332536393", + "name": "comments", + "type": 9 + }, + { + "id": "13:1681165445808619896", + "name": "cost", + "type": 9 + }, + { + "id": "14:9201579815421133529", + "name": "serviceTypeName", + "type": 9 + }, + { + "id": "15:3532685957779838245", + "name": "dateScheduled", + "type": 9 + }, + { + "id": "16:1013511014762768893", + "name": "dateServiceRequest", + "type": 9 + }, + { + "id": "17:5141030834751053579", + "name": "idUserSchedule", + "type": 6 + }, + { + "id": "18:8316834618735938730", + "name": "userScheduleName", + "type": 9 + }, + { + "id": "19:3183152972501875602", + "name": "userScheduleLastName", + "type": 9 + }, + { + "id": "20:5109048459257258350", + "name": "getUserScheduleMothersLastName", + "type": 9 + }, + { + "id": "21:1871730937425078021", + "name": "duracion", + "type": 9 + }, + { + "id": "22:1907694265195596804", + "name": "clientDefined", + "type": 6 + }, + { + "id": "23:6625662055987934614", + "name": "idClient", + "type": 6 + }, + { + "id": "24:8843797071646769572", + "name": "clientDenomination", + "type": 9 + }, + { + "id": "25:9072820172183837569", + "name": "clientContactName", + "type": 9 + }, + { + "id": "26:6655742830533723645", + "name": "getClientContactCellphone", + "type": 9 + }, + { + "id": "27:4178398874819395513", + "name": "idClientAddress", + "type": 6 + }, + { + "id": "28:684820188619097939", + "name": "streetAddress", + "type": 9 + }, + { + "id": "29:8119650734803796332", + "name": "extNumberAddress", + "type": 9 + }, + { + "id": "30:8725509132974294337", + "name": "intNumberAddress", + "type": 9 + }, + { + "id": "31:559116993585388728", + "name": "neighborhoodNameAddress", + "type": 9 + }, + { + "id": "32:722618039033631165", + "name": "clientPostalCodeAddress", + "type": 9 + }, + { + "id": "33:5122355857579820755", + "name": "clientPhone", + "type": 9 + }, + { + "id": "34:5061809862649615222", + "name": "clientLat", + "type": 9 + }, + { + "id": "35:4421805919905742210", + "name": "clientLng", + "type": 9 + }, + { + "id": "36:16208392747324573", + "name": "idOperator", + "type": 6 + }, + { + "id": "37:1010399692004420493", + "name": "operatorName", + "type": 9 + }, + { + "id": "38:3377567735585312268", + "name": "operatorLastName", + "type": 9 + }, + { + "id": "39:3462303112734700369", + "name": "operatorMothersName", + "type": 9 + }, + { + "id": "40:8652651578878147290", + "name": "idVehicle", + "type": 6 + }, + { + "id": "41:7910381791883008711", + "name": "vehicleCodeName", + "type": 9 + }, + { + "id": "42:9132815036783490247", + "name": "vehicleOfficeName", + "type": 9 + }, + { + "id": "43:7372464010650191612", + "name": "idVehicleOffice", + "type": 6 + }, + { + "id": "44:1418155582741812092", + "name": "operatorOfficeName", + "type": 9 + }, + { + "id": "45:1573622015946405668", + "name": "idOperatorOffice", + "type": 6 + }, + { + "id": "46:8767471268383720558", + "name": "idAssistant1", + "type": 6 + }, + { + "id": "47:3801283899175927660", + "name": "idAssistant2", + "type": 6 + }, + { + "id": "48:4218765227230057063", + "name": "assistant1Name", + "type": 9 + }, + { + "id": "49:4638061793666351178", + "name": "assistant2Name", + "type": 9 + }, + { + "id": "50:7070724765117443211", + "name": "assistant1OfficeName", + "type": 9 + }, + { + "id": "51:5901287936339728517", + "name": "assistant2OfficeName", + "type": 9 + }, + { + "id": "52:3442054022641070646", + "name": "SurveyRequired", + "type": 5 + }, + { + "id": "53:1912263222050268605", + "name": "isSent", + "type": 1 + } + ], + "relations": [] + }, + { + "id": "12:6957973966007774656", + "lastPropertyId": "21:4053300102365953297", + "name": "OrderProgress", + "properties": [ + { + "id": "1:4909409867696772588", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:2376195153552155877", + "name": "idRequest", + "type": 6 + }, + { + "id": "3:8737352292837009750", + "name": "startDate", + "type": 9 + }, + { + "id": "4:8420872139229865602", + "name": "endDate", + "type": 9 + }, + { + "id": "5:4280475918090828107", + "name": "comments", + "type": 9 + }, + { + "id": "6:1833202438305001872", + "name": "elapsedTime", + "type": 9 + }, + { + "id": "7:4615340923415236343", + "name": "startLat", + "type": 9 + }, + { + "id": "8:1860619079461141044", + "name": "startLng", + "type": 9 + }, + { + "id": "9:7560964551558538330", + "name": "endLat", + "type": 9 + }, + { + "id": "10:3899822933000985859", + "name": "endLng", + "type": 9 + }, + { + "id": "11:7652484697176584591", + "name": "signaturePath", + "type": 9 + }, + { + "id": "12:6492043671717914407", + "name": "warranty", + "type": 1 + }, + { + "id": "13:2408814832806683450", + "name": "isSent", + "type": 1 + }, + { + "id": "14:7034569438693192646", + "name": "shouldBeSent", + "type": 1 + }, + { + "id": "15:2804509603161477239", + "name": "signatureSent", + "type": 1 + }, + { + "id": "16:4180953079476690512", + "name": "isOnSurvey", + "type": 1 + }, + { + "id": "17:3644568522287365574", + "name": "shouldSendSurvey", + "type": 1 + }, + { + "id": "18:6301736258721676414", + "name": "surveySent", + "type": 1 + }, + { + "id": "19:7669451713533174054", + "name": "liters", + "type": 5 + }, + { + "id": "20:3018195537892130994", + "name": "negativeService", + "type": 5 + }, + { + "id": "21:4053300102365953297", + "name": "signatureName", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "13:1724014081733562326", + "lastPropertyId": "5:7510073138677565100", + "name": "SavedAnswer", + "properties": [ + { + "id": "1:1211012512255883947", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:4555912609217419213", + "name": "questionId", + "type": 6 + }, + { + "id": "3:4690130029848358340", + "name": "orderId", + "type": 6 + }, + { + "id": "4:1609083068078937709", + "name": "isOpen", + "type": 1 + }, + { + "id": "5:7510073138677565100", + "name": "answer", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "14:6869636155040222828", + "lastPropertyId": "6:4385486216402212475", + "name": "Operator", + "properties": [ + { + "id": "1:8291666170904199724", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:8545676381506904920", + "name": "nombre", + "type": 9 + }, + { + "id": "3:3719435367169286614", + "name": "apellido_paterno", + "type": 9 + }, + { + "id": "4:878061371212085518", + "name": "apellido_materno", + "type": 9 + }, + { + "id": "5:1604294184145832145", + "name": "servicios_total", + "type": 6 + }, + { + "id": "6:4385486216402212475", + "name": "servicios_pendiente", + "type": 6 + } + ], + "relations": [] + } + ], + "lastEntityId": "14:6869636155040222828", + "lastIndexId": "0:0", + "lastRelationId": "0:0", + "lastSequenceId": "0:0", + "modelVersion": 5, + "modelVersionParserMinimum": 5, + "retiredEntityUids": [], + "retiredIndexUids": [], + "retiredPropertyUids": [ + 8671758719262032228, + 6526292377427765903, + 6681525836793970759, + 7520878950629538641, + 1018517886500659076, + 3603523460509643630, + 7114932134790372235, + 3232242511921438300, + 1382612365248212275, + 3351796694055151590, + 6316955532661353856, + 7554667419929082362, + 7587010331144518890, + 8355522860524560355, + 8052528201055769722, + 585043060788268876, + 7737740310883011682, + 755391302787676104 + ], + "retiredRelationUids": [], + "version": 1 +} \ No newline at end of file diff --git a/app/objectbox-models/default.json.bak b/app/objectbox-models/default.json.bak new file mode 100644 index 0000000..9f9bcf6 --- /dev/null +++ b/app/objectbox-models/default.json.bak @@ -0,0 +1,1148 @@ +{ + "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", + "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", + "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", + "entities": [ + { + "id": "1:5665101293431379024", + "lastPropertyId": "15:208191760973984819", + "name": "User", + "properties": [ + { + "id": "1:5321525569462566807", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:5712919134554378309", + "name": "email", + "type": 9 + }, + { + "id": "11:2978276056752430768", + "name": "nombre", + "type": 9 + }, + { + "id": "12:5508122438903068159", + "name": "apellido_paterno", + "type": 9 + }, + { + "id": "13:2241675499279008452", + "name": "apellido_materno", + "type": 9 + }, + { + "id": "14:8281964782181768919", + "name": "telefono", + "type": 9 + }, + { + "id": "15:208191760973984819", + "name": "tipo_empleado_id", + "type": 6 + } + ], + "relations": [] + }, + { + "id": "2:4372468123517637909", + "lastPropertyId": "16:1177766786685605044", + "name": "CheckListQuestion", + "properties": [ + { + "id": "1:3400228994932651467", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:8650406753973225582", + "name": "nombre", + "type": 9 + }, + { + "id": "6:1643483388542391711", + "name": "tipo", + "type": 9 + }, + { + "id": "10:8688927534108840815", + "name": "fecha", + "type": 9 + }, + { + "id": "11:2400276669962255845", + "name": "tipo_radio_btn", + "type": 5 + }, + { + "id": "12:6562151703877694550", + "name": "tipo_text", + "type": 5 + }, + { + "id": "13:602620057792595382", + "name": "tipo_checkbox", + "type": 5 + }, + { + "id": "14:2026798835348264681", + "name": "respuesta_radio_btn", + "type": 9 + }, + { + "id": "15:7423604211200632430", + "name": "respuesta_text", + "type": 9 + }, + { + "id": "16:1177766786685605044", + "name": "respuesta_checkbox", + "type": 1 + } + ], + "relations": [] + }, + { + "id": "3:4377792362946807137", + "lastPropertyId": "2:670135943743083929", + "name": "Vehicle", + "properties": [ + { + "id": "1:4705575517894212104", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:670135943743083929", + "name": "nombre", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "4:9026394323396024465", + "lastPropertyId": "64:7631710765117535428", + "name": "Order", + "properties": [ + { + "id": "1:4790167818510396202", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:5822431497542564601", + "name": "idService", + "type": 6 + }, + { + "id": "3:368051226835786410", + "name": "idRequest", + "type": 6 + }, + { + "id": "4:5299115277318286719", + "name": "serviceName", + "type": 9 + }, + { + "id": "5:8509828799642636144", + "name": "idServiceStatus", + "type": 6 + }, + { + "id": "6:758228903305954550", + "name": "serviceStatusName", + "type": 9 + }, + { + "id": "7:3565593439842136067", + "name": "serviceStatusColor1", + "type": 9 + }, + { + "id": "8:1414901339937410644", + "name": "serviceStatusColor2", + "type": 9 + }, + { + "id": "9:766211398812222140", + "name": "idPayMethod", + "type": 6 + }, + { + "id": "10:5147361690223074723", + "name": "payMethodName", + "type": 9 + }, + { + "id": "11:4500258100657127431", + "name": "idServiceType", + "type": 6 + }, + { + "id": "12:8196932021437273167", + "name": "comments", + "type": 9 + }, + { + "id": "13:5865529873029354021", + "name": "cost", + "type": 9 + }, + { + "id": "14:4506031685435688370", + "name": "serviceTypeName", + "type": 9 + }, + { + "id": "15:8767456587919863166", + "name": "dateScheduled", + "type": 9 + }, + { + "id": "16:390465234933298875", + "name": "dateServiceRequest", + "type": 9 + }, + { + "id": "17:2232773430973000819", + "name": "idUserSchedule", + "type": 6 + }, + { + "id": "18:16974732538945366", + "name": "userScheduleName", + "type": 9 + }, + { + "id": "19:8555992252505178313", + "name": "userScheduleLastName", + "type": 9 + }, + { + "id": "20:6193392468828583706", + "name": "getUserScheduleMothersLastName", + "type": 9 + }, + { + "id": "21:4660796289579119889", + "name": "duracion", + "type": 9 + }, + { + "id": "22:3784527562475848084", + "name": "clientDefined", + "type": 6 + }, + { + "id": "23:622745567829234352", + "name": "idClient", + "type": 6 + }, + { + "id": "24:3488955318550079798", + "name": "clientDenomination", + "type": 9 + }, + { + "id": "25:1043990105761854142", + "name": "clientContactName", + "type": 9 + }, + { + "id": "26:2306458271923532534", + "name": "getClientContactCellphone", + "type": 9 + }, + { + "id": "27:8427015918175385673", + "name": "idClientAddress", + "type": 6 + }, + { + "id": "28:5613227685156295965", + "name": "streetAddress", + "type": 9 + }, + { + "id": "29:626834541446943773", + "name": "extNumberAddress", + "type": 9 + }, + { + "id": "30:3097111174238127635", + "name": "intNumberAddress", + "type": 9 + }, + { + "id": "31:2579197447570912033", + "name": "neighborhoodNameAddress", + "type": 9 + }, + { + "id": "32:8725626942246743515", + "name": "clientPostalCodeAddress", + "type": 9 + }, + { + "id": "33:7167249096611278992", + "name": "clientPhone", + "type": 9 + }, + { + "id": "34:1717487907822141185", + "name": "clientLat", + "type": 9 + }, + { + "id": "35:3923208556696213877", + "name": "clientLng", + "type": 9 + }, + { + "id": "36:2587769480300510869", + "name": "idOperator", + "type": 6 + }, + { + "id": "37:7098221334791768219", + "name": "operatorName", + "type": 9 + }, + { + "id": "38:2569311124845582577", + "name": "operatorLastName", + "type": 9 + }, + { + "id": "39:2199904261882308305", + "name": "operatorMothersName", + "type": 9 + }, + { + "id": "40:7770932078561947689", + "name": "idVehicle", + "type": 6 + }, + { + "id": "41:98427989004918261", + "name": "vehicleCodeName", + "type": 9 + }, + { + "id": "42:2451788547455986315", + "name": "vehicleOfficeName", + "type": 9 + }, + { + "id": "43:7831510545763858022", + "name": "idVehicleOffice", + "type": 6 + }, + { + "id": "44:8113716121116249544", + "name": "operatorOfficeName", + "type": 9 + }, + { + "id": "45:5857477213726475000", + "name": "idOperatorOffice", + "type": 6 + }, + { + "id": "46:1843499857429967030", + "name": "idAssistant1", + "type": 6 + }, + { + "id": "47:4275027260571981678", + "name": "idAssistant2", + "type": 6 + }, + { + "id": "48:7964662182008538023", + "name": "assistant1Name", + "type": 9 + }, + { + "id": "49:8566831797326428251", + "name": "assistant2Name", + "type": 9 + }, + { + "id": "50:9132735670991254606", + "name": "assistant1OfficeName", + "type": 9 + }, + { + "id": "51:1462071515349543367", + "name": "assistant2OfficeName", + "type": 9 + }, + { + "id": "52:3238931045124412371", + "name": "SurveyRequired", + "type": 5 + }, + { + "id": "53:2207836726115018568", + "name": "isSent", + "type": 1 + }, + { + "id": "57:2645779966994040528", + "name": "assistant1LastName", + "type": 9 + }, + { + "id": "58:3608510605234859928", + "name": "assistant1MothersName", + "type": 9 + }, + { + "id": "59:992561270306835037", + "name": "clientCity", + "type": 9 + }, + { + "id": "60:3211907636147776442", + "name": "negativeCost", + "type": 9 + }, + { + "id": "61:5525637547111531029", + "name": "pdfPath", + "type": 9 + }, + { + "id": "63:7095836277252619975", + "name": "sketchName", + "type": 9 + }, + { + "id": "64:7631710765117535428", + "name": "sketchPath", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "5:2910966717665207605", + "lastPropertyId": "2:359659400093111854", + "name": "NegativeServiceReason", + "properties": [ + { + "id": "1:8047838382872767093", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:359659400093111854", + "name": "descripcion", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "6:6355541814294916142", + "lastPropertyId": "5:1116445808186977890", + "name": "BusinessQuestion", + "properties": [ + { + "id": "1:9037400062941888607", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:231310261271246192", + "name": "nombre", + "type": 9 + }, + { + "id": "3:2611867171191406507", + "name": "orden", + "type": 5 + }, + { + "id": "4:4644663952015206374", + "name": "mostrar_numero", + "type": 5 + }, + { + "id": "5:1116445808186977890", + "name": "obligatorio", + "type": 1 + } + ], + "relations": [] + }, + { + "id": "7:2859876280723130335", + "lastPropertyId": "5:8986637898173850281", + "name": "BusinessAnswer", + "properties": [ + { + "id": "1:1827935606743061935", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:8469863193806391937", + "name": "pregunta_id", + "type": 6 + }, + { + "id": "3:8600426673309812655", + "name": "nombre", + "type": 9 + }, + { + "id": "4:4263203625425115275", + "name": "orden", + "type": 5 + }, + { + "id": "5:8986637898173850281", + "name": "tipo_campo", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "8:6173653981125352089", + "lastPropertyId": "5:4524742060668120515", + "name": "DomesticAnswer", + "properties": [ + { + "id": "1:1246178104144041976", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:9022364118858654246", + "name": "pregunta_id", + "type": 6 + }, + { + "id": "3:1328613281292912347", + "name": "nombre", + "type": 9 + }, + { + "id": "4:6284466592499946392", + "name": "orden", + "type": 5 + }, + { + "id": "5:4524742060668120515", + "name": "tipo_campo", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "9:5835999145885427316", + "lastPropertyId": "5:7827790482540953055", + "name": "DomesticQuestion", + "properties": [ + { + "id": "1:7983234636383478791", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:3482813846550658400", + "name": "nombre", + "type": 9 + }, + { + "id": "3:9092368960227262920", + "name": "orden", + "type": 5 + }, + { + "id": "4:2553545940366448952", + "name": "mostrar_numero", + "type": 5 + }, + { + "id": "5:7827790482540953055", + "name": "obligatorio", + "type": 1 + } + ], + "relations": [] + }, + { + "id": "10:5265773309037872977", + "lastPropertyId": "11:4204834214255679706", + "name": "Evidence", + "properties": [ + { + "id": "1:5097679613150378396", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:7852987476761877434", + "name": "evidenceNo", + "type": 6 + }, + { + "id": "3:6189591302802867443", + "name": "idOrder", + "type": 6 + }, + { + "id": "4:861441360886912220", + "name": "idRequest", + "type": 6 + }, + { + "id": "5:1047576737599164892", + "name": "type", + "type": 5 + }, + { + "id": "6:8546869156481956087", + "name": "path", + "type": 9 + }, + { + "id": "7:2483671667245694693", + "name": "lat", + "type": 9 + }, + { + "id": "8:6829820330214152801", + "name": "lng", + "type": 9 + }, + { + "id": "9:5161753908652537972", + "name": "viewRef", + "type": 5 + }, + { + "id": "10:2272502976273257358", + "name": "isSent", + "type": 1 + }, + { + "id": "11:4204834214255679706", + "name": "name", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "11:6475205932376807337", + "lastPropertyId": "53:1912263222050268605", + "name": "NextDayOrder", + "properties": [ + { + "id": "1:3264868661678103871", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:4456053841841817525", + "name": "idService", + "type": 6 + }, + { + "id": "3:7997711242381267225", + "name": "idRequest", + "type": 6 + }, + { + "id": "4:8271086619232851703", + "name": "serviceName", + "type": 9 + }, + { + "id": "5:7245352167591810796", + "name": "idServiceStatus", + "type": 6 + }, + { + "id": "6:5241727878047629545", + "name": "serviceStatusName", + "type": 9 + }, + { + "id": "7:4800279603731351311", + "name": "serviceStatusColor1", + "type": 9 + }, + { + "id": "8:1503098083362916520", + "name": "serviceStatusColor2", + "type": 9 + }, + { + "id": "9:4162194526312159096", + "name": "idPayMethod", + "type": 6 + }, + { + "id": "10:2343353027460528639", + "name": "payMethodName", + "type": 9 + }, + { + "id": "11:6561915191130038168", + "name": "idServiceType", + "type": 6 + }, + { + "id": "12:6839695974332536393", + "name": "comments", + "type": 9 + }, + { + "id": "13:1681165445808619896", + "name": "cost", + "type": 9 + }, + { + "id": "14:9201579815421133529", + "name": "serviceTypeName", + "type": 9 + }, + { + "id": "15:3532685957779838245", + "name": "dateScheduled", + "type": 9 + }, + { + "id": "16:1013511014762768893", + "name": "dateServiceRequest", + "type": 9 + }, + { + "id": "17:5141030834751053579", + "name": "idUserSchedule", + "type": 6 + }, + { + "id": "18:8316834618735938730", + "name": "userScheduleName", + "type": 9 + }, + { + "id": "19:3183152972501875602", + "name": "userScheduleLastName", + "type": 9 + }, + { + "id": "20:5109048459257258350", + "name": "getUserScheduleMothersLastName", + "type": 9 + }, + { + "id": "21:1871730937425078021", + "name": "duracion", + "type": 9 + }, + { + "id": "22:1907694265195596804", + "name": "clientDefined", + "type": 6 + }, + { + "id": "23:6625662055987934614", + "name": "idClient", + "type": 6 + }, + { + "id": "24:8843797071646769572", + "name": "clientDenomination", + "type": 9 + }, + { + "id": "25:9072820172183837569", + "name": "clientContactName", + "type": 9 + }, + { + "id": "26:6655742830533723645", + "name": "getClientContactCellphone", + "type": 9 + }, + { + "id": "27:4178398874819395513", + "name": "idClientAddress", + "type": 6 + }, + { + "id": "28:684820188619097939", + "name": "streetAddress", + "type": 9 + }, + { + "id": "29:8119650734803796332", + "name": "extNumberAddress", + "type": 9 + }, + { + "id": "30:8725509132974294337", + "name": "intNumberAddress", + "type": 9 + }, + { + "id": "31:559116993585388728", + "name": "neighborhoodNameAddress", + "type": 9 + }, + { + "id": "32:722618039033631165", + "name": "clientPostalCodeAddress", + "type": 9 + }, + { + "id": "33:5122355857579820755", + "name": "clientPhone", + "type": 9 + }, + { + "id": "34:5061809862649615222", + "name": "clientLat", + "type": 9 + }, + { + "id": "35:4421805919905742210", + "name": "clientLng", + "type": 9 + }, + { + "id": "36:16208392747324573", + "name": "idOperator", + "type": 6 + }, + { + "id": "37:1010399692004420493", + "name": "operatorName", + "type": 9 + }, + { + "id": "38:3377567735585312268", + "name": "operatorLastName", + "type": 9 + }, + { + "id": "39:3462303112734700369", + "name": "operatorMothersName", + "type": 9 + }, + { + "id": "40:8652651578878147290", + "name": "idVehicle", + "type": 6 + }, + { + "id": "41:7910381791883008711", + "name": "vehicleCodeName", + "type": 9 + }, + { + "id": "42:9132815036783490247", + "name": "vehicleOfficeName", + "type": 9 + }, + { + "id": "43:7372464010650191612", + "name": "idVehicleOffice", + "type": 6 + }, + { + "id": "44:1418155582741812092", + "name": "operatorOfficeName", + "type": 9 + }, + { + "id": "45:1573622015946405668", + "name": "idOperatorOffice", + "type": 6 + }, + { + "id": "46:8767471268383720558", + "name": "idAssistant1", + "type": 6 + }, + { + "id": "47:3801283899175927660", + "name": "idAssistant2", + "type": 6 + }, + { + "id": "48:4218765227230057063", + "name": "assistant1Name", + "type": 9 + }, + { + "id": "49:4638061793666351178", + "name": "assistant2Name", + "type": 9 + }, + { + "id": "50:7070724765117443211", + "name": "assistant1OfficeName", + "type": 9 + }, + { + "id": "51:5901287936339728517", + "name": "assistant2OfficeName", + "type": 9 + }, + { + "id": "52:3442054022641070646", + "name": "SurveyRequired", + "type": 5 + }, + { + "id": "53:1912263222050268605", + "name": "isSent", + "type": 1 + } + ], + "relations": [] + }, + { + "id": "12:6957973966007774656", + "lastPropertyId": "21:4053300102365953297", + "name": "OrderProgress", + "properties": [ + { + "id": "1:4909409867696772588", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:2376195153552155877", + "name": "idRequest", + "type": 6 + }, + { + "id": "3:8737352292837009750", + "name": "startDate", + "type": 9 + }, + { + "id": "4:8420872139229865602", + "name": "endDate", + "type": 9 + }, + { + "id": "5:4280475918090828107", + "name": "comments", + "type": 9 + }, + { + "id": "6:1833202438305001872", + "name": "elapsedTime", + "type": 9 + }, + { + "id": "7:4615340923415236343", + "name": "startLat", + "type": 9 + }, + { + "id": "8:1860619079461141044", + "name": "startLng", + "type": 9 + }, + { + "id": "9:7560964551558538330", + "name": "endLat", + "type": 9 + }, + { + "id": "10:3899822933000985859", + "name": "endLng", + "type": 9 + }, + { + "id": "11:7652484697176584591", + "name": "signaturePath", + "type": 9 + }, + { + "id": "12:6492043671717914407", + "name": "warranty", + "type": 1 + }, + { + "id": "13:2408814832806683450", + "name": "isSent", + "type": 1 + }, + { + "id": "14:7034569438693192646", + "name": "shouldBeSent", + "type": 1 + }, + { + "id": "15:2804509603161477239", + "name": "signatureSent", + "type": 1 + }, + { + "id": "16:4180953079476690512", + "name": "isOnSurvey", + "type": 1 + }, + { + "id": "17:3644568522287365574", + "name": "shouldSendSurvey", + "type": 1 + }, + { + "id": "18:6301736258721676414", + "name": "surveySent", + "type": 1 + }, + { + "id": "19:7669451713533174054", + "name": "liters", + "type": 5 + }, + { + "id": "20:3018195537892130994", + "name": "negativeService", + "type": 5 + }, + { + "id": "21:4053300102365953297", + "name": "signatureName", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "13:1724014081733562326", + "lastPropertyId": "5:7510073138677565100", + "name": "SavedAnswer", + "properties": [ + { + "id": "1:1211012512255883947", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:4555912609217419213", + "name": "questionId", + "type": 6 + }, + { + "id": "3:4690130029848358340", + "name": "orderId", + "type": 6 + }, + { + "id": "4:1609083068078937709", + "name": "isOpen", + "type": 1 + }, + { + "id": "5:7510073138677565100", + "name": "answer", + "type": 9 + } + ], + "relations": [] + }, + { + "id": "14:6869636155040222828", + "lastPropertyId": "6:4385486216402212475", + "name": "Operator", + "properties": [ + { + "id": "1:8291666170904199724", + "name": "id", + "type": 6, + "flags": 129 + }, + { + "id": "2:8545676381506904920", + "name": "nombre", + "type": 9 + }, + { + "id": "3:3719435367169286614", + "name": "apellido_paterno", + "type": 9 + }, + { + "id": "4:878061371212085518", + "name": "apellido_materno", + "type": 9 + }, + { + "id": "5:1604294184145832145", + "name": "servicios_total", + "type": 6 + }, + { + "id": "6:4385486216402212475", + "name": "servicios_pendiente", + "type": 6 + } + ], + "relations": [] + } + ], + "lastEntityId": "14:6869636155040222828", + "lastIndexId": "0:0", + "lastRelationId": "0:0", + "lastSequenceId": "0:0", + "modelVersion": 5, + "modelVersionParserMinimum": 5, + "retiredEntityUids": [], + "retiredIndexUids": [], + "retiredPropertyUids": [ + 8671758719262032228, + 6526292377427765903, + 6681525836793970759, + 7520878950629538641, + 1018517886500659076, + 3603523460509643630, + 7114932134790372235, + 3232242511921438300, + 1382612365248212275, + 3351796694055151590, + 6316955532661353856, + 7554667419929082362, + 7587010331144518890, + 8355522860524560355, + 8052528201055769722, + 585043060788268876, + 7737740310883011682, + 755391302787676104 + ], + "retiredRelationUids": [], + "version": 1 +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/com/iesoluciones/siodrenax/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/iesoluciones/siodrenax/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..e6a82d8 --- /dev/null +++ b/app/src/androidTest/java/com/iesoluciones/siodrenax/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.iesoluciones.siodrenax + +import androidx.test.InstrumentationRegistry +import androidx.test.runner.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getTargetContext() + assertEquals("com.iesoluciones.siodrenax", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0913d12 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/images/logo_drenax.png b/app/src/main/assets/images/logo_drenax.png new file mode 100644 index 0000000000000000000000000000000000000000..1806c3ca888d3774aa3df7e4c0d87cef24cb2c2e GIT binary patch literal 49934 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 literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/iesoluciones/siodrenax/App.kt b/app/src/main/java/com/iesoluciones/siodrenax/App.kt new file mode 100644 index 0000000..49816ff --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/App.kt @@ -0,0 +1,116 @@ +package com.iesoluciones.siodrenax + +import android.annotation.SuppressLint +import android.app.Application +import android.content.Context +import android.util.Log +import com.iesoluciones.siodrenax.BuildConfig.BASE_URL +import com.iesoluciones.siodrenax.entities.* +import com.iesoluciones.siodrenax.network.Api +import com.iesoluciones.siodrenax.network.TokenInterceptor +import io.objectbox.Box +import io.objectbox.BoxStore +import io.objectbox.android.AndroidObjectBrowser +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + +val preferencesHelper: PreferencesHelper by lazy { App.prefs!! } +val api: Api by lazy { App.api!! } +val userBox: Box by lazy { App.userBox!! } +val checkListQuestionBox: Box by lazy { App.checkListQuestionBox!! } +val vehicleBox: Box by lazy { App.vehicleBox!! } +val orderBox: Box by lazy { App.orderBox!! } +val orderProgressBox: Box by lazy { App.orderProgressBox!! } +val evidenceBox: Box by lazy { App.evidenceBox!! } +val businessQuestionBox: Box by lazy { App.businessQuestionBox!! } +val domesticQuestionBox: Box by lazy { App.domesticQuestionBox!! } +val negativeServiceReasonBox: Box by lazy { App.negativeServiceReasonBox!! } +val businessAnswerBox: Box by lazy { App.businessAnswerBox!! } +val domesticAnswerBox: Box by lazy { App.domesticAnswerBox!! } +val savedAnswerBox: Box by lazy { App.savedAnswerBox!! } +val operatorBox: Box by lazy {App.operatorBox!!} +val nextDayOrderBox: Box by lazy {App.nextDayOrderBox!!} + +class App : Application() { + + companion object { + var prefs: PreferencesHelper? = null + var api: Api? = null + var context: Context? = null + var shareInstance: App? = null + var boxStore: BoxStore? = null + var userBox: Box? = null + var checkListQuestionBox: Box? = null + var vehicleBox: Box? = null + var orderBox: Box? = null + var orderProgressBox: Box? = null + var evidenceBox: Box? = null + var businessQuestionBox: Box? = null + var domesticQuestionBox: Box? = null + var negativeServiceReasonBox: Box? = null + var businessAnswerBox: Box? = null + var domesticAnswerBox: Box? = null + var savedAnswerBox: Box? = null + var operatorBox: Box? = null + var nextDayOrderBox: Box? = null + } + + @SuppressLint("CheckResult") + override fun onCreate() { + super.onCreate() + + prefs = PreferencesHelper(applicationContext) + shareInstance = this + context = this + + boxStore = MyObjectBox.builder().androidContext(this).build() + userBox = boxStore!!.boxFor(User::class.java) + checkListQuestionBox = boxStore!!.boxFor(CheckListQuestion::class.java) + vehicleBox = boxStore!!.boxFor(Vehicle::class.java) + orderBox = boxStore!!.boxFor(Order::class.java) + orderProgressBox = boxStore!!.boxFor(OrderProgress::class.java) + evidenceBox = boxStore!!.boxFor(Evidence::class.java) + businessQuestionBox = boxStore!!.boxFor(BusinessQuestion::class.java) + domesticQuestionBox = boxStore!!.boxFor(DomesticQuestion::class.java) + negativeServiceReasonBox = boxStore!!.boxFor(NegativeServiceReason::class.java) + businessAnswerBox = boxStore!!.boxFor(BusinessAnswer::class.java) + domesticAnswerBox = boxStore!!.boxFor(DomesticAnswer::class.java) + savedAnswerBox = boxStore!!.boxFor(SavedAnswer::class.java) + operatorBox = boxStore!!.boxFor(Operator::class.java) + nextDayOrderBox = boxStore!!.boxFor(NextDayOrder::class.java) + + //Starting ObjectBox Data Browser (ONLY FOR TEST!!!) + // TODO Comentar antes de liberar + if (BuildConfig.DEBUG) { + val started = AndroidObjectBrowser(boxStore).start(this) + Log.i("ObjectBrowser", "Started: $started") + } + + //Instantiate Logging interceptor + val logging = HttpLoggingInterceptor() + logging.level = HttpLoggingInterceptor.Level.BASIC + + //Build HTTP Client + val client = OkHttpClient.Builder() + .addInterceptor(TokenInterceptor()) + .addInterceptor(logging) + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .build() + + //Build Retrofit Client + val retrofit = Retrofit.Builder() + .client(client) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + .baseUrl(BASE_URL) + .build() + + api = retrofit.create(Api::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/PreferencesHelper.kt b/app/src/main/java/com/iesoluciones/siodrenax/PreferencesHelper.kt new file mode 100644 index 0000000..8e7c7f6 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/PreferencesHelper.kt @@ -0,0 +1,65 @@ +package com.iesoluciones.siodrenax + +import android.content.Context + +class PreferencesHelper(context: Context) { + + private val fileName = "share_preferences_file" + private val prefs = context.getSharedPreferences(fileName, Context.MODE_PRIVATE) + + private val TOKEN_API = "tokenApi" + private val TOKEN_FIREBASE = "tokenFirebase" + private val IS_MANAGER = "isManager" + private val CHECK_LIST_PROGRESS = "checkListProgress" + private val IS_ENABLE_HERRAMIENTA_SURVEY = "isEnableHerramientaSurvey" + private val WORKDAY_STARTED = "workday_started" + private val WORKDAY_ID = "workday_id" + private val GPS_INTERVAL_IN_MINUTES = "gps_interval" + private val ORDER_PROGRESS = "order_in_progress" + private val IS_ENABLE_TO_SYNC = "syncEnable" + + var tokenApi: String? + get() = prefs.getString(TOKEN_API, null) + set(value) = prefs.edit().putString(TOKEN_API, value).apply() + + var tokenFirebase: String? + get() = prefs.getString(TOKEN_FIREBASE, "default_token_firebase") + set(value) = prefs.edit().putString(TOKEN_FIREBASE, value).apply() + + var isManager: Boolean + get() = prefs.getBoolean(IS_MANAGER, false) + set(value) = prefs.edit().putBoolean(IS_MANAGER, value).apply() + + var checkListProgress: String? + get() = prefs.getString(CHECK_LIST_PROGRESS, null) + set(value) = prefs.edit().putString(CHECK_LIST_PROGRESS, value).apply() + + var isEnableHerramientaSurvey: Boolean + get() = prefs.getBoolean(IS_ENABLE_HERRAMIENTA_SURVEY, true) + set(value) = prefs.edit().putBoolean(IS_ENABLE_HERRAMIENTA_SURVEY, value).apply() + + var workdayStarted: Boolean + get() = prefs.getBoolean(WORKDAY_STARTED, false) + set(value) = prefs.edit().putBoolean(WORKDAY_STARTED, value).apply() + + var workdayId: Int + get() = prefs.getInt(WORKDAY_ID, 0) + set(value) = prefs.edit().putInt(WORKDAY_ID, value).apply() + + var gpsInterval: Int + get() = prefs.getInt(GPS_INTERVAL_IN_MINUTES, 3) + set(value) = prefs.edit().putInt(GPS_INTERVAL_IN_MINUTES, value).apply() + + var orderInProgress: Long? + get() = prefs.getLong(ORDER_PROGRESS, 0L) + set(value){ + if (value == null) + prefs.edit().remove(ORDER_PROGRESS).apply() + else + prefs.edit().putLong(ORDER_PROGRESS, value).apply() + } + + var isEnableToSync: Boolean + get() = prefs.getBoolean(IS_ENABLE_TO_SYNC, true) + set(value) = prefs.edit().putBoolean(IS_ENABLE_TO_SYNC, value).apply() +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/CameraActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/CameraActivity.kt new file mode 100644 index 0000000..1548b72 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/CameraActivity.kt @@ -0,0 +1,262 @@ +package com.iesoluciones.siodrenax.activities + +import android.animation.Animator +import android.graphics.Bitmap +import android.os.Bundle +import android.view.View +import android.view.animation.DecelerateInterpolator +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.databinding.ActivityCameraBinding +import com.iesoluciones.siodrenax.entities.Evidence +import com.iesoluciones.siodrenax.repositories.EvidenceRepository +import com.iesoluciones.siodrenax.utils.Constants.Companion.DEFAULT_LATITUDE +import com.iesoluciones.siodrenax.utils.Constants.Companion.DEFAULT_LONGITUDE +import com.iesoluciones.siodrenax.utils.Constants.Companion.EVIDENCE_ID +import com.iesoluciones.siodrenax.utils.Constants.Companion.EVIDENCIA +import com.iesoluciones.siodrenax.utils.Constants.Companion.HIDE_ALPHA +import com.iesoluciones.siodrenax.utils.Constants.Companion.HIDE_DURATION +import com.iesoluciones.siodrenax.utils.Constants.Companion.SHOW_ALPHA +import com.iesoluciones.siodrenax.utils.Constants.Companion.SHOW_DURATION +import com.iesoluciones.siodrenax.utils.Constants.Companion.SHOW_TITLE_ALPHA +import com.iesoluciones.siodrenax.utils.Constants.Companion.SHOW_TITLE_DELAY +import com.iesoluciones.siodrenax.utils.Constants.Companion.SHOW_TITLE_DURATION +import com.iesoluciones.siodrenax.utils.HelperUtil +import com.iesoluciones.siodrenax.utils.toastLong +import com.iesoluciones.siodrenax.viewmodels.EvidenceViewModel +import io.fotoapparat.Fotoapparat +import io.fotoapparat.configuration.UpdateConfiguration +import io.fotoapparat.error.CameraErrorListener +import io.fotoapparat.exception.camera.CameraException +import io.fotoapparat.log.fileLogger +import io.fotoapparat.log.logcat +import io.fotoapparat.log.loggers +import io.fotoapparat.parameter.ScaleType +import io.fotoapparat.result.BitmapPhoto +import io.fotoapparat.result.WhenDoneListener +import io.fotoapparat.selector.back +import io.fotoapparat.selector.off +import io.fotoapparat.selector.torch + +class CameraActivity : AppCompatActivity() { + + private var resultado: Int = RESULT_OK + private var torchOn = false + private var show = true + + private lateinit var binding: ActivityCameraBinding + private lateinit var evidenceViewModel: EvidenceViewModel + private lateinit var evidenceRepository: EvidenceRepository + private lateinit var evidence: Evidence + private lateinit var fotoapparat: Fotoapparat + private var bitmapResult: Bitmap? = null + private lateinit var animListenerHide: Animator.AnimatorListener + private lateinit var animListenerShow: Animator.AnimatorListener + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityCameraBinding.inflate(layoutInflater) + setContentView(binding.root) + + evidenceViewModel = ViewModelProvider(this)[EvidenceViewModel::class.java] + evidenceRepository = EvidenceRepository() + + binding.cameraView.visibility = View.VISIBLE + evidence = evidenceRepository.getEvidenceById(intent.getLongExtra(EVIDENCE_ID, 0L)) + + showTitle(EVIDENCIA + evidence.getTypeDescription() + " " + evidence.evidenceNo) + fotoapparat = createFotoapparat() + + binding.capture.setOnClickListener { takePicture() } + binding.ivTorch.setImageResource(R.drawable.torch_off) + binding.ivTorch.setOnClickListener { toggleTorchOnImageView() } + binding.buttonConfirm.setOnClickListener { confirm() } + binding.buttonCancel.setOnClickListener { cancel() } + binding.tvCerrar.setOnClickListener { + fotoapparat.stop() + finish() + } + prepareGuidelines() + attachObservers() + } + + override fun onStart() { + super.onStart() + fotoapparat.start() + } + + override fun onStop() { + super.onStop() + fotoapparat.stop() + finish() + } + + override fun finish() { + setResult(resultado) + super.finish() + } + + private fun createFotoapparat(): Fotoapparat { + return Fotoapparat.with(this) + .into(binding.cameraView) + .previewScaleType(ScaleType.CenterInside) + .lensPosition(back()) + .logger(loggers(logcat(), fileLogger(this))) + .cameraErrorCallback(object : CameraErrorListener { + override fun onError(e: CameraException) { + toastLong(e.toString()) + } + }) + .build() + } + + private fun toggleTorchOnImageView() { + torchOn = !torchOn + binding.ivTorch.setImageResource(if (torchOn) R.drawable.torch else R.drawable.torch_off) + fotoapparat.updateConfiguration( + UpdateConfiguration.builder() + .flash(if (torchOn) torch() else off()) + .build() + ) + } + + private fun takePicture() { + binding.capture.isEnabled = false + binding.frameLoading.visibility = View.VISIBLE + + val photoResult = fotoapparat.takePicture() + + photoResult + .toBitmap() + .whenDone(object : WhenDoneListener { + override fun whenDone(it: BitmapPhoto?) { + runOnUiThread { + if (it != null) { + binding.frameContent.visibility = View.VISIBLE + binding.tvDescription.text = getString( + R.string.evidence_description, + EVIDENCIA, + evidence.getTypeDescription(), + evidence.evidenceNo + ) + + bitmapResult = HelperUtil().scaleBitmap(it.bitmap, binding.root.context.resources.displayMetrics.widthPixels) + binding.touchImageView.setImageBitmap(bitmapResult) + binding.frameLoading.visibility = View.GONE + binding.capture.isEnabled = true + } + } + } + }) + } + + private fun prepareGuidelines() { + animListenerHide = object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) {} + override fun onAnimationEnd(animator: Animator) { + binding.tvCerrar.visibility = View.GONE + binding.relativeFullScreen.visibility = View.GONE + } + + override fun onAnimationCancel(animator: Animator) {} + override fun onAnimationRepeat(animator: Animator) {} + } + + animListenerShow = object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) { + binding.tvCerrar.visibility = View.VISIBLE + binding.relativeFullScreen.visibility = View.VISIBLE + } + + override fun onAnimationEnd(animator: Animator) {} + override fun onAnimationCancel(animator: Animator) {} + override fun onAnimationRepeat(animator: Animator) {} + } + + binding.touchImageView.setOnClickListener { + show = !show + if (show) + show() + else + hide() + } + } + + private fun hide() { + binding.relativeFullScreen.animate() + .alpha(HIDE_ALPHA) + .setDuration(HIDE_DURATION) + .setInterpolator(DecelerateInterpolator()) + .setListener(animListenerHide) + .start() + + binding.tvCerrar.animate() + .alpha(HIDE_ALPHA) + .setDuration(HIDE_DURATION) + .setInterpolator(DecelerateInterpolator()) + .setListener(animListenerHide) + .start() + } + + private fun show() { + binding.relativeFullScreen.animate() + .alpha(SHOW_ALPHA) + .setDuration(SHOW_DURATION) + .setInterpolator(DecelerateInterpolator()) + .setListener(animListenerShow) + .start() + + binding.tvCerrar.animate() + .alpha(SHOW_ALPHA) + .setDuration(SHOW_DURATION) + .setInterpolator(DecelerateInterpolator()) + .setListener(animListenerShow) + .start() + } + + private fun showTitle(title: String?) { + binding.cardTitle.visibility = View.VISIBLE + binding.tvTitle.text = title + binding.cardTitle.alpha = SHOW_ALPHA + binding.cardTitle.animate() + .alpha(SHOW_TITLE_ALPHA) + .setDuration(SHOW_TITLE_DURATION) + .setInterpolator(DecelerateInterpolator()) + .setStartDelay(SHOW_TITLE_DELAY) + .start() + } + + private fun confirm() { + if (bitmapResult != null) { + binding.buttonConfirm.visibility = View.GONE + binding.buttonCancel.visibility = View.GONE + binding.buttonConfirm.isEnabled = false + evidenceViewModel.saveImage(filesDir, bitmapResult!!) + } + } + + private fun cancel() { + bitmapResult = null + binding.touchImageView.setImageBitmap(null) + binding.frameContent.visibility = View.GONE + } + + private fun attachObservers() { + evidenceViewModel.onImageSaved.observe(this) { evidenceSignatureLocal -> + binding.buttonConfirm.isEnabled = true + + if (evidenceSignatureLocal.path != null && evidenceSignatureLocal.name != null) { + evidence.path = evidenceSignatureLocal.path + evidence.name = evidenceSignatureLocal.name + evidence.lat = DEFAULT_LATITUDE.toString() + evidence.lng = DEFAULT_LONGITUDE.toString() + evidenceRepository.saveEvidence(evidence) + } else { + resultado = RESULT_CANCELED + } + + finish() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/ConfirmationActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/ConfirmationActivity.kt new file mode 100644 index 0000000..8a23180 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/ConfirmationActivity.kt @@ -0,0 +1,157 @@ +package com.iesoluciones.siodrenax.activities + +import android.animation.Animator +import android.graphics.Bitmap +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.animation.DecelerateInterpolator +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.databinding.ActivityConfirmationBinding +import com.iesoluciones.siodrenax.entities.Evidence +import com.iesoluciones.siodrenax.repositories.EvidenceRepository +import com.iesoluciones.siodrenax.utils.Constants.Companion.EVIDENCE_ID +import com.iesoluciones.siodrenax.utils.Constants.Companion.EVIDENCIA +import com.iesoluciones.siodrenax.utils.Constants.Companion.HIDE_ALPHA +import com.iesoluciones.siodrenax.utils.Constants.Companion.HIDE_DURATION +import com.iesoluciones.siodrenax.utils.Constants.Companion.SHOW_ALPHA +import com.iesoluciones.siodrenax.utils.Constants.Companion.SHOW_DURATION +import com.iesoluciones.siodrenax.utils.HelperUtil +import com.iesoluciones.siodrenax.utils.TouchImageView +import com.iesoluciones.siodrenax.utils.toastLong + +class ConfirmationActivity : AppCompatActivity(), View.OnClickListener { + + private var show = true + private var bitmap: Bitmap? = null + private lateinit var evidence: Evidence + private lateinit var evidenceRepository: EvidenceRepository + private lateinit var animListenerHide: Animator.AnimatorListener + private lateinit var animListenerShow: Animator.AnimatorListener + private lateinit var binding: ActivityConfirmationBinding + + private lateinit var tvDescription: TextView + private lateinit var tvCerrar: TextView + private lateinit var touchImageView: TouchImageView + private lateinit var relativeFullScreen: RelativeLayout + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + //window.setDecorFitsSystemWindows(false) + + binding = ActivityConfirmationBinding.inflate(layoutInflater) + setContentView(binding.root) + viewBinding() + + evidenceRepository = EvidenceRepository() + evidence = evidenceRepository.getEvidenceById( + intent.getLongExtra(EVIDENCE_ID, 0L) + ) + + try { + bitmap = HelperUtil().decodeFromFile(evidence.path!!, 0) + touchImageView.drawingCacheQuality = View.DRAWING_CACHE_QUALITY_HIGH + touchImageView.setImageBitmap(bitmap) + } catch (outOfMemoryError: OutOfMemoryError) { + toastLong(resources.getString(R.string.out_of_memory_error)) + } + + animListenerHide = object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) {} + override fun onAnimationEnd(animator: Animator) { + tvCerrar.visibility = View.GONE + relativeFullScreen.visibility = View.GONE + } + + override fun onAnimationCancel(animator: Animator) {} + override fun onAnimationRepeat(animator: Animator) {} + } + + animListenerShow = object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) { + tvCerrar.visibility = View.VISIBLE + relativeFullScreen.visibility = View.VISIBLE + } + + override fun onAnimationEnd(animator: Animator) {} + override fun onAnimationCancel(animator: Animator) {} + override fun onAnimationRepeat(animator: Animator) {} + } + + tvCerrar.setOnClickListener(this) + tvCerrar.visibility = View.VISIBLE + val description = EVIDENCIA + evidence.getTypeDescription() + " " + evidence.evidenceNo + tvDescription.text = description + + touchImageView.setOnClickListener { + show = !show + if (show) show() else hide() + } + } + + override fun onDestroy() { + super.onDestroy() + bitmap?.recycle() + } + + override fun onClick(v: View?) { + if (v != null) { + when (v.id) { + R.id.tvCerrar -> onBackPressed() + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + private fun hide() { + relativeFullScreen.animate() + .alpha(HIDE_ALPHA) + .setDuration(HIDE_DURATION) + .setInterpolator(DecelerateInterpolator()) + .setListener(animListenerHide) + .start() + + tvCerrar.animate() + .alpha(HIDE_ALPHA) + .setDuration(HIDE_DURATION) + .setInterpolator(DecelerateInterpolator()) + .setListener(animListenerHide) + .start() + } + + private fun show() { + relativeFullScreen.animate() + .alpha(SHOW_ALPHA) + .setDuration(SHOW_DURATION) + .setInterpolator(DecelerateInterpolator()) + .setListener(animListenerShow) + .start() + + tvCerrar.animate() + .alpha(SHOW_ALPHA) + .setDuration(SHOW_DURATION) + .setInterpolator(DecelerateInterpolator()) + .setListener(animListenerShow) + .start() + } + + private fun viewBinding(){ + tvDescription = binding.tvDescription + tvCerrar = binding.tvCerrar + touchImageView = binding.touchImageView + relativeFullScreen = binding.relativeFullScreen + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/HerramientaSurveyActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/HerramientaSurveyActivity.kt new file mode 100644 index 0000000..9a56705 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/HerramientaSurveyActivity.kt @@ -0,0 +1,112 @@ +package com.iesoluciones.siodrenax.activities + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import com.iesoluciones.mylibrary.activities.ToolbarActivity +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.adapters.HerramientaSurveyAdapter +import com.iesoluciones.siodrenax.databinding.ActivityHerramientaSurveyBinding +import com.iesoluciones.siodrenax.entities.CheckListQuestion +import com.iesoluciones.siodrenax.preferencesHelper +import com.iesoluciones.siodrenax.repositories.CheckListRepository +import com.iesoluciones.siodrenax.utils.Constants +import com.iesoluciones.siodrenax.utils.HelperUtil +import com.iesoluciones.siodrenax.utils.ListPaddingDecoration +import com.iesoluciones.siodrenax.viewmodels.CheckListViewModel + +class HerramientaSurveyActivity : ToolbarActivity() { + + private lateinit var checkListRepository: CheckListRepository + private lateinit var checkListQuestion: List + private lateinit var adapter: HerramientaSurveyAdapter + private lateinit var binding: ActivityHerramientaSurveyBinding + private lateinit var checkListViewModel: CheckListViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + binding = ActivityHerramientaSurveyBinding.inflate(layoutInflater) + setContentView(binding.root) + + @Suppress("USELESS_CAST") + val toolbar = binding.toolbar as Toolbar + toolbar.title = getString(R.string.herramienta) + toolbarToLoad(toolbar) + + checkListRepository = CheckListRepository() + checkListQuestion = checkListRepository.getSurveyCheckList(Constants.HERRAMIENTA) + + val recyclerView = binding.recycler + recyclerView.visibility = View.VISIBLE + recyclerView.addItemDecoration(ListPaddingDecoration(this@HerramientaSurveyActivity)) + + adapter = HerramientaSurveyAdapter(checkListQuestion) + recyclerView.adapter = adapter + recyclerView.layoutManager = LinearLayoutManager(this@HerramientaSurveyActivity) + + checkListViewModel = ViewModelProvider(this)[CheckListViewModel::class.java] + + attachObservers() + + binding.btnSiguiente.text = getString(R.string.hold_to_finish) + + binding.btnSiguiente.setOnClickListener { finalizarEncuesta() } + } + + private fun finalizarEncuesta() { + AlertDialog.Builder(this@HerramientaSurveyActivity) + .setTitle(R.string.titleDialogEncuestaOperadorFinalizar) + .setMessage(R.string.messageDialogEncuestaOperadorFinalizar) + .setPositiveButton(R.string.accept) { _, _ -> checkListViewModel.checkList() } + .setNegativeButton(R.string.cancel, null) + .show() + } + + private fun attachObservers() { + checkListViewModel.isLoading.observe(this) { s -> + if (s != null) { + binding.btnSiguiente.visibility = View.GONE + binding.frameLoading.visibility = View.VISIBLE + binding.tvLoading.text = s + } else { + binding.btnSiguiente.isEnabled = true + binding.frameLoading.visibility = View.GONE + } + } + + checkListViewModel.checkListSuccess.observe(this) { checkResponse -> + if (checkResponse != null) { + preferencesHelper.checkListProgress = Constants.WORKDAY + val intent = Intent(this@HerramientaSurveyActivity, WorkdayActivity::class.java) + startActivity(intent) + finish() + } + } + + checkListViewModel.checkListFailure.observe(this) { throwable -> + binding.btnSiguiente.visibility = View.VISIBLE + if (throwable != null) { + disableSurvey() + HelperUtil().parseError(this, throwable) + checkListViewModel.checkListFailure.postValue(null) + } + } + } + + private fun disableSurvey() { + if (!preferencesHelper.isEnableHerramientaSurvey) + return + + preferencesHelper.isEnableHerramientaSurvey = false + + val count = checkListQuestion.count() + for (position in 0 until count) { + adapter.notifyItemChanged(position) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/LoginActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/LoginActivity.kt new file mode 100644 index 0000000..9be8a84 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/LoginActivity.kt @@ -0,0 +1,119 @@ +package com.iesoluciones.siodrenax.activities + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.provider.Settings +import android.util.Log +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import com.google.android.gms.tasks.Task +import com.google.firebase.messaging.FirebaseMessaging +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.databinding.ActivityLoginBinding +import com.iesoluciones.siodrenax.preferencesHelper +import com.iesoluciones.siodrenax.repositories.CheckListRepository +import com.iesoluciones.siodrenax.utils.* +import com.iesoluciones.siodrenax.viewmodels.LoginViewModel +import java.io.File + +class LoginActivity : AppCompatActivity() { + + private lateinit var binding: ActivityLoginBinding + private lateinit var loginViewModel: LoginViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityLoginBinding.inflate(layoutInflater) + setContentView(binding.root) + + loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java] + attachObservers() + + binding.btnLogin.setOnClickListener { login() } + + //binding.etEmail.setText("operador@mail.com") + //binding.etPassword.setText("secret") + + firebaseNotifications() + + deleteSketchFolder() + } + + @SuppressLint("HardwareIds") + private fun login() { + val email = binding.etEmail.text.toString() + val password = binding.etPassword.text.toString() + + if (email.isNotEmpty() && password.isNotEmpty()) { + val deviceId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) + loginViewModel.login(email, password, deviceId) + } else if (email.isEmpty()) { + toast(getString(R.string.required_email)) + } else if (password.isEmpty()) { + toast(getString(R.string.required_password)) + } + } + + private fun attachObservers() { + + loginViewModel.isLoading.observe(this) { s -> + if (s != null) { + binding.btnLogin.isEnabled = false + binding.frameLoading.visibility = View.VISIBLE + binding.tvLoading.text = s + } else { + binding.btnLogin.isEnabled = true + binding.frameLoading.visibility = View.GONE + } + } + + loginViewModel.loginSuccess.observe(this) { success -> + if (success) { + + if (preferencesHelper.isManager) { + goToActivity { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + finish() + } else if (!preferencesHelper.isManager) { + var progressActivity: Class<*> = WorkdayActivity::class.java + + if (CheckListRepository().isCheckListDone()) + progressActivity = RevisionSurveyActivity::class.java + + val intent = Intent(this, progressActivity) + startActivity(intent) + finish() + } + } + } + + loginViewModel.loginFailure.observe(this) { throwable -> + if (throwable != null) { + HelperUtil().parseError(this, throwable) + loginViewModel.loginFailure.postValue(null) + } + } + } + + private fun firebaseNotifications() { + FirebaseMessaging.getInstance().token.addOnCompleteListener { task: Task -> + if (!task.isSuccessful) { + Log.w("FIREBASE", getString(R.string.firebase_token_registration_failed), task.exception) + return@addOnCompleteListener + } + // Get new FCM registration token + preferencesHelper.tokenFirebase = task.result!! + } + } + + private fun deleteSketchFolder(){ + try { + File("$filesDir","pdf_sketchs").deleteRecursively() + } catch (e: Exception) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/MaterialSurveyActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/MaterialSurveyActivity.kt new file mode 100644 index 0000000..0c50bed --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/MaterialSurveyActivity.kt @@ -0,0 +1,61 @@ +package com.iesoluciones.siodrenax.activities + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.Toolbar +import androidx.recyclerview.widget.LinearLayoutManager +import com.iesoluciones.mylibrary.activities.ToolbarActivity +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.adapters.MaterialSurveyAdapter +import com.iesoluciones.siodrenax.databinding.ActivityMaterialSurveyBinding +import com.iesoluciones.siodrenax.entities.CheckListQuestion +import com.iesoluciones.siodrenax.preferencesHelper +import com.iesoluciones.siodrenax.repositories.CheckListRepository +import com.iesoluciones.siodrenax.utils.Constants +import com.iesoluciones.siodrenax.utils.ListPaddingDecoration + +class MaterialSurveyActivity : ToolbarActivity() { + + private lateinit var checkListRepository: CheckListRepository + private lateinit var checkListQuestion: List + lateinit var binding: ActivityMaterialSurveyBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMaterialSurveyBinding.inflate(layoutInflater) + setContentView(binding.root) + + @Suppress("USELESS_CAST") + val toolbar = binding.toolbar as Toolbar + toolbar.title = getString(R.string.material) + toolbarToLoad(toolbar) + + checkListRepository = CheckListRepository() + checkListQuestion = checkListRepository.getSurveyCheckList(Constants.MATERIAL) + + val recyclerView = binding.recycler + recyclerView.visibility = View.VISIBLE + recyclerView.addItemDecoration(ListPaddingDecoration(this@MaterialSurveyActivity)) + recyclerView.adapter = MaterialSurveyAdapter(checkListQuestion) + recyclerView.layoutManager = LinearLayoutManager(this@MaterialSurveyActivity) + + binding.btnSiguiente.text = getString(R.string.siguiente) + binding.btnSiguiente.setOnClickListener { onClickBtnSiguiente() } + } + + private fun onClickBtnSiguiente() { + AlertDialog.Builder(this@MaterialSurveyActivity) + .setTitle(R.string.titleDialogEncuestaOperador) + .setMessage(R.string.messageDialogEncuestaOperador) + .setPositiveButton(R.string.accept) { _, _ -> + preferencesHelper.checkListProgress = Constants.HERRAMIENTA + val intent = Intent(this@MaterialSurveyActivity, HerramientaSurveyActivity::class.java) + startActivity(intent) + finish() + } + .setNegativeButton(R.string.cancel, null) + .show() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/NextServiceActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/NextServiceActivity.kt new file mode 100644 index 0000000..7b0d2ef --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/NextServiceActivity.kt @@ -0,0 +1,126 @@ +package com.iesoluciones.siodrenax.activities + +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.widget.TextView +import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.iesoluciones.mylibrary.activities.ToolbarActivity +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.adapters.NextDayOrdersAdapter +import com.iesoluciones.siodrenax.databinding.ActivityNextServiceBinding +import com.iesoluciones.siodrenax.entities.NextDayOrder +import com.iesoluciones.siodrenax.receivers.UpdateUIReceiver +import com.iesoluciones.siodrenax.utils.ListPaddingDecoration +import com.iesoluciones.siodrenax.viewmodels.OrdersViewModel + +class NextServiceActivity : ToolbarActivity(), SwipeRefreshLayout.OnRefreshListener, + UpdateUIReceiver.UiUpdateListener { + + private lateinit var binding: ActivityNextServiceBinding + private lateinit var ordersViewModel: OrdersViewModel + + private lateinit var orderList: List + private var adapter: NextDayOrdersAdapter? = null + + private lateinit var recyclerView: RecyclerView + private lateinit var swipeRefresh: SwipeRefreshLayout + private lateinit var tvEmpty: TextView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityNextServiceBinding.inflate(layoutInflater) + setContentView(binding.root) + viewBinding() + + @Suppress("USELESS_CAST") + val toolbar = binding.toolbar as Toolbar + toolbar.title = getString(R.string.next_day_services) + toolbarToLoad(toolbar) + + ordersViewModel = ViewModelProvider(this)[OrdersViewModel::class.java] + + attachToObservables() + swipeRefresh.setOnRefreshListener(this) + } + + override fun onRefresh() { + ordersViewModel.isLoading.value = getString(R.string.loading) + ordersViewModel.refreshNextDayOrders() + } + + override fun updateUI() {} + + override fun onResume() { + super.onResume() + ordersViewModel.updateNextDayOrdersFromDB() + } + + override fun onDestroy() { + super.onDestroy() + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + } + + override fun onBackPressed() { + super.onBackPressed() + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.slide_out_right_airbnb) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + finish() + overridePendingTransition( + R.anim.slide_in_rigth_airbnb, + R.anim.slide_out_right_airbnb + ) + } + } + + return super.onOptionsItemSelected(item) + } + + override fun onPointerCaptureChanged(hasCapture: Boolean) {} + + private fun attachToObservables() { + ordersViewModel.refreshNextDayOrders() + ordersViewModel.getNextDayOrders().observe(this) { nextDayOrders -> + orderList = nextDayOrders + if (nextDayOrders.isNotEmpty()) { + if (adapter == null) { + tvEmpty.visibility = View.GONE + recyclerView.visibility = View.VISIBLE + recyclerView.addItemDecoration(ListPaddingDecoration(this@NextServiceActivity)) + adapter = NextDayOrdersAdapter(this@NextServiceActivity, nextDayOrders) + val linearLayoutManager = LinearLayoutManager(this@NextServiceActivity) + recyclerView.adapter = adapter + recyclerView.layoutManager = linearLayoutManager + adapter!!.notifyDataSetChanged() + ordersViewModel.refreshNextDayOrders() + + } else { + tvEmpty.visibility = View.GONE + recyclerView.visibility = View.VISIBLE + adapter!!.setOrders(nextDayOrders) + adapter!!.notifyDataSetChanged() + } + + } else { + recyclerView.visibility = View.GONE + tvEmpty.visibility = View.VISIBLE + } + + if (swipeRefresh.isRefreshing) swipeRefresh.isRefreshing = false + } + } + + private fun viewBinding(){ + recyclerView = binding.recycler + swipeRefresh = binding.swipeRefresh + tvEmpty = binding.tvEmpty + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/OperatorsActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/OperatorsActivity.kt new file mode 100644 index 0000000..1993870 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/OperatorsActivity.kt @@ -0,0 +1,124 @@ +package com.iesoluciones.siodrenax.activities + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import com.iesoluciones.mylibrary.activities.ToolbarActivity +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.adapters.OperatorAdapter +import com.iesoluciones.siodrenax.databinding.ActivityOperatorsBinding +import com.iesoluciones.siodrenax.entities.Operator +import com.iesoluciones.siodrenax.utils.Constants.Companion.OPERATOR_ID +import com.iesoluciones.siodrenax.utils.Constants.Companion.OPERATOR_NAME +import com.iesoluciones.siodrenax.utils.HelperUtil +import com.iesoluciones.siodrenax.utils.ListPaddingDecoration +import com.iesoluciones.siodrenax.viewmodels.ManagerViewModel + +class OperatorsActivity : ToolbarActivity(), OnRefreshListener, + OperatorAdapter.OnOperatorSelectedListener { + + private lateinit var binding: ActivityOperatorsBinding + private lateinit var managerViewModel: ManagerViewModel + var adapter: OperatorAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityOperatorsBinding.inflate(layoutInflater) + setContentView(binding.root) + + managerViewModel = ViewModelProvider(this)[ManagerViewModel::class.java] + + @Suppress("USELESS_CAST") + val toolbar = binding.toolbar as Toolbar + toolbar.title = getString(R.string.t_OperatorsActivity) + toolbarToLoad(toolbar) + + attachToObservables() + binding.swipeRefresh.setOnRefreshListener(this) + managerViewModel.getOperators() + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.workday, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.log_out -> showLogOutConfirmation() + } + return super.onOptionsItemSelected(item) + } + + private fun attachToObservables(){ + managerViewModel.onCallFailure.observe(this) { throwable: Throwable? -> + if (throwable != null) { + HelperUtil().parseError(this@OperatorsActivity, throwable) + managerViewModel.onCallFailure.value = null + } + } + + managerViewModel.isLoading.observe(this) { s: String? -> + if (s != null) { + binding.frameLoading.visibility = View.VISIBLE + binding.tvLoading.text = s + binding.recycler.isEnabled = false + } else { + binding.frameLoading.visibility = View.GONE + binding.recycler.isEnabled = true + } + if (binding.swipeRefresh.isRefreshing) binding.swipeRefresh.isRefreshing = false + } + + managerViewModel.getOperatorList()!!.observe(this) { operators: List -> + + //Refresh adapter and recyclerView + if (adapter == null) { + adapter = OperatorAdapter(operators, this@OperatorsActivity) + binding.recycler.layoutManager = LinearLayoutManager(this@OperatorsActivity) + binding.recycler.addItemDecoration( + ListPaddingDecoration(this@OperatorsActivity) + ) + binding.recycler.adapter = adapter + } else { + adapter!!.operatorList = operators + adapter!!.notifyDataSetChanged() + } + } + + managerViewModel.onWorkdaySuccess.observe(this) { success: Boolean -> + if (success) { + startActivity(Intent(this@OperatorsActivity, LoginActivity::class.java)) + finish() + } + } + } + + override fun onRefresh() { + managerViewModel.getOperators() + } + + private fun showLogOutConfirmation() { + AlertDialog.Builder(this) + .setTitle(getString(R.string.ad_t_cerrar_sesion)) + .setMessage(getString(R.string.ad_m_cerrar_sesion)) + .setPositiveButton(getString(R.string.ad_aceptar)) { _, _ -> managerViewModel.endWorkload() } + .setNegativeButton(getString(R.string.ad_cancelar), null) + .setCancelable(false) + .show() + } + + override fun onOperatorClicked(operator: Operator) { + val intent = Intent(this@OperatorsActivity, OrdersManagerActivity::class.java) + intent.putExtra(OPERATOR_ID, operator.id) + intent.putExtra(OPERATOR_NAME,"${operator.name} ${operator.lastName} ${operator.mothersName}") + startActivity(intent) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/OrderDetailActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/OrderDetailActivity.kt new file mode 100644 index 0000000..ed76f59 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/OrderDetailActivity.kt @@ -0,0 +1,1439 @@ +package com.iesoluciones.siodrenax.activities + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.provider.Settings +import android.telephony.PhoneNumberUtils +import android.text.Editable +import android.text.InputType +import android.text.TextWatcher +import android.util.Log +import android.view.View +import android.view.View.OnLongClickListener +import android.widget.ImageView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.AppCompatEditText +import androidx.appcompat.widget.Toolbar +import androidx.core.app.ActivityCompat +import androidx.lifecycle.ViewModelProvider +import com.iesoluciones.mylibrary.activities.ToolbarActivity +import com.iesoluciones.siodrenax.* +import com.iesoluciones.siodrenax.adapters.OrdersAdapter.Companion.inFormatter +import com.iesoluciones.siodrenax.adapters.OrdersAdapter.Companion.outFormatter +import com.iesoluciones.siodrenax.databinding.ActivityOrderDetailBinding +import com.iesoluciones.siodrenax.entities.Evidence +import com.iesoluciones.siodrenax.entities.Order +import com.iesoluciones.siodrenax.entities.OrderProgress +import com.iesoluciones.siodrenax.models.OrderPdf +import com.iesoluciones.siodrenax.models.Reason +import com.iesoluciones.siodrenax.repositories.OrdersRepository +import com.iesoluciones.siodrenax.repositories.SurveyRepository +import com.iesoluciones.siodrenax.utils.* +import com.iesoluciones.siodrenax.utils.Constants.Companion.DEFAULT_LATITUDE +import com.iesoluciones.siodrenax.utils.Constants.Companion.DEFAULT_LITERS +import com.iesoluciones.siodrenax.utils.Constants.Companion.DEFAULT_LONGITUDE +import com.iesoluciones.siodrenax.utils.Constants.Companion.EVIDENCE_CODE +import com.iesoluciones.siodrenax.utils.Constants.Companion.EVIDENCE_ID +import com.iesoluciones.siodrenax.utils.Constants.Companion.EVIDENCE_NAME +import com.iesoluciones.siodrenax.utils.Constants.Companion.ORDER_ID +import com.iesoluciones.siodrenax.utils.Constants.Companion.REQUEST_PERMISSIONS_REQUEST_CODE +import com.iesoluciones.siodrenax.utils.Constants.Companion.SIGNATURE_CODE +import com.iesoluciones.siodrenax.utils.Constants.Companion.SURVEY_CODE +import com.iesoluciones.siodrenax.viewmodels.OrderProgressViewModel +import java.io.File +import java.text.NumberFormat +import java.text.ParseException +import java.util.* + + +class OrderDetailActivity : ToolbarActivity() { + + private lateinit var orderProgressViewModel: OrderProgressViewModel + private lateinit var binding: ActivityOrderDetailBinding + + private val ordersRepository = OrdersRepository() + private val surveyRepository = SurveyRepository() + + private lateinit var startImageViews: List + private lateinit var processImageViews: List + private lateinit var finalImageViews: List + private lateinit var actualImageView: ImageView + + private lateinit var order: Order + private lateinit var orderProgress: OrderProgress + private lateinit var reason: Array + + private var id: Int = 0 + private var elapsedTime: Long = 0 + private var orderId: Long = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityOrderDetailBinding.inflate(layoutInflater) + setContentView(binding.root) + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + + startImageViews = listOf( + binding.ivEvidenceStart1, + binding.ivEvidenceStart2, + binding.ivEvidenceStart3 + ) + processImageViews = listOf( + binding.ivEvidenceProcess1, + binding.ivEvidenceProcess2, + binding.ivEvidenceProcess3 + ) + finalImageViews = listOf( + binding.ivEvidenceFinal1, + binding.ivEvidenceFinal2, + binding.ivEvidenceFinal3 + ) + + @Suppress("USELESS_CAST") + val toolbar = binding.toolbar as Toolbar + toolbar.title = getString(R.string.t_OrderDetailActivity) + toolbarToLoad(toolbar) + + editLiters = binding.editLiters + + orderProgressViewModel = ViewModelProvider(this)[OrderProgressViewModel::class.java] + + order = ordersRepository.getOrderById(intent.getLongExtra(ORDER_ID, 0L))!! + orderProgressViewModel.getEvidences(order.id) + orderId = intent.getLongExtra(ORDER_ID, 0L) + orderProgress = ordersRepository.getOrderProgressById(orderId)!! + + setUpViews() + attachObservers() + + if (orderProgress.endDate != null && !orderProgress.isOnSurvey) { + binding.tvTimer.text = orderProgress.elapsedTime + binding.tvFinishOrder.text = getString(R.string.firmar) + binding.tvFinishOrder.isEnabled = false + + if (orderProgress.signaturePath == null) { + val intent = Intent(this@OrderDetailActivity, SignatureActivity::class.java) + startActivityForResult(intent, SIGNATURE_CODE) + overridePendingTransition( + R.anim.slide_in_rigth_airbnb, + R.anim.scale_out_airbnb + ) + } + + } else if (orderProgress.endDate != null && orderProgress.isOnSurvey) { + val intent = Intent(this@OrderDetailActivity, SurveyActivity::class.java) + intent.putExtra(ORDER_ID, orderProgress.id) + startActivityForResult(intent, SURVEY_CODE) + overridePendingTransition( + R.anim.slide_in_rigth_airbnb, + R.anim.scale_out_airbnb + ) + } + + if (savedInstanceState != null) + orderProgressViewModel.getEvidences(order.id) + + binding.ivEvidenceStart1.setOnClickListener { onClickEvidence(it as ImageView) } + binding.ivEvidenceStart2.setOnClickListener { onClickEvidence(it as ImageView) } + binding.ivEvidenceStart3.setOnClickListener { onClickEvidence(it as ImageView) } + binding.ivEvidenceProcess1.setOnClickListener { onClickEvidence(it as ImageView) } + binding.ivEvidenceProcess2.setOnClickListener { onClickEvidence(it as ImageView) } + binding.ivEvidenceProcess3.setOnClickListener { onClickEvidence(it as ImageView) } + binding.ivEvidenceFinal1.setOnClickListener { onClickEvidence(it as ImageView) } + binding.ivEvidenceFinal2.setOnClickListener { onClickEvidence(it as ImageView) } + binding.ivEvidenceFinal3.setOnClickListener { onClickEvidence(it as ImageView) } + binding.tvFinishOrder.setOnClickListener { onTvFinishOrderClick() } + binding.checkBoxNegativeService.setOnClickListener { onNegativeServiceClick() } + binding.showSketch.setOnClickListener { + if(order.sketchPath == null) + toast(getString(R.string.m_no_pdf_client), Toast.LENGTH_LONG) + else + goToActivity{ + putExtra(Constants.BRANCH_NAME, order.branchName) + putExtra(Constants.SKETCH_PATH, order.sketchPath) + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean("refresh", true) + } + + override fun onStop() { + super.onStop() + orderProgress.comments = binding.editComments.text.toString() + orderProgress.warranty = binding.checkBoxWarranty.isChecked + + if (binding.checkBoxNegativeService.isChecked) + orderProgress.negativeService = id + else + orderProgress.negativeService = 0 + + + if (editLiters!!.text.toString() != "" && editLiters!!.text.toString() != "0") { + try { + orderProgress.liters = + NumberFormat.getNumberInstance().parse(editLiters!!.text.toString()).toString() + .toInt() + } catch (e: ParseException) { + e.printStackTrace() + } + } + + ordersRepository.saveOrderProgress(orderProgress) + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) { + when { + grantResults.isEmpty() -> { + Log.i(TAG, "User interaction was cancelled.") + } + grantResults[0] == PackageManager.PERMISSION_GRANTED -> { + onClickEvidence(actualImageView) + } + else -> { + AlertDialog.Builder(this) + .setTitle(getString(R.string.ad_t_permisos_necesarios)) + .setMessage(getString(R.string.ad_m_permisos_necesarios)) + .setCancelable(false) + .setPositiveButton( + getString(R.string.ad_aceptar) + ) { _, _ -> if (!checkPermissions()) requestPermissions() } + .show() + } + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == RESULT_OK) { + orderProgress = ordersRepository.getOrderProgressById(orderId)!! + orderProgressViewModel.getEvidences(order.id) + if (requestCode == SIGNATURE_CODE) { + orderProgress.signaturePath = data!!.getStringExtra(EVIDENCE_ID) + orderProgress.signatureName = data.getStringExtra(EVIDENCE_NAME) + orderProgress.shouldBeSent = true + orderProgressBox.put(orderProgress) + preferencesHelper.orderInProgress = null + createPDF() + } + if (requestCode == SURVEY_CODE) { + val intent = Intent(this@OrderDetailActivity, SignatureActivity::class.java) + startActivityForResult(intent, SIGNATURE_CODE) + overridePendingTransition( + R.anim.slide_in_rigth_airbnb, + R.anim.scale_out_airbnb + ) + } + } + if (resultCode == RESULT_CANCELED) { + if (requestCode == EVIDENCE_CODE) { + toastLong(getText(R.string.evidence_error)) + } + } + } + + private fun setUpViews() { + try { + val temp: Date = inFormatter.parse(order.dateServiceRequest)!! + binding.tvClientDenomination.text = order.clientDenomination + binding.tvOrderScheduledTime.text = getString( + R.string.scheduled_time, outFormatter.format( + temp + ) + ) + } catch (e: ParseException) { + e.printStackTrace() + binding.tvClientDenomination.text = order.clientContactName + } + + binding.tvOrderName.text = order.serviceName + binding.tvServiceStatus.text = order.serviceStatusName + + if (order.getClientContactCellphone.trim().isNotEmpty()) { + binding.tvContact.text = order.clientContactName + binding.tvContactPhone.text = getString( + R.string.contact_phone, PhoneNumberUtils.formatNumber( + order.getClientContactCellphone, + "MX" + ) + ) + binding.tvContactPhone.visibility = View.VISIBLE + } else { + binding.tvContact.text = order.clientContactName + binding.tvContactPhone.visibility = View.GONE + } + + binding.tvPayMethod.text = order.payMethodName + val drawable = StatusDrawable(order.serviceStatusColor1, order.serviceStatusColor2) + + binding.tvTitle.background = drawable + binding.tvOrderId.text = order.idRequest.toString() + binding.tvCost.text = getString(R.string.cost, order.cost.currencyFormat()) + binding.tvAddress.text = getString( + R.string.address, + order.streetAddress, + order.extNumberAddress, + order.neighborhoodNameAddress, + order.clientPostalCodeAddress + ) + + binding.editLiters.inputType = InputType.TYPE_CLASS_NUMBER + binding.editLiters.addTextChangedListener(CurrencyTextWatcher()) + binding.editLiters.setText(DEFAULT_LITERS) + + if (order.comments != null) { + binding.linearComments.visibility = View.VISIBLE + binding.tvComments.text = order.comments + } else { + binding.linearComments.visibility = View.GONE + } + + if (orderProgress.comments != null) + binding.editComments.setText(orderProgress.comments) + + + if (orderProgress.liters != 0) + binding.editLiters.setText(orderProgress.liters.toString()) + + if (orderProgress.warranty) + binding.checkBoxWarranty.isChecked = true + + if (orderProgress.negativeService > 0) { + binding.checkBoxNegativeService.isChecked = true + id = orderProgress.negativeService + binding.checkBoxNegativeService.text = getReason(id) + } else { + binding.checkBoxNegativeService.setText(R.string.negative) + } + + binding.linearContact.setOnLongClickListener(OnLongClickListener { + if (order.getClientContactCellphone.trim().isNotEmpty()) { + val callIntent = Intent(Intent.ACTION_CALL) + callIntent.data = Uri.parse("tel:" + order.getClientContactCellphone.trim()) + if (ActivityCompat.checkSelfPermission( + this@OrderDetailActivity, + Manifest.permission.CALL_PHONE + ) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + this@OrderDetailActivity, + arrayOf(Manifest.permission.CALL_PHONE), + 3 + ) + return@OnLongClickListener true + } + startActivity(callIntent) + } + + false + }) + } + private fun onClickEvidence(iv: ImageView) { + actualImageView = iv + + if (!checkPermissions()) + requestPermissions() + else { + val evidence = iv.tag as Evidence + if (evidence.path == null) { + //System.gc() + val intent = Intent(this@OrderDetailActivity, CameraActivity::class.java) + intent.putExtra(EVIDENCE_ID, evidence.id) + startActivityForResult(intent, EVIDENCE_CODE) + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + } else { + val intent = Intent(this@OrderDetailActivity, ConfirmationActivity::class.java) + intent.putExtra(EVIDENCE_ID, evidence.id) + startActivity(intent) + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + } + } + } + + private fun attachObservers() { + if (orderProgress.elapsedTime == null) { + orderProgressViewModel.getTimer(orderProgress.startDate)!!.observe(this) { millis -> + if (millis != null) { + binding.tvTimer.text = HelperUtil().formatTime(millis) + elapsedTime = millis + } + } + } + + orderProgressViewModel.onEvidenceRefresh.observe(this) { evidences -> + if (evidences != null) { + try { + for (i in evidences.indices) { + val actualEvidence: Evidence = evidences[i] + when (actualEvidence.type) { + 1 -> { + if (actualEvidence.path != null) { + startImageViews[i].setImageBitmap( + HelperUtil().decodeFromFile( + actualEvidence.path!!, + 5 + ) + ) + } + startImageViews[i].tag = actualEvidence + } + 2 -> { + if (actualEvidence.path != null) { + processImageViews[i - 3].setImageBitmap( + HelperUtil().decodeFromFile( + actualEvidence.path!!, + 5 + ) + ) + } + processImageViews[i - 3].tag = actualEvidence + } + 3 -> { + if (actualEvidence.path != null) { + finalImageViews[i - 6].setImageBitmap( + HelperUtil().decodeFromFile( + actualEvidence.path!!, + 5 + ) + ) + } + finalImageViews[i - 6].tag = actualEvidence + } + } + } + } catch (e: Exception) { + Log.i( + TAG, + "Error al tratar de hacer map de evidencias, error: ${e.message}" + ) + } + } + } + + orderProgressViewModel.onSendSuccess.observe(this) { + startActivity(Intent(this@OrderDetailActivity, OrdersActivity::class.java)) + finish() + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.slide_out_right_airbnb) + } + + orderProgressViewModel.onSendFailure.observe(this) { + startActivity(Intent(this@OrderDetailActivity, OrdersActivity::class.java)) + finish() + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.slide_out_right_airbnb) + } + + orderProgressViewModel.isLoading.observe(this) { s -> + if (s != null) { + binding.frameLoading.visibility = View.VISIBLE + binding.tvLoading.text = s + binding.tvFinishOrder.isEnabled = false + binding.ivEvidenceStart1.isEnabled = false + binding.ivEvidenceStart2.isEnabled = false + binding.ivEvidenceStart3.isEnabled = false + binding.ivEvidenceProcess1.isEnabled = false + binding.ivEvidenceProcess2.isEnabled = false + binding.ivEvidenceProcess3.isEnabled = false + binding.ivEvidenceFinal1.isEnabled = false + binding.ivEvidenceFinal2.isEnabled = false + binding.ivEvidenceFinal3.isEnabled = false + binding.editComments.isEnabled = false + editLiters!!.isEnabled = false + binding.checkBoxNegativeService.isEnabled = false + binding.checkBoxWarranty.isEnabled = false + } else { + binding.frameLoading.visibility = View.GONE + binding.ivEvidenceStart1.isEnabled = true + binding.ivEvidenceStart2.isEnabled = true + binding.ivEvidenceStart3.isEnabled = true + binding.ivEvidenceProcess1.isEnabled = true + binding.ivEvidenceProcess2.isEnabled = true + binding.ivEvidenceProcess3.isEnabled = true + binding.ivEvidenceFinal1.isEnabled = true + binding.ivEvidenceFinal2.isEnabled = true + binding.ivEvidenceFinal3.isEnabled = true + binding.editComments.isEnabled = true + editLiters!!.isEnabled = true + binding.checkBoxNegativeService.isEnabled = true + binding.checkBoxWarranty.isEnabled = true + } + } + orderProgressViewModel.isAllowedToFinishOrder.observe(this) { canFinish -> + if (canFinish != null) { + if (canFinish) { + if (isValidLitters()) { + AlertDialog.Builder(this@OrderDetailActivity) + .setTitle(getString(R.string.ad_t_servicio_exitoso)) + .setMessage(getString(R.string.ad_m_servicio_exitoso)) + .setPositiveButton( + getString(R.string.ad_si) + ) { _, _ -> + + //createZip(ordersRepository.getOrderById(orderProgress.id)!!) + + orderProgress.elapsedTime = HelperUtil().formatTime(elapsedTime) + if (editLiters!!.text.toString() == "") { + orderProgress.liters = 0 + } else { + try { + orderProgress.liters = + NumberFormat.getNumberInstance().parse( + editLiters!!.text.toString() + ).toString().toInt() + } catch (e: ParseException) { + e.printStackTrace() + } + } + orderProgress.comments = binding.editComments.text.toString() + + orderProgress.endLat = DEFAULT_LATITUDE.toString() + orderProgress.endLng = DEFAULT_LONGITUDE.toString() + orderProgress.endDate = inFormatter.format(Date()) + orderProgress.warranty = binding.checkBoxWarranty.isChecked + + if (binding.checkBoxNegativeService.isChecked) + orderProgress.negativeService = id + + if (order.SurveyRequired == 1) { + orderProgress.isOnSurvey = true + ordersRepository.saveOrderProgress(orderProgress) + + val intent = Intent( + this@OrderDetailActivity, + SurveyActivity::class.java + ) + intent.putExtra(ORDER_ID, orderProgress.id) + startActivityForResult( + intent, SURVEY_CODE + ) + overridePendingTransition( + R.anim.slide_in_rigth_airbnb, + R.anim.scale_out_airbnb + ) + + } else { + ordersRepository.saveOrderProgress(orderProgress) + val intent = Intent( + this@OrderDetailActivity, + SignatureActivity::class.java + ) + startActivityForResult( + intent, SIGNATURE_CODE + ) + overridePendingTransition( + R.anim.slide_in_rigth_airbnb, + R.anim.scale_out_airbnb + ) + } + } + .setNegativeButton(getString(R.string.ad_no), null) + .setCancelable(false) + .show() + + } else { + AlertDialog.Builder(this@OrderDetailActivity) + .setTitle(getString(R.string.ad_t_litraje_invalido)) + .setMessage(getString(R.string.ad_m_litraje_invalido)) + .setPositiveButton(getString(R.string.ad_aceptar), null) + .show() + } + + } else { + AlertDialog.Builder(this@OrderDetailActivity) + .setTitle(getString(R.string.ad_t_faltan_evidencias)) + .setMessage(getString(R.string.ad_m_faltan_evidencias_todas)) + .setPositiveButton(getString(R.string.ad_aceptar), null) + .show() + } + orderProgressViewModel.isAllowedToFinishOrder.value = null + } + } + } + + private fun isValidLitters(): Boolean { + return if (editLiters!!.text.toString() == "") { + true + } else { + try { + try { + orderProgress.liters = + NumberFormat.getNumberInstance().parse(editLiters!!.text.toString()) + .toString().toInt() + } catch (e: ParseException) { + e.printStackTrace() + } + true + } catch (e: java.lang.NumberFormatException) { + Log.e(TAG, "Error cachado en conversion litros: " + e.message) + false + } + } + } + + private fun checkPermissions(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + //Android is 11 (R) or above + val cameraPermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + Environment.isExternalStorageManager() && cameraPermission == PackageManager.PERMISSION_GRANTED + } else { + //Below android 11 + PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) && PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } + + private fun requestPermissions() { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + val intent = Intent() + intent.action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION + val uri = Uri.fromParts("package", this.packageName, null) + intent.data = uri + storageActivityResultLauncher.launch(intent) + } catch (e: java.lang.Exception) { + val intent = Intent() + intent.action = Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION + storageActivityResultLauncher.launch(intent) + } + } else { + //Below android 11 + ActivityCompat.requestPermissions( + this@OrderDetailActivity, + arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE), + REQUEST_PERMISSIONS_REQUEST_CODE + ) + } + } + + private val storageActivityResultLauncher = registerForActivityResult(StartActivityForResult() + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + //Android is 11 (R) or above + if (Environment.isExternalStorageManager()) { + //Manage External Storage Permissions Granted + ActivityCompat.requestPermissions( + this@OrderDetailActivity, + arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE), + REQUEST_PERMISSIONS_REQUEST_CODE + ) + + } else { + Toast.makeText( + this@OrderDetailActivity, + "Permiso denegado", + Toast.LENGTH_SHORT + ).show() + } + } else { + //Below android 11 + } + } + + private fun onTvFinishOrderClick() { + if (orderProgress.endDate == null) { + orderProgressViewModel.attemptFinishOrder(orderProgress.id.toString()) + } else { + val intent = Intent(this@OrderDetailActivity, SignatureActivity::class.java) + startActivityForResult(intent, SIGNATURE_CODE) + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + } + } + + private fun onNegativeServiceClick() { + val radioButtonDialog: CustomDialogFragment = CustomDialogFragment(this).messageType( + CustomDialogFragment.MessageViewType.RADIOBUTTON + ) + if (binding.checkBoxNegativeService.isChecked) { + val reasons: List = surveyRepository.getNegativeServiceReason() + val descriptionList: ArrayList = ArrayList() + val idList: ArrayList = ArrayList() + for (i in reasons.indices) { + reasons[i].getTitle() + idList.add(reasons[i].getId()) + descriptionList.add(reasons[i].getTitle()) + } + reason = descriptionList.toTypedArray() + reason = arrayOfNulls(descriptionList.size) + descriptionList.toArray(reason) + radioButtonDialog.showRadioButtonMessage( + getString(R.string.service_title), + descriptionList.toArray(reason), + object : CustomDialogFragment.OnClickActionButton { + override fun onPositiveRadioButtonClicked(selection: Int) { + id = idList[selection].toInt() + binding.checkBoxNegativeService.text = getReason(id) + if (!binding.checkBoxNegativeService.isChecked) { + binding.checkBoxNegativeService.isChecked = true + } + } + + override fun onPositiveButtonClicked() {} + override fun onNegativeButtonClicked() { + if (binding.checkBoxNegativeService.isChecked) { + binding.checkBoxNegativeService.setText(R.string.negative) + binding.checkBoxNegativeService.isChecked = false + } + } + }).show() + } else { + binding.checkBoxNegativeService.setText(R.string.negative) + binding.checkBoxNegativeService.isChecked = false + } + } + + private fun getReason(q: Int): String { + return negativeServiceReasonBox.get(q.toLong()).getTitle() + } + + private fun mapOrderToPDF(): OrderPdf { + // MAPPING INFO + val order = ordersRepository.getOrderById(orderProgress.id)!! + val user = userBox.all[0] + + val auxDate = order.dateServiceRequest.split(' ').first().split('-') + val date = "${auxDate[2]}/${auxDate[1]}/${auxDate[0]}" + + val auxInt = if (order.intNumberAddress != null) " INT. ${order.intNumberAddress}" else "" + val auxCP = if (order.clientPostalCodeAddress != null) " C.P. ${order.clientPostalCodeAddress}" else "" + val address = "CALLE ${order.streetAddress} ${order.extNumberAddress} $auxInt, COL. ${order.neighborhoodNameAddress}$auxCP" + + var observations: String = orderProgress.comments ?: "" + var cost = "0.00" + var negativeCost = "0.00" + + if (orderProgress.negativeService > 0) { + val negativeServiceReason = negativeServiceReasonBox.get(orderProgress.negativeService.toLong()).getDescripcion() + observations = "Servicio negativo por: $negativeServiceReason.
$observations" + negativeCost = order.negativeCost + } else { + cost = order.cost + } + + val warrantyApplied = if (orderProgress.warranty) "Nota: Este servicio cuenta con 1 garantía con 3 meses de vigencia." else "" + + val startTime = orderProgress.startDate.split(' ').last() + val auxElapsedTime = orderProgress.elapsedTime!!.split(':') + val elapsedTime = "${auxElapsedTime[0]}hr ${auxElapsedTime[1]}m ${auxElapsedTime[2]}s" + val finishTime = orderProgress.endDate!!.split(' ').last() + + val advisor = "${user.username} ${user.lastName} ${user.mothersName}" + val auxiliar = if (order.assistant1Name != null) "${order.assistant1Name} ${order.assistant1LastName} ${order.assistant1MothersName}" else "" + + // GETTING SIGN BASE64 + val sign = HelperUtil().encodeBase64WithData(orderProgress.signaturePath!!) ?: "" + + // MAPPING AND GETTING EVIDENCE BASE64 + var evidencesMap: MutableMap> = HashMap() + evidencesMap["inicio"] = ArrayList() + evidencesMap["proceso"] = ArrayList() + evidencesMap["terminado"] = ArrayList() + + orderProgressViewModel.getEvidencesForPdf(order.id).forEach { + val evidenceBase64 = HelperUtil().encodeBase64WithData(it.path!!) ?: "" + + when (it.type) { + 1 -> evidencesMap["inicio"]!!.add(evidenceBase64) + 2 -> evidencesMap["proceso"]!!.add(evidenceBase64) + 3 -> evidencesMap["terminado"]!!.add(evidenceBase64) + } + } + + return OrderPdf( + order.idRequest, + order.clientDenomination, + date, + address, + order.clientCity, + order.getClientContactCellphone, + order.clientContactName, + order.serviceName, + observations, + warrantyApplied, + "$$cost", + "$$negativeCost", + startTime, + elapsedTime, + finishTime, + advisor, + order.operatorOfficeName, + sign, + auxiliar, + order.vehicleCodeName, + evidencesMap + ) + } + + private fun html(orderPdf: OrderPdf): String { + val srcLogo = "file:///android_asset/images/logo_drenax.png" + + return """ + + + + + DRENAX + + + + + +
+
+
+ +
+
+

Una Solución de Limpieza Integral

+

"Creamos ambientes limpios y seguros para el bienestar
de la comunidad"

+
+
+
DRENAX, S.A. DE C.V.
+ CALLE DIEZ #3482 + COL. SAN RAFAEL C.P. 80150 + CULIACÁN, SINALOA + R.F.C DRE0611035M4 + Tels. (667) 713 9250 y 60 + www.drenax.com.mx +
+
+
+ +
+
+ RECIBO DE SERVICIO +
+
+ ${orderPdf.requestId} +
+
+ +
+
+ +
+
+
Nombre
+
${orderPdf.client}
+
+
+
Fecha
+
${orderPdf.date}
+
+
+ +
+
+ Dirección +

${orderPdf.address}

+
+
+ +
+
+ Ciudad + ${orderPdf.city} +
+ +
+ Tel. + ${orderPdf.cellphone} +
+ +
+ Contacto + ${orderPdf.contactName} +
+
+
+
+ +
+
+

CON ESTA FECHA REALIZAMOS EL SERVICIO DE:   ${orderPdf.service}

+
+
+ +
+
+
+ Observaciones +

${orderPdf.observations}

+
+
${orderPdf.warrantyApplied}
+
+
+ +
+
+
+ EVIDENCIAS +
+
+
+ +
+
+
+
+ Inicio +
+ +
+
+ +
+
+ +
+
+ +
+ Proceso +
+ +
+
+ +
+
+ +
+
+ +
+ Terminado +
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ Costo de Servicio + ${orderPdf.cost} +
+
+ Costo Negativo + ${orderPdf.negativeCost} +
+
+ Hora de Inicio + ${orderPdf.startTime} hrs +
+
+ Duración + ${orderPdf.elapsedTime} +
+
+ Hora de Finalización + ${orderPdf.finishTime} hrs +
+
+
+ +
+
+ +
+
+
+ Asesor de Operaciones + ${orderPdf.advisor} +
+
+ Sucursal + ${orderPdf.branchOffice} +
+
+ +
+
+ Auxiliar de Operaciones + ${orderPdf.auxiliar} +
+
+ Vehículo + ${orderPdf.vehicle} +
+
+
+ +
+ Autorizó +
+ +
+
+ +
+
+ + + +""".trimIndent() + } + + private fun getPdfFilePath(): File? { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { + getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + + } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> { + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + } + else -> { + File(Environment.getExternalStorageDirectory().toString() + "/Documents/") + } + } + } + + private fun createPDF() { + val orderPdf = mapOrderToPDF() + val htmlString = html(orderPdf) + //val pdfLocation = File(getPdfFilePath(), "${System.currentTimeMillis()}.pdf") + + val pdfLocation = File(filesDir, "${System.currentTimeMillis()}.pdf") + val htmlToPdfConvertor = CustomHtmlToPdfConvertor(this) + + htmlToPdfConvertor.convert( + pdfLocation = pdfLocation, + htmlString = htmlString, + onPdfGenerationFailed = { exception -> + exception.printStackTrace() + }, + onPdfGenerated = { pdfFile -> + order.pdfPath = pdfFile.path + orderBox.put(order) + HelperUtil().startSync() + startActivity(Intent(this@OrderDetailActivity, OrdersActivity::class.java)) + finish() + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.slide_out_right_airbnb) + }) + } + + class CurrencyTextWatcher : TextWatcher { + private var current = "" + + @Synchronized + override fun afterTextChanged(s: Editable) { + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + if (s.toString() != current) { + editLiters!!.removeTextChangedListener(this) + val cleanString = s.toString().replace("[,]".toRegex(), "") + var parsed = 0 + try { + parsed = cleanString.toInt() + } catch (ignored: NumberFormatException) { + } + val formatted = NumberFormat.getNumberInstance().format(parsed.toLong()) + current = formatted + editLiters!!.setText(formatted) + editLiters!!.setSelection(formatted.length) + editLiters!!.addTextChangedListener(this) + } + } + } + + companion object { + var editLiters: AppCompatEditText? = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/OrderProgressActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/OrderProgressActivity.kt new file mode 100644 index 0000000..4e2524c --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/OrderProgressActivity.kt @@ -0,0 +1,373 @@ +package com.iesoluciones.siodrenax.activities + +import android.Manifest +import android.annotation.SuppressLint +import android.content.DialogInterface +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.* +import android.telephony.PhoneNumberUtils +import android.util.Log +import android.view.MenuItem +import android.view.View +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.AppCompatButton +import androidx.appcompat.widget.Toolbar +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.lifecycle.ViewModelProvider +import com.iesoluciones.mylibrary.activities.ToolbarActivity +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.adapters.OrdersAdapter.Companion.inFormatter +import com.iesoluciones.siodrenax.adapters.OrdersAdapter.Companion.outFormatter +import com.iesoluciones.siodrenax.databinding.ActivityOrderProgressBinding +import com.iesoluciones.siodrenax.entities.Order +import com.iesoluciones.siodrenax.repositories.OrdersRepository +import com.iesoluciones.siodrenax.utils.Constants.Companion.DEFAULT_LATITUDE +import com.iesoluciones.siodrenax.utils.Constants.Companion.DEFAULT_LONGITUDE +import com.iesoluciones.siodrenax.utils.Constants.Companion.ORDER_ID +import com.iesoluciones.siodrenax.viewmodels.OrderProgressViewModel +import java.text.ParseException +import java.util.* +import android.view.WindowManager +import android.widget.Toast +import com.iesoluciones.siodrenax.utils.* +import com.iesoluciones.siodrenax.utils.Constants.Companion.BRANCH_NAME +import com.iesoluciones.siodrenax.utils.Constants.Companion.SKETCH_PATH + +class OrderProgressActivity : ToolbarActivity() { + + private lateinit var binding: ActivityOrderProgressBinding + + //VIEWMODELS + private lateinit var orderProgressViewModel: OrderProgressViewModel + private val ordersRepository: OrdersRepository = OrdersRepository() + private var order: Order? = null + private var street = "" + private var extNumber = "" + private var neighborhoodName = "" + private var postalCode = "" + private val googleLocate = "geo:0,0?q=" + + //BINDINGS + private lateinit var tvClientDemonination: TextView + private lateinit var tvOrderName: TextView + private lateinit var tvPayMethod: TextView + private lateinit var tvContact: TextView + private lateinit var tvAddress: TextView + private lateinit var tvServiceStatus: TextView + private lateinit var tvTitle: TextView + private lateinit var tvCost: TextView + private lateinit var tvOrderId: TextView + private lateinit var tvOrderScheduledTime: TextView + private lateinit var tvContactPhone: TextView + private lateinit var tvLoading: TextView + private lateinit var tvComments: TextView + private lateinit var tvVehicle: TextView + private lateinit var startServiceButton: AppCompatButton + private lateinit var linearContact: LinearLayout + private lateinit var linearComments: LinearLayout + private lateinit var getRoute: LinearLayout + private lateinit var frameLoading: FrameLayout + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityOrderProgressBinding.inflate(layoutInflater) + setContentView(binding.root) + initializeBinding() + + @Suppress("USELESS_CAST") + val toolbar = binding.toolbar as Toolbar + toolbar.title = resources.getString(R.string.t_OrderProgressActivity) + toolbarToLoad(toolbar) + enableHomeDisplay(true) + toolbar.setNavigationOnClickListener { onBackPressed() } + + orderProgressViewModel = ViewModelProvider(this)[OrderProgressViewModel::class.java] + + attachObservers() + + order = ordersRepository.getOrderById(intent.getLongExtra(ORDER_ID, 0L)) + + if(order == null){ + toastLong(resources.getString(R.string.error_order_detail)) + return finish() + } + + val orderProgress = ordersRepository.getOrderProgressById(order!!.id) + + if (orderProgress != null) { + startServiceButton.isEnabled = false + startServiceButton.text = resources.getString(R.string.pendiente_envio) + } + + try { + val temp: Date = inFormatter.parse(order!!.dateServiceRequest)!! + tvClientDemonination.text = order!!.clientDenomination + tvOrderScheduledTime.text = getString( + R.string.scheduled_time, + outFormatter.format(temp) + ) + } catch (e: ParseException) { + e.printStackTrace() + tvClientDemonination.text = order!!.clientContactName + } + + tvOrderName.text = order!!.serviceName + tvVehicle.text = order!!.vehicleCodeName + + tvServiceStatus.text = + if (orderProgress != null) resources.getString(R.string.sincronizando) else order!!.serviceStatusName + + if (order!!.getClientContactCellphone.trim().isNotEmpty()) { + tvContact.text = order!!.clientContactName + tvContactPhone.text = getString( + R.string.contact_phone, PhoneNumberUtils.formatNumber( + order!!.getClientContactCellphone, + "MX" + ) + ) + tvContactPhone.visibility = View.VISIBLE + } else { + tvContact.text = order!!.clientContactName + tvContactPhone.visibility = View.GONE + } + + tvPayMethod.text = order!!.payMethodName + val drawable = StatusDrawable(order!!.serviceStatusColor1, order!!.serviceStatusColor2) + tvTitle.background = drawable + tvOrderId.text = "${order!!.idRequest}" + tvCost.text = getString(R.string.cost, order!!.cost.currencyFormat()) + tvAddress.text = getString( + R.string.address, + order!!.streetAddress, + order!!.extNumberAddress, + order!!.neighborhoodNameAddress, + order!!.clientPostalCodeAddress + ) + + if (order!!.comments != null) { + linearComments.visibility = View.VISIBLE + tvComments.text = order!!.comments + } else linearComments.visibility = View.GONE + + linearContact.setOnLongClickListener { + if (order!!.getClientContactCellphone.trim().isNotEmpty()) { + if (ActivityCompat.checkSelfPermission(this@OrderProgressActivity, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions((this@OrderProgressActivity as AppCompatActivity), arrayOf(Manifest.permission.CALL_PHONE), 3) + return@setOnLongClickListener true + }else{ + val callIntent = Intent(Intent.ACTION_CALL) + callIntent.data = Uri.parse("tel:" + order!!.getClientContactCellphone.trim()) // change the number + vibrate() + startActivity(callIntent) + } + } + false + } + + getRoute.setOnClickListener { + if (order!!.streetAddress != null) { + street = order!!.streetAddress!! + } + + if (order!!.extNumberAddress != null) { + val en: Int + try { + en = order!!.extNumberAddress!!.toInt() + if (en != 0) { + extNumber = en.toString() + } + } catch (e: NumberFormatException) { + Log.e(TAG, "Error cachado en conversion: " + e.message) + } + } + + if (order!!.neighborhoodNameAddress!= null) { + neighborhoodName = order!!.neighborhoodNameAddress!! + } + + if (order!!.clientPostalCodeAddress != null) { + val pc: Int + try { + pc = order!!.clientPostalCodeAddress!!.toInt() + if (pc != 0) { + postalCode = pc.toString() + } + } catch (e: NumberFormatException) { + Log.e(TAG, "Error cachado en conversion: " + e.message) + } + } + + if (street !== "" && extNumber !== "" && neighborhoodName !== "" && postalCode !== "") { + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("$googleLocate$street+$extNumber+$neighborhoodName+$postalCode") + ) + startActivity(intent) + } else { + val addressDialog = + AlertDialog.Builder(this@OrderProgressActivity, R.style.MyAlertDialogStyle) + .setTitle(resources.getString(R.string.ad_t_direccion_servicio)) + .setMessage(resources.getString(R.string.ad_m_direccion_servicio)) + .setCancelable(false) + .setPositiveButton( + resources.getString(R.string.ir_al_mapa) + ) { _, _ -> + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("$googleLocate$street+$extNumber+$neighborhoodName+$postalCode") + ) + startActivity(intent) + } + .setNegativeButton( + resources.getString(R.string.cerrar) + ) { _, _ -> } + val ad = addressDialog.create() + ad.show() + val buttonPositive = ad.getButton(DialogInterface.BUTTON_POSITIVE) + buttonPositive.setTextColor(ContextCompat.getColor(this, R.color.primary)) + } + + } + startServiceButton.setOnClickListener { startService() } + + binding.showSketch.setOnClickListener { + if(order!!.sketchPath == null) + toast(getString(R.string.m_no_pdf_client), Toast.LENGTH_LONG) + else + goToActivity{ + putExtra(BRANCH_NAME, order!!.branchName) + putExtra(SKETCH_PATH, order!!.sketchPath) + } + } + } + + override fun onDestroy() { + super.onDestroy() + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + } + + override fun onBackPressed() { + super.onBackPressed() + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.slide_out_right_airbnb) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + finish() + overridePendingTransition( + R.anim.slide_in_rigth_airbnb, + R.anim.slide_out_right_airbnb + ) + } + } + return super.onOptionsItemSelected(item) + } + + private fun initializeBinding(){ + tvClientDemonination = binding.tvClientDenomination + tvOrderName = binding.tvOrderName + tvPayMethod = binding.tvPayMethod + tvContact = binding.tvContact + tvAddress = binding.tvAddress + tvServiceStatus = binding.tvServiceStatus + tvTitle = binding.tvTitle + tvCost = binding.tvCost + tvOrderId = binding.tvOrderId + tvOrderScheduledTime = binding.tvOrderScheduledTime + tvContactPhone = binding.tvContactPhone + tvLoading = binding.tvLoading + tvComments = binding.tvComments + tvVehicle = binding.tvVehicle + startServiceButton = binding.startServiceButton + linearContact = binding.linearContact + linearComments = binding.linearComments + getRoute = binding.getRoute + frameLoading = binding.frameLoading + } + + private fun startService() { + val alertDialogBuilder: AlertDialog.Builder = AlertDialog.Builder(this) + .setCancelable(false) + .setTitle(R.string.progress_start_dialog_title) + .setMessage(R.string.progress_start_dialog_message) + .setPositiveButton(R.string.accept) { _, _ -> + toastLong(getString(R.string.processing_request)) + orderProgressViewModel.startOrder( + order!!.id.toString(), + order!!.idRequest.toString(), + inFormatter.format(Date()), + DEFAULT_LATITUDE.toString(), + DEFAULT_LONGITUDE.toString() + ) + } + .setNegativeButton(R.string.cancel) { dialogInterface, _ -> dialogInterface.dismiss() } + + val alertDialog: AlertDialog = alertDialogBuilder.create() + + alertDialog.window!!.setFlags( + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + ) + alertDialog.window!!.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_FULLSCREEN or + View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + alertDialog.show() + alertDialog.window!!.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) + } + + private fun attachObservers(){ + orderProgressViewModel.onSendSuccess.observe(this) { responseBody -> + if (responseBody != null) { + + if (order!!.SurveyRequired == 1) + ordersRepository.createSurveyAnswerHolders(order!!) + + val intent = Intent(this@OrderProgressActivity, OrderDetailActivity::class.java) + intent.putExtra(ORDER_ID, order!!.id) + intent.flags = (Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + orderProgressViewModel.onSendSuccess.value = null + startActivity(intent) + finish() + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + } + } + + orderProgressViewModel.onSendFailure.observe(this) { throwable -> + if (throwable != null) { + HelperUtil().parseError(this@OrderProgressActivity, throwable) + orderProgressViewModel.onSendFailure.value = null + } + } + + orderProgressViewModel.isLoading.observe(this) { s -> + if (s != null) { + frameLoading.visibility = View.VISIBLE + tvLoading.text = s + } else { + frameLoading.visibility = View.GONE + } + } + } + + @SuppressLint("MissingPermission") + private fun vibrate() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = this.getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibratorManager.defaultVibrator.vibrate(VibrationEffect.createOneShot(150, VibrationEffect.DEFAULT_AMPLITUDE)) + }else{ + (getSystemService(VIBRATOR_SERVICE) as Vibrator).vibrate(VibrationEffect.createOneShot(150, VibrationEffect.DEFAULT_AMPLITUDE)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/OrdersActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/OrdersActivity.kt new file mode 100644 index 0000000..36abbfe --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/OrdersActivity.kt @@ -0,0 +1,379 @@ +package com.iesoluciones.siodrenax.activities + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.FrameLayout +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import com.iesoluciones.mylibrary.activities.ToolbarActivity +import com.iesoluciones.siodrenax.* +import com.iesoluciones.siodrenax.adapters.OrdersAdapter +import com.iesoluciones.siodrenax.databinding.ActivityOrdersBinding +import com.iesoluciones.siodrenax.entities.CheckListQuestion +import com.iesoluciones.siodrenax.entities.Vehicle +import com.iesoluciones.siodrenax.interfaces.OnIncidentListener +import com.iesoluciones.siodrenax.interfaces.OnMileageListener +import com.iesoluciones.siodrenax.receivers.UpdateUIReceiver +import com.iesoluciones.siodrenax.receivers.UpdateUIReceiver.UiUpdateListener +import com.iesoluciones.siodrenax.utils.Constants +import com.iesoluciones.siodrenax.utils.Constants.Companion.DEFAULT_LATITUDE +import com.iesoluciones.siodrenax.utils.Constants.Companion.DEFAULT_LONGITUDE +import com.iesoluciones.siodrenax.utils.Constants.Companion.MILEAGE +import com.iesoluciones.siodrenax.utils.HelperUtil +import com.iesoluciones.siodrenax.utils.ListPaddingDecoration +import com.iesoluciones.siodrenax.viewmodels.IncidenceViewModel +import com.iesoluciones.siodrenax.viewmodels.OrdersViewModel +import com.iesoluciones.siodrenax.viewmodels.WorkdayStatusViewModel +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.observers.ResourceObserver +import io.reactivex.schedulers.Schedulers +import java.io.* + +class OrdersActivity : ToolbarActivity(), OnMileageListener, OnIncidentListener, OnRefreshListener, UiUpdateListener { + + private lateinit var binding: ActivityOrdersBinding + private var localMileage: String? = null + + private lateinit var workdayStatusViewModel: WorkdayStatusViewModel + private lateinit var incidenceViewModel: IncidenceViewModel + private lateinit var ordersViewModel: OrdersViewModel + + private var adapter: OrdersAdapter? = null + + private lateinit var recyclerView: RecyclerView + private lateinit var swipeRefresh: SwipeRefreshLayout + private lateinit var frameHidden: FrameLayout + private lateinit var frameLoading: FrameLayout + private lateinit var tvLoading: TextView + private lateinit var tvEmpty: TextView + + private var changeVehicleAux = false + private var isWaitingForExitAux = false + + private lateinit var updateUIReceiver: UpdateUIReceiver + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Si se abre por medio de una notificación y hay una orden en progreso, entonces se cierra + if(intent.getBooleanExtra(Constants.EXTRA_NOTIFICATION_CLICKED, false) && preferencesHelper.orderInProgress != 0L) + return finish() + + if (savedInstanceState != null) { + localMileage = savedInstanceState.getString(MILEAGE) + } + + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + + binding = ActivityOrdersBinding.inflate(layoutInflater) + setContentView(binding.root) + + @Suppress("USELESS_CAST") + val toolbar = binding.toolbar as Toolbar + toolbar.title = getString(R.string.t_OrdersActivity) + toolbarToLoad(toolbar) + + workdayStatusViewModel = ViewModelProvider(this)[WorkdayStatusViewModel::class.java] + incidenceViewModel = ViewModelProvider(this)[IncidenceViewModel::class.java] + ordersViewModel = ViewModelProvider(this)[OrdersViewModel::class.java] + + //Bindings + recyclerView = binding.recycler + swipeRefresh = binding.swipeRefresh + frameHidden = binding.frameHidden + frameLoading = binding.frameLoading + tvLoading = binding.tvLoading + tvEmpty = binding.tvEmpty + + attachToObservables() + + swipeRefresh.setOnRefreshListener { + onRefresh() + } + } + + + override fun onRefresh() { + ordersViewModel.isLoading.value = getString(R.string.loading) + ordersViewModel.refreshOrders(filesDir.absolutePath) + } + + override fun onMileageInput(mileage: String?) { + if (mileage != null) { + localMileage = mileage + HelperUtil().incidentDialogFragment(supportFragmentManager, this) + } + } + + override fun onNoIncident() { + logout() + } + + override fun onIncidentInput(incident: String?) { + if (incident != null) { + incidenceViewModel.sendVehicleIncidence(incident) + } + } + + override fun updateUI() { + ordersViewModel.updateOrdersFromDB() + } + + override fun onStart() { + super.onStart() + updateUIReceiver = UpdateUIReceiver() + updateUIReceiver.updateUIReceiver(this) + } + + override fun onResume() { + super.onResume() + updateUIReceiver.registerReceiver(this) + } + + + override fun onStop() { + super.onStop() + updateUIReceiver.unRegisterReceiver(this) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.orders, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.log_out -> workdayStatusViewModel.validateLogOut() + R.id.sync -> HelperUtil().startSync() + R.id.change_vehicle -> workdayStatusViewModel.validateChangeVehicle() + R.id.next_day_service -> { + val nextService = Intent(this, NextServiceActivity::class.java) + startActivity(nextService) + } + } + return super.onOptionsItemSelected(item) + } + + private fun attachToObservables() { + + workdayStatusViewModel.isLoading.observe(this) { s -> + if (s != null) { + // This is a hidden frame over the activity used to not permit click the screen + // this avoids to click the Drenax services when screen is loading + frameHidden.visibility = View.VISIBLE + frameLoading.visibility = View.VISIBLE + tvLoading.text = s + } else { + frameLoading.visibility = View.GONE + Handler(Looper.getMainLooper()).postDelayed( + { frameHidden.visibility = View.GONE }, + 500 + ) + } + } + + workdayStatusViewModel.endWorkdaySuccess.observe(this) { + workdayStatusViewModel.logout() + } + + workdayStatusViewModel.endWorkdayFailure.observe(this) { throwable -> + if (throwable != null) { + HelperUtil().parseError(this@OrdersActivity, throwable) + workdayStatusViewModel.endWorkdayFailure.value = null + } + } + + ordersViewModel.getOrders().observe(this) { orders -> + if (orders.isNotEmpty()) { + if (adapter == null) { + + tvEmpty.visibility = View.GONE + recyclerView.visibility = View.VISIBLE + recyclerView.addItemDecoration(ListPaddingDecoration(this@OrdersActivity)) + adapter = OrdersAdapter(this@OrdersActivity, orders) + val linearLayoutManager = LinearLayoutManager(this@OrdersActivity) + recyclerView.adapter = adapter + recyclerView.layoutManager = linearLayoutManager + adapter!!.notifyDataSetChanged() + preferencesHelper.isEnableToSync = true + } else { + tvEmpty.visibility = View.GONE + recyclerView.visibility = View.VISIBLE + adapter!!.orders = orders + adapter!!.notifyDataSetChanged() + preferencesHelper.isEnableToSync = true + } + } else { + recyclerView.visibility = View.GONE + tvEmpty.visibility = View.VISIBLE + } + + if (swipeRefresh.isRefreshing) swipeRefresh.isRefreshing = false + } + + ordersViewModel.orderRefreshFailure.observe(this) { throwable -> + if (throwable != null) { + HelperUtil().parseError(this@OrdersActivity, throwable) + ordersViewModel.orderRefreshFailure.postValue(null) + } + if (swipeRefresh.isRefreshing) swipeRefresh.isRefreshing = false + } + + workdayStatusViewModel.logOutSuccessObservable.observe(this) { logoutSuccess -> + if (logoutSuccess) { + startActivity(Intent(this@OrdersActivity, LoginActivity::class.java)) + finish() + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + } + } + + workdayStatusViewModel.isAllowedToLogOut.observe(this) { isAllowed -> + if (isAllowed != null) { + if (isAllowed) { + showLogOutDialog(this@OrdersActivity) + } else { + showCantLogOutDialog(this@OrdersActivity) + } + workdayStatusViewModel.isAllowedToLogOut.value = null + } + } + + workdayStatusViewModel.isAllowedToChangeVehicle.observe(this) { isAllowed -> + if (isAllowed != null) { + if (isAllowed) { + showChangeVehicleConfirmation(this@OrdersActivity) + } else { + showCantChangeVehicleDialog(this@OrdersActivity) + } + workdayStatusViewModel.isAllowedToChangeVehicle.value = null + } + } + + workdayStatusViewModel.changeVehicleSuccess.observe(this) { responseBody -> + if (responseBody != null) { + val obCheckList: Observable> = api.getCheckList(1) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .map { checkListQuestions -> + checkListQuestionBox.put(checkListQuestions) + checkListQuestions + } + val obVehicle: Observable> = api.getVehicle() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .map { vehicle -> + vehicleBox.put(vehicle) + vehicle + } + + Observable.concat(obCheckList, obVehicle).subscribe(object : + ResourceObserver() { + override fun onNext(token: Any) {} + override fun onError(e: Throwable) {} + override fun onComplete() { + val intent = Intent( + this@OrdersActivity, + if (checkListQuestionBox.query().build().find().isNotEmpty()) + RevisionSurveyActivity::class.java else WorkdayActivity::class.java + ) + startActivity(intent) + finish() + } + }) + } + } + + incidenceViewModel.incidenceSuccess.observe(this) { response -> + if (response != null) { + logout() + } + } + } + + private fun logout() { + // These validates the kind of logout with auxiliary flags + if (isWaitingForExitAux) { + workdayStatusViewModel.endWorkday( + localMileage!!, + DEFAULT_LATITUDE.toString(), + DEFAULT_LONGITUDE.toString() + ) + } else if (changeVehicleAux) { + workdayStatusViewModel.changeVehicle( + localMileage!!, + DEFAULT_LATITUDE.toString(), + DEFAULT_LONGITUDE.toString() + ) + } + + workdayStatusViewModel.isLoading.value = getString(R.string.loading_wait_a_moment) + } + + private fun showLogOutDialog(context: Context?) { + AlertDialog.Builder(context!!) + .setTitle(R.string.logout_dialog_title) + .setMessage(R.string.logout_dialog_message) + .setPositiveButton(R.string.accept) { _, _ -> + isWaitingForExitAux = true + HelperUtil().mileageDialogFragment(supportFragmentManager, this) + } + .setNegativeButton(R.string.cancel) { dialogInterface, _ -> dialogInterface.dismiss() } + .setCancelable(false) + .show() + } + + private fun showCantLogOutDialog(context: Context?) { + AlertDialog.Builder(context!!) + .setTitle(R.string.cant_logout_dialog_title) + .setMessage(R.string.cant_logout_dialog_message) + .setPositiveButton(R.string.accept, null) + .setCancelable(false) + .show() + } + + private fun showChangeVehicleConfirmation(context: Context?) { + AlertDialog.Builder(context!!) + .setTitle(getString(R.string.ad_t_cambiar_vehiculo)) + .setMessage(getString(R.string.ad_m_cambiar_vehiculo)) + .setPositiveButton(getString(R.string.ad_aceptar)) { _, _ -> + changeVehicleAux = true + HelperUtil().mileageDialogFragment(supportFragmentManager, this@OrdersActivity) + } + .setNegativeButton(R.string.ad_cancelar) { dialogInterface, _ -> dialogInterface.dismiss() } + .setCancelable(false) + .show() + } + + private fun showCantChangeVehicleDialog(context: Context?) { + AlertDialog.Builder(context!!) + .setTitle(R.string.cant_logout_dialog_title) + .setMessage(R.string.cant_change_vehicle_dialog_message) + .setPositiveButton(R.string.accept) { dialogInterface, _ -> dialogInterface.dismiss() } + .setCancelable(false) + .show() + } + + /*private fun showAutoCloseDialog(context: Context?) { + AlertDialog.Builder(context!!) + .setMessage(R.string.auto_close) + .setPositiveButton(R.string.accept) { _, _ -> + workdayStatusViewModel.logout() + } + .setCancelable(false) + .show() + }*/ +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/OrdersManagerActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/OrdersManagerActivity.kt new file mode 100644 index 0000000..ea9a257 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/OrdersManagerActivity.kt @@ -0,0 +1,104 @@ +package com.iesoluciones.siodrenax.activities + +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import com.iesoluciones.mylibrary.activities.ToolbarActivity +import com.iesoluciones.siodrenax.adapters.OrdersManagerAdapter +import com.iesoluciones.siodrenax.databinding.ActivityOrdersManagerBinding +import com.iesoluciones.siodrenax.entities.Order +import com.iesoluciones.siodrenax.utils.Constants.Companion.OPERATOR_ID +import com.iesoluciones.siodrenax.utils.Constants.Companion.OPERATOR_NAME +import com.iesoluciones.siodrenax.utils.HelperUtil +import com.iesoluciones.siodrenax.utils.ListPaddingDecoration +import com.iesoluciones.siodrenax.viewmodels.ManagerViewModel + +class OrdersManagerActivity : ToolbarActivity(), OnRefreshListener { + + private lateinit var binding: ActivityOrdersManagerBinding + private lateinit var managerViewModel: ManagerViewModel + var adapter: OrdersManagerAdapter? = null + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityOrdersManagerBinding.inflate(layoutInflater) + setContentView(binding.root) + + @Suppress("USELESS_CAST") + val toolbar = binding.toolbar as Toolbar + toolbar.title = intent.getStringExtra(OPERATOR_NAME) + toolbarToLoad(toolbar) + + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + managerViewModel = ViewModelProvider(this)[ManagerViewModel::class.java] + binding.swipeRefresh.setOnRefreshListener(this) + attachObservers() + + managerViewModel.getOperatorOrders( + intent.getLongExtra(OPERATOR_ID, 0L).toString() + "" + ) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + finish() + } + } + return super.onOptionsItemSelected(item) + } + + private fun attachObservers(){ + managerViewModel.onOrdersRetrieveSuccess.observe(this + ) { orders: List -> + if (orders.isNotEmpty()) { + if (adapter == null) { + binding.tvEmpty.visibility = View.GONE + binding.recycler.visibility = View.VISIBLE + binding.recycler.addItemDecoration(ListPaddingDecoration(this@OrdersManagerActivity)) + adapter = OrdersManagerAdapter(this@OrdersManagerActivity, orders) + val linearLayoutManager = LinearLayoutManager(this@OrdersManagerActivity) + binding.recycler.adapter = adapter + binding.recycler.layoutManager = linearLayoutManager + adapter!!.notifyDataSetChanged() + } else { + binding.tvEmpty.visibility = View.GONE + binding.recycler.visibility = View.VISIBLE + adapter!!.orders = orders + adapter!!.notifyDataSetChanged() + } + } else { + binding.recycler.visibility = View.GONE + binding.tvEmpty.visibility = View.VISIBLE + } + if (binding.swipeRefresh.isRefreshing) binding.swipeRefresh.isRefreshing = false + } + + managerViewModel.onCallFailure.observe(this) { throwable: Throwable? -> + if (throwable != null) HelperUtil().parseError(this@OrdersManagerActivity, throwable) + if (binding.swipeRefresh.isRefreshing) binding.swipeRefresh.isRefreshing = false + } + + managerViewModel.isLoading.observe(this + ) { s: String? -> + if (s != null) { + binding.frameLoading.visibility = View.VISIBLE + binding.tvLoading.text = s + } else { + binding.frameLoading.visibility = View.GONE + } + } + } + + override fun onRefresh() { + managerViewModel.getOperatorOrders( + intent.getLongExtra(OPERATOR_ID, 0L).toString() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/PdfViewerActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/PdfViewerActivity.kt new file mode 100644 index 0000000..b5e3ef2 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/PdfViewerActivity.kt @@ -0,0 +1,110 @@ +package com.iesoluciones.siodrenax.activities + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.pdf.PdfRenderer +import android.os.Bundle +import android.os.ParcelFileDescriptor +import android.view.View +import androidx.appcompat.widget.Toolbar +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.iesoluciones.mylibrary.activities.ToolbarActivity +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.adapters.PdfViewerAdapter +import com.iesoluciones.siodrenax.databinding.ActivityPdfViewerBinding +import com.iesoluciones.siodrenax.utils.Constants.Companion.BRANCH_NAME +import com.iesoluciones.siodrenax.utils.Constants.Companion.SKETCH_PATH +import java.io.File +import java.io.IOException + +class PdfViewerActivity : ToolbarActivity() { + + private lateinit var binding: ActivityPdfViewerBinding + private lateinit var recyclerView: RecyclerView + private var adapter: PdfViewerAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityPdfViewerBinding.inflate(layoutInflater) + setContentView(binding.root) + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + + @Suppress("USELESS_CAST") + val toolbar = binding.toolbar as Toolbar + toolbar.title = intent.getStringExtra(BRANCH_NAME) + toolbarToLoad(toolbar) + enableHomeDisplay(true) + recyclerView = binding.recycler + + + val file = File(intent.getStringExtra(SKETCH_PATH)!!) + + try { + binding.tvEmpty.visibility = View.GONE; + openPDF(file) + } catch (ioe: IOException) { + binding.tvEmpty.visibility = View.VISIBLE; + ioe.printStackTrace() + } + + toolbar.setNavigationOnClickListener { + finish() + } + } + + @Throws(IOException::class) + fun openPDF(file: File?) { + var fileDescriptor: ParcelFileDescriptor? = null + fileDescriptor = ParcelFileDescriptor.open( + file, ParcelFileDescriptor.MODE_READ_ONLY + ) + + var pdfRenderer: PdfRenderer? = null + pdfRenderer = PdfRenderer(fileDescriptor) + val pageCount: Int = pdfRenderer.pageCount + + val pages : MutableList = ArrayList() + + for (i in 0 until pageCount){ + val rendererPage: PdfRenderer.Page = pdfRenderer.openPage(i) + val rendererPageWidth: Int = rendererPage.width + val rendererPageHeight: Int = rendererPage.height + val bitmap: Bitmap = Bitmap.createBitmap( + rendererPageWidth, + rendererPageHeight, + Bitmap.Config.ARGB_8888 + ) + + val canvas = Canvas(bitmap) + canvas.drawColor(Color.WHITE) + canvas.drawBitmap(bitmap, 0f, 0f, null) + + rendererPage.render( + bitmap, null, null, + PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY + ) + + pages.add(bitmap) + rendererPage.close() + } + + pdfRenderer.close() + fileDescriptor.close() + + if(adapter == null){ + recyclerView.visibility = View.VISIBLE + adapter = PdfViewerAdapter(this@PdfViewerActivity, pages) + val linearLayoutManager = LinearLayoutManager(this@PdfViewerActivity) + recyclerView.adapter = adapter + recyclerView.layoutManager = linearLayoutManager + adapter!!.notifyDataSetChanged() + }else{ + recyclerView.visibility = View.VISIBLE + adapter!!.pages = pages + adapter!!.notifyDataSetChanged() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/RevisionSurveyActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/RevisionSurveyActivity.kt new file mode 100644 index 0000000..43309cf --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/RevisionSurveyActivity.kt @@ -0,0 +1,155 @@ +package com.iesoluciones.siodrenax.activities + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.iesoluciones.mylibrary.activities.ToolbarActivity +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.adapters.RevisionSurveyAdapter +import com.iesoluciones.siodrenax.databinding.ActivityRevisionSurveyBinding +import com.iesoluciones.siodrenax.interfaces.OnIncidentShowListener +import com.iesoluciones.siodrenax.preferencesHelper +import com.iesoluciones.siodrenax.repositories.CheckListRepository +import com.iesoluciones.siodrenax.utils.Constants +import com.iesoluciones.siodrenax.utils.HelperUtil +import com.iesoluciones.siodrenax.utils.ListPaddingDecoration +import com.iesoluciones.siodrenax.utils.toast +import com.iesoluciones.siodrenax.vehicleBox +import com.iesoluciones.siodrenax.viewmodels.IncidenceViewModel + +class RevisionSurveyActivity : ToolbarActivity(), OnIncidentShowListener { + + private lateinit var binding: ActivityRevisionSurveyBinding + private lateinit var recyclerView: RecyclerView + private lateinit var adapter: RevisionSurveyAdapter + private lateinit var incidenceViewModel: IncidenceViewModel + private var incidenceId: Int? = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityRevisionSurveyBinding.inflate(layoutInflater) + setContentView(binding.root) + + @Suppress("USELESS_CAST") + val toolbar = binding.toolbar as Toolbar + toolbar.title = getString(R.string.revision) + toolbarToLoad(toolbar) + + preferencesHelper.checkListProgress = Constants.REVISION + binding.btnSiguiente.text = getString(R.string.siguiente) + + incidenceViewModel = ViewModelProvider(this)[IncidenceViewModel::class.java] + attachObservers() + + val checkListQuestion = CheckListRepository().getSurveyCheckList(Constants.REVISION) + + val vehicle: ArrayList = ArrayList() + val vehicleIds: ArrayList = ArrayList() + vehicle.add(getString(R.string.seleccionaVehiculo)) + vehicleIds.add(0) + + for (v in vehicleBox.all){ + vehicle.add(v.nombre) + vehicleIds.add(v.id) + } + + recyclerView = binding.recycler + recyclerView.visibility = View.VISIBLE + recyclerView.addItemDecoration(ListPaddingDecoration(this@RevisionSurveyActivity)) + + adapter = RevisionSurveyAdapter( + binding.btnSiguiente, + checkListQuestion, + vehicle, + vehicleIds + ) + + recyclerView.adapter = adapter + recyclerView.layoutManager = LinearLayoutManager(this) + + binding.btnSiguiente.setOnClickListener { + recyclerView.clearFocus() + onClickBtnSiguiente() + } + } + + private fun onClickBtnSiguiente() { + incidenceViewModel.getVehicleIncidence(RevisionSurveyAdapter.vehicleSelectedId) + } + + private fun attachObservers() { + incidenceViewModel.isLoading.observe(this) { s -> + if (s != null) { + binding.btnSiguiente.isEnabled = false + binding.frameLoading.visibility = View.VISIBLE + binding.tvLoading.text = s + } else { + binding.btnSiguiente.isEnabled = true + binding.frameLoading.visibility = View.GONE + } + } + + incidenceViewModel.incidenceSuccess.observe(this) { response -> + if (response != null) { + toast(getString(R.string.toast_incidence_solved)) + alertDialog() + } + } + + incidenceViewModel.incidenceFailure.observe(this) { throwable -> + if (throwable != null) { + HelperUtil().parseError(this, throwable) + incidenceViewModel.incidenceFailure.postValue(null) + } + } + + incidenceViewModel.getIncidenceSuccess.observe(this) { vehicleIncidenceResponse -> + if (vehicleIncidenceResponse != null && vehicleIncidenceResponse.id > 0) { + incidenceId = vehicleIncidenceResponse.id + val incidence: String = vehicleIncidenceResponse.description + HelperUtil().incidentShowDialogFragment(supportFragmentManager, this, incidence) + } else { + alertDialog() + } + } + + incidenceViewModel.getIncidenceFailure.observe(this) { throwable -> + if (throwable != null) { + HelperUtil().parseError(this, throwable) + incidenceViewModel.getIncidenceFailure.postValue(null) + } + } + } + + private fun alertDialog() { + AlertDialog.Builder(this@RevisionSurveyActivity) + .setTitle(R.string.titleDialogEncuestaOperador) + .setMessage(R.string.messageDialogEncuestaOperador) + .setPositiveButton(R.string.accept) { _, _ -> + preferencesHelper.checkListProgress = Constants.MATERIAL + val intent = Intent(this@RevisionSurveyActivity, MaterialSurveyActivity::class.java) + startActivity(intent) + finish() + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + override fun onIncidenceSolved() { + incidenceViewModel.resolveVehicleIncidence(incidenceId.toString()) + } + + override fun onIncidenceNotSolved() { + alertDialog() + } + + override fun onStop() { + super.onStop() + adapter.checkListQuestionBoxObserve!!.cancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/SignatureActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/SignatureActivity.kt new file mode 100644 index 0000000..d54528d --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/SignatureActivity.kt @@ -0,0 +1,122 @@ +package com.iesoluciones.siodrenax.activities + +import android.content.Intent +import android.os.Bundle +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.AppCompatButton +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.lifecycle.ViewModelProvider +import com.github.gcacace.signaturepad.views.SignaturePad +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.databinding.ActivitySignatureBinding +import com.iesoluciones.siodrenax.models.EvidenceSignatureLocal +import com.iesoluciones.siodrenax.utils.Constants.Companion.ANIMATE_FADE_IN_ALPHA +import com.iesoluciones.siodrenax.utils.Constants.Companion.ANIMATE_FADE_IN_DURATION +import com.iesoluciones.siodrenax.utils.Constants.Companion.ANIMATE_FADE_OUT_ALPHA +import com.iesoluciones.siodrenax.utils.Constants.Companion.ANIMATE_FADE_OUT_DURATION +import com.iesoluciones.siodrenax.utils.Constants.Companion.EVIDENCE_ID +import com.iesoluciones.siodrenax.utils.Constants.Companion.EVIDENCE_NAME +import com.iesoluciones.siodrenax.utils.snackBar +import com.iesoluciones.siodrenax.utils.toast +import com.iesoluciones.siodrenax.viewmodels.EvidenceViewModel + +class SignatureActivity : AppCompatActivity(), SignaturePad.OnSignedListener { + + private var isAtLeastSigned = false + + private lateinit var binding: ActivitySignatureBinding + private lateinit var evidenceViewModel: EvidenceViewModel + + private lateinit var coordinatorSignature: CoordinatorLayout + private lateinit var signaturePad: SignaturePad + private lateinit var tvTitle: TextView + private lateinit var buttonClear: AppCompatButton + private lateinit var buttonPrint: AppCompatButton + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + binding = ActivitySignatureBinding.inflate(layoutInflater) + setContentView(binding.root) + viewBinding() + + signaturePad.setOnSignedListener(this) + evidenceViewModel = ViewModelProvider(this)[EvidenceViewModel::class.java] + attachObservers() + + buttonClear.setOnClickListener { onClickIvClear() } + buttonPrint.setOnClickListener { onClickFabCheck() } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.clear() + } + + override fun onStartSigning() { + animateFadeOut() + } + + override fun onSigned() { + isAtLeastSigned = true + animateFadeIn() + } + + override fun onClear() {} + + override fun onBackPressed() {} + + private fun animateFadeOut() { + tvTitle.animate().alpha(ANIMATE_FADE_OUT_ALPHA) + .setDuration(ANIMATE_FADE_OUT_DURATION).start() + buttonClear.animate().alpha(ANIMATE_FADE_OUT_ALPHA) + .setDuration(ANIMATE_FADE_OUT_DURATION).start() + buttonPrint.animate().alpha(ANIMATE_FADE_OUT_ALPHA) + .setDuration(ANIMATE_FADE_OUT_DURATION).start() + } + + private fun animateFadeIn() { + tvTitle.animate().alpha(ANIMATE_FADE_IN_ALPHA) + .setDuration(ANIMATE_FADE_IN_DURATION).start() + buttonPrint.animate().alpha(ANIMATE_FADE_IN_ALPHA) + .setDuration(ANIMATE_FADE_IN_DURATION).start() + buttonClear.animate().alpha(ANIMATE_FADE_IN_ALPHA) + .setDuration(ANIMATE_FADE_IN_DURATION).start() + } + + private fun onClickIvClear() { + signaturePad.clear() + isAtLeastSigned = false + } + + private fun onClickFabCheck() { + if (isAtLeastSigned) + evidenceViewModel.saveImage(filesDir, signaturePad.signatureBitmap) + else + snackBar(getString(R.string.s_dibuje_firma)) + } + + private fun attachObservers() { + evidenceViewModel.onImageSaved.observe(this) { evidenceSignatureLocal: EvidenceSignatureLocal -> + if (evidenceSignatureLocal.path != null && evidenceSignatureLocal.name != null) { + val intent = Intent() + intent.putExtra(EVIDENCE_ID, evidenceSignatureLocal.path) + intent.putExtra(EVIDENCE_NAME, evidenceSignatureLocal.name) + setResult(RESULT_OK, intent) + finish() + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + } else { + toast(getString(R.string.toast_ocurrio_problema)) + } + } + } + + private fun viewBinding() { + coordinatorSignature = binding.coordinatorSignature + signaturePad = binding.signaturePad + tvTitle = binding.tvTitle + buttonClear = binding.buttonClear + buttonPrint = binding.buttonPrint + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/SplashActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/SplashActivity.kt new file mode 100644 index 0000000..bdd3bc3 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/SplashActivity.kt @@ -0,0 +1,104 @@ +package com.iesoluciones.siodrenax.activities + +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.os.Bundle +import android.view.View.SYSTEM_UI_FLAG_FULLSCREEN +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import com.iesoluciones.siodrenax.App +import com.iesoluciones.siodrenax.databinding.ActivitySplashBinding +import com.iesoluciones.siodrenax.preferencesHelper +import com.iesoluciones.siodrenax.repositories.CheckListRepository +import com.iesoluciones.siodrenax.utils.Constants +import com.iesoluciones.siodrenax.utils.Constants.Companion.LOGGED_IN +import com.iesoluciones.siodrenax.utils.Constants.Companion.LOGGED_IN_MANAGER +import com.iesoluciones.siodrenax.utils.Constants.Companion.NO_SESSION +import com.iesoluciones.siodrenax.utils.Constants.Companion.ORDER_ID +import com.iesoluciones.siodrenax.utils.Constants.Companion.ORDER_IN_PROGRESS +import com.iesoluciones.siodrenax.utils.Constants.Companion.WORKDAY_STARTED +import com.iesoluciones.siodrenax.utils.Constants.Companion.WORKDAY_STARTED_MANAGER +import com.iesoluciones.siodrenax.utils.goToActivity +import com.iesoluciones.siodrenax.viewmodels.SplashViewModel + +class SplashActivity : AppCompatActivity() { + + private lateinit var binding: ActivitySplashBinding + private var splashViewModel: SplashViewModel? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySplashBinding.inflate(layoutInflater) + setContentView(binding.root) + + @Suppress("DEPRECATION") + window.decorView.systemUiVisibility = (SYSTEM_UI_FLAG_FULLSCREEN) + + splashViewModel = ViewModelProvider(this)[SplashViewModel::class.java] + attachObservers() + + splashViewModel!!.getStartSplashObservable() + } + + private fun attachObservers() { + splashViewModel!!.sessionStatusObservable.observe(this) { status -> + processStatus(status!!) + } + } + + private fun processStatus(status: Int) { + when (status) { + LOGGED_IN -> { + + var progressActivity: Class<*> = WorkdayActivity::class.java + if (CheckListRepository().isCheckListDone()) { + when (App.prefs!!.checkListProgress) { + Constants.REVISION -> progressActivity = RevisionSurveyActivity::class.java + Constants.MATERIAL -> progressActivity = MaterialSurveyActivity::class.java + Constants.HERRAMIENTA -> progressActivity = + HerramientaSurveyActivity::class.java + Constants.WORKDAY -> progressActivity = WorkdayActivity::class.java + else -> { + } + } + } + val intent = Intent(this@SplashActivity, progressActivity) + startActivity(intent) + finish() + } + LOGGED_IN_MANAGER -> { + goToActivity { + flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK + } + finish() + } + WORKDAY_STARTED -> { + goToActivity { + flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK + } + finish() + } + WORKDAY_STARTED_MANAGER -> { + goToActivity { + flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK + } + finish() + } + NO_SESSION -> { + goToActivity { + flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK + } + finish() + } + ORDER_IN_PROGRESS -> { + goToActivity { + flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK + putExtra(ORDER_ID, preferencesHelper.orderInProgress) + } + finish() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/SurveyActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/SurveyActivity.kt new file mode 100644 index 0000000..d4722db --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/SurveyActivity.kt @@ -0,0 +1,162 @@ +package com.iesoluciones.siodrenax.activities + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.ScrollView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.AppCompatButton +import androidx.appcompat.widget.Toolbar +import androidx.core.widget.NestedScrollView +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.iesoluciones.mylibrary.activities.ToolbarActivity +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.adapters.MultipleViewHolderAdapter +import com.iesoluciones.siodrenax.databinding.ActivitySurveyBinding +import com.iesoluciones.siodrenax.entities.Order +import com.iesoluciones.siodrenax.entities.OrderProgress +import com.iesoluciones.siodrenax.models.Question +import com.iesoluciones.siodrenax.repositories.OrdersRepository +import com.iesoluciones.siodrenax.repositories.SurveyRepository +import com.iesoluciones.siodrenax.utils.Constants.Companion.DRIVE_READER +import com.iesoluciones.siodrenax.utils.Constants.Companion.IDSERVICETYPEDOMESTIC +import com.iesoluciones.siodrenax.utils.Constants.Companion.ORDER_ID +import com.iesoluciones.siodrenax.utils.Constants.Companion.PDF_URL +import com.iesoluciones.siodrenax.utils.toast + +class SurveyActivity : ToolbarActivity() { + + private var orderId: Long = 0 + private var isDomestic = false + private lateinit var skipItem: MenuItem + private lateinit var acceptItem: MenuItem + private lateinit var order: Order + private lateinit var orderProgress: OrderProgress + + private lateinit var binding: ActivitySurveyBinding + private lateinit var ordersRepository: OrdersRepository + private lateinit var surveyRepository: SurveyRepository + + private lateinit var recycler: RecyclerView + private lateinit var svWebView: ScrollView + private lateinit var nsvSurvey: NestedScrollView + private lateinit var webview: WebView + private lateinit var btnPrivacyAdvice: AppCompatButton + private lateinit var btnSaveSurvey: AppCompatButton + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + binding = ActivitySurveyBinding.inflate(layoutInflater) + setContentView(binding.root) + + @Suppress("USELESS_CAST") + val toolbar = binding.toolbar as Toolbar + toolbar.title = getString(R.string.t_SurveyActivity) + toolbarToLoad(toolbar) + + viewBinding() + + ordersRepository = OrdersRepository() + surveyRepository = SurveyRepository() + + orderId = intent.getLongExtra(ORDER_ID, 0L) + order = ordersRepository.getOrderById(orderId)!! + orderProgress = ordersRepository.getOrderProgressById(orderId)!! + isDomestic = order.idServiceType == IDSERVICETYPEDOMESTIC + + val surveyQuestions: List = surveyRepository.getSurveyQuestions(isDomestic) + + val adapter = MultipleViewHolderAdapter(surveyQuestions, order) + + recycler.layoutManager = LinearLayoutManager(this) + recycler.setHasFixedSize(true) + recycler.adapter = adapter + + btnPrivacyAdvice.setOnClickListener { onPrivacyAdviceClick() } + btnSaveSurvey.setOnClickListener { onSurveyButtonClick() } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.survey, menu) + skipItem = menu.findItem(R.id.skip) + acceptItem = menu.findItem(R.id.accept) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.skip -> AlertDialog.Builder(this) + .setTitle(getString(R.string.ad_t_omitir_encuesta)) + .setMessage(getString(R.string.ad_m_omitir_encuesta)) + .setPositiveButton(R.string.accept) { _, _ -> + orderProgress.isOnSurvey = false + ordersRepository.saveOrderProgress(orderProgress) + surveyRepository.deleteAllSavedAnswers(order) + setResult(RESULT_OK) + finish() + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + } + .setNegativeButton(R.string.cancel, null) + .show() + R.id.accept -> { + svWebView.visibility = View.GONE + nsvSurvey.visibility = View.VISIBLE + acceptItem.isVisible = false + skipItem.isVisible = true + } + } + return super.onOptionsItemSelected(item) + } + + override fun onBackPressed() {} + + @SuppressLint("SetJavaScriptEnabled") + private fun onPrivacyAdviceClick() { + skipItem.isVisible = false + acceptItem.isVisible = true + webview.settings.javaScriptEnabled = true + webview.loadUrl(DRIVE_READER + PDF_URL) + webview.webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + view.loadUrl(url) + return true + } + } + nsvSurvey.visibility = View.GONE + svWebView.visibility = View.VISIBLE + } + + private fun onSurveyButtonClick() { + recycler.requestFocus() + recycler.requestFocusFromTouch() + btnSaveSurvey.isEnabled = false + + if (surveyRepository.validateAnswers(order)) { + btnSaveSurvey.isEnabled = true + orderProgress.isOnSurvey = false + orderProgress.shouldSendSurvey = true + ordersRepository.saveOrderProgress(orderProgress) + setResult(RESULT_OK) + finish() + overridePendingTransition(R.anim.slide_in_rigth_airbnb, R.anim.scale_out_airbnb) + } else { + toast(R.string.toast_faltan_campos) + btnSaveSurvey.isEnabled = true + } + } + + private fun viewBinding() { + recycler = binding.recycler + svWebView = binding.svWebView + nsvSurvey = binding.nsvSurvey + webview = binding.webview + btnPrivacyAdvice = binding.btnPrivacyAdvice + btnSaveSurvey = binding.btnSaveSurvey + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/WorkdayActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/WorkdayActivity.kt new file mode 100644 index 0000000..55f0ade --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/WorkdayActivity.kt @@ -0,0 +1,137 @@ +package com.iesoluciones.siodrenax.activities + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.telephony.PhoneNumberUtils +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.ViewModelProvider +import com.iesoluciones.mylibrary.activities.ToolbarActivity +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.databinding.ActivityWorkdayBinding +import com.iesoluciones.siodrenax.interfaces.OnMileageListener +import com.iesoluciones.siodrenax.preferencesHelper +import com.iesoluciones.siodrenax.repositories.WorkdayRepository +import com.iesoluciones.siodrenax.utils.CircularProgressButton +import com.iesoluciones.siodrenax.utils.Constants.Companion.DEFAULT_LATITUDE +import com.iesoluciones.siodrenax.utils.Constants.Companion.DEFAULT_LONGITUDE +import com.iesoluciones.siodrenax.utils.HelperUtil +import com.iesoluciones.siodrenax.viewmodels.WorkdayStatusViewModel +import java.text.SimpleDateFormat +import java.util.* + +class WorkdayActivity : ToolbarActivity(), CircularProgressButton.ProgressListener, OnMileageListener { + + private lateinit var binding: ActivityWorkdayBinding + private lateinit var workdayStatusViewModel: WorkdayStatusViewModel + private lateinit var tokenFirebase: String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityWorkdayBinding.inflate(layoutInflater) + setContentView(binding.root) + + @Suppress("USELESS_CAST") + val toolbar = binding.toolbar as Toolbar + toolbar.title = getString(R.string.t_WorkdayActivity) + toolbarToLoad(toolbar) + + val user = WorkdayRepository().getUser() + binding.tvUsername.text = getString(R.string.username, user.username, user.lastName, user.mothersName) + binding.tvInitials.text = user.username.substring(0, 1) + binding.tvPhone.text = PhoneNumberUtils.formatNumber(user.phone, "MX") + val inFormatter = SimpleDateFormat("dd 'de' MMM 'de' yyyy", Locale.getDefault()) + binding.tvDate.text = inFormatter.format(Date()) + + binding.iniciarJornadaButton.progressListener = this + workdayStatusViewModel = ViewModelProvider(this)[WorkdayStatusViewModel::class.java] + attachToObservables() + tokenFirebase = preferencesHelper.tokenFirebase!! + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.workday, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.log_out -> workdayStatusViewModel.logout() + } + return super.onOptionsItemSelected(item) + } + + override fun onProgressStart() {} + + override fun onProgressFinish() { + HelperUtil().mileageDialogFragment(supportFragmentManager, this) + } + + override fun onMileageInput(mileage: String?) { + if (mileage != null) { + workdayStatusViewModel.isLoading.value = getString(R.string.loading_wait_a_moment) + workdayStatusViewModel.startWorkday( + tokenFirebase, + mileage, + DEFAULT_LATITUDE.toString(), + DEFAULT_LONGITUDE.toString(), + filesDir.absolutePath + ) + } + } + + private fun attachToObservables() { + workdayStatusViewModel.isLoading.observe(this) { s -> + if (s != null) { + binding.frameLoading.visibility = View.VISIBLE + binding.tvLoading.text = s + binding.iniciarJornadaButton.deactivate() + } else { + binding.frameLoading.visibility = View.GONE + binding.iniciarJornadaButton.activate() + } + } + + workdayStatusViewModel.logOutSuccessObservable.observe(this) { logoutSuccess -> + if (logoutSuccess != null) if (logoutSuccess) { + startActivity(Intent(this@WorkdayActivity, LoginActivity::class.java)) + finish() + } + } + + workdayStatusViewModel.startWorkdaySuccess.observe(this) { + startActivity(Intent(this@WorkdayActivity, OrdersActivity::class.java)) + finish() + } + + workdayStatusViewModel.startWorkdayFailure.observe(this) { throwable -> + if (throwable != null) { + HelperUtil().parseError(this, throwable) + workdayStatusViewModel.startWorkdayFailure.value = null + } + } + + workdayStatusViewModel.endWorkdaySuccess.observe(this) { + startActivity(Intent(this@WorkdayActivity, LoginActivity::class.java)) + finish() + } + + workdayStatusViewModel.endWorkdayFailure.observe(this) { throwable -> + HelperUtil().parseError(this, throwable) + } + } + + /*private fun showAutoCloseDialog(context: Context?) { + AlertDialog.Builder(context!!) + .setMessage(R.string.auto_close) + .setPositiveButton(R.string.accept) { _, _ -> + workdayStatusViewModel.logout() + } + .setCancelable(false) + .show() + }*/ +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/activities/WorkdayManagerActivity.kt b/app/src/main/java/com/iesoluciones/siodrenax/activities/WorkdayManagerActivity.kt new file mode 100644 index 0000000..dc3b2ac --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/activities/WorkdayManagerActivity.kt @@ -0,0 +1,105 @@ +package com.iesoluciones.siodrenax.activities + +import android.content.Intent +import android.os.Bundle +import android.telephony.PhoneNumberUtils +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.ViewModelProvider +import com.iesoluciones.mylibrary.activities.ToolbarActivity +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.databinding.ActivityWorkdayBinding +import com.iesoluciones.siodrenax.preferencesHelper +import com.iesoluciones.siodrenax.repositories.ManagerRepository +import com.iesoluciones.siodrenax.utils.CircularProgressButton +import com.iesoluciones.siodrenax.utils.HelperUtil +import com.iesoluciones.siodrenax.utils.goToActivity +import com.iesoluciones.siodrenax.viewmodels.ManagerViewModel +import java.text.SimpleDateFormat +import java.util.* + +class WorkdayManagerActivity : ToolbarActivity(), CircularProgressButton.ProgressListener { + + private lateinit var binding: ActivityWorkdayBinding + private lateinit var managerViewModel: ManagerViewModel + private lateinit var managerRepository: ManagerRepository + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityWorkdayBinding.inflate(layoutInflater) + setContentView(binding.root) + + @Suppress("USELESS_CAST") + val toolbar = binding.toolbar as Toolbar + toolbar.title = getString(R.string.t_WorkdayManagerActivity) + toolbarToLoad(toolbar) + + managerRepository = ManagerRepository() + binding.iniciarJornadaButton.progressListener = this + managerViewModel = ViewModelProvider(this)[ManagerViewModel::class.java] + + attachToObservables() + } + + override fun onProgressStart() {} + + override fun onProgressFinish() { + managerViewModel.startWorkload() + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.workday, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.log_out -> { + managerRepository.logOut() + goToActivity { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + finish() + } + } + return super.onOptionsItemSelected(item) + } + + private fun attachToObservables() { + managerViewModel.onCallFailure.observe(this) { throwable: Throwable? -> + if (throwable != null) { + HelperUtil().parseError(this@WorkdayManagerActivity, throwable) + managerViewModel.onCallFailure.value = null + } + } + + managerViewModel.isLoading.observe(this) { s: String? -> + if (s != null) { + binding.frameLoading.visibility = View.VISIBLE + binding.tvLoading.text = s + binding.iniciarJornadaButton.deactivate() + } else { + binding.frameLoading.visibility = View.GONE + } + } + + managerViewModel.onWorkdaySuccess.observe(this) { success: Boolean -> + if (success) { + preferencesHelper.workdayStarted = true + goToActivity { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + } + } + + managerViewModel.getUser()!!.observe(this) { (_, _, username, lastName, mothersName, phone) -> + binding.tvUsername.text = getString(R.string.username, username, lastName, mothersName) + binding.tvInitials.text = username.substring(0, 1) + binding.tvPhone.text = PhoneNumberUtils.formatNumber(phone, "MX") + val inFormatter = SimpleDateFormat("dd 'de' MMM 'de' yyyy", Locale.getDefault()) + binding.tvDate.text = inFormatter.format(Date()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/adapters/HerramientaSurveyAdapter.kt b/app/src/main/java/com/iesoluciones/siodrenax/adapters/HerramientaSurveyAdapter.kt new file mode 100644 index 0000000..62413ef --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/adapters/HerramientaSurveyAdapter.kt @@ -0,0 +1,111 @@ +package com.iesoluciones.siodrenax.adapters + +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.checkListQuestionBox +import com.iesoluciones.siodrenax.databinding.ViewholderHerramientaBinding +import com.iesoluciones.siodrenax.entities.CheckListQuestion +import com.iesoluciones.siodrenax.entities.CheckListQuestion_ +import com.iesoluciones.siodrenax.preferencesHelper +import com.iesoluciones.siodrenax.utils.Constants +import io.objectbox.android.AndroidScheduler +import io.objectbox.kotlin.equal +import io.objectbox.reactive.DataSubscription + +class HerramientaSurveyAdapter( + private var checkListQuestionList: List +) : RecyclerView.Adapter() { + + var checkListQuestionBoxObserve: DataSubscription? = null + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + super.onAttachedToRecyclerView(recyclerView) + + checkListQuestionBoxObserve = checkListQuestionBox.query( + CheckListQuestion_.tipo equal Constants.HERRAMIENTA + ) + .build() + .subscribe() + .on(AndroidScheduler.mainThread()) + .observer { checkListQuestion -> checkListQuestionList = checkListQuestion } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { + val binding = ViewholderHerramientaBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return MyViewHolder(binding) + } + + override fun onBindViewHolder(holder: MyViewHolder, i: Int) { + holder.bind(checkListQuestionList[i]) + } + + override fun getItemCount() = checkListQuestionList.size + + class MyViewHolder(var binding: ViewholderHerramientaBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(checkListQuestion: CheckListQuestion) = with(itemView) { + itemView.tag = checkListQuestion.id + binding.tvQuestionTitle.text = checkListQuestion.nombre + + binding.lLCheck.visibility = + if (checkListQuestion.tipoCheckBox == 1) View.VISIBLE else View.GONE + binding.frameEdit.visibility = + if (checkListQuestion.tipoText == 1) View.VISIBLE else View.GONE + + binding.eTComentario.setText(checkListQuestion.respuestaText) + binding.cBQuestion.isChecked = checkListQuestion.respuestaCheckBox + + val isEnabled = preferencesHelper.isEnableHerramientaSurvey + + binding.eTComentario.isEnabled = isEnabled + binding.cBQuestion.isEnabled = isEnabled + + if (!isEnabled) { + binding.tvQuestionTitle.setTextColor( + ContextCompat.getColor( + context, + R.color.letraGrisBloqueado + ) + ) + } else { + listeners() + } + } + + private fun listeners() { + val id = itemView.tag.toString().toLong() + val objCheck: CheckListQuestion = checkListQuestionBox.get(id) + + if (objCheck.tipoText == 1) { + binding.eTComentario.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(s: Editable) { + objCheck.respuestaText = s.toString() + checkListQuestionBox.put(objCheck) + } + }) + } + + if (objCheck.tipoCheckBox == 1) { + binding.cBQuestion.setOnCheckedChangeListener { _, isChecked -> + objCheck.respuestaCheckBox = isChecked + checkListQuestionBox.put(objCheck) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/adapters/MaterialSurveyAdapter.kt b/app/src/main/java/com/iesoluciones/siodrenax/adapters/MaterialSurveyAdapter.kt new file mode 100644 index 0000000..6b5d1c9 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/adapters/MaterialSurveyAdapter.kt @@ -0,0 +1,76 @@ +package com.iesoluciones.siodrenax.adapters + +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.iesoluciones.siodrenax.checkListQuestionBox +import com.iesoluciones.siodrenax.databinding.ViewholderMaterialBinding +import com.iesoluciones.siodrenax.entities.CheckListQuestion + +class MaterialSurveyAdapter( + private val checkListQuestionList: List +): RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { + val binding = ViewholderMaterialBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return MyViewHolder(binding) + } + + override fun onBindViewHolder(holder: MyViewHolder, i: Int) { + holder.bind(checkListQuestionList[i]) + } + + override fun getItemCount() = checkListQuestionList.size + + class MyViewHolder(var binding: ViewholderMaterialBinding) : RecyclerView.ViewHolder(binding.root) { + + fun bind(checkListQuestion: CheckListQuestion) = with(itemView) { + itemView.tag = checkListQuestion.id + binding.tvQuestionTitle.text = checkListQuestion.nombre + + binding.lLCheck.visibility = if (checkListQuestion.tipoCheckBox == 1) View.VISIBLE else View.GONE + binding.frameEdit.visibility = if (checkListQuestion.tipoText == 1) View.VISIBLE else View.GONE + + binding.eTComentario.setText(checkListQuestion.respuestaText) + binding.cBQuestion.isChecked = checkListQuestion.respuestaCheckBox + + listeners() + } + + private fun listeners() { + val id = itemView.tag.toString().toLong() + val objCheck: CheckListQuestion = checkListQuestionBox.get(id) + + binding.eTComentario.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged( + s: CharSequence, + start: Int, + count: Int, + after: Int + ) { + } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + if (objCheck.tipoText == 1) { + objCheck.respuestaText = binding.eTComentario.text.toString() + checkListQuestionBox.put(objCheck) + } + } + + override fun afterTextChanged(s: Editable) {} + }) + + binding.cBQuestion.setOnCheckedChangeListener { _, isChecked -> + objCheck.respuestaCheckBox = isChecked + checkListQuestionBox.put(objCheck) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/adapters/MultipleViewHolderAdapter.kt b/app/src/main/java/com/iesoluciones/siodrenax/adapters/MultipleViewHolderAdapter.kt new file mode 100644 index 0000000..dfb47cb --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/adapters/MultipleViewHolderAdapter.kt @@ -0,0 +1,355 @@ +package com.iesoluciones.siodrenax.adapters + +import android.app.Activity +import android.app.DatePickerDialog +import android.text.Editable +import android.text.InputType +import android.text.TextWatcher +import android.util.Patterns +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnFocusChangeListener +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.appcompat.widget.AppCompatCheckBox +import androidx.recyclerview.widget.RecyclerView +import com.iesoluciones.siodrenax.App +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.databinding.ViewholderAnswerOpenBinding +import com.iesoluciones.siodrenax.databinding.ViewholderCheckboxAnswerBinding +import com.iesoluciones.siodrenax.entities.Order +import com.iesoluciones.siodrenax.models.Answer +import com.iesoluciones.siodrenax.models.Question +import com.iesoluciones.siodrenax.repositories.SurveyRepository +import com.iesoluciones.siodrenax.utils.Constants.Companion.ANSWER_CHECKBOX +import com.iesoluciones.siodrenax.utils.Constants.Companion.ANSWER_CURRENCY +import com.iesoluciones.siodrenax.utils.Constants.Companion.ANSWER_DATE +import com.iesoluciones.siodrenax.utils.Constants.Companion.ANSWER_EMAIL +import com.iesoluciones.siodrenax.utils.Constants.Companion.ANSWER_NUMBER +import com.iesoluciones.siodrenax.utils.Constants.Companion.ANSWER_TEXT +import com.iesoluciones.siodrenax.utils.Constants.Companion.CERO +import com.iesoluciones.siodrenax.utils.Constants.Companion.IDSERVICETYPEDOMESTIC +import java.text.NumberFormat +import java.util.* + +class MultipleViewHolderAdapter( + private var questionList: List, + private val order: Order +) : + RecyclerView.Adapter() { + + private val isDomestic = order.idServiceType == IDSERVICETYPEDOMESTIC + private val surveyRepository = SurveyRepository() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnswerViewHolder { + when (viewType) { + ANSWER_CHECKBOX -> return CheckBoxViewHolder( + ViewholderCheckboxAnswerBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + ANSWER_CURRENCY, ANSWER_DATE, ANSWER_EMAIL, ANSWER_NUMBER, ANSWER_TEXT -> return OpenAnswerViewHolder( + ViewholderAnswerOpenBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + viewType + ) + } + throw RuntimeException("Uncaught viewholder Type") + } + + override fun onBindViewHolder(holder: AnswerViewHolder, position: Int) { + holder.bindViews(questionList[position]) + } + + override fun getItemCount(): Int { + return questionList.size + } + + override fun getItemViewType(position: Int): Int { + return getViewType(questionList[position]) + } + + abstract class AnswerViewHolder(itemView: View) : + RecyclerView.ViewHolder(itemView) { + abstract fun bindViews(question: Question) + abstract fun cleanOnRecycled() + } + + inner class CheckBoxViewHolder(var binding: ViewholderCheckboxAnswerBinding) : + AnswerViewHolder(binding.root), View.OnClickListener { + private var tvQuestionTitle = binding.tvQuestionTitle + private var linearAnswer = binding.linearAnswer + private lateinit var answers: List + private lateinit var answer: Answer + private var checkBoxes: MutableList = ArrayList() + private lateinit var question: Question + + override fun bindViews(question: Question) { + this.question = question + + if (question.getShowNumber() == 0) + tvQuestionTitle.text = question.getTitle() + else + tvQuestionTitle.text = App.context!!.resources.getString( + R.string.question_title, + (adapterPosition + 1), + question.getTitle() + ) + + answers = surveyRepository.getAnswers(question, isDomestic) + for (answer in answers) { + val checkBox = AppCompatCheckBox(itemView.context) + checkBox.layoutParams = ViewGroup.LayoutParams( + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + ViewGroup.LayoutParams.WRAP_CONTENT.toFloat(), + itemView.context.resources.displayMetrics + ) + .toInt(), + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + ViewGroup.LayoutParams.WRAP_CONTENT.toFloat(), + itemView.context.resources.displayMetrics + ) + .toInt() + ) + checkBox.text = answer.getTitle() + checkBox.isChecked = false + checkBox.tag = answer + checkBox.setOnClickListener(this) + + val savedAnswer = surveyRepository.getQuestionSavedAnswer(question, order) + if (savedAnswer != null && savedAnswer != "" && savedAnswer.toLong() == answer.getId()) { + this.answer = answer + checkBox.isChecked = true + } + + linearAnswer.addView(checkBox) + checkBoxes.add(checkBox) + } + } + + override fun cleanOnRecycled() { + linearAnswer.removeAllViews() + } + + override fun onClick(v: View) { + if (!(v as AppCompatCheckBox).isChecked) { + surveyRepository.saveQuestionAnswer(question, order, null, false) + } else { + answer = v.getTag() as Answer + surveyRepository.saveQuestionAnswer( + question, + order, + answer.getId().toString() + "", + false + ) + } + + for (checkBox in checkBoxes) + if (checkBox.tag !== answer) + checkBox.isChecked = false + } + + } + + inner class OpenAnswerViewHolder( + var binding: ViewholderAnswerOpenBinding, + private var viewType: Int + ) : + AnswerViewHolder(binding.root), View.OnClickListener { + private var tvQuestionTitle = binding.tvQuestionTitle + private var frameDate = binding.frameDate + private var tvDate = binding.tvDate + private var editAnswer = binding.editAnswer + private var linearQuestion = binding.linearQuestion + private lateinit var question: Question + + //Calendario para obtener fecha & hora + private val c = Calendar.getInstance() + + //Variables para obtener la fecha + private val mes = c[Calendar.MONTH] + private val dia = c[Calendar.DAY_OF_MONTH] + private val anio = c[Calendar.YEAR] + + override fun bindViews(question: Question) { + this.question = question + if (question.getShowNumber() == 0) + tvQuestionTitle.text = question.getTitle() + else + tvQuestionTitle.text = App.context!!.resources.getString( + R.string.question_title, + (adapterPosition + 1), + question.getTitle() + ) + + when (viewType) { + ANSWER_CURRENCY -> { + //Currency TextWatcher, set + editAnswer.inputType = InputType.TYPE_CLASS_NUMBER + editAnswer.addTextChangedListener(CurrencyTextWatcher()) + editAnswer.setText("0") + editAnswer.onFocusChangeListener = + OnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + var formatted = editAnswer.text.toString() + formatted = formatted.replace(",".toRegex(), "") + formatted = formatted.replace("\\$".toRegex(), "") + surveyRepository.saveQuestionAnswer( + question, + order, + formatted, + true + ) + } + } + } + ANSWER_DATE -> { + //Block clicks to show date picker, and format it + editAnswer.visibility = View.GONE + frameDate.visibility = View.VISIBLE + linearQuestion.setOnClickListener(this) + } + ANSWER_EMAIL -> { + //Email TextWatcher + editAnswer.inputType = InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + editAnswer.imeOptions = EditorInfo.IME_ACTION_NEXT + editAnswer.onFocusChangeListener = + OnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + if (!isValidEmail(editAnswer.text.toString())) editAnswer.error = + "Correo inválido" else surveyRepository.saveQuestionAnswer( + question, order, editAnswer.text.toString(), true + ) + } + } + } + ANSWER_NUMBER -> //Number formatter + editAnswer.inputType = InputType.TYPE_CLASS_NUMBER + ANSWER_TEXT -> { + //Text TextWatcher + editAnswer.imeOptions = EditorInfo.IME_ACTION_NEXT + editAnswer.onFocusChangeListener = + OnFocusChangeListener { _, hasFocus -> + var answers = editAnswer.text.toString() + answers = answers.replace(" ", "") + if (!hasFocus && answers != "") { + surveyRepository.saveQuestionAnswer( + question, + order, + editAnswer.text.toString(), + true + ) + } + } + + if (!editAnswer.hasFocus() && editAnswer.text.toString() == "") + surveyRepository.saveQuestionAnswer(question, order, null, true) + } + } + + val savedAnswer = surveyRepository.getQuestionSavedAnswer(question, order) + if (viewType == ANSWER_DATE) + tvDate.text = savedAnswer + else + editAnswer.setText(savedAnswer) + } + + override fun cleanOnRecycled() {} + private fun isValidEmail(target: String?): Boolean { + return if (target == null) { + false + } else { + //android Regex to check the email address Validation + Patterns.EMAIL_ADDRESS.matcher(target).matches() + } + } + + private fun obtenerFecha() { + val recogerFecha = DatePickerDialog( + itemView.context, + { _, year, month, dayOfMonth -> //Esta variable lo que realiza es aumentar en uno el mes ya que comienza desde 0 = enero + val mesActual = month + 1 + //Formateo el día obtenido: antepone el 0 si son menores de 10 + val diaFormateado = + if (dayOfMonth < 10) CERO + dayOfMonth.toString() else dayOfMonth.toString() + //Formateo el mes obtenido: antepone el 0 si son menores de 10 + val mesFormateado = + if (mesActual < 10) CERO + mesActual.toString() else mesActual.toString() + //Muestro la fecha con el formato deseado + val dateFormatted = App.context!!.resources.getString( + R.string.date_format, + year.toString(), + mesFormateado, + diaFormateado + ) + + tvDate.text = dateFormatted + surveyRepository.saveQuestionAnswer( + question, + order, + dateFormatted, + true + ) + }, //Estos valores deben ir en ese orden, de lo contrario no mostrara la fecha actual + anio, mes, dia + ) + //Muestro el widget + recogerFecha.show() + } + + internal inner class CurrencyTextWatcher : TextWatcher { + private var current = "" + + @Synchronized + override fun afterTextChanged(s: Editable) { + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + if (s.toString() != current) { + editAnswer.removeTextChangedListener(this) + val cleanString = s.toString().replace("[$,.]".toRegex(), "") + var parsed = 0.0 + try { + parsed = cleanString.toDouble() + } catch (ignored: NumberFormatException) { + } + val formatted = NumberFormat.getCurrencyInstance().format(parsed / 100) + current = formatted + editAnswer.setText(formatted) + editAnswer.setSelection(formatted.length) + editAnswer.addTextChangedListener(this) + } + } + } + + override fun onClick(v: View) { + val imm = + itemView.context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(itemView.windowToken, 0) + obtenerFecha() + } + + } + + private fun getViewType(question: Question): Int { + return when (surveyRepository.getAnswers(question, isDomestic)[0].getType()) { + "Checkbox" -> ANSWER_CHECKBOX + "Fecha" -> ANSWER_DATE + "Email" -> ANSWER_EMAIL + "Numero" -> ANSWER_NUMBER + "Moneda" -> ANSWER_CURRENCY + "Texto" -> ANSWER_TEXT + else -> throw RuntimeException("Question Type Not Supported") + } + } +} diff --git a/app/src/main/java/com/iesoluciones/siodrenax/adapters/NextDayOrdersAdapter.kt b/app/src/main/java/com/iesoluciones/siodrenax/adapters/NextDayOrdersAdapter.kt new file mode 100644 index 0000000..9bb8d02 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/adapters/NextDayOrdersAdapter.kt @@ -0,0 +1,159 @@ +package com.iesoluciones.siodrenax.adapters + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.Context.VIBRATOR_SERVICE +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager +import android.telephony.PhoneNumberUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.recyclerview.widget.RecyclerView +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.databinding.ViewholderOrderBinding +import com.iesoluciones.siodrenax.entities.NextDayOrder +import com.iesoluciones.siodrenax.utils.StatusDrawable +import com.iesoluciones.siodrenax.utils.currencyFormat +import java.text.SimpleDateFormat +import java.util.* + +class NextDayOrdersAdapter( + private val context: Context, + private var orders: List +) : RecyclerView.Adapter() { + + companion object { + val inFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + val outFormatter = SimpleDateFormat("HH:mm", Locale.getDefault()) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = + ViewholderOrderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding, context) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(orders[position]) + } + + override fun getItemCount(): Int { + return orders.size + } + + fun setOrders(orders: List) { + this.orders = orders + } + + class ViewHolder( + val binding: ViewholderOrderBinding, + val context: Context + ) : RecyclerView.ViewHolder(binding.root) { + + private val tvClientDenomination = binding.tvClientDenomination + private val tvOrderName = binding.tvOrderName + private val tvPayMethod = binding.tvPayMethod + private val tvContact = binding.tvContact + private val tvAddress = binding.tvAddress + private val tvServiceStatus = binding.tvServiceStatus + private val tvTitle = binding.tvTitle + private val tvCost = binding.tvCost + private val tvOrderId = binding.tvOrderId + private val tvOrderScheduledTime = binding.tvOrderScheduledTime + private val tvContactPhone = binding.tvContactPhone + private val linearContact = binding.linearContact + private val linearComments = binding.linearComments + private val tvComments = binding.tvComments + private val tvVehicle = binding.tvVehicle + + fun bind(order: NextDayOrder) = with(itemView) { + try { + val temp: Date = inFormatter.parse(order.dateServiceRequest)!! + inFormatter.parse(order.dateServiceRequest)!! + tvClientDenomination.text = order.clientDenomination + tvOrderScheduledTime.text = context.resources.getString( + R.string.scheduled_time, outFormatter.format( + temp + ) + ) + } catch (e: Exception) { + e.printStackTrace() + tvClientDenomination.text = order.clientContactName + tvOrderScheduledTime.text = + context.resources.getString(R.string.scheduled_time_error) + } + + if (order.getClientContactCellphone.trim().isNotEmpty()) { + tvContact.text = order.clientContactName + tvContactPhone.text = context.resources.getString( + R.string.contact_phone, PhoneNumberUtils.formatNumber( + order.getClientContactCellphone, + "MX" + ) + ) + tvContactPhone.visibility = View.VISIBLE + } else { + tvContact.text = order.clientContactName + tvContactPhone.visibility = View.GONE + } + + if (order.comments != null) { + linearComments.visibility = View.VISIBLE + tvComments.text = order.comments + } else + linearComments.visibility = View.GONE + + tvOrderName.text = order.serviceName + tvServiceStatus.text = order.serviceStatusName + tvPayMethod.text = order.payMethodName + tvVehicle.text = order.vehicleCodeName + tvOrderId.text = "${order.idRequest}" + tvCost.text = context.resources.getString(R.string.cost, order.cost.currencyFormat()) + tvAddress.text = context.resources.getString( + R.string.address, + order.streetAddress, + order.extNumberAddress, + order.neighborhoodNameAddress, + order.clientPostalCodeAddress + ) + + val drawable = StatusDrawable(order.serviceStatusColor1, order.serviceStatusColor2) + tvTitle.background = drawable + + linearContact.setOnLongClickListener { + if (order.getClientContactCellphone.trim().isNotEmpty()) { + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions((itemView.context as AppCompatActivity), arrayOf(Manifest.permission.CALL_PHONE), 3) + return@setOnLongClickListener true + } else { + val callIntent = Intent(Intent.ACTION_CALL) + callIntent.data = Uri.parse("tel:" + order.getClientContactCellphone.trim()) // change the number + vibrate() + context.startActivity(callIntent) + } + } + + false + } + } + + @SuppressLint("MissingPermission") + private fun vibrate() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = context.getSystemService(AppCompatActivity.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibratorManager.defaultVibrator.vibrate(VibrationEffect.createOneShot(150, VibrationEffect.DEFAULT_AMPLITUDE)) + }else{ + (context.getSystemService(VIBRATOR_SERVICE) as Vibrator).vibrate(VibrationEffect.createOneShot(150, VibrationEffect.DEFAULT_AMPLITUDE)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/adapters/OperatorAdapter.kt b/app/src/main/java/com/iesoluciones/siodrenax/adapters/OperatorAdapter.kt new file mode 100644 index 0000000..98c3dfd --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/adapters/OperatorAdapter.kt @@ -0,0 +1,68 @@ +package com.iesoluciones.siodrenax.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.databinding.ViewholderOperatorBinding +import com.iesoluciones.siodrenax.entities.Operator +import com.iesoluciones.siodrenax.utils.HelperUtil + +class OperatorAdapter( + var operatorList: List, + private val listener: OnOperatorSelectedListener + +) : RecyclerView.Adapter() { + + interface OnOperatorSelectedListener { + fun onOperatorClicked(operator: Operator) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = + ViewholderOperatorBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding, listener) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(operatorList[position], position%2 == 0) + } + + override fun getItemCount(): Int { + return operatorList.size + } + + class ViewHolder( + val binding: ViewholderOperatorBinding, + val listener: OnOperatorSelectedListener + ): RecyclerView.ViewHolder(binding.root){ + + private val tvInitials = binding.tvInitials + private val tvName = binding.tvName + private val tvOrdersIndicator = binding.tvOrdersIndicator + private val cardOperator = binding.cardOperator + + fun bind(operator: Operator, colorFlag: Boolean) = with(itemView){ + tvName.text = "${operator.name} ${operator.lastName} ${operator.mothersName}" + + tvOrdersIndicator.text = "${operator.pendingOrders} / ${operator.totalOrders}" + + tvInitials.text = HelperUtil().getInitials( + operator.name, + operator.lastName + ) + + if (operator.pendingOrders == 0L) cardOperator.setCardBackgroundColor( + itemView.context.resources.getColor( + R.color.lightDarkGray + ) + ) else cardOperator.setCardBackgroundColor(itemView.context.resources.getColor(android.R.color.white)) + + if (colorFlag) tvInitials.setBackgroundResource(R.drawable.circle_border_accent) else tvInitials.setBackgroundResource( + R.drawable.circle_border_primary + ) + + itemView.setOnClickListener { listener.onOperatorClicked(operator) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/adapters/OrdersAdapter.kt b/app/src/main/java/com/iesoluciones/siodrenax/adapters/OrdersAdapter.kt new file mode 100644 index 0000000..8b0d778 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/adapters/OrdersAdapter.kt @@ -0,0 +1,201 @@ +package com.iesoluciones.siodrenax.adapters + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.Context.VIBRATOR_SERVICE +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager +import android.telephony.PhoneNumberUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.recyclerview.widget.RecyclerView +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.activities.OrderProgressActivity +import com.iesoluciones.siodrenax.databinding.ViewholderOrderBinding +import com.iesoluciones.siodrenax.entities.Order +import com.iesoluciones.siodrenax.entities.OrderProgress +import com.iesoluciones.siodrenax.repositories.OrdersRepository +import com.iesoluciones.siodrenax.utils.Constants.Companion.ORDER_ID +import com.iesoluciones.siodrenax.utils.StatusDrawable +import com.iesoluciones.siodrenax.utils.currencyFormat +import java.text.SimpleDateFormat +import java.util.* + +class OrdersAdapter( + private val context: Context, + var orders: List +) : RecyclerView.Adapter() { + + companion object{ + val inFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + val outFormatter = SimpleDateFormat("HH:mm", Locale.getDefault()) + val ordersRepository: OrdersRepository = OrdersRepository() + } + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = + ViewholderOrderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding, context) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(orders[position]) + } + + override fun getItemCount(): Int { + return orders.size + } + + class ViewHolder( + val binding: ViewholderOrderBinding, + val context: Context + ): RecyclerView.ViewHolder(binding.root) { + + private val tvClientDenomination = binding.tvClientDenomination + private val tvOrderName = binding.tvOrderName + private val tvPayMethod = binding.tvPayMethod + private val tvContact = binding.tvContact + private val tvAddress = binding.tvAddress + private val tvServiceStatus = binding.tvServiceStatus + private val tvTitle = binding.tvTitle + private val tvCost = binding.tvCost + private val tvOrderId = binding.tvOrderId + private val tvOrderScheduledTime = binding.tvOrderScheduledTime + private val tvContactPhone = binding.tvContactPhone + private val linearContact = binding.linearContact + private val linearComments = binding.linearComments + private val tvComments = binding.tvComments + private val tvVehicle = binding.tvVehicle + + fun bind(order: Order) = with(itemView){ + try { + // NullPointerException launched once. The "dateServiceRequest" is a non null field. + // The WS sends the field and the Local Data Base saves it properly + // Commented to track the error -------------> Incidents = 1 + val temp: Date = inFormatter.parse(order.dateServiceRequest)!! + tvClientDenomination.text = order.clientDenomination + tvOrderScheduledTime.text = context.resources.getString( + R.string.scheduled_time, outFormatter.format( + temp + ) + ) + } catch (e: Exception) { + e.printStackTrace() + tvClientDenomination.text = order.clientContactName + tvOrderScheduledTime.text = context.resources.getString(R.string.scheduled_time_error) + } + + tvOrderName.text = order.serviceName + + val orderProgress: OrderProgress? = ordersRepository.getOrderProgressById(order.id) + tvServiceStatus.text = if(orderProgress != null) context.getString(R.string.sincronizando) else order.serviceStatusName + tvVehicle.text = order.vehicleCodeName + + + if (order.getClientContactCellphone.trim().isNotEmpty()) { + tvContact.text = order.clientContactName + tvContactPhone.text = context.resources.getString( + R.string.contact_phone, PhoneNumberUtils.formatNumber( + order.getClientContactCellphone, + "MX" + ) + ) + tvContactPhone.visibility = View.VISIBLE + } else { + tvContact.text = order.clientContactName + tvContactPhone.visibility = View.GONE + } + + tvPayMethod.text = order.payMethodName + + val drawable = StatusDrawable(order.serviceStatusColor1, order.serviceStatusColor2) + + tvTitle.background = drawable + + tvOrderId.text = "${order.idRequest}" + + tvCost.text = context.resources.getString(R.string.cost, order.cost.currencyFormat()) + + tvAddress.text = context.resources.getString( + R.string.address, + order.streetAddress, + order.extNumberAddress, + order.neighborhoodNameAddress, + order.clientPostalCodeAddress + ) + + if (order.comments != null) { + linearComments.visibility = View.VISIBLE + tvComments.text = order.comments + } else linearComments.visibility = View.GONE + + linearContact.setOnLongClickListener { + if (order.getClientContactCellphone.trim().isNotEmpty()) { + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions((itemView.context as AppCompatActivity), arrayOf(Manifest.permission.CALL_PHONE), 3) + return@setOnLongClickListener true + }else{ + val callIntent = Intent(Intent.ACTION_CALL) + callIntent.data = Uri.parse("tel:" + order.getClientContactCellphone.trim()) // change the number + vibrate() + context.startActivity(callIntent) + } + } + false + } + + linearContact.setOnClickListener { + //Se agrega por error en Crashlytics + val orderRepo: Order? = ordersRepository.getOrderById(order.id) + //Se agrega por error en Crashlytics + if (orderRepo != null) { + changeToOrderProgressActivity(order.id) + } else { + Toast.makeText(context, "No se encontró la información.", Toast.LENGTH_SHORT).show() + } + } + + itemView.setOnClickListener { + //Se agrega por error en Crashlytics + val orderRepo: Order? = ordersRepository.getOrderById(order.id) + //Se agrega por error en Crashlytics + if (orderRepo != null) { + changeToOrderProgressActivity(order.id) + } else { + Toast.makeText(context, "No se encontró la información.", Toast.LENGTH_SHORT).show() + } + } + } + + private fun changeToOrderProgressActivity(orderId: Long){ + val i = Intent(context, OrderProgressActivity::class.java) + i.putExtra(ORDER_ID, orderId) + context.startActivity(i) + (context as AppCompatActivity).overridePendingTransition( + R.anim.slide_in_rigth_airbnb, + R.anim.scale_out_airbnb + ) + } + + @SuppressLint("MissingPermission") + private fun vibrate() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = context.getSystemService(AppCompatActivity.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibratorManager.defaultVibrator.vibrate(VibrationEffect.createOneShot(150, VibrationEffect.DEFAULT_AMPLITUDE)) + }else{ + (context.getSystemService(VIBRATOR_SERVICE) as Vibrator).vibrate(VibrationEffect.createOneShot(150, VibrationEffect.DEFAULT_AMPLITUDE)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/adapters/OrdersManagerAdapter.kt b/app/src/main/java/com/iesoluciones/siodrenax/adapters/OrdersManagerAdapter.kt new file mode 100644 index 0000000..43dae05 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/adapters/OrdersManagerAdapter.kt @@ -0,0 +1,144 @@ +package com.iesoluciones.siodrenax.adapters + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager +import android.telephony.PhoneNumberUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.recyclerview.widget.RecyclerView +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.databinding.ViewholderOrderBinding +import com.iesoluciones.siodrenax.entities.Order +import com.iesoluciones.siodrenax.utils.StatusDrawable +import com.iesoluciones.siodrenax.utils.currencyFormat +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + +class OrdersManagerAdapter( + private val context: Context, + var orders: List +) : RecyclerView.Adapter() { + + companion object{ + var inFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + var outFormatter = SimpleDateFormat("HH:mm", Locale.getDefault()) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = + ViewholderOrderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding, context) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(orders[position]) + } + + override fun getItemCount(): Int { + return orders.size + } + + class ViewHolder( + val binding: ViewholderOrderBinding, + val context: Context + ): RecyclerView.ViewHolder(binding.root) { + + fun bind(order: Order){ + try { + val temp: Date = inFormatter.parse(order.dateServiceRequest)!! + binding.tvClientDenomination.text = order.clientDenomination + binding.tvOrderScheduledTime.text = context.resources.getString( + R.string.scheduled_time, outFormatter.format( + temp + ) + ) + } catch (e: ParseException) { + e.printStackTrace() + binding.tvClientDenomination.text = order.clientContactName + binding.tvOrderScheduledTime.text = context.resources.getString(R.string.scheduled_time_error) + } + + binding.tvOrderName.text = order.serviceName + binding.tvServiceStatus.text = order.serviceStatusName + + if (order.getClientContactCellphone.trim().isNotEmpty()) { + binding.tvContact.text = order.clientContactName + binding.tvContactPhone.text = context.resources.getString( + R.string.contact_phone, PhoneNumberUtils.formatNumber( + order.getClientContactCellphone, + "MX" + ) + ) + binding.tvContactPhone.visibility = View.VISIBLE + } else { + binding.tvContact.text = order.clientContactName + binding.tvContactPhone.visibility = View.GONE + } + + binding.tvPayMethod.text = order.payMethodName + + val drawable = StatusDrawable(order.serviceStatusColor1, order.serviceStatusColor2) + + binding.tvTitle.background = drawable + + binding.tvVehicle.text = order.vehicleCodeName + + binding.tvOrderId.text = "${order.idRequest}" + + binding.tvCost.text = context.resources.getString( + R.string.cost, + order.cost.currencyFormat() + ) + + binding.tvAddress.text = context.resources.getString( + R.string.address, + order.streetAddress, + order.extNumberAddress, + order.neighborhoodNameAddress, + order.clientPostalCodeAddress + ) + + if (order.comments != null) { + binding.linearComments.visibility = View.VISIBLE + binding.tvComments.text = order.comments + } else binding.linearComments.visibility = View.GONE + + binding.linearContact.setOnLongClickListener { + if (order.getClientContactCellphone.trim().isNotEmpty()) { + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions((itemView.context as AppCompatActivity), arrayOf(Manifest.permission.CALL_PHONE), 3) + return@setOnLongClickListener true + }else{ + val callIntent = Intent(Intent.ACTION_CALL) + callIntent.data = Uri.parse("tel:" + order.getClientContactCellphone.trim()) // change the number + vibrate() + context.startActivity(callIntent) + } + } + false + } + } + + @SuppressLint("MissingPermission") + private fun vibrate() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = context.getSystemService(AppCompatActivity.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibratorManager.defaultVibrator.vibrate(VibrationEffect.createOneShot(150, VibrationEffect.DEFAULT_AMPLITUDE)) + }else{ + (context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).vibrate(VibrationEffect.createOneShot(150, VibrationEffect.DEFAULT_AMPLITUDE)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/adapters/PdfViewerAdapter.kt b/app/src/main/java/com/iesoluciones/siodrenax/adapters/PdfViewerAdapter.kt new file mode 100644 index 0000000..e4270f1 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/adapters/PdfViewerAdapter.kt @@ -0,0 +1,59 @@ +package com.iesoluciones.siodrenax.adapters + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView +import com.iesoluciones.siodrenax.databinding.ViewholderPdfBinding + +class PdfViewerAdapter( + private val context: Context, + var pages: List +): RecyclerView.Adapter() { + + class ViewHolder( + val binding: ViewholderPdfBinding, + val context: Context + ): RecyclerView.ViewHolder(binding.root), View.OnTouchListener { + + @SuppressLint("ClickableViewAccessibility") + fun bind(bitmap: Bitmap) = with(itemView){ + val imageView: ImageView = binding.imageView + imageView.setImageBitmap(bitmap) + imageView.setOnTouchListener(this@ViewHolder) + } + + override fun onTouch(view: View?, motionEvent: MotionEvent?): Boolean { + if(motionEvent!!.pointerCount >= 2 || (view!!.canScrollHorizontally(1) && view.canScrollHorizontally(-1))){ + when (motionEvent.action){ + MotionEvent.ACTION_MOVE -> { + view!!.parent.requestDisallowInterceptTouchEvent(true) + } + MotionEvent.ACTION_UP -> { + view!!.parent.requestDisallowInterceptTouchEvent(false) + } + else -> {return true} + } + } + return true + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ViewholderPdfBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding, context) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(pages[position]) + } + + override fun getItemCount(): Int { + return pages.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/adapters/RevisionSurveyAdapter.kt b/app/src/main/java/com/iesoluciones/siodrenax/adapters/RevisionSurveyAdapter.kt new file mode 100644 index 0000000..7340d72 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/adapters/RevisionSurveyAdapter.kt @@ -0,0 +1,185 @@ +package com.iesoluciones.siodrenax.adapters + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnFocusChangeListener +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Button +import androidx.recyclerview.widget.RecyclerView +import com.iesoluciones.siodrenax.App.Companion.context +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.checkListQuestionBox +import com.iesoluciones.siodrenax.databinding.ViewholderRevisionBinding +import com.iesoluciones.siodrenax.entities.CheckListQuestion +import com.iesoluciones.siodrenax.entities.CheckListQuestion_ +import com.iesoluciones.siodrenax.repositories.CheckListRepository +import com.iesoluciones.siodrenax.utils.Constants +import io.objectbox.android.AndroidScheduler +import io.objectbox.kotlin.equal +import io.objectbox.reactive.DataSubscription + +class RevisionSurveyAdapter( + private val btnSiguiente: Button, + private var checkListQuestionList: List, + private val vehicle: List, + private val vehicleIds: List +) : RecyclerView.Adapter() { + + companion object { + var vehicleSelectedId: Long = 0 + } + + var checkListQuestionBoxObserve: DataSubscription? = null + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + super.onAttachedToRecyclerView(recyclerView) + + checkListQuestionBoxObserve = checkListQuestionBox.query( + CheckListQuestion_.tipo equal Constants.REVISION + ) + .build() + .subscribe() + .on(AndroidScheduler.mainThread()) + .observer { checkListQuestion -> checkListQuestionList = checkListQuestion } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = + ViewholderRevisionBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding, vehicle, vehicleIds) + } + + override fun onBindViewHolder(holder: ViewHolder, i: Int) { + holder.bind(btnSiguiente, checkListQuestionList[i]) + } + + override fun getItemCount() = checkListQuestionList.size + + class ViewHolder( + var binding: ViewholderRevisionBinding, + private val vehicle: List, + val vehicleIds: List + ) : RecyclerView.ViewHolder(binding.root) { + + var isSpinnerTouched = false + + fun bind(btnSiguiente: Button, checkListQuestion: CheckListQuestion) = with(itemView) { + + binding.radioGroup.visibility = + if (checkListQuestion.tipoRadioBtn == 1) View.VISIBLE else View.GONE + + binding.eTComentario.visibility = + if (checkListQuestion.tipoText == 1) View.VISIBLE else View.GONE + + binding.spVehiculos.visibility = + if (checkListQuestion.id == 1L) View.VISIBLE else View.GONE + + val isTipoSpinner = + checkListQuestion.tipoCheckBox == 0 && checkListQuestion.tipoRadioBtn == 0 && checkListQuestion.tipoText == 0 + if (isTipoSpinner) { + val dataAdapter: ArrayAdapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_item, + vehicle + ) + dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.spVehiculos.adapter = dataAdapter + if (checkListQuestion.respuestaText != "") { + for (i in vehicle.indices) { + if (vehicle[i] == checkListQuestion.respuestaText) { + binding.spVehiculos.setSelection(i) + } + } + } + } + + itemView.tag = checkListQuestion.id + binding.tvQuestionTitle.text = checkListQuestion.nombre + binding.eTComentario.setText(checkListQuestion.respuestaText) + val radioCheck: String? = checkListQuestion.respuestaRadioBtn + if (radioCheck != null && itemView.tag == checkListQuestion.id) { + + binding.radioNormal.tag = checkListQuestion.id + binding.radioBajo.tag = checkListQuestion.id + + if (radioCheck == context.getString(R.string.normal)) { + binding.radioGroup.check(R.id.radioNormal) + } else if (radioCheck == context.getString(R.string.bajo)) { + binding.radioGroup.check(R.id.radioBajo) + } + } + + listeners(isTipoSpinner, btnSiguiente) + } + + @SuppressLint("ClickableViewAccessibility") + private fun listeners(isTipoSpinner: Boolean, btnSiguiente: Button) { + val id = itemView.tag.toString().toLong() + val objCheck: CheckListQuestion = checkListQuestionBox.get(id) + + if(objCheck.tipoText == 1){ + binding.eTComentario.onFocusChangeListener = OnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + objCheck.respuestaText = (binding.eTComentario.text.toString()) + checkListQuestionBox.put(objCheck) + } + } + } + + if(objCheck.tipoRadioBtn == 1){ + binding.radioBajo.setOnClickListener { + setCheckListText(objCheck, btnSiguiente, context!!.getString(R.string.bajo)) + } + + binding.radioNormal.setOnClickListener { + setCheckListText(objCheck, btnSiguiente, context!!.getString(R.string.normal)) + } + } + + if (isTipoSpinner) { + btnSiguiente.visibility = if (CheckListRepository().isValidRevision() && binding.spVehiculos.selectedItemPosition != 0) View.VISIBLE else View.GONE + binding.spVehiculos.setOnTouchListener { _, _ -> + isSpinnerTouched = true + false + } + + binding.spVehiculos.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>, + view: View, + position: Int, + id: Long + ) { + vehicleSelectedId = vehicleIds[position] + if (isSpinnerTouched) { + val item = parent.getItemAtPosition(position).toString() + objCheck.respuestaText = (if (position != 0) item else "") + checkListQuestionBox.put(objCheck) + isSpinnerTouched = false + btnSiguiente.visibility = if (CheckListRepository().isValidRevision() && vehicleSelectedId != 0L) View.VISIBLE else View.GONE + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + } + } + } + + private fun setCheckListText(objCheck:CheckListQuestion, btnSiguiente:Button, text:String){ + objCheck.respuestaRadioBtn = text + checkListQuestionBox.put(objCheck) + btnSiguiente.visibility = if (CheckListRepository().isValidRevision() && vehicleSelectedId != 0L) View.VISIBLE else View.GONE + } + } + + override fun onViewRecycled(holder: ViewHolder) { + super.onViewRecycled(holder) + holder.binding.radioGroup.setOnCheckedChangeListener(null) + holder.binding.spVehiculos.onItemSelectedListener = null + holder.binding.radioGroup.clearCheck() + } +} diff --git a/app/src/main/java/com/iesoluciones/siodrenax/entities/BusinessAnswer.kt b/app/src/main/java/com/iesoluciones/siodrenax/entities/BusinessAnswer.kt new file mode 100644 index 0000000..3444860 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/entities/BusinessAnswer.kt @@ -0,0 +1,54 @@ +package com.iesoluciones.siodrenax.entities + +import com.google.gson.annotations.SerializedName +import com.iesoluciones.siodrenax.models.Answer +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.NameInDb + +@Entity +data class BusinessAnswer( + @Id(assignable = true) + private var id: Long, + + @NameInDb("pregunta_id") + @SerializedName("pregunta_id") + private var idQuestion: Long, + + @NameInDb("nombre") + @SerializedName("nombre") + private var title: String, + + @NameInDb("orden") + @SerializedName("orden") + private var order: Int, + + @NameInDb("tipo_campo") + @SerializedName("tipo_campo") + private var type: String, + +): Answer() { + override fun getId(): Long { + return id + } + + override fun getIdQuestion(): Long { + return idQuestion + } + + override fun getTitle(): String { + return title + } + + override fun getOrder(): Int { + return order + } + + override fun getType(): String { + return type + } + + fun setId(id: Long){ + this.id = id + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/entities/BusinessQuestion.kt b/app/src/main/java/com/iesoluciones/siodrenax/entities/BusinessQuestion.kt new file mode 100644 index 0000000..1acb0d5 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/entities/BusinessQuestion.kt @@ -0,0 +1,54 @@ +package com.iesoluciones.siodrenax.entities + +import com.google.gson.annotations.SerializedName +import com.iesoluciones.siodrenax.models.Question +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.NameInDb + +@Entity +data class BusinessQuestion( + @Id(assignable = true) + private var id: Long, + + @NameInDb("nombre") + @SerializedName("nombre") + private var title: String, + + @NameInDb("orden") + @SerializedName("orden") + private var order: Int, + + @NameInDb("mostrar_numero") + @SerializedName("mostrar_numero") + private var showNumber: Int, + + @NameInDb("obligatorio") + @SerializedName("obligatorio") + private var required: Boolean, + +): Question() { + override fun getId(): Long { + return id + } + + override fun getTitle(): String { + return title + } + + override fun getOrder(): Int { + return order + } + + override fun getShowNumber(): Int { + return showNumber + } + + override fun getRequired(): Boolean { + return required + } + + fun setId(id: Long){ + this.id = id + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/entities/CheckListQuestion.kt b/app/src/main/java/com/iesoluciones/siodrenax/entities/CheckListQuestion.kt new file mode 100644 index 0000000..8a145c4 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/entities/CheckListQuestion.kt @@ -0,0 +1,51 @@ +package com.iesoluciones.siodrenax.entities + +import com.google.gson.annotations.SerializedName +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.NameInDb +import javax.annotation.Nullable + +@Entity +data class CheckListQuestion( + @Id(assignable = true) + var id: Long, + + var nombre: String, + + @NameInDb("tipo_radio_btn") + @SerializedName("tipo_radio_btn") + var tipoRadioBtn: Int, + + @NameInDb("tipo_text") + @SerializedName("tipo_text") + var tipoText: Int, + + @NameInDb("tipo_checkbox") + @SerializedName("tipo_checkbox") + var tipoCheckBox: Int, + + @NameInDb("tipo") + @SerializedName("tipo") + var tipo: String, + + @Nullable + @NameInDb("respuesta_radio_btn") + @SerializedName("respuesta_radio_btn") + var respuestaRadioBtn: String? = null, + + @Nullable + @NameInDb("respuesta_text") + @SerializedName("respuesta_text") + var respuestaText: String? = null, + + @Nullable + @NameInDb("respuesta_checkbox") + @SerializedName("respuesta_checkbox") + var respuestaCheckBox: Boolean = false, + + @Nullable + @NameInDb("fecha") + @SerializedName("fecha") + var fecha: String +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/entities/DomesticAnswer.kt b/app/src/main/java/com/iesoluciones/siodrenax/entities/DomesticAnswer.kt new file mode 100644 index 0000000..1fec2f2 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/entities/DomesticAnswer.kt @@ -0,0 +1,52 @@ +package com.iesoluciones.siodrenax.entities + +import com.google.gson.annotations.SerializedName +import com.iesoluciones.siodrenax.models.Answer +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.NameInDb + +@Entity +data class DomesticAnswer( + @Id(assignable = true) + private var id: Long, + + @NameInDb("pregunta_id") + @SerializedName("pregunta_id") + private var idQuestion: Long, + + @NameInDb("nombre") + @SerializedName("nombre") + private var title: String, + + @NameInDb("orden") + @SerializedName("orden") + private var order: Int, + + @NameInDb("tipo_campo") + @SerializedName("tipo_campo") + private var type: String, +): Answer() { + override fun getId(): Long { + return id + } + + override fun getIdQuestion(): Long { + return idQuestion + } + + override fun getTitle(): String { + return title + } + + override fun getOrder(): Int { + return order + } + + override fun getType(): String { + return type + } + fun setId(id: Long){ + this.id = id + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/entities/DomesticQuestion.kt b/app/src/main/java/com/iesoluciones/siodrenax/entities/DomesticQuestion.kt new file mode 100644 index 0000000..b2fc3ac --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/entities/DomesticQuestion.kt @@ -0,0 +1,54 @@ +package com.iesoluciones.siodrenax.entities + +import com.google.gson.annotations.SerializedName +import com.iesoluciones.siodrenax.models.Question +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.NameInDb + +@Entity +class DomesticQuestion( + @Id(assignable = true) + private var id: Long, + + @NameInDb("nombre") + @SerializedName("nombre") + private var title: String, + + @NameInDb("orden") + @SerializedName("orden") + private var order: Int, + + @NameInDb("mostrar_numero") + @SerializedName("mostrar_numero") + private var showNumber: Int, + + @NameInDb("obligatorio") + @SerializedName("obligatorio") + private var required: Boolean, + +):Question() { + override fun getId(): Long { + return id + } + + override fun getTitle(): String { + return title + } + + override fun getOrder(): Int { + return order + } + + override fun getShowNumber(): Int { + return showNumber + } + + override fun getRequired(): Boolean { + return required + } + + fun setId(id: Long){ + this.id = id + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/entities/Evidence.kt b/app/src/main/java/com/iesoluciones/siodrenax/entities/Evidence.kt new file mode 100644 index 0000000..a63e1c6 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/entities/Evidence.kt @@ -0,0 +1,48 @@ +package com.iesoluciones.siodrenax.entities + +import com.iesoluciones.siodrenax.utils.Constants.Companion.DEFAULT_LATITUDE +import com.iesoluciones.siodrenax.utils.Constants.Companion.DEFAULT_LONGITUDE +import com.iesoluciones.siodrenax.utils.Constants.Companion.EVIDENCE_FINAL +import com.iesoluciones.siodrenax.utils.Constants.Companion.EVIDENCE_PROCESS +import com.iesoluciones.siodrenax.utils.Constants.Companion.EVIDENCE_START +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id + +@Entity +data class Evidence( + @Id + var id: Long, + var evidenceNo: Long, + var idOrder: Long, + var idRequest: Long, + var type: Int, + var path: String?, + var name: String?, + var lat: String, + var lng: String, + var viewRef: Int, + var isSent: Boolean +){ + constructor() : this( + 0, + 0, + 0, + 0, + 0, + null, + null, + DEFAULT_LATITUDE.toString(), + DEFAULT_LONGITUDE.toString(), + 0, + false + ) + + fun getTypeDescription(): String { + return when (this.type) { + EVIDENCE_START -> "Inicio" + EVIDENCE_PROCESS -> "Proceso" + EVIDENCE_FINAL -> "Final" + else -> "" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/entities/NegativeServiceReason.kt b/app/src/main/java/com/iesoluciones/siodrenax/entities/NegativeServiceReason.kt new file mode 100644 index 0000000..fdc0e4c --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/entities/NegativeServiceReason.kt @@ -0,0 +1,29 @@ +package com.iesoluciones.siodrenax.entities + +import com.iesoluciones.siodrenax.models.Reason +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id + +@Entity +data class NegativeServiceReason( + @Id(assignable = true) + private var id: Long, + private var descripcion: String + +) : Reason() { + override fun getId(): Long { + return id + } + + override fun getTitle(): String { + return descripcion + } + + fun setId(id: Long){ + this.id = id + } + + fun getDescripcion(): String{ + return descripcion + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/entities/NextDayOrder.kt b/app/src/main/java/com/iesoluciones/siodrenax/entities/NextDayOrder.kt new file mode 100644 index 0000000..73ea026 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/entities/NextDayOrder.kt @@ -0,0 +1,167 @@ +package com.iesoluciones.siodrenax.entities + +import com.google.gson.annotations.SerializedName +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id + +@Entity +data class NextDayOrder ( + @Id(assignable = true) + @SerializedName("id") + var id: Long, + + @SerializedName("servicio_id") + var idService: Long, + + @SerializedName("solicitud_servicio_id") + var idRequest: Long, + + @SerializedName("servicio_nombre") + var serviceName: String, + + @SerializedName("estatus_servicio_id") + var idServiceStatus: Long, + + @SerializedName("estatus_servicio_nombre") + var serviceStatusName: String, + + @SerializedName("estatus_servicio_color_1") + var serviceStatusColor1: String, + + @SerializedName("estatus_servicio_color_2") + var serviceStatusColor2: String? = null, + + @SerializedName("forma_pago_id") + var idPayMethod: Long, + + @SerializedName("forma_pago_nombre") + var payMethodName: String, + + @SerializedName("tipo_servicio_id") + var idServiceType: Long, + + @SerializedName("observacion_atencion_cliente") + var comments: String?, + + @SerializedName("costo_servicio") + var cost: String, + + @SerializedName("tipo_servicio_nombre") + var serviceTypeName: String, + + @SerializedName("fecha_agenda") + var dateScheduled: String, + + @SerializedName("fecha_solicitud") + var dateServiceRequest: String, + + @SerializedName("usuario_agenda_id") + var idUserSchedule: Long, + + @SerializedName("usuario_agenda_nombre") + var userScheduleName: String, + + @SerializedName("usuario_agenda_apellido_paterno") + var userScheduleLastName: String, + + @SerializedName("usuario_agenda_apellido_materno") + var getUserScheduleMothersLastName: String, + + @SerializedName("duracion") + var duracion: String, + + @SerializedName("definido_cliente") + var clientDefined: Long, + + @SerializedName("cliente_id") + var idClient: Long, + + @SerializedName("denominacion") + var clientDenomination: String, + + @SerializedName("clientes_nombre_responsable_sucursal") + var clientContactName: String, + + @SerializedName("clientes_celular_responsable") + var getClientContactCellphone: String, + + @SerializedName("cliente_domicilio_id") + var idClientAddress: Long, + + @SerializedName("clientes_calle") + var streetAddress: String? = null, + + @SerializedName("clientes_num_ext") + var extNumberAddress: String? = null, + + @SerializedName("clientes_num_int") + var intNumberAddress: String? = null, + + @SerializedName("clientes_colonia") + var neighborhoodNameAddress: String? = null, + + @SerializedName("clientes_cp") + var clientPostalCodeAddress: String? = null, + + @SerializedName("clientes_telefono") + var clientPhone: String, + + @SerializedName("clientes_lat") + var clientLat: String, + + @SerializedName("clientes_lng") + var clientLng: String, + + @SerializedName("operador_id") + var idOperator: Long, + + @SerializedName("operador_nombre") + var operatorName: String, + + @SerializedName("operador_apellido_paterno") + var operatorLastName: String, + + @SerializedName("operador_apellido_materno") + var operatorMothersName: String, + + @SerializedName("vehiculo_id") + var idVehicle: Long, + + @SerializedName("vehiculo_num_economico") + var vehicleCodeName: String, + + @SerializedName("vehiculo_sucursal") + var vehicleOfficeName: String, + + @SerializedName("vehiculo_sucursal_id") + var idVehicleOffice: Long, + + @SerializedName("operador_sucursal") + var operatorOfficeName: String, + + @SerializedName("operador_sucursal_id") + var idOperatorOffice: Long, + + @SerializedName("auxiliar_1_id") + var idAssistant1: Long, + + @SerializedName("auxiliar_2_id") + var idAssistant2: Long, + + @SerializedName("auxiliar_1") + var assistant1Name: String? = null, + + @SerializedName("auxiliar_2") + var assistant2Name: String? = null, + + @SerializedName("sucursal_auxiliar_1") + var assistant1OfficeName: String? = null, + + @SerializedName("sucursal_auxiliar_2") + var assistant2OfficeName: String? = null, + + @SerializedName("requiere_encuesta") + var SurveyRequired: Int, + + var isSent: Boolean, +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/entities/Operator.kt b/app/src/main/java/com/iesoluciones/siodrenax/entities/Operator.kt new file mode 100644 index 0000000..d922f29 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/entities/Operator.kt @@ -0,0 +1,31 @@ +package com.iesoluciones.siodrenax.entities + +import com.google.gson.annotations.SerializedName +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.NameInDb + +@Entity +data class Operator( + @Id(assignable = true) var id: Long, + + @NameInDb("nombre") + @SerializedName("nombre") + var name: String, + + @NameInDb("apellido_paterno") + @SerializedName("apellido_paterno") + var lastName: String, + + @NameInDb("apellido_materno") + @SerializedName("apellido_materno") + var mothersName: String, + + @NameInDb("servicios_total") + @SerializedName("servicios_total") + var totalOrders: Long, + + @NameInDb("servicios_pendiente") + @SerializedName("servicios_pendiente") + var pendingOrders: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/entities/Order.kt b/app/src/main/java/com/iesoluciones/siodrenax/entities/Order.kt new file mode 100644 index 0000000..b9a6aa0 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/entities/Order.kt @@ -0,0 +1,190 @@ +package com.iesoluciones.siodrenax.entities + +import com.google.gson.annotations.SerializedName +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id + +@Entity +data class Order ( + @Id(assignable = true) + @SerializedName("id") + var id: Long, + + @SerializedName("servicio_id") + var idService: Long, + + @SerializedName("solicitud_servicio_id") + var idRequest: Long, + + @SerializedName("servicio_nombre") + var serviceName: String, + + @SerializedName("estatus_servicio_id") + var idServiceStatus: Long, + + @SerializedName("estatus_servicio_nombre") + var serviceStatusName: String, + + @SerializedName("estatus_servicio_color_1") + var serviceStatusColor1: String, + + @SerializedName("estatus_servicio_color_2") + var serviceStatusColor2: String? = null, + + @SerializedName("forma_pago_id") + var idPayMethod: Long, + + @SerializedName("forma_pago_nombre") + var payMethodName: String, + + @SerializedName("tipo_servicio_id") + var idServiceType: Long, + + @SerializedName("observacion_atencion_cliente") + var comments: String?, + + @SerializedName("costo_servicio") + var cost: String, + + @SerializedName("costo_servicio_negativo") + var negativeCost: String, + + @SerializedName("tipo_servicio_nombre") + var serviceTypeName: String, + + @SerializedName("fecha_agenda") + var dateScheduled: String, + + @SerializedName("fecha_solicitud") + var dateServiceRequest: String, + + @SerializedName("usuario_agenda_id") + var idUserSchedule: Long, + + @SerializedName("usuario_agenda_nombre") + var userScheduleName: String, + + @SerializedName("usuario_agenda_apellido_paterno") + var userScheduleLastName: String, + + @SerializedName("usuario_agenda_apellido_materno") + var getUserScheduleMothersLastName: String, + + @SerializedName("duracion") + var duracion: String, + + @SerializedName("definido_cliente") + var clientDefined: Long, + + @SerializedName("cliente_id") + var idClient: Long, + + @SerializedName("denominacion") + var clientDenomination: String, + + @SerializedName("clientes_nombre_responsable_sucursal") + var clientContactName: String, + + @SerializedName("clientes_celular_responsable") + var getClientContactCellphone: String, + + @SerializedName("cliente_domicilio_id") + var idClientAddress: Long, + + @SerializedName("clientes_calle") + var streetAddress: String? = null, + + @SerializedName("clientes_num_ext") + var extNumberAddress: String? = null, + + @SerializedName("clientes_num_int") + var intNumberAddress: String? = null, + + @SerializedName("clientes_colonia") + var neighborhoodNameAddress: String? = null, + + @SerializedName("clientes_cp") + var clientPostalCodeAddress: String? = null, + + @SerializedName("clientes_ciudad") + var clientCity: String, + + @SerializedName("clientes_telefono") + var clientPhone: String, + + @SerializedName("clientes_lat") + var clientLat: String, + + @SerializedName("clientes_lng") + var clientLng: String, + + @SerializedName("operador_id") + var idOperator: Long, + + @SerializedName("operador_nombre") + var operatorName: String, + + @SerializedName("operador_apellido_paterno") + var operatorLastName: String, + + @SerializedName("operador_apellido_materno") + var operatorMothersName: String, + + @SerializedName("vehiculo_id") + var idVehicle: Long, + + @SerializedName("vehiculo_num_economico") + var vehicleCodeName: String, + + @SerializedName("vehiculo_sucursal") + var vehicleOfficeName: String, + + @SerializedName("vehiculo_sucursal_id") + var idVehicleOffice: Long, + + @SerializedName("operador_sucursal") + var operatorOfficeName: String, + + @SerializedName("operador_sucursal_id") + var idOperatorOffice: Long, + + @SerializedName("auxiliar_1_id") + var idAssistant1: Long, + + @SerializedName("auxiliar_2_id") + var idAssistant2: Long, + + @SerializedName("auxiliar_1") + var assistant1Name: String? = null, + + @SerializedName("auxiliar_1_apellido_paterno") + var assistant1LastName: String? = null, + + @SerializedName("auxiliar_1_apellido_materno") + var assistant1MothersName: String? = null, + + @SerializedName("auxiliar_2") + var assistant2Name: String? = null, + + @SerializedName("sucursal_auxiliar_1") + var assistant1OfficeName: String? = null, + + @SerializedName("sucursal_auxiliar_2") + var assistant2OfficeName: String? = null, + + @SerializedName("requiere_encuesta") + var SurveyRequired: Int, + + var isSent: Boolean, + + @SerializedName("pdf_path") + var pdfPath: String?, + + @SerializedName("nombre_croquis") + var sketchName: String?, + + var sketchPath: String?, + + @SerializedName("nombre_sucursal") + var branchName: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/entities/OrderProgress.kt b/app/src/main/java/com/iesoluciones/siodrenax/entities/OrderProgress.kt new file mode 100644 index 0000000..72ff7c5 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/entities/OrderProgress.kt @@ -0,0 +1,54 @@ +package com.iesoluciones.siodrenax.entities + +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id + +@Entity +data class OrderProgress ( + + @Id(assignable = true) + var id: Long, + var idRequest: Long, + var startDate: String, + var endDate: String?, + var comments: String?, + var elapsedTime: String?, + var startLat: String, + var startLng: String, + var endLat: String, + var endLng: String, + var signaturePath: String?, + var signatureName: String?, + var warranty: Boolean, + var isSent: Boolean, + var shouldBeSent: Boolean, + var signatureSent: Boolean, + var isOnSurvey: Boolean, + var shouldSendSurvey: Boolean, + var surveySent: Boolean, + var liters: Int, + var negativeService: Int, +){ + constructor() : this(0, + 0, + "", + null, + "", + null, + "", + "", + "", + "", + null, + null, + false, + false, + false, + false, + false, + false, + false, + 0, + 0 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/entities/SavedAnswer.kt b/app/src/main/java/com/iesoluciones/siodrenax/entities/SavedAnswer.kt new file mode 100644 index 0000000..3a1cb31 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/entities/SavedAnswer.kt @@ -0,0 +1,21 @@ +package com.iesoluciones.siodrenax.entities + +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id + +@Entity +data class SavedAnswer( + @Id(assignable = true) + var id: Long, + var questionId: Long, + var orderId: Long, + var isOpen: Boolean, + var answer: String? +){ + constructor() : this(0, + 0, + 0, + false, + null + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/entities/User.kt b/app/src/main/java/com/iesoluciones/siodrenax/entities/User.kt new file mode 100644 index 0000000..c3a12ab --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/entities/User.kt @@ -0,0 +1,32 @@ +package com.iesoluciones.siodrenax.entities + +import com.google.gson.annotations.SerializedName +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.NameInDb + +@Entity +data class User( + @Id(assignable = true) var id: Long, + var email: String, + + @NameInDb("nombre") + @SerializedName("nombre") + var username: String, + + @NameInDb("apellido_paterno") + @SerializedName("apellido_paterno") + var lastName: String, + + @NameInDb("apellido_materno") + @SerializedName("apellido_materno") + var mothersName: String, + + @NameInDb("telefono") + @SerializedName("telefono") + var phone: String, + + @NameInDb("tipo_empleado_id") + @SerializedName("tipo_empleado_id") + var idEmployeeType: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/entities/Vehicle.kt b/app/src/main/java/com/iesoluciones/siodrenax/entities/Vehicle.kt new file mode 100644 index 0000000..d14bed5 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/entities/Vehicle.kt @@ -0,0 +1,10 @@ +package com.iesoluciones.siodrenax.entities + +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id + +@Entity +data class Vehicle( + @Id(assignable = true) var id: Long, + var nombre: String +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/fragments/dialogs/IncidentsInputFragment.kt b/app/src/main/java/com/iesoluciones/siodrenax/fragments/dialogs/IncidentsInputFragment.kt new file mode 100644 index 0000000..26debe6 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/fragments/dialogs/IncidentsInputFragment.kt @@ -0,0 +1,82 @@ +package com.iesoluciones.siodrenax.fragments.dialogs + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.databinding.FragmentIncidentsInputBinding +import com.iesoluciones.siodrenax.interfaces.OnIncidentListener + +class IncidentsInputFragment : DialogFragment(){ + + private lateinit var _binding: FragmentIncidentsInputBinding + private val binding get() = _binding + lateinit var listener: OnIncidentListener + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + + _binding = FragmentIncidentsInputBinding.inflate(inflater, container, false) + + binding.tvContinue.setOnClickListener { onClickTvContinue() } + binding.tvCancel.setOnClickListener { onClickTvCancel() } + + return binding.root + } + + private fun onClickTvContinue() { + val editIncident = binding.editIncident + + if (isEmpty(editIncident)) { + binding.labelIncident.error = getText(R.string.incident_dialog_error) + listener.onIncidentInput(null) + } else { + listener.onIncidentInput(editIncident.text.toString().replace(",".toRegex(), "")) + dismiss() + } + } + + private fun onClickTvCancel() { + listener.onNoIncident() + dismiss() + } + + override fun show(manager: FragmentManager, tag: String?) { + try { + val ft = manager.beginTransaction() + ft.add(this, tag) + ft.commitAllowingStateLoss() + } catch (e: IllegalStateException) { + Log.e("Exception", e.toString()) + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + + listener = if (activity is OnIncidentListener) { + (activity as OnIncidentListener?)!! + } else throw RuntimeException("Parent Activity not implementing OnIncidentListener") + } + + private fun isEmpty(incident: EditText): Boolean { + return incident.text.toString().trim { it <= ' ' }.isEmpty() || incident.text == null + } + + companion object { + fun newInstance(listener: OnIncidentListener): IncidentsInputFragment { + val fragment = IncidentsInputFragment() + fragment.listener = listener + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/fragments/dialogs/MileageInputDialogFragment.kt b/app/src/main/java/com/iesoluciones/siodrenax/fragments/dialogs/MileageInputDialogFragment.kt new file mode 100644 index 0000000..01c5d68 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/fragments/dialogs/MileageInputDialogFragment.kt @@ -0,0 +1,86 @@ +package com.iesoluciones.siodrenax.fragments.dialogs + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.databinding.FragmentMileageInputDialogBinding +import com.iesoluciones.siodrenax.interfaces.OnMileageListener +import com.iesoluciones.siodrenax.utils.NumberFormatterTextWatcher + +class MileageInputDialogFragment : DialogFragment() { + + private lateinit var _binding: FragmentMileageInputDialogBinding + private val binding get() = _binding + lateinit var listener: OnMileageListener + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentMileageInputDialogBinding.inflate(inflater, container, false) + val view = binding.root + isCancelable = false + binding.labelMileage.suffixText = getText(R.string.mileage_dialog_suffix) + + binding.tvContinue.setOnClickListener { + if (isEmpty(binding.editMileage)) { + binding.labelMileage.error = getText(R.string.mileage_dialog_error) + listener.onMileageInput(null) + } else { + listener.onMileageInput( + binding.editMileage.text.toString().replace(",".toRegex(), "") + ) + dismiss() + } + } + + binding.tvCancel.setOnClickListener { + dismiss() + } + + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.labelMileage.suffixText = getText(R.string.mileage_dialog_suffix) + binding.editMileage.addTextChangedListener(NumberFormatterTextWatcher(binding.editMileage)) + } + + override fun show(manager: FragmentManager, tag: String?) { + try { + val ft = manager.beginTransaction() + ft.add(this, tag) + ft.commitAllowingStateLoss() + } catch (e: IllegalStateException) { + Log.e("Exception", e.toString()) + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + listener = if (activity is OnMileageListener) { + activity as OnMileageListener + } else throw RuntimeException("Parent Activity not implementing OnMileageListener") + } + + private fun isEmpty(mileage: EditText?): Boolean { + return mileage!!.text.toString().trim { it <= ' ' }.isEmpty() || mileage.text == null + } + + companion object { + fun newInstance(listener: OnMileageListener): MileageInputDialogFragment { + val fragment = MileageInputDialogFragment() + fragment.listener = listener + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/fragments/dialogs/VehicleIncidentsFragment.kt b/app/src/main/java/com/iesoluciones/siodrenax/fragments/dialogs/VehicleIncidentsFragment.kt new file mode 100644 index 0000000..4e53000 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/fragments/dialogs/VehicleIncidentsFragment.kt @@ -0,0 +1,66 @@ +package com.iesoluciones.siodrenax.fragments.dialogs + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import com.iesoluciones.siodrenax.databinding.FragmentVehicleIncidentsBinding +import com.iesoluciones.siodrenax.interfaces.OnIncidentShowListener + +class VehicleIncidentsFragment : DialogFragment() { + + private lateinit var _binding: FragmentVehicleIncidentsBinding + private val binding get() = _binding + lateinit var listener: OnIncidentShowListener + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentVehicleIncidentsBinding.inflate(inflater, container, false) + val view = binding.root + + binding.tvIncidence.text = incidence + isCancelable = false + + binding.tvContinue.setOnClickListener{ + listener.onIncidenceSolved() + dismiss() + } + + binding.tvCancel.setOnClickListener{ + listener.onIncidenceNotSolved() + dismiss() + } + + return view + } + + override fun show(manager: FragmentManager, tag: String?) { + try { + val ft = manager.beginTransaction() + ft.add(this, tag) + ft.commitAllowingStateLoss() + } catch (e: IllegalStateException) { + Log.e("Exception", e.toString()) + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + listener = if (activity is OnIncidentShowListener) { + activity as OnIncidentShowListener + } else throw RuntimeException("Parent Activity not implementing OnIncidentListener") + } + + companion object { + var incidence: String? = null + fun newInstance(listener: OnIncidentShowListener, incidence: String?): VehicleIncidentsFragment { + val fragment = VehicleIncidentsFragment() + fragment.listener = listener + Companion.incidence = incidence + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/interfaces/OnIncidentListener.kt b/app/src/main/java/com/iesoluciones/siodrenax/interfaces/OnIncidentListener.kt new file mode 100644 index 0000000..ce1e5b2 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/interfaces/OnIncidentListener.kt @@ -0,0 +1,6 @@ +package com.iesoluciones.siodrenax.interfaces + +interface OnIncidentListener { + fun onNoIncident() + fun onIncidentInput(incident: String?) +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/interfaces/OnIncidentShowListener.kt b/app/src/main/java/com/iesoluciones/siodrenax/interfaces/OnIncidentShowListener.kt new file mode 100644 index 0000000..8dacd7a --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/interfaces/OnIncidentShowListener.kt @@ -0,0 +1,6 @@ +package com.iesoluciones.siodrenax.interfaces + +interface OnIncidentShowListener { + fun onIncidenceSolved() + fun onIncidenceNotSolved() +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/interfaces/OnMileageListener.kt b/app/src/main/java/com/iesoluciones/siodrenax/interfaces/OnMileageListener.kt new file mode 100644 index 0000000..ddda224 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/interfaces/OnMileageListener.kt @@ -0,0 +1,5 @@ +package com.iesoluciones.siodrenax.interfaces + +interface OnMileageListener { + fun onMileageInput(mileage: String?) +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/Answer.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/Answer.kt new file mode 100644 index 0000000..d932ddc --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/Answer.kt @@ -0,0 +1,10 @@ +package com.iesoluciones.siodrenax.models + +abstract class Answer { + abstract fun getId(): Long + abstract fun getIdQuestion(): Long + abstract fun getTitle(): String + abstract fun getOrder(): Int + abstract fun getType(): String + +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/AnswerPOJO.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/AnswerPOJO.kt new file mode 100644 index 0000000..232e5ac --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/AnswerPOJO.kt @@ -0,0 +1,30 @@ +package com.iesoluciones.siodrenax.models + +import com.google.gson.annotations.SerializedName +import com.iesoluciones.siodrenax.entities.BusinessAnswer +import com.iesoluciones.siodrenax.entities.DomesticAnswer + +data class AnswerPOJO( + @SerializedName("id") + var id: Long, + + @SerializedName("pregunta_id") + var idQuestion: Long, + + @SerializedName("nombre") + var title: String, + + @SerializedName("orden") + var order: Int, + + @SerializedName("tipo_campo") + var type: String, +){ + fun getDomesticAnswer(): DomesticAnswer { + return DomesticAnswer(id, idQuestion, title, order, type) + } + + fun getBusinessAnswer(): BusinessAnswer { + return BusinessAnswer(id, idQuestion, title, order, type) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/CheckBoxAnswer.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/CheckBoxAnswer.kt new file mode 100644 index 0000000..c2e2d07 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/CheckBoxAnswer.kt @@ -0,0 +1,11 @@ +package com.iesoluciones.siodrenax.models + +import com.google.gson.annotations.SerializedName + +data class CheckBoxAnswer ( + @SerializedName("pregunta_id") + var questionId: String, + + @SerializedName("respuesta_id") + var answerId: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/EvidenceRequest.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/EvidenceRequest.kt new file mode 100644 index 0000000..00313eb --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/EvidenceRequest.kt @@ -0,0 +1,18 @@ +package com.iesoluciones.siodrenax.models + +import com.google.gson.annotations.SerializedName + +data class EvidenceRequest( + + @SerializedName("nombre") + var name: String, + + @SerializedName("etapa") + var stage: String, + + @SerializedName("lat") + var lat: String, + + @SerializedName("lng") + var lng: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/EvidenceSignatureLocal.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/EvidenceSignatureLocal.kt new file mode 100644 index 0000000..6de8730 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/EvidenceSignatureLocal.kt @@ -0,0 +1,6 @@ +package com.iesoluciones.siodrenax.models + +data class EvidenceSignatureLocal( + var path: String?, + var name: String?, +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/FinishOrderRequest.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/FinishOrderRequest.kt new file mode 100644 index 0000000..8686746 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/FinishOrderRequest.kt @@ -0,0 +1,44 @@ +package com.iesoluciones.siodrenax.models + +import com.google.gson.annotations.SerializedName +import javax.annotation.Nullable + +data class FinishOrderRequest( + + @SerializedName("servicio_det_id") + var idOrder: String, + + @SerializedName("servicio_enc_id") + var idRequest: String, + + @SerializedName("fecha_fin_celular") + var endDate: String, + + @SerializedName("lat_fin") + var endLat: String, + + @SerializedName("lng_fin") + var endLng: String, + + @SerializedName("duracion") + var elapsedTime: String, + + @SerializedName("comentarios") + var comments: String, + + @SerializedName("aplica_garantia") + var warranty: Int, + + @SerializedName("litraje") + var liters: Int, + + @SerializedName("cat_motivos_estatus_id") + var negativeService: Int, + + @Nullable + @SerializedName("encuesta") + var survey: List?, + + @SerializedName("recibo") + var receipt: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/GenericResponse.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/GenericResponse.kt new file mode 100644 index 0000000..547c39a --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/GenericResponse.kt @@ -0,0 +1,8 @@ +package com.iesoluciones.siodrenax.models + +import com.google.gson.annotations.SerializedName + +data class GenericResponse ( + @SerializedName("result") + internal var response: String +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/KeyValue.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/KeyValue.kt new file mode 100644 index 0000000..8741f98 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/KeyValue.kt @@ -0,0 +1,18 @@ +package com.iesoluciones.siodrenax.models + +import com.google.gson.annotations.SerializedName + +data class KeyValue ( + @SerializedName("llave") + var key: String, + + @SerializedName("valor") + var value: String +){ + override fun toString(): String { + return "KeyValue{" + + "key='" + key + '\'' + + ", value='" + value + '\'' + + '}' + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/LoginResponse.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/LoginResponse.kt new file mode 100644 index 0000000..f59a1d5 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/LoginResponse.kt @@ -0,0 +1,23 @@ +package com.iesoluciones.siodrenax.models + +import com.google.gson.annotations.SerializedName +import com.iesoluciones.siodrenax.entities.User + +data class LoginResponse ( + @SerializedName("user") + internal var user: User, + + @SerializedName("token") + internal var token: String, + + @SerializedName("parametros") + internal var parmams: List, +) { + override fun toString(): String { + return "LoginResponse{" + + "user=" + user + + ", token='" + token + '\'' + + ", parmams=" + parmams + + '}' + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/OpenAnswer.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/OpenAnswer.kt new file mode 100644 index 0000000..cac4284 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/OpenAnswer.kt @@ -0,0 +1,11 @@ +package com.iesoluciones.siodrenax.models + +import com.google.gson.annotations.SerializedName + +data class OpenAnswer ( + @SerializedName("pregunta_id") + var questionId: String, + + @SerializedName("respuesta") + var answer: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/OrderPdf.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/OrderPdf.kt new file mode 100644 index 0000000..ba6a39a --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/OrderPdf.kt @@ -0,0 +1,25 @@ +package com.iesoluciones.siodrenax.models + +data class OrderPdf( + var requestId: Long, + var client: String, + var date: String, + var address: String, + var city: String, + var cellphone: String, + var contactName: String, + var service: String, + var observations: String, + var warrantyApplied: String, + var cost: String, + var negativeCost: String, + var startTime: String, + var elapsedTime: String, + var finishTime: String, + var advisor: String, + var branchOffice: String, + var sign: String, + var auxiliar: String, + var vehicle: String, + var evidences: MutableMap> +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/Question.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/Question.kt new file mode 100644 index 0000000..2571ad9 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/Question.kt @@ -0,0 +1,9 @@ +package com.iesoluciones.siodrenax.models + +abstract class Question { + abstract fun getId(): Long + abstract fun getTitle(): String + abstract fun getOrder(): Int + abstract fun getShowNumber(): Int + abstract fun getRequired(): Boolean +} diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/QuestionPOJO.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/QuestionPOJO.kt new file mode 100644 index 0000000..dada30c --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/QuestionPOJO.kt @@ -0,0 +1,33 @@ +package com.iesoluciones.siodrenax.models + +import com.google.gson.annotations.SerializedName +import com.iesoluciones.siodrenax.entities.BusinessQuestion +import com.iesoluciones.siodrenax.entities.DomesticQuestion + +data class QuestionPOJO( + @SerializedName("id") + var id: Long, + + @SerializedName("nombre") + var title: String, + + @SerializedName("orden") + var order: Int, + + @SerializedName("mostrar_numero") + var showNumber: Int, + + @SerializedName("obligatorio") + var required: Int, + + @SerializedName("respuestas") + var answers: List +){ + fun getDomesticQuestion(): DomesticQuestion { + return DomesticQuestion(id, title, order, showNumber, (required == 1)) + } + + fun getBusinessQuestion(): BusinessQuestion { + return BusinessQuestion(id, title, order, showNumber, (required == 1)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/Reason.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/Reason.kt new file mode 100644 index 0000000..1ab3bf0 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/Reason.kt @@ -0,0 +1,6 @@ +package com.iesoluciones.siodrenax.models + +abstract class Reason { + abstract fun getId(): Long + abstract fun getTitle(): String +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/StartWorkdayResponse.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/StartWorkdayResponse.kt new file mode 100644 index 0000000..62f00f7 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/StartWorkdayResponse.kt @@ -0,0 +1,47 @@ +package com.iesoluciones.siodrenax.models + +import com.google.gson.annotations.SerializedName +import com.iesoluciones.siodrenax.entities.BusinessQuestion +import com.iesoluciones.siodrenax.entities.DomesticQuestion +import com.iesoluciones.siodrenax.entities.NegativeServiceReason +import com.iesoluciones.siodrenax.entities.Order +import java.util.* + +data class StartWorkdayResponse( + + @SerializedName("servicios") + var orders: List, + + @SerializedName("parametros") + var keyValues: List, + + @SerializedName("jornada") + var workdayInfo: Workday, + + @SerializedName("encuesta_domestico") + var domesticSurvey: List, + + @SerializedName("encuesta_empresarial") + var businessSurvey: List, + + @SerializedName("motivos_estatus") + var negativeServiceReason: List +){ + fun parseDomesticSurvey(): List { + val survey: MutableList = ArrayList() + for (q in domesticSurvey) { + val question: DomesticQuestion = q.getDomesticQuestion() + survey.add(question) + } + return survey + } + + fun parseBussinesSurvey(): List { + val survey: MutableList = ArrayList() + for (q in businessSurvey) { + val question: BusinessQuestion = q.getBusinessQuestion() + survey.add(question) + } + return survey + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/ValidationOrderAnswer.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/ValidationOrderAnswer.kt new file mode 100644 index 0000000..cfcb41c --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/ValidationOrderAnswer.kt @@ -0,0 +1,5 @@ +package com.iesoluciones.siodrenax.models + +data class ValidationOrderAnswer( + var servicio_id: String +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/VehicleIncidenceResponse.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/VehicleIncidenceResponse.kt new file mode 100644 index 0000000..0de7270 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/VehicleIncidenceResponse.kt @@ -0,0 +1,18 @@ +package com.iesoluciones.siodrenax.models + +import com.google.gson.annotations.SerializedName + +data class VehicleIncidenceResponse ( + @SerializedName("id") + var id: Int, + + @SerializedName("descripcion") + var description: String +){ + override fun toString(): String{ + return "Incidence {" + + "id =" + id + + ", descripcion = " + description + + '}' + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/models/Workday.kt b/app/src/main/java/com/iesoluciones/siodrenax/models/Workday.kt new file mode 100644 index 0000000..44ccc29 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/models/Workday.kt @@ -0,0 +1,26 @@ +package com.iesoluciones.siodrenax.models + +import com.google.gson.annotations.SerializedName + +data class Workday ( + @SerializedName("kilometraje_inicial") + var startOdometer: String, + + @SerializedName("lat_ini") + var initLat: String, + + @SerializedName("lng_ini") + var initLng: String, + + @SerializedName("vehiculo_id") + var idVehicle: Long, + + @SerializedName("usuario_id") + var idUser: Long, + + @SerializedName("fecha_hora_ini") + var initDate: String, + + @SerializedName("id") + var idWorkload: Long, +) \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/network/Api.kt b/app/src/main/java/com/iesoluciones/siodrenax/network/Api.kt new file mode 100644 index 0000000..15f014b --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/network/Api.kt @@ -0,0 +1,118 @@ +package com.iesoluciones.siodrenax.network + +import com.iesoluciones.siodrenax.entities.* +import com.iesoluciones.siodrenax.models.* +import io.reactivex.Observable +import okhttp3.ResponseBody +import retrofit2.http.* +import java.util.* + +interface Api { + + @FormUrlEncoded + @POST("login") + fun login( + @Header("Application") appId: String, + @Field("email") email: String, + @Field("password") password: String, + @Field("version_apk") version: String, + @Field("dispositivo_id") deviceId: String + ): Observable + + @GET("operador/checklist") + fun getCheckList( + @Query("cambiar_vehiculo") changeVehicle: Int + ): Observable> + + @GET("operador/checklist/vehiculos") + fun getVehicle(): Observable> + + @FormUrlEncoded + @POST("operador/vehiculos_incidencias") + fun sendVehicleIncidence( + @Field("descripcion") incidence: String? + ): Observable + + @FormUrlEncoded + @POST("operador/vehiculos_incidencias/ultima_incidencia") + fun getVehicleIncidence( + @Field("vehiculo_id") vehicleId: Long + ): Observable + + @PUT("operador/vehiculos_incidencias/{incidencia_id}/resolver") + fun resolveVehicleIncidence( + @Path("incidencia_id") incidenceId: String + ): Observable + + @POST("operador/checklist") + fun sendCheckList( + @Body checkListQuestions: List + ): Observable + + @FormUrlEncoded + @POST("operador/iniciarjornada") + fun startWorkday( + @Field("token_firebase") firebaseToken: String, + @Field("kilometraje_inicial") odometer: String, + @Field("lat_ini") initLat: String, + @Field("lng_ini") initLng: String, + @Field("modelo_celular") phoneModel: String, + @Field("bateria") batteryLevel: String + ): Observable + + @FormUrlEncoded + @POST("operador/finalizarjornada/{bitacora_id}") + fun endWorkday( + @Path("bitacora_id") idWorkday: String, + @Field("kilometraje_final") odometer: String, + @Field("lat_fin") lastLat: String, + @Field("lng_fin") lastLng: String, + @Field("modelo_celular") phoneModel: String, + @Field("bateria") batteryLevel: String + ): Observable + + @GET("operador/solicitud_servicios") + fun getOrders(): Observable> + + @GET("operador/solicitud_servicios/{idOrder}") + fun getOrderInfo( + @Path("idOrder") idWorkday: String + ): Observable + + @FormUrlEncoded + @POST("operador/solicitud_servicios/iniciar") + fun startOrder( + @Field("servicio_det_id") idOrder: String, + @Field("servicio_enc_id") idRequest: String, + @Field("fecha_ini_celular") startDate: String, + @Field("lat_ini") startLat: String, + @Field("lng_ini") startLng: String + ): Observable + + @POST("operador/solicitud_servicios/verificar/servicios") + fun verificarServicio(@Body servicios_id: ArrayList): Observable> + + @POST("operador/solicitud_servicios/finalizar-new") + fun finishOrder( + @Body finishOrderRequest: FinishOrderRequest, + ): Observable + + @GET("supervisoroperaciones/asesores") + fun listOperators(): Observable> + + @GET("supervisoroperaciones/asesores/{asesor_id}/servicios") + fun getOperatorOrders( + @Path("asesor_id") idOperator: String + ): Observable> + + @POST("supervisoroperaciones/iniciarjornada") + fun startWorkdayManager(): Observable + + @POST("supervisoroperaciones/finalizarjornada/{bitacora_id}") + fun endWorkdayManager( + @Path("bitacora_id") workdayId: String + ): Observable + + @GET("operador/servicios/diasiguiente") + fun getNextDayOrders(): Observable> +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/network/HttpError.kt b/app/src/main/java/com/iesoluciones/siodrenax/network/HttpError.kt new file mode 100644 index 0000000..d5ebde0 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/network/HttpError.kt @@ -0,0 +1,35 @@ +package com.iesoluciones.siodrenax.network + +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import retrofit2.HttpException +import java.io.IOException + +internal class HttpError(json: String) { + + @SerializedName("error") + var error: String? = null + + init { + val gson = Gson() + val temp = gson.fromJson(json, HttpError::class.java) + error = temp.error + } + + companion object { + + fun parseException(exception: HttpException): HttpError? { + return try { + HttpError(exception.response().errorBody()!!.source().readUtf8()) + } catch (e: IOException) { + e.printStackTrace() + null + } + } + } + + override fun toString(): String { + return "{error : $error}" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/network/TokenInterceptor.kt b/app/src/main/java/com/iesoluciones/siodrenax/network/TokenInterceptor.kt new file mode 100644 index 0000000..56c7d29 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/network/TokenInterceptor.kt @@ -0,0 +1,166 @@ +package com.iesoluciones.siodrenax.network + +import com.iesoluciones.siodrenax.App +import com.iesoluciones.siodrenax.BuildConfig.BASE_URL +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.preferencesHelper +import com.iesoluciones.siodrenax.userBox +import okhttp3.* +import okhttp3.logging.HttpLoggingInterceptor +import okio.Buffer +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.util.concurrent.TimeUnit + +class TokenInterceptor : Interceptor { + + private var client: OkHttpClient? = null + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val original = chain.request() + val builder = original.newBuilder() + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .addHeader("Authorization", "Bearer " + preferencesHelper.tokenApi) + + val request = builder.build() + val response: Response = chain.proceed(request) + + //Instantiate Logging interceptor + val logging = HttpLoggingInterceptor() + logging.level = HttpLoggingInterceptor.Level.BODY + + //Let's finish last workday started before starting a new one + //Instantiate Http client to use + client = OkHttpClient.Builder() + .addInterceptor(TokenInterceptor()) + .addInterceptor(logging) + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .build() + + when (response.code()) { + 401 -> { + val body1 = response.body()!!.source().readUtf8() + val error = HttpError(body1) + + if (error.error != null) { + if (error.error.toString().equals( + App.shareInstance!!.resources.getString(R.string.token_expired), + true + ) + ) { + val request1 = Request.Builder() + .url(BASE_URL + "refresh") + .build() + val response1 = client!!.newCall(request1).execute() + try { + val json = JSONObject(response1.body()!!.source().readUtf8()) + preferencesHelper.tokenApi = json.getString("token") + val original1 = chain.request() + val builder1 = original1.newBuilder() + val request2 = builder1.build() + return client!!.newCall(request2).execute() + } catch (e: JSONException) { + e.printStackTrace() + } + } else + return response.newBuilder() + .body(ResponseBody.create(response.body()!!.contentType(), body1)) + .code(401).build() + } else + return response.newBuilder() + .body(ResponseBody.create(response.body()!!.contentType(), body1)) + .code(401).build() + } + 420 -> { + val body1 = response.body()!!.source().readUtf8() + + return response.newBuilder() + .body(ResponseBody.create(response.body()!!.contentType(), body1)) + .code(420).build() + } + 422 -> { + val body1 = response.body()!!.source().readUtf8() + val error1 = UnprocessableEntity(body1) + + if (error1.message.contentEquals(App.shareInstance!!.resources.getString(R.string.workday_already_started))) { + + //Extract the request body, the same data of the body will be used to finish pending workday + var requestBody = request.body() + + //Modify some keys to match the one requested by the web service + requestBody = processFormDataRequestBody(requestBody!!) + val response1: Response + + if (userBox.query().build().findUnique()!!.idEmployeeType == 2L) { + //Set up request + val request1 = Request.Builder() + .url( + BASE_URL + "operador/finalizarjornada/" + error1.errors[0] + ) //concatenate id of last workday + .post(requestBody!!) + .build() + //Call response + response1 = client!!.newCall(request1).execute() + } else { + //Set up request + val request1 = Request.Builder() + .url( + BASE_URL + "supervisoroperaciones/finalizarjornada/" + error1.errors[0] + ) //concatenate id of last workday + .post(requestBody!!) + .build() + //Call response + response1 = client!!.newCall(request1).execute() + } + + //Validate succesful workday finish + return if (response1.code() == 200) { + //try again startworkday + client!!.newCall(chain.request()).execute() + } else { + //There was something wrong with the process , just pass the error to main Thread + val body = response1.body()!!.source().readUtf8() + response.newBuilder() + .body(ResponseBody.create(response.body()!!.contentType(), body)) + .code(422).build() + } + + } else { + return response.newBuilder() + .body(ResponseBody.create(response.body()!!.contentType(), body1)) + .code(422).build() + } + } + else -> return response + } + return response + } + + private fun bodyToString(request: RequestBody?): String { + try { + val buffer = Buffer() + if (request != null) + request.writeTo(buffer) + else + return "" + return buffer.readUtf8() + } catch (e: IOException) { + return "did not work" + } + } + + private fun processFormDataRequestBody(requestBody: RequestBody): RequestBody? { + val formBody: RequestBody = FormBody.Builder().build() + var postBodyString = bodyToString(requestBody) + postBodyString += (if (postBodyString.isNotEmpty()) "&" else "") + bodyToString(formBody) + postBodyString = postBodyString.replace("kilometraje_inicial", "kilometraje_final") + postBodyString = postBodyString.replace("lat_ini", "lat_fin") + postBodyString = postBodyString.replace("lng_ini", "lng_fin") + return RequestBody.create(requestBody.contentType(), postBodyString) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/network/UnprocessableEntity.kt b/app/src/main/java/com/iesoluciones/siodrenax/network/UnprocessableEntity.kt new file mode 100644 index 0000000..d5fe464 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/network/UnprocessableEntity.kt @@ -0,0 +1,43 @@ +package com.iesoluciones.siodrenax.network + +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import retrofit2.HttpException +import java.io.IOException + +class UnprocessableEntity(json: String) { + + @SerializedName("message") + var message: String + internal set + @SerializedName("errors") + var errors: List + internal set + + init { + val gson = Gson() + val temp = gson.fromJson(json, UnprocessableEntity::class.java) + message = temp.message + errors = temp.errors + } + + companion object { + + fun parseException(exception: HttpException): UnprocessableEntity? { + return try { + UnprocessableEntity(exception.response().errorBody()!!.source().readUtf8().toString()) + } catch (e: IOException) { + e.printStackTrace() + null + } + } + } + + override fun toString(): String { + return "UnprocessableEntity{" + + "message='" + message + '\''.toString() + + ", errors=" + errors + + '}'.toString() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/receivers/UpdateUIReceiver.kt b/app/src/main/java/com/iesoluciones/siodrenax/receivers/UpdateUIReceiver.kt new file mode 100644 index 0000000..26b0bf9 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/receivers/UpdateUIReceiver.kt @@ -0,0 +1,34 @@ +package com.iesoluciones.siodrenax.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import com.iesoluciones.siodrenax.services.NotificationService.Companion.UPDATE_UI + +class UpdateUIReceiver: BroadcastReceiver() { + + private var listener: UiUpdateListener? = null + + interface UiUpdateListener { + fun updateUI() + } + + fun updateUIReceiver(listener: UiUpdateListener) { + this.listener = listener + (listener as Context).registerReceiver(this, IntentFilter(UPDATE_UI)) + } + + fun unRegisterReceiver(listener: UiUpdateListener) { + (listener as Context).unregisterReceiver(this) + } + + fun registerReceiver(listener: UiUpdateListener) { + this.listener = listener + (listener as Context).registerReceiver(this, IntentFilter(UPDATE_UI)) + } + + override fun onReceive(context: Context?, intent: Intent?) { + if (listener != null) listener!!.updateUI() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/repositories/CheckListRepository.kt b/app/src/main/java/com/iesoluciones/siodrenax/repositories/CheckListRepository.kt new file mode 100644 index 0000000..61b2a7f --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/repositories/CheckListRepository.kt @@ -0,0 +1,76 @@ +package com.iesoluciones.siodrenax.repositories + +import com.iesoluciones.siodrenax.api +import com.iesoluciones.siodrenax.checkListQuestionBox +import com.iesoluciones.siodrenax.entities.CheckListQuestion +import com.iesoluciones.siodrenax.entities.CheckListQuestion_ +import com.iesoluciones.siodrenax.utils.Constants +import io.objectbox.kotlin.and +import io.objectbox.kotlin.equal +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.observers.ResourceObserver +import io.reactivex.schedulers.Schedulers +import okhttp3.ResponseBody + +class CheckListRepository { + + interface CheckListInterface { + fun success() + fun failure(throwable: Throwable) + } + + fun isCheckListDone(): Boolean { + return checkListQuestionBox.query() + .build() + .find().isNotEmpty() + } + + fun getSurveyCheckList(tipo: String): List { + + return checkListQuestionBox.query( + CheckListQuestion_.tipo equal tipo + ) + .build() + .find() + } + + fun isValidRevision(): Boolean { + val totalRadio = checkListQuestionBox.query( + CheckListQuestion_.tipo equal Constants.REVISION + and (CheckListQuestion_.tipoRadioBtn equal 1) + and (CheckListQuestion_.respuestaRadioBtn.isNull) + ) + .build() + .count() + + val totalSelect = checkListQuestionBox.query( + CheckListQuestion_.tipo equal Constants.REVISION + and (CheckListQuestion_.tipoCheckBox equal 0) + and (CheckListQuestion_.tipoRadioBtn equal 0) + and (CheckListQuestion_.tipoText equal 0) + and (CheckListQuestion_.respuestaText.isNull) + ) + .build() + .count() + + return (totalRadio + totalSelect) == 0L + } + + fun finalizarEncuesta(checkListInterface: CheckListInterface) { + + api.sendCheckList(checkListQuestionBox.all) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .map { response -> + checkListQuestionBox.removeAll() + response + } + .subscribe(object : ResourceObserver() { + override fun onNext(responseBody: ResponseBody) {} + + override fun onError(e: Throwable) = checkListInterface.failure(e) + + override fun onComplete() = checkListInterface.success() + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/repositories/EvidenceRepository.kt b/app/src/main/java/com/iesoluciones/siodrenax/repositories/EvidenceRepository.kt new file mode 100644 index 0000000..cc3cf6d --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/repositories/EvidenceRepository.kt @@ -0,0 +1,15 @@ +package com.iesoluciones.siodrenax.repositories + +import com.iesoluciones.siodrenax.entities.Evidence +import com.iesoluciones.siodrenax.evidenceBox + +class EvidenceRepository { + + fun getEvidenceById(idEvidence: Long): Evidence { + return evidenceBox.get(idEvidence) + } + + fun saveEvidence(evidence: Evidence) { + evidenceBox.put(evidence) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/repositories/IncidenceRepository.kt b/app/src/main/java/com/iesoluciones/siodrenax/repositories/IncidenceRepository.kt new file mode 100644 index 0000000..6003ab5 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/repositories/IncidenceRepository.kt @@ -0,0 +1,91 @@ +package com.iesoluciones.siodrenax.repositories + +import com.iesoluciones.siodrenax.api +import com.iesoluciones.siodrenax.models.GenericResponse +import com.iesoluciones.siodrenax.models.VehicleIncidenceResponse +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.observers.ResourceObserver +import io.reactivex.schedulers.Schedulers + +class IncidenceRepository { + + interface IncidenceInterface { + fun success(genericResponse: String?) + fun failure(throwable: Throwable) + } + + interface GetIncidenceInterface { + fun success(response: VehicleIncidenceResponse?) + fun failure(throwable: Throwable) + } + + fun sendVehicleIncidenceRequest(incidence: String, incidenceInterface: IncidenceInterface) { + + api.sendVehicleIncidence(incidence) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .map { response -> + response + } + .subscribe(object : ResourceObserver() { + override fun onNext(s: GenericResponse) { + incidenceInterface.success(s.response) + } + + override fun onError(e: Throwable) { + incidenceInterface.failure(e) + } + + override fun onComplete() { + + } + }) + } + + fun getVehicleIncidenceRequest(vehicleId: Long, incidenceInterface: GetIncidenceInterface) { + + api.getVehicleIncidence(vehicleId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .map { response -> + response + } + .subscribe(object : ResourceObserver() { + override fun onNext(s: VehicleIncidenceResponse) { + incidenceInterface.success(s) + } + + override fun onError(e: Throwable) { + incidenceInterface.failure(e) + } + + override fun onComplete() { + + } + }) + } + + fun resolveVehicleIncidenceRequest(incidenceId: String, incidenceInterface: IncidenceInterface) { + + api.resolveVehicleIncidence(incidenceId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .map { response -> + response + } + .subscribe(object : ResourceObserver() { + override fun onNext(s: GenericResponse) { + incidenceInterface.success(s.response) + } + + override fun onError(e: Throwable) { + incidenceInterface.failure(e) + } + + override fun onComplete() { + + } + }) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/repositories/LoginRepository.kt b/app/src/main/java/com/iesoluciones/siodrenax/repositories/LoginRepository.kt new file mode 100644 index 0000000..87b4dc3 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/repositories/LoginRepository.kt @@ -0,0 +1,62 @@ +package com.iesoluciones.siodrenax.repositories + +import com.iesoluciones.siodrenax.* +import com.iesoluciones.siodrenax.utils.Constants.Companion.APP_ID +import com.iesoluciones.siodrenax.utils.Constants.Companion.EMPLOYEE_MANAGER +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.observers.ResourceObserver +import io.reactivex.schedulers.Schedulers + +class LoginRepository { + + interface LoginInterface { + fun success(result: Boolean) + fun failure(throwable: Throwable) + } + + fun loginRequest(username: String, password: String, deviceId: String, loginInterface: LoginInterface) { + + val obLogin = api.login(APP_ID, username, password, BuildConfig.VERSION_NAME, deviceId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .map { response -> + preferencesHelper.tokenApi = response.token + val user = response.user + userBox.put(user) + preferencesHelper.isManager = user.idEmployeeType == EMPLOYEE_MANAGER + response + } + + val obCheckList = api.getCheckList(0) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .map { response -> + checkListQuestionBox.put(response) + response + } + + val obVehicle = api.getVehicle() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .map { response -> + vehicleBox.put(response) + response + } + + Observable.concat(obLogin, obCheckList, obVehicle) + .subscribe(object : ResourceObserver() { + override fun onNext(t: Any) { + } + + override fun onError(e: Throwable) { + loginInterface.failure(e) + } + + override fun onComplete() { + loginInterface.success(true) + } + }) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/repositories/ManagerRepository.kt b/app/src/main/java/com/iesoluciones/siodrenax/repositories/ManagerRepository.kt new file mode 100644 index 0000000..d062aa9 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/repositories/ManagerRepository.kt @@ -0,0 +1,121 @@ +package com.iesoluciones.siodrenax.repositories + +import com.iesoluciones.siodrenax.api +import com.iesoluciones.siodrenax.entities.Operator +import com.iesoluciones.siodrenax.entities.Order +import com.iesoluciones.siodrenax.entities.User +import com.iesoluciones.siodrenax.models.Workday +import com.iesoluciones.siodrenax.operatorBox +import com.iesoluciones.siodrenax.preferencesHelper +import com.iesoluciones.siodrenax.userBox +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.observers.ResourceObserver +import io.reactivex.schedulers.Schedulers +import okhttp3.ResponseBody + +class ManagerRepository { + + interface OnOrdersRetrievedListener { + fun success(orders: List) + fun failure(throwable: Throwable) + } + + interface OnOperatorsRetrievedListener { + fun success(operators: List) + fun failure(throwable: Throwable) + } + + interface OnWebServiceCalled { + fun success() + fun failure(e: Throwable) + } + + fun getOperatorList(listener: OnOperatorsRetrievedListener){ + api.listOperators() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .subscribe(object : ResourceObserver>() { + override fun onNext(operators: List) { + operatorBox.put(operators) + listener.success(operators) + } + + override fun onError(e: Throwable) { + listener.failure(e) + } + + override fun onComplete() {} + }) + } + + fun getOperatorOrders(idOperator: String, listener: OnOrdersRetrievedListener){ + api.getOperatorOrders(idOperator) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .subscribe(object : ResourceObserver>() { + override fun onNext(orders: List) { + listener.success(orders) + } + + override fun onError(e: Throwable) { + listener.failure(e) + } + + override fun onComplete() {} + }) + } + + fun getOperatorsFromDb(): List { + return operatorBox.all + } + + fun logOut(){ + operatorBox.removeAll() + userBox.removeAll() + preferencesHelper.tokenApi = null + preferencesHelper.tokenFirebase = null + preferencesHelper.isManager = false + preferencesHelper.workdayStarted = false + preferencesHelper.workdayId = 0 + } + + fun startWorkday(listener: OnWebServiceCalled){ + api.startWorkdayManager() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .subscribe(object : ResourceObserver() { + override fun onNext(workday: Workday) { + preferencesHelper.workdayId = workday.idWorkload.toInt() + listener.success() + } + + override fun onError(e: Throwable) { + listener.failure(e) + } + + override fun onComplete() {} + }) + } + + fun endWorkday(listener: OnWebServiceCalled){ + api.endWorkdayManager(preferencesHelper.workdayId.toString()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .subscribe(object : ResourceObserver() { + override fun onNext(responseBody: ResponseBody) { + logOut() + listener.success() + } + + override fun onError(e: Throwable) { + listener.failure(e) + } + + override fun onComplete() {} + }) + } + + fun getUserData(): User { + return userBox.all[0] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/repositories/OrdersRepository.kt b/app/src/main/java/com/iesoluciones/siodrenax/repositories/OrdersRepository.kt new file mode 100644 index 0000000..0b00050 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/repositories/OrdersRepository.kt @@ -0,0 +1,349 @@ +package com.iesoluciones.siodrenax.repositories + +import android.content.Intent +import com.iesoluciones.siodrenax.* +import com.iesoluciones.siodrenax.App.Companion.shareInstance +import com.iesoluciones.siodrenax.entities.* +import com.iesoluciones.siodrenax.models.EvidenceRequest +import com.iesoluciones.siodrenax.models.FinishOrderRequest +import com.iesoluciones.siodrenax.models.Question +import com.iesoluciones.siodrenax.models.ValidationOrderAnswer +import com.iesoluciones.siodrenax.services.NotificationService +import com.iesoluciones.siodrenax.utils.Constants +import com.iesoluciones.siodrenax.utils.HelperUtil +import io.objectbox.kotlin.equal +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.observers.ResourceObserver +import io.reactivex.schedulers.Schedulers +import okhttp3.ResponseBody + + +class OrdersRepository { + + interface OnRefreshOrders { + fun success(orders: List) + fun failure(throwable: Throwable) + } + + interface OnRefreshONextDayOrders { + fun success(nextDayOrders: List) + fun failure(throwable: Throwable) + } + + interface OnOrderRequestAnswered { + fun success(orderResponse: ResponseBody) + fun failure(throwable: Throwable) + } + + fun getOrders(): List { + return orderBox.query() + .order(Order_.dateServiceRequest) + .build() + .find() + } + + fun getOrderById(id: Long): Order? { + return orderBox.query() + .equal(Order_.id, id) + .build() + .findFirst() + } + + fun getOrderByIdRequest(idRequest: Long): Order? { + return orderBox.query() + .equal(Order_.idRequest, idRequest) + .build() + .findFirst() + } + + fun getOrderProgressById(id: Long): OrderProgress? { + return orderProgressBox.query() + .equal(OrderProgress_.id, id) + .build() + .findFirst() + } + + fun refreshOrders(listener: OnRefreshOrders, filesDir: String) { + + api.getOrders() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .subscribe(object : ResourceObserver>() { + override fun onNext(orders: List) { + + val ordersInBox: List = + orderBox.query().order(Order_.dateServiceRequest).build().find() + for (o in ordersInBox) { + val orderProgress: OrderProgress? = getOrderProgressById(o.id) + if (orderProgress == null || orderProgress.isSent) { + orderBox.remove(o.id) + } + } + orderBox.put(orders) + HelperUtil().downloadSketchs(filesDir,orders) + listener.success(getOrders()) + } + + override fun onError(e: Throwable) { + listener.failure(e) + } + + override fun onComplete() {} + }) + } + + fun startOrder( + idOrder: String, + idRequest: String, + startDate: String, + startLat: String, + startLng: String, + listener: OnOrderRequestAnswered + ) { + api.startOrder(idOrder, idRequest, startDate, startLat, startLng) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .subscribe(object : ResourceObserver() { + override fun onNext(responseBody: ResponseBody) { + HelperUtil().createEvidences(getOrderById(idOrder.toLong())!!) + val orderProgress = OrderProgress() + orderProgress.id = idOrder.toLong() + orderProgress.idRequest = idRequest.toLong() + orderProgress.startDate = startDate + orderProgress.startLat = startLat + orderProgress.startLng = startLng + + orderProgressBox.put(orderProgress) + preferencesHelper.orderInProgress = idOrder.toLong() + listener.success(responseBody) + } + + override fun onError(e: Throwable) { + listener.failure(e) + } + + override fun onComplete() {} + }) + } + + fun createSurveyAnswerHolders(order: Order) { + val surveyRepository = SurveyRepository() + + val questions: List = + if (order.idServiceType == Constants.ID_SERVICE_TYPE_DOMESTIC) { + surveyRepository.getSurveyQuestions(true) + } else { + surveyRepository.getSurveyQuestions(false) + } + + for (q in questions) { + val savedAnswer = SavedAnswer() + savedAnswer.orderId = order.id + savedAnswer.questionId = q.getId() + + savedAnswerBox.put(savedAnswer) + } + } + + fun getEvidences(idOrder: Long): List { + return evidenceBox.query() + .equal(Evidence_.idOrder, idOrder) + .order(Evidence_.id) + .build() + .find(0, 9) + } + + fun getEvidencesForZip(idOrder: Long): List { + return evidenceBox.query() + .equal(Evidence_.idOrder, idOrder) + .and() + .notNull(Evidence_.path) + .build() + .find() + } + + fun getEvidencesForPdf(idOrder: Long): List { + return evidenceBox.query() + .equal(Evidence_.idOrder, idOrder) + .and() + .notNull(Evidence_.path) + .order(Evidence_.evidenceNo) + .build() + .find() + } + + fun saveOrderProgress(orderProgress: OrderProgress) { + orderProgressBox.put(orderProgress) + } + + fun getPendingOrders(): List { + //System.gc() + + val ordersIdList = orderProgressBox.query() + .equal(OrderProgress_.shouldBeSent, true) + .and().equal(OrderProgress_.isSent, false) + .build() + .findIds() + + validateOrdersSuccess(ordersIdList.toCollection(ArrayList())) + + return orderProgressBox.query() + .equal(OrderProgress_.shouldBeSent, true) + .and().equal(OrderProgress_.isSent, false) + .build() + .find() + } + + private fun validateOrdersSuccess(ordersIdList: ArrayList) { + api.verificarServicio(ordersIdList) + .subscribeOn(Schedulers.trampoline()) + .observeOn(Schedulers.trampoline()) + .subscribe(object : ResourceObserver>() { + override fun onNext(validationOrderAnswers: List) { + for (v in validationOrderAnswers) { + + evidenceBox.query( + Evidence_.idOrder equal v.servicio_id + ) + .build() + .remove() + + orderProgressBox.remove(v.servicio_id.toLong()) + orderBox.remove(v.servicio_id.toLong()) + + //TODO + // shareInstance!!.sendBroadcast(Intent(PushNotificationMessagingService.UPDATE_UI)) + // shareInstance!!.startService(Intent(shareInstance, CleaningService::class.java)) + preferencesHelper.orderInProgress = null + } + } + + override fun onError(e: Throwable) {} + override fun onComplete() {} + }) + } + + private fun getPendingEvidences(idOrder: String): List { + return evidenceBox.query() + .equal(Evidence_.idOrder, idOrder.toLong()) + .and().equal(Evidence_.isSent, false) + .and().notNull(Evidence_.path) + .build() + .find() + } + + fun prepareFinishOrder(orderProgress: OrderProgress): Observable { + + val order = getOrderById(orderProgress.id)!! + val answers: List = SurveyRepository().getSurveyBody(order) + + val evidences = arrayListOf() + getEvidencesForZip(orderProgress.id).forEach { + evidences.add(EvidenceRequest(it.name!!, it.getTypeDescription(), it.lat, it.lng)) + } + + val finishOrderRequest = FinishOrderRequest( + idOrder = orderProgress.id.toString(), + idRequest = orderProgress.idRequest.toString(), + endDate = orderProgress.endDate!!, + endLat = orderProgress.endLat, + endLng = orderProgress.endLng, + elapsedTime = orderProgress.elapsedTime!!, + comments = orderProgress.comments!!, + warranty = if (orderProgress.warranty) 1 else 0, + liters = orderProgress.liters, + negativeService = orderProgress.negativeService, + survey = if (order.SurveyRequired == 1 && orderProgress.shouldSendSurvey) answers else null, + receipt = HelperUtil().encodeBase64(order.pdfPath!!)!! + ) + + return api.finishOrder(finishOrderRequest) + .doOnNext { + removeAllOrdersInformation( + orderProgress.id, + order.pdfPath!!, + orderProgress.signaturePath!! + ) + shareInstance!!.sendBroadcast(Intent(NotificationService.UPDATE_UI)) + preferencesHelper.orderInProgress = null + } + .doOnError { + orderProgress.isSent = false + orderProgressBox.put(orderProgress) + } + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + } + + fun validateEvidences(idOrder: String): Boolean { + // Validar almenos una imagen de cada etapa + /* + val evidences = getPendingEvidences(idOrder) + var hasAtLeastOneStartEvicende = false + var hasAtLeastOneProcessEvicende = false + var hasAtLeastOneFinalEvicende = false + + for (e in evidences) { + when (e.type) { + Constants.EVIDENCE_START -> hasAtLeastOneStartEvicende = true + Constants.EVIDENCE_PROCESS -> hasAtLeastOneProcessEvicende = true + Constants.EVIDENCE_FINAL -> hasAtLeastOneFinalEvicende = true + } + } + return hasAtLeastOneStartEvicende && hasAtLeastOneProcessEvicende && hasAtLeastOneFinalEvicende + */ + + return getPendingEvidences(idOrder).size == 9 + } + + fun getNextDayOrders(): List { + return nextDayOrderBox.query() + .order(NextDayOrder_.dateServiceRequest) + .build() + .find() + } + + fun refreshNextDayOrders(listener: OnRefreshONextDayOrders) { + api.getNextDayOrders() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .subscribe(object : ResourceObserver>() { + override fun onNext(orders: List) { + nextDayOrderBox.removeAll() + nextDayOrderBox.put(orders) + listener.success(getNextDayOrders()) + } + + override fun onError(e: Throwable) { + listener.failure(e) + } + + override fun onComplete() {} + }) + } + + private fun removeAllOrdersInformation(idOrder: Long, pdfPath: String, signaturePath: String) { + + getEvidencesForZip(idOrder).forEach { + HelperUtil().deleteFile(it.path!!) + } + + HelperUtil().deleteFile(pdfPath) + HelperUtil().deleteFile(signaturePath) + + evidenceBox.query() + .equal(Evidence_.idOrder, idOrder) + .build() + .remove() + + savedAnswerBox.query() + .equal(SavedAnswer_.orderId, idOrder) + .build() + .remove() + + orderProgressBox.remove(idOrder) + + orderBox.remove(idOrder) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/repositories/SurveyRepository.kt b/app/src/main/java/com/iesoluciones/siodrenax/repositories/SurveyRepository.kt new file mode 100644 index 0000000..b7bf557 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/repositories/SurveyRepository.kt @@ -0,0 +1,104 @@ +package com.iesoluciones.siodrenax.repositories + +import android.util.Log +import com.iesoluciones.siodrenax.* +import com.iesoluciones.siodrenax.entities.* +import com.iesoluciones.siodrenax.models.* +import com.iesoluciones.siodrenax.utils.Constants.Companion.IDSERVICETYPEDOMESTIC +import java.util.* + +class SurveyRepository { + + fun getSurveyQuestions(isDomestic: Boolean): List { + val questions: MutableList = ArrayList() + if (isDomestic) { + questions.addAll(domesticQuestionBox.all) + } else { + questions.addAll(businessQuestionBox.all) + } + return questions + } + + fun getSurveyBody(order: Order): List { + // NullPointerException launched once. The "Order" is a non null object. + // Commented to track the error -------------> Incidents = 12 + val answerKeyValues: MutableList = ArrayList() + val questions = + getSurveyQuestions(order.idServiceType == IDSERVICETYPEDOMESTIC) + for (q in questions) { + val answer: SavedAnswer? = getSavedAnswer(q, order) + if (answer != null) { + if (answer.isOpen) + answerKeyValues.add(OpenAnswer(q.getId().toString(), answer.answer)) + else + answerKeyValues.add(CheckBoxAnswer(q.getId().toString(), answer.answer)) + } + } + return answerKeyValues + } + + fun deleteAllSavedAnswers(order: Order) { + savedAnswerBox.query() + .equal(SavedAnswer_.orderId, order.id) + .build() + .remove() + } + + private fun getSavedAnswer(question: Question, order: Order): SavedAnswer? { + return savedAnswerBox.query() + .equal(SavedAnswer_.orderId, order.id) + .and().equal(SavedAnswer_.questionId, question.getId()) + .build() + .findFirst() + } + + fun getNegativeServiceReason(): List { + return negativeServiceReasonBox.all + } + + fun validateAnswers(order: Order): Boolean { + val questions = getSurveyQuestions(order.idServiceType == IDSERVICETYPEDOMESTIC) + for (q in questions) { + + val answer: String? = getQuestionSavedAnswer(q, order) + if (q.getRequired() && (answer == null || answer.trim { it <= ' ' } == "")) + return false + } + + return true + } + + fun getQuestionSavedAnswer(question: Question, order: Order): String? { + return getSavedAnswer(question, order)?.answer + } + + fun getAnswers(q: Question, isDomestic: Boolean): List { + return if (isDomestic) getDomesticQuestionAnswers(q) else getBusinessQuestionAnswers(q) + } + + private fun getDomesticQuestionAnswers(q: Question): List { + return domesticAnswerBox.query() + .equal(DomesticAnswer_.idQuestion, q.getId()) + .build() + .find() + } + + private fun getBusinessQuestionAnswers(q: Question): List { + return businessAnswerBox.query() + .equal(BusinessAnswer_.idQuestion, q.getId()) + .build() + .find() + } + + fun saveQuestionAnswer(question: Question, order: Order, answer: String?, isOpen: Boolean) { + val savedAnswer = getSavedAnswer(question, order) + + if (savedAnswer != null) { + savedAnswer.isOpen = isOpen + savedAnswer.answer = answer + savedAnswerBox.put(savedAnswer) + } else { + Log.e("SurveyRepository", "Trouble trying to save answer $answer") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/repositories/WorkdayRepository.kt b/app/src/main/java/com/iesoluciones/siodrenax/repositories/WorkdayRepository.kt new file mode 100644 index 0000000..fff44c8 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/repositories/WorkdayRepository.kt @@ -0,0 +1,251 @@ +package com.iesoluciones.siodrenax.repositories + +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import android.os.Build +import android.os.Build.VERSION_CODES +import androidx.core.app.NotificationManagerCompat +import com.iesoluciones.siodrenax.* +import com.iesoluciones.siodrenax.App.Companion.context +import com.iesoluciones.siodrenax.App.Companion.shareInstance +import com.iesoluciones.siodrenax.entities.OrderProgress_ +import com.iesoluciones.siodrenax.entities.User +import com.iesoluciones.siodrenax.models.StartWorkdayResponse +import com.iesoluciones.siodrenax.utils.HelperUtil +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.observers.ResourceObserver +import io.reactivex.schedulers.Schedulers +import okhttp3.ResponseBody + +class WorkdayRepository { + + interface WorkdayInterface { + fun success(response: StartWorkdayResponse) + fun failure(throwable: Throwable) + } + + interface WorkdayEndInterface { + fun success(response: ResponseBody) + fun failure(throwable: Throwable) + } + + fun startWorkdayRequest( + firebaseToken: String, + odometer: String, + initLat: String, + initLng: String, + filesDir: String, + listener: WorkdayInterface + ) { + //obtain device model info + val reqString = (Build.MANUFACTURER + + " " + Build.MODEL + " " + Build.VERSION.RELEASE + + " " + VERSION_CODES::class.java.fields[Build.VERSION.SDK_INT].name) + + //Obtain device battery percentage in integer + val ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + val batteryStatus: Intent = shareInstance!!.registerReceiver(null, ifilter)!! + val level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + val scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + val batteryPct = level / scale.toDouble() * 100.0 + val battery = batteryPct.toInt() + + api.startWorkday( + firebaseToken, + odometer, + initLat, + initLng, + reqString, + battery.toString() + "" + ) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .map { response -> + processWorkdayResponse(response, filesDir) + response + } + .subscribe(object : ResourceObserver() { + override fun onNext(response: StartWorkdayResponse) { + listener.success(response) + } + + override fun onError(e: Throwable) { + listener.failure(e) + } + + override fun onComplete() { + + } + }) + } + + + private fun processWorkdayResponse(response: StartWorkdayResponse, filesDir: String){ + preferencesHelper.workdayStarted = true + preferencesHelper.workdayId = response.workdayInfo.idWorkload.toInt() + + orderBox.put(response.orders) + domesticQuestionBox.put(response.parseDomesticSurvey()) + businessQuestionBox.put(response.parseBussinesSurvey()) + negativeServiceReasonBox.put(response.negativeServiceReason) + + for (q in response.businessSurvey) + for (answerPOJO in q.answers) + businessAnswerBox.put(answerPOJO.getBusinessAnswer()) + + for (q in response.domesticSurvey) + for (answerPOJO in q.answers) + domesticAnswerBox.put(answerPOJO.getDomesticAnswer()) + + + for (keyValue in response.keyValues) + if (keyValue.key == shareInstance!!.getString(R.string.gps_interval_param)) + preferencesHelper.gpsInterval = keyValue.value.toInt() + + + HelperUtil().downloadSketchs(filesDir,response.orders) + } + + fun endWorkdayRequest( + odometer: String, + endLat: String, + endLng: String, + listener: WorkdayEndInterface + ){ + + val reqString = (Build.MANUFACTURER + + " " + Build.MODEL + " " + Build.VERSION.RELEASE + + " " + VERSION_CODES::class.java.fields[Build.VERSION.SDK_INT].name) + + val ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + val batteryStatus: Intent = shareInstance!!.registerReceiver(null, ifilter)!! + val level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + val scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + val batteryPct = level / scale.toDouble() * 100.0 + val battery = batteryPct.toInt() + + api.endWorkday( + preferencesHelper.workdayId.toString(), + odometer, + endLat, + endLng, + reqString, + battery.toString() + ) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .map { response -> + preferencesHelper.workdayStarted = false + preferencesHelper.workdayId = 0 + response + } + .subscribe(object : ResourceObserver() { + override fun onNext(response: ResponseBody) { + listener.success(response) + } + + override fun onError(e: Throwable) { + listener.failure(e) + } + + override fun onComplete() { + + } + }) + } + + fun validateChangeVehicle(): Boolean { + return orderProgressBox.query() + .equal(OrderProgress_.isSent, false) + .build() + .find() + .isEmpty() + } + + fun changeVehicle( + odometer: String, + endLat: String, + endLng: String, + listener: WorkdayEndInterface + ) { + val reqString = (Build.MANUFACTURER + + " " + Build.MODEL + " " + Build.VERSION.RELEASE + + " " + VERSION_CODES::class.java.fields[Build.VERSION.SDK_INT].name) + + val ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + val batteryStatus: Intent = shareInstance!!.registerReceiver(null, ifilter)!! + val level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + val scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + val batteryPct = level / scale.toDouble() * 100.0 + val battery = batteryPct.toInt() + + api.endWorkday( + preferencesHelper.workdayId.toString(), + odometer, + endLat, + endLng, + reqString, + battery.toString() + ) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .map { response -> + preferencesHelper.workdayStarted = false + preferencesHelper.workdayId = 0 + response + } + .subscribe(object : ResourceObserver() { + override fun onNext(response: ResponseBody) { + listener.success(response) + } + + override fun onError(e: Throwable) { + listener.failure(e) + } + + override fun onComplete() { + + } + }) + } + + fun getUser(): User { + return userBox.all[0] + } + + fun logout() { + preferencesHelper.tokenApi = null + preferencesHelper.isEnableHerramientaSurvey = true + preferencesHelper.workdayStarted = false + preferencesHelper.orderInProgress = 0L + + userBox.removeAll() + checkListQuestionBox.removeAll() + vehicleBox.removeAll() + orderBox.removeAll() + evidenceBox.removeAll() + businessQuestionBox.removeAll() + domesticQuestionBox.removeAll() + negativeServiceReasonBox.removeAll() + businessAnswerBox.removeAll() + domesticAnswerBox.removeAll() + savedAnswerBox.removeAll() + orderProgressBox.removeAll() + nextDayOrderBox.removeAll() + + NotificationManagerCompat.from(context!!).cancelAll() + } + + fun validateLogOut(): Boolean { + return orderProgressBox.query() + .equal(OrderProgress_.isSent, false) + .build() + .find() + .isEmpty() + } + + fun validateRenewSession() : Boolean { + return orderBox.isEmpty + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/services/NotificationService.kt b/app/src/main/java/com/iesoluciones/siodrenax/services/NotificationService.kt new file mode 100644 index 0000000..8234fcf --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/services/NotificationService.kt @@ -0,0 +1,71 @@ +package com.iesoluciones.siodrenax.services + +import android.content.Intent +import android.os.Handler +import android.os.Looper +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.api +import com.iesoluciones.siodrenax.entities.Order +import com.iesoluciones.siodrenax.orderBox +import com.iesoluciones.siodrenax.utils.HelperUtil +import com.iesoluciones.siodrenax.utils.NotificationUtils +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.observers.ResourceObserver +import io.reactivex.schedulers.Schedulers + +class NotificationService: FirebaseMessagingService() { + + companion object { + const val UPDATE_UI = "update_ui" + } + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + super.onMessageReceived(remoteMessage) + + // ----------------------------------------------------------------------------- + // ---------- Second way to launch notification (valid to foreground) ---------- + + val notification = remoteMessage.data + + if(notification.containsKey("servicio_id")){ + Handler(Looper.getMainLooper()).postDelayed({ + + api.getOrderInfo(notification["servicio_id"]!!) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.newThread()) + .subscribe(object : ResourceObserver() { + override fun onNext(order: Order) { + if (order.id != 0L) { + orderBox.put(order) + + HelperUtil().downloadSketchs(filesDir.absolutePath, mutableListOf(order)) + + sendBroadcast(Intent(UPDATE_UI)) + + NotificationUtils().showNotification( + resources.getString(R.string.app_name), + resources.getString(R.string.notification_accept_succes_message) + ) + } + } + + override fun onError(e: Throwable) { + NotificationUtils().showNotification( + resources.getString(R.string.app_name), + resources.getString(R.string.notification_accept_success_refresh_message) + ) + } + + override fun onComplete() { + } + }) + }, 1000) + } + + + + // ----------------------------------------------------------------------------- + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/utils/CircularProgressButton.java b/app/src/main/java/com/iesoluciones/siodrenax/utils/CircularProgressButton.java new file mode 100644 index 0000000..91eda85 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/utils/CircularProgressButton.java @@ -0,0 +1,206 @@ +package com.iesoluciones.siodrenax.utils; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.os.Build; +import android.os.VibrationEffect; +import android.os.Vibrator; +import androidx.fragment.app.Fragment; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.animation.AccelerateDecelerateInterpolator; + +import com.iesoluciones.siodrenax.R; + + +/** + * Creado por Informática Electoral Proceso Electoral 2017. + * Gustavo@ie-soluciones.com + */ + + +public class CircularProgressButton extends androidx.appcompat.widget.AppCompatButton { + + public interface ProgressListener { + void onProgressStart(); + + void onProgressFinish(); + } + + private final CircularProgressDrawable mInactiveDrawable; + private final CircularProgressDrawable mActiveDrawable; + private CircularProgressDrawable mCurrentDrawable; + private final Vibrator mVibrator; + private ProgressListener mProgressListener; + private Animator currentAnimation; + private boolean mHoldingDown; + + @SuppressWarnings("deprecation") + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public CircularProgressButton(Context context, AttributeSet attrs) { + super(context, attrs); + setTransformationMethod(null); + if (this.isInEditMode()) { + this.mInactiveDrawable = null; + this.mActiveDrawable = null; + this.mVibrator = null; + return; + } + this.mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + this.mActiveDrawable = this.mCurrentDrawable = new CircularProgressDrawable.Builder() + .setRingWidth(this.getResources().getDimensionPixelOffset(R.dimen.drawable_ring_size)) + .setOutlineColor(this.getResources().getColor(R.color.colorAccent)) + .setCenterColor(this.getResources().getColor(R.color.colorAccent)) + .setRingColor(this.getResources().getColor(R.color.colorPrimaryDark)) + .setFillColor(this.getResources().getColor(R.color.colorPrimaryDark)) + .create(); + this.mInactiveDrawable = new CircularProgressDrawable.Builder() + .setRingWidth(this.getResources().getDimensionPixelOffset(R.dimen.drawable_ring_size)) + .setOutlineColor(this.getResources().getColor(R.color.colorAccent)) + .setCenterColor(this.getResources().getColor(R.color.colorAccent)) + .setRingColor(this.getResources().getColor(R.color.colorPrimaryDark)) + .setFillColor(this.getResources().getColor(R.color.colorPrimaryDark)) + .create(); + this.post(() -> setBackground(mCurrentDrawable)); + } + + @Override + @SuppressWarnings("SuspiciousNameCombination") + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // set height as long as the width so the widget is perfectly circular + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } + + @Override + @SuppressLint({"ClickableViewAccessibility", "MissingPermission"}) + public boolean onTouchEvent(MotionEvent event) { + if (this.mCurrentDrawable == this.mInactiveDrawable) { + // do no more + return true; + } + if (event.getAction() == MotionEvent.ACTION_DOWN) { + double distance = this.distanceFromCenter(event.getX(), event.getY()); + if (distance < this.getWidth() / 2F) { + this.mVibrator.vibrate(VibrationEffect.createOneShot(25, 10)); + this.mHoldingDown = true; + if (this.mProgressListener != null) { + this.mProgressListener.onProgressStart(); + } + if (this.currentAnimation != null) { + this.currentAnimation.cancel(); + } + this.currentAnimation = prepareStyle2Animation(); + this.currentAnimation.start(); + } + } + if (event.getAction() == MotionEvent.ACTION_UP) { + this.mHoldingDown = false; + if (this.currentAnimation != null) { + this.currentAnimation.cancel(); + } + this.mCurrentDrawable.setProgress(0f); + this.mCurrentDrawable.setCircleScale(0f); + } + if (event.getAction() == MotionEvent.ACTION_MOVE) { + double distance = this.distanceFromCenter(event.getX(), event.getY()); + //if (event.getX() < 0 || event.getY() < 0 || event.getX() > this.getWidth() || event.getY() > this.getHeight()) + if (distance > this.getWidth() / 2F) { + this.mHoldingDown = false; + if (this.currentAnimation != null) { + this.currentAnimation.cancel(); + } + this.mCurrentDrawable.setProgress(0f); + this.mCurrentDrawable.setCircleScale(0f); + } + } + return true; + } + + private double distanceFromCenter(float touchX, float touchY) { + // get centers + int centerX = this.getWidth() / 2; + int centerY = this.getHeight() / 2; + // calculate distances + float distanceX = touchX - centerX; + float distanceY = touchY - centerY; + // pitagora suichi! + return Math.sqrt((distanceX * distanceX) + (distanceY * distanceY)); + } + + /* ownmeth */ + public void setProgressListener(ProgressListener progressListener) { + this.mProgressListener = progressListener; + } + + public ProgressListener getProgressListener() { + return this.mProgressListener; + } + + public void activate() { + this.mCurrentDrawable = this.mActiveDrawable; + this.setBackground(this.mCurrentDrawable); + } + + public void deactivate() { + this.mCurrentDrawable = this.mInactiveDrawable; + this.setBackground(this.mCurrentDrawable); + } + + private Animator prepareStyle2Animation() { + AnimatorSet animation = new AnimatorSet(); + final ObjectAnimator progressAnimation = ObjectAnimator.ofFloat(this.mCurrentDrawable, CircularProgressDrawable.PROGRESS_PROPERTY, 0f, 1f); + progressAnimation.setDuration(750); + progressAnimation.setInterpolator(new AccelerateDecelerateInterpolator()); + Animator innerCircleAnimation = ObjectAnimator.ofFloat(this.mCurrentDrawable, CircularProgressDrawable.CIRCLE_SCALE_PROPERTY, 0f, 1f); + innerCircleAnimation.setDuration(500); + innerCircleAnimation.addListener(new AnimatorListenerAdapter() { + final CircularProgressButton $this = CircularProgressButton.this; + + @SuppressLint("MissingPermission") + @Override + public void onAnimationStart(Animator animation) { + // catch and ignore possible npe's due to ui refreshing + try { + if ($this.mProgressListener instanceof Fragment) { + ((Fragment) $this.mProgressListener).getActivity().getApplicationContext(); + } + if ($this.mProgressListener instanceof Activity) { + ((Activity) $this.mProgressListener).getApplicationContext(); + } + $this.mVibrator.vibrate(VibrationEffect.createOneShot(25, 10)); + } catch (Exception ignore) { + } + } + + @SuppressLint("MissingPermission") + @Override + public void onAnimationEnd(Animator animation) { + // catch and ignore possible npe's due to ui refreshing + try { + if ($this.mProgressListener instanceof Fragment) { + ((Fragment) $this.mProgressListener).getActivity().getApplicationContext(); + } + if ($this.mProgressListener instanceof Activity) { + ((Activity) $this.mProgressListener).getApplicationContext(); + } + if ($this.mHoldingDown) { + $this.mVibrator.vibrate(VibrationEffect.createOneShot(100, 10)); + if ($this.mProgressListener != null) { + $this.mProgressListener.onProgressFinish(); + } + } + } catch (Exception ignore) { + } + } + }); + animation.playSequentially(progressAnimation, innerCircleAnimation); + return animation; + } +} + diff --git a/app/src/main/java/com/iesoluciones/siodrenax/utils/CircularProgressDrawable.java b/app/src/main/java/com/iesoluciones/siodrenax/utils/CircularProgressDrawable.java new file mode 100644 index 0000000..d405c5a --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/utils/CircularProgressDrawable.java @@ -0,0 +1,419 @@ +package com.iesoluciones.siodrenax.utils; + +/* + * Created by iedeveloper on 14/12/17. + */ + +import android.graphics.BlurMaskFilter; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; + +/** + * Circular Progress Drawable. + *

+ * This drawable will produce a circular shape with a ring surrounding it. The ring can appear + * both filled and give a little cue when it is empty. + *

+ * The inner circle size, the progress of the outer ring and if it is loading parameters can be + * controlled, as well the different colors for the three components. + * + * @author Saul Diaz + */ +public class CircularProgressDrawable extends Drawable { + /** + * Factor to convert the factor to paint the arc. + *

+ * In this way the developer can use a more user-friendly [0..1f] progress + */ + public static final int PROGRESS_FACTOR = 360; + /** + * Property Inner Circle Scale. + *

+ * The inner ring is supposed to defaults stay 3/4 radius off the outer ring at (75% scale), but this + * property can make it grow or shrink via this equation: OuterRadius * Scale. + *

+ * A 100% scale will make the inner circle to be the same radius as the outer ring. + */ + public static final String CIRCLE_SCALE_PROPERTY = "circleScale"; + /** + * Property Progress of the outer circle. + *

+ * The progress of the circle. If {@link #setIndeterminate(boolean) indeterminate flag} is set + * to FALSE, this property will be used to indicate the completion of the outer circle [0..1f]. + *

+ * If set to TRUE, the drawable will activate the loading mode, where the drawable will + * show a 90º arc which will be spinning around the outer circle as much as progress goes. + */ + public static final String PROGRESS_PROPERTY = "progress"; + /** + * Property Ring color. + *

+ * Changes the ring filling color + */ + public static final String RING_COLOR_PROPERTY = "ringColor"; + /** + * Property circle color. + *

+ * Changes the inner circle color + */ + public static final String CIRCLE_COLOR_PROPERTY = "circleColor"; + /** + * Property outline color. + *

+ * Changes the outline of the ring color. + */ + public static final String OUTLINE_COLOR_PROPERTY = "outlineColor"; + /** + * Logger Tag for Logging purposes. + */ + public static final String TAG = "CircularProgressDrawable"; + /** + * Paint object to draw the element. + */ + private final Paint paint; + /** + * Ring progress. + */ + protected float progress; + /** + * Color for the empty outer ring. + */ + protected int outlineColor; + /** + * Color for the completed ring. + */ + protected int ringColor; + /** + * Color for the inner circle. + */ + protected int centerColor; + protected final int fillColor; + /** + * Rectangle where the filling ring will be drawn into. + */ + protected final RectF arcElements; + /** + * Width of the filling ring. + */ + protected final int ringWidth; + /** + * Scale of the inner circle. It will affect the inner circle size on this equation: + * ([Biggest length of the Drawable] / 2) - (ringWidth / 2) * scale. + */ + protected float circleScale; + /** + * Set if it is an indeterminate + */ + protected boolean indeterminate; + + /** + * Creates a new CouponDrawable. + * + * @param ringWidth Width of the filled ring + * @param circleScale Scale difference between the outer ring and the inner circle + * @param outlineColor Color for the outline color + * @param ringColor Color for the filled ring + * @param centerColor Color for the center element + */ + CircularProgressDrawable(int ringWidth, float circleScale, int outlineColor, int ringColor, int centerColor, int fillColor) { + this.progress = 0; + this.outlineColor = outlineColor; + this.ringColor = ringColor; + this.centerColor = centerColor; + this.fillColor = fillColor; + this.paint = new Paint(); + this.paint.setAntiAlias(true); + this.ringWidth = ringWidth; + this.arcElements = new RectF(); + this.circleScale = circleScale; + this.indeterminate = false; + } + + @Override + public void draw(Canvas canvas) { + final Rect bounds = getBounds(); + // Calculations on the different components sizes + int size = Math.min(bounds.height(), bounds.width()); + float outerRadius = (size / 2F) - (ringWidth / 2F); + float innerRadius = outerRadius * circleScale; + float offsetX = (bounds.width() - outerRadius * 2) / 2; + float offsetY = (bounds.height() - outerRadius * 2) / 2; + // Outline Circle + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(1); + paint.setColor(outlineColor); + canvas.drawCircle(bounds.centerX(), bounds.centerY(), outerRadius, paint); + // Fill circle + paint.setStyle(Paint.Style.FILL); + paint.setColor(centerColor); + canvas.drawCircle(bounds.centerX(), bounds.centerY(), outerRadius, paint); + // Inner circle + paint.setStyle(Paint.Style.FILL); + paint.setColor(fillColor); + paint.setAlpha(255); + paint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL)); + canvas.drawCircle(bounds.centerX(), bounds.centerY(), innerRadius, paint); + paint.setMaskFilter(null); + int halfRingWidth = ringWidth / 2; + float arcX0 = offsetX + halfRingWidth; + float arcY0 = offsetY + halfRingWidth; + float arcX = offsetX + outerRadius * 2 - halfRingWidth; + float arcY = offsetY + outerRadius * 2 - halfRingWidth; + // Outer Circle + paint.setColor(ringColor); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(ringWidth); + paint.setStrokeCap(Paint.Cap.ROUND); + arcElements.set(arcX0, arcY0, arcX, arcY); + if (indeterminate) { + canvas.drawArc(arcElements, progress, 90, false, paint); + } else { + canvas.drawArc(arcElements, 360 - 90, progress, false, paint); + } + } + + @Override + public void setAlpha(int alpha) { + paint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + paint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return 1 - paint.getAlpha(); + } + + /** + * Returns the progress of the outer ring. + *

+ * Will output a correct value only when the indeterminate mode is set to FALSE. + * + * @return Progress of the outer ring. + */ + public float getProgress() { + return progress / PROGRESS_FACTOR; + } + + /** + * Sets the progress [0..1f] + * + * @param progress Sets the progress + */ + public void setProgress(float progress) { + if (indeterminate) { + this.progress = progress; + } else { + this.progress = PROGRESS_FACTOR * progress; + } + invalidateSelf(); + } + + /** + * Returns the inner circle scale. + * + * @return Inner circle scale in float multiplier. + */ + public float getCircleScale() { + return circleScale; + } + + /** + * Sets the inner circle scale. + * + * @param circleScale Inner circle scale. + */ + public void setCircleScale(float circleScale) { + this.circleScale = circleScale; + invalidateSelf(); + } + + /** + * Get the indeterminate status of the Drawable + * + * @return TRUE if the Drawable is in indeterminate mode or FALSE if it is in progress mode. + */ + public boolean isIndeterminate() { + return indeterminate; + } + + /** + * Sets the indeterminate parameter. + *

+ * The indeterminate parameter will change the behavior of the Drawable. If the indeterminate + * mode is set to FALSE, the outer ring will be able to be filled by using {@link #setProgress(float) setProgress}. + *

+ * Otherwise the drawable will enter "loading mode" and a 90º arc will be able to be spinned around + * the inner circle. + *

+ * By default, indeterminate mode is set to FALSE. + * + * @param indeterminate TRUE to activate loading mode. FALSE to activate progress mode. + */ + public void setIndeterminate(boolean indeterminate) { + this.indeterminate = indeterminate; + } + + /** + * Gets the outline color. + * + * @return Outline color of the empty ring. + */ + public int getOutlineColor() { + return outlineColor; + } + + /** + * Gets the filled ring color. + * + * @return Returns the filled ring color. + */ + public int getRingColor() { + return ringColor; + } + + /** + * Gets the color of the inner circle. + * + * @return Inner circle color. + */ + public int getCenterColor() { + return centerColor; + } + + /** + * Sets the empty progress outline color. + * + * @param outlineColor Outline color in #AARRGGBB format. + */ + public void setOutlineColor(int outlineColor) { + this.outlineColor = outlineColor; + invalidateSelf(); + } + + /** + * Sets the progress ring color. + * + * @param ringColor Ring color in #AARRGGBB format. + */ + public void setRingColor(int ringColor) { + this.ringColor = ringColor; + invalidateSelf(); + } + + /** + * Sets the inner circle color. + * + * @param centerColor Inner circle color in #AARRGGBB format. + */ + public void setCenterColor(int centerColor) { + this.centerColor = centerColor; + invalidateSelf(); + } + + /** + * Helper class to manage the creation of a CircularProgressDrawable + * + * @author Saul Diaz + */ + public static class Builder { + /** + * Witdh of the stroke of the filled ring + */ + int ringWidth; + /** + * Color of the outline of the empty ring in #AARRGGBB mode. + */ + int outlineColor; + /** + * Color of the filled ring in #AARRGGBB mode. + */ + int ringColor; + /** + * Color of the inner circle in #AARRGGBB mode. + */ + int centerColor; + int fillcolor; + /** + * Scale between the outer ring and the inner circle + */ + float circleScale = 0f; + + /** + * Sets the ring width. + * + * @param ringWidth Default ring width + * @return This builder + */ + public Builder setRingWidth(int ringWidth) { + this.ringWidth = ringWidth; + return this; + } + + /** + * Sets the default empty outer ring outline color. + * + * @param outlineColor Outline color in #AARRGGBB format. + * @return This Builder + */ + public Builder setOutlineColor(int outlineColor) { + this.outlineColor = outlineColor; + return this; + } + + /** + * Sets the progress ring color. + * + * @param ringColor Ring color in #AARRGGBB format. + * @return This Builder + */ + public Builder setRingColor(int ringColor) { + this.ringColor = ringColor; + return this; + } + + /** + * Sets the inner circle color. + * + * @param centerColor Inner circle color in #AARRGGBB format. + * @return This builder + */ + public Builder setCenterColor(int centerColor) { + this.centerColor = centerColor; + return this; + } + + public Builder setFillColor(int fillcolor) { + this.fillcolor = fillcolor; + return this; + } + + /** + * Sets the inner circle scale. Defaults to 0.75. + * + * @param circleScale Inner circle scale. + * @return This builder + */ + public Builder setInnerCircleScale(float circleScale) { + this.circleScale = circleScale; + return this; + } + + /** + * Creates a new CircularProgressDrawable with the requested parameters + * + * @return New CircularProgressDrawableInstance + */ + public com.iesoluciones.siodrenax.utils.CircularProgressDrawable create() { + return new com.iesoluciones.siodrenax.utils.CircularProgressDrawable(ringWidth, circleScale, outlineColor, ringColor, centerColor, fillcolor); + } + } +} + diff --git a/app/src/main/java/com/iesoluciones/siodrenax/utils/Constants.kt b/app/src/main/java/com/iesoluciones/siodrenax/utils/Constants.kt new file mode 100644 index 0000000..86aec90 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/utils/Constants.kt @@ -0,0 +1,98 @@ +package com.iesoluciones.siodrenax.utils + +class Constants { + + companion object { + const val LOGGED_IN = 1 + const val WORKDAY_STARTED=2 + const val NO_SESSION = 3 + const val ORDER_IN_PROGRESS = 4 + const val LOGGED_IN_MANAGER = 5 + const val WORKDAY_STARTED_MANAGER = 6 + + const val APP_ID = "MOVIL" + + const val EMPLOYEE_MANAGER: Long = 6 + + // CheckList + const val REVISION = "REVISION" + const val MATERIAL = "MATERIAL" + const val HERRAMIENTA = "HERRAMIENTA" + const val WORKDAY = "WORKDAY" + + // Evidences + const val EVIDENCE_START = 1 + const val EVIDENCE_PROCESS = 2 + const val EVIDENCE_FINAL = 3 + + // Location + const val DEFAULT_LATITUDE = 24.7726885 + const val DEFAULT_LONGITUDE = -107.4434088 + + // Notifications + const val ORDER_ID = "servicio_id" + + // Survey + const val ID_SERVICE_TYPE_DOMESTIC: Long = 1L + + // Parameters + const val DEFAULT_LITERS: String = "0" + const val HIDE_ALPHA = 0f + const val HIDE_DURATION = 200L + const val SHOW_ALPHA = 1f + const val SHOW_DURATION = 300L + + // OrderDetail + const val EVIDENCE_ID: String = "evidence_id" + const val EVIDENCE_NAME: String = "evidence_name" + const val MILEAGE = "mileage" + const val IDSERVICETYPEDOMESTIC = 1L + const val EVIDENCE_CODE = 2 + const val SIGNATURE_CODE = 10 + const val SURVEY_CODE = 20 + const val REQUEST_PERMISSIONS_REQUEST_CODE = 33 + + // Camera + const val COMPRESS_JPEG_QUALITY_LOW: Int = 15 + const val EVIDENCIA = "Evidencia " + const val SHOW_TITLE_ALPHA = 0f + const val SHOW_TITLE_DURATION = 200L + const val SHOW_TITLE_DELAY = 1000L + + // Survey + const val PDF_URL = "http://drenax.mx/assets/documents/aviso_privacidad.pdf" + const val DRIVE_READER = "https://drive.google.com/viewerng/viewer?embedded=true&url=" + + // Answer Type + const val ANSWER_CHECKBOX = 1 + const val ANSWER_DATE = 2 + const val ANSWER_EMAIL = 3 + const val ANSWER_CURRENCY = 4 + const val ANSWER_NUMBER = 5 + const val ANSWER_TEXT = 6 + + // Notifications + const val EXTRA_NOTIFICATION_CLICKED: String = "notificationClicked" + + // MultipleViewHolderAdapter + const val CERO = "0" + + // Signature + const val ANIMATE_FADE_OUT_ALPHA = 0.0f + const val ANIMATE_FADE_OUT_DURATION = 200L + const val ANIMATE_FADE_IN_DURATION = 200L + const val ANIMATE_FADE_IN_ALPHA = 1.0f + + //Manager + const val OPERATOR_ID: String = "operator_id" + const val OPERATOR_NAME = "operator_name" + + //ZIP + const val BUFFER_SIZE = 6 * 1024 + + //SKETCH + const val BUFFER_SIZE_SKETCH = 1024 * 1024 + const val BRANCH_NAME = "branch_name" + const val SKETCH_PATH = "sketch_path" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/utils/CustomDialogFragment.kt b/app/src/main/java/com/iesoluciones/siodrenax/utils/CustomDialogFragment.kt new file mode 100644 index 0000000..93bb668 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/utils/CustomDialogFragment.kt @@ -0,0 +1,128 @@ +package com.iesoluciones.siodrenax.utils + +import android.app.AlertDialog +import android.app.Dialog +import android.app.ProgressDialog +import android.content.Context +import android.view.View +import com.iesoluciones.siodrenax.R + +class CustomDialogFragment(context: Context) { + + private var mContext: Context = context + private lateinit var mDialog: ProgressDialog + private lateinit var mMessage: String + private lateinit var mTitle: String + private lateinit var mSubtitle: String + private lateinit var mTitleCloseButton: String + private var mSelection = 0 + private lateinit var mArrayMessage: Array + private lateinit var mViewType: MessageViewType + private lateinit var mView: View + + enum class MessageViewType { + RADIOBUTTON, DIALOG, ALERTDIALOG, WEBVIEWDIALOG + } + + + fun setTitle(title: String): CustomDialogFragment { + this.mTitle = title + return this + } + + fun messageType(messageViewType: MessageViewType): CustomDialogFragment { + mViewType = messageViewType + return this + } + + fun show() { + when (mViewType) { + MessageViewType.RADIOBUTTON -> showRadioButtonMessage(mTitle, mArrayMessage) + MessageViewType.DIALOG -> showDialog(mTitle, mSubtitle) + MessageViewType.ALERTDIALOG -> showAlertDialogMessage( + mTitle, + mMessage, + mTitleCloseButton + ) + MessageViewType.WEBVIEWDIALOG -> showWebViewDialog(mView) + } + } + + private fun showRadioButtonMessage(titulo: String, arrayMessage: Array) { + val builder = AlertDialog.Builder(mContext) + builder.setTitle(titulo) + builder.setSingleChoiceItems( + arrayMessage, 1 + ) { _, which -> + mSelection = which + } + builder.setNegativeButton( + mContext.getString(R.string.cancel) + ) { _, _ -> } + builder.setPositiveButton( + mContext.getString(R.string.accept) + ) { _, _ -> } + val alert = builder.create() + alert.setCanceledOnTouchOutside(false) + alert.show() + } + + fun showRadioButtonMessage( + titulo: String, + arrayMessage: Array, + onClickActionButton: OnClickActionButton + ): Dialog { + val builder = AlertDialog.Builder(mContext, R.style.DialogMessageTheme) + builder.setTitle(titulo) + builder.setSingleChoiceItems(arrayMessage, 0) { _, which -> mSelection = which } + builder.setPositiveButton(mContext.getString(R.string.accept)) { _, _ -> + onClickActionButton.onPositiveRadioButtonClicked( + mSelection + ) + } + builder.setCancelable(false) + builder.setNegativeButton(mContext.getString(R.string.cancel)) { _, _ -> onClickActionButton.onNegativeButtonClicked() } + return builder.create() + } + + private fun showDialog(titulo: String, descripcion: String) { + if (mDialog.isShowing) + mDialog.dismiss() + + mDialog = ProgressDialog(mContext, R.style.DialogMessageTheme) + mDialog.setTitle(titulo) + mDialog.setMessage(descripcion) + mDialog.setCancelable(false) + mDialog.setCanceledOnTouchOutside(false) + mDialog.show() + } + + private fun showWebViewDialog(view: View) { + if (mDialog.isShowing) + mDialog.dismiss() + + mDialog = ProgressDialog(mContext, R.style.DialogMessageTheme) + mDialog.setView(view) + mDialog.setCancelable(false) + mDialog.setCanceledOnTouchOutside(false) + mDialog.show() + } + + private fun showAlertDialogMessage( + title: String, + message: String, + positiveButton: String + ): Dialog { + val builder = AlertDialog.Builder(mContext) + builder.setTitle(title) + builder.setMessage(message) + builder.setPositiveButton(positiveButton) { _, _ -> } + return builder.create() + } + + interface OnClickActionButton { + fun onPositiveRadioButtonClicked(selection: Int) + fun onPositiveButtonClicked() + fun onNegativeButtonClicked() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/utils/CustomHtmlToPdfConvertor.kt b/app/src/main/java/com/iesoluciones/siodrenax/utils/CustomHtmlToPdfConvertor.kt new file mode 100644 index 0000000..2e1a15b --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/utils/CustomHtmlToPdfConvertor.kt @@ -0,0 +1,96 @@ +package com.iesoluciones.siodrenax.utils + +import android.content.Context +import android.os.Build +import android.print.PdfPrinter +import android.print.PrintAttributes +import android.print.PrintDocumentAdapter +import android.webkit.WebChromeClient +import android.webkit.WebView +import androidx.annotation.UiThread +import java.io.File + +class CustomHtmlToPdfConvertor(private val context: Context) { + + private var baseUrl: String? = null + + fun setBaseUrl(baseUrl: String) { + this.baseUrl = baseUrl + } + + @UiThread + fun convert( + pdfLocation: File, + htmlString: String, + onPdfGenerationFailed: PdfGenerationFailedCallback? = null, + onPdfGenerated: PdfGeneratedCallback, + ) { + + // maintain pdf generation status + var pdfGenerationStarted = false + try { + + // create new webview + val pdfWebView = WebView(context) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + pdfWebView.settings.safeBrowsingEnabled = false + } + + // job name + val jobName = Math.random().toString() + + // generate pdf attributes and properties + val attributes = getPrintAttributes() + + // generate print document adapter + val printAdapter = getPrintAdapter(pdfWebView, jobName) + + pdfWebView.webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView, newProgress: Int) { + super.onProgressChanged(view, newProgress) + + // some times progress provided by this method is wrong, that's why we are getting progress directly provided by WebView + val progress = pdfWebView.progress + + + // when page is fully loaded, start creating PDF + if (progress == 100 && !pdfGenerationStarted) { + + // change the status of pdf generation + pdfGenerationStarted = true + + // generate pdf + val pdfPrinter = PdfPrinter(attributes) + pdfPrinter.generate(printAdapter, pdfLocation, onPdfGenerated) + } + } + } + + // load html in WebView when it's setup is completed + pdfWebView.loadDataWithBaseURL(baseUrl, htmlString, "text/html", "utf-8", null) + + } catch (e: Exception) { + onPdfGenerationFailed?.invoke(e) + } + } + + + private fun getPrintAdapter(pdfWebView: WebView, jobName: String): PrintDocumentAdapter { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + pdfWebView.createPrintDocumentAdapter(jobName) + } else { + pdfWebView.createPrintDocumentAdapter() + } + } + + private fun getPrintAttributes(): PrintAttributes { + return PrintAttributes.Builder().apply { + setMediaSize(PrintAttributes.MediaSize.NA_LETTER) + setResolution(PrintAttributes.Resolution("pdf", Context.PRINT_SERVICE, 100, 100)) + setMinMargins(PrintAttributes.Margins.NO_MARGINS) + }.build() + } +} + +private typealias PdfGeneratedCallback = (File) -> Unit +private typealias PdfGenerationFailedCallback = (Exception) -> Unit \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/utils/Extensions.kt b/app/src/main/java/com/iesoluciones/siodrenax/utils/Extensions.kt new file mode 100644 index 0000000..424d6d8 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/utils/Extensions.kt @@ -0,0 +1,76 @@ +package com.iesoluciones.siodrenax.utils + +import android.app.Activity +import android.content.Intent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import com.google.android.material.snackbar.Snackbar +import com.iesoluciones.siodrenax.R +import java.nio.file.Files +import java.nio.file.Path +import java.text.DecimalFormat + +fun Activity.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) = + Toast.makeText(this, message, duration).show() + +fun Activity.toastLong(message: CharSequence, duration: Int = Toast.LENGTH_LONG) = + Toast.makeText(this, message, duration).show() + +fun Activity.toast(resourceId: Int, duration: Int = Toast.LENGTH_SHORT) = + Toast.makeText(this, resourceId, duration).show() + +fun Activity.snackBar( + message: CharSequence, + view: View? = findViewById(R.id.container), + duration: Int = Snackbar.LENGTH_SHORT, + action: String? = null, + actionEvt: (v: View) -> Unit = {} +) { + + if (view != null) { + val snackBar = Snackbar.make(view, message, duration) + if (!action.isNullOrEmpty()) { + snackBar.setAction(action, actionEvt) + } + snackBar.show() + } +} + +fun ViewGroup.inflate(layoutId: Int) = LayoutInflater.from(context).inflate(layoutId, this, false)!! + +inline fun Activity.goToActivity(noinline init: Intent.() -> Unit = {}) { + val intent = Intent(this, T::class.java) + intent.init() + startActivity(intent) +} + +fun String.currencyFormat(): String { + return try { + val formatter = DecimalFormat("###,###,##0.00") + formatter.format(this.toDouble()) + } catch (e: Exception) { + "0.00" + } +} + +fun Path.exists(): Boolean = Files.exists(this) + +fun Path.isFile(): Boolean = !Files.isDirectory(this) + +fun Path.delete(): Boolean { + return if (isFile() && exists()) { + //Actual delete operation + Files.delete(this) + true + } else { + false + } +} + +val Any.TAG: String + get() { + val tag = javaClass.simpleName + return if (tag.length <= 23) tag else tag.substring(0, 23) + } \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/utils/HelperUtil.kt b/app/src/main/java/com/iesoluciones/siodrenax/utils/HelperUtil.kt new file mode 100644 index 0000000..058e967 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/utils/HelperUtil.kt @@ -0,0 +1,464 @@ +package com.iesoluciones.siodrenax.utils + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.Handler +import android.os.Looper +import android.os.StrictMode +import android.provider.Settings +import android.util.Log +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import androidx.work.Constraints +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import com.iesoluciones.siodrenax.* +import com.iesoluciones.siodrenax.App.Companion.context +import com.iesoluciones.siodrenax.activities.LoginActivity +import com.iesoluciones.siodrenax.entities.* +import com.iesoluciones.siodrenax.fragments.dialogs.IncidentsInputFragment +import com.iesoluciones.siodrenax.fragments.dialogs.MileageInputDialogFragment +import com.iesoluciones.siodrenax.fragments.dialogs.VehicleIncidentsFragment +import com.iesoluciones.siodrenax.interfaces.OnIncidentListener +import com.iesoluciones.siodrenax.interfaces.OnIncidentShowListener +import com.iesoluciones.siodrenax.interfaces.OnMileageListener +import com.iesoluciones.siodrenax.network.HttpError +import com.iesoluciones.siodrenax.network.UnprocessableEntity +import com.iesoluciones.siodrenax.repositories.WorkdayRepository +import com.iesoluciones.siodrenax.utils.Constants.Companion.BUFFER_SIZE +import com.iesoluciones.siodrenax.utils.Constants.Companion.BUFFER_SIZE_SKETCH +import com.iesoluciones.siodrenax.workers.SyncWorker +import retrofit2.HttpException +import java.io.* +import java.net.* +import java.nio.file.Files +import java.nio.file.Paths +import java.util.* +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import javax.net.ssl.SSLPeerUnverifiedException + + +class HelperUtil { + + private val tagIncidenceDialog = "incidenceDialog" + private val tagShowIncidenceDialog = "showIncidenceDialog" + private val tagMileageDialog = "mileageDialog" + + fun parseError(context: Context, e: Throwable) { + + if (e is HttpException) { + when (e.code()) { + 401, 400, 403, 404, 405, 423 -> { + val error: HttpError? = HttpError.parseException(e) + if (error != null) { + AlertDialog.Builder(context, R.style.MyAlertDialogStyle) + .setMessage(error.error) + .setTitle(R.string.title_error) + .setCancelable(false) + .setPositiveButton( + App.shareInstance!!.resources.getString(R.string.button_ok), + null + ) + .show() + } + } + 420 -> { + val error: HttpError? = HttpError.parseException(e) + if (error != null) { + val workdayRepository = WorkdayRepository() + if (workdayRepository.validateRenewSession()) { + AlertDialog.Builder(context, R.style.MyAlertDialogStyle) + .setMessage(R.string.session_expired_message) + .setTitle(R.string.session_expired) + .setCancelable(false) + .setPositiveButton(App.shareInstance!!.resources.getString(R.string.button_renew_session)) { _, _ -> + workdayRepository.logout() + val intent = Intent(context, LoginActivity::class.java); + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + (context as Activity).startActivity(intent) + (context as Activity).finish() + } + .show() + } else { + AlertDialog.Builder(context, R.style.MyAlertDialogStyle) + .setMessage(R.string.session_expired_with_orders_message) + .setTitle(R.string.session_expired) + .setCancelable(false) + .setPositiveButton(App.shareInstance!!.resources.getString(R.string.button_ok), null) + .show() + } + + + } + } + 422 -> { + val errors = UnprocessableEntity.parseException(e) + AlertDialog.Builder(context, R.style.MyAlertDialogStyle) + .setMessage(errors!!.errors[0]) + .setTitle(errors.message) + .setCancelable(false) + .setPositiveButton( + App.shareInstance!!.resources.getString(R.string.button_ok), + null + ) + .show() + } + 500 -> AlertDialog.Builder(context, R.style.MyAlertDialogStyle) + .setMessage(App.shareInstance!!.resources.getString(R.string.unexpected_error)) + .setTitle(App.shareInstance!!.resources.getString(R.string.title_error)) + .setCancelable(false) + .setPositiveButton(context.resources.getString(R.string.button_ok), null) + .show() + } + } else if (e is IOException) { + when (e) { + is ConnectException -> AlertDialog.Builder(context, R.style.MyAlertDialogStyle) + .setMessage(App.shareInstance!!.resources.getString(R.string.unreachable_network)) + .setTitle(App.shareInstance!!.resources.getString(R.string.title_error)) + .setCancelable(false) + .setPositiveButton( + App.shareInstance!!.resources.getString(R.string.button_ok), + null + ) + .setNeutralButton(App.shareInstance!!.resources.getString(R.string.network_settings)) { _, _ -> + context.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS)) + }.show() + + is SocketException -> AlertDialog.Builder(context, R.style.MyAlertDialogStyle) + .setMessage(App.shareInstance!!.resources.getString(R.string.unreachable_network)) + .setTitle(App.shareInstance!!.resources.getString(R.string.title_error)) + .setCancelable(false) + .setPositiveButton( + App.shareInstance!!.resources.getString(R.string.button_ok), + null + ) + .setNeutralButton(App.shareInstance!!.resources.getString(R.string.network_settings)) { _, _ -> + context.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS)) + }.show() + + is SocketTimeoutException -> AlertDialog.Builder( + context, + R.style.MyAlertDialogStyle + ) + .setMessage(App.shareInstance!!.resources.getString(R.string.timeout)) + .setTitle(App.shareInstance!!.resources.getString(R.string.title_error)) + .setCancelable(false) + .setPositiveButton( + App.shareInstance!!.resources.getString(R.string.button_ok), + null + ) + .show() + + is UnknownHostException -> Toast.makeText( + context, + context.getString(R.string.unknown_host_exception), + Toast.LENGTH_LONG + ).show() + + is SSLPeerUnverifiedException -> AlertDialog.Builder( + context, + R.style.MyAlertDialogStyle + ) + .setMessage(App.shareInstance!!.resources.getString(R.string.certificate_error_message)) + .setTitle(App.shareInstance!!.resources.getString(R.string.certificate_error)) + .setCancelable(false) + .setPositiveButton( + App.shareInstance!!.resources.getString(R.string.button_ok), + null + ) + .show() + + else -> Toast.makeText( + context, + context.getString(R.string.unexpected_error), + Toast.LENGTH_LONG + ).show() + } + } + } + + fun incidentShowDialogFragment( + fm: FragmentManager, + listener: OnIncidentShowListener, + incidence: String? + ) { + val ft = fm.beginTransaction() + val prev = fm.findFragmentByTag(tagShowIncidenceDialog) + if (prev != null) { + ft.remove(prev) + } + ft.addToBackStack(null) + Handler(Looper.getMainLooper()).postDelayed({ + val dialogFragment: DialogFragment = VehicleIncidentsFragment.newInstance( + listener, + incidence + ) + dialogFragment.show(ft, tagShowIncidenceDialog) + }, 500) + } + + fun createEvidences(order: Order) { + + val evidence = evidenceBox.query() + .equal(Evidence_.idOrder, order.id) + .and().equal(Evidence_.idRequest, order.idRequest) + .build() + .findFirst() + + if(evidence == null) + this.createEvidenceHolders(order.id, order.idRequest) + } + + private fun createEvidenceHolders(idOrder: Long, idRequest: Long) { + for (i in 1..9) { + val evidence = Evidence() + evidence.idOrder = idOrder + evidence.idRequest = idRequest + when (i) { + 1, 2, 3 -> { + evidence.type = Constants.EVIDENCE_START + evidence.evidenceNo = ((i.toString() + "").toLong()) + } + 4, 5, 6 -> { + evidence.type = Constants.EVIDENCE_PROCESS + evidence.evidenceNo = ((i - 3).toString() + "").toLong() + } + 7, 8, 9 -> { + evidence.type = Constants.EVIDENCE_FINAL + evidence.evidenceNo = ((i - 6).toString() + "").toLong() + } + else -> { + evidence.type = Constants.EVIDENCE_START + 1 + evidence.evidenceNo = (i.toString() + "").toLong() + } + } + + try { + evidenceBox.put(evidence) + } catch (ex: Exception) { + Log.e("CREAR EVIDENCIA", ex.toString()) + } + } + } + + fun mileageDialogFragment(fm: FragmentManager, listener: OnMileageListener) { + val ft = fm.beginTransaction() + val prev = fm.findFragmentByTag(tagMileageDialog) + if (prev != null) { + ft.remove(prev) + } + ft.addToBackStack(null) + Handler(Looper.getMainLooper()).postDelayed({ + val dialogFragment: DialogFragment = + MileageInputDialogFragment.newInstance(listener) + dialogFragment.show(ft, tagMileageDialog) + + }, 0) + } + + fun incidentDialogFragment(fm: FragmentManager, listener: OnIncidentListener) { + val ft = fm.beginTransaction() + val prev = fm.findFragmentByTag(tagIncidenceDialog) + if (prev != null) { + ft.remove(prev) + } + ft.addToBackStack(null) + Handler(Looper.getMainLooper()).postDelayed({ + val dialogFragment: DialogFragment = IncidentsInputFragment.newInstance(listener) + dialogFragment.show(ft, tagIncidenceDialog) + }, 500) + } + + fun formatTime(millis: Long): String { + val secs = millis / 1000 + + return String.format("%02d:%02d:%02d", secs / 3600, secs % 3600 / 60, secs % 60) + } + + fun decodeFromFile(path: String, sampleSize: Int): Bitmap { + val options2 = BitmapFactory.Options() + options2.inSampleSize = sampleSize + + return if (sampleSize != 0) BitmapFactory.decodeFile( + path, + options2 + ) else BitmapFactory.decodeFile( + path, + null + ) + } + + fun saveBitmap(filePath: File, filename: String, bitmap: Bitmap): String? { + val file = File(filePath, filename) + val fos: FileOutputStream + + try { + fos = FileOutputStream(file) + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos) + fos.flush() + } catch (e: java.lang.Exception) { + e.printStackTrace() + return null + } + + return file.absolutePath + } + + fun startSync() { + //System.gc() + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val syncWorker = OneTimeWorkRequest.Builder(SyncWorker::class.java) + .setConstraints(constraints) + .build() + + Toast.makeText( + context, + context!!.getString(R.string.toast_sync_progress), + Toast.LENGTH_LONG + ).show() + + WorkManager.getInstance().enqueue(syncWorker) + } + + /** + * Extracts first letter of Name and LastName sent in Params + * + * @param name Name + * @param lastname Last Name + * @return String with 2 Letters in UpperCase + */ + fun getInitials(name: String?, lastname: String?): String { + var initials = "" + if (name != null) if (name.isNotEmpty() && name.substring(0, 1) != " ") { + initials += name.substring(0, 1) + } + if (lastname != null) if (lastname.isNotEmpty() && lastname.substring(0, 1) != " ") { + initials += lastname.substring(0, 1) + } + return initials.toUpperCase(Locale.ROOT) + } + + @Throws(IOException::class) + fun zip(files: Array, zipFile: String): String { + var origin: BufferedInputStream? + val out = ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))) + try { + val data = ByteArray(BUFFER_SIZE) + for (i in files.indices) { + val fi = FileInputStream(files[i]) + origin = BufferedInputStream(fi, BUFFER_SIZE) + try { + val entry = ZipEntry(files[i].substring(files[i].lastIndexOf("/") + 1)) + out.putNextEntry(entry) + var count: Int + while (origin.read(data, 0, BUFFER_SIZE) + .also { count = it } != -1 + ) { + out.write(data, 0, count) + } + } finally { + origin.close() + } + } + } finally { + out.close() + } + + return zipFile + } + + fun deleteFile(path: String): Boolean{ + val p = Paths.get(path) + return p.delete() + } + + fun encodeBase64(filePath: String): String?{ + return try { + val bytes = File(filePath).readBytes() + Base64.getEncoder().encodeToString(bytes) + }catch (e: Exception){ + null + } + } + + fun encodeBase64WithData(filePath: String): String?{ + return try { + val bytes = File(filePath).readBytes() + val base64 = Base64.getEncoder().encodeToString(bytes) + val extension = filePath.split(".").last() + "data:image/$extension;base64,$base64" + }catch (e: Exception){ + null + } + } + + fun scaleBitmap(rotated: Bitmap, maxSize: Int = 300): Bitmap? { + val outWidth: Int + val outHeight: Int + val inWidth = rotated.width + val inHeight = rotated.height + if (inWidth > inHeight) { + outWidth = maxSize + outHeight = inHeight * maxSize / inWidth + } else { + outHeight = maxSize + outWidth = inWidth * maxSize / inHeight + } + + return Bitmap.createScaledBitmap(rotated, outWidth, outHeight, false) + } + + fun downloadFile(fileUrl: String?, directory: File?, order: Order?) { + val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() + StrictMode.setThreadPolicy(policy) + try { + val url = URL(fileUrl) + val urlConnection: HttpURLConnection = url.openConnection() as HttpURLConnection + urlConnection.connect() + val inputStream: InputStream = urlConnection.inputStream + val fileOutputStream = FileOutputStream(directory) + val buffer = ByteArray(BUFFER_SIZE_SKETCH) + var bufferLength = 0 + while (inputStream.read(buffer).also { bufferLength = it } > 0) { + fileOutputStream.write(buffer, 0, bufferLength) + } + fileOutputStream.close() + + if(order != null){ + order.sketchPath = directory!!.absolutePath + orderBox.put(order) + } + + } catch (e: FileNotFoundException) { + e.printStackTrace() + } catch (e: MalformedURLException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + } + + fun downloadSketchs(filesDir:String, orders: List){ + + if(!File("$filesDir/pdf_sketchs").isDirectory){ + Files.createDirectory(Paths.get("$filesDir/pdf_sketchs")) + } + + for (order in orders) { + if(order.sketchName != null && order.sketchPath == null){ + downloadFile("${BuildConfig.STORAGE_URL}pdf_croquis/${order.sketchName}", File("$filesDir/pdf_sketchs/${order.sketchName}"), order) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/utils/ImageUtils.java b/app/src/main/java/com/iesoluciones/siodrenax/utils/ImageUtils.java new file mode 100644 index 0000000..b676efd --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/utils/ImageUtils.java @@ -0,0 +1,129 @@ +package com.iesoluciones.siodrenax.utils; + +import static com.iesoluciones.siodrenax.utils.Constants.COMPRESS_JPEG_QUALITY_LOW; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.media.ExifInterface; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class ImageUtils { + public static ImageUtils mInstant; + + public static ImageUtils getInstant(){ + if(mInstant==null){ + mInstant = new ImageUtils(); + } + return mInstant; + } + + public Bitmap getCompressedBitmap(String imagePath) { + float maxHeight = 1920.0f; + float maxWidth = 1080.0f; + Bitmap scaledBitmap = null; + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + Bitmap bmp = BitmapFactory.decodeFile(imagePath, options); + + int actualHeight = options.outHeight; + int actualWidth = options.outWidth; + float imgRatio = (float) actualWidth / (float) actualHeight; + float maxRatio = maxWidth / maxHeight; + + if (actualHeight > maxHeight || actualWidth > maxWidth) { + if (imgRatio < maxRatio) { + imgRatio = maxHeight / actualHeight; + actualWidth = (int) (imgRatio * actualWidth); + actualHeight = (int) maxHeight; + } else if (imgRatio > maxRatio) { + imgRatio = maxWidth / actualWidth; + actualHeight = (int) (imgRatio * actualHeight); + actualWidth = (int) maxWidth; + } else { + actualHeight = (int) maxHeight; + actualWidth = (int) maxWidth; + + } + } + + options.inSampleSize = calculateInSampleSize(options, actualWidth, actualHeight); + options.inJustDecodeBounds = false; + options.inDither = false; + options.inPurgeable = true; + options.inInputShareable = true; + options.inTempStorage = new byte[16 * 1024]; + + try { + bmp = BitmapFactory.decodeFile(imagePath, options); + } catch (OutOfMemoryError exception) { + exception.printStackTrace(); + + } + try { + scaledBitmap = Bitmap.createBitmap(actualWidth, actualHeight, Bitmap.Config.ARGB_8888); + } catch (OutOfMemoryError exception) { + exception.printStackTrace(); + } + + float ratioX = actualWidth / (float) options.outWidth; + float ratioY = actualHeight / (float) options.outHeight; + float middleX = actualWidth / 2.0f; + float middleY = actualHeight / 2.0f; + + Matrix scaleMatrix = new Matrix(); + scaleMatrix.setScale(ratioX, ratioY, middleX, middleY); + + Canvas canvas = new Canvas(scaledBitmap); + canvas.setMatrix(scaleMatrix); + canvas.drawBitmap(bmp, middleX - bmp.getWidth() / 2, middleY - bmp.getHeight() / 2, new Paint(Paint.FILTER_BITMAP_FLAG)); + + ExifInterface exif = null; + try { + exif = new ExifInterface(imagePath); + int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0); + Matrix matrix = new Matrix(); + if (orientation == 6) { + matrix.postRotate(90); + } else if (orientation == 3) { + matrix.postRotate(180); + } else if (orientation == 8) { + matrix.postRotate(270); + } + scaledBitmap = Bitmap.createBitmap(scaledBitmap, 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight(), matrix, true); + } catch (IOException e) { + e.printStackTrace(); + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + scaledBitmap.compress(Bitmap.CompressFormat.JPEG, COMPRESS_JPEG_QUALITY_LOW, out); + + byte[] byteArray = out.toByteArray(); + + Bitmap updatedBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length); + + return updatedBitmap; + } + + private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + + if (height > reqHeight || width > reqWidth) { + final int heightRatio = Math.round((float) height / (float) reqHeight); + final int widthRatio = Math.round((float) width / (float) reqWidth); + inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; + } + final float totalPixels = width * height; + final float totalReqPixelsCap = reqWidth * reqHeight * 2; + + while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) { + inSampleSize++; + } + return inSampleSize; + } +} diff --git a/app/src/main/java/com/iesoluciones/siodrenax/utils/ListPaddingDecoration.kt b/app/src/main/java/com/iesoluciones/siodrenax/utils/ListPaddingDecoration.kt new file mode 100644 index 0000000..005db52 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/utils/ListPaddingDecoration.kt @@ -0,0 +1,34 @@ +package com.iesoluciones.siodrenax.utils + +import android.content.Context +import android.graphics.Rect +import android.util.TypedValue +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration + +class ListPaddingDecoration(context: Context) : ItemDecoration() { + private val mPadding: Int + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + val itemPosition = parent.getChildAdapterPosition(view) + if (itemPosition == RecyclerView.NO_POSITION) { + return + } + if (itemPosition == 0) { + outRect.top = mPadding + } + val adapter = parent.adapter + if (adapter != null && itemPosition == adapter.itemCount - 1) { + outRect.bottom = mPadding + } + } + + companion object { + private const val PADDING_IN_DIPS = 8 + } + + init { + val metrics = context.resources.displayMetrics + mPadding = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, PADDING_IN_DIPS.toFloat(), metrics).toInt() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/utils/NotificationUtils.kt b/app/src/main/java/com/iesoluciones/siodrenax/utils/NotificationUtils.kt new file mode 100644 index 0000000..fbb8971 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/utils/NotificationUtils.kt @@ -0,0 +1,91 @@ +package com.iesoluciones.siodrenax.utils + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.media.RingtoneManager +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.iesoluciones.siodrenax.App +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.activities.OrdersActivity +import com.iesoluciones.siodrenax.utils.Constants.Companion.EXTRA_NOTIFICATION_CLICKED + +class NotificationUtils { + + fun showNotification(title: String?, message: String?) { + + val context: Context = App.context!! + val notificationId = System.currentTimeMillis().toInt() + val notificationManager = NotificationManagerCompat.from(context) + createNotificationChannel(context) + + val builder: NotificationCompat.Builder = NotificationCompat.Builder( + context, + context.getString(R.string.default_notification_channel_id) + ) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(title) + .setContentText(message) + .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) + .setPriority(NotificationCompat.PRIORITY_MAX) //FOR HEADS-UP NOTIFICATIONS + .setDefaults(Notification.DEFAULT_VIBRATE) + .setAutoCancel(true) + .setWhen(System.currentTimeMillis()) + .setOngoing(false) + + goToOrdersActivity(context, notificationId, builder) + + builder.setChannelId(context.getString(R.string.default_notification_channel_id)) + + // ----- Construct notification ----- + val notification = builder.build() + notificationManager.notify(notificationId, notification) + } + + private fun createNotificationChannel(context: Context) { + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + val description = context.getString(R.string.default_notification_channel_description) + + // ----- Configure notification channel ----- + val channel = NotificationChannel( + context.getString(R.string.default_notification_channel_id), + context.getString(R.string.default_notification_channel_name), + NotificationManager.IMPORTANCE_HIGH + ) + channel.description = description + channel.enableLights(true) + channel.lightColor = Color.RED + channel.enableVibration(true) + // ------------------------------------------ + + // Register the channel with the system + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + + private fun goToOrdersActivity( + context: Context, + notificationId: Int, + builder: NotificationCompat.Builder + ) { + val mainIntent = Intent(context, OrdersActivity::class.java) + mainIntent.putExtra(EXTRA_NOTIFICATION_CLICKED, true) + mainIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + + val pendingIntent = PendingIntent.getActivity( + context, + notificationId, + mainIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + builder.setContentIntent(pendingIntent) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/utils/NumberFormatterTextWatcher.kt b/app/src/main/java/com/iesoluciones/siodrenax/utils/NumberFormatterTextWatcher.kt new file mode 100644 index 0000000..600c75e --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/utils/NumberFormatterTextWatcher.kt @@ -0,0 +1,34 @@ +package com.iesoluciones.siodrenax.utils + +import android.text.Editable +import android.text.TextWatcher +import androidx.appcompat.widget.AppCompatEditText +import java.text.DecimalFormat +import java.text.NumberFormat +import java.util.* + +class NumberFormatterTextWatcher(private var editText: AppCompatEditText) : + TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + override fun afterTextChanged(editable: Editable) { + editText.removeTextChangedListener(this) + try { + var originalString = editable.toString() + if (originalString.contains(",")) { + originalString = originalString.replace(",".toRegex(), "") + } + val longval: Long = originalString.toLong() + val formatter = NumberFormat.getInstance(Locale.US) as DecimalFormat + formatter.applyPattern("#,###,###,###") + val formattedString = formatter.format(longval) + + //setting text after format to EditText + editText.setText(formattedString) + editText.setSelection(editText.text!!.length) + } catch (nfe: NumberFormatException) { + nfe.printStackTrace() + } + editText.addTextChangedListener(this) + } +} diff --git a/app/src/main/java/com/iesoluciones/siodrenax/utils/StatusDrawable.kt b/app/src/main/java/com/iesoluciones/siodrenax/utils/StatusDrawable.kt new file mode 100644 index 0000000..1ab582f --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/utils/StatusDrawable.kt @@ -0,0 +1,39 @@ +package com.iesoluciones.siodrenax.utils + +import android.graphics.* +import android.graphics.drawable.Drawable + +class StatusDrawable(private val color1: String, private val color2: String?) : Drawable() { + override fun draw(canvas: Canvas) { + val width = bounds.width().toFloat() + val height = bounds.height().toFloat() + + val radius: Float = if (width > height) { + height / 2 + } else { + width / 2 + } + val path = Path() + path.addCircle(width, + height, radius, + Path.Direction.CW) + val paint = Paint() + paint.color = Color.parseColor(color1.ifEmpty { "#ffffff" }) + paint.style = Paint.Style.FILL + val oval = RectF() + paint.style = Paint.Style.FILL_AND_STROKE + val centerX: Float = width / 2 + val centerY: Float = height / 2 + oval[centerX - radius, centerY - radius, centerX + radius] = centerY + radius + paint.isAntiAlias = true + canvas.drawArc(oval, 0f, 180f, false, paint) + if (color2 != null) paint.color = Color.parseColor(color2) + canvas.drawArc(oval, 0f, -180f, false, paint) + } + + override fun setAlpha(alpha: Int) {} + override fun setColorFilter(colorFilter: ColorFilter?) {} + override fun getOpacity(): Int { + return PixelFormat.UNKNOWN + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/utils/TouchImageView.java b/app/src/main/java/com/iesoluciones/siodrenax/utils/TouchImageView.java new file mode 100644 index 0000000..e8fb31a --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/utils/TouchImageView.java @@ -0,0 +1,1120 @@ +package com.iesoluciones.siodrenax.utils; + + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.OverScroller; +import android.widget.Scroller; + +public class TouchImageView extends androidx.appcompat.widget.AppCompatImageView { + + // + // SuperMin and SuperMax multipliers. Determine how much the image can be + // zoomed below or above the zoom boundaries, before animating back to the + // min/max zoom boundary. + // + private static final float SUPER_MIN_MULTIPLIER = .75f; + private static final float SUPER_MAX_MULTIPLIER = 1.25f; + + // + // Scale of image ranges from minScale to maxScale, where minScale == 1 + // when the image is stretched to fit view. + // + private float normalizedScale; + + // + // Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal. + // MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix + // saved prior to the screen rotating. + // + private Matrix matrix, prevMatrix; + + private enum State {NONE, DRAG, ZOOM, FLING, ANIMATE_ZOOM} + + private State state; + + private float minScale; + private float maxScale; + private float superMinScale; + private float superMaxScale; + private float[] m; + + private Context context; + private Fling fling; + + private ScaleType mScaleType; + + private boolean imageRenderedAtLeastOnce; + private boolean onDrawReady; + + private ZoomVariables delayedZoomVariables; + + // + // Size of view and previous view size (ie before rotation) + // + private int viewWidth, viewHeight, prevViewWidth, prevViewHeight; + + // + // Size of image when it is stretched to fit view. Before and After rotation. + // + private float matchViewWidth, matchViewHeight, prevMatchViewWidth, prevMatchViewHeight; + + private ScaleGestureDetector mScaleDetector; + private GestureDetector mGestureDetector; + private OnTouchListener userTouchListener = null; + + public TouchImageView(Context context) { + super(context); + sharedConstructing(context); + } + + public TouchImageView(Context context, AttributeSet attrs) { + super(context, attrs); + sharedConstructing(context); + } + + public TouchImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + sharedConstructing(context); + } + + private void sharedConstructing(Context context) { + super.setClickable(true); + this.context = context; + mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); + mGestureDetector = new GestureDetector(context, new GestureListener()); + matrix = new Matrix(); + prevMatrix = new Matrix(); + m = new float[9]; + normalizedScale = 1; + if (mScaleType == null) { + mScaleType = ScaleType.FIT_CENTER; + } + minScale = 1; + maxScale = 3; + superMinScale = SUPER_MIN_MULTIPLIER * minScale; + superMaxScale = SUPER_MAX_MULTIPLIER * maxScale; + setImageMatrix(matrix); + setScaleType(ScaleType.MATRIX); + setState(State.NONE); + onDrawReady = false; + super.setOnTouchListener(new PrivateOnTouchListener()); + } + + @Override + public void setOnTouchListener(OnTouchListener l) { + userTouchListener = l; + } + + @Override + public void setImageResource(int resId) { + super.setImageResource(resId); + savePreviousImageValues(); + fitImageToView(); + } + + @Override + public void setImageBitmap(Bitmap bm) { + super.setImageBitmap(bm); + savePreviousImageValues(); + fitImageToView(); + } + + @Override + public void setImageDrawable(Drawable drawable) { + super.setImageDrawable(drawable); + savePreviousImageValues(); + fitImageToView(); + } + + @Override + public void setImageURI(Uri uri) { + super.setImageURI(uri); + savePreviousImageValues(); + fitImageToView(); + } + + @Override + public void setScaleType(ScaleType type) { + if (type == ScaleType.FIT_START || type == ScaleType.FIT_END) { + throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END"); + } + if (type == ScaleType.MATRIX) { + super.setScaleType(ScaleType.MATRIX); + + } else { + mScaleType = type; + if (onDrawReady) { + // + // If the image is already rendered, scaleType has been called programmatically + // and the TouchImageView should be updated with the new scaleType. + // + setZoom(this); + } + } + } + + @Override + public ScaleType getScaleType() { + return mScaleType; + } + + /** + * Returns false if image is in initial, unzoomed state. False, otherwise. + * + * @return true if image is zoomed + */ + public boolean isZoomed() { + return normalizedScale != 1; + } + + /** + * Save the current matrix and view dimensions + * in the prevMatrix and prevView variables. + */ + private void savePreviousImageValues() { + if (matrix != null && viewHeight != 0 && viewWidth != 0) { + matrix.getValues(m); + prevMatrix.setValues(m); + prevMatchViewHeight = matchViewHeight; + prevMatchViewWidth = matchViewWidth; + prevViewHeight = viewHeight; + prevViewWidth = viewWidth; + } + } + + @Override + public Parcelable onSaveInstanceState() { + Bundle bundle = new Bundle(); + bundle.putParcelable("instanceState", super.onSaveInstanceState()); + bundle.putFloat("saveScale", normalizedScale); + bundle.putFloat("matchViewHeight", matchViewHeight); + bundle.putFloat("matchViewWidth", matchViewWidth); + bundle.putInt("viewWidth", viewWidth); + bundle.putInt("viewHeight", viewHeight); + matrix.getValues(m); + bundle.putFloatArray("matrix", m); + bundle.putBoolean("imageRendered", imageRenderedAtLeastOnce); + return bundle; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof Bundle) { + Bundle bundle = (Bundle) state; + normalizedScale = bundle.getFloat("saveScale"); + m = bundle.getFloatArray("matrix"); + prevMatrix.setValues(m); + prevMatchViewHeight = bundle.getFloat("matchViewHeight"); + prevMatchViewWidth = bundle.getFloat("matchViewWidth"); + prevViewHeight = bundle.getInt("viewHeight"); + prevViewWidth = bundle.getInt("viewWidth"); + imageRenderedAtLeastOnce = bundle.getBoolean("imageRendered"); + super.onRestoreInstanceState(bundle.getParcelable("instanceState")); + return; + } + + super.onRestoreInstanceState(state); + } + + @Override + protected void onDraw(Canvas canvas) { + onDrawReady = true; + imageRenderedAtLeastOnce = true; + if (delayedZoomVariables != null) { + setZoom(delayedZoomVariables.scale, delayedZoomVariables.focusX, delayedZoomVariables.focusY, delayedZoomVariables.scaleType); + delayedZoomVariables = null; + } + super.onDraw(canvas); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + savePreviousImageValues(); + } + + /** + * Get the current zoom. This is the zoom relative to the initial + * scale, not the original resource. + * + * @return current zoom multiplier. + */ + public float getCurrentZoom() { + return normalizedScale; + } + + /** + * Reset zoom and translation to initial state. + */ + public void resetZoom() { + normalizedScale = 1; + fitImageToView(); + } + + /** + * Set zoom to the specified scale. Image will be centered around the point + * (focusX, focusY). These floats range from 0 to 1 and denote the focus point + * as a fraction from the left and top of the view. For example, the top left + * corner of the image would be (0, 0). And the bottom right corner would be (1, 1). + * + * @param scale + * @param focusX + * @param focusY + * @param scaleType + */ + public void setZoom(float scale, float focusX, float focusY, ScaleType scaleType) { + // + // setZoom can be called before the image is on the screen, but at this point, + // image and view sizes have not yet been calculated in onMeasure. Thus, we should + // delay calling setZoom until the view has been measured. + // + if (!onDrawReady) { + delayedZoomVariables = new ZoomVariables(scale, focusX, focusY, scaleType); + return; + } + + if (scaleType != mScaleType) { + setScaleType(scaleType); + } + resetZoom(); + scaleImage(scale, viewWidth / 2f, viewHeight / 2f, true); + matrix.getValues(m); + m[Matrix.MTRANS_X] = -((focusX * getImageWidth()) - (viewWidth * 0.5f)); + m[Matrix.MTRANS_Y] = -((focusY * getImageHeight()) - (viewHeight * 0.5f)); + matrix.setValues(m); + fixTrans(); + setImageMatrix(matrix); + } + + /** + * Set zoom parameters equal to another TouchImageView. Including scale, position, + * and ScaleType. + */ + public void setZoom(com.iesoluciones.siodrenax.utils.TouchImageView img) { + PointF center = img.getScrollPosition(); + setZoom(img.getCurrentZoom(), center.x, center.y, img.getScaleType()); + } + + /** + * Return the point at the center of the zoomed image. The PointF coordinates range + * in value between 0 and 1 and the focus point is denoted as a fraction from the left + * and top of the view. For example, the top left corner of the image would be (0, 0). + * And the bottom right corner would be (1, 1). + * + * @return PointF representing the scroll position of the zoomed image. + */ + public PointF getScrollPosition() { + Drawable drawable = getDrawable(); + if (drawable == null) { + return null; + } + int drawableWidth = drawable.getIntrinsicWidth(); + int drawableHeight = drawable.getIntrinsicHeight(); + + PointF point = transformCoordTouchToBitmap(viewWidth / 2f, viewHeight / 2f, true); + point.x /= drawableWidth; + point.y /= drawableHeight; + return point; + } + + /** + * Performs boundary checking and fixes the image matrix if it + * is out of bounds. + */ + private void fixTrans() { + matrix.getValues(m); + float transX = m[Matrix.MTRANS_X]; + float transY = m[Matrix.MTRANS_Y]; + + float fixTransX = getFixTrans(transX, viewWidth, getImageWidth()); + float fixTransY = getFixTrans(transY, viewHeight, getImageHeight()); + + if (fixTransX != 0 || fixTransY != 0) { + matrix.postTranslate(fixTransX, fixTransY); + } + } + + /** + * When transitioning from zooming from focus to zoom from center (or vice versa) + * the image can become unaligned within the view. This is apparent when zooming + * quickly. When the content size is less than the view size, the content will often + * be centered incorrectly within the view. fixScaleTrans first calls fixTrans() and + * then makes sure the image is centered correctly within the view. + */ + private void fixScaleTrans() { + fixTrans(); + matrix.getValues(m); + if (getImageWidth() < viewWidth) { + m[Matrix.MTRANS_X] = (viewWidth - getImageWidth()) / 2; + } + + if (getImageHeight() < viewHeight) { + m[Matrix.MTRANS_Y] = (viewHeight - getImageHeight()) / 2; + } + matrix.setValues(m); + } + + private float getFixTrans(float trans, float viewSize, float contentSize) { + float minTrans, maxTrans; + + if (contentSize <= viewSize) { + minTrans = 0; + maxTrans = viewSize - contentSize; + + } else { + minTrans = viewSize - contentSize; + maxTrans = 0; + } + + if (trans < minTrans) + return -trans + minTrans; + if (trans > maxTrans) + return -trans + maxTrans; + return 0; + } + + private float getFixDragTrans(float delta, float viewSize, float contentSize) { + if (contentSize <= viewSize) { + return 0; + } + return delta; + } + + private float getImageWidth() { + return matchViewWidth * normalizedScale; + } + + private float getImageHeight() { + return matchViewHeight * normalizedScale; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + Drawable drawable = getDrawable(); + if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) { + setMeasuredDimension(0, 0); + return; + } + + int drawableWidth = drawable.getIntrinsicWidth(); + int drawableHeight = drawable.getIntrinsicHeight(); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + viewWidth = setViewSize(widthMode, widthSize, drawableWidth); + viewHeight = setViewSize(heightMode, heightSize, drawableHeight); + + // + // Set view dimensions + // + setMeasuredDimension(viewWidth, viewHeight); + + // + // Fit content within view + // + fitImageToView(); + } + + /** + * If the normalizedScale is equal to 1, then the image is made to fit the screen. Otherwise, + * it is made to fit the screen according to the dimensions of the previous image matrix. This + * allows the image to maintain its zoom after rotation. + */ + private void fitImageToView() { + Drawable drawable = getDrawable(); + if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) { + return; + } + if (matrix == null || prevMatrix == null) { + return; + } + + int drawableWidth = drawable.getIntrinsicWidth(); + int drawableHeight = drawable.getIntrinsicHeight(); + + // + // Scale image for view + // + float scaleX = (float) viewWidth / drawableWidth; + float scaleY = (float) viewHeight / drawableHeight; + + switch (mScaleType) { + case CENTER: + scaleX = scaleY = 1; + break; + + case CENTER_CROP: + scaleX = scaleY = Math.max(scaleX, scaleY); + break; + + case CENTER_INSIDE: + scaleX = scaleY = Math.min(1, Math.min(scaleX, scaleY)); + + case FIT_CENTER: + scaleX = scaleY = Math.min(scaleX, scaleY); + break; + + case FIT_XY: + break; + + default: + // + // FIT_START and FIT_END not supported + // + throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END"); + + } + + // + // Center the image + // + float redundantXSpace = viewWidth - (scaleX * drawableWidth); + float redundantYSpace = viewHeight - (scaleY * drawableHeight); + matchViewWidth = viewWidth - redundantXSpace; + matchViewHeight = viewHeight - redundantYSpace; + if (!isZoomed() && !imageRenderedAtLeastOnce) { + // + // Stretch and center image to fit view + // + matrix.setScale(scaleX, scaleY); + matrix.postTranslate(redundantXSpace / 2, redundantYSpace / 2); + normalizedScale = 1; + + } else { + // + // These values should never be 0 or we will set viewWidth and viewHeight + // to NaN in translateMatrixAfterRotate. To avoid this, call savePreviousImageValues + // to set them equal to the current values. + // + if (prevMatchViewWidth == 0 || prevMatchViewHeight == 0) { + savePreviousImageValues(); + } + + prevMatrix.getValues(m); + + // + // Rescale Matrix after rotation + // + m[Matrix.MSCALE_X] = matchViewWidth / drawableWidth * normalizedScale; + m[Matrix.MSCALE_Y] = matchViewHeight / drawableHeight * normalizedScale; + + // + // TransX and TransY from previous matrix + // + float transX = m[Matrix.MTRANS_X]; + float transY = m[Matrix.MTRANS_Y]; + + // + // Width + // + float prevActualWidth = prevMatchViewWidth * normalizedScale; + float actualWidth = getImageWidth(); + translateMatrixAfterRotate(Matrix.MTRANS_X, transX, prevActualWidth, actualWidth, prevViewWidth, viewWidth, drawableWidth); + + // + // Height + // + float prevActualHeight = prevMatchViewHeight * normalizedScale; + float actualHeight = getImageHeight(); + translateMatrixAfterRotate(Matrix.MTRANS_Y, transY, prevActualHeight, actualHeight, prevViewHeight, viewHeight, drawableHeight); + + // + // Set the matrix to the adjusted scale and translate values. + // + matrix.setValues(m); + } + fixTrans(); + setImageMatrix(matrix); + } + + /** + * Set view dimensions based on layout params + * + * @param mode + * @param size + * @param measure + * @return + */ + private int setViewSize(int mode, int size, int measure) { + int viewSize; + switch (mode) { + case MeasureSpec.AT_MOST: + viewSize = Math.min(measure, size); + break; + + case MeasureSpec.UNSPECIFIED: + viewSize = measure; + break; + + default: + viewSize = size; + break; + } + return viewSize; + } + + /** + * After rotating, the matrix needs to be translated. This function finds the area of image + * which was previously centered and adjusts translations so that is again the center, post-rotation. + * + * @param axis Matrix.MTRANS_X or Matrix.MTRANS_Y + * @param trans the value of trans in that axis before the rotation + * @param prevImageSize the width/height of the image before the rotation + * @param imageSize width/height of the image after rotation + * @param prevViewSize width/height of view before rotation + * @param viewSize width/height of view after rotation + * @param drawableSize width/height of drawable + */ + private void translateMatrixAfterRotate(int axis, float trans, float prevImageSize, float imageSize, int prevViewSize, int viewSize, int drawableSize) { + if (imageSize < viewSize) { + // + // The width/height of image is less than the view's width/height. Center it. + // + m[axis] = (viewSize - (drawableSize * m[Matrix.MSCALE_X])) * 0.5f; + + } else if (trans > 0) { + // + // The image is larger than the view, but was not before rotation. Center it. + // + m[axis] = -((imageSize - viewSize) * 0.5f); + + } else { + // + // Find the area of the image which was previously centered in the view. Determine its distance + // from the left/top side of the view as a fraction of the entire image's width/height. Use that percentage + // to calculate the trans in the new view width/height. + // + float percentage = (Math.abs(trans) + (0.5f * prevViewSize)) / prevImageSize; + m[axis] = -((percentage * imageSize) - (viewSize * 0.5f)); + } + } + + private void setState(State state) { + this.state = state; + } + + @Override + public boolean canScrollHorizontally(int direction) { + matrix.getValues(m); + float x = m[Matrix.MTRANS_X]; + + return (!(getImageWidth() < viewWidth)) && (!(x >= -1) || direction >= 0) && (!(Math.abs(x) + viewWidth + 1 >= getImageWidth()) || direction <= 0); + } + + /** + * Gesture Listener detects a single click or long click and passes that on + * to the view's listener. + * + * @author Ortiz + */ + private class GestureListener extends GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + return performClick(); + } + + @Override + public void onLongPress(MotionEvent e) { + performLongClick(); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (fling != null) { + // + // If a previous fling is still active, it should be cancelled so that two flings + // are not run simultaenously. + // + fling.cancelFling(); + } + fling = new Fling((int) velocityX, (int) velocityY); + compatPostOnAnimation(fling); + return super.onFling(e1, e2, velocityX, velocityY); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + boolean consumed = false; + if (state == State.NONE) { + float targetZoom = (normalizedScale == minScale) ? maxScale : minScale; + DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, e.getX(), e.getY(), false); + compatPostOnAnimation(doubleTap); + consumed = true; + } + return consumed; + } + + @Override + public boolean onDoubleTapEvent(MotionEvent e) { + return false; + } + } + + public interface OnTouchImageViewListener { + void onMove(); + } + + /** + * Responsible for all touch events. Handles the heavy lifting of drag and also sends + * touch events to Scale Detector and Gesture Detector. + * + * @author Ortiz + */ + private class PrivateOnTouchListener implements OnTouchListener { + + // + // Remember last point position for dragging + // + private final PointF last = new PointF(); + + @Override + public boolean onTouch(View v, MotionEvent event) { + mScaleDetector.onTouchEvent(event); + mGestureDetector.onTouchEvent(event); + PointF curr = new PointF(event.getX(), event.getY()); + + if (state == State.NONE || state == State.DRAG || state == State.FLING) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + last.set(curr); + if (fling != null) + fling.cancelFling(); + setState(State.DRAG); + break; + + case MotionEvent.ACTION_MOVE: + if (state == State.DRAG) { + float deltaX = curr.x - last.x; + float deltaY = curr.y - last.y; + float fixTransX = getFixDragTrans(deltaX, viewWidth, getImageWidth()); + float fixTransY = getFixDragTrans(deltaY, viewHeight, getImageHeight()); + matrix.postTranslate(fixTransX, fixTransY); + fixTrans(); + last.set(curr.x, curr.y); + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + setState(State.NONE); + break; + } + } + + setImageMatrix(matrix); + + // + // User-defined OnTouchListener + // + if (userTouchListener != null) { + userTouchListener.onTouch(v, event); + } + + // + // OnTouchImageViewListener is set: TouchImageView dragged by user. + // + + // + // indicate event was handled + // + return true; + } + } + + /** + * ScaleListener detects user two finger scaling and scales image. + * + * @author Ortiz + */ + private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + setState(State.ZOOM); + return true; + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + scaleImage(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY(), true); + + // + // OnTouchImageViewListener is set: TouchImageView pinch zoomed by user. + // + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + super.onScaleEnd(detector); + setState(State.NONE); + boolean animateToZoomBoundary = false; + float targetZoom = normalizedScale; + if (normalizedScale > maxScale) { + targetZoom = maxScale; + animateToZoomBoundary = true; + + } else if (normalizedScale < minScale) { + targetZoom = minScale; + animateToZoomBoundary = true; + } + + if (animateToZoomBoundary) { + DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, viewWidth / 2f, viewHeight / 2f, true); + compatPostOnAnimation(doubleTap); + } + } + } + + private void scaleImage(double deltaScale, float focusX, float focusY, boolean stretchImageToSuper) { + + float lowerScale, upperScale; + if (stretchImageToSuper) { + lowerScale = superMinScale; + upperScale = superMaxScale; + + } else { + lowerScale = minScale; + upperScale = maxScale; + } + + float origScale = normalizedScale; + normalizedScale *= deltaScale; + if (normalizedScale > upperScale) { + normalizedScale = upperScale; + deltaScale = upperScale / origScale; + } else if (normalizedScale < lowerScale) { + normalizedScale = lowerScale; + deltaScale = lowerScale / origScale; + } + + matrix.postScale((float) deltaScale, (float) deltaScale, focusX, focusY); + fixScaleTrans(); + } + + /** + * DoubleTapZoom calls a series of runnables which apply + * an animated zoom in/out graphic to the image. + * + * @author Ortiz + */ + private class DoubleTapZoom implements Runnable { + + private final long startTime; + private static final float ZOOM_TIME = 500; + private final float startZoom; + private final float targetZoom; + private final float bitmapX; + private final float bitmapY; + private final boolean stretchImageToSuper; + private final AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator(); + private final PointF startTouch; + private final PointF endTouch; + + DoubleTapZoom(float targetZoom, float focusX, float focusY, boolean stretchImageToSuper) { + setState(State.ANIMATE_ZOOM); + startTime = System.currentTimeMillis(); + this.startZoom = normalizedScale; + this.targetZoom = targetZoom; + this.stretchImageToSuper = stretchImageToSuper; + PointF bitmapPoint = transformCoordTouchToBitmap(focusX, focusY, false); + this.bitmapX = bitmapPoint.x; + this.bitmapY = bitmapPoint.y; + + // + // Used for translating image during scaling + // + startTouch = transformCoordBitmapToTouch(bitmapX, bitmapY); + endTouch = new PointF(viewWidth / 2f, viewHeight / 2f); + } + + @Override + public void run() { + float t = interpolate(); + double deltaScale = calculateDeltaScale(t); + scaleImage(deltaScale, bitmapX, bitmapY, stretchImageToSuper); + translateImageToCenterTouchPosition(t); + fixScaleTrans(); + setImageMatrix(matrix); + + // + // OnTouchImageViewListener is set: double tap runnable updates listener + // with every frame. + // + + if (t < 1f) { + // + // We haven't finished zooming + // + compatPostOnAnimation(this); + + } else { + // + // Finished zooming + // + setState(State.NONE); + } + } + + /** + * Interpolate between where the image should start and end in order to translate + * the image so that the point that is touched is what ends up centered at the end + * of the zoom. + * + * @param t + */ + private void translateImageToCenterTouchPosition(float t) { + float targetX = startTouch.x + t * (endTouch.x - startTouch.x); + float targetY = startTouch.y + t * (endTouch.y - startTouch.y); + PointF curr = transformCoordBitmapToTouch(bitmapX, bitmapY); + matrix.postTranslate(targetX - curr.x, targetY - curr.y); + } + + /** + * Use interpolator to get t + * + * @return + */ + private float interpolate() { + long currTime = System.currentTimeMillis(); + float elapsed = (currTime - startTime) / ZOOM_TIME; + elapsed = Math.min(1f, elapsed); + return interpolator.getInterpolation(elapsed); + } + + /** + * Interpolate the current targeted zoom and get the delta + * from the current zoom. + * + * @param t + * @return + */ + private double calculateDeltaScale(float t) { + double zoom = startZoom + t * (targetZoom - startZoom); + return zoom / normalizedScale; + } + } + + /** + * This function will transform the coordinates in the touch event to the coordinate + * system of the drawable that the imageview contain + * + * @param x x-coordinate of touch event + * @param y y-coordinate of touch event + * @param clipToBitmap Touch event may occur within view, but outside image content. True, to clip return value + * to the bounds of the bitmap size. + * @return Coordinates of the point touched, in the coordinate system of the original drawable. + */ + private PointF transformCoordTouchToBitmap(float x, float y, boolean clipToBitmap) { + matrix.getValues(m); + float origW = getDrawable().getIntrinsicWidth(); + float origH = getDrawable().getIntrinsicHeight(); + float transX = m[Matrix.MTRANS_X]; + float transY = m[Matrix.MTRANS_Y]; + float finalX = ((x - transX) * origW) / getImageWidth(); + float finalY = ((y - transY) * origH) / getImageHeight(); + + if (clipToBitmap) { + finalX = Math.min(Math.max(finalX, 0), origW); + finalY = Math.min(Math.max(finalY, 0), origH); + } + + return new PointF(finalX, finalY); + } + + /** + * Inverse of transformCoordTouchToBitmap. This function will transform the coordinates in the + * drawable's coordinate system to the view's coordinate system. + * + * @param bx x-coordinate in original bitmap coordinate system + * @param by y-coordinate in original bitmap coordinate system + * @return Coordinates of the point in the view's coordinate system. + */ + private PointF transformCoordBitmapToTouch(float bx, float by) { + matrix.getValues(m); + float origW = getDrawable().getIntrinsicWidth(); + float origH = getDrawable().getIntrinsicHeight(); + float px = bx / origW; + float py = by / origH; + float finalX = m[Matrix.MTRANS_X] + getImageWidth() * px; + float finalY = m[Matrix.MTRANS_Y] + getImageHeight() * py; + return new PointF(finalX, finalY); + } + + /** + * Fling launches sequential runnables which apply + * the fling graphic to the image. The values for the translation + * are interpolated by the Scroller. + * + * @author Ortiz + */ + private class Fling implements Runnable { + + CompatScroller scroller; + int currX, currY; + + Fling(int velocityX, int velocityY) { + setState(State.FLING); + scroller = new CompatScroller(context); + matrix.getValues(m); + + int startX = (int) m[Matrix.MTRANS_X]; + int startY = (int) m[Matrix.MTRANS_Y]; + int minX, maxX, minY, maxY; + + if (getImageWidth() > viewWidth) { + minX = viewWidth - (int) getImageWidth(); + maxX = 0; + + } else { + minX = maxX = startX; + } + + if (getImageHeight() > viewHeight) { + minY = viewHeight - (int) getImageHeight(); + maxY = 0; + + } else { + minY = maxY = startY; + } + + scroller.fling(startX, startY, velocityX, velocityY, minX, + maxX, minY, maxY); + currX = startX; + currY = startY; + } + + public void cancelFling() { + if (scroller != null) { + setState(State.NONE); + scroller.forceFinished(true); + } + } + + @Override + public void run() { + + // + // OnTouchImageViewListener is set: TouchImageView listener has been flung by user. + // Listener runnable updated with each frame of fling animation. + // + + if (scroller.isFinished()) { + scroller = null; + return; + } + + if (scroller.computeScrollOffset()) { + int newX = scroller.getCurrX(); + int newY = scroller.getCurrY(); + int transX = newX - currX; + int transY = newY - currY; + currX = newX; + currY = newY; + matrix.postTranslate(transX, transY); + fixTrans(); + setImageMatrix(matrix); + compatPostOnAnimation(this); + } + } + } + + @TargetApi(Build.VERSION_CODES.GINGERBREAD) + private static class CompatScroller { + Scroller scroller; + final OverScroller overScroller; + final boolean isPreGingerbread; + + public CompatScroller(Context context) { + isPreGingerbread = false; + overScroller = new OverScroller(context); + } + + public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) { + if (isPreGingerbread) { + scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); + } else { + overScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); + } + } + + public void forceFinished(boolean finished) { + if (isPreGingerbread) { + scroller.forceFinished(finished); + } else { + overScroller.forceFinished(finished); + } + } + + public boolean isFinished() { + if (isPreGingerbread) { + return scroller.isFinished(); + } else { + return overScroller.isFinished(); + } + } + + public boolean computeScrollOffset() { + if (isPreGingerbread) { + return scroller.computeScrollOffset(); + } else { + overScroller.computeScrollOffset(); + return overScroller.computeScrollOffset(); + } + } + + public int getCurrX() { + if (isPreGingerbread) { + return scroller.getCurrX(); + } else { + return overScroller.getCurrX(); + } + } + + public int getCurrY() { + if (isPreGingerbread) { + return scroller.getCurrY(); + } else { + return overScroller.getCurrY(); + } + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private void compatPostOnAnimation(Runnable runnable) { + postOnAnimation(runnable); + } + + private static class ZoomVariables { + public final float scale; + public final float focusX; + public final float focusY; + public final ScaleType scaleType; + + public ZoomVariables(float scale, float focusX, float focusY, ScaleType scaleType) { + this.scale = scale; + this.focusX = focusX; + this.focusY = focusY; + this.scaleType = scaleType; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/CheckListViewModel.kt b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/CheckListViewModel.kt new file mode 100644 index 0000000..3383afa --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/CheckListViewModel.kt @@ -0,0 +1,33 @@ +package com.iesoluciones.siodrenax.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.iesoluciones.siodrenax.App +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.repositories.CheckListRepository + +class CheckListViewModel : ViewModel(){ + private val checkListRepository = CheckListRepository() + + var isLoading = MutableLiveData() + var checkListSuccess = MutableLiveData() + var checkListFailure = MutableLiveData() + + fun checkList() { + isLoading.value = App.context!!.resources.getString(R.string.loading_sending_information) + + checkListRepository.finalizarEncuesta( + object : CheckListRepository.CheckListInterface { + override fun success() { + isLoading.value = null + checkListSuccess.value = "" + } + + override fun failure(throwable: Throwable) { + isLoading.value = null + checkListFailure.value = throwable + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/EvidenceViewModel.kt b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/EvidenceViewModel.kt new file mode 100644 index 0000000..edbd808 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/EvidenceViewModel.kt @@ -0,0 +1,31 @@ +package com.iesoluciones.siodrenax.viewmodels + +import android.graphics.Bitmap +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.iesoluciones.siodrenax.App.Companion.shareInstance +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.models.EvidenceSignatureLocal +import com.iesoluciones.siodrenax.utils.HelperUtil +import com.iesoluciones.siodrenax.utils.ImageUtils +import java.io.File + + +class EvidenceViewModel : ViewModel() { + + var isLoading = MutableLiveData() + var onImageSaved = MutableLiveData() + + fun saveImage(filePath: File, bitmap: Bitmap) { + isLoading.value = shareInstance!!.getString(R.string.loading) + val name = System.currentTimeMillis().toString() + ".jpg" + val path: String? = HelperUtil().saveBitmap(filePath, name, bitmap) + + val compressedBitmap = ImageUtils.getInstant().getCompressedBitmap(path) + HelperUtil().saveBitmap(filePath, name, compressedBitmap) + + isLoading.value = null + onImageSaved.value = EvidenceSignatureLocal(path = path, name = name) + //System.gc() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/IncidenceViewModel.kt b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/IncidenceViewModel.kt new file mode 100644 index 0000000..c6e96a0 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/IncidenceViewModel.kt @@ -0,0 +1,74 @@ +package com.iesoluciones.siodrenax.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.iesoluciones.siodrenax.App +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.models.VehicleIncidenceResponse +import com.iesoluciones.siodrenax.repositories.IncidenceRepository + +class IncidenceViewModel : ViewModel() { + + private val incidenceRepository = IncidenceRepository() + + var isLoading = MutableLiveData() + var incidenceSuccess = MutableLiveData() + var incidenceFailure = MutableLiveData() + var getIncidenceSuccess = MutableLiveData() + var getIncidenceFailure = MutableLiveData() + + fun sendVehicleIncidence(incidence: String) { + isLoading.value = App.context!!.resources.getString(R.string.vehicle_incidence_registering_incidence) + + incidenceRepository.sendVehicleIncidenceRequest(incidence, + object : IncidenceRepository.IncidenceInterface { + override fun success(genericResponse: String?) { + isLoading.value = null + incidenceSuccess.value = genericResponse + } + + override fun failure(throwable: Throwable) { + isLoading.value = null + incidenceFailure.value = throwable + } + } + ) + } + + fun getVehicleIncidence(vehicleId: Long) { + isLoading.value = App.context!!.resources.getString(R.string.vehicle_incidence_searching_incidence) + + incidenceRepository.getVehicleIncidenceRequest(vehicleId, + object : IncidenceRepository.GetIncidenceInterface { + override fun success(response: VehicleIncidenceResponse?) { + isLoading.value = null + getIncidenceSuccess.value = response + } + + override fun failure(throwable: Throwable) { + isLoading.value = null + getIncidenceFailure.value = throwable + } + } + ) + } + + fun resolveVehicleIncidence(incidenceId: String) { + isLoading.value = App.context!!.resources.getString(R.string.vehicle_incidence_resolving_incidence) + + incidenceRepository.resolveVehicleIncidenceRequest(incidenceId, + object : IncidenceRepository.IncidenceInterface { + override fun success(genericResponse: String?) { + incidenceSuccess.value = genericResponse + isLoading.value = null + } + + override fun failure(throwable: Throwable) { + isLoading.value = null + incidenceFailure.value = throwable + } + } + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/LoginViewModel.kt b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/LoginViewModel.kt new file mode 100644 index 0000000..7ef0b54 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/LoginViewModel.kt @@ -0,0 +1,34 @@ +package com.iesoluciones.siodrenax.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.iesoluciones.siodrenax.App +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.repositories.LoginRepository + +class LoginViewModel : ViewModel() { + + private val loginRepository = LoginRepository() + + var isLoading = MutableLiveData() + var loginSuccess = MutableLiveData() + var loginFailure = MutableLiveData() + + fun login(username: String, password: String, deviceId: String) { + isLoading.value = App.context!!.resources.getString(R.string.loading_login) + + loginRepository.loginRequest(username, password, deviceId, + object : LoginRepository.LoginInterface { + override fun success(result: Boolean) { + isLoading.value = null + loginSuccess.value = result + } + + override fun failure(throwable: Throwable) { + isLoading.value = null + loginFailure.value = throwable + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/ManagerViewModel.kt b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/ManagerViewModel.kt new file mode 100644 index 0000000..8396563 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/ManagerViewModel.kt @@ -0,0 +1,103 @@ +package com.iesoluciones.siodrenax.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.iesoluciones.siodrenax.App +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.entities.Operator +import com.iesoluciones.siodrenax.entities.Order +import com.iesoluciones.siodrenax.entities.User +import com.iesoluciones.siodrenax.repositories.ManagerRepository + +class ManagerViewModel : ViewModel(){ + + var isLoading = MutableLiveData() + var onOrdersRetrieveSuccess = MutableLiveData>() + var operatorsObservable: MutableLiveData>? = null + var onCallFailure = MutableLiveData() + var onWorkdaySuccess = MutableLiveData() + private var managerRepository: ManagerRepository = ManagerRepository() + private var getUser: MutableLiveData? = null + + fun getOperatorList(): MutableLiveData>? { + if (operatorsObservable == null) { + operatorsObservable = MutableLiveData>() + operatorsObservable!!.postValue(managerRepository.getOperatorsFromDb()) + } + return operatorsObservable + } + + fun getOperatorOrders(idOperator: String) { + isLoading.value = App.shareInstance!!.resources.getString(R.string.loading_wait_a_moment) + managerRepository.getOperatorOrders( + idOperator, + object : ManagerRepository.OnOrdersRetrievedListener { + override fun success(orders: List) { + isLoading.postValue(null) + onOrdersRetrieveSuccess.postValue(orders) + } + + override fun failure(throwable: Throwable) { + isLoading.postValue(null) + onCallFailure.postValue(throwable) + } + }) + } + + fun getOperators() { + isLoading.value = App.shareInstance!!.resources.getString(R.string.loading_wait_a_moment) + managerRepository.getOperatorList( + object : ManagerRepository.OnOperatorsRetrievedListener { + override fun success(operators: List) { + operatorsObservable!!.postValue(operators) + isLoading.postValue(null) + } + + override fun failure(throwable: Throwable) { + onCallFailure.postValue(throwable) + isLoading.postValue(null) + } + }) + } + + fun endWorkload() { + isLoading.value = App.shareInstance!!.resources.getString(R.string.loading_wait_a_moment) + managerRepository.endWorkday( + object : ManagerRepository.OnWebServiceCalled { + override fun success() { + onWorkdaySuccess.postValue(true) + isLoading.postValue(null) + } + + override fun failure(e: Throwable) { + onCallFailure.postValue(e) + isLoading.postValue(null) + } + }) + } + + fun startWorkload() { + isLoading.value = App.shareInstance!!.resources.getString(R.string.loading_wait_a_moment) + managerRepository.startWorkday( + object : ManagerRepository.OnWebServiceCalled { + override fun success() { + onWorkdaySuccess.postValue(true) + isLoading.postValue(null) + } + + override fun failure(e: Throwable) { + onCallFailure.postValue(e) + isLoading.postValue(null) + } + }) + } + + fun getUser(): LiveData? { + if (getUser == null) { + getUser = MutableLiveData() + getUser!!.value = managerRepository.getUserData() + } + return getUser + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/OrderProgressViewModel.kt b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/OrderProgressViewModel.kt new file mode 100644 index 0000000..5c67d6f --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/OrderProgressViewModel.kt @@ -0,0 +1,102 @@ +package com.iesoluciones.siodrenax.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.iesoluciones.siodrenax.App.Companion.context +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.adapters.OrdersAdapter.Companion.inFormatter +import com.iesoluciones.siodrenax.entities.Evidence +import com.iesoluciones.siodrenax.repositories.OrdersRepository +import okhttp3.ResponseBody +import java.lang.Exception +import java.util.* + +class OrderProgressViewModel: ViewModel() { + + private val oneSecond = 1000L + private var ordersRepository = OrdersRepository() + var timerObservable: MutableLiveData? = null + var isLoading = MutableLiveData() + var isAllowedToFinishOrder = MutableLiveData() + var onSendSuccess = MutableLiveData() + var onSendFailure = MutableLiveData() + var onEvidenceRefresh = MutableLiveData>() + + fun getTimer(startDate: String?): LiveData? { + if (timerObservable == null) { + timerObservable = MutableLiveData() + val timer = Timer() + val startTime: Long = try { inFormatter.parse(startDate!!)!!.time } catch (e: Exception) { System.currentTimeMillis() } + // Update the elapsed time every second. + timer.scheduleAtFixedRate( + object : TimerTask() { + override fun run() { + val newValue = System.currentTimeMillis() - startTime + // setValue() cannot be called from a background thread so post to main thread. + timerObservable!!.postValue(newValue) + } + }, + oneSecond, + oneSecond + ) + } + return timerObservable + } + + /** + * Method to be used in OrderProgressActiviy to start an order + * + * @param idOrder id of the order + * @param idRequest id of the request that holds the order + * @param startDate start date in yyyy-MM-dd HH:mm:ss format + * @param startLat start latitude coordenate + * @param startLng start longitude coordenate + */ + fun startOrder( + idOrder: String, + idRequest: String, + startDate: String, + startLat: String, + startLng: String + ) { + isLoading.value = context!!.resources.getString(R.string.loading_sending_information) + ordersRepository.startOrder( + idOrder, + idRequest, + startDate, + startLat, + startLng, + object : OrdersRepository.OnOrderRequestAnswered { + override fun success(orderResponse: ResponseBody) { + isLoading.value = null + onSendSuccess.value = orderResponse + } + + override fun failure(throwable: Throwable) { + isLoading.value = null + onSendFailure.value = throwable + } + }) + } + + fun getEvidences(idOrder: Long) { + onEvidenceRefresh.value = ordersRepository.getEvidences(idOrder) + } + + fun attemptFinishOrder(idOrder: String) { + if (ordersRepository.validateEvidences(idOrder)) + isAllowedToFinishOrder.setValue(true) + else + isAllowedToFinishOrder.setValue(false) + + } + + fun getEvidencesForZip(idOrder: Long): List{ + return ordersRepository.getEvidencesForZip(idOrder) + } + + fun getEvidencesForPdf(idOrder: Long): List{ + return ordersRepository.getEvidencesForPdf(idOrder) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/OrdersViewModel.kt b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/OrdersViewModel.kt new file mode 100644 index 0000000..dd001ba --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/OrdersViewModel.kt @@ -0,0 +1,77 @@ +package com.iesoluciones.siodrenax.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.iesoluciones.siodrenax.entities.NextDayOrder +import com.iesoluciones.siodrenax.entities.Order +import com.iesoluciones.siodrenax.repositories.OrdersRepository + +class OrdersViewModel : ViewModel() { + + private val ordersRepository: OrdersRepository = OrdersRepository() + + private var ordersObservable: MutableLiveData>? = null + private var nextDayOrdersObservable: MutableLiveData>? = null + var orderRefreshFailure = MutableLiveData() + private var showMap = MutableLiveData() + var isLoading = MutableLiveData() + + fun getOrders(): LiveData> { + if (ordersObservable == null) { + ordersObservable = MutableLiveData() + showMap.value = false + ordersObservable!!.value = ordersRepository.getOrders() + } + return ordersObservable!! + } + + fun refreshOrders(filesDir: String) { + ordersRepository.refreshOrders( + object : OrdersRepository.OnRefreshOrders { + override fun success(orders: List) { + ordersObservable!!.value = orders + isLoading.value = null + } + + override fun failure(throwable: Throwable) { + orderRefreshFailure.value = throwable + isLoading.value = null + } + } + , filesDir) + } + + fun updateOrdersFromDB() { + if (ordersObservable != null) ordersObservable!!.value = ordersRepository.getOrders() + } + + fun getNextDayOrders(): LiveData> { + if (nextDayOrdersObservable == null) { + nextDayOrdersObservable = MutableLiveData() + showMap.value = false + nextDayOrdersObservable!!.value = ordersRepository.getNextDayOrders() + } + + return nextDayOrdersObservable!! + } + + fun refreshNextDayOrders() { + ordersRepository.refreshNextDayOrders(object : OrdersRepository.OnRefreshONextDayOrders { + override fun success(nextDayOrders: List) { + nextDayOrdersObservable!!.value = nextDayOrders + isLoading.value = null + } + + override fun failure(throwable: Throwable) { + orderRefreshFailure.value = throwable + isLoading.value = null + } + }) + } + + fun updateNextDayOrdersFromDB() { + if (nextDayOrdersObservable != null) nextDayOrdersObservable!!.value = + ordersRepository.getNextDayOrders() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/SplashViewModel.kt b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/SplashViewModel.kt new file mode 100644 index 0000000..cbc1a94 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/SplashViewModel.kt @@ -0,0 +1,73 @@ +package com.iesoluciones.siodrenax.viewmodels + +import android.os.Handler +import android.os.Looper +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.iesoluciones.siodrenax.preferencesHelper +import com.iesoluciones.siodrenax.repositories.OrdersRepository +import com.iesoluciones.siodrenax.utils.Constants.Companion.LOGGED_IN +import com.iesoluciones.siodrenax.utils.Constants.Companion.LOGGED_IN_MANAGER +import com.iesoluciones.siodrenax.utils.Constants.Companion.NO_SESSION +import com.iesoluciones.siodrenax.utils.Constants.Companion.ORDER_IN_PROGRESS +import com.iesoluciones.siodrenax.utils.Constants.Companion.WORKDAY_STARTED +import com.iesoluciones.siodrenax.utils.Constants.Companion.WORKDAY_STARTED_MANAGER + +class SplashViewModel : ViewModel() { + + private val ordersRepository: OrdersRepository = OrdersRepository() + var sessionStatusObservable: MutableLiveData = MutableLiveData() + private var startSplashObservable: MutableLiveData? = null + + private fun validateSession() { + + val isLogged = preferencesHelper.tokenApi != null + val workdayStarted: Boolean = preferencesHelper.workdayStarted + val isOrderInProgress = preferencesHelper.orderInProgress != 0L + val isManager: Boolean = preferencesHelper.isManager + val orderId: Long = preferencesHelper.orderInProgress!! + + Handler(Looper.getMainLooper()).postDelayed({ + + if (!isLogged) sessionStatusObservable.postValue(NO_SESSION) + + if (isLogged && !workdayStarted && !isManager) sessionStatusObservable.postValue( + LOGGED_IN + ) + + if (isLogged && !workdayStarted && isManager) sessionStatusObservable.postValue( + LOGGED_IN_MANAGER + ) + + if (workdayStarted && !isOrderInProgress && !isManager) sessionStatusObservable.postValue( + WORKDAY_STARTED + ) + + if (workdayStarted && !isOrderInProgress && isManager) sessionStatusObservable.postValue( + WORKDAY_STARTED_MANAGER + ) + + if (workdayStarted && isOrderInProgress) { + if (ordersRepository.getOrderById(orderId) == null) { + preferencesHelper.orderInProgress = null + sessionStatusObservable.postValue(WORKDAY_STARTED) + } else { + sessionStatusObservable.postValue(ORDER_IN_PROGRESS) + } + } + + }, 1000) + } + + fun getStartSplashObservable(): MutableLiveData { + + return if (startSplashObservable == null) { + startSplashObservable = MutableLiveData() + validateSession() + startSplashObservable as MutableLiveData + } else { + startSplashObservable as MutableLiveData + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/WorkdayStatusViewModel.kt b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/WorkdayStatusViewModel.kt new file mode 100644 index 0000000..501e0b2 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/viewmodels/WorkdayStatusViewModel.kt @@ -0,0 +1,110 @@ +package com.iesoluciones.siodrenax.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.iesoluciones.siodrenax.App +import com.iesoluciones.siodrenax.R +import com.iesoluciones.siodrenax.models.StartWorkdayResponse +import com.iesoluciones.siodrenax.repositories.WorkdayRepository +import okhttp3.ResponseBody + +class WorkdayStatusViewModel : ViewModel() { + private val workdayRepository: WorkdayRepository = WorkdayRepository() + + var isLoading = MutableLiveData() + + var startWorkdaySuccess = MutableLiveData() + var startWorkdayFailure = MutableLiveData() + + var endWorkdaySuccess = MutableLiveData() + var endWorkdayFailure = MutableLiveData() + + var isAllowedToLogOut = MutableLiveData() + var isAllowedToChangeVehicle = MutableLiveData() + + var changeVehicleSuccess = MutableLiveData() + var changeVehicleFailure = MutableLiveData() + + var logOutSuccessObservable = MutableLiveData() + + fun startWorkday(firebaseToken: String, odometer: String, lat: String, lng: String, filesDir: String) { + isLoading.value = App.context!!.resources.getString(R.string.loading_sending_information) + workdayRepository.startWorkdayRequest( + firebaseToken, + odometer, + lat, + lng, + filesDir, + object : WorkdayRepository.WorkdayInterface { + override fun success(response: StartWorkdayResponse) { + isLoading.value = null + startWorkdaySuccess.value = response + } + + override fun failure(throwable: Throwable) { + isLoading.value = null + startWorkdayFailure.value = throwable + } + }) + } + + + fun endWorkday(odometer: String, lat: String, lng: String) { + isLoading.value = App.context!!.resources.getString(R.string.loading_sending_information) + workdayRepository.endWorkdayRequest( + odometer, + lat, + lng, + object : WorkdayRepository.WorkdayEndInterface { + override fun success(response: ResponseBody) { + isLoading.value = null + endWorkdaySuccess.value = response + } + + override fun failure(throwable: Throwable) { + isLoading.value = null + endWorkdayFailure.value = throwable + } + }) + } + + fun validateChangeVehicle() { + if (workdayRepository.validateChangeVehicle()) { + isAllowedToChangeVehicle.setValue(true) + } else { + isAllowedToChangeVehicle.setValue(false) + } + } + + fun changeVehicle(odometer: String, lat: String, lng: String) { + isLoading.value = App.context!!.resources.getString(R.string.loading_sending_information) + workdayRepository.changeVehicle( + odometer, + lat, + lng, + object : WorkdayRepository.WorkdayEndInterface { + override fun success(response: ResponseBody) { + isLoading.value = null + changeVehicleSuccess.value = response + } + + override fun failure(throwable: Throwable) { + isLoading.value = null + changeVehicleFailure.value = throwable + } + }) + } + + fun logout() { + workdayRepository.logout() + logOutSuccessObservable.value = true + } + + fun validateLogOut() { + if (workdayRepository.validateLogOut()) { + isAllowedToLogOut.setValue(true) + } else { + isAllowedToLogOut.setValue(false) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/iesoluciones/siodrenax/workers/SyncWorker.kt b/app/src/main/java/com/iesoluciones/siodrenax/workers/SyncWorker.kt new file mode 100644 index 0000000..8de3456 --- /dev/null +++ b/app/src/main/java/com/iesoluciones/siodrenax/workers/SyncWorker.kt @@ -0,0 +1,74 @@ +package com.iesoluciones.siodrenax.workers + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.iesoluciones.siodrenax.entities.OrderProgress +import com.iesoluciones.siodrenax.preferencesHelper +import com.iesoluciones.siodrenax.repositories.OrdersRepository +import io.reactivex.observers.ResourceObserver +import io.reactivex.schedulers.Schedulers +import okhttp3.ResponseBody +import java.util.concurrent.LinkedBlockingQueue + +class SyncWorker(context: Context, workerParameters: WorkerParameters) : Worker( + context, + workerParameters +) { + override fun doWork(): Result { + + val result = LinkedBlockingQueue() + val ordersRepository = OrdersRepository() + val pendingOrders: List = ordersRepository.getPendingOrders() + + return if (pendingOrders.isNotEmpty() && preferencesHelper.isEnableToSync) { + + preferencesHelper.isEnableToSync = false + + for (o in pendingOrders) { + + val order = ordersRepository.getOrderByIdRequest(o.idRequest) + + if (order?.pdfPath != null) { + + OrdersRepository().prepareFinishOrder(o).subscribeOn(Schedulers.newThread()) + .observeOn(Schedulers.newThread()) + .subscribe(object : ResourceObserver() { + override fun onNext(responseBody: ResponseBody) { + try { + preferencesHelper.isEnableToSync = true + result.put(Result.success()) + } catch (e: InterruptedException) { + e.printStackTrace() + } + } + + override fun onError(e: Throwable) { + try { + preferencesHelper.isEnableToSync = true + result.put(Result.success()) + } catch (e1: InterruptedException) { + e1.printStackTrace() + } + } + + override fun onComplete() { + preferencesHelper.isEnableToSync = true + } + }) + } + } + + try { + result.take() + } catch (e: InterruptedException) { + preferencesHelper.isEnableToSync = true + e.printStackTrace() + Result.success() + } + + } else { + Result.success() + } + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/scale_out_airbnb.xml b/app/src/main/res/anim/scale_out_airbnb.xml new file mode 100644 index 0000000..c50aedd --- /dev/null +++ b/app/src/main/res/anim/scale_out_airbnb.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_rigth_airbnb.xml b/app/src/main/res/anim/slide_in_rigth_airbnb.xml new file mode 100644 index 0000000..10aa9ff --- /dev/null +++ b/app/src/main/res/anim/slide_in_rigth_airbnb.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_right_airbnb.xml b/app/src/main/res/anim/slide_out_right_airbnb.xml new file mode 100644 index 0000000..b031b5e --- /dev/null +++ b/app/src/main/res/anim/slide_out_right_airbnb.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/chevron_right.png b/app/src/main/res/drawable-hdpi/chevron_right.png new file mode 100644 index 0000000000000000000000000000000000000000..5d04645c0ebc697aa7f64e6c2db016f621a3750f GIT binary patch literal 350 zcmV-k0iphhP);LWQ_mB8uqA|W4REEEMhxqBc13D{kn2xr4Ux-4eT z>^VXxDJdy^sdQZ*p?=swG))txX--|&U7g?nATh7xrBSV_z7C^CU@p)B{COXUZv-@O%T*P + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/drenax_logo.png b/app/src/main/res/drawable-hdpi/drenax_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b9ba15c7005f238faed0a78476188f48e6e96418 GIT binary patch literal 20629 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 z z!U#-Lt7oYw1+2CxAMI|E|26ZAqQ;F#nw4G7MNAqj~e(lwH^TtW} z-d#5jM>L#m&uq9=OWXi5WjS3Q?geC2#!bKNSV(X(Fn{v*En#eA*obDCcrMmh$Gz=% z>$OELUD`}%!_WnQ=mcY^bE8bMF~IjboszAM@H1^#thTP+>w9fK+I^e9tqe&qCnIiD zZ~|aqF&RCV-$qS+^)Ix9Yq38gIXN_$sF3_kUxG~1c&w6nKpYSN literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_map_outline_white.png b/app/src/main/res/drawable-hdpi/ic_map_outline_white.png new file mode 100644 index 0000000000000000000000000000000000000000..1c1ec02ffbb417709a81e91a08cc035927758c45 GIT binary patch literal 1444 zcmV;V1zY-wP)szI1d$|Ngw~B+^h1d@@2D6kp074O>7W-fzHA(j^STMEDHfKP!x@c-6SNm8~nBbW|2zZXGlD;A4i z0%w6dPynWYOTcMhAKJW#ws)aVJNh=o98Z&CxgUPsgulbUATSOT@vP5DGL1zD)3yPD zT#G<%g3~vEBO+iJm?-+&xQP1;eZEKk=P}Op7_XJHuCZscoLex?VvP43#{B`~p8-aI zD_-Y#mrH~oE(tCT1keE=w*XH72bC$K%927cycHDuFvd9zd;~lQEQ60W_~M>b6C$2Y z^jQh)08VLwru>}7Mvyy(mQOnZxf^4@1RtLR{lL)l*(uYQS?7Ucz_ch-A z3cu^|>^i)=6K!7bEa%ZMPGTX5{d;?RGd(>$ix9{*jClxSpVHD8FD7f8{mUZj1blyn z7}j!GOG}Axh76fZU$1u7zmpE3cSj@ z53;TcOLJ+dzI?`ax|^&bS=v}Hm6H{oc(@Op!I*ow;g=#QErOUSzv1-<_}DePckuG1 z}HuN$1&xJNVhxhL`JYwas}5g&z=bHLBM_h@71DcBJdLy`my;qhY#WD$aBEp;QF ziJ%hg8+0<=p_A!j;3RM{fE?~czo7j)zy@Y6Z_b|6*Qp3We1v>7lBsEMQwxR^hez199QO!1&Kk^5^CFW|1A2sIp)@ z#PS(X+2>p6e>28Or#@B$^?B|rgF0EZBanx9IgGpZQ}-E%OH_*ceX33KRvhJZa(#t1 zuc7S&ilrq;H=P7QRJ?CxAuMaQfP1)q@TThBTN3i^t!}&xcuIxA%#>$=6)YT9=|O#_ yQl& literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/rounded_background_accent.xml b/app/src/main/res/drawable-hdpi/rounded_background_accent.xml new file mode 100644 index 0000000..a375ffa --- /dev/null +++ b/app/src/main/res/drawable-hdpi/rounded_background_accent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/torch.png b/app/src/main/res/drawable-hdpi/torch.png new file mode 100644 index 0000000000000000000000000000000000000000..620addd5f2bc8ce05d6c566935405f5122c88d91 GIT binary patch literal 438 zcmV;n0ZIOeP)FA?+&^cDIR5~T4V^aZ?lN*~8l4>_1a+iP3urTG0I3sNwo z+0CEQ3=D)Y^UZH}huM(9;MmL<(;b!oNs^oeL10`>46&R=3G{_698Cd@Tg+B4`f;pHeMzKCG=YK`1CrS*%QSD;vTw>+->_Aj7 z2qR)1yv&^vf-Y51JzFpcb!;z+ZAws_7q+&^-P($n*R}*{H{Rtu&;JOD+Yq#o5E}SO z&hs1Z*1e#;Bb!S|E@sj8ky%^kzUzvhed`7Ff_gzGDX6*2m2aJA3Z?~Lj43fuBZ91` g5`SQt5p#0(3o-yD^a8G$RsaA107*qoM6N<$f_o;wbpQYW literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/torch_off.png b/app/src/main/res/drawable-hdpi/torch_off.png new file mode 100644 index 0000000000000000000000000000000000000000..b89e27a8465e692bff2e0e951c7698ff593275b8 GIT binary patch literal 633 zcmV-<0*3vGP) z3VT2Uxq0-@BuH`oDsW?dVh=sFHY^`K#c@IE68GOmka@>*r@%}5jz3QF(Nk{Z!@ju& zPF`8=@hRUU2yK{SnkrImyemJj1oB7vHNXF3ovqvQjrlqrre zc<#XtaJ?)YcYMg#9T7Zl@LR>bc;E#a<-uYnA_Ch5&pgI2=%r8yaS`Sw6Cum#BPon$ z&|eB%G6zDI(>F-SVb$0qRJS4YBOmuvm_@%6p}x6vi3%ah>5B@X@Z^&q6pnlngg!%& z&m|#}F+{l=tb8sBy}*u{*M@VSa`{{m>M?jCNM`Ix7p0z(AXMkGzmzT&Wrdprp`$@^ z9V%U(#aKcaA%L^yF&y5U4LoDX_QF$TlEH6AY4OH|Tzr9v+r5m6ZwK0c^V+@u#r5HH T*IXI?00000NkvXXu0mjf!RQ>A literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/capture.xml b/app/src/main/res/drawable-mdpi/capture.xml new file mode 100644 index 0000000..2644e39 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/capture.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-mdpi/capture_active.xml b/app/src/main/res/drawable-mdpi/capture_active.xml new file mode 100644 index 0000000..360705c --- /dev/null +++ b/app/src/main/res/drawable-mdpi/capture_active.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-mdpi/capture_inactive.xml b/app/src/main/res/drawable-mdpi/capture_inactive.xml new file mode 100644 index 0000000..05adcfd --- /dev/null +++ b/app/src/main/res/drawable-mdpi/capture_inactive.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-mdpi/chevron_right.png b/app/src/main/res/drawable-mdpi/chevron_right.png new file mode 100644 index 0000000000000000000000000000000000000000..f6a14d55796ad1c8bbb85c4cdec5d2d3b8fe99ed GIT binary patch literal 252 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 PHGr(7l0>bzaq#~EqX~75 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_map_outline_white.png b/app/src/main/res/drawable-mdpi/ic_map_outline_white.png new file mode 100644 index 0000000000000000000000000000000000000000..7bce5b8b7fdf96816af5763af4675988b41ea528 GIT binary patch literal 911 zcmV;A191F_P)Yyf-sp&TVON;ljo04bSsh zK_}=jbbw}Yw-{eq+|5q9Bb&{p^ZESFVzD>|UV=$*hQ2|@HZrF@i5S+jvAzqQt?-=( zuW9ER5)nwc2eK@rXTe0ZTAc=S;D|S|;hZP%^Xdd;m8|_5exuN)YWM^gS~EavVWj0u+#Y zta)QeFGO%wEU9AD#g-8J9egcXh{L$TQEo_pCPyS!u?v1==mEQTe0X?xC^%1T`gSvR zn&ykZGqPC2N%@=d4qOk4Xoi4_jv9=RTpuUYZEqPbEWrC6{##1-_C=6q_;bu{Qnm;G zq5rIaOcZZo0cu0qAAU8)2?+mBuyzy4?O}L4B%(~;CC0Yft2b2(Zz2Iv1od2q?zK@n zOJARaCguHW0#=gO32*|O04Kl+Z~~kFC%_4C0-OLRV08)5PkjzXd?IG_`S{NYFz>RP z5AgxIFPLrO$2a<}GPYMA@@D+5sdHoG2KKLnDJ%Fl5WY6*fmxXT6%31w z%xxD;dIFp?w=V5hed;Qfn1F)a$8+ZM--h&{c3Q=Vu$vaakpdg+!5m@zV|kzv1rhDl zE4|S|Nw+pqkx@}2r}B=>tZ(Nd604YeXra0PBckV}nt6;T`bh>|?dcSZ3-b6vuPePJ-N!wNydjHV5Y$>}Tlr=||B)-ELHIyf092ap@$8gHQ^+p>$~Q|Ca>x zFuB}$_CW|q-h02~@m>-k{#&G!B1sYvMUnIs>e7a`PRB-Quz?&rg}St{I>(|&)ARv- zcozVjE2W0eo5gY5F(E?hMjis29&7?@h`4h&p8)cOen|+FLx9KF;I`lZd)k`QyTa6g z;@m(uMtkO|FqbL2Y@yS~hJwNjvOu03kPK^Y&}n+7X$i<5!n5XD=~{3=rf|B`vH!sM z6`^^oRB+cIaMsd=b;_>F&U283jyG9tlOK0i&Lwbw{AU|i}YBPlkQ`7Tqk{Y0L zkg)LnsQ|cu)DfX;fD6dI0QEr}0Lu5x5C`BF(<}6!H$iLw&f(S_-qrXA9`LPXpLHGql_E@KwgB@Bpo^y00000NkvXXu0mjf D-VD9V literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/chevron_right.png b/app/src/main/res/drawable-xhdpi/chevron_right.png new file mode 100644 index 0000000000000000000000000000000000000000..fa9447d9c1f07b9d0f3fc7d842457d43df79ba57 GIT binary patch literal 325 zcmV-L0lNN)P)LjyXyB`R;DN() z@QeR{!ytqZLI~*xmSs(3RLVP!GmD~VhdJPRu2J5vs;b7^<2XL4iI61&`HjdIMe&d+ z%TgkaDk2atK)?dxmpsoOG7_QcP1v?Q41&Og{J9;`ya`4y;?oGox)9eJ0m+|=xTuau zl0<(*mSro5U%USa*>?1P?<4_fnyw(;4X6>|^*Z3)5^Cgq*+WsD@|36iSL8=(aY`1U$ds*!Ikp(%v-&7oeGd?#Xy<>aZ8Z{JYho6vc&MF=5;kbmR} XSD(Y@Li5MT00000NkvXXu0mjf0;rG9 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/drenax_logo.png b/app/src/main/res/drawable-xhdpi/drenax_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1c8ac7994168b343248f04be16e3bbe76c2b27e3 GIT binary patch literal 29088 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 H6P^ASPR0>% literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/torch.png b/app/src/main/res/drawable-xhdpi/torch.png new file mode 100644 index 0000000000000000000000000000000000000000..305fb59ad95556a54e3ef281984645e78125ac3a GIT binary patch literal 428 zcmV;d0aN~oP)b;@5Htl<8W-16;|h|$-ETJ5`yufZB1Qm1u_M99_6AwW01;6bD9^M1{lN{2U5-aop zq8SL!9VUM!Cb5&W6d)R5%QqmtbddaoSj0@(6AiIq-#ogzIR1%6%%qJSk36j04-%+b zBwqt#C2b#`7<$^$&@}EuZd{R}(;By9L#H?HGeaYx-3yo2?k&dsV;6=E+J=54-(*sg zPhRRaTRwTI2Oas~;sW9=8(WRz@7vmf04BV~Ut>OaUp|VkAb>x|Fe(k6MG+^f0tR?5 ztO;PbEMo~V`L(qYx{El)r}ksi1-Jkg-~wEL3vdB0pr-;%5B2}zvr3ah`cy$4ZQ=%z Wbb@rfR<&;c0000c z-@2h7`1m}5_ZK8m2Wf~gAJ?MkgD?aGch4zs{}IA(;0A_}30wh)8yNbL^U`_|3tXQN zFtA;aHiS3c1|lza60f{`LS%BM!8CUgue>ZFCMCJkRvOr2@0FJ$L=%P`!mzI*h?&41 zsR2k0TgfRkO%Tjt2aE|#{UTGXY>J6>O%5n4_7hWOR~LZ7M{HdHbVF3=7wLqk*znQ^ zVbQVE1!388)C1wrGt~j%-~k;FmND>(@v8#D0)V$4dN-*?+^e0002ovPDHLkV1gj! B7X|+P)bUQB@!?x8W7 z8dG%BCZoe%qBqTE-<-N+w_VizW|LttC@jjvDHRt$v?P1#;_sO5jFS<#oflg>sSJ#}%eb6AQi43(A?nOLT7>RJ$> zv1InyWy*s7DWdj{miND}KV(qTTd^l0JxtJgp0MYxYg_ibowq+?c4*U*;H4$mase}I x`6dhNp4?-!`{K$^yL8&#DxZ^4g9SK4Ur&DQ-t!E#Qoou&VxF#kF6*2UngGqq=}-Uw literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/drenax_logo.png b/app/src/main/res/drawable-xxhdpi/drenax_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5855a6b0c167ab48af45dc7145138fca22514c35 GIT binary patch literal 46641 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 YCumcjQjy{TM>RE*wx&L;T*E%{PZVLMv;Y7A literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/torch.png b/app/src/main/res/drawable-xxhdpi/torch.png new file mode 100644 index 0000000000000000000000000000000000000000..ab90092eba29b5d87a65ef679f3843c52ff83d74 GIT binary patch literal 628 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2V6ygfaSW-r^>)^IFQ!0|V{2Qt z3f$@8Ul4R-(+as|t`EAO99piSQN<{Ku#BF@#nF9%;G66 z)%$*)-T8c8b()dn?6Ya!J6!`)d{4KSNcH+G4eAW(iCW>wCd9fmQD4bT_sSBHbAqjV zyCt@|c)h;Yz;mKu&XT=nv-3X}wJz#eHk0ENw`)M0ZquiTZ{`vIY#T+cG*8RU3c7W) zO!1YBw1@O#-GjV6`{Y-)&bo4~hR=UJTjOCCM&6Z;Q6m25tt*P$1#TXx@MYkuwJT^} zE_QMo`@B1LEODE{ycgLm$0pHU_Eo$@a%jG(3ANS%1~gOTx8BJLa`?K!?nsgV$mo)E2jxdD^;M z-OqmH^wOKv&+=omAMbButi4x!u|bSk%J6wh@`>w0+7+E=qwQ5*7I|E0`TRuXZ}DvT zmfX5Gx%XNZF@kKtg)Xdm_P%+Vd;fle-`pxkcjlPQ_PuGx@cGO;Y58k2tAVMB!PC{x JWt~$(69C5W8G!%* literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/torch_off.png b/app/src/main/res/drawable-xxhdpi/torch_off.png new file mode 100644 index 0000000000000000000000000000000000000000..2281a3cdb02f6be3a25e1d6a02870d75fe56ae8c GIT binary patch literal 890 zcmV-=1BLvFP)q7?KzbR;a&|KE6o zxh`VPXZU=89{U3N9r_u^4{?6dg+!6|8>JOJ2cW!!{%E834$ePK1F7xW zA?S3;9yDRpgD~&~&}vC}&RN+}o|Hznp=Psr71zFj?v=CzeT981Y4+&^Xc&_$0*sH) z;}noSb)p;2)$rW{e$#~hwj97{E7#~7xQ9X093IC+_b}m?4#sq(8_u1>cen7HP3Rws z03E@DmNEaRt%Z&tJyswccSAP?(!2u6h9~`nt~0{Jdj@PqgO(?4_dqv-bkh>unE%|t zoFip~%rW6~`vq#Pmih!Qm~OOX3joborItf`@U3t1!CpPuj$ovlx3Ik|KiW z#)bJV#vGRTtW!4Keq!BO6ZL9U5ZziC^L67Hv!isgn=v~^H)hOlt!K;<=mxLYfbQo! zt#9MEw?!_@Ch5kt1Ugmll~2vtGIT$5bGg)Qjc$2wCdHMNOQKur&E#w7uIRFIiP5d5 z@}yJIOS~BS!Ky9A%NIiL<^q5S(qbGR@kh;;!IpyiRWHKS$xF&fCP{I0r9^Dd_ zlo;Kr3Q!*1YKU&v80D%AP$u2H1g8gp3MG?O2Pl(n0U(XfZoUIl4Z3M))AO5dn)(?1 zrJKe^Pe19Vxf9kux*2E{_KR+Yda3=Po55ytKj>z-8{d7pS=g@NHr+G>wBKzZph^H8 zVwflR{9vl?<}^SYBT15(e(P>t1EdOwJ=fhT0g!qHYrM?uG(ZZZF)!$53ZOPma+_`@ z0XjpsP8)Ez-|c?Z&PlHUDnPdbT>6|_y($Y(2Hg&!D>#2iY*E%0Kq+)PfWE@`(T!O^)hx|~)z3t1_WFX=ecx;!e zu=vCCB5mv1BPVa$WxTfYjB23Vtl~*|59$KkN@Rgrz~DfWm!`9|xay-v_v4RqG=A*e z(DcJC!m&oVCiqeRP;)ZPE%k@CJD9U!Luz8_Fxf6tuH=lrWa zD$CgXJQ($0@sER9Yt=tKUcY$rkB`>_-#2r^9QkJ!`=c`pnce25D}#hQUHx3vIVCg! E0Fgv;;{X5v literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_map_outline_white.png b/app/src/main/res/drawable-xxxhdpi/ic_map_outline_white.png new file mode 100644 index 0000000000000000000000000000000000000000..48309faca3eb3f43d9499774a61f15cb71832b2a GIT binary patch literal 2091 zcmcIldpOhkAOB8vlz5CYVY<*$499IQOQE^nQ|3~+tVmk!$0T8Pay=(X9G1w?<~T_r zGptyrB9Z%Y9cg59%q7d#FlYWd&pCgbe}2Eu^L*Z)=Xrfz@6Y@7dj0eH`Sd9#d0Ch& z008nX&h}oCld-dReinrhXH%^Id1E7f1m$EdbKfg zvaEZu9$oAbmM>VMFTa>p=yfdUN}Av7>q$yja<7n#{?7$5>bQ4}dk^Q?s(0Y-`53Kz zb?W5WK?^JjmVbj1-^@m4n{%sio!*z-TeWz^ee6;4R#f3BSI(-63f3+9wB?3QtO7+zFwxab%?E|%F6bX;HyjhD zUY)u3_Q*(>_%N1+Yv~Ag$5d8*C3%r@B85+c&&m*>%OVDVU%jeF8u#UZn?PqlQvdSvdJRAAfW zjDh}(DbKkS&V1N)PL1)W-jOmvDG+sqJ}IAacs7=EQdeCxqBVUT;~OuY@T>IKRLvt+ zka$7CY9i?(pRd=^EcSS3s^XwaSPF;M64tEJL)#AZ*%BjY$=^b%weM3aNIvHLS3UJR z=%kE6TAAtN@=$KzSTvc~6CN?jEUu|~7v;0%pM^Vrqo}Ij-5)Eodkg+Sng+GN2K#B+vB}>Hlk_}RO#m9gNa#sHdxgWrpD6&P zV%wCe6h*A)UM3dD2p7j6mx)Vq_Ac+SKNOF)E zlSB#m2lOwmSR<6eRtBG1Y>6cA8uXI5w^1+8Dlz00;Ee{>d>7(E&q^LdLB8W?R=Ji? z4sxlhud;DA(R|3pKv;kKC zdI^7xVHMXzsQ^UOqGMTQQaK?6G4>(O7iK>8I>>tellXJY5dzn#q&stJV(usyZry=E zLhrcatFh3@7i+zl7Oh%Y4MwK$Iyuiim_xBDNtl#y6jmuSY8mu~-AncOXWZtJ_JpFaq@VJA0#e*9M?BiU~@H;LJp)C}93 zr~zT~E}PH`E4zj)CKP5O5N}vz?I!%?CY-7A7ss0&3NV_$dS;C@6BN5xFE<~d4jg4|tP~gWUYzQ)vs4-0Vh}Hd`M1Aj!8beJiT8ARxr?QyiAI zK#eW(S$1}MFWrU*Yqj@`u%{kSk+^wsZR}#_PYKf#AtctatAHHhb)xq_|5X4$z1`f^ XdI9vK6Z89x9lDFdDf=h37jFIqVD`X3 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/torch.png b/app/src/main/res/drawable-xxxhdpi/torch.png new file mode 100644 index 0000000000000000000000000000000000000000..f92ae533b4849779cf3fd353705bb404ec8cf21a GIT binary patch literal 407 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V2ty0aSW-r_4byp*I@^Swuh0g z7EOJ{YvbSIwDP*=c@h1`$9nfR>y{pYHj3MWB>h#dY^T>X?hcb zRdY_oY?&ujZQCz6zH@$?YIEW%Z+>F)oz`imawfE2h?3+j`g?fxPlfLbuNp}_DQh#o z?D(!}67Q!6&o1*8?LIvFq{8<+o4X3q_l^J!QuX|m-0^p_F5EFd#aoY#Zzj^^_LZ2kXL_6qwW$VQ{HcO)_}urM?*FbFU(Fflkx0*24FH{vfslaAe4 RBVqzl?dj_0vd$@?2>=0&pmhKM literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/torch_off.png b/app/src/main/res/drawable-xxxhdpi/torch_off.png new file mode 100644 index 0000000000000000000000000000000000000000..3caf965f55f10d6b0bb3c08f4d1867b3f22abbee GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V0`ZB;uunK>+KzDzrzk9tq&W! zN;mLJWvy?T&Z2WDTVaZg*u%dYxL86|dIGt`1?M?_JF~O=pYmP3pf#V5?%SB8%HqVK z*dpM>5ouxlm=S0!0@QKu?f5M7e8%oMpPt4a`+2Y4BF<4G`rp+he`GmQPw%%2u;XYw zwZC)o+?%1E93M^73-9s1ec;HLFu7ubekaHCLOY%*|3#mNFh0;Y=lsaA+Df=;>FI!M zj{HKuD`k$}9CKeeBMv*U^@%NZTZ9~0>btK?IEhD{k>X%s5#ZoZc%qXw zYkiNy>#2endyJTJ!`&9VtZcdR=XGRA)I5D1^_%HTy!zY|dL0g*W=@Ii%cziLcsGNU z!9jq5!I6Q1g^@vkg%nQW@`+`FTvLAUc*6ge;Yqvu<7}ok_nBK}F(_~_G$=4IG%+x6 zFhMy6jvjj>tCy + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_ripple_bg.xml b/app/src/main/res/drawable/button_ripple_bg.xml new file mode 100644 index 0000000..9b4b0bd --- /dev/null +++ b/app/src/main/res/drawable/button_ripple_bg.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_border_accent.xml b/app/src/main/res/drawable/circle_border_accent.xml new file mode 100644 index 0000000..128d168 --- /dev/null +++ b/app/src/main/res/drawable/circle_border_accent.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_border_primary.xml b/app/src/main/res/drawable/circle_border_primary.xml new file mode 100644 index 0000000..8bebc3b --- /dev/null +++ b/app/src/main/res/drawable/circle_border_primary.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_camera_black.xml b/app/src/main/res/drawable/ic_camera_black.xml new file mode 100644 index 0000000..13186de --- /dev/null +++ b/app/src/main/res/drawable/ic_camera_black.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..071825e --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_location_on_blue_24dp.xml b/app/src/main/res/drawable/ic_location_on_blue_24dp.xml new file mode 100644 index 0000000..541f734 --- /dev/null +++ b/app/src/main/res/drawable/ic_location_on_blue_24dp.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_pdf.xml b/app/src/main/res/drawable/ic_pdf.xml new file mode 100644 index 0000000..e5cdaae --- /dev/null +++ b/app/src/main/res/drawable/ic_pdf.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout-land/activity_workday.xml b/app/src/main/res/layout-land/activity_workday.xml new file mode 100644 index 0000000..676949a --- /dev/null +++ b/app/src/main/res/layout-land/activity_workday.xml @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_camera.xml b/app/src/main/res/layout/activity_camera.xml new file mode 100644 index 0000000..d84003c --- /dev/null +++ b/app/src/main/res/layout/activity_camera.xml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_confirmation.xml b/app/src/main/res/layout/activity_confirmation.xml new file mode 100644 index 0000000..61ef71d --- /dev/null +++ b/app/src/main/res/layout/activity_confirmation.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_herramienta_survey.xml b/app/src/main/res/layout/activity_herramienta_survey.xml new file mode 100644 index 0000000..7224e37 --- /dev/null +++ b/app/src/main/res/layout/activity_herramienta_survey.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..d08e664 --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + +